@serendb/serendesktop 0.1.19 → 0.1.21
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/dist/server.js +1717 -144
- package/package.json +1 -1
- package/public/assets/{auto-connect-eUiUL4wC.js → auto-connect-BYCwLt45.js} +1 -1
- package/public/assets/index-C47M3-rN.css +10 -0
- package/public/assets/index-DdVUUsaR.js +259 -0
- package/public/index.html +2 -2
- package/public/assets/index-B0sqAT65.css +0 -10
- package/public/assets/index-czh6D9Hj.js +0 -225
package/dist/server.js
CHANGED
|
@@ -4,8 +4,8 @@ import { exec as exec2 } from "child_process";
|
|
|
4
4
|
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
|
|
5
5
|
import { createServer as createServer2 } from "http";
|
|
6
6
|
import { request as httpsRequest } from "https";
|
|
7
|
-
import { homedir as
|
|
8
|
-
import { join as
|
|
7
|
+
import { homedir as homedir8, platform as platform4 } from "os";
|
|
8
|
+
import { join as join9, extname as extname2 } from "path";
|
|
9
9
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
10
10
|
import { createRequire as createRequire2 } from "module";
|
|
11
11
|
import { WebSocketServer } from "ws";
|
|
@@ -201,42 +201,1149 @@ async function handleMessage(raw) {
|
|
|
201
201
|
}
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
// src/services/orchestrator.ts
|
|
205
|
+
import { readFile } from "fs/promises";
|
|
206
|
+
import { join } from "path";
|
|
207
|
+
|
|
208
|
+
// src/services/rlm.ts
|
|
209
|
+
var PUBLISHER_SLUG = "seren-models";
|
|
210
|
+
var RLM_THRESHOLD = 0.85;
|
|
211
|
+
var CHUNK_TARGET_FRACTION = 0.45;
|
|
212
|
+
var CHUNK_OVERLAP_CHARS = 800;
|
|
213
|
+
var REQUEST_TIMEOUT_MS = 6e5;
|
|
214
|
+
function modelContextLimitChars(modelId) {
|
|
215
|
+
let tokens;
|
|
216
|
+
if (modelId.includes("gemini-1.5") || modelId.includes("gemini-2") || modelId.includes("gemini-3")) {
|
|
217
|
+
tokens = 1e6;
|
|
218
|
+
} else if (modelId.includes("claude")) {
|
|
219
|
+
tokens = 2e5;
|
|
220
|
+
} else if (modelId.includes("gpt-4")) {
|
|
221
|
+
tokens = 128e3;
|
|
222
|
+
} else {
|
|
223
|
+
tokens = 1e5;
|
|
224
|
+
}
|
|
225
|
+
return tokens * 4;
|
|
226
|
+
}
|
|
227
|
+
function needsRlm(inputChars, modelId) {
|
|
228
|
+
const limit = modelContextLimitChars(modelId);
|
|
229
|
+
const threshold = Math.floor(limit * RLM_THRESHOLD);
|
|
230
|
+
return inputChars > threshold;
|
|
231
|
+
}
|
|
232
|
+
async function processRlm(query, content, model, apiBase, apiKey, onEvent) {
|
|
233
|
+
const limit = modelContextLimitChars(model);
|
|
234
|
+
const chunkBudget = Math.floor(limit * CHUNK_TARGET_FRACTION);
|
|
235
|
+
let strategy;
|
|
236
|
+
try {
|
|
237
|
+
strategy = await classifyTask(query, apiBase, apiKey);
|
|
238
|
+
} catch (err) {
|
|
239
|
+
strategy = "sequential";
|
|
240
|
+
}
|
|
241
|
+
const chunks = chunkContent(content, chunkBudget);
|
|
242
|
+
onEvent?.({ type: "rlm_start", data: { index: 0, total: chunks.length } });
|
|
243
|
+
let finalAnswer;
|
|
244
|
+
if (strategy === "synthesis") {
|
|
245
|
+
finalAnswer = await processMapReduce(
|
|
246
|
+
chunks,
|
|
247
|
+
query,
|
|
248
|
+
model,
|
|
249
|
+
apiBase,
|
|
250
|
+
apiKey,
|
|
251
|
+
onEvent
|
|
252
|
+
);
|
|
253
|
+
} else {
|
|
254
|
+
finalAnswer = await processSequential(
|
|
255
|
+
chunks,
|
|
256
|
+
query,
|
|
257
|
+
model,
|
|
258
|
+
apiBase,
|
|
259
|
+
apiKey,
|
|
260
|
+
onEvent
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
return finalAnswer;
|
|
264
|
+
}
|
|
265
|
+
async function classifyTask(query, apiBase, apiKey) {
|
|
266
|
+
const url = `${apiBase}/publishers/${PUBLISHER_SLUG}/chat/completions`;
|
|
267
|
+
const system = "You are a task classifier. Respond with exactly one word.";
|
|
268
|
+
const user = [
|
|
269
|
+
'Classify this task as either "synthesis" (requires reasoning across a whole document:',
|
|
270
|
+
'summarize, analyze, compare, find themes) or "sequential" (can be done chunk by chunk:',
|
|
271
|
+
"translate, reformat, extract).",
|
|
272
|
+
"",
|
|
273
|
+
`Task: ${query}`,
|
|
274
|
+
"",
|
|
275
|
+
"Respond with exactly one word: synthesis or sequential"
|
|
276
|
+
].join("\n");
|
|
277
|
+
const body = {
|
|
278
|
+
model: "anthropic/claude-sonnet-4",
|
|
279
|
+
// cheap model for classification
|
|
280
|
+
messages: [
|
|
281
|
+
{ role: "system", content: system },
|
|
282
|
+
{ role: "user", content: user }
|
|
283
|
+
],
|
|
284
|
+
stream: false,
|
|
285
|
+
max_tokens: 10
|
|
286
|
+
};
|
|
287
|
+
const response = await fetchWithTimeout(url, {
|
|
288
|
+
method: "POST",
|
|
289
|
+
headers: {
|
|
290
|
+
"Content-Type": "application/json",
|
|
291
|
+
Authorization: `Bearer ${apiKey}`
|
|
292
|
+
},
|
|
293
|
+
body: JSON.stringify(body)
|
|
294
|
+
});
|
|
295
|
+
if (!response.ok) {
|
|
296
|
+
const text = await response.text();
|
|
297
|
+
throw new Error(`Classify HTTP ${response.status}: ${text}`);
|
|
298
|
+
}
|
|
299
|
+
const json = await response.json();
|
|
300
|
+
const answer = (json?.choices?.[0]?.message?.content ?? "").trim().toLowerCase();
|
|
301
|
+
return answer.includes("sequential") ? "sequential" : "synthesis";
|
|
302
|
+
}
|
|
303
|
+
function chunkContent(content, budget) {
|
|
304
|
+
const raw = splitAtBudget(content, budget);
|
|
305
|
+
const total = raw.length;
|
|
306
|
+
return raw.map((text, i) => ({ index: i, total, text }));
|
|
307
|
+
}
|
|
308
|
+
function overlapStartFor(tailStart) {
|
|
309
|
+
return tailStart > CHUNK_OVERLAP_CHARS ? tailStart - CHUNK_OVERLAP_CHARS : tailStart;
|
|
310
|
+
}
|
|
311
|
+
function splitAtBudget(text, budget) {
|
|
312
|
+
if (text.length <= budget) {
|
|
313
|
+
return [text];
|
|
314
|
+
}
|
|
315
|
+
const windowStart = Math.floor(budget / 2);
|
|
316
|
+
const searchRegion = text.slice(0, Math.min(budget, text.length));
|
|
317
|
+
const headingPos = findHeadingBoundary(searchRegion, windowStart);
|
|
318
|
+
if (headingPos !== -1) {
|
|
319
|
+
const head2 = text.slice(0, headingPos);
|
|
320
|
+
const tailWithOverlap2 = text.slice(overlapStartFor(head2.length));
|
|
321
|
+
return [head2, ...splitAtBudget(tailWithOverlap2, budget)];
|
|
322
|
+
}
|
|
323
|
+
const dblNewlinePos = rfindInRange(searchRegion, "\n\n", windowStart);
|
|
324
|
+
if (dblNewlinePos !== -1) {
|
|
325
|
+
const splitPos = dblNewlinePos + 2;
|
|
326
|
+
const head2 = text.slice(0, splitPos);
|
|
327
|
+
const tailWithOverlap2 = text.slice(overlapStartFor(head2.length));
|
|
328
|
+
return [head2, ...splitAtBudget(tailWithOverlap2, budget)];
|
|
329
|
+
}
|
|
330
|
+
const newlinePos = rfindInRange(searchRegion, "\n", windowStart);
|
|
331
|
+
if (newlinePos !== -1) {
|
|
332
|
+
const splitPos = newlinePos + 1;
|
|
333
|
+
const head2 = text.slice(0, splitPos);
|
|
334
|
+
const tailWithOverlap2 = text.slice(overlapStartFor(head2.length));
|
|
335
|
+
return [head2, ...splitAtBudget(tailWithOverlap2, budget)];
|
|
336
|
+
}
|
|
337
|
+
for (const sep of [". ", "! ", "? "]) {
|
|
338
|
+
const sentPos = rfindInRange(searchRegion, sep, windowStart);
|
|
339
|
+
if (sentPos !== -1) {
|
|
340
|
+
const splitPos = sentPos + sep.length;
|
|
341
|
+
const head2 = text.slice(0, splitPos);
|
|
342
|
+
const tailWithOverlap2 = text.slice(overlapStartFor(head2.length));
|
|
343
|
+
return [head2, ...splitAtBudget(tailWithOverlap2, budget)];
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const head = text.slice(0, budget);
|
|
347
|
+
const tailWithOverlap = text.slice(overlapStartFor(head.length));
|
|
348
|
+
return [head, ...splitAtBudget(tailWithOverlap, budget)];
|
|
349
|
+
}
|
|
350
|
+
function rfindInRange(haystack, needle, minPos) {
|
|
351
|
+
const lastIdx = haystack.lastIndexOf(needle);
|
|
352
|
+
return lastIdx >= minPos ? lastIdx : -1;
|
|
353
|
+
}
|
|
354
|
+
function findHeadingBoundary(text, minPos) {
|
|
355
|
+
let idx = text.indexOf("\n", minPos);
|
|
356
|
+
while (idx !== -1 && idx + 1 < text.length) {
|
|
357
|
+
const nextChar = text[idx + 1];
|
|
358
|
+
if (nextChar === "#" || nextChar >= "0" && nextChar <= "9") {
|
|
359
|
+
return idx + 1;
|
|
360
|
+
}
|
|
361
|
+
idx = text.indexOf("\n", idx + 1);
|
|
362
|
+
}
|
|
363
|
+
return -1;
|
|
364
|
+
}
|
|
365
|
+
async function processMapReduce(chunks, query, model, apiBase, apiKey, onEvent) {
|
|
366
|
+
const promises = chunks.map(async (chunk) => {
|
|
367
|
+
const prompt = [
|
|
368
|
+
`Question: ${query}`,
|
|
369
|
+
"",
|
|
370
|
+
`Document section ${chunk.index + 1}/${chunk.total}:`,
|
|
371
|
+
"",
|
|
372
|
+
chunk.text,
|
|
373
|
+
"",
|
|
374
|
+
"Answer the question based only on the content in this section. If the section does not contain relevant information, say so briefly."
|
|
375
|
+
].join("\n");
|
|
376
|
+
const summary = await callSimple(prompt, model, apiBase, apiKey);
|
|
377
|
+
onEvent?.({
|
|
378
|
+
type: "rlm_chunk_complete",
|
|
379
|
+
data: { index: chunk.index, total: chunk.total, summary }
|
|
380
|
+
});
|
|
381
|
+
return { index: chunk.index, total: chunk.total, summary };
|
|
382
|
+
});
|
|
383
|
+
const results = await Promise.all(promises);
|
|
384
|
+
results.sort((a, b) => a.index - b.index);
|
|
385
|
+
const mergePrompt = buildMergePrompt(query, results.map((r) => r.summary));
|
|
386
|
+
return callSimple(mergePrompt, model, apiBase, apiKey);
|
|
387
|
+
}
|
|
388
|
+
async function processSequential(chunks, query, model, apiBase, apiKey, onEvent) {
|
|
389
|
+
let runningSummary = "";
|
|
390
|
+
let lastAnswer = "";
|
|
391
|
+
for (const chunk of chunks) {
|
|
392
|
+
let prompt;
|
|
393
|
+
if (!runningSummary) {
|
|
394
|
+
prompt = [
|
|
395
|
+
`Question: ${query}`,
|
|
396
|
+
"",
|
|
397
|
+
`Document section ${chunk.index + 1}/${chunk.total}:`,
|
|
398
|
+
"",
|
|
399
|
+
chunk.text
|
|
400
|
+
].join("\n");
|
|
401
|
+
} else {
|
|
402
|
+
prompt = [
|
|
403
|
+
`Question: ${query}`,
|
|
404
|
+
"",
|
|
405
|
+
`Progress so far:`,
|
|
406
|
+
runningSummary,
|
|
407
|
+
"",
|
|
408
|
+
`Document section ${chunk.index + 1}/${chunk.total}:`,
|
|
409
|
+
"",
|
|
410
|
+
chunk.text
|
|
411
|
+
].join("\n");
|
|
412
|
+
}
|
|
413
|
+
const answer = await callSimple(prompt, model, apiBase, apiKey);
|
|
414
|
+
onEvent?.({
|
|
415
|
+
type: "rlm_chunk_complete",
|
|
416
|
+
data: { index: chunk.index, total: chunk.total, summary: answer }
|
|
417
|
+
});
|
|
418
|
+
runningSummary = answer;
|
|
419
|
+
lastAnswer = answer;
|
|
420
|
+
}
|
|
421
|
+
return lastAnswer;
|
|
422
|
+
}
|
|
423
|
+
async function callSimple(prompt, model, apiBase, apiKey) {
|
|
424
|
+
const url = `${apiBase}/publishers/${PUBLISHER_SLUG}/chat/completions`;
|
|
425
|
+
const messages = [
|
|
426
|
+
{ role: "system", content: "You are a helpful AI assistant." },
|
|
427
|
+
{ role: "user", content: prompt }
|
|
428
|
+
];
|
|
429
|
+
const body = { model, messages, stream: false };
|
|
430
|
+
const response = await fetchWithTimeout(url, {
|
|
431
|
+
method: "POST",
|
|
432
|
+
headers: {
|
|
433
|
+
"Content-Type": "application/json",
|
|
434
|
+
Authorization: `Bearer ${apiKey}`
|
|
435
|
+
},
|
|
436
|
+
body: JSON.stringify(body)
|
|
437
|
+
});
|
|
438
|
+
if (!response.ok) {
|
|
439
|
+
const text = await response.text();
|
|
440
|
+
throw new Error(`RLM sub-call HTTP ${response.status}: ${text}`);
|
|
441
|
+
}
|
|
442
|
+
const json = await response.json();
|
|
443
|
+
const content = json?.choices?.[0]?.message?.content;
|
|
444
|
+
if (typeof content !== "string") {
|
|
445
|
+
throw new Error(`No content in RLM response: ${JSON.stringify(json)}`);
|
|
446
|
+
}
|
|
447
|
+
return content;
|
|
448
|
+
}
|
|
449
|
+
async function fetchWithTimeout(url, init) {
|
|
450
|
+
const controller = new AbortController();
|
|
451
|
+
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
452
|
+
try {
|
|
453
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
454
|
+
} finally {
|
|
455
|
+
clearTimeout(timeoutId);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
function buildMergePrompt(question, summaries) {
|
|
459
|
+
const parts = summaries.map(
|
|
460
|
+
(s, i) => `Section ${i + 1} answer:
|
|
461
|
+
${s}`
|
|
462
|
+
);
|
|
463
|
+
return [
|
|
464
|
+
`Question: ${question}`,
|
|
465
|
+
"",
|
|
466
|
+
`I processed a large document in ${summaries.length} sections. Here are the answers from each section:`,
|
|
467
|
+
"",
|
|
468
|
+
parts.join("\n\n"),
|
|
469
|
+
"",
|
|
470
|
+
"Synthesize these section answers into a single, coherent final answer to the question."
|
|
471
|
+
].join("\n");
|
|
472
|
+
}
|
|
473
|
+
function splitContentAndQuestion(prompt) {
|
|
474
|
+
const trimmed = prompt.trim();
|
|
475
|
+
const lastDoubleNewline = trimmed.lastIndexOf("\n\n");
|
|
476
|
+
if (lastDoubleNewline !== -1) {
|
|
477
|
+
const content = trimmed.slice(0, lastDoubleNewline).trim();
|
|
478
|
+
const question = trimmed.slice(lastDoubleNewline + 2).trim();
|
|
479
|
+
if (content && question) {
|
|
480
|
+
return [content, question];
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return [trimmed, trimmed];
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// src/services/tool-relevance.ts
|
|
487
|
+
var K1 = 1.5;
|
|
488
|
+
var B = 0.75;
|
|
489
|
+
var AVG_TOOL_WORDS = 60;
|
|
490
|
+
var CHARS_PER_TOKEN = 4;
|
|
491
|
+
var TOOL_TOKEN_BUDGET = 2e3;
|
|
492
|
+
var MIN_TOOLS = 3;
|
|
493
|
+
var MAX_TOOLS = 20;
|
|
494
|
+
var HARD_BYTE_BUDGET = 400 * 1024;
|
|
495
|
+
function selectRelevantTools(query, tools) {
|
|
496
|
+
const totalBytes = JSON.stringify(tools).length;
|
|
497
|
+
if (totalBytes <= HARD_BYTE_BUDGET && tools.length <= MAX_TOOLS) {
|
|
498
|
+
return [...tools];
|
|
499
|
+
}
|
|
500
|
+
if (tools.length === 0) {
|
|
501
|
+
return [];
|
|
502
|
+
}
|
|
503
|
+
const queryTerms = tokenize(query);
|
|
504
|
+
if (queryTerms.length === 0) {
|
|
505
|
+
return applyHardBudget(tools);
|
|
506
|
+
}
|
|
507
|
+
const docs = tools.map(toolText);
|
|
508
|
+
const scores = bm25Scores(queryTerms, docs);
|
|
509
|
+
const ranked = scores.map((score, idx) => [idx, score]).sort((a, b) => b[1] - a[1]);
|
|
510
|
+
const selectedIndices = [];
|
|
511
|
+
let tokenCount = 0;
|
|
512
|
+
for (const [idx] of ranked) {
|
|
513
|
+
if (selectedIndices.length >= MAX_TOOLS) {
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
const toolTokens = approximateTokens(docs[idx]);
|
|
517
|
+
const budgetExceeded = tokenCount + toolTokens > TOOL_TOKEN_BUDGET && selectedIndices.length >= MIN_TOOLS;
|
|
518
|
+
if (budgetExceeded) {
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
selectedIndices.push(idx);
|
|
522
|
+
tokenCount += toolTokens;
|
|
523
|
+
}
|
|
524
|
+
selectedIndices.sort((a, b) => a - b);
|
|
525
|
+
const result = selectedIndices.map((i) => tools[i]);
|
|
526
|
+
console.log(
|
|
527
|
+
`[ToolRelevance] Selected ${result.length} of ${tools.length} tools (~${tokenCount} tokens)`
|
|
528
|
+
);
|
|
529
|
+
return applyHardBudget(result);
|
|
530
|
+
}
|
|
531
|
+
function toolText(tool) {
|
|
532
|
+
const parts = [];
|
|
533
|
+
if (tool.function?.name) {
|
|
534
|
+
parts.push(tool.function.name);
|
|
535
|
+
}
|
|
536
|
+
if (tool.function?.description) {
|
|
537
|
+
parts.push(tool.function.description);
|
|
538
|
+
}
|
|
539
|
+
const props = tool.function?.parameters?.properties;
|
|
540
|
+
if (props) {
|
|
541
|
+
for (const [key, val] of Object.entries(props)) {
|
|
542
|
+
parts.push(key);
|
|
543
|
+
if (val?.description) {
|
|
544
|
+
parts.push(val.description);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return parts.join(" ").toLowerCase();
|
|
549
|
+
}
|
|
550
|
+
function tokenize(text) {
|
|
551
|
+
return text.split(/[^a-zA-Z0-9]+/).filter((t) => t.length > 1).map((t) => t.toLowerCase());
|
|
552
|
+
}
|
|
553
|
+
function bm25Scores(queryOrTerms, docs) {
|
|
554
|
+
const queryTerms = typeof queryOrTerms === "string" ? tokenize(queryOrTerms) : queryOrTerms;
|
|
555
|
+
const n = docs.length;
|
|
556
|
+
const tokenizedDocs = docs.map((d) => tokenize(d));
|
|
557
|
+
const dfMap = /* @__PURE__ */ new Map();
|
|
558
|
+
for (const term of queryTerms) {
|
|
559
|
+
if (dfMap.has(term)) continue;
|
|
560
|
+
let df = 0;
|
|
561
|
+
for (const doc of tokenizedDocs) {
|
|
562
|
+
if (doc.includes(term)) {
|
|
563
|
+
df++;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
dfMap.set(term, df);
|
|
567
|
+
}
|
|
568
|
+
return tokenizedDocs.map((docTerms) => {
|
|
569
|
+
const dl = docTerms.length;
|
|
570
|
+
const lengthNorm = K1 * (1 - B + B * dl / AVG_TOOL_WORDS);
|
|
571
|
+
let score = 0;
|
|
572
|
+
for (const term of queryTerms) {
|
|
573
|
+
const tf = docTerms.filter((t) => t === term).length;
|
|
574
|
+
if (tf === 0) continue;
|
|
575
|
+
const dfT = dfMap.get(term) ?? 0;
|
|
576
|
+
const idf = Math.log((n - dfT + 0.5) / (dfT + 0.5) + 1);
|
|
577
|
+
const tfNorm = tf * (K1 + 1) / (tf + lengthNorm);
|
|
578
|
+
score += idf * tfNorm;
|
|
579
|
+
}
|
|
580
|
+
return score;
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
function approximateTokens(text) {
|
|
584
|
+
return Math.max(Math.floor(text.length / CHARS_PER_TOKEN), 1);
|
|
585
|
+
}
|
|
586
|
+
function applyHardBudget(tools, budget = HARD_BYTE_BUDGET) {
|
|
587
|
+
const total = JSON.stringify(tools).length;
|
|
588
|
+
if (total <= budget) {
|
|
589
|
+
return [...tools];
|
|
590
|
+
}
|
|
591
|
+
const result = [];
|
|
592
|
+
let running = 2;
|
|
593
|
+
for (const tool of tools) {
|
|
594
|
+
const bytes = JSON.stringify(tool).length + 1;
|
|
595
|
+
if (running + bytes > budget) {
|
|
596
|
+
break;
|
|
597
|
+
}
|
|
598
|
+
running += bytes;
|
|
599
|
+
result.push(tool);
|
|
600
|
+
}
|
|
601
|
+
console.warn(
|
|
602
|
+
`[ToolRelevance] Hard byte budget applied: keeping ${result.length} of ${tools.length} tools (${running} bytes)`
|
|
603
|
+
);
|
|
604
|
+
return result;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// src/services/router.ts
|
|
608
|
+
var CODE_PREFERRED_MODELS = [
|
|
609
|
+
"anthropic/claude-opus-4-6",
|
|
610
|
+
"openai/gpt-5.3"
|
|
611
|
+
];
|
|
612
|
+
var SIMPLE_PREFERRED_MODELS = [
|
|
613
|
+
"minimax/minimax-m2.5",
|
|
614
|
+
"google/gemini-3-flash-preview",
|
|
615
|
+
"google/gemini-2.5-flash",
|
|
616
|
+
"anthropic/claude-haiku-4.5",
|
|
617
|
+
"moonshot/kimi-k2.5",
|
|
618
|
+
"thudm/glm-4.7",
|
|
619
|
+
"anthropic/claude-sonnet-4"
|
|
620
|
+
];
|
|
621
|
+
var LARGE_CONTEXT_FALLBACK_MODELS = [
|
|
622
|
+
"google/gemini-3.1-pro-preview",
|
|
623
|
+
"google/gemini-3-flash-preview",
|
|
624
|
+
"anthropic/claude-opus-4.6"
|
|
625
|
+
];
|
|
626
|
+
var REROUTABLE_STATUS_CODES = [408, 429, 502, 503, 504];
|
|
627
|
+
var MAX_REROUTE_ATTEMPTS = 2;
|
|
628
|
+
var MODEL_DISPLAY_NAMES = {
|
|
629
|
+
"anthropic/claude-opus-4-6": "Claude Opus",
|
|
630
|
+
"anthropic/claude-opus-4.5": "Claude Opus",
|
|
631
|
+
"anthropic/claude-sonnet-4": "Claude Sonnet",
|
|
632
|
+
"anthropic/claude-haiku-4.5": "Claude Haiku",
|
|
633
|
+
"openai/gpt-5.3": "GPT-5.3",
|
|
634
|
+
"openai/gpt-5": "GPT-5",
|
|
635
|
+
"openai/gpt-4o": "GPT-4o",
|
|
636
|
+
"openai/gpt-4o-mini": "GPT-4o Mini",
|
|
637
|
+
"anthropic/claude-opus-4.6": "Claude Opus 4.6",
|
|
638
|
+
"anthropic/claude-sonnet-4.6": "Claude Sonnet 4.6",
|
|
639
|
+
"google/gemini-3.1-pro-preview": "Gemini 3.1 Pro",
|
|
640
|
+
"google/gemini-2.5-pro": "Gemini Pro",
|
|
641
|
+
"google/gemini-2.5-flash": "Gemini Flash",
|
|
642
|
+
"google/gemini-3-flash-preview": "Gemini 3 Flash",
|
|
643
|
+
"moonshot/kimi-k2.5": "Kimi K2.5",
|
|
644
|
+
"thudm/glm-4.7": "GLM-4.7",
|
|
645
|
+
"thudm/glm-4": "GLM-4"
|
|
646
|
+
};
|
|
647
|
+
function humanizeModelId(modelId) {
|
|
648
|
+
return MODEL_DISPLAY_NAMES[modelId] ?? modelId;
|
|
649
|
+
}
|
|
650
|
+
function humanizeTaskType(taskType) {
|
|
651
|
+
return taskType.replace(/_/g, " ");
|
|
652
|
+
}
|
|
653
|
+
function route(classification, capabilities, query) {
|
|
654
|
+
const workerType = selectWorkerType(classification, capabilities);
|
|
655
|
+
const modelId = selectModel(classification, capabilities);
|
|
656
|
+
const selectedSkills = resolveSkills(classification, capabilities);
|
|
657
|
+
const reason = buildReason(classification, workerType, modelId);
|
|
658
|
+
const publisherSlug = extractPublisherSlug(workerType, capabilities, query);
|
|
659
|
+
const delegation = workerType === "local_agent" ? "full_handoff" : "in_loop";
|
|
660
|
+
return {
|
|
661
|
+
worker_type: workerType,
|
|
662
|
+
model_id: modelId,
|
|
663
|
+
delegation,
|
|
664
|
+
reason,
|
|
665
|
+
selected_skills: selectedSkills,
|
|
666
|
+
publisher_slug: publisherSlug,
|
|
667
|
+
reasoning_effort: capabilities.reasoning_effort
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
function selectWorkerType(classification, capabilities) {
|
|
671
|
+
if (classification.task_type === "code_generation" && classification.requires_file_system && capabilities.has_local_agent && capabilities.active_agent_session_id) {
|
|
672
|
+
return "local_agent";
|
|
673
|
+
}
|
|
674
|
+
if (classification.requires_tools && !classification.requires_file_system && hasAnyGatewayTool(capabilities)) {
|
|
675
|
+
return "mcp_publisher";
|
|
676
|
+
}
|
|
677
|
+
return "chat_model";
|
|
678
|
+
}
|
|
679
|
+
function selectModel(classification, capabilities) {
|
|
680
|
+
if (capabilities.selected_model) {
|
|
681
|
+
return capabilities.selected_model;
|
|
682
|
+
}
|
|
683
|
+
if (capabilities.model_rankings.length > 0) {
|
|
684
|
+
for (const [modelId] of capabilities.model_rankings) {
|
|
685
|
+
if (capabilities.available_models.includes(modelId)) {
|
|
686
|
+
return modelId;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
let preferred;
|
|
691
|
+
if (classification.task_type === "code_generation") {
|
|
692
|
+
preferred = CODE_PREFERRED_MODELS;
|
|
693
|
+
} else if (classification.complexity === "complex" || classification.complexity === "moderate") {
|
|
694
|
+
preferred = CODE_PREFERRED_MODELS;
|
|
695
|
+
} else {
|
|
696
|
+
preferred = SIMPLE_PREFERRED_MODELS;
|
|
697
|
+
}
|
|
698
|
+
for (const model of preferred) {
|
|
699
|
+
if (capabilities.available_models.includes(model)) {
|
|
700
|
+
return model;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
return capabilities.available_models[0] ?? "anthropic/claude-sonnet-4";
|
|
704
|
+
}
|
|
705
|
+
function parseGatewaySlug(toolName) {
|
|
706
|
+
if (!toolName.startsWith("gateway__")) return null;
|
|
707
|
+
const rest = toolName.slice("gateway__".length);
|
|
708
|
+
const slugEnd = rest.indexOf("__");
|
|
709
|
+
if (slugEnd === -1) return null;
|
|
710
|
+
return rest.slice(0, slugEnd);
|
|
711
|
+
}
|
|
712
|
+
function hasAnyGatewayTool(capabilities) {
|
|
713
|
+
return capabilities.available_tools.some((t) => parseGatewaySlug(t) !== null);
|
|
714
|
+
}
|
|
715
|
+
function extractGatewaySlug(capabilities, query) {
|
|
716
|
+
const slugTools = /* @__PURE__ */ new Map();
|
|
717
|
+
for (const toolName of capabilities.available_tools) {
|
|
718
|
+
const slug = parseGatewaySlug(toolName);
|
|
719
|
+
if (slug) {
|
|
720
|
+
if (!slugTools.has(slug)) slugTools.set(slug, []);
|
|
721
|
+
slugTools.get(slug).push(toolName);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
if (slugTools.size === 0) return void 0;
|
|
725
|
+
if (slugTools.size === 1) return slugTools.keys().next().value;
|
|
726
|
+
const queryTerms = query.toLowerCase().split(/\s+/).filter((t) => t.length > 0);
|
|
727
|
+
if (queryTerms.length === 0) return slugTools.keys().next().value;
|
|
728
|
+
let bestSlug;
|
|
729
|
+
let bestScore = 0;
|
|
730
|
+
for (const [slug, tools] of slugTools) {
|
|
731
|
+
let score = 0;
|
|
732
|
+
const slugLower = slug.toLowerCase();
|
|
733
|
+
for (const term of queryTerms) {
|
|
734
|
+
if (slugLower.includes(term)) score += 10;
|
|
735
|
+
}
|
|
736
|
+
for (const toolName of tools) {
|
|
737
|
+
const toolLower = toolName.toLowerCase();
|
|
738
|
+
for (const term of queryTerms) {
|
|
739
|
+
if (toolLower.includes(term)) score += 1;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
if (score > bestScore) {
|
|
743
|
+
bestScore = score;
|
|
744
|
+
bestSlug = slug;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
return bestSlug ?? slugTools.keys().next().value;
|
|
748
|
+
}
|
|
749
|
+
function extractPublisherSlug(workerType, capabilities, query) {
|
|
750
|
+
if (workerType !== "mcp_publisher") return void 0;
|
|
751
|
+
return extractGatewaySlug(capabilities, query);
|
|
752
|
+
}
|
|
753
|
+
function resolveSkills(classification, capabilities) {
|
|
754
|
+
return classification.relevant_skills.map((slug) => capabilities.installed_skills.find((s) => s.slug === slug)).filter((s) => s !== void 0);
|
|
755
|
+
}
|
|
756
|
+
function buildReason(classification, workerType, modelId) {
|
|
757
|
+
const modelName = humanizeModelId(modelId);
|
|
758
|
+
const taskDesc = humanizeTaskType(classification.task_type);
|
|
759
|
+
switch (workerType) {
|
|
760
|
+
case "local_agent":
|
|
761
|
+
return `Working with agent on ${taskDesc}`;
|
|
762
|
+
case "mcp_publisher":
|
|
763
|
+
return `Working with publisher on ${taskDesc}`;
|
|
764
|
+
case "chat_model":
|
|
765
|
+
default:
|
|
766
|
+
return `Working with ${modelName} on ${taskDesc}`;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
function isContextOverflowError(errorMessage) {
|
|
770
|
+
return errorMessage.includes("prompt is too long") || errorMessage.includes("context_length_exceeded") || errorMessage.includes("maximum context length");
|
|
771
|
+
}
|
|
772
|
+
function isReroutableError(errorMessage) {
|
|
773
|
+
if (isContextOverflowError(errorMessage)) return true;
|
|
774
|
+
if (errorMessage.includes("401") || errorMessage.includes("403") || errorMessage.includes("400") || errorMessage.includes("API key") || errorMessage.includes("Insufficient credits")) {
|
|
775
|
+
return false;
|
|
776
|
+
}
|
|
777
|
+
return REROUTABLE_STATUS_CODES.some(
|
|
778
|
+
(code) => errorMessage.includes(String(code))
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
function getLargeContextFallback(triedModels) {
|
|
782
|
+
return LARGE_CONTEXT_FALLBACK_MODELS.find((m) => !triedModels.includes(m));
|
|
783
|
+
}
|
|
784
|
+
function rerouteOnFailure(classification, triedModels, availableModels) {
|
|
785
|
+
const preferred = classification.task_type === "code_generation" || classification.complexity === "complex" || classification.complexity === "moderate" ? CODE_PREFERRED_MODELS : SIMPLE_PREFERRED_MODELS;
|
|
786
|
+
for (const model of preferred) {
|
|
787
|
+
if (!triedModels.includes(model) && availableModels.includes(model)) {
|
|
788
|
+
const reason = `Rerouted to ${humanizeModelId(model)} for ${humanizeTaskType(classification.task_type)}`;
|
|
789
|
+
return [model, reason];
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
for (const model of availableModels) {
|
|
793
|
+
if (!triedModels.includes(model)) {
|
|
794
|
+
const reason = `Rerouted to ${humanizeModelId(model)} (fallback)`;
|
|
795
|
+
return [model, reason];
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return void 0;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// src/services/orchestrator.ts
|
|
802
|
+
var PUBLISHER_SLUG2 = "seren-models";
|
|
803
|
+
var DEFAULT_GATEWAY_BASE = "https://api.serendb.com";
|
|
804
|
+
var REQUEST_TIMEOUT_MS2 = 6e5;
|
|
805
|
+
var activeSessions = /* @__PURE__ */ new Map();
|
|
806
|
+
function cancelOrchestration(conversationId) {
|
|
807
|
+
const controller = activeSessions.get(conversationId);
|
|
808
|
+
if (controller) {
|
|
809
|
+
controller.abort();
|
|
810
|
+
activeSessions.delete(conversationId);
|
|
811
|
+
console.log(`[Orchestrator] Cancelled orchestration for ${conversationId}`);
|
|
812
|
+
return true;
|
|
813
|
+
}
|
|
814
|
+
return false;
|
|
815
|
+
}
|
|
816
|
+
async function orchestrate(params) {
|
|
817
|
+
const {
|
|
818
|
+
conversationId,
|
|
819
|
+
prompt,
|
|
820
|
+
history,
|
|
821
|
+
capabilities,
|
|
822
|
+
images = [],
|
|
823
|
+
authToken
|
|
824
|
+
} = params;
|
|
825
|
+
const gatewayBase = params.gatewayBase ?? DEFAULT_GATEWAY_BASE;
|
|
826
|
+
console.log(
|
|
827
|
+
`[Orchestrator] Starting orchestration for conversation ${conversationId}`
|
|
828
|
+
);
|
|
829
|
+
const abortController = new AbortController();
|
|
830
|
+
activeSessions.set(conversationId, abortController);
|
|
831
|
+
try {
|
|
832
|
+
const modelForLimit = capabilities.selected_model || "anthropic/claude-sonnet-4";
|
|
833
|
+
const totalInputChars = prompt.length + history.reduce((acc, m) => acc + (m.content?.length ?? 0), 0) + images.length * 1e3;
|
|
834
|
+
if (needsRlm(totalInputChars, modelForLimit)) {
|
|
835
|
+
console.log(
|
|
836
|
+
"[Orchestrator] Input exceeds context threshold \u2014 activating RLM"
|
|
837
|
+
);
|
|
838
|
+
const [content, question] = splitContentAndQuestion(prompt);
|
|
839
|
+
const rlmAnswer = await processRlm(
|
|
840
|
+
question,
|
|
841
|
+
content,
|
|
842
|
+
modelForLimit,
|
|
843
|
+
gatewayBase,
|
|
844
|
+
authToken,
|
|
845
|
+
(event) => {
|
|
846
|
+
const workerEvent = event.type === "rlm_start" ? { type: "rlm_start", chunk_count: event.data.total } : {
|
|
847
|
+
type: "rlm_chunk_complete",
|
|
848
|
+
index: event.data.index,
|
|
849
|
+
total: event.data.total,
|
|
850
|
+
summary: event.data.summary ?? ""
|
|
851
|
+
};
|
|
852
|
+
emitOrchestratorEvent(conversationId, workerEvent);
|
|
853
|
+
}
|
|
854
|
+
);
|
|
855
|
+
const completeEvent = {
|
|
856
|
+
type: "complete",
|
|
857
|
+
final_content: rlmAnswer
|
|
858
|
+
};
|
|
859
|
+
emitOrchestratorEvent(conversationId, completeEvent);
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
const classification = classifyTask2(prompt, capabilities);
|
|
863
|
+
console.log(
|
|
864
|
+
`[Orchestrator] Classification: type=${classification.task_type}, complexity=${classification.complexity}`
|
|
865
|
+
);
|
|
866
|
+
const routing = route(classification, capabilities, prompt);
|
|
867
|
+
console.log(
|
|
868
|
+
`[Orchestrator] Routed to ${routing.worker_type} with model ${routing.model_id}`
|
|
869
|
+
);
|
|
870
|
+
await executeWithReroute({
|
|
871
|
+
conversationId,
|
|
872
|
+
prompt,
|
|
873
|
+
history,
|
|
874
|
+
capabilities,
|
|
875
|
+
images,
|
|
876
|
+
classification,
|
|
877
|
+
routing,
|
|
878
|
+
gatewayBase,
|
|
879
|
+
authToken,
|
|
880
|
+
abortSignal: abortController.signal
|
|
881
|
+
});
|
|
882
|
+
} catch (err) {
|
|
883
|
+
if (abortController.signal.aborted) {
|
|
884
|
+
console.log(
|
|
885
|
+
`[Orchestrator] Orchestration cancelled for ${conversationId}`
|
|
886
|
+
);
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
890
|
+
console.error(`[Orchestrator] Error: ${message}`);
|
|
891
|
+
const errorEvent = { type: "error", message };
|
|
892
|
+
emitOrchestratorEvent(conversationId, errorEvent);
|
|
893
|
+
} finally {
|
|
894
|
+
activeSessions.delete(conversationId);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
async function executeWithReroute(params) {
|
|
898
|
+
const {
|
|
899
|
+
conversationId,
|
|
900
|
+
prompt,
|
|
901
|
+
history,
|
|
902
|
+
capabilities,
|
|
903
|
+
images,
|
|
904
|
+
classification,
|
|
905
|
+
gatewayBase,
|
|
906
|
+
authToken,
|
|
907
|
+
abortSignal
|
|
908
|
+
} = params;
|
|
909
|
+
let routing = { ...params.routing };
|
|
910
|
+
const triedModels = [routing.model_id];
|
|
911
|
+
let rerouteCount = 0;
|
|
912
|
+
const userExplicitlySelected = Boolean(capabilities.selected_model);
|
|
913
|
+
while (true) {
|
|
914
|
+
if (abortSignal.aborted) {
|
|
915
|
+
console.log(
|
|
916
|
+
`[Orchestrator] Cancelled before execution for ${conversationId}`
|
|
917
|
+
);
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
const skillContent = await loadSkillContent(routing.selected_skills);
|
|
921
|
+
const relevantTools = selectRelevantTools(
|
|
922
|
+
prompt,
|
|
923
|
+
capabilities.tool_definitions
|
|
924
|
+
);
|
|
925
|
+
const transition = {
|
|
926
|
+
conversation_id: conversationId,
|
|
927
|
+
model_name: routing.model_id,
|
|
928
|
+
task_description: routing.reason
|
|
929
|
+
};
|
|
930
|
+
emit("orchestrator://transition", transition);
|
|
931
|
+
const messages = buildMessages(
|
|
932
|
+
prompt,
|
|
933
|
+
history,
|
|
934
|
+
skillContent,
|
|
935
|
+
images,
|
|
936
|
+
routing
|
|
937
|
+
);
|
|
938
|
+
try {
|
|
939
|
+
await streamChatCompletion({
|
|
940
|
+
conversationId,
|
|
941
|
+
messages,
|
|
942
|
+
model: routing.model_id,
|
|
943
|
+
tools: relevantTools,
|
|
944
|
+
gatewayBase,
|
|
945
|
+
authToken,
|
|
946
|
+
abortSignal,
|
|
947
|
+
reasoningEffort: routing.reasoning_effort,
|
|
948
|
+
publisherSlug: routing.publisher_slug
|
|
949
|
+
});
|
|
950
|
+
console.log(
|
|
951
|
+
`[Orchestrator] Completed orchestration for conversation ${conversationId}`
|
|
952
|
+
);
|
|
953
|
+
return;
|
|
954
|
+
} catch (err) {
|
|
955
|
+
if (abortSignal.aborted) return;
|
|
956
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
957
|
+
console.error(`[Orchestrator] Worker error: ${errorMessage}`);
|
|
958
|
+
if (!isReroutableError(errorMessage)) {
|
|
959
|
+
const errorEvent = {
|
|
960
|
+
type: "error",
|
|
961
|
+
message: errorMessage
|
|
962
|
+
};
|
|
963
|
+
emitOrchestratorEvent(conversationId, errorEvent);
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
if (rerouteCount >= MAX_REROUTE_ATTEMPTS) {
|
|
967
|
+
console.warn(
|
|
968
|
+
`[Orchestrator] Max reroute attempts (${MAX_REROUTE_ATTEMPTS}) exhausted`
|
|
969
|
+
);
|
|
970
|
+
const errorEvent = {
|
|
971
|
+
type: "error",
|
|
972
|
+
message: errorMessage
|
|
973
|
+
};
|
|
974
|
+
emitOrchestratorEvent(conversationId, errorEvent);
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
if (isContextOverflowError(errorMessage)) {
|
|
978
|
+
const fallback = getLargeContextFallback(triedModels);
|
|
979
|
+
if (fallback) {
|
|
980
|
+
const fromModel2 = routing.model_id;
|
|
981
|
+
console.log(
|
|
982
|
+
`[Orchestrator] Context overflow on ${fromModel2}, falling back to ${fallback}`
|
|
983
|
+
);
|
|
984
|
+
const rerouteEvent2 = {
|
|
985
|
+
type: "reroute",
|
|
986
|
+
from_model: fromModel2,
|
|
987
|
+
to_model: fallback,
|
|
988
|
+
reason: "Switched to larger context model \u2014 conversation exceeded model limit"
|
|
989
|
+
};
|
|
990
|
+
emitOrchestratorEvent(conversationId, rerouteEvent2);
|
|
991
|
+
routing = { ...routing, model_id: fallback };
|
|
992
|
+
triedModels.push(fallback);
|
|
993
|
+
rerouteCount++;
|
|
994
|
+
continue;
|
|
995
|
+
}
|
|
996
|
+
console.warn(
|
|
997
|
+
"[Orchestrator] Context overflow but all large-context fallbacks exhausted"
|
|
998
|
+
);
|
|
999
|
+
const errorEvent = {
|
|
1000
|
+
type: "error",
|
|
1001
|
+
message: errorMessage
|
|
1002
|
+
};
|
|
1003
|
+
emitOrchestratorEvent(conversationId, errorEvent);
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
if (userExplicitlySelected) {
|
|
1007
|
+
const errorEvent = {
|
|
1008
|
+
type: "error",
|
|
1009
|
+
message: errorMessage
|
|
1010
|
+
};
|
|
1011
|
+
emitOrchestratorEvent(conversationId, errorEvent);
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
const fallbackResult = rerouteOnFailure(
|
|
1015
|
+
classification,
|
|
1016
|
+
triedModels,
|
|
1017
|
+
capabilities.available_models
|
|
1018
|
+
);
|
|
1019
|
+
if (!fallbackResult) {
|
|
1020
|
+
console.warn("[Orchestrator] No fallback models available for reroute");
|
|
1021
|
+
const errorEvent = {
|
|
1022
|
+
type: "error",
|
|
1023
|
+
message: errorMessage
|
|
1024
|
+
};
|
|
1025
|
+
emitOrchestratorEvent(conversationId, errorEvent);
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
const [fallbackModel, reason] = fallbackResult;
|
|
1029
|
+
const fromModel = routing.model_id;
|
|
1030
|
+
console.log(
|
|
1031
|
+
`[Orchestrator] Rerouting from ${fromModel} to ${fallbackModel}: ${reason}`
|
|
1032
|
+
);
|
|
1033
|
+
const rerouteEvent = {
|
|
1034
|
+
type: "reroute",
|
|
1035
|
+
from_model: fromModel,
|
|
1036
|
+
to_model: fallbackModel,
|
|
1037
|
+
reason
|
|
1038
|
+
};
|
|
1039
|
+
emitOrchestratorEvent(conversationId, rerouteEvent);
|
|
1040
|
+
routing = { ...routing, model_id: fallbackModel };
|
|
1041
|
+
triedModels.push(fallbackModel);
|
|
1042
|
+
rerouteCount++;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
async function streamChatCompletion(params) {
|
|
1047
|
+
const {
|
|
1048
|
+
conversationId,
|
|
1049
|
+
messages,
|
|
1050
|
+
model,
|
|
1051
|
+
tools,
|
|
1052
|
+
gatewayBase,
|
|
1053
|
+
authToken,
|
|
1054
|
+
abortSignal,
|
|
1055
|
+
reasoningEffort
|
|
1056
|
+
} = params;
|
|
1057
|
+
const publisherSlug = params.publisherSlug ?? PUBLISHER_SLUG2;
|
|
1058
|
+
const url = `${gatewayBase}/publishers/${publisherSlug}/chat/completions`;
|
|
1059
|
+
const body = {
|
|
1060
|
+
model,
|
|
1061
|
+
messages,
|
|
1062
|
+
stream: true
|
|
1063
|
+
};
|
|
1064
|
+
if (tools.length > 0) {
|
|
1065
|
+
body.tools = tools;
|
|
1066
|
+
}
|
|
1067
|
+
if (reasoningEffort) {
|
|
1068
|
+
body.reasoning_effort = reasoningEffort;
|
|
1069
|
+
}
|
|
1070
|
+
const timeoutId = setTimeout(() => {
|
|
1071
|
+
if (!abortSignal.aborted) {
|
|
1072
|
+
const controller = activeSessions.get(conversationId);
|
|
1073
|
+
controller?.abort();
|
|
1074
|
+
}
|
|
1075
|
+
}, REQUEST_TIMEOUT_MS2);
|
|
1076
|
+
let response;
|
|
1077
|
+
try {
|
|
1078
|
+
response = await fetch(url, {
|
|
1079
|
+
method: "POST",
|
|
1080
|
+
headers: {
|
|
1081
|
+
"Content-Type": "application/json",
|
|
1082
|
+
Authorization: `Bearer ${authToken}`,
|
|
1083
|
+
Accept: "text/event-stream"
|
|
1084
|
+
},
|
|
1085
|
+
body: JSON.stringify(body),
|
|
1086
|
+
signal: abortSignal
|
|
1087
|
+
});
|
|
1088
|
+
} catch (err) {
|
|
1089
|
+
clearTimeout(timeoutId);
|
|
1090
|
+
if (abortSignal.aborted) return;
|
|
1091
|
+
throw err;
|
|
1092
|
+
}
|
|
1093
|
+
if (!response.ok) {
|
|
1094
|
+
clearTimeout(timeoutId);
|
|
1095
|
+
const text = await response.text().catch(() => "");
|
|
1096
|
+
throw new Error(`Gateway HTTP ${response.status}: ${text}`);
|
|
1097
|
+
}
|
|
1098
|
+
if (!response.body) {
|
|
1099
|
+
clearTimeout(timeoutId);
|
|
1100
|
+
throw new Error("No response body from Gateway");
|
|
1101
|
+
}
|
|
1102
|
+
try {
|
|
1103
|
+
await processSSEStream(conversationId, response.body, abortSignal);
|
|
1104
|
+
} finally {
|
|
1105
|
+
clearTimeout(timeoutId);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
async function processSSEStream(conversationId, body, abortSignal) {
|
|
1109
|
+
const reader = body.getReader();
|
|
1110
|
+
const decoder = new TextDecoder();
|
|
1111
|
+
let buffer = "";
|
|
1112
|
+
let fullContent = "";
|
|
1113
|
+
let fullThinking = "";
|
|
1114
|
+
let totalCost = 0;
|
|
1115
|
+
try {
|
|
1116
|
+
while (true) {
|
|
1117
|
+
if (abortSignal.aborted) return;
|
|
1118
|
+
const { done, value } = await reader.read();
|
|
1119
|
+
if (done) break;
|
|
1120
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1121
|
+
const lines = buffer.split("\n");
|
|
1122
|
+
buffer = lines.pop() ?? "";
|
|
1123
|
+
for (const line of lines) {
|
|
1124
|
+
if (!line.startsWith("data: ")) continue;
|
|
1125
|
+
const data = line.slice(6).trim();
|
|
1126
|
+
if (data === "[DONE]") continue;
|
|
1127
|
+
let chunk;
|
|
1128
|
+
try {
|
|
1129
|
+
chunk = JSON.parse(data);
|
|
1130
|
+
} catch {
|
|
1131
|
+
continue;
|
|
1132
|
+
}
|
|
1133
|
+
if (typeof chunk.cost === "number") {
|
|
1134
|
+
totalCost = chunk.cost;
|
|
1135
|
+
}
|
|
1136
|
+
const choices = chunk.choices;
|
|
1137
|
+
if (!choices || choices.length === 0) continue;
|
|
1138
|
+
const delta = choices[0].delta;
|
|
1139
|
+
if (!delta) continue;
|
|
1140
|
+
if (typeof delta.content === "string" && delta.content) {
|
|
1141
|
+
fullContent += delta.content;
|
|
1142
|
+
emitOrchestratorEvent(conversationId, {
|
|
1143
|
+
type: "content",
|
|
1144
|
+
text: delta.content
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
if (typeof delta.thinking === "string" && delta.thinking) {
|
|
1148
|
+
fullThinking += delta.thinking;
|
|
1149
|
+
emitOrchestratorEvent(conversationId, {
|
|
1150
|
+
type: "thinking",
|
|
1151
|
+
text: delta.thinking
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
const toolCalls = delta.tool_calls;
|
|
1155
|
+
if (toolCalls) {
|
|
1156
|
+
for (const tc of toolCalls) {
|
|
1157
|
+
const fn = tc.function;
|
|
1158
|
+
if (fn?.name) {
|
|
1159
|
+
emitOrchestratorEvent(conversationId, {
|
|
1160
|
+
type: "tool_call",
|
|
1161
|
+
tool_call_id: tc.id ?? "",
|
|
1162
|
+
name: fn.name,
|
|
1163
|
+
arguments: fn.arguments ?? "{}",
|
|
1164
|
+
title: fn.name ?? ""
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
} finally {
|
|
1172
|
+
reader.releaseLock();
|
|
1173
|
+
}
|
|
1174
|
+
const completeEvent = {
|
|
1175
|
+
type: "complete",
|
|
1176
|
+
final_content: fullContent,
|
|
1177
|
+
thinking: fullThinking || void 0,
|
|
1178
|
+
cost: totalCost || void 0
|
|
1179
|
+
};
|
|
1180
|
+
emitOrchestratorEvent(conversationId, completeEvent);
|
|
1181
|
+
}
|
|
1182
|
+
function buildMessages(prompt, history, skillContent, images, routing) {
|
|
1183
|
+
const messages = [];
|
|
1184
|
+
const systemParts = ["You are a helpful AI assistant."];
|
|
1185
|
+
if (skillContent) {
|
|
1186
|
+
systemParts.push(
|
|
1187
|
+
"",
|
|
1188
|
+
"## Relevant Skills",
|
|
1189
|
+
"",
|
|
1190
|
+
skillContent
|
|
1191
|
+
);
|
|
1192
|
+
}
|
|
1193
|
+
messages.push({ role: "system", content: systemParts.join("\n") });
|
|
1194
|
+
for (const msg of history) {
|
|
1195
|
+
messages.push({ role: msg.role, content: msg.content });
|
|
1196
|
+
}
|
|
1197
|
+
if (images.length > 0) {
|
|
1198
|
+
const contentParts = [
|
|
1199
|
+
{ type: "text", text: prompt }
|
|
1200
|
+
];
|
|
1201
|
+
for (const img of images) {
|
|
1202
|
+
contentParts.push({
|
|
1203
|
+
type: "image_url",
|
|
1204
|
+
image_url: {
|
|
1205
|
+
url: `data:${img.mime_type};base64,${img.base64}`
|
|
1206
|
+
}
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
messages.push({ role: "user", content: contentParts });
|
|
1210
|
+
} else {
|
|
1211
|
+
messages.push({ role: "user", content: prompt });
|
|
1212
|
+
}
|
|
1213
|
+
return messages;
|
|
1214
|
+
}
|
|
1215
|
+
async function loadSkillContent(skills) {
|
|
1216
|
+
if (skills.length === 0) return "";
|
|
1217
|
+
const parts = [];
|
|
1218
|
+
for (const skill of skills) {
|
|
1219
|
+
try {
|
|
1220
|
+
const skillPath = join(skill.path, "SKILL.md");
|
|
1221
|
+
const content = await readFile(skillPath, "utf-8");
|
|
1222
|
+
parts.push(`### ${skill.name}
|
|
1223
|
+
|
|
1224
|
+
${content}`);
|
|
1225
|
+
} catch (err) {
|
|
1226
|
+
console.warn(
|
|
1227
|
+
`[Orchestrator] Failed to load skill ${skill.slug}: ${err instanceof Error ? err.message : err}`
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
return parts.join("\n\n---\n\n");
|
|
1232
|
+
}
|
|
1233
|
+
function classifyTask2(prompt, capabilities) {
|
|
1234
|
+
const lower = prompt.toLowerCase();
|
|
1235
|
+
let taskType = "general";
|
|
1236
|
+
let requiresTools = false;
|
|
1237
|
+
let requiresFileSystem = false;
|
|
1238
|
+
const codeSignals = [
|
|
1239
|
+
"write code",
|
|
1240
|
+
"create a function",
|
|
1241
|
+
"implement",
|
|
1242
|
+
"build a",
|
|
1243
|
+
"code",
|
|
1244
|
+
"program",
|
|
1245
|
+
"script",
|
|
1246
|
+
"refactor",
|
|
1247
|
+
"debug",
|
|
1248
|
+
"fix the bug",
|
|
1249
|
+
"add a feature",
|
|
1250
|
+
"create a file",
|
|
1251
|
+
"write a file",
|
|
1252
|
+
"edit the file",
|
|
1253
|
+
"modify the code"
|
|
1254
|
+
];
|
|
1255
|
+
if (codeSignals.some((s) => lower.includes(s))) {
|
|
1256
|
+
taskType = "code_generation";
|
|
1257
|
+
requiresFileSystem = true;
|
|
1258
|
+
}
|
|
1259
|
+
const toolSignals = [
|
|
1260
|
+
"search the web",
|
|
1261
|
+
"browse",
|
|
1262
|
+
"crawl",
|
|
1263
|
+
"scrape",
|
|
1264
|
+
"fetch",
|
|
1265
|
+
"look up",
|
|
1266
|
+
"find online",
|
|
1267
|
+
"perplexity",
|
|
1268
|
+
"firecrawl"
|
|
1269
|
+
];
|
|
1270
|
+
if (toolSignals.some((s) => lower.includes(s))) {
|
|
1271
|
+
requiresTools = true;
|
|
1272
|
+
if (taskType === "general") taskType = "tool_use";
|
|
1273
|
+
}
|
|
1274
|
+
let complexity = "simple";
|
|
1275
|
+
const wordCount = prompt.split(/\s+/).length;
|
|
1276
|
+
if (wordCount > 200 || lower.includes("step by step") || lower.includes("detailed")) {
|
|
1277
|
+
complexity = "complex";
|
|
1278
|
+
} else if (wordCount > 50) {
|
|
1279
|
+
complexity = "moderate";
|
|
1280
|
+
}
|
|
1281
|
+
const relevantSkills = [];
|
|
1282
|
+
for (const skill of capabilities.installed_skills) {
|
|
1283
|
+
const skillText = `${skill.name} ${skill.description} ${skill.tags.join(" ")}`.toLowerCase();
|
|
1284
|
+
const promptTerms = lower.split(/\s+/).filter((t) => t.length > 3);
|
|
1285
|
+
const overlap = promptTerms.filter((t) => skillText.includes(t)).length;
|
|
1286
|
+
if (overlap >= 2) {
|
|
1287
|
+
relevantSkills.push(skill.slug);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
return {
|
|
1291
|
+
task_type: taskType,
|
|
1292
|
+
requires_tools: requiresTools,
|
|
1293
|
+
requires_file_system: requiresFileSystem,
|
|
1294
|
+
complexity,
|
|
1295
|
+
relevant_skills: relevantSkills
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
function emitOrchestratorEvent(conversationId, workerEvent, subtaskId) {
|
|
1299
|
+
const event = {
|
|
1300
|
+
conversation_id: conversationId,
|
|
1301
|
+
worker_event: workerEvent,
|
|
1302
|
+
subtask_id: subtaskId
|
|
1303
|
+
};
|
|
1304
|
+
emit("orchestrator://event", event);
|
|
1305
|
+
}
|
|
1306
|
+
|
|
204
1307
|
// src/handlers/acp.ts
|
|
205
1308
|
import * as acp from "@agentclientprotocol/sdk";
|
|
206
|
-
import { spawn, execFile } from "child_process";
|
|
1309
|
+
import { spawn, spawnSync, execFile } from "child_process";
|
|
207
1310
|
import { Readable, Writable } from "stream";
|
|
208
|
-
import { existsSync } from "fs";
|
|
209
|
-
import { readFile, writeFile } from "fs/promises";
|
|
1311
|
+
import { existsSync, readdirSync } from "fs";
|
|
1312
|
+
import { readFile as readFile2, writeFile } from "fs/promises";
|
|
210
1313
|
import { randomUUID } from "crypto";
|
|
211
|
-
import { resolve } from "path";
|
|
212
|
-
import { platform } from "os";
|
|
1314
|
+
import path, { resolve } from "path";
|
|
1315
|
+
import { platform, homedir } from "os";
|
|
213
1316
|
function isAuthError(message) {
|
|
214
1317
|
const lower = message.toLowerCase();
|
|
215
|
-
return lower.includes("invalid api key") || lower.includes("authentication required") || lower.includes("auth required") || lower.includes("please run /login") || lower.includes("authrequired");
|
|
1318
|
+
return lower.includes("invalid api key") || lower.includes("authentication required") || lower.includes("auth required") || lower.includes("please run /login") || lower.includes("authrequired") || lower.includes("failed to authenticate") || lower.includes("login required") || lower.includes("not logged in") || lower.includes("please login again") || lower.includes("please sign in") || lower.includes("session expired") || lower.includes("does not have access") || lower.includes("re-authenticate");
|
|
216
1319
|
}
|
|
217
1320
|
function authErrorMessage(agentType) {
|
|
218
1321
|
if (agentType === "claude-code") {
|
|
219
1322
|
return "Claude Code login required. A terminal window has been opened \u2014 please complete the login there, then try starting the agent again.";
|
|
220
1323
|
}
|
|
1324
|
+
if (agentType === "codex") {
|
|
1325
|
+
return "Codex login required. A terminal window has been opened \u2014 please complete the login there, then try starting the agent again.";
|
|
1326
|
+
}
|
|
221
1327
|
return "Agent authentication required. Please log in via the agent CLI first.";
|
|
222
1328
|
}
|
|
223
|
-
function
|
|
224
|
-
const
|
|
1329
|
+
function launchLoginCommand(command) {
|
|
1330
|
+
const loginCommand = `${command} login`;
|
|
1331
|
+
const currentPlatform = platform();
|
|
225
1332
|
try {
|
|
226
|
-
if (
|
|
1333
|
+
if (currentPlatform === "darwin") {
|
|
227
1334
|
spawn("osascript", [
|
|
228
1335
|
"-e",
|
|
229
|
-
|
|
1336
|
+
`tell application "Terminal" to do script "${loginCommand}"`,
|
|
230
1337
|
"-e",
|
|
231
1338
|
'tell application "Terminal" to activate'
|
|
232
1339
|
], { detached: true, stdio: "ignore" }).unref();
|
|
233
|
-
} else if (
|
|
234
|
-
spawn("cmd", ["/c", "start", "cmd", "/c",
|
|
1340
|
+
} else if (currentPlatform === "win32") {
|
|
1341
|
+
spawn("cmd", ["/c", "start", "cmd", "/c", loginCommand], {
|
|
235
1342
|
detached: true,
|
|
236
1343
|
stdio: "ignore"
|
|
237
1344
|
}).unref();
|
|
238
1345
|
} else {
|
|
239
|
-
spawn("x-terminal-emulator", ["-e",
|
|
1346
|
+
spawn("x-terminal-emulator", ["-e", command, "login"], {
|
|
240
1347
|
detached: true,
|
|
241
1348
|
stdio: "ignore"
|
|
242
1349
|
}).unref();
|
|
@@ -244,6 +1351,160 @@ function launchClaudeLogin() {
|
|
|
244
1351
|
} catch {
|
|
245
1352
|
}
|
|
246
1353
|
}
|
|
1354
|
+
function launchClaudeLogin() {
|
|
1355
|
+
launchLoginCommand("claude");
|
|
1356
|
+
}
|
|
1357
|
+
function killChildTree(child) {
|
|
1358
|
+
if (platform() === "win32" && child.pid !== void 0) {
|
|
1359
|
+
try {
|
|
1360
|
+
spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
|
|
1361
|
+
stdio: "ignore"
|
|
1362
|
+
});
|
|
1363
|
+
return;
|
|
1364
|
+
} catch {
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
try {
|
|
1368
|
+
child.kill();
|
|
1369
|
+
} catch {
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
function buildExtendedPath() {
|
|
1373
|
+
const sep = platform() === "win32" ? ";" : ":";
|
|
1374
|
+
const base2 = process.env.PATH ?? "";
|
|
1375
|
+
if (platform() === "win32") return base2;
|
|
1376
|
+
const home3 = homedir();
|
|
1377
|
+
const extra = [
|
|
1378
|
+
// nvm (most common)
|
|
1379
|
+
path.join(home3, ".nvm", "versions", "node"),
|
|
1380
|
+
// fnm
|
|
1381
|
+
path.join(home3, ".local", "share", "fnm", "aliases", "default", "bin"),
|
|
1382
|
+
path.join(home3, "Library", "Application Support", "fnm", "aliases", "default", "bin"),
|
|
1383
|
+
// Volta
|
|
1384
|
+
path.join(home3, ".volta", "bin"),
|
|
1385
|
+
// Homebrew (Apple Silicon + Intel)
|
|
1386
|
+
"/opt/homebrew/bin",
|
|
1387
|
+
"/usr/local/bin",
|
|
1388
|
+
// Common Linux paths
|
|
1389
|
+
"/usr/bin"
|
|
1390
|
+
];
|
|
1391
|
+
const nvmDir = extra[0];
|
|
1392
|
+
if (existsSync(nvmDir)) {
|
|
1393
|
+
try {
|
|
1394
|
+
const versions = readdirSync(nvmDir).sort().reverse();
|
|
1395
|
+
for (const ver of versions) {
|
|
1396
|
+
const binDir = path.join(nvmDir, ver, "bin");
|
|
1397
|
+
if (existsSync(binDir)) {
|
|
1398
|
+
extra[0] = binDir;
|
|
1399
|
+
break;
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
} catch {
|
|
1403
|
+
extra[0] = "";
|
|
1404
|
+
}
|
|
1405
|
+
} else {
|
|
1406
|
+
extra[0] = "";
|
|
1407
|
+
}
|
|
1408
|
+
const additions = extra.filter((p) => p && !base2.includes(p));
|
|
1409
|
+
return additions.length > 0 ? `${additions.join(sep)}${sep}${base2}` : base2;
|
|
1410
|
+
}
|
|
1411
|
+
function resolveNpmCliScript() {
|
|
1412
|
+
const nodeDir = path.dirname(process.execPath);
|
|
1413
|
+
if (platform() === "win32") {
|
|
1414
|
+
const candidate = path.join(nodeDir, "node_modules", "npm", "bin", "npm-cli.js");
|
|
1415
|
+
if (existsSync(candidate)) {
|
|
1416
|
+
return candidate;
|
|
1417
|
+
}
|
|
1418
|
+
} else {
|
|
1419
|
+
const prefix = path.dirname(nodeDir);
|
|
1420
|
+
const candidate = path.join(prefix, "lib", "node_modules", "npm", "bin", "npm-cli.js");
|
|
1421
|
+
if (existsSync(candidate)) {
|
|
1422
|
+
return candidate;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
return null;
|
|
1426
|
+
}
|
|
1427
|
+
async function ensureGlobalNpmPackage({
|
|
1428
|
+
command,
|
|
1429
|
+
packageName,
|
|
1430
|
+
label
|
|
1431
|
+
}) {
|
|
1432
|
+
if (await isCommandAvailable(command)) {
|
|
1433
|
+
return command;
|
|
1434
|
+
}
|
|
1435
|
+
emit("provider://cli-install-progress", {
|
|
1436
|
+
stage: "installing",
|
|
1437
|
+
message: `Installing ${label} CLI...`
|
|
1438
|
+
});
|
|
1439
|
+
const npmCliScript = resolveNpmCliScript();
|
|
1440
|
+
if (npmCliScript) {
|
|
1441
|
+
await new Promise((resolvePromise, rejectPromise) => {
|
|
1442
|
+
execFile(
|
|
1443
|
+
process.execPath,
|
|
1444
|
+
[npmCliScript, "install", "-g", packageName],
|
|
1445
|
+
(error, stdout, stderr) => {
|
|
1446
|
+
if (error) {
|
|
1447
|
+
rejectPromise(new Error(stderr || error.message));
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
resolvePromise(stdout.trim());
|
|
1451
|
+
}
|
|
1452
|
+
);
|
|
1453
|
+
});
|
|
1454
|
+
} else {
|
|
1455
|
+
const npmCommand = platform() === "win32" ? "npm.cmd" : "npm";
|
|
1456
|
+
await new Promise((resolvePromise, rejectPromise) => {
|
|
1457
|
+
execFile(
|
|
1458
|
+
npmCommand,
|
|
1459
|
+
["install", "-g", packageName],
|
|
1460
|
+
(error, stdout, stderr) => {
|
|
1461
|
+
if (error) {
|
|
1462
|
+
rejectPromise(new Error(stderr || error.message));
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
resolvePromise(stdout.trim());
|
|
1466
|
+
}
|
|
1467
|
+
);
|
|
1468
|
+
});
|
|
1469
|
+
}
|
|
1470
|
+
emit("provider://cli-install-progress", {
|
|
1471
|
+
stage: "complete",
|
|
1472
|
+
message: `${label} CLI installed successfully`
|
|
1473
|
+
});
|
|
1474
|
+
return command;
|
|
1475
|
+
}
|
|
1476
|
+
async function ensureClaudeCodeViaNativeInstaller() {
|
|
1477
|
+
if (await isCommandAvailable("claude")) {
|
|
1478
|
+
return "claude";
|
|
1479
|
+
}
|
|
1480
|
+
emit("provider://cli-install-progress", {
|
|
1481
|
+
stage: "installing",
|
|
1482
|
+
message: "Installing Claude Code CLI via official installer..."
|
|
1483
|
+
});
|
|
1484
|
+
await new Promise((resolvePromise, rejectPromise) => {
|
|
1485
|
+
let cmd;
|
|
1486
|
+
let args;
|
|
1487
|
+
if (platform() === "win32") {
|
|
1488
|
+
cmd = "powershell";
|
|
1489
|
+
args = ["-NoProfile", "-Command", "irm https://claude.ai/install.ps1 | iex"];
|
|
1490
|
+
} else {
|
|
1491
|
+
cmd = "bash";
|
|
1492
|
+
args = ["-c", "curl -fsSL https://claude.ai/install.sh | bash"];
|
|
1493
|
+
}
|
|
1494
|
+
execFile(cmd, args, { timeout: 12e4 }, (error, stdout, stderr) => {
|
|
1495
|
+
if (error) {
|
|
1496
|
+
rejectPromise(new Error(stderr || error.message));
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
resolvePromise(stdout.trim());
|
|
1500
|
+
});
|
|
1501
|
+
});
|
|
1502
|
+
emit("provider://cli-install-progress", {
|
|
1503
|
+
stage: "complete",
|
|
1504
|
+
message: "Claude Code CLI installed successfully"
|
|
1505
|
+
});
|
|
1506
|
+
return "claude";
|
|
1507
|
+
}
|
|
247
1508
|
var sessions = /* @__PURE__ */ new Map();
|
|
248
1509
|
function createClient(sessionId) {
|
|
249
1510
|
return {
|
|
@@ -257,8 +1518,8 @@ function createClient(sessionId) {
|
|
|
257
1518
|
toolCall: params.toolCall,
|
|
258
1519
|
options: params.options
|
|
259
1520
|
});
|
|
260
|
-
const optionId = await new Promise((
|
|
261
|
-
session.pendingPermissions.set(requestId,
|
|
1521
|
+
const optionId = await new Promise((resolve5, reject) => {
|
|
1522
|
+
session.pendingPermissions.set(requestId, resolve5);
|
|
262
1523
|
setTimeout(() => {
|
|
263
1524
|
session.pendingPermissions.delete(requestId);
|
|
264
1525
|
reject(new Error("Permission request timed out"));
|
|
@@ -272,7 +1533,7 @@ function createClient(sessionId) {
|
|
|
272
1533
|
handleSessionUpdate(sessionId, params);
|
|
273
1534
|
},
|
|
274
1535
|
async readTextFile(params) {
|
|
275
|
-
const content = await
|
|
1536
|
+
const content = await readFile2(params.path, "utf-8");
|
|
276
1537
|
return { content };
|
|
277
1538
|
},
|
|
278
1539
|
async writeTextFile(params) {
|
|
@@ -280,7 +1541,7 @@ function createClient(sessionId) {
|
|
|
280
1541
|
if (!session) throw new Error("Session not found");
|
|
281
1542
|
let oldText = "";
|
|
282
1543
|
try {
|
|
283
|
-
oldText = await
|
|
1544
|
+
oldText = await readFile2(params.path, "utf-8");
|
|
284
1545
|
} catch {
|
|
285
1546
|
}
|
|
286
1547
|
const proposalId = randomUUID();
|
|
@@ -291,8 +1552,8 @@ function createClient(sessionId) {
|
|
|
291
1552
|
oldText,
|
|
292
1553
|
newText: params.content
|
|
293
1554
|
});
|
|
294
|
-
const accepted = await new Promise((
|
|
295
|
-
session.pendingDiffProposals.set(proposalId,
|
|
1555
|
+
const accepted = await new Promise((resolve5, reject) => {
|
|
1556
|
+
session.pendingDiffProposals.set(proposalId, resolve5);
|
|
296
1557
|
setTimeout(() => {
|
|
297
1558
|
session.pendingDiffProposals.delete(proposalId);
|
|
298
1559
|
reject(new Error("Diff proposal timed out"));
|
|
@@ -381,21 +1642,21 @@ function findAgentCommand(agentType) {
|
|
|
381
1642
|
function findAgentBinary(binBase) {
|
|
382
1643
|
const ext = platform() === "win32" ? ".exe" : "";
|
|
383
1644
|
const binName = `${binBase}${ext}`;
|
|
384
|
-
const
|
|
1645
|
+
const home3 = process.env.HOME ?? "~";
|
|
385
1646
|
const candidates = [
|
|
386
1647
|
// 1. runtime/bin/ (bundled with seren-local — dist/ is one level below bin/)
|
|
387
1648
|
resolve(import.meta.dirname, "../bin", binName),
|
|
388
1649
|
// 2. ~/.seren-local/bin/ (user install location)
|
|
389
|
-
resolve(
|
|
1650
|
+
resolve(home3, ".seren-local/bin", binName),
|
|
390
1651
|
// 3. Seren Desktop embedded-runtime (development)
|
|
391
|
-
resolve(
|
|
1652
|
+
resolve(home3, "Projects/Seren_Projects/seren-desktop/src-tauri/embedded-runtime/bin", binName)
|
|
392
1653
|
];
|
|
393
1654
|
if (binBase === "seren-acp-claude") {
|
|
394
1655
|
const legacyName = `acp_agent${ext}`;
|
|
395
1656
|
candidates.push(
|
|
396
1657
|
resolve(import.meta.dirname, "../bin", legacyName),
|
|
397
|
-
resolve(
|
|
398
|
-
resolve(
|
|
1658
|
+
resolve(home3, ".seren-local/bin", legacyName),
|
|
1659
|
+
resolve(home3, "Projects/Seren_Projects/seren-desktop/src-tauri/embedded-runtime/bin", legacyName)
|
|
399
1660
|
);
|
|
400
1661
|
}
|
|
401
1662
|
for (const candidate of candidates) {
|
|
@@ -411,8 +1672,8 @@ ${candidates.map((p) => ` - ${p}`).join("\n")}`
|
|
|
411
1672
|
}
|
|
412
1673
|
async function isCommandAvailable(command) {
|
|
413
1674
|
const which = platform() === "win32" ? "where" : "which";
|
|
414
|
-
return new Promise((
|
|
415
|
-
execFile(which, [command], (err) =>
|
|
1675
|
+
return new Promise((resolve5) => {
|
|
1676
|
+
execFile(which, [command], (err) => resolve5(!err));
|
|
416
1677
|
});
|
|
417
1678
|
}
|
|
418
1679
|
async function acpSpawn(params) {
|
|
@@ -424,10 +1685,12 @@ async function acpSpawn(params) {
|
|
|
424
1685
|
if (sandboxMode) {
|
|
425
1686
|
args.push("--sandbox", sandboxMode);
|
|
426
1687
|
}
|
|
1688
|
+
const extendedPath = buildExtendedPath();
|
|
427
1689
|
const agentProcess = spawn(command, args, {
|
|
428
1690
|
cwd: resolvedCwd,
|
|
429
1691
|
stdio: ["pipe", "pipe", "pipe"],
|
|
430
|
-
env: { ...process.env }
|
|
1692
|
+
env: { ...process.env, PATH: extendedPath },
|
|
1693
|
+
shell: platform() === "win32"
|
|
431
1694
|
});
|
|
432
1695
|
if (!agentProcess.stdin || !agentProcess.stdout) {
|
|
433
1696
|
throw new Error("Failed to create agent process stdio");
|
|
@@ -462,6 +1725,18 @@ async function acpSpawn(params) {
|
|
|
462
1725
|
cancelling: false
|
|
463
1726
|
};
|
|
464
1727
|
sessions.set(sessionId, session);
|
|
1728
|
+
agentProcess.on("error", (spawnError) => {
|
|
1729
|
+
console.error(`[ACP] Spawn error: ${spawnError.message}`);
|
|
1730
|
+
sessions.delete(sessionId);
|
|
1731
|
+
emit("acp://error", {
|
|
1732
|
+
sessionId,
|
|
1733
|
+
error: spawnError.code === "ENOENT" ? `Agent binary not found at "${command}". Ensure the agent is installed.` : `Failed to start agent: ${spawnError.message}`
|
|
1734
|
+
});
|
|
1735
|
+
emit("acp://session-status", {
|
|
1736
|
+
sessionId,
|
|
1737
|
+
status: "terminated"
|
|
1738
|
+
});
|
|
1739
|
+
});
|
|
465
1740
|
agentProcess.on("exit", (code, signal) => {
|
|
466
1741
|
session.status = "terminated";
|
|
467
1742
|
emit("acp://session-status", {
|
|
@@ -500,7 +1775,13 @@ async function acpSpawn(params) {
|
|
|
500
1775
|
session.status = "error";
|
|
501
1776
|
const rawMessage = err instanceof Error ? err.message : JSON.stringify(err);
|
|
502
1777
|
const authDetected = isAuthError(rawMessage);
|
|
503
|
-
if (authDetected)
|
|
1778
|
+
if (authDetected) {
|
|
1779
|
+
if (agentType === "codex") {
|
|
1780
|
+
launchLoginCommand("codex");
|
|
1781
|
+
} else {
|
|
1782
|
+
launchClaudeLogin();
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
504
1785
|
const errorMsg = authDetected ? authErrorMessage(agentType) : `Failed to initialize agent: ${rawMessage}`;
|
|
505
1786
|
emit("acp://error", {
|
|
506
1787
|
sessionId,
|
|
@@ -577,7 +1858,7 @@ async function acpTerminate(params) {
|
|
|
577
1858
|
const { sessionId } = params;
|
|
578
1859
|
const session = sessions.get(sessionId);
|
|
579
1860
|
if (!session) throw new Error(`Session not found: ${sessionId}`);
|
|
580
|
-
session.process
|
|
1861
|
+
killChildTree(session.process);
|
|
581
1862
|
sessions.delete(sessionId);
|
|
582
1863
|
session.status = "terminated";
|
|
583
1864
|
emit("acp://session-status", { sessionId, status: "terminated" });
|
|
@@ -621,20 +1902,38 @@ async function acpRespondToDiffProposal(params) {
|
|
|
621
1902
|
}
|
|
622
1903
|
async function acpGetAvailableAgents() {
|
|
623
1904
|
const agents = [
|
|
624
|
-
{
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
1905
|
+
{
|
|
1906
|
+
type: "claude-code",
|
|
1907
|
+
name: "Claude Code",
|
|
1908
|
+
description: "Anthropic Claude Code via direct provider runtime",
|
|
1909
|
+
command: "claude"
|
|
1910
|
+
},
|
|
1911
|
+
{
|
|
1912
|
+
type: "codex",
|
|
1913
|
+
name: "Codex",
|
|
1914
|
+
description: "OpenAI Codex via direct App Server integration",
|
|
1915
|
+
command: "codex"
|
|
635
1916
|
}
|
|
636
|
-
|
|
637
|
-
|
|
1917
|
+
];
|
|
1918
|
+
return Promise.all(
|
|
1919
|
+
agents.map(async (agent) => {
|
|
1920
|
+
let hasSidecar = false;
|
|
1921
|
+
try {
|
|
1922
|
+
findAgentCommand(agent.type);
|
|
1923
|
+
hasSidecar = true;
|
|
1924
|
+
} catch {
|
|
1925
|
+
}
|
|
1926
|
+
const cliInstalled = await isCommandAvailable(agent.command);
|
|
1927
|
+
const installed = hasSidecar || cliInstalled;
|
|
1928
|
+
return {
|
|
1929
|
+
...agent,
|
|
1930
|
+
available: true,
|
|
1931
|
+
...installed ? {} : {
|
|
1932
|
+
unavailableReason: `${agent.name} CLI is not installed yet. Seren can install it automatically on first launch.`
|
|
1933
|
+
}
|
|
1934
|
+
};
|
|
1935
|
+
})
|
|
1936
|
+
);
|
|
638
1937
|
}
|
|
639
1938
|
async function acpCheckAgentAvailable(params) {
|
|
640
1939
|
try {
|
|
@@ -645,29 +1944,39 @@ async function acpCheckAgentAvailable(params) {
|
|
|
645
1944
|
}
|
|
646
1945
|
}
|
|
647
1946
|
async function acpEnsureClaudeCli() {
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
["install", "-g", "@anthropic-ai/claude-code"],
|
|
656
|
-
(err, stdout, stderr) => {
|
|
657
|
-
if (err) {
|
|
658
|
-
reject(
|
|
659
|
-
new Error(
|
|
660
|
-
`Failed to install Claude Code CLI: ${stderr || err.message}`
|
|
661
|
-
)
|
|
662
|
-
);
|
|
663
|
-
return;
|
|
664
|
-
}
|
|
665
|
-
console.log(`[ACP] Claude Code CLI installed: ${stdout}`);
|
|
666
|
-
resolve4("claude");
|
|
667
|
-
}
|
|
668
|
-
);
|
|
1947
|
+
return ensureClaudeCodeViaNativeInstaller();
|
|
1948
|
+
}
|
|
1949
|
+
async function acpEnsureCodexCli() {
|
|
1950
|
+
return ensureGlobalNpmPackage({
|
|
1951
|
+
command: "codex",
|
|
1952
|
+
packageName: "@openai/codex",
|
|
1953
|
+
label: "Codex"
|
|
669
1954
|
});
|
|
670
1955
|
}
|
|
1956
|
+
async function acpEnsureAgentCli(params) {
|
|
1957
|
+
const { agentType } = params;
|
|
1958
|
+
if (agentType === "claude-code") {
|
|
1959
|
+
return ensureClaudeCodeViaNativeInstaller();
|
|
1960
|
+
}
|
|
1961
|
+
if (agentType === "codex") {
|
|
1962
|
+
return ensureGlobalNpmPackage({
|
|
1963
|
+
command: "codex",
|
|
1964
|
+
packageName: "@openai/codex",
|
|
1965
|
+
label: "Codex"
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1968
|
+
throw new Error(`Unknown agent type for CLI install: ${agentType}`);
|
|
1969
|
+
}
|
|
1970
|
+
async function acpLaunchLogin(params) {
|
|
1971
|
+
const { agentType } = params;
|
|
1972
|
+
if (agentType === "claude-code") {
|
|
1973
|
+
launchLoginCommand("claude");
|
|
1974
|
+
} else if (agentType === "codex") {
|
|
1975
|
+
launchLoginCommand("codex");
|
|
1976
|
+
} else {
|
|
1977
|
+
throw new Error(`Unknown agent type for login: ${agentType}`);
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
671
1980
|
|
|
672
1981
|
// src/handlers/dialogs.ts
|
|
673
1982
|
import { execFile as execFile2 } from "child_process";
|
|
@@ -675,17 +1984,17 @@ import { platform as platform2 } from "os";
|
|
|
675
1984
|
import { dirname } from "path";
|
|
676
1985
|
var os = platform2();
|
|
677
1986
|
function exec(cmd, args) {
|
|
678
|
-
return new Promise((
|
|
1987
|
+
return new Promise((resolve5, reject) => {
|
|
679
1988
|
execFile2(cmd, args, { timeout: 6e4 }, (err, stdout) => {
|
|
680
1989
|
if (err) {
|
|
681
1990
|
if (err.code === 1 || err.killed) {
|
|
682
|
-
|
|
1991
|
+
resolve5("");
|
|
683
1992
|
return;
|
|
684
1993
|
}
|
|
685
1994
|
reject(err);
|
|
686
1995
|
return;
|
|
687
1996
|
}
|
|
688
|
-
|
|
1997
|
+
resolve5(stdout.trim());
|
|
689
1998
|
});
|
|
690
1999
|
});
|
|
691
2000
|
}
|
|
@@ -774,9 +2083,9 @@ import {
|
|
|
774
2083
|
stat
|
|
775
2084
|
} from "fs/promises";
|
|
776
2085
|
import { realpathSync as realpathSyncNative } from "fs";
|
|
777
|
-
import { homedir, tmpdir } from "os";
|
|
778
|
-
import { join, resolve as resolve2 } from "path";
|
|
779
|
-
var home =
|
|
2086
|
+
import { homedir as homedir2, tmpdir } from "os";
|
|
2087
|
+
import { join as join2, resolve as resolve2 } from "path";
|
|
2088
|
+
var home = homedir2();
|
|
780
2089
|
var tmp = tmpdir();
|
|
781
2090
|
var homeReal;
|
|
782
2091
|
var tmpReal;
|
|
@@ -816,11 +2125,11 @@ async function listDirectory(params) {
|
|
|
816
2125
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
817
2126
|
return entries.map((entry) => ({
|
|
818
2127
|
name: entry.name,
|
|
819
|
-
path:
|
|
2128
|
+
path: join2(dir, entry.name),
|
|
820
2129
|
is_directory: entry.isDirectory()
|
|
821
2130
|
}));
|
|
822
2131
|
}
|
|
823
|
-
async function
|
|
2132
|
+
async function readFile3(params) {
|
|
824
2133
|
const filePath = await validatePathReal(params.path);
|
|
825
2134
|
return fsReadFile(filePath, "utf-8");
|
|
826
2135
|
}
|
|
@@ -874,22 +2183,22 @@ import Database2 from "better-sqlite3";
|
|
|
874
2183
|
import * as sqliteVec from "sqlite-vec";
|
|
875
2184
|
import { createHash } from "crypto";
|
|
876
2185
|
import { mkdirSync, existsSync as existsSync2 } from "fs";
|
|
877
|
-
import { join as
|
|
878
|
-
import { homedir as
|
|
2186
|
+
import { join as join3 } from "path";
|
|
2187
|
+
import { homedir as homedir3 } from "os";
|
|
879
2188
|
var EMBEDDING_DIM = 1536;
|
|
880
2189
|
function getDataDir() {
|
|
881
|
-
return
|
|
2190
|
+
return join3(homedir3(), ".seren-local", "data");
|
|
882
2191
|
}
|
|
883
2192
|
function getVectorDbPath(projectPath) {
|
|
884
2193
|
const hash = createHash("sha256").update(projectPath).digest("hex").slice(0, 16);
|
|
885
|
-
return
|
|
2194
|
+
return join3(getDataDir(), "indexes", `${hash}.db`);
|
|
886
2195
|
}
|
|
887
2196
|
function hasIndex(projectPath) {
|
|
888
2197
|
return existsSync2(getVectorDbPath(projectPath));
|
|
889
2198
|
}
|
|
890
2199
|
function openDb(projectPath) {
|
|
891
2200
|
const dbPath = getVectorDbPath(projectPath);
|
|
892
|
-
const dir =
|
|
2201
|
+
const dir = join3(getDataDir(), "indexes");
|
|
893
2202
|
if (!existsSync2(dir)) {
|
|
894
2203
|
mkdirSync(dir, { recursive: true });
|
|
895
2204
|
}
|
|
@@ -1049,8 +2358,8 @@ function fileNeedsReindex(projectPath, filePath, currentHash) {
|
|
|
1049
2358
|
}
|
|
1050
2359
|
|
|
1051
2360
|
// src/services/chunker.ts
|
|
1052
|
-
import { readFileSync, readdirSync, statSync } from "fs";
|
|
1053
|
-
import { join as
|
|
2361
|
+
import { readFileSync, readdirSync as readdirSync2, statSync } from "fs";
|
|
2362
|
+
import { join as join4, extname, relative } from "path";
|
|
1054
2363
|
import { createHash as createHash2 } from "crypto";
|
|
1055
2364
|
var MAX_CHUNK_LINES = 100;
|
|
1056
2365
|
var MIN_CHUNK_LINES = 5;
|
|
@@ -1148,25 +2457,25 @@ function discoverFiles(projectPath) {
|
|
|
1148
2457
|
function discoverRecursive(root, current, files) {
|
|
1149
2458
|
let entries;
|
|
1150
2459
|
try {
|
|
1151
|
-
entries =
|
|
2460
|
+
entries = readdirSync2(current);
|
|
1152
2461
|
} catch {
|
|
1153
2462
|
return;
|
|
1154
2463
|
}
|
|
1155
2464
|
for (const name of entries) {
|
|
1156
2465
|
if (shouldIgnore(name)) continue;
|
|
1157
|
-
const fullPath =
|
|
1158
|
-
let
|
|
2466
|
+
const fullPath = join4(current, name);
|
|
2467
|
+
let stat3;
|
|
1159
2468
|
try {
|
|
1160
|
-
|
|
2469
|
+
stat3 = statSync(fullPath);
|
|
1161
2470
|
} catch {
|
|
1162
2471
|
continue;
|
|
1163
2472
|
}
|
|
1164
|
-
if (
|
|
2473
|
+
if (stat3.isDirectory()) {
|
|
1165
2474
|
discoverRecursive(root, fullPath, files);
|
|
1166
|
-
} else if (
|
|
2475
|
+
} else if (stat3.isFile()) {
|
|
1167
2476
|
const language = detectLanguage(fullPath);
|
|
1168
2477
|
if (!language) continue;
|
|
1169
|
-
if (
|
|
2478
|
+
if (stat3.size > MAX_FILE_SIZE) continue;
|
|
1170
2479
|
let content;
|
|
1171
2480
|
try {
|
|
1172
2481
|
content = readFileSync(fullPath, "utf-8");
|
|
@@ -1177,7 +2486,7 @@ function discoverRecursive(root, current, files) {
|
|
|
1177
2486
|
path: fullPath,
|
|
1178
2487
|
relative_path: relative(root, fullPath),
|
|
1179
2488
|
language,
|
|
1180
|
-
size:
|
|
2489
|
+
size: stat3.size,
|
|
1181
2490
|
hash: computeHash(content)
|
|
1182
2491
|
});
|
|
1183
2492
|
}
|
|
@@ -1444,7 +2753,7 @@ import { randomUUID as randomUUID2 } from "crypto";
|
|
|
1444
2753
|
var processes = /* @__PURE__ */ new Map();
|
|
1445
2754
|
function sendRequest(proc, method, params) {
|
|
1446
2755
|
const id = randomUUID2();
|
|
1447
|
-
return new Promise((
|
|
2756
|
+
return new Promise((resolve5, reject) => {
|
|
1448
2757
|
const timeout = setTimeout(() => {
|
|
1449
2758
|
proc.pendingRequests.delete(id);
|
|
1450
2759
|
reject(new Error(`MCP request timeout: ${method}`));
|
|
@@ -1452,7 +2761,7 @@ function sendRequest(proc, method, params) {
|
|
|
1452
2761
|
proc.pendingRequests.set(id, {
|
|
1453
2762
|
resolve: (v) => {
|
|
1454
2763
|
clearTimeout(timeout);
|
|
1455
|
-
|
|
2764
|
+
resolve5(v);
|
|
1456
2765
|
},
|
|
1457
2766
|
reject: (e) => {
|
|
1458
2767
|
clearTimeout(timeout);
|
|
@@ -1482,14 +2791,14 @@ async function mcpReadResource(params) {
|
|
|
1482
2791
|
import { execFile as execFile3, spawn as spawn2 } from "child_process";
|
|
1483
2792
|
import { createServer } from "net";
|
|
1484
2793
|
import {
|
|
1485
|
-
readFile as
|
|
2794
|
+
readFile as readFile4,
|
|
1486
2795
|
writeFile as writeFile3,
|
|
1487
2796
|
mkdir as mkdir2,
|
|
1488
2797
|
access as access2,
|
|
1489
2798
|
constants
|
|
1490
2799
|
} from "fs/promises";
|
|
1491
|
-
import { join as
|
|
1492
|
-
import { homedir as
|
|
2800
|
+
import { join as join5 } from "path";
|
|
2801
|
+
import { homedir as homedir4 } from "os";
|
|
1493
2802
|
import { randomBytes } from "crypto";
|
|
1494
2803
|
import WebSocket from "ws";
|
|
1495
2804
|
var childProcess = null;
|
|
@@ -1505,19 +2814,19 @@ var channels = [];
|
|
|
1505
2814
|
var trustSettings = /* @__PURE__ */ new Map();
|
|
1506
2815
|
var approvedIds = /* @__PURE__ */ new Set();
|
|
1507
2816
|
var MAX_RESTART_ATTEMPTS = 3;
|
|
1508
|
-
var OPENCLAW_DIR =
|
|
1509
|
-
var CONFIG_PATH =
|
|
1510
|
-
var SETTINGS_PATH =
|
|
2817
|
+
var OPENCLAW_DIR = join5(homedir4(), ".openclaw");
|
|
2818
|
+
var CONFIG_PATH = join5(OPENCLAW_DIR, "openclaw.json");
|
|
2819
|
+
var SETTINGS_PATH = join5(homedir4(), ".seren-local", "settings.json");
|
|
1511
2820
|
async function loadSettings() {
|
|
1512
2821
|
try {
|
|
1513
|
-
const data = await
|
|
2822
|
+
const data = await readFile4(SETTINGS_PATH, "utf-8");
|
|
1514
2823
|
return JSON.parse(data);
|
|
1515
2824
|
} catch {
|
|
1516
2825
|
return {};
|
|
1517
2826
|
}
|
|
1518
2827
|
}
|
|
1519
2828
|
async function saveSettings(settings) {
|
|
1520
|
-
await mkdir2(
|
|
2829
|
+
await mkdir2(join5(homedir4(), ".seren-local"), { recursive: true });
|
|
1521
2830
|
await writeFile3(SETTINGS_PATH, JSON.stringify(settings, null, 2), "utf-8");
|
|
1522
2831
|
}
|
|
1523
2832
|
async function getSetting(params) {
|
|
@@ -1530,13 +2839,13 @@ async function setSetting(params) {
|
|
|
1530
2839
|
await saveSettings(settings);
|
|
1531
2840
|
}
|
|
1532
2841
|
function findAvailablePort() {
|
|
1533
|
-
return new Promise((
|
|
2842
|
+
return new Promise((resolve5, reject) => {
|
|
1534
2843
|
const server = createServer();
|
|
1535
2844
|
server.listen(0, "127.0.0.1", () => {
|
|
1536
2845
|
const addr = server.address();
|
|
1537
2846
|
if (addr && typeof addr === "object") {
|
|
1538
2847
|
const p = addr.port;
|
|
1539
|
-
server.close(() =>
|
|
2848
|
+
server.close(() => resolve5(p));
|
|
1540
2849
|
} else {
|
|
1541
2850
|
server.close(() => reject(new Error("Failed to get port")));
|
|
1542
2851
|
}
|
|
@@ -1547,7 +2856,7 @@ function findAvailablePort() {
|
|
|
1547
2856
|
async function getOrCreateToken() {
|
|
1548
2857
|
if (hookToken) return hookToken;
|
|
1549
2858
|
try {
|
|
1550
|
-
const config2 = JSON.parse(await
|
|
2859
|
+
const config2 = JSON.parse(await readFile4(CONFIG_PATH, "utf-8"));
|
|
1551
2860
|
if (config2.hookToken) {
|
|
1552
2861
|
hookToken = config2.hookToken;
|
|
1553
2862
|
return hookToken;
|
|
@@ -1558,7 +2867,7 @@ async function getOrCreateToken() {
|
|
|
1558
2867
|
await mkdir2(OPENCLAW_DIR, { recursive: true });
|
|
1559
2868
|
let config = {};
|
|
1560
2869
|
try {
|
|
1561
|
-
config = JSON.parse(await
|
|
2870
|
+
config = JSON.parse(await readFile4(CONFIG_PATH, "utf-8"));
|
|
1562
2871
|
} catch {
|
|
1563
2872
|
}
|
|
1564
2873
|
config.hookToken = hookToken;
|
|
@@ -1570,19 +2879,19 @@ async function getOrCreateToken() {
|
|
|
1570
2879
|
async function findOpenClawEntrypoint() {
|
|
1571
2880
|
const candidates = [
|
|
1572
2881
|
// Global npm install
|
|
1573
|
-
|
|
2882
|
+
join5(homedir4(), ".seren-local", "lib", "node_modules", "openclaw", "openclaw.mjs"),
|
|
1574
2883
|
// Development
|
|
1575
|
-
|
|
2884
|
+
join5(homedir4(), ".openclaw", "openclaw.mjs")
|
|
1576
2885
|
];
|
|
1577
2886
|
try {
|
|
1578
2887
|
const cmd = process.platform === "win32" ? "where" : "which";
|
|
1579
|
-
const
|
|
2888
|
+
const path2 = await new Promise((resolve5, reject) => {
|
|
1580
2889
|
execFile3(cmd, ["openclaw"], (err, stdout) => {
|
|
1581
2890
|
if (err) reject(err);
|
|
1582
|
-
else
|
|
2891
|
+
else resolve5(stdout.trim());
|
|
1583
2892
|
});
|
|
1584
2893
|
});
|
|
1585
|
-
if (
|
|
2894
|
+
if (path2) candidates.unshift(path2);
|
|
1586
2895
|
} catch {
|
|
1587
2896
|
}
|
|
1588
2897
|
for (const candidate of candidates) {
|
|
@@ -1708,7 +3017,7 @@ async function openclawStart(_params) {
|
|
|
1708
3017
|
emit("openclaw://status-changed", { status: "crashed" });
|
|
1709
3018
|
}
|
|
1710
3019
|
});
|
|
1711
|
-
await new Promise((
|
|
3020
|
+
await new Promise((resolve5) => setTimeout(resolve5, 1e3));
|
|
1712
3021
|
if (childProcess && !childProcess.killed) {
|
|
1713
3022
|
processStatus = "running";
|
|
1714
3023
|
startedAt = Date.now();
|
|
@@ -1749,7 +3058,7 @@ async function openclawStop(_params) {
|
|
|
1749
3058
|
}
|
|
1750
3059
|
async function openclawRestart(_params) {
|
|
1751
3060
|
await openclawStop({});
|
|
1752
|
-
await new Promise((
|
|
3061
|
+
await new Promise((resolve5) => setTimeout(resolve5, 500));
|
|
1753
3062
|
await openclawStart({});
|
|
1754
3063
|
}
|
|
1755
3064
|
async function openclawStatus(_params) {
|
|
@@ -1764,7 +3073,7 @@ async function openclawStatus(_params) {
|
|
|
1764
3073
|
async function openclawListChannels(_params) {
|
|
1765
3074
|
if (processStatus !== "running") return [];
|
|
1766
3075
|
const entrypoint = await findOpenClawEntrypoint();
|
|
1767
|
-
return new Promise((
|
|
3076
|
+
return new Promise((resolve5) => {
|
|
1768
3077
|
execFile3(
|
|
1769
3078
|
"node",
|
|
1770
3079
|
[entrypoint, "channels", "status", "--json"],
|
|
@@ -1772,7 +3081,7 @@ async function openclawListChannels(_params) {
|
|
|
1772
3081
|
(err, stdout) => {
|
|
1773
3082
|
if (err) {
|
|
1774
3083
|
console.error("[OpenClaw] Failed to list channels:", err);
|
|
1775
|
-
|
|
3084
|
+
resolve5([]);
|
|
1776
3085
|
return;
|
|
1777
3086
|
}
|
|
1778
3087
|
try {
|
|
@@ -1791,9 +3100,9 @@ async function openclawListChannels(_params) {
|
|
|
1791
3100
|
}
|
|
1792
3101
|
channels.length = 0;
|
|
1793
3102
|
channels.push(...result);
|
|
1794
|
-
|
|
3103
|
+
resolve5(result);
|
|
1795
3104
|
} catch {
|
|
1796
|
-
|
|
3105
|
+
resolve5([]);
|
|
1797
3106
|
}
|
|
1798
3107
|
}
|
|
1799
3108
|
);
|
|
@@ -1818,7 +3127,7 @@ async function openclawConnectChannel(params) {
|
|
|
1818
3127
|
}
|
|
1819
3128
|
let config = {};
|
|
1820
3129
|
try {
|
|
1821
|
-
config = JSON.parse(await
|
|
3130
|
+
config = JSON.parse(await readFile4(CONFIG_PATH, "utf-8"));
|
|
1822
3131
|
} catch {
|
|
1823
3132
|
}
|
|
1824
3133
|
if (!config.channels) config.channels = {};
|
|
@@ -1864,14 +3173,14 @@ async function openclawConnectChannel(params) {
|
|
|
1864
3173
|
async function openclawDisconnectChannel(params) {
|
|
1865
3174
|
const { channelId } = params;
|
|
1866
3175
|
const entrypoint = await findOpenClawEntrypoint();
|
|
1867
|
-
return new Promise((
|
|
3176
|
+
return new Promise((resolve5, reject) => {
|
|
1868
3177
|
execFile3(
|
|
1869
3178
|
"node",
|
|
1870
3179
|
[entrypoint, "channels", "remove", "--channel", channelId, "--delete"],
|
|
1871
3180
|
{ cwd: OPENCLAW_DIR, timeout: 1e4 },
|
|
1872
3181
|
(err) => {
|
|
1873
3182
|
if (err) reject(new Error(`Failed to disconnect: ${err.message}`));
|
|
1874
|
-
else
|
|
3183
|
+
else resolve5();
|
|
1875
3184
|
}
|
|
1876
3185
|
);
|
|
1877
3186
|
});
|
|
@@ -1926,7 +3235,7 @@ async function openclawGrantApproval(params) {
|
|
|
1926
3235
|
async function openclawGetQr(params) {
|
|
1927
3236
|
const { platform: plat } = params;
|
|
1928
3237
|
const entrypoint = await findOpenClawEntrypoint();
|
|
1929
|
-
return new Promise((
|
|
3238
|
+
return new Promise((resolve5, reject) => {
|
|
1930
3239
|
execFile3(
|
|
1931
3240
|
"node",
|
|
1932
3241
|
[entrypoint, "channels", "qr", "--platform", plat, "--json"],
|
|
@@ -1936,9 +3245,9 @@ async function openclawGetQr(params) {
|
|
|
1936
3245
|
else {
|
|
1937
3246
|
try {
|
|
1938
3247
|
const data = JSON.parse(stdout);
|
|
1939
|
-
|
|
3248
|
+
resolve5(data.qr || data.qrCode || stdout.trim());
|
|
1940
3249
|
} catch {
|
|
1941
|
-
|
|
3250
|
+
resolve5(stdout.trim());
|
|
1942
3251
|
}
|
|
1943
3252
|
}
|
|
1944
3253
|
}
|
|
@@ -2012,7 +3321,7 @@ async function stopWatching() {
|
|
|
2012
3321
|
|
|
2013
3322
|
// src/handlers/updater.ts
|
|
2014
3323
|
import { spawn as spawn3 } from "child_process";
|
|
2015
|
-
import { platform as platform3, homedir as
|
|
3324
|
+
import { platform as platform3, homedir as homedir5 } from "os";
|
|
2016
3325
|
import { resolve as resolve3 } from "path";
|
|
2017
3326
|
import { createRequire } from "module";
|
|
2018
3327
|
var require2 = createRequire(import.meta.url);
|
|
@@ -2044,8 +3353,8 @@ async function checkForUpdate() {
|
|
|
2044
3353
|
}
|
|
2045
3354
|
}
|
|
2046
3355
|
async function installUpdate() {
|
|
2047
|
-
const
|
|
2048
|
-
const serenDir = resolve3(
|
|
3356
|
+
const home3 = homedir5();
|
|
3357
|
+
const serenDir = resolve3(home3, ".seren-local");
|
|
2049
3358
|
const nodeDir = resolve3(serenDir, "node");
|
|
2050
3359
|
const binDir = resolve3(serenDir, "bin");
|
|
2051
3360
|
const isWin = platform3() === "win32";
|
|
@@ -2092,11 +3401,253 @@ function isNewer(a, b) {
|
|
|
2092
3401
|
return false;
|
|
2093
3402
|
}
|
|
2094
3403
|
|
|
3404
|
+
// src/handlers/skills.ts
|
|
3405
|
+
import {
|
|
3406
|
+
mkdir as mkdir3,
|
|
3407
|
+
readFile as fsReadFile2,
|
|
3408
|
+
readdir as readdir2,
|
|
3409
|
+
rename as rename2,
|
|
3410
|
+
rm as rm2,
|
|
3411
|
+
stat as stat2,
|
|
3412
|
+
symlink,
|
|
3413
|
+
lstat,
|
|
3414
|
+
writeFile as fsWriteFile2
|
|
3415
|
+
} from "fs/promises";
|
|
3416
|
+
import { homedir as homedir6 } from "os";
|
|
3417
|
+
import { dirname as dirname2, isAbsolute, join as join6, normalize, resolve as resolve4 } from "path";
|
|
3418
|
+
var home2 = homedir6();
|
|
3419
|
+
function serenConfigDir() {
|
|
3420
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
3421
|
+
if (xdg && isAbsolute(xdg)) {
|
|
3422
|
+
return join6(xdg, "seren");
|
|
3423
|
+
}
|
|
3424
|
+
return join6(home2, ".config", "seren");
|
|
3425
|
+
}
|
|
3426
|
+
async function ensureDir(dir) {
|
|
3427
|
+
await mkdir3(dir, { recursive: true });
|
|
3428
|
+
}
|
|
3429
|
+
function isSafeRelativePath(p) {
|
|
3430
|
+
if (isAbsolute(p)) return false;
|
|
3431
|
+
const normalized = normalize(p);
|
|
3432
|
+
if (normalized.startsWith("..")) return false;
|
|
3433
|
+
const parts = normalized.split(/[\\/]/);
|
|
3434
|
+
return !parts.some((part) => part === "..");
|
|
3435
|
+
}
|
|
3436
|
+
async function getSerenSkillsDir() {
|
|
3437
|
+
const dir = join6(serenConfigDir(), "skills");
|
|
3438
|
+
await ensureDir(dir);
|
|
3439
|
+
return dir;
|
|
3440
|
+
}
|
|
3441
|
+
async function getClaudeSkillsDir() {
|
|
3442
|
+
const dir = join6(home2, ".claude", "skills");
|
|
3443
|
+
await ensureDir(dir);
|
|
3444
|
+
return dir;
|
|
3445
|
+
}
|
|
3446
|
+
async function getProjectSkillsDir(params) {
|
|
3447
|
+
const { projectRoot } = params;
|
|
3448
|
+
if (!projectRoot) return null;
|
|
3449
|
+
const root = resolve4(projectRoot);
|
|
3450
|
+
try {
|
|
3451
|
+
const s = await stat2(root);
|
|
3452
|
+
if (!s.isDirectory()) return null;
|
|
3453
|
+
} catch {
|
|
3454
|
+
return null;
|
|
3455
|
+
}
|
|
3456
|
+
const localSkillsDir = join6(root, "skills");
|
|
3457
|
+
try {
|
|
3458
|
+
const s = await stat2(localSkillsDir);
|
|
3459
|
+
if (s.isDirectory()) return localSkillsDir;
|
|
3460
|
+
} catch {
|
|
3461
|
+
}
|
|
3462
|
+
return null;
|
|
3463
|
+
}
|
|
3464
|
+
async function readSkillFile(params) {
|
|
3465
|
+
const { skillsDir, slug, relativePath } = params;
|
|
3466
|
+
if (!isSafeRelativePath(relativePath)) {
|
|
3467
|
+
throw new Error(
|
|
3468
|
+
`Invalid skill-relative path (must be relative, no ..): ${relativePath}`
|
|
3469
|
+
);
|
|
3470
|
+
}
|
|
3471
|
+
const skillDir = join6(resolve4(skillsDir), slug);
|
|
3472
|
+
const filePath = join6(skillDir, relativePath);
|
|
3473
|
+
return fsReadFile2(filePath, "utf-8");
|
|
3474
|
+
}
|
|
3475
|
+
async function writeSkillFile(params) {
|
|
3476
|
+
const { skillsDir, slug, relativePath, content } = params;
|
|
3477
|
+
if (!isSafeRelativePath(relativePath)) {
|
|
3478
|
+
throw new Error(
|
|
3479
|
+
`Invalid skill-relative path (must be relative, no ..): ${relativePath}`
|
|
3480
|
+
);
|
|
3481
|
+
}
|
|
3482
|
+
const skillDir = join6(resolve4(skillsDir), slug);
|
|
3483
|
+
const filePath = join6(skillDir, relativePath);
|
|
3484
|
+
await ensureDir(dirname2(filePath));
|
|
3485
|
+
await fsWriteFile2(filePath, content, "utf-8");
|
|
3486
|
+
if (relativePath.endsWith(".sh") || relativePath.endsWith(".py")) {
|
|
3487
|
+
const { chmod } = await import("fs/promises");
|
|
3488
|
+
await chmod(filePath, 493).catch(() => {
|
|
3489
|
+
});
|
|
3490
|
+
}
|
|
3491
|
+
}
|
|
3492
|
+
async function listSkillFiles(params) {
|
|
3493
|
+
const { skillsDir } = params;
|
|
3494
|
+
const dirPath = resolve4(skillsDir);
|
|
3495
|
+
try {
|
|
3496
|
+
await stat2(dirPath);
|
|
3497
|
+
} catch {
|
|
3498
|
+
return [];
|
|
3499
|
+
}
|
|
3500
|
+
const entries = await readdir2(dirPath, { withFileTypes: true });
|
|
3501
|
+
const slugs = [];
|
|
3502
|
+
for (const entry of entries) {
|
|
3503
|
+
if (!entry.isDirectory()) continue;
|
|
3504
|
+
if (entry.name.startsWith(".")) continue;
|
|
3505
|
+
const entryPath = join6(dirPath, entry.name);
|
|
3506
|
+
try {
|
|
3507
|
+
const skillFile = join6(entryPath, "SKILL.md");
|
|
3508
|
+
const s = await stat2(skillFile);
|
|
3509
|
+
if (s.isFile()) {
|
|
3510
|
+
slugs.push(entry.name);
|
|
3511
|
+
continue;
|
|
3512
|
+
}
|
|
3513
|
+
} catch {
|
|
3514
|
+
}
|
|
3515
|
+
try {
|
|
3516
|
+
const subEntries = await readdir2(entryPath, { withFileTypes: true });
|
|
3517
|
+
for (const subEntry of subEntries) {
|
|
3518
|
+
if (!subEntry.isDirectory()) continue;
|
|
3519
|
+
const subSkillFile = join6(entryPath, subEntry.name, "SKILL.md");
|
|
3520
|
+
try {
|
|
3521
|
+
const s = await stat2(subSkillFile);
|
|
3522
|
+
if (s.isFile()) {
|
|
3523
|
+
slugs.push(`${entry.name}-${subEntry.name}`);
|
|
3524
|
+
}
|
|
3525
|
+
} catch {
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
} catch {
|
|
3529
|
+
}
|
|
3530
|
+
}
|
|
3531
|
+
slugs.sort();
|
|
3532
|
+
return [...new Set(slugs)];
|
|
3533
|
+
}
|
|
3534
|
+
async function writeSkillTree(params) {
|
|
3535
|
+
const { skillsDir, slug, content, extraFiles = [] } = params;
|
|
3536
|
+
const dirPath = resolve4(skillsDir);
|
|
3537
|
+
const skillDir = join6(dirPath, slug);
|
|
3538
|
+
const tempDir = join6(dirPath, `.${slug}.installing.${process.pid}.${Date.now()}`);
|
|
3539
|
+
let backupDir = null;
|
|
3540
|
+
try {
|
|
3541
|
+
await ensureDir(tempDir);
|
|
3542
|
+
await fsWriteFile2(join6(tempDir, "SKILL.md"), content, "utf-8");
|
|
3543
|
+
for (const file of extraFiles) {
|
|
3544
|
+
if (!isSafeRelativePath(file.path)) {
|
|
3545
|
+
throw new Error(
|
|
3546
|
+
`Invalid file path (must be relative, no ..): ${file.path}`
|
|
3547
|
+
);
|
|
3548
|
+
}
|
|
3549
|
+
const target = join6(tempDir, file.path);
|
|
3550
|
+
await ensureDir(dirname2(target));
|
|
3551
|
+
await fsWriteFile2(target, file.content, "utf-8");
|
|
3552
|
+
if (file.path.endsWith(".sh") || file.path.endsWith(".py")) {
|
|
3553
|
+
const { chmod } = await import("fs/promises");
|
|
3554
|
+
await chmod(target, 493).catch(() => {
|
|
3555
|
+
});
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3558
|
+
try {
|
|
3559
|
+
const s = await stat2(skillDir);
|
|
3560
|
+
if (s.isDirectory()) {
|
|
3561
|
+
backupDir = join6(dirPath, `.${slug}.backup.${process.pid}.${Date.now()}`);
|
|
3562
|
+
await rename2(skillDir, backupDir);
|
|
3563
|
+
}
|
|
3564
|
+
} catch {
|
|
3565
|
+
}
|
|
3566
|
+
await rename2(tempDir, skillDir);
|
|
3567
|
+
} catch (error) {
|
|
3568
|
+
await rm2(tempDir, { recursive: true, force: true }).catch(() => {
|
|
3569
|
+
});
|
|
3570
|
+
if (backupDir) {
|
|
3571
|
+
try {
|
|
3572
|
+
await stat2(skillDir);
|
|
3573
|
+
} catch {
|
|
3574
|
+
await rename2(backupDir, skillDir).catch(() => {
|
|
3575
|
+
});
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3578
|
+
throw error;
|
|
3579
|
+
}
|
|
3580
|
+
if (backupDir) {
|
|
3581
|
+
await rm2(backupDir, { recursive: true, force: true }).catch(() => {
|
|
3582
|
+
});
|
|
3583
|
+
}
|
|
3584
|
+
return join6(skillDir, "SKILL.md");
|
|
3585
|
+
}
|
|
3586
|
+
async function createSkillsSymlink(params) {
|
|
3587
|
+
const { projectRoot } = params;
|
|
3588
|
+
const root = resolve4(projectRoot);
|
|
3589
|
+
try {
|
|
3590
|
+
const s = await stat2(root);
|
|
3591
|
+
if (!s.isDirectory()) throw new Error("Project root is not a directory");
|
|
3592
|
+
} catch (err) {
|
|
3593
|
+
if (err.code === "ENOENT") {
|
|
3594
|
+
throw new Error("Project root does not exist");
|
|
3595
|
+
}
|
|
3596
|
+
throw err;
|
|
3597
|
+
}
|
|
3598
|
+
const claudeDir = join6(root, ".claude");
|
|
3599
|
+
const symlinkPath = join6(claudeDir, "skills");
|
|
3600
|
+
const skillsDir = join6(root, "skills");
|
|
3601
|
+
try {
|
|
3602
|
+
const s = await stat2(skillsDir);
|
|
3603
|
+
if (!s.isDirectory()) {
|
|
3604
|
+
throw new Error(
|
|
3605
|
+
`Could not find a skills directory. Expected ${skillsDir}`
|
|
3606
|
+
);
|
|
3607
|
+
}
|
|
3608
|
+
} catch (err) {
|
|
3609
|
+
if (err.code === "ENOENT") {
|
|
3610
|
+
throw new Error(
|
|
3611
|
+
`Could not find a skills directory. Expected ${skillsDir}`
|
|
3612
|
+
);
|
|
3613
|
+
}
|
|
3614
|
+
throw err;
|
|
3615
|
+
}
|
|
3616
|
+
await ensureDir(claudeDir);
|
|
3617
|
+
try {
|
|
3618
|
+
const linkStat = await lstat(symlinkPath);
|
|
3619
|
+
if (linkStat.isSymbolicLink()) {
|
|
3620
|
+
const { unlink } = await import("fs/promises");
|
|
3621
|
+
await unlink(symlinkPath);
|
|
3622
|
+
} else {
|
|
3623
|
+
throw new Error(
|
|
3624
|
+
".claude/skills exists but is not a symlink. Please remove it manually."
|
|
3625
|
+
);
|
|
3626
|
+
}
|
|
3627
|
+
} catch (err) {
|
|
3628
|
+
if (err.code !== "ENOENT") {
|
|
3629
|
+
throw err;
|
|
3630
|
+
}
|
|
3631
|
+
}
|
|
3632
|
+
const relativeTarget = join6("..", "skills");
|
|
3633
|
+
await symlink(relativeTarget, symlinkPath);
|
|
3634
|
+
}
|
|
3635
|
+
function registerSkillsHandlers(register) {
|
|
3636
|
+
register("get_seren_skills_dir", getSerenSkillsDir);
|
|
3637
|
+
register("get_claude_skills_dir", getClaudeSkillsDir);
|
|
3638
|
+
register("get_project_skills_dir", getProjectSkillsDir);
|
|
3639
|
+
register("read_skill_file", readSkillFile);
|
|
3640
|
+
register("write_skill_file", writeSkillFile);
|
|
3641
|
+
register("list_skill_files", listSkillFiles);
|
|
3642
|
+
register("write_skill_tree", writeSkillTree);
|
|
3643
|
+
register("create_skills_symlink", createSkillsSymlink);
|
|
3644
|
+
}
|
|
3645
|
+
|
|
2095
3646
|
// src/handlers/wallet.ts
|
|
2096
3647
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
2097
|
-
import { readFile as
|
|
2098
|
-
import { join as
|
|
2099
|
-
import { homedir as
|
|
3648
|
+
import { readFile as readFile5, writeFile as writeFile4, mkdir as mkdir4 } from "fs/promises";
|
|
3649
|
+
import { join as join7 } from "path";
|
|
3650
|
+
import { homedir as homedir7 } from "os";
|
|
2100
3651
|
import {
|
|
2101
3652
|
privateKeyToAccount
|
|
2102
3653
|
} from "viem/accounts";
|
|
@@ -2109,20 +3660,20 @@ import {
|
|
|
2109
3660
|
getAddress
|
|
2110
3661
|
} from "viem";
|
|
2111
3662
|
import { base } from "viem/chains";
|
|
2112
|
-
var SEREN_DIR =
|
|
2113
|
-
var WALLET_FILE =
|
|
3663
|
+
var SEREN_DIR = join7(homedir7(), ".seren-local");
|
|
3664
|
+
var WALLET_FILE = join7(SEREN_DIR, "data", "crypto-wallet.json");
|
|
2114
3665
|
var USDC_CONTRACT_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
2115
3666
|
var BASE_RPC_URL = "https://mainnet.base.org";
|
|
2116
3667
|
async function loadStore() {
|
|
2117
3668
|
try {
|
|
2118
|
-
const data = await
|
|
3669
|
+
const data = await readFile5(WALLET_FILE, "utf-8");
|
|
2119
3670
|
return JSON.parse(data);
|
|
2120
3671
|
} catch {
|
|
2121
3672
|
return null;
|
|
2122
3673
|
}
|
|
2123
3674
|
}
|
|
2124
3675
|
async function saveStore(store) {
|
|
2125
|
-
await
|
|
3676
|
+
await mkdir4(join7(SEREN_DIR, "data"), { recursive: true });
|
|
2126
3677
|
await writeFile4(WALLET_FILE, JSON.stringify(store), "utf-8");
|
|
2127
3678
|
}
|
|
2128
3679
|
async function clearStore() {
|
|
@@ -2210,9 +3761,9 @@ var transferWithAuthorizationTypes = {
|
|
|
2210
3761
|
{ name: "nonce", type: "bytes32" }
|
|
2211
3762
|
]
|
|
2212
3763
|
};
|
|
2213
|
-
function getExtra(option, ...
|
|
3764
|
+
function getExtra(option, ...path2) {
|
|
2214
3765
|
let current = option.extra;
|
|
2215
|
-
for (const key of
|
|
3766
|
+
for (const key of path2) {
|
|
2216
3767
|
if (current == null || typeof current !== "object") return void 0;
|
|
2217
3768
|
current = current[key];
|
|
2218
3769
|
}
|
|
@@ -2367,7 +3918,7 @@ async function getCryptoUsdcBalance() {
|
|
|
2367
3918
|
// src/handlers/index.ts
|
|
2368
3919
|
function registerAllHandlers() {
|
|
2369
3920
|
registerHandler("list_directory", listDirectory);
|
|
2370
|
-
registerHandler("read_file",
|
|
3921
|
+
registerHandler("read_file", readFile3);
|
|
2371
3922
|
registerHandler("read_file_base64", readFileBase64);
|
|
2372
3923
|
registerHandler("write_file", writeFile2);
|
|
2373
3924
|
registerHandler("path_exists", pathExists);
|
|
@@ -2391,6 +3942,9 @@ function registerAllHandlers() {
|
|
|
2391
3942
|
registerHandler("acp_get_available_agents", acpGetAvailableAgents);
|
|
2392
3943
|
registerHandler("acp_check_agent_available", acpCheckAgentAvailable);
|
|
2393
3944
|
registerHandler("acp_ensure_claude_cli", acpEnsureClaudeCli);
|
|
3945
|
+
registerHandler("acp_ensure_codex_cli", acpEnsureCodexCli);
|
|
3946
|
+
registerHandler("acp_ensure_agent_cli", acpEnsureAgentCli);
|
|
3947
|
+
registerHandler("acp_launch_login", acpLaunchLogin);
|
|
2394
3948
|
registerHandler("openclaw_start", openclawStart);
|
|
2395
3949
|
registerHandler("openclaw_stop", openclawStop);
|
|
2396
3950
|
registerHandler("openclaw_restart", openclawRestart);
|
|
@@ -2427,6 +3981,7 @@ function registerAllHandlers() {
|
|
|
2427
3981
|
registerHandler("mcp_read_resource", mcpReadResource);
|
|
2428
3982
|
registerHandler("check_for_update", checkForUpdate);
|
|
2429
3983
|
registerHandler("install_update", installUpdate);
|
|
3984
|
+
registerSkillsHandlers(registerHandler);
|
|
2430
3985
|
registerHandler("create_conversation", createConversation);
|
|
2431
3986
|
registerHandler("get_conversations", getConversations);
|
|
2432
3987
|
registerHandler("get_conversation", getConversation);
|
|
@@ -2435,17 +3990,35 @@ function registerAllHandlers() {
|
|
|
2435
3990
|
registerHandler("delete_conversation", deleteConversation);
|
|
2436
3991
|
registerHandler("save_message", saveMessage);
|
|
2437
3992
|
registerHandler("get_messages", getMessages);
|
|
3993
|
+
registerHandler("orchestrate", async (params) => {
|
|
3994
|
+
orchestrate({
|
|
3995
|
+
conversationId: params.conversationId,
|
|
3996
|
+
prompt: params.prompt,
|
|
3997
|
+
history: params.history,
|
|
3998
|
+
capabilities: params.capabilities,
|
|
3999
|
+
images: params.images,
|
|
4000
|
+
gatewayBase: params.gatewayBase,
|
|
4001
|
+
authToken: params.authToken
|
|
4002
|
+
}).catch((err) => {
|
|
4003
|
+
console.error("[Orchestrator] Unhandled error:", err);
|
|
4004
|
+
});
|
|
4005
|
+
return { accepted: true };
|
|
4006
|
+
});
|
|
4007
|
+
registerHandler("cancel_orchestration", async (params) => {
|
|
4008
|
+
const cancelled = cancelOrchestration(params.conversationId);
|
|
4009
|
+
return { cancelled };
|
|
4010
|
+
});
|
|
2438
4011
|
}
|
|
2439
4012
|
|
|
2440
4013
|
// src/update-check.ts
|
|
2441
4014
|
import { readFileSync as readFileSync2 } from "fs";
|
|
2442
|
-
import { join as
|
|
4015
|
+
import { join as join8, dirname as dirname3 } from "path";
|
|
2443
4016
|
import { fileURLToPath } from "url";
|
|
2444
|
-
var __dirname =
|
|
4017
|
+
var __dirname = dirname3(fileURLToPath(import.meta.url));
|
|
2445
4018
|
function getInstalledVersion() {
|
|
2446
4019
|
try {
|
|
2447
4020
|
const pkg2 = JSON.parse(
|
|
2448
|
-
readFileSync2(
|
|
4021
|
+
readFileSync2(join8(__dirname, "..", "package.json"), "utf-8")
|
|
2449
4022
|
);
|
|
2450
4023
|
return pkg2.version ?? "0.0.0";
|
|
2451
4024
|
} catch {
|
|
@@ -2498,10 +4071,10 @@ var PORT = Number(process.env.SEREN_PORT) || 19420;
|
|
|
2498
4071
|
var NO_OPEN = process.argv.includes("--no-open");
|
|
2499
4072
|
var AUTH_TOKEN = process.env.SEREN_RUNTIME_TOKEN || randomBytes3(32).toString("hex");
|
|
2500
4073
|
var __dirname2 = fileURLToPath2(new URL(".", import.meta.url));
|
|
2501
|
-
var PUBLIC_DIR =
|
|
4074
|
+
var PUBLIC_DIR = join9(__dirname2, "..", "public");
|
|
2502
4075
|
function computeBuildHash() {
|
|
2503
4076
|
try {
|
|
2504
|
-
const indexPath =
|
|
4077
|
+
const indexPath = join9(PUBLIC_DIR, "index.html");
|
|
2505
4078
|
const content = readFileSync3(indexPath, "utf-8");
|
|
2506
4079
|
return createHash3("sha256").update(content).digest("hex").slice(0, 12);
|
|
2507
4080
|
} catch {
|
|
@@ -2526,7 +4099,7 @@ var MIME_TYPES = {
|
|
|
2526
4099
|
".wasm": "application/wasm"
|
|
2527
4100
|
};
|
|
2528
4101
|
function serveHtml(res) {
|
|
2529
|
-
const indexPath =
|
|
4102
|
+
const indexPath = join9(PUBLIC_DIR, "index.html");
|
|
2530
4103
|
try {
|
|
2531
4104
|
let html = readFileSync3(indexPath, "utf-8");
|
|
2532
4105
|
html = html.replace(
|
|
@@ -2550,13 +4123,13 @@ function serveStatic(urlPath, res) {
|
|
|
2550
4123
|
if (safePath === "/" || safePath === "/index.html") {
|
|
2551
4124
|
return serveHtml(res);
|
|
2552
4125
|
}
|
|
2553
|
-
const filePath =
|
|
4126
|
+
const filePath = join9(PUBLIC_DIR, safePath);
|
|
2554
4127
|
if (!filePath.startsWith(PUBLIC_DIR)) {
|
|
2555
4128
|
return false;
|
|
2556
4129
|
}
|
|
2557
4130
|
try {
|
|
2558
|
-
const
|
|
2559
|
-
if (
|
|
4131
|
+
const stat3 = statSync2(filePath);
|
|
4132
|
+
if (stat3.isFile()) {
|
|
2560
4133
|
const ext = extname2(filePath).toLowerCase();
|
|
2561
4134
|
const mime = MIME_TYPES[ext] || "application/octet-stream";
|
|
2562
4135
|
const content = readFileSync3(filePath);
|
|
@@ -2701,13 +4274,13 @@ wss.on("connection", (ws, req) => {
|
|
|
2701
4274
|
console.log("[Seren Local] Browser disconnected");
|
|
2702
4275
|
});
|
|
2703
4276
|
});
|
|
2704
|
-
var dataDir =
|
|
4277
|
+
var dataDir = join9(homedir8(), ".seren-local");
|
|
2705
4278
|
mkdirSync2(dataDir, { recursive: true });
|
|
2706
|
-
initChatDb(
|
|
4279
|
+
initChatDb(join9(dataDir, "conversations.db"));
|
|
2707
4280
|
registerAllHandlers();
|
|
2708
4281
|
httpServer.listen(PORT, "127.0.0.1", () => {
|
|
2709
4282
|
const url = `http://127.0.0.1:${PORT}`;
|
|
2710
|
-
const hasSpa = existsSync3(
|
|
4283
|
+
const hasSpa = existsSync3(join9(PUBLIC_DIR, "index.html"));
|
|
2711
4284
|
console.log(`[Seren Local] Listening on ${url}`);
|
|
2712
4285
|
if (hasSpa) {
|
|
2713
4286
|
console.log(`[Seren Local] Serving app at ${url}`);
|