mini-coder 0.3.1 → 0.4.0

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/mc.js CHANGED
@@ -13,7 +13,7 @@ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
13
13
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
14
14
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
15
15
  // src/internal/version.ts
16
- var PACKAGE_VERSION = "0.3.1";
16
+ var PACKAGE_VERSION = "0.4.0";
17
17
 
18
18
  // src/mcp/client.ts
19
19
  async function connectMcpServer(config) {
@@ -68,6 +68,43 @@ async function connectMcpServer(config) {
68
68
  };
69
69
  }
70
70
 
71
+ // src/mcp/client-registry.ts
72
+ class McpClientRegistry {
73
+ clients = new Set;
74
+ add(client) {
75
+ this.clients.add(client);
76
+ }
77
+ async closeAll() {
78
+ const clients = Array.from(this.clients);
79
+ this.clients.clear();
80
+ await Promise.allSettled(clients.map((client) => client.close()));
81
+ }
82
+ }
83
+
84
+ // src/mcp/tool-sync.ts
85
+ function removeMcpTools(tools, mcpTools) {
86
+ for (const tool of mcpTools) {
87
+ const index = tools.indexOf(tool);
88
+ if (index >= 0)
89
+ tools.splice(index, 1);
90
+ }
91
+ mcpTools.length = 0;
92
+ }
93
+ async function reconnectMcpTools(opts) {
94
+ await opts.clients.closeAll();
95
+ removeMcpTools(opts.tools, opts.mcpTools);
96
+ for (const name of opts.names) {
97
+ try {
98
+ const client = await opts.connectByName(name);
99
+ opts.clients.add(client);
100
+ opts.tools.push(...client.tools);
101
+ opts.mcpTools.push(...client.tools);
102
+ } catch (error) {
103
+ opts.onError?.(name, error);
104
+ }
105
+ }
106
+ }
107
+
71
108
  // src/session/db/connection.ts
72
109
  import { Database } from "bun:sqlite";
73
110
  import { existsSync, mkdirSync, renameSync } from "fs";
@@ -342,336 +379,96 @@ function upsertMcpServer(server) {
342
379
  function deleteMcpServer(name) {
343
380
  getDb().run("DELETE FROM mcp_servers WHERE name = ?", [name]);
344
381
  }
345
- // src/cli/output.ts
346
- import { homedir as homedir4 } from "os";
347
- import * as c7 from "yoctocolors";
348
-
349
- // src/agent/context-files.ts
350
- import { existsSync as existsSync2, readFileSync } from "fs";
351
- import { homedir as homedir2 } from "os";
352
- import { dirname, join as join2, resolve } from "path";
353
- var HOME = homedir2();
354
- function tilde(p) {
355
- return p.startsWith(HOME) ? `~${p.slice(HOME.length)}` : p;
356
- }
357
- function globalContextCandidates(homeDir = HOME) {
358
- return [
359
- {
360
- abs: join2(homeDir, ".agents", "AGENTS.md"),
361
- label: "~/.agents/AGENTS.md"
362
- },
363
- {
364
- abs: join2(homeDir, ".agents", "CLAUDE.md"),
365
- label: "~/.agents/CLAUDE.md"
366
- },
367
- {
368
- abs: join2(homeDir, ".claude", "CLAUDE.md"),
369
- label: "~/.claude/CLAUDE.md"
370
- }
371
- ];
382
+ // src/logging/context.ts
383
+ var _currentContext = null;
384
+ function setLogContext(context) {
385
+ _currentContext = context;
372
386
  }
373
- function dirContextCandidates(dir) {
374
- const rel = (p) => tilde(resolve(dir, p));
375
- return [
376
- {
377
- abs: join2(dir, ".agents", "AGENTS.md"),
378
- label: `${rel(".agents/AGENTS.md")}`
379
- },
380
- {
381
- abs: join2(dir, ".agents", "CLAUDE.md"),
382
- label: `${rel(".agents/CLAUDE.md")}`
383
- },
384
- {
385
- abs: join2(dir, ".claude", "CLAUDE.md"),
386
- label: `${rel(".claude/CLAUDE.md")}`
387
- },
388
- { abs: join2(dir, "CLAUDE.md"), label: `${rel("CLAUDE.md")}` },
389
- { abs: join2(dir, "AGENTS.md"), label: `${rel("AGENTS.md")}` }
390
- ];
387
+ function getLogContext() {
388
+ return _currentContext;
391
389
  }
392
- function discoverContextFiles(cwd, homeDir) {
393
- const candidates = [
394
- ...globalContextCandidates(homeDir),
395
- ...dirContextCandidates(cwd)
396
- ];
397
- return candidates.filter((c) => existsSync2(c.abs)).map((c) => c.label);
390
+ function logError(err, context) {
391
+ const logCtx = _currentContext;
392
+ if (!logCtx)
393
+ return;
394
+ logCtx.logsRepo.write(logCtx.sessionId, "error", { error: err, context });
398
395
  }
399
- function tryReadFile(p) {
400
- if (!existsSync2(p))
401
- return null;
402
- try {
403
- return readFileSync(p, "utf-8");
404
- } catch {
405
- return null;
406
- }
396
+ function logApiEvent(event, data) {
397
+ const logCtx = _currentContext;
398
+ if (!logCtx)
399
+ return;
400
+ logCtx.logsRepo.write(logCtx.sessionId, "api", { event, data });
407
401
  }
408
- function readCandidates(candidates) {
409
- const parts = [];
410
- for (const c of candidates) {
411
- const content = tryReadFile(c.abs);
412
- if (content)
413
- parts.push(content);
414
- }
415
- return parts.length > 0 ? parts.join(`
416
402
 
417
- `) : null;
418
- }
419
- function loadGlobalContextFile(homeDir) {
420
- return readCandidates(globalContextCandidates(homeDir));
403
+ // src/session/db/message-repo.ts
404
+ var _insertMsgStmt = null;
405
+ var _addPromptHistoryStmt = null;
406
+ var _getPromptHistoryStmt = null;
407
+ function getInsertMsgStmt() {
408
+ if (!_insertMsgStmt) {
409
+ _insertMsgStmt = getDb().prepare(`INSERT INTO messages (session_id, payload, turn_index, created_at)
410
+ VALUES (?, ?, ?, ?)`);
411
+ }
412
+ return _insertMsgStmt;
421
413
  }
422
- function loadLocalContextFile(cwd) {
423
- let current = resolve(cwd);
424
- while (true) {
425
- const content = readCandidates(dirContextCandidates(current));
426
- if (content)
427
- return content;
428
- if (existsSync2(join2(current, ".git")))
429
- break;
430
- const parent = dirname(current);
431
- if (parent === current)
432
- break;
433
- current = parent;
414
+ function getAddPromptHistoryStmt() {
415
+ if (!_addPromptHistoryStmt) {
416
+ _addPromptHistoryStmt = getDb().prepare("INSERT INTO prompt_history (text, created_at) VALUES (?, ?)");
434
417
  }
435
- return null;
418
+ return _addPromptHistoryStmt;
436
419
  }
437
-
438
- // src/llm-api/providers.ts
439
- import { createAnthropic } from "@ai-sdk/anthropic";
440
- import { createGoogleGenerativeAI } from "@ai-sdk/google";
441
- import { createOpenAI } from "@ai-sdk/openai";
442
- import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
443
-
444
- // src/session/oauth/openai.ts
445
- import { createServer } from "http";
446
-
447
- // src/session/oauth/pkce.ts
448
- function base64urlEncode(bytes) {
449
- let binary = "";
450
- for (const byte of bytes) {
451
- binary += String.fromCharCode(byte);
420
+ function getPromptHistoryStmt() {
421
+ if (!_getPromptHistoryStmt) {
422
+ _getPromptHistoryStmt = getDb().prepare("SELECT text FROM prompt_history ORDER BY id DESC LIMIT ?");
452
423
  }
453
- return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
424
+ return _getPromptHistoryStmt;
454
425
  }
455
- async function generatePKCE() {
456
- const verifierBytes = new Uint8Array(32);
457
- crypto.getRandomValues(verifierBytes);
458
- const verifier = base64urlEncode(verifierBytes);
459
- const data = new TextEncoder().encode(verifier);
460
- const hashBuffer = await crypto.subtle.digest("SHA-256", data);
461
- const challenge = base64urlEncode(new Uint8Array(hashBuffer));
462
- return { verifier, challenge };
426
+ function saveMessages(sessionId, msgs, turnIndex = 0) {
427
+ const db = getDb();
428
+ const stmt = getInsertMsgStmt();
429
+ const now = Date.now();
430
+ db.transaction(() => {
431
+ for (const msg of msgs) {
432
+ stmt.run(sessionId, JSON.stringify(msg), turnIndex, now);
433
+ }
434
+ })();
463
435
  }
464
-
465
- // src/session/oauth/openai.ts
466
- var CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
467
- var AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize";
468
- var TOKEN_URL = "https://auth.openai.com/oauth/token";
469
- var CALLBACK_HOST = "127.0.0.1";
470
- var CALLBACK_PORT = 1455;
471
- var CALLBACK_PATH = "/auth/callback";
472
- var REDIRECT_URI = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
473
- var SCOPES = "openid profile email offline_access";
474
- var LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
475
- var SUCCESS_HTML = `<!doctype html>
476
- <html lang="en"><head><meta charset="utf-8"><title>Authenticated</title></head>
477
- <body><p>OpenAI authentication successful. Return to your terminal.</p></body></html>`;
478
- function createState() {
479
- const bytes = new Uint8Array(16);
480
- crypto.getRandomValues(bytes);
481
- let hex = "";
482
- for (const b of bytes)
483
- hex += b.toString(16).padStart(2, "0");
484
- return hex;
436
+ function getMaxTurnIndex(sessionId) {
437
+ const row = getDb().query("SELECT MAX(turn_index) AS max_turn FROM messages WHERE session_id = ?").get(sessionId);
438
+ return row?.max_turn ?? -1;
485
439
  }
486
- function startCallbackServer(expectedState) {
487
- return new Promise((resolve2, reject) => {
488
- let result = null;
489
- let cancelled = false;
490
- const server = createServer((req, res) => {
491
- const url = new URL(req.url ?? "", "http://localhost");
492
- if (url.pathname !== CALLBACK_PATH) {
493
- res.writeHead(404).end("Not found");
494
- return;
495
- }
496
- const code = url.searchParams.get("code");
497
- const state = url.searchParams.get("state");
498
- const error = url.searchParams.get("error");
499
- if (error || !code || !state || state !== expectedState) {
500
- res.writeHead(400).end("Authentication failed.");
501
- return;
502
- }
503
- res.writeHead(200, { "Content-Type": "text/html" }).end(SUCCESS_HTML);
504
- result = { code };
505
- });
506
- server.on("error", reject);
507
- server.listen(CALLBACK_PORT, CALLBACK_HOST, () => {
508
- resolve2({
509
- server,
510
- cancel: () => {
511
- cancelled = true;
512
- },
513
- waitForCode: async () => {
514
- const deadline = Date.now() + LOGIN_TIMEOUT_MS;
515
- while (!result && !cancelled && Date.now() < deadline) {
516
- await new Promise((r) => setTimeout(r, 100));
517
- }
518
- return result;
519
- }
520
- });
521
- });
522
- });
440
+ function deleteLastTurn(sessionId, turnIndex) {
441
+ const target = turnIndex !== undefined ? turnIndex : getMaxTurnIndex(sessionId);
442
+ if (target < 0)
443
+ return false;
444
+ getDb().run("DELETE FROM messages WHERE session_id = ? AND turn_index = ?", [
445
+ sessionId,
446
+ target
447
+ ]);
448
+ return true;
523
449
  }
524
- async function postTokenRequest(body, label) {
525
- const res = await fetch(TOKEN_URL, {
526
- method: "POST",
527
- headers: {
528
- "Content-Type": "application/x-www-form-urlencoded"
529
- },
530
- body,
531
- signal: AbortSignal.timeout(30000)
532
- });
533
- if (!res.ok) {
534
- const text = await res.text();
535
- throw new Error(`${label} failed (${res.status}): ${text}`);
450
+ function loadMessages(sessionId) {
451
+ const rows = getDb().query("SELECT payload, id FROM messages WHERE session_id = ? ORDER BY id ASC").all(sessionId);
452
+ const messages = [];
453
+ for (const row of rows) {
454
+ try {
455
+ messages.push(JSON.parse(row.payload));
456
+ } catch (_err) {
457
+ logError(new Error(`Failed to parse message ID ${row.id} for session ${sessionId}`), "message-repo.loadMessages");
458
+ }
536
459
  }
537
- const data = await res.json();
538
- return {
539
- access: data.access_token,
540
- refresh: data.refresh_token,
541
- expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000
542
- };
460
+ return messages;
543
461
  }
544
- function exchangeCode(code, verifier) {
545
- return postTokenRequest(new URLSearchParams({
546
- grant_type: "authorization_code",
547
- client_id: CLIENT_ID,
548
- code,
549
- code_verifier: verifier,
550
- redirect_uri: REDIRECT_URI
551
- }), "Token exchange");
552
- }
553
- function refreshOpenAIToken(refreshToken) {
554
- return postTokenRequest(new URLSearchParams({
555
- grant_type: "refresh_token",
556
- refresh_token: refreshToken,
557
- client_id: CLIENT_ID
558
- }), "Token refresh");
559
- }
560
- async function loginOpenAI(callbacks) {
561
- const { verifier, challenge } = await generatePKCE();
562
- const state = createState();
563
- const cb = await startCallbackServer(state);
564
- try {
565
- const params = new URLSearchParams({
566
- response_type: "code",
567
- client_id: CLIENT_ID,
568
- redirect_uri: REDIRECT_URI,
569
- scope: SCOPES,
570
- code_challenge: challenge,
571
- code_challenge_method: "S256",
572
- state,
573
- id_token_add_organizations: "true",
574
- codex_cli_simplified_flow: "true",
575
- originator: "mc"
576
- });
577
- callbacks.onOpenUrl(`${AUTHORIZE_URL}?${params}`, "Complete login in your browser.");
578
- const result = await cb.waitForCode();
579
- if (!result)
580
- throw new Error("Login cancelled or no code received");
581
- callbacks.onProgress("Exchanging authorization code for tokens\u2026");
582
- return exchangeCode(result.code, verifier);
583
- } finally {
584
- cb.server.close();
585
- }
586
- }
587
- function extractAccountId(accessToken) {
588
- try {
589
- const parts = accessToken.split(".");
590
- const jwt = parts[1];
591
- if (parts.length !== 3 || !jwt)
592
- return null;
593
- const payload = JSON.parse(atob(jwt));
594
- const auth = payload["https://api.openai.com/auth"];
595
- return auth?.chatgpt_account_id ?? null;
596
- } catch {
597
- return null;
598
- }
599
- }
600
- var openaiOAuth = {
601
- id: "openai",
602
- name: "OpenAI (ChatGPT Plus/Pro)",
603
- login: loginOpenAI,
604
- refreshToken: refreshOpenAIToken
605
- };
606
-
607
- // src/session/oauth/auth-storage.ts
608
- var PROVIDERS = new Map([
609
- [openaiOAuth.id, openaiOAuth]
610
- ]);
611
- function getOAuthProviders() {
612
- return [...PROVIDERS.values()];
613
- }
614
- function getOAuthProvider(id) {
615
- return PROVIDERS.get(id);
616
- }
617
- function getStoredToken(provider) {
618
- return getDb().query("SELECT provider, access_token, refresh_token, expires_at, updated_at FROM oauth_tokens WHERE provider = ?").get(provider) ?? null;
619
- }
620
- function upsertToken(provider, creds) {
621
- getDb().run(`INSERT INTO oauth_tokens (provider, access_token, refresh_token, expires_at, updated_at)
622
- VALUES (?, ?, ?, ?, ?)
623
- ON CONFLICT(provider) DO UPDATE SET
624
- access_token = excluded.access_token,
625
- refresh_token = excluded.refresh_token,
626
- expires_at = excluded.expires_at,
627
- updated_at = excluded.updated_at`, [provider, creds.access, creds.refresh, creds.expires, Date.now()]);
628
- }
629
- function deleteToken(provider) {
630
- getDb().run("DELETE FROM oauth_tokens WHERE provider = ?", [provider]);
631
- }
632
- function isAuthError(err) {
633
- if (!(err instanceof Error))
634
- return false;
635
- return /\b(401|403)\b/.test(err.message);
636
- }
637
- function isLoggedIn(provider) {
638
- return PROVIDERS.has(provider) && getStoredToken(provider) !== null;
639
- }
640
- function listLoggedInProviders() {
641
- return getDb().query("SELECT provider FROM oauth_tokens ORDER BY provider").all().map((r) => r.provider).filter((provider) => PROVIDERS.has(provider));
642
- }
643
- async function login(providerId, callbacks) {
644
- const provider = PROVIDERS.get(providerId);
645
- if (!provider)
646
- throw new Error(`Unknown OAuth provider: ${providerId}`);
647
- const creds = await provider.login(callbacks);
648
- upsertToken(providerId, creds);
649
- }
650
- function logout(providerId) {
651
- deleteToken(providerId);
462
+ function addPromptHistory(text) {
463
+ const trimmed = text.trim();
464
+ if (!trimmed)
465
+ return;
466
+ getAddPromptHistoryStmt().run(trimmed, Date.now());
652
467
  }
653
- async function getAccessToken(providerId) {
654
- const row = getStoredToken(providerId);
655
- if (!row)
656
- return null;
657
- if (Date.now() < row.expires_at) {
658
- return row.access_token;
659
- }
660
- const provider = PROVIDERS.get(providerId);
661
- if (!provider)
662
- return null;
663
- try {
664
- const refreshed = await provider.refreshToken(row.refresh_token);
665
- upsertToken(providerId, refreshed);
666
- return refreshed.access;
667
- } catch (err) {
668
- if (isAuthError(err)) {
669
- deleteToken(providerId);
670
- }
671
- return null;
672
- }
468
+ function getPromptHistory(limit = 200) {
469
+ const rows = getPromptHistoryStmt().all(limit);
470
+ return rows.map((r) => r.text).reverse();
673
471
  }
674
-
675
472
  // src/session/db/model-info-repo.ts
676
473
  function listModelCapabilities() {
677
474
  return getDb().query("SELECT canonical_model_id, context_window, max_output_tokens, reasoning, source_provider, raw_json, updated_at FROM model_capabilities").all();
@@ -730,1176 +527,1241 @@ function setModelInfoState(key, value) {
730
527
  function listModelInfoState() {
731
528
  return getDb().query("SELECT key, value FROM model_info_state").all();
732
529
  }
733
-
734
- // src/llm-api/history/shared.ts
735
- function isRecord(value) {
736
- return value !== null && typeof value === "object";
737
- }
738
- function normalizeProviderOptions(part) {
739
- if (!isRecord(part))
740
- return part;
741
- if (part.providerOptions !== undefined || part.providerMetadata === undefined) {
742
- return part;
530
+ // src/session/db/session-repo.ts
531
+ function createSession(opts) {
532
+ const db = getDb();
533
+ const now = Date.now();
534
+ db.run(`INSERT INTO sessions (id, title, cwd, model, created_at, updated_at)
535
+ VALUES (?, ?, ?, ?, ?, ?)`, [opts.id, opts.title ?? "", opts.cwd, opts.model, now, now]);
536
+ const session = getSession(opts.id);
537
+ if (!session) {
538
+ throw new Error(`Failed to create session ${opts.id}`);
743
539
  }
744
- return {
745
- ...part,
746
- providerOptions: part.providerMetadata
747
- };
540
+ return session;
748
541
  }
749
- function normalizeMessageProviderOptions(message) {
750
- if (!Array.isArray(message.content))
751
- return message;
752
- return {
753
- ...message,
754
- content: message.content.map((part) => normalizeProviderOptions(part))
755
- };
542
+ function getSession(id) {
543
+ return getDb().query("SELECT * FROM sessions WHERE id = ?").get(id) ?? null;
756
544
  }
757
- function getPartProviderOptions(part) {
758
- if (!isRecord(part))
759
- return null;
760
- if (isRecord(part.providerOptions))
761
- return part.providerOptions;
762
- if (isRecord(part.providerMetadata))
763
- return part.providerMetadata;
764
- return null;
545
+ function touchSession(id, model) {
546
+ getDb().run("UPDATE sessions SET updated_at = ?, model = ? WHERE id = ?", [
547
+ Date.now(),
548
+ model,
549
+ id
550
+ ]);
765
551
  }
766
- function isToolCallPart(part) {
767
- return isRecord(part) && part.type === "tool-call";
552
+ function setSessionTitle(id, title) {
553
+ getDb().run("UPDATE sessions SET title = ? WHERE id = ? AND title = ''", [
554
+ title,
555
+ id
556
+ ]);
768
557
  }
769
- function hasObjectToolCallInput(part) {
770
- return isToolCallPart(part) && "input" in part && isRecord(part.input) && !Array.isArray(part.input);
558
+ function listSessions(limit = 20) {
559
+ return getDb().query("SELECT * FROM sessions ORDER BY updated_at DESC LIMIT ?").all(limit);
771
560
  }
772
- function mapAssistantParts(messages, transform) {
773
- let mutated = false;
774
- const result = messages.map((message) => {
775
- if (message.role !== "assistant" || !Array.isArray(message.content)) {
776
- return message;
777
- }
778
- let contentMutated = false;
779
- const nextContent = message.content.map((part) => {
780
- const next = transform(part);
781
- if (next !== part)
782
- contentMutated = true;
783
- return next;
784
- });
785
- if (!contentMutated)
786
- return message;
787
- mutated = true;
788
- return {
789
- ...message,
790
- content: nextContent
791
- };
792
- });
793
- return mutated ? result : messages;
561
+ function generateSessionId() {
562
+ const ts = Date.now().toString(36);
563
+ const rand = Math.random().toString(36).slice(2, 7);
564
+ return `${ts}-${rand}`;
794
565
  }
795
- var TOOL_RUNTIME_INPUT_KEYS = new Set(["cwd"]);
796
- function stripToolRuntimeInputFields(messages) {
797
- return mapAssistantParts(messages, (part) => {
798
- if (!hasObjectToolCallInput(part))
799
- return part;
800
- let inputMutated = false;
801
- const nextInput = { ...part.input };
802
- for (const key of TOOL_RUNTIME_INPUT_KEYS) {
803
- if (!(key in nextInput))
804
- continue;
805
- delete nextInput[key];
806
- inputMutated = true;
807
- }
808
- return inputMutated ? { ...part, input: nextInput } : part;
809
- });
566
+ // src/session/db/settings-repo.ts
567
+ function getSetting(key) {
568
+ const row = getDb().query("SELECT value FROM settings WHERE key = ?").get(key);
569
+ return row?.value ?? null;
810
570
  }
811
-
812
- // src/llm-api/model-info-normalize.ts
813
- function basename(value) {
814
- const idx = value.lastIndexOf("/");
815
- return idx === -1 ? value : value.slice(idx + 1);
571
+ function setSetting(key, value) {
572
+ getDb().run(`INSERT INTO settings (key, value) VALUES (?, ?)
573
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value`, [key, value]);
816
574
  }
817
- function normalizeModelId(modelId) {
818
- let out = modelId.trim().toLowerCase();
819
- while (out.startsWith("models/")) {
820
- out = out.slice("models/".length);
575
+ function parseBooleanSetting(value, fallback) {
576
+ if (value === null)
577
+ return fallback;
578
+ const normalized = value.trim().toLowerCase();
579
+ if (normalized === "true" || normalized === "on" || normalized === "1") {
580
+ return true;
821
581
  }
822
- return out;
582
+ if (normalized === "false" || normalized === "off" || normalized === "0") {
583
+ return false;
584
+ }
585
+ return fallback;
823
586
  }
824
- function parseContextWindow(model) {
825
- const limit = model.limit;
826
- if (!isRecord(limit))
827
- return null;
828
- const context = limit.context;
829
- if (typeof context !== "number" || !Number.isFinite(context))
830
- return null;
831
- return Math.max(0, Math.trunc(context));
587
+ function getPreferredModel() {
588
+ return getSetting("preferred_model");
832
589
  }
833
- function parseMaxOutputTokens(model) {
834
- const limit = model.limit;
835
- if (!isRecord(limit))
836
- return null;
837
- const output = limit.output;
838
- if (typeof output !== "number" || !Number.isFinite(output))
839
- return null;
840
- return Math.max(0, Math.trunc(output));
590
+ function setPreferredModel(model) {
591
+ setSetting("preferred_model", model);
841
592
  }
842
- function parseModelsDevCapabilities(payload, updatedAt) {
843
- if (!isRecord(payload))
844
- return [];
845
- const merged = new Map;
846
- for (const [provider, providerValue] of Object.entries(payload)) {
847
- if (!isRecord(providerValue))
848
- continue;
849
- const models = providerValue.models;
850
- if (!isRecord(models))
851
- continue;
852
- for (const [modelKey, modelValue] of Object.entries(models)) {
853
- if (!isRecord(modelValue))
854
- continue;
855
- const explicitId = typeof modelValue.id === "string" && modelValue.id.trim().length > 0 ? modelValue.id : modelKey;
856
- const canonicalModelId = normalizeModelId(explicitId);
857
- if (!canonicalModelId)
858
- continue;
859
- const contextWindow = parseContextWindow(modelValue);
860
- const maxOutputTokens = parseMaxOutputTokens(modelValue);
861
- const reasoning = modelValue.reasoning === true;
862
- const rawJson = JSON.stringify(modelValue);
863
- const prev = merged.get(canonicalModelId);
864
- if (!prev) {
865
- merged.set(canonicalModelId, {
866
- canonicalModelId,
867
- contextWindow,
868
- maxOutputTokens,
869
- reasoning,
870
- sourceProvider: provider,
871
- rawJson
872
- });
873
- continue;
874
- }
875
- merged.set(canonicalModelId, {
876
- canonicalModelId,
877
- contextWindow: prev.contextWindow ?? contextWindow,
878
- maxOutputTokens: prev.maxOutputTokens ?? maxOutputTokens,
879
- reasoning: prev.reasoning || reasoning,
880
- sourceProvider: prev.sourceProvider,
881
- rawJson: prev.rawJson ?? rawJson
882
- });
883
- }
593
+ function getPreferredThinkingEffort() {
594
+ const v = getSetting("preferred_thinking_effort");
595
+ if (v === "low" || v === "medium" || v === "high" || v === "xhigh")
596
+ return v;
597
+ return null;
598
+ }
599
+ function setPreferredThinkingEffort(effort) {
600
+ if (effort === null) {
601
+ getDb().run("DELETE FROM settings WHERE key = 'preferred_thinking_effort'");
602
+ } else {
603
+ setSetting("preferred_thinking_effort", effort);
884
604
  }
885
- return Array.from(merged.values()).map((entry) => ({
886
- canonical_model_id: entry.canonicalModelId,
887
- context_window: entry.contextWindow,
888
- max_output_tokens: entry.maxOutputTokens,
889
- reasoning: entry.reasoning ? 1 : 0,
890
- source_provider: entry.sourceProvider,
891
- raw_json: entry.rawJson,
892
- updated_at: updatedAt
893
- }));
894
605
  }
895
- function buildModelMatchIndex(canonicalModelIds) {
896
- const exact = new Map;
897
- const aliasCandidates = new Map;
898
- for (const rawCanonical of canonicalModelIds) {
899
- const canonical = normalizeModelId(rawCanonical);
900
- if (!canonical)
901
- continue;
902
- exact.set(canonical, canonical);
903
- const short = basename(canonical);
904
- if (!short)
905
- continue;
906
- let set = aliasCandidates.get(short);
907
- if (!set) {
908
- set = new Set;
909
- aliasCandidates.set(short, set);
910
- }
911
- set.add(canonical);
606
+ function getPreferredShowReasoning() {
607
+ return parseBooleanSetting(getSetting("preferred_show_reasoning"), false);
608
+ }
609
+ function setPreferredShowReasoning(show) {
610
+ setSetting("preferred_show_reasoning", show ? "true" : "false");
611
+ }
612
+ function getPreferredVerboseOutput() {
613
+ return parseBooleanSetting(getSetting("preferred_verbose_output"), false);
614
+ }
615
+ function setPreferredVerboseOutput(verbose) {
616
+ setSetting("preferred_verbose_output", verbose ? "true" : "false");
617
+ }
618
+ // src/agent/session-runner.ts
619
+ import * as c10 from "yoctocolors";
620
+
621
+ // src/cli/input.ts
622
+ import * as c8 from "yoctocolors";
623
+
624
+ // src/cli/input-buffer.ts
625
+ var PASTE_TOKEN_START = 57344;
626
+ var PASTE_TOKEN_END = 63743;
627
+ function createPasteToken(buf, pasteTokens) {
628
+ for (let code = PASTE_TOKEN_START;code <= PASTE_TOKEN_END; code++) {
629
+ const token = String.fromCharCode(code);
630
+ if (!buf.includes(token) && !pasteTokens.has(token))
631
+ return token;
912
632
  }
913
- const alias = new Map;
914
- for (const [short, candidates] of aliasCandidates) {
915
- if (candidates.size === 1) {
916
- for (const value of candidates) {
917
- alias.set(short, value);
918
- }
919
- } else {
920
- alias.set(short, null);
921
- }
633
+ throw new Error("Too many pasted chunks in a single prompt");
634
+ }
635
+ function pasteLabel(text) {
636
+ const lines = text.split(`
637
+ `);
638
+ const first = lines[0] ?? "";
639
+ const preview = first.length > 40 ? `${first.slice(0, 40)}\u2026` : first;
640
+ const extra = lines.length - 1;
641
+ const more = extra > 0 ? ` +${extra} more line${extra === 1 ? "" : "s"}` : "";
642
+ return `[pasted: "${preview}"${more}]`;
643
+ }
644
+ function processInputBuffer(buf, pasteTokens, replacer) {
645
+ let out = "";
646
+ for (let i = 0;i < buf.length; i++) {
647
+ const ch = buf[i] ?? "";
648
+ out += replacer(ch, pasteTokens.get(ch));
922
649
  }
923
- return { exact, alias };
650
+ return out;
924
651
  }
925
- function matchCanonicalModelId(providerModelId, index) {
926
- const normalized = normalizeModelId(providerModelId);
927
- if (!normalized)
928
- return null;
929
- const exactMatch = index.exact.get(normalized);
930
- if (exactMatch)
931
- return exactMatch;
932
- const short = basename(normalized);
933
- if (!short)
934
- return null;
935
- const alias = index.alias.get(short);
936
- return alias ?? null;
652
+ function renderInputBuffer(buf, pasteTokens) {
653
+ return processInputBuffer(buf, pasteTokens, (ch, pasted) => pasted ? pasteLabel(pasted) : ch);
937
654
  }
938
-
939
- // src/llm-api/model-info-cache.ts
940
- function parseModelStringLoose(modelString) {
941
- const slash = modelString.indexOf("/");
942
- if (slash === -1) {
943
- return { provider: null, modelId: modelString };
655
+ function expandInputBuffer(buf, pasteTokens) {
656
+ return processInputBuffer(buf, pasteTokens, (ch, pasted) => pasted ?? ch);
657
+ }
658
+ function pruneInputPasteTokens(pasteTokens, ...buffers) {
659
+ const referenced = buffers.join("");
660
+ const next = new Map;
661
+ for (const [token, text] of pasteTokens) {
662
+ if (referenced.includes(token))
663
+ next.set(token, text);
944
664
  }
945
- const provider = modelString.slice(0, slash).trim().toLowerCase();
946
- const modelId = modelString.slice(slash + 1);
947
- return { provider: provider || null, modelId };
665
+ return next;
948
666
  }
949
- function providerModelKey(provider, modelId) {
950
- return `${provider}/${modelId}`;
667
+ function getVisualCursor(buf, cursor, pasteTokens) {
668
+ let visual = 0;
669
+ for (let i = 0;i < Math.min(cursor, buf.length); i++) {
670
+ const ch = buf[i] ?? "";
671
+ const pasted = pasteTokens.get(ch);
672
+ visual += pasted ? pasteLabel(pasted).length : 1;
673
+ }
674
+ return visual;
951
675
  }
952
- function emptyRuntimeCache() {
676
+ function buildPromptDisplay(text, cursor, maxLen) {
677
+ const clampedCursor = Math.max(0, Math.min(cursor, text.length));
678
+ if (maxLen <= 0)
679
+ return { display: "", cursor: 0 };
680
+ if (text.length <= maxLen)
681
+ return { display: text, cursor: clampedCursor };
682
+ let start = Math.max(0, clampedCursor - maxLen);
683
+ const end = Math.min(text.length, start + maxLen);
684
+ if (end - start < maxLen)
685
+ start = Math.max(0, end - maxLen);
686
+ let display = text.slice(start, end);
687
+ if (start > 0 && display.length > 0)
688
+ display = `\u2026${display.slice(1)}`;
689
+ if (end < text.length && display.length > 0)
690
+ display = `${display.slice(0, -1)}\u2026`;
953
691
  return {
954
- capabilitiesByCanonical: new Map,
955
- providerModelsByKey: new Map,
956
- providerModelUniqIndex: new Map,
957
- matchIndex: {
958
- exact: new Map,
959
- alias: new Map
960
- },
961
- state: new Map
692
+ display,
693
+ cursor: Math.min(clampedCursor - start, display.length)
962
694
  };
963
695
  }
964
- function buildRuntimeCache(capabilityRows, providerRows, stateRows) {
965
- const capabilitiesByCanonical = new Map;
966
- for (const row of capabilityRows) {
967
- const canonical = normalizeModelId(row.canonical_model_id);
968
- if (!canonical)
969
- continue;
970
- capabilitiesByCanonical.set(canonical, {
971
- canonicalModelId: canonical,
972
- contextWindow: row.context_window,
973
- maxOutputTokens: row.max_output_tokens,
974
- reasoning: row.reasoning === 1,
975
- sourceProvider: row.source_provider
976
- });
696
+
697
+ // src/cli/completions.ts
698
+ import { join as join4, relative } from "path";
699
+
700
+ // src/session/oauth/openai.ts
701
+ import { createServer } from "http";
702
+
703
+ // src/session/oauth/pkce.ts
704
+ function base64urlEncode(bytes) {
705
+ let binary = "";
706
+ for (const byte of bytes) {
707
+ binary += String.fromCharCode(byte);
977
708
  }
978
- const providerModelsByKey = new Map;
979
- const providerModelUniqIndex = new Map;
980
- for (const row of providerRows) {
981
- const provider = row.provider.trim().toLowerCase();
982
- const providerModelId = normalizeModelId(row.provider_model_id);
983
- if (!provider || !providerModelId)
984
- continue;
985
- const key = providerModelKey(provider, providerModelId);
986
- providerModelsByKey.set(key, {
987
- provider,
988
- providerModelId,
989
- displayName: row.display_name,
990
- canonicalModelId: row.canonical_model_id ? normalizeModelId(row.canonical_model_id) : null,
991
- contextWindow: row.context_window,
992
- free: row.free === 1
709
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
710
+ }
711
+ async function generatePKCE() {
712
+ const verifierBytes = new Uint8Array(32);
713
+ crypto.getRandomValues(verifierBytes);
714
+ const verifier = base64urlEncode(verifierBytes);
715
+ const data = new TextEncoder().encode(verifier);
716
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
717
+ const challenge = base64urlEncode(new Uint8Array(hashBuffer));
718
+ return { verifier, challenge };
719
+ }
720
+
721
+ // src/session/oauth/openai.ts
722
+ var CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
723
+ var AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize";
724
+ var TOKEN_URL = "https://auth.openai.com/oauth/token";
725
+ var CALLBACK_HOST = "127.0.0.1";
726
+ var CALLBACK_PORT = 1455;
727
+ var CALLBACK_PATH = "/auth/callback";
728
+ var REDIRECT_URI = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
729
+ var SCOPES = "openid profile email offline_access";
730
+ var LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
731
+ var SUCCESS_HTML = `<!doctype html>
732
+ <html lang="en"><head><meta charset="utf-8"><title>Authenticated</title></head>
733
+ <body><p>OpenAI authentication successful. Return to your terminal.</p></body></html>`;
734
+ function createState() {
735
+ const bytes = new Uint8Array(16);
736
+ crypto.getRandomValues(bytes);
737
+ let hex = "";
738
+ for (const b of bytes)
739
+ hex += b.toString(16).padStart(2, "0");
740
+ return hex;
741
+ }
742
+ function startCallbackServer(expectedState) {
743
+ return new Promise((resolve, reject) => {
744
+ let result = null;
745
+ let cancelled = false;
746
+ const server = createServer((req, res) => {
747
+ const url = new URL(req.url ?? "", "http://localhost");
748
+ if (url.pathname !== CALLBACK_PATH) {
749
+ res.writeHead(404).end("Not found");
750
+ return;
751
+ }
752
+ const code = url.searchParams.get("code");
753
+ const state = url.searchParams.get("state");
754
+ const error = url.searchParams.get("error");
755
+ if (error || !code || !state || state !== expectedState) {
756
+ res.writeHead(400).end("Authentication failed.");
757
+ return;
758
+ }
759
+ res.writeHead(200, { "Content-Type": "text/html" }).end(SUCCESS_HTML);
760
+ result = { code };
993
761
  });
994
- const prev = providerModelUniqIndex.get(providerModelId);
995
- if (prev === undefined) {
996
- providerModelUniqIndex.set(providerModelId, key);
997
- } else if (prev !== key) {
998
- providerModelUniqIndex.set(providerModelId, null);
999
- }
1000
- }
1001
- const matchIndex = buildModelMatchIndex(capabilitiesByCanonical.keys());
1002
- const state = new Map;
1003
- for (const row of stateRows) {
1004
- state.set(row.key, row.value);
1005
- }
1006
- return {
1007
- capabilitiesByCanonical,
1008
- providerModelsByKey,
1009
- providerModelUniqIndex,
1010
- matchIndex,
1011
- state
1012
- };
762
+ server.on("error", reject);
763
+ server.listen(CALLBACK_PORT, CALLBACK_HOST, () => {
764
+ resolve({
765
+ server,
766
+ cancel: () => {
767
+ cancelled = true;
768
+ },
769
+ waitForCode: async () => {
770
+ const deadline = Date.now() + LOGIN_TIMEOUT_MS;
771
+ while (!result && !cancelled && Date.now() < deadline) {
772
+ await new Promise((r) => setTimeout(r, 100));
773
+ }
774
+ return result;
775
+ }
776
+ });
777
+ });
778
+ });
1013
779
  }
1014
- function resolveFromProviderRow(row, cache) {
1015
- if (row.canonicalModelId) {
1016
- const capability = cache.capabilitiesByCanonical.get(row.canonicalModelId);
1017
- if (capability) {
1018
- return {
1019
- canonicalModelId: capability.canonicalModelId,
1020
- contextWindow: capability.contextWindow ?? row.contextWindow,
1021
- maxOutputTokens: capability.maxOutputTokens,
1022
- reasoning: capability.reasoning
1023
- };
1024
- }
780
+ async function postTokenRequest(body, label) {
781
+ const res = await fetch(TOKEN_URL, {
782
+ method: "POST",
783
+ headers: {
784
+ "Content-Type": "application/x-www-form-urlencoded"
785
+ },
786
+ body,
787
+ signal: AbortSignal.timeout(30000)
788
+ });
789
+ if (!res.ok) {
790
+ const text = await res.text();
791
+ throw new Error(`${label} failed (${res.status}): ${text}`);
1025
792
  }
793
+ const data = await res.json();
1026
794
  return {
1027
- canonicalModelId: row.canonicalModelId,
1028
- contextWindow: row.contextWindow,
1029
- maxOutputTokens: null,
1030
- reasoning: false
795
+ access: data.access_token,
796
+ refresh: data.refresh_token,
797
+ expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000
1031
798
  };
1032
799
  }
1033
- function resolveModelInfoInCache(modelString, cache) {
1034
- const parsed = parseModelStringLoose(modelString);
1035
- const normalizedModelId = normalizeModelId(parsed.modelId);
1036
- if (!normalizedModelId)
1037
- return null;
1038
- if (parsed.provider) {
1039
- const providerRow = cache.providerModelsByKey.get(providerModelKey(parsed.provider, normalizedModelId));
1040
- if (providerRow)
1041
- return resolveFromProviderRow(providerRow, cache);
1042
- }
1043
- const canonical = matchCanonicalModelId(normalizedModelId, cache.matchIndex);
1044
- if (canonical) {
1045
- const capability = cache.capabilitiesByCanonical.get(canonical);
1046
- if (capability) {
1047
- return {
1048
- canonicalModelId: capability.canonicalModelId,
1049
- contextWindow: capability.contextWindow,
1050
- maxOutputTokens: capability.maxOutputTokens,
1051
- reasoning: capability.reasoning
1052
- };
1053
- }
1054
- }
1055
- if (!parsed.provider) {
1056
- const uniqueProviderKey = cache.providerModelUniqIndex.get(normalizedModelId);
1057
- if (uniqueProviderKey) {
1058
- const providerRow = cache.providerModelsByKey.get(uniqueProviderKey);
1059
- if (providerRow)
1060
- return resolveFromProviderRow(providerRow, cache);
1061
- }
1062
- }
1063
- return null;
800
+ function exchangeCode(code, verifier) {
801
+ return postTokenRequest(new URLSearchParams({
802
+ grant_type: "authorization_code",
803
+ client_id: CLIENT_ID,
804
+ code,
805
+ code_verifier: verifier,
806
+ redirect_uri: REDIRECT_URI
807
+ }), "Token exchange");
1064
808
  }
1065
- function readLiveModelsFromCache(cache, visibleProviders) {
1066
- const models = [];
1067
- for (const row of cache.providerModelsByKey.values()) {
1068
- if (!visibleProviders.has(row.provider))
1069
- continue;
1070
- const info = resolveFromProviderRow(row, cache);
1071
- models.push({
1072
- id: `${row.provider}/${row.providerModelId}`,
1073
- displayName: row.displayName,
1074
- provider: row.provider,
1075
- context: info.contextWindow ?? undefined,
1076
- free: row.free ? true : undefined
1077
- });
1078
- }
1079
- models.sort((a, b) => a.provider.localeCompare(b.provider) || a.id.localeCompare(b.id));
1080
- return models;
809
+ function refreshOpenAIToken(refreshToken) {
810
+ return postTokenRequest(new URLSearchParams({
811
+ grant_type: "refresh_token",
812
+ refresh_token: refreshToken,
813
+ client_id: CLIENT_ID
814
+ }), "Token refresh");
1081
815
  }
1082
-
1083
- // src/llm-api/model-info-fetch.ts
1084
- var ZEN_BASE = "https://opencode.ai/zen/v1";
1085
- var OPENAI_BASE = "https://api.openai.com";
1086
- var ANTHROPIC_BASE = "https://api.anthropic.com";
1087
- var GOOGLE_BASE = "https://generativelanguage.googleapis.com/v1beta";
1088
- var MODELS_DEV_URL = "https://models.dev/api.json";
1089
- function normalizeModelId2(modelId) {
1090
- let out = modelId.trim().toLowerCase();
1091
- while (out.startsWith("models/")) {
1092
- out = out.slice("models/".length);
816
+ async function loginOpenAI(callbacks) {
817
+ const { verifier, challenge } = await generatePKCE();
818
+ const state = createState();
819
+ const cb = await startCallbackServer(state);
820
+ try {
821
+ const params = new URLSearchParams({
822
+ response_type: "code",
823
+ client_id: CLIENT_ID,
824
+ redirect_uri: REDIRECT_URI,
825
+ scope: SCOPES,
826
+ code_challenge: challenge,
827
+ code_challenge_method: "S256",
828
+ state,
829
+ id_token_add_organizations: "true",
830
+ codex_cli_simplified_flow: "true",
831
+ originator: "mc"
832
+ });
833
+ callbacks.onOpenUrl(`${AUTHORIZE_URL}?${params}`, "Complete login in your browser.");
834
+ const result = await cb.waitForCode();
835
+ if (!result)
836
+ throw new Error("Login cancelled or no code received");
837
+ callbacks.onProgress("Exchanging authorization code for tokens\u2026");
838
+ return exchangeCode(result.code, verifier);
839
+ } finally {
840
+ cb.server.close();
1093
841
  }
1094
- return out;
1095
842
  }
1096
- async function fetchJson(url, init, timeoutMs) {
843
+ function extractAccountId(accessToken) {
1097
844
  try {
1098
- const response = await fetch(url, {
1099
- ...init,
1100
- signal: AbortSignal.timeout(timeoutMs)
1101
- });
1102
- if (!response.ok)
845
+ const parts = accessToken.split(".");
846
+ const jwt = parts[1];
847
+ if (parts.length !== 3 || !jwt)
1103
848
  return null;
1104
- return await response.json();
849
+ const payload = JSON.parse(atob(jwt));
850
+ const auth = payload["https://api.openai.com/auth"];
851
+ return auth?.chatgpt_account_id ?? null;
1105
852
  } catch {
1106
853
  return null;
1107
854
  }
1108
855
  }
1109
- async function fetchModelsDevPayload() {
1110
- return fetchJson(MODELS_DEV_URL, {}, 1e4);
856
+ var openaiOAuth = {
857
+ id: "openai",
858
+ name: "OpenAI (ChatGPT Plus/Pro)",
859
+ login: loginOpenAI,
860
+ refreshToken: refreshOpenAIToken
861
+ };
862
+
863
+ // src/session/oauth/auth-storage.ts
864
+ var PROVIDERS = new Map([
865
+ [openaiOAuth.id, openaiOAuth]
866
+ ]);
867
+ function getOAuthProviders() {
868
+ return [...PROVIDERS.values()];
1111
869
  }
1112
- function processModelsList(payload, arrayKey, idKey, mapper) {
1113
- if (!isRecord(payload) || !Array.isArray(payload[arrayKey]))
1114
- return null;
1115
- const out = [];
1116
- for (const item of payload[arrayKey]) {
1117
- if (!isRecord(item) || typeof item[idKey] !== "string")
1118
- continue;
1119
- const modelId = normalizeModelId2(item[idKey]);
1120
- if (!modelId)
1121
- continue;
1122
- const mapped = mapper(item, modelId);
1123
- if (mapped)
1124
- out.push(mapped);
1125
- }
1126
- return out;
870
+ function getOAuthProvider(id) {
871
+ return PROVIDERS.get(id);
1127
872
  }
1128
- async function fetchPaginatedModelsList(url, init, timeoutMs, arrayKey, idKey, mapper) {
1129
- const out = [];
1130
- const seen = new Set;
1131
- const baseUrl = new URL(url);
1132
- let nextAfter = null;
1133
- for (let page = 0;page < 10; page += 1) {
1134
- const currentUrl = new URL(baseUrl);
1135
- if (nextAfter !== null)
1136
- currentUrl.searchParams.set("after", nextAfter);
1137
- const payload = await fetchJson(currentUrl.toString(), init, timeoutMs);
1138
- const rows = processModelsList(payload, arrayKey, idKey, mapper);
1139
- if (rows === null)
1140
- return null;
1141
- for (const row of rows) {
1142
- if (seen.has(row.providerModelId))
1143
- continue;
1144
- seen.add(row.providerModelId);
1145
- out.push(row);
1146
- }
1147
- if (!isRecord(payload))
1148
- break;
1149
- if (payload.has_more !== true || typeof payload.last_id !== "string")
1150
- break;
1151
- nextAfter = payload.last_id;
1152
- }
1153
- return out;
873
+ function getStoredToken(provider) {
874
+ return getDb().query("SELECT provider, access_token, refresh_token, expires_at, updated_at FROM oauth_tokens WHERE provider = ?").get(provider) ?? null;
1154
875
  }
1155
- async function fetchZenModels() {
1156
- const key = process.env.OPENCODE_API_KEY;
1157
- if (!key)
1158
- return null;
1159
- return fetchPaginatedModelsList(`${ZEN_BASE}/models`, { headers: { Authorization: `Bearer ${key}` } }, 8000, "data", "id", (item, modelId) => {
1160
- const contextWindow = typeof item.context_window === "number" && Number.isFinite(item.context_window) ? Math.max(0, Math.trunc(item.context_window)) : null;
1161
- return {
1162
- providerModelId: modelId,
1163
- displayName: item.id,
1164
- contextWindow,
1165
- free: item.id.endsWith("-free") || item.id === "gpt-5-nano" || item.id === "big-pickle"
1166
- };
1167
- });
876
+ function upsertToken(provider, creds) {
877
+ getDb().run(`INSERT INTO oauth_tokens (provider, access_token, refresh_token, expires_at, updated_at)
878
+ VALUES (?, ?, ?, ?, ?)
879
+ ON CONFLICT(provider) DO UPDATE SET
880
+ access_token = excluded.access_token,
881
+ refresh_token = excluded.refresh_token,
882
+ expires_at = excluded.expires_at,
883
+ updated_at = excluded.updated_at`, [provider, creds.access, creds.refresh, creds.expires, Date.now()]);
1168
884
  }
1169
- async function fetchOpenAIModels() {
1170
- const envKey = process.env.OPENAI_API_KEY;
1171
- if (envKey) {
1172
- return fetchPaginatedModelsList(`${OPENAI_BASE}/v1/models`, { headers: { Authorization: `Bearer ${envKey}` } }, 6000, "data", "id", (_item, modelId) => ({
1173
- providerModelId: modelId,
1174
- displayName: modelId,
1175
- contextWindow: null,
1176
- free: false
1177
- }));
1178
- }
1179
- if (isLoggedIn("openai")) {
1180
- const token = await getAccessToken("openai");
1181
- if (!token)
1182
- return null;
1183
- return fetchCodexOAuthModels(token);
1184
- }
1185
- return null;
885
+ function deleteToken(provider) {
886
+ getDb().run("DELETE FROM oauth_tokens WHERE provider = ?", [provider]);
1186
887
  }
1187
- var CODEX_MODELS_URL = "https://chatgpt.com/backend-api/codex/models?client_version=1.0.0";
1188
- async function fetchCodexOAuthModels(token) {
1189
- try {
1190
- const res = await fetch(CODEX_MODELS_URL, {
1191
- headers: { Authorization: `Bearer ${token}` },
1192
- signal: AbortSignal.timeout(6000)
1193
- });
1194
- if (!res.ok)
1195
- return null;
1196
- const data = await res.json();
1197
- return (data.models ?? []).filter((m) => m.visibility === "list").map((m) => ({
1198
- providerModelId: m.slug,
1199
- displayName: m.display_name || m.slug,
1200
- contextWindow: m.context_window,
1201
- free: false
1202
- }));
1203
- } catch {
888
+ function isAuthError(err) {
889
+ if (!(err instanceof Error))
890
+ return false;
891
+ return /\b(401|403)\b/.test(err.message);
892
+ }
893
+ function isLoggedIn(provider) {
894
+ return PROVIDERS.has(provider) && getStoredToken(provider) !== null;
895
+ }
896
+ function listLoggedInProviders() {
897
+ return getDb().query("SELECT provider FROM oauth_tokens ORDER BY provider").all().map((r) => r.provider).filter((provider) => PROVIDERS.has(provider));
898
+ }
899
+ async function login(providerId, callbacks) {
900
+ const provider = PROVIDERS.get(providerId);
901
+ if (!provider)
902
+ throw new Error(`Unknown OAuth provider: ${providerId}`);
903
+ const creds = await provider.login(callbacks);
904
+ upsertToken(providerId, creds);
905
+ }
906
+ function logout(providerId) {
907
+ deleteToken(providerId);
908
+ }
909
+ async function getAccessToken(providerId) {
910
+ const row = getStoredToken(providerId);
911
+ if (!row)
1204
912
  return null;
913
+ if (Date.now() < row.expires_at) {
914
+ return row.access_token;
1205
915
  }
1206
- }
1207
- async function fetchAnthropicModels() {
1208
- const key = process.env.ANTHROPIC_API_KEY;
1209
- if (!key)
916
+ const provider = PROVIDERS.get(providerId);
917
+ if (!provider)
1210
918
  return null;
1211
- const payload = await fetchJson(`${ANTHROPIC_BASE}/v1/models`, {
1212
- headers: {
1213
- "anthropic-version": "2023-06-01",
1214
- "x-api-key": key
919
+ try {
920
+ const refreshed = await provider.refreshToken(row.refresh_token);
921
+ upsertToken(providerId, refreshed);
922
+ return refreshed.access;
923
+ } catch (err) {
924
+ if (isAuthError(err)) {
925
+ deleteToken(providerId);
1215
926
  }
1216
- }, 6000);
1217
- return processModelsList(payload, "data", "id", (item, modelId) => {
1218
- const displayName = typeof item.display_name === "string" && item.display_name.trim().length > 0 ? item.display_name : modelId;
1219
- return {
1220
- providerModelId: modelId,
1221
- displayName,
1222
- contextWindow: null,
1223
- free: false
1224
- };
1225
- });
927
+ return null;
928
+ }
1226
929
  }
1227
- async function fetchGoogleModels() {
1228
- const key = process.env.GOOGLE_API_KEY ?? process.env.GEMINI_API_KEY;
1229
- if (!key)
930
+
931
+ // src/llm-api/history/shared.ts
932
+ function isRecord(value) {
933
+ return value !== null && typeof value === "object";
934
+ }
935
+ function normalizeProviderOptions(part) {
936
+ if (!isRecord(part))
937
+ return part;
938
+ if (part.providerOptions !== undefined || part.providerMetadata === undefined) {
939
+ return part;
940
+ }
941
+ return {
942
+ ...part,
943
+ providerOptions: part.providerMetadata
944
+ };
945
+ }
946
+ function normalizeMessageProviderOptions(message) {
947
+ if (!Array.isArray(message.content))
948
+ return message;
949
+ return {
950
+ ...message,
951
+ content: message.content.map((part) => normalizeProviderOptions(part))
952
+ };
953
+ }
954
+ function getPartProviderOptions(part) {
955
+ if (!isRecord(part))
1230
956
  return null;
1231
- const payload = await fetchJson(`${GOOGLE_BASE}/models?key=${encodeURIComponent(key)}`, {}, 6000);
1232
- return processModelsList(payload, "models", "name", (item, modelId) => {
1233
- const displayName = typeof item.displayName === "string" && item.displayName.trim().length > 0 ? item.displayName : modelId;
1234
- const contextWindow = typeof item.inputTokenLimit === "number" && Number.isFinite(item.inputTokenLimit) ? Math.max(0, Math.trunc(item.inputTokenLimit)) : null;
957
+ if (isRecord(part.providerOptions))
958
+ return part.providerOptions;
959
+ if (isRecord(part.providerMetadata))
960
+ return part.providerMetadata;
961
+ return null;
962
+ }
963
+ function isToolCallPart(part) {
964
+ return isRecord(part) && part.type === "tool-call";
965
+ }
966
+ function hasObjectToolCallInput(part) {
967
+ return isToolCallPart(part) && "input" in part && isRecord(part.input) && !Array.isArray(part.input);
968
+ }
969
+ function mapAssistantParts(messages, transform) {
970
+ let mutated = false;
971
+ const result = messages.map((message) => {
972
+ if (message.role !== "assistant" || !Array.isArray(message.content)) {
973
+ return message;
974
+ }
975
+ let contentMutated = false;
976
+ const nextContent = message.content.map((part) => {
977
+ const next = transform(part);
978
+ if (next !== part)
979
+ contentMutated = true;
980
+ return next;
981
+ });
982
+ if (!contentMutated)
983
+ return message;
984
+ mutated = true;
1235
985
  return {
1236
- providerModelId: modelId,
1237
- displayName,
1238
- contextWindow,
1239
- free: false
986
+ ...message,
987
+ content: nextContent
1240
988
  };
1241
989
  });
990
+ return mutated ? result : messages;
1242
991
  }
1243
- async function fetchOllamaModels() {
1244
- const base = process.env.OLLAMA_BASE_URL ?? "http://localhost:11434";
1245
- const payload = await fetchJson(`${base}/api/tags`, {}, 3000);
1246
- return processModelsList(payload, "models", "name", (item, modelId) => {
1247
- const details = item.details;
1248
- let sizeSuffix = "";
1249
- if (isRecord(details) && typeof details.parameter_size === "string") {
1250
- sizeSuffix = ` (${details.parameter_size})`;
992
+ var TOOL_RUNTIME_INPUT_KEYS = new Set(["cwd"]);
993
+ function stripToolRuntimeInputFields(messages) {
994
+ return mapAssistantParts(messages, (part) => {
995
+ if (!hasObjectToolCallInput(part))
996
+ return part;
997
+ let inputMutated = false;
998
+ const nextInput = { ...part.input };
999
+ for (const key of TOOL_RUNTIME_INPUT_KEYS) {
1000
+ if (!(key in nextInput))
1001
+ continue;
1002
+ delete nextInput[key];
1003
+ inputMutated = true;
1251
1004
  }
1252
- return {
1253
- providerModelId: modelId,
1254
- displayName: `${item.name}${sizeSuffix}`,
1255
- contextWindow: null,
1256
- free: false
1257
- };
1005
+ return inputMutated ? { ...part, input: nextInput } : part;
1258
1006
  });
1259
1007
  }
1260
- var PROVIDER_CANDIDATE_FETCHERS = {
1261
- zen: fetchZenModels,
1262
- openai: fetchOpenAIModels,
1263
- anthropic: fetchAnthropicModels,
1264
- google: fetchGoogleModels,
1265
- ollama: fetchOllamaModels
1266
- };
1267
- async function fetchProviderCandidates(provider) {
1268
- const fetcher = PROVIDER_CANDIDATE_FETCHERS[provider];
1269
- if (!fetcher)
1270
- return null;
1271
- return fetcher();
1272
- }
1273
1008
 
1274
- // src/llm-api/model-info.ts
1275
- var MODELS_DEV_SYNC_KEY = "last_models_dev_sync_at";
1276
- var PROVIDER_SYNC_KEY_PREFIX = "last_provider_sync_at:";
1277
- var CACHE_VERSION_KEY = "model_info_cache_version";
1278
- var CACHE_VERSION = 5;
1279
- var MODEL_INFO_TTL_MS = 24 * 60 * 60 * 1000;
1280
- var REMOTE_PROVIDER_ENV_KEYS = [
1281
- { provider: "zen", envKeys: ["OPENCODE_API_KEY"] },
1282
- { provider: "openai", envKeys: ["OPENAI_API_KEY"] },
1283
- { provider: "anthropic", envKeys: ["ANTHROPIC_API_KEY"] },
1284
- { provider: "google", envKeys: ["GOOGLE_API_KEY", "GEMINI_API_KEY"] }
1285
- ];
1286
- var runtimeCache = emptyRuntimeCache();
1287
- var loaded = false;
1288
- var refreshInFlight = null;
1289
- function isStaleTimestamp(timestamp, now = Date.now(), ttlMs = MODEL_INFO_TTL_MS) {
1290
- if (timestamp === null)
1291
- return true;
1292
- return now - timestamp > ttlMs;
1293
- }
1294
- function loadCacheFromDb() {
1295
- runtimeCache = buildRuntimeCache(listModelCapabilities(), listProviderModels(), listModelInfoState());
1296
- loaded = true;
1297
- }
1298
- function ensureLoaded() {
1299
- if (!loaded)
1300
- loadCacheFromDb();
1009
+ // src/llm-api/model-info-normalize.ts
1010
+ function basename(value) {
1011
+ const idx = value.lastIndexOf("/");
1012
+ return idx === -1 ? value : value.slice(idx + 1);
1301
1013
  }
1302
- function initModelInfoCache() {
1303
- loadCacheFromDb();
1014
+ function normalizeModelId(modelId) {
1015
+ let out = modelId.trim().toLowerCase();
1016
+ while (out.startsWith("models/")) {
1017
+ out = out.slice("models/".length);
1018
+ }
1019
+ return out;
1304
1020
  }
1305
- function parseStateInt(key) {
1306
- const raw = runtimeCache.state.get(key);
1307
- if (!raw)
1021
+ function parseContextWindow(model) {
1022
+ const limit = model.limit;
1023
+ if (!isRecord(limit))
1308
1024
  return null;
1309
- const value = Number.parseInt(raw, 10);
1310
- if (!Number.isFinite(value))
1025
+ const context = limit.context;
1026
+ if (typeof context !== "number" || !Number.isFinite(context))
1311
1027
  return null;
1312
- return value;
1313
- }
1314
- function hasAnyEnvKey(env, keys) {
1315
- for (const key of keys) {
1316
- if (env[key])
1317
- return true;
1318
- }
1319
- return false;
1320
- }
1321
- function getRemoteProvidersFromEnv(env) {
1322
- const providers = REMOTE_PROVIDER_ENV_KEYS.filter((entry) => hasAnyEnvKey(env, entry.envKeys)).map((entry) => entry.provider);
1323
- if (!providers.includes("openai") && isLoggedIn("openai")) {
1324
- providers.push("openai");
1325
- }
1326
- return providers;
1327
- }
1328
- function getProvidersToRefreshFromEnv(env) {
1329
- return [...getRemoteProvidersFromEnv(env), "ollama"];
1330
- }
1331
- function getVisibleProvidersForSnapshotFromEnv(env) {
1332
- return new Set(getProvidersToRefreshFromEnv(env));
1333
- }
1334
- function getConfiguredProvidersForSync() {
1335
- return getProvidersToRefreshFromEnv(process.env);
1336
- }
1337
- function getProvidersRequiredForFreshness() {
1338
- return getRemoteProvidersFromEnv(process.env);
1339
- }
1340
- function getProviderSyncKey(provider) {
1341
- return `${PROVIDER_SYNC_KEY_PREFIX}${provider}`;
1028
+ return Math.max(0, Math.trunc(context));
1342
1029
  }
1343
- function isModelInfoStale(now = Date.now()) {
1344
- ensureLoaded();
1345
- if (parseStateInt(CACHE_VERSION_KEY) !== CACHE_VERSION)
1346
- return true;
1347
- if (isStaleTimestamp(parseStateInt(MODELS_DEV_SYNC_KEY), now))
1348
- return true;
1349
- for (const provider of getProvidersRequiredForFreshness()) {
1350
- const providerSync = parseStateInt(getProviderSyncKey(provider));
1351
- if (isStaleTimestamp(providerSync, now))
1352
- return true;
1353
- }
1354
- return false;
1030
+ function parseMaxOutputTokens(model) {
1031
+ const limit = model.limit;
1032
+ if (!isRecord(limit))
1033
+ return null;
1034
+ const output = limit.output;
1035
+ if (typeof output !== "number" || !Number.isFinite(output))
1036
+ return null;
1037
+ return Math.max(0, Math.trunc(output));
1355
1038
  }
1356
- function getLastSyncAt() {
1357
- let latest = parseStateInt(MODELS_DEV_SYNC_KEY);
1358
- for (const provider of getProvidersRequiredForFreshness()) {
1359
- const value = parseStateInt(getProviderSyncKey(provider));
1360
- if (value !== null && (latest === null || value > latest))
1361
- latest = value;
1039
+ function parseModelsDevCapabilities(payload, updatedAt) {
1040
+ if (!isRecord(payload))
1041
+ return [];
1042
+ const merged = new Map;
1043
+ for (const [provider, providerValue] of Object.entries(payload)) {
1044
+ if (!isRecord(providerValue))
1045
+ continue;
1046
+ const models = providerValue.models;
1047
+ if (!isRecord(models))
1048
+ continue;
1049
+ for (const [modelKey, modelValue] of Object.entries(models)) {
1050
+ if (!isRecord(modelValue))
1051
+ continue;
1052
+ const explicitId = typeof modelValue.id === "string" && modelValue.id.trim().length > 0 ? modelValue.id : modelKey;
1053
+ const canonicalModelId = normalizeModelId(explicitId);
1054
+ if (!canonicalModelId)
1055
+ continue;
1056
+ const contextWindow = parseContextWindow(modelValue);
1057
+ const maxOutputTokens = parseMaxOutputTokens(modelValue);
1058
+ const reasoning = modelValue.reasoning === true;
1059
+ const rawJson = JSON.stringify(modelValue);
1060
+ const prev = merged.get(canonicalModelId);
1061
+ if (!prev) {
1062
+ merged.set(canonicalModelId, {
1063
+ canonicalModelId,
1064
+ contextWindow,
1065
+ maxOutputTokens,
1066
+ reasoning,
1067
+ sourceProvider: provider,
1068
+ rawJson
1069
+ });
1070
+ continue;
1071
+ }
1072
+ merged.set(canonicalModelId, {
1073
+ canonicalModelId,
1074
+ contextWindow: prev.contextWindow ?? contextWindow,
1075
+ maxOutputTokens: prev.maxOutputTokens ?? maxOutputTokens,
1076
+ reasoning: prev.reasoning || reasoning,
1077
+ sourceProvider: prev.sourceProvider,
1078
+ rawJson: prev.rawJson ?? rawJson
1079
+ });
1080
+ }
1362
1081
  }
1363
- return latest;
1364
- }
1365
- function providerRowsFromCandidates(candidates, matchIndex, updatedAt) {
1366
- return candidates.map((candidate) => ({
1367
- provider_model_id: candidate.providerModelId,
1368
- display_name: candidate.displayName,
1369
- canonical_model_id: matchCanonicalModelId(candidate.providerModelId, matchIndex),
1370
- context_window: candidate.contextWindow,
1371
- free: candidate.free ? 1 : 0,
1082
+ return Array.from(merged.values()).map((entry) => ({
1083
+ canonical_model_id: entry.canonicalModelId,
1084
+ context_window: entry.contextWindow,
1085
+ max_output_tokens: entry.maxOutputTokens,
1086
+ reasoning: entry.reasoning ? 1 : 0,
1087
+ source_provider: entry.sourceProvider,
1088
+ raw_json: entry.rawJson,
1372
1089
  updated_at: updatedAt
1373
1090
  }));
1374
1091
  }
1375
- async function refreshModelInfoInternal() {
1376
- ensureLoaded();
1377
- const now = Date.now();
1378
- const providers = getConfiguredProvidersForSync();
1379
- const providerResults = await Promise.all(providers.map(async (provider) => ({
1380
- provider,
1381
- candidates: await fetchProviderCandidates(provider)
1382
- })));
1383
- const modelsDevPayload = await fetchModelsDevPayload();
1384
- let matchIndex = runtimeCache.matchIndex;
1385
- if (modelsDevPayload !== null) {
1386
- const capabilityRows = parseModelsDevCapabilities(modelsDevPayload, now);
1387
- if (capabilityRows.length > 0) {
1388
- replaceModelCapabilities(capabilityRows);
1389
- setModelInfoState(MODELS_DEV_SYNC_KEY, String(now));
1390
- matchIndex = buildModelMatchIndex(capabilityRows.map((row) => row.canonical_model_id));
1391
- }
1392
- }
1393
- for (const result of providerResults) {
1394
- if (result.candidates === null)
1092
+ function buildModelMatchIndex(canonicalModelIds) {
1093
+ const exact = new Map;
1094
+ const aliasCandidates = new Map;
1095
+ for (const rawCanonical of canonicalModelIds) {
1096
+ const canonical = normalizeModelId(rawCanonical);
1097
+ if (!canonical)
1395
1098
  continue;
1396
- const rows = providerRowsFromCandidates(result.candidates, matchIndex, now);
1397
- replaceProviderModels(result.provider, rows);
1398
- setModelInfoState(getProviderSyncKey(result.provider), String(now));
1399
- }
1400
- setModelInfoState(CACHE_VERSION_KEY, String(CACHE_VERSION));
1401
- loadCacheFromDb();
1402
- }
1403
- function refreshModelInfoInBackground(opts) {
1404
- ensureLoaded();
1405
- const force = opts?.force ?? false;
1406
- if (!force && !isModelInfoStale())
1407
- return Promise.resolve();
1408
- if (refreshInFlight)
1409
- return refreshInFlight;
1410
- refreshInFlight = refreshModelInfoInternal().finally(() => {
1411
- refreshInFlight = null;
1412
- });
1413
- return refreshInFlight;
1414
- }
1415
- function isModelInfoRefreshing() {
1416
- return refreshInFlight !== null;
1417
- }
1418
- function resolveModelInfo(modelString) {
1419
- ensureLoaded();
1420
- return resolveModelInfoInCache(modelString, runtimeCache);
1421
- }
1422
- function getContextWindow(modelString) {
1423
- return resolveModelInfo(modelString)?.contextWindow ?? null;
1424
- }
1425
- function getMaxOutputTokens(modelString) {
1426
- return resolveModelInfo(modelString)?.maxOutputTokens ?? null;
1427
- }
1428
- function supportsThinking(modelString) {
1429
- return resolveModelInfo(modelString)?.reasoning ?? false;
1430
- }
1431
- function getCachedModelIds() {
1432
- ensureLoaded();
1433
- const visible = getVisibleProvidersForSnapshotFromEnv(process.env);
1434
- const ids = [];
1435
- for (const row of runtimeCache.providerModelsByKey.values()) {
1436
- if (visible.has(row.provider)) {
1437
- ids.push(`${row.provider}/${row.providerModelId}`);
1099
+ exact.set(canonical, canonical);
1100
+ const short = basename(canonical);
1101
+ if (!short)
1102
+ continue;
1103
+ let set = aliasCandidates.get(short);
1104
+ if (!set) {
1105
+ set = new Set;
1106
+ aliasCandidates.set(short, set);
1438
1107
  }
1108
+ set.add(canonical);
1439
1109
  }
1440
- ids.sort((a, b) => a.localeCompare(b));
1441
- return ids;
1442
- }
1443
- function hasCachedModelsForAllVisibleProviders() {
1444
- const visible = getVisibleProvidersForSnapshotFromEnv(process.env);
1445
- for (const provider of visible) {
1446
- let found = false;
1447
- for (const row of runtimeCache.providerModelsByKey.values()) {
1448
- if (row.provider === provider) {
1449
- found = true;
1450
- break;
1110
+ const alias = new Map;
1111
+ for (const [short, candidates] of aliasCandidates) {
1112
+ if (candidates.size === 1) {
1113
+ for (const value of candidates) {
1114
+ alias.set(short, value);
1451
1115
  }
1452
- }
1453
- if (!found)
1454
- return false;
1455
- }
1456
- return true;
1457
- }
1458
- async function fetchAvailableModelsSnapshot() {
1459
- ensureLoaded();
1460
- if (isModelInfoStale() && !isModelInfoRefreshing()) {
1461
- if (runtimeCache.providerModelsByKey.size === 0 || !hasCachedModelsForAllVisibleProviders()) {
1462
- await refreshModelInfoInBackground({ force: true });
1463
1116
  } else {
1464
- refreshModelInfoInBackground();
1117
+ alias.set(short, null);
1465
1118
  }
1466
1119
  }
1467
- return {
1468
- models: readLiveModelsFromCache(runtimeCache, getVisibleProvidersForSnapshotFromEnv(process.env)),
1469
- stale: isModelInfoStale(),
1470
- refreshing: isModelInfoRefreshing(),
1471
- lastSyncAt: getLastSyncAt()
1472
- };
1120
+ return { exact, alias };
1473
1121
  }
1474
-
1475
- // src/llm-api/model-routing.ts
1476
- function parseModelString(modelString) {
1477
- const slashIdx = modelString.indexOf("/");
1478
- if (slashIdx === -1)
1479
- return { provider: modelString, modelId: "" };
1480
- return {
1481
- provider: modelString.slice(0, slashIdx),
1482
- modelId: modelString.slice(slashIdx + 1)
1483
- };
1122
+ function matchCanonicalModelId(providerModelId, index) {
1123
+ const normalized = normalizeModelId(providerModelId);
1124
+ if (!normalized)
1125
+ return null;
1126
+ const exactMatch = index.exact.get(normalized);
1127
+ if (exactMatch)
1128
+ return exactMatch;
1129
+ const short = basename(normalized);
1130
+ if (!short)
1131
+ return null;
1132
+ const alias = index.alias.get(short);
1133
+ return alias ?? null;
1484
1134
  }
1485
- function isZenProvider(provider) {
1486
- return provider === "zen";
1135
+
1136
+ // src/llm-api/model-info-cache.ts
1137
+ function parseModelStringLoose(modelString) {
1138
+ const slash = modelString.indexOf("/");
1139
+ if (slash === -1) {
1140
+ return { provider: null, modelId: modelString };
1141
+ }
1142
+ const provider = modelString.slice(0, slash).trim().toLowerCase();
1143
+ const modelId = modelString.slice(slash + 1);
1144
+ return { provider: provider || null, modelId };
1487
1145
  }
1488
- function isAnthropicModelFamily(modelString) {
1489
- const { provider, modelId } = parseModelString(modelString);
1490
- return provider === "anthropic" || isZenProvider(provider) && modelId.startsWith("claude-");
1146
+ function providerModelKey(provider, modelId) {
1147
+ return `${provider}/${modelId}`;
1491
1148
  }
1492
- function isGeminiModelFamily(modelString) {
1493
- const { provider, modelId } = parseModelString(modelString);
1494
- return (provider === "google" || isZenProvider(provider)) && modelId.startsWith("gemini-");
1149
+ function emptyRuntimeCache() {
1150
+ return {
1151
+ capabilitiesByCanonical: new Map,
1152
+ providerModelsByKey: new Map,
1153
+ providerModelUniqIndex: new Map,
1154
+ matchIndex: {
1155
+ exact: new Map,
1156
+ alias: new Map
1157
+ },
1158
+ state: new Map
1159
+ };
1495
1160
  }
1496
- function isOpenAIGPTModelFamily(modelString) {
1497
- const { provider, modelId } = parseModelString(modelString);
1498
- return (provider === "openai" || isZenProvider(provider)) && modelId.startsWith("gpt-");
1161
+ function buildRuntimeCache(capabilityRows, providerRows, stateRows) {
1162
+ const capabilitiesByCanonical = new Map;
1163
+ for (const row of capabilityRows) {
1164
+ const canonical = normalizeModelId(row.canonical_model_id);
1165
+ if (!canonical)
1166
+ continue;
1167
+ capabilitiesByCanonical.set(canonical, {
1168
+ canonicalModelId: canonical,
1169
+ contextWindow: row.context_window,
1170
+ maxOutputTokens: row.max_output_tokens,
1171
+ reasoning: row.reasoning === 1,
1172
+ sourceProvider: row.source_provider
1173
+ });
1174
+ }
1175
+ const providerModelsByKey = new Map;
1176
+ const providerModelUniqIndex = new Map;
1177
+ for (const row of providerRows) {
1178
+ const provider = row.provider.trim().toLowerCase();
1179
+ const providerModelId = normalizeModelId(row.provider_model_id);
1180
+ if (!provider || !providerModelId)
1181
+ continue;
1182
+ const key = providerModelKey(provider, providerModelId);
1183
+ providerModelsByKey.set(key, {
1184
+ provider,
1185
+ providerModelId,
1186
+ displayName: row.display_name,
1187
+ canonicalModelId: row.canonical_model_id ? normalizeModelId(row.canonical_model_id) : null,
1188
+ contextWindow: row.context_window,
1189
+ free: row.free === 1
1190
+ });
1191
+ const prev = providerModelUniqIndex.get(providerModelId);
1192
+ if (prev === undefined) {
1193
+ providerModelUniqIndex.set(providerModelId, key);
1194
+ } else if (prev !== key) {
1195
+ providerModelUniqIndex.set(providerModelId, null);
1196
+ }
1197
+ }
1198
+ const matchIndex = buildModelMatchIndex(capabilitiesByCanonical.keys());
1199
+ const state = new Map;
1200
+ for (const row of stateRows) {
1201
+ state.set(row.key, row.value);
1202
+ }
1203
+ return {
1204
+ capabilitiesByCanonical,
1205
+ providerModelsByKey,
1206
+ providerModelUniqIndex,
1207
+ matchIndex,
1208
+ state
1209
+ };
1499
1210
  }
1500
- function isOpenAIReasoningModelFamily(modelString) {
1501
- const { provider, modelId } = parseModelString(modelString);
1502
- return (provider === "openai" || isZenProvider(provider)) && (modelId.startsWith("o") || modelId.startsWith("gpt-5"));
1211
+ function resolveFromProviderRow(row, cache) {
1212
+ if (row.canonicalModelId) {
1213
+ const capability = cache.capabilitiesByCanonical.get(row.canonicalModelId);
1214
+ if (capability) {
1215
+ return {
1216
+ canonicalModelId: capability.canonicalModelId,
1217
+ contextWindow: capability.contextWindow ?? row.contextWindow,
1218
+ maxOutputTokens: capability.maxOutputTokens,
1219
+ reasoning: capability.reasoning
1220
+ };
1221
+ }
1222
+ }
1223
+ return {
1224
+ canonicalModelId: row.canonicalModelId,
1225
+ contextWindow: row.contextWindow,
1226
+ maxOutputTokens: null,
1227
+ reasoning: false
1228
+ };
1503
1229
  }
1504
- function isZenOpenAICompatibleChatModel(modelString) {
1505
- const { provider, modelId } = parseModelString(modelString);
1506
- if (!isZenProvider(provider))
1507
- return false;
1508
- return !modelId.startsWith("gpt-") && !modelId.startsWith("gemini-") && !modelId.startsWith("claude-");
1230
+ function resolveModelInfoInCache(modelString, cache) {
1231
+ const parsed = parseModelStringLoose(modelString);
1232
+ const normalizedModelId = normalizeModelId(parsed.modelId);
1233
+ if (!normalizedModelId)
1234
+ return null;
1235
+ if (parsed.provider) {
1236
+ const providerRow = cache.providerModelsByKey.get(providerModelKey(parsed.provider, normalizedModelId));
1237
+ if (providerRow)
1238
+ return resolveFromProviderRow(providerRow, cache);
1239
+ }
1240
+ const canonical = matchCanonicalModelId(normalizedModelId, cache.matchIndex);
1241
+ if (canonical) {
1242
+ const capability = cache.capabilitiesByCanonical.get(canonical);
1243
+ if (capability) {
1244
+ return {
1245
+ canonicalModelId: capability.canonicalModelId,
1246
+ contextWindow: capability.contextWindow,
1247
+ maxOutputTokens: capability.maxOutputTokens,
1248
+ reasoning: capability.reasoning
1249
+ };
1250
+ }
1251
+ }
1252
+ if (!parsed.provider) {
1253
+ const uniqueProviderKey = cache.providerModelUniqIndex.get(normalizedModelId);
1254
+ if (uniqueProviderKey) {
1255
+ const providerRow = cache.providerModelsByKey.get(uniqueProviderKey);
1256
+ if (providerRow)
1257
+ return resolveFromProviderRow(providerRow, cache);
1258
+ }
1259
+ }
1260
+ return null;
1509
1261
  }
1510
- function getZenBackend(modelId) {
1511
- if (modelId.startsWith("claude-"))
1512
- return "anthropic";
1513
- if (modelId.startsWith("gpt-"))
1514
- return "openai";
1515
- if (modelId.startsWith("gemini-"))
1516
- return "google";
1517
- return "compat";
1262
+ function readLiveModelsFromCache(cache, visibleProviders) {
1263
+ const models = [];
1264
+ for (const row of cache.providerModelsByKey.values()) {
1265
+ if (!visibleProviders.has(row.provider))
1266
+ continue;
1267
+ const info = resolveFromProviderRow(row, cache);
1268
+ models.push({
1269
+ id: `${row.provider}/${row.providerModelId}`,
1270
+ displayName: row.displayName,
1271
+ provider: row.provider,
1272
+ context: info.contextWindow ?? undefined,
1273
+ free: row.free ? true : undefined
1274
+ });
1275
+ }
1276
+ models.sort((a, b) => a.provider.localeCompare(b.provider) || a.id.localeCompare(b.id));
1277
+ return models;
1518
1278
  }
1519
1279
 
1520
- // src/llm-api/providers.ts
1521
- var SUPPORTED_PROVIDERS = [
1522
- "zen",
1523
- "anthropic",
1524
- "openai",
1525
- "google",
1526
- "ollama"
1527
- ];
1528
- var ZEN_BASE2 = "https://opencode.ai/zen/v1";
1529
- function requireEnv(name) {
1530
- const value = process.env[name];
1531
- if (!value)
1532
- throw new Error(`${name} is not set`);
1533
- return value;
1280
+ // src/llm-api/model-info-fetch.ts
1281
+ var ZEN_BASE = "https://opencode.ai/zen/v1";
1282
+ var OPENAI_BASE = "https://api.openai.com";
1283
+ var ANTHROPIC_BASE = "https://api.anthropic.com";
1284
+ var GOOGLE_BASE = "https://generativelanguage.googleapis.com/v1beta";
1285
+ var MODELS_DEV_URL = "https://models.dev/api.json";
1286
+ function normalizeModelId2(modelId) {
1287
+ let out = modelId.trim().toLowerCase();
1288
+ while (out.startsWith("models/")) {
1289
+ out = out.slice("models/".length);
1290
+ }
1291
+ return out;
1534
1292
  }
1535
- function requireAnyEnv(names) {
1536
- for (const name of names) {
1537
- const value = process.env[name];
1538
- if (value)
1539
- return value;
1293
+ async function fetchJson(url, init, timeoutMs) {
1294
+ try {
1295
+ const response = await fetch(url, {
1296
+ ...init,
1297
+ signal: AbortSignal.timeout(timeoutMs)
1298
+ });
1299
+ if (!response.ok)
1300
+ return null;
1301
+ return await response.json();
1302
+ } catch {
1303
+ return null;
1540
1304
  }
1541
- throw new Error(`${names.join(" or ")} is not set`);
1542
1305
  }
1543
- function lazy(factory) {
1544
- let instance = null;
1545
- return () => {
1546
- if (instance === null) {
1547
- instance = factory();
1548
- }
1549
- return instance;
1550
- };
1306
+ async function fetchModelsDevPayload() {
1307
+ return fetchJson(MODELS_DEV_URL, {}, 1e4);
1551
1308
  }
1552
- var zenProviders = {
1553
- anthropic: lazy(() => createAnthropic({
1554
- fetch,
1555
- apiKey: requireEnv("OPENCODE_API_KEY"),
1556
- baseURL: ZEN_BASE2
1557
- })),
1558
- openai: lazy(() => createOpenAI({
1559
- fetch,
1560
- apiKey: requireEnv("OPENCODE_API_KEY"),
1561
- baseURL: ZEN_BASE2
1562
- })),
1563
- google: lazy(() => createGoogleGenerativeAI({
1564
- fetch,
1565
- apiKey: requireEnv("OPENCODE_API_KEY"),
1566
- baseURL: ZEN_BASE2
1567
- })),
1568
- compat: lazy(() => createOpenAICompatible({
1569
- fetch,
1570
- name: "zen-compat",
1571
- apiKey: requireEnv("OPENCODE_API_KEY"),
1572
- baseURL: ZEN_BASE2
1573
- }))
1574
- };
1575
- var directProviders = {
1576
- anthropic: lazy(() => createAnthropic({
1577
- fetch,
1578
- apiKey: requireEnv("ANTHROPIC_API_KEY")
1579
- })),
1580
- openai: lazy(() => createOpenAI({
1581
- fetch,
1582
- apiKey: requireEnv("OPENAI_API_KEY")
1583
- })),
1584
- google: lazy(() => createGoogleGenerativeAI({
1585
- fetch,
1586
- apiKey: requireAnyEnv(["GOOGLE_API_KEY", "GEMINI_API_KEY"])
1587
- })),
1588
- ollama: lazy(() => {
1589
- const baseURL = process.env.OLLAMA_BASE_URL ?? "http://localhost:11434";
1590
- return createOpenAICompatible({
1591
- name: "ollama",
1592
- baseURL: `${baseURL}/v1`,
1593
- apiKey: "ollama",
1594
- fetch
1595
- });
1596
- })
1597
- };
1598
- var ZEN_BACKEND_RESOLVERS = {
1599
- anthropic: (modelId) => zenProviders.anthropic()(modelId),
1600
- openai: (modelId) => zenProviders.openai().responses(modelId),
1601
- google: (modelId) => zenProviders.google()(modelId),
1602
- compat: (modelId) => zenProviders.compat()(modelId)
1603
- };
1604
- function resolveZenModel(modelId) {
1605
- return ZEN_BACKEND_RESOLVERS[getZenBackend(modelId)](modelId);
1309
+ function processModelsList(payload, arrayKey, idKey, mapper) {
1310
+ if (!isRecord(payload) || !Array.isArray(payload[arrayKey]))
1311
+ return null;
1312
+ const out = [];
1313
+ for (const item of payload[arrayKey]) {
1314
+ if (!isRecord(item) || typeof item[idKey] !== "string")
1315
+ continue;
1316
+ const modelId = normalizeModelId2(item[idKey]);
1317
+ if (!modelId)
1318
+ continue;
1319
+ const mapped = mapper(item, modelId);
1320
+ if (mapped)
1321
+ out.push(mapped);
1322
+ }
1323
+ return out;
1606
1324
  }
1607
- function createOAuthOpenAIProvider(token) {
1608
- const accountId = extractAccountId(token);
1609
- return createOpenAI({
1610
- apiKey: "oauth",
1611
- baseURL: OPENAI_CODEX_BASE_URL,
1612
- fetch: (input, init) => {
1613
- const h = new Headers(init?.headers);
1614
- h.delete("OpenAI-Organization");
1615
- h.delete("OpenAI-Project");
1616
- h.set("Authorization", `Bearer ${token}`);
1617
- if (accountId)
1618
- h.set("chatgpt-account-id", accountId);
1619
- let body = init?.body;
1620
- if (typeof body === "string") {
1621
- try {
1622
- const parsed = JSON.parse(body);
1623
- if (parsed.input && Array.isArray(parsed.input)) {
1624
- if (!parsed.instructions) {
1625
- const sysIdx = parsed.input.findIndex((m) => m.role === "developer" || m.role === "system");
1626
- if (sysIdx !== -1) {
1627
- const sysMsg = parsed.input[sysIdx];
1628
- parsed.instructions = typeof sysMsg.content === "string" ? sysMsg.content : JSON.stringify(sysMsg.content);
1629
- parsed.input.splice(sysIdx, 1);
1630
- }
1631
- }
1632
- delete parsed.max_output_tokens;
1633
- parsed.store = false;
1634
- parsed.stream = true;
1635
- body = JSON.stringify(parsed);
1636
- }
1637
- } catch {}
1638
- }
1639
- return fetch(input, {
1640
- ...init,
1641
- body,
1642
- headers: Object.fromEntries(h.entries())
1643
- });
1325
+ async function fetchPaginatedModelsList(url, init, timeoutMs, arrayKey, idKey, mapper) {
1326
+ const out = [];
1327
+ const seen = new Set;
1328
+ const baseUrl = new URL(url);
1329
+ let nextAfter = null;
1330
+ for (let page = 0;page < 10; page += 1) {
1331
+ const currentUrl = new URL(baseUrl);
1332
+ if (nextAfter !== null)
1333
+ currentUrl.searchParams.set("after", nextAfter);
1334
+ const payload = await fetchJson(currentUrl.toString(), init, timeoutMs);
1335
+ const rows = processModelsList(payload, arrayKey, idKey, mapper);
1336
+ if (rows === null)
1337
+ return null;
1338
+ for (const row of rows) {
1339
+ if (seen.has(row.providerModelId))
1340
+ continue;
1341
+ seen.add(row.providerModelId);
1342
+ out.push(row);
1644
1343
  }
1344
+ if (!isRecord(payload))
1345
+ break;
1346
+ if (payload.has_more !== true || typeof payload.last_id !== "string")
1347
+ break;
1348
+ nextAfter = payload.last_id;
1349
+ }
1350
+ return out;
1351
+ }
1352
+ async function fetchZenModels() {
1353
+ const key = process.env.OPENCODE_API_KEY;
1354
+ if (!key)
1355
+ return null;
1356
+ return fetchPaginatedModelsList(`${ZEN_BASE}/models`, { headers: { Authorization: `Bearer ${key}` } }, 8000, "data", "id", (item, modelId) => {
1357
+ const contextWindow = typeof item.context_window === "number" && Number.isFinite(item.context_window) ? Math.max(0, Math.trunc(item.context_window)) : null;
1358
+ return {
1359
+ providerModelId: modelId,
1360
+ displayName: item.id,
1361
+ contextWindow,
1362
+ free: item.id.endsWith("-free") || item.id === "gpt-5-nano" || item.id === "big-pickle"
1363
+ };
1645
1364
  });
1646
1365
  }
1647
- async function resolveOpenAIModel(modelId) {
1366
+ async function fetchOpenAIModels() {
1367
+ const envKey = process.env.OPENAI_API_KEY;
1368
+ if (envKey) {
1369
+ return fetchPaginatedModelsList(`${OPENAI_BASE}/v1/models`, { headers: { Authorization: `Bearer ${envKey}` } }, 6000, "data", "id", (_item, modelId) => ({
1370
+ providerModelId: modelId,
1371
+ displayName: modelId,
1372
+ contextWindow: null,
1373
+ free: false
1374
+ }));
1375
+ }
1648
1376
  if (isLoggedIn("openai")) {
1649
1377
  const token = await getAccessToken("openai");
1650
- if (token) {
1651
- if (!oauthOpenAICache || oauthOpenAICache.token !== token) {
1652
- oauthOpenAICache = {
1653
- token,
1654
- provider: createOAuthOpenAIProvider(token)
1655
- };
1656
- }
1657
- return oauthOpenAICache.provider.responses(modelId);
1658
- }
1378
+ if (!token)
1379
+ return null;
1380
+ return fetchCodexOAuthModels(token);
1659
1381
  }
1660
- return modelId.startsWith("gpt-") ? directProviders.openai().responses(modelId) : directProviders.openai()(modelId);
1661
- }
1662
- var OPENAI_CODEX_BASE_URL = "https://chatgpt.com/backend-api/codex";
1663
- var oauthOpenAICache = null;
1664
- async function resolveAnthropicModel(modelId) {
1665
- return directProviders.anthropic()(modelId);
1666
- }
1667
- var PROVIDER_MODEL_RESOLVERS = {
1668
- zen: resolveZenModel,
1669
- anthropic: resolveAnthropicModel,
1670
- openai: resolveOpenAIModel,
1671
- google: (modelId) => directProviders.google()(modelId),
1672
- ollama: (modelId) => directProviders.ollama().chatModel(modelId)
1673
- };
1674
- function isProviderName(provider) {
1675
- return SUPPORTED_PROVIDERS.includes(provider);
1382
+ return null;
1676
1383
  }
1677
- async function resolveModel(modelString) {
1678
- const slashIdx = modelString.indexOf("/");
1679
- if (slashIdx === -1) {
1680
- throw new Error(`Invalid model string "${modelString}". Expected format: "<provider>/<model-id>"`);
1681
- }
1682
- const provider = modelString.slice(0, slashIdx);
1683
- const modelId = modelString.slice(slashIdx + 1);
1684
- if (!isProviderName(provider)) {
1685
- throw new Error(`Unknown provider "${provider}". Supported: ${SUPPORTED_PROVIDERS.join(", ")}`);
1384
+ var CODEX_MODELS_URL = "https://chatgpt.com/backend-api/codex/models?client_version=1.0.0";
1385
+ async function fetchCodexOAuthModels(token) {
1386
+ try {
1387
+ const res = await fetch(CODEX_MODELS_URL, {
1388
+ headers: { Authorization: `Bearer ${token}` },
1389
+ signal: AbortSignal.timeout(6000)
1390
+ });
1391
+ if (!res.ok)
1392
+ return null;
1393
+ const data = await res.json();
1394
+ return (data.models ?? []).filter((m) => m.visibility === "list").map((m) => ({
1395
+ providerModelId: m.slug,
1396
+ displayName: m.display_name || m.slug,
1397
+ contextWindow: m.context_window,
1398
+ free: false
1399
+ }));
1400
+ } catch {
1401
+ return null;
1686
1402
  }
1687
- return PROVIDER_MODEL_RESOLVERS[provider](modelId);
1688
1403
  }
1689
- function discoverConnectedProviders() {
1690
- const result = [];
1691
- if (process.env.OPENCODE_API_KEY)
1692
- result.push({ name: "zen", via: "env" });
1693
- if (process.env.ANTHROPIC_API_KEY)
1694
- result.push({ name: "anthropic", via: "env" });
1695
- if (isLoggedIn("openai"))
1696
- result.push({ name: "openai", via: "oauth" });
1697
- else if (process.env.OPENAI_API_KEY)
1698
- result.push({ name: "openai", via: "env" });
1699
- if (process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY)
1700
- result.push({ name: "google", via: "env" });
1701
- if (process.env.OLLAMA_BASE_URL)
1702
- result.push({ name: "ollama", via: "env" });
1703
- return result;
1404
+ async function fetchAnthropicModels() {
1405
+ const key = process.env.ANTHROPIC_API_KEY;
1406
+ if (!key)
1407
+ return null;
1408
+ const payload = await fetchJson(`${ANTHROPIC_BASE}/v1/models`, {
1409
+ headers: {
1410
+ "anthropic-version": "2023-06-01",
1411
+ "x-api-key": key
1412
+ }
1413
+ }, 6000);
1414
+ return processModelsList(payload, "data", "id", (item, modelId) => {
1415
+ const displayName = typeof item.display_name === "string" && item.display_name.trim().length > 0 ? item.display_name : modelId;
1416
+ return {
1417
+ providerModelId: modelId,
1418
+ displayName,
1419
+ contextWindow: null,
1420
+ free: false
1421
+ };
1422
+ });
1704
1423
  }
1705
- function autoDiscoverModel() {
1706
- if (process.env.OPENCODE_API_KEY)
1707
- return "zen/claude-sonnet-4-6";
1708
- if (process.env.ANTHROPIC_API_KEY)
1709
- return "anthropic/claude-sonnet-4-6";
1710
- if (process.env.OPENAI_API_KEY || isLoggedIn("openai"))
1711
- return "openai/gpt-5.4";
1712
- if (process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY)
1713
- return "google/gemini-3.1-pro";
1714
- return "ollama/llama3.2";
1424
+ async function fetchGoogleModels() {
1425
+ const key = process.env.GOOGLE_API_KEY ?? process.env.GEMINI_API_KEY;
1426
+ if (!key)
1427
+ return null;
1428
+ const payload = await fetchJson(`${GOOGLE_BASE}/models?key=${encodeURIComponent(key)}`, {}, 6000);
1429
+ return processModelsList(payload, "models", "name", (item, modelId) => {
1430
+ const displayName = typeof item.displayName === "string" && item.displayName.trim().length > 0 ? item.displayName : modelId;
1431
+ const contextWindow = typeof item.inputTokenLimit === "number" && Number.isFinite(item.inputTokenLimit) ? Math.max(0, Math.trunc(item.inputTokenLimit)) : null;
1432
+ return {
1433
+ providerModelId: modelId,
1434
+ displayName,
1435
+ contextWindow,
1436
+ free: false
1437
+ };
1438
+ });
1715
1439
  }
1716
- async function fetchAvailableModels() {
1717
- return fetchAvailableModelsSnapshot();
1718
- }
1719
-
1720
- // src/logging/context.ts
1721
- var _currentContext = null;
1722
- function setLogContext(context) {
1723
- _currentContext = context;
1724
- }
1725
- function getLogContext() {
1726
- return _currentContext;
1727
- }
1728
- function logError(err, context) {
1729
- const logCtx = _currentContext;
1730
- if (!logCtx)
1731
- return;
1732
- logCtx.logsRepo.write(logCtx.sessionId, "error", { error: err, context });
1440
+ async function fetchOllamaModels() {
1441
+ const base = process.env.OLLAMA_BASE_URL ?? "http://localhost:11434";
1442
+ const payload = await fetchJson(`${base}/api/tags`, {}, 3000);
1443
+ return processModelsList(payload, "models", "name", (item, modelId) => {
1444
+ const details = item.details;
1445
+ let sizeSuffix = "";
1446
+ if (isRecord(details) && typeof details.parameter_size === "string") {
1447
+ sizeSuffix = ` (${details.parameter_size})`;
1448
+ }
1449
+ return {
1450
+ providerModelId: modelId,
1451
+ displayName: `${item.name}${sizeSuffix}`,
1452
+ contextWindow: null,
1453
+ free: false
1454
+ };
1455
+ });
1733
1456
  }
1734
- function logApiEvent(event, data) {
1735
- const logCtx = _currentContext;
1736
- if (!logCtx)
1737
- return;
1738
- logCtx.logsRepo.write(logCtx.sessionId, "api", { event, data });
1457
+ var PROVIDER_CANDIDATE_FETCHERS = {
1458
+ zen: fetchZenModels,
1459
+ openai: fetchOpenAIModels,
1460
+ anthropic: fetchAnthropicModels,
1461
+ google: fetchGoogleModels,
1462
+ ollama: fetchOllamaModels
1463
+ };
1464
+ async function fetchProviderCandidates(provider) {
1465
+ const fetcher = PROVIDER_CANDIDATE_FETCHERS[provider];
1466
+ if (!fetcher)
1467
+ return null;
1468
+ return fetcher();
1739
1469
  }
1740
1470
 
1741
- // src/cli/error-parse.ts
1742
- import {
1743
- APICallError,
1744
- LoadAPIKeyError,
1745
- NoContentGeneratedError,
1746
- NoSuchModelError,
1747
- RetryError
1748
- } from "ai";
1749
-
1750
- // src/llm-api/error-utils.ts
1751
- function extractObjectMessage(error) {
1752
- const record = error;
1753
- const direct = record.message;
1754
- if (typeof direct === "string" && direct.trim())
1755
- return direct.trim();
1756
- const nested = record.error;
1757
- if (typeof nested === "object" && nested !== null) {
1758
- const nestedMessage = nested.message;
1759
- if (typeof nestedMessage === "string" && nestedMessage.trim()) {
1760
- return nestedMessage.trim();
1761
- }
1471
+ // src/llm-api/provider-discovery.ts
1472
+ var DEFAULT_OLLAMA_BASE_URL = "http://localhost:11434";
1473
+ var LOCAL_PROVIDER_CACHE_TTL_MS = 30000;
1474
+ var LOCAL_PROVIDER_TIMEOUT_MS = 300;
1475
+ var knownLocalProviders = new Set;
1476
+ var knownLocalProvidersRefreshedAt = 0;
1477
+ var REMOTE_PROVIDER_ENV_KEYS = [
1478
+ { provider: "zen", envKeys: ["OPENCODE_API_KEY"] },
1479
+ { provider: "openai", envKeys: ["OPENAI_API_KEY"] },
1480
+ { provider: "anthropic", envKeys: ["ANTHROPIC_API_KEY"] },
1481
+ { provider: "google", envKeys: ["GOOGLE_API_KEY", "GEMINI_API_KEY"] }
1482
+ ];
1483
+ function hasAnyEnvKey(env, keys) {
1484
+ for (const key of keys) {
1485
+ if (env[key])
1486
+ return true;
1762
1487
  }
1763
- return null;
1488
+ return false;
1764
1489
  }
1765
- function stringifyUnknown(error) {
1766
- if (error === null || error === undefined)
1767
- return "Unknown error";
1768
- if (typeof error === "string") {
1769
- const message = error.trim();
1770
- return message || "Unknown error";
1771
- }
1772
- if (typeof error === "number" || typeof error === "boolean" || typeof error === "bigint") {
1773
- return String(error);
1774
- }
1775
- if (typeof error !== "object")
1776
- return "Unknown error";
1777
- const objectMessage = extractObjectMessage(error);
1778
- if (objectMessage)
1779
- return objectMessage;
1490
+ function appendUnique(target, value) {
1491
+ if (!target.includes(value))
1492
+ target.push(value);
1493
+ }
1494
+ async function canReachOllama(env) {
1780
1495
  try {
1781
- const value = JSON.stringify(error);
1782
- if (!value || value === "{}")
1783
- return "Unknown error";
1784
- const maxLen = 500;
1785
- return value.length > maxLen ? `${value.slice(0, maxLen - 1)}\u2026` : value;
1496
+ const response = await fetch(new URL("/api/tags", env.OLLAMA_BASE_URL ?? DEFAULT_OLLAMA_BASE_URL), { signal: AbortSignal.timeout(LOCAL_PROVIDER_TIMEOUT_MS) });
1497
+ return response.ok;
1786
1498
  } catch {
1787
- return "Unknown error";
1499
+ return false;
1788
1500
  }
1789
1501
  }
1790
- function normalizeUnknownError(error) {
1791
- if (error instanceof Error)
1792
- return error;
1793
- return new Error(stringifyUnknown(error));
1794
- }
1795
-
1796
- // src/cli/error-parse.ts
1797
- function safeStringifyErrorObject(value) {
1798
- try {
1799
- const json = JSON.stringify(value);
1800
- if (!json || json === "{}") {
1801
- return "Unknown error";
1802
- }
1803
- const maxLen = 240;
1804
- return json.length > maxLen ? `${json.slice(0, maxLen - 1)}\u2026` : json;
1805
- } catch {
1806
- return "Unknown error";
1502
+ function getRemoteConfiguredProviders(env, opts) {
1503
+ const providers = REMOTE_PROVIDER_ENV_KEYS.filter((entry) => hasAnyEnvKey(env, entry.envKeys)).map((entry) => entry.provider);
1504
+ if (!providers.includes("openai") && opts?.openaiLoggedIn) {
1505
+ providers.push("openai");
1807
1506
  }
1507
+ return providers;
1808
1508
  }
1809
- function toUserErrorMessage(err) {
1810
- if (err instanceof Error) {
1811
- const msg = err.message.trim();
1812
- if (msg)
1813
- return msg;
1509
+ function isLocalProviderConnectionStateStale(now = Date.now()) {
1510
+ return now - knownLocalProvidersRefreshedAt > LOCAL_PROVIDER_CACHE_TTL_MS;
1511
+ }
1512
+ function getKnownLocalProviders(now = Date.now()) {
1513
+ if (isLocalProviderConnectionStateStale(now))
1514
+ return [];
1515
+ return Array.from(knownLocalProviders).sort((a, b) => a.localeCompare(b));
1516
+ }
1517
+ async function refreshLocalProviderConnections(env, now = Date.now()) {
1518
+ const localProviders = [];
1519
+ if (await canReachOllama(env))
1520
+ localProviders.push("ollama");
1521
+ knownLocalProviders.clear();
1522
+ for (const provider of localProviders)
1523
+ knownLocalProviders.add(provider);
1524
+ knownLocalProvidersRefreshedAt = now;
1525
+ return localProviders;
1526
+ }
1527
+ function getLocalProviderNames(connectedProviders) {
1528
+ return connectedProviders.filter((provider) => provider.via === "local").map((provider) => provider.name).sort((a, b) => a.localeCompare(b));
1529
+ }
1530
+ function getVisibleProviders(env, opts) {
1531
+ const providers = getRemoteConfiguredProviders(env, opts);
1532
+ for (const provider of opts?.localProviders ?? getKnownLocalProviders(opts?.now)) {
1533
+ appendUnique(providers, provider);
1814
1534
  }
1815
- if (typeof err === "string") {
1816
- const msg = err.trim();
1817
- if (msg)
1818
- return msg;
1535
+ return providers;
1536
+ }
1537
+ async function discoverProviderConnections(env, opts) {
1538
+ const result = [];
1539
+ if (env.OPENCODE_API_KEY)
1540
+ result.push({ name: "zen", via: "env" });
1541
+ if (env.ANTHROPIC_API_KEY)
1542
+ result.push({ name: "anthropic", via: "env" });
1543
+ if (opts?.openaiLoggedIn)
1544
+ result.push({ name: "openai", via: "oauth" });
1545
+ else if (env.OPENAI_API_KEY)
1546
+ result.push({ name: "openai", via: "env" });
1547
+ if (env.GOOGLE_API_KEY || env.GEMINI_API_KEY) {
1548
+ result.push({ name: "google", via: "env" });
1819
1549
  }
1820
- if (typeof err === "number" || typeof err === "boolean" || typeof err === "bigint") {
1821
- return String(err);
1550
+ const localProviders = opts?.localProviders ?? await refreshLocalProviderConnections(env, opts?.now);
1551
+ for (const provider of localProviders) {
1552
+ result.push({ name: provider, via: "local" });
1822
1553
  }
1823
- if (typeof err === "object" && err !== null) {
1824
- const objectMessage = extractObjectMessage(err);
1825
- if (objectMessage)
1826
- return objectMessage;
1827
- return safeStringifyErrorObject(err);
1554
+ return result;
1555
+ }
1556
+
1557
+ // src/llm-api/model-info.ts
1558
+ var MODELS_DEV_SYNC_KEY = "last_models_dev_sync_at";
1559
+ var PROVIDER_SYNC_KEY_PREFIX = "last_provider_sync_at:";
1560
+ var CACHE_VERSION_KEY = "model_info_cache_version";
1561
+ var CACHE_VERSION = 5;
1562
+ var MODEL_INFO_TTL_MS = 24 * 60 * 60 * 1000;
1563
+ var runtimeCache = emptyRuntimeCache();
1564
+ var loaded = false;
1565
+ var refreshInFlight = null;
1566
+ function isStaleTimestamp(timestamp, now = Date.now(), ttlMs = MODEL_INFO_TTL_MS) {
1567
+ if (timestamp === null)
1568
+ return true;
1569
+ return now - timestamp > ttlMs;
1570
+ }
1571
+ function loadCacheFromDb() {
1572
+ runtimeCache = buildRuntimeCache(listModelCapabilities(), listProviderModels(), listModelInfoState());
1573
+ loaded = true;
1574
+ }
1575
+ function ensureLoaded() {
1576
+ if (!loaded)
1577
+ loadCacheFromDb();
1578
+ }
1579
+ function initModelInfoCache() {
1580
+ loadCacheFromDb();
1581
+ }
1582
+ function parseStateInt(key) {
1583
+ const raw = runtimeCache.state.get(key);
1584
+ if (!raw)
1585
+ return null;
1586
+ const value = Number.parseInt(raw, 10);
1587
+ if (!Number.isFinite(value))
1588
+ return null;
1589
+ return value;
1590
+ }
1591
+ function getRemoteProvidersFromEnv(env) {
1592
+ return getRemoteConfiguredProviders(env, {
1593
+ openaiLoggedIn: isLoggedIn("openai")
1594
+ });
1595
+ }
1596
+ function getProvidersToRefreshFromEnv(env, opts) {
1597
+ const localProviders = opts?.localProviders;
1598
+ return getVisibleProviders(env, {
1599
+ openaiLoggedIn: isLoggedIn("openai"),
1600
+ ...localProviders ? { localProviders } : {}
1601
+ });
1602
+ }
1603
+ function getVisibleProvidersForSnapshotFromEnv(env, opts) {
1604
+ const localProviders = opts?.localProviders;
1605
+ return new Set(getProvidersToRefreshFromEnv(env, localProviders ? { localProviders } : undefined));
1606
+ }
1607
+ function getConfiguredProvidersForSync(localProviders) {
1608
+ return getProvidersToRefreshFromEnv(process.env, localProviders ? { localProviders } : undefined);
1609
+ }
1610
+ function getProvidersRequiredForFreshness() {
1611
+ return getRemoteProvidersFromEnv(process.env);
1612
+ }
1613
+ function getProviderSyncKey(provider) {
1614
+ return `${PROVIDER_SYNC_KEY_PREFIX}${provider}`;
1615
+ }
1616
+ function isModelInfoStale(now = Date.now()) {
1617
+ ensureLoaded();
1618
+ if (parseStateInt(CACHE_VERSION_KEY) !== CACHE_VERSION)
1619
+ return true;
1620
+ if (isStaleTimestamp(parseStateInt(MODELS_DEV_SYNC_KEY), now))
1621
+ return true;
1622
+ if (isLocalProviderConnectionStateStale(now))
1623
+ return true;
1624
+ for (const provider of getProvidersRequiredForFreshness()) {
1625
+ const providerSync = parseStateInt(getProviderSyncKey(provider));
1626
+ if (isStaleTimestamp(providerSync, now))
1627
+ return true;
1828
1628
  }
1829
- return "Unknown error";
1629
+ return false;
1830
1630
  }
1831
- function parseAppError(err) {
1832
- if (typeof err === "string") {
1833
- return { headline: err };
1631
+ function getLastSyncAt() {
1632
+ let latest = parseStateInt(MODELS_DEV_SYNC_KEY);
1633
+ for (const provider of getProvidersRequiredForFreshness()) {
1634
+ const value = parseStateInt(getProviderSyncKey(provider));
1635
+ if (value !== null && (latest === null || value > latest))
1636
+ latest = value;
1834
1637
  }
1835
- if (err instanceof RetryError) {
1836
- const inner = parseAppError(err.lastError);
1837
- return {
1838
- headline: `Retries exhausted: ${inner.headline}`,
1839
- ...inner.hint ? { hint: inner.hint } : {}
1840
- };
1638
+ return latest;
1639
+ }
1640
+ function providerRowsFromCandidates(candidates, matchIndex, updatedAt) {
1641
+ return candidates.map((candidate) => ({
1642
+ provider_model_id: candidate.providerModelId,
1643
+ display_name: candidate.displayName,
1644
+ canonical_model_id: matchCanonicalModelId(candidate.providerModelId, matchIndex),
1645
+ context_window: candidate.contextWindow,
1646
+ free: candidate.free ? 1 : 0,
1647
+ updated_at: updatedAt
1648
+ }));
1649
+ }
1650
+ async function refreshModelInfoInternal(localProviders) {
1651
+ ensureLoaded();
1652
+ const now = Date.now();
1653
+ const discoveredLocalProviders = localProviders ?? await refreshLocalProviderConnections(process.env);
1654
+ const providers = getConfiguredProvidersForSync(discoveredLocalProviders);
1655
+ const providerResults = await Promise.all(providers.map(async (provider) => ({
1656
+ provider,
1657
+ candidates: await fetchProviderCandidates(provider)
1658
+ })));
1659
+ const modelsDevPayload = await fetchModelsDevPayload();
1660
+ let matchIndex = runtimeCache.matchIndex;
1661
+ if (modelsDevPayload !== null) {
1662
+ const capabilityRows = parseModelsDevCapabilities(modelsDevPayload, now);
1663
+ if (capabilityRows.length > 0) {
1664
+ replaceModelCapabilities(capabilityRows);
1665
+ setModelInfoState(MODELS_DEV_SYNC_KEY, String(now));
1666
+ matchIndex = buildModelMatchIndex(capabilityRows.map((row) => row.canonical_model_id));
1667
+ }
1841
1668
  }
1842
- if (err instanceof APICallError) {
1843
- const body = String(err.message).toLowerCase();
1844
- if (body.includes("context_length_exceeded") || body.includes("maximum context length") || body.includes("too many tokens") || body.includes("request too large")) {
1845
- return {
1846
- headline: "Max context size reached",
1847
- hint: "Use /new to start a fresh session"
1848
- };
1849
- }
1850
- if (err.statusCode === 429) {
1851
- return {
1852
- headline: "Rate limit hit",
1853
- hint: "Wait a moment and retry, or switch model with /model"
1854
- };
1855
- }
1856
- if (err.statusCode === 401 || err.statusCode === 403) {
1857
- return {
1858
- headline: "Auth failed",
1859
- hint: "Check the relevant provider API key env var"
1860
- };
1861
- }
1862
- return {
1863
- headline: `API error ${err.statusCode ?? "unknown"}`,
1864
- ...err.url ? { hint: err.url } : {}
1865
- };
1866
- }
1867
- if (err instanceof NoContentGeneratedError) {
1868
- return {
1869
- headline: "Model returned empty response",
1870
- hint: "Try rephrasing or switching model with /model"
1871
- };
1669
+ for (const result of providerResults) {
1670
+ if (result.candidates === null)
1671
+ continue;
1672
+ const rows = providerRowsFromCandidates(result.candidates, matchIndex, now);
1673
+ replaceProviderModels(result.provider, rows);
1674
+ setModelInfoState(getProviderSyncKey(result.provider), String(now));
1872
1675
  }
1873
- if (err instanceof LoadAPIKeyError) {
1874
- return {
1875
- headline: "API key not found",
1876
- hint: "Set the relevant provider env var"
1877
- };
1676
+ setModelInfoState(CACHE_VERSION_KEY, String(CACHE_VERSION));
1677
+ loadCacheFromDb();
1678
+ }
1679
+ function refreshModelInfoInBackground(opts) {
1680
+ ensureLoaded();
1681
+ const force = opts?.force ?? false;
1682
+ if (!force && !isModelInfoStale() && !shouldBlockOnMissingVisibleProviderModels({
1683
+ hasAnyCachedModels: runtimeCache.providerModelsByKey.size > 0,
1684
+ hasCachedModelsForAllVisibleProviders: hasCachedModelsForAllVisibleProviders()
1685
+ })) {
1686
+ return Promise.resolve();
1878
1687
  }
1879
- if (err instanceof NoSuchModelError) {
1880
- return {
1881
- headline: "Model not found",
1882
- hint: "Use /model to pick a valid model"
1883
- };
1688
+ if (refreshInFlight)
1689
+ return refreshInFlight;
1690
+ refreshInFlight = refreshModelInfoInternal(opts?.localProviders).finally(() => {
1691
+ refreshInFlight = null;
1692
+ });
1693
+ return refreshInFlight;
1694
+ }
1695
+ function isModelInfoRefreshing() {
1696
+ return refreshInFlight !== null;
1697
+ }
1698
+ function resolveModelInfo(modelString) {
1699
+ ensureLoaded();
1700
+ return resolveModelInfoInCache(modelString, runtimeCache);
1701
+ }
1702
+ function getContextWindow(modelString) {
1703
+ return resolveModelInfo(modelString)?.contextWindow ?? null;
1704
+ }
1705
+ function getMaxOutputTokens(modelString) {
1706
+ return resolveModelInfo(modelString)?.maxOutputTokens ?? null;
1707
+ }
1708
+ function supportsThinking(modelString) {
1709
+ return resolveModelInfo(modelString)?.reasoning ?? false;
1710
+ }
1711
+ function getCachedModelIds() {
1712
+ ensureLoaded();
1713
+ const visible = getVisibleProvidersForSnapshotFromEnv(process.env, {
1714
+ localProviders: getKnownLocalProviders()
1715
+ });
1716
+ const ids = [];
1717
+ for (const row of runtimeCache.providerModelsByKey.values()) {
1718
+ if (visible.has(row.provider)) {
1719
+ ids.push(`${row.provider}/${row.providerModelId}`);
1720
+ }
1884
1721
  }
1885
- const isObj = typeof err === "object" && err !== null;
1886
- const code = isObj && "code" in err ? String(err.code) : undefined;
1887
- const message = toUserErrorMessage(err);
1888
- if (code === "ECONNREFUSED" || message.includes("ECONNREFUSED")) {
1889
- return {
1890
- headline: "Connection failed",
1891
- hint: "Check network or local server"
1892
- };
1722
+ ids.sort((a, b) => a.localeCompare(b));
1723
+ return ids;
1724
+ }
1725
+ function hasCachedModelsForAllVisibleProviders() {
1726
+ const visible = getVisibleProvidersForSnapshotFromEnv(process.env, {
1727
+ localProviders: getKnownLocalProviders()
1728
+ });
1729
+ for (const provider of visible) {
1730
+ let found = false;
1731
+ for (const row of runtimeCache.providerModelsByKey.values()) {
1732
+ if (row.provider === provider) {
1733
+ found = true;
1734
+ break;
1735
+ }
1736
+ }
1737
+ if (!found)
1738
+ return false;
1893
1739
  }
1894
- if (code === "ECONNRESET" || message.includes("ECONNRESET") || message.includes("socket connection was closed unexpectedly")) {
1895
- return {
1896
- headline: "Connection lost",
1897
- hint: "The server closed the connection \u2014 retry or switch model with /model"
1898
- };
1740
+ return true;
1741
+ }
1742
+ function shouldBlockOnMissingVisibleProviderModels(opts) {
1743
+ return !opts.hasAnyCachedModels || !opts.hasCachedModelsForAllVisibleProviders;
1744
+ }
1745
+ async function fetchAvailableModelsSnapshot() {
1746
+ ensureLoaded();
1747
+ const stale = isModelInfoStale();
1748
+ const shouldBlock = shouldBlockOnMissingVisibleProviderModels({
1749
+ hasAnyCachedModels: runtimeCache.providerModelsByKey.size > 0,
1750
+ hasCachedModelsForAllVisibleProviders: hasCachedModelsForAllVisibleProviders()
1751
+ });
1752
+ if (shouldBlock) {
1753
+ await refreshModelInfoInBackground({ force: true });
1754
+ } else if (stale && !isModelInfoRefreshing()) {
1755
+ refreshModelInfoInBackground();
1899
1756
  }
1900
- const firstLine = message.split(`
1901
- `)[0]?.trim() || "Unknown error";
1902
- return { headline: firstLine };
1757
+ return {
1758
+ models: readLiveModelsFromCache(runtimeCache, getVisibleProvidersForSnapshotFromEnv(process.env, {
1759
+ localProviders: getKnownLocalProviders()
1760
+ })),
1761
+ stale: isModelInfoStale(),
1762
+ refreshing: isModelInfoRefreshing(),
1763
+ lastSyncAt: getLastSyncAt()
1764
+ };
1903
1765
  }
1904
1766
 
1905
1767
  // src/cli/skills.ts
@@ -1912,9 +1774,9 @@ import {
1912
1774
  readSync,
1913
1775
  statSync
1914
1776
  } from "fs";
1915
- import { homedir as homedir3 } from "os";
1777
+ import { homedir as homedir4 } from "os";
1916
1778
  import { dirname as dirname2, join as join3, resolve as resolve2 } from "path";
1917
- import * as c from "yoctocolors";
1779
+ import * as c7 from "yoctocolors";
1918
1780
 
1919
1781
  // src/cli/frontmatter.ts
1920
1782
  var FM_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
@@ -1962,253 +1824,510 @@ function parseFrontmatter(raw) {
1962
1824
  return { meta, body: (m[2] ?? "").trim() };
1963
1825
  }
1964
1826
 
1965
- // src/cli/skills.ts
1966
- var MAX_FRONTMATTER_BYTES = 64 * 1024;
1967
- var SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
1968
- var MAX_SKILL_NAME_LENGTH = 64;
1969
- var warnedInvalidSkills = new Set;
1970
- var warnedSkillIssues = new Set;
1971
- function parseSkillFrontmatter(filePath) {
1972
- let fd = null;
1973
- try {
1974
- fd = openSync(filePath, "r");
1975
- const chunk = Buffer.allocUnsafe(MAX_FRONTMATTER_BYTES);
1976
- const bytesRead = readSync(fd, chunk, 0, MAX_FRONTMATTER_BYTES, 0);
1977
- const text = chunk.toString("utf8", 0, bytesRead);
1978
- const { meta } = parseFrontmatter(text);
1979
- const result = {};
1980
- if (typeof meta.name === "string" && meta.name)
1981
- result.name = meta.name;
1982
- if (typeof meta.description === "string" && meta.description)
1983
- result.description = meta.description;
1984
- if (typeof meta.context === "string" && meta.context)
1985
- result.context = meta.context;
1986
- if (typeof meta.compatibility === "string" && meta.compatibility)
1987
- result.compatibility = meta.compatibility;
1988
- return result;
1989
- } catch {
1990
- return {};
1991
- } finally {
1992
- if (fd !== null)
1993
- closeSync(fd);
1994
- }
1827
+ // src/cli/output.ts
1828
+ import { homedir as homedir3 } from "os";
1829
+ import * as c6 from "yoctocolors";
1830
+
1831
+ // src/agent/context-files.ts
1832
+ import { existsSync as existsSync2, readFileSync } from "fs";
1833
+ import { homedir as homedir2 } from "os";
1834
+ import { dirname, join as join2, resolve } from "path";
1835
+ var HOME = homedir2();
1836
+ function tilde(p) {
1837
+ return p.startsWith(HOME) ? `~${p.slice(HOME.length)}` : p;
1995
1838
  }
1996
- function getCandidateFrontmatter(candidate) {
1997
- if (!candidate.frontmatter) {
1998
- candidate.frontmatter = parseSkillFrontmatter(candidate.filePath);
1999
- }
2000
- return candidate.frontmatter;
1839
+ function globalContextCandidates(homeDir = HOME) {
1840
+ return [
1841
+ {
1842
+ abs: join2(homeDir, ".agents", "AGENTS.md"),
1843
+ label: "~/.agents/AGENTS.md"
1844
+ },
1845
+ {
1846
+ abs: join2(homeDir, ".agents", "CLAUDE.md"),
1847
+ label: "~/.agents/CLAUDE.md"
1848
+ },
1849
+ {
1850
+ abs: join2(homeDir, ".claude", "CLAUDE.md"),
1851
+ label: "~/.claude/CLAUDE.md"
1852
+ }
1853
+ ];
2001
1854
  }
2002
- function candidateConflictName(candidate) {
2003
- return getCandidateFrontmatter(candidate).name?.trim() || candidate.folderName;
1855
+ function dirContextCandidates(dir) {
1856
+ const rel = (p) => tilde(resolve(dir, p));
1857
+ return [
1858
+ {
1859
+ abs: join2(dir, ".agents", "AGENTS.md"),
1860
+ label: `${rel(".agents/AGENTS.md")}`
1861
+ },
1862
+ {
1863
+ abs: join2(dir, ".agents", "CLAUDE.md"),
1864
+ label: `${rel(".agents/CLAUDE.md")}`
1865
+ },
1866
+ {
1867
+ abs: join2(dir, ".claude", "CLAUDE.md"),
1868
+ label: `${rel(".claude/CLAUDE.md")}`
1869
+ },
1870
+ { abs: join2(dir, "CLAUDE.md"), label: `${rel("CLAUDE.md")}` },
1871
+ { abs: join2(dir, "AGENTS.md"), label: `${rel("AGENTS.md")}` }
1872
+ ];
2004
1873
  }
2005
- function findGitBoundary(cwd) {
2006
- let current = resolve2(cwd);
2007
- while (true) {
2008
- if (existsSync3(join3(current, ".git")))
2009
- return current;
2010
- const parent = dirname2(current);
2011
- if (parent === current)
2012
- return null;
2013
- current = parent;
2014
- }
1874
+ function existingCandidates(candidates) {
1875
+ return candidates.filter((candidate) => existsSync2(candidate.abs));
2015
1876
  }
2016
- function localSearchRoots(cwd) {
2017
- const start = resolve2(cwd);
2018
- const stop = findGitBoundary(start);
2019
- if (!stop)
2020
- return [start];
2021
- const roots = [];
2022
- let current = start;
1877
+ function nearestLocalContextCandidates(cwd) {
1878
+ let current = resolve(cwd);
2023
1879
  while (true) {
2024
- roots.push(current);
2025
- if (current === stop)
1880
+ const matches = existingCandidates(dirContextCandidates(current));
1881
+ if (matches.length > 0)
1882
+ return matches;
1883
+ if (existsSync2(join2(current, ".git")))
2026
1884
  break;
2027
- const parent = dirname2(current);
1885
+ const parent = dirname(current);
2028
1886
  if (parent === current)
2029
1887
  break;
2030
1888
  current = parent;
2031
1889
  }
2032
- return roots;
1890
+ return [];
2033
1891
  }
2034
- var MAX_SKILL_SCAN_DEPTH = 5;
2035
- var MAX_SKILL_DIRS_SCANNED = 2000;
2036
- function listSkillCandidates(skillsDir, source, rootPath) {
2037
- if (!existsSync3(skillsDir))
2038
- return [];
2039
- const candidates = [];
2040
- let dirsScanned = 0;
2041
- function walk(dir, depth) {
2042
- if (depth > MAX_SKILL_SCAN_DEPTH || dirsScanned >= MAX_SKILL_DIRS_SCANNED)
2043
- return;
2044
- let entries;
2045
- try {
2046
- entries = readdirSync(dir).sort((a, b) => a.localeCompare(b));
2047
- } catch {
2048
- return;
2049
- }
2050
- for (const entry of entries) {
2051
- if (dirsScanned >= MAX_SKILL_DIRS_SCANNED)
2052
- return;
2053
- const entryPath = join3(dir, entry);
2054
- try {
2055
- if (!statSync(entryPath).isDirectory())
2056
- continue;
2057
- } catch {
2058
- continue;
1892
+ function discoverContextFiles(cwd, homeDir) {
1893
+ return [
1894
+ ...existingCandidates(globalContextCandidates(homeDir)).map((c) => c.label),
1895
+ ...nearestLocalContextCandidates(cwd).map((c) => c.label)
1896
+ ];
1897
+ }
1898
+ function tryReadFile(p) {
1899
+ if (!existsSync2(p))
1900
+ return null;
1901
+ try {
1902
+ return readFileSync(p, "utf-8");
1903
+ } catch {
1904
+ return null;
1905
+ }
1906
+ }
1907
+ function readCandidates(candidates) {
1908
+ const parts = [];
1909
+ for (const c of candidates) {
1910
+ const content = tryReadFile(c.abs);
1911
+ if (content)
1912
+ parts.push(content);
1913
+ }
1914
+ return parts.length > 0 ? parts.join(`
1915
+
1916
+ `) : null;
1917
+ }
1918
+ function loadGlobalContextFile(homeDir) {
1919
+ return readCandidates(globalContextCandidates(homeDir));
1920
+ }
1921
+ function loadLocalContextFile(cwd) {
1922
+ return readCandidates(nearestLocalContextCandidates(cwd));
1923
+ }
1924
+
1925
+ // src/llm-api/providers.ts
1926
+ import { createAnthropic } from "@ai-sdk/anthropic";
1927
+ import { createGoogleGenerativeAI } from "@ai-sdk/google";
1928
+ import { createOpenAI } from "@ai-sdk/openai";
1929
+ import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
1930
+
1931
+ // src/llm-api/model-routing.ts
1932
+ function parseModelString(modelString) {
1933
+ const slashIdx = modelString.indexOf("/");
1934
+ if (slashIdx === -1)
1935
+ return { provider: modelString, modelId: "" };
1936
+ return {
1937
+ provider: modelString.slice(0, slashIdx),
1938
+ modelId: modelString.slice(slashIdx + 1)
1939
+ };
1940
+ }
1941
+ function isZenProvider(provider) {
1942
+ return provider === "zen";
1943
+ }
1944
+ function isAnthropicModelFamily(modelString) {
1945
+ const { provider, modelId } = parseModelString(modelString);
1946
+ return provider === "anthropic" || isZenProvider(provider) && modelId.startsWith("claude-");
1947
+ }
1948
+ function isGeminiModelFamily(modelString) {
1949
+ const { provider, modelId } = parseModelString(modelString);
1950
+ return (provider === "google" || isZenProvider(provider)) && modelId.startsWith("gemini-");
1951
+ }
1952
+ function isOpenAIGPTModelFamily(modelString) {
1953
+ const { provider, modelId } = parseModelString(modelString);
1954
+ return (provider === "openai" || isZenProvider(provider)) && modelId.startsWith("gpt-");
1955
+ }
1956
+ function isOpenAIReasoningModelFamily(modelString) {
1957
+ const { provider, modelId } = parseModelString(modelString);
1958
+ return (provider === "openai" || isZenProvider(provider)) && (modelId.startsWith("o") || modelId.startsWith("gpt-5"));
1959
+ }
1960
+ function isZenOpenAICompatibleChatModel(modelString) {
1961
+ const { provider, modelId } = parseModelString(modelString);
1962
+ if (!isZenProvider(provider))
1963
+ return false;
1964
+ return !modelId.startsWith("gpt-") && !modelId.startsWith("gemini-") && !modelId.startsWith("claude-");
1965
+ }
1966
+ function getZenBackend(modelId) {
1967
+ if (modelId.startsWith("claude-"))
1968
+ return "anthropic";
1969
+ if (modelId.startsWith("gpt-"))
1970
+ return "openai";
1971
+ if (modelId.startsWith("gemini-"))
1972
+ return "google";
1973
+ return "compat";
1974
+ }
1975
+
1976
+ // src/llm-api/providers.ts
1977
+ var SUPPORTED_PROVIDERS = [
1978
+ "zen",
1979
+ "anthropic",
1980
+ "openai",
1981
+ "google",
1982
+ "ollama"
1983
+ ];
1984
+ var ZEN_BASE2 = "https://opencode.ai/zen/v1";
1985
+ function requireEnv(name) {
1986
+ const value = process.env[name];
1987
+ if (!value)
1988
+ throw new Error(`${name} is not set`);
1989
+ return value;
1990
+ }
1991
+ function requireAnyEnv(names) {
1992
+ for (const name of names) {
1993
+ const value = process.env[name];
1994
+ if (value)
1995
+ return value;
1996
+ }
1997
+ throw new Error(`${names.join(" or ")} is not set`);
1998
+ }
1999
+ function lazy(factory) {
2000
+ let instance = null;
2001
+ return () => {
2002
+ if (instance === null) {
2003
+ instance = factory();
2004
+ }
2005
+ return instance;
2006
+ };
2007
+ }
2008
+ var zenProviders = {
2009
+ anthropic: lazy(() => createAnthropic({
2010
+ fetch,
2011
+ apiKey: requireEnv("OPENCODE_API_KEY"),
2012
+ baseURL: ZEN_BASE2
2013
+ })),
2014
+ openai: lazy(() => createOpenAI({
2015
+ fetch,
2016
+ apiKey: requireEnv("OPENCODE_API_KEY"),
2017
+ baseURL: ZEN_BASE2
2018
+ })),
2019
+ google: lazy(() => createGoogleGenerativeAI({
2020
+ fetch,
2021
+ apiKey: requireEnv("OPENCODE_API_KEY"),
2022
+ baseURL: ZEN_BASE2
2023
+ })),
2024
+ compat: lazy(() => createOpenAICompatible({
2025
+ fetch,
2026
+ name: "zen-compat",
2027
+ apiKey: requireEnv("OPENCODE_API_KEY"),
2028
+ baseURL: ZEN_BASE2
2029
+ }))
2030
+ };
2031
+ var directProviders = {
2032
+ anthropic: lazy(() => createAnthropic({
2033
+ fetch,
2034
+ apiKey: requireEnv("ANTHROPIC_API_KEY")
2035
+ })),
2036
+ openai: lazy(() => createOpenAI({
2037
+ fetch,
2038
+ apiKey: requireEnv("OPENAI_API_KEY")
2039
+ })),
2040
+ google: lazy(() => createGoogleGenerativeAI({
2041
+ fetch,
2042
+ apiKey: requireAnyEnv(["GOOGLE_API_KEY", "GEMINI_API_KEY"])
2043
+ })),
2044
+ ollama: lazy(() => {
2045
+ const baseURL = process.env.OLLAMA_BASE_URL ?? "http://localhost:11434";
2046
+ return createOpenAICompatible({
2047
+ name: "ollama",
2048
+ baseURL: `${baseURL}/v1`,
2049
+ apiKey: "ollama",
2050
+ fetch
2051
+ });
2052
+ })
2053
+ };
2054
+ var ZEN_BACKEND_RESOLVERS = {
2055
+ anthropic: (modelId) => zenProviders.anthropic()(modelId),
2056
+ openai: (modelId) => zenProviders.openai().responses(modelId),
2057
+ google: (modelId) => zenProviders.google()(modelId),
2058
+ compat: (modelId) => zenProviders.compat()(modelId)
2059
+ };
2060
+ function resolveZenModel(modelId) {
2061
+ return ZEN_BACKEND_RESOLVERS[getZenBackend(modelId)](modelId);
2062
+ }
2063
+ function createOAuthOpenAIProvider(token) {
2064
+ const accountId = extractAccountId(token);
2065
+ return createOpenAI({
2066
+ apiKey: "oauth",
2067
+ baseURL: OPENAI_CODEX_BASE_URL,
2068
+ fetch: (input, init) => {
2069
+ const h = new Headers(init?.headers);
2070
+ h.delete("OpenAI-Organization");
2071
+ h.delete("OpenAI-Project");
2072
+ h.set("Authorization", `Bearer ${token}`);
2073
+ if (accountId)
2074
+ h.set("chatgpt-account-id", accountId);
2075
+ let body = init?.body;
2076
+ if (typeof body === "string") {
2077
+ try {
2078
+ const parsed = JSON.parse(body);
2079
+ if (parsed.input && Array.isArray(parsed.input)) {
2080
+ if (!parsed.instructions) {
2081
+ const sysIdx = parsed.input.findIndex((m) => m.role === "developer" || m.role === "system");
2082
+ if (sysIdx !== -1) {
2083
+ const sysMsg = parsed.input[sysIdx];
2084
+ parsed.instructions = typeof sysMsg.content === "string" ? sysMsg.content : JSON.stringify(sysMsg.content);
2085
+ parsed.input.splice(sysIdx, 1);
2086
+ }
2087
+ }
2088
+ delete parsed.max_output_tokens;
2089
+ parsed.store = false;
2090
+ parsed.stream = true;
2091
+ body = JSON.stringify(parsed);
2092
+ }
2093
+ } catch {}
2059
2094
  }
2060
- dirsScanned++;
2061
- const filePath = join3(entryPath, "SKILL.md");
2062
- if (existsSync3(filePath)) {
2063
- candidates.push({
2064
- folderName: entry,
2065
- filePath,
2066
- rootPath,
2067
- source
2068
- });
2069
- } else {
2070
- walk(entryPath, depth + 1);
2095
+ return fetch(input, {
2096
+ ...init,
2097
+ body,
2098
+ headers: Object.fromEntries(h.entries())
2099
+ });
2100
+ }
2101
+ });
2102
+ }
2103
+ async function resolveOpenAIModel(modelId) {
2104
+ if (isLoggedIn("openai")) {
2105
+ const token = await getAccessToken("openai");
2106
+ if (token) {
2107
+ if (!oauthOpenAICache || oauthOpenAICache.token !== token) {
2108
+ oauthOpenAICache = {
2109
+ token,
2110
+ provider: createOAuthOpenAIProvider(token)
2111
+ };
2071
2112
  }
2113
+ return oauthOpenAICache.provider.responses(modelId);
2114
+ }
2115
+ }
2116
+ return modelId.startsWith("gpt-") ? directProviders.openai().responses(modelId) : directProviders.openai()(modelId);
2117
+ }
2118
+ var OPENAI_CODEX_BASE_URL = "https://chatgpt.com/backend-api/codex";
2119
+ var oauthOpenAICache = null;
2120
+ async function resolveAnthropicModel(modelId) {
2121
+ return directProviders.anthropic()(modelId);
2122
+ }
2123
+ var PROVIDER_MODEL_RESOLVERS = {
2124
+ zen: resolveZenModel,
2125
+ anthropic: resolveAnthropicModel,
2126
+ openai: resolveOpenAIModel,
2127
+ google: (modelId) => directProviders.google()(modelId),
2128
+ ollama: (modelId) => directProviders.ollama().chatModel(modelId)
2129
+ };
2130
+ function isProviderName(provider) {
2131
+ return SUPPORTED_PROVIDERS.includes(provider);
2132
+ }
2133
+ async function resolveModel(modelString) {
2134
+ const slashIdx = modelString.indexOf("/");
2135
+ if (slashIdx === -1) {
2136
+ throw new Error(`Invalid model string "${modelString}". Expected format: "<provider>/<model-id>"`);
2137
+ }
2138
+ const provider = modelString.slice(0, slashIdx);
2139
+ const modelId = modelString.slice(slashIdx + 1);
2140
+ if (!isProviderName(provider)) {
2141
+ throw new Error(`Unknown provider "${provider}". Supported: ${SUPPORTED_PROVIDERS.join(", ")}`);
2142
+ }
2143
+ return PROVIDER_MODEL_RESOLVERS[provider](modelId);
2144
+ }
2145
+ async function discoverConnectedProviders() {
2146
+ return discoverProviderConnections(process.env, {
2147
+ openaiLoggedIn: isLoggedIn("openai")
2148
+ });
2149
+ }
2150
+ function autoDiscoverModel() {
2151
+ if (process.env.OPENCODE_API_KEY)
2152
+ return "zen/claude-sonnet-4-6";
2153
+ if (process.env.ANTHROPIC_API_KEY)
2154
+ return "anthropic/claude-sonnet-4-6";
2155
+ if (process.env.OPENAI_API_KEY || isLoggedIn("openai"))
2156
+ return "openai/gpt-5.4";
2157
+ if (process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY)
2158
+ return "google/gemini-3.1-pro";
2159
+ return "ollama/llama3.2";
2160
+ }
2161
+ async function fetchAvailableModels() {
2162
+ return fetchAvailableModelsSnapshot();
2163
+ }
2164
+
2165
+ // src/cli/error-parse.ts
2166
+ import {
2167
+ APICallError,
2168
+ LoadAPIKeyError,
2169
+ NoContentGeneratedError,
2170
+ NoSuchModelError,
2171
+ RetryError
2172
+ } from "ai";
2173
+
2174
+ // src/llm-api/error-utils.ts
2175
+ function extractObjectMessage(error) {
2176
+ const record = error;
2177
+ const direct = record.message;
2178
+ if (typeof direct === "string" && direct.trim())
2179
+ return direct.trim();
2180
+ const nested = record.error;
2181
+ if (typeof nested === "object" && nested !== null) {
2182
+ const nestedMessage = nested.message;
2183
+ if (typeof nestedMessage === "string" && nestedMessage.trim()) {
2184
+ return nestedMessage.trim();
2072
2185
  }
2073
2186
  }
2074
- walk(skillsDir, 1);
2075
- return candidates;
2076
- }
2077
- function warnInvalidSkill(filePath, reason) {
2078
- const key = `${filePath}:${reason}`;
2079
- if (warnedInvalidSkills.has(key))
2080
- return;
2081
- warnedInvalidSkills.add(key);
2082
- writeln(`${G.warn} skipping invalid skill ${filePath}: ${reason}`);
2187
+ return null;
2188
+ }
2189
+ function stringifyUnknown(error) {
2190
+ if (error === null || error === undefined)
2191
+ return "Unknown error";
2192
+ if (typeof error === "string") {
2193
+ const message = error.trim();
2194
+ return message || "Unknown error";
2195
+ }
2196
+ if (typeof error === "number" || typeof error === "boolean" || typeof error === "bigint") {
2197
+ return String(error);
2198
+ }
2199
+ if (typeof error !== "object")
2200
+ return "Unknown error";
2201
+ const objectMessage = extractObjectMessage(error);
2202
+ if (objectMessage)
2203
+ return objectMessage;
2204
+ try {
2205
+ const value = JSON.stringify(error);
2206
+ if (!value || value === "{}")
2207
+ return "Unknown error";
2208
+ const maxLen = 500;
2209
+ return value.length > maxLen ? `${value.slice(0, maxLen - 1)}\u2026` : value;
2210
+ } catch {
2211
+ return "Unknown error";
2212
+ }
2083
2213
  }
2084
- function warnSkillIssue(filePath, reason) {
2085
- const key = `${filePath}:${reason}`;
2086
- if (warnedSkillIssues.has(key))
2087
- return;
2088
- warnedSkillIssues.add(key);
2089
- writeln(`${G.warn} skill ${filePath}: ${reason}`);
2214
+ function normalizeUnknownError(error) {
2215
+ if (error instanceof Error)
2216
+ return error;
2217
+ return new Error(stringifyUnknown(error));
2090
2218
  }
2091
- function warnConventionConflicts(kind, scope, agentsNames, claudeNames) {
2092
- const agents = new Set(agentsNames);
2093
- const claude = new Set(claudeNames);
2094
- const conflicts = [];
2095
- for (const name of agents) {
2096
- if (claude.has(name))
2097
- conflicts.push(name);
2219
+
2220
+ // src/cli/error-parse.ts
2221
+ function safeStringifyErrorObject(value) {
2222
+ try {
2223
+ const json = JSON.stringify(value);
2224
+ if (!json || json === "{}") {
2225
+ return "Unknown error";
2226
+ }
2227
+ const maxLen = 240;
2228
+ return json.length > maxLen ? `${json.slice(0, maxLen - 1)}\u2026` : json;
2229
+ } catch {
2230
+ return "Unknown error";
2098
2231
  }
2099
- if (conflicts.length === 0)
2100
- return;
2101
- conflicts.sort((a, b) => a.localeCompare(b));
2102
- const list = conflicts.map((n) => c.cyan(n)).join(c.dim(", "));
2103
- writeln(`${G.warn} conflicting ${kind} in ${scope} .agents and .claude: ${list} ${c.dim("\u2014 using .agents version")}`);
2104
2232
  }
2105
- function validateSkill(candidate) {
2106
- const meta = getCandidateFrontmatter(candidate);
2107
- const name = meta.name?.trim();
2108
- const description = meta.description?.trim();
2109
- if (!name) {
2110
- warnInvalidSkill(candidate.filePath, "frontmatter field `name` is required");
2111
- return null;
2233
+ function toUserErrorMessage(err) {
2234
+ if (err instanceof Error) {
2235
+ const msg = err.message.trim();
2236
+ if (msg)
2237
+ return msg;
2112
2238
  }
2113
- if (!description) {
2114
- warnInvalidSkill(candidate.filePath, "frontmatter field `description` is required");
2115
- return null;
2239
+ if (typeof err === "string") {
2240
+ const msg = err.trim();
2241
+ if (msg)
2242
+ return msg;
2116
2243
  }
2117
- if (name.length > MAX_SKILL_NAME_LENGTH) {
2118
- warnSkillIssue(candidate.filePath, `name exceeds ${MAX_SKILL_NAME_LENGTH} characters`);
2244
+ if (typeof err === "number" || typeof err === "boolean" || typeof err === "bigint") {
2245
+ return String(err);
2119
2246
  }
2120
- if (!SKILL_NAME_RE.test(name)) {
2121
- warnSkillIssue(candidate.filePath, "name does not match lowercase alnum + hyphen format");
2247
+ if (typeof err === "object" && err !== null) {
2248
+ const objectMessage = extractObjectMessage(err);
2249
+ if (objectMessage)
2250
+ return objectMessage;
2251
+ return safeStringifyErrorObject(err);
2122
2252
  }
2123
- return {
2124
- name,
2125
- description,
2126
- source: candidate.source,
2127
- rootPath: candidate.rootPath,
2128
- filePath: candidate.filePath,
2129
- ...meta.context === "fork" && { context: "fork" },
2130
- ...meta.compatibility && { compatibility: meta.compatibility }
2131
- };
2253
+ return "Unknown error";
2132
2254
  }
2133
- function allSkillCandidates(cwd, homeDir) {
2134
- const home = homeDir ?? homedir3();
2135
- const localRootsNearToFar = localSearchRoots(cwd);
2136
- const ordered = [];
2137
- const globalClaude = listSkillCandidates(join3(home, ".claude", "skills"), "global", home);
2138
- const globalAgents = listSkillCandidates(join3(home, ".agents", "skills"), "global", home);
2139
- warnConventionConflicts("skills", "global", globalAgents.map((skill) => candidateConflictName(skill)), globalClaude.map((skill) => candidateConflictName(skill)));
2140
- ordered.push(...globalClaude, ...globalAgents);
2141
- for (const root of [...localRootsNearToFar].reverse()) {
2142
- const localClaude = listSkillCandidates(join3(root, ".claude", "skills"), "local", root);
2143
- const localAgents = listSkillCandidates(join3(root, ".agents", "skills"), "local", root);
2144
- warnConventionConflicts("skills", "local", localAgents.map((skill) => candidateConflictName(skill)), localClaude.map((skill) => candidateConflictName(skill)));
2145
- ordered.push(...localClaude, ...localAgents);
2255
+ function parseAppError(err) {
2256
+ if (typeof err === "string") {
2257
+ return { headline: err };
2146
2258
  }
2147
- return ordered;
2148
- }
2149
- function loadSkillsIndex(cwd, homeDir) {
2150
- const index = new Map;
2151
- for (const candidate of allSkillCandidates(cwd, homeDir)) {
2152
- const skill = validateSkill(candidate);
2153
- if (!skill)
2154
- continue;
2155
- index.set(skill.name, skill);
2259
+ if (err instanceof RetryError) {
2260
+ const inner = parseAppError(err.lastError);
2261
+ return {
2262
+ headline: `Retries exhausted: ${inner.headline}`,
2263
+ ...inner.hint ? { hint: inner.hint } : {}
2264
+ };
2156
2265
  }
2157
- return index;
2158
- }
2159
- var MAX_RESOURCE_LISTING = 50;
2160
- function listSkillResources(skillDir) {
2161
- const resources = [];
2162
- function walk(dir, prefix) {
2163
- let entries;
2164
- try {
2165
- entries = readdirSync(dir);
2166
- } catch {
2167
- return;
2266
+ if (err instanceof APICallError) {
2267
+ const body = String(err.message).toLowerCase();
2268
+ if (body.includes("context_length_exceeded") || body.includes("maximum context length") || body.includes("too many tokens") || body.includes("request too large")) {
2269
+ return {
2270
+ headline: "Max context size reached",
2271
+ hint: "Use /new to start a fresh session"
2272
+ };
2168
2273
  }
2169
- for (const entry of entries) {
2170
- if (resources.length >= MAX_RESOURCE_LISTING)
2171
- return;
2172
- if (entry === "SKILL.md")
2173
- continue;
2174
- const full = join3(dir, entry);
2175
- const rel = prefix ? `${prefix}/${entry}` : entry;
2176
- try {
2177
- if (statSync(full).isDirectory()) {
2178
- walk(full, rel);
2179
- } else {
2180
- resources.push(rel);
2181
- }
2182
- } catch {}
2274
+ if (err.statusCode === 429) {
2275
+ return {
2276
+ headline: "Rate limit hit",
2277
+ hint: "Wait a moment and retry, or switch model with /model"
2278
+ };
2279
+ }
2280
+ if (err.statusCode === 401 || err.statusCode === 403) {
2281
+ return {
2282
+ headline: "Auth failed",
2283
+ hint: "Check the relevant provider API key env var"
2284
+ };
2183
2285
  }
2286
+ return {
2287
+ headline: `API error ${err.statusCode ?? "unknown"}`,
2288
+ ...err.url ? { hint: err.url } : {}
2289
+ };
2184
2290
  }
2185
- walk(skillDir, "");
2186
- return resources;
2187
- }
2188
- function loadSkillContentFromMeta(skill) {
2189
- try {
2190
- const content = readFileSync2(skill.filePath, "utf-8");
2191
- const skillDir = dirname2(skill.filePath);
2291
+ if (err instanceof NoContentGeneratedError) {
2192
2292
  return {
2193
- name: skill.name,
2194
- content,
2195
- source: skill.source,
2196
- skillDir,
2197
- resources: listSkillResources(skillDir)
2293
+ headline: "Model returned empty response",
2294
+ hint: "Try rephrasing or switching model with /model"
2198
2295
  };
2199
- } catch {
2200
- return null;
2201
2296
  }
2202
- }
2203
- function loadSkillContent(name, cwd, homeDir) {
2204
- const skill = loadSkillsIndex(cwd, homeDir).get(name);
2205
- if (!skill)
2206
- return null;
2207
- return loadSkillContentFromMeta(skill);
2297
+ if (err instanceof LoadAPIKeyError) {
2298
+ return {
2299
+ headline: "API key not found",
2300
+ hint: "Set the relevant provider env var"
2301
+ };
2302
+ }
2303
+ if (err instanceof NoSuchModelError) {
2304
+ return {
2305
+ headline: "Model not found",
2306
+ hint: "Use /model to pick a valid model"
2307
+ };
2308
+ }
2309
+ const isObj = typeof err === "object" && err !== null;
2310
+ const code = isObj && "code" in err ? String(err.code) : undefined;
2311
+ const message = toUserErrorMessage(err);
2312
+ if (code === "ECONNREFUSED" || message.includes("ECONNREFUSED")) {
2313
+ return {
2314
+ headline: "Connection failed",
2315
+ hint: "Check network or local server"
2316
+ };
2317
+ }
2318
+ if (code === "ECONNRESET" || message.includes("ECONNRESET") || message.includes("socket connection was closed unexpectedly")) {
2319
+ return {
2320
+ headline: "Connection lost",
2321
+ hint: "The server closed the connection \u2014 retry or switch model with /model"
2322
+ };
2323
+ }
2324
+ const firstLine = message.split(`
2325
+ `)[0]?.trim() || "Unknown error";
2326
+ return { headline: firstLine };
2208
2327
  }
2209
2328
 
2210
2329
  // src/cli/spinner.ts
2211
- import * as c2 from "yoctocolors";
2330
+ import * as c from "yoctocolors";
2212
2331
 
2213
2332
  // src/cli/terminal-io.ts
2214
2333
  class TerminalIO {
@@ -2339,13 +2458,13 @@ class Spinner {
2339
2458
  }
2340
2459
  _tick() {
2341
2460
  const f = SPINNER_FRAMES[this.frame++ % SPINNER_FRAMES.length] ?? "\u28FE";
2342
- const label = this.label ? c2.dim(` ${this.label}`) : "";
2343
- terminal.stderrWrite(`\r${c2.dim(f)}${label}`);
2461
+ const label = this.label ? c.dim(` ${this.label}`) : "";
2462
+ terminal.stderrWrite(`\r${c.dim(f)}${label}`);
2344
2463
  }
2345
2464
  }
2346
2465
 
2347
2466
  // src/cli/status-bar.ts
2348
- import * as c3 from "yoctocolors";
2467
+ import * as c2 from "yoctocolors";
2349
2468
 
2350
2469
  // src/internal/ansi.ts
2351
2470
  var ANSI_REGEX = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
@@ -2366,7 +2485,7 @@ function truncateText(value, max) {
2366
2485
  }
2367
2486
 
2368
2487
  // src/cli/status-bar.ts
2369
- var STATUS_SEP = c3.dim(" \xB7 ");
2488
+ var STATUS_SEP = c2.dim(" \xB7 ");
2370
2489
  function fmtTokens(n) {
2371
2490
  if (n >= 1000)
2372
2491
  return `${(n / 1000).toFixed(1)}k`;
@@ -2376,16 +2495,16 @@ function buildContextSegment(opts) {
2376
2495
  if (opts.contextTokens <= 0)
2377
2496
  return null;
2378
2497
  if (opts.contextWindow === null) {
2379
- return c3.dim(`ctx ${fmtTokens(opts.contextTokens)}`);
2498
+ return c2.dim(`ctx ${fmtTokens(opts.contextTokens)}`);
2380
2499
  }
2381
2500
  const pct = Math.round(opts.contextTokens / opts.contextWindow * 100);
2382
2501
  const pctStr = `${pct}%`;
2383
- let pctColored = c3.dim(pctStr);
2502
+ let pctColored = c2.dim(pctStr);
2384
2503
  if (pct >= 90)
2385
- pctColored = c3.red(pctStr);
2504
+ pctColored = c2.red(pctStr);
2386
2505
  else if (pct >= 75)
2387
- pctColored = c3.yellow(pctStr);
2388
- return c3.dim(`ctx ${fmtTokens(opts.contextTokens)}/${fmtTokens(opts.contextWindow)} `) + pctColored;
2506
+ pctColored = c2.yellow(pctStr);
2507
+ return c2.dim(`ctx ${fmtTokens(opts.contextTokens)}/${fmtTokens(opts.contextWindow)} `) + pctColored;
2389
2508
  }
2390
2509
  function renderStatusLine(segments) {
2391
2510
  return segments.join(STATUS_SEP);
@@ -2406,17 +2525,17 @@ function fitStatusSegments(required, optional, cols) {
2406
2525
  return truncateText(fixedPrefix, cols);
2407
2526
  const maxTailLen = Math.max(8, cols - fixedPrefix.length - sepLen);
2408
2527
  const truncatedTail = truncateText(plainRequired[1] ?? "", maxTailLen);
2409
- return `${required[0]}${STATUS_SEP}${c3.dim(truncatedTail)}`;
2528
+ return `${required[0]}${STATUS_SEP}${c2.dim(truncatedTail)}`;
2410
2529
  }
2411
2530
  function renderStatusBar(opts) {
2412
2531
  const cols = Math.max(20, terminal.stdoutColumns || 80);
2413
- const modelSegment = opts.thinkingEffort ? `${c3.cyan(opts.model)} ${c3.magenta(c3.italic(`\u2726 ${opts.thinkingEffort}`))}` : c3.cyan(opts.model);
2414
- const required = [modelSegment, c3.dim(`#${opts.sessionId}`)];
2532
+ const modelSegment = opts.thinkingEffort ? `${c2.cyan(opts.model)} ${c2.magenta(c2.italic(`\u2726 ${opts.thinkingEffort}`))}` : c2.cyan(opts.model);
2533
+ const required = [modelSegment, c2.dim(`#${opts.sessionId}`)];
2415
2534
  const optional = [];
2416
2535
  if (opts.gitBranch)
2417
- optional.push(c3.dim(`\u2387 ${opts.gitBranch}`));
2536
+ optional.push(c2.dim(`\u2387 ${opts.gitBranch}`));
2418
2537
  if (opts.inputTokens > 0 || opts.outputTokens > 0) {
2419
- optional.push(c3.dim(`tok ${fmtTokens(opts.inputTokens)}/${fmtTokens(opts.outputTokens)}`));
2538
+ optional.push(c2.dim(`tok ${fmtTokens(opts.inputTokens)}/${fmtTokens(opts.outputTokens)}`));
2420
2539
  }
2421
2540
  const contextSegment = buildContextSegment({
2422
2541
  contextTokens: opts.contextTokens,
@@ -2424,14 +2543,14 @@ function renderStatusBar(opts) {
2424
2543
  });
2425
2544
  if (contextSegment)
2426
2545
  optional.push(contextSegment);
2427
- optional.push(c3.dim(opts.cwd));
2546
+ optional.push(c2.dim(opts.cwd));
2428
2547
  const out = fitStatusSegments(required, optional, cols);
2429
2548
  terminal.stdoutWrite(`${out}
2430
2549
  `);
2431
2550
  }
2432
2551
 
2433
2552
  // src/cli/stream-render.ts
2434
- import * as c6 from "yoctocolors";
2553
+ import * as c5 from "yoctocolors";
2435
2554
  import { createHighlighter } from "yoctomarkdown";
2436
2555
 
2437
2556
  // src/llm-api/history/gemini.ts
@@ -2724,17 +2843,17 @@ function normalizeReasoningText(text) {
2724
2843
  }
2725
2844
 
2726
2845
  // src/cli/tool-render.ts
2727
- import * as c5 from "yoctocolors";
2846
+ import * as c4 from "yoctocolors";
2728
2847
 
2729
2848
  // src/cli/tool-result-renderers.ts
2730
- import * as c4 from "yoctocolors";
2849
+ import * as c3 from "yoctocolors";
2731
2850
  function writePreviewLines(opts) {
2732
2851
  if (!opts.value.trim())
2733
2852
  return;
2734
2853
  const lines = opts.value.split(`
2735
2854
  `);
2736
2855
  if (opts.label)
2737
- writeln(`${c4.dim(opts.label)} ${c4.dim(`(${lines.length} lines)`)}`);
2856
+ writeln(`${c3.dim(opts.label)} ${c3.dim(`(${lines.length} lines)`)}`);
2738
2857
  if (!Number.isFinite(opts.maxLines) || lines.length <= opts.maxLines) {
2739
2858
  for (const line of lines)
2740
2859
  writeln(line);
@@ -2746,7 +2865,7 @@ function writePreviewLines(opts) {
2746
2865
  writeln(line);
2747
2866
  const hiddenLines = Math.max(0, lines.length - (headCount + tailCount));
2748
2867
  if (hiddenLines > 0) {
2749
- writeln(c4.dim(`\u2026 +${hiddenLines} lines`));
2868
+ writeln(c3.dim(`\u2026 +${hiddenLines} lines`));
2750
2869
  }
2751
2870
  if (tailCount > 0) {
2752
2871
  for (const line of lines.slice(-tailCount))
@@ -2809,11 +2928,11 @@ function renderShellResult(result, opts) {
2809
2928
  const stdoutLines = countShellLines(r.stdout);
2810
2929
  const stderrLines = countShellLines(r.stderr);
2811
2930
  const stdoutSingleLine = getSingleShellLine(r.stdout);
2812
- let badge = c4.red("error");
2931
+ let badge = c3.red("error");
2813
2932
  if (r.timedOut)
2814
- badge = c4.yellow("timeout");
2933
+ badge = c3.yellow("timeout");
2815
2934
  else if (r.success)
2816
- badge = c4.green("done");
2935
+ badge = c3.green("done");
2817
2936
  const parts = buildShellSummaryParts({
2818
2937
  exitCode: r.exitCode,
2819
2938
  stdoutLines,
@@ -2821,7 +2940,7 @@ function renderShellResult(result, opts) {
2821
2940
  stdoutSingleLine,
2822
2941
  verboseOutput
2823
2942
  });
2824
- writeln(`${badge} ${c4.dim(parts.join(" \xB7 "))}`);
2943
+ writeln(`${badge} ${c3.dim(parts.join(" \xB7 "))}`);
2825
2944
  writePreviewLines({
2826
2945
  label: "stderr",
2827
2946
  value: displayStderr,
@@ -2846,21 +2965,21 @@ function buildSkillDescriptionPart(description, verboseOutput = false) {
2846
2965
  if (!trimmed)
2847
2966
  return "";
2848
2967
  if (verboseOutput)
2849
- return ` ${c4.dim("\xB7")} ${c4.dim(trimmed)}`;
2850
- return ` ${c4.dim("\xB7")} ${c4.dim(trimmed.length > 60 ? `${trimmed.slice(0, 57)}\u2026` : trimmed)}`;
2968
+ return ` ${c3.dim("\xB7")} ${c3.dim(trimmed)}`;
2969
+ return ` ${c3.dim("\xB7")} ${c3.dim(trimmed.length > 60 ? `${trimmed.slice(0, 57)}\u2026` : trimmed)}`;
2851
2970
  }
2852
2971
  function renderSkillSummaryLine(skill, opts) {
2853
2972
  const name = skill.name ?? "(unknown)";
2854
2973
  const source = skill.source ?? "unknown";
2855
- const labelPrefix = opts?.label ? `${c4.dim(opts.label)} ` : "";
2856
- writeln(`${G.info} ${labelPrefix}${name} ${c4.dim("\xB7")} ${c4.dim(source)}${buildSkillDescriptionPart(skill.description, opts?.verboseOutput === true)}`);
2974
+ const labelPrefix = opts?.label ? `${c3.dim(opts.label)} ` : "";
2975
+ writeln(`${G.info} ${labelPrefix}${name} ${c3.dim("\xB7")} ${c3.dim(source)}${buildSkillDescriptionPart(skill.description, opts?.verboseOutput === true)}`);
2857
2976
  }
2858
2977
  function renderListSkillsResult(result, opts) {
2859
2978
  const r = result;
2860
2979
  if (!Array.isArray(r?.skills))
2861
2980
  return false;
2862
2981
  if (r.skills.length === 0) {
2863
- writeln(`${G.info} ${c4.dim("no skills")}`);
2982
+ writeln(`${G.info} ${c3.dim("no skills")}`);
2864
2983
  return true;
2865
2984
  }
2866
2985
  const maxSkills = opts?.verboseOutput ? r.skills.length : 6;
@@ -2870,7 +2989,7 @@ function renderListSkillsResult(result, opts) {
2870
2989
  });
2871
2990
  }
2872
2991
  if (r.skills.length > maxSkills) {
2873
- writeln(`${c4.dim(`+${r.skills.length - maxSkills} more skills`)}`);
2992
+ writeln(`${c3.dim(`+${r.skills.length - maxSkills} more skills`)}`);
2874
2993
  }
2875
2994
  return true;
2876
2995
  }
@@ -2880,10 +2999,10 @@ function renderReadSkillResult(result, _opts) {
2880
2999
  return false;
2881
3000
  if (!r.skill) {
2882
3001
  if (typeof r.note === "string" && r.note.trim().length > 0) {
2883
- writeln(`${G.info} ${c4.dim("skill")} ${c4.dim(r.note.trim())}`);
3002
+ writeln(`${G.info} ${c3.dim("skill")} ${c3.dim(r.note.trim())}`);
2884
3003
  return true;
2885
3004
  }
2886
- writeln(`${G.info} ${c4.dim("skill")} ${c4.dim("(not found)")}`);
3005
+ writeln(`${G.info} ${c3.dim("skill")} ${c3.dim("(not found)")}`);
2887
3006
  return true;
2888
3007
  }
2889
3008
  renderSkillSummaryLine(r.skill, {
@@ -2891,7 +3010,7 @@ function renderReadSkillResult(result, _opts) {
2891
3010
  verboseOutput: _opts?.verboseOutput === true
2892
3011
  });
2893
3012
  if (typeof r.note === "string" && r.note.trim().length > 0) {
2894
- writeln(c4.dim(r.note.trim()));
3013
+ writeln(c3.dim(r.note.trim()));
2895
3014
  }
2896
3015
  return true;
2897
3016
  }
@@ -2900,19 +3019,19 @@ function renderWebSearchResult(result, opts) {
2900
3019
  if (!Array.isArray(r?.results))
2901
3020
  return false;
2902
3021
  if (r.results.length === 0) {
2903
- writeln(`${G.info} ${c4.dim("no results")}`);
3022
+ writeln(`${G.info} ${c3.dim("no results")}`);
2904
3023
  return true;
2905
3024
  }
2906
3025
  const maxResults = opts?.verboseOutput ? r.results.length : 5;
2907
3026
  for (const item of r.results.slice(0, maxResults)) {
2908
3027
  const title = (item.title?.trim() || item.url || "(untitled)").replace(/\s+/g, " ");
2909
- const score = typeof item.score === "number" ? c4.dim(` (${item.score.toFixed(2)})`) : "";
2910
- writeln(`${c4.dim("\u2022")} ${title}${score}`);
3028
+ const score = typeof item.score === "number" ? c3.dim(` (${item.score.toFixed(2)})`) : "";
3029
+ writeln(`${c3.dim("\u2022")} ${title}${score}`);
2911
3030
  if (item.url)
2912
- writeln(` ${c4.dim(item.url)}`);
3031
+ writeln(` ${c3.dim(item.url)}`);
2913
3032
  }
2914
3033
  if (r.results.length > maxResults) {
2915
- writeln(`${c4.dim(`+${r.results.length - maxResults} more`)}`);
3034
+ writeln(`${c3.dim(`+${r.results.length - maxResults} more`)}`);
2916
3035
  }
2917
3036
  return true;
2918
3037
  }
@@ -2921,23 +3040,23 @@ function renderWebContentResult(result, opts) {
2921
3040
  if (!Array.isArray(r?.results))
2922
3041
  return false;
2923
3042
  if (r.results.length === 0) {
2924
- writeln(`${G.info} ${c4.dim("no pages")}`);
3043
+ writeln(`${G.info} ${c3.dim("no pages")}`);
2925
3044
  return true;
2926
3045
  }
2927
3046
  const maxPages = opts?.verboseOutput ? r.results.length : 3;
2928
3047
  for (const item of r.results.slice(0, maxPages)) {
2929
3048
  const title = (item.title?.trim() || item.url || "(untitled)").replace(/\s+/g, " ");
2930
- writeln(`${c4.dim("\u2022")} ${title}`);
3049
+ writeln(`${c3.dim("\u2022")} ${title}`);
2931
3050
  if (item.url)
2932
- writeln(` ${c4.dim(item.url)}`);
3051
+ writeln(` ${c3.dim(item.url)}`);
2933
3052
  const preview = (item.text ?? "").replace(/\s+/g, " ").trim();
2934
3053
  if (preview) {
2935
3054
  const trimmed = opts?.verboseOutput || preview.length <= 220 ? preview : `${preview.slice(0, 217)}\u2026`;
2936
- writeln(` ${c4.dim(trimmed)}`);
3055
+ writeln(` ${c3.dim(trimmed)}`);
2937
3056
  }
2938
3057
  }
2939
3058
  if (r.results.length > maxPages) {
2940
- writeln(`${c4.dim(`+${r.results.length - maxPages} more`)}`);
3059
+ writeln(`${c3.dim(`+${r.results.length - maxPages} more`)}`);
2941
3060
  }
2942
3061
  return true;
2943
3062
  }
@@ -3039,7 +3158,7 @@ function formatShellCallLine(cmd) {
3039
3158
  const { cwd, rest } = parseShellCdPrefix(cmd);
3040
3159
  const firstCmd = extractFirstCommand(rest);
3041
3160
  const glyph = shellCmdGlyph(firstCmd, rest);
3042
- const cwdSuffix = cwd ? ` ${c5.dim(`in ${cwd}`)}` : "";
3161
+ const cwdSuffix = cwd ? ` ${c4.dim(`in ${cwd}`)}` : "";
3043
3162
  return `${glyph} ${rest}${cwdSuffix}`;
3044
3163
  }
3045
3164
  function buildToolCallLine(name, args) {
@@ -3047,29 +3166,29 @@ function buildToolCallLine(name, args) {
3047
3166
  if (name === "shell") {
3048
3167
  const cmd = String(a.command ?? "").trim();
3049
3168
  if (!cmd)
3050
- return `${G.run} ${c5.dim("shell")}`;
3169
+ return `${G.run} ${c4.dim("shell")}`;
3051
3170
  return formatShellCallLine(cmd);
3052
3171
  }
3053
3172
  if (name === "listSkills") {
3054
- return `${G.search} ${c5.dim("list skills")}`;
3173
+ return `${G.search} ${c4.dim("list skills")}`;
3055
3174
  }
3056
3175
  if (name === "readSkill") {
3057
3176
  const skillName = typeof a.name === "string" ? a.name : "";
3058
- return `${G.read} ${c5.dim("read skill")}${skillName ? ` ${skillName}` : ""}`;
3177
+ return `${G.read} ${c4.dim("read skill")}${skillName ? ` ${skillName}` : ""}`;
3059
3178
  }
3060
3179
  if (name === "webSearch") {
3061
3180
  const query = typeof a.query === "string" ? a.query : "";
3062
- return `${G.search} ${c5.dim("search")}${query ? ` ${query}` : ""}`;
3181
+ return `${G.search} ${c4.dim("search")}${query ? ` ${query}` : ""}`;
3063
3182
  }
3064
3183
  if (name === "webContent") {
3065
3184
  const urls = Array.isArray(a.urls) ? a.urls : [];
3066
3185
  const label = urls.length === 1 ? String(urls[0]) : `${urls.length} url${urls.length !== 1 ? "s" : ""}`;
3067
- return `${G.read} ${c5.dim("fetch")} ${label}`;
3186
+ return `${G.read} ${c4.dim("fetch")} ${label}`;
3068
3187
  }
3069
3188
  if (name.startsWith("mcp_")) {
3070
- return `${G.mcp} ${c5.dim(name)}`;
3189
+ return `${G.mcp} ${c4.dim(name)}`;
3071
3190
  }
3072
- return `${toolGlyph(name)} ${c5.dim(name)}`;
3191
+ return `${toolGlyph(name)} ${c4.dim(name)}`;
3073
3192
  }
3074
3193
  function renderToolCall(toolName, args, opts) {
3075
3194
  const line = buildToolCallLine(toolName, args);
@@ -3088,7 +3207,7 @@ function renderToolCall(toolName, args, opts) {
3088
3207
  const hidden = lines.length - head.length - tail.length;
3089
3208
  for (const l of head)
3090
3209
  writeln(l);
3091
- writeln(c5.dim(`\u2026 +${hidden} lines`));
3210
+ writeln(c4.dim(`\u2026 +${hidden} lines`));
3092
3211
  for (const l of tail)
3093
3212
  writeln(l);
3094
3213
  }
@@ -3102,7 +3221,7 @@ function formatErrorBadge(result) {
3102
3221
  msg = JSON.stringify(result);
3103
3222
  const oneLiner = msg.split(`
3104
3223
  `)[0] ?? msg;
3105
- return `${G.err} ${c5.red(oneLiner)}`;
3224
+ return `${G.err} ${c4.red(oneLiner)}`;
3106
3225
  }
3107
3226
  function renderToolResult(toolName, result, isError, opts) {
3108
3227
  if (isError) {
@@ -3114,21 +3233,21 @@ function renderToolResult(toolName, result, isError, opts) {
3114
3233
  }
3115
3234
  const text = JSON.stringify(result);
3116
3235
  if (opts?.verboseOutput || text.length <= 120) {
3117
- writeln(c5.dim(text));
3236
+ writeln(c4.dim(text));
3118
3237
  return;
3119
3238
  }
3120
- writeln(c5.dim(`${text.slice(0, 117)}\u2026`));
3239
+ writeln(c4.dim(`${text.slice(0, 117)}\u2026`));
3121
3240
  }
3122
3241
 
3123
3242
  // src/cli/stream-render.ts
3124
3243
  function styleReasoning(text) {
3125
- return c6.italic(c6.dim(text));
3244
+ return c5.italic(c5.dim(text));
3126
3245
  }
3127
3246
  function writeReasoningDelta(delta, state) {
3128
3247
  if (!delta)
3129
3248
  return;
3130
3249
  if (!state.blockOpen) {
3131
- writeln(`${G.info} ${c6.dim("reasoning")}`);
3250
+ writeln(`${G.info} ${c5.dim("reasoning")}`);
3132
3251
  state.blockOpen = true;
3133
3252
  }
3134
3253
  const lines = delta.split(`
@@ -3311,7 +3430,7 @@ ${appended}`;
3311
3430
  if (!quiet) {
3312
3431
  spinner.stop();
3313
3432
  if (parallelCallCount > 1 && callInfo) {
3314
- writeln(`${c6.dim("\u21B3")} ${callInfo.label}`);
3433
+ writeln(`${c5.dim("\u21B3")} ${callInfo.label}`);
3315
3434
  }
3316
3435
  if (toolCallInfo.size === 0)
3317
3436
  parallelCallCount = 0;
@@ -3332,7 +3451,7 @@ ${appended}`;
3332
3451
  if (!quiet) {
3333
3452
  spinner.stop();
3334
3453
  const removedKb = (event.removedBytes / 1024).toFixed(1);
3335
- writeln(`${G.info} ${c6.dim("context pruned")} ${c6.dim(`\u2013${event.removedMessageCount} messages`)} ${c6.dim(`\u2013${removedKb} KB`)}`);
3454
+ writeln(`${G.info} ${c5.dim("context pruned")} ${c5.dim(`\u2013${event.removedMessageCount} messages`)} ${c5.dim(`\u2013${removedKb} KB`)}`);
3336
3455
  renderedVisibleOutput = true;
3337
3456
  spinner.start("thinking");
3338
3457
  }
@@ -3377,7 +3496,7 @@ ${appended}`;
3377
3496
  }
3378
3497
 
3379
3498
  // src/cli/output.ts
3380
- var HOME2 = homedir4();
3499
+ var HOME2 = homedir3();
3381
3500
  function tildePath(p) {
3382
3501
  return p.startsWith(HOME2) ? `~${p.slice(HOME2.length)}` : p;
3383
3502
  }
@@ -3407,17 +3526,17 @@ function renderUserMessage(text) {
3407
3526
  }
3408
3527
  }
3409
3528
  var G = {
3410
- prompt: c7.green("\u203A"),
3411
- reply: c7.cyan("\u25C6"),
3412
- search: c7.yellow("?"),
3413
- read: c7.dim("\u2190"),
3414
- write: c7.green("\u270E"),
3415
- run: c7.dim("$"),
3416
- mcp: c7.yellow("\u2699"),
3417
- ok: c7.green("\u2714"),
3418
- err: c7.red("\u2716"),
3419
- warn: c7.yellow("!"),
3420
- info: c7.dim("\xB7")
3529
+ prompt: c6.green("\u203A"),
3530
+ reply: c6.cyan("\u25C6"),
3531
+ search: c6.yellow("?"),
3532
+ read: c6.dim("\u2190"),
3533
+ write: c6.green("\u270E"),
3534
+ run: c6.dim("$"),
3535
+ mcp: c6.yellow("\u2699"),
3536
+ ok: c6.green("\u2714"),
3537
+ err: c6.red("\u2716"),
3538
+ warn: c6.yellow("!"),
3539
+ info: c6.dim("\xB7")
3421
3540
  };
3422
3541
  var PREFIX = {
3423
3542
  user: G.prompt,
@@ -3439,17 +3558,17 @@ class RenderedError extends Error {
3439
3558
  function renderError(err, context = "render") {
3440
3559
  logError(err, context);
3441
3560
  const parsed = parseAppError(err);
3442
- writeln(`${G.err} ${c7.red(parsed.headline)}`);
3561
+ writeln(`${G.err} ${c6.red(parsed.headline)}`);
3443
3562
  if (parsed.hint) {
3444
- writeln(` ${c7.dim(parsed.hint)}`);
3563
+ writeln(` ${c6.dim(parsed.hint)}`);
3445
3564
  }
3446
3565
  }
3447
- function renderBanner(model, cwd) {
3566
+ async function renderBanner(model, cwd, connectedProviders) {
3448
3567
  writeln();
3449
3568
  const title = PACKAGE_VERSION ? `mini-coder \xB7 v${PACKAGE_VERSION}` : "mini-coder";
3450
- writeln(` ${c7.cyan("mc")} ${c7.dim(title)}`);
3451
- writeln(` ${c7.dim(model)} ${c7.dim("\xB7")} ${c7.dim(tildePath(cwd))}`);
3452
- writeln(` ${c7.dim("/help for commands \xB7 esc cancel \xB7 ctrl+d exit")}`);
3569
+ writeln(` ${c6.cyan("mc")} ${c6.dim(title)}`);
3570
+ writeln(` ${c6.dim(model)} ${c6.dim("\xB7")} ${c6.dim(tildePath(cwd))}`);
3571
+ writeln(` ${c6.dim("/help for commands \xB7 esc cancel \xB7 ctrl+d exit")}`);
3453
3572
  const items = [];
3454
3573
  if (getPreferredShowReasoning())
3455
3574
  items.push("reasoning: on");
@@ -3462,17 +3581,22 @@ function renderBanner(model, cwd) {
3462
3581
  if (skills.size > 0)
3463
3582
  items.push(`${skills.size} skill${skills.size > 1 ? "s" : ""}`);
3464
3583
  if (items.length > 0) {
3465
- writeln(` ${c7.dim(items.join(" \xB7 "))}`);
3584
+ writeln(` ${c6.dim(items.join(" \xB7 "))}`);
3466
3585
  }
3467
3586
  const connParts = [];
3468
- for (const p of discoverConnectedProviders()) {
3469
- connParts.push(p.via === "oauth" ? `${p.name} (oauth)` : p.name);
3587
+ for (const p of connectedProviders ?? await discoverConnectedProviders()) {
3588
+ if (p.via === "oauth")
3589
+ connParts.push(`${p.name} (oauth)`);
3590
+ else if (p.via === "local")
3591
+ connParts.push(`${p.name} (local)`);
3592
+ else
3593
+ connParts.push(p.name);
3470
3594
  }
3471
3595
  const mcpCount = listMcpServers().length;
3472
3596
  if (mcpCount > 0)
3473
3597
  connParts.push(`${mcpCount} mcp`);
3474
3598
  if (connParts.length > 0) {
3475
- writeln(` ${c7.dim(connParts.join(" \xB7 "))}`);
3599
+ writeln(` ${c6.dim(connParts.join(" \xB7 "))}`);
3476
3600
  }
3477
3601
  writeln();
3478
3602
  }
@@ -3487,7 +3611,7 @@ class CliReporter {
3487
3611
  if (this.quiet)
3488
3612
  return;
3489
3613
  this.spinner.stop();
3490
- writeln(`${G.info} ${c7.dim(msg)}`);
3614
+ writeln(`${G.info} ${c6.dim(msg)}`);
3491
3615
  }
3492
3616
  error(msg, hint) {
3493
3617
  this.spinner.stop();
@@ -3526,251 +3650,260 @@ class CliReporter {
3526
3650
  renderStatusBar(data) {
3527
3651
  if (this.quiet)
3528
3652
  return;
3529
- renderStatusBar(data);
3530
- }
3531
- restoreTerminal() {
3532
- restoreTerminal();
3533
- }
3534
- }
3535
-
3536
- // src/session/db/message-repo.ts
3537
- var _insertMsgStmt = null;
3538
- var _addPromptHistoryStmt = null;
3539
- var _getPromptHistoryStmt = null;
3540
- function getInsertMsgStmt() {
3541
- if (!_insertMsgStmt) {
3542
- _insertMsgStmt = getDb().prepare(`INSERT INTO messages (session_id, payload, turn_index, created_at)
3543
- VALUES (?, ?, ?, ?)`);
3653
+ renderStatusBar(data);
3544
3654
  }
3545
- return _insertMsgStmt;
3546
- }
3547
- function getAddPromptHistoryStmt() {
3548
- if (!_addPromptHistoryStmt) {
3549
- _addPromptHistoryStmt = getDb().prepare("INSERT INTO prompt_history (text, created_at) VALUES (?, ?)");
3655
+ restoreTerminal() {
3656
+ restoreTerminal();
3550
3657
  }
3551
- return _addPromptHistoryStmt;
3552
3658
  }
3553
- function getPromptHistoryStmt() {
3554
- if (!_getPromptHistoryStmt) {
3555
- _getPromptHistoryStmt = getDb().prepare("SELECT text FROM prompt_history ORDER BY id DESC LIMIT ?");
3659
+
3660
+ // src/cli/skills.ts
3661
+ var MAX_FRONTMATTER_BYTES = 64 * 1024;
3662
+ var SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
3663
+ var MAX_SKILL_NAME_LENGTH = 64;
3664
+ var warnedInvalidSkills = new Set;
3665
+ var warnedSkillIssues = new Set;
3666
+ function parseSkillFrontmatter(filePath) {
3667
+ let fd = null;
3668
+ try {
3669
+ fd = openSync(filePath, "r");
3670
+ const chunk = Buffer.allocUnsafe(MAX_FRONTMATTER_BYTES);
3671
+ const bytesRead = readSync(fd, chunk, 0, MAX_FRONTMATTER_BYTES, 0);
3672
+ const text = chunk.toString("utf8", 0, bytesRead);
3673
+ const { meta } = parseFrontmatter(text);
3674
+ const result = {};
3675
+ if (typeof meta.name === "string" && meta.name)
3676
+ result.name = meta.name;
3677
+ if (typeof meta.description === "string" && meta.description)
3678
+ result.description = meta.description;
3679
+ if (typeof meta.context === "string" && meta.context)
3680
+ result.context = meta.context;
3681
+ if (typeof meta.compatibility === "string" && meta.compatibility)
3682
+ result.compatibility = meta.compatibility;
3683
+ return result;
3684
+ } catch {
3685
+ return {};
3686
+ } finally {
3687
+ if (fd !== null)
3688
+ closeSync(fd);
3556
3689
  }
3557
- return _getPromptHistoryStmt;
3558
- }
3559
- function saveMessages(sessionId, msgs, turnIndex = 0) {
3560
- const db = getDb();
3561
- const stmt = getInsertMsgStmt();
3562
- const now = Date.now();
3563
- db.transaction(() => {
3564
- for (const msg of msgs) {
3565
- stmt.run(sessionId, JSON.stringify(msg), turnIndex, now);
3566
- }
3567
- })();
3568
- }
3569
- function getMaxTurnIndex(sessionId) {
3570
- const row = getDb().query("SELECT MAX(turn_index) AS max_turn FROM messages WHERE session_id = ?").get(sessionId);
3571
- return row?.max_turn ?? -1;
3572
- }
3573
- function deleteLastTurn(sessionId, turnIndex) {
3574
- const target = turnIndex !== undefined ? turnIndex : getMaxTurnIndex(sessionId);
3575
- if (target < 0)
3576
- return false;
3577
- getDb().run("DELETE FROM messages WHERE session_id = ? AND turn_index = ?", [
3578
- sessionId,
3579
- target
3580
- ]);
3581
- return true;
3582
3690
  }
3583
- function loadMessages(sessionId) {
3584
- const rows = getDb().query("SELECT payload, id FROM messages WHERE session_id = ? ORDER BY id ASC").all(sessionId);
3585
- const messages = [];
3586
- for (const row of rows) {
3587
- try {
3588
- messages.push(JSON.parse(row.payload));
3589
- } catch (_err) {
3590
- renderError(new Error(`Failed to parse message ID ${row.id} for session ${sessionId}`));
3591
- }
3691
+ function getCandidateFrontmatter(candidate) {
3692
+ if (!candidate.frontmatter) {
3693
+ candidate.frontmatter = parseSkillFrontmatter(candidate.filePath);
3592
3694
  }
3593
- return messages;
3594
- }
3595
- function addPromptHistory(text) {
3596
- const trimmed = text.trim();
3597
- if (!trimmed)
3598
- return;
3599
- getAddPromptHistoryStmt().run(trimmed, Date.now());
3695
+ return candidate.frontmatter;
3600
3696
  }
3601
- function getPromptHistory(limit = 200) {
3602
- const rows = getPromptHistoryStmt().all(limit);
3603
- return rows.map((r) => r.text).reverse();
3697
+ function candidateConflictName(candidate) {
3698
+ return getCandidateFrontmatter(candidate).name?.trim() || candidate.folderName;
3604
3699
  }
3605
- // src/session/db/session-repo.ts
3606
- function createSession(opts) {
3607
- const db = getDb();
3608
- const now = Date.now();
3609
- db.run(`INSERT INTO sessions (id, title, cwd, model, created_at, updated_at)
3610
- VALUES (?, ?, ?, ?, ?, ?)`, [opts.id, opts.title ?? "", opts.cwd, opts.model, now, now]);
3611
- const session = getSession(opts.id);
3612
- if (!session) {
3613
- throw new Error(`Failed to create session ${opts.id}`);
3700
+ function findGitBoundary(cwd) {
3701
+ let current = resolve2(cwd);
3702
+ while (true) {
3703
+ if (existsSync3(join3(current, ".git")))
3704
+ return current;
3705
+ const parent = dirname2(current);
3706
+ if (parent === current)
3707
+ return null;
3708
+ current = parent;
3614
3709
  }
3615
- return session;
3616
- }
3617
- function getSession(id) {
3618
- return getDb().query("SELECT * FROM sessions WHERE id = ?").get(id) ?? null;
3619
- }
3620
- function touchSession(id, model) {
3621
- getDb().run("UPDATE sessions SET updated_at = ?, model = ? WHERE id = ?", [
3622
- Date.now(),
3623
- model,
3624
- id
3625
- ]);
3626
- }
3627
- function setSessionTitle(id, title) {
3628
- getDb().run("UPDATE sessions SET title = ? WHERE id = ? AND title = ''", [
3629
- title,
3630
- id
3631
- ]);
3632
- }
3633
- function listSessions(limit = 20) {
3634
- return getDb().query("SELECT * FROM sessions ORDER BY updated_at DESC LIMIT ?").all(limit);
3635
- }
3636
- function generateSessionId() {
3637
- const ts = Date.now().toString(36);
3638
- const rand = Math.random().toString(36).slice(2, 7);
3639
- return `${ts}-${rand}`;
3640
- }
3641
- // src/session/db/settings-repo.ts
3642
- function getSetting(key) {
3643
- const row = getDb().query("SELECT value FROM settings WHERE key = ?").get(key);
3644
- return row?.value ?? null;
3645
- }
3646
- function setSetting(key, value) {
3647
- getDb().run(`INSERT INTO settings (key, value) VALUES (?, ?)
3648
- ON CONFLICT(key) DO UPDATE SET value = excluded.value`, [key, value]);
3649
3710
  }
3650
- function parseBooleanSetting(value, fallback) {
3651
- if (value === null)
3652
- return fallback;
3653
- const normalized = value.trim().toLowerCase();
3654
- if (normalized === "true" || normalized === "on" || normalized === "1") {
3655
- return true;
3656
- }
3657
- if (normalized === "false" || normalized === "off" || normalized === "0") {
3658
- return false;
3711
+ function localSearchRoots(cwd) {
3712
+ const start = resolve2(cwd);
3713
+ const stop = findGitBoundary(start);
3714
+ if (!stop)
3715
+ return [start];
3716
+ const roots = [];
3717
+ let current = start;
3718
+ while (true) {
3719
+ roots.push(current);
3720
+ if (current === stop)
3721
+ break;
3722
+ const parent = dirname2(current);
3723
+ if (parent === current)
3724
+ break;
3725
+ current = parent;
3659
3726
  }
3660
- return fallback;
3661
- }
3662
- function getPreferredModel() {
3663
- return getSetting("preferred_model");
3664
- }
3665
- function setPreferredModel(model) {
3666
- setSetting("preferred_model", model);
3667
- }
3668
- function getPreferredThinkingEffort() {
3669
- const v = getSetting("preferred_thinking_effort");
3670
- if (v === "low" || v === "medium" || v === "high" || v === "xhigh")
3671
- return v;
3672
- return null;
3727
+ return roots;
3673
3728
  }
3674
- function setPreferredThinkingEffort(effort) {
3675
- if (effort === null) {
3676
- getDb().run("DELETE FROM settings WHERE key = 'preferred_thinking_effort'");
3677
- } else {
3678
- setSetting("preferred_thinking_effort", effort);
3729
+ var MAX_SKILL_SCAN_DEPTH = 5;
3730
+ var MAX_SKILL_DIRS_SCANNED = 2000;
3731
+ function listSkillCandidates(skillsDir, source, rootPath) {
3732
+ if (!existsSync3(skillsDir))
3733
+ return [];
3734
+ const candidates = [];
3735
+ let dirsScanned = 0;
3736
+ function walk(dir, depth) {
3737
+ if (depth > MAX_SKILL_SCAN_DEPTH || dirsScanned >= MAX_SKILL_DIRS_SCANNED)
3738
+ return;
3739
+ let entries;
3740
+ try {
3741
+ entries = readdirSync(dir).sort((a, b) => a.localeCompare(b));
3742
+ } catch {
3743
+ return;
3744
+ }
3745
+ for (const entry of entries) {
3746
+ if (dirsScanned >= MAX_SKILL_DIRS_SCANNED)
3747
+ return;
3748
+ const entryPath = join3(dir, entry);
3749
+ try {
3750
+ if (!statSync(entryPath).isDirectory())
3751
+ continue;
3752
+ } catch {
3753
+ continue;
3754
+ }
3755
+ dirsScanned++;
3756
+ const filePath = join3(entryPath, "SKILL.md");
3757
+ if (existsSync3(filePath)) {
3758
+ candidates.push({
3759
+ folderName: entry,
3760
+ filePath,
3761
+ rootPath,
3762
+ source
3763
+ });
3764
+ } else {
3765
+ walk(entryPath, depth + 1);
3766
+ }
3767
+ }
3679
3768
  }
3769
+ walk(skillsDir, 1);
3770
+ return candidates;
3680
3771
  }
3681
- function getPreferredShowReasoning() {
3682
- return parseBooleanSetting(getSetting("preferred_show_reasoning"), false);
3683
- }
3684
- function setPreferredShowReasoning(show) {
3685
- setSetting("preferred_show_reasoning", show ? "true" : "false");
3686
- }
3687
- function getPreferredVerboseOutput() {
3688
- return parseBooleanSetting(getSetting("preferred_verbose_output"), false);
3689
- }
3690
- function setPreferredVerboseOutput(verbose) {
3691
- setSetting("preferred_verbose_output", verbose ? "true" : "false");
3692
- }
3693
- // src/agent/session-runner.ts
3694
- import * as c10 from "yoctocolors";
3695
-
3696
- // src/cli/input.ts
3697
- import * as c8 from "yoctocolors";
3698
-
3699
- // src/cli/input-buffer.ts
3700
- var PASTE_TOKEN_START = 57344;
3701
- var PASTE_TOKEN_END = 63743;
3702
- function createPasteToken(buf, pasteTokens) {
3703
- for (let code = PASTE_TOKEN_START;code <= PASTE_TOKEN_END; code++) {
3704
- const token = String.fromCharCode(code);
3705
- if (!buf.includes(token) && !pasteTokens.has(token))
3706
- return token;
3707
- }
3708
- throw new Error("Too many pasted chunks in a single prompt");
3772
+ function warnInvalidSkill(filePath, reason) {
3773
+ const key = `${filePath}:${reason}`;
3774
+ if (warnedInvalidSkills.has(key))
3775
+ return;
3776
+ warnedInvalidSkills.add(key);
3777
+ writeln(`${G.warn} skipping invalid skill ${filePath}: ${reason}`);
3709
3778
  }
3710
- function pasteLabel(text) {
3711
- const lines = text.split(`
3712
- `);
3713
- const first = lines[0] ?? "";
3714
- const preview = first.length > 40 ? `${first.slice(0, 40)}\u2026` : first;
3715
- const extra = lines.length - 1;
3716
- const more = extra > 0 ? ` +${extra} more line${extra === 1 ? "" : "s"}` : "";
3717
- return `[pasted: "${preview}"${more}]`;
3779
+ function warnSkillIssue(filePath, reason) {
3780
+ const key = `${filePath}:${reason}`;
3781
+ if (warnedSkillIssues.has(key))
3782
+ return;
3783
+ warnedSkillIssues.add(key);
3784
+ writeln(`${G.warn} skill ${filePath}: ${reason}`);
3718
3785
  }
3719
- function processInputBuffer(buf, pasteTokens, replacer) {
3720
- let out = "";
3721
- for (let i = 0;i < buf.length; i++) {
3722
- const ch = buf[i] ?? "";
3723
- out += replacer(ch, pasteTokens.get(ch));
3786
+ function warnConventionConflicts(kind, scope, agentsNames, claudeNames) {
3787
+ const agents = new Set(agentsNames);
3788
+ const claude = new Set(claudeNames);
3789
+ const conflicts = [];
3790
+ for (const name of agents) {
3791
+ if (claude.has(name))
3792
+ conflicts.push(name);
3724
3793
  }
3725
- return out;
3794
+ if (conflicts.length === 0)
3795
+ return;
3796
+ conflicts.sort((a, b) => a.localeCompare(b));
3797
+ const list = conflicts.map((n) => c7.cyan(n)).join(c7.dim(", "));
3798
+ writeln(`${G.warn} conflicting ${kind} in ${scope} .agents and .claude: ${list} ${c7.dim("\u2014 using .agents version")}`);
3726
3799
  }
3727
- function renderInputBuffer(buf, pasteTokens) {
3728
- return processInputBuffer(buf, pasteTokens, (ch, pasted) => pasted ? pasteLabel(pasted) : ch);
3800
+ function validateSkill(candidate) {
3801
+ const meta = getCandidateFrontmatter(candidate);
3802
+ const name = meta.name?.trim();
3803
+ const description = meta.description?.trim();
3804
+ if (!name) {
3805
+ warnInvalidSkill(candidate.filePath, "frontmatter field `name` is required");
3806
+ return null;
3807
+ }
3808
+ if (!description) {
3809
+ warnInvalidSkill(candidate.filePath, "frontmatter field `description` is required");
3810
+ return null;
3811
+ }
3812
+ if (name.length > MAX_SKILL_NAME_LENGTH) {
3813
+ warnSkillIssue(candidate.filePath, `name exceeds ${MAX_SKILL_NAME_LENGTH} characters`);
3814
+ }
3815
+ if (!SKILL_NAME_RE.test(name)) {
3816
+ warnSkillIssue(candidate.filePath, "name does not match lowercase alnum + hyphen format");
3817
+ }
3818
+ return {
3819
+ name,
3820
+ description,
3821
+ source: candidate.source,
3822
+ rootPath: candidate.rootPath,
3823
+ filePath: candidate.filePath,
3824
+ ...meta.context === "fork" && { context: "fork" },
3825
+ ...meta.compatibility && { compatibility: meta.compatibility }
3826
+ };
3729
3827
  }
3730
- function expandInputBuffer(buf, pasteTokens) {
3731
- return processInputBuffer(buf, pasteTokens, (ch, pasted) => pasted ?? ch);
3828
+ function allSkillCandidates(cwd, homeDir) {
3829
+ const home = homeDir ?? homedir4();
3830
+ const localRootsNearToFar = localSearchRoots(cwd);
3831
+ const ordered = [];
3832
+ const globalClaude = listSkillCandidates(join3(home, ".claude", "skills"), "global", home);
3833
+ const globalAgents = listSkillCandidates(join3(home, ".agents", "skills"), "global", home);
3834
+ warnConventionConflicts("skills", "global", globalAgents.map((skill) => candidateConflictName(skill)), globalClaude.map((skill) => candidateConflictName(skill)));
3835
+ ordered.push(...globalClaude, ...globalAgents);
3836
+ for (const root of [...localRootsNearToFar].reverse()) {
3837
+ const localClaude = listSkillCandidates(join3(root, ".claude", "skills"), "local", root);
3838
+ const localAgents = listSkillCandidates(join3(root, ".agents", "skills"), "local", root);
3839
+ warnConventionConflicts("skills", "local", localAgents.map((skill) => candidateConflictName(skill)), localClaude.map((skill) => candidateConflictName(skill)));
3840
+ ordered.push(...localClaude, ...localAgents);
3841
+ }
3842
+ return ordered;
3732
3843
  }
3733
- function pruneInputPasteTokens(pasteTokens, ...buffers) {
3734
- const referenced = buffers.join("");
3735
- const next = new Map;
3736
- for (const [token, text] of pasteTokens) {
3737
- if (referenced.includes(token))
3738
- next.set(token, text);
3844
+ function loadSkillsIndex(cwd, homeDir) {
3845
+ const index = new Map;
3846
+ for (const candidate of allSkillCandidates(cwd, homeDir)) {
3847
+ const skill = validateSkill(candidate);
3848
+ if (!skill)
3849
+ continue;
3850
+ index.set(skill.name, skill);
3739
3851
  }
3740
- return next;
3852
+ return index;
3741
3853
  }
3742
- function getVisualCursor(buf, cursor, pasteTokens) {
3743
- let visual = 0;
3744
- for (let i = 0;i < Math.min(cursor, buf.length); i++) {
3745
- const ch = buf[i] ?? "";
3746
- const pasted = pasteTokens.get(ch);
3747
- visual += pasted ? pasteLabel(pasted).length : 1;
3854
+ var MAX_RESOURCE_LISTING = 50;
3855
+ function listSkillResources(skillDir) {
3856
+ const resources = [];
3857
+ function walk(dir, prefix) {
3858
+ let entries;
3859
+ try {
3860
+ entries = readdirSync(dir);
3861
+ } catch {
3862
+ return;
3863
+ }
3864
+ for (const entry of entries) {
3865
+ if (resources.length >= MAX_RESOURCE_LISTING)
3866
+ return;
3867
+ if (entry === "SKILL.md")
3868
+ continue;
3869
+ const full = join3(dir, entry);
3870
+ const rel = prefix ? `${prefix}/${entry}` : entry;
3871
+ try {
3872
+ if (statSync(full).isDirectory()) {
3873
+ walk(full, rel);
3874
+ } else {
3875
+ resources.push(rel);
3876
+ }
3877
+ } catch {}
3878
+ }
3748
3879
  }
3749
- return visual;
3880
+ walk(skillDir, "");
3881
+ return resources;
3750
3882
  }
3751
- function buildPromptDisplay(text, cursor, maxLen) {
3752
- const clampedCursor = Math.max(0, Math.min(cursor, text.length));
3753
- if (maxLen <= 0)
3754
- return { display: "", cursor: 0 };
3755
- if (text.length <= maxLen)
3756
- return { display: text, cursor: clampedCursor };
3757
- let start = Math.max(0, clampedCursor - maxLen);
3758
- const end = Math.min(text.length, start + maxLen);
3759
- if (end - start < maxLen)
3760
- start = Math.max(0, end - maxLen);
3761
- let display = text.slice(start, end);
3762
- if (start > 0 && display.length > 0)
3763
- display = `\u2026${display.slice(1)}`;
3764
- if (end < text.length && display.length > 0)
3765
- display = `${display.slice(0, -1)}\u2026`;
3766
- return {
3767
- display,
3768
- cursor: Math.min(clampedCursor - start, display.length)
3769
- };
3883
+ function loadSkillContentFromMeta(skill) {
3884
+ try {
3885
+ const content = readFileSync2(skill.filePath, "utf-8");
3886
+ const skillDir = dirname2(skill.filePath);
3887
+ return {
3888
+ name: skill.name,
3889
+ description: skill.description,
3890
+ content,
3891
+ source: skill.source,
3892
+ skillDir,
3893
+ resources: listSkillResources(skillDir)
3894
+ };
3895
+ } catch {
3896
+ return null;
3897
+ }
3898
+ }
3899
+ function loadSkillContent(name, cwd, homeDir) {
3900
+ const skill = loadSkillsIndex(cwd, homeDir).get(name);
3901
+ if (!skill)
3902
+ return null;
3903
+ return loadSkillContentFromMeta(skill);
3770
3904
  }
3771
3905
 
3772
3906
  // src/cli/completions.ts
3773
- import { join as join4, relative } from "path";
3774
3907
  var BUILTIN_COMMANDS = [
3775
3908
  "model",
3776
3909
  "models",
@@ -5514,82 +5647,57 @@ var readSkillTool = {
5514
5647
 
5515
5648
  // src/agent/system-prompt.ts
5516
5649
  import { homedir as homedir5 } from "os";
5517
- var AUTONOMY = `
5518
-
5519
- # Autonomy
5520
- - Begin work immediately using tools. Gather context, implement, and verify \u2014 do not ask for permission to start.
5521
- - Carry changes through to completion. If blocked, summarise what is preventing progress instead of looping.
5522
- - Verify facts by inspecting files or running commands \u2014 never guess unknown state.`;
5523
- var SAFETY = `
5524
-
5525
- # Safety
5526
- - Never expose, print, or commit secrets, tokens, or keys.
5527
- - Never invent URLs \u2014 only use URLs the user provided or that exist in project files.
5528
- - Never revert user-authored changes unless explicitly asked. This includes \`git checkout\`, \`git restore\`, \`git stash\`, or any command that discards uncommitted work \u2014 even to "separate concerns" across commits.
5529
- - Before any destructive or irreversible action (deleting data, force-pushing, resetting history), ask one targeted confirmation question \u2014 mistakes here are unrecoverable.
5530
- - If files you are editing change unexpectedly, pause and ask how to proceed.`;
5531
- var COMMUNICATION = `
5532
-
5533
- # Communication
5534
- - Be concise: short bullets or a brief paragraph. No ceremonial preambles.
5535
- - For long tasks, send a one-sentence progress update every 3-5 tool calls.
5536
- - For code changes, state what changed, where, and why. Reference files with line numbers.
5537
- - Do not paste large file contents unless asked.`;
5538
- var ERROR_HANDLING = `
5539
-
5540
- # Error handling
5541
- - On tool failure: read the error, adjust your approach, and retry once. If it fails again, explain the blocker to the user.
5542
- - If you find yourself re-reading or re-editing the same files without progress, stop and summarise what is blocking you.`;
5543
5650
  function buildSystemPrompt(sessionTimeAnchor, cwd, homeDir) {
5544
5651
  const globalContext = loadGlobalContextFile(homeDir ?? homedir5());
5545
5652
  const localContext = loadLocalContextFile(cwd);
5546
5653
  const cwdDisplay = tildePath(cwd);
5547
- let prompt = `You are mini-coder, a small and fast CLI coding agent.
5548
- You have access to shell, listSkills, readSkill, connected MCP tools, and optional web tools.
5654
+ let prompt = `You are using mini-coder, a small and fast CLI coding agent harness.
5549
5655
 
5550
5656
  Current working directory: ${cwdDisplay}
5551
5657
  Current date/time: ${sessionTimeAnchor}
5552
5658
 
5553
- Guidelines:
5554
- - You are a capable senior engineer. Proactively gather context and implement \u2014 work the problem, not just the symptom. Prefer root-cause fixes over patches.
5555
- - Inspect code and files primarily through shell commands. Use temp files for large content to avoid filling your context window.
5556
- - For file edits, invoke \`mc-edit\` via shell. Prefer small, targeted edits over full rewrites so diffs stay reviewable.
5557
- - Make parallel tool calls when the lookups are independent \u2014 this speeds up multi-file investigation.
5558
- - Before starting work, scan the skills list below. If there is even a small chance a skill applies to your task, load it with \`readSkill\` and follow its instructions before writing code or responding. Skills are mandatory when they match \u2014 not optional references.
5559
- - Keep it simple: DRY, KISS, YAGNI. Avoid unnecessary complexity.
5560
- - Apply Rob Pike's 5 Rules of Programming:
5659
+ # Guidelines:
5660
+ - Act like am experienced senior engineer. Proactively gather context and implement \u2014 work the problem, not just the symptom. Prefer root-cause fixes over patches and avoid hacks and over-engineering.
5661
+ - Inspect code and files primarily through shell commands. Always prefer \`mc-edit\` command via shell tool for file edits.
5662
+ - Always apply DRY, KISS, and YAGNI.
5663
+ - Always apply Rob Pike's 5 Rules of Programming:
5561
5664
  1. You can't tell where a program is going to spend its time. Bottlenecks occur in surprising places, so don't try to second guess and put in a speed hack until you've proven that's where the bottleneck is.
5562
5665
  2. Measure. Don't tune for speed until you've measured, and even then don't unless one part of the code overwhelms the rest.
5563
5666
  3. Fancy algorithms are slow when n is small, and n is usually small. Fancy algorithms have big constants. Until you know that n is frequently going to be big, don't get fancy. (Even if n does get big, use Rule 2 first.)
5564
5667
  4. Fancy algorithms are buggier than simple ones, and they're much harder to implement. Use simple algorithms as well as simple data structures.
5565
5668
  5. Data dominates. If you've chosen the right data structures and organized things well, the algorithms will almost always be self-evident. Data structures, not algorithms, are central to programming.
5566
5669
 
5670
+ ## Autonomy
5671
+ - Begin work immediately using tools. Gather context, implement, and verify \u2014 do not ask for permission to start.
5672
+ - Carry changes through to completion. If blocked, summarise what is preventing progress instead of looping
5673
+
5674
+ ## Safety
5675
+ - Never expose, print, or commit secrets, tokens, or keys.
5676
+ - Never invent URLs \u2014 only use URLs the user provided or that exist in project files.
5677
+ - Verify facts by inspecting files, running commands, and checking online \u2014 never guess unknown state, never make assumptions
5678
+ - Never revert user-authored changes unless explicitly asked. This includes \`git checkout\`, \`git restore\`, \`git stash\`, or any command that discards uncommitted work \u2014 even to "separate concerns" across commits.
5679
+ - Before any destructive or irreversible action (deleting data, force-pushing, resetting history), ask one targeted confirmation question \u2014 mistakes here are unrecoverable.
5680
+ - If files you are editing change unexpectedly, pause and ask how to proceed.
5681
+
5682
+ ## Communication
5683
+ - Be concise: short bullets or a brief paragraph. No ceremonial preambles.
5684
+ - For code changes, state what changed, where, and why.
5685
+ - Do not paste large file contents unless asked.
5686
+
5687
+ ## Error handling
5688
+ - On tool failure: read the error, adjust your approach, and retry once. If it fails again, explain the blocker to the user.
5689
+ - If you find yourself re-reading or re-editing the same files without progress, stop and summarise what is blocking you.
5690
+
5567
5691
  # File editing with mc-edit
5568
5692
  \`mc-edit\` applies one exact-text replacement per invocation. It fails deterministically if the old text is missing or matches more than once.
5569
5693
 
5570
5694
  Usage: mc-edit <path> (--old <text> | --old-file <path>) [--new <text> | --new-file <path>] [--cwd <path>]
5571
5695
  - Omit --new / --new-file to delete the matched text.
5572
5696
  - To create new files, use shell commands (e.g. \`cat > file.txt << 'EOF'\\n...\\nEOF\`).
5573
- `;
5574
- prompt += AUTONOMY;
5575
- prompt += SAFETY;
5576
- prompt += COMMUNICATION;
5577
- prompt += ERROR_HANDLING;
5578
- if (globalContext || localContext) {
5579
- prompt += `
5580
-
5581
- # Project context`;
5582
- if (globalContext) {
5583
- prompt += `
5584
-
5585
- ${globalContext}`;
5586
- }
5587
- if (localContext) {
5588
- prompt += `
5589
5697
 
5590
- ${localContext}`;
5591
- }
5592
- }
5698
+ # Subagents via mc
5699
+ Use the \`mc\` command via the shell tool to instantiate a sub agent with a prompt for their task.
5700
+ `;
5593
5701
  const skills = Array.from(loadSkillsIndex(cwd, homeDir).values());
5594
5702
  if (skills.length > 0) {
5595
5703
  prompt += `
@@ -5618,6 +5726,18 @@ ${localContext}`;
5618
5726
  prompt += `
5619
5727
  </available_skills>`;
5620
5728
  }
5729
+ if (globalContext || localContext) {
5730
+ if (globalContext) {
5731
+ prompt += `
5732
+
5733
+ ${globalContext}`;
5734
+ }
5735
+ if (localContext) {
5736
+ prompt += `
5737
+
5738
+ ${localContext}`;
5739
+ }
5740
+ }
5621
5741
  return prompt;
5622
5742
  }
5623
5743
 
@@ -5656,6 +5776,7 @@ class SessionRunner {
5656
5776
  totalOut = 0;
5657
5777
  lastContextTokens = 0;
5658
5778
  _systemPrompt;
5779
+ onTeardown;
5659
5780
  constructor(opts) {
5660
5781
  this.cwd = opts.cwd;
5661
5782
  this.reporter = opts.reporter;
@@ -5665,6 +5786,7 @@ class SessionRunner {
5665
5786
  this.currentThinkingEffort = opts.initialThinkingEffort;
5666
5787
  this.showReasoning = opts.initialShowReasoning;
5667
5788
  this.verboseOutput = opts.initialVerboseOutput;
5789
+ this.onTeardown = opts.onTeardown;
5668
5790
  this.initSession(opts.sessionId);
5669
5791
  }
5670
5792
  getStatusInfo() {
@@ -5681,8 +5803,7 @@ class SessionRunner {
5681
5803
  if (sessionId) {
5682
5804
  const resumed = resumeSession(sessionId);
5683
5805
  if (!resumed) {
5684
- this.reporter.error(`Session "${sessionId}" not found.`);
5685
- process.exit(1);
5806
+ throw new Error(`Session "${sessionId}" not found.`);
5686
5807
  }
5687
5808
  this.session = resumed;
5688
5809
  this.currentModel = this.session.model;
@@ -5713,6 +5834,7 @@ class SessionRunner {
5713
5834
  this.session = resumed;
5714
5835
  this.currentModel = resumed.model;
5715
5836
  this.coreHistory = [...resumed.messages];
5837
+ resetActivatedSkills();
5716
5838
  this.turnIndex = getMaxTurnIndex(resumed.id) + 1;
5717
5839
  this.totalIn = 0;
5718
5840
  this.totalOut = 0;
@@ -5721,6 +5843,9 @@ class SessionRunner {
5721
5843
  this.rebuildSystemPrompt();
5722
5844
  return true;
5723
5845
  }
5846
+ async teardown() {
5847
+ await this.onTeardown?.();
5848
+ }
5724
5849
  addShellContext(command, output) {
5725
5850
  const thisTurn = this.turnIndex++;
5726
5851
  const msg = {
@@ -6087,7 +6212,8 @@ async function initAgent(opts) {
6087
6212
  const cwd = opts.cwd;
6088
6213
  const tools = buildToolSet2({ cwd });
6089
6214
  const mcpTools = [];
6090
- async function connectAndAddMcp(name) {
6215
+ const mcpClients = new McpClientRegistry;
6216
+ async function connectMcpClient(name) {
6091
6217
  const rows = listMcpServers();
6092
6218
  const row = rows.find((r) => r.name === name);
6093
6219
  if (!row)
@@ -6100,10 +6226,26 @@ async function initAgent(opts) {
6100
6226
  ...row.args ? { args: JSON.parse(row.args) } : {},
6101
6227
  ...row.env ? { env: JSON.parse(row.env) } : {}
6102
6228
  };
6103
- const client = await connectMcpServer(cfg);
6229
+ return connectMcpServer(cfg);
6230
+ }
6231
+ async function connectAndAddMcp(name) {
6232
+ const client = await connectMcpClient(name);
6233
+ mcpClients.add(client);
6104
6234
  tools.push(...client.tools);
6105
6235
  mcpTools.push(...client.tools);
6106
6236
  }
6237
+ async function reconnectConfiguredMcpServers() {
6238
+ await reconnectMcpTools({
6239
+ tools,
6240
+ mcpTools,
6241
+ clients: mcpClients,
6242
+ names: listMcpServers().map((row) => row.name),
6243
+ connectByName: connectMcpClient,
6244
+ onError: (name, error) => {
6245
+ opts.reporter.error(`MCP: failed to connect ${name}: ${String(error)}`);
6246
+ }
6247
+ });
6248
+ }
6107
6249
  for (const row of listMcpServers()) {
6108
6250
  try {
6109
6251
  await connectAndAddMcp(row.name);
@@ -6121,7 +6263,8 @@ async function initAgent(opts) {
6121
6263
  initialThinkingEffort: opts.initialThinkingEffort,
6122
6264
  initialShowReasoning: opts.initialShowReasoning,
6123
6265
  initialVerboseOutput: opts.initialVerboseOutput,
6124
- sessionId: opts.sessionId
6266
+ sessionId: opts.sessionId,
6267
+ onTeardown: () => mcpClients.closeAll()
6125
6268
  });
6126
6269
  const cmdCtx = {
6127
6270
  get currentModel() {
@@ -6158,12 +6301,48 @@ async function initAgent(opts) {
6158
6301
  connectMcpServer: connectAndAddMcp,
6159
6302
  startSpinner: (label) => opts.reporter.startSpinner(label),
6160
6303
  stopSpinner: () => opts.reporter.stopSpinner(),
6161
- startNewSession: () => runner.startNewSession(),
6162
- switchSession: (id) => runner.switchSession(id)
6304
+ startNewSession: async () => {
6305
+ runner.startNewSession();
6306
+ await reconnectConfiguredMcpServers();
6307
+ },
6308
+ switchSession: async (id) => {
6309
+ const switched = runner.switchSession(id);
6310
+ if (!switched)
6311
+ return false;
6312
+ await reconnectConfiguredMcpServers();
6313
+ return true;
6314
+ }
6163
6315
  };
6164
6316
  return { runner, cmdCtx };
6165
6317
  }
6166
6318
 
6319
+ // src/agent/run-with-teardown.ts
6320
+ async function runWithTeardown(opts) {
6321
+ let hasPrimaryError = false;
6322
+ let primaryError;
6323
+ let result;
6324
+ try {
6325
+ result = await opts.run();
6326
+ } catch (error) {
6327
+ hasPrimaryError = true;
6328
+ if (error instanceof RenderedError) {
6329
+ primaryError = error;
6330
+ } else {
6331
+ opts.renderError(error);
6332
+ primaryError = new RenderedError(error);
6333
+ }
6334
+ }
6335
+ try {
6336
+ await opts.teardown?.();
6337
+ } catch (teardownError) {
6338
+ if (!hasPrimaryError)
6339
+ throw teardownError;
6340
+ }
6341
+ if (hasPrimaryError)
6342
+ throw primaryError;
6343
+ return result;
6344
+ }
6345
+
6167
6346
  // src/cli/args.ts
6168
6347
  import * as c12 from "yoctocolors";
6169
6348
  function parseArgs(argv) {
@@ -6315,6 +6494,7 @@ function renderHelpCommand(ctx) {
6315
6494
  writeln(` ${c13.dim("model + context")}`);
6316
6495
  renderEntries([
6317
6496
  ["/model [id]", "list or switch models"],
6497
+ ["/models [id]", "alias for /model"],
6318
6498
  ["/reasoning [on|off]", "toggle reasoning display"],
6319
6499
  ["/verbose [on|off]", "toggle output truncation"],
6320
6500
  ["/mcp list", "list MCP servers"],
@@ -6659,7 +6839,7 @@ async function handleSessionCommand(ctx, args) {
6659
6839
  const id = args.trim();
6660
6840
  if (id) {
6661
6841
  ctx.startSpinner("switching session");
6662
- const ok2 = ctx.switchSession(id);
6842
+ const ok2 = await ctx.switchSession(id);
6663
6843
  ctx.stopSpinner();
6664
6844
  if (ok2) {
6665
6845
  writeln(`${PREFIX.success} switched to session ${c17.cyan(id)} (${c17.cyan(ctx.currentModel)})`);
@@ -6693,7 +6873,7 @@ async function handleSessionCommand(ctx, args) {
6693
6873
  }
6694
6874
  if (!picked)
6695
6875
  return;
6696
- const ok = ctx.switchSession(picked);
6876
+ const ok = await ctx.switchSession(picked);
6697
6877
  if (ok) {
6698
6878
  writeln(`${PREFIX.success} switched to session ${c17.cyan(picked)} (${c17.cyan(ctx.currentModel)})`);
6699
6879
  } else {
@@ -6715,10 +6895,10 @@ async function handleUndo(ctx) {
6715
6895
  ctx.stopSpinner();
6716
6896
  }
6717
6897
  }
6718
- function handleNew(ctx) {
6719
- ctx.startNewSession();
6898
+ async function handleNew(ctx) {
6899
+ await ctx.startNewSession();
6720
6900
  process.stdout.write("\x1B[2J\x1B[H");
6721
- renderBanner(ctx.currentModel, ctx.cwd);
6901
+ await renderBanner(ctx.currentModel, ctx.cwd);
6722
6902
  }
6723
6903
  async function handleCommand(command, args, ctx) {
6724
6904
  switch (command.toLowerCase()) {
@@ -6749,7 +6929,7 @@ async function handleCommand(command, args, ctx) {
6749
6929
  await handleSessionCommand(ctx, args);
6750
6930
  return { type: "handled" };
6751
6931
  case "new":
6752
- handleNew(ctx);
6932
+ await handleNew(ctx);
6753
6933
  return { type: "handled" };
6754
6934
  case "help":
6755
6935
  case "?":
@@ -6998,7 +7178,6 @@ globalThis.AI_SDK_LOG_WARNINGS = false;
6998
7178
  registerTerminalCleanup();
6999
7179
  initModelInfoCache();
7000
7180
  pruneOldData();
7001
- refreshModelInfoInBackground().catch(() => {});
7002
7181
  function buildAgentOptions(opts) {
7003
7182
  return {
7004
7183
  model: opts.model,
@@ -7038,38 +7217,47 @@ async function main() {
7038
7217
  sessionId = args.sessionId;
7039
7218
  }
7040
7219
  const model = args.model ?? getPreferredModel() ?? autoDiscoverModel();
7220
+ const connectedProviders = prompt ? undefined : await discoverConnectedProviders();
7221
+ const startupLocalProviders = connectedProviders ? getLocalProviderNames(connectedProviders) : undefined;
7222
+ refreshModelInfoInBackground(startupLocalProviders ? { localProviders: startupLocalProviders } : undefined).catch(() => {});
7041
7223
  if (!prompt) {
7042
- renderBanner(model, args.cwd);
7224
+ await renderBanner(model, args.cwd, connectedProviders);
7043
7225
  }
7044
- try {
7045
- const oneShot = !!prompt;
7046
- const agentOpts = buildAgentOptions({
7047
- model,
7048
- cwd: args.cwd,
7049
- reporter: new CliReporter(oneShot),
7050
- sessionId
7051
- });
7052
- const { runner, cmdCtx } = await initAgent(agentOpts);
7053
- if (oneShot) {
7054
- const { text: resolvedText, images: refImages } = await resolveFileRefs(prompt, args.cwd);
7055
- const responseText = await runner.processUserInput(resolvedText, refImages);
7056
- if (responseText) {
7057
- writeln(responseText.trimStart());
7226
+ let runner = null;
7227
+ const oneShot = !!prompt;
7228
+ const agentOpts = buildAgentOptions({
7229
+ model,
7230
+ cwd: args.cwd,
7231
+ reporter: new CliReporter(oneShot),
7232
+ sessionId
7233
+ });
7234
+ await runWithTeardown({
7235
+ run: async () => {
7236
+ const init = await initAgent(agentOpts);
7237
+ runner = init.runner;
7238
+ const { cmdCtx } = init;
7239
+ if (oneShot) {
7240
+ const { text: resolvedText, images: refImages } = await resolveFileRefs(prompt, args.cwd);
7241
+ const responseText = await runner.processUserInput(resolvedText, refImages);
7242
+ if (responseText) {
7243
+ writeln(responseText.trimStart());
7244
+ }
7245
+ return;
7246
+ }
7247
+ await runInputLoop({
7248
+ cwd: args.cwd,
7249
+ reporter: agentOpts.reporter,
7250
+ cmdCtx,
7251
+ runner
7252
+ });
7253
+ },
7254
+ teardown: () => runner?.teardown() ?? Promise.resolve(),
7255
+ renderError: (err) => {
7256
+ if (!(err instanceof RenderedError)) {
7257
+ renderError(err, "agent");
7058
7258
  }
7059
- return;
7060
- }
7061
- await runInputLoop({
7062
- cwd: args.cwd,
7063
- reporter: agentOpts.reporter,
7064
- cmdCtx,
7065
- runner
7066
- });
7067
- } catch (err) {
7068
- if (!(err instanceof RenderedError)) {
7069
- renderError(err, "agent");
7070
7259
  }
7071
- process.exit(1);
7072
- }
7260
+ });
7073
7261
  }
7074
7262
  main().then(() => process.exit(0), (err) => {
7075
7263
  if (!(err instanceof RenderedError)) {