@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.
- package/CHANGELOG.md +24 -0
- package/dist/engine/index.cjs +724 -139
- package/dist/engine/index.cjs.map +1 -1
- package/dist/engine/index.d.cts +4 -1
- package/dist/engine/index.d.ts +4 -1
- package/dist/engine/index.js +717 -132
- package/dist/engine/index.js.map +1 -1
- package/dist/engine/worker.js +14 -2
- package/dist/engine/worker.js.map +1 -1
- package/dist/{index-RYOb-YZT.d.cts → index-DEPwyr2j.d.cts} +148 -0
- package/dist/{index-RYOb-YZT.d.ts → index-DEPwyr2j.d.ts} +148 -0
- package/dist/index.cjs +21 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +105 -1
- package/dist/index.d.ts +105 -1
- package/dist/index.js +21 -3
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +722 -410
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +105 -7
- package/dist/react/index.d.ts +105 -7
- package/dist/react/index.js +730 -405
- package/dist/react/index.js.map +1 -1
- package/dist/server/index.cjs +792 -155
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +4 -1
- package/dist/server/index.d.ts +4 -1
- package/dist/server/index.js +783 -146
- package/dist/server/index.js.map +1 -1
- package/dist/ui/assets/index-DSWoryQZ.js +72 -0
- package/dist/ui/assets/index-Gp2bMDtx.css +2 -0
- package/dist/ui/index.html +2 -2
- package/package.json +4 -2
- package/dist/ui/assets/index-B3Ic2ziA.js +0 -69
- package/dist/ui/assets/index-BAkpc01s.css +0 -2
package/dist/server/index.js
CHANGED
|
@@ -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
|
|
4
|
+
import fs9 from "fs/promises";
|
|
5
5
|
import http from "http";
|
|
6
|
-
import
|
|
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
|
|
12
|
-
import { randomUUID as
|
|
13
|
-
import {
|
|
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, "&").replace(/"/g, """).replace(/</g, "<");
|
|
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
|
|
75
|
-
import
|
|
76
|
-
import
|
|
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/
|
|
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
|
|
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
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
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 ??
|
|
1325
|
+
const home = out.HOME ?? os2.homedir();
|
|
789
1326
|
out.HOME = home;
|
|
790
|
-
const hostPath = (hostEnv.PATH ?? "").split(
|
|
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(
|
|
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
|
|
803
|
-
import
|
|
804
|
-
import
|
|
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
|
-
|
|
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 =
|
|
1408
|
+
function defaultProviderHomes(home = os3.homedir()) {
|
|
872
1409
|
return {
|
|
873
|
-
claudeProjects:
|
|
874
|
-
codexSessions:
|
|
875
|
-
codexIndex:
|
|
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
|
|
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
|
|
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 =
|
|
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",
|
|
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(
|
|
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(
|
|
1452
|
+
for (const day of await safeReaddir(path5.join(root, year.name, month.name))) {
|
|
916
1453
|
if (!day.isDirectory()) continue;
|
|
917
|
-
const dir =
|
|
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",
|
|
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
|
|
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
|
|
1539
|
+
const stat = await fs4.stat(file);
|
|
1003
1540
|
const budget = 4 * 1024 * 1024;
|
|
1004
|
-
text = stat.size <= budget ? await
|
|
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
|
|
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
|
|
1181
|
-
import
|
|
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
|
|
1727
|
+
return path6.join(this.dir, "preferences.json");
|
|
1191
1728
|
}
|
|
1192
1729
|
async load() {
|
|
1193
1730
|
try {
|
|
1194
|
-
const parsed = JSON.parse(await
|
|
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
|
|
1233
|
-
const tmp = `${this.file}.${
|
|
1234
|
-
await
|
|
1235
|
-
await
|
|
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
|
|
1393
|
-
import
|
|
1394
|
-
import
|
|
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
|
|
1941
|
+
return path7.join(this.dir, "index.json");
|
|
1405
1942
|
}
|
|
1406
1943
|
async load() {
|
|
1407
|
-
await
|
|
1944
|
+
await fs6.mkdir(this.dir, { recursive: true });
|
|
1408
1945
|
try {
|
|
1409
|
-
const parsed = JSON.parse(await
|
|
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}.${
|
|
1452
|
-
await
|
|
1453
|
-
await
|
|
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 =
|
|
2000
|
+
const file = path7.join(this.dir, "identity.json");
|
|
1464
2001
|
try {
|
|
1465
|
-
const parsed = JSON.parse(await
|
|
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:
|
|
1472
|
-
await
|
|
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
|
|
1479
|
-
import
|
|
1480
|
-
import
|
|
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
|
|
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 =
|
|
2029
|
+
async function claudeDefaults(cwd, home = os4.homedir()) {
|
|
1493
2030
|
const files = [
|
|
1494
|
-
...cwd ? [
|
|
1495
|
-
|
|
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 =
|
|
2045
|
+
async function codexDefaults(home = os4.homedir()) {
|
|
1509
2046
|
let text;
|
|
1510
2047
|
try {
|
|
1511
|
-
text = await
|
|
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
|
|
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
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
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 =
|
|
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
|
|
1553
|
-
const target =
|
|
1554
|
-
const current = await
|
|
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
|
|
1557
|
-
await
|
|
2093
|
+
await fs8.mkdir(path9.dirname(target), { recursive: true });
|
|
2094
|
+
await fs8.writeFile(target, body, { mode: 493 });
|
|
1558
2095
|
}
|
|
1559
|
-
await
|
|
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
|
|
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 ??
|
|
1670
|
-
const stateDir =
|
|
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
|
|
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 =
|
|
2212
|
+
const healthFile = path9.join(dataDir, "provider-health.json");
|
|
1685
2213
|
let health = {};
|
|
1686
2214
|
try {
|
|
1687
|
-
health = JSON.parse(await
|
|
2215
|
+
health = JSON.parse(await fs8.readFile(healthFile, "utf8"));
|
|
1688
2216
|
} catch {
|
|
1689
2217
|
health = {};
|
|
1690
2218
|
}
|
|
1691
|
-
const saveHealth = () =>
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
2598
|
+
const entries = await fs8.readdir(runsDir).catch(() => []);
|
|
2034
2599
|
await Promise.all(
|
|
2035
|
-
entries.filter((name) => !live.has(name)).map((name) =>
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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 =
|
|
2103
|
-
const stat = await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2256
|
-
return { available: true, fileName:
|
|
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
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
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
|
-
|
|
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
|
|
3042
|
+
return path10.dirname(fileURLToPath2(import.meta.url));
|
|
2455
3043
|
}
|
|
2456
3044
|
function defaultUiDir() {
|
|
2457
3045
|
for (const candidate of [
|
|
2458
|
-
|
|
2459
|
-
|
|
3046
|
+
path10.resolve(moduleDir2(), "..", "ui"),
|
|
3047
|
+
path10.resolve(moduleDir2(), "..", "..", "dist", "ui")
|
|
2460
3048
|
]) {
|
|
2461
|
-
if (existsSync2(
|
|
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 =
|
|
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
|
|
2490
|
-
if (!stat || stat.isDirectory()) file =
|
|
2491
|
-
const body = await
|
|
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 =
|
|
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 ??
|
|
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()));
|