@ai-matrx/coding-sessions 0.1.83 → 0.1.85

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.
@@ -1,15 +1,15 @@
1
1
  // src/server/index.ts
2
- import { randomBytes, timingSafeEqual } from "crypto";
2
+ import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
3
3
  import { existsSync as existsSync2 } from "fs";
4
- import fs7 from "fs/promises";
4
+ import fs9 from "fs/promises";
5
5
  import http from "http";
6
- import path8 from "path";
6
+ import path10 from "path";
7
7
  import { fileURLToPath as fileURLToPath2 } from "url";
8
8
  import { WebSocketServer } from "ws";
9
9
 
10
10
  // src/engine/index.ts
11
11
  import { execFile as execFile3, fork } from "child_process";
12
- import { randomUUID as randomUUID3 } from "crypto";
12
+ import { randomUUID as randomUUID4 } from "crypto";
13
13
  import { existsSync } from "fs";
14
14
  import { formatFileSize } from "@ai-matrx/kit/format";
15
15
 
@@ -70,10 +70,379 @@ function createRawBackupScheduler(options) {
70
70
  };
71
71
  }
72
72
 
73
+ // src/engine/agent-attach.ts
74
+ import fs2 from "fs/promises";
75
+ import path from "path";
76
+ var AGENT_MCP_SERVER_NAME = "matrx-agent";
77
+ var AGENT_PLUGIN_NAME = "matrx-agent";
78
+ function escapeAttr(value) {
79
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
80
+ }
81
+ function versionWords(bundle) {
82
+ return bundle.version_number !== null ? String(bundle.version_number) : "latest";
83
+ }
84
+ function renderAgentInstructions(bundle, options = {}) {
85
+ const included = bundle.skills.filter((skill) => skill.tier === "included");
86
+ const listed2 = bundle.skills.filter((skill) => skill.tier === "listed");
87
+ const parts = [`<ai_matrx_agent name="${escapeAttr(bundle.name)}" version="${versionWords(bundle)}">`];
88
+ if (included.length > 0) {
89
+ parts.push(
90
+ "## Skills (always in effect)",
91
+ ...included.map((skill) => `### ${skill.label}
92
+
93
+ ${skill.body.trim()}`)
94
+ );
95
+ }
96
+ if (options.claudeSkills && listed2.length > 0) {
97
+ parts.push(
98
+ [
99
+ "## Your skills",
100
+ "When a request matches one of these, load it with your Skill tool and follow it before answering:",
101
+ ...listed2.map((skill) => `- ${AGENT_PLUGIN_NAME}:${skill.slug} \u2014 ${skill.description || skill.label}`)
102
+ ].join("\n")
103
+ );
104
+ }
105
+ if (included.length > 0 || options.claudeSkills && listed2.length > 0) parts.push("## Your instructions");
106
+ parts.push(bundle.instructions.trim());
107
+ parts.push("</ai_matrx_agent>");
108
+ return `${parts.filter(Boolean).join("\n\n")}
109
+ `;
110
+ }
111
+ function renderCodexSkillsList(bundle, skillsRoot) {
112
+ const listed2 = bundle.skills.filter((skill) => skill.tier === "listed");
113
+ if (listed2.length === 0) return "";
114
+ const lines = [
115
+ "<skills_instructions>",
116
+ "## AI Matrx agent skills",
117
+ "A skill is a set of instructions stored in a `SKILL.md` file. Each entry has a name, a description and the absolute path of its file. When a task matches a skill's description, read that file and follow it.",
118
+ "### Available skills",
119
+ ...listed2.map(
120
+ (skill) => `- ${skill.slug}: ${skill.description || skill.label} (file: ${path.join(skillsRoot, skill.slug, "SKILL.md")})`
121
+ ),
122
+ "</skills_instructions>"
123
+ ];
124
+ return lines.join("\n");
125
+ }
126
+ function tomlString(value) {
127
+ const scalar = value.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "\uFFFD");
128
+ return JSON.stringify(scalar).replace(/\u007f/g, "\\u007f");
129
+ }
130
+ async function writeAgentAttachment(dir, bundle, options) {
131
+ const pluginDir = path.join(dir, "plugin");
132
+ const skillsRoot = path.join(dir, "skills");
133
+ await fs2.mkdir(path.join(pluginDir, ".claude-plugin"), { recursive: true, mode: 448 });
134
+ await fs2.mkdir(skillsRoot, { recursive: true, mode: 448 });
135
+ const instructions = renderAgentInstructions(bundle);
136
+ const instructionsFile = path.join(dir, "instructions.md");
137
+ await fs2.writeFile(instructionsFile, renderAgentInstructions(bundle, { claudeSkills: true }), { mode: 384 });
138
+ await fs2.writeFile(
139
+ path.join(pluginDir, ".claude-plugin", "plugin.json"),
140
+ `${JSON.stringify(
141
+ {
142
+ name: AGENT_PLUGIN_NAME,
143
+ description: `Skills of the AI Matrx agent "${bundle.name}"`,
144
+ version: versionWords(bundle) === "latest" ? "1.0.0" : `${versionWords(bundle)}.0.0`
145
+ },
146
+ null,
147
+ 2
148
+ )}
149
+ `,
150
+ { mode: 384 }
151
+ );
152
+ const listed2 = bundle.skills.filter((skill) => skill.tier === "listed");
153
+ const wanted = new Set(listed2.map((skill) => skill.slug));
154
+ for (const root of [path.join(pluginDir, "skills"), skillsRoot]) {
155
+ await fs2.mkdir(root, { recursive: true, mode: 448 });
156
+ for (const existing of await fs2.readdir(root).catch(() => [])) {
157
+ if (!wanted.has(existing)) await fs2.rm(path.join(root, existing), { recursive: true, force: true });
158
+ }
159
+ for (const skill of listed2) {
160
+ const skillDir = path.join(root, skill.slug);
161
+ await fs2.mkdir(skillDir, { recursive: true, mode: 448 });
162
+ await fs2.writeFile(path.join(skillDir, "SKILL.md"), skill.content, { mode: 384 });
163
+ }
164
+ }
165
+ let mcpConfigFile = null;
166
+ const mcpUrl = options.mcpUrl && bundle.tools.length > 0 ? options.mcpUrl : null;
167
+ const mcpPath = path.join(dir, "mcp.json");
168
+ if (mcpUrl) {
169
+ mcpConfigFile = mcpPath;
170
+ await fs2.writeFile(
171
+ mcpPath,
172
+ `${JSON.stringify({ mcpServers: { [AGENT_MCP_SERVER_NAME]: { type: "http", url: mcpUrl } } }, null, 2)}
173
+ `,
174
+ { mode: 384 }
175
+ );
176
+ } else {
177
+ await fs2.rm(mcpPath, { force: true });
178
+ }
179
+ const skillsList = renderCodexSkillsList(bundle, skillsRoot);
180
+ return {
181
+ dir,
182
+ instructionsFile,
183
+ pluginDir,
184
+ mcpConfigFile,
185
+ mcpUrl,
186
+ // Same order as Claude's: the skills list first, the agent's words last.
187
+ codexInstructions: skillsList ? `${skillsList}
188
+
189
+ ${instructions}` : instructions
190
+ };
191
+ }
192
+ function agentArgsFor(provider, attachment, options) {
193
+ if (provider === "claude") {
194
+ const args2 = [
195
+ // Every turn: without snapshot off, a resumed Claude drops appended instructions.
196
+ "--append-system-prompt-file",
197
+ attachment.instructionsFile,
198
+ "--system-prompt-snapshot",
199
+ "off",
200
+ "--plugin-dir",
201
+ attachment.pluginDir
202
+ ];
203
+ if (attachment.mcpConfigFile) args2.push("--mcp-config", attachment.mcpConfigFile);
204
+ return args2;
205
+ }
206
+ const args = [];
207
+ if (!options.resuming) args.push("-c", `developer_instructions=${tomlString(attachment.codexInstructions)}`);
208
+ if (attachment.mcpUrl) {
209
+ const key = `mcp_servers.${AGENT_MCP_SERVER_NAME}`;
210
+ args.push(
211
+ "-c",
212
+ `${key}.url=${tomlString(attachment.mcpUrl)}`,
213
+ "-c",
214
+ `${key}.default_tools_approval_mode="approve"`
215
+ );
216
+ }
217
+ return args;
218
+ }
219
+ function sessionAgentRecord(bundle, options) {
220
+ const unavailable = bundle.unavailable.map((u) => ({ kind: u.kind, name: u.name, reason: u.reason }));
221
+ const missingSkills = unavailable.filter((u) => u.kind === "skill").length;
222
+ const missingTools = unavailable.filter((u) => u.kind === "tool").length;
223
+ if (!options.toolsServed && bundle.tools.length > 0) {
224
+ for (const tool of bundle.tools) {
225
+ unavailable.push({ kind: "tool", name: tool.canonical_name, reason: "Tools need the coding sessions host" });
226
+ }
227
+ }
228
+ const toolsLoaded = options.toolsServed ? bundle.tools.length : 0;
229
+ const loaded = [
230
+ { part: "instructions", loaded: bundle.instructions.trim() ? 1 : 0, total: 1 },
231
+ { part: "skills", loaded: bundle.skills.length, total: bundle.skills.length + missingSkills },
232
+ { part: "tools", loaded: toolsLoaded, total: bundle.tools.length + missingTools }
233
+ ];
234
+ return {
235
+ agentId: bundle.agent_id,
236
+ versionId: bundle.version_id,
237
+ versionNumber: bundle.version_number,
238
+ name: bundle.name,
239
+ definitionHash: bundle.definition_hash,
240
+ loaded,
241
+ unavailable
242
+ };
243
+ }
244
+
245
+ // src/engine/agents.ts
246
+ import { randomBytes, randomUUID } from "crypto";
247
+ import fs3 from "fs/promises";
248
+ import path2 from "path";
249
+ var DEFAULT_SERVER_URL = "https://server.app.matrxserver.com";
250
+ var DEFAULT_SUPABASE_URL = "https://db.matrxserver.com";
251
+ var AGENT_CATALOG_RPCS = /* @__PURE__ */ new Set([
252
+ "agx_get_list_full",
253
+ "agx_search",
254
+ "agx_resolve_agent_address",
255
+ "ues_get_bulk",
256
+ "ues_list",
257
+ "ues_set",
258
+ "ues_touch"
259
+ ]);
260
+ var agentsClient = async () => {
261
+ const [matrx, portable, mcp] = await Promise.all([
262
+ import("@ai-matrx/agents/matrx"),
263
+ import("@ai-matrx/agents/portable"),
264
+ import("@ai-matrx/agents/portable/mcp")
265
+ ]);
266
+ return { ...matrx, ...portable, ...mcp };
267
+ };
268
+ var SIGNED_OUT_AGENT_MESSAGE = "Sign in to AI Matrx to use your agents.";
269
+ function userIdOf(credential) {
270
+ if (!credential || credential.kind !== "user") return null;
271
+ const payload = credential.accessToken.split(".")[1];
272
+ if (!payload) return null;
273
+ try {
274
+ const claims = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
275
+ return typeof claims.sub === "string" ? claims.sub : null;
276
+ } catch {
277
+ return null;
278
+ }
279
+ }
280
+ function createAgentConnection(options, dataDir) {
281
+ const credentials = options?.credentials ?? null;
282
+ const serverUrl = (options?.serverUrl ?? DEFAULT_SERVER_URL).replace(/\/+$/, "");
283
+ const supabaseUrl = (options?.supabaseUrl ?? DEFAULT_SUPABASE_URL).replace(/\/+$/, "");
284
+ const supabaseKey = options?.supabaseKey ?? null;
285
+ const root = path2.join(dataDir, "agents");
286
+ let mcpBaseUrl = null;
287
+ const handlers = /* @__PURE__ */ new Map();
288
+ const secretToSession = /* @__PURE__ */ new Map();
289
+ const organizationId = () => {
290
+ const raw = typeof options?.organizationId === "function" ? options.organizationId() : options?.organizationId;
291
+ return raw && raw.trim() ? raw.trim() : null;
292
+ };
293
+ const transport = async () => (await agentsClient()).createMatrxTransport({
294
+ baseUrl: serverUrl,
295
+ ...credentials ? { credentials } : {},
296
+ ...organizationId() ? { organizationId: () => organizationId() } : {},
297
+ source: "codingSessionsAgents"
298
+ });
299
+ const credential = async () => credentials ? credentials.get() : null;
300
+ const requireSignedIn = async () => {
301
+ if (!await credential()) throw new Error(SIGNED_OUT_AGENT_MESSAGE);
302
+ };
303
+ const sessionDir = (sessionId) => path2.join(root, sessionId.replace(/[^A-Za-z0-9_-]/g, "_"));
304
+ async function meta(sessionId) {
305
+ const file = path2.join(sessionDir(sessionId), "session.json");
306
+ const existing = await fs3.readFile(file, "utf8").then((text) => JSON.parse(text)).catch(() => null);
307
+ if (existing?.mcpSecret && existing.conversationId) return existing;
308
+ const created = { mcpSecret: randomBytes(24).toString("base64url"), conversationId: randomUUID() };
309
+ await fs3.mkdir(sessionDir(sessionId), { recursive: true, mode: 448 });
310
+ await fs3.writeFile(file, JSON.stringify(created), { mode: 384 });
311
+ return created;
312
+ }
313
+ async function findSessionBySecret(secret) {
314
+ for (const name of await fs3.readdir(root).catch(() => [])) {
315
+ const found = await fs3.readFile(path2.join(root, name, "session.json"), "utf8").then((text) => JSON.parse(text).mcpSecret === secret).catch(() => false);
316
+ if (found) {
317
+ secretToSession.set(secret, name);
318
+ return name;
319
+ }
320
+ }
321
+ return null;
322
+ }
323
+ async function readSessionBundle(sessionId) {
324
+ return fs3.readFile(path2.join(sessionDir(sessionId), "bundle.json"), "utf8").then((text) => JSON.parse(text)).catch(() => null);
325
+ }
326
+ return {
327
+ async access() {
328
+ const current = await credential().catch(() => null);
329
+ const userId = userIdOf(current);
330
+ return {
331
+ configured: credentials !== null && supabaseKey !== null,
332
+ signedIn: userId !== null,
333
+ userId,
334
+ organizationId: organizationId()
335
+ };
336
+ },
337
+ async catalogRpc(request) {
338
+ if (!AGENT_CATALOG_RPCS.has(request.fn)) {
339
+ return { data: null, error: { message: `Not an agent catalogue read: ${request.fn}` } };
340
+ }
341
+ const current = await credential();
342
+ if (!current || current.kind !== "user") return { data: null, error: { message: SIGNED_OUT_AGENT_MESSAGE, code: "signed_out" } };
343
+ if (!supabaseKey) return { data: null, error: { message: "This host has no AI Matrx database key.", code: "not_configured" } };
344
+ const query = new URLSearchParams();
345
+ if (request.order) query.set("order", `${request.order.column}.${request.order.ascending === false ? "desc" : "asc"}`);
346
+ if (request.range) {
347
+ query.set("offset", String(request.range[0]));
348
+ query.set("limit", String(request.range[1] - request.range[0] + 1));
349
+ }
350
+ const headers = {
351
+ apikey: supabaseKey,
352
+ Authorization: `Bearer ${current.accessToken}`,
353
+ "Content-Type": "application/json",
354
+ // Every catalogue RPC lives in `public`; the database's default
355
+ // exposed schema is not `public`, so name it (supabase-js does).
356
+ "Content-Profile": "public",
357
+ "Accept-Profile": "public"
358
+ };
359
+ if (request.count) headers.Prefer = `count=${request.count}`;
360
+ const qs = query.toString();
361
+ const response = await fetch(`${supabaseUrl}/rest/v1/rpc/${encodeURIComponent(request.fn)}${qs ? `?${qs}` : ""}`, {
362
+ method: "POST",
363
+ headers,
364
+ body: JSON.stringify(request.args ?? {})
365
+ });
366
+ const text = await response.text();
367
+ const body = text ? JSON.parse(text) : null;
368
+ if (!response.ok) {
369
+ const err = body ?? {};
370
+ return {
371
+ data: null,
372
+ error: { message: err.message ?? `HTTP ${response.status}`, code: err.code, details: err.details ?? null, hint: err.hint ?? null }
373
+ };
374
+ }
375
+ const range = response.headers.get("content-range");
376
+ const total = range?.split("/")[1];
377
+ return { data: body, error: null, count: total && total !== "*" ? Number(total) : null };
378
+ },
379
+ async details(agentId) {
380
+ await requireSignedIn();
381
+ const { fetchPortableAgent, fetchPortableAgentVersions } = await agentsClient();
382
+ const [bundle, versions] = await Promise.all([
383
+ fetchPortableAgent(await transport(), { agentId }),
384
+ fetchPortableAgentVersions(await transport(), agentId).catch(() => [])
385
+ ]);
386
+ return {
387
+ agentId: bundle.agent_id,
388
+ name: bundle.name,
389
+ variables: bundle.variables,
390
+ versions: versions.map((v) => ({ id: v.id, versionNumber: v.version_number, changedAt: v.changed_at })).sort((a, b) => b.versionNumber - a.versionNumber),
391
+ latestVersionNumber: bundle.version_number
392
+ };
393
+ },
394
+ async loadBundle(selection) {
395
+ await requireSignedIn();
396
+ const { fetchPortableAgent } = await agentsClient();
397
+ return fetchPortableAgent(
398
+ await transport(),
399
+ selection.versionId ? { versionId: selection.versionId } : { agentId: selection.agentId },
400
+ { variables: selection.variables ?? {} }
401
+ );
402
+ },
403
+ async saveSessionBundle(sessionId, bundle) {
404
+ await fs3.mkdir(sessionDir(sessionId), { recursive: true, mode: 448 });
405
+ await fs3.writeFile(path2.join(sessionDir(sessionId), "bundle.json"), JSON.stringify(bundle), { mode: 384 });
406
+ await meta(sessionId);
407
+ },
408
+ readSessionBundle,
409
+ sessionDir,
410
+ async mcpUrlFor(sessionId) {
411
+ if (!mcpBaseUrl) return null;
412
+ const { mcpSecret } = await meta(sessionId);
413
+ secretToSession.set(mcpSecret, sessionId);
414
+ return `${mcpBaseUrl}/mcp/${mcpSecret}`;
415
+ },
416
+ setMcpBaseUrl(baseUrl) {
417
+ mcpBaseUrl = baseUrl ? baseUrl.replace(/\/+$/, "") : null;
418
+ },
419
+ async handleMcp(secret, body) {
420
+ let handler = handlers.get(secret);
421
+ if (!handler) {
422
+ const sessionId = secretToSession.get(secret) ?? await findSessionBySecret(secret);
423
+ if (!sessionId) return null;
424
+ const bundle = await readSessionBundle(sessionId);
425
+ if (!bundle) return null;
426
+ const { conversationId } = await meta(sessionId);
427
+ const { createAgentToolsMcpHandler } = await agentsClient();
428
+ handler = createAgentToolsMcpHandler({
429
+ bundle,
430
+ credentials,
431
+ organizationId: () => organizationId(),
432
+ baseUrl: serverUrl,
433
+ conversationId
434
+ });
435
+ handlers.set(secret, handler);
436
+ }
437
+ return handler.handle(body);
438
+ }
439
+ };
440
+ }
441
+
73
442
  // src/engine/index.ts
74
- import fs6 from "fs/promises";
443
+ import fs8 from "fs/promises";
75
444
  import os5 from "os";
76
- import path7 from "path";
445
+ import path9 from "path";
77
446
  import { fileURLToPath } from "url";
78
447
 
79
448
  // src/normalize/claude.ts
@@ -743,7 +1112,7 @@ function describeProblem(provider, problem) {
743
1112
  import { execFile } from "child_process";
744
1113
  import { accessSync, constants as fsConstants, readdirSync, readFileSync, realpathSync } from "fs";
745
1114
  import os from "os";
746
- import path from "path";
1115
+ import path3 from "path";
747
1116
  var SOURCE_LABEL = {
748
1117
  codex: { "desktop-app": "from ChatGPT app", path: "from PATH", "npm-global": "from npm", configured: "set by host" },
749
1118
  claude: { "desktop-app": "from Claude app", path: "from PATH", "npm-global": "from npm", configured: "set by host" }
@@ -792,10 +1161,10 @@ function exeName(name, platform) {
792
1161
  }
793
1162
  function allOnPath(name, pathValue, platform = process.platform) {
794
1163
  const found = [];
795
- for (const dir of pathValue.split(path.delimiter)) {
1164
+ for (const dir of pathValue.split(path3.delimiter)) {
796
1165
  if (!dir) continue;
797
1166
  for (const file of exeName(name, platform)) {
798
- const candidate = path.join(dir, file);
1167
+ const candidate = path3.join(dir, file);
799
1168
  if (executable(candidate)) {
800
1169
  found.push(candidate);
801
1170
  break;
@@ -808,17 +1177,17 @@ function desktopAppCandidates(provider, platform, home, applications) {
808
1177
  if (platform !== "darwin") return [];
809
1178
  if (provider === "codex") {
810
1179
  return applications.map(
811
- (apps) => path.join(apps, "ChatGPT.app", "Contents", "Resources", "codex-cli", "bin", "codex")
1180
+ (apps) => path3.join(apps, "ChatGPT.app", "Contents", "Resources", "codex-cli", "bin", "codex")
812
1181
  );
813
1182
  }
814
- const root = path.join(home, "Library", "Application Support", "Claude", "claude-code");
1183
+ const root = path3.join(home, "Library", "Application Support", "Claude", "claude-code");
815
1184
  let versions = [];
816
1185
  try {
817
1186
  versions = readdirSync(root);
818
1187
  } catch {
819
1188
  return [];
820
1189
  }
821
- return versions.map((v) => path.join(root, v, "claude.app", "Contents", "MacOS", "claude"));
1190
+ return versions.map((v) => path3.join(root, v, "claude.app", "Contents", "MacOS", "claude"));
822
1191
  }
823
1192
  function npmGlobalPrefix(env) {
824
1193
  const fromEnv = env.npm_config_prefix ?? env.NPM_CONFIG_PREFIX;
@@ -836,13 +1205,13 @@ function declaredVersion(provider, source, file) {
836
1205
  if (source !== "desktop-app") return null;
837
1206
  if (provider === "codex") {
838
1207
  try {
839
- const pkg = JSON.parse(readFileSync(path.join(path.dirname(path.dirname(file)), "codex-package.json"), "utf8"));
1208
+ const pkg = JSON.parse(readFileSync(path3.join(path3.dirname(path3.dirname(file)), "codex-package.json"), "utf8"));
840
1209
  return typeof pkg.version === "string" ? parseVersion(pkg.version) : null;
841
1210
  } catch {
842
1211
  return null;
843
1212
  }
844
1213
  }
845
- return parseVersion(path.basename(path.resolve(file, "..", "..", "..", "..")));
1214
+ return parseVersion(path3.basename(path3.resolve(file, "..", "..", "..", "..")));
846
1215
  }
847
1216
  function runVersion(file, env) {
848
1217
  return new Promise((resolve) => {
@@ -856,14 +1225,14 @@ async function discoverBinaries(provider, options) {
856
1225
  const platform = options.platform ?? process.platform;
857
1226
  const home = options.home ?? options.env.HOME ?? os.homedir();
858
1227
  const listed2 = [
859
- ...desktopAppCandidates(provider, platform, home, options.applications ?? ["/Applications", path.join(home, "Applications")]).filter(executable).map((p) => ({ path: p, source: "desktop-app" })),
1228
+ ...desktopAppCandidates(provider, platform, home, options.applications ?? ["/Applications", path3.join(home, "Applications")]).filter(executable).map((p) => ({ path: p, source: "desktop-app" })),
860
1229
  ...allOnPath(provider, options.env.PATH ?? "", platform).map((p) => ({ path: p, source: "path" }))
861
1230
  ];
862
1231
  const prefix = await npmGlobalPrefix(options.env);
863
1232
  if (prefix) {
864
- const dir = platform === "win32" ? prefix : path.join(prefix, "bin");
1233
+ const dir = platform === "win32" ? prefix : path3.join(prefix, "bin");
865
1234
  for (const file of exeName(provider, platform)) {
866
- const candidate = path.join(dir, file);
1235
+ const candidate = path3.join(dir, file);
867
1236
  if (executable(candidate)) {
868
1237
  listed2.push({ path: candidate, source: "npm-global" });
869
1238
  break;
@@ -909,7 +1278,7 @@ async function resolveBinary(provider, options) {
909
1278
 
910
1279
  // src/engine/clean-env.ts
911
1280
  import os2 from "os";
912
- import path2 from "path";
1281
+ import path4 from "path";
913
1282
  var ALLOWED_NAMES = /* @__PURE__ */ new Set([
914
1283
  "HOME",
915
1284
  "USER",
@@ -931,11 +1300,11 @@ var ALLOWED_NAMES = /* @__PURE__ */ new Set([
931
1300
  var ALLOWED_PREFIXES = ["LC_"];
932
1301
  function wellKnownBinDirs(home) {
933
1302
  return [
934
- path2.join(home, ".local", "bin"),
935
- path2.join(home, ".claude", "local"),
936
- path2.join(home, ".npm-global", "bin"),
937
- path2.join(home, ".bun", "bin"),
938
- path2.join(home, ".volta", "bin"),
1303
+ path4.join(home, ".local", "bin"),
1304
+ path4.join(home, ".claude", "local"),
1305
+ path4.join(home, ".npm-global", "bin"),
1306
+ path4.join(home, ".bun", "bin"),
1307
+ path4.join(home, ".volta", "bin"),
939
1308
  "/opt/homebrew/bin",
940
1309
  "/usr/local/bin",
941
1310
  "/usr/bin",
@@ -955,21 +1324,21 @@ function buildCleanEnv(hostEnv, extra = {}) {
955
1324
  }
956
1325
  const home = out.HOME ?? os2.homedir();
957
1326
  out.HOME = home;
958
- const hostPath = (hostEnv.PATH ?? "").split(path2.delimiter).filter(Boolean);
1327
+ const hostPath = (hostEnv.PATH ?? "").split(path4.delimiter).filter(Boolean);
959
1328
  const merged = [];
960
1329
  for (const dir of [...hostPath, ...wellKnownBinDirs(home)]) {
961
1330
  if (!merged.includes(dir)) merged.push(dir);
962
1331
  }
963
- out.PATH = merged.join(path2.delimiter);
1332
+ out.PATH = merged.join(path4.delimiter);
964
1333
  for (const [name, value] of Object.entries(extra)) out[name] = value;
965
1334
  return out;
966
1335
  }
967
1336
 
968
1337
  // src/engine/files.ts
969
1338
  import { createReadStream } from "fs";
970
- import fs2 from "fs/promises";
1339
+ import fs4 from "fs/promises";
971
1340
  import os3 from "os";
972
- import path3 from "path";
1341
+ import path5 from "path";
973
1342
 
974
1343
  // src/engine/activity.ts
975
1344
  import { execFile as execFile2 } from "child_process";
@@ -1038,15 +1407,15 @@ function describeActivity(input) {
1038
1407
  // src/engine/files.ts
1039
1408
  function defaultProviderHomes(home = os3.homedir()) {
1040
1409
  return {
1041
- claudeProjects: path3.join(home, ".claude", "projects"),
1042
- codexSessions: path3.join(home, ".codex", "sessions"),
1043
- codexIndex: path3.join(home, ".codex", "session_index.jsonl")
1410
+ claudeProjects: path5.join(home, ".claude", "projects"),
1411
+ codexSessions: path5.join(home, ".codex", "sessions"),
1412
+ codexIndex: path5.join(home, ".codex", "session_index.jsonl")
1044
1413
  };
1045
1414
  }
1046
1415
  var UUIDISH = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i;
1047
1416
  async function safeReaddir(dir) {
1048
1417
  try {
1049
- return await fs2.readdir(dir, { withFileTypes: true });
1418
+ return await fs4.readdir(dir, { withFileTypes: true });
1050
1419
  } catch {
1051
1420
  return [];
1052
1421
  }
@@ -1055,7 +1424,7 @@ async function statCandidate(provider, file) {
1055
1424
  const match = UUIDISH.exec(file);
1056
1425
  if (!match?.[1]) return null;
1057
1426
  try {
1058
- const stat = await fs2.stat(file);
1427
+ const stat = await fs4.stat(file);
1059
1428
  return { provider, file, id: match[1], mtimeMs: stat.mtimeMs, size: stat.size };
1060
1429
  } catch {
1061
1430
  return null;
@@ -1065,10 +1434,10 @@ async function claudeCandidates(root) {
1065
1434
  const out = [];
1066
1435
  for (const project of await safeReaddir(root)) {
1067
1436
  if (!project.isDirectory()) continue;
1068
- const dir = path3.join(root, project.name);
1437
+ const dir = path5.join(root, project.name);
1069
1438
  const entries = await safeReaddir(dir);
1070
1439
  const stats = await Promise.all(
1071
- entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl")).map((e) => statCandidate("claude", path3.join(dir, e.name)))
1440
+ entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl")).map((e) => statCandidate("claude", path5.join(dir, e.name)))
1072
1441
  );
1073
1442
  for (const s of stats) if (s) out.push(s);
1074
1443
  }
@@ -1078,14 +1447,14 @@ async function codexCandidates(root) {
1078
1447
  const out = [];
1079
1448
  for (const year of await safeReaddir(root)) {
1080
1449
  if (!year.isDirectory()) continue;
1081
- for (const month of await safeReaddir(path3.join(root, year.name))) {
1450
+ for (const month of await safeReaddir(path5.join(root, year.name))) {
1082
1451
  if (!month.isDirectory()) continue;
1083
- for (const day of await safeReaddir(path3.join(root, year.name, month.name))) {
1452
+ for (const day of await safeReaddir(path5.join(root, year.name, month.name))) {
1084
1453
  if (!day.isDirectory()) continue;
1085
- const dir = path3.join(root, year.name, month.name, day.name);
1454
+ const dir = path5.join(root, year.name, month.name, day.name);
1086
1455
  const entries = await safeReaddir(dir);
1087
1456
  const stats = await Promise.all(
1088
- entries.filter((e) => e.isFile() && e.name.startsWith("rollout-") && e.name.endsWith(".jsonl")).map((e) => statCandidate("codex", path3.join(dir, e.name)))
1457
+ entries.filter((e) => e.isFile() && e.name.startsWith("rollout-") && e.name.endsWith(".jsonl")).map((e) => statCandidate("codex", path5.join(dir, e.name)))
1089
1458
  );
1090
1459
  for (const s of stats) if (s) out.push(s);
1091
1460
  }
@@ -1094,7 +1463,7 @@ async function codexCandidates(root) {
1094
1463
  return out;
1095
1464
  }
1096
1465
  async function readSlice(file, start, length) {
1097
- const handle = await fs2.open(file, "r");
1466
+ const handle = await fs4.open(file, "r");
1098
1467
  try {
1099
1468
  const buffer = Buffer.alloc(length);
1100
1469
  const { bytesRead } = await handle.read(buffer, 0, length, start);
@@ -1167,9 +1536,9 @@ async function readCodexIndex(file) {
1167
1536
  const names = /* @__PURE__ */ new Map();
1168
1537
  let text;
1169
1538
  try {
1170
- const stat = await fs2.stat(file);
1539
+ const stat = await fs4.stat(file);
1171
1540
  const budget = 4 * 1024 * 1024;
1172
- text = stat.size <= budget ? await fs2.readFile(file, "utf8") : completeLines(await readSlice(file, stat.size - budget, budget), true);
1541
+ text = stat.size <= budget ? await fs4.readFile(file, "utf8") : completeLines(await readSlice(file, stat.size - budget, budget), true);
1173
1542
  } catch {
1174
1543
  return names;
1175
1544
  }
@@ -1290,7 +1659,7 @@ var MAX_HISTORY_BUDGET = 256 * 1024 * 1024;
1290
1659
  async function readSessionFile(provider, file, endOffset, maxBytes = HISTORY_BUDGET) {
1291
1660
  let size;
1292
1661
  try {
1293
- size = (await fs2.stat(file)).size;
1662
+ size = (await fs4.stat(file)).size;
1294
1663
  } catch {
1295
1664
  return {
1296
1665
  items: [],
@@ -1344,9 +1713,9 @@ async function describeSessionFile(candidate, codexNames) {
1344
1713
  }
1345
1714
 
1346
1715
  // src/engine/preferences.ts
1347
- import { randomUUID } from "crypto";
1348
- import fs3 from "fs/promises";
1349
- import path4 from "path";
1716
+ import { randomUUID as randomUUID2 } from "crypto";
1717
+ import fs5 from "fs/promises";
1718
+ import path6 from "path";
1350
1719
  var PreferenceStore = class {
1351
1720
  constructor(dir) {
1352
1721
  this.dir = dir;
@@ -1355,11 +1724,11 @@ var PreferenceStore = class {
1355
1724
  data = {};
1356
1725
  writing = Promise.resolve();
1357
1726
  get file() {
1358
- return path4.join(this.dir, "preferences.json");
1727
+ return path6.join(this.dir, "preferences.json");
1359
1728
  }
1360
1729
  async load() {
1361
1730
  try {
1362
- const parsed = JSON.parse(await fs3.readFile(this.file, "utf8"));
1731
+ const parsed = JSON.parse(await fs5.readFile(this.file, "utf8"));
1363
1732
  this.data = typeof parsed === "object" && parsed !== null ? parsed : {};
1364
1733
  } catch {
1365
1734
  this.data = {};
@@ -1397,10 +1766,10 @@ var PreferenceStore = class {
1397
1766
  persist() {
1398
1767
  const snapshot = JSON.stringify(this.data, null, 1);
1399
1768
  this.writing = this.writing.then(async () => {
1400
- await fs3.mkdir(this.dir, { recursive: true });
1401
- const tmp = `${this.file}.${randomUUID()}.tmp`;
1402
- await fs3.writeFile(tmp, snapshot, { mode: 384 });
1403
- await fs3.rename(tmp, this.file);
1769
+ await fs5.mkdir(this.dir, { recursive: true });
1770
+ const tmp = `${this.file}.${randomUUID2()}.tmp`;
1771
+ await fs5.writeFile(tmp, snapshot, { mode: 384 });
1772
+ await fs5.rename(tmp, this.file);
1404
1773
  }).catch(() => {
1405
1774
  });
1406
1775
  }
@@ -1557,9 +1926,9 @@ async function searchLocalChats(query, options = {}) {
1557
1926
  }
1558
1927
 
1559
1928
  // src/engine/store.ts
1560
- import { randomUUID as randomUUID2 } from "crypto";
1561
- import fs4 from "fs/promises";
1562
- import path5 from "path";
1929
+ import { randomUUID as randomUUID3 } from "crypto";
1930
+ import fs6 from "fs/promises";
1931
+ import path7 from "path";
1563
1932
  var MAX_RUNS = 2e3;
1564
1933
  var RunIndex = class {
1565
1934
  constructor(dir) {
@@ -1569,12 +1938,12 @@ var RunIndex = class {
1569
1938
  data = { version: 1, sessions: [], runs: [] };
1570
1939
  writing = Promise.resolve();
1571
1940
  get file() {
1572
- return path5.join(this.dir, "index.json");
1941
+ return path7.join(this.dir, "index.json");
1573
1942
  }
1574
1943
  async load() {
1575
- await fs4.mkdir(this.dir, { recursive: true });
1944
+ await fs6.mkdir(this.dir, { recursive: true });
1576
1945
  try {
1577
- const parsed = JSON.parse(await fs4.readFile(this.file, "utf8"));
1946
+ const parsed = JSON.parse(await fs6.readFile(this.file, "utf8"));
1578
1947
  this.data = {
1579
1948
  version: 1,
1580
1949
  sessions: Array.isArray(parsed.sessions) ? parsed.sessions : [],
@@ -1616,9 +1985,9 @@ var RunIndex = class {
1616
1985
  persist() {
1617
1986
  const snapshot = JSON.stringify(this.data, null, 1);
1618
1987
  this.writing = this.writing.then(async () => {
1619
- const tmp = `${this.file}.${randomUUID2()}.tmp`;
1620
- await fs4.writeFile(tmp, snapshot, { mode: 384 });
1621
- await fs4.rename(tmp, this.file);
1988
+ const tmp = `${this.file}.${randomUUID3()}.tmp`;
1989
+ await fs6.writeFile(tmp, snapshot, { mode: 384 });
1990
+ await fs6.rename(tmp, this.file);
1622
1991
  }).catch(() => {
1623
1992
  });
1624
1993
  }
@@ -1628,27 +1997,27 @@ var RunIndex = class {
1628
1997
  /** A stable Paperclip agent/company identity for this install, so the
1629
1998
  * driver's scratch state is reused rather than growing per run. */
1630
1999
  async identity() {
1631
- const file = path5.join(this.dir, "identity.json");
2000
+ const file = path7.join(this.dir, "identity.json");
1632
2001
  try {
1633
- const parsed = JSON.parse(await fs4.readFile(file, "utf8"));
2002
+ const parsed = JSON.parse(await fs6.readFile(file, "utf8"));
1634
2003
  if (typeof parsed.companyId === "string" && typeof parsed.agentId === "string") {
1635
2004
  return { companyId: parsed.companyId, agentId: parsed.agentId };
1636
2005
  }
1637
2006
  } catch {
1638
2007
  }
1639
- const identity = { companyId: randomUUID2(), agentId: randomUUID2() };
1640
- await fs4.writeFile(file, JSON.stringify(identity), { mode: 384 });
2008
+ const identity = { companyId: randomUUID3(), agentId: randomUUID3() };
2009
+ await fs6.writeFile(file, JSON.stringify(identity), { mode: 384 });
1641
2010
  return identity;
1642
2011
  }
1643
2012
  };
1644
2013
 
1645
2014
  // src/engine/user-defaults.ts
1646
- import fs5 from "fs/promises";
2015
+ import fs7 from "fs/promises";
1647
2016
  import os4 from "os";
1648
- import path6 from "path";
2017
+ import path8 from "path";
1649
2018
  async function readJson(file) {
1650
2019
  try {
1651
- const parsed = JSON.parse(await fs5.readFile(file, "utf8"));
2020
+ const parsed = JSON.parse(await fs7.readFile(file, "utf8"));
1652
2021
  return typeof parsed === "object" && parsed !== null ? parsed : null;
1653
2022
  } catch {
1654
2023
  return null;
@@ -1659,8 +2028,8 @@ function str3(value) {
1659
2028
  }
1660
2029
  async function claudeDefaults(cwd, home = os4.homedir()) {
1661
2030
  const files = [
1662
- ...cwd ? [path6.join(cwd, ".claude", "settings.local.json"), path6.join(cwd, ".claude", "settings.json")] : [],
1663
- path6.join(home, ".claude", "settings.json")
2031
+ ...cwd ? [path8.join(cwd, ".claude", "settings.local.json"), path8.join(cwd, ".claude", "settings.json")] : [],
2032
+ path8.join(home, ".claude", "settings.json")
1664
2033
  ];
1665
2034
  let model = null;
1666
2035
  let effort = null;
@@ -1676,7 +2045,7 @@ async function claudeDefaults(cwd, home = os4.homedir()) {
1676
2045
  async function codexDefaults(home = os4.homedir()) {
1677
2046
  let text;
1678
2047
  try {
1679
- text = await fs5.readFile(path6.join(home, ".codex", "config.toml"), "utf8");
2048
+ text = await fs7.readFile(path8.join(home, ".codex", "config.toml"), "utf8");
1680
2049
  } catch {
1681
2050
  return { model: null, effort: null };
1682
2051
  }
@@ -1700,31 +2069,31 @@ var EFFORT_LEVELS = {
1700
2069
  // src/engine/index.ts
1701
2070
  var RAW_TRANSCRIPT_MAX_BYTES = 200 * 1048576;
1702
2071
  function moduleDir() {
1703
- return path7.dirname(fileURLToPath(import.meta.url));
2072
+ return path9.dirname(fileURLToPath(import.meta.url));
1704
2073
  }
1705
2074
  function resolveWorkerPath() {
1706
2075
  const dir = moduleDir();
1707
2076
  for (const candidate of [
1708
- path7.join(dir, "worker.js"),
1709
- path7.join(dir, "worker.ts"),
1710
- path7.join(dir, "..", "engine", "worker.js"),
1711
- path7.join(dir, "..", "engine", "worker.ts")
2077
+ path9.join(dir, "worker.js"),
2078
+ path9.join(dir, "worker.ts"),
2079
+ path9.join(dir, "..", "engine", "worker.js"),
2080
+ path9.join(dir, "..", "engine", "worker.ts")
1712
2081
  ]) {
1713
2082
  if (existsSync(candidate)) return candidate;
1714
2083
  }
1715
2084
  throw new Error(`[coding-sessions] The driver worker is missing next to ${dir}.`);
1716
2085
  }
1717
2086
  async function installShim(dataDir) {
1718
- const source = path7.resolve(moduleDir(), "..", "..", "bin", "cli-shim.sh");
2087
+ const source = path9.resolve(moduleDir(), "..", "..", "bin", "cli-shim.sh");
1719
2088
  if (!existsSync(source)) throw new Error(`[coding-sessions] The CLI shim is missing at ${source}.`);
1720
- const body = await fs6.readFile(source, "utf8");
1721
- const target = path7.join(dataDir, "bin", "cli-shim.sh");
1722
- const current = await fs6.readFile(target, "utf8").catch(() => null);
2089
+ const body = await fs8.readFile(source, "utf8");
2090
+ const target = path9.join(dataDir, "bin", "cli-shim.sh");
2091
+ const current = await fs8.readFile(target, "utf8").catch(() => null);
1723
2092
  if (current !== body) {
1724
- await fs6.mkdir(path7.dirname(target), { recursive: true });
1725
- await fs6.writeFile(target, body, { mode: 493 });
2093
+ await fs8.mkdir(path9.dirname(target), { recursive: true });
2094
+ await fs8.writeFile(target, body, { mode: 493 });
1726
2095
  }
1727
- await fs6.chmod(target, 493);
2096
+ await fs8.chmod(target, 493);
1728
2097
  return target;
1729
2098
  }
1730
2099
  function nowIso() {
@@ -1824,29 +2193,30 @@ function describeAuthStatus(provider, probe) {
1824
2193
  };
1825
2194
  }
1826
2195
  async function createCodingSessionsEngine(options = {}) {
1827
- const dataDir = options.dataDir ?? path7.join(os5.homedir(), ".matrx", "coding-sessions");
1828
- const stateDir = path7.join(dataDir, "paperclip-state");
2196
+ const dataDir = options.dataDir ?? path9.join(os5.homedir(), ".matrx", "coding-sessions");
2197
+ const stateDir = path9.join(dataDir, "paperclip-state");
1829
2198
  const homes = options.homes ?? defaultProviderHomes();
1830
2199
  const index = new RunIndex(dataDir);
1831
2200
  await index.load();
1832
2201
  const preferences = new PreferenceStore(dataDir);
1833
2202
  await preferences.load();
1834
- await fs6.mkdir(stateDir, { recursive: true });
2203
+ await fs8.mkdir(stateDir, { recursive: true });
1835
2204
  const identity = await index.identity();
2205
+ const agents = createAgentConnection(options.agents, dataDir);
1836
2206
  const backups = options.rawBackup ? createRawBackupScheduler({
1837
2207
  sink: options.rawBackup,
1838
2208
  findFile: (provider, providerSessionId) => findSessionFile(provider, providerSessionId, homes),
1839
2209
  onError: (error) => console.warn("[coding-sessions] raw transcript backup failed:", error)
1840
2210
  }) : null;
1841
2211
  const discoveredSeen = /* @__PURE__ */ new Map();
1842
- const healthFile = path7.join(dataDir, "provider-health.json");
2212
+ const healthFile = path9.join(dataDir, "provider-health.json");
1843
2213
  let health = {};
1844
2214
  try {
1845
- health = JSON.parse(await fs6.readFile(healthFile, "utf8"));
2215
+ health = JSON.parse(await fs8.readFile(healthFile, "utf8"));
1846
2216
  } catch {
1847
2217
  health = {};
1848
2218
  }
1849
- const saveHealth = () => fs6.writeFile(healthFile, JSON.stringify(health), { mode: 384 }).catch(() => void 0);
2219
+ const saveHealth = () => fs8.writeFile(healthFile, JSON.stringify(health), { mode: 384 }).catch(() => void 0);
1850
2220
  function noteRunOutcome(provider, status, texts) {
1851
2221
  if (status === "completed") {
1852
2222
  if (health[provider]) {
@@ -1963,7 +2333,7 @@ async function createCodingSessionsEngine(options = {}) {
1963
2333
  async function startRun(session, prompt) {
1964
2334
  const provider = session.provider;
1965
2335
  const realCommand = await commandFor(provider);
1966
- const runId = randomUUID3();
2336
+ const runId = randomUUID4();
1967
2337
  const model = session.model ?? (provider === "claude" ? (await userDefaults(provider, session.cwd)).model ?? null : null);
1968
2338
  const run = {
1969
2339
  runId,
@@ -2001,7 +2371,26 @@ async function createCodingSessionsEngine(options = {}) {
2001
2371
  let fileOffset = null;
2002
2372
  if (session.providerSessionId) {
2003
2373
  const file = await findSessionFile(provider, session.providerSessionId, homes);
2004
- if (file) fileOffset = (await fs6.stat(file).catch(() => null))?.size ?? null;
2374
+ if (file) fileOffset = (await fs8.stat(file).catch(() => null))?.size ?? null;
2375
+ }
2376
+ let agentArgs = [];
2377
+ if (session.agent) {
2378
+ const bundle = await agents.readSessionBundle(session.id);
2379
+ if (!bundle) {
2380
+ const failed = saveRun({
2381
+ ...run,
2382
+ status: "failed",
2383
+ endedAt: nowIso(),
2384
+ error: `The agent ${session.agent.name} could not be loaded on this computer anymore. Start a new session to load it again.`
2385
+ });
2386
+ const current = index.session(session.id);
2387
+ if (current) saveSession({ ...current, lastRunStatus: "failed" });
2388
+ return failed;
2389
+ }
2390
+ const attachment = await writeAgentAttachment(agents.sessionDir(session.id), bundle, {
2391
+ mcpUrl: await agents.mcpUrlFor(session.id)
2392
+ });
2393
+ agentArgs = agentArgsFor(provider, attachment, { resuming: session.providerSessionId !== null });
2005
2394
  }
2006
2395
  const child = forkWorker();
2007
2396
  const liveRun = {
@@ -2175,7 +2564,8 @@ async function createCodingSessionsEngine(options = {}) {
2175
2564
  resumeSessionId: session.providerSessionId,
2176
2565
  shimPath,
2177
2566
  realCommand,
2178
- identity
2567
+ identity,
2568
+ agentArgs
2179
2569
  });
2180
2570
  return run;
2181
2571
  }
@@ -2194,7 +2584,7 @@ async function createCodingSessionsEngine(options = {}) {
2194
2584
  }, 3e3);
2195
2585
  }
2196
2586
  async function pruneDriverScratch() {
2197
- const runsDir = path7.join(
2587
+ const runsDir = path9.join(
2198
2588
  stateDir,
2199
2589
  "instances",
2200
2590
  "default",
@@ -2205,9 +2595,9 @@ async function createCodingSessionsEngine(options = {}) {
2205
2595
  "claude-runtime",
2206
2596
  "runs"
2207
2597
  );
2208
- const entries = await fs6.readdir(runsDir).catch(() => []);
2598
+ const entries = await fs8.readdir(runsDir).catch(() => []);
2209
2599
  await Promise.all(
2210
- entries.filter((name) => !live.has(name)).map((name) => fs6.rm(path7.join(runsDir, name), { recursive: true, force: true }).catch(() => void 0))
2600
+ entries.filter((name) => !live.has(name)).map((name) => fs8.rm(path9.join(runsDir, name), { recursive: true, force: true }).catch(() => void 0))
2211
2601
  );
2212
2602
  }
2213
2603
  async function probe() {
@@ -2248,7 +2638,7 @@ async function createCodingSessionsEngine(options = {}) {
2248
2638
  function newSession(input) {
2249
2639
  const at = nowIso();
2250
2640
  return {
2251
- id: randomUUID3(),
2641
+ id: randomUUID4(),
2252
2642
  provider: input.provider,
2253
2643
  providerSessionId: input.providerSessionId,
2254
2644
  cwd: input.cwd,
@@ -2266,17 +2656,17 @@ async function createCodingSessionsEngine(options = {}) {
2266
2656
  function resolveFolder(raw) {
2267
2657
  const trimmed = raw.trim();
2268
2658
  if (!trimmed) return null;
2269
- return path7.resolve(trimmed.replace(/^~(?=$|\/)/, os5.homedir()));
2659
+ return path9.resolve(trimmed.replace(/^~(?=$|\/)/, os5.homedir()));
2270
2660
  }
2271
2661
  async function checkFolder(raw) {
2272
2662
  const target = resolveFolder(raw);
2273
2663
  if (!target) return { path: "", exists: false, isDirectory: false };
2274
- const stat = await fs6.stat(target).catch(() => null);
2664
+ const stat = await fs8.stat(target).catch(() => null);
2275
2665
  return { path: target, exists: stat !== null, isDirectory: stat?.isDirectory() === true };
2276
2666
  }
2277
2667
  async function assertFolder(cwd) {
2278
- const resolved = path7.resolve(cwd.replace(/^~(?=$|\/)/, os5.homedir()));
2279
- const stat = await fs6.stat(resolved).catch(() => null);
2668
+ const resolved = path9.resolve(cwd.replace(/^~(?=$|\/)/, os5.homedir()));
2669
+ const stat = await fs8.stat(resolved).catch(() => null);
2280
2670
  if (!stat?.isDirectory()) {
2281
2671
  throw new Error(`The folder ${resolved} doesn't exist on this computer.`);
2282
2672
  }
@@ -2338,7 +2728,7 @@ async function createCodingSessionsEngine(options = {}) {
2338
2728
  folders.map(async ([folder, lastUsedAt]) => ({
2339
2729
  path: folder,
2340
2730
  lastUsedAt,
2341
- exists: (await fs6.stat(folder).catch(() => null))?.isDirectory() === true
2731
+ exists: (await fs8.stat(folder).catch(() => null))?.isDirectory() === true
2342
2732
  }))
2343
2733
  );
2344
2734
  },
@@ -2383,7 +2773,7 @@ async function createCodingSessionsEngine(options = {}) {
2383
2773
  async createFolder(raw) {
2384
2774
  const target = resolveFolder(raw);
2385
2775
  if (!target) throw new Error("Type a folder path first.");
2386
- await fs6.mkdir(target, { recursive: true });
2776
+ await fs8.mkdir(target, { recursive: true });
2387
2777
  return checkFolder(target);
2388
2778
  },
2389
2779
  async sessionActivity(provider, providerSessionId) {
@@ -2421,31 +2811,35 @@ async function createCodingSessionsEngine(options = {}) {
2421
2811
  reason: `${PROVIDER_LABEL[provider]} has no saved transcript for this chat on this computer.`
2422
2812
  };
2423
2813
  }
2424
- const stat = await fs6.stat(file);
2814
+ const stat = await fs8.stat(file);
2425
2815
  if (stat.size > RAW_TRANSCRIPT_MAX_BYTES) {
2426
2816
  return {
2427
2817
  available: false,
2428
2818
  reason: `This transcript is ${formatFileSize(stat.size)}, larger than the ${formatFileSize(RAW_TRANSCRIPT_MAX_BYTES)} this page can hand over. It is at ${file}.`
2429
2819
  };
2430
2820
  }
2431
- const content = await fs6.readFile(file, "utf8");
2432
- return { available: true, fileName: path7.basename(file), bytes: stat.size, content };
2821
+ const content = await fs8.readFile(file, "utf8");
2822
+ return { available: true, fileName: path9.basename(file), bytes: stat.size, content };
2433
2823
  },
2434
2824
  async start(input) {
2435
2825
  const prompt = input.prompt.trim();
2436
2826
  if (!prompt) throw new Error("Type what you want done before starting.");
2437
2827
  const cwd = await assertFolder(input.cwd);
2438
- const session = saveSession(
2439
- newSession({
2440
- provider: input.provider,
2441
- cwd,
2442
- title: titleFrom(prompt),
2443
- model: input.model?.trim() || null,
2444
- effort: input.effort?.trim() || null,
2445
- origin: "started",
2446
- providerSessionId: null
2447
- })
2448
- );
2828
+ const bundle = input.agent ? await agents.loadBundle(input.agent) : null;
2829
+ const draft = newSession({
2830
+ provider: input.provider,
2831
+ cwd,
2832
+ title: titleFrom(prompt),
2833
+ model: input.model?.trim() || null,
2834
+ effort: input.effort?.trim() || null,
2835
+ origin: "started",
2836
+ providerSessionId: null
2837
+ });
2838
+ if (bundle) {
2839
+ await agents.saveSessionBundle(draft.id, bundle);
2840
+ draft.agent = sessionAgentRecord(bundle, { toolsServed: await agents.mcpUrlFor(draft.id) !== null });
2841
+ }
2842
+ const session = saveSession(draft);
2449
2843
  const run = await startRun(session, prompt);
2450
2844
  return { session: index.session(session.id) ?? session, run, queued: false };
2451
2845
  },
@@ -2514,6 +2908,21 @@ async function createCodingSessionsEngine(options = {}) {
2514
2908
  if (opts && "effort" in opts) next.effort = typeof opts.effort === "string" && opts.effort.trim() ? opts.effort.trim() : null;
2515
2909
  return saveSession(next);
2516
2910
  },
2911
+ async agentAccess() {
2912
+ return agents.access();
2913
+ },
2914
+ async agentCatalogRpc(request) {
2915
+ return agents.catalogRpc(request);
2916
+ },
2917
+ async agentDetails(agentId) {
2918
+ return agents.details(String(agentId ?? ""));
2919
+ },
2920
+ setMcpBaseUrl(baseUrl) {
2921
+ agents.setMcpBaseUrl(baseUrl);
2922
+ },
2923
+ async handleMcp(secret, body) {
2924
+ return agents.handleMcp(secret, body);
2925
+ },
2517
2926
  async dispose() {
2518
2927
  for (const liveRun of live.values()) {
2519
2928
  liveRun.stopRequested = true;
@@ -2583,7 +2992,10 @@ var BRIDGE_METHODS = [
2583
2992
  "setChatPreference",
2584
2993
  "searchChats",
2585
2994
  "openFolder",
2586
- "setSessionOptions"
2995
+ "setSessionOptions",
2996
+ "agentAccess",
2997
+ "agentCatalogRpc",
2998
+ "agentDetails"
2587
2999
  ];
2588
3000
  var BridgeError = class extends Error {
2589
3001
  constructor(message, code) {
@@ -2627,14 +3039,14 @@ var MIME = {
2627
3039
  ".map": "application/json"
2628
3040
  };
2629
3041
  function moduleDir2() {
2630
- return path8.dirname(fileURLToPath2(import.meta.url));
3042
+ return path10.dirname(fileURLToPath2(import.meta.url));
2631
3043
  }
2632
3044
  function defaultUiDir() {
2633
3045
  for (const candidate of [
2634
- path8.resolve(moduleDir2(), "..", "ui"),
2635
- path8.resolve(moduleDir2(), "..", "..", "dist", "ui")
3046
+ path10.resolve(moduleDir2(), "..", "ui"),
3047
+ path10.resolve(moduleDir2(), "..", "..", "dist", "ui")
2636
3048
  ]) {
2637
- if (existsSync2(path8.join(candidate, "index.html"))) return candidate;
3049
+ if (existsSync2(path10.join(candidate, "index.html"))) return candidate;
2638
3050
  }
2639
3051
  return null;
2640
3052
  }
@@ -2657,19 +3069,19 @@ async function serveStatic(uiDir, req, res) {
2657
3069
  return;
2658
3070
  }
2659
3071
  const requested = decodeURIComponent(url.pathname);
2660
- let file = path8.resolve(uiDir, `.${requested}`);
3072
+ let file = path10.resolve(uiDir, `.${requested}`);
2661
3073
  if (!file.startsWith(uiDir)) {
2662
3074
  res.writeHead(403).end();
2663
3075
  return;
2664
3076
  }
2665
- const stat = await fs7.stat(file).catch(() => null);
2666
- if (!stat || stat.isDirectory()) file = path8.join(uiDir, "index.html");
2667
- const body = await fs7.readFile(file).catch(() => null);
3077
+ const stat = await fs9.stat(file).catch(() => null);
3078
+ if (!stat || stat.isDirectory()) file = path10.join(uiDir, "index.html");
3079
+ const body = await fs9.readFile(file).catch(() => null);
2668
3080
  if (!body) {
2669
3081
  res.writeHead(404).end();
2670
3082
  return;
2671
3083
  }
2672
- const ext = path8.extname(file);
3084
+ const ext = path10.extname(file);
2673
3085
  res.writeHead(200, {
2674
3086
  "content-type": MIME[ext] ?? "application/octet-stream",
2675
3087
  "cache-control": ext === ".html" ? "no-store" : "public, max-age=31536000, immutable",
@@ -2678,6 +3090,46 @@ async function serveStatic(uiDir, req, res) {
2678
3090
  });
2679
3091
  res.end(body);
2680
3092
  }
3093
+ var MAX_MCP_BODY = 4 * 1024 * 1024;
3094
+ async function handleMcp(engine, secret, req, res, allowedOrigin) {
3095
+ if (!allowedOrigin(req.headers.origin) || !/^[A-Za-z0-9_-]{16,}$/.test(secret)) {
3096
+ res.writeHead(403).end();
3097
+ return;
3098
+ }
3099
+ if (req.method !== "POST") {
3100
+ res.writeHead(405, { allow: "POST" }).end();
3101
+ return;
3102
+ }
3103
+ const chunks = [];
3104
+ let size = 0;
3105
+ for await (const chunk of req) {
3106
+ size += chunk.length;
3107
+ if (size > MAX_MCP_BODY) {
3108
+ res.writeHead(413).end();
3109
+ return;
3110
+ }
3111
+ chunks.push(chunk);
3112
+ }
3113
+ let body;
3114
+ try {
3115
+ body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
3116
+ } catch {
3117
+ res.writeHead(400, { "content-type": "application/json" });
3118
+ res.end(JSON.stringify({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error." } }));
3119
+ return;
3120
+ }
3121
+ const reply = engine.handleMcp ? await engine.handleMcp(secret, body) : null;
3122
+ if (!reply) {
3123
+ res.writeHead(404).end();
3124
+ return;
3125
+ }
3126
+ if (reply.body === void 0) {
3127
+ res.writeHead(reply.status).end();
3128
+ return;
3129
+ }
3130
+ res.writeHead(reply.status, { "content-type": "application/json" });
3131
+ res.end(JSON.stringify(reply.body));
3132
+ }
2681
3133
  function attachSocket(socket, bridge) {
2682
3134
  const unsubscribe = bridge.subscribe((event) => {
2683
3135
  if (socket.readyState === socket.OPEN) socket.send(JSON.stringify({ event }));
@@ -2708,7 +3160,7 @@ function attachSocket(socket, bridge) {
2708
3160
  socket.on("close", unsubscribe);
2709
3161
  }
2710
3162
  async function startCodingSessionsHost(options = {}) {
2711
- const token = options.token ?? randomBytes(24).toString("base64url");
3163
+ const token = options.token ?? randomBytes2(24).toString("base64url");
2712
3164
  const engine = options.engine ?? await createCodingSessionsEngine(options);
2713
3165
  const bridge = createHostBridge(engine);
2714
3166
  const uiDir = options.uiDir ?? defaultUiDir();
@@ -2719,6 +3171,13 @@ async function startCodingSessionsHost(options = {}) {
2719
3171
  };
2720
3172
  const server = http.createServer((req, res) => {
2721
3173
  const url2 = new URL(req.url ?? "/", `http://${HOST}`);
3174
+ if (url2.pathname.startsWith("/mcp/")) {
3175
+ handleMcp(engine, url2.pathname.slice("/mcp/".length), req, res, allowedOrigin).catch(() => {
3176
+ if (!res.headersSent) res.writeHead(500);
3177
+ res.end();
3178
+ });
3179
+ return;
3180
+ }
2722
3181
  if (url2.pathname === "/health") {
2723
3182
  res.writeHead(200, { "content-type": "application/json" });
2724
3183
  res.end(JSON.stringify({ ok: true, service: "@ai-matrx/coding-sessions" }));
@@ -2749,6 +3208,7 @@ async function startCodingSessionsHost(options = {}) {
2749
3208
  server.off("error", reject);
2750
3209
  const address = server.address();
2751
3210
  if (address && typeof address === "object") port = address.port;
3211
+ engine.setMcpBaseUrl?.(`http://${HOST}:${port}`);
2752
3212
  resolve();
2753
3213
  });
2754
3214
  });
@@ -2759,6 +3219,7 @@ async function startCodingSessionsHost(options = {}) {
2759
3219
  token,
2760
3220
  engine,
2761
3221
  async close() {
3222
+ engine.setMcpBaseUrl?.(null);
2762
3223
  for (const client of wss.clients) client.terminate();
2763
3224
  await new Promise((resolve) => wss.close(() => resolve()));
2764
3225
  await new Promise((resolve) => server.close(() => resolve()));