@ai-matrx/coding-sessions 0.1.81 → 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,16 +1,16 @@
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 path7 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
- import { execFile as execFile2, fork } from "child_process";
12
- import { randomUUID as randomUUID3 } from "crypto";
13
- import { accessSync, constants as fsConstants, existsSync } from "fs";
11
+ import { execFile as execFile3, fork } from "child_process";
12
+ import { randomUUID as randomUUID4 } from "crypto";
13
+ import { existsSync } from "fs";
14
14
  import { formatFileSize } from "@ai-matrx/kit/format";
15
15
 
16
16
  // src/engine/raw-backup.ts
@@ -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";
75
- import os4 from "os";
76
- import path6 from "path";
443
+ import fs8 from "fs/promises";
444
+ import os5 from "os";
445
+ import path9 from "path";
77
446
  import { fileURLToPath } from "url";
78
447
 
79
448
  // src/normalize/claude.ts
@@ -739,9 +1108,177 @@ function describeProblem(provider, problem) {
739
1108
  };
740
1109
  }
741
1110
 
742
- // src/engine/clean-env.ts
1111
+ // src/engine/binaries.ts
1112
+ import { execFile } from "child_process";
1113
+ import { accessSync, constants as fsConstants, readdirSync, readFileSync, realpathSync } from "fs";
743
1114
  import os from "os";
744
- import path from "path";
1115
+ import path3 from "path";
1116
+ var SOURCE_LABEL = {
1117
+ codex: { "desktop-app": "from ChatGPT app", path: "from PATH", "npm-global": "from npm", configured: "set by host" },
1118
+ claude: { "desktop-app": "from Claude app", path: "from PATH", "npm-global": "from npm", configured: "set by host" }
1119
+ };
1120
+ function sourceLabel(provider, source) {
1121
+ return SOURCE_LABEL[provider][source];
1122
+ }
1123
+ function parseVersion(text) {
1124
+ const match = /(\d+)\.(\d+)\.(\d+)(?:[-.][0-9A-Za-z.]+)?/.exec(text ?? "");
1125
+ return match ? match[0] : null;
1126
+ }
1127
+ function compareVersions(a, b) {
1128
+ const [aCore = "", aPre] = a.split("-", 2);
1129
+ const [bCore = "", bPre] = b.split("-", 2);
1130
+ const pa = aCore.split(".").map((n) => Number.parseInt(n, 10) || 0);
1131
+ const pb = bCore.split(".").map((n) => Number.parseInt(n, 10) || 0);
1132
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
1133
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
1134
+ if (d !== 0) return d < 0 ? -1 : 1;
1135
+ }
1136
+ if (aPre && !bPre) return -1;
1137
+ if (!aPre && bPre) return 1;
1138
+ return 0;
1139
+ }
1140
+ function pickBinary(candidates) {
1141
+ let best = null;
1142
+ for (const c of candidates) {
1143
+ if (!best) {
1144
+ best = c;
1145
+ continue;
1146
+ }
1147
+ if (c.version && (!best.version || compareVersions(c.version, best.version) > 0)) best = c;
1148
+ }
1149
+ return best;
1150
+ }
1151
+ function executable(file) {
1152
+ try {
1153
+ accessSync(file, fsConstants.X_OK);
1154
+ return true;
1155
+ } catch {
1156
+ return false;
1157
+ }
1158
+ }
1159
+ function exeName(name, platform) {
1160
+ return platform === "win32" ? [`${name}.exe`, `${name}.cmd`, name] : [name];
1161
+ }
1162
+ function allOnPath(name, pathValue, platform = process.platform) {
1163
+ const found = [];
1164
+ for (const dir of pathValue.split(path3.delimiter)) {
1165
+ if (!dir) continue;
1166
+ for (const file of exeName(name, platform)) {
1167
+ const candidate = path3.join(dir, file);
1168
+ if (executable(candidate)) {
1169
+ found.push(candidate);
1170
+ break;
1171
+ }
1172
+ }
1173
+ }
1174
+ return found;
1175
+ }
1176
+ function desktopAppCandidates(provider, platform, home, applications) {
1177
+ if (platform !== "darwin") return [];
1178
+ if (provider === "codex") {
1179
+ return applications.map(
1180
+ (apps) => path3.join(apps, "ChatGPT.app", "Contents", "Resources", "codex-cli", "bin", "codex")
1181
+ );
1182
+ }
1183
+ const root = path3.join(home, "Library", "Application Support", "Claude", "claude-code");
1184
+ let versions = [];
1185
+ try {
1186
+ versions = readdirSync(root);
1187
+ } catch {
1188
+ return [];
1189
+ }
1190
+ return versions.map((v) => path3.join(root, v, "claude.app", "Contents", "MacOS", "claude"));
1191
+ }
1192
+ function npmGlobalPrefix(env) {
1193
+ const fromEnv = env.npm_config_prefix ?? env.NPM_CONFIG_PREFIX;
1194
+ if (fromEnv) return Promise.resolve(fromEnv);
1195
+ const npm = allOnPath("npm", env.PATH ?? "")[0];
1196
+ if (!npm) return Promise.resolve(null);
1197
+ return new Promise((resolve) => {
1198
+ const child = execFile(npm, ["prefix", "-g"], { timeout: 5e3, env }, (error, stdout) => {
1199
+ resolve(error ? null : String(stdout).trim() || null);
1200
+ });
1201
+ child.stdin?.end();
1202
+ });
1203
+ }
1204
+ function declaredVersion(provider, source, file) {
1205
+ if (source !== "desktop-app") return null;
1206
+ if (provider === "codex") {
1207
+ try {
1208
+ const pkg = JSON.parse(readFileSync(path3.join(path3.dirname(path3.dirname(file)), "codex-package.json"), "utf8"));
1209
+ return typeof pkg.version === "string" ? parseVersion(pkg.version) : null;
1210
+ } catch {
1211
+ return null;
1212
+ }
1213
+ }
1214
+ return parseVersion(path3.basename(path3.resolve(file, "..", "..", "..", "..")));
1215
+ }
1216
+ function runVersion(file, env) {
1217
+ return new Promise((resolve) => {
1218
+ const child = execFile(file, ["--version"], { timeout: 8e3, env }, (error, stdout) => {
1219
+ resolve(error ? null : parseVersion(String(stdout)));
1220
+ });
1221
+ child.stdin?.end();
1222
+ });
1223
+ }
1224
+ async function discoverBinaries(provider, options) {
1225
+ const platform = options.platform ?? process.platform;
1226
+ const home = options.home ?? options.env.HOME ?? os.homedir();
1227
+ const listed2 = [
1228
+ ...desktopAppCandidates(provider, platform, home, options.applications ?? ["/Applications", path3.join(home, "Applications")]).filter(executable).map((p) => ({ path: p, source: "desktop-app" })),
1229
+ ...allOnPath(provider, options.env.PATH ?? "", platform).map((p) => ({ path: p, source: "path" }))
1230
+ ];
1231
+ const prefix = await npmGlobalPrefix(options.env);
1232
+ if (prefix) {
1233
+ const dir = platform === "win32" ? prefix : path3.join(prefix, "bin");
1234
+ for (const file of exeName(provider, platform)) {
1235
+ const candidate = path3.join(dir, file);
1236
+ if (executable(candidate)) {
1237
+ listed2.push({ path: candidate, source: "npm-global" });
1238
+ break;
1239
+ }
1240
+ }
1241
+ }
1242
+ const seen = /* @__PURE__ */ new Set();
1243
+ const unique = listed2.filter((c) => {
1244
+ let real = c.path;
1245
+ try {
1246
+ real = realpathSync(c.path);
1247
+ } catch {
1248
+ }
1249
+ if (seen.has(real)) return false;
1250
+ seen.add(real);
1251
+ return true;
1252
+ });
1253
+ return Promise.all(
1254
+ unique.map(async (c) => ({
1255
+ ...c,
1256
+ version: declaredVersion(provider, c.source, c.path) ?? await runVersion(c.path, options.env)
1257
+ }))
1258
+ );
1259
+ }
1260
+ async function resolveBinary(provider, options) {
1261
+ const candidates = options.discover === false ? [] : await discoverBinaries(provider, options);
1262
+ const best = pickBinary(candidates);
1263
+ const inUse = options.forced ? {
1264
+ path: options.forced,
1265
+ source: "configured",
1266
+ version: candidates.find((c) => c.path === options.forced)?.version ?? (options.discover === false ? null : await runVersion(options.forced, options.env))
1267
+ } : best;
1268
+ if (!inUse) return null;
1269
+ const newer = best && best.path !== inUse.path && best.version && (!inUse.version || compareVersions(best.version, inUse.version) > 0) ? { path: best.path, version: best.version, sourceLabel: sourceLabel(provider, best.source) } : null;
1270
+ return {
1271
+ path: inUse.path,
1272
+ version: inUse.version,
1273
+ source: inUse.source,
1274
+ sourceLabel: sourceLabel(provider, inUse.source),
1275
+ newer
1276
+ };
1277
+ }
1278
+
1279
+ // src/engine/clean-env.ts
1280
+ import os2 from "os";
1281
+ import path4 from "path";
745
1282
  var ALLOWED_NAMES = /* @__PURE__ */ new Set([
746
1283
  "HOME",
747
1284
  "USER",
@@ -763,11 +1300,11 @@ var ALLOWED_NAMES = /* @__PURE__ */ new Set([
763
1300
  var ALLOWED_PREFIXES = ["LC_"];
764
1301
  function wellKnownBinDirs(home) {
765
1302
  return [
766
- path.join(home, ".local", "bin"),
767
- path.join(home, ".claude", "local"),
768
- path.join(home, ".npm-global", "bin"),
769
- path.join(home, ".bun", "bin"),
770
- path.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"),
771
1308
  "/opt/homebrew/bin",
772
1309
  "/usr/local/bin",
773
1310
  "/usr/bin",
@@ -785,31 +1322,31 @@ function buildCleanEnv(hostEnv, extra = {}) {
785
1322
  if (typeof value !== "string") continue;
786
1323
  if (isAllowedEnvName(name)) out[name] = value;
787
1324
  }
788
- const home = out.HOME ?? os.homedir();
1325
+ const home = out.HOME ?? os2.homedir();
789
1326
  out.HOME = home;
790
- const hostPath = (hostEnv.PATH ?? "").split(path.delimiter).filter(Boolean);
1327
+ const hostPath = (hostEnv.PATH ?? "").split(path4.delimiter).filter(Boolean);
791
1328
  const merged = [];
792
1329
  for (const dir of [...hostPath, ...wellKnownBinDirs(home)]) {
793
1330
  if (!merged.includes(dir)) merged.push(dir);
794
1331
  }
795
- out.PATH = merged.join(path.delimiter);
1332
+ out.PATH = merged.join(path4.delimiter);
796
1333
  for (const [name, value] of Object.entries(extra)) out[name] = value;
797
1334
  return out;
798
1335
  }
799
1336
 
800
1337
  // src/engine/files.ts
801
1338
  import { createReadStream } from "fs";
802
- import fs2 from "fs/promises";
803
- import os2 from "os";
804
- import path2 from "path";
1339
+ import fs4 from "fs/promises";
1340
+ import os3 from "os";
1341
+ import path5 from "path";
805
1342
 
806
1343
  // src/engine/activity.ts
807
- import { execFile } from "child_process";
1344
+ import { execFile as execFile2 } from "child_process";
808
1345
  var RECENT_WRITE_MS = 2 * 60 * 1e3;
809
1346
  var OPEN_TOOL_WINDOW_MS = 15 * 60 * 1e3;
810
1347
  function listProcesses() {
811
1348
  return new Promise((resolve) => {
812
- execFile("ps", ["-axww", "-o", "pid=,args="], { maxBuffer: 64 * 1024 * 1024, timeout: 5e3 }, (error, stdout) => {
1349
+ execFile2("ps", ["-axww", "-o", "pid=,args="], { maxBuffer: 64 * 1024 * 1024, timeout: 5e3 }, (error, stdout) => {
813
1350
  if (error) {
814
1351
  resolve([]);
815
1352
  return;
@@ -868,17 +1405,17 @@ function describeActivity(input) {
868
1405
  }
869
1406
 
870
1407
  // src/engine/files.ts
871
- function defaultProviderHomes(home = os2.homedir()) {
1408
+ function defaultProviderHomes(home = os3.homedir()) {
872
1409
  return {
873
- claudeProjects: path2.join(home, ".claude", "projects"),
874
- codexSessions: path2.join(home, ".codex", "sessions"),
875
- codexIndex: path2.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")
876
1413
  };
877
1414
  }
878
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;
879
1416
  async function safeReaddir(dir) {
880
1417
  try {
881
- return await fs2.readdir(dir, { withFileTypes: true });
1418
+ return await fs4.readdir(dir, { withFileTypes: true });
882
1419
  } catch {
883
1420
  return [];
884
1421
  }
@@ -887,7 +1424,7 @@ async function statCandidate(provider, file) {
887
1424
  const match = UUIDISH.exec(file);
888
1425
  if (!match?.[1]) return null;
889
1426
  try {
890
- const stat = await fs2.stat(file);
1427
+ const stat = await fs4.stat(file);
891
1428
  return { provider, file, id: match[1], mtimeMs: stat.mtimeMs, size: stat.size };
892
1429
  } catch {
893
1430
  return null;
@@ -897,10 +1434,10 @@ async function claudeCandidates(root) {
897
1434
  const out = [];
898
1435
  for (const project of await safeReaddir(root)) {
899
1436
  if (!project.isDirectory()) continue;
900
- const dir = path2.join(root, project.name);
1437
+ const dir = path5.join(root, project.name);
901
1438
  const entries = await safeReaddir(dir);
902
1439
  const stats = await Promise.all(
903
- entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl")).map((e) => statCandidate("claude", path2.join(dir, e.name)))
1440
+ entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl")).map((e) => statCandidate("claude", path5.join(dir, e.name)))
904
1441
  );
905
1442
  for (const s of stats) if (s) out.push(s);
906
1443
  }
@@ -910,14 +1447,14 @@ async function codexCandidates(root) {
910
1447
  const out = [];
911
1448
  for (const year of await safeReaddir(root)) {
912
1449
  if (!year.isDirectory()) continue;
913
- for (const month of await safeReaddir(path2.join(root, year.name))) {
1450
+ for (const month of await safeReaddir(path5.join(root, year.name))) {
914
1451
  if (!month.isDirectory()) continue;
915
- for (const day of await safeReaddir(path2.join(root, year.name, month.name))) {
1452
+ for (const day of await safeReaddir(path5.join(root, year.name, month.name))) {
916
1453
  if (!day.isDirectory()) continue;
917
- const dir = path2.join(root, year.name, month.name, day.name);
1454
+ const dir = path5.join(root, year.name, month.name, day.name);
918
1455
  const entries = await safeReaddir(dir);
919
1456
  const stats = await Promise.all(
920
- entries.filter((e) => e.isFile() && e.name.startsWith("rollout-") && e.name.endsWith(".jsonl")).map((e) => statCandidate("codex", path2.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)))
921
1458
  );
922
1459
  for (const s of stats) if (s) out.push(s);
923
1460
  }
@@ -926,7 +1463,7 @@ async function codexCandidates(root) {
926
1463
  return out;
927
1464
  }
928
1465
  async function readSlice(file, start, length) {
929
- const handle = await fs2.open(file, "r");
1466
+ const handle = await fs4.open(file, "r");
930
1467
  try {
931
1468
  const buffer = Buffer.alloc(length);
932
1469
  const { bytesRead } = await handle.read(buffer, 0, length, start);
@@ -999,9 +1536,9 @@ async function readCodexIndex(file) {
999
1536
  const names = /* @__PURE__ */ new Map();
1000
1537
  let text;
1001
1538
  try {
1002
- const stat = await fs2.stat(file);
1539
+ const stat = await fs4.stat(file);
1003
1540
  const budget = 4 * 1024 * 1024;
1004
- 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);
1005
1542
  } catch {
1006
1543
  return names;
1007
1544
  }
@@ -1122,7 +1659,7 @@ var MAX_HISTORY_BUDGET = 256 * 1024 * 1024;
1122
1659
  async function readSessionFile(provider, file, endOffset, maxBytes = HISTORY_BUDGET) {
1123
1660
  let size;
1124
1661
  try {
1125
- size = (await fs2.stat(file)).size;
1662
+ size = (await fs4.stat(file)).size;
1126
1663
  } catch {
1127
1664
  return {
1128
1665
  items: [],
@@ -1176,9 +1713,9 @@ async function describeSessionFile(candidate, codexNames) {
1176
1713
  }
1177
1714
 
1178
1715
  // src/engine/preferences.ts
1179
- import { randomUUID } from "crypto";
1180
- import fs3 from "fs/promises";
1181
- import path3 from "path";
1716
+ import { randomUUID as randomUUID2 } from "crypto";
1717
+ import fs5 from "fs/promises";
1718
+ import path6 from "path";
1182
1719
  var PreferenceStore = class {
1183
1720
  constructor(dir) {
1184
1721
  this.dir = dir;
@@ -1187,11 +1724,11 @@ var PreferenceStore = class {
1187
1724
  data = {};
1188
1725
  writing = Promise.resolve();
1189
1726
  get file() {
1190
- return path3.join(this.dir, "preferences.json");
1727
+ return path6.join(this.dir, "preferences.json");
1191
1728
  }
1192
1729
  async load() {
1193
1730
  try {
1194
- const parsed = JSON.parse(await fs3.readFile(this.file, "utf8"));
1731
+ const parsed = JSON.parse(await fs5.readFile(this.file, "utf8"));
1195
1732
  this.data = typeof parsed === "object" && parsed !== null ? parsed : {};
1196
1733
  } catch {
1197
1734
  this.data = {};
@@ -1229,10 +1766,10 @@ var PreferenceStore = class {
1229
1766
  persist() {
1230
1767
  const snapshot = JSON.stringify(this.data, null, 1);
1231
1768
  this.writing = this.writing.then(async () => {
1232
- await fs3.mkdir(this.dir, { recursive: true });
1233
- const tmp = `${this.file}.${randomUUID()}.tmp`;
1234
- await fs3.writeFile(tmp, snapshot, { mode: 384 });
1235
- 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);
1236
1773
  }).catch(() => {
1237
1774
  });
1238
1775
  }
@@ -1389,9 +1926,9 @@ async function searchLocalChats(query, options = {}) {
1389
1926
  }
1390
1927
 
1391
1928
  // src/engine/store.ts
1392
- import { randomUUID as randomUUID2 } from "crypto";
1393
- import fs4 from "fs/promises";
1394
- import path4 from "path";
1929
+ import { randomUUID as randomUUID3 } from "crypto";
1930
+ import fs6 from "fs/promises";
1931
+ import path7 from "path";
1395
1932
  var MAX_RUNS = 2e3;
1396
1933
  var RunIndex = class {
1397
1934
  constructor(dir) {
@@ -1401,12 +1938,12 @@ var RunIndex = class {
1401
1938
  data = { version: 1, sessions: [], runs: [] };
1402
1939
  writing = Promise.resolve();
1403
1940
  get file() {
1404
- return path4.join(this.dir, "index.json");
1941
+ return path7.join(this.dir, "index.json");
1405
1942
  }
1406
1943
  async load() {
1407
- await fs4.mkdir(this.dir, { recursive: true });
1944
+ await fs6.mkdir(this.dir, { recursive: true });
1408
1945
  try {
1409
- const parsed = JSON.parse(await fs4.readFile(this.file, "utf8"));
1946
+ const parsed = JSON.parse(await fs6.readFile(this.file, "utf8"));
1410
1947
  this.data = {
1411
1948
  version: 1,
1412
1949
  sessions: Array.isArray(parsed.sessions) ? parsed.sessions : [],
@@ -1448,9 +1985,9 @@ var RunIndex = class {
1448
1985
  persist() {
1449
1986
  const snapshot = JSON.stringify(this.data, null, 1);
1450
1987
  this.writing = this.writing.then(async () => {
1451
- const tmp = `${this.file}.${randomUUID2()}.tmp`;
1452
- await fs4.writeFile(tmp, snapshot, { mode: 384 });
1453
- 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);
1454
1991
  }).catch(() => {
1455
1992
  });
1456
1993
  }
@@ -1460,27 +1997,27 @@ var RunIndex = class {
1460
1997
  /** A stable Paperclip agent/company identity for this install, so the
1461
1998
  * driver's scratch state is reused rather than growing per run. */
1462
1999
  async identity() {
1463
- const file = path4.join(this.dir, "identity.json");
2000
+ const file = path7.join(this.dir, "identity.json");
1464
2001
  try {
1465
- const parsed = JSON.parse(await fs4.readFile(file, "utf8"));
2002
+ const parsed = JSON.parse(await fs6.readFile(file, "utf8"));
1466
2003
  if (typeof parsed.companyId === "string" && typeof parsed.agentId === "string") {
1467
2004
  return { companyId: parsed.companyId, agentId: parsed.agentId };
1468
2005
  }
1469
2006
  } catch {
1470
2007
  }
1471
- const identity = { companyId: randomUUID2(), agentId: randomUUID2() };
1472
- 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 });
1473
2010
  return identity;
1474
2011
  }
1475
2012
  };
1476
2013
 
1477
2014
  // src/engine/user-defaults.ts
1478
- import fs5 from "fs/promises";
1479
- import os3 from "os";
1480
- import path5 from "path";
2015
+ import fs7 from "fs/promises";
2016
+ import os4 from "os";
2017
+ import path8 from "path";
1481
2018
  async function readJson(file) {
1482
2019
  try {
1483
- const parsed = JSON.parse(await fs5.readFile(file, "utf8"));
2020
+ const parsed = JSON.parse(await fs7.readFile(file, "utf8"));
1484
2021
  return typeof parsed === "object" && parsed !== null ? parsed : null;
1485
2022
  } catch {
1486
2023
  return null;
@@ -1489,10 +2026,10 @@ async function readJson(file) {
1489
2026
  function str3(value) {
1490
2027
  return typeof value === "string" && value.trim() ? value.trim() : null;
1491
2028
  }
1492
- async function claudeDefaults(cwd, home = os3.homedir()) {
2029
+ async function claudeDefaults(cwd, home = os4.homedir()) {
1493
2030
  const files = [
1494
- ...cwd ? [path5.join(cwd, ".claude", "settings.local.json"), path5.join(cwd, ".claude", "settings.json")] : [],
1495
- path5.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")
1496
2033
  ];
1497
2034
  let model = null;
1498
2035
  let effort = null;
@@ -1505,10 +2042,10 @@ async function claudeDefaults(cwd, home = os3.homedir()) {
1505
2042
  }
1506
2043
  return { model, effort };
1507
2044
  }
1508
- async function codexDefaults(home = os3.homedir()) {
2045
+ async function codexDefaults(home = os4.homedir()) {
1509
2046
  let text;
1510
2047
  try {
1511
- text = await fs5.readFile(path5.join(home, ".codex", "config.toml"), "utf8");
2048
+ text = await fs7.readFile(path8.join(home, ".codex", "config.toml"), "utf8");
1512
2049
  } catch {
1513
2050
  return { model: null, effort: null };
1514
2051
  }
@@ -1532,45 +2069,33 @@ var EFFORT_LEVELS = {
1532
2069
  // src/engine/index.ts
1533
2070
  var RAW_TRANSCRIPT_MAX_BYTES = 200 * 1048576;
1534
2071
  function moduleDir() {
1535
- return path6.dirname(fileURLToPath(import.meta.url));
2072
+ return path9.dirname(fileURLToPath(import.meta.url));
1536
2073
  }
1537
2074
  function resolveWorkerPath() {
1538
2075
  const dir = moduleDir();
1539
2076
  for (const candidate of [
1540
- path6.join(dir, "worker.js"),
1541
- path6.join(dir, "worker.ts"),
1542
- path6.join(dir, "..", "engine", "worker.js"),
1543
- path6.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")
1544
2081
  ]) {
1545
2082
  if (existsSync(candidate)) return candidate;
1546
2083
  }
1547
2084
  throw new Error(`[coding-sessions] The driver worker is missing next to ${dir}.`);
1548
2085
  }
1549
2086
  async function installShim(dataDir) {
1550
- const source = path6.resolve(moduleDir(), "..", "..", "bin", "cli-shim.sh");
2087
+ const source = path9.resolve(moduleDir(), "..", "..", "bin", "cli-shim.sh");
1551
2088
  if (!existsSync(source)) throw new Error(`[coding-sessions] The CLI shim is missing at ${source}.`);
1552
- const body = await fs6.readFile(source, "utf8");
1553
- const target = path6.join(dataDir, "bin", "cli-shim.sh");
1554
- 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);
1555
2092
  if (current !== body) {
1556
- await fs6.mkdir(path6.dirname(target), { recursive: true });
1557
- await fs6.writeFile(target, body, { mode: 493 });
2093
+ await fs8.mkdir(path9.dirname(target), { recursive: true });
2094
+ await fs8.writeFile(target, body, { mode: 493 });
1558
2095
  }
1559
- await fs6.chmod(target, 493);
2096
+ await fs8.chmod(target, 493);
1560
2097
  return target;
1561
2098
  }
1562
- function whichOnPath(name, pathValue) {
1563
- for (const dir of pathValue.split(path6.delimiter)) {
1564
- if (!dir) continue;
1565
- const candidate = path6.join(dir, name);
1566
- try {
1567
- accessSync(candidate, fsConstants.X_OK);
1568
- return candidate;
1569
- } catch {
1570
- }
1571
- }
1572
- return null;
1573
- }
1574
2099
  function nowIso() {
1575
2100
  return (/* @__PURE__ */ new Date()).toISOString();
1576
2101
  }
@@ -1609,12 +2134,14 @@ function describeProviderStatus(provider, probe, lastProblem = null, quota = [])
1609
2134
  function describeAuthStatus(provider, probe) {
1610
2135
  const label = PROVIDER_LABEL[provider];
1611
2136
  const installed = probe?.installed === true;
1612
- const version = typeof probe?.version === "string" ? probe.version : null;
2137
+ const binary = probe?.binary ?? null;
2138
+ const version = binary?.version ?? (typeof probe?.version === "string" ? probe.version : null);
1613
2139
  const base = {
1614
2140
  provider,
1615
2141
  label,
1616
2142
  installed,
1617
2143
  version,
2144
+ binary,
1618
2145
  loggedIn: false,
1619
2146
  billing: "unknown",
1620
2147
  plan: null,
@@ -1666,29 +2193,30 @@ function describeAuthStatus(provider, probe) {
1666
2193
  };
1667
2194
  }
1668
2195
  async function createCodingSessionsEngine(options = {}) {
1669
- const dataDir = options.dataDir ?? path6.join(os4.homedir(), ".matrx", "coding-sessions");
1670
- const stateDir = path6.join(dataDir, "paperclip-state");
2196
+ const dataDir = options.dataDir ?? path9.join(os5.homedir(), ".matrx", "coding-sessions");
2197
+ const stateDir = path9.join(dataDir, "paperclip-state");
1671
2198
  const homes = options.homes ?? defaultProviderHomes();
1672
2199
  const index = new RunIndex(dataDir);
1673
2200
  await index.load();
1674
2201
  const preferences = new PreferenceStore(dataDir);
1675
2202
  await preferences.load();
1676
- await fs6.mkdir(stateDir, { recursive: true });
2203
+ await fs8.mkdir(stateDir, { recursive: true });
1677
2204
  const identity = await index.identity();
2205
+ const agents = createAgentConnection(options.agents, dataDir);
1678
2206
  const backups = options.rawBackup ? createRawBackupScheduler({
1679
2207
  sink: options.rawBackup,
1680
2208
  findFile: (provider, providerSessionId) => findSessionFile(provider, providerSessionId, homes),
1681
2209
  onError: (error) => console.warn("[coding-sessions] raw transcript backup failed:", error)
1682
2210
  }) : null;
1683
2211
  const discoveredSeen = /* @__PURE__ */ new Map();
1684
- const healthFile = path6.join(dataDir, "provider-health.json");
2212
+ const healthFile = path9.join(dataDir, "provider-health.json");
1685
2213
  let health = {};
1686
2214
  try {
1687
- health = JSON.parse(await fs6.readFile(healthFile, "utf8"));
2215
+ health = JSON.parse(await fs8.readFile(healthFile, "utf8"));
1688
2216
  } catch {
1689
2217
  health = {};
1690
2218
  }
1691
- 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);
1692
2220
  function noteRunOutcome(provider, status, texts) {
1693
2221
  if (status === "completed") {
1694
2222
  if (health[provider]) {
@@ -1716,7 +2244,24 @@ async function createCodingSessionsEngine(options = {}) {
1716
2244
  // Marks the forked process as a driver worker (see worker.ts).
1717
2245
  MATRX_CODING_SESSIONS_WORKER: "1"
1718
2246
  });
1719
- const commandFor = (provider) => options.commands?.[provider] ?? whichOnPath(provider, workerEnv.PATH ?? "");
2247
+ const binaryHome = options.hostEnv?.HOME ?? os5.homedir();
2248
+ const binaryCache = /* @__PURE__ */ new Map();
2249
+ function binaryFor(provider, fresh = false) {
2250
+ const cached = binaryCache.get(provider);
2251
+ if (cached && !fresh && Date.now() - cached.at < 5 * 6e4) return cached.value;
2252
+ const forced = options.commands?.[provider];
2253
+ const value = resolveBinary(provider, {
2254
+ env: workerEnv,
2255
+ home: binaryHome,
2256
+ forced,
2257
+ // A forced command needs no discovery to run; the status check still
2258
+ // looks, so it can say when a newer copy exists.
2259
+ discover: !forced || fresh
2260
+ }).catch(() => forced ? { path: forced, version: null, source: "configured", sourceLabel: "set by host", newer: null } : null);
2261
+ if (!forced || !fresh) binaryCache.set(provider, { at: Date.now(), value });
2262
+ return value;
2263
+ }
2264
+ const commandFor = async (provider) => (await binaryFor(provider))?.path ?? null;
1720
2265
  const workerPath = resolveWorkerPath();
1721
2266
  const shimPath = await installShim(dataDir);
1722
2267
  const listeners = /* @__PURE__ */ new Set();
@@ -1787,8 +2332,8 @@ async function createCodingSessionsEngine(options = {}) {
1787
2332
  }
1788
2333
  async function startRun(session, prompt) {
1789
2334
  const provider = session.provider;
1790
- const realCommand = commandFor(provider);
1791
- const runId = randomUUID3();
2335
+ const realCommand = await commandFor(provider);
2336
+ const runId = randomUUID4();
1792
2337
  const model = session.model ?? (provider === "claude" ? (await userDefaults(provider, session.cwd)).model ?? null : null);
1793
2338
  const run = {
1794
2339
  runId,
@@ -1826,7 +2371,26 @@ async function createCodingSessionsEngine(options = {}) {
1826
2371
  let fileOffset = null;
1827
2372
  if (session.providerSessionId) {
1828
2373
  const file = await findSessionFile(provider, session.providerSessionId, homes);
1829
- 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 });
1830
2394
  }
1831
2395
  const child = forkWorker();
1832
2396
  const liveRun = {
@@ -2000,7 +2564,8 @@ async function createCodingSessionsEngine(options = {}) {
2000
2564
  resumeSessionId: session.providerSessionId,
2001
2565
  shimPath,
2002
2566
  realCommand,
2003
- identity
2567
+ identity,
2568
+ agentArgs
2004
2569
  });
2005
2570
  return run;
2006
2571
  }
@@ -2019,7 +2584,7 @@ async function createCodingSessionsEngine(options = {}) {
2019
2584
  }, 3e3);
2020
2585
  }
2021
2586
  async function pruneDriverScratch() {
2022
- const runsDir = path6.join(
2587
+ const runsDir = path9.join(
2023
2588
  stateDir,
2024
2589
  "instances",
2025
2590
  "default",
@@ -2030,12 +2595,13 @@ async function createCodingSessionsEngine(options = {}) {
2030
2595
  "claude-runtime",
2031
2596
  "runs"
2032
2597
  );
2033
- const entries = await fs6.readdir(runsDir).catch(() => []);
2598
+ const entries = await fs8.readdir(runsDir).catch(() => []);
2034
2599
  await Promise.all(
2035
- entries.filter((name) => !live.has(name)).map((name) => fs6.rm(path6.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))
2036
2601
  );
2037
2602
  }
2038
2603
  async function probe() {
2604
+ const [claudeBinary, codexBinary] = await Promise.all([binaryFor("claude", true), binaryFor("codex", true)]);
2039
2605
  return new Promise((resolve) => {
2040
2606
  const child = forkWorker();
2041
2607
  let answered = false;
@@ -2051,8 +2617,8 @@ async function createCodingSessionsEngine(options = {}) {
2051
2617
  answered = true;
2052
2618
  clearTimeout(timer);
2053
2619
  resolve({
2054
- claude: message.claude ?? {},
2055
- codex: message.codex ?? {}
2620
+ claude: { ...message.claude ?? {}, binary: claudeBinary },
2621
+ codex: { ...message.codex ?? {}, binary: codexBinary }
2056
2622
  });
2057
2623
  }
2058
2624
  });
@@ -2064,7 +2630,7 @@ async function createCodingSessionsEngine(options = {}) {
2064
2630
  });
2065
2631
  child.send({
2066
2632
  kind: "probe",
2067
- commands: { claude: commandFor("claude"), codex: commandFor("codex") },
2633
+ commands: { claude: claudeBinary?.path ?? null, codex: codexBinary?.path ?? null },
2068
2634
  quota: options.quota !== false
2069
2635
  });
2070
2636
  });
@@ -2072,7 +2638,7 @@ async function createCodingSessionsEngine(options = {}) {
2072
2638
  function newSession(input) {
2073
2639
  const at = nowIso();
2074
2640
  return {
2075
- id: randomUUID3(),
2641
+ id: randomUUID4(),
2076
2642
  provider: input.provider,
2077
2643
  providerSessionId: input.providerSessionId,
2078
2644
  cwd: input.cwd,
@@ -2090,17 +2656,17 @@ async function createCodingSessionsEngine(options = {}) {
2090
2656
  function resolveFolder(raw) {
2091
2657
  const trimmed = raw.trim();
2092
2658
  if (!trimmed) return null;
2093
- return path6.resolve(trimmed.replace(/^~(?=$|\/)/, os4.homedir()));
2659
+ return path9.resolve(trimmed.replace(/^~(?=$|\/)/, os5.homedir()));
2094
2660
  }
2095
2661
  async function checkFolder(raw) {
2096
2662
  const target = resolveFolder(raw);
2097
2663
  if (!target) return { path: "", exists: false, isDirectory: false };
2098
- const stat = await fs6.stat(target).catch(() => null);
2664
+ const stat = await fs8.stat(target).catch(() => null);
2099
2665
  return { path: target, exists: stat !== null, isDirectory: stat?.isDirectory() === true };
2100
2666
  }
2101
2667
  async function assertFolder(cwd) {
2102
- const resolved = path6.resolve(cwd.replace(/^~(?=$|\/)/, os4.homedir()));
2103
- 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);
2104
2670
  if (!stat?.isDirectory()) {
2105
2671
  throw new Error(`The folder ${resolved} doesn't exist on this computer.`);
2106
2672
  }
@@ -2162,7 +2728,7 @@ async function createCodingSessionsEngine(options = {}) {
2162
2728
  folders.map(async ([folder, lastUsedAt]) => ({
2163
2729
  path: folder,
2164
2730
  lastUsedAt,
2165
- exists: (await fs6.stat(folder).catch(() => null))?.isDirectory() === true
2731
+ exists: (await fs8.stat(folder).catch(() => null))?.isDirectory() === true
2166
2732
  }))
2167
2733
  );
2168
2734
  },
@@ -2207,7 +2773,7 @@ async function createCodingSessionsEngine(options = {}) {
2207
2773
  async createFolder(raw) {
2208
2774
  const target = resolveFolder(raw);
2209
2775
  if (!target) throw new Error("Type a folder path first.");
2210
- await fs6.mkdir(target, { recursive: true });
2776
+ await fs8.mkdir(target, { recursive: true });
2211
2777
  return checkFolder(target);
2212
2778
  },
2213
2779
  async sessionActivity(provider, providerSessionId) {
@@ -2245,31 +2811,35 @@ async function createCodingSessionsEngine(options = {}) {
2245
2811
  reason: `${PROVIDER_LABEL[provider]} has no saved transcript for this chat on this computer.`
2246
2812
  };
2247
2813
  }
2248
- const stat = await fs6.stat(file);
2814
+ const stat = await fs8.stat(file);
2249
2815
  if (stat.size > RAW_TRANSCRIPT_MAX_BYTES) {
2250
2816
  return {
2251
2817
  available: false,
2252
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}.`
2253
2819
  };
2254
2820
  }
2255
- const content = await fs6.readFile(file, "utf8");
2256
- return { available: true, fileName: path6.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 };
2257
2823
  },
2258
2824
  async start(input) {
2259
2825
  const prompt = input.prompt.trim();
2260
2826
  if (!prompt) throw new Error("Type what you want done before starting.");
2261
2827
  const cwd = await assertFolder(input.cwd);
2262
- const session = saveSession(
2263
- newSession({
2264
- provider: input.provider,
2265
- cwd,
2266
- title: titleFrom(prompt),
2267
- model: input.model?.trim() || null,
2268
- effort: input.effort?.trim() || null,
2269
- origin: "started",
2270
- providerSessionId: null
2271
- })
2272
- );
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);
2273
2843
  const run = await startRun(session, prompt);
2274
2844
  return { session: index.session(session.id) ?? session, run, queued: false };
2275
2845
  },
@@ -2338,6 +2908,21 @@ async function createCodingSessionsEngine(options = {}) {
2338
2908
  if (opts && "effort" in opts) next.effort = typeof opts.effort === "string" && opts.effort.trim() ? opts.effort.trim() : null;
2339
2909
  return saveSession(next);
2340
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
+ },
2341
2926
  async dispose() {
2342
2927
  for (const liveRun of live.values()) {
2343
2928
  liveRun.stopRequested = true;
@@ -2374,7 +2959,7 @@ function openFolderIn(folder, target) {
2374
2959
  args = ["-a", app.app, folder];
2375
2960
  }
2376
2961
  return new Promise((resolve, reject) => {
2377
- execFile2(command, args, { timeout: 1e4 }, (error) => {
2962
+ execFile3(command, args, { timeout: 1e4 }, (error) => {
2378
2963
  if (!error) return resolve();
2379
2964
  const label = target === "finder" ? "the folder" : OPEN_APP[target]?.label ?? target;
2380
2965
  reject(
@@ -2407,7 +2992,10 @@ var BRIDGE_METHODS = [
2407
2992
  "setChatPreference",
2408
2993
  "searchChats",
2409
2994
  "openFolder",
2410
- "setSessionOptions"
2995
+ "setSessionOptions",
2996
+ "agentAccess",
2997
+ "agentCatalogRpc",
2998
+ "agentDetails"
2411
2999
  ];
2412
3000
  var BridgeError = class extends Error {
2413
3001
  constructor(message, code) {
@@ -2451,14 +3039,14 @@ var MIME = {
2451
3039
  ".map": "application/json"
2452
3040
  };
2453
3041
  function moduleDir2() {
2454
- return path7.dirname(fileURLToPath2(import.meta.url));
3042
+ return path10.dirname(fileURLToPath2(import.meta.url));
2455
3043
  }
2456
3044
  function defaultUiDir() {
2457
3045
  for (const candidate of [
2458
- path7.resolve(moduleDir2(), "..", "ui"),
2459
- path7.resolve(moduleDir2(), "..", "..", "dist", "ui")
3046
+ path10.resolve(moduleDir2(), "..", "ui"),
3047
+ path10.resolve(moduleDir2(), "..", "..", "dist", "ui")
2460
3048
  ]) {
2461
- if (existsSync2(path7.join(candidate, "index.html"))) return candidate;
3049
+ if (existsSync2(path10.join(candidate, "index.html"))) return candidate;
2462
3050
  }
2463
3051
  return null;
2464
3052
  }
@@ -2481,19 +3069,19 @@ async function serveStatic(uiDir, req, res) {
2481
3069
  return;
2482
3070
  }
2483
3071
  const requested = decodeURIComponent(url.pathname);
2484
- let file = path7.resolve(uiDir, `.${requested}`);
3072
+ let file = path10.resolve(uiDir, `.${requested}`);
2485
3073
  if (!file.startsWith(uiDir)) {
2486
3074
  res.writeHead(403).end();
2487
3075
  return;
2488
3076
  }
2489
- const stat = await fs7.stat(file).catch(() => null);
2490
- if (!stat || stat.isDirectory()) file = path7.join(uiDir, "index.html");
2491
- 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);
2492
3080
  if (!body) {
2493
3081
  res.writeHead(404).end();
2494
3082
  return;
2495
3083
  }
2496
- const ext = path7.extname(file);
3084
+ const ext = path10.extname(file);
2497
3085
  res.writeHead(200, {
2498
3086
  "content-type": MIME[ext] ?? "application/octet-stream",
2499
3087
  "cache-control": ext === ".html" ? "no-store" : "public, max-age=31536000, immutable",
@@ -2502,6 +3090,46 @@ async function serveStatic(uiDir, req, res) {
2502
3090
  });
2503
3091
  res.end(body);
2504
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
+ }
2505
3133
  function attachSocket(socket, bridge) {
2506
3134
  const unsubscribe = bridge.subscribe((event) => {
2507
3135
  if (socket.readyState === socket.OPEN) socket.send(JSON.stringify({ event }));
@@ -2532,7 +3160,7 @@ function attachSocket(socket, bridge) {
2532
3160
  socket.on("close", unsubscribe);
2533
3161
  }
2534
3162
  async function startCodingSessionsHost(options = {}) {
2535
- const token = options.token ?? randomBytes(24).toString("base64url");
3163
+ const token = options.token ?? randomBytes2(24).toString("base64url");
2536
3164
  const engine = options.engine ?? await createCodingSessionsEngine(options);
2537
3165
  const bridge = createHostBridge(engine);
2538
3166
  const uiDir = options.uiDir ?? defaultUiDir();
@@ -2543,6 +3171,13 @@ async function startCodingSessionsHost(options = {}) {
2543
3171
  };
2544
3172
  const server = http.createServer((req, res) => {
2545
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
+ }
2546
3181
  if (url2.pathname === "/health") {
2547
3182
  res.writeHead(200, { "content-type": "application/json" });
2548
3183
  res.end(JSON.stringify({ ok: true, service: "@ai-matrx/coding-sessions" }));
@@ -2573,6 +3208,7 @@ async function startCodingSessionsHost(options = {}) {
2573
3208
  server.off("error", reject);
2574
3209
  const address = server.address();
2575
3210
  if (address && typeof address === "object") port = address.port;
3211
+ engine.setMcpBaseUrl?.(`http://${HOST}:${port}`);
2576
3212
  resolve();
2577
3213
  });
2578
3214
  });
@@ -2583,6 +3219,7 @@ async function startCodingSessionsHost(options = {}) {
2583
3219
  token,
2584
3220
  engine,
2585
3221
  async close() {
3222
+ engine.setMcpBaseUrl?.(null);
2586
3223
  for (const client of wss.clients) client.terminate();
2587
3224
  await new Promise((resolve) => wss.close(() => resolve()));
2588
3225
  await new Promise((resolve) => server.close(() => resolve()));