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/README.md +2 -2
- package/dist/mc.js +2156 -1968
- package/docs/mini-coder.1.md +13 -6
- package/package.json +1 -1
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.
|
|
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/
|
|
346
|
-
|
|
347
|
-
|
|
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
|
|
374
|
-
|
|
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
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
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
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
|
423
|
-
|
|
424
|
-
|
|
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
|
|
418
|
+
return _addPromptHistoryStmt;
|
|
436
419
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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
|
|
424
|
+
return _getPromptHistoryStmt;
|
|
454
425
|
}
|
|
455
|
-
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
-
|
|
466
|
-
|
|
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
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
-
|
|
525
|
-
const
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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
|
-
|
|
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
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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
|
-
|
|
654
|
-
const
|
|
655
|
-
|
|
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
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
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
|
|
750
|
-
|
|
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
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
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
|
|
767
|
-
|
|
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
|
|
770
|
-
return
|
|
558
|
+
function listSessions(limit = 20) {
|
|
559
|
+
return getDb().query("SELECT * FROM sessions ORDER BY updated_at DESC LIMIT ?").all(limit);
|
|
771
560
|
}
|
|
772
|
-
function
|
|
773
|
-
|
|
774
|
-
const
|
|
775
|
-
|
|
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
|
-
|
|
796
|
-
function
|
|
797
|
-
|
|
798
|
-
|
|
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
|
-
|
|
813
|
-
|
|
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
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
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
|
-
|
|
582
|
+
if (normalized === "false" || normalized === "off" || normalized === "0") {
|
|
583
|
+
return false;
|
|
584
|
+
}
|
|
585
|
+
return fallback;
|
|
823
586
|
}
|
|
824
|
-
function
|
|
825
|
-
|
|
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
|
|
834
|
-
|
|
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
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
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
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
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
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
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
|
|
650
|
+
return out;
|
|
924
651
|
}
|
|
925
|
-
function
|
|
926
|
-
|
|
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
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
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
|
-
|
|
946
|
-
const modelId = modelString.slice(slash + 1);
|
|
947
|
-
return { provider: provider || null, modelId };
|
|
665
|
+
return next;
|
|
948
666
|
}
|
|
949
|
-
function
|
|
950
|
-
|
|
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
|
|
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
|
-
|
|
955
|
-
|
|
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
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
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
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
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
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
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
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
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
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
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
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
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
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
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
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
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
|
-
|
|
843
|
+
function extractAccountId(accessToken) {
|
|
1097
844
|
try {
|
|
1098
|
-
const
|
|
1099
|
-
|
|
1100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1110
|
-
|
|
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
|
|
1113
|
-
|
|
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
|
-
|
|
1129
|
-
|
|
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
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
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
|
-
|
|
1170
|
-
|
|
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
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
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
|
-
|
|
1217
|
-
|
|
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
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
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
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
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
|
-
|
|
1237
|
-
|
|
1238
|
-
contextWindow,
|
|
1239
|
-
free: false
|
|
986
|
+
...message,
|
|
987
|
+
content: nextContent
|
|
1240
988
|
};
|
|
1241
989
|
});
|
|
990
|
+
return mutated ? result : messages;
|
|
1242
991
|
}
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
let
|
|
1249
|
-
|
|
1250
|
-
|
|
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
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
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
|
|
1303
|
-
|
|
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
|
|
1306
|
-
const
|
|
1307
|
-
if (!
|
|
1021
|
+
function parseContextWindow(model) {
|
|
1022
|
+
const limit = model.limit;
|
|
1023
|
+
if (!isRecord(limit))
|
|
1308
1024
|
return null;
|
|
1309
|
-
const
|
|
1310
|
-
if (!Number.isFinite(
|
|
1025
|
+
const context = limit.context;
|
|
1026
|
+
if (typeof context !== "number" || !Number.isFinite(context))
|
|
1311
1027
|
return null;
|
|
1312
|
-
return
|
|
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
|
|
1344
|
-
|
|
1345
|
-
if (
|
|
1346
|
-
return
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
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
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
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
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
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
|
-
|
|
1376
|
-
|
|
1377
|
-
const
|
|
1378
|
-
const
|
|
1379
|
-
|
|
1380
|
-
|
|
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
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
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
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
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
|
-
|
|
1486
|
-
|
|
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
|
|
1489
|
-
|
|
1490
|
-
return provider === "anthropic" || isZenProvider(provider) && modelId.startsWith("claude-");
|
|
1146
|
+
function providerModelKey(provider, modelId) {
|
|
1147
|
+
return `${provider}/${modelId}`;
|
|
1491
1148
|
}
|
|
1492
|
-
function
|
|
1493
|
-
|
|
1494
|
-
|
|
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
|
|
1497
|
-
const
|
|
1498
|
-
|
|
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
|
|
1501
|
-
|
|
1502
|
-
|
|
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
|
|
1505
|
-
const
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
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
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
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/
|
|
1521
|
-
var
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
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
|
|
1536
|
-
|
|
1537
|
-
const
|
|
1538
|
-
|
|
1539
|
-
|
|
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
|
|
1544
|
-
|
|
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
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
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
|
|
1608
|
-
const
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
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
|
|
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
|
-
|
|
1652
|
-
|
|
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
|
|
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
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
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
|
|
1690
|
-
const
|
|
1691
|
-
if (
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
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
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
return
|
|
1714
|
-
|
|
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
|
|
1717
|
-
|
|
1718
|
-
}
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
}
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
}
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
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
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
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/
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
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
|
|
1488
|
+
return false;
|
|
1764
1489
|
}
|
|
1765
|
-
function
|
|
1766
|
-
if (
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
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
|
|
1782
|
-
|
|
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
|
|
1499
|
+
return false;
|
|
1788
1500
|
}
|
|
1789
1501
|
}
|
|
1790
|
-
function
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
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
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
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
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
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
|
-
|
|
1821
|
-
|
|
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
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
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
|
|
1629
|
+
return false;
|
|
1830
1630
|
}
|
|
1831
|
-
function
|
|
1832
|
-
|
|
1833
|
-
|
|
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
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
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
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
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
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
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 (
|
|
1880
|
-
return
|
|
1881
|
-
|
|
1882
|
-
|
|
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
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
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
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
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
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
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
|
|
1777
|
+
import { homedir as homedir4 } from "os";
|
|
1916
1778
|
import { dirname as dirname2, join as join3, resolve as resolve2 } from "path";
|
|
1917
|
-
import * as
|
|
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/
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
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
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
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
|
|
2003
|
-
|
|
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
|
|
2006
|
-
|
|
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
|
|
2017
|
-
|
|
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
|
-
|
|
2025
|
-
if (
|
|
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 =
|
|
1885
|
+
const parent = dirname(current);
|
|
2028
1886
|
if (parent === current)
|
|
2029
1887
|
break;
|
|
2030
1888
|
current = parent;
|
|
2031
1889
|
}
|
|
2032
|
-
return
|
|
1890
|
+
return [];
|
|
2033
1891
|
}
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
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
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
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
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
if (
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
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
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
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
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
if (
|
|
2097
|
-
|
|
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
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
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 (
|
|
2114
|
-
|
|
2115
|
-
|
|
2239
|
+
if (typeof err === "string") {
|
|
2240
|
+
const msg = err.trim();
|
|
2241
|
+
if (msg)
|
|
2242
|
+
return msg;
|
|
2116
2243
|
}
|
|
2117
|
-
if (
|
|
2118
|
-
|
|
2244
|
+
if (typeof err === "number" || typeof err === "boolean" || typeof err === "bigint") {
|
|
2245
|
+
return String(err);
|
|
2119
2246
|
}
|
|
2120
|
-
if (
|
|
2121
|
-
|
|
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
|
|
2134
|
-
|
|
2135
|
-
|
|
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
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
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
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
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
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2194
|
-
|
|
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
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
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
|
|
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 ?
|
|
2343
|
-
terminal.stderrWrite(`\r${
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
2502
|
+
let pctColored = c2.dim(pctStr);
|
|
2384
2503
|
if (pct >= 90)
|
|
2385
|
-
pctColored =
|
|
2504
|
+
pctColored = c2.red(pctStr);
|
|
2386
2505
|
else if (pct >= 75)
|
|
2387
|
-
pctColored =
|
|
2388
|
-
return
|
|
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}${
|
|
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 ? `${
|
|
2414
|
-
const required = [modelSegment,
|
|
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(
|
|
2536
|
+
optional.push(c2.dim(`\u2387 ${opts.gitBranch}`));
|
|
2418
2537
|
if (opts.inputTokens > 0 || opts.outputTokens > 0) {
|
|
2419
|
-
optional.push(
|
|
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(
|
|
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
|
|
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
|
|
2846
|
+
import * as c4 from "yoctocolors";
|
|
2728
2847
|
|
|
2729
2848
|
// src/cli/tool-result-renderers.ts
|
|
2730
|
-
import * as
|
|
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(`${
|
|
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(
|
|
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 =
|
|
2931
|
+
let badge = c3.red("error");
|
|
2813
2932
|
if (r.timedOut)
|
|
2814
|
-
badge =
|
|
2933
|
+
badge = c3.yellow("timeout");
|
|
2815
2934
|
else if (r.success)
|
|
2816
|
-
badge =
|
|
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} ${
|
|
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 ` ${
|
|
2850
|
-
return ` ${
|
|
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 ? `${
|
|
2856
|
-
writeln(`${G.info} ${labelPrefix}${name} ${
|
|
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} ${
|
|
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(`${
|
|
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} ${
|
|
3002
|
+
writeln(`${G.info} ${c3.dim("skill")} ${c3.dim(r.note.trim())}`);
|
|
2884
3003
|
return true;
|
|
2885
3004
|
}
|
|
2886
|
-
writeln(`${G.info} ${
|
|
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(
|
|
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} ${
|
|
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" ?
|
|
2910
|
-
writeln(`${
|
|
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(` ${
|
|
3031
|
+
writeln(` ${c3.dim(item.url)}`);
|
|
2913
3032
|
}
|
|
2914
3033
|
if (r.results.length > maxResults) {
|
|
2915
|
-
writeln(`${
|
|
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} ${
|
|
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(`${
|
|
3049
|
+
writeln(`${c3.dim("\u2022")} ${title}`);
|
|
2931
3050
|
if (item.url)
|
|
2932
|
-
writeln(` ${
|
|
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(` ${
|
|
3055
|
+
writeln(` ${c3.dim(trimmed)}`);
|
|
2937
3056
|
}
|
|
2938
3057
|
}
|
|
2939
3058
|
if (r.results.length > maxPages) {
|
|
2940
|
-
writeln(`${
|
|
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 ? ` ${
|
|
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} ${
|
|
3169
|
+
return `${G.run} ${c4.dim("shell")}`;
|
|
3051
3170
|
return formatShellCallLine(cmd);
|
|
3052
3171
|
}
|
|
3053
3172
|
if (name === "listSkills") {
|
|
3054
|
-
return `${G.search} ${
|
|
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} ${
|
|
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} ${
|
|
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} ${
|
|
3186
|
+
return `${G.read} ${c4.dim("fetch")} ${label}`;
|
|
3068
3187
|
}
|
|
3069
3188
|
if (name.startsWith("mcp_")) {
|
|
3070
|
-
return `${G.mcp} ${
|
|
3189
|
+
return `${G.mcp} ${c4.dim(name)}`;
|
|
3071
3190
|
}
|
|
3072
|
-
return `${toolGlyph(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(
|
|
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} ${
|
|
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(
|
|
3236
|
+
writeln(c4.dim(text));
|
|
3118
3237
|
return;
|
|
3119
3238
|
}
|
|
3120
|
-
writeln(
|
|
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
|
|
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} ${
|
|
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(`${
|
|
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} ${
|
|
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 =
|
|
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:
|
|
3411
|
-
reply:
|
|
3412
|
-
search:
|
|
3413
|
-
read:
|
|
3414
|
-
write:
|
|
3415
|
-
run:
|
|
3416
|
-
mcp:
|
|
3417
|
-
ok:
|
|
3418
|
-
err:
|
|
3419
|
-
warn:
|
|
3420
|
-
info:
|
|
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} ${
|
|
3561
|
+
writeln(`${G.err} ${c6.red(parsed.headline)}`);
|
|
3443
3562
|
if (parsed.hint) {
|
|
3444
|
-
writeln(` ${
|
|
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(` ${
|
|
3451
|
-
writeln(` ${
|
|
3452
|
-
writeln(` ${
|
|
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(` ${
|
|
3584
|
+
writeln(` ${c6.dim(items.join(" \xB7 "))}`);
|
|
3466
3585
|
}
|
|
3467
3586
|
const connParts = [];
|
|
3468
|
-
for (const p of discoverConnectedProviders()) {
|
|
3469
|
-
|
|
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(` ${
|
|
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} ${
|
|
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
|
-
|
|
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
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
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
|
|
3584
|
-
|
|
3585
|
-
|
|
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
|
|
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
|
|
3602
|
-
|
|
3603
|
-
return rows.map((r) => r.text).reverse();
|
|
3697
|
+
function candidateConflictName(candidate) {
|
|
3698
|
+
return getCandidateFrontmatter(candidate).name?.trim() || candidate.folderName;
|
|
3604
3699
|
}
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
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
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
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
|
|
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
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
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
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
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
|
|
3711
|
-
const
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
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
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
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
|
-
|
|
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
|
|
3728
|
-
|
|
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
|
|
3731
|
-
|
|
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
|
|
3734
|
-
const
|
|
3735
|
-
const
|
|
3736
|
-
|
|
3737
|
-
if (
|
|
3738
|
-
|
|
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
|
|
3852
|
+
return index;
|
|
3741
3853
|
}
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
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
|
-
|
|
3880
|
+
walk(skillDir, "");
|
|
3881
|
+
return resources;
|
|
3750
3882
|
}
|
|
3751
|
-
function
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
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
|
-
-
|
|
5555
|
-
- Inspect code and files primarily through shell commands.
|
|
5556
|
-
-
|
|
5557
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: () =>
|
|
6162
|
-
|
|
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
|
-
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
|
|
7048
|
-
|
|
7049
|
-
|
|
7050
|
-
|
|
7051
|
-
|
|
7052
|
-
|
|
7053
|
-
|
|
7054
|
-
const
|
|
7055
|
-
|
|
7056
|
-
|
|
7057
|
-
|
|
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
|
-
|
|
7072
|
-
}
|
|
7260
|
+
});
|
|
7073
7261
|
}
|
|
7074
7262
|
main().then(() => process.exit(0), (err) => {
|
|
7075
7263
|
if (!(err instanceof RenderedError)) {
|