@ai-matrx/coding-sessions 0.1.83 → 0.1.85
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/engine/index.cjs +521 -112
- 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 +521 -112
- package/dist/engine/index.js.map +1 -1
- package/dist/engine/worker.js +2 -2
- package/dist/engine/worker.js.map +1 -1
- package/dist/{index-DQ0teQvw.d.cts → index-DEPwyr2j.d.cts} +130 -0
- package/dist/{index-DQ0teQvw.d.ts → index-DEPwyr2j.d.ts} +130 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +87 -1
- package/dist/index.d.ts +87 -1
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +666 -408
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +87 -7
- package/dist/react/index.d.ts +87 -7
- package/dist/react/index.js +674 -403
- package/dist/react/index.js.map +1 -1
- package/dist/server/index.cjs +588 -127
- 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 +587 -126
- 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-C-_1qjy9.css +0 -2
- package/dist/ui/assets/index-CmFgFvLa.js +0 -69
package/dist/server/index.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
// src/server/index.ts
|
|
2
|
-
import { randomBytes, timingSafeEqual } from "crypto";
|
|
2
|
+
import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
|
|
3
3
|
import { existsSync as existsSync2 } from "fs";
|
|
4
|
-
import
|
|
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
11
|
import { execFile as execFile3, fork } from "child_process";
|
|
12
|
-
import { randomUUID as
|
|
12
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
13
13
|
import { existsSync } from "fs";
|
|
14
14
|
import { formatFileSize } from "@ai-matrx/kit/format";
|
|
15
15
|
|
|
@@ -70,10 +70,379 @@ function createRawBackupScheduler(options) {
|
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
// src/engine/agent-attach.ts
|
|
74
|
+
import fs2 from "fs/promises";
|
|
75
|
+
import path from "path";
|
|
76
|
+
var AGENT_MCP_SERVER_NAME = "matrx-agent";
|
|
77
|
+
var AGENT_PLUGIN_NAME = "matrx-agent";
|
|
78
|
+
function escapeAttr(value) {
|
|
79
|
+
return value.replace(/&/g, "&").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
|
|
443
|
+
import fs8 from "fs/promises";
|
|
75
444
|
import os5 from "os";
|
|
76
|
-
import
|
|
445
|
+
import path9 from "path";
|
|
77
446
|
import { fileURLToPath } from "url";
|
|
78
447
|
|
|
79
448
|
// src/normalize/claude.ts
|
|
@@ -743,7 +1112,7 @@ function describeProblem(provider, problem) {
|
|
|
743
1112
|
import { execFile } from "child_process";
|
|
744
1113
|
import { accessSync, constants as fsConstants, readdirSync, readFileSync, realpathSync } from "fs";
|
|
745
1114
|
import os from "os";
|
|
746
|
-
import
|
|
1115
|
+
import path3 from "path";
|
|
747
1116
|
var SOURCE_LABEL = {
|
|
748
1117
|
codex: { "desktop-app": "from ChatGPT app", path: "from PATH", "npm-global": "from npm", configured: "set by host" },
|
|
749
1118
|
claude: { "desktop-app": "from Claude app", path: "from PATH", "npm-global": "from npm", configured: "set by host" }
|
|
@@ -792,10 +1161,10 @@ function exeName(name, platform) {
|
|
|
792
1161
|
}
|
|
793
1162
|
function allOnPath(name, pathValue, platform = process.platform) {
|
|
794
1163
|
const found = [];
|
|
795
|
-
for (const dir of pathValue.split(
|
|
1164
|
+
for (const dir of pathValue.split(path3.delimiter)) {
|
|
796
1165
|
if (!dir) continue;
|
|
797
1166
|
for (const file of exeName(name, platform)) {
|
|
798
|
-
const candidate =
|
|
1167
|
+
const candidate = path3.join(dir, file);
|
|
799
1168
|
if (executable(candidate)) {
|
|
800
1169
|
found.push(candidate);
|
|
801
1170
|
break;
|
|
@@ -808,17 +1177,17 @@ function desktopAppCandidates(provider, platform, home, applications) {
|
|
|
808
1177
|
if (platform !== "darwin") return [];
|
|
809
1178
|
if (provider === "codex") {
|
|
810
1179
|
return applications.map(
|
|
811
|
-
(apps) =>
|
|
1180
|
+
(apps) => path3.join(apps, "ChatGPT.app", "Contents", "Resources", "codex-cli", "bin", "codex")
|
|
812
1181
|
);
|
|
813
1182
|
}
|
|
814
|
-
const root =
|
|
1183
|
+
const root = path3.join(home, "Library", "Application Support", "Claude", "claude-code");
|
|
815
1184
|
let versions = [];
|
|
816
1185
|
try {
|
|
817
1186
|
versions = readdirSync(root);
|
|
818
1187
|
} catch {
|
|
819
1188
|
return [];
|
|
820
1189
|
}
|
|
821
|
-
return versions.map((v) =>
|
|
1190
|
+
return versions.map((v) => path3.join(root, v, "claude.app", "Contents", "MacOS", "claude"));
|
|
822
1191
|
}
|
|
823
1192
|
function npmGlobalPrefix(env) {
|
|
824
1193
|
const fromEnv = env.npm_config_prefix ?? env.NPM_CONFIG_PREFIX;
|
|
@@ -836,13 +1205,13 @@ function declaredVersion(provider, source, file) {
|
|
|
836
1205
|
if (source !== "desktop-app") return null;
|
|
837
1206
|
if (provider === "codex") {
|
|
838
1207
|
try {
|
|
839
|
-
const pkg = JSON.parse(readFileSync(
|
|
1208
|
+
const pkg = JSON.parse(readFileSync(path3.join(path3.dirname(path3.dirname(file)), "codex-package.json"), "utf8"));
|
|
840
1209
|
return typeof pkg.version === "string" ? parseVersion(pkg.version) : null;
|
|
841
1210
|
} catch {
|
|
842
1211
|
return null;
|
|
843
1212
|
}
|
|
844
1213
|
}
|
|
845
|
-
return parseVersion(
|
|
1214
|
+
return parseVersion(path3.basename(path3.resolve(file, "..", "..", "..", "..")));
|
|
846
1215
|
}
|
|
847
1216
|
function runVersion(file, env) {
|
|
848
1217
|
return new Promise((resolve) => {
|
|
@@ -856,14 +1225,14 @@ async function discoverBinaries(provider, options) {
|
|
|
856
1225
|
const platform = options.platform ?? process.platform;
|
|
857
1226
|
const home = options.home ?? options.env.HOME ?? os.homedir();
|
|
858
1227
|
const listed2 = [
|
|
859
|
-
...desktopAppCandidates(provider, platform, home, options.applications ?? ["/Applications",
|
|
1228
|
+
...desktopAppCandidates(provider, platform, home, options.applications ?? ["/Applications", path3.join(home, "Applications")]).filter(executable).map((p) => ({ path: p, source: "desktop-app" })),
|
|
860
1229
|
...allOnPath(provider, options.env.PATH ?? "", platform).map((p) => ({ path: p, source: "path" }))
|
|
861
1230
|
];
|
|
862
1231
|
const prefix = await npmGlobalPrefix(options.env);
|
|
863
1232
|
if (prefix) {
|
|
864
|
-
const dir = platform === "win32" ? prefix :
|
|
1233
|
+
const dir = platform === "win32" ? prefix : path3.join(prefix, "bin");
|
|
865
1234
|
for (const file of exeName(provider, platform)) {
|
|
866
|
-
const candidate =
|
|
1235
|
+
const candidate = path3.join(dir, file);
|
|
867
1236
|
if (executable(candidate)) {
|
|
868
1237
|
listed2.push({ path: candidate, source: "npm-global" });
|
|
869
1238
|
break;
|
|
@@ -909,7 +1278,7 @@ async function resolveBinary(provider, options) {
|
|
|
909
1278
|
|
|
910
1279
|
// src/engine/clean-env.ts
|
|
911
1280
|
import os2 from "os";
|
|
912
|
-
import
|
|
1281
|
+
import path4 from "path";
|
|
913
1282
|
var ALLOWED_NAMES = /* @__PURE__ */ new Set([
|
|
914
1283
|
"HOME",
|
|
915
1284
|
"USER",
|
|
@@ -931,11 +1300,11 @@ var ALLOWED_NAMES = /* @__PURE__ */ new Set([
|
|
|
931
1300
|
var ALLOWED_PREFIXES = ["LC_"];
|
|
932
1301
|
function wellKnownBinDirs(home) {
|
|
933
1302
|
return [
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
1303
|
+
path4.join(home, ".local", "bin"),
|
|
1304
|
+
path4.join(home, ".claude", "local"),
|
|
1305
|
+
path4.join(home, ".npm-global", "bin"),
|
|
1306
|
+
path4.join(home, ".bun", "bin"),
|
|
1307
|
+
path4.join(home, ".volta", "bin"),
|
|
939
1308
|
"/opt/homebrew/bin",
|
|
940
1309
|
"/usr/local/bin",
|
|
941
1310
|
"/usr/bin",
|
|
@@ -955,21 +1324,21 @@ function buildCleanEnv(hostEnv, extra = {}) {
|
|
|
955
1324
|
}
|
|
956
1325
|
const home = out.HOME ?? os2.homedir();
|
|
957
1326
|
out.HOME = home;
|
|
958
|
-
const hostPath = (hostEnv.PATH ?? "").split(
|
|
1327
|
+
const hostPath = (hostEnv.PATH ?? "").split(path4.delimiter).filter(Boolean);
|
|
959
1328
|
const merged = [];
|
|
960
1329
|
for (const dir of [...hostPath, ...wellKnownBinDirs(home)]) {
|
|
961
1330
|
if (!merged.includes(dir)) merged.push(dir);
|
|
962
1331
|
}
|
|
963
|
-
out.PATH = merged.join(
|
|
1332
|
+
out.PATH = merged.join(path4.delimiter);
|
|
964
1333
|
for (const [name, value] of Object.entries(extra)) out[name] = value;
|
|
965
1334
|
return out;
|
|
966
1335
|
}
|
|
967
1336
|
|
|
968
1337
|
// src/engine/files.ts
|
|
969
1338
|
import { createReadStream } from "fs";
|
|
970
|
-
import
|
|
1339
|
+
import fs4 from "fs/promises";
|
|
971
1340
|
import os3 from "os";
|
|
972
|
-
import
|
|
1341
|
+
import path5 from "path";
|
|
973
1342
|
|
|
974
1343
|
// src/engine/activity.ts
|
|
975
1344
|
import { execFile as execFile2 } from "child_process";
|
|
@@ -1038,15 +1407,15 @@ function describeActivity(input) {
|
|
|
1038
1407
|
// src/engine/files.ts
|
|
1039
1408
|
function defaultProviderHomes(home = os3.homedir()) {
|
|
1040
1409
|
return {
|
|
1041
|
-
claudeProjects:
|
|
1042
|
-
codexSessions:
|
|
1043
|
-
codexIndex:
|
|
1410
|
+
claudeProjects: path5.join(home, ".claude", "projects"),
|
|
1411
|
+
codexSessions: path5.join(home, ".codex", "sessions"),
|
|
1412
|
+
codexIndex: path5.join(home, ".codex", "session_index.jsonl")
|
|
1044
1413
|
};
|
|
1045
1414
|
}
|
|
1046
1415
|
var UUIDISH = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i;
|
|
1047
1416
|
async function safeReaddir(dir) {
|
|
1048
1417
|
try {
|
|
1049
|
-
return await
|
|
1418
|
+
return await fs4.readdir(dir, { withFileTypes: true });
|
|
1050
1419
|
} catch {
|
|
1051
1420
|
return [];
|
|
1052
1421
|
}
|
|
@@ -1055,7 +1424,7 @@ async function statCandidate(provider, file) {
|
|
|
1055
1424
|
const match = UUIDISH.exec(file);
|
|
1056
1425
|
if (!match?.[1]) return null;
|
|
1057
1426
|
try {
|
|
1058
|
-
const stat = await
|
|
1427
|
+
const stat = await fs4.stat(file);
|
|
1059
1428
|
return { provider, file, id: match[1], mtimeMs: stat.mtimeMs, size: stat.size };
|
|
1060
1429
|
} catch {
|
|
1061
1430
|
return null;
|
|
@@ -1065,10 +1434,10 @@ async function claudeCandidates(root) {
|
|
|
1065
1434
|
const out = [];
|
|
1066
1435
|
for (const project of await safeReaddir(root)) {
|
|
1067
1436
|
if (!project.isDirectory()) continue;
|
|
1068
|
-
const dir =
|
|
1437
|
+
const dir = path5.join(root, project.name);
|
|
1069
1438
|
const entries = await safeReaddir(dir);
|
|
1070
1439
|
const stats = await Promise.all(
|
|
1071
|
-
entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl")).map((e) => statCandidate("claude",
|
|
1440
|
+
entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl")).map((e) => statCandidate("claude", path5.join(dir, e.name)))
|
|
1072
1441
|
);
|
|
1073
1442
|
for (const s of stats) if (s) out.push(s);
|
|
1074
1443
|
}
|
|
@@ -1078,14 +1447,14 @@ async function codexCandidates(root) {
|
|
|
1078
1447
|
const out = [];
|
|
1079
1448
|
for (const year of await safeReaddir(root)) {
|
|
1080
1449
|
if (!year.isDirectory()) continue;
|
|
1081
|
-
for (const month of await safeReaddir(
|
|
1450
|
+
for (const month of await safeReaddir(path5.join(root, year.name))) {
|
|
1082
1451
|
if (!month.isDirectory()) continue;
|
|
1083
|
-
for (const day of await safeReaddir(
|
|
1452
|
+
for (const day of await safeReaddir(path5.join(root, year.name, month.name))) {
|
|
1084
1453
|
if (!day.isDirectory()) continue;
|
|
1085
|
-
const dir =
|
|
1454
|
+
const dir = path5.join(root, year.name, month.name, day.name);
|
|
1086
1455
|
const entries = await safeReaddir(dir);
|
|
1087
1456
|
const stats = await Promise.all(
|
|
1088
|
-
entries.filter((e) => e.isFile() && e.name.startsWith("rollout-") && e.name.endsWith(".jsonl")).map((e) => statCandidate("codex",
|
|
1457
|
+
entries.filter((e) => e.isFile() && e.name.startsWith("rollout-") && e.name.endsWith(".jsonl")).map((e) => statCandidate("codex", path5.join(dir, e.name)))
|
|
1089
1458
|
);
|
|
1090
1459
|
for (const s of stats) if (s) out.push(s);
|
|
1091
1460
|
}
|
|
@@ -1094,7 +1463,7 @@ async function codexCandidates(root) {
|
|
|
1094
1463
|
return out;
|
|
1095
1464
|
}
|
|
1096
1465
|
async function readSlice(file, start, length) {
|
|
1097
|
-
const handle = await
|
|
1466
|
+
const handle = await fs4.open(file, "r");
|
|
1098
1467
|
try {
|
|
1099
1468
|
const buffer = Buffer.alloc(length);
|
|
1100
1469
|
const { bytesRead } = await handle.read(buffer, 0, length, start);
|
|
@@ -1167,9 +1536,9 @@ async function readCodexIndex(file) {
|
|
|
1167
1536
|
const names = /* @__PURE__ */ new Map();
|
|
1168
1537
|
let text;
|
|
1169
1538
|
try {
|
|
1170
|
-
const stat = await
|
|
1539
|
+
const stat = await fs4.stat(file);
|
|
1171
1540
|
const budget = 4 * 1024 * 1024;
|
|
1172
|
-
text = stat.size <= budget ? await
|
|
1541
|
+
text = stat.size <= budget ? await fs4.readFile(file, "utf8") : completeLines(await readSlice(file, stat.size - budget, budget), true);
|
|
1173
1542
|
} catch {
|
|
1174
1543
|
return names;
|
|
1175
1544
|
}
|
|
@@ -1290,7 +1659,7 @@ var MAX_HISTORY_BUDGET = 256 * 1024 * 1024;
|
|
|
1290
1659
|
async function readSessionFile(provider, file, endOffset, maxBytes = HISTORY_BUDGET) {
|
|
1291
1660
|
let size;
|
|
1292
1661
|
try {
|
|
1293
|
-
size = (await
|
|
1662
|
+
size = (await fs4.stat(file)).size;
|
|
1294
1663
|
} catch {
|
|
1295
1664
|
return {
|
|
1296
1665
|
items: [],
|
|
@@ -1344,9 +1713,9 @@ async function describeSessionFile(candidate, codexNames) {
|
|
|
1344
1713
|
}
|
|
1345
1714
|
|
|
1346
1715
|
// src/engine/preferences.ts
|
|
1347
|
-
import { randomUUID } from "crypto";
|
|
1348
|
-
import
|
|
1349
|
-
import
|
|
1716
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1717
|
+
import fs5 from "fs/promises";
|
|
1718
|
+
import path6 from "path";
|
|
1350
1719
|
var PreferenceStore = class {
|
|
1351
1720
|
constructor(dir) {
|
|
1352
1721
|
this.dir = dir;
|
|
@@ -1355,11 +1724,11 @@ var PreferenceStore = class {
|
|
|
1355
1724
|
data = {};
|
|
1356
1725
|
writing = Promise.resolve();
|
|
1357
1726
|
get file() {
|
|
1358
|
-
return
|
|
1727
|
+
return path6.join(this.dir, "preferences.json");
|
|
1359
1728
|
}
|
|
1360
1729
|
async load() {
|
|
1361
1730
|
try {
|
|
1362
|
-
const parsed = JSON.parse(await
|
|
1731
|
+
const parsed = JSON.parse(await fs5.readFile(this.file, "utf8"));
|
|
1363
1732
|
this.data = typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
1364
1733
|
} catch {
|
|
1365
1734
|
this.data = {};
|
|
@@ -1397,10 +1766,10 @@ var PreferenceStore = class {
|
|
|
1397
1766
|
persist() {
|
|
1398
1767
|
const snapshot = JSON.stringify(this.data, null, 1);
|
|
1399
1768
|
this.writing = this.writing.then(async () => {
|
|
1400
|
-
await
|
|
1401
|
-
const tmp = `${this.file}.${
|
|
1402
|
-
await
|
|
1403
|
-
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);
|
|
1404
1773
|
}).catch(() => {
|
|
1405
1774
|
});
|
|
1406
1775
|
}
|
|
@@ -1557,9 +1926,9 @@ async function searchLocalChats(query, options = {}) {
|
|
|
1557
1926
|
}
|
|
1558
1927
|
|
|
1559
1928
|
// src/engine/store.ts
|
|
1560
|
-
import { randomUUID as
|
|
1561
|
-
import
|
|
1562
|
-
import
|
|
1929
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1930
|
+
import fs6 from "fs/promises";
|
|
1931
|
+
import path7 from "path";
|
|
1563
1932
|
var MAX_RUNS = 2e3;
|
|
1564
1933
|
var RunIndex = class {
|
|
1565
1934
|
constructor(dir) {
|
|
@@ -1569,12 +1938,12 @@ var RunIndex = class {
|
|
|
1569
1938
|
data = { version: 1, sessions: [], runs: [] };
|
|
1570
1939
|
writing = Promise.resolve();
|
|
1571
1940
|
get file() {
|
|
1572
|
-
return
|
|
1941
|
+
return path7.join(this.dir, "index.json");
|
|
1573
1942
|
}
|
|
1574
1943
|
async load() {
|
|
1575
|
-
await
|
|
1944
|
+
await fs6.mkdir(this.dir, { recursive: true });
|
|
1576
1945
|
try {
|
|
1577
|
-
const parsed = JSON.parse(await
|
|
1946
|
+
const parsed = JSON.parse(await fs6.readFile(this.file, "utf8"));
|
|
1578
1947
|
this.data = {
|
|
1579
1948
|
version: 1,
|
|
1580
1949
|
sessions: Array.isArray(parsed.sessions) ? parsed.sessions : [],
|
|
@@ -1616,9 +1985,9 @@ var RunIndex = class {
|
|
|
1616
1985
|
persist() {
|
|
1617
1986
|
const snapshot = JSON.stringify(this.data, null, 1);
|
|
1618
1987
|
this.writing = this.writing.then(async () => {
|
|
1619
|
-
const tmp = `${this.file}.${
|
|
1620
|
-
await
|
|
1621
|
-
await
|
|
1988
|
+
const tmp = `${this.file}.${randomUUID3()}.tmp`;
|
|
1989
|
+
await fs6.writeFile(tmp, snapshot, { mode: 384 });
|
|
1990
|
+
await fs6.rename(tmp, this.file);
|
|
1622
1991
|
}).catch(() => {
|
|
1623
1992
|
});
|
|
1624
1993
|
}
|
|
@@ -1628,27 +1997,27 @@ var RunIndex = class {
|
|
|
1628
1997
|
/** A stable Paperclip agent/company identity for this install, so the
|
|
1629
1998
|
* driver's scratch state is reused rather than growing per run. */
|
|
1630
1999
|
async identity() {
|
|
1631
|
-
const file =
|
|
2000
|
+
const file = path7.join(this.dir, "identity.json");
|
|
1632
2001
|
try {
|
|
1633
|
-
const parsed = JSON.parse(await
|
|
2002
|
+
const parsed = JSON.parse(await fs6.readFile(file, "utf8"));
|
|
1634
2003
|
if (typeof parsed.companyId === "string" && typeof parsed.agentId === "string") {
|
|
1635
2004
|
return { companyId: parsed.companyId, agentId: parsed.agentId };
|
|
1636
2005
|
}
|
|
1637
2006
|
} catch {
|
|
1638
2007
|
}
|
|
1639
|
-
const identity = { companyId:
|
|
1640
|
-
await
|
|
2008
|
+
const identity = { companyId: randomUUID3(), agentId: randomUUID3() };
|
|
2009
|
+
await fs6.writeFile(file, JSON.stringify(identity), { mode: 384 });
|
|
1641
2010
|
return identity;
|
|
1642
2011
|
}
|
|
1643
2012
|
};
|
|
1644
2013
|
|
|
1645
2014
|
// src/engine/user-defaults.ts
|
|
1646
|
-
import
|
|
2015
|
+
import fs7 from "fs/promises";
|
|
1647
2016
|
import os4 from "os";
|
|
1648
|
-
import
|
|
2017
|
+
import path8 from "path";
|
|
1649
2018
|
async function readJson(file) {
|
|
1650
2019
|
try {
|
|
1651
|
-
const parsed = JSON.parse(await
|
|
2020
|
+
const parsed = JSON.parse(await fs7.readFile(file, "utf8"));
|
|
1652
2021
|
return typeof parsed === "object" && parsed !== null ? parsed : null;
|
|
1653
2022
|
} catch {
|
|
1654
2023
|
return null;
|
|
@@ -1659,8 +2028,8 @@ function str3(value) {
|
|
|
1659
2028
|
}
|
|
1660
2029
|
async function claudeDefaults(cwd, home = os4.homedir()) {
|
|
1661
2030
|
const files = [
|
|
1662
|
-
...cwd ? [
|
|
1663
|
-
|
|
2031
|
+
...cwd ? [path8.join(cwd, ".claude", "settings.local.json"), path8.join(cwd, ".claude", "settings.json")] : [],
|
|
2032
|
+
path8.join(home, ".claude", "settings.json")
|
|
1664
2033
|
];
|
|
1665
2034
|
let model = null;
|
|
1666
2035
|
let effort = null;
|
|
@@ -1676,7 +2045,7 @@ async function claudeDefaults(cwd, home = os4.homedir()) {
|
|
|
1676
2045
|
async function codexDefaults(home = os4.homedir()) {
|
|
1677
2046
|
let text;
|
|
1678
2047
|
try {
|
|
1679
|
-
text = await
|
|
2048
|
+
text = await fs7.readFile(path8.join(home, ".codex", "config.toml"), "utf8");
|
|
1680
2049
|
} catch {
|
|
1681
2050
|
return { model: null, effort: null };
|
|
1682
2051
|
}
|
|
@@ -1700,31 +2069,31 @@ var EFFORT_LEVELS = {
|
|
|
1700
2069
|
// src/engine/index.ts
|
|
1701
2070
|
var RAW_TRANSCRIPT_MAX_BYTES = 200 * 1048576;
|
|
1702
2071
|
function moduleDir() {
|
|
1703
|
-
return
|
|
2072
|
+
return path9.dirname(fileURLToPath(import.meta.url));
|
|
1704
2073
|
}
|
|
1705
2074
|
function resolveWorkerPath() {
|
|
1706
2075
|
const dir = moduleDir();
|
|
1707
2076
|
for (const candidate of [
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
2077
|
+
path9.join(dir, "worker.js"),
|
|
2078
|
+
path9.join(dir, "worker.ts"),
|
|
2079
|
+
path9.join(dir, "..", "engine", "worker.js"),
|
|
2080
|
+
path9.join(dir, "..", "engine", "worker.ts")
|
|
1712
2081
|
]) {
|
|
1713
2082
|
if (existsSync(candidate)) return candidate;
|
|
1714
2083
|
}
|
|
1715
2084
|
throw new Error(`[coding-sessions] The driver worker is missing next to ${dir}.`);
|
|
1716
2085
|
}
|
|
1717
2086
|
async function installShim(dataDir) {
|
|
1718
|
-
const source =
|
|
2087
|
+
const source = path9.resolve(moduleDir(), "..", "..", "bin", "cli-shim.sh");
|
|
1719
2088
|
if (!existsSync(source)) throw new Error(`[coding-sessions] The CLI shim is missing at ${source}.`);
|
|
1720
|
-
const body = await
|
|
1721
|
-
const target =
|
|
1722
|
-
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);
|
|
1723
2092
|
if (current !== body) {
|
|
1724
|
-
await
|
|
1725
|
-
await
|
|
2093
|
+
await fs8.mkdir(path9.dirname(target), { recursive: true });
|
|
2094
|
+
await fs8.writeFile(target, body, { mode: 493 });
|
|
1726
2095
|
}
|
|
1727
|
-
await
|
|
2096
|
+
await fs8.chmod(target, 493);
|
|
1728
2097
|
return target;
|
|
1729
2098
|
}
|
|
1730
2099
|
function nowIso() {
|
|
@@ -1824,29 +2193,30 @@ function describeAuthStatus(provider, probe) {
|
|
|
1824
2193
|
};
|
|
1825
2194
|
}
|
|
1826
2195
|
async function createCodingSessionsEngine(options = {}) {
|
|
1827
|
-
const dataDir = options.dataDir ??
|
|
1828
|
-
const stateDir =
|
|
2196
|
+
const dataDir = options.dataDir ?? path9.join(os5.homedir(), ".matrx", "coding-sessions");
|
|
2197
|
+
const stateDir = path9.join(dataDir, "paperclip-state");
|
|
1829
2198
|
const homes = options.homes ?? defaultProviderHomes();
|
|
1830
2199
|
const index = new RunIndex(dataDir);
|
|
1831
2200
|
await index.load();
|
|
1832
2201
|
const preferences = new PreferenceStore(dataDir);
|
|
1833
2202
|
await preferences.load();
|
|
1834
|
-
await
|
|
2203
|
+
await fs8.mkdir(stateDir, { recursive: true });
|
|
1835
2204
|
const identity = await index.identity();
|
|
2205
|
+
const agents = createAgentConnection(options.agents, dataDir);
|
|
1836
2206
|
const backups = options.rawBackup ? createRawBackupScheduler({
|
|
1837
2207
|
sink: options.rawBackup,
|
|
1838
2208
|
findFile: (provider, providerSessionId) => findSessionFile(provider, providerSessionId, homes),
|
|
1839
2209
|
onError: (error) => console.warn("[coding-sessions] raw transcript backup failed:", error)
|
|
1840
2210
|
}) : null;
|
|
1841
2211
|
const discoveredSeen = /* @__PURE__ */ new Map();
|
|
1842
|
-
const healthFile =
|
|
2212
|
+
const healthFile = path9.join(dataDir, "provider-health.json");
|
|
1843
2213
|
let health = {};
|
|
1844
2214
|
try {
|
|
1845
|
-
health = JSON.parse(await
|
|
2215
|
+
health = JSON.parse(await fs8.readFile(healthFile, "utf8"));
|
|
1846
2216
|
} catch {
|
|
1847
2217
|
health = {};
|
|
1848
2218
|
}
|
|
1849
|
-
const saveHealth = () =>
|
|
2219
|
+
const saveHealth = () => fs8.writeFile(healthFile, JSON.stringify(health), { mode: 384 }).catch(() => void 0);
|
|
1850
2220
|
function noteRunOutcome(provider, status, texts) {
|
|
1851
2221
|
if (status === "completed") {
|
|
1852
2222
|
if (health[provider]) {
|
|
@@ -1963,7 +2333,7 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
1963
2333
|
async function startRun(session, prompt) {
|
|
1964
2334
|
const provider = session.provider;
|
|
1965
2335
|
const realCommand = await commandFor(provider);
|
|
1966
|
-
const runId =
|
|
2336
|
+
const runId = randomUUID4();
|
|
1967
2337
|
const model = session.model ?? (provider === "claude" ? (await userDefaults(provider, session.cwd)).model ?? null : null);
|
|
1968
2338
|
const run = {
|
|
1969
2339
|
runId,
|
|
@@ -2001,7 +2371,26 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
2001
2371
|
let fileOffset = null;
|
|
2002
2372
|
if (session.providerSessionId) {
|
|
2003
2373
|
const file = await findSessionFile(provider, session.providerSessionId, homes);
|
|
2004
|
-
if (file) fileOffset = (await
|
|
2374
|
+
if (file) fileOffset = (await fs8.stat(file).catch(() => null))?.size ?? null;
|
|
2375
|
+
}
|
|
2376
|
+
let agentArgs = [];
|
|
2377
|
+
if (session.agent) {
|
|
2378
|
+
const bundle = await agents.readSessionBundle(session.id);
|
|
2379
|
+
if (!bundle) {
|
|
2380
|
+
const failed = saveRun({
|
|
2381
|
+
...run,
|
|
2382
|
+
status: "failed",
|
|
2383
|
+
endedAt: nowIso(),
|
|
2384
|
+
error: `The agent ${session.agent.name} could not be loaded on this computer anymore. Start a new session to load it again.`
|
|
2385
|
+
});
|
|
2386
|
+
const current = index.session(session.id);
|
|
2387
|
+
if (current) saveSession({ ...current, lastRunStatus: "failed" });
|
|
2388
|
+
return failed;
|
|
2389
|
+
}
|
|
2390
|
+
const attachment = await writeAgentAttachment(agents.sessionDir(session.id), bundle, {
|
|
2391
|
+
mcpUrl: await agents.mcpUrlFor(session.id)
|
|
2392
|
+
});
|
|
2393
|
+
agentArgs = agentArgsFor(provider, attachment, { resuming: session.providerSessionId !== null });
|
|
2005
2394
|
}
|
|
2006
2395
|
const child = forkWorker();
|
|
2007
2396
|
const liveRun = {
|
|
@@ -2175,7 +2564,8 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
2175
2564
|
resumeSessionId: session.providerSessionId,
|
|
2176
2565
|
shimPath,
|
|
2177
2566
|
realCommand,
|
|
2178
|
-
identity
|
|
2567
|
+
identity,
|
|
2568
|
+
agentArgs
|
|
2179
2569
|
});
|
|
2180
2570
|
return run;
|
|
2181
2571
|
}
|
|
@@ -2194,7 +2584,7 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
2194
2584
|
}, 3e3);
|
|
2195
2585
|
}
|
|
2196
2586
|
async function pruneDriverScratch() {
|
|
2197
|
-
const runsDir =
|
|
2587
|
+
const runsDir = path9.join(
|
|
2198
2588
|
stateDir,
|
|
2199
2589
|
"instances",
|
|
2200
2590
|
"default",
|
|
@@ -2205,9 +2595,9 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
2205
2595
|
"claude-runtime",
|
|
2206
2596
|
"runs"
|
|
2207
2597
|
);
|
|
2208
|
-
const entries = await
|
|
2598
|
+
const entries = await fs8.readdir(runsDir).catch(() => []);
|
|
2209
2599
|
await Promise.all(
|
|
2210
|
-
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))
|
|
2211
2601
|
);
|
|
2212
2602
|
}
|
|
2213
2603
|
async function probe() {
|
|
@@ -2248,7 +2638,7 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
2248
2638
|
function newSession(input) {
|
|
2249
2639
|
const at = nowIso();
|
|
2250
2640
|
return {
|
|
2251
|
-
id:
|
|
2641
|
+
id: randomUUID4(),
|
|
2252
2642
|
provider: input.provider,
|
|
2253
2643
|
providerSessionId: input.providerSessionId,
|
|
2254
2644
|
cwd: input.cwd,
|
|
@@ -2266,17 +2656,17 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
2266
2656
|
function resolveFolder(raw) {
|
|
2267
2657
|
const trimmed = raw.trim();
|
|
2268
2658
|
if (!trimmed) return null;
|
|
2269
|
-
return
|
|
2659
|
+
return path9.resolve(trimmed.replace(/^~(?=$|\/)/, os5.homedir()));
|
|
2270
2660
|
}
|
|
2271
2661
|
async function checkFolder(raw) {
|
|
2272
2662
|
const target = resolveFolder(raw);
|
|
2273
2663
|
if (!target) return { path: "", exists: false, isDirectory: false };
|
|
2274
|
-
const stat = await
|
|
2664
|
+
const stat = await fs8.stat(target).catch(() => null);
|
|
2275
2665
|
return { path: target, exists: stat !== null, isDirectory: stat?.isDirectory() === true };
|
|
2276
2666
|
}
|
|
2277
2667
|
async function assertFolder(cwd) {
|
|
2278
|
-
const resolved =
|
|
2279
|
-
const stat = await
|
|
2668
|
+
const resolved = path9.resolve(cwd.replace(/^~(?=$|\/)/, os5.homedir()));
|
|
2669
|
+
const stat = await fs8.stat(resolved).catch(() => null);
|
|
2280
2670
|
if (!stat?.isDirectory()) {
|
|
2281
2671
|
throw new Error(`The folder ${resolved} doesn't exist on this computer.`);
|
|
2282
2672
|
}
|
|
@@ -2338,7 +2728,7 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
2338
2728
|
folders.map(async ([folder, lastUsedAt]) => ({
|
|
2339
2729
|
path: folder,
|
|
2340
2730
|
lastUsedAt,
|
|
2341
|
-
exists: (await
|
|
2731
|
+
exists: (await fs8.stat(folder).catch(() => null))?.isDirectory() === true
|
|
2342
2732
|
}))
|
|
2343
2733
|
);
|
|
2344
2734
|
},
|
|
@@ -2383,7 +2773,7 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
2383
2773
|
async createFolder(raw) {
|
|
2384
2774
|
const target = resolveFolder(raw);
|
|
2385
2775
|
if (!target) throw new Error("Type a folder path first.");
|
|
2386
|
-
await
|
|
2776
|
+
await fs8.mkdir(target, { recursive: true });
|
|
2387
2777
|
return checkFolder(target);
|
|
2388
2778
|
},
|
|
2389
2779
|
async sessionActivity(provider, providerSessionId) {
|
|
@@ -2421,31 +2811,35 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
2421
2811
|
reason: `${PROVIDER_LABEL[provider]} has no saved transcript for this chat on this computer.`
|
|
2422
2812
|
};
|
|
2423
2813
|
}
|
|
2424
|
-
const stat = await
|
|
2814
|
+
const stat = await fs8.stat(file);
|
|
2425
2815
|
if (stat.size > RAW_TRANSCRIPT_MAX_BYTES) {
|
|
2426
2816
|
return {
|
|
2427
2817
|
available: false,
|
|
2428
2818
|
reason: `This transcript is ${formatFileSize(stat.size)}, larger than the ${formatFileSize(RAW_TRANSCRIPT_MAX_BYTES)} this page can hand over. It is at ${file}.`
|
|
2429
2819
|
};
|
|
2430
2820
|
}
|
|
2431
|
-
const content = await
|
|
2432
|
-
return { available: true, fileName:
|
|
2821
|
+
const content = await fs8.readFile(file, "utf8");
|
|
2822
|
+
return { available: true, fileName: path9.basename(file), bytes: stat.size, content };
|
|
2433
2823
|
},
|
|
2434
2824
|
async start(input) {
|
|
2435
2825
|
const prompt = input.prompt.trim();
|
|
2436
2826
|
if (!prompt) throw new Error("Type what you want done before starting.");
|
|
2437
2827
|
const cwd = await assertFolder(input.cwd);
|
|
2438
|
-
const
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
)
|
|
2828
|
+
const bundle = input.agent ? await agents.loadBundle(input.agent) : null;
|
|
2829
|
+
const draft = newSession({
|
|
2830
|
+
provider: input.provider,
|
|
2831
|
+
cwd,
|
|
2832
|
+
title: titleFrom(prompt),
|
|
2833
|
+
model: input.model?.trim() || null,
|
|
2834
|
+
effort: input.effort?.trim() || null,
|
|
2835
|
+
origin: "started",
|
|
2836
|
+
providerSessionId: null
|
|
2837
|
+
});
|
|
2838
|
+
if (bundle) {
|
|
2839
|
+
await agents.saveSessionBundle(draft.id, bundle);
|
|
2840
|
+
draft.agent = sessionAgentRecord(bundle, { toolsServed: await agents.mcpUrlFor(draft.id) !== null });
|
|
2841
|
+
}
|
|
2842
|
+
const session = saveSession(draft);
|
|
2449
2843
|
const run = await startRun(session, prompt);
|
|
2450
2844
|
return { session: index.session(session.id) ?? session, run, queued: false };
|
|
2451
2845
|
},
|
|
@@ -2514,6 +2908,21 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
2514
2908
|
if (opts && "effort" in opts) next.effort = typeof opts.effort === "string" && opts.effort.trim() ? opts.effort.trim() : null;
|
|
2515
2909
|
return saveSession(next);
|
|
2516
2910
|
},
|
|
2911
|
+
async agentAccess() {
|
|
2912
|
+
return agents.access();
|
|
2913
|
+
},
|
|
2914
|
+
async agentCatalogRpc(request) {
|
|
2915
|
+
return agents.catalogRpc(request);
|
|
2916
|
+
},
|
|
2917
|
+
async agentDetails(agentId) {
|
|
2918
|
+
return agents.details(String(agentId ?? ""));
|
|
2919
|
+
},
|
|
2920
|
+
setMcpBaseUrl(baseUrl) {
|
|
2921
|
+
agents.setMcpBaseUrl(baseUrl);
|
|
2922
|
+
},
|
|
2923
|
+
async handleMcp(secret, body) {
|
|
2924
|
+
return agents.handleMcp(secret, body);
|
|
2925
|
+
},
|
|
2517
2926
|
async dispose() {
|
|
2518
2927
|
for (const liveRun of live.values()) {
|
|
2519
2928
|
liveRun.stopRequested = true;
|
|
@@ -2583,7 +2992,10 @@ var BRIDGE_METHODS = [
|
|
|
2583
2992
|
"setChatPreference",
|
|
2584
2993
|
"searchChats",
|
|
2585
2994
|
"openFolder",
|
|
2586
|
-
"setSessionOptions"
|
|
2995
|
+
"setSessionOptions",
|
|
2996
|
+
"agentAccess",
|
|
2997
|
+
"agentCatalogRpc",
|
|
2998
|
+
"agentDetails"
|
|
2587
2999
|
];
|
|
2588
3000
|
var BridgeError = class extends Error {
|
|
2589
3001
|
constructor(message, code) {
|
|
@@ -2627,14 +3039,14 @@ var MIME = {
|
|
|
2627
3039
|
".map": "application/json"
|
|
2628
3040
|
};
|
|
2629
3041
|
function moduleDir2() {
|
|
2630
|
-
return
|
|
3042
|
+
return path10.dirname(fileURLToPath2(import.meta.url));
|
|
2631
3043
|
}
|
|
2632
3044
|
function defaultUiDir() {
|
|
2633
3045
|
for (const candidate of [
|
|
2634
|
-
|
|
2635
|
-
|
|
3046
|
+
path10.resolve(moduleDir2(), "..", "ui"),
|
|
3047
|
+
path10.resolve(moduleDir2(), "..", "..", "dist", "ui")
|
|
2636
3048
|
]) {
|
|
2637
|
-
if (existsSync2(
|
|
3049
|
+
if (existsSync2(path10.join(candidate, "index.html"))) return candidate;
|
|
2638
3050
|
}
|
|
2639
3051
|
return null;
|
|
2640
3052
|
}
|
|
@@ -2657,19 +3069,19 @@ async function serveStatic(uiDir, req, res) {
|
|
|
2657
3069
|
return;
|
|
2658
3070
|
}
|
|
2659
3071
|
const requested = decodeURIComponent(url.pathname);
|
|
2660
|
-
let file =
|
|
3072
|
+
let file = path10.resolve(uiDir, `.${requested}`);
|
|
2661
3073
|
if (!file.startsWith(uiDir)) {
|
|
2662
3074
|
res.writeHead(403).end();
|
|
2663
3075
|
return;
|
|
2664
3076
|
}
|
|
2665
|
-
const stat = await
|
|
2666
|
-
if (!stat || stat.isDirectory()) file =
|
|
2667
|
-
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);
|
|
2668
3080
|
if (!body) {
|
|
2669
3081
|
res.writeHead(404).end();
|
|
2670
3082
|
return;
|
|
2671
3083
|
}
|
|
2672
|
-
const ext =
|
|
3084
|
+
const ext = path10.extname(file);
|
|
2673
3085
|
res.writeHead(200, {
|
|
2674
3086
|
"content-type": MIME[ext] ?? "application/octet-stream",
|
|
2675
3087
|
"cache-control": ext === ".html" ? "no-store" : "public, max-age=31536000, immutable",
|
|
@@ -2678,6 +3090,46 @@ async function serveStatic(uiDir, req, res) {
|
|
|
2678
3090
|
});
|
|
2679
3091
|
res.end(body);
|
|
2680
3092
|
}
|
|
3093
|
+
var MAX_MCP_BODY = 4 * 1024 * 1024;
|
|
3094
|
+
async function handleMcp(engine, secret, req, res, allowedOrigin) {
|
|
3095
|
+
if (!allowedOrigin(req.headers.origin) || !/^[A-Za-z0-9_-]{16,}$/.test(secret)) {
|
|
3096
|
+
res.writeHead(403).end();
|
|
3097
|
+
return;
|
|
3098
|
+
}
|
|
3099
|
+
if (req.method !== "POST") {
|
|
3100
|
+
res.writeHead(405, { allow: "POST" }).end();
|
|
3101
|
+
return;
|
|
3102
|
+
}
|
|
3103
|
+
const chunks = [];
|
|
3104
|
+
let size = 0;
|
|
3105
|
+
for await (const chunk of req) {
|
|
3106
|
+
size += chunk.length;
|
|
3107
|
+
if (size > MAX_MCP_BODY) {
|
|
3108
|
+
res.writeHead(413).end();
|
|
3109
|
+
return;
|
|
3110
|
+
}
|
|
3111
|
+
chunks.push(chunk);
|
|
3112
|
+
}
|
|
3113
|
+
let body;
|
|
3114
|
+
try {
|
|
3115
|
+
body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
3116
|
+
} catch {
|
|
3117
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
3118
|
+
res.end(JSON.stringify({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error." } }));
|
|
3119
|
+
return;
|
|
3120
|
+
}
|
|
3121
|
+
const reply = engine.handleMcp ? await engine.handleMcp(secret, body) : null;
|
|
3122
|
+
if (!reply) {
|
|
3123
|
+
res.writeHead(404).end();
|
|
3124
|
+
return;
|
|
3125
|
+
}
|
|
3126
|
+
if (reply.body === void 0) {
|
|
3127
|
+
res.writeHead(reply.status).end();
|
|
3128
|
+
return;
|
|
3129
|
+
}
|
|
3130
|
+
res.writeHead(reply.status, { "content-type": "application/json" });
|
|
3131
|
+
res.end(JSON.stringify(reply.body));
|
|
3132
|
+
}
|
|
2681
3133
|
function attachSocket(socket, bridge) {
|
|
2682
3134
|
const unsubscribe = bridge.subscribe((event) => {
|
|
2683
3135
|
if (socket.readyState === socket.OPEN) socket.send(JSON.stringify({ event }));
|
|
@@ -2708,7 +3160,7 @@ function attachSocket(socket, bridge) {
|
|
|
2708
3160
|
socket.on("close", unsubscribe);
|
|
2709
3161
|
}
|
|
2710
3162
|
async function startCodingSessionsHost(options = {}) {
|
|
2711
|
-
const token = options.token ??
|
|
3163
|
+
const token = options.token ?? randomBytes2(24).toString("base64url");
|
|
2712
3164
|
const engine = options.engine ?? await createCodingSessionsEngine(options);
|
|
2713
3165
|
const bridge = createHostBridge(engine);
|
|
2714
3166
|
const uiDir = options.uiDir ?? defaultUiDir();
|
|
@@ -2719,6 +3171,13 @@ async function startCodingSessionsHost(options = {}) {
|
|
|
2719
3171
|
};
|
|
2720
3172
|
const server = http.createServer((req, res) => {
|
|
2721
3173
|
const url2 = new URL(req.url ?? "/", `http://${HOST}`);
|
|
3174
|
+
if (url2.pathname.startsWith("/mcp/")) {
|
|
3175
|
+
handleMcp(engine, url2.pathname.slice("/mcp/".length), req, res, allowedOrigin).catch(() => {
|
|
3176
|
+
if (!res.headersSent) res.writeHead(500);
|
|
3177
|
+
res.end();
|
|
3178
|
+
});
|
|
3179
|
+
return;
|
|
3180
|
+
}
|
|
2722
3181
|
if (url2.pathname === "/health") {
|
|
2723
3182
|
res.writeHead(200, { "content-type": "application/json" });
|
|
2724
3183
|
res.end(JSON.stringify({ ok: true, service: "@ai-matrx/coding-sessions" }));
|
|
@@ -2749,6 +3208,7 @@ async function startCodingSessionsHost(options = {}) {
|
|
|
2749
3208
|
server.off("error", reject);
|
|
2750
3209
|
const address = server.address();
|
|
2751
3210
|
if (address && typeof address === "object") port = address.port;
|
|
3211
|
+
engine.setMcpBaseUrl?.(`http://${HOST}:${port}`);
|
|
2752
3212
|
resolve();
|
|
2753
3213
|
});
|
|
2754
3214
|
});
|
|
@@ -2759,6 +3219,7 @@ async function startCodingSessionsHost(options = {}) {
|
|
|
2759
3219
|
token,
|
|
2760
3220
|
engine,
|
|
2761
3221
|
async close() {
|
|
3222
|
+
engine.setMcpBaseUrl?.(null);
|
|
2762
3223
|
for (const client of wss.clients) client.terminate();
|
|
2763
3224
|
await new Promise((resolve) => wss.close(() => resolve()));
|
|
2764
3225
|
await new Promise((resolve) => server.close(() => resolve()));
|