@north-light/crouter 0.3.324 → 0.3.325
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.
- package/dist/api/client.d.ts +12 -4
- package/dist/api/client.js +2 -2
- package/dist/api/dto/bash.d.ts +17 -0
- package/dist/api/dto/bash.js +0 -0
- package/dist/api/dto/files.d.ts +16 -6
- package/dist/api/dto/node-events.d.ts +65 -0
- package/dist/api/dto/node-events.js +0 -0
- package/dist/api/index.d.ts +3 -0
- package/dist/api/index.js +1 -1
- package/dist/api/routes.d.ts +4 -0
- package/dist/api/routes.js +1 -1
- package/dist/builtin-memory/crouter-sdk.md +11 -7
- package/dist/clients/attach/viewer.js +123 -123
- package/dist/core/runtime/run-command.d.ts +29 -0
- package/dist/core/runtime/run-command.js +1 -0
- package/dist/daemon/api/handlers/bash.d.ts +2 -0
- package/dist/daemon/api/handlers/bash.js +1 -0
- package/dist/daemon/api/handlers/files.js +1 -1
- package/dist/daemon/api/handlers/node-events.d.ts +36 -0
- package/dist/daemon/api/handlers/node-events.js +6 -0
- package/dist/daemon/api/handlers/reports.js +5 -5
- package/dist/daemon/api/server.js +3 -3
- package/dist/daemon/cron/capture.d.ts +3 -1
- package/dist/daemon/cron/capture.js +2 -2
- package/dist/daemon/cron-run.js +1 -1
- package/docs/sdk/README.md +9 -6
- package/docs/sdk/bash.md +34 -0
- package/docs/sdk/client.md +5 -7
- package/docs/sdk/errors.md +23 -18
- package/docs/sdk/files.md +44 -0
- package/docs/sdk/getting-started.md +1 -1
- package/docs/sdk/migration.md +2 -2
- package/docs/sdk/nodes.md +25 -28
- package/docs/sdk/resources.md +49 -41
- package/docs/sdk/streaming.md +54 -38
- package/package.json +1 -1
- package/runtime.lock.json +6 -6
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface RunCommandOnceOptions {
|
|
2
|
+
command: string;
|
|
3
|
+
cwd: string;
|
|
4
|
+
env: NodeJS.ProcessEnv;
|
|
5
|
+
timeoutMs: number;
|
|
6
|
+
headBytes: number;
|
|
7
|
+
}
|
|
8
|
+
export interface RunCommandOnceResult {
|
|
9
|
+
exitCode: number | null;
|
|
10
|
+
signal: NodeJS.Signals | null;
|
|
11
|
+
stdout: string;
|
|
12
|
+
stdoutTruncated: boolean;
|
|
13
|
+
stderr: string;
|
|
14
|
+
stderrTruncated: boolean;
|
|
15
|
+
timedOut: boolean;
|
|
16
|
+
durationMs: number;
|
|
17
|
+
stdoutSha256: string;
|
|
18
|
+
processError: Error | null;
|
|
19
|
+
}
|
|
20
|
+
export interface RunningCommand {
|
|
21
|
+
pid: number | null;
|
|
22
|
+
completion: Promise<RunCommandOnceResult>;
|
|
23
|
+
terminate(): void;
|
|
24
|
+
}
|
|
25
|
+
/** Start one admitted-PATH bash command in its own process group. The returned
|
|
26
|
+
* handle exposes the pid immediately for callers that must record ownership
|
|
27
|
+
* before waiting for completion; `completion` resolves after the process group
|
|
28
|
+
* has stopped, including timeout teardown. */
|
|
29
|
+
export declare function runCommandOnce({ command, cwd, env, timeoutMs, headBytes }: RunCommandOnceOptions): RunningCommand;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var R=Object.defineProperty;var u=(a,s)=>R(a,"name",{value:s,configurable:!0});import{spawn as x}from"node:child_process";import{createHash as G}from"node:crypto";import{captureTeardownSnapshot as v,isAnyPidAlive as C,killProcessTreePids as g}from"../canvas/pid.js";import{CRON_DEATH_POLL_MS as K,CRON_KILL_GRACE_MS as N,newHeadCapture as k}from"../../daemon/cron/capture.js";import{resolveAdmittedHostCommand as b}from"./spawn-env.js";function Q({command:a,cwd:s,env:w,timeoutMs:D,headBytes:S}){const E=Date.now(),r=x(b("bash",w,s),["-c",a],{cwd:s,env:w,detached:!0,stdio:["ignore","pipe","pipe"]}),i=r.pid??null,f=k(S),c=k(S),A=G("sha256");r.stdout?.on("data",t=>{A.update(t),f.take(t)}),r.stderr?.on("data",t=>c.take(t));let L=!1,m=null,o,_=null,d=!1,H=!1,n,l,p,e,O;const I=new Promise(t=>{O=t}),T=u(()=>{d||o===void 0||(d=!0,n!==void 0&&clearTimeout(n),l!==void 0&&clearTimeout(l),p!==void 0&&clearTimeout(p),O({exitCode:o,signal:_,stdout:f.decode(),stdoutTruncated:f.truncated(),stderr:c.decode(),stderrTruncated:c.truncated(),timedOut:L,durationMs:Date.now()-E,stdoutSha256:A.digest("hex"),processError:m}))},"finish"),h=u(()=>{if(e===void 0||!C(e.tree)){T();return}p=setTimeout(h,K)},"awaitTeardownDeath"),P=u(()=>{i===null||d||(e??=v(i,null),g(e.tree,"SIGTERM",e.identities),l===void 0&&(l=setTimeout(()=>{C(e.tree)&&g(e.tree,"SIGKILL",e.identities),H=!0,o!==void 0&&h()},N),l.unref?.()))},"terminate");return r.on("error",t=>{m===null&&(m=t),i===null&&(o=null,T())}),r.on("close",(t,M)=>{if(!d){if(o=t,_=M,n!==void 0&&clearTimeout(n),e!==void 0&&C(e.tree)){H&&h();return}T()}}),i!==null&&(n=setTimeout(()=>{d||(L=!0,P())},D),n.unref?.()),{pid:i,completion:I,terminate:P}}u(Q,"runCommandOnce");export{Q as runCommandOnce};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var m=Object.defineProperty;var d=(r,t)=>m(r,"name",{value:t,configurable:!0});import{statSync as u}from"node:fs";import{isAbsolute as f}from"node:path";import{ApiError as a}from"../../../api/index.js";import{buildOperationalEnvBase as l}from"../../../core/runtime/spawn-env.js";import{runCommandOnce as p}from"../../../core/runtime/run-command.js";import{readProfileEnvVars as w}from"../../../core/profiles/env-store.js";import{usage as s}from"../../../core/errors.js";const _=60,c=600,y=256*1024;function b(r){if(typeof r!="object"||r===null||Array.isArray(r))throw s("bash requires an object body");const t=r;if(typeof t.command!="string"||t.command==="")throw s("bash requires a non-empty `command`");if(typeof t.cwd!="string"||!f(t.cwd))throw new a(404,"cwd_not_found","cwd must name an absolute directory");try{if(!u(t.cwd).isDirectory())throw new Error("not a directory")}catch{throw new a(404,"cwd_not_found",`cwd not found: ${t.cwd}`)}const e=t.timeout_s??_;if(typeof e!="number"||!Number.isFinite(e)||e<=0||e>c)throw new a(422,"invalid_timeout",`timeout_s must be greater than 0 and at most ${c}`);if(t.profile!==void 0&&typeof t.profile!="string")throw s("profile must be a string");let n;if(t.env!==void 0){if(typeof t.env!="object"||t.env===null||Array.isArray(t.env))throw s("env must be an object of string values");n={};for(const[i,o]of Object.entries(t.env)){if(typeof o!="string")throw s("env must be an object of string values");n[i]=o}}return{command:t.command,cwd:t.cwd,timeout_s:e,env:n,profile:t.profile}}d(b,"parseRequest");async function T(r){const t=b(r.body),e=t.profile??null,n=l({targetCwd:t.cwd,targetProfileId:e}),i={...n,...w(e),...t.env,...e===null?{}:{CRTR_PROFILE_ID:e},CRTR_ADMITTED_HOST_PATH:n.CRTR_ADMITTED_HOST_PATH},o=await p({command:t.command,cwd:t.cwd,env:i,timeoutMs:t.timeout_s*1e3,headBytes:y}).completion;if(o.processError!==null)throw o.processError;return{status:200,body:{exit_code:o.timedOut?null:o.exitCode,signal:o.signal,stdout:o.stdout,stdout_truncated:o.stdoutTruncated,stderr:o.stderr,stderr_truncated:o.stderrTruncated,timed_out:o.timedOut,duration_ms:o.durationMs}}}d(T,"handleBash");const C=[{method:"POST",pattern:"/v1/bash",handler:T}];export{C as bashRoutes};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var p=Object.defineProperty;var
|
|
1
|
+
var p=Object.defineProperty;var c=(e,t)=>p(e,"name",{value:t,configurable:!0});import{closeSync as y,fsyncSync as S,fstatSync as T,lstatSync as $,mkdirSync as A,openSync as g,readSync as q,readdirSync as _,realpathSync as I,renameSync as L,statSync as N,unlinkSync as k,writeSync as z}from"node:fs";import{randomUUID as B}from"node:crypto";import{basename as M,dirname as F,isAbsolute as O,join as b}from"node:path";import{ApiError as a}from"../../../api/index.js";import{notFound as P,usage as u}from"../../../core/errors.js";import{InputError as D}from"../../../core/io.js";const m=512*1024,w=1024*1024,R=1e3;function h(e,t){if(e===null||e==="")throw u(`a file ${t} requires a \`path\` ${t==="write"?"body field":"query param"}`);if(!O(e))throw u(`file ${t} path must be absolute`,{received:e});return e}c(h,"requireAbsolutePath");function E(e){if(e==null)return"utf8";if(e==="utf8"||e==="base64")return e;throw u("encoding must be `utf8` or `base64`",{received:e})}c(E,"parseEncoding");function l(e,t,n){if(e instanceof a||e instanceof D)throw e;const r=e.code;throw r==="ENOENT"?P(`no such file: ${t}`,{received:t}):r==="EACCES"||r==="EPERM"?new a(403,"forbidden",`permission denied ${n} ${t}`):new a(500,"internal",`cannot ${n} ${t}: ${e instanceof Error?e.message:String(e)}`)}c(l,"mapFilesystemError");function U(e,t){let n;try{n=g(e,"r")}catch(r){return l(r,e,"reading")}try{const r=T(n);if(r.isDirectory())throw u(`file peek path is a directory: ${e}`,{received:e});const f=Buffer.allocUnsafe(Math.min(r.size,m)+1),o=q(n,f,0,f.length,0),i=o>m,s=f.subarray(0,Math.min(o,m)).toString(t);return{path:e,content:s,truncated:i}}catch(r){return l(r,e,"reading")}finally{y(n)}}c(U,"readHead");function j(e){const t=h(e.query.get("path"),"peek");return{status:200,body:U(t,E(e.query.get("encoding")))}}c(j,"handlePeek");function C(e){return e.length%4===0&&/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(e)}c(C,"isBase64");function W(e){if(typeof e!="object"||e===null||Array.isArray(e))throw u("file write requires an object body");const t=e,n=h(typeof t.path=="string"?t.path:null,"write");if(typeof t.content!="string")throw u("file write requires a string `content` body field");const r=E(t.encoding);if(r==="base64"&&!C(t.content))throw u("base64 file content must be valid base64");return{path:n,content:t.content,encoding:r}}c(W,"parseWriteBody");function Z(e){try{if(!$(e).isSymbolicLink())return e}catch(t){return t.code!=="ENOENT"?l(t,e,"resolving"):e}try{return I(e)}catch(t){return l(t,e,"resolving")}}c(Z,"writeTarget");function v(e,t){if(t.byteLength>w)throw new a(413,"file_too_large",`file content exceeds the ${w} byte limit`);const n=Z(e),r=F(n);try{A(r,{recursive:!0})}catch(i){return l(i,e,"creating parent directory for")}const f=b(r,`.${M(e)}.${B()}.tmp`);let o;try{o=g(f,"wx",384);let i=0;for(;i<t.length;)i+=z(o,t,i,t.length-i);return S(o),y(o),o=void 0,L(f,n),{path:e,bytes_written:t.byteLength}}catch(i){return l(i,e,"writing")}finally{o!==void 0&&y(o);try{k(f)}catch(i){if(i.code!=="ENOENT")throw i}}}c(v,"writeAtomically");function G(e){const{path:t,content:n,encoding:r}=W(e.body);return{status:200,body:v(t,Buffer.from(n,r))}}c(G,"handleWrite");function X(e){if(e===null)return R;const t=Number(e);if(!Number.isInteger(t)||t<1)throw u("file list limit must be a positive integer",{received:e});return t}c(X,"parseLimit");function Y(e){const t=h(e.query.get("path"),"list"),n=X(e.query.get("limit"));let r;try{r=_(t,{withFileTypes:!0}).sort((s,d)=>s.name.localeCompare(d.name))}catch(s){return l(s,t,"listing")}const f=r.slice(0,n),o=[];try{for(const s of f){const d=N(b(t,s.name));o.push({name:s.name,type:d.isFile()?"file":d.isDirectory()?"dir":"other",size:d.size,modified:d.mtime.toISOString()})}}catch(s){return l(s,t,"listing")}return{status:200,body:{path:t,entries:o,truncated:r.length>n}}}c(Y,"handleList");const te=[{method:"GET",pattern:"/v1/files/peek",handler:j},{method:"POST",pattern:"/v1/files/write",handler:G},{method:"GET",pattern:"/v1/files/list",handler:Y}];export{te as fileRoutes};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { NodeEventDTO, NodeEventErrorDTO, ReportDTO } from '../../../api/index.js';
|
|
2
|
+
import type { BrokerToClient } from '../../../core/runtime/broker-protocol.js';
|
|
3
|
+
import type { RouteTable } from '../router.js';
|
|
4
|
+
type StreamEvent = Exclude<NodeEventDTO, NodeEventErrorDTO>;
|
|
5
|
+
type WithoutSequence<T> = T extends unknown ? Omit<T, 'sequence_number'> : never;
|
|
6
|
+
type StreamEventPayload = WithoutSequence<StreamEvent>;
|
|
7
|
+
type Subscriber = (event: NodeEventDTO) => void;
|
|
8
|
+
/** The bounded per-node resume buffer. Event sequence numbers belong here rather
|
|
9
|
+
* than to HTTP connections, so every subscriber observes the same cursor. */
|
|
10
|
+
export declare class NodeEventRing {
|
|
11
|
+
private events;
|
|
12
|
+
private bytes;
|
|
13
|
+
private nextSequence;
|
|
14
|
+
private earliestAvailable;
|
|
15
|
+
append(event: StreamEventPayload): StreamEvent;
|
|
16
|
+
replay(after: number | undefined): {
|
|
17
|
+
gap: NodeEventErrorDTO | null;
|
|
18
|
+
events: readonly StreamEvent[];
|
|
19
|
+
};
|
|
20
|
+
get empty(): boolean;
|
|
21
|
+
}
|
|
22
|
+
/** Translate only the application-safe subset of broker frames. The map retains
|
|
23
|
+
* tool argument summaries between start/end; raw arguments and tool output never
|
|
24
|
+
* cross this boundary. */
|
|
25
|
+
export declare function translateBrokerFrame(nodeId: string, frame: BrokerToClient, toolSummaries?: Map<string, string>): StreamEventPayload[];
|
|
26
|
+
declare class NodeEventStreams {
|
|
27
|
+
private readonly hubs;
|
|
28
|
+
subscribe(nodeId: string, after: number | undefined, subscriber: Subscriber): {
|
|
29
|
+
unsubscribe: () => void;
|
|
30
|
+
terminal: boolean;
|
|
31
|
+
};
|
|
32
|
+
report(nodeId: string, report: ReportDTO): void;
|
|
33
|
+
}
|
|
34
|
+
export declare const nodeEventStreams: NodeEventStreams;
|
|
35
|
+
export declare const nodeEventRoutes: RouteTable;
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
var q=Object.defineProperty;var o=(n,e)=>q(n,"name",{value:e,configurable:!0});import{randomUUID as T}from"node:crypto";import{ApiError as E}from"../../../api/errors.js";import{BrokerClient as C}from"../../../core/broker-client/client.js";import{getNode as b}from"../../../core/canvas/canvas.js";import{isPidAlive as A}from"../../../core/canvas/pid.js";import{BROKER_VIEW_SOCKET_WAIT_MS as M}from"../../../core/runtime/view-socket.js";import{toNodeOutcomeDTO as B}from"../map.js";import{reportsForNode as k}from"./reports.js";const L=15e3,$=6e4,w=250,P=512,R=256*1024,j=1e3,D=32*1024*1024;class G{static{o(this,"NodeEventRing")}events=[];bytes=0;nextSequence=1;earliestAvailable=1;append(e){const t={...e,sequence_number:this.nextSequence++},s=Buffer.byteLength(JSON.stringify(t),"utf8");if(s>R)return this.events=[],this.bytes=0,this.earliestAvailable=this.nextSequence,t;for(this.events.push(t),this.bytes+=s;this.events.length>P||this.bytes>R;){const r=this.events.shift();if(r===void 0)break;this.bytes-=Buffer.byteLength(JSON.stringify(r),"utf8"),this.earliestAvailable=r.sequence_number+1}return t}replay(e){if(e===void 0)return{gap:null,events:this.events};const t=e<this.earliestAvailable-1||e>=this.nextSequence?N(this.earliestAvailable):null;return{gap:t,events:t===null?this.events.filter(s=>s.sequence_number>e):this.events}}get empty(){return this.events.length===0}}function N(n){return{type:"error",error:{code:"stream_gap",message:"the requested event cursor is older than this daemon can resume",details:{earliest_sequence:n}}}}o(N,"streamGap");function V(n){return n===null?"arguments: null":Array.isArray(n)?`arguments: array (${n.length} items)`:typeof n=="object"?`arguments: object (${Object.keys(n).length} fields)`:`arguments: ${typeof n}`}o(V,"toolSummary");function I(n){if(typeof n!="object"||n===null)return"";const e=n.content;return typeof e=="string"?e:Array.isArray(e)?e.filter(t=>typeof t=="object"&&t!==null).filter(t=>t.type==="text"&&typeof t.text=="string").map(t=>t.text).join(""):""}o(I,"assistantText");function H(n,e,t=new Map){switch(e.type){case"message_update":return e.message.role==="assistant"&&e.assistantMessageEvent.type==="text_delta"?[{type:"node.output_text.delta",node_id:n,delta:e.assistantMessageEvent.delta}]:[];case"message_end":return e.message.role==="assistant"?[{type:"node.output_text.done",node_id:n,text:I(e.message)}]:[];case"tool_execution_start":{const s=V(e.args);return t.set(e.toolCallId,s),[{type:"node.tool_call.started",node_id:n,tool_call_id:e.toolCallId,tool:e.toolName,summary:s}]}case"tool_execution_end":{const s=t.get(e.toolCallId)??"[arguments unavailable]";return t.delete(e.toolCallId),[{type:"node.tool_call.completed",node_id:n,tool_call_id:e.toolCallId,tool:e.toolName,status:e.isError?"error":"ok",summary:s}]}case"turn_start":return[{type:"node.turn.started",node_id:n}];case"turn_end":return[{type:"node.turn.completed",node_id:n}];default:return[]}}o(H,"translateBrokerFrame");class z{static{o(this,"NodeEventHub")}nodeId;remove;ring=new G;subscribers=new Set;observer;observerErrorAt;poll;reconnect;idleClose;status;settledRevision;settledOutcome;settledSequence;snapshotSeeded=!1;reportsSeeded=!1;toolSummaries=new Map;fresh=!0;constructor(e,t){this.nodeId=e,this.remove=t}subscribe(e,t){const s=this.fresh&&e!==void 0;this.fresh=!1,this.start();const r=this.settledOutcome!==void 0,d=s?{gap:N(1),events:this.ring.replay(void 0).events}:this.ring.replay(e);r||(this.subscribers.add(t),this.status!=="dormant"&&this.openObserver());let h=!1;return queueMicrotask(()=>{if(!h){d.gap!==null&&t(d.gap);for(const l of d.events)t(l)}}),r&&this.scheduleIdleClose(),{unsubscribe:o(()=>{h=!0,r||this.unsubscribe(t)},"unsubscribe"),terminal:r}}report(e){this.settledRevision===void 0&&this.publish({type:"node.report.pushed",node_id:this.nodeId,report:e})}start(){this.idleClose!==void 0&&(clearTimeout(this.idleClose),this.idleClose=void 0),this.poll===void 0&&(this.check(),this.settledRevision===void 0&&(this.seedReports(),this.poll=setInterval(()=>this.check(),w)))}seedReports(){if(!this.reportsSeeded){this.reportsSeeded=!0;for(const e of k(this.nodeId).reverse())this.report(e)}}check(){const e=b(this.nodeId);if(e===null){this.fail("stream_error",`node ${this.nodeId} no longer exists`);return}const t=B(e);if(t!==null){this.settledRevision!==t.revision&&(this.settledRevision=t.revision,this.settledOutcome=t,this.settledSequence=this.publish({type:"node.settled",node_id:this.nodeId,outcome:t}).sequence_number,this.stopObserver(),this.stopPoll());return}const s=e.pi_pid===null?"dormant":e.status;if(s==="dormant"){this.status!=="dormant"&&(this.status=s,this.publish({type:"node.status.changed",node_id:this.nodeId,status:s}));return}this.status===void 0?this.status=s:this.status!=="dormant"&&this.status!==s&&(this.status=s,this.publish({type:"node.status.changed",node_id:this.nodeId,status:s})),this.openObserver()}openObserver(){if(this.observer!==void 0||this.subscribers.size===0)return;const e=new C(this.nodeId);this.observer=e,e.on("connect",()=>{this.observerErrorAt=void 0;const t=b(this.nodeId);this.status==="dormant"&&t!==null&&t.pi_pid!==null&&(this.status=t.status,this.publish({type:"node.status.changed",node_id:this.nodeId,status:t.status})),e.send({type:"hello",role:"observer",client_id:T(),attends:!1})}),e.on("frame",t=>{try{t.type==="welcome"&&this.seedSnapshot(t.snapshot);for(const s of H(this.nodeId,t,this.toolSummaries))this.publish(s)}catch(s){this.fail("stream_error",s instanceof Error?s.message:String(s))}}),e.on("error",()=>{const t=b(this.nodeId),s=Date.now();t!==null&&t.pi_pid!==null&&A(t.pi_pid)&&(this.observerErrorAt??=s,s-this.observerErrorAt<M)||this.fail("stream_error","observer connection failed")}),e.on("close",()=>{this.observer===e&&(this.observer=void 0,!(this.subscribers.size===0||this.settledRevision!==void 0)&&(this.status!=="dormant"&&(this.publish({type:"node.status.changed",node_id:this.nodeId,status:"dormant"}),this.status="dormant"),this.scheduleReconnect()))}),e.connect()}seedSnapshot(e){if(this.snapshotSeeded)return;this.snapshotSeeded=!0;const t=e.messages.filter(r=>r.role==="assistant"),s=e.state.isStreaming?t.pop():void 0;for(const r of t)this.publish({type:"node.output_text.done",node_id:this.nodeId,text:I(r)});s!==void 0&&this.publish({type:"node.output_text.delta",node_id:this.nodeId,delta:I(s)})}scheduleReconnect(){this.reconnect===void 0&&(this.reconnect=setTimeout(()=>{this.reconnect=void 0,this.check()},w))}publish(e){const t=this.ring.append(e);for(const s of[...this.subscribers])s(t);return t}fail(e,t){const s={type:"error",error:{code:e,message:t}};for(const r of[...this.subscribers])r(s);this.stopObserver(),this.stopPoll()}unsubscribe(e){this.subscribers.delete(e),this.subscribers.size===0&&this.scheduleIdleClose()}scheduleIdleClose(){this.idleClose===void 0&&(this.idleClose=setTimeout(()=>{this.stopObserver(),this.stopPoll(),this.remove()},$))}stopObserver(){this.reconnect!==void 0&&(clearTimeout(this.reconnect),this.reconnect=void 0);const e=this.observer;this.observer=void 0,e?.close()}stopPoll(){this.poll!==void 0&&(clearInterval(this.poll),this.poll=void 0)}}class J{static{o(this,"NodeEventStreams")}hubs=new Map;subscribe(e,t,s){let r=this.hubs.get(e);return r===void 0&&(r=new z(e,()=>this.hubs.delete(e)),this.hubs.set(e,r)),r.subscribe(t,s)}report(e,t){this.hubs.get(e)?.report(t)}}const F=new J;function K(n){const e=n.get("after");if(e===null||e==="")return{};if(!/^(?:0|[1-9]\d*)$/.test(e))throw new E(400,"invalid_request","`after` must be a non-negative integer");const t=Number(e);if(!Number.isSafeInteger(t))throw new E(400,"invalid_request","`after` must be a safe integer");return{after:t}}o(K,"parseQuery");function S(n){const{type:e,...t}=n;return`event: ${e}
|
|
2
|
+
data: ${JSON.stringify(t)}
|
|
3
|
+
|
|
4
|
+
`}o(S,"sseRecord");async function U(n){const e=n.params.id??"";if(b(e)===null)throw new E(404,"node_not_found",`no node ${e}`);const{after:t}=K(n.query),{req:s,res:r}=n;r.writeHead(200,{"content-type":"text/event-stream; charset=utf-8","cache-control":"no-cache",connection:"keep-alive"});let d=!1,h=!1,l=!1,c=0;const u=[];let m,f;const v=o(()=>{r.writableEnded||r.end()},"finish"),p=o(()=>{if(!d){if(d=!0,f!==void 0&&clearInterval(f),m?.(),s.destroyed||r.destroyed){u.length=0,c=0,v();return}if(l||u.length>0){h=!0;return}v()}},"close"),_=o(()=>{for(l=!1;u.length>0;){const i=u.shift();if(c-=Buffer.byteLength(i,"utf8"),!r.write(i)){l=!0,r.once("drain",_);return}}h&&v()},"flush"),O=o(()=>{if(d)return;u.length=0,c=0;const i=S({type:"error",error:{code:"stream_dropped",message:"stream reader is too slow"}});u.push(i),c=Buffer.byteLength(i,"utf8"),d=!0,h=!0,f!==void 0&&clearInterval(f),m?.(),l||_()},"dropSlowReader"),y=o(i=>{if(d)return!1;if(l||u.length>0){const a=Buffer.byteLength(i,"utf8");return u.length>=j||c+a>D?(O(),!1):(u.push(i),c+=a,!0)}return r.write(i)||(l=!0,r.once("drain",_)),!0},"write"),g=o((i,a)=>{d||y(S({type:"error",error:{code:i,message:a}})),p()},"fail"),x=o(i=>{try{if(!y(S(i)))return;(i.type==="node.settled"||i.type==="error"&&i.error.code!=="stream_gap")&&p()}catch(a){g("stream_error",a instanceof Error?a.message:String(a))}},"onEvent");f=setInterval(()=>{try{d||y(`: keepalive
|
|
5
|
+
|
|
6
|
+
`)}catch(i){g("stream_error",i instanceof Error?i.message:String(i))}},L),s.once("close",p);try{const i=F.subscribe(e,t,x);m=i.unsubscribe,i.terminal&&queueMicrotask(p)}catch(i){g("stream_error",i instanceof Error?i.message:String(i))}return{status:200}}o(U,"handleEvents");const re=[{method:"GET",pattern:"/v1/nodes/:id/events",handler:U}];export{G as NodeEventRing,re as nodeEventRoutes,F as nodeEventStreams,H as translateBrokerFrame};
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
var
|
|
2
|
-
\u2026 (truncated \u2014 the full result is at the result path above)`:
|
|
1
|
+
var D=Object.defineProperty;var s=(t,e)=>D(t,"name",{value:e,configurable:!0});import{existsSync as P,readdirSync as E,readFileSync as A,statSync as C}from"node:fs";import{join as I}from"node:path";import{getNode as f}from"../../../core/canvas/canvas.js";import{push as F,FinalizationError as S}from"../../../core/feed/feed.js";import{branchIsPushedAtCurrentTip as j,hasOpenManagedWorktree as J,missingWorktreeRecoveryHint as M}from"../../../core/worktree.js";import{autoDropCleanManagedWorktreeAsync as z}from"../../../core/worktree-mutation-async.js";import{reportsDir as B}from"../../../core/canvas/paths.js";import{notFound as H,usage as c}from"../../../core/errors.js";import{InputError as d}from"../../../core/io.js";import{parseFrontmatterGeneric as W}from"../../../core/frontmatter.js";import{toReportResultDTO as L}from"../map.js";import{outputSchemaPath as G,readOutputRequest as k,removeOutputSchema as X,validateStructuredResult as K,writeOutputDeclined as V,writeOutputResult as Q}from"../../../core/runtime/structured-output.js";import{nodeEventStreams as O}from"./node-events.js";function g(t){if(f(t)===null)throw H(`unknown node: ${t}`,{received:t})}s(g,"requireNode");function U(t){if(typeof t!="object"||t===null)throw c("a push report requires a JSON body { tier, body }");const e=t.tier,r=t.delivery_tier,n=t.body;if(e!=="update"&&e!=="urgent"&&e!=="final")throw c(`invalid report tier: ${String(e)} (expected update|urgent|final)`);if(r!==void 0&&r!=="deferred"&&r!=="normal"&&r!=="urgent")throw c(`invalid delivery_tier: ${String(r)} (expected deferred|normal|urgent)`);if(e!=="update"&&r!==void 0)throw c("delivery_tier is only valid for update reports");if(typeof n!="string"||n.trim()==="")throw c("a push report requires a non-empty body");return{tier:e,...r===void 0?{}:{delivery_tier:r},body:n}}s(U,"parsePushBody");async function Y(t){const e=t.params.id;g(e);const r=U(t.body);if(r.tier==="final"&&k(e).state==="valid")throw new d({error:"structured_result_pending",message:"This node owes a structured result \u2014 a plain `push final` would finish it without recording one.",next:'Submit the result with `crtr push result` instead; it validates against the pending schema, records it, and pushes the final report for you. If the schema cannot be honestly satisfied, `crtr push result --decline "<reason>" --code <token>` records the decline and finishes the node.'});const n=r.tier==="final"?f(e)?.status:void 0,i=r.tier==="final"?await $(e):void 0,o=await q(e,r),u=r.tier==="final"&&n!==void 0?{from:n,to:f(e)?.status??"done"}:void 0,l=L(o,u,i),p=b(e).find(a=>a.path===o.reportPath);return p!==void 0&&O.report(e,p),{status:200,body:l}}s(Y,"handlePush");async function $(t){if(!J(f(t)))return;const e=await z(t);if(e!==null)return e.worktree_path;if(!await j(f(t).managed_worktree))throw new d({error:"open_managed_worktree",message:"This node still owns an open managed worktree that is neither clean and locally landed nor clean with its exact tip on origin \u2014 `push final` cannot finish.",next:(()=>{const r=f(t)?.managed_worktree;return r!=null&&r.state==="open"&&!P(r.path)?M(r):"Commit the remaining work in the worktree, push its branch to origin at its current tip, then rerun `crtr push final`; or run `crtr node worktree close` to land it locally."})()})}s($,"guardFinalWorktree");const R=2e3;function Z(t,e){const r=JSON.stringify(e,null,2),n=r.length>R?`${r.slice(0,R)}
|
|
2
|
+
\u2026 (truncated \u2014 the full result is at the result path above)`:r;return`Structured output submitted.
|
|
3
3
|
|
|
4
4
|
Result path: ${t}
|
|
5
5
|
|
|
6
6
|
\`\`\`json
|
|
7
|
-
${
|
|
7
|
+
${n}
|
|
8
8
|
\`\`\`
|
|
9
|
-
`}s(
|
|
9
|
+
`}s(Z,"submitReportBody");function ee(t,e){return`Structured result declined.
|
|
10
10
|
|
|
11
11
|
Reason: ${e.reason}
|
|
12
12
|
Code: ${e.code}
|
|
13
13
|
Retryable: ${e.retryable?"yes":"no"}
|
|
14
14
|
|
|
15
15
|
Declined marker: ${t}
|
|
16
|
-
`}s(
|
|
16
|
+
`}s(ee,"declineReportBody");const T=1024,N=64;function te(t){if(typeof t!="object"||t===null)throw c("a result submission requires a JSON body { value } or { decline, code, retryable }");const e=Object.hasOwn(t,"value"),r=Object.hasOwn(t,"decline");if(e&&r)throw c("a result submission carries either { value } or { decline, code, retryable }, not both");if(r){const{decline:n,code:i,retryable:o}=t;if(typeof n!="string"||n.trim()==="")throw new d({error:"missing_decline_reason",message:"A decline needs a reason.",next:'Rerun with `--decline "<one sentence on why the schema cannot be honestly satisfied>" --code <token>`.'});if(n.length>T)throw new d({error:"decline_reason_too_long",message:`A decline reason is at most ${T} characters (got ${n.length}).`,next:"Keep the reason to one plain sentence; put any detail in an artifact and name its path."});if(typeof i!="string"||i.trim()===""||/\s/.test(i.trim())||i.trim().length>N)throw new d({error:"invalid_decline_code",message:`A decline needs a code: one whitespace-free token of at most ${N} characters naming the decline class.`,next:"Rerun with `--code <token>` \u2014 the token your requester documented for this case."});if(typeof o!="boolean")throw c("a decline carries `retryable` as a boolean");return{declined:{reason:n.trim(),code:i.trim(),retryable:o}}}if(!e)throw c("a result submission requires a JSON body { value } or { decline, code, retryable }");return{value:t.value}}s(te,"parseSubmitBody");async function re(t){const e=t.params.id;g(e);const r=te(t.body),n=k(e);if(n.state==="absent")throw new d({error:"no_output_request",message:"No structured output is currently requested of this node.",next:"Nothing to submit here \u2014 continue working, or finish with `crtr push final`."});if(n.state==="invalid")throw new d({error:"invalid_output_request",message:`The structured-output request file is invalid: ${n.error}`,next:`Have the requester re-issue the request, or remove ${G(e)} and finish with \`crtr push final\`.`});const{mode:i,schema:o}=n.request;let u;if("value"in r)try{u=await K(o,r.value)}catch(h){throw new d({error:"result_schema_mismatch",message:`the submitted result does not match the requested schema: ${h.message}`,next:'Fix the listed fields and rerun `crtr push result`; if the schema cannot be honestly satisfied, rerun with `--decline "<reason>" --code <token>`.'})}const l=i==="terminal"?f(e)?.status:void 0,p=i==="terminal"?await $(e):void 0;let a,m,w;if("declined"in r){const{declined:h}=r;if(a=V(e,{mode:i,...h,at:new Date().toISOString()}),m=ee(a,h),i==="terminal"){const x={schema:"crtr.node-outcome-detail/v1",message:h.reason,declined:h};w={kind:"failure",reason:"declined",detailJson:JSON.stringify(x)}}}else a=Q(e,u),m=Z(a,u);const y=await q(e,{tier:i==="terminal"?"final":"update",body:m},w);X(e);const _=i==="terminal"&&l!==void 0?{from:l,to:f(e)?.status??"done"}:void 0,v=b(e).find(h=>h.path===y.reportPath);return v!==void 0&&O.report(e,v),{status:200,body:{mode:i,result_path:a,report_path:y.reportPath,notified:[...y.deliveredTo],..."declined"in r?{declined:r.declined}:{},..._===void 0?{}:{transitioned:_},...p===void 0?{}:{worktree_auto_dropped:!0,worktree_auto_dropped_path:p}}}}s(re,"handleSubmitResult");async function q(t,e,r){try{return await F(t,{kind:e.tier,body:e.body,...e.delivery_tier===void 0?{}:{deliveryTier:e.delivery_tier},...r===void 0?{}:{outcome:r}})}catch(n){throw n instanceof S&&n.code==="already_finalized"?new d({error:"already_finalized",message:"This node already submitted its canonical final result.",next:"Push an update with the new information, then stop."}):n instanceof S&&n.code==="finalization_forbidden"?new d({error:"finalization_forbidden",message:"A canceled or dead node cannot submit a canonical final result.",next:"Do not push final from this node."}):n}}s(q,"pushOrRethrow");function ne(t){const e=t.match(/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})$/);return e===null?t:`${e[1]}-${e[2]}-${e[3]}T${e[4]}:${e[5]}:${e[6]}`}s(ne,"compactToIso");function b(t){const e=B(t);let r;try{r=E(e).filter(i=>i.endsWith(".md"))}catch{r=[]}const n=[];for(const i of r){const o=I(e,i),u=A(o,"utf8"),{data:l,body:p}=W(u),a=i.match(/^([0-9]{8}T[0-9]{6})-(\w+)\.md$/),m=(typeof l?.kind=="string"?l.kind:void 0)??(a?a[2]:"update"),w=(typeof l?.ts=="string"?l.ts:void 0)??(a?ne(a[1]):new Date(C(o).mtimeMs).toISOString());n.push({path:o,tier:m,body:p.trim(),created:w})}return n.sort((i,o)=>i.created<o.created?1:i.created>o.created?-1:0),n}s(b,"reportsForNode");function ie(t){const e=t.params.id;g(e);const r=t.query.get("tier"),n=t.query.get("limit"),i=n!==null?Number(n):void 0;let o=b(e);return r!==null&&(o=o.filter(u=>u.tier===r)),i!==void 0&&Number.isFinite(i)&&i>=0&&(o=o.slice(0,i)),{status:200,body:o}}s(ie,"handleList");const _e=[{method:"POST",pattern:"/v1/nodes/:id/reports",handler:Y},{method:"GET",pattern:"/v1/nodes/:id/reports",handler:ie},{method:"POST",pattern:"/v1/nodes/:id/result",handler:re}];export{_e as reportRoutes,b as reportsForNode};
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
var j=Object.defineProperty;var i=(e,t)=>j(e,"name",{value:t,configurable:!0});import{createServer as w}from"node:http";import{chmodSync as z,existsSync as C,unlinkSync as O}from"node:fs";import{apiSocketPath as x}from"../../core/canvas/paths.js";import{readConfig as G}from"../../core/config.js";import{getApiToken as J}from"../../core/secrets.js";import{emitEvent as g}from"../../core/events/emit.js";import{envCrtrdTcp as L,envCrtrdToken as _}from"../../shared/env.js";import{Router as U}from"./router.js";import{handleAttachUpgrade as D}from"./bridge.js";import{boundDaemonControl as F}from"../control.js";import{attachRoutes as M}from"./handlers/attach.js";import{bashJobRoutes as Y}from"./handlers/bash-jobs.js";import{brokerOperationRoutes as
|
|
1
|
+
var j=Object.defineProperty;var i=(e,t)=>j(e,"name",{value:t,configurable:!0});import{createServer as w}from"node:http";import{chmodSync as z,existsSync as C,unlinkSync as O}from"node:fs";import{apiSocketPath as x}from"../../core/canvas/paths.js";import{readConfig as G}from"../../core/config.js";import{getApiToken as J}from"../../core/secrets.js";import{emitEvent as g}from"../../core/events/emit.js";import{envCrtrdTcp as L,envCrtrdToken as _}from"../../shared/env.js";import{Router as U}from"./router.js";import{handleAttachUpgrade as D}from"./bridge.js";import{boundDaemonControl as F}from"../control.js";import{attachRoutes as M}from"./handlers/attach.js";import{bashJobRoutes as Y}from"./handlers/bash-jobs.js";import{bashRoutes as Z}from"./handlers/bash.js";import{brokerOperationRoutes as E}from"./handlers/broker-ops.js";import{brokerRecoveryRoutes as k,nodeFaultRoutes as K}from"./handlers/broker-recovery.js";import{canvasRoutes as Q}from"./handlers/canvas.js";import{chatInventoryRoutes as V}from"./handlers/chat-inventory.js";import{daemonRoutes as W}from"./handlers/daemon.js";import{fileRoutes as X}from"./handlers/files.js";import{focusRoutes as q}from"./handlers/focus.js";import{healthRoutes as ee}from"./handlers/health.js";import{humanRequestRoutes as re}from"./handlers/human-requests.js";import{inboxRoutes as te}from"./handlers/inbox.js";import{feedbackCommentRoutes as oe}from"./handlers/feedback-comments.js";import{memoryRoutes as ne}from"./handlers/memory.js";import{brokerMailRoutes as P,messageRoutes as ie}from"./handlers/messages.js";import{modelAuthRoutes as se}from"./handlers/modelauth.js";import{modelConfigRoutes as le}from"./handlers/model-config.js";import{nodeRoutes as ce}from"./handlers/nodes.js";import{nodeOutcomeRoutes as ae}from"./handlers/node-outcomes.js";import{nodeEventRoutes as ue}from"./handlers/node-events.js";import{profileRoutes as de}from"./handlers/profiles.js";import{prospectiveChatInventoryRoutes as pe}from"./handlers/prospective-chat-inventory.js";import{reportRoutes as me}from"./handlers/reports.js";import{reviewCommentRoutes as fe}from"./handlers/review-comments.js";import{reviewRoutes as ge}from"./handlers/reviews.js";import{subscriptionRoutes as he}from"./handlers/subscriptions.js";import{cronRoutes as Ae}from"./handlers/crons.js";import{worktreeRoutes as ye}from"./handlers/worktree.js";function Re(e){return new U().registerAll(ee(e)).registerAll(W).registerAll(ce).registerAll(ae).registerAll(ue).registerAll(Y).registerAll(Z).registerAll(E).registerAll(k).registerAll(K).registerAll(ye).registerAll(P).registerAll(ie).registerAll(me).registerAll(M).registerAll(Q).registerAll(X).registerAll(ne).registerAll(q).registerAll(he).registerAll(Ae).registerAll(de).registerAll(se).registerAll(le).registerAll(ge).registerAll(fe).registerAll(te).registerAll(re).registerAll(V).registerAll(pe).registerAll(oe)}i(Re,"buildRouter");function ve(e,t){if(e.toUpperCase()!=="GET")return null;const s=t.split("/").filter(m=>m.length>0);if(s.length!==4||s[0]!=="v1"||s[1]!=="nodes"||s[3]!=="attach")return null;const a=s[2];return a===void 0||a===""?null:decodeURIComponent(a)}i(ve,"attachNodeId");function Te(e){const t=e.trim();if(t==="")return null;const s=t.lastIndexOf(":");if(s<0)return null;const a=t.slice(0,s),m=t.slice(s+1),c=Number(m);return!Number.isInteger(c)||c<0||c>65535?null:{host:a===""?"0.0.0.0":a,port:c}}i(Te,"parseTcp");function be(e,t){return`${e.includes(":")?`[${e}]`:e}:${t}`}i(be,"formatTcpAddress");const H={error:{code:"unauthorized",message:"missing or invalid bearer token"}};function N(e,t){return e.headers.authorization===`Bearer ${t}`}i(N,"isAuthorized");function Se(e){const t=JSON.stringify(H);e.writeHead(401,{"content-type":"application/json; charset=utf-8"}),e.end(t)}i(Se,"writeUnauthorized");function we(e,t){e.writeHead(503,{"content-type":"application/json; charset=utf-8"}),e.end(JSON.stringify({error:{code:"startup_blocked",message:t}}))}i(we,"writeStartupBlocked");function Ce(e,t){const s=JSON.stringify({error:{code:"startup_blocked",message:t}}),a=`HTTP/1.1 503 Service Unavailable\r
|
|
2
2
|
content-type: application/json; charset=utf-8\r
|
|
3
3
|
content-length: ${Buffer.byteLength(s)}\r
|
|
4
4
|
connection: close\r
|
|
5
5
|
\r
|
|
6
|
-
${s}`;try{e.end(a)}catch{e.destroy()}}i(
|
|
6
|
+
${s}`;try{e.end(a)}catch{e.destroy()}}i(Ce,"destroyUpgradeStartupBlocked");function Oe(e){const t=JSON.stringify(H),s=`HTTP/1.1 401 Unauthorized\r
|
|
7
7
|
content-type: application/json; charset=utf-8\r
|
|
8
8
|
content-length: ${Buffer.byteLength(t)}\r
|
|
9
9
|
connection: close\r
|
|
10
10
|
\r
|
|
11
|
-
${t}`;try{e.end(s)}catch{try{e.destroy()}catch{}}}i(
|
|
11
|
+
${t}`;try{e.end(s)}catch{try{e.destroy()}catch{}}}i(Oe,"destroyUpgradeUnauthorized");function I(e){return new Promise(t=>{try{e.closeAllConnections()}catch{}e.close(()=>t())})}i(I,"closeServer");function fr(e={}){let t=null;const s=Re(()=>t),a=new U().registerAll(E).registerAll(k).registerAll(P),m=i((o,r)=>{const u=F()?.startupBlock()??null;return u===null||o==="GET"&&(r==="/healthz"||r==="/v1/status")||o==="POST"&&(r==="/v1/daemon/migrate"||r==="/v1/daemon/admit")||u.phase==="recovering"&&a.match(o,r)!==null?null:u},"startupBlockFor"),c=x(),A=i((o,r)=>{const u=i(n=>{g({event:"api.request.failed",level:"error",error:n instanceof Error?n:new Error(String(n))});try{r.headersSent||r.writeHead(500,{"content-type":"application/json; charset=utf-8"}),r.end(JSON.stringify({error:{code:"internal",message:"internal error"}}))}catch{}},"internalFailure");let p;try{p=new URL(o.url??"/","http://127.0.0.1")}catch{try{r.writeHead(400,{"content-type":"application/json; charset=utf-8"}),r.end(JSON.stringify({error:{code:"invalid_request",message:"request URL is invalid"}}))}catch{}return}try{const n=m((o.method??"GET").toUpperCase(),p.pathname);if(n!==null){we(r,n.message);return}s.handle(o,r).catch(u)}catch(n){u(n)}},"onRequest"),y=i((o,r,u)=>{r.on("error",()=>{});try{const p=new URL(o.url??"/","http://127.0.0.1"),n=m((o.method??"GET").toUpperCase(),p.pathname);if(n!==null){Ce(r,n.message);return}const l=ve(o.method??"GET",p.pathname);if(l===null){r.destroy();return}D(o,r,u,l).catch(()=>{try{r.destroy()}catch{}})}catch{try{r.destroy()}catch{}}},"onUpgrade"),B=i(o=>{o.on("error",r=>{g({event:"api.server.failed",level:"error",error:r})})},"bindErrorHandler"),f=w(A);f.on("upgrade",y);let v=!1,T,b;const $=new Promise((o,r)=>{T=o,b=r});f.once("listening",()=>{v=!0,T()}),f.on("error",o=>{g({event:"api.server.failed",level:"error",error:o}),v||b(o)});try{C(c)&&O(c)}catch{}f.listen(c,()=>{try{z(c,384)}catch(o){g({event:"api.socket.chmod_failed",level:"error",error:o instanceof Error?o:new Error(String(o))})}});let d;const h=e.tcp??L()??G("user").api.tcp;if(h!==void 0&&h!==""){const o=Te(h);if(o===null)g({event:"api.tcp.invalid",level:"error",error:new Error(`invalid CRTRD_TCP/--tcp spec: ${h}`)});else{const r=e.token??_()??J("user"),u=r===void 0?A:(n,l)=>{if((n.method??"GET").toUpperCase()==="OPTIONS"){l.writeHead(204,{"Access-Control-Allow-Origin":"*","Access-Control-Allow-Headers":"authorization, content-type","Access-Control-Allow-Methods":"GET, POST, PATCH, PUT, DELETE, OPTIONS","Access-Control-Max-Age":"600"}),l.end();return}if(!N(n,r)){Se(l);return}l.setHeader("Access-Control-Allow-Origin","*"),A(n,l)},p=r===void 0?y:(n,l,R)=>{if(!N(n,r)){Oe(l);return}y(n,l,R)};d=w(u),d.on("upgrade",p),B(d),d.once("listening",()=>{const n=d?.address();if(n!==null&&typeof n=="object"){const{address:l,port:R}=n;t=be(l,R)}}),d.listen(o.port,o.host)}}let S=!1;return{ready:$,tcpAddress:i(()=>t,"tcpAddress"),close:i(async()=>{if(!S){S=!0,await Promise.all([I(f),d!==void 0?I(d):Promise.resolve()]);try{C(c)&&O(c)}catch{}}},"close")}}i(fr,"createApiServer");export{fr as createApiServer};
|
|
@@ -14,7 +14,9 @@ export declare const EMPTY_STDOUT_SHA256: string;
|
|
|
14
14
|
* cap (UTF-16 code units, not bytes) and corrupted any multi-byte sequence
|
|
15
15
|
* that straddled a chunk boundary. Truncation is MARKED, never silent: a
|
|
16
16
|
* clipped body reads as complete output to whoever receives the delivery. */
|
|
17
|
-
export declare function newHeadCapture(): {
|
|
17
|
+
export declare function newHeadCapture(headBytes?: number): {
|
|
18
18
|
take(chunk: Buffer): void;
|
|
19
19
|
decode(): string;
|
|
20
|
+
truncated(): boolean;
|
|
20
21
|
};
|
|
22
|
+
export declare function formatCronOutputHead(text: string, truncated: boolean): string;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var
|
|
2
|
-
[\u2026 output truncated at ${s} bytes]`:t}
|
|
1
|
+
var c=Object.defineProperty;var a=(t,e)=>c(t,"name",{value:e,configurable:!0});import{createHash as p}from"node:crypto";const s=4*1024,d=2e3,T=250,i=p("sha256").update("").digest("hex");function O(t=s){const e=[];let n=0,o=!1;return{take(r){const u=t-n;if(u<=0){o=!0;return}if(r.length>u){e.push(r.subarray(0,u)),n=t,o=!0;return}e.push(r),n+=r.length},decode(){return Buffer.concat(e).toString("utf8")},truncated(){return o}}}a(O,"newHeadCapture");function x(t,e){return e?`${t}
|
|
2
|
+
[\u2026 output truncated at ${s} bytes]`:t}a(x,"formatCronOutputHead");export{T as CRON_DEATH_POLL_MS,d as CRON_KILL_GRACE_MS,s as CRON_OUTPUT_HEAD_BYTES,i as EMPTY_STDOUT_SHA256,x as formatCronOutputHead,O as newHeadCapture};
|
package/dist/daemon/cron-run.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var O=Object.defineProperty;var m=(e,t)=>O(e,"name",{value:t,configurable:!0});import{randomUUID as D}from"node:crypto";import{CRON_OUTPUT_HEAD_BYTES as h,EMPTY_STDOUT_SHA256 as x,formatCronOutputHead as _}from"./cron/capture.js";import{clearLiveRun as A,cronDaemonInstanceId as R,liveRunFor as T,registerLiveRun as v}from"./cron/live-runs.js";import{buildArmedRunContext as g}from"./cron/armed-context.js";import{decideHeldSettle as M,EXIT_HELD as k,HELD_DELIVERED as B}from"./cron/held.js";import{settleRun as H}from"./cron/settle.js";import{applyOverlapPolicies as L,expireDueCrons as U}from"./cron/passes.js";import{dueClockCrons as b,acquireCronRunLease as y,advanceCron as P,setCronState as $}from"../core/canvas/crons.js";import{emitEvent as N}from"../core/events/emit.js";import{runCommandOnce as Y}from"../core/runtime/run-command.js";import{nextSlotAfter as F}from"../core/wake.js";import{CRON_OUTPUT_HEAD_BYTES as ae}from"./cron/capture.js";import{cronDaemonInstanceId as ue,markSelfCanceledCronRun as le}from"./cron/live-runs.js";import{recoverStaleCronLeases as me}from"./cron/lease-recovery.js";async function q(e,t){const{runId:d,outOfBand:r,startedAtIso:n,finishedAtMs:o,code:u,timedOut:l,processError:i}=t,p=!r&&!l&&i==null&&u===k&&!t.wasReplaced&&!t.wasSelfCanceled;let a,c;return t.wasReplaced?(a={kind:"stated",delivered:"replaced (overlap=replace)"},c="release"):p?(a={kind:"stated",delivered:B},c=M(e,n,o)??"release"):(a={kind:"disposed",ctx:{runId:d,startedAtIso:n,exitCode:l?-1:u,timedOut:l,runTimeoutS:e.run_timeout_s,processErrorMessage:i!=null?i.message:null,stdoutHead:t.stdoutHead,stdoutSha256:t.stdoutSha256,stderrHead:t.stderrHead},selfCanceled:t.wasSelfCanceled},c=r?"release":{kind:"clear"}),H(e,{outOfBand:r,outcome:a,record:{runId:d,startedAtIso:n,finishedAtMs:o,durationMs:o-t.startedAtMs,exitCode:l?-1:u,stdoutHead:t.stdoutHead===""?null:t.stdoutHead,stderrHead:i!=null?`run process error: ${i.message}`.slice(0,h):t.stderrHead===""?null:t.stderrHead},lease:c})}m(q,"settleClosedRun");function I(e,t,d,r){const n=Date.now(),o=`${d}: ${r.message}`.slice(0,h);return H(e,{outOfBand:t.outOfBand,outcome:{kind:"disposed",ctx:{runId:t.runId,startedAtIso:t.startedAtIso,exitCode:null,timedOut:!1,runTimeoutS:e.run_timeout_s,processErrorMessage:r.message,stdoutHead:"",stdoutSha256:x,stderrHead:o}},record:{runId:t.runId,startedAtIso:t.startedAtIso,finishedAtMs:n,durationMs:n-t.startedAtMs,exitCode:null,stdoutHead:null,stderrHead:o},lease:"none"})}m(I,"settleUnspawnedRun");function J(e,t){let d;try{d=F(e.recur,new Date(t))}catch(r){return N({level:"error",event:"cron.recur.unparseable",error:r,fields:{cron_id:e.cron_id,name:e.name,recur:e.recur}}),$(e.cron_id,"paused"),new Error(`unparseable recurrence ${JSON.stringify(e.recur)}: ${r.message} \u2014 this cron cannot be rescheduled until its recurrence is fixed (\`crtr cron cancel\` and re-arm it), so it has been paused.`)}return P(e.cron_id,d),null}m(J,"advanceRecurrence");function V(e,t){const{outOfBand:d}=t;return new Promise((r,n)=>{const o=D(),u=Date.now(),l=new Date(u).toISOString(),i={runId:o,outOfBand:d,startedAtIso:l,startedAtMs:u};let p;try{({env:p}=g(e,o))}catch(s){I(e,i,"run could not be armed",s).then(r,n);return}let a;try{a=Y({command:e.command,cwd:e.cwd,env:p,timeoutMs:e.run_timeout_s*1e3,headBytes:h})}catch(s){I(e,i,"run failed to spawn",s).then(r,n);return}if(a.pid===null){a.completion.then(s=>{I(e,i,"run failed to spawn",s.processError??new Error("bash did not provide a process id")).then(r,n)},n);return}let c=!1,C=null;const S=m(()=>{a.completion.then(s=>{const f=T(e.cron_id),w=f!==void 0&&f.runId===o&&f.replaced,E=f!==void 0&&f.runId===o&&f.selfCanceled;if(A(e.cron_id,o),c){r(null);return}q(e,{runId:o,outOfBand:d,startedAtIso:l,startedAtMs:u,finishedAtMs:Date.now(),code:s.exitCode,timedOut:s.timedOut,processError:C??s.processError,stdoutHead:_(s.stdout,s.stdoutTruncated),stderrHead:_(s.stderr,s.stderrTruncated),stdoutSha256:s.stdoutSha256,wasReplaced:w,wasSelfCanceled:E}).then(r,n)},n)},"settleCommand");if(!y(e.cron_id,R,a.pid,l,d)){c=!0,a.terminate(),S();return}v(e.cron_id,{runId:o,replaced:!1,selfCanceled:!1,kill:a.terminate}),!d&&e.recur!=null&&(C=J(e,u),C!==null&&a.terminate()),S()})}m(V,"executeCron");function oe(e,t={}){const d=new Date(e).toISOString();U(d),L(e,d);const r=[];for(const n of b(d)){const o=V(n,{outOfBand:!1}).then(()=>{});r.push(o),t.register?.(o)}for(const n of r)n.catch(()=>{});return r}m(oe,"runDueCrons");export{ae as CRON_OUTPUT_HEAD_BYTES,ue as cronDaemonInstanceId,V as executeCron,le as markSelfCanceledCronRun,me as recoverStaleCronLeases,oe as runDueCrons};
|
package/docs/sdk/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `@north-light/crouter-sdk`
|
|
2
2
|
|
|
3
|
-
The client an application installs to drive a crouter daemon: create agent runs, wait for typed results, and reach the rest of the daemon's `/v1` API.
|
|
3
|
+
The ESM-only client an application installs to drive a crouter daemon: create agent runs, watch streamed events, wait for typed results, and reach the rest of the daemon's `/v1` API. Memory is a planned phase-3 surface, not a client method today.
|
|
4
4
|
|
|
5
5
|
One package, one class. `new Crouter()` in Node talks to the owner's unix socket. `new Crouter({ baseURL, token })` in a browser or a remote process talks to the daemon's TCP listener with a bearer token. Resource methods issue `/v1` requests; `createAndWait`, `parse`, and `auth.status` compose more than one request. crtrd stays the sole owner of canvas state.
|
|
6
6
|
|
|
@@ -26,8 +26,10 @@ if (run.kind === 'result') console.log(run.output_parsed.root_cause);
|
|
|
26
26
|
| [Getting started](./getting-started.md) | Install, `new Crouter()` on the owner's socket, `client.auth.status()` and `crtr sys connect` for a browser or remote app, Chrome's local-network prompt |
|
|
27
27
|
| [Client construction](./client.md) | Every constructor option and its environment-variable fallback; per-request options |
|
|
28
28
|
| [Nodes](./nodes.md) | `create` parameters, the outcome union, `parse()` with a zod schema, `waitForOutcome`, `message`, `cancel`, nested resources |
|
|
29
|
-
| [Streaming](./streaming.md) | The event table, `stream()` / `events()`, resume with `after`, `stream_gap` and `stream_dropped`
|
|
29
|
+
| [Streaming](./streaming.md) | The event table, `stream()` / `events()`, the activity helper, resume with `after`, `stream_gap` and `stream_dropped` |
|
|
30
30
|
| [Memory](./memory.md) | The scope object and every `client.memory` method — **phase 3** |
|
|
31
|
+
| [Files](./files.md) | Absolute-path reads, writes, and one-level lists |
|
|
32
|
+
| [Bash](./bash.md) | One bounded command and its output result |
|
|
31
33
|
| [Resource map](./resources.md) | Every namespace with its phase, what is deliberately excluded, and the `client.request()` escape hatch |
|
|
32
34
|
| [Errors](./errors.md) | The error class table and the retry policy |
|
|
33
35
|
| [Docker environment](./docker.md) | `start()`, `attach()`, `connection()` from `@north-light/crouter-env-docker` |
|
|
@@ -35,19 +37,20 @@ if (run.kind === 'result') console.log(run.output_parsed.root_cause);
|
|
|
35
37
|
|
|
36
38
|
## Phases
|
|
37
39
|
|
|
38
|
-
The surface ships in three cuts.
|
|
40
|
+
The surface ships in three cuts. Phase 3 methods do not exist yet.
|
|
39
41
|
|
|
40
42
|
| Phase | What lands | State |
|
|
41
43
|
|---|---|---|
|
|
42
|
-
| **Phase 1** | Client construction and transports; `client.auth.status()`; `client.nodes.create`, `retrieve`, `list`, `outcome`, `waitForOutcome`, `createAndWait`, `parse`, `message`, `cancel`, and `interrupt`; `nodes.reports.list`; `profiles.ensure` and `retrieve`; `system.status` and `health`;
|
|
43
|
-
| **Phase 2** |
|
|
44
|
+
| **Phase 1** | Client construction and transports; `client.auth.status()`; `client.nodes.create`, `retrieve`, `list`, `outcome`, `waitForOutcome`, `createAndWait`, `parse`, `message`, `cancel`, and `interrupt`; `nodes.reports.list`; `profiles.ensure` and `retrieve`; `system.status` and `health`; the error hierarchy; per-request options; `crtr sys connect`; and `env-docker.connection()` | Shipped |
|
|
45
|
+
| **Phase 2 namespaces** | Node lifecycle, job, worktree, and result; canvas and canvas history; crons; human requests and inbox; models; `client.files.read` through `/v1/files/peek`, `write`, and `list`; and `client.bash.run` | Shipped |
|
|
46
|
+
| **Phase 2 streaming** | `GET /v1/nodes/{id}/events`, `nodes.stream(params, options?)`, `nodes.events(id, options?)`, `NodeStream`, and the activity helper | Shipped |
|
|
44
47
|
| **Phase 3** | Memory routes and `client.memory`, plus the review, comment, and chat-inventory namespaces | Not shipped |
|
|
45
48
|
|
|
46
49
|
The approved per-run `scopes` field and `nodes.update` scope support are not in the shipped client or wire declarations, so this reference does not document them as phase 1.
|
|
47
50
|
|
|
48
51
|
## Verification status
|
|
49
52
|
|
|
50
|
-
|
|
53
|
+
The SDK declarations and streaming examples are verified. The remaining verification markers are confined to the phase-3 memory page.
|
|
51
54
|
|
|
52
55
|
## Where the agents read this
|
|
53
56
|
|
package/docs/sdk/bash.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# `client.bash`
|
|
2
|
+
|
|
3
|
+
`client.bash.run` executes one bounded `bash -c` command through the daemon.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
const result = await client.bash.run({
|
|
7
|
+
command: 'printf "%s\\n" "$MESSAGE"',
|
|
8
|
+
cwd: '/absolute/path/to/project',
|
|
9
|
+
env: { MESSAGE: 'hello' },
|
|
10
|
+
timeout_s: 30,
|
|
11
|
+
profile: 'my-app',
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
if (result.exit_code !== 0) console.error(result.stderr);
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`run(params, options?)` returns `Promise<BashRunDTO>`. Its final `options` object is the normal request-options object: `headers`, `signal`, `timeout`, and `maxRetries`.
|
|
18
|
+
|
|
19
|
+
| Result field | Type | Meaning |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| `exit_code` | `number \| null` | Process exit code, or `null` after a timeout. A non-zero code is returned, not thrown. |
|
|
22
|
+
| `signal` | `string \| null` | Signal that ended the process, when one did. |
|
|
23
|
+
| `stdout` / `stderr` | `string` | Captured output prefix. |
|
|
24
|
+
| `stdout_truncated` / `stderr_truncated` | `boolean` | Whether that output stream exceeded its 256 KiB capture limit. |
|
|
25
|
+
| `timed_out` | `boolean` | `true` after the daemon terminates the process group for a timeout. |
|
|
26
|
+
| `duration_ms` | `number` | Command duration in milliseconds. |
|
|
27
|
+
|
|
28
|
+
`cwd` is required, absolute, and must name an existing directory. The daemon never uses its own working directory as a fallback. `timeout_s` defaults to 60, must be greater than 0, and cannot exceed 600. The SDK keeps the HTTP request open for the command timeout plus 15 seconds, unless its configured request timeout is already longer.
|
|
29
|
+
|
|
30
|
+
Pass dynamic values through `env` and reference them from the command as `"$NAME"`, rather than interpolating them into shell source. The command receives the daemon's default-deny operational environment, selected profile environment, then your `env` values.
|
|
31
|
+
|
|
32
|
+
## Authority
|
|
33
|
+
|
|
34
|
+
The token is the owner credential. Anyone holding it can already create an agent with a bash tool, so this command runs with the daemon user's authority and does not provide a restricted command area.
|
package/docs/sdk/client.md
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# Client construction
|
|
2
2
|
|
|
3
|
-
Phase 1.
|
|
4
|
-
|
|
5
3
|
```ts
|
|
6
4
|
import Crouter from '@north-light/crouter-sdk';
|
|
7
5
|
|
|
@@ -22,7 +20,7 @@ const customSocketClient = new Crouter({ socketPath: '/custom/path/crtrd.sock' }
|
|
|
22
20
|
| `timeout` | `number` (ms) | `30_000` | Per-request wall clock. Does not apply to a stream. |
|
|
23
21
|
| `maxRetries` | `number` | `2` | Transient-failure retries. Never applied to `POST` or `PATCH` — see [Errors](./errors.md). |
|
|
24
22
|
| `defaultHeaders` | `Record<string, string>` | `{}` | Merged into every request. |
|
|
25
|
-
| `headers` | `Record<string, string>` | unset | Merged after `defaultHeaders`; a matching name wins. |
|
|
23
|
+
| `headers` | `Record<string, string>` | unset | Merged after `defaultHeaders`; a matching name wins unless `token` is set, in which case the generated `Authorization` header wins at construction. |
|
|
26
24
|
| `fetch` | `typeof fetch` | global `fetch`, or the socket implementation when `socketPath` is used | Transport override, for proxies and instrumentation. |
|
|
27
25
|
| `autostart` | `boolean` | `true` for a socket, `false` for `baseURL` | On a cold socket, run `crtr sys daemon start` and retry once. Node only. |
|
|
28
26
|
|
|
@@ -72,9 +70,9 @@ await client.nodes.retrieve(id, {
|
|
|
72
70
|
| `signal` | `AbortSignal` | Aborts the underlying `fetch`. Raises `APIUserAbortError`. |
|
|
73
71
|
| `timeout` | `number` (ms) | Overrides the client `timeout`. |
|
|
74
72
|
| `maxRetries` | `number` | Overrides the client `maxRetries`. |
|
|
75
|
-
| `headers` | `Record<string, string>` | Merged over `defaultHeaders` and the
|
|
73
|
+
| `headers` | `Record<string, string>` | Merged over `defaultHeaders`, client `headers`, and the token-generated header; a matching name, including `Authorization`, wins. |
|
|
76
74
|
|
|
77
|
-
`signal` is real cancellation. The long-polling helpers (`waitForOutcome`, `createAndWait`, `parse`) honour it between polls and during the in-flight request, and aborting them stops the client — it does not cancel the node. To stop the run itself, call `client.nodes.cancel(id)` or `client.nodes.interrupt(id)`.
|
|
75
|
+
`signal` is real cancellation. The long-polling helpers (`waitForOutcome`, `createAndWait`, `parse`) honour it between polls and during the in-flight request, and aborting them stops the client — it does not cancel the node. To stop the run itself, call `client.nodes.cancel(id)` or `client.nodes.interrupt(id)`. Streams use `NodeEventsOptions`: the same fields except `timeout`, plus `after` for `nodes.events`; see [Streaming](./streaming.md).
|
|
78
76
|
|
|
79
77
|
## What is not on the client
|
|
80
78
|
|
|
@@ -83,10 +81,10 @@ await client.nodes.retrieve(id, {
|
|
|
83
81
|
| `.withResponse()` / `.asResponse()` | They exist to hand back the raw `Response` and an `x-request-id`. The daemon emits no request id, and a caller who needs raw bytes uses `client.request()`. |
|
|
84
82
|
| `local()` and the `Environment` interface | Deleted. The client *is* the connection — `new Crouter()` covers what `local()` did. See [Migration](./migration.md). |
|
|
85
83
|
| `apiKey` | The daemon's credential is a token (`CRTRD_TOKEN`) everywhere in the product. `apiKey` would be a second name for one thing. |
|
|
86
|
-
| Resource-prefixed ids | Node ids have their own format
|
|
84
|
+
| Resource-prefixed ids | Node ids have their own format. The SDK validates unsafe path identifiers locally with `TypeError`, and the daemon validates the request too; nothing is re-prefixed. |
|
|
87
85
|
|
|
88
86
|
## Wire naming
|
|
89
87
|
|
|
90
|
-
Wire fields are `snake_case` — `output_schema`, `root_lifecycle`, `pin_cwd`, `final_report_path`. The SDK does not camelize them. Namespaces and method names are camelCase (`client.nodes.waitForOutcome`, `client.nodes.
|
|
88
|
+
Wire fields are `snake_case` — `output_schema`, `root_lifecycle`, `pin_cwd`, `final_report_path`. The SDK does not camelize them. Namespaces and method names are camelCase (`client.nodes.waitForOutcome`, `client.nodes.worktree`).
|
|
91
89
|
|
|
92
90
|
Timestamps are ISO-8601 strings (`created`, `settled_at`, `finalized_at`, `deadline_at`), not Unix seconds.
|
package/docs/sdk/errors.md
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
# Errors
|
|
2
2
|
|
|
3
|
-
Phase 1.
|
|
4
|
-
|
|
5
3
|
## What throws and what does not
|
|
6
4
|
|
|
7
5
|
**An agent's own outcome is returned, never thrown.** A run that declined the schema, hit its deadline, or crashed comes back as a settled `NodeOutcome` from `waitForOutcome`, `createAndWait`, or `parse`. Narrow on it — see [Nodes](./nodes.md#outcomes).
|
|
8
6
|
|
|
9
|
-
Exceptions are for the layer underneath: the daemon refused the request, the connection failed, or you aborted.
|
|
7
|
+
Exceptions are for the layer underneath: the SDK rejected an invalid local identifier or configuration, the daemon refused the request, the connection failed, the request timed out, or you aborted.
|
|
10
8
|
|
|
11
|
-
## Classes
|
|
9
|
+
## Classes and mappings
|
|
12
10
|
|
|
13
|
-
Every class extends `APIError`, which carries `status`, `code`, `message`, `details`, and `headers`.
|
|
11
|
+
Every class below extends `APIError`, which carries `status`, `code`, `message`, `details`, and `headers`.
|
|
14
12
|
|
|
15
13
|
| Condition | Class |
|
|
16
14
|
|---|---|
|
|
15
|
+
| SDK configuration error | `CrouterError` with `status: 0`, `code: 'crouter_error'` |
|
|
17
16
|
| HTTP 400 | `BadRequestError` |
|
|
18
17
|
| HTTP 401 | `AuthenticationError` |
|
|
19
18
|
| HTTP 403 | `PermissionDeniedError` |
|
|
@@ -21,29 +20,35 @@ Every class extends `APIError`, which carries `status`, `code`, `message`, `deta
|
|
|
21
20
|
| HTTP 409 | `ConflictError` |
|
|
22
21
|
| HTTP 413, 422 | `UnprocessableEntityError` |
|
|
23
22
|
| HTTP 429 | `RateLimitError` |
|
|
24
|
-
| HTTP
|
|
25
|
-
|
|
|
26
|
-
|
|
|
27
|
-
|
|
|
28
|
-
|
|
|
23
|
+
| HTTP 504 or code `request_timeout` | `APIConnectionTimeoutError` (extends `APIConnectionError`) |
|
|
24
|
+
| Other HTTP ≥ 500 | `InternalServerError` |
|
|
25
|
+
| Code `daemon_unavailable`, `transport_error`, `daemon_request_interrupted`, or `daemon_health_unavailable`; or a non-API transport failure | `APIConnectionError` |
|
|
26
|
+
| Code `request_aborted` | `APIUserAbortError` |
|
|
27
|
+
| Any other daemon response | `APIError` |
|
|
28
|
+
|
|
29
|
+
A terminal SSE `error` event other than `stream_gap` rejects `NodeStream.finalOutcome()` and the iterator with `APIError` carrying that event's code, message, and details. `stream_gap` is delivered as an event and the stream continues. See [Streaming](./streaming.md).
|
|
29
30
|
|
|
30
31
|
```ts
|
|
31
32
|
import { ConflictError, NotFoundError, APIConnectionError } from '@north-light/crouter-sdk';
|
|
32
33
|
|
|
33
34
|
try {
|
|
34
35
|
await client.nodes.create({ prompt, node_id: 'my-run-42' });
|
|
35
|
-
} catch (
|
|
36
|
-
if (
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (error instanceof ConflictError && error.code === 'node_id_exists') {
|
|
37
38
|
// the run already exists — attach to it instead of spawning a second one
|
|
38
39
|
return client.nodes.waitForOutcome('my-run-42');
|
|
39
40
|
}
|
|
40
|
-
if (
|
|
41
|
-
if (
|
|
42
|
-
throw
|
|
41
|
+
if (error instanceof NotFoundError) throw new Error('no such node');
|
|
42
|
+
if (error instanceof APIConnectionError) throw new Error('the daemon is not reachable');
|
|
43
|
+
throw error;
|
|
43
44
|
}
|
|
44
45
|
```
|
|
45
46
|
|
|
46
|
-
The subclasses add **no fields**. `status` and `code` on the base class already decide everything there is to branch on. They exist so that `catch (
|
|
47
|
+
The subclasses add **no fields**. `status` and `code` on the base class already decide everything there is to branch on. They exist so that `catch (error) { if (error instanceof NotFoundError) … }` — what an OpenAI SDK user writes without thinking about it — works here too.
|
|
48
|
+
|
|
49
|
+
## Identifier validation
|
|
50
|
+
|
|
51
|
+
The SDK validates path-segment identifiers before it makes a request. Invalid node ids, cron ids, bash-job ids, human-request ids, inbox ticket ids, provider names, and profile names throw `TypeError` locally. Node ids apply to core node calls, lifecycle calls, and nested node resources. `nodes.events()` returns a `NodeStream` synchronously, so its invalid-id `TypeError` rejects `stream.node`, `stream.finalOutcome()`, and iteration instead. File paths are not identifiers; invalid or relative file paths reach the daemon and return its mapped API error. `nodes.outcome(id, { wait })` separately throws `RangeError` when `wait` is not an integer from 0 through 25.
|
|
47
52
|
|
|
48
53
|
## The wire body
|
|
49
54
|
|
|
@@ -71,8 +76,8 @@ const runId = `invoice-${invoice.id}`;
|
|
|
71
76
|
|
|
72
77
|
try {
|
|
73
78
|
await client.nodes.create({ prompt, node_id: runId, root: true });
|
|
74
|
-
} catch (
|
|
75
|
-
if (!(
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (!(error instanceof ConflictError && error.code === 'node_id_exists')) throw error;
|
|
76
81
|
}
|
|
77
82
|
|
|
78
83
|
const outcome = await client.nodes.waitForOutcome(runId);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# `client.files`
|
|
2
|
+
|
|
3
|
+
`client.files` reads, writes, and lists host files through the daemon.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
const source = await client.files.read('/absolute/path/to/input.txt');
|
|
7
|
+
const written = await client.files.write('/absolute/path/to/output.txt', source.content);
|
|
8
|
+
const directory = await client.files.list('/absolute/path/to');
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Every `path` is absolute. Relative paths and `~` are not expanded. File paths are sent to the daemon unchanged; the daemon checks that they are absolute and returns a mapped API error when they are not.
|
|
12
|
+
|
|
13
|
+
Every method accepts ordinary request options in its final object: `headers`, `signal`, `timeout`, and `maxRetries`. `read` and `write` add `encoding`; `list` adds `limit`.
|
|
14
|
+
|
|
15
|
+
## Read
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
await client.files.read(path, { encoding: 'utf8', timeout: 5_000 });
|
|
19
|
+
await client.files.read(path, { encoding: 'base64' });
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`read(path, options?)` returns `Promise<FilePeekDTO>`, `{ path, content, truncated }`, through `GET /v1/files/peek`. It captures at most 512 KiB of file bytes. `truncated: true` means `content` is only the prefix. Request `base64` for binary content.
|
|
23
|
+
|
|
24
|
+
## Write
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
const result = await client.files.write(path, content, { encoding: 'utf8' });
|
|
28
|
+
// result is { path, bytes_written }
|
|
29
|
+
await client.files.write(path, bytes.toString('base64'), { encoding: 'base64' });
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`write(path, content, options?)` returns `Promise<FileWriteDTO>`, `{ path, bytes_written }`. Writes create parent directories and atomically replace the target. If `path` is an existing symlink, the daemon resolves it and atomically replaces its destination; the symlink remains. Decoded content above 1 MiB is refused with `UnprocessableEntityError` carrying `status: 413` and `code: 'file_too_large'`; it is never truncated or partially written.
|
|
33
|
+
|
|
34
|
+
## List
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
const result = await client.files.list(path, { limit: 100 });
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`list(path, options?)` returns `Promise<FileListDTO>`, `{ path, entries, truncated }`, for one directory level. Each entry has `name`, `type: 'file' | 'dir' | 'other'`, `size`, and ISO-8601 `modified`. `limit` must be a positive integer; the default is 1000. `truncated: true` means the directory had more entries than the requested limit.
|
|
41
|
+
|
|
42
|
+
## Authority
|
|
43
|
+
|
|
44
|
+
The token is the owner credential. Anyone holding it can already create an agent with a bash tool, so these methods run with the daemon user's authority and do not provide a restricted file area.
|
|
@@ -156,5 +156,5 @@ An outcome is returned, never thrown — including a decline and a failure. Only
|
|
|
156
156
|
- Every constructor option and environment-variable fallback: [Client construction](./client.md)
|
|
157
157
|
- Checking the daemon connection and selected provider before a run: `client.auth.status()` above
|
|
158
158
|
- The full create-parameter table and the outcome union: [Nodes](./nodes.md)
|
|
159
|
-
- Watching a run as it works: [Streaming](./streaming.md)
|
|
159
|
+
- Watching a run as it works: [Streaming](./streaming.md)
|
|
160
160
|
- Running the daemon in a container instead: [Docker environment](./docker.md)
|