@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,7 +1,7 @@
1
1
  // src/engine/index.ts
2
- import { execFile as execFile2, fork } from "child_process";
3
- import { randomUUID as randomUUID3 } from "crypto";
4
- import { accessSync, constants as fsConstants, existsSync } from "fs";
2
+ import { execFile as execFile3, fork } from "child_process";
3
+ import { randomUUID as randomUUID4 } from "crypto";
4
+ import { existsSync } from "fs";
5
5
  import { formatFileSize } from "@ai-matrx/kit/format";
6
6
 
7
7
  // src/engine/raw-backup.ts
@@ -98,10 +98,379 @@ function createAiMatrxRawBackupSink(options) {
98
98
  };
99
99
  }
100
100
 
101
+ // src/engine/agent-attach.ts
102
+ import fs2 from "fs/promises";
103
+ import path from "path";
104
+ var AGENT_MCP_SERVER_NAME = "matrx-agent";
105
+ var AGENT_PLUGIN_NAME = "matrx-agent";
106
+ function escapeAttr(value) {
107
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
108
+ }
109
+ function versionWords(bundle) {
110
+ return bundle.version_number !== null ? String(bundle.version_number) : "latest";
111
+ }
112
+ function renderAgentInstructions(bundle, options = {}) {
113
+ const included = bundle.skills.filter((skill) => skill.tier === "included");
114
+ const listed2 = bundle.skills.filter((skill) => skill.tier === "listed");
115
+ const parts = [`<ai_matrx_agent name="${escapeAttr(bundle.name)}" version="${versionWords(bundle)}">`];
116
+ if (included.length > 0) {
117
+ parts.push(
118
+ "## Skills (always in effect)",
119
+ ...included.map((skill) => `### ${skill.label}
120
+
121
+ ${skill.body.trim()}`)
122
+ );
123
+ }
124
+ if (options.claudeSkills && listed2.length > 0) {
125
+ parts.push(
126
+ [
127
+ "## Your skills",
128
+ "When a request matches one of these, load it with your Skill tool and follow it before answering:",
129
+ ...listed2.map((skill) => `- ${AGENT_PLUGIN_NAME}:${skill.slug} \u2014 ${skill.description || skill.label}`)
130
+ ].join("\n")
131
+ );
132
+ }
133
+ if (included.length > 0 || options.claudeSkills && listed2.length > 0) parts.push("## Your instructions");
134
+ parts.push(bundle.instructions.trim());
135
+ parts.push("</ai_matrx_agent>");
136
+ return `${parts.filter(Boolean).join("\n\n")}
137
+ `;
138
+ }
139
+ function renderCodexSkillsList(bundle, skillsRoot) {
140
+ const listed2 = bundle.skills.filter((skill) => skill.tier === "listed");
141
+ if (listed2.length === 0) return "";
142
+ const lines = [
143
+ "<skills_instructions>",
144
+ "## AI Matrx agent skills",
145
+ "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.",
146
+ "### Available skills",
147
+ ...listed2.map(
148
+ (skill) => `- ${skill.slug}: ${skill.description || skill.label} (file: ${path.join(skillsRoot, skill.slug, "SKILL.md")})`
149
+ ),
150
+ "</skills_instructions>"
151
+ ];
152
+ return lines.join("\n");
153
+ }
154
+ function tomlString(value) {
155
+ const scalar = value.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "\uFFFD");
156
+ return JSON.stringify(scalar).replace(/\u007f/g, "\\u007f");
157
+ }
158
+ async function writeAgentAttachment(dir, bundle, options) {
159
+ const pluginDir = path.join(dir, "plugin");
160
+ const skillsRoot = path.join(dir, "skills");
161
+ await fs2.mkdir(path.join(pluginDir, ".claude-plugin"), { recursive: true, mode: 448 });
162
+ await fs2.mkdir(skillsRoot, { recursive: true, mode: 448 });
163
+ const instructions = renderAgentInstructions(bundle);
164
+ const instructionsFile = path.join(dir, "instructions.md");
165
+ await fs2.writeFile(instructionsFile, renderAgentInstructions(bundle, { claudeSkills: true }), { mode: 384 });
166
+ await fs2.writeFile(
167
+ path.join(pluginDir, ".claude-plugin", "plugin.json"),
168
+ `${JSON.stringify(
169
+ {
170
+ name: AGENT_PLUGIN_NAME,
171
+ description: `Skills of the AI Matrx agent "${bundle.name}"`,
172
+ version: versionWords(bundle) === "latest" ? "1.0.0" : `${versionWords(bundle)}.0.0`
173
+ },
174
+ null,
175
+ 2
176
+ )}
177
+ `,
178
+ { mode: 384 }
179
+ );
180
+ const listed2 = bundle.skills.filter((skill) => skill.tier === "listed");
181
+ const wanted = new Set(listed2.map((skill) => skill.slug));
182
+ for (const root of [path.join(pluginDir, "skills"), skillsRoot]) {
183
+ await fs2.mkdir(root, { recursive: true, mode: 448 });
184
+ for (const existing of await fs2.readdir(root).catch(() => [])) {
185
+ if (!wanted.has(existing)) await fs2.rm(path.join(root, existing), { recursive: true, force: true });
186
+ }
187
+ for (const skill of listed2) {
188
+ const skillDir = path.join(root, skill.slug);
189
+ await fs2.mkdir(skillDir, { recursive: true, mode: 448 });
190
+ await fs2.writeFile(path.join(skillDir, "SKILL.md"), skill.content, { mode: 384 });
191
+ }
192
+ }
193
+ let mcpConfigFile = null;
194
+ const mcpUrl = options.mcpUrl && bundle.tools.length > 0 ? options.mcpUrl : null;
195
+ const mcpPath = path.join(dir, "mcp.json");
196
+ if (mcpUrl) {
197
+ mcpConfigFile = mcpPath;
198
+ await fs2.writeFile(
199
+ mcpPath,
200
+ `${JSON.stringify({ mcpServers: { [AGENT_MCP_SERVER_NAME]: { type: "http", url: mcpUrl } } }, null, 2)}
201
+ `,
202
+ { mode: 384 }
203
+ );
204
+ } else {
205
+ await fs2.rm(mcpPath, { force: true });
206
+ }
207
+ const skillsList = renderCodexSkillsList(bundle, skillsRoot);
208
+ return {
209
+ dir,
210
+ instructionsFile,
211
+ pluginDir,
212
+ mcpConfigFile,
213
+ mcpUrl,
214
+ // Same order as Claude's: the skills list first, the agent's words last.
215
+ codexInstructions: skillsList ? `${skillsList}
216
+
217
+ ${instructions}` : instructions
218
+ };
219
+ }
220
+ function agentArgsFor(provider, attachment, options) {
221
+ if (provider === "claude") {
222
+ const args2 = [
223
+ // Every turn: without snapshot off, a resumed Claude drops appended instructions.
224
+ "--append-system-prompt-file",
225
+ attachment.instructionsFile,
226
+ "--system-prompt-snapshot",
227
+ "off",
228
+ "--plugin-dir",
229
+ attachment.pluginDir
230
+ ];
231
+ if (attachment.mcpConfigFile) args2.push("--mcp-config", attachment.mcpConfigFile);
232
+ return args2;
233
+ }
234
+ const args = [];
235
+ if (!options.resuming) args.push("-c", `developer_instructions=${tomlString(attachment.codexInstructions)}`);
236
+ if (attachment.mcpUrl) {
237
+ const key = `mcp_servers.${AGENT_MCP_SERVER_NAME}`;
238
+ args.push(
239
+ "-c",
240
+ `${key}.url=${tomlString(attachment.mcpUrl)}`,
241
+ "-c",
242
+ `${key}.default_tools_approval_mode="approve"`
243
+ );
244
+ }
245
+ return args;
246
+ }
247
+ function sessionAgentRecord(bundle, options) {
248
+ const unavailable = bundle.unavailable.map((u) => ({ kind: u.kind, name: u.name, reason: u.reason }));
249
+ const missingSkills = unavailable.filter((u) => u.kind === "skill").length;
250
+ const missingTools = unavailable.filter((u) => u.kind === "tool").length;
251
+ if (!options.toolsServed && bundle.tools.length > 0) {
252
+ for (const tool of bundle.tools) {
253
+ unavailable.push({ kind: "tool", name: tool.canonical_name, reason: "Tools need the coding sessions host" });
254
+ }
255
+ }
256
+ const toolsLoaded = options.toolsServed ? bundle.tools.length : 0;
257
+ const loaded = [
258
+ { part: "instructions", loaded: bundle.instructions.trim() ? 1 : 0, total: 1 },
259
+ { part: "skills", loaded: bundle.skills.length, total: bundle.skills.length + missingSkills },
260
+ { part: "tools", loaded: toolsLoaded, total: bundle.tools.length + missingTools }
261
+ ];
262
+ return {
263
+ agentId: bundle.agent_id,
264
+ versionId: bundle.version_id,
265
+ versionNumber: bundle.version_number,
266
+ name: bundle.name,
267
+ definitionHash: bundle.definition_hash,
268
+ loaded,
269
+ unavailable
270
+ };
271
+ }
272
+
273
+ // src/engine/agents.ts
274
+ import { randomBytes, randomUUID } from "crypto";
275
+ import fs3 from "fs/promises";
276
+ import path2 from "path";
277
+ var DEFAULT_SERVER_URL = "https://server.app.matrxserver.com";
278
+ var DEFAULT_SUPABASE_URL = "https://db.matrxserver.com";
279
+ var AGENT_CATALOG_RPCS = /* @__PURE__ */ new Set([
280
+ "agx_get_list_full",
281
+ "agx_search",
282
+ "agx_resolve_agent_address",
283
+ "ues_get_bulk",
284
+ "ues_list",
285
+ "ues_set",
286
+ "ues_touch"
287
+ ]);
288
+ var agentsClient = async () => {
289
+ const [matrx, portable, mcp] = await Promise.all([
290
+ import("@ai-matrx/agents/matrx"),
291
+ import("@ai-matrx/agents/portable"),
292
+ import("@ai-matrx/agents/portable/mcp")
293
+ ]);
294
+ return { ...matrx, ...portable, ...mcp };
295
+ };
296
+ var SIGNED_OUT_AGENT_MESSAGE = "Sign in to AI Matrx to use your agents.";
297
+ function userIdOf(credential) {
298
+ if (!credential || credential.kind !== "user") return null;
299
+ const payload = credential.accessToken.split(".")[1];
300
+ if (!payload) return null;
301
+ try {
302
+ const claims = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
303
+ return typeof claims.sub === "string" ? claims.sub : null;
304
+ } catch {
305
+ return null;
306
+ }
307
+ }
308
+ function createAgentConnection(options, dataDir) {
309
+ const credentials = options?.credentials ?? null;
310
+ const serverUrl = (options?.serverUrl ?? DEFAULT_SERVER_URL).replace(/\/+$/, "");
311
+ const supabaseUrl = (options?.supabaseUrl ?? DEFAULT_SUPABASE_URL).replace(/\/+$/, "");
312
+ const supabaseKey = options?.supabaseKey ?? null;
313
+ const root = path2.join(dataDir, "agents");
314
+ let mcpBaseUrl = null;
315
+ const handlers = /* @__PURE__ */ new Map();
316
+ const secretToSession = /* @__PURE__ */ new Map();
317
+ const organizationId = () => {
318
+ const raw = typeof options?.organizationId === "function" ? options.organizationId() : options?.organizationId;
319
+ return raw && raw.trim() ? raw.trim() : null;
320
+ };
321
+ const transport = async () => (await agentsClient()).createMatrxTransport({
322
+ baseUrl: serverUrl,
323
+ ...credentials ? { credentials } : {},
324
+ ...organizationId() ? { organizationId: () => organizationId() } : {},
325
+ source: "codingSessionsAgents"
326
+ });
327
+ const credential = async () => credentials ? credentials.get() : null;
328
+ const requireSignedIn = async () => {
329
+ if (!await credential()) throw new Error(SIGNED_OUT_AGENT_MESSAGE);
330
+ };
331
+ const sessionDir = (sessionId) => path2.join(root, sessionId.replace(/[^A-Za-z0-9_-]/g, "_"));
332
+ async function meta(sessionId) {
333
+ const file = path2.join(sessionDir(sessionId), "session.json");
334
+ const existing = await fs3.readFile(file, "utf8").then((text) => JSON.parse(text)).catch(() => null);
335
+ if (existing?.mcpSecret && existing.conversationId) return existing;
336
+ const created = { mcpSecret: randomBytes(24).toString("base64url"), conversationId: randomUUID() };
337
+ await fs3.mkdir(sessionDir(sessionId), { recursive: true, mode: 448 });
338
+ await fs3.writeFile(file, JSON.stringify(created), { mode: 384 });
339
+ return created;
340
+ }
341
+ async function findSessionBySecret(secret) {
342
+ for (const name of await fs3.readdir(root).catch(() => [])) {
343
+ const found = await fs3.readFile(path2.join(root, name, "session.json"), "utf8").then((text) => JSON.parse(text).mcpSecret === secret).catch(() => false);
344
+ if (found) {
345
+ secretToSession.set(secret, name);
346
+ return name;
347
+ }
348
+ }
349
+ return null;
350
+ }
351
+ async function readSessionBundle(sessionId) {
352
+ return fs3.readFile(path2.join(sessionDir(sessionId), "bundle.json"), "utf8").then((text) => JSON.parse(text)).catch(() => null);
353
+ }
354
+ return {
355
+ async access() {
356
+ const current = await credential().catch(() => null);
357
+ const userId = userIdOf(current);
358
+ return {
359
+ configured: credentials !== null && supabaseKey !== null,
360
+ signedIn: userId !== null,
361
+ userId,
362
+ organizationId: organizationId()
363
+ };
364
+ },
365
+ async catalogRpc(request) {
366
+ if (!AGENT_CATALOG_RPCS.has(request.fn)) {
367
+ return { data: null, error: { message: `Not an agent catalogue read: ${request.fn}` } };
368
+ }
369
+ const current = await credential();
370
+ if (!current || current.kind !== "user") return { data: null, error: { message: SIGNED_OUT_AGENT_MESSAGE, code: "signed_out" } };
371
+ if (!supabaseKey) return { data: null, error: { message: "This host has no AI Matrx database key.", code: "not_configured" } };
372
+ const query = new URLSearchParams();
373
+ if (request.order) query.set("order", `${request.order.column}.${request.order.ascending === false ? "desc" : "asc"}`);
374
+ if (request.range) {
375
+ query.set("offset", String(request.range[0]));
376
+ query.set("limit", String(request.range[1] - request.range[0] + 1));
377
+ }
378
+ const headers = {
379
+ apikey: supabaseKey,
380
+ Authorization: `Bearer ${current.accessToken}`,
381
+ "Content-Type": "application/json",
382
+ // Every catalogue RPC lives in `public`; the database's default
383
+ // exposed schema is not `public`, so name it (supabase-js does).
384
+ "Content-Profile": "public",
385
+ "Accept-Profile": "public"
386
+ };
387
+ if (request.count) headers.Prefer = `count=${request.count}`;
388
+ const qs = query.toString();
389
+ const response = await fetch(`${supabaseUrl}/rest/v1/rpc/${encodeURIComponent(request.fn)}${qs ? `?${qs}` : ""}`, {
390
+ method: "POST",
391
+ headers,
392
+ body: JSON.stringify(request.args ?? {})
393
+ });
394
+ const text = await response.text();
395
+ const body = text ? JSON.parse(text) : null;
396
+ if (!response.ok) {
397
+ const err = body ?? {};
398
+ return {
399
+ data: null,
400
+ error: { message: err.message ?? `HTTP ${response.status}`, code: err.code, details: err.details ?? null, hint: err.hint ?? null }
401
+ };
402
+ }
403
+ const range = response.headers.get("content-range");
404
+ const total = range?.split("/")[1];
405
+ return { data: body, error: null, count: total && total !== "*" ? Number(total) : null };
406
+ },
407
+ async details(agentId) {
408
+ await requireSignedIn();
409
+ const { fetchPortableAgent, fetchPortableAgentVersions } = await agentsClient();
410
+ const [bundle, versions] = await Promise.all([
411
+ fetchPortableAgent(await transport(), { agentId }),
412
+ fetchPortableAgentVersions(await transport(), agentId).catch(() => [])
413
+ ]);
414
+ return {
415
+ agentId: bundle.agent_id,
416
+ name: bundle.name,
417
+ variables: bundle.variables,
418
+ versions: versions.map((v) => ({ id: v.id, versionNumber: v.version_number, changedAt: v.changed_at })).sort((a, b) => b.versionNumber - a.versionNumber),
419
+ latestVersionNumber: bundle.version_number
420
+ };
421
+ },
422
+ async loadBundle(selection) {
423
+ await requireSignedIn();
424
+ const { fetchPortableAgent } = await agentsClient();
425
+ return fetchPortableAgent(
426
+ await transport(),
427
+ selection.versionId ? { versionId: selection.versionId } : { agentId: selection.agentId },
428
+ { variables: selection.variables ?? {} }
429
+ );
430
+ },
431
+ async saveSessionBundle(sessionId, bundle) {
432
+ await fs3.mkdir(sessionDir(sessionId), { recursive: true, mode: 448 });
433
+ await fs3.writeFile(path2.join(sessionDir(sessionId), "bundle.json"), JSON.stringify(bundle), { mode: 384 });
434
+ await meta(sessionId);
435
+ },
436
+ readSessionBundle,
437
+ sessionDir,
438
+ async mcpUrlFor(sessionId) {
439
+ if (!mcpBaseUrl) return null;
440
+ const { mcpSecret } = await meta(sessionId);
441
+ secretToSession.set(mcpSecret, sessionId);
442
+ return `${mcpBaseUrl}/mcp/${mcpSecret}`;
443
+ },
444
+ setMcpBaseUrl(baseUrl) {
445
+ mcpBaseUrl = baseUrl ? baseUrl.replace(/\/+$/, "") : null;
446
+ },
447
+ async handleMcp(secret, body) {
448
+ let handler = handlers.get(secret);
449
+ if (!handler) {
450
+ const sessionId = secretToSession.get(secret) ?? await findSessionBySecret(secret);
451
+ if (!sessionId) return null;
452
+ const bundle = await readSessionBundle(sessionId);
453
+ if (!bundle) return null;
454
+ const { conversationId } = await meta(sessionId);
455
+ const { createAgentToolsMcpHandler } = await agentsClient();
456
+ handler = createAgentToolsMcpHandler({
457
+ bundle,
458
+ credentials,
459
+ organizationId: () => organizationId(),
460
+ baseUrl: serverUrl,
461
+ conversationId
462
+ });
463
+ handlers.set(secret, handler);
464
+ }
465
+ return handler.handle(body);
466
+ }
467
+ };
468
+ }
469
+
101
470
  // src/engine/index.ts
102
- import fs6 from "fs/promises";
103
- import os4 from "os";
104
- import path6 from "path";
471
+ import fs8 from "fs/promises";
472
+ import os5 from "os";
473
+ import path9 from "path";
105
474
  import { fileURLToPath } from "url";
106
475
 
107
476
  // src/normalize/claude.ts
@@ -767,9 +1136,177 @@ function describeProblem(provider, problem) {
767
1136
  };
768
1137
  }
769
1138
 
770
- // src/engine/clean-env.ts
1139
+ // src/engine/binaries.ts
1140
+ import { execFile } from "child_process";
1141
+ import { accessSync, constants as fsConstants, readdirSync, readFileSync, realpathSync } from "fs";
771
1142
  import os from "os";
772
- import path from "path";
1143
+ import path3 from "path";
1144
+ var SOURCE_LABEL = {
1145
+ codex: { "desktop-app": "from ChatGPT app", path: "from PATH", "npm-global": "from npm", configured: "set by host" },
1146
+ claude: { "desktop-app": "from Claude app", path: "from PATH", "npm-global": "from npm", configured: "set by host" }
1147
+ };
1148
+ function sourceLabel(provider, source) {
1149
+ return SOURCE_LABEL[provider][source];
1150
+ }
1151
+ function parseVersion(text) {
1152
+ const match = /(\d+)\.(\d+)\.(\d+)(?:[-.][0-9A-Za-z.]+)?/.exec(text ?? "");
1153
+ return match ? match[0] : null;
1154
+ }
1155
+ function compareVersions(a, b) {
1156
+ const [aCore = "", aPre] = a.split("-", 2);
1157
+ const [bCore = "", bPre] = b.split("-", 2);
1158
+ const pa = aCore.split(".").map((n) => Number.parseInt(n, 10) || 0);
1159
+ const pb = bCore.split(".").map((n) => Number.parseInt(n, 10) || 0);
1160
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
1161
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
1162
+ if (d !== 0) return d < 0 ? -1 : 1;
1163
+ }
1164
+ if (aPre && !bPre) return -1;
1165
+ if (!aPre && bPre) return 1;
1166
+ return 0;
1167
+ }
1168
+ function pickBinary(candidates) {
1169
+ let best = null;
1170
+ for (const c of candidates) {
1171
+ if (!best) {
1172
+ best = c;
1173
+ continue;
1174
+ }
1175
+ if (c.version && (!best.version || compareVersions(c.version, best.version) > 0)) best = c;
1176
+ }
1177
+ return best;
1178
+ }
1179
+ function executable(file) {
1180
+ try {
1181
+ accessSync(file, fsConstants.X_OK);
1182
+ return true;
1183
+ } catch {
1184
+ return false;
1185
+ }
1186
+ }
1187
+ function exeName(name, platform) {
1188
+ return platform === "win32" ? [`${name}.exe`, `${name}.cmd`, name] : [name];
1189
+ }
1190
+ function allOnPath(name, pathValue, platform = process.platform) {
1191
+ const found = [];
1192
+ for (const dir of pathValue.split(path3.delimiter)) {
1193
+ if (!dir) continue;
1194
+ for (const file of exeName(name, platform)) {
1195
+ const candidate = path3.join(dir, file);
1196
+ if (executable(candidate)) {
1197
+ found.push(candidate);
1198
+ break;
1199
+ }
1200
+ }
1201
+ }
1202
+ return found;
1203
+ }
1204
+ function desktopAppCandidates(provider, platform, home, applications) {
1205
+ if (platform !== "darwin") return [];
1206
+ if (provider === "codex") {
1207
+ return applications.map(
1208
+ (apps) => path3.join(apps, "ChatGPT.app", "Contents", "Resources", "codex-cli", "bin", "codex")
1209
+ );
1210
+ }
1211
+ const root = path3.join(home, "Library", "Application Support", "Claude", "claude-code");
1212
+ let versions = [];
1213
+ try {
1214
+ versions = readdirSync(root);
1215
+ } catch {
1216
+ return [];
1217
+ }
1218
+ return versions.map((v) => path3.join(root, v, "claude.app", "Contents", "MacOS", "claude"));
1219
+ }
1220
+ function npmGlobalPrefix(env) {
1221
+ const fromEnv = env.npm_config_prefix ?? env.NPM_CONFIG_PREFIX;
1222
+ if (fromEnv) return Promise.resolve(fromEnv);
1223
+ const npm = allOnPath("npm", env.PATH ?? "")[0];
1224
+ if (!npm) return Promise.resolve(null);
1225
+ return new Promise((resolve) => {
1226
+ const child = execFile(npm, ["prefix", "-g"], { timeout: 5e3, env }, (error, stdout) => {
1227
+ resolve(error ? null : String(stdout).trim() || null);
1228
+ });
1229
+ child.stdin?.end();
1230
+ });
1231
+ }
1232
+ function declaredVersion(provider, source, file) {
1233
+ if (source !== "desktop-app") return null;
1234
+ if (provider === "codex") {
1235
+ try {
1236
+ const pkg = JSON.parse(readFileSync(path3.join(path3.dirname(path3.dirname(file)), "codex-package.json"), "utf8"));
1237
+ return typeof pkg.version === "string" ? parseVersion(pkg.version) : null;
1238
+ } catch {
1239
+ return null;
1240
+ }
1241
+ }
1242
+ return parseVersion(path3.basename(path3.resolve(file, "..", "..", "..", "..")));
1243
+ }
1244
+ function runVersion(file, env) {
1245
+ return new Promise((resolve) => {
1246
+ const child = execFile(file, ["--version"], { timeout: 8e3, env }, (error, stdout) => {
1247
+ resolve(error ? null : parseVersion(String(stdout)));
1248
+ });
1249
+ child.stdin?.end();
1250
+ });
1251
+ }
1252
+ async function discoverBinaries(provider, options) {
1253
+ const platform = options.platform ?? process.platform;
1254
+ const home = options.home ?? options.env.HOME ?? os.homedir();
1255
+ const listed2 = [
1256
+ ...desktopAppCandidates(provider, platform, home, options.applications ?? ["/Applications", path3.join(home, "Applications")]).filter(executable).map((p) => ({ path: p, source: "desktop-app" })),
1257
+ ...allOnPath(provider, options.env.PATH ?? "", platform).map((p) => ({ path: p, source: "path" }))
1258
+ ];
1259
+ const prefix = await npmGlobalPrefix(options.env);
1260
+ if (prefix) {
1261
+ const dir = platform === "win32" ? prefix : path3.join(prefix, "bin");
1262
+ for (const file of exeName(provider, platform)) {
1263
+ const candidate = path3.join(dir, file);
1264
+ if (executable(candidate)) {
1265
+ listed2.push({ path: candidate, source: "npm-global" });
1266
+ break;
1267
+ }
1268
+ }
1269
+ }
1270
+ const seen = /* @__PURE__ */ new Set();
1271
+ const unique = listed2.filter((c) => {
1272
+ let real = c.path;
1273
+ try {
1274
+ real = realpathSync(c.path);
1275
+ } catch {
1276
+ }
1277
+ if (seen.has(real)) return false;
1278
+ seen.add(real);
1279
+ return true;
1280
+ });
1281
+ return Promise.all(
1282
+ unique.map(async (c) => ({
1283
+ ...c,
1284
+ version: declaredVersion(provider, c.source, c.path) ?? await runVersion(c.path, options.env)
1285
+ }))
1286
+ );
1287
+ }
1288
+ async function resolveBinary(provider, options) {
1289
+ const candidates = options.discover === false ? [] : await discoverBinaries(provider, options);
1290
+ const best = pickBinary(candidates);
1291
+ const inUse = options.forced ? {
1292
+ path: options.forced,
1293
+ source: "configured",
1294
+ version: candidates.find((c) => c.path === options.forced)?.version ?? (options.discover === false ? null : await runVersion(options.forced, options.env))
1295
+ } : best;
1296
+ if (!inUse) return null;
1297
+ 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;
1298
+ return {
1299
+ path: inUse.path,
1300
+ version: inUse.version,
1301
+ source: inUse.source,
1302
+ sourceLabel: sourceLabel(provider, inUse.source),
1303
+ newer
1304
+ };
1305
+ }
1306
+
1307
+ // src/engine/clean-env.ts
1308
+ import os2 from "os";
1309
+ import path4 from "path";
773
1310
  var ALLOWED_NAMES = /* @__PURE__ */ new Set([
774
1311
  "HOME",
775
1312
  "USER",
@@ -791,11 +1328,11 @@ var ALLOWED_NAMES = /* @__PURE__ */ new Set([
791
1328
  var ALLOWED_PREFIXES = ["LC_"];
792
1329
  function wellKnownBinDirs(home) {
793
1330
  return [
794
- path.join(home, ".local", "bin"),
795
- path.join(home, ".claude", "local"),
796
- path.join(home, ".npm-global", "bin"),
797
- path.join(home, ".bun", "bin"),
798
- path.join(home, ".volta", "bin"),
1331
+ path4.join(home, ".local", "bin"),
1332
+ path4.join(home, ".claude", "local"),
1333
+ path4.join(home, ".npm-global", "bin"),
1334
+ path4.join(home, ".bun", "bin"),
1335
+ path4.join(home, ".volta", "bin"),
799
1336
  "/opt/homebrew/bin",
800
1337
  "/usr/local/bin",
801
1338
  "/usr/bin",
@@ -813,31 +1350,31 @@ function buildCleanEnv(hostEnv, extra = {}) {
813
1350
  if (typeof value !== "string") continue;
814
1351
  if (isAllowedEnvName(name)) out[name] = value;
815
1352
  }
816
- const home = out.HOME ?? os.homedir();
1353
+ const home = out.HOME ?? os2.homedir();
817
1354
  out.HOME = home;
818
- const hostPath = (hostEnv.PATH ?? "").split(path.delimiter).filter(Boolean);
1355
+ const hostPath = (hostEnv.PATH ?? "").split(path4.delimiter).filter(Boolean);
819
1356
  const merged = [];
820
1357
  for (const dir of [...hostPath, ...wellKnownBinDirs(home)]) {
821
1358
  if (!merged.includes(dir)) merged.push(dir);
822
1359
  }
823
- out.PATH = merged.join(path.delimiter);
1360
+ out.PATH = merged.join(path4.delimiter);
824
1361
  for (const [name, value] of Object.entries(extra)) out[name] = value;
825
1362
  return out;
826
1363
  }
827
1364
 
828
1365
  // src/engine/files.ts
829
1366
  import { createReadStream } from "fs";
830
- import fs2 from "fs/promises";
831
- import os2 from "os";
832
- import path2 from "path";
1367
+ import fs4 from "fs/promises";
1368
+ import os3 from "os";
1369
+ import path5 from "path";
833
1370
 
834
1371
  // src/engine/activity.ts
835
- import { execFile } from "child_process";
1372
+ import { execFile as execFile2 } from "child_process";
836
1373
  var RECENT_WRITE_MS = 2 * 60 * 1e3;
837
1374
  var OPEN_TOOL_WINDOW_MS = 15 * 60 * 1e3;
838
1375
  function listProcesses() {
839
1376
  return new Promise((resolve) => {
840
- execFile("ps", ["-axww", "-o", "pid=,args="], { maxBuffer: 64 * 1024 * 1024, timeout: 5e3 }, (error, stdout) => {
1377
+ execFile2("ps", ["-axww", "-o", "pid=,args="], { maxBuffer: 64 * 1024 * 1024, timeout: 5e3 }, (error, stdout) => {
841
1378
  if (error) {
842
1379
  resolve([]);
843
1380
  return;
@@ -896,17 +1433,17 @@ function describeActivity(input) {
896
1433
  }
897
1434
 
898
1435
  // src/engine/files.ts
899
- function defaultProviderHomes(home = os2.homedir()) {
1436
+ function defaultProviderHomes(home = os3.homedir()) {
900
1437
  return {
901
- claudeProjects: path2.join(home, ".claude", "projects"),
902
- codexSessions: path2.join(home, ".codex", "sessions"),
903
- codexIndex: path2.join(home, ".codex", "session_index.jsonl")
1438
+ claudeProjects: path5.join(home, ".claude", "projects"),
1439
+ codexSessions: path5.join(home, ".codex", "sessions"),
1440
+ codexIndex: path5.join(home, ".codex", "session_index.jsonl")
904
1441
  };
905
1442
  }
906
1443
  var UUIDISH = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i;
907
1444
  async function safeReaddir(dir) {
908
1445
  try {
909
- return await fs2.readdir(dir, { withFileTypes: true });
1446
+ return await fs4.readdir(dir, { withFileTypes: true });
910
1447
  } catch {
911
1448
  return [];
912
1449
  }
@@ -915,7 +1452,7 @@ async function statCandidate(provider, file) {
915
1452
  const match = UUIDISH.exec(file);
916
1453
  if (!match?.[1]) return null;
917
1454
  try {
918
- const stat = await fs2.stat(file);
1455
+ const stat = await fs4.stat(file);
919
1456
  return { provider, file, id: match[1], mtimeMs: stat.mtimeMs, size: stat.size };
920
1457
  } catch {
921
1458
  return null;
@@ -925,10 +1462,10 @@ async function claudeCandidates(root) {
925
1462
  const out = [];
926
1463
  for (const project of await safeReaddir(root)) {
927
1464
  if (!project.isDirectory()) continue;
928
- const dir = path2.join(root, project.name);
1465
+ const dir = path5.join(root, project.name);
929
1466
  const entries = await safeReaddir(dir);
930
1467
  const stats = await Promise.all(
931
- entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl")).map((e) => statCandidate("claude", path2.join(dir, e.name)))
1468
+ entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl")).map((e) => statCandidate("claude", path5.join(dir, e.name)))
932
1469
  );
933
1470
  for (const s of stats) if (s) out.push(s);
934
1471
  }
@@ -938,14 +1475,14 @@ async function codexCandidates(root) {
938
1475
  const out = [];
939
1476
  for (const year of await safeReaddir(root)) {
940
1477
  if (!year.isDirectory()) continue;
941
- for (const month of await safeReaddir(path2.join(root, year.name))) {
1478
+ for (const month of await safeReaddir(path5.join(root, year.name))) {
942
1479
  if (!month.isDirectory()) continue;
943
- for (const day of await safeReaddir(path2.join(root, year.name, month.name))) {
1480
+ for (const day of await safeReaddir(path5.join(root, year.name, month.name))) {
944
1481
  if (!day.isDirectory()) continue;
945
- const dir = path2.join(root, year.name, month.name, day.name);
1482
+ const dir = path5.join(root, year.name, month.name, day.name);
946
1483
  const entries = await safeReaddir(dir);
947
1484
  const stats = await Promise.all(
948
- entries.filter((e) => e.isFile() && e.name.startsWith("rollout-") && e.name.endsWith(".jsonl")).map((e) => statCandidate("codex", path2.join(dir, e.name)))
1485
+ entries.filter((e) => e.isFile() && e.name.startsWith("rollout-") && e.name.endsWith(".jsonl")).map((e) => statCandidate("codex", path5.join(dir, e.name)))
949
1486
  );
950
1487
  for (const s of stats) if (s) out.push(s);
951
1488
  }
@@ -954,7 +1491,7 @@ async function codexCandidates(root) {
954
1491
  return out;
955
1492
  }
956
1493
  async function readSlice(file, start, length) {
957
- const handle = await fs2.open(file, "r");
1494
+ const handle = await fs4.open(file, "r");
958
1495
  try {
959
1496
  const buffer = Buffer.alloc(length);
960
1497
  const { bytesRead } = await handle.read(buffer, 0, length, start);
@@ -1027,9 +1564,9 @@ async function readCodexIndex(file) {
1027
1564
  const names = /* @__PURE__ */ new Map();
1028
1565
  let text;
1029
1566
  try {
1030
- const stat = await fs2.stat(file);
1567
+ const stat = await fs4.stat(file);
1031
1568
  const budget = 4 * 1024 * 1024;
1032
- text = stat.size <= budget ? await fs2.readFile(file, "utf8") : completeLines(await readSlice(file, stat.size - budget, budget), true);
1569
+ text = stat.size <= budget ? await fs4.readFile(file, "utf8") : completeLines(await readSlice(file, stat.size - budget, budget), true);
1033
1570
  } catch {
1034
1571
  return names;
1035
1572
  }
@@ -1150,7 +1687,7 @@ var MAX_HISTORY_BUDGET = 256 * 1024 * 1024;
1150
1687
  async function readSessionFile(provider, file, endOffset, maxBytes = HISTORY_BUDGET) {
1151
1688
  let size;
1152
1689
  try {
1153
- size = (await fs2.stat(file)).size;
1690
+ size = (await fs4.stat(file)).size;
1154
1691
  } catch {
1155
1692
  return {
1156
1693
  items: [],
@@ -1204,9 +1741,9 @@ async function describeSessionFile(candidate, codexNames) {
1204
1741
  }
1205
1742
 
1206
1743
  // src/engine/preferences.ts
1207
- import { randomUUID } from "crypto";
1208
- import fs3 from "fs/promises";
1209
- import path3 from "path";
1744
+ import { randomUUID as randomUUID2 } from "crypto";
1745
+ import fs5 from "fs/promises";
1746
+ import path6 from "path";
1210
1747
  var PreferenceStore = class {
1211
1748
  constructor(dir) {
1212
1749
  this.dir = dir;
@@ -1215,11 +1752,11 @@ var PreferenceStore = class {
1215
1752
  data = {};
1216
1753
  writing = Promise.resolve();
1217
1754
  get file() {
1218
- return path3.join(this.dir, "preferences.json");
1755
+ return path6.join(this.dir, "preferences.json");
1219
1756
  }
1220
1757
  async load() {
1221
1758
  try {
1222
- const parsed = JSON.parse(await fs3.readFile(this.file, "utf8"));
1759
+ const parsed = JSON.parse(await fs5.readFile(this.file, "utf8"));
1223
1760
  this.data = typeof parsed === "object" && parsed !== null ? parsed : {};
1224
1761
  } catch {
1225
1762
  this.data = {};
@@ -1257,10 +1794,10 @@ var PreferenceStore = class {
1257
1794
  persist() {
1258
1795
  const snapshot = JSON.stringify(this.data, null, 1);
1259
1796
  this.writing = this.writing.then(async () => {
1260
- await fs3.mkdir(this.dir, { recursive: true });
1261
- const tmp = `${this.file}.${randomUUID()}.tmp`;
1262
- await fs3.writeFile(tmp, snapshot, { mode: 384 });
1263
- await fs3.rename(tmp, this.file);
1797
+ await fs5.mkdir(this.dir, { recursive: true });
1798
+ const tmp = `${this.file}.${randomUUID2()}.tmp`;
1799
+ await fs5.writeFile(tmp, snapshot, { mode: 384 });
1800
+ await fs5.rename(tmp, this.file);
1264
1801
  }).catch(() => {
1265
1802
  });
1266
1803
  }
@@ -1417,9 +1954,9 @@ async function searchLocalChats(query, options = {}) {
1417
1954
  }
1418
1955
 
1419
1956
  // src/engine/store.ts
1420
- import { randomUUID as randomUUID2 } from "crypto";
1421
- import fs4 from "fs/promises";
1422
- import path4 from "path";
1957
+ import { randomUUID as randomUUID3 } from "crypto";
1958
+ import fs6 from "fs/promises";
1959
+ import path7 from "path";
1423
1960
  var MAX_RUNS = 2e3;
1424
1961
  var RunIndex = class {
1425
1962
  constructor(dir) {
@@ -1429,12 +1966,12 @@ var RunIndex = class {
1429
1966
  data = { version: 1, sessions: [], runs: [] };
1430
1967
  writing = Promise.resolve();
1431
1968
  get file() {
1432
- return path4.join(this.dir, "index.json");
1969
+ return path7.join(this.dir, "index.json");
1433
1970
  }
1434
1971
  async load() {
1435
- await fs4.mkdir(this.dir, { recursive: true });
1972
+ await fs6.mkdir(this.dir, { recursive: true });
1436
1973
  try {
1437
- const parsed = JSON.parse(await fs4.readFile(this.file, "utf8"));
1974
+ const parsed = JSON.parse(await fs6.readFile(this.file, "utf8"));
1438
1975
  this.data = {
1439
1976
  version: 1,
1440
1977
  sessions: Array.isArray(parsed.sessions) ? parsed.sessions : [],
@@ -1476,9 +2013,9 @@ var RunIndex = class {
1476
2013
  persist() {
1477
2014
  const snapshot = JSON.stringify(this.data, null, 1);
1478
2015
  this.writing = this.writing.then(async () => {
1479
- const tmp = `${this.file}.${randomUUID2()}.tmp`;
1480
- await fs4.writeFile(tmp, snapshot, { mode: 384 });
1481
- await fs4.rename(tmp, this.file);
2016
+ const tmp = `${this.file}.${randomUUID3()}.tmp`;
2017
+ await fs6.writeFile(tmp, snapshot, { mode: 384 });
2018
+ await fs6.rename(tmp, this.file);
1482
2019
  }).catch(() => {
1483
2020
  });
1484
2021
  }
@@ -1488,27 +2025,27 @@ var RunIndex = class {
1488
2025
  /** A stable Paperclip agent/company identity for this install, so the
1489
2026
  * driver's scratch state is reused rather than growing per run. */
1490
2027
  async identity() {
1491
- const file = path4.join(this.dir, "identity.json");
2028
+ const file = path7.join(this.dir, "identity.json");
1492
2029
  try {
1493
- const parsed = JSON.parse(await fs4.readFile(file, "utf8"));
2030
+ const parsed = JSON.parse(await fs6.readFile(file, "utf8"));
1494
2031
  if (typeof parsed.companyId === "string" && typeof parsed.agentId === "string") {
1495
2032
  return { companyId: parsed.companyId, agentId: parsed.agentId };
1496
2033
  }
1497
2034
  } catch {
1498
2035
  }
1499
- const identity = { companyId: randomUUID2(), agentId: randomUUID2() };
1500
- await fs4.writeFile(file, JSON.stringify(identity), { mode: 384 });
2036
+ const identity = { companyId: randomUUID3(), agentId: randomUUID3() };
2037
+ await fs6.writeFile(file, JSON.stringify(identity), { mode: 384 });
1501
2038
  return identity;
1502
2039
  }
1503
2040
  };
1504
2041
 
1505
2042
  // src/engine/user-defaults.ts
1506
- import fs5 from "fs/promises";
1507
- import os3 from "os";
1508
- import path5 from "path";
2043
+ import fs7 from "fs/promises";
2044
+ import os4 from "os";
2045
+ import path8 from "path";
1509
2046
  async function readJson(file) {
1510
2047
  try {
1511
- const parsed = JSON.parse(await fs5.readFile(file, "utf8"));
2048
+ const parsed = JSON.parse(await fs7.readFile(file, "utf8"));
1512
2049
  return typeof parsed === "object" && parsed !== null ? parsed : null;
1513
2050
  } catch {
1514
2051
  return null;
@@ -1517,10 +2054,10 @@ async function readJson(file) {
1517
2054
  function str3(value) {
1518
2055
  return typeof value === "string" && value.trim() ? value.trim() : null;
1519
2056
  }
1520
- async function claudeDefaults(cwd, home = os3.homedir()) {
2057
+ async function claudeDefaults(cwd, home = os4.homedir()) {
1521
2058
  const files = [
1522
- ...cwd ? [path5.join(cwd, ".claude", "settings.local.json"), path5.join(cwd, ".claude", "settings.json")] : [],
1523
- path5.join(home, ".claude", "settings.json")
2059
+ ...cwd ? [path8.join(cwd, ".claude", "settings.local.json"), path8.join(cwd, ".claude", "settings.json")] : [],
2060
+ path8.join(home, ".claude", "settings.json")
1524
2061
  ];
1525
2062
  let model = null;
1526
2063
  let effort = null;
@@ -1533,10 +2070,10 @@ async function claudeDefaults(cwd, home = os3.homedir()) {
1533
2070
  }
1534
2071
  return { model, effort };
1535
2072
  }
1536
- async function codexDefaults(home = os3.homedir()) {
2073
+ async function codexDefaults(home = os4.homedir()) {
1537
2074
  let text;
1538
2075
  try {
1539
- text = await fs5.readFile(path5.join(home, ".codex", "config.toml"), "utf8");
2076
+ text = await fs7.readFile(path8.join(home, ".codex", "config.toml"), "utf8");
1540
2077
  } catch {
1541
2078
  return { model: null, effort: null };
1542
2079
  }
@@ -1560,45 +2097,33 @@ var EFFORT_LEVELS = {
1560
2097
  // src/engine/index.ts
1561
2098
  var RAW_TRANSCRIPT_MAX_BYTES = 200 * 1048576;
1562
2099
  function moduleDir() {
1563
- return path6.dirname(fileURLToPath(import.meta.url));
2100
+ return path9.dirname(fileURLToPath(import.meta.url));
1564
2101
  }
1565
2102
  function resolveWorkerPath() {
1566
2103
  const dir = moduleDir();
1567
2104
  for (const candidate of [
1568
- path6.join(dir, "worker.js"),
1569
- path6.join(dir, "worker.ts"),
1570
- path6.join(dir, "..", "engine", "worker.js"),
1571
- path6.join(dir, "..", "engine", "worker.ts")
2105
+ path9.join(dir, "worker.js"),
2106
+ path9.join(dir, "worker.ts"),
2107
+ path9.join(dir, "..", "engine", "worker.js"),
2108
+ path9.join(dir, "..", "engine", "worker.ts")
1572
2109
  ]) {
1573
2110
  if (existsSync(candidate)) return candidate;
1574
2111
  }
1575
2112
  throw new Error(`[coding-sessions] The driver worker is missing next to ${dir}.`);
1576
2113
  }
1577
2114
  async function installShim(dataDir) {
1578
- const source = path6.resolve(moduleDir(), "..", "..", "bin", "cli-shim.sh");
2115
+ const source = path9.resolve(moduleDir(), "..", "..", "bin", "cli-shim.sh");
1579
2116
  if (!existsSync(source)) throw new Error(`[coding-sessions] The CLI shim is missing at ${source}.`);
1580
- const body = await fs6.readFile(source, "utf8");
1581
- const target = path6.join(dataDir, "bin", "cli-shim.sh");
1582
- const current = await fs6.readFile(target, "utf8").catch(() => null);
2117
+ const body = await fs8.readFile(source, "utf8");
2118
+ const target = path9.join(dataDir, "bin", "cli-shim.sh");
2119
+ const current = await fs8.readFile(target, "utf8").catch(() => null);
1583
2120
  if (current !== body) {
1584
- await fs6.mkdir(path6.dirname(target), { recursive: true });
1585
- await fs6.writeFile(target, body, { mode: 493 });
2121
+ await fs8.mkdir(path9.dirname(target), { recursive: true });
2122
+ await fs8.writeFile(target, body, { mode: 493 });
1586
2123
  }
1587
- await fs6.chmod(target, 493);
2124
+ await fs8.chmod(target, 493);
1588
2125
  return target;
1589
2126
  }
1590
- function whichOnPath(name, pathValue) {
1591
- for (const dir of pathValue.split(path6.delimiter)) {
1592
- if (!dir) continue;
1593
- const candidate = path6.join(dir, name);
1594
- try {
1595
- accessSync(candidate, fsConstants.X_OK);
1596
- return candidate;
1597
- } catch {
1598
- }
1599
- }
1600
- return null;
1601
- }
1602
2127
  function nowIso() {
1603
2128
  return (/* @__PURE__ */ new Date()).toISOString();
1604
2129
  }
@@ -1637,12 +2162,14 @@ function describeProviderStatus(provider, probe, lastProblem = null, quota = [])
1637
2162
  function describeAuthStatus(provider, probe) {
1638
2163
  const label = PROVIDER_LABEL[provider];
1639
2164
  const installed = probe?.installed === true;
1640
- const version = typeof probe?.version === "string" ? probe.version : null;
2165
+ const binary = probe?.binary ?? null;
2166
+ const version = binary?.version ?? (typeof probe?.version === "string" ? probe.version : null);
1641
2167
  const base = {
1642
2168
  provider,
1643
2169
  label,
1644
2170
  installed,
1645
2171
  version,
2172
+ binary,
1646
2173
  loggedIn: false,
1647
2174
  billing: "unknown",
1648
2175
  plan: null,
@@ -1694,29 +2221,30 @@ function describeAuthStatus(provider, probe) {
1694
2221
  };
1695
2222
  }
1696
2223
  async function createCodingSessionsEngine(options = {}) {
1697
- const dataDir = options.dataDir ?? path6.join(os4.homedir(), ".matrx", "coding-sessions");
1698
- const stateDir = path6.join(dataDir, "paperclip-state");
2224
+ const dataDir = options.dataDir ?? path9.join(os5.homedir(), ".matrx", "coding-sessions");
2225
+ const stateDir = path9.join(dataDir, "paperclip-state");
1699
2226
  const homes = options.homes ?? defaultProviderHomes();
1700
2227
  const index = new RunIndex(dataDir);
1701
2228
  await index.load();
1702
2229
  const preferences = new PreferenceStore(dataDir);
1703
2230
  await preferences.load();
1704
- await fs6.mkdir(stateDir, { recursive: true });
2231
+ await fs8.mkdir(stateDir, { recursive: true });
1705
2232
  const identity = await index.identity();
2233
+ const agents = createAgentConnection(options.agents, dataDir);
1706
2234
  const backups = options.rawBackup ? createRawBackupScheduler({
1707
2235
  sink: options.rawBackup,
1708
2236
  findFile: (provider, providerSessionId) => findSessionFile(provider, providerSessionId, homes),
1709
2237
  onError: (error) => console.warn("[coding-sessions] raw transcript backup failed:", error)
1710
2238
  }) : null;
1711
2239
  const discoveredSeen = /* @__PURE__ */ new Map();
1712
- const healthFile = path6.join(dataDir, "provider-health.json");
2240
+ const healthFile = path9.join(dataDir, "provider-health.json");
1713
2241
  let health = {};
1714
2242
  try {
1715
- health = JSON.parse(await fs6.readFile(healthFile, "utf8"));
2243
+ health = JSON.parse(await fs8.readFile(healthFile, "utf8"));
1716
2244
  } catch {
1717
2245
  health = {};
1718
2246
  }
1719
- const saveHealth = () => fs6.writeFile(healthFile, JSON.stringify(health), { mode: 384 }).catch(() => void 0);
2247
+ const saveHealth = () => fs8.writeFile(healthFile, JSON.stringify(health), { mode: 384 }).catch(() => void 0);
1720
2248
  function noteRunOutcome(provider, status, texts) {
1721
2249
  if (status === "completed") {
1722
2250
  if (health[provider]) {
@@ -1744,7 +2272,24 @@ async function createCodingSessionsEngine(options = {}) {
1744
2272
  // Marks the forked process as a driver worker (see worker.ts).
1745
2273
  MATRX_CODING_SESSIONS_WORKER: "1"
1746
2274
  });
1747
- const commandFor = (provider) => options.commands?.[provider] ?? whichOnPath(provider, workerEnv.PATH ?? "");
2275
+ const binaryHome = options.hostEnv?.HOME ?? os5.homedir();
2276
+ const binaryCache = /* @__PURE__ */ new Map();
2277
+ function binaryFor(provider, fresh = false) {
2278
+ const cached = binaryCache.get(provider);
2279
+ if (cached && !fresh && Date.now() - cached.at < 5 * 6e4) return cached.value;
2280
+ const forced = options.commands?.[provider];
2281
+ const value = resolveBinary(provider, {
2282
+ env: workerEnv,
2283
+ home: binaryHome,
2284
+ forced,
2285
+ // A forced command needs no discovery to run; the status check still
2286
+ // looks, so it can say when a newer copy exists.
2287
+ discover: !forced || fresh
2288
+ }).catch(() => forced ? { path: forced, version: null, source: "configured", sourceLabel: "set by host", newer: null } : null);
2289
+ if (!forced || !fresh) binaryCache.set(provider, { at: Date.now(), value });
2290
+ return value;
2291
+ }
2292
+ const commandFor = async (provider) => (await binaryFor(provider))?.path ?? null;
1748
2293
  const workerPath = resolveWorkerPath();
1749
2294
  const shimPath = await installShim(dataDir);
1750
2295
  const listeners = /* @__PURE__ */ new Set();
@@ -1815,8 +2360,8 @@ async function createCodingSessionsEngine(options = {}) {
1815
2360
  }
1816
2361
  async function startRun(session, prompt) {
1817
2362
  const provider = session.provider;
1818
- const realCommand = commandFor(provider);
1819
- const runId = randomUUID3();
2363
+ const realCommand = await commandFor(provider);
2364
+ const runId = randomUUID4();
1820
2365
  const model = session.model ?? (provider === "claude" ? (await userDefaults(provider, session.cwd)).model ?? null : null);
1821
2366
  const run = {
1822
2367
  runId,
@@ -1854,7 +2399,26 @@ async function createCodingSessionsEngine(options = {}) {
1854
2399
  let fileOffset = null;
1855
2400
  if (session.providerSessionId) {
1856
2401
  const file = await findSessionFile(provider, session.providerSessionId, homes);
1857
- if (file) fileOffset = (await fs6.stat(file).catch(() => null))?.size ?? null;
2402
+ if (file) fileOffset = (await fs8.stat(file).catch(() => null))?.size ?? null;
2403
+ }
2404
+ let agentArgs = [];
2405
+ if (session.agent) {
2406
+ const bundle = await agents.readSessionBundle(session.id);
2407
+ if (!bundle) {
2408
+ const failed = saveRun({
2409
+ ...run,
2410
+ status: "failed",
2411
+ endedAt: nowIso(),
2412
+ error: `The agent ${session.agent.name} could not be loaded on this computer anymore. Start a new session to load it again.`
2413
+ });
2414
+ const current = index.session(session.id);
2415
+ if (current) saveSession({ ...current, lastRunStatus: "failed" });
2416
+ return failed;
2417
+ }
2418
+ const attachment = await writeAgentAttachment(agents.sessionDir(session.id), bundle, {
2419
+ mcpUrl: await agents.mcpUrlFor(session.id)
2420
+ });
2421
+ agentArgs = agentArgsFor(provider, attachment, { resuming: session.providerSessionId !== null });
1858
2422
  }
1859
2423
  const child = forkWorker();
1860
2424
  const liveRun = {
@@ -2028,7 +2592,8 @@ async function createCodingSessionsEngine(options = {}) {
2028
2592
  resumeSessionId: session.providerSessionId,
2029
2593
  shimPath,
2030
2594
  realCommand,
2031
- identity
2595
+ identity,
2596
+ agentArgs
2032
2597
  });
2033
2598
  return run;
2034
2599
  }
@@ -2047,7 +2612,7 @@ async function createCodingSessionsEngine(options = {}) {
2047
2612
  }, 3e3);
2048
2613
  }
2049
2614
  async function pruneDriverScratch() {
2050
- const runsDir = path6.join(
2615
+ const runsDir = path9.join(
2051
2616
  stateDir,
2052
2617
  "instances",
2053
2618
  "default",
@@ -2058,12 +2623,13 @@ async function createCodingSessionsEngine(options = {}) {
2058
2623
  "claude-runtime",
2059
2624
  "runs"
2060
2625
  );
2061
- const entries = await fs6.readdir(runsDir).catch(() => []);
2626
+ const entries = await fs8.readdir(runsDir).catch(() => []);
2062
2627
  await Promise.all(
2063
- entries.filter((name) => !live.has(name)).map((name) => fs6.rm(path6.join(runsDir, name), { recursive: true, force: true }).catch(() => void 0))
2628
+ entries.filter((name) => !live.has(name)).map((name) => fs8.rm(path9.join(runsDir, name), { recursive: true, force: true }).catch(() => void 0))
2064
2629
  );
2065
2630
  }
2066
2631
  async function probe() {
2632
+ const [claudeBinary, codexBinary] = await Promise.all([binaryFor("claude", true), binaryFor("codex", true)]);
2067
2633
  return new Promise((resolve) => {
2068
2634
  const child = forkWorker();
2069
2635
  let answered = false;
@@ -2079,8 +2645,8 @@ async function createCodingSessionsEngine(options = {}) {
2079
2645
  answered = true;
2080
2646
  clearTimeout(timer);
2081
2647
  resolve({
2082
- claude: message.claude ?? {},
2083
- codex: message.codex ?? {}
2648
+ claude: { ...message.claude ?? {}, binary: claudeBinary },
2649
+ codex: { ...message.codex ?? {}, binary: codexBinary }
2084
2650
  });
2085
2651
  }
2086
2652
  });
@@ -2092,7 +2658,7 @@ async function createCodingSessionsEngine(options = {}) {
2092
2658
  });
2093
2659
  child.send({
2094
2660
  kind: "probe",
2095
- commands: { claude: commandFor("claude"), codex: commandFor("codex") },
2661
+ commands: { claude: claudeBinary?.path ?? null, codex: codexBinary?.path ?? null },
2096
2662
  quota: options.quota !== false
2097
2663
  });
2098
2664
  });
@@ -2100,7 +2666,7 @@ async function createCodingSessionsEngine(options = {}) {
2100
2666
  function newSession(input) {
2101
2667
  const at = nowIso();
2102
2668
  return {
2103
- id: randomUUID3(),
2669
+ id: randomUUID4(),
2104
2670
  provider: input.provider,
2105
2671
  providerSessionId: input.providerSessionId,
2106
2672
  cwd: input.cwd,
@@ -2118,17 +2684,17 @@ async function createCodingSessionsEngine(options = {}) {
2118
2684
  function resolveFolder(raw) {
2119
2685
  const trimmed = raw.trim();
2120
2686
  if (!trimmed) return null;
2121
- return path6.resolve(trimmed.replace(/^~(?=$|\/)/, os4.homedir()));
2687
+ return path9.resolve(trimmed.replace(/^~(?=$|\/)/, os5.homedir()));
2122
2688
  }
2123
2689
  async function checkFolder(raw) {
2124
2690
  const target = resolveFolder(raw);
2125
2691
  if (!target) return { path: "", exists: false, isDirectory: false };
2126
- const stat = await fs6.stat(target).catch(() => null);
2692
+ const stat = await fs8.stat(target).catch(() => null);
2127
2693
  return { path: target, exists: stat !== null, isDirectory: stat?.isDirectory() === true };
2128
2694
  }
2129
2695
  async function assertFolder(cwd) {
2130
- const resolved = path6.resolve(cwd.replace(/^~(?=$|\/)/, os4.homedir()));
2131
- const stat = await fs6.stat(resolved).catch(() => null);
2696
+ const resolved = path9.resolve(cwd.replace(/^~(?=$|\/)/, os5.homedir()));
2697
+ const stat = await fs8.stat(resolved).catch(() => null);
2132
2698
  if (!stat?.isDirectory()) {
2133
2699
  throw new Error(`The folder ${resolved} doesn't exist on this computer.`);
2134
2700
  }
@@ -2190,7 +2756,7 @@ async function createCodingSessionsEngine(options = {}) {
2190
2756
  folders.map(async ([folder, lastUsedAt]) => ({
2191
2757
  path: folder,
2192
2758
  lastUsedAt,
2193
- exists: (await fs6.stat(folder).catch(() => null))?.isDirectory() === true
2759
+ exists: (await fs8.stat(folder).catch(() => null))?.isDirectory() === true
2194
2760
  }))
2195
2761
  );
2196
2762
  },
@@ -2235,7 +2801,7 @@ async function createCodingSessionsEngine(options = {}) {
2235
2801
  async createFolder(raw) {
2236
2802
  const target = resolveFolder(raw);
2237
2803
  if (!target) throw new Error("Type a folder path first.");
2238
- await fs6.mkdir(target, { recursive: true });
2804
+ await fs8.mkdir(target, { recursive: true });
2239
2805
  return checkFolder(target);
2240
2806
  },
2241
2807
  async sessionActivity(provider, providerSessionId) {
@@ -2273,31 +2839,35 @@ async function createCodingSessionsEngine(options = {}) {
2273
2839
  reason: `${PROVIDER_LABEL[provider]} has no saved transcript for this chat on this computer.`
2274
2840
  };
2275
2841
  }
2276
- const stat = await fs6.stat(file);
2842
+ const stat = await fs8.stat(file);
2277
2843
  if (stat.size > RAW_TRANSCRIPT_MAX_BYTES) {
2278
2844
  return {
2279
2845
  available: false,
2280
2846
  reason: `This transcript is ${formatFileSize(stat.size)}, larger than the ${formatFileSize(RAW_TRANSCRIPT_MAX_BYTES)} this page can hand over. It is at ${file}.`
2281
2847
  };
2282
2848
  }
2283
- const content = await fs6.readFile(file, "utf8");
2284
- return { available: true, fileName: path6.basename(file), bytes: stat.size, content };
2849
+ const content = await fs8.readFile(file, "utf8");
2850
+ return { available: true, fileName: path9.basename(file), bytes: stat.size, content };
2285
2851
  },
2286
2852
  async start(input) {
2287
2853
  const prompt = input.prompt.trim();
2288
2854
  if (!prompt) throw new Error("Type what you want done before starting.");
2289
2855
  const cwd = await assertFolder(input.cwd);
2290
- const session = saveSession(
2291
- newSession({
2292
- provider: input.provider,
2293
- cwd,
2294
- title: titleFrom(prompt),
2295
- model: input.model?.trim() || null,
2296
- effort: input.effort?.trim() || null,
2297
- origin: "started",
2298
- providerSessionId: null
2299
- })
2300
- );
2856
+ const bundle = input.agent ? await agents.loadBundle(input.agent) : null;
2857
+ const draft = newSession({
2858
+ provider: input.provider,
2859
+ cwd,
2860
+ title: titleFrom(prompt),
2861
+ model: input.model?.trim() || null,
2862
+ effort: input.effort?.trim() || null,
2863
+ origin: "started",
2864
+ providerSessionId: null
2865
+ });
2866
+ if (bundle) {
2867
+ await agents.saveSessionBundle(draft.id, bundle);
2868
+ draft.agent = sessionAgentRecord(bundle, { toolsServed: await agents.mcpUrlFor(draft.id) !== null });
2869
+ }
2870
+ const session = saveSession(draft);
2301
2871
  const run = await startRun(session, prompt);
2302
2872
  return { session: index.session(session.id) ?? session, run, queued: false };
2303
2873
  },
@@ -2366,6 +2936,21 @@ async function createCodingSessionsEngine(options = {}) {
2366
2936
  if (opts && "effort" in opts) next.effort = typeof opts.effort === "string" && opts.effort.trim() ? opts.effort.trim() : null;
2367
2937
  return saveSession(next);
2368
2938
  },
2939
+ async agentAccess() {
2940
+ return agents.access();
2941
+ },
2942
+ async agentCatalogRpc(request) {
2943
+ return agents.catalogRpc(request);
2944
+ },
2945
+ async agentDetails(agentId) {
2946
+ return agents.details(String(agentId ?? ""));
2947
+ },
2948
+ setMcpBaseUrl(baseUrl) {
2949
+ agents.setMcpBaseUrl(baseUrl);
2950
+ },
2951
+ async handleMcp(secret, body) {
2952
+ return agents.handleMcp(secret, body);
2953
+ },
2369
2954
  async dispose() {
2370
2955
  for (const liveRun of live.values()) {
2371
2956
  liveRun.stopRequested = true;
@@ -2402,7 +2987,7 @@ function openFolderIn(folder, target) {
2402
2987
  args = ["-a", app.app, folder];
2403
2988
  }
2404
2989
  return new Promise((resolve, reject) => {
2405
- execFile2(command, args, { timeout: 1e4 }, (error) => {
2990
+ execFile3(command, args, { timeout: 1e4 }, (error) => {
2406
2991
  if (!error) return resolve();
2407
2992
  const label = target === "finder" ? "the folder" : OPEN_APP[target]?.label ?? target;
2408
2993
  reject(