@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.
Files changed (99) hide show
  1. package/dist/index.d.ts +2406 -82
  2. package/dist/index.js +8314 -44
  3. package/dist/managed-update-helper.js +621 -26
  4. package/dist/mcp-send.js +76 -90
  5. package/dist/start.d.ts +591 -9
  6. package/dist/start.js +7629 -353
  7. package/package.json +3 -3
  8. package/dist/auth.d.ts +0 -63
  9. package/dist/auth.js +0 -133
  10. package/dist/claude-auth-service.d.ts +0 -76
  11. package/dist/claude-auth-service.js +0 -217
  12. package/dist/claude-latest-service.d.ts +0 -34
  13. package/dist/claude-latest-service.js +0 -61
  14. package/dist/config.d.ts +0 -78
  15. package/dist/config.js +0 -59
  16. package/dist/data-dir.d.ts +0 -42
  17. package/dist/data-dir.js +0 -70
  18. package/dist/diag.d.ts +0 -43
  19. package/dist/diag.js +0 -83
  20. package/dist/fs-service.d.ts +0 -90
  21. package/dist/fs-service.js +0 -290
  22. package/dist/managed-runtime.d.ts +0 -51
  23. package/dist/managed-runtime.js +0 -411
  24. package/dist/managed-update-helper.d.ts +0 -2
  25. package/dist/mcp-send.d.ts +0 -33
  26. package/dist/origin-check.d.ts +0 -37
  27. package/dist/origin-check.js +0 -101
  28. package/dist/pane-status.d.ts +0 -61
  29. package/dist/pane-status.js +0 -145
  30. package/dist/providers/claude-metadata-service.d.ts +0 -58
  31. package/dist/providers/claude-metadata-service.js +0 -352
  32. package/dist/providers/claude-provider.d.ts +0 -11
  33. package/dist/providers/claude-provider.js +0 -166
  34. package/dist/providers/codex-activity.d.ts +0 -21
  35. package/dist/providers/codex-activity.js +0 -122
  36. package/dist/providers/codex-app-server-client.d.ts +0 -90
  37. package/dist/providers/codex-app-server-client.js +0 -485
  38. package/dist/providers/codex-latest-service.d.ts +0 -50
  39. package/dist/providers/codex-latest-service.js +0 -174
  40. package/dist/providers/codex-metadata-service.d.ts +0 -161
  41. package/dist/providers/codex-metadata-service.js +0 -686
  42. package/dist/providers/codex-profile-client.d.ts +0 -16
  43. package/dist/providers/codex-profile-client.js +0 -52
  44. package/dist/providers/codex-profile-security.d.ts +0 -23
  45. package/dist/providers/codex-profile-security.js +0 -161
  46. package/dist/providers/codex-provider.d.ts +0 -15
  47. package/dist/providers/codex-provider.js +0 -174
  48. package/dist/providers/codex-thread-coordinator.d.ts +0 -18
  49. package/dist/providers/codex-thread-coordinator.js +0 -93
  50. package/dist/providers/codex-thread-persistence.d.ts +0 -9
  51. package/dist/providers/codex-thread-persistence.js +0 -45
  52. package/dist/providers/codex-thread-resolver.d.ts +0 -59
  53. package/dist/providers/codex-thread-resolver.js +0 -322
  54. package/dist/providers/options.d.ts +0 -7
  55. package/dist/providers/options.js +0 -155
  56. package/dist/providers/provider-artifacts.d.ts +0 -3
  57. package/dist/providers/provider-artifacts.js +0 -30
  58. package/dist/providers/registry.d.ts +0 -7
  59. package/dist/providers/registry.js +0 -23
  60. package/dist/providers/types.d.ts +0 -95
  61. package/dist/providers/types.js +0 -8
  62. package/dist/push-dispatch.d.ts +0 -81
  63. package/dist/push-dispatch.js +0 -100
  64. package/dist/push-store.d.ts +0 -25
  65. package/dist/push-store.js +0 -79
  66. package/dist/rate-limit.d.ts +0 -52
  67. package/dist/rate-limit.js +0 -72
  68. package/dist/server-config.d.ts +0 -60
  69. package/dist/server-config.js +0 -77
  70. package/dist/service-install.d.ts +0 -60
  71. package/dist/service-install.js +0 -221
  72. package/dist/session-defaults.d.ts +0 -26
  73. package/dist/session-defaults.js +0 -60
  74. package/dist/session-store.d.ts +0 -81
  75. package/dist/session-store.js +0 -654
  76. package/dist/static-routes.d.ts +0 -101
  77. package/dist/static-routes.js +0 -188
  78. package/dist/terminal-capability.d.ts +0 -5
  79. package/dist/terminal-capability.js +0 -27
  80. package/dist/terminal-manager.d.ts +0 -224
  81. package/dist/terminal-manager.js +0 -917
  82. package/dist/terminal-process.d.ts +0 -85
  83. package/dist/terminal-process.js +0 -238
  84. package/dist/terminal-shared.d.ts +0 -36
  85. package/dist/terminal-shared.js +0 -43
  86. package/dist/tmux-list.d.ts +0 -11
  87. package/dist/tmux-list.js +0 -39
  88. package/dist/transport.d.ts +0 -123
  89. package/dist/transport.js +0 -1559
  90. package/dist/updater.d.ts +0 -161
  91. package/dist/updater.js +0 -451
  92. package/dist/usage-service.d.ts +0 -118
  93. package/dist/usage-service.js +0 -173
  94. package/dist/vapid.d.ts +0 -17
  95. package/dist/vapid.js +0 -31
  96. package/dist/web-push-send.d.ts +0 -20
  97. package/dist/web-push-send.js +0 -21
  98. package/dist/ws-ticket.d.ts +0 -47
  99. package/dist/ws-ticket.js +0 -62
package/dist/start.d.ts CHANGED
@@ -1,8 +1,588 @@
1
- import type { ClaudeAvailability, ClaudeVersionProbe } from "./diag.js";
2
- import type { CreateServerResult } from "./transport.js";
3
- import type { ProviderAvailability } from "./providers/types.js";
4
- export declare function providerPreflightWarning(name: string, availability: ProviderAvailability): string | undefined;
5
- export declare function runProviderPreflight(providers: ReadonlyArray<{
1
+ import { FastifyInstance } from 'fastify';
2
+
3
+ /**
4
+ * Wiring for the mcp-send server (Claude user attachments). When present, the spawn layer writes a
5
+ * per-session 0600 MCP config FILE (carrying the loopback base URL, this session's id, and the access
6
+ * token via env) and passes its PATH to the terminal spawn as `--mcp-config <path>`. The token therefore
7
+ * NEVER lands in any process's argv (where `ps`/`/proc` would expose it to other local users) — it
8
+ * lives only in the mode-0600 file. ABSENT → spawn exactly as before (the feature is additive).
9
+ */
10
+ interface AttachSpawnOptions {
11
+ /** Loopback base URL of roamcode (e.g. http://127.0.0.1:4280) the tool POSTs back to. */
12
+ baseUrl: string;
13
+ /** The access token the mcp-send tool sends as `Authorization: Bearer <token>`. */
14
+ token: string;
15
+ /** Absolute path to the built dist/mcp-send.js node script. */
16
+ mcpScriptPath: string;
17
+ /** Host data dir (mode 0700) the per-session 0600 mcp-config-<id>.json is written into. */
18
+ dataDir: string;
19
+ }
20
+
21
+ type ProviderId = "claude" | "codex";
22
+ interface ProviderAvailability {
23
+ terminalAvailable: boolean;
24
+ metadataAvailable: boolean;
25
+ version?: string;
26
+ detail?: string;
27
+ }
28
+ type ClaudeSessionOptions = {
29
+ provider: "claude";
30
+ model?: string;
31
+ effort?: string;
32
+ permissionMode?: "default" | "acceptEdits" | "plan" | "bypassPermissions";
33
+ dangerouslySkip?: boolean;
34
+ addDirs?: string[];
35
+ legacyArgs?: string[];
36
+ };
37
+ type CodexSessionOptions = {
38
+ provider: "codex";
39
+ model?: string;
40
+ reasoningEffort?: string;
41
+ sandbox?: "read-only" | "workspace-write" | "danger-full-access";
42
+ approvalPolicy?: "untrusted" | "on-request" | "never";
43
+ profile?: string;
44
+ webSearch?: boolean;
45
+ dangerouslyBypassApprovalsAndSandbox?: boolean;
46
+ addDirs?: string[];
47
+ };
48
+ type ProviderSessionOptions = ClaudeSessionOptions | CodexSessionOptions;
49
+ type LaunchIntent = "fresh" | "resume";
50
+ interface ProcessSpec {
51
+ executable: string;
52
+ args: string[];
53
+ env: NodeJS.ProcessEnv;
54
+ cleanupPaths: string[];
55
+ /** Revalidates ephemeral launch proof immediately before the terminal process is spawned. */
56
+ preSpawnCheck?: () => void | Promise<void>;
57
+ integration?: {
58
+ attachments: "ready" | "degraded";
59
+ activity: "ready" | "degraded";
60
+ detail?: string;
61
+ };
62
+ }
63
+ interface ProviderProcessContext {
64
+ roamSessionId: string;
65
+ cwd: string;
66
+ intent: LaunchIntent;
67
+ options: ProviderSessionOptions;
68
+ providerSessionId?: string;
69
+ /** Late-bound after listen so both providers share the real loopback MCP callback configuration. */
70
+ attach?: AttachSpawnOptions;
71
+ /** Transfer temporary-artifact ownership as soon as paths exist, so a later build rejection is cleanable. */
72
+ registerCleanupPaths?(paths: readonly string[]): void;
73
+ }
74
+ type ProviderRuntimeSignal = {
75
+ type: "working";
76
+ } | {
77
+ type: "blocked";
78
+ } | {
79
+ type: "idle";
80
+ } | {
81
+ type: "provider-session-id";
82
+ id: string;
83
+ };
84
+ interface ProviderRuntimeSignalParser {
85
+ push(chunk: string): ProviderRuntimeSignal[];
86
+ }
87
+ interface ProviderRuntimeMetadata {
88
+ model?: string;
89
+ effort?: string;
90
+ }
91
+ interface AgentProvider {
92
+ readonly id: ProviderId;
93
+ readonly displayName: string;
94
+ readonly resumeIdentity: "optional" | "required";
95
+ probe(): Promise<ProviderAvailability>;
96
+ buildProcess(context: ProviderProcessContext): Promise<ProcessSpec>;
97
+ /** Stateful parsers must be created per spawned process; providers are registry singletons. */
98
+ createRuntimeSignalParser?(): ProviderRuntimeSignalParser;
99
+ runtimeSignals(chunk: string): ProviderRuntimeSignal[];
100
+ classifyPane(pane: string): "working" | "blocked" | "idle";
101
+ /** Read provider-owned live model/effort chrome from a captured pane. Optional because not every TUI
102
+ * exposes these values. A missing/invalid result must leave launch metadata untouched. */
103
+ runtimeMetadata?(pane: string): ProviderRuntimeMetadata | undefined;
104
+ cleanup(paths: readonly string[]): void;
105
+ }
106
+
107
+ interface ClaudeAvailability {
108
+ available: boolean;
109
+ /** The parsed version string (e.g. "1.2.3") when available; absent otherwise. */
110
+ version?: string;
111
+ }
112
+ interface ClaudeVersionProbe {
113
+ /** Resolve the (cached) availability. Never rejects. */
114
+ get(): Promise<ClaudeAvailability>;
115
+ }
116
+
117
+ interface AuthGateOptions {
118
+ token?: string;
119
+ /** Consecutive failures from one client before it is locked out. Default 10. */
120
+ maxFailures?: number;
121
+ /** Lockout duration in ms. Default 60000. */
122
+ lockoutMs?: number;
123
+ /**
124
+ * Grace window (ms) after {@link AuthGate.rotateToken} during which the PREVIOUS token is still
125
+ * accepted, so a callback already in flight with the old token (e.g. a running mcp-send subprocess that
126
+ * captured RC_TOKEN at spawn) survives the rotation instead of 401-ing mid-conversation. Default 60000.
127
+ */
128
+ graceMs?: number;
129
+ /** Injectable clock for tests. Default Date.now. */
130
+ now?: () => number;
131
+ }
132
+ type AuthCheckResult = {
133
+ ok: true;
134
+ } | {
135
+ ok: false;
136
+ reason: "locked" | "invalid" | "missing-token-config";
137
+ };
138
+ /**
139
+ * Verifies a bearer access token in constant time and applies a per-client
140
+ * lockout after repeated failures. Pure / I/O-free so it is unit-testable;
141
+ * Task 8 wires it as a single global Fastify `preHandler` that also gates the
142
+ * WebSocket upgrade.
143
+ *
144
+ * Proxy lockout caveat: the lockout is keyed by `clientKey` (`request.ip`).
145
+ * Behind a reverse proxy (Caddy / Cloudflare) `request.ip` is the proxy's IP
146
+ * for EVERY client, collapsing the per-client lockout to one shared key. To
147
+ * stop that from self-DoS-ing the sole legitimate user, {@link check} validates
148
+ * the token FIRST: a CORRECT token is ALWAYS accepted, even while its key is
149
+ * locked out — the lockout only ever throttles WRONG guesses. Supplying a real
150
+ * per-client key (Fastify `trustProxy` scoped to the proxy) still makes the
151
+ * throttle per-client, but is no longer required to keep the user from being
152
+ * locked out by someone else's bad traffic.
153
+ */
154
+ declare class AuthGate {
155
+ private token?;
156
+ private previousToken?;
157
+ private previousValidUntil;
158
+ private readonly maxFailures;
159
+ private readonly lockoutMs;
160
+ private readonly graceMs;
161
+ private readonly now;
162
+ private readonly clients;
163
+ constructor(opts: AuthGateOptions);
164
+ check(presentedToken: string | undefined, clientKey: string): AuthCheckResult;
165
+ /**
166
+ * Atomically swap in a fresh access token (POST /token/rotate). Every subsequent {@link check} compares
167
+ * against the new token; the OLD token is accepted only for a brief grace window (graceMs) so in-flight
168
+ * callbacks holding it (a running mcp-send subprocess) survive the rotation instead of 401-ing mid-turn.
169
+ * A 2nd rotation within the window REPLACES previousToken with the most-recent old token (no list grows).
170
+ * Also clears all per-client lockout state — rotation is an explicit administrative reset, so a fresh
171
+ * slate is correct (and avoids leaving a client locked out against a token that no longer exists).
172
+ */
173
+ rotateToken(newToken: string): void;
174
+ /** Drop entries whose lockout has expired and whose failure count is 0 — keeps the map bounded. */
175
+ private sweepExpired;
176
+ /** TEST ONLY: number of tracked clients currently locked (lockedUntil in the future). */
177
+ lockedClientCount(): number;
178
+ }
179
+
180
+ interface SessionDefaults {
181
+ effort: string;
182
+ model?: string;
183
+ dangerouslySkip: boolean;
184
+ permissionMode?: string;
185
+ codex?: {
186
+ model?: string;
187
+ reasoningEffort?: string;
188
+ sandbox?: "read-only" | "workspace-write" | "danger-full-access";
189
+ approvalPolicy?: "untrusted" | "on-request" | "never";
190
+ profile?: string;
191
+ webSearch?: boolean;
192
+ addDirs?: string[];
193
+ dangerouslyBypassApprovalsAndSandbox?: boolean;
194
+ };
195
+ }
196
+ interface StoredSessionDefaults {
197
+ defaults: SessionDefaults;
198
+ revision: number;
199
+ updatedAt: number;
200
+ }
201
+
202
+ type StoredStatus = "running" | "dormant" | "errored" | "stopped";
203
+ interface StoredSessionBase {
204
+ id: string;
205
+ cwd: string;
206
+ status: StoredStatus;
207
+ createdAt: number;
208
+ lastActivityAt: number;
209
+ /** Always "terminal" — the only session kind. Kept so rehydrate can filter/guard on it. */
210
+ mode: "terminal";
211
+ /** Optional user-set display name (PATCH /sessions/:id). Absent = unnamed (the UI shows the cwd). */
212
+ name?: string;
213
+ }
214
+ interface StoredClaudeSession extends StoredSessionBase {
215
+ provider: "claude";
216
+ dangerouslySkip: boolean;
217
+ /** The USER-chosen spawn flags the session was created with (`--model`/`--effort`/`--permission-mode`/
218
+ * `--dangerously-skip-permissions`/`--add-dir …`) — everything EXCEPT the ephemeral per-session
219
+ * `--mcp-config`/`--settings` paths (those are regenerated per spawn). Persisted so a RESTART can
220
+ * respawn the ended session with the same settings instead of a bare claude. Absent = no extra flags. */
221
+ spawnArgs?: string[];
222
+ providerSessionId?: never;
223
+ launchOptions?: never;
224
+ integrationStatus?: never;
225
+ }
226
+ interface StoredIntegrationStatus {
227
+ attachments: "ready" | "degraded";
228
+ activity: "ready" | "degraded";
229
+ detail?: string;
230
+ }
231
+ interface StoredCodexSession extends StoredSessionBase {
232
+ provider: "codex";
233
+ launchOptions: CodexSessionOptions;
234
+ providerSessionId?: string;
235
+ integrationStatus?: StoredIntegrationStatus;
236
+ dangerouslySkip?: never;
237
+ spawnArgs?: never;
238
+ }
239
+ type StoredSession = StoredClaudeSession | StoredCodexSession;
240
+ /** How a store is actually backed: "sqlite" (durable) or "memory-fallback" (the native module failed to
241
+ * load — NOT durable across restarts). Surfaced so start.ts can warn loudly + /diag can report it. */
242
+ type StoreMode = "sqlite" | "memory-fallback";
243
+ interface SessionStore {
244
+ /** Atomically claim a brand-new physical id. Rejects any existing owner without changing its data. */
245
+ claimNew(session: StoredSession): void;
246
+ upsert(session: StoredSession): void;
247
+ /** True when any physical provider table owns the id, including corrupt or cross-table ambiguous rows. */
248
+ has(id: string): boolean;
249
+ get(id: string): StoredSession | undefined;
250
+ list(): StoredSession[];
251
+ setStatus(id: string, status: StoredStatus): void;
252
+ touch(id: string, at: number): void;
253
+ /** Set/clear a session's display name (undefined clears — the row's `name` goes back to NULL). */
254
+ setName(id: string, name: string | undefined): void;
255
+ /** Persist/clear the exact resumable identity for Codex sessions only. Claude rows are never touched. */
256
+ setProviderSessionId(id: string, value: string | undefined): void;
257
+ /** Claim a hidden, non-resumable identity pending exact-thread cross-check. */
258
+ markProvisionalProviderSessionId(id: string, value: string): void;
259
+ /** Remove only the matching hidden provisional identity. */
260
+ clearProvisionalProviderSessionId(id: string, value: string): void;
261
+ /** Atomically promote only the matching hidden provisional identity to resumable state. */
262
+ commitProvisionalProviderSessionId(id: string, value: string): void;
263
+ getSessionDefaults(): StoredSessionDefaults | undefined;
264
+ putSessionDefaults(defaults: SessionDefaults, expectedRevision: number, updatedAt: number): StoredSessionDefaults;
265
+ delete(id: string): void;
266
+ close(): void;
267
+ /** "sqlite" when better-sqlite3 loaded; "memory-fallback" when it didn't (non-durable). */
268
+ readonly mode: StoreMode;
269
+ }
270
+
271
+ declare global {
272
+ /** Stable package version injected by tsup. Source/test builds fall back to package.json. */
273
+ const __SERVER_VERSION__: string | undefined;
274
+ }
275
+
276
+ interface IPty {
277
+ onData(cb: (d: string) => void): void;
278
+ onExit(cb: (e: {
279
+ exitCode: number;
280
+ }) => void): void;
281
+ write(d: string): void;
282
+ resize(c: number, r: number): void;
283
+ kill(sig?: string): void;
284
+ }
285
+ type PtySpawn = (file: string, args: string[], opts: {
286
+ name: string;
287
+ cols: number;
288
+ rows: number;
289
+ cwd: string;
290
+ env: NodeJS.ProcessEnv;
291
+ }) => IPty;
292
+
293
+ type PaneStatus = "working" | "blocked" | "idle";
294
+
295
+ declare class ProviderRegistry {
296
+ private readonly byId;
297
+ constructor(providers: readonly AgentProvider[]);
298
+ get(id: ProviderId): AgentProvider;
299
+ list(): AgentProvider[];
300
+ }
301
+
302
+ interface CodexSpawnLease {
303
+ readonly started: Promise<void>;
304
+ cancel(): Promise<void>;
305
+ }
306
+
307
+ interface CodexThreadPersistence {
308
+ markProvisional(id: string): void;
309
+ clear(id: string): void;
310
+ commit(id: string): void;
311
+ }
312
+
313
+ interface CodexThreadInventoryEntry {
314
+ readonly id: string;
315
+ readonly cwd: string;
316
+ readonly source: "cli" | "vscode" | "exec" | "appServer" | "unknown" | Record<string, unknown>;
317
+ /** Current protocol uses Unix seconds; milliseconds are normalized defensively for compatible revisions. */
318
+ readonly createdAt: number;
319
+ }
320
+ type ReadCodexThreadInventory = () => Promise<readonly CodexThreadInventoryEntry[]>;
321
+ interface CodexThreadResolverOptions {
322
+ readonly inventory: ReadCodexThreadInventory;
323
+ readonly now?: () => number;
324
+ readonly sleep?: (milliseconds: number, signal?: AbortSignal) => Promise<void>;
325
+ readonly deadlineMs?: number;
326
+ readonly pollIntervalMs?: number;
327
+ readonly maxInventoryItems?: number;
328
+ readonly creationSkewMs?: number;
329
+ readonly cancellationAckMs?: number;
330
+ }
331
+ interface ResolveCodexThreadOptions {
332
+ readonly cwd: string;
333
+ /** The launch callback is intentionally inside the process-wide identity mutex. */
334
+ readonly spawn: (signal: AbortSignal) => CodexSpawnLease;
335
+ readonly persistence: CodexThreadPersistence;
336
+ readonly signal?: AbortSignal;
337
+ }
338
+ declare class CodexThreadResolver {
339
+ private readonly inventory;
340
+ private readonly now;
341
+ private readonly sleep;
342
+ private readonly deadlineMs;
343
+ private readonly pollIntervalMs;
344
+ private readonly maxInventoryItems;
345
+ private readonly creationSkewMs;
346
+ private readonly cancellationAckMs;
347
+ private readonly maxPolls;
348
+ constructor(options: CodexThreadResolverOptions);
349
+ resolveAfterSpawn(options: ResolveCodexThreadOptions): Promise<string>;
350
+ private resolveLocked;
351
+ private readInventory;
352
+ private candidates;
353
+ private sameIdentity;
354
+ private throwIfCancelled;
355
+ }
356
+
357
+ interface TerminalMeta {
358
+ id: string;
359
+ provider: ProviderId;
360
+ cwd: string;
361
+ mode: "terminal";
362
+ status: "running" | "ended";
363
+ createdAt: number;
364
+ lastActivityAt: number;
365
+ /**
366
+ * The session's LIVE activity, derived every ~2.5s from its rendered tmux pane by the capture-pane monitor
367
+ * (TerminalManager.refreshActivity → pane-status.classifyPaneStatus). UNIVERSAL (no hooks) + works while
368
+ * detached. Surfaced in GET /sessions to drive the rail's per-session status:
369
+ * - "working" — actively generating (main spinner OR background agents still developing);
370
+ * - "blocked" — claude is WAITING ON YOUR DECISION (a permission or plan prompt) → the loud
371
+ * "needs you". This is the ONLY state that alerts;
372
+ * - "idle" — a finished turn sitting at an empty prompt, nothing running, nothing to decide (calm).
373
+ */
374
+ activity: PaneStatus;
375
+ /**
376
+ * Back-compat boolean = `activity === "blocked"`. Drives the loud "needs you" badge/chip, the away push, and
377
+ * the badge count. Kept as a distinct field so existing consumers (awaitingCount, the push gate) don't all
378
+ * have to learn the 3-state enum. Set together with `activity` by the monitor.
379
+ */
380
+ awaiting: boolean;
381
+ /**
382
+ * Whether this session's claude runs with `--dangerously-skip-permissions` (RCE-by-design: claude can run
383
+ * any tool without asking). Derived at create() from the spawn args, persisted, and surfaced in GET /sessions
384
+ * so the web rail can badge a session as running in skip-permissions mode.
385
+ */
386
+ dangerouslySkip: boolean;
387
+ /** The effective model: initialized from launch options, then refreshed from live provider chrome when the
388
+ * TUI exposes it. Surfaced in GET /sessions so the chat header/rail follow in-session changes. */
389
+ model?: string;
390
+ /** Effective effort/reasoning: initialized from launch options, then refreshed from live provider chrome.
391
+ * Absent means the provider controls its default. */
392
+ effort?: string;
393
+ /** Provider-native safety controls captured at launch for exact UI display. */
394
+ permissionMode?: ClaudeSessionOptions["permissionMode"];
395
+ sandbox?: CodexSessionOptions["sandbox"];
396
+ approvalPolicy?: CodexSessionOptions["approvalPolicy"];
397
+ /** User-set display name (PATCH /sessions/:id). SERVER-side so a rename shows on every device, not just
398
+ * the one that typed it. Persisted; absent = unnamed (the UI falls back to the cwd). */
399
+ name?: string;
400
+ /** Codex exact-thread capture state. Claude sessions do not need a provider resume identity. */
401
+ identityState?: "pending" | "exact" | "ambiguous";
402
+ providerSessionId?: string;
403
+ }
404
+ interface TerminalSub {
405
+ unsubscribe(): void;
406
+ }
407
+ interface TerminalManagerDeps {
408
+ store: SessionStore;
409
+ providers: ProviderRegistry;
410
+ now: () => number;
411
+ ptySpawn?: PtySpawn;
412
+ runTmux?: (args: string[]) => void;
413
+ /** Dedicated tmux server socket. Defaults to the unchanged production socket; integration tests inject a
414
+ * unique socket so spawn, capture, resume, and cleanup cannot touch a live RoamCode instance. */
415
+ tmuxSocket?: string;
416
+ /**
417
+ * Best-effort notifier fired on a false→true `awaiting` transition WHEN NO CLIENT IS ATTACHED (i.e. the
418
+ * user is away from the desk). Wired by the transport to the push dispatcher; omitted in tests. Called in
419
+ * a try/catch — a throw here can NEVER break the terminal.
420
+ */
421
+ onAwaiting?: (id: string) => void;
422
+ /**
423
+ * Best-effort notifier fired when a session's claude exits (the "done" ping). `wasAttached` reports whether
424
+ * a client was still attached at the moment of exit — captured BEFORE the subs are torn down — so the
425
+ * transport can gate the away-from-desk push on "nobody was watching" (you already see the WS close when you
426
+ * are). Same never-throw contract.
427
+ */
428
+ onFinished?: (id: string, wasAttached: boolean) => void;
429
+ /**
430
+ * Capture a tmux session's CURRENT rendered pane as plain text (READ-ONLY). Injected for tests; in
431
+ * production it defaults to a real `capture-pane -p` on {@link TMUX_SOCKET}. Drives {@link refreshActivity},
432
+ * the universal (hook-free) working-vs-awaiting classifier.
433
+ */
434
+ capturePane?: (sessionName: string) => Promise<string>;
435
+ /** Builds the cwd-scoped exact-thread resolver used around a fresh Codex TUI spawn. */
436
+ codexThreadResolver?: (cwd: string) => CodexThreadResolver;
437
+ }
438
+ type CreateTerminalOptions = {
439
+ id: string;
440
+ cwd: string;
441
+ provider: "claude";
442
+ options: ClaudeSessionOptions;
443
+ cols?: number;
444
+ rows?: number;
445
+ } | {
446
+ id: string;
447
+ cwd: string;
448
+ provider: "codex";
449
+ options: CodexSessionOptions;
450
+ cols?: number;
451
+ rows?: number;
452
+ };
453
+ type LegacyCreateTerminalOptions = {
454
+ id: string;
455
+ cwd: string;
456
+ claudeArgs?: string[];
457
+ cols?: number;
458
+ rows?: number;
459
+ };
460
+ declare class TerminalManager {
461
+ private readonly deps;
462
+ private readonly records;
463
+ private readonly providers;
464
+ private attachConfig?;
465
+ constructor(deps: TerminalManagerDeps);
466
+ /** Late-bound (after listen(), which resolves the loopback port) — same config the chat SessionManager
467
+ * gets. When set, each terminal's claude is spawned with `--mcp-config` so send_image/send_file work. */
468
+ setAttachConfig(attach: AttachSpawnOptions | undefined): void;
469
+ create(explicit: CreateTerminalOptions): TerminalMeta;
470
+ /** Temporary explicit-Claude seam for the pre-provider transport. */
471
+ createLegacyClaude(opts: LegacyCreateTerminalOptions): TerminalMeta;
472
+ /**
473
+ * Delete stale per-session 0600 files that hold the access token — `mcp-config-<id>.json`, `hooks-<id>.json`,
474
+ * `hook-auth-<id>`, and `codex-mcp-token-<id>`. A file is stale when no live session owns its id: leaked by a
475
+ * crash, an orphan-reap, a rehydrated record (which carries no such paths, so stop() never unlinks its files),
476
+ * or a token rotation. Call at boot AFTER rehydrate + setAttachConfig so `records` reflects the surviving
477
+ * sessions. No-op without an attach config.
478
+ */
479
+ sweepStaleMcpConfigs(): number;
480
+ /**
481
+ * Kill running terminal sessions with NO attached client that have been idle longer than `ttlMs`. OFF by
482
+ * default (ttlMs <= 0) and opt-in via env (SESSION_IDLE_TTL_MS): sessions intentionally survive a
483
+ * disconnect for later reattach, so reaping them must never be the default — it's only for hosts that
484
+ * choose to bound the accumulation of detached claude+tmux processes. Returns the number reaped.
485
+ */
486
+ reapIdle(ttlMs: number): number;
487
+ /**
488
+ * Push a JSON control message to every attached client of a terminal session. An `attach` (file/image)
489
+ * frame is ALSO buffered (bounded, oldest-dropped) so a client that (re)connects later still sees files
490
+ * that arrived while it was away — see {@link attach}'s replay. Other control frames (e.g. `ask`, which
491
+ * has its own answer/replay flow in the transport) are delivered live only.
492
+ */
493
+ pushControl(id: string, msg: unknown): boolean;
494
+ /**
495
+ * Set a session's `awaiting` flag ("claude is blocked on YOU"). The capture-pane monitor
496
+ * ({@link refreshActivity}) is the authority for this flag in production; this explicit setter exists for
497
+ * direct overrides and is exercised by the manager's tests. Deterministic — no timers, no terminal scraping.
498
+ * A missing session is a no-op. Does NOT fire the away-from-desk push — the monitor owns that so it can gate
499
+ * on {@link isAttached}.
500
+ */
501
+ setAwaiting(id: string, value: boolean): void;
502
+ /** Clear the awaiting flag (user is active / session ended). */
503
+ private clearAwaiting;
504
+ /** Whether a session currently has ≥1 attached client (a live browser WS). The hook route uses this so the
505
+ * away-from-desk push fires ONLY when nobody is watching. */
506
+ isAttached(id: string): boolean;
507
+ /** How many sessions are currently `awaiting` you. Threaded into each away-from-desk push as `badgeCount`
508
+ * so the home-screen app badge tracks "how many sessions need you" (Android/desktop badge; iOS can't). */
509
+ awaitingCount(): number;
510
+ /**
511
+ * Set/clear a session's display name (PATCH /sessions/:id). `undefined` clears back to unnamed. Written
512
+ * through to the store so the name survives restarts + rehydrate. Returns false for an unknown id so the
513
+ * route can 404 without a separate lookup.
514
+ */
515
+ setName(id: string, name: string | undefined): boolean;
516
+ /**
517
+ * Re-derive every RUNNING session's `awaiting` flag from its LIVE rendered tmux pane (`capture-pane`) — the
518
+ * single, UNIVERSAL source of truth for working / blocked / idle. Works for EVERY session (no per-session
519
+ * hooks needed → correct for sessions created before hooks existed) and works while the browser is DETACHED
520
+ * (it reads tmux directly). Called on a ~2.5s timer from start.ts. Read-only: it can never disturb a live
521
+ * session.
522
+ *
523
+ * {@link classifyPaneStatus} decides. A capture that returns "" (tmux hiccup) leaves the last value
524
+ * untouched so the status never flaps on a transient miss.
525
+ *
526
+ * `awaiting` (the loud "needs you" flag) tracks activity==="blocked" ONLY — a finished-but-idle session, or
527
+ * one whose background agents are still developing, is NOT "needs you". Fires
528
+ * {@link TerminalManagerDeps.onAwaiting} (the away push) on a not-blocked→blocked transition when no client is
529
+ * attached, so the nudge is universal + fires only for a genuine block.
530
+ */
531
+ refreshActivity(): Promise<void>;
532
+ /**
533
+ * Nudge tmux to repaint the WHOLE screen for a session whose pty is already running — used when a client
534
+ * REATTACHES (a fresh xterm) to a still-live tmux client that drew its screen earlier and won't redraw on
535
+ * its own, so the new client would otherwise show only a blinking cursor until something changes. A brief
536
+ * pty size wiggle (+1 row, then back) sends SIGWINCH, which makes tmux redraw — exactly what the manual
537
+ * window-resize the user found does, minus the manual part. Deferred so the new client's own initial resize
538
+ * lands first (rec.cols/rec.rows are then the final size we wiggle around). Best-effort + unref'd timers.
539
+ */
540
+ private forceRedraw;
541
+ /** Subscribe to provider output, spawning lazily through the owning provider on the first attach. */
542
+ attach(id: string, handlers: {
543
+ onData: (chunk: string) => void;
544
+ onExit?: () => void;
545
+ onControl?: (msg: string) => void;
546
+ }, size?: {
547
+ cols: number;
548
+ rows: number;
549
+ }, opts?: {
550
+ respawn?: "continue" | "fresh";
551
+ signal?: AbortSignal;
552
+ }): Promise<TerminalSub | undefined>;
553
+ private spawnForRecord;
554
+ private cleanupProviderPaths;
555
+ private cleanupRecordPaths;
556
+ private applyRuntimeSignal;
557
+ write(id: string, data: string): void;
558
+ resize(id: string, cols: number, rows: number): void;
559
+ stop(id: string): void;
560
+ get(id: string): TerminalMeta | undefined;
561
+ list(): TerminalMeta[];
562
+ /**
563
+ * After a server/OTA restart: adopt stored terminal sessions whose tmux session is still alive (so they
564
+ * reappear, resumable), prune store rows whose tmux session is gone, AND kill ORPHAN tmux sessions — live
565
+ * `rc-*` sessions with no store row (leaked by a crash or an interrupted cleanup) — so they don't pile up.
566
+ */
567
+ rehydrate(opts: {
568
+ liveTmuxNames: string[];
569
+ }): void;
570
+ /** Kill a tmux session for an id without needing a live proc (reuses TerminalProcess's socketed kill). */
571
+ private killTmux;
572
+ }
573
+
574
+ interface CreateServerResult {
575
+ app: FastifyInstance;
576
+ authGate: AuthGate;
577
+ /** Exposed so startServer can late-bind the MCP attach config (after listen() resolves the port) —
578
+ * this is what gives the terminal's claude send_image/send_file. */
579
+ terminalManager: TerminalManager;
580
+ /** False when tmux/node-pty is unavailable → terminal sessions are disabled (startServer warns loudly). */
581
+ terminalAvailable: boolean;
582
+ }
583
+
584
+ declare function providerPreflightWarning(name: string, availability: ProviderAvailability): string | undefined;
585
+ declare function runProviderPreflight(providers: ReadonlyArray<{
6
586
  name: string;
7
587
  probe(): Promise<ProviderAvailability>;
8
588
  }>, warn?: (msg: string) => void): Promise<void>;
@@ -12,14 +592,14 @@ export declare function runProviderPreflight(providers: ReadonlyArray<{
12
592
  * to surface WHY in the logs immediately, like the better-sqlite3 fallback warning. Returns `undefined`
13
593
  * when claude is available (no warning). PURE so it's unit-testable without spawning.
14
594
  */
15
- export declare function claudePreflightWarning(availability: ClaudeAvailability): string | undefined;
595
+ declare function claudePreflightWarning(availability: ClaudeAvailability): string | undefined;
16
596
  /**
17
597
  * Run the startup preflight: best-effort probe `claude --version` and print {@link claudePreflightWarning}
18
598
  * if it's missing/failing. NEVER throws and NEVER blocks boot — a hung/slow claude can't stall startup
19
599
  * (the probe is short-timeout-guarded). Injectable probe + log sink so it's testable without a spawn.
20
600
  */
21
- export declare function runClaudePreflight(probe: ClaudeVersionProbe, warn?: (msg: string) => void): Promise<void>;
22
- export declare function startServer(env?: NodeJS.ProcessEnv): Promise<CreateServerResult & {
601
+ declare function runClaudePreflight(probe: ClaudeVersionProbe, warn?: (msg: string) => void): Promise<void>;
602
+ declare function startServer(env?: NodeJS.ProcessEnv): Promise<CreateServerResult & {
23
603
  url: string;
24
604
  token?: string;
25
605
  tokenGenerated: boolean;
@@ -28,4 +608,6 @@ export declare function startServer(env?: NodeJS.ProcessEnv): Promise<CreateServ
28
608
  * `error` (e.g. a write-after-teardown on a dying claude child, or a detached updater spawn failure)
29
609
  * LOGS instead of taking the whole server down — for an always-on self-hosted server, staying up beats
30
610
  * crashing. Install ONCE at a process entry (never in startServer, which tests call repeatedly). */
31
- export declare function installCrashGuards(): void;
611
+ declare function installCrashGuards(): void;
612
+
613
+ export { claudePreflightWarning, installCrashGuards, providerPreflightWarning, runClaudePreflight, runProviderPreflight, startServer };