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

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/cli.mjs CHANGED
@@ -1,19 +1,27 @@
1
1
  #!/usr/bin/env node
2
- import { spawn } from 'child_process';
3
- import { userInfo, hostname, homedir, platform, tmpdir } from 'os';
4
- import { parseArgs } from 'util';
5
- import { readFile, mkdir, writeFile, chmod, mkdtemp, rm, unlink } from 'fs/promises';
6
- import { resolve, basename, join, dirname } from 'path';
7
- import { createHash } from 'crypto';
2
+ import { execFile, spawn } from 'child_process';
3
+ import { homedir, userInfo, hostname, platform, tmpdir } from 'os';
4
+ import { promisify, parseArgs } from 'util';
5
+ import { readFile, readdir, writeFile, mkdir, chmod, mkdtemp, rm, unlink, stat } from 'fs/promises';
6
+ import { resolve, basename, dirname, isAbsolute, join, normalize, relative } from 'path';
7
+ import { randomUUID, randomBytes, createHash } from 'crypto';
8
8
  import { promises, existsSync } from 'fs';
9
9
  import { createInterface } from 'readline/promises';
10
10
  import { stdout, stdin } from 'process';
11
11
  import { createAgentCliRuntime } from '@agentproto/driver-agent-cli';
12
12
  import { wrapWebSocket, createTunnelServer } from '@agentproto/acp/tunnel';
13
- import { createGateway } from '@agentproto/runtime';
13
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
14
+ import '@modelcontextprotocol/sdk/server/stdio.js';
15
+ import { z } from 'zod';
16
+ import matter from 'gray-matter';
17
+ import { EventEmitter } from 'events';
18
+ import http, { createServer } from 'http';
19
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
20
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
21
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
22
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
23
+ import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
14
24
  import WebSocket from 'ws';
15
- import { DEFAULT_CONFIG_PATH, loadWorkspacesConfig, getActiveWorkspace, setActiveWorkspace, saveWorkspacesConfig, sanitizeSlug, removeWorkspace, addWorkspace } from '@agentproto/runtime/workspaces-config';
16
- import http from 'http';
17
25
  import https from 'https';
18
26
 
19
27
  /**
@@ -423,15 +431,15 @@ async function postForm(url, body) {
423
431
  body: params.toString()
424
432
  });
425
433
  if (!res.ok && res.status !== 400) {
426
- const text = await res.text().catch(() => "");
434
+ const text3 = await res.text().catch(() => "");
427
435
  throw new Error(
428
- `POST ${url} \u2192 ${res.status} ${res.statusText}${text ? ": " + text.slice(0, 200) : ""}`
436
+ `POST ${url} \u2192 ${res.status} ${res.statusText}${text3 ? ": " + text3.slice(0, 200) : ""}`
429
437
  );
430
438
  }
431
439
  return await res.json();
432
440
  }
433
441
  function sleep(ms) {
434
- return new Promise((resolve3) => setTimeout(resolve3, ms));
442
+ return new Promise((resolve6) => setTimeout(resolve6, ms));
435
443
  }
436
444
  function formatDuration(ms) {
437
445
  if (ms < 6e4) return `${Math.round(ms / 1e3)}s`;
@@ -442,16 +450,16 @@ function openBrowser(url) {
442
450
  const p = platform();
443
451
  const cmd = p === "darwin" ? "open" : p === "win32" ? "cmd" : "xdg-open";
444
452
  const args = p === "win32" ? ["/c", "start", url] : [url];
445
- return new Promise((resolve3) => {
453
+ return new Promise((resolve6) => {
446
454
  try {
447
455
  const child = spawn(cmd, args, { stdio: "ignore", detached: true });
448
- child.once("error", () => resolve3());
456
+ child.once("error", () => resolve6());
449
457
  child.once("spawn", () => {
450
458
  child.unref();
451
- resolve3();
459
+ resolve6();
452
460
  });
453
461
  } catch {
454
- resolve3();
462
+ resolve6();
455
463
  }
456
464
  });
457
465
  }
@@ -530,8 +538,8 @@ async function collectAgentprotoNamespaceRoots(start) {
530
538
  for (let depth = 0; depth < 16; depth++) {
531
539
  for (const candidate of candidatesAt(cur)) {
532
540
  try {
533
- const stat = await promises.stat(candidate);
534
- if (stat.isDirectory()) roots.push(candidate);
541
+ const stat2 = await promises.stat(candidate);
542
+ if (stat2.isDirectory()) roots.push(candidate);
535
543
  } catch {
536
544
  }
537
545
  }
@@ -714,10 +722,10 @@ async function runExternalStep(step, ledger, head) {
714
722
  `);
715
723
  const opener = platform() === "darwin" ? "open" : platform() === "win32" ? "cmd" : "xdg-open";
716
724
  const args = platform() === "win32" ? ["/c", "start", step.url] : [step.url];
717
- await new Promise((resolve3) => {
725
+ await new Promise((resolve6) => {
718
726
  const child = spawn(opener, args, { stdio: "ignore", detached: true });
719
- child.once("error", () => resolve3());
720
- child.once("spawn", () => resolve3());
727
+ child.once("error", () => resolve6());
728
+ child.once("spawn", () => resolve6());
721
729
  });
722
730
  let value = "";
723
731
  if (step.callback?.param) {
@@ -790,7 +798,7 @@ async function promptString(question, opts) {
790
798
  process.stdin.setRawMode?.(true);
791
799
  let buf = "";
792
800
  process.stdout.write(prompt);
793
- return new Promise((resolve3) => {
801
+ return new Promise((resolve6) => {
794
802
  const onData = (chunk) => {
795
803
  for (const code of chunk) {
796
804
  if (code === 13 || code === 10) {
@@ -798,7 +806,7 @@ async function promptString(question, opts) {
798
806
  process.stdin.setRawMode?.(false);
799
807
  process.stdout.write("\n");
800
808
  rl.close();
801
- resolve3(buf || opts.defaultValue || "");
809
+ resolve6(buf || opts.defaultValue || "");
802
810
  return;
803
811
  }
804
812
  if (code === 3) {
@@ -880,7 +888,7 @@ async function resolveSelectOptions(options) {
880
888
  });
881
889
  }
882
890
  async function runShellCapturing(cmd, opts) {
883
- return new Promise((resolve3) => {
891
+ return new Promise((resolve6) => {
884
892
  const child = spawn("bash", ["-lc", cmd], {
885
893
  stdio: opts.interactive ? "inherit" : ["ignore", "pipe", "pipe"]
886
894
  });
@@ -898,11 +906,11 @@ async function runShellCapturing(cmd, opts) {
898
906
  }, opts.timeoutMs);
899
907
  child.once("error", () => {
900
908
  clearTimeout(timer);
901
- resolve3({ exitCode: 127, stdout, stderr: stderr || "spawn error" });
909
+ resolve6({ exitCode: 127, stdout, stderr: stderr || "spawn error" });
902
910
  });
903
911
  child.once("exit", (code) => {
904
912
  clearTimeout(timer);
905
- resolve3({ exitCode: code ?? 0, stdout, stderr });
913
+ resolve6({ exitCode: code ?? 0, stdout, stderr });
906
914
  });
907
915
  });
908
916
  }
@@ -1261,15 +1269,15 @@ function homedir3() {
1261
1269
  return process.env["HOME"] ?? process.env["USERPROFILE"] ?? "/";
1262
1270
  }
1263
1271
  function spawnInherit(cmd, argv) {
1264
- return new Promise((resolve3, reject) => {
1272
+ return new Promise((resolve6, reject) => {
1265
1273
  const child = spawn(cmd, argv, { stdio: "inherit" });
1266
1274
  child.once("error", reject);
1267
- child.once("exit", (code) => resolve3(code ?? 0));
1275
+ child.once("exit", (code) => resolve6(code ?? 0));
1268
1276
  });
1269
1277
  }
1270
1278
  async function runVersionCheck(check) {
1271
1279
  if (!check) return { ok: false, message: "no version_check declared" };
1272
- return new Promise((resolve3) => {
1280
+ return new Promise((resolve6) => {
1273
1281
  const child = spawn("bash", ["-lc", check.cmd], {
1274
1282
  stdio: ["ignore", "pipe", "ignore"]
1275
1283
  });
@@ -1277,19 +1285,19 @@ async function runVersionCheck(check) {
1277
1285
  child.stdout.on("data", (c) => {
1278
1286
  buf += c.toString("utf8");
1279
1287
  });
1280
- child.once("error", () => resolve3({ ok: false, message: "check failed" }));
1288
+ child.once("error", () => resolve6({ ok: false, message: "check failed" }));
1281
1289
  child.once("exit", (code) => {
1282
1290
  if (code !== 0) {
1283
- resolve3({ ok: false, message: `check exited ${code}` });
1291
+ resolve6({ ok: false, message: `check exited ${code}` });
1284
1292
  return;
1285
1293
  }
1286
1294
  const re = new RegExp(check.parse);
1287
1295
  const m = buf.match(re);
1288
1296
  if (!m || !m[1]) {
1289
- resolve3({ ok: false, message: "could not parse version" });
1297
+ resolve6({ ok: false, message: "could not parse version" });
1290
1298
  return;
1291
1299
  }
1292
- resolve3({ ok: true, message: `version ${m[1]}` });
1300
+ resolve6({ ok: true, message: `version ${m[1]}` });
1293
1301
  });
1294
1302
  });
1295
1303
  }
@@ -1301,8 +1309,8 @@ async function readStdinIfPiped() {
1301
1309
  for await (const chunk of process.stdin) {
1302
1310
  chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
1303
1311
  }
1304
- const text = Buffer.concat(chunks).toString("utf8").trim();
1305
- return text.length > 0 ? text : null;
1312
+ const text3 = Buffer.concat(chunks).toString("utf8").trim();
1313
+ return text3.length > 0 ? text3 : null;
1306
1314
  }
1307
1315
 
1308
1316
  // src/commands/run.ts
@@ -1425,6 +1433,3664 @@ ${JSON.stringify(rest, null, 2)}\x1B[0m
1425
1433
  }
1426
1434
  }
1427
1435
  }
1436
+
1437
+ // ../define-doctype/dist/index.mjs
1438
+ var DEFAULT_ID_PATTERN = /^[a-z0-9][a-z0-9._-]{1,79}$/;
1439
+ var MAX_DESCRIPTION_LEN = 2e3;
1440
+ function createDoctype(opts) {
1441
+ const idPattern = opts.idPattern ?? DEFAULT_ID_PATTERN;
1442
+ const readIdentity = opts.readIdentity ?? ((def) => def.id);
1443
+ const readDescription = opts.readDescription === false ? null : opts.readDescription ?? ((def) => def.description);
1444
+ const maxLen = opts.maxDescriptionLen ?? MAX_DESCRIPTION_LEN;
1445
+ const prefix = `define${capitalize(opts.name)}`;
1446
+ const aipTag = `(AIP-${opts.aip})`;
1447
+ return function constructDoctype(def) {
1448
+ const identity = readIdentity(def);
1449
+ if (typeof identity !== "string" || !idPattern.test(identity)) {
1450
+ throw new Error(
1451
+ `${prefix} ${aipTag}: invalid id '${String(
1452
+ identity
1453
+ )}' \u2014 must match ${idPattern}`
1454
+ );
1455
+ }
1456
+ if (readDescription) {
1457
+ const description = readDescription(def);
1458
+ if (typeof description !== "string" || description.length === 0 || description.length > maxLen) {
1459
+ throw new Error(
1460
+ `${prefix} ${aipTag}: id='${identity}' description must be 1\u2013${maxLen} chars`
1461
+ );
1462
+ }
1463
+ }
1464
+ opts.validate?.(def);
1465
+ return Object.freeze(opts.build(def));
1466
+ };
1467
+ }
1468
+ function capitalize(s) {
1469
+ return s.length === 0 ? s : s.charAt(0).toUpperCase() + s.slice(1);
1470
+ }
1471
+ function filterSerializable(value) {
1472
+ if (value === null || value === void 0) return value;
1473
+ if (typeof value === "function") return void 0;
1474
+ if (typeof value === "object" && value !== null && Object.prototype.hasOwnProperty.call(value, "_def") && "parse" in value && typeof value.parse === "function") {
1475
+ return void 0;
1476
+ }
1477
+ if (Array.isArray(value)) {
1478
+ return value.map(filterSerializable).filter((v) => v !== void 0);
1479
+ }
1480
+ if (typeof value === "object") {
1481
+ const out = {};
1482
+ for (const [k, v] of Object.entries(value)) {
1483
+ const filtered = filterSerializable(v);
1484
+ if (filtered !== void 0) out[k] = filtered;
1485
+ }
1486
+ return out;
1487
+ }
1488
+ return value;
1489
+ }
1490
+ var DEFAULT_SKIP_DIRS = [
1491
+ "node_modules",
1492
+ ".git",
1493
+ "dist",
1494
+ ".next",
1495
+ ".turbo"
1496
+ ];
1497
+ function createVerbs(spec) {
1498
+ const filename = spec.filename ?? `${spec.name.toUpperCase()}.md`;
1499
+ const toFrontmatter = spec.toFrontmatter ?? ((params) => filterSerializable({
1500
+ schema: spec.schemaLiteral,
1501
+ ...params
1502
+ }));
1503
+ async function create(params, opts) {
1504
+ const handle = spec.define(params);
1505
+ const relativePath = spec.pathOf(handle);
1506
+ const path = join(opts.dir, relativePath);
1507
+ const frontmatter = toFrontmatter(params);
1508
+ const rendered = matter.stringify(
1509
+ opts.body ?? defaultBody(spec, frontmatter),
1510
+ frontmatter
1511
+ );
1512
+ if (!opts.dryRun) {
1513
+ await mkdir(dirname(path), { recursive: true });
1514
+ await writeFile(path, rendered, "utf8");
1515
+ }
1516
+ return { path, handle, rendered };
1517
+ }
1518
+ async function load(path) {
1519
+ const source = await readFile(path, "utf8");
1520
+ const { frontmatter, body } = spec.parse(source);
1521
+ const handle = spec.define(frontmatter);
1522
+ return { path, handle, body };
1523
+ }
1524
+ async function list(dir, opts = {}) {
1525
+ const skip = new Set(opts.skipDirs ?? DEFAULT_SKIP_DIRS);
1526
+ const out = [];
1527
+ async function walk(current) {
1528
+ let entries;
1529
+ try {
1530
+ entries = await readdir(current, { withFileTypes: true });
1531
+ } catch {
1532
+ return;
1533
+ }
1534
+ for (const entry of entries) {
1535
+ const entryName = String(entry.name);
1536
+ if (entry.isDirectory()) {
1537
+ if (skip.has(entryName)) continue;
1538
+ await walk(join(current, entryName));
1539
+ continue;
1540
+ }
1541
+ if (!entry.isFile()) continue;
1542
+ if (entryName !== filename) continue;
1543
+ try {
1544
+ const { handle } = await load(join(current, entryName));
1545
+ if (!opts.filter || opts.filter(handle)) out.push(handle);
1546
+ } catch {
1547
+ }
1548
+ }
1549
+ }
1550
+ await walk(dir);
1551
+ return out;
1552
+ }
1553
+ async function update(path, mutator, opts = {}) {
1554
+ const { handle, body } = await load(path);
1555
+ const source = await readFile(path, "utf8");
1556
+ const { frontmatter } = spec.parse(source);
1557
+ const params = frontmatter;
1558
+ const mutated = await mutator(params, { handle, body });
1559
+ const newHandle = spec.define(mutated);
1560
+ const newFrontmatter = toFrontmatter(mutated);
1561
+ const rendered = matter.stringify(
1562
+ opts.body ?? body,
1563
+ newFrontmatter
1564
+ );
1565
+ if (!opts.dryRun) {
1566
+ await mkdir(dirname(path), { recursive: true });
1567
+ await writeFile(path, rendered, "utf8");
1568
+ }
1569
+ return { path, handle: newHandle, rendered };
1570
+ }
1571
+ async function resolve22(block, ctx = {}) {
1572
+ if ("inline" in block) {
1573
+ return spec.define(block.inline);
1574
+ }
1575
+ if ("file" in block) {
1576
+ const baseDir = ctx.baseDir ?? ".";
1577
+ const path = isAbsolute(block.file) ? block.file : join(baseDir, block.file);
1578
+ const { handle } = await load(path);
1579
+ return handle;
1580
+ }
1581
+ if ("ref" in block) {
1582
+ if (!ctx.resolveRef) {
1583
+ throw new Error(
1584
+ `${spec.name}.resolve (AIP-${spec.aip}): block has \`ref: '${block.ref}'\` but no resolveRef provided in context \u2014 registries must inject their own resolver`
1585
+ );
1586
+ }
1587
+ const resolved = await ctx.resolveRef(block.ref);
1588
+ return spec.define(resolved);
1589
+ }
1590
+ throw new Error(
1591
+ `${spec.name}.resolve (AIP-${spec.aip}): block must have one of inline | ref | file`
1592
+ );
1593
+ }
1594
+ async function deleteFn(path) {
1595
+ await rm(path, { force: true });
1596
+ }
1597
+ return { create, load, list, update, resolve: resolve22, delete: deleteFn };
1598
+ }
1599
+ function defaultBody(spec, frontmatter) {
1600
+ const id = typeof frontmatter.name === "string" ? frontmatter.name : typeof frontmatter.id === "string" ? frontmatter.id : typeof frontmatter.slug === "string" ? frontmatter.slug : spec.name;
1601
+ return `# ${id}
1602
+ `;
1603
+ }
1604
+ var extensionFrontmatterSchema = z.object({ "schema": z.literal("agentproto/extension/v1").describe("Pins the spec version this manifest conforms to."), "slug": z.string().regex(new RegExp("^[a-z][a-z0-9-]*:[a-z][a-z0-9-]*[a-z0-9]$")).describe("Namespaced identifier \u2014 `<namespace>:<name>`. Lowercase, digits, dashes; single colon separator. Example: `acme:deal`."), "title": z.string().min(1).max(80).describe("Human-readable display name."), "description": z.string().min(1).max(2e3).describe("One-paragraph statement of the extension's purpose."), "version": z.string().regex(new RegExp("^\\d+\\.\\d+\\.\\d+(?:[-+][a-zA-Z0-9.-]+)?$")).describe("Extension's own semver. Bump on breaking change."), "status": z.literal("Local").describe("Extensions are perpetually Local \u2014 they never enter the public registry's Draft/Review/Final lifecycle."), "extends": z.union([z.string().regex(new RegExp("^aip-\\d+$")).describe("Inherit a public AIP's schema (e.g. `aip-14`)."), z.literal("none").describe("Brand-new doctype, no inheritance.")]).describe("Parent AIP this extension inherits from, or `none` for a root doctype."), "add_fields": z.object({ "properties": z.record(z.string(), z.any()).describe("JSON Schema property definitions to merge into the parent's properties. Collisions are an error \u2014 use `tighten` to narrow an existing field.").optional(), "required": z.array(z.string()).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), { message: "All items must be unique!" }).describe("Field names that become required in addition to parent's required[]. Unioned with parent's required, never replaces.").default([]) }).strict().describe("Schema-level additions: new properties + new required entries.").optional(), "tighten": z.record(z.string(), z.object({ "pattern": z.string().optional(), "enum": z.array(z.any()).optional(), "minLength": z.number().int().gte(0).optional(), "maxLength": z.number().int().gte(0).optional(), "minimum": z.number().optional(), "maximum": z.number().optional() }).strict()).describe("Per-field constraint overrides. Each entry MUST tighten (not loosen) the parent's constraints; runtimes verify monotonicity at registration.").optional(), "defaults": z.record(z.string(), z.any()).describe("Default values applied when a field is omitted. Layered on top of parent's defaults; extension's value wins on the same key.").optional(), "path_convention": z.string().min(1).describe("Filesystem convention template, e.g. `\"deals/<slug>/DEAL.md\"`. Tokens: `<slug>` (doctype identity), `<DOCTYPE>` (extension's slug name part). Falls back to parent's convention when omitted.").optional(), "requires": z.array(z.number().int().gte(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), { message: "All items must be unique!" }).describe("Public AIPs the extension depends on, in addition to its parent.").default([]), "metadata": z.record(z.string(), z.any()).describe("Free-form vendor extensions under namespaced keys (`metadata.<vendor>.<field>`).").default({}) }).strict().describe("Validates the YAML frontmatter portion of an AIP-40 EXTENSION.md manifest. An extension declares a workspace-local doctype that inherits a public AIP's schema and may add fields, tighten constraints, set defaults, and override the path convention.");
1605
+ createDoctype({
1606
+ aip: 40,
1607
+ name: "extension",
1608
+ // Extension slugs are namespaced: `<namespace>:<name>` (e.g.
1609
+ // `acme:deal`). Override the default kebab-only pattern.
1610
+ idPattern: /^[a-z][a-z0-9-]*:[a-z][a-z0-9-]*[a-z0-9]$/,
1611
+ readIdentity: (def) => def.slug,
1612
+ validate(def) {
1613
+ const result = extensionFrontmatterSchema.safeParse(def);
1614
+ if (!result.success) {
1615
+ throw new Error(
1616
+ `defineExtension (AIP-40): ${result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
1617
+ );
1618
+ }
1619
+ },
1620
+ build(def) {
1621
+ return { ...def };
1622
+ }
1623
+ });
1624
+ function parseExtensionManifest(source) {
1625
+ const parsed = matter(source);
1626
+ if (Object.keys(parsed.data).length === 0) {
1627
+ throw new Error("parseExtensionManifest: missing or empty frontmatter");
1628
+ }
1629
+ const result = extensionFrontmatterSchema.safeParse(parsed.data);
1630
+ if (!result.success) {
1631
+ throw new Error(
1632
+ `parseExtensionManifest: invalid frontmatter \u2014 ${result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
1633
+ );
1634
+ }
1635
+ return { frontmatter: result.data, body: parsed.content };
1636
+ }
1637
+
1638
+ // ../extension/dist/index.mjs
1639
+ function specFromExtension(extension, opts = {}) {
1640
+ const { parent } = opts;
1641
+ const isRoot = extension.extends === "none";
1642
+ if (!isRoot && !parent) {
1643
+ throw new Error(
1644
+ `specFromExtension (AIP-40): extension '${extension.slug}' extends '${extension.extends}' but no parent spec was provided in opts.parent`
1645
+ );
1646
+ }
1647
+ if (isRoot && !extension.path_convention) {
1648
+ throw new Error(
1649
+ `specFromExtension (AIP-40): extension '${extension.slug}' is a root doctype (extends: none) and MUST declare path_convention`
1650
+ );
1651
+ }
1652
+ if (parent && extension.tighten) {
1653
+ verifyTightening(extension);
1654
+ }
1655
+ const slugName = extension.slug.split(":")[1] ?? extension.slug;
1656
+ const pathTemplate = extension.path_convention ?? null;
1657
+ const extensionKeys = new Set(
1658
+ Object.keys(extension.add_fields?.properties ?? {})
1659
+ );
1660
+ const define = (params) => {
1661
+ const withDefaults = { ...params };
1662
+ if (extension.defaults) {
1663
+ for (const [key, value] of Object.entries(extension.defaults)) {
1664
+ if (withDefaults[key] === void 0 && value !== void 0) {
1665
+ withDefaults[key] = value;
1666
+ }
1667
+ }
1668
+ }
1669
+ if (isRoot) {
1670
+ return Object.freeze(withDefaults);
1671
+ }
1672
+ const parentOnly = {};
1673
+ const extensionOnly = {};
1674
+ for (const [key, value] of Object.entries(withDefaults)) {
1675
+ if (extensionKeys.has(key)) extensionOnly[key] = value;
1676
+ else parentOnly[key] = value;
1677
+ }
1678
+ const parentHandle = parent.define(parentOnly);
1679
+ return Object.freeze({
1680
+ ...parentHandle,
1681
+ ...extensionOnly
1682
+ });
1683
+ };
1684
+ const parse = isRoot ? rootParse(extension) : parent.parse;
1685
+ const pathOf = (handle) => {
1686
+ if (pathTemplate) {
1687
+ return resolvePathTemplate(
1688
+ pathTemplate,
1689
+ handle,
1690
+ slugName
1691
+ );
1692
+ }
1693
+ return parent.pathOf(handle);
1694
+ };
1695
+ return {
1696
+ name: extension.slug,
1697
+ aip: 40,
1698
+ schemaLiteral: parent?.schemaLiteral ?? `agentproto/extension/v1`,
1699
+ pathOf,
1700
+ define,
1701
+ parse
1702
+ };
1703
+ }
1704
+ function verifyTightening(extension, parent) {
1705
+ const t = extension.tighten ?? {};
1706
+ for (const [field, override] of Object.entries(t)) {
1707
+ if (typeof override.minLength === "number" && typeof override.maxLength === "number" && override.minLength > override.maxLength) {
1708
+ throw new Error(
1709
+ `specFromExtension (AIP-40): extension '${extension.slug}' tighten.${field}: minLength (${override.minLength}) > maxLength (${override.maxLength})`
1710
+ );
1711
+ }
1712
+ if (typeof override.minimum === "number" && typeof override.maximum === "number" && override.minimum > override.maximum) {
1713
+ throw new Error(
1714
+ `specFromExtension (AIP-40): extension '${extension.slug}' tighten.${field}: minimum (${override.minimum}) > maximum (${override.maximum})`
1715
+ );
1716
+ }
1717
+ if (override.enum !== void 0 && !Array.isArray(override.enum)) {
1718
+ throw new Error(
1719
+ `specFromExtension (AIP-40): extension '${extension.slug}' tighten.${field}: enum must be an array`
1720
+ );
1721
+ }
1722
+ }
1723
+ }
1724
+ function rootParse(extension) {
1725
+ return (source) => {
1726
+ throw new Error(
1727
+ `specFromExtension (AIP-40): extension '${extension.slug}' is a root doctype \u2014 supply your own parser or extend a public AIP for parser inheritance`
1728
+ );
1729
+ };
1730
+ }
1731
+ function resolvePathTemplate(template, handle, doctypeSlug) {
1732
+ const identity = typeof handle.id === "string" && handle.id || typeof handle.slug === "string" && handle.slug || typeof handle.name === "string" && handle.name || "unknown";
1733
+ return template.replace(/<slug>/g, identity).replace(/<DOCTYPE>/g, doctypeSlug.toUpperCase());
1734
+ }
1735
+
1736
+ // ../mcp-server/dist/index.mjs
1737
+ async function createMcpServer(opts) {
1738
+ const server = new McpServer({
1739
+ name: opts.name ?? "agentproto-mcp-server",
1740
+ version: opts.version ?? "0.1.0-alpha"
1741
+ });
1742
+ const allSpecs = [...opts.specs];
1743
+ if (opts.workspace) {
1744
+ const extensions = await loadExtensions(opts.workspace, opts.specs);
1745
+ allSpecs.push(...extensions);
1746
+ }
1747
+ const anchor = (p) => isAbsolute(p) || !opts.workspace ? p : join(opts.workspace, p);
1748
+ for (const spec of allSpecs) {
1749
+ registerVerbs(server, spec, anchor);
1750
+ }
1751
+ return {
1752
+ server,
1753
+ registered: allSpecs.map((s) => s.name)
1754
+ };
1755
+ }
1756
+ function registerVerbs(server, spec, anchor) {
1757
+ const verbs = createVerbs(spec);
1758
+ const verbName = (verb) => `${verb}_${spec.name.replace(/[-:]/g, "_")}`;
1759
+ const description = (verb, body) => `${verb} the AIP-${spec.aip} ${spec.name} doctype. ${body}`;
1760
+ server.tool(
1761
+ verbName("create"),
1762
+ description(
1763
+ "create",
1764
+ "Author a new manifest from params. Returns { path, rendered }."
1765
+ ),
1766
+ {
1767
+ params: z.record(z.string(), z.unknown()).describe(`The ${spec.name} definition fields.`),
1768
+ dir: z.string().describe("Workspace-relative or absolute target directory."),
1769
+ body: z.string().optional().describe("Markdown body after the frontmatter. Defaults to a stub."),
1770
+ dryRun: z.boolean().optional().describe("Render only \u2014 don't write to disk.")
1771
+ },
1772
+ async ({ params, dir, body, dryRun }) => {
1773
+ const result = await verbs.create(params, {
1774
+ dir: anchor(dir),
1775
+ body,
1776
+ dryRun
1777
+ });
1778
+ return contentText({
1779
+ path: result.path,
1780
+ rendered: result.rendered
1781
+ });
1782
+ }
1783
+ );
1784
+ server.tool(
1785
+ verbName("load"),
1786
+ description("load", "Read a manifest from disk. Returns the parsed handle."),
1787
+ {
1788
+ path: z.string().describe("Absolute or workspace-relative path to the .md file.")
1789
+ },
1790
+ async ({ path }) => {
1791
+ const result = await verbs.load(anchor(path));
1792
+ return contentText({ path: result.path, handle: result.handle });
1793
+ }
1794
+ );
1795
+ server.tool(
1796
+ verbName("list"),
1797
+ description(
1798
+ "list",
1799
+ "Walk a directory tree and return all manifests of this doctype."
1800
+ ),
1801
+ {
1802
+ dir: z.string().describe("Directory to walk."),
1803
+ skipDirs: z.array(z.string()).optional().describe("Subdir names to skip (default: node_modules, .git, dist).")
1804
+ },
1805
+ async ({ dir, skipDirs }) => {
1806
+ const handles = await verbs.list(anchor(dir), { skipDirs });
1807
+ return contentText({ count: handles.length, handles });
1808
+ }
1809
+ );
1810
+ server.tool(
1811
+ verbName("update"),
1812
+ description(
1813
+ "update",
1814
+ "Patch an existing manifest. The patch is shallow-merged into the existing params."
1815
+ ),
1816
+ {
1817
+ path: z.string().describe("Path to the manifest to update."),
1818
+ patch: z.record(z.string(), z.unknown()).describe("Partial fields to merge into the existing manifest."),
1819
+ body: z.string().optional().describe("New body markdown (default: keep existing).")
1820
+ },
1821
+ async ({ path, patch, body }) => {
1822
+ const result = await verbs.update(
1823
+ anchor(path),
1824
+ (existing) => ({ ...existing, ...patch }),
1825
+ { body }
1826
+ );
1827
+ return contentText({ path: result.path, rendered: result.rendered });
1828
+ }
1829
+ );
1830
+ server.tool(
1831
+ verbName("resolve"),
1832
+ description(
1833
+ "resolve",
1834
+ "Resolve an inline | ref | file block to a fully-typed handle."
1835
+ ),
1836
+ {
1837
+ block: z.union([
1838
+ z.object({ inline: z.record(z.string(), z.unknown()) }),
1839
+ z.object({ ref: z.string() }),
1840
+ z.object({ file: z.string() })
1841
+ ]).describe("The composition block."),
1842
+ baseDir: z.string().optional().describe("Base dir for `file:` references.")
1843
+ },
1844
+ async ({ block, baseDir }) => {
1845
+ const handle = await verbs.resolve(block, {
1846
+ baseDir: baseDir ? anchor(baseDir) : void 0
1847
+ });
1848
+ return contentText({ handle });
1849
+ }
1850
+ );
1851
+ server.tool(
1852
+ verbName("delete"),
1853
+ description("delete", "Remove a manifest file from disk."),
1854
+ {
1855
+ path: z.string().describe("Path to the manifest to delete.")
1856
+ },
1857
+ async ({ path }) => {
1858
+ const target = anchor(path);
1859
+ await verbs.delete(target);
1860
+ return contentText({ deleted: target });
1861
+ }
1862
+ );
1863
+ }
1864
+ async function loadExtensions(workspace, specs) {
1865
+ const extDir = resolve(workspace, "extensions");
1866
+ if (!existsSync(extDir)) return [];
1867
+ const out = [];
1868
+ const { readdir: readdir3 } = await import('fs/promises');
1869
+ let entries;
1870
+ try {
1871
+ entries = await readdir3(extDir, {
1872
+ withFileTypes: true
1873
+ });
1874
+ } catch {
1875
+ return out;
1876
+ }
1877
+ for (const entry of entries) {
1878
+ if (!entry.isDirectory()) continue;
1879
+ const manifestPath = join(extDir, String(entry.name), "EXTENSION.md");
1880
+ if (!existsSync(manifestPath)) continue;
1881
+ const source = await readFile(manifestPath, "utf8");
1882
+ const parsed = parseExtensionManifest(source);
1883
+ const ext = parsed.frontmatter;
1884
+ let parent;
1885
+ if (ext.extends && ext.extends !== "none") {
1886
+ const aipMatch = ext.extends.match(/^aip-(\d+)$/);
1887
+ if (!aipMatch) {
1888
+ throw new Error(
1889
+ `mcp-server: extension '${ext.slug}' has invalid extends '${ext.extends}'`
1890
+ );
1891
+ }
1892
+ const parentAip = Number(aipMatch[1]);
1893
+ parent = specs.find((s) => s.aip === parentAip);
1894
+ if (!parent) {
1895
+ throw new Error(
1896
+ `mcp-server: extension '${ext.slug}' extends aip-${parentAip}, but no spec for that AIP was registered. Pass the parent spec in opts.specs.`
1897
+ );
1898
+ }
1899
+ }
1900
+ out.push(specFromExtension(ext, { parent }));
1901
+ }
1902
+ return out;
1903
+ }
1904
+ function contentText(payload) {
1905
+ return {
1906
+ content: [
1907
+ {
1908
+ type: "text",
1909
+ text: JSON.stringify(payload, null, 2)
1910
+ }
1911
+ ]
1912
+ };
1913
+ }
1914
+ async function writeRuntimeMeta(workspace, meta) {
1915
+ const dir = join(workspace, ".agentproto");
1916
+ try {
1917
+ await mkdir(dir, { recursive: true });
1918
+ await writeFile(
1919
+ join(dir, "runtime.json"),
1920
+ JSON.stringify(meta, null, 2) + "\n",
1921
+ "utf8"
1922
+ );
1923
+ } catch (err) {
1924
+ console.error("[runtime] failed to write .agentproto/runtime.json:", err);
1925
+ }
1926
+ }
1927
+ var DEFAULT_TIMEOUT_MS = 6e4;
1928
+ var MAX_TIMEOUT_MS = 6e5;
1929
+ var ALLOWLIST_REL = ".agentproto/allowed-commands.json";
1930
+ var allowlistCache = null;
1931
+ async function loadAllowlist(workspace) {
1932
+ const path = resolve(workspace, ALLOWLIST_REL);
1933
+ if (!existsSync(path)) {
1934
+ allowlistCache = null;
1935
+ return /* @__PURE__ */ new Set();
1936
+ }
1937
+ try {
1938
+ const s = await stat(path);
1939
+ if (allowlistCache && allowlistCache.path === path && allowlistCache.entry.mtimeMs === s.mtimeMs) {
1940
+ return allowlistCache.entry.commands;
1941
+ }
1942
+ const raw = await readFile(path, "utf8");
1943
+ const parsed = JSON.parse(raw);
1944
+ const list = Array.isArray(parsed.commands) ? parsed.commands : [];
1945
+ const commands = new Set(
1946
+ list.filter((x) => typeof x === "string" && x.length > 0).map((x) => x.trim())
1947
+ );
1948
+ allowlistCache = { path, entry: { mtimeMs: s.mtimeMs, commands } };
1949
+ return commands;
1950
+ } catch (err) {
1951
+ console.error(
1952
+ `[runtime] failed to load ${ALLOWLIST_REL} (will deny all):`,
1953
+ err
1954
+ );
1955
+ allowlistCache = null;
1956
+ return /* @__PURE__ */ new Set();
1957
+ }
1958
+ }
1959
+ function makeCwdAnchor(workspace) {
1960
+ const root = resolve(workspace);
1961
+ return (input2) => {
1962
+ if (!input2 || input2.length === 0) return root;
1963
+ const candidate = isAbsolute(input2) ? normalize(input2) : normalize(join(root, input2));
1964
+ const rel = relative(root, candidate);
1965
+ if (rel.startsWith("..") || isAbsolute(rel)) {
1966
+ throw new Error(
1967
+ `cwd escapes the workspace: '${input2}' (workspace=${root})`
1968
+ );
1969
+ }
1970
+ return candidate;
1971
+ };
1972
+ }
1973
+ function registerCommandTools(server, opts) {
1974
+ const anchorCwd = makeCwdAnchor(opts.workspace);
1975
+ server.tool(
1976
+ "execute_command",
1977
+ "Run a shell command on the host running the runtime. The command basename must be in `<workspace>/.agentproto/allowed-commands.json`; default-deny otherwise. Captures stdout / stderr / exit code and returns them as JSON. Use this to drive local CLIs (Claude Code, gh, pnpm, \u2026) from a remote agent.",
1978
+ {
1979
+ command: z.string().min(1).describe(
1980
+ "Executable name or absolute path. Must be allowlisted by basename in the workspace's allowed-commands.json."
1981
+ ),
1982
+ args: z.array(z.string()).optional().describe(
1983
+ "Argv array. Passed verbatim \u2014 no shell expansion (we spawn with shell:false so quoting doesn't bite)."
1984
+ ),
1985
+ cwd: z.string().optional().describe(
1986
+ "Working directory, workspace-relative or an absolute path inside the workspace. Defaults to the workspace root."
1987
+ ),
1988
+ stdin: z.string().optional().describe("Optional input piped to the process's stdin."),
1989
+ timeoutMs: z.number().int().positive().max(MAX_TIMEOUT_MS).optional().describe(
1990
+ `Hard kill after this many ms. Defaults to ${DEFAULT_TIMEOUT_MS}; capped at ${MAX_TIMEOUT_MS}.`
1991
+ )
1992
+ },
1993
+ async ({ command, args, cwd, stdin, timeoutMs }) => {
1994
+ const allowlist = await loadAllowlist(opts.workspace);
1995
+ const baseName = basename(command);
1996
+ if (!allowlist.has(baseName)) {
1997
+ const allowed = [...allowlist].sort().join(", ") || "(empty)";
1998
+ throw new Error(
1999
+ `command '${baseName}' is not in the allowlist. Add it to ${join(opts.workspace, ALLOWLIST_REL)} under "commands": [...]. Currently allowed: ${allowed}.`
2000
+ );
2001
+ }
2002
+ const resolvedCwd = anchorCwd(cwd);
2003
+ const limit = Math.min(timeoutMs ?? DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS);
2004
+ const result = await runCommand({
2005
+ command,
2006
+ args: args ?? [],
2007
+ cwd: resolvedCwd,
2008
+ stdin,
2009
+ timeoutMs: limit
2010
+ });
2011
+ return {
2012
+ content: [{ type: "text", text: JSON.stringify(result) }]
2013
+ };
2014
+ }
2015
+ );
2016
+ }
2017
+ var STREAM_BUFFER_CAP = 1048576;
2018
+ async function runCommand(input2) {
2019
+ return new Promise((resolvePromise) => {
2020
+ const startedAt = Date.now();
2021
+ const child = spawn(input2.command, input2.args, {
2022
+ cwd: input2.cwd,
2023
+ shell: false,
2024
+ // Inherit user env so PATH lookups for `claude`, `gh`, etc. work.
2025
+ env: process.env,
2026
+ stdio: ["pipe", "pipe", "pipe"]
2027
+ });
2028
+ let stdout = "";
2029
+ let stderr = "";
2030
+ let truncated = false;
2031
+ let timedOut = false;
2032
+ function appendCapped(buf, chunk) {
2033
+ if (buf.length >= STREAM_BUFFER_CAP) {
2034
+ truncated = true;
2035
+ return buf;
2036
+ }
2037
+ const room = STREAM_BUFFER_CAP - buf.length;
2038
+ const text3 = chunk.toString("utf8");
2039
+ if (text3.length > room) {
2040
+ truncated = true;
2041
+ return buf + text3.slice(0, room);
2042
+ }
2043
+ return buf + text3;
2044
+ }
2045
+ child.stdout?.on("data", (d) => {
2046
+ stdout = appendCapped(stdout, d);
2047
+ });
2048
+ child.stderr?.on("data", (d) => {
2049
+ stderr = appendCapped(stderr, d);
2050
+ });
2051
+ if (input2.stdin) {
2052
+ child.stdin?.write(input2.stdin);
2053
+ }
2054
+ child.stdin?.end();
2055
+ const timer = setTimeout(() => {
2056
+ timedOut = true;
2057
+ try {
2058
+ child.kill("SIGTERM");
2059
+ } catch {
2060
+ }
2061
+ setTimeout(() => {
2062
+ try {
2063
+ child.kill("SIGKILL");
2064
+ } catch {
2065
+ }
2066
+ }, 2e3).unref();
2067
+ }, input2.timeoutMs);
2068
+ timer.unref();
2069
+ child.on("error", (err) => {
2070
+ clearTimeout(timer);
2071
+ resolvePromise({
2072
+ exitCode: -1,
2073
+ signal: null,
2074
+ stdout,
2075
+ stderr: stderr + (stderr ? "\n" : "") + err.message,
2076
+ truncated,
2077
+ durationMs: Date.now() - startedAt
2078
+ });
2079
+ });
2080
+ child.on("close", (code, signal) => {
2081
+ clearTimeout(timer);
2082
+ resolvePromise({
2083
+ exitCode: typeof code === "number" ? code : -1,
2084
+ signal: signal ?? (timedOut ? "SIGTERM-timeout" : null),
2085
+ stdout,
2086
+ stderr,
2087
+ truncated,
2088
+ durationMs: Date.now() - startedAt
2089
+ });
2090
+ });
2091
+ });
2092
+ }
2093
+ var FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n/;
2094
+ function fileConversationStore(opts) {
2095
+ const dir = opts.dir ?? join(opts.workspace, "conversations");
2096
+ const filePath = (id) => join(dir, `${id}.md`);
2097
+ return {
2098
+ pathFor: filePath,
2099
+ async open(id, meta) {
2100
+ await mkdir(dir, { recursive: true });
2101
+ const path = filePath(id);
2102
+ if (existsSync(path)) return;
2103
+ const header = renderHeader({
2104
+ id,
2105
+ agent: meta.agent,
2106
+ started: (/* @__PURE__ */ new Date()).toISOString(),
2107
+ status: "open"
2108
+ });
2109
+ await writeFile(path, header, "utf8");
2110
+ },
2111
+ async appendTurn(id, role, content, options) {
2112
+ const path = filePath(id);
2113
+ if (!existsSync(path)) {
2114
+ await this.open(id, { agent: options?.attribution ?? "unknown" });
2115
+ }
2116
+ const block = renderTurn({
2117
+ role,
2118
+ at: options?.at ?? (/* @__PURE__ */ new Date()).toISOString(),
2119
+ attribution: options?.attribution,
2120
+ content
2121
+ });
2122
+ await writeFile(path, block, { encoding: "utf8", flag: "a" });
2123
+ },
2124
+ async read(id) {
2125
+ const path = filePath(id);
2126
+ const source = await readFile(path, "utf8");
2127
+ return parseConversation(source, id);
2128
+ },
2129
+ async list() {
2130
+ if (!existsSync(dir)) return [];
2131
+ const entries = await readdir(dir, { withFileTypes: true });
2132
+ const out = [];
2133
+ for (const entry of entries) {
2134
+ if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
2135
+ const id = entry.name.slice(0, -3);
2136
+ try {
2137
+ const source = await readFile(join(dir, entry.name), "utf8");
2138
+ const { meta } = parseConversation(source, id);
2139
+ out.push({ id, meta });
2140
+ } catch {
2141
+ }
2142
+ }
2143
+ return out;
2144
+ }
2145
+ };
2146
+ }
2147
+ function renderHeader(meta) {
2148
+ return [
2149
+ "---",
2150
+ `schema: conversation/v1`,
2151
+ `id: ${meta.id}`,
2152
+ `agent: ${meta.agent}`,
2153
+ `started: ${meta.started}`,
2154
+ `status: ${meta.status}`,
2155
+ "---",
2156
+ "",
2157
+ ""
2158
+ ].join("\n");
2159
+ }
2160
+ function renderTurn(turn) {
2161
+ const heading = turn.attribution ? `## ${turn.role} \u2014 ${turn.at} (${turn.attribution})` : `## ${turn.role} \u2014 ${turn.at}`;
2162
+ return `${heading}
2163
+
2164
+ ${turn.content.trimEnd()}
2165
+
2166
+ `;
2167
+ }
2168
+ function parseConversation(source, fallbackId) {
2169
+ const match = source.match(FRONTMATTER_RE);
2170
+ let meta = {
2171
+ id: fallbackId,
2172
+ agent: "unknown",
2173
+ started: "",
2174
+ status: "open"
2175
+ };
2176
+ let body = source;
2177
+ if (match) {
2178
+ meta = parseFrontmatter(match[1] ?? "", fallbackId);
2179
+ body = source.slice(match[0].length);
2180
+ }
2181
+ const turns = [];
2182
+ const lines = body.split("\n");
2183
+ let current = null;
2184
+ let buffer = [];
2185
+ const flush = () => {
2186
+ if (!current) return;
2187
+ current.content = buffer.join("\n").trim();
2188
+ turns.push(current);
2189
+ current = null;
2190
+ buffer = [];
2191
+ };
2192
+ const headingRe = /^##\s+(user|assistant|system)\s+—\s+(\S+)(?:\s+\(([^)]+)\))?\s*$/;
2193
+ for (const line of lines) {
2194
+ const m = line.match(headingRe);
2195
+ if (m) {
2196
+ flush();
2197
+ current = {
2198
+ role: m[1],
2199
+ at: m[2] ?? "",
2200
+ attribution: m[3],
2201
+ content: ""
2202
+ };
2203
+ continue;
2204
+ }
2205
+ if (current) buffer.push(line);
2206
+ }
2207
+ flush();
2208
+ return { meta, turns };
2209
+ }
2210
+ function parseFrontmatter(raw, fallbackId) {
2211
+ const out = {};
2212
+ for (const line of raw.split("\n")) {
2213
+ const colon = line.indexOf(":");
2214
+ if (colon < 1) continue;
2215
+ const key = line.slice(0, colon).trim();
2216
+ const value = line.slice(colon + 1).trim();
2217
+ out[key] = value;
2218
+ }
2219
+ return {
2220
+ id: out.id ?? fallbackId,
2221
+ agent: out.agent ?? "unknown",
2222
+ started: out.started ?? "",
2223
+ status: out.status === "closed" ? "closed" : "open"
2224
+ };
2225
+ }
2226
+ function createRuntimeEvents() {
2227
+ const ee = new EventEmitter();
2228
+ ee.setMaxListeners(50);
2229
+ return {
2230
+ on(type, handler) {
2231
+ const wrapped = (ev) => {
2232
+ if (ev.type === type) handler(ev);
2233
+ };
2234
+ ee.on("event", wrapped);
2235
+ return () => ee.off("event", wrapped);
2236
+ },
2237
+ onAny(handler) {
2238
+ ee.on("event", handler);
2239
+ return () => ee.off("event", handler);
2240
+ },
2241
+ emit(ev) {
2242
+ ee.emit("event", ev);
2243
+ }
2244
+ };
2245
+ }
2246
+ var FsPathError = class extends Error {
2247
+ constructor(message) {
2248
+ super(message);
2249
+ this.name = "FsPathError";
2250
+ }
2251
+ };
2252
+ function makeAnchor(workspace) {
2253
+ const root = resolve(workspace);
2254
+ return (input2) => {
2255
+ if (typeof input2 !== "string" || input2.length === 0) {
2256
+ throw new FsPathError("path must be a non-empty string");
2257
+ }
2258
+ const candidate = isAbsolute(input2) ? normalize(input2) : normalize(join(root, input2));
2259
+ const rel = relative(root, candidate);
2260
+ if (rel.startsWith("..") || isAbsolute(rel)) {
2261
+ throw new FsPathError(`path escapes the workspace: '${input2}'`);
2262
+ }
2263
+ return candidate;
2264
+ };
2265
+ }
2266
+ function text(value) {
2267
+ return {
2268
+ content: [
2269
+ {
2270
+ type: "text",
2271
+ text: typeof value === "string" ? value : JSON.stringify(value)
2272
+ }
2273
+ ]
2274
+ };
2275
+ }
2276
+ function registerFsTools(server, opts) {
2277
+ const anchor = makeAnchor(opts.workspace);
2278
+ server.tool(
2279
+ "read_file",
2280
+ "Read a UTF-8 file from the workspace.",
2281
+ { path: z.string().describe("Workspace-relative path to the file.") },
2282
+ async ({ path }) => {
2283
+ const abs = anchor(path);
2284
+ const buf = await readFile(abs);
2285
+ return text(buf.toString("utf8"));
2286
+ }
2287
+ );
2288
+ server.tool(
2289
+ "write_file",
2290
+ "Write a file to the workspace. Parent directories are created on demand.",
2291
+ {
2292
+ path: z.string().describe("Workspace-relative path to the file."),
2293
+ content: z.string().describe("File body (UTF-8).")
2294
+ },
2295
+ async ({ path, content }) => {
2296
+ const abs = anchor(path);
2297
+ await mkdir(dirname(abs), { recursive: true });
2298
+ await writeFile(abs, content, "utf8");
2299
+ return text("ok");
2300
+ }
2301
+ );
2302
+ server.tool(
2303
+ "list_directory",
2304
+ "List entries of a directory in the workspace. Returns one '[FILE]' or '[DIR]' line per entry, mirroring `@modelcontextprotocol/server-filesystem`.",
2305
+ {
2306
+ path: z.string().optional().describe("Workspace-relative directory (defaults to workspace root).")
2307
+ },
2308
+ async ({ path }) => {
2309
+ const abs = anchor(path && path.length > 0 ? path : ".");
2310
+ const entries = await readdir(abs, { withFileTypes: true });
2311
+ const lines = entries.map(
2312
+ (e) => e.isDirectory() ? `[DIR] ${e.name}` : `[FILE] ${e.name}`
2313
+ );
2314
+ return text(lines.join("\n"));
2315
+ }
2316
+ );
2317
+ server.tool(
2318
+ "get_file_info",
2319
+ "Stat a file or directory in the workspace.",
2320
+ { path: z.string().describe("Workspace-relative path.") },
2321
+ async ({ path }) => {
2322
+ const abs = anchor(path);
2323
+ const info = await stat(abs);
2324
+ return text({
2325
+ name: abs.split("/").pop() ?? path,
2326
+ path,
2327
+ type: info.isDirectory() ? "directory" : "file",
2328
+ size: info.size,
2329
+ modified: info.mtime.toISOString(),
2330
+ created: info.birthtime.toISOString()
2331
+ });
2332
+ }
2333
+ );
2334
+ server.tool(
2335
+ "create_directory",
2336
+ "Create a directory (recursive) in the workspace.",
2337
+ { path: z.string().describe("Workspace-relative directory path.") },
2338
+ async ({ path }) => {
2339
+ const abs = anchor(path);
2340
+ await mkdir(abs, { recursive: true });
2341
+ return text("ok");
2342
+ }
2343
+ );
2344
+ server.tool(
2345
+ "delete_file",
2346
+ "Delete a file or empty directory in the workspace.",
2347
+ { path: z.string().describe("Workspace-relative path.") },
2348
+ async ({ path }) => {
2349
+ const abs = anchor(path);
2350
+ await rm(abs, { recursive: true, force: true });
2351
+ return text("ok");
2352
+ }
2353
+ );
2354
+ }
2355
+ var WORKSPACES_CONFIG_VERSION = 1;
2356
+ var DEFAULT_CONFIG_DIR = () => resolve(homedir(), ".agentproto");
2357
+ var DEFAULT_CONFIG_PATH = () => resolve(DEFAULT_CONFIG_DIR(), "workspaces.json");
2358
+ function sanitizeSlug(input2) {
2359
+ const trimmed = input2.trim().toLowerCase();
2360
+ const cleaned = trimmed.replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
2361
+ return cleaned || "workspace";
2362
+ }
2363
+ async function loadWorkspacesConfig(path = DEFAULT_CONFIG_PATH()) {
2364
+ let raw;
2365
+ try {
2366
+ raw = await promises.readFile(path, "utf8");
2367
+ } catch (err) {
2368
+ if (err.code === "ENOENT") {
2369
+ return { version: WORKSPACES_CONFIG_VERSION, workspaces: [] };
2370
+ }
2371
+ throw err;
2372
+ }
2373
+ let parsed;
2374
+ try {
2375
+ parsed = JSON.parse(raw);
2376
+ } catch (err) {
2377
+ throw new Error(
2378
+ `agentproto: ${path} is not valid JSON (${err instanceof Error ? err.message : String(err)}). Delete the file or fix it manually.`
2379
+ );
2380
+ }
2381
+ return normaliseConfig(parsed);
2382
+ }
2383
+ function findWorkspace(config, slug) {
2384
+ return config.workspaces.find((w) => w.slug === sanitizeSlug(slug));
2385
+ }
2386
+ function getActiveWorkspace(config) {
2387
+ if (!config.active) return config.workspaces[0];
2388
+ return findWorkspace(config, config.active) ?? config.workspaces[0];
2389
+ }
2390
+ function normaliseConfig(parsed) {
2391
+ if (!parsed || typeof parsed !== "object") {
2392
+ return { version: WORKSPACES_CONFIG_VERSION, workspaces: [] };
2393
+ }
2394
+ const obj = parsed;
2395
+ const workspaces = [];
2396
+ if (Array.isArray(obj.workspaces)) {
2397
+ for (const entry of obj.workspaces) {
2398
+ if (!entry || typeof entry !== "object") continue;
2399
+ const e = entry;
2400
+ const slug = typeof e.slug === "string" ? sanitizeSlug(e.slug) : "";
2401
+ const path = typeof e.path === "string" ? e.path : "";
2402
+ if (!slug || !path) continue;
2403
+ const addedAt = typeof e.addedAt === "string" ? e.addedAt : (/* @__PURE__ */ new Date()).toISOString();
2404
+ const updatedAt = typeof e.updatedAt === "string" ? e.updatedAt : addedAt;
2405
+ const we = { slug, path, addedAt, updatedAt };
2406
+ if (typeof e.label === "string" && e.label.trim()) {
2407
+ we.label = e.label.trim();
2408
+ }
2409
+ workspaces.push(we);
2410
+ }
2411
+ }
2412
+ const dedup = /* @__PURE__ */ new Map();
2413
+ for (const w of workspaces) dedup.set(w.slug, w);
2414
+ const finalList = Array.from(dedup.values());
2415
+ const active = typeof obj.active === "string" && finalList.some((w) => w.slug === obj.active) ? obj.active : finalList[0]?.slug;
2416
+ const out = {
2417
+ version: WORKSPACES_CONFIG_VERSION,
2418
+ workspaces: finalList
2419
+ };
2420
+ if (active !== void 0) out.active = active;
2421
+ return out;
2422
+ }
2423
+ async function discoverMcps(opts = {}) {
2424
+ const home = opts.home ?? homedir();
2425
+ const out = [];
2426
+ const errors = [];
2427
+ await Promise.all([
2428
+ scanClaudeCode(home, out, errors),
2429
+ scanCursor(home, out, errors),
2430
+ scanGoose(home, out),
2431
+ scanRegisteredWorkspaces(out, errors)
2432
+ ]);
2433
+ const seen = /* @__PURE__ */ new Set();
2434
+ const dedup = [];
2435
+ for (const m of out) {
2436
+ const key = `${m.source}:${m.scope}:${m.name}`;
2437
+ if (seen.has(key)) continue;
2438
+ seen.add(key);
2439
+ dedup.push(m);
2440
+ }
2441
+ if (errors.length > 0) {
2442
+ for (const e of errors) console.warn(`[mcp-discovery] ${e}`);
2443
+ }
2444
+ return dedup.sort((a, b) => {
2445
+ if (a.source !== b.source) return a.source.localeCompare(b.source);
2446
+ if (a.scope !== b.scope) return a.scope.localeCompare(b.scope);
2447
+ return a.name.localeCompare(b.name);
2448
+ });
2449
+ }
2450
+ async function scanClaudeCode(home, out, errors) {
2451
+ const path = resolve(home, ".claude.json");
2452
+ let raw;
2453
+ try {
2454
+ raw = await promises.readFile(path, "utf8");
2455
+ } catch (err) {
2456
+ if (err.code === "ENOENT") return;
2457
+ errors.push(`claude-code: read ${path} \u2014 ${err.message}`);
2458
+ return;
2459
+ }
2460
+ let parsed;
2461
+ try {
2462
+ parsed = JSON.parse(raw);
2463
+ } catch (err) {
2464
+ errors.push(`claude-code: JSON parse ${path} \u2014 ${err.message}`);
2465
+ return;
2466
+ }
2467
+ if (!parsed || typeof parsed !== "object") return;
2468
+ const root = parsed;
2469
+ const top = root.mcpServers;
2470
+ if (top && typeof top === "object") {
2471
+ pushMcpMap({
2472
+ out,
2473
+ source: "claude-code",
2474
+ scope: "global",
2475
+ map: top
2476
+ });
2477
+ }
2478
+ const projects = root.projects;
2479
+ if (projects && typeof projects === "object") {
2480
+ for (const [projPath, proj] of Object.entries(
2481
+ projects
2482
+ )) {
2483
+ if (!proj || typeof proj !== "object") continue;
2484
+ const projMcps = proj.mcpServers;
2485
+ if (!projMcps || typeof projMcps !== "object") continue;
2486
+ pushMcpMap({
2487
+ out,
2488
+ source: "claude-code",
2489
+ scope: `project:${projPath}`,
2490
+ map: projMcps
2491
+ });
2492
+ }
2493
+ }
2494
+ }
2495
+ async function scanCursor(home, out, errors) {
2496
+ const path = resolve(home, ".cursor", "mcp.json");
2497
+ let raw;
2498
+ try {
2499
+ raw = await promises.readFile(path, "utf8");
2500
+ } catch (err) {
2501
+ if (err.code === "ENOENT") return;
2502
+ errors.push(`cursor: read ${path} \u2014 ${err.message}`);
2503
+ return;
2504
+ }
2505
+ let parsed;
2506
+ try {
2507
+ parsed = JSON.parse(raw);
2508
+ } catch (err) {
2509
+ errors.push(`cursor: JSON parse ${path} \u2014 ${err.message}`);
2510
+ return;
2511
+ }
2512
+ if (!parsed || typeof parsed !== "object") return;
2513
+ const root = parsed;
2514
+ const map = root.mcpServers;
2515
+ if (map && typeof map === "object") {
2516
+ pushMcpMap({
2517
+ out,
2518
+ source: "cursor",
2519
+ scope: "global",
2520
+ map
2521
+ });
2522
+ }
2523
+ }
2524
+ async function scanGoose(home, out, errors) {
2525
+ const path = resolve(home, ".config", "goose", "config.yaml");
2526
+ try {
2527
+ await promises.access(path);
2528
+ } catch {
2529
+ return;
2530
+ }
2531
+ out.push({
2532
+ id: "goose:global:_detected",
2533
+ source: "goose",
2534
+ scope: "global",
2535
+ name: "(goose config detected)",
2536
+ type: "unknown",
2537
+ parseNote: "Goose config found but YAML parsing is not yet implemented. Open ~/.config/goose/config.yaml to inspect MCP entries."
2538
+ });
2539
+ }
2540
+ async function scanRegisteredWorkspaces(out, errors) {
2541
+ let workspaces = [];
2542
+ try {
2543
+ const cfg = await loadWorkspacesConfig();
2544
+ workspaces = cfg.workspaces;
2545
+ } catch (err) {
2546
+ errors.push(`workspaces: load failed \u2014 ${err.message}`);
2547
+ return;
2548
+ }
2549
+ await Promise.all(
2550
+ workspaces.map(async (ws) => {
2551
+ const candidates = [
2552
+ resolve(ws.path, ".mcp.json"),
2553
+ resolve(ws.path, ".cursor", "mcp.json"),
2554
+ resolve(ws.path, ".vscode", "mcp.json")
2555
+ ];
2556
+ for (const path of candidates) {
2557
+ let raw;
2558
+ try {
2559
+ raw = await promises.readFile(path, "utf8");
2560
+ } catch (err) {
2561
+ if (err.code === "ENOENT") continue;
2562
+ errors.push(
2563
+ `workspace ${ws.slug}: read ${path} \u2014 ${err.message}`
2564
+ );
2565
+ continue;
2566
+ }
2567
+ let parsed;
2568
+ try {
2569
+ parsed = JSON.parse(raw);
2570
+ } catch (err) {
2571
+ errors.push(
2572
+ `workspace ${ws.slug}: JSON parse ${path} \u2014 ${err.message}`
2573
+ );
2574
+ continue;
2575
+ }
2576
+ if (!parsed || typeof parsed !== "object") continue;
2577
+ const map = parsed.mcpServers;
2578
+ if (!map || typeof map !== "object") continue;
2579
+ pushMcpMap({
2580
+ out,
2581
+ source: "workspace",
2582
+ scope: `workspace:${ws.slug}`,
2583
+ map
2584
+ });
2585
+ }
2586
+ })
2587
+ );
2588
+ }
2589
+ function pushMcpMap(args) {
2590
+ for (const [name, raw] of Object.entries(args.map)) {
2591
+ if (!raw || typeof raw !== "object") continue;
2592
+ const entry = raw;
2593
+ const id = `${args.source}:${args.scope}:${name}`;
2594
+ const declared = typeof entry.type === "string" ? entry.type : null;
2595
+ const url = typeof entry.url === "string" ? entry.url : void 0;
2596
+ const command = typeof entry.command === "string" ? entry.command : void 0;
2597
+ let type;
2598
+ if (declared === "http" || declared === "sse" || declared === "stdio") {
2599
+ type = declared;
2600
+ } else if (url) {
2601
+ type = "http";
2602
+ } else if (command) {
2603
+ type = "stdio";
2604
+ } else {
2605
+ type = "unknown";
2606
+ }
2607
+ const m = {
2608
+ id,
2609
+ source: args.source,
2610
+ scope: args.scope,
2611
+ name,
2612
+ type
2613
+ };
2614
+ if (command !== void 0) m.command = command;
2615
+ if (Array.isArray(entry.args)) {
2616
+ m.args = entry.args.filter((a) => typeof a === "string");
2617
+ }
2618
+ if (entry.env && typeof entry.env === "object") {
2619
+ m.env = stringifyValues(entry.env);
2620
+ }
2621
+ if (url !== void 0) m.url = url;
2622
+ if (entry.headers && typeof entry.headers === "object") {
2623
+ m.headers = stringifyValues(entry.headers);
2624
+ }
2625
+ if (type === "unknown") {
2626
+ m.parseNote = "Entry has neither `command` nor `url` \u2014 couldn't classify transport.";
2627
+ }
2628
+ args.out.push(m);
2629
+ }
2630
+ }
2631
+ function stringifyValues(raw) {
2632
+ const out = {};
2633
+ for (const [k, v] of Object.entries(raw)) {
2634
+ if (typeof v === "string") out[k] = v;
2635
+ else if (v != null) out[k] = String(v);
2636
+ }
2637
+ return out;
2638
+ }
2639
+ var IMPORTED_MCPS_PATH = () => resolve(homedir(), ".agentproto", "imported-mcps.json");
2640
+ var IMPORTED_MCPS_VERSION = 1;
2641
+ var EMPTY = {
2642
+ version: IMPORTED_MCPS_VERSION,
2643
+ imports: []
2644
+ };
2645
+ async function loadImportedMcps(path = IMPORTED_MCPS_PATH()) {
2646
+ let raw;
2647
+ try {
2648
+ raw = await promises.readFile(path, "utf8");
2649
+ } catch (err) {
2650
+ if (err.code === "ENOENT") return EMPTY;
2651
+ throw err;
2652
+ }
2653
+ let parsed;
2654
+ try {
2655
+ parsed = JSON.parse(raw);
2656
+ } catch (err) {
2657
+ throw new Error(
2658
+ `agentproto: ${path} is not valid JSON (${err instanceof Error ? err.message : String(err)}). Delete or fix manually.`
2659
+ );
2660
+ }
2661
+ return normalise(parsed);
2662
+ }
2663
+ async function saveImportedMcps(config, path = IMPORTED_MCPS_PATH()) {
2664
+ await promises.mkdir(dirname(path), { recursive: true });
2665
+ const tmp = `${path}.tmp.${process.pid}`;
2666
+ await promises.writeFile(tmp, JSON.stringify(config, null, 2) + "\n", "utf8");
2667
+ await promises.rename(tmp, path);
2668
+ }
2669
+ function addImport(config, input2) {
2670
+ const alias = (input2.alias ?? input2.snapshot.name).trim();
2671
+ if (!alias) {
2672
+ throw new Error("addImport: alias resolves to empty string");
2673
+ }
2674
+ const addedAt = (/* @__PURE__ */ new Date()).toISOString();
2675
+ const next = {
2676
+ id: input2.snapshot.id,
2677
+ alias,
2678
+ addedAt,
2679
+ snapshot: input2.snapshot
2680
+ };
2681
+ const others = config.imports.filter((e) => e.id !== input2.snapshot.id);
2682
+ return { ...config, imports: [...others, next] };
2683
+ }
2684
+ function removeImport(config, id) {
2685
+ return { ...config, imports: config.imports.filter((e) => e.id !== id) };
2686
+ }
2687
+ function normalise(parsed) {
2688
+ if (!parsed || typeof parsed !== "object") return EMPTY;
2689
+ const obj = parsed;
2690
+ const imports = [];
2691
+ if (Array.isArray(obj.imports)) {
2692
+ for (const entry of obj.imports) {
2693
+ if (!entry || typeof entry !== "object") continue;
2694
+ const e = entry;
2695
+ const id = typeof e.id === "string" ? e.id : "";
2696
+ const alias = typeof e.alias === "string" ? e.alias : id;
2697
+ const addedAt = typeof e.addedAt === "string" ? e.addedAt : (/* @__PURE__ */ new Date()).toISOString();
2698
+ const snapshot = e.snapshot;
2699
+ if (!id || !snapshot) continue;
2700
+ imports.push({ id, alias, addedAt, snapshot });
2701
+ }
2702
+ }
2703
+ return { version: IMPORTED_MCPS_VERSION, imports };
2704
+ }
2705
+ function registerSessionTools(server, opts) {
2706
+ const { registry, resolveAgentAdapter, listAgentAdapters, mcpProxy } = opts;
2707
+ server.tool(
2708
+ "start_agent_session",
2709
+ "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.",
2710
+ {
2711
+ adapter: z.string().min(1).describe(
2712
+ "Adapter slug \u2014 one of the installed `@agentproto/adapter-*` packages (e.g. 'claude-code', 'hermes', 'aider')."
2713
+ ),
2714
+ workspaceSlug: z.string().optional().describe(
2715
+ "Workspace slug from `agentproto workspace list`. The daemon resolves it to an absolute path. Omit to use the `cwd` field or the active workspace."
2716
+ ),
2717
+ cwd: z.string().optional().describe(
2718
+ "Absolute path to spawn the agent in. Wins over `workspaceSlug` when both are set."
2719
+ ),
2720
+ prompt: z.string().optional().describe(
2721
+ "Optional initial prompt. The session is spawned and the prompt dispatched in one shot \u2014 equivalent to `start` then `prompt` back-to-back. Skip to spawn idle."
2722
+ ),
2723
+ label: z.string().optional().describe(
2724
+ "Free-text label that surfaces in `list_agent_sessions` and the UI \u2014 useful for tagging sessions with a conversation id or operator name."
2725
+ )
2726
+ },
2727
+ async (input2) => {
2728
+ if (!resolveAgentAdapter) {
2729
+ return {
2730
+ content: [
2731
+ {
2732
+ type: "text",
2733
+ text: "start_agent_session is not enabled \u2014 the daemon was started without an adapter resolver. Re-run the daemon with the `@agentproto/cli` shim wired (see playground/scripts/gateway.ts)."
2734
+ }
2735
+ ],
2736
+ isError: true
2737
+ };
2738
+ }
2739
+ let cwd = input2.cwd;
2740
+ let resolvedSlug = input2.workspaceSlug ?? "default";
2741
+ if (!cwd) {
2742
+ try {
2743
+ const config = await loadWorkspacesConfig();
2744
+ const ws = input2.workspaceSlug ? findWorkspace(config, input2.workspaceSlug) : getActiveWorkspace(config);
2745
+ if (ws) {
2746
+ cwd = ws.path;
2747
+ resolvedSlug = ws.slug;
2748
+ }
2749
+ } catch {
2750
+ }
2751
+ }
2752
+ if (!cwd) {
2753
+ return {
2754
+ content: [
2755
+ {
2756
+ type: "text",
2757
+ text: "start_agent_session: no cwd resolvable. Pass `cwd` explicitly, or pass `workspaceSlug` matching `agentproto workspace list`, or set an active workspace via `agentproto workspace use <slug>`."
2758
+ }
2759
+ ],
2760
+ isError: true
2761
+ };
2762
+ }
2763
+ const resolved = await resolveAgentAdapter(input2.adapter);
2764
+ if (!resolved) {
2765
+ return {
2766
+ content: [
2767
+ {
2768
+ type: "text",
2769
+ text: `start_agent_session: adapter "${input2.adapter}" not found. Try \`agentproto install <slug>\` first.`
2770
+ }
2771
+ ],
2772
+ isError: true
2773
+ };
2774
+ }
2775
+ try {
2776
+ const agentSession = await resolved.startSession({ cwd });
2777
+ const desc = registry.spawnAgent({
2778
+ workspaceSlug: resolvedSlug,
2779
+ cwd,
2780
+ agentSession,
2781
+ adapterSlug: input2.adapter,
2782
+ ...input2.prompt ? { initialPrompt: input2.prompt } : {},
2783
+ ...input2.label ? { label: input2.label } : {},
2784
+ ...resolved.commandPreview ? { commandPreview: resolved.commandPreview } : {}
2785
+ });
2786
+ return {
2787
+ content: [{ type: "text", text: JSON.stringify(desc, null, 2) }]
2788
+ };
2789
+ } catch (err) {
2790
+ return {
2791
+ content: [
2792
+ {
2793
+ type: "text",
2794
+ text: `start_agent_session: spawn failed \u2014 ${err instanceof Error ? err.message : String(err)}`
2795
+ }
2796
+ ],
2797
+ isError: true
2798
+ };
2799
+ }
2800
+ }
2801
+ );
2802
+ server.tool(
2803
+ "prompt_agent_session",
2804
+ "Send a follow-up prompt to a live agent session \u2014 multi-turn continuity without re-spawning. The session id comes from `start_agent_session` (or `list_agent_sessions`). Returns immediately; tail output via `get_agent_session_output` or the SSE /sessions/:id/stream endpoint.",
2805
+ {
2806
+ sessionId: z.string().describe("Session id returned by start_agent_session."),
2807
+ prompt: z.string().min(1).describe("The next user turn (plain text).")
2808
+ },
2809
+ async (input2) => {
2810
+ try {
2811
+ void registry.sendPrompt(input2.sessionId, input2.prompt).catch(() => {
2812
+ });
2813
+ return {
2814
+ content: [
2815
+ {
2816
+ type: "text",
2817
+ text: JSON.stringify(
2818
+ { ok: true, sessionId: input2.sessionId, queued: true },
2819
+ null,
2820
+ 2
2821
+ )
2822
+ }
2823
+ ]
2824
+ };
2825
+ } catch (err) {
2826
+ return {
2827
+ content: [
2828
+ {
2829
+ type: "text",
2830
+ text: `prompt_agent_session: ${err instanceof Error ? err.message : String(err)}`
2831
+ }
2832
+ ],
2833
+ isError: true
2834
+ };
2835
+ }
2836
+ }
2837
+ );
2838
+ server.tool(
2839
+ "list_agent_sessions",
2840
+ "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.",
2841
+ {
2842
+ onlyAlive: z.boolean().optional().describe("Filter to status running/starting/mounting only. Default false.")
2843
+ },
2844
+ async (input2) => {
2845
+ const all = registry.list();
2846
+ const filtered = input2.onlyAlive ? all.filter(
2847
+ (s) => s.status === "running" || s.status === "starting" || s.status === "mounting"
2848
+ ) : all;
2849
+ return {
2850
+ content: [
2851
+ {
2852
+ type: "text",
2853
+ text: JSON.stringify({ sessions: filtered }, null, 2)
2854
+ }
2855
+ ]
2856
+ };
2857
+ }
2858
+ );
2859
+ server.tool(
2860
+ "get_agent_session_output",
2861
+ "Tail the recent output of a session. Returns the last N lines of the ring buffer (stdout + stderr inter-leaved, newest last). Use this to read an agent's reply after `prompt_agent_session`.",
2862
+ {
2863
+ sessionId: z.string().describe("Session id."),
2864
+ lastN: z.number().int().min(1).max(500).optional().describe("Max lines to return. Default 80, max 500.")
2865
+ },
2866
+ async (input2) => {
2867
+ const desc = registry.get(input2.sessionId);
2868
+ if (!desc) {
2869
+ return {
2870
+ content: [
2871
+ { type: "text", text: `get_agent_session_output: no session "${input2.sessionId}"` }
2872
+ ],
2873
+ isError: true
2874
+ };
2875
+ }
2876
+ const limit = input2.lastN ?? 80;
2877
+ const lines = [];
2878
+ const unsub = registry.attach(input2.sessionId, (line, _stream) => {
2879
+ lines.push(line);
2880
+ });
2881
+ if (unsub) unsub();
2882
+ const tail = lines.slice(-limit);
2883
+ return {
2884
+ content: [
2885
+ {
2886
+ type: "text",
2887
+ text: JSON.stringify(
2888
+ {
2889
+ sessionId: input2.sessionId,
2890
+ status: desc.status,
2891
+ lastOutputAt: desc.lastOutputAt,
2892
+ lines: tail
2893
+ },
2894
+ null,
2895
+ 2
2896
+ )
2897
+ }
2898
+ ]
2899
+ };
2900
+ }
2901
+ );
2902
+ server.tool(
2903
+ "list_adapters",
2904
+ "Enumerate every agent CLI adapter installed on the host (claude-code, hermes, aider, \u2026). Returns slug + display name + version + protocol so callers can let users pick from the installed set instead of guessing. Use before `start_agent_session` when the model doesn't already know what's available.",
2905
+ {},
2906
+ async () => {
2907
+ if (!listAgentAdapters) {
2908
+ return {
2909
+ content: [
2910
+ {
2911
+ type: "text",
2912
+ text: "list_adapters is not enabled \u2014 the daemon was started without an adapter lister. Wire `@agentproto/cli`'s `listInstalledAdapters` via `createGateway({ listAgentAdapters })`."
2913
+ }
2914
+ ],
2915
+ isError: true
2916
+ };
2917
+ }
2918
+ try {
2919
+ const adapters = await listAgentAdapters();
2920
+ return {
2921
+ content: [{ type: "text", text: JSON.stringify({ adapters }, null, 2) }]
2922
+ };
2923
+ } catch (err) {
2924
+ return {
2925
+ content: [
2926
+ {
2927
+ type: "text",
2928
+ text: `list_adapters failed: ${err instanceof Error ? err.message : String(err)}`
2929
+ }
2930
+ ],
2931
+ isError: true
2932
+ };
2933
+ }
2934
+ }
2935
+ );
2936
+ server.tool(
2937
+ "list_discovered_mcps",
2938
+ "Discover MCP servers already configured in the user's other agent tooling (claude-code, cursor, goose). Returns the union with source attribution so the operator can suggest 'I see you have a chrome-devtools MCP set up in claude \u2014 want me to use it?' instead of asking the user to re-configure. Read-only \u2014 does not modify any host's config.",
2939
+ {},
2940
+ async () => {
2941
+ try {
2942
+ const mcps = await discoverMcps();
2943
+ return {
2944
+ content: [{ type: "text", text: JSON.stringify({ mcps }, null, 2) }]
2945
+ };
2946
+ } catch (err) {
2947
+ return {
2948
+ content: [
2949
+ {
2950
+ type: "text",
2951
+ text: `list_discovered_mcps failed: ${err instanceof Error ? err.message : String(err)}`
2952
+ }
2953
+ ],
2954
+ isError: true
2955
+ };
2956
+ }
2957
+ }
2958
+ );
2959
+ server.tool(
2960
+ "list_imported_mcps",
2961
+ "Return the user's curated set of MCP servers \u2014 the ones they've imported from claude / cursor / workspace configs into the daemon. Use to know which MCPs the operator may freely call vs. ones still showing up in `list_discovered_mcps` waiting on the user's blessing.",
2962
+ {},
2963
+ async () => {
2964
+ try {
2965
+ const config = await loadImportedMcps();
2966
+ return {
2967
+ content: [
2968
+ { type: "text", text: JSON.stringify(config, null, 2) }
2969
+ ]
2970
+ };
2971
+ } catch (err) {
2972
+ return {
2973
+ content: [
2974
+ {
2975
+ type: "text",
2976
+ text: `list_imported_mcps failed: ${err instanceof Error ? err.message : String(err)}`
2977
+ }
2978
+ ],
2979
+ isError: true
2980
+ };
2981
+ }
2982
+ }
2983
+ );
2984
+ server.tool(
2985
+ "import_mcp",
2986
+ "Import a discovered MCP into the daemon's curated set. The agent calls `list_discovered_mcps` first, asks the user, then commits the choice via this tool. The snapshot is captured at import time so the entry stays usable if the source config (claude/cursor) is later removed.",
2987
+ {
2988
+ sourceMcpId: z.string().min(1).describe(
2989
+ "The discovered MCP id from `list_discovered_mcps` (e.g. 'claude-code:project:/path:chrome-devtools')."
2990
+ ),
2991
+ alias: z.string().optional().describe(
2992
+ "Optional friendly name to display. Defaults to the source MCP's name."
2993
+ )
2994
+ },
2995
+ async (input2) => {
2996
+ try {
2997
+ const discovered = await discoverMcps();
2998
+ const snapshot = discovered.find((d) => d.id === input2.sourceMcpId);
2999
+ if (!snapshot) {
3000
+ return {
3001
+ content: [
3002
+ {
3003
+ type: "text",
3004
+ text: `import_mcp: discovered MCP "${input2.sourceMcpId}" not found. Re-run list_discovered_mcps to get current ids.`
3005
+ }
3006
+ ],
3007
+ isError: true
3008
+ };
3009
+ }
3010
+ const cfg = await loadImportedMcps();
3011
+ const next = addImport(cfg, {
3012
+ snapshot,
3013
+ ...input2.alias ? { alias: input2.alias } : {}
3014
+ });
3015
+ await saveImportedMcps(next);
3016
+ const entry = next.imports.find((e) => e.id === snapshot.id);
3017
+ return {
3018
+ content: [{ type: "text", text: JSON.stringify(entry, null, 2) }]
3019
+ };
3020
+ } catch (err) {
3021
+ return {
3022
+ content: [
3023
+ {
3024
+ type: "text",
3025
+ text: `import_mcp failed: ${err instanceof Error ? err.message : String(err)}`
3026
+ }
3027
+ ],
3028
+ isError: true
3029
+ };
3030
+ }
3031
+ }
3032
+ );
3033
+ server.tool(
3034
+ "remove_imported_mcp",
3035
+ "Remove a previously-imported MCP from the daemon's curated set. Use when the user no longer wants the operator referencing it.",
3036
+ {
3037
+ id: z.string().min(1).describe(
3038
+ "The imported MCP id (matches the discovered MCP id at import time)."
3039
+ )
3040
+ },
3041
+ async (input2) => {
3042
+ try {
3043
+ const cfg = await loadImportedMcps();
3044
+ if (!cfg.imports.some((e) => e.id === input2.id)) {
3045
+ return {
3046
+ content: [
3047
+ {
3048
+ type: "text",
3049
+ text: `remove_imported_mcp: id "${input2.id}" not in imports. Use list_imported_mcps to see current entries.`
3050
+ }
3051
+ ],
3052
+ isError: true
3053
+ };
3054
+ }
3055
+ await saveImportedMcps(removeImport(cfg, input2.id));
3056
+ return {
3057
+ content: [
3058
+ { type: "text", text: JSON.stringify({ ok: true, id: input2.id }, null, 2) }
3059
+ ]
3060
+ };
3061
+ } catch (err) {
3062
+ return {
3063
+ content: [
3064
+ {
3065
+ type: "text",
3066
+ text: `remove_imported_mcp failed: ${err instanceof Error ? err.message : String(err)}`
3067
+ }
3068
+ ],
3069
+ isError: true
3070
+ };
3071
+ }
3072
+ }
3073
+ );
3074
+ server.tool(
3075
+ "mcp_imported_status",
3076
+ "Snapshot every imported MCP server with its connection status, transport type, and tool count. Use this first when an operator wonders 'what MCPs do I actually have access to right now?' \u2014 the answer covers both 'imported but not yet connected' and 'connected with N tools'. Errors during connect surface in `lastError`.",
3077
+ {},
3078
+ async () => {
3079
+ if (!mcpProxy) {
3080
+ return {
3081
+ content: [
3082
+ {
3083
+ type: "text",
3084
+ text: "mcp_imported_status is not enabled \u2014 daemon was started without an MCP proxy. The host must wire `mcpProxy` in createGateway."
3085
+ }
3086
+ ],
3087
+ isError: true
3088
+ };
3089
+ }
3090
+ try {
3091
+ const aliases = await mcpProxy.listAliases();
3092
+ return {
3093
+ content: [
3094
+ { type: "text", text: JSON.stringify({ imports: aliases }, null, 2) }
3095
+ ]
3096
+ };
3097
+ } catch (err) {
3098
+ return {
3099
+ content: [
3100
+ {
3101
+ type: "text",
3102
+ text: `mcp_imported_status failed: ${err instanceof Error ? err.message : String(err)}`
3103
+ }
3104
+ ],
3105
+ isError: true
3106
+ };
3107
+ }
3108
+ }
3109
+ );
3110
+ server.tool(
3111
+ "mcp_imported_list_tools",
3112
+ "List the tools exposed by one imported MCP server. The proxy lazily connects on first call \u2014 first-use latency includes the transport handshake (stdio: ~1-2s for npx-spawned servers; http/sse: <100ms). Returns the upstream `inputSchema` (JSON Schema) verbatim so the operator can build a valid `arguments` object for the follow-up `mcp_imported_call` invocation.",
3113
+ {
3114
+ alias: z.string().min(1).describe(
3115
+ "Alias from `list_imported_mcps` / `mcp_imported_status` (typically the original MCP name, e.g. 'chrome-devtools')."
3116
+ )
3117
+ },
3118
+ async (input2) => {
3119
+ if (!mcpProxy) {
3120
+ return {
3121
+ content: [
3122
+ {
3123
+ type: "text",
3124
+ text: "mcp_imported_list_tools is not enabled \u2014 see mcp_imported_status."
3125
+ }
3126
+ ],
3127
+ isError: true
3128
+ };
3129
+ }
3130
+ const out = await mcpProxy.listTools(input2.alias);
3131
+ if (!out.ok) {
3132
+ return {
3133
+ content: [
3134
+ {
3135
+ type: "text",
3136
+ text: `mcp_imported_list_tools "${input2.alias}": ${out.error}`
3137
+ }
3138
+ ],
3139
+ isError: true
3140
+ };
3141
+ }
3142
+ return {
3143
+ content: [
3144
+ { type: "text", text: JSON.stringify({ alias: input2.alias, tools: out.tools }, null, 2) }
3145
+ ]
3146
+ };
3147
+ }
3148
+ );
3149
+ server.tool(
3150
+ "mcp_imported_call",
3151
+ "Invoke a tool on an imported MCP server. The daemon proxies the call through the live client connection \u2014 the upstream server validates `arguments` against its own input schema (which you can fetch via `mcp_imported_list_tools`). The full upstream result is returned verbatim, including `isError` flags so the operator sees the original failure shape.",
3152
+ {
3153
+ alias: z.string().min(1).describe("Imported MCP alias."),
3154
+ toolName: z.string().min(1).describe(
3155
+ "Tool name as it appears in `mcp_imported_list_tools` (NOT a namespaced version \u2014 pass the upstream's own name)."
3156
+ ),
3157
+ args: z.record(z.string(), z.unknown()).optional().describe(
3158
+ "Tool arguments as a JSON object. Schema is the upstream's \u2014 the proxy doesn't validate, only forwards. Default: empty object."
3159
+ )
3160
+ },
3161
+ async (input2) => {
3162
+ if (!mcpProxy) {
3163
+ return {
3164
+ content: [
3165
+ {
3166
+ type: "text",
3167
+ text: "mcp_imported_call is not enabled \u2014 see mcp_imported_status."
3168
+ }
3169
+ ],
3170
+ isError: true
3171
+ };
3172
+ }
3173
+ const out = await mcpProxy.callTool(
3174
+ input2.alias,
3175
+ input2.toolName,
3176
+ input2.args ?? {}
3177
+ );
3178
+ if (!out.ok) {
3179
+ return {
3180
+ content: [
3181
+ {
3182
+ type: "text",
3183
+ text: `mcp_imported_call "${input2.alias}".${input2.toolName}: ${out.error}`
3184
+ }
3185
+ ],
3186
+ isError: true
3187
+ };
3188
+ }
3189
+ return out.result;
3190
+ }
3191
+ );
3192
+ server.tool(
3193
+ "kill_agent_session",
3194
+ "Stop a session \u2014 SIGTERM the underlying child + close the agent protocol session. Use to free resources after the operator is done, or when a session is wedged.",
3195
+ {
3196
+ sessionId: z.string().describe("Session id.")
3197
+ },
3198
+ async (input2) => {
3199
+ const ok = registry.kill(input2.sessionId);
3200
+ return {
3201
+ content: [
3202
+ {
3203
+ type: "text",
3204
+ text: JSON.stringify({ ok, sessionId: input2.sessionId }, null, 2)
3205
+ }
3206
+ ]
3207
+ };
3208
+ }
3209
+ );
3210
+ }
3211
+ function startHeartbeat(opts) {
3212
+ const filename = opts.filename ?? "HEARTBEAT.md";
3213
+ const path = join(opts.workspace, filename);
3214
+ const now = opts.now ?? (() => /* @__PURE__ */ new Date());
3215
+ let timer = null;
3216
+ let currentEveryMs = null;
3217
+ let firing = false;
3218
+ async function load() {
3219
+ if (!existsSync(path)) return null;
3220
+ let source;
3221
+ try {
3222
+ source = await readFile(path, "utf8");
3223
+ } catch {
3224
+ return null;
3225
+ }
3226
+ const parsed = matter(source);
3227
+ const fm = parsed.data;
3228
+ const agent = typeof fm.agent === "string" ? fm.agent : null;
3229
+ if (!agent) return null;
3230
+ const enabled = fm.enabled === void 0 ? true : Boolean(fm.enabled);
3231
+ const everyRaw = typeof fm.every === "string" ? fm.every : "60s";
3232
+ const everyMs = parseDuration(everyRaw);
3233
+ if (!everyMs) return null;
3234
+ const conversationTemplate = typeof fm.conversation === "string" ? fm.conversation : "heartbeat-{date}";
3235
+ return {
3236
+ agent,
3237
+ everyMs,
3238
+ enabled,
3239
+ conversationTemplate,
3240
+ body: parsed.content.trim()
3241
+ };
3242
+ }
3243
+ async function tick() {
3244
+ if (firing) return;
3245
+ if (opts.shouldFire && !opts.shouldFire()) return;
3246
+ firing = true;
3247
+ let agentId;
3248
+ try {
3249
+ const hb = await load();
3250
+ if (!hb || !hb.enabled) return;
3251
+ agentId = hb.agent;
3252
+ const agent = await opts.buildAgent(hb.agent);
3253
+ if (!agent) {
3254
+ opts.events.emit({
3255
+ type: "heartbeat-error",
3256
+ at: now().toISOString(),
3257
+ agent: hb.agent,
3258
+ error: `buildAgent('${hb.agent}') returned null`
3259
+ });
3260
+ return;
3261
+ }
3262
+ if (!hb.body) {
3263
+ opts.events.emit({
3264
+ type: "heartbeat-error",
3265
+ at: now().toISOString(),
3266
+ agent: hb.agent,
3267
+ error: "HEARTBEAT.md body is empty"
3268
+ });
3269
+ return;
3270
+ }
3271
+ const conversationId = expandTemplate(hb.conversationTemplate, now());
3272
+ await opts.conversations.open(conversationId, { agent: hb.agent });
3273
+ const startedAt = now();
3274
+ await opts.conversations.appendTurn(
3275
+ conversationId,
3276
+ "user",
3277
+ hb.body,
3278
+ { at: startedAt.toISOString(), attribution: "heartbeat" }
3279
+ );
3280
+ opts.events.emit({
3281
+ type: "conv-turn-appended",
3282
+ at: startedAt.toISOString(),
3283
+ conversationId,
3284
+ role: "user",
3285
+ contentPreview: preview(hb.body)
3286
+ });
3287
+ const t0 = Date.now();
3288
+ const reply = await agent.generate(hb.body);
3289
+ const durationMs = Date.now() - t0;
3290
+ const finishedAt = now();
3291
+ await opts.conversations.appendTurn(
3292
+ conversationId,
3293
+ "assistant",
3294
+ reply.text,
3295
+ { at: finishedAt.toISOString(), attribution: hb.agent }
3296
+ );
3297
+ opts.events.emit({
3298
+ type: "conv-turn-appended",
3299
+ at: finishedAt.toISOString(),
3300
+ conversationId,
3301
+ role: "assistant",
3302
+ contentPreview: preview(reply.text)
3303
+ });
3304
+ opts.events.emit({
3305
+ type: "heartbeat-fired",
3306
+ at: finishedAt.toISOString(),
3307
+ agent: hb.agent,
3308
+ conversationId,
3309
+ prompt: hb.body,
3310
+ reply: reply.text,
3311
+ durationMs
3312
+ });
3313
+ if (currentEveryMs !== null && currentEveryMs !== hb.everyMs) {
3314
+ reschedule(hb.everyMs);
3315
+ }
3316
+ } catch (err) {
3317
+ opts.events.emit({
3318
+ type: "heartbeat-error",
3319
+ at: now().toISOString(),
3320
+ agent: agentId,
3321
+ error: err instanceof Error ? err.message : String(err)
3322
+ });
3323
+ } finally {
3324
+ firing = false;
3325
+ }
3326
+ }
3327
+ function reschedule(everyMs) {
3328
+ if (timer) clearInterval(timer);
3329
+ currentEveryMs = everyMs;
3330
+ timer = setInterval(() => {
3331
+ void tick();
3332
+ }, everyMs);
3333
+ }
3334
+ return {
3335
+ start() {
3336
+ if (timer) return;
3337
+ void load().then((hb) => {
3338
+ const everyMs = hb?.everyMs ?? 6e4;
3339
+ reschedule(everyMs);
3340
+ });
3341
+ },
3342
+ stop() {
3343
+ if (timer) {
3344
+ clearInterval(timer);
3345
+ timer = null;
3346
+ currentEveryMs = null;
3347
+ }
3348
+ },
3349
+ async fireNow() {
3350
+ await tick();
3351
+ }
3352
+ };
3353
+ }
3354
+ var DURATION_RE = /^(\d+)\s*(s|m|h|d)$/;
3355
+ function parseDuration(input2) {
3356
+ const trimmed = input2.trim();
3357
+ const match = trimmed.match(DURATION_RE);
3358
+ if (!match) return null;
3359
+ const n = Number(match[1]);
3360
+ if (!Number.isFinite(n) || n <= 0) return null;
3361
+ switch (match[2]) {
3362
+ case "s":
3363
+ return n * 1e3;
3364
+ case "m":
3365
+ return n * 6e4;
3366
+ case "h":
3367
+ return n * 36e5;
3368
+ case "d":
3369
+ return n * 864e5;
3370
+ default:
3371
+ return null;
3372
+ }
3373
+ }
3374
+ function expandTemplate(template, when) {
3375
+ const date = when.toISOString().slice(0, 10);
3376
+ return template.replace(/\{date\}/g, date);
3377
+ }
3378
+ function preview(text3, max = 120) {
3379
+ const flat = text3.replace(/\s+/g, " ").trim();
3380
+ return flat.length > max ? `${flat.slice(0, max - 1)}\u2026` : flat;
3381
+ }
3382
+ async function startHttpServer(opts) {
3383
+ const startedAt = Date.now();
3384
+ const authSource = opts.auth ?? { mode: "none" };
3385
+ const readAuth = () => typeof authSource === "function" ? authSource() : authSource;
3386
+ function isLoopback(req) {
3387
+ const addr = req.socket.remoteAddress ?? "";
3388
+ const isLocalAddr = addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
3389
+ if (!isLocalAddr) return false;
3390
+ if (req.headers["x-forwarded-for"]) return false;
3391
+ return true;
3392
+ }
3393
+ function authorize(req, res) {
3394
+ const auth = readAuth();
3395
+ if (auth.mode === "none") return true;
3396
+ if (isLoopback(req)) return true;
3397
+ const header = req.headers.authorization;
3398
+ const expected = `Bearer ${auth.token}`;
3399
+ if (header !== expected) {
3400
+ res.writeHead(401, { "content-type": "application/json" });
3401
+ res.end(JSON.stringify({ error: "unauthorized" }));
3402
+ return false;
3403
+ }
3404
+ return true;
3405
+ }
3406
+ async function handleMcp(req, res) {
3407
+ if (!authorize(req, res)) return;
3408
+ const server2 = await opts.mcpServerFactory();
3409
+ const transport = new StreamableHTTPServerTransport({
3410
+ sessionIdGenerator: void 0
3411
+ });
3412
+ transport.onerror = (err) => {
3413
+ console.error("[mcp transport.onerror]", err);
3414
+ };
3415
+ res.on("close", () => {
3416
+ void transport.close();
3417
+ void server2.close();
3418
+ });
3419
+ try {
3420
+ await server2.connect(transport);
3421
+ await transport.handleRequest(req, res);
3422
+ } catch (err) {
3423
+ console.error("[mcp] handleRequest threw:", err);
3424
+ if (!res.headersSent) {
3425
+ res.writeHead(500, { "content-type": "application/json" });
3426
+ res.end(
3427
+ JSON.stringify({
3428
+ error: "mcp_transport_failed",
3429
+ message: err instanceof Error ? err.message : String(err)
3430
+ })
3431
+ );
3432
+ }
3433
+ }
3434
+ }
3435
+ function handleHealth(_req, res) {
3436
+ res.writeHead(200, { "content-type": "application/json" });
3437
+ res.end(
3438
+ JSON.stringify({
3439
+ status: "ok",
3440
+ workspace: opts.meta.workspace,
3441
+ registered: opts.meta.registered,
3442
+ uptimeMs: Date.now() - startedAt
3443
+ })
3444
+ );
3445
+ }
3446
+ function handleEvents(req, res) {
3447
+ if (!authorize(req, res)) return;
3448
+ res.writeHead(200, {
3449
+ "content-type": "text/event-stream",
3450
+ "cache-control": "no-cache, no-transform",
3451
+ connection: "keep-alive"
3452
+ });
3453
+ res.write(`: connected
3454
+
3455
+ `);
3456
+ const off = opts.events.onAny((ev) => {
3457
+ res.write(`data: ${JSON.stringify(ev)}
3458
+
3459
+ `);
3460
+ });
3461
+ req.on("close", off);
3462
+ }
3463
+ async function handleListConversations(req, res) {
3464
+ if (!authorize(req, res)) return;
3465
+ const list = await opts.conversations.list();
3466
+ res.writeHead(200, { "content-type": "application/json" });
3467
+ res.end(JSON.stringify(list));
3468
+ }
3469
+ async function handleGetConversation(req, res, id) {
3470
+ if (!authorize(req, res)) return;
3471
+ try {
3472
+ const data = await opts.conversations.read(id);
3473
+ res.writeHead(200, { "content-type": "application/json" });
3474
+ res.end(JSON.stringify(data));
3475
+ } catch (err) {
3476
+ res.writeHead(404, { "content-type": "application/json" });
3477
+ res.end(
3478
+ JSON.stringify({
3479
+ error: "not_found",
3480
+ message: err instanceof Error ? err.message : String(err)
3481
+ })
3482
+ );
3483
+ }
3484
+ }
3485
+ async function handleHeartbeatTick(req, res) {
3486
+ if (!authorize(req, res)) return;
3487
+ await opts.heartbeat.fireNow();
3488
+ res.writeHead(202, { "content-type": "application/json" });
3489
+ res.end(JSON.stringify({ status: "fired" }));
3490
+ }
3491
+ function applyCors(req, res) {
3492
+ const origin = req.headers.origin ?? "*";
3493
+ res.setHeader("Access-Control-Allow-Origin", origin);
3494
+ res.setHeader("Vary", "Origin");
3495
+ res.setHeader("Access-Control-Allow-Credentials", "true");
3496
+ res.setHeader(
3497
+ "Access-Control-Allow-Headers",
3498
+ req.headers["access-control-request-headers"] ?? "authorization,content-type,mcp-session-id"
3499
+ );
3500
+ res.setHeader(
3501
+ "Access-Control-Allow-Methods",
3502
+ "GET,POST,PUT,PATCH,DELETE,OPTIONS"
3503
+ );
3504
+ }
3505
+ const server = createServer((req, res) => {
3506
+ void (async () => {
3507
+ try {
3508
+ const url = req.url ?? "/";
3509
+ const path = url.split("?")[0] ?? "/";
3510
+ if (req.headers["x-forwarded-for"]) {
3511
+ opts.events.emit({
3512
+ type: "remote-log",
3513
+ at: (/* @__PURE__ */ new Date()).toISOString(),
3514
+ line: `[http-in] ${req.method} ${url} host=${req.headers.host ?? "?"} xff=${String(req.headers["x-forwarded-for"])}`
3515
+ });
3516
+ }
3517
+ applyCors(req, res);
3518
+ if (req.method === "OPTIONS") {
3519
+ res.writeHead(204);
3520
+ res.end();
3521
+ return;
3522
+ }
3523
+ if (path === "/health" && req.method === "GET") {
3524
+ handleHealth(req, res);
3525
+ return;
3526
+ }
3527
+ if (path === "/events" && req.method === "GET") {
3528
+ handleEvents(req, res);
3529
+ return;
3530
+ }
3531
+ if (path === "/mcp") {
3532
+ await handleMcp(req, res);
3533
+ return;
3534
+ }
3535
+ if (path === "/conversations" && req.method === "GET") {
3536
+ await handleListConversations(req, res);
3537
+ return;
3538
+ }
3539
+ if (path.startsWith("/conversations/") && req.method === "GET") {
3540
+ const id = path.slice("/conversations/".length);
3541
+ await handleGetConversation(req, res, id);
3542
+ return;
3543
+ }
3544
+ if (path === "/heartbeat/tick" && req.method === "POST") {
3545
+ await handleHeartbeatTick(req, res);
3546
+ return;
3547
+ }
3548
+ if (opts.sessions && path.startsWith("/sessions")) {
3549
+ const handled = await handleSessions(
3550
+ req,
3551
+ res,
3552
+ path,
3553
+ opts.sessions,
3554
+ opts.resolveAgentAdapter
3555
+ );
3556
+ if (handled) return;
3557
+ }
3558
+ if (path === "/workspaces" && req.method === "GET") {
3559
+ try {
3560
+ const config = await loadWorkspacesConfig();
3561
+ res.writeHead(200, { "content-type": "application/json" });
3562
+ res.end(JSON.stringify(config));
3563
+ } catch (err) {
3564
+ res.writeHead(500, { "content-type": "application/json" });
3565
+ res.end(
3566
+ JSON.stringify({
3567
+ error: "workspaces_load_failed",
3568
+ message: err instanceof Error ? err.message : String(err)
3569
+ })
3570
+ );
3571
+ }
3572
+ return;
3573
+ }
3574
+ if (path === "/mcps/imports" && req.method === "GET") {
3575
+ const config = await loadImportedMcps();
3576
+ res.writeHead(200, { "content-type": "application/json" });
3577
+ res.end(JSON.stringify(config));
3578
+ return;
3579
+ }
3580
+ if (path === "/mcps/imports" && req.method === "POST") {
3581
+ const body = await readJsonBody(req);
3582
+ if (!body || typeof body.sourceMcpId !== "string") {
3583
+ res.writeHead(400, { "content-type": "application/json" });
3584
+ res.end(JSON.stringify({ error: "missing_sourceMcpId" }));
3585
+ return;
3586
+ }
3587
+ const discovered = await discoverMcps();
3588
+ const snapshot = discovered.find((d) => d.id === body.sourceMcpId);
3589
+ if (!snapshot) {
3590
+ res.writeHead(404, { "content-type": "application/json" });
3591
+ res.end(
3592
+ JSON.stringify({
3593
+ error: "discovered_mcp_not_found",
3594
+ sourceMcpId: body.sourceMcpId
3595
+ })
3596
+ );
3597
+ return;
3598
+ }
3599
+ const cfg = await loadImportedMcps();
3600
+ const next = addImport(cfg, {
3601
+ snapshot,
3602
+ ...body.alias ? { alias: body.alias } : {}
3603
+ });
3604
+ await saveImportedMcps(next);
3605
+ res.writeHead(201, { "content-type": "application/json" });
3606
+ res.end(JSON.stringify(next.imports.find((e) => e.id === snapshot.id)));
3607
+ return;
3608
+ }
3609
+ const importMatch = path.match(/^\/mcps\/imports\/(.+)$/);
3610
+ if (importMatch && req.method === "DELETE") {
3611
+ const id = decodeURIComponent(importMatch[1] ?? "");
3612
+ const cfg = await loadImportedMcps();
3613
+ if (!cfg.imports.some((e) => e.id === id)) {
3614
+ res.writeHead(404, { "content-type": "application/json" });
3615
+ res.end(JSON.stringify({ error: "import_not_found", id }));
3616
+ return;
3617
+ }
3618
+ await saveImportedMcps(removeImport(cfg, id));
3619
+ res.writeHead(200, { "content-type": "application/json" });
3620
+ res.end(JSON.stringify({ ok: true, id }));
3621
+ return;
3622
+ }
3623
+ if (path === "/mcps/discovered" && req.method === "GET") {
3624
+ try {
3625
+ const mcps = await discoverMcps();
3626
+ res.writeHead(200, { "content-type": "application/json" });
3627
+ res.end(JSON.stringify({ mcps }));
3628
+ } catch (err) {
3629
+ res.writeHead(500, { "content-type": "application/json" });
3630
+ res.end(
3631
+ JSON.stringify({
3632
+ error: "discovery_failed",
3633
+ message: err instanceof Error ? err.message : String(err)
3634
+ })
3635
+ );
3636
+ }
3637
+ return;
3638
+ }
3639
+ if (path === "/mcps/proxy/status" && req.method === "GET") {
3640
+ if (!opts.mcpProxy) {
3641
+ res.writeHead(501, { "content-type": "application/json" });
3642
+ res.end(JSON.stringify({ error: "mcp_proxy_not_configured" }));
3643
+ return;
3644
+ }
3645
+ try {
3646
+ const imports = await opts.mcpProxy.listAliases();
3647
+ res.writeHead(200, { "content-type": "application/json" });
3648
+ res.end(JSON.stringify({ imports }));
3649
+ } catch (err) {
3650
+ res.writeHead(500, { "content-type": "application/json" });
3651
+ res.end(
3652
+ JSON.stringify({
3653
+ error: "proxy_status_failed",
3654
+ message: err instanceof Error ? err.message : String(err)
3655
+ })
3656
+ );
3657
+ }
3658
+ return;
3659
+ }
3660
+ const proxyToolsMatch = path.match(/^\/mcps\/proxy\/tools\/(.+)$/);
3661
+ if (proxyToolsMatch && req.method === "GET") {
3662
+ if (!opts.mcpProxy) {
3663
+ res.writeHead(501, { "content-type": "application/json" });
3664
+ res.end(JSON.stringify({ error: "mcp_proxy_not_configured" }));
3665
+ return;
3666
+ }
3667
+ const alias = decodeURIComponent(proxyToolsMatch[1] ?? "");
3668
+ const out = await opts.mcpProxy.listTools(alias);
3669
+ res.writeHead(out.ok ? 200 : 502, {
3670
+ "content-type": "application/json"
3671
+ });
3672
+ res.end(JSON.stringify(out));
3673
+ return;
3674
+ }
3675
+ if (path === "/mcps/proxy/call" && req.method === "POST") {
3676
+ if (!opts.mcpProxy) {
3677
+ res.writeHead(501, { "content-type": "application/json" });
3678
+ res.end(JSON.stringify({ error: "mcp_proxy_not_configured" }));
3679
+ return;
3680
+ }
3681
+ const body = await readJsonBody(req);
3682
+ if (!body || typeof body.alias !== "string" || typeof body.toolName !== "string") {
3683
+ res.writeHead(400, { "content-type": "application/json" });
3684
+ res.end(
3685
+ JSON.stringify({
3686
+ error: "invalid_body",
3687
+ message: "expected { alias, toolName, args? }"
3688
+ })
3689
+ );
3690
+ return;
3691
+ }
3692
+ const result = await opts.mcpProxy.callTool(
3693
+ body.alias,
3694
+ body.toolName,
3695
+ body.args ?? {}
3696
+ );
3697
+ res.writeHead(result.ok ? 200 : 502, {
3698
+ "content-type": "application/json"
3699
+ });
3700
+ res.end(JSON.stringify(result));
3701
+ return;
3702
+ }
3703
+ if (path === "/adapters" && req.method === "GET") {
3704
+ if (!opts.listAgentAdapters) {
3705
+ res.writeHead(501, { "content-type": "application/json" });
3706
+ res.end(
3707
+ JSON.stringify({
3708
+ error: "lister_not_configured",
3709
+ message: "Daemon was started without `listAgentAdapters` \u2014 see @agentproto/cli's `listInstalledAdapters` for the canonical impl."
3710
+ })
3711
+ );
3712
+ return;
3713
+ }
3714
+ try {
3715
+ const adapters = await opts.listAgentAdapters();
3716
+ res.writeHead(200, { "content-type": "application/json" });
3717
+ res.end(JSON.stringify({ adapters }));
3718
+ } catch (err) {
3719
+ res.writeHead(500, { "content-type": "application/json" });
3720
+ res.end(
3721
+ JSON.stringify({
3722
+ error: "list_failed",
3723
+ message: err instanceof Error ? err.message : String(err)
3724
+ })
3725
+ );
3726
+ }
3727
+ return;
3728
+ }
3729
+ res.writeHead(404, { "content-type": "application/json" });
3730
+ res.end(JSON.stringify({ error: "not_found", path }));
3731
+ } catch (err) {
3732
+ if (!res.headersSent) {
3733
+ res.writeHead(500, { "content-type": "application/json" });
3734
+ res.end(
3735
+ JSON.stringify({
3736
+ error: "internal_error",
3737
+ message: err instanceof Error ? err.message : String(err)
3738
+ })
3739
+ );
3740
+ }
3741
+ }
3742
+ })();
3743
+ });
3744
+ const bind = opts.bind ?? "127.0.0.1";
3745
+ await new Promise((resolve7, reject) => {
3746
+ server.once("error", reject);
3747
+ server.listen(opts.port, bind, () => resolve7());
3748
+ });
3749
+ return {
3750
+ url: `http://${bind}:${opts.port}`,
3751
+ async stop() {
3752
+ await new Promise((resolve7) => server.close(() => resolve7()));
3753
+ }
3754
+ };
3755
+ }
3756
+ async function handleSessions(req, res, path, registry, resolveAgentAdapter) {
3757
+ const json = (status, body) => {
3758
+ res.writeHead(status, { "content-type": "application/json" });
3759
+ res.end(JSON.stringify(body));
3760
+ };
3761
+ if (path === "/sessions" && req.method === "GET") {
3762
+ json(200, { sessions: registry.list() });
3763
+ return true;
3764
+ }
3765
+ if (path === "/sessions/agent" && req.method === "POST") {
3766
+ if (!resolveAgentAdapter) {
3767
+ json(501, {
3768
+ error: "agent_resolver_not_configured",
3769
+ message: "POST /sessions/agent needs the host to inject `resolveAgentAdapter` (e.g. via @agentproto/cli's resolveAdapter shim)."
3770
+ });
3771
+ return true;
3772
+ }
3773
+ const body = await readJsonBody(req);
3774
+ if (!body || typeof body !== "object") {
3775
+ json(400, { error: "invalid_body" });
3776
+ return true;
3777
+ }
3778
+ const b = body;
3779
+ const adapter = typeof b.adapter === "string" ? b.adapter : "";
3780
+ if (!adapter) {
3781
+ json(400, { error: "missing_adapter" });
3782
+ return true;
3783
+ }
3784
+ let cwd = typeof b.cwd === "string" && b.cwd.length > 0 ? b.cwd : null;
3785
+ let workspaceSlug = typeof b.workspaceSlug === "string" ? b.workspaceSlug : "";
3786
+ if (!cwd) {
3787
+ try {
3788
+ const config = await loadWorkspacesConfig();
3789
+ const ws = workspaceSlug ? findWorkspace(config, workspaceSlug) : getActiveWorkspace(config);
3790
+ if (ws) {
3791
+ cwd = ws.path;
3792
+ workspaceSlug = ws.slug;
3793
+ }
3794
+ } catch {
3795
+ }
3796
+ }
3797
+ if (!cwd) {
3798
+ cwd = process.cwd();
3799
+ console.warn(
3800
+ `[/sessions/agent] no cwd resolvable for adapter=${adapter} (no body.cwd, no workspaceSlug match in ~/.agentproto/workspaces.json, no active workspace) \u2014 falling back to daemon's cwd ${cwd}`
3801
+ );
3802
+ }
3803
+ if (!workspaceSlug) workspaceSlug = "default";
3804
+ const resolved = await resolveAgentAdapter(adapter);
3805
+ if (!resolved) {
3806
+ json(404, { error: "adapter_not_found", adapter });
3807
+ return true;
3808
+ }
3809
+ try {
3810
+ const agentSession = await resolved.startSession({ cwd });
3811
+ const desc = registry.spawnAgent({
3812
+ workspaceSlug,
3813
+ cwd,
3814
+ agentSession,
3815
+ adapterSlug: adapter,
3816
+ ...typeof b.prompt === "string" ? { initialPrompt: b.prompt } : {},
3817
+ ...typeof b.label === "string" ? { label: b.label } : {},
3818
+ ...resolved.commandPreview ? { commandPreview: resolved.commandPreview } : {}
3819
+ });
3820
+ json(201, desc);
3821
+ } catch (err) {
3822
+ json(500, {
3823
+ error: "agent_spawn_failed",
3824
+ message: err instanceof Error ? err.message : String(err)
3825
+ });
3826
+ }
3827
+ return true;
3828
+ }
3829
+ const promptMatch = path.match(/^\/sessions\/([^/]+)\/prompt$/);
3830
+ if (promptMatch && req.method === "POST") {
3831
+ const id2 = promptMatch[1];
3832
+ if (!id2) return false;
3833
+ const body = await readJsonBody(req);
3834
+ const prompt = body?.prompt;
3835
+ if (typeof prompt !== "string") {
3836
+ json(400, { error: "missing_prompt" });
3837
+ return true;
3838
+ }
3839
+ const reqUrl = req.url ?? "";
3840
+ const queryString = reqUrl.includes("?") ? reqUrl.slice(reqUrl.indexOf("?") + 1) : "";
3841
+ const wait = new URLSearchParams(queryString).get("wait");
3842
+ const fireAndForget = wait === "false" || wait === "0";
3843
+ try {
3844
+ if (fireAndForget) {
3845
+ registry.enqueuePrompt(id2, prompt);
3846
+ json(202, { ok: true, id: id2, queued: true });
3847
+ } else {
3848
+ await registry.sendPrompt(id2, prompt);
3849
+ json(200, { ok: true, id: id2 });
3850
+ }
3851
+ } catch (err) {
3852
+ const msg = err instanceof Error ? err.message : String(err);
3853
+ const status = msg.includes("no session") ? 404 : msg.includes("not an agent") ? 400 : msg.includes("mid-turn") ? 409 : 500;
3854
+ json(status, { error: "send_prompt_failed", message: msg });
3855
+ }
3856
+ return true;
3857
+ }
3858
+ if (path === "/sessions" && req.method === "POST") {
3859
+ const body = await readJsonBody(req);
3860
+ if (!body || typeof body !== "object") {
3861
+ json(400, { error: "invalid_body" });
3862
+ return true;
3863
+ }
3864
+ const b = body;
3865
+ const argv = Array.isArray(b.argv) ? b.argv.filter((v) => typeof v === "string") : [];
3866
+ if (argv.length === 0) {
3867
+ json(400, { error: "missing_argv" });
3868
+ return true;
3869
+ }
3870
+ try {
3871
+ const desc = registry.spawn({
3872
+ kind: b.kind === "terminal" || b.kind === "agent-cli" || b.kind === "command" ? b.kind : "command",
3873
+ workspaceSlug: typeof b.workspaceSlug === "string" ? b.workspaceSlug : "default",
3874
+ cwd: typeof b.cwd === "string" ? b.cwd : process.cwd(),
3875
+ argv,
3876
+ env: b.env && typeof b.env === "object" ? b.env : void 0,
3877
+ label: typeof b.label === "string" ? b.label : void 0
3878
+ });
3879
+ json(201, desc);
3880
+ } catch (err) {
3881
+ json(500, {
3882
+ error: "spawn_failed",
3883
+ message: err instanceof Error ? err.message : String(err)
3884
+ });
3885
+ }
3886
+ return true;
3887
+ }
3888
+ const idMatch = path.match(/^\/sessions\/([^/]+)(\/stream|\/kill)?$/);
3889
+ if (!idMatch) return false;
3890
+ const [, id, suffix] = idMatch;
3891
+ if (!id) return false;
3892
+ if (suffix === "/stream" && req.method === "GET") {
3893
+ const desc = registry.get(id);
3894
+ if (!desc) {
3895
+ json(404, { error: "session_not_found", id });
3896
+ return true;
3897
+ }
3898
+ res.writeHead(200, {
3899
+ "content-type": "text/event-stream",
3900
+ "cache-control": "no-cache",
3901
+ connection: "keep-alive"
3902
+ });
3903
+ const ping = setInterval(() => {
3904
+ try {
3905
+ res.write(`: keep-alive
3906
+
3907
+ `);
3908
+ } catch {
3909
+ clearInterval(ping);
3910
+ }
3911
+ }, 25e3);
3912
+ const unsub = registry.attach(id, (line, stream) => {
3913
+ try {
3914
+ res.write(
3915
+ `data: ${JSON.stringify({ line, stream })}
3916
+
3917
+ `
3918
+ );
3919
+ } catch {
3920
+ }
3921
+ });
3922
+ if (!unsub) {
3923
+ clearInterval(ping);
3924
+ res.end();
3925
+ return true;
3926
+ }
3927
+ req.once("close", () => {
3928
+ unsub();
3929
+ clearInterval(ping);
3930
+ });
3931
+ return true;
3932
+ }
3933
+ if (suffix === "/kill" && req.method === "POST") {
3934
+ const ok = registry.kill(id);
3935
+ json(ok ? 200 : 404, { ok, id });
3936
+ return true;
3937
+ }
3938
+ if (!suffix && req.method === "GET") {
3939
+ const desc = registry.get(id);
3940
+ if (!desc) {
3941
+ json(404, { error: "session_not_found", id });
3942
+ return true;
3943
+ }
3944
+ json(200, desc);
3945
+ return true;
3946
+ }
3947
+ if (!suffix && req.method === "DELETE") {
3948
+ const ok = registry.forget(id);
3949
+ json(ok ? 200 : 404, { ok, id });
3950
+ return true;
3951
+ }
3952
+ return false;
3953
+ }
3954
+ async function readJsonBody(req) {
3955
+ const chunks = [];
3956
+ for await (const chunk of req) {
3957
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
3958
+ }
3959
+ if (chunks.length === 0) return null;
3960
+ try {
3961
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
3962
+ } catch {
3963
+ return null;
3964
+ }
3965
+ }
3966
+ var SESSIONS_FILE_PATH = () => resolve(homedir(), ".agentproto", "sessions.json");
3967
+ var RECENT_LINES_CAP = 500;
3968
+ var PERSIST_DEBOUNCE_MS = 1500;
3969
+ function createSessionsRegistry(opts) {
3970
+ const persistPath = SESSIONS_FILE_PATH();
3971
+ const sessions = /* @__PURE__ */ new Map();
3972
+ let persistTimer = null;
3973
+ const schedulePersist = () => {
3974
+ if (persistTimer) clearTimeout(persistTimer);
3975
+ persistTimer = setTimeout(() => {
3976
+ void persistSnapshot();
3977
+ }, PERSIST_DEBOUNCE_MS);
3978
+ };
3979
+ const persistSnapshot = async () => {
3980
+ try {
3981
+ const snapshot = {
3982
+ savedAt: (/* @__PURE__ */ new Date()).toISOString(),
3983
+ sessions: Array.from(sessions.values()).map((s) => s.desc)
3984
+ };
3985
+ await promises.mkdir(dirname(persistPath), { recursive: true });
3986
+ await promises.writeFile(persistPath, JSON.stringify(snapshot, null, 2) + "\n");
3987
+ } catch (err) {
3988
+ console.warn(
3989
+ `[sessions] persist failed: ${err instanceof Error ? err.message : String(err)}`
3990
+ );
3991
+ }
3992
+ };
3993
+ const appendLine = (rt, line, stream) => {
3994
+ rt.recentLines.push(line);
3995
+ if (rt.recentLines.length > RECENT_LINES_CAP) {
3996
+ rt.recentLines.splice(0, rt.recentLines.length - RECENT_LINES_CAP);
3997
+ }
3998
+ rt.desc.lastOutputAt = (/* @__PURE__ */ new Date()).toISOString();
3999
+ rt.emitter.emit("line", { line, stream });
4000
+ };
4001
+ const projectEvent = (rt, evt) => {
4002
+ switch (evt.kind) {
4003
+ case "text-delta":
4004
+ if (evt.text) {
4005
+ const combined = rt.textBuf + evt.text;
4006
+ const lines = combined.split(/\r?\n/);
4007
+ rt.textBuf = lines.pop() ?? "";
4008
+ for (const line of lines) appendLine(rt, line, "stdout");
4009
+ }
4010
+ break;
4011
+ case "thought":
4012
+ if (evt.text) {
4013
+ const combined = rt.thoughtBuf + evt.text;
4014
+ const lines = combined.split(/\r?\n/);
4015
+ rt.thoughtBuf = lines.pop() ?? "";
4016
+ for (const line of lines) {
4017
+ if (line.trim()) {
4018
+ appendLine(rt, `\x1B[2m[thought] ${line}\x1B[0m`, "stdout");
4019
+ }
4020
+ }
4021
+ }
4022
+ break;
4023
+ case "tool-call":
4024
+ appendLine(
4025
+ rt,
4026
+ `\x1B[36m[tool] ${evt.toolName ?? "?"}\x1B[0m`,
4027
+ "stdout"
4028
+ );
4029
+ break;
4030
+ case "tool-result":
4031
+ if (evt.isError)
4032
+ appendLine(rt, `\x1B[31m[tool-error]\x1B[0m`, "stderr");
4033
+ break;
4034
+ case "agent-prompt":
4035
+ appendLine(rt, `\x1B[33m[awaiting input]\x1B[0m`, "stdout");
4036
+ break;
4037
+ case "turn-end": {
4038
+ if (rt.thoughtBuf.trim()) {
4039
+ appendLine(
4040
+ rt,
4041
+ `\x1B[2m[thought] ${rt.thoughtBuf}\x1B[0m`,
4042
+ "stdout"
4043
+ );
4044
+ }
4045
+ rt.thoughtBuf = "";
4046
+ if (rt.textBuf) {
4047
+ appendLine(rt, rt.textBuf, "stdout");
4048
+ rt.textBuf = "";
4049
+ }
4050
+ appendLine(
4051
+ rt,
4052
+ `\x1B[2m\u2500\u2500 turn-end (${evt.reason ?? "completed"}) \u2500\u2500\x1B[0m`,
4053
+ "stdout"
4054
+ );
4055
+ break;
4056
+ }
4057
+ case "error": {
4058
+ const code = typeof evt.error?.code === "number" ? ` (code ${evt.error.code})` : "";
4059
+ appendLine(
4060
+ rt,
4061
+ `\x1B[31m[error]${code} ${evt.error?.message ?? "unknown"}\x1B[0m`,
4062
+ "stderr"
4063
+ );
4064
+ const data = evt.error?.data;
4065
+ if (data && typeof data === "object" && typeof data.stderr === "string") {
4066
+ for (const line of data.stderr.split(/\r?\n/)) {
4067
+ if (line) appendLine(rt, `\x1B[2m ${line}\x1B[0m`, "stderr");
4068
+ }
4069
+ }
4070
+ break;
4071
+ }
4072
+ }
4073
+ };
4074
+ const validateAgentTurn = (id, caller) => {
4075
+ const rt = sessions.get(id);
4076
+ if (!rt) throw new Error(`${caller}: no session "${id}"`);
4077
+ if (!rt.agentSession) {
4078
+ throw new Error(
4079
+ `${caller}: session "${id}" is not an agent session (kind=${rt.desc.kind})`
4080
+ );
4081
+ }
4082
+ if (rt.busy) {
4083
+ throw new Error(
4084
+ `${caller}: session "${id}" is mid-turn \u2014 wait for it to finish or cancel`
4085
+ );
4086
+ }
4087
+ return rt;
4088
+ };
4089
+ const runAgentTurn = async (rt, message) => {
4090
+ if (!rt.agentSession) {
4091
+ throw new Error("runAgentTurn: session has no agentSession");
4092
+ }
4093
+ rt.busy = true;
4094
+ try {
4095
+ appendLine(
4096
+ rt,
4097
+ `\x1B[2m\u2500\u2500 \u25B6 ${typeof message === "string" ? message : JSON.stringify(message)} \u2500\u2500\x1B[0m`,
4098
+ "stdout"
4099
+ );
4100
+ const wrapped = typeof message === "string" ? { type: "text", text: message } : message;
4101
+ for await (const evt of rt.agentSession.send(wrapped)) {
4102
+ projectEvent(rt, evt);
4103
+ }
4104
+ } catch (err) {
4105
+ rt.desc.status = "error";
4106
+ rt.desc.endedAt = (/* @__PURE__ */ new Date()).toISOString();
4107
+ appendLine(
4108
+ rt,
4109
+ `[turn error] ${err instanceof Error ? err.message : String(err)}`,
4110
+ "stderr"
4111
+ );
4112
+ schedulePersist();
4113
+ } finally {
4114
+ rt.busy = false;
4115
+ }
4116
+ };
4117
+ const wireOutputStreams = (rt) => {
4118
+ const onChunk = (stream) => (chunk) => {
4119
+ const text3 = typeof chunk === "string" ? chunk : chunk.toString("utf8");
4120
+ for (const line of text3.split(/\r?\n/)) {
4121
+ if (line.length > 0) appendLine(rt, line, stream);
4122
+ }
4123
+ };
4124
+ if (!rt.child) return;
4125
+ rt.child.stdout?.on("data", onChunk("stdout"));
4126
+ rt.child.stderr?.on("data", onChunk("stderr"));
4127
+ };
4128
+ return {
4129
+ spawn(input2) {
4130
+ const id = input2.id ?? `sess_${randomUUID().slice(0, 8)}`;
4131
+ if (sessions.has(id)) {
4132
+ throw new Error(`sessions.spawn: id "${id}" already in use`);
4133
+ }
4134
+ const env = { ...process.env, ...input2.env ?? {} };
4135
+ delete env.NODE_OPTIONS;
4136
+ const [bin, ...args] = input2.argv;
4137
+ if (!bin) throw new Error("sessions.spawn: argv must include a binary");
4138
+ const stdinMode = input2.keepStdin ? "pipe" : "ignore";
4139
+ const child = spawn(bin, args, {
4140
+ cwd: input2.cwd,
4141
+ env,
4142
+ stdio: [stdinMode, "pipe", "pipe"]
4143
+ });
4144
+ const desc = {
4145
+ id,
4146
+ kind: input2.kind,
4147
+ workspaceSlug: input2.workspaceSlug,
4148
+ command: [bin, ...args].map(quoteArg).join(" "),
4149
+ pid: child.pid ?? null,
4150
+ status: "starting",
4151
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
4152
+ ...input2.label ? { label: input2.label } : {}
4153
+ };
4154
+ const rt = {
4155
+ desc,
4156
+ child,
4157
+ recentLines: [],
4158
+ emitter: new EventEmitter(),
4159
+ busy: false,
4160
+ textBuf: "",
4161
+ thoughtBuf: ""
4162
+ };
4163
+ rt.emitter.setMaxListeners(50);
4164
+ sessions.set(id, rt);
4165
+ wireOutputStreams(rt);
4166
+ child.once("spawn", () => {
4167
+ desc.status = "running";
4168
+ rt.emitter.emit("status", desc.status);
4169
+ schedulePersist();
4170
+ });
4171
+ child.once("error", (err) => {
4172
+ desc.status = "error";
4173
+ desc.endedAt = (/* @__PURE__ */ new Date()).toISOString();
4174
+ appendLine(rt, `[spawn error] ${err.message}`, "stderr");
4175
+ rt.emitter.emit("status", desc.status);
4176
+ schedulePersist();
4177
+ });
4178
+ child.once("exit", (code, signal) => {
4179
+ if (signal && desc.status !== "killed") desc.status = "killed";
4180
+ else if (desc.status === "running" || desc.status === "starting") {
4181
+ desc.status = "exited";
4182
+ }
4183
+ desc.endedAt = (/* @__PURE__ */ new Date()).toISOString();
4184
+ if (typeof code === "number") desc.exitCode = code;
4185
+ rt.emitter.emit("status", desc.status);
4186
+ schedulePersist();
4187
+ });
4188
+ schedulePersist();
4189
+ return desc;
4190
+ },
4191
+ register(input2) {
4192
+ if (sessions.has(input2.id)) {
4193
+ const existing = sessions.get(input2.id);
4194
+ if (existing) return existing.desc;
4195
+ }
4196
+ const desc = {
4197
+ id: input2.id,
4198
+ kind: input2.kind ?? "command",
4199
+ workspaceSlug: input2.workspaceSlug,
4200
+ command: input2.command,
4201
+ pid: input2.child.pid ?? null,
4202
+ status: input2.child.killed ? "killed" : "running",
4203
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
4204
+ ...input2.label ? { label: input2.label } : {}
4205
+ };
4206
+ const rt = {
4207
+ desc,
4208
+ child: input2.child,
4209
+ recentLines: [],
4210
+ emitter: new EventEmitter(),
4211
+ busy: false,
4212
+ textBuf: "",
4213
+ thoughtBuf: ""
4214
+ };
4215
+ rt.emitter.setMaxListeners(50);
4216
+ sessions.set(input2.id, rt);
4217
+ wireOutputStreams(rt);
4218
+ input2.child.once("exit", (code, signal) => {
4219
+ if (signal && desc.status !== "killed") desc.status = "killed";
4220
+ else if (desc.status === "running") desc.status = "exited";
4221
+ desc.endedAt = (/* @__PURE__ */ new Date()).toISOString();
4222
+ if (typeof code === "number") desc.exitCode = code;
4223
+ rt.emitter.emit("status", desc.status);
4224
+ schedulePersist();
4225
+ });
4226
+ input2.child.once("error", (err) => {
4227
+ desc.status = "error";
4228
+ desc.endedAt = (/* @__PURE__ */ new Date()).toISOString();
4229
+ appendLine(rt, `[child error] ${err.message}`, "stderr");
4230
+ rt.emitter.emit("status", desc.status);
4231
+ schedulePersist();
4232
+ });
4233
+ schedulePersist();
4234
+ return desc;
4235
+ },
4236
+ spawnAgent(input2) {
4237
+ const id = `sess_${randomUUID().slice(0, 8)}`;
4238
+ const desc = {
4239
+ id,
4240
+ kind: "agent-cli",
4241
+ workspaceSlug: input2.workspaceSlug,
4242
+ command: input2.commandPreview ?? `${input2.adapterSlug} (agent)`,
4243
+ pid: null,
4244
+ status: "running",
4245
+ // driver already started the session
4246
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
4247
+ ...input2.label ? { label: input2.label } : {}
4248
+ };
4249
+ const rt = {
4250
+ desc,
4251
+ agentSession: input2.agentSession,
4252
+ adapterSlug: input2.adapterSlug,
4253
+ recentLines: [],
4254
+ emitter: new EventEmitter(),
4255
+ busy: false,
4256
+ textBuf: "",
4257
+ thoughtBuf: ""
4258
+ };
4259
+ rt.emitter.setMaxListeners(50);
4260
+ sessions.set(id, rt);
4261
+ appendLine(
4262
+ rt,
4263
+ `\u2500\u2500 ${input2.adapterSlug} agent session ${input2.agentSession.sessionId} (cwd ${input2.cwd}) \u2500\u2500`,
4264
+ "stdout"
4265
+ );
4266
+ schedulePersist();
4267
+ if (input2.initialPrompt) {
4268
+ void runAgentTurn(rt, input2.initialPrompt).catch((err) => {
4269
+ appendLine(
4270
+ rt,
4271
+ `[turn error] ${err instanceof Error ? err.message : String(err)}`,
4272
+ "stderr"
4273
+ );
4274
+ });
4275
+ }
4276
+ return desc;
4277
+ },
4278
+ async sendPrompt(id, message) {
4279
+ const rt = validateAgentTurn(id, "sendPrompt");
4280
+ await runAgentTurn(rt, message);
4281
+ },
4282
+ enqueuePrompt(id, message) {
4283
+ const rt = validateAgentTurn(id, "enqueuePrompt");
4284
+ void runAgentTurn(rt, message).catch(() => {
4285
+ });
4286
+ },
4287
+ list() {
4288
+ return Array.from(sessions.values()).map((s) => s.desc).sort((a, b) => b.startedAt.localeCompare(a.startedAt));
4289
+ },
4290
+ get(id) {
4291
+ return sessions.get(id)?.desc;
4292
+ },
4293
+ attach(id, onLine) {
4294
+ const rt = sessions.get(id);
4295
+ if (!rt) return null;
4296
+ for (const line of rt.recentLines) onLine(line, "stdout");
4297
+ const handler = (evt) => onLine(evt.line, evt.stream);
4298
+ rt.emitter.on("line", handler);
4299
+ return () => rt.emitter.off("line", handler);
4300
+ },
4301
+ kill(id, signal = "SIGTERM") {
4302
+ const rt = sessions.get(id);
4303
+ if (!rt) return false;
4304
+ if (rt.desc.status === "exited" || rt.desc.status === "killed" || rt.desc.status === "error") {
4305
+ return false;
4306
+ }
4307
+ rt.desc.status = "killed";
4308
+ rt.desc.endedAt = (/* @__PURE__ */ new Date()).toISOString();
4309
+ if (rt.agentSession) {
4310
+ void rt.agentSession.close().catch(() => void 0);
4311
+ }
4312
+ rt.child?.kill(signal);
4313
+ schedulePersist();
4314
+ return true;
4315
+ },
4316
+ forget(id) {
4317
+ const rt = sessions.get(id);
4318
+ if (!rt) return false;
4319
+ rt.emitter.removeAllListeners();
4320
+ sessions.delete(id);
4321
+ schedulePersist();
4322
+ return true;
4323
+ },
4324
+ shutdown() {
4325
+ if (persistTimer) clearTimeout(persistTimer);
4326
+ for (const rt of sessions.values()) {
4327
+ rt.emitter.removeAllListeners();
4328
+ if (rt.desc.status === "running" || rt.desc.status === "starting") {
4329
+ if (rt.agentSession) {
4330
+ void rt.agentSession.close().catch(() => void 0);
4331
+ }
4332
+ rt.child?.kill("SIGTERM");
4333
+ }
4334
+ }
4335
+ sessions.clear();
4336
+ }
4337
+ };
4338
+ }
4339
+ function quoteArg(arg) {
4340
+ if (arg === "") return '""';
4341
+ if (/^[a-zA-Z0-9._/=:@,+-]+$/.test(arg)) return arg;
4342
+ return `"${arg.replace(/(["\\$`])/g, "\\$1")}"`;
4343
+ }
4344
+ var McpProxyRegistry = class {
4345
+ clients = /* @__PURE__ */ new Map();
4346
+ lastMtimeMs = 0;
4347
+ /** Sentinel so we know whether the file has *ever* been read.
4348
+ * Distinguishes "file missing on first call" (legit empty state)
4349
+ * from "we've read it before and it's gone" (treat as empty too,
4350
+ * but only after closing live clients). */
4351
+ hasReadOnce = false;
4352
+ /**
4353
+ * Reload the imports file when its mtime has advanced. Avoids
4354
+ * repeated parsing when nothing changed — most calls are no-ops.
4355
+ * Disconnects clients whose import was removed; connects the new
4356
+ * set lazily (the first call that touches them).
4357
+ */
4358
+ async ensureCurrent() {
4359
+ let mtimeMs = 0;
4360
+ try {
4361
+ const st = await promises.stat(IMPORTED_MCPS_PATH());
4362
+ mtimeMs = st.mtimeMs;
4363
+ } catch {
4364
+ mtimeMs = 0;
4365
+ }
4366
+ if (this.hasReadOnce && mtimeMs === this.lastMtimeMs) return;
4367
+ const config = await loadImportedMcps().catch(() => ({
4368
+ version: 1,
4369
+ imports: []
4370
+ }));
4371
+ const wantedIds = new Set(config.imports.map((e) => e.id));
4372
+ for (const [id, c] of this.clients.entries()) {
4373
+ if (!wantedIds.has(id)) {
4374
+ await safeClose(c.client);
4375
+ this.clients.delete(id);
4376
+ }
4377
+ }
4378
+ for (const entry of config.imports) {
4379
+ const existing = this.clients.get(entry.id);
4380
+ if (existing) {
4381
+ if (JSON.stringify(existing.entry.snapshot) !== JSON.stringify(entry.snapshot)) {
4382
+ await safeClose(existing.client);
4383
+ this.clients.set(entry.id, freshPlaceholder(entry));
4384
+ } else {
4385
+ existing.entry = entry;
4386
+ }
4387
+ } else {
4388
+ this.clients.set(entry.id, freshPlaceholder(entry));
4389
+ }
4390
+ }
4391
+ this.lastMtimeMs = mtimeMs;
4392
+ this.hasReadOnce = true;
4393
+ }
4394
+ /**
4395
+ * Lazy connect: the first call to `connectIfNeeded(alias)`
4396
+ * actually opens the transport + handshakes. Subsequent calls
4397
+ * reuse the live client.
4398
+ */
4399
+ async connectIfNeeded(alias) {
4400
+ await this.ensureCurrent();
4401
+ const handle = this.findByAlias(alias);
4402
+ if (!handle) return null;
4403
+ if (handle.client) return handle;
4404
+ try {
4405
+ const client = await openClient(handle.entry);
4406
+ handle.client = client;
4407
+ handle.lastError = null;
4408
+ const t = await client.listTools();
4409
+ handle.tools = t.tools.map(toDescriptor);
4410
+ return handle;
4411
+ } catch (err) {
4412
+ handle.client = null;
4413
+ handle.tools = null;
4414
+ handle.lastError = err instanceof Error ? err.message : String(err);
4415
+ return handle;
4416
+ }
4417
+ }
4418
+ /** Find a client handle by alias *or* import id (defensive — agents
4419
+ * sometimes pass the id from `list_imported_mcps`). */
4420
+ findByAlias(aliasOrId) {
4421
+ for (const c of this.clients.values()) {
4422
+ if (c.entry.alias === aliasOrId) return c;
4423
+ if (c.entry.id === aliasOrId) return c;
4424
+ }
4425
+ return void 0;
4426
+ }
4427
+ async listAliases() {
4428
+ await this.ensureCurrent();
4429
+ return Array.from(this.clients.values()).map((c) => ({
4430
+ alias: c.entry.alias,
4431
+ importId: c.entry.id,
4432
+ source: c.entry.snapshot.source,
4433
+ type: c.entry.snapshot.type,
4434
+ status: c.client ? "connected" : c.lastError ? "error" : "pending",
4435
+ toolCount: c.tools?.length ?? 0,
4436
+ ...c.lastError ? { lastError: c.lastError } : {}
4437
+ }));
4438
+ }
4439
+ async listTools(alias) {
4440
+ const handle = await this.connectIfNeeded(alias);
4441
+ if (!handle) return { ok: false, error: `unknown alias "${alias}"` };
4442
+ if (!handle.client || !handle.tools) {
4443
+ return {
4444
+ ok: false,
4445
+ error: handle.lastError ?? "client not connected"
4446
+ };
4447
+ }
4448
+ return { ok: true, tools: handle.tools };
4449
+ }
4450
+ async callTool(alias, toolName, args) {
4451
+ const handle = await this.connectIfNeeded(alias);
4452
+ if (!handle) return { ok: false, error: `unknown alias "${alias}"` };
4453
+ if (!handle.client) {
4454
+ return {
4455
+ ok: false,
4456
+ error: handle.lastError ?? "client not connected"
4457
+ };
4458
+ }
4459
+ try {
4460
+ const result = await handle.client.callTool({
4461
+ name: toolName,
4462
+ arguments: args && typeof args === "object" ? args : {}
4463
+ });
4464
+ return { ok: true, result };
4465
+ } catch (err) {
4466
+ const msg = err instanceof Error ? err.message : String(err);
4467
+ if (/closed|disconnect|EPIPE|ECONNRESET/i.test(msg)) {
4468
+ await safeClose(handle.client);
4469
+ handle.client = null;
4470
+ handle.tools = null;
4471
+ handle.lastError = msg;
4472
+ }
4473
+ return { ok: false, error: msg };
4474
+ }
4475
+ }
4476
+ async closeAll() {
4477
+ await Promise.all(
4478
+ Array.from(this.clients.values()).map((c) => safeClose(c.client))
4479
+ );
4480
+ this.clients.clear();
4481
+ }
4482
+ };
4483
+ function freshPlaceholder(entry) {
4484
+ return { entry, client: null, tools: null, lastError: null };
4485
+ }
4486
+ function toDescriptor(tool) {
4487
+ return {
4488
+ name: tool.name,
4489
+ ...tool.description ? { description: tool.description } : {},
4490
+ ...tool.inputSchema ? { inputSchema: tool.inputSchema } : {}
4491
+ };
4492
+ }
4493
+ function expandEnvPlaceholders(raw) {
4494
+ const out = {};
4495
+ for (const [k, v] of Object.entries(raw)) {
4496
+ const expanded = v.replace(
4497
+ /\$\{([A-Z_][A-Z0-9_]*)(?::-([^}]*))?\}/gi,
4498
+ (_match, name, fallback) => {
4499
+ const fromEnv = process.env[name];
4500
+ if (fromEnv !== void 0) return fromEnv;
4501
+ if (fallback !== void 0) return fallback;
4502
+ return "";
4503
+ }
4504
+ );
4505
+ if (expanded.length === 0) continue;
4506
+ out[k] = expanded;
4507
+ }
4508
+ return out;
4509
+ }
4510
+ async function safeClose(client) {
4511
+ if (!client) return;
4512
+ try {
4513
+ await client.close();
4514
+ } catch {
4515
+ }
4516
+ }
4517
+ async function openClient(entry) {
4518
+ const snap = entry.snapshot;
4519
+ const client = new Client(
4520
+ { name: "agentproto-daemon-proxy", version: "0.1.0" },
4521
+ { capabilities: {} }
4522
+ );
4523
+ if (snap.type === "stdio") {
4524
+ if (!snap.command) {
4525
+ throw new Error(
4526
+ `import "${entry.alias}" is stdio but has no command field`
4527
+ );
4528
+ }
4529
+ const expandedEnv = expandEnvPlaceholders(snap.env ?? {});
4530
+ const transport = new StdioClientTransport({
4531
+ command: snap.command,
4532
+ args: snap.args ?? [],
4533
+ env: { ...process.env, ...expandedEnv }
4534
+ });
4535
+ await client.connect(transport);
4536
+ return client;
4537
+ }
4538
+ if (snap.type === "http") {
4539
+ if (!snap.url) throw new Error(`import "${entry.alias}" has no url`);
4540
+ const transport = new StreamableHTTPClientTransport(new URL(snap.url), {
4541
+ requestInit: {
4542
+ headers: snap.headers ?? {}
4543
+ }
4544
+ });
4545
+ await client.connect(transport);
4546
+ return client;
4547
+ }
4548
+ if (snap.type === "sse") {
4549
+ if (!snap.url) throw new Error(`import "${entry.alias}" has no url`);
4550
+ const transport = new SSEClientTransport(new URL(snap.url), {
4551
+ requestInit: {
4552
+ headers: snap.headers ?? {}
4553
+ }
4554
+ });
4555
+ await client.connect(transport);
4556
+ return client;
4557
+ }
4558
+ throw new Error(
4559
+ `import "${entry.alias}" has unsupported transport type "${snap.type}"`
4560
+ );
4561
+ }
4562
+ var execFileAsync = promisify(execFile);
4563
+ var URL_REGEX = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/i;
4564
+ var STARTUP_TIMEOUT_MS = 3e4;
4565
+ function quickTunnelProvider() {
4566
+ let child = null;
4567
+ let configPath = null;
4568
+ return {
4569
+ id: "quick",
4570
+ async start(opts) {
4571
+ await assertCloudflaredOnPath();
4572
+ const targetUrl = `http://${opts.target.host}:${opts.target.port}`;
4573
+ const id = randomBytes(4).toString("hex");
4574
+ const dir = join(opts.workspace, ".agentproto");
4575
+ await mkdir(dir, { recursive: true });
4576
+ configPath = join(dir, `cloudflared-${id}.yml`);
4577
+ const configBody = [
4578
+ `# generated by agentproto-runtime \u2014 sentinel only`,
4579
+ `# overrides ~/.cloudflared/config.yml so its ingress rules`,
4580
+ `# don't shadow the inline --url rule. Safe to delete when the`,
4581
+ `# tunnel is down.`,
4582
+ `no-autoupdate: true`,
4583
+ `loglevel: debug`,
4584
+ ``
4585
+ ].join("\n");
4586
+ await writeFile(configPath, configBody, "utf8");
4587
+ const proc = spawn(
4588
+ "cloudflared",
4589
+ ["tunnel", "--config", configPath, "--url", targetUrl],
4590
+ { stdio: ["ignore", "pipe", "pipe"], shell: false }
4591
+ );
4592
+ child = proc;
4593
+ const url = await new Promise((resolve7, reject) => {
4594
+ let settled = false;
4595
+ const timer = setTimeout(() => {
4596
+ if (settled) return;
4597
+ settled = true;
4598
+ try {
4599
+ proc.kill("SIGTERM");
4600
+ } catch {
4601
+ }
4602
+ reject(
4603
+ new Error(
4604
+ `cloudflared did not emit a public URL within ${STARTUP_TIMEOUT_MS / 1e3}s`
4605
+ )
4606
+ );
4607
+ }, STARTUP_TIMEOUT_MS);
4608
+ const onData = (chunk) => {
4609
+ const text3 = chunk.toString("utf8");
4610
+ for (const line of text3.split(/\r?\n/)) {
4611
+ if (line.length > 0) opts.onLog?.(`[cloudflared] ${line}`);
4612
+ }
4613
+ if (settled) return;
4614
+ const match = text3.match(URL_REGEX);
4615
+ if (match) {
4616
+ settled = true;
4617
+ clearTimeout(timer);
4618
+ resolve7(match[0]);
4619
+ }
4620
+ };
4621
+ proc.stderr?.on("data", onData);
4622
+ proc.stdout?.on("data", onData);
4623
+ proc.once("error", (err) => {
4624
+ if (settled) return;
4625
+ settled = true;
4626
+ clearTimeout(timer);
4627
+ reject(err);
4628
+ });
4629
+ proc.once("exit", (code, signal) => {
4630
+ if (settled) return;
4631
+ settled = true;
4632
+ clearTimeout(timer);
4633
+ reject(
4634
+ new Error(
4635
+ `cloudflared exited before emitting a URL (code=${code}, signal=${signal})`
4636
+ )
4637
+ );
4638
+ });
4639
+ });
4640
+ proc.once("exit", (code, signal) => {
4641
+ opts.onLog?.(
4642
+ `[cloudflared] exited (code=${code ?? "?"} signal=${signal ?? "-"})`
4643
+ );
4644
+ });
4645
+ return { publicUrl: url, pid: proc.pid ?? null };
4646
+ },
4647
+ async stop() {
4648
+ const proc = child;
4649
+ const cfg = configPath;
4650
+ child = null;
4651
+ configPath = null;
4652
+ if (cfg) {
4653
+ try {
4654
+ await rm(cfg, { force: true });
4655
+ } catch {
4656
+ }
4657
+ }
4658
+ if (!proc || proc.exitCode !== null) return;
4659
+ proc.kill("SIGTERM");
4660
+ const timer = setTimeout(() => {
4661
+ try {
4662
+ proc.kill("SIGKILL");
4663
+ } catch {
4664
+ }
4665
+ }, 3e3);
4666
+ timer.unref();
4667
+ await new Promise((resolve7) => {
4668
+ if (proc.exitCode !== null) {
4669
+ clearTimeout(timer);
4670
+ resolve7();
4671
+ return;
4672
+ }
4673
+ proc.once("exit", () => {
4674
+ clearTimeout(timer);
4675
+ resolve7();
4676
+ });
4677
+ });
4678
+ }
4679
+ };
4680
+ }
4681
+ async function assertCloudflaredOnPath() {
4682
+ try {
4683
+ await execFileAsync("cloudflared", ["--version"], { timeout: 3e3 });
4684
+ } catch {
4685
+ throw new Error(
4686
+ "cloudflared not found on PATH. Install it first:\n macOS: brew install cloudflared\n Linux: https://pkg.cloudflare.com/index.html\n Windows: winget install --id Cloudflare.cloudflared\nOr download a prebuilt binary from https://github.com/cloudflare/cloudflared/releases."
4687
+ );
4688
+ }
4689
+ }
4690
+ var RemoteController = class {
4691
+ state = null;
4692
+ bearerToken = null;
4693
+ provider = null;
4694
+ lastError;
4695
+ opts;
4696
+ constructor(opts) {
4697
+ this.opts = opts;
4698
+ }
4699
+ /**
4700
+ * Auth getter handed to startHttpServer. The HTTP server calls this
4701
+ * on every request so flipping enable/disable is effective without
4702
+ * a restart.
4703
+ */
4704
+ readAuth = () => {
4705
+ if (this.bearerToken === null) return { mode: "none" };
4706
+ return { mode: "bearer", token: this.bearerToken };
4707
+ };
4708
+ status() {
4709
+ if (!this.state) {
4710
+ return { enabled: false, lastError: this.lastError };
4711
+ }
4712
+ return {
4713
+ enabled: true,
4714
+ provider: this.state.provider,
4715
+ publicUrl: this.state.publicUrl,
4716
+ pid: this.state.pid,
4717
+ createdAt: this.state.createdAt,
4718
+ target: this.state.target,
4719
+ exposesGateway: this.state.exposesGateway,
4720
+ lastError: this.lastError
4721
+ };
4722
+ }
4723
+ async enable(input2) {
4724
+ if (this.state) {
4725
+ throw new Error(
4726
+ "remote tunnel already active \u2014 call remote_disable first to rotate"
4727
+ );
4728
+ }
4729
+ const providerId = input2.provider ?? "quick";
4730
+ const provider = pickProvider(providerId);
4731
+ this.lastError = void 0;
4732
+ const target = {
4733
+ host: input2.targetHost ?? "127.0.0.1",
4734
+ port: input2.targetPort ?? this.opts.port
4735
+ };
4736
+ const exposesGateway = target.host === "127.0.0.1" && target.port === this.opts.port;
4737
+ let started;
4738
+ try {
4739
+ started = await provider.start({
4740
+ target,
4741
+ workspace: this.opts.workspace,
4742
+ onLog: (line) => {
4743
+ this.opts.onLog?.(line);
4744
+ }
4745
+ });
4746
+ } catch (err) {
4747
+ this.lastError = errToMessage(err);
4748
+ throw err;
4749
+ }
4750
+ let bearerToken = null;
4751
+ let bearerTokenHash = "";
4752
+ if (exposesGateway) {
4753
+ bearerToken = randomBytes(32).toString("base64url");
4754
+ bearerTokenHash = sha256(bearerToken);
4755
+ }
4756
+ const state = {
4757
+ provider: providerId,
4758
+ publicUrl: started.publicUrl,
4759
+ bearerTokenHash,
4760
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
4761
+ pid: started.pid,
4762
+ target,
4763
+ exposesGateway
4764
+ };
4765
+ this.state = state;
4766
+ this.bearerToken = bearerToken;
4767
+ this.provider = provider;
4768
+ await persistState(this.opts.workspace, state);
4769
+ const result = {
4770
+ provider: providerId,
4771
+ publicUrl: started.publicUrl,
4772
+ target,
4773
+ exposesGateway
4774
+ };
4775
+ if (exposesGateway && bearerToken) {
4776
+ const mcpEndpoint = `${started.publicUrl}/mcp`;
4777
+ result.bearerToken = bearerToken;
4778
+ result.mcpEndpoint = mcpEndpoint;
4779
+ result.mcpConfigSnippet = buildMcpConfigSnippet(mcpEndpoint, bearerToken);
4780
+ }
4781
+ return result;
4782
+ }
4783
+ async disable() {
4784
+ if (!this.state) return { disabled: false };
4785
+ const provider = this.provider;
4786
+ this.state = null;
4787
+ this.bearerToken = null;
4788
+ this.provider = null;
4789
+ if (provider) {
4790
+ try {
4791
+ await provider.stop();
4792
+ } catch (err) {
4793
+ this.lastError = errToMessage(err);
4794
+ }
4795
+ }
4796
+ await clearState(this.opts.workspace);
4797
+ return { disabled: true };
4798
+ }
4799
+ /**
4800
+ * Stop the provider on gateway shutdown. Does NOT clear persisted
4801
+ * state — that survives across restarts so the user can `remote_status`
4802
+ * after rebooting and see "stale state, run remote_enable to refresh".
4803
+ * (Stale-recovery semantics land in v2; today we just leave the file.)
4804
+ */
4805
+ async shutdown() {
4806
+ if (this.provider) {
4807
+ try {
4808
+ await this.provider.stop();
4809
+ } catch {
4810
+ }
4811
+ }
4812
+ }
4813
+ };
4814
+ function pickProvider(id) {
4815
+ if (id === "quick") return quickTunnelProvider();
4816
+ const _exhaustive = id;
4817
+ throw new Error(`unknown remote provider: ${String(_exhaustive)}`);
4818
+ }
4819
+ function sha256(input2) {
4820
+ return createHash("sha256").update(input2).digest("hex");
4821
+ }
4822
+ function errToMessage(err) {
4823
+ return err instanceof Error ? err.message : String(err);
4824
+ }
4825
+ async function persistState(workspace, state) {
4826
+ const dir = join(workspace, ".agentproto");
4827
+ await mkdir(dir, { recursive: true });
4828
+ const file = join(dir, "remote.json");
4829
+ await writeFile(file, JSON.stringify(state, null, 2) + "\n", "utf8");
4830
+ try {
4831
+ await chmod(file, 384);
4832
+ } catch {
4833
+ }
4834
+ }
4835
+ async function clearState(workspace) {
4836
+ const file = join(workspace, ".agentproto/remote.json");
4837
+ if (!existsSync(file)) return;
4838
+ await rm(file, { force: true });
4839
+ }
4840
+ function buildMcpConfigSnippet(endpoint, token) {
4841
+ return JSON.stringify(
4842
+ {
4843
+ mcpServers: {
4844
+ "agentproto-remote": {
4845
+ transport: "http",
4846
+ url: endpoint,
4847
+ headers: { Authorization: `Bearer ${token}` }
4848
+ }
4849
+ }
4850
+ },
4851
+ null,
4852
+ 2
4853
+ );
4854
+ }
4855
+ function text2(value) {
4856
+ return {
4857
+ content: [
4858
+ {
4859
+ type: "text",
4860
+ text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
4861
+ }
4862
+ ]
4863
+ };
4864
+ }
4865
+ function registerRemoteTools(server, opts) {
4866
+ const { controller } = opts;
4867
+ server.tool(
4868
+ "remote_enable",
4869
+ "Publish a local port to the internet via Cloudflare. By default exposes this gateway and gates it with a bearer token (returned ONCE in the response \u2014 store it; only its hash is persisted). Pass `targetPort` to tunnel a different local service (Supabase, dev server, \u2026); in that mode the daemon does not gate the proxied traffic \u2014 the upstream service handles its own auth. Returns the public URL plus a paste-ready `.mcp.json` snippet (gateway mode only). Re-running while a tunnel is already up errors; call `remote_disable` first to rotate.",
4870
+ {
4871
+ provider: z.enum(["quick"]).optional().describe(
4872
+ "Tunnel backend. `quick` = Cloudflare Quick Tunnel (no API key, ephemeral *.trycloudflare.com URL). Defaults to `quick`. Named-CF / Tailscale providers TBD."
4873
+ ),
4874
+ targetPort: z.number().int().min(1).max(65535).optional().describe(
4875
+ "Local port to expose. Defaults to this gateway's own port (= publish the MCP server). Pass another port to tunnel a different local service (e.g. 54371 for Supabase) \u2014 in that mode bearer auth is OFF since the upstream handles its own gating."
4876
+ ),
4877
+ targetHost: z.string().optional().describe(
4878
+ "Host the tunnel forwards to. Defaults to `127.0.0.1`. Use `localhost` only if the target is explicitly IPv6-bound; cloudflared resolves DNS and IPv6-first lookups can break IPv4-only origins."
4879
+ )
4880
+ },
4881
+ async ({ provider, targetPort, targetHost }) => {
4882
+ const result = await controller.enable({ provider, targetPort, targetHost });
4883
+ return text2(result);
4884
+ }
4885
+ );
4886
+ server.tool(
4887
+ "remote_disable",
4888
+ "Tear down the active remote tunnel and drop bearer auth back to loopback-only. Idempotent \u2014 calling when no tunnel is active is a no-op.",
4889
+ {},
4890
+ async () => {
4891
+ const result = await controller.disable();
4892
+ return text2(result);
4893
+ }
4894
+ );
4895
+ server.tool(
4896
+ "remote_status",
4897
+ "Report whether a remote tunnel is active. When active: provider, public URL, supervised PID, createdAt. The bearer token itself is never returned by status \u2014 it was shown once at enable.",
4898
+ {},
4899
+ async () => {
4900
+ return text2(controller.status());
4901
+ }
4902
+ );
4903
+ }
4904
+ var WorkspacePathError = class extends Error {
4905
+ constructor(message) {
4906
+ super(message);
4907
+ this.name = "WorkspacePathError";
4908
+ }
4909
+ };
4910
+ function createWorkspaceFs(opts) {
4911
+ const root = resolve(opts.workspace);
4912
+ function resolvePath32(path) {
4913
+ if (typeof path !== "string" || path.length === 0) {
4914
+ throw new WorkspacePathError("path must be a non-empty string");
4915
+ }
4916
+ if (isAbsolute(path)) {
4917
+ throw new WorkspacePathError(
4918
+ "absolute paths are not allowed; use a workspace-relative path"
4919
+ );
4920
+ }
4921
+ const joined = normalize(join(root, path));
4922
+ const rel = relative(root, joined);
4923
+ if (rel.startsWith("..") || isAbsolute(rel)) {
4924
+ throw new WorkspacePathError(
4925
+ `path escapes the workspace: '${path}'`
4926
+ );
4927
+ }
4928
+ return joined;
4929
+ }
4930
+ return {
4931
+ async readFile(path) {
4932
+ const abs = resolvePath32(path);
4933
+ const buf = await readFile(abs);
4934
+ return buf.toString("utf8");
4935
+ },
4936
+ async writeFile(path, content) {
4937
+ const abs = resolvePath32(path);
4938
+ await mkdir(dirname(abs), { recursive: true });
4939
+ await writeFile(abs, content);
4940
+ },
4941
+ async exists(path) {
4942
+ try {
4943
+ const abs = resolvePath32(path);
4944
+ return existsSync(abs);
4945
+ } catch {
4946
+ return false;
4947
+ }
4948
+ }
4949
+ };
4950
+ }
4951
+ async function createGateway(opts) {
4952
+ const workspace = resolve(opts.workspace);
4953
+ if (!existsSync(workspace)) {
4954
+ throw new Error(`runtime: workspace dir does not exist: ${workspace}`);
4955
+ }
4956
+ const port = opts.port ?? 18790;
4957
+ const events = createRuntimeEvents();
4958
+ const conversations = fileConversationStore({ workspace });
4959
+ const workspaceFs = createWorkspaceFs({ workspace });
4960
+ const remote = new RemoteController({
4961
+ workspace,
4962
+ port,
4963
+ onLog: (line) => events.emit({
4964
+ type: "remote-log",
4965
+ at: (/* @__PURE__ */ new Date()).toISOString(),
4966
+ line
4967
+ })
4968
+ });
4969
+ const { registered } = await createMcpServer({
4970
+ specs: opts.specs,
4971
+ workspace,
4972
+ name: opts.name ?? "agentproto-runtime",
4973
+ version: opts.version ?? "0.1.0-alpha"
4974
+ });
4975
+ const sessions = createSessionsRegistry();
4976
+ const mcpProxy = new McpProxyRegistry();
4977
+ const mcpServerFactory = async () => {
4978
+ const { server } = await createMcpServer({
4979
+ specs: opts.specs,
4980
+ workspace,
4981
+ name: opts.name ?? "agentproto-runtime",
4982
+ version: opts.version ?? "0.1.0-alpha"
4983
+ });
4984
+ registerFsTools(server, { workspace });
4985
+ registerCommandTools(server, { workspace });
4986
+ registerRemoteTools(server, { controller: remote });
4987
+ registerSessionTools(server, {
4988
+ registry: sessions,
4989
+ mcpProxy,
4990
+ ...opts.resolveAgentAdapter ? { resolveAgentAdapter: opts.resolveAgentAdapter } : {},
4991
+ ...opts.listAgentAdapters ? { listAgentAdapters: opts.listAgentAdapters } : {}
4992
+ });
4993
+ return server;
4994
+ };
4995
+ if (opts.boot !== false) {
4996
+ await runBoot(workspace, opts, conversations, events).catch((err) => {
4997
+ events.emit({
4998
+ type: "heartbeat-error",
4999
+ at: (/* @__PURE__ */ new Date()).toISOString(),
5000
+ agent: opts.defaultBootAgent,
5001
+ error: `BOOT.md failed: ${err instanceof Error ? err.message : String(err)}`
5002
+ });
5003
+ });
5004
+ }
5005
+ const heartbeat = startHeartbeat({
5006
+ workspace,
5007
+ conversations,
5008
+ events,
5009
+ buildAgent: opts.buildAgent ?? noopBuildAgent
5010
+ });
5011
+ const http2 = await startHttpServer({
5012
+ port,
5013
+ bind: opts.bind,
5014
+ // Auth is read on every request via this getter. `opts.auth`
5015
+ // (when provided) is the startup default and represents the
5016
+ // operator's intent — bearer-only deployments stay bearer-only
5017
+ // even when no remote tunnel is up. The controller's auth wins
5018
+ // once a tunnel is enabled (it issues a fresh token); on disable
5019
+ // we fall back to `opts.auth` if set, otherwise `mode: "none"`.
5020
+ auth: () => {
5021
+ const remoteAuth = remote.readAuth();
5022
+ if (remoteAuth.mode === "bearer") return remoteAuth;
5023
+ return opts.auth ?? { mode: "none" };
5024
+ },
5025
+ mcpServerFactory,
5026
+ conversations,
5027
+ events,
5028
+ heartbeat,
5029
+ sessions,
5030
+ mcpProxy,
5031
+ ...opts.resolveAgentAdapter ? { resolveAgentAdapter: opts.resolveAgentAdapter } : {},
5032
+ ...opts.listAgentAdapters ? { listAgentAdapters: opts.listAgentAdapters } : {},
5033
+ meta: { workspace, registered }
5034
+ });
5035
+ heartbeat.start();
5036
+ events.emit({
5037
+ type: "boot",
5038
+ at: (/* @__PURE__ */ new Date()).toISOString(),
5039
+ workspace,
5040
+ registered
5041
+ });
5042
+ void writeRuntimeMeta(workspace, {
5043
+ workspace,
5044
+ port,
5045
+ bind: opts.bind ?? "127.0.0.1",
5046
+ pid: process.pid,
5047
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
5048
+ name: opts.name ?? "agentproto-runtime",
5049
+ registered
5050
+ });
5051
+ return {
5052
+ url: http2.url,
5053
+ workspace,
5054
+ workspaceFs,
5055
+ registered,
5056
+ sessions,
5057
+ async stop() {
5058
+ heartbeat.stop();
5059
+ sessions.shutdown();
5060
+ await mcpProxy.closeAll();
5061
+ await remote.shutdown();
5062
+ await http2.stop();
5063
+ }
5064
+ };
5065
+ }
5066
+ var noopBuildAgent = async () => null;
5067
+ async function runBoot(workspace, opts, conversations, events) {
5068
+ const path = join(workspace, "BOOT.md");
5069
+ if (!existsSync(path)) return;
5070
+ const body = (await readFile(path, "utf8")).trim();
5071
+ if (!body) return;
5072
+ if (!opts.buildAgent || !opts.defaultBootAgent) return;
5073
+ const agent = await opts.buildAgent(opts.defaultBootAgent);
5074
+ if (!agent) return;
5075
+ const conversationId = `boot-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace(/[:.]/g, "-")}`;
5076
+ await conversations.open(conversationId, { agent: opts.defaultBootAgent });
5077
+ await conversations.appendTurn(conversationId, "user", body, {
5078
+ attribution: "boot"
5079
+ });
5080
+ const reply = await agent.generate(body);
5081
+ await conversations.appendTurn(conversationId, "assistant", reply.text, {
5082
+ attribution: opts.defaultBootAgent
5083
+ });
5084
+ events.emit({
5085
+ type: "heartbeat-fired",
5086
+ at: (/* @__PURE__ */ new Date()).toISOString(),
5087
+ agent: opts.defaultBootAgent,
5088
+ conversationId,
5089
+ prompt: body,
5090
+ reply: reply.text,
5091
+ durationMs: 0
5092
+ });
5093
+ }
1428
5094
  async function runServe(args) {
1429
5095
  const { values } = parseArgs({
1430
5096
  args: [...args],
@@ -1441,8 +5107,8 @@ async function runServe(args) {
1441
5107
  });
1442
5108
  const workspace = resolve(values.workspace ?? process.cwd());
1443
5109
  try {
1444
- const stat = await promises.stat(workspace);
1445
- if (!stat.isDirectory()) {
5110
+ const stat2 = await promises.stat(workspace);
5111
+ if (!stat2.isDirectory()) {
1446
5112
  process.stderr.write(
1447
5113
  `agentproto serve: --workspace "${workspace}" is not a directory.
1448
5114
  `
@@ -1556,8 +5222,8 @@ agentproto serve: ${signal} \u2014 shutting down.
1556
5222
  `agentproto serve: running local-only (no --connect). Press Ctrl-C to stop.
1557
5223
  `
1558
5224
  );
1559
- await new Promise((resolve3) => {
1560
- aborter.signal.addEventListener("abort", () => resolve3());
5225
+ await new Promise((resolve6) => {
5226
+ aborter.signal.addEventListener("abort", () => resolve6());
1561
5227
  });
1562
5228
  return 0;
1563
5229
  }
@@ -1592,10 +5258,10 @@ async function runOneTunnel(opts, gateway, signal) {
1592
5258
  };
1593
5259
  if (opts.token) headers.authorization = `Bearer ${opts.token}`;
1594
5260
  const ws = new WebSocket(opts.connect, { headers });
1595
- await new Promise((resolve3, reject) => {
5261
+ await new Promise((resolve6, reject) => {
1596
5262
  const onOpen = () => {
1597
5263
  ws.off("error", onError);
1598
- resolve3();
5264
+ resolve6();
1599
5265
  };
1600
5266
  const onError = (err) => {
1601
5267
  ws.off("open", onOpen);
@@ -1640,31 +5306,156 @@ async function runOneTunnel(opts, gateway, signal) {
1640
5306
  });
1641
5307
  }
1642
5308
  });
1643
- await new Promise((resolve3) => {
5309
+ await new Promise((resolve6) => {
1644
5310
  const offClose = sink.onClose(() => {
1645
5311
  offClose();
1646
- resolve3();
5312
+ resolve6();
1647
5313
  });
1648
5314
  if (signal.aborted) {
1649
- server.close().finally(() => resolve3());
5315
+ server.close().finally(() => resolve6());
1650
5316
  }
1651
5317
  signal.addEventListener("abort", () => {
1652
- server.close().finally(() => resolve3());
5318
+ server.close().finally(() => resolve6());
1653
5319
  });
1654
5320
  });
1655
5321
  process.stderr.write(`agentproto serve: tunnel closed.
1656
5322
  `);
1657
5323
  }
1658
5324
  function sleep2(ms, signal) {
1659
- return new Promise((resolve3) => {
1660
- if (signal.aborted) return resolve3();
1661
- const timer = setTimeout(resolve3, ms);
5325
+ return new Promise((resolve6) => {
5326
+ if (signal.aborted) return resolve6();
5327
+ const timer = setTimeout(resolve6, ms);
1662
5328
  signal.addEventListener("abort", () => {
1663
5329
  clearTimeout(timer);
1664
- resolve3();
5330
+ resolve6();
1665
5331
  });
1666
5332
  });
1667
5333
  }
5334
+ var WORKSPACES_CONFIG_VERSION2 = 1;
5335
+ var DEFAULT_CONFIG_DIR2 = () => resolve(homedir(), ".agentproto");
5336
+ var DEFAULT_CONFIG_PATH2 = () => resolve(DEFAULT_CONFIG_DIR2(), "workspaces.json");
5337
+ function sanitizeSlug2(input2) {
5338
+ const trimmed = input2.trim().toLowerCase();
5339
+ const cleaned = trimmed.replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
5340
+ return cleaned || "workspace";
5341
+ }
5342
+ async function loadWorkspacesConfig2(path = DEFAULT_CONFIG_PATH2()) {
5343
+ let raw;
5344
+ try {
5345
+ raw = await promises.readFile(path, "utf8");
5346
+ } catch (err) {
5347
+ if (err.code === "ENOENT") {
5348
+ return { version: WORKSPACES_CONFIG_VERSION2, workspaces: [] };
5349
+ }
5350
+ throw err;
5351
+ }
5352
+ let parsed;
5353
+ try {
5354
+ parsed = JSON.parse(raw);
5355
+ } catch (err) {
5356
+ throw new Error(
5357
+ `agentproto: ${path} is not valid JSON (${err instanceof Error ? err.message : String(err)}). Delete the file or fix it manually.`
5358
+ );
5359
+ }
5360
+ return normaliseConfig2(parsed);
5361
+ }
5362
+ async function saveWorkspacesConfig(config, path = DEFAULT_CONFIG_PATH2()) {
5363
+ const normalised = normaliseConfig2(config);
5364
+ await promises.mkdir(dirname(path), { recursive: true });
5365
+ const tmp = `${path}.tmp.${process.pid}`;
5366
+ await promises.writeFile(tmp, JSON.stringify(normalised, null, 2) + "\n", "utf8");
5367
+ await promises.rename(tmp, path);
5368
+ }
5369
+ function addWorkspace(config, input2) {
5370
+ if (!isAbsolute(input2.path)) {
5371
+ throw new Error(
5372
+ `addWorkspace: path must be absolute, got "${input2.path}".`
5373
+ );
5374
+ }
5375
+ const slug = sanitizeSlug2(input2.slug);
5376
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5377
+ const existingIdx = config.workspaces.findIndex((w) => w.slug === slug);
5378
+ const next = existingIdx >= 0 ? {
5379
+ ...config.workspaces[existingIdx],
5380
+ path: input2.path,
5381
+ ...input2.label !== void 0 ? { label: input2.label } : {},
5382
+ updatedAt: now
5383
+ } : {
5384
+ slug,
5385
+ path: input2.path,
5386
+ ...input2.label ? { label: input2.label } : {},
5387
+ addedAt: now,
5388
+ updatedAt: now
5389
+ };
5390
+ const workspaces = [...config.workspaces];
5391
+ if (existingIdx >= 0) workspaces[existingIdx] = next;
5392
+ else workspaces.push(next);
5393
+ const active = config.active ?? slug;
5394
+ return { ...config, workspaces, active };
5395
+ }
5396
+ function removeWorkspace(config, slug) {
5397
+ const sanitised = sanitizeSlug2(slug);
5398
+ const workspaces = config.workspaces.filter((w) => w.slug !== sanitised);
5399
+ let active = config.active;
5400
+ if (active === sanitised) {
5401
+ active = workspaces[0]?.slug;
5402
+ }
5403
+ const next = { ...config, workspaces };
5404
+ if (active !== void 0) next.active = active;
5405
+ else delete next.active;
5406
+ return next;
5407
+ }
5408
+ function setActiveWorkspace(config, slug) {
5409
+ const sanitised = sanitizeSlug2(slug);
5410
+ if (!config.workspaces.some((w) => w.slug === sanitised)) {
5411
+ throw new Error(
5412
+ `setActiveWorkspace: no workspace registered with slug "${sanitised}". Run \`agentproto workspace add <path> --slug ${sanitised}\` first.`
5413
+ );
5414
+ }
5415
+ return { ...config, active: sanitised };
5416
+ }
5417
+ function findWorkspace2(config, slug) {
5418
+ return config.workspaces.find((w) => w.slug === sanitizeSlug2(slug));
5419
+ }
5420
+ function getActiveWorkspace2(config) {
5421
+ if (!config.active) return config.workspaces[0];
5422
+ return findWorkspace2(config, config.active) ?? config.workspaces[0];
5423
+ }
5424
+ function normaliseConfig2(parsed) {
5425
+ if (!parsed || typeof parsed !== "object") {
5426
+ return { version: WORKSPACES_CONFIG_VERSION2, workspaces: [] };
5427
+ }
5428
+ const obj = parsed;
5429
+ const workspaces = [];
5430
+ if (Array.isArray(obj.workspaces)) {
5431
+ for (const entry of obj.workspaces) {
5432
+ if (!entry || typeof entry !== "object") continue;
5433
+ const e = entry;
5434
+ const slug = typeof e.slug === "string" ? sanitizeSlug2(e.slug) : "";
5435
+ const path = typeof e.path === "string" ? e.path : "";
5436
+ if (!slug || !path) continue;
5437
+ const addedAt = typeof e.addedAt === "string" ? e.addedAt : (/* @__PURE__ */ new Date()).toISOString();
5438
+ const updatedAt = typeof e.updatedAt === "string" ? e.updatedAt : addedAt;
5439
+ const we = { slug, path, addedAt, updatedAt };
5440
+ if (typeof e.label === "string" && e.label.trim()) {
5441
+ we.label = e.label.trim();
5442
+ }
5443
+ workspaces.push(we);
5444
+ }
5445
+ }
5446
+ const dedup = /* @__PURE__ */ new Map();
5447
+ for (const w of workspaces) dedup.set(w.slug, w);
5448
+ const finalList = Array.from(dedup.values());
5449
+ const active = typeof obj.active === "string" && finalList.some((w) => w.slug === obj.active) ? obj.active : finalList[0]?.slug;
5450
+ const out = {
5451
+ version: WORKSPACES_CONFIG_VERSION2,
5452
+ workspaces: finalList
5453
+ };
5454
+ if (active !== void 0) out.active = active;
5455
+ return out;
5456
+ }
5457
+
5458
+ // src/commands/workspace.ts
1668
5459
  var USAGE2 = `agentproto workspace \u2014 manage local workspaces
1669
5460
 
1670
5461
  Usage:
@@ -1674,7 +5465,7 @@ Usage:
1674
5465
  agentproto workspace use <slug>
1675
5466
  agentproto workspace --help
1676
5467
 
1677
- Config file: ${DEFAULT_CONFIG_PATH()}
5468
+ Config file: ${DEFAULT_CONFIG_PATH2()}
1678
5469
  `;
1679
5470
  async function runWorkspace(args) {
1680
5471
  if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
@@ -1728,8 +5519,8 @@ async function runAdd(args) {
1728
5519
  );
1729
5520
  return 2;
1730
5521
  }
1731
- const slug = sanitizeSlug(values.slug ?? basename(absPath));
1732
- const config = await loadWorkspacesConfig();
5522
+ const slug = sanitizeSlug2(values.slug ?? basename(absPath));
5523
+ const config = await loadWorkspacesConfig2();
1733
5524
  const next = addWorkspace(config, {
1734
5525
  slug,
1735
5526
  path: absPath,
@@ -1750,7 +5541,7 @@ async function runList(args) {
1750
5541
  strict: true,
1751
5542
  options: { json: { type: "boolean" } }
1752
5543
  });
1753
- const config = await loadWorkspacesConfig();
5544
+ const config = await loadWorkspacesConfig2();
1754
5545
  if (values.json) {
1755
5546
  process.stdout.write(JSON.stringify(config, null, 2) + "\n");
1756
5547
  return 0;
@@ -1794,8 +5585,8 @@ async function runRemove(args) {
1794
5585
  );
1795
5586
  return 2;
1796
5587
  }
1797
- const config = await loadWorkspacesConfig();
1798
- const sanitised = sanitizeSlug(slug);
5588
+ const config = await loadWorkspacesConfig2();
5589
+ const sanitised = sanitizeSlug2(slug);
1799
5590
  const existed = config.workspaces.some((w) => w.slug === sanitised);
1800
5591
  if (!existed) {
1801
5592
  process.stderr.write(
@@ -1829,7 +5620,7 @@ async function runUse(args) {
1829
5620
  process.stderr.write("agentproto workspace use: missing <slug>.\n");
1830
5621
  return 2;
1831
5622
  }
1832
- const config = await loadWorkspacesConfig();
5623
+ const config = await loadWorkspacesConfig2();
1833
5624
  try {
1834
5625
  const next = setActiveWorkspace(config, slug);
1835
5626
  await saveWorkspacesConfig(next);
@@ -1900,10 +5691,10 @@ async function resolveDaemonUrl() {
1900
5691
  if (process.env.AGENTPROTO_DAEMON_URL) {
1901
5692
  return process.env.AGENTPROTO_DAEMON_URL.replace(/\/+$/, "");
1902
5693
  }
1903
- const config = await loadWorkspacesConfig().catch(() => null);
5694
+ const config = await loadWorkspacesConfig2().catch(() => null);
1904
5695
  if (!config) return null;
1905
5696
  const candidates = [
1906
- getActiveWorkspace(config),
5697
+ getActiveWorkspace2(config),
1907
5698
  ...config.workspaces
1908
5699
  ].filter(
1909
5700
  (w, i, arr) => !!w && arr.findIndex((x) => x?.slug === w.slug) === i
@@ -2032,7 +5823,7 @@ async function runWatch(baseUrl) {
2032
5823
  return 0;
2033
5824
  }
2034
5825
  async function runAttach({ baseUrl, id, colour }) {
2035
- return new Promise((resolve3) => {
5826
+ return new Promise((resolve6) => {
2036
5827
  const url = new URL(`${baseUrl}/sessions/${id}/stream`);
2037
5828
  const lib = url.protocol === "https:" ? https : http;
2038
5829
  const req = lib.get(
@@ -2044,7 +5835,7 @@ async function runAttach({ baseUrl, id, colour }) {
2044
5835
  `agentproto sessions --attach: no session "${id}".
2045
5836
  `
2046
5837
  );
2047
- resolve3(2);
5838
+ resolve6(2);
2048
5839
  return;
2049
5840
  }
2050
5841
  if (res.statusCode !== 200) {
@@ -2052,7 +5843,7 @@ async function runAttach({ baseUrl, id, colour }) {
2052
5843
  `agentproto sessions --attach: HTTP ${res.statusCode}
2053
5844
  `
2054
5845
  );
2055
- resolve3(1);
5846
+ resolve6(1);
2056
5847
  return;
2057
5848
  }
2058
5849
  let buf = "";
@@ -2082,22 +5873,22 @@ async function runAttach({ baseUrl, id, colour }) {
2082
5873
  idx = buf.indexOf("\n\n");
2083
5874
  }
2084
5875
  });
2085
- res.on("end", () => resolve3(0));
5876
+ res.on("end", () => resolve6(0));
2086
5877
  }
2087
5878
  );
2088
5879
  req.on("error", (err) => {
2089
5880
  process.stderr.write(`agentproto sessions --attach: ${err.message}
2090
5881
  `);
2091
- resolve3(1);
5882
+ resolve6(1);
2092
5883
  });
2093
5884
  process.on("SIGINT", () => {
2094
5885
  req.destroy();
2095
- resolve3(0);
5886
+ resolve6(0);
2096
5887
  });
2097
5888
  });
2098
5889
  }
2099
5890
  function httpGetJson(url) {
2100
- return new Promise((resolve3, reject) => {
5891
+ return new Promise((resolve6, reject) => {
2101
5892
  const u = new URL(url);
2102
5893
  const lib = u.protocol === "https:" ? https : http;
2103
5894
  lib.get(u, (res) => {
@@ -2110,7 +5901,7 @@ function httpGetJson(url) {
2110
5901
  return;
2111
5902
  }
2112
5903
  try {
2113
- resolve3(JSON.parse(body));
5904
+ resolve6(JSON.parse(body));
2114
5905
  } catch (err) {
2115
5906
  reject(err instanceof Error ? err : new Error(String(err)));
2116
5907
  }