diff --git a/bin/knowledge-mcp.js b/bin/knowledge-mcp.js index 3db7645..aea543d 100755 --- a/bin/knowledge-mcp.js +++ b/bin/knowledge-mcp.js @@ -16360,7 +16360,7 @@ function resolveKnowledgeModeSelection(env = process.env) { normalized = normalizeStorageMode3(value); } catch (error51) { const message = error51 instanceof Error ? error51.message : String(error51); - throw new Error(`knowledge: ${name}=${value} is not a valid mode. ${message}`); + throw new Error(`knowledge: ${name}=${value} is not a valid mode. ${message} ` + `Unset ${name} to use the default sqlite backend, or set ${name}=sqlite or ${name}=postgres.`); } const warnings = []; if (name !== canonicalModeKey) { diff --git a/bin/knowledge.js b/bin/knowledge.js index d449fcf..e01fe68 100755 --- a/bin/knowledge.js +++ b/bin/knowledge.js @@ -96,7 +96,7 @@ Expecting one of '${D.join("', '")}'`);let U=`${_}Help`;return this.on(U,(g)=>{l `,{mode:384}),Tz(_,384)}function Gz(_){let $=JSON.parse(ZD(_,"utf8"));if(!$||typeof $!=="object"||!Array.isArray($.items))return{store:{items:[]},skippedInvalid:0};let D={items:[]},U=0;for(let g of $.items)if(g&&typeof g==="object"&&typeof g.id==="string"&&g.id.length>0)D.items.push(g);else U+=1;return{store:D,skippedInvalid:U}}function uN(_={}){if(_.dryRun===!0)return Yz(_);return v$(kU(),()=>Yz(_),{createParent:!0})}function Yz(_={}){let $=_.dryRun===!0,D=_.now??new Date,U=g$(bU()),g=FD(),I=U.jsonStorePath,j=U0(g),N=U0(I),O={ok:!0,dry_run:$,legacy_path:g,canonical_path:I,legacy_exists:j,canonical_existed:N,canonical_created:!1,would_create_canonical:!1,imported:0,skipped_existing:0,skipped_invalid:0,backup_path:null,report_path:null,errors:[],message:j?"Legacy global store already imported":"No legacy global store found"};if(!j)return O;let A;try{let P=Gz(g);A=P.store,O.skipped_invalid=P.skippedInvalid}catch(P){return O.ok=!1,O.errors.push(`Could not read legacy store: ${P instanceof Error?P.message:String(P)}`),O.message="Legacy global store import failed",O}let L={items:[]};if(N)try{L=Gz(I).store}catch(P){return O.ok=!1,O.errors.push(`Could not read canonical store: ${P instanceof Error?P.message:String(P)}`),O.message="Legacy global store import failed",O}let z=JY(L.items),W={items:[...L.items]};for(let P of A.items){if(!P?.id){O.skipped_invalid+=1;continue}if(PY(z,P)){O.skipped_existing+=1;continue}W.items.push(P);for(let S of wN(P))z.add(S);O.imported+=1}if(O.would_create_canonical=!N&&O.imported>0,O.canonical_created=!$&&O.would_create_canonical,O.message=O.imported>0?`Imported ${O.imported} legacy item(s) into canonical knowledge store`:"Legacy global store already imported",$||O.imported===0)return O;let J=`${LY(D)}-${HD().slice(0,8)}`;if(N)O.backup_path=kN(U.exportsDir,`legacy-open-knowledge-db-before-import-${J}.json`),ZN(O.backup_path,L);return ZN(I,W),O.report_path=kN(U.runsDir,`legacy-open-knowledge-import-${J}.json`),ZN(O.report_path,O),O}function I0(_){if(!U0(_))return{exists:!1,items:[]};let $=ZD(_,"utf8"),D=JSON.parse($);if(!D||!Array.isArray(D.items))return{exists:!0,items:[]};return{exists:!0,items:D.items}}function zY(_){return`${_}.lock`}var HU=1e4,Vz=25,Qz=120000,SY=new Int32Array(new SharedArrayBuffer(4));function xN(_){return typeof _==="object"&&_!==null&&"code"in _?String(_.code):void 0}function Fz(_){let $=null;try{$=rN(Bz(_),"r"),CN($)}catch{}finally{if($!==null)try{bD($)}catch{}}}var HN=new Set;function Mz(_,$){X6(_);let D=kN(Bz(_),`.${AY(_)}.tmp.${HD()}`),U=null;try{U=rN(D,"wx",384),fN(U,$),CN(U),bD(U),U=null,qz(D,_);try{Tz(_,384)}catch{}Fz(_)}catch(g){if(U!==null)try{bD(U)}catch{}try{vN(D)}catch{}throw g}}function bz(_){Atomics.wait(SY,0,0,_)}function WY(_){if(typeof _!=="number"||!Number.isInteger(_)||_<=0)return!1;try{return process.kill(_,0),!0}catch($){return xN($)!=="ESRCH"}}function Zz(_,$){try{let D=ZD(_,"utf8"),U=JSON.parse(D);if(typeof U.ts==="number")return $-U.ts>Qz&&!WY(U.pid)}catch{}try{return $-OY(_).mtimeMs>Qz}catch{return!1}}function XY(_){let $=new Date().toISOString().replace(/[-:]/g,"").replace(/\.\d{3}Z$/,"Z"),D=`${_}.stale.${$}.${HD()}`;try{qz(_,D)}catch(U){if(xN(U)!=="ENOENT")throw U;return}}function RY(_){let $=HD(),D=`${_}.breaker`,U=Date.now();while(Date.now()-U{U=I};while(Date.now()-DZY)return null;D=BY(_,"utf8")}catch{return null}let U=vY(D);for(let g of $){let I=U.get(g)?.trim();if(I)return I}return null}function j0(_,$,D){if(!CY.test(D))return;throw new rD(_,`The credential from ${$} contains characters that cannot be sent in an HTTP header (a control character or non-ASCII byte). A file written with CR-only line endings is the usual cause. Rewrite that credential file with one LF-terminated KEY=value line. The value is not shown here, and is deliberately never logged.`,[$])}var fY=Symbol.for("nodejs.util.inspect.custom");function N0(_){let{apiKey:$,...D}=_,U={...D};return Object.defineProperty(U,"apiKey",{value:$,enumerable:!1,writable:!1,configurable:!1}),Object.defineProperty(U,fY,{value:()=>({...D,apiKey:"[redacted]"}),enumerable:!1,writable:!1,configurable:!1}),U}function wY(_,$){return j0(_,"explicit apiKey option",$),N0({apiKey:$,tier:"argument",source:"explicit apiKey option",deliberate:!0,deprecated:!1,diskCandidates:[],warning:null})}function uz(_,$){for(let D of $){let U=_[D]?.trim();if(U)return{key:D,value:U}}return null}var xz=Symbol.for("hasna:contracts:credentialDeprecationNotices");function uY(){let _=globalThis,$=_[xz];if($ instanceof Set)return $;let D=new Set;return _[xz]=D,D}function xY(_){if(typeof process<"u"&&process.stderr)process.stderr.write(`${_} -`)}function nN(_,$,D={}){let{apiKeyKeys:U}=nz(_),g=dz(_,$),I=D.apiKey?.trim();if(I)return j0(_,"the explicit apiKey argument",I),N0({apiKey:I,tier:"argument",source:"explicit apiKey argument",deliberate:!0,deprecated:!1,diskCandidates:g,warning:null});let j=qY(_),N=$[j];if(N!==void 0){let z=N.trim();if(!z)throw new rD(_,`${j} is set but empty. It is a deliberate override, so it is not resolved around: either give it a real key or unset it to fall back to the credential on disk.`,[j]);return j0(_,j,z),N0({apiKey:z,tier:"override",source:j,deliberate:!0,deprecated:!1,diskCandidates:g,warning:null})}let O=D.profile?.trim()||$[yN]?.trim();if(O){let z=D.profile?.trim()?"explicit profile argument":yN;if(!kY.test(O))throw new rD(_,`Profile name from ${z} is not usable in a path. Use letters, digits, dot, dash, or underscore.`,[z]);let W=mz(_,$,O);for(let J of W){let P=wz(J,U);if(P)return j0(_,J,P),N0({apiKey:P,tier:"profile",source:J,deliberate:!0,deprecated:!1,diskCandidates:W,warning:null})}throw new rD(_,`Profile '${O}' (from ${z}) has no ${U[0]} for '${_}'. Looked in: ${W.join(", ")||""}. `+"A profile names WHICH identity to use, so it is never resolved around \u2014 "+`create the profile's credential file or unset ${yN}.`,W)}let A=g.map((z)=>({path:z,value:wz(z,U)})).filter((z)=>z.value!==null);if(A.length>0){let z=A[0];j0(_,z.path,z.value);let W=[...A.slice(1).filter((P)=>P.value!==z.value).map((P)=>P.path),...(()=>{let P=uz($,U);return P&&P.value!==z.value?[P.key]:[]})()],J=W.length>0?`Credential sources disagree for '${_}': ${z.path} and ${W.join(", ")} hold different keys. ${z.path} wins, because a file on `+"disk is re-read on every call while an environment variable is a snapshot. Reconcile them \u2014 "+"a rotation that updated only one leaves the other to fail 401 wherever it is loaded first.":null;return N0({apiKey:z.value,tier:"disk",source:z.path,deliberate:!1,deprecated:!1,diskCandidates:g,warning:J})}let L=uz($,U);if(L){j0(_,L.key,L.value);let z=g.length>0?`Put the current key in ${g[0]} \u2014 it is re-read on every call, so rotations take effect immediately.`:"This environment has no HOME, so no credential file could be consulted at all; the disk tier is unavailable here and this process will keep using the environment snapshot.",W=`[${_}] DEPRECATED: the API key came from ${L.key} in this process's environment. Environment variables are a snapshot taken when this process started, so a shell that started before a key rotation keeps using the old key until it exits. ${z}`,J=D.onDeprecation??xY,P=uY();if(!P.has(_))P.add(_),J(W);return N0({apiKey:L.value,tier:"legacy-env",source:L.key,deliberate:!1,deprecated:!0,diskCandidates:g,warning:W})}return null}var R6="HASNA_FLEET_API_DOMAIN",E0="your-deployment.example",mN=/[\u0000-\u001f\u007f]/,iz=/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;function CU(_){if(_.length===0||_.length>253||mN.test(_)||/[^\x00-\x7f]/.test(_))return!1;return _.split(".").every(($)=>$.length<=63&&!$.startsWith("xn--")&&iz.test($))}function yY(_){let $=_[R6];if($===void 0)return{domain:E0,source:"default",misconfigured:!0,warning:`${R6} is not set; using the non-resolving ${E0} fallback.`};let D=$.trim().toLowerCase();if(mN.test($)||!CU(D))return{domain:E0,source:R6,misconfigured:!0,warning:`${R6} is blank or invalid; using the non-resolving ${E0} fallback.`};return{domain:D,source:R6,misconfigured:!1,warning:null}}function lz(_){if(_.length>63||!iz.test(_))throw Error("App name must be one lowercase DNS label.");return _}function hY(_,$){let D=`${lz(_)}.${$}`;if(!CU(D))throw Error("Composed cloud hostname must be a valid DNS domain");return D}function cY(_,$){let D=lz(_),U=yY($),g=`${D}.${U.domain}`;if(CU(g))return{baseUrl:`https://${g}`,source:U.source,misconfigured:U.misconfigured,warning:U.warning};return{baseUrl:`https://${hY(D,E0)}`,source:U.source,misconfigured:!0,warning:`${R6} cannot form a valid composed cloud hostname for app '${D}'; using the non-resolving ${E0} fallback.`}}function hN(_,$,D={}){for(let U of $){let g=_[U],I=g?.trim();if(I)return{key:U,value:D.preserveRaw?g:I}}return null}function nY(_){let $=/^[a-z][a-z0-9+.-]*:\/\//i.exec(_);if(!$)throw Error("API URL must be absolute.");let D=_.slice($[0].length),U=D.search(/[/?#]/),g=U===-1?D:D.slice(0,U);if(!g)throw Error("API URL must include a hostname.");return g}function yz(_){if(!/^[0-9]+$/.test(_)||_.length>1&&_.startsWith("0"))throw Error("API URL authority must contain a canonical port between 1 and 65535.");let $=Number(_);if(!Number.isSafeInteger($)||$<1||$>65535)throw Error("API URL authority must contain a canonical port between 1 and 65535.")}function dY(_){let $;if(_.startsWith("[")){let D=_.indexOf("]");if(D===-1)throw Error("API URL authority must contain a canonical hostname.");$=_.slice(0,D+1);let U=_.slice(D+1);if(U){if(!U.startsWith(":"))throw Error("API URL authority must contain a canonical hostname and port.");yz(U.slice(1))}if(vz($.slice(1,-1))!==6)throw Error("API URL authority must contain a canonical IPv6 literal.")}else{let D=_.indexOf(":"),U=_.lastIndexOf(":");if(D!==U)throw Error("IPv6 API URL authorities must use brackets.");if(U!==-1){let N=_.slice(U+1);yz(N),$=_.slice(0,U)}else $=_;let g=vz($),j=$.split(".").every((N)=>/^(?:0x[0-9a-f]+|[0-9]+)$/i.test(N));if(g!==4&&j||g!==4&&!CU($.toLowerCase()))throw Error("API URL authority must contain a canonical ASCII hostname.")}return $.toLowerCase()}function mY(_){return/^(?:localhost|127\.0\.0\.1|\[::1\])(?::[0-9]+)?$/i.test(_)}function tz(_){if(mN.test(_))throw Error("API URL must not contain ASCII control characters.");let $=_.trim(),D=nY($);if(D.includes("@")||D.includes("\\")||D.includes("%")||/[^\x00-\x7f]/.test(D))throw Error("API URL authority must be canonical ASCII without credentials.");let U=dY(D),g=new URL($);if(g.protocol!=="http:"&&g.protocol!=="https:")throw Error("API URL must use http or https.");if(g.username||g.password)throw Error("API URL must not include credentials.");if(!g.hostname||g.hostname.endsWith("."))throw Error("API URL must include a canonical hostname.");if(g.hostname.toLowerCase()!==U)throw Error("API URL authority must not rely on parser hostname normalization.");if(g.hostname.split(".").some((j)=>j.toLowerCase().startsWith("xn--")))throw Error("API URL must not use IDN or punycode hostnames.");if(g.protocol==="http:"&&!mY(D))throw Error("API URL may use http only for an exact loopback authority.");if(g.search||g.hash)throw Error("API URL must not include a query string or fragment.");let I=g.pathname.replace(/\/+$/,"");if(I.endsWith("/v1"))I=I.slice(0,-3);return g.pathname=`${I}/v1`,g.toString().replace(/\/+$/,"")}function iY(_,$=process.env,D={}){let U=nz(_),g=hN($,U.modeKeys),I=hN($,U.apiUrlKeys,{preserveRaw:!0}),j=hN($,U.apiKeyKeys),N="sqlite",O="default",A=[],L;if(g)N=TY(g.value).mode,O=g.key;else if(I){if(L=nN(_,$,D.credentials),L)N="postgres",O=`${I.key}+${L.source}`}if(N==="sqlite")return{transport:"sqlite",mode:N,modeSource:O,baseUrl:null,apiUrlSource:null,apiKeyPresent:Boolean(j),apiKeySource:j?j.key:null,apiKeyTier:null,misconfigured:!1,warning:A.length>0?A.join(" "):null};if(L===void 0)L=nN(_,$,D.credentials);if(!L){let P=oz(_,$);return A.push(`${O}=postgres but no API key could be resolved for '${_}'. A client reaches server data over HTTP only; refusing to route. Using the local sqlite store. Looked for a credential file at ${P}, then for ${U.apiKeyKeys[0]} in the environment.`),{transport:"sqlite",mode:N,modeSource:O,baseUrl:null,apiUrlSource:null,apiKeyPresent:!1,apiKeySource:null,apiKeyTier:null,misconfigured:!0,warning:A.join(" ")}}if(L.warning)A.push(L.warning);let z=null,W=I?.key??($[R6]===void 0?"default":R6),J;try{if(!I)z=cY(_,$),W=z.source;let P=I?.value??z.baseUrl;J=tz(P)}catch(P){let S=P instanceof Error?P.message:String(P);return A.push(`Invalid API URL from ${W}: ${S}. Using local store.`),{transport:"sqlite",mode:N,modeSource:O,baseUrl:null,apiUrlSource:null,apiKeyPresent:!0,apiKeySource:L.source,apiKeyTier:L.tier,misconfigured:!0,warning:A.join(" ")}}if(z?.warning)A.push(z.warning);return{transport:"http",mode:N,modeSource:O,baseUrl:J,apiUrlSource:W,apiKeyPresent:!0,apiKeySource:L.source,apiKeyTier:L.tier,misconfigured:z?.misconfigured??!1,warning:A.length>0?A.join(" "):null}}function oz(_,$){let D=dz(_,$);return D.length>0?D.join(" or "):""}class O0 extends Error{status;method;path;body;credentialSource;credentialTier;constructor(_,$,D,U,g){let I=g?`. ${g.guidance}`:"";super(`Hasna cloud request failed: ${_} ${$} -> ${D}${I}`);this.name="HasnaHttpError",this.status=D,this.method=_,this.path=$,this.body=U,this.credentialSource=g?.source??null,this.credentialTier=g?.tier??null}}function lY(_,$){if(typeof $==="function")return $();return wY(_,$)}function tY(_){let $=`The API key for this request came from ${_.source}`;if(_.deliberate)return`${$} \u2014 a credential you selected deliberately. It was NOT substituted with any other key: `+"falling back here would authenticate as a different principal than the one you named, which is exactly the failure an override exists to prevent. Rotate that key, or unset the override to use the credential on disk.";if(_.deprecated){let D=_.diskCandidates[0],U=D?`Write the CURRENT key to ${D} \u2014 that file is re-read on every call, so rotations take `+`effect immediately and in every shell. Do not simply unset ${_.source}: nothing was found on disk, so that would leave this client with no credential at all.`:"This environment has no HOME, so no credential file could be consulted; the disk tier is unavailable here and there is nothing to fall back to. Set HOME, or supply the key explicitly.";return`${$}, a variable in this process's environment \u2014 which is a snapshot taken when the process `+`started. A STALE SHELL is the most common cause of this error: this shell exported the key before it was rotated, and will keep sending the old one until it exits. ${U}`}return`${$}, which was re-read from disk on this very call \u2014 so a stale shell is NOT the cause here. `+"The stored credential is genuinely being rejected: rotate it, or re-run the fleet key distribution so this machine gets the current key."}var oY=[408,425,429,500,502,503,504],pY=new Set(["GET","HEAD","PUT","DELETE","OPTIONS"]),eY=new Set(["host",":authority","forwarded","x-forwarded-host","x-original-host"]);function hz(_,$){if(!_)return;let D=Object.keys(_).find((U)=>eY.has(U.trim().toLowerCase()));if(D)throw Error(`Authenticated ${$} headers must not set authority header '${D}'.`)}function aY(_,$){if(!$)return _;let D=$ instanceof URLSearchParams?$:new URLSearchParams;if(!($ instanceof URLSearchParams))for(let[g,I]of Object.entries($)){if(I===null||I===void 0)continue;if(Array.isArray(I))for(let j of I)D.append(g,String(j));else D.append(g,String(I))}let U=D.toString();if(!U)return _;return`${_}${_.includes("?")?"&":"?"}${U}`}var sY=(_)=>new Promise(($)=>setTimeout($,_));function _Q(_){let $=_.fetchImpl??((A,L)=>fetch(A,L)),D=tz(_.baseUrl),U=_.timeoutMs??30000,g=_.sleepImpl??sY,I=_.retry;function j(A){let L=A!==void 0?A:I;if(L===!1)return null;let z=L??{};return{retries:z.retries??2,baseDelayMs:z.baseDelayMs??200,maxDelayMs:z.maxDelayMs??2000,retryStatuses:z.retryStatuses??[...oY]}}async function N(A,L,z,W,J,P){hz(_.headers,"transport"),hz(J.headers,"request");let S={"x-api-key":P.apiKey,Authorization:`Bearer ${P.apiKey}`,Accept:"application/json",..._.headers??{},...J.headers??{}};if(J.idempotencyKey)S["Idempotency-Key"]=J.idempotencyKey;let X={method:A,headers:S,redirect:"manual"};if(W!==void 0)S["Content-Type"]="application/json",X.body=JSON.stringify(W);let G=new AbortController,R=()=>G.abort();if(J.signal)if(J.signal.aborted)G.abort();else J.signal.addEventListener("abort",R,{once:!0});let K=setTimeout(()=>G.abort(),J.timeoutMs??U);X.signal=G.signal;let Q;try{Q=await $(z,X)}catch(V){let b=V instanceof Error?V:Error(String(V));if(J.signal?.aborted)return{ok:!1,retryable:!1,error:b};return{ok:!1,retryable:!0,error:b}}finally{if(clearTimeout(K),J.signal)J.signal.removeEventListener("abort",R)}let T=await Q.text(),q=void 0;if(T.length>0)try{q=JSON.parse(T)}catch{q=T}if(!Q.ok){if(Q.status>=300&&Q.status<400)return{ok:!1,retryable:!1,error:new O0(A,L,Q.status,q)};if(Q.status===401||Q.status===403)return{ok:!1,retryable:!1,error:new O0(A,L,Q.status,q,{source:P.source,tier:P.tier,guidance:tY(P)})};let V=j(J.retry);return{ok:!1,retryable:V?V.retryStatuses.includes(Q.status):!1,error:new O0(A,L,Q.status,q)}}return{ok:!0,value:q}}async function O(A,L,z,W={}){let J=A.toUpperCase(),P=aY(L.startsWith("/")?L:`/${L}`,W.query),S=`${D}${P}`,X=j(W.retry),G=pY.has(J)||Boolean(W.idempotencyKey),R=X&&G?X.retries+1:1,K=lY(_.name,_.apiKey),Q=null;for(let T=1;T<=R;T++){let q=await N(J,P,S,z,W,K);if(q.ok)return q.value;if(Q=q,!(X!==null&&G&&q.retryable&&TO("GET",A,void 0,L),post:(A,L,z)=>O("POST",A,L,z),put:(A,L,z)=>O("PUT",A,L,z),patch:(A,L,z)=>O("PATCH",A,L,z),del:(A,L,z)=>O("DELETE",A,L,z)}}function $Q(_,$=process.env,D){let U=D?.credentials,g=iY(_,$,{...U?{credentials:U}:{}});if(g.misconfigured)throw Error(g.warning??`Client for '${_}' is misconfigured for the API client.`);if(g.transport==="sqlite"||!g.baseUrl)return{transport:"sqlite",client:null,resolution:g};let I=()=>{let j=nN(_,$,U);if(!j)throw Error(`Client for '${_}' resolved to the http transport but no API key is available any more. Looked at ${oz(_,$)}, then the environment. A credential file that was removed after this client was built is the usual cause.`);return j};return{transport:"http",client:_Q({name:_,baseUrl:g.baseUrl,apiKey:I,...D?.fetchImpl?{fetchImpl:D.fetchImpl}:{},...D?.headers?{headers:D.headers}:{},...D?.timeoutMs?{timeoutMs:D.timeoutMs}:{},...D?.retry!==void 0?{retry:D.retry}:{},...D?.sleepImpl?{sleepImpl:D.sleepImpl}:{}}),resolution:g}}function dN(_){let $=_.replace(/^\/+|\/+$/g,"");if(!$)throw Error("resource must be a non-empty path segment");return`/${$}`}function cN(_,$){if($===void 0||$===null||`${$}`.length===0)throw Error("id must be a non-empty string");return`${dN(_)}/${encodeURIComponent(String($))}`}function DQ(){let _=globalThis;if(_.crypto?.randomUUID)return _.crypto.randomUUID();return`idmp_${Date.now().toString(36)}_${Math.random().toString(36).slice(2,12)}`}function gQ(_){if(Array.isArray(_))return _;if(_&&typeof _==="object"){let $=_;for(let D of["items","data","results","rows","records"])if(Array.isArray($[D]))return $[D]}return[]}function UQ(_){if(_&&typeof _==="object"){let $=_;for(let D of["total","count","totalCount","total_count"])if(typeof $[D]==="number")return $[D]}return null}function IQ(_){if(_&&typeof _==="object"){let $=_;for(let D of["cursor","nextCursor","next_cursor","next"])if(typeof $[D]==="string")return $[D]}return null}function jQ(_,$){return{name:_,baseUrl:$.baseUrl,transport:$,async list(D,U={}){let g=await $.get(dN(D),U);return{items:gQ(g),total:UQ(g),cursor:IQ(g),raw:g}},async get(D,U,g={}){try{return await $.get(cN(D,U),g)}catch(I){if(I instanceof O0&&I.status===404)return null;throw I}},async create(D,U,g={}){let{idempotencyKey:I,...j}=g;return $.post(dN(D),U,{...j,idempotencyKey:I??DQ()})},async update(D,U,g,I={}){let{method:j="PATCH",idempotencyKey:N,...O}=I;return(j==="PUT"?$.put:$.patch)(cN(D,U),g,{...O,...N?{idempotencyKey:N}:{}})},async delete(D,U,g={}){try{await $.del(cN(D,U),void 0,g)}catch(I){if(I instanceof O0&&I.status===404)return;throw I}}}}function iN(_,$=process.env,D){let U=$Q(_,$,D);if(U.transport==="http")return{transport:"http",client:jQ(_,U.client)};return{transport:"sqlite",client:null}}function NQ(_){return _.toUpperCase().replace(/-/g,"_")}function pz(_){let $=NQ(_);return{modeKeys:[`HASNA_${$}_STORAGE_MODE`,`HASNA_${$}_MODE`,`${$}_STORAGE_MODE`,`${$}_MODE`],apiUrlKeys:[`HASNA_${$}_API_URL`,`${$}_API_URL`],apiKeyKeys:[`HASNA_${$}_API_KEY`,`${$}_API_KEY`]}}function vD(_){let $=_.trim().toLowerCase().replace(/-/g,"_");if($==="sqlite")return{mode:"sqlite"};if($==="postgres"||$==="postgresql")return{mode:"postgres"};throw Error(`Unknown storage mode '${_}'. The runtime-placement axis was removed; set sqlite for the on-box SQLite file or postgres for a PostgreSQL server (DATABASE_URL).`)}function rU(_){let $=_.trim().toLowerCase().replace(/-/g,"_");if($==="sqlite")return{mode:"sqlite"};if($==="postgres"||$==="postgresql")return{mode:"postgres"};throw Error(`Unknown storage mode '${_}'. The runtime-placement axis was removed; set sqlite for the on-box SQLite file or postgres for a PostgreSQL server (DATABASE_URL).`)}import Iw from"pg";class vU extends Error{scheme;port;constructor(_,$){super(_);this.name="KnowledgeNetworkGuardError",this.scheme=$.scheme,this.port=$.port}}function A0(_=process.env){return(_.NODE_ENV??"").trim().toLowerCase()==="test"}function ez(_){let $=_.split(".");if($.length!==4)return!1;if(!$.every((D)=>/^\d{1,3}$/.test(D)&&Number(D)<=255))return!1;return $[0]==="127"}function JQ(_){let $=_.trim().toLowerCase();if($.length===0)return!1;if($==="localhost"||$.endsWith(".localhost"))return!0;if(ez($))return!0;if(!$.startsWith("[")||!$.endsWith("]"))return!1;let D=$.slice(1,-1);if(D==="::1"||/^(0:){7}1$/.test(D))return!0;let U=D.split(":").pop()??"";if(/^(::ffff:|::)/.test(D)&&ez(U))return!0;return/^::(ffff:)?7f[0-9a-f]{2}:[0-9a-f]{1,4}$/.test(D)}function _S(_){if(typeof _==="string")return _;if(_ instanceof URL)return _.href;return _.url}function az(_,$=process.env){if(!A0($))return;let D=_S(_),U;try{U=new URL(D)}catch{throw new vU("knowledge: refused an outbound request with an unparseable target while NODE_ENV=test. Under test, only loopback requests are permitted.",{scheme:"unknown",port:""})}if(JQ(U.hostname))return;throw new vU(`knowledge: refused a non-loopback ${U.protocol.replace(":","")} request while NODE_ENV=test (target host withheld on purpose). This process resolved to the cloud backend under test, which means a read or write was about to leave the machine and reach the live store. Select the mode explicitly (HASNA_KNOWLEDGE_STORAGE_MODE=sqlite) or point the API URL at 127.0.0.1 for a hermetic test.`,{scheme:U.protocol.replace(":",""),port:U.port})}var PQ=new Set([301,302,303,307,308]),sz=5;function zQ(_,$){if($?.method)return $.method.toUpperCase();if(typeof _!=="string"&&!(_ instanceof URL))return _.method.toUpperCase();return"GET"}async function fU(_,$){if(az(_),!A0()||$?.redirect!==void 0)return fetch(_,$);let D=_S(_),U=zQ(_,$),g=$?.body,I=await fetch(_,{...$??{},redirect:"manual"});for(let j=0;PQ.has(I.status);j++){let N=I.headers.get("location");if(!N)return I;let O=new URL(N,D).href;if(az(O),j>=sz){let L=new URL(O);throw new vU(`knowledge: refused to follow more than ${sz} redirects while NODE_ENV=test (target host withheld on purpose). Under test the guard follows redirects itself so every hop is checked, and a chain this long is a loop, not a route.`,{scheme:L.protocol.replace(":",""),port:L.port})}if(I.status===303||(I.status===301||I.status===302)&&U!=="GET"&&U!=="HEAD")U="GET",g=void 0;let A={...$??{},method:U,redirect:"manual"};if(g===void 0)delete A.body;else A.body=g;I=await fetch(O,A),D=O}return I}var uU="knowledge",lN=pz(uU),o$=lN.modeKeys,xU=lN.apiUrlKeys,yU=lN.apiKeyKeys;function wU(_,$){return $.filter((D)=>(_[D]??"").trim().length>0)}function fD(_=process.env){let $=[...wU(_,xU),...wU(_,yU)],D=o$[0];for(let U of o$){let g=_[U]?.trim();if(!g)continue;let I;try{I=rU(g)}catch(N){let O=N instanceof Error?N.message:String(N);throw Error(`knowledge: ${U}=${g} is not a valid mode. ${O}`)}let j=[];if(U!==D)j.push(`Using alias env ${U}; the canonical key is ${D}.`);if(I.mode==="sqlite"&&$.length>0)j.push(`${U}=sqlite pins the on-box store; ${$.join(", ")} are set but ignored.`);return{mode:I.mode,source:{kind:"env",name:U,value:g},pointer_env_present:$,pointer_ignored:I.mode==="sqlite"&&$.length>0,warning:j.length>0?j.join(" "):null}}return{mode:"sqlite",source:{kind:"default",name:null,value:null},pointer_env_present:$,pointer_ignored:$.length>0,warning:$.length>0?`${$.join(", ")} are set but do NOT select a backend: mode is sqlite by default. Set ${D}=postgres to route reads and writes to the API, or unset those vars to silence this note.`:null}}var SQ=["postgres"],WQ=["sqlite"],$S=new Map;function DS(_,$,D){let U=$===vD;if(U){let g=$S.get(_);if(g!==void 0)return g}for(let g of _)try{if($(g),U)$S.set(_,g);return g}catch{}throw Error(`knowledge: no known storage token is accepted by the installed @hasna/contracts (tried ${_.join(", ")}). The storage-mode enum has changed; add the new token to ${D} in src/knowledge-mode.ts.`)}function XQ(_=vD){return DS(SQ,_,"SERVER_MODE_CANDIDATES")}function RQ(_=vD){return DS(WQ,_,"LOCAL_MODE_CANDIDATES")}function GQ(_,$=vD){return _==="postgres"?XQ($):RQ($)}function tN(_,$){return{..._,[o$[0]]:GQ($)}}class gS extends Error{code="knowledge_mode_unset_with_api_url";constructor(_){let $=o$[0];super(`knowledge: ${_.join(", ")} names an API store, but no mode variable says to use it, so this command would silently read and write the on-box store instead. Set ${$}=postgres to use the API, or ${$}=sqlite to confirm you want the on-box store. Run 'knowledge mode' to see the full resolution.`);this.name="HalfConfiguredKnowledgeClientError"}}function US(_=process.env,$={}){let D=fD(_);if($.storePathOverridden)return D;if(D.source.kind!=="default")return D;let U=wU(_,xU);if(U.length===0)return D;throw new gS(U)}function IS(_=process.env){let $=fD(_);return{...$,store_transport:$.mode==="postgres"?"api":"local",api_key_present:wU(_,yU).length>0,network_guard_active:A0(_)}}function jS(_){return{fetchImpl:fU,...A0(_)?{retry:!1}:{}}}var G6="notes";class L0 extends Error{expected;current;code="version_conflict";constructor(_,$){super(`version_conflict: this edit was written against version ${_} but the stored entry is now at version ${$}. Nothing was written. Re-read the entry and re-apply only if the fields you are changing are untouched between the two versions.`);this.expected=_;this.current=$;this.name="KnowledgeVersionConflictError"}}function YQ(_){let $={};if(_.search)$.search=_.search;if(_.limit!==void 0)$.limit=_.limit;if(_.offset!==void 0)$.offset=_.offset;if(_.includeArchived||_.archivedOnly)$.includeArchived=!0;return $}function QQ(_){return{baseUrl:_.baseUrl,async list($={}){let D=$.limit??200,U=YQ({...$,limit:Math.min(Math.max(D,1),200)}),g=await _.list(G6,{query:U}),I=g.items;if($.archivedOnly)I=I.filter((j)=>j.archived===!0);if($.tag){let j=$.tag.toLowerCase();I=I.filter((N)=>(N.tags??[]).some((O)=>O.toLowerCase()===j))}return{items:I,total:g.total}},async get($){return _.get(G6,$)},async create($){return _.create(G6,{...$.id?{id:$.id}:{},title:$.title,content:$.content,url:$.url??null,tags:$.tags??[],...$.metadata?{metadata:$.metadata}:{}})},async update($,D,U={}){try{return await _.update(G6,$,D,{...U.expectedVersion!==void 0?{headers:{"if-match":String(U.expectedVersion)}}:{}})}catch(g){if(oN(g))return null;let I=TQ(g);if(I)throw I;throw g}},async delete($){let D=await _.get(G6,$);if(!D)return!1;return await _.delete(G6,D.id),!0},async listVersions($,D={}){try{return await _.transport.get(`/${G6}/${encodeURIComponent($)}/versions`,{query:{limit:D.limit,offset:D.offset}})}catch(U){if(oN(U))return null;throw U}},async getVersion($,D){try{return await _.transport.get(`/${G6}/${encodeURIComponent($)}/versions/${D}`)}catch(U){if(oN(U))return null;throw U}}}}function TQ(_){if(!_||typeof _!=="object")return null;if(_.status!==409)return null;let $=_.body,U=(typeof $==="string"?qQ($):$)??{};if(U.error!=="version_conflict")return null;return new L0(Number(U.expected??0),Number(U.current??0))}function qQ(_){try{return JSON.parse(_)}catch{return null}}function oN(_){return Boolean(_&&typeof _==="object"&&_.status===404)}function wD(_=process.env){let $=BQ(_);return $?QQ($):null}function BQ(_){if(fD(_).mode!=="postgres")return null;let $=iN(uU,tN(_,"postgres"),jS(_));if($.transport!=="http")return null;return $.client}function Y6(_=process.env){if(fD(_).mode!=="postgres")return!1;return iN(uU,tN(_,"postgres"),jS(_)).transport==="http"}async function hU(_){let D=[];for(let U=0;;U+=200){let{items:g}=await _.list({includeArchived:!0,limit:200,offset:U});if(D.push(...g),g.length<200)break;if(U>1e5)break}return D}class eN extends Error{location;code="version_history_unsupported";constructor(_){super(`Version history is not kept by the local JSON knowledge store (${_}). It has no version line, so an empty history here would be a claim, not a measurement. Entry versioning lives in the Postgres-backed store: point this CLI at it (HASNA_KNOWLEDGE_STORAGE_MODE=postgres plus the API url/key) and re-run.`);this.location=_;this.name="VersionHistoryUnsupportedError"}}function pN(_,$){return _.id===$||_.short_id===$}class NS{storePath;kind="local";supportsVersions=!1;constructor(_){this.storePath=_}async listVersions(){throw new eN(this.storePath)}async getVersion(){throw new eN(this.storePath)}get location(){return this.storePath}get exists(){return KQ(this.storePath)}async listAll(){let _=I0(this.storePath);return{items:_.items,exists:_.exists}}async get(_){return I0(this.storePath).items.find((D)=>pN(D,_))??null}async create(_){return v$(this.storePath,()=>{let $=CD(this.storePath),D=new Date().toISOString(),U=_.id??Cz(),g={id:U,short_id:rz(U),title:_.title,content:_.content,url:_.url??null,tags:_.tags??[],metadata:_.metadata??{},archived:!1,created_at:D,updated_at:D,version:1};return $.items.push(g),t$(this.storePath,$),g},{createParent:!0})}async update(_,$,D={}){return v$(this.storePath,()=>{let U=CD(this.storePath),g=U.items.findIndex((N)=>pN(N,_));if(g===-1)return null;let I=U.items[g],j=I.version??1;if(D.expectedVersion!==void 0&&D.expectedVersion!==j)throw new L0(D.expectedVersion,j);if($.title!==void 0)I.title=$.title;if($.content!==void 0)I.content=$.content;if($.url!==void 0)I.url=$.url;if($.tags!==void 0)I.tags=$.tags;if($.metadata!==void 0)I.metadata=$.metadata;if($.archived!==void 0)I.archived=$.archived;return I.updated_at=new Date().toISOString(),I.version=j+1,U.items[g]=I,t$(this.storePath,U),I},{createParent:!0})}async delete(_){return v$(this.storePath,()=>{let $=CD(this.storePath),D=$.items.length;$.items=$.items.filter((g)=>!pN(g,_));let U=D!==$.items.length;if(U)t$(this.storePath,$);return U},{createParent:!0})}async deleteMany(_){if(_.length===0)return 0;let $=new Set(_);return v$(this.storePath,()=>{let D=CD(this.storePath),U=D.items.length;D.items=D.items.filter((I)=>!$.has(I.id)&&!(I.short_id!=null&&$.has(I.short_id)));let g=U-D.items.length;if(g>0)t$(this.storePath,D);return g},{createParent:!0})}}class ES{cloud;kind="api";exists=!0;supportsVersions=!0;constructor(_){this.cloud=_}async listVersions(_,$={}){return this.cloud.listVersions(_,$)}async getVersion(_,$){return this.cloud.getVersion(_,$)}get location(){return this.cloud.baseUrl}async listAll(){return{items:await hU(this.cloud),exists:!0}}async get(_){return this.cloud.get(_)}async create(_){return this.cloud.create({..._.id?{id:_.id}:{},title:_.title,content:_.content,url:_.url??null,tags:_.tags??[],..._.metadata?{metadata:_.metadata}:{}})}async update(_,$,D={}){return this.cloud.update(_,$,{expectedVersion:D.expectedVersion})}async delete(_){return this.cloud.delete(_)}async deleteMany(_){let $=0;for(let D of _)if(await this.cloud.delete(D))$+=1;return $}}function cU(_){let $=_.storePathOverridden?null:wD(_.env??process.env);if($)return new ES($);return new NS(_.storePath)}function OS(_){let $=_??"";if($==="")return[];return $.replace(/\n$/,"").split(` +`)}function nN(_,$,D={}){let{apiKeyKeys:U}=nz(_),g=dz(_,$),I=D.apiKey?.trim();if(I)return j0(_,"the explicit apiKey argument",I),N0({apiKey:I,tier:"argument",source:"explicit apiKey argument",deliberate:!0,deprecated:!1,diskCandidates:g,warning:null});let j=qY(_),N=$[j];if(N!==void 0){let z=N.trim();if(!z)throw new rD(_,`${j} is set but empty. It is a deliberate override, so it is not resolved around: either give it a real key or unset it to fall back to the credential on disk.`,[j]);return j0(_,j,z),N0({apiKey:z,tier:"override",source:j,deliberate:!0,deprecated:!1,diskCandidates:g,warning:null})}let O=D.profile?.trim()||$[yN]?.trim();if(O){let z=D.profile?.trim()?"explicit profile argument":yN;if(!kY.test(O))throw new rD(_,`Profile name from ${z} is not usable in a path. Use letters, digits, dot, dash, or underscore.`,[z]);let W=mz(_,$,O);for(let J of W){let P=wz(J,U);if(P)return j0(_,J,P),N0({apiKey:P,tier:"profile",source:J,deliberate:!0,deprecated:!1,diskCandidates:W,warning:null})}throw new rD(_,`Profile '${O}' (from ${z}) has no ${U[0]} for '${_}'. Looked in: ${W.join(", ")||""}. `+"A profile names WHICH identity to use, so it is never resolved around \u2014 "+`create the profile's credential file or unset ${yN}.`,W)}let A=g.map((z)=>({path:z,value:wz(z,U)})).filter((z)=>z.value!==null);if(A.length>0){let z=A[0];j0(_,z.path,z.value);let W=[...A.slice(1).filter((P)=>P.value!==z.value).map((P)=>P.path),...(()=>{let P=uz($,U);return P&&P.value!==z.value?[P.key]:[]})()],J=W.length>0?`Credential sources disagree for '${_}': ${z.path} and ${W.join(", ")} hold different keys. ${z.path} wins, because a file on `+"disk is re-read on every call while an environment variable is a snapshot. Reconcile them \u2014 "+"a rotation that updated only one leaves the other to fail 401 wherever it is loaded first.":null;return N0({apiKey:z.value,tier:"disk",source:z.path,deliberate:!1,deprecated:!1,diskCandidates:g,warning:J})}let L=uz($,U);if(L){j0(_,L.key,L.value);let z=g.length>0?`Put the current key in ${g[0]} \u2014 it is re-read on every call, so rotations take effect immediately.`:"This environment has no HOME, so no credential file could be consulted at all; the disk tier is unavailable here and this process will keep using the environment snapshot.",W=`[${_}] DEPRECATED: the API key came from ${L.key} in this process's environment. Environment variables are a snapshot taken when this process started, so a shell that started before a key rotation keeps using the old key until it exits. ${z}`,J=D.onDeprecation??xY,P=uY();if(!P.has(_))P.add(_),J(W);return N0({apiKey:L.value,tier:"legacy-env",source:L.key,deliberate:!1,deprecated:!0,diskCandidates:g,warning:W})}return null}var R6="HASNA_FLEET_API_DOMAIN",E0="your-deployment.example",mN=/[\u0000-\u001f\u007f]/,iz=/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;function CU(_){if(_.length===0||_.length>253||mN.test(_)||/[^\x00-\x7f]/.test(_))return!1;return _.split(".").every(($)=>$.length<=63&&!$.startsWith("xn--")&&iz.test($))}function yY(_){let $=_[R6];if($===void 0)return{domain:E0,source:"default",misconfigured:!0,warning:`${R6} is not set; using the non-resolving ${E0} fallback.`};let D=$.trim().toLowerCase();if(mN.test($)||!CU(D))return{domain:E0,source:R6,misconfigured:!0,warning:`${R6} is blank or invalid; using the non-resolving ${E0} fallback.`};return{domain:D,source:R6,misconfigured:!1,warning:null}}function lz(_){if(_.length>63||!iz.test(_))throw Error("App name must be one lowercase DNS label.");return _}function hY(_,$){let D=`${lz(_)}.${$}`;if(!CU(D))throw Error("Composed cloud hostname must be a valid DNS domain");return D}function cY(_,$){let D=lz(_),U=yY($),g=`${D}.${U.domain}`;if(CU(g))return{baseUrl:`https://${g}`,source:U.source,misconfigured:U.misconfigured,warning:U.warning};return{baseUrl:`https://${hY(D,E0)}`,source:U.source,misconfigured:!0,warning:`${R6} cannot form a valid composed cloud hostname for app '${D}'; using the non-resolving ${E0} fallback.`}}function hN(_,$,D={}){for(let U of $){let g=_[U],I=g?.trim();if(I)return{key:U,value:D.preserveRaw?g:I}}return null}function nY(_){let $=/^[a-z][a-z0-9+.-]*:\/\//i.exec(_);if(!$)throw Error("API URL must be absolute.");let D=_.slice($[0].length),U=D.search(/[/?#]/),g=U===-1?D:D.slice(0,U);if(!g)throw Error("API URL must include a hostname.");return g}function yz(_){if(!/^[0-9]+$/.test(_)||_.length>1&&_.startsWith("0"))throw Error("API URL authority must contain a canonical port between 1 and 65535.");let $=Number(_);if(!Number.isSafeInteger($)||$<1||$>65535)throw Error("API URL authority must contain a canonical port between 1 and 65535.")}function dY(_){let $;if(_.startsWith("[")){let D=_.indexOf("]");if(D===-1)throw Error("API URL authority must contain a canonical hostname.");$=_.slice(0,D+1);let U=_.slice(D+1);if(U){if(!U.startsWith(":"))throw Error("API URL authority must contain a canonical hostname and port.");yz(U.slice(1))}if(vz($.slice(1,-1))!==6)throw Error("API URL authority must contain a canonical IPv6 literal.")}else{let D=_.indexOf(":"),U=_.lastIndexOf(":");if(D!==U)throw Error("IPv6 API URL authorities must use brackets.");if(U!==-1){let N=_.slice(U+1);yz(N),$=_.slice(0,U)}else $=_;let g=vz($),j=$.split(".").every((N)=>/^(?:0x[0-9a-f]+|[0-9]+)$/i.test(N));if(g!==4&&j||g!==4&&!CU($.toLowerCase()))throw Error("API URL authority must contain a canonical ASCII hostname.")}return $.toLowerCase()}function mY(_){return/^(?:localhost|127\.0\.0\.1|\[::1\])(?::[0-9]+)?$/i.test(_)}function tz(_){if(mN.test(_))throw Error("API URL must not contain ASCII control characters.");let $=_.trim(),D=nY($);if(D.includes("@")||D.includes("\\")||D.includes("%")||/[^\x00-\x7f]/.test(D))throw Error("API URL authority must be canonical ASCII without credentials.");let U=dY(D),g=new URL($);if(g.protocol!=="http:"&&g.protocol!=="https:")throw Error("API URL must use http or https.");if(g.username||g.password)throw Error("API URL must not include credentials.");if(!g.hostname||g.hostname.endsWith("."))throw Error("API URL must include a canonical hostname.");if(g.hostname.toLowerCase()!==U)throw Error("API URL authority must not rely on parser hostname normalization.");if(g.hostname.split(".").some((j)=>j.toLowerCase().startsWith("xn--")))throw Error("API URL must not use IDN or punycode hostnames.");if(g.protocol==="http:"&&!mY(D))throw Error("API URL may use http only for an exact loopback authority.");if(g.search||g.hash)throw Error("API URL must not include a query string or fragment.");let I=g.pathname.replace(/\/+$/,"");if(I.endsWith("/v1"))I=I.slice(0,-3);return g.pathname=`${I}/v1`,g.toString().replace(/\/+$/,"")}function iY(_,$=process.env,D={}){let U=nz(_),g=hN($,U.modeKeys),I=hN($,U.apiUrlKeys,{preserveRaw:!0}),j=hN($,U.apiKeyKeys),N="sqlite",O="default",A=[],L;if(g)N=TY(g.value).mode,O=g.key;else if(I){if(L=nN(_,$,D.credentials),L)N="postgres",O=`${I.key}+${L.source}`}if(N==="sqlite")return{transport:"sqlite",mode:N,modeSource:O,baseUrl:null,apiUrlSource:null,apiKeyPresent:Boolean(j),apiKeySource:j?j.key:null,apiKeyTier:null,misconfigured:!1,warning:A.length>0?A.join(" "):null};if(L===void 0)L=nN(_,$,D.credentials);if(!L){let P=oz(_,$);return A.push(`${O}=postgres but no API key could be resolved for '${_}'. A client reaches server data over HTTP only; refusing to route. Using the local sqlite store. Looked for a credential file at ${P}, then for ${U.apiKeyKeys[0]} in the environment.`),{transport:"sqlite",mode:N,modeSource:O,baseUrl:null,apiUrlSource:null,apiKeyPresent:!1,apiKeySource:null,apiKeyTier:null,misconfigured:!0,warning:A.join(" ")}}if(L.warning)A.push(L.warning);let z=null,W=I?.key??($[R6]===void 0?"default":R6),J;try{if(!I)z=cY(_,$),W=z.source;let P=I?.value??z.baseUrl;J=tz(P)}catch(P){let S=P instanceof Error?P.message:String(P);return A.push(`Invalid API URL from ${W}: ${S}. Using local store.`),{transport:"sqlite",mode:N,modeSource:O,baseUrl:null,apiUrlSource:null,apiKeyPresent:!0,apiKeySource:L.source,apiKeyTier:L.tier,misconfigured:!0,warning:A.join(" ")}}if(z?.warning)A.push(z.warning);return{transport:"http",mode:N,modeSource:O,baseUrl:J,apiUrlSource:W,apiKeyPresent:!0,apiKeySource:L.source,apiKeyTier:L.tier,misconfigured:z?.misconfigured??!1,warning:A.length>0?A.join(" "):null}}function oz(_,$){let D=dz(_,$);return D.length>0?D.join(" or "):""}class O0 extends Error{status;method;path;body;credentialSource;credentialTier;constructor(_,$,D,U,g){let I=g?`. ${g.guidance}`:"";super(`Hasna cloud request failed: ${_} ${$} -> ${D}${I}`);this.name="HasnaHttpError",this.status=D,this.method=_,this.path=$,this.body=U,this.credentialSource=g?.source??null,this.credentialTier=g?.tier??null}}function lY(_,$){if(typeof $==="function")return $();return wY(_,$)}function tY(_){let $=`The API key for this request came from ${_.source}`;if(_.deliberate)return`${$} \u2014 a credential you selected deliberately. It was NOT substituted with any other key: `+"falling back here would authenticate as a different principal than the one you named, which is exactly the failure an override exists to prevent. Rotate that key, or unset the override to use the credential on disk.";if(_.deprecated){let D=_.diskCandidates[0],U=D?`Write the CURRENT key to ${D} \u2014 that file is re-read on every call, so rotations take `+`effect immediately and in every shell. Do not simply unset ${_.source}: nothing was found on disk, so that would leave this client with no credential at all.`:"This environment has no HOME, so no credential file could be consulted; the disk tier is unavailable here and there is nothing to fall back to. Set HOME, or supply the key explicitly.";return`${$}, a variable in this process's environment \u2014 which is a snapshot taken when the process `+`started. A STALE SHELL is the most common cause of this error: this shell exported the key before it was rotated, and will keep sending the old one until it exits. ${U}`}return`${$}, which was re-read from disk on this very call \u2014 so a stale shell is NOT the cause here. `+"The stored credential is genuinely being rejected: rotate it, or re-run the fleet key distribution so this machine gets the current key."}var oY=[408,425,429,500,502,503,504],pY=new Set(["GET","HEAD","PUT","DELETE","OPTIONS"]),eY=new Set(["host",":authority","forwarded","x-forwarded-host","x-original-host"]);function hz(_,$){if(!_)return;let D=Object.keys(_).find((U)=>eY.has(U.trim().toLowerCase()));if(D)throw Error(`Authenticated ${$} headers must not set authority header '${D}'.`)}function aY(_,$){if(!$)return _;let D=$ instanceof URLSearchParams?$:new URLSearchParams;if(!($ instanceof URLSearchParams))for(let[g,I]of Object.entries($)){if(I===null||I===void 0)continue;if(Array.isArray(I))for(let j of I)D.append(g,String(j));else D.append(g,String(I))}let U=D.toString();if(!U)return _;return`${_}${_.includes("?")?"&":"?"}${U}`}var sY=(_)=>new Promise(($)=>setTimeout($,_));function _Q(_){let $=_.fetchImpl??((A,L)=>fetch(A,L)),D=tz(_.baseUrl),U=_.timeoutMs??30000,g=_.sleepImpl??sY,I=_.retry;function j(A){let L=A!==void 0?A:I;if(L===!1)return null;let z=L??{};return{retries:z.retries??2,baseDelayMs:z.baseDelayMs??200,maxDelayMs:z.maxDelayMs??2000,retryStatuses:z.retryStatuses??[...oY]}}async function N(A,L,z,W,J,P){hz(_.headers,"transport"),hz(J.headers,"request");let S={"x-api-key":P.apiKey,Authorization:`Bearer ${P.apiKey}`,Accept:"application/json",..._.headers??{},...J.headers??{}};if(J.idempotencyKey)S["Idempotency-Key"]=J.idempotencyKey;let X={method:A,headers:S,redirect:"manual"};if(W!==void 0)S["Content-Type"]="application/json",X.body=JSON.stringify(W);let G=new AbortController,R=()=>G.abort();if(J.signal)if(J.signal.aborted)G.abort();else J.signal.addEventListener("abort",R,{once:!0});let K=setTimeout(()=>G.abort(),J.timeoutMs??U);X.signal=G.signal;let Q;try{Q=await $(z,X)}catch(V){let b=V instanceof Error?V:Error(String(V));if(J.signal?.aborted)return{ok:!1,retryable:!1,error:b};return{ok:!1,retryable:!0,error:b}}finally{if(clearTimeout(K),J.signal)J.signal.removeEventListener("abort",R)}let T=await Q.text(),q=void 0;if(T.length>0)try{q=JSON.parse(T)}catch{q=T}if(!Q.ok){if(Q.status>=300&&Q.status<400)return{ok:!1,retryable:!1,error:new O0(A,L,Q.status,q)};if(Q.status===401||Q.status===403)return{ok:!1,retryable:!1,error:new O0(A,L,Q.status,q,{source:P.source,tier:P.tier,guidance:tY(P)})};let V=j(J.retry);return{ok:!1,retryable:V?V.retryStatuses.includes(Q.status):!1,error:new O0(A,L,Q.status,q)}}return{ok:!0,value:q}}async function O(A,L,z,W={}){let J=A.toUpperCase(),P=aY(L.startsWith("/")?L:`/${L}`,W.query),S=`${D}${P}`,X=j(W.retry),G=pY.has(J)||Boolean(W.idempotencyKey),R=X&&G?X.retries+1:1,K=lY(_.name,_.apiKey),Q=null;for(let T=1;T<=R;T++){let q=await N(J,P,S,z,W,K);if(q.ok)return q.value;if(Q=q,!(X!==null&&G&&q.retryable&&TO("GET",A,void 0,L),post:(A,L,z)=>O("POST",A,L,z),put:(A,L,z)=>O("PUT",A,L,z),patch:(A,L,z)=>O("PATCH",A,L,z),del:(A,L,z)=>O("DELETE",A,L,z)}}function $Q(_,$=process.env,D){let U=D?.credentials,g=iY(_,$,{...U?{credentials:U}:{}});if(g.misconfigured)throw Error(g.warning??`Client for '${_}' is misconfigured for the API client.`);if(g.transport==="sqlite"||!g.baseUrl)return{transport:"sqlite",client:null,resolution:g};let I=()=>{let j=nN(_,$,U);if(!j)throw Error(`Client for '${_}' resolved to the http transport but no API key is available any more. Looked at ${oz(_,$)}, then the environment. A credential file that was removed after this client was built is the usual cause.`);return j};return{transport:"http",client:_Q({name:_,baseUrl:g.baseUrl,apiKey:I,...D?.fetchImpl?{fetchImpl:D.fetchImpl}:{},...D?.headers?{headers:D.headers}:{},...D?.timeoutMs?{timeoutMs:D.timeoutMs}:{},...D?.retry!==void 0?{retry:D.retry}:{},...D?.sleepImpl?{sleepImpl:D.sleepImpl}:{}}),resolution:g}}function dN(_){let $=_.replace(/^\/+|\/+$/g,"");if(!$)throw Error("resource must be a non-empty path segment");return`/${$}`}function cN(_,$){if($===void 0||$===null||`${$}`.length===0)throw Error("id must be a non-empty string");return`${dN(_)}/${encodeURIComponent(String($))}`}function DQ(){let _=globalThis;if(_.crypto?.randomUUID)return _.crypto.randomUUID();return`idmp_${Date.now().toString(36)}_${Math.random().toString(36).slice(2,12)}`}function gQ(_){if(Array.isArray(_))return _;if(_&&typeof _==="object"){let $=_;for(let D of["items","data","results","rows","records"])if(Array.isArray($[D]))return $[D]}return[]}function UQ(_){if(_&&typeof _==="object"){let $=_;for(let D of["total","count","totalCount","total_count"])if(typeof $[D]==="number")return $[D]}return null}function IQ(_){if(_&&typeof _==="object"){let $=_;for(let D of["cursor","nextCursor","next_cursor","next"])if(typeof $[D]==="string")return $[D]}return null}function jQ(_,$){return{name:_,baseUrl:$.baseUrl,transport:$,async list(D,U={}){let g=await $.get(dN(D),U);return{items:gQ(g),total:UQ(g),cursor:IQ(g),raw:g}},async get(D,U,g={}){try{return await $.get(cN(D,U),g)}catch(I){if(I instanceof O0&&I.status===404)return null;throw I}},async create(D,U,g={}){let{idempotencyKey:I,...j}=g;return $.post(dN(D),U,{...j,idempotencyKey:I??DQ()})},async update(D,U,g,I={}){let{method:j="PATCH",idempotencyKey:N,...O}=I;return(j==="PUT"?$.put:$.patch)(cN(D,U),g,{...O,...N?{idempotencyKey:N}:{}})},async delete(D,U,g={}){try{await $.del(cN(D,U),void 0,g)}catch(I){if(I instanceof O0&&I.status===404)return;throw I}}}}function iN(_,$=process.env,D){let U=$Q(_,$,D);if(U.transport==="http")return{transport:"http",client:jQ(_,U.client)};return{transport:"sqlite",client:null}}function NQ(_){return _.toUpperCase().replace(/-/g,"_")}function pz(_){let $=NQ(_);return{modeKeys:[`HASNA_${$}_STORAGE_MODE`,`HASNA_${$}_MODE`,`${$}_STORAGE_MODE`,`${$}_MODE`],apiUrlKeys:[`HASNA_${$}_API_URL`,`${$}_API_URL`],apiKeyKeys:[`HASNA_${$}_API_KEY`,`${$}_API_KEY`]}}function vD(_){let $=_.trim().toLowerCase().replace(/-/g,"_");if($==="sqlite")return{mode:"sqlite"};if($==="postgres"||$==="postgresql")return{mode:"postgres"};throw Error(`Unknown storage mode '${_}'. The runtime-placement axis was removed; set sqlite for the on-box SQLite file or postgres for a PostgreSQL server (DATABASE_URL).`)}function rU(_){let $=_.trim().toLowerCase().replace(/-/g,"_");if($==="sqlite")return{mode:"sqlite"};if($==="postgres"||$==="postgresql")return{mode:"postgres"};throw Error(`Unknown storage mode '${_}'. The runtime-placement axis was removed; set sqlite for the on-box SQLite file or postgres for a PostgreSQL server (DATABASE_URL).`)}import Iw from"pg";class vU extends Error{scheme;port;constructor(_,$){super(_);this.name="KnowledgeNetworkGuardError",this.scheme=$.scheme,this.port=$.port}}function A0(_=process.env){return(_.NODE_ENV??"").trim().toLowerCase()==="test"}function ez(_){let $=_.split(".");if($.length!==4)return!1;if(!$.every((D)=>/^\d{1,3}$/.test(D)&&Number(D)<=255))return!1;return $[0]==="127"}function JQ(_){let $=_.trim().toLowerCase();if($.length===0)return!1;if($==="localhost"||$.endsWith(".localhost"))return!0;if(ez($))return!0;if(!$.startsWith("[")||!$.endsWith("]"))return!1;let D=$.slice(1,-1);if(D==="::1"||/^(0:){7}1$/.test(D))return!0;let U=D.split(":").pop()??"";if(/^(::ffff:|::)/.test(D)&&ez(U))return!0;return/^::(ffff:)?7f[0-9a-f]{2}:[0-9a-f]{1,4}$/.test(D)}function _S(_){if(typeof _==="string")return _;if(_ instanceof URL)return _.href;return _.url}function az(_,$=process.env){if(!A0($))return;let D=_S(_),U;try{U=new URL(D)}catch{throw new vU("knowledge: refused an outbound request with an unparseable target while NODE_ENV=test. Under test, only loopback requests are permitted.",{scheme:"unknown",port:""})}if(JQ(U.hostname))return;throw new vU(`knowledge: refused a non-loopback ${U.protocol.replace(":","")} request while NODE_ENV=test (target host withheld on purpose). This process resolved to the cloud backend under test, which means a read or write was about to leave the machine and reach the live store. Select the mode explicitly (HASNA_KNOWLEDGE_STORAGE_MODE=sqlite) or point the API URL at 127.0.0.1 for a hermetic test.`,{scheme:U.protocol.replace(":",""),port:U.port})}var PQ=new Set([301,302,303,307,308]),sz=5;function zQ(_,$){if($?.method)return $.method.toUpperCase();if(typeof _!=="string"&&!(_ instanceof URL))return _.method.toUpperCase();return"GET"}async function fU(_,$){if(az(_),!A0()||$?.redirect!==void 0)return fetch(_,$);let D=_S(_),U=zQ(_,$),g=$?.body,I=await fetch(_,{...$??{},redirect:"manual"});for(let j=0;PQ.has(I.status);j++){let N=I.headers.get("location");if(!N)return I;let O=new URL(N,D).href;if(az(O),j>=sz){let L=new URL(O);throw new vU(`knowledge: refused to follow more than ${sz} redirects while NODE_ENV=test (target host withheld on purpose). Under test the guard follows redirects itself so every hop is checked, and a chain this long is a loop, not a route.`,{scheme:L.protocol.replace(":",""),port:L.port})}if(I.status===303||(I.status===301||I.status===302)&&U!=="GET"&&U!=="HEAD")U="GET",g=void 0;let A={...$??{},method:U,redirect:"manual"};if(g===void 0)delete A.body;else A.body=g;I=await fetch(O,A),D=O}return I}var uU="knowledge",lN=pz(uU),o$=lN.modeKeys,xU=lN.apiUrlKeys,yU=lN.apiKeyKeys;function wU(_,$){return $.filter((D)=>(_[D]??"").trim().length>0)}function fD(_=process.env){let $=[...wU(_,xU),...wU(_,yU)],D=o$[0];for(let U of o$){let g=_[U]?.trim();if(!g)continue;let I;try{I=rU(g)}catch(N){let O=N instanceof Error?N.message:String(N);throw Error(`knowledge: ${U}=${g} is not a valid mode. ${O} Unset ${U} to use the default sqlite backend, or set ${U}=sqlite or ${U}=postgres.`)}let j=[];if(U!==D)j.push(`Using alias env ${U}; the canonical key is ${D}.`);if(I.mode==="sqlite"&&$.length>0)j.push(`${U}=sqlite pins the on-box store; ${$.join(", ")} are set but ignored.`);return{mode:I.mode,source:{kind:"env",name:U,value:g},pointer_env_present:$,pointer_ignored:I.mode==="sqlite"&&$.length>0,warning:j.length>0?j.join(" "):null}}return{mode:"sqlite",source:{kind:"default",name:null,value:null},pointer_env_present:$,pointer_ignored:$.length>0,warning:$.length>0?`${$.join(", ")} are set but do NOT select a backend: mode is sqlite by default. Set ${D}=postgres to route reads and writes to the API, or unset those vars to silence this note.`:null}}var SQ=["postgres"],WQ=["sqlite"],$S=new Map;function DS(_,$,D){let U=$===vD;if(U){let g=$S.get(_);if(g!==void 0)return g}for(let g of _)try{if($(g),U)$S.set(_,g);return g}catch{}throw Error(`knowledge: no known storage token is accepted by the installed @hasna/contracts (tried ${_.join(", ")}). The storage-mode enum has changed; add the new token to ${D} in src/knowledge-mode.ts.`)}function XQ(_=vD){return DS(SQ,_,"SERVER_MODE_CANDIDATES")}function RQ(_=vD){return DS(WQ,_,"LOCAL_MODE_CANDIDATES")}function GQ(_,$=vD){return _==="postgres"?XQ($):RQ($)}function tN(_,$){return{..._,[o$[0]]:GQ($)}}class gS extends Error{code="knowledge_mode_unset_with_api_url";constructor(_){let $=o$[0];super(`knowledge: ${_.join(", ")} names an API store, but no mode variable says to use it, so this command would silently read and write the on-box store instead. Set ${$}=postgres to use the API, or ${$}=sqlite to confirm you want the on-box store. Run 'knowledge mode' to see the full resolution.`);this.name="HalfConfiguredKnowledgeClientError"}}function US(_=process.env,$={}){let D=fD(_);if($.storePathOverridden)return D;if(D.source.kind!=="default")return D;let U=wU(_,xU);if(U.length===0)return D;throw new gS(U)}function IS(_=process.env){let $=fD(_);return{...$,store_transport:$.mode==="postgres"?"api":"local",api_key_present:wU(_,yU).length>0,network_guard_active:A0(_)}}function jS(_){return{fetchImpl:fU,...A0(_)?{retry:!1}:{}}}var G6="notes";class L0 extends Error{expected;current;code="version_conflict";constructor(_,$){super(`version_conflict: this edit was written against version ${_} but the stored entry is now at version ${$}. Nothing was written. Re-read the entry and re-apply only if the fields you are changing are untouched between the two versions.`);this.expected=_;this.current=$;this.name="KnowledgeVersionConflictError"}}function YQ(_){let $={};if(_.search)$.search=_.search;if(_.limit!==void 0)$.limit=_.limit;if(_.offset!==void 0)$.offset=_.offset;if(_.includeArchived||_.archivedOnly)$.includeArchived=!0;return $}function QQ(_){return{baseUrl:_.baseUrl,async list($={}){let D=$.limit??200,U=YQ({...$,limit:Math.min(Math.max(D,1),200)}),g=await _.list(G6,{query:U}),I=g.items;if($.archivedOnly)I=I.filter((j)=>j.archived===!0);if($.tag){let j=$.tag.toLowerCase();I=I.filter((N)=>(N.tags??[]).some((O)=>O.toLowerCase()===j))}return{items:I,total:g.total}},async get($){return _.get(G6,$)},async create($){return _.create(G6,{...$.id?{id:$.id}:{},title:$.title,content:$.content,url:$.url??null,tags:$.tags??[],...$.metadata?{metadata:$.metadata}:{}})},async update($,D,U={}){try{return await _.update(G6,$,D,{...U.expectedVersion!==void 0?{headers:{"if-match":String(U.expectedVersion)}}:{}})}catch(g){if(oN(g))return null;let I=TQ(g);if(I)throw I;throw g}},async delete($){let D=await _.get(G6,$);if(!D)return!1;return await _.delete(G6,D.id),!0},async listVersions($,D={}){try{return await _.transport.get(`/${G6}/${encodeURIComponent($)}/versions`,{query:{limit:D.limit,offset:D.offset}})}catch(U){if(oN(U))return null;throw U}},async getVersion($,D){try{return await _.transport.get(`/${G6}/${encodeURIComponent($)}/versions/${D}`)}catch(U){if(oN(U))return null;throw U}}}}function TQ(_){if(!_||typeof _!=="object")return null;if(_.status!==409)return null;let $=_.body,U=(typeof $==="string"?qQ($):$)??{};if(U.error!=="version_conflict")return null;return new L0(Number(U.expected??0),Number(U.current??0))}function qQ(_){try{return JSON.parse(_)}catch{return null}}function oN(_){return Boolean(_&&typeof _==="object"&&_.status===404)}function wD(_=process.env){let $=BQ(_);return $?QQ($):null}function BQ(_){if(fD(_).mode!=="postgres")return null;let $=iN(uU,tN(_,"postgres"),jS(_));if($.transport!=="http")return null;return $.client}function Y6(_=process.env){if(fD(_).mode!=="postgres")return!1;return iN(uU,tN(_,"postgres"),jS(_)).transport==="http"}async function hU(_){let D=[];for(let U=0;;U+=200){let{items:g}=await _.list({includeArchived:!0,limit:200,offset:U});if(D.push(...g),g.length<200)break;if(U>1e5)break}return D}class eN extends Error{location;code="version_history_unsupported";constructor(_){super(`Version history is not kept by the local JSON knowledge store (${_}). It has no version line, so an empty history here would be a claim, not a measurement. Entry versioning lives in the Postgres-backed store: point this CLI at it (HASNA_KNOWLEDGE_STORAGE_MODE=postgres plus the API url/key) and re-run.`);this.location=_;this.name="VersionHistoryUnsupportedError"}}function pN(_,$){return _.id===$||_.short_id===$}class NS{storePath;kind="local";supportsVersions=!1;constructor(_){this.storePath=_}async listVersions(){throw new eN(this.storePath)}async getVersion(){throw new eN(this.storePath)}get location(){return this.storePath}get exists(){return KQ(this.storePath)}async listAll(){let _=I0(this.storePath);return{items:_.items,exists:_.exists}}async get(_){return I0(this.storePath).items.find((D)=>pN(D,_))??null}async create(_){return v$(this.storePath,()=>{let $=CD(this.storePath),D=new Date().toISOString(),U=_.id??Cz(),g={id:U,short_id:rz(U),title:_.title,content:_.content,url:_.url??null,tags:_.tags??[],metadata:_.metadata??{},archived:!1,created_at:D,updated_at:D,version:1};return $.items.push(g),t$(this.storePath,$),g},{createParent:!0})}async update(_,$,D={}){return v$(this.storePath,()=>{let U=CD(this.storePath),g=U.items.findIndex((N)=>pN(N,_));if(g===-1)return null;let I=U.items[g],j=I.version??1;if(D.expectedVersion!==void 0&&D.expectedVersion!==j)throw new L0(D.expectedVersion,j);if($.title!==void 0)I.title=$.title;if($.content!==void 0)I.content=$.content;if($.url!==void 0)I.url=$.url;if($.tags!==void 0)I.tags=$.tags;if($.metadata!==void 0)I.metadata=$.metadata;if($.archived!==void 0)I.archived=$.archived;return I.updated_at=new Date().toISOString(),I.version=j+1,U.items[g]=I,t$(this.storePath,U),I},{createParent:!0})}async delete(_){return v$(this.storePath,()=>{let $=CD(this.storePath),D=$.items.length;$.items=$.items.filter((g)=>!pN(g,_));let U=D!==$.items.length;if(U)t$(this.storePath,$);return U},{createParent:!0})}async deleteMany(_){if(_.length===0)return 0;let $=new Set(_);return v$(this.storePath,()=>{let D=CD(this.storePath),U=D.items.length;D.items=D.items.filter((I)=>!$.has(I.id)&&!(I.short_id!=null&&$.has(I.short_id)));let g=U-D.items.length;if(g>0)t$(this.storePath,D);return g},{createParent:!0})}}class ES{cloud;kind="api";exists=!0;supportsVersions=!0;constructor(_){this.cloud=_}async listVersions(_,$={}){return this.cloud.listVersions(_,$)}async getVersion(_,$){return this.cloud.getVersion(_,$)}get location(){return this.cloud.baseUrl}async listAll(){return{items:await hU(this.cloud),exists:!0}}async get(_){return this.cloud.get(_)}async create(_){return this.cloud.create({..._.id?{id:_.id}:{},title:_.title,content:_.content,url:_.url??null,tags:_.tags??[],..._.metadata?{metadata:_.metadata}:{}})}async update(_,$,D={}){return this.cloud.update(_,$,{expectedVersion:D.expectedVersion})}async delete(_){return this.cloud.delete(_)}async deleteMany(_){let $=0;for(let D of _)if(await this.cloud.delete(D))$+=1;return $}}function cU(_){let $=_.storePathOverridden?null:wD(_.env??process.env);if($)return new ES($);return new NS(_.storePath)}function OS(_){let $=_??"";if($==="")return[];return $.replace(/\n$/,"").split(` `)}var aN=5000;function VQ(_,$){let D=OS(_),U=OS($);if(D.length>aN||U.length>aN)throw Error(`Refusing to line-diff ${Math.max(D.length,U.length)} lines (limit ${aN}). Fetch the two versions and diff them with a dedicated tool.`);let g=Array.from({length:D.length+1},()=>Array(U.length+1).fill(0));for(let O=D.length-1;O>=0;O-=1)for(let A=U.length-1;A>=0;A-=1)g[O][A]=D[O]===U[A]?g[O+1][A+1]+1:Math.max(g[O+1][A],g[O][A+1]);let I=[],j=0,N=0;while(j=g[j][N+1])I.push({op:"remove",from_line:j+1,to_line:null,text:D[j]}),j+=1;else I.push({op:"add",from_line:null,to_line:N+1,text:U[N]}),N+=1;while(j{if(!FQ(_[N],$[N]))D.push({field:N,from:_[N]??null,to:$[N]??null})};U("title"),U("url"),U("tags"),U("metadata"),U("archived");let g=VQ(_.content,$.content),I=g.filter((N)=>N.op==="add").length,j=g.filter((N)=>N.op==="remove").length;return{identical:D.length===0&&I===0&&j===0,fields:D,content:g,added:I,removed:j}}function LS(_,$,D){let U=[`--- ${$}`,`+++ ${D}`];if(_.identical)return U.push("(no changes)"),U.join(` `);for(let g of _.fields)U.push(`~ ${g.field}: ${JSON.stringify(g.from)} -> ${JSON.stringify(g.to)}`);if(_.added===0&&_.removed===0){if(_.fields.length>0)U.push("(content unchanged)")}else{U.push(`@@ content +${_.added} -${_.removed} @@`);for(let g of _.content){let I=g.op==="add"?"+":g.op==="remove"?"-":" ";U.push(`${I}${g.text}`)}}return U.join(` `)}import{Database as JS}from"bun:sqlite";function nU(_="catalog"){if(Y6()){let $=o$[0];throw Error(`knowledge: ${_} builds/reads the on-box sqlite RAG catalog (source ingestion, chunk embeddings, wiki compilation, cross-machine sync, machine registry). That local indexing pipeline is not available in cloud mode. In cloud mode the shared corpus is the cloud knowledge-items: 'add/list/get/update/delete' item commands AND 'search/ask/build/context' over that shared corpus all route to the cloud. Set ${$}=local `+"(or unset it \u2014 local is the default) to use the full local catalog pipeline; run 'knowledge mode' to see "+"which variable selected the current backend.")}}var MQ="porter unicode61 remove_diacritics 2",PS=` diff --git a/dist/index.js b/dist/index.js index 8139df4..840aa37 100644 --- a/dist/index.js +++ b/dist/index.js @@ -16458,7 +16458,7 @@ function resolveKnowledgeModeSelection(env = process.env) { normalized = normalizeStorageMode3(value); } catch (error) { const message = error instanceof Error ? error.message : String(error); - throw new Error(`knowledge: ${name}=${value} is not a valid mode. ${message}`); + throw new Error(`knowledge: ${name}=${value} is not a valid mode. ${message} ` + `Unset ${name} to use the default sqlite backend, or set ${name}=sqlite or ${name}=postgres.`); } const warnings = []; if (name !== canonicalModeKey) { diff --git a/dist/storage.js b/dist/storage.js index 11e7cae..b214cfc 100644 --- a/dist/storage.js +++ b/dist/storage.js @@ -1588,7 +1588,7 @@ function resolveKnowledgeModeSelection(env = process.env) { normalized = normalizeStorageMode3(value); } catch (error) { const message = error instanceof Error ? error.message : String(error); - throw new Error(`knowledge: ${name}=${value} is not a valid mode. ${message}`); + throw new Error(`knowledge: ${name}=${value} is not a valid mode. ${message} ` + `Unset ${name} to use the default sqlite backend, or set ${name}=sqlite or ${name}=postgres.`); } const warnings = []; if (name !== canonicalModeKey) { diff --git a/src/knowledge-mode.ts b/src/knowledge-mode.ts index adddece..f3a0a67 100644 --- a/src/knowledge-mode.ts +++ b/src/knowledge-mode.ts @@ -122,7 +122,10 @@ export function resolveKnowledgeModeSelection(env: NodeJS.ProcessEnv = process.e normalized = normalizeVendoredMode(value); } catch (error) { const message = error instanceof Error ? error.message : String(error); - throw new Error(`knowledge: ${name}=${value} is not a valid mode. ${message}`); + throw new Error( + `knowledge: ${name}=${value} is not a valid mode. ${message} ` + + `Unset ${name} to use the default sqlite backend, or set ${name}=sqlite or ${name}=postgres.`, + ); } const warnings: string[] = []; if (name !== canonicalModeKey) { diff --git a/tests/cli.test.ts b/tests/cli.test.ts index 4d77274..8ff71f8 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -3778,6 +3778,38 @@ describe('a half-configured CLI fails loudly instead of reading the wrong store' expect(combined).not.toContain(FAKE_API_URL); }); + test('REGRESSION: a removed cloud mode names the unset migration path', () => { + const result = runCliNoMode( + ['list', '--tag', 'convention', '--limit', '40', '--sort', 'created', '--desc', '--json'], + { + ...sandboxHome(), + HASNA_KNOWLEDGE_STORAGE_MODE: 'cloud', + }, + ); + const stderr = decode(result.stderr); + + expect(result.exitCode).not.toBe(0); + expect(stderr).toContain('HASNA_KNOWLEDGE_STORAGE_MODE=cloud'); + expect(stderr).toContain('Unset HASNA_KNOWLEDGE_STORAGE_MODE'); + }); + + test('the exact convention read defaults to sqlite after the removed mode is absent', () => { + const args = ['list', '--tag', 'convention', '--limit', '40', '--sort', 'created', '--desc', '--json']; + const result = runCliNoMode(args, sandboxHome()); + expect(result.exitCode).toBe(0); + expect((JSON.parse(decode(result.stdout)) as { ok: boolean }).ok).toBe(true); + }); + + test('the exact convention read still succeeds with explicit sqlite', () => { + const args = ['list', '--tag', 'convention', '--limit', '40', '--sort', 'created', '--desc', '--json']; + const result = runCliNoMode(args, { + ...sandboxHome(), + HASNA_KNOWLEDGE_STORAGE_MODE: 'sqlite', + }); + expect(result.exitCode).toBe(0); + expect((JSON.parse(decode(result.stdout)) as { ok: boolean }).ok).toBe(true); + }); + test('`mode` still answers in the environment the guard rejects', () => { // The command whose entire job is explaining this state must survive it, // or the error message points at a diagnostic that also fails.