From 01ce5a747c0121d27c3582cf26510e632fd62148 Mon Sep 17 00:00:00 2001 From: Andrei Hasna Date: Sat, 8 Aug 2026 01:08:14 +0300 Subject: [PATCH] fix(mode): explain stale cloud storage migration Keep removed cloud placement values invalid while telling operators how to return to the sqlite default or select postgres. Add exact CLI regressions for the default convention read and explicit sqlite control. Agent: cornificia --- bin/knowledge-mcp.js | 2 +- bin/knowledge.js | 2 +- dist/index.js | 2 +- dist/storage.js | 2 +- src/knowledge-mode.ts | 5 ++++- tests/cli.test.ts | 32 ++++++++++++++++++++++++++++++++ 6 files changed, 40 insertions(+), 5 deletions(-) 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.