@crouter/api 0.3.387 → 0.3.388
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/__tests__/integration/client.test.js +97 -0
- package/dist/api/client.d.ts +7 -0
- package/dist/api/client.js +40 -21
- package/dist/core/asset-root.d.ts +7 -0
- package/dist/core/asset-root.js +18 -0
- package/dist/core/canvas/boot-id.d.ts +6 -0
- package/dist/core/canvas/boot-id.js +26 -0
- package/dist/core/canvas/paths.d.ts +72 -0
- package/dist/core/canvas/paths.js +163 -0
- package/dist/core/canvas/pid.d.ts +391 -0
- package/dist/core/canvas/pid.js +948 -0
- package/dist/core/command-plugins/bundle.d.ts +149 -0
- package/dist/core/command-plugins/bundle.js +588 -0
- package/dist/core/command-plugins/endpoint.d.ts +24 -0
- package/dist/core/command-plugins/endpoint.js +51 -0
- package/dist/core/config.d.ts +233 -0
- package/dist/core/config.js +1120 -0
- package/dist/core/env-name.d.ts +6 -0
- package/dist/core/env-name.js +9 -0
- package/dist/core/errors.d.ts +38 -0
- package/dist/core/errors.js +90 -0
- package/dist/core/events/emit.d.ts +6 -0
- package/dist/core/events/emit.js +42 -0
- package/dist/core/events/envelope.d.ts +2 -0
- package/dist/core/events/envelope.js +84 -0
- package/dist/core/events/errors.d.ts +4 -0
- package/dist/core/events/errors.js +69 -0
- package/dist/core/events/operation-id.d.ts +4 -0
- package/dist/core/events/operation-id.js +24 -0
- package/dist/core/events/serialize.d.ts +4 -0
- package/dist/core/events/serialize.js +199 -0
- package/dist/core/events/source.d.ts +16 -0
- package/dist/core/events/source.js +31 -0
- package/dist/core/events/types.d.ts +68 -0
- package/dist/core/events/types.js +11 -0
- package/dist/core/exclusive-lock.d.ts +34 -0
- package/dist/core/exclusive-lock.js +197 -0
- package/dist/core/fs-utils.d.ts +44 -0
- package/dist/core/fs-utils.js +208 -0
- package/dist/core/help.d.ts +309 -0
- package/dist/core/help.js +406 -0
- package/dist/core/human/page-catalog.d.ts +57 -0
- package/dist/core/human/page-catalog.js +172 -0
- package/dist/core/installed-plugins.d.ts +2 -0
- package/dist/core/installed-plugins.js +79 -0
- package/dist/core/io.d.ts +122 -0
- package/dist/core/io.js +373 -0
- package/dist/core/keybindings/attach-control.d.ts +49 -0
- package/dist/core/keybindings/attach-control.js +42 -0
- package/dist/core/keybindings/catalog.d.ts +18 -0
- package/dist/core/keybindings/catalog.js +257 -0
- package/dist/core/keybindings/types.d.ts +42 -0
- package/dist/core/keybindings/types.js +1 -0
- package/dist/core/layout.d.ts +26 -0
- package/dist/core/layout.js +94 -0
- package/dist/core/locked-file.d.ts +27 -0
- package/dist/core/locked-file.js +118 -0
- package/dist/core/log.d.ts +9 -0
- package/dist/core/log.js +89 -0
- package/dist/core/manifest.d.ts +5 -0
- package/dist/core/manifest.js +15 -0
- package/dist/core/plugin-env.d.ts +8 -0
- package/dist/core/plugin-env.js +31 -0
- package/dist/core/plugin-extensions.d.ts +29 -0
- package/dist/core/plugin-extensions.js +191 -0
- package/dist/core/plugin-swap-lock.d.ts +9 -0
- package/dist/core/plugin-swap-lock.js +31 -0
- package/dist/core/preview-result-path.d.ts +4 -0
- package/dist/core/preview-result-path.js +26 -0
- package/dist/core/profiles/env-store.d.ts +22 -0
- package/dist/core/profiles/env-store.js +163 -0
- package/dist/core/profiles/fuzzy-match.d.ts +19 -0
- package/dist/core/profiles/fuzzy-match.js +92 -0
- package/dist/core/profiles/manifest.d.ts +120 -0
- package/dist/core/profiles/manifest.js +529 -0
- package/dist/core/rate-limit-scope.d.ts +25 -0
- package/dist/core/rate-limit-scope.js +64 -0
- package/dist/core/render.d.ts +12 -0
- package/dist/core/render.js +138 -0
- package/dist/core/resolver.d.ts +14 -0
- package/dist/core/resolver.js +111 -0
- package/dist/core/runtime/branded-host.d.ts +25 -0
- package/dist/core/runtime/branded-host.js +264 -0
- package/dist/core/runtime/broker/daemon-ops.d.ts +65 -0
- package/dist/core/runtime/broker/daemon-ops.js +177 -0
- package/dist/core/runtime/broker/signal-stream.d.ts +30 -0
- package/dist/core/runtime/broker/signal-stream.js +149 -0
- package/dist/core/scope.d.ts +32 -0
- package/dist/core/scope.js +184 -0
- package/dist/core/scoped-state/db.d.ts +17 -0
- package/dist/core/scoped-state/db.js +247 -0
- package/dist/core/scoped-state/migrate.d.ts +8 -0
- package/dist/core/scoped-state/migrate.js +187 -0
- package/dist/core/scoped-state/paths.d.ts +9 -0
- package/dist/core/scoped-state/paths.js +27 -0
- package/dist/core/scoped-state/profiles.d.ts +27 -0
- package/dist/core/scoped-state/profiles.js +93 -0
- package/dist/core/scoped-state/providers.d.ts +24 -0
- package/dist/core/scoped-state/providers.js +19 -0
- package/dist/core/scoped-state/schema.d.ts +6 -0
- package/dist/core/scoped-state/schema.js +43 -0
- package/dist/core/scoped-state/settings.d.ts +28 -0
- package/dist/core/scoped-state/settings.js +83 -0
- package/dist/core/spaces/open-beneath.d.ts +71 -0
- package/dist/core/spaces/open-beneath.js +581 -0
- package/dist/core/sqlite-statements.d.ts +4 -0
- package/dist/core/sqlite-statements.js +17 -0
- package/dist/core/subscription-state.d.ts +121 -0
- package/dist/core/subscription-state.js +287 -0
- package/dist/core/user-settings.d.ts +377 -0
- package/dist/core/user-settings.js +458 -0
- package/dist/daemon/broker-signals/bus.d.ts +30 -0
- package/dist/daemon/broker-signals/bus.js +87 -0
- package/dist/daemon/manage.d.ts +176 -0
- package/dist/daemon/manage.js +664 -0
- package/dist/daemon/pidfile.d.ts +8 -0
- package/dist/daemon/pidfile.js +37 -0
- package/dist/daemon/startup-policy.d.ts +1 -0
- package/dist/daemon/startup-policy.js +1 -0
- package/dist/native/linux.d.ts +29 -0
- package/dist/native/linux.js +20 -0
- package/dist/shared/env.d.ts +116 -0
- package/dist/shared/env.js +271 -0
- package/dist/shared/inbox-entry-body.d.ts +22 -0
- package/dist/shared/inbox-entry-body.js +116 -0
- package/dist/shared/working-activity.d.ts +9 -0
- package/dist/shared/working-activity.js +27 -0
- package/dist/types.d.ts +562 -0
- package/dist/types.js +186 -0
- package/package.json +1 -1
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// pidfile.ts — the lean crtrd pidfile + liveness helpers, split out of crtrd.ts
|
|
2
|
+
// so the CLI's daemon front door (`daemon/manage.ts` → `ensureDaemon`) can
|
|
3
|
+
// resolve "is the daemon up?" WITHOUT importing crtrd.ts, whose module graph
|
|
4
|
+
// pulls the entire canvas state store (`core/canvas/index.js` → `openDb`). A
|
|
5
|
+
// `crtr` CLI process asking whether crtrd is running must not, by that question
|
|
6
|
+
// alone, open canvas.db — this module keeps that path fs/process-only.
|
|
7
|
+
//
|
|
8
|
+
// PURITY: Node built-ins + `core/canvas/paths.ts` (pure path builder) + the
|
|
9
|
+
// db-free `core/canvas/pid.ts` liveness probe. Never `openDb` / crtrd.ts / any
|
|
10
|
+
// canvas state accessor. crtrd.ts imports + re-exports these to preserve its
|
|
11
|
+
// historical export surface (tests, `commands/sys/daemon.ts`).
|
|
12
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { crtrHome } from '../core/canvas/paths.js';
|
|
15
|
+
import { isPidAlive } from '../core/canvas/pid.js';
|
|
16
|
+
import { envPidfileOverride } from '../shared/env.js';
|
|
17
|
+
// Re-exported so `isPidAlive` stays reachable from the daemon layer's public
|
|
18
|
+
// surface (crtrd.ts already re-exports it; callers may reach it either way).
|
|
19
|
+
export { isPidAlive };
|
|
20
|
+
/** Absolute path to crtrd's pidfile. `CRTR_PIDFILE` overrides for tests. */
|
|
21
|
+
export function pidfilePath() {
|
|
22
|
+
return envPidfileOverride() ?? join(crtrHome(), 'crtrd.pid');
|
|
23
|
+
}
|
|
24
|
+
/** Read the pid stored in the pidfile, or null if absent / malformed. */
|
|
25
|
+
export function readPidfile() {
|
|
26
|
+
const p = pidfilePath();
|
|
27
|
+
if (!existsSync(p))
|
|
28
|
+
return null;
|
|
29
|
+
const raw = readFileSync(p, 'utf8').trim();
|
|
30
|
+
const n = Number(raw);
|
|
31
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
32
|
+
}
|
|
33
|
+
/** True when a crtrd process is already running (pidfile exists + pid alive). */
|
|
34
|
+
export function isDaemonRunning() {
|
|
35
|
+
const pid = readPidfile();
|
|
36
|
+
return pid !== null && isPidAlive(pid);
|
|
37
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const DAEMON_STARTUP_WINDOW_MS: number;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DAEMON_STARTUP_WINDOW_MS = 5 * 60 * 1_000;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Socket } from 'node:net';
|
|
2
|
+
export interface LinuxNative {
|
|
3
|
+
peerCred(fd: number): {
|
|
4
|
+
pid: number;
|
|
5
|
+
uid: number;
|
|
6
|
+
gid: number;
|
|
7
|
+
};
|
|
8
|
+
openat2(dirfd: number, path: string, flags: number, resolve: number): number;
|
|
9
|
+
fdPath(fd: number): string;
|
|
10
|
+
fstat(fd: number): {
|
|
11
|
+
uid: number;
|
|
12
|
+
gid: number;
|
|
13
|
+
mode: number;
|
|
14
|
+
nlink: number;
|
|
15
|
+
dev: bigint;
|
|
16
|
+
ino: bigint;
|
|
17
|
+
};
|
|
18
|
+
sendFds(socketFd: number, data: string, fds: number[]): void;
|
|
19
|
+
recvFds(socketFd: number): {
|
|
20
|
+
data: string;
|
|
21
|
+
fds: number[];
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export declare const O_PATH = 2097152;
|
|
25
|
+
export declare const O_CLOEXEC = 524288;
|
|
26
|
+
export declare const RESOLVE_NO_SYMLINKS = 4;
|
|
27
|
+
export declare const RESOLVE_BENEATH = 8;
|
|
28
|
+
export declare function loadLinuxNative(): LinuxNative | undefined;
|
|
29
|
+
export declare function peerUid(socket: Socket): number;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
export const O_PATH = 0x200000;
|
|
3
|
+
export const O_CLOEXEC = 0x80000;
|
|
4
|
+
export const RESOLVE_NO_SYMLINKS = 0x04;
|
|
5
|
+
export const RESOLVE_BENEATH = 0x08;
|
|
6
|
+
export function loadLinuxNative() {
|
|
7
|
+
if (process.platform !== 'linux')
|
|
8
|
+
return undefined;
|
|
9
|
+
// One prebuilt addon per architecture (see scripts/ensure-native-linux.mjs).
|
|
10
|
+
return createRequire(import.meta.url)(`./linux/${process.arch}/addon.node`);
|
|
11
|
+
}
|
|
12
|
+
export function peerUid(socket) {
|
|
13
|
+
const fd = socket._handle?.fd;
|
|
14
|
+
if (fd === undefined || fd < 0)
|
|
15
|
+
throw new Error('unix socket peer fd is unavailable');
|
|
16
|
+
const native = loadLinuxNative();
|
|
17
|
+
if (!native)
|
|
18
|
+
throw new Error('Linux peer credentials are unavailable');
|
|
19
|
+
return native.peerCred(fd).uid;
|
|
20
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/** The current node's id (`CRTR_NODE_ID`), or undefined outside a node. */
|
|
2
|
+
export declare function envNodeId(): string | undefined;
|
|
3
|
+
/** Find the single launch snapshot mounted by the launcher into this process's
|
|
4
|
+
* Linux namespace. Cron bash has no CRTR_NODE_ID, but its creator's mounted
|
|
5
|
+
* launch snapshot supplies config and granted-command projection without
|
|
6
|
+
* claiming the cron is a node. Its broker relay may already have closed when
|
|
7
|
+
* the cron fires; still classify the sandbox so CLI bootstrap never falls back
|
|
8
|
+
* to host stores or person authority. Neither CRTR_NODE_ID, CRTR_ANCHOR, nor
|
|
9
|
+
* the caller-editable CRTR_API_SOCKET selects the launch identity. */
|
|
10
|
+
export declare function sandboxCliNodeId(): string | undefined;
|
|
11
|
+
/** A Linux app command uses its mounted launch snapshot, not host user stores. */
|
|
12
|
+
export declare function isSandboxedNode(): boolean;
|
|
13
|
+
/** The app that owns the current node, supplied in its broker launch identity. */
|
|
14
|
+
export declare function envGrantee(): string | undefined;
|
|
15
|
+
/** Set to '1' by `nodeEnv` for an app-started run's brokers, which admit inbox
|
|
16
|
+
* mail only between agent runs. Terminal nodes also carry `CRTR_RUN_ID`, so
|
|
17
|
+
* that variable is not this signal. */
|
|
18
|
+
export declare const INBOX_BETWEEN_TURNS_ENV = "CRTR_INBOX_BETWEEN_TURNS";
|
|
19
|
+
export declare function envInboxBetweenTurns(): boolean;
|
|
20
|
+
/** The current run's scope allow-list; null means the row inherits everything. */
|
|
21
|
+
export declare function envScopes(): readonly string[] | null;
|
|
22
|
+
/** Whether a launched scope list covers a capability. Null means not narrowed. */
|
|
23
|
+
export declare function scopeAllowed(scopes: readonly string[] | null | undefined, scope: string): boolean;
|
|
24
|
+
export declare function envExecutionId(): string | undefined;
|
|
25
|
+
/** The current node's target profile id (`CRTR_PROFILE_ID`), or undefined. */
|
|
26
|
+
export declare function envProfileId(): string | undefined;
|
|
27
|
+
/** The current node's working directory (`CRTR_NODE_CWD`), or undefined. */
|
|
28
|
+
export declare function envNodeCwd(): string | undefined;
|
|
29
|
+
/** An explicit model-routing override for this turn (`CRTR_MODEL_INTENT`), or
|
|
30
|
+
* undefined. Cleared (not read) via `delete process.env['CRTR_MODEL_INTENT']`
|
|
31
|
+
* in `runtime/broker.ts` — a mutation, not a read, so it stays there. */
|
|
32
|
+
export declare function envModelIntent(): string | undefined;
|
|
33
|
+
/** Whether the launch recipe requires this broker to keep its exact provider/model. */
|
|
34
|
+
export declare function envModelExact(): boolean;
|
|
35
|
+
/** The raw `CRTR_HOME` override, unresolved. Use `crtrHome()`
|
|
36
|
+
* (`core/canvas/paths.ts`) to get the resolved canvas-home path; use this
|
|
37
|
+
* only when a caller needs the override itself — to check whether one is
|
|
38
|
+
* set, or to propagate it verbatim into a child process's env. */
|
|
39
|
+
export declare function envHomeOverride(): string | undefined;
|
|
40
|
+
/** The raw installed-runtime directory override, unresolved. */
|
|
41
|
+
export declare function envRuntimeHomeOverride(): string | undefined;
|
|
42
|
+
/** Dead-provider watchdog timeout (`CRTR_STREAM_WATCHDOG_MS`), 5 minutes by
|
|
43
|
+
* default. See `runtime/stream-watchdog.ts` for what it guards. */
|
|
44
|
+
export declare function envStreamWatchdogMs(): number;
|
|
45
|
+
/** The canvas inbox watcher's poll tick (`CRTR_WATCHER_TICK_MS`). */
|
|
46
|
+
export declare function envWatcherTickMs(): number;
|
|
47
|
+
/** Claim backstop while broker signals are connected (`CRTR_WATCHER_BACKSTOP_MS`). */
|
|
48
|
+
export declare function envWatcherBackstopMs(): number;
|
|
49
|
+
/** The canvas inbox watcher's debounce window (`CRTR_WATCHER_DEBOUNCE_MS`). */
|
|
50
|
+
export declare function envWatcherDebounceMs(): number;
|
|
51
|
+
/** No-new-message window before a recap is shown (`CRTR_RECAP_IDLE_MS`), 60s
|
|
52
|
+
* by default. */
|
|
53
|
+
export declare function envRecapIdleMs(): number;
|
|
54
|
+
/** Explicit recap-model override (`CRTR_RECAP_MODEL`), trimmed; undefined if
|
|
55
|
+
* unset or blank. */
|
|
56
|
+
export declare function envRecapModelOverride(): string | undefined;
|
|
57
|
+
/** The bash safety-valve deadline (`CRTR_BASH_VALVE_MS`), 5 minutes by
|
|
58
|
+
* default; undocumented test-only override so the E2E valve check doesn't
|
|
59
|
+
* sleep 5 minutes. */
|
|
60
|
+
export declare function envBashValveMs(): number;
|
|
61
|
+
/** How long an AUTO-backgrounded bash job may run before the daemon cancels it
|
|
62
|
+
* unless the agent claims it (`CRTR_BASH_AUTO_BG_DEADLINE_MS`), 60s by default;
|
|
63
|
+
* undocumented test-only override so the deadline check doesn't wait a minute. */
|
|
64
|
+
export declare function envBashAutoBgDeadlineMs(): number;
|
|
65
|
+
/** Explicit pidfile path override (`CRTR_PIDFILE`, tests only); undefined if
|
|
66
|
+
* unset or blank. */
|
|
67
|
+
export declare function envPidfileOverride(): string | undefined;
|
|
68
|
+
/** The broker engine module spec (`CRTR_BROKER_ENGINE`, the T11 test seam),
|
|
69
|
+
* defaulting to the real SDK. `runtime/host.ts` layers `inv.env` ahead of
|
|
70
|
+
* this (a launch-time override mirrored into the child's env, since
|
|
71
|
+
* spawn-env.ts's default-deny allowlist strips `CRTR_*` from ambient env). */
|
|
72
|
+
export declare function envBrokerEngine(): string;
|
|
73
|
+
/** Test-only pi-binary substitution (`CRTR_PI_BINARY`) so the integration
|
|
74
|
+
* harness can point a real `crtr node new` at a deterministic fake-pi
|
|
75
|
+
* vehicle. Undefined in production. */
|
|
76
|
+
export declare function envPiBinaryOverride(): string | undefined;
|
|
77
|
+
/** Whether implicit daemon autostart is suppressed for this invocation
|
|
78
|
+
* (`CRTR_NO_DAEMON_AUTOSTART=1`). */
|
|
79
|
+
export declare function envNoDaemonAutostart(): boolean;
|
|
80
|
+
/** The `--tcp` fallback for crtrd's opt-in TCP listener (`CRTRD_TCP`), raw
|
|
81
|
+
* (`host:port` or undefined — absent means unix socket only). */
|
|
82
|
+
export declare function envCrtrdTcp(): string | undefined;
|
|
83
|
+
/** The opt-in bearer token guarding crtrd's TCP listener (`CRTRD_TOKEN`).
|
|
84
|
+
* Unset (or blank) → undefined, and the TCP listener performs NO auth check
|
|
85
|
+
* at all — exactly today's behavior. The unix socket never checks this,
|
|
86
|
+
* set or not. */
|
|
87
|
+
export declare function envCrtrdToken(): string | undefined;
|
|
88
|
+
/** Kill switch for the host-exports writer/pruner (`CRTR_NO_EXPORTS=1`). */
|
|
89
|
+
export declare function envNoExports(): boolean;
|
|
90
|
+
/** Verbose-diagnostics gate for otherwise-silent best-effort catches
|
|
91
|
+
* (`CRTR_DEBUG=1`). Distinct from `CRTR_DEBUG_PID_LIVENESS`
|
|
92
|
+
* (`core/canvas/pid.ts`), a different, unrelated var. */
|
|
93
|
+
export declare function envDebug(): boolean;
|
|
94
|
+
/** Live-broker automatic-revive cap override (`CRTR_MAX_LIVE_BROKERS`); wins
|
|
95
|
+
* over `readConfig('user').brokerThresholds.automaticReviveCap` when set. */
|
|
96
|
+
export declare function envMaxLiveBrokers(): number | undefined;
|
|
97
|
+
/** Live-broker warning threshold override (`CRTR_WARN_LIVE_BROKERS`); wins
|
|
98
|
+
* over `readConfig('user').brokerThresholds.warning` when set. */
|
|
99
|
+
export declare function envWarnLiveBrokers(): number | undefined;
|
|
100
|
+
/** Test-only unattended-park interval override (`CRTR_TEST_UNATTENDED_PARK_MS`),
|
|
101
|
+
* gated by `envNoDaemonAutostart()` at the call site; no production default. */
|
|
102
|
+
export declare function envTestUnattendedParkMs(): number | undefined;
|
|
103
|
+
/** Production unattended-park interval override (`CRTR_UNATTENDED_PARK_MS`), in
|
|
104
|
+
* milliseconds; wins over the user-scope config value
|
|
105
|
+
* (`readConfig('user').lifecycle.unattendedParkMs`, itself defaulting to 15
|
|
106
|
+
* min) whenever set, autostart or not. Unlike `CRTR_TEST_UNATTENDED_PARK_MS`
|
|
107
|
+
* this is not gated on `envNoDaemonAutostart()`: it is the env escape hatch
|
|
108
|
+
* above the configured knob a deployment sets to tune how long an eligible
|
|
109
|
+
* resident idles before its parking turn. */
|
|
110
|
+
export declare function envUnattendedParkMs(): number | undefined;
|
|
111
|
+
/** Test-only park-summary grace-period override
|
|
112
|
+
* (`CRTR_TEST_PARK_SUMMARY_GRACE_MS`); no production default. */
|
|
113
|
+
export declare function envTestParkSummaryGraceMs(): number | undefined;
|
|
114
|
+
/** The bounded lifetime shared by the daemon's pending-park sweep and its
|
|
115
|
+
* in-broker isolated turn. */
|
|
116
|
+
export declare function parkSummaryGraceMs(): number;
|
|
@@ -0,0 +1,271 @@
|
|
|
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
|
+
import { lstatSync, readFileSync, readdirSync } from 'node:fs';
|
|
25
|
+
import { join } from 'node:path';
|
|
26
|
+
import { covers } from '@crouter/identity';
|
|
27
|
+
// Identity
|
|
28
|
+
/** The current node's id (`CRTR_NODE_ID`), or undefined outside a node. */
|
|
29
|
+
export function envNodeId() {
|
|
30
|
+
return process.env['CRTR_NODE_ID'];
|
|
31
|
+
}
|
|
32
|
+
/** Find the single launch snapshot mounted by the launcher into this process's
|
|
33
|
+
* Linux namespace. Cron bash has no CRTR_NODE_ID, but its creator's mounted
|
|
34
|
+
* launch snapshot supplies config and granted-command projection without
|
|
35
|
+
* claiming the cron is a node. Its broker relay may already have closed when
|
|
36
|
+
* the cron fires; still classify the sandbox so CLI bootstrap never falls back
|
|
37
|
+
* to host stores or person authority. Neither CRTR_NODE_ID, CRTR_ANCHOR, nor
|
|
38
|
+
* the caller-editable CRTR_API_SOCKET selects the launch identity. */
|
|
39
|
+
export function sandboxCliNodeId() {
|
|
40
|
+
if (process.platform !== 'linux')
|
|
41
|
+
return undefined;
|
|
42
|
+
try {
|
|
43
|
+
const root = '/run/crtr/nodes';
|
|
44
|
+
const mounted = new Set(readFileSync('/proc/self/mountinfo', 'utf8').split('\n').map((line) => line.split(' ')[4]));
|
|
45
|
+
const ids = readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() &&
|
|
46
|
+
/^[a-zA-Z0-9][a-zA-Z0-9-]{0,127}$/.test(entry.name) &&
|
|
47
|
+
mounted.has(join(root, entry.name)) &&
|
|
48
|
+
lstatSync(join(root, entry.name, 'broker-launch.json'), { throwIfNoEntry: false })?.isFile());
|
|
49
|
+
return ids.length === 1 ? ids[0].name : undefined;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** A Linux app command uses its mounted launch snapshot, not host user stores. */
|
|
56
|
+
export function isSandboxedNode() {
|
|
57
|
+
return sandboxCliNodeId() !== undefined;
|
|
58
|
+
}
|
|
59
|
+
/** The app that owns the current node, supplied in its broker launch identity. */
|
|
60
|
+
export function envGrantee() {
|
|
61
|
+
return process.env['CRTR_GRANTEE'];
|
|
62
|
+
}
|
|
63
|
+
/** Set to '1' by `nodeEnv` for an app-started run's brokers, which admit inbox
|
|
64
|
+
* mail only between agent runs. Terminal nodes also carry `CRTR_RUN_ID`, so
|
|
65
|
+
* that variable is not this signal. */
|
|
66
|
+
export const INBOX_BETWEEN_TURNS_ENV = 'CRTR_INBOX_BETWEEN_TURNS';
|
|
67
|
+
export function envInboxBetweenTurns() {
|
|
68
|
+
return process.env[INBOX_BETWEEN_TURNS_ENV] === '1';
|
|
69
|
+
}
|
|
70
|
+
/** The current run's scope allow-list; null means the row inherits everything. */
|
|
71
|
+
export function envScopes() {
|
|
72
|
+
const raw = process.env['CRTR_SCOPES'];
|
|
73
|
+
return raw === undefined ? null : raw === '' ? [] : raw.split(',');
|
|
74
|
+
}
|
|
75
|
+
/** Whether a launched scope list covers a capability. Null means not narrowed. */
|
|
76
|
+
export function scopeAllowed(scopes, scope) {
|
|
77
|
+
if (scopes === null || scopes === undefined)
|
|
78
|
+
return true;
|
|
79
|
+
return scopes.some((held) => covers(held, scope));
|
|
80
|
+
}
|
|
81
|
+
export function envExecutionId() {
|
|
82
|
+
return process.env['CRTR_EXECUTION_ID'];
|
|
83
|
+
}
|
|
84
|
+
/** The current node's target profile id (`CRTR_PROFILE_ID`), or undefined. */
|
|
85
|
+
export function envProfileId() {
|
|
86
|
+
return process.env['CRTR_PROFILE_ID'];
|
|
87
|
+
}
|
|
88
|
+
/** The current node's working directory (`CRTR_NODE_CWD`), or undefined. */
|
|
89
|
+
export function envNodeCwd() {
|
|
90
|
+
return process.env['CRTR_NODE_CWD'];
|
|
91
|
+
}
|
|
92
|
+
/** An explicit model-routing override for this turn (`CRTR_MODEL_INTENT`), or
|
|
93
|
+
* undefined. Cleared (not read) via `delete process.env['CRTR_MODEL_INTENT']`
|
|
94
|
+
* in `runtime/broker.ts` — a mutation, not a read, so it stays there. */
|
|
95
|
+
export function envModelIntent() {
|
|
96
|
+
return process.env['CRTR_MODEL_INTENT'];
|
|
97
|
+
}
|
|
98
|
+
/** Whether the launch recipe requires this broker to keep its exact provider/model. */
|
|
99
|
+
export function envModelExact() {
|
|
100
|
+
return process.env['CRTR_MODEL_EXACT'] === '1';
|
|
101
|
+
}
|
|
102
|
+
/** The raw `CRTR_HOME` override, unresolved. Use `crtrHome()`
|
|
103
|
+
* (`core/canvas/paths.ts`) to get the resolved canvas-home path; use this
|
|
104
|
+
* only when a caller needs the override itself — to check whether one is
|
|
105
|
+
* set, or to propagate it verbatim into a child process's env. */
|
|
106
|
+
export function envHomeOverride() {
|
|
107
|
+
return process.env['CRTR_HOME'];
|
|
108
|
+
}
|
|
109
|
+
/** The raw installed-runtime directory override, unresolved. */
|
|
110
|
+
export function envRuntimeHomeOverride() {
|
|
111
|
+
return process.env['CRTR_RUNTIME_HOME'];
|
|
112
|
+
}
|
|
113
|
+
// Tuning
|
|
114
|
+
/** Generic `Number(raw)` parse with a positive-finite guard, shared by every
|
|
115
|
+
* millisecond tuning var below (each previously reimplemented this locally,
|
|
116
|
+
* identically). */
|
|
117
|
+
function parsePositiveMs(raw, fallback) {
|
|
118
|
+
if (raw === undefined)
|
|
119
|
+
return fallback;
|
|
120
|
+
const n = Number(raw);
|
|
121
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
122
|
+
}
|
|
123
|
+
const DEFAULT_STREAM_WATCHDOG_MS = 5 * 60_000; // 5 minutes.
|
|
124
|
+
/** Dead-provider watchdog timeout (`CRTR_STREAM_WATCHDOG_MS`), 5 minutes by
|
|
125
|
+
* default. See `runtime/stream-watchdog.ts` for what it guards. */
|
|
126
|
+
export function envStreamWatchdogMs() {
|
|
127
|
+
return parsePositiveMs(process.env['CRTR_STREAM_WATCHDOG_MS'], DEFAULT_STREAM_WATCHDOG_MS);
|
|
128
|
+
}
|
|
129
|
+
const DEFAULT_WATCHER_TICK_MS = 800;
|
|
130
|
+
/** The canvas inbox watcher's poll tick (`CRTR_WATCHER_TICK_MS`). */
|
|
131
|
+
export function envWatcherTickMs() {
|
|
132
|
+
return parsePositiveMs(process.env['CRTR_WATCHER_TICK_MS'], DEFAULT_WATCHER_TICK_MS);
|
|
133
|
+
}
|
|
134
|
+
const DEFAULT_WATCHER_BACKSTOP_MS = 60_000;
|
|
135
|
+
/** Claim backstop while broker signals are connected (`CRTR_WATCHER_BACKSTOP_MS`). */
|
|
136
|
+
export function envWatcherBackstopMs() {
|
|
137
|
+
return parsePositiveMs(process.env['CRTR_WATCHER_BACKSTOP_MS'], DEFAULT_WATCHER_BACKSTOP_MS);
|
|
138
|
+
}
|
|
139
|
+
const DEFAULT_WATCHER_DEBOUNCE_MS = 1000;
|
|
140
|
+
/** The canvas inbox watcher's debounce window (`CRTR_WATCHER_DEBOUNCE_MS`). */
|
|
141
|
+
export function envWatcherDebounceMs() {
|
|
142
|
+
return parsePositiveMs(process.env['CRTR_WATCHER_DEBOUNCE_MS'], DEFAULT_WATCHER_DEBOUNCE_MS);
|
|
143
|
+
}
|
|
144
|
+
const DEFAULT_RECAP_IDLE_MS = 60_000;
|
|
145
|
+
/** No-new-message window before a recap is shown (`CRTR_RECAP_IDLE_MS`), 60s
|
|
146
|
+
* by default. */
|
|
147
|
+
export function envRecapIdleMs() {
|
|
148
|
+
return parsePositiveMs(process.env['CRTR_RECAP_IDLE_MS'], DEFAULT_RECAP_IDLE_MS);
|
|
149
|
+
}
|
|
150
|
+
/** Explicit recap-model override (`CRTR_RECAP_MODEL`), trimmed; undefined if
|
|
151
|
+
* unset or blank. */
|
|
152
|
+
export function envRecapModelOverride() {
|
|
153
|
+
const raw = process.env['CRTR_RECAP_MODEL'];
|
|
154
|
+
return raw !== undefined && raw.trim() !== '' ? raw.trim() : undefined;
|
|
155
|
+
}
|
|
156
|
+
const DEFAULT_BASH_VALVE_MS = 5 * 60 * 1000;
|
|
157
|
+
/** The bash safety-valve deadline (`CRTR_BASH_VALVE_MS`), 5 minutes by
|
|
158
|
+
* default; undocumented test-only override so the E2E valve check doesn't
|
|
159
|
+
* sleep 5 minutes. */
|
|
160
|
+
export function envBashValveMs() {
|
|
161
|
+
return parsePositiveMs(process.env['CRTR_BASH_VALVE_MS'], DEFAULT_BASH_VALVE_MS);
|
|
162
|
+
}
|
|
163
|
+
const DEFAULT_BASH_AUTO_BG_DEADLINE_MS = 60 * 1000;
|
|
164
|
+
/** How long an AUTO-backgrounded bash job may run before the daemon cancels it
|
|
165
|
+
* unless the agent claims it (`CRTR_BASH_AUTO_BG_DEADLINE_MS`), 60s by default;
|
|
166
|
+
* undocumented test-only override so the deadline check doesn't wait a minute. */
|
|
167
|
+
export function envBashAutoBgDeadlineMs() {
|
|
168
|
+
return parsePositiveMs(process.env['CRTR_BASH_AUTO_BG_DEADLINE_MS'], DEFAULT_BASH_AUTO_BG_DEADLINE_MS);
|
|
169
|
+
}
|
|
170
|
+
/** Explicit pidfile path override (`CRTR_PIDFILE`, tests only); undefined if
|
|
171
|
+
* unset or blank. */
|
|
172
|
+
export function envPidfileOverride() {
|
|
173
|
+
const raw = process.env['CRTR_PIDFILE'];
|
|
174
|
+
return raw !== undefined && raw !== '' ? raw : undefined;
|
|
175
|
+
}
|
|
176
|
+
const DEFAULT_BROKER_ENGINE = '@earendil-works/pi-coding-agent';
|
|
177
|
+
/** The broker engine module spec (`CRTR_BROKER_ENGINE`, the T11 test seam),
|
|
178
|
+
* defaulting to the real SDK. `runtime/host.ts` layers `inv.env` ahead of
|
|
179
|
+
* this (a launch-time override mirrored into the child's env, since
|
|
180
|
+
* spawn-env.ts's default-deny allowlist strips `CRTR_*` from ambient env). */
|
|
181
|
+
export function envBrokerEngine() {
|
|
182
|
+
return process.env['CRTR_BROKER_ENGINE'] ?? DEFAULT_BROKER_ENGINE;
|
|
183
|
+
}
|
|
184
|
+
/** Test-only pi-binary substitution (`CRTR_PI_BINARY`) so the integration
|
|
185
|
+
* harness can point a real `crtr node new` at a deterministic fake-pi
|
|
186
|
+
* vehicle. Undefined in production. */
|
|
187
|
+
export function envPiBinaryOverride() {
|
|
188
|
+
return process.env['CRTR_PI_BINARY'];
|
|
189
|
+
}
|
|
190
|
+
/** Whether implicit daemon autostart is suppressed for this invocation
|
|
191
|
+
* (`CRTR_NO_DAEMON_AUTOSTART=1`). */
|
|
192
|
+
export function envNoDaemonAutostart() {
|
|
193
|
+
return process.env['CRTR_NO_DAEMON_AUTOSTART'] === '1';
|
|
194
|
+
}
|
|
195
|
+
/** The `--tcp` fallback for crtrd's opt-in TCP listener (`CRTRD_TCP`), raw
|
|
196
|
+
* (`host:port` or undefined — absent means unix socket only). */
|
|
197
|
+
export function envCrtrdTcp() {
|
|
198
|
+
return process.env['CRTRD_TCP'];
|
|
199
|
+
}
|
|
200
|
+
/** The opt-in bearer token guarding crtrd's TCP listener (`CRTRD_TOKEN`).
|
|
201
|
+
* Unset (or blank) → undefined, and the TCP listener performs NO auth check
|
|
202
|
+
* at all — exactly today's behavior. The unix socket never checks this,
|
|
203
|
+
* set or not. */
|
|
204
|
+
export function envCrtrdToken() {
|
|
205
|
+
const raw = process.env['CRTRD_TOKEN'];
|
|
206
|
+
return raw !== undefined && raw !== '' ? raw : undefined;
|
|
207
|
+
}
|
|
208
|
+
/** Kill switch for the host-exports writer/pruner (`CRTR_NO_EXPORTS=1`). */
|
|
209
|
+
export function envNoExports() {
|
|
210
|
+
return process.env['CRTR_NO_EXPORTS'] === '1';
|
|
211
|
+
}
|
|
212
|
+
/** Verbose-diagnostics gate for otherwise-silent best-effort catches
|
|
213
|
+
* (`CRTR_DEBUG=1`). Distinct from `CRTR_DEBUG_PID_LIVENESS`
|
|
214
|
+
* (`core/canvas/pid.ts`), a different, unrelated var. */
|
|
215
|
+
export function envDebug() {
|
|
216
|
+
return process.env['CRTR_DEBUG'] === '1';
|
|
217
|
+
}
|
|
218
|
+
// Precedence — broker-supervision.ts's threshold/interval overrides. Each
|
|
219
|
+
// returns the raw parsed value with NO default: the caller supplies the
|
|
220
|
+
// default (user config for the broker thresholds and the unattended-park
|
|
221
|
+
// interval, a hardcoded constant for the test-only park-summary grace), and
|
|
222
|
+
// env wins over it when set. Undefined, unparseable, or non-positive all mean
|
|
223
|
+
// "no override" — the caller's own default applies.
|
|
224
|
+
function parsePositiveInteger(raw) {
|
|
225
|
+
if (raw === undefined || raw === '')
|
|
226
|
+
return undefined;
|
|
227
|
+
const n = Number(raw);
|
|
228
|
+
return Number.isSafeInteger(n) && n >= 1 ? n : undefined;
|
|
229
|
+
}
|
|
230
|
+
/** Live-broker automatic-revive cap override (`CRTR_MAX_LIVE_BROKERS`); wins
|
|
231
|
+
* over `readConfig('user').brokerThresholds.automaticReviveCap` when set. */
|
|
232
|
+
export function envMaxLiveBrokers() {
|
|
233
|
+
return parsePositiveInteger(process.env['CRTR_MAX_LIVE_BROKERS']);
|
|
234
|
+
}
|
|
235
|
+
/** Live-broker warning threshold override (`CRTR_WARN_LIVE_BROKERS`); wins
|
|
236
|
+
* over `readConfig('user').brokerThresholds.warning` when set. */
|
|
237
|
+
export function envWarnLiveBrokers() {
|
|
238
|
+
return parsePositiveInteger(process.env['CRTR_WARN_LIVE_BROKERS']);
|
|
239
|
+
}
|
|
240
|
+
function parsePositiveMsNoDefault(raw) {
|
|
241
|
+
if (raw === undefined)
|
|
242
|
+
return undefined;
|
|
243
|
+
const n = Number(raw);
|
|
244
|
+
return Number.isFinite(n) && n > 0 ? n : undefined;
|
|
245
|
+
}
|
|
246
|
+
/** Test-only unattended-park interval override (`CRTR_TEST_UNATTENDED_PARK_MS`),
|
|
247
|
+
* gated by `envNoDaemonAutostart()` at the call site; no production default. */
|
|
248
|
+
export function envTestUnattendedParkMs() {
|
|
249
|
+
return parsePositiveMsNoDefault(process.env['CRTR_TEST_UNATTENDED_PARK_MS']);
|
|
250
|
+
}
|
|
251
|
+
/** Production unattended-park interval override (`CRTR_UNATTENDED_PARK_MS`), in
|
|
252
|
+
* milliseconds; wins over the user-scope config value
|
|
253
|
+
* (`readConfig('user').lifecycle.unattendedParkMs`, itself defaulting to 15
|
|
254
|
+
* min) whenever set, autostart or not. Unlike `CRTR_TEST_UNATTENDED_PARK_MS`
|
|
255
|
+
* this is not gated on `envNoDaemonAutostart()`: it is the env escape hatch
|
|
256
|
+
* above the configured knob a deployment sets to tune how long an eligible
|
|
257
|
+
* resident idles before its parking turn. */
|
|
258
|
+
export function envUnattendedParkMs() {
|
|
259
|
+
return parsePositiveMsNoDefault(process.env['CRTR_UNATTENDED_PARK_MS']);
|
|
260
|
+
}
|
|
261
|
+
/** Test-only park-summary grace-period override
|
|
262
|
+
* (`CRTR_TEST_PARK_SUMMARY_GRACE_MS`); no production default. */
|
|
263
|
+
export function envTestParkSummaryGraceMs() {
|
|
264
|
+
return parsePositiveMsNoDefault(process.env['CRTR_TEST_PARK_SUMMARY_GRACE_MS']);
|
|
265
|
+
}
|
|
266
|
+
const DEFAULT_PARK_SUMMARY_GRACE_MS = 5 * 60_000;
|
|
267
|
+
/** The bounded lifetime shared by the daemon's pending-park sweep and its
|
|
268
|
+
* in-broker isolated turn. */
|
|
269
|
+
export function parkSummaryGraceMs() {
|
|
270
|
+
return envTestParkSummaryGraceMs() ?? DEFAULT_PARK_SUMMARY_GRACE_MS;
|
|
271
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare const INLINE_REPORT_MAX_CHARS = 1000;
|
|
2
|
+
/** The rendering fields both inbox projections share. */
|
|
3
|
+
export interface InboxEntryFields {
|
|
4
|
+
ts: string;
|
|
5
|
+
from: string | null;
|
|
6
|
+
from_name?: string;
|
|
7
|
+
label: string;
|
|
8
|
+
ref?: string;
|
|
9
|
+
data?: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
/** A report document's ref (`<node>/reports/<name>`), else undefined. */
|
|
12
|
+
export declare function reportRefNodeId(ref: string | undefined): string | undefined;
|
|
13
|
+
/** Clip a body to the bounded preview stored beside a spilled message body. */
|
|
14
|
+
export declare function clipBody(body: string): {
|
|
15
|
+
text: string;
|
|
16
|
+
clipped: boolean;
|
|
17
|
+
};
|
|
18
|
+
export declare function inlineBody(entry: Pick<InboxEntryFields, 'data' | 'label'>): string;
|
|
19
|
+
/** One entry's card body: an inlined short report, a bounded inline message body, an event
|
|
20
|
+
* push rendered from its metadata, or the label. `report` is the report document's body when
|
|
21
|
+
* the caller could read it. */
|
|
22
|
+
export declare function renderEntryBody(entry: InboxEntryFields, report: string | null): string;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// One inbox entry's card body, shared by the daemon (kickoff digest) and broker (live inbox) renderers.
|
|
2
|
+
import { formatInboxRefInstruction } from './generated-context.js';
|
|
3
|
+
const BODY_MAX_LINES = 12;
|
|
4
|
+
const BODY_MAX_CHARS = 1000;
|
|
5
|
+
export const INLINE_REPORT_MAX_CHARS = 1000;
|
|
6
|
+
/** A report document's ref (`<node>/reports/<name>`), else undefined. */
|
|
7
|
+
export function reportRefNodeId(ref) {
|
|
8
|
+
const match = ref === undefined ? null : /^([a-z0-9-]+)\/reports\/[^/]+$/.exec(ref);
|
|
9
|
+
return match?.[1];
|
|
10
|
+
}
|
|
11
|
+
/** Clip a body to the bounded preview stored beside a spilled message body. */
|
|
12
|
+
export function clipBody(body) {
|
|
13
|
+
let text = body;
|
|
14
|
+
let clipped = false;
|
|
15
|
+
const lines = text.split('\n');
|
|
16
|
+
if (lines.length > BODY_MAX_LINES) {
|
|
17
|
+
text = lines.slice(0, BODY_MAX_LINES).join('\n');
|
|
18
|
+
clipped = true;
|
|
19
|
+
}
|
|
20
|
+
if (text.length > BODY_MAX_CHARS) {
|
|
21
|
+
text = text.slice(0, BODY_MAX_CHARS);
|
|
22
|
+
clipped = true;
|
|
23
|
+
}
|
|
24
|
+
return { text: text.trimEnd(), clipped };
|
|
25
|
+
}
|
|
26
|
+
export function inlineBody(entry) {
|
|
27
|
+
const body = typeof entry.data?.['body'] === 'string' ? String(entry.data['body']).trim() : '';
|
|
28
|
+
return body === '' || body === entry.label ? '' : body;
|
|
29
|
+
}
|
|
30
|
+
function pushOf(entry) {
|
|
31
|
+
const push = entry.data?.['push'];
|
|
32
|
+
return typeof push === 'object' && push !== null && typeof push.event === 'string' ? push : null;
|
|
33
|
+
}
|
|
34
|
+
/** The name a reader resolves the object by: `<owner handle>/<name>`, or the owner's id on an older push. */
|
|
35
|
+
function objectRef(o) {
|
|
36
|
+
const owner = o.owner_handle ?? o.owner;
|
|
37
|
+
return owner === null || owner === undefined ? o.name : `${owner}/${o.name}`;
|
|
38
|
+
}
|
|
39
|
+
function objectText(o) {
|
|
40
|
+
if (o.type === 'document')
|
|
41
|
+
return `[[${objectRef(o)}]]`;
|
|
42
|
+
if (o.type === 'job')
|
|
43
|
+
return `job ${o.id} (\`${o.name}\`)`;
|
|
44
|
+
if (o.type === 'node')
|
|
45
|
+
return `node ${o.name} (${o.id})`;
|
|
46
|
+
return `${o.type} ${o.name}`;
|
|
47
|
+
}
|
|
48
|
+
function actorText(entry, push) {
|
|
49
|
+
if (push.actor === null)
|
|
50
|
+
return 'the runtime';
|
|
51
|
+
return entry.from_name === undefined || entry.from_name === push.actor ? push.actor : `${entry.from_name} (${push.actor})`;
|
|
52
|
+
}
|
|
53
|
+
function list(value) {
|
|
54
|
+
return Array.isArray(value) ? value.filter((v) => typeof v === 'string') : [];
|
|
55
|
+
}
|
|
56
|
+
/** One sentence per event: the object, the actor and the pointer — never the act's content. */
|
|
57
|
+
function renderPush(entry, push) {
|
|
58
|
+
const obj = objectText(push.object);
|
|
59
|
+
const actor = actorText(entry, push);
|
|
60
|
+
const a = push.attrs;
|
|
61
|
+
const readHint = push.object.type === 'document'
|
|
62
|
+
? ` Read it with \`crtr canvas read ${objectRef(push.object)}\`.`
|
|
63
|
+
: '';
|
|
64
|
+
switch (push.event) {
|
|
65
|
+
case 'created':
|
|
66
|
+
return push.object.type === 'node'
|
|
67
|
+
? `${actor} created ${obj}${typeof a['kind'] === 'string' ? `, kind ${a['kind']}` : ''}.`
|
|
68
|
+
: `${actor} created ${obj}.${readHint}`;
|
|
69
|
+
case 'read': return `${actor} read ${obj}.`;
|
|
70
|
+
case 'watched': return `${actor} started watching ${obj}.`;
|
|
71
|
+
case 'deleted': return `${actor} deleted ${obj}.`;
|
|
72
|
+
case 'edited': {
|
|
73
|
+
const authors = list(a['authors']);
|
|
74
|
+
const rationales = list(a['rationales']);
|
|
75
|
+
const range = typeof a['from_revision'] === 'number' && typeof a['revision'] === 'number' && a['revision'] - a['from_revision'] > 1
|
|
76
|
+
? `revisions ${a['from_revision'] + 1}–${a['revision']}` : typeof a['revision'] === 'number' ? `revision ${a['revision']}` : 'a new revision';
|
|
77
|
+
const by = authors.length === 0 ? actor : authors.join(', ');
|
|
78
|
+
const why = rationales.length === 0 ? '' : ` Rationale: ${rationales.join('; ')}.`;
|
|
79
|
+
const merge = a['needs_merge'] === true ? ' Two edits were made from the same revision; read it and edit it to merge them.' : '';
|
|
80
|
+
const unwatchHint = ` You get this because you read or wrote it; if it no longer concerns your work, stop with \`crtr canvas unwatch ${objectRef(push.object)}\`.`;
|
|
81
|
+
return `${by} edited ${obj} (${range}).${why}${merge}${readHint}${unwatchHint}`;
|
|
82
|
+
}
|
|
83
|
+
case 'messaged': return `${actor} messaged ${obj}.`;
|
|
84
|
+
case 'sent': return `${actor} sent a message as ${obj}.`;
|
|
85
|
+
case 'configured': return `${actor} changed the settings of ${obj}${typeof a['changed'] === 'string' ? `: ${a['changed']}` : ''}.`;
|
|
86
|
+
case 'exited': return `${obj[0].toUpperCase()}${obj.slice(1)} exited with code ${String(a['exit-code'])}. Log: ${String(a['log-path'])}`;
|
|
87
|
+
case 'killed': {
|
|
88
|
+
const by = push.actor === null ? '' : ` by ${actor}`;
|
|
89
|
+
const why = typeof a['reason'] === 'string' ? ` (${a['reason']})` : '';
|
|
90
|
+
const claim = a['reason'] === 'deadline'
|
|
91
|
+
? ` It was canceled at its deadline; claim it with \`crtr node bash extend ${push.object.id} --for <duration>\` next time.`
|
|
92
|
+
: '';
|
|
93
|
+
return `${obj[0].toUpperCase()}${obj.slice(1)} was killed${by}${why}.${claim} Log: ${String(a['log-path'])}`;
|
|
94
|
+
}
|
|
95
|
+
default: return entry.label;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** One entry's card body: an inlined short report, a bounded inline message body, an event
|
|
99
|
+
* push rendered from its metadata, or the label. `report` is the report document's body when
|
|
100
|
+
* the caller could read it. */
|
|
101
|
+
export function renderEntryBody(entry, report) {
|
|
102
|
+
if (report !== null && report.length < INLINE_REPORT_MAX_CHARS && entry.ref !== undefined) {
|
|
103
|
+
return `${report.trimEnd()}\n\n${formatInboxRefInstruction(entry.ref)}`;
|
|
104
|
+
}
|
|
105
|
+
const body = inlineBody(entry);
|
|
106
|
+
if (body === '') {
|
|
107
|
+
const push = pushOf(entry);
|
|
108
|
+
if (push !== null && entry.ref === undefined)
|
|
109
|
+
return renderPush(entry, push);
|
|
110
|
+
return entry.ref === undefined ? entry.label : `${entry.label}\n\n${formatInboxRefInstruction(entry.ref)}`;
|
|
111
|
+
}
|
|
112
|
+
const { text, clipped } = clipBody(body);
|
|
113
|
+
if (entry.ref !== undefined)
|
|
114
|
+
return `${text}\n\n${formatInboxRefInstruction(entry.ref)}`;
|
|
115
|
+
return clipped ? `${text}\n… (body clipped)` : text;
|
|
116
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const DEFAULT_WORKING_GERUND = "working";
|
|
2
|
+
export declare const DEFAULT_WORKING_GERUNDS: readonly ["thinking", "planning", "checking", "working"];
|
|
3
|
+
export declare const DEFAULT_WORKING_ACTIVITY = "Working...";
|
|
4
|
+
/** Keep only usable configured labels; malformed or empty lists fall back to working. */
|
|
5
|
+
export declare function normalizeWorkingGerunds(value: unknown): string[];
|
|
6
|
+
/** Format one configured gerund as the activity label shown by every viewer. */
|
|
7
|
+
export declare function formatWorkingActivity(gerund: unknown): string;
|
|
8
|
+
/** Pick one configured gerund when the broker receives a user message. */
|
|
9
|
+
export declare function randomWorkingActivity(value: unknown, random?: () => number): string;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const DEFAULT_WORKING_GERUND = 'working';
|
|
2
|
+
export const DEFAULT_WORKING_GERUNDS = ['thinking', 'planning', 'checking', DEFAULT_WORKING_GERUND];
|
|
3
|
+
export const DEFAULT_WORKING_ACTIVITY = 'Working...';
|
|
4
|
+
/** Keep only usable configured labels; malformed or empty lists fall back to working. */
|
|
5
|
+
export function normalizeWorkingGerunds(value) {
|
|
6
|
+
if (!Array.isArray(value))
|
|
7
|
+
return [DEFAULT_WORKING_GERUND];
|
|
8
|
+
const gerunds = value
|
|
9
|
+
.filter((entry) => typeof entry === 'string')
|
|
10
|
+
.map((entry) => entry.trim())
|
|
11
|
+
.filter((entry) => entry.length > 0);
|
|
12
|
+
return gerunds.length > 0 ? gerunds : [DEFAULT_WORKING_GERUND];
|
|
13
|
+
}
|
|
14
|
+
/** Format one configured gerund as the activity label shown by every viewer. */
|
|
15
|
+
export function formatWorkingActivity(gerund) {
|
|
16
|
+
const normalized = normalizeWorkingGerunds([gerund])[0] ?? DEFAULT_WORKING_GERUND;
|
|
17
|
+
return `${normalized[0].toUpperCase()}${normalized.slice(1)}...`;
|
|
18
|
+
}
|
|
19
|
+
/** Pick one configured gerund when the broker receives a user message. */
|
|
20
|
+
export function randomWorkingActivity(value, random = Math.random) {
|
|
21
|
+
const gerunds = normalizeWorkingGerunds(value);
|
|
22
|
+
const draw = random();
|
|
23
|
+
const index = Number.isFinite(draw)
|
|
24
|
+
? Math.min(gerunds.length - 1, Math.max(0, Math.floor(draw * gerunds.length)))
|
|
25
|
+
: 0;
|
|
26
|
+
return formatWorkingActivity(gerunds[index]);
|
|
27
|
+
}
|