@north-light/crouter-api 0.3.232 → 0.3.234

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.
@@ -9,6 +9,7 @@
9
9
  //
10
10
  // TRANSPORT (spec O-1): `node:http`/`node:https` `request()` — NO `undici`.
11
11
  // unix socket via `{ socketPath }`; TCP/remote via a parsed `baseUrl`.
12
+ import { envHomeOverride } from '../shared/env.js';
12
13
  import { request as httpRequest } from 'node:http';
13
14
  import { request as httpsRequest } from 'node:https';
14
15
  import { homedir } from 'node:os';
@@ -27,7 +28,7 @@ const SOCKET_BASENAME = 'crtrd.sock';
27
28
  /** Resolve crtrd's default unix socket path the same way `apiSocketPath()` does,
28
29
  * via Node built-ins only (no `core/canvas/paths.ts` import). */
29
30
  function defaultSocketPath() {
30
- const override = process.env['CRTR_HOME'];
31
+ const override = envHomeOverride();
31
32
  const home = override !== undefined && override !== ''
32
33
  ? override
33
34
  : join(homedir(), CRTR_DIR_NAME, 'canvas');
@@ -74,7 +75,7 @@ export class CrtrClient {
74
75
  static forLocalSocket(opts) {
75
76
  return new CrtrClient({ socketPath: defaultSocketPath(), autostart: true, ...opts });
76
77
  }
77
- // ---- Health / status ---------------------------------------------------
78
+ // Health / status
78
79
  healthz() {
79
80
  return this.request('GET', routes.healthz());
80
81
  }
@@ -88,7 +89,7 @@ export class CrtrClient {
88
89
  restartDaemon() {
89
90
  return this.request('POST', routes.daemonRestart());
90
91
  }
91
- // ---- Nodes -------------------------------------------------------------
92
+ // Nodes
92
93
  createNode(req) {
93
94
  return this.request('POST', routes.nodes(), req);
94
95
  }
@@ -178,7 +179,7 @@ export class CrtrClient {
178
179
  subscribe(id, req) {
179
180
  return this.request('POST', routes.nodeSubscriptions(this.nodePath(id)), req);
180
181
  }
181
- // ---- Focuses (viewer registry) ----------------------------------------
182
+ // Focuses (viewer registry)
182
183
  // The tmux verbs that open/move/close a viewer pane run LOCALLY in the
183
184
  // caller's session (placement-tmux); only the canvas.db focus rows route here.
184
185
  // Reads GC lazily server-side, so a returned row is always live; a `null` body
@@ -245,7 +246,7 @@ export class CrtrClient {
245
246
  ensureAttach(id, req) {
246
247
  return this.request('POST', routes.nodeAttach(this.nodePath(id)), req ?? {});
247
248
  }
248
- // ---- Reads / feed ------------------------------------------------------
249
+ // Reads / feed
249
250
  getReports(id, q) {
250
251
  return this.request('GET', withQuery(routes.nodeReports(this.nodePath(id)), q));
251
252
  }
@@ -283,20 +284,20 @@ export class CrtrClient {
283
284
  getContext(id) {
284
285
  return this.request('GET', routes.nodeContext(this.nodePath(id)));
285
286
  }
286
- // ---- Host file read ----------------------------------------------------
287
+ // Host file read
287
288
  /** Read an absolute host path as UTF-8 (capped, `truncated` when clipped) for
288
289
  * the browser file-peek panel. */
289
290
  peekFile(path) {
290
291
  return this.request('GET', withQuery(routes.filePeek(), { path }));
291
292
  }
292
- // ---- Memory documents --------------------------------------------------
293
+ // Memory documents
293
294
  /** Resolve an exact canonical `[[name]]` memory-document link to the winning
294
295
  * document's physical origin for the given node — the node's own precedence
295
296
  * chain, not this process's. Pair with `peekFile` to render the document. */
296
297
  resolveMemoryDoc(name, nodeId) {
297
298
  return this.request('GET', withQuery(routes.memoryResolve(), { name, node: nodeId }));
298
299
  }
299
- // ---- Profiles ----------------------------------------------------------
300
+ // Profiles
300
301
  /** Create-or-return by name. Supplied `projects` are shape-checked even when
301
302
  * the profile already exists; their directories are only required to exist
302
303
  * when this call creates the profile. */
@@ -317,7 +318,7 @@ export class CrtrClient {
317
318
  deleteProfile(name, req) {
318
319
  return this.request('DELETE', routes.profile(name), req);
319
320
  }
320
- // ---- Model auth --------------------------------------------------------
321
+ // Model auth
321
322
  listModelAuth() {
322
323
  return this.request('GET', routes.modelAuths());
323
324
  }
@@ -327,7 +328,7 @@ export class CrtrClient {
327
328
  removeCredential(provider) {
328
329
  return this.request('DELETE', routes.modelAuth(provider));
329
330
  }
330
- // ---- Human bridge + completion-handler forwarding (spec §6.5) ----------
331
+ // Human bridge + completion-handler forwarding (spec §6.5)
331
332
  /** Create a terminal `kind:'human'` bridge node with NO broker engine
332
333
  * (`spawnNode` server-side). Distinct from `createNode` (which launches a
333
334
  * broker) precisely because a human bridge must never have one. */
@@ -344,7 +345,7 @@ export class CrtrClient {
344
345
  cancelHumanTicket(ticketId, body) {
345
346
  return this.request('POST', routes.humanTicketCancel(this.interactionPath(ticketId)), body);
346
347
  }
347
- // ---- Daemon-owned document reviews and comments -----------------------
348
+ // Daemon-owned document reviews and comments
348
349
  createReview(req) {
349
350
  return this.request('POST', routes.humanReviews(), req);
350
351
  }
@@ -433,7 +434,7 @@ export class CrtrClient {
433
434
  cancelHumanInboxTicket(ticketId, request) {
434
435
  return this.request('POST', routes.humanInboxCancel(this.ticketId(ticketId)), request ?? {});
435
436
  }
436
- // ---- Durable programmatic human requests -------------------------------
437
+ // Durable programmatic human requests
437
438
  /** Create one durable human request. Its `request_id` is the same opaque id
438
439
  * the inbox routes address, so the request and the inbox ticket are one
439
440
  * record. An unresolvable `action.name` is rejected before the page is
@@ -474,7 +475,7 @@ export class CrtrClient {
474
475
  }
475
476
  return this.request('POST', routes.humanInboxFeedbackResolve(this.ticketId(ticketId), commentId), { node_id: nodeId });
476
477
  }
477
- // ---- Canvas reads / maintenance ---------------------------------------
478
+ // Canvas reads / maintenance
478
479
  /** Composed client-side from `GET /v1/nodes` + `GET /v1/status` (spec §6.3 —
479
480
  * the dashboard is absorbed into those two reads; there is no single route).
480
481
  * `generated_at` is the client-side capture instant of the composition. */
@@ -527,7 +528,7 @@ export class CrtrClient {
527
528
  rebuildIndex() {
528
529
  return this.request('POST', routes.canvasRebuildIndex(), {});
529
530
  }
530
- // ---- Escape hatch ------------------------------------------------------
531
+ // Escape hatch
531
532
  /** Raw request for routes not yet method-wrapped. Applies the same
532
533
  * autostart + error-mapping semantics. */
533
534
  async request(method, path, body) {
@@ -549,7 +550,7 @@ export class CrtrClient {
549
550
  throw toTransportApiError(err);
550
551
  }
551
552
  }
552
- // ---- internals ---------------------------------------------------------
553
+ // internals
553
554
  nodePath(id) {
554
555
  if (!isSafeNodeId(id)) {
555
556
  throw new ApiError(400, 'invalid_node_id', `invalid node id: ${JSON.stringify(id)}`);
@@ -0,0 +1,68 @@
1
+ /** The current node's id (`CRTR_NODE_ID`), or undefined outside a node. */
2
+ export declare function envNodeId(): string | undefined;
3
+ /** The current node's target profile id (`CRTR_PROFILE_ID`), or undefined. */
4
+ export declare function envProfileId(): string | undefined;
5
+ /** The current node's working directory (`CRTR_NODE_CWD`), or undefined. */
6
+ export declare function envNodeCwd(): string | undefined;
7
+ /** An explicit model-routing override for this turn (`CRTR_MODEL_INTENT`), or
8
+ * undefined. Cleared (not read) via `delete process.env['CRTR_MODEL_INTENT']`
9
+ * in `runtime/broker.ts` — a mutation, not a read, so it stays there. */
10
+ export declare function envModelIntent(): string | undefined;
11
+ /** The raw `CRTR_HOME` override, unresolved. Use `crtrHome()`
12
+ * (`core/canvas/paths.ts`) to get the resolved canvas-home path; use this
13
+ * only when a caller needs the override itself — to check whether one is
14
+ * set, or to propagate it verbatim into a child process's env. */
15
+ export declare function envHomeOverride(): string | undefined;
16
+ /** Dead-provider watchdog timeout (`CRTR_STREAM_WATCHDOG_MS`), 5 minutes by
17
+ * default. See `runtime/stream-watchdog.ts` for what it guards. */
18
+ export declare function envStreamWatchdogMs(): number;
19
+ /** The canvas inbox watcher's poll tick (`CRTR_WATCHER_TICK_MS`). */
20
+ export declare function envWatcherTickMs(): number;
21
+ /** The canvas inbox watcher's debounce window (`CRTR_WATCHER_DEBOUNCE_MS`). */
22
+ export declare function envWatcherDebounceMs(): number;
23
+ /** No-new-message window before a recap is shown (`CRTR_RECAP_IDLE_MS`), 60s
24
+ * by default. */
25
+ export declare function envRecapIdleMs(): number;
26
+ /** Explicit recap-model override (`CRTR_RECAP_MODEL`), trimmed; undefined if
27
+ * unset or blank. */
28
+ export declare function envRecapModelOverride(): string | undefined;
29
+ /** The bash safety-valve deadline (`CRTR_BASH_VALVE_MS`), 5 minutes by
30
+ * default; undocumented test-only override so the E2E valve check doesn't
31
+ * sleep 5 minutes. */
32
+ export declare function envBashValveMs(): number;
33
+ /** Explicit pidfile path override (`CRTR_PIDFILE`, tests only); undefined if
34
+ * unset or blank. */
35
+ export declare function envPidfileOverride(): string | undefined;
36
+ /** The broker engine module spec (`CRTR_BROKER_ENGINE`, the T11 test seam),
37
+ * defaulting to the real SDK. `runtime/host.ts` layers `inv.env` ahead of
38
+ * this (a launch-time override mirrored into the child's env, since
39
+ * spawn-env.ts's default-deny allowlist strips `CRTR_*` from ambient env). */
40
+ export declare function envBrokerEngine(): string;
41
+ /** Test-only pi-binary substitution (`CRTR_PI_BINARY`) so the integration
42
+ * harness can point a real `crtr node new` at a deterministic fake-pi
43
+ * vehicle. Undefined in production. */
44
+ export declare function envPiBinaryOverride(): string | undefined;
45
+ /** Whether implicit daemon autostart is suppressed for this invocation
46
+ * (`CRTR_NO_DAEMON_AUTOSTART=1`). */
47
+ export declare function envNoDaemonAutostart(): boolean;
48
+ /** The `--tcp` fallback for crtrd's opt-in TCP listener (`CRTRD_TCP`), raw
49
+ * (`host:port` or undefined — absent means unix socket only). */
50
+ export declare function envCrtrdTcp(): string | undefined;
51
+ /** Kill switch for the host-exports writer/pruner (`CRTR_NO_EXPORTS=1`). */
52
+ export declare function envNoExports(): boolean;
53
+ /** Verbose-diagnostics gate for otherwise-silent best-effort catches
54
+ * (`CRTR_DEBUG=1`). Distinct from `CRTR_DEBUG_PID_LIVENESS`
55
+ * (`core/canvas/pid.ts`), a different, unrelated var. */
56
+ export declare function envDebug(): boolean;
57
+ /** Live-broker automatic-revive cap override (`CRTR_MAX_LIVE_BROKERS`); wins
58
+ * over `readConfig('user').brokerThresholds.automaticReviveCap` when set. */
59
+ export declare function envMaxLiveBrokers(): number | undefined;
60
+ /** Live-broker warning threshold override (`CRTR_WARN_LIVE_BROKERS`); wins
61
+ * over `readConfig('user').brokerThresholds.warning` when set. */
62
+ export declare function envWarnLiveBrokers(): number | undefined;
63
+ /** Test-only unattended-park interval override (`CRTR_TEST_UNATTENDED_PARK_MS`),
64
+ * gated by `envNoDaemonAutostart()` at the call site; no production default. */
65
+ export declare function envTestUnattendedParkMs(): number | undefined;
66
+ /** Test-only park-summary grace-period override
67
+ * (`CRTR_TEST_PARK_SUMMARY_GRACE_MS`); no production default. */
68
+ export declare function envTestParkSummaryGraceMs(): number | undefined;
@@ -0,0 +1,171 @@
1
+ // The reader-side surface for every environment variable crouter reads back
2
+ // out of `process.env`, outside `core/runtime/spawn-env.ts`'s writer
3
+ // allowlist. That module documents what a broker child is ADMITTED to see;
4
+ // this one documents what crouter itself reads. Two families:
5
+ //
6
+ // - Identity: raw passthrough (`string | undefined`, exactly what
7
+ // `process.env[name]` returns). Every existing call site keeps its own
8
+ // established fallback (`?? ''`, `|| null`, `?? cfg.cwd`, a presence
9
+ // check, ...) — the fallback has always been call-site-owned, not
10
+ // var-owned, so centralizing it here would be a behavior change, not a
11
+ // dedup. `CRTR_HOME` also has a resolved-path accessor, `crtrHome()` in
12
+ // `core/canvas/paths.ts`; that stays the canonical way to get the
13
+ // resolved canvas home; `envHomeOverride()` here is for the handful of
14
+ // sites that want the raw override value itself (to check presence or
15
+ // propagate it verbatim to a child env).
16
+ //
17
+ // - Tuning: parsed, with the default baked into the accessor. Each of
18
+ // these had exactly one parse site before this module existed; the
19
+ // parse logic moved here unchanged so a second site, if one appears,
20
+ // reads the single implementation instead of re-deriving it.
21
+ //
22
+ // `CRTR_DIR_NAME` is NOT here — it is a plain exported string constant
23
+ // (`types.ts`), never read off `process.env`.
24
+ // Identity
25
+ /** The current node's id (`CRTR_NODE_ID`), or undefined outside a node. */
26
+ export function envNodeId() {
27
+ return process.env['CRTR_NODE_ID'];
28
+ }
29
+ /** The current node's target profile id (`CRTR_PROFILE_ID`), or undefined. */
30
+ export function envProfileId() {
31
+ return process.env['CRTR_PROFILE_ID'];
32
+ }
33
+ /** The current node's working directory (`CRTR_NODE_CWD`), or undefined. */
34
+ export function envNodeCwd() {
35
+ return process.env['CRTR_NODE_CWD'];
36
+ }
37
+ /** An explicit model-routing override for this turn (`CRTR_MODEL_INTENT`), or
38
+ * undefined. Cleared (not read) via `delete process.env['CRTR_MODEL_INTENT']`
39
+ * in `runtime/broker.ts` — a mutation, not a read, so it stays there. */
40
+ export function envModelIntent() {
41
+ return process.env['CRTR_MODEL_INTENT'];
42
+ }
43
+ /** The raw `CRTR_HOME` override, unresolved. Use `crtrHome()`
44
+ * (`core/canvas/paths.ts`) to get the resolved canvas-home path; use this
45
+ * only when a caller needs the override itself — to check whether one is
46
+ * set, or to propagate it verbatim into a child process's env. */
47
+ export function envHomeOverride() {
48
+ return process.env['CRTR_HOME'];
49
+ }
50
+ // Tuning
51
+ /** Generic `Number(raw)` parse with a positive-finite guard, shared by every
52
+ * millisecond tuning var below (each previously reimplemented this locally,
53
+ * identically). */
54
+ function parsePositiveMs(raw, fallback) {
55
+ if (raw === undefined)
56
+ return fallback;
57
+ const n = Number(raw);
58
+ return Number.isFinite(n) && n > 0 ? n : fallback;
59
+ }
60
+ const DEFAULT_STREAM_WATCHDOG_MS = 5 * 60_000; // 5 minutes.
61
+ /** Dead-provider watchdog timeout (`CRTR_STREAM_WATCHDOG_MS`), 5 minutes by
62
+ * default. See `runtime/stream-watchdog.ts` for what it guards. */
63
+ export function envStreamWatchdogMs() {
64
+ return parsePositiveMs(process.env['CRTR_STREAM_WATCHDOG_MS'], DEFAULT_STREAM_WATCHDOG_MS);
65
+ }
66
+ const DEFAULT_WATCHER_TICK_MS = 800;
67
+ /** The canvas inbox watcher's poll tick (`CRTR_WATCHER_TICK_MS`). */
68
+ export function envWatcherTickMs() {
69
+ return parsePositiveMs(process.env['CRTR_WATCHER_TICK_MS'], DEFAULT_WATCHER_TICK_MS);
70
+ }
71
+ const DEFAULT_WATCHER_DEBOUNCE_MS = 1000;
72
+ /** The canvas inbox watcher's debounce window (`CRTR_WATCHER_DEBOUNCE_MS`). */
73
+ export function envWatcherDebounceMs() {
74
+ return parsePositiveMs(process.env['CRTR_WATCHER_DEBOUNCE_MS'], DEFAULT_WATCHER_DEBOUNCE_MS);
75
+ }
76
+ const DEFAULT_RECAP_IDLE_MS = 60_000;
77
+ /** No-new-message window before a recap is shown (`CRTR_RECAP_IDLE_MS`), 60s
78
+ * by default. */
79
+ export function envRecapIdleMs() {
80
+ return parsePositiveMs(process.env['CRTR_RECAP_IDLE_MS'], DEFAULT_RECAP_IDLE_MS);
81
+ }
82
+ /** Explicit recap-model override (`CRTR_RECAP_MODEL`), trimmed; undefined if
83
+ * unset or blank. */
84
+ export function envRecapModelOverride() {
85
+ const raw = process.env['CRTR_RECAP_MODEL'];
86
+ return raw !== undefined && raw.trim() !== '' ? raw.trim() : undefined;
87
+ }
88
+ const DEFAULT_BASH_VALVE_MS = 5 * 60 * 1000;
89
+ /** The bash safety-valve deadline (`CRTR_BASH_VALVE_MS`), 5 minutes by
90
+ * default; undocumented test-only override so the E2E valve check doesn't
91
+ * sleep 5 minutes. */
92
+ export function envBashValveMs() {
93
+ return parsePositiveMs(process.env['CRTR_BASH_VALVE_MS'], DEFAULT_BASH_VALVE_MS);
94
+ }
95
+ /** Explicit pidfile path override (`CRTR_PIDFILE`, tests only); undefined if
96
+ * unset or blank. */
97
+ export function envPidfileOverride() {
98
+ const raw = process.env['CRTR_PIDFILE'];
99
+ return raw !== undefined && raw !== '' ? raw : undefined;
100
+ }
101
+ const DEFAULT_BROKER_ENGINE = '@earendil-works/pi-coding-agent';
102
+ /** The broker engine module spec (`CRTR_BROKER_ENGINE`, the T11 test seam),
103
+ * defaulting to the real SDK. `runtime/host.ts` layers `inv.env` ahead of
104
+ * this (a launch-time override mirrored into the child's env, since
105
+ * spawn-env.ts's default-deny allowlist strips `CRTR_*` from ambient env). */
106
+ export function envBrokerEngine() {
107
+ return process.env['CRTR_BROKER_ENGINE'] ?? DEFAULT_BROKER_ENGINE;
108
+ }
109
+ /** Test-only pi-binary substitution (`CRTR_PI_BINARY`) so the integration
110
+ * harness can point a real `crtr node new` at a deterministic fake-pi
111
+ * vehicle. Undefined in production. */
112
+ export function envPiBinaryOverride() {
113
+ return process.env['CRTR_PI_BINARY'];
114
+ }
115
+ /** Whether implicit daemon autostart is suppressed for this invocation
116
+ * (`CRTR_NO_DAEMON_AUTOSTART=1`). */
117
+ export function envNoDaemonAutostart() {
118
+ return process.env['CRTR_NO_DAEMON_AUTOSTART'] === '1';
119
+ }
120
+ /** The `--tcp` fallback for crtrd's opt-in TCP listener (`CRTRD_TCP`), raw
121
+ * (`host:port` or undefined — absent means unix socket only). */
122
+ export function envCrtrdTcp() {
123
+ return process.env['CRTRD_TCP'];
124
+ }
125
+ /** Kill switch for the host-exports writer/pruner (`CRTR_NO_EXPORTS=1`). */
126
+ export function envNoExports() {
127
+ return process.env['CRTR_NO_EXPORTS'] === '1';
128
+ }
129
+ /** Verbose-diagnostics gate for otherwise-silent best-effort catches
130
+ * (`CRTR_DEBUG=1`). Distinct from `CRTR_DEBUG_PID_LIVENESS`
131
+ * (`core/canvas/pid.ts`), a different, unrelated var. */
132
+ export function envDebug() {
133
+ return process.env['CRTR_DEBUG'] === '1';
134
+ }
135
+ // Precedence — broker-supervision.ts's threshold/interval overrides. Each
136
+ // returns the raw parsed value with NO default: the caller (config for the
137
+ // thresholds, a hardcoded production constant for the intervals) supplies
138
+ // the default, and env wins over it when set. Undefined, unparseable, or
139
+ // non-positive all mean "no override" — the caller's own default applies.
140
+ function parsePositiveInteger(raw) {
141
+ if (raw === undefined || raw === '')
142
+ return undefined;
143
+ const n = Number(raw);
144
+ return Number.isSafeInteger(n) && n >= 1 ? n : undefined;
145
+ }
146
+ /** Live-broker automatic-revive cap override (`CRTR_MAX_LIVE_BROKERS`); wins
147
+ * over `readConfig('user').brokerThresholds.automaticReviveCap` when set. */
148
+ export function envMaxLiveBrokers() {
149
+ return parsePositiveInteger(process.env['CRTR_MAX_LIVE_BROKERS']);
150
+ }
151
+ /** Live-broker warning threshold override (`CRTR_WARN_LIVE_BROKERS`); wins
152
+ * over `readConfig('user').brokerThresholds.warning` when set. */
153
+ export function envWarnLiveBrokers() {
154
+ return parsePositiveInteger(process.env['CRTR_WARN_LIVE_BROKERS']);
155
+ }
156
+ function parsePositiveMsNoDefault(raw) {
157
+ if (raw === undefined)
158
+ return undefined;
159
+ const n = Number(raw);
160
+ return Number.isFinite(n) && n > 0 ? n : undefined;
161
+ }
162
+ /** Test-only unattended-park interval override (`CRTR_TEST_UNATTENDED_PARK_MS`),
163
+ * gated by `envNoDaemonAutostart()` at the call site; no production default. */
164
+ export function envTestUnattendedParkMs() {
165
+ return parsePositiveMsNoDefault(process.env['CRTR_TEST_UNATTENDED_PARK_MS']);
166
+ }
167
+ /** Test-only park-summary grace-period override
168
+ * (`CRTR_TEST_PARK_SUMMARY_GRACE_MS`); no production default. */
169
+ export function envTestParkSummaryGraceMs() {
170
+ return parsePositiveMsNoDefault(process.env['CRTR_TEST_PARK_SUMMARY_GRACE_MS']);
171
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/crouter-api",
3
- "version": "0.3.232",
3
+ "version": "0.3.234",
4
4
  "description": "Typed crtrd /v1 API contract — DTOs, route builders, the error contract, and the CrtrClient. Zero runtime dependencies.",
5
5
  "type": "module",
6
6
  "main": "./dist/api/index.js",