@ai-matrx/coding-sessions 0.1.83 → 0.1.86
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 +18 -0
- package/dist/engine/index.cjs +656 -126
- 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 +654 -126
- package/dist/engine/index.js.map +1 -1
- package/dist/engine/worker.js +7 -5
- package/dist/engine/worker.js.map +1 -1
- package/dist/{index-DQ0teQvw.d.cts → index-aavbdzSo.d.cts} +161 -1
- package/dist/{index-DQ0teQvw.d.ts → index-aavbdzSo.d.ts} +161 -1
- package/dist/index.cjs +16 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +112 -1
- package/dist/index.d.ts +112 -1
- package/dist/index.js +16 -2
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +821 -412
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +113 -8
- package/dist/react/index.d.ts +113 -8
- package/dist/react/index.js +851 -418
- package/dist/react/index.js.map +1 -1
- package/dist/server/index.cjs +720 -141
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +5 -1
- package/dist/server/index.d.ts +5 -1
- package/dist/server/index.js +719 -140
- package/dist/server/index.js.map +1 -1
- package/dist/ui/assets/index-XIcCZr8A.js +71 -0
- package/dist/ui/assets/index-eqe0sxR4.css +2 -0
- package/dist/ui/index.html +2 -2
- package/package.json +3 -1
- 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,448 @@ 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
|
+
|
|
77
|
+
// src/normalize/agent-block.ts
|
|
78
|
+
var AGENT_BLOCK_MARKER = "<ai_matrx_agent ";
|
|
79
|
+
function stripAgentBlock(text) {
|
|
80
|
+
const removed = text.match(/^<ai_matrx_agent_removed>[\s\S]*?<\/ai_matrx_agent_removed>\s*/);
|
|
81
|
+
if (removed) return text.slice(removed[0].length);
|
|
82
|
+
if (!text.startsWith(AGENT_BLOCK_MARKER)) return text;
|
|
83
|
+
const end = text.indexOf("</ai_matrx_agent>");
|
|
84
|
+
if (end < 0) return text;
|
|
85
|
+
let rest = text.slice(end + "</ai_matrx_agent>".length);
|
|
86
|
+
const skills = rest.match(/^\s*<skills_instructions>[\s\S]*?<\/skills_instructions>/);
|
|
87
|
+
if (skills) rest = rest.slice(skills[0].length);
|
|
88
|
+
return rest.replace(/^\s+/, "");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/engine/agent-attach.ts
|
|
92
|
+
var AGENT_MCP_SERVER_NAME = "matrx-agent";
|
|
93
|
+
var AGENT_PLUGIN_NAME = "matrx-agent";
|
|
94
|
+
function escapeAttr(value) {
|
|
95
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
96
|
+
}
|
|
97
|
+
function versionWords(bundle) {
|
|
98
|
+
return bundle.version_number !== null ? String(bundle.version_number) : "latest";
|
|
99
|
+
}
|
|
100
|
+
function renderAgentInstructions(bundle, options = {}) {
|
|
101
|
+
const included = bundle.skills.filter((skill) => skill.tier === "included");
|
|
102
|
+
const listed2 = bundle.skills.filter((skill) => skill.tier === "listed");
|
|
103
|
+
const parts = [`<ai_matrx_agent name="${escapeAttr(bundle.name)}" version="${versionWords(bundle)}">`];
|
|
104
|
+
if (included.length > 0) {
|
|
105
|
+
parts.push(
|
|
106
|
+
"## Skills (always in effect)",
|
|
107
|
+
...included.map((skill) => `### ${skill.label}
|
|
108
|
+
|
|
109
|
+
${skill.body.trim()}`)
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
if (options.claudeSkills && listed2.length > 0) {
|
|
113
|
+
parts.push(
|
|
114
|
+
[
|
|
115
|
+
"## Your skills",
|
|
116
|
+
"When a request matches one of these, load it with your Skill tool and follow it before answering:",
|
|
117
|
+
...listed2.map((skill) => `- ${AGENT_PLUGIN_NAME}:${skill.slug} \u2014 ${skill.description || skill.label}`)
|
|
118
|
+
].join("\n")
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
if (included.length > 0 || options.claudeSkills && listed2.length > 0) parts.push("## Your instructions");
|
|
122
|
+
parts.push(bundle.instructions.trim());
|
|
123
|
+
parts.push("</ai_matrx_agent>");
|
|
124
|
+
return `${parts.filter(Boolean).join("\n\n")}
|
|
125
|
+
`;
|
|
126
|
+
}
|
|
127
|
+
function renderCodexSkillsList(bundle, skillsRoot) {
|
|
128
|
+
const listed2 = bundle.skills.filter((skill) => skill.tier === "listed");
|
|
129
|
+
if (listed2.length === 0) return "";
|
|
130
|
+
const lines = [
|
|
131
|
+
"<skills_instructions>",
|
|
132
|
+
"## AI Matrx agent skills",
|
|
133
|
+
"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.",
|
|
134
|
+
"### Available skills",
|
|
135
|
+
...listed2.map(
|
|
136
|
+
(skill) => `- ${skill.slug}: ${skill.description || skill.label} (file: ${path.join(skillsRoot, skill.slug, "SKILL.md")})`
|
|
137
|
+
),
|
|
138
|
+
"</skills_instructions>"
|
|
139
|
+
];
|
|
140
|
+
return lines.join("\n");
|
|
141
|
+
}
|
|
142
|
+
function renderFallbackBlock(bundle, skillsRoot) {
|
|
143
|
+
const skills = renderCodexSkillsList(bundle, skillsRoot);
|
|
144
|
+
const body = renderAgentInstructions(bundle).trim().replace(/^(<ai_matrx_agent [^>]*>)/, "$1\n\nThe instructions below are your AI Matrx agent for this chat from now on; they replace any AI Matrx agent instructions given earlier.");
|
|
145
|
+
return skills ? `${body}
|
|
146
|
+
|
|
147
|
+
${skills}
|
|
148
|
+
|
|
149
|
+
` : `${body}
|
|
150
|
+
|
|
151
|
+
`;
|
|
152
|
+
}
|
|
153
|
+
function tomlString(value) {
|
|
154
|
+
const scalar = value.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "\uFFFD");
|
|
155
|
+
return JSON.stringify(scalar).replace(/\u007f/g, "\\u007f");
|
|
156
|
+
}
|
|
157
|
+
async function writeAgentAttachment(dir, bundle, options) {
|
|
158
|
+
const pluginDir = path.join(dir, "plugin");
|
|
159
|
+
const skillsRoot = path.join(dir, "skills");
|
|
160
|
+
await fs2.mkdir(path.join(pluginDir, ".claude-plugin"), { recursive: true, mode: 448 });
|
|
161
|
+
await fs2.mkdir(skillsRoot, { recursive: true, mode: 448 });
|
|
162
|
+
const instructions = renderAgentInstructions(bundle);
|
|
163
|
+
const instructionsFile = path.join(dir, "instructions.md");
|
|
164
|
+
await fs2.writeFile(instructionsFile, renderAgentInstructions(bundle, { claudeSkills: true }), { mode: 384 });
|
|
165
|
+
await fs2.writeFile(
|
|
166
|
+
path.join(pluginDir, ".claude-plugin", "plugin.json"),
|
|
167
|
+
`${JSON.stringify(
|
|
168
|
+
{
|
|
169
|
+
name: AGENT_PLUGIN_NAME,
|
|
170
|
+
description: `Skills of the AI Matrx agent "${bundle.name}"`,
|
|
171
|
+
version: versionWords(bundle) === "latest" ? "1.0.0" : `${versionWords(bundle)}.0.0`
|
|
172
|
+
},
|
|
173
|
+
null,
|
|
174
|
+
2
|
|
175
|
+
)}
|
|
176
|
+
`,
|
|
177
|
+
{ mode: 384 }
|
|
178
|
+
);
|
|
179
|
+
const listed2 = bundle.skills.filter((skill) => skill.tier === "listed");
|
|
180
|
+
const wanted = new Set(listed2.map((skill) => skill.slug));
|
|
181
|
+
for (const root of [path.join(pluginDir, "skills"), skillsRoot]) {
|
|
182
|
+
await fs2.mkdir(root, { recursive: true, mode: 448 });
|
|
183
|
+
for (const existing of await fs2.readdir(root).catch(() => [])) {
|
|
184
|
+
if (!wanted.has(existing)) await fs2.rm(path.join(root, existing), { recursive: true, force: true });
|
|
185
|
+
}
|
|
186
|
+
for (const skill of listed2) {
|
|
187
|
+
const skillDir = path.join(root, skill.slug);
|
|
188
|
+
await fs2.mkdir(skillDir, { recursive: true, mode: 448 });
|
|
189
|
+
await fs2.writeFile(path.join(skillDir, "SKILL.md"), skill.content, { mode: 384 });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
let mcpConfigFile = null;
|
|
193
|
+
const mcpUrl = options.mcpUrl && bundle.tools.length > 0 ? options.mcpUrl : null;
|
|
194
|
+
const mcpPath = path.join(dir, "mcp.json");
|
|
195
|
+
if (mcpUrl) {
|
|
196
|
+
mcpConfigFile = mcpPath;
|
|
197
|
+
await fs2.writeFile(
|
|
198
|
+
mcpPath,
|
|
199
|
+
`${JSON.stringify({ mcpServers: { [AGENT_MCP_SERVER_NAME]: { type: "http", url: mcpUrl } } }, null, 2)}
|
|
200
|
+
`,
|
|
201
|
+
{ mode: 384 }
|
|
202
|
+
);
|
|
203
|
+
} else {
|
|
204
|
+
await fs2.rm(mcpPath, { force: true });
|
|
205
|
+
}
|
|
206
|
+
const skillsList = renderCodexSkillsList(bundle, skillsRoot);
|
|
207
|
+
return {
|
|
208
|
+
dir,
|
|
209
|
+
instructionsFile,
|
|
210
|
+
pluginDir,
|
|
211
|
+
mcpConfigFile,
|
|
212
|
+
mcpUrl,
|
|
213
|
+
// Same order as Claude's: the skills list first, the agent's words last.
|
|
214
|
+
codexInstructions: skillsList ? `${skillsList}
|
|
215
|
+
|
|
216
|
+
${instructions}` : instructions
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function agentArgsFor(provider, attachment, options) {
|
|
220
|
+
if (provider === "claude") {
|
|
221
|
+
const args2 = [
|
|
222
|
+
// Every turn: without snapshot off, a resumed Claude drops appended instructions.
|
|
223
|
+
"--append-system-prompt-file",
|
|
224
|
+
attachment.instructionsFile,
|
|
225
|
+
"--system-prompt-snapshot",
|
|
226
|
+
"off",
|
|
227
|
+
"--plugin-dir",
|
|
228
|
+
attachment.pluginDir
|
|
229
|
+
];
|
|
230
|
+
if (attachment.mcpConfigFile) args2.push("--mcp-config", attachment.mcpConfigFile);
|
|
231
|
+
return args2;
|
|
232
|
+
}
|
|
233
|
+
const args = [];
|
|
234
|
+
if (!options.resuming) args.push("-c", `developer_instructions=${tomlString(attachment.codexInstructions)}`);
|
|
235
|
+
if (attachment.mcpUrl) {
|
|
236
|
+
const key = `mcp_servers.${AGENT_MCP_SERVER_NAME}`;
|
|
237
|
+
args.push(
|
|
238
|
+
"-c",
|
|
239
|
+
`${key}.url=${tomlString(attachment.mcpUrl)}`,
|
|
240
|
+
"-c",
|
|
241
|
+
`${key}.default_tools_approval_mode="approve"`
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
return args;
|
|
245
|
+
}
|
|
246
|
+
function sessionAgentRecord(bundle, options) {
|
|
247
|
+
const unavailable = bundle.unavailable.map((u) => ({ kind: u.kind, name: u.name, reason: u.reason }));
|
|
248
|
+
const missingSkills = unavailable.filter((u) => u.kind === "skill").length;
|
|
249
|
+
const missingTools = unavailable.filter((u) => u.kind === "tool").length;
|
|
250
|
+
if (!options.toolsServed && bundle.tools.length > 0) {
|
|
251
|
+
for (const tool of bundle.tools) {
|
|
252
|
+
unavailable.push({ kind: "tool", name: tool.canonical_name, reason: "Tools need the coding sessions host" });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const toolsLoaded = options.toolsServed ? bundle.tools.length : 0;
|
|
256
|
+
const loaded = [
|
|
257
|
+
{ part: "instructions", loaded: bundle.instructions.trim() ? 1 : 0, total: 1 },
|
|
258
|
+
{ part: "skills", loaded: bundle.skills.length, total: bundle.skills.length + missingSkills },
|
|
259
|
+
{ part: "tools", loaded: toolsLoaded, total: bundle.tools.length + missingTools }
|
|
260
|
+
];
|
|
261
|
+
return {
|
|
262
|
+
agentId: bundle.agent_id,
|
|
263
|
+
versionId: bundle.version_id,
|
|
264
|
+
versionNumber: bundle.version_number,
|
|
265
|
+
name: bundle.name,
|
|
266
|
+
definitionHash: bundle.definition_hash,
|
|
267
|
+
loaded,
|
|
268
|
+
unavailable,
|
|
269
|
+
skills: bundle.skills.map((skill) => skill.label),
|
|
270
|
+
tools: options.toolsServed ? bundle.tools.map((tool) => tool.canonical_name) : [],
|
|
271
|
+
...options.variables ? { variables: options.variables } : {}
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
var AGENT_REMOVED_BLOCK = "<ai_matrx_agent_removed>The AI Matrx agent instructions given earlier in this chat no longer apply.</ai_matrx_agent_removed>\n\n";
|
|
275
|
+
async function codexCompactionDroppedAgent(rolloutFile, sinceIso) {
|
|
276
|
+
const text = await fs2.readFile(rolloutFile, "utf8").catch(() => "");
|
|
277
|
+
if (!text.includes('"type":"compacted"')) return false;
|
|
278
|
+
let latest = null;
|
|
279
|
+
for (const line of text.split("\n")) {
|
|
280
|
+
if (!line.includes('"type":"compacted"')) continue;
|
|
281
|
+
try {
|
|
282
|
+
const entry = JSON.parse(line);
|
|
283
|
+
if (entry.type === "compacted") latest = entry;
|
|
284
|
+
} catch {
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (!latest) return false;
|
|
288
|
+
if (sinceIso && latest.timestamp && latest.timestamp <= sinceIso) return false;
|
|
289
|
+
return !JSON.stringify(latest.payload?.replacement_history ?? "").includes(AGENT_BLOCK_MARKER.trim());
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/engine/agents.ts
|
|
293
|
+
import { createHash as createHash2, randomBytes, randomUUID } from "crypto";
|
|
294
|
+
import fs3 from "fs/promises";
|
|
295
|
+
import path2 from "path";
|
|
296
|
+
var DEFAULT_SERVER_URL = "https://server.app.matrxserver.com";
|
|
297
|
+
var DEFAULT_SUPABASE_URL = "https://db.matrxserver.com";
|
|
298
|
+
var DEFAULT_SUPABASE_KEY = "sb_publishable_scm0jzEJiFUB5ItiqOx5Fg_8BOjB6oA";
|
|
299
|
+
var AGENT_CATALOG_RPCS = /* @__PURE__ */ new Set([
|
|
300
|
+
"agx_get_list_full",
|
|
301
|
+
"agx_search",
|
|
302
|
+
"agx_resolve_agent_address",
|
|
303
|
+
"ues_get_bulk",
|
|
304
|
+
"ues_list",
|
|
305
|
+
"ues_set",
|
|
306
|
+
"ues_touch"
|
|
307
|
+
]);
|
|
308
|
+
var agentsClient = async () => {
|
|
309
|
+
const [matrx, portable, mcp] = await Promise.all([
|
|
310
|
+
import("@ai-matrx/agents/matrx"),
|
|
311
|
+
import("@ai-matrx/agents/portable"),
|
|
312
|
+
import("@ai-matrx/agents/portable/mcp")
|
|
313
|
+
]);
|
|
314
|
+
return { ...matrx, ...portable, ...mcp };
|
|
315
|
+
};
|
|
316
|
+
function sortedValues(values) {
|
|
317
|
+
return Object.keys(values).sort().map((key) => [key, values[key]]);
|
|
318
|
+
}
|
|
319
|
+
var BUNDLE_CACHE_MS = 24 * 60 * 60 * 1e3;
|
|
320
|
+
var SIGNED_OUT_AGENT_MESSAGE = "Sign in to AI Matrx to use your agents.";
|
|
321
|
+
function userIdOf(credential) {
|
|
322
|
+
if (!credential || credential.kind !== "user") return null;
|
|
323
|
+
const payload = credential.accessToken.split(".")[1];
|
|
324
|
+
if (!payload) return null;
|
|
325
|
+
try {
|
|
326
|
+
const claims = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
327
|
+
return typeof claims.sub === "string" ? claims.sub : null;
|
|
328
|
+
} catch {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function createAgentConnection(options, dataDir) {
|
|
333
|
+
const credentials = options?.credentials ?? null;
|
|
334
|
+
const serverUrl = (options?.serverUrl ?? DEFAULT_SERVER_URL).replace(/\/+$/, "");
|
|
335
|
+
const supabaseUrl = (options?.supabaseUrl ?? DEFAULT_SUPABASE_URL).replace(/\/+$/, "");
|
|
336
|
+
const supabaseKey = options?.supabaseKey ?? (options?.supabaseUrl ? null : DEFAULT_SUPABASE_KEY);
|
|
337
|
+
const root = path2.join(dataDir, "agents");
|
|
338
|
+
let mcpBaseUrl = null;
|
|
339
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
340
|
+
const secretToSession = /* @__PURE__ */ new Map();
|
|
341
|
+
const organizationId = async () => {
|
|
342
|
+
const raw = typeof options?.organizationId === "function" ? await options.organizationId() : options?.organizationId;
|
|
343
|
+
return raw && raw.trim() ? raw.trim() : null;
|
|
344
|
+
};
|
|
345
|
+
const transport = async () => {
|
|
346
|
+
const org = await organizationId();
|
|
347
|
+
return (await agentsClient()).createMatrxTransport({
|
|
348
|
+
baseUrl: serverUrl,
|
|
349
|
+
...credentials ? { credentials } : {},
|
|
350
|
+
...org ? { organizationId: org } : {},
|
|
351
|
+
source: "codingSessionsAgents"
|
|
352
|
+
});
|
|
353
|
+
};
|
|
354
|
+
const cacheDir = path2.join(dataDir, "agents", ".bundles");
|
|
355
|
+
const cacheKey = (definitionHash, selection, surface) => createHash2("sha256").update(JSON.stringify([definitionHash, sortedValues(selection.variables ?? {}), surface])).digest("hex");
|
|
356
|
+
const credential = async () => credentials ? credentials.get() : null;
|
|
357
|
+
const requireSignedIn = async () => {
|
|
358
|
+
if (!await credential()) throw new Error(SIGNED_OUT_AGENT_MESSAGE);
|
|
359
|
+
};
|
|
360
|
+
const sessionDir = (sessionId) => path2.join(root, sessionId.replace(/[^A-Za-z0-9_-]/g, "_"));
|
|
361
|
+
async function meta(sessionId) {
|
|
362
|
+
const file = path2.join(sessionDir(sessionId), "session.json");
|
|
363
|
+
const existing = await fs3.readFile(file, "utf8").then((text) => JSON.parse(text)).catch(() => null);
|
|
364
|
+
if (existing?.mcpSecret && existing.conversationId) return existing;
|
|
365
|
+
const created = { mcpSecret: randomBytes(24).toString("base64url"), conversationId: randomUUID() };
|
|
366
|
+
await fs3.mkdir(sessionDir(sessionId), { recursive: true, mode: 448 });
|
|
367
|
+
await fs3.writeFile(file, JSON.stringify(created), { mode: 384 });
|
|
368
|
+
return created;
|
|
369
|
+
}
|
|
370
|
+
async function findSessionBySecret(secret) {
|
|
371
|
+
for (const name of await fs3.readdir(root).catch(() => [])) {
|
|
372
|
+
const found = await fs3.readFile(path2.join(root, name, "session.json"), "utf8").then((text) => JSON.parse(text).mcpSecret === secret).catch(() => false);
|
|
373
|
+
if (found) {
|
|
374
|
+
secretToSession.set(secret, name);
|
|
375
|
+
return name;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
async function readSessionBundle(sessionId) {
|
|
381
|
+
return fs3.readFile(path2.join(sessionDir(sessionId), "bundle.json"), "utf8").then((text) => JSON.parse(text)).catch(() => null);
|
|
382
|
+
}
|
|
383
|
+
return {
|
|
384
|
+
async access() {
|
|
385
|
+
const current = await credential().catch(() => null);
|
|
386
|
+
const userId = userIdOf(current);
|
|
387
|
+
return {
|
|
388
|
+
configured: credentials !== null && supabaseKey !== null,
|
|
389
|
+
signedIn: userId !== null,
|
|
390
|
+
userId,
|
|
391
|
+
organizationId: await organizationId().catch(() => null)
|
|
392
|
+
};
|
|
393
|
+
},
|
|
394
|
+
async catalogRpc(request) {
|
|
395
|
+
if (!AGENT_CATALOG_RPCS.has(request.fn)) {
|
|
396
|
+
return { data: null, error: { message: `Not an agent catalogue read: ${request.fn}` } };
|
|
397
|
+
}
|
|
398
|
+
const current = await credential();
|
|
399
|
+
if (!current || current.kind !== "user") return { data: null, error: { message: SIGNED_OUT_AGENT_MESSAGE, code: "signed_out" } };
|
|
400
|
+
if (!supabaseKey) return { data: null, error: { message: "This host has no AI Matrx database key.", code: "not_configured" } };
|
|
401
|
+
const query = new URLSearchParams();
|
|
402
|
+
if (request.order) query.set("order", `${request.order.column}.${request.order.ascending === false ? "desc" : "asc"}`);
|
|
403
|
+
if (request.range) {
|
|
404
|
+
query.set("offset", String(request.range[0]));
|
|
405
|
+
query.set("limit", String(request.range[1] - request.range[0] + 1));
|
|
406
|
+
}
|
|
407
|
+
const headers = {
|
|
408
|
+
apikey: supabaseKey,
|
|
409
|
+
Authorization: `Bearer ${current.accessToken}`,
|
|
410
|
+
"Content-Type": "application/json",
|
|
411
|
+
// Every catalogue RPC lives in `public`; the database's default
|
|
412
|
+
// exposed schema is not `public`, so name it (supabase-js does).
|
|
413
|
+
"Content-Profile": "public",
|
|
414
|
+
"Accept-Profile": "public"
|
|
415
|
+
};
|
|
416
|
+
if (request.count) headers.Prefer = `count=${request.count}`;
|
|
417
|
+
const qs = query.toString();
|
|
418
|
+
const response = await fetch(`${supabaseUrl}/rest/v1/rpc/${encodeURIComponent(request.fn)}${qs ? `?${qs}` : ""}`, {
|
|
419
|
+
method: "POST",
|
|
420
|
+
headers,
|
|
421
|
+
body: JSON.stringify(request.args ?? {})
|
|
422
|
+
});
|
|
423
|
+
const text = await response.text();
|
|
424
|
+
const body = text ? JSON.parse(text) : null;
|
|
425
|
+
if (!response.ok) {
|
|
426
|
+
const err = body ?? {};
|
|
427
|
+
return {
|
|
428
|
+
data: null,
|
|
429
|
+
error: { message: err.message ?? `HTTP ${response.status}`, code: err.code, details: err.details ?? null, hint: err.hint ?? null }
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
const range = response.headers.get("content-range");
|
|
433
|
+
const total = range?.split("/")[1];
|
|
434
|
+
return { data: body, error: null, count: total && total !== "*" ? Number(total) : null };
|
|
435
|
+
},
|
|
436
|
+
async details(agentId) {
|
|
437
|
+
await requireSignedIn();
|
|
438
|
+
const summary = await (await agentsClient()).fetchPortableAgentSummary(await transport(), agentId);
|
|
439
|
+
return {
|
|
440
|
+
agentId: summary.agent_id,
|
|
441
|
+
name: summary.name,
|
|
442
|
+
variables: summary.variables,
|
|
443
|
+
versions: summary.versions.map((v) => ({ id: v.id, versionNumber: v.version_number, changedAt: v.changed_at })),
|
|
444
|
+
latestVersionNumber: summary.version_number,
|
|
445
|
+
latestVersionId: summary.version_id
|
|
446
|
+
};
|
|
447
|
+
},
|
|
448
|
+
async loadBundle(selection) {
|
|
449
|
+
await requireSignedIn();
|
|
450
|
+
const client = await agentsClient();
|
|
451
|
+
const summary = await client.fetchPortableAgentSummary(await transport(), selection.agentId);
|
|
452
|
+
const versionId = selection.versionId ?? summary.version_id;
|
|
453
|
+
const definitionHash = versionId ? summary.versions.find((v) => v.id === versionId)?.definition_hash ?? null : summary.definition_hash;
|
|
454
|
+
const surface = "matrx-user/code-editor";
|
|
455
|
+
const file = definitionHash ? path2.join(cacheDir, `${cacheKey(definitionHash, selection, surface)}.json`) : null;
|
|
456
|
+
if (file) {
|
|
457
|
+
const cached = await fs3.readFile(file, "utf8").then((text) => JSON.parse(text)).catch(() => null);
|
|
458
|
+
const fresh = await fs3.stat(file).then((st) => Date.now() - st.mtimeMs < BUNDLE_CACHE_MS).catch(() => false);
|
|
459
|
+
if (cached && fresh && cached.definition_hash === definitionHash) return cached;
|
|
460
|
+
}
|
|
461
|
+
const bundle = await client.fetchPortableAgent(
|
|
462
|
+
await transport(),
|
|
463
|
+
versionId ? { versionId } : { agentId: selection.agentId },
|
|
464
|
+
{ variables: selection.variables ?? {}, surface }
|
|
465
|
+
);
|
|
466
|
+
if (file && bundle.definition_hash === definitionHash) {
|
|
467
|
+
await fs3.mkdir(cacheDir, { recursive: true, mode: 448 });
|
|
468
|
+
await fs3.writeFile(file, JSON.stringify(bundle), { mode: 384 });
|
|
469
|
+
}
|
|
470
|
+
return bundle;
|
|
471
|
+
},
|
|
472
|
+
async saveSessionBundle(sessionId, bundle) {
|
|
473
|
+
await fs3.mkdir(sessionDir(sessionId), { recursive: true, mode: 448 });
|
|
474
|
+
await fs3.writeFile(path2.join(sessionDir(sessionId), "bundle.json"), JSON.stringify(bundle), { mode: 384 });
|
|
475
|
+
await meta(sessionId);
|
|
476
|
+
},
|
|
477
|
+
readSessionBundle,
|
|
478
|
+
sessionDir,
|
|
479
|
+
async mcpUrlFor(sessionId) {
|
|
480
|
+
if (!mcpBaseUrl) return null;
|
|
481
|
+
const { mcpSecret } = await meta(sessionId);
|
|
482
|
+
secretToSession.set(mcpSecret, sessionId);
|
|
483
|
+
return `${mcpBaseUrl}/mcp/${mcpSecret}`;
|
|
484
|
+
},
|
|
485
|
+
setMcpBaseUrl(baseUrl) {
|
|
486
|
+
mcpBaseUrl = baseUrl ? baseUrl.replace(/\/+$/, "") : null;
|
|
487
|
+
},
|
|
488
|
+
async handleMcp(secret, body) {
|
|
489
|
+
let handler = handlers.get(secret);
|
|
490
|
+
if (!handler) {
|
|
491
|
+
const sessionId = secretToSession.get(secret) ?? await findSessionBySecret(secret);
|
|
492
|
+
if (!sessionId) return null;
|
|
493
|
+
const bundle = await readSessionBundle(sessionId);
|
|
494
|
+
if (!bundle) return null;
|
|
495
|
+
const { conversationId } = await meta(sessionId);
|
|
496
|
+
const { createAgentToolsMcpHandler } = await agentsClient();
|
|
497
|
+
handler = createAgentToolsMcpHandler({
|
|
498
|
+
bundle,
|
|
499
|
+
credentials,
|
|
500
|
+
organizationId: () => organizationId(),
|
|
501
|
+
baseUrl: serverUrl,
|
|
502
|
+
conversationId
|
|
503
|
+
});
|
|
504
|
+
handlers.set(secret, handler);
|
|
505
|
+
}
|
|
506
|
+
return handler.handle(body);
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
73
511
|
// src/engine/index.ts
|
|
74
|
-
import
|
|
512
|
+
import fs8 from "fs/promises";
|
|
75
513
|
import os5 from "os";
|
|
76
|
-
import
|
|
514
|
+
import path9 from "path";
|
|
77
515
|
import { fileURLToPath } from "url";
|
|
78
516
|
|
|
79
517
|
// src/normalize/claude.ts
|
|
@@ -444,7 +882,7 @@ function rolloutItem(item, fallbackId, at) {
|
|
|
444
882
|
const id = str2(item.id) ?? fallbackId;
|
|
445
883
|
switch (item.type) {
|
|
446
884
|
case "UserMessage": {
|
|
447
|
-
const text = stripCodexInjectedContext(itemText(item.content));
|
|
885
|
+
const text = stripCodexInjectedContext(stripAgentBlock(itemText(item.content)));
|
|
448
886
|
return text.trim() ? [{ kind: "user", id, text, at }] : [];
|
|
449
887
|
}
|
|
450
888
|
case "AgentMessage": {
|
|
@@ -527,7 +965,7 @@ function createCodexRolloutNormalizer() {
|
|
|
527
965
|
return rolloutItem(payload.item, fallbackId, at);
|
|
528
966
|
}
|
|
529
967
|
if (payload.type === "user_message" && !appItems) {
|
|
530
|
-
const text = stripCodexInjectedContext(str2(payload.message) ?? "");
|
|
968
|
+
const text = stripCodexInjectedContext(stripAgentBlock(str2(payload.message) ?? ""));
|
|
531
969
|
return text.trim() ? [{ kind: "user", id: fallbackId, text, at }] : [];
|
|
532
970
|
}
|
|
533
971
|
return [];
|
|
@@ -743,7 +1181,7 @@ function describeProblem(provider, problem) {
|
|
|
743
1181
|
import { execFile } from "child_process";
|
|
744
1182
|
import { accessSync, constants as fsConstants, readdirSync, readFileSync, realpathSync } from "fs";
|
|
745
1183
|
import os from "os";
|
|
746
|
-
import
|
|
1184
|
+
import path3 from "path";
|
|
747
1185
|
var SOURCE_LABEL = {
|
|
748
1186
|
codex: { "desktop-app": "from ChatGPT app", path: "from PATH", "npm-global": "from npm", configured: "set by host" },
|
|
749
1187
|
claude: { "desktop-app": "from Claude app", path: "from PATH", "npm-global": "from npm", configured: "set by host" }
|
|
@@ -792,10 +1230,10 @@ function exeName(name, platform) {
|
|
|
792
1230
|
}
|
|
793
1231
|
function allOnPath(name, pathValue, platform = process.platform) {
|
|
794
1232
|
const found = [];
|
|
795
|
-
for (const dir of pathValue.split(
|
|
1233
|
+
for (const dir of pathValue.split(path3.delimiter)) {
|
|
796
1234
|
if (!dir) continue;
|
|
797
1235
|
for (const file of exeName(name, platform)) {
|
|
798
|
-
const candidate =
|
|
1236
|
+
const candidate = path3.join(dir, file);
|
|
799
1237
|
if (executable(candidate)) {
|
|
800
1238
|
found.push(candidate);
|
|
801
1239
|
break;
|
|
@@ -808,17 +1246,17 @@ function desktopAppCandidates(provider, platform, home, applications) {
|
|
|
808
1246
|
if (platform !== "darwin") return [];
|
|
809
1247
|
if (provider === "codex") {
|
|
810
1248
|
return applications.map(
|
|
811
|
-
(apps) =>
|
|
1249
|
+
(apps) => path3.join(apps, "ChatGPT.app", "Contents", "Resources", "codex-cli", "bin", "codex")
|
|
812
1250
|
);
|
|
813
1251
|
}
|
|
814
|
-
const root =
|
|
1252
|
+
const root = path3.join(home, "Library", "Application Support", "Claude", "claude-code");
|
|
815
1253
|
let versions = [];
|
|
816
1254
|
try {
|
|
817
1255
|
versions = readdirSync(root);
|
|
818
1256
|
} catch {
|
|
819
1257
|
return [];
|
|
820
1258
|
}
|
|
821
|
-
return versions.map((v) =>
|
|
1259
|
+
return versions.map((v) => path3.join(root, v, "claude.app", "Contents", "MacOS", "claude"));
|
|
822
1260
|
}
|
|
823
1261
|
function npmGlobalPrefix(env) {
|
|
824
1262
|
const fromEnv = env.npm_config_prefix ?? env.NPM_CONFIG_PREFIX;
|
|
@@ -836,13 +1274,13 @@ function declaredVersion(provider, source, file) {
|
|
|
836
1274
|
if (source !== "desktop-app") return null;
|
|
837
1275
|
if (provider === "codex") {
|
|
838
1276
|
try {
|
|
839
|
-
const pkg = JSON.parse(readFileSync(
|
|
1277
|
+
const pkg = JSON.parse(readFileSync(path3.join(path3.dirname(path3.dirname(file)), "codex-package.json"), "utf8"));
|
|
840
1278
|
return typeof pkg.version === "string" ? parseVersion(pkg.version) : null;
|
|
841
1279
|
} catch {
|
|
842
1280
|
return null;
|
|
843
1281
|
}
|
|
844
1282
|
}
|
|
845
|
-
return parseVersion(
|
|
1283
|
+
return parseVersion(path3.basename(path3.resolve(file, "..", "..", "..", "..")));
|
|
846
1284
|
}
|
|
847
1285
|
function runVersion(file, env) {
|
|
848
1286
|
return new Promise((resolve) => {
|
|
@@ -856,14 +1294,14 @@ async function discoverBinaries(provider, options) {
|
|
|
856
1294
|
const platform = options.platform ?? process.platform;
|
|
857
1295
|
const home = options.home ?? options.env.HOME ?? os.homedir();
|
|
858
1296
|
const listed2 = [
|
|
859
|
-
...desktopAppCandidates(provider, platform, home, options.applications ?? ["/Applications",
|
|
1297
|
+
...desktopAppCandidates(provider, platform, home, options.applications ?? ["/Applications", path3.join(home, "Applications")]).filter(executable).map((p) => ({ path: p, source: "desktop-app" })),
|
|
860
1298
|
...allOnPath(provider, options.env.PATH ?? "", platform).map((p) => ({ path: p, source: "path" }))
|
|
861
1299
|
];
|
|
862
1300
|
const prefix = await npmGlobalPrefix(options.env);
|
|
863
1301
|
if (prefix) {
|
|
864
|
-
const dir = platform === "win32" ? prefix :
|
|
1302
|
+
const dir = platform === "win32" ? prefix : path3.join(prefix, "bin");
|
|
865
1303
|
for (const file of exeName(provider, platform)) {
|
|
866
|
-
const candidate =
|
|
1304
|
+
const candidate = path3.join(dir, file);
|
|
867
1305
|
if (executable(candidate)) {
|
|
868
1306
|
listed2.push({ path: candidate, source: "npm-global" });
|
|
869
1307
|
break;
|
|
@@ -909,7 +1347,7 @@ async function resolveBinary(provider, options) {
|
|
|
909
1347
|
|
|
910
1348
|
// src/engine/clean-env.ts
|
|
911
1349
|
import os2 from "os";
|
|
912
|
-
import
|
|
1350
|
+
import path4 from "path";
|
|
913
1351
|
var ALLOWED_NAMES = /* @__PURE__ */ new Set([
|
|
914
1352
|
"HOME",
|
|
915
1353
|
"USER",
|
|
@@ -931,11 +1369,11 @@ var ALLOWED_NAMES = /* @__PURE__ */ new Set([
|
|
|
931
1369
|
var ALLOWED_PREFIXES = ["LC_"];
|
|
932
1370
|
function wellKnownBinDirs(home) {
|
|
933
1371
|
return [
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
1372
|
+
path4.join(home, ".local", "bin"),
|
|
1373
|
+
path4.join(home, ".claude", "local"),
|
|
1374
|
+
path4.join(home, ".npm-global", "bin"),
|
|
1375
|
+
path4.join(home, ".bun", "bin"),
|
|
1376
|
+
path4.join(home, ".volta", "bin"),
|
|
939
1377
|
"/opt/homebrew/bin",
|
|
940
1378
|
"/usr/local/bin",
|
|
941
1379
|
"/usr/bin",
|
|
@@ -955,21 +1393,21 @@ function buildCleanEnv(hostEnv, extra = {}) {
|
|
|
955
1393
|
}
|
|
956
1394
|
const home = out.HOME ?? os2.homedir();
|
|
957
1395
|
out.HOME = home;
|
|
958
|
-
const hostPath = (hostEnv.PATH ?? "").split(
|
|
1396
|
+
const hostPath = (hostEnv.PATH ?? "").split(path4.delimiter).filter(Boolean);
|
|
959
1397
|
const merged = [];
|
|
960
1398
|
for (const dir of [...hostPath, ...wellKnownBinDirs(home)]) {
|
|
961
1399
|
if (!merged.includes(dir)) merged.push(dir);
|
|
962
1400
|
}
|
|
963
|
-
out.PATH = merged.join(
|
|
1401
|
+
out.PATH = merged.join(path4.delimiter);
|
|
964
1402
|
for (const [name, value] of Object.entries(extra)) out[name] = value;
|
|
965
1403
|
return out;
|
|
966
1404
|
}
|
|
967
1405
|
|
|
968
1406
|
// src/engine/files.ts
|
|
969
1407
|
import { createReadStream } from "fs";
|
|
970
|
-
import
|
|
1408
|
+
import fs4 from "fs/promises";
|
|
971
1409
|
import os3 from "os";
|
|
972
|
-
import
|
|
1410
|
+
import path5 from "path";
|
|
973
1411
|
|
|
974
1412
|
// src/engine/activity.ts
|
|
975
1413
|
import { execFile as execFile2 } from "child_process";
|
|
@@ -1038,15 +1476,15 @@ function describeActivity(input) {
|
|
|
1038
1476
|
// src/engine/files.ts
|
|
1039
1477
|
function defaultProviderHomes(home = os3.homedir()) {
|
|
1040
1478
|
return {
|
|
1041
|
-
claudeProjects:
|
|
1042
|
-
codexSessions:
|
|
1043
|
-
codexIndex:
|
|
1479
|
+
claudeProjects: path5.join(home, ".claude", "projects"),
|
|
1480
|
+
codexSessions: path5.join(home, ".codex", "sessions"),
|
|
1481
|
+
codexIndex: path5.join(home, ".codex", "session_index.jsonl")
|
|
1044
1482
|
};
|
|
1045
1483
|
}
|
|
1046
1484
|
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
1485
|
async function safeReaddir(dir) {
|
|
1048
1486
|
try {
|
|
1049
|
-
return await
|
|
1487
|
+
return await fs4.readdir(dir, { withFileTypes: true });
|
|
1050
1488
|
} catch {
|
|
1051
1489
|
return [];
|
|
1052
1490
|
}
|
|
@@ -1055,7 +1493,7 @@ async function statCandidate(provider, file) {
|
|
|
1055
1493
|
const match = UUIDISH.exec(file);
|
|
1056
1494
|
if (!match?.[1]) return null;
|
|
1057
1495
|
try {
|
|
1058
|
-
const stat = await
|
|
1496
|
+
const stat = await fs4.stat(file);
|
|
1059
1497
|
return { provider, file, id: match[1], mtimeMs: stat.mtimeMs, size: stat.size };
|
|
1060
1498
|
} catch {
|
|
1061
1499
|
return null;
|
|
@@ -1065,10 +1503,10 @@ async function claudeCandidates(root) {
|
|
|
1065
1503
|
const out = [];
|
|
1066
1504
|
for (const project of await safeReaddir(root)) {
|
|
1067
1505
|
if (!project.isDirectory()) continue;
|
|
1068
|
-
const dir =
|
|
1506
|
+
const dir = path5.join(root, project.name);
|
|
1069
1507
|
const entries = await safeReaddir(dir);
|
|
1070
1508
|
const stats = await Promise.all(
|
|
1071
|
-
entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl")).map((e) => statCandidate("claude",
|
|
1509
|
+
entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl")).map((e) => statCandidate("claude", path5.join(dir, e.name)))
|
|
1072
1510
|
);
|
|
1073
1511
|
for (const s of stats) if (s) out.push(s);
|
|
1074
1512
|
}
|
|
@@ -1078,14 +1516,14 @@ async function codexCandidates(root) {
|
|
|
1078
1516
|
const out = [];
|
|
1079
1517
|
for (const year of await safeReaddir(root)) {
|
|
1080
1518
|
if (!year.isDirectory()) continue;
|
|
1081
|
-
for (const month of await safeReaddir(
|
|
1519
|
+
for (const month of await safeReaddir(path5.join(root, year.name))) {
|
|
1082
1520
|
if (!month.isDirectory()) continue;
|
|
1083
|
-
for (const day of await safeReaddir(
|
|
1521
|
+
for (const day of await safeReaddir(path5.join(root, year.name, month.name))) {
|
|
1084
1522
|
if (!day.isDirectory()) continue;
|
|
1085
|
-
const dir =
|
|
1523
|
+
const dir = path5.join(root, year.name, month.name, day.name);
|
|
1086
1524
|
const entries = await safeReaddir(dir);
|
|
1087
1525
|
const stats = await Promise.all(
|
|
1088
|
-
entries.filter((e) => e.isFile() && e.name.startsWith("rollout-") && e.name.endsWith(".jsonl")).map((e) => statCandidate("codex",
|
|
1526
|
+
entries.filter((e) => e.isFile() && e.name.startsWith("rollout-") && e.name.endsWith(".jsonl")).map((e) => statCandidate("codex", path5.join(dir, e.name)))
|
|
1089
1527
|
);
|
|
1090
1528
|
for (const s of stats) if (s) out.push(s);
|
|
1091
1529
|
}
|
|
@@ -1094,7 +1532,7 @@ async function codexCandidates(root) {
|
|
|
1094
1532
|
return out;
|
|
1095
1533
|
}
|
|
1096
1534
|
async function readSlice(file, start, length) {
|
|
1097
|
-
const handle = await
|
|
1535
|
+
const handle = await fs4.open(file, "r");
|
|
1098
1536
|
try {
|
|
1099
1537
|
const buffer = Buffer.alloc(length);
|
|
1100
1538
|
const { bytesRead } = await handle.read(buffer, 0, length, start);
|
|
@@ -1167,9 +1605,9 @@ async function readCodexIndex(file) {
|
|
|
1167
1605
|
const names = /* @__PURE__ */ new Map();
|
|
1168
1606
|
let text;
|
|
1169
1607
|
try {
|
|
1170
|
-
const stat = await
|
|
1608
|
+
const stat = await fs4.stat(file);
|
|
1171
1609
|
const budget = 4 * 1024 * 1024;
|
|
1172
|
-
text = stat.size <= budget ? await
|
|
1610
|
+
text = stat.size <= budget ? await fs4.readFile(file, "utf8") : completeLines(await readSlice(file, stat.size - budget, budget), true);
|
|
1173
1611
|
} catch {
|
|
1174
1612
|
return names;
|
|
1175
1613
|
}
|
|
@@ -1290,7 +1728,7 @@ var MAX_HISTORY_BUDGET = 256 * 1024 * 1024;
|
|
|
1290
1728
|
async function readSessionFile(provider, file, endOffset, maxBytes = HISTORY_BUDGET) {
|
|
1291
1729
|
let size;
|
|
1292
1730
|
try {
|
|
1293
|
-
size = (await
|
|
1731
|
+
size = (await fs4.stat(file)).size;
|
|
1294
1732
|
} catch {
|
|
1295
1733
|
return {
|
|
1296
1734
|
items: [],
|
|
@@ -1344,9 +1782,9 @@ async function describeSessionFile(candidate, codexNames) {
|
|
|
1344
1782
|
}
|
|
1345
1783
|
|
|
1346
1784
|
// src/engine/preferences.ts
|
|
1347
|
-
import { randomUUID } from "crypto";
|
|
1348
|
-
import
|
|
1349
|
-
import
|
|
1785
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1786
|
+
import fs5 from "fs/promises";
|
|
1787
|
+
import path6 from "path";
|
|
1350
1788
|
var PreferenceStore = class {
|
|
1351
1789
|
constructor(dir) {
|
|
1352
1790
|
this.dir = dir;
|
|
@@ -1355,11 +1793,11 @@ var PreferenceStore = class {
|
|
|
1355
1793
|
data = {};
|
|
1356
1794
|
writing = Promise.resolve();
|
|
1357
1795
|
get file() {
|
|
1358
|
-
return
|
|
1796
|
+
return path6.join(this.dir, "preferences.json");
|
|
1359
1797
|
}
|
|
1360
1798
|
async load() {
|
|
1361
1799
|
try {
|
|
1362
|
-
const parsed = JSON.parse(await
|
|
1800
|
+
const parsed = JSON.parse(await fs5.readFile(this.file, "utf8"));
|
|
1363
1801
|
this.data = typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
1364
1802
|
} catch {
|
|
1365
1803
|
this.data = {};
|
|
@@ -1397,10 +1835,10 @@ var PreferenceStore = class {
|
|
|
1397
1835
|
persist() {
|
|
1398
1836
|
const snapshot = JSON.stringify(this.data, null, 1);
|
|
1399
1837
|
this.writing = this.writing.then(async () => {
|
|
1400
|
-
await
|
|
1401
|
-
const tmp = `${this.file}.${
|
|
1402
|
-
await
|
|
1403
|
-
await
|
|
1838
|
+
await fs5.mkdir(this.dir, { recursive: true });
|
|
1839
|
+
const tmp = `${this.file}.${randomUUID2()}.tmp`;
|
|
1840
|
+
await fs5.writeFile(tmp, snapshot, { mode: 384 });
|
|
1841
|
+
await fs5.rename(tmp, this.file);
|
|
1404
1842
|
}).catch(() => {
|
|
1405
1843
|
});
|
|
1406
1844
|
}
|
|
@@ -1557,9 +1995,9 @@ async function searchLocalChats(query, options = {}) {
|
|
|
1557
1995
|
}
|
|
1558
1996
|
|
|
1559
1997
|
// src/engine/store.ts
|
|
1560
|
-
import { randomUUID as
|
|
1561
|
-
import
|
|
1562
|
-
import
|
|
1998
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1999
|
+
import fs6 from "fs/promises";
|
|
2000
|
+
import path7 from "path";
|
|
1563
2001
|
var MAX_RUNS = 2e3;
|
|
1564
2002
|
var RunIndex = class {
|
|
1565
2003
|
constructor(dir) {
|
|
@@ -1569,12 +2007,12 @@ var RunIndex = class {
|
|
|
1569
2007
|
data = { version: 1, sessions: [], runs: [] };
|
|
1570
2008
|
writing = Promise.resolve();
|
|
1571
2009
|
get file() {
|
|
1572
|
-
return
|
|
2010
|
+
return path7.join(this.dir, "index.json");
|
|
1573
2011
|
}
|
|
1574
2012
|
async load() {
|
|
1575
|
-
await
|
|
2013
|
+
await fs6.mkdir(this.dir, { recursive: true });
|
|
1576
2014
|
try {
|
|
1577
|
-
const parsed = JSON.parse(await
|
|
2015
|
+
const parsed = JSON.parse(await fs6.readFile(this.file, "utf8"));
|
|
1578
2016
|
this.data = {
|
|
1579
2017
|
version: 1,
|
|
1580
2018
|
sessions: Array.isArray(parsed.sessions) ? parsed.sessions : [],
|
|
@@ -1616,9 +2054,9 @@ var RunIndex = class {
|
|
|
1616
2054
|
persist() {
|
|
1617
2055
|
const snapshot = JSON.stringify(this.data, null, 1);
|
|
1618
2056
|
this.writing = this.writing.then(async () => {
|
|
1619
|
-
const tmp = `${this.file}.${
|
|
1620
|
-
await
|
|
1621
|
-
await
|
|
2057
|
+
const tmp = `${this.file}.${randomUUID3()}.tmp`;
|
|
2058
|
+
await fs6.writeFile(tmp, snapshot, { mode: 384 });
|
|
2059
|
+
await fs6.rename(tmp, this.file);
|
|
1622
2060
|
}).catch(() => {
|
|
1623
2061
|
});
|
|
1624
2062
|
}
|
|
@@ -1628,27 +2066,27 @@ var RunIndex = class {
|
|
|
1628
2066
|
/** A stable Paperclip agent/company identity for this install, so the
|
|
1629
2067
|
* driver's scratch state is reused rather than growing per run. */
|
|
1630
2068
|
async identity() {
|
|
1631
|
-
const file =
|
|
2069
|
+
const file = path7.join(this.dir, "identity.json");
|
|
1632
2070
|
try {
|
|
1633
|
-
const parsed = JSON.parse(await
|
|
2071
|
+
const parsed = JSON.parse(await fs6.readFile(file, "utf8"));
|
|
1634
2072
|
if (typeof parsed.companyId === "string" && typeof parsed.agentId === "string") {
|
|
1635
2073
|
return { companyId: parsed.companyId, agentId: parsed.agentId };
|
|
1636
2074
|
}
|
|
1637
2075
|
} catch {
|
|
1638
2076
|
}
|
|
1639
|
-
const identity = { companyId:
|
|
1640
|
-
await
|
|
2077
|
+
const identity = { companyId: randomUUID3(), agentId: randomUUID3() };
|
|
2078
|
+
await fs6.writeFile(file, JSON.stringify(identity), { mode: 384 });
|
|
1641
2079
|
return identity;
|
|
1642
2080
|
}
|
|
1643
2081
|
};
|
|
1644
2082
|
|
|
1645
2083
|
// src/engine/user-defaults.ts
|
|
1646
|
-
import
|
|
2084
|
+
import fs7 from "fs/promises";
|
|
1647
2085
|
import os4 from "os";
|
|
1648
|
-
import
|
|
2086
|
+
import path8 from "path";
|
|
1649
2087
|
async function readJson(file) {
|
|
1650
2088
|
try {
|
|
1651
|
-
const parsed = JSON.parse(await
|
|
2089
|
+
const parsed = JSON.parse(await fs7.readFile(file, "utf8"));
|
|
1652
2090
|
return typeof parsed === "object" && parsed !== null ? parsed : null;
|
|
1653
2091
|
} catch {
|
|
1654
2092
|
return null;
|
|
@@ -1659,8 +2097,8 @@ function str3(value) {
|
|
|
1659
2097
|
}
|
|
1660
2098
|
async function claudeDefaults(cwd, home = os4.homedir()) {
|
|
1661
2099
|
const files = [
|
|
1662
|
-
...cwd ? [
|
|
1663
|
-
|
|
2100
|
+
...cwd ? [path8.join(cwd, ".claude", "settings.local.json"), path8.join(cwd, ".claude", "settings.json")] : [],
|
|
2101
|
+
path8.join(home, ".claude", "settings.json")
|
|
1664
2102
|
];
|
|
1665
2103
|
let model = null;
|
|
1666
2104
|
let effort = null;
|
|
@@ -1676,7 +2114,7 @@ async function claudeDefaults(cwd, home = os4.homedir()) {
|
|
|
1676
2114
|
async function codexDefaults(home = os4.homedir()) {
|
|
1677
2115
|
let text;
|
|
1678
2116
|
try {
|
|
1679
|
-
text = await
|
|
2117
|
+
text = await fs7.readFile(path8.join(home, ".codex", "config.toml"), "utf8");
|
|
1680
2118
|
} catch {
|
|
1681
2119
|
return { model: null, effort: null };
|
|
1682
2120
|
}
|
|
@@ -1700,31 +2138,31 @@ var EFFORT_LEVELS = {
|
|
|
1700
2138
|
// src/engine/index.ts
|
|
1701
2139
|
var RAW_TRANSCRIPT_MAX_BYTES = 200 * 1048576;
|
|
1702
2140
|
function moduleDir() {
|
|
1703
|
-
return
|
|
2141
|
+
return path9.dirname(fileURLToPath(import.meta.url));
|
|
1704
2142
|
}
|
|
1705
2143
|
function resolveWorkerPath() {
|
|
1706
2144
|
const dir = moduleDir();
|
|
1707
2145
|
for (const candidate of [
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
2146
|
+
path9.join(dir, "worker.js"),
|
|
2147
|
+
path9.join(dir, "worker.ts"),
|
|
2148
|
+
path9.join(dir, "..", "engine", "worker.js"),
|
|
2149
|
+
path9.join(dir, "..", "engine", "worker.ts")
|
|
1712
2150
|
]) {
|
|
1713
2151
|
if (existsSync(candidate)) return candidate;
|
|
1714
2152
|
}
|
|
1715
2153
|
throw new Error(`[coding-sessions] The driver worker is missing next to ${dir}.`);
|
|
1716
2154
|
}
|
|
1717
2155
|
async function installShim(dataDir) {
|
|
1718
|
-
const source =
|
|
2156
|
+
const source = path9.resolve(moduleDir(), "..", "..", "bin", "cli-shim.sh");
|
|
1719
2157
|
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
|
|
2158
|
+
const body = await fs8.readFile(source, "utf8");
|
|
2159
|
+
const target = path9.join(dataDir, "bin", "cli-shim.sh");
|
|
2160
|
+
const current = await fs8.readFile(target, "utf8").catch(() => null);
|
|
1723
2161
|
if (current !== body) {
|
|
1724
|
-
await
|
|
1725
|
-
await
|
|
2162
|
+
await fs8.mkdir(path9.dirname(target), { recursive: true });
|
|
2163
|
+
await fs8.writeFile(target, body, { mode: 493 });
|
|
1726
2164
|
}
|
|
1727
|
-
await
|
|
2165
|
+
await fs8.chmod(target, 493);
|
|
1728
2166
|
return target;
|
|
1729
2167
|
}
|
|
1730
2168
|
function nowIso() {
|
|
@@ -1824,29 +2262,30 @@ function describeAuthStatus(provider, probe) {
|
|
|
1824
2262
|
};
|
|
1825
2263
|
}
|
|
1826
2264
|
async function createCodingSessionsEngine(options = {}) {
|
|
1827
|
-
const dataDir = options.dataDir ??
|
|
1828
|
-
const stateDir =
|
|
2265
|
+
const dataDir = options.dataDir ?? path9.join(os5.homedir(), ".matrx", "coding-sessions");
|
|
2266
|
+
const stateDir = path9.join(dataDir, "paperclip-state");
|
|
1829
2267
|
const homes = options.homes ?? defaultProviderHomes();
|
|
1830
2268
|
const index = new RunIndex(dataDir);
|
|
1831
2269
|
await index.load();
|
|
1832
2270
|
const preferences = new PreferenceStore(dataDir);
|
|
1833
2271
|
await preferences.load();
|
|
1834
|
-
await
|
|
2272
|
+
await fs8.mkdir(stateDir, { recursive: true });
|
|
1835
2273
|
const identity = await index.identity();
|
|
2274
|
+
const agents = createAgentConnection(options.agents, dataDir);
|
|
1836
2275
|
const backups = options.rawBackup ? createRawBackupScheduler({
|
|
1837
2276
|
sink: options.rawBackup,
|
|
1838
2277
|
findFile: (provider, providerSessionId) => findSessionFile(provider, providerSessionId, homes),
|
|
1839
2278
|
onError: (error) => console.warn("[coding-sessions] raw transcript backup failed:", error)
|
|
1840
2279
|
}) : null;
|
|
1841
2280
|
const discoveredSeen = /* @__PURE__ */ new Map();
|
|
1842
|
-
const healthFile =
|
|
2281
|
+
const healthFile = path9.join(dataDir, "provider-health.json");
|
|
1843
2282
|
let health = {};
|
|
1844
2283
|
try {
|
|
1845
|
-
health = JSON.parse(await
|
|
2284
|
+
health = JSON.parse(await fs8.readFile(healthFile, "utf8"));
|
|
1846
2285
|
} catch {
|
|
1847
2286
|
health = {};
|
|
1848
2287
|
}
|
|
1849
|
-
const saveHealth = () =>
|
|
2288
|
+
const saveHealth = () => fs8.writeFile(healthFile, JSON.stringify(health), { mode: 384 }).catch(() => void 0);
|
|
1850
2289
|
function noteRunOutcome(provider, status, texts) {
|
|
1851
2290
|
if (status === "completed") {
|
|
1852
2291
|
if (health[provider]) {
|
|
@@ -1963,7 +2402,7 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
1963
2402
|
async function startRun(session, prompt) {
|
|
1964
2403
|
const provider = session.provider;
|
|
1965
2404
|
const realCommand = await commandFor(provider);
|
|
1966
|
-
const runId =
|
|
2405
|
+
const runId = randomUUID4();
|
|
1967
2406
|
const model = session.model ?? (provider === "claude" ? (await userDefaults(provider, session.cwd)).model ?? null : null);
|
|
1968
2407
|
const run = {
|
|
1969
2408
|
runId,
|
|
@@ -2001,7 +2440,36 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
2001
2440
|
let fileOffset = null;
|
|
2002
2441
|
if (session.providerSessionId) {
|
|
2003
2442
|
const file = await findSessionFile(provider, session.providerSessionId, homes);
|
|
2004
|
-
if (file) fileOffset = (await
|
|
2443
|
+
if (file) fileOffset = (await fs8.stat(file).catch(() => null))?.size ?? null;
|
|
2444
|
+
}
|
|
2445
|
+
let agentArgs = [];
|
|
2446
|
+
let cliPrompt = prompt;
|
|
2447
|
+
let delivered = null;
|
|
2448
|
+
if (!session.agent && session.agentRemovedDue) {
|
|
2449
|
+
if (provider === "codex") cliPrompt = `${AGENT_REMOVED_BLOCK}${prompt}`;
|
|
2450
|
+
delivered = { agentRemovedDue: false };
|
|
2451
|
+
}
|
|
2452
|
+
if (session.agent) {
|
|
2453
|
+
const bundle = await agents.readSessionBundle(session.id);
|
|
2454
|
+
if (!bundle) {
|
|
2455
|
+
const failed = saveRun({
|
|
2456
|
+
...run,
|
|
2457
|
+
status: "failed",
|
|
2458
|
+
endedAt: nowIso(),
|
|
2459
|
+
error: `The agent ${session.agent.name} could not be loaded on this computer anymore. Start a new session to load it again.`
|
|
2460
|
+
});
|
|
2461
|
+
const current = index.session(session.id);
|
|
2462
|
+
if (current) saveSession({ ...current, lastRunStatus: "failed" });
|
|
2463
|
+
return failed;
|
|
2464
|
+
}
|
|
2465
|
+
const attachment = await writeAgentAttachment(agents.sessionDir(session.id), bundle, {
|
|
2466
|
+
mcpUrl: await agents.mcpUrlFor(session.id)
|
|
2467
|
+
});
|
|
2468
|
+
agentArgs = agentArgsFor(provider, attachment, { resuming: session.providerSessionId !== null });
|
|
2469
|
+
if (provider === "codex" && session.agent.blockDue) {
|
|
2470
|
+
cliPrompt = `${renderFallbackBlock(bundle, path9.join(attachment.dir, "skills"))}${prompt}`;
|
|
2471
|
+
}
|
|
2472
|
+
delivered = { agent: { ...session.agent, blockDue: false, deliveredAt: run.startedAt } };
|
|
2005
2473
|
}
|
|
2006
2474
|
const child = forkWorker();
|
|
2007
2475
|
const liveRun = {
|
|
@@ -2017,6 +2485,10 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
2017
2485
|
};
|
|
2018
2486
|
live.set(runId, liveRun);
|
|
2019
2487
|
emitItem(liveRun, { kind: "user", id: `run-${runId}`, text: prompt, at: run.startedAt });
|
|
2488
|
+
if (delivered) {
|
|
2489
|
+
const current = index.session(session.id);
|
|
2490
|
+
if (current) saveSession({ ...current, ...delivered });
|
|
2491
|
+
}
|
|
2020
2492
|
const splitter = createLineSplitter();
|
|
2021
2493
|
const claude = provider === "claude" ? createClaudeNormalizer() : null;
|
|
2022
2494
|
const codex = provider === "codex" ? createCodexStreamNormalizer(runId.slice(0, 8)) : null;
|
|
@@ -2055,6 +2527,7 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
2055
2527
|
}
|
|
2056
2528
|
}
|
|
2057
2529
|
void pruneDriverScratch();
|
|
2530
|
+
if (provider === "codex" && final.status === "completed") void checkCompaction(session.id);
|
|
2058
2531
|
};
|
|
2059
2532
|
let settled = false;
|
|
2060
2533
|
child.on("message", (raw) => {
|
|
@@ -2169,16 +2642,39 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
2169
2642
|
provider,
|
|
2170
2643
|
runId,
|
|
2171
2644
|
cwd: session.cwd,
|
|
2172
|
-
prompt,
|
|
2645
|
+
prompt: cliPrompt,
|
|
2173
2646
|
model,
|
|
2174
2647
|
effort: session.effort ?? null,
|
|
2175
2648
|
resumeSessionId: session.providerSessionId,
|
|
2176
2649
|
shimPath,
|
|
2177
2650
|
realCommand,
|
|
2178
|
-
identity
|
|
2651
|
+
identity,
|
|
2652
|
+
agentArgs
|
|
2179
2653
|
});
|
|
2180
2654
|
return run;
|
|
2181
2655
|
}
|
|
2656
|
+
async function checkCompaction(sessionId) {
|
|
2657
|
+
const session = index.session(sessionId);
|
|
2658
|
+
if (!session?.agent || session.agent.blockDue || !session.providerSessionId) return;
|
|
2659
|
+
const file = await findSessionFile("codex", session.providerSessionId, homes);
|
|
2660
|
+
if (!file || !await codexCompactionDroppedAgent(file, session.agent.deliveredAt ?? null)) return;
|
|
2661
|
+
const current = index.session(sessionId);
|
|
2662
|
+
if (current?.agent) {
|
|
2663
|
+
saveSession({ ...current, agent: { ...current.agent, blockDue: true } });
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
async function attachAgent(sessionId, selection, options2) {
|
|
2667
|
+
const bundle = await agents.loadBundle(selection);
|
|
2668
|
+
await agents.saveSessionBundle(sessionId, bundle);
|
|
2669
|
+
return {
|
|
2670
|
+
...sessionAgentRecord(bundle, {
|
|
2671
|
+
toolsServed: await agents.mcpUrlFor(sessionId) !== null,
|
|
2672
|
+
variables: selection.variables ?? {}
|
|
2673
|
+
}),
|
|
2674
|
+
blockDue: options2.blockDue,
|
|
2675
|
+
deliveredAt: null
|
|
2676
|
+
};
|
|
2677
|
+
}
|
|
2182
2678
|
function killGroup(liveRun) {
|
|
2183
2679
|
const target = liveRun.pgid;
|
|
2184
2680
|
if (target === null) return;
|
|
@@ -2194,7 +2690,7 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
2194
2690
|
}, 3e3);
|
|
2195
2691
|
}
|
|
2196
2692
|
async function pruneDriverScratch() {
|
|
2197
|
-
const runsDir =
|
|
2693
|
+
const runsDir = path9.join(
|
|
2198
2694
|
stateDir,
|
|
2199
2695
|
"instances",
|
|
2200
2696
|
"default",
|
|
@@ -2205,9 +2701,9 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
2205
2701
|
"claude-runtime",
|
|
2206
2702
|
"runs"
|
|
2207
2703
|
);
|
|
2208
|
-
const entries = await
|
|
2704
|
+
const entries = await fs8.readdir(runsDir).catch(() => []);
|
|
2209
2705
|
await Promise.all(
|
|
2210
|
-
entries.filter((name) => !live.has(name)).map((name) =>
|
|
2706
|
+
entries.filter((name) => !live.has(name)).map((name) => fs8.rm(path9.join(runsDir, name), { recursive: true, force: true }).catch(() => void 0))
|
|
2211
2707
|
);
|
|
2212
2708
|
}
|
|
2213
2709
|
async function probe() {
|
|
@@ -2248,7 +2744,7 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
2248
2744
|
function newSession(input) {
|
|
2249
2745
|
const at = nowIso();
|
|
2250
2746
|
return {
|
|
2251
|
-
id:
|
|
2747
|
+
id: randomUUID4(),
|
|
2252
2748
|
provider: input.provider,
|
|
2253
2749
|
providerSessionId: input.providerSessionId,
|
|
2254
2750
|
cwd: input.cwd,
|
|
@@ -2266,17 +2762,17 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
2266
2762
|
function resolveFolder(raw) {
|
|
2267
2763
|
const trimmed = raw.trim();
|
|
2268
2764
|
if (!trimmed) return null;
|
|
2269
|
-
return
|
|
2765
|
+
return path9.resolve(trimmed.replace(/^~(?=$|\/)/, os5.homedir()));
|
|
2270
2766
|
}
|
|
2271
2767
|
async function checkFolder(raw) {
|
|
2272
2768
|
const target = resolveFolder(raw);
|
|
2273
2769
|
if (!target) return { path: "", exists: false, isDirectory: false };
|
|
2274
|
-
const stat = await
|
|
2770
|
+
const stat = await fs8.stat(target).catch(() => null);
|
|
2275
2771
|
return { path: target, exists: stat !== null, isDirectory: stat?.isDirectory() === true };
|
|
2276
2772
|
}
|
|
2277
2773
|
async function assertFolder(cwd) {
|
|
2278
|
-
const resolved =
|
|
2279
|
-
const stat = await
|
|
2774
|
+
const resolved = path9.resolve(cwd.replace(/^~(?=$|\/)/, os5.homedir()));
|
|
2775
|
+
const stat = await fs8.stat(resolved).catch(() => null);
|
|
2280
2776
|
if (!stat?.isDirectory()) {
|
|
2281
2777
|
throw new Error(`The folder ${resolved} doesn't exist on this computer.`);
|
|
2282
2778
|
}
|
|
@@ -2338,7 +2834,7 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
2338
2834
|
folders.map(async ([folder, lastUsedAt]) => ({
|
|
2339
2835
|
path: folder,
|
|
2340
2836
|
lastUsedAt,
|
|
2341
|
-
exists: (await
|
|
2837
|
+
exists: (await fs8.stat(folder).catch(() => null))?.isDirectory() === true
|
|
2342
2838
|
}))
|
|
2343
2839
|
);
|
|
2344
2840
|
},
|
|
@@ -2383,7 +2879,7 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
2383
2879
|
async createFolder(raw) {
|
|
2384
2880
|
const target = resolveFolder(raw);
|
|
2385
2881
|
if (!target) throw new Error("Type a folder path first.");
|
|
2386
|
-
await
|
|
2882
|
+
await fs8.mkdir(target, { recursive: true });
|
|
2387
2883
|
return checkFolder(target);
|
|
2388
2884
|
},
|
|
2389
2885
|
async sessionActivity(provider, providerSessionId) {
|
|
@@ -2421,48 +2917,63 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
2421
2917
|
reason: `${PROVIDER_LABEL[provider]} has no saved transcript for this chat on this computer.`
|
|
2422
2918
|
};
|
|
2423
2919
|
}
|
|
2424
|
-
const stat = await
|
|
2920
|
+
const stat = await fs8.stat(file);
|
|
2425
2921
|
if (stat.size > RAW_TRANSCRIPT_MAX_BYTES) {
|
|
2426
2922
|
return {
|
|
2427
2923
|
available: false,
|
|
2428
2924
|
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
2925
|
};
|
|
2430
2926
|
}
|
|
2431
|
-
const content = await
|
|
2432
|
-
return { available: true, fileName:
|
|
2927
|
+
const content = await fs8.readFile(file, "utf8");
|
|
2928
|
+
return { available: true, fileName: path9.basename(file), bytes: stat.size, content };
|
|
2433
2929
|
},
|
|
2434
2930
|
async start(input) {
|
|
2435
2931
|
const prompt = input.prompt.trim();
|
|
2436
2932
|
if (!prompt) throw new Error("Type what you want done before starting.");
|
|
2437
2933
|
const cwd = await assertFolder(input.cwd);
|
|
2438
|
-
const
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
);
|
|
2934
|
+
const draft = newSession({
|
|
2935
|
+
provider: input.provider,
|
|
2936
|
+
cwd,
|
|
2937
|
+
title: titleFrom(prompt),
|
|
2938
|
+
model: input.model?.trim() || null,
|
|
2939
|
+
effort: input.effort?.trim() || null,
|
|
2940
|
+
origin: "started",
|
|
2941
|
+
providerSessionId: null
|
|
2942
|
+
});
|
|
2943
|
+
if (input.agent) draft.agent = await attachAgent(draft.id, input.agent, { blockDue: false });
|
|
2944
|
+
const session = saveSession(draft);
|
|
2449
2945
|
const run = await startRun(session, prompt);
|
|
2450
2946
|
return { session: index.session(session.id) ?? session, run, queued: false };
|
|
2451
2947
|
},
|
|
2452
2948
|
async takeover(input) {
|
|
2453
2949
|
const existing = index.session(input.providerSessionId);
|
|
2454
|
-
if (existing)
|
|
2950
|
+
if (existing) {
|
|
2951
|
+
if (!input.agent) return existing;
|
|
2952
|
+
return engine.setAgent(existing.id, input.agent);
|
|
2953
|
+
}
|
|
2455
2954
|
const cwd = await assertFolder(input.cwd);
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
);
|
|
2955
|
+
const draft = newSession({
|
|
2956
|
+
provider: input.provider,
|
|
2957
|
+
cwd,
|
|
2958
|
+
title: input.title?.trim() || "Taken-over chat",
|
|
2959
|
+
model: null,
|
|
2960
|
+
origin: "takeover",
|
|
2961
|
+
providerSessionId: input.providerSessionId
|
|
2962
|
+
});
|
|
2963
|
+
if (input.agent) draft.agent = await attachAgent(draft.id, input.agent, { blockDue: input.provider === "codex" });
|
|
2964
|
+
return saveSession(draft);
|
|
2965
|
+
},
|
|
2966
|
+
async setAgent(sessionId, selection) {
|
|
2967
|
+
const session = index.session(String(sessionId ?? ""));
|
|
2968
|
+
if (!session) throw new Error("This session is not in the list anymore.");
|
|
2969
|
+
if (!selection) {
|
|
2970
|
+
const told = session.provider === "codex" && session.agent?.deliveredAt != null;
|
|
2971
|
+
return saveSession({ ...session, agent: null, agentRemovedDue: told });
|
|
2972
|
+
}
|
|
2973
|
+
const agent = await attachAgent(session.id, selection, {
|
|
2974
|
+
blockDue: session.provider === "codex" && session.providerSessionId !== null
|
|
2975
|
+
});
|
|
2976
|
+
return saveSession({ ...session, agent, agentRemovedDue: false });
|
|
2466
2977
|
},
|
|
2467
2978
|
async send(sessionId, rawPrompt) {
|
|
2468
2979
|
const prompt = rawPrompt.trim();
|
|
@@ -2514,6 +3025,21 @@ async function createCodingSessionsEngine(options = {}) {
|
|
|
2514
3025
|
if (opts && "effort" in opts) next.effort = typeof opts.effort === "string" && opts.effort.trim() ? opts.effort.trim() : null;
|
|
2515
3026
|
return saveSession(next);
|
|
2516
3027
|
},
|
|
3028
|
+
async agentAccess() {
|
|
3029
|
+
return agents.access();
|
|
3030
|
+
},
|
|
3031
|
+
async agentCatalogRpc(request) {
|
|
3032
|
+
return agents.catalogRpc(request);
|
|
3033
|
+
},
|
|
3034
|
+
async agentDetails(agentId) {
|
|
3035
|
+
return agents.details(String(agentId ?? ""));
|
|
3036
|
+
},
|
|
3037
|
+
setMcpBaseUrl(baseUrl) {
|
|
3038
|
+
agents.setMcpBaseUrl(baseUrl);
|
|
3039
|
+
},
|
|
3040
|
+
async handleMcp(secret, body) {
|
|
3041
|
+
return agents.handleMcp(secret, body);
|
|
3042
|
+
},
|
|
2517
3043
|
async dispose() {
|
|
2518
3044
|
for (const liveRun of live.values()) {
|
|
2519
3045
|
liveRun.stopRequested = true;
|
|
@@ -2583,7 +3109,11 @@ var BRIDGE_METHODS = [
|
|
|
2583
3109
|
"setChatPreference",
|
|
2584
3110
|
"searchChats",
|
|
2585
3111
|
"openFolder",
|
|
2586
|
-
"setSessionOptions"
|
|
3112
|
+
"setSessionOptions",
|
|
3113
|
+
"agentAccess",
|
|
3114
|
+
"agentCatalogRpc",
|
|
3115
|
+
"agentDetails",
|
|
3116
|
+
"setAgent"
|
|
2587
3117
|
];
|
|
2588
3118
|
var BridgeError = class extends Error {
|
|
2589
3119
|
constructor(message, code) {
|
|
@@ -2627,14 +3157,14 @@ var MIME = {
|
|
|
2627
3157
|
".map": "application/json"
|
|
2628
3158
|
};
|
|
2629
3159
|
function moduleDir2() {
|
|
2630
|
-
return
|
|
3160
|
+
return path10.dirname(fileURLToPath2(import.meta.url));
|
|
2631
3161
|
}
|
|
2632
3162
|
function defaultUiDir() {
|
|
2633
3163
|
for (const candidate of [
|
|
2634
|
-
|
|
2635
|
-
|
|
3164
|
+
path10.resolve(moduleDir2(), "..", "ui"),
|
|
3165
|
+
path10.resolve(moduleDir2(), "..", "..", "dist", "ui")
|
|
2636
3166
|
]) {
|
|
2637
|
-
if (existsSync2(
|
|
3167
|
+
if (existsSync2(path10.join(candidate, "index.html"))) return candidate;
|
|
2638
3168
|
}
|
|
2639
3169
|
return null;
|
|
2640
3170
|
}
|
|
@@ -2657,19 +3187,19 @@ async function serveStatic(uiDir, req, res) {
|
|
|
2657
3187
|
return;
|
|
2658
3188
|
}
|
|
2659
3189
|
const requested = decodeURIComponent(url.pathname);
|
|
2660
|
-
let file =
|
|
3190
|
+
let file = path10.resolve(uiDir, `.${requested}`);
|
|
2661
3191
|
if (!file.startsWith(uiDir)) {
|
|
2662
3192
|
res.writeHead(403).end();
|
|
2663
3193
|
return;
|
|
2664
3194
|
}
|
|
2665
|
-
const stat = await
|
|
2666
|
-
if (!stat || stat.isDirectory()) file =
|
|
2667
|
-
const body = await
|
|
3195
|
+
const stat = await fs9.stat(file).catch(() => null);
|
|
3196
|
+
if (!stat || stat.isDirectory()) file = path10.join(uiDir, "index.html");
|
|
3197
|
+
const body = await fs9.readFile(file).catch(() => null);
|
|
2668
3198
|
if (!body) {
|
|
2669
3199
|
res.writeHead(404).end();
|
|
2670
3200
|
return;
|
|
2671
3201
|
}
|
|
2672
|
-
const ext =
|
|
3202
|
+
const ext = path10.extname(file);
|
|
2673
3203
|
res.writeHead(200, {
|
|
2674
3204
|
"content-type": MIME[ext] ?? "application/octet-stream",
|
|
2675
3205
|
"cache-control": ext === ".html" ? "no-store" : "public, max-age=31536000, immutable",
|
|
@@ -2678,6 +3208,46 @@ async function serveStatic(uiDir, req, res) {
|
|
|
2678
3208
|
});
|
|
2679
3209
|
res.end(body);
|
|
2680
3210
|
}
|
|
3211
|
+
var MAX_MCP_BODY = 4 * 1024 * 1024;
|
|
3212
|
+
async function handleMcp(engine, secret, req, res, allowedOrigin) {
|
|
3213
|
+
if (!allowedOrigin(req.headers.origin) || !/^[A-Za-z0-9_-]{16,}$/.test(secret)) {
|
|
3214
|
+
res.writeHead(403).end();
|
|
3215
|
+
return;
|
|
3216
|
+
}
|
|
3217
|
+
if (req.method !== "POST") {
|
|
3218
|
+
res.writeHead(405, { allow: "POST" }).end();
|
|
3219
|
+
return;
|
|
3220
|
+
}
|
|
3221
|
+
const chunks = [];
|
|
3222
|
+
let size = 0;
|
|
3223
|
+
for await (const chunk of req) {
|
|
3224
|
+
size += chunk.length;
|
|
3225
|
+
if (size > MAX_MCP_BODY) {
|
|
3226
|
+
res.writeHead(413).end();
|
|
3227
|
+
return;
|
|
3228
|
+
}
|
|
3229
|
+
chunks.push(chunk);
|
|
3230
|
+
}
|
|
3231
|
+
let body;
|
|
3232
|
+
try {
|
|
3233
|
+
body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
3234
|
+
} catch {
|
|
3235
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
3236
|
+
res.end(JSON.stringify({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error." } }));
|
|
3237
|
+
return;
|
|
3238
|
+
}
|
|
3239
|
+
const reply = engine.handleMcp ? await engine.handleMcp(secret, body) : null;
|
|
3240
|
+
if (!reply) {
|
|
3241
|
+
res.writeHead(404).end();
|
|
3242
|
+
return;
|
|
3243
|
+
}
|
|
3244
|
+
if (reply.body === void 0) {
|
|
3245
|
+
res.writeHead(reply.status).end();
|
|
3246
|
+
return;
|
|
3247
|
+
}
|
|
3248
|
+
res.writeHead(reply.status, { "content-type": "application/json" });
|
|
3249
|
+
res.end(JSON.stringify(reply.body));
|
|
3250
|
+
}
|
|
2681
3251
|
function attachSocket(socket, bridge) {
|
|
2682
3252
|
const unsubscribe = bridge.subscribe((event) => {
|
|
2683
3253
|
if (socket.readyState === socket.OPEN) socket.send(JSON.stringify({ event }));
|
|
@@ -2708,7 +3278,7 @@ function attachSocket(socket, bridge) {
|
|
|
2708
3278
|
socket.on("close", unsubscribe);
|
|
2709
3279
|
}
|
|
2710
3280
|
async function startCodingSessionsHost(options = {}) {
|
|
2711
|
-
const token = options.token ??
|
|
3281
|
+
const token = options.token ?? randomBytes2(24).toString("base64url");
|
|
2712
3282
|
const engine = options.engine ?? await createCodingSessionsEngine(options);
|
|
2713
3283
|
const bridge = createHostBridge(engine);
|
|
2714
3284
|
const uiDir = options.uiDir ?? defaultUiDir();
|
|
@@ -2719,6 +3289,13 @@ async function startCodingSessionsHost(options = {}) {
|
|
|
2719
3289
|
};
|
|
2720
3290
|
const server = http.createServer((req, res) => {
|
|
2721
3291
|
const url2 = new URL(req.url ?? "/", `http://${HOST}`);
|
|
3292
|
+
if (url2.pathname.startsWith("/mcp/")) {
|
|
3293
|
+
handleMcp(engine, url2.pathname.slice("/mcp/".length), req, res, allowedOrigin).catch(() => {
|
|
3294
|
+
if (!res.headersSent) res.writeHead(500);
|
|
3295
|
+
res.end();
|
|
3296
|
+
});
|
|
3297
|
+
return;
|
|
3298
|
+
}
|
|
2722
3299
|
if (url2.pathname === "/health") {
|
|
2723
3300
|
res.writeHead(200, { "content-type": "application/json" });
|
|
2724
3301
|
res.end(JSON.stringify({ ok: true, service: "@ai-matrx/coding-sessions" }));
|
|
@@ -2749,6 +3326,7 @@ async function startCodingSessionsHost(options = {}) {
|
|
|
2749
3326
|
server.off("error", reject);
|
|
2750
3327
|
const address = server.address();
|
|
2751
3328
|
if (address && typeof address === "object") port = address.port;
|
|
3329
|
+
engine.setMcpBaseUrl?.(`http://${HOST}:${port}`);
|
|
2752
3330
|
resolve();
|
|
2753
3331
|
});
|
|
2754
3332
|
});
|
|
@@ -2759,6 +3337,7 @@ async function startCodingSessionsHost(options = {}) {
|
|
|
2759
3337
|
token,
|
|
2760
3338
|
engine,
|
|
2761
3339
|
async close() {
|
|
3340
|
+
engine.setMcpBaseUrl?.(null);
|
|
2762
3341
|
for (const client of wss.clients) client.terminate();
|
|
2763
3342
|
await new Promise((resolve) => wss.close(() => resolve()));
|
|
2764
3343
|
await new Promise((resolve) => server.close(() => resolve()));
|