@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/index.mjs CHANGED
@@ -1,15 +1,25 @@
1
- import { spawn } from 'child_process';
2
- import { createHash } from 'crypto';
3
- import { mkdtemp, writeFile, chmod, rm, mkdir, readFile } from 'fs/promises';
1
+ import { execFile, spawn } from 'child_process';
2
+ import { createHash, randomUUID, randomBytes } from 'crypto';
3
+ import { mkdtemp, writeFile, chmod, rm, mkdir, readFile, readdir, stat } from 'fs/promises';
4
4
  import { tmpdir, userInfo, hostname, homedir, platform } from 'os';
5
- import { resolve, join, dirname } from 'path';
6
- import { parseArgs } from 'util';
7
- import { promises } from 'fs';
5
+ import { resolve, join, dirname, isAbsolute, normalize, relative, basename } from 'path';
6
+ import { promisify, parseArgs } from 'util';
7
+ import { promises, existsSync } from 'fs';
8
8
  import { createInterface } from 'readline/promises';
9
9
  import { stdout, stdin } from 'process';
10
10
  import { createAgentCliRuntime } from '@agentproto/driver-agent-cli';
11
11
  import { wrapWebSocket, createTunnelServer } from '@agentproto/acp/tunnel';
12
- import { createGateway } from '@agentproto/runtime';
12
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
13
+ import '@modelcontextprotocol/sdk/server/stdio.js';
14
+ import { z } from 'zod';
15
+ import matter from 'gray-matter';
16
+ import { EventEmitter } from 'events';
17
+ import { createServer } from 'http';
18
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
19
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
20
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
21
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
22
+ import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
13
23
  import WebSocket from 'ws';
14
24
 
15
25
  /**
@@ -92,8 +102,8 @@ async function collectAgentprotoNamespaceRoots(start) {
92
102
  for (let depth = 0; depth < 16; depth++) {
93
103
  for (const candidate of candidatesAt(cur)) {
94
104
  try {
95
- const stat = await promises.stat(candidate);
96
- if (stat.isDirectory()) roots.push(candidate);
105
+ const stat2 = await promises.stat(candidate);
106
+ if (stat2.isDirectory()) roots.push(candidate);
97
107
  } catch {
98
108
  }
99
109
  }
@@ -249,10 +259,10 @@ async function runExternalStep(step, ledger, head) {
249
259
  `);
250
260
  const opener = platform() === "darwin" ? "open" : platform() === "win32" ? "cmd" : "xdg-open";
251
261
  const args = platform() === "win32" ? ["/c", "start", step.url] : [step.url];
252
- await new Promise((resolve) => {
262
+ await new Promise((resolve3) => {
253
263
  const child = spawn(opener, args, { stdio: "ignore", detached: true });
254
- child.once("error", () => resolve());
255
- child.once("spawn", () => resolve());
264
+ child.once("error", () => resolve3());
265
+ child.once("spawn", () => resolve3());
256
266
  });
257
267
  let value = "";
258
268
  if (step.callback?.param) {
@@ -325,7 +335,7 @@ async function promptString(question, opts) {
325
335
  process.stdin.setRawMode?.(true);
326
336
  let buf = "";
327
337
  process.stdout.write(prompt);
328
- return new Promise((resolve) => {
338
+ return new Promise((resolve3) => {
329
339
  const onData = (chunk) => {
330
340
  for (const code of chunk) {
331
341
  if (code === 13 || code === 10) {
@@ -333,7 +343,7 @@ async function promptString(question, opts) {
333
343
  process.stdin.setRawMode?.(false);
334
344
  process.stdout.write("\n");
335
345
  rl.close();
336
- resolve(buf || opts.defaultValue || "");
346
+ resolve3(buf || opts.defaultValue || "");
337
347
  return;
338
348
  }
339
349
  if (code === 3) {
@@ -415,7 +425,7 @@ async function resolveSelectOptions(options) {
415
425
  });
416
426
  }
417
427
  async function runShellCapturing(cmd, opts) {
418
- return new Promise((resolve) => {
428
+ return new Promise((resolve3) => {
419
429
  const child = spawn("bash", ["-lc", cmd], {
420
430
  stdio: opts.interactive ? "inherit" : ["ignore", "pipe", "pipe"]
421
431
  });
@@ -433,11 +443,11 @@ async function runShellCapturing(cmd, opts) {
433
443
  }, opts.timeoutMs);
434
444
  child.once("error", () => {
435
445
  clearTimeout(timer);
436
- resolve({ exitCode: 127, stdout, stderr: stderr || "spawn error" });
446
+ resolve3({ exitCode: 127, stdout, stderr: stderr || "spawn error" });
437
447
  });
438
448
  child.once("exit", (code) => {
439
449
  clearTimeout(timer);
440
- resolve({ exitCode: code ?? 0, stdout, stderr });
450
+ resolve3({ exitCode: code ?? 0, stdout, stderr });
441
451
  });
442
452
  });
443
453
  }
@@ -796,15 +806,15 @@ function homedir2() {
796
806
  return process.env["HOME"] ?? process.env["USERPROFILE"] ?? "/";
797
807
  }
798
808
  function spawnInherit(cmd, argv) {
799
- return new Promise((resolve, reject) => {
809
+ return new Promise((resolve3, reject) => {
800
810
  const child = spawn(cmd, argv, { stdio: "inherit" });
801
811
  child.once("error", reject);
802
- child.once("exit", (code) => resolve(code ?? 0));
812
+ child.once("exit", (code) => resolve3(code ?? 0));
803
813
  });
804
814
  }
805
815
  async function runVersionCheck(check) {
806
816
  if (!check) return { ok: false, message: "no version_check declared" };
807
- return new Promise((resolve) => {
817
+ return new Promise((resolve3) => {
808
818
  const child = spawn("bash", ["-lc", check.cmd], {
809
819
  stdio: ["ignore", "pipe", "ignore"]
810
820
  });
@@ -812,19 +822,19 @@ async function runVersionCheck(check) {
812
822
  child.stdout.on("data", (c) => {
813
823
  buf += c.toString("utf8");
814
824
  });
815
- child.once("error", () => resolve({ ok: false, message: "check failed" }));
825
+ child.once("error", () => resolve3({ ok: false, message: "check failed" }));
816
826
  child.once("exit", (code) => {
817
827
  if (code !== 0) {
818
- resolve({ ok: false, message: `check exited ${code}` });
828
+ resolve3({ ok: false, message: `check exited ${code}` });
819
829
  return;
820
830
  }
821
831
  const re = new RegExp(check.parse);
822
832
  const m = buf.match(re);
823
833
  if (!m || !m[1]) {
824
- resolve({ ok: false, message: "could not parse version" });
834
+ resolve3({ ok: false, message: "could not parse version" });
825
835
  return;
826
836
  }
827
- resolve({ ok: true, message: `version ${m[1]}` });
837
+ resolve3({ ok: true, message: `version ${m[1]}` });
828
838
  });
829
839
  });
830
840
  }
@@ -836,8 +846,8 @@ async function readStdinIfPiped() {
836
846
  for await (const chunk of process.stdin) {
837
847
  chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
838
848
  }
839
- const text = Buffer.concat(chunks).toString("utf8").trim();
840
- return text.length > 0 ? text : null;
849
+ const text3 = Buffer.concat(chunks).toString("utf8").trim();
850
+ return text3.length > 0 ? text3 : null;
841
851
  }
842
852
 
843
853
  // src/commands/run.ts
@@ -1008,6 +1018,3664 @@ function formatRelative(ms) {
1008
1018
  if (ms < 864e5) return `${Math.round(ms / 36e5)}h`;
1009
1019
  return `${Math.round(ms / 864e5)}d`;
1010
1020
  }
1021
+
1022
+ // ../define-doctype/dist/index.mjs
1023
+ var DEFAULT_ID_PATTERN = /^[a-z0-9][a-z0-9._-]{1,79}$/;
1024
+ var MAX_DESCRIPTION_LEN = 2e3;
1025
+ function createDoctype(opts) {
1026
+ const idPattern = opts.idPattern ?? DEFAULT_ID_PATTERN;
1027
+ const readIdentity = opts.readIdentity ?? ((def) => def.id);
1028
+ const readDescription = opts.readDescription === false ? null : opts.readDescription ?? ((def) => def.description);
1029
+ const maxLen = opts.maxDescriptionLen ?? MAX_DESCRIPTION_LEN;
1030
+ const prefix = `define${capitalize(opts.name)}`;
1031
+ const aipTag = `(AIP-${opts.aip})`;
1032
+ return function constructDoctype(def) {
1033
+ const identity = readIdentity(def);
1034
+ if (typeof identity !== "string" || !idPattern.test(identity)) {
1035
+ throw new Error(
1036
+ `${prefix} ${aipTag}: invalid id '${String(
1037
+ identity
1038
+ )}' \u2014 must match ${idPattern}`
1039
+ );
1040
+ }
1041
+ if (readDescription) {
1042
+ const description = readDescription(def);
1043
+ if (typeof description !== "string" || description.length === 0 || description.length > maxLen) {
1044
+ throw new Error(
1045
+ `${prefix} ${aipTag}: id='${identity}' description must be 1\u2013${maxLen} chars`
1046
+ );
1047
+ }
1048
+ }
1049
+ opts.validate?.(def);
1050
+ return Object.freeze(opts.build(def));
1051
+ };
1052
+ }
1053
+ function capitalize(s) {
1054
+ return s.length === 0 ? s : s.charAt(0).toUpperCase() + s.slice(1);
1055
+ }
1056
+ function filterSerializable(value) {
1057
+ if (value === null || value === void 0) return value;
1058
+ if (typeof value === "function") return void 0;
1059
+ if (typeof value === "object" && value !== null && Object.prototype.hasOwnProperty.call(value, "_def") && "parse" in value && typeof value.parse === "function") {
1060
+ return void 0;
1061
+ }
1062
+ if (Array.isArray(value)) {
1063
+ return value.map(filterSerializable).filter((v) => v !== void 0);
1064
+ }
1065
+ if (typeof value === "object") {
1066
+ const out = {};
1067
+ for (const [k, v] of Object.entries(value)) {
1068
+ const filtered = filterSerializable(v);
1069
+ if (filtered !== void 0) out[k] = filtered;
1070
+ }
1071
+ return out;
1072
+ }
1073
+ return value;
1074
+ }
1075
+ var DEFAULT_SKIP_DIRS = [
1076
+ "node_modules",
1077
+ ".git",
1078
+ "dist",
1079
+ ".next",
1080
+ ".turbo"
1081
+ ];
1082
+ function createVerbs(spec) {
1083
+ const filename = spec.filename ?? `${spec.name.toUpperCase()}.md`;
1084
+ const toFrontmatter = spec.toFrontmatter ?? ((params) => filterSerializable({
1085
+ schema: spec.schemaLiteral,
1086
+ ...params
1087
+ }));
1088
+ async function create(params, opts) {
1089
+ const handle = spec.define(params);
1090
+ const relativePath = spec.pathOf(handle);
1091
+ const path = join(opts.dir, relativePath);
1092
+ const frontmatter = toFrontmatter(params);
1093
+ const rendered = matter.stringify(
1094
+ opts.body ?? defaultBody(spec, frontmatter),
1095
+ frontmatter
1096
+ );
1097
+ if (!opts.dryRun) {
1098
+ await mkdir(dirname(path), { recursive: true });
1099
+ await writeFile(path, rendered, "utf8");
1100
+ }
1101
+ return { path, handle, rendered };
1102
+ }
1103
+ async function load(path) {
1104
+ const source = await readFile(path, "utf8");
1105
+ const { frontmatter, body } = spec.parse(source);
1106
+ const handle = spec.define(frontmatter);
1107
+ return { path, handle, body };
1108
+ }
1109
+ async function list(dir, opts = {}) {
1110
+ const skip = new Set(opts.skipDirs ?? DEFAULT_SKIP_DIRS);
1111
+ const out = [];
1112
+ async function walk(current) {
1113
+ let entries;
1114
+ try {
1115
+ entries = await readdir(current, { withFileTypes: true });
1116
+ } catch {
1117
+ return;
1118
+ }
1119
+ for (const entry of entries) {
1120
+ const entryName = String(entry.name);
1121
+ if (entry.isDirectory()) {
1122
+ if (skip.has(entryName)) continue;
1123
+ await walk(join(current, entryName));
1124
+ continue;
1125
+ }
1126
+ if (!entry.isFile()) continue;
1127
+ if (entryName !== filename) continue;
1128
+ try {
1129
+ const { handle } = await load(join(current, entryName));
1130
+ if (!opts.filter || opts.filter(handle)) out.push(handle);
1131
+ } catch {
1132
+ }
1133
+ }
1134
+ }
1135
+ await walk(dir);
1136
+ return out;
1137
+ }
1138
+ async function update(path, mutator, opts = {}) {
1139
+ const { handle, body } = await load(path);
1140
+ const source = await readFile(path, "utf8");
1141
+ const { frontmatter } = spec.parse(source);
1142
+ const params = frontmatter;
1143
+ const mutated = await mutator(params, { handle, body });
1144
+ const newHandle = spec.define(mutated);
1145
+ const newFrontmatter = toFrontmatter(mutated);
1146
+ const rendered = matter.stringify(
1147
+ opts.body ?? body,
1148
+ newFrontmatter
1149
+ );
1150
+ if (!opts.dryRun) {
1151
+ await mkdir(dirname(path), { recursive: true });
1152
+ await writeFile(path, rendered, "utf8");
1153
+ }
1154
+ return { path, handle: newHandle, rendered };
1155
+ }
1156
+ async function resolve22(block, ctx = {}) {
1157
+ if ("inline" in block) {
1158
+ return spec.define(block.inline);
1159
+ }
1160
+ if ("file" in block) {
1161
+ const baseDir = ctx.baseDir ?? ".";
1162
+ const path = isAbsolute(block.file) ? block.file : join(baseDir, block.file);
1163
+ const { handle } = await load(path);
1164
+ return handle;
1165
+ }
1166
+ if ("ref" in block) {
1167
+ if (!ctx.resolveRef) {
1168
+ throw new Error(
1169
+ `${spec.name}.resolve (AIP-${spec.aip}): block has \`ref: '${block.ref}'\` but no resolveRef provided in context \u2014 registries must inject their own resolver`
1170
+ );
1171
+ }
1172
+ const resolved = await ctx.resolveRef(block.ref);
1173
+ return spec.define(resolved);
1174
+ }
1175
+ throw new Error(
1176
+ `${spec.name}.resolve (AIP-${spec.aip}): block must have one of inline | ref | file`
1177
+ );
1178
+ }
1179
+ async function deleteFn(path) {
1180
+ await rm(path, { force: true });
1181
+ }
1182
+ return { create, load, list, update, resolve: resolve22, delete: deleteFn };
1183
+ }
1184
+ function defaultBody(spec, frontmatter) {
1185
+ const id = typeof frontmatter.name === "string" ? frontmatter.name : typeof frontmatter.id === "string" ? frontmatter.id : typeof frontmatter.slug === "string" ? frontmatter.slug : spec.name;
1186
+ return `# ${id}
1187
+ `;
1188
+ }
1189
+ 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.");
1190
+ createDoctype({
1191
+ aip: 40,
1192
+ name: "extension",
1193
+ // Extension slugs are namespaced: `<namespace>:<name>` (e.g.
1194
+ // `acme:deal`). Override the default kebab-only pattern.
1195
+ idPattern: /^[a-z][a-z0-9-]*:[a-z][a-z0-9-]*[a-z0-9]$/,
1196
+ readIdentity: (def) => def.slug,
1197
+ validate(def) {
1198
+ const result = extensionFrontmatterSchema.safeParse(def);
1199
+ if (!result.success) {
1200
+ throw new Error(
1201
+ `defineExtension (AIP-40): ${result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
1202
+ );
1203
+ }
1204
+ },
1205
+ build(def) {
1206
+ return { ...def };
1207
+ }
1208
+ });
1209
+ function parseExtensionManifest(source) {
1210
+ const parsed = matter(source);
1211
+ if (Object.keys(parsed.data).length === 0) {
1212
+ throw new Error("parseExtensionManifest: missing or empty frontmatter");
1213
+ }
1214
+ const result = extensionFrontmatterSchema.safeParse(parsed.data);
1215
+ if (!result.success) {
1216
+ throw new Error(
1217
+ `parseExtensionManifest: invalid frontmatter \u2014 ${result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
1218
+ );
1219
+ }
1220
+ return { frontmatter: result.data, body: parsed.content };
1221
+ }
1222
+
1223
+ // ../extension/dist/index.mjs
1224
+ function specFromExtension(extension, opts = {}) {
1225
+ const { parent } = opts;
1226
+ const isRoot = extension.extends === "none";
1227
+ if (!isRoot && !parent) {
1228
+ throw new Error(
1229
+ `specFromExtension (AIP-40): extension '${extension.slug}' extends '${extension.extends}' but no parent spec was provided in opts.parent`
1230
+ );
1231
+ }
1232
+ if (isRoot && !extension.path_convention) {
1233
+ throw new Error(
1234
+ `specFromExtension (AIP-40): extension '${extension.slug}' is a root doctype (extends: none) and MUST declare path_convention`
1235
+ );
1236
+ }
1237
+ if (parent && extension.tighten) {
1238
+ verifyTightening(extension);
1239
+ }
1240
+ const slugName = extension.slug.split(":")[1] ?? extension.slug;
1241
+ const pathTemplate = extension.path_convention ?? null;
1242
+ const extensionKeys = new Set(
1243
+ Object.keys(extension.add_fields?.properties ?? {})
1244
+ );
1245
+ const define = (params) => {
1246
+ const withDefaults = { ...params };
1247
+ if (extension.defaults) {
1248
+ for (const [key, value] of Object.entries(extension.defaults)) {
1249
+ if (withDefaults[key] === void 0 && value !== void 0) {
1250
+ withDefaults[key] = value;
1251
+ }
1252
+ }
1253
+ }
1254
+ if (isRoot) {
1255
+ return Object.freeze(withDefaults);
1256
+ }
1257
+ const parentOnly = {};
1258
+ const extensionOnly = {};
1259
+ for (const [key, value] of Object.entries(withDefaults)) {
1260
+ if (extensionKeys.has(key)) extensionOnly[key] = value;
1261
+ else parentOnly[key] = value;
1262
+ }
1263
+ const parentHandle = parent.define(parentOnly);
1264
+ return Object.freeze({
1265
+ ...parentHandle,
1266
+ ...extensionOnly
1267
+ });
1268
+ };
1269
+ const parse = isRoot ? rootParse(extension) : parent.parse;
1270
+ const pathOf = (handle) => {
1271
+ if (pathTemplate) {
1272
+ return resolvePathTemplate(
1273
+ pathTemplate,
1274
+ handle,
1275
+ slugName
1276
+ );
1277
+ }
1278
+ return parent.pathOf(handle);
1279
+ };
1280
+ return {
1281
+ name: extension.slug,
1282
+ aip: 40,
1283
+ schemaLiteral: parent?.schemaLiteral ?? `agentproto/extension/v1`,
1284
+ pathOf,
1285
+ define,
1286
+ parse
1287
+ };
1288
+ }
1289
+ function verifyTightening(extension, parent) {
1290
+ const t = extension.tighten ?? {};
1291
+ for (const [field, override] of Object.entries(t)) {
1292
+ if (typeof override.minLength === "number" && typeof override.maxLength === "number" && override.minLength > override.maxLength) {
1293
+ throw new Error(
1294
+ `specFromExtension (AIP-40): extension '${extension.slug}' tighten.${field}: minLength (${override.minLength}) > maxLength (${override.maxLength})`
1295
+ );
1296
+ }
1297
+ if (typeof override.minimum === "number" && typeof override.maximum === "number" && override.minimum > override.maximum) {
1298
+ throw new Error(
1299
+ `specFromExtension (AIP-40): extension '${extension.slug}' tighten.${field}: minimum (${override.minimum}) > maximum (${override.maximum})`
1300
+ );
1301
+ }
1302
+ if (override.enum !== void 0 && !Array.isArray(override.enum)) {
1303
+ throw new Error(
1304
+ `specFromExtension (AIP-40): extension '${extension.slug}' tighten.${field}: enum must be an array`
1305
+ );
1306
+ }
1307
+ }
1308
+ }
1309
+ function rootParse(extension) {
1310
+ return (source) => {
1311
+ throw new Error(
1312
+ `specFromExtension (AIP-40): extension '${extension.slug}' is a root doctype \u2014 supply your own parser or extend a public AIP for parser inheritance`
1313
+ );
1314
+ };
1315
+ }
1316
+ function resolvePathTemplate(template, handle, doctypeSlug) {
1317
+ const identity = typeof handle.id === "string" && handle.id || typeof handle.slug === "string" && handle.slug || typeof handle.name === "string" && handle.name || "unknown";
1318
+ return template.replace(/<slug>/g, identity).replace(/<DOCTYPE>/g, doctypeSlug.toUpperCase());
1319
+ }
1320
+
1321
+ // ../mcp-server/dist/index.mjs
1322
+ async function createMcpServer(opts) {
1323
+ const server = new McpServer({
1324
+ name: opts.name ?? "agentproto-mcp-server",
1325
+ version: opts.version ?? "0.1.0-alpha"
1326
+ });
1327
+ const allSpecs = [...opts.specs];
1328
+ if (opts.workspace) {
1329
+ const extensions = await loadExtensions(opts.workspace, opts.specs);
1330
+ allSpecs.push(...extensions);
1331
+ }
1332
+ const anchor = (p) => isAbsolute(p) || !opts.workspace ? p : join(opts.workspace, p);
1333
+ for (const spec of allSpecs) {
1334
+ registerVerbs(server, spec, anchor);
1335
+ }
1336
+ return {
1337
+ server,
1338
+ registered: allSpecs.map((s) => s.name)
1339
+ };
1340
+ }
1341
+ function registerVerbs(server, spec, anchor) {
1342
+ const verbs = createVerbs(spec);
1343
+ const verbName = (verb) => `${verb}_${spec.name.replace(/[-:]/g, "_")}`;
1344
+ const description = (verb, body) => `${verb} the AIP-${spec.aip} ${spec.name} doctype. ${body}`;
1345
+ server.tool(
1346
+ verbName("create"),
1347
+ description(
1348
+ "create",
1349
+ "Author a new manifest from params. Returns { path, rendered }."
1350
+ ),
1351
+ {
1352
+ params: z.record(z.string(), z.unknown()).describe(`The ${spec.name} definition fields.`),
1353
+ dir: z.string().describe("Workspace-relative or absolute target directory."),
1354
+ body: z.string().optional().describe("Markdown body after the frontmatter. Defaults to a stub."),
1355
+ dryRun: z.boolean().optional().describe("Render only \u2014 don't write to disk.")
1356
+ },
1357
+ async ({ params, dir, body, dryRun }) => {
1358
+ const result = await verbs.create(params, {
1359
+ dir: anchor(dir),
1360
+ body,
1361
+ dryRun
1362
+ });
1363
+ return contentText({
1364
+ path: result.path,
1365
+ rendered: result.rendered
1366
+ });
1367
+ }
1368
+ );
1369
+ server.tool(
1370
+ verbName("load"),
1371
+ description("load", "Read a manifest from disk. Returns the parsed handle."),
1372
+ {
1373
+ path: z.string().describe("Absolute or workspace-relative path to the .md file.")
1374
+ },
1375
+ async ({ path }) => {
1376
+ const result = await verbs.load(anchor(path));
1377
+ return contentText({ path: result.path, handle: result.handle });
1378
+ }
1379
+ );
1380
+ server.tool(
1381
+ verbName("list"),
1382
+ description(
1383
+ "list",
1384
+ "Walk a directory tree and return all manifests of this doctype."
1385
+ ),
1386
+ {
1387
+ dir: z.string().describe("Directory to walk."),
1388
+ skipDirs: z.array(z.string()).optional().describe("Subdir names to skip (default: node_modules, .git, dist).")
1389
+ },
1390
+ async ({ dir, skipDirs }) => {
1391
+ const handles = await verbs.list(anchor(dir), { skipDirs });
1392
+ return contentText({ count: handles.length, handles });
1393
+ }
1394
+ );
1395
+ server.tool(
1396
+ verbName("update"),
1397
+ description(
1398
+ "update",
1399
+ "Patch an existing manifest. The patch is shallow-merged into the existing params."
1400
+ ),
1401
+ {
1402
+ path: z.string().describe("Path to the manifest to update."),
1403
+ patch: z.record(z.string(), z.unknown()).describe("Partial fields to merge into the existing manifest."),
1404
+ body: z.string().optional().describe("New body markdown (default: keep existing).")
1405
+ },
1406
+ async ({ path, patch, body }) => {
1407
+ const result = await verbs.update(
1408
+ anchor(path),
1409
+ (existing) => ({ ...existing, ...patch }),
1410
+ { body }
1411
+ );
1412
+ return contentText({ path: result.path, rendered: result.rendered });
1413
+ }
1414
+ );
1415
+ server.tool(
1416
+ verbName("resolve"),
1417
+ description(
1418
+ "resolve",
1419
+ "Resolve an inline | ref | file block to a fully-typed handle."
1420
+ ),
1421
+ {
1422
+ block: z.union([
1423
+ z.object({ inline: z.record(z.string(), z.unknown()) }),
1424
+ z.object({ ref: z.string() }),
1425
+ z.object({ file: z.string() })
1426
+ ]).describe("The composition block."),
1427
+ baseDir: z.string().optional().describe("Base dir for `file:` references.")
1428
+ },
1429
+ async ({ block, baseDir }) => {
1430
+ const handle = await verbs.resolve(block, {
1431
+ baseDir: baseDir ? anchor(baseDir) : void 0
1432
+ });
1433
+ return contentText({ handle });
1434
+ }
1435
+ );
1436
+ server.tool(
1437
+ verbName("delete"),
1438
+ description("delete", "Remove a manifest file from disk."),
1439
+ {
1440
+ path: z.string().describe("Path to the manifest to delete.")
1441
+ },
1442
+ async ({ path }) => {
1443
+ const target = anchor(path);
1444
+ await verbs.delete(target);
1445
+ return contentText({ deleted: target });
1446
+ }
1447
+ );
1448
+ }
1449
+ async function loadExtensions(workspace, specs) {
1450
+ const extDir = resolve(workspace, "extensions");
1451
+ if (!existsSync(extDir)) return [];
1452
+ const out = [];
1453
+ const { readdir: readdir3 } = await import('fs/promises');
1454
+ let entries;
1455
+ try {
1456
+ entries = await readdir3(extDir, {
1457
+ withFileTypes: true
1458
+ });
1459
+ } catch {
1460
+ return out;
1461
+ }
1462
+ for (const entry of entries) {
1463
+ if (!entry.isDirectory()) continue;
1464
+ const manifestPath = join(extDir, String(entry.name), "EXTENSION.md");
1465
+ if (!existsSync(manifestPath)) continue;
1466
+ const source = await readFile(manifestPath, "utf8");
1467
+ const parsed = parseExtensionManifest(source);
1468
+ const ext = parsed.frontmatter;
1469
+ let parent;
1470
+ if (ext.extends && ext.extends !== "none") {
1471
+ const aipMatch = ext.extends.match(/^aip-(\d+)$/);
1472
+ if (!aipMatch) {
1473
+ throw new Error(
1474
+ `mcp-server: extension '${ext.slug}' has invalid extends '${ext.extends}'`
1475
+ );
1476
+ }
1477
+ const parentAip = Number(aipMatch[1]);
1478
+ parent = specs.find((s) => s.aip === parentAip);
1479
+ if (!parent) {
1480
+ throw new Error(
1481
+ `mcp-server: extension '${ext.slug}' extends aip-${parentAip}, but no spec for that AIP was registered. Pass the parent spec in opts.specs.`
1482
+ );
1483
+ }
1484
+ }
1485
+ out.push(specFromExtension(ext, { parent }));
1486
+ }
1487
+ return out;
1488
+ }
1489
+ function contentText(payload) {
1490
+ return {
1491
+ content: [
1492
+ {
1493
+ type: "text",
1494
+ text: JSON.stringify(payload, null, 2)
1495
+ }
1496
+ ]
1497
+ };
1498
+ }
1499
+ async function writeRuntimeMeta(workspace, meta) {
1500
+ const dir = join(workspace, ".agentproto");
1501
+ try {
1502
+ await mkdir(dir, { recursive: true });
1503
+ await writeFile(
1504
+ join(dir, "runtime.json"),
1505
+ JSON.stringify(meta, null, 2) + "\n",
1506
+ "utf8"
1507
+ );
1508
+ } catch (err) {
1509
+ console.error("[runtime] failed to write .agentproto/runtime.json:", err);
1510
+ }
1511
+ }
1512
+ var DEFAULT_TIMEOUT_MS = 6e4;
1513
+ var MAX_TIMEOUT_MS = 6e5;
1514
+ var ALLOWLIST_REL = ".agentproto/allowed-commands.json";
1515
+ var allowlistCache = null;
1516
+ async function loadAllowlist(workspace) {
1517
+ const path = resolve(workspace, ALLOWLIST_REL);
1518
+ if (!existsSync(path)) {
1519
+ allowlistCache = null;
1520
+ return /* @__PURE__ */ new Set();
1521
+ }
1522
+ try {
1523
+ const s = await stat(path);
1524
+ if (allowlistCache && allowlistCache.path === path && allowlistCache.entry.mtimeMs === s.mtimeMs) {
1525
+ return allowlistCache.entry.commands;
1526
+ }
1527
+ const raw = await readFile(path, "utf8");
1528
+ const parsed = JSON.parse(raw);
1529
+ const list = Array.isArray(parsed.commands) ? parsed.commands : [];
1530
+ const commands = new Set(
1531
+ list.filter((x) => typeof x === "string" && x.length > 0).map((x) => x.trim())
1532
+ );
1533
+ allowlistCache = { path, entry: { mtimeMs: s.mtimeMs, commands } };
1534
+ return commands;
1535
+ } catch (err) {
1536
+ console.error(
1537
+ `[runtime] failed to load ${ALLOWLIST_REL} (will deny all):`,
1538
+ err
1539
+ );
1540
+ allowlistCache = null;
1541
+ return /* @__PURE__ */ new Set();
1542
+ }
1543
+ }
1544
+ function makeCwdAnchor(workspace) {
1545
+ const root = resolve(workspace);
1546
+ return (input2) => {
1547
+ if (!input2 || input2.length === 0) return root;
1548
+ const candidate = isAbsolute(input2) ? normalize(input2) : normalize(join(root, input2));
1549
+ const rel = relative(root, candidate);
1550
+ if (rel.startsWith("..") || isAbsolute(rel)) {
1551
+ throw new Error(
1552
+ `cwd escapes the workspace: '${input2}' (workspace=${root})`
1553
+ );
1554
+ }
1555
+ return candidate;
1556
+ };
1557
+ }
1558
+ function registerCommandTools(server, opts) {
1559
+ const anchorCwd = makeCwdAnchor(opts.workspace);
1560
+ server.tool(
1561
+ "execute_command",
1562
+ "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.",
1563
+ {
1564
+ command: z.string().min(1).describe(
1565
+ "Executable name or absolute path. Must be allowlisted by basename in the workspace's allowed-commands.json."
1566
+ ),
1567
+ args: z.array(z.string()).optional().describe(
1568
+ "Argv array. Passed verbatim \u2014 no shell expansion (we spawn with shell:false so quoting doesn't bite)."
1569
+ ),
1570
+ cwd: z.string().optional().describe(
1571
+ "Working directory, workspace-relative or an absolute path inside the workspace. Defaults to the workspace root."
1572
+ ),
1573
+ stdin: z.string().optional().describe("Optional input piped to the process's stdin."),
1574
+ timeoutMs: z.number().int().positive().max(MAX_TIMEOUT_MS).optional().describe(
1575
+ `Hard kill after this many ms. Defaults to ${DEFAULT_TIMEOUT_MS}; capped at ${MAX_TIMEOUT_MS}.`
1576
+ )
1577
+ },
1578
+ async ({ command, args, cwd, stdin, timeoutMs }) => {
1579
+ const allowlist = await loadAllowlist(opts.workspace);
1580
+ const baseName = basename(command);
1581
+ if (!allowlist.has(baseName)) {
1582
+ const allowed = [...allowlist].sort().join(", ") || "(empty)";
1583
+ throw new Error(
1584
+ `command '${baseName}' is not in the allowlist. Add it to ${join(opts.workspace, ALLOWLIST_REL)} under "commands": [...]. Currently allowed: ${allowed}.`
1585
+ );
1586
+ }
1587
+ const resolvedCwd = anchorCwd(cwd);
1588
+ const limit = Math.min(timeoutMs ?? DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS);
1589
+ const result = await runCommand({
1590
+ command,
1591
+ args: args ?? [],
1592
+ cwd: resolvedCwd,
1593
+ stdin,
1594
+ timeoutMs: limit
1595
+ });
1596
+ return {
1597
+ content: [{ type: "text", text: JSON.stringify(result) }]
1598
+ };
1599
+ }
1600
+ );
1601
+ }
1602
+ var STREAM_BUFFER_CAP = 1048576;
1603
+ async function runCommand(input2) {
1604
+ return new Promise((resolvePromise) => {
1605
+ const startedAt = Date.now();
1606
+ const child = spawn(input2.command, input2.args, {
1607
+ cwd: input2.cwd,
1608
+ shell: false,
1609
+ // Inherit user env so PATH lookups for `claude`, `gh`, etc. work.
1610
+ env: process.env,
1611
+ stdio: ["pipe", "pipe", "pipe"]
1612
+ });
1613
+ let stdout = "";
1614
+ let stderr = "";
1615
+ let truncated = false;
1616
+ let timedOut = false;
1617
+ function appendCapped(buf, chunk) {
1618
+ if (buf.length >= STREAM_BUFFER_CAP) {
1619
+ truncated = true;
1620
+ return buf;
1621
+ }
1622
+ const room = STREAM_BUFFER_CAP - buf.length;
1623
+ const text3 = chunk.toString("utf8");
1624
+ if (text3.length > room) {
1625
+ truncated = true;
1626
+ return buf + text3.slice(0, room);
1627
+ }
1628
+ return buf + text3;
1629
+ }
1630
+ child.stdout?.on("data", (d) => {
1631
+ stdout = appendCapped(stdout, d);
1632
+ });
1633
+ child.stderr?.on("data", (d) => {
1634
+ stderr = appendCapped(stderr, d);
1635
+ });
1636
+ if (input2.stdin) {
1637
+ child.stdin?.write(input2.stdin);
1638
+ }
1639
+ child.stdin?.end();
1640
+ const timer = setTimeout(() => {
1641
+ timedOut = true;
1642
+ try {
1643
+ child.kill("SIGTERM");
1644
+ } catch {
1645
+ }
1646
+ setTimeout(() => {
1647
+ try {
1648
+ child.kill("SIGKILL");
1649
+ } catch {
1650
+ }
1651
+ }, 2e3).unref();
1652
+ }, input2.timeoutMs);
1653
+ timer.unref();
1654
+ child.on("error", (err) => {
1655
+ clearTimeout(timer);
1656
+ resolvePromise({
1657
+ exitCode: -1,
1658
+ signal: null,
1659
+ stdout,
1660
+ stderr: stderr + (stderr ? "\n" : "") + err.message,
1661
+ truncated,
1662
+ durationMs: Date.now() - startedAt
1663
+ });
1664
+ });
1665
+ child.on("close", (code, signal) => {
1666
+ clearTimeout(timer);
1667
+ resolvePromise({
1668
+ exitCode: typeof code === "number" ? code : -1,
1669
+ signal: signal ?? (timedOut ? "SIGTERM-timeout" : null),
1670
+ stdout,
1671
+ stderr,
1672
+ truncated,
1673
+ durationMs: Date.now() - startedAt
1674
+ });
1675
+ });
1676
+ });
1677
+ }
1678
+ var FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n/;
1679
+ function fileConversationStore(opts) {
1680
+ const dir = opts.dir ?? join(opts.workspace, "conversations");
1681
+ const filePath = (id) => join(dir, `${id}.md`);
1682
+ return {
1683
+ pathFor: filePath,
1684
+ async open(id, meta) {
1685
+ await mkdir(dir, { recursive: true });
1686
+ const path = filePath(id);
1687
+ if (existsSync(path)) return;
1688
+ const header = renderHeader({
1689
+ id,
1690
+ agent: meta.agent,
1691
+ started: (/* @__PURE__ */ new Date()).toISOString(),
1692
+ status: "open"
1693
+ });
1694
+ await writeFile(path, header, "utf8");
1695
+ },
1696
+ async appendTurn(id, role, content, options) {
1697
+ const path = filePath(id);
1698
+ if (!existsSync(path)) {
1699
+ await this.open(id, { agent: options?.attribution ?? "unknown" });
1700
+ }
1701
+ const block = renderTurn({
1702
+ role,
1703
+ at: options?.at ?? (/* @__PURE__ */ new Date()).toISOString(),
1704
+ attribution: options?.attribution,
1705
+ content
1706
+ });
1707
+ await writeFile(path, block, { encoding: "utf8", flag: "a" });
1708
+ },
1709
+ async read(id) {
1710
+ const path = filePath(id);
1711
+ const source = await readFile(path, "utf8");
1712
+ return parseConversation(source, id);
1713
+ },
1714
+ async list() {
1715
+ if (!existsSync(dir)) return [];
1716
+ const entries = await readdir(dir, { withFileTypes: true });
1717
+ const out = [];
1718
+ for (const entry of entries) {
1719
+ if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
1720
+ const id = entry.name.slice(0, -3);
1721
+ try {
1722
+ const source = await readFile(join(dir, entry.name), "utf8");
1723
+ const { meta } = parseConversation(source, id);
1724
+ out.push({ id, meta });
1725
+ } catch {
1726
+ }
1727
+ }
1728
+ return out;
1729
+ }
1730
+ };
1731
+ }
1732
+ function renderHeader(meta) {
1733
+ return [
1734
+ "---",
1735
+ `schema: conversation/v1`,
1736
+ `id: ${meta.id}`,
1737
+ `agent: ${meta.agent}`,
1738
+ `started: ${meta.started}`,
1739
+ `status: ${meta.status}`,
1740
+ "---",
1741
+ "",
1742
+ ""
1743
+ ].join("\n");
1744
+ }
1745
+ function renderTurn(turn) {
1746
+ const heading = turn.attribution ? `## ${turn.role} \u2014 ${turn.at} (${turn.attribution})` : `## ${turn.role} \u2014 ${turn.at}`;
1747
+ return `${heading}
1748
+
1749
+ ${turn.content.trimEnd()}
1750
+
1751
+ `;
1752
+ }
1753
+ function parseConversation(source, fallbackId) {
1754
+ const match = source.match(FRONTMATTER_RE);
1755
+ let meta = {
1756
+ id: fallbackId,
1757
+ agent: "unknown",
1758
+ started: "",
1759
+ status: "open"
1760
+ };
1761
+ let body = source;
1762
+ if (match) {
1763
+ meta = parseFrontmatter(match[1] ?? "", fallbackId);
1764
+ body = source.slice(match[0].length);
1765
+ }
1766
+ const turns = [];
1767
+ const lines = body.split("\n");
1768
+ let current = null;
1769
+ let buffer = [];
1770
+ const flush = () => {
1771
+ if (!current) return;
1772
+ current.content = buffer.join("\n").trim();
1773
+ turns.push(current);
1774
+ current = null;
1775
+ buffer = [];
1776
+ };
1777
+ const headingRe = /^##\s+(user|assistant|system)\s+—\s+(\S+)(?:\s+\(([^)]+)\))?\s*$/;
1778
+ for (const line of lines) {
1779
+ const m = line.match(headingRe);
1780
+ if (m) {
1781
+ flush();
1782
+ current = {
1783
+ role: m[1],
1784
+ at: m[2] ?? "",
1785
+ attribution: m[3],
1786
+ content: ""
1787
+ };
1788
+ continue;
1789
+ }
1790
+ if (current) buffer.push(line);
1791
+ }
1792
+ flush();
1793
+ return { meta, turns };
1794
+ }
1795
+ function parseFrontmatter(raw, fallbackId) {
1796
+ const out = {};
1797
+ for (const line of raw.split("\n")) {
1798
+ const colon = line.indexOf(":");
1799
+ if (colon < 1) continue;
1800
+ const key = line.slice(0, colon).trim();
1801
+ const value = line.slice(colon + 1).trim();
1802
+ out[key] = value;
1803
+ }
1804
+ return {
1805
+ id: out.id ?? fallbackId,
1806
+ agent: out.agent ?? "unknown",
1807
+ started: out.started ?? "",
1808
+ status: out.status === "closed" ? "closed" : "open"
1809
+ };
1810
+ }
1811
+ function createRuntimeEvents() {
1812
+ const ee = new EventEmitter();
1813
+ ee.setMaxListeners(50);
1814
+ return {
1815
+ on(type, handler) {
1816
+ const wrapped = (ev) => {
1817
+ if (ev.type === type) handler(ev);
1818
+ };
1819
+ ee.on("event", wrapped);
1820
+ return () => ee.off("event", wrapped);
1821
+ },
1822
+ onAny(handler) {
1823
+ ee.on("event", handler);
1824
+ return () => ee.off("event", handler);
1825
+ },
1826
+ emit(ev) {
1827
+ ee.emit("event", ev);
1828
+ }
1829
+ };
1830
+ }
1831
+ var FsPathError = class extends Error {
1832
+ constructor(message) {
1833
+ super(message);
1834
+ this.name = "FsPathError";
1835
+ }
1836
+ };
1837
+ function makeAnchor(workspace) {
1838
+ const root = resolve(workspace);
1839
+ return (input2) => {
1840
+ if (typeof input2 !== "string" || input2.length === 0) {
1841
+ throw new FsPathError("path must be a non-empty string");
1842
+ }
1843
+ const candidate = isAbsolute(input2) ? normalize(input2) : normalize(join(root, input2));
1844
+ const rel = relative(root, candidate);
1845
+ if (rel.startsWith("..") || isAbsolute(rel)) {
1846
+ throw new FsPathError(`path escapes the workspace: '${input2}'`);
1847
+ }
1848
+ return candidate;
1849
+ };
1850
+ }
1851
+ function text(value) {
1852
+ return {
1853
+ content: [
1854
+ {
1855
+ type: "text",
1856
+ text: typeof value === "string" ? value : JSON.stringify(value)
1857
+ }
1858
+ ]
1859
+ };
1860
+ }
1861
+ function registerFsTools(server, opts) {
1862
+ const anchor = makeAnchor(opts.workspace);
1863
+ server.tool(
1864
+ "read_file",
1865
+ "Read a UTF-8 file from the workspace.",
1866
+ { path: z.string().describe("Workspace-relative path to the file.") },
1867
+ async ({ path }) => {
1868
+ const abs = anchor(path);
1869
+ const buf = await readFile(abs);
1870
+ return text(buf.toString("utf8"));
1871
+ }
1872
+ );
1873
+ server.tool(
1874
+ "write_file",
1875
+ "Write a file to the workspace. Parent directories are created on demand.",
1876
+ {
1877
+ path: z.string().describe("Workspace-relative path to the file."),
1878
+ content: z.string().describe("File body (UTF-8).")
1879
+ },
1880
+ async ({ path, content }) => {
1881
+ const abs = anchor(path);
1882
+ await mkdir(dirname(abs), { recursive: true });
1883
+ await writeFile(abs, content, "utf8");
1884
+ return text("ok");
1885
+ }
1886
+ );
1887
+ server.tool(
1888
+ "list_directory",
1889
+ "List entries of a directory in the workspace. Returns one '[FILE]' or '[DIR]' line per entry, mirroring `@modelcontextprotocol/server-filesystem`.",
1890
+ {
1891
+ path: z.string().optional().describe("Workspace-relative directory (defaults to workspace root).")
1892
+ },
1893
+ async ({ path }) => {
1894
+ const abs = anchor(path && path.length > 0 ? path : ".");
1895
+ const entries = await readdir(abs, { withFileTypes: true });
1896
+ const lines = entries.map(
1897
+ (e) => e.isDirectory() ? `[DIR] ${e.name}` : `[FILE] ${e.name}`
1898
+ );
1899
+ return text(lines.join("\n"));
1900
+ }
1901
+ );
1902
+ server.tool(
1903
+ "get_file_info",
1904
+ "Stat a file or directory in the workspace.",
1905
+ { path: z.string().describe("Workspace-relative path.") },
1906
+ async ({ path }) => {
1907
+ const abs = anchor(path);
1908
+ const info = await stat(abs);
1909
+ return text({
1910
+ name: abs.split("/").pop() ?? path,
1911
+ path,
1912
+ type: info.isDirectory() ? "directory" : "file",
1913
+ size: info.size,
1914
+ modified: info.mtime.toISOString(),
1915
+ created: info.birthtime.toISOString()
1916
+ });
1917
+ }
1918
+ );
1919
+ server.tool(
1920
+ "create_directory",
1921
+ "Create a directory (recursive) in the workspace.",
1922
+ { path: z.string().describe("Workspace-relative directory path.") },
1923
+ async ({ path }) => {
1924
+ const abs = anchor(path);
1925
+ await mkdir(abs, { recursive: true });
1926
+ return text("ok");
1927
+ }
1928
+ );
1929
+ server.tool(
1930
+ "delete_file",
1931
+ "Delete a file or empty directory in the workspace.",
1932
+ { path: z.string().describe("Workspace-relative path.") },
1933
+ async ({ path }) => {
1934
+ const abs = anchor(path);
1935
+ await rm(abs, { recursive: true, force: true });
1936
+ return text("ok");
1937
+ }
1938
+ );
1939
+ }
1940
+ var WORKSPACES_CONFIG_VERSION = 1;
1941
+ var DEFAULT_CONFIG_DIR = () => resolve(homedir(), ".agentproto");
1942
+ var DEFAULT_CONFIG_PATH = () => resolve(DEFAULT_CONFIG_DIR(), "workspaces.json");
1943
+ function sanitizeSlug(input2) {
1944
+ const trimmed = input2.trim().toLowerCase();
1945
+ const cleaned = trimmed.replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
1946
+ return cleaned || "workspace";
1947
+ }
1948
+ async function loadWorkspacesConfig(path = DEFAULT_CONFIG_PATH()) {
1949
+ let raw;
1950
+ try {
1951
+ raw = await promises.readFile(path, "utf8");
1952
+ } catch (err) {
1953
+ if (err.code === "ENOENT") {
1954
+ return { version: WORKSPACES_CONFIG_VERSION, workspaces: [] };
1955
+ }
1956
+ throw err;
1957
+ }
1958
+ let parsed;
1959
+ try {
1960
+ parsed = JSON.parse(raw);
1961
+ } catch (err) {
1962
+ throw new Error(
1963
+ `agentproto: ${path} is not valid JSON (${err instanceof Error ? err.message : String(err)}). Delete the file or fix it manually.`
1964
+ );
1965
+ }
1966
+ return normaliseConfig(parsed);
1967
+ }
1968
+ function findWorkspace(config, slug) {
1969
+ return config.workspaces.find((w) => w.slug === sanitizeSlug(slug));
1970
+ }
1971
+ function getActiveWorkspace(config) {
1972
+ if (!config.active) return config.workspaces[0];
1973
+ return findWorkspace(config, config.active) ?? config.workspaces[0];
1974
+ }
1975
+ function normaliseConfig(parsed) {
1976
+ if (!parsed || typeof parsed !== "object") {
1977
+ return { version: WORKSPACES_CONFIG_VERSION, workspaces: [] };
1978
+ }
1979
+ const obj = parsed;
1980
+ const workspaces = [];
1981
+ if (Array.isArray(obj.workspaces)) {
1982
+ for (const entry of obj.workspaces) {
1983
+ if (!entry || typeof entry !== "object") continue;
1984
+ const e = entry;
1985
+ const slug = typeof e.slug === "string" ? sanitizeSlug(e.slug) : "";
1986
+ const path = typeof e.path === "string" ? e.path : "";
1987
+ if (!slug || !path) continue;
1988
+ const addedAt = typeof e.addedAt === "string" ? e.addedAt : (/* @__PURE__ */ new Date()).toISOString();
1989
+ const updatedAt = typeof e.updatedAt === "string" ? e.updatedAt : addedAt;
1990
+ const we = { slug, path, addedAt, updatedAt };
1991
+ if (typeof e.label === "string" && e.label.trim()) {
1992
+ we.label = e.label.trim();
1993
+ }
1994
+ workspaces.push(we);
1995
+ }
1996
+ }
1997
+ const dedup = /* @__PURE__ */ new Map();
1998
+ for (const w of workspaces) dedup.set(w.slug, w);
1999
+ const finalList = Array.from(dedup.values());
2000
+ const active = typeof obj.active === "string" && finalList.some((w) => w.slug === obj.active) ? obj.active : finalList[0]?.slug;
2001
+ const out = {
2002
+ version: WORKSPACES_CONFIG_VERSION,
2003
+ workspaces: finalList
2004
+ };
2005
+ if (active !== void 0) out.active = active;
2006
+ return out;
2007
+ }
2008
+ async function discoverMcps(opts = {}) {
2009
+ const home = opts.home ?? homedir();
2010
+ const out = [];
2011
+ const errors = [];
2012
+ await Promise.all([
2013
+ scanClaudeCode(home, out, errors),
2014
+ scanCursor(home, out, errors),
2015
+ scanGoose(home, out),
2016
+ scanRegisteredWorkspaces(out, errors)
2017
+ ]);
2018
+ const seen = /* @__PURE__ */ new Set();
2019
+ const dedup = [];
2020
+ for (const m of out) {
2021
+ const key = `${m.source}:${m.scope}:${m.name}`;
2022
+ if (seen.has(key)) continue;
2023
+ seen.add(key);
2024
+ dedup.push(m);
2025
+ }
2026
+ if (errors.length > 0) {
2027
+ for (const e of errors) console.warn(`[mcp-discovery] ${e}`);
2028
+ }
2029
+ return dedup.sort((a, b) => {
2030
+ if (a.source !== b.source) return a.source.localeCompare(b.source);
2031
+ if (a.scope !== b.scope) return a.scope.localeCompare(b.scope);
2032
+ return a.name.localeCompare(b.name);
2033
+ });
2034
+ }
2035
+ async function scanClaudeCode(home, out, errors) {
2036
+ const path = resolve(home, ".claude.json");
2037
+ let raw;
2038
+ try {
2039
+ raw = await promises.readFile(path, "utf8");
2040
+ } catch (err) {
2041
+ if (err.code === "ENOENT") return;
2042
+ errors.push(`claude-code: read ${path} \u2014 ${err.message}`);
2043
+ return;
2044
+ }
2045
+ let parsed;
2046
+ try {
2047
+ parsed = JSON.parse(raw);
2048
+ } catch (err) {
2049
+ errors.push(`claude-code: JSON parse ${path} \u2014 ${err.message}`);
2050
+ return;
2051
+ }
2052
+ if (!parsed || typeof parsed !== "object") return;
2053
+ const root = parsed;
2054
+ const top = root.mcpServers;
2055
+ if (top && typeof top === "object") {
2056
+ pushMcpMap({
2057
+ out,
2058
+ source: "claude-code",
2059
+ scope: "global",
2060
+ map: top
2061
+ });
2062
+ }
2063
+ const projects = root.projects;
2064
+ if (projects && typeof projects === "object") {
2065
+ for (const [projPath, proj] of Object.entries(
2066
+ projects
2067
+ )) {
2068
+ if (!proj || typeof proj !== "object") continue;
2069
+ const projMcps = proj.mcpServers;
2070
+ if (!projMcps || typeof projMcps !== "object") continue;
2071
+ pushMcpMap({
2072
+ out,
2073
+ source: "claude-code",
2074
+ scope: `project:${projPath}`,
2075
+ map: projMcps
2076
+ });
2077
+ }
2078
+ }
2079
+ }
2080
+ async function scanCursor(home, out, errors) {
2081
+ const path = resolve(home, ".cursor", "mcp.json");
2082
+ let raw;
2083
+ try {
2084
+ raw = await promises.readFile(path, "utf8");
2085
+ } catch (err) {
2086
+ if (err.code === "ENOENT") return;
2087
+ errors.push(`cursor: read ${path} \u2014 ${err.message}`);
2088
+ return;
2089
+ }
2090
+ let parsed;
2091
+ try {
2092
+ parsed = JSON.parse(raw);
2093
+ } catch (err) {
2094
+ errors.push(`cursor: JSON parse ${path} \u2014 ${err.message}`);
2095
+ return;
2096
+ }
2097
+ if (!parsed || typeof parsed !== "object") return;
2098
+ const root = parsed;
2099
+ const map = root.mcpServers;
2100
+ if (map && typeof map === "object") {
2101
+ pushMcpMap({
2102
+ out,
2103
+ source: "cursor",
2104
+ scope: "global",
2105
+ map
2106
+ });
2107
+ }
2108
+ }
2109
+ async function scanGoose(home, out, errors) {
2110
+ const path = resolve(home, ".config", "goose", "config.yaml");
2111
+ try {
2112
+ await promises.access(path);
2113
+ } catch {
2114
+ return;
2115
+ }
2116
+ out.push({
2117
+ id: "goose:global:_detected",
2118
+ source: "goose",
2119
+ scope: "global",
2120
+ name: "(goose config detected)",
2121
+ type: "unknown",
2122
+ parseNote: "Goose config found but YAML parsing is not yet implemented. Open ~/.config/goose/config.yaml to inspect MCP entries."
2123
+ });
2124
+ }
2125
+ async function scanRegisteredWorkspaces(out, errors) {
2126
+ let workspaces = [];
2127
+ try {
2128
+ const cfg = await loadWorkspacesConfig();
2129
+ workspaces = cfg.workspaces;
2130
+ } catch (err) {
2131
+ errors.push(`workspaces: load failed \u2014 ${err.message}`);
2132
+ return;
2133
+ }
2134
+ await Promise.all(
2135
+ workspaces.map(async (ws) => {
2136
+ const candidates = [
2137
+ resolve(ws.path, ".mcp.json"),
2138
+ resolve(ws.path, ".cursor", "mcp.json"),
2139
+ resolve(ws.path, ".vscode", "mcp.json")
2140
+ ];
2141
+ for (const path of candidates) {
2142
+ let raw;
2143
+ try {
2144
+ raw = await promises.readFile(path, "utf8");
2145
+ } catch (err) {
2146
+ if (err.code === "ENOENT") continue;
2147
+ errors.push(
2148
+ `workspace ${ws.slug}: read ${path} \u2014 ${err.message}`
2149
+ );
2150
+ continue;
2151
+ }
2152
+ let parsed;
2153
+ try {
2154
+ parsed = JSON.parse(raw);
2155
+ } catch (err) {
2156
+ errors.push(
2157
+ `workspace ${ws.slug}: JSON parse ${path} \u2014 ${err.message}`
2158
+ );
2159
+ continue;
2160
+ }
2161
+ if (!parsed || typeof parsed !== "object") continue;
2162
+ const map = parsed.mcpServers;
2163
+ if (!map || typeof map !== "object") continue;
2164
+ pushMcpMap({
2165
+ out,
2166
+ source: "workspace",
2167
+ scope: `workspace:${ws.slug}`,
2168
+ map
2169
+ });
2170
+ }
2171
+ })
2172
+ );
2173
+ }
2174
+ function pushMcpMap(args) {
2175
+ for (const [name, raw] of Object.entries(args.map)) {
2176
+ if (!raw || typeof raw !== "object") continue;
2177
+ const entry = raw;
2178
+ const id = `${args.source}:${args.scope}:${name}`;
2179
+ const declared = typeof entry.type === "string" ? entry.type : null;
2180
+ const url = typeof entry.url === "string" ? entry.url : void 0;
2181
+ const command = typeof entry.command === "string" ? entry.command : void 0;
2182
+ let type;
2183
+ if (declared === "http" || declared === "sse" || declared === "stdio") {
2184
+ type = declared;
2185
+ } else if (url) {
2186
+ type = "http";
2187
+ } else if (command) {
2188
+ type = "stdio";
2189
+ } else {
2190
+ type = "unknown";
2191
+ }
2192
+ const m = {
2193
+ id,
2194
+ source: args.source,
2195
+ scope: args.scope,
2196
+ name,
2197
+ type
2198
+ };
2199
+ if (command !== void 0) m.command = command;
2200
+ if (Array.isArray(entry.args)) {
2201
+ m.args = entry.args.filter((a) => typeof a === "string");
2202
+ }
2203
+ if (entry.env && typeof entry.env === "object") {
2204
+ m.env = stringifyValues(entry.env);
2205
+ }
2206
+ if (url !== void 0) m.url = url;
2207
+ if (entry.headers && typeof entry.headers === "object") {
2208
+ m.headers = stringifyValues(entry.headers);
2209
+ }
2210
+ if (type === "unknown") {
2211
+ m.parseNote = "Entry has neither `command` nor `url` \u2014 couldn't classify transport.";
2212
+ }
2213
+ args.out.push(m);
2214
+ }
2215
+ }
2216
+ function stringifyValues(raw) {
2217
+ const out = {};
2218
+ for (const [k, v] of Object.entries(raw)) {
2219
+ if (typeof v === "string") out[k] = v;
2220
+ else if (v != null) out[k] = String(v);
2221
+ }
2222
+ return out;
2223
+ }
2224
+ var IMPORTED_MCPS_PATH = () => resolve(homedir(), ".agentproto", "imported-mcps.json");
2225
+ var IMPORTED_MCPS_VERSION = 1;
2226
+ var EMPTY = {
2227
+ version: IMPORTED_MCPS_VERSION,
2228
+ imports: []
2229
+ };
2230
+ async function loadImportedMcps(path = IMPORTED_MCPS_PATH()) {
2231
+ let raw;
2232
+ try {
2233
+ raw = await promises.readFile(path, "utf8");
2234
+ } catch (err) {
2235
+ if (err.code === "ENOENT") return EMPTY;
2236
+ throw err;
2237
+ }
2238
+ let parsed;
2239
+ try {
2240
+ parsed = JSON.parse(raw);
2241
+ } catch (err) {
2242
+ throw new Error(
2243
+ `agentproto: ${path} is not valid JSON (${err instanceof Error ? err.message : String(err)}). Delete or fix manually.`
2244
+ );
2245
+ }
2246
+ return normalise(parsed);
2247
+ }
2248
+ async function saveImportedMcps(config, path = IMPORTED_MCPS_PATH()) {
2249
+ await promises.mkdir(dirname(path), { recursive: true });
2250
+ const tmp = `${path}.tmp.${process.pid}`;
2251
+ await promises.writeFile(tmp, JSON.stringify(config, null, 2) + "\n", "utf8");
2252
+ await promises.rename(tmp, path);
2253
+ }
2254
+ function addImport(config, input2) {
2255
+ const alias = (input2.alias ?? input2.snapshot.name).trim();
2256
+ if (!alias) {
2257
+ throw new Error("addImport: alias resolves to empty string");
2258
+ }
2259
+ const addedAt = (/* @__PURE__ */ new Date()).toISOString();
2260
+ const next = {
2261
+ id: input2.snapshot.id,
2262
+ alias,
2263
+ addedAt,
2264
+ snapshot: input2.snapshot
2265
+ };
2266
+ const others = config.imports.filter((e) => e.id !== input2.snapshot.id);
2267
+ return { ...config, imports: [...others, next] };
2268
+ }
2269
+ function removeImport(config, id) {
2270
+ return { ...config, imports: config.imports.filter((e) => e.id !== id) };
2271
+ }
2272
+ function normalise(parsed) {
2273
+ if (!parsed || typeof parsed !== "object") return EMPTY;
2274
+ const obj = parsed;
2275
+ const imports = [];
2276
+ if (Array.isArray(obj.imports)) {
2277
+ for (const entry of obj.imports) {
2278
+ if (!entry || typeof entry !== "object") continue;
2279
+ const e = entry;
2280
+ const id = typeof e.id === "string" ? e.id : "";
2281
+ const alias = typeof e.alias === "string" ? e.alias : id;
2282
+ const addedAt = typeof e.addedAt === "string" ? e.addedAt : (/* @__PURE__ */ new Date()).toISOString();
2283
+ const snapshot = e.snapshot;
2284
+ if (!id || !snapshot) continue;
2285
+ imports.push({ id, alias, addedAt, snapshot });
2286
+ }
2287
+ }
2288
+ return { version: IMPORTED_MCPS_VERSION, imports };
2289
+ }
2290
+ function registerSessionTools(server, opts) {
2291
+ const { registry, resolveAgentAdapter, listAgentAdapters, mcpProxy } = opts;
2292
+ server.tool(
2293
+ "start_agent_session",
2294
+ "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.",
2295
+ {
2296
+ adapter: z.string().min(1).describe(
2297
+ "Adapter slug \u2014 one of the installed `@agentproto/adapter-*` packages (e.g. 'claude-code', 'hermes', 'aider')."
2298
+ ),
2299
+ workspaceSlug: z.string().optional().describe(
2300
+ "Workspace slug from `agentproto workspace list`. The daemon resolves it to an absolute path. Omit to use the `cwd` field or the active workspace."
2301
+ ),
2302
+ cwd: z.string().optional().describe(
2303
+ "Absolute path to spawn the agent in. Wins over `workspaceSlug` when both are set."
2304
+ ),
2305
+ prompt: z.string().optional().describe(
2306
+ "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."
2307
+ ),
2308
+ label: z.string().optional().describe(
2309
+ "Free-text label that surfaces in `list_agent_sessions` and the UI \u2014 useful for tagging sessions with a conversation id or operator name."
2310
+ )
2311
+ },
2312
+ async (input2) => {
2313
+ if (!resolveAgentAdapter) {
2314
+ return {
2315
+ content: [
2316
+ {
2317
+ type: "text",
2318
+ 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)."
2319
+ }
2320
+ ],
2321
+ isError: true
2322
+ };
2323
+ }
2324
+ let cwd = input2.cwd;
2325
+ let resolvedSlug = input2.workspaceSlug ?? "default";
2326
+ if (!cwd) {
2327
+ try {
2328
+ const config = await loadWorkspacesConfig();
2329
+ const ws = input2.workspaceSlug ? findWorkspace(config, input2.workspaceSlug) : getActiveWorkspace(config);
2330
+ if (ws) {
2331
+ cwd = ws.path;
2332
+ resolvedSlug = ws.slug;
2333
+ }
2334
+ } catch {
2335
+ }
2336
+ }
2337
+ if (!cwd) {
2338
+ return {
2339
+ content: [
2340
+ {
2341
+ type: "text",
2342
+ 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>`."
2343
+ }
2344
+ ],
2345
+ isError: true
2346
+ };
2347
+ }
2348
+ const resolved = await resolveAgentAdapter(input2.adapter);
2349
+ if (!resolved) {
2350
+ return {
2351
+ content: [
2352
+ {
2353
+ type: "text",
2354
+ text: `start_agent_session: adapter "${input2.adapter}" not found. Try \`agentproto install <slug>\` first.`
2355
+ }
2356
+ ],
2357
+ isError: true
2358
+ };
2359
+ }
2360
+ try {
2361
+ const agentSession = await resolved.startSession({ cwd });
2362
+ const desc = registry.spawnAgent({
2363
+ workspaceSlug: resolvedSlug,
2364
+ cwd,
2365
+ agentSession,
2366
+ adapterSlug: input2.adapter,
2367
+ ...input2.prompt ? { initialPrompt: input2.prompt } : {},
2368
+ ...input2.label ? { label: input2.label } : {},
2369
+ ...resolved.commandPreview ? { commandPreview: resolved.commandPreview } : {}
2370
+ });
2371
+ return {
2372
+ content: [{ type: "text", text: JSON.stringify(desc, null, 2) }]
2373
+ };
2374
+ } catch (err) {
2375
+ return {
2376
+ content: [
2377
+ {
2378
+ type: "text",
2379
+ text: `start_agent_session: spawn failed \u2014 ${err instanceof Error ? err.message : String(err)}`
2380
+ }
2381
+ ],
2382
+ isError: true
2383
+ };
2384
+ }
2385
+ }
2386
+ );
2387
+ server.tool(
2388
+ "prompt_agent_session",
2389
+ "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.",
2390
+ {
2391
+ sessionId: z.string().describe("Session id returned by start_agent_session."),
2392
+ prompt: z.string().min(1).describe("The next user turn (plain text).")
2393
+ },
2394
+ async (input2) => {
2395
+ try {
2396
+ void registry.sendPrompt(input2.sessionId, input2.prompt).catch(() => {
2397
+ });
2398
+ return {
2399
+ content: [
2400
+ {
2401
+ type: "text",
2402
+ text: JSON.stringify(
2403
+ { ok: true, sessionId: input2.sessionId, queued: true },
2404
+ null,
2405
+ 2
2406
+ )
2407
+ }
2408
+ ]
2409
+ };
2410
+ } catch (err) {
2411
+ return {
2412
+ content: [
2413
+ {
2414
+ type: "text",
2415
+ text: `prompt_agent_session: ${err instanceof Error ? err.message : String(err)}`
2416
+ }
2417
+ ],
2418
+ isError: true
2419
+ };
2420
+ }
2421
+ }
2422
+ );
2423
+ server.tool(
2424
+ "list_agent_sessions",
2425
+ "List all sessions tracked by the daemon \u2014 running, exited, killed. Use to find a session id when the operator's previous start call returned long ago, or to tell the user what's currently active on their machine.",
2426
+ {
2427
+ onlyAlive: z.boolean().optional().describe("Filter to status running/starting/mounting only. Default false.")
2428
+ },
2429
+ async (input2) => {
2430
+ const all = registry.list();
2431
+ const filtered = input2.onlyAlive ? all.filter(
2432
+ (s) => s.status === "running" || s.status === "starting" || s.status === "mounting"
2433
+ ) : all;
2434
+ return {
2435
+ content: [
2436
+ {
2437
+ type: "text",
2438
+ text: JSON.stringify({ sessions: filtered }, null, 2)
2439
+ }
2440
+ ]
2441
+ };
2442
+ }
2443
+ );
2444
+ server.tool(
2445
+ "get_agent_session_output",
2446
+ "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`.",
2447
+ {
2448
+ sessionId: z.string().describe("Session id."),
2449
+ lastN: z.number().int().min(1).max(500).optional().describe("Max lines to return. Default 80, max 500.")
2450
+ },
2451
+ async (input2) => {
2452
+ const desc = registry.get(input2.sessionId);
2453
+ if (!desc) {
2454
+ return {
2455
+ content: [
2456
+ { type: "text", text: `get_agent_session_output: no session "${input2.sessionId}"` }
2457
+ ],
2458
+ isError: true
2459
+ };
2460
+ }
2461
+ const limit = input2.lastN ?? 80;
2462
+ const lines = [];
2463
+ const unsub = registry.attach(input2.sessionId, (line, _stream) => {
2464
+ lines.push(line);
2465
+ });
2466
+ if (unsub) unsub();
2467
+ const tail = lines.slice(-limit);
2468
+ return {
2469
+ content: [
2470
+ {
2471
+ type: "text",
2472
+ text: JSON.stringify(
2473
+ {
2474
+ sessionId: input2.sessionId,
2475
+ status: desc.status,
2476
+ lastOutputAt: desc.lastOutputAt,
2477
+ lines: tail
2478
+ },
2479
+ null,
2480
+ 2
2481
+ )
2482
+ }
2483
+ ]
2484
+ };
2485
+ }
2486
+ );
2487
+ server.tool(
2488
+ "list_adapters",
2489
+ "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.",
2490
+ {},
2491
+ async () => {
2492
+ if (!listAgentAdapters) {
2493
+ return {
2494
+ content: [
2495
+ {
2496
+ type: "text",
2497
+ text: "list_adapters is not enabled \u2014 the daemon was started without an adapter lister. Wire `@agentproto/cli`'s `listInstalledAdapters` via `createGateway({ listAgentAdapters })`."
2498
+ }
2499
+ ],
2500
+ isError: true
2501
+ };
2502
+ }
2503
+ try {
2504
+ const adapters = await listAgentAdapters();
2505
+ return {
2506
+ content: [{ type: "text", text: JSON.stringify({ adapters }, null, 2) }]
2507
+ };
2508
+ } catch (err) {
2509
+ return {
2510
+ content: [
2511
+ {
2512
+ type: "text",
2513
+ text: `list_adapters failed: ${err instanceof Error ? err.message : String(err)}`
2514
+ }
2515
+ ],
2516
+ isError: true
2517
+ };
2518
+ }
2519
+ }
2520
+ );
2521
+ server.tool(
2522
+ "list_discovered_mcps",
2523
+ "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.",
2524
+ {},
2525
+ async () => {
2526
+ try {
2527
+ const mcps = await discoverMcps();
2528
+ return {
2529
+ content: [{ type: "text", text: JSON.stringify({ mcps }, null, 2) }]
2530
+ };
2531
+ } catch (err) {
2532
+ return {
2533
+ content: [
2534
+ {
2535
+ type: "text",
2536
+ text: `list_discovered_mcps failed: ${err instanceof Error ? err.message : String(err)}`
2537
+ }
2538
+ ],
2539
+ isError: true
2540
+ };
2541
+ }
2542
+ }
2543
+ );
2544
+ server.tool(
2545
+ "list_imported_mcps",
2546
+ "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.",
2547
+ {},
2548
+ async () => {
2549
+ try {
2550
+ const config = await loadImportedMcps();
2551
+ return {
2552
+ content: [
2553
+ { type: "text", text: JSON.stringify(config, null, 2) }
2554
+ ]
2555
+ };
2556
+ } catch (err) {
2557
+ return {
2558
+ content: [
2559
+ {
2560
+ type: "text",
2561
+ text: `list_imported_mcps failed: ${err instanceof Error ? err.message : String(err)}`
2562
+ }
2563
+ ],
2564
+ isError: true
2565
+ };
2566
+ }
2567
+ }
2568
+ );
2569
+ server.tool(
2570
+ "import_mcp",
2571
+ "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.",
2572
+ {
2573
+ sourceMcpId: z.string().min(1).describe(
2574
+ "The discovered MCP id from `list_discovered_mcps` (e.g. 'claude-code:project:/path:chrome-devtools')."
2575
+ ),
2576
+ alias: z.string().optional().describe(
2577
+ "Optional friendly name to display. Defaults to the source MCP's name."
2578
+ )
2579
+ },
2580
+ async (input2) => {
2581
+ try {
2582
+ const discovered = await discoverMcps();
2583
+ const snapshot = discovered.find((d) => d.id === input2.sourceMcpId);
2584
+ if (!snapshot) {
2585
+ return {
2586
+ content: [
2587
+ {
2588
+ type: "text",
2589
+ text: `import_mcp: discovered MCP "${input2.sourceMcpId}" not found. Re-run list_discovered_mcps to get current ids.`
2590
+ }
2591
+ ],
2592
+ isError: true
2593
+ };
2594
+ }
2595
+ const cfg = await loadImportedMcps();
2596
+ const next = addImport(cfg, {
2597
+ snapshot,
2598
+ ...input2.alias ? { alias: input2.alias } : {}
2599
+ });
2600
+ await saveImportedMcps(next);
2601
+ const entry = next.imports.find((e) => e.id === snapshot.id);
2602
+ return {
2603
+ content: [{ type: "text", text: JSON.stringify(entry, null, 2) }]
2604
+ };
2605
+ } catch (err) {
2606
+ return {
2607
+ content: [
2608
+ {
2609
+ type: "text",
2610
+ text: `import_mcp failed: ${err instanceof Error ? err.message : String(err)}`
2611
+ }
2612
+ ],
2613
+ isError: true
2614
+ };
2615
+ }
2616
+ }
2617
+ );
2618
+ server.tool(
2619
+ "remove_imported_mcp",
2620
+ "Remove a previously-imported MCP from the daemon's curated set. Use when the user no longer wants the operator referencing it.",
2621
+ {
2622
+ id: z.string().min(1).describe(
2623
+ "The imported MCP id (matches the discovered MCP id at import time)."
2624
+ )
2625
+ },
2626
+ async (input2) => {
2627
+ try {
2628
+ const cfg = await loadImportedMcps();
2629
+ if (!cfg.imports.some((e) => e.id === input2.id)) {
2630
+ return {
2631
+ content: [
2632
+ {
2633
+ type: "text",
2634
+ text: `remove_imported_mcp: id "${input2.id}" not in imports. Use list_imported_mcps to see current entries.`
2635
+ }
2636
+ ],
2637
+ isError: true
2638
+ };
2639
+ }
2640
+ await saveImportedMcps(removeImport(cfg, input2.id));
2641
+ return {
2642
+ content: [
2643
+ { type: "text", text: JSON.stringify({ ok: true, id: input2.id }, null, 2) }
2644
+ ]
2645
+ };
2646
+ } catch (err) {
2647
+ return {
2648
+ content: [
2649
+ {
2650
+ type: "text",
2651
+ text: `remove_imported_mcp failed: ${err instanceof Error ? err.message : String(err)}`
2652
+ }
2653
+ ],
2654
+ isError: true
2655
+ };
2656
+ }
2657
+ }
2658
+ );
2659
+ server.tool(
2660
+ "mcp_imported_status",
2661
+ "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`.",
2662
+ {},
2663
+ async () => {
2664
+ if (!mcpProxy) {
2665
+ return {
2666
+ content: [
2667
+ {
2668
+ type: "text",
2669
+ text: "mcp_imported_status is not enabled \u2014 daemon was started without an MCP proxy. The host must wire `mcpProxy` in createGateway."
2670
+ }
2671
+ ],
2672
+ isError: true
2673
+ };
2674
+ }
2675
+ try {
2676
+ const aliases = await mcpProxy.listAliases();
2677
+ return {
2678
+ content: [
2679
+ { type: "text", text: JSON.stringify({ imports: aliases }, null, 2) }
2680
+ ]
2681
+ };
2682
+ } catch (err) {
2683
+ return {
2684
+ content: [
2685
+ {
2686
+ type: "text",
2687
+ text: `mcp_imported_status failed: ${err instanceof Error ? err.message : String(err)}`
2688
+ }
2689
+ ],
2690
+ isError: true
2691
+ };
2692
+ }
2693
+ }
2694
+ );
2695
+ server.tool(
2696
+ "mcp_imported_list_tools",
2697
+ "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.",
2698
+ {
2699
+ alias: z.string().min(1).describe(
2700
+ "Alias from `list_imported_mcps` / `mcp_imported_status` (typically the original MCP name, e.g. 'chrome-devtools')."
2701
+ )
2702
+ },
2703
+ async (input2) => {
2704
+ if (!mcpProxy) {
2705
+ return {
2706
+ content: [
2707
+ {
2708
+ type: "text",
2709
+ text: "mcp_imported_list_tools is not enabled \u2014 see mcp_imported_status."
2710
+ }
2711
+ ],
2712
+ isError: true
2713
+ };
2714
+ }
2715
+ const out = await mcpProxy.listTools(input2.alias);
2716
+ if (!out.ok) {
2717
+ return {
2718
+ content: [
2719
+ {
2720
+ type: "text",
2721
+ text: `mcp_imported_list_tools "${input2.alias}": ${out.error}`
2722
+ }
2723
+ ],
2724
+ isError: true
2725
+ };
2726
+ }
2727
+ return {
2728
+ content: [
2729
+ { type: "text", text: JSON.stringify({ alias: input2.alias, tools: out.tools }, null, 2) }
2730
+ ]
2731
+ };
2732
+ }
2733
+ );
2734
+ server.tool(
2735
+ "mcp_imported_call",
2736
+ "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.",
2737
+ {
2738
+ alias: z.string().min(1).describe("Imported MCP alias."),
2739
+ toolName: z.string().min(1).describe(
2740
+ "Tool name as it appears in `mcp_imported_list_tools` (NOT a namespaced version \u2014 pass the upstream's own name)."
2741
+ ),
2742
+ args: z.record(z.string(), z.unknown()).optional().describe(
2743
+ "Tool arguments as a JSON object. Schema is the upstream's \u2014 the proxy doesn't validate, only forwards. Default: empty object."
2744
+ )
2745
+ },
2746
+ async (input2) => {
2747
+ if (!mcpProxy) {
2748
+ return {
2749
+ content: [
2750
+ {
2751
+ type: "text",
2752
+ text: "mcp_imported_call is not enabled \u2014 see mcp_imported_status."
2753
+ }
2754
+ ],
2755
+ isError: true
2756
+ };
2757
+ }
2758
+ const out = await mcpProxy.callTool(
2759
+ input2.alias,
2760
+ input2.toolName,
2761
+ input2.args ?? {}
2762
+ );
2763
+ if (!out.ok) {
2764
+ return {
2765
+ content: [
2766
+ {
2767
+ type: "text",
2768
+ text: `mcp_imported_call "${input2.alias}".${input2.toolName}: ${out.error}`
2769
+ }
2770
+ ],
2771
+ isError: true
2772
+ };
2773
+ }
2774
+ return out.result;
2775
+ }
2776
+ );
2777
+ server.tool(
2778
+ "kill_agent_session",
2779
+ "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.",
2780
+ {
2781
+ sessionId: z.string().describe("Session id.")
2782
+ },
2783
+ async (input2) => {
2784
+ const ok = registry.kill(input2.sessionId);
2785
+ return {
2786
+ content: [
2787
+ {
2788
+ type: "text",
2789
+ text: JSON.stringify({ ok, sessionId: input2.sessionId }, null, 2)
2790
+ }
2791
+ ]
2792
+ };
2793
+ }
2794
+ );
2795
+ }
2796
+ function startHeartbeat(opts) {
2797
+ const filename = opts.filename ?? "HEARTBEAT.md";
2798
+ const path = join(opts.workspace, filename);
2799
+ const now = opts.now ?? (() => /* @__PURE__ */ new Date());
2800
+ let timer = null;
2801
+ let currentEveryMs = null;
2802
+ let firing = false;
2803
+ async function load() {
2804
+ if (!existsSync(path)) return null;
2805
+ let source;
2806
+ try {
2807
+ source = await readFile(path, "utf8");
2808
+ } catch {
2809
+ return null;
2810
+ }
2811
+ const parsed = matter(source);
2812
+ const fm = parsed.data;
2813
+ const agent = typeof fm.agent === "string" ? fm.agent : null;
2814
+ if (!agent) return null;
2815
+ const enabled = fm.enabled === void 0 ? true : Boolean(fm.enabled);
2816
+ const everyRaw = typeof fm.every === "string" ? fm.every : "60s";
2817
+ const everyMs = parseDuration(everyRaw);
2818
+ if (!everyMs) return null;
2819
+ const conversationTemplate = typeof fm.conversation === "string" ? fm.conversation : "heartbeat-{date}";
2820
+ return {
2821
+ agent,
2822
+ everyMs,
2823
+ enabled,
2824
+ conversationTemplate,
2825
+ body: parsed.content.trim()
2826
+ };
2827
+ }
2828
+ async function tick() {
2829
+ if (firing) return;
2830
+ if (opts.shouldFire && !opts.shouldFire()) return;
2831
+ firing = true;
2832
+ let agentId;
2833
+ try {
2834
+ const hb = await load();
2835
+ if (!hb || !hb.enabled) return;
2836
+ agentId = hb.agent;
2837
+ const agent = await opts.buildAgent(hb.agent);
2838
+ if (!agent) {
2839
+ opts.events.emit({
2840
+ type: "heartbeat-error",
2841
+ at: now().toISOString(),
2842
+ agent: hb.agent,
2843
+ error: `buildAgent('${hb.agent}') returned null`
2844
+ });
2845
+ return;
2846
+ }
2847
+ if (!hb.body) {
2848
+ opts.events.emit({
2849
+ type: "heartbeat-error",
2850
+ at: now().toISOString(),
2851
+ agent: hb.agent,
2852
+ error: "HEARTBEAT.md body is empty"
2853
+ });
2854
+ return;
2855
+ }
2856
+ const conversationId = expandTemplate(hb.conversationTemplate, now());
2857
+ await opts.conversations.open(conversationId, { agent: hb.agent });
2858
+ const startedAt = now();
2859
+ await opts.conversations.appendTurn(
2860
+ conversationId,
2861
+ "user",
2862
+ hb.body,
2863
+ { at: startedAt.toISOString(), attribution: "heartbeat" }
2864
+ );
2865
+ opts.events.emit({
2866
+ type: "conv-turn-appended",
2867
+ at: startedAt.toISOString(),
2868
+ conversationId,
2869
+ role: "user",
2870
+ contentPreview: preview(hb.body)
2871
+ });
2872
+ const t0 = Date.now();
2873
+ const reply = await agent.generate(hb.body);
2874
+ const durationMs = Date.now() - t0;
2875
+ const finishedAt = now();
2876
+ await opts.conversations.appendTurn(
2877
+ conversationId,
2878
+ "assistant",
2879
+ reply.text,
2880
+ { at: finishedAt.toISOString(), attribution: hb.agent }
2881
+ );
2882
+ opts.events.emit({
2883
+ type: "conv-turn-appended",
2884
+ at: finishedAt.toISOString(),
2885
+ conversationId,
2886
+ role: "assistant",
2887
+ contentPreview: preview(reply.text)
2888
+ });
2889
+ opts.events.emit({
2890
+ type: "heartbeat-fired",
2891
+ at: finishedAt.toISOString(),
2892
+ agent: hb.agent,
2893
+ conversationId,
2894
+ prompt: hb.body,
2895
+ reply: reply.text,
2896
+ durationMs
2897
+ });
2898
+ if (currentEveryMs !== null && currentEveryMs !== hb.everyMs) {
2899
+ reschedule(hb.everyMs);
2900
+ }
2901
+ } catch (err) {
2902
+ opts.events.emit({
2903
+ type: "heartbeat-error",
2904
+ at: now().toISOString(),
2905
+ agent: agentId,
2906
+ error: err instanceof Error ? err.message : String(err)
2907
+ });
2908
+ } finally {
2909
+ firing = false;
2910
+ }
2911
+ }
2912
+ function reschedule(everyMs) {
2913
+ if (timer) clearInterval(timer);
2914
+ currentEveryMs = everyMs;
2915
+ timer = setInterval(() => {
2916
+ void tick();
2917
+ }, everyMs);
2918
+ }
2919
+ return {
2920
+ start() {
2921
+ if (timer) return;
2922
+ void load().then((hb) => {
2923
+ const everyMs = hb?.everyMs ?? 6e4;
2924
+ reschedule(everyMs);
2925
+ });
2926
+ },
2927
+ stop() {
2928
+ if (timer) {
2929
+ clearInterval(timer);
2930
+ timer = null;
2931
+ currentEveryMs = null;
2932
+ }
2933
+ },
2934
+ async fireNow() {
2935
+ await tick();
2936
+ }
2937
+ };
2938
+ }
2939
+ var DURATION_RE = /^(\d+)\s*(s|m|h|d)$/;
2940
+ function parseDuration(input2) {
2941
+ const trimmed = input2.trim();
2942
+ const match = trimmed.match(DURATION_RE);
2943
+ if (!match) return null;
2944
+ const n = Number(match[1]);
2945
+ if (!Number.isFinite(n) || n <= 0) return null;
2946
+ switch (match[2]) {
2947
+ case "s":
2948
+ return n * 1e3;
2949
+ case "m":
2950
+ return n * 6e4;
2951
+ case "h":
2952
+ return n * 36e5;
2953
+ case "d":
2954
+ return n * 864e5;
2955
+ default:
2956
+ return null;
2957
+ }
2958
+ }
2959
+ function expandTemplate(template, when) {
2960
+ const date = when.toISOString().slice(0, 10);
2961
+ return template.replace(/\{date\}/g, date);
2962
+ }
2963
+ function preview(text3, max = 120) {
2964
+ const flat = text3.replace(/\s+/g, " ").trim();
2965
+ return flat.length > max ? `${flat.slice(0, max - 1)}\u2026` : flat;
2966
+ }
2967
+ async function startHttpServer(opts) {
2968
+ const startedAt = Date.now();
2969
+ const authSource = opts.auth ?? { mode: "none" };
2970
+ const readAuth = () => typeof authSource === "function" ? authSource() : authSource;
2971
+ function isLoopback(req) {
2972
+ const addr = req.socket.remoteAddress ?? "";
2973
+ const isLocalAddr = addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
2974
+ if (!isLocalAddr) return false;
2975
+ if (req.headers["x-forwarded-for"]) return false;
2976
+ return true;
2977
+ }
2978
+ function authorize(req, res) {
2979
+ const auth = readAuth();
2980
+ if (auth.mode === "none") return true;
2981
+ if (isLoopback(req)) return true;
2982
+ const header = req.headers.authorization;
2983
+ const expected = `Bearer ${auth.token}`;
2984
+ if (header !== expected) {
2985
+ res.writeHead(401, { "content-type": "application/json" });
2986
+ res.end(JSON.stringify({ error: "unauthorized" }));
2987
+ return false;
2988
+ }
2989
+ return true;
2990
+ }
2991
+ async function handleMcp(req, res) {
2992
+ if (!authorize(req, res)) return;
2993
+ const server2 = await opts.mcpServerFactory();
2994
+ const transport = new StreamableHTTPServerTransport({
2995
+ sessionIdGenerator: void 0
2996
+ });
2997
+ transport.onerror = (err) => {
2998
+ console.error("[mcp transport.onerror]", err);
2999
+ };
3000
+ res.on("close", () => {
3001
+ void transport.close();
3002
+ void server2.close();
3003
+ });
3004
+ try {
3005
+ await server2.connect(transport);
3006
+ await transport.handleRequest(req, res);
3007
+ } catch (err) {
3008
+ console.error("[mcp] handleRequest threw:", err);
3009
+ if (!res.headersSent) {
3010
+ res.writeHead(500, { "content-type": "application/json" });
3011
+ res.end(
3012
+ JSON.stringify({
3013
+ error: "mcp_transport_failed",
3014
+ message: err instanceof Error ? err.message : String(err)
3015
+ })
3016
+ );
3017
+ }
3018
+ }
3019
+ }
3020
+ function handleHealth(_req, res) {
3021
+ res.writeHead(200, { "content-type": "application/json" });
3022
+ res.end(
3023
+ JSON.stringify({
3024
+ status: "ok",
3025
+ workspace: opts.meta.workspace,
3026
+ registered: opts.meta.registered,
3027
+ uptimeMs: Date.now() - startedAt
3028
+ })
3029
+ );
3030
+ }
3031
+ function handleEvents(req, res) {
3032
+ if (!authorize(req, res)) return;
3033
+ res.writeHead(200, {
3034
+ "content-type": "text/event-stream",
3035
+ "cache-control": "no-cache, no-transform",
3036
+ connection: "keep-alive"
3037
+ });
3038
+ res.write(`: connected
3039
+
3040
+ `);
3041
+ const off = opts.events.onAny((ev) => {
3042
+ res.write(`data: ${JSON.stringify(ev)}
3043
+
3044
+ `);
3045
+ });
3046
+ req.on("close", off);
3047
+ }
3048
+ async function handleListConversations(req, res) {
3049
+ if (!authorize(req, res)) return;
3050
+ const list = await opts.conversations.list();
3051
+ res.writeHead(200, { "content-type": "application/json" });
3052
+ res.end(JSON.stringify(list));
3053
+ }
3054
+ async function handleGetConversation(req, res, id) {
3055
+ if (!authorize(req, res)) return;
3056
+ try {
3057
+ const data = await opts.conversations.read(id);
3058
+ res.writeHead(200, { "content-type": "application/json" });
3059
+ res.end(JSON.stringify(data));
3060
+ } catch (err) {
3061
+ res.writeHead(404, { "content-type": "application/json" });
3062
+ res.end(
3063
+ JSON.stringify({
3064
+ error: "not_found",
3065
+ message: err instanceof Error ? err.message : String(err)
3066
+ })
3067
+ );
3068
+ }
3069
+ }
3070
+ async function handleHeartbeatTick(req, res) {
3071
+ if (!authorize(req, res)) return;
3072
+ await opts.heartbeat.fireNow();
3073
+ res.writeHead(202, { "content-type": "application/json" });
3074
+ res.end(JSON.stringify({ status: "fired" }));
3075
+ }
3076
+ function applyCors(req, res) {
3077
+ const origin = req.headers.origin ?? "*";
3078
+ res.setHeader("Access-Control-Allow-Origin", origin);
3079
+ res.setHeader("Vary", "Origin");
3080
+ res.setHeader("Access-Control-Allow-Credentials", "true");
3081
+ res.setHeader(
3082
+ "Access-Control-Allow-Headers",
3083
+ req.headers["access-control-request-headers"] ?? "authorization,content-type,mcp-session-id"
3084
+ );
3085
+ res.setHeader(
3086
+ "Access-Control-Allow-Methods",
3087
+ "GET,POST,PUT,PATCH,DELETE,OPTIONS"
3088
+ );
3089
+ }
3090
+ const server = createServer((req, res) => {
3091
+ void (async () => {
3092
+ try {
3093
+ const url = req.url ?? "/";
3094
+ const path = url.split("?")[0] ?? "/";
3095
+ if (req.headers["x-forwarded-for"]) {
3096
+ opts.events.emit({
3097
+ type: "remote-log",
3098
+ at: (/* @__PURE__ */ new Date()).toISOString(),
3099
+ line: `[http-in] ${req.method} ${url} host=${req.headers.host ?? "?"} xff=${String(req.headers["x-forwarded-for"])}`
3100
+ });
3101
+ }
3102
+ applyCors(req, res);
3103
+ if (req.method === "OPTIONS") {
3104
+ res.writeHead(204);
3105
+ res.end();
3106
+ return;
3107
+ }
3108
+ if (path === "/health" && req.method === "GET") {
3109
+ handleHealth(req, res);
3110
+ return;
3111
+ }
3112
+ if (path === "/events" && req.method === "GET") {
3113
+ handleEvents(req, res);
3114
+ return;
3115
+ }
3116
+ if (path === "/mcp") {
3117
+ await handleMcp(req, res);
3118
+ return;
3119
+ }
3120
+ if (path === "/conversations" && req.method === "GET") {
3121
+ await handleListConversations(req, res);
3122
+ return;
3123
+ }
3124
+ if (path.startsWith("/conversations/") && req.method === "GET") {
3125
+ const id = path.slice("/conversations/".length);
3126
+ await handleGetConversation(req, res, id);
3127
+ return;
3128
+ }
3129
+ if (path === "/heartbeat/tick" && req.method === "POST") {
3130
+ await handleHeartbeatTick(req, res);
3131
+ return;
3132
+ }
3133
+ if (opts.sessions && path.startsWith("/sessions")) {
3134
+ const handled = await handleSessions(
3135
+ req,
3136
+ res,
3137
+ path,
3138
+ opts.sessions,
3139
+ opts.resolveAgentAdapter
3140
+ );
3141
+ if (handled) return;
3142
+ }
3143
+ if (path === "/workspaces" && req.method === "GET") {
3144
+ try {
3145
+ const config = await loadWorkspacesConfig();
3146
+ res.writeHead(200, { "content-type": "application/json" });
3147
+ res.end(JSON.stringify(config));
3148
+ } catch (err) {
3149
+ res.writeHead(500, { "content-type": "application/json" });
3150
+ res.end(
3151
+ JSON.stringify({
3152
+ error: "workspaces_load_failed",
3153
+ message: err instanceof Error ? err.message : String(err)
3154
+ })
3155
+ );
3156
+ }
3157
+ return;
3158
+ }
3159
+ if (path === "/mcps/imports" && req.method === "GET") {
3160
+ const config = await loadImportedMcps();
3161
+ res.writeHead(200, { "content-type": "application/json" });
3162
+ res.end(JSON.stringify(config));
3163
+ return;
3164
+ }
3165
+ if (path === "/mcps/imports" && req.method === "POST") {
3166
+ const body = await readJsonBody(req);
3167
+ if (!body || typeof body.sourceMcpId !== "string") {
3168
+ res.writeHead(400, { "content-type": "application/json" });
3169
+ res.end(JSON.stringify({ error: "missing_sourceMcpId" }));
3170
+ return;
3171
+ }
3172
+ const discovered = await discoverMcps();
3173
+ const snapshot = discovered.find((d) => d.id === body.sourceMcpId);
3174
+ if (!snapshot) {
3175
+ res.writeHead(404, { "content-type": "application/json" });
3176
+ res.end(
3177
+ JSON.stringify({
3178
+ error: "discovered_mcp_not_found",
3179
+ sourceMcpId: body.sourceMcpId
3180
+ })
3181
+ );
3182
+ return;
3183
+ }
3184
+ const cfg = await loadImportedMcps();
3185
+ const next = addImport(cfg, {
3186
+ snapshot,
3187
+ ...body.alias ? { alias: body.alias } : {}
3188
+ });
3189
+ await saveImportedMcps(next);
3190
+ res.writeHead(201, { "content-type": "application/json" });
3191
+ res.end(JSON.stringify(next.imports.find((e) => e.id === snapshot.id)));
3192
+ return;
3193
+ }
3194
+ const importMatch = path.match(/^\/mcps\/imports\/(.+)$/);
3195
+ if (importMatch && req.method === "DELETE") {
3196
+ const id = decodeURIComponent(importMatch[1] ?? "");
3197
+ const cfg = await loadImportedMcps();
3198
+ if (!cfg.imports.some((e) => e.id === id)) {
3199
+ res.writeHead(404, { "content-type": "application/json" });
3200
+ res.end(JSON.stringify({ error: "import_not_found", id }));
3201
+ return;
3202
+ }
3203
+ await saveImportedMcps(removeImport(cfg, id));
3204
+ res.writeHead(200, { "content-type": "application/json" });
3205
+ res.end(JSON.stringify({ ok: true, id }));
3206
+ return;
3207
+ }
3208
+ if (path === "/mcps/discovered" && req.method === "GET") {
3209
+ try {
3210
+ const mcps = await discoverMcps();
3211
+ res.writeHead(200, { "content-type": "application/json" });
3212
+ res.end(JSON.stringify({ mcps }));
3213
+ } catch (err) {
3214
+ res.writeHead(500, { "content-type": "application/json" });
3215
+ res.end(
3216
+ JSON.stringify({
3217
+ error: "discovery_failed",
3218
+ message: err instanceof Error ? err.message : String(err)
3219
+ })
3220
+ );
3221
+ }
3222
+ return;
3223
+ }
3224
+ if (path === "/mcps/proxy/status" && req.method === "GET") {
3225
+ if (!opts.mcpProxy) {
3226
+ res.writeHead(501, { "content-type": "application/json" });
3227
+ res.end(JSON.stringify({ error: "mcp_proxy_not_configured" }));
3228
+ return;
3229
+ }
3230
+ try {
3231
+ const imports = await opts.mcpProxy.listAliases();
3232
+ res.writeHead(200, { "content-type": "application/json" });
3233
+ res.end(JSON.stringify({ imports }));
3234
+ } catch (err) {
3235
+ res.writeHead(500, { "content-type": "application/json" });
3236
+ res.end(
3237
+ JSON.stringify({
3238
+ error: "proxy_status_failed",
3239
+ message: err instanceof Error ? err.message : String(err)
3240
+ })
3241
+ );
3242
+ }
3243
+ return;
3244
+ }
3245
+ const proxyToolsMatch = path.match(/^\/mcps\/proxy\/tools\/(.+)$/);
3246
+ if (proxyToolsMatch && req.method === "GET") {
3247
+ if (!opts.mcpProxy) {
3248
+ res.writeHead(501, { "content-type": "application/json" });
3249
+ res.end(JSON.stringify({ error: "mcp_proxy_not_configured" }));
3250
+ return;
3251
+ }
3252
+ const alias = decodeURIComponent(proxyToolsMatch[1] ?? "");
3253
+ const out = await opts.mcpProxy.listTools(alias);
3254
+ res.writeHead(out.ok ? 200 : 502, {
3255
+ "content-type": "application/json"
3256
+ });
3257
+ res.end(JSON.stringify(out));
3258
+ return;
3259
+ }
3260
+ if (path === "/mcps/proxy/call" && req.method === "POST") {
3261
+ if (!opts.mcpProxy) {
3262
+ res.writeHead(501, { "content-type": "application/json" });
3263
+ res.end(JSON.stringify({ error: "mcp_proxy_not_configured" }));
3264
+ return;
3265
+ }
3266
+ const body = await readJsonBody(req);
3267
+ if (!body || typeof body.alias !== "string" || typeof body.toolName !== "string") {
3268
+ res.writeHead(400, { "content-type": "application/json" });
3269
+ res.end(
3270
+ JSON.stringify({
3271
+ error: "invalid_body",
3272
+ message: "expected { alias, toolName, args? }"
3273
+ })
3274
+ );
3275
+ return;
3276
+ }
3277
+ const result = await opts.mcpProxy.callTool(
3278
+ body.alias,
3279
+ body.toolName,
3280
+ body.args ?? {}
3281
+ );
3282
+ res.writeHead(result.ok ? 200 : 502, {
3283
+ "content-type": "application/json"
3284
+ });
3285
+ res.end(JSON.stringify(result));
3286
+ return;
3287
+ }
3288
+ if (path === "/adapters" && req.method === "GET") {
3289
+ if (!opts.listAgentAdapters) {
3290
+ res.writeHead(501, { "content-type": "application/json" });
3291
+ res.end(
3292
+ JSON.stringify({
3293
+ error: "lister_not_configured",
3294
+ message: "Daemon was started without `listAgentAdapters` \u2014 see @agentproto/cli's `listInstalledAdapters` for the canonical impl."
3295
+ })
3296
+ );
3297
+ return;
3298
+ }
3299
+ try {
3300
+ const adapters = await opts.listAgentAdapters();
3301
+ res.writeHead(200, { "content-type": "application/json" });
3302
+ res.end(JSON.stringify({ adapters }));
3303
+ } catch (err) {
3304
+ res.writeHead(500, { "content-type": "application/json" });
3305
+ res.end(
3306
+ JSON.stringify({
3307
+ error: "list_failed",
3308
+ message: err instanceof Error ? err.message : String(err)
3309
+ })
3310
+ );
3311
+ }
3312
+ return;
3313
+ }
3314
+ res.writeHead(404, { "content-type": "application/json" });
3315
+ res.end(JSON.stringify({ error: "not_found", path }));
3316
+ } catch (err) {
3317
+ if (!res.headersSent) {
3318
+ res.writeHead(500, { "content-type": "application/json" });
3319
+ res.end(
3320
+ JSON.stringify({
3321
+ error: "internal_error",
3322
+ message: err instanceof Error ? err.message : String(err)
3323
+ })
3324
+ );
3325
+ }
3326
+ }
3327
+ })();
3328
+ });
3329
+ const bind = opts.bind ?? "127.0.0.1";
3330
+ await new Promise((resolve7, reject) => {
3331
+ server.once("error", reject);
3332
+ server.listen(opts.port, bind, () => resolve7());
3333
+ });
3334
+ return {
3335
+ url: `http://${bind}:${opts.port}`,
3336
+ async stop() {
3337
+ await new Promise((resolve7) => server.close(() => resolve7()));
3338
+ }
3339
+ };
3340
+ }
3341
+ async function handleSessions(req, res, path, registry, resolveAgentAdapter) {
3342
+ const json = (status, body) => {
3343
+ res.writeHead(status, { "content-type": "application/json" });
3344
+ res.end(JSON.stringify(body));
3345
+ };
3346
+ if (path === "/sessions" && req.method === "GET") {
3347
+ json(200, { sessions: registry.list() });
3348
+ return true;
3349
+ }
3350
+ if (path === "/sessions/agent" && req.method === "POST") {
3351
+ if (!resolveAgentAdapter) {
3352
+ json(501, {
3353
+ error: "agent_resolver_not_configured",
3354
+ message: "POST /sessions/agent needs the host to inject `resolveAgentAdapter` (e.g. via @agentproto/cli's resolveAdapter shim)."
3355
+ });
3356
+ return true;
3357
+ }
3358
+ const body = await readJsonBody(req);
3359
+ if (!body || typeof body !== "object") {
3360
+ json(400, { error: "invalid_body" });
3361
+ return true;
3362
+ }
3363
+ const b = body;
3364
+ const adapter = typeof b.adapter === "string" ? b.adapter : "";
3365
+ if (!adapter) {
3366
+ json(400, { error: "missing_adapter" });
3367
+ return true;
3368
+ }
3369
+ let cwd = typeof b.cwd === "string" && b.cwd.length > 0 ? b.cwd : null;
3370
+ let workspaceSlug = typeof b.workspaceSlug === "string" ? b.workspaceSlug : "";
3371
+ if (!cwd) {
3372
+ try {
3373
+ const config = await loadWorkspacesConfig();
3374
+ const ws = workspaceSlug ? findWorkspace(config, workspaceSlug) : getActiveWorkspace(config);
3375
+ if (ws) {
3376
+ cwd = ws.path;
3377
+ workspaceSlug = ws.slug;
3378
+ }
3379
+ } catch {
3380
+ }
3381
+ }
3382
+ if (!cwd) {
3383
+ cwd = process.cwd();
3384
+ console.warn(
3385
+ `[/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}`
3386
+ );
3387
+ }
3388
+ if (!workspaceSlug) workspaceSlug = "default";
3389
+ const resolved = await resolveAgentAdapter(adapter);
3390
+ if (!resolved) {
3391
+ json(404, { error: "adapter_not_found", adapter });
3392
+ return true;
3393
+ }
3394
+ try {
3395
+ const agentSession = await resolved.startSession({ cwd });
3396
+ const desc = registry.spawnAgent({
3397
+ workspaceSlug,
3398
+ cwd,
3399
+ agentSession,
3400
+ adapterSlug: adapter,
3401
+ ...typeof b.prompt === "string" ? { initialPrompt: b.prompt } : {},
3402
+ ...typeof b.label === "string" ? { label: b.label } : {},
3403
+ ...resolved.commandPreview ? { commandPreview: resolved.commandPreview } : {}
3404
+ });
3405
+ json(201, desc);
3406
+ } catch (err) {
3407
+ json(500, {
3408
+ error: "agent_spawn_failed",
3409
+ message: err instanceof Error ? err.message : String(err)
3410
+ });
3411
+ }
3412
+ return true;
3413
+ }
3414
+ const promptMatch = path.match(/^\/sessions\/([^/]+)\/prompt$/);
3415
+ if (promptMatch && req.method === "POST") {
3416
+ const id2 = promptMatch[1];
3417
+ if (!id2) return false;
3418
+ const body = await readJsonBody(req);
3419
+ const prompt = body?.prompt;
3420
+ if (typeof prompt !== "string") {
3421
+ json(400, { error: "missing_prompt" });
3422
+ return true;
3423
+ }
3424
+ const reqUrl = req.url ?? "";
3425
+ const queryString = reqUrl.includes("?") ? reqUrl.slice(reqUrl.indexOf("?") + 1) : "";
3426
+ const wait = new URLSearchParams(queryString).get("wait");
3427
+ const fireAndForget = wait === "false" || wait === "0";
3428
+ try {
3429
+ if (fireAndForget) {
3430
+ registry.enqueuePrompt(id2, prompt);
3431
+ json(202, { ok: true, id: id2, queued: true });
3432
+ } else {
3433
+ await registry.sendPrompt(id2, prompt);
3434
+ json(200, { ok: true, id: id2 });
3435
+ }
3436
+ } catch (err) {
3437
+ const msg = err instanceof Error ? err.message : String(err);
3438
+ const status = msg.includes("no session") ? 404 : msg.includes("not an agent") ? 400 : msg.includes("mid-turn") ? 409 : 500;
3439
+ json(status, { error: "send_prompt_failed", message: msg });
3440
+ }
3441
+ return true;
3442
+ }
3443
+ if (path === "/sessions" && req.method === "POST") {
3444
+ const body = await readJsonBody(req);
3445
+ if (!body || typeof body !== "object") {
3446
+ json(400, { error: "invalid_body" });
3447
+ return true;
3448
+ }
3449
+ const b = body;
3450
+ const argv = Array.isArray(b.argv) ? b.argv.filter((v) => typeof v === "string") : [];
3451
+ if (argv.length === 0) {
3452
+ json(400, { error: "missing_argv" });
3453
+ return true;
3454
+ }
3455
+ try {
3456
+ const desc = registry.spawn({
3457
+ kind: b.kind === "terminal" || b.kind === "agent-cli" || b.kind === "command" ? b.kind : "command",
3458
+ workspaceSlug: typeof b.workspaceSlug === "string" ? b.workspaceSlug : "default",
3459
+ cwd: typeof b.cwd === "string" ? b.cwd : process.cwd(),
3460
+ argv,
3461
+ env: b.env && typeof b.env === "object" ? b.env : void 0,
3462
+ label: typeof b.label === "string" ? b.label : void 0
3463
+ });
3464
+ json(201, desc);
3465
+ } catch (err) {
3466
+ json(500, {
3467
+ error: "spawn_failed",
3468
+ message: err instanceof Error ? err.message : String(err)
3469
+ });
3470
+ }
3471
+ return true;
3472
+ }
3473
+ const idMatch = path.match(/^\/sessions\/([^/]+)(\/stream|\/kill)?$/);
3474
+ if (!idMatch) return false;
3475
+ const [, id, suffix] = idMatch;
3476
+ if (!id) return false;
3477
+ if (suffix === "/stream" && req.method === "GET") {
3478
+ const desc = registry.get(id);
3479
+ if (!desc) {
3480
+ json(404, { error: "session_not_found", id });
3481
+ return true;
3482
+ }
3483
+ res.writeHead(200, {
3484
+ "content-type": "text/event-stream",
3485
+ "cache-control": "no-cache",
3486
+ connection: "keep-alive"
3487
+ });
3488
+ const ping = setInterval(() => {
3489
+ try {
3490
+ res.write(`: keep-alive
3491
+
3492
+ `);
3493
+ } catch {
3494
+ clearInterval(ping);
3495
+ }
3496
+ }, 25e3);
3497
+ const unsub = registry.attach(id, (line, stream) => {
3498
+ try {
3499
+ res.write(
3500
+ `data: ${JSON.stringify({ line, stream })}
3501
+
3502
+ `
3503
+ );
3504
+ } catch {
3505
+ }
3506
+ });
3507
+ if (!unsub) {
3508
+ clearInterval(ping);
3509
+ res.end();
3510
+ return true;
3511
+ }
3512
+ req.once("close", () => {
3513
+ unsub();
3514
+ clearInterval(ping);
3515
+ });
3516
+ return true;
3517
+ }
3518
+ if (suffix === "/kill" && req.method === "POST") {
3519
+ const ok = registry.kill(id);
3520
+ json(ok ? 200 : 404, { ok, id });
3521
+ return true;
3522
+ }
3523
+ if (!suffix && req.method === "GET") {
3524
+ const desc = registry.get(id);
3525
+ if (!desc) {
3526
+ json(404, { error: "session_not_found", id });
3527
+ return true;
3528
+ }
3529
+ json(200, desc);
3530
+ return true;
3531
+ }
3532
+ if (!suffix && req.method === "DELETE") {
3533
+ const ok = registry.forget(id);
3534
+ json(ok ? 200 : 404, { ok, id });
3535
+ return true;
3536
+ }
3537
+ return false;
3538
+ }
3539
+ async function readJsonBody(req) {
3540
+ const chunks = [];
3541
+ for await (const chunk of req) {
3542
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
3543
+ }
3544
+ if (chunks.length === 0) return null;
3545
+ try {
3546
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
3547
+ } catch {
3548
+ return null;
3549
+ }
3550
+ }
3551
+ var SESSIONS_FILE_PATH = () => resolve(homedir(), ".agentproto", "sessions.json");
3552
+ var RECENT_LINES_CAP = 500;
3553
+ var PERSIST_DEBOUNCE_MS = 1500;
3554
+ function createSessionsRegistry(opts) {
3555
+ const persistPath = SESSIONS_FILE_PATH();
3556
+ const sessions = /* @__PURE__ */ new Map();
3557
+ let persistTimer = null;
3558
+ const schedulePersist = () => {
3559
+ if (persistTimer) clearTimeout(persistTimer);
3560
+ persistTimer = setTimeout(() => {
3561
+ void persistSnapshot();
3562
+ }, PERSIST_DEBOUNCE_MS);
3563
+ };
3564
+ const persistSnapshot = async () => {
3565
+ try {
3566
+ const snapshot = {
3567
+ savedAt: (/* @__PURE__ */ new Date()).toISOString(),
3568
+ sessions: Array.from(sessions.values()).map((s) => s.desc)
3569
+ };
3570
+ await promises.mkdir(dirname(persistPath), { recursive: true });
3571
+ await promises.writeFile(persistPath, JSON.stringify(snapshot, null, 2) + "\n");
3572
+ } catch (err) {
3573
+ console.warn(
3574
+ `[sessions] persist failed: ${err instanceof Error ? err.message : String(err)}`
3575
+ );
3576
+ }
3577
+ };
3578
+ const appendLine = (rt, line, stream) => {
3579
+ rt.recentLines.push(line);
3580
+ if (rt.recentLines.length > RECENT_LINES_CAP) {
3581
+ rt.recentLines.splice(0, rt.recentLines.length - RECENT_LINES_CAP);
3582
+ }
3583
+ rt.desc.lastOutputAt = (/* @__PURE__ */ new Date()).toISOString();
3584
+ rt.emitter.emit("line", { line, stream });
3585
+ };
3586
+ const projectEvent = (rt, evt) => {
3587
+ switch (evt.kind) {
3588
+ case "text-delta":
3589
+ if (evt.text) {
3590
+ const combined = rt.textBuf + evt.text;
3591
+ const lines = combined.split(/\r?\n/);
3592
+ rt.textBuf = lines.pop() ?? "";
3593
+ for (const line of lines) appendLine(rt, line, "stdout");
3594
+ }
3595
+ break;
3596
+ case "thought":
3597
+ if (evt.text) {
3598
+ const combined = rt.thoughtBuf + evt.text;
3599
+ const lines = combined.split(/\r?\n/);
3600
+ rt.thoughtBuf = lines.pop() ?? "";
3601
+ for (const line of lines) {
3602
+ if (line.trim()) {
3603
+ appendLine(rt, `\x1B[2m[thought] ${line}\x1B[0m`, "stdout");
3604
+ }
3605
+ }
3606
+ }
3607
+ break;
3608
+ case "tool-call":
3609
+ appendLine(
3610
+ rt,
3611
+ `\x1B[36m[tool] ${evt.toolName ?? "?"}\x1B[0m`,
3612
+ "stdout"
3613
+ );
3614
+ break;
3615
+ case "tool-result":
3616
+ if (evt.isError)
3617
+ appendLine(rt, `\x1B[31m[tool-error]\x1B[0m`, "stderr");
3618
+ break;
3619
+ case "agent-prompt":
3620
+ appendLine(rt, `\x1B[33m[awaiting input]\x1B[0m`, "stdout");
3621
+ break;
3622
+ case "turn-end": {
3623
+ if (rt.thoughtBuf.trim()) {
3624
+ appendLine(
3625
+ rt,
3626
+ `\x1B[2m[thought] ${rt.thoughtBuf}\x1B[0m`,
3627
+ "stdout"
3628
+ );
3629
+ }
3630
+ rt.thoughtBuf = "";
3631
+ if (rt.textBuf) {
3632
+ appendLine(rt, rt.textBuf, "stdout");
3633
+ rt.textBuf = "";
3634
+ }
3635
+ appendLine(
3636
+ rt,
3637
+ `\x1B[2m\u2500\u2500 turn-end (${evt.reason ?? "completed"}) \u2500\u2500\x1B[0m`,
3638
+ "stdout"
3639
+ );
3640
+ break;
3641
+ }
3642
+ case "error": {
3643
+ const code = typeof evt.error?.code === "number" ? ` (code ${evt.error.code})` : "";
3644
+ appendLine(
3645
+ rt,
3646
+ `\x1B[31m[error]${code} ${evt.error?.message ?? "unknown"}\x1B[0m`,
3647
+ "stderr"
3648
+ );
3649
+ const data = evt.error?.data;
3650
+ if (data && typeof data === "object" && typeof data.stderr === "string") {
3651
+ for (const line of data.stderr.split(/\r?\n/)) {
3652
+ if (line) appendLine(rt, `\x1B[2m ${line}\x1B[0m`, "stderr");
3653
+ }
3654
+ }
3655
+ break;
3656
+ }
3657
+ }
3658
+ };
3659
+ const validateAgentTurn = (id, caller) => {
3660
+ const rt = sessions.get(id);
3661
+ if (!rt) throw new Error(`${caller}: no session "${id}"`);
3662
+ if (!rt.agentSession) {
3663
+ throw new Error(
3664
+ `${caller}: session "${id}" is not an agent session (kind=${rt.desc.kind})`
3665
+ );
3666
+ }
3667
+ if (rt.busy) {
3668
+ throw new Error(
3669
+ `${caller}: session "${id}" is mid-turn \u2014 wait for it to finish or cancel`
3670
+ );
3671
+ }
3672
+ return rt;
3673
+ };
3674
+ const runAgentTurn = async (rt, message) => {
3675
+ if (!rt.agentSession) {
3676
+ throw new Error("runAgentTurn: session has no agentSession");
3677
+ }
3678
+ rt.busy = true;
3679
+ try {
3680
+ appendLine(
3681
+ rt,
3682
+ `\x1B[2m\u2500\u2500 \u25B6 ${typeof message === "string" ? message : JSON.stringify(message)} \u2500\u2500\x1B[0m`,
3683
+ "stdout"
3684
+ );
3685
+ const wrapped = typeof message === "string" ? { type: "text", text: message } : message;
3686
+ for await (const evt of rt.agentSession.send(wrapped)) {
3687
+ projectEvent(rt, evt);
3688
+ }
3689
+ } catch (err) {
3690
+ rt.desc.status = "error";
3691
+ rt.desc.endedAt = (/* @__PURE__ */ new Date()).toISOString();
3692
+ appendLine(
3693
+ rt,
3694
+ `[turn error] ${err instanceof Error ? err.message : String(err)}`,
3695
+ "stderr"
3696
+ );
3697
+ schedulePersist();
3698
+ } finally {
3699
+ rt.busy = false;
3700
+ }
3701
+ };
3702
+ const wireOutputStreams = (rt) => {
3703
+ const onChunk = (stream) => (chunk) => {
3704
+ const text3 = typeof chunk === "string" ? chunk : chunk.toString("utf8");
3705
+ for (const line of text3.split(/\r?\n/)) {
3706
+ if (line.length > 0) appendLine(rt, line, stream);
3707
+ }
3708
+ };
3709
+ if (!rt.child) return;
3710
+ rt.child.stdout?.on("data", onChunk("stdout"));
3711
+ rt.child.stderr?.on("data", onChunk("stderr"));
3712
+ };
3713
+ return {
3714
+ spawn(input2) {
3715
+ const id = input2.id ?? `sess_${randomUUID().slice(0, 8)}`;
3716
+ if (sessions.has(id)) {
3717
+ throw new Error(`sessions.spawn: id "${id}" already in use`);
3718
+ }
3719
+ const env = { ...process.env, ...input2.env ?? {} };
3720
+ delete env.NODE_OPTIONS;
3721
+ const [bin, ...args] = input2.argv;
3722
+ if (!bin) throw new Error("sessions.spawn: argv must include a binary");
3723
+ const stdinMode = input2.keepStdin ? "pipe" : "ignore";
3724
+ const child = spawn(bin, args, {
3725
+ cwd: input2.cwd,
3726
+ env,
3727
+ stdio: [stdinMode, "pipe", "pipe"]
3728
+ });
3729
+ const desc = {
3730
+ id,
3731
+ kind: input2.kind,
3732
+ workspaceSlug: input2.workspaceSlug,
3733
+ command: [bin, ...args].map(quoteArg).join(" "),
3734
+ pid: child.pid ?? null,
3735
+ status: "starting",
3736
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
3737
+ ...input2.label ? { label: input2.label } : {}
3738
+ };
3739
+ const rt = {
3740
+ desc,
3741
+ child,
3742
+ recentLines: [],
3743
+ emitter: new EventEmitter(),
3744
+ busy: false,
3745
+ textBuf: "",
3746
+ thoughtBuf: ""
3747
+ };
3748
+ rt.emitter.setMaxListeners(50);
3749
+ sessions.set(id, rt);
3750
+ wireOutputStreams(rt);
3751
+ child.once("spawn", () => {
3752
+ desc.status = "running";
3753
+ rt.emitter.emit("status", desc.status);
3754
+ schedulePersist();
3755
+ });
3756
+ child.once("error", (err) => {
3757
+ desc.status = "error";
3758
+ desc.endedAt = (/* @__PURE__ */ new Date()).toISOString();
3759
+ appendLine(rt, `[spawn error] ${err.message}`, "stderr");
3760
+ rt.emitter.emit("status", desc.status);
3761
+ schedulePersist();
3762
+ });
3763
+ child.once("exit", (code, signal) => {
3764
+ if (signal && desc.status !== "killed") desc.status = "killed";
3765
+ else if (desc.status === "running" || desc.status === "starting") {
3766
+ desc.status = "exited";
3767
+ }
3768
+ desc.endedAt = (/* @__PURE__ */ new Date()).toISOString();
3769
+ if (typeof code === "number") desc.exitCode = code;
3770
+ rt.emitter.emit("status", desc.status);
3771
+ schedulePersist();
3772
+ });
3773
+ schedulePersist();
3774
+ return desc;
3775
+ },
3776
+ register(input2) {
3777
+ if (sessions.has(input2.id)) {
3778
+ const existing = sessions.get(input2.id);
3779
+ if (existing) return existing.desc;
3780
+ }
3781
+ const desc = {
3782
+ id: input2.id,
3783
+ kind: input2.kind ?? "command",
3784
+ workspaceSlug: input2.workspaceSlug,
3785
+ command: input2.command,
3786
+ pid: input2.child.pid ?? null,
3787
+ status: input2.child.killed ? "killed" : "running",
3788
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
3789
+ ...input2.label ? { label: input2.label } : {}
3790
+ };
3791
+ const rt = {
3792
+ desc,
3793
+ child: input2.child,
3794
+ recentLines: [],
3795
+ emitter: new EventEmitter(),
3796
+ busy: false,
3797
+ textBuf: "",
3798
+ thoughtBuf: ""
3799
+ };
3800
+ rt.emitter.setMaxListeners(50);
3801
+ sessions.set(input2.id, rt);
3802
+ wireOutputStreams(rt);
3803
+ input2.child.once("exit", (code, signal) => {
3804
+ if (signal && desc.status !== "killed") desc.status = "killed";
3805
+ else if (desc.status === "running") desc.status = "exited";
3806
+ desc.endedAt = (/* @__PURE__ */ new Date()).toISOString();
3807
+ if (typeof code === "number") desc.exitCode = code;
3808
+ rt.emitter.emit("status", desc.status);
3809
+ schedulePersist();
3810
+ });
3811
+ input2.child.once("error", (err) => {
3812
+ desc.status = "error";
3813
+ desc.endedAt = (/* @__PURE__ */ new Date()).toISOString();
3814
+ appendLine(rt, `[child error] ${err.message}`, "stderr");
3815
+ rt.emitter.emit("status", desc.status);
3816
+ schedulePersist();
3817
+ });
3818
+ schedulePersist();
3819
+ return desc;
3820
+ },
3821
+ spawnAgent(input2) {
3822
+ const id = `sess_${randomUUID().slice(0, 8)}`;
3823
+ const desc = {
3824
+ id,
3825
+ kind: "agent-cli",
3826
+ workspaceSlug: input2.workspaceSlug,
3827
+ command: input2.commandPreview ?? `${input2.adapterSlug} (agent)`,
3828
+ pid: null,
3829
+ status: "running",
3830
+ // driver already started the session
3831
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
3832
+ ...input2.label ? { label: input2.label } : {}
3833
+ };
3834
+ const rt = {
3835
+ desc,
3836
+ agentSession: input2.agentSession,
3837
+ adapterSlug: input2.adapterSlug,
3838
+ recentLines: [],
3839
+ emitter: new EventEmitter(),
3840
+ busy: false,
3841
+ textBuf: "",
3842
+ thoughtBuf: ""
3843
+ };
3844
+ rt.emitter.setMaxListeners(50);
3845
+ sessions.set(id, rt);
3846
+ appendLine(
3847
+ rt,
3848
+ `\u2500\u2500 ${input2.adapterSlug} agent session ${input2.agentSession.sessionId} (cwd ${input2.cwd}) \u2500\u2500`,
3849
+ "stdout"
3850
+ );
3851
+ schedulePersist();
3852
+ if (input2.initialPrompt) {
3853
+ void runAgentTurn(rt, input2.initialPrompt).catch((err) => {
3854
+ appendLine(
3855
+ rt,
3856
+ `[turn error] ${err instanceof Error ? err.message : String(err)}`,
3857
+ "stderr"
3858
+ );
3859
+ });
3860
+ }
3861
+ return desc;
3862
+ },
3863
+ async sendPrompt(id, message) {
3864
+ const rt = validateAgentTurn(id, "sendPrompt");
3865
+ await runAgentTurn(rt, message);
3866
+ },
3867
+ enqueuePrompt(id, message) {
3868
+ const rt = validateAgentTurn(id, "enqueuePrompt");
3869
+ void runAgentTurn(rt, message).catch(() => {
3870
+ });
3871
+ },
3872
+ list() {
3873
+ return Array.from(sessions.values()).map((s) => s.desc).sort((a, b) => b.startedAt.localeCompare(a.startedAt));
3874
+ },
3875
+ get(id) {
3876
+ return sessions.get(id)?.desc;
3877
+ },
3878
+ attach(id, onLine) {
3879
+ const rt = sessions.get(id);
3880
+ if (!rt) return null;
3881
+ for (const line of rt.recentLines) onLine(line, "stdout");
3882
+ const handler = (evt) => onLine(evt.line, evt.stream);
3883
+ rt.emitter.on("line", handler);
3884
+ return () => rt.emitter.off("line", handler);
3885
+ },
3886
+ kill(id, signal = "SIGTERM") {
3887
+ const rt = sessions.get(id);
3888
+ if (!rt) return false;
3889
+ if (rt.desc.status === "exited" || rt.desc.status === "killed" || rt.desc.status === "error") {
3890
+ return false;
3891
+ }
3892
+ rt.desc.status = "killed";
3893
+ rt.desc.endedAt = (/* @__PURE__ */ new Date()).toISOString();
3894
+ if (rt.agentSession) {
3895
+ void rt.agentSession.close().catch(() => void 0);
3896
+ }
3897
+ rt.child?.kill(signal);
3898
+ schedulePersist();
3899
+ return true;
3900
+ },
3901
+ forget(id) {
3902
+ const rt = sessions.get(id);
3903
+ if (!rt) return false;
3904
+ rt.emitter.removeAllListeners();
3905
+ sessions.delete(id);
3906
+ schedulePersist();
3907
+ return true;
3908
+ },
3909
+ shutdown() {
3910
+ if (persistTimer) clearTimeout(persistTimer);
3911
+ for (const rt of sessions.values()) {
3912
+ rt.emitter.removeAllListeners();
3913
+ if (rt.desc.status === "running" || rt.desc.status === "starting") {
3914
+ if (rt.agentSession) {
3915
+ void rt.agentSession.close().catch(() => void 0);
3916
+ }
3917
+ rt.child?.kill("SIGTERM");
3918
+ }
3919
+ }
3920
+ sessions.clear();
3921
+ }
3922
+ };
3923
+ }
3924
+ function quoteArg(arg) {
3925
+ if (arg === "") return '""';
3926
+ if (/^[a-zA-Z0-9._/=:@,+-]+$/.test(arg)) return arg;
3927
+ return `"${arg.replace(/(["\\$`])/g, "\\$1")}"`;
3928
+ }
3929
+ var McpProxyRegistry = class {
3930
+ clients = /* @__PURE__ */ new Map();
3931
+ lastMtimeMs = 0;
3932
+ /** Sentinel so we know whether the file has *ever* been read.
3933
+ * Distinguishes "file missing on first call" (legit empty state)
3934
+ * from "we've read it before and it's gone" (treat as empty too,
3935
+ * but only after closing live clients). */
3936
+ hasReadOnce = false;
3937
+ /**
3938
+ * Reload the imports file when its mtime has advanced. Avoids
3939
+ * repeated parsing when nothing changed — most calls are no-ops.
3940
+ * Disconnects clients whose import was removed; connects the new
3941
+ * set lazily (the first call that touches them).
3942
+ */
3943
+ async ensureCurrent() {
3944
+ let mtimeMs = 0;
3945
+ try {
3946
+ const st = await promises.stat(IMPORTED_MCPS_PATH());
3947
+ mtimeMs = st.mtimeMs;
3948
+ } catch {
3949
+ mtimeMs = 0;
3950
+ }
3951
+ if (this.hasReadOnce && mtimeMs === this.lastMtimeMs) return;
3952
+ const config = await loadImportedMcps().catch(() => ({
3953
+ version: 1,
3954
+ imports: []
3955
+ }));
3956
+ const wantedIds = new Set(config.imports.map((e) => e.id));
3957
+ for (const [id, c] of this.clients.entries()) {
3958
+ if (!wantedIds.has(id)) {
3959
+ await safeClose(c.client);
3960
+ this.clients.delete(id);
3961
+ }
3962
+ }
3963
+ for (const entry of config.imports) {
3964
+ const existing = this.clients.get(entry.id);
3965
+ if (existing) {
3966
+ if (JSON.stringify(existing.entry.snapshot) !== JSON.stringify(entry.snapshot)) {
3967
+ await safeClose(existing.client);
3968
+ this.clients.set(entry.id, freshPlaceholder(entry));
3969
+ } else {
3970
+ existing.entry = entry;
3971
+ }
3972
+ } else {
3973
+ this.clients.set(entry.id, freshPlaceholder(entry));
3974
+ }
3975
+ }
3976
+ this.lastMtimeMs = mtimeMs;
3977
+ this.hasReadOnce = true;
3978
+ }
3979
+ /**
3980
+ * Lazy connect: the first call to `connectIfNeeded(alias)`
3981
+ * actually opens the transport + handshakes. Subsequent calls
3982
+ * reuse the live client.
3983
+ */
3984
+ async connectIfNeeded(alias) {
3985
+ await this.ensureCurrent();
3986
+ const handle = this.findByAlias(alias);
3987
+ if (!handle) return null;
3988
+ if (handle.client) return handle;
3989
+ try {
3990
+ const client = await openClient(handle.entry);
3991
+ handle.client = client;
3992
+ handle.lastError = null;
3993
+ const t = await client.listTools();
3994
+ handle.tools = t.tools.map(toDescriptor);
3995
+ return handle;
3996
+ } catch (err) {
3997
+ handle.client = null;
3998
+ handle.tools = null;
3999
+ handle.lastError = err instanceof Error ? err.message : String(err);
4000
+ return handle;
4001
+ }
4002
+ }
4003
+ /** Find a client handle by alias *or* import id (defensive — agents
4004
+ * sometimes pass the id from `list_imported_mcps`). */
4005
+ findByAlias(aliasOrId) {
4006
+ for (const c of this.clients.values()) {
4007
+ if (c.entry.alias === aliasOrId) return c;
4008
+ if (c.entry.id === aliasOrId) return c;
4009
+ }
4010
+ return void 0;
4011
+ }
4012
+ async listAliases() {
4013
+ await this.ensureCurrent();
4014
+ return Array.from(this.clients.values()).map((c) => ({
4015
+ alias: c.entry.alias,
4016
+ importId: c.entry.id,
4017
+ source: c.entry.snapshot.source,
4018
+ type: c.entry.snapshot.type,
4019
+ status: c.client ? "connected" : c.lastError ? "error" : "pending",
4020
+ toolCount: c.tools?.length ?? 0,
4021
+ ...c.lastError ? { lastError: c.lastError } : {}
4022
+ }));
4023
+ }
4024
+ async listTools(alias) {
4025
+ const handle = await this.connectIfNeeded(alias);
4026
+ if (!handle) return { ok: false, error: `unknown alias "${alias}"` };
4027
+ if (!handle.client || !handle.tools) {
4028
+ return {
4029
+ ok: false,
4030
+ error: handle.lastError ?? "client not connected"
4031
+ };
4032
+ }
4033
+ return { ok: true, tools: handle.tools };
4034
+ }
4035
+ async callTool(alias, toolName, args) {
4036
+ const handle = await this.connectIfNeeded(alias);
4037
+ if (!handle) return { ok: false, error: `unknown alias "${alias}"` };
4038
+ if (!handle.client) {
4039
+ return {
4040
+ ok: false,
4041
+ error: handle.lastError ?? "client not connected"
4042
+ };
4043
+ }
4044
+ try {
4045
+ const result = await handle.client.callTool({
4046
+ name: toolName,
4047
+ arguments: args && typeof args === "object" ? args : {}
4048
+ });
4049
+ return { ok: true, result };
4050
+ } catch (err) {
4051
+ const msg = err instanceof Error ? err.message : String(err);
4052
+ if (/closed|disconnect|EPIPE|ECONNRESET/i.test(msg)) {
4053
+ await safeClose(handle.client);
4054
+ handle.client = null;
4055
+ handle.tools = null;
4056
+ handle.lastError = msg;
4057
+ }
4058
+ return { ok: false, error: msg };
4059
+ }
4060
+ }
4061
+ async closeAll() {
4062
+ await Promise.all(
4063
+ Array.from(this.clients.values()).map((c) => safeClose(c.client))
4064
+ );
4065
+ this.clients.clear();
4066
+ }
4067
+ };
4068
+ function freshPlaceholder(entry) {
4069
+ return { entry, client: null, tools: null, lastError: null };
4070
+ }
4071
+ function toDescriptor(tool) {
4072
+ return {
4073
+ name: tool.name,
4074
+ ...tool.description ? { description: tool.description } : {},
4075
+ ...tool.inputSchema ? { inputSchema: tool.inputSchema } : {}
4076
+ };
4077
+ }
4078
+ function expandEnvPlaceholders(raw) {
4079
+ const out = {};
4080
+ for (const [k, v] of Object.entries(raw)) {
4081
+ const expanded = v.replace(
4082
+ /\$\{([A-Z_][A-Z0-9_]*)(?::-([^}]*))?\}/gi,
4083
+ (_match, name, fallback) => {
4084
+ const fromEnv = process.env[name];
4085
+ if (fromEnv !== void 0) return fromEnv;
4086
+ if (fallback !== void 0) return fallback;
4087
+ return "";
4088
+ }
4089
+ );
4090
+ if (expanded.length === 0) continue;
4091
+ out[k] = expanded;
4092
+ }
4093
+ return out;
4094
+ }
4095
+ async function safeClose(client) {
4096
+ if (!client) return;
4097
+ try {
4098
+ await client.close();
4099
+ } catch {
4100
+ }
4101
+ }
4102
+ async function openClient(entry) {
4103
+ const snap = entry.snapshot;
4104
+ const client = new Client(
4105
+ { name: "agentproto-daemon-proxy", version: "0.1.0" },
4106
+ { capabilities: {} }
4107
+ );
4108
+ if (snap.type === "stdio") {
4109
+ if (!snap.command) {
4110
+ throw new Error(
4111
+ `import "${entry.alias}" is stdio but has no command field`
4112
+ );
4113
+ }
4114
+ const expandedEnv = expandEnvPlaceholders(snap.env ?? {});
4115
+ const transport = new StdioClientTransport({
4116
+ command: snap.command,
4117
+ args: snap.args ?? [],
4118
+ env: { ...process.env, ...expandedEnv }
4119
+ });
4120
+ await client.connect(transport);
4121
+ return client;
4122
+ }
4123
+ if (snap.type === "http") {
4124
+ if (!snap.url) throw new Error(`import "${entry.alias}" has no url`);
4125
+ const transport = new StreamableHTTPClientTransport(new URL(snap.url), {
4126
+ requestInit: {
4127
+ headers: snap.headers ?? {}
4128
+ }
4129
+ });
4130
+ await client.connect(transport);
4131
+ return client;
4132
+ }
4133
+ if (snap.type === "sse") {
4134
+ if (!snap.url) throw new Error(`import "${entry.alias}" has no url`);
4135
+ const transport = new SSEClientTransport(new URL(snap.url), {
4136
+ requestInit: {
4137
+ headers: snap.headers ?? {}
4138
+ }
4139
+ });
4140
+ await client.connect(transport);
4141
+ return client;
4142
+ }
4143
+ throw new Error(
4144
+ `import "${entry.alias}" has unsupported transport type "${snap.type}"`
4145
+ );
4146
+ }
4147
+ var execFileAsync = promisify(execFile);
4148
+ var URL_REGEX = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/i;
4149
+ var STARTUP_TIMEOUT_MS = 3e4;
4150
+ function quickTunnelProvider() {
4151
+ let child = null;
4152
+ let configPath = null;
4153
+ return {
4154
+ id: "quick",
4155
+ async start(opts) {
4156
+ await assertCloudflaredOnPath();
4157
+ const targetUrl = `http://${opts.target.host}:${opts.target.port}`;
4158
+ const id = randomBytes(4).toString("hex");
4159
+ const dir = join(opts.workspace, ".agentproto");
4160
+ await mkdir(dir, { recursive: true });
4161
+ configPath = join(dir, `cloudflared-${id}.yml`);
4162
+ const configBody = [
4163
+ `# generated by agentproto-runtime \u2014 sentinel only`,
4164
+ `# overrides ~/.cloudflared/config.yml so its ingress rules`,
4165
+ `# don't shadow the inline --url rule. Safe to delete when the`,
4166
+ `# tunnel is down.`,
4167
+ `no-autoupdate: true`,
4168
+ `loglevel: debug`,
4169
+ ``
4170
+ ].join("\n");
4171
+ await writeFile(configPath, configBody, "utf8");
4172
+ const proc = spawn(
4173
+ "cloudflared",
4174
+ ["tunnel", "--config", configPath, "--url", targetUrl],
4175
+ { stdio: ["ignore", "pipe", "pipe"], shell: false }
4176
+ );
4177
+ child = proc;
4178
+ const url = await new Promise((resolve7, reject) => {
4179
+ let settled = false;
4180
+ const timer = setTimeout(() => {
4181
+ if (settled) return;
4182
+ settled = true;
4183
+ try {
4184
+ proc.kill("SIGTERM");
4185
+ } catch {
4186
+ }
4187
+ reject(
4188
+ new Error(
4189
+ `cloudflared did not emit a public URL within ${STARTUP_TIMEOUT_MS / 1e3}s`
4190
+ )
4191
+ );
4192
+ }, STARTUP_TIMEOUT_MS);
4193
+ const onData = (chunk) => {
4194
+ const text3 = chunk.toString("utf8");
4195
+ for (const line of text3.split(/\r?\n/)) {
4196
+ if (line.length > 0) opts.onLog?.(`[cloudflared] ${line}`);
4197
+ }
4198
+ if (settled) return;
4199
+ const match = text3.match(URL_REGEX);
4200
+ if (match) {
4201
+ settled = true;
4202
+ clearTimeout(timer);
4203
+ resolve7(match[0]);
4204
+ }
4205
+ };
4206
+ proc.stderr?.on("data", onData);
4207
+ proc.stdout?.on("data", onData);
4208
+ proc.once("error", (err) => {
4209
+ if (settled) return;
4210
+ settled = true;
4211
+ clearTimeout(timer);
4212
+ reject(err);
4213
+ });
4214
+ proc.once("exit", (code, signal) => {
4215
+ if (settled) return;
4216
+ settled = true;
4217
+ clearTimeout(timer);
4218
+ reject(
4219
+ new Error(
4220
+ `cloudflared exited before emitting a URL (code=${code}, signal=${signal})`
4221
+ )
4222
+ );
4223
+ });
4224
+ });
4225
+ proc.once("exit", (code, signal) => {
4226
+ opts.onLog?.(
4227
+ `[cloudflared] exited (code=${code ?? "?"} signal=${signal ?? "-"})`
4228
+ );
4229
+ });
4230
+ return { publicUrl: url, pid: proc.pid ?? null };
4231
+ },
4232
+ async stop() {
4233
+ const proc = child;
4234
+ const cfg = configPath;
4235
+ child = null;
4236
+ configPath = null;
4237
+ if (cfg) {
4238
+ try {
4239
+ await rm(cfg, { force: true });
4240
+ } catch {
4241
+ }
4242
+ }
4243
+ if (!proc || proc.exitCode !== null) return;
4244
+ proc.kill("SIGTERM");
4245
+ const timer = setTimeout(() => {
4246
+ try {
4247
+ proc.kill("SIGKILL");
4248
+ } catch {
4249
+ }
4250
+ }, 3e3);
4251
+ timer.unref();
4252
+ await new Promise((resolve7) => {
4253
+ if (proc.exitCode !== null) {
4254
+ clearTimeout(timer);
4255
+ resolve7();
4256
+ return;
4257
+ }
4258
+ proc.once("exit", () => {
4259
+ clearTimeout(timer);
4260
+ resolve7();
4261
+ });
4262
+ });
4263
+ }
4264
+ };
4265
+ }
4266
+ async function assertCloudflaredOnPath() {
4267
+ try {
4268
+ await execFileAsync("cloudflared", ["--version"], { timeout: 3e3 });
4269
+ } catch {
4270
+ throw new Error(
4271
+ "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."
4272
+ );
4273
+ }
4274
+ }
4275
+ var RemoteController = class {
4276
+ state = null;
4277
+ bearerToken = null;
4278
+ provider = null;
4279
+ lastError;
4280
+ opts;
4281
+ constructor(opts) {
4282
+ this.opts = opts;
4283
+ }
4284
+ /**
4285
+ * Auth getter handed to startHttpServer. The HTTP server calls this
4286
+ * on every request so flipping enable/disable is effective without
4287
+ * a restart.
4288
+ */
4289
+ readAuth = () => {
4290
+ if (this.bearerToken === null) return { mode: "none" };
4291
+ return { mode: "bearer", token: this.bearerToken };
4292
+ };
4293
+ status() {
4294
+ if (!this.state) {
4295
+ return { enabled: false, lastError: this.lastError };
4296
+ }
4297
+ return {
4298
+ enabled: true,
4299
+ provider: this.state.provider,
4300
+ publicUrl: this.state.publicUrl,
4301
+ pid: this.state.pid,
4302
+ createdAt: this.state.createdAt,
4303
+ target: this.state.target,
4304
+ exposesGateway: this.state.exposesGateway,
4305
+ lastError: this.lastError
4306
+ };
4307
+ }
4308
+ async enable(input2) {
4309
+ if (this.state) {
4310
+ throw new Error(
4311
+ "remote tunnel already active \u2014 call remote_disable first to rotate"
4312
+ );
4313
+ }
4314
+ const providerId = input2.provider ?? "quick";
4315
+ const provider = pickProvider(providerId);
4316
+ this.lastError = void 0;
4317
+ const target = {
4318
+ host: input2.targetHost ?? "127.0.0.1",
4319
+ port: input2.targetPort ?? this.opts.port
4320
+ };
4321
+ const exposesGateway = target.host === "127.0.0.1" && target.port === this.opts.port;
4322
+ let started;
4323
+ try {
4324
+ started = await provider.start({
4325
+ target,
4326
+ workspace: this.opts.workspace,
4327
+ onLog: (line) => {
4328
+ this.opts.onLog?.(line);
4329
+ }
4330
+ });
4331
+ } catch (err) {
4332
+ this.lastError = errToMessage(err);
4333
+ throw err;
4334
+ }
4335
+ let bearerToken = null;
4336
+ let bearerTokenHash = "";
4337
+ if (exposesGateway) {
4338
+ bearerToken = randomBytes(32).toString("base64url");
4339
+ bearerTokenHash = sha256(bearerToken);
4340
+ }
4341
+ const state = {
4342
+ provider: providerId,
4343
+ publicUrl: started.publicUrl,
4344
+ bearerTokenHash,
4345
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
4346
+ pid: started.pid,
4347
+ target,
4348
+ exposesGateway
4349
+ };
4350
+ this.state = state;
4351
+ this.bearerToken = bearerToken;
4352
+ this.provider = provider;
4353
+ await persistState(this.opts.workspace, state);
4354
+ const result = {
4355
+ provider: providerId,
4356
+ publicUrl: started.publicUrl,
4357
+ target,
4358
+ exposesGateway
4359
+ };
4360
+ if (exposesGateway && bearerToken) {
4361
+ const mcpEndpoint = `${started.publicUrl}/mcp`;
4362
+ result.bearerToken = bearerToken;
4363
+ result.mcpEndpoint = mcpEndpoint;
4364
+ result.mcpConfigSnippet = buildMcpConfigSnippet(mcpEndpoint, bearerToken);
4365
+ }
4366
+ return result;
4367
+ }
4368
+ async disable() {
4369
+ if (!this.state) return { disabled: false };
4370
+ const provider = this.provider;
4371
+ this.state = null;
4372
+ this.bearerToken = null;
4373
+ this.provider = null;
4374
+ if (provider) {
4375
+ try {
4376
+ await provider.stop();
4377
+ } catch (err) {
4378
+ this.lastError = errToMessage(err);
4379
+ }
4380
+ }
4381
+ await clearState(this.opts.workspace);
4382
+ return { disabled: true };
4383
+ }
4384
+ /**
4385
+ * Stop the provider on gateway shutdown. Does NOT clear persisted
4386
+ * state — that survives across restarts so the user can `remote_status`
4387
+ * after rebooting and see "stale state, run remote_enable to refresh".
4388
+ * (Stale-recovery semantics land in v2; today we just leave the file.)
4389
+ */
4390
+ async shutdown() {
4391
+ if (this.provider) {
4392
+ try {
4393
+ await this.provider.stop();
4394
+ } catch {
4395
+ }
4396
+ }
4397
+ }
4398
+ };
4399
+ function pickProvider(id) {
4400
+ if (id === "quick") return quickTunnelProvider();
4401
+ const _exhaustive = id;
4402
+ throw new Error(`unknown remote provider: ${String(_exhaustive)}`);
4403
+ }
4404
+ function sha256(input2) {
4405
+ return createHash("sha256").update(input2).digest("hex");
4406
+ }
4407
+ function errToMessage(err) {
4408
+ return err instanceof Error ? err.message : String(err);
4409
+ }
4410
+ async function persistState(workspace, state) {
4411
+ const dir = join(workspace, ".agentproto");
4412
+ await mkdir(dir, { recursive: true });
4413
+ const file = join(dir, "remote.json");
4414
+ await writeFile(file, JSON.stringify(state, null, 2) + "\n", "utf8");
4415
+ try {
4416
+ await chmod(file, 384);
4417
+ } catch {
4418
+ }
4419
+ }
4420
+ async function clearState(workspace) {
4421
+ const file = join(workspace, ".agentproto/remote.json");
4422
+ if (!existsSync(file)) return;
4423
+ await rm(file, { force: true });
4424
+ }
4425
+ function buildMcpConfigSnippet(endpoint, token) {
4426
+ return JSON.stringify(
4427
+ {
4428
+ mcpServers: {
4429
+ "agentproto-remote": {
4430
+ transport: "http",
4431
+ url: endpoint,
4432
+ headers: { Authorization: `Bearer ${token}` }
4433
+ }
4434
+ }
4435
+ },
4436
+ null,
4437
+ 2
4438
+ );
4439
+ }
4440
+ function text2(value) {
4441
+ return {
4442
+ content: [
4443
+ {
4444
+ type: "text",
4445
+ text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
4446
+ }
4447
+ ]
4448
+ };
4449
+ }
4450
+ function registerRemoteTools(server, opts) {
4451
+ const { controller } = opts;
4452
+ server.tool(
4453
+ "remote_enable",
4454
+ "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.",
4455
+ {
4456
+ provider: z.enum(["quick"]).optional().describe(
4457
+ "Tunnel backend. `quick` = Cloudflare Quick Tunnel (no API key, ephemeral *.trycloudflare.com URL). Defaults to `quick`. Named-CF / Tailscale providers TBD."
4458
+ ),
4459
+ targetPort: z.number().int().min(1).max(65535).optional().describe(
4460
+ "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."
4461
+ ),
4462
+ targetHost: z.string().optional().describe(
4463
+ "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."
4464
+ )
4465
+ },
4466
+ async ({ provider, targetPort, targetHost }) => {
4467
+ const result = await controller.enable({ provider, targetPort, targetHost });
4468
+ return text2(result);
4469
+ }
4470
+ );
4471
+ server.tool(
4472
+ "remote_disable",
4473
+ "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.",
4474
+ {},
4475
+ async () => {
4476
+ const result = await controller.disable();
4477
+ return text2(result);
4478
+ }
4479
+ );
4480
+ server.tool(
4481
+ "remote_status",
4482
+ "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.",
4483
+ {},
4484
+ async () => {
4485
+ return text2(controller.status());
4486
+ }
4487
+ );
4488
+ }
4489
+ var WorkspacePathError = class extends Error {
4490
+ constructor(message) {
4491
+ super(message);
4492
+ this.name = "WorkspacePathError";
4493
+ }
4494
+ };
4495
+ function createWorkspaceFs(opts) {
4496
+ const root = resolve(opts.workspace);
4497
+ function resolvePath32(path) {
4498
+ if (typeof path !== "string" || path.length === 0) {
4499
+ throw new WorkspacePathError("path must be a non-empty string");
4500
+ }
4501
+ if (isAbsolute(path)) {
4502
+ throw new WorkspacePathError(
4503
+ "absolute paths are not allowed; use a workspace-relative path"
4504
+ );
4505
+ }
4506
+ const joined = normalize(join(root, path));
4507
+ const rel = relative(root, joined);
4508
+ if (rel.startsWith("..") || isAbsolute(rel)) {
4509
+ throw new WorkspacePathError(
4510
+ `path escapes the workspace: '${path}'`
4511
+ );
4512
+ }
4513
+ return joined;
4514
+ }
4515
+ return {
4516
+ async readFile(path) {
4517
+ const abs = resolvePath32(path);
4518
+ const buf = await readFile(abs);
4519
+ return buf.toString("utf8");
4520
+ },
4521
+ async writeFile(path, content) {
4522
+ const abs = resolvePath32(path);
4523
+ await mkdir(dirname(abs), { recursive: true });
4524
+ await writeFile(abs, content);
4525
+ },
4526
+ async exists(path) {
4527
+ try {
4528
+ const abs = resolvePath32(path);
4529
+ return existsSync(abs);
4530
+ } catch {
4531
+ return false;
4532
+ }
4533
+ }
4534
+ };
4535
+ }
4536
+ async function createGateway(opts) {
4537
+ const workspace = resolve(opts.workspace);
4538
+ if (!existsSync(workspace)) {
4539
+ throw new Error(`runtime: workspace dir does not exist: ${workspace}`);
4540
+ }
4541
+ const port = opts.port ?? 18790;
4542
+ const events = createRuntimeEvents();
4543
+ const conversations = fileConversationStore({ workspace });
4544
+ const workspaceFs = createWorkspaceFs({ workspace });
4545
+ const remote = new RemoteController({
4546
+ workspace,
4547
+ port,
4548
+ onLog: (line) => events.emit({
4549
+ type: "remote-log",
4550
+ at: (/* @__PURE__ */ new Date()).toISOString(),
4551
+ line
4552
+ })
4553
+ });
4554
+ const { registered } = await createMcpServer({
4555
+ specs: opts.specs,
4556
+ workspace,
4557
+ name: opts.name ?? "agentproto-runtime",
4558
+ version: opts.version ?? "0.1.0-alpha"
4559
+ });
4560
+ const sessions = createSessionsRegistry();
4561
+ const mcpProxy = new McpProxyRegistry();
4562
+ const mcpServerFactory = async () => {
4563
+ const { server } = await createMcpServer({
4564
+ specs: opts.specs,
4565
+ workspace,
4566
+ name: opts.name ?? "agentproto-runtime",
4567
+ version: opts.version ?? "0.1.0-alpha"
4568
+ });
4569
+ registerFsTools(server, { workspace });
4570
+ registerCommandTools(server, { workspace });
4571
+ registerRemoteTools(server, { controller: remote });
4572
+ registerSessionTools(server, {
4573
+ registry: sessions,
4574
+ mcpProxy,
4575
+ ...opts.resolveAgentAdapter ? { resolveAgentAdapter: opts.resolveAgentAdapter } : {},
4576
+ ...opts.listAgentAdapters ? { listAgentAdapters: opts.listAgentAdapters } : {}
4577
+ });
4578
+ return server;
4579
+ };
4580
+ if (opts.boot !== false) {
4581
+ await runBoot(workspace, opts, conversations, events).catch((err) => {
4582
+ events.emit({
4583
+ type: "heartbeat-error",
4584
+ at: (/* @__PURE__ */ new Date()).toISOString(),
4585
+ agent: opts.defaultBootAgent,
4586
+ error: `BOOT.md failed: ${err instanceof Error ? err.message : String(err)}`
4587
+ });
4588
+ });
4589
+ }
4590
+ const heartbeat = startHeartbeat({
4591
+ workspace,
4592
+ conversations,
4593
+ events,
4594
+ buildAgent: opts.buildAgent ?? noopBuildAgent
4595
+ });
4596
+ const http = await startHttpServer({
4597
+ port,
4598
+ bind: opts.bind,
4599
+ // Auth is read on every request via this getter. `opts.auth`
4600
+ // (when provided) is the startup default and represents the
4601
+ // operator's intent — bearer-only deployments stay bearer-only
4602
+ // even when no remote tunnel is up. The controller's auth wins
4603
+ // once a tunnel is enabled (it issues a fresh token); on disable
4604
+ // we fall back to `opts.auth` if set, otherwise `mode: "none"`.
4605
+ auth: () => {
4606
+ const remoteAuth = remote.readAuth();
4607
+ if (remoteAuth.mode === "bearer") return remoteAuth;
4608
+ return opts.auth ?? { mode: "none" };
4609
+ },
4610
+ mcpServerFactory,
4611
+ conversations,
4612
+ events,
4613
+ heartbeat,
4614
+ sessions,
4615
+ mcpProxy,
4616
+ ...opts.resolveAgentAdapter ? { resolveAgentAdapter: opts.resolveAgentAdapter } : {},
4617
+ ...opts.listAgentAdapters ? { listAgentAdapters: opts.listAgentAdapters } : {},
4618
+ meta: { workspace, registered }
4619
+ });
4620
+ heartbeat.start();
4621
+ events.emit({
4622
+ type: "boot",
4623
+ at: (/* @__PURE__ */ new Date()).toISOString(),
4624
+ workspace,
4625
+ registered
4626
+ });
4627
+ void writeRuntimeMeta(workspace, {
4628
+ workspace,
4629
+ port,
4630
+ bind: opts.bind ?? "127.0.0.1",
4631
+ pid: process.pid,
4632
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
4633
+ name: opts.name ?? "agentproto-runtime",
4634
+ registered
4635
+ });
4636
+ return {
4637
+ url: http.url,
4638
+ workspace,
4639
+ workspaceFs,
4640
+ registered,
4641
+ sessions,
4642
+ async stop() {
4643
+ heartbeat.stop();
4644
+ sessions.shutdown();
4645
+ await mcpProxy.closeAll();
4646
+ await remote.shutdown();
4647
+ await http.stop();
4648
+ }
4649
+ };
4650
+ }
4651
+ var noopBuildAgent = async () => null;
4652
+ async function runBoot(workspace, opts, conversations, events) {
4653
+ const path = join(workspace, "BOOT.md");
4654
+ if (!existsSync(path)) return;
4655
+ const body = (await readFile(path, "utf8")).trim();
4656
+ if (!body) return;
4657
+ if (!opts.buildAgent || !opts.defaultBootAgent) return;
4658
+ const agent = await opts.buildAgent(opts.defaultBootAgent);
4659
+ if (!agent) return;
4660
+ const conversationId = `boot-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace(/[:.]/g, "-")}`;
4661
+ await conversations.open(conversationId, { agent: opts.defaultBootAgent });
4662
+ await conversations.appendTurn(conversationId, "user", body, {
4663
+ attribution: "boot"
4664
+ });
4665
+ const reply = await agent.generate(body);
4666
+ await conversations.appendTurn(conversationId, "assistant", reply.text, {
4667
+ attribution: opts.defaultBootAgent
4668
+ });
4669
+ events.emit({
4670
+ type: "heartbeat-fired",
4671
+ at: (/* @__PURE__ */ new Date()).toISOString(),
4672
+ agent: opts.defaultBootAgent,
4673
+ conversationId,
4674
+ prompt: body,
4675
+ reply: reply.text,
4676
+ durationMs: 0
4677
+ });
4678
+ }
1011
4679
  async function runServe(args) {
1012
4680
  const { values } = parseArgs({
1013
4681
  args: [...args],
@@ -1024,8 +4692,8 @@ async function runServe(args) {
1024
4692
  });
1025
4693
  const workspace = resolve(values.workspace ?? process.cwd());
1026
4694
  try {
1027
- const stat = await promises.stat(workspace);
1028
- if (!stat.isDirectory()) {
4695
+ const stat2 = await promises.stat(workspace);
4696
+ if (!stat2.isDirectory()) {
1029
4697
  process.stderr.write(
1030
4698
  `agentproto serve: --workspace "${workspace}" is not a directory.
1031
4699
  `
@@ -1139,8 +4807,8 @@ agentproto serve: ${signal} \u2014 shutting down.
1139
4807
  `agentproto serve: running local-only (no --connect). Press Ctrl-C to stop.
1140
4808
  `
1141
4809
  );
1142
- await new Promise((resolve) => {
1143
- aborter.signal.addEventListener("abort", () => resolve());
4810
+ await new Promise((resolve3) => {
4811
+ aborter.signal.addEventListener("abort", () => resolve3());
1144
4812
  });
1145
4813
  return 0;
1146
4814
  }
@@ -1175,10 +4843,10 @@ async function runOneTunnel(opts, gateway, signal) {
1175
4843
  };
1176
4844
  if (opts.token) headers.authorization = `Bearer ${opts.token}`;
1177
4845
  const ws = new WebSocket(opts.connect, { headers });
1178
- await new Promise((resolve, reject) => {
4846
+ await new Promise((resolve3, reject) => {
1179
4847
  const onOpen = () => {
1180
4848
  ws.off("error", onError);
1181
- resolve();
4849
+ resolve3();
1182
4850
  };
1183
4851
  const onError = (err) => {
1184
4852
  ws.off("open", onOpen);
@@ -1223,28 +4891,28 @@ async function runOneTunnel(opts, gateway, signal) {
1223
4891
  });
1224
4892
  }
1225
4893
  });
1226
- await new Promise((resolve) => {
4894
+ await new Promise((resolve3) => {
1227
4895
  const offClose = sink.onClose(() => {
1228
4896
  offClose();
1229
- resolve();
4897
+ resolve3();
1230
4898
  });
1231
4899
  if (signal.aborted) {
1232
- server.close().finally(() => resolve());
4900
+ server.close().finally(() => resolve3());
1233
4901
  }
1234
4902
  signal.addEventListener("abort", () => {
1235
- server.close().finally(() => resolve());
4903
+ server.close().finally(() => resolve3());
1236
4904
  });
1237
4905
  });
1238
4906
  process.stderr.write(`agentproto serve: tunnel closed.
1239
4907
  `);
1240
4908
  }
1241
4909
  function sleep(ms, signal) {
1242
- return new Promise((resolve) => {
1243
- if (signal.aborted) return resolve();
1244
- const timer = setTimeout(resolve, ms);
4910
+ return new Promise((resolve3) => {
4911
+ if (signal.aborted) return resolve3();
4912
+ const timer = setTimeout(resolve3, ms);
1245
4913
  signal.addEventListener("abort", () => {
1246
4914
  clearTimeout(timer);
1247
- resolve();
4915
+ resolve3();
1248
4916
  });
1249
4917
  });
1250
4918
  }