cc-claw 0.6.1 → 0.7.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.
Files changed (2) hide show
  1. package/dist/cli.js +396 -74
  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.1" : (() => {
58
+ VERSION = true ? "0.7.1" : (() => {
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) {
@@ -3894,6 +3919,16 @@ var init_loader = __esm({
3894
3919
  });
3895
3920
 
3896
3921
  // src/memory/session-log.ts
3922
+ var session_log_exports = {};
3923
+ __export(session_log_exports, {
3924
+ appendToLog: () => appendToLog,
3925
+ clearLog: () => clearLog,
3926
+ getCachedLog: () => getCachedLog,
3927
+ getLastMessageTimestamp: () => getLastMessageTimestamp,
3928
+ getLog: () => getLog,
3929
+ getLoggedChatIds: () => getLoggedChatIds,
3930
+ getMessagePairCount: () => getMessagePairCount
3931
+ });
3897
3932
  function appendToLog(chatId, userMessage, assistantResponse, backend2, model2, sessionId) {
3898
3933
  const now = Date.now();
3899
3934
  appendMessageLog(chatId, "user", userMessage, backend2 ?? null, model2 ?? null, sessionId ?? null);
@@ -3937,6 +3972,9 @@ function getLastMessageTimestamp(chatId) {
3937
3972
  const last = rows[rows.length - 1];
3938
3973
  return (/* @__PURE__ */ new Date(last.created_at + (last.created_at.includes("Z") ? "" : "Z"))).getTime();
3939
3974
  }
3975
+ function getCachedLog(chatId) {
3976
+ return cache.get(chatId) ?? [];
3977
+ }
3940
3978
  function getLoggedChatIds() {
3941
3979
  return getUnsummarizedChatIds();
3942
3980
  }
@@ -4073,7 +4111,7 @@ Key details: ${keyDetails}`;
4073
4111
  return false;
4074
4112
  }
4075
4113
  }
4076
- async function summarizeWithFallbackChain(chatId, targetBackendId) {
4114
+ async function summarizeWithFallbackChain(chatId, targetBackendId, excludeBackend) {
4077
4115
  const pairCount = getMessagePairCount(chatId);
4078
4116
  if (pairCount < MIN_PAIRS) {
4079
4117
  clearLog(chatId);
@@ -4082,6 +4120,10 @@ async function summarizeWithFallbackChain(chatId, targetBackendId) {
4082
4120
  const entries = getLog(chatId);
4083
4121
  if (entries.length === 0) return false;
4084
4122
  const tried = /* @__PURE__ */ new Set();
4123
+ if (excludeBackend) {
4124
+ const excluded = getAdapter(excludeBackend);
4125
+ tried.add(`${excluded.id}:${excluded.summarizerModel}`);
4126
+ }
4085
4127
  try {
4086
4128
  const config2 = getSummarizer(chatId);
4087
4129
  if (config2.backend !== "off") {
@@ -4193,6 +4235,9 @@ Be specific. The new assistant has no prior context. Cover any domain \u2014 per
4193
4235
 
4194
4236
  // src/gemini/quota.ts
4195
4237
  function classifyGeminiQuota(errorText) {
4238
+ for (const p of CAPACITY_PATTERNS) {
4239
+ if (p.test(errorText)) return "rate_limited";
4240
+ }
4196
4241
  for (const p of RATE_LIMITED_PATTERNS) {
4197
4242
  if (p.test(errorText)) return "rate_limited";
4198
4243
  }
@@ -4204,7 +4249,7 @@ function classifyGeminiQuota(errorText) {
4204
4249
  }
4205
4250
  return "unknown";
4206
4251
  }
4207
- var RATE_LIMITED_PATTERNS, BILLING_PATTERNS, DAILY_QUOTA_PATTERNS, GEMINI_SLOTS_EXHAUSTED_MSG;
4252
+ 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
4253
  var init_quota = __esm({
4209
4254
  "src/gemini/quota.ts"() {
4210
4255
  "use strict";
@@ -4227,7 +4272,13 @@ var init_quota = __esm({
4227
4272
  /RESOURCE_EXHAUSTED/,
4228
4273
  /exhausted.*quota/i
4229
4274
  ];
4275
+ CAPACITY_PATTERNS = [
4276
+ /MODEL_CAPACITY_EXHAUSTED/,
4277
+ /No capacity available/i
4278
+ ];
4230
4279
  GEMINI_SLOTS_EXHAUSTED_MSG = "Gemini usage limit \u2014 all credential slots exhausted";
4280
+ GEMINI_NO_SLOTS_FOR_MODE_MSG = "Gemini rotation \u2014 no eligible slots for current rotation mode";
4281
+ GEMINI_ALL_SLOTS_COOLDOWN_MSG = "Gemini rotation \u2014 all eligible slots in cooldown";
4231
4282
  }
4232
4283
  });
4233
4284
 
@@ -6737,22 +6788,23 @@ function spawnQuery(adapter, config2, model2, cancelState, thinkingLevel, timeou
6737
6788
  });
6738
6789
  });
6739
6790
  }
6740
- async function spawnGeminiWithRotation(chatId, adapter, config2, model2, cancelState, thinkingLevel, timeoutMs, maxTurns, opts, onSlotRotation) {
6791
+ async function spawnGeminiWithRotation(chatId, adapter, baseConfig, configWithSession, model2, cancelState, thinkingLevel, timeoutMs, maxTurns, rotationMode, opts, onSlotRotation) {
6741
6792
  const geminiAdapter = adapter;
6742
- const slots = getEligibleGeminiSlots();
6793
+ const slots = getEligibleGeminiSlots(rotationMode);
6743
6794
  if (slots.length === 0) {
6744
- return spawnQuery(adapter, config2, model2, cancelState, thinkingLevel, timeoutMs, maxTurns, opts);
6795
+ return spawnQuery(adapter, configWithSession, model2, cancelState, thinkingLevel, timeoutMs, maxTurns, opts);
6745
6796
  }
6746
6797
  const maxAttempts = Math.min(slots.length, 10);
6747
6798
  let lastError;
6748
6799
  for (let i = 0; i < maxAttempts; i++) {
6749
- const { env, slot } = geminiAdapter.getEnvForSlot(chatId, void 0);
6800
+ const { env, slot } = geminiAdapter.getEnvForSlot(chatId, void 0, rotationMode);
6750
6801
  if (!slot) break;
6751
6802
  const slotLabel = slot.label || `slot-${slot.id}`;
6752
6803
  log(`[agent:gemini-rotation] Trying ${slotLabel} (${slot.slotType}, attempt ${i + 1}/${maxAttempts})`);
6753
6804
  if (i === 0) pinChatGeminiSlot(chatId, slot.id);
6805
+ const effectiveConfig = i === 0 ? configWithSession : baseConfig;
6754
6806
  try {
6755
- const result = await spawnQuery(adapter, config2, model2, cancelState, thinkingLevel, timeoutMs, maxTurns, { ...opts, envOverride: env });
6807
+ const result = await spawnQuery(adapter, effectiveConfig, model2, cancelState, thinkingLevel, timeoutMs, maxTurns, { ...opts, envOverride: env });
6756
6808
  const combinedText = result.resultText || "";
6757
6809
  if (combinedText && /RESOURCE.?EXHAUSTED|resource has been exhausted/i.test(combinedText)) {
6758
6810
  throw new Error(combinedText);
@@ -6770,11 +6822,11 @@ async function spawnGeminiWithRotation(chatId, adapter, config2, model2, cancelS
6770
6822
  markSlotExhausted(slot.id, effectiveClass);
6771
6823
  lastError = err instanceof Error ? err : new Error(errMsg);
6772
6824
  try {
6773
- await summarizeWithFallbackChain(chatId);
6825
+ await summarizeWithFallbackChain(chatId, void 0, "gemini");
6774
6826
  clearSession(chatId);
6775
6827
  } catch {
6776
6828
  }
6777
- const nextSlot = getEligibleGeminiSlots()[0];
6829
+ const nextSlot = getEligibleGeminiSlots(rotationMode)[0];
6778
6830
  if (nextSlot && onSlotRotation) {
6779
6831
  const nextLabel = nextSlot.label || `slot-${nextSlot.id}`;
6780
6832
  onSlotRotation(slotLabel, nextLabel);
@@ -6782,6 +6834,20 @@ async function spawnGeminiWithRotation(chatId, adapter, config2, model2, cancelS
6782
6834
  }
6783
6835
  }
6784
6836
  }
6837
+ const allSlotsForMode = getGeminiSlots().filter((s) => {
6838
+ if (!s.enabled) return false;
6839
+ if (rotationMode === "accounts") return s.slotType === "oauth";
6840
+ if (rotationMode === "keys") return s.slotType === "api_key";
6841
+ return true;
6842
+ });
6843
+ if (allSlotsForMode.length === 0) {
6844
+ throw new Error(GEMINI_NO_SLOTS_FOR_MODE_MSG);
6845
+ }
6846
+ const soonest = allSlotsForMode.filter((s) => s.cooldownUntil).map((s) => new Date(s.cooldownUntil).getTime()).sort((a, b) => a - b)[0];
6847
+ if (soonest) {
6848
+ const hoursLeft = Math.max(1, Math.ceil((soonest - Date.now()) / 36e5));
6849
+ throw new Error(`${GEMINI_ALL_SLOTS_COOLDOWN_MSG}|${hoursLeft}h`);
6850
+ }
6785
6851
  throw new Error(GEMINI_SLOTS_EXHAUSTED_MSG);
6786
6852
  }
6787
6853
  function askAgent(chatId, userMessage, opts) {
@@ -6833,11 +6899,12 @@ async function askAgentImpl(chatId, userMessage, opts) {
6833
6899
  const spawnOpts = { onStream, onToolAction, onSubagentActivity };
6834
6900
  const resolvedModel = model2 ?? adapter.defaultModel;
6835
6901
  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;
6902
+ const rotationMode = adapter.id === "gemini" ? getGeminiRotationMode() : "off";
6903
+ const useGeminiRotation = rotationMode !== "off" && getEligibleGeminiSlots(rotationMode).length > 0;
6837
6904
  try {
6838
6905
  if (useGeminiRotation) {
6839
6906
  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);
6907
+ result = await spawnGeminiWithRotation(chatId, adapter, baseConfig, configWithSession, resolvedModel, cancelState, thinkingLevel, timeoutMs, maxTurns, rotationMode, spawnOpts, rotationCb);
6841
6908
  } else {
6842
6909
  result = await spawnQuery(adapter, configWithSession, resolvedModel, cancelState, thinkingLevel, timeoutMs, maxTurns, spawnOpts);
6843
6910
  }
@@ -6848,7 +6915,7 @@ async function askAgentImpl(chatId, userMessage, opts) {
6848
6915
  clearSession(chatId);
6849
6916
  if (useGeminiRotation) {
6850
6917
  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);
6918
+ result = await spawnGeminiWithRotation(chatId, adapter, baseConfig, baseConfig, resolvedModel, cancelState, thinkingLevel, timeoutMs, maxTurns, rotationMode, spawnOpts, rotationCb);
6852
6919
  } else {
6853
6920
  result = await spawnQuery(adapter, baseConfig, resolvedModel, cancelState, thinkingLevel, timeoutMs, maxTurns, spawnOpts);
6854
6921
  }
@@ -6877,7 +6944,10 @@ async function askAgentImpl(chatId, userMessage, opts) {
6877
6944
  if (pairCount >= AUTO_SUMMARIZE_THRESHOLD) {
6878
6945
  log(`[agent] Auto-summarizing chat ${chatId} after ${pairCount} turns`);
6879
6946
  summarizeWithFallbackChain(chatId).then((saved) => {
6880
- if (saved) opts?.onCompaction?.(chatId);
6947
+ if (saved) {
6948
+ clearSession(chatId);
6949
+ opts?.onCompaction?.(chatId);
6950
+ }
6881
6951
  }).catch((err) => {
6882
6952
  warn(`[agent] Auto-summarize failed for chat ${chatId}: ${err}`);
6883
6953
  });
@@ -10588,7 +10658,7 @@ async function handleCommand(msg, channel) {
10588
10658
  case "help":
10589
10659
  await channel.sendText(
10590
10660
  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",
10661
+ "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_accounts - Manage Gemini credentials & rotation\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
10662
  "plain"
10593
10663
  );
10594
10664
  break;
@@ -10751,12 +10821,24 @@ Tap to toggle:`,
10751
10821
  const iStats = getIntentStats();
10752
10822
  const iTotal = iStats.chat + iStats.agentic;
10753
10823
  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`;
10824
+ let geminiSlotInfo = "";
10825
+ if ((backendId ?? "claude") === "gemini") {
10826
+ try {
10827
+ const active = getNextGeminiSlot(chatId);
10828
+ const totalSlots = getEligibleGeminiSlots().length;
10829
+ if (active) {
10830
+ const label2 = active.label || `slot-${active.id}`;
10831
+ geminiSlotInfo = ` \xB7 \u{1F511} ${label2} (${totalSlots} slots)`;
10832
+ }
10833
+ } catch {
10834
+ }
10835
+ }
10754
10836
  const lines = [
10755
10837
  `\u{1F43E} CC-Claw v${VERSION}`,
10756
10838
  `\u23F1 Uptime: ${uptimeStr}`,
10757
10839
  ``,
10758
10840
  `\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)}`,
10841
+ `\u{1F9E0} ${adapter?.displayName ?? backendId ?? "not set"} \xB7 ${formatModelShort(model2)}${geminiSlotInfo}`,
10760
10842
  `\u{1F4AD} Think: ${thinking2} \xB7 Mode: ${mode}`,
10761
10843
  `\u{1F916} Agents: ${getAgentMode(chatId)}`,
10762
10844
  `\u{1F507} Voice: ${voice2 ? "on" : "off"} \xB7 Sig: ${modelSig}`,
@@ -10800,7 +10882,6 @@ Tap to toggle:`,
10800
10882
  break;
10801
10883
  }
10802
10884
  case "claude":
10803
- case "gemini":
10804
10885
  case "codex": {
10805
10886
  const backendId = command;
10806
10887
  if (getAllBackendIds().includes(backendId)) {
@@ -10810,6 +10891,56 @@ Tap to toggle:`,
10810
10891
  }
10811
10892
  break;
10812
10893
  }
10894
+ case "gemini_accounts": {
10895
+ const slots = getGeminiSlots();
10896
+ if (slots.length === 0) {
10897
+ 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");
10898
+ break;
10899
+ }
10900
+ if (typeof channel.sendKeyboard === "function") {
10901
+ const currentMode = getGeminiRotationMode();
10902
+ const pinnedSlotId = getChatGeminiSlotId(chatId);
10903
+ const slotButtons = slots.filter((s) => s.enabled).map((s) => {
10904
+ const label2 = s.label || `slot-${s.id}`;
10905
+ const icon = s.slotType === "oauth" ? "\u{1F468}\u{1F3FD}\u200D\u{1F4BB}" : "\u{1F511}";
10906
+ const marker = pinnedSlotId === s.id ? " \u2713" : "";
10907
+ return { label: `${icon} ${label2}${marker}`, data: `gslot:${s.id}` };
10908
+ });
10909
+ const rows = [];
10910
+ for (let i = 0; i < slotButtons.length; i += 2) {
10911
+ rows.push(slotButtons.slice(i, i + 2));
10912
+ }
10913
+ rows.push([{ label: "\u{1F504} Auto (rotation)", data: "gslot:auto" }]);
10914
+ const modeLabels = { off: "Off", all: "All", accounts: "\u{1F468}\u{1F3FD}\u200D\u{1F4BB} Only", keys: "\u{1F511} Only" };
10915
+ const modeButtons = ["off", "all", "accounts", "keys"].map((m) => ({
10916
+ label: `${m === currentMode ? "\u2713 " : ""}${modeLabels[m]}`,
10917
+ data: `grotation:${m}`
10918
+ }));
10919
+ rows.push(modeButtons);
10920
+ await channel.sendKeyboard(chatId, "Gemini Accounts & Rotation:", rows);
10921
+ } else {
10922
+ const currentMode = getGeminiRotationMode();
10923
+ const list = slots.filter((s) => s.enabled).map((s) => {
10924
+ const icon = s.slotType === "oauth" ? "\u{1F468}\u{1F3FD}\u200D\u{1F4BB}" : "\u{1F511}";
10925
+ return `${icon} ${s.label || `slot-${s.id}`} (#${s.id})`;
10926
+ }).join("\n");
10927
+ await channel.sendText(chatId, `Slots:
10928
+ ${list}
10929
+
10930
+ Rotation mode: ${currentMode}
10931
+ Use: /gemini_accounts <name> to pin`, "plain");
10932
+ }
10933
+ break;
10934
+ }
10935
+ case "gemini": {
10936
+ const backendId = command;
10937
+ if (getAllBackendIds().includes(backendId)) {
10938
+ await sendBackendSwitchConfirmation(chatId, backendId, channel);
10939
+ } else {
10940
+ await channel.sendText(chatId, `Backend "${command}" is not available.`, "plain");
10941
+ }
10942
+ break;
10943
+ }
10813
10944
  case "model": {
10814
10945
  let adapter;
10815
10946
  try {
@@ -12125,7 +12256,12 @@ async function handleText(msg, channel) {
12125
12256
  });
12126
12257
  },
12127
12258
  onSlotRotation: (cid, from, to) => {
12128
- channel.sendText(cid, `\u{1F511} Gemini quota reached on ${from} \u2014 continuing on ${to}. Context saved.`).catch(() => {
12259
+ const slots = getGeminiSlots();
12260
+ const fromSlot = slots.find((s) => (s.label || `slot-${s.id}`) === from);
12261
+ const toSlot = slots.find((s) => (s.label || `slot-${s.id}`) === to);
12262
+ const fromIcon = fromSlot?.slotType === "oauth" ? "\u{1F468}\u{1F3FD}\u200D\u{1F4BB}" : "\u{1F511}";
12263
+ const toIcon = toSlot?.slotType === "oauth" ? "\u{1F468}\u{1F3FD}\u200D\u{1F4BB}" : "\u{1F511}";
12264
+ channel.sendText(cid, `\u26A0\uFE0F Quota reached on ${fromIcon} ${from} \u2014 switched to ${toIcon} ${to}. Context saved.`, "plain").catch(() => {
12129
12265
  });
12130
12266
  }
12131
12267
  });
@@ -12165,6 +12301,32 @@ async function handleText(msg, channel) {
12165
12301
  } catch (err) {
12166
12302
  error("[router] Error:", err);
12167
12303
  const errMsg = errorMessage(err);
12304
+ if (errMsg.includes(GEMINI_NO_SLOTS_FOR_MODE_MSG)) {
12305
+ const mode = getGeminiRotationMode();
12306
+ const modeLabel = mode === "accounts" ? "\u{1F468}\u{1F3FD}\u200D\u{1F4BB} Only" : mode === "keys" ? "\u{1F511} Only" : mode;
12307
+ if (typeof channel.sendKeyboard === "function") {
12308
+ const rows = [
12309
+ [{ label: "Claude", data: "backend:claude" }, { label: "Codex", data: "backend:codex" }],
12310
+ [{ label: "\u2699\uFE0F Change rotation mode", data: "gopen:accounts" }]
12311
+ ];
12312
+ await channel.sendKeyboard(chatId, `\u26A0\uFE0F No eligible slots for rotation mode (${modeLabel}). Switch backend?`, rows);
12313
+ } else {
12314
+ await channel.sendText(chatId, `\u26A0\uFE0F No eligible slots for rotation mode (${modeLabel}).`, "plain");
12315
+ }
12316
+ return;
12317
+ } else if (errMsg.includes(GEMINI_ALL_SLOTS_COOLDOWN_MSG)) {
12318
+ const timeLeft = errMsg.split("|")[1] || "?";
12319
+ if (typeof channel.sendKeyboard === "function") {
12320
+ const rows = [
12321
+ [{ label: "Claude", data: "backend:claude" }, { label: "Codex", data: "backend:codex" }],
12322
+ [{ label: "\u2699\uFE0F Change rotation mode", data: "gopen:accounts" }]
12323
+ ];
12324
+ await channel.sendKeyboard(chatId, `\u26A0\uFE0F All eligible slots in cooldown. Next available in ~${timeLeft}. Switch backend?`, rows);
12325
+ } else {
12326
+ await channel.sendText(chatId, `\u26A0\uFE0F All eligible slots in cooldown. Next available in ~${timeLeft}.`, "plain");
12327
+ }
12328
+ return;
12329
+ }
12168
12330
  const errorClass = classifyError(err);
12169
12331
  if (errorClass === "exhausted") {
12170
12332
  if (await handleResponseExhaustion(errMsg, chatId, msg, channel)) return;
@@ -12499,7 +12661,7 @@ async function doBackendSwitch(chatId, backendId, channel) {
12499
12661
  }
12500
12662
  if (summarized) {
12501
12663
  await channel.sendText(chatId, "\u{1F4BE} Context saved \u2014 session summarized to memory.", "plain");
12502
- } else if (pairCount > 0 && bridge) {
12664
+ } else if (bridge) {
12503
12665
  await channel.sendText(chatId, "\u{1F4AC} Context preserved.", "plain");
12504
12666
  }
12505
12667
  clearSession(chatId);
@@ -12771,6 +12933,73 @@ ${PERM_MODES[chosen]}`,
12771
12933
  await channel.sendText(chatId, `Agent mode set to <b>${mode}</b>. Session cleared.`, "html");
12772
12934
  }
12773
12935
  return;
12936
+ } else if (data.startsWith("grotation:")) {
12937
+ const mode = data.split(":")[1];
12938
+ const validModes = ["off", "all", "accounts", "keys"];
12939
+ if (!validModes.includes(mode)) return;
12940
+ if (mode === "accounts") {
12941
+ const oauthSlots = getGeminiSlots().filter((s) => s.enabled && s.slotType === "oauth");
12942
+ if (oauthSlots.length === 0) {
12943
+ 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");
12944
+ return;
12945
+ }
12946
+ } else if (mode === "keys") {
12947
+ const keySlots = getGeminiSlots().filter((s) => s.enabled && s.slotType === "api_key");
12948
+ if (keySlots.length === 0) {
12949
+ 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");
12950
+ return;
12951
+ }
12952
+ }
12953
+ setGeminiRotationMode(mode);
12954
+ const modeLabels = { off: "Off", all: "All", accounts: "\u{1F468}\u{1F3FD}\u200D\u{1F4BB} Accounts only", keys: "\u{1F511} Keys only" };
12955
+ await channel.sendText(chatId, `Rotation mode set to <b>${modeLabels[mode]}</b>.`, "html");
12956
+ return;
12957
+ } else if (data.startsWith("gslot:")) {
12958
+ const val = data.split(":")[1];
12959
+ if (val === "auto") {
12960
+ clearChatGeminiSlot(chatId);
12961
+ await channel.sendText(chatId, "Gemini slot set to <b>\u{1F504} auto rotation</b>.", "html");
12962
+ } else {
12963
+ const slotId = parseInt(val, 10);
12964
+ const slots = getGeminiSlots();
12965
+ const slot = slots.find((s) => s.id === slotId);
12966
+ if (slot) {
12967
+ pinChatGeminiSlot(chatId, slotId);
12968
+ const label2 = slot.label || `slot-${slot.id}`;
12969
+ const icon = slot.slotType === "oauth" ? "\u{1F468}\u{1F3FD}\u200D\u{1F4BB}" : "\u{1F511}";
12970
+ await channel.sendText(chatId, `Pinned to ${icon} <b>${label2}</b>`, "html");
12971
+ }
12972
+ }
12973
+ return;
12974
+ } else if (data === "gopen:accounts") {
12975
+ const slots = getGeminiSlots();
12976
+ if (slots.length === 0) {
12977
+ 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");
12978
+ return;
12979
+ }
12980
+ if (typeof channel.sendKeyboard === "function") {
12981
+ const currentMode = getGeminiRotationMode();
12982
+ const pinnedSlotId = getChatGeminiSlotId(chatId);
12983
+ const slotButtons = slots.filter((s) => s.enabled).map((s) => {
12984
+ const label2 = s.label || `slot-${s.id}`;
12985
+ const icon = s.slotType === "oauth" ? "\u{1F468}\u{1F3FD}\u200D\u{1F4BB}" : "\u{1F511}";
12986
+ const marker = pinnedSlotId === s.id ? " \u2713" : "";
12987
+ return { label: `${icon} ${label2}${marker}`, data: `gslot:${s.id}` };
12988
+ });
12989
+ const rows = [];
12990
+ for (let i = 0; i < slotButtons.length; i += 2) {
12991
+ rows.push(slotButtons.slice(i, i + 2));
12992
+ }
12993
+ rows.push([{ label: "\u{1F504} Auto (rotation)", data: "gslot:auto" }]);
12994
+ const modeLabels = { off: "Off", all: "All", accounts: "\u{1F468}\u{1F3FD}\u200D\u{1F4BB} Only", keys: "\u{1F511} Only" };
12995
+ const modeButtons = ["off", "all", "accounts", "keys"].map((m) => ({
12996
+ label: `${m === currentMode ? "\u2713 " : ""}${modeLabels[m]}`,
12997
+ data: `grotation:${m}`
12998
+ }));
12999
+ rows.push(modeButtons);
13000
+ await channel.sendKeyboard(chatId, "Gemini Accounts & Rotation:", rows);
13001
+ }
13002
+ return;
12774
13003
  } else if (data.startsWith("model_sig:")) {
12775
13004
  const value = data.slice(10);
12776
13005
  setModelSignature(chatId, value);
@@ -13033,6 +13262,7 @@ var init_router = __esm({
13033
13262
  init_format_time();
13034
13263
  init_agent();
13035
13264
  init_retry();
13265
+ init_quota();
13036
13266
  init_classify();
13037
13267
  init_version();
13038
13268
  init_image_gen();
@@ -13855,22 +14085,16 @@ async function main() {
13855
14085
  initDatabase();
13856
14086
  pruneMessageLog(30, 2e3);
13857
14087
  bootstrapBuiltinMcps(getDb());
13858
- const SUMMARIZE_TIMEOUT_MS2 = 3e4;
13859
- try {
13860
- let timer;
13861
- const timeoutPromise = new Promise((_, reject) => {
13862
- timer = setTimeout(() => reject(new Error("timeout")), SUMMARIZE_TIMEOUT_MS2);
14088
+ let pendingSummarizeNotify = null;
14089
+ const { getLoggedChatIds: getLoggedChatIds2 } = await Promise.resolve().then(() => (init_session_log(), session_log_exports));
14090
+ const pendingCount = getLoggedChatIds2().length;
14091
+ if (pendingCount > 0) {
14092
+ summarizeAllPending().then(() => {
14093
+ log(`[cc-claw] Background summarization complete (${pendingCount} session(s))`);
14094
+ if (pendingSummarizeNotify) pendingSummarizeNotify();
14095
+ }).catch((err) => {
14096
+ log(`[cc-claw] Background summarization failed: ${err}`);
13863
14097
  });
13864
- try {
13865
- await Promise.race([
13866
- summarizeAllPending(),
13867
- timeoutPromise
13868
- ]);
13869
- } finally {
13870
- clearTimeout(timer);
13871
- }
13872
- } catch {
13873
- log("[cc-claw] Session summarization skipped (timeout or backend unavailable)");
13874
14098
  }
13875
14099
  setBootTime();
13876
14100
  log("[cc-claw] Database initialized (sessions preserved for resume)");
@@ -13883,6 +14107,17 @@ async function main() {
13883
14107
  }
13884
14108
  await channelRegistry.startAll(handleMessage);
13885
14109
  log("[cc-claw] Channels started");
14110
+ if (pendingCount > 0) {
14111
+ const primaryChatId = (process.env.ALLOWED_CHAT_ID ?? "").split(",")[0]?.trim();
14112
+ pendingSummarizeNotify = () => {
14113
+ if (primaryChatId) {
14114
+ for (const ch of channelRegistry.list()) {
14115
+ ch.sendText(primaryChatId, "\u{1F504} Restarted \u2014 your conversations were summarized and saved.", "plain").catch(() => {
14116
+ });
14117
+ }
14118
+ }
14119
+ };
14120
+ }
13886
14121
  const telegramChannel = channelRegistry.get("telegram");
13887
14122
  if (telegramChannel && typeof telegramChannel.onCallback === "function") {
13888
14123
  telegramChannel.onCallback(handleCallback);
@@ -14787,8 +15022,8 @@ async function doctorCommand(globalOpts, localOpts) {
14787
15022
  }
14788
15023
  if (existsSync20(ERROR_LOG_PATH)) {
14789
15024
  try {
14790
- const { readFileSync: readFileSync18 } = await import("fs");
14791
- const logContent = readFileSync18(ERROR_LOG_PATH, "utf-8");
15025
+ const { readFileSync: readFileSync19 } = await import("fs");
15026
+ const logContent = readFileSync19(ERROR_LOG_PATH, "utf-8");
14792
15027
  const recentLines = logContent.split("\n").filter(Boolean).slice(-100);
14793
15028
  const last24h = Date.now() - 864e5;
14794
15029
  const recentErrors = recentLines.filter((line) => {
@@ -14963,9 +15198,10 @@ __export(gemini_exports, {
14963
15198
  geminiEnable: () => geminiEnable,
14964
15199
  geminiList: () => geminiList,
14965
15200
  geminiRemove: () => geminiRemove,
14966
- geminiReorder: () => geminiReorder
15201
+ geminiReorder: () => geminiReorder,
15202
+ geminiRotation: () => geminiRotation
14967
15203
  });
14968
- import { existsSync as existsSync22, mkdirSync as mkdirSync9, writeFileSync as writeFileSync7, chmodSync } from "fs";
15204
+ import { existsSync as existsSync22, mkdirSync as mkdirSync9, writeFileSync as writeFileSync7, readFileSync as readFileSync15, chmodSync } from "fs";
14969
15205
  import { join as join20 } from "path";
14970
15206
  import { createInterface as createInterface4 } from "readline";
14971
15207
  function requireDb() {
@@ -14982,12 +15218,32 @@ async function requireWriteDb() {
14982
15218
  dbInitialized = true;
14983
15219
  }
14984
15220
  }
15221
+ async function resolveSlotId(idOrLabel) {
15222
+ const numeric = parseInt(idOrLabel, 10);
15223
+ if (!isNaN(numeric)) return numeric;
15224
+ const { getGeminiSlots: getGeminiSlots2 } = await Promise.resolve().then(() => (init_store4(), store_exports3));
15225
+ const match = getGeminiSlots2().find(
15226
+ (s) => s.label?.toLowerCase() === idOrLabel.toLowerCase()
15227
+ );
15228
+ return match?.id ?? null;
15229
+ }
15230
+ function resolveOAuthEmail(configHome) {
15231
+ if (!configHome) return null;
15232
+ try {
15233
+ const accountsPath = join20(configHome, ".gemini", "google_accounts.json");
15234
+ if (!existsSync22(accountsPath)) return null;
15235
+ const accounts = JSON.parse(readFileSync15(accountsPath, "utf-8"));
15236
+ return accounts.active || null;
15237
+ } catch {
15238
+ return null;
15239
+ }
15240
+ }
14985
15241
  async function geminiList(globalOpts) {
14986
15242
  requireDb();
14987
15243
  const { openDatabaseReadOnly: openDatabaseReadOnly2 } = await Promise.resolve().then(() => (init_store4(), store_exports3));
14988
15244
  const readDb = openDatabaseReadOnly2();
14989
15245
  const slots = readDb.prepare(`
14990
- SELECT id, slot_type, label, priority, enabled, cooldown_until, last_used, consecutive_errors, created_at
15246
+ SELECT id, slot_type, label, config_home, priority, enabled, cooldown_until, last_used, consecutive_errors, created_at
14991
15247
  FROM gemini_credentials ORDER BY priority ASC, id ASC
14992
15248
  `).all();
14993
15249
  readDb.close();
@@ -14995,7 +15251,11 @@ async function geminiList(globalOpts) {
14995
15251
  output({ slots: [] }, () => "No Gemini credential slots configured.\nAdd one with: cc-claw gemini add-key or cc-claw gemini add-account");
14996
15252
  return;
14997
15253
  }
14998
- output(slots, (data) => {
15254
+ const enriched = slots.map((s) => ({
15255
+ ...s,
15256
+ email: s.slot_type === "oauth" ? resolveOAuthEmail(s.config_home) : null
15257
+ }));
15258
+ output(enriched, (data) => {
14999
15259
  const list = data;
15000
15260
  const lines = ["", divider("Gemini Credential Slots"), ""];
15001
15261
  for (const s of list) {
@@ -15003,8 +15263,9 @@ async function geminiList(globalOpts) {
15003
15263
  const inCooldown = s.cooldown_until && s.cooldown_until > now;
15004
15264
  const icon = !s.enabled ? error2("\u25CB disabled") : inCooldown ? warning("\u25D1 cooldown") : success("\u25CF active");
15005
15265
  const label2 = s.label || `slot-${s.id}`;
15006
- const type = s.slot_type === "oauth" ? "OAuth" : "API key";
15007
- lines.push(` ${icon} ${label2} ${muted(`(${type}, #${s.id}, priority ${s.priority})`)}`);
15266
+ const type = s.slot_type === "oauth" ? "\u{1F468}\u{1F3FD}\u200D\u{1F4BB}" : "\u{1F511}";
15267
+ const emailStr = s.email ? ` ${muted(s.email)}` : "";
15268
+ lines.push(` ${icon} ${label2}${emailStr} ${muted(`(${type}, #${s.id}, priority ${s.priority})`)}`);
15008
15269
  if (inCooldown) lines.push(` Cooldown until: ${warning(s.cooldown_until)}`);
15009
15270
  if (s.consecutive_errors > 0) lines.push(` Consecutive errors: ${warning(String(s.consecutive_errors))}`);
15010
15271
  if (s.last_used) lines.push(` Last used: ${muted(s.last_used)}`);
@@ -15023,6 +15284,22 @@ async function geminiAddKey(globalOpts, opts) {
15023
15284
  outputError("EMPTY_KEY", "No key provided.");
15024
15285
  process.exit(1);
15025
15286
  }
15287
+ console.log(" Validating API key...");
15288
+ try {
15289
+ const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(key.trim())}`);
15290
+ if (!res.ok) {
15291
+ const body = await res.text().catch(() => "");
15292
+ if (res.status === 400 || res.status === 403) {
15293
+ outputError("INVALID_KEY", `API key is invalid or unauthorized (HTTP ${res.status}).`);
15294
+ process.exit(1);
15295
+ }
15296
+ console.log(warning(` Warning: validation returned HTTP ${res.status} \u2014 saving anyway. ${body.slice(0, 100)}`));
15297
+ } else {
15298
+ console.log(success(" \u2713 API key is valid."));
15299
+ }
15300
+ } catch (err) {
15301
+ console.log(warning(` Warning: could not validate key (network error) \u2014 saving anyway.`));
15302
+ }
15026
15303
  const { addGeminiSlot: addGeminiSlot2 } = await Promise.resolve().then(() => (init_store4(), store_exports3));
15027
15304
  const id = addGeminiSlot2({
15028
15305
  slotType: "api_key",
@@ -15087,37 +15364,78 @@ async function geminiAddAccount(globalOpts, opts) {
15087
15364
  Added OAuth slot #${id} (${accountEmail})`)
15088
15365
  );
15089
15366
  }
15090
- async function geminiRemove(globalOpts, id) {
15367
+ async function geminiRemove(globalOpts, idOrLabel) {
15091
15368
  await requireWriteDb();
15369
+ const slotId = await resolveSlotId(idOrLabel);
15370
+ if (!slotId) {
15371
+ outputError("NOT_FOUND", `Slot "${idOrLabel}" not found.`);
15372
+ return;
15373
+ }
15092
15374
  const { removeGeminiSlot: removeGeminiSlot2 } = await Promise.resolve().then(() => (init_store4(), store_exports3));
15093
- const removed = removeGeminiSlot2(parseInt(id, 10));
15375
+ const removed = removeGeminiSlot2(slotId);
15094
15376
  if (removed) {
15095
- output({ removed: true, id: parseInt(id, 10) }, () => success(`Removed slot #${id}`));
15377
+ output({ removed: true, id: slotId }, () => success(`Removed slot "${idOrLabel}" (#${slotId})`));
15096
15378
  } else {
15097
- outputError("NOT_FOUND", `Slot #${id} not found.`);
15379
+ outputError("NOT_FOUND", `Slot "${idOrLabel}" not found.`);
15098
15380
  }
15099
15381
  }
15100
- async function geminiEnable(globalOpts, id) {
15382
+ async function geminiEnable(globalOpts, idOrLabel) {
15101
15383
  await requireWriteDb();
15384
+ const slotId = await resolveSlotId(idOrLabel);
15385
+ if (!slotId) {
15386
+ outputError("NOT_FOUND", `Slot "${idOrLabel}" not found.`);
15387
+ return;
15388
+ }
15102
15389
  const { setGeminiSlotEnabled: setGeminiSlotEnabled2 } = await Promise.resolve().then(() => (init_store4(), store_exports3));
15103
- setGeminiSlotEnabled2(parseInt(id, 10), true);
15104
- output({ id: parseInt(id, 10), enabled: true }, () => success(`Enabled slot #${id}`));
15390
+ setGeminiSlotEnabled2(slotId, true);
15391
+ output({ id: slotId, enabled: true }, () => success(`Enabled slot "${idOrLabel}" (#${slotId})`));
15105
15392
  }
15106
- async function geminiDisable(globalOpts, id) {
15393
+ async function geminiDisable(globalOpts, idOrLabel) {
15107
15394
  await requireWriteDb();
15395
+ const slotId = await resolveSlotId(idOrLabel);
15396
+ if (!slotId) {
15397
+ outputError("NOT_FOUND", `Slot "${idOrLabel}" not found.`);
15398
+ return;
15399
+ }
15108
15400
  const { setGeminiSlotEnabled: setGeminiSlotEnabled2 } = await Promise.resolve().then(() => (init_store4(), store_exports3));
15109
- setGeminiSlotEnabled2(parseInt(id, 10), false);
15110
- output({ id: parseInt(id, 10), enabled: false }, () => warning(`Disabled slot #${id}`));
15401
+ setGeminiSlotEnabled2(slotId, false);
15402
+ output({ id: slotId, enabled: false }, () => warning(`Disabled slot "${idOrLabel}" (#${slotId})`));
15111
15403
  }
15112
- async function geminiReorder(globalOpts, id, priority) {
15404
+ async function geminiReorder(globalOpts, idOrLabel, priority) {
15113
15405
  await requireWriteDb();
15406
+ const slotId = await resolveSlotId(idOrLabel);
15407
+ if (!slotId) {
15408
+ outputError("NOT_FOUND", `Slot "${idOrLabel}" not found.`);
15409
+ return;
15410
+ }
15114
15411
  const { reorderGeminiSlot: reorderGeminiSlot2 } = await Promise.resolve().then(() => (init_store4(), store_exports3));
15115
- reorderGeminiSlot2(parseInt(id, 10), parseInt(priority, 10));
15412
+ reorderGeminiSlot2(slotId, parseInt(priority, 10));
15116
15413
  output(
15117
- { id: parseInt(id, 10), priority: parseInt(priority, 10) },
15118
- () => success(`Slot #${id} priority set to ${priority}`)
15414
+ { id: slotId, priority: parseInt(priority, 10) },
15415
+ () => success(`Slot "${idOrLabel}" (#${slotId}) priority set to ${priority}`)
15119
15416
  );
15120
15417
  }
15418
+ async function geminiRotation(globalOpts, mode) {
15419
+ const validModes = ["off", "all", "accounts", "keys"];
15420
+ if (!mode) {
15421
+ requireDb();
15422
+ const { openDatabaseReadOnly: openDatabaseReadOnly2 } = await Promise.resolve().then(() => (init_store4(), store_exports3));
15423
+ const readDb = openDatabaseReadOnly2();
15424
+ const row = readDb.prepare("SELECT value FROM meta WHERE key = 'gemini_rotation_mode'").get();
15425
+ readDb.close();
15426
+ const current = row?.value ?? "all";
15427
+ output({ mode: current }, () => `Gemini rotation mode: ${success(current)}`);
15428
+ return;
15429
+ }
15430
+ if (!validModes.includes(mode)) {
15431
+ outputError("INVALID_MODE", `Invalid mode: "${mode}". Valid modes: ${validModes.join(", ")}`);
15432
+ process.exit(1);
15433
+ }
15434
+ await requireWriteDb();
15435
+ const { setGeminiRotationMode: setGeminiRotationMode2 } = await Promise.resolve().then(() => (init_store4(), store_exports3));
15436
+ setGeminiRotationMode2(mode);
15437
+ output({ mode }, () => success(`Gemini rotation mode set to "${mode}"`));
15438
+ }
15121
15439
  var dbInitialized;
15122
15440
  var init_gemini2 = __esm({
15123
15441
  "src/cli/commands/gemini.ts"() {
@@ -16116,7 +16434,7 @@ __export(config_exports, {
16116
16434
  configList: () => configList,
16117
16435
  configSet: () => configSet
16118
16436
  });
16119
- import { existsSync as existsSync30, readFileSync as readFileSync15 } from "fs";
16437
+ import { existsSync as existsSync30, readFileSync as readFileSync16 } from "fs";
16120
16438
  async function configList(globalOpts) {
16121
16439
  if (!existsSync30(DB_PATH)) {
16122
16440
  outputError("DB_NOT_FOUND", "Database not found.");
@@ -16202,7 +16520,7 @@ async function configEnv(_globalOpts) {
16202
16520
  outputError("ENV_NOT_FOUND", `No .env file at ${ENV_PATH}. Run cc-claw setup.`);
16203
16521
  process.exit(1);
16204
16522
  }
16205
- const content = readFileSync15(ENV_PATH, "utf-8");
16523
+ const content = readFileSync16(ENV_PATH, "utf-8");
16206
16524
  const entries = {};
16207
16525
  const secretPatterns = /TOKEN|KEY|SECRET|PASSWORD|CREDENTIALS/i;
16208
16526
  for (const line of content.split("\n")) {
@@ -16860,11 +17178,11 @@ __export(chat_exports, {
16860
17178
  chatSend: () => chatSend
16861
17179
  });
16862
17180
  import { request as httpRequest2 } from "http";
16863
- import { readFileSync as readFileSync16, existsSync as existsSync38 } from "fs";
17181
+ import { readFileSync as readFileSync17, existsSync as existsSync38 } from "fs";
16864
17182
  function getToken2() {
16865
17183
  if (process.env.CC_CLAW_API_TOKEN) return process.env.CC_CLAW_API_TOKEN;
16866
17184
  try {
16867
- if (existsSync38(TOKEN_PATH2)) return readFileSync16(TOKEN_PATH2, "utf-8").trim();
17185
+ if (existsSync38(TOKEN_PATH2)) return readFileSync17(TOKEN_PATH2, "utf-8").trim();
16868
17186
  } catch {
16869
17187
  }
16870
17188
  return null;
@@ -17290,7 +17608,7 @@ var init_completion = __esm({
17290
17608
 
17291
17609
  // src/setup.ts
17292
17610
  var setup_exports = {};
17293
- import { existsSync as existsSync39, writeFileSync as writeFileSync8, readFileSync as readFileSync17, copyFileSync as copyFileSync3, mkdirSync as mkdirSync11, statSync as statSync6 } from "fs";
17611
+ import { existsSync as existsSync39, writeFileSync as writeFileSync8, readFileSync as readFileSync18, copyFileSync as copyFileSync3, mkdirSync as mkdirSync11, statSync as statSync6 } from "fs";
17294
17612
  import { execFileSync as execFileSync4 } from "child_process";
17295
17613
  import { createInterface as createInterface6 } from "readline";
17296
17614
  import { join as join21 } from "path";
@@ -17374,7 +17692,7 @@ async function setup() {
17374
17692
  if (envSource) {
17375
17693
  console.log(yellow(` Found existing config at ${envSource} \u2014 your values will be preserved`));
17376
17694
  console.log(yellow(" unless you enter new ones. Just press Enter to keep existing values.\n"));
17377
- const existing = readFileSync17(envSource, "utf-8");
17695
+ const existing = readFileSync18(envSource, "utf-8");
17378
17696
  for (const line of existing.split("\n")) {
17379
17697
  const match = line.match(/^([^#=]+)=(.*)$/);
17380
17698
  if (match) env[match[1].trim()] = match[2].trim();
@@ -17732,22 +18050,26 @@ gemini.command("add-account").description("Add an OAuth account slot (opens brow
17732
18050
  const { geminiAddAccount: geminiAddAccount2 } = await Promise.resolve().then(() => (init_gemini2(), gemini_exports));
17733
18051
  await geminiAddAccount2(program.opts(), opts);
17734
18052
  });
17735
- gemini.command("remove <id>").description("Remove a credential slot").action(async (id) => {
18053
+ gemini.command("remove <id-or-label>").description("Remove a credential slot (by ID or label)").action(async (id) => {
17736
18054
  const { geminiRemove: geminiRemove2 } = await Promise.resolve().then(() => (init_gemini2(), gemini_exports));
17737
18055
  await geminiRemove2(program.opts(), id);
17738
18056
  });
17739
- gemini.command("enable <id>").description("Re-enable a disabled slot").action(async (id) => {
18057
+ gemini.command("enable <id-or-label>").description("Re-enable a disabled slot (by ID or label)").action(async (id) => {
17740
18058
  const { geminiEnable: geminiEnable2 } = await Promise.resolve().then(() => (init_gemini2(), gemini_exports));
17741
18059
  await geminiEnable2(program.opts(), id);
17742
18060
  });
17743
- gemini.command("disable <id>").description("Disable a slot (skip during rotation)").action(async (id) => {
18061
+ gemini.command("disable <id-or-label>").description("Disable a slot (by ID or label)").action(async (id) => {
17744
18062
  const { geminiDisable: geminiDisable2 } = await Promise.resolve().then(() => (init_gemini2(), gemini_exports));
17745
18063
  await geminiDisable2(program.opts(), id);
17746
18064
  });
17747
- gemini.command("reorder <id> <priority>").description("Set slot priority (lower = preferred)").action(async (id, priority) => {
18065
+ gemini.command("reorder <id-or-label> <priority>").description("Set slot priority (by ID or label, lower = preferred)").action(async (id, priority) => {
17748
18066
  const { geminiReorder: geminiReorder2 } = await Promise.resolve().then(() => (init_gemini2(), gemini_exports));
17749
18067
  await geminiReorder2(program.opts(), id, priority);
17750
18068
  });
18069
+ gemini.command("rotation [mode]").description("Get or set rotation mode (off, all, accounts, keys)").action(async (mode) => {
18070
+ const { geminiRotation: geminiRotation2 } = await Promise.resolve().then(() => (init_gemini2(), gemini_exports));
18071
+ await geminiRotation2(program.opts(), mode);
18072
+ });
17751
18073
  var backend = program.command("backend").description("Manage AI backend");
17752
18074
  backend.command("list").description("Available backends with status").action(async () => {
17753
18075
  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.1",
3
+ "version": "0.7.1",
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",