diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 679cf44..09c4e14 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,18 @@ jobs: - name: Setup Bun uses: oven-sh/setup-bun@v2 with: - bun-version: 1.3.13 + # LOAD-BEARING for `Verify generated artifacts`, not just a test-runtime choice. + # That step rebuilds bin/ and dist/ and compares the result BYTE-FOR-BYTE against + # what is committed, so it passes only when this version equals the bun that built + # the committed bundles. Measured: at 19738de the committed bundles are bun 1.3.14 + # output, and rebuilding them under bun 1.3.13 drifts by 4 lines in + # bin/knowledge-mcp.js and dist/index.js — bun 1.3.13 keeps the empty `else {}` + # blocks that 1.3.14's dead-code elimination collapses. Same shape both directions, + # so this is a bun minifier/DCE difference between patch releases, NOT zod codegen + # drift, and bun.lock cannot pin it (a lockfile pins dependencies, not the bun + # binary). Bumping or lowering this line without rebuilding bin/ and dist/ in the + # same commit turns the gate red. Keep both jobs on the same version. + bun-version: 1.3.14 - name: Setup Node if: matrix.runtime == 'node' @@ -34,7 +45,7 @@ jobs: run: bun test - name: Verify generated artifacts - run: bun run build && bun scripts/verify-generated-artifacts.mjs + run: bun run verify:generated test-matrix: strategy: @@ -46,6 +57,8 @@ jobs: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 with: - bun-version: 1.3.13 + # Must match the `test` job above — see the note there on why this version is + # load-bearing for the generated-artifact byte comparison. + bun-version: 1.3.14 - run: bun install - run: bun test --timeout 20000 diff --git a/bin/knowledge-mcp.js b/bin/knowledge-mcp.js index 17a27c0..3b48598 100755 --- a/bin/knowledge-mcp.js +++ b/bin/knowledge-mcp.js @@ -11250,7 +11250,7 @@ function finalize(ctx, schema) { result.$schema = "http://json-schema.org/draft-07/schema#"; } else if (ctx.target === "draft-04") { result.$schema = "http://json-schema.org/draft-04/schema#"; - } else if (ctx.target === "openapi-3.0") {} else {} + } else if (ctx.target === "openapi-3.0") {} if (ctx.external?.uri) { const id = ctx.external.registry.get(schema)?.id; if (!id) @@ -11511,7 +11511,7 @@ var formatMap, stringProcessor = (schema, ctx, _json, _params) => { if (val === undefined) { if (ctx.unrepresentable === "throw") { throw new Error("Literal `undefined` cannot be represented in JSON Schema"); - } else {} + } } else if (typeof val === "bigint") { if (ctx.unrepresentable === "throw") { throw new Error("BigInt literals cannot be represented in JSON Schema"); @@ -15025,6 +15025,7 @@ var package_default = { "bin", "dist", "scripts/apply-cloud-migrations.mjs", + "scripts/lib/remote-temp-dir.mjs", "scripts/smoke-machine-sync-release.mjs", "scripts/smoke-machines-adapter.mjs", "scripts/smoke-open-files-installed-boundary.mjs", @@ -15051,7 +15052,7 @@ var package_default = { "smoke:open-files-installed-boundary": "bun scripts/smoke-open-files-installed-boundary.mjs", "migrate:cloud": "bun scripts/apply-cloud-migrations.mjs", serve: "bun src/serve-entry.ts", - "verify:generated": "bun run build && bun scripts/verify-generated-artifacts.mjs", + "verify:generated": "bun scripts/verify-generated-artifacts.mjs", build: "rm -rf dist && bun build --target=bun --outfile=bin/knowledge.js --minify --external pg --external @hasna/machines --external @hasna/machines/consumer --external @aws-sdk/client-s3 --external @aws-sdk/credential-providers --external ai --external @ai-sdk/openai --external @ai-sdk/anthropic --external @ai-sdk/deepseek src/cli.ts && bun build --target=bun --outfile=bin/knowledge-mcp.js --external pg --external @hasna/machines --external @hasna/machines/consumer --external @modelcontextprotocol/sdk --external @aws-sdk/client-s3 --external @aws-sdk/credential-providers --external ai --external @ai-sdk/openai --external @ai-sdk/anthropic --external @ai-sdk/deepseek src/mcp.js && bun build --target=bun --outfile=bin/knowledge-serve.js --external pg --external @hasna/machines --external @hasna/machines/consumer --external @aws-sdk/client-s3 --external @aws-sdk/credential-providers --external ai --external @ai-sdk/openai --external @ai-sdk/anthropic --external @ai-sdk/deepseek src/serve-entry.ts && bun build ./src/index.ts ./src/storage.ts ./src/serve.ts --outdir ./dist --target bun --external pg --external @hasna/machines --external @hasna/machines/consumer --external @aws-sdk/client-s3 --external @aws-sdk/credential-providers --external ai --external @ai-sdk/openai --external @ai-sdk/anthropic --external @ai-sdk/deepseek && bun scripts/strip-generated-trailing-whitespace.mjs && bunx tsc -p tsconfig.build.json", prepublishOnly: "bun run build && node scripts/validate-public-package.mjs" }, diff --git a/bin/knowledge.js b/bin/knowledge.js index 67e0011..0cbd61b 100755 --- a/bin/knowledge.js +++ b/bin/knowledge.js @@ -1204,7 +1204,7 @@ Pages should be concise, cited, and organized for both humans and agents. ) `)}import jc from"pg";var FR=OB(IR(),1),{program:sc,createCommand:_i,createArgument:$i,createOption:Ui,CommanderError:Di,InvalidArgumentError:Ji,InvalidOptionArgumentError:ji,Command:MR,Argument:Wi,Option:Gi,Help:Xi}=FR.default;import{chmod as Sz,mkdir as mv,readFile as dv,rename as cv,writeFile as wR}from"fs/promises";import{Buffer as xR}from"buffer";import{existsSync as rR}from"fs";import{homedir as iv}from"os";import{join as gD}from"path";import{createHmac as ev,timingSafeEqual as Ei}from"crypto";import{randomUUID as $y}from"crypto";import{spawn as Uy}from"child_process";import{randomUUID as Xy}from"crypto";function Zv(_,$){return $.split(".").reduce((U,J)=>{if(U&&typeof U==="object"&&J in U)return U[J];return},_)}function vv(_,$){let U=[],J=(j)=>{if(!U.some((W)=>Object.is(W,j)))U.push(j)};if($.includes(".")&&$ in _)J(_[$]);let D=Zv(_,$);if(D!==void 0||!$.includes("."))J(D);return U}function yv(_,$={}){let U="";for(let J=0;J<_.length;J+=1){let D=_[J];if(D==="*")if(_[J+1]==="*")U+=".*",J+=1;else U+=$.segmentSafe?"[^/]*":".*";else U+=D.replace(/[|\\{}()[\]^$+?.]/g,"\\$&")}return new RegExp(`^${U}$`)}function qD(_,$,U={}){if($===void 0)return!0;if(_===void 0)return!1;return(Array.isArray($)?$:[$]).some((D)=>yv(D,U).test(_))}function PR(_,$){if(!$)return!0;return Object.entries($).every(([U,J])=>{let D=vv(_,U);return hv(D,J,U)})}function hv(_,$,U){if(uv($))return!_.some((J)=>kR(J,$.not,U));return _.some((J)=>kR(J,$,U))}function kR(_,$,U){if(typeof $==="string"||Array.isArray($))return xv(_).some((J)=>qD(J,$,{segmentSafe:U.endsWith("_path")||U.endsWith(".path")}));if(Array.isArray(_))return _.some((J)=>J===$);return _===$}function xv(_){if(_===void 0)return[];if(Array.isArray(_))return _.flatMap(($)=>rv($)?[String($)]:[]);return[String(_)]}function rv(_){return _===null||typeof _==="string"||typeof _==="number"||typeof _==="boolean"}function uv(_){return Boolean(_&&typeof _==="object"&&!Array.isArray(_)&&"not"in _)}function nv(_,$){return qD(_.source,$.source)&&qD(_.type,$.type)&&qD(_.subject,$.subject)&&qD(_.severity,$.severity)&&PR(_.data,$.data)&&PR(_.metadata,$.metadata)}function SR(_,$){if(!_.enabled)return!1;if(!_.filters||_.filters.length===0)return!0;return _.filters.some((U)=>nv($,U))}var $G="HASNA_EVENTS_DIR",UG="HASNA_EVENTS_HOME",Cz="local-json-v1:",lv=100,ov=1000;function uR(_){return _||process.env[$G]||process.env[UG]||gD(iv(),".hasna","events")}function pv(){if(process.env[$G])return $G;if(process.env[UG])return UG;return null}class DG{dataDir;runtime;channelsPath;eventsPath;deliveriesPath;constructor(_=uR()){this.dataDir=_,this.runtime=tv(_),this.channelsPath=gD(_,"channels.json"),this.eventsPath=gD(_,"events.json"),this.deliveriesPath=gD(_,"deliveries.json")}async init(){await mv(this.dataDir,{recursive:!0,mode:448}),await Sz(this.dataDir,448).catch(()=>{return}),await this.ensureArrayFile(this.channelsPath),await this.ensureArrayFile(this.eventsPath),await this.ensureArrayFile(this.deliveriesPath)}async addChannel(_){await this.init();let $=await this.readJson(this.channelsPath,[]),U=$.findIndex((J)=>J.id===_.id);if(U>=0)$[U]={..._,createdAt:$[U].createdAt,updatedAt:new Date().toISOString()};else $.push(_);return await this.writeJson(this.channelsPath,$),U>=0?$[U]:_}async listChannels(){return await this.init(),this.readJson(this.channelsPath,[])}async getChannel(_){return(await this.listChannels()).find((U)=>U.id===_)}async removeChannel(_){await this.init();let $=await this.readJson(this.channelsPath,[]),U=$.filter((J)=>J.id!==_);return await this.writeJson(this.channelsPath,U),U.length!==$.length}async appendEvent(_){await this.init();let $=await this.readJson(this.eventsPath,[]);return $.push(_),await this.writeJson(this.eventsPath,$),_}async appendEventOnce(_,$={}){await this.init();let U=await this.readJson(this.eventsPath,[]);if($.dedupe!==!1){let D=TR(U,{id:_.id,dedupeKey:_.dedupeKey});if(D)return{event:D,stored:!1,deduped:!0,identity:{id:D.id,dedupeKey:D.dedupeKey}}}return U.push(_),await this.writeJson(this.eventsPath,U),{event:_,stored:!0,deduped:!1,identity:{id:_.id,dedupeKey:_.dedupeKey}}}async listEvents(_={}){await this.init();let $=await this.readJson(this.eventsPath,[]);return fR($,_)}async listEventsPage(_={}){await this.init();let $=await this.readJson(this.eventsPath,[]),U=fR($,{eventId:_.eventId,source:_.source,type:_.type}),J=JG(_.cursor,_),D=jG(_.limit),j=U.slice(J,J+D),W=J+j.length,G=W{return})}async readJson(_,$){try{let U=await dv(_,"utf-8");if(!U.trim())return $;return JSON.parse(U)}catch(U){if(U.code==="ENOENT")return $;throw U}}async writeJson(_,$){let U=`${_}.${process.pid}.${Date.now()}.tmp`;await wR(U,`${JSON.stringify($,null,2)} -`,{encoding:"utf-8",mode:384}),await cv(U,_),await Sz(_,384).catch(()=>{return})}}function tv(_=uR()){return{mode:"local-files",name:"json-events-store",remote:!1,localFiles:!0,localSqlite:!1,postgres:!1,s3:!1,aws:!1,durable:!0,idempotency:"best-effort-local",replayCursors:!0,description:`Local JSON files in ${_}; no SQLite, Postgres, S3, or AWS runtime is configured by this store.`}}function nR(_,$={}){if(!Number.isInteger(_)||_<0)throw Error(`Invalid event cursor offset: ${_}`);let U={offset:_,eventId:$.eventId,source:$.source,type:$.type};return`${Cz}${xR.from(JSON.stringify(U),"utf-8").toString("base64url")}`}function JG(_,$={}){if(!_)return 0;if(!_.startsWith(Cz))throw Error(`Invalid local JSON event cursor: ${_}`);let U=_.slice(Cz.length),J;try{J=JSON.parse(xR.from(U,"base64url").toString("utf-8"))}catch{throw Error(`Invalid local JSON event cursor: ${_}`)}let D=J.offset;if(!Number.isInteger(D)||D<0)throw Error(`Invalid local JSON event cursor: ${_}`);return wz("eventId",J.eventId,$.eventId),wz("source",J.source,$.source),wz("type",J.type,$.type),D}function jG(_){if(_===void 0)return lv;if(!Number.isInteger(_)||_<1)throw Error(`Event page limit must be a positive integer, got ${_}`);return Math.min(_,ov)}function fR(_,$){let U=_;if($.eventId)U=U.filter((J)=>J.id===$.eventId);if($.source)U=U.filter((J)=>J.source===$.source);if($.type)U=U.filter((J)=>J.type===$.type);if($.cursor){let J=JG($.cursor,$);U=U.slice(J)}if($.limit!==void 0)U=U.slice(0,jG($.limit));return U}function wz(_,$,U){if($!==U)throw Error(`Local JSON event cursor ${_} filter mismatch`)}function TR(_,$){return _.find((U)=>$.id!==void 0&&U.id===$.id||$.dedupeKey!==void 0&&U.dedupeKey===$.dedupeKey)}async function av(_){let $=new DG(_);await $.init();let[U,J,D]=await Promise.all([$.listChannels(),$.listEvents(),$.listDeliveries()]),j=U.reduce((W,G)=>{return W[G.transport]=(W[G.transport]??0)+1,W},{});return{service:"events",schemaVersion:"1.0",dataDir:$.dataDir,storage:$.runtime,env:{primary:$G,fallback:UG,active:pv()},files:{channels:fz($.dataDir,"channels.json",U.length),events:fz($.dataDir,"events.json",J.length),deliveries:fz($.dataDir,"deliveries.json",D.length)},counts:{channels:U.length,enabledChannels:U.filter((W)=>W.enabled).length,disabledChannels:U.filter((W)=>!W.enabled).length,events:J.length,deliveries:D.length},transports:j,safety:{includesEventPayloads:!1,includesWebhookSecrets:!1,listOutputsRedactSecrets:!0,statusOutputIsMetadataOnly:!0}}}function fz(_,$,U){let J=gD(_,$);return{path:J,exists:rR(J),records:U}}function sv(_,$){return`${_}.${$}`}function _y(_,$,U){return`sha256=${ev("sha256",_).update(sv($,U)).digest("hex")}`}function L6(){return new Date().toISOString()}function ED(_,$=4096){return _.length>$?`${_.slice(0,$)}...`:_}function Dy(_,$){if(!$.webhook)throw Error(`Channel ${$.id} has no webhook config`);let U=JSON.stringify(_),J=_.time,D={"Content-Type":"application/json","User-Agent":"@hasna/events","X-Hasna-Event-Id":_.id,"X-Hasna-Event-Type":_.type,"X-Hasna-Timestamp":J,...$.webhook.headers};if($.webhook.secret)D["X-Hasna-Signature"]=_y($.webhook.secret,J,U);return{body:U,headers:D}}async function Jy(_,$,U={}){if(!$.webhook)throw Error(`Channel ${$.id} has no webhook config`);let J=L6(),{body:D,headers:j}=Dy(_,$),W=new AbortController,G=setTimeout(()=>W.abort(),$.webhook.timeoutMs??15000);try{let X=await(U.fetchImpl??fetch)($.webhook.url,{method:"POST",headers:j,body:D,signal:W.signal}),N=ED(await X.text());return{attempt:1,status:X.ok?"success":"failed",startedAt:J,completedAt:L6(),responseStatus:X.status,responseBody:N,error:X.ok?void 0:`Webhook returned HTTP ${X.status}`}}catch(X){return{attempt:1,status:"failed",startedAt:J,completedAt:L6(),error:X instanceof Error?X.message:String(X)}}finally{clearTimeout(G)}}async function jy(_,$){if(!$.command)throw Error(`Channel ${$.id} has no command config`);let U=L6(),J=JSON.stringify(_),D={...process.env,...$.command.env,HASNA_CHANNEL_ID:$.id,HASNA_EVENT_ID:_.id,HASNA_EVENT_TYPE:_.type,HASNA_EVENT_SOURCE:_.source,HASNA_EVENT_SUBJECT:_.subject??"",HASNA_EVENT_SEVERITY:_.severity,HASNA_EVENT_TIME:_.time,HASNA_EVENT_DEDUPE_KEY:_.dedupeKey??"",HASNA_EVENT_SCHEMA_VERSION:_.schemaVersion,HASNA_EVENT_JSON:J};return new Promise((j)=>{let W=Uy($.command.command,$.command.args??[],{cwd:$.command.cwd,env:D,stdio:["pipe","pipe","pipe"]}),G="",X="",N=setTimeout(()=>W.kill("SIGTERM"),$.command.timeoutMs??15000);W.stdin.end(J),W.stdout.on("data",(z)=>{G+=z.toString()}),W.stderr.on("data",(z)=>{X+=z.toString()}),W.on("error",(z)=>{clearTimeout(N),j({attempt:1,status:"failed",startedAt:U,completedAt:L6(),stdout:ED(G),stderr:ED(X),error:z.message})}),W.on("close",(z,q)=>{clearTimeout(N);let g=z===0;j({attempt:1,status:g?"success":"failed",startedAt:U,completedAt:L6(),stdout:ED(G),stderr:ED(X),error:g?void 0:`Command exited with ${q?`signal ${q}`:`code ${z}`}`})})})}async function Wy(_,$,U={}){if($.transport==="webhook")return Jy(_,$,U);if($.transport==="command")return jy(_,$);return{attempt:1,status:"skipped",startedAt:L6(),completedAt:L6(),error:`Unsupported transport: ${$.transport}`}}function CR(_,$,U){let J=U.some((D)=>D.status==="success")?"success":U.every((D)=>D.status==="skipped")?"skipped":"failed";return{id:$y(),eventId:_.id,channelId:$.id,transport:$.transport,status:J,attempts:U,createdAt:U[0]?.startedAt??L6(),completedAt:U.at(-1)?.completedAt??L6()}}class mR extends Error{eventType;issues;constructor(_,$){let U=$.map((J)=>`${J.path||""}: ${J.message}`).join("; ");super(`Event validation failed for type "${_}": ${U}`);this.name="EventValidationError",this.eventType=_,this.issues=$}}class dR{definitions=new Map;register(_){return this.definitions.set(_.type,_),this}unregister(_){return this.definitions.delete(_)}has(_){return this.definitions.has(_)}get(_){return this.definitions.get(_)}list(){return[...this.definitions.values()]}validateEvent(_){let $=this.definitions.get(_.type);if(!$)return{ok:!0};return $.validate(_.data,_)}assertEventValid(_){let $=this.validateEvent(_);if(!$.ok)throw new mR(_.type,$.issues)}}var Gy=new dR;function Tz(_){return{id:_.id??Xy(),source:_.source,type:_.type,time:Qy(_.time),subject:_.subject,severity:_.severity??"info",data:_.data??{},message:_.message,dedupeKey:_.dedupeKey,schemaVersion:_.schemaVersion??"1.0",metadata:_.metadata??{}}}class cR{store;redactors;transportOptions;catalog;validateCatalogTypes;constructor(_={}){this.store=_.store??new DG(_.dataDir),this.redactors=_.redactors??[],this.transportOptions={fetchImpl:_.fetchImpl},this.catalog=_.catalog??Gy,this.validateCatalogTypes=_.validateCatalogTypes??!1}async addChannel(_){let $=new Date().toISOString();return this.store.addChannel({..._,createdAt:_.createdAt??$,updatedAt:_.updatedAt??$})}async listChannels(){return this.store.listChannels()}async removeChannel(_){return this.store.removeChannel(_)}async emit(_,$={}){let U=$.redactSensitiveData===!1?Tz(_):Yy(Tz(_));if($.validate??this.validateCatalogTypes)this.catalog.assertEventValid(U);let J=await this.appendEvent(U,{dedupe:$.dedupe!==!1});if(J.deduped)return{event:J.event,deliveries:[],deduped:!0};let D=$.deliver===!1?[]:await this.deliver(J.event);return{event:J.event,deliveries:D,deduped:!1}}async listEvents(_={}){if(Object.keys(_).length===0)return this.store.listEvents();return ZR(await this.store.listEvents(),_)}async listEventsPage(_={}){if(this.store.listEventsPage)return this.store.listEventsPage(_);let $=ZR(await this.store.listEvents(),{eventId:_.eventId,source:_.source,type:_.type}),U=JG(_.cursor,_),J=jG(_.limit),D=$.slice(U,U+J),j=U+D.length,W=j<$.length;return{events:D,cursor:_.cursor,nextCursor:W?nR(j,_):void 0,hasMore:W}}async listDeliveries(){return this.store.listDeliveries()}async deliver(_){let U=(await this.store.listChannels()).filter((D)=>SR(D,_)),J=[];for(let D of U){let j=await this.applyRedaction(_,D),W=await this.deliverWithRetry(j,D);await this.store.appendDelivery(W),J.push(W)}return J}async matchChannel(_,$={}){let U=await this.store.getChannel(_);if(!U)throw Error(`Channel not found: ${_}`);let J=Tz({source:$.source??"hasna.events",type:$.type??"events.test",subject:$.subject??_,severity:$.severity??"info",data:$.data??{test:!0},message:$.message??"Hasna events test delivery",dedupeKey:$.dedupeKey,schemaVersion:$.schemaVersion,metadata:$.metadata,time:$.time,id:$.id}),D=SR(U,J);return{channelId:U.id,matched:D,event:J,filters:U.filters,reason:D?void 0:U.enabled?"event did not match channel filters":"channel is disabled"}}async testChannel(_,$={},U={}){let J=await this.store.getChannel(_);if(!J)throw Error(`Channel not found: ${_}`);let D=await this.matchChannel(_,$),j=D.event;if(U.honorFilters&&!D.matched){let X=new Date().toISOString(),N=CR(j,J,[{attempt:1,status:"skipped",startedAt:X,completedAt:X,error:D.reason}]);return N.metadata={reason:"filter_mismatch"},await this.store.appendDelivery(N),N}let W=await this.applyRedaction(j,J),G=await this.deliverWithRetry(W,J);return await this.store.appendDelivery(G),G}async replay(_={}){let $=_.cursor||_.limit!==void 0?await this.listEventsPage(_):{events:await this.listEvents(_),hasMore:!1};if(_.dryRun)return{events:$.events,deliveries:[],cursor:$.cursor,nextCursor:$.nextCursor,hasMore:$.hasMore};let U=[];for(let J of $.events)U.push(...await this.deliver(J));return{events:$.events,deliveries:U,cursor:$.cursor,nextCursor:$.nextCursor,hasMore:$.hasMore}}async appendEvent(_,$){if(this.store.appendEventOnce)return this.store.appendEventOnce(_,{dedupe:$.dedupe});if($.dedupe){let J=await this.store.findEventByIdentity({id:_.id,dedupeKey:_.dedupeKey});if(J)return{event:J,stored:!1,deduped:!0,identity:{id:J.id,dedupeKey:J.dedupeKey}}}let U=await this.store.appendEvent(_);return{event:U,stored:!0,deduped:!1,identity:{id:U.id,dedupeKey:U.dedupeKey}}}async applyRedaction(_,$){let U=Ny(_,$.redact?.paths??[],$.redact?.replacement??"[REDACTED]");for(let J of this.redactors)U=await J(U,$);return U}async deliverWithRetry(_,$){let U=Ly($.retry),J=[];for(let D=0;D[U,lR(U)?"[REDACTED]":J]));return $}function Oy(_){return _.map(iR)}function Yy(_,$="[REDACTED]"){return Zz(_,$)}function lR(_){return/secret|token|password|api[_-]?key|authorization/i.test(_)}function Zz(_,$){if(Array.isArray(_))return _.map((U)=>Zz(U,$));if(!_||typeof _!=="object")return _;return Object.fromEntries(Object.entries(_).map(([U,J])=>[U,lR(U)?$:Zz(J,$)]))}function zy(_,$,U){let J=$.split("."),D=_;for(let W of J.slice(0,-1)){let G=D[W];if(!G||typeof G!=="object")return;D=G}let j=J.at(-1);if(j&&j in D)D[j]=U}function ZR(_,$){let U=_;if($.eventId)U=U.filter((J)=>J.id===$.eventId);if($.source)U=U.filter((J)=>J.source===$.source);if($.type)U=U.filter((J)=>J.type===$.type);if($.cursor)U=U.slice(JG($.cursor,$));if($.limit!==void 0)U=U.slice(0,jG($.limit));return U}function Qy(_){if(!_)return new Date().toISOString();return _ instanceof Date?_.toISOString():_}function Ly(_){return{maxAttempts:Math.max(1,_?.maxAttempts??1),backoffMs:Math.max(0,_?.backoffMs??250),multiplier:Math.max(1,_?.multiplier??2)}}function _G(_,$,U=!1){if(!_?.length)return;let J={};for(let D of _){let j=gy(D,$),W=j.path;if(W in J)throw Error(`Duplicate ${$} filter path: ${W}`);let G=U?Ey(j.rawValue,$):j.rawValue;J[W]=j.negated?{not:G}:G}return J}function qy(_){let $={};if(_.source)$.source=_.source;if(_.type)$.type=_.type;if(_.subject)$.subject=_.subject;if(_.severity)$.severity=_.severity;let U=vR(_G(_.data,"data"),_G(_.dataJson,"data-json",!0)),J=vR(_G(_.metadata,"metadata"),_G(_.metadataJson,"metadata-json",!0));if(Object.keys(U).length>0)$.data=U;if(Object.keys(J).length>0)$.metadata=J;return Object.keys($).length>0?[$]:void 0}function vR(..._){let $={};for(let U of _){if(!U)continue;for(let[J,D]of Object.entries(U)){if(J in $)throw Error(`Duplicate filter path: ${J}`);$[J]=D}}return $}function Ey(_,$){let U=JSON.parse(_);if(U===null||typeof U==="string"||typeof U==="number"||typeof U==="boolean"||Array.isArray(U)&&U.every((J)=>typeof J==="string"))return U;throw Error(`${$} filter JSON values must be string, string[], number, boolean, or null`)}function gy(_,$){let U=_.indexOf("!=");if(U>0)return{path:_.slice(0,U),rawValue:_.slice(U+2),negated:!0};let J=_.indexOf("=");if(J<=0)throw Error(`Invalid ${$} filter, expected path=value or path!=value: ${_}`);return{path:_.slice(0,J),rawValue:_.slice(J+1),negated:!1}}var Ry=100;function OU(_,$){if(!_)return $;let U=JSON.parse(_);if(!U||typeof U!=="object"||Array.isArray(U))throw Error("Expected a JSON object");return U}function By(_){if(!_?.length)return;let $={};for(let U of _){let J=U.indexOf("=");if(J===-1)throw Error(`Invalid header, expected name=value: ${U}`);$[U.slice(0,J)]=U.slice(J+1)}return $}function A4(_){if(_.createClient)return _.createClient();return new cR({store:new DG(_.dataDir)})}function S0(_,$,U){if($)console.log(JSON.stringify(_,null,2));else console.log(U)}function yR(_,$){let U=_ instanceof Error?_.message:String(_);if($)console.log(JSON.stringify({error:U},null,2));else console.error(U);process.exitCode=1}function hR(_){return Boolean(_?.json||_?.opts?.().json||_?.optsWithGlobals?.().json||_?.parent?.opts?.().json||_?.parent?.optsWithGlobals?.().json)}function n6(_,$){return hR(_)||hR($)}function Hy(_,$){let U=_.command($.channelsCommandName??"channels").description("Manage Hasna event channels");return U.command("add").description("Add or replace a channel").argument("","Webhook URL or command binary").requiredOption("--id ","Channel identifier").option("--transport ","Transport kind: webhook or command","webhook").option("--name ","Display name").option("--type ","Event type filter, e.g. todos.task.*").option("--source ","Event source filter").option("--subject ","Event subject filter").option("--severity ","Event severity filter").option("--data ","Event data field filter; string values, path!=value negatives, array-member matching, dot paths, * segment wildcard, ** recursive wildcard",k0,[]).option("--metadata ","Event metadata field filter; string values, path!=value negatives, array-member matching, dot paths, * segment wildcard, ** recursive wildcard",k0,[]).option("--data-json ","Event data field filter with typed JSON value; path!=json negatives supported",k0,[]).option("--metadata-json ","Event metadata field filter with typed JSON value; path!=json negatives supported",k0,[]).option("--secret ","Webhook HMAC secret").option("--header ","Webhook header",k0,[]).option("--arg ","Command argument",k0,[]).option("--timeout-ms ","Transport timeout in milliseconds",RD).option("--retry-attempts ","Maximum delivery attempts",RD).option("--retry-backoff-ms ","Initial retry backoff in milliseconds",RD).option("--redact ","Event field path to redact before delivery",k0,[]).option("--disabled","Create channel disabled",!1).option("-j, --json","Print JSON output",!1).action(async(J,D,j)=>{let W=new Date().toISOString(),G={id:D.id,name:D.name,enabled:!D.disabled,transport:D.transport,filters:qy(D),retry:D.retryAttempts||D.retryBackoffMs?{maxAttempts:D.retryAttempts,backoffMs:D.retryBackoffMs}:void 0,redact:D.redact?.length?{paths:D.redact}:void 0,createdAt:W,updatedAt:W};if(D.transport==="webhook")G.webhook={url:J,secret:D.secret,headers:By(D.header),timeoutMs:D.timeoutMs};else if(D.transport==="command")G.command={command:J,args:D.arg??[],timeoutMs:D.timeoutMs};else throw Error(`Transport ${D.transport} is reserved for future use and cannot be added yet`);let X=await A4($).addChannel(G);S0(iR(X),n6(D,j),`Added ${X.transport} channel ${X.id}`)}),U.command("list").description("List configured channels").option("-j, --json","Print JSON output",!1).action(async(J,D)=>{let j=await A4($).listChannels();if(n6(J,D)){console.log(JSON.stringify(Oy(j),null,2));return}if(!j.length){console.log("No channels configured.");return}for(let W of j)console.log(`${W.id} ${W.enabled?"enabled":"disabled"} ${W.transport} ${W.webhook?.url??W.command?.command??W.transport}`)}),U.command("status").description("Show events channel storage status").option("-j, --json","Print JSON output",!1).action(async(J,D)=>{let j=await av($.dataDir);S0(j,n6(J,D),`events dataDir: ${j.dataDir}`)}),U.command("remove").description("Remove a channel").argument("","Channel identifier").option("-j, --json","Print JSON output",!1).action(async(J,D,j)=>{let W=await A4($).removeChannel(J);S0({removed:W},n6(D,j),W?`Removed ${J}`:`Channel not found: ${J}`)}),U.command("test").description("Send a test event to one channel").argument("","Channel identifier").option("--source ","Event source override").option("--type ","Event type","events.test").option("--subject ","Event subject").option("--message ","Event message","Hasna events test delivery").option("--data ","Event data JSON object").option("--metadata ","Event metadata JSON object").option("--honor-filters","Skip delivery when the sample event does not match channel filters",!1).option("-j, --json","Print JSON output",!1).action(async(J,D,j)=>{let W=n6(D,j);try{let G=await A4($).testChannel(J,{source:D.source??$.source,type:D.type,subject:D.subject??J,message:D.message,data:OU(D.data,{test:!0}),metadata:OU(D.metadata,{})},{honorFilters:D.honorFilters});S0(G,W,`${G.status}: ${G.channelId}`)}catch(G){yR(G,W)}}),U.command("match").description("Check whether a sample event matches one channel without delivering").argument("","Channel identifier").option("--source ","Event source override").option("--type ","Event type","events.test").option("--subject ","Event subject").option("--message ","Event message","Hasna events match preview").option("--data ","Event data JSON object").option("--metadata ","Event metadata JSON object").option("-j, --json","Print JSON output",!1).action(async(J,D,j)=>{let W=n6(D,j);try{let G=await A4($).matchChannel(J,{source:D.source??$.source,type:D.type,subject:D.subject??J,message:D.message,data:OU(D.data,{test:!0}),metadata:OU(D.metadata,{})});S0(G,W,`${G.matched?"matched":"skipped"}: ${G.channelId}`)}catch(G){yR(G,W)}}),U}function Ay(_,$){let U=_.command($.eventsCommandName??"events").description("Emit, list, and replay Hasna events");U.command("emit").description("Emit an event from this app").argument("","Event type").option("--source ","Event source override").option("--subject ","Event subject").option("--severity ","Event severity","info").option("--message ","Event message").option("--dedupe-key ","Dedupe key").option("--data ","Event data JSON object").option("--metadata ","Event metadata JSON object").option("--no-deliver","Record without delivering").option("--no-dedupe","Allow duplicate id/dedupeKey events").option("-j, --json","Print JSON output",!1).action(async(D,j,W)=>{let G=await A4($).emit({source:j.source??$.source,type:D,subject:j.subject,severity:j.severity,message:j.message,dedupeKey:j.dedupeKey,data:OU(j.data,{}),metadata:OU(j.metadata,{})},{deliver:j.deliver,dedupe:j.dedupe});S0(G,n6(j,W),`${G.deduped?"Deduped":"Emitted"} ${G.event.id} to ${G.deliveries.length} channel(s)`)});let J=$.defaultEventListLimit??Ry;return U.command("list").description("List recorded events").option("--source ","Filter by source").option("--type ","Filter by type").option("--limit ",`Limit to the most recent events (default ${J}; use 0 for all)`,RD,J).option("-j, --json","Print JSON output",!1).action(async(D,j)=>{let W=await A4($).listEvents();if(D.source)W=W.filter((G)=>G.source===D.source);if(D.type)W=W.filter((G)=>G.type===D.type);if(D.limit)W=W.slice(-D.limit);if(n6(D,j)){console.log(JSON.stringify(W,null,2));return}if(!W.length){console.log("No events recorded.");return}for(let G of W)console.log(`${G.time} ${G.id} ${G.source} ${G.type} ${G.severity}`)}),U.command("replay").description("Replay recorded events").option("--id ","Replay one event id").option("--source ","Filter by source").option("--type ","Filter by type").option("--cursor ","Opaque replay cursor from a previous page").option("--limit ","Maximum events to replay",RD).option("--dry-run","Preview without delivery",!1).option("-j, --json","Print JSON output",!1).action(async(D,j)=>{let W=await A4($).replay({eventId:D.id,source:D.source,type:D.type,cursor:D.cursor,limit:D.limit,dryRun:D.dryRun});S0(W,n6(D,j),by(W.events.length,W.deliveries.length,W.nextCursor))}),U}function oR(_,$){Hy(_,$),Ay(_,$)}function RD(_){let $=Number(_);if(!Number.isFinite($))throw Error(`Expected a number, got ${_}`);return $}function k0(_,$){return $.push(_),$}function by(_,$,U){let J=U?`, next cursor: ${U}`:"";return`Replayed ${_} event(s), ${$} delivery result(s)${J}`}import{basename as Ky}from"path";var s$={name:"@hasna/knowledge",version:"0.2.91",description:"Agent-friendly local knowledge CLI with JSON output, pagination, and safe destructive actions",type:"module",exports:{".":{import:"./dist/index.js",types:"./dist/index.d.ts"},"./storage":{import:"./dist/storage.js",types:"./dist/storage.d.ts"},"./serve":{import:"./dist/serve.js",types:"./dist/serve.d.ts"}},main:"./dist/index.js",types:"./dist/index.d.ts",bin:{knowledge:"bin/knowledge.js","knowledge-mcp":"bin/knowledge-mcp.js","knowledge-serve":"bin/knowledge-serve.js"},files:["bin","dist","scripts/apply-cloud-migrations.mjs","scripts/lib/remote-temp-dir.mjs","scripts/smoke-machine-sync-release.mjs","scripts/smoke-machines-adapter.mjs","scripts/smoke-open-files-installed-boundary.mjs","scripts/strip-generated-trailing-whitespace.mjs","scripts/verify-generated-artifacts.mjs","docs/architecture/ai-native-knowledge-base.md","docs/architecture/hosted-wrapper-responsibilities.md","docs/architecture/hybrid-semantic-search.md","docs/architecture/machine-sync-schema.md","docs/examples/app-project-wiki-standard.md","docs/examples/company-wiki-workflow.md","docs/migration/global-rules-provenance-import.md","docs/migration/json-to-sqlite.md","LICENSE","README.md"],scripts:{test:"bun test","test:cli":"bun test tests/cli.test.ts","test:package":"bun test tests/package-release.test.ts","release:pack:check":"node scripts/validate-public-package.mjs","smoke:machines-adapter":"bun scripts/smoke-machines-adapter.mjs","smoke:machine-sync-release":"bun scripts/smoke-machine-sync-release.mjs","smoke:open-files-installed-boundary":"bun scripts/smoke-open-files-installed-boundary.mjs","migrate:cloud":"bun scripts/apply-cloud-migrations.mjs",serve:"bun src/serve-entry.ts","verify:generated":"bun run build && bun scripts/verify-generated-artifacts.mjs",build:"rm -rf dist && bun build --target=bun --outfile=bin/knowledge.js --minify --external pg --external @hasna/machines --external @hasna/machines/consumer --external @aws-sdk/client-s3 --external @aws-sdk/credential-providers --external ai --external @ai-sdk/openai --external @ai-sdk/anthropic --external @ai-sdk/deepseek src/cli.ts && bun build --target=bun --outfile=bin/knowledge-mcp.js --external pg --external @hasna/machines --external @hasna/machines/consumer --external @modelcontextprotocol/sdk --external @aws-sdk/client-s3 --external @aws-sdk/credential-providers --external ai --external @ai-sdk/openai --external @ai-sdk/anthropic --external @ai-sdk/deepseek src/mcp.js && bun build --target=bun --outfile=bin/knowledge-serve.js --external pg --external @hasna/machines --external @hasna/machines/consumer --external @aws-sdk/client-s3 --external @aws-sdk/credential-providers --external ai --external @ai-sdk/openai --external @ai-sdk/anthropic --external @ai-sdk/deepseek src/serve-entry.ts && bun build ./src/index.ts ./src/storage.ts ./src/serve.ts --outdir ./dist --target bun --external pg --external @hasna/machines --external @hasna/machines/consumer --external @aws-sdk/client-s3 --external @aws-sdk/credential-providers --external ai --external @ai-sdk/openai --external @ai-sdk/anthropic --external @ai-sdk/deepseek && bun scripts/strip-generated-trailing-whitespace.mjs && bunx tsc -p tsconfig.build.json",prepublishOnly:"bun run build && node scripts/validate-public-package.mjs"},keywords:["knowledge","cli","agents","json","notes","local","store"],license:"Apache-2.0",publishConfig:{registry:"https://registry.npmjs.org",access:"public"},repository:{type:"git",url:"git+https://github.com/hasna/knowledge.git"},bugs:{url:"https://github.com/hasna/knowledge/issues"},author:"Hasna Inc. ",engines:{bun:">=1.0",node:">=18"},dependencies:{"@ai-sdk/anthropic":"^3.0.81","@ai-sdk/deepseek":"^2.0.35","@ai-sdk/openai":"^3.0.68","@aws-sdk/client-s3":"^3.1063.0","@aws-sdk/credential-providers":"^3.1063.0","@hasna/events":"^0.1.3","@modelcontextprotocol/sdk":"^1.29.0","@types/json-schema":"^7.0.15",ai:"^6.0.197",commander:"^13.1.0",pg:"^8.16.3",zod:"^4.3.6"},devDependencies:{"@electric-sql/pglite":"^0.5.4","@hasna/contracts":"0.5.2","@types/bun":"^1.3.14","@types/pg":"^8.15.6"}};var pR={debug:0,info:1,warn:2,error:3},Iy=()=>{if(process.env.DEBUG)return"debug";if(process.env.LOG_LEVEL==="debug")return"debug";if(process.env.LOG_LEVEL==="warn")return"warn";if(process.env.LOG_LEVEL==="error")return"error";return"info"};function w0(_,$,U){if(pR[_]J.toLowerCase()));return $.filter((J)=>!U.has(J.toLowerCase()))}function vz(_,$,U){if(U===void 0)return{..._,message:$};return{..._,added:U.length,message:`${$} (added ${U.length} tag${U.length===1?"":"s"})`}}function My(_,$){if($===void 0)throw Error("Missing value for --tag. Example: knowledge add <content> -t <tag> -t <tag>");let U=$.split(",").map((J)=>J.trim()).filter((J)=>J.length>0);if(U.length===0)throw Error(`Invalid --tag value ${JSON.stringify($)}: no tag name found. Example: knowledge add <title> <content> -t <tag> -t <tag>`);return Fy([..._??[],...U])}function Py(_){let $=[],U={};for(let J=0;J<_.length;J+=1){let D=_[J];if(!D.startsWith("-")){$.push(D);continue}switch(D){case"--json":U.json=!0;break;case"--verbose":U.verbose=!0;break;case"--yes":case"-y":U.yes=!0;break;case"--help":case"-h":U.help=!0;break;case"--version":case"-v":U.version=!0;break;case"--desc":U.desc=!0;break;case"--page":case"-p":U.page=Number(_[J+1]),J+=1;break;case"--limit":case"-l":U.limit=Number(_[J+1]),J+=1;break;case"--search":case"-s":U.search=_[J+1],J+=1;break;case"--sort":U.sort=_[J+1],J+=1;break;case"--id":U.id=_[J+1],J+=1;break;case"--store":U.store=_[J+1],J+=1;break;case"--title":U.title=_[J+1],J+=1;break;case"--content":U.content=_[J+1],J+=1;break;case"--url":U.url=_[J+1],J+=1;break;case"--tag":case"-t":U.tag=My(U.tag,_[J+1]),U.tagRaw=[...U.tagRaw??[],_[J+1]],J+=1;break;case"--format":U.format=_[J+1],J+=1;break;case"--completions":U.completions=_[J+1],J+=1;break;case"--purpose":U.purpose=_[J+1],J+=1;break;case"--model":U.model=_[J+1],J+=1;break;case"--strategy":U.strategy=_[J+1],J+=1;break;case"--dimensions":U.dimensions=Number(_[J+1]),J+=1;break;case"--semantic":U.semantic=!0;break;case"--context":U.context=!0;break;case"--max-tokens":U.maxTokens=Number(_[J+1]),J+=1;break;case"--max-items":U.maxItems=Number(_[J+1]),J+=1;break;case"--from":U.from=_[J+1],J+=1;break;case"--since":U.since=_[J+1],J+=1;break;case"--topic":U.topic=_[J+1],J+=1;break;case"--dedupe":U.dedupe=!0;break;case"--generate":U.generate=!0;break;case"--approve-write":U.approveWrite=!0;break;case"--provider":U.provider=_[J+1],J+=1;break;case"--mode":U.mode=_[J+1],J+=1;break;case"--machine":U.machine=_[J+1],J+=1;break;case"--workspace":U.workspace=_[J+1],J+=1;break;case"--api-url":U.apiUrl=_[J+1],J+=1;break;case"--canonical-example":U.canonicalExample=!0;break;case"--api-key":U.apiKey=_[J+1],J+=1;break;case"--email":U.email=_[J+1],J+=1;break;case"--org":U.org=_[J+1],J+=1;break;case"--org-id":U.orgId=_[J+1],J+=1;break;case"--user-id":U.userId=_[J+1],J+=1;break;case"--owner":U.owner=_[J+1],J+=1;break;case"--approved-by":U.approvedBy=_[J+1],J+=1;break;case"--patch-uri":U.patchUri=_[J+1],J+=1;break;case"--domain":U.domain=[...U.domain??[],_[J+1]],J+=1;break;case"--file-results":U.fileResults=!0;break;case"--full":U.full=!0;break;case"--dry-run":U.dryRun=!0;break;case"--fake":U.fake=!0;break;case"--no-tailscale":U.tailscale=!1;break;case"--no-artifact-content":U.artifactContent=!1;break;case"--no-color":U.noColor=!0;break;case"--scope":U.scope=_[J+1],J+=1;break;case"--tables":U.tables=_[J+1],J+=1;break;case"--peer-workspace":U.peerWorkspace=_[J+1],J+=1;break;case"--older-than":U.olderThan=Number(_[J+1]),J+=1;break;case"--empty":U.empty=!0;break;case"--archived":U.archived=!0;break;case"--include-archived":U.includeArchived=!0;break;case"--project":U.project=_[J+1],J+=1;break;case"--contract":U.contract=!0;break;case"--source-ref":U.sourceRef=[...U.sourceRef??[],_[J+1]],J+=1;break;case"--allow-global":U.allowGlobal=!0;break;default:throw Error(`Unknown flag: ${D}. Run 'knowledge --help' for valid options.`)}}return{positional:$,flags:U}}function ky(_){if(!_)return"";return UB[_]??_}function Sy(_,$){let U=Array.from({length:_.length+1},()=>Array($.length+1).fill(0));for(let J=0;J<=_.length;J+=1)U[J][0]=J;for(let J=0;J<=$.length;J+=1)U[0][J]=J;for(let J=1;J<=_.length;J+=1)for(let D=1;D<=$.length;D+=1){let j=_[J-1]===$[D-1]?0:1;U[J][D]=Math.min(U[J-1][D]+1,U[J][D-1]+1,U[J-1][D-1]+j)}return U[_.length][$.length]}function wy(_){if(!_)return"";let $=[...$B,...Object.keys(UB)],U="",J=Number.POSITIVE_INFINITY;for(let D of $){let j=Sy(_,D);if(j<J)J=j,U=D}return J<=3?U:""}function fy(){return Ky(process.argv[1]??"").replace(/\.(?:js|ts|mjs|cjs)$/,"")==="knowledge"}async function Ty(_){if(!_B.includes(_[0]??""))return!1;let $=new MR;return $.name("knowledge").description("Agent-friendly local knowledge CLI with JSON output, pagination, and safe destructive actions"),oR($,{source:"knowledge"}),await $.parseAsync(_,{from:"user"}),!0}function Cy(){console.log(`knowledge - local agent knowledge store +`,{encoding:"utf-8",mode:384}),await cv(U,_),await Sz(_,384).catch(()=>{return})}}function tv(_=uR()){return{mode:"local-files",name:"json-events-store",remote:!1,localFiles:!0,localSqlite:!1,postgres:!1,s3:!1,aws:!1,durable:!0,idempotency:"best-effort-local",replayCursors:!0,description:`Local JSON files in ${_}; no SQLite, Postgres, S3, or AWS runtime is configured by this store.`}}function nR(_,$={}){if(!Number.isInteger(_)||_<0)throw Error(`Invalid event cursor offset: ${_}`);let U={offset:_,eventId:$.eventId,source:$.source,type:$.type};return`${Cz}${xR.from(JSON.stringify(U),"utf-8").toString("base64url")}`}function JG(_,$={}){if(!_)return 0;if(!_.startsWith(Cz))throw Error(`Invalid local JSON event cursor: ${_}`);let U=_.slice(Cz.length),J;try{J=JSON.parse(xR.from(U,"base64url").toString("utf-8"))}catch{throw Error(`Invalid local JSON event cursor: ${_}`)}let D=J.offset;if(!Number.isInteger(D)||D<0)throw Error(`Invalid local JSON event cursor: ${_}`);return wz("eventId",J.eventId,$.eventId),wz("source",J.source,$.source),wz("type",J.type,$.type),D}function jG(_){if(_===void 0)return lv;if(!Number.isInteger(_)||_<1)throw Error(`Event page limit must be a positive integer, got ${_}`);return Math.min(_,ov)}function fR(_,$){let U=_;if($.eventId)U=U.filter((J)=>J.id===$.eventId);if($.source)U=U.filter((J)=>J.source===$.source);if($.type)U=U.filter((J)=>J.type===$.type);if($.cursor){let J=JG($.cursor,$);U=U.slice(J)}if($.limit!==void 0)U=U.slice(0,jG($.limit));return U}function wz(_,$,U){if($!==U)throw Error(`Local JSON event cursor ${_} filter mismatch`)}function TR(_,$){return _.find((U)=>$.id!==void 0&&U.id===$.id||$.dedupeKey!==void 0&&U.dedupeKey===$.dedupeKey)}async function av(_){let $=new DG(_);await $.init();let[U,J,D]=await Promise.all([$.listChannels(),$.listEvents(),$.listDeliveries()]),j=U.reduce((W,G)=>{return W[G.transport]=(W[G.transport]??0)+1,W},{});return{service:"events",schemaVersion:"1.0",dataDir:$.dataDir,storage:$.runtime,env:{primary:$G,fallback:UG,active:pv()},files:{channels:fz($.dataDir,"channels.json",U.length),events:fz($.dataDir,"events.json",J.length),deliveries:fz($.dataDir,"deliveries.json",D.length)},counts:{channels:U.length,enabledChannels:U.filter((W)=>W.enabled).length,disabledChannels:U.filter((W)=>!W.enabled).length,events:J.length,deliveries:D.length},transports:j,safety:{includesEventPayloads:!1,includesWebhookSecrets:!1,listOutputsRedactSecrets:!0,statusOutputIsMetadataOnly:!0}}}function fz(_,$,U){let J=gD(_,$);return{path:J,exists:rR(J),records:U}}function sv(_,$){return`${_}.${$}`}function _y(_,$,U){return`sha256=${ev("sha256",_).update(sv($,U)).digest("hex")}`}function L6(){return new Date().toISOString()}function ED(_,$=4096){return _.length>$?`${_.slice(0,$)}...`:_}function Dy(_,$){if(!$.webhook)throw Error(`Channel ${$.id} has no webhook config`);let U=JSON.stringify(_),J=_.time,D={"Content-Type":"application/json","User-Agent":"@hasna/events","X-Hasna-Event-Id":_.id,"X-Hasna-Event-Type":_.type,"X-Hasna-Timestamp":J,...$.webhook.headers};if($.webhook.secret)D["X-Hasna-Signature"]=_y($.webhook.secret,J,U);return{body:U,headers:D}}async function Jy(_,$,U={}){if(!$.webhook)throw Error(`Channel ${$.id} has no webhook config`);let J=L6(),{body:D,headers:j}=Dy(_,$),W=new AbortController,G=setTimeout(()=>W.abort(),$.webhook.timeoutMs??15000);try{let X=await(U.fetchImpl??fetch)($.webhook.url,{method:"POST",headers:j,body:D,signal:W.signal}),N=ED(await X.text());return{attempt:1,status:X.ok?"success":"failed",startedAt:J,completedAt:L6(),responseStatus:X.status,responseBody:N,error:X.ok?void 0:`Webhook returned HTTP ${X.status}`}}catch(X){return{attempt:1,status:"failed",startedAt:J,completedAt:L6(),error:X instanceof Error?X.message:String(X)}}finally{clearTimeout(G)}}async function jy(_,$){if(!$.command)throw Error(`Channel ${$.id} has no command config`);let U=L6(),J=JSON.stringify(_),D={...process.env,...$.command.env,HASNA_CHANNEL_ID:$.id,HASNA_EVENT_ID:_.id,HASNA_EVENT_TYPE:_.type,HASNA_EVENT_SOURCE:_.source,HASNA_EVENT_SUBJECT:_.subject??"",HASNA_EVENT_SEVERITY:_.severity,HASNA_EVENT_TIME:_.time,HASNA_EVENT_DEDUPE_KEY:_.dedupeKey??"",HASNA_EVENT_SCHEMA_VERSION:_.schemaVersion,HASNA_EVENT_JSON:J};return new Promise((j)=>{let W=Uy($.command.command,$.command.args??[],{cwd:$.command.cwd,env:D,stdio:["pipe","pipe","pipe"]}),G="",X="",N=setTimeout(()=>W.kill("SIGTERM"),$.command.timeoutMs??15000);W.stdin.end(J),W.stdout.on("data",(z)=>{G+=z.toString()}),W.stderr.on("data",(z)=>{X+=z.toString()}),W.on("error",(z)=>{clearTimeout(N),j({attempt:1,status:"failed",startedAt:U,completedAt:L6(),stdout:ED(G),stderr:ED(X),error:z.message})}),W.on("close",(z,q)=>{clearTimeout(N);let g=z===0;j({attempt:1,status:g?"success":"failed",startedAt:U,completedAt:L6(),stdout:ED(G),stderr:ED(X),error:g?void 0:`Command exited with ${q?`signal ${q}`:`code ${z}`}`})})})}async function Wy(_,$,U={}){if($.transport==="webhook")return Jy(_,$,U);if($.transport==="command")return jy(_,$);return{attempt:1,status:"skipped",startedAt:L6(),completedAt:L6(),error:`Unsupported transport: ${$.transport}`}}function CR(_,$,U){let J=U.some((D)=>D.status==="success")?"success":U.every((D)=>D.status==="skipped")?"skipped":"failed";return{id:$y(),eventId:_.id,channelId:$.id,transport:$.transport,status:J,attempts:U,createdAt:U[0]?.startedAt??L6(),completedAt:U.at(-1)?.completedAt??L6()}}class mR extends Error{eventType;issues;constructor(_,$){let U=$.map((J)=>`${J.path||"<root>"}: ${J.message}`).join("; ");super(`Event validation failed for type "${_}": ${U}`);this.name="EventValidationError",this.eventType=_,this.issues=$}}class dR{definitions=new Map;register(_){return this.definitions.set(_.type,_),this}unregister(_){return this.definitions.delete(_)}has(_){return this.definitions.has(_)}get(_){return this.definitions.get(_)}list(){return[...this.definitions.values()]}validateEvent(_){let $=this.definitions.get(_.type);if(!$)return{ok:!0};return $.validate(_.data,_)}assertEventValid(_){let $=this.validateEvent(_);if(!$.ok)throw new mR(_.type,$.issues)}}var Gy=new dR;function Tz(_){return{id:_.id??Xy(),source:_.source,type:_.type,time:Qy(_.time),subject:_.subject,severity:_.severity??"info",data:_.data??{},message:_.message,dedupeKey:_.dedupeKey,schemaVersion:_.schemaVersion??"1.0",metadata:_.metadata??{}}}class cR{store;redactors;transportOptions;catalog;validateCatalogTypes;constructor(_={}){this.store=_.store??new DG(_.dataDir),this.redactors=_.redactors??[],this.transportOptions={fetchImpl:_.fetchImpl},this.catalog=_.catalog??Gy,this.validateCatalogTypes=_.validateCatalogTypes??!1}async addChannel(_){let $=new Date().toISOString();return this.store.addChannel({..._,createdAt:_.createdAt??$,updatedAt:_.updatedAt??$})}async listChannels(){return this.store.listChannels()}async removeChannel(_){return this.store.removeChannel(_)}async emit(_,$={}){let U=$.redactSensitiveData===!1?Tz(_):Yy(Tz(_));if($.validate??this.validateCatalogTypes)this.catalog.assertEventValid(U);let J=await this.appendEvent(U,{dedupe:$.dedupe!==!1});if(J.deduped)return{event:J.event,deliveries:[],deduped:!0};let D=$.deliver===!1?[]:await this.deliver(J.event);return{event:J.event,deliveries:D,deduped:!1}}async listEvents(_={}){if(Object.keys(_).length===0)return this.store.listEvents();return ZR(await this.store.listEvents(),_)}async listEventsPage(_={}){if(this.store.listEventsPage)return this.store.listEventsPage(_);let $=ZR(await this.store.listEvents(),{eventId:_.eventId,source:_.source,type:_.type}),U=JG(_.cursor,_),J=jG(_.limit),D=$.slice(U,U+J),j=U+D.length,W=j<$.length;return{events:D,cursor:_.cursor,nextCursor:W?nR(j,_):void 0,hasMore:W}}async listDeliveries(){return this.store.listDeliveries()}async deliver(_){let U=(await this.store.listChannels()).filter((D)=>SR(D,_)),J=[];for(let D of U){let j=await this.applyRedaction(_,D),W=await this.deliverWithRetry(j,D);await this.store.appendDelivery(W),J.push(W)}return J}async matchChannel(_,$={}){let U=await this.store.getChannel(_);if(!U)throw Error(`Channel not found: ${_}`);let J=Tz({source:$.source??"hasna.events",type:$.type??"events.test",subject:$.subject??_,severity:$.severity??"info",data:$.data??{test:!0},message:$.message??"Hasna events test delivery",dedupeKey:$.dedupeKey,schemaVersion:$.schemaVersion,metadata:$.metadata,time:$.time,id:$.id}),D=SR(U,J);return{channelId:U.id,matched:D,event:J,filters:U.filters,reason:D?void 0:U.enabled?"event did not match channel filters":"channel is disabled"}}async testChannel(_,$={},U={}){let J=await this.store.getChannel(_);if(!J)throw Error(`Channel not found: ${_}`);let D=await this.matchChannel(_,$),j=D.event;if(U.honorFilters&&!D.matched){let X=new Date().toISOString(),N=CR(j,J,[{attempt:1,status:"skipped",startedAt:X,completedAt:X,error:D.reason}]);return N.metadata={reason:"filter_mismatch"},await this.store.appendDelivery(N),N}let W=await this.applyRedaction(j,J),G=await this.deliverWithRetry(W,J);return await this.store.appendDelivery(G),G}async replay(_={}){let $=_.cursor||_.limit!==void 0?await this.listEventsPage(_):{events:await this.listEvents(_),hasMore:!1};if(_.dryRun)return{events:$.events,deliveries:[],cursor:$.cursor,nextCursor:$.nextCursor,hasMore:$.hasMore};let U=[];for(let J of $.events)U.push(...await this.deliver(J));return{events:$.events,deliveries:U,cursor:$.cursor,nextCursor:$.nextCursor,hasMore:$.hasMore}}async appendEvent(_,$){if(this.store.appendEventOnce)return this.store.appendEventOnce(_,{dedupe:$.dedupe});if($.dedupe){let J=await this.store.findEventByIdentity({id:_.id,dedupeKey:_.dedupeKey});if(J)return{event:J,stored:!1,deduped:!0,identity:{id:J.id,dedupeKey:J.dedupeKey}}}let U=await this.store.appendEvent(_);return{event:U,stored:!0,deduped:!1,identity:{id:U.id,dedupeKey:U.dedupeKey}}}async applyRedaction(_,$){let U=Ny(_,$.redact?.paths??[],$.redact?.replacement??"[REDACTED]");for(let J of this.redactors)U=await J(U,$);return U}async deliverWithRetry(_,$){let U=Ly($.retry),J=[];for(let D=0;D<U.maxAttempts;D+=1){let j=await Wy(_,$,this.transportOptions);if(j.attempt=D+1,j.status==="failed"&&D+1<U.maxAttempts)j.nextBackoffMs=Math.round(U.backoffMs*U.multiplier**D);if(J.push(j),j.status!=="failed")break;if(j.nextBackoffMs)await Bun.sleep(j.nextBackoffMs)}return CR(_,$,J)}}function Ny(_,$,U="[REDACTED]"){if($.length===0)return _;let J=structuredClone(_);for(let D of $)zy(J,D,U);return J}function iR(_){let $=structuredClone(_);if($.webhook?.secret)$.webhook.secret="[REDACTED]";if($.command?.env)$.command.env=Object.fromEntries(Object.entries($.command.env).map(([U,J])=>[U,lR(U)?"[REDACTED]":J]));return $}function Oy(_){return _.map(iR)}function Yy(_,$="[REDACTED]"){return Zz(_,$)}function lR(_){return/secret|token|password|api[_-]?key|authorization/i.test(_)}function Zz(_,$){if(Array.isArray(_))return _.map((U)=>Zz(U,$));if(!_||typeof _!=="object")return _;return Object.fromEntries(Object.entries(_).map(([U,J])=>[U,lR(U)?$:Zz(J,$)]))}function zy(_,$,U){let J=$.split("."),D=_;for(let W of J.slice(0,-1)){let G=D[W];if(!G||typeof G!=="object")return;D=G}let j=J.at(-1);if(j&&j in D)D[j]=U}function ZR(_,$){let U=_;if($.eventId)U=U.filter((J)=>J.id===$.eventId);if($.source)U=U.filter((J)=>J.source===$.source);if($.type)U=U.filter((J)=>J.type===$.type);if($.cursor)U=U.slice(JG($.cursor,$));if($.limit!==void 0)U=U.slice(0,jG($.limit));return U}function Qy(_){if(!_)return new Date().toISOString();return _ instanceof Date?_.toISOString():_}function Ly(_){return{maxAttempts:Math.max(1,_?.maxAttempts??1),backoffMs:Math.max(0,_?.backoffMs??250),multiplier:Math.max(1,_?.multiplier??2)}}function _G(_,$,U=!1){if(!_?.length)return;let J={};for(let D of _){let j=gy(D,$),W=j.path;if(W in J)throw Error(`Duplicate ${$} filter path: ${W}`);let G=U?Ey(j.rawValue,$):j.rawValue;J[W]=j.negated?{not:G}:G}return J}function qy(_){let $={};if(_.source)$.source=_.source;if(_.type)$.type=_.type;if(_.subject)$.subject=_.subject;if(_.severity)$.severity=_.severity;let U=vR(_G(_.data,"data"),_G(_.dataJson,"data-json",!0)),J=vR(_G(_.metadata,"metadata"),_G(_.metadataJson,"metadata-json",!0));if(Object.keys(U).length>0)$.data=U;if(Object.keys(J).length>0)$.metadata=J;return Object.keys($).length>0?[$]:void 0}function vR(..._){let $={};for(let U of _){if(!U)continue;for(let[J,D]of Object.entries(U)){if(J in $)throw Error(`Duplicate filter path: ${J}`);$[J]=D}}return $}function Ey(_,$){let U=JSON.parse(_);if(U===null||typeof U==="string"||typeof U==="number"||typeof U==="boolean"||Array.isArray(U)&&U.every((J)=>typeof J==="string"))return U;throw Error(`${$} filter JSON values must be string, string[], number, boolean, or null`)}function gy(_,$){let U=_.indexOf("!=");if(U>0)return{path:_.slice(0,U),rawValue:_.slice(U+2),negated:!0};let J=_.indexOf("=");if(J<=0)throw Error(`Invalid ${$} filter, expected path=value or path!=value: ${_}`);return{path:_.slice(0,J),rawValue:_.slice(J+1),negated:!1}}var Ry=100;function OU(_,$){if(!_)return $;let U=JSON.parse(_);if(!U||typeof U!=="object"||Array.isArray(U))throw Error("Expected a JSON object");return U}function By(_){if(!_?.length)return;let $={};for(let U of _){let J=U.indexOf("=");if(J===-1)throw Error(`Invalid header, expected name=value: ${U}`);$[U.slice(0,J)]=U.slice(J+1)}return $}function A4(_){if(_.createClient)return _.createClient();return new cR({store:new DG(_.dataDir)})}function S0(_,$,U){if($)console.log(JSON.stringify(_,null,2));else console.log(U)}function yR(_,$){let U=_ instanceof Error?_.message:String(_);if($)console.log(JSON.stringify({error:U},null,2));else console.error(U);process.exitCode=1}function hR(_){return Boolean(_?.json||_?.opts?.().json||_?.optsWithGlobals?.().json||_?.parent?.opts?.().json||_?.parent?.optsWithGlobals?.().json)}function n6(_,$){return hR(_)||hR($)}function Hy(_,$){let U=_.command($.channelsCommandName??"channels").description("Manage Hasna event channels");return U.command("add").description("Add or replace a channel").argument("<target>","Webhook URL or command binary").requiredOption("--id <id>","Channel identifier").option("--transport <kind>","Transport kind: webhook or command","webhook").option("--name <name>","Display name").option("--type <pattern>","Event type filter, e.g. todos.task.*").option("--source <pattern>","Event source filter").option("--subject <pattern>","Event subject filter").option("--severity <pattern>","Event severity filter").option("--data <path=value...>","Event data field filter; string values, path!=value negatives, array-member matching, dot paths, * segment wildcard, ** recursive wildcard",k0,[]).option("--metadata <path=value...>","Event metadata field filter; string values, path!=value negatives, array-member matching, dot paths, * segment wildcard, ** recursive wildcard",k0,[]).option("--data-json <path=json...>","Event data field filter with typed JSON value; path!=json negatives supported",k0,[]).option("--metadata-json <path=json...>","Event metadata field filter with typed JSON value; path!=json negatives supported",k0,[]).option("--secret <secret>","Webhook HMAC secret").option("--header <name=value...>","Webhook header",k0,[]).option("--arg <arg...>","Command argument",k0,[]).option("--timeout-ms <ms>","Transport timeout in milliseconds",RD).option("--retry-attempts <n>","Maximum delivery attempts",RD).option("--retry-backoff-ms <ms>","Initial retry backoff in milliseconds",RD).option("--redact <path...>","Event field path to redact before delivery",k0,[]).option("--disabled","Create channel disabled",!1).option("-j, --json","Print JSON output",!1).action(async(J,D,j)=>{let W=new Date().toISOString(),G={id:D.id,name:D.name,enabled:!D.disabled,transport:D.transport,filters:qy(D),retry:D.retryAttempts||D.retryBackoffMs?{maxAttempts:D.retryAttempts,backoffMs:D.retryBackoffMs}:void 0,redact:D.redact?.length?{paths:D.redact}:void 0,createdAt:W,updatedAt:W};if(D.transport==="webhook")G.webhook={url:J,secret:D.secret,headers:By(D.header),timeoutMs:D.timeoutMs};else if(D.transport==="command")G.command={command:J,args:D.arg??[],timeoutMs:D.timeoutMs};else throw Error(`Transport ${D.transport} is reserved for future use and cannot be added yet`);let X=await A4($).addChannel(G);S0(iR(X),n6(D,j),`Added ${X.transport} channel ${X.id}`)}),U.command("list").description("List configured channels").option("-j, --json","Print JSON output",!1).action(async(J,D)=>{let j=await A4($).listChannels();if(n6(J,D)){console.log(JSON.stringify(Oy(j),null,2));return}if(!j.length){console.log("No channels configured.");return}for(let W of j)console.log(`${W.id} ${W.enabled?"enabled":"disabled"} ${W.transport} ${W.webhook?.url??W.command?.command??W.transport}`)}),U.command("status").description("Show events channel storage status").option("-j, --json","Print JSON output",!1).action(async(J,D)=>{let j=await av($.dataDir);S0(j,n6(J,D),`events dataDir: ${j.dataDir}`)}),U.command("remove").description("Remove a channel").argument("<id>","Channel identifier").option("-j, --json","Print JSON output",!1).action(async(J,D,j)=>{let W=await A4($).removeChannel(J);S0({removed:W},n6(D,j),W?`Removed ${J}`:`Channel not found: ${J}`)}),U.command("test").description("Send a test event to one channel").argument("<id>","Channel identifier").option("--source <source>","Event source override").option("--type <type>","Event type","events.test").option("--subject <subject>","Event subject").option("--message <message>","Event message","Hasna events test delivery").option("--data <json>","Event data JSON object").option("--metadata <json>","Event metadata JSON object").option("--honor-filters","Skip delivery when the sample event does not match channel filters",!1).option("-j, --json","Print JSON output",!1).action(async(J,D,j)=>{let W=n6(D,j);try{let G=await A4($).testChannel(J,{source:D.source??$.source,type:D.type,subject:D.subject??J,message:D.message,data:OU(D.data,{test:!0}),metadata:OU(D.metadata,{})},{honorFilters:D.honorFilters});S0(G,W,`${G.status}: ${G.channelId}`)}catch(G){yR(G,W)}}),U.command("match").description("Check whether a sample event matches one channel without delivering").argument("<id>","Channel identifier").option("--source <source>","Event source override").option("--type <type>","Event type","events.test").option("--subject <subject>","Event subject").option("--message <message>","Event message","Hasna events match preview").option("--data <json>","Event data JSON object").option("--metadata <json>","Event metadata JSON object").option("-j, --json","Print JSON output",!1).action(async(J,D,j)=>{let W=n6(D,j);try{let G=await A4($).matchChannel(J,{source:D.source??$.source,type:D.type,subject:D.subject??J,message:D.message,data:OU(D.data,{test:!0}),metadata:OU(D.metadata,{})});S0(G,W,`${G.matched?"matched":"skipped"}: ${G.channelId}`)}catch(G){yR(G,W)}}),U}function Ay(_,$){let U=_.command($.eventsCommandName??"events").description("Emit, list, and replay Hasna events");U.command("emit").description("Emit an event from this app").argument("<type>","Event type").option("--source <source>","Event source override").option("--subject <subject>","Event subject").option("--severity <severity>","Event severity","info").option("--message <message>","Event message").option("--dedupe-key <key>","Dedupe key").option("--data <json>","Event data JSON object").option("--metadata <json>","Event metadata JSON object").option("--no-deliver","Record without delivering").option("--no-dedupe","Allow duplicate id/dedupeKey events").option("-j, --json","Print JSON output",!1).action(async(D,j,W)=>{let G=await A4($).emit({source:j.source??$.source,type:D,subject:j.subject,severity:j.severity,message:j.message,dedupeKey:j.dedupeKey,data:OU(j.data,{}),metadata:OU(j.metadata,{})},{deliver:j.deliver,dedupe:j.dedupe});S0(G,n6(j,W),`${G.deduped?"Deduped":"Emitted"} ${G.event.id} to ${G.deliveries.length} channel(s)`)});let J=$.defaultEventListLimit??Ry;return U.command("list").description("List recorded events").option("--source <source>","Filter by source").option("--type <type>","Filter by type").option("--limit <n>",`Limit to the most recent <n> events (default ${J}; use 0 for all)`,RD,J).option("-j, --json","Print JSON output",!1).action(async(D,j)=>{let W=await A4($).listEvents();if(D.source)W=W.filter((G)=>G.source===D.source);if(D.type)W=W.filter((G)=>G.type===D.type);if(D.limit)W=W.slice(-D.limit);if(n6(D,j)){console.log(JSON.stringify(W,null,2));return}if(!W.length){console.log("No events recorded.");return}for(let G of W)console.log(`${G.time} ${G.id} ${G.source} ${G.type} ${G.severity}`)}),U.command("replay").description("Replay recorded events").option("--id <id>","Replay one event id").option("--source <source>","Filter by source").option("--type <type>","Filter by type").option("--cursor <cursor>","Opaque replay cursor from a previous page").option("--limit <n>","Maximum events to replay",RD).option("--dry-run","Preview without delivery",!1).option("-j, --json","Print JSON output",!1).action(async(D,j)=>{let W=await A4($).replay({eventId:D.id,source:D.source,type:D.type,cursor:D.cursor,limit:D.limit,dryRun:D.dryRun});S0(W,n6(D,j),by(W.events.length,W.deliveries.length,W.nextCursor))}),U}function oR(_,$){Hy(_,$),Ay(_,$)}function RD(_){let $=Number(_);if(!Number.isFinite($))throw Error(`Expected a number, got ${_}`);return $}function k0(_,$){return $.push(_),$}function by(_,$,U){let J=U?`, next cursor: ${U}`:"";return`Replayed ${_} event(s), ${$} delivery result(s)${J}`}import{basename as Ky}from"path";var s$={name:"@hasna/knowledge",version:"0.2.91",description:"Agent-friendly local knowledge CLI with JSON output, pagination, and safe destructive actions",type:"module",exports:{".":{import:"./dist/index.js",types:"./dist/index.d.ts"},"./storage":{import:"./dist/storage.js",types:"./dist/storage.d.ts"},"./serve":{import:"./dist/serve.js",types:"./dist/serve.d.ts"}},main:"./dist/index.js",types:"./dist/index.d.ts",bin:{knowledge:"bin/knowledge.js","knowledge-mcp":"bin/knowledge-mcp.js","knowledge-serve":"bin/knowledge-serve.js"},files:["bin","dist","scripts/apply-cloud-migrations.mjs","scripts/lib/remote-temp-dir.mjs","scripts/smoke-machine-sync-release.mjs","scripts/smoke-machines-adapter.mjs","scripts/smoke-open-files-installed-boundary.mjs","scripts/strip-generated-trailing-whitespace.mjs","scripts/verify-generated-artifacts.mjs","docs/architecture/ai-native-knowledge-base.md","docs/architecture/hosted-wrapper-responsibilities.md","docs/architecture/hybrid-semantic-search.md","docs/architecture/machine-sync-schema.md","docs/examples/app-project-wiki-standard.md","docs/examples/company-wiki-workflow.md","docs/migration/global-rules-provenance-import.md","docs/migration/json-to-sqlite.md","LICENSE","README.md"],scripts:{test:"bun test","test:cli":"bun test tests/cli.test.ts","test:package":"bun test tests/package-release.test.ts","release:pack:check":"node scripts/validate-public-package.mjs","smoke:machines-adapter":"bun scripts/smoke-machines-adapter.mjs","smoke:machine-sync-release":"bun scripts/smoke-machine-sync-release.mjs","smoke:open-files-installed-boundary":"bun scripts/smoke-open-files-installed-boundary.mjs","migrate:cloud":"bun scripts/apply-cloud-migrations.mjs",serve:"bun src/serve-entry.ts","verify:generated":"bun scripts/verify-generated-artifacts.mjs",build:"rm -rf dist && bun build --target=bun --outfile=bin/knowledge.js --minify --external pg --external @hasna/machines --external @hasna/machines/consumer --external @aws-sdk/client-s3 --external @aws-sdk/credential-providers --external ai --external @ai-sdk/openai --external @ai-sdk/anthropic --external @ai-sdk/deepseek src/cli.ts && bun build --target=bun --outfile=bin/knowledge-mcp.js --external pg --external @hasna/machines --external @hasna/machines/consumer --external @modelcontextprotocol/sdk --external @aws-sdk/client-s3 --external @aws-sdk/credential-providers --external ai --external @ai-sdk/openai --external @ai-sdk/anthropic --external @ai-sdk/deepseek src/mcp.js && bun build --target=bun --outfile=bin/knowledge-serve.js --external pg --external @hasna/machines --external @hasna/machines/consumer --external @aws-sdk/client-s3 --external @aws-sdk/credential-providers --external ai --external @ai-sdk/openai --external @ai-sdk/anthropic --external @ai-sdk/deepseek src/serve-entry.ts && bun build ./src/index.ts ./src/storage.ts ./src/serve.ts --outdir ./dist --target bun --external pg --external @hasna/machines --external @hasna/machines/consumer --external @aws-sdk/client-s3 --external @aws-sdk/credential-providers --external ai --external @ai-sdk/openai --external @ai-sdk/anthropic --external @ai-sdk/deepseek && bun scripts/strip-generated-trailing-whitespace.mjs && bunx tsc -p tsconfig.build.json",prepublishOnly:"bun run build && node scripts/validate-public-package.mjs"},keywords:["knowledge","cli","agents","json","notes","local","store"],license:"Apache-2.0",publishConfig:{registry:"https://registry.npmjs.org",access:"public"},repository:{type:"git",url:"git+https://github.com/hasna/knowledge.git"},bugs:{url:"https://github.com/hasna/knowledge/issues"},author:"Hasna Inc. <hasna@example.com>",engines:{bun:">=1.0",node:">=18"},dependencies:{"@ai-sdk/anthropic":"^3.0.81","@ai-sdk/deepseek":"^2.0.35","@ai-sdk/openai":"^3.0.68","@aws-sdk/client-s3":"^3.1063.0","@aws-sdk/credential-providers":"^3.1063.0","@hasna/events":"^0.1.3","@modelcontextprotocol/sdk":"^1.29.0","@types/json-schema":"^7.0.15",ai:"^6.0.197",commander:"^13.1.0",pg:"^8.16.3",zod:"^4.3.6"},devDependencies:{"@electric-sql/pglite":"^0.5.4","@hasna/contracts":"0.5.2","@types/bun":"^1.3.14","@types/pg":"^8.15.6"}};var pR={debug:0,info:1,warn:2,error:3},Iy=()=>{if(process.env.DEBUG)return"debug";if(process.env.LOG_LEVEL==="debug")return"debug";if(process.env.LOG_LEVEL==="warn")return"warn";if(process.env.LOG_LEVEL==="error")return"error";return"info"};function w0(_,$,U){if(pR[_]<pR[Iy()])return;let J={debug:"[DEBUG]",info:"[INFO]",warn:"[WARN]",error:"[ERROR]"}[_],D=U?`${J} ${$} ${JSON.stringify(U)}`:`${J} ${$}`;if(_==="error")console.error(D);else console.error(D)}var _B=["events","webhooks"],$B=["add","list","get","delete","update","archive","restore","upsert","untag","export","prune","dedupe","stats","inventory","project-panel","paths","setup","auth","storage","machines","sync","db","wiki","app-wiki","source","ingest","reindex","search","context","proposals","web","ask","build","embeddings","providers","safety","help",..._B],UB={ls:"list",rm:"delete",edit:"update",unarchive:"restore"};function Fy(_){let $=new Set,U=[];for(let J of _){let D=J.toLowerCase();if($.has(D))continue;$.add(D),U.push(J)}return U}function tR(_,$){let U=new Set((_??[]).map((J)=>J.toLowerCase()));return $.filter((J)=>!U.has(J.toLowerCase()))}function vz(_,$,U){if(U===void 0)return{..._,message:$};return{..._,added:U.length,message:`${$} (added ${U.length} tag${U.length===1?"":"s"})`}}function My(_,$){if($===void 0)throw Error("Missing value for --tag. Example: knowledge add <title> <content> -t <tag> -t <tag>");let U=$.split(",").map((J)=>J.trim()).filter((J)=>J.length>0);if(U.length===0)throw Error(`Invalid --tag value ${JSON.stringify($)}: no tag name found. Example: knowledge add <title> <content> -t <tag> -t <tag>`);return Fy([..._??[],...U])}function Py(_){let $=[],U={};for(let J=0;J<_.length;J+=1){let D=_[J];if(!D.startsWith("-")){$.push(D);continue}switch(D){case"--json":U.json=!0;break;case"--verbose":U.verbose=!0;break;case"--yes":case"-y":U.yes=!0;break;case"--help":case"-h":U.help=!0;break;case"--version":case"-v":U.version=!0;break;case"--desc":U.desc=!0;break;case"--page":case"-p":U.page=Number(_[J+1]),J+=1;break;case"--limit":case"-l":U.limit=Number(_[J+1]),J+=1;break;case"--search":case"-s":U.search=_[J+1],J+=1;break;case"--sort":U.sort=_[J+1],J+=1;break;case"--id":U.id=_[J+1],J+=1;break;case"--store":U.store=_[J+1],J+=1;break;case"--title":U.title=_[J+1],J+=1;break;case"--content":U.content=_[J+1],J+=1;break;case"--url":U.url=_[J+1],J+=1;break;case"--tag":case"-t":U.tag=My(U.tag,_[J+1]),U.tagRaw=[...U.tagRaw??[],_[J+1]],J+=1;break;case"--format":U.format=_[J+1],J+=1;break;case"--completions":U.completions=_[J+1],J+=1;break;case"--purpose":U.purpose=_[J+1],J+=1;break;case"--model":U.model=_[J+1],J+=1;break;case"--strategy":U.strategy=_[J+1],J+=1;break;case"--dimensions":U.dimensions=Number(_[J+1]),J+=1;break;case"--semantic":U.semantic=!0;break;case"--context":U.context=!0;break;case"--max-tokens":U.maxTokens=Number(_[J+1]),J+=1;break;case"--max-items":U.maxItems=Number(_[J+1]),J+=1;break;case"--from":U.from=_[J+1],J+=1;break;case"--since":U.since=_[J+1],J+=1;break;case"--topic":U.topic=_[J+1],J+=1;break;case"--dedupe":U.dedupe=!0;break;case"--generate":U.generate=!0;break;case"--approve-write":U.approveWrite=!0;break;case"--provider":U.provider=_[J+1],J+=1;break;case"--mode":U.mode=_[J+1],J+=1;break;case"--machine":U.machine=_[J+1],J+=1;break;case"--workspace":U.workspace=_[J+1],J+=1;break;case"--api-url":U.apiUrl=_[J+1],J+=1;break;case"--canonical-example":U.canonicalExample=!0;break;case"--api-key":U.apiKey=_[J+1],J+=1;break;case"--email":U.email=_[J+1],J+=1;break;case"--org":U.org=_[J+1],J+=1;break;case"--org-id":U.orgId=_[J+1],J+=1;break;case"--user-id":U.userId=_[J+1],J+=1;break;case"--owner":U.owner=_[J+1],J+=1;break;case"--approved-by":U.approvedBy=_[J+1],J+=1;break;case"--patch-uri":U.patchUri=_[J+1],J+=1;break;case"--domain":U.domain=[...U.domain??[],_[J+1]],J+=1;break;case"--file-results":U.fileResults=!0;break;case"--full":U.full=!0;break;case"--dry-run":U.dryRun=!0;break;case"--fake":U.fake=!0;break;case"--no-tailscale":U.tailscale=!1;break;case"--no-artifact-content":U.artifactContent=!1;break;case"--no-color":U.noColor=!0;break;case"--scope":U.scope=_[J+1],J+=1;break;case"--tables":U.tables=_[J+1],J+=1;break;case"--peer-workspace":U.peerWorkspace=_[J+1],J+=1;break;case"--older-than":U.olderThan=Number(_[J+1]),J+=1;break;case"--empty":U.empty=!0;break;case"--archived":U.archived=!0;break;case"--include-archived":U.includeArchived=!0;break;case"--project":U.project=_[J+1],J+=1;break;case"--contract":U.contract=!0;break;case"--source-ref":U.sourceRef=[...U.sourceRef??[],_[J+1]],J+=1;break;case"--allow-global":U.allowGlobal=!0;break;default:throw Error(`Unknown flag: ${D}. Run 'knowledge --help' for valid options.`)}}return{positional:$,flags:U}}function ky(_){if(!_)return"";return UB[_]??_}function Sy(_,$){let U=Array.from({length:_.length+1},()=>Array($.length+1).fill(0));for(let J=0;J<=_.length;J+=1)U[J][0]=J;for(let J=0;J<=$.length;J+=1)U[0][J]=J;for(let J=1;J<=_.length;J+=1)for(let D=1;D<=$.length;D+=1){let j=_[J-1]===$[D-1]?0:1;U[J][D]=Math.min(U[J-1][D]+1,U[J][D-1]+1,U[J-1][D-1]+j)}return U[_.length][$.length]}function wy(_){if(!_)return"";let $=[...$B,...Object.keys(UB)],U="",J=Number.POSITIVE_INFINITY;for(let D of $){let j=Sy(_,D);if(j<J)J=j,U=D}return J<=3?U:""}function fy(){return Ky(process.argv[1]??"").replace(/\.(?:js|ts|mjs|cjs)$/,"")==="knowledge"}async function Ty(_){if(!_B.includes(_[0]??""))return!1;let $=new MR;return $.name("knowledge").description("Agent-friendly local knowledge CLI with JSON output, pagination, and safe destructive actions"),oR($,{source:"knowledge"}),await $.parseAsync(_,{from:"user"}),!0}function Cy(){console.log(`knowledge - local agent knowledge store Usage: knowledge <command> [options] diff --git a/dist/index.js b/dist/index.js index fc9b3f0..ba00d56 100644 --- a/dist/index.js +++ b/dist/index.js @@ -11249,7 +11249,7 @@ function finalize(ctx, schema) { result.$schema = "http://json-schema.org/draft-07/schema#"; } else if (ctx.target === "draft-04") { result.$schema = "http://json-schema.org/draft-04/schema#"; - } else if (ctx.target === "openapi-3.0") {} else {} + } else if (ctx.target === "openapi-3.0") {} if (ctx.external?.uri) { const id = ctx.external.registry.get(schema)?.id; if (!id) @@ -11510,7 +11510,7 @@ var formatMap, stringProcessor = (schema, ctx, _json, _params) => { if (val === undefined) { if (ctx.unrepresentable === "throw") { throw new Error("Literal `undefined` cannot be represented in JSON Schema"); - } else {} + } } else if (typeof val === "bigint") { if (ctx.unrepresentable === "throw") { throw new Error("BigInt literals cannot be represented in JSON Schema"); diff --git a/package.json b/package.json index 97b4025..193b22d 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "smoke:open-files-installed-boundary": "bun scripts/smoke-open-files-installed-boundary.mjs", "migrate:cloud": "bun scripts/apply-cloud-migrations.mjs", "serve": "bun src/serve-entry.ts", - "verify:generated": "bun run build && bun scripts/verify-generated-artifacts.mjs", + "verify:generated": "bun scripts/verify-generated-artifacts.mjs", "build": "rm -rf dist && bun build --target=bun --outfile=bin/knowledge.js --minify --external pg --external @hasna/machines --external @hasna/machines/consumer --external @aws-sdk/client-s3 --external @aws-sdk/credential-providers --external ai --external @ai-sdk/openai --external @ai-sdk/anthropic --external @ai-sdk/deepseek src/cli.ts && bun build --target=bun --outfile=bin/knowledge-mcp.js --external pg --external @hasna/machines --external @hasna/machines/consumer --external @modelcontextprotocol/sdk --external @aws-sdk/client-s3 --external @aws-sdk/credential-providers --external ai --external @ai-sdk/openai --external @ai-sdk/anthropic --external @ai-sdk/deepseek src/mcp.js && bun build --target=bun --outfile=bin/knowledge-serve.js --external pg --external @hasna/machines --external @hasna/machines/consumer --external @aws-sdk/client-s3 --external @aws-sdk/credential-providers --external ai --external @ai-sdk/openai --external @ai-sdk/anthropic --external @ai-sdk/deepseek src/serve-entry.ts && bun build ./src/index.ts ./src/storage.ts ./src/serve.ts --outdir ./dist --target bun --external pg --external @hasna/machines --external @hasna/machines/consumer --external @aws-sdk/client-s3 --external @aws-sdk/credential-providers --external ai --external @ai-sdk/openai --external @ai-sdk/anthropic --external @ai-sdk/deepseek && bun scripts/strip-generated-trailing-whitespace.mjs && bunx tsc -p tsconfig.build.json", "prepublishOnly": "bun run build && node scripts/validate-public-package.mjs" }, diff --git a/scripts/verify-generated-artifacts.mjs b/scripts/verify-generated-artifacts.mjs index ecaadad..8e0fc61 100644 --- a/scripts/verify-generated-artifacts.mjs +++ b/scripts/verify-generated-artifacts.mjs @@ -1,34 +1,201 @@ #!/usr/bin/env bun +/** + * @hasna/knowledge + * Copyright 2026 Hasna Inc. + * Licensed under the Apache License, Version 2.0 + * + * Verify that the committed generated artifacts are the ones the current source produces. + * + * WHAT THIS USED TO BE, AND WHY IT WAS NOT ENOUGH. The previous version ran + * `git diff --exit-code -- bin/knowledge-mcp.js dist` and then grepped four hardcoded files + * for two stale Windows-path patterns. It never rebuilt anything, so on a clean checkout its + * exit 0 meant only "the files I did not touch are untouched". Measured against that version: + * it exited 0 with `src/cli.ts` diverged from `bin/knowledge.js`, and exited 0 with + * `bin/knowledge.js` corrupted by 30 junk bytes — it did not require that file to be + * byte-stable at all. Meanwhile `bun run verify:generated`, which prefixed a `bun run build`, + * exited 1. One check, two names, two different answers, and the passing one was cited as + * evidence of bundle/source sync in a CHANGELOG. + * + * THE THREE RULES THIS VERSION HOLDS TO: + * + * 1. ONE ENTRY POINT. The build happens inside this script, so there is no way to run it + * without the rebuild and read the result as a sync check. `package.json`'s + * `verify:generated` is this script and nothing else; `tests/generated-artifacts.test.ts` + * pins that it stays a single command. + * + * 2. NO HARDCODED ARTIFACT LIST. The gate covers whole directories (GENERATED_PATHS) and + * the pattern scan derives its file list from `git ls-files`, so a bundle added later is + * covered without anyone remembering to add it here. The old four-file list had already + * drifted: it omitted `bin/knowledge-serve.js` and `dist/serve.js`. + * + * 3. THE CHECK PROVES ITS OWN PATTERNS ARE LIVE BEFORE TRUSTING A CLEAN RESULT. A regex + * that can no longer match anything reports "clean" forever. Each entry in + * STALE_PATTERNS carries a fixture it MUST match; if it does not, this script fails + * instead of reporting success. That is the difference between a passing check and a + * check that measured nothing. + */ import { readFileSync } from 'node:fs'; import { spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import { dirname, join, resolve } from 'node:path'; -const stableDiff = spawnSync('git', ['diff', '--exit-code', '--', 'bin/knowledge-mcp.js', 'dist'], { - encoding: 'utf8', - stdio: 'inherit', -}); +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); -if ((stableDiff.status ?? 1) !== 0) { - process.exit(stableDiff.status ?? 1); -} +/** + * Directories whose entire tracked contents are build output. Both are wiped or overwritten + * by `bun run build`, so after a rebuild any difference against the index is drift. + */ +export const GENERATED_PATHS = ['bin', 'dist']; -const generatedFiles = [ - 'bin/knowledge.js', - 'bin/knowledge-mcp.js', - 'dist/index.js', - 'dist/storage.js', +/** + * Generated code that must never ship again, with the fixture that proves each pattern can + * still fire. Both patterns come from the Windows path handling replaced in #33: a bundle + * built before that fix decoded `import.meta.url` by hand and interpolated `file://${...}`, + * which breaks on Windows drive letters. + */ +export const STALE_PATTERNS = [ + { + pattern: /path:\s*decodeURIComponent\([^)]*\.pathname\)/, + describe: 'hand-decoded URL pathname instead of fileURLToPath', + // Verbatim the line 2bea200 removed from src/source-ref.ts, which is what this pattern + // exists to catch. Must match; if a refactor makes this fixture stop matching, the pattern + // is dead and this script fails rather than reporting every artifact clean. + // + // NOTE the pattern is narrow on purpose and this fixture documents the bound: `[^)]*` + // cannot cross a nested `)`, so it does NOT catch + // `path: decodeURIComponent(new URL(uri).pathname)`. Writing that as the fixture was the + // first thing tried here and the self-check rejected it — which is the reason the + // self-check exists. Widening the pattern would trade a known bound for unknown false + // positives across four minified bundles; if that inline form ever ships, add it as its + // own entry with its own fixture. + fixture: "return { kind: 'file', uri, path: decodeURIComponent(parsed.pathname) };", + // Must NOT match, so the pattern is not so loose that it fires on the fixed form. + counterFixture: "return { kind: 'file', uri, path: fileURLToPath(uri) };", + }, + { + pattern: /file:\/\/\$\{/, + describe: 'interpolated file:// URL instead of pathToFileURL', + fixture: 'await import(`file://${target}`);', + counterFixture: 'await import(pathToFileURL(target).href);', + }, ]; -const stalePatterns = [ - /path:\s*decodeURIComponent\([^)]*\.pathname\)/, - /file:\/\/\$\{/, -]; +function git(args) { + const run = spawnSync('git', args, { cwd: repoRoot, encoding: 'utf8' }); + // A spawn that never started reports status null. Treat that as failure: this whole script + // is a control, and a control that cannot run must not pass. + return { status: run.status ?? 1, stdout: run.stdout ?? '', stderr: run.stderr ?? '' }; +} + +/** Tracked `.js` files under the generated directories, derived rather than listed. */ +export function generatedJsFiles() { + const listed = git(['ls-files', '--', ...GENERATED_PATHS]); + if (listed.status !== 0) throw new Error(`git ls-files failed: ${listed.stderr.trim()}`); + return listed.stdout.split('\n').map((line) => line.trim()).filter((line) => line.endsWith('.js')).sort(); +} + +/** + * Assert every stale pattern still matches its fixture and still rejects its counter-fixture. + * Returns the list of problems; an empty list means the scan below is worth believing. + */ +export function patternSelfCheck() { + const problems = []; + for (const { pattern, describe, fixture, counterFixture } of STALE_PATTERNS) { + if (!pattern.test(fixture)) problems.push(`stale pattern ${pattern} (${describe}) no longer matches its own fixture — it cannot detect anything`); + if (pattern.test(counterFixture)) problems.push(`stale pattern ${pattern} (${describe}) matches its counter-fixture — it is too loose to be meaningful`); + } + return problems; +} -for (const file of generatedFiles) { - const text = readFileSync(file, 'utf8'); - for (const pattern of stalePatterns) { - if (pattern.test(text)) { - console.error(`${file} contains stale generated Windows path handling: ${pattern}`); - process.exit(1); +/** + * Scan the generated bundles for stale generated code. Returns the list of problems. + * + * `root` is a parameter rather than a closed-over constant so a test can point the scanner at + * a temp directory holding a deliberately stale file and prove the scan actually fires. A scan + * that has only ever been run against clean input is not evidence that it detects anything. + */ +export function scanForStalePatterns(files, root = repoRoot) { + const problems = []; + for (const file of files) { + const text = readFileSync(join(root, file), 'utf8'); + for (const { pattern, describe } of STALE_PATTERNS) { + if (pattern.test(text)) problems.push(`${file} contains stale generated code (${describe}): ${pattern}`); } } + return problems; +} + +function fail(message) { + console.error(`verify-generated-artifacts: ${message}`); + process.exit(1); } + +function main() { + // The patterns are checked BEFORE anything else, so a dead pattern cannot be masked by a + // clean rebuild. + const patternProblems = patternSelfCheck(); + if (patternProblems.length > 0) { + for (const problem of patternProblems) console.error(`verify-generated-artifacts: ${problem}`); + process.exit(1); + } + + // Precondition. The gate below is `git diff` after a rebuild, which only means something if + // the generated paths matched the index BEFORE the rebuild. If they already differed, a + // difference afterwards proves nothing about whether the source and the bundles agree. + const dirtyBefore = git(['status', '--porcelain', '--', ...GENERATED_PATHS]); + if (dirtyBefore.status !== 0) fail(`git status failed: ${dirtyBefore.stderr.trim()}`); + if (dirtyBefore.stdout.trim() !== '') { + fail( + `generated paths are already modified before the rebuild, so this check cannot tell drift from your edits:\n${dirtyBefore.stdout.trimEnd()}\n` + + 'Commit or restore them, then re-run.' + ); + } + + // Rebuild through the package script rather than repeating its command here, so the two can + // never disagree. + const build = spawnSync('bun', ['run', 'build'], { cwd: repoRoot, stdio: 'inherit' }); + if ((build.status ?? 1) !== 0) fail(`\`bun run build\` exited ${build.status ?? 'without a status'}`); + + // The gate: whatever the rebuild produced must equal what is committed. + const drift = spawnSync('git', ['diff', '--exit-code', '--', ...GENERATED_PATHS], { + cwd: repoRoot, + encoding: 'utf8', + stdio: 'inherit', + }); + if ((drift.status ?? 1) !== 0) { + // The bun version is printed because it is a real cause of this failure and the least + // guessable one. Measured: the same source at this commit builds 4 bytes-differing lines + // under bun 1.3.13 versus 1.3.14 (1.3.13 keeps empty `else {}` blocks that 1.3.14's + // dead-code elimination collapses), across bin/knowledge-mcp.js and dist/index.js. So a + // developer on a bun other than CI's pin sees this message with nothing stale at all — and + // "commit it" would then be the WRONG action: it makes CI red instead. Check the version + // against .github/workflows/ci.yml before committing a rebuild. + console.error( + 'verify-generated-artifacts: the committed generated artifacts are not what the current source builds.\n' + + `This bun is ${process.versions?.bun ?? 'unknown'}. The byte comparison is bun-version-sensitive, so FIRST check that this\n` + + 'matches the `bun-version` pinned in .github/workflows/ci.yml. If it does not, fix the version — do NOT\n' + + 'commit a rebuild, which would only move the failure into CI.\n' + + 'If the version already matches, run `bun run build` and commit the result. Genuine toolchain churn at the\n' + + 'pinned version is still drift — commit it.' + ); + process.exit(drift.status ?? 1); + } + + const files = generatedJsFiles(); + // A silently emptied file list would make the scan below vacuous, so require it to be + // non-empty rather than looping zero times and reporting success. + if (files.length === 0) fail(`no tracked .js files found under ${GENERATED_PATHS.join(', ')} — the pattern scan would measure nothing`); + + const scanProblems = scanForStalePatterns(files); + if (scanProblems.length > 0) { + for (const problem of scanProblems) console.error(`verify-generated-artifacts: ${problem}`); + process.exit(1); + } + + console.log(`verify-generated-artifacts: ${files.length} generated bundles rebuild byte-identically and carry no stale generated code.`); +} + +// Run only when invoked directly, so the exports above are importable from tests. `process.argv[1]` +// rather than `import.meta.main`, which is not available on the older Node versions that may run +// this script from a published install. +if (process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) main(); diff --git a/tests/generated-artifacts.test.ts b/tests/generated-artifacts.test.ts new file mode 100644 index 0000000..e24bdd6 --- /dev/null +++ b/tests/generated-artifacts.test.ts @@ -0,0 +1,173 @@ +/** + * @hasna/knowledge + * Copyright 2026 Hasna Inc. + * Licensed under the Apache License, Version 2.0 + */ +import { describe, expect, test } from 'bun:test'; +import { mkdtempSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { fileURLToPath } from 'node:url'; +import { + GENERATED_PATHS, + STALE_PATTERNS, + generatedJsFiles, + patternSelfCheck, + scanForStalePatterns, + // @ts-expect-error - .mjs build script, intentionally untyped and not part of the public API +} from '../scripts/verify-generated-artifacts.mjs'; + +const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..'); + +const packageJson = JSON.parse(readFileSync(join(repoRoot, 'package.json'), 'utf8')) as { + scripts: Record<string, string>; +}; + +/** + * The bundles this repo ships today. Hardcoded on purpose, as the counterweight to + * `generatedJsFiles()` deriving its list from git: if the derivation ever comes back short — + * a `.gitignore` rule swallowing a bundle, a rename, an emptied result — the scan it feeds + * would silently cover fewer files while still reporting success. The old verifier had + * exactly that drift: its hardcoded four-file list had stopped naming bin/knowledge-serve.js + * and dist/serve.js. + */ +const EXPECTED_BUNDLES = [ + 'bin/knowledge-mcp.js', + 'bin/knowledge-serve.js', + 'bin/knowledge.js', + 'dist/index.js', + 'dist/serve.js', + 'dist/storage.js', +].sort(); + +describe('generated artifact verification', () => { + test('the gate covers whole generated directories, not a list that can drift', () => { + expect(GENERATED_PATHS.length).toBeGreaterThan(0); + expect([...GENERATED_PATHS].sort()).toEqual(['bin', 'dist']); + }); + + test('the scanned file list is derived from git and covers every shipped bundle', () => { + const files = generatedJsFiles(); + // Non-empty first: a zero-length list makes the pattern scan loop zero times and report + // clean, which is the failure mode this file exists to prevent. + expect(files.length).toBeGreaterThan(0); + for (const bundle of EXPECTED_BUNDLES) { + expect(files, `${bundle} must be scanned for stale generated code`).toContain(bundle); + } + // And nothing outside the generated directories crept in. + for (const file of files) { + expect(GENERATED_PATHS.some((prefix) => file.startsWith(`${prefix}/`)), `${file} is outside ${GENERATED_PATHS.join(', ')}`).toBe(true); + } + }); + + // A regex that can no longer match anything reports "clean" forever. This is the assertion + // that makes a clean scan mean something. + test('every stale pattern still fires on its own fixture and still rejects ordinary code', () => { + expect(STALE_PATTERNS.length).toBeGreaterThan(0); + for (const entry of STALE_PATTERNS) { + expect(entry.fixture, `${entry.describe}: fixture must not be empty`).not.toBe(''); + expect(entry.counterFixture, `${entry.describe}: counter-fixture must not be empty`).not.toBe(''); + expect(entry.pattern.test(entry.fixture), `${entry.describe}: pattern must match its fixture`).toBe(true); + expect(entry.pattern.test(entry.counterFixture), `${entry.describe}: pattern must not match its counter-fixture`).toBe(false); + } + expect(patternSelfCheck()).toEqual([]); + }); + + test('the shipped bundles carry no stale generated code', () => { + expect(scanForStalePatterns(generatedJsFiles())).toEqual([]); + }); + + // The scan above has only ever been run against clean input, which proves nothing about + // whether it detects anything. Point it at a directory holding a deliberately stale file and + // require one problem per pattern. + test('the scan reports real breakage when a bundle is stale', () => { + const root = mkdtempSync(join(tmpdir(), 'kn-generated-')); + mkdirSync(join(root, 'bin')); + const planted: string[] = []; + for (const [index, entry] of STALE_PATTERNS.entries()) { + const file = `bin/planted-${index}.js`; + writeFileSync(join(root, file), `// synthetic stale bundle\n${entry.fixture}\n`); + planted.push(file); + } + const problems = scanForStalePatterns(planted, root); + expect(problems.length).toBe(STALE_PATTERNS.length); + for (const entry of STALE_PATTERNS) { + expect(problems.some((problem: string) => problem.includes(entry.describe)), `no problem reported for: ${entry.describe}`).toBe(true); + } + // And the counter-fixtures must come back clean through the same code path, so the scan is + // not simply flagging every file it is handed. + const clean: string[] = []; + for (const [index, entry] of STALE_PATTERNS.entries()) { + const file = `bin/clean-${index}.js`; + writeFileSync(join(root, file), `// synthetic clean bundle\n${entry.counterFixture}\n`); + clean.push(file); + } + expect(scanForStalePatterns(clean, root)).toEqual([]); + }); + + // The defect this replaces was not a wrong answer, it was TWO answers under two names: + // `bun scripts/verify-generated-artifacts.mjs` exited 0 while `bun run verify:generated` + // exited 1, because only the npm script prefixed a rebuild. The rebuild now lives inside the + // script, so there must be exactly one command and it must be the script. + test('verify:generated is a single command, so there is only one answer', () => { + const script = packageJson.scripts['verify:generated']; + expect(script).toBeDefined(); + expect(script).toContain('scripts/verify-generated-artifacts.mjs'); + // No `&&`/`;`/`|` chaining: a prefixed build is what made the bare script's exit code + // misleading, and a suffixed command would hide this script's exit code. + expect(script).not.toContain('&&'); + expect(script).not.toContain(';'); + expect(script).not.toContain('|'); + }); + + test('CI runs the single entry point rather than reassembling it', () => { + const workflow = readFileSync(join(repoRoot, '.github', 'workflows', 'ci.yml'), 'utf8'); + // Positive control: prove this test read the right file and that the step still exists, + // so the two `not.toContain` assertions below are absences in a file that has content. + expect(workflow).toContain('Verify generated artifacts'); + // The two-command form is what produced two different exit codes under two names. + expect(workflow).not.toContain('bun run build && bun scripts/verify-generated-artifacts.mjs'); + expect(workflow).not.toContain('bun scripts/verify-generated-artifacts.mjs'); + expect(workflow).toContain('bun run verify:generated'); + }); + + // Regression test for a defect found in adversarial review of this PR. `Verify generated + // artifacts` rebuilds bin/ and dist/ and compares BYTE-FOR-BYTE, so it passes only when CI's + // bun equals the bun that built the committed bundles. Measured at this commit: the committed + // bundles are bun 1.3.14 output, and rebuilding under bun 1.3.13 drifts by 4 lines across + // bin/knowledge-mcp.js and dist/index.js (1.3.13 keeps the empty `else {}` blocks 1.3.14's + // dead-code elimination collapses). The version cannot be asserted against the bundles from + // inside the suite without running a build, which is the trade documented at the bottom of + // this file — but the two jobs pinning DIFFERENT versions is checkable here, and pin skew + // would make the gate pass in one job and fail in the other for reasons unrelated to source. + test('every CI job pins the same bun version, so the byte comparison has one answer', () => { + const workflow = readFileSync(join(repoRoot, '.github', 'workflows', 'ci.yml'), 'utf8'); + const pins = [...workflow.matchAll(/^\s*bun-version:\s*(\S+)\s*$/gm)].map((match) => match[1]); + // Positive control: an empty match list would make the uniqueness assertion below vacuous. + expect(pins.length).toBeGreaterThan(1); + expect(new Set(pins).size, `CI pins more than one bun version: ${pins.join(', ')}`).toBe(1); + // And it is pinned to an exact patch, not a floating range: `latest` or `1.3` would let the + // minifier change under the gate without any commit in this repo. + expect(pins[0]).toMatch(/^\d+\.\d+\.\d+$/); + }); +}); + +// DELIBERATELY NOT TESTED HERE: end-to-end behaviour of the script itself — that it exits 1 on a +// planted bundle divergence and 1 on a dirty-before-rebuild tree. Both would require running +// `bun run build` inside the suite, which overwrites bin/ and dist/ in the working tree while +// other test files are running. A guard that corrupts the tree it guards is a worse trade than +// the coverage it buys. The pieces the script composes are unit-tested above. +// +// AND THE COMPENSATING CONTROL IS NOT YET OPERATIVE — corrected in adversarial review, because +// an earlier version of this comment claimed "the end-to-end path is exercised by CI on every +// PR, which is where a real divergence shows up." It is not. `Verify generated artifacts` runs +// AFTER `Run tests` in ci.yml with no `if: always()`, so a failing test suite skips it. Measured +// on this PR's own CI run 30310387905, job `test (ubuntu-latest, bun)`: step 6 `Run tests` +// failure, step 7 `Verify generated artifacts` **skipped**. The pre-existing +// `context pack and proposal context commands return bounded agent JSON` failure is red on main +// too, so the step is skipped on every run today — which is exactly the masking this PR's own +// description identifies as the reason the stale bundle survived in the first place. Citing a +// check that does not execute is the defect this PR exists to fix, so it must not be this file's +// justification. Until the suite is green the end-to-end path is exercised only by hand. +// Tracked: reorder the step or add `if: always()` so the gate cannot be skipped by an unrelated +// red test.