@north-light/cli 0.3.414 → 0.3.415

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.
Files changed (88) hide show
  1. package/dist/api/client.d.ts +20 -0
  2. package/dist/api/client.js +3 -3
  3. package/dist/api/dto/common.d.ts +1 -1
  4. package/dist/api/dto/crons.d.ts +7 -1
  5. package/dist/api/dto/human-requests.d.ts +1 -1
  6. package/dist/api/dto/run-events.d.ts +1 -1
  7. package/dist/api/dto/usage.d.ts +27 -0
  8. package/dist/api/dto/usage.js +0 -0
  9. package/dist/api/errors.js +1 -1
  10. package/dist/api/index.d.ts +3 -2
  11. package/dist/api/index.js +1 -1
  12. package/dist/builtin-memory/crouter-sdk/README.md +22 -4
  13. package/dist/builtin-memory/crouter-sdk/client.md +32 -1
  14. package/dist/builtin-memory/crouter-sdk/errors.md +3 -0
  15. package/dist/builtin-memory/crouter-sdk/getting-started.md +26 -4
  16. package/dist/builtin-memory/crouter-sdk/release-notes.md +23 -0
  17. package/dist/builtin-memory/crouter-sdk/testing.md +9 -3
  18. package/dist/builtin-memory/internal/nodes-and-canvas.md +4 -0
  19. package/dist/clients/attach/viewer.js +606 -606
  20. package/dist/commands/cron.js +1 -1
  21. package/dist/commands/node/create.js +1 -1
  22. package/dist/core/canvas/crons.d.ts +15 -1
  23. package/dist/core/canvas/crons.js +13 -13
  24. package/dist/core/canvas/delivery-contract.d.ts +1 -1
  25. package/dist/core/canvas/migrations.js +73 -66
  26. package/dist/core/canvas/types.d.ts +1 -1
  27. package/dist/core/command.d.ts +7 -0
  28. package/dist/core/command.js +9 -9
  29. package/dist/core/grants/mirror.d.ts +4 -0
  30. package/dist/core/grants/mirror.js +2 -2
  31. package/dist/core/layout.d.ts +2 -0
  32. package/dist/core/layout.js +2 -2
  33. package/dist/core/llm/run.d.ts +5 -1
  34. package/dist/core/llm/run.js +3 -3
  35. package/dist/core/providers/call.d.ts +2 -0
  36. package/dist/core/providers/call.js +1 -1
  37. package/dist/core/providers/errors.js +1 -1
  38. package/dist/core/providers/receipts.d.ts +2 -0
  39. package/dist/core/providers/receipts.js +2 -3
  40. package/dist/core/runs/operations.d.ts +2 -0
  41. package/dist/core/runs/operations.js +4 -4
  42. package/dist/core/runtime/broker/model-adapter.js +2 -2
  43. package/dist/core/runtime/run-command.d.ts +6 -0
  44. package/dist/core/runtime/run-command.js +1 -1
  45. package/dist/core/spaces/mounts.d.ts +2 -0
  46. package/dist/core/spaces/mounts.js +1 -1
  47. package/dist/core/spaces/process.d.ts +14 -2
  48. package/dist/core/spaces/process.js +1 -1
  49. package/dist/core/usage/ledger.d.ts +49 -0
  50. package/dist/core/usage/ledger.js +10 -0
  51. package/dist/daemon/api/app-listener.js +12 -12
  52. package/dist/daemon/api/handlers/crons.js +1 -1
  53. package/dist/daemon/api/handlers/llm.d.ts +8 -0
  54. package/dist/daemon/api/handlers/llm.js +3 -3
  55. package/dist/daemon/api/map.d.ts +1 -1
  56. package/dist/daemon/api/map.js +1 -1
  57. package/dist/daemon/api/operations.js +1 -1
  58. package/dist/daemon/cron/armed-context.d.ts +21 -4
  59. package/dist/daemon/cron/armed-context.js +1 -1
  60. package/dist/daemon/cron/authority.d.ts +27 -0
  61. package/dist/daemon/cron/authority.js +1 -0
  62. package/dist/daemon/cron/disposition.d.ts +2 -0
  63. package/dist/daemon/cron/disposition.js +1 -1
  64. package/dist/daemon/cron/escalation.js +3 -3
  65. package/dist/daemon/cron/folders.d.ts +5 -0
  66. package/dist/daemon/cron/folders.js +1 -0
  67. package/dist/daemon/cron/lease-recovery.js +1 -1
  68. package/dist/daemon/cron/settle.d.ts +10 -0
  69. package/dist/daemon/cron/settle.js +1 -1
  70. package/dist/daemon/cron/sinks.d.ts +2 -1
  71. package/dist/daemon/cron/sinks.js +2 -2
  72. package/dist/daemon/cron-run.js +1 -1
  73. package/dist/daemon/reconcilers/action-lane.js +1 -1
  74. package/dist/daemon/reconcilers/broker-supervision.d.ts +10 -0
  75. package/dist/daemon/reconcilers/broker-supervision.js +4 -4
  76. package/dist/daemon/reconcilers/cron-lane.d.ts +1 -0
  77. package/dist/daemon/reconcilers/cron-lane.js +1 -1
  78. package/docs/sdk/README.md +22 -4
  79. package/docs/sdk/client.md +32 -1
  80. package/docs/sdk/errors.md +3 -0
  81. package/docs/sdk/getting-started.md +26 -4
  82. package/docs/sdk/release-notes.md +23 -0
  83. package/docs/sdk/testing.md +9 -3
  84. package/package.json +2 -2
  85. package/packages/crouter-identity/dist/index.d.ts +29 -2
  86. package/packages/crouter-identity/dist/index.js +81 -12
  87. package/packages/crouter-identity/package.json +1 -1
  88. package/runtime.lock.json +12 -12
@@ -1,3 +1,4 @@
1
+ import { ApiError } from './errors.js';
1
2
  import type { AppLimitsChangeDTO, LimitsDTO, SetAppLimitsRequest } from './dto/limits.js';
2
3
  import type { DaemonAdmitDTO, DaemonRestartDTO, DueDTO, HealthDTO, MigrateStateDTO, MigrateStateRequest, StatusDTO } from './dto/health.js';
3
4
  import type { BashJobStatusDTO, BashJobStopResultDTO } from './dto/bash-jobs.js';
@@ -88,7 +89,18 @@ export interface CrtrRequestOptions {
88
89
  * interrupted-response recovery — to one wall-clock budget, without changing
89
90
  * the client's default inactivity timeout. */
90
91
  budgetMs?: number;
92
+ /** The same limit as an absolute instant (epoch milliseconds), for a caller that already holds one deadline
93
+ * for a larger operation and passes it to every call inside it. With `budgetMs` too, the earlier wins. */
94
+ deadlineAt?: number;
91
95
  }
96
+ /** Why a call stopped at its deadline: `elapsed` — the time ran out; `retry_after` — the server asked for a wait
97
+ * longer than the time left, so the call stopped without sleeping. */
98
+ export type DeadlineReason = 'elapsed' | 'retry_after';
99
+ /** The one error a call raises when its deadline passes (or a wait would pass it): status 504, code `deadline_exceeded`,
100
+ * `details: { deadline_at, reason, retry_after_s? }`. */
101
+ /** The longest delay a timer accepts (2^31-1 ms, about 24.8 days); a longer deadline would fire at once. */
102
+ export declare const MAX_TIMER_MS = 2147483647;
103
+ export declare function deadlineExceeded(deadlineAt: number, reason?: DeadlineReason, retryAfterS?: number): ApiError;
92
104
  export declare class CrtrClient {
93
105
  private readonly baseUrl;
94
106
  private readonly fetch;
@@ -498,4 +510,12 @@ export declare class CrtrClient {
498
510
  * managed daemons still get the same bounded readiness window. */
499
511
  private handleColdSocket;
500
512
  }
513
+ /** One retry wait inside a call's deadline. A server-requested wait (`retryAfterS`) is honored only against a deadline:
514
+ * when it would not finish before the deadline the call fails now with `deadline_exceeded` (`reason: 'retry_after'`)
515
+ * instead of sleeping past it; a call without a deadline keeps the plain backoff. The sleep ends early, with the same
516
+ * error, if the caller aborts. */
517
+ export declare function sleepWithin(backoffMs: number, opts: {
518
+ deadlineAt?: number;
519
+ signal?: AbortSignal;
520
+ }, retryAfterS?: number, cause?: unknown): Promise<void>;
501
521
  export declare function safeColdStartDiagnostic(hook: (() => string | undefined) | undefined): string | undefined;
@@ -1,3 +1,3 @@
1
- var k=Object.defineProperty;var u=(n,e)=>k(n,"name",{value:e,configurable:!0});import{ApiError as m,isErrorBody as C}from"./errors.js";import{routes as r}from"./routes.js";import{isSafeNodeId as p}from"./dto/common.js";import{isSafeBashJobId as G}from"./dto/bash-jobs.js";import{isSafeCronId as A}from"./dto/crons.js";const M=3e4,N=Symbol.for("@north-light/api/local-socket-fetch"),_=1e4,D=200;async function x({windowMs:n,probe:e,initialError:t,pollIntervalMs:s=D,retry:i=u(()=>!0,"retry"),now:o=Date.now,sleep:h=y}){const d=o()+n;let c=t;for(;;){const f=d-o();if(f<=0)throw c??new Error("crtrd availability window expired without a probe failure");try{await e(f);return}catch(P){if(c=P,!i(P))throw P}const l=d-o();if(l<=0)throw c;await h(Math.min(s,l))}}u(x,"waitForDaemonAvailability");const E=u(()=>Object.assign(new Error("request budget exhausted"),{code:"ETIMEDOUT"}),"budgetExhausted");class W{static{u(this,"CrtrClient")}baseUrl;fetch;headers;autostart;timeoutMs;maxRetries;localSocketTransport;onColdSocket;coldStartDiagnostic;coldStartFailure;coldStartPollWindowMs;startupFailureReported=!1;coldStartAttempted=!1;constructor(e){if(e.baseUrl==="")throw new TypeError("CrtrClient requires baseUrl");this.baseUrl=new URL(e.baseUrl),this.fetch=e.fetch??globalThis.fetch,this.headers={...e.headers},this.autostart=e.autostart??!1,this.timeoutMs=e.timeoutMs??M,this.maxRetries=e.maxRetries??2,this.localSocketTransport=this.fetch[N]===!0,e.onColdSocket!==void 0&&(this.onColdSocket=e.onColdSocket),e.coldStartDiagnostic!==void 0&&(this.coldStartDiagnostic=e.coldStartDiagnostic),e.coldStartFailure!==void 0&&(this.coldStartFailure=e.coldStartFailure),this.coldStartPollWindowMs=e.coldStartPollWindowMs??_}healthz(){return this.request("GET",r.healthz())}async probeHealthz(e){const t=await this.transport("GET",r.healthz(),void 0,{timeout:e});if(t.status===503)try{const s=await t.clone().json();if(typeof s.startup_blocked=="string")return s}catch{}return w(t)}status(){return this.request("GET",r.status())}due(){return this.request("GET",r.due())}restartDaemon(){return this.request("POST",r.daemonRestart())}admitDaemon(){return this.request("POST",r.daemonAdmit())}migrateState(e){return this.request("POST",r.daemonMigrate(),e)}appendNodeLog(e,t){return this.request("POST",r.nodeLog(this.nodePath(e)),t)}async putNodeTelemetry(e,t){await this.request("PUT",r.nodeTelemetry(this.nodePath(e)),t)}async putNodeRecap(e,t){await this.request("PUT",r.nodeRecap(this.nodePath(e)),t)}readNodeInboxReport(e,t,s){return this.request("GET",a(r.nodeInboxReport(this.nodePath(e)),{ref:t,execution_id:s}))}readNodePassiveMessage(e,t){return this.request("GET",a(r.nodePassiveMessage(this.nodePath(e)),{ref:t}))}nodePushedFinal(e,t){return this.request("POST",r.nodePushedFinal(this.nodePath(e)),t)}createNode(e){return this.request("POST",r.nodes(),e)}listNodes(e){return this.request("GET",a(r.nodes(),e))}getNode(e){return this.request("GET",r.node(this.nodePath(e)))}getNodeOutcome(e,{waitSeconds:t}={}){if(t!==void 0&&(!Number.isInteger(t)||t<0||t>25))throw new RangeError("waitSeconds must be an integer between 0 and 25");return this.request("GET",a(r.nodeOutcome(this.nodePath(e)),{wait:t}),void 0,{timeout:(t??0)*1e3+this.timeoutMs})}getNodeEvents(e,t={},s={}){if(t.after!==void 0&&(!Number.isSafeInteger(t.after)||t.after<0))throw new RangeError("after must be a non-negative safe integer");return this.transport("GET",a(r.nodeEvents(this.nodePath(e)),t),void 0,{...s,headers:{accept:"text/event-stream",...s.headers??{}},timeout:0})}async*getBrokerSignals(e,t,s={}){const i=await this.send("GET",a(r.nodeBrokerSignals(this.nodePath(e)),{execution_id:t}),void 0,{...s,headers:{accept:"application/x-ndjson",...s.headers??{}},timeout:0});if(!i.ok){await w(i);return}if(i.body===null)throw new m(i.status,"invalid_response","broker signal stream has no body");const o=i.body.getReader(),h=new TextDecoder;let d="";try{for(;;){const{done:c,value:f}=await o.read();if(c)break;d+=h.decode(f,{stream:!0});let l;for(;(l=d.indexOf(`
2
- `))!==-1;){const P=d.slice(0,l);d=d.slice(l+1),P!==""&&(yield JSON.parse(P))}}if(d+=h.decode(),d.trim()!=="")throw new m(502,"invalid_response","broker signal stream ended mid-line")}finally{await o.cancel().catch(()=>{})}}createStructuredRequest(e,t){return e.mode==="sync"?this.sendOnce("POST","/v1/structured-requests",e,t):this.request("POST","/v1/structured-requests",e,t)}getStructuredRequest(e,{wait:t=0,budgetMs:s}={}){if(!Number.isInteger(t)||t<0||t>25)throw new RangeError("wait must be an integer between 0 and 25");return this.request("GET",a(`/v1/structured-requests/${this.requestPath(e)}`,{wait:t}),void 0,{budgetMs:s})}cancelStructuredRequest(e,t){return this.request("POST",`/v1/structured-requests/${this.requestPath(e)}/cancel`,{},t)}registerOutcomeDelivery(e,t){return this.request("PUT",r.nodeOutcomeDelivery(this.nodePath(e)),t)}getOutcomeDelivery(e){return this.request("GET",r.nodeOutcomeDelivery(this.nodePath(e)))}async disarmOutcomeDelivery(e){await this.request("DELETE",r.nodeOutcomeDelivery(this.nodePath(e)))}listBashJobs(e){return this.request("GET",r.nodeJobs(this.nodePath(e)))}stopBashJob(e,t){return this.request("DELETE",r.nodeJob(this.nodePath(e),this.jobPath(t)))}sendMessage(e,t){return this.request("POST",r.nodeMessages(this.nodePath(e)),t)}interruptNode(e){return this.request("POST",r.nodeInterrupt(this.nodePath(e)),{})}pushReport(e,t){return this.request("POST",r.nodeReports(this.nodePath(e)),t)}submitResult(e,t){return this.request("POST",r.nodeResult(this.nodePath(e)),t)}forkNode(e){return this.request("POST",r.nodeFork(this.nodePath(e)),{})}reviveNode(e,t){return this.request("POST",r.nodeRevive(this.nodePath(e)),t??{})}relaunchRoot(e){return this.request("POST",r.nodeRelaunchRoot(this.nodePath(e)),{})}bindBrokerSession(e,t){return this.request("POST",r.nodeBrokerSessionBound(this.nodePath(e)),t)}settleBroker(e,t){return this.request("POST",r.nodeBrokerSettle(this.nodePath(e)),t)}completeBrokerPark(e,t){return this.request("POST",r.nodeBrokerParkComplete(this.nodePath(e)),t)}recordBrokerParkActivity(e,t){return this.request("POST",r.nodeBrokerParkActivity(this.nodePath(e)),t)}async recordBrokerTelemetry(e,t){await this.request("POST",r.nodeBrokerTelemetry(this.nodePath(e)),t)}claimNodeMail(e,t){return this.request("POST",r.nodeMailClaim(this.nodePath(e)),t)}acknowledgeNodeMail(e,t){return this.request("POST",r.nodeMailAcknowledge(this.nodePath(e)),t)}recordBrokerTurn(e,t){return this.request("POST",r.brokerTurn(this.nodePath(e)),t)}mutateBrokerProviderRetry(e,t){return this.request("POST",r.brokerProviderRetry(this.nodePath(e)),t)}mutateBrokerFault(e,t){return this.request("POST",r.brokerFault(this.nodePath(e)),t)}getBrokerRecovery(e,t){return this.request("GET",`${r.brokerRecovery(this.nodePath(e))}?expected_execution_id=${encodeURIComponent(t)}`)}recordNodeFault(e,t){return this.request("POST",r.nodeFault(this.nodePath(e)),t)}clearNodeFault(e,t={}){const s=new URLSearchParams;t.link!==void 0&&s.set("link",t.link),t.preserve_episode!==void 0&&s.set("preserve_episode",String(t.preserve_episode));const i=s.size===0?"":`?${s.toString()}`;return this.request("DELETE",`${r.nodeFault(this.nodePath(e))}${i}`)}commitBrokerModel(e,t){return this.request("POST",r.nodeBrokerModel(this.nodePath(e)),t)}brokerExtensionState(e){return this.request("GET",r.nodeBrokerExtensionState(this.nodePath(e)))}commitBrokerGeneratedName(e,t){return this.request("POST",r.nodeBrokerGeneratedName(this.nodePath(e)),t)}commitBrokerPersonaAck(e,t){return this.request("POST",r.nodeBrokerPersonaAck(this.nodePath(e)),t)}closeNode(e,t){return this.request("POST",r.nodeClose(this.nodePath(e)),t??{})}recycleNode(e){return this.request("POST",r.nodeRecycle(this.nodePath(e)),{})}demoteNode(e){return this.request("POST",r.nodeDemote(this.nodePath(e)),{})}promoteNode(e,t){return this.request("POST",r.nodePromote(this.nodePath(e)),t)}yieldNode(e,t){return this.request("POST",r.nodeYield(this.nodePath(e)),t)}waitNode(e,t){return this.request("POST",r.nodeWait(this.nodePath(e)),t)}listKinds(){return this.request("GET",r.kinds())}patchConfig(e,t){return this.request("PATCH",r.nodeConfig(this.nodePath(e)),t,t.model!==void 0?{timeout:45e3+this.timeoutMs}:void 0)}closeWorktree(e){return this.request("POST",r.nodeWorktreeClose(this.nodePath(e)),{})}abandonWorktree(e,t){return this.request("POST",r.nodeWorktreeAbandon(this.nodePath(e)),t)}retryDelivery(e){return this.request("POST",r.deliveryRetry(e),{})}listQuarantinedWorktrees(){return this.request("GET",r.quarantinedWorktrees())}showLimits(){return this.request("GET",r.limits())}setAppLimits(e,t){return this.request("PUT",r.limitsApp(e),t)}removeAppLimits(e,t){return this.request("DELETE",a(r.limitsApp(e),{fields:t.join(",")}))}listSpaces(){return this.request("GET",r.spaces())}subscribe(e,t){return this.request("POST",r.nodeSubscriptions(this.nodePath(e)),t)}listFocuses(){return this.request("GET",r.focuses())}focusOf(e){return this.request("GET",a(r.focusByNode(),{node_id:e}))}focusByPane(e){return this.request("GET",a(r.focusByPane(),{pane:e}))}registerFocus(e){return this.request("POST",r.focuses(),e)}async setFocusPane(e,t){await this.request("PATCH",r.focus(e),t)}async closeFocus(e){await this.request("DELETE",r.focus(e))}async unsubscribe(e,t){await this.request("DELETE",r.nodeSubscription(this.nodePath(e),this.nodePath(t)))}armCron(e){return this.request("POST",r.crons(),e)}listCrons(e){return this.request("GET",a(r.crons(),e))}showCron(e,t){return this.request("GET",a(r.cron(this.cronPath(e)),t))}pauseCron(e,t){return this.request("POST",a(r.cronPause(this.cronPath(e)),t),{})}resumeCron(e,t){return this.request("POST",a(r.cronResume(this.cronPath(e)),t),{})}runCron(e,t){return this.request("POST",a(r.cronRun(this.cronPath(e)),t),{})}async cancelCron(e,t){await this.request("DELETE",a(r.cron(this.cronPath(e)),t))}pokeCrons(){return this.request("POST",r.cronsPoke(),{})}ensureAttach(e,t){return this.request("POST",r.nodeAttach(this.nodePath(e)),t??{})}getReports(e,t){return this.request("GET",a(r.nodeReports(this.nodePath(e)),t))}getTranscript(e,t){return this.request("GET",a(r.nodeTranscript(this.nodePath(e)),t))}getSnapshot(e){return this.request("GET",r.nodeSnapshot(this.nodePath(e)))}nodeSubject(e){return this.request("GET",r.nodeSubject(this.nodePath(e)))}getNodeMessages(e,t){return this.request("GET",a(r.nodeMessages(this.nodePath(e)),t))}getSession(e){return this.request("GET",r.nodeSession(this.nodePath(e)))}getChatInventory(e){return this.request("GET",r.nodeChatInventory(this.nodePath(e)))}getProspectiveChatInventory(e){return this.request("GET",a(r.prospectiveChatInventory(),e))}getContext(e){return this.request("GET",r.nodeContext(this.nodePath(e)))}peekFile(e,t,s){return this.request("GET",a(r.filePeek(),{path:e,encoding:t}),void 0,s)}writeFile(e,t,s,i){return this.request("POST",r.fileWrite(),{path:e,content:t,encoding:s},i)}listFiles(e,t,s){return this.request("GET",a(r.fileList(),{path:e,limit:t}),void 0,s)}runBash(e,t){return this.request("POST",r.bash(),e,t)}objects={read:u((e,t)=>this.request("GET",a(r.object(e),t)),"read"),list:u(e=>this.request("GET",a(r.objects(),e)),"list"),search:u(e=>this.request("POST",r.objectsSearch(),e),"search"),watch:u((e,t={})=>this.request("POST",r.objectWatch(e),t),"watch"),unwatch:u((e,t)=>this.request("DELETE",a(r.objectWatch(e),t)),"unwatch"),edges:u((e,t)=>this.request("GET",a(r.objectEdges(e),t)),"edges")};docs={write:u(e=>this.request("POST",r.docs(),e),"write"),edit:u((e,t)=>this.request("PATCH",r.doc(e),t),"edit"),move:u((e,t)=>this.request("POST",r.docMove(e),t),"move"),delete:u(e=>this.request("DELETE",r.doc(e)),"delete"),history:u((e,t)=>this.request("GET",a(r.docHistory(e),t)),"history"),lint:u((e={})=>this.request("POST",r.docsLint(),e),"lint")};documents={reconcilePackages:u(()=>this.request("POST",r.documentsReconcilePackages()),"reconcilePackages")};nodeDelivery(e,t){return this.request("POST",r.nodeDelivery(this.nodePath(e)),t)}deliveryDryRun(e){return this.request("POST",r.deliveryDryRun(),e)}recordBackgroundedJob(e,t,s){return this.request("POST",r.nodeJobBackground(this.nodePath(e),this.jobPath(t)),s)}ensureProfile(e,t){return this.request("PUT",r.profile(e),t??{})}createProfile(e){return this.request("POST",r.profiles(),e)}listProfiles(){return this.request("GET",r.profiles())}updateProfile(e,t){return this.request("PATCH",r.profile(e),t)}listAppEnv(){return this.request("GET",r.appEnv())}setAppEnv(e,t){return this.request("PUT",r.appEnvVar(e),{value:t})}removeAppEnv(e){return this.request("DELETE",r.appEnvVar(e))}listProfileEnv(e){return this.request("GET",r.profileEnv(e))}setProfileEnv(e,t,s){return this.request("PUT",r.profileEnvVar(e,t),{value:s})}removeProfileEnv(e,t){return this.request("DELETE",r.profileEnvVar(e,t))}getProfile(e){return this.request("GET",r.profile(e))}pauseProfile(e){return this.request("POST",r.profilePause(e),{})}resumeProfile(e){return this.request("POST",r.profileResume(e),{})}updateProfileMetadata(e,t){return this.request("PATCH",r.profileMetadata(e),t)}deleteProfile(e,t){return this.request("DELETE",r.profile(e),t)}listModelAuth(){return this.request("GET",r.modelAuths())}getModelAuthReadiness(e){return this.request("GET",a(r.modelAuthReadiness(),e))}installCredential(e,t){return this.request("PUT",r.modelAuth(e),t)}removeCredential(e){return this.request("DELETE",r.modelAuth(e))}startModelAuth(e){return this.request("POST",r.modelAuthFlows(),e,{timeout:3e4+this.timeoutMs})}completeModelAuth(e,t){return this.request("POST",r.modelAuthFlowComplete(e),t,{timeout:35e3+this.timeoutMs})}cancelModelAuth(e){return this.request("POST",r.modelAuthFlowCancel(e),{})}getModelAuthFlow(e,t){return this.request("GET",a(r.modelAuthFlow(e),{wait:t}),void 0,{timeout:(t??0)*1e3+this.timeoutMs})}createReview(e){return this.request("POST",r.humanReviews(),e)}listReviews(e){return this.request("GET",a(r.humanReviews(),e))}getReview(e){return this.request("GET",r.humanReview(this.reviewPath(e)))}submitReview(e){return this.request("POST",r.humanReviewSubmit(this.reviewPath(e)),{})}cancelReview(e,t={}){return this.request("POST",r.humanReviewCancel(this.reviewPath(e)),t)}getReviewDocumentBase(e){return this.request("GET",r.humanReviewDocument(this.reviewPath(e)))}createReviewComment(e,t){const s=e===void 0?"self":this.reviewPath(e);return this.request("POST",r.humanReviewComments(s),t)}listReviewComments(e,t){const s=e===void 0?"self":this.reviewPath(e);return this.request("GET",a(r.humanReviewComments(s),t))}readReviewCommentEvents(e,t){return this.request("GET",a(r.humanReviewCommentEvents(this.reviewPath(e)),t))}updateReviewCommentRanges(e,t){return this.request("POST",r.humanReviewCommentRanges(this.reviewPath(e)),t)}getReviewComment(e){return this.request("GET",r.humanComment(this.commentPath(e)))}editReviewComment(e,t){return this.request("POST",r.humanCommentEdit(this.commentPath(e)),t)}resolveReviewComment(e,t={}){return this.request("POST",r.humanCommentResolve(this.commentPath(e)),t)}reopenReviewComment(e,t={}){return this.request("POST",r.humanCommentReopen(this.commentPath(e)),t)}deleteReviewComment(e,t={}){return this.request("POST",r.humanCommentDelete(this.commentPath(e)),t)}forkReviewComment(e){return this.request("POST",r.humanCommentFork(this.commentPath(e)),{})}listHumanInbox(){return this.request("GET",r.humanInbox())}getHumanInboxPage(e){return this.request("GET",r.humanInboxTicket(this.ticketId(e)))}getInboxHistory(e){if(e===void 0)return this.request("GET",r.humanInbox()+"/history");if(typeof e!="string"||e==="")throw new TypeError(`invalid node id: ${JSON.stringify(e)}`);return this.request("GET",a(r.humanInbox()+"/history",{node_id:e}))}respondHumanInboxPage(e,t){return this.request("POST",r.humanInboxRespond(this.ticketId(e)),t)}postInboxProgress(e,t){return this.request("POST",r.humanInboxProgress(this.ticketId(e)),{responses:t})}getInboxResponse(e){return this.request("GET",r.humanInboxResponse(this.ticketId(e)))}cancelHumanInboxTicket(e,t){return this.request("POST",r.humanInboxCancel(this.ticketId(e)),t??{})}createHumanRequest(e){return this.request("POST",r.humanRequests(),e)}listPageComponents(){return this.request("GET",r.humanComponents())}getHumanRequest(e){return this.request("GET",r.humanRequest(this.requestPath(e)))}replaceHumanRequest(e,t){return this.request("POST",r.humanRequestReplace(this.requestPath(e)),t)}respondHumanRequest(e,t){return this.request("POST",r.humanRequestRespond(this.requestPath(e)),t)}dismissHumanRequest(e,t={}){return this.request("POST",r.humanRequestDismiss(this.requestPath(e)),t)}cancelHumanRequest(e,t={}){return this.request("POST",r.humanRequestCancel(this.requestPath(e)),t)}resolvePageFeedbackComment(e,t,s){if(!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(t))throw new TypeError(`invalid feedback comment id: ${JSON.stringify(t)}`);return this.request("POST",r.humanInboxFeedbackResolve(this.ticketId(e),t),{node_id:s})}async dashboard(e){const t=e?.under!==void 0?{under:e.under}:void 0,[s,i]=await Promise.all([this.listNodes(t),this.status()]);return{nodes:s,counts:i.node_counts,generated_at:new Date().toISOString()}}attention(){return this.request("GET",r.canvasAttention())}attentionCounts(e){const t={node_ids:e};return this.request("POST",r.canvasAttentionCounts(),t)}historySearch(e){return this.request("POST",r.canvasHistorySearch(),e)}historyGrep(e){return this.request("POST",r.canvasHistoryGrep(),e)}historyRead(e){return this.request("GET",a(r.canvasHistoryRead(),e))}historyStats(e){return this.request("POST",r.canvasHistoryStats(),e)}canvasSnapshot(){return this.request("GET",r.canvasSnapshot())}analytics(e){return this.request("GET",a(r.canvasAnalytics(),e),void 0,{timeout:6e4})}async recordMemoryReads(e,t){await this.request("POST",r.nodeMemoryReads(this.nodePath(e)),t)}canvasRoster(){return this.request("GET",r.canvasRoster())}async canvasBrowseCompressed(){const e=await this.send("GET",r.canvasBrowse());return e.ok||await w(e),new Uint8Array(await e.arrayBuffer())}canvasGraph(){return this.request("GET",r.canvasGraph())}prune(e){return this.request("POST",r.canvasPrune(),e)}async request(e,t,s,i){try{return await w(await this.send(e,t,s,i))}catch(o){throw T(o,i?.signal)}}async rawResponse(e,t,s){try{return await this.send(e,t,void 0,s)}catch(i){throw T(i,s?.signal)}}async send(e,t,s,i={}){const o=this.resolveBudget(i),h=e==="GET"||e==="HEAD"||e==="DELETE",d=o.maxRetries??this.maxRetries;for(let c=0;;c++){let f;try{f=await this.transport(e,t,s,o)}catch(l){if(this.isColdSocketError(l))try{await this.handleColdSocket(l,o.deadlineAt),f=await this.transport(e,t,s,o)}catch(P){throw T(P,o.signal)}else{if(this.isInterruptedSocketError(l)&&this.localSocketTransport&&(!h||c<d))return await this.rideOutInterruptedRequest(e,t,s,o);if(h&&c<d&&!I(l,o.signal)&&(!this.localSocketTransport||!this.isInterruptedSocketError(l))){await y(R(c+1));continue}else throw T(l,o.signal)}}if(h&&c<d&&B(f.status)){await b(f),await y(R(c+1));continue}return f}}resolveBudget(e){const{budgetMs:t,...s}=e;if(t===void 0)return s;if(!Number.isFinite(t)||t<=0)throw new RangeError("budgetMs must be a positive finite number");return{...s,deadlineAt:Date.now()+t}}async awaitWithinBudget(e,t){if(t===void 0)return e;const s=t-Date.now();if(s<=0)throw E();let i;try{return await Promise.race([e,new Promise((o,h)=>{i=setTimeout(()=>h(E()),s)})])}finally{i!==void 0&&clearTimeout(i)}}async sendOnce(e,t,s,i={}){const o=this.resolveBudget(i);try{return await w(await this.transport(e,t,s,o))}catch(h){throw T(h,o.signal)}}nodePath(e){if(!p(e))throw new m(400,"invalid_node_id",`invalid node id: ${JSON.stringify(e)}`);return e}jobPath(e){if(!G(e))throw new m(400,"invalid_job_id",`invalid background job id: ${JSON.stringify(e)}`);return e}cronPath(e){if(!A(e))throw new m(400,"invalid_cron_id",`invalid cron id: ${JSON.stringify(e)}`);return e}requestPath(e){if(!p(e))throw new m(400,"invalid_request_id",`invalid request id: ${JSON.stringify(e)}`);return e}ticketId(e){if(!/^[a-f0-9]{64}$/.test(e))throw new TypeError(`invalid inbox ticket id: ${JSON.stringify(e)}`);return e}reviewPath(e){if(!p(e))throw new m(400,"invalid_review_id",`invalid review id: ${JSON.stringify(e)}`);return e}commentPath(e){if(!/^[a-f0-9]{32}$/.test(e))throw new m(400,"invalid_comment_id",`invalid comment id: ${JSON.stringify(e)}`);return e}transport(e,t,s,i={}){const o=i.deadlineAt===void 0?void 0:i.deadlineAt-Date.now();if(o!==void 0&&o<=0)return Promise.reject(E());const h=s===void 0?void 0:JSON.stringify(s),d={accept:"application/json",...this.headers,...i.headers};h!==void 0&&(d["content-type"]="application/json");const c=i.timeout??this.timeoutMs,f=o===void 0?c:c>0?Math.min(c,o):o,l=new AbortController,P=f>0?setTimeout(()=>l.abort(new DOMException("request timed out","TimeoutError")),f):void 0,q=i.signal,g=u(()=>l.abort(q?.reason),"onExternalAbort");q!==void 0&&(q.aborted?l.abort(q.reason):q.addEventListener("abort",g,{once:!0}));const O=u(()=>{P!==void 0&&clearTimeout(P),q?.removeEventListener("abort",g)},"finish");return this.fetch(new URL(t,this.baseUrl),{method:e,headers:d,body:h,signal:l.signal}).then(S=>this.retainRequestLifetime(S,O,()=>l.signal.aborted?l.signal.reason:void 0),S=>{throw O(),S})}retainRequestLifetime(e,t,s){if(e.body===null)return t(),e;const i=e.body.getReader();let o=!1;const h=u(()=>{o||(o=!0,t())},"close"),d=new ReadableStream({async pull(c){try{const{done:f,value:l}=await i.read();f?(h(),c.close()):c.enqueue(l)}catch(f){h(),c.error(s()??f)}},async cancel(c){h(),await i.cancel(c)}});return new Response(d,{status:e.status,statusText:e.statusText,headers:e.headers})}isColdSocketError(e){const t=v(e);return t==="ECONNREFUSED"||t==="ENOENT"||t==="ENOTSOCK"}isInterruptedSocketError(e){const t=v(e);return t==="ECONNRESET"||t==="EPIPE"}async rideOutInterruptedRequest(e,t,s,i={}){try{await this.awaitAvailability(void 0,i.deadlineAt)}catch(o){throw T(o,i.signal)}if(e!=="GET"&&e!=="HEAD")throw new m(503,"daemon_request_interrupted",`crtrd connection ended before this ${e} response; the request may or may not have been applied.`);try{return await this.transport(e,t,s,i)}catch(o){throw T(o,i.signal)}}async awaitAvailability(e,t){const s=t===void 0?void 0:t-Date.now();if(s!==void 0&&s<=0)throw E();let i;try{await x({windowMs:Math.min(this.coldStartPollWindowMs,s??this.coldStartPollWindowMs),initialError:e,retry:u(o=>o!==i,"retry"),probe:u(async o=>{const h=this.coldStartFailure?.();if(h!==void 0)throw i=new m(503,"daemon_unavailable",`crtrd failed to start: ${h}`),i;const d=await this.transport("GET",r.healthz(),void 0,{timeout:o});if(d.status>=200&&d.status<300){await b(d);return}const c=await d.text();try{if(typeof JSON.parse(c).startup_blocked=="string")return}catch{}throw new m(d.status,"daemon_health_unavailable",`crtrd health check returned HTTP ${d.status}: ${c.slice(0,500)}`)},"probe")})}catch(o){throw o===i&&(this.startupFailureReported=!0),o}}async handleColdSocket(e,t){const s=this.autostart&&this.onColdSocket!==void 0&&!this.coldStartAttempted;s&&(this.coldStartAttempted=!0,await this.awaitWithinBudget(this.onColdSocket(),t));try{await this.awaitAvailability(e,t)}catch(i){if(s&&!this.startupFailureReported){const o=F(this.coldStartDiagnostic);if(o!==void 0){const h=T(i);throw new m(h.status,h.code,`${h.message}
3
- ${o}`)}}throw i}}}function a(n,e){if(e===void 0)return n;const t=new URLSearchParams;for(const[i,o]of Object.entries(e))o!=null&&t.set(i,String(o));const s=t.toString();return s===""?n:`${n}?${s}`}u(a,"withQuery");async function w(n){const e=n.status>=200&&n.status<300,t=(await n.text()).trim();let s;if(t!=="")try{s=JSON.parse(t)}catch{if(e)return;throw new m(n.status,"invalid_response",t.slice(0,500),void 0,n.headers)}if(e)return s;throw C(s)?new m(n.status,s.error.code,s.error.message,s.error.details,n.headers,s.error):new m(n.status,"internal",`request failed with status ${n.status}`,void 0,n.headers)}u(w,"parse");function B(n){return n===429||n>=500}u(B,"isRetryableStatus");async function b(n){try{await n.body?.cancel()}catch{}}u(b,"drainResponseBody");function R(n){return Math.min(2e3,250*2**(n-1))}u(R,"retryDelayMs");function v(n){if(typeof n!="object"||n===null)return;const e=n.code;if(typeof e=="string")return e;const t=n.cause;if(typeof t=="object"&&t!==null){const s=t.code;if(typeof s=="string")return s}}u(v,"transportErrorCode");function I(n,e){return e?.aborted?!0:n instanceof DOMException&&(n.name==="AbortError"||n.name==="TimeoutError")}u(I,"isAbortError");function T(n,e){if(e?.aborted)return new m(0,"request_aborted","request aborted by caller signal");if(n instanceof m)return n;if(n instanceof DOMException&&n.name==="TimeoutError")return new m(504,"request_timeout",`crtrd request timed out: ${n.message}`);if(n instanceof DOMException&&n.name==="AbortError")return new m(0,"request_aborted","request aborted by caller signal");const t=v(n),s=n instanceof Error?n.message:String(n);return t==="ECONNREFUSED"||t==="ENOENT"||t==="ENOTSOCK"?new m(503,"daemon_unavailable",`crtrd is not reachable: ${s}`):t==="ETIMEDOUT"?new m(504,"request_timeout",`crtrd request timed out: ${s}`):new m(503,"transport_error",s)}u(T,"toTransportApiError");function y(n){return new Promise(e=>setTimeout(e,n))}u(y,"sleepMs");function F(n){if(n!==void 0)try{return n()}catch{return}}u(F,"safeColdStartDiagnostic");export{W as CrtrClient,F as safeColdStartDiagnostic,x as waitForDaemonAvailability};
1
+ var D=Object.defineProperty;var a=(o,e)=>D(o,"name",{value:e,configurable:!0});import{ApiError as m,isErrorBody as x}from"./errors.js";import{routes as r}from"./routes.js";import{isSafeNodeId as y}from"./dto/common.js";import{isSafeBashJobId as A}from"./dto/bash-jobs.js";import{isSafeCronId as N}from"./dto/crons.js";const B=3e4,I=Symbol.for("@north-light/api/local-socket-fetch"),L=1e4,F=200;async function $({windowMs:o,probe:e,initialError:t,pollIntervalMs:n=F,retry:s=a(()=>!0,"retry"),now:i=Date.now,sleep:u=M}){const c=i()+o;let h=t;for(;;){const f=c-i();if(f<=0)throw h??new Error("crtrd availability window expired without a probe failure");try{await e(f);return}catch(P){if(h=P,!s(P))throw P}const l=c-i();if(l<=0)throw h;await u(Math.min(n,l))}}a($,"waitForDaemonAvailability");const p=2147483647;function q(o,e="elapsed",t){const n=e==="retry_after"?`the server asked to retry after ${t}s, which is later than the call's deadline (${new Date(o).toISOString()}); not waiting`:`the call's deadline (${new Date(o).toISOString()}) passed`;return new m(504,"deadline_exceeded",n,{deadline_at:o,reason:e,...t===void 0?{}:{retry_after_s:t}})}a(q,"deadlineExceeded");const E=a(o=>q(o??Date.now()),"budgetExhausted");class Y{static{a(this,"CrtrClient")}baseUrl;fetch;headers;autostart;timeoutMs;maxRetries;localSocketTransport;onColdSocket;coldStartDiagnostic;coldStartFailure;coldStartPollWindowMs;startupFailureReported=!1;coldStartAttempted=!1;constructor(e){if(e.baseUrl==="")throw new TypeError("CrtrClient requires baseUrl");this.baseUrl=new URL(e.baseUrl),this.fetch=e.fetch??globalThis.fetch,this.headers={...e.headers},this.autostart=e.autostart??!1,this.timeoutMs=e.timeoutMs??B,this.maxRetries=e.maxRetries??2,this.localSocketTransport=this.fetch[I]===!0,e.onColdSocket!==void 0&&(this.onColdSocket=e.onColdSocket),e.coldStartDiagnostic!==void 0&&(this.coldStartDiagnostic=e.coldStartDiagnostic),e.coldStartFailure!==void 0&&(this.coldStartFailure=e.coldStartFailure),this.coldStartPollWindowMs=e.coldStartPollWindowMs??L}healthz(){return this.request("GET",r.healthz())}async probeHealthz(e){const t=await this.transport("GET",r.healthz(),void 0,{timeout:e});if(t.status===503)try{const n=await t.clone().json();if(typeof n.startup_blocked=="string")return n}catch{}return S(t)}status(){return this.request("GET",r.status())}due(){return this.request("GET",r.due())}restartDaemon(){return this.request("POST",r.daemonRestart())}admitDaemon(){return this.request("POST",r.daemonAdmit())}migrateState(e){return this.request("POST",r.daemonMigrate(),e)}appendNodeLog(e,t){return this.request("POST",r.nodeLog(this.nodePath(e)),t)}async putNodeTelemetry(e,t){await this.request("PUT",r.nodeTelemetry(this.nodePath(e)),t)}async putNodeRecap(e,t){await this.request("PUT",r.nodeRecap(this.nodePath(e)),t)}readNodeInboxReport(e,t,n){return this.request("GET",d(r.nodeInboxReport(this.nodePath(e)),{ref:t,execution_id:n}))}readNodePassiveMessage(e,t){return this.request("GET",d(r.nodePassiveMessage(this.nodePath(e)),{ref:t}))}nodePushedFinal(e,t){return this.request("POST",r.nodePushedFinal(this.nodePath(e)),t)}createNode(e){return this.request("POST",r.nodes(),e)}listNodes(e){return this.request("GET",d(r.nodes(),e))}getNode(e){return this.request("GET",r.node(this.nodePath(e)))}getNodeOutcome(e,{waitSeconds:t}={}){if(t!==void 0&&(!Number.isInteger(t)||t<0||t>25))throw new RangeError("waitSeconds must be an integer between 0 and 25");return this.request("GET",d(r.nodeOutcome(this.nodePath(e)),{wait:t}),void 0,{timeout:(t??0)*1e3+this.timeoutMs})}getNodeEvents(e,t={},n={}){if(t.after!==void 0&&(!Number.isSafeInteger(t.after)||t.after<0))throw new RangeError("after must be a non-negative safe integer");return this.transport("GET",d(r.nodeEvents(this.nodePath(e)),t),void 0,{...n,headers:{accept:"text/event-stream",...n.headers??{}},timeout:0})}async*getBrokerSignals(e,t,n={}){const s=await this.send("GET",d(r.nodeBrokerSignals(this.nodePath(e)),{execution_id:t}),void 0,{...n,headers:{accept:"application/x-ndjson",...n.headers??{}},timeout:0});if(!s.ok){await S(s);return}if(s.body===null)throw new m(s.status,"invalid_response","broker signal stream has no body");const i=s.body.getReader(),u=new TextDecoder;let c="";try{for(;;){const{done:h,value:f}=await i.read();if(h)break;c+=u.decode(f,{stream:!0});let l;for(;(l=c.indexOf(`
2
+ `))!==-1;){const P=c.slice(0,l);c=c.slice(l+1),P!==""&&(yield JSON.parse(P))}}if(c+=u.decode(),c.trim()!=="")throw new m(502,"invalid_response","broker signal stream ended mid-line")}finally{await i.cancel().catch(()=>{})}}createStructuredRequest(e,t){return e.mode==="sync"?this.sendOnce("POST","/v1/structured-requests",e,t):this.request("POST","/v1/structured-requests",e,t)}getStructuredRequest(e,{wait:t=0,budgetMs:n}={}){if(!Number.isInteger(t)||t<0||t>25)throw new RangeError("wait must be an integer between 0 and 25");return this.request("GET",d(`/v1/structured-requests/${this.requestPath(e)}`,{wait:t}),void 0,{budgetMs:n})}cancelStructuredRequest(e,t){return this.request("POST",`/v1/structured-requests/${this.requestPath(e)}/cancel`,{},t)}registerOutcomeDelivery(e,t){return this.request("PUT",r.nodeOutcomeDelivery(this.nodePath(e)),t)}getOutcomeDelivery(e){return this.request("GET",r.nodeOutcomeDelivery(this.nodePath(e)))}async disarmOutcomeDelivery(e){await this.request("DELETE",r.nodeOutcomeDelivery(this.nodePath(e)))}listBashJobs(e){return this.request("GET",r.nodeJobs(this.nodePath(e)))}stopBashJob(e,t){return this.request("DELETE",r.nodeJob(this.nodePath(e),this.jobPath(t)))}sendMessage(e,t){return this.request("POST",r.nodeMessages(this.nodePath(e)),t)}interruptNode(e){return this.request("POST",r.nodeInterrupt(this.nodePath(e)),{})}pushReport(e,t){return this.request("POST",r.nodeReports(this.nodePath(e)),t)}submitResult(e,t){return this.request("POST",r.nodeResult(this.nodePath(e)),t)}forkNode(e){return this.request("POST",r.nodeFork(this.nodePath(e)),{})}reviveNode(e,t){return this.request("POST",r.nodeRevive(this.nodePath(e)),t??{})}relaunchRoot(e){return this.request("POST",r.nodeRelaunchRoot(this.nodePath(e)),{})}bindBrokerSession(e,t){return this.request("POST",r.nodeBrokerSessionBound(this.nodePath(e)),t)}settleBroker(e,t){return this.request("POST",r.nodeBrokerSettle(this.nodePath(e)),t)}completeBrokerPark(e,t){return this.request("POST",r.nodeBrokerParkComplete(this.nodePath(e)),t)}recordBrokerParkActivity(e,t){return this.request("POST",r.nodeBrokerParkActivity(this.nodePath(e)),t)}async recordBrokerTelemetry(e,t){await this.request("POST",r.nodeBrokerTelemetry(this.nodePath(e)),t)}claimNodeMail(e,t){return this.request("POST",r.nodeMailClaim(this.nodePath(e)),t)}acknowledgeNodeMail(e,t){return this.request("POST",r.nodeMailAcknowledge(this.nodePath(e)),t)}recordBrokerTurn(e,t){return this.request("POST",r.brokerTurn(this.nodePath(e)),t)}mutateBrokerProviderRetry(e,t){return this.request("POST",r.brokerProviderRetry(this.nodePath(e)),t)}mutateBrokerFault(e,t){return this.request("POST",r.brokerFault(this.nodePath(e)),t)}getBrokerRecovery(e,t){return this.request("GET",`${r.brokerRecovery(this.nodePath(e))}?expected_execution_id=${encodeURIComponent(t)}`)}recordNodeFault(e,t){return this.request("POST",r.nodeFault(this.nodePath(e)),t)}clearNodeFault(e,t={}){const n=new URLSearchParams;t.link!==void 0&&n.set("link",t.link),t.preserve_episode!==void 0&&n.set("preserve_episode",String(t.preserve_episode));const s=n.size===0?"":`?${n.toString()}`;return this.request("DELETE",`${r.nodeFault(this.nodePath(e))}${s}`)}commitBrokerModel(e,t){return this.request("POST",r.nodeBrokerModel(this.nodePath(e)),t)}brokerExtensionState(e){return this.request("GET",r.nodeBrokerExtensionState(this.nodePath(e)))}commitBrokerGeneratedName(e,t){return this.request("POST",r.nodeBrokerGeneratedName(this.nodePath(e)),t)}commitBrokerPersonaAck(e,t){return this.request("POST",r.nodeBrokerPersonaAck(this.nodePath(e)),t)}closeNode(e,t){return this.request("POST",r.nodeClose(this.nodePath(e)),t??{})}recycleNode(e){return this.request("POST",r.nodeRecycle(this.nodePath(e)),{})}demoteNode(e){return this.request("POST",r.nodeDemote(this.nodePath(e)),{})}promoteNode(e,t){return this.request("POST",r.nodePromote(this.nodePath(e)),t)}yieldNode(e,t){return this.request("POST",r.nodeYield(this.nodePath(e)),t)}waitNode(e,t){return this.request("POST",r.nodeWait(this.nodePath(e)),t)}listKinds(){return this.request("GET",r.kinds())}patchConfig(e,t){return this.request("PATCH",r.nodeConfig(this.nodePath(e)),t,t.model!==void 0?{timeout:45e3+this.timeoutMs}:void 0)}closeWorktree(e){return this.request("POST",r.nodeWorktreeClose(this.nodePath(e)),{})}abandonWorktree(e,t){return this.request("POST",r.nodeWorktreeAbandon(this.nodePath(e)),t)}retryDelivery(e){return this.request("POST",r.deliveryRetry(e),{})}listQuarantinedWorktrees(){return this.request("GET",r.quarantinedWorktrees())}showLimits(){return this.request("GET",r.limits())}setAppLimits(e,t){return this.request("PUT",r.limitsApp(e),t)}removeAppLimits(e,t){return this.request("DELETE",d(r.limitsApp(e),{fields:t.join(",")}))}listSpaces(){return this.request("GET",r.spaces())}subscribe(e,t){return this.request("POST",r.nodeSubscriptions(this.nodePath(e)),t)}listFocuses(){return this.request("GET",r.focuses())}focusOf(e){return this.request("GET",d(r.focusByNode(),{node_id:e}))}focusByPane(e){return this.request("GET",d(r.focusByPane(),{pane:e}))}registerFocus(e){return this.request("POST",r.focuses(),e)}async setFocusPane(e,t){await this.request("PATCH",r.focus(e),t)}async closeFocus(e){await this.request("DELETE",r.focus(e))}async unsubscribe(e,t){await this.request("DELETE",r.nodeSubscription(this.nodePath(e),this.nodePath(t)))}armCron(e){return this.request("POST",r.crons(),e)}listCrons(e){return this.request("GET",d(r.crons(),e))}showCron(e,t){return this.request("GET",d(r.cron(this.cronPath(e)),t))}pauseCron(e,t){return this.request("POST",d(r.cronPause(this.cronPath(e)),t),{})}resumeCron(e,t){return this.request("POST",d(r.cronResume(this.cronPath(e)),t),{})}runCron(e,t){return this.request("POST",d(r.cronRun(this.cronPath(e)),t),{})}async cancelCron(e,t){await this.request("DELETE",d(r.cron(this.cronPath(e)),t))}pokeCrons(){return this.request("POST",r.cronsPoke(),{})}ensureAttach(e,t){return this.request("POST",r.nodeAttach(this.nodePath(e)),t??{})}getReports(e,t){return this.request("GET",d(r.nodeReports(this.nodePath(e)),t))}getTranscript(e,t){return this.request("GET",d(r.nodeTranscript(this.nodePath(e)),t))}getSnapshot(e){return this.request("GET",r.nodeSnapshot(this.nodePath(e)))}nodeSubject(e){return this.request("GET",r.nodeSubject(this.nodePath(e)))}getNodeMessages(e,t){return this.request("GET",d(r.nodeMessages(this.nodePath(e)),t))}getSession(e){return this.request("GET",r.nodeSession(this.nodePath(e)))}getChatInventory(e){return this.request("GET",r.nodeChatInventory(this.nodePath(e)))}getProspectiveChatInventory(e){return this.request("GET",d(r.prospectiveChatInventory(),e))}getContext(e){return this.request("GET",r.nodeContext(this.nodePath(e)))}peekFile(e,t,n){return this.request("GET",d(r.filePeek(),{path:e,encoding:t}),void 0,n)}writeFile(e,t,n,s){return this.request("POST",r.fileWrite(),{path:e,content:t,encoding:n},s)}listFiles(e,t,n){return this.request("GET",d(r.fileList(),{path:e,limit:t}),void 0,n)}runBash(e,t){return this.request("POST",r.bash(),e,t)}objects={read:a((e,t)=>this.request("GET",d(r.object(e),t)),"read"),list:a(e=>this.request("GET",d(r.objects(),e)),"list"),search:a(e=>this.request("POST",r.objectsSearch(),e),"search"),watch:a((e,t={})=>this.request("POST",r.objectWatch(e),t),"watch"),unwatch:a((e,t)=>this.request("DELETE",d(r.objectWatch(e),t)),"unwatch"),edges:a((e,t)=>this.request("GET",d(r.objectEdges(e),t)),"edges")};docs={write:a(e=>this.request("POST",r.docs(),e),"write"),edit:a((e,t)=>this.request("PATCH",r.doc(e),t),"edit"),move:a((e,t)=>this.request("POST",r.docMove(e),t),"move"),delete:a(e=>this.request("DELETE",r.doc(e)),"delete"),history:a((e,t)=>this.request("GET",d(r.docHistory(e),t)),"history"),lint:a((e={})=>this.request("POST",r.docsLint(),e),"lint")};documents={reconcilePackages:a(()=>this.request("POST",r.documentsReconcilePackages()),"reconcilePackages")};nodeDelivery(e,t){return this.request("POST",r.nodeDelivery(this.nodePath(e)),t)}deliveryDryRun(e){return this.request("POST",r.deliveryDryRun(),e)}recordBackgroundedJob(e,t,n){return this.request("POST",r.nodeJobBackground(this.nodePath(e),this.jobPath(t)),n)}ensureProfile(e,t){return this.request("PUT",r.profile(e),t??{})}createProfile(e){return this.request("POST",r.profiles(),e)}listProfiles(){return this.request("GET",r.profiles())}updateProfile(e,t){return this.request("PATCH",r.profile(e),t)}listAppEnv(){return this.request("GET",r.appEnv())}setAppEnv(e,t){return this.request("PUT",r.appEnvVar(e),{value:t})}removeAppEnv(e){return this.request("DELETE",r.appEnvVar(e))}listProfileEnv(e){return this.request("GET",r.profileEnv(e))}setProfileEnv(e,t,n){return this.request("PUT",r.profileEnvVar(e,t),{value:n})}removeProfileEnv(e,t){return this.request("DELETE",r.profileEnvVar(e,t))}getProfile(e){return this.request("GET",r.profile(e))}pauseProfile(e){return this.request("POST",r.profilePause(e),{})}resumeProfile(e){return this.request("POST",r.profileResume(e),{})}updateProfileMetadata(e,t){return this.request("PATCH",r.profileMetadata(e),t)}deleteProfile(e,t){return this.request("DELETE",r.profile(e),t)}listModelAuth(){return this.request("GET",r.modelAuths())}getModelAuthReadiness(e){return this.request("GET",d(r.modelAuthReadiness(),e))}installCredential(e,t){return this.request("PUT",r.modelAuth(e),t)}removeCredential(e){return this.request("DELETE",r.modelAuth(e))}startModelAuth(e){return this.request("POST",r.modelAuthFlows(),e,{timeout:3e4+this.timeoutMs})}completeModelAuth(e,t){return this.request("POST",r.modelAuthFlowComplete(e),t,{timeout:35e3+this.timeoutMs})}cancelModelAuth(e){return this.request("POST",r.modelAuthFlowCancel(e),{})}getModelAuthFlow(e,t){return this.request("GET",d(r.modelAuthFlow(e),{wait:t}),void 0,{timeout:(t??0)*1e3+this.timeoutMs})}createReview(e){return this.request("POST",r.humanReviews(),e)}listReviews(e){return this.request("GET",d(r.humanReviews(),e))}getReview(e){return this.request("GET",r.humanReview(this.reviewPath(e)))}submitReview(e){return this.request("POST",r.humanReviewSubmit(this.reviewPath(e)),{})}cancelReview(e,t={}){return this.request("POST",r.humanReviewCancel(this.reviewPath(e)),t)}getReviewDocumentBase(e){return this.request("GET",r.humanReviewDocument(this.reviewPath(e)))}createReviewComment(e,t){const n=e===void 0?"self":this.reviewPath(e);return this.request("POST",r.humanReviewComments(n),t)}listReviewComments(e,t){const n=e===void 0?"self":this.reviewPath(e);return this.request("GET",d(r.humanReviewComments(n),t))}readReviewCommentEvents(e,t){return this.request("GET",d(r.humanReviewCommentEvents(this.reviewPath(e)),t))}updateReviewCommentRanges(e,t){return this.request("POST",r.humanReviewCommentRanges(this.reviewPath(e)),t)}getReviewComment(e){return this.request("GET",r.humanComment(this.commentPath(e)))}editReviewComment(e,t){return this.request("POST",r.humanCommentEdit(this.commentPath(e)),t)}resolveReviewComment(e,t={}){return this.request("POST",r.humanCommentResolve(this.commentPath(e)),t)}reopenReviewComment(e,t={}){return this.request("POST",r.humanCommentReopen(this.commentPath(e)),t)}deleteReviewComment(e,t={}){return this.request("POST",r.humanCommentDelete(this.commentPath(e)),t)}forkReviewComment(e){return this.request("POST",r.humanCommentFork(this.commentPath(e)),{})}listHumanInbox(){return this.request("GET",r.humanInbox())}getHumanInboxPage(e){return this.request("GET",r.humanInboxTicket(this.ticketId(e)))}getInboxHistory(e){if(e===void 0)return this.request("GET",r.humanInbox()+"/history");if(typeof e!="string"||e==="")throw new TypeError(`invalid node id: ${JSON.stringify(e)}`);return this.request("GET",d(r.humanInbox()+"/history",{node_id:e}))}respondHumanInboxPage(e,t){return this.request("POST",r.humanInboxRespond(this.ticketId(e)),t)}postInboxProgress(e,t){return this.request("POST",r.humanInboxProgress(this.ticketId(e)),{responses:t})}getInboxResponse(e){return this.request("GET",r.humanInboxResponse(this.ticketId(e)))}cancelHumanInboxTicket(e,t){return this.request("POST",r.humanInboxCancel(this.ticketId(e)),t??{})}createHumanRequest(e){return this.request("POST",r.humanRequests(),e)}listPageComponents(){return this.request("GET",r.humanComponents())}getHumanRequest(e){return this.request("GET",r.humanRequest(this.requestPath(e)))}replaceHumanRequest(e,t){return this.request("POST",r.humanRequestReplace(this.requestPath(e)),t)}respondHumanRequest(e,t){return this.request("POST",r.humanRequestRespond(this.requestPath(e)),t)}dismissHumanRequest(e,t={}){return this.request("POST",r.humanRequestDismiss(this.requestPath(e)),t)}cancelHumanRequest(e,t={}){return this.request("POST",r.humanRequestCancel(this.requestPath(e)),t)}resolvePageFeedbackComment(e,t,n){if(!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(t))throw new TypeError(`invalid feedback comment id: ${JSON.stringify(t)}`);return this.request("POST",r.humanInboxFeedbackResolve(this.ticketId(e),t),{node_id:n})}async dashboard(e){const t=e?.under!==void 0?{under:e.under}:void 0,[n,s]=await Promise.all([this.listNodes(t),this.status()]);return{nodes:n,counts:s.node_counts,generated_at:new Date().toISOString()}}attention(){return this.request("GET",r.canvasAttention())}attentionCounts(e){const t={node_ids:e};return this.request("POST",r.canvasAttentionCounts(),t)}historySearch(e){return this.request("POST",r.canvasHistorySearch(),e)}historyGrep(e){return this.request("POST",r.canvasHistoryGrep(),e)}historyRead(e){return this.request("GET",d(r.canvasHistoryRead(),e))}historyStats(e){return this.request("POST",r.canvasHistoryStats(),e)}canvasSnapshot(){return this.request("GET",r.canvasSnapshot())}analytics(e){return this.request("GET",d(r.canvasAnalytics(),e),void 0,{timeout:6e4})}async recordMemoryReads(e,t){await this.request("POST",r.nodeMemoryReads(this.nodePath(e)),t)}canvasRoster(){return this.request("GET",r.canvasRoster())}async canvasBrowseCompressed(){const e=await this.send("GET",r.canvasBrowse());return e.ok||await S(e),new Uint8Array(await e.arrayBuffer())}canvasGraph(){return this.request("GET",r.canvasGraph())}prune(e){return this.request("POST",r.canvasPrune(),e)}async request(e,t,n,s){try{const i=s===void 0?void 0:this.resolveBudget(s),u=await this.send(e,t,n,i);return await S(u,i?.deadlineAt)}catch(i){throw T(i,s?.signal)}}async rawResponse(e,t,n){try{return await this.send(e,t,void 0,n)}catch(s){throw T(s,n?.signal)}}async send(e,t,n,s={}){const i=this.resolveBudget(s),u=e==="GET"||e==="HEAD"||e==="DELETE",c=i.maxRetries??this.maxRetries;for(let h=0;;h++){let f;try{f=await this.transport(e,t,n,i)}catch(l){if(this.isColdSocketError(l))try{await this.handleColdSocket(l,i.deadlineAt),f=await this.transport(e,t,n,i)}catch(P){throw i.deadlineAt!==void 0&&Date.now()>=i.deadlineAt&&!G(P,i.signal)?q(i.deadlineAt):T(P,i.signal)}else{if(this.isInterruptedSocketError(l)&&this.localSocketTransport&&(!u||h<c))return await this.rideOutInterruptedRequest(e,t,n,i);if(u&&h<c&&!G(l,i.signal)&&(!this.localSocketTransport||!this.isInterruptedSocketError(l))){await _(C(h+1),i);continue}else throw T(l,i.signal)}}if(u&&h<c&&H(f.status)){await k(f,i.deadlineAt),await _(C(h+1),i,j(f));continue}return f}}resolveBudget(e){const{budgetMs:t,deadlineAt:n,...s}=e;if(t===void 0&&n===void 0)return s;if(t!==void 0&&(!Number.isFinite(t)||t<=0||t>p))throw new RangeError(`budgetMs must be a positive finite number of at most ${p}`);if(n!==void 0&&(!Number.isFinite(n)||n-Date.now()>p))throw new RangeError(`deadlineAt must be a finite epoch-millisecond instant at most ${p} ms from now`);return{...s,deadlineAt:Math.min(n??1/0,t===void 0?1/0:Date.now()+t)}}async awaitWithinBudget(e,t){if(t===void 0)return e;const n=t-Date.now();if(n<=0)throw E(t);let s;try{return await Promise.race([e,new Promise((i,u)=>{s=setTimeout(()=>u(E(t)),n)})])}finally{s!==void 0&&clearTimeout(s)}}async sendOnce(e,t,n,s={}){const i=this.resolveBudget(s);try{return await S(await this.transport(e,t,n,i))}catch(u){throw T(u,i.signal)}}nodePath(e){if(!y(e))throw new m(400,"invalid_node_id",`invalid node id: ${JSON.stringify(e)}`);return e}jobPath(e){if(!A(e))throw new m(400,"invalid_job_id",`invalid background job id: ${JSON.stringify(e)}`);return e}cronPath(e){if(!N(e))throw new m(400,"invalid_cron_id",`invalid cron id: ${JSON.stringify(e)}`);return e}requestPath(e){if(!y(e))throw new m(400,"invalid_request_id",`invalid request id: ${JSON.stringify(e)}`);return e}ticketId(e){if(!/^[a-f0-9]{64}$/.test(e))throw new TypeError(`invalid inbox ticket id: ${JSON.stringify(e)}`);return e}reviewPath(e){if(!y(e))throw new m(400,"invalid_review_id",`invalid review id: ${JSON.stringify(e)}`);return e}commentPath(e){if(!/^[a-f0-9]{32}$/.test(e))throw new m(400,"invalid_comment_id",`invalid comment id: ${JSON.stringify(e)}`);return e}transport(e,t,n,s={}){const i=s.deadlineAt===void 0?void 0:s.deadlineAt-Date.now();if(i!==void 0&&i<=0)return Promise.reject(E(s.deadlineAt));const u=n===void 0?void 0:JSON.stringify(n),c={accept:"application/json",...this.headers,...s.headers};u!==void 0&&(c["content-type"]="application/json");const h=s.timeout??this.timeoutMs,f=i===void 0?h:h>0?Math.min(h,i):i,l=new AbortController,P=i!==void 0&&(h<=0||i<=h),g=f>0?setTimeout(()=>l.abort(P?E(s.deadlineAt):new DOMException("request timed out","TimeoutError")),f):void 0,w=s.signal,O=a(()=>l.abort(w?.reason),"onExternalAbort");w!==void 0&&(w.aborted?l.abort(w.reason):w.addEventListener("abort",O,{once:!0}));const R=a(()=>{g!==void 0&&clearTimeout(g),w?.removeEventListener("abort",O)},"finish");return W(this.fetch(new URL(t,this.baseUrl),{method:e,headers:c,body:u,signal:l.signal}),l.signal).then(v=>this.retainRequestLifetime(v,R,()=>l.signal.aborted?l.signal.reason:void 0),v=>{throw R(),v})}retainRequestLifetime(e,t,n){if(e.body===null)return t(),e;const s=e.body.getReader();let i=!1;const u=a(()=>{i||(i=!0,t())},"close"),c=new ReadableStream({async pull(h){try{const{done:f,value:l}=await s.read();f?(u(),h.close()):h.enqueue(l)}catch(f){u(),h.error(n()??f)}},async cancel(h){u(),await s.cancel(h)}});return new Response(c,{status:e.status,statusText:e.statusText,headers:e.headers})}isColdSocketError(e){const t=b(e);return t==="ECONNREFUSED"||t==="ENOENT"||t==="ENOTSOCK"}isInterruptedSocketError(e){const t=b(e);return t==="ECONNRESET"||t==="EPIPE"}async rideOutInterruptedRequest(e,t,n,s={}){try{await this.awaitAvailability(void 0,s.deadlineAt)}catch(i){throw T(i,s.signal)}if(e!=="GET"&&e!=="HEAD")throw new m(503,"daemon_request_interrupted",`crtrd connection ended before this ${e} response; the request may or may not have been applied.`);try{return await this.transport(e,t,n,s)}catch(i){throw T(i,s.signal)}}async awaitAvailability(e,t){const n=t===void 0?void 0:t-Date.now();if(n!==void 0&&n<=0)throw E(t);let s;try{await $({windowMs:Math.min(this.coldStartPollWindowMs,n??this.coldStartPollWindowMs),initialError:e,retry:a(i=>i!==s,"retry"),probe:a(async i=>{const u=this.coldStartFailure?.();if(u!==void 0)throw s=new m(503,"daemon_unavailable",`crtrd failed to start: ${u}`),s;const c=await this.transport("GET",r.healthz(),void 0,{timeout:i});if(c.status>=200&&c.status<300){await k(c);return}const h=await c.text();try{if(typeof JSON.parse(h).startup_blocked=="string")return}catch{}throw new m(c.status,"daemon_health_unavailable",`crtrd health check returned HTTP ${c.status}: ${h.slice(0,500)}`)},"probe")})}catch(i){throw i===s&&(this.startupFailureReported=!0),i}}async handleColdSocket(e,t){const n=this.autostart&&this.onColdSocket!==void 0&&!this.coldStartAttempted;n&&(this.coldStartAttempted=!0,await this.awaitWithinBudget(this.onColdSocket(),t));try{await this.awaitAvailability(e,t)}catch(s){if(n&&!this.startupFailureReported){const i=J(this.coldStartDiagnostic);if(i!==void 0){const u=T(s);throw new m(u.status,u.code,`${u.message}
3
+ ${i}`)}}throw s}}}function d(o,e){if(e===void 0)return o;const t=new URLSearchParams;for(const[s,i]of Object.entries(e))i!=null&&t.set(s,String(i));const n=t.toString();return n===""?o:`${o}?${n}`}a(d,"withQuery");async function S(o,e){const t=o.status>=200&&o.status<300,n=(await U(o,e)).trim();let s;if(n!=="")try{s=JSON.parse(n)}catch{if(t)return;throw new m(o.status,"invalid_response",n.slice(0,500),void 0,o.headers)}if(t)return s;throw x(s)?new m(o.status,s.error.code,s.error.message,s.error.details,o.headers,s.error):new m(o.status,"internal",`request failed with status ${o.status}`,void 0,o.headers)}a(S,"parse");function H(o){return o===429||o>=500}a(H,"isRetryableStatus");async function k(o,e){const t=(async()=>{try{await o.body?.cancel()}catch{}})();if(e===void 0)return t;let n;try{await Promise.race([t,new Promise(s=>{n=setTimeout(s,Math.max(0,e-Date.now()))})])}finally{n!==void 0&&clearTimeout(n)}}a(k,"drainResponseBody");async function U(o,e){if(e===void 0)return o.text();const t=e-Date.now(),n=o.text();if(n.catch(()=>{}),t<=0)throw o.body?.cancel().catch(()=>{}),q(e);let s;try{return await Promise.race([n,new Promise((i,u)=>{s=setTimeout(()=>{o.body?.cancel().catch(()=>{}),u(q(e))},t)})])}finally{s!==void 0&&clearTimeout(s)}}a(U,"readTextWithin");function C(o){return Math.min(2e3,250*2**(o-1))}a(C,"retryDelayMs");function b(o){if(typeof o!="object"||o===null)return;const e=o.code;if(typeof e=="string")return e;const t=o.cause;if(typeof t=="object"&&t!==null){const n=t.code;if(typeof n=="string")return n}}a(b,"transportErrorCode");function G(o,e){return e?.aborted?!0:o instanceof DOMException&&(o.name==="AbortError"||o.name==="TimeoutError")}a(G,"isAbortError");function T(o,e){if(e?.aborted)return new m(0,"request_aborted","request aborted by caller signal");if(o instanceof m)return o;if(o instanceof DOMException&&o.name==="TimeoutError")return new m(504,"request_timeout",`crtrd request timed out: ${o.message}`);if(o instanceof DOMException&&o.name==="AbortError")return new m(0,"request_aborted","request aborted by caller signal");const t=b(o),n=o instanceof Error?o.message:String(o);return t==="ECONNREFUSED"||t==="ENOENT"||t==="ENOTSOCK"?new m(503,"daemon_unavailable",`crtrd is not reachable: ${n}`):t==="ETIMEDOUT"?new m(504,"request_timeout",`crtrd request timed out: ${n}`):new m(503,"transport_error",n)}a(T,"toTransportApiError");function j(o){const e=Number(o.headers.get("retry-after"));return Number.isFinite(e)&&e>0?e:void 0}a(j,"retryAfterHeaderS");async function _(o,e,t,n){const{deadlineAt:s,signal:i}=e;if(i?.aborted)throw new m(0,"request_aborted","request aborted by caller signal");const u=s!==void 0&&t!==void 0?t*1e3:void 0,c=Math.max(o,u??0);if(s!==void 0&&Date.now()+c>=s){const h=q(s,u!==void 0&&u>=o?"retry_after":"elapsed",u===void 0?void 0:t);throw n!==void 0&&Object.defineProperty(h,"cause",{value:n,configurable:!0,writable:!0}),h}if(i===void 0)return M(c);await new Promise((h,f)=>{const l=a(()=>{clearTimeout(P),i.removeEventListener("abort",l),f(new m(0,"request_aborted","request aborted by caller signal"))},"onAbort"),P=setTimeout(()=>{i.removeEventListener("abort",l),h()},c);i.addEventListener("abort",l,{once:!0})})}a(_,"sleepWithin");function W(o,e){return new Promise((t,n)=>{const s=a(()=>n(e.reason),"onAbort");e.aborted?s():e.addEventListener("abort",s,{once:!0}),o.then(i=>{e.removeEventListener("abort",s),e.aborted?i.body?.cancel().catch(()=>{}):t(i)},i=>{e.removeEventListener("abort",s),n(i)})})}a(W,"abortable");function M(o){return new Promise(e=>setTimeout(e,o))}a(M,"sleepMs");function J(o){if(o!==void 0)try{return o()}catch{return}}a(J,"safeColdStartDiagnostic");export{Y as CrtrClient,p as MAX_TIMER_MS,q as deadlineExceeded,J as safeColdStartDiagnostic,_ as sleepWithin,$ as waitForDaemonAvailability};
@@ -20,7 +20,7 @@ export type ModeDTO = 'base' | 'orchestrator';
20
20
  /** Why a node last stopped (mirrors the runtime `ExitIntent` union). */
21
21
  export type ExitIntentDTO = 'done' | 'refresh' | 'idle-release' | 'parked' | null;
22
22
  /** Why a terminal node ended (mirrors the runtime `TerminalReason` union). */
23
- export type TerminalReasonDTO = 'finalized' | 'finished' | 'parked' | 'closed' | 'retired' | 'crashed' | 'stranded' | 'boot_failed' | 'crash_looped' | 'launch_failed' | 'context_overflow' | 'deadline_exceeded' | 'provider_fatal' | 'declined' | 'out_of_memory';
23
+ export type TerminalReasonDTO = 'finalized' | 'finished' | 'parked' | 'closed' | 'retired' | 'crashed' | 'stranded' | 'boot_failed' | 'crash_looped' | 'launch_failed' | 'context_overflow' | 'deadline_exceeded' | 'provider_fatal' | 'declined' | 'out_of_memory' | 'cap_exhausted';
24
24
  /** Inbox urgency tier for a delivered message (mirrors feed/inbox `InboxTier`). */
25
25
  export type InboxTierDTO = 'critical' | 'urgent' | 'normal' | 'deferred';
26
26
  /** Whether a node id is a safe single filesystem path segment. Re-declared here
@@ -27,6 +27,12 @@ export interface CronDTO {
27
27
  created_by: NodeIdDTO | null;
28
28
  /** The bash command each fire runs. */
29
29
  command: string;
30
+ /** The grant the cron runs under (checked at every fire, call and birth); null only for the terminal on a runtime with no issuer. */
31
+ grant_id: string | null;
32
+ /** The scopes the cron is bounded to; null = the grant's whole current set (a person-armed terminal cron). */
33
+ scopes: string[] | null;
34
+ /** The run of the node that armed it, when a node did. */
35
+ origin_run_id: string | null;
30
36
  /** The shell and any spawned node stay isolated when armed from an isolated node. */
31
37
  isolated: boolean;
32
38
  /** Next occurrence (UTC). */
@@ -131,7 +137,7 @@ export interface ArmCronRequest {
131
137
  isolated?: boolean;
132
138
  /** Execution context, snapshotted at arm time — chosen by the client, else
133
139
  * inherited from the creating node; never re-resolved at run time. Omitted
134
- * means the daemon's default: the creating node's folder for an isolated
140
+ * means the daemon's default: the cron's own folder for an isolated
135
141
  * cron, else the app's files directory. A runtime whose cron runs are
136
142
  * sandboxed refuses (400 `cwd_not_mounted`) a chosen cwd the run's mounts do
137
143
  * not reach, so a cron is never armed with a directory it cannot start in. */
@@ -41,7 +41,7 @@ export interface CreateHumanRequestDTO {
41
41
  delivery_state: HumanRequestDeliveryState;
42
42
  }
43
43
  export interface HumanRequestDeliveryFailureDTO {
44
- kind: 'exit' | 'signal' | 'timeout' | 'spawn_error';
44
+ kind: 'exit' | 'signal' | 'timeout' | 'spawn_error' | 'grant_refused';
45
45
  exit_code?: number;
46
46
  signal?: string;
47
47
  message?: string;
@@ -57,7 +57,7 @@ export type RunOutcomeDTO = (OutcomeBase & {
57
57
  details: DeclinedResultDTO | null;
58
58
  }) | (OutcomeBase & {
59
59
  kind: 'failure';
60
- reason: 'canceled' | 'deadline_exceeded';
60
+ reason: 'canceled' | 'deadline_exceeded' | 'cap_exhausted';
61
61
  declined: null;
62
62
  }) | (OutcomeBase & {
63
63
  kind: 'failure';
@@ -0,0 +1,27 @@
1
+ /** One served model's month-to-date usage (`usage.get` `by_model`). */
2
+ export interface SpendUsageModelRowDTO {
3
+ served_model: string;
4
+ input_tokens: number;
5
+ output_tokens: number;
6
+ /** List-price cost; null when the served model has no list price (an app-registered endpoint, or an unpriced model). */
7
+ usd_equivalent: number | null;
8
+ }
9
+ /** `usage.get`: the caller's grantee's month-to-date spend (daemon operations, `usage.get`). */
10
+ export interface SpendUsageDTO {
11
+ grantee: string;
12
+ /** UTC calendar month, `YYYY-MM`. */
13
+ month: string;
14
+ /** User-paid spend this month: every model completion and provider receipt. Receipts have no `by_model` row, so this can exceed the sum of `by_model`. */
15
+ usd_equivalent: number;
16
+ /** User-paid model completions, one row per served model. */
17
+ by_model: SpendUsageModelRowDTO[];
18
+ /** App-paid usage; never counts toward `spend_cap`. */
19
+ app_paid: {
20
+ usd_equivalent: number;
21
+ by_model: SpendUsageModelRowDTO[];
22
+ };
23
+ /** The monthly cap on user-paid spend; once `usd_equivalent` reaches it, user-paid model calls answer `cap_exhausted` until `reset_at`. Null when uncapped. */
24
+ spend_cap: number | null;
25
+ /** 00:00Z on the first day of the next UTC month, when the count starts over. */
26
+ reset_at: string;
27
+ }
File without changes
@@ -1 +1 @@
1
- var d=Object.defineProperty;var e=(t,r)=>d(t,"name",{value:r,configurable:!0});class o extends Error{static{e(this,"APIError")}status;code;details;headers;type;param;origin;retryable;retryAfterS;resetAt;requestId;runId;questionId;scope;userAction;constructor(r,i,a,n,u,s){super(a),this.name="APIError",this.status=r,this.code=i,this.headers=new Headers(u),n!==void 0&&(this.details=n),this.type=s?.type,this.param=s?.param,this.origin=s?.origin,this.retryable=s?.retryable,this.retryAfterS=s?.retry_after_s,this.resetAt=s?.reset_at,this.requestId=s?.request_id??this.headers.get("Request-Id")??void 0,this.runId=s?.run_id,this.questionId=s?.question_id,this.scope=s?.scope,this.userAction=s?.user_action}}function h(t){return t instanceof o?t.status===503&&(t.code==="daemon_unavailable"||t.code==="transport_error"||t.code==="daemon_request_interrupted")||t.status===504&&t.code==="request_timeout"||t.status>=500&&t.code==="daemon_health_unavailable":!1}e(h,"isDaemonTransportApiError");function f(t){if(typeof t!="object"||t===null)return!1;const r=t.error;if(typeof r!="object"||r===null)return!1;const{code:i,message:a}=r;return typeof i=="string"&&typeof a=="string"}e(f,"isErrorBody");export{o as APIError,o as ApiError,h as isDaemonTransportApiError,f as isErrorBody};
1
+ var u=Object.defineProperty;var r=(t,e)=>u(t,"name",{value:e,configurable:!0});class n extends Error{static{r(this,"APIError")}status;code;details;headers;type;param;origin;retryable;retryAfterS;resetAt;requestId;runId;questionId;scope;userAction;constructor(e,i,a,d,o,s){super(a),this.name="APIError",this.status=e,this.code=i,this.headers=new Headers(o),d!==void 0&&(this.details=d),this.type=s?.type,this.param=s?.param,this.origin=s?.origin,this.retryable=s?.retryable,this.retryAfterS=s?.retry_after_s,this.resetAt=s?.reset_at,this.requestId=s?.request_id??this.headers.get("Request-Id")??void 0,this.runId=s?.run_id,this.questionId=s?.question_id,this.scope=s?.scope,this.userAction=s?.user_action}}function h(t){return t instanceof n?t.status===503&&(t.code==="daemon_unavailable"||t.code==="transport_error"||t.code==="daemon_request_interrupted")||t.status===504&&(t.code==="request_timeout"||t.code==="deadline_exceeded")||t.status>=500&&t.code==="daemon_health_unavailable":!1}r(h,"isDaemonTransportApiError");function f(t){if(typeof t!="object"||t===null)return!1;const e=t.error;if(typeof e!="object"||e===null)return!1;const{code:i,message:a}=e;return typeof i=="string"&&typeof a=="string"}r(f,"isErrorBody");export{n as APIError,n as ApiError,h as isDaemonTransportApiError,f as isErrorBody};
@@ -1,5 +1,5 @@
1
- export { CrtrClient, waitForDaemonAvailability } from './client.js';
2
- export type { CrtrClientOptions, CrtrRequestOptions } from './client.js';
1
+ export { CrtrClient, deadlineExceeded, MAX_TIMER_MS, sleepWithin, waitForDaemonAvailability } from './client.js';
2
+ export type { CrtrClientOptions, CrtrRequestOptions, DeadlineReason } from './client.js';
3
3
  export { APIError, ApiError, isErrorBody } from './errors.js';
4
4
  export type { ErrorBody } from './errors.js';
5
5
  export { errorCodes } from './error-codes.js';
@@ -14,6 +14,7 @@ export * from './dto/common.js';
14
14
  export * from './dto/health.js';
15
15
  export * from './dto/limits.js';
16
16
  export * from './dto/grants.js';
17
+ export * from './dto/usage.js';
17
18
  export * from './dto/nodes.js';
18
19
  export * from './dto/node-outcomes.js';
19
20
  export * from './dto/node-events.js';
package/dist/api/index.js CHANGED
@@ -1 +1 @@
1
- import{CrtrClient as e,waitForDaemonAvailability as t}from"./client.js";import{APIError as p,ApiError as f,isErrorBody as x}from"./errors.js";import{errorCodes as i}from"./error-codes.js";import{API_VERSION as R,routes as _}from"./routes.js";import{RUNTIME_VERSION_HEADER as s,MINIMUM_RUNTIME_VERSION_HEADER as N,compareReleaseVersions as O,minimumRuntimeRefusal as a}from"./runtime-version.js";import{LONG_POLL_MAX_WAIT_SECONDS as A,LONG_POLL_ROUTES as S,longPollHoldSeconds as u,longPollRoute as L,requestedWait as M}from"./long-poll.js";export*from"../shared/generated-context.js";export*from"./dto/common.js";export*from"./dto/health.js";export*from"./dto/limits.js";export*from"./dto/grants.js";export*from"./dto/nodes.js";export*from"./dto/node-outcomes.js";export*from"./dto/node-events.js";export*from"./dto/run-events.js";export*from"./dto/broker-signals.js";export*from"./dto/bash-jobs.js";export*from"./dto/messages.js";export*from"./dto/reports.js";export*from"./dto/structured-requests.js";export*from"./dto/lifecycle.js";export*from"./dto/subscriptions.js";export*from"./dto/focus.js";export*from"./dto/crons.js";export*from"./dto/config.js";export*from"./dto/attach.js";export*from"./dto/broker.js";export*from"./dto/profiles.js";export*from"./dto/modelauth.js";export*from"./dto/model-config.js";export*from"./dto/canvas.js";export*from"./dto/worktree.js";export*from"./dto/human.js";export*from"./dto/files.js";export*from"./dto/bash.js";export*from"./dto/inbox.js";export*from"./dto/human-requests.js";export*from"./dto/reviews.js";export*from"./dto/review-comments.js";export*from"./dto/chat-inventory.js";export*from"./dto/analytics.js";export*from"./dto/custom-objects.js";export{p as APIError,R as API_VERSION,f as ApiError,e as CrtrClient,A as LONG_POLL_MAX_WAIT_SECONDS,S as LONG_POLL_ROUTES,N as MINIMUM_RUNTIME_VERSION_HEADER,s as RUNTIME_VERSION_HEADER,O as compareReleaseVersions,i as errorCodes,x as isErrorBody,u as longPollHoldSeconds,L as longPollRoute,a as minimumRuntimeRefusal,M as requestedWait,_ as routes,t as waitForDaemonAvailability};
1
+ import{CrtrClient as e,deadlineExceeded as t,MAX_TIMER_MS as m,sleepWithin as p,waitForDaemonAvailability as f}from"./client.js";import{APIError as E,ApiError as i,isErrorBody as l}from"./errors.js";import{errorCodes as R}from"./error-codes.js";import{API_VERSION as n,routes as s}from"./routes.js";import{RUNTIME_VERSION_HEADER as d,MINIMUM_RUNTIME_VERSION_HEADER as A,compareReleaseVersions as M,minimumRuntimeRefusal as N}from"./runtime-version.js";import{LONG_POLL_MAX_WAIT_SECONDS as S,LONG_POLL_ROUTES as u,longPollHoldSeconds as L,longPollRoute as P,requestedWait as T}from"./long-poll.js";export*from"../shared/generated-context.js";export*from"./dto/common.js";export*from"./dto/health.js";export*from"./dto/limits.js";export*from"./dto/grants.js";export*from"./dto/usage.js";export*from"./dto/nodes.js";export*from"./dto/node-outcomes.js";export*from"./dto/node-events.js";export*from"./dto/run-events.js";export*from"./dto/broker-signals.js";export*from"./dto/bash-jobs.js";export*from"./dto/messages.js";export*from"./dto/reports.js";export*from"./dto/structured-requests.js";export*from"./dto/lifecycle.js";export*from"./dto/subscriptions.js";export*from"./dto/focus.js";export*from"./dto/crons.js";export*from"./dto/config.js";export*from"./dto/attach.js";export*from"./dto/broker.js";export*from"./dto/profiles.js";export*from"./dto/modelauth.js";export*from"./dto/model-config.js";export*from"./dto/canvas.js";export*from"./dto/worktree.js";export*from"./dto/human.js";export*from"./dto/files.js";export*from"./dto/bash.js";export*from"./dto/inbox.js";export*from"./dto/human-requests.js";export*from"./dto/reviews.js";export*from"./dto/review-comments.js";export*from"./dto/chat-inventory.js";export*from"./dto/analytics.js";export*from"./dto/custom-objects.js";export{E as APIError,n as API_VERSION,i as ApiError,e as CrtrClient,S as LONG_POLL_MAX_WAIT_SECONDS,u as LONG_POLL_ROUTES,m as MAX_TIMER_MS,A as MINIMUM_RUNTIME_VERSION_HEADER,d as RUNTIME_VERSION_HEADER,M as compareReleaseVersions,t as deadlineExceeded,R as errorCodes,l as isErrorBody,L as longPollHoldSeconds,P as longPollRoute,N as minimumRuntimeRefusal,T as requestedWait,s as routes,p as sleepWithin,f as waitForDaemonAvailability};
@@ -28,20 +28,38 @@ if (run.kind === 'result') console.log(run.output_parsed.root_cause);
28
28
 
29
29
  ## Go live with an app key
30
30
 
31
- Once you have installed the SDK and tried the local flow in [[crouter-sdk/getting-started]], generate the app's key on your server:
31
+ Once you have installed the SDK and tried the local flow in [[crouter-sdk/getting-started]], register the app with `crtr cloud apps register` (or the console), then generate its key and pull its settings on your server:
32
32
 
33
33
  ```bash
34
34
  npx @north-light/sdk keygen --app <client id>
35
+ npx @north-light/sdk env --app <client id>
35
36
  ```
36
37
 
37
- `keygen` saves the private JWK to `local/app-key.json` (mode `0600`); keep that file out of git. With `--app`, it offers to add the public key to your registered app through your own `crtr`, as an additional key that never replaces the current one. Otherwise, click **Add key** on the app's **Sign-in** tab and paste the public JWK printed on stdout (or paste it under **Register an app** for a new app). An app authenticates only with its key; the directory issues no client secret. On your server, load the saved key and pass it as `privateKey`:
38
+ `keygen` saves the private JWK to `local/app-key.json` (mode `0600`); keep that file out of git. With `--app`, it offers to add the public key to your registered app through your own `crtr`, as an additional key that never replaces the current one. Otherwise, click **Add key** on the app's **Sign-in** tab and paste the public JWK printed on stdout (or paste it under **Register an app** for a new app). An app authenticates only with its key; the directory issues no client secret.
39
+
40
+ `env` reads the registration through `crtr cloud apps get` and writes `CROUTER_ISSUER`, `CROUTER_CLIENT_ID` and `APP_ORIGIN` into `local/app.env` (`--out` picks another file), keeping every other line. Both commands need this machine's `crtr` joined to your crouter cloud account; on a machine that isn't, they print the join step and exit 2. An app is configured from four variables:
41
+
42
+ | Variable | Value |
43
+ |---|---|
44
+ | `CROUTER_ISSUER` | The directory's issuer URL |
45
+ | `CROUTER_CLIENT_ID` | The app's client id |
46
+ | `APP_ORIGIN` | The app's origin; its redirect URI is `${APP_ORIGIN}/auth/callback` by convention |
47
+ | `CROUTER_KEY_FILE` | Path to the private JWK, default `local/app-key.json`. `env` never writes it |
48
+
49
+ The SDK reads no environment itself: load the values in your own config and pass them to `OAuth2Client`.
38
50
 
39
51
  ```ts
40
52
  import {readFile} from 'node:fs/promises';
41
53
  import {OAuth2Client} from '@north-light/sdk';
42
54
 
43
- const privateKey = JSON.parse(await readFile('local/app-key.json', 'utf8'));
44
- const oauth = new OAuth2Client({clientId, issuer, redirectUri, privateKey, store}); // store: your ConnectionStore
55
+ const privateKey = JSON.parse(await readFile(process.env.CROUTER_KEY_FILE ?? 'local/app-key.json', 'utf8'));
56
+ const oauth = new OAuth2Client({
57
+ clientId: process.env.CROUTER_CLIENT_ID!,
58
+ issuer: process.env.CROUTER_ISSUER!,
59
+ redirectUri: `${process.env.APP_ORIGIN}/auth/callback`,
60
+ privateKey,
61
+ store, // your ConnectionStore
62
+ });
45
63
  const {url, state, codeVerifier, nonce} = await oauth.authorizeUrl({scopes: ['openid', 'crtr:llm', 'crtr:act'], state: sessionState});
46
64
  // Save state, codeVerifier, nonce in the user's session, then redirect to url.
47
65
  const connection = await oauth.exchangeCode({code: callbackCode, state: callbackState, expectedState: state, codeVerifier, nonce});
@@ -201,6 +201,20 @@ await watcher.stop();
201
201
  - **`stop()`** stops sweeping, aborts every open read, and resolves once every turn has ended; no handler starts afterwards.
202
202
  - **One person, one request.** Without the watcher, `runtime.grant.get({wait: 25, since: grant.version})` holds a single read the same way; the request timeout is extended by `wait`. `wait` outside integer 1–25 throws `RangeError` before sending.
203
203
 
204
+ ### Show what a person's month has cost
205
+
206
+ A person's grant caps what your app may spend of their AI subscription each UTC month (`grant.spend_cap`; crouter cloud sets $50). `usage.get()` answers where the month stands:
207
+
208
+ ```ts
209
+ const usage = await runtime.usage.get(); // or {month: '2026-09'}
210
+ // {grantee, month, usd_equivalent, by_model, app_paid, spend_cap, reset_at}
211
+ ```
212
+
213
+ - **`usd_equivalent`** is user-paid spend at list price: model completions and provider receipts. Receipts have no `by_model` row, so it can exceed the sum of `by_model`. A model with no list price reports `usd_equivalent: null` on its row and adds nothing.
214
+ - **`app_paid`** is usage your app pays for. It never counts toward `spend_cap`.
215
+ - **At the cap.** Once `usd_equivalent` reaches `spend_cap`, user-paid model and provider calls answer 402 `cap_exhausted` (a `RateLimitError` with `retryable: false`, `userAction: 'raise_cap'` and `resetAt`) until `reset_at`, the first instant of the next UTC month. A run whose model call is refused ends with outcome reason `cap_exhausted`. The check runs before each call, so the call that crosses the cap completes and the next one is refused. Do not retry; tell the person when the count resets.
216
+ - `spend_cap: null` means the grant is not capped.
217
+
204
218
  ### Link to the app's settings on crouter cloud
205
219
 
206
220
  Each person who uses your app has a page on their crouter cloud account where they change or remove what the app may do. Link to it with the URL the directory advertises; never compose the path yourself:
@@ -211,7 +225,7 @@ const href = await oauth.accountAppUrl(); // e.g. https://<directory>/account/ap
211
225
 
212
226
  - **What it is.** The discovery document's `account_app_uri_template` with its one `{client_id}` replaced by your encoded client id. It is the same URL for every person: the directory shows whoever is signed in to it their own page for your app (and sends someone without a directory session through sign-in first). Someone without an active grant, a sign-in-only visitor for one, sees that the app is not connected to their account.
213
227
  - **What it needs.** No user and no connection, so it serves owners and visitors alike. Its only request is the discovery read that `authorizeUrl`, `verifyIdToken` and `revoke` share.
214
- - **Errors.** Each is a `CrouterError` (code `crouter_error`) whose message names the fault: the directory advertises no `account_app_uri_template`, the template does not contain `{client_id}` exactly once, or the expansion is not a URL on the issuer's origin. A discovery read that fails throws `Directory discovery failed: <status>` or the fetch error; one that takes longer than 10 seconds throws the timeout. A failed or timed-out read is not kept, so the next call reads again; a successful one is kept for the client's life, so a template the directory changes reaches your app at its next start.
228
+ - **Errors.** Each is a `CrouterError` (code `crouter_error`) whose message names the fault: the directory advertises no `account_app_uri_template`, the template does not contain `{client_id}` exactly once, the expansion is not a URL on the issuer's origin, or an endpoint field of the discovery document (`authorization_endpoint`, `token_endpoint`, `jwks_uri`, `revocation_endpoint`) is not an absolute https URL (`http:` when the issuer is `http:`) or `token_endpoint_auth_methods_supported` is not an array of strings. A discovery read that fails throws `Directory discovery failed: <status>` or the fetch error; one that takes longer than `timeoutMs` (default 10 seconds) throws `DeadlineExceededError`. A failed or timed-out read is not kept, so the next call reads again; a successful one is kept for the client's life, so a template the directory changes reaches your app at its next start.
215
229
  - **Warm it at startup and bound it on page loads.** Call `void oauth.accountAppUrl().catch(log)` when the process starts, so the discovery read is usually done before the first page needs it. A page that must not wait on the directory races it against a short timer (a couple of seconds, under the SDK's 10 s) and hides the link when the race is lost; the read keeps running and a later page load reuses its result.
216
230
 
217
231
  ### Testing your app
@@ -278,10 +292,27 @@ await client.nodes.retrieve(id, {
278
292
  | `signal` | `AbortSignal` | Aborts the underlying `fetch`. Raises `APIUserAbortError`. |
279
293
  | `timeout` | `number` (ms) | Overrides the client `timeout`. |
280
294
  | `maxRetries` | `number` | Overrides the client `maxRetries`. |
295
+ | `budgetMs` | `number` (ms) | One overall deadline for the call, from now. It covers every attempt, every retry wait (a server's `Retry-After` too) and the response body; `timeout` still bounds each attempt but never past it. See [One deadline per call](#one-deadline-per-call). |
296
+ | `deadlineAt` | `number` (epoch ms) | The same deadline as an absolute instant, for passing one deadline to several calls. With `budgetMs` too, the earlier wins. |
281
297
  | `headers` | `Record<string, string>` | Merged over `defaultHeaders`, client `headers`, and the token-generated header; a matching name, including `Authorization`, wins. |
282
298
 
283
299
  `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 [[crouter-sdk/streaming]].
284
300
 
301
+ ### One deadline per call
302
+
303
+ Without `budgetMs`/`deadlineAt` a call has one `timeout` per attempt, so a keyed send (`runs.start`, `runs.message`) that is retried twice can take three timeouts plus the waits between them, and a server's `retry_after_s: 120` is waited out in full. Give the call a deadline and its total time is bounded:
304
+
305
+ ```ts
306
+ await client.runs.message(runId, text, { budgetMs: 20_000 });
307
+ ```
308
+
309
+ - The deadline is fixed once, when the call starts, and every attempt, every backoff, every `retry_after_s`/`Retry-After` wait, the token fetch and the refresh after a 401 share it. The call rejects at the deadline (plus the scheduling of one timer) whether the time goes to the headers, the body, a retry wait or the token hook, even when your `fetch` ignores its `AbortSignal`. The compound helpers (`runs.wait`, `runs.parse`, `nodes.waitForOutcome`, `nodes.createAndWait`, `nodes.parse`) fix one deadline for the whole helper, not one per poll.
310
+ - A retry wait that would reach the deadline is not slept: the call rejects at once with `DeadlineExceededError` (`code: 'deadline_exceeded'`, status 504). `reason` is `'retry_after'` when the server asked for a wait longer than the time left (`retryAfterRequestS` is that request), and `'elapsed'` when time simply ran out; `deadlineAt` is the instant, and `cause` is the last refusal.
311
+ - A stream (`nodes.stream`, `nodes.events`): `budgetMs` / `deadlineAt` bound opening it (the create or retrieve, the token hook and the connection); once open it lives until it ends or its `signal` aborts.
312
+ - `budgetMs` and `deadlineAt` must fit a timer: at most 2,147,483,647 ms (about 24.8 days) from now, or the call throws `RangeError`.
313
+ - **Holding a lease while you call.** An app that takes a lease or lock for an SDK call sets `budgetMs` and keeps the lease longer than it, with margin, so the call has ended or been abandoned before the lease can be taken over: `LEASE_MS >= BUDGET_MS + MARGIN_MS`. State that inequality next to the constants (or assert it at module load) rather than renewing the lease while a call is unbounded.
314
+ - `OAuth2Client` gives each directory operation (discovery, a code exchange or refresh, a revocation, the JWKS read behind `verifyIdToken`) the same kind of deadline, `OAuth2Options.timeoutMs` (default 10 s). A refresh runs under the user's store lock, so the lock is held no longer than `timeoutMs` plus the store's own time.
315
+
285
316
  ## What is not on the client
286
317
 
287
318
  | Convention | Why it is absent |
@@ -34,6 +34,7 @@ Every class below extends `APIError`, which carries `status`, `code`, `message`,
34
34
  | HTTP 409 | `ConflictError` |
35
35
  | HTTP 413, 422 | `UnprocessableEntityError` |
36
36
  | HTTP 429 | `RateLimitError` |
37
+ | Code `deadline_exceeded` | `DeadlineExceededError` (extends `APIConnectionTimeoutError`; status 504; `reason` `elapsed` or `retry_after`, `deadlineAt`, `cause`) |
37
38
  | HTTP 504 or code `request_timeout` | `APIConnectionTimeoutError` (extends `APIConnectionError`) |
38
39
  | Other HTTP ≥ 500 | `InternalServerError` |
39
40
  | Code `daemon_unavailable`, `transport_error`, `daemon_request_interrupted`, or `daemon_health_unavailable`; or a non-API transport failure | `APIConnectionError` |
@@ -106,6 +107,8 @@ try {
106
107
  const outcome = await client.nodes.waitForOutcome(runId);
107
108
  ```
108
109
 
110
+ A call with `budgetMs` (or `deadlineAt`) has one deadline over all of these attempts and waits, a server's `Retry-After` included: a wait longer than the time left rejects at once with `DeadlineExceededError` instead of sleeping (see [[crouter-sdk/client]]). Without one, a keyed send honours any `retry_after_s`.
111
+
109
112
  Override the policy per request with `{ maxRetries: 0 }` or `{ maxRetries: 5 }`. Raising it on a `POST` other than `runs.start` and `runs.message` still does not make the client retry that `POST`.
110
113
 
111
114
  ## In-repo callers
@@ -174,20 +174,42 @@ An outcome is returned, never thrown — including a decline and a failure. Only
174
174
 
175
175
  ## Go live with an app key
176
176
 
177
- Register your app in the directory console, then generate its signing key on your app server:
177
+ Register your app with `crtr cloud apps register` (or in the directory console), then generate its signing key and pull its settings on your app server:
178
178
 
179
179
  ```bash
180
180
  npx @north-light/sdk keygen --app <client id>
181
+ npx @north-light/sdk env --app <client id>
181
182
  ```
182
183
 
183
- This creates `local/app-key.json` with mode `0600` (keep it out of git) and prints the public JWK on stdout. With `--app`, `keygen` offers to add that public key to your app through your own `crtr` (`crtr cloud apps keys add`). It asks first and `--yes` skips the question; with no terminal to ask on (CI, piped input) and no `--yes`, it adds nothing. The upload only ever adds a key: it never replaces or retires the key your app already has. If `crtr` isn't installed or the add fails, `keygen` says why and prints the manual steps instead: click **Add key** on your app's **Sign-in** tab and paste the public JWK, or paste it under **Register an app** for a new app. An app authenticates only with its key: the directory issues no client secret. Use the saved private JWK for the OAuth connect flow:
184
+ `keygen` creates `local/app-key.json` with mode `0600` (keep it out of git) and prints the public JWK on stdout. With `--app`, `keygen` offers to add that public key to your app through your own `crtr` (`crtr cloud apps keys add`). It asks first and `--yes` skips the question; with no terminal to ask on (CI, piped input) and no `--yes`, it adds nothing. The upload only ever adds a key: it never replaces or retires the key your app already has. If `crtr` isn't installed or the add fails, `keygen` says why and prints the manual steps instead: click **Add key** on your app's **Sign-in** tab and paste the public JWK, or paste it under **Register an app** for a new app. An app authenticates only with its key: the directory issues no client secret.
185
+
186
+ `env` runs `crtr cloud apps get --app-id <client id>` and writes `CROUTER_ISSUER`, `CROUTER_CLIENT_ID`, and `APP_ORIGIN` into `local/app.env` (`--out <path>` writes another file). It updates those three lines in place, keeps every other line and comment, and creates the file with mode `0600` when it doesn't exist. It never writes `CROUTER_KEY_FILE` or anything secret. It prints the values it set and the key file your app will read.
187
+
188
+ Both commands reach your app through this machine's `crtr`, so the machine must be joined to your crouter cloud account. On one that isn't, they say so, print the join step (create a join token on `/account/machines`, then `crtr sys join`), and exit 2.
189
+
190
+ An app built on the SDK is configured from four variables:
191
+
192
+ | Variable | Value | Where it comes from |
193
+ |---|---|---|
194
+ | `CROUTER_ISSUER` | The directory's issuer URL | `env` |
195
+ | `CROUTER_CLIENT_ID` | The app's client id | `env` |
196
+ | `APP_ORIGIN` | The app's origin. Its redirect URI is `${APP_ORIGIN}/auth/callback` by convention | `env` (the registration's origin) |
197
+ | `CROUTER_KEY_FILE` | Path to the private JWK, default `local/app-key.json` | You; it differs per deployment (a container might read `/run/secrets/app_key`) |
198
+
199
+ The SDK reads no environment and has no `fromEnv` helper: your app loads the values and passes them to `OAuth2Client`:
184
200
 
185
201
  ```ts
186
202
  import {readFile} from 'node:fs/promises';
187
203
  import {OAuth2Client} from '@north-light/sdk';
188
204
 
189
- const privateKey = JSON.parse(await readFile('local/app-key.json', 'utf8'));
190
- const oauth = new OAuth2Client({clientId, issuer, redirectUri, privateKey, store}); // store: your ConnectionStore
205
+ const config = {
206
+ issuer: process.env.CROUTER_ISSUER!,
207
+ clientId: process.env.CROUTER_CLIENT_ID!,
208
+ origin: process.env.APP_ORIGIN!,
209
+ keyFile: process.env.CROUTER_KEY_FILE ?? 'local/app-key.json',
210
+ };
211
+ const privateKey = JSON.parse(await readFile(config.keyFile, 'utf8'));
212
+ const oauth = new OAuth2Client({clientId: config.clientId, issuer: config.issuer, redirectUri: `${config.origin}/auth/callback`, privateKey, store}); // store: your ConnectionStore
191
213
  const {url, codeVerifier, state, nonce} = await oauth.authorizeUrl({scopes: ['openid', 'crtr:llm', 'crtr:act'], state: sessionState});
192
214
  // Save codeVerifier, state, nonce in the user's session; redirect them to url.
193
215
  const connection = await oauth.exchangeCode({code: callbackCode, codeVerifier, state: callbackState, expectedState: state, nonce});
@@ -9,6 +9,29 @@ when-and-why-to-read: When you need Release notes, this knowledge should be read
9
9
 
10
10
  Changes to `@north-light/sdk` that can change what an existing app sees. Newest first.
11
11
 
12
+ ## 0.3.415
13
+
14
+ ### Monthly spend cap enforced; `usage.get` (additive; one behavior change)
15
+
16
+ - **Behavior change:** user-paid model calls (`/v1/llm`) and provider calls now count against the grant's `spend_cap` (from `grant.get()`; crouter cloud sets $50) for the current UTC month. Once the month reaches it, the next call answers 402 `cap_exhausted` (a `RateLimitError`, `retryable: false`, `userAction: 'raise_cap'`, `resetAt` the first instant of next month) before any model or provider is called. A run whose model call is refused this way ends with outcome reason `cap_exhausted`. The check runs before each call, so the call that crosses the cap still completes. App-paid usage never counts. A grant with `spend_cap: null` is not capped.
17
+ - New `client.usage.get({month?})` (`month` is `YYYY-MM`, UTC; default the current month) answers `SpendUsageDTO`: `usd_equivalent` (user-paid spend, provider receipts included), `by_model`, `app_paid`, `spend_cap` and `reset_at`. See [[crouter-sdk/client]].
18
+ - A completion's `usage.usd_equivalent` is now the served model's list-price cost; before it was always `null`. It stays `null` for a model with no list price, and that usage adds nothing to the month.
19
+
20
+ ### `npx @north-light/sdk env` (additive); keygen on an unjoined machine exits 2
21
+
22
+ - New `npx @north-light/sdk env --app <client id> [--out <path>]` writes `CROUTER_ISSUER`, `CROUTER_CLIENT_ID` and `APP_ORIGIN` from `crtr cloud apps get` into `local/app.env`, keeping every other line. See [[crouter-sdk/getting-started]].
23
+ - `keygen --app` on a machine whose `crtr` isn't joined to a crouter cloud account now prints the join step and the manual steps and exits 2. A missing `crtr` keeps its message and exit 0.
24
+
25
+ ### One deadline per SDK call, and a deadline on every OAuth/JWKS fetch (additive, with one behaviour change)
26
+
27
+ Not breaking: no type or wire shape an app reads is removed or changed. Apps pinned to the previous release keep working unchanged.
28
+
29
+ - `RequestOptions` gains `budgetMs` and `deadlineAt`: one absolute deadline for the call covering every attempt, every retry wait (including a `Retry-After`) and the token fetch/refresh. A wait that would pass the deadline rejects at once with the new `DeadlineExceededError` (`code: 'deadline_exceeded'`, `reason: 'elapsed' | 'retry_after'`, `deadlineAt`, `cause`), which extends `APIConnectionTimeoutError`, so code catching that still catches it. A transport that ignores its `AbortSignal` can no longer outlive the deadline. Without `budgetMs`/`deadlineAt` nothing about retries changes. See [[crouter-sdk/client]].
30
+ - An app that holds a lease or lock while it makes an SDK call should pass `budgetMs` and keep the lease longer than it, with margin, and state that in code.
31
+ - `OAuth2Options.timeoutMs` (default 10 s): one deadline per directory operation (discovery, token exchange, refresh, revoke, `runtime_provisioning` retries and their `retry_after_s`). A refresh under the connection-store lock now holds the lock at most `timeoutMs`. `JwksCache` (and so `verifyIdToken`) takes `{timeoutMs}` (default 10 s) over its discovery and key-set reads together.
32
+ - **Behaviour change:** these directory fetches used to have no deadline (only discovery had one). A directory that takes longer than 10 s now fails with `DeadlineExceededError` where it used to wait; set `timeoutMs` to change it.
33
+ - `OAuth2Client` validates the discovery document: `authorization_endpoint`, `token_endpoint`, `jwks_uri` (and `revocation_endpoint`, when present) must be absolute https URLs (`http:` when the issuer is `http:`), and `token_endpoint_auth_methods_supported` an array of strings. A wrong type is a `CrouterError` naming the field instead of a later `TypeError`.
34
+
12
35
  ## 0.3.409
13
36
 
14
37
  ### `@north-light/sdk/testing`: isolated runs start, `FakeCall.runId`, one `runs` shape (one breaking change)
@@ -34,7 +34,11 @@ const client = await oauth.client('user:o1'); // first call refres
34
34
 
35
35
  `cloud.crouter(userId)` returns a real `Crouter` on that person's runtime with a fresh token and no refresh. Refusals and refreshes never reach it. Use it only for code whose production signature takes a `Crouter`. Code that goes through `OAuth2Client` in production should go through `oauth.client` in tests too.
36
36
 
37
- Options: `new FakeCloud({clientId, issuer = 'https://directory.test', runtimeUrl = 'https://runtime.test' (or (userId) => url), runtimeId = 'runtime:test', runtimeClientId = 'runtime-client', runtimeVersion?, streamIdleMs = 60000, accountAppUriTemplate = '<issuer>/account/apps/{client_id}' (null omits it), runNotice?, isolatedRuns = 'available'})`.
37
+ Options: `new FakeCloud({clientId, issuer = 'https://directory.test', runtimeUrl = 'https://runtime.test' (or (userId) => url), runtimeId = 'runtime:test', runtimeClientId = 'runtime-client', runtimeVersion?, streamIdleMs = 60000, accountAppUriTemplate = '<issuer>/account/apps/{client_id}' (null omits it), runNotice?, isolatedRuns = 'available', clock = Date.now})`.
38
+
39
+ `clock` is `{now(): number}`; every timestamp the fake writes reads it (run `created_at` and `settled_at`, grant `exp`, memory `created_at`/`updated_at`, access-token `iat`/`exp`, `calls[].at`, `directoryCalls[].at`) and the idempotency window runs on it. Pass `manualClock()` (from `@north-light/sdk/testing`; `now()`, `advance(ms)`) to step time by hand. Stream idle, long-poll waits and run-notice retries stay on real timers.
40
+
41
+ `cloud.activate(userId, {scopes?, dependencies?})` makes the grant active with no stored connection, for an app that mints a per-user runtime token itself. The fake directory serves that mint at `POST <issuer>/token` with `grant_type=client_credentials&sub=<userId>` (the client assertion is not verified): 200 `{access_token, token_type: 'Bearer', expires_in: 600, runtimeUrl}` (the token's `exp` claim is 600 s after `iat`; the fake runtime does not enforce token expiry); a missing `sub` is 400 `unauthorized_client`; an unknown or revoked user is 400 `invalid_grant` (`no active grant`); `user.refuse('runtime_provisioning')` is 400 `invalid_grant` with `error_description: runtime_provisioning`. A mint is a `directory.token` call (`userId` is the `sub`), so `failNext('directory.token', …, {user})` applies to it.
38
42
 
39
43
  ### Isolated runs
40
44
 
@@ -73,7 +77,9 @@ What a turn's input is: `reply` and `failTurn` deliver `messageIds` (default: ev
73
77
 
74
78
  Messages follow the runtime: `start_turn` true makes the run `running` at once and calls the responder; `start_turn` false only records it. A message to a run settled `canceled` or `deadline_exceeded` is refused 409 `run_settled`; to a run settled any other way it reopens the run. A deleted run answers 404.
75
79
 
76
- Idempotency follows the runtime too: the same `Idempotency-Key` with the same body replays the first answer (no new run, no responder call); the same key with a different body, or reused between `run.start` and `run.message`, is 409 `idempotency_conflict`. After a run is deleted, its start key creates a new run.
80
+ Idempotency follows the runtime too: the same `Idempotency-Key` with the same body replays the first answer (no new run, no responder call); the same key with a different body, or reused between `run.start` and `run.message`, is 409 `idempotency_conflict`. After a run is deleted, its start key creates a new run. A key is live for 24 hours (`IDEMPOTENCY_WINDOW_MS`): at exactly the window it reads as absent, and the next accept replaces the record. Step the window with `clock`.
81
+
82
+ `run.openStreams` counts the run's open event streams. It rises when the SDK starts reading the response body, which is after `cloud.calls` records `run.events`, so poll `openStreams`, not `calls`, to know a stream is attached. It falls on idle, error, abort or revoke. Run notices are owed only while no stream is open.
77
83
 
78
84
  ## Grants and setup state
79
85
 
@@ -160,6 +166,6 @@ Build two app instances (two `OAuth2Client`s) on the same `cloud`. They see the
160
166
 
161
167
  ## Fidelity
162
168
 
163
- FakeCloud is checked against the real runtime on every crouter release: one set of conformance scenarios runs on the fake and on the real app listener (run start and idempotency, messages, reply streams, long-poll, every settle outcome, delete, the event stream's edges, grant holds, memory, providers, run notices). Two behaviours are fake-only because the runtime cannot be scripted that way: a successful provider call (the real side needs an installed provider), and `setActivity`. An isolated run that starts is checked only against `startRun`'s documented checks: the real listener in the suite runs without the Linux root launcher, so on it the `isolatedRuns: 'unavailable'` refusal is what is conformance-checked.
169
+ FakeCloud is checked against the real runtime on every crouter release: one set of conformance scenarios runs on the fake and on the real app listener (run start and idempotency, messages, reply streams, long-poll, every settle outcome, delete, the event stream's edges, grant holds, memory, providers, run notices). The keyed-write scenarios K1–K8 (equal-key replay, conflict, concurrent equal keys accepted once, the 24h window, replay after cancel, a key bound to its operation, keys scoped to the grantee, concurrent notes) are exported as `@north-light/sdk/testing/conformance` (`keyedWriteScenarios`, `IDEMPOTENCY_WINDOW_MS`) and run on FakeCloud and on the real daemon; an app test double for the runtime should run them against its own target. Two behaviours are fake-only because the runtime cannot be scripted that way: a successful provider call (the real side needs an installed provider), and `setActivity`. An isolated run that starts is checked only against `startRun`'s documented checks: the real listener in the suite runs without the Linux root launcher, so on it the `isolatedRuns: 'unavailable'` refusal is what is conformance-checked.
164
170
 
165
171
  Not faked: sign-in (`authorizeUrl`, `exchangeCode`, ID tokens — start from `cloud.connect`), token revocation, the owner and local-daemon surface (`nodes`, `canvas`, `crons`, `files`, …), and several apps on one cloud. `trace.get`, `run.rename`, `run.interrupt` and `profile.list` answer the right shape only.
@@ -26,6 +26,10 @@ The **daemon** (`crtrd`) is the sole owner of canvas persistent state: it is the
26
26
 
27
27
  Navigate/steer: `surface node focus` (put an attach viewer for a node in your pane), `surface attach` (connect to a node's existing broker), `surface node cycle` (DFS-walk neighbors), `surface node id` (copy the current node id), `node message send` (direct-message any node at a wake tier, reviving a dormant target), `node subscription add`/`node subscription remove` (wire edges spawn didn't create). Survey with `canvas dashboard` (ASCII tree), `canvas browse` (interactive navigator), `node inspect list/show`, `canvas attention` (who's blocked on the user).
28
28
 
29
+ ## The node's shell environment
30
+
31
+ A node's shell is not the person's. `HOME` depends on the app and execution mode: the terminal app on macOS gets the person's files root, any other app gets its app home, and a node of an isolated run gets its own `nodes/<id>/home` with no network and no shared home or files mount. Hook cwds are checked against the node's mounts, so a path outside them (`/tmp`, say) is refused. This isolation is intended. So **a path you hand to another node — in a brief, a message, a cron command — must be an absolute host path, never `~`-relative**: `~` resolves to the reader's `HOME`, not yours. A node's scripts and logs go in `$CRTR_CONTEXT_DIR`; a cron anchored to a node (`--anchor`, `--anchor-self`) gets that node's `$CRTR_CONTEXT_DIR` when it fires, and an unanchored cron gets none.
32
+
29
33
  ## Watches — how events reach a node
30
34
 
31
35
  Every object emits events, each with a level (1–10), and a **watch** routes an object's events to a watcher. Reading an object, writing or editing a document, messaging a node, spawning a node or starting a job makes you a watcher of it, so a document you read can steer you when it is edited and a child you spawn wakes you when it finishes. The watcher's listening configuration decides how an event arrives — interrupt, steer, after the current turn, or at the next turn — by level (by default 10 interrupts, 7–9 steer, 5–6 wait for the turn to end, 3–4 wait for the next turn). `crtr canvas watch <name> [--pattern … --listen …]` adds a watch without reading; `crtr canvas unwatch <name>` removes one — do it when an object no longer bears on your work so its events stop arriving. `crtr canvas edges <your id> --type watches --direction out` lists everything you watch. Node events include `report.update`/`report.final` (from `push`), `messaged`, and `notice.crash`/`close`; a job emits `exited`/`killed`; a document emits `edited`/`deleted`.