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 CHANGED
@@ -2,7 +2,7 @@
2
2
  // @bun
3
3
 
4
4
  // src/index.ts
5
- import * as c21 from "yoctocolors";
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.0";
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, unlinkSync } from "fs";
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
- for (const path of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
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/anthropic.ts
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/anthropic.ts
461
- var CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
462
- var AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
463
- var TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
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 = 53692;
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 = "org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload";
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 startCallbackServer2(expectedState) {
486
+ function startCallbackServer(expectedState) {
603
487
  return new Promise((resolve2, reject) => {
604
488
  let result = null;
605
489
  let cancelled = false;
606
- const server = createServer2((req, res) => {
490
+ const server = createServer((req, res) => {
607
491
  const url = new URL(req.url ?? "", "http://localhost");
608
- if (url.pathname !== CALLBACK_PATH2) {
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(SUCCESS_HTML2);
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(CALLBACK_PORT2, CALLBACK_HOST2, () => {
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() + LOGIN_TIMEOUT_MS2;
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 postTokenRequest2(body, label) {
641
- const res = await fetch(TOKEN_URL2, {
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 exchangeCode2(code, verifier) {
661
- return postTokenRequest2(new URLSearchParams({
544
+ function exchangeCode(code, verifier) {
545
+ return postTokenRequest(new URLSearchParams({
662
546
  grant_type: "authorization_code",
663
- client_id: CLIENT_ID2,
547
+ client_id: CLIENT_ID,
664
548
  code,
665
549
  code_verifier: verifier,
666
- redirect_uri: REDIRECT_URI2
550
+ redirect_uri: REDIRECT_URI
667
551
  }), "Token exchange");
668
552
  }
669
553
  function refreshOpenAIToken(refreshToken) {
670
- return postTokenRequest2(new URLSearchParams({
554
+ return postTokenRequest(new URLSearchParams({
671
555
  grant_type: "refresh_token",
672
556
  refresh_token: refreshToken,
673
- client_id: CLIENT_ID2
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 startCallbackServer2(state);
563
+ const cb = await startCallbackServer(state);
680
564
  try {
681
565
  const params = new URLSearchParams({
682
566
  response_type: "code",
683
- client_id: CLIENT_ID2,
684
- redirect_uri: REDIRECT_URI2,
685
- scope: SCOPES2,
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(`${AUTHORIZE_URL2}?${params}`, "Complete login in your browser.");
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 exchangeCode2(result.code, verifier);
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 auth = await getAnthropicAuth();
1337
- if (!auth)
1208
+ const key = process.env.ANTHROPIC_API_KEY;
1209
+ if (!key)
1338
1210
  return null;
1339
- const headers = {
1340
- "anthropic-version": "2023-06-01"
1341
- };
1342
- if (auth.oauth) {
1343
- headers.Authorization = `Bearer ${auth.key}`;
1344
- headers["anthropic-beta"] = "oauth-2025-04-20";
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
- for (const p of ["anthropic", "openai"]) {
1456
- if (!providers.includes(p) && isLoggedIn(p)) {
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 (isLoggedIn("anthropic"))
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 || isLoggedIn("anthropic"))
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: finalMessages,
5278
- systemPrompt: finalSystemPrompt,
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 = 1e4;
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 join8 } from "path";
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 : join8(cwd, 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 c20 from "yoctocolors";
6289
+ import * as c19 from "yoctocolors";
6495
6290
 
6496
6291
  // src/cli/commands.ts
6497
6292
  import { randomBytes } from "crypto";
6498
- import { unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
6293
+ import { unlinkSync, writeFileSync } from "fs";
6499
6294
  import { tmpdir } from "os";
6500
- import { join as join9 } from "path";
6501
- import * as c19 from "yoctocolors";
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 c14 from "yoctocolors";
6299
+ import * as c13 from "yoctocolors";
6505
6300
  function renderEntries(entries) {
6506
6301
  for (const [label, description] of entries) {
6507
- writeln(` ${c14.cyan(label.padEnd(28))} ${c14.dim(description)}`);
6302
+ writeln(` ${c13.cyan(label.padEnd(28))} ${c13.dim(description)}`);
6508
6303
  }
6509
6304
  }
6510
6305
  function renderHelpCommand(ctx) {
6511
6306
  writeln();
6512
- writeln(` ${c14.dim("session")}`);
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(` ${c14.dim("model + context")}`);
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. anthropic)"],
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(` ${c14.dim("prompt")}`);
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(` ${c14.dim("skills")}`);
6338
+ writeln(` ${c13.dim("skills")}`);
6544
6339
  for (const skill of skills.values()) {
6545
- const source = skill.source === "local" ? c14.dim("local") : c14.dim("global");
6340
+ const source = skill.source === "local" ? c13.dim("local") : c13.dim("global");
6546
6341
  const desc = truncateText(skill.description, 80);
6547
- writeln(` ${c14.green(`/${skill.name}`.padEnd(28))} ${c14.dim(desc)} ${c14.dim("\xB7")} ${source}`);
6342
+ writeln(` ${c13.green(`/${skill.name}`.padEnd(28))} ${c13.dim(desc)} ${c13.dim("\xB7")} ${source}`);
6548
6343
  }
6549
6344
  }
6550
6345
  writeln();
6551
- writeln(` ${c14.dim("keys")} ${c14.dim("esc")} cancel response ${c14.dim("\xB7")} ${c14.dim("ctrl+c / ctrl+d")} exit ${c14.dim("\xB7")} ${c14.dim("ctrl+r")} history search ${c14.dim("\xB7")} ${c14.dim("\u2191\u2193")} history`);
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 c15 from "yoctocolors";
6351
+ import * as c14 from "yoctocolors";
6557
6352
  function renderLoginHelp() {
6558
6353
  writeln();
6559
- writeln(` ${c15.dim("usage:")}`);
6560
- writeln(` /login ${c15.dim("show login status")}`);
6561
- writeln(` /login <provider> ${c15.dim("login via OAuth")}`);
6562
- writeln(` /logout <provider> ${c15.dim("clear saved tokens")}`);
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(` ${c15.dim("providers:")}`);
6359
+ writeln(` ${c14.dim("providers:")}`);
6565
6360
  for (const p of getOAuthProviders()) {
6566
- const status = isLoggedIn(p.id) ? c15.green("logged in") : c15.dim("not logged in");
6567
- writeln(` ${c15.cyan(p.id.padEnd(20))} ${p.name} ${c15.dim("\xB7")} ${status}`);
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} ${c15.dim("no OAuth logins \u2014 use")} /login <provider>`);
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} ${c15.cyan(id)} ${c15.dim(name)}`);
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 ${c15.cyan(provider.name)}`);
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(` ${c15.cyan(url)}`);
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} ${c15.dim(msg)}`);
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 ${c15.cyan(provider.name)}`);
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} ${c15.dim("not logged in to")} ${providerId}`);
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 ${c15.cyan(providerId)}`);
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 c16 from "yoctocolors";
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(c16.dim(" no MCP servers configured"));
6659
- writeln(c16.dim(" /mcp add <name> http <url> \xB7 /mcp add <name> stdio <cmd> [args...]"));
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 ? c16.dim(` ${detailText}`) : "";
6666
- writeln(` ${c16.yellow("\u2699")} ${c16.bold(s.name)} ${c16.dim(s.transport)}${detail}`);
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 ${c16.cyan(name)} added and connected`);
6511
+ writeln(`${PREFIX.success} mcp server ${c15.cyan(name)} added and connected`);
6712
6512
  } catch (e) {
6713
- writeln(`${PREFIX.success} mcp server ${c16.cyan(name)} saved ${c16.dim(`(connection failed: ${String(e)})`)}`);
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 ${c16.cyan(name)} removed`);
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(c16.dim(" subcommands: list \xB7 add \xB7 remove"));
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 c17 from "yoctocolors";
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 ${c17.cyan(modelId)} ${c17.dim("(thinking disabled)")}`);
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 ${c17.cyan(modelId)} ${c17.dim(`(\u2726 ${effort})`)}`);
6561
+ writeln(`${PREFIX.success} model \u2192 ${c16.cyan(modelId)} ${c16.dim(`(\u2726 ${effort})`)}`);
6762
6562
  return;
6763
6563
  }
6764
- writeln(`${PREFIX.success} model \u2192 ${c17.cyan(modelId)}`);
6765
- writeln(`${PREFIX.error} unknown effort level ${c17.cyan(effortArg)} (use low, medium, high, xhigh, off)`);
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 ? c17.dim(` (\u2726 ${ctx.thinkingEffort})`) : "";
6769
- writeln(`${PREFIX.success} model \u2192 ${c17.cyan(modelId)}${effortTag}`);
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 ${c17.cyan(idArg)} ${c17.dim("\u2014 run /models for the full list")}`);
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 ${c17.cyan(effort)}`);
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(c17.dim(" Set OPENCODE_API_KEY for Zen, or start Ollama for local models."));
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(c17.dim(` model metadata is stale (last sync: ${lastSync})${refreshTag}`));
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 ? c17.green(" free") : "";
6821
- const contextTag = model.context ? c17.dim(` ${Math.round(model.context / 1000)}k`) : "";
6822
- const currentTag = isCurrent ? c17.cyan(" \u25C0") : "";
6823
- const providerTag = c17.dim(` [${model.provider}]`);
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 c18 from "yoctocolors";
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 ${c18.cyan(id)} (${c18.cyan(ctx.currentModel)})`);
6665
+ writeln(`${PREFIX.success} switched to session ${c17.cyan(id)} (${c17.cyan(ctx.currentModel)})`);
6866
6666
  } else {
6867
- writeln(`${PREFIX.error} session ${c18.cyan(id)} not found`);
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} ${c18.dim("no sessions found")}`);
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: `${c18.dim(s.id)} ${title} ${c18.cyan(model)} ${c18.dim(cwd)} ${c18.dim(date)}`,
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 ${c18.cyan(picked)} (${c18.cyan(ctx.currentModel)})`);
6698
+ writeln(`${PREFIX.success} switched to session ${c17.cyan(picked)} (${c17.cyan(ctx.currentModel)})`);
6899
6699
  } else {
6900
- writeln(`${PREFIX.error} session ${c18.cyan(picked)} not found`);
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} ${c19.dim("last conversation turn removed")}`);
6710
+ writeln(`${PREFIX.success} ${c18.dim("last conversation turn removed")}`);
6911
6711
  } else {
6912
- writeln(`${PREFIX.info} ${c19.dim("nothing to undo")}`);
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} ${c19.cyan(skill.name)} ${c19.dim(`[${srcPath}] (forked subagent)`)}`);
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} ${c19.cyan(skill.name)} ${c19.dim(`[${srcPath}]`)}`);
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} ${c19.dim("\u2014 /help for commands")}`);
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 = join9(tmpdir(), `mc-fork-${randomBytes(8).toString("hex")}.md`);
6993
- writeFileSync2(tmpFile, prompt, "utf8");
6792
+ const tmpFile = join8(tmpdir(), `mc-fork-${randomBytes(8).toString("hex")}.md`);
6793
+ writeFileSync(tmpFile, prompt, "utf8");
6994
6794
  try {
6995
- writeln(`${PREFIX.info} ${c19.dim("running subagent\u2026")}`);
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
- unlinkSync2(tmpFile);
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 ? c19.green("on") : c19.dim("off")}`);
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} ${c19.green("on")}`);
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} ${c19.dim("off")}`);
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(c20.dim("Goodbye."));
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(c20.dim("Goodbye."));
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(c21.dim("No previous session found, starting fresh."));
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
  }
@@ -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
 
@@ -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
 
@@ -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 `anthropic` and `openai` (`openai` uses the Codex / ChatGPT Plus/Pro flow).
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 never auto-loaded. Load explicitly:
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, api.log, errors.log).
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mini-coder",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "A small, fast CLI coding agent",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",