@north-light/cli 0.3.427 → 0.3.429

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,2 +1,2 @@
1
- var f=Object.defineProperty;var o=(e,t)=>f(e,"name",{value:t,configurable:!0});import{usage as m}from"../../core/errors.js";import{ApiError as w}from"../../api/errors.js";import{rethrowAsCliError as c}from"../api-client.js";import{RULE_ON as v,RULE_DELIVER as y,DOC_KINDS as g}from"../../api/dto/docs.js";import{PRIVACY_LEVELS_DTO as b}from"../../api/dto/objects.js";const O="A document name (with or without [[ ]]) or a raw object id.",p="node | user | profile | profile:<name> | app | repo | repo:<handle>";function j(e){return{kind:"flag",name:"owner",type:"string",required:!1,constraint:e,focusedHelp:{whenToUse:"Omit it for a document only this node and the nodes it points at need: the writing node owns it and it is deleted with the node, unless a kept document links to it. Give another owner when the document must outlive this node or reach other nodes by itself. A document worth another owner carries reusable current truth: a non-obvious procedure, a gotcha, a lasting preference, a convention across repos. Task notes, conversation summaries and facts the repo already records stay with the node or are not written. Before writing one, search for a document that already covers it (`crtr canvas search`) and revise that one with `crtr doc edit` instead of adding a near copy. When one document grows long, keep the most important content in it and move separate subjects into documents named under it (`topic/part`), each pointed at with a `[[link]]`. Choose the narrowest owner that still reaches the next agent who needs it.",value:`One of ${p}. \`node\` \u2014 the writing node; only this node loads it by rule, other nodes read it when pointed at it; deleted with the node. \`profile\` \u2014 the node's profile, or \`profile:<name>\`; loads for nodes running in that profile; stays on this runtime. \`app\` \u2014 the app that runs the node; kept until deleted; readable by that app's nodes. \`repo\` \u2014 the repo the node works in (from its working directory), or \`repo:<handle>\`; every clone and worktree of the repo shares it, and it syncs to collaborators over the repo's \`refs/crtr/docs\`. A repo owns only public documents: a private or friends document that should load in one repo is owned by \`user\` and carries a rule with \`if: {"repo": "<handle>"}\`. \`user\` \u2014 you, the person; loads for every node by its rules; readable by apps by privacy level. Default: the writing node; a person's own run (not a node) defaults to \`user\`. The owner becomes the start of the document's full name (\`<owner-handle>/<name>\`); \`crtr doc move --owner\` changes it later and keeps the id and every link.`,effects:["Decides who may read and change the document, where it syncs, and what deleting takes it with."]}}}o(j,"ownerParam");const k="`if` is a condition on the receiving node. Fields: `kind`, `mode`, `lifecycle`, `cwd`, `repo` (the handle of a repo the node works in), `profile`, `depth`. A field map is ANDed across fields; dotted fields resolve nested values. A scalar matches by equality (a list value matches when any element is equal); a list matcher matches by membership or overlap; an object matcher takes `eq`, `ne`, `in`, `nin`, `exists`, `contains`, `containsAll`, `containsAny`, `matches`, `imatches`, `gt`, `gte`, `lt`, `lte`. `all`, `any` and `not` combine conditions; field matchers beside them are ANDed in. An empty condition always matches; an unknown operator never matches. `match_frontmatter` uses the same language against the read file's YAML frontmatter.";function S(e){return{kind:"flag",name:"rule",type:"string",required:!1,repeatable:!0,constraint:e,focusedHelp:{whenToUse:"Omit it for most documents: a document with no rule is read when a node is pointed at it (`[[name]]`), finds it with `crtr canvas search`, or sees it in a listing. Add a rule only when the document must load on its own because of what a node is doing; every rule costs context in every node it fires on. Prefer the narrowest trigger: a `file-read` rule with a real glob over `boot` for knowledge about specific code; `boot` for knowledge about a person or process with no file boundary. When the whole document is one sentence, deliver `content` \u2014 a preview line would be longer than the document.",value:`One JSON object per occurrence: \`{"on": \u2026, "match"?: [globs], "match_frontmatter"?: {\u2026}, "if"?: {\u2026}, "deliver": \u2026}\`. \`on\` is the node's activity: ${v.join(" | ")}. \`boot\` \u2014 every session start of the node. \`workspace-open\` \u2014 the node's first message when it works in the document's repo. \`file-read\` \u2014 the node reads a file whose absolute path or basename matches a \`match\` glob, or whose frontmatter matches \`match_frontmatter\`. \`document-read\` \u2014 another document whose name matches is delivered at content, including an explicit read. \`command\` \u2014 a shell segment of a bash command matches (segments split on \`&&\`, \`||\`, \`;\`, \`|\` and newlines outside quotes, leading \`VAR=value\` dropped; \`*\` spans whole whitespace-separated tokens, \`?\` one character, quoted text is opaque). \`pre-command\` \u2014 as \`command\`, but runs before the command: when the node has not read the document, the command does not run and the document is returned instead; the node re-issues the command. \`slash-command\` \u2014 the document is a \`/<name>\` slash command whose body is the injected prompt. \`match\` is required on \`file-read\` (unless \`match_frontmatter\` is given), \`document-read\`, \`command\` and \`pre-command\`, and not allowed on \`boot\`, \`workspace-open\` and \`slash-command\`. `+k+` \`deliver\` is ${y.join(" | ")}: the bare name, the preview line, or the whole body. The flag set replaces the document's whole rule list; on \`crtr doc edit\`, \`--unset rules\` removes every rule.`,effects:["When several rules deliver one document to a node, the highest delivery wins: content over preview over name; content already in the node's context is not delivered again.","A document delivered at content is a read: it records the node as a watcher, so the node is pushed later edits.","The profile's delivery limit per owner caps what `boot` and `workspace-open` rules deliver for that owner.",'A rule that delivers at `preview` requires the preview in the shape "When <situation in the reader\'s work>, read this because <what the reader gains>"; `crtr doc lint` checks it. There the line alone decides whether a node that did not ask for the document opens it.']}}}o(S,"ruleParam");const N={preview:o(e=>({kind:"flag",name:"preview",type:"string",required:!1,constraint:e}),"preview"),kind:{kind:"flag",name:"kind",type:"enum",choices:[...g],required:!1,constraint:"knowledge is consulted for facts or procedures; preference directs behavior. Default knowledge."},summary:{kind:"flag",name:"summary",type:"string",required:!1,constraint:"A very short version of the content, shown in `crtr canvas list`."},privacy:o(e=>({kind:"flag",name:"privacy",type:"enum",choices:[...b],required:!1,constraint:e}),"privacy"),whyItExists:{kind:"flag",name:"why-it-exists",type:"string",required:!1,constraint:"The observed failure this document exists to prevent; shown only with `crtr canvas read --fields`."},unlisted:o(e=>({kind:"flag",name:"unlisted",type:"bool",required:!1,constraint:e}),"unlisted"),lintIgnore:{kind:"flag",name:"lint-ignore",type:"string",required:!1,repeatable:!0,constraint:"A `crtr doc lint` rule name to skip for this document."},extension:{kind:"flag",name:"extension",type:"string",required:!1,repeatable:!0,constraint:"One declared plugin field as `<plugin>.<field>=<json value>`."}};function h(e){return e===void 0?[]:Array.isArray(e)?e.map(String):[String(e)]}o(h,"list");function T(e){const t=[],n={};for(const[r,a]of[["preview","preview"],["kind","kind"],["summary","summary"],["privacy","privacy"],["why-it-exists","why_it_exists"]])typeof e[r]=="string"&&(n[a]=e[r]);typeof n.preview=="string"&&/[\r\n]/.test(n.preview)&&t.push("--preview must be one line"),e.unlisted===!0&&(n.unlisted=!0);const s=h(e["lint-ignore"]);s.length>0&&(n.lint_ignore=s);const i=h(e.rule);if(i.length>0){const r=[];i.forEach((a,u)=>{try{const d=JSON.parse(a);typeof d!="object"||d===null||Array.isArray(d)?t.push(`--rule #${u+1} must be a JSON object`):r.push(d)}catch{t.push(`--rule #${u+1} is not valid JSON: ${a}`)}}),n.rules=r}const l={};for(const r of h(e.extension)){const a=/^([^.=]+)\.([^=]+)=(.*)$/s.exec(r);if(!a){t.push(`--extension ${r} is not <plugin>.<field>=<json value>`);continue}try{(l[a[1]]??={})[a[2]]=JSON.parse(a[3])}catch{t.push(`--extension ${r}: the value after = is not valid JSON`)}}if(Object.keys(l).length>0&&(n.extensions=l),t.length>0)throw m(t.join("; "),{next:"Fix every value above and rerun; `--rule -h` gives the rule grammar."});return n}o(T,"fieldsFromInput");function D(e){const t=e.owner;if(t===void 0)return{};const n=String(t);if(!/^(node|user|profile|app|repo)$|^(profile|repo):.+$/.test(n))throw m(`invalid --owner ${n}`,{field:"owner",next:`Pass one of ${p}.`});return{owner:n}}o(D,"ownerFromInput");function R(e,t){e instanceof w&&(e.code==="read_first"&&c(e,`Read it first with \`crtr canvas read ${t}\`, then rerun the edit.`),e.code==="stale_revision"&&c(e,`Someone edited it after your read. Read it again with \`crtr canvas read ${t}\`, make your change on the current text, and rerun the edit.`)),c(e)}o(R,"rethrowDocError");function I(e){const t=[],n=e.unresolved_links,s=e.remote_links;return n?.length&&t.push(`Unresolved links (no document by these names yet): ${n.map(i=>`[[${i}]]`).join(", ")}`),s?.length&&t.push(`Not on this runtime: ${s.map(i=>`[[${i}]]`).join(", ")} \u2014 each may be on another runtime, or the name may be misspelled.`),t.map(i=>`
2
- ${i}`).join("")}o(I,"renderLinkReport");export{N as FIELD_FLAGS,p as OWNER_VALUES,O as REF_CONSTRAINT,T as fieldsFromInput,D as ownerFromInput,j as ownerParam,I as renderLinkReport,R as rethrowDocError,S as ruleParam};
1
+ var f=Object.defineProperty;var o=(e,t)=>f(e,"name",{value:t,configurable:!0});import{usage as m}from"../../core/errors.js";import{ApiError as w}from"../../api/errors.js";import{rethrowAsCliError as c}from"../api-client.js";import{RULE_ON as v,RULE_DELIVER as y,DOC_KINDS as g}from"../../api/dto/docs.js";import{PRIVACY_LEVELS_DTO as b}from"../../api/dto/objects.js";const O="A document name (with or without [[ ]]) or a raw object id.",p="node | user | profile | profile:<name> | app | repo | repo:<handle>";function j(e){return{kind:"flag",name:"owner",type:"string",required:!1,constraint:e,focusedHelp:{whenToUse:"Omit it for a document only this node and the nodes it points at need: the writing node owns it and it is deleted with the node, unless a kept document links to it. Give another owner when the document must outlive this node or reach other nodes by itself. A document worth another owner carries reusable current truth: a non-obvious procedure, a gotcha, a lasting preference, a convention across repos. Task notes, conversation summaries and facts the repo already records stay with the node or are not written. Before writing one, search for a document that already covers it (`crtr canvas search`) and revise that one with `crtr doc edit` instead of adding a near copy. When one document grows long, keep the most important content in it and move separate subjects into documents named under it (`topic/part`), each pointed at with a `[[link]]`. Choose the narrowest owner that still reaches the next agent who needs it.",value:`One of ${p}. \`node\` \u2014 the writing node; only this node loads it by rule, other nodes read it when pointed at it; deleted with the node. \`profile\` \u2014 the node's profile, or \`profile:<name>\`; loads for nodes running in that profile; stays on this runtime. \`app\` \u2014 the app that runs the node; kept until deleted; readable by that app's nodes. \`repo\` \u2014 the repo the node works in (from its working directory), or \`repo:<handle>\`; every clone and worktree of the repo shares it, and it syncs to collaborators over the repo's \`refs/crtr/docs\`. A repo owns only public documents: a private or friends document that should load in one repo is owned by \`user\` and carries a rule with \`if: {"repo": "<handle>"}\`. \`user\` \u2014 you, the person; loads for every node by its rules; readable by apps by privacy level. Default: the writing node; a person's own run (not a node) defaults to \`user\`. The owner becomes the start of the document's full name (\`<owner-handle>/<name>\`); \`crtr doc move --owner\` changes it later and keeps the id and every link.`,effects:["Decides who may read and change the document, where it syncs, and what deleting takes it with."]}}}o(j,"ownerParam");const k="`if` is a condition on the receiving node. Fields: `kind`, `mode`, `lifecycle`, `cwd`, `repo` (the handle of a repo the node works in), `profile`, `depth`. A field map is ANDed across fields; dotted fields resolve nested values. A scalar matches by equality (a list value matches when any element is equal); a list matcher matches by membership or overlap; an object matcher takes `eq`, `ne`, `in`, `nin`, `exists`, `contains`, `containsAll`, `containsAny`, `matches`, `imatches`, `gt`, `gte`, `lt`, `lte`. `all`, `any` and `not` combine conditions; field matchers beside them are ANDed in. An empty condition always matches; an unknown operator never matches. `match_frontmatter` uses the same language against the read file's YAML frontmatter.";function S(e){return{kind:"flag",name:"rule",type:"string",required:!1,repeatable:!0,constraint:e,focusedHelp:{whenToUse:"Omit it for most documents: a document with no rule is read when a node is pointed at it (`[[name]]`), finds it with `crtr canvas search`, or sees it in a listing. Add a rule only when the document must load on its own because of what a node is doing; every rule costs context in every node it fires on. Prefer the narrowest trigger: a `file-read` rule with a real glob over `boot` for knowledge about specific code; `boot` for knowledge about a person or process with no file boundary. When the whole document is one sentence, deliver `content` \u2014 a preview line would be longer than the document.",value:`One JSON object per occurrence: \`{"on": \u2026, "match"?: [globs], "match_frontmatter"?: {\u2026}, "if"?: {\u2026}, "deliver": \u2026}\`. \`on\` is the node's activity: ${v.join(" | ")}. \`boot\` \u2014 every session start of the node. \`workspace-open\` \u2014 the node's first message when it works in the document's repo. \`file-read\` \u2014 the node reads a file whose absolute path or basename matches a \`match\` glob, or whose frontmatter matches \`match_frontmatter\`. \`document-read\` \u2014 another document whose name matches is delivered at content, including an explicit read. \`command\` \u2014 a shell segment of a bash command matches (segments split on \`&&\`, \`||\`, \`;\`, \`|\` and newlines outside quotes, leading \`VAR=value\` dropped; \`*\` spans whole whitespace-separated tokens, \`?\` one character, quoted text is opaque). \`pre-command\` \u2014 as \`command\`, but runs before the command: when the node has not read the document, the command does not run and the document is returned instead; the node re-issues the command. \`slash-command\` \u2014 the document is a \`/<name>\` slash command whose body is the injected prompt. \`match\` is required on \`file-read\` (unless \`match_frontmatter\` is given), \`document-read\`, \`command\` and \`pre-command\`, and not allowed on \`boot\`, \`workspace-open\` and \`slash-command\`. `+k+` \`deliver\` is ${y.join(" | ")}: the bare name, the preview line, or the whole body. The flag set replaces the document's whole rule list; on \`crtr doc edit\`, \`--unset rules\` removes every rule.`,effects:["When several rules deliver one document to a node, the highest delivery wins: content over preview over name; content already in the node's context is not delivered again.","A document delivered at content is a read: it records the node as a watcher, so the node is pushed later edits.","The profile's delivery limit per owner caps what `boot` and `workspace-open` rules deliver for that owner.",'A rule that delivers at `preview` requires the preview in the shape "When <situation in the reader\'s work>, read this because <what the reader gains>"; `crtr doc lint` checks it. There the line alone decides whether a node that did not ask for the document opens it.']}}}o(S,"ruleParam");const N={preview:o(e=>({kind:"flag",name:"preview",type:"string",required:!1,constraint:e}),"preview"),kind:{kind:"flag",name:"kind",type:"enum",choices:[...g],required:!1,constraint:"knowledge is consulted for facts or procedures; preference directs behavior. Default knowledge."},summary:{kind:"flag",name:"summary",type:"string",required:!1,constraint:"A very short version of the content, shown in `crtr canvas list`."},privacy:o(e=>({kind:"flag",name:"privacy",type:"enum",choices:[...b],required:!1,constraint:e}),"privacy"),whyItExists:{kind:"flag",name:"why-it-exists",type:"string",required:!1,constraint:"The observed failure this document exists to prevent; shown only with `crtr canvas read --fields`."},unlisted:o(e=>({kind:"flag",name:"unlisted",type:"bool",required:!1,constraint:e}),"unlisted"),lintIgnore:{kind:"flag",name:"lint-ignore",type:"string",required:!1,repeatable:!0,constraint:"A `crtr doc lint` rule name to skip for this document."},extension:{kind:"flag",name:"extension",type:"string",required:!1,repeatable:!0,constraint:"One declared plugin field as `<plugin>.<field>=<json value>`."}};function h(e){return e===void 0?[]:Array.isArray(e)?e.map(String):[String(e)]}o(h,"list");function T(e){const t=[],n={};for(const[r,a]of[["preview","preview"],["kind","kind"],["summary","summary"],["privacy","privacy"],["whyItExists","why_it_exists"]])typeof e[r]=="string"&&(n[a]=e[r]);typeof n.preview=="string"&&/[\r\n]/.test(n.preview)&&t.push("--preview must be one line"),e.unlisted===!0&&(n.unlisted=!0);const s=h(e.lintIgnore);s.length>0&&(n.lint_ignore=s);const i=h(e.rule);if(i.length>0){const r=[];i.forEach((a,u)=>{try{const d=JSON.parse(a);typeof d!="object"||d===null||Array.isArray(d)?t.push(`--rule #${u+1} must be a JSON object`):r.push(d)}catch{t.push(`--rule #${u+1} is not valid JSON: ${a}`)}}),n.rules=r}const l={};for(const r of h(e.extension)){const a=/^([^.=]+)\.([^=]+)=(.*)$/s.exec(r);if(!a){t.push(`--extension ${r} is not <plugin>.<field>=<json value>`);continue}try{(l[a[1]]??={})[a[2]]=JSON.parse(a[3])}catch{t.push(`--extension ${r}: the value after = is not valid JSON`)}}if(Object.keys(l).length>0&&(n.extensions=l),t.length>0)throw m(t.join("; "),{next:"Fix every value above and rerun; `--rule -h` gives the rule grammar."});return n}o(T,"fieldsFromInput");function D(e){const t=e.owner;if(t===void 0)return{};const n=String(t);if(!/^(node|user|profile|app|repo)$|^(profile|repo):.+$/.test(n))throw m(`invalid --owner ${n}`,{field:"owner",next:`Pass one of ${p}.`});return{owner:n}}o(D,"ownerFromInput");function I(e,t){e instanceof w&&(e.code==="read_first"&&c(e,`Read it first with \`crtr canvas read ${t}\`, then rerun the edit.`),e.code==="stale_revision"&&c(e,`Someone edited it after your read. Read it again with \`crtr canvas read ${t}\`, make your change on the current text, and rerun the edit.`)),c(e)}o(I,"rethrowDocError");function R(e){const t=[],n=e.unresolved_links,s=e.remote_links;return n?.length&&t.push(`Unresolved links (no document by these names yet): ${n.map(i=>`[[${i}]]`).join(", ")}`),s?.length&&t.push(`Not on this runtime: ${s.map(i=>`[[${i}]]`).join(", ")} \u2014 each may be on another runtime, or the name may be misspelled.`),t.map(i=>`
2
+ ${i}`).join("")}o(R,"renderLinkReport");export{N as FIELD_FLAGS,p as OWNER_VALUES,O as REF_CONSTRAINT,T as fieldsFromInput,D as ownerFromInput,j as ownerParam,R as renderLinkReport,I as rethrowDocError,S as ruleParam};
@@ -1,2 +1,2 @@
1
- var k=Object.defineProperty;var s=(l,w)=>k(l,"name",{value:w,configurable:!0});import{MessageChannel as E,Worker as S,receiveMessageOnPort as b}from"node:worker_threads";import{ConversationNotFoundError as g}from"./index.js";function N(l,w){const u=new S(new URL(import.meta.url.endsWith(".ts")?"./client-worker.ts":"./client-worker.js",import.meta.url),{workerData:{address:l,token:w}});u.unref();let c;u.on("error",e=>{c=e}),u.on("exit",e=>{c=new Error(`conversation store worker exited with ${e}`)});function m(e,r,t){if(!t)throw new Error("conversation store worker returned no reply");if(t.error)throw new Error(t.error);if(t.status===404)throw new g(r.split("/")[2]??r);if(!t.status||t.status>=400)throw new Error(`conversation store ${e} ${r} failed (${t.status}): ${t.body}`);return{body:t.body??"",...t.path?{path:t.path}:{},...t.tail?{tail:Number(t.tail)}:{}}}s(m,"settle");function n(e,r,t){if(c)throw c;const o=new SharedArrayBuffer(4),{port1:i,port2:d}=new E;try{if(u.postMessage({method:e,path:r,data:t,signal:o,reply:d},[d]),Atomics.wait(new Int32Array(o),0,0,3e4)==="timed-out")throw i.postMessage("cancel"),new Error(`conversation store request timed out: ${e} ${r}`);return m(e,r,b(i)?.message)}finally{i.close()}}s(n,"call");function p(e,r){if(c)return Promise.reject(c);const{port1:t,port2:o}=new E;return new Promise((i,d)=>{const f=setTimeout(()=>{t.postMessage("cancel"),t.close(),d(new Error(`conversation store request timed out: ${e} ${r}`))},3e4);f.unref(),t.once("message",v=>{clearTimeout(f),t.close();try{i(m(e,r,v))}catch(T){d(T)}}),u.postMessage({method:e,path:r,reply:o},[o])})}s(p,"callAsync");const a=s(e=>`/conversations/${encodeURIComponent(e)}`,"url"),$=s(e=>{const[r,...t]=e.body.trimEnd().split(`
2
- `).map(i=>JSON.parse(i)),o=t;return e.tail===void 0?{header:r,entries:o,path:e.path??""}:{header:r,entries:o.slice(0,e.tail-1),tail:o.slice(e.tail-1),path:e.path??""}},"parseRead"),h=s((e,r)=>r?`${a(e)}?head=${r.head}&tail=${r.tail}`:a(e),"readPath");return{create(e,r,t,o){return JSON.parse(n("POST","/conversations",{node_id:e,header:r,entries:t,path:o}).body).conversation_id},append(e,r){n("POST",`${a(e)}/entries`,{entries:r})},replace(e,r,t){n("PUT",a(e),{header:r,entries:t})},read(e,r){return $(n("GET",h(e,r)))},async readAsync(e,r){return $(await p("GET",h(e,r)))},header(e){return JSON.parse(n("GET",`${a(e)}/header`).body)},fork(e,r,t,o){return JSON.parse(n("POST",`${a(e)}/fork`,{node_id:r,leaf_id:t,cwd:o}).body).conversation_id},list(e){const r=e===void 0?"":`?node_id=${encodeURIComponent(e)}`;return JSON.parse(n("GET",`/conversations${r}`).body).conversations},delete(e){n("DELETE",a(e))}}}s(N,"createStoreClient");export{N as createStoreClient};
1
+ var k=Object.defineProperty;var s=(l,w)=>k(l,"name",{value:w,configurable:!0});import{MessageChannel as p,Worker as S,receiveMessageOnPort as b}from"node:worker_threads";import{ConversationNotFoundError as g}from"./index.js";function N(l,w){const u=new S(new URL(import.meta.url.endsWith(".ts")?"./client-worker.ts":"./client-worker.js",import.meta.url),{workerData:{address:l,token:w}});u.unref();let c;u.on("error",e=>{c=e}),u.on("exit",e=>{c=new Error(`conversation store worker exited with ${e}`)});function m(e,r,t){if(!t)throw new Error("conversation store worker returned no reply");if(t.error)throw new Error(t.error);if(t.status===404)throw new g(r.split("/")[2]??r);if(!t.status||t.status>=400)throw new Error(`conversation store ${e} ${r} failed (${t.status}): ${t.body}`);return{body:t.body??"",...t.path?{path:t.path}:{},...t.tail?{tail:Number(t.tail)}:{}}}s(m,"settle");function n(e,r,t){if(c)throw c;const o=new SharedArrayBuffer(4),{port1:i,port2:d}=new p;try{if(u.postMessage({method:e,path:r,data:t,signal:o,reply:d},[d]),Atomics.wait(new Int32Array(o),0,0,3e4)==="timed-out")throw i.postMessage("cancel"),new Error(`conversation store request timed out: ${e} ${r}`);return m(e,r,b(i)?.message)}finally{i.close()}}s(n,"call");function E(e,r){if(c)return Promise.reject(c);const{port1:t,port2:o}=new p;return new Promise((i,d)=>{const f=setTimeout(()=>{t.postMessage("cancel"),t.close(),d(new Error(`conversation store request timed out: ${e} ${r}`))},3e4);f.unref(),t.once("message",v=>{clearTimeout(f),t.close();try{i(m(e,r,v))}catch(T){d(T)}}),u.postMessage({method:e,path:r,reply:o},[o])})}s(E,"callAsync");const a=s(e=>`/conversations/${encodeURIComponent(e)}`,"url"),$=s(e=>{const[r,...t]=e.body.trimEnd().split(`
2
+ `).map(i=>JSON.parse(i)),o=t;return e.tail===void 0?{header:r,entries:o,path:e.path??""}:{header:r,entries:o.slice(0,e.tail-1),tail:o.slice(e.tail-1),path:e.path??""}},"parseRead"),h=s((e,r)=>r?`${a(e)}?head=${r.head}&tail=${r.tail}`:a(e),"readPath");return{create(e,r,t,o){return JSON.parse(n("POST","/conversations",{node_id:e,header:r,entries:t,path:o}).body).conversation_id},append(e,r,t){n("POST",`${a(e)}/entries`,{entries:r,...t?.dedupe?{dedupe:!0}:{}})},replace(e,r,t){n("PUT",a(e),{header:r,entries:t})},read(e,r){return $(n("GET",h(e,r)))},async readAsync(e,r){return $(await E("GET",h(e,r)))},header(e){return JSON.parse(n("GET",`${a(e)}/header`).body)},fork(e,r,t,o){return JSON.parse(n("POST",`${a(e)}/fork`,{node_id:r,leaf_id:t,cwd:o}).body).conversation_id},list(e){const r=e===void 0?"":`?node_id=${encodeURIComponent(e)}`;return JSON.parse(n("GET",`/conversations${r}`).body).conversations},delete(e){n("DELETE",a(e))}}}s(N,"createStoreClient");export{N as createStoreClient};
@@ -1,9 +1,9 @@
1
- var G=Object.defineProperty;var u=(a,s)=>G(a,"name",{value:s,configurable:!0});import{randomUUID as J}from"node:crypto";import{closeSync as v,createReadStream as K,existsSync as $,fsyncSync as k,mkdirSync as L,openSync as z,readFileSync as P,readSync as M,renameSync as Q,statSync as S,unlinkSync as O,writeFileSync as j}from"node:fs";import{createInterface as T}from"node:readline";import{dirname as V,join as F,relative as X,resolve as b}from"node:path";import{SessionManager as Y}from"@earendil-works/pi-coding-agent";import{ConversationNotFoundError as Z}from"./index.js";function x(a){const s=[];for(const p of a.split(`
2
- `))if(p.trim())try{s.push(JSON.parse(p))}catch{}return s}u(x,"parseLines");function I(a){const s=z(a,"r");try{const p=Buffer.allocUnsafe(4096);let c="";for(;;){const w=M(s,p,0,p.length,null);c+=p.toString("utf8",0,w);let m=c.indexOf(`
3
- `);for(;m!==-1||w===0;){const y=m===-1?c:c.slice(0,m),[h]=x(y);if(h!==void 0)return h;if(m===-1)return;c=c.slice(m+1),m=c.indexOf(`
4
- `)}}}finally{v(s)}}u(I,"firstLine");function ee(a,s){const p=z(a,"r");try{const c=Buffer.alloc(1);return M(p,c,0,1,s-1),c[0]}finally{v(p)}}u(ee,"lastByte");function C(a,s){return[a,...s].map(p=>`${JSON.stringify(p)}
5
- `).join("")}u(C,"lines");function W(a,s){const p=`--${b(s).replace(/^[/\\]/,"").replace(/[/\\:]/g,"-")}--`;return F(a,p)}u(W,"sessionDirectory");function A(a){return`${a.timestamp.replace(/[:.]/g,"-")}_${a.id}.jsonl`}u(A,"fileName");function ae(a,s){const{appendSection:p}=s,c=new Map,w=new Map;L(a,{recursive:!0,mode:448});const m=new Set;function y(e){if(m.has(e))return;const t=h(e);if(s.repair?.(e,t),!$(t)){c.delete(e),w.delete(e);return}m.add(e)}u(y,"repair");function h(e){const t=c.get(e)??s.pathFor?.(e);if(!t)throw new Z(e);return c.set(e,t),t}u(h,"pathFor");function E(e){y(e);const t=h(e),r=x(P(t,"utf8")),n=r.shift();if(n?.type!=="session"||n.id!==e)throw new Error(`invalid conversation: ${e}`);return{header:n,entries:r,path:t}}u(E,"read");function B(e,t){const r=I(t);return r?.type==="session"&&r.id===e?r:void 0}u(B,"validHeader");function N(e){const t=h(e),r=B(e,t);if(!r)throw new Error(`invalid conversation: ${e}`);return{header:r,path:t}}u(N,"headerOf");function H(e,t){y(e);const{header:r,path:n}=N(e),i=S(n).size;if(i<=t.head+t.tail)return E(e);const f=z(n,"r");try{const l=u((R,_)=>{if(_<=0)return[];const U=Buffer.allocUnsafe(_),q=M(f,U,0,_,R);return U.subarray(0,q).toString("utf8").split(`
6
- `)},"slice"),o=l(0,t.head),d=l(i-t.tail,t.tail);o.pop(),d.shift();const g=x(o.join(`
7
- `));return g[0]?.type==="session"&&g.shift(),{header:r,entries:g,tail:x(d.join(`
8
- `)),path:n}}finally{v(f)}}u(H,"readWindow");function D(e,t,r,n,i){if(c.has(t.id))throw new Error(`conversation already exists: ${t.id}`);const f=X(a,n);if(f.startsWith("..")||f===""||b(n)===b(a))throw new Error("conversation path outside store root");if(L(V(n),{recursive:!0,mode:448}),$(n))throw new Error(`conversation file already exists: ${n}`);let l=!1;try{const o=u(()=>{const d=z(n,"wx",384);l=!0;try{return j(d,C(t,r)),k(d),l=!1,S(n).size}finally{v(d)}},"write");return e===void 0?o():p(t.id,{kind:"create",nodeId:e,path:n,entries:r,...i?{sourceId:i}:{}},o),c.set(t.id,n),t.id}catch(o){throw l||!$(n)?($(n)&&O(n),s.abortCreate?.(t.id)):c.set(t.id,n),o}}return u(D,"createAt"),{recover(e){for(const t of e){if(!t.transcript_path)continue;const r=c.get(t.session_id);if(r&&r!==t.transcript_path)throw new Error(`conflicting conversation paths: ${t.session_id}`);c.set(t.session_id,t.transcript_path)}},repair:y,readLines(e){y(e);const t=h(e),r=T({input:K(t),crlfDelay:1/0});return{path:t,lines:r}},create(e,t,r,n){return D(e,t,r,s.createPath?.(e,t)??n??F(W(a,t.cwd),A(t)))},append(e,t){if(t.length===0)return;y(e);const r=h(e),n=S(r).size;if(n>0&&ee(r,n)!==10)throw new Error(`incomplete conversation tail: ${e}`);try{p(e,{kind:"append",entries:t},()=>{const i=z(r,"a");try{return j(i,t.map(f=>`${JSON.stringify(f)}
9
- `).join("")),k(i),S(r).size}finally{v(i)}})}catch(i){throw m.delete(e),i}},replace(e,t,r){y(e);const n=h(e);if(t.id!==e)throw new Error("replacement header id differs from conversation id");const i=P(n),f=u(o=>{const d=`${n}.${J()}.tmp`;try{j(d,o,{flag:"wx",mode:384});const g=z(d,"r");try{k(g)}finally{v(g)}Q(d,n)}finally{$(d)&&O(d)}},"swap");let l=!1;try{p(e,{kind:"replace"},()=>(f(C(t,r)),l=!0,S(n).size))}catch(o){throw l&&f(i),o}finally{w.delete(e)}},read(e,t){return t?H(e,t):E(e)},header(e){y(e);const{header:t,path:r}=N(e),n=S(r);return{header:t,path:r,mtime:n.mtimeMs,size:n.size}},fork(e,t,r,n){const i=E(e);let f=i.entries;if(r!==void 0){const d=Y.inMemory(i.header.cwd,void 0,[i.header,...f]);d.createBranchedSession(r),f=d.getEntries()}const l=new Date().toISOString(),o={...i.header,id:J(),timestamp:l,cwd:n??i.header.cwd,parentSession:i.path};return D(t,o,f,t&&s.createPath?s.createPath(t,o):F(W(a,o.cwd),A(o)),e)},list(e){const t=[],r=s.owners();for(const n of new Set([...c.keys(),...r.keys()])){const i=r.get(n);if(e!==void 0&&i!==e||(y(n),!c.has(n)))continue;const f=h(n),l=S(f);let o=w.get(n);(!o||o.mtime!==l.mtimeMs||o.size!==l.size)&&(o={mtime:l.mtimeMs,size:l.size,header:B(n,f)},w.set(n,o)),o.header&&t.push({conversation_id:n,...i?{node_id:i}:{},header:o.header,mtime:l.mtimeMs,size:l.size,path:f})}return t.sort((n,i)=>i.mtime-n.mtime)},delete(e){O(h(e)),s.delete?.(e),c.delete(e),m.delete(e),w.delete(e)}}}u(ae,"createFileStore");export{ae as createFileStore,W as sessionDirectory};
1
+ var G=Object.defineProperty;var u=(l,c)=>G(l,"name",{value:c,configurable:!0});import{randomUUID as J}from"node:crypto";import{closeSync as v,createReadStream as K,existsSync as $,fsyncSync as k,mkdirSync as L,openSync as z,readFileSync as P,readSync as M,renameSync as Q,statSync as S,unlinkSync as O,writeFileSync as j}from"node:fs";import{createInterface as T}from"node:readline";import{dirname as V,join as F,relative as X,resolve as b}from"node:path";import{SessionManager as Y}from"@earendil-works/pi-coding-agent";import{ConversationNotFoundError as Z}from"./index.js";function E(l){const c=[];for(const p of l.split(`
2
+ `))if(p.trim())try{c.push(JSON.parse(p))}catch{}return c}u(E,"parseLines");function I(l){const c=z(l,"r");try{const p=Buffer.allocUnsafe(4096);let f="";for(;;){const w=M(c,p,0,p.length,null);f+=p.toString("utf8",0,w);let m=f.indexOf(`
3
+ `);for(;m!==-1||w===0;){const y=m===-1?f:f.slice(0,m),[h]=E(y);if(h!==void 0)return h;if(m===-1)return;f=f.slice(m+1),m=f.indexOf(`
4
+ `)}}}finally{v(c)}}u(I,"firstLine");function ee(l,c){const p=z(l,"r");try{const f=Buffer.alloc(1);return M(p,f,0,1,c-1),f[0]}finally{v(p)}}u(ee,"lastByte");function C(l,c){return[l,...c].map(p=>`${JSON.stringify(p)}
5
+ `).join("")}u(C,"lines");function W(l,c){const p=`--${b(c).replace(/^[/\\]/,"").replace(/[/\\:]/g,"-")}--`;return F(l,p)}u(W,"sessionDirectory");function A(l){return`${l.timestamp.replace(/[:.]/g,"-")}_${l.id}.jsonl`}u(A,"fileName");function ae(l,c){const{appendSection:p}=c,f=new Map,w=new Map;L(l,{recursive:!0,mode:448});const m=new Set;function y(e){if(m.has(e))return;const t=h(e);if(c.repair?.(e,t),!$(t)){f.delete(e),w.delete(e);return}m.add(e)}u(y,"repair");function h(e){const t=f.get(e)??c.pathFor?.(e);if(!t)throw new Z(e);return f.set(e,t),t}u(h,"pathFor");function x(e){y(e);const t=h(e),r=E(P(t,"utf8")),n=r.shift();if(n?.type!=="session"||n.id!==e)throw new Error(`invalid conversation: ${e}`);return{header:n,entries:r,path:t}}u(x,"read");function B(e,t){const r=I(t);return r?.type==="session"&&r.id===e?r:void 0}u(B,"validHeader");function N(e){const t=h(e),r=B(e,t);if(!r)throw new Error(`invalid conversation: ${e}`);return{header:r,path:t}}u(N,"headerOf");function H(e,t){y(e);const{header:r,path:n}=N(e),a=S(n).size;if(a<=t.head+t.tail)return x(e);const i=z(n,"r");try{const o=u((R,_)=>{if(_<=0)return[];const U=Buffer.allocUnsafe(_),q=M(i,U,0,_,R);return U.subarray(0,q).toString("utf8").split(`
6
+ `)},"slice"),s=o(0,t.head),d=o(a-t.tail,t.tail);s.pop(),d.shift();const g=E(s.join(`
7
+ `));return g[0]?.type==="session"&&g.shift(),{header:r,entries:g,tail:E(d.join(`
8
+ `)),path:n}}finally{v(i)}}u(H,"readWindow");function D(e,t,r,n,a){if(f.has(t.id))throw new Error(`conversation already exists: ${t.id}`);const i=X(l,n);if(i.startsWith("..")||i===""||b(n)===b(l))throw new Error("conversation path outside store root");if(L(V(n),{recursive:!0,mode:448}),$(n))throw new Error(`conversation file already exists: ${n}`);let o=!1;try{const s=u(()=>{const d=z(n,"wx",384);o=!0;try{return j(d,C(t,r)),k(d),o=!1,S(n).size}finally{v(d)}},"write");return e===void 0?s():p(t.id,{kind:"create",nodeId:e,path:n,entries:r,...a?{sourceId:a}:{}},s),f.set(t.id,n),t.id}catch(s){throw o||!$(n)?($(n)&&O(n),c.abortCreate?.(t.id)):f.set(t.id,n),s}}return u(D,"createAt"),{recover(e){for(const t of e){if(!t.transcript_path)continue;const r=f.get(t.session_id);if(r&&r!==t.transcript_path)throw new Error(`conflicting conversation paths: ${t.session_id}`);f.set(t.session_id,t.transcript_path)}},repair:y,readLines(e){y(e);const t=h(e),r=T({input:K(t),crlfDelay:1/0});return{path:t,lines:r}},create(e,t,r,n){return D(e,t,r,c.createPath?.(e,t)??n??F(W(l,t.cwd),A(t)))},append(e,t,r){if(t.length===0)return;y(e);const n=h(e);if(r?.dedupe){const i=new Set(x(e).entries.map(o=>o.id));if(t=t.filter(o=>!i.has(o.id)),t.length===0)return}const a=S(n).size;if(a>0&&ee(n,a)!==10)throw new Error(`incomplete conversation tail: ${e}`);try{p(e,{kind:"append",entries:t},()=>{const i=z(n,"a");try{return j(i,t.map(o=>`${JSON.stringify(o)}
9
+ `).join("")),k(i),S(n).size}finally{v(i)}})}catch(i){throw m.delete(e),i}},replace(e,t,r){y(e);const n=h(e);if(t.id!==e)throw new Error("replacement header id differs from conversation id");const a=P(n),i=u(s=>{const d=`${n}.${J()}.tmp`;try{j(d,s,{flag:"wx",mode:384});const g=z(d,"r");try{k(g)}finally{v(g)}Q(d,n)}finally{$(d)&&O(d)}},"swap");let o=!1;try{p(e,{kind:"replace"},()=>(i(C(t,r)),o=!0,S(n).size))}catch(s){throw o&&i(a),s}finally{w.delete(e)}},read(e,t){return t?H(e,t):x(e)},header(e){y(e);const{header:t,path:r}=N(e),n=S(r);return{header:t,path:r,mtime:n.mtimeMs,size:n.size}},fork(e,t,r,n){const a=x(e);let i=a.entries;if(r!==void 0){const d=Y.inMemory(a.header.cwd,void 0,[a.header,...i]);d.createBranchedSession(r),i=d.getEntries()}const o=new Date().toISOString(),s={...a.header,id:J(),timestamp:o,cwd:n??a.header.cwd,parentSession:a.path};return D(t,s,i,t&&c.createPath?c.createPath(t,s):F(W(l,s.cwd),A(s)),e)},list(e){const t=[],r=c.owners();for(const n of new Set([...f.keys(),...r.keys()])){const a=r.get(n);if(e!==void 0&&a!==e||(y(n),!f.has(n)))continue;const i=h(n),o=S(i);let s=w.get(n);(!s||s.mtime!==o.mtimeMs||s.size!==o.size)&&(s={mtime:o.mtimeMs,size:o.size,header:B(n,i)},w.set(n,s)),s.header&&t.push({conversation_id:n,...a?{node_id:a}:{},header:s.header,mtime:o.mtimeMs,size:o.size,path:i})}return t.sort((n,a)=>a.mtime-n.mtime)},delete(e){O(h(e)),c.delete?.(e),f.delete(e),m.delete(e),w.delete(e)}}}u(ae,"createFileStore");export{ae as createFileStore,W as sessionDirectory};
@@ -20,7 +20,12 @@ export interface ConversationWindow {
20
20
  }
21
21
  export interface ConversationStore {
22
22
  create(nodeId: string, header: SessionHeader, entries: SessionEntry[], path?: string): string;
23
- append(id: string, entries: SessionEntry[]): void;
23
+ /** `dedupe` makes the append idempotent by entry id: an entry already in the file is skipped.
24
+ * A caller sets it when an earlier attempt failed ambiguously (the connection ended before a
25
+ * response), because the store may have written the entries anyway. */
26
+ append(id: string, entries: SessionEntry[], options?: {
27
+ dedupe?: boolean;
28
+ }): void;
24
29
  replace(id: string, header: SessionHeader, entries: SessionEntry[]): void;
25
30
  /** Given a window and a file larger than head + tail bytes, `entries` holds the entries wholly
26
31
  * inside the first `head` bytes and `tail` those wholly inside the last `tail` bytes.
@@ -58,3 +63,4 @@ export declare function conversationStore(): ConversationStore;
58
63
  export declare function readConversationAsync(id: string, window?: ConversationWindow): Promise<ReturnType<ConversationStore['read']>>;
59
64
  export { createFileStore, sessionDirectory, type AppendSection, type ConversationCanvas, type ConversationWrite } from './file-store.js';
60
65
  export { installSessionManagerStorePatch } from './patch.js';
66
+ export { findOrphanedParents, relinkOrphanedParents, repairConversationFile, type FileRepair, type OrphanRelink } from './repair.js';
@@ -1 +1 @@
1
- var c=Object.defineProperty;var r=(o,t)=>c(o,"name",{value:t,configurable:!0});import{appsRoot as a,socketDir as i}from"../layout.js";import{join as u}from"node:path";import{createStoreClient as f}from"./client.js";import{currentNodeToken as p}from"../runtime/broker/node-token.js";class d extends Error{static{r(this,"ConversationNotFoundError")}code="not_found";constructor(t){super(`conversation not found: ${t}`)}}function y(o){return o instanceof d}r(y,"isConversationNotFound");function A(o){return/(?:^|\/)[^/]+_([0-9a-f-]{36})\.jsonl$/i.exec(o)?.[1]??null}r(A,"conversationIdFromPath");function h(){return a()}r(h,"defaultConversationRoot");function l(){return process.env.CRTR_STORE_ADDRESS||u(i(),"store.sock")}r(l,"storeAddress");let n,s;function D(o){n=o}r(D,"installConversationStore");function m(){if(n)return n;const o=l();return s?.address!==o&&(s={address:o,store:f(o,p())}),s.store}r(m,"conversationStore");async function E(o,t){const e=m();return e.readAsync?e.readAsync(o,t):e.read(o,t)}r(E,"readConversationAsync");import{createFileStore as k,sessionDirectory as N}from"./file-store.js";import{installSessionManagerStorePatch as j}from"./patch.js";export{d as ConversationNotFoundError,A as conversationIdFromPath,m as conversationStore,k as createFileStore,h as defaultConversationRoot,D as installConversationStore,j as installSessionManagerStorePatch,y as isConversationNotFound,E as readConversationAsync,N as sessionDirectory,l as storeAddress};
1
+ var a=Object.defineProperty;var r=(o,e)=>a(o,"name",{value:e,configurable:!0});import{appsRoot as i,socketDir as c}from"../layout.js";import{join as u}from"node:path";import{createStoreClient as f}from"./client.js";import{currentNodeToken as p}from"../runtime/broker/node-token.js";class d extends Error{static{r(this,"ConversationNotFoundError")}code="not_found";constructor(e){super(`conversation not found: ${e}`)}}function R(o){return o instanceof d}r(R,"isConversationNotFound");function y(o){return/(?:^|\/)[^/]+_([0-9a-f-]{36})\.jsonl$/i.exec(o)?.[1]??null}r(y,"conversationIdFromPath");function A(){return i()}r(A,"defaultConversationRoot");function l(){return process.env.CRTR_STORE_ADDRESS||u(c(),"store.sock")}r(l,"storeAddress");let n,s;function F(o){n=o}r(F,"installConversationStore");function m(){if(n)return n;const o=l();return s?.address!==o&&(s={address:o,store:f(o,p())}),s.store}r(m,"conversationStore");async function k(o,e){const t=m();return t.readAsync?t.readAsync(o,e):t.read(o,e)}r(k,"readConversationAsync");import{createFileStore as P,sessionDirectory as _}from"./file-store.js";import{installSessionManagerStorePatch as O}from"./patch.js";import{findOrphanedParents as j,relinkOrphanedParents as $,repairConversationFile as g}from"./repair.js";export{d as ConversationNotFoundError,y as conversationIdFromPath,m as conversationStore,P as createFileStore,A as defaultConversationRoot,j as findOrphanedParents,F as installConversationStore,O as installSessionManagerStorePatch,R as isConversationNotFound,k as readConversationAsync,$ as relinkOrphanedParents,g as repairConversationFile,_ as sessionDirectory,l as storeAddress};
@@ -1,8 +1,8 @@
1
1
  var O=Object.defineProperty;var v=(r,t)=>O(r,"name",{value:t,configurable:!0});import{createServer as _}from"node:http";import{chmodSync as k,existsSync as N,unlinkSync as T}from"node:fs";import{isConversationNotFound as b}from"./index.js";import{resolveNodeToken as j}from"../canvas/node-tokens.js";import{conversationOwner as H}from"../canvas/sessions.js";import{peerUid as J}from"../../native/linux.js";async function x(r){const t=[];for await(const s of r)t.push(Buffer.from(s));return JSON.parse(Buffer.concat(t).toString("utf8"))}v(x,"body");function l(r,t,s){r.writeHead(t,{"content-type":"application/json"}),r.end(JSON.stringify(s))}v(l,"json");async function P(r,t,s){if(!t.readLines){const f=t.read(s);r.writeHead(200,{"content-type":"application/x-ndjson","x-conversation-path":f.path}),r.end([f.header,...f.entries].map(d=>JSON.stringify(d)).join(`
2
2
  `)+`
3
- `);return}const{path:o,lines:e}=t.readLines(s);let a=!1,c=0;for await(const f of e){if(!f.trim())continue;let d;try{d=JSON.parse(f)}catch{continue}if(!a){if(d.type!=="session"||d.id!==s)throw new Error(`invalid conversation: ${s}`);r.writeHead(200,{"content-type":"application/x-ndjson","x-conversation-path":o}),a=!0}const S=JSON.stringify(d)+`
4
- `;c+=Buffer.byteLength(S),c>=256*1024?(c=0,await new Promise((p,u)=>r.write(S,m=>m?u(m):p()))):r.write(S)}if(!a)throw new Error(`invalid conversation: ${s}`);r.end()}v(P,"fullRead");async function C(r,t){const s=_(async(o,e)=>{try{const a=o.headers.authorization,c=typeof a=="string"&&a.startsWith("Bearer ")?j(a.slice(7)):null,f=new URL(o.url??"/","http://store"),d=f.pathname.split("/").filter(Boolean);if(a===void 0&&process.platform!=="linux"&&o.method==="GET"&&d[0]==="conversations"&&(d.length===1||d.length===2||d.length===3&&d[2]==="header")){const n=d[1];if(!n)l(e,200,{conversations:t.list(f.searchParams.get("node_id")??void 0)});else if(d[2]==="header")l(e,200,t.header(n));else{const y=f.searchParams.get("head"),h=f.searchParams.get("tail");if(y===null||h===null)await P(e,t,n);else{const g=t.read(n,{head:Number(y),tail:Number(h)});e.writeHead(200,{"content-type":"application/x-ndjson","x-conversation-path":g.path,...g.tail?{"x-conversation-tail":String(1+g.entries.length)}:{}}),e.end([g.header,...g.entries,...g.tail??[]].map(E=>JSON.stringify(E)).join(`
3
+ `);return}const{path:i,lines:e}=t.readLines(s);let a=!1,c=0;for await(const f of e){if(!f.trim())continue;let d;try{d=JSON.parse(f)}catch{continue}if(!a){if(d.type!=="session"||d.id!==s)throw new Error(`invalid conversation: ${s}`);r.writeHead(200,{"content-type":"application/x-ndjson","x-conversation-path":i}),a=!0}const S=JSON.stringify(d)+`
4
+ `;c+=Buffer.byteLength(S),c>=256*1024?(c=0,await new Promise((p,u)=>r.write(S,m=>m?u(m):p()))):r.write(S)}if(!a)throw new Error(`invalid conversation: ${s}`);r.end()}v(P,"fullRead");async function C(r,t){const s=_(async(i,e)=>{try{const a=i.headers.authorization,c=typeof a=="string"&&a.startsWith("Bearer ")?j(a.slice(7)):null,f=new URL(i.url??"/","http://store"),d=f.pathname.split("/").filter(Boolean);if(a===void 0&&process.platform!=="linux"&&i.method==="GET"&&d[0]==="conversations"&&(d.length===1||d.length===2||d.length===3&&d[2]==="header")){const n=d[1];if(!n)l(e,200,{conversations:t.list(f.searchParams.get("node_id")??void 0)});else if(d[2]==="header")l(e,200,t.header(n));else{const y=f.searchParams.get("head"),h=f.searchParams.get("tail");if(y===null||h===null)await P(e,t,n);else{const g=t.read(n,{head:Number(y),tail:Number(h)});e.writeHead(200,{"content-type":"application/x-ndjson","x-conversation-path":g.path,...g.tail?{"x-conversation-tail":String(1+g.entries.length)}:{}}),e.end([g.header,...g.entries,...g.tail??[]].map(E=>JSON.stringify(E)).join(`
5
5
  `)+`
6
- `)}}return}if(!c){l(e,401,{error:"unauthorized"});return}if(process.platform==="linux"){let n;try{n=J(o.socket)}catch{l(e,401,{error:"unauthorized"});return}if(n!==c.uid){l(e,401,{error:"unauthorized"});return}}const p=v(n=>H(n)===c.node,"owns"),u=v(()=>l(e,404,{error:"not_found"}),"notFound"),m=new URL(o.url??"/","http://store"),w=m.pathname.split("/").filter(Boolean);if(w[0]!=="conversations"){l(e,404,{error:"not_found"});return}const i=w[1];if(!i&&o.method==="POST"){const n=await x(o);if(n.node_id!==c.node){u();return}l(e,201,{conversation_id:t.create(n.node_id,n.header,n.entries,n.path)})}else if(!i&&o.method==="GET"){if(m.searchParams.has("node_id")&&m.searchParams.get("node_id")!==c.node){u();return}l(e,200,{conversations:t.list(c.node)})}else if(i&&w[2]==="entries"&&o.method==="POST"){if(!p(i)){u();return}const n=await x(o);t.append(i,n.entries),e.writeHead(204).end()}else if(i&&w[2]==="header"&&o.method==="GET"){if(!p(i)){u();return}l(e,200,t.header(i))}else if(i&&w[2]==="fork"&&o.method==="POST"){if(!p(i)){u();return}const n=await x(o);if(n.node_id!==c.node){u();return}l(e,201,{conversation_id:t.fork(i,c.node,n.leaf_id,n.cwd)})}else if(i&&w.length===2&&o.method==="GET"){if(!p(i)){u();return}const n=m.searchParams.get("head"),y=m.searchParams.get("tail");if(n===null||y===null)await P(e,t,i);else{const h=t.read(i,{head:Number(n),tail:Number(y)});e.writeHead(200,{"content-type":"application/x-ndjson","x-conversation-path":h.path,...h.tail?{"x-conversation-tail":String(1+h.entries.length)}:{}}),e.end([h.header,...h.entries,...h.tail??[]].map(g=>JSON.stringify(g)).join(`
6
+ `)}}return}if(!c){l(e,401,{error:"unauthorized"});return}if(process.platform==="linux"){let n;try{n=J(i.socket)}catch{l(e,401,{error:"unauthorized"});return}if(n!==c.uid){l(e,401,{error:"unauthorized"});return}}const p=v(n=>H(n)===c.node,"owns"),u=v(()=>l(e,404,{error:"not_found"}),"notFound"),m=new URL(i.url??"/","http://store"),w=m.pathname.split("/").filter(Boolean);if(w[0]!=="conversations"){l(e,404,{error:"not_found"});return}const o=w[1];if(!o&&i.method==="POST"){const n=await x(i);if(n.node_id!==c.node){u();return}l(e,201,{conversation_id:t.create(n.node_id,n.header,n.entries,n.path)})}else if(!o&&i.method==="GET"){if(m.searchParams.has("node_id")&&m.searchParams.get("node_id")!==c.node){u();return}l(e,200,{conversations:t.list(c.node)})}else if(o&&w[2]==="entries"&&i.method==="POST"){if(!p(o)){u();return}const n=await x(i);t.append(o,n.entries,n.dedupe===!0?{dedupe:!0}:void 0),e.writeHead(204).end()}else if(o&&w[2]==="header"&&i.method==="GET"){if(!p(o)){u();return}l(e,200,t.header(o))}else if(o&&w[2]==="fork"&&i.method==="POST"){if(!p(o)){u();return}const n=await x(i);if(n.node_id!==c.node){u();return}l(e,201,{conversation_id:t.fork(o,c.node,n.leaf_id,n.cwd)})}else if(o&&w.length===2&&i.method==="GET"){if(!p(o)){u();return}const n=m.searchParams.get("head"),y=m.searchParams.get("tail");if(n===null||y===null)await P(e,t,o);else{const h=t.read(o,{head:Number(n),tail:Number(y)});e.writeHead(200,{"content-type":"application/x-ndjson","x-conversation-path":h.path,...h.tail?{"x-conversation-tail":String(1+h.entries.length)}:{}}),e.end([h.header,...h.entries,...h.tail??[]].map(g=>JSON.stringify(g)).join(`
7
7
  `)+`
8
- `)}}else if(i&&w.length===2&&o.method==="PUT"){if(!p(i)){u();return}const n=await x(o);t.replace(i,n.header,n.entries),e.writeHead(204).end()}else if(i&&w.length===2&&o.method==="DELETE"){if(!p(i)){u();return}t.delete(i),e.writeHead(204).end()}else l(e,404,{error:"not_found"})}catch(a){const c=a instanceof Error?a.message:String(a);e.headersSent?e.destroy(a instanceof Error?a:new Error(c)):l(e,b(a)?404:500,{error:c})}});if(N(r)&&T(r),await new Promise((o,e)=>{s.once("error",e),s.listen(r,()=>{s.off("error",e),o()})}),typeof s.address()!="string")throw s.close(),new Error(`store listener must bind a unix socket: ${JSON.stringify(s.address())}`);return k(r,process.platform==="linux"&&N("/run/crtr/launcher.sock")?438:384),{close:v(()=>new Promise((o,e)=>s.close(a=>a?e(a):o())),"close")}}v(C,"listenConversationStore");export{C as listenConversationStore};
8
+ `)}}else if(o&&w.length===2&&i.method==="PUT"){if(!p(o)){u();return}const n=await x(i);t.replace(o,n.header,n.entries),e.writeHead(204).end()}else if(o&&w.length===2&&i.method==="DELETE"){if(!p(o)){u();return}t.delete(o),e.writeHead(204).end()}else l(e,404,{error:"not_found"})}catch(a){const c=a instanceof Error?a.message:String(a);e.headersSent?e.destroy(a instanceof Error?a:new Error(c)):l(e,b(a)?404:500,{error:c})}});if(N(r)&&T(r),await new Promise((i,e)=>{s.once("error",e),s.listen(r,()=>{s.off("error",e),i()})}),typeof s.address()!="string")throw s.close(),new Error(`store listener must bind a unix socket: ${JSON.stringify(s.address())}`);return k(r,process.platform==="linux"&&N("/run/crtr/launcher.sock")?438:384),{close:v(()=>new Promise((i,e)=>s.close(a=>a?e(a):i())),"close")}}v(C,"listenConversationStore");export{C as listenConversationStore};
@@ -1 +1 @@
1
- var b=Object.defineProperty;var l=(u,a)=>b(u,"name",{value:a,configurable:!0});import{dirname as E,resolve as v}from"node:path";import{SessionManager as m}from"@earendil-works/pi-coding-agent";import{conversationIdFromPath as x,conversationStore as B}from"./index.js";let N=!1;function j(u){if(N)return;const a=m.prototype,h=m;for(const e of["_setSessionFile","_rewriteFile","_persist","createBranchedSession","newSession"])if(typeof a[e]!="function")throw new Error(`pi SessionManager.${e} changed; store patch cannot start`);for(const e of["create","open","continueRecent","forkFrom","findById","list","listAll"])if(typeof h[e]!="function")throw new Error(`pi SessionManager.${e} changed; store patch cannot start`);const D=a.createBranchedSession,w=a.newSession,r=B();function y(e,s){const{header:t,entries:i,path:o}=r.read(e),n=m.inMemory(s??t.cwd);n.sessionFile=o,n.sessionDir=E(o),n._loadEntries([t,...i]),n.persist=!0,n.flushed=!0;const d=n.fileEntries[0];return d?.type==="session"&&d.version!==t.version&&n._rewriteFile(),n}l(y,"fromStore");function p(e){const[s,...t]=e.fileEntries;if(!s||s.type!=="session")throw new Error("conversation has no session header");r.create(u,s,t),e.sessionFile=r.header(s.id).path,e.sessionDir=E(e.sessionFile),e.flushed=!0}l(p,"persistNew");function g(e){const s=x(e);if(!s)throw new Error(`invalid conversation path: ${e}`);return s}l(g,"idOf"),a._setSessionFile=function(e){const{header:s,entries:t}=r.read(g(e));this.sessionFile=r.header(s.id).path,this.sessionDir=E(this.sessionFile),this._loadEntries([s,...t]),this.flushed=!0},a._rewriteFile=function(){if(!this.persist||!this.sessionFile)return;const[e,...s]=this.fileEntries;if(!e||e.type!=="session")throw new Error("conversation has no session header");r.replace(e.id,e,s)},a._persist=function(e){!this.persist||!this.sessionFile||(this.flushed?r.append(this.sessionId,[e]):p(this))},a.newSession=function(e){if(!this.persist)return w.call(this,e);const s=this.flushed&&this.fileEntries.length===1&&this.sessionFile!==void 0?this.sessionId:void 0;if(s!==void 0&&(e?.id===void 0||e.id===s)){const t=this.sessionFile;w.call(this,{...e,id:s});const i=this.fileEntries[0];if(!i||i.type!=="session")throw new Error("conversation has no session header");return r.replace(s,i,[]),this.sessionFile=t,this.flushed=!0,t}return w.call(this,e),p(this),this.sessionFile},a.createBranchedSession=function(e){if(!this.persist)return D.call(this,e);const s=this.sessionFile;this.persist=!1;try{D.call(this,e)}finally{this.persist=!0}const t=this.fileEntries[0];if(!t||t.type!=="session")throw new Error("branch has no header");return this.fileEntries[0]={...t,parentSession:s},p(this),this.sessionFile},h.create=(e,s,t)=>{const i=m.inMemory(e,t);return i.persist=!0,p(i),i},h.open=(e,s,t)=>y(g(e),t),h.continueRecent=e=>{const s=r.list().find(t=>t.header.cwd===v(e));return s?y(s.conversation_id,e):h.create(e)},h.forkFrom=(e,s)=>y(r.fork(g(e),u,void 0,s)),h.findById=(e,s)=>r.list().find(t=>t.conversation_id===s&&t.header.cwd===v(e))?.path;function M(e){return r.list().filter(s=>e===void 0||s.header.cwd===v(e)).map(s=>{let t,i=0,o="",n;const d=[];for(const f of r.read(s.conversation_id).entries){if(f.type==="session_info"&&(t=f.name?.trim()||void 0),f.type!=="message")continue;i++;const c=f.message;if(c.role!=="user"&&c.role!=="assistant")continue;const A=typeof c.timestamp=="number"?c.timestamp:Date.parse(f.timestamp);Number.isNaN(A)||(n=Math.max(n??0,A));const S=typeof c.content=="string"?c.content:Array.isArray(c.content)?c.content.filter(_=>_.type==="text").map(_=>_.text??"").join(" "):"";S&&(d.push(S),!o&&c.role==="user"&&(o=S))}const F=new Date(s.header.timestamp);return{path:s.path,id:s.conversation_id,cwd:s.header.cwd,...t?{name:t}:{},...s.header.parentSession?{parentSessionPath:s.header.parentSession}:{},created:F,modified:n&&n>0?new Date(n):Number.isNaN(F.getTime())?new Date(s.mtime):F,messageCount:i,firstMessage:o||"(no messages)",allMessagesText:d.join(" ")}})}l(M,"sessionInfos"),h.list=async(e,s,t,i)=>{i?.throwIfAborted();const o=M(e);return t?.(o.length,o.length,o),o},h.listAll=async(...e)=>(e.find(t=>t instanceof AbortSignal)?.throwIfAborted(),M()),N=!0}l(j,"installSessionManagerStorePatch");export{j as installSessionManagerStorePatch};
1
+ var P=Object.defineProperty;var h=(a,c)=>P(a,"name",{value:c,configurable:!0});import{dirname as E,resolve as A}from"node:path";import{SessionManager as w}from"@earendil-works/pi-coding-agent";import{conversationIdFromPath as R,conversationStore as O}from"./index.js";import{findOrphanedParents as T}from"./repair.js";import{emitEvent as I}from"../events/emit.js";const f=new WeakMap;function k(a){a>0&&Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,a)}h(k,"sleep");function C(){const a=Number(process.env.CRTR_CONVERSATION_APPEND_RETRY_MS);return Number.isFinite(a)&&a>=0?a:500}h(C,"retryDelayMs");let x=!1;function z(a){if(x)return;const c=w.prototype,d=w;for(const e of["_setSessionFile","_rewriteFile","_persist","createBranchedSession","newSession"])if(typeof c[e]!="function")throw new Error(`pi SessionManager.${e} changed; store patch cannot start`);for(const e of["create","open","continueRecent","forkFrom","findById","list","listAll"])if(typeof d[e]!="function")throw new Error(`pi SessionManager.${e} changed; store patch cannot start`);const M=c.createBranchedSession,y=c.newSession,o=O();function N(e,s){const t=T(s);t.length!==0&&I({level:"error",event:"conversation.orphaned_parent",fields:{conversation_id:e,orphan_count:t.length,orphans:t.slice(0,20).map(i=>({entry_id:i.entryId,missing_parent_id:i.missingParentId}))}})}h(N,"reportOrphans");function _(e,s){const{header:t,entries:i,path:r}=o.read(e);N(t.id,i);const n=w.inMemory(s??t.cwd);n.sessionFile=r,n.sessionDir=E(r),n._loadEntries([t,...i]),n.persist=!0,n.flushed=!0;const u=n.fileEntries[0];return u?.type==="session"&&u.version!==t.version&&n._rewriteFile(),n}h(_,"fromStore");function m(e){const[s,...t]=e.fileEntries;if(!s||s.type!=="session")throw new Error("conversation has no session header");o.create(a,s,t),e.sessionFile=o.header(s.id).path,e.sessionDir=E(e.sessionFile),e.flushed=!0,f.delete(e)}h(m,"persistNew");function B(e,s){const t=f.get(e)??{entries:[],ambiguous:!1};t.entries.push(s),f.set(e,t);let i;for(let r=0;r<2;r++){r>0&&k(C());try{o.append(e.sessionId,t.entries,t.ambiguous?{dedupe:!0}:void 0),f.delete(e);return}catch(n){i=n,t.ambiguous=!0}}throw I({level:"error",event:"conversation.append_failed",error:i,fields:{conversation_id:e.sessionId,pending_entries:t.entries.length}}),i}h(B,"appendInOrder");function g(e){const s=R(e);if(!s)throw new Error(`invalid conversation path: ${e}`);return s}h(g,"idOf"),c._setSessionFile=function(e){const{header:s,entries:t}=o.read(g(e));this.sessionFile=o.header(s.id).path,this.sessionDir=E(this.sessionFile),N(s.id,t),this._loadEntries([s,...t]),this.flushed=!0,f.delete(this)},c._rewriteFile=function(){if(!this.persist||!this.sessionFile)return;const[e,...s]=this.fileEntries;if(!e||e.type!=="session")throw new Error("conversation has no session header");o.replace(e.id,e,s),f.delete(this)},c._persist=function(e){!this.persist||!this.sessionFile||(this.flushed?B(this,e):m(this))},c.newSession=function(e){if(!this.persist)return y.call(this,e);const s=this.flushed&&this.fileEntries.length===1&&this.sessionFile!==void 0?this.sessionId:void 0;if(s!==void 0&&(e?.id===void 0||e.id===s)){const t=this.sessionFile;y.call(this,{...e,id:s});const i=this.fileEntries[0];if(!i||i.type!=="session")throw new Error("conversation has no session header");return o.replace(s,i,[]),this.sessionFile=t,this.flushed=!0,t}return y.call(this,e),m(this),this.sessionFile},c.createBranchedSession=function(e){if(!this.persist)return M.call(this,e);const s=this.sessionFile;this.persist=!1;try{M.call(this,e)}finally{this.persist=!0}const t=this.fileEntries[0];if(!t||t.type!=="session")throw new Error("branch has no header");return this.fileEntries[0]={...t,parentSession:s},m(this),this.sessionFile},d.create=(e,s,t)=>{const i=w.inMemory(e,t);return i.persist=!0,m(i),i},d.open=(e,s,t)=>_(g(e),t),d.continueRecent=e=>{const s=o.list().find(t=>t.header.cwd===A(e));return s?_(s.conversation_id,e):d.create(e)},d.forkFrom=(e,s)=>_(o.fork(g(e),a,void 0,s)),d.findById=(e,s)=>o.list().find(t=>t.conversation_id===s&&t.header.cwd===A(e))?.path;function b(e){return o.list().filter(s=>e===void 0||s.header.cwd===A(e)).map(s=>{let t,i=0,r="",n;const u=[];for(const p of o.read(s.conversation_id).entries){if(p.type==="session_info"&&(t=p.name?.trim()||void 0),p.type!=="message")continue;i++;const l=p.message;if(l.role!=="user"&&l.role!=="assistant")continue;const D=typeof l.timestamp=="number"?l.timestamp:Date.parse(p.timestamp);Number.isNaN(D)||(n=Math.max(n??0,D));const S=typeof l.content=="string"?l.content:Array.isArray(l.content)?l.content.filter(F=>F.type==="text").map(F=>F.text??"").join(" "):"";S&&(u.push(S),!r&&l.role==="user"&&(r=S))}const v=new Date(s.header.timestamp);return{path:s.path,id:s.conversation_id,cwd:s.header.cwd,...t?{name:t}:{},...s.header.parentSession?{parentSessionPath:s.header.parentSession}:{},created:v,modified:n&&n>0?new Date(n):Number.isNaN(v.getTime())?new Date(s.mtime):v,messageCount:i,firstMessage:r||"(no messages)",allMessagesText:u.join(" ")}})}h(b,"sessionInfos"),d.list=async(e,s,t,i)=>{i?.throwIfAborted();const r=b(e);return t?.(r.length,r.length,r),r},d.listAll=async(...e)=>(e.find(t=>t instanceof AbortSignal)?.throwIfAborted(),b()),x=!0}h(z,"installSessionManagerStorePatch");export{z as installSessionManagerStorePatch};
@@ -0,0 +1,45 @@
1
+ /** Pure helpers for conversations whose entries name a `parentId` that is not in the file.
2
+ * That shape comes from an append that failed after pi had already advanced its in-memory leaf:
3
+ * later entries were written chained onto the entry that never reached the file, and every
4
+ * reload then stops walking the parent chain at the gap. */
5
+ import type { SessionEntry, SessionHeader } from '@earendil-works/pi-coding-agent';
6
+ export interface OrphanedParent {
7
+ /** Index into the entries array (header excluded) of the entry whose parent is missing. */
8
+ index: number;
9
+ entryId: string;
10
+ missingParentId: string;
11
+ }
12
+ /** Entries whose non-null `parentId` is absent from `entries`. */
13
+ export declare function findOrphanedParents(entries: readonly SessionEntry[]): OrphanedParent[];
14
+ export interface OrphanRelink extends OrphanedParent {
15
+ /** The entry it was relinked to: the one before it in file order, or null at the head. */
16
+ relinkedTo: string | null;
17
+ /** What the lost entry's absence leaves behind for the provider transcript. */
18
+ note?: string;
19
+ }
20
+ /** Relink each orphan onto the entry before it in file order — the leaf pi held when the lost
21
+ * entry was appended. A lost toolResult leaves a tool call without a result, which pi-ai's
22
+ * message transform answers with a synthetic "No result provided" result; a lost toolCall
23
+ * would leave a toolResult with no call, which the transform does not fix, so that case is
24
+ * reported in `note` for the caller to resolve. Returns new entry objects; the input is untouched. */
25
+ export declare function relinkOrphanedParents(entries: readonly SessionEntry[]): {
26
+ entries: SessionEntry[];
27
+ relinks: OrphanRelink[];
28
+ };
29
+ /** Serialize a conversation file: header first, one JSON line per entry. */
30
+ export declare function conversationLines(header: SessionHeader, entries: readonly SessionEntry[]): string;
31
+ export interface FileRepair {
32
+ path: string;
33
+ entries: number;
34
+ /** Branch length walked from the last entry, before and after the relink. */
35
+ branchBefore: number;
36
+ branchAfter: number;
37
+ relinks: OrphanRelink[];
38
+ backup?: string;
39
+ }
40
+ /** Relink one conversation file's orphaned entries in place, after copying it to `<path>.bak-orphans`.
41
+ * The caller must have stopped the owning node's broker: a running broker holds the conversation in
42
+ * memory and appends to the file. `write: false` reports what would change and touches nothing. */
43
+ export declare function repairConversationFile(path: string, options: {
44
+ write: boolean;
45
+ }): FileRepair;
@@ -0,0 +1,3 @@
1
+ var f=Object.defineProperty;var i=(e,r)=>f(e,"name",{value:r,configurable:!0});import{copyFileSync as m,existsSync as h,readFileSync as g,renameSync as I,writeFileSync as w}from"node:fs";function y(e){const r=new Set(e.map(t=>t.id)),n=[];return e.forEach((t,o)=>{t.parentId&&!r.has(t.parentId)&&n.push({index:o,entryId:t.id,missingParentId:t.parentId})}),n}i(y,"findOrphanedParents");function k(e){if(e?.type!=="message")return[];const r=e.message;return r.role!=="assistant"||!Array.isArray(r.content)?[]:r.content.filter(n=>n.type==="toolCall"&&n.id).map(n=>n.id)}i(k,"toolCallIds");function x(e){const r=new Map(y(e).map(o=>[o.index,o])),n=[];return{entries:e.map((o,l)=>{const p=r.get(l);if(!p)return o;const a=e[l-1],s={...p,relinkedTo:a?.id??null};if(o.type==="message"&&o.message.role==="toolResult"){const c=o.message.toolCallId;(!c||!k(a).includes(c))&&(s.note=`toolResult ${c??"?"} has no tool call on the entry before it`)}return n.push(s),{...o,parentId:a?.id??null}}),relinks:n}}i(x,"relinkOrphanedParents");function S(e,r){return[e,...r].map(n=>`${JSON.stringify(n)}
2
+ `).join("")}i(S,"conversationLines");function u(e){const r=new Map(e.map(t=>[t.id,t]));let n=0;for(let t=e.at(-1);t;t=t.parentId?r.get(t.parentId):void 0)n++;return n}i(u,"branchLength");function C(e,r){const n=[];for(const d of g(e,"utf8").split(`
3
+ `))if(d.trim())try{n.push(JSON.parse(d))}catch{}const t=n.shift();if(t?.type!=="session")throw new Error(`no session header: ${e}`);const o=n,{entries:l,relinks:p}=x(o),a={path:e,entries:o.length,branchBefore:u(o),branchAfter:u(l),relinks:p};if(p.length===0||!r.write)return a;const s=`${e}.bak-orphans`;if(h(s))throw new Error(`backup already exists: ${s}`);m(e,s);const c=`${e}.${process.pid}.repair.tmp`;return w(c,S(t,l),{mode:384}),I(c,e),{...a,backup:s}}i(C,"repairConversationFile");export{S as conversationLines,y as findOrphanedParents,x as relinkOrphanedParents,C as repairConversationFile};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/cli",
3
- "version": "0.3.427",
3
+ "version": "0.3.429",
4
4
  "description": "crtr — agent runtime with memory, plugins, and marketplaces",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -137,7 +137,7 @@
137
137
  },
138
138
  "license": "UNLICENSED",
139
139
  "dependencies": {
140
- "@north-light/identity": "0.3.427",
140
+ "@north-light/identity": "0.3.429",
141
141
  "@earendil-works/pi-agent-core": "1.1.0",
142
142
  "@earendil-works/pi-ai": "1.1.0",
143
143
  "@earendil-works/pi-coding-agent": "1.1.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/identity",
3
- "version": "0.3.427",
3
+ "version": "0.3.429",
4
4
  "description": "crouter cloud access-token verification and scope rules",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/runtime.lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@north-light/cli",
3
- "version": "0.3.427",
3
+ "version": "0.3.429",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@north-light/cli",
9
- "version": "0.3.427",
9
+ "version": "0.3.429",
10
10
  "hasInstallScript": true,
11
11
  "license": "UNLICENSED",
12
12
  "workspaces": [
@@ -18,7 +18,7 @@
18
18
  "@earendil-works/pi-ai": "1.1.0",
19
19
  "@earendil-works/pi-coding-agent": "1.1.0",
20
20
  "@earendil-works/pi-tui": "1.1.0",
21
- "@north-light/identity": "0.3.427",
21
+ "@north-light/identity": "0.3.429",
22
22
  "cron-parser": "^5.6.0",
23
23
  "esbuild": "^0.27.7",
24
24
  "jose": "^6.2.12",
@@ -12095,17 +12095,17 @@
12095
12095
  },
12096
12096
  "packages/crouter-api": {
12097
12097
  "name": "@north-light/api",
12098
- "version": "0.3.427",
12098
+ "version": "0.3.429",
12099
12099
  "license": "UNLICENSED"
12100
12100
  },
12101
12101
  "packages/crouter-env-docker": {
12102
12102
  "name": "@north-light/env-docker",
12103
- "version": "0.3.427",
12103
+ "version": "0.3.429",
12104
12104
  "license": "UNLICENSED"
12105
12105
  },
12106
12106
  "packages/crouter-identity": {
12107
12107
  "name": "@north-light/identity",
12108
- "version": "0.3.427",
12108
+ "version": "0.3.429",
12109
12109
  "license": "UNLICENSED",
12110
12110
  "dependencies": {
12111
12111
  "jose": "^6.2.1"
@@ -12113,20 +12113,20 @@
12113
12113
  },
12114
12114
  "packages/crouter-plugin": {
12115
12115
  "name": "@north-light/plugin",
12116
- "version": "0.3.427",
12116
+ "version": "0.3.429",
12117
12117
  "license": "UNLICENSED",
12118
12118
  "dependencies": {
12119
- "@north-light/api": "0.3.427",
12120
- "@north-light/identity": "0.3.427"
12119
+ "@north-light/api": "0.3.429",
12120
+ "@north-light/identity": "0.3.429"
12121
12121
  }
12122
12122
  },
12123
12123
  "packages/crouter-sdk": {
12124
12124
  "name": "@north-light/sdk",
12125
- "version": "0.3.427",
12125
+ "version": "0.3.429",
12126
12126
  "license": "UNLICENSED",
12127
12127
  "dependencies": {
12128
- "@north-light/api": "0.3.427",
12129
- "@north-light/identity": "0.3.427",
12128
+ "@north-light/api": "0.3.429",
12129
+ "@north-light/identity": "0.3.429",
12130
12130
  "jose": "^6.2.1"
12131
12131
  },
12132
12132
  "bin": {