@agentproto/cli 0.1.0-alpha.5 → 0.1.0-alpha.8

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.mjs CHANGED
@@ -1,10 +1,11 @@
1
+ import * as childProc from 'child_process';
1
2
  import { execFile, spawn } from 'child_process';
2
3
  import { createHash, randomUUID, randomBytes } from 'crypto';
3
- import { mkdtemp, writeFile, chmod, rm, mkdir, readFile, readdir, stat } from 'fs/promises';
4
+ import { mkdtemp, writeFile, chmod, rm, mkdir, unlink, readFile, readdir, stat } from 'fs/promises';
4
5
  import { tmpdir, userInfo, hostname, homedir, platform } from 'os';
5
6
  import { resolve, join, dirname, isAbsolute, normalize, relative, basename } from 'path';
6
7
  import { promisify, parseArgs } from 'util';
7
- import { promises, existsSync } from 'fs';
8
+ import { promises, existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
8
9
  import { createInterface } from 'readline/promises';
9
10
  import { stdout, stdin } from 'process';
10
11
  import { createAgentCliRuntime } from '@agentproto/driver-agent-cli';
@@ -16,11 +17,11 @@ import matter from 'gray-matter';
16
17
  import { EventEmitter } from 'events';
17
18
  import { createServer } from 'http';
18
19
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
20
+ import WebSocket, { WebSocketServer } from 'ws';
19
21
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
20
22
  import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
21
23
  import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
22
24
  import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
23
- import WebSocket from 'ws';
24
25
 
25
26
  /**
26
27
  * @agentproto/cli v0.1.0-alpha
@@ -84,9 +85,12 @@ async function listInstalledAdapters(opts) {
84
85
  };
85
86
  out.push(info);
86
87
  } catch (err) {
87
- console.warn(
88
- `[agentproto/cli] listInstalledAdapters: skipping broken adapter '${slug}': ${err instanceof Error ? err.message : String(err)}`
89
- );
88
+ const msg = err instanceof Error ? err.message : String(err);
89
+ if (/Cannot find package|ERR_MODULE_NOT_FOUND/i.test(msg) && msg.includes(`@agentproto/adapter-${slug}`)) ; else {
90
+ console.warn(
91
+ `[agentproto/cli] listInstalledAdapters: skipping broken adapter '${slug}': ${msg}`
92
+ );
93
+ }
90
94
  }
91
95
  }
92
96
  }
@@ -259,10 +263,10 @@ async function runExternalStep(step, ledger, head) {
259
263
  `);
260
264
  const opener = platform() === "darwin" ? "open" : platform() === "win32" ? "cmd" : "xdg-open";
261
265
  const args = platform() === "win32" ? ["/c", "start", step.url] : [step.url];
262
- await new Promise((resolve3) => {
266
+ await new Promise((resolve4) => {
263
267
  const child = spawn(opener, args, { stdio: "ignore", detached: true });
264
- child.once("error", () => resolve3());
265
- child.once("spawn", () => resolve3());
268
+ child.once("error", () => resolve4());
269
+ child.once("spawn", () => resolve4());
266
270
  });
267
271
  let value = "";
268
272
  if (step.callback?.param) {
@@ -335,7 +339,7 @@ async function promptString(question, opts) {
335
339
  process.stdin.setRawMode?.(true);
336
340
  let buf = "";
337
341
  process.stdout.write(prompt);
338
- return new Promise((resolve3) => {
342
+ return new Promise((resolve4) => {
339
343
  const onData = (chunk) => {
340
344
  for (const code of chunk) {
341
345
  if (code === 13 || code === 10) {
@@ -343,7 +347,7 @@ async function promptString(question, opts) {
343
347
  process.stdin.setRawMode?.(false);
344
348
  process.stdout.write("\n");
345
349
  rl.close();
346
- resolve3(buf || opts.defaultValue || "");
350
+ resolve4(buf || opts.defaultValue || "");
347
351
  return;
348
352
  }
349
353
  if (code === 3) {
@@ -425,7 +429,7 @@ async function resolveSelectOptions(options) {
425
429
  });
426
430
  }
427
431
  async function runShellCapturing(cmd, opts) {
428
- return new Promise((resolve3) => {
432
+ return new Promise((resolve4) => {
429
433
  const child = spawn("bash", ["-lc", cmd], {
430
434
  stdio: opts.interactive ? "inherit" : ["ignore", "pipe", "pipe"]
431
435
  });
@@ -443,11 +447,11 @@ async function runShellCapturing(cmd, opts) {
443
447
  }, opts.timeoutMs);
444
448
  child.once("error", () => {
445
449
  clearTimeout(timer);
446
- resolve3({ exitCode: 127, stdout, stderr: stderr || "spawn error" });
450
+ resolve4({ exitCode: 127, stdout, stderr: stderr || "spawn error" });
447
451
  });
448
452
  child.once("exit", (code) => {
449
453
  clearTimeout(timer);
450
- resolve3({ exitCode: code ?? 0, stdout, stderr });
454
+ resolve4({ exitCode: code ?? 0, stdout, stderr });
451
455
  });
452
456
  });
453
457
  }
@@ -806,15 +810,15 @@ function homedir2() {
806
810
  return process.env["HOME"] ?? process.env["USERPROFILE"] ?? "/";
807
811
  }
808
812
  function spawnInherit(cmd, argv) {
809
- return new Promise((resolve3, reject) => {
813
+ return new Promise((resolve4, reject) => {
810
814
  const child = spawn(cmd, argv, { stdio: "inherit" });
811
815
  child.once("error", reject);
812
- child.once("exit", (code) => resolve3(code ?? 0));
816
+ child.once("exit", (code) => resolve4(code ?? 0));
813
817
  });
814
818
  }
815
819
  async function runVersionCheck(check) {
816
820
  if (!check) return { ok: false, message: "no version_check declared" };
817
- return new Promise((resolve3) => {
821
+ return new Promise((resolve4) => {
818
822
  const child = spawn("bash", ["-lc", check.cmd], {
819
823
  stdio: ["ignore", "pipe", "ignore"]
820
824
  });
@@ -822,19 +826,19 @@ async function runVersionCheck(check) {
822
826
  child.stdout.on("data", (c) => {
823
827
  buf += c.toString("utf8");
824
828
  });
825
- child.once("error", () => resolve3({ ok: false, message: "check failed" }));
829
+ child.once("error", () => resolve4({ ok: false, message: "check failed" }));
826
830
  child.once("exit", (code) => {
827
831
  if (code !== 0) {
828
- resolve3({ ok: false, message: `check exited ${code}` });
832
+ resolve4({ ok: false, message: `check exited ${code}` });
829
833
  return;
830
834
  }
831
835
  const re = new RegExp(check.parse);
832
836
  const m = buf.match(re);
833
837
  if (!m || !m[1]) {
834
- resolve3({ ok: false, message: "could not parse version" });
838
+ resolve4({ ok: false, message: "could not parse version" });
835
839
  return;
836
840
  }
837
- resolve3({ ok: true, message: `version ${m[1]}` });
841
+ resolve4({ ok: true, message: `version ${m[1]}` });
838
842
  });
839
843
  });
840
844
  }
@@ -1019,6 +1023,134 @@ function formatRelative(ms) {
1019
1023
  return `${Math.round(ms / 864e5)}d`;
1020
1024
  }
1021
1025
 
1026
+ // src/util/pty-factory.ts
1027
+ async function loadNodePtyFactory() {
1028
+ try {
1029
+ const nodePtyMod = await import('node-pty');
1030
+ const nodePty = nodePtyMod.default ?? nodePtyMod;
1031
+ if (typeof nodePty.spawn !== "function") return null;
1032
+ return (opts) => {
1033
+ const pty = nodePty.spawn(opts.command, opts.args, {
1034
+ name: "xterm-256color",
1035
+ cols: opts.cols,
1036
+ rows: opts.rows,
1037
+ cwd: opts.cwd,
1038
+ env: {
1039
+ ...process.env,
1040
+ ...opts.env ?? {}
1041
+ }
1042
+ });
1043
+ const proc = {
1044
+ get pid() {
1045
+ return pty.pid;
1046
+ },
1047
+ write(data) {
1048
+ pty.write(data);
1049
+ },
1050
+ resize(cols, rows) {
1051
+ pty.resize(cols, rows);
1052
+ },
1053
+ kill(signal) {
1054
+ pty.kill(signal);
1055
+ },
1056
+ onData(handler) {
1057
+ pty.onData(handler);
1058
+ },
1059
+ onExit(handler) {
1060
+ pty.onExit(handler);
1061
+ }
1062
+ };
1063
+ return proc;
1064
+ };
1065
+ } catch {
1066
+ return null;
1067
+ }
1068
+ }
1069
+ var CONFIG_FILE_PATH = () => join(homedir(), ".agentproto", "config.json");
1070
+ async function loadConfig(path) {
1071
+ const target = CONFIG_FILE_PATH();
1072
+ try {
1073
+ const raw = await promises.readFile(target, "utf8");
1074
+ const parsed = JSON.parse(raw);
1075
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1076
+ return parsed;
1077
+ }
1078
+ console.warn(
1079
+ `[runtime/config] ${target}: top-level value is not an object \u2014 ignoring`
1080
+ );
1081
+ return {};
1082
+ } catch (err) {
1083
+ const code = err.code;
1084
+ if (code && code !== "ENOENT") {
1085
+ console.warn(
1086
+ `[runtime/config] failed to read ${target}: ${err instanceof Error ? err.message : String(err)}`
1087
+ );
1088
+ }
1089
+ return {};
1090
+ }
1091
+ }
1092
+ var WORKSPACES_CONFIG_VERSION = 1;
1093
+ var DEFAULT_CONFIG_DIR = () => resolve(homedir(), ".agentproto");
1094
+ var DEFAULT_CONFIG_PATH = () => resolve(DEFAULT_CONFIG_DIR(), "workspaces.json");
1095
+ function sanitizeSlug(input2) {
1096
+ const trimmed = input2.trim().toLowerCase();
1097
+ const cleaned = trimmed.replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
1098
+ return cleaned || "workspace";
1099
+ }
1100
+ async function loadWorkspacesConfig(path = DEFAULT_CONFIG_PATH()) {
1101
+ let raw;
1102
+ try {
1103
+ raw = await promises.readFile(path, "utf8");
1104
+ } catch (err) {
1105
+ if (err.code === "ENOENT") {
1106
+ return { version: WORKSPACES_CONFIG_VERSION, workspaces: [] };
1107
+ }
1108
+ throw err;
1109
+ }
1110
+ let parsed;
1111
+ try {
1112
+ parsed = JSON.parse(raw);
1113
+ } catch (err) {
1114
+ throw new Error(
1115
+ `agentproto: ${path} is not valid JSON (${err instanceof Error ? err.message : String(err)}). Delete the file or fix it manually.`
1116
+ );
1117
+ }
1118
+ return normalizeConfig(parsed);
1119
+ }
1120
+ function normalizeConfig(parsed) {
1121
+ if (!parsed || typeof parsed !== "object") {
1122
+ return { version: WORKSPACES_CONFIG_VERSION, workspaces: [] };
1123
+ }
1124
+ const obj = parsed;
1125
+ const workspaces = [];
1126
+ if (Array.isArray(obj.workspaces)) {
1127
+ for (const entry of obj.workspaces) {
1128
+ if (!entry || typeof entry !== "object") continue;
1129
+ const e = entry;
1130
+ const slug = typeof e.slug === "string" ? sanitizeSlug(e.slug) : "";
1131
+ const path = typeof e.path === "string" ? e.path : "";
1132
+ if (!slug || !path) continue;
1133
+ const addedAt = typeof e.addedAt === "string" ? e.addedAt : (/* @__PURE__ */ new Date()).toISOString();
1134
+ const updatedAt = typeof e.updatedAt === "string" ? e.updatedAt : addedAt;
1135
+ const we = { slug, path, addedAt, updatedAt };
1136
+ if (typeof e.label === "string" && e.label.trim()) {
1137
+ we.label = e.label.trim();
1138
+ }
1139
+ workspaces.push(we);
1140
+ }
1141
+ }
1142
+ const dedup = /* @__PURE__ */ new Map();
1143
+ for (const w of workspaces) dedup.set(w.slug, w);
1144
+ const finalList = Array.from(dedup.values());
1145
+ const active = typeof obj.active === "string" && finalList.some((w) => w.slug === obj.active) ? obj.active : finalList[0]?.slug;
1146
+ const out = {
1147
+ version: WORKSPACES_CONFIG_VERSION,
1148
+ workspaces: finalList
1149
+ };
1150
+ if (active !== void 0) out.active = active;
1151
+ return out;
1152
+ }
1153
+
1022
1154
  // ../define-doctype/dist/index.mjs
1023
1155
  var DEFAULT_ID_PATTERN = /^[a-z0-9][a-z0-9._-]{1,79}$/;
1024
1156
  var MAX_DESCRIPTION_LEN = 2e3;
@@ -1153,7 +1285,7 @@ function createVerbs(spec) {
1153
1285
  }
1154
1286
  return { path, handle: newHandle, rendered };
1155
1287
  }
1156
- async function resolve22(block, ctx = {}) {
1288
+ async function resolve4(block, ctx = {}) {
1157
1289
  if ("inline" in block) {
1158
1290
  return spec.define(block.inline);
1159
1291
  }
@@ -1179,7 +1311,7 @@ function createVerbs(spec) {
1179
1311
  async function deleteFn(path) {
1180
1312
  await rm(path, { force: true });
1181
1313
  }
1182
- return { create, load, list, update, resolve: resolve22, delete: deleteFn };
1314
+ return { create, load, list, update, resolve: resolve4, delete: deleteFn };
1183
1315
  }
1184
1316
  function defaultBody(spec, frontmatter) {
1185
1317
  const id = typeof frontmatter.name === "string" ? frontmatter.name : typeof frontmatter.id === "string" ? frontmatter.id : typeof frontmatter.slug === "string" ? frontmatter.slug : spec.name;
@@ -1197,51 +1329,28 @@ createDoctype({
1197
1329
  validate(def) {
1198
1330
  const result = extensionFrontmatterSchema.safeParse(def);
1199
1331
  if (!result.success) {
1200
- throw new Error(
1201
- `defineExtension (AIP-40): ${result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
1202
- );
1332
+ throw new Error(`defineExtension (AIP-40): ${result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`);
1203
1333
  }
1204
1334
  },
1205
1335
  build(def) {
1206
1336
  return { ...def };
1207
1337
  }
1208
1338
  });
1209
- function parseExtensionManifest(source) {
1210
- const parsed = matter(source);
1211
- if (Object.keys(parsed.data).length === 0) {
1212
- throw new Error("parseExtensionManifest: missing or empty frontmatter");
1213
- }
1214
- const result = extensionFrontmatterSchema.safeParse(parsed.data);
1215
- if (!result.success) {
1216
- throw new Error(
1217
- `parseExtensionManifest: invalid frontmatter \u2014 ${result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
1218
- );
1219
- }
1220
- return { frontmatter: result.data, body: parsed.content };
1221
- }
1222
-
1223
- // ../extension/dist/index.mjs
1224
1339
  function specFromExtension(extension, opts = {}) {
1225
1340
  const { parent } = opts;
1226
1341
  const isRoot = extension.extends === "none";
1227
1342
  if (!isRoot && !parent) {
1228
- throw new Error(
1229
- `specFromExtension (AIP-40): extension '${extension.slug}' extends '${extension.extends}' but no parent spec was provided in opts.parent`
1230
- );
1343
+ throw new Error(`specFromExtension (AIP-40): extension '${extension.slug}' extends '${extension.extends}' but no parent spec was provided in opts.parent`);
1231
1344
  }
1232
1345
  if (isRoot && !extension.path_convention) {
1233
- throw new Error(
1234
- `specFromExtension (AIP-40): extension '${extension.slug}' is a root doctype (extends: none) and MUST declare path_convention`
1235
- );
1346
+ throw new Error(`specFromExtension (AIP-40): extension '${extension.slug}' is a root doctype (extends: none) and MUST declare path_convention`);
1236
1347
  }
1237
1348
  if (parent && extension.tighten) {
1238
1349
  verifyTightening(extension);
1239
1350
  }
1240
1351
  const slugName = extension.slug.split(":")[1] ?? extension.slug;
1241
1352
  const pathTemplate = extension.path_convention ?? null;
1242
- const extensionKeys = new Set(
1243
- Object.keys(extension.add_fields?.properties ?? {})
1244
- );
1353
+ const extensionKeys = new Set(Object.keys(extension.add_fields?.properties ?? {}));
1245
1354
  const define = (params) => {
1246
1355
  const withDefaults = { ...params };
1247
1356
  if (extension.defaults) {
@@ -1257,8 +1366,10 @@ function specFromExtension(extension, opts = {}) {
1257
1366
  const parentOnly = {};
1258
1367
  const extensionOnly = {};
1259
1368
  for (const [key, value] of Object.entries(withDefaults)) {
1260
- if (extensionKeys.has(key)) extensionOnly[key] = value;
1261
- else parentOnly[key] = value;
1369
+ if (extensionKeys.has(key))
1370
+ extensionOnly[key] = value;
1371
+ else
1372
+ parentOnly[key] = value;
1262
1373
  }
1263
1374
  const parentHandle = parent.define(parentOnly);
1264
1375
  return Object.freeze({
@@ -1269,11 +1380,7 @@ function specFromExtension(extension, opts = {}) {
1269
1380
  const parse = isRoot ? rootParse(extension) : parent.parse;
1270
1381
  const pathOf = (handle) => {
1271
1382
  if (pathTemplate) {
1272
- return resolvePathTemplate(
1273
- pathTemplate,
1274
- handle,
1275
- slugName
1276
- );
1383
+ return resolvePathTemplate(pathTemplate, handle, slugName);
1277
1384
  }
1278
1385
  return parent.pathOf(handle);
1279
1386
  };
@@ -1290,33 +1397,36 @@ function verifyTightening(extension, parent) {
1290
1397
  const t = extension.tighten ?? {};
1291
1398
  for (const [field, override] of Object.entries(t)) {
1292
1399
  if (typeof override.minLength === "number" && typeof override.maxLength === "number" && override.minLength > override.maxLength) {
1293
- throw new Error(
1294
- `specFromExtension (AIP-40): extension '${extension.slug}' tighten.${field}: minLength (${override.minLength}) > maxLength (${override.maxLength})`
1295
- );
1400
+ throw new Error(`specFromExtension (AIP-40): extension '${extension.slug}' tighten.${field}: minLength (${override.minLength}) > maxLength (${override.maxLength})`);
1296
1401
  }
1297
1402
  if (typeof override.minimum === "number" && typeof override.maximum === "number" && override.minimum > override.maximum) {
1298
- throw new Error(
1299
- `specFromExtension (AIP-40): extension '${extension.slug}' tighten.${field}: minimum (${override.minimum}) > maximum (${override.maximum})`
1300
- );
1403
+ throw new Error(`specFromExtension (AIP-40): extension '${extension.slug}' tighten.${field}: minimum (${override.minimum}) > maximum (${override.maximum})`);
1301
1404
  }
1302
1405
  if (override.enum !== void 0 && !Array.isArray(override.enum)) {
1303
- throw new Error(
1304
- `specFromExtension (AIP-40): extension '${extension.slug}' tighten.${field}: enum must be an array`
1305
- );
1406
+ throw new Error(`specFromExtension (AIP-40): extension '${extension.slug}' tighten.${field}: enum must be an array`);
1306
1407
  }
1307
1408
  }
1308
1409
  }
1309
1410
  function rootParse(extension) {
1310
1411
  return (source) => {
1311
- throw new Error(
1312
- `specFromExtension (AIP-40): extension '${extension.slug}' is a root doctype \u2014 supply your own parser or extend a public AIP for parser inheritance`
1313
- );
1412
+ throw new Error(`specFromExtension (AIP-40): extension '${extension.slug}' is a root doctype \u2014 supply your own parser or extend a public AIP for parser inheritance`);
1314
1413
  };
1315
1414
  }
1316
1415
  function resolvePathTemplate(template, handle, doctypeSlug) {
1317
1416
  const identity = typeof handle.id === "string" && handle.id || typeof handle.slug === "string" && handle.slug || typeof handle.name === "string" && handle.name || "unknown";
1318
1417
  return template.replace(/<slug>/g, identity).replace(/<DOCTYPE>/g, doctypeSlug.toUpperCase());
1319
1418
  }
1419
+ function parseExtensionManifest(source) {
1420
+ const parsed = matter(source);
1421
+ if (Object.keys(parsed.data).length === 0) {
1422
+ throw new Error("parseExtensionManifest: missing or empty frontmatter");
1423
+ }
1424
+ const result = extensionFrontmatterSchema.safeParse(parsed.data);
1425
+ if (!result.success) {
1426
+ throw new Error(`parseExtensionManifest: invalid frontmatter \u2014 ${result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`);
1427
+ }
1428
+ return { frontmatter: result.data, body: parsed.content };
1429
+ }
1320
1430
 
1321
1431
  // ../mcp-server/dist/index.mjs
1322
1432
  async function createMcpServer(opts) {
@@ -1503,12 +1613,55 @@ async function writeRuntimeMeta(workspace, meta) {
1503
1613
  await writeFile(
1504
1614
  join(dir, "runtime.json"),
1505
1615
  JSON.stringify(meta, null, 2) + "\n",
1506
- "utf8"
1616
+ { encoding: "utf8", mode: 384 }
1507
1617
  );
1508
1618
  } catch (err) {
1509
1619
  console.error("[runtime] failed to write .agentproto/runtime.json:", err);
1510
1620
  }
1511
1621
  }
1622
+ async function unlinkRuntimeMeta(workspace) {
1623
+ try {
1624
+ await unlink(join(workspace, ".agentproto", "runtime.json"));
1625
+ } catch {
1626
+ }
1627
+ }
1628
+ async function readRuntimeMeta(workspace) {
1629
+ const path = join(workspace, ".agentproto", "runtime.json");
1630
+ try {
1631
+ const [raw, st] = await Promise.all([readFile(path, "utf8"), stat(path)]);
1632
+ const meta = JSON.parse(raw);
1633
+ return { meta, mtime: st.mtime };
1634
+ } catch {
1635
+ return null;
1636
+ }
1637
+ }
1638
+ async function sweepStaleRuntimeMetas(workspaces, currentWorkspace) {
1639
+ const cleaned = [];
1640
+ for (const ws of workspaces) {
1641
+ if (ws === currentWorkspace) continue;
1642
+ const meta = await readRuntimeMeta(ws);
1643
+ if (!meta) continue;
1644
+ const pid = meta.meta.pid;
1645
+ if (typeof pid === "number" && !isPidAlive(pid)) {
1646
+ try {
1647
+ await unlink(join(ws, ".agentproto", "runtime.json"));
1648
+ cleaned.push(join(ws, ".agentproto", "runtime.json"));
1649
+ } catch {
1650
+ }
1651
+ }
1652
+ }
1653
+ return cleaned;
1654
+ }
1655
+ function isPidAlive(pid) {
1656
+ try {
1657
+ process.kill(pid, 0);
1658
+ return true;
1659
+ } catch (err) {
1660
+ const code = err.code;
1661
+ if (code === "EPERM") return true;
1662
+ return false;
1663
+ }
1664
+ }
1512
1665
  var DEFAULT_TIMEOUT_MS = 6e4;
1513
1666
  var MAX_TIMEOUT_MS = 6e5;
1514
1667
  var ALLOWLIST_REL = ".agentproto/allowed-commands.json";
@@ -1937,21 +2090,21 @@ function registerFsTools(server, opts) {
1937
2090
  }
1938
2091
  );
1939
2092
  }
1940
- var WORKSPACES_CONFIG_VERSION = 1;
1941
- var DEFAULT_CONFIG_DIR = () => resolve(homedir(), ".agentproto");
1942
- var DEFAULT_CONFIG_PATH = () => resolve(DEFAULT_CONFIG_DIR(), "workspaces.json");
1943
- function sanitizeSlug(input2) {
2093
+ var WORKSPACES_CONFIG_VERSION2 = 1;
2094
+ var DEFAULT_CONFIG_DIR2 = () => resolve(homedir(), ".agentproto");
2095
+ var DEFAULT_CONFIG_PATH2 = () => resolve(DEFAULT_CONFIG_DIR2(), "workspaces.json");
2096
+ function sanitizeSlug2(input2) {
1944
2097
  const trimmed = input2.trim().toLowerCase();
1945
2098
  const cleaned = trimmed.replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
1946
2099
  return cleaned || "workspace";
1947
2100
  }
1948
- async function loadWorkspacesConfig(path = DEFAULT_CONFIG_PATH()) {
2101
+ async function loadWorkspacesConfig2(path = DEFAULT_CONFIG_PATH2()) {
1949
2102
  let raw;
1950
2103
  try {
1951
2104
  raw = await promises.readFile(path, "utf8");
1952
2105
  } catch (err) {
1953
2106
  if (err.code === "ENOENT") {
1954
- return { version: WORKSPACES_CONFIG_VERSION, workspaces: [] };
2107
+ return { version: WORKSPACES_CONFIG_VERSION2, workspaces: [] };
1955
2108
  }
1956
2109
  throw err;
1957
2110
  }
@@ -1963,18 +2116,18 @@ async function loadWorkspacesConfig(path = DEFAULT_CONFIG_PATH()) {
1963
2116
  `agentproto: ${path} is not valid JSON (${err instanceof Error ? err.message : String(err)}). Delete the file or fix it manually.`
1964
2117
  );
1965
2118
  }
1966
- return normaliseConfig(parsed);
2119
+ return normalizeConfig2(parsed);
1967
2120
  }
1968
2121
  function findWorkspace(config, slug) {
1969
- return config.workspaces.find((w) => w.slug === sanitizeSlug(slug));
2122
+ return config.workspaces.find((w) => w.slug === sanitizeSlug2(slug));
1970
2123
  }
1971
2124
  function getActiveWorkspace(config) {
1972
2125
  if (!config.active) return config.workspaces[0];
1973
2126
  return findWorkspace(config, config.active) ?? config.workspaces[0];
1974
2127
  }
1975
- function normaliseConfig(parsed) {
2128
+ function normalizeConfig2(parsed) {
1976
2129
  if (!parsed || typeof parsed !== "object") {
1977
- return { version: WORKSPACES_CONFIG_VERSION, workspaces: [] };
2130
+ return { version: WORKSPACES_CONFIG_VERSION2, workspaces: [] };
1978
2131
  }
1979
2132
  const obj = parsed;
1980
2133
  const workspaces = [];
@@ -1982,7 +2135,7 @@ function normaliseConfig(parsed) {
1982
2135
  for (const entry of obj.workspaces) {
1983
2136
  if (!entry || typeof entry !== "object") continue;
1984
2137
  const e = entry;
1985
- const slug = typeof e.slug === "string" ? sanitizeSlug(e.slug) : "";
2138
+ const slug = typeof e.slug === "string" ? sanitizeSlug2(e.slug) : "";
1986
2139
  const path = typeof e.path === "string" ? e.path : "";
1987
2140
  if (!slug || !path) continue;
1988
2141
  const addedAt = typeof e.addedAt === "string" ? e.addedAt : (/* @__PURE__ */ new Date()).toISOString();
@@ -1999,7 +2152,7 @@ function normaliseConfig(parsed) {
1999
2152
  const finalList = Array.from(dedup.values());
2000
2153
  const active = typeof obj.active === "string" && finalList.some((w) => w.slug === obj.active) ? obj.active : finalList[0]?.slug;
2001
2154
  const out = {
2002
- version: WORKSPACES_CONFIG_VERSION,
2155
+ version: WORKSPACES_CONFIG_VERSION2,
2003
2156
  workspaces: finalList
2004
2157
  };
2005
2158
  if (active !== void 0) out.active = active;
@@ -2125,7 +2278,7 @@ async function scanGoose(home, out, errors) {
2125
2278
  async function scanRegisteredWorkspaces(out, errors) {
2126
2279
  let workspaces = [];
2127
2280
  try {
2128
- const cfg = await loadWorkspacesConfig();
2281
+ const cfg = await loadWorkspacesConfig2();
2129
2282
  workspaces = cfg.workspaces;
2130
2283
  } catch (err) {
2131
2284
  errors.push(`workspaces: load failed \u2014 ${err.message}`);
@@ -2243,7 +2396,7 @@ async function loadImportedMcps(path = IMPORTED_MCPS_PATH()) {
2243
2396
  `agentproto: ${path} is not valid JSON (${err instanceof Error ? err.message : String(err)}). Delete or fix manually.`
2244
2397
  );
2245
2398
  }
2246
- return normalise(parsed);
2399
+ return normalize3(parsed);
2247
2400
  }
2248
2401
  async function saveImportedMcps(config, path = IMPORTED_MCPS_PATH()) {
2249
2402
  await promises.mkdir(dirname(path), { recursive: true });
@@ -2269,7 +2422,7 @@ function addImport(config, input2) {
2269
2422
  function removeImport(config, id) {
2270
2423
  return { ...config, imports: config.imports.filter((e) => e.id !== id) };
2271
2424
  }
2272
- function normalise(parsed) {
2425
+ function normalize3(parsed) {
2273
2426
  if (!parsed || typeof parsed !== "object") return EMPTY;
2274
2427
  const obj = parsed;
2275
2428
  const imports = [];
@@ -2289,6 +2442,7 @@ function normalise(parsed) {
2289
2442
  }
2290
2443
  function registerSessionTools(server, opts) {
2291
2444
  const { registry, resolveAgentAdapter, listAgentAdapters, mcpProxy } = opts;
2445
+ const ptyEnabled = opts.ptyEnabled === true;
2292
2446
  server.tool(
2293
2447
  "start_agent_session",
2294
2448
  "Spawn a long-running agent CLI (claude-code, hermes, \u2026) on the host. The session stays alive across multiple turns \u2014 call `prompt_agent_session` to continue the conversation. Returns the session id + initial descriptor. When `workspaceSlug` is set, resolves the cwd via `~/.agentproto/workspaces.json`; otherwise pass `cwd` explicitly or fall back to the active workspace.",
@@ -2325,7 +2479,7 @@ function registerSessionTools(server, opts) {
2325
2479
  let resolvedSlug = input2.workspaceSlug ?? "default";
2326
2480
  if (!cwd) {
2327
2481
  try {
2328
- const config = await loadWorkspacesConfig();
2482
+ const config = await loadWorkspacesConfig2();
2329
2483
  const ws = input2.workspaceSlug ? findWorkspace(config, input2.workspaceSlug) : getActiveWorkspace(config);
2330
2484
  if (ws) {
2331
2485
  cwd = ws.path;
@@ -2420,17 +2574,44 @@ function registerSessionTools(server, opts) {
2420
2574
  }
2421
2575
  }
2422
2576
  );
2577
+ server.tool(
2578
+ "list_sessions",
2579
+ "List sessions tracked by the daemon \u2014 agent-CLI sessions (claude-code, hermes, \u2026), terminal/PTY sessions (claude TUI, bash, \u2026), and raw commands. Each entry includes `kind`, `pty` (true for real PTYs), `name` (when set at spawn), `status`, `command`, age + exit code. Use this when you need to know what's already running before spawning anything new, or to discover a session id by name.",
2580
+ {
2581
+ kind: z.enum(["terminal", "agent-cli", "command", "all"]).optional().describe(
2582
+ "Filter by session kind. `all` (default) returns every kind. Use `terminal` to list only PTY sessions, `agent-cli` for structured ACP agents."
2583
+ ),
2584
+ onlyAlive: z.boolean().optional().describe("When true, only running/starting sessions. Default false."),
2585
+ status: z.enum(["starting", "running", "exited", "killed", "error"]).optional().describe("Filter by exact status (overrides onlyAlive).")
2586
+ },
2587
+ async (input2) => {
2588
+ let rows = registry.list();
2589
+ if (input2.kind && input2.kind !== "all") {
2590
+ rows = rows.filter((s) => s.kind === input2.kind);
2591
+ }
2592
+ if (input2.status) {
2593
+ rows = rows.filter((s) => s.status === input2.status);
2594
+ } else if (input2.onlyAlive) {
2595
+ rows = rows.filter(
2596
+ (s) => s.status === "running" || s.status === "starting"
2597
+ );
2598
+ }
2599
+ return {
2600
+ content: [
2601
+ { type: "text", text: JSON.stringify({ sessions: rows }, null, 2) }
2602
+ ]
2603
+ };
2604
+ }
2605
+ );
2423
2606
  server.tool(
2424
2607
  "list_agent_sessions",
2425
- "List all sessions tracked by the daemon \u2014 running, exited, killed. Use to find a session id when the operator's previous start call returned long ago, or to tell the user what's currently active on their machine.",
2608
+ "DEPRECATED \u2014 prefer `list_sessions` which returns ALL kinds + filters. Despite the name this tool already returns every kind, not just agent-cli sessions; the new tool's name reflects the actual surface.",
2426
2609
  {
2427
- onlyAlive: z.boolean().optional().describe("Filter to status running/starting/mounting only. Default false.")
2610
+ onlyAlive: z.boolean().optional().describe("Filter to status running/starting only. Default false.")
2428
2611
  },
2429
2612
  async (input2) => {
2430
2613
  const all = registry.list();
2431
- const filtered = input2.onlyAlive ? all.filter(
2432
- (s) => s.status === "running" || s.status === "starting" || s.status === "mounting"
2433
- ) : all;
2614
+ const filtered = input2.onlyAlive ? all.filter((s) => s.status === "running" || s.status === "starting") : all;
2434
2615
  return {
2435
2616
  content: [
2436
2617
  {
@@ -2792,69 +2973,269 @@ function registerSessionTools(server, opts) {
2792
2973
  };
2793
2974
  }
2794
2975
  );
2795
- }
2796
- function startHeartbeat(opts) {
2797
- const filename = opts.filename ?? "HEARTBEAT.md";
2798
- const path = join(opts.workspace, filename);
2799
- const now = opts.now ?? (() => /* @__PURE__ */ new Date());
2800
- let timer = null;
2801
- let currentEveryMs = null;
2802
- let firing = false;
2803
- async function load() {
2804
- if (!existsSync(path)) return null;
2805
- let source;
2806
- try {
2807
- source = await readFile(path, "utf8");
2808
- } catch {
2809
- return null;
2810
- }
2811
- const parsed = matter(source);
2812
- const fm = parsed.data;
2813
- const agent = typeof fm.agent === "string" ? fm.agent : null;
2814
- if (!agent) return null;
2815
- const enabled = fm.enabled === void 0 ? true : Boolean(fm.enabled);
2816
- const everyRaw = typeof fm.every === "string" ? fm.every : "60s";
2817
- const everyMs = parseDuration(everyRaw);
2818
- if (!everyMs) return null;
2819
- const conversationTemplate = typeof fm.conversation === "string" ? fm.conversation : "heartbeat-{date}";
2820
- return {
2821
- agent,
2822
- everyMs,
2823
- enabled,
2824
- conversationTemplate,
2825
- body: parsed.content.trim()
2826
- };
2827
- }
2828
- async function tick() {
2829
- if (firing) return;
2830
- if (opts.shouldFire && !opts.shouldFire()) return;
2831
- firing = true;
2832
- let agentId;
2833
- try {
2834
- const hb = await load();
2835
- if (!hb || !hb.enabled) return;
2836
- agentId = hb.agent;
2837
- const agent = await opts.buildAgent(hb.agent);
2838
- if (!agent) {
2839
- opts.events.emit({
2840
- type: "heartbeat-error",
2841
- at: now().toISOString(),
2842
- agent: hb.agent,
2843
- error: `buildAgent('${hb.agent}') returned null`
2844
- });
2845
- return;
2976
+ const ptyNotConfigured = (toolName) => ({
2977
+ content: [
2978
+ {
2979
+ type: "text",
2980
+ text: `${toolName}: PTY support not enabled \u2014 the daemon was started without a node-pty factory. Re-run \`agentproto serve\` from a build that ships node-pty (the optional dep ships with @agentproto/cli).`
2846
2981
  }
2847
- if (!hb.body) {
2848
- opts.events.emit({
2849
- type: "heartbeat-error",
2850
- at: now().toISOString(),
2851
- agent: hb.agent,
2852
- error: "HEARTBEAT.md body is empty"
2853
- });
2854
- return;
2982
+ ],
2983
+ isError: true
2984
+ });
2985
+ server.tool(
2986
+ "start_terminal_session",
2987
+ "Spawn a process under a real PTY (node-pty) on the host. Bytes (including ANSI escapes, alt-screen sequences) flow through the daemon's byte ring buffer; subscribers attach via the WS at /sessions/:id/pty. Use for interactive TUIs (claude, vim, htop) or to orchestrate shells from another agent. Returns the session descriptor.",
2988
+ {
2989
+ argv: z.array(z.string()).min(1).describe(
2990
+ "Argv array. First element is the binary, rest are arguments. e.g. ['claude'] or ['bash', '-l']."
2991
+ ),
2992
+ workspaceSlug: z.string().optional().describe(
2993
+ "Workspace slug from `agentproto workspace list`. Resolves cwd. Omit to use `cwd` explicitly or the active workspace."
2994
+ ),
2995
+ cwd: z.string().optional().describe("Absolute cwd. Wins over workspaceSlug when both set."),
2996
+ cols: z.number().int().min(1).max(500).optional().describe("Initial cols. Default 80."),
2997
+ rows: z.number().int().min(1).max(200).optional().describe("Initial rows. Default 24."),
2998
+ name: z.string().optional().describe(
2999
+ "User-friendly slug. Becomes an alias for the session id in subsequent tool calls (read/write/kill accept either)."
3000
+ ),
3001
+ label: z.string().optional().describe(
3002
+ "Free-text label surfaced in list_agent_sessions and the UI."
3003
+ )
3004
+ },
3005
+ async (input2) => {
3006
+ if (!ptyEnabled) return ptyNotConfigured("start_terminal_session");
3007
+ let cwd = input2.cwd;
3008
+ let resolvedSlug = input2.workspaceSlug ?? "default";
3009
+ if (!cwd) {
3010
+ try {
3011
+ const config = await loadWorkspacesConfig2();
3012
+ const ws = input2.workspaceSlug ? findWorkspace(config, input2.workspaceSlug) : getActiveWorkspace(config);
3013
+ if (ws) {
3014
+ cwd = ws.path;
3015
+ resolvedSlug = ws.slug;
3016
+ }
3017
+ } catch {
3018
+ }
2855
3019
  }
2856
- const conversationId = expandTemplate(hb.conversationTemplate, now());
2857
- await opts.conversations.open(conversationId, { agent: hb.agent });
3020
+ if (!cwd) {
3021
+ return {
3022
+ content: [
3023
+ {
3024
+ type: "text",
3025
+ text: "start_terminal_session: no cwd resolvable. Pass `cwd` explicitly or `workspaceSlug` matching `agentproto workspace list`."
3026
+ }
3027
+ ],
3028
+ isError: true
3029
+ };
3030
+ }
3031
+ try {
3032
+ const desc = registry.spawnPty({
3033
+ argv: input2.argv,
3034
+ cwd,
3035
+ workspaceSlug: resolvedSlug,
3036
+ cols: input2.cols ?? 80,
3037
+ rows: input2.rows ?? 24,
3038
+ ...input2.name ? { name: input2.name } : {},
3039
+ ...input2.label ? { label: input2.label } : {}
3040
+ });
3041
+ return {
3042
+ content: [{ type: "text", text: JSON.stringify(desc, null, 2) }]
3043
+ };
3044
+ } catch (err) {
3045
+ return {
3046
+ content: [
3047
+ {
3048
+ type: "text",
3049
+ text: `start_terminal_session: ${err instanceof Error ? err.message : String(err)}`
3050
+ }
3051
+ ],
3052
+ isError: true
3053
+ };
3054
+ }
3055
+ }
3056
+ );
3057
+ server.tool(
3058
+ "write_terminal_input",
3059
+ "Send keystrokes to a PTY session's stdin. The text is forwarded verbatim \u2014 include trailing newlines if the target needs them (e.g. shell commands). Use after `start_terminal_session` to drive an interactive CLI.",
3060
+ {
3061
+ sessionId: z.string().describe("Session id OR name from start_terminal_session."),
3062
+ text: z.string().describe("Text to write. Sent as-is to the PTY's stdin.")
3063
+ },
3064
+ async (input2) => {
3065
+ if (!ptyEnabled) return ptyNotConfigured("write_terminal_input");
3066
+ const desc = registry.findByIdOrName(input2.sessionId);
3067
+ if (!desc) {
3068
+ return {
3069
+ content: [
3070
+ {
3071
+ type: "text",
3072
+ text: `write_terminal_input: no session "${input2.sessionId}"`
3073
+ }
3074
+ ],
3075
+ isError: true
3076
+ };
3077
+ }
3078
+ const ok = registry.writeTerminalInput(desc.id, input2.text);
3079
+ return {
3080
+ content: [
3081
+ {
3082
+ type: "text",
3083
+ text: JSON.stringify({ ok, sessionId: desc.id }, null, 2)
3084
+ }
3085
+ ],
3086
+ ...ok ? {} : { isError: true }
3087
+ };
3088
+ }
3089
+ );
3090
+ server.tool(
3091
+ "read_terminal_output",
3092
+ "Snapshot the recent byte buffer of a PTY session. Returns base64-encoded bytes (the buffer is RAW including ANSI escapes \u2014 strip with a regex if you want plain text). `lastBytes` caps the read from the tail.",
3093
+ {
3094
+ sessionId: z.string().describe("Session id OR name from start_terminal_session."),
3095
+ lastBytes: z.number().int().min(1).max(64 * 1024).optional().describe("Max bytes from the tail. Default: full ring buffer (~64 KiB).")
3096
+ },
3097
+ async (input2) => {
3098
+ if (!ptyEnabled) return ptyNotConfigured("read_terminal_output");
3099
+ const desc = registry.findByIdOrName(input2.sessionId);
3100
+ if (!desc) {
3101
+ return {
3102
+ content: [
3103
+ {
3104
+ type: "text",
3105
+ text: `read_terminal_output: no session "${input2.sessionId}"`
3106
+ }
3107
+ ],
3108
+ isError: true
3109
+ };
3110
+ }
3111
+ const buf = registry.readTerminalOutput(
3112
+ desc.id,
3113
+ input2.lastBytes
3114
+ );
3115
+ if (!buf) {
3116
+ return {
3117
+ content: [
3118
+ {
3119
+ type: "text",
3120
+ text: `read_terminal_output: session "${desc.id}" is not a PTY`
3121
+ }
3122
+ ],
3123
+ isError: true
3124
+ };
3125
+ }
3126
+ return {
3127
+ content: [
3128
+ {
3129
+ type: "text",
3130
+ text: JSON.stringify(
3131
+ {
3132
+ sessionId: desc.id,
3133
+ status: desc.status,
3134
+ bytes: buf.byteLength,
3135
+ b64: buf.toString("base64")
3136
+ },
3137
+ null,
3138
+ 2
3139
+ )
3140
+ }
3141
+ ]
3142
+ };
3143
+ }
3144
+ );
3145
+ server.tool(
3146
+ "kill_terminal_session",
3147
+ "SIGTERM a PTY session and drop it from the alive set. Same effect as `kill_agent_session` for the PTY family \u2014 separate name so it's obvious what's being stopped.",
3148
+ {
3149
+ sessionId: z.string().describe("Session id OR name from start_terminal_session.")
3150
+ },
3151
+ async (input2) => {
3152
+ if (!ptyEnabled) return ptyNotConfigured("kill_terminal_session");
3153
+ const desc = registry.findByIdOrName(input2.sessionId);
3154
+ if (!desc) {
3155
+ return {
3156
+ content: [
3157
+ {
3158
+ type: "text",
3159
+ text: `kill_terminal_session: no session "${input2.sessionId}"`
3160
+ }
3161
+ ],
3162
+ isError: true
3163
+ };
3164
+ }
3165
+ const ok = registry.kill(desc.id);
3166
+ return {
3167
+ content: [
3168
+ {
3169
+ type: "text",
3170
+ text: JSON.stringify({ ok, sessionId: desc.id }, null, 2)
3171
+ }
3172
+ ]
3173
+ };
3174
+ }
3175
+ );
3176
+ }
3177
+ function startHeartbeat(opts) {
3178
+ const filename = opts.filename ?? "HEARTBEAT.md";
3179
+ const path = join(opts.workspace, filename);
3180
+ const now = opts.now ?? (() => /* @__PURE__ */ new Date());
3181
+ let timer = null;
3182
+ let currentEveryMs = null;
3183
+ let firing = false;
3184
+ async function load() {
3185
+ if (!existsSync(path)) return null;
3186
+ let source;
3187
+ try {
3188
+ source = await readFile(path, "utf8");
3189
+ } catch {
3190
+ return null;
3191
+ }
3192
+ const parsed = matter(source);
3193
+ const fm = parsed.data;
3194
+ const agent = typeof fm.agent === "string" ? fm.agent : null;
3195
+ if (!agent) return null;
3196
+ const enabled = fm.enabled === void 0 ? true : Boolean(fm.enabled);
3197
+ const everyRaw = typeof fm.every === "string" ? fm.every : "60s";
3198
+ const everyMs = parseDuration(everyRaw);
3199
+ if (!everyMs) return null;
3200
+ const conversationTemplate = typeof fm.conversation === "string" ? fm.conversation : "heartbeat-{date}";
3201
+ return {
3202
+ agent,
3203
+ everyMs,
3204
+ enabled,
3205
+ conversationTemplate,
3206
+ body: parsed.content.trim()
3207
+ };
3208
+ }
3209
+ async function tick() {
3210
+ if (firing) return;
3211
+ if (opts.shouldFire && !opts.shouldFire()) return;
3212
+ firing = true;
3213
+ let agentId;
3214
+ try {
3215
+ const hb = await load();
3216
+ if (!hb || !hb.enabled) return;
3217
+ agentId = hb.agent;
3218
+ const agent = await opts.buildAgent(hb.agent);
3219
+ if (!agent) {
3220
+ opts.events.emit({
3221
+ type: "heartbeat-error",
3222
+ at: now().toISOString(),
3223
+ agent: hb.agent,
3224
+ error: `buildAgent('${hb.agent}') returned null`
3225
+ });
3226
+ return;
3227
+ }
3228
+ if (!hb.body) {
3229
+ opts.events.emit({
3230
+ type: "heartbeat-error",
3231
+ at: now().toISOString(),
3232
+ agent: hb.agent,
3233
+ error: "HEARTBEAT.md body is empty"
3234
+ });
3235
+ return;
3236
+ }
3237
+ const conversationId = expandTemplate(hb.conversationTemplate, now());
3238
+ await opts.conversations.open(conversationId, { agent: hb.agent });
2858
3239
  const startedAt = now();
2859
3240
  await opts.conversations.appendTurn(
2860
3241
  conversationId,
@@ -2964,6 +3345,11 @@ function preview(text3, max = 120) {
2964
3345
  const flat = text3.replace(/\s+/g, " ").trim();
2965
3346
  return flat.length > max ? `${flat.slice(0, max - 1)}\u2026` : flat;
2966
3347
  }
3348
+ var DEFAULT_ALLOWED_ORIGINS = [
3349
+ "http://localhost:*",
3350
+ "http://127.0.0.1:*",
3351
+ "https://localhost:*"
3352
+ ];
2967
3353
  async function startHttpServer(opts) {
2968
3354
  const startedAt = Date.now();
2969
3355
  const authSource = opts.auth ?? { mode: "none" };
@@ -2988,15 +3374,69 @@ async function startHttpServer(opts) {
2988
3374
  }
2989
3375
  return true;
2990
3376
  }
3377
+ function checkSessionsToken(req) {
3378
+ if (!opts.token) return "ok";
3379
+ const header = req.headers.authorization;
3380
+ const expected = `Bearer ${opts.token}`;
3381
+ if (header === expected) return "ok";
3382
+ const urlStr = req.url ?? "";
3383
+ if (urlStr.includes("?")) {
3384
+ const qs = new URLSearchParams(urlStr.slice(urlStr.indexOf("?") + 1));
3385
+ const qsToken = qs.get("token");
3386
+ if (qsToken && qsToken === opts.token) return "ok";
3387
+ }
3388
+ const origin = req.headers.origin;
3389
+ if (typeof origin === "string" && originAllowed(origin)) return "ok";
3390
+ return header ? "bad" : "missing";
3391
+ }
3392
+ function originAllowed(origin) {
3393
+ const list = opts.strictOrigins ? opts.allowedOrigins ?? [] : [...DEFAULT_ALLOWED_ORIGINS, ...opts.allowedOrigins ?? []];
3394
+ for (const pattern of list) {
3395
+ if (pattern === origin) return true;
3396
+ if (pattern.endsWith(":*")) {
3397
+ const prefix = pattern.slice(0, -2);
3398
+ if (origin === prefix || origin.startsWith(prefix + ":")) return true;
3399
+ }
3400
+ }
3401
+ return false;
3402
+ }
3403
+ function rejectUnauthorizedSession(req, res, kind) {
3404
+ const origin = req.headers.origin ?? null;
3405
+ const allowList = opts.strictOrigins ? opts.allowedOrigins ?? [] : [...DEFAULT_ALLOWED_ORIGINS, ...opts.allowedOrigins ?? []];
3406
+ res.writeHead(401, { "content-type": "application/json" });
3407
+ const message = kind === "missing" ? origin ? `Origin "${origin}" not in the daemon's allowlist, and no Bearer token sent. Either add it (\`agentproto config set daemon.allowedOrigins ${origin}\` then restart the daemon), or send Authorization: Bearer <token> read from <workspace>/.agentproto/runtime.json.` : `Authorization: Bearer <token> required on mutating /sessions/* routes. Read the token from <workspace>/.agentproto/runtime.json. (Browser callers can use an Origin in the allowlist instead.)` : `Invalid bearer token. The daemon regenerated its token on its last boot \u2014 re-read <workspace>/.agentproto/runtime.json.`;
3408
+ res.end(
3409
+ JSON.stringify({
3410
+ error: "sessions_unauthorized",
3411
+ message,
3412
+ // Debug data so a UI / network-tab inspection points the user
3413
+ // straight at the fix without needing the daemon's stderr.
3414
+ rejectedOrigin: origin,
3415
+ allowedOrigins: allowList,
3416
+ // We never echo the token; only the absence-or-presence flag.
3417
+ receivedAuthHeader: typeof req.headers.authorization === "string"
3418
+ })
3419
+ );
3420
+ }
3421
+ function isMutatingSessionsRoute(method, path) {
3422
+ if (!path.startsWith("/sessions")) return false;
3423
+ if (method === "POST" || method === "DELETE" || method === "PUT" || method === "PATCH") {
3424
+ return true;
3425
+ }
3426
+ return false;
3427
+ }
2991
3428
  async function handleMcp(req, res) {
2992
3429
  if (!authorize(req, res)) return;
2993
3430
  const server2 = await opts.mcpServerFactory();
2994
3431
  const transport = new StreamableHTTPServerTransport({
2995
3432
  sessionIdGenerator: void 0
2996
3433
  });
2997
- transport.onerror = (err) => {
2998
- console.error("[mcp transport.onerror]", err);
2999
- };
3434
+ const transportInternal = transport;
3435
+ if ("onerror" in transport) {
3436
+ transportInternal["onerror"] = (err) => {
3437
+ console.error("[mcp transport.onerror]", err);
3438
+ };
3439
+ }
3000
3440
  res.on("close", () => {
3001
3441
  void transport.close();
3002
3442
  void server2.close();
@@ -3073,6 +3513,77 @@ async function startHttpServer(opts) {
3073
3513
  res.writeHead(202, { "content-type": "application/json" });
3074
3514
  res.end(JSON.stringify({ status: "fired" }));
3075
3515
  }
3516
+ async function handleFileUpload(req, res, url) {
3517
+ const reply = (status, body) => {
3518
+ res.writeHead(status, { "content-type": "application/json" });
3519
+ res.end(JSON.stringify(body));
3520
+ };
3521
+ const qs = new URLSearchParams(url.includes("?") ? url.slice(url.indexOf("?") + 1) : "");
3522
+ const cwd = qs.get("cwd");
3523
+ const rawName = qs.get("name");
3524
+ if (!cwd || !rawName) {
3525
+ reply(400, { error: "missing_cwd_or_name" });
3526
+ return;
3527
+ }
3528
+ if (!isAbsolute(cwd)) {
3529
+ reply(400, { error: "cwd_not_absolute" });
3530
+ return;
3531
+ }
3532
+ const safeName = rawName.replace(/[/\\]/g, "_").replace(/\.\.+/g, ".").replace(/^\.+/, "").slice(0, 200);
3533
+ if (safeName.length === 0) {
3534
+ reply(400, { error: "invalid_name" });
3535
+ return;
3536
+ }
3537
+ try {
3538
+ const st = await stat(cwd);
3539
+ if (!st.isDirectory()) {
3540
+ reply(400, { error: "cwd_not_a_directory" });
3541
+ return;
3542
+ }
3543
+ } catch {
3544
+ reply(400, { error: "cwd_not_found" });
3545
+ return;
3546
+ }
3547
+ const dir = join(cwd, ".agentproto-attachments");
3548
+ const target = resolve(dir, safeName);
3549
+ if (!target.startsWith(resolve(dir) + (dir.endsWith("/") ? "" : "/"))) {
3550
+ reply(400, { error: "path_traversal_blocked" });
3551
+ return;
3552
+ }
3553
+ const MAX_BYTES = 32 * 1024 * 1024;
3554
+ const chunks = [];
3555
+ let total = 0;
3556
+ let aborted = false;
3557
+ await new Promise((resolveBody, rejectBody) => {
3558
+ req.on("data", (chunk) => {
3559
+ if (aborted) return;
3560
+ const buf = chunk;
3561
+ total += buf.length;
3562
+ if (total > MAX_BYTES) {
3563
+ aborted = true;
3564
+ reply(413, { error: "file_too_large", maxBytes: MAX_BYTES });
3565
+ rejectBody(new Error("too_large"));
3566
+ return;
3567
+ }
3568
+ chunks.push(buf);
3569
+ });
3570
+ req.on("end", () => {
3571
+ if (!aborted) resolveBody();
3572
+ });
3573
+ req.on("error", (err) => rejectBody(err));
3574
+ }).catch(() => void 0);
3575
+ if (aborted) return;
3576
+ try {
3577
+ await mkdir(dir, { recursive: true });
3578
+ await writeFile(target, Buffer.concat(chunks));
3579
+ reply(200, { path: target, bytes: total });
3580
+ } catch (err) {
3581
+ reply(500, {
3582
+ error: "write_failed",
3583
+ message: err instanceof Error ? err.message : String(err)
3584
+ });
3585
+ }
3586
+ }
3076
3587
  function applyCors(req, res) {
3077
3588
  const origin = req.headers.origin ?? "*";
3078
3589
  res.setHeader("Access-Control-Allow-Origin", origin);
@@ -3086,6 +3597,9 @@ async function startHttpServer(opts) {
3086
3597
  "Access-Control-Allow-Methods",
3087
3598
  "GET,POST,PUT,PATCH,DELETE,OPTIONS"
3088
3599
  );
3600
+ if (req.headers["access-control-request-private-network"] === "true") {
3601
+ res.setHeader("Access-Control-Allow-Private-Network", "true");
3602
+ }
3089
3603
  }
3090
3604
  const server = createServer((req, res) => {
3091
3605
  void (async () => {
@@ -3130,19 +3644,36 @@ async function startHttpServer(opts) {
3130
3644
  await handleHeartbeatTick(req, res);
3131
3645
  return;
3132
3646
  }
3647
+ if (path === "/files/upload" && req.method === "POST") {
3648
+ const gate = checkSessionsToken(req);
3649
+ if (gate !== "ok") {
3650
+ rejectUnauthorizedSession(req, res, gate);
3651
+ return;
3652
+ }
3653
+ await handleFileUpload(req, res, url);
3654
+ return;
3655
+ }
3133
3656
  if (opts.sessions && path.startsWith("/sessions")) {
3657
+ if (isMutatingSessionsRoute(req.method ?? "GET", path)) {
3658
+ const gate = checkSessionsToken(req);
3659
+ if (gate !== "ok") {
3660
+ rejectUnauthorizedSession(req, res, gate);
3661
+ return;
3662
+ }
3663
+ }
3134
3664
  const handled = await handleSessions(
3135
3665
  req,
3136
3666
  res,
3137
3667
  path,
3138
3668
  opts.sessions,
3139
- opts.resolveAgentAdapter
3669
+ opts.resolveAgentAdapter,
3670
+ opts.ptyEnabled === true
3140
3671
  );
3141
3672
  if (handled) return;
3142
3673
  }
3143
3674
  if (path === "/workspaces" && req.method === "GET") {
3144
3675
  try {
3145
- const config = await loadWorkspacesConfig();
3676
+ const config = await loadWorkspacesConfig2();
3146
3677
  res.writeHead(200, { "content-type": "application/json" });
3147
3678
  res.end(JSON.stringify(config));
3148
3679
  } catch (err) {
@@ -3326,19 +3857,179 @@ async function startHttpServer(opts) {
3326
3857
  }
3327
3858
  })();
3328
3859
  });
3860
+ const wss = new WebSocketServer({ noServer: true });
3861
+ server.on("upgrade", (req, socket, head) => {
3862
+ const url = req.url ?? "/";
3863
+ const path = url.split("?")[0] ?? "/";
3864
+ const ptyMatch = path.match(/^\/sessions\/([^/]+)\/pty$/);
3865
+ if (!ptyMatch) {
3866
+ socket.destroy();
3867
+ return;
3868
+ }
3869
+ if (!opts.sessions || opts.ptyEnabled !== true) {
3870
+ rejectUpgrade(socket, 501, "pty_not_configured");
3871
+ return;
3872
+ }
3873
+ const gate = checkSessionsToken(req);
3874
+ if (gate !== "ok") {
3875
+ rejectUpgrade(socket, 401, gate === "missing" ? "missing_token" : "bad_token");
3876
+ return;
3877
+ }
3878
+ const id = ptyMatch[1];
3879
+ if (!id) {
3880
+ rejectUpgrade(socket, 400, "missing_id");
3881
+ return;
3882
+ }
3883
+ const desc = opts.sessions.findByIdOrName(id);
3884
+ if (!desc) {
3885
+ rejectUpgrade(socket, 404, "session_not_found");
3886
+ return;
3887
+ }
3888
+ if (desc.pty !== true) {
3889
+ rejectUpgrade(socket, 400, "session_not_pty");
3890
+ return;
3891
+ }
3892
+ if (desc.status === "exited" || desc.status === "killed" || desc.status === "error") {
3893
+ rejectUpgrade(
3894
+ socket,
3895
+ 410,
3896
+ `session_${desc.status}`,
3897
+ `Session ${desc.id}${desc.name ? ` (${desc.name})` : ""} has ${desc.status}. Spawn a fresh one with: agentproto sessions restart ${desc.name ?? desc.id}`
3898
+ );
3899
+ return;
3900
+ }
3901
+ wss.handleUpgrade(req, socket, head, (ws) => {
3902
+ handlePtyWebSocket(ws, req, desc.id, opts.sessions);
3903
+ });
3904
+ });
3329
3905
  const bind = opts.bind ?? "127.0.0.1";
3330
- await new Promise((resolve7, reject) => {
3906
+ await new Promise((resolve8, reject) => {
3331
3907
  server.once("error", reject);
3332
- server.listen(opts.port, bind, () => resolve7());
3908
+ server.listen(opts.port, bind, () => resolve8());
3333
3909
  });
3334
3910
  return {
3335
3911
  url: `http://${bind}:${opts.port}`,
3336
3912
  async stop() {
3337
- await new Promise((resolve7) => server.close(() => resolve7()));
3913
+ wss.close();
3914
+ await new Promise((resolve8) => server.close(() => resolve8()));
3338
3915
  }
3339
3916
  };
3340
3917
  }
3341
- async function handleSessions(req, res, path, registry, resolveAgentAdapter) {
3918
+ function rejectUpgrade(socket, status, reason, message) {
3919
+ const reasonText = `${status} ${reason}`;
3920
+ const body = { error: reason };
3921
+ if (message) body.message = message;
3922
+ try {
3923
+ socket.write(
3924
+ `HTTP/1.1 ${reasonText}\r
3925
+ Content-Type: application/json\r
3926
+ Connection: close\r
3927
+ \r
3928
+ ` + JSON.stringify(body) + `
3929
+ `
3930
+ );
3931
+ } catch {
3932
+ }
3933
+ socket.destroy();
3934
+ }
3935
+ function handlePtyWebSocket(ws, req, sessionId, registry) {
3936
+ const query = new URLSearchParams(
3937
+ (req.url ?? "").includes("?") ? (req.url ?? "").split("?")[1] : ""
3938
+ );
3939
+ const cols = clampPositiveInt(query.get("cols"), 80);
3940
+ const rows = clampPositiveInt(query.get("rows"), 24);
3941
+ const BACKPRESSURE_BYTES = 256 * 1024;
3942
+ const handle = registry.attachPty(
3943
+ sessionId,
3944
+ { cols, rows },
3945
+ (chunk) => {
3946
+ if (ws.readyState !== ws.OPEN) return;
3947
+ if (ws.bufferedAmount > BACKPRESSURE_BYTES) return;
3948
+ ws.send(
3949
+ JSON.stringify({ kind: "data", b64: chunk.toString("base64") })
3950
+ );
3951
+ },
3952
+ (evt) => {
3953
+ if (ws.readyState !== ws.OPEN) return;
3954
+ ws.send(
3955
+ JSON.stringify({
3956
+ kind: "exit",
3957
+ exitCode: evt.exitCode,
3958
+ ...evt.signal != null ? { signal: evt.signal } : {}
3959
+ })
3960
+ );
3961
+ try {
3962
+ ws.close(1e3, "session exited");
3963
+ } catch {
3964
+ }
3965
+ }
3966
+ );
3967
+ if (!handle) {
3968
+ try {
3969
+ ws.close(1011, "session not attachable");
3970
+ } catch {
3971
+ }
3972
+ return;
3973
+ }
3974
+ ws.on("message", (raw, isBinary) => {
3975
+ if (isBinary) {
3976
+ return;
3977
+ }
3978
+ let frame;
3979
+ try {
3980
+ frame = JSON.parse(raw.toString("utf8"));
3981
+ } catch {
3982
+ return;
3983
+ }
3984
+ if (!frame || typeof frame !== "object") return;
3985
+ const f = frame;
3986
+ switch (f.kind) {
3987
+ case "input": {
3988
+ if (typeof f.text === "string") {
3989
+ handle.write(f.text);
3990
+ } else if (typeof f.b64 === "string") {
3991
+ try {
3992
+ handle.write(Buffer.from(f.b64, "base64").toString("utf8"));
3993
+ } catch {
3994
+ }
3995
+ }
3996
+ break;
3997
+ }
3998
+ case "resize": {
3999
+ const c = typeof f.cols === "number" && f.cols > 0 ? Math.floor(f.cols) : null;
4000
+ const r = typeof f.rows === "number" && f.rows > 0 ? Math.floor(f.rows) : null;
4001
+ if (c && r) handle.resize(c, r);
4002
+ break;
4003
+ }
4004
+ case "ping":
4005
+ try {
4006
+ ws.send(JSON.stringify({ kind: "pong" }));
4007
+ } catch {
4008
+ }
4009
+ break;
4010
+ }
4011
+ });
4012
+ ws.on("close", () => {
4013
+ handle.detach();
4014
+ });
4015
+ ws.on("error", () => {
4016
+ handle.detach();
4017
+ });
4018
+ }
4019
+ function clampPositiveInt(raw, fallback) {
4020
+ if (!raw) return fallback;
4021
+ const n = Number.parseInt(raw, 10);
4022
+ return Number.isFinite(n) && n > 0 ? n : fallback;
4023
+ }
4024
+ function clampInt(raw, fallback, min, max) {
4025
+ if (!raw) return fallback;
4026
+ const n = Number.parseInt(raw, 10);
4027
+ if (!Number.isFinite(n)) return fallback;
4028
+ if (n < min) return min;
4029
+ if (n > max) return max;
4030
+ return n;
4031
+ }
4032
+ async function handleSessions(req, res, path, registry, resolveAgentAdapter, ptyEnabled = false) {
3342
4033
  const json = (status, body) => {
3343
4034
  res.writeHead(status, { "content-type": "application/json" });
3344
4035
  res.end(JSON.stringify(body));
@@ -3370,7 +4061,7 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter) {
3370
4061
  let workspaceSlug = typeof b.workspaceSlug === "string" ? b.workspaceSlug : "";
3371
4062
  if (!cwd) {
3372
4063
  try {
3373
- const config = await loadWorkspacesConfig();
4064
+ const config = await loadWorkspacesConfig2();
3374
4065
  const ws = workspaceSlug ? findWorkspace(config, workspaceSlug) : getActiveWorkspace(config);
3375
4066
  if (ws) {
3376
4067
  cwd = ws.path;
@@ -3392,7 +4083,11 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter) {
3392
4083
  return true;
3393
4084
  }
3394
4085
  try {
3395
- const agentSession = await resolved.startSession({ cwd });
4086
+ const resumeSessionId = typeof b.resumeSessionId === "string" && b.resumeSessionId.length > 0 ? b.resumeSessionId : void 0;
4087
+ const agentSession = await resolved.startSession({
4088
+ cwd,
4089
+ ...resumeSessionId ? { resumeSessionId } : {}
4090
+ });
3396
4091
  const desc = registry.spawnAgent({
3397
4092
  workspaceSlug,
3398
4093
  cwd,
@@ -3411,14 +4106,78 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter) {
3411
4106
  }
3412
4107
  return true;
3413
4108
  }
4109
+ if (path === "/sessions/terminal" && req.method === "POST") {
4110
+ if (!ptyEnabled) {
4111
+ json(501, {
4112
+ error: "pty_not_configured",
4113
+ message: "POST /sessions/terminal needs the host to inject `spawnPty` into createGateway (node-pty optional dep \u2014 install in @agentproto/cli)."
4114
+ });
4115
+ return true;
4116
+ }
4117
+ const body = await readJsonBody(req);
4118
+ if (!body || typeof body !== "object") {
4119
+ json(400, { error: "invalid_body" });
4120
+ return true;
4121
+ }
4122
+ const b = body;
4123
+ const argv = Array.isArray(b.argv) ? b.argv.filter((v) => typeof v === "string") : [];
4124
+ if (argv.length === 0) {
4125
+ json(400, { error: "missing_argv" });
4126
+ return true;
4127
+ }
4128
+ const cols = typeof b.cols === "number" && b.cols > 0 ? Math.floor(b.cols) : 80;
4129
+ const rows = typeof b.rows === "number" && b.rows > 0 ? Math.floor(b.rows) : 24;
4130
+ let cwd = typeof b.cwd === "string" && b.cwd.length > 0 ? b.cwd : null;
4131
+ let workspaceSlug = typeof b.workspaceSlug === "string" ? b.workspaceSlug : "";
4132
+ if (!cwd) {
4133
+ try {
4134
+ const config = await loadWorkspacesConfig2();
4135
+ const ws = workspaceSlug ? findWorkspace(config, workspaceSlug) : getActiveWorkspace(config);
4136
+ if (ws) {
4137
+ cwd = ws.path;
4138
+ workspaceSlug = ws.slug;
4139
+ }
4140
+ } catch {
4141
+ }
4142
+ }
4143
+ if (!cwd) {
4144
+ cwd = process.cwd();
4145
+ console.warn(
4146
+ `[/sessions/terminal] no cwd resolvable for argv=${argv.join(" ")} \u2014 falling back to daemon's cwd ${cwd}`
4147
+ );
4148
+ }
4149
+ if (!workspaceSlug) workspaceSlug = "default";
4150
+ try {
4151
+ const desc = registry.spawnPty({
4152
+ argv,
4153
+ cwd,
4154
+ workspaceSlug,
4155
+ cols,
4156
+ rows,
4157
+ ...b.env && typeof b.env === "object" ? { env: b.env } : {},
4158
+ ...typeof b.name === "string" ? { name: b.name } : {},
4159
+ ...typeof b.label === "string" ? { label: b.label } : {}
4160
+ });
4161
+ json(201, desc);
4162
+ } catch (err) {
4163
+ const msg = err instanceof Error ? err.message : String(err);
4164
+ const status = msg.includes("already in use") ? 409 : msg.includes("no PTY factory") ? 501 : 500;
4165
+ json(status, { error: "pty_spawn_failed", message: msg });
4166
+ }
4167
+ return true;
4168
+ }
3414
4169
  const promptMatch = path.match(/^\/sessions\/([^/]+)\/prompt$/);
3415
4170
  if (promptMatch && req.method === "POST") {
3416
4171
  const id2 = promptMatch[1];
3417
4172
  if (!id2) return false;
3418
4173
  const body = await readJsonBody(req);
3419
4174
  const prompt = body?.prompt;
3420
- if (typeof prompt !== "string") {
3421
- json(400, { error: "missing_prompt" });
4175
+ const validPrompt = typeof prompt === "string" && prompt.length > 0 || Array.isArray(prompt) && prompt.length > 0 && prompt.every((b) => b !== null && typeof b === "object") || prompt !== null && typeof prompt === "object" && !Array.isArray(prompt);
4176
+ if (!validPrompt) {
4177
+ json(400, {
4178
+ error: "missing_prompt",
4179
+ message: "Body `prompt` must be a non-empty string, a content block object, or an array of content blocks."
4180
+ });
3422
4181
  return true;
3423
4182
  }
3424
4183
  const reqUrl = req.url ?? "";
@@ -3470,14 +4229,43 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter) {
3470
4229
  }
3471
4230
  return true;
3472
4231
  }
3473
- const idMatch = path.match(/^\/sessions\/([^/]+)(\/stream|\/kill)?$/);
3474
- if (!idMatch) return false;
3475
- const [, id, suffix] = idMatch;
3476
- if (!id) return false;
4232
+ const idMatch = path.match(
4233
+ /^\/sessions\/([^/]+)(\/stream|\/kill|\/preview)?$/
4234
+ );
4235
+ if (!idMatch) return false;
4236
+ const [, rawIdOrName, suffix] = idMatch;
4237
+ if (!rawIdOrName) return false;
4238
+ const resolvedDesc = registry.findByIdOrName(rawIdOrName);
4239
+ const id = resolvedDesc?.id ?? rawIdOrName;
4240
+ if (suffix === "/preview" && req.method === "GET") {
4241
+ if (!resolvedDesc) {
4242
+ json(404, { error: "session_not_found", id: rawIdOrName });
4243
+ return true;
4244
+ }
4245
+ const reqUrl = req.url ?? "";
4246
+ const qs = new URLSearchParams(
4247
+ reqUrl.includes("?") ? reqUrl.slice(reqUrl.indexOf("?") + 1) : ""
4248
+ );
4249
+ const lineCap = clampInt(qs.get("lines"), 10, 1, 200);
4250
+ const byteCap = clampInt(qs.get("bytes"), 16 * 1024, 256, 64 * 1024);
4251
+ const lines = [];
4252
+ if (resolvedDesc.pty !== true) {
4253
+ const unsub = registry.attach(id, (line) => {
4254
+ lines.push(line);
4255
+ });
4256
+ if (unsub) unsub();
4257
+ }
4258
+ const bufBytes = registry.readTerminalOutput(id, byteCap);
4259
+ json(200, {
4260
+ id: resolvedDesc.id,
4261
+ lines: lines.slice(-lineCap),
4262
+ bytes: bufBytes ? bufBytes.toString("base64") : null
4263
+ });
4264
+ return true;
4265
+ }
3477
4266
  if (suffix === "/stream" && req.method === "GET") {
3478
- const desc = registry.get(id);
3479
- if (!desc) {
3480
- json(404, { error: "session_not_found", id });
4267
+ if (!resolvedDesc) {
4268
+ json(404, { error: "session_not_found", id: rawIdOrName });
3481
4269
  return true;
3482
4270
  }
3483
4271
  res.writeHead(200, {
@@ -3521,12 +4309,11 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter) {
3521
4309
  return true;
3522
4310
  }
3523
4311
  if (!suffix && req.method === "GET") {
3524
- const desc = registry.get(id);
3525
- if (!desc) {
3526
- json(404, { error: "session_not_found", id });
4312
+ if (!resolvedDesc) {
4313
+ json(404, { error: "session_not_found", id: rawIdOrName });
3527
4314
  return true;
3528
4315
  }
3529
- json(200, desc);
4316
+ json(200, resolvedDesc);
3530
4317
  return true;
3531
4318
  }
3532
4319
  if (!suffix && req.method === "DELETE") {
@@ -3548,14 +4335,80 @@ async function readJsonBody(req) {
3548
4335
  return null;
3549
4336
  }
3550
4337
  }
4338
+ var RESUME_STRATEGIES = {
4339
+ "claude-code": {
4340
+ // Printed by claude on graceful exit when session persistence
4341
+ // is on (default). Example: `claude --resume 0e483f81-1a44-4bec-9667-b37158450296`
4342
+ outputHint: /claude\s+--resume\s+([0-9a-f-]{8,})/i,
4343
+ storeAs: "claudeResumeId",
4344
+ fsProbe: probeMtimeLatestJsonl(".claude/projects"),
4345
+ spawnArgs: (id) => ["claude", "--resume", id]
4346
+ }
4347
+ // Stubs for other shipped adapters — fill in as we learn each
4348
+ // provider's resume mechanism. Today they fall back to ACP-level
4349
+ // resume (whatever the agent-cli runtime supports) or fresh spawn.
4350
+ //
4351
+ // hermes: { storeAs: "hermesResumeId", ... }
4352
+ // codex: { storeAs: "codexResumeId", ... }
4353
+ // openclaw: { storeAs: "openClawResumeId", ... }
4354
+ // opencode: { storeAs: "openCodeResumeId", ... }
4355
+ };
4356
+ function probeMtimeLatestJsonl(storeRel) {
4357
+ return async (cwd, prevStartedAt) => {
4358
+ const encoded = cwd.replace(/\//g, "-");
4359
+ const dir = resolve(homedir(), storeRel, encoded);
4360
+ let entries;
4361
+ try {
4362
+ entries = await promises.readdir(dir);
4363
+ } catch {
4364
+ return null;
4365
+ }
4366
+ const jsonl = entries.filter((e) => e.endsWith(".jsonl"));
4367
+ if (jsonl.length === 0) return null;
4368
+ const startedAtMs = Date.parse(prevStartedAt);
4369
+ const candidates = [];
4370
+ for (const f of jsonl) {
4371
+ try {
4372
+ const st = await promises.stat(join(dir, f));
4373
+ if (!Number.isFinite(startedAtMs) || st.mtimeMs >= startedAtMs - 1e3) {
4374
+ candidates.push({ name: f, mtime: st.mtimeMs });
4375
+ }
4376
+ } catch {
4377
+ }
4378
+ }
4379
+ if (candidates.length === 0) return null;
4380
+ candidates.sort((a, b) => b.mtime - a.mtime);
4381
+ return candidates[0].name.replace(/\.jsonl$/, "");
4382
+ };
4383
+ }
3551
4384
  var SESSIONS_FILE_PATH = () => resolve(homedir(), ".agentproto", "sessions.json");
3552
4385
  var RECENT_LINES_CAP = 500;
4386
+ var RECENT_BYTES_CAP = 64 * 1024;
3553
4387
  var PERSIST_DEBOUNCE_MS = 1500;
4388
+ var HISTORY_CAP = 200;
4389
+ function stripAnsiCodes(s) {
4390
+ return s.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, "");
4391
+ }
3554
4392
  function createSessionsRegistry(opts) {
3555
- const persistPath = SESSIONS_FILE_PATH();
4393
+ const persistPath = opts?.persistPath ?? SESSIONS_FILE_PATH();
4394
+ const persist = opts?.persist ?? true;
4395
+ const ptyFactory = opts?.spawnPty;
4396
+ const resumeAgent = opts?.resumeAgent;
3556
4397
  const sessions = /* @__PURE__ */ new Map();
3557
4398
  let persistTimer = null;
4399
+ let nextSubId = 1;
4400
+ let shutdownDone = false;
4401
+ if (persist) {
4402
+ loadHistorySnapshot(persistPath, sessions);
4403
+ }
4404
+ const onProcessExit = () => {
4405
+ shutdownImpl();
4406
+ };
4407
+ if (persist) {
4408
+ process.on("exit", onProcessExit);
4409
+ }
3558
4410
  const schedulePersist = () => {
4411
+ if (!persist) return;
3559
4412
  if (persistTimer) clearTimeout(persistTimer);
3560
4413
  persistTimer = setTimeout(() => {
3561
4414
  void persistSnapshot();
@@ -3581,8 +4434,47 @@ function createSessionsRegistry(opts) {
3581
4434
  rt.recentLines.splice(0, rt.recentLines.length - RECENT_LINES_CAP);
3582
4435
  }
3583
4436
  rt.desc.lastOutputAt = (/* @__PURE__ */ new Date()).toISOString();
4437
+ sniffResumeHints(rt, line);
3584
4438
  rt.emitter.emit("line", { line, stream });
3585
4439
  };
4440
+ const sniffResumeHints = (rt, line) => {
4441
+ if (rt.desc.kind !== "agent-cli") return;
4442
+ const strategy = rt.desc.adapterSlug ? RESUME_STRATEGIES[rt.desc.adapterSlug] : void 0;
4443
+ if (!strategy?.outputHint) return;
4444
+ const plain = stripAnsiCodes(line);
4445
+ const m = plain.match(strategy.outputHint);
4446
+ if (m && m[1]) {
4447
+ rt.desc.resumeMetadata = {
4448
+ ...rt.desc.resumeMetadata ?? {},
4449
+ [strategy.storeAs]: m[1]
4450
+ };
4451
+ schedulePersist();
4452
+ }
4453
+ };
4454
+ const appendBytes = (rt, chunk) => {
4455
+ rt.recentBytes.push(chunk);
4456
+ rt.recentBytesSize += chunk.byteLength;
4457
+ while (rt.recentBytesSize > RECENT_BYTES_CAP && rt.recentBytes.length > 1) {
4458
+ const dropped = rt.recentBytes.shift();
4459
+ if (dropped) rt.recentBytesSize -= dropped.byteLength;
4460
+ }
4461
+ rt.desc.lastOutputAt = (/* @__PURE__ */ new Date()).toISOString();
4462
+ rt.emitter.emit("data", chunk);
4463
+ };
4464
+ const reconcilePtySize = (rt) => {
4465
+ if (!rt.pty || !rt.ptySubscribers || rt.ptySubscribers.size === 0) return;
4466
+ let cols = Infinity;
4467
+ let rows = Infinity;
4468
+ for (const dim of rt.ptySubscribers.values()) {
4469
+ if (dim.cols < cols) cols = dim.cols;
4470
+ if (dim.rows < rows) rows = dim.rows;
4471
+ }
4472
+ if (!Number.isFinite(cols) || !Number.isFinite(rows)) return;
4473
+ try {
4474
+ rt.pty.resize(cols, rows);
4475
+ } catch {
4476
+ }
4477
+ };
3586
4478
  const projectEvent = (rt, evt) => {
3587
4479
  switch (evt.kind) {
3588
4480
  case "text-delta":
@@ -3671,6 +4563,59 @@ function createSessionsRegistry(opts) {
3671
4563
  }
3672
4564
  return rt;
3673
4565
  };
4566
+ const maybeResumeAgent = async (rt) => {
4567
+ if (rt.agentSession) return;
4568
+ if (!resumeAgent) return;
4569
+ if (rt.desc.kind !== "agent-cli") return;
4570
+ const adapterSlug = rt.desc.adapterSlug ?? rt.adapterSlug;
4571
+ const adapterSessionId = rt.desc.adapterSessionId;
4572
+ const cwd = rt.desc.cwd;
4573
+ if (!adapterSlug || !adapterSessionId || !cwd) return;
4574
+ if (rt.resumePromise) {
4575
+ await rt.resumePromise;
4576
+ return;
4577
+ }
4578
+ rt.resumePromise = (async () => {
4579
+ appendLine(
4580
+ rt,
4581
+ `\u2500\u2500 resuming ${adapterSlug} session ${adapterSessionId} \u2500\u2500`,
4582
+ "stdout"
4583
+ );
4584
+ try {
4585
+ const fresh = await resumeAgent({
4586
+ adapterSlug,
4587
+ cwd,
4588
+ resumeSessionId: adapterSessionId
4589
+ });
4590
+ if (!fresh) {
4591
+ appendLine(
4592
+ rt,
4593
+ `[error] resume failed: adapter '${adapterSlug}' returned null`,
4594
+ "stderr"
4595
+ );
4596
+ return;
4597
+ }
4598
+ rt.agentSession = fresh;
4599
+ rt.adapterSlug = adapterSlug;
4600
+ rt.desc.adapterSessionId = fresh.sessionId;
4601
+ if (rt.desc.status !== "running") {
4602
+ rt.desc.status = "running";
4603
+ delete rt.desc.endedAt;
4604
+ delete rt.desc.exitCode;
4605
+ rt.emitter.emit("status", rt.desc.status);
4606
+ }
4607
+ schedulePersist();
4608
+ } catch (err) {
4609
+ const msg = err instanceof Error ? err.message : String(err);
4610
+ appendLine(rt, `[error] resume failed: ${msg}`, "stderr");
4611
+ }
4612
+ })();
4613
+ try {
4614
+ await rt.resumePromise;
4615
+ } finally {
4616
+ rt.resumePromise = void 0;
4617
+ }
4618
+ };
3674
4619
  const runAgentTurn = async (rt, message) => {
3675
4620
  if (!rt.agentSession) {
3676
4621
  throw new Error("runAgentTurn: session has no agentSession");
@@ -3734,12 +4679,16 @@ function createSessionsRegistry(opts) {
3734
4679
  pid: child.pid ?? null,
3735
4680
  status: "starting",
3736
4681
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
4682
+ argv: input2.argv,
4683
+ cwd: input2.cwd,
3737
4684
  ...input2.label ? { label: input2.label } : {}
3738
4685
  };
3739
4686
  const rt = {
3740
4687
  desc,
3741
4688
  child,
3742
4689
  recentLines: [],
4690
+ recentBytes: [],
4691
+ recentBytesSize: 0,
3743
4692
  emitter: new EventEmitter(),
3744
4693
  busy: false,
3745
4694
  textBuf: "",
@@ -3792,6 +4741,8 @@ function createSessionsRegistry(opts) {
3792
4741
  desc,
3793
4742
  child: input2.child,
3794
4743
  recentLines: [],
4744
+ recentBytes: [],
4745
+ recentBytesSize: 0,
3795
4746
  emitter: new EventEmitter(),
3796
4747
  busy: false,
3797
4748
  textBuf: "",
@@ -3829,6 +4780,13 @@ function createSessionsRegistry(opts) {
3829
4780
  status: "running",
3830
4781
  // driver already started the session
3831
4782
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
4783
+ cwd: input2.cwd,
4784
+ adapterSlug: input2.adapterSlug,
4785
+ // ACP-level session id — sticks across daemon restart so
4786
+ // `agentproto sessions restart <id>` can pass it as
4787
+ // `resumeSessionId` and the adapter reattaches to the prior
4788
+ // conversation rather than starting blank.
4789
+ adapterSessionId: input2.agentSession.sessionId,
3832
4790
  ...input2.label ? { label: input2.label } : {}
3833
4791
  };
3834
4792
  const rt = {
@@ -3836,6 +4794,8 @@ function createSessionsRegistry(opts) {
3836
4794
  agentSession: input2.agentSession,
3837
4795
  adapterSlug: input2.adapterSlug,
3838
4796
  recentLines: [],
4797
+ recentBytes: [],
4798
+ recentBytesSize: 0,
3839
4799
  emitter: new EventEmitter(),
3840
4800
  busy: false,
3841
4801
  textBuf: "",
@@ -3860,14 +4820,117 @@ function createSessionsRegistry(opts) {
3860
4820
  }
3861
4821
  return desc;
3862
4822
  },
4823
+ spawnPty(input2) {
4824
+ if (!ptyFactory) {
4825
+ throw new Error(
4826
+ "sessions.spawnPty: no PTY factory configured (node-pty optional dep missing in the daemon)"
4827
+ );
4828
+ }
4829
+ const id = `sess_${randomUUID().slice(0, 8)}`;
4830
+ if (input2.name) {
4831
+ for (const rt2 of sessions.values()) {
4832
+ if (rt2.desc.name !== input2.name) continue;
4833
+ const status = rt2.desc.status;
4834
+ if (status === "running" || status === "starting") {
4835
+ throw new Error(
4836
+ `sessions.spawnPty: name "${input2.name}" already in use by ${rt2.desc.id} (status=${status})`
4837
+ );
4838
+ }
4839
+ rt2.desc.name = void 0;
4840
+ }
4841
+ }
4842
+ const [bin, ...args] = input2.argv;
4843
+ if (!bin) {
4844
+ throw new Error("sessions.spawnPty: argv must include a binary");
4845
+ }
4846
+ const env = { ...process.env, ...input2.env ?? {} };
4847
+ delete env.NODE_OPTIONS;
4848
+ const pty = ptyFactory({
4849
+ command: bin,
4850
+ args,
4851
+ cwd: input2.cwd,
4852
+ env,
4853
+ cols: input2.cols,
4854
+ rows: input2.rows
4855
+ });
4856
+ const desc = {
4857
+ id,
4858
+ kind: "terminal",
4859
+ workspaceSlug: input2.workspaceSlug,
4860
+ command: [bin, ...args].map(quoteArg).join(" "),
4861
+ pid: pty.pid,
4862
+ status: "running",
4863
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
4864
+ pty: true,
4865
+ argv: input2.argv,
4866
+ cwd: input2.cwd,
4867
+ ...input2.name ? { name: input2.name } : {},
4868
+ ...input2.label ? { label: input2.label } : {}
4869
+ };
4870
+ const rt = {
4871
+ desc,
4872
+ pty,
4873
+ ptySubscribers: /* @__PURE__ */ new Map(),
4874
+ recentLines: [],
4875
+ recentBytes: [],
4876
+ recentBytesSize: 0,
4877
+ emitter: new EventEmitter(),
4878
+ busy: false,
4879
+ textBuf: "",
4880
+ thoughtBuf: ""
4881
+ };
4882
+ rt.emitter.setMaxListeners(50);
4883
+ sessions.set(id, rt);
4884
+ pty.onData((chunk) => {
4885
+ appendBytes(rt, Buffer.from(chunk, "utf8"));
4886
+ });
4887
+ pty.onExit((evt) => {
4888
+ if (rt.desc.status !== "killed") {
4889
+ rt.desc.status = "exited";
4890
+ }
4891
+ rt.desc.endedAt = (/* @__PURE__ */ new Date()).toISOString();
4892
+ if (typeof evt.exitCode === "number") rt.desc.exitCode = evt.exitCode;
4893
+ rt.emitter.emit("exit", evt);
4894
+ rt.emitter.emit("status", rt.desc.status);
4895
+ schedulePersist();
4896
+ });
4897
+ schedulePersist();
4898
+ return desc;
4899
+ },
3863
4900
  async sendPrompt(id, message) {
4901
+ const rtPre = sessions.get(id);
4902
+ if (rtPre) await maybeResumeAgent(rtPre);
3864
4903
  const rt = validateAgentTurn(id, "sendPrompt");
3865
4904
  await runAgentTurn(rt, message);
3866
4905
  },
3867
4906
  enqueuePrompt(id, message) {
3868
- const rt = validateAgentTurn(id, "enqueuePrompt");
3869
- void runAgentTurn(rt, message).catch(() => {
3870
- });
4907
+ const rtPre = sessions.get(id);
4908
+ if (!rtPre) {
4909
+ throw new Error(`enqueuePrompt: no session "${id}"`);
4910
+ }
4911
+ if (rtPre.desc.kind !== "agent-cli") {
4912
+ throw new Error(
4913
+ `enqueuePrompt: session "${id}" is not an agent session (kind=${rtPre.desc.kind})`
4914
+ );
4915
+ }
4916
+ if (rtPre.busy) {
4917
+ throw new Error(
4918
+ `enqueuePrompt: session "${id}" is mid-turn \u2014 wait for it to finish or cancel`
4919
+ );
4920
+ }
4921
+ void (async () => {
4922
+ try {
4923
+ await maybeResumeAgent(rtPre);
4924
+ const rt = validateAgentTurn(id, "enqueuePrompt");
4925
+ await runAgentTurn(rt, message);
4926
+ } catch (err) {
4927
+ appendLine(
4928
+ rtPre,
4929
+ `[error] ${err instanceof Error ? err.message : String(err)}`,
4930
+ "stderr"
4931
+ );
4932
+ }
4933
+ })();
3871
4934
  },
3872
4935
  list() {
3873
4936
  return Array.from(sessions.values()).map((s) => s.desc).sort((a, b) => b.startedAt.localeCompare(a.startedAt));
@@ -3883,6 +4946,65 @@ function createSessionsRegistry(opts) {
3883
4946
  rt.emitter.on("line", handler);
3884
4947
  return () => rt.emitter.off("line", handler);
3885
4948
  },
4949
+ attachPty(id, initial, onData, onExit) {
4950
+ const rt = sessions.get(id);
4951
+ if (!rt || !rt.pty || !rt.ptySubscribers) return null;
4952
+ for (const chunk of rt.recentBytes) onData(chunk);
4953
+ const subId = nextSubId++;
4954
+ rt.ptySubscribers.set(subId, { cols: initial.cols, rows: initial.rows });
4955
+ reconcilePtySize(rt);
4956
+ const dataHandler = (chunk) => onData(chunk);
4957
+ const exitHandler = (evt) => onExit(evt);
4958
+ rt.emitter.on("data", dataHandler);
4959
+ rt.emitter.once("exit", exitHandler);
4960
+ return {
4961
+ write(data) {
4962
+ if (!rt.pty) return;
4963
+ try {
4964
+ rt.pty.write(data);
4965
+ } catch {
4966
+ }
4967
+ },
4968
+ resize(cols, rows) {
4969
+ if (!rt.ptySubscribers) return;
4970
+ rt.ptySubscribers.set(subId, { cols, rows });
4971
+ reconcilePtySize(rt);
4972
+ },
4973
+ detach() {
4974
+ rt.emitter.off("data", dataHandler);
4975
+ rt.emitter.off("exit", exitHandler);
4976
+ rt.ptySubscribers?.delete(subId);
4977
+ reconcilePtySize(rt);
4978
+ }
4979
+ };
4980
+ },
4981
+ findByIdOrName(query) {
4982
+ const direct = sessions.get(query);
4983
+ if (direct) return direct.desc;
4984
+ for (const rt of sessions.values()) {
4985
+ if (rt.desc.name === query) return rt.desc;
4986
+ }
4987
+ return void 0;
4988
+ },
4989
+ writeTerminalInput(id, data) {
4990
+ const rt = sessions.get(id);
4991
+ if (!rt || !rt.pty) return false;
4992
+ try {
4993
+ rt.pty.write(data);
4994
+ return true;
4995
+ } catch {
4996
+ return false;
4997
+ }
4998
+ },
4999
+ readTerminalOutput(id, lastBytes) {
5000
+ const rt = sessions.get(id);
5001
+ if (!rt || !rt.pty) return null;
5002
+ const joined = Buffer.concat(rt.recentBytes, rt.recentBytesSize);
5003
+ if (typeof lastBytes === "number" && lastBytes > 0 && joined.byteLength > lastBytes) {
5004
+ return joined.subarray(joined.byteLength - lastBytes);
5005
+ }
5006
+ return joined;
5007
+ },
3886
5008
  kill(id, signal = "SIGTERM") {
3887
5009
  const rt = sessions.get(id);
3888
5010
  if (!rt) return false;
@@ -3894,6 +5016,12 @@ function createSessionsRegistry(opts) {
3894
5016
  if (rt.agentSession) {
3895
5017
  void rt.agentSession.close().catch(() => void 0);
3896
5018
  }
5019
+ if (rt.pty) {
5020
+ try {
5021
+ rt.pty.kill(signal);
5022
+ } catch {
5023
+ }
5024
+ }
3897
5025
  rt.child?.kill(signal);
3898
5026
  schedulePersist();
3899
5027
  return true;
@@ -3907,19 +5035,91 @@ function createSessionsRegistry(opts) {
3907
5035
  return true;
3908
5036
  },
3909
5037
  shutdown() {
3910
- if (persistTimer) clearTimeout(persistTimer);
3911
- for (const rt of sessions.values()) {
3912
- rt.emitter.removeAllListeners();
3913
- if (rt.desc.status === "running" || rt.desc.status === "starting") {
3914
- if (rt.agentSession) {
3915
- void rt.agentSession.close().catch(() => void 0);
5038
+ shutdownImpl();
5039
+ }
5040
+ };
5041
+ function shutdownImpl() {
5042
+ if (shutdownDone) return;
5043
+ shutdownDone = true;
5044
+ if (persistTimer) clearTimeout(persistTimer);
5045
+ if (persist) {
5046
+ try {
5047
+ process.off("exit", onProcessExit);
5048
+ } catch {
5049
+ }
5050
+ }
5051
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
5052
+ for (const rt of sessions.values()) {
5053
+ rt.emitter.removeAllListeners();
5054
+ if (rt.desc.status === "running" || rt.desc.status === "starting") {
5055
+ rt.desc.status = "killed";
5056
+ rt.desc.endedAt = nowIso;
5057
+ if (rt.agentSession) {
5058
+ void rt.agentSession.close().catch(() => void 0);
5059
+ }
5060
+ if (rt.pty) {
5061
+ try {
5062
+ rt.pty.kill("SIGTERM");
5063
+ } catch {
3916
5064
  }
3917
- rt.child?.kill("SIGTERM");
3918
5065
  }
5066
+ rt.child?.kill("SIGTERM");
3919
5067
  }
3920
- sessions.clear();
3921
5068
  }
3922
- };
5069
+ if (persist) {
5070
+ try {
5071
+ const snapshot = {
5072
+ savedAt: nowIso,
5073
+ sessions: Array.from(sessions.values()).map((s) => s.desc)
5074
+ };
5075
+ mkdirSync(dirname(persistPath), { recursive: true });
5076
+ writeFileSync(persistPath, JSON.stringify(snapshot, null, 2) + "\n");
5077
+ } catch {
5078
+ }
5079
+ }
5080
+ sessions.clear();
5081
+ }
5082
+ }
5083
+ function loadHistorySnapshot(persistPath, sessions) {
5084
+ let raw;
5085
+ try {
5086
+ raw = readFileSync(persistPath, "utf8");
5087
+ } catch {
5088
+ return;
5089
+ }
5090
+ let parsed;
5091
+ try {
5092
+ parsed = JSON.parse(raw);
5093
+ } catch {
5094
+ console.warn(
5095
+ `[sessions] history file ${persistPath} is malformed \u2014 ignoring`
5096
+ );
5097
+ return;
5098
+ }
5099
+ if (!Array.isArray(parsed.sessions)) return;
5100
+ const sorted = parsed.sessions.filter((s) => !!s && typeof s.id === "string").sort((a, b) => b.startedAt.localeCompare(a.startedAt)).slice(0, HISTORY_CAP);
5101
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5102
+ for (const desc of sorted) {
5103
+ if (sessions.has(desc.id)) continue;
5104
+ const wasAlive = desc.status === "running" || desc.status === "starting";
5105
+ const reclassified = wasAlive ? {
5106
+ ...desc,
5107
+ status: "killed",
5108
+ endedAt: desc.endedAt ?? now
5109
+ } : desc;
5110
+ const rt = {
5111
+ desc: reclassified,
5112
+ recentLines: [],
5113
+ recentBytes: [],
5114
+ recentBytesSize: 0,
5115
+ emitter: new EventEmitter(),
5116
+ busy: false,
5117
+ textBuf: "",
5118
+ thoughtBuf: ""
5119
+ };
5120
+ rt.emitter.setMaxListeners(50);
5121
+ sessions.set(desc.id, rt);
5122
+ }
3923
5123
  }
3924
5124
  function quoteArg(arg) {
3925
5125
  if (arg === "") return '""';
@@ -4115,9 +5315,24 @@ async function openClient(entry) {
4115
5315
  const transport = new StdioClientTransport({
4116
5316
  command: snap.command,
4117
5317
  args: snap.args ?? [],
4118
- env: { ...process.env, ...expandedEnv }
5318
+ env: { ...process.env, ...expandedEnv },
5319
+ // Pipe stderr so we can include it in the error message instead of
5320
+ // surfacing the opaque "Connection closed" MCP error code.
5321
+ stderr: "pipe"
4119
5322
  });
4120
- await client.connect(transport);
5323
+ const stderrBuf = [];
5324
+ transport.stderr?.on("data", (chunk) => {
5325
+ stderrBuf.push(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
5326
+ if (stderrBuf.length > 40) stderrBuf.shift();
5327
+ });
5328
+ try {
5329
+ await client.connect(transport);
5330
+ } catch (err) {
5331
+ const stderrText = stderrBuf.join("").trim();
5332
+ const msg = err instanceof Error ? err.message : String(err);
5333
+ throw new Error(stderrText ? `${msg}
5334
+ stderr: ${stderrText.slice(-600)}` : msg);
5335
+ }
4121
5336
  return client;
4122
5337
  }
4123
5338
  if (snap.type === "http") {
@@ -4175,7 +5390,7 @@ function quickTunnelProvider() {
4175
5390
  { stdio: ["ignore", "pipe", "pipe"], shell: false }
4176
5391
  );
4177
5392
  child = proc;
4178
- const url = await new Promise((resolve7, reject) => {
5393
+ const url = await new Promise((resolve8, reject) => {
4179
5394
  let settled = false;
4180
5395
  const timer = setTimeout(() => {
4181
5396
  if (settled) return;
@@ -4200,7 +5415,7 @@ function quickTunnelProvider() {
4200
5415
  if (match) {
4201
5416
  settled = true;
4202
5417
  clearTimeout(timer);
4203
- resolve7(match[0]);
5418
+ resolve8(match[0]);
4204
5419
  }
4205
5420
  };
4206
5421
  proc.stderr?.on("data", onData);
@@ -4249,15 +5464,15 @@ function quickTunnelProvider() {
4249
5464
  }
4250
5465
  }, 3e3);
4251
5466
  timer.unref();
4252
- await new Promise((resolve7) => {
5467
+ await new Promise((resolve8) => {
4253
5468
  if (proc.exitCode !== null) {
4254
5469
  clearTimeout(timer);
4255
- resolve7();
5470
+ resolve8();
4256
5471
  return;
4257
5472
  }
4258
5473
  proc.once("exit", () => {
4259
5474
  clearTimeout(timer);
4260
- resolve7();
5475
+ resolve8();
4261
5476
  });
4262
5477
  });
4263
5478
  }
@@ -4494,7 +5709,7 @@ var WorkspacePathError = class extends Error {
4494
5709
  };
4495
5710
  function createWorkspaceFs(opts) {
4496
5711
  const root = resolve(opts.workspace);
4497
- function resolvePath32(path) {
5712
+ function resolvePath4(path) {
4498
5713
  if (typeof path !== "string" || path.length === 0) {
4499
5714
  throw new WorkspacePathError("path must be a non-empty string");
4500
5715
  }
@@ -4514,18 +5729,18 @@ function createWorkspaceFs(opts) {
4514
5729
  }
4515
5730
  return {
4516
5731
  async readFile(path) {
4517
- const abs = resolvePath32(path);
5732
+ const abs = resolvePath4(path);
4518
5733
  const buf = await readFile(abs);
4519
5734
  return buf.toString("utf8");
4520
5735
  },
4521
5736
  async writeFile(path, content) {
4522
- const abs = resolvePath32(path);
5737
+ const abs = resolvePath4(path);
4523
5738
  await mkdir(dirname(abs), { recursive: true });
4524
5739
  await writeFile(abs, content);
4525
5740
  },
4526
5741
  async exists(path) {
4527
5742
  try {
4528
- const abs = resolvePath32(path);
5743
+ const abs = resolvePath4(path);
4529
5744
  return existsSync(abs);
4530
5745
  } catch {
4531
5746
  return false;
@@ -4557,7 +5772,31 @@ async function createGateway(opts) {
4557
5772
  name: opts.name ?? "agentproto-runtime",
4558
5773
  version: opts.version ?? "0.1.0-alpha"
4559
5774
  });
4560
- const sessions = createSessionsRegistry();
5775
+ const sessions = createSessionsRegistry({
5776
+ ...opts.spawnPty ? { spawnPty: opts.spawnPty } : {},
5777
+ // Resume hook: when a prompt arrives for a dead agent-cli row
5778
+ // (typical after daemon restart), the registry calls back into
5779
+ // the adapter resolver to re-create the AgentSession with
5780
+ // `resumeSessionId = adapterSessionId`. ACP semantics: the
5781
+ // upstream provider reattaches to the prior conversation.
5782
+ // Unwired (no resolveAgentAdapter) → legacy "not an agent
5783
+ // session" error, user must spawn fresh.
5784
+ ...opts.resolveAgentAdapter ? {
5785
+ resumeAgent: async ({ adapterSlug, cwd, resumeSessionId }) => {
5786
+ const adapter = await opts.resolveAgentAdapter(adapterSlug);
5787
+ if (!adapter) return null;
5788
+ try {
5789
+ return await adapter.startSession({ cwd, resumeSessionId });
5790
+ } catch (err) {
5791
+ console.warn(
5792
+ `[agentproto] resumeAgent('${adapterSlug}', ${resumeSessionId}) failed: ${err instanceof Error ? err.message : String(err)}`
5793
+ );
5794
+ return null;
5795
+ }
5796
+ }
5797
+ } : {}
5798
+ });
5799
+ const token = opts.token ?? randomUUID();
4561
5800
  const mcpProxy = new McpProxyRegistry();
4562
5801
  const mcpServerFactory = async () => {
4563
5802
  const { server } = await createMcpServer({
@@ -4572,6 +5811,7 @@ async function createGateway(opts) {
4572
5811
  registerSessionTools(server, {
4573
5812
  registry: sessions,
4574
5813
  mcpProxy,
5814
+ ptyEnabled: opts.spawnPty != null,
4575
5815
  ...opts.resolveAgentAdapter ? { resolveAgentAdapter: opts.resolveAgentAdapter } : {},
4576
5816
  ...opts.listAgentAdapters ? { listAgentAdapters: opts.listAgentAdapters } : {}
4577
5817
  });
@@ -4613,6 +5853,10 @@ async function createGateway(opts) {
4613
5853
  heartbeat,
4614
5854
  sessions,
4615
5855
  mcpProxy,
5856
+ token,
5857
+ ptyEnabled: opts.spawnPty != null,
5858
+ ...opts.allowedOrigins ? { allowedOrigins: opts.allowedOrigins } : {},
5859
+ ...opts.strictOrigins ? { strictOrigins: true } : {},
4616
5860
  ...opts.resolveAgentAdapter ? { resolveAgentAdapter: opts.resolveAgentAdapter } : {},
4617
5861
  ...opts.listAgentAdapters ? { listAgentAdapters: opts.listAgentAdapters } : {},
4618
5862
  meta: { workspace, registered }
@@ -4631,7 +5875,8 @@ async function createGateway(opts) {
4631
5875
  pid: process.pid,
4632
5876
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
4633
5877
  name: opts.name ?? "agentproto-runtime",
4634
- registered
5878
+ registered,
5879
+ token
4635
5880
  });
4636
5881
  return {
4637
5882
  url: http.url,
@@ -4639,6 +5884,7 @@ async function createGateway(opts) {
4639
5884
  workspaceFs,
4640
5885
  registered,
4641
5886
  sessions,
5887
+ token,
4642
5888
  async stop() {
4643
5889
  heartbeat.stop();
4644
5890
  sessions.shutdown();
@@ -4687,10 +5933,17 @@ async function runServe(args) {
4687
5933
  label: { type: "string", short: "l" },
4688
5934
  workspace: { type: "string", short: "w" },
4689
5935
  port: { type: "string", short: "p" },
4690
- bind: { type: "string", short: "b" }
5936
+ bind: { type: "string", short: "b" },
5937
+ "allow-origin": { type: "string", multiple: true },
5938
+ interactive: { type: "boolean", short: "i" }
4691
5939
  }
4692
5940
  });
4693
- const workspace = resolve(values.workspace ?? process.cwd());
5941
+ const cfg = await loadConfig();
5942
+ const cfgDaemon = cfg.daemon ?? {};
5943
+ const cfgTunnel = cfg.tunnel ?? {};
5944
+ const workspace = resolve(
5945
+ values.workspace ?? cfgDaemon.workspace ?? process.cwd()
5946
+ );
4694
5947
  try {
4695
5948
  const stat2 = await promises.stat(workspace);
4696
5949
  if (!stat2.isDirectory()) {
@@ -4708,20 +5961,21 @@ async function runServe(args) {
4708
5961
  );
4709
5962
  return 2;
4710
5963
  }
4711
- const port = values.port ? Number.parseInt(values.port, 10) : 18790;
5964
+ const port = values.port ? Number.parseInt(values.port, 10) : typeof cfgDaemon.port === "number" ? cfgDaemon.port : 18790;
4712
5965
  if (!Number.isFinite(port) || port <= 0 || port > 65535) {
4713
5966
  process.stderr.write(`agentproto serve: invalid --port "${values.port}".
4714
5967
  `);
4715
5968
  return 2;
4716
5969
  }
4717
- const label = values.label ?? `${userInfo().username}@${hostname()}`;
5970
+ const label = values.label ?? cfgDaemon.label ?? `${userInfo().username}@${hostname()}`;
5971
+ const connectFlag = values.connect ?? (cfgTunnel.autoconnect && cfgTunnel.host ? cfgTunnel.host : void 0);
4718
5972
  let token = values.token ?? process.env.AGENTPROTO_TOKEN;
4719
- if (!token && values.connect) {
4720
- const cred = await readHost(values.connect);
5973
+ if (!token && connectFlag) {
5974
+ const cred = await readHost(connectFlag);
4721
5975
  if (cred) {
4722
5976
  if (isExpired(cred)) {
4723
5977
  process.stderr.write(
4724
- `agentproto serve: \u26A0 credentials for ${values.connect} are expired (${formatExpiry(cred)}). Re-run \`agentproto auth login --host ${values.connect}\`.
5978
+ `agentproto serve: \u26A0 credentials for ${connectFlag} are expired (${formatExpiry(cred)}). Re-run \`agentproto auth login --host ${connectFlag}\`.
4725
5979
  `
4726
5980
  );
4727
5981
  }
@@ -4732,21 +5986,31 @@ async function runServe(args) {
4732
5986
  );
4733
5987
  }
4734
5988
  }
5989
+ const allowOriginRaw = values["allow-origin"];
5990
+ const cliOrigins = Array.isArray(allowOriginRaw) ? allowOriginRaw : [];
5991
+ const cfgOrigins = Array.isArray(cfgDaemon.allowedOrigins) ? cfgDaemon.allowedOrigins : [];
5992
+ const merged = [.../* @__PURE__ */ new Set([...cfgOrigins, ...cliOrigins])];
5993
+ const allowedOrigins = merged.length > 0 ? merged : void 0;
4735
5994
  const opts = {
4736
5995
  workspace,
4737
5996
  port,
4738
- bind: values.bind ?? "127.0.0.1",
4739
- ...values.connect ? { connect: values.connect } : {},
5997
+ bind: values.bind ?? cfgDaemon.bind ?? "127.0.0.1",
5998
+ ...connectFlag ? { connect: connectFlag } : {},
4740
5999
  ...token ? { token } : {},
4741
- label
6000
+ label,
6001
+ ...allowedOrigins ? { allowedOrigins } : {},
6002
+ ...cfgDaemon.strictOrigins === true ? { strictOrigins: true } : {}
4742
6003
  };
4743
6004
  const resolveAgentAdapter = async (slug) => {
4744
6005
  try {
4745
6006
  const adapter = await resolveAdapter(slug);
4746
6007
  const runtime = createAgentCliRuntime(adapter.handle);
4747
6008
  return {
4748
- async startSession({ cwd }) {
4749
- return runtime.start({ cwd });
6009
+ async startSession({ cwd, resumeSessionId }) {
6010
+ return runtime.start({
6011
+ cwd,
6012
+ ...resumeSessionId ? { resumeSessionId } : {}
6013
+ });
4750
6014
  },
4751
6015
  commandPreview: `${adapter.handle.bin} ${(adapter.handle.bin_args ?? []).join(" ")}`.trim()
4752
6016
  };
@@ -4757,6 +6021,7 @@ async function runServe(args) {
4757
6021
  return null;
4758
6022
  }
4759
6023
  };
6024
+ const spawnPty = await loadNodePtyFactory();
4760
6025
  let gateway;
4761
6026
  try {
4762
6027
  gateway = await createGateway({
@@ -4771,7 +6036,10 @@ async function runServe(args) {
4771
6036
  // Discovery for UIs / operators — `GET /adapters` + `list_adapters`
4772
6037
  // MCP tool. Walks node_modules @agentproto/adapter-* on each call;
4773
6038
  // cheap enough that we don't bother caching here.
4774
- listAgentAdapters: listInstalledAdapters
6039
+ listAgentAdapters: listInstalledAdapters,
6040
+ ...spawnPty ? { spawnPty } : {},
6041
+ ...opts.allowedOrigins ? { allowedOrigins: opts.allowedOrigins } : {},
6042
+ ...opts.strictOrigins ? { strictOrigins: true } : {}
4775
6043
  });
4776
6044
  } catch (err) {
4777
6045
  process.stderr.write(
@@ -4780,48 +6048,105 @@ async function runServe(args) {
4780
6048
  );
4781
6049
  return 1;
4782
6050
  }
4783
- process.stderr.write(
4784
- `agentproto serve: gateway up on ${gateway.url}
4785
- workspace: ${gateway.workspace}
4786
- mcp: ${gateway.url}/mcp
4787
- sessions: ${gateway.url}/sessions
4788
- events: ${gateway.url}/events
6051
+ printBootBanner({
6052
+ url: gateway.url,
6053
+ workspace: gateway.workspace,
6054
+ ptyEnabled: spawnPty != null,
6055
+ allowedOrigins: opts.allowedOrigins,
6056
+ strictOrigins: opts.strictOrigins === true,
6057
+ connect: opts.connect
6058
+ });
6059
+ try {
6060
+ const wsConfig = await loadWorkspacesConfig();
6061
+ const paths = wsConfig.workspaces.map((w) => w.path);
6062
+ const cleaned = await sweepStaleRuntimeMetas(paths, opts.workspace);
6063
+ if (cleaned.length > 0) {
6064
+ process.stderr.write(
6065
+ `${color.dim}cleaned ${cleaned.length} stale runtime.json file(s) (dead PID)${color.reset}
4789
6066
  `
4790
- );
6067
+ );
6068
+ }
6069
+ } catch {
6070
+ }
4791
6071
  const aborter = new AbortController();
4792
6072
  let shuttingDown = false;
4793
6073
  const shutdown = async (signal) => {
4794
6074
  if (shuttingDown) return;
4795
6075
  shuttingDown = true;
4796
- process.stderr.write(`
4797
- agentproto serve: ${signal} \u2014 shutting down.
4798
- `);
6076
+ process.stderr.write(
6077
+ `
6078
+ ${color.dim}\u2500\u2500 shutting down (${signal}) \u2500\u2500${color.reset}
6079
+ `
6080
+ );
4799
6081
  aborter.abort();
4800
6082
  await gateway.stop().catch(() => void 0);
6083
+ await unlinkRuntimeMeta(opts.workspace).catch(() => void 0);
4801
6084
  process.exit(0);
4802
6085
  };
4803
6086
  process.once("SIGINT", () => void shutdown("SIGINT"));
4804
6087
  process.once("SIGTERM", () => void shutdown("SIGTERM"));
6088
+ process.once("SIGHUP", () => void shutdown("SIGHUP"));
6089
+ if (values.interactive) {
6090
+ process.stderr.write(
6091
+ `${color.dim}entering interactive monitor \u2014 q in the TUI to quit (daemon + TUI both).${color.reset}
6092
+ `
6093
+ );
6094
+ const childArgv = [process.argv[1] ?? "", "sessions", "--watch"];
6095
+ const child = childProc.spawn(process.execPath, childArgv, {
6096
+ stdio: "inherit",
6097
+ env: {
6098
+ ...process.env,
6099
+ AGENTPROTO_DAEMON_URL: gateway.url,
6100
+ // `gateway.token` is the per-boot daemon bearer (different
6101
+ // from `token`, which was the tunnel JWT). Without this, the
6102
+ // child's restart/kill calls would 401 against its own
6103
+ // parent — exactly the bug that closed --interactive after
6104
+ // any failed action.
6105
+ AGENTPROTO_DAEMON_TOKEN: gateway.token
6106
+ }
6107
+ });
6108
+ await new Promise((resolve4) => {
6109
+ child.once("exit", () => resolve4());
6110
+ aborter.signal.addEventListener("abort", () => {
6111
+ try {
6112
+ child.kill("SIGTERM");
6113
+ } catch {
6114
+ }
6115
+ });
6116
+ });
6117
+ if (!shuttingDown) {
6118
+ aborter.abort();
6119
+ await gateway.stop().catch(() => void 0);
6120
+ await unlinkRuntimeMeta(opts.workspace).catch(() => void 0);
6121
+ }
6122
+ return 0;
6123
+ }
4805
6124
  if (!opts.connect) {
4806
6125
  process.stderr.write(
4807
- `agentproto serve: running local-only (no --connect). Press Ctrl-C to stop.
6126
+ `${color.dim}Press Ctrl-C to stop.${color.reset}
4808
6127
  `
4809
6128
  );
4810
- await new Promise((resolve3) => {
4811
- aborter.signal.addEventListener("abort", () => resolve3());
6129
+ await new Promise((resolve4) => {
6130
+ aborter.signal.addEventListener("abort", () => resolve4());
4812
6131
  });
4813
6132
  return 0;
4814
6133
  }
4815
6134
  process.stderr.write(
4816
- `agentproto serve: tunnel \u2014 connecting to ${opts.connect} as '${opts.label}'\u2026
6135
+ `${color.dim}tunnel \xB7 connecting to ${opts.connect} as '${opts.label}'\u2026${color.reset}
4817
6136
  `
4818
6137
  );
4819
6138
  let backoffMs = opts.reconnectMinMs ?? 1e3;
4820
6139
  const backoffMax = opts.reconnectMaxMs ?? 3e4;
6140
+ const reconnectState = { immediate: false };
4821
6141
  while (!aborter.signal.aborted) {
4822
6142
  try {
4823
- await runOneTunnel(opts, gateway, aborter.signal);
6143
+ await runOneTunnel(opts, gateway, spawnPty, aborter.signal, reconnectState);
4824
6144
  backoffMs = opts.reconnectMinMs ?? 1e3;
6145
+ if (reconnectState.immediate) {
6146
+ reconnectState.immediate = false;
6147
+ continue;
6148
+ }
6149
+ await sleep(2e3, aborter.signal);
4825
6150
  } catch (err) {
4826
6151
  if (aborter.signal.aborted) break;
4827
6152
  const msg = err instanceof Error ? err.message : String(err);
@@ -4836,17 +6161,17 @@ agentproto serve: ${signal} \u2014 shutting down.
4836
6161
  }
4837
6162
  return 0;
4838
6163
  }
4839
- async function runOneTunnel(opts, gateway, signal) {
6164
+ async function runOneTunnel(opts, gateway, spawnPty, signal, reconnectState) {
4840
6165
  if (!opts.connect) throw new Error("runOneTunnel: --connect not set");
4841
6166
  const headers = {
4842
6167
  "user-agent": "agentproto/0.1.0-alpha"
4843
6168
  };
4844
6169
  if (opts.token) headers.authorization = `Bearer ${opts.token}`;
4845
6170
  const ws = new WebSocket(opts.connect, { headers });
4846
- await new Promise((resolve3, reject) => {
6171
+ await new Promise((resolve4, reject) => {
4847
6172
  const onOpen = () => {
4848
6173
  ws.off("error", onError);
4849
- resolve3();
6174
+ resolve4();
4850
6175
  };
4851
6176
  const onError = (err) => {
4852
6177
  ws.off("open", onOpen);
@@ -4866,10 +6191,86 @@ async function runOneTunnel(opts, gateway, signal) {
4866
6191
  process.stderr.write(`agentproto serve: tunnel up.
4867
6192
  `);
4868
6193
  const sink = wrapWebSocket(ws);
6194
+ const keepaliveInterval = setInterval(() => {
6195
+ try {
6196
+ sink.send({ t: "ping", nonce: randomUUID() });
6197
+ } catch {
6198
+ }
6199
+ }, 3e4);
4869
6200
  const server = createTunnelServer({
4870
6201
  sink,
4871
6202
  label: opts.label,
4872
- pty: false,
6203
+ pty: spawnPty !== null,
6204
+ ...spawnPty ? { spawnPty } : {},
6205
+ // Generic HTTP-relay upstream for tunnel `http_request` frames.
6206
+ // Cloud-side callers (e.g. the API's local-daemon filesystem
6207
+ // provider) can now route MCP JSON-RPC + any other HTTP through
6208
+ // the daemon without needing a public URL. We point at the local
6209
+ // gateway since that's where `/mcp`, `/sessions`, `/events` live.
6210
+ httpUpstream: gateway.url,
6211
+ // WS forwarding upstream — daemon dials the local gateway's WS
6212
+ // endpoints (/sessions/:id/pty, etc) and pipes frames to the host.
6213
+ // Used by the cloud tunnel pod so browsers on mobile can attach to
6214
+ // interactive PTY sessions even though the daemon is only reachable
6215
+ // through the host (not directly).
6216
+ dialUpstreamWs: async ({ url, protocols, headers: headers2 }) => {
6217
+ const upstreamHeaders = {
6218
+ ...headers2
6219
+ };
6220
+ if (!upstreamHeaders["origin"] && !upstreamHeaders["Origin"]) {
6221
+ try {
6222
+ const u = new URL(url);
6223
+ const httpScheme = u.protocol === "wss:" ? "https:" : "http:";
6224
+ upstreamHeaders["Origin"] = `${httpScheme}//${u.host}`;
6225
+ } catch {
6226
+ }
6227
+ }
6228
+ return await new Promise((resolve4, reject) => {
6229
+ const sock = new WebSocket(url, protocols ? [...protocols] : void 0, {
6230
+ headers: upstreamHeaders
6231
+ });
6232
+ const onceOpen = () => {
6233
+ sock.off("error", onceError);
6234
+ sock.off("unexpected-response", onceUnexpected);
6235
+ resolve4({
6236
+ protocol: sock.protocol ?? "",
6237
+ send: (data, sendOpts) => {
6238
+ sock.send(data, { binary: sendOpts.binary });
6239
+ },
6240
+ close: (code, reason) => {
6241
+ try {
6242
+ sock.close(code, reason);
6243
+ } catch {
6244
+ }
6245
+ },
6246
+ onMessage: (handler) => {
6247
+ sock.on("message", (raw, isBinary) => {
6248
+ handler(raw, isBinary);
6249
+ });
6250
+ },
6251
+ onClose: (handler) => {
6252
+ sock.on("close", (code, reason) => {
6253
+ handler(code, reason.toString("utf8"));
6254
+ });
6255
+ },
6256
+ onError: (handler) => {
6257
+ sock.on("error", (err) => handler(err));
6258
+ }
6259
+ });
6260
+ };
6261
+ const onceError = (err) => {
6262
+ sock.off("open", onceOpen);
6263
+ reject(err);
6264
+ };
6265
+ const onceUnexpected = (_req, res) => {
6266
+ sock.off("open", onceOpen);
6267
+ reject(new Error(`Unexpected server response: ${res.statusCode ?? 0}`));
6268
+ };
6269
+ sock.once("open", onceOpen);
6270
+ sock.once("error", onceError);
6271
+ sock.once("unexpected-response", onceUnexpected);
6272
+ });
6273
+ },
4873
6274
  // v0 authorize hook: trust the bearer-authenticated host completely.
4874
6275
  // Token possession proves the host was provisioned for this daemon.
4875
6276
  // Per-spawn policy filtering will land alongside the policy.toml.
@@ -4889,33 +6290,93 @@ async function runOneTunnel(opts, gateway, signal) {
4889
6290
  kind: "agent-cli",
4890
6291
  label: `tunnel: ${request.command.split("/").pop() ?? request.command}`
4891
6292
  });
6293
+ },
6294
+ // Graceful drain hook — flip the outer loop's "reconnect immediately"
6295
+ // flag and close the WS so the supervisor reconnects without backoff.
6296
+ // Host will follow up with close(1012) ~2s later as a hard backstop.
6297
+ onReconnectSoon: ({ reasonMs }) => {
6298
+ process.stderr.write(
6299
+ `agentproto serve: host signaled drain (reasonMs=${reasonMs ?? "?"}) \u2014 reconnecting immediately
6300
+ `
6301
+ );
6302
+ reconnectState.immediate = true;
6303
+ try {
6304
+ ws.close(1e3, "host_drain");
6305
+ } catch {
6306
+ }
4892
6307
  }
4893
6308
  });
4894
- await new Promise((resolve3) => {
6309
+ await new Promise((resolve4) => {
4895
6310
  const offClose = sink.onClose(() => {
6311
+ clearInterval(keepaliveInterval);
4896
6312
  offClose();
4897
- resolve3();
6313
+ resolve4();
4898
6314
  });
4899
6315
  if (signal.aborted) {
4900
- server.close().finally(() => resolve3());
6316
+ clearInterval(keepaliveInterval);
6317
+ server.close().finally(() => resolve4());
4901
6318
  }
4902
6319
  signal.addEventListener("abort", () => {
4903
- server.close().finally(() => resolve3());
6320
+ clearInterval(keepaliveInterval);
6321
+ server.close().finally(() => resolve4());
4904
6322
  });
4905
6323
  });
4906
6324
  process.stderr.write(`agentproto serve: tunnel closed.
4907
6325
  `);
4908
6326
  }
4909
6327
  function sleep(ms, signal) {
4910
- return new Promise((resolve3) => {
4911
- if (signal.aborted) return resolve3();
4912
- const timer = setTimeout(resolve3, ms);
6328
+ return new Promise((resolve4) => {
6329
+ if (signal.aborted) return resolve4();
6330
+ const timer = setTimeout(resolve4, ms);
4913
6331
  signal.addEventListener("abort", () => {
4914
6332
  clearTimeout(timer);
4915
- resolve3();
6333
+ resolve4();
4916
6334
  });
4917
6335
  });
4918
6336
  }
6337
+ function printBootBanner(opts) {
6338
+ const c = color;
6339
+ const home = process.env.HOME ?? "";
6340
+ const workspace = home && opts.workspace.startsWith(home) ? "~" + opts.workspace.slice(home.length) : opts.workspace;
6341
+ const ptyState = opts.ptyEnabled ? `${c.green}enabled${c.reset} ${c.dim}(node-pty)${c.reset}` : `${c.amber}disabled${c.reset} ${c.dim}(install node-pty to enable)${c.reset}`;
6342
+ let origins;
6343
+ if (opts.strictOrigins) {
6344
+ origins = opts.allowedOrigins && opts.allowedOrigins.length > 0 ? `${c.amber}STRICT${c.reset} ${opts.allowedOrigins.join(" \xB7 ")} ${c.dim}(localhost defaults disabled)${c.reset}` : `${c.red}STRICT \xB7 empty allowlist${c.reset} ${c.dim}(every Origin will 401 \u2014 only Bearer-token works)${c.reset}`;
6345
+ } else {
6346
+ origins = opts.allowedOrigins && opts.allowedOrigins.length > 0 ? opts.allowedOrigins.join(" \xB7 ") + ` ${c.dim}+ localhost (default)${c.reset}` : `${c.dim}localhost:* only (default)${c.reset}`;
6347
+ }
6348
+ const mode = opts.connect ? `${c.cyan}tunnel${c.reset} ${c.dim}\u2192 ${opts.connect}${c.reset}` : `${c.dim}local-only${c.reset}`;
6349
+ const line = `${c.dim}\u2500${c.reset}`;
6350
+ process.stderr.write(
6351
+ `
6352
+ ${line} ${c.bold}agentproto${c.reset} ${c.dim}\xB7${c.reset} gateway up ${c.dim}\xB7${c.reset} ${c.cyan}${opts.url}${c.reset} ${line}
6353
+ ${c.dim}workspace${c.reset} ${workspace}
6354
+ ${c.dim}pty${c.reset} ${ptyState}
6355
+ ${c.dim}origins${c.reset} ${origins}
6356
+ ${c.dim}endpoints${c.reset} /mcp \xB7 /sessions \xB7 /events \xB7 /sessions/:id/pty ${c.dim}(WS)${c.reset}
6357
+ ${c.dim}mode${c.reset} ${mode}
6358
+
6359
+ `
6360
+ );
6361
+ }
6362
+ var _isTty = !!process.stderr.isTTY;
6363
+ var color = _isTty ? {
6364
+ reset: "\x1B[0m",
6365
+ bold: "\x1B[1m",
6366
+ dim: "\x1B[2m",
6367
+ green: "\x1B[32m",
6368
+ amber: "\x1B[33m",
6369
+ cyan: "\x1B[36m",
6370
+ red: "\x1B[31m"
6371
+ } : {
6372
+ reset: "",
6373
+ bold: "",
6374
+ dim: "",
6375
+ green: "",
6376
+ amber: "",
6377
+ cyan: "",
6378
+ red: ""
6379
+ };
4919
6380
 
4920
6381
  export { listInstalledAdapters, resolveAdapter, runInstall, runRun, runServe };
4921
6382
  //# sourceMappingURL=index.mjs.map