cc-claw 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +364 -66
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -55,7 +55,7 @@ var VERSION;
55
55
  var init_version = __esm({
56
56
  "src/version.ts"() {
57
57
  "use strict";
58
- VERSION = true ? "0.6.0" : (() => {
58
+ VERSION = true ? "0.7.0" : (() => {
59
59
  try {
60
60
  return JSON.parse(readFileSync(join2(process.cwd(), "package.json"), "utf-8")).version ?? "unknown";
61
61
  } catch {
@@ -964,12 +964,14 @@ __export(store_exports3, {
964
964
  getBackendLimit: () => getBackendLimit,
965
965
  getBackendUsageInWindow: () => getBackendUsageInWindow,
966
966
  getBookmark: () => getBookmark,
967
+ getChatGeminiSlotId: () => getChatGeminiSlotId,
967
968
  getChatIdByAlias: () => getChatIdByAlias,
968
969
  getChatUsageByModel: () => getChatUsageByModel,
969
970
  getCwd: () => getCwd,
970
971
  getDb: () => getDb,
971
972
  getEligibleGeminiSlots: () => getEligibleGeminiSlots,
972
973
  getEnabledTools: () => getEnabledTools,
974
+ getGeminiRotationMode: () => getGeminiRotationMode,
973
975
  getGeminiSlots: () => getGeminiSlots,
974
976
  getHeartbeatConfig: () => getHeartbeatConfig,
975
977
  getJobById: () => getJobById,
@@ -1028,6 +1030,7 @@ __export(store_exports3, {
1028
1030
  setBootTime: () => setBootTime,
1029
1031
  setChatAlias: () => setChatAlias,
1030
1032
  setCwd: () => setCwd,
1033
+ setGeminiRotationMode: () => setGeminiRotationMode,
1031
1034
  setGeminiSlotEnabled: () => setGeminiSlotEnabled,
1032
1035
  setHeartbeatConfig: () => setHeartbeatConfig,
1033
1036
  setMode: () => setMode,
@@ -2406,17 +2409,24 @@ function getGeminiSlots() {
2406
2409
  FROM gemini_credentials ORDER BY priority ASC, id ASC
2407
2410
  `).all();
2408
2411
  }
2409
- function getEligibleGeminiSlots() {
2412
+ function getEligibleGeminiSlots(mode) {
2413
+ if (mode === "off") return [];
2414
+ const slotTypeFilter = mode === "accounts" ? "AND slot_type = 'oauth'" : mode === "keys" ? "AND slot_type = 'api_key'" : "";
2410
2415
  return db.prepare(`
2411
2416
  SELECT id, slot_type AS slotType, label, api_key AS apiKey, config_home AS configHome,
2412
2417
  priority, enabled, cooldown_until AS cooldownUntil, last_used AS lastUsed,
2413
2418
  consecutive_errors AS consecutiveErrors, created_at AS createdAt
2414
2419
  FROM gemini_credentials
2415
2420
  WHERE enabled = 1 AND (cooldown_until IS NULL OR cooldown_until <= datetime('now'))
2421
+ ${slotTypeFilter}
2416
2422
  ORDER BY priority ASC, last_used ASC NULLS FIRST
2417
2423
  `).all();
2418
2424
  }
2419
- function getNextGeminiSlot(chatId) {
2425
+ function getChatGeminiSlotId(chatId) {
2426
+ const row = db.prepare("SELECT slot_id FROM chat_gemini_slot WHERE chat_id = ?").get(chatId);
2427
+ return row?.slot_id ?? null;
2428
+ }
2429
+ function getNextGeminiSlot(chatId, mode) {
2420
2430
  const pinned = db.prepare(`
2421
2431
  SELECT gc.id, gc.slot_type AS slotType, gc.label, gc.api_key AS apiKey, gc.config_home AS configHome,
2422
2432
  gc.priority, gc.enabled, gc.cooldown_until AS cooldownUntil, gc.last_used AS lastUsed,
@@ -2426,7 +2436,7 @@ function getNextGeminiSlot(chatId) {
2426
2436
  AND (gc.cooldown_until IS NULL OR gc.cooldown_until <= datetime('now'))
2427
2437
  `).get(chatId);
2428
2438
  if (pinned) return pinned;
2429
- const eligible = getEligibleGeminiSlots();
2439
+ const eligible = getEligibleGeminiSlots(mode);
2430
2440
  return eligible.length > 0 ? eligible[0] : null;
2431
2441
  }
2432
2442
  function markSlotExhausted(slotId, quotaClass) {
@@ -2473,6 +2483,13 @@ function setGeminiSlotEnabled(id, enabled) {
2473
2483
  function reorderGeminiSlot(id, priority) {
2474
2484
  db.prepare("UPDATE gemini_credentials SET priority = ? WHERE id = ?").run(priority, id);
2475
2485
  }
2486
+ function getGeminiRotationMode() {
2487
+ const row = db.prepare("SELECT value FROM meta WHERE key = 'gemini_rotation_mode'").get();
2488
+ return row?.value ?? "all";
2489
+ }
2490
+ function setGeminiRotationMode(mode) {
2491
+ db.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES ('gemini_rotation_mode', ?)").run(mode);
2492
+ }
2476
2493
  function getAgentMode(chatId) {
2477
2494
  const row = db.prepare("SELECT mode FROM chat_agent_mode WHERE chat_id = ?").get(chatId);
2478
2495
  return row?.mode ?? "auto";
@@ -2911,17 +2928,25 @@ var init_gemini = __esm({
2911
2928
  thinking: "adjustable",
2912
2929
  thinkingLevels: ["low", "high"],
2913
2930
  defaultThinkingLevel: "high"
2931
+ },
2932
+ "gemini-3.1-flash-lite-preview": {
2933
+ label: "Gemini 3.1 Flash Lite \u2014 lightest, cheapest",
2934
+ thinking: "adjustable",
2935
+ thinkingLevels: ["low", "high"],
2936
+ defaultThinkingLevel: "low"
2914
2937
  }
2915
2938
  };
2916
2939
  defaultModel = "gemini-3.1-pro-preview";
2917
- summarizerModel = "gemini-3-flash-preview";
2940
+ summarizerModel = "gemini-3.1-flash-lite-preview";
2918
2941
  pricing = {
2919
2942
  "gemini-3.1-pro-preview": { in: 1.25, out: 10, cache: 0.32 },
2920
- "gemini-3-flash-preview": { in: 0.15, out: 0.6, cache: 0.04 }
2943
+ "gemini-3-flash-preview": { in: 0.15, out: 0.6, cache: 0.04 },
2944
+ "gemini-3.1-flash-lite-preview": { in: 0.04, out: 0.15, cache: 0.01 }
2921
2945
  };
2922
2946
  contextWindow = {
2923
2947
  "gemini-3.1-pro-preview": 1e6,
2924
- "gemini-3-flash-preview": 1e6
2948
+ "gemini-3-flash-preview": 1e6,
2949
+ "gemini-3.1-flash-lite-preview": 1e6
2925
2950
  };
2926
2951
  _resolvedPath = "";
2927
2952
  getExecutablePath() {
@@ -3024,8 +3049,8 @@ var init_gemini = __esm({
3024
3049
  * OAuth slots set GEMINI_CLI_HOME for isolated config and unset GEMINI_API_KEY so
3025
3050
  * the CLI uses OAuth instead. Returns the slot used (or null for default behavior).
3026
3051
  */
3027
- getEnvForSlot(chatId, thinkingOverrides) {
3028
- const slot = getNextGeminiSlot(chatId);
3052
+ getEnvForSlot(chatId, thinkingOverrides, mode) {
3053
+ const slot = getNextGeminiSlot(chatId, mode);
3029
3054
  const env = this.getEnv(thinkingOverrides);
3030
3055
  if (!slot) return { env, slot: null };
3031
3056
  if (slot.slotType === "api_key" && slot.apiKey) {
@@ -4073,7 +4098,7 @@ Key details: ${keyDetails}`;
4073
4098
  return false;
4074
4099
  }
4075
4100
  }
4076
- async function summarizeWithFallbackChain(chatId, targetBackendId) {
4101
+ async function summarizeWithFallbackChain(chatId, targetBackendId, excludeBackend) {
4077
4102
  const pairCount = getMessagePairCount(chatId);
4078
4103
  if (pairCount < MIN_PAIRS) {
4079
4104
  clearLog(chatId);
@@ -4082,6 +4107,10 @@ async function summarizeWithFallbackChain(chatId, targetBackendId) {
4082
4107
  const entries = getLog(chatId);
4083
4108
  if (entries.length === 0) return false;
4084
4109
  const tried = /* @__PURE__ */ new Set();
4110
+ if (excludeBackend) {
4111
+ const excluded = getAdapter(excludeBackend);
4112
+ tried.add(`${excluded.id}:${excluded.summarizerModel}`);
4113
+ }
4085
4114
  try {
4086
4115
  const config2 = getSummarizer(chatId);
4087
4116
  if (config2.backend !== "off") {
@@ -4193,6 +4222,9 @@ Be specific. The new assistant has no prior context. Cover any domain \u2014 per
4193
4222
 
4194
4223
  // src/gemini/quota.ts
4195
4224
  function classifyGeminiQuota(errorText) {
4225
+ for (const p of CAPACITY_PATTERNS) {
4226
+ if (p.test(errorText)) return "rate_limited";
4227
+ }
4196
4228
  for (const p of RATE_LIMITED_PATTERNS) {
4197
4229
  if (p.test(errorText)) return "rate_limited";
4198
4230
  }
@@ -4204,7 +4236,7 @@ function classifyGeminiQuota(errorText) {
4204
4236
  }
4205
4237
  return "unknown";
4206
4238
  }
4207
- var RATE_LIMITED_PATTERNS, BILLING_PATTERNS, DAILY_QUOTA_PATTERNS, GEMINI_SLOTS_EXHAUSTED_MSG;
4239
+ var RATE_LIMITED_PATTERNS, BILLING_PATTERNS, DAILY_QUOTA_PATTERNS, CAPACITY_PATTERNS, GEMINI_SLOTS_EXHAUSTED_MSG, GEMINI_NO_SLOTS_FOR_MODE_MSG, GEMINI_ALL_SLOTS_COOLDOWN_MSG;
4208
4240
  var init_quota = __esm({
4209
4241
  "src/gemini/quota.ts"() {
4210
4242
  "use strict";
@@ -4227,7 +4259,13 @@ var init_quota = __esm({
4227
4259
  /RESOURCE_EXHAUSTED/,
4228
4260
  /exhausted.*quota/i
4229
4261
  ];
4262
+ CAPACITY_PATTERNS = [
4263
+ /MODEL_CAPACITY_EXHAUSTED/,
4264
+ /No capacity available/i
4265
+ ];
4230
4266
  GEMINI_SLOTS_EXHAUSTED_MSG = "Gemini usage limit \u2014 all credential slots exhausted";
4267
+ GEMINI_NO_SLOTS_FOR_MODE_MSG = "Gemini rotation \u2014 no eligible slots for current rotation mode";
4268
+ GEMINI_ALL_SLOTS_COOLDOWN_MSG = "Gemini rotation \u2014 all eligible slots in cooldown";
4231
4269
  }
4232
4270
  });
4233
4271
 
@@ -6737,22 +6775,23 @@ function spawnQuery(adapter, config2, model2, cancelState, thinkingLevel, timeou
6737
6775
  });
6738
6776
  });
6739
6777
  }
6740
- async function spawnGeminiWithRotation(chatId, adapter, config2, model2, cancelState, thinkingLevel, timeoutMs, maxTurns, opts, onSlotRotation) {
6778
+ async function spawnGeminiWithRotation(chatId, adapter, baseConfig, configWithSession, model2, cancelState, thinkingLevel, timeoutMs, maxTurns, rotationMode, opts, onSlotRotation) {
6741
6779
  const geminiAdapter = adapter;
6742
- const slots = getEligibleGeminiSlots();
6780
+ const slots = getEligibleGeminiSlots(rotationMode);
6743
6781
  if (slots.length === 0) {
6744
- return spawnQuery(adapter, config2, model2, cancelState, thinkingLevel, timeoutMs, maxTurns, opts);
6782
+ return spawnQuery(adapter, configWithSession, model2, cancelState, thinkingLevel, timeoutMs, maxTurns, opts);
6745
6783
  }
6746
6784
  const maxAttempts = Math.min(slots.length, 10);
6747
6785
  let lastError;
6748
6786
  for (let i = 0; i < maxAttempts; i++) {
6749
- const { env, slot } = geminiAdapter.getEnvForSlot(chatId, void 0);
6787
+ const { env, slot } = geminiAdapter.getEnvForSlot(chatId, void 0, rotationMode);
6750
6788
  if (!slot) break;
6751
6789
  const slotLabel = slot.label || `slot-${slot.id}`;
6752
6790
  log(`[agent:gemini-rotation] Trying ${slotLabel} (${slot.slotType}, attempt ${i + 1}/${maxAttempts})`);
6753
6791
  if (i === 0) pinChatGeminiSlot(chatId, slot.id);
6792
+ const effectiveConfig = i === 0 ? configWithSession : baseConfig;
6754
6793
  try {
6755
- const result = await spawnQuery(adapter, config2, model2, cancelState, thinkingLevel, timeoutMs, maxTurns, { ...opts, envOverride: env });
6794
+ const result = await spawnQuery(adapter, effectiveConfig, model2, cancelState, thinkingLevel, timeoutMs, maxTurns, { ...opts, envOverride: env });
6756
6795
  const combinedText = result.resultText || "";
6757
6796
  if (combinedText && /RESOURCE.?EXHAUSTED|resource has been exhausted/i.test(combinedText)) {
6758
6797
  throw new Error(combinedText);
@@ -6770,11 +6809,11 @@ async function spawnGeminiWithRotation(chatId, adapter, config2, model2, cancelS
6770
6809
  markSlotExhausted(slot.id, effectiveClass);
6771
6810
  lastError = err instanceof Error ? err : new Error(errMsg);
6772
6811
  try {
6773
- await summarizeWithFallbackChain(chatId);
6812
+ await summarizeWithFallbackChain(chatId, void 0, "gemini");
6774
6813
  clearSession(chatId);
6775
6814
  } catch {
6776
6815
  }
6777
- const nextSlot = getEligibleGeminiSlots()[0];
6816
+ const nextSlot = getEligibleGeminiSlots(rotationMode)[0];
6778
6817
  if (nextSlot && onSlotRotation) {
6779
6818
  const nextLabel = nextSlot.label || `slot-${nextSlot.id}`;
6780
6819
  onSlotRotation(slotLabel, nextLabel);
@@ -6782,6 +6821,20 @@ async function spawnGeminiWithRotation(chatId, adapter, config2, model2, cancelS
6782
6821
  }
6783
6822
  }
6784
6823
  }
6824
+ const allSlotsForMode = getGeminiSlots().filter((s) => {
6825
+ if (!s.enabled) return false;
6826
+ if (rotationMode === "accounts") return s.slotType === "oauth";
6827
+ if (rotationMode === "keys") return s.slotType === "api_key";
6828
+ return true;
6829
+ });
6830
+ if (allSlotsForMode.length === 0) {
6831
+ throw new Error(GEMINI_NO_SLOTS_FOR_MODE_MSG);
6832
+ }
6833
+ const soonest = allSlotsForMode.filter((s) => s.cooldownUntil).map((s) => new Date(s.cooldownUntil).getTime()).sort((a, b) => a - b)[0];
6834
+ if (soonest) {
6835
+ const hoursLeft = Math.max(1, Math.ceil((soonest - Date.now()) / 36e5));
6836
+ throw new Error(`${GEMINI_ALL_SLOTS_COOLDOWN_MSG}|${hoursLeft}h`);
6837
+ }
6785
6838
  throw new Error(GEMINI_SLOTS_EXHAUSTED_MSG);
6786
6839
  }
6787
6840
  function askAgent(chatId, userMessage, opts) {
@@ -6833,11 +6886,12 @@ async function askAgentImpl(chatId, userMessage, opts) {
6833
6886
  const spawnOpts = { onStream, onToolAction, onSubagentActivity };
6834
6887
  const resolvedModel = model2 ?? adapter.defaultModel;
6835
6888
  let result = { resultText: "", sessionId: void 0, input: 0, output: 0, cacheRead: 0, sawToolEvents: false, sawResultEvent: false };
6836
- const useGeminiRotation = adapter.id === "gemini" && getEligibleGeminiSlots().length > 0;
6889
+ const rotationMode = adapter.id === "gemini" ? getGeminiRotationMode() : "off";
6890
+ const useGeminiRotation = rotationMode !== "off" && getEligibleGeminiSlots(rotationMode).length > 0;
6837
6891
  try {
6838
6892
  if (useGeminiRotation) {
6839
6893
  const rotationCb = onSlotRotation ? (from, to) => onSlotRotation(chatId, from, to) : void 0;
6840
- result = await spawnGeminiWithRotation(chatId, adapter, configWithSession, resolvedModel, cancelState, thinkingLevel, timeoutMs, maxTurns, spawnOpts, rotationCb);
6894
+ result = await spawnGeminiWithRotation(chatId, adapter, baseConfig, configWithSession, resolvedModel, cancelState, thinkingLevel, timeoutMs, maxTurns, rotationMode, spawnOpts, rotationCb);
6841
6895
  } else {
6842
6896
  result = await spawnQuery(adapter, configWithSession, resolvedModel, cancelState, thinkingLevel, timeoutMs, maxTurns, spawnOpts);
6843
6897
  }
@@ -6848,7 +6902,7 @@ async function askAgentImpl(chatId, userMessage, opts) {
6848
6902
  clearSession(chatId);
6849
6903
  if (useGeminiRotation) {
6850
6904
  const rotationCb = onSlotRotation ? (from, to) => onSlotRotation(chatId, from, to) : void 0;
6851
- result = await spawnGeminiWithRotation(chatId, adapter, baseConfig, resolvedModel, cancelState, thinkingLevel, timeoutMs, maxTurns, spawnOpts, rotationCb);
6905
+ result = await spawnGeminiWithRotation(chatId, adapter, baseConfig, baseConfig, resolvedModel, cancelState, thinkingLevel, timeoutMs, maxTurns, rotationMode, spawnOpts, rotationCb);
6852
6906
  } else {
6853
6907
  result = await spawnQuery(adapter, baseConfig, resolvedModel, cancelState, thinkingLevel, timeoutMs, maxTurns, spawnOpts);
6854
6908
  }
@@ -6877,7 +6931,10 @@ async function askAgentImpl(chatId, userMessage, opts) {
6877
6931
  if (pairCount >= AUTO_SUMMARIZE_THRESHOLD) {
6878
6932
  log(`[agent] Auto-summarizing chat ${chatId} after ${pairCount} turns`);
6879
6933
  summarizeWithFallbackChain(chatId).then((saved) => {
6880
- if (saved) opts?.onCompaction?.(chatId);
6934
+ if (saved) {
6935
+ clearSession(chatId);
6936
+ opts?.onCompaction?.(chatId);
6937
+ }
6881
6938
  }).catch((err) => {
6882
6939
  warn(`[agent] Auto-summarize failed for chat ${chatId}: ${err}`);
6883
6940
  });
@@ -10588,7 +10645,7 @@ async function handleCommand(msg, channel) {
10588
10645
  case "help":
10589
10646
  await channel.sendText(
10590
10647
  chatId,
10591
- "Hey! I'm CC-Claw \u2014 your personal AI assistant on Telegram.\n\nI use AI coding CLIs (Claude, Gemini, Codex) as my brain. Just send me a message to get started.\n\nCommands:\n/backend [name] - Switch AI backend (or /claude /gemini /codex)\n/model - Switch model for active backend\n/summarizer - Configure session summarization model\n/status - Show session, model, backend, and usage\n/cost - Show estimated API cost (use /cost all for all-time)\n/usage - Show usage per backend with limits\n/limits - Configure usage limits per backend\n/newchat - Start a fresh conversation\n/summarize - Save session to memory (without resetting)\n/summarize all - Summarize all pending sessions (pre-restart)\n/cwd <path> - Set working directory\n/cwd - Show current working directory\n/memory - List stored memories\n/remember <text> - Save a memory\n/forget <keyword> - Remove a memory\n/voice - Toggle voice responses\n/voice_config - Configure voice provider and voice\n/imagine <prompt> - Generate an image (or /image)\n/cron <description> - Schedule a task (or /schedule)\n/cron - List scheduled jobs (or /jobs)\n/cron cancel <id> - Cancel a job\n/cron pause <id> - Pause a job\n/cron resume <id> - Resume a job\n/cron run <id> - Trigger a job now\n/cron runs [id] - View run history\n/cron edit <id> - Edit a job\n/cron health - Scheduler health\n/skills - List skills from all backends\n/skill-install <url> - Install a skill from GitHub\n/setup-profile - Set up your user profile\n/chats - List authorized chats and aliases\n/heartbeat - Proactive awareness (on/off/interval/hours)\n/history - List recent session summaries\n/stop - Cancel the current running task\n/tools - Configure which tools the agent can use\n/permissions - Switch permission mode (yolo/safe/plan)\n/verbose - Tool visibility (off/normal/verbose)\n/model_signature - Toggle model+thinking signature on responses\n/intent <msg> - Test intent classifier (chat vs agentic)\n/agents - List active sub-agents\n/agents mode [auto|native|claw] - Set agent mode (native vs orchestrated)\n/agents history - Native sub-agent activity (24h)\n/tasks - Show task board for current orchestration\n/stopagent <id> - Cancel a specific sub-agent\n/stopall - Cancel all sub-agents in this chat\n/runners - List registered CLI runners\n/mcps - List registered MCP servers\n/help - Show this message",
10648
+ "Hey! I'm CC-Claw \u2014 your personal AI assistant on Telegram.\n\nI use AI coding CLIs (Claude, Gemini, Codex) as my brain. Just send me a message to get started.\n\nCommands:\n/backend [name] - Switch AI backend (or /claude /gemini /codex)\n/model - Switch model for active backend\n/gemini slot - Select Gemini credential (account/key)\n/summarizer - Configure session summarization model\n/status - Show session, model, backend, and usage\n/cost - Show estimated API cost (use /cost all for all-time)\n/usage - Show usage per backend with limits\n/limits - Configure usage limits per backend\n/newchat - Start a fresh conversation\n/summarize - Save session to memory (without resetting)\n/summarize all - Summarize all pending sessions (pre-restart)\n/cwd <path> - Set working directory\n/cwd - Show current working directory\n/memory - List stored memories\n/remember <text> - Save a memory\n/forget <keyword> - Remove a memory\n/voice - Toggle voice responses\n/voice_config - Configure voice provider and voice\n/imagine <prompt> - Generate an image (or /image)\n/cron <description> - Schedule a task (or /schedule)\n/cron - List scheduled jobs (or /jobs)\n/cron cancel <id> - Cancel a job\n/cron pause <id> - Pause a job\n/cron resume <id> - Resume a job\n/cron run <id> - Trigger a job now\n/cron runs [id] - View run history\n/cron edit <id> - Edit a job\n/cron health - Scheduler health\n/skills - List skills from all backends\n/skill-install <url> - Install a skill from GitHub\n/setup-profile - Set up your user profile\n/chats - List authorized chats and aliases\n/heartbeat - Proactive awareness (on/off/interval/hours)\n/history - List recent session summaries\n/stop - Cancel the current running task\n/tools - Configure which tools the agent can use\n/permissions - Switch permission mode (yolo/safe/plan)\n/verbose - Tool visibility (off/normal/verbose)\n/model_signature - Toggle model+thinking signature on responses\n/intent <msg> - Test intent classifier (chat vs agentic)\n/agents - List active sub-agents\n/agents mode [auto|native|claw] - Set agent mode (native vs orchestrated)\n/agents history - Native sub-agent activity (24h)\n/tasks - Show task board for current orchestration\n/stopagent <id> - Cancel a specific sub-agent\n/stopall - Cancel all sub-agents in this chat\n/runners - List registered CLI runners\n/mcps - List registered MCP servers\n/help - Show this message",
10592
10649
  "plain"
10593
10650
  );
10594
10651
  break;
@@ -10751,12 +10808,24 @@ Tap to toggle:`,
10751
10808
  const iStats = getIntentStats();
10752
10809
  const iTotal = iStats.chat + iStats.agentic;
10753
10810
  const intentLine = iTotal > 0 ? `\u26A1 ${iStats.chat} chat \xB7 ${iStats.agentic} agentic (${(iStats.chat / iTotal * 100).toFixed(0)}% fast-path)` : `\u26A1 No messages classified yet`;
10811
+ let geminiSlotInfo = "";
10812
+ if ((backendId ?? "claude") === "gemini") {
10813
+ try {
10814
+ const active = getNextGeminiSlot(chatId);
10815
+ const totalSlots = getEligibleGeminiSlots().length;
10816
+ if (active) {
10817
+ const label2 = active.label || `slot-${active.id}`;
10818
+ geminiSlotInfo = ` \xB7 \u{1F511} ${label2} (${totalSlots} slots)`;
10819
+ }
10820
+ } catch {
10821
+ }
10822
+ }
10754
10823
  const lines = [
10755
10824
  `\u{1F43E} CC-Claw v${VERSION}`,
10756
10825
  `\u23F1 Uptime: ${uptimeStr}`,
10757
10826
  ``,
10758
10827
  `\u2501\u2501 Engine \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`,
10759
- `\u{1F9E0} ${adapter?.displayName ?? backendId ?? "not set"} \xB7 ${formatModelShort(model2)}`,
10828
+ `\u{1F9E0} ${adapter?.displayName ?? backendId ?? "not set"} \xB7 ${formatModelShort(model2)}${geminiSlotInfo}`,
10760
10829
  `\u{1F4AD} Think: ${thinking2} \xB7 Mode: ${mode}`,
10761
10830
  `\u{1F916} Agents: ${getAgentMode(chatId)}`,
10762
10831
  `\u{1F507} Voice: ${voice2 ? "on" : "off"} \xB7 Sig: ${modelSig}`,
@@ -10800,7 +10869,6 @@ Tap to toggle:`,
10800
10869
  break;
10801
10870
  }
10802
10871
  case "claude":
10803
- case "gemini":
10804
10872
  case "codex": {
10805
10873
  const backendId = command;
10806
10874
  if (getAllBackendIds().includes(backendId)) {
@@ -10810,6 +10878,56 @@ Tap to toggle:`,
10810
10878
  }
10811
10879
  break;
10812
10880
  }
10881
+ case "gemini_accounts": {
10882
+ const slots = getGeminiSlots();
10883
+ if (slots.length === 0) {
10884
+ await channel.sendText(chatId, "No Gemini credentials configured.\nAdd with: <code>cc-claw gemini add-key</code> or <code>cc-claw gemini add-account</code>", "html");
10885
+ break;
10886
+ }
10887
+ if (typeof channel.sendKeyboard === "function") {
10888
+ const currentMode = getGeminiRotationMode();
10889
+ const pinnedSlotId = getChatGeminiSlotId(chatId);
10890
+ const slotButtons = slots.filter((s) => s.enabled).map((s) => {
10891
+ const label2 = s.label || `slot-${s.id}`;
10892
+ const icon = s.slotType === "oauth" ? "\u{1F468}\u{1F3FD}\u200D\u{1F4BB}" : "\u{1F511}";
10893
+ const marker = pinnedSlotId === s.id ? " \u2713" : "";
10894
+ return { label: `${icon} ${label2}${marker}`, data: `gslot:${s.id}` };
10895
+ });
10896
+ const rows = [];
10897
+ for (let i = 0; i < slotButtons.length; i += 2) {
10898
+ rows.push(slotButtons.slice(i, i + 2));
10899
+ }
10900
+ rows.push([{ label: "\u{1F504} Auto (rotation)", data: "gslot:auto" }]);
10901
+ const modeLabels = { off: "Off", all: "All", accounts: "\u{1F468}\u{1F3FD}\u200D\u{1F4BB} Only", keys: "\u{1F511} Only" };
10902
+ const modeButtons = ["off", "all", "accounts", "keys"].map((m) => ({
10903
+ label: `${m === currentMode ? "\u2713 " : ""}${modeLabels[m]}`,
10904
+ data: `grotation:${m}`
10905
+ }));
10906
+ rows.push(modeButtons);
10907
+ await channel.sendKeyboard(chatId, "Gemini Accounts & Rotation:", rows);
10908
+ } else {
10909
+ const currentMode = getGeminiRotationMode();
10910
+ const list = slots.filter((s) => s.enabled).map((s) => {
10911
+ const icon = s.slotType === "oauth" ? "\u{1F468}\u{1F3FD}\u200D\u{1F4BB}" : "\u{1F511}";
10912
+ return `${icon} ${s.label || `slot-${s.id}`} (#${s.id})`;
10913
+ }).join("\n");
10914
+ await channel.sendText(chatId, `Slots:
10915
+ ${list}
10916
+
10917
+ Rotation mode: ${currentMode}
10918
+ Use: /gemini_accounts <name> to pin`, "plain");
10919
+ }
10920
+ break;
10921
+ }
10922
+ case "gemini": {
10923
+ const backendId = command;
10924
+ if (getAllBackendIds().includes(backendId)) {
10925
+ await sendBackendSwitchConfirmation(chatId, backendId, channel);
10926
+ } else {
10927
+ await channel.sendText(chatId, `Backend "${command}" is not available.`, "plain");
10928
+ }
10929
+ break;
10930
+ }
10813
10931
  case "model": {
10814
10932
  let adapter;
10815
10933
  try {
@@ -12125,7 +12243,12 @@ async function handleText(msg, channel) {
12125
12243
  });
12126
12244
  },
12127
12245
  onSlotRotation: (cid, from, to) => {
12128
- channel.sendText(cid, `\u{1F511} Gemini quota reached on ${from} \u2014 continuing on ${to}. Context saved.`).catch(() => {
12246
+ const slots = getGeminiSlots();
12247
+ const fromSlot = slots.find((s) => (s.label || `slot-${s.id}`) === from);
12248
+ const toSlot = slots.find((s) => (s.label || `slot-${s.id}`) === to);
12249
+ const fromIcon = fromSlot?.slotType === "oauth" ? "\u{1F468}\u{1F3FD}\u200D\u{1F4BB}" : "\u{1F511}";
12250
+ const toIcon = toSlot?.slotType === "oauth" ? "\u{1F468}\u{1F3FD}\u200D\u{1F4BB}" : "\u{1F511}";
12251
+ channel.sendText(cid, `\u26A0\uFE0F Quota reached on ${fromIcon} ${from} \u2014 switched to ${toIcon} ${to}. Context saved.`, "plain").catch(() => {
12129
12252
  });
12130
12253
  }
12131
12254
  });
@@ -12165,6 +12288,32 @@ async function handleText(msg, channel) {
12165
12288
  } catch (err) {
12166
12289
  error("[router] Error:", err);
12167
12290
  const errMsg = errorMessage(err);
12291
+ if (errMsg.includes(GEMINI_NO_SLOTS_FOR_MODE_MSG)) {
12292
+ const mode = getGeminiRotationMode();
12293
+ const modeLabel = mode === "accounts" ? "\u{1F468}\u{1F3FD}\u200D\u{1F4BB} Only" : mode === "keys" ? "\u{1F511} Only" : mode;
12294
+ if (typeof channel.sendKeyboard === "function") {
12295
+ const rows = [
12296
+ [{ label: "Claude", data: "backend:claude" }, { label: "Codex", data: "backend:codex" }],
12297
+ [{ label: "\u2699\uFE0F Change rotation mode", data: "gopen:accounts" }]
12298
+ ];
12299
+ await channel.sendKeyboard(chatId, `\u26A0\uFE0F No eligible slots for rotation mode (${modeLabel}). Switch backend?`, rows);
12300
+ } else {
12301
+ await channel.sendText(chatId, `\u26A0\uFE0F No eligible slots for rotation mode (${modeLabel}).`, "plain");
12302
+ }
12303
+ return;
12304
+ } else if (errMsg.includes(GEMINI_ALL_SLOTS_COOLDOWN_MSG)) {
12305
+ const timeLeft = errMsg.split("|")[1] || "?";
12306
+ if (typeof channel.sendKeyboard === "function") {
12307
+ const rows = [
12308
+ [{ label: "Claude", data: "backend:claude" }, { label: "Codex", data: "backend:codex" }],
12309
+ [{ label: "\u2699\uFE0F Change rotation mode", data: "gopen:accounts" }]
12310
+ ];
12311
+ await channel.sendKeyboard(chatId, `\u26A0\uFE0F All eligible slots in cooldown. Next available in ~${timeLeft}. Switch backend?`, rows);
12312
+ } else {
12313
+ await channel.sendText(chatId, `\u26A0\uFE0F All eligible slots in cooldown. Next available in ~${timeLeft}.`, "plain");
12314
+ }
12315
+ return;
12316
+ }
12168
12317
  const errorClass = classifyError(err);
12169
12318
  if (errorClass === "exhausted") {
12170
12319
  if (await handleResponseExhaustion(errMsg, chatId, msg, channel)) return;
@@ -12771,6 +12920,73 @@ ${PERM_MODES[chosen]}`,
12771
12920
  await channel.sendText(chatId, `Agent mode set to <b>${mode}</b>. Session cleared.`, "html");
12772
12921
  }
12773
12922
  return;
12923
+ } else if (data.startsWith("grotation:")) {
12924
+ const mode = data.split(":")[1];
12925
+ const validModes = ["off", "all", "accounts", "keys"];
12926
+ if (!validModes.includes(mode)) return;
12927
+ if (mode === "accounts") {
12928
+ const oauthSlots = getGeminiSlots().filter((s) => s.enabled && s.slotType === "oauth");
12929
+ if (oauthSlots.length === 0) {
12930
+ await channel.sendText(chatId, "\u26A0\uFE0F No OAuth accounts configured. Add one with <code>cc-claw gemini add-account</code> or choose a different mode.", "html");
12931
+ return;
12932
+ }
12933
+ } else if (mode === "keys") {
12934
+ const keySlots = getGeminiSlots().filter((s) => s.enabled && s.slotType === "api_key");
12935
+ if (keySlots.length === 0) {
12936
+ await channel.sendText(chatId, "\u26A0\uFE0F No API keys configured. Add one with <code>cc-claw gemini add-key</code> or choose a different mode.", "html");
12937
+ return;
12938
+ }
12939
+ }
12940
+ setGeminiRotationMode(mode);
12941
+ const modeLabels = { off: "Off", all: "All", accounts: "\u{1F468}\u{1F3FD}\u200D\u{1F4BB} Accounts only", keys: "\u{1F511} Keys only" };
12942
+ await channel.sendText(chatId, `Rotation mode set to <b>${modeLabels[mode]}</b>.`, "html");
12943
+ return;
12944
+ } else if (data.startsWith("gslot:")) {
12945
+ const val = data.split(":")[1];
12946
+ if (val === "auto") {
12947
+ clearChatGeminiSlot(chatId);
12948
+ await channel.sendText(chatId, "Gemini slot set to <b>\u{1F504} auto rotation</b>.", "html");
12949
+ } else {
12950
+ const slotId = parseInt(val, 10);
12951
+ const slots = getGeminiSlots();
12952
+ const slot = slots.find((s) => s.id === slotId);
12953
+ if (slot) {
12954
+ pinChatGeminiSlot(chatId, slotId);
12955
+ const label2 = slot.label || `slot-${slot.id}`;
12956
+ const icon = slot.slotType === "oauth" ? "\u{1F468}\u{1F3FD}\u200D\u{1F4BB}" : "\u{1F511}";
12957
+ await channel.sendText(chatId, `Pinned to ${icon} <b>${label2}</b>`, "html");
12958
+ }
12959
+ }
12960
+ return;
12961
+ } else if (data === "gopen:accounts") {
12962
+ const slots = getGeminiSlots();
12963
+ if (slots.length === 0) {
12964
+ await channel.sendText(chatId, "No Gemini credentials configured.\nAdd with: <code>cc-claw gemini add-key</code> or <code>cc-claw gemini add-account</code>", "html");
12965
+ return;
12966
+ }
12967
+ if (typeof channel.sendKeyboard === "function") {
12968
+ const currentMode = getGeminiRotationMode();
12969
+ const pinnedSlotId = getChatGeminiSlotId(chatId);
12970
+ const slotButtons = slots.filter((s) => s.enabled).map((s) => {
12971
+ const label2 = s.label || `slot-${s.id}`;
12972
+ const icon = s.slotType === "oauth" ? "\u{1F468}\u{1F3FD}\u200D\u{1F4BB}" : "\u{1F511}";
12973
+ const marker = pinnedSlotId === s.id ? " \u2713" : "";
12974
+ return { label: `${icon} ${label2}${marker}`, data: `gslot:${s.id}` };
12975
+ });
12976
+ const rows = [];
12977
+ for (let i = 0; i < slotButtons.length; i += 2) {
12978
+ rows.push(slotButtons.slice(i, i + 2));
12979
+ }
12980
+ rows.push([{ label: "\u{1F504} Auto (rotation)", data: "gslot:auto" }]);
12981
+ const modeLabels = { off: "Off", all: "All", accounts: "\u{1F468}\u{1F3FD}\u200D\u{1F4BB} Only", keys: "\u{1F511} Only" };
12982
+ const modeButtons = ["off", "all", "accounts", "keys"].map((m) => ({
12983
+ label: `${m === currentMode ? "\u2713 " : ""}${modeLabels[m]}`,
12984
+ data: `grotation:${m}`
12985
+ }));
12986
+ rows.push(modeButtons);
12987
+ await channel.sendKeyboard(chatId, "Gemini Accounts & Rotation:", rows);
12988
+ }
12989
+ return;
12774
12990
  } else if (data.startsWith("model_sig:")) {
12775
12991
  const value = data.slice(10);
12776
12992
  setModelSignature(chatId, value);
@@ -13033,6 +13249,7 @@ var init_router = __esm({
13033
13249
  init_format_time();
13034
13250
  init_agent();
13035
13251
  init_retry();
13252
+ init_quota();
13036
13253
  init_classify();
13037
13254
  init_version();
13038
13255
  init_image_gen();
@@ -14787,8 +15004,8 @@ async function doctorCommand(globalOpts, localOpts) {
14787
15004
  }
14788
15005
  if (existsSync20(ERROR_LOG_PATH)) {
14789
15006
  try {
14790
- const { readFileSync: readFileSync18 } = await import("fs");
14791
- const logContent = readFileSync18(ERROR_LOG_PATH, "utf-8");
15007
+ const { readFileSync: readFileSync19 } = await import("fs");
15008
+ const logContent = readFileSync19(ERROR_LOG_PATH, "utf-8");
14792
15009
  const recentLines = logContent.split("\n").filter(Boolean).slice(-100);
14793
15010
  const last24h = Date.now() - 864e5;
14794
15011
  const recentErrors = recentLines.filter((line) => {
@@ -14963,9 +15180,10 @@ __export(gemini_exports, {
14963
15180
  geminiEnable: () => geminiEnable,
14964
15181
  geminiList: () => geminiList,
14965
15182
  geminiRemove: () => geminiRemove,
14966
- geminiReorder: () => geminiReorder
15183
+ geminiReorder: () => geminiReorder,
15184
+ geminiRotation: () => geminiRotation
14967
15185
  });
14968
- import { existsSync as existsSync22, mkdirSync as mkdirSync9, writeFileSync as writeFileSync7, chmodSync } from "fs";
15186
+ import { existsSync as existsSync22, mkdirSync as mkdirSync9, writeFileSync as writeFileSync7, readFileSync as readFileSync15, chmodSync } from "fs";
14969
15187
  import { join as join20 } from "path";
14970
15188
  import { createInterface as createInterface4 } from "readline";
14971
15189
  function requireDb() {
@@ -14974,12 +15192,40 @@ function requireDb() {
14974
15192
  process.exit(1);
14975
15193
  }
14976
15194
  }
15195
+ async function requireWriteDb() {
15196
+ requireDb();
15197
+ if (!dbInitialized) {
15198
+ const { initDatabase: initDatabase2 } = await Promise.resolve().then(() => (init_store4(), store_exports3));
15199
+ initDatabase2();
15200
+ dbInitialized = true;
15201
+ }
15202
+ }
15203
+ async function resolveSlotId(idOrLabel) {
15204
+ const numeric = parseInt(idOrLabel, 10);
15205
+ if (!isNaN(numeric)) return numeric;
15206
+ const { getGeminiSlots: getGeminiSlots2 } = await Promise.resolve().then(() => (init_store4(), store_exports3));
15207
+ const match = getGeminiSlots2().find(
15208
+ (s) => s.label?.toLowerCase() === idOrLabel.toLowerCase()
15209
+ );
15210
+ return match?.id ?? null;
15211
+ }
15212
+ function resolveOAuthEmail(configHome) {
15213
+ if (!configHome) return null;
15214
+ try {
15215
+ const accountsPath = join20(configHome, ".gemini", "google_accounts.json");
15216
+ if (!existsSync22(accountsPath)) return null;
15217
+ const accounts = JSON.parse(readFileSync15(accountsPath, "utf-8"));
15218
+ return accounts.active || null;
15219
+ } catch {
15220
+ return null;
15221
+ }
15222
+ }
14977
15223
  async function geminiList(globalOpts) {
14978
15224
  requireDb();
14979
15225
  const { openDatabaseReadOnly: openDatabaseReadOnly2 } = await Promise.resolve().then(() => (init_store4(), store_exports3));
14980
15226
  const readDb = openDatabaseReadOnly2();
14981
15227
  const slots = readDb.prepare(`
14982
- SELECT id, slot_type, label, priority, enabled, cooldown_until, last_used, consecutive_errors, created_at
15228
+ SELECT id, slot_type, label, config_home, priority, enabled, cooldown_until, last_used, consecutive_errors, created_at
14983
15229
  FROM gemini_credentials ORDER BY priority ASC, id ASC
14984
15230
  `).all();
14985
15231
  readDb.close();
@@ -14987,7 +15233,11 @@ async function geminiList(globalOpts) {
14987
15233
  output({ slots: [] }, () => "No Gemini credential slots configured.\nAdd one with: cc-claw gemini add-key or cc-claw gemini add-account");
14988
15234
  return;
14989
15235
  }
14990
- output(slots, (data) => {
15236
+ const enriched = slots.map((s) => ({
15237
+ ...s,
15238
+ email: s.slot_type === "oauth" ? resolveOAuthEmail(s.config_home) : null
15239
+ }));
15240
+ output(enriched, (data) => {
14991
15241
  const list = data;
14992
15242
  const lines = ["", divider("Gemini Credential Slots"), ""];
14993
15243
  for (const s of list) {
@@ -14995,8 +15245,9 @@ async function geminiList(globalOpts) {
14995
15245
  const inCooldown = s.cooldown_until && s.cooldown_until > now;
14996
15246
  const icon = !s.enabled ? error2("\u25CB disabled") : inCooldown ? warning("\u25D1 cooldown") : success("\u25CF active");
14997
15247
  const label2 = s.label || `slot-${s.id}`;
14998
- const type = s.slot_type === "oauth" ? "OAuth" : "API key";
14999
- lines.push(` ${icon} ${label2} ${muted(`(${type}, #${s.id}, priority ${s.priority})`)}`);
15248
+ const type = s.slot_type === "oauth" ? "\u{1F468}\u{1F3FD}\u200D\u{1F4BB}" : "\u{1F511}";
15249
+ const emailStr = s.email ? ` ${muted(s.email)}` : "";
15250
+ lines.push(` ${icon} ${label2}${emailStr} ${muted(`(${type}, #${s.id}, priority ${s.priority})`)}`);
15000
15251
  if (inCooldown) lines.push(` Cooldown until: ${warning(s.cooldown_until)}`);
15001
15252
  if (s.consecutive_errors > 0) lines.push(` Consecutive errors: ${warning(String(s.consecutive_errors))}`);
15002
15253
  if (s.last_used) lines.push(` Last used: ${muted(s.last_used)}`);
@@ -15006,7 +15257,7 @@ async function geminiList(globalOpts) {
15006
15257
  });
15007
15258
  }
15008
15259
  async function geminiAddKey(globalOpts, opts) {
15009
- requireDb();
15260
+ await requireWriteDb();
15010
15261
  const rl2 = createInterface4({ input: process.stdin, output: process.stdout });
15011
15262
  const ask2 = (q) => new Promise((r) => rl2.question(q, r));
15012
15263
  const key = await ask2("Paste your Gemini API key: ");
@@ -15028,7 +15279,7 @@ async function geminiAddKey(globalOpts, opts) {
15028
15279
  );
15029
15280
  }
15030
15281
  async function geminiAddAccount(globalOpts, opts) {
15031
- requireDb();
15282
+ await requireWriteDb();
15032
15283
  const slotsDir = join20(CC_CLAW_HOME, "gemini-slots");
15033
15284
  if (!existsSync22(slotsDir)) mkdirSync9(slotsDir, { recursive: true });
15034
15285
  const { addGeminiSlot: addGeminiSlot2 } = await Promise.resolve().then(() => (init_store4(), store_exports3));
@@ -15079,42 +15330,85 @@ async function geminiAddAccount(globalOpts, opts) {
15079
15330
  Added OAuth slot #${id} (${accountEmail})`)
15080
15331
  );
15081
15332
  }
15082
- async function geminiRemove(globalOpts, id) {
15083
- requireDb();
15333
+ async function geminiRemove(globalOpts, idOrLabel) {
15334
+ await requireWriteDb();
15335
+ const slotId = await resolveSlotId(idOrLabel);
15336
+ if (!slotId) {
15337
+ outputError("NOT_FOUND", `Slot "${idOrLabel}" not found.`);
15338
+ return;
15339
+ }
15084
15340
  const { removeGeminiSlot: removeGeminiSlot2 } = await Promise.resolve().then(() => (init_store4(), store_exports3));
15085
- const removed = removeGeminiSlot2(parseInt(id, 10));
15341
+ const removed = removeGeminiSlot2(slotId);
15086
15342
  if (removed) {
15087
- output({ removed: true, id: parseInt(id, 10) }, () => success(`Removed slot #${id}`));
15343
+ output({ removed: true, id: slotId }, () => success(`Removed slot "${idOrLabel}" (#${slotId})`));
15088
15344
  } else {
15089
- outputError("NOT_FOUND", `Slot #${id} not found.`);
15345
+ outputError("NOT_FOUND", `Slot "${idOrLabel}" not found.`);
15090
15346
  }
15091
15347
  }
15092
- async function geminiEnable(globalOpts, id) {
15093
- requireDb();
15348
+ async function geminiEnable(globalOpts, idOrLabel) {
15349
+ await requireWriteDb();
15350
+ const slotId = await resolveSlotId(idOrLabel);
15351
+ if (!slotId) {
15352
+ outputError("NOT_FOUND", `Slot "${idOrLabel}" not found.`);
15353
+ return;
15354
+ }
15094
15355
  const { setGeminiSlotEnabled: setGeminiSlotEnabled2 } = await Promise.resolve().then(() => (init_store4(), store_exports3));
15095
- setGeminiSlotEnabled2(parseInt(id, 10), true);
15096
- output({ id: parseInt(id, 10), enabled: true }, () => success(`Enabled slot #${id}`));
15097
- }
15098
- async function geminiDisable(globalOpts, id) {
15099
- requireDb();
15356
+ setGeminiSlotEnabled2(slotId, true);
15357
+ output({ id: slotId, enabled: true }, () => success(`Enabled slot "${idOrLabel}" (#${slotId})`));
15358
+ }
15359
+ async function geminiDisable(globalOpts, idOrLabel) {
15360
+ await requireWriteDb();
15361
+ const slotId = await resolveSlotId(idOrLabel);
15362
+ if (!slotId) {
15363
+ outputError("NOT_FOUND", `Slot "${idOrLabel}" not found.`);
15364
+ return;
15365
+ }
15100
15366
  const { setGeminiSlotEnabled: setGeminiSlotEnabled2 } = await Promise.resolve().then(() => (init_store4(), store_exports3));
15101
- setGeminiSlotEnabled2(parseInt(id, 10), false);
15102
- output({ id: parseInt(id, 10), enabled: false }, () => warning(`Disabled slot #${id}`));
15103
- }
15104
- async function geminiReorder(globalOpts, id, priority) {
15105
- requireDb();
15367
+ setGeminiSlotEnabled2(slotId, false);
15368
+ output({ id: slotId, enabled: false }, () => warning(`Disabled slot "${idOrLabel}" (#${slotId})`));
15369
+ }
15370
+ async function geminiReorder(globalOpts, idOrLabel, priority) {
15371
+ await requireWriteDb();
15372
+ const slotId = await resolveSlotId(idOrLabel);
15373
+ if (!slotId) {
15374
+ outputError("NOT_FOUND", `Slot "${idOrLabel}" not found.`);
15375
+ return;
15376
+ }
15106
15377
  const { reorderGeminiSlot: reorderGeminiSlot2 } = await Promise.resolve().then(() => (init_store4(), store_exports3));
15107
- reorderGeminiSlot2(parseInt(id, 10), parseInt(priority, 10));
15378
+ reorderGeminiSlot2(slotId, parseInt(priority, 10));
15108
15379
  output(
15109
- { id: parseInt(id, 10), priority: parseInt(priority, 10) },
15110
- () => success(`Slot #${id} priority set to ${priority}`)
15380
+ { id: slotId, priority: parseInt(priority, 10) },
15381
+ () => success(`Slot "${idOrLabel}" (#${slotId}) priority set to ${priority}`)
15111
15382
  );
15112
15383
  }
15384
+ async function geminiRotation(globalOpts, mode) {
15385
+ const validModes = ["off", "all", "accounts", "keys"];
15386
+ if (!mode) {
15387
+ requireDb();
15388
+ const { openDatabaseReadOnly: openDatabaseReadOnly2 } = await Promise.resolve().then(() => (init_store4(), store_exports3));
15389
+ const readDb = openDatabaseReadOnly2();
15390
+ const row = readDb.prepare("SELECT value FROM meta WHERE key = 'gemini_rotation_mode'").get();
15391
+ readDb.close();
15392
+ const current = row?.value ?? "all";
15393
+ output({ mode: current }, () => `Gemini rotation mode: ${success(current)}`);
15394
+ return;
15395
+ }
15396
+ if (!validModes.includes(mode)) {
15397
+ outputError("INVALID_MODE", `Invalid mode: "${mode}". Valid modes: ${validModes.join(", ")}`);
15398
+ process.exit(1);
15399
+ }
15400
+ await requireWriteDb();
15401
+ const { setGeminiRotationMode: setGeminiRotationMode2 } = await Promise.resolve().then(() => (init_store4(), store_exports3));
15402
+ setGeminiRotationMode2(mode);
15403
+ output({ mode }, () => success(`Gemini rotation mode set to "${mode}"`));
15404
+ }
15405
+ var dbInitialized;
15113
15406
  var init_gemini2 = __esm({
15114
15407
  "src/cli/commands/gemini.ts"() {
15115
15408
  "use strict";
15116
15409
  init_format();
15117
15410
  init_paths();
15411
+ dbInitialized = false;
15118
15412
  }
15119
15413
  });
15120
15414
 
@@ -16106,7 +16400,7 @@ __export(config_exports, {
16106
16400
  configList: () => configList,
16107
16401
  configSet: () => configSet
16108
16402
  });
16109
- import { existsSync as existsSync30, readFileSync as readFileSync15 } from "fs";
16403
+ import { existsSync as existsSync30, readFileSync as readFileSync16 } from "fs";
16110
16404
  async function configList(globalOpts) {
16111
16405
  if (!existsSync30(DB_PATH)) {
16112
16406
  outputError("DB_NOT_FOUND", "Database not found.");
@@ -16192,7 +16486,7 @@ async function configEnv(_globalOpts) {
16192
16486
  outputError("ENV_NOT_FOUND", `No .env file at ${ENV_PATH}. Run cc-claw setup.`);
16193
16487
  process.exit(1);
16194
16488
  }
16195
- const content = readFileSync15(ENV_PATH, "utf-8");
16489
+ const content = readFileSync16(ENV_PATH, "utf-8");
16196
16490
  const entries = {};
16197
16491
  const secretPatterns = /TOKEN|KEY|SECRET|PASSWORD|CREDENTIALS/i;
16198
16492
  for (const line of content.split("\n")) {
@@ -16850,11 +17144,11 @@ __export(chat_exports, {
16850
17144
  chatSend: () => chatSend
16851
17145
  });
16852
17146
  import { request as httpRequest2 } from "http";
16853
- import { readFileSync as readFileSync16, existsSync as existsSync38 } from "fs";
17147
+ import { readFileSync as readFileSync17, existsSync as existsSync38 } from "fs";
16854
17148
  function getToken2() {
16855
17149
  if (process.env.CC_CLAW_API_TOKEN) return process.env.CC_CLAW_API_TOKEN;
16856
17150
  try {
16857
- if (existsSync38(TOKEN_PATH2)) return readFileSync16(TOKEN_PATH2, "utf-8").trim();
17151
+ if (existsSync38(TOKEN_PATH2)) return readFileSync17(TOKEN_PATH2, "utf-8").trim();
16858
17152
  } catch {
16859
17153
  }
16860
17154
  return null;
@@ -17280,7 +17574,7 @@ var init_completion = __esm({
17280
17574
 
17281
17575
  // src/setup.ts
17282
17576
  var setup_exports = {};
17283
- import { existsSync as existsSync39, writeFileSync as writeFileSync8, readFileSync as readFileSync17, copyFileSync as copyFileSync3, mkdirSync as mkdirSync11, statSync as statSync6 } from "fs";
17577
+ import { existsSync as existsSync39, writeFileSync as writeFileSync8, readFileSync as readFileSync18, copyFileSync as copyFileSync3, mkdirSync as mkdirSync11, statSync as statSync6 } from "fs";
17284
17578
  import { execFileSync as execFileSync4 } from "child_process";
17285
17579
  import { createInterface as createInterface6 } from "readline";
17286
17580
  import { join as join21 } from "path";
@@ -17364,7 +17658,7 @@ async function setup() {
17364
17658
  if (envSource) {
17365
17659
  console.log(yellow(` Found existing config at ${envSource} \u2014 your values will be preserved`));
17366
17660
  console.log(yellow(" unless you enter new ones. Just press Enter to keep existing values.\n"));
17367
- const existing = readFileSync17(envSource, "utf-8");
17661
+ const existing = readFileSync18(envSource, "utf-8");
17368
17662
  for (const line of existing.split("\n")) {
17369
17663
  const match = line.match(/^([^#=]+)=(.*)$/);
17370
17664
  if (match) env[match[1].trim()] = match[2].trim();
@@ -17722,22 +18016,26 @@ gemini.command("add-account").description("Add an OAuth account slot (opens brow
17722
18016
  const { geminiAddAccount: geminiAddAccount2 } = await Promise.resolve().then(() => (init_gemini2(), gemini_exports));
17723
18017
  await geminiAddAccount2(program.opts(), opts);
17724
18018
  });
17725
- gemini.command("remove <id>").description("Remove a credential slot").action(async (id) => {
18019
+ gemini.command("remove <id-or-label>").description("Remove a credential slot (by ID or label)").action(async (id) => {
17726
18020
  const { geminiRemove: geminiRemove2 } = await Promise.resolve().then(() => (init_gemini2(), gemini_exports));
17727
18021
  await geminiRemove2(program.opts(), id);
17728
18022
  });
17729
- gemini.command("enable <id>").description("Re-enable a disabled slot").action(async (id) => {
18023
+ gemini.command("enable <id-or-label>").description("Re-enable a disabled slot (by ID or label)").action(async (id) => {
17730
18024
  const { geminiEnable: geminiEnable2 } = await Promise.resolve().then(() => (init_gemini2(), gemini_exports));
17731
18025
  await geminiEnable2(program.opts(), id);
17732
18026
  });
17733
- gemini.command("disable <id>").description("Disable a slot (skip during rotation)").action(async (id) => {
18027
+ gemini.command("disable <id-or-label>").description("Disable a slot (by ID or label)").action(async (id) => {
17734
18028
  const { geminiDisable: geminiDisable2 } = await Promise.resolve().then(() => (init_gemini2(), gemini_exports));
17735
18029
  await geminiDisable2(program.opts(), id);
17736
18030
  });
17737
- gemini.command("reorder <id> <priority>").description("Set slot priority (lower = preferred)").action(async (id, priority) => {
18031
+ gemini.command("reorder <id-or-label> <priority>").description("Set slot priority (by ID or label, lower = preferred)").action(async (id, priority) => {
17738
18032
  const { geminiReorder: geminiReorder2 } = await Promise.resolve().then(() => (init_gemini2(), gemini_exports));
17739
18033
  await geminiReorder2(program.opts(), id, priority);
17740
18034
  });
18035
+ gemini.command("rotation [mode]").description("Get or set rotation mode (off, all, accounts, keys)").action(async (mode) => {
18036
+ const { geminiRotation: geminiRotation2 } = await Promise.resolve().then(() => (init_gemini2(), gemini_exports));
18037
+ await geminiRotation2(program.opts(), mode);
18038
+ });
17741
18039
  var backend = program.command("backend").description("Manage AI backend");
17742
18040
  backend.command("list").description("Available backends with status").action(async () => {
17743
18041
  const { backendList: backendList2 } = await Promise.resolve().then(() => (init_backend(), backend_exports));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-claw",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "CC-Claw: Personal AI assistant on Telegram — multi-backend (Claude, Gemini, Codex), sub-agent orchestration, MCP management",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",