mini-coder 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mc.js +183 -384
- package/docs/KNOWN_ISSUES.md +1 -0
- package/docs/design-decisions.md +1 -1
- package/docs/mini-coder.1.md +3 -8
- package/docs/superpowers/plans/2026-03-30-anthropic-oauth-removal.md +61 -0
- package/docs/superpowers/specs/2026-03-30-anthropic-oauth-removal-design.md +47 -0
- package/package.json +1 -1
package/dist/mc.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// @bun
|
|
3
3
|
|
|
4
4
|
// src/index.ts
|
|
5
|
-
import * as
|
|
5
|
+
import * as c20 from "yoctocolors";
|
|
6
6
|
|
|
7
7
|
// src/agent/agent.ts
|
|
8
8
|
import * as c11 from "yoctocolors";
|
|
@@ -13,7 +13,7 @@ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
|
13
13
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
14
14
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
15
15
|
// src/internal/version.ts
|
|
16
|
-
var PACKAGE_VERSION = "0.3.
|
|
16
|
+
var PACKAGE_VERSION = "0.3.1";
|
|
17
17
|
|
|
18
18
|
// src/mcp/client.ts
|
|
19
19
|
async function connectMcpServer(config) {
|
|
@@ -70,7 +70,7 @@ async function connectMcpServer(config) {
|
|
|
70
70
|
|
|
71
71
|
// src/session/db/connection.ts
|
|
72
72
|
import { Database } from "bun:sqlite";
|
|
73
|
-
import { existsSync, mkdirSync,
|
|
73
|
+
import { existsSync, mkdirSync, renameSync } from "fs";
|
|
74
74
|
import { homedir } from "os";
|
|
75
75
|
import { join } from "path";
|
|
76
76
|
function getConfigDir() {
|
|
@@ -201,27 +201,32 @@ function configureDb(db) {
|
|
|
201
201
|
db.exec("PRAGMA foreign_keys=ON;");
|
|
202
202
|
db.exec("PRAGMA busy_timeout=1000;");
|
|
203
203
|
}
|
|
204
|
+
function rotateDbFiles(dbPath, version) {
|
|
205
|
+
const backupBase = `${dbPath}.bak-v${version}-${Date.now()}`;
|
|
206
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
207
|
+
const path = `${dbPath}${suffix}`;
|
|
208
|
+
if (!existsSync(path))
|
|
209
|
+
continue;
|
|
210
|
+
renameSync(path, `${backupBase}${suffix}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
204
213
|
function getDb() {
|
|
205
214
|
if (!_db) {
|
|
206
215
|
const dbPath = getDbPath();
|
|
216
|
+
const dbExists = existsSync(dbPath);
|
|
207
217
|
let db = new Database(dbPath, { create: true });
|
|
208
218
|
configureDb(db);
|
|
209
219
|
const version = db.query("PRAGMA user_version").get()?.user_version ?? 0;
|
|
210
|
-
if (version !== DB_VERSION) {
|
|
220
|
+
if (dbExists && version !== DB_VERSION) {
|
|
211
221
|
try {
|
|
212
222
|
db.close();
|
|
213
223
|
} catch {}
|
|
214
|
-
|
|
215
|
-
if (existsSync(path))
|
|
216
|
-
unlinkSync(path);
|
|
217
|
-
}
|
|
224
|
+
rotateDbFiles(dbPath, version);
|
|
218
225
|
db = new Database(dbPath, { create: true });
|
|
219
226
|
configureDb(db);
|
|
220
|
-
db.exec(SCHEMA);
|
|
221
|
-
db.exec(`PRAGMA user_version = ${DB_VERSION};`);
|
|
222
|
-
} else {
|
|
223
|
-
db.exec(SCHEMA);
|
|
224
227
|
}
|
|
228
|
+
db.exec(SCHEMA);
|
|
229
|
+
db.exec(`PRAGMA user_version = ${DB_VERSION};`);
|
|
225
230
|
_db = db;
|
|
226
231
|
}
|
|
227
232
|
return _db;
|
|
@@ -436,7 +441,7 @@ import { createGoogleGenerativeAI } from "@ai-sdk/google";
|
|
|
436
441
|
import { createOpenAI } from "@ai-sdk/openai";
|
|
437
442
|
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
438
443
|
|
|
439
|
-
// src/session/oauth/
|
|
444
|
+
// src/session/oauth/openai.ts
|
|
440
445
|
import { createServer } from "http";
|
|
441
446
|
|
|
442
447
|
// src/session/oauth/pkce.ts
|
|
@@ -457,139 +462,18 @@ async function generatePKCE() {
|
|
|
457
462
|
return { verifier, challenge };
|
|
458
463
|
}
|
|
459
464
|
|
|
460
|
-
// src/session/oauth/
|
|
461
|
-
var CLIENT_ID = "
|
|
462
|
-
var AUTHORIZE_URL = "https://
|
|
463
|
-
var TOKEN_URL = "https://
|
|
465
|
+
// src/session/oauth/openai.ts
|
|
466
|
+
var CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
467
|
+
var AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize";
|
|
468
|
+
var TOKEN_URL = "https://auth.openai.com/oauth/token";
|
|
464
469
|
var CALLBACK_HOST = "127.0.0.1";
|
|
465
|
-
var CALLBACK_PORT =
|
|
466
|
-
var CALLBACK_PATH = "/callback";
|
|
470
|
+
var CALLBACK_PORT = 1455;
|
|
471
|
+
var CALLBACK_PATH = "/auth/callback";
|
|
467
472
|
var REDIRECT_URI = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
|
|
468
|
-
var SCOPES = "
|
|
473
|
+
var SCOPES = "openid profile email offline_access";
|
|
469
474
|
var LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
470
475
|
var SUCCESS_HTML = `<!doctype html>
|
|
471
476
|
<html lang="en"><head><meta charset="utf-8"><title>Authenticated</title></head>
|
|
472
|
-
<body><p>Authentication successful. Return to your terminal.</p></body></html>`;
|
|
473
|
-
function startCallbackServer(expectedState) {
|
|
474
|
-
return new Promise((resolve2, reject) => {
|
|
475
|
-
let result = null;
|
|
476
|
-
let cancelled = false;
|
|
477
|
-
const server = createServer((req, res) => {
|
|
478
|
-
const url = new URL(req.url ?? "", "http://localhost");
|
|
479
|
-
if (url.pathname !== CALLBACK_PATH) {
|
|
480
|
-
res.writeHead(404).end("Not found");
|
|
481
|
-
return;
|
|
482
|
-
}
|
|
483
|
-
const code = url.searchParams.get("code");
|
|
484
|
-
const state = url.searchParams.get("state");
|
|
485
|
-
const error = url.searchParams.get("error");
|
|
486
|
-
if (error || !code || !state || state !== expectedState) {
|
|
487
|
-
res.writeHead(400).end("Authentication failed.");
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
490
|
-
res.writeHead(200, { "Content-Type": "text/html" }).end(SUCCESS_HTML);
|
|
491
|
-
result = { code, state };
|
|
492
|
-
});
|
|
493
|
-
server.on("error", reject);
|
|
494
|
-
server.listen(CALLBACK_PORT, CALLBACK_HOST, () => {
|
|
495
|
-
resolve2({
|
|
496
|
-
server,
|
|
497
|
-
cancel: () => {
|
|
498
|
-
cancelled = true;
|
|
499
|
-
},
|
|
500
|
-
waitForCode: async () => {
|
|
501
|
-
const deadline = Date.now() + LOGIN_TIMEOUT_MS;
|
|
502
|
-
while (!result && !cancelled && Date.now() < deadline) {
|
|
503
|
-
await new Promise((r) => setTimeout(r, 100));
|
|
504
|
-
}
|
|
505
|
-
return result;
|
|
506
|
-
}
|
|
507
|
-
});
|
|
508
|
-
});
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
async function postTokenRequest(body, label) {
|
|
512
|
-
const res = await fetch(TOKEN_URL, {
|
|
513
|
-
method: "POST",
|
|
514
|
-
headers: {
|
|
515
|
-
"Content-Type": "application/json",
|
|
516
|
-
Accept: "application/json"
|
|
517
|
-
},
|
|
518
|
-
body: JSON.stringify(body),
|
|
519
|
-
signal: AbortSignal.timeout(30000)
|
|
520
|
-
});
|
|
521
|
-
if (!res.ok) {
|
|
522
|
-
const text = await res.text();
|
|
523
|
-
throw new Error(`${label} failed (${res.status}): ${text}`);
|
|
524
|
-
}
|
|
525
|
-
const data = await res.json();
|
|
526
|
-
return {
|
|
527
|
-
access: data.access_token,
|
|
528
|
-
refresh: data.refresh_token,
|
|
529
|
-
expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000
|
|
530
|
-
};
|
|
531
|
-
}
|
|
532
|
-
function exchangeCode(code, state, verifier) {
|
|
533
|
-
return postTokenRequest({
|
|
534
|
-
grant_type: "authorization_code",
|
|
535
|
-
client_id: CLIENT_ID,
|
|
536
|
-
code,
|
|
537
|
-
state,
|
|
538
|
-
redirect_uri: REDIRECT_URI,
|
|
539
|
-
code_verifier: verifier
|
|
540
|
-
}, "Token exchange");
|
|
541
|
-
}
|
|
542
|
-
function refreshAnthropicToken(refreshToken) {
|
|
543
|
-
return postTokenRequest({
|
|
544
|
-
grant_type: "refresh_token",
|
|
545
|
-
client_id: CLIENT_ID,
|
|
546
|
-
refresh_token: refreshToken
|
|
547
|
-
}, "Token refresh");
|
|
548
|
-
}
|
|
549
|
-
async function loginAnthropic(callbacks) {
|
|
550
|
-
const { verifier, challenge } = await generatePKCE();
|
|
551
|
-
const cb = await startCallbackServer(verifier);
|
|
552
|
-
try {
|
|
553
|
-
const params = new URLSearchParams({
|
|
554
|
-
code: "true",
|
|
555
|
-
client_id: CLIENT_ID,
|
|
556
|
-
response_type: "code",
|
|
557
|
-
redirect_uri: REDIRECT_URI,
|
|
558
|
-
scope: SCOPES,
|
|
559
|
-
code_challenge: challenge,
|
|
560
|
-
code_challenge_method: "S256",
|
|
561
|
-
state: verifier
|
|
562
|
-
});
|
|
563
|
-
callbacks.onOpenUrl(`${AUTHORIZE_URL}?${params}`, "Complete login in your browser.");
|
|
564
|
-
const result = await cb.waitForCode();
|
|
565
|
-
if (!result)
|
|
566
|
-
throw new Error("Login cancelled or no code received");
|
|
567
|
-
callbacks.onProgress("Exchanging authorization code for tokens\u2026");
|
|
568
|
-
return exchangeCode(result.code, result.state, verifier);
|
|
569
|
-
} finally {
|
|
570
|
-
cb.server.close();
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
var anthropicOAuth = {
|
|
574
|
-
id: "anthropic",
|
|
575
|
-
name: "Anthropic (Claude Pro/Max)",
|
|
576
|
-
login: loginAnthropic,
|
|
577
|
-
refreshToken: refreshAnthropicToken
|
|
578
|
-
};
|
|
579
|
-
|
|
580
|
-
// src/session/oauth/openai.ts
|
|
581
|
-
import { createServer as createServer2 } from "http";
|
|
582
|
-
var CLIENT_ID2 = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
583
|
-
var AUTHORIZE_URL2 = "https://auth.openai.com/oauth/authorize";
|
|
584
|
-
var TOKEN_URL2 = "https://auth.openai.com/oauth/token";
|
|
585
|
-
var CALLBACK_HOST2 = "127.0.0.1";
|
|
586
|
-
var CALLBACK_PORT2 = 1455;
|
|
587
|
-
var CALLBACK_PATH2 = "/auth/callback";
|
|
588
|
-
var REDIRECT_URI2 = `http://localhost:${CALLBACK_PORT2}${CALLBACK_PATH2}`;
|
|
589
|
-
var SCOPES2 = "openid profile email offline_access";
|
|
590
|
-
var LOGIN_TIMEOUT_MS2 = 5 * 60 * 1000;
|
|
591
|
-
var SUCCESS_HTML2 = `<!doctype html>
|
|
592
|
-
<html lang="en"><head><meta charset="utf-8"><title>Authenticated</title></head>
|
|
593
477
|
<body><p>OpenAI authentication successful. Return to your terminal.</p></body></html>`;
|
|
594
478
|
function createState() {
|
|
595
479
|
const bytes = new Uint8Array(16);
|
|
@@ -599,13 +483,13 @@ function createState() {
|
|
|
599
483
|
hex += b.toString(16).padStart(2, "0");
|
|
600
484
|
return hex;
|
|
601
485
|
}
|
|
602
|
-
function
|
|
486
|
+
function startCallbackServer(expectedState) {
|
|
603
487
|
return new Promise((resolve2, reject) => {
|
|
604
488
|
let result = null;
|
|
605
489
|
let cancelled = false;
|
|
606
|
-
const server =
|
|
490
|
+
const server = createServer((req, res) => {
|
|
607
491
|
const url = new URL(req.url ?? "", "http://localhost");
|
|
608
|
-
if (url.pathname !==
|
|
492
|
+
if (url.pathname !== CALLBACK_PATH) {
|
|
609
493
|
res.writeHead(404).end("Not found");
|
|
610
494
|
return;
|
|
611
495
|
}
|
|
@@ -616,18 +500,18 @@ function startCallbackServer2(expectedState) {
|
|
|
616
500
|
res.writeHead(400).end("Authentication failed.");
|
|
617
501
|
return;
|
|
618
502
|
}
|
|
619
|
-
res.writeHead(200, { "Content-Type": "text/html" }).end(
|
|
503
|
+
res.writeHead(200, { "Content-Type": "text/html" }).end(SUCCESS_HTML);
|
|
620
504
|
result = { code };
|
|
621
505
|
});
|
|
622
506
|
server.on("error", reject);
|
|
623
|
-
server.listen(
|
|
507
|
+
server.listen(CALLBACK_PORT, CALLBACK_HOST, () => {
|
|
624
508
|
resolve2({
|
|
625
509
|
server,
|
|
626
510
|
cancel: () => {
|
|
627
511
|
cancelled = true;
|
|
628
512
|
},
|
|
629
513
|
waitForCode: async () => {
|
|
630
|
-
const deadline = Date.now() +
|
|
514
|
+
const deadline = Date.now() + LOGIN_TIMEOUT_MS;
|
|
631
515
|
while (!result && !cancelled && Date.now() < deadline) {
|
|
632
516
|
await new Promise((r) => setTimeout(r, 100));
|
|
633
517
|
}
|
|
@@ -637,8 +521,8 @@ function startCallbackServer2(expectedState) {
|
|
|
637
521
|
});
|
|
638
522
|
});
|
|
639
523
|
}
|
|
640
|
-
async function
|
|
641
|
-
const res = await fetch(
|
|
524
|
+
async function postTokenRequest(body, label) {
|
|
525
|
+
const res = await fetch(TOKEN_URL, {
|
|
642
526
|
method: "POST",
|
|
643
527
|
headers: {
|
|
644
528
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
@@ -657,32 +541,32 @@ async function postTokenRequest2(body, label) {
|
|
|
657
541
|
expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000
|
|
658
542
|
};
|
|
659
543
|
}
|
|
660
|
-
function
|
|
661
|
-
return
|
|
544
|
+
function exchangeCode(code, verifier) {
|
|
545
|
+
return postTokenRequest(new URLSearchParams({
|
|
662
546
|
grant_type: "authorization_code",
|
|
663
|
-
client_id:
|
|
547
|
+
client_id: CLIENT_ID,
|
|
664
548
|
code,
|
|
665
549
|
code_verifier: verifier,
|
|
666
|
-
redirect_uri:
|
|
550
|
+
redirect_uri: REDIRECT_URI
|
|
667
551
|
}), "Token exchange");
|
|
668
552
|
}
|
|
669
553
|
function refreshOpenAIToken(refreshToken) {
|
|
670
|
-
return
|
|
554
|
+
return postTokenRequest(new URLSearchParams({
|
|
671
555
|
grant_type: "refresh_token",
|
|
672
556
|
refresh_token: refreshToken,
|
|
673
|
-
client_id:
|
|
557
|
+
client_id: CLIENT_ID
|
|
674
558
|
}), "Token refresh");
|
|
675
559
|
}
|
|
676
560
|
async function loginOpenAI(callbacks) {
|
|
677
561
|
const { verifier, challenge } = await generatePKCE();
|
|
678
562
|
const state = createState();
|
|
679
|
-
const cb = await
|
|
563
|
+
const cb = await startCallbackServer(state);
|
|
680
564
|
try {
|
|
681
565
|
const params = new URLSearchParams({
|
|
682
566
|
response_type: "code",
|
|
683
|
-
client_id:
|
|
684
|
-
redirect_uri:
|
|
685
|
-
scope:
|
|
567
|
+
client_id: CLIENT_ID,
|
|
568
|
+
redirect_uri: REDIRECT_URI,
|
|
569
|
+
scope: SCOPES,
|
|
686
570
|
code_challenge: challenge,
|
|
687
571
|
code_challenge_method: "S256",
|
|
688
572
|
state,
|
|
@@ -690,12 +574,12 @@ async function loginOpenAI(callbacks) {
|
|
|
690
574
|
codex_cli_simplified_flow: "true",
|
|
691
575
|
originator: "mc"
|
|
692
576
|
});
|
|
693
|
-
callbacks.onOpenUrl(`${
|
|
577
|
+
callbacks.onOpenUrl(`${AUTHORIZE_URL}?${params}`, "Complete login in your browser.");
|
|
694
578
|
const result = await cb.waitForCode();
|
|
695
579
|
if (!result)
|
|
696
580
|
throw new Error("Login cancelled or no code received");
|
|
697
581
|
callbacks.onProgress("Exchanging authorization code for tokens\u2026");
|
|
698
|
-
return
|
|
582
|
+
return exchangeCode(result.code, verifier);
|
|
699
583
|
} finally {
|
|
700
584
|
cb.server.close();
|
|
701
585
|
}
|
|
@@ -722,7 +606,6 @@ var openaiOAuth = {
|
|
|
722
606
|
|
|
723
607
|
// src/session/oauth/auth-storage.ts
|
|
724
608
|
var PROVIDERS = new Map([
|
|
725
|
-
[anthropicOAuth.id, anthropicOAuth],
|
|
726
609
|
[openaiOAuth.id, openaiOAuth]
|
|
727
610
|
]);
|
|
728
611
|
function getOAuthProviders() {
|
|
@@ -752,10 +635,10 @@ function isAuthError(err) {
|
|
|
752
635
|
return /\b(401|403)\b/.test(err.message);
|
|
753
636
|
}
|
|
754
637
|
function isLoggedIn(provider) {
|
|
755
|
-
return getStoredToken(provider) !== null;
|
|
638
|
+
return PROVIDERS.has(provider) && getStoredToken(provider) !== null;
|
|
756
639
|
}
|
|
757
640
|
function listLoggedInProviders() {
|
|
758
|
-
return getDb().query("SELECT provider FROM oauth_tokens ORDER BY provider").all().map((r) => r.provider);
|
|
641
|
+
return getDb().query("SELECT provider FROM oauth_tokens ORDER BY provider").all().map((r) => r.provider).filter((provider) => PROVIDERS.has(provider));
|
|
759
642
|
}
|
|
760
643
|
async function login(providerId, callbacks) {
|
|
761
644
|
const provider = PROVIDERS.get(providerId);
|
|
@@ -1321,31 +1204,16 @@ async function fetchCodexOAuthModels(token) {
|
|
|
1321
1204
|
return null;
|
|
1322
1205
|
}
|
|
1323
1206
|
}
|
|
1324
|
-
async function getAnthropicAuth() {
|
|
1325
|
-
const envKey = process.env.ANTHROPIC_API_KEY;
|
|
1326
|
-
if (envKey)
|
|
1327
|
-
return { key: envKey, oauth: false };
|
|
1328
|
-
if (isLoggedIn("anthropic")) {
|
|
1329
|
-
const token = await getAccessToken("anthropic");
|
|
1330
|
-
if (token)
|
|
1331
|
-
return { key: token, oauth: true };
|
|
1332
|
-
}
|
|
1333
|
-
return null;
|
|
1334
|
-
}
|
|
1335
1207
|
async function fetchAnthropicModels() {
|
|
1336
|
-
const
|
|
1337
|
-
if (!
|
|
1208
|
+
const key = process.env.ANTHROPIC_API_KEY;
|
|
1209
|
+
if (!key)
|
|
1338
1210
|
return null;
|
|
1339
|
-
const
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
} else {
|
|
1346
|
-
headers["x-api-key"] = auth.key;
|
|
1347
|
-
}
|
|
1348
|
-
const payload = await fetchJson(`${ANTHROPIC_BASE}/v1/models`, { headers }, 6000);
|
|
1211
|
+
const payload = await fetchJson(`${ANTHROPIC_BASE}/v1/models`, {
|
|
1212
|
+
headers: {
|
|
1213
|
+
"anthropic-version": "2023-06-01",
|
|
1214
|
+
"x-api-key": key
|
|
1215
|
+
}
|
|
1216
|
+
}, 6000);
|
|
1349
1217
|
return processModelsList(payload, "data", "id", (item, modelId) => {
|
|
1350
1218
|
const displayName = typeof item.display_name === "string" && item.display_name.trim().length > 0 ? item.display_name : modelId;
|
|
1351
1219
|
return {
|
|
@@ -1452,10 +1320,8 @@ function hasAnyEnvKey(env, keys) {
|
|
|
1452
1320
|
}
|
|
1453
1321
|
function getRemoteProvidersFromEnv(env) {
|
|
1454
1322
|
const providers = REMOTE_PROVIDER_ENV_KEYS.filter((entry) => hasAnyEnvKey(env, entry.envKeys)).map((entry) => entry.provider);
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
providers.push(p);
|
|
1458
|
-
}
|
|
1323
|
+
if (!providers.includes("openai") && isLoggedIn("openai")) {
|
|
1324
|
+
providers.push("openai");
|
|
1459
1325
|
}
|
|
1460
1326
|
return providers;
|
|
1461
1327
|
}
|
|
@@ -1660,27 +1526,6 @@ var SUPPORTED_PROVIDERS = [
|
|
|
1660
1526
|
"ollama"
|
|
1661
1527
|
];
|
|
1662
1528
|
var ZEN_BASE2 = "https://opencode.ai/zen/v1";
|
|
1663
|
-
var OAUTH_STRIP_BETAS = new Set;
|
|
1664
|
-
function createOAuthFetch(accessToken) {
|
|
1665
|
-
const oauthFetch = async (input, init) => {
|
|
1666
|
-
let opts = init;
|
|
1667
|
-
if (opts?.headers) {
|
|
1668
|
-
const h = new Headers(opts.headers);
|
|
1669
|
-
const beta = h.get("anthropic-beta");
|
|
1670
|
-
if (beta) {
|
|
1671
|
-
const filtered = beta.split(",").filter((b) => !OAUTH_STRIP_BETAS.has(b)).join(",");
|
|
1672
|
-
h.set("anthropic-beta", filtered);
|
|
1673
|
-
}
|
|
1674
|
-
h.delete("x-api-key");
|
|
1675
|
-
h.set("authorization", `Bearer ${accessToken}`);
|
|
1676
|
-
h.set("user-agent", "claude-cli/2.1.75");
|
|
1677
|
-
h.set("x-app", "cli");
|
|
1678
|
-
opts = { ...opts, headers: Object.fromEntries(h.entries()) };
|
|
1679
|
-
}
|
|
1680
|
-
return fetch(input, opts);
|
|
1681
|
-
};
|
|
1682
|
-
return oauthFetch;
|
|
1683
|
-
}
|
|
1684
1529
|
function requireEnv(name) {
|
|
1685
1530
|
const value = process.env[name];
|
|
1686
1531
|
if (!value)
|
|
@@ -1815,30 +1660,8 @@ async function resolveOpenAIModel(modelId) {
|
|
|
1815
1660
|
return modelId.startsWith("gpt-") ? directProviders.openai().responses(modelId) : directProviders.openai()(modelId);
|
|
1816
1661
|
}
|
|
1817
1662
|
var OPENAI_CODEX_BASE_URL = "https://chatgpt.com/backend-api/codex";
|
|
1818
|
-
var oauthAnthropicCache = null;
|
|
1819
1663
|
var oauthOpenAICache = null;
|
|
1820
|
-
function createOAuthAnthropicProvider(token) {
|
|
1821
|
-
return createAnthropic({
|
|
1822
|
-
apiKey: "",
|
|
1823
|
-
fetch: createOAuthFetch(token),
|
|
1824
|
-
headers: {
|
|
1825
|
-
"anthropic-beta": "claude-code-20250219,oauth-2025-04-20"
|
|
1826
|
-
}
|
|
1827
|
-
});
|
|
1828
|
-
}
|
|
1829
1664
|
async function resolveAnthropicModel(modelId) {
|
|
1830
|
-
if (isLoggedIn("anthropic")) {
|
|
1831
|
-
const token = await getAccessToken("anthropic");
|
|
1832
|
-
if (token) {
|
|
1833
|
-
if (!oauthAnthropicCache || oauthAnthropicCache.token !== token) {
|
|
1834
|
-
oauthAnthropicCache = {
|
|
1835
|
-
token,
|
|
1836
|
-
provider: createOAuthAnthropicProvider(token)
|
|
1837
|
-
};
|
|
1838
|
-
}
|
|
1839
|
-
return oauthAnthropicCache.provider(modelId);
|
|
1840
|
-
}
|
|
1841
|
-
}
|
|
1842
1665
|
return directProviders.anthropic()(modelId);
|
|
1843
1666
|
}
|
|
1844
1667
|
var PROVIDER_MODEL_RESOLVERS = {
|
|
@@ -1863,16 +1686,11 @@ async function resolveModel(modelString) {
|
|
|
1863
1686
|
}
|
|
1864
1687
|
return PROVIDER_MODEL_RESOLVERS[provider](modelId);
|
|
1865
1688
|
}
|
|
1866
|
-
function isAnthropicOAuth() {
|
|
1867
|
-
return isLoggedIn("anthropic");
|
|
1868
|
-
}
|
|
1869
1689
|
function discoverConnectedProviders() {
|
|
1870
1690
|
const result = [];
|
|
1871
1691
|
if (process.env.OPENCODE_API_KEY)
|
|
1872
1692
|
result.push({ name: "zen", via: "env" });
|
|
1873
|
-
if (
|
|
1874
|
-
result.push({ name: "anthropic", via: "oauth" });
|
|
1875
|
-
else if (process.env.ANTHROPIC_API_KEY)
|
|
1693
|
+
if (process.env.ANTHROPIC_API_KEY)
|
|
1876
1694
|
result.push({ name: "anthropic", via: "env" });
|
|
1877
1695
|
if (isLoggedIn("openai"))
|
|
1878
1696
|
result.push({ name: "openai", via: "oauth" });
|
|
@@ -1887,7 +1705,7 @@ function discoverConnectedProviders() {
|
|
|
1887
1705
|
function autoDiscoverModel() {
|
|
1888
1706
|
if (process.env.OPENCODE_API_KEY)
|
|
1889
1707
|
return "zen/claude-sonnet-4-6";
|
|
1890
|
-
if (process.env.ANTHROPIC_API_KEY
|
|
1708
|
+
if (process.env.ANTHROPIC_API_KEY)
|
|
1891
1709
|
return "anthropic/claude-sonnet-4-6";
|
|
1892
1710
|
if (process.env.OPENAI_API_KEY || isLoggedIn("openai"))
|
|
1893
1711
|
return "openai/gpt-5.4";
|
|
@@ -3061,6 +2879,10 @@ function renderReadSkillResult(result, _opts) {
|
|
|
3061
2879
|
if (!r || typeof r !== "object")
|
|
3062
2880
|
return false;
|
|
3063
2881
|
if (!r.skill) {
|
|
2882
|
+
if (typeof r.note === "string" && r.note.trim().length > 0) {
|
|
2883
|
+
writeln(`${G.info} ${c4.dim("skill")} ${c4.dim(r.note.trim())}`);
|
|
2884
|
+
return true;
|
|
2885
|
+
}
|
|
3064
2886
|
writeln(`${G.info} ${c4.dim("skill")} ${c4.dim("(not found)")}`);
|
|
3065
2887
|
return true;
|
|
3066
2888
|
}
|
|
@@ -3068,6 +2890,9 @@ function renderReadSkillResult(result, _opts) {
|
|
|
3068
2890
|
label: "skill",
|
|
3069
2891
|
verboseOutput: _opts?.verboseOutput === true
|
|
3070
2892
|
});
|
|
2893
|
+
if (typeof r.note === "string" && r.note.trim().length > 0) {
|
|
2894
|
+
writeln(c4.dim(r.note.trim()));
|
|
2895
|
+
}
|
|
3071
2896
|
return true;
|
|
3072
2897
|
}
|
|
3073
2898
|
function renderWebSearchResult(result, opts) {
|
|
@@ -5062,11 +4887,34 @@ function getMessageDiagnostics(messages) {
|
|
|
5062
4887
|
}
|
|
5063
4888
|
};
|
|
5064
4889
|
}
|
|
4890
|
+
function collectPrunableToolNames(messages) {
|
|
4891
|
+
const names = new Set;
|
|
4892
|
+
for (const message of messages) {
|
|
4893
|
+
if (!Array.isArray(message.content))
|
|
4894
|
+
continue;
|
|
4895
|
+
for (const part of message.content) {
|
|
4896
|
+
if (!isRecord(part))
|
|
4897
|
+
continue;
|
|
4898
|
+
const partRecord = part;
|
|
4899
|
+
const toolName = partRecord.toolName;
|
|
4900
|
+
if (typeof toolName !== "string" || toolName.length === 0)
|
|
4901
|
+
continue;
|
|
4902
|
+
if (toolName === "readSkill")
|
|
4903
|
+
continue;
|
|
4904
|
+
names.add(toolName);
|
|
4905
|
+
}
|
|
4906
|
+
}
|
|
4907
|
+
return [...names].sort((a, b) => a.localeCompare(b));
|
|
4908
|
+
}
|
|
4909
|
+
function buildToolCallPruning(messages, type) {
|
|
4910
|
+
const tools = collectPrunableToolNames(messages);
|
|
4911
|
+
return tools.length === 0 ? "none" : [{ type, tools }];
|
|
4912
|
+
}
|
|
5065
4913
|
function applyContextPruning(messages) {
|
|
5066
4914
|
return pruneMessages({
|
|
5067
4915
|
messages,
|
|
5068
4916
|
reasoning: "before-last-message",
|
|
5069
|
-
toolCalls: "before-last-40-messages",
|
|
4917
|
+
toolCalls: buildToolCallPruning(messages, "before-last-40-messages"),
|
|
5070
4918
|
emptyMessages: "remove"
|
|
5071
4919
|
});
|
|
5072
4920
|
}
|
|
@@ -5075,7 +4923,7 @@ function applyStepPruning(messages, initialMessageCount) {
|
|
|
5075
4923
|
return pruneMessages({
|
|
5076
4924
|
messages,
|
|
5077
4925
|
reasoning: "none",
|
|
5078
|
-
toolCalls: `before-last-${40 + newMessageCount}-messages
|
|
4926
|
+
toolCalls: buildToolCallPruning(messages, `before-last-${40 + newMessageCount}-messages`),
|
|
5079
4927
|
emptyMessages: "remove"
|
|
5080
4928
|
});
|
|
5081
4929
|
}
|
|
@@ -5256,26 +5104,10 @@ function prepareTurnMessages(input) {
|
|
|
5256
5104
|
diagnostics: getMessageDiagnostics(compacted)
|
|
5257
5105
|
});
|
|
5258
5106
|
}
|
|
5259
|
-
let finalMessages = compacted;
|
|
5260
|
-
let finalSystemPrompt = systemPrompt;
|
|
5261
|
-
if (isAnthropicModelFamily(modelString) && isAnthropicOAuth()) {
|
|
5262
|
-
const ccIdentity = "You are Claude Code, Anthropic's official CLI for Claude.";
|
|
5263
|
-
const systemMessages = [
|
|
5264
|
-
{ role: "system", content: ccIdentity }
|
|
5265
|
-
];
|
|
5266
|
-
if (finalSystemPrompt) {
|
|
5267
|
-
systemMessages.push({
|
|
5268
|
-
role: "system",
|
|
5269
|
-
content: finalSystemPrompt
|
|
5270
|
-
});
|
|
5271
|
-
finalSystemPrompt = undefined;
|
|
5272
|
-
}
|
|
5273
|
-
finalMessages = [...systemMessages, ...finalMessages];
|
|
5274
|
-
}
|
|
5275
5107
|
const wasPruned = postStats.messageCount < preStats.messageCount;
|
|
5276
5108
|
return {
|
|
5277
|
-
messages:
|
|
5278
|
-
systemPrompt
|
|
5109
|
+
messages: compacted,
|
|
5110
|
+
systemPrompt,
|
|
5279
5111
|
pruned: wasPruned,
|
|
5280
5112
|
prePruneMessageCount: preStats.messageCount,
|
|
5281
5113
|
prePruneTotalBytes: preStats.totalBytes,
|
|
@@ -5725,6 +5557,12 @@ Guidelines:
|
|
|
5725
5557
|
- Make parallel tool calls when the lookups are independent \u2014 this speeds up multi-file investigation.
|
|
5726
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.
|
|
5727
5559
|
- Keep it simple: DRY, KISS, YAGNI. Avoid unnecessary complexity.
|
|
5560
|
+
- Apply Rob Pike's 5 Rules of Programming:
|
|
5561
|
+
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
|
+
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
|
+
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
|
+
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
|
+
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.
|
|
5728
5566
|
|
|
5729
5567
|
# File editing with mc-edit
|
|
5730
5568
|
\`mc-edit\` applies one exact-text replacement per invocation. It fails deterministically if the old text is missing or matches more than once.
|
|
@@ -6097,7 +5935,7 @@ var ShellSchema = z3.object({
|
|
|
6097
5935
|
timeout: z3.number().int().min(1000).nullable().describe("Timeout in milliseconds. If omitted, the command runs until it exits."),
|
|
6098
5936
|
env: z3.record(z3.string(), z3.string()).nullable().describe("Additional environment variables to set")
|
|
6099
5937
|
});
|
|
6100
|
-
var MAX_OUTPUT_BYTES =
|
|
5938
|
+
var MAX_OUTPUT_BYTES = 24000;
|
|
6101
5939
|
async function runShellCommand(input, options) {
|
|
6102
5940
|
const cwd = input.cwd ?? process.cwd();
|
|
6103
5941
|
const timeout = input.timeout ?? undefined;
|
|
@@ -6415,51 +6253,8 @@ ${c12.bold("Examples:")}`);
|
|
|
6415
6253
|
writeln(` mc -l ${c12.dim("# list sessions")}`);
|
|
6416
6254
|
}
|
|
6417
6255
|
|
|
6418
|
-
// src/cli/bootstrap.ts
|
|
6419
|
-
import { existsSync as existsSync5, mkdirSync as mkdirSync2, writeFileSync } from "fs";
|
|
6420
|
-
import { homedir as homedir6 } from "os";
|
|
6421
|
-
import { join as join7 } from "path";
|
|
6422
|
-
import * as c13 from "yoctocolors";
|
|
6423
|
-
var REVIEW_SKILL_CONTENT = `---
|
|
6424
|
-
name: review
|
|
6425
|
-
description: "Review recent changes for correctness, code quality, and performance. Use when the user asks to review, check, or audit recent code changes, diffs, or pull requests."
|
|
6426
|
-
context: fork
|
|
6427
|
-
---
|
|
6428
|
-
|
|
6429
|
-
Review recent changes and provide actionable feedback.
|
|
6430
|
-
|
|
6431
|
-
## Steps
|
|
6432
|
-
|
|
6433
|
-
1. Identify the changes to review \u2014 check \`git diff\`, \`git log\`, and staged files.
|
|
6434
|
-
2. Read the changed files and understand the intent behind each change.
|
|
6435
|
-
3. Evaluate each change against the criteria below.
|
|
6436
|
-
4. Output a concise summary with only the issues found. If nothing is wrong, say so.
|
|
6437
|
-
|
|
6438
|
-
## Review criteria
|
|
6439
|
-
|
|
6440
|
-
- **Correctness** \u2014 Are the changes aligned with their stated goal? Do they introduce bugs or regressions?
|
|
6441
|
-
- **Code quality** \u2014 Is there duplicate, dead, or overly complex code? Are abstractions appropriate?
|
|
6442
|
-
- **Performance** \u2014 Are there unnecessary allocations, redundant I/O, or algorithmic concerns?
|
|
6443
|
-
- **Edge cases** \u2014 Are boundary conditions and error paths handled?
|
|
6444
|
-
|
|
6445
|
-
## Guidelines
|
|
6446
|
-
|
|
6447
|
-
- Never flag style choices as bugs \u2014 don't be a zealot.
|
|
6448
|
-
- Never flag false positives \u2014 verify before raising an issue.
|
|
6449
|
-
- Keep feedback actionable: say what's wrong and suggest a fix.
|
|
6450
|
-
`;
|
|
6451
|
-
function bootstrapGlobalDefaults() {
|
|
6452
|
-
const skillDir = join7(homedir6(), ".agents", "skills", "review");
|
|
6453
|
-
const skillPath = join7(skillDir, "SKILL.md");
|
|
6454
|
-
if (!existsSync5(skillPath)) {
|
|
6455
|
-
mkdirSync2(skillDir, { recursive: true });
|
|
6456
|
-
writeFileSync(skillPath, REVIEW_SKILL_CONTENT, "utf-8");
|
|
6457
|
-
writeln(`${c13.green("\u2713")} created ${c13.dim("~/.agents/skills/review/SKILL.md")} ${c13.dim("(edit it to customise your reviews)")}`);
|
|
6458
|
-
}
|
|
6459
|
-
}
|
|
6460
|
-
|
|
6461
6256
|
// src/cli/file-refs.ts
|
|
6462
|
-
import { join as
|
|
6257
|
+
import { join as join7 } from "path";
|
|
6463
6258
|
async function resolveFileRefs(text, cwd) {
|
|
6464
6259
|
const atPattern = /@([\w./\-_]+)/g;
|
|
6465
6260
|
let result = text;
|
|
@@ -6469,7 +6264,7 @@ async function resolveFileRefs(text, cwd) {
|
|
|
6469
6264
|
const ref = match[1];
|
|
6470
6265
|
if (!ref)
|
|
6471
6266
|
continue;
|
|
6472
|
-
const filePath = ref.startsWith("/") ? ref :
|
|
6267
|
+
const filePath = ref.startsWith("/") ? ref : join7(cwd, ref);
|
|
6473
6268
|
if (isImageFilename(ref)) {
|
|
6474
6269
|
const attachment = await loadImageFile(filePath);
|
|
6475
6270
|
if (attachment) {
|
|
@@ -6491,25 +6286,25 @@ ${content}
|
|
|
6491
6286
|
}
|
|
6492
6287
|
|
|
6493
6288
|
// src/cli/input-loop.ts
|
|
6494
|
-
import * as
|
|
6289
|
+
import * as c19 from "yoctocolors";
|
|
6495
6290
|
|
|
6496
6291
|
// src/cli/commands.ts
|
|
6497
6292
|
import { randomBytes } from "crypto";
|
|
6498
|
-
import { unlinkSync
|
|
6293
|
+
import { unlinkSync, writeFileSync } from "fs";
|
|
6499
6294
|
import { tmpdir } from "os";
|
|
6500
|
-
import { join as
|
|
6501
|
-
import * as
|
|
6295
|
+
import { join as join8 } from "path";
|
|
6296
|
+
import * as c18 from "yoctocolors";
|
|
6502
6297
|
|
|
6503
6298
|
// src/cli/commands-help.ts
|
|
6504
|
-
import * as
|
|
6299
|
+
import * as c13 from "yoctocolors";
|
|
6505
6300
|
function renderEntries(entries) {
|
|
6506
6301
|
for (const [label, description] of entries) {
|
|
6507
|
-
writeln(` ${
|
|
6302
|
+
writeln(` ${c13.cyan(label.padEnd(28))} ${c13.dim(description)}`);
|
|
6508
6303
|
}
|
|
6509
6304
|
}
|
|
6510
6305
|
function renderHelpCommand(ctx) {
|
|
6511
6306
|
writeln();
|
|
6512
|
-
writeln(` ${
|
|
6307
|
+
writeln(` ${c13.dim("session")}`);
|
|
6513
6308
|
renderEntries([
|
|
6514
6309
|
["/session [id]", "list sessions or switch to one"],
|
|
6515
6310
|
["/new", "start a fresh session"],
|
|
@@ -6517,7 +6312,7 @@ function renderHelpCommand(ctx) {
|
|
|
6517
6312
|
["/exit", "quit"]
|
|
6518
6313
|
]);
|
|
6519
6314
|
writeln();
|
|
6520
|
-
writeln(` ${
|
|
6315
|
+
writeln(` ${c13.dim("model + context")}`);
|
|
6521
6316
|
renderEntries([
|
|
6522
6317
|
["/model [id]", "list or switch models"],
|
|
6523
6318
|
["/reasoning [on|off]", "toggle reasoning display"],
|
|
@@ -6525,12 +6320,12 @@ function renderHelpCommand(ctx) {
|
|
|
6525
6320
|
["/mcp list", "list MCP servers"],
|
|
6526
6321
|
["/mcp add <n> <t> [u]", "add an MCP server"],
|
|
6527
6322
|
["/mcp remove <name>", "remove an MCP server"],
|
|
6528
|
-
["/login [provider]", "login via OAuth (e.g.
|
|
6323
|
+
["/login [provider]", "login via OAuth (e.g. openai)"],
|
|
6529
6324
|
["/logout <provider>", "clear OAuth tokens"],
|
|
6530
6325
|
["/help", "show this help"]
|
|
6531
6326
|
]);
|
|
6532
6327
|
writeln();
|
|
6533
|
-
writeln(` ${
|
|
6328
|
+
writeln(` ${c13.dim("prompt")}`);
|
|
6534
6329
|
renderEntries([
|
|
6535
6330
|
["ask normally", "send a prompt to the current agent"],
|
|
6536
6331
|
["!cmd", "run a shell command and keep the result in context"],
|
|
@@ -6540,44 +6335,44 @@ function renderHelpCommand(ctx) {
|
|
|
6540
6335
|
const skills = loadSkillsIndex(ctx.cwd);
|
|
6541
6336
|
if (skills.size > 0) {
|
|
6542
6337
|
writeln();
|
|
6543
|
-
writeln(` ${
|
|
6338
|
+
writeln(` ${c13.dim("skills")}`);
|
|
6544
6339
|
for (const skill of skills.values()) {
|
|
6545
|
-
const source = skill.source === "local" ?
|
|
6340
|
+
const source = skill.source === "local" ? c13.dim("local") : c13.dim("global");
|
|
6546
6341
|
const desc = truncateText(skill.description, 80);
|
|
6547
|
-
writeln(` ${
|
|
6342
|
+
writeln(` ${c13.green(`/${skill.name}`.padEnd(28))} ${c13.dim(desc)} ${c13.dim("\xB7")} ${source}`);
|
|
6548
6343
|
}
|
|
6549
6344
|
}
|
|
6550
6345
|
writeln();
|
|
6551
|
-
writeln(` ${
|
|
6346
|
+
writeln(` ${c13.dim("keys")} ${c13.dim("esc")} cancel response ${c13.dim("\xB7")} ${c13.dim("ctrl+c / ctrl+d")} exit ${c13.dim("\xB7")} ${c13.dim("ctrl+r")} history search ${c13.dim("\xB7")} ${c13.dim("\u2191\u2193")} history`);
|
|
6552
6347
|
writeln();
|
|
6553
6348
|
}
|
|
6554
6349
|
|
|
6555
6350
|
// src/cli/commands-login.ts
|
|
6556
|
-
import * as
|
|
6351
|
+
import * as c14 from "yoctocolors";
|
|
6557
6352
|
function renderLoginHelp() {
|
|
6558
6353
|
writeln();
|
|
6559
|
-
writeln(` ${
|
|
6560
|
-
writeln(` /login ${
|
|
6561
|
-
writeln(` /login <provider> ${
|
|
6562
|
-
writeln(` /logout <provider> ${
|
|
6354
|
+
writeln(` ${c14.dim("usage:")}`);
|
|
6355
|
+
writeln(` /login ${c14.dim("show login status")}`);
|
|
6356
|
+
writeln(` /login <provider> ${c14.dim("login via OAuth")}`);
|
|
6357
|
+
writeln(` /logout <provider> ${c14.dim("clear saved tokens")}`);
|
|
6563
6358
|
writeln();
|
|
6564
|
-
writeln(` ${
|
|
6359
|
+
writeln(` ${c14.dim("providers:")}`);
|
|
6565
6360
|
for (const p of getOAuthProviders()) {
|
|
6566
|
-
const status = isLoggedIn(p.id) ?
|
|
6567
|
-
writeln(` ${
|
|
6361
|
+
const status = isLoggedIn(p.id) ? c14.green("logged in") : c14.dim("not logged in");
|
|
6362
|
+
writeln(` ${c14.cyan(p.id.padEnd(20))} ${p.name} ${c14.dim("\xB7")} ${status}`);
|
|
6568
6363
|
}
|
|
6569
6364
|
writeln();
|
|
6570
6365
|
}
|
|
6571
6366
|
function renderStatus() {
|
|
6572
6367
|
const loggedIn = listLoggedInProviders();
|
|
6573
6368
|
if (loggedIn.length === 0) {
|
|
6574
|
-
writeln(`${PREFIX.info} ${
|
|
6369
|
+
writeln(`${PREFIX.info} ${c14.dim("no OAuth logins \u2014 use")} /login <provider>`);
|
|
6575
6370
|
return;
|
|
6576
6371
|
}
|
|
6577
6372
|
for (const id of loggedIn) {
|
|
6578
6373
|
const provider = getOAuthProvider(id);
|
|
6579
6374
|
const name = provider?.name ?? id;
|
|
6580
|
-
writeln(`${PREFIX.success} ${
|
|
6375
|
+
writeln(`${PREFIX.success} ${c14.cyan(id)} ${c14.dim(name)}`);
|
|
6581
6376
|
}
|
|
6582
6377
|
}
|
|
6583
6378
|
async function handleLoginCommand(ctx, args) {
|
|
@@ -6598,7 +6393,7 @@ async function handleLoginCommand(ctx, args) {
|
|
|
6598
6393
|
if (isLoggedIn(providerId)) {
|
|
6599
6394
|
const token = await getAccessToken(providerId);
|
|
6600
6395
|
if (token) {
|
|
6601
|
-
writeln(`${PREFIX.success} already logged in to ${
|
|
6396
|
+
writeln(`${PREFIX.success} already logged in to ${c14.cyan(provider.name)}`);
|
|
6602
6397
|
return;
|
|
6603
6398
|
}
|
|
6604
6399
|
}
|
|
@@ -6609,7 +6404,7 @@ async function handleLoginCommand(ctx, args) {
|
|
|
6609
6404
|
ctx.stopSpinner();
|
|
6610
6405
|
writeln(`${PREFIX.info} ${instructions}`);
|
|
6611
6406
|
writeln();
|
|
6612
|
-
writeln(` ${
|
|
6407
|
+
writeln(` ${c14.cyan(url)}`);
|
|
6613
6408
|
writeln();
|
|
6614
6409
|
let open = "xdg-open";
|
|
6615
6410
|
if (process.platform === "darwin")
|
|
@@ -6621,12 +6416,12 @@ async function handleLoginCommand(ctx, args) {
|
|
|
6621
6416
|
},
|
|
6622
6417
|
onProgress: (msg) => {
|
|
6623
6418
|
ctx.stopSpinner();
|
|
6624
|
-
writeln(`${PREFIX.info} ${
|
|
6419
|
+
writeln(`${PREFIX.info} ${c14.dim(msg)}`);
|
|
6625
6420
|
ctx.startSpinner("exchanging tokens");
|
|
6626
6421
|
}
|
|
6627
6422
|
});
|
|
6628
6423
|
ctx.stopSpinner();
|
|
6629
|
-
writeln(`${PREFIX.success} logged in to ${
|
|
6424
|
+
writeln(`${PREFIX.success} logged in to ${c14.cyan(provider.name)}`);
|
|
6630
6425
|
} catch (err) {
|
|
6631
6426
|
ctx.stopSpinner();
|
|
6632
6427
|
writeln(`${PREFIX.error} login failed: ${err.message}`);
|
|
@@ -6638,16 +6433,21 @@ function handleLogoutCommand(_ctx, args) {
|
|
|
6638
6433
|
writeln(`${PREFIX.error} usage: /logout <provider>`);
|
|
6639
6434
|
return;
|
|
6640
6435
|
}
|
|
6436
|
+
const provider = getOAuthProvider(providerId);
|
|
6437
|
+
if (!provider) {
|
|
6438
|
+
writeln(`${PREFIX.error} unknown provider "${providerId}" \u2014 available: ${getOAuthProviders().map((p) => p.id).join(", ")}`);
|
|
6439
|
+
return;
|
|
6440
|
+
}
|
|
6641
6441
|
if (!isLoggedIn(providerId)) {
|
|
6642
|
-
writeln(`${PREFIX.info} ${
|
|
6442
|
+
writeln(`${PREFIX.info} ${c14.dim("not logged in to")} ${providerId}`);
|
|
6643
6443
|
return;
|
|
6644
6444
|
}
|
|
6645
6445
|
logout(providerId);
|
|
6646
|
-
writeln(`${PREFIX.success} logged out of ${
|
|
6446
|
+
writeln(`${PREFIX.success} logged out of ${c14.cyan(provider.id)}`);
|
|
6647
6447
|
}
|
|
6648
6448
|
|
|
6649
6449
|
// src/cli/commands-mcp.ts
|
|
6650
|
-
import * as
|
|
6450
|
+
import * as c15 from "yoctocolors";
|
|
6651
6451
|
async function handleMcpCommand(ctx, args) {
|
|
6652
6452
|
const parts = args.trim().split(/\s+/);
|
|
6653
6453
|
const sub = parts[0] ?? "list";
|
|
@@ -6655,15 +6455,15 @@ async function handleMcpCommand(ctx, args) {
|
|
|
6655
6455
|
case "list": {
|
|
6656
6456
|
const servers = listMcpServers();
|
|
6657
6457
|
if (servers.length === 0) {
|
|
6658
|
-
writeln(
|
|
6659
|
-
writeln(
|
|
6458
|
+
writeln(c15.dim(" no MCP servers configured"));
|
|
6459
|
+
writeln(c15.dim(" /mcp add <name> http <url> \xB7 /mcp add <name> stdio <cmd> [args...]"));
|
|
6660
6460
|
return;
|
|
6661
6461
|
}
|
|
6662
6462
|
writeln();
|
|
6663
6463
|
for (const s of servers) {
|
|
6664
6464
|
const detailText = s.url ?? s.command ?? "";
|
|
6665
|
-
const detail = detailText ?
|
|
6666
|
-
writeln(` ${
|
|
6465
|
+
const detail = detailText ? c15.dim(` ${detailText}`) : "";
|
|
6466
|
+
writeln(` ${c15.yellow("\u2699")} ${c15.bold(s.name)} ${c15.dim(s.transport)}${detail}`);
|
|
6667
6467
|
}
|
|
6668
6468
|
return;
|
|
6669
6469
|
}
|
|
@@ -6708,9 +6508,9 @@ async function handleMcpCommand(ctx, args) {
|
|
|
6708
6508
|
}
|
|
6709
6509
|
try {
|
|
6710
6510
|
await ctx.connectMcpServer(name);
|
|
6711
|
-
writeln(`${PREFIX.success} mcp server ${
|
|
6511
|
+
writeln(`${PREFIX.success} mcp server ${c15.cyan(name)} added and connected`);
|
|
6712
6512
|
} catch (e) {
|
|
6713
|
-
writeln(`${PREFIX.success} mcp server ${
|
|
6513
|
+
writeln(`${PREFIX.success} mcp server ${c15.cyan(name)} saved ${c15.dim(`(connection failed: ${String(e)})`)}`);
|
|
6714
6514
|
}
|
|
6715
6515
|
return;
|
|
6716
6516
|
}
|
|
@@ -6722,17 +6522,17 @@ async function handleMcpCommand(ctx, args) {
|
|
|
6722
6522
|
return;
|
|
6723
6523
|
}
|
|
6724
6524
|
deleteMcpServer(name);
|
|
6725
|
-
writeln(`${PREFIX.success} mcp server ${
|
|
6525
|
+
writeln(`${PREFIX.success} mcp server ${c15.cyan(name)} removed`);
|
|
6726
6526
|
return;
|
|
6727
6527
|
}
|
|
6728
6528
|
default:
|
|
6729
6529
|
writeln(`${PREFIX.error} unknown: /mcp ${sub}`);
|
|
6730
|
-
writeln(
|
|
6530
|
+
writeln(c15.dim(" subcommands: list \xB7 add \xB7 remove"));
|
|
6731
6531
|
}
|
|
6732
6532
|
}
|
|
6733
6533
|
|
|
6734
6534
|
// src/cli/commands-model.ts
|
|
6735
|
-
import * as
|
|
6535
|
+
import * as c16 from "yoctocolors";
|
|
6736
6536
|
import { select } from "yoctoselect";
|
|
6737
6537
|
var THINKING_EFFORTS = ["low", "medium", "high", "xhigh"];
|
|
6738
6538
|
function parseThinkingEffort(value) {
|
|
@@ -6752,21 +6552,21 @@ function renderModelUpdatedMessage(ctx, modelId, effortArg) {
|
|
|
6752
6552
|
if (effortArg) {
|
|
6753
6553
|
if (effortArg === "off") {
|
|
6754
6554
|
ctx.setThinkingEffort(null);
|
|
6755
|
-
writeln(`${PREFIX.success} model \u2192 ${
|
|
6555
|
+
writeln(`${PREFIX.success} model \u2192 ${c16.cyan(modelId)} ${c16.dim("(thinking disabled)")}`);
|
|
6756
6556
|
return;
|
|
6757
6557
|
}
|
|
6758
6558
|
const effort = parseThinkingEffort(effortArg);
|
|
6759
6559
|
if (effort) {
|
|
6760
6560
|
ctx.setThinkingEffort(effort);
|
|
6761
|
-
writeln(`${PREFIX.success} model \u2192 ${
|
|
6561
|
+
writeln(`${PREFIX.success} model \u2192 ${c16.cyan(modelId)} ${c16.dim(`(\u2726 ${effort})`)}`);
|
|
6762
6562
|
return;
|
|
6763
6563
|
}
|
|
6764
|
-
writeln(`${PREFIX.success} model \u2192 ${
|
|
6765
|
-
writeln(`${PREFIX.error} unknown effort level ${
|
|
6564
|
+
writeln(`${PREFIX.success} model \u2192 ${c16.cyan(modelId)}`);
|
|
6565
|
+
writeln(`${PREFIX.error} unknown effort level ${c16.cyan(effortArg)} (use low, medium, high, xhigh, off)`);
|
|
6766
6566
|
return;
|
|
6767
6567
|
}
|
|
6768
|
-
const effortTag = ctx.thinkingEffort ?
|
|
6769
|
-
writeln(`${PREFIX.success} model \u2192 ${
|
|
6568
|
+
const effortTag = ctx.thinkingEffort ? c16.dim(` (\u2726 ${ctx.thinkingEffort})`) : "";
|
|
6569
|
+
writeln(`${PREFIX.success} model \u2192 ${c16.cyan(modelId)}${effortTag}`);
|
|
6770
6570
|
}
|
|
6771
6571
|
async function handleModelSet(ctx, args) {
|
|
6772
6572
|
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
@@ -6779,7 +6579,7 @@ async function handleModelSet(ctx, args) {
|
|
|
6779
6579
|
const snapshot = await fetchAvailableModels();
|
|
6780
6580
|
const match = findModelIdByAlias(idArg, snapshot.models.map((model) => model.id));
|
|
6781
6581
|
if (!match) {
|
|
6782
|
-
writeln(`${PREFIX.error} unknown model ${
|
|
6582
|
+
writeln(`${PREFIX.error} unknown model ${c16.cyan(idArg)} ${c16.dim("\u2014 run /models for the full list")}`);
|
|
6783
6583
|
return;
|
|
6784
6584
|
}
|
|
6785
6585
|
modelId = match;
|
|
@@ -6799,7 +6599,7 @@ function handleModelEffort(ctx, effortArg) {
|
|
|
6799
6599
|
return;
|
|
6800
6600
|
}
|
|
6801
6601
|
ctx.setThinkingEffort(effort);
|
|
6802
|
-
writeln(`${PREFIX.success} thinking effort \u2192 ${
|
|
6602
|
+
writeln(`${PREFIX.success} thinking effort \u2192 ${c16.cyan(effort)}`);
|
|
6803
6603
|
}
|
|
6804
6604
|
async function handleModelSelect(ctx) {
|
|
6805
6605
|
ctx.startSpinner("fetching models");
|
|
@@ -6807,20 +6607,20 @@ async function handleModelSelect(ctx) {
|
|
|
6807
6607
|
ctx.stopSpinner();
|
|
6808
6608
|
if (snapshot.models.length === 0) {
|
|
6809
6609
|
writeln(`${PREFIX.error} No models found. Check your API keys or Ollama connection.`);
|
|
6810
|
-
writeln(
|
|
6610
|
+
writeln(c16.dim(" Set OPENCODE_API_KEY for Zen, or start Ollama for local models."));
|
|
6811
6611
|
return;
|
|
6812
6612
|
}
|
|
6813
6613
|
if (snapshot.stale) {
|
|
6814
6614
|
const lastSync = snapshot.lastSyncAt ? new Date(snapshot.lastSyncAt).toLocaleString() : "never";
|
|
6815
6615
|
const refreshTag = snapshot.refreshing ? " (refreshing in background)" : "";
|
|
6816
|
-
writeln(
|
|
6616
|
+
writeln(c16.dim(` model metadata is stale (last sync: ${lastSync})${refreshTag}`));
|
|
6817
6617
|
}
|
|
6818
6618
|
const items = snapshot.models.map((model) => {
|
|
6819
6619
|
const isCurrent = ctx.currentModel === model.id;
|
|
6820
|
-
const freeTag = model.free ?
|
|
6821
|
-
const contextTag = model.context ?
|
|
6822
|
-
const currentTag = isCurrent ?
|
|
6823
|
-
const providerTag =
|
|
6620
|
+
const freeTag = model.free ? c16.green(" free") : "";
|
|
6621
|
+
const contextTag = model.context ? c16.dim(` ${Math.round(model.context / 1000)}k`) : "";
|
|
6622
|
+
const currentTag = isCurrent ? c16.cyan(" \u25C0") : "";
|
|
6623
|
+
const providerTag = c16.dim(` [${model.provider}]`);
|
|
6824
6624
|
return {
|
|
6825
6625
|
label: `${model.displayName}${freeTag}${contextTag}${currentTag}${providerTag}`,
|
|
6826
6626
|
value: model.id,
|
|
@@ -6853,7 +6653,7 @@ async function handleModelCommand(ctx, args) {
|
|
|
6853
6653
|
}
|
|
6854
6654
|
|
|
6855
6655
|
// src/cli/commands-session.ts
|
|
6856
|
-
import * as
|
|
6656
|
+
import * as c17 from "yoctocolors";
|
|
6857
6657
|
import { select as select2 } from "yoctoselect";
|
|
6858
6658
|
async function handleSessionCommand(ctx, args) {
|
|
6859
6659
|
const id = args.trim();
|
|
@@ -6862,15 +6662,15 @@ async function handleSessionCommand(ctx, args) {
|
|
|
6862
6662
|
const ok2 = ctx.switchSession(id);
|
|
6863
6663
|
ctx.stopSpinner();
|
|
6864
6664
|
if (ok2) {
|
|
6865
|
-
writeln(`${PREFIX.success} switched to session ${
|
|
6665
|
+
writeln(`${PREFIX.success} switched to session ${c17.cyan(id)} (${c17.cyan(ctx.currentModel)})`);
|
|
6866
6666
|
} else {
|
|
6867
|
-
writeln(`${PREFIX.error} session ${
|
|
6667
|
+
writeln(`${PREFIX.error} session ${c17.cyan(id)} not found`);
|
|
6868
6668
|
}
|
|
6869
6669
|
return;
|
|
6870
6670
|
}
|
|
6871
6671
|
const sessions = listSessions(50);
|
|
6872
6672
|
if (sessions.length === 0) {
|
|
6873
|
-
writeln(`${PREFIX.info} ${
|
|
6673
|
+
writeln(`${PREFIX.info} ${c17.dim("no sessions found")}`);
|
|
6874
6674
|
return;
|
|
6875
6675
|
}
|
|
6876
6676
|
const items = sessions.map((s) => {
|
|
@@ -6879,7 +6679,7 @@ async function handleSessionCommand(ctx, args) {
|
|
|
6879
6679
|
const cwd = tildePath(s.cwd);
|
|
6880
6680
|
const date = new Date(s.updated_at).toLocaleDateString();
|
|
6881
6681
|
return {
|
|
6882
|
-
label: `${
|
|
6682
|
+
label: `${c17.dim(s.id)} ${title} ${c17.cyan(model)} ${c17.dim(cwd)} ${c17.dim(date)}`,
|
|
6883
6683
|
value: s.id,
|
|
6884
6684
|
filterText: `${s.id} ${s.title} ${s.model} ${s.cwd}`
|
|
6885
6685
|
};
|
|
@@ -6895,9 +6695,9 @@ async function handleSessionCommand(ctx, args) {
|
|
|
6895
6695
|
return;
|
|
6896
6696
|
const ok = ctx.switchSession(picked);
|
|
6897
6697
|
if (ok) {
|
|
6898
|
-
writeln(`${PREFIX.success} switched to session ${
|
|
6698
|
+
writeln(`${PREFIX.success} switched to session ${c17.cyan(picked)} (${c17.cyan(ctx.currentModel)})`);
|
|
6899
6699
|
} else {
|
|
6900
|
-
writeln(`${PREFIX.error} session ${
|
|
6700
|
+
writeln(`${PREFIX.error} session ${c17.cyan(picked)} not found`);
|
|
6901
6701
|
}
|
|
6902
6702
|
}
|
|
6903
6703
|
|
|
@@ -6907,9 +6707,9 @@ async function handleUndo(ctx) {
|
|
|
6907
6707
|
try {
|
|
6908
6708
|
const ok = await ctx.undoLastTurn();
|
|
6909
6709
|
if (ok) {
|
|
6910
|
-
writeln(`${PREFIX.success} ${
|
|
6710
|
+
writeln(`${PREFIX.success} ${c18.dim("last conversation turn removed")}`);
|
|
6911
6711
|
} else {
|
|
6912
|
-
writeln(`${PREFIX.info} ${
|
|
6712
|
+
writeln(`${PREFIX.info} ${c18.dim("nothing to undo")}`);
|
|
6913
6713
|
}
|
|
6914
6714
|
} finally {
|
|
6915
6715
|
ctx.stopSpinner();
|
|
@@ -6967,7 +6767,7 @@ async function handleCommand(command, args, ctx) {
|
|
|
6967
6767
|
if (loaded2) {
|
|
6968
6768
|
const srcPath = skill.source === "local" ? `.agents/skills/${skill.name}/SKILL.md` : `~/.agents/skills/${skill.name}/SKILL.md`;
|
|
6969
6769
|
if (skill.context === "fork") {
|
|
6970
|
-
writeln(`${PREFIX.info} ${
|
|
6770
|
+
writeln(`${PREFIX.info} ${c18.cyan(skill.name)} ${c18.dim(`[${srcPath}] (forked subagent)`)}`);
|
|
6971
6771
|
writeln();
|
|
6972
6772
|
const subagentPrompt = args ? `${loaded2.content}
|
|
6973
6773
|
|
|
@@ -6975,7 +6775,7 @@ ${args}` : loaded2.content;
|
|
|
6975
6775
|
const result = await runForkedSkill(skill.name, subagentPrompt, ctx.cwd);
|
|
6976
6776
|
return { type: "inject-user-message", text: result };
|
|
6977
6777
|
}
|
|
6978
|
-
writeln(`${PREFIX.info} ${
|
|
6778
|
+
writeln(`${PREFIX.info} ${c18.cyan(skill.name)} ${c18.dim(`[${srcPath}]`)}`);
|
|
6979
6779
|
writeln();
|
|
6980
6780
|
const prompt = args ? `${loaded2.content}
|
|
6981
6781
|
|
|
@@ -6983,16 +6783,16 @@ ${args}` : loaded2.content;
|
|
|
6983
6783
|
return { type: "inject-user-message", text: prompt };
|
|
6984
6784
|
}
|
|
6985
6785
|
}
|
|
6986
|
-
writeln(`${PREFIX.error} unknown: /${command} ${
|
|
6786
|
+
writeln(`${PREFIX.error} unknown: /${command} ${c18.dim("\u2014 /help for commands")}`);
|
|
6987
6787
|
return { type: "unknown", command };
|
|
6988
6788
|
}
|
|
6989
6789
|
}
|
|
6990
6790
|
}
|
|
6991
6791
|
async function runForkedSkill(skillName, prompt, cwd) {
|
|
6992
|
-
const tmpFile =
|
|
6993
|
-
|
|
6792
|
+
const tmpFile = join8(tmpdir(), `mc-fork-${randomBytes(8).toString("hex")}.md`);
|
|
6793
|
+
writeFileSync(tmpFile, prompt, "utf8");
|
|
6994
6794
|
try {
|
|
6995
|
-
writeln(`${PREFIX.info} ${
|
|
6795
|
+
writeln(`${PREFIX.info} ${c18.dim("running subagent\u2026")}`);
|
|
6996
6796
|
const proc = Bun.spawn([process.execPath, Bun.main], {
|
|
6997
6797
|
cwd,
|
|
6998
6798
|
stdin: Bun.file(tmpFile),
|
|
@@ -7017,24 +6817,24 @@ ${stderr.trim()}`;
|
|
|
7017
6817
|
${output}`;
|
|
7018
6818
|
} finally {
|
|
7019
6819
|
try {
|
|
7020
|
-
|
|
6820
|
+
unlinkSync(tmpFile);
|
|
7021
6821
|
} catch {}
|
|
7022
6822
|
}
|
|
7023
6823
|
}
|
|
7024
6824
|
function handleBooleanToggleCommand(opts) {
|
|
7025
6825
|
const mode = opts.args.trim().toLowerCase();
|
|
7026
6826
|
if (!mode) {
|
|
7027
|
-
writeln(`${PREFIX.success} ${opts.label} ${opts.current ?
|
|
6827
|
+
writeln(`${PREFIX.success} ${opts.label} ${opts.current ? c18.green("on") : c18.dim("off")}`);
|
|
7028
6828
|
return;
|
|
7029
6829
|
}
|
|
7030
6830
|
if (mode === "on") {
|
|
7031
6831
|
opts.set(true);
|
|
7032
|
-
writeln(`${PREFIX.success} ${opts.label} ${
|
|
6832
|
+
writeln(`${PREFIX.success} ${opts.label} ${c18.green("on")}`);
|
|
7033
6833
|
return;
|
|
7034
6834
|
}
|
|
7035
6835
|
if (mode === "off") {
|
|
7036
6836
|
opts.set(false);
|
|
7037
|
-
writeln(`${PREFIX.success} ${opts.label} ${
|
|
6837
|
+
writeln(`${PREFIX.success} ${opts.label} ${c18.dim("off")}`);
|
|
7038
6838
|
return;
|
|
7039
6839
|
}
|
|
7040
6840
|
writeln(`${PREFIX.error} usage: ${opts.usage}`);
|
|
@@ -7118,14 +6918,14 @@ async function runInputLoop(opts) {
|
|
|
7118
6918
|
}
|
|
7119
6919
|
switch (input.type) {
|
|
7120
6920
|
case "eof":
|
|
7121
|
-
reporter.writeText(
|
|
6921
|
+
reporter.writeText(c19.dim("Goodbye."));
|
|
7122
6922
|
return;
|
|
7123
6923
|
case "interrupt":
|
|
7124
6924
|
continue;
|
|
7125
6925
|
case "command": {
|
|
7126
6926
|
const result = await handleCommand(input.command, input.args, cmdCtx);
|
|
7127
6927
|
if (result.type === "exit") {
|
|
7128
|
-
reporter.writeText(
|
|
6928
|
+
reporter.writeText(c19.dim("Goodbye."));
|
|
7129
6929
|
return;
|
|
7130
6930
|
}
|
|
7131
6931
|
if (result.type === "inject-user-message") {
|
|
@@ -7232,13 +7032,12 @@ async function main() {
|
|
|
7232
7032
|
if (last) {
|
|
7233
7033
|
sessionId = last.id;
|
|
7234
7034
|
} else {
|
|
7235
|
-
writeln(
|
|
7035
|
+
writeln(c20.dim("No previous session found, starting fresh."));
|
|
7236
7036
|
}
|
|
7237
7037
|
} else if (args.sessionId) {
|
|
7238
7038
|
sessionId = args.sessionId;
|
|
7239
7039
|
}
|
|
7240
7040
|
const model = args.model ?? getPreferredModel() ?? autoDiscoverModel();
|
|
7241
|
-
bootstrapGlobalDefaults();
|
|
7242
7041
|
if (!prompt) {
|
|
7243
7042
|
renderBanner(model, args.cwd);
|
|
7244
7043
|
}
|
package/docs/KNOWN_ISSUES.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
- AI SDK expands `claude-3-5-haiku` → dated variant that Zen doesn't serve (404).
|
|
6
6
|
- Shell tool: model can `tmux kill-session` the host tmux session if names collide (e.g. audit skill creates session named "audit" matching the user's). Not a code bug — the skill/model just picks a conflicting name. Mitigate via skill wording or session name prefixing.
|
|
7
|
+
- Shell tool output truncation only stops reading stdout/stderr; it does not terminate the underlying process yet. Commands that emit a lot of output and then keep running can still block until they exit or hit timeout.
|
|
7
8
|
|
|
8
9
|
## Features
|
|
9
10
|
|
package/docs/design-decisions.md
CHANGED
|
@@ -20,7 +20,7 @@ A coding agent runs dozens of shell commands per task. Requiring approval for ea
|
|
|
20
20
|
|
|
21
21
|
### Isolation is a separate concern
|
|
22
22
|
|
|
23
|
-
Sandboxing is a real need, but it belongs at the OS/container level — not inside the agent. Tools like [nono](https://nono.sh/) provide proper filesystem and network isolation that the LLM cannot circumvent. This is defense in depth done right: the agent runs unrestricted inside a sandbox that enforces actual boundaries.
|
|
23
|
+
Sandboxing is a real need, but it belongs at the OS/container level — not inside the agent. Tools like [Anthropic Sandbox Runtime (`srt`)](https://github.com/anthropic-experimental/sandbox-runtime) and [nono](https://nono.sh/) provide proper filesystem and network isolation that the LLM cannot circumvent. This is defense in depth done right: the agent runs unrestricted inside a sandbox that enforces actual boundaries.
|
|
24
24
|
|
|
25
25
|
### Our approach
|
|
26
26
|
|
package/docs/mini-coder.1.md
CHANGED
|
@@ -73,14 +73,11 @@ _prompt_
|
|
|
73
73
|
`/mcp remove` _name_
|
|
74
74
|
: Remove MCP server.
|
|
75
75
|
|
|
76
|
-
`/review`
|
|
77
|
-
: Review recent changes (global skill, auto-created at first run).
|
|
78
|
-
|
|
79
76
|
`/login`
|
|
80
77
|
: Show OAuth login status.
|
|
81
78
|
|
|
82
79
|
`/login` _provider_
|
|
83
|
-
: Login via OAuth (opens browser for device flow). Currently supports `
|
|
80
|
+
: Login via OAuth (opens browser for device flow). Currently supports `openai`.
|
|
84
81
|
|
|
85
82
|
`/logout` _provider_
|
|
86
83
|
: Clear saved OAuth tokens.
|
|
@@ -169,15 +166,13 @@ Skills are reusable instruction files at `.agents/skills/<name>/SKILL.md`.
|
|
|
169
166
|
`description`
|
|
170
167
|
: Help text.
|
|
171
168
|
|
|
172
|
-
Skills are
|
|
169
|
+
Skills are auto discovered. To load explicitly:
|
|
173
170
|
|
|
174
171
|
- `/skill-name` in prompts (injects body as a user message).
|
|
175
172
|
- **listSkills** / **readSkill** tools at runtime.
|
|
176
173
|
|
|
177
174
|
Local discovery walks up from cwd to the git worktree root.
|
|
178
175
|
|
|
179
|
-
A default **review** skill is created at `~/.agents/skills/review/SKILL.md` on first run if it doesn't exist. It can be customized or shadowed locally.
|
|
180
|
-
|
|
181
176
|
## CONFIGURATION
|
|
182
177
|
|
|
183
178
|
Config roots: `.agents/`, `.claude/` — local (repo) or global (`~/`).
|
|
@@ -216,7 +211,7 @@ Config roots: `.agents/`, `.claude/` — local (repo) or global (`~/`).
|
|
|
216
211
|
## FILES
|
|
217
212
|
|
|
218
213
|
`~/.config/mini-coder/`
|
|
219
|
-
: App data directory (sessions.db,
|
|
214
|
+
: App data directory (sessions.db, with tables for error and other logs).
|
|
220
215
|
|
|
221
216
|
`.agents/` or `.claude/`
|
|
222
217
|
: Config directories for skills and context files.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Anthropic OAuth Removal Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Remove Claude Code subscription OAuth support without affecting Anthropic API-key access or Zen Claude models.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Delete the Anthropic OAuth provider registration and stop consulting Anthropic OAuth state in discovery/model-info paths. Preserve all direct `ANTHROPIC_API_KEY` behavior and Anthropic-family handling needed by Zen and direct API use.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Bun, TypeScript, bun:test, SQLite-backed auth storage, AI SDK providers.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
### Task 1: Track the work and lock the user-facing contract
|
|
14
|
+
|
|
15
|
+
**Files:**
|
|
16
|
+
|
|
17
|
+
- Modify: `TODO.md`
|
|
18
|
+
- Reference: `docs/superpowers/specs/2026-03-30-anthropic-oauth-removal-design.md`
|
|
19
|
+
|
|
20
|
+
- [ ] **Step 1: Update TODO.md with active work item**
|
|
21
|
+
- [ ] **Step 2: Keep TODO.md current as tasks complete**
|
|
22
|
+
|
|
23
|
+
### Task 2: Write failing tests for Anthropic OAuth removal
|
|
24
|
+
|
|
25
|
+
**Files:**
|
|
26
|
+
|
|
27
|
+
- Modify: `src/llm-api/providers-resolve.test.ts`
|
|
28
|
+
- Modify: `src/llm-api/model-info.test.ts`
|
|
29
|
+
- Create: `src/session/oauth/auth-storage.test.ts`
|
|
30
|
+
|
|
31
|
+
- [ ] **Step 1: Add a test asserting OAuth providers list only OpenAI**
|
|
32
|
+
- [ ] **Step 2: Add tests asserting Anthropic discovery requires `ANTHROPIC_API_KEY`**
|
|
33
|
+
- [ ] **Step 3: Run focused tests to verify they fail for the expected reason**
|
|
34
|
+
|
|
35
|
+
### Task 3: Remove Anthropic OAuth registration and discovery usage
|
|
36
|
+
|
|
37
|
+
**Files:**
|
|
38
|
+
|
|
39
|
+
- Delete: `src/session/oauth/anthropic.ts`
|
|
40
|
+
- Modify: `src/session/oauth/auth-storage.ts`
|
|
41
|
+
- Modify: `src/llm-api/providers.ts`
|
|
42
|
+
- Modify: `src/llm-api/model-info.ts`
|
|
43
|
+
- Modify: `src/llm-api/model-info-fetch.ts`
|
|
44
|
+
|
|
45
|
+
- [ ] **Step 1: Remove the Anthropic OAuth provider from auth storage**
|
|
46
|
+
- [ ] **Step 2: Remove Anthropic OAuth-based provider discovery/autodiscovery**
|
|
47
|
+
- [ ] **Step 3: Remove Anthropic OAuth-based model list fetching**
|
|
48
|
+
- [ ] **Step 4: Run the focused tests and make them pass**
|
|
49
|
+
|
|
50
|
+
### Task 4: Update CLI/docs and finish verification
|
|
51
|
+
|
|
52
|
+
**Files:**
|
|
53
|
+
|
|
54
|
+
- Modify: `src/cli/commands-help.ts`
|
|
55
|
+
- Modify: `docs/mini-coder.1.md`
|
|
56
|
+
- Modify: `TODO.md`
|
|
57
|
+
|
|
58
|
+
- [ ] **Step 1: Remove Anthropic OAuth mentions from help/manpage**
|
|
59
|
+
- [ ] **Step 2: Run formatting if needed**
|
|
60
|
+
- [ ] **Step 3: Run focused tests, then broader verification commands**
|
|
61
|
+
- [ ] **Step 4: Clear the completed TODO item**
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Anthropic OAuth Removal Design
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Remove Claude Code subscription OAuth support from mini-coder while preserving:
|
|
6
|
+
|
|
7
|
+
- direct Anthropic API key support via `ANTHROPIC_API_KEY`
|
|
8
|
+
- Anthropic-family models served through Opencode Zen (`zen/claude-*`)
|
|
9
|
+
|
|
10
|
+
## Approved behavior
|
|
11
|
+
|
|
12
|
+
- `anthropic` is removed completely from the user-facing OAuth surface.
|
|
13
|
+
- `/login anthropic` is no longer supported and behaves like any unknown provider.
|
|
14
|
+
- `/logout anthropic` is no longer documented or advertised as a supported path.
|
|
15
|
+
- Existing saved Anthropic OAuth rows in SQLite are ignored; no migration or cleanup is required.
|
|
16
|
+
- Anthropic remains available through `ANTHROPIC_API_KEY`.
|
|
17
|
+
- Zen-backed Claude models remain available and unchanged.
|
|
18
|
+
|
|
19
|
+
## Approach
|
|
20
|
+
|
|
21
|
+
Use a narrow OAuth-only removal:
|
|
22
|
+
|
|
23
|
+
1. Remove the Anthropic OAuth provider module and provider registration.
|
|
24
|
+
2. Stop consulting Anthropic OAuth state during provider discovery and model-info refresh visibility.
|
|
25
|
+
3. Keep direct Anthropic provider resolution via `ANTHROPIC_API_KEY`.
|
|
26
|
+
4. Keep Anthropic-family request handling and Zen backend routing unchanged.
|
|
27
|
+
5. Update tests and docs to match the new OAuth surface.
|
|
28
|
+
|
|
29
|
+
## Files in scope
|
|
30
|
+
|
|
31
|
+
- `src/session/oauth/anthropic.ts` — delete
|
|
32
|
+
- `src/session/oauth/auth-storage.ts` — unregister Anthropic OAuth
|
|
33
|
+
- `src/llm-api/providers.ts` — stop treating Anthropic OAuth as connected
|
|
34
|
+
- `src/llm-api/model-info.ts` — stop treating Anthropic OAuth as a refresh/visibility source
|
|
35
|
+
- `src/llm-api/model-info-fetch.ts` — stop fetching Anthropic models via OAuth tokens
|
|
36
|
+
- `src/cli/commands-help.ts` — remove Anthropic OAuth example text
|
|
37
|
+
- `docs/mini-coder.1.md` — document OpenAI-only OAuth support
|
|
38
|
+
- tests around provider discovery/model info — update to be deterministic and Anthropic-OAuth-free
|
|
39
|
+
|
|
40
|
+
## Risks and mitigations
|
|
41
|
+
|
|
42
|
+
- Risk: accidentally breaking direct Anthropic API-key support.
|
|
43
|
+
- Mitigation: keep direct provider resolver and add/update tests that verify `ANTHROPIC_API_KEY` still wins.
|
|
44
|
+
- Risk: accidentally breaking Zen Claude behavior.
|
|
45
|
+
- Mitigation: avoid touching Anthropic-family routing/caching code used for Zen.
|
|
46
|
+
- Risk: stale Anthropic OAuth tokens still affecting behavior.
|
|
47
|
+
- Mitigation: remove all Anthropic OAuth checks from discovery and model fetching paths.
|