@roamcode.ai/server 1.0.0 → 1.0.2

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 +8324 -44
  3. package/dist/managed-update-helper.js +631 -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
@@ -1,411 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import { randomUUID } from "node:crypto";
3
- import { chmodSync, existsSync, lstatSync, mkdirSync, openSync, closeSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, symlinkSync, unlinkSync, writeFileSync, } from "node:fs";
4
- import { homedir, tmpdir } from "node:os";
5
- import { basename, dirname, join, relative, resolve } from "node:path";
6
- import { migrateServiceToLauncher, readServiceRecord, restartService } from "./service-install.js";
7
- const VERSION_RE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
8
- const LOCK_STALE_MS = 30 * 60_000;
9
- export function resolveInstallRoot(env = process.env, home = homedir()) {
10
- return env.ROAMCODE_INSTALL_ROOT || join(home, ".local", "share", "roamcode");
11
- }
12
- export function managedPaths(root) {
13
- return {
14
- root,
15
- releases: join(root, "releases"),
16
- staging: join(root, "staging"),
17
- current: join(root, "current"),
18
- previous: join(root, "previous"),
19
- bin: join(root, "bin"),
20
- launcher: join(root, "bin", "roamcode"),
21
- lock: join(root, "update.lock"),
22
- };
23
- }
24
- export function isStableVersion(value) {
25
- return VERSION_RE.test(value);
26
- }
27
- export function compareVersions(a, b) {
28
- if (!isStableVersion(a) || !isStableVersion(b))
29
- throw new Error(`invalid stable version: ${a} / ${b}`);
30
- const av = a.split(".").map(Number);
31
- const bv = b.split(".").map(Number);
32
- for (let i = 0; i < 3; i += 1) {
33
- if (av[i] !== bv[i])
34
- return av[i] < bv[i] ? -1 : 1;
35
- }
36
- return 0;
37
- }
38
- function atomicWrite(path, value, mode = 0o600) {
39
- mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
40
- const temp = `${path}.${process.pid}.${randomUUID()}.tmp`;
41
- writeFileSync(temp, value, { mode });
42
- chmodSync(temp, mode);
43
- renameSync(temp, path);
44
- }
45
- export function renderManagedLauncher(root, nodePath) {
46
- const q = (value) => `'${value.replace(/'/g, `'\\''`)}'`;
47
- return [
48
- "#!/bin/sh",
49
- "set -eu",
50
- `ROOT=${q(root)}`,
51
- `NODE=${q(nodePath)}`,
52
- 'if [ ! -x "$NODE" ]; then NODE="$(command -v node || true)"; fi',
53
- 'if [ -z "$NODE" ]; then echo "Node.js >= 24 is required" >&2; exit 1; fi',
54
- 'ENTRY="$ROOT/current/node_modules/roamcode/dist/index.js"',
55
- 'if [ ! -f "$ENTRY" ]; then',
56
- ' echo "roamcode managed runtime is missing; run: npx roamcode@latest install" >&2',
57
- " exit 1",
58
- "fi",
59
- 'export ROAMCODE_INSTALL_ROOT="$ROOT"',
60
- 'export ROAMCODE_MANAGED_EXEC="1"',
61
- 'exec "$NODE" "$ENTRY" "$@"',
62
- "",
63
- ].join("\n");
64
- }
65
- export function writeManagedLauncher(root, nodePath = process.execPath) {
66
- const paths = managedPaths(root);
67
- mkdirSync(paths.bin, { recursive: true, mode: 0o700 });
68
- atomicWrite(paths.launcher, renderManagedLauncher(root, nodePath), 0o700);
69
- return paths.launcher;
70
- }
71
- function versionFromReleaseDir(dir) {
72
- try {
73
- const manifest = JSON.parse(readFileSync(join(dir, "release.json"), "utf8"));
74
- if (typeof manifest.version === "string" && isStableVersion(manifest.version))
75
- return manifest.version;
76
- }
77
- catch {
78
- // Older managed installs may not have release.json; fall back to the npm package manifest.
79
- }
80
- try {
81
- const pkg = JSON.parse(readFileSync(join(dir, "node_modules", "roamcode", "package.json"), "utf8"));
82
- return typeof pkg.version === "string" && isStableVersion(pkg.version) ? pkg.version : undefined;
83
- }
84
- catch {
85
- return undefined;
86
- }
87
- }
88
- function integrityFromReleaseDir(dir) {
89
- try {
90
- const manifest = JSON.parse(readFileSync(join(dir, "release.json"), "utf8"));
91
- return typeof manifest.integrity === "string" ? manifest.integrity : undefined;
92
- }
93
- catch {
94
- return undefined;
95
- }
96
- }
97
- export function readActiveVersion(root) {
98
- const current = managedPaths(root).current;
99
- try {
100
- return versionFromReleaseDir(realpathSync(current));
101
- }
102
- catch {
103
- return undefined;
104
- }
105
- }
106
- export function readPreviousVersion(root) {
107
- try {
108
- return versionFromReleaseDir(realpathSync(managedPaths(root).previous));
109
- }
110
- catch {
111
- return undefined;
112
- }
113
- }
114
- function acquireLock(path, now) {
115
- mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
116
- try {
117
- const fd = openSync(path, "wx", 0o600);
118
- writeFileSync(fd, JSON.stringify({ pid: process.pid, at: now }) + "\n");
119
- closeSync(fd);
120
- }
121
- catch {
122
- try {
123
- if (now - lstatSync(path).mtimeMs > LOCK_STALE_MS) {
124
- unlinkSync(path);
125
- return acquireLock(path, now);
126
- }
127
- }
128
- catch {
129
- // The competing process may have removed it; the retry below will either acquire or report busy.
130
- try {
131
- return acquireLock(path, now);
132
- }
133
- catch {
134
- // handled by the stable error below
135
- }
136
- }
137
- throw new Error("an update is already in progress");
138
- }
139
- return () => {
140
- try {
141
- unlinkSync(path);
142
- }
143
- catch {
144
- // best effort
145
- }
146
- };
147
- }
148
- async function runLogged(command, args, opts) {
149
- await new Promise((resolvePromise, reject) => {
150
- const child = spawn(command, args, {
151
- cwd: opts.cwd,
152
- env: opts.env ?? process.env,
153
- stdio: ["ignore", "pipe", "pipe"],
154
- });
155
- child.stdout?.on("data", (chunk) => opts.log(chunk.toString()));
156
- child.stderr?.on("data", (chunk) => opts.log(chunk.toString()));
157
- child.on("error", reject);
158
- child.on("close", (code) => code === 0 ? resolvePromise() : reject(new Error(`${basename(command)} exited ${code}`)));
159
- });
160
- }
161
- function npmInvocation(npmCommand, args, nodePath) {
162
- if (npmCommand.endsWith(".js") || npmCommand.endsWith(".cjs"))
163
- return { command: nodePath, args: [npmCommand, ...args] };
164
- return { command: npmCommand, args };
165
- }
166
- async function npmIntegrity(npmCommand, packageName, version, nodePath, log) {
167
- let output = "";
168
- const invocation = npmInvocation(npmCommand, ["view", `${packageName}@${version}`, "dist.integrity", "--json"], nodePath);
169
- await new Promise((resolvePromise, reject) => {
170
- const child = spawn(invocation.command, invocation.args, { stdio: ["ignore", "pipe", "pipe"] });
171
- child.stdout?.on("data", (chunk) => (output += chunk.toString()));
172
- child.stderr?.on("data", (chunk) => log(chunk.toString()));
173
- child.on("error", reject);
174
- child.on("close", (code) => (code === 0 ? resolvePromise() : reject(new Error(`npm view exited ${code}`))));
175
- });
176
- const parsed = JSON.parse(output);
177
- if (typeof parsed !== "string" || !parsed.startsWith("sha512-"))
178
- throw new Error("npm returned no package integrity");
179
- return parsed;
180
- }
181
- async function smokeServer(serverEntry, dataDir, nodePath, log) {
182
- const smokeDir = join(tmpdir(), `roamcode-smoke-${process.pid}-${randomUUID()}`);
183
- mkdirSync(smokeDir, { recursive: true, mode: 0o700 });
184
- let output = "";
185
- const child = spawn(nodePath, [serverEntry], {
186
- env: {
187
- ...process.env,
188
- PORT: "0",
189
- BIND_ADDRESS: "127.0.0.1",
190
- ACCESS_TOKEN: `rc-smoke-${randomUUID()}`,
191
- ROAMCODE_DATA_DIR: smokeDir,
192
- RC_TMUX_SOCKET: `rc-smoke-${process.pid}`,
193
- ROAMCODE_INSTALL_ROOT: "",
194
- },
195
- stdio: ["ignore", "pipe", "pipe"],
196
- });
197
- const collect = (chunk) => {
198
- const text = chunk.toString();
199
- output += text;
200
- log(text);
201
- };
202
- child.stdout?.on("data", collect);
203
- child.stderr?.on("data", collect);
204
- try {
205
- const url = await new Promise((resolveUrl, reject) => {
206
- const deadline = setTimeout(() => reject(new Error("new release did not start within 25s")), 25_000);
207
- const poll = setInterval(() => {
208
- const match = output.match(/listening on (http:\/\/127\.0\.0\.1:\d+)/);
209
- if (match?.[1]) {
210
- clearInterval(poll);
211
- clearTimeout(deadline);
212
- resolveUrl(match[1]);
213
- }
214
- }, 100);
215
- child.once("exit", (code) => {
216
- clearInterval(poll);
217
- clearTimeout(deadline);
218
- reject(new Error(`new release exited before health check (${code})`));
219
- });
220
- });
221
- const healthy = await fetch(`${url}/health`, { signal: AbortSignal.timeout(5_000) });
222
- if (!healthy.ok)
223
- throw new Error(`new release health check returned ${healthy.status}`);
224
- }
225
- finally {
226
- child.kill("SIGTERM");
227
- rmSync(smokeDir, { recursive: true, force: true });
228
- // dataDir is intentionally part of the signature: callers cannot accidentally smoke with production data.
229
- void dataDir;
230
- }
231
- }
232
- function replaceSymlink(link, target) {
233
- const temp = `${link}.${process.pid}.${randomUUID()}.new`;
234
- try {
235
- unlinkSync(temp);
236
- }
237
- catch {
238
- // absent
239
- }
240
- // Canonicalize both sides before computing a relative link. On macOS `/var` resolves to
241
- // `/private/var`; mixing one spelling from mkdtemp with the other from realpath creates a broken link.
242
- const parent = realpathSync(dirname(link));
243
- const canonicalTarget = realpathSync(target);
244
- symlinkSync(relative(parent, canonicalTarget), temp, "dir");
245
- renameSync(temp, link);
246
- }
247
- function trimLog(log) {
248
- return log.split("\n").slice(-20).join("\n").slice(-8_000);
249
- }
250
- export async function installManagedRelease(opts) {
251
- if (!isStableVersion(opts.version))
252
- throw new Error(`invalid release version: ${opts.version}`);
253
- const now = opts.now ?? Date.now;
254
- const operationId = opts.operationId ?? randomUUID();
255
- const nodePath = opts.nodePath ?? process.execPath;
256
- const npmCommand = opts.npmCommand ?? process.env.npm_execpath ?? "npm";
257
- const expectedIntegrities = opts.expectedIntegrities ?? (opts.expectedIntegrity ? { roamcode: opts.expectedIntegrity } : {});
258
- const rootIntegrity = expectedIntegrities.roamcode;
259
- const paths = managedPaths(opts.installRoot);
260
- mkdirSync(paths.releases, { recursive: true, mode: 0o700 });
261
- mkdirSync(paths.staging, { recursive: true, mode: 0o700 });
262
- const release = join(paths.releases, opts.version);
263
- const stage = join(paths.staging, `${opts.version}-${process.pid}-${randomUUID()}`);
264
- let logText = "";
265
- const log = (line) => {
266
- logText += line.endsWith("\n") ? line : `${line}\n`;
267
- mkdirSync(opts.dataDir, { recursive: true, mode: 0o700 });
268
- writeFileSync(join(opts.dataDir, "update.log"), line.endsWith("\n") ? line : `${line}\n`, { flag: "a" });
269
- };
270
- const fromVersion = readActiveVersion(opts.installRoot);
271
- const status = (state, phase, error) => {
272
- opts.onStatus?.({
273
- operationId,
274
- state,
275
- phase,
276
- target: opts.version,
277
- ...(fromVersion ? { fromVersion } : {}),
278
- ...(error ? { error } : {}),
279
- log: trimLog(logText),
280
- updatedAt: now(),
281
- });
282
- };
283
- const releaseLock = acquireLock(paths.lock, now());
284
- let activated = false;
285
- try {
286
- status("starting", "preparing");
287
- const reusableRelease = existsSync(release) &&
288
- versionFromReleaseDir(release) === opts.version &&
289
- (!rootIntegrity || integrityFromReleaseDir(release) === rootIntegrity);
290
- if (!reusableRelease) {
291
- if (Object.keys(expectedIntegrities).length > 0) {
292
- status("downloading", "checking package integrity");
293
- for (const [packageName, expected] of Object.entries(expectedIntegrities)) {
294
- const actual = await npmIntegrity(npmCommand, packageName, opts.version, nodePath, log);
295
- if (actual !== expected)
296
- throw new Error(`GitHub release and npm integrity do not match for ${packageName}`);
297
- }
298
- }
299
- status("installing", "installing package");
300
- mkdirSync(stage, { recursive: true, mode: 0o700 });
301
- const invocation = npmInvocation(npmCommand, [
302
- "install",
303
- "--prefix",
304
- stage,
305
- "--omit=dev",
306
- "--no-audit",
307
- "--no-fund",
308
- "--package-lock=false",
309
- `roamcode@${opts.version}`,
310
- ], nodePath);
311
- await runLogged(invocation.command, invocation.args, { log });
312
- const installed = versionFromReleaseDir(stage);
313
- if (installed !== opts.version)
314
- throw new Error(`installed roamcode ${installed ?? "unknown"}, expected ${opts.version}`);
315
- const serverEntry = join(stage, "node_modules", "@roamcode.ai", "server", "dist", "start.js");
316
- if (!existsSync(serverEntry))
317
- throw new Error("published package is missing @roamcode.ai/server/dist/start.js");
318
- status("verifying", "boot smoke");
319
- await smokeServer(serverEntry, opts.dataDir, nodePath, log);
320
- atomicWrite(join(stage, "release.json"), JSON.stringify({
321
- version: opts.version,
322
- installedAt: now(),
323
- package: `roamcode@${opts.version}`,
324
- ...(rootIntegrity ? { integrity: rootIntegrity } : {}),
325
- }, null, 2) + "\n");
326
- if (existsSync(release))
327
- rmSync(release, { recursive: true, force: true });
328
- renameSync(stage, release);
329
- }
330
- else {
331
- const serverEntry = join(release, "node_modules", "@roamcode.ai", "server", "dist", "start.js");
332
- if (!existsSync(serverEntry))
333
- throw new Error(`managed release ${opts.version} is incomplete`);
334
- status("verifying", "verifying installed release");
335
- await smokeServer(serverEntry, opts.dataDir, nodePath, log);
336
- }
337
- status("activating", "activating release");
338
- let oldTarget;
339
- try {
340
- oldTarget = realpathSync(paths.current);
341
- }
342
- catch {
343
- oldTarget = undefined;
344
- }
345
- if (oldTarget && resolve(oldTarget) !== resolve(release))
346
- replaceSymlink(paths.previous, oldTarget);
347
- replaceSymlink(paths.current, release);
348
- activated = true;
349
- const launcherPath = writeManagedLauncher(opts.installRoot, nodePath);
350
- const existingService = readServiceRecord(opts.dataDir);
351
- const record = existingService
352
- ? migrateServiceToLauncher({
353
- dataDir: opts.dataDir,
354
- installRoot: opts.installRoot,
355
- launcherPath,
356
- nodePath,
357
- })
358
- : undefined;
359
- if (opts.restart && record) {
360
- status("restarting", "restarting service");
361
- const restarted = restartService(record);
362
- if (!restarted.ok)
363
- throw new Error(restarted.error ?? "service restart failed");
364
- }
365
- status("done", "done");
366
- // Keep only the active release and its rollback target. A failed/staged release is cleaned in finally.
367
- const keep = new Set([opts.version, fromVersion].filter((value) => !!value));
368
- for (const name of readdirSync(paths.releases)) {
369
- if (isStableVersion(name) && !keep.has(name))
370
- rmSync(join(paths.releases, name), { recursive: true, force: true });
371
- }
372
- return {
373
- version: opts.version,
374
- ...(fromVersion ? { previousVersion: fromVersion } : {}),
375
- releaseDir: release,
376
- launcherPath,
377
- };
378
- }
379
- catch (error) {
380
- const message = error instanceof Error ? error.message : String(error);
381
- log(`FAILED: ${message}`);
382
- // If activation happened but restart failed, restore the previous pointer when available.
383
- if (activated) {
384
- try {
385
- const previous = realpathSync(paths.previous);
386
- replaceSymlink(paths.current, previous);
387
- }
388
- catch {
389
- // no previous managed release (first migration); the new release already passed boot-smoke
390
- }
391
- }
392
- status("failed", "failed", message);
393
- throw error;
394
- }
395
- finally {
396
- rmSync(stage, { recursive: true, force: true });
397
- releaseLock();
398
- }
399
- }
400
- export function writeManagedStatus(dataDir, status) {
401
- atomicWrite(join(dataDir, "update-status.json"), JSON.stringify(status, null, 2) + "\n", 0o600);
402
- }
403
- export function readManagedStatus(dataDir) {
404
- try {
405
- const parsed = JSON.parse(readFileSync(join(dataDir, "update-status.json"), "utf8"));
406
- return parsed && typeof parsed.operationId === "string" && typeof parsed.state === "string" ? parsed : undefined;
407
- }
408
- catch {
409
- return undefined;
410
- }
411
- }
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- export {};
@@ -1,33 +0,0 @@
1
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- export interface McpEnv {
3
- RC_BASE_URL?: string;
4
- RC_SESSION_ID?: string;
5
- RC_TOKEN?: string;
6
- RC_TOKEN_FILE?: string;
7
- }
8
- export interface DeliverArgs {
9
- path: string;
10
- caption?: string;
11
- kind: "image" | "file";
12
- }
13
- /**
14
- * A minimal MCP tool-result shape (content blocks + optional isError), enough for our two tools. The
15
- * index signature keeps it assignable to the SDK's `Result` (`{ [x: string]: unknown }`) tool-callback
16
- * return type without casting at the call sites.
17
- */
18
- export interface ToolResult {
19
- [key: string]: unknown;
20
- content: {
21
- type: "text";
22
- text: string;
23
- }[];
24
- isError?: boolean;
25
- }
26
- /**
27
- * Pure, unit-testable core: POST the attachment to roamcode and map the response to a tool-result.
28
- * NEVER throws — a bad path, a down server, or a network error all return an `isError` tool-result so
29
- * Claude learns it failed and can tell the user. `fetchImpl` is injectable for tests (defaults to fetch).
30
- */
31
- export declare function deliver(env: McpEnv, args: DeliverArgs, fetchImpl?: typeof fetch): Promise<ToolResult>;
32
- /** Build the MCP server with the two send tools wired to `deliver`. Reads env lazily per call. */
33
- export declare function createMcpSendServer(env?: McpEnv): McpServer;
@@ -1,37 +0,0 @@
1
- /**
2
- * Origin / CSWSH (cross-site WebSocket hijacking) guard for the WS upgrade and state-changing HTTP.
3
- *
4
- * The token is the only auth boundary, and it can leak into a URL (`?token=` connect link, push deep
5
- * link, browser history). The WS handshake (and any browser-driven HTTP) carries an `Origin` header the
6
- * page CANNOT forge — so a malicious cross-origin page that somehow holds the token still can't make the
7
- * BROWSER attach to our WS / drive our API, because the browser stamps its OWN origin on the request and
8
- * we reject it. Without this check, a leaked-in-URL token is enough for a foreign page to puppet the host.
9
- *
10
- * SAFE DEFAULT (must never reject the real app): a request is allowed when
11
- * - the `Origin` header is ABSENT (native/non-browser clients, same-origin navigations + most
12
- * same-origin GETs omit it — the PWA's own fetches are same-origin), OR
13
- * - the Origin's host:port equals the request `Host` (same-origin — the PWA always is), OR
14
- * - the Origin matches the configured public URL (ROAMCODE_PUBLIC_URL), OR
15
- * - the Origin is a loopback/localhost origin (local dev), OR
16
- * - the Origin is in the explicit ROAMCODE_ALLOWED_ORIGINS allow-list.
17
- * Only a PRESENT, cross-origin, non-allow-listed Origin is rejected (403).
18
- */
19
- /** Normalize a URL/origin string to a comparable `scheme://host[:port]` origin (lowercased). Returns
20
- * undefined for anything that isn't a parseable absolute URL (so it can never match by accident). */
21
- export declare function normalizeOrigin(value: string | undefined): string | undefined;
22
- export interface OriginCheckOptions {
23
- /** The configured public-facing origin (from ROAMCODE_PUBLIC_URL). May be a full URL. */
24
- publicUrl?: string;
25
- /** Extra allow-listed origins (ROAMCODE_ALLOWED_ORIGINS, comma-separated → array). */
26
- allowedOrigins?: string[];
27
- }
28
- /**
29
- * Decide whether a request's Origin is allowed. PURE / I/O-free so it is unit-testable; the transport
30
- * preHandler passes `origin` (the header) and `host` (the request Host header).
31
- *
32
- * @param origin the request's `Origin` header (may be undefined)
33
- * @param host the request's `Host` header (e.g. `remotecode.example.com` or `127.0.0.1:4280`)
34
- */
35
- export declare function isOriginAllowed(origin: string | undefined, host: string | undefined, opts?: OriginCheckOptions): boolean;
36
- /** Parse a comma-separated ROAMCODE_ALLOWED_ORIGINS value into a trimmed, non-empty list. */
37
- export declare function parseAllowedOrigins(raw: string | undefined): string[];
@@ -1,101 +0,0 @@
1
- /**
2
- * Origin / CSWSH (cross-site WebSocket hijacking) guard for the WS upgrade and state-changing HTTP.
3
- *
4
- * The token is the only auth boundary, and it can leak into a URL (`?token=` connect link, push deep
5
- * link, browser history). The WS handshake (and any browser-driven HTTP) carries an `Origin` header the
6
- * page CANNOT forge — so a malicious cross-origin page that somehow holds the token still can't make the
7
- * BROWSER attach to our WS / drive our API, because the browser stamps its OWN origin on the request and
8
- * we reject it. Without this check, a leaked-in-URL token is enough for a foreign page to puppet the host.
9
- *
10
- * SAFE DEFAULT (must never reject the real app): a request is allowed when
11
- * - the `Origin` header is ABSENT (native/non-browser clients, same-origin navigations + most
12
- * same-origin GETs omit it — the PWA's own fetches are same-origin), OR
13
- * - the Origin's host:port equals the request `Host` (same-origin — the PWA always is), OR
14
- * - the Origin matches the configured public URL (ROAMCODE_PUBLIC_URL), OR
15
- * - the Origin is a loopback/localhost origin (local dev), OR
16
- * - the Origin is in the explicit ROAMCODE_ALLOWED_ORIGINS allow-list.
17
- * Only a PRESENT, cross-origin, non-allow-listed Origin is rejected (403).
18
- */
19
- /** Normalize a URL/origin string to a comparable `scheme://host[:port]` origin (lowercased). Returns
20
- * undefined for anything that isn't a parseable absolute URL (so it can never match by accident). */
21
- export function normalizeOrigin(value) {
22
- if (!value)
23
- return undefined;
24
- const trimmed = value.trim();
25
- if (!trimmed || trimmed === "null")
26
- return undefined; // a sandboxed/opaque origin serializes to "null"
27
- try {
28
- const u = new URL(trimmed);
29
- // `URL.origin` is `scheme://host[:port]` with the default port elided — exactly the comparison we want.
30
- return u.origin.toLowerCase();
31
- }
32
- catch {
33
- return undefined;
34
- }
35
- }
36
- /** True if a host (no scheme) is a loopback/localhost name. Accepts a bare host or host:port. */
37
- function isLoopbackHost(host) {
38
- const h = host.toLowerCase().replace(/:\d+$/, ""); // drop a :port suffix
39
- if (h === "localhost" || h === "::1" || h === "[::1]")
40
- return true;
41
- return /^127(?:\.\d{1,3}){3}$/.test(h);
42
- }
43
- /** True if a normalized origin (`scheme://host[:port]`) is loopback. */
44
- function isLoopbackOrigin(origin) {
45
- try {
46
- return isLoopbackHost(new URL(origin).host);
47
- }
48
- catch {
49
- return false;
50
- }
51
- }
52
- /**
53
- * Decide whether a request's Origin is allowed. PURE / I/O-free so it is unit-testable; the transport
54
- * preHandler passes `origin` (the header) and `host` (the request Host header).
55
- *
56
- * @param origin the request's `Origin` header (may be undefined)
57
- * @param host the request's `Host` header (e.g. `remotecode.example.com` or `127.0.0.1:4280`)
58
- */
59
- export function isOriginAllowed(origin, host, opts = {}) {
60
- // Absent Origin → allow (native clients, same-origin navigations + most same-origin GETs omit it). A
61
- // present-but-unparseable / opaque ("null") Origin normalizes to undefined and is treated the same way
62
- // (it cannot be a real cross-origin browser page driving us — those send a concrete origin).
63
- const reqOrigin = normalizeOrigin(origin);
64
- if (reqOrigin === undefined)
65
- return true;
66
- // Same-origin: the Origin's host:port equals the request Host. The PWA (served same-origin) always is.
67
- if (host) {
68
- const sameOriginHost = (() => {
69
- try {
70
- return new URL(reqOrigin).host.toLowerCase() === host.trim().toLowerCase();
71
- }
72
- catch {
73
- return false;
74
- }
75
- })();
76
- if (sameOriginHost)
77
- return true;
78
- }
79
- // Loopback origin (local dev directly against 127.0.0.1/localhost).
80
- if (isLoopbackOrigin(reqOrigin))
81
- return true;
82
- // Configured public URL (the tunnel/user-facing origin the PWA is actually installed under).
83
- const publicOrigin = normalizeOrigin(opts.publicUrl);
84
- if (publicOrigin !== undefined && reqOrigin === publicOrigin)
85
- return true;
86
- // Explicit allow-list extension.
87
- for (const extra of opts.allowedOrigins ?? []) {
88
- if (normalizeOrigin(extra) === reqOrigin)
89
- return true;
90
- }
91
- return false;
92
- }
93
- /** Parse a comma-separated ROAMCODE_ALLOWED_ORIGINS value into a trimmed, non-empty list. */
94
- export function parseAllowedOrigins(raw) {
95
- if (!raw)
96
- return [];
97
- return raw
98
- .split(",")
99
- .map((s) => s.trim())
100
- .filter((s) => s.length > 0);
101
- }
@@ -1,61 +0,0 @@
1
- export type PaneStatus = "working" | "blocked" | "idle";
2
- /**
3
- * The newest Claude Code MAJOR.MINOR the classifier's markers below were VERIFIED against on a live box.
4
- * The whole classifier is tied to Claude Code's ENGLISH TUI strings — the load-bearing markers are:
5
- * - "Do you want to proceed" / "Would you like to proceed" (blocked — permission/plan prompts)
6
- * - the gerund ellipsis "…" + "↓ <n>k tokens" live counter (working — main spinner + live agents)
7
- * - a "…(<n>s"-style parenthesised spinner timer (working — pre-token-flow window)
8
- * - "Waiting for … to finish" / "esc to interrupt" (working)
9
- * A NEWER claude may reword any of these and silently degrade every rail status to "idle", so boot logs a
10
- * one-time warning (see start.ts) when the installed claude's MAJOR.MINOR exceeds this. Bump it after
11
- * re-verifying the markers against a newer claude.
12
- */
13
- export declare const CLASSIFIER_TESTED_UP_TO = "2.1";
14
- /**
15
- * True iff `current`'s MAJOR.MINOR is strictly NEWER than `testedUpTo`'s. Patch versions are ignored — a
16
- * patch release doesn't reword the TUI. Unparseable input (either side) → false, so a weird version string
17
- * can never produce a spurious warning. Pure.
18
- */
19
- export declare function isNewerMajorMinor(current: string, testedUpTo: string): boolean;
20
- /**
21
- * The one-line boot warning when the RUNNING claude is newer than the classifier was verified against —
22
- * or undefined when there is nothing to warn about (older/equal/unknown version). Pure so it's testable;
23
- * start.ts logs it once via console.warn and NEVER throws (a version bump must not affect boot).
24
- */
25
- export declare function classifierVersionWarning(claudeVersion: string | undefined): string | undefined;
26
- /**
27
- * Classify a session's live ACTIVITY from its RENDERED tmux pane (`capture-pane -p` — the CURRENT screen, not
28
- * scrollback). UNIVERSAL: works for any running session regardless of how claude was spawned (no per-session
29
- * hooks needed), and works while the browser is DETACHED (it reads the tmux session directly). Grounded in
30
- * Claude Code's real output — see pane-status.test.ts for captured samples.
31
- *
32
- * working → something is actively generating. The strongest tell is a LIVE token-flow counter "↓ 216.5k
33
- * tokens" — it appears on the MAIN spinner ("Schlepping… (1m 17s · ↓ 2.1k tokens)") AND on an
34
- * ACTIVE background agent ("⏺ general-purpose Listing f… 24m 23s · ↓ 216.5k tokens"), and is GONE
35
- * the moment a turn finishes ("Baked for 23m 15s"). So a session whose MAIN loop is at the prompt
36
- * but has agents still developing in the background reads "working" — NOT idle (the user's
37
- * explicit correction). Also: a spinner's parenthesised timer, "Waiting for … to finish", or
38
- * "esc to interrupt".
39
- * blocked → claude is WAITING ON A DECISION FROM YOU: a permission prompt ("Do you want to proceed?"), a
40
- * plan-mode approval ("Would you like to proceed?"). This is the ONLY state that earns the loud
41
- * "needs you" — so it must stay RARE + real; that's why it's just the explicit-prompt phrasings.
42
- * idle → none of the above: claude finished a turn and is sitting at an empty prompt with nothing running
43
- * and nothing to decide. A calm "your turn whenever" — NOT a loud alert.
44
- *
45
- * Order matters: blocked is checked first (a decision prompt overrides any stale spinner text), then working,
46
- * else idle.
47
- */
48
- export declare function classifyPaneStatus(pane: string): PaneStatus;
49
- /** How capturePane locates a session's tmux pane. */
50
- export interface CaptureOptions {
51
- tmuxBin?: string;
52
- socket: string;
53
- sessionName: string;
54
- timeoutMs?: number;
55
- }
56
- /**
57
- * Capture a tmux session's CURRENT pane as plain text (`capture-pane -p`, no escape sequences). READ-ONLY —
58
- * it never sends input or resizes, so it can NEVER disturb a live session. Best-effort: resolves "" on any
59
- * error/timeout and never throws. Async (non-blocking) so the activity monitor doesn't stall the event loop.
60
- */
61
- export declare function capturePane(opts: CaptureOptions): Promise<string>;