@serendb/serendesktop 0.1.20 → 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 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 homedir6, platform as platform4 } from "os";
8
- import { join as join7, extname as extname2 } from "path";
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 launchClaudeLogin() {
224
- const os2 = platform();
1329
+ function launchLoginCommand(command) {
1330
+ const loginCommand = `${command} login`;
1331
+ const currentPlatform = platform();
225
1332
  try {
226
- if (os2 === "darwin") {
1333
+ if (currentPlatform === "darwin") {
227
1334
  spawn("osascript", [
228
1335
  "-e",
229
- 'tell application "Terminal" to do script "claude login"',
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 (os2 === "win32") {
234
- spawn("cmd", ["/c", "start", "cmd", "/c", "claude login"], {
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", "claude", "login"], {
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((resolve4, reject) => {
261
- session.pendingPermissions.set(requestId, resolve4);
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 readFile(params.path, "utf-8");
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 readFile(params.path, "utf-8");
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((resolve4, reject) => {
295
- session.pendingDiffProposals.set(proposalId, resolve4);
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 home2 = process.env.HOME ?? "~";
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(home2, ".seren-local/bin", binName),
1650
+ resolve(home3, ".seren-local/bin", binName),
390
1651
  // 3. Seren Desktop embedded-runtime (development)
391
- resolve(home2, "Projects/Seren_Projects/seren-desktop/src-tauri/embedded-runtime/bin", binName)
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(home2, ".seren-local/bin", legacyName),
398
- resolve(home2, "Projects/Seren_Projects/seren-desktop/src-tauri/embedded-runtime/bin", legacyName)
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((resolve4) => {
415
- execFile(which, [command], (err) => resolve4(!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) launchClaudeLogin();
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.kill();
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
- { type: "claude-code", name: "Claude Code", description: "AI coding assistant by Anthropic", command: "seren-acp-claude" },
625
- { type: "codex", name: "Codex", description: "AI coding assistant powered by OpenAI Codex", command: "seren-acp-codex" }
626
- ];
627
- return agents.map((agent) => {
628
- let available = false;
629
- let unavailableReason;
630
- try {
631
- findAgentCommand(agent.type);
632
- available = true;
633
- } catch (err) {
634
- unavailableReason = err.message;
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
- return { ...agent, available, unavailableReason };
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
- if (await isCommandAvailable("claude")) {
649
- return "claude";
650
- }
651
- const npmCmd = platform() === "win32" ? "npm.cmd" : "npm";
652
- return new Promise((resolve4, reject) => {
653
- const proc = execFile(
654
- npmCmd,
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((resolve4, reject) => {
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
- resolve4("");
1991
+ resolve5("");
683
1992
  return;
684
1993
  }
685
1994
  reject(err);
686
1995
  return;
687
1996
  }
688
- resolve4(stdout.trim());
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 = homedir();
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: join(dir, entry.name),
2128
+ path: join2(dir, entry.name),
820
2129
  is_directory: entry.isDirectory()
821
2130
  }));
822
2131
  }
823
- async function readFile2(params) {
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 join2 } from "path";
878
- import { homedir as homedir2 } from "os";
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 join2(homedir2(), ".seren-local", "data");
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 join2(getDataDir(), "indexes", `${hash}.db`);
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 = join2(getDataDir(), "indexes");
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 join3, extname, relative } from "path";
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 = readdirSync(current);
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 = join3(current, name);
1158
- let stat2;
2466
+ const fullPath = join4(current, name);
2467
+ let stat3;
1159
2468
  try {
1160
- stat2 = statSync(fullPath);
2469
+ stat3 = statSync(fullPath);
1161
2470
  } catch {
1162
2471
  continue;
1163
2472
  }
1164
- if (stat2.isDirectory()) {
2473
+ if (stat3.isDirectory()) {
1165
2474
  discoverRecursive(root, fullPath, files);
1166
- } else if (stat2.isFile()) {
2475
+ } else if (stat3.isFile()) {
1167
2476
  const language = detectLanguage(fullPath);
1168
2477
  if (!language) continue;
1169
- if (stat2.size > MAX_FILE_SIZE) continue;
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: stat2.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((resolve4, reject) => {
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
- resolve4(v);
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 readFile3,
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 join4 } from "path";
1492
- import { homedir as homedir3 } from "os";
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 = join4(homedir3(), ".openclaw");
1509
- var CONFIG_PATH = join4(OPENCLAW_DIR, "openclaw.json");
1510
- var SETTINGS_PATH = join4(homedir3(), ".seren-local", "settings.json");
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 readFile3(SETTINGS_PATH, "utf-8");
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(join4(homedir3(), ".seren-local"), { recursive: true });
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((resolve4, reject) => {
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(() => resolve4(p));
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 readFile3(CONFIG_PATH, "utf-8"));
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 readFile3(CONFIG_PATH, "utf-8"));
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
- join4(homedir3(), ".seren-local", "lib", "node_modules", "openclaw", "openclaw.mjs"),
2882
+ join5(homedir4(), ".seren-local", "lib", "node_modules", "openclaw", "openclaw.mjs"),
1574
2883
  // Development
1575
- join4(homedir3(), ".openclaw", "openclaw.mjs")
2884
+ join5(homedir4(), ".openclaw", "openclaw.mjs")
1576
2885
  ];
1577
2886
  try {
1578
2887
  const cmd = process.platform === "win32" ? "where" : "which";
1579
- const path = await new Promise((resolve4, reject) => {
2888
+ const path2 = await new Promise((resolve5, reject) => {
1580
2889
  execFile3(cmd, ["openclaw"], (err, stdout) => {
1581
2890
  if (err) reject(err);
1582
- else resolve4(stdout.trim());
2891
+ else resolve5(stdout.trim());
1583
2892
  });
1584
2893
  });
1585
- if (path) candidates.unshift(path);
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((resolve4) => setTimeout(resolve4, 1e3));
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((resolve4) => setTimeout(resolve4, 500));
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((resolve4) => {
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
- resolve4([]);
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
- resolve4(result);
3103
+ resolve5(result);
1795
3104
  } catch {
1796
- resolve4([]);
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 readFile3(CONFIG_PATH, "utf-8"));
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((resolve4, reject) => {
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 resolve4();
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((resolve4, reject) => {
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
- resolve4(data.qr || data.qrCode || stdout.trim());
3248
+ resolve5(data.qr || data.qrCode || stdout.trim());
1940
3249
  } catch {
1941
- resolve4(stdout.trim());
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 homedir4 } from "os";
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 home2 = homedir4();
2048
- const serenDir = resolve3(home2, ".seren-local");
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 readFile4, writeFile as writeFile4, mkdir as mkdir3 } from "fs/promises";
2098
- import { join as join5 } from "path";
2099
- import { homedir as homedir5 } from "os";
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 = join5(homedir5(), ".seren-local");
2113
- var WALLET_FILE = join5(SEREN_DIR, "data", "crypto-wallet.json");
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 readFile4(WALLET_FILE, "utf-8");
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 mkdir3(join5(SEREN_DIR, "data"), { recursive: true });
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, ...path) {
3764
+ function getExtra(option, ...path2) {
2214
3765
  let current = option.extra;
2215
- for (const key of path) {
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", readFile2);
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 join6, dirname as dirname2 } from "path";
4015
+ import { join as join8, dirname as dirname3 } from "path";
2443
4016
  import { fileURLToPath } from "url";
2444
- var __dirname = dirname2(fileURLToPath(import.meta.url));
4017
+ var __dirname = dirname3(fileURLToPath(import.meta.url));
2445
4018
  function getInstalledVersion() {
2446
4019
  try {
2447
4020
  const pkg2 = JSON.parse(
2448
- readFileSync2(join6(__dirname, "..", "package.json"), "utf-8")
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 = join7(__dirname2, "..", "public");
4074
+ var PUBLIC_DIR = join9(__dirname2, "..", "public");
2502
4075
  function computeBuildHash() {
2503
4076
  try {
2504
- const indexPath = join7(PUBLIC_DIR, "index.html");
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 = join7(PUBLIC_DIR, "index.html");
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 = join7(PUBLIC_DIR, safePath);
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 stat2 = statSync2(filePath);
2559
- if (stat2.isFile()) {
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 = join7(homedir6(), ".seren-local");
4277
+ var dataDir = join9(homedir8(), ".seren-local");
2705
4278
  mkdirSync2(dataDir, { recursive: true });
2706
- initChatDb(join7(dataDir, "conversations.db"));
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(join7(PUBLIC_DIR, "index.html"));
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}`);