@roamcode.ai/server 1.0.0 → 1.0.1
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/index.d.ts +2406 -82
- package/dist/index.js +8314 -44
- package/dist/managed-update-helper.js +621 -26
- package/dist/mcp-send.js +76 -90
- package/dist/start.d.ts +591 -9
- package/dist/start.js +7629 -353
- package/package.json +3 -3
- package/dist/auth.d.ts +0 -63
- package/dist/auth.js +0 -133
- package/dist/claude-auth-service.d.ts +0 -76
- package/dist/claude-auth-service.js +0 -217
- package/dist/claude-latest-service.d.ts +0 -34
- package/dist/claude-latest-service.js +0 -61
- package/dist/config.d.ts +0 -78
- package/dist/config.js +0 -59
- package/dist/data-dir.d.ts +0 -42
- package/dist/data-dir.js +0 -70
- package/dist/diag.d.ts +0 -43
- package/dist/diag.js +0 -83
- package/dist/fs-service.d.ts +0 -90
- package/dist/fs-service.js +0 -290
- package/dist/managed-runtime.d.ts +0 -51
- package/dist/managed-runtime.js +0 -411
- package/dist/managed-update-helper.d.ts +0 -2
- package/dist/mcp-send.d.ts +0 -33
- package/dist/origin-check.d.ts +0 -37
- package/dist/origin-check.js +0 -101
- package/dist/pane-status.d.ts +0 -61
- package/dist/pane-status.js +0 -145
- package/dist/providers/claude-metadata-service.d.ts +0 -58
- package/dist/providers/claude-metadata-service.js +0 -352
- package/dist/providers/claude-provider.d.ts +0 -11
- package/dist/providers/claude-provider.js +0 -166
- package/dist/providers/codex-activity.d.ts +0 -21
- package/dist/providers/codex-activity.js +0 -122
- package/dist/providers/codex-app-server-client.d.ts +0 -90
- package/dist/providers/codex-app-server-client.js +0 -485
- package/dist/providers/codex-latest-service.d.ts +0 -50
- package/dist/providers/codex-latest-service.js +0 -174
- package/dist/providers/codex-metadata-service.d.ts +0 -161
- package/dist/providers/codex-metadata-service.js +0 -686
- package/dist/providers/codex-profile-client.d.ts +0 -16
- package/dist/providers/codex-profile-client.js +0 -52
- package/dist/providers/codex-profile-security.d.ts +0 -23
- package/dist/providers/codex-profile-security.js +0 -161
- package/dist/providers/codex-provider.d.ts +0 -15
- package/dist/providers/codex-provider.js +0 -174
- package/dist/providers/codex-thread-coordinator.d.ts +0 -18
- package/dist/providers/codex-thread-coordinator.js +0 -93
- package/dist/providers/codex-thread-persistence.d.ts +0 -9
- package/dist/providers/codex-thread-persistence.js +0 -45
- package/dist/providers/codex-thread-resolver.d.ts +0 -59
- package/dist/providers/codex-thread-resolver.js +0 -322
- package/dist/providers/options.d.ts +0 -7
- package/dist/providers/options.js +0 -155
- package/dist/providers/provider-artifacts.d.ts +0 -3
- package/dist/providers/provider-artifacts.js +0 -30
- package/dist/providers/registry.d.ts +0 -7
- package/dist/providers/registry.js +0 -23
- package/dist/providers/types.d.ts +0 -95
- package/dist/providers/types.js +0 -8
- package/dist/push-dispatch.d.ts +0 -81
- package/dist/push-dispatch.js +0 -100
- package/dist/push-store.d.ts +0 -25
- package/dist/push-store.js +0 -79
- package/dist/rate-limit.d.ts +0 -52
- package/dist/rate-limit.js +0 -72
- package/dist/server-config.d.ts +0 -60
- package/dist/server-config.js +0 -77
- package/dist/service-install.d.ts +0 -60
- package/dist/service-install.js +0 -221
- package/dist/session-defaults.d.ts +0 -26
- package/dist/session-defaults.js +0 -60
- package/dist/session-store.d.ts +0 -81
- package/dist/session-store.js +0 -654
- package/dist/static-routes.d.ts +0 -101
- package/dist/static-routes.js +0 -188
- package/dist/terminal-capability.d.ts +0 -5
- package/dist/terminal-capability.js +0 -27
- package/dist/terminal-manager.d.ts +0 -224
- package/dist/terminal-manager.js +0 -917
- package/dist/terminal-process.d.ts +0 -85
- package/dist/terminal-process.js +0 -238
- package/dist/terminal-shared.d.ts +0 -36
- package/dist/terminal-shared.js +0 -43
- package/dist/tmux-list.d.ts +0 -11
- package/dist/tmux-list.js +0 -39
- package/dist/transport.d.ts +0 -123
- package/dist/transport.js +0 -1559
- package/dist/updater.d.ts +0 -161
- package/dist/updater.js +0 -451
- package/dist/usage-service.d.ts +0 -118
- package/dist/usage-service.js +0 -173
- package/dist/vapid.d.ts +0 -17
- package/dist/vapid.js +0 -31
- package/dist/web-push-send.d.ts +0 -20
- package/dist/web-push-send.js +0 -21
- package/dist/ws-ticket.d.ts +0 -47
- package/dist/ws-ticket.js +0 -62
package/dist/index.d.ts
CHANGED
|
@@ -1,82 +1,2406 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
1
|
+
import * as better_sqlite3 from 'better-sqlite3';
|
|
2
|
+
import { FastifyInstance } from 'fastify';
|
|
3
|
+
import { spawn, ChildProcess, SpawnOptions } from 'node:child_process';
|
|
4
|
+
import { EventEmitter } from 'node:events';
|
|
5
|
+
import { ZodType } from 'zod';
|
|
6
|
+
|
|
7
|
+
interface ServerConfig {
|
|
8
|
+
claudeBin: string;
|
|
9
|
+
}
|
|
10
|
+
declare function loadConfig(env: NodeJS.ProcessEnv): ServerConfig;
|
|
11
|
+
/**
|
|
12
|
+
* Wiring for the mcp-send server (Claude → user attachments). When present, the spawn layer writes a
|
|
13
|
+
* per-session 0600 MCP config FILE (carrying the loopback base URL, this session's id, and the access
|
|
14
|
+
* token via env) and passes its PATH to the terminal spawn as `--mcp-config <path>`. The token therefore
|
|
15
|
+
* NEVER lands in any process's argv (where `ps`/`/proc` would expose it to other local users) — it
|
|
16
|
+
* lives only in the mode-0600 file. ABSENT → spawn exactly as before (the feature is additive).
|
|
17
|
+
*/
|
|
18
|
+
interface AttachSpawnOptions {
|
|
19
|
+
/** Loopback base URL of roamcode (e.g. http://127.0.0.1:4280) the tool POSTs back to. */
|
|
20
|
+
baseUrl: string;
|
|
21
|
+
/** The access token the mcp-send tool sends as `Authorization: Bearer <token>`. */
|
|
22
|
+
token: string;
|
|
23
|
+
/** Absolute path to the built dist/mcp-send.js node script. */
|
|
24
|
+
mcpScriptPath: string;
|
|
25
|
+
/** Host data dir (mode 0700) the per-session 0600 mcp-config-<id>.json is written into. */
|
|
26
|
+
dataDir: string;
|
|
27
|
+
}
|
|
28
|
+
/** The `{ mcpServers: { ... } }` document written to the per-session 0600 config file. */
|
|
29
|
+
interface McpConfigDocument {
|
|
30
|
+
mcpServers: {
|
|
31
|
+
roamcode: {
|
|
32
|
+
command: string;
|
|
33
|
+
args: string[];
|
|
34
|
+
env: {
|
|
35
|
+
RC_BASE_URL: string;
|
|
36
|
+
RC_SESSION_ID: string;
|
|
37
|
+
RC_TOKEN: string;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Build the MCP config document for a session. PURE: no fs, no token leakage — the caller writes the
|
|
44
|
+
* returned object to a 0600 file and passes its path to the terminal spawn as `--mcp-config`.
|
|
45
|
+
*/
|
|
46
|
+
declare function buildMcpConfigDocument(sessionId: string, attach: AttachSpawnOptions): McpConfigDocument;
|
|
47
|
+
/** Absolute path of the per-session MCP config file inside the data dir. */
|
|
48
|
+
declare function mcpConfigPathFor(dataDir: string, sessionId: string): string;
|
|
49
|
+
/**
|
|
50
|
+
* Per-session Claude Code settings document (written 0600, passed as `--settings <path>`). Its HOOKS let
|
|
51
|
+
* claude signal turn boundaries DIRECTLY instead of us scraping the terminal (which can't tell "generating /
|
|
52
|
+
* running a background agent" from "waiting for you" — the source of the false "needs you"):
|
|
53
|
+
* - `Stop` → claude finished its turn and is now waiting on YOU (mark awaiting).
|
|
54
|
+
* - `UserPromptSubmit` → you submitted a prompt → clear it.
|
|
55
|
+
* Each hook curls a loopback endpoint, reading the access token from a 0600 file via `-H '@authFile'` so the
|
|
56
|
+
* token never lands in the hook's argv (ps/proc), and ends in `|| true` so a hook can NEVER block/fail claude.
|
|
57
|
+
* Hooks fire in an interactive tmux pane, including under `--dangerously-skip-permissions` (verified).
|
|
58
|
+
*/
|
|
59
|
+
interface HookCommand {
|
|
60
|
+
type: "command";
|
|
61
|
+
command: string;
|
|
62
|
+
}
|
|
63
|
+
interface HooksSettingsDocument {
|
|
64
|
+
hooks: {
|
|
65
|
+
Stop: Array<{
|
|
66
|
+
hooks: HookCommand[];
|
|
67
|
+
}>;
|
|
68
|
+
UserPromptSubmit: Array<{
|
|
69
|
+
hooks: HookCommand[];
|
|
70
|
+
}>;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/** Build the per-session hooks settings. PURE: the caller writes it to a 0600 file + the 0600 `authFilePath`. */
|
|
74
|
+
declare function buildHooksSettingsDocument(sessionId: string, attach: Pick<AttachSpawnOptions, "baseUrl">, authFilePath: string): HooksSettingsDocument;
|
|
75
|
+
/** Absolute path of the per-session hooks settings file inside the data dir. */
|
|
76
|
+
declare function hooksSettingsPathFor(dataDir: string, sessionId: string): string;
|
|
77
|
+
/** Absolute path of the per-session 0600 auth-header file the hook curls read via `-H '@file'`. */
|
|
78
|
+
declare function hookAuthPathFor(dataDir: string, sessionId: string): string;
|
|
79
|
+
/** The exact bytes of that auth-header file (a single curl `-H` line). Keeps the token out of hook argv. */
|
|
80
|
+
declare function hookAuthFileContent(token: string): string;
|
|
81
|
+
|
|
82
|
+
interface ServerRuntimeConfig {
|
|
83
|
+
/** TCP port to listen on. Default 4280. */
|
|
84
|
+
port: number;
|
|
85
|
+
/** Address to bind. Default "127.0.0.1" (loopback). */
|
|
86
|
+
bindAddress: string;
|
|
87
|
+
/** Mandatory access token. Optional only for loopback binds (spec §9). */
|
|
88
|
+
accessToken?: string;
|
|
89
|
+
/** Root directory the file picker / fs-service is confined to. Default $HOME or cwd. */
|
|
90
|
+
fsRoot: string;
|
|
91
|
+
/** Max bytes accepted for an upload. Default 25 MiB. */
|
|
92
|
+
maxUploadBytes: number;
|
|
93
|
+
/** Host data dir for the SQLite DB + access token file. */
|
|
94
|
+
dataDir: string;
|
|
95
|
+
/**
|
|
96
|
+
* Trust X-Forwarded-* (passed to Fastify as `trustProxy`). Default false. PREFER a specific proxy IP/CIDR
|
|
97
|
+
* (e.g. "127.0.0.1" for a same-host cloudflared/Caddy) over `true`: `true` trusts EVERY hop and takes the
|
|
98
|
+
* left-most XFF entry, which a client can prepend to spoof `request.ip` and poison the rate limiter. A
|
|
99
|
+
* string here is Fastify's trustProxy spec (IP, CIDR, or comma-list); boolean true = trust all hops.
|
|
100
|
+
*/
|
|
101
|
+
trustProxy?: boolean | string;
|
|
102
|
+
/**
|
|
103
|
+
* The public-facing origin (ROAMCODE_PUBLIC_URL). Used by the Origin/CSWSH guard as an allow-listed
|
|
104
|
+
* origin (the PWA is installed under this when behind a tunnel) AND by start.ts for push deep-links.
|
|
105
|
+
*/
|
|
106
|
+
publicUrl?: string;
|
|
107
|
+
/**
|
|
108
|
+
* Extra Origins the CSWSH guard allows, beyond same-origin / loopback / publicUrl
|
|
109
|
+
* (ROAMCODE_ALLOWED_ORIGINS, comma-separated). Empty by default — the safe default already lets the
|
|
110
|
+
* real app through; this is only to permit an additional known front-end origin.
|
|
111
|
+
*/
|
|
112
|
+
allowedOrigins: string[];
|
|
113
|
+
/**
|
|
114
|
+
* Global per-client request rate limit (token bucket). `rateLimitRpm` requests/minute sustained,
|
|
115
|
+
* `rateLimitBurst` instantaneous. Set rateLimitRpm to 0 to DISABLE the limiter entirely.
|
|
116
|
+
* (ROAMCODE_RATE_LIMIT_RPM / ROAMCODE_RATE_LIMIT_BURST.)
|
|
117
|
+
*/
|
|
118
|
+
rateLimitRpm: number;
|
|
119
|
+
rateLimitBurst: number;
|
|
120
|
+
/** Max concurrent LIVE sessions/processes; POST /sessions is refused (429) at the cap. 0 disables the
|
|
121
|
+
* cap. (ROAMCODE_MAX_SESSIONS.) */
|
|
122
|
+
maxSessions: number;
|
|
123
|
+
/** Kill running terminal sessions with NO attached client that have been idle longer than this many ms.
|
|
124
|
+
* 0 (default) DISABLES it — sessions survive a disconnect indefinitely for later reattach. Opt-in
|
|
125
|
+
* (SESSION_IDLE_TTL_MS) for hosts that want to bound detached claude+tmux accumulation. Optional in the
|
|
126
|
+
* type (older config literals / tests omit it); consumers treat an absent value as 0 (disabled). */
|
|
127
|
+
sessionIdleTtlMs?: number;
|
|
128
|
+
/** The claude-spawn config (claudeBin). */
|
|
129
|
+
claude: ServerConfig;
|
|
130
|
+
/** Codex CLI executable. Defaults to `codex`; independently overridable with CODEX_BIN. */
|
|
131
|
+
codexBin: string;
|
|
132
|
+
}
|
|
133
|
+
declare function loadServerConfig(env: NodeJS.ProcessEnv): ServerRuntimeConfig;
|
|
134
|
+
declare function isLoopbackAddress(address: string): boolean;
|
|
135
|
+
/** Spec §9: refuse to serve a non-loopback bind without a token. */
|
|
136
|
+
declare function assertConfigAllowsStart(cfg: ServerRuntimeConfig): void;
|
|
137
|
+
|
|
138
|
+
declare function extractBearerToken(authorizationHeader: string | undefined): string | undefined;
|
|
139
|
+
interface AuthGateOptions {
|
|
140
|
+
token?: string;
|
|
141
|
+
/** Consecutive failures from one client before it is locked out. Default 10. */
|
|
142
|
+
maxFailures?: number;
|
|
143
|
+
/** Lockout duration in ms. Default 60000. */
|
|
144
|
+
lockoutMs?: number;
|
|
145
|
+
/**
|
|
146
|
+
* Grace window (ms) after {@link AuthGate.rotateToken} during which the PREVIOUS token is still
|
|
147
|
+
* accepted, so a callback already in flight with the old token (e.g. a running mcp-send subprocess that
|
|
148
|
+
* captured RC_TOKEN at spawn) survives the rotation instead of 401-ing mid-conversation. Default 60000.
|
|
149
|
+
*/
|
|
150
|
+
graceMs?: number;
|
|
151
|
+
/** Injectable clock for tests. Default Date.now. */
|
|
152
|
+
now?: () => number;
|
|
153
|
+
}
|
|
154
|
+
type AuthCheckResult = {
|
|
155
|
+
ok: true;
|
|
156
|
+
} | {
|
|
157
|
+
ok: false;
|
|
158
|
+
reason: "locked" | "invalid" | "missing-token-config";
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* Verifies a bearer access token in constant time and applies a per-client
|
|
162
|
+
* lockout after repeated failures. Pure / I/O-free so it is unit-testable;
|
|
163
|
+
* Task 8 wires it as a single global Fastify `preHandler` that also gates the
|
|
164
|
+
* WebSocket upgrade.
|
|
165
|
+
*
|
|
166
|
+
* Proxy lockout caveat: the lockout is keyed by `clientKey` (`request.ip`).
|
|
167
|
+
* Behind a reverse proxy (Caddy / Cloudflare) `request.ip` is the proxy's IP
|
|
168
|
+
* for EVERY client, collapsing the per-client lockout to one shared key. To
|
|
169
|
+
* stop that from self-DoS-ing the sole legitimate user, {@link check} validates
|
|
170
|
+
* the token FIRST: a CORRECT token is ALWAYS accepted, even while its key is
|
|
171
|
+
* locked out — the lockout only ever throttles WRONG guesses. Supplying a real
|
|
172
|
+
* per-client key (Fastify `trustProxy` scoped to the proxy) still makes the
|
|
173
|
+
* throttle per-client, but is no longer required to keep the user from being
|
|
174
|
+
* locked out by someone else's bad traffic.
|
|
175
|
+
*/
|
|
176
|
+
declare class AuthGate {
|
|
177
|
+
private token?;
|
|
178
|
+
private previousToken?;
|
|
179
|
+
private previousValidUntil;
|
|
180
|
+
private readonly maxFailures;
|
|
181
|
+
private readonly lockoutMs;
|
|
182
|
+
private readonly graceMs;
|
|
183
|
+
private readonly now;
|
|
184
|
+
private readonly clients;
|
|
185
|
+
constructor(opts: AuthGateOptions);
|
|
186
|
+
check(presentedToken: string | undefined, clientKey: string): AuthCheckResult;
|
|
187
|
+
/**
|
|
188
|
+
* Atomically swap in a fresh access token (POST /token/rotate). Every subsequent {@link check} compares
|
|
189
|
+
* against the new token; the OLD token is accepted only for a brief grace window (graceMs) so in-flight
|
|
190
|
+
* callbacks holding it (a running mcp-send subprocess) survive the rotation instead of 401-ing mid-turn.
|
|
191
|
+
* A 2nd rotation within the window REPLACES previousToken with the most-recent old token (no list grows).
|
|
192
|
+
* Also clears all per-client lockout state — rotation is an explicit administrative reset, so a fresh
|
|
193
|
+
* slate is correct (and avoids leaving a client locked out against a token that no longer exists).
|
|
194
|
+
*/
|
|
195
|
+
rotateToken(newToken: string): void;
|
|
196
|
+
/** Drop entries whose lockout has expired and whose failure count is 0 — keeps the map bounded. */
|
|
197
|
+
private sweepExpired;
|
|
198
|
+
/** TEST ONLY: number of tracked clients currently locked (lockedUntil in the future). */
|
|
199
|
+
lockedClientCount(): number;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
type FsErrorCode = "forbidden" | "not-found" | "exists";
|
|
203
|
+
declare class FsError extends Error {
|
|
204
|
+
readonly code: FsErrorCode;
|
|
205
|
+
constructor(code: FsErrorCode, message: string);
|
|
206
|
+
}
|
|
207
|
+
interface DirEntry {
|
|
208
|
+
name: string;
|
|
209
|
+
path: string;
|
|
210
|
+
isDirectory: boolean;
|
|
211
|
+
isGitRepo: boolean;
|
|
212
|
+
gitBranch?: string;
|
|
213
|
+
}
|
|
214
|
+
interface DirListing {
|
|
215
|
+
path: string;
|
|
216
|
+
parent?: string;
|
|
217
|
+
entries: DirEntry[];
|
|
218
|
+
}
|
|
219
|
+
interface FsServiceOptions {
|
|
220
|
+
root: string;
|
|
221
|
+
}
|
|
222
|
+
/** One GET /fs/search hit — a DIRECTORY whose name matched, shaped for the picker. */
|
|
223
|
+
interface DirSearchResult {
|
|
224
|
+
path: string;
|
|
225
|
+
name: string;
|
|
226
|
+
isGitRepo: boolean;
|
|
227
|
+
}
|
|
228
|
+
/** GET /fs/search walk bounds. Depth 5 / 400 dirs keeps the worst case (a huge home dir) to a bounded,
|
|
229
|
+
* sub-second readdir sweep; 30 results is more than a picker list ever shows. */
|
|
230
|
+
declare const SEARCH_MAX_DEPTH = 5;
|
|
231
|
+
declare const SEARCH_MAX_DIRS = 400;
|
|
232
|
+
declare const SEARCH_MAX_RESULTS = 30;
|
|
233
|
+
declare class FsService {
|
|
234
|
+
private readonly root;
|
|
235
|
+
constructor(opts: FsServiceOptions);
|
|
236
|
+
/** Resolve a target (absolute or relative to root) and confine it to root. */
|
|
237
|
+
resolveWithinRoot(target: string): string;
|
|
238
|
+
/** Resolve real paths so a symlink inside root cannot point outside it. Missing -> not-found. */
|
|
239
|
+
private realWithinRoot;
|
|
240
|
+
listDirectory(target: string): Promise<DirListing>;
|
|
241
|
+
/** Read .git/HEAD cheaply; return the branch name or undefined if not a repo. */
|
|
242
|
+
private readGitBranch;
|
|
243
|
+
readFileForDownload(target: string): Promise<{
|
|
244
|
+
filename: string;
|
|
245
|
+
data: Buffer;
|
|
246
|
+
}>;
|
|
247
|
+
/**
|
|
248
|
+
* Validate that `target` is a real in-root file and describe it for an attachment frame WITHOUT
|
|
249
|
+
* reading its bytes. Reuses the same resolveWithinRoot + realpath defense as readFileForDownload so
|
|
250
|
+
* a traversal/symlink-escape path throws FsError("forbidden") and a missing path FsError("not-found").
|
|
251
|
+
*/
|
|
252
|
+
describeForAttachment(target: string): Promise<{
|
|
253
|
+
name: string;
|
|
254
|
+
isImage: boolean;
|
|
255
|
+
}>;
|
|
256
|
+
writeUploadedFile(targetDir: string, filename: string, data: Buffer): Promise<{
|
|
257
|
+
path: string;
|
|
258
|
+
}>;
|
|
259
|
+
/** Ensure a directory (confined to root) exists, creating parents as needed; returns its absolute path. */
|
|
260
|
+
ensureDirWithinRoot(target: string): Promise<string>;
|
|
261
|
+
/**
|
|
262
|
+
* POST /fs/mkdir: create ONE new directory (recursive:false — the picker creates a single folder under
|
|
263
|
+
* an existing parent; a missing parent is a client bug, not something to silently paper over). Same
|
|
264
|
+
* confinement as listDirectory: resolveWithinRoot on the target + realpath on the PARENT (the target
|
|
265
|
+
* itself doesn't exist yet), so a symlinked parent can't smuggle the create outside root.
|
|
266
|
+
* Errors: "exists" (→409) when the path is already taken, "not-found" (→404) when the parent is
|
|
267
|
+
* missing, "forbidden" (→403) on any escape.
|
|
268
|
+
*/
|
|
269
|
+
makeDirectory(target: string): Promise<{
|
|
270
|
+
path: string;
|
|
271
|
+
}>;
|
|
272
|
+
/**
|
|
273
|
+
* GET /fs/search: case-insensitive SUBSTRING match on DIRECTORY names under `base` (default root),
|
|
274
|
+
* for the project picker's "type to find your repo" flow. Breadth-first — so results come back
|
|
275
|
+
* shallowest-first without a sort — bounded by {@link SEARCH_MAX_DEPTH} / {@link SEARCH_MAX_DIRS} /
|
|
276
|
+
* {@link SEARCH_MAX_RESULTS} so a giant tree can never wedge the request. Dot-entries and node_modules
|
|
277
|
+
* are skipped (never a project root; node_modules alone would blow the dir budget). Unreadable dirs are
|
|
278
|
+
* skipped, not fatal. Same fsRoot confinement as every other fs route; entries are reported by their
|
|
279
|
+
* in-root path (symlinked children are NOT re-resolved — mirroring listDirectory).
|
|
280
|
+
*/
|
|
281
|
+
searchDirectories(query: string, base?: string): Promise<DirSearchResult[]>;
|
|
282
|
+
/** Prune expired files inside EACH immediate subdirectory of `base` (confined to root). Used to age out
|
|
283
|
+
* the terminal shared-files folders (one per session) in a single sweep — including ORPHANED folders
|
|
284
|
+
* whose session no longer exists (the old per-live-session sweep leaked those forever). Best-effort:
|
|
285
|
+
* returns the total files removed; a missing base / unreadable entry is skipped, not thrown. */
|
|
286
|
+
pruneChildDirsOlderThan(base: string, maxAgeMs: number, now?: number): Promise<number>;
|
|
287
|
+
/** Delete top-level regular files in `target` (confined to root) whose mtime is older than `maxAgeMs`.
|
|
288
|
+
* Best-effort — returns how many were removed; a missing dir / unreadable entry is skipped, not thrown.
|
|
289
|
+
* Subdirectories are left untouched. Used to give terminal shared-files uploads a bounded lifetime. */
|
|
290
|
+
pruneOlderThan(target: string, maxAgeMs: number, now?: number): Promise<number>;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
type ProviderId = "claude" | "codex";
|
|
294
|
+
interface ProviderAvailability {
|
|
295
|
+
terminalAvailable: boolean;
|
|
296
|
+
metadataAvailable: boolean;
|
|
297
|
+
version?: string;
|
|
298
|
+
detail?: string;
|
|
299
|
+
}
|
|
300
|
+
declare class ProviderError extends Error {
|
|
301
|
+
readonly code: "PROVIDER_UNAVAILABLE" | "INVALID_PROVIDER_OPTIONS" | "RESUME_IDENTITY_UNAVAILABLE" | "OSS_PROVIDER_DEFERRED";
|
|
302
|
+
constructor(code: "PROVIDER_UNAVAILABLE" | "INVALID_PROVIDER_OPTIONS" | "RESUME_IDENTITY_UNAVAILABLE" | "OSS_PROVIDER_DEFERRED", message: string);
|
|
303
|
+
}
|
|
304
|
+
type ClaudeSessionOptions = {
|
|
305
|
+
provider: "claude";
|
|
306
|
+
model?: string;
|
|
307
|
+
effort?: string;
|
|
308
|
+
permissionMode?: "default" | "acceptEdits" | "plan" | "bypassPermissions";
|
|
309
|
+
dangerouslySkip?: boolean;
|
|
310
|
+
addDirs?: string[];
|
|
311
|
+
legacyArgs?: string[];
|
|
312
|
+
};
|
|
313
|
+
type CodexSessionOptions = {
|
|
314
|
+
provider: "codex";
|
|
315
|
+
model?: string;
|
|
316
|
+
reasoningEffort?: string;
|
|
317
|
+
sandbox?: "read-only" | "workspace-write" | "danger-full-access";
|
|
318
|
+
approvalPolicy?: "untrusted" | "on-request" | "never";
|
|
319
|
+
profile?: string;
|
|
320
|
+
webSearch?: boolean;
|
|
321
|
+
dangerouslyBypassApprovalsAndSandbox?: boolean;
|
|
322
|
+
addDirs?: string[];
|
|
323
|
+
};
|
|
324
|
+
type ProviderSessionOptions = ClaudeSessionOptions | CodexSessionOptions;
|
|
325
|
+
type LaunchIntent = "fresh" | "resume";
|
|
326
|
+
interface ProcessSpec {
|
|
327
|
+
executable: string;
|
|
328
|
+
args: string[];
|
|
329
|
+
env: NodeJS.ProcessEnv;
|
|
330
|
+
cleanupPaths: string[];
|
|
331
|
+
/** Revalidates ephemeral launch proof immediately before the terminal process is spawned. */
|
|
332
|
+
preSpawnCheck?: () => void | Promise<void>;
|
|
333
|
+
integration?: {
|
|
334
|
+
attachments: "ready" | "degraded";
|
|
335
|
+
activity: "ready" | "degraded";
|
|
336
|
+
detail?: string;
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
interface CodexProfileLaunchProof {
|
|
340
|
+
readonly profile: string;
|
|
341
|
+
readonly codexHome: string;
|
|
342
|
+
readonly assertUnchanged: () => Promise<void>;
|
|
343
|
+
}
|
|
344
|
+
interface ProviderProcessContext {
|
|
345
|
+
roamSessionId: string;
|
|
346
|
+
cwd: string;
|
|
347
|
+
intent: LaunchIntent;
|
|
348
|
+
options: ProviderSessionOptions;
|
|
349
|
+
providerSessionId?: string;
|
|
350
|
+
/** Late-bound after listen so both providers share the real loopback MCP callback configuration. */
|
|
351
|
+
attach?: AttachSpawnOptions;
|
|
352
|
+
/** Transfer temporary-artifact ownership as soon as paths exist, so a later build rejection is cleanable. */
|
|
353
|
+
registerCleanupPaths?(paths: readonly string[]): void;
|
|
354
|
+
}
|
|
355
|
+
type ProviderRuntimeSignal = {
|
|
356
|
+
type: "working";
|
|
357
|
+
} | {
|
|
358
|
+
type: "blocked";
|
|
359
|
+
} | {
|
|
360
|
+
type: "idle";
|
|
361
|
+
} | {
|
|
362
|
+
type: "provider-session-id";
|
|
363
|
+
id: string;
|
|
364
|
+
};
|
|
365
|
+
interface ProviderRuntimeSignalParser {
|
|
366
|
+
push(chunk: string): ProviderRuntimeSignal[];
|
|
367
|
+
}
|
|
368
|
+
interface ProviderRuntimeMetadata {
|
|
369
|
+
model?: string;
|
|
370
|
+
effort?: string;
|
|
371
|
+
}
|
|
372
|
+
interface AgentProvider {
|
|
373
|
+
readonly id: ProviderId;
|
|
374
|
+
readonly displayName: string;
|
|
375
|
+
readonly resumeIdentity: "optional" | "required";
|
|
376
|
+
probe(): Promise<ProviderAvailability>;
|
|
377
|
+
buildProcess(context: ProviderProcessContext): Promise<ProcessSpec>;
|
|
378
|
+
/** Stateful parsers must be created per spawned process; providers are registry singletons. */
|
|
379
|
+
createRuntimeSignalParser?(): ProviderRuntimeSignalParser;
|
|
380
|
+
runtimeSignals(chunk: string): ProviderRuntimeSignal[];
|
|
381
|
+
classifyPane(pane: string): "working" | "blocked" | "idle";
|
|
382
|
+
/** Read provider-owned live model/effort chrome from a captured pane. Optional because not every TUI
|
|
383
|
+
* exposes these values. A missing/invalid result must leave launch metadata untouched. */
|
|
384
|
+
runtimeMetadata?(pane: string): ProviderRuntimeMetadata | undefined;
|
|
385
|
+
cleanup(paths: readonly string[]): void;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
interface SessionDefaults {
|
|
389
|
+
effort: string;
|
|
390
|
+
model?: string;
|
|
391
|
+
dangerouslySkip: boolean;
|
|
392
|
+
permissionMode?: string;
|
|
393
|
+
codex?: {
|
|
394
|
+
model?: string;
|
|
395
|
+
reasoningEffort?: string;
|
|
396
|
+
sandbox?: "read-only" | "workspace-write" | "danger-full-access";
|
|
397
|
+
approvalPolicy?: "untrusted" | "on-request" | "never";
|
|
398
|
+
profile?: string;
|
|
399
|
+
webSearch?: boolean;
|
|
400
|
+
addDirs?: string[];
|
|
401
|
+
dangerouslyBypassApprovalsAndSandbox?: boolean;
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
interface StoredSessionDefaults {
|
|
405
|
+
defaults: SessionDefaults;
|
|
406
|
+
revision: number;
|
|
407
|
+
updatedAt: number;
|
|
408
|
+
}
|
|
409
|
+
declare function normalizeSessionDefaults(value: unknown): SessionDefaults;
|
|
410
|
+
declare class SessionDefaultsConflictError extends Error {
|
|
411
|
+
readonly current: StoredSessionDefaults | undefined;
|
|
412
|
+
constructor(current: StoredSessionDefaults | undefined);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
type StoredStatus = "running" | "dormant" | "errored" | "stopped";
|
|
416
|
+
interface StoredSessionBase {
|
|
417
|
+
id: string;
|
|
418
|
+
cwd: string;
|
|
419
|
+
status: StoredStatus;
|
|
420
|
+
createdAt: number;
|
|
421
|
+
lastActivityAt: number;
|
|
422
|
+
/** Always "terminal" — the only session kind. Kept so rehydrate can filter/guard on it. */
|
|
423
|
+
mode: "terminal";
|
|
424
|
+
/** Optional user-set display name (PATCH /sessions/:id). Absent = unnamed (the UI shows the cwd). */
|
|
425
|
+
name?: string;
|
|
426
|
+
}
|
|
427
|
+
interface StoredClaudeSession extends StoredSessionBase {
|
|
428
|
+
provider: "claude";
|
|
429
|
+
dangerouslySkip: boolean;
|
|
430
|
+
/** The USER-chosen spawn flags the session was created with (`--model`/`--effort`/`--permission-mode`/
|
|
431
|
+
* `--dangerously-skip-permissions`/`--add-dir …`) — everything EXCEPT the ephemeral per-session
|
|
432
|
+
* `--mcp-config`/`--settings` paths (those are regenerated per spawn). Persisted so a RESTART can
|
|
433
|
+
* respawn the ended session with the same settings instead of a bare claude. Absent = no extra flags. */
|
|
434
|
+
spawnArgs?: string[];
|
|
435
|
+
providerSessionId?: never;
|
|
436
|
+
launchOptions?: never;
|
|
437
|
+
integrationStatus?: never;
|
|
438
|
+
}
|
|
439
|
+
interface StoredIntegrationStatus {
|
|
440
|
+
attachments: "ready" | "degraded";
|
|
441
|
+
activity: "ready" | "degraded";
|
|
442
|
+
detail?: string;
|
|
443
|
+
}
|
|
444
|
+
interface StoredCodexSession extends StoredSessionBase {
|
|
445
|
+
provider: "codex";
|
|
446
|
+
launchOptions: CodexSessionOptions;
|
|
447
|
+
providerSessionId?: string;
|
|
448
|
+
integrationStatus?: StoredIntegrationStatus;
|
|
449
|
+
dangerouslySkip?: never;
|
|
450
|
+
spawnArgs?: never;
|
|
451
|
+
}
|
|
452
|
+
type StoredSession = StoredClaudeSession | StoredCodexSession;
|
|
453
|
+
/** How a store is actually backed: "sqlite" (durable) or "memory-fallback" (the native module failed to
|
|
454
|
+
* load — NOT durable across restarts). Surfaced so start.ts can warn loudly + /diag can report it. */
|
|
455
|
+
type StoreMode = "sqlite" | "memory-fallback";
|
|
456
|
+
interface SessionStore {
|
|
457
|
+
/** Atomically claim a brand-new physical id. Rejects any existing owner without changing its data. */
|
|
458
|
+
claimNew(session: StoredSession): void;
|
|
459
|
+
upsert(session: StoredSession): void;
|
|
460
|
+
/** True when any physical provider table owns the id, including corrupt or cross-table ambiguous rows. */
|
|
461
|
+
has(id: string): boolean;
|
|
462
|
+
get(id: string): StoredSession | undefined;
|
|
463
|
+
list(): StoredSession[];
|
|
464
|
+
setStatus(id: string, status: StoredStatus): void;
|
|
465
|
+
touch(id: string, at: number): void;
|
|
466
|
+
/** Set/clear a session's display name (undefined clears — the row's `name` goes back to NULL). */
|
|
467
|
+
setName(id: string, name: string | undefined): void;
|
|
468
|
+
/** Persist/clear the exact resumable identity for Codex sessions only. Claude rows are never touched. */
|
|
469
|
+
setProviderSessionId(id: string, value: string | undefined): void;
|
|
470
|
+
/** Claim a hidden, non-resumable identity pending exact-thread cross-check. */
|
|
471
|
+
markProvisionalProviderSessionId(id: string, value: string): void;
|
|
472
|
+
/** Remove only the matching hidden provisional identity. */
|
|
473
|
+
clearProvisionalProviderSessionId(id: string, value: string): void;
|
|
474
|
+
/** Atomically promote only the matching hidden provisional identity to resumable state. */
|
|
475
|
+
commitProvisionalProviderSessionId(id: string, value: string): void;
|
|
476
|
+
getSessionDefaults(): StoredSessionDefaults | undefined;
|
|
477
|
+
putSessionDefaults(defaults: SessionDefaults, expectedRevision: number, updatedAt: number): StoredSessionDefaults;
|
|
478
|
+
delete(id: string): void;
|
|
479
|
+
close(): void;
|
|
480
|
+
/** "sqlite" when better-sqlite3 loaded; "memory-fallback" when it didn't (non-durable). */
|
|
481
|
+
readonly mode: StoreMode;
|
|
482
|
+
}
|
|
483
|
+
interface OpenSessionStoreOptions {
|
|
484
|
+
/** Path to the SQLite file. ":memory:" uses an in-process DB. */
|
|
485
|
+
dbPath: string;
|
|
486
|
+
/** Injectable better-sqlite3 loader (the seam tests use to FORCE the in-memory fallback by throwing).
|
|
487
|
+
* Defaults to `require("better-sqlite3")`. */
|
|
488
|
+
loadDatabase?: () => typeof better_sqlite3;
|
|
489
|
+
}
|
|
490
|
+
declare function openSessionStore(opts: OpenSessionStoreOptions): SessionStore;
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Host data dir for the SQLite DB + access token. Never inside the project tree by default.
|
|
494
|
+
*
|
|
495
|
+
* Rename compat (Remote Coder → RoamCode): installs that predate the rename keep their data where it
|
|
496
|
+
* already lives — the legacy `REMOTE_CODER_DATA_DIR` env still wins over defaults, and an existing
|
|
497
|
+
* `…/remote-coder` dir is preferred over creating a fresh `…/roamcode` (their token / service.json /
|
|
498
|
+
* session index MUST survive an OTA update). Only brand-new installs get the new directory name.
|
|
499
|
+
*/
|
|
500
|
+
declare function resolveDataDir(env: NodeJS.ProcessEnv, exists?: (p: string) => boolean): string;
|
|
501
|
+
declare function ensureDataDir(dir: string): void;
|
|
502
|
+
/**
|
|
503
|
+
* Strong default token: 32 bytes of CSPRNG entropy (>= spec §9 baseline),
|
|
504
|
+
* base64url-encoded (43 chars, no padding). Never Math.random / randomUUID.
|
|
505
|
+
*/
|
|
506
|
+
declare function generateAccessToken(): string;
|
|
507
|
+
/**
|
|
508
|
+
* Persist a token to `<dataDir>/token` with mode 0600 (overwriting any prior file). Used by both first-run
|
|
509
|
+
* generation and POST /token/rotate. `writeFileSync`'s `mode` is honored only when CREATING a file, so we
|
|
510
|
+
* chmod unconditionally afterwards — a rotated secret can never land in a too-permissive (e.g. world-
|
|
511
|
+
* readable) file left behind by an earlier write.
|
|
512
|
+
*/
|
|
513
|
+
declare function persistAccessToken(dataDir: string, token: string): void;
|
|
514
|
+
interface ResolveAccessTokenOptions {
|
|
515
|
+
/** From ACCESS_TOKEN; when set it wins and is not persisted. */
|
|
516
|
+
configured?: string;
|
|
517
|
+
dataDir: string;
|
|
518
|
+
/** Injectable generator for tests. Defaults to a 32-byte base64url CSPRNG token. */
|
|
519
|
+
generate?: () => string;
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Spec §9: a long random secret generated on first run (printed once, stored).
|
|
523
|
+
* Precedence: explicit ACCESS_TOKEN > persisted token file > freshly generated.
|
|
524
|
+
*
|
|
525
|
+
* A configured (env) token is used verbatim and never written to disk. A
|
|
526
|
+
* generated token is persisted to `<dataDir>/token` with mode 0600 so other
|
|
527
|
+
* users on the host cannot read it; `generated: true` lets the caller print it
|
|
528
|
+
* once with the access URL.
|
|
529
|
+
*/
|
|
530
|
+
declare function resolveAccessToken(opts: ResolveAccessTokenOptions): {
|
|
531
|
+
token: string;
|
|
532
|
+
generated: boolean;
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Origin / CSWSH (cross-site WebSocket hijacking) guard for the WS upgrade and state-changing HTTP.
|
|
537
|
+
*
|
|
538
|
+
* The token is the only auth boundary, and it can leak into a URL (`?token=` connect link, push deep
|
|
539
|
+
* link, browser history). The WS handshake (and any browser-driven HTTP) carries an `Origin` header the
|
|
540
|
+
* page CANNOT forge — so a malicious cross-origin page that somehow holds the token still can't make the
|
|
541
|
+
* BROWSER attach to our WS / drive our API, because the browser stamps its OWN origin on the request and
|
|
542
|
+
* we reject it. Without this check, a leaked-in-URL token is enough for a foreign page to puppet the host.
|
|
543
|
+
*
|
|
544
|
+
* SAFE DEFAULT (must never reject the real app): a request is allowed when
|
|
545
|
+
* - the `Origin` header is ABSENT (native/non-browser clients, same-origin navigations + most
|
|
546
|
+
* same-origin GETs omit it — the PWA's own fetches are same-origin), OR
|
|
547
|
+
* - the Origin's host:port equals the request `Host` (same-origin — the PWA always is), OR
|
|
548
|
+
* - the Origin matches the configured public URL (ROAMCODE_PUBLIC_URL), OR
|
|
549
|
+
* - the Origin is a loopback/localhost origin (local dev), OR
|
|
550
|
+
* - the Origin is in the explicit ROAMCODE_ALLOWED_ORIGINS allow-list.
|
|
551
|
+
* Only a PRESENT, cross-origin, non-allow-listed Origin is rejected (403).
|
|
552
|
+
*/
|
|
553
|
+
/** Normalize a URL/origin string to a comparable `scheme://host[:port]` origin (lowercased). Returns
|
|
554
|
+
* undefined for anything that isn't a parseable absolute URL (so it can never match by accident). */
|
|
555
|
+
declare function normalizeOrigin(value: string | undefined): string | undefined;
|
|
556
|
+
interface OriginCheckOptions {
|
|
557
|
+
/** The configured public-facing origin (from ROAMCODE_PUBLIC_URL). May be a full URL. */
|
|
558
|
+
publicUrl?: string;
|
|
559
|
+
/** Extra allow-listed origins (ROAMCODE_ALLOWED_ORIGINS, comma-separated → array). */
|
|
560
|
+
allowedOrigins?: string[];
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Decide whether a request's Origin is allowed. PURE / I/O-free so it is unit-testable; the transport
|
|
564
|
+
* preHandler passes `origin` (the header) and `host` (the request Host header).
|
|
565
|
+
*
|
|
566
|
+
* @param origin the request's `Origin` header (may be undefined)
|
|
567
|
+
* @param host the request's `Host` header (e.g. `remotecode.example.com` or `127.0.0.1:4280`)
|
|
568
|
+
*/
|
|
569
|
+
declare function isOriginAllowed(origin: string | undefined, host: string | undefined, opts?: OriginCheckOptions): boolean;
|
|
570
|
+
/** Parse a comma-separated ROAMCODE_ALLOWED_ORIGINS value into a trimmed, non-empty list. */
|
|
571
|
+
declare function parseAllowedOrigins(raw: string | undefined): string[];
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Lightweight, in-memory, per-client rate limiter for the global preHandler. The per-IP auth LOCKOUT
|
|
575
|
+
* (auth.ts) only counts FAILED auth attempts and collapses behind a proxy; this adds a generous request
|
|
576
|
+
* cap so a flood (auth-guessing, scraping, accidental client loop) can't hammer the host — while staying
|
|
577
|
+
* well clear of any legitimate use (the PWA's periodic /sessions + /version poll, a burst of opens).
|
|
578
|
+
*
|
|
579
|
+
* Algorithm: a TOKEN BUCKET per client key (request.ip, honoring trustProxy — same key as the lockout).
|
|
580
|
+
* `capacity` tokens, refilled continuously at `capacity / windowMs` tokens/ms; each request spends one.
|
|
581
|
+
* A token bucket allows a short BURST (up to `capacity`) yet bounds the sustained rate — friendlier to
|
|
582
|
+
* bursty real traffic than a hard fixed window. PURE/I-O-free + an injectable clock so it is unit-testable.
|
|
583
|
+
*
|
|
584
|
+
* SAFE DEFAULTS (see server-config.ts): 600 requests / 60s sustained, burst 120 — orders of magnitude
|
|
585
|
+
* above the app's real cadence. Env-overridable and fully DISABLE-able (enabled:false → allow everything).
|
|
586
|
+
*/
|
|
587
|
+
interface RateLimiterOptions {
|
|
588
|
+
/** Sustained budget: `capacity` requests refill over `windowMs` (→ capacity/windowMs tokens per ms). */
|
|
589
|
+
capacity: number;
|
|
590
|
+
/** Refill window in ms for the full capacity. */
|
|
591
|
+
windowMs: number;
|
|
592
|
+
/** Max burst (bucket size). Defaults to `capacity`. A larger value tolerates a bigger instantaneous spike. */
|
|
593
|
+
burst?: number;
|
|
594
|
+
/** Master switch. false → {@link RateLimiter.take} always allows (the limiter is off). Default true. */
|
|
595
|
+
enabled?: boolean;
|
|
596
|
+
/** Injectable clock for tests. Default Date.now. */
|
|
597
|
+
now?: () => number;
|
|
598
|
+
}
|
|
599
|
+
interface RateLimitDecision {
|
|
600
|
+
/** True → allow the request (a token was spent or the limiter is off). */
|
|
601
|
+
allowed: boolean;
|
|
602
|
+
/** When NOT allowed, seconds the client should wait before retrying (for the Retry-After header). */
|
|
603
|
+
retryAfterSeconds: number;
|
|
604
|
+
}
|
|
605
|
+
declare class RateLimiter {
|
|
606
|
+
private readonly capacity;
|
|
607
|
+
private readonly windowMs;
|
|
608
|
+
private readonly burst;
|
|
609
|
+
private readonly enabled;
|
|
610
|
+
private readonly now;
|
|
611
|
+
private readonly ratePerMs;
|
|
612
|
+
private readonly clients;
|
|
613
|
+
constructor(opts: RateLimiterOptions);
|
|
614
|
+
/**
|
|
615
|
+
* Account for ONE request from `clientKey`. Refills the bucket by elapsed time, then spends a token if
|
|
616
|
+
* one is available. Returns {allowed:true} when a token was spent (or the limiter is disabled), else
|
|
617
|
+
* {allowed:false, retryAfterSeconds} — the whole-seconds wait until the next token is available.
|
|
618
|
+
*/
|
|
619
|
+
take(clientKey: string): RateLimitDecision;
|
|
620
|
+
/** Drop fully-refilled buckets so the map stays bounded (a one-shot client doesn't linger forever). */
|
|
621
|
+
private sweepFull;
|
|
622
|
+
/** TEST ONLY: number of tracked client buckets. */
|
|
623
|
+
trackedClientCount(): number;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
interface VapidKeys {
|
|
627
|
+
/** URL-safe base64 public key — safe to send to the browser for the push subscription. */
|
|
628
|
+
publicKey: string;
|
|
629
|
+
/** URL-safe base64 private key — NEVER leaves the server (signs pushes). */
|
|
630
|
+
privateKey: string;
|
|
631
|
+
}
|
|
632
|
+
interface ResolveVapidKeysOptions {
|
|
633
|
+
dataDir: string;
|
|
634
|
+
/** Injectable generator for tests. Defaults to web-push's NIST P-256 generator. */
|
|
635
|
+
generate?: () => VapidKeys;
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Generate the VAPID keypair on first run and persist it to `<dataDir>/vapid.json` (mode 0600 — the
|
|
639
|
+
* private key is a server secret). On subsequent runs the persisted pair is reused so existing
|
|
640
|
+
* browser subscriptions stay valid (rotating the key would invalidate every subscription).
|
|
641
|
+
*/
|
|
642
|
+
declare function resolveVapidKeys(opts: ResolveVapidKeysOptions): VapidKeys;
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Single-use, short-lived tickets for the terminal WebSocket handshake.
|
|
646
|
+
*
|
|
647
|
+
* WHY: a browser WebSocket cannot send an Authorization header, so the client historically put the
|
|
648
|
+
* LONG-LIVED access token in the WS URL (`?token=`). Query strings are routinely written into proxy /
|
|
649
|
+
* tunnel / access logs, so that leaked a full-access credential. The flow now is: POST /ws-ticket (over
|
|
650
|
+
* the normal header-authed API) → get a 32-byte ticket → open the WS with `?ticket=`. A logged ticket is
|
|
651
|
+
* worthless: it is consumed by the very connection that carried it (single-use) and dies within TTL_MS
|
|
652
|
+
* anyway. The legacy `?token=` path is kept for old bundles (see transport.ts) but is deprecated.
|
|
653
|
+
*/
|
|
654
|
+
/** Ticket lifetime. Long enough for the client to turn the POST response into a WS connect (even on a
|
|
655
|
+
* slow mobile link), short enough that a logged/leaked ticket is stale almost immediately. */
|
|
656
|
+
declare const WS_TICKET_TTL_MS = 30000;
|
|
657
|
+
interface WsTicketStoreOptions {
|
|
658
|
+
/** Override the TTL (tests). Default {@link WS_TICKET_TTL_MS}. */
|
|
659
|
+
ttlMs?: number;
|
|
660
|
+
/** Injectable clock for tests. Default Date.now. */
|
|
661
|
+
now?: () => number;
|
|
662
|
+
/** Injectable ticket generator for tests. Default: 32 CSPRNG bytes, base64url. */
|
|
663
|
+
generate?: () => string;
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* In-memory single-use ticket store. Deliberately NOT persisted: a ticket outliving a server restart
|
|
667
|
+
* would defeat its 30s point, and the client just re-POSTs on reconnect. Expired entries are swept
|
|
668
|
+
* LAZILY on each issue/consume — issuance is token-gated + rate-limited, so the map stays tiny without
|
|
669
|
+
* a timer.
|
|
670
|
+
*/
|
|
671
|
+
declare class WsTicketStore {
|
|
672
|
+
/** ticket → expiry (epoch ms). */
|
|
673
|
+
private readonly tickets;
|
|
674
|
+
private readonly ttlMs;
|
|
675
|
+
private readonly now;
|
|
676
|
+
private readonly generate;
|
|
677
|
+
constructor(opts?: WsTicketStoreOptions);
|
|
678
|
+
/** Mint a fresh single-use ticket. The response shape is exactly what POST /ws-ticket returns. */
|
|
679
|
+
issue(): {
|
|
680
|
+
ticket: string;
|
|
681
|
+
expiresInMs: number;
|
|
682
|
+
};
|
|
683
|
+
/** Consume a ticket: true exactly ONCE for a live ticket; false for unknown, expired, or already-used
|
|
684
|
+
* (the entry is deleted on first consume, so a replayed WS URL is rejected). */
|
|
685
|
+
consume(ticket: string): boolean;
|
|
686
|
+
/** Drop expired entries so an unconsumed flood can't grow the map (lazy — no timer to leak). */
|
|
687
|
+
private sweep;
|
|
688
|
+
/** TEST ONLY: how many un-consumed tickets are currently held (post-sweep count). */
|
|
689
|
+
size(): number;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
interface PushSubscriptionRecord {
|
|
693
|
+
/** Browser push endpoint URL (PRIMARY KEY — one per subscription). */
|
|
694
|
+
endpoint: string;
|
|
695
|
+
/** PushSubscription.keys.p256dh (client public key). */
|
|
696
|
+
p256dh: string;
|
|
697
|
+
/** PushSubscription.keys.auth (client auth secret). */
|
|
698
|
+
auth: string;
|
|
699
|
+
/** Optional session scope; undefined = subscribed to ALL sessions. */
|
|
700
|
+
sessionId?: string;
|
|
701
|
+
createdAt: number;
|
|
702
|
+
}
|
|
703
|
+
interface PushStore {
|
|
704
|
+
upsert(sub: PushSubscriptionRecord): void;
|
|
705
|
+
/** All subscriptions, or — with { sessionId } — the global (NULL) ones UNION the session-scoped ones. */
|
|
706
|
+
list(opts?: {
|
|
707
|
+
sessionId?: string;
|
|
708
|
+
}): PushSubscriptionRecord[];
|
|
709
|
+
remove(endpoint: string): void;
|
|
710
|
+
close(): void;
|
|
711
|
+
}
|
|
712
|
+
interface OpenPushStoreOptions {
|
|
713
|
+
/** SQLite file path. ":memory:" uses an in-process DB. */
|
|
714
|
+
dbPath: string;
|
|
715
|
+
}
|
|
716
|
+
declare function openPushStore(opts: OpenPushStoreOptions): PushStore;
|
|
717
|
+
|
|
718
|
+
interface PushRecipient {
|
|
719
|
+
endpoint: string;
|
|
720
|
+
p256dh: string;
|
|
721
|
+
auth: string;
|
|
722
|
+
}
|
|
723
|
+
type PushSendFn = (sub: PushRecipient, payload: string) => Promise<{
|
|
724
|
+
statusCode?: number;
|
|
725
|
+
}>;
|
|
726
|
+
interface CreateWebPushSendOptions {
|
|
727
|
+
vapid: VapidKeys;
|
|
728
|
+
/** VAPID subject: a mailto: or https: URL the push service can contact (web-push requires it). */
|
|
729
|
+
subject: string;
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Bind a real Web Push sender. Maps our flat PushSubscriptionRecord to the {endpoint, keys} shape
|
|
733
|
+
* web-push.sendNotification expects, and normalizes the outcome to { statusCode } so the dispatcher
|
|
734
|
+
* can prune on 404/410 without depending on web-push's error type.
|
|
735
|
+
*/
|
|
736
|
+
declare function createWebPushSend(opts: CreateWebPushSendOptions): PushSendFn;
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* The "away-from-desk" events that warrant a phone push. This is the whole point of roamcode: get
|
|
740
|
+
* pinged when claude needs you (awaiting), when it's done (finished), or when it hands you a file.
|
|
741
|
+
* "test" is the odd one out — a user-triggered "are notifications working?" ping (POST /push/test), which
|
|
742
|
+
* carries no session and never touches the home-screen badge.
|
|
743
|
+
*/
|
|
744
|
+
type PushEventKind = "awaiting" | "finished" | "file" | "test";
|
|
745
|
+
interface PushEvent {
|
|
746
|
+
kind: PushEventKind;
|
|
747
|
+
/** The session the event is about — becomes the deep-link (`/?session=<id>`) AND the notification tag.
|
|
748
|
+
* Absent for a "test" ping (which isn't about any session). */
|
|
749
|
+
sessionId?: string;
|
|
750
|
+
/** Optional enrichment: the file name (kind:"file") for the body. */
|
|
751
|
+
detail?: string;
|
|
752
|
+
/** Provider/session identity is bounded display metadata, never an auth or protocol payload. */
|
|
753
|
+
provider?: ProviderId;
|
|
754
|
+
label?: string;
|
|
755
|
+
/**
|
|
756
|
+
* Home-screen app-badge value = the count of sessions currently awaiting you. Stamped by the transport on
|
|
757
|
+
* every real dispatch (awaiting/finished/file) from {@link TerminalManager.awaitingCount}, so the SW
|
|
758
|
+
* can keep the badge in sync with "how many sessions need you". Omitted for a "test" ping (it must never
|
|
759
|
+
* clobber the badge). A missing value leaves the badge untouched on the client.
|
|
760
|
+
*/
|
|
761
|
+
badgeCount?: number;
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* The Web Push payload the browser service worker consumes (packages/web `push` handler). The `tag` +
|
|
765
|
+
* `renotify` pair COLLAPSES repeated pushes for the same session into one re-alerting notification (so a
|
|
766
|
+
* flapping awaiting detector can't spam a phone), and `requireInteraction` keeps a "needs-you" alert
|
|
767
|
+
* sticky while a "done"/"file" alert can auto-dismiss.
|
|
768
|
+
*/
|
|
769
|
+
interface PushPayload {
|
|
770
|
+
title: string;
|
|
771
|
+
body: string;
|
|
772
|
+
/** Deep-link the notification opens — always `/?session=<sessionId>`. */
|
|
773
|
+
url: string;
|
|
774
|
+
/** Notification tag = sessionId, so the OS collapses per-session alerts (with renotify) rather than stack. */
|
|
775
|
+
tag: string;
|
|
776
|
+
renotify: boolean;
|
|
777
|
+
/** True for a waiting-for-user alert (awaiting); false for a done/file alert. */
|
|
778
|
+
requireInteraction: boolean;
|
|
779
|
+
/**
|
|
780
|
+
* The home-screen app-badge value the SW should apply (applyBadgeFromPush): the count of sessions currently
|
|
781
|
+
* awaiting you. Present on every real away-from-desk payload; ABSENT on a "test" ping (so it never clobbers
|
|
782
|
+
* the badge). The SW treats a missing value as "leave the badge alone" and 0 as "clear it". Android/desktop
|
|
783
|
+
* honor the app badge; iOS can't badge from a push.
|
|
784
|
+
*/
|
|
785
|
+
badgeCount?: number;
|
|
786
|
+
}
|
|
787
|
+
interface PushDispatcher {
|
|
788
|
+
/**
|
|
789
|
+
* Build the payload for an away-from-desk event and fan it out over every matching subscription. NEVER
|
|
790
|
+
* throws (a push failure must never break the terminal / the calling route); dead subscriptions (404/410)
|
|
791
|
+
* are pruned from the store as a side effect.
|
|
792
|
+
*/
|
|
793
|
+
dispatch(event: PushEvent): Promise<void>;
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Map a semantic away-from-desk event to the Web Push payload the SW renders. Pure + exported so the exact
|
|
797
|
+
* contract is unit-testable (and the web agents can match it): every payload deep-links to the session and
|
|
798
|
+
* tags on the session id; only "needs-you" alerts (awaiting) set requireInteraction.
|
|
799
|
+
*/
|
|
800
|
+
declare function buildPushPayload(event: PushEvent): PushPayload;
|
|
801
|
+
interface CreatePushDispatcherDeps {
|
|
802
|
+
pushStore: PushStore;
|
|
803
|
+
/** The bound Web Push sender (createWebPushSend). Injected so tests drive a fake without real crypto/HTTP. */
|
|
804
|
+
send: PushSendFn;
|
|
805
|
+
/** Optional log sink for a non-HTTP send failure (defaults to a no-op). */
|
|
806
|
+
log?: (msg: string) => void;
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Build the push dispatcher. Fans an event out to the GLOBAL subscriptions UNION the ones scoped to the
|
|
810
|
+
* event's session (that's exactly what `pushStore.list({ sessionId })` returns), delivering to all in
|
|
811
|
+
* parallel. A send that reports 404 (Not Found) / 410 (Gone) means the browser subscription is dead — it's
|
|
812
|
+
* pruned from the store so it isn't retried forever. A non-HTTP failure (encryption, network throw) is
|
|
813
|
+
* logged and the sub is KEPT (it isn't known-dead). Never throws.
|
|
814
|
+
*/
|
|
815
|
+
declare function createPushDispatcher(deps: CreatePushDispatcherDeps): PushDispatcher;
|
|
816
|
+
|
|
817
|
+
interface ManagedPaths {
|
|
818
|
+
root: string;
|
|
819
|
+
releases: string;
|
|
820
|
+
staging: string;
|
|
821
|
+
current: string;
|
|
822
|
+
previous: string;
|
|
823
|
+
bin: string;
|
|
824
|
+
launcher: string;
|
|
825
|
+
lock: string;
|
|
826
|
+
}
|
|
827
|
+
declare function resolveInstallRoot(env?: NodeJS.ProcessEnv, home?: string): string;
|
|
828
|
+
declare function managedPaths(root: string): ManagedPaths;
|
|
829
|
+
declare function isStableVersion(value: string): boolean;
|
|
830
|
+
declare function compareVersions(a: string, b: string): number;
|
|
831
|
+
declare function renderManagedLauncher(root: string, nodePath: string): string;
|
|
832
|
+
declare function writeManagedLauncher(root: string, nodePath?: string): string;
|
|
833
|
+
declare function readActiveVersion(root: string): string | undefined;
|
|
834
|
+
declare function readPreviousVersion(root: string): string | undefined;
|
|
835
|
+
interface ManagedInstallStatus {
|
|
836
|
+
operationId: string;
|
|
837
|
+
state: "starting" | "downloading" | "installing" | "verifying" | "activating" | "restarting" | "done" | "failed";
|
|
838
|
+
phase?: string;
|
|
839
|
+
target?: string;
|
|
840
|
+
fromVersion?: string;
|
|
841
|
+
error?: string;
|
|
842
|
+
log?: string;
|
|
843
|
+
updatedAt: number;
|
|
844
|
+
}
|
|
845
|
+
interface ManagedInstallOptions {
|
|
846
|
+
version: string;
|
|
847
|
+
installRoot: string;
|
|
848
|
+
dataDir: string;
|
|
849
|
+
operationId?: string;
|
|
850
|
+
expectedIntegrity?: string;
|
|
851
|
+
expectedIntegrities?: Record<string, string>;
|
|
852
|
+
nodePath?: string;
|
|
853
|
+
npmCommand?: string;
|
|
854
|
+
restart?: boolean;
|
|
855
|
+
rollback?: boolean;
|
|
856
|
+
now?: () => number;
|
|
857
|
+
onStatus?: (status: ManagedInstallStatus) => void;
|
|
858
|
+
}
|
|
859
|
+
interface ManagedInstallResult {
|
|
860
|
+
version: string;
|
|
861
|
+
previousVersion?: string;
|
|
862
|
+
releaseDir: string;
|
|
863
|
+
launcherPath: string;
|
|
864
|
+
}
|
|
865
|
+
declare function installManagedRelease(opts: ManagedInstallOptions): Promise<ManagedInstallResult>;
|
|
866
|
+
|
|
867
|
+
declare global {
|
|
868
|
+
/** Stable package version injected by tsup. Source/test builds fall back to package.json. */
|
|
869
|
+
const __SERVER_VERSION__: string | undefined;
|
|
870
|
+
}
|
|
871
|
+
declare const RUNNING_VERSION: string;
|
|
872
|
+
/** One-release compatibility export for diagnostics/tests that used the old SHA name. */
|
|
873
|
+
declare const RUNNING_BUILD: string;
|
|
874
|
+
declare const CHECK_CACHE_MS: number;
|
|
875
|
+
declare const FETCH_TIMEOUT_MS = 20000;
|
|
876
|
+
interface UpdaterFs {
|
|
877
|
+
existsSync: (path: string) => boolean;
|
|
878
|
+
readFileSync: (path: string) => string;
|
|
879
|
+
writeFileSync: (path: string, data: string, mode?: number) => void;
|
|
880
|
+
mkdirSync: (path: string) => void;
|
|
881
|
+
chmodSync: (path: string, mode: number) => void;
|
|
882
|
+
renameSync?: (from: string, to: string) => void;
|
|
883
|
+
}
|
|
884
|
+
declare const defaultUpdaterFs: UpdaterFs;
|
|
885
|
+
interface GitHubReleaseAsset {
|
|
886
|
+
name: string;
|
|
887
|
+
browser_download_url: string;
|
|
888
|
+
}
|
|
889
|
+
interface GitHubRelease {
|
|
890
|
+
tag_name: string;
|
|
891
|
+
name?: string;
|
|
892
|
+
body?: string;
|
|
893
|
+
published_at?: string;
|
|
894
|
+
draft: boolean;
|
|
895
|
+
prerelease: boolean;
|
|
896
|
+
assets?: GitHubReleaseAsset[];
|
|
897
|
+
}
|
|
898
|
+
interface ReleaseRecord {
|
|
899
|
+
version: string;
|
|
900
|
+
tag: string;
|
|
901
|
+
name: string;
|
|
902
|
+
body: string;
|
|
903
|
+
publishedAt: string;
|
|
904
|
+
manifestUrl?: string;
|
|
905
|
+
}
|
|
906
|
+
interface ReleaseFetchResult {
|
|
907
|
+
releases?: GitHubRelease[];
|
|
908
|
+
etag?: string;
|
|
909
|
+
notModified?: boolean;
|
|
910
|
+
}
|
|
911
|
+
type FetchReleases = (etag?: string) => Promise<ReleaseFetchResult>;
|
|
912
|
+
type FetchManifest = (url: string) => Promise<unknown>;
|
|
913
|
+
interface ChangelogEntry {
|
|
914
|
+
id: string;
|
|
915
|
+
version: string;
|
|
916
|
+
subject: string;
|
|
917
|
+
group: "new" | "fixes" | "improvements" | "other";
|
|
918
|
+
when: string;
|
|
919
|
+
date: string;
|
|
920
|
+
}
|
|
921
|
+
type UpdateAction = "none" | "migrate" | "update" | "restart";
|
|
922
|
+
type InstallationKind = "managed" | "legacy-git" | "unmanaged";
|
|
923
|
+
interface VersionInfo {
|
|
924
|
+
current: string;
|
|
925
|
+
latest: string;
|
|
926
|
+
/** Compatibility alias: number of stable releases behind, never commit count. */
|
|
927
|
+
behind: number;
|
|
928
|
+
releaseCount: number;
|
|
929
|
+
updatable: boolean;
|
|
930
|
+
updateAvailable: boolean;
|
|
931
|
+
updateAction: UpdateAction;
|
|
932
|
+
installation: InstallationKind;
|
|
933
|
+
rollbackAvailable: boolean;
|
|
934
|
+
changelog: ChangelogEntry[];
|
|
935
|
+
runningVersion: string;
|
|
936
|
+
activeVersion?: string;
|
|
937
|
+
installDrift: boolean;
|
|
938
|
+
checkStatus: "fresh" | "stale" | "error";
|
|
939
|
+
checkedAt?: number;
|
|
940
|
+
error?: string;
|
|
941
|
+
/** Deprecated aliases retained until old precached clients have crossed the v1 bridge. */
|
|
942
|
+
runningBuild: string;
|
|
943
|
+
buildDrift: boolean;
|
|
944
|
+
}
|
|
945
|
+
type UpdateState = ManagedInstallStatus["state"] | "idle";
|
|
946
|
+
interface UpdateStatus {
|
|
947
|
+
operationId?: string;
|
|
948
|
+
state: UpdateState;
|
|
949
|
+
phase?: string;
|
|
950
|
+
target?: string;
|
|
951
|
+
fromVersion?: string;
|
|
952
|
+
error?: string;
|
|
953
|
+
log?: string;
|
|
954
|
+
updatedAt?: number;
|
|
955
|
+
}
|
|
956
|
+
interface UpdaterDeps {
|
|
957
|
+
fs?: UpdaterFs;
|
|
958
|
+
spawn?: typeof spawn;
|
|
959
|
+
now?: () => number;
|
|
960
|
+
dataDir: string;
|
|
961
|
+
env?: NodeJS.ProcessEnv;
|
|
962
|
+
repoRoot?: string;
|
|
963
|
+
helperPath?: string;
|
|
964
|
+
fetchReleases?: FetchReleases;
|
|
965
|
+
fetchManifest?: FetchManifest;
|
|
966
|
+
runningVersion?: string;
|
|
967
|
+
}
|
|
968
|
+
declare function normalizeRelease(raw: GitHubRelease): ReleaseRecord | undefined;
|
|
969
|
+
declare function stableReleases(raw: GitHubRelease[]): ReleaseRecord[];
|
|
970
|
+
declare function relativeWhen(iso: string, now: number): string;
|
|
971
|
+
declare function parseReleaseNotes(release: ReleaseRecord, now: number): ChangelogEntry[];
|
|
972
|
+
declare function computeInstallDrift(runningVersion: string, activeVersion: string | undefined): boolean;
|
|
973
|
+
/** Deprecated name retained as a version comparison alias. */
|
|
974
|
+
declare const computeBuildDrift: (running: string, active: string) => boolean;
|
|
975
|
+
declare function defaultFetchReleases(etag?: string): Promise<ReleaseFetchResult>;
|
|
976
|
+
declare function defaultFetchManifest(url: string): Promise<unknown>;
|
|
977
|
+
declare class Updater {
|
|
978
|
+
private readonly fs;
|
|
979
|
+
private readonly spawn;
|
|
980
|
+
private readonly now;
|
|
981
|
+
private readonly dataDir;
|
|
982
|
+
private readonly env;
|
|
983
|
+
private readonly repoRoot;
|
|
984
|
+
private readonly helperPath;
|
|
985
|
+
private readonly fetchReleases;
|
|
986
|
+
private readonly fetchManifest;
|
|
987
|
+
private readonly runningVersion;
|
|
988
|
+
private cache?;
|
|
989
|
+
private inFlight;
|
|
990
|
+
constructor(deps: UpdaterDeps);
|
|
991
|
+
private installRoot;
|
|
992
|
+
private installation;
|
|
993
|
+
private hasRestartableService;
|
|
994
|
+
private cachePath;
|
|
995
|
+
private loadPersistedReleaseCache;
|
|
996
|
+
private persistReleaseCache;
|
|
997
|
+
private refresh;
|
|
998
|
+
getVersion(force?: boolean): Promise<VersionInfo>;
|
|
999
|
+
readStatus(): UpdateStatus;
|
|
1000
|
+
private writeStatus;
|
|
1001
|
+
readLastGoodVersion(): string | undefined;
|
|
1002
|
+
/** Deprecated compatibility method: rollback is now version/pointer based. */
|
|
1003
|
+
readLastGoodSha(): string | undefined;
|
|
1004
|
+
private updateIsRunning;
|
|
1005
|
+
private finalizeRestartIfHealthy;
|
|
1006
|
+
startUpdate(opts?: {
|
|
1007
|
+
targetVersion?: string;
|
|
1008
|
+
rollback?: boolean;
|
|
1009
|
+
}): Promise<{
|
|
1010
|
+
started: boolean;
|
|
1011
|
+
reason?: string;
|
|
1012
|
+
operationId?: string;
|
|
1013
|
+
target?: string;
|
|
1014
|
+
}>;
|
|
1015
|
+
}
|
|
1016
|
+
declare function createUpdater(opts: {
|
|
1017
|
+
dataDir: string;
|
|
1018
|
+
env?: NodeJS.ProcessEnv;
|
|
1019
|
+
repoRoot?: string;
|
|
1020
|
+
runningVersion?: string;
|
|
1021
|
+
}): Updater;
|
|
1022
|
+
|
|
1023
|
+
/** Merge host and provider capability probes while replacing all provider/child detail with stable text. */
|
|
1024
|
+
declare function normalizeProviderAvailability(hostTerminalAvailable: boolean, probed: ProviderAvailability, metadataAvailable?: boolean): ProviderAvailability;
|
|
1025
|
+
/**
|
|
1026
|
+
* Best-effort, CACHED `claude --version` probe for the authed GET /diag (fleet observability). It must
|
|
1027
|
+
* NOT block the request long, so the spawn is timeout-guarded (short) and the result is cached for
|
|
1028
|
+
* CLAUDE_VERSION_CACHE_MS — a /diag poll then never re-spawns within the window. A missing/erroring
|
|
1029
|
+
* claude resolves to {available:false} (never throws), so /diag degrades instead of 500ing.
|
|
1030
|
+
*
|
|
1031
|
+
* The spawn is injectable (a function returning {stdout} or rejecting) so tests assert the caching +
|
|
1032
|
+
* the available/unavailable shaping without spawning a real binary.
|
|
1033
|
+
*/
|
|
1034
|
+
/** How long a probe result (success OR failure) is reused before re-spawning. Long enough that the
|
|
1035
|
+
* /diag poll is cheap; short enough that a claude install/upgrade is reflected within a few minutes. */
|
|
1036
|
+
declare const CLAUDE_VERSION_CACHE_MS: number;
|
|
1037
|
+
/** Short hard timeout on the spawn so a hung claude never makes /diag hang. */
|
|
1038
|
+
declare const CLAUDE_VERSION_TIMEOUT_MS = 3000;
|
|
1039
|
+
interface ClaudeAvailability {
|
|
1040
|
+
available: boolean;
|
|
1041
|
+
/** The parsed version string (e.g. "1.2.3") when available; absent otherwise. */
|
|
1042
|
+
version?: string;
|
|
1043
|
+
}
|
|
1044
|
+
/** Run `<bin> --version` (timeout-guarded), resolving its stdout. Rejects on spawn error / non-zero. */
|
|
1045
|
+
type RunClaudeVersion = () => Promise<{
|
|
1046
|
+
stdout: string;
|
|
1047
|
+
}>;
|
|
1048
|
+
interface ClaudeVersionProbe {
|
|
1049
|
+
/** Resolve the (cached) availability. Never rejects. */
|
|
1050
|
+
get(): Promise<ClaudeAvailability>;
|
|
1051
|
+
}
|
|
1052
|
+
/** The real runner: `<claudeBin> --version` with the server env + a short timeout. */
|
|
1053
|
+
declare function defaultRunClaudeVersion(claudeBin: string, env: NodeJS.ProcessEnv): RunClaudeVersion;
|
|
1054
|
+
/**
|
|
1055
|
+
* Extract a version-looking token from `claude --version` output (e.g. "1.2.3 (Claude Code)" → "1.2.3").
|
|
1056
|
+
* Returns the trimmed raw string when no dotted-number token is found (still useful), or undefined for
|
|
1057
|
+
* empty output.
|
|
1058
|
+
*/
|
|
1059
|
+
declare function parseClaudeVersion(raw: string): string | undefined;
|
|
1060
|
+
/** Build a cached probe over an injected runner + clock. */
|
|
1061
|
+
declare function createClaudeVersionProbe(deps: {
|
|
1062
|
+
run: RunClaudeVersion;
|
|
1063
|
+
now?: () => number;
|
|
1064
|
+
}): ClaudeVersionProbe;
|
|
1065
|
+
|
|
1066
|
+
/**
|
|
1067
|
+
* Claude usage limits (the `/usage` slash command), surfaced in the session rail and settings:
|
|
1068
|
+
* the 5-hour SESSION limit, the all-model WEEKLY limit, and any provider-named weekly buckets.
|
|
1069
|
+
*
|
|
1070
|
+
* DATA SOURCE: the real `claude` CLI's `/usage` command returns the exact numbers. Invoked headlessly
|
|
1071
|
+
* as `claudeBin -p "/usage" --output-format json --dangerously-skip-permissions` (clean exit, ONE JSON
|
|
1072
|
+
* line). Its `.result` string carries the human-readable usage block we parse here, e.g.:
|
|
1073
|
+
*
|
|
1074
|
+
* Current session: 12% used · resets Jun 25 at 11:30pm (Europe/Istanbul)
|
|
1075
|
+
* Current week (all models): 72% used · resets Jun 25 at 10pm (Europe/Istanbul)
|
|
1076
|
+
* Current week (Fable): 2% used · resets Jun 25 at 9:59pm (Europe/Istanbul)
|
|
1077
|
+
*
|
|
1078
|
+
* The spawn is the cost; a short TTL cache keeps it to ~once/3min regardless of how fast the rail
|
|
1079
|
+
* polls. A failed/empty fetch (claude missing / not logged in / parse fail) degrades GRACEFULLY to the
|
|
1080
|
+
* last good cached value, or null when there's none — the feature simply hides in the UI, never 500s.
|
|
1081
|
+
*
|
|
1082
|
+
* Deps are INJECTABLE (`runUsage` + `now`, mirroring the updater's style) so the parse + cache logic is
|
|
1083
|
+
* unit-testable against fixture text with no real spawn.
|
|
1084
|
+
*/
|
|
1085
|
+
/** A single usage bar: percent used (0–100) plus a reset string when the provider supplies one. */
|
|
1086
|
+
interface Bar {
|
|
1087
|
+
percent: number;
|
|
1088
|
+
/** Claude omits this while a window is still at 0%, so reset time is optional. */
|
|
1089
|
+
resets?: string;
|
|
1090
|
+
}
|
|
1091
|
+
interface ModelWeekBar extends Bar {
|
|
1092
|
+
model: string;
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Parsed usage snapshot. `session` is the 5-hour limit; `week` is the all-models weekly limit;
|
|
1096
|
+
* `weekModels` contains provider-named weekly buckets. `weekSonnet` remains for older clients.
|
|
1097
|
+
*/
|
|
1098
|
+
interface UsageInfo {
|
|
1099
|
+
session?: Bar;
|
|
1100
|
+
week?: Bar;
|
|
1101
|
+
/** Provider-named weekly buckets such as Fable or the legacy Sonnet-only limit. */
|
|
1102
|
+
weekModels?: ModelWeekBar[];
|
|
1103
|
+
/** Legacy compatibility for older clients; new code should prefer weekModels. */
|
|
1104
|
+
weekSonnet?: Bar;
|
|
1105
|
+
fetchedAt: number;
|
|
1106
|
+
}
|
|
1107
|
+
/** Resolve the raw `.result` string from `claude /usage`. NEVER rejects — resolves "" on any failure. */
|
|
1108
|
+
type RunUsage = () => Promise<string>;
|
|
1109
|
+
interface UsageServiceDeps {
|
|
1110
|
+
runUsage: RunUsage;
|
|
1111
|
+
/** Clock seam so the cache TTL is testable. */
|
|
1112
|
+
now: () => number;
|
|
1113
|
+
/** Cache TTL in ms (default USAGE_CACHE_MS). A getUsage within this window reuses the cache. */
|
|
1114
|
+
ttlMs?: number;
|
|
1115
|
+
}
|
|
1116
|
+
interface UsagePty {
|
|
1117
|
+
onData(cb: (data: string) => void): void;
|
|
1118
|
+
onExit(cb: (event: {
|
|
1119
|
+
exitCode: number;
|
|
1120
|
+
}) => void): void;
|
|
1121
|
+
kill(signal?: string): void;
|
|
1122
|
+
}
|
|
1123
|
+
type UsagePtySpawn = (file: string, args: string[], options: {
|
|
1124
|
+
name: string;
|
|
1125
|
+
cols: number;
|
|
1126
|
+
rows: number;
|
|
1127
|
+
cwd: string;
|
|
1128
|
+
env: NodeJS.ProcessEnv;
|
|
1129
|
+
}) => UsagePty;
|
|
1130
|
+
/** Cache TTL for a parsed usage snapshot. The rail polls ~every 60s; this throttles the real spawn to
|
|
1131
|
+
* roughly once every few minutes regardless of poll rate. */
|
|
1132
|
+
declare const USAGE_CACHE_MS: number;
|
|
1133
|
+
/** Hard timeout for the `claude /usage` spawn so a hung CLI never blocks the /usage response. */
|
|
1134
|
+
declare const USAGE_TIMEOUT_MS = 15000;
|
|
1135
|
+
/**
|
|
1136
|
+
* Parse the `/usage` result text into a UsageInfo. PURE: no spawn, no clock (the `now` arg stamps
|
|
1137
|
+
* `fetchedAt`). Returns null when neither the session nor the all-models week line parses (the feature
|
|
1138
|
+
* is then unavailable → the UI hides). Model-specific weeks and reset strings are optional. Reset
|
|
1139
|
+
* strings are kept as-is (trimmed); the UI may shorten them further.
|
|
1140
|
+
*/
|
|
1141
|
+
declare function parseUsage(text: string, now?: number): UsageInfo | null;
|
|
1142
|
+
/**
|
|
1143
|
+
* The real `runUsage` adapter. Claude 2.1.207+ omits account-limit lines when stdin is not a TTY, so a
|
|
1144
|
+
* normal child-process pipe returns only the diagnostic "What's contributing" section and parses as no
|
|
1145
|
+
* limits. Run the short-lived `/usage` command in its own isolated PTY instead. This is NOT a RoamCode tmux
|
|
1146
|
+
* session; it exits with the command and never attaches to a user's chat. ANTHROPIC_API_KEY is stripped so
|
|
1147
|
+
* subscription auth is used. NEVER rejects — resolves "" on spawn error, timeout, malformed JSON, or a
|
|
1148
|
+
* missing `.result`.
|
|
1149
|
+
*/
|
|
1150
|
+
declare function createUsageRunner(opts: {
|
|
1151
|
+
claudeBin: string;
|
|
1152
|
+
env?: NodeJS.ProcessEnv;
|
|
1153
|
+
timeoutMs?: number;
|
|
1154
|
+
/** Test seam; production lazily loads node-pty, already required by terminal sessions. */
|
|
1155
|
+
ptySpawn?: UsagePtySpawn;
|
|
1156
|
+
}): RunUsage;
|
|
1157
|
+
/**
|
|
1158
|
+
* Resolves + caches the parsed Claude usage. The cache stores the LAST GOOD snapshot; the TTL governs
|
|
1159
|
+
* when `runUsage` is re-run. A failed/empty fetch returns the last good value (or null), never throws.
|
|
1160
|
+
*/
|
|
1161
|
+
declare class UsageService {
|
|
1162
|
+
private readonly runUsage;
|
|
1163
|
+
private readonly now;
|
|
1164
|
+
private readonly ttlMs;
|
|
1165
|
+
private cache?;
|
|
1166
|
+
constructor(deps: UsageServiceDeps);
|
|
1167
|
+
/**
|
|
1168
|
+
* Get the current usage. Within the TTL (and not forced) returns the cached snapshot WITHOUT
|
|
1169
|
+
* re-spawning. Otherwise runs `runUsage`, parses, and (on success) caches it as the new last-good.
|
|
1170
|
+
* A failed/empty refresh keeps the last good value (or null) and re-stamps the cache time so an
|
|
1171
|
+
* outage doesn't re-spawn on every poll.
|
|
1172
|
+
*/
|
|
1173
|
+
getUsage(force?: boolean): Promise<UsageInfo | null>;
|
|
1174
|
+
}
|
|
1175
|
+
/** Construct a UsageService with the real spawn adapter (the production wiring). */
|
|
1176
|
+
declare function createUsageService(opts: {
|
|
1177
|
+
claudeBin: string;
|
|
1178
|
+
env?: NodeJS.ProcessEnv;
|
|
1179
|
+
now?: () => number;
|
|
1180
|
+
timeoutMs?: number;
|
|
1181
|
+
ttlMs?: number;
|
|
1182
|
+
}): UsageService;
|
|
1183
|
+
|
|
1184
|
+
/**
|
|
1185
|
+
* In-app re-authentication of the `claude` CLI's Claude subscription, so a user whose server-side login
|
|
1186
|
+
* expired (Anthropic returns 401 "Invalid authentication credentials" → every turn fails) can sign in
|
|
1187
|
+
* again FROM THE APP instead of SSHing to the server to run `claude auth login`.
|
|
1188
|
+
*
|
|
1189
|
+
* It wraps `claude`'s OWN manual OAuth flow (no reverse-engineering of Anthropic's OAuth):
|
|
1190
|
+
* 1. `claude auth login --claudeai` prints an authorize URL whose redirect is a HOSTED page
|
|
1191
|
+
* (platform.claude.com/oauth/code/callback — not localhost), then waits on stdin for a pasted code.
|
|
1192
|
+
* 2. We spawn it, capture that URL, and hand it to the UI; the user opens it in ANY browser (their
|
|
1193
|
+
* phone), authorizes, and copies the code the callback page shows.
|
|
1194
|
+
* 3. The UI posts the code back; we write it to the subprocess stdin; `claude` exchanges it and saves
|
|
1195
|
+
* FRESH credentials where every future claude spawn reads them — auth is restored, no restart needed.
|
|
1196
|
+
*
|
|
1197
|
+
* Single in-flight login at a time (a new start supersedes a stale one); a started login self-expires so
|
|
1198
|
+
* an abandoned flow can't hold a subprocess forever. Spawn deps are injectable for unit tests.
|
|
1199
|
+
*/
|
|
1200
|
+
interface ClaudeAuthStatus {
|
|
1201
|
+
/** Whether `claude` has stored credentials. NOTE: this reflects the LOCAL creds existing, not that they
|
|
1202
|
+
* still work — expired creds report loggedIn:true yet 401 on use, so re-auth is offered regardless. */
|
|
1203
|
+
loggedIn: boolean;
|
|
1204
|
+
email?: string;
|
|
1205
|
+
/** "max" | "pro" | … (the Claude subscription tier), when reported. */
|
|
1206
|
+
subscriptionType?: string;
|
|
1207
|
+
authMethod?: string;
|
|
1208
|
+
orgName?: string;
|
|
1209
|
+
}
|
|
1210
|
+
/** Parse `claude auth status` JSON output into a status. Returns {loggedIn:false} on any unparhseable/empty. */
|
|
1211
|
+
declare function parseAuthStatus(stdout: string): ClaudeAuthStatus;
|
|
1212
|
+
/** Pull the authorize URL out of `claude auth login` output ("…visit: https://…/oauth/authorize?…"). */
|
|
1213
|
+
declare function extractLoginUrl(output: string): string | undefined;
|
|
1214
|
+
interface ClaudeAuthDeps {
|
|
1215
|
+
/** Spawn a claude subcommand: `claude <args>` with the server env (login-session creds resolve). */
|
|
1216
|
+
spawn: (args: string[]) => ChildProcess;
|
|
1217
|
+
now: () => number;
|
|
1218
|
+
/** How long a started login waits for the URL before giving up (default 20s). */
|
|
1219
|
+
urlTimeoutMs?: number;
|
|
1220
|
+
/** How long a started login may sit awaiting its code before self-expiring (default 5min). */
|
|
1221
|
+
loginTtlMs?: number;
|
|
1222
|
+
}
|
|
1223
|
+
declare class ClaudeAuthService {
|
|
1224
|
+
private readonly deps;
|
|
1225
|
+
private pending?;
|
|
1226
|
+
constructor(deps: ClaudeAuthDeps);
|
|
1227
|
+
/** Current `claude auth status` (which account is signed in). Never rejects → {loggedIn:false} on error. */
|
|
1228
|
+
status(): Promise<ClaudeAuthStatus>;
|
|
1229
|
+
/**
|
|
1230
|
+
* Start a login: spawn `claude auth login --claudeai`, resolve with the authorize URL once it's printed.
|
|
1231
|
+
* Supersedes any prior in-flight login. Rejects if the URL never appears (timeout / immediate exit).
|
|
1232
|
+
*/
|
|
1233
|
+
startLogin(): Promise<{
|
|
1234
|
+
loginId: string;
|
|
1235
|
+
url: string;
|
|
1236
|
+
}>;
|
|
1237
|
+
/**
|
|
1238
|
+
* Submit the pasted authorization code for an in-flight login: write it to the subprocess stdin and
|
|
1239
|
+
* resolve once `claude` finishes the exchange. {ok:true} on a clean exit (fresh creds saved); {ok:false}
|
|
1240
|
+
* + message otherwise. `loginId` must match the in-flight login (else it's stale/unknown).
|
|
1241
|
+
*/
|
|
1242
|
+
submitCode(loginId: string, code: string): Promise<{
|
|
1243
|
+
ok: boolean;
|
|
1244
|
+
message?: string;
|
|
1245
|
+
}>;
|
|
1246
|
+
/** Abandon any in-flight login (e.g. the user cancels). */
|
|
1247
|
+
cancel(): void;
|
|
1248
|
+
private cancelIf;
|
|
1249
|
+
}
|
|
1250
|
+
/** Production wiring: spawn `claude <args>` with the server env (ANTHROPIC_API_KEY stripped → subscription
|
|
1251
|
+
* auth, mirroring the chat/usage spawns). stdin is piped so the code can be written for the login flow. */
|
|
1252
|
+
declare function createClaudeAuthService(opts: {
|
|
1253
|
+
claudeBin: string;
|
|
1254
|
+
env?: NodeJS.ProcessEnv;
|
|
1255
|
+
now?: () => number;
|
|
1256
|
+
}): ClaudeAuthService;
|
|
1257
|
+
|
|
1258
|
+
/**
|
|
1259
|
+
* The LATEST published `claude` CLI version, for update-awareness: the UI compares a session's
|
|
1260
|
+
* `claudeVersion` (what it spawned with) against this to show a subtle "update available" hint when a
|
|
1261
|
+
* chat is running an older Claude than what's out.
|
|
1262
|
+
*
|
|
1263
|
+
* Source: the npm registry's dist-tag for `@anthropic-ai/claude-code` (the same release train as the
|
|
1264
|
+
* native installer), fetched best-effort and TTL-cached so the rare /claude/version poll is cheap. Any
|
|
1265
|
+
* failure (offline, timeout, registry hiccup) degrades to the last good value, or undefined — the UI
|
|
1266
|
+
* simply hides the hint, never errors. The fetch is an INJECTABLE seam so the cache logic is unit-testable.
|
|
1267
|
+
*/
|
|
1268
|
+
declare const CLAUDE_LATEST_CACHE_MS: number;
|
|
1269
|
+
/** Resolve the latest version string (e.g. "2.1.195"). NEVER rejects — resolves undefined on any failure. */
|
|
1270
|
+
type FetchLatest = () => Promise<string | undefined>;
|
|
1271
|
+
interface ClaudeLatestDeps {
|
|
1272
|
+
fetchLatest: FetchLatest;
|
|
1273
|
+
now: () => number;
|
|
1274
|
+
ttlMs?: number;
|
|
1275
|
+
}
|
|
1276
|
+
/** Pull `version` out of an npm dist-tag document (`{ "version": "2.1.195", … }`). */
|
|
1277
|
+
declare function parseNpmLatest(json: unknown): string | undefined;
|
|
1278
|
+
declare class ClaudeLatestService {
|
|
1279
|
+
private readonly fetchLatest;
|
|
1280
|
+
private readonly now;
|
|
1281
|
+
private readonly ttlMs;
|
|
1282
|
+
private cache?;
|
|
1283
|
+
constructor(deps: ClaudeLatestDeps);
|
|
1284
|
+
/** The latest version, TTL-cached. A failed refresh keeps the last good value (or undefined). */
|
|
1285
|
+
getLatest(): Promise<string | undefined>;
|
|
1286
|
+
}
|
|
1287
|
+
/** The real fetch adapter: GET the npm dist-tag doc with a short timeout. Resolves undefined on any error. */
|
|
1288
|
+
declare function createClaudeLatestService(opts?: {
|
|
1289
|
+
now?: () => number;
|
|
1290
|
+
ttlMs?: number;
|
|
1291
|
+
}): ClaudeLatestService;
|
|
1292
|
+
|
|
1293
|
+
interface IPty {
|
|
1294
|
+
onData(cb: (d: string) => void): void;
|
|
1295
|
+
onExit(cb: (e: {
|
|
1296
|
+
exitCode: number;
|
|
1297
|
+
}) => void): void;
|
|
1298
|
+
write(d: string): void;
|
|
1299
|
+
resize(c: number, r: number): void;
|
|
1300
|
+
kill(sig?: string): void;
|
|
1301
|
+
}
|
|
1302
|
+
type PtySpawn = (file: string, args: string[], opts: {
|
|
1303
|
+
name: string;
|
|
1304
|
+
cols: number;
|
|
1305
|
+
rows: number;
|
|
1306
|
+
cwd: string;
|
|
1307
|
+
env: NodeJS.ProcessEnv;
|
|
1308
|
+
}) => IPty;
|
|
1309
|
+
interface TerminalProcessOptions {
|
|
1310
|
+
sessionId: string;
|
|
1311
|
+
cwd: string;
|
|
1312
|
+
executable: string;
|
|
1313
|
+
args?: string[];
|
|
1314
|
+
/** Attach to an already-proven live tmux session without supplying a provider command. If the session
|
|
1315
|
+
* disappeared, tmux fails closed instead of silently creating a fresh, identity-ambiguous conversation. */
|
|
1316
|
+
attachOnly?: boolean;
|
|
1317
|
+
/** Enable tmux mouse history for this session only. Codex uses inline mode; Claude keeps its existing
|
|
1318
|
+
* alternate-screen behavior and browser-owned selection. */
|
|
1319
|
+
enableMouseHistory?: boolean;
|
|
1320
|
+
tmuxBin?: string;
|
|
1321
|
+
cols?: number;
|
|
1322
|
+
rows?: number;
|
|
1323
|
+
env?: NodeJS.ProcessEnv;
|
|
1324
|
+
/** Injectable PTY spawner (default loads node-pty). Tests pass a fake. */
|
|
1325
|
+
ptySpawn?: PtySpawn;
|
|
1326
|
+
/** Injectable one-shot tmux command runner (kill-session). Default: async fire-and-forget spawn. */
|
|
1327
|
+
runTmux?: (args: string[]) => void;
|
|
1328
|
+
/** Read the current global tmux update-environment array. The default reader invokes tmux with an argv
|
|
1329
|
+
* array and returns variable NAMES only; injection keeps version-specific behavior deterministic in tests. */
|
|
1330
|
+
readTmuxUpdateEnvironment?: () => readonly string[] | undefined;
|
|
1331
|
+
/** Dedicated tmux server socket (`-L <socket>`). Defaults to {@link TMUX_SOCKET}. Injected by the
|
|
1332
|
+
* real-tmux integration test so it runs on a UNIQUE socket and can NEVER touch the live "roamcode"
|
|
1333
|
+
* server (a shared socket is how the full suite used to kill a running session). */
|
|
1334
|
+
tmuxSocket?: string;
|
|
1335
|
+
}
|
|
1336
|
+
/** The tmux session name for a roamcode session id. Stable so attach/kill always target the same one. */
|
|
1337
|
+
declare function tmuxSessionName(id: string): string;
|
|
1338
|
+
declare class TerminalProcess extends EventEmitter {
|
|
1339
|
+
readonly tmuxName: string;
|
|
1340
|
+
private readonly opts;
|
|
1341
|
+
private pty?;
|
|
1342
|
+
private started;
|
|
1343
|
+
private readonly tmuxBin;
|
|
1344
|
+
private readonly runTmux;
|
|
1345
|
+
private readonly ptySpawn;
|
|
1346
|
+
private readonly tmuxSocket;
|
|
1347
|
+
private readonly readTmuxUpdateEnvironment;
|
|
1348
|
+
constructor(opts: TerminalProcessOptions);
|
|
1349
|
+
start(): void;
|
|
1350
|
+
write(d: string): void;
|
|
1351
|
+
resize(c: number, r: number): void;
|
|
1352
|
+
/** Detach (kill the pty client; tmux + claude keep running). `kill:true` also kills the tmux session. */
|
|
1353
|
+
stop(opts?: {
|
|
1354
|
+
kill?: boolean;
|
|
1355
|
+
}): void;
|
|
1356
|
+
}
|
|
1357
|
+
interface TerminalProcess {
|
|
1358
|
+
on(event: "data", listener: (chunk: string) => void): this;
|
|
1359
|
+
on(event: "exit", listener: (info: {
|
|
1360
|
+
exitCode: number;
|
|
1361
|
+
}) => void): this;
|
|
1362
|
+
emit(event: "data", chunk: string): boolean;
|
|
1363
|
+
emit(event: "exit", info: {
|
|
1364
|
+
exitCode: number;
|
|
1365
|
+
}): boolean;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
type PaneStatus = "working" | "blocked" | "idle";
|
|
1369
|
+
/**
|
|
1370
|
+
* The newest Claude Code MAJOR.MINOR the classifier's markers below were VERIFIED against on a live box.
|
|
1371
|
+
* The whole classifier is tied to Claude Code's ENGLISH TUI strings — the load-bearing markers are:
|
|
1372
|
+
* - "Do you want to proceed" / "Would you like to proceed" (blocked — permission/plan prompts)
|
|
1373
|
+
* - the gerund ellipsis "…" + "↓ <n>k tokens" live counter (working — main spinner + live agents)
|
|
1374
|
+
* - a "…(<n>s"-style parenthesised spinner timer (working — pre-token-flow window)
|
|
1375
|
+
* - "Waiting for … to finish" / "esc to interrupt" (working)
|
|
1376
|
+
* A NEWER claude may reword any of these and silently degrade every rail status to "idle", so boot logs a
|
|
1377
|
+
* one-time warning (see start.ts) when the installed claude's MAJOR.MINOR exceeds this. Bump it after
|
|
1378
|
+
* re-verifying the markers against a newer claude.
|
|
1379
|
+
*/
|
|
1380
|
+
declare const CLASSIFIER_TESTED_UP_TO = "2.1";
|
|
1381
|
+
/**
|
|
1382
|
+
* True iff `current`'s MAJOR.MINOR is strictly NEWER than `testedUpTo`'s. Patch versions are ignored — a
|
|
1383
|
+
* patch release doesn't reword the TUI. Unparseable input (either side) → false, so a weird version string
|
|
1384
|
+
* can never produce a spurious warning. Pure.
|
|
1385
|
+
*/
|
|
1386
|
+
declare function isNewerMajorMinor(current: string, testedUpTo: string): boolean;
|
|
1387
|
+
/**
|
|
1388
|
+
* The one-line boot warning when the RUNNING claude is newer than the classifier was verified against —
|
|
1389
|
+
* or undefined when there is nothing to warn about (older/equal/unknown version). Pure so it's testable;
|
|
1390
|
+
* start.ts logs it once via console.warn and NEVER throws (a version bump must not affect boot).
|
|
1391
|
+
*/
|
|
1392
|
+
declare function classifierVersionWarning(claudeVersion: string | undefined): string | undefined;
|
|
1393
|
+
/**
|
|
1394
|
+
* Classify a session's live ACTIVITY from its RENDERED tmux pane (`capture-pane -p` — the CURRENT screen, not
|
|
1395
|
+
* scrollback). UNIVERSAL: works for any running session regardless of how claude was spawned (no per-session
|
|
1396
|
+
* hooks needed), and works while the browser is DETACHED (it reads the tmux session directly). Grounded in
|
|
1397
|
+
* Claude Code's real output — see pane-status.test.ts for captured samples.
|
|
1398
|
+
*
|
|
1399
|
+
* working → something is actively generating. The strongest tell is a LIVE token-flow counter "↓ 216.5k
|
|
1400
|
+
* tokens" — it appears on the MAIN spinner ("Schlepping… (1m 17s · ↓ 2.1k tokens)") AND on an
|
|
1401
|
+
* ACTIVE background agent ("⏺ general-purpose Listing f… 24m 23s · ↓ 216.5k tokens"), and is GONE
|
|
1402
|
+
* the moment a turn finishes ("Baked for 23m 15s"). So a session whose MAIN loop is at the prompt
|
|
1403
|
+
* but has agents still developing in the background reads "working" — NOT idle (the user's
|
|
1404
|
+
* explicit correction). Also: a spinner's parenthesised timer, "Waiting for … to finish", or
|
|
1405
|
+
* "esc to interrupt".
|
|
1406
|
+
* blocked → claude is WAITING ON A DECISION FROM YOU: a permission prompt ("Do you want to proceed?"), a
|
|
1407
|
+
* plan-mode approval ("Would you like to proceed?"). This is the ONLY state that earns the loud
|
|
1408
|
+
* "needs you" — so it must stay RARE + real; that's why it's just the explicit-prompt phrasings.
|
|
1409
|
+
* idle → none of the above: claude finished a turn and is sitting at an empty prompt with nothing running
|
|
1410
|
+
* and nothing to decide. A calm "your turn whenever" — NOT a loud alert.
|
|
1411
|
+
*
|
|
1412
|
+
* Order matters: blocked is checked first (a decision prompt overrides any stale spinner text), then working,
|
|
1413
|
+
* else idle.
|
|
1414
|
+
*/
|
|
1415
|
+
declare function classifyPaneStatus(pane: string): PaneStatus;
|
|
1416
|
+
/** How capturePane locates a session's tmux pane. */
|
|
1417
|
+
interface CaptureOptions {
|
|
1418
|
+
tmuxBin?: string;
|
|
1419
|
+
socket: string;
|
|
1420
|
+
sessionName: string;
|
|
1421
|
+
timeoutMs?: number;
|
|
1422
|
+
}
|
|
1423
|
+
/**
|
|
1424
|
+
* Capture a tmux session's CURRENT pane as plain text (`capture-pane -p`, no escape sequences). READ-ONLY —
|
|
1425
|
+
* it never sends input or resizes, so it can NEVER disturb a live session. Best-effort: resolves "" on any
|
|
1426
|
+
* error/timeout and never throws. Async (non-blocking) so the activity monitor doesn't stall the event loop.
|
|
1427
|
+
*/
|
|
1428
|
+
declare function capturePane(opts: CaptureOptions): Promise<string>;
|
|
1429
|
+
|
|
1430
|
+
declare class ProviderRegistry {
|
|
1431
|
+
private readonly byId;
|
|
1432
|
+
constructor(providers: readonly AgentProvider[]);
|
|
1433
|
+
get(id: ProviderId): AgentProvider;
|
|
1434
|
+
list(): AgentProvider[];
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
interface CodexSpawnLease {
|
|
1438
|
+
readonly started: Promise<void>;
|
|
1439
|
+
cancel(): Promise<void>;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
interface CodexThreadPersistence {
|
|
1443
|
+
markProvisional(id: string): void;
|
|
1444
|
+
clear(id: string): void;
|
|
1445
|
+
commit(id: string): void;
|
|
1446
|
+
}
|
|
1447
|
+
/** Creates the only persistence object accepted by the exact-thread resolver. */
|
|
1448
|
+
declare function createCodexThreadPersistence(store: SessionStore, roamSessionId: string): CodexThreadPersistence;
|
|
1449
|
+
|
|
1450
|
+
interface CodexThreadInventoryEntry {
|
|
1451
|
+
readonly id: string;
|
|
1452
|
+
readonly cwd: string;
|
|
1453
|
+
readonly source: "cli" | "vscode" | "exec" | "appServer" | "unknown" | Record<string, unknown>;
|
|
1454
|
+
/** Current protocol uses Unix seconds; milliseconds are normalized defensively for compatible revisions. */
|
|
1455
|
+
readonly createdAt: number;
|
|
1456
|
+
}
|
|
1457
|
+
type ReadCodexThreadInventory = () => Promise<readonly CodexThreadInventoryEntry[]>;
|
|
1458
|
+
interface CodexThreadRpc {
|
|
1459
|
+
request<T>(method: string, params: unknown, schema: ZodType<T>): Promise<T>;
|
|
1460
|
+
}
|
|
1461
|
+
interface CreateCodexThreadInventoryOptions {
|
|
1462
|
+
readonly cwd: string;
|
|
1463
|
+
readonly maxPages?: number;
|
|
1464
|
+
readonly maxItems?: number;
|
|
1465
|
+
}
|
|
1466
|
+
interface CodexThreadResolverOptions {
|
|
1467
|
+
readonly inventory: ReadCodexThreadInventory;
|
|
1468
|
+
readonly now?: () => number;
|
|
1469
|
+
readonly sleep?: (milliseconds: number, signal?: AbortSignal) => Promise<void>;
|
|
1470
|
+
readonly deadlineMs?: number;
|
|
1471
|
+
readonly pollIntervalMs?: number;
|
|
1472
|
+
readonly maxInventoryItems?: number;
|
|
1473
|
+
readonly creationSkewMs?: number;
|
|
1474
|
+
readonly cancellationAckMs?: number;
|
|
1475
|
+
}
|
|
1476
|
+
interface ResolveCodexThreadOptions {
|
|
1477
|
+
readonly cwd: string;
|
|
1478
|
+
/** The launch callback is intentionally inside the process-wide identity mutex. */
|
|
1479
|
+
readonly spawn: (signal: AbortSignal) => CodexSpawnLease;
|
|
1480
|
+
readonly persistence: CodexThreadPersistence;
|
|
1481
|
+
readonly signal?: AbortSignal;
|
|
1482
|
+
}
|
|
1483
|
+
declare function resetCodexThreadResolutionCoordinatorForTests(): void;
|
|
1484
|
+
/** Creates the bounded current-protocol `thread/list` reader used by resolver snapshots and polls. */
|
|
1485
|
+
declare function createCodexThreadInventory(rpc: CodexThreadRpc, options: CreateCodexThreadInventoryOptions): ReadCodexThreadInventory;
|
|
1486
|
+
declare class CodexThreadResolver {
|
|
1487
|
+
private readonly inventory;
|
|
1488
|
+
private readonly now;
|
|
1489
|
+
private readonly sleep;
|
|
1490
|
+
private readonly deadlineMs;
|
|
1491
|
+
private readonly pollIntervalMs;
|
|
1492
|
+
private readonly maxInventoryItems;
|
|
1493
|
+
private readonly creationSkewMs;
|
|
1494
|
+
private readonly cancellationAckMs;
|
|
1495
|
+
private readonly maxPolls;
|
|
1496
|
+
constructor(options: CodexThreadResolverOptions);
|
|
1497
|
+
resolveAfterSpawn(options: ResolveCodexThreadOptions): Promise<string>;
|
|
1498
|
+
private resolveLocked;
|
|
1499
|
+
private readInventory;
|
|
1500
|
+
private candidates;
|
|
1501
|
+
private sameIdentity;
|
|
1502
|
+
private throwIfCancelled;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
interface TerminalMeta {
|
|
1506
|
+
id: string;
|
|
1507
|
+
provider: ProviderId;
|
|
1508
|
+
cwd: string;
|
|
1509
|
+
mode: "terminal";
|
|
1510
|
+
status: "running" | "ended";
|
|
1511
|
+
createdAt: number;
|
|
1512
|
+
lastActivityAt: number;
|
|
1513
|
+
/**
|
|
1514
|
+
* The session's LIVE activity, derived every ~2.5s from its rendered tmux pane by the capture-pane monitor
|
|
1515
|
+
* (TerminalManager.refreshActivity → pane-status.classifyPaneStatus). UNIVERSAL (no hooks) + works while
|
|
1516
|
+
* detached. Surfaced in GET /sessions to drive the rail's per-session status:
|
|
1517
|
+
* - "working" — actively generating (main spinner OR background agents still developing);
|
|
1518
|
+
* - "blocked" — claude is WAITING ON YOUR DECISION (a permission or plan prompt) → the loud
|
|
1519
|
+
* "needs you". This is the ONLY state that alerts;
|
|
1520
|
+
* - "idle" — a finished turn sitting at an empty prompt, nothing running, nothing to decide (calm).
|
|
1521
|
+
*/
|
|
1522
|
+
activity: PaneStatus;
|
|
1523
|
+
/**
|
|
1524
|
+
* Back-compat boolean = `activity === "blocked"`. Drives the loud "needs you" badge/chip, the away push, and
|
|
1525
|
+
* the badge count. Kept as a distinct field so existing consumers (awaitingCount, the push gate) don't all
|
|
1526
|
+
* have to learn the 3-state enum. Set together with `activity` by the monitor.
|
|
1527
|
+
*/
|
|
1528
|
+
awaiting: boolean;
|
|
1529
|
+
/**
|
|
1530
|
+
* Whether this session's claude runs with `--dangerously-skip-permissions` (RCE-by-design: claude can run
|
|
1531
|
+
* any tool without asking). Derived at create() from the spawn args, persisted, and surfaced in GET /sessions
|
|
1532
|
+
* so the web rail can badge a session as running in skip-permissions mode.
|
|
1533
|
+
*/
|
|
1534
|
+
dangerouslySkip: boolean;
|
|
1535
|
+
/** The effective model: initialized from launch options, then refreshed from live provider chrome when the
|
|
1536
|
+
* TUI exposes it. Surfaced in GET /sessions so the chat header/rail follow in-session changes. */
|
|
1537
|
+
model?: string;
|
|
1538
|
+
/** Effective effort/reasoning: initialized from launch options, then refreshed from live provider chrome.
|
|
1539
|
+
* Absent means the provider controls its default. */
|
|
1540
|
+
effort?: string;
|
|
1541
|
+
/** Provider-native safety controls captured at launch for exact UI display. */
|
|
1542
|
+
permissionMode?: ClaudeSessionOptions["permissionMode"];
|
|
1543
|
+
sandbox?: CodexSessionOptions["sandbox"];
|
|
1544
|
+
approvalPolicy?: CodexSessionOptions["approvalPolicy"];
|
|
1545
|
+
/** User-set display name (PATCH /sessions/:id). SERVER-side so a rename shows on every device, not just
|
|
1546
|
+
* the one that typed it. Persisted; absent = unnamed (the UI falls back to the cwd). */
|
|
1547
|
+
name?: string;
|
|
1548
|
+
/** Codex exact-thread capture state. Claude sessions do not need a provider resume identity. */
|
|
1549
|
+
identityState?: "pending" | "exact" | "ambiguous";
|
|
1550
|
+
providerSessionId?: string;
|
|
1551
|
+
}
|
|
1552
|
+
interface TerminalSub {
|
|
1553
|
+
unsubscribe(): void;
|
|
1554
|
+
}
|
|
1555
|
+
interface TerminalManagerDeps {
|
|
1556
|
+
store: SessionStore;
|
|
1557
|
+
providers: ProviderRegistry;
|
|
1558
|
+
now: () => number;
|
|
1559
|
+
ptySpawn?: PtySpawn;
|
|
1560
|
+
runTmux?: (args: string[]) => void;
|
|
1561
|
+
/** Dedicated tmux server socket. Defaults to the unchanged production socket; integration tests inject a
|
|
1562
|
+
* unique socket so spawn, capture, resume, and cleanup cannot touch a live RoamCode instance. */
|
|
1563
|
+
tmuxSocket?: string;
|
|
1564
|
+
/**
|
|
1565
|
+
* Best-effort notifier fired on a false→true `awaiting` transition WHEN NO CLIENT IS ATTACHED (i.e. the
|
|
1566
|
+
* user is away from the desk). Wired by the transport to the push dispatcher; omitted in tests. Called in
|
|
1567
|
+
* a try/catch — a throw here can NEVER break the terminal.
|
|
1568
|
+
*/
|
|
1569
|
+
onAwaiting?: (id: string) => void;
|
|
1570
|
+
/**
|
|
1571
|
+
* Best-effort notifier fired when a session's claude exits (the "done" ping). `wasAttached` reports whether
|
|
1572
|
+
* a client was still attached at the moment of exit — captured BEFORE the subs are torn down — so the
|
|
1573
|
+
* transport can gate the away-from-desk push on "nobody was watching" (you already see the WS close when you
|
|
1574
|
+
* are). Same never-throw contract.
|
|
1575
|
+
*/
|
|
1576
|
+
onFinished?: (id: string, wasAttached: boolean) => void;
|
|
1577
|
+
/**
|
|
1578
|
+
* Capture a tmux session's CURRENT rendered pane as plain text (READ-ONLY). Injected for tests; in
|
|
1579
|
+
* production it defaults to a real `capture-pane -p` on {@link TMUX_SOCKET}. Drives {@link refreshActivity},
|
|
1580
|
+
* the universal (hook-free) working-vs-awaiting classifier.
|
|
1581
|
+
*/
|
|
1582
|
+
capturePane?: (sessionName: string) => Promise<string>;
|
|
1583
|
+
/** Builds the cwd-scoped exact-thread resolver used around a fresh Codex TUI spawn. */
|
|
1584
|
+
codexThreadResolver?: (cwd: string) => CodexThreadResolver;
|
|
1585
|
+
}
|
|
1586
|
+
type CreateTerminalOptions = {
|
|
1587
|
+
id: string;
|
|
1588
|
+
cwd: string;
|
|
1589
|
+
provider: "claude";
|
|
1590
|
+
options: ClaudeSessionOptions;
|
|
1591
|
+
cols?: number;
|
|
1592
|
+
rows?: number;
|
|
1593
|
+
} | {
|
|
1594
|
+
id: string;
|
|
1595
|
+
cwd: string;
|
|
1596
|
+
provider: "codex";
|
|
1597
|
+
options: CodexSessionOptions;
|
|
1598
|
+
cols?: number;
|
|
1599
|
+
rows?: number;
|
|
1600
|
+
};
|
|
1601
|
+
type LegacyCreateTerminalOptions = {
|
|
1602
|
+
id: string;
|
|
1603
|
+
cwd: string;
|
|
1604
|
+
claudeArgs?: string[];
|
|
1605
|
+
cols?: number;
|
|
1606
|
+
rows?: number;
|
|
1607
|
+
};
|
|
1608
|
+
declare class TerminalManager {
|
|
1609
|
+
private readonly deps;
|
|
1610
|
+
private readonly records;
|
|
1611
|
+
private readonly providers;
|
|
1612
|
+
private attachConfig?;
|
|
1613
|
+
constructor(deps: TerminalManagerDeps);
|
|
1614
|
+
/** Late-bound (after listen(), which resolves the loopback port) — same config the chat SessionManager
|
|
1615
|
+
* gets. When set, each terminal's claude is spawned with `--mcp-config` so send_image/send_file work. */
|
|
1616
|
+
setAttachConfig(attach: AttachSpawnOptions | undefined): void;
|
|
1617
|
+
create(explicit: CreateTerminalOptions): TerminalMeta;
|
|
1618
|
+
/** Temporary explicit-Claude seam for the pre-provider transport. */
|
|
1619
|
+
createLegacyClaude(opts: LegacyCreateTerminalOptions): TerminalMeta;
|
|
1620
|
+
/**
|
|
1621
|
+
* Delete stale per-session 0600 files that hold the access token — `mcp-config-<id>.json`, `hooks-<id>.json`,
|
|
1622
|
+
* `hook-auth-<id>`, and `codex-mcp-token-<id>`. A file is stale when no live session owns its id: leaked by a
|
|
1623
|
+
* crash, an orphan-reap, a rehydrated record (which carries no such paths, so stop() never unlinks its files),
|
|
1624
|
+
* or a token rotation. Call at boot AFTER rehydrate + setAttachConfig so `records` reflects the surviving
|
|
1625
|
+
* sessions. No-op without an attach config.
|
|
1626
|
+
*/
|
|
1627
|
+
sweepStaleMcpConfigs(): number;
|
|
1628
|
+
/**
|
|
1629
|
+
* Kill running terminal sessions with NO attached client that have been idle longer than `ttlMs`. OFF by
|
|
1630
|
+
* default (ttlMs <= 0) and opt-in via env (SESSION_IDLE_TTL_MS): sessions intentionally survive a
|
|
1631
|
+
* disconnect for later reattach, so reaping them must never be the default — it's only for hosts that
|
|
1632
|
+
* choose to bound the accumulation of detached claude+tmux processes. Returns the number reaped.
|
|
1633
|
+
*/
|
|
1634
|
+
reapIdle(ttlMs: number): number;
|
|
1635
|
+
/**
|
|
1636
|
+
* Push a JSON control message to every attached client of a terminal session. An `attach` (file/image)
|
|
1637
|
+
* frame is ALSO buffered (bounded, oldest-dropped) so a client that (re)connects later still sees files
|
|
1638
|
+
* that arrived while it was away — see {@link attach}'s replay. Other control frames (e.g. `ask`, which
|
|
1639
|
+
* has its own answer/replay flow in the transport) are delivered live only.
|
|
1640
|
+
*/
|
|
1641
|
+
pushControl(id: string, msg: unknown): boolean;
|
|
1642
|
+
/**
|
|
1643
|
+
* Set a session's `awaiting` flag ("claude is blocked on YOU"). The capture-pane monitor
|
|
1644
|
+
* ({@link refreshActivity}) is the authority for this flag in production; this explicit setter exists for
|
|
1645
|
+
* direct overrides and is exercised by the manager's tests. Deterministic — no timers, no terminal scraping.
|
|
1646
|
+
* A missing session is a no-op. Does NOT fire the away-from-desk push — the monitor owns that so it can gate
|
|
1647
|
+
* on {@link isAttached}.
|
|
1648
|
+
*/
|
|
1649
|
+
setAwaiting(id: string, value: boolean): void;
|
|
1650
|
+
/** Clear the awaiting flag (user is active / session ended). */
|
|
1651
|
+
private clearAwaiting;
|
|
1652
|
+
/** Whether a session currently has ≥1 attached client (a live browser WS). The hook route uses this so the
|
|
1653
|
+
* away-from-desk push fires ONLY when nobody is watching. */
|
|
1654
|
+
isAttached(id: string): boolean;
|
|
1655
|
+
/** How many sessions are currently `awaiting` you. Threaded into each away-from-desk push as `badgeCount`
|
|
1656
|
+
* so the home-screen app badge tracks "how many sessions need you" (Android/desktop badge; iOS can't). */
|
|
1657
|
+
awaitingCount(): number;
|
|
1658
|
+
/**
|
|
1659
|
+
* Set/clear a session's display name (PATCH /sessions/:id). `undefined` clears back to unnamed. Written
|
|
1660
|
+
* through to the store so the name survives restarts + rehydrate. Returns false for an unknown id so the
|
|
1661
|
+
* route can 404 without a separate lookup.
|
|
1662
|
+
*/
|
|
1663
|
+
setName(id: string, name: string | undefined): boolean;
|
|
1664
|
+
/**
|
|
1665
|
+
* Re-derive every RUNNING session's `awaiting` flag from its LIVE rendered tmux pane (`capture-pane`) — the
|
|
1666
|
+
* single, UNIVERSAL source of truth for working / blocked / idle. Works for EVERY session (no per-session
|
|
1667
|
+
* hooks needed → correct for sessions created before hooks existed) and works while the browser is DETACHED
|
|
1668
|
+
* (it reads tmux directly). Called on a ~2.5s timer from start.ts. Read-only: it can never disturb a live
|
|
1669
|
+
* session.
|
|
1670
|
+
*
|
|
1671
|
+
* {@link classifyPaneStatus} decides. A capture that returns "" (tmux hiccup) leaves the last value
|
|
1672
|
+
* untouched so the status never flaps on a transient miss.
|
|
1673
|
+
*
|
|
1674
|
+
* `awaiting` (the loud "needs you" flag) tracks activity==="blocked" ONLY — a finished-but-idle session, or
|
|
1675
|
+
* one whose background agents are still developing, is NOT "needs you". Fires
|
|
1676
|
+
* {@link TerminalManagerDeps.onAwaiting} (the away push) on a not-blocked→blocked transition when no client is
|
|
1677
|
+
* attached, so the nudge is universal + fires only for a genuine block.
|
|
1678
|
+
*/
|
|
1679
|
+
refreshActivity(): Promise<void>;
|
|
1680
|
+
/**
|
|
1681
|
+
* Nudge tmux to repaint the WHOLE screen for a session whose pty is already running — used when a client
|
|
1682
|
+
* REATTACHES (a fresh xterm) to a still-live tmux client that drew its screen earlier and won't redraw on
|
|
1683
|
+
* its own, so the new client would otherwise show only a blinking cursor until something changes. A brief
|
|
1684
|
+
* pty size wiggle (+1 row, then back) sends SIGWINCH, which makes tmux redraw — exactly what the manual
|
|
1685
|
+
* window-resize the user found does, minus the manual part. Deferred so the new client's own initial resize
|
|
1686
|
+
* lands first (rec.cols/rec.rows are then the final size we wiggle around). Best-effort + unref'd timers.
|
|
1687
|
+
*/
|
|
1688
|
+
private forceRedraw;
|
|
1689
|
+
/** Subscribe to provider output, spawning lazily through the owning provider on the first attach. */
|
|
1690
|
+
attach(id: string, handlers: {
|
|
1691
|
+
onData: (chunk: string) => void;
|
|
1692
|
+
onExit?: () => void;
|
|
1693
|
+
onControl?: (msg: string) => void;
|
|
1694
|
+
}, size?: {
|
|
1695
|
+
cols: number;
|
|
1696
|
+
rows: number;
|
|
1697
|
+
}, opts?: {
|
|
1698
|
+
respawn?: "continue" | "fresh";
|
|
1699
|
+
signal?: AbortSignal;
|
|
1700
|
+
}): Promise<TerminalSub | undefined>;
|
|
1701
|
+
private spawnForRecord;
|
|
1702
|
+
private cleanupProviderPaths;
|
|
1703
|
+
private cleanupRecordPaths;
|
|
1704
|
+
private applyRuntimeSignal;
|
|
1705
|
+
write(id: string, data: string): void;
|
|
1706
|
+
resize(id: string, cols: number, rows: number): void;
|
|
1707
|
+
stop(id: string): void;
|
|
1708
|
+
get(id: string): TerminalMeta | undefined;
|
|
1709
|
+
list(): TerminalMeta[];
|
|
1710
|
+
/**
|
|
1711
|
+
* After a server/OTA restart: adopt stored terminal sessions whose tmux session is still alive (so they
|
|
1712
|
+
* reappear, resumable), prune store rows whose tmux session is gone, AND kill ORPHAN tmux sessions — live
|
|
1713
|
+
* `rc-*` sessions with no store row (leaked by a crash or an interrupted cleanup) — so they don't pile up.
|
|
1714
|
+
*/
|
|
1715
|
+
rehydrate(opts: {
|
|
1716
|
+
liveTmuxNames: string[];
|
|
1717
|
+
}): void;
|
|
1718
|
+
/** Kill a tmux session for an id without needing a live proc (reuses TerminalProcess's socketed kill). */
|
|
1719
|
+
private killTmux;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
type DiagnosticIssue = "exit" | "malformed_json" | "notification_listener" | "spawn" | "stderr_limit" | "stdout_limit" | "transport_error" | "unknown_message" | "write_error";
|
|
1723
|
+
interface CodexMetadataDiagnostics {
|
|
1724
|
+
readonly lastIssue?: DiagnosticIssue;
|
|
1725
|
+
readonly stderrBytes: number;
|
|
1726
|
+
readonly stderrTruncated: boolean;
|
|
1727
|
+
}
|
|
1728
|
+
interface CodexAppServerNotification {
|
|
1729
|
+
readonly method: string;
|
|
1730
|
+
readonly params?: unknown;
|
|
1731
|
+
}
|
|
1732
|
+
interface WritableTransport extends EventEmitter {
|
|
1733
|
+
write(value: string | Uint8Array, callback?: (error?: Error | null) => void): boolean;
|
|
1734
|
+
end(): void;
|
|
1735
|
+
}
|
|
1736
|
+
type ReadableTransport = EventEmitter;
|
|
1737
|
+
/** The intentionally small child-process surface used by the protocol client and its fake tests. */
|
|
1738
|
+
interface CodexAppServerTransport extends EventEmitter {
|
|
1739
|
+
readonly stdin: WritableTransport;
|
|
1740
|
+
readonly stdout: ReadableTransport;
|
|
1741
|
+
readonly stderr: ReadableTransport;
|
|
1742
|
+
kill(signal?: NodeJS.Signals): boolean | void;
|
|
1743
|
+
}
|
|
1744
|
+
type SpawnCodexAppServerTransport = (command: string, args: readonly string[], options?: {
|
|
1745
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
1746
|
+
}) => CodexAppServerTransport;
|
|
1747
|
+
interface CodexAppServerClientOptions {
|
|
1748
|
+
readonly codexBin?: string;
|
|
1749
|
+
/** Selects one validated `$CODEX_HOME/<name>.config.toml` layer before app-server starts. */
|
|
1750
|
+
readonly profile?: string;
|
|
1751
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
1752
|
+
readonly spawnTransport?: SpawnCodexAppServerTransport;
|
|
1753
|
+
readonly timeoutMs?: number;
|
|
1754
|
+
readonly maxStdoutLineBytes?: number;
|
|
1755
|
+
readonly maxStdoutBufferBytes?: number;
|
|
1756
|
+
readonly maxStderrBytes?: number;
|
|
1757
|
+
}
|
|
1758
|
+
/**
|
|
1759
|
+
* Bounded JSON-lines client for the auxiliary Codex app-server metadata channel.
|
|
1760
|
+
* It never participates in terminal streaming and deliberately knows no method-specific result schemas.
|
|
1761
|
+
*/
|
|
1762
|
+
declare class CodexAppServerClient {
|
|
1763
|
+
readonly limits: {
|
|
1764
|
+
readonly timeoutMs: number;
|
|
1765
|
+
readonly maxStdoutLineBytes: number;
|
|
1766
|
+
readonly maxStdoutBufferBytes: number;
|
|
1767
|
+
readonly maxStderrBytes: number;
|
|
1768
|
+
};
|
|
1769
|
+
private readonly codexBin;
|
|
1770
|
+
private readonly profile?;
|
|
1771
|
+
private readonly env?;
|
|
1772
|
+
private readonly spawnTransport;
|
|
1773
|
+
private readonly notificationListeners;
|
|
1774
|
+
private active?;
|
|
1775
|
+
private startPromise?;
|
|
1776
|
+
private stopPromise?;
|
|
1777
|
+
private nextRequestId;
|
|
1778
|
+
private nextGeneration;
|
|
1779
|
+
private lastDiagnostics;
|
|
1780
|
+
constructor(options?: CodexAppServerClientOptions);
|
|
1781
|
+
get diagnostics(): CodexMetadataDiagnostics;
|
|
1782
|
+
start(): Promise<void>;
|
|
1783
|
+
request<T>(method: string, params: unknown, schema: ZodType<T>): Promise<T>;
|
|
1784
|
+
onNotification(listener: (notification: CodexAppServerNotification) => void): () => void;
|
|
1785
|
+
stop(): Promise<void>;
|
|
1786
|
+
private createGeneration;
|
|
1787
|
+
private installListeners;
|
|
1788
|
+
private sendRequest;
|
|
1789
|
+
private writeFrame;
|
|
1790
|
+
private flushWrites;
|
|
1791
|
+
private onStdout;
|
|
1792
|
+
private onMessage;
|
|
1793
|
+
private resolveResponse;
|
|
1794
|
+
private rejectResponse;
|
|
1795
|
+
private onStderr;
|
|
1796
|
+
private failGeneration;
|
|
1797
|
+
private removeListeners;
|
|
1798
|
+
private recordIssue;
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
interface CodexProfileClientLifecycle {
|
|
1802
|
+
readConfig<T>(profile: string, codexHome: string, cwd: string, schema: ZodType<T>): Promise<T>;
|
|
1803
|
+
}
|
|
1804
|
+
interface CreateCodexProfileClientLifecycleOptions {
|
|
1805
|
+
readonly codexBin: string;
|
|
1806
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
1807
|
+
readonly spawnTransport?: SpawnCodexAppServerTransport;
|
|
1808
|
+
readonly timeoutMs?: number;
|
|
1809
|
+
}
|
|
1810
|
+
/** Required selected-profile lifecycle. It owns Task 6 client construction, canonical env, start, and stop. */
|
|
1811
|
+
declare function createCodexProfileClientLifecycle(options: CreateCodexProfileClientLifecycleOptions): CodexProfileClientLifecycle;
|
|
1812
|
+
declare function isCodexProfileClientLifecycle(value: unknown): value is CodexProfileClientLifecycle;
|
|
1813
|
+
|
|
1814
|
+
declare const CODEX_METADATA_ERROR_CODE: "CODEX_METADATA_UNAVAILABLE";
|
|
1815
|
+
interface CodexMetadataRpc {
|
|
1816
|
+
request<T>(method: string, params: unknown, schema: ZodType<T>): Promise<T>;
|
|
1817
|
+
onNotification(listener: (notification: {
|
|
1818
|
+
method: string;
|
|
1819
|
+
params?: unknown;
|
|
1820
|
+
}) => void): () => void;
|
|
1821
|
+
start?(): Promise<void>;
|
|
1822
|
+
stop?(): Promise<void>;
|
|
1823
|
+
}
|
|
1824
|
+
interface CodexAccount {
|
|
1825
|
+
readonly authenticated: boolean;
|
|
1826
|
+
readonly authMethod: "none" | "apiKey" | "chatgpt";
|
|
1827
|
+
readonly plan?: string;
|
|
1828
|
+
}
|
|
1829
|
+
interface CodexModel {
|
|
1830
|
+
readonly value: string;
|
|
1831
|
+
readonly id: string;
|
|
1832
|
+
readonly displayName: string;
|
|
1833
|
+
readonly description: string;
|
|
1834
|
+
readonly isDefault: boolean;
|
|
1835
|
+
readonly reasoningOptions: ReadonlyArray<{
|
|
1836
|
+
value: string;
|
|
1837
|
+
description: string;
|
|
1838
|
+
isDefault: boolean;
|
|
1839
|
+
}>;
|
|
1840
|
+
readonly supportedReasoningEfforts: string[];
|
|
1841
|
+
readonly defaultReasoningEffort: string;
|
|
1842
|
+
}
|
|
1843
|
+
interface CodexUsageBar {
|
|
1844
|
+
readonly id: string;
|
|
1845
|
+
readonly label: string;
|
|
1846
|
+
readonly percent: number;
|
|
1847
|
+
readonly resetsAt?: number;
|
|
1848
|
+
readonly windowDurationMs?: number;
|
|
1849
|
+
}
|
|
1850
|
+
interface CodexUsage {
|
|
1851
|
+
readonly bars: CodexUsageBar[];
|
|
1852
|
+
readonly credits?: {
|
|
1853
|
+
readonly hasCredits: boolean;
|
|
1854
|
+
readonly unlimited: boolean;
|
|
1855
|
+
readonly balance?: string;
|
|
1856
|
+
readonly resetCreditsAvailable?: number;
|
|
1857
|
+
};
|
|
1858
|
+
readonly fetchedAt: number;
|
|
1859
|
+
}
|
|
1860
|
+
type CodexLoginCompletion = {
|
|
1861
|
+
readonly status: "completed";
|
|
1862
|
+
} | {
|
|
1863
|
+
readonly status: "failed";
|
|
1864
|
+
} | {
|
|
1865
|
+
readonly status: "canceled";
|
|
1866
|
+
} | {
|
|
1867
|
+
readonly status: "expired";
|
|
1868
|
+
};
|
|
1869
|
+
type CodexLoginStatus = CodexLoginCompletion | {
|
|
1870
|
+
readonly status: "pending";
|
|
1871
|
+
} | {
|
|
1872
|
+
readonly status: "notFound";
|
|
1873
|
+
};
|
|
1874
|
+
interface CodexDeviceLogin {
|
|
1875
|
+
readonly loginId: string;
|
|
1876
|
+
readonly userCode: string;
|
|
1877
|
+
readonly verificationUrl: string;
|
|
1878
|
+
readonly expiresAt: number;
|
|
1879
|
+
readonly completion: Promise<CodexLoginCompletion>;
|
|
1880
|
+
}
|
|
1881
|
+
interface CodexMetadataServiceOptions {
|
|
1882
|
+
readonly now?: () => number;
|
|
1883
|
+
readonly loginTtlMs?: number;
|
|
1884
|
+
readonly modelCacheTtlMs?: number;
|
|
1885
|
+
readonly capabilityCacheTtlMs?: number;
|
|
1886
|
+
readonly capabilityTimeoutMs?: number;
|
|
1887
|
+
readonly maxModelPages?: number;
|
|
1888
|
+
readonly maxModelItems?: number;
|
|
1889
|
+
readonly codexHome?: string;
|
|
1890
|
+
readonly profileClient?: CodexProfileClientLifecycle;
|
|
1891
|
+
}
|
|
1892
|
+
declare class CodexMetadataService {
|
|
1893
|
+
private readonly rpc;
|
|
1894
|
+
private readonly now;
|
|
1895
|
+
private readonly loginTtlMs;
|
|
1896
|
+
private readonly modelCacheTtlMs;
|
|
1897
|
+
private readonly capabilityCacheTtlMs;
|
|
1898
|
+
private readonly capabilityTimeoutMs;
|
|
1899
|
+
private readonly maxModelPages;
|
|
1900
|
+
private readonly maxModelItems;
|
|
1901
|
+
private readonly codexHome?;
|
|
1902
|
+
private readonly profileClient?;
|
|
1903
|
+
private readonly activeLogins;
|
|
1904
|
+
private readonly earlyLoginCompletions;
|
|
1905
|
+
private readonly retiredLoginIds;
|
|
1906
|
+
private readonly loginOutcomes;
|
|
1907
|
+
private activeStartGeneration?;
|
|
1908
|
+
private nextStartGeneration;
|
|
1909
|
+
private startingLogin?;
|
|
1910
|
+
private modelCache?;
|
|
1911
|
+
private modelRequest?;
|
|
1912
|
+
private lastUsageDiagnostic?;
|
|
1913
|
+
private lastCapabilityDiagnostic?;
|
|
1914
|
+
private capabilityCache?;
|
|
1915
|
+
private capabilityRequest?;
|
|
1916
|
+
private readonly unsubscribeNotification;
|
|
1917
|
+
private disposed;
|
|
1918
|
+
constructor(rpc: CodexMetadataRpc, options?: CodexMetadataServiceOptions);
|
|
1919
|
+
get diagnostics(): {
|
|
1920
|
+
readonly usage?: {
|
|
1921
|
+
readonly code: typeof CODEX_METADATA_ERROR_CODE;
|
|
1922
|
+
readonly message: string;
|
|
1923
|
+
};
|
|
1924
|
+
readonly capabilities?: {
|
|
1925
|
+
readonly code: typeof CODEX_METADATA_ERROR_CODE;
|
|
1926
|
+
readonly message: string;
|
|
1927
|
+
};
|
|
1928
|
+
};
|
|
1929
|
+
probeCapabilities(cwd: string, readThreadInventory: ReadCodexThreadInventory): Promise<boolean>;
|
|
1930
|
+
private loadCapabilities;
|
|
1931
|
+
getAccount(): Promise<CodexAccount>;
|
|
1932
|
+
startDeviceLogin(): Promise<CodexDeviceLogin>;
|
|
1933
|
+
cancelLogin(loginId: string): Promise<{
|
|
1934
|
+
status: "canceled" | "notFound";
|
|
1935
|
+
}>;
|
|
1936
|
+
getLoginStatus(loginId: string): CodexLoginStatus;
|
|
1937
|
+
dispose(): void;
|
|
1938
|
+
getModels(force?: boolean): Promise<CodexModel[]>;
|
|
1939
|
+
validateModelSelection(model: string, reasoningEffort?: string): Promise<void>;
|
|
1940
|
+
getUsage(): Promise<CodexUsage | null>;
|
|
1941
|
+
listProfiles(): Promise<string[]>;
|
|
1942
|
+
readonly validateProfile: (profile: string, cwd: string) => Promise<CodexProfileLaunchProof>;
|
|
1943
|
+
private readProfileConfig;
|
|
1944
|
+
private installLogin;
|
|
1945
|
+
private onNotification;
|
|
1946
|
+
private settleLogin;
|
|
1947
|
+
private finishStartGeneration;
|
|
1948
|
+
private retireLoginId;
|
|
1949
|
+
private pruneEarlyLoginCompletions;
|
|
1950
|
+
private pruneRetiredLoginIds;
|
|
1951
|
+
private recordLoginOutcome;
|
|
1952
|
+
private pruneLoginOutcomes;
|
|
1953
|
+
private expireLogin;
|
|
1954
|
+
private loadModels;
|
|
1955
|
+
private appendSnapshotBars;
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
interface ClaudeModelCatalogItem {
|
|
1959
|
+
value: string;
|
|
1960
|
+
displayName: string;
|
|
1961
|
+
description?: string;
|
|
1962
|
+
supportedEffortLevels: string[];
|
|
1963
|
+
isDefault: boolean;
|
|
1964
|
+
}
|
|
1965
|
+
interface ClaudeMetadataRunner {
|
|
1966
|
+
run(): Promise<unknown>;
|
|
1967
|
+
/** Claude 2.1.207+ no longer includes models in initialize; `/model` is the catalog fallback. */
|
|
1968
|
+
runModelList?(): Promise<unknown>;
|
|
1969
|
+
dispose?(): void | Promise<void>;
|
|
1970
|
+
}
|
|
1971
|
+
interface ClaudeMetadataChildProcess {
|
|
1972
|
+
readonly stdin: {
|
|
1973
|
+
end(chunk: string): unknown;
|
|
1974
|
+
on(event: "error", listener: (error: Error) => void): unknown;
|
|
1975
|
+
off(event: "error", listener: (error: Error) => void): unknown;
|
|
1976
|
+
};
|
|
1977
|
+
readonly stdout: NodeJS.ReadableStream;
|
|
1978
|
+
readonly stderr: NodeJS.ReadableStream;
|
|
1979
|
+
on(event: "error", listener: (error: Error) => void): this;
|
|
1980
|
+
on(event: "exit", listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
|
|
1981
|
+
off(event: "error", listener: (error: Error) => void): this;
|
|
1982
|
+
off(event: "exit", listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
|
|
1983
|
+
kill(): boolean;
|
|
1984
|
+
}
|
|
1985
|
+
type ClaudeMetadataSpawnProcess = (command: string, args: readonly string[], options: SpawnOptions) => ClaudeMetadataChildProcess;
|
|
1986
|
+
interface CreateClaudeMetadataRunnerOptions {
|
|
1987
|
+
claudeBin: string;
|
|
1988
|
+
cwd: string;
|
|
1989
|
+
env: NodeJS.ProcessEnv;
|
|
1990
|
+
timeoutMs?: number;
|
|
1991
|
+
maxOutputBytes?: number;
|
|
1992
|
+
/** Process boundary injected by lifecycle tests. */
|
|
1993
|
+
spawnProcess?: ClaudeMetadataSpawnProcess;
|
|
1994
|
+
/** `/model` process boundary injected by fallback tests. */
|
|
1995
|
+
modelListRun?: () => Promise<unknown>;
|
|
1996
|
+
}
|
|
1997
|
+
declare class ClaudeMetadataService {
|
|
1998
|
+
private readonly runner;
|
|
1999
|
+
private readonly now;
|
|
2000
|
+
private readonly ttlMs;
|
|
2001
|
+
private cache?;
|
|
2002
|
+
private inFlight?;
|
|
2003
|
+
private disposed;
|
|
2004
|
+
constructor(runner: ClaudeMetadataRunner, options?: {
|
|
2005
|
+
now?: () => number;
|
|
2006
|
+
ttlMs?: number;
|
|
2007
|
+
});
|
|
2008
|
+
getModels(force?: boolean): Promise<ClaudeModelCatalogItem[]>;
|
|
2009
|
+
validateModelSelection(model: string, effort?: string): Promise<void>;
|
|
2010
|
+
dispose(): void | Promise<void>;
|
|
2011
|
+
private loadModels;
|
|
2012
|
+
}
|
|
2013
|
+
declare function createClaudeMetadataRunner(options: CreateClaudeMetadataRunnerOptions): ClaudeMetadataRunner;
|
|
2014
|
+
|
|
2015
|
+
type CodexInstallProvenance = "npm" | "chatgpt" | "homebrew" | "unknown";
|
|
2016
|
+
interface BoundedVersionRunOptions {
|
|
2017
|
+
readonly timeoutMs: number;
|
|
2018
|
+
readonly maxOutputBytes: number;
|
|
2019
|
+
}
|
|
2020
|
+
interface BoundedLatestFetchOptions {
|
|
2021
|
+
readonly timeoutMs: number;
|
|
2022
|
+
readonly maxResponseBytes: number;
|
|
2023
|
+
}
|
|
2024
|
+
type RunCodexVersion = (args: readonly string[], options: BoundedVersionRunOptions) => Promise<{
|
|
2025
|
+
readonly code: number;
|
|
2026
|
+
readonly stdout: string;
|
|
2027
|
+
readonly stderr: string;
|
|
2028
|
+
}>;
|
|
2029
|
+
type DetectCodexProvenance = () => CodexInstallProvenance | Promise<CodexInstallProvenance>;
|
|
2030
|
+
type FetchNpmLatest = (packageName: string, options: BoundedLatestFetchOptions) => Promise<string>;
|
|
2031
|
+
interface CodexLatestServiceOptions {
|
|
2032
|
+
readonly runVersion: RunCodexVersion;
|
|
2033
|
+
readonly detectProvenance: DetectCodexProvenance;
|
|
2034
|
+
readonly fetchNpmLatest: FetchNpmLatest;
|
|
2035
|
+
readonly now?: () => number;
|
|
2036
|
+
readonly cacheTtlMs?: number;
|
|
2037
|
+
}
|
|
2038
|
+
interface CodexVersionInfo {
|
|
2039
|
+
readonly installed: string;
|
|
2040
|
+
readonly provenance: CodexInstallProvenance;
|
|
2041
|
+
readonly latest?: string;
|
|
2042
|
+
readonly updateAvailable?: boolean;
|
|
2043
|
+
readonly updateHint?: string;
|
|
2044
|
+
}
|
|
2045
|
+
declare function parseCodexVersion(stdout: string): string;
|
|
2046
|
+
declare class CodexLatestService {
|
|
2047
|
+
private readonly options;
|
|
2048
|
+
private readonly now;
|
|
2049
|
+
private readonly cacheTtlMs;
|
|
2050
|
+
private cache?;
|
|
2051
|
+
private inFlight?;
|
|
2052
|
+
constructor(options: CodexLatestServiceOptions);
|
|
2053
|
+
getVersion(force?: boolean): Promise<CodexVersionInfo>;
|
|
2054
|
+
private loadVersion;
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
interface CreateServerDeps {
|
|
2058
|
+
store?: SessionStore;
|
|
2059
|
+
/** Absolute path to the built PWA (packages/web/dist). When set, the server also serves the UI. */
|
|
2060
|
+
webDir?: string;
|
|
2061
|
+
pushStore?: PushStore;
|
|
2062
|
+
/** VAPID public key exposed at GET /push/vapid for the browser subscription. */
|
|
2063
|
+
vapidPublicKey?: string;
|
|
2064
|
+
/**
|
|
2065
|
+
* Away-from-desk Web Push dispatcher (fan-out for awaiting/finished/file events). Wired by start.ts
|
|
2066
|
+
* from the push store + VAPID keys. When omitted (tests / push not configured) the "get pinged" side of
|
|
2067
|
+
* the loop is simply a no-op — every route/heuristic still functions, it just sends no notifications.
|
|
2068
|
+
*/
|
|
2069
|
+
pushDispatcher?: PushDispatcher;
|
|
2070
|
+
/**
|
|
2071
|
+
* In-app OTA self-update (GET /version, POST /update, GET /update/status). Injected so tests can use a
|
|
2072
|
+
* fixture release feed without network access. When omitted, a real stable-release updater is built.
|
|
2073
|
+
*/
|
|
2074
|
+
updater?: Updater;
|
|
2075
|
+
/**
|
|
2076
|
+
* Claude usage limits (GET /usage → the session + weekly bars). Injected here so tests can pass a
|
|
2077
|
+
* fake (no real `claude` spawn). When omitted the route reports `usage:null` (the feature is off in
|
|
2078
|
+
* the UI). A real UsageService is wired by start.ts from the configured claude bin + the server env.
|
|
2079
|
+
*/
|
|
2080
|
+
usage?: UsageService;
|
|
2081
|
+
/**
|
|
2082
|
+
* In-app Claude re-authentication (GET /auth/status, POST /auth/login/start|code|cancel). Injected so
|
|
2083
|
+
* tests can pass a fake (no real `claude auth` spawn). When omitted the auth routes report "unavailable"
|
|
2084
|
+
* (the UI hides the sign-in). A real ClaudeAuthService is wired by start.ts from the claude bin + env.
|
|
2085
|
+
*/
|
|
2086
|
+
claudeAuth?: ClaudeAuthService;
|
|
2087
|
+
/**
|
|
2088
|
+
* The latest published claude CLI version (GET /claude/version → {installed, latest}), for update
|
|
2089
|
+
* awareness. Injected so tests don't hit the npm registry; absent → latest:null (the UI hides the hint).
|
|
2090
|
+
*/
|
|
2091
|
+
claudeLatest?: ClaudeLatestService;
|
|
2092
|
+
/**
|
|
2093
|
+
* How the session store is actually backed — "sqlite" (durable) or "memory-fallback" (better-sqlite3
|
|
2094
|
+
* failed to load; NOT durable across restarts). Surfaced by the authed GET /diag for fleet observability.
|
|
2095
|
+
* Threaded from start.ts (it opens the store). Defaults to "sqlite" when omitted.
|
|
2096
|
+
*/
|
|
2097
|
+
storeMode?: StoreMode;
|
|
2098
|
+
/**
|
|
2099
|
+
* Cached best-effort `claude --version` probe for the authed GET /diag. Injected so tests pass a fake
|
|
2100
|
+
* (no real spawn). When omitted a real probe is built from the configured claude bin + server env.
|
|
2101
|
+
*/
|
|
2102
|
+
claudeVersionProbe?: ClaudeVersionProbe;
|
|
2103
|
+
/**
|
|
2104
|
+
* Global per-client request rate limiter (token bucket). Injected so tests can drive an injectable clock
|
|
2105
|
+
* / a tiny limit. When omitted one is built from `config.rateLimitRpm`/`config.rateLimitBurst` (a
|
|
2106
|
+
* rpm of 0 disables it). Applied in the global preHandler AFTER the auth gate + origin check.
|
|
2107
|
+
*/
|
|
2108
|
+
rateLimiter?: RateLimiter;
|
|
2109
|
+
/**
|
|
2110
|
+
* CSPRNG token generator for POST /token/rotate (reuses data-dir.ts's default). Injected so tests get a
|
|
2111
|
+
* deterministic rotated token. When omitted, resolveAccessToken's default 32-byte base64url generator.
|
|
2112
|
+
*/
|
|
2113
|
+
generateToken?: () => string;
|
|
2114
|
+
/**
|
|
2115
|
+
* The token gate. Injected so tests can control the rotation grace window / clock (e.g. graceMs:0 to
|
|
2116
|
+
* assert the OLD token is rejected the instant after rotation). When omitted one is built from
|
|
2117
|
+
* `config.accessToken` with the default 60s rotation grace.
|
|
2118
|
+
*/
|
|
2119
|
+
authGate?: AuthGate;
|
|
2120
|
+
/**
|
|
2121
|
+
* Whether terminal mode (tmux + node-pty) is available on this host. Injected so tests can force it
|
|
2122
|
+
* on/off without real tmux/pty. When omitted, detectTerminalSupport() is called at boot.
|
|
2123
|
+
*/
|
|
2124
|
+
terminalAvailable?: boolean;
|
|
2125
|
+
/**
|
|
2126
|
+
* Terminal session manager (injectable for tests; a real one is constructed from deps.store +
|
|
2127
|
+
* config.claude.claudeBin when omitted).
|
|
2128
|
+
*/
|
|
2129
|
+
terminalManager?: TerminalManager;
|
|
2130
|
+
/** Exact provider registry shared with the terminal manager and provider capability routes. */
|
|
2131
|
+
providers?: ProviderRegistry;
|
|
2132
|
+
/** Auxiliary Codex app-server metadata. Its failure never disables terminal sessions. */
|
|
2133
|
+
codexMetadata?: CodexMetadataService;
|
|
2134
|
+
/** Auxiliary Claude model metadata. Its failure never disables terminal sessions. */
|
|
2135
|
+
claudeMetadata?: ClaudeMetadataService;
|
|
2136
|
+
/** Cached aggregate of every stable Codex metadata method/schema used by this server. */
|
|
2137
|
+
codexCapabilityProbe?: {
|
|
2138
|
+
get(): Promise<boolean>;
|
|
2139
|
+
};
|
|
2140
|
+
/** Installation-aware Codex version/update service. */
|
|
2141
|
+
codexLatest?: CodexLatestService;
|
|
2142
|
+
codexThreadResolver?: (cwd: string) => CodexThreadResolver;
|
|
2143
|
+
disposeProviders?: () => void | Promise<void>;
|
|
2144
|
+
/**
|
|
2145
|
+
* Single-use terminal-WS ticket store (POST /ws-ticket → `?ticket=` on the WS URL, so the long-lived
|
|
2146
|
+
* token stays OUT of WS URLs / proxy logs). Injectable so tests drive the clock/TTL; a real 30s-TTL
|
|
2147
|
+
* store is built when omitted.
|
|
2148
|
+
*/
|
|
2149
|
+
wsTickets?: WsTicketStore;
|
|
2150
|
+
}
|
|
2151
|
+
interface CreateServerResult {
|
|
2152
|
+
app: FastifyInstance;
|
|
2153
|
+
authGate: AuthGate;
|
|
2154
|
+
/** Exposed so startServer can late-bind the MCP attach config (after listen() resolves the port) —
|
|
2155
|
+
* this is what gives the terminal's claude send_image/send_file. */
|
|
2156
|
+
terminalManager: TerminalManager;
|
|
2157
|
+
/** False when tmux/node-pty is unavailable → terminal sessions are disabled (startServer warns loudly). */
|
|
2158
|
+
terminalAvailable: boolean;
|
|
2159
|
+
}
|
|
2160
|
+
declare function createServer(config: ServerRuntimeConfig, deps?: CreateServerDeps): CreateServerResult;
|
|
2161
|
+
|
|
2162
|
+
/**
|
|
2163
|
+
* Server-side mirror of the web SW's `apiNavigationDenylist` (packages/web/src/pwa/sw-exclusions.ts),
|
|
2164
|
+
* EXTENDED with /health, /push and the /ws suffix. A request whose path matches one of these is a
|
|
2165
|
+
* live API/WS/health/push route — the SPA navigation fallback must NOT serve index.html for it (it
|
|
2166
|
+
* must hit the real handler), and the auth gate must NOT treat it as a public static asset.
|
|
2167
|
+
*
|
|
2168
|
+
* SYNC INVARIANT: this denylist (and `isPublicForRequest` below) decides which requests skip the
|
|
2169
|
+
* token gate, while Fastify's find-my-way router decides which handler a request reaches. The two
|
|
2170
|
+
* MUST agree on path normalization, or a request can look public to the gate yet reach a protected
|
|
2171
|
+
* handler (an auth bypass). We gate on the DECODED path (`pathForGate`) to match the router's
|
|
2172
|
+
* percent-decoding, and we reject encoded separators (`hasEncodedSep`). If Fastify is ever
|
|
2173
|
+
* configured with `caseSensitive:false` or `ignoreDuplicateSlashes:true`, this gate must apply the
|
|
2174
|
+
* SAME case/slash normalization first, or those options will silently reopen the bypass.
|
|
2175
|
+
*/
|
|
2176
|
+
declare const API_PATH_DENYLIST: RegExp[];
|
|
2177
|
+
/**
|
|
2178
|
+
* The EXPLICIT public-shell allowlist — the only paths the auth gate lets a REGISTERED route serve
|
|
2179
|
+
* without a token. It must cover exactly what an unauthenticated PWA boot needs (the login screen has to
|
|
2180
|
+
* render before a token exists): the root shell, the Vite asset dir, and the top-level bundle files
|
|
2181
|
+
* (index.html, sw.js, manifest.webmanifest, icons — matched by extension so a new icon the web build adds
|
|
2182
|
+
* doesn't need a server change). Everything else — including any FUTURE route nobody remembered to gate —
|
|
2183
|
+
* is token-gated by default (see the transport preHandler).
|
|
2184
|
+
*
|
|
2185
|
+
* SAFE because only static handlers live at these shapes: every API route is extensionless and no API
|
|
2186
|
+
* route sits at `/` or under `/assets/`. Keep it that way — an API route named like a top-level static
|
|
2187
|
+
* file (e.g. `/report.json`) would be silently public.
|
|
2188
|
+
*/
|
|
2189
|
+
declare const SHELL_PATH_ALLOWLIST: RegExp[];
|
|
2190
|
+
/** True for the static PWA shell paths above (the auth gate's explicit allowlist). */
|
|
2191
|
+
declare function isShellPath(path: string): boolean;
|
|
2192
|
+
/**
|
|
2193
|
+
* Normalize a raw request URL to the path the FASTIFY ROUTER will route, for the auth gate.
|
|
2194
|
+
* find-my-way routes the percent-DECODED path, so we must gate on the decoded path too — otherwise
|
|
2195
|
+
* `GET /%73essions` (`%73`=`s`) looks public to a raw-path gate but routes to `/sessions`.
|
|
2196
|
+
* A malformed escape (decodeURIComponent throws) falls back to the raw path (still gated by the
|
|
2197
|
+
* caller's encoded-separator check), never crashing the gate.
|
|
2198
|
+
*/
|
|
2199
|
+
declare function pathForGate(rawUrl: string): string;
|
|
2200
|
+
/**
|
|
2201
|
+
* True if the RAW (pre-decode) path carries an encoded slash or backslash (`%2f`/`%2F`/`%5c`/`%5C`).
|
|
2202
|
+
* find-my-way's handling of encoded slashes can desync from a single `decodeURIComponent`, and any
|
|
2203
|
+
* request that needs an encoded separator to look public is inherently suspicious — so we treat such
|
|
2204
|
+
* requests as NON-public (gated) regardless of what they decode to.
|
|
2205
|
+
*/
|
|
2206
|
+
declare function hasEncodedSep(rawUrl: string): boolean;
|
|
2207
|
+
/**
|
|
2208
|
+
* The auth-boundary decision for a real request: a request is public (skips the token gate / may get
|
|
2209
|
+
* the SPA shell) IFF it has no encoded separator AND its decoded path is a public static/shell path.
|
|
2210
|
+
* Both the preHandler bypass (transport.ts) and the SPA `setNotFoundHandler` MUST use this, so the
|
|
2211
|
+
* gate and the router agree on what is reachable.
|
|
2212
|
+
*/
|
|
2213
|
+
declare function isPublicForRequest(rawUrl: string): boolean;
|
|
2214
|
+
/**
|
|
2215
|
+
* True for the PUBLIC static shell (HTML/JS/CSS/icons/manifest/sw + SPA navigations) — served WITHOUT
|
|
2216
|
+
* a token so the login screen can load and THEN authenticate. A path is public iff it is on the shell
|
|
2217
|
+
* allowlist, OR it is an extensionless NAVIGATION path (an SPA client route like `/login` gets
|
|
2218
|
+
* index.html on a hard refresh) outside the reserved API namespace. (The served bundle carries no
|
|
2219
|
+
* secret: the token lives only in the browser localStorage.)
|
|
2220
|
+
*
|
|
2221
|
+
* NOTE this predicate alone is NOT the auth boundary for routes: the transport preHandler is
|
|
2222
|
+
* DEFAULT-DENY — it only honors the navigation half of this when the request matched NO registered
|
|
2223
|
+
* route (fastify's `is404`), so a real handler can never hide behind an extensionless path. The
|
|
2224
|
+
* navigation half here also drives the SPA fallback in {@link registerStatic}'s notFound handler.
|
|
2225
|
+
*
|
|
2226
|
+
* INVARIANT: a built shell asset must NEVER live at a path starting `/sessions`, `/fs`, `/ws`,
|
|
2227
|
+
* `/health`, or `/push`. This holds for the Vite build (assets are emitted under `/assets/`, plus
|
|
2228
|
+
* root `/index.html`, `/icon-*.svg`, `/manifest.webmanifest`, `/sw.js`) — none collide with the
|
|
2229
|
+
* denylist. If a future asset were ever emitted under one of those prefixes, this default-deny would
|
|
2230
|
+
* wrongly 401 a real asset to an unauthenticated shell load (the shell would fail to boot). Keep the
|
|
2231
|
+
* Vite output prefixes clear of the denylist, or special-case the asset path here.
|
|
2232
|
+
*/
|
|
2233
|
+
declare function isPublicPath(path: string): boolean;
|
|
2234
|
+
/**
|
|
2235
|
+
* True if the path looks like a request for a static FILE — it lives under `/assets/` or its last
|
|
2236
|
+
* segment has an extension (e.g. `.js`/`.css`/`.png`/`.svg`). A file request that reaches the
|
|
2237
|
+
* notFound handler means the file does NOT exist on disk, so it must 404 — it must NOT fall back to
|
|
2238
|
+
* index.html. Serving the HTML shell for a missing `.js` makes the browser block the module script
|
|
2239
|
+
* (MIME mismatch → blank page) and can POISON a service-worker precache (HTML cached as the JS
|
|
2240
|
+
* entry). Only extensionless navigation paths (client routes like `/`, `/login`) get the SPA shell.
|
|
2241
|
+
*
|
|
2242
|
+
* Operational note: `@fastify/static` with `wildcard:false` globs the dist and registers one route
|
|
2243
|
+
* per file AT STARTUP, so after rebuilding the web bundle (new content-hashed filenames) the server
|
|
2244
|
+
* must be RESTARTED for the new assets to get routes — otherwise they reach this handler and (now)
|
|
2245
|
+
* correctly 404 instead of silently serving the shell.
|
|
2246
|
+
*/
|
|
2247
|
+
declare function looksLikeAssetRequest(path: string): boolean;
|
|
2248
|
+
interface RegisterStaticOptions {
|
|
2249
|
+
/** Absolute path to the built PWA (packages/web/dist). */
|
|
2250
|
+
webDir: string;
|
|
2251
|
+
}
|
|
2252
|
+
/**
|
|
2253
|
+
* Serve the built PWA at `/` with an SPA fallback: a GET NAVIGATION (extensionless, public, non-API)
|
|
2254
|
+
* that no static file matched returns index.html, so client routes (e.g. /login) work on a hard
|
|
2255
|
+
* refresh. A request that looks like a missing FILE (`/assets/*`, or any `*.ext`) is served LIVE from
|
|
2256
|
+
* disk when the file exists under webDir but @fastify/static's startup glob missed it (a build that
|
|
2257
|
+
* landed after this server started — an OTA window / manual rebuild); otherwise it returns 404, never
|
|
2258
|
+
* the shell — so a missing asset fails loudly instead of poisoning a browser/SW cache with HTML. An
|
|
2259
|
+
* unknown /sessions/... still 404/401s from the real handlers / the gate.
|
|
2260
|
+
*/
|
|
2261
|
+
declare function registerStatic(app: FastifyInstance, opts: RegisterStaticOptions): void;
|
|
2262
|
+
|
|
2263
|
+
declare function providerPreflightWarning(name: string, availability: ProviderAvailability): string | undefined;
|
|
2264
|
+
declare function runProviderPreflight(providers: ReadonlyArray<{
|
|
2265
|
+
name: string;
|
|
2266
|
+
probe(): Promise<ProviderAvailability>;
|
|
2267
|
+
}>, warn?: (msg: string) => void): Promise<void>;
|
|
2268
|
+
/**
|
|
2269
|
+
* STARTUP PREFLIGHT (#7): format a prominent, actionable boot warning when `claude --version` couldn't
|
|
2270
|
+
* run. The server still boots (sessions just won't start until the operator fixes it) — this is purely
|
|
2271
|
+
* to surface WHY in the logs immediately, like the better-sqlite3 fallback warning. Returns `undefined`
|
|
2272
|
+
* when claude is available (no warning). PURE so it's unit-testable without spawning.
|
|
2273
|
+
*/
|
|
2274
|
+
declare function claudePreflightWarning(availability: ClaudeAvailability): string | undefined;
|
|
2275
|
+
/**
|
|
2276
|
+
* Run the startup preflight: best-effort probe `claude --version` and print {@link claudePreflightWarning}
|
|
2277
|
+
* if it's missing/failing. NEVER throws and NEVER blocks boot — a hung/slow claude can't stall startup
|
|
2278
|
+
* (the probe is short-timeout-guarded). Injectable probe + log sink so it's testable without a spawn.
|
|
2279
|
+
*/
|
|
2280
|
+
declare function runClaudePreflight(probe: ClaudeVersionProbe, warn?: (msg: string) => void): Promise<void>;
|
|
2281
|
+
declare function startServer(env?: NodeJS.ProcessEnv): Promise<CreateServerResult & {
|
|
2282
|
+
url: string;
|
|
2283
|
+
token?: string;
|
|
2284
|
+
tokenGenerated: boolean;
|
|
2285
|
+
}>;
|
|
2286
|
+
|
|
2287
|
+
/** Terminal mode needs BOTH a tmux binary and a loadable node-pty. Injectable for tests. */
|
|
2288
|
+
declare function detectTerminalSupport(deps?: {
|
|
2289
|
+
hasTmux?: () => boolean;
|
|
2290
|
+
hasPty?: () => boolean;
|
|
2291
|
+
}): boolean;
|
|
2292
|
+
|
|
2293
|
+
/** Outcome of probing the tmux server. `ok:false` means the probe FAILED in a way we can't interpret —
|
|
2294
|
+
* the caller MUST NOT treat that as "zero live sessions" (doing so would prune every stored terminal). */
|
|
2295
|
+
interface TmuxProbe {
|
|
2296
|
+
ok: boolean;
|
|
2297
|
+
out: string;
|
|
2298
|
+
}
|
|
2299
|
+
/**
|
|
2300
|
+
* Live tmux session names, or `undefined` when the probe could not be determined (a transient tmux error).
|
|
2301
|
+
* A definitive "no sessions" is `[]`; `undefined` signals the caller to NOT prune the store.
|
|
2302
|
+
*/
|
|
2303
|
+
declare function listTmuxSessions(run?: () => TmuxProbe): string[] | undefined;
|
|
2304
|
+
|
|
2305
|
+
declare function buildServicePath(nodePath: string, home: string): string;
|
|
2306
|
+
interface RenderLaunchdOptions {
|
|
2307
|
+
label: string;
|
|
2308
|
+
nodePath?: string;
|
|
2309
|
+
cliPath?: string;
|
|
2310
|
+
executablePath?: string;
|
|
2311
|
+
dataDir: string;
|
|
2312
|
+
installRoot?: string;
|
|
2313
|
+
servicePath?: string;
|
|
2314
|
+
}
|
|
2315
|
+
declare function renderLaunchdPlist(opts: RenderLaunchdOptions): string;
|
|
2316
|
+
interface RenderSystemdOptions {
|
|
2317
|
+
nodePath?: string;
|
|
2318
|
+
cliPath?: string;
|
|
2319
|
+
executablePath?: string;
|
|
2320
|
+
dataDir: string;
|
|
2321
|
+
installRoot?: string;
|
|
2322
|
+
servicePath?: string;
|
|
2323
|
+
}
|
|
2324
|
+
declare function renderSystemdUnit(opts: RenderSystemdOptions): string;
|
|
2325
|
+
interface ServiceRecord {
|
|
2326
|
+
manager: "launchd" | "systemd";
|
|
2327
|
+
label: string;
|
|
2328
|
+
path: string;
|
|
2329
|
+
launcherPath?: string;
|
|
2330
|
+
installRoot?: string;
|
|
2331
|
+
}
|
|
2332
|
+
interface InstallServiceContext {
|
|
2333
|
+
nodePath: string;
|
|
2334
|
+
cliPath?: string;
|
|
2335
|
+
executablePath?: string;
|
|
2336
|
+
dataDir: string;
|
|
2337
|
+
installRoot?: string;
|
|
2338
|
+
home?: string;
|
|
2339
|
+
os?: NodeJS.Platform;
|
|
2340
|
+
label?: string;
|
|
2341
|
+
}
|
|
2342
|
+
interface InstallServiceResult {
|
|
2343
|
+
path: string;
|
|
2344
|
+
instructions: string;
|
|
2345
|
+
record: ServiceRecord;
|
|
2346
|
+
}
|
|
2347
|
+
declare function installService(ctx: InstallServiceContext): InstallServiceResult;
|
|
2348
|
+
declare function readServiceRecord(dataDir: string): ServiceRecord | undefined;
|
|
2349
|
+
declare function migrateServiceToLauncher(opts: {
|
|
2350
|
+
dataDir: string;
|
|
2351
|
+
installRoot: string;
|
|
2352
|
+
launcherPath: string;
|
|
2353
|
+
nodePath: string;
|
|
2354
|
+
}): ServiceRecord | undefined;
|
|
2355
|
+
declare function restartService(record: ServiceRecord): {
|
|
2356
|
+
ok: boolean;
|
|
2357
|
+
error?: string;
|
|
2358
|
+
};
|
|
2359
|
+
/** Enable the per-user service and start it immediately. This is only called by the explicit
|
|
2360
|
+
* `roamcode install` command; OTA updates use restartService and never change enablement. */
|
|
2361
|
+
declare function enableService(record: ServiceRecord): {
|
|
2362
|
+
ok: boolean;
|
|
2363
|
+
error?: string;
|
|
2364
|
+
};
|
|
2365
|
+
|
|
2366
|
+
declare class ProviderOptionsError extends ProviderError {
|
|
2367
|
+
constructor(message: string);
|
|
2368
|
+
}
|
|
2369
|
+
declare function parseProviderOptions(provider: ProviderId, raw: unknown): ProviderSessionOptions;
|
|
2370
|
+
declare function parseLegacyClaudeArgs(args: readonly string[]): ClaudeSessionOptions;
|
|
2371
|
+
|
|
2372
|
+
interface CreateClaudeProviderOptions {
|
|
2373
|
+
claudeBin: string;
|
|
2374
|
+
env?: NodeJS.ProcessEnv;
|
|
2375
|
+
attach?: AttachSpawnOptions;
|
|
2376
|
+
getAttach?: () => AttachSpawnOptions | undefined;
|
|
2377
|
+
probe?: () => Promise<ProviderAvailability>;
|
|
2378
|
+
}
|
|
2379
|
+
declare function createClaudeProvider(options: CreateClaudeProviderOptions): AgentProvider;
|
|
2380
|
+
|
|
2381
|
+
interface CreateCodexProviderOptions {
|
|
2382
|
+
codexBin: string;
|
|
2383
|
+
env?: NodeJS.ProcessEnv;
|
|
2384
|
+
attach?: AttachSpawnOptions;
|
|
2385
|
+
getAttach?: () => AttachSpawnOptions | undefined;
|
|
2386
|
+
probe?: () => Promise<ProviderAvailability>;
|
|
2387
|
+
/** Task 7 supplies the effective-provider capability check; this adapter never reads profile files. */
|
|
2388
|
+
validateProfile?(profile: string, cwd: string): CodexProfileLaunchProof | Promise<CodexProfileLaunchProof>;
|
|
2389
|
+
}
|
|
2390
|
+
type CodexAttachContext = Pick<AttachSpawnOptions, "baseUrl" | "token" | "mcpScriptPath">;
|
|
2391
|
+
declare function buildCodexArgs(context: ProviderProcessContext, attach?: CodexAttachContext): string[];
|
|
2392
|
+
declare function createCodexProvider(options: CreateCodexProviderOptions): AgentProvider;
|
|
2393
|
+
|
|
2394
|
+
declare const CODEX_OSC_MAX_CARRY: number;
|
|
2395
|
+
declare const CODEX_CLASSIFIER_TESTED_UP_TO = "0.144";
|
|
2396
|
+
interface CodexOscParser extends ProviderRuntimeSignalParser {
|
|
2397
|
+
readonly bufferedLength: number;
|
|
2398
|
+
}
|
|
2399
|
+
declare function createCodexOscParser(): CodexOscParser;
|
|
2400
|
+
declare function parseCodexOscNotifications(input: string): ProviderRuntimeSignal[];
|
|
2401
|
+
declare function classifyCodexPane(pane: string): PaneStatus;
|
|
2402
|
+
declare function codexClassifierVersionWarning(codexVersion: string | undefined): string | undefined;
|
|
2403
|
+
|
|
2404
|
+
declare const SERVER_PACKAGE = "@roamcode.ai/server";
|
|
2405
|
+
|
|
2406
|
+
export { API_PATH_DENYLIST, type AgentProvider, type AttachSpawnOptions, type AuthCheckResult, AuthGate, type AuthGateOptions, type Bar, CHECK_CACHE_MS, CLASSIFIER_TESTED_UP_TO, CLAUDE_LATEST_CACHE_MS, CLAUDE_VERSION_CACHE_MS, CLAUDE_VERSION_TIMEOUT_MS, CODEX_CLASSIFIER_TESTED_UP_TO, CODEX_OSC_MAX_CARRY, type CaptureOptions, type ChangelogEntry, type ClaudeAuthDeps, ClaudeAuthService, type ClaudeAuthStatus, type ClaudeAvailability, type ClaudeLatestDeps, ClaudeLatestService, type ClaudeMetadataRunner, ClaudeMetadataService, type ClaudeModelCatalogItem, type ClaudeSessionOptions, type ClaudeVersionProbe, type CodexAccount, CodexAppServerClient, type CodexAppServerClientOptions, type CodexDeviceLogin, type CodexInstallProvenance, CodexLatestService, type CodexLoginCompletion, type CodexLoginStatus, type CodexMetadataDiagnostics, type CodexMetadataRpc, CodexMetadataService, type CodexMetadataServiceOptions, type CodexModel, type CodexOscParser, type CodexProfileClientLifecycle, type CodexSessionOptions, type CodexSpawnLease, type CodexThreadInventoryEntry, type CodexThreadPersistence, CodexThreadResolver, type CodexThreadResolverOptions, type CodexUsage, type CodexVersionInfo, type CreateClaudeProviderOptions, type CreateCodexProfileClientLifecycleOptions, type CreateCodexProviderOptions, type CreatePushDispatcherDeps, type CreateServerDeps, type CreateServerResult, type CreateWebPushSendOptions, type DirEntry, type DirListing, type DirSearchResult, FETCH_TIMEOUT_MS, type FetchLatest, type FetchManifest, type FetchReleases, FsError, type FsErrorCode, FsService, type FsServiceOptions, type GitHubRelease, type HooksSettingsDocument, type IPty, type InstallServiceContext, type InstallServiceResult, type InstallationKind, type LaunchIntent, type ManagedInstallOptions, type ManagedInstallResult, type ManagedInstallStatus, type ManagedPaths, type McpConfigDocument, type ModelWeekBar, type OpenPushStoreOptions, type OpenSessionStoreOptions, type OriginCheckOptions, type PaneStatus, type ProcessSpec, type ProviderAvailability, ProviderError, type ProviderId, ProviderOptionsError, type ProviderProcessContext, ProviderRegistry, type ProviderRuntimeSignal, type ProviderRuntimeSignalParser, type ProviderSessionOptions, type PtySpawn, type PushDispatcher, type PushEvent, type PushEventKind, type PushPayload, type PushRecipient, type PushSendFn, type PushStore, type PushSubscriptionRecord, RUNNING_BUILD, RUNNING_VERSION, type RateLimitDecision, RateLimiter, type RateLimiterOptions, type RegisterStaticOptions, type ReleaseRecord, type RenderLaunchdOptions, type RenderSystemdOptions, type ResolveAccessTokenOptions, type ResolveCodexThreadOptions, type ResolveVapidKeysOptions, type RunClaudeVersion, type RunUsage, SEARCH_MAX_DEPTH, SEARCH_MAX_DIRS, SEARCH_MAX_RESULTS, SERVER_PACKAGE, SHELL_PATH_ALLOWLIST, type ServerConfig, type ServerRuntimeConfig, type ServiceRecord, type SessionDefaults, SessionDefaultsConflictError, type SessionStore, type StoreMode, type StoredSession, type StoredSessionDefaults, type StoredStatus, TerminalManager, type TerminalManagerDeps, type TerminalMeta, TerminalProcess, type TerminalProcessOptions, type TerminalSub, USAGE_CACHE_MS, USAGE_TIMEOUT_MS, type UpdateAction, type UpdateState, type UpdateStatus, Updater, type UpdaterDeps, type UpdaterFs, type UsageInfo, UsageService, type UsageServiceDeps, type VapidKeys, type VersionInfo, WS_TICKET_TTL_MS, WsTicketStore, type WsTicketStoreOptions, assertConfigAllowsStart, buildCodexArgs, buildHooksSettingsDocument, buildMcpConfigDocument, buildPushPayload, buildServicePath, capturePane, classifierVersionWarning, classifyCodexPane, classifyPaneStatus, claudePreflightWarning, codexClassifierVersionWarning, compareVersions, computeBuildDrift, computeInstallDrift, createClaudeAuthService, createClaudeLatestService, createClaudeMetadataRunner, createClaudeProvider, createClaudeVersionProbe, createCodexOscParser, createCodexProfileClientLifecycle, createCodexProvider, createCodexThreadInventory, createCodexThreadPersistence, createPushDispatcher, createServer, createUpdater, createUsageRunner, createUsageService, createWebPushSend, defaultFetchManifest, defaultFetchReleases, defaultRunClaudeVersion, defaultUpdaterFs, detectTerminalSupport, enableService, ensureDataDir, extractBearerToken, extractLoginUrl, generateAccessToken, hasEncodedSep, hookAuthFileContent, hookAuthPathFor, hooksSettingsPathFor, installManagedRelease, installService, isCodexProfileClientLifecycle, isLoopbackAddress, isNewerMajorMinor, isOriginAllowed, isPublicForRequest, isPublicPath, isShellPath, isStableVersion, listTmuxSessions, loadConfig, loadServerConfig, looksLikeAssetRequest, managedPaths, mcpConfigPathFor, migrateServiceToLauncher, normalizeOrigin, normalizeProviderAvailability, normalizeRelease, normalizeSessionDefaults, openPushStore, openSessionStore, parseAllowedOrigins, parseAuthStatus, parseClaudeVersion, parseCodexOscNotifications, parseCodexVersion, parseLegacyClaudeArgs, parseNpmLatest, parseProviderOptions, parseReleaseNotes, parseUsage, pathForGate, persistAccessToken, providerPreflightWarning, readActiveVersion, readPreviousVersion, readServiceRecord, registerStatic, relativeWhen, renderLaunchdPlist, renderManagedLauncher, renderSystemdUnit, resetCodexThreadResolutionCoordinatorForTests, resolveAccessToken, resolveDataDir, resolveInstallRoot, resolveVapidKeys, restartService, runClaudePreflight, runProviderPreflight, stableReleases, startServer, tmuxSessionName, writeManagedLauncher };
|