cc-claw 0.3.10 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -48,7 +48,7 @@ var VERSION;
48
48
  var init_version = __esm({
49
49
  "src/version.ts"() {
50
50
  "use strict";
51
- VERSION = true ? "0.3.10" : (() => {
51
+ VERSION = true ? "0.4.0" : (() => {
52
52
  try {
53
53
  return JSON.parse(readFileSync(join2(process.cwd(), "package.json"), "utf-8")).version ?? "unknown";
54
54
  } catch {
@@ -922,12 +922,14 @@ __export(store_exports3, {
922
922
  ALL_TOOLS: () => ALL_TOOLS,
923
923
  addHeartbeatWatch: () => addHeartbeatWatch,
924
924
  addUsage: () => addUsage,
925
+ appendMessageLog: () => appendMessageLog,
925
926
  cancelJobById: () => cancelJobById,
926
927
  checkBackendLimits: () => checkBackendLimits,
927
928
  cleanExpiredWatches: () => cleanExpiredWatches,
928
929
  clearAllSessions: () => clearAllSessions,
929
930
  clearBackendLimit: () => clearBackendLimit,
930
931
  clearCwd: () => clearCwd,
932
+ clearMessageLog: () => clearMessageLog,
931
933
  clearModel: () => clearModel,
932
934
  clearSession: () => clearSession,
933
935
  clearSummarizer: () => clearSummarizer,
@@ -935,6 +937,7 @@ __export(store_exports3, {
935
937
  clearUsage: () => clearUsage,
936
938
  completeJobRun: () => completeJobRun,
937
939
  deleteBookmark: () => deleteBookmark,
940
+ determineEscalationTarget: () => determineEscalationTarget,
938
941
  findBookmarksByPrefix: () => findBookmarksByPrefix,
939
942
  forgetMemory: () => forgetMemory,
940
943
  getActiveJobs: () => getActiveJobs,
@@ -961,14 +964,18 @@ __export(store_exports3, {
961
964
  getMemoriesWithoutEmbeddings: () => getMemoriesWithoutEmbeddings,
962
965
  getMode: () => getMode,
963
966
  getModel: () => getModel,
967
+ getPendingEscalation: () => getPendingEscalation,
964
968
  getRecentBookmarks: () => getRecentBookmarks,
965
969
  getRecentMemories: () => getRecentMemories,
970
+ getRecentMessageLog: () => getRecentMessageLog,
966
971
  getSessionId: () => getSessionId,
967
972
  getSessionStartedAt: () => getSessionStartedAt,
968
973
  getSessionSummariesWithoutEmbeddings: () => getSessionSummariesWithoutEmbeddings,
969
974
  getSummarizer: () => getSummarizer,
970
975
  getThinkingLevel: () => getThinkingLevel,
971
976
  getToolsMap: () => getToolsMap,
977
+ getUnsummarizedChatIds: () => getUnsummarizedChatIds,
978
+ getUnsummarizedLog: () => getUnsummarizedLog,
972
979
  getUsage: () => getUsage,
973
980
  getVerboseLevel: () => getVerboseLevel,
974
981
  incrementJobFailures: () => incrementJobFailures,
@@ -977,10 +984,13 @@ __export(store_exports3, {
977
984
  insertJobRun: () => insertJobRun,
978
985
  listMemories: () => listMemories,
979
986
  listSessionSummaries: () => listSessionSummaries,
987
+ markMessageLogSummarized: () => markMessageLogSummarized,
980
988
  openDatabaseReadOnly: () => openDatabaseReadOnly,
981
989
  pruneJobRuns: () => pruneJobRuns,
990
+ pruneMessageLog: () => pruneMessageLog,
982
991
  removeChatAlias: () => removeChatAlias,
983
992
  removeHeartbeatWatch: () => removeHeartbeatWatch,
993
+ removePendingEscalation: () => removePendingEscalation,
984
994
  resetJobFailures: () => resetJobFailures,
985
995
  resetTools: () => resetTools,
986
996
  saveMemory: () => saveMemory,
@@ -989,6 +999,7 @@ __export(store_exports3, {
989
999
  saveSessionSummaryWithEmbedding: () => saveSessionSummaryWithEmbedding,
990
1000
  searchMemories: () => searchMemories,
991
1001
  searchMemoriesReadOnly: () => searchMemoriesReadOnly,
1002
+ searchMessageLog: () => searchMessageLog,
992
1003
  searchSessionSummaries: () => searchSessionSummaries,
993
1004
  setBackend: () => setBackend,
994
1005
  setBackendLimit: () => setBackendLimit,
@@ -1003,6 +1014,8 @@ __export(store_exports3, {
1003
1014
  setSummarizer: () => setSummarizer,
1004
1015
  setThinkingLevel: () => setThinkingLevel,
1005
1016
  setVerboseLevel: () => setVerboseLevel,
1017
+ storePendingEscalation: () => storePendingEscalation,
1018
+ toFts5Query: () => toFts5Query,
1006
1019
  toggleTool: () => toggleTool,
1007
1020
  touchBookmark: () => touchBookmark,
1008
1021
  updateHeartbeatTimestamps: () => updateHeartbeatTimestamps,
@@ -1084,6 +1097,7 @@ function initDatabase() {
1084
1097
  mode TEXT NOT NULL DEFAULT 'yolo'
1085
1098
  );
1086
1099
  `);
1100
+ db.prepare("UPDATE chat_mode SET mode = 'plan' WHERE mode = 'readonly'").run();
1087
1101
  db.exec(`
1088
1102
  CREATE TABLE IF NOT EXISTS chat_verbose (
1089
1103
  chat_id TEXT PRIMARY KEY,
@@ -1144,6 +1158,7 @@ function initDatabase() {
1144
1158
  model TEXT,
1145
1159
  thinking TEXT,
1146
1160
  timeout INTEGER,
1161
+ fallbacks TEXT,
1147
1162
  session_type TEXT NOT NULL DEFAULT 'isolated',
1148
1163
  channel TEXT,
1149
1164
  target TEXT,
@@ -1263,6 +1278,10 @@ function initDatabase() {
1263
1278
  db.exec("ALTER TABLE jobs ADD COLUMN timeout INTEGER");
1264
1279
  } catch {
1265
1280
  }
1281
+ try {
1282
+ db.exec("ALTER TABLE jobs ADD COLUMN fallbacks TEXT");
1283
+ } catch {
1284
+ }
1266
1285
  }
1267
1286
  } catch {
1268
1287
  }
@@ -1390,6 +1409,52 @@ function initDatabase() {
1390
1409
  PRIMARY KEY (chatId, alias)
1391
1410
  )
1392
1411
  `);
1412
+ db.exec(`
1413
+ CREATE TABLE IF NOT EXISTS message_log (
1414
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1415
+ chat_id TEXT NOT NULL,
1416
+ role TEXT NOT NULL,
1417
+ content TEXT NOT NULL,
1418
+ backend TEXT,
1419
+ model TEXT,
1420
+ session_id TEXT,
1421
+ summarized INTEGER NOT NULL DEFAULT 0,
1422
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
1423
+ )
1424
+ `);
1425
+ db.exec(`
1426
+ CREATE INDEX IF NOT EXISTS idx_message_log_chat
1427
+ ON message_log (chat_id, created_at DESC)
1428
+ `);
1429
+ db.exec(`
1430
+ CREATE INDEX IF NOT EXISTS idx_message_log_unsummarized
1431
+ ON message_log (chat_id, summarized, created_at ASC)
1432
+ `);
1433
+ db.exec(`
1434
+ CREATE VIRTUAL TABLE IF NOT EXISTS message_log_fts
1435
+ USING fts5(content, content='message_log', content_rowid='id')
1436
+ `);
1437
+ db.exec(`
1438
+ CREATE TRIGGER IF NOT EXISTS message_log_ai
1439
+ AFTER INSERT ON message_log BEGIN
1440
+ INSERT INTO message_log_fts(rowid, content) VALUES (new.id, new.content);
1441
+ END
1442
+ `);
1443
+ db.exec(`
1444
+ CREATE TRIGGER IF NOT EXISTS message_log_ad
1445
+ AFTER DELETE ON message_log BEGIN
1446
+ INSERT INTO message_log_fts(message_log_fts, rowid, content)
1447
+ VALUES ('delete', old.id, old.content);
1448
+ END
1449
+ `);
1450
+ db.exec(`
1451
+ CREATE TRIGGER IF NOT EXISTS message_log_au
1452
+ AFTER UPDATE ON message_log BEGIN
1453
+ INSERT INTO message_log_fts(message_log_fts, rowid, content)
1454
+ VALUES ('delete', old.id, old.content);
1455
+ INSERT INTO message_log_fts(rowid, content) VALUES (new.id, new.content);
1456
+ END
1457
+ `);
1393
1458
  try {
1394
1459
  db.exec("ALTER TABLE memories ADD COLUMN embedding TEXT");
1395
1460
  } catch {
@@ -1669,7 +1734,8 @@ function getMode(chatId) {
1669
1734
  const row = db.prepare(
1670
1735
  "SELECT mode FROM chat_mode WHERE chat_id = ?"
1671
1736
  ).get(chatId);
1672
- return row?.mode ?? "yolo";
1737
+ const raw = row?.mode ?? "yolo";
1738
+ return raw === "readonly" ? "plan" : raw;
1673
1739
  }
1674
1740
  function setMode(chatId, mode) {
1675
1741
  db.prepare(`
@@ -1678,6 +1744,30 @@ function setMode(chatId, mode) {
1678
1744
  ON CONFLICT(chat_id) DO UPDATE SET mode = ?
1679
1745
  `).run(chatId, mode, mode);
1680
1746
  }
1747
+ function storePendingEscalation(chatId, message) {
1748
+ pendingEscalations.set(chatId, { message, createdAt: Date.now() });
1749
+ setTimeout(() => pendingEscalations.delete(chatId), ESCALATION_TTL_MS);
1750
+ }
1751
+ function getPendingEscalation(chatId) {
1752
+ const entry = pendingEscalations.get(chatId);
1753
+ if (!entry) return void 0;
1754
+ if (Date.now() - entry.createdAt > ESCALATION_TTL_MS) {
1755
+ pendingEscalations.delete(chatId);
1756
+ return void 0;
1757
+ }
1758
+ return entry.message;
1759
+ }
1760
+ function removePendingEscalation(chatId) {
1761
+ pendingEscalations.delete(chatId);
1762
+ }
1763
+ function determineEscalationTarget(chatId, currentMode) {
1764
+ if (currentMode === "plan") {
1765
+ const tools2 = getEnabledTools(chatId);
1766
+ if (tools2.length > 0 && tools2.length < ALL_TOOLS.length) return "safe";
1767
+ return "yolo";
1768
+ }
1769
+ return "yolo";
1770
+ }
1681
1771
  function getUsage(chatId) {
1682
1772
  const row = db.prepare(
1683
1773
  "SELECT input_tokens, output_tokens, cache_read_tokens, request_count, last_input_tokens, last_cache_read_tokens FROM chat_usage WHERE chat_id = ?"
@@ -1955,8 +2045,8 @@ function getBackendUsageInWindow(backend2, windowType) {
1955
2045
  function insertJob(params) {
1956
2046
  const result = db.prepare(`
1957
2047
  INSERT INTO jobs (schedule_type, cron, at_time, every_ms, description, chat_id,
1958
- backend, model, thinking, timeout, session_type, channel, target, delivery_mode, timezone)
1959
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2048
+ backend, model, thinking, timeout, fallbacks, session_type, channel, target, delivery_mode, timezone)
2049
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1960
2050
  `).run(
1961
2051
  params.scheduleType,
1962
2052
  params.cron ?? null,
@@ -1968,6 +2058,7 @@ function insertJob(params) {
1968
2058
  params.model ?? null,
1969
2059
  params.thinking ?? null,
1970
2060
  params.timeout ?? null,
2061
+ params.fallbacks?.length ? JSON.stringify(params.fallbacks) : null,
1971
2062
  params.sessionType ?? "isolated",
1972
2063
  params.channel ?? null,
1973
2064
  params.target ?? null,
@@ -1976,14 +2067,19 @@ function insertJob(params) {
1976
2067
  );
1977
2068
  return getJobById(Number(result.lastInsertRowid));
1978
2069
  }
2070
+ function mapJobRow(row) {
2071
+ if (!row) return void 0;
2072
+ row.fallbacks = row.fallbacks ? JSON.parse(row.fallbacks) : [];
2073
+ return row;
2074
+ }
1979
2075
  function getJobById(id) {
1980
- return db.prepare(`${JOB_SELECT} WHERE id = ?`).get(id);
2076
+ return mapJobRow(db.prepare(`${JOB_SELECT} WHERE id = ?`).get(id));
1981
2077
  }
1982
2078
  function getActiveJobs() {
1983
- return db.prepare(`${JOB_SELECT} WHERE active = 1 AND enabled = 1`).all();
2079
+ return db.prepare(`${JOB_SELECT} WHERE active = 1 AND enabled = 1`).all().map((r) => mapJobRow(r));
1984
2080
  }
1985
2081
  function getAllJobs() {
1986
- return db.prepare(`${JOB_SELECT} WHERE active = 1 ORDER BY id`).all();
2082
+ return db.prepare(`${JOB_SELECT} WHERE active = 1 ORDER BY id`).all().map((r) => mapJobRow(r));
1987
2083
  }
1988
2084
  function updateJobEnabled(id, enabled) {
1989
2085
  const result = db.prepare("UPDATE jobs SET enabled = ? WHERE id = ?").run(enabled ? 1 : 0, id);
@@ -2155,7 +2251,77 @@ function getAllChatAliases() {
2155
2251
  function removeChatAlias(alias) {
2156
2252
  return db.prepare("DELETE FROM chat_aliases WHERE alias = ?").run(alias.toLowerCase()).changes > 0;
2157
2253
  }
2158
- var db, STOP_WORDS, ALL_TOOLS, JOB_SELECT;
2254
+ function appendMessageLog(chatId, role, content, backend2, model2, sessionId) {
2255
+ getDb().prepare(`
2256
+ INSERT INTO message_log (chat_id, role, content, backend, model, session_id)
2257
+ VALUES (?, ?, ?, ?, ?, ?)
2258
+ `).run(chatId, role, content, backend2, model2, sessionId);
2259
+ }
2260
+ function getRecentMessageLog(chatId, limit) {
2261
+ return getDb().prepare(`
2262
+ SELECT * FROM message_log
2263
+ WHERE chat_id = ?
2264
+ ORDER BY created_at DESC, id DESC
2265
+ LIMIT ?
2266
+ `).all(chatId, limit);
2267
+ }
2268
+ function getUnsummarizedLog(chatId) {
2269
+ return getDb().prepare(`
2270
+ SELECT * FROM message_log
2271
+ WHERE chat_id = ? AND summarized = 0
2272
+ ORDER BY created_at ASC, id ASC
2273
+ `).all(chatId);
2274
+ }
2275
+ function searchMessageLog(chatId, query, limit = 20) {
2276
+ const sanitized = toFts5Query(query);
2277
+ if (!sanitized) return [];
2278
+ try {
2279
+ return getDb().prepare(`
2280
+ SELECT ml.* FROM message_log ml
2281
+ JOIN message_log_fts fts ON fts.rowid = ml.id
2282
+ WHERE ml.chat_id = ? AND message_log_fts MATCH ?
2283
+ ORDER BY fts.rank
2284
+ LIMIT ?
2285
+ `).all(chatId, sanitized, limit);
2286
+ } catch {
2287
+ return [];
2288
+ }
2289
+ }
2290
+ function markMessageLogSummarized(chatId) {
2291
+ getDb().prepare(`
2292
+ UPDATE message_log SET summarized = 1
2293
+ WHERE chat_id = ? AND summarized = 0
2294
+ `).run(chatId);
2295
+ }
2296
+ function clearMessageLog(chatId) {
2297
+ getDb().prepare(`DELETE FROM message_log WHERE chat_id = ?`).run(chatId);
2298
+ }
2299
+ function getUnsummarizedChatIds() {
2300
+ const rows = getDb().prepare(
2301
+ `SELECT DISTINCT chat_id FROM message_log WHERE summarized = 0`
2302
+ ).all();
2303
+ return rows.map((r) => r.chat_id);
2304
+ }
2305
+ function pruneMessageLog(retentionDays = 30, rowCapPerChat = 2e3) {
2306
+ const db3 = getDb();
2307
+ db3.prepare(`
2308
+ DELETE FROM message_log
2309
+ WHERE created_at < datetime('now', '-' || ? || ' days')
2310
+ `).run(retentionDays);
2311
+ const chats2 = db3.prepare(
2312
+ `SELECT DISTINCT chat_id FROM message_log`
2313
+ ).all();
2314
+ for (const { chat_id } of chats2) {
2315
+ db3.prepare(`
2316
+ DELETE FROM message_log
2317
+ WHERE chat_id = ? AND id NOT IN (
2318
+ SELECT id FROM message_log WHERE chat_id = ?
2319
+ ORDER BY created_at DESC, id DESC LIMIT ?
2320
+ )
2321
+ `).run(chat_id, chat_id, rowCapPerChat);
2322
+ }
2323
+ }
2324
+ var db, STOP_WORDS, pendingEscalations, ESCALATION_TTL_MS, ALL_TOOLS, JOB_SELECT;
2159
2325
  var init_store4 = __esm({
2160
2326
  "src/memory/store.ts"() {
2161
2327
  "use strict";
@@ -2280,10 +2446,12 @@ var init_store4 = __esm({
2280
2446
  "near",
2281
2447
  "or"
2282
2448
  ]);
2449
+ pendingEscalations = /* @__PURE__ */ new Map();
2450
+ ESCALATION_TTL_MS = 5 * 60 * 1e3;
2283
2451
  ALL_TOOLS = ["Read", "Glob", "Grep", "Bash", "Write", "Edit", "WebFetch", "WebSearch", "Agent", "AskUserQuestion"];
2284
2452
  JOB_SELECT = `
2285
2453
  SELECT id, schedule_type as scheduleType, cron, at_time as atTime, every_ms as everyMs,
2286
- description, chat_id as chatId, backend, model, thinking, timeout,
2454
+ description, chat_id as chatId, backend, model, thinking, timeout, fallbacks,
2287
2455
  session_type as sessionType, channel, target, delivery_mode as deliveryMode,
2288
2456
  timezone, enabled, active, created_at as createdAt, last_run_at as lastRunAt,
2289
2457
  next_run_at as nextRunAt, consecutive_failures as consecutiveFailures
@@ -2433,15 +2601,14 @@ var init_claude = __esm({
2433
2601
  switch (opts.permMode) {
2434
2602
  case "plan":
2435
2603
  args.push("--permission-mode", "plan");
2436
- break;
2437
- case "readonly":
2438
- args.push("--permission-mode", "bypassPermissions");
2439
- args.push("--allowedTools", "Read,Glob,Grep");
2604
+ args.push("--strict-mcp-config");
2440
2605
  break;
2441
2606
  case "safe":
2442
2607
  args.push("--permission-mode", "bypassPermissions");
2443
2608
  if (opts.allowedTools.length > 0) {
2444
- args.push("--allowedTools", opts.allowedTools.join(","));
2609
+ args.push("--tools", opts.allowedTools.join(","));
2610
+ } else {
2611
+ args.push("--tools", "Read,Glob,Grep");
2445
2612
  }
2446
2613
  break;
2447
2614
  case "yolo":
@@ -2600,11 +2767,10 @@ var init_gemini = __esm({
2600
2767
  if (opts.model) args.push("-m", opts.model);
2601
2768
  switch (opts.permMode) {
2602
2769
  case "plan":
2603
- case "readonly":
2604
2770
  args.push("--approval-mode", "plan");
2605
2771
  break;
2606
2772
  case "safe":
2607
- args.push("--approval-mode", "auto_edit");
2773
+ args.push("--approval-mode", "plan");
2608
2774
  break;
2609
2775
  case "yolo":
2610
2776
  default:
@@ -2686,21 +2852,34 @@ var init_codex = __esm({
2686
2852
  thinking: "adjustable",
2687
2853
  thinkingLevels: ["low", "medium", "high", "extra_high"],
2688
2854
  defaultThinkingLevel: "medium"
2855
+ },
2856
+ "gpt-5.1-codex-max": {
2857
+ label: "GPT-5.1 Codex Max \u2014 deep reasoning",
2858
+ thinking: "adjustable",
2859
+ thinkingLevels: ["low", "medium", "high", "extra_high"],
2860
+ defaultThinkingLevel: "medium"
2861
+ },
2862
+ "gpt-5.1-codex-mini": {
2863
+ label: "GPT-5.1 Codex Mini \u2014 cheaper, faster",
2864
+ thinking: "adjustable",
2865
+ thinkingLevels: ["low", "medium", "high", "extra_high"],
2866
+ defaultThinkingLevel: "low"
2689
2867
  }
2690
2868
  };
2691
2869
  defaultModel = "gpt-5.4";
2692
- // Internal-only model for summarization — not shown in /model picker
2693
2870
  summarizerModel = "gpt-5.1-codex-mini";
2694
2871
  pricing = {
2695
2872
  "gpt-5.4": { in: 2.5, out: 10, cache: 0.63 },
2696
2873
  "gpt-5.3-codex": { in: 2, out: 8, cache: 0.5 },
2697
2874
  "gpt-5.2-codex": { in: 2, out: 8, cache: 0.5 },
2875
+ "gpt-5.1-codex-max": { in: 1.25, out: 10, cache: 0.125 },
2698
2876
  "gpt-5.1-codex-mini": { in: 0.3, out: 1.2, cache: 0.08 }
2699
2877
  };
2700
2878
  contextWindow = {
2701
2879
  "gpt-5.4": 2e5,
2702
2880
  "gpt-5.3-codex": 2e5,
2703
2881
  "gpt-5.2-codex": 2e5,
2882
+ "gpt-5.1-codex-max": 4e5,
2704
2883
  "gpt-5.1-codex-mini": 2e5
2705
2884
  };
2706
2885
  // Codex has no single result event with final text -- the caller (spawnQuery)
@@ -2742,11 +2921,10 @@ var init_codex = __esm({
2742
2921
  if (opts.model) args.push("-m", opts.model);
2743
2922
  switch (opts.permMode) {
2744
2923
  case "plan":
2745
- case "readonly":
2746
2924
  args.push("--sandbox", "read-only");
2747
2925
  break;
2748
2926
  case "safe":
2749
- args.push("--full-auto");
2927
+ args.push("--sandbox", "read-only");
2750
2928
  break;
2751
2929
  case "yolo":
2752
2930
  default:
@@ -2904,6 +3082,31 @@ function mergeAndScore(allItems, vectorScores, ftsScores, getDays, decayRate, to
2904
3082
  results.sort((a, b) => b.score - a.score);
2905
3083
  return results.slice(0, topK);
2906
3084
  }
3085
+ function setPendingContextBridge(chatId, bridge) {
3086
+ pendingContextBridges.set(chatId, bridge);
3087
+ }
3088
+ function consumeContextBridge(chatId) {
3089
+ const bridge = pendingContextBridges.get(chatId) ?? null;
3090
+ pendingContextBridges.delete(chatId);
3091
+ return bridge;
3092
+ }
3093
+ function buildContextBridge(chatId, pairs = 15) {
3094
+ const rows = getRecentMessageLog(chatId, pairs * 2).reverse();
3095
+ if (rows.length === 0) return null;
3096
+ const lines = [`[Conversation history \u2014 continued from previous session]`];
3097
+ let totalChars = lines[0].length;
3098
+ for (const row of rows) {
3099
+ const backendLabel = row.backend ?? "unknown";
3100
+ const date = row.created_at.slice(0, 10);
3101
+ const roleLabel = row.role === "user" ? "You" : "Assistant";
3102
+ const line = `[${backendLabel} \xB7 ${date}] ${roleLabel}: ${row.content}`;
3103
+ if (totalChars + line.length > MAX_BRIDGE_CHARS) break;
3104
+ lines.push(line);
3105
+ totalChars += line.length;
3106
+ }
3107
+ lines.push(`[End of recent history \u2014 you are continuing this conversation]`);
3108
+ return lines.join("\n");
3109
+ }
2907
3110
  async function injectMemoryContext(userMessage) {
2908
3111
  const provider = resolveProvider();
2909
3112
  const useVectors = !!provider;
@@ -2982,17 +3185,21 @@ async function injectMemoryContext(userMessage) {
2982
3185
  if (combinedMemories.length === 0 && combinedSessions.length === 0) return null;
2983
3186
  const lines = [];
2984
3187
  for (const m of combinedMemories) {
2985
- lines.push(`- [${m.type}] ${m.trigger}: ${m.content}`);
3188
+ let text = `- [${m.type}] ${m.trigger}: ${m.content}`;
3189
+ if (text.length > MAX_MEMORY_CHARS) text = text.slice(0, MAX_MEMORY_CHARS) + "\u2026";
3190
+ lines.push(text);
2986
3191
  }
2987
3192
  for (const s of combinedSessions) {
2988
3193
  const date = s.created_at.split("T")[0] ?? s.created_at.split(" ")[0];
2989
- lines.push(`- [episodic] ${date} session (${s.message_count} msgs): ${s.summary}${s.topics ? ` Topics: ${s.topics}` : ""}`);
3194
+ let text = `- [episodic] ${date} session (${s.message_count} msgs): ${s.summary}${s.topics ? ` Topics: ${s.topics}` : ""}`;
3195
+ if (text.length > MAX_SESSION_CHARS) text = text.slice(0, MAX_SESSION_CHARS) + "\u2026";
3196
+ lines.push(text);
2990
3197
  }
2991
3198
  return `[Memory context]
2992
3199
  ${lines.join("\n")}
2993
3200
  [End memory context]`;
2994
3201
  }
2995
- var MEMORY_DECAY_RATE, SESSION_DECAY_RATE, VECTOR_TOP_K, FTS_TOP_K, FINAL_TOP_K_MEMORIES, FINAL_TOP_K_SESSIONS;
3202
+ var MEMORY_DECAY_RATE, SESSION_DECAY_RATE, VECTOR_TOP_K, FTS_TOP_K, FINAL_TOP_K_MEMORIES, FINAL_TOP_K_SESSIONS, MAX_MEMORY_CHARS, MAX_SESSION_CHARS, MAX_BRIDGE_CHARS, pendingContextBridges;
2996
3203
  var init_inject = __esm({
2997
3204
  "src/memory/inject.ts"() {
2998
3205
  "use strict";
@@ -3004,6 +3211,10 @@ var init_inject = __esm({
3004
3211
  FTS_TOP_K = 20;
3005
3212
  FINAL_TOP_K_MEMORIES = 5;
3006
3213
  FINAL_TOP_K_SESSIONS = 3;
3214
+ MAX_MEMORY_CHARS = 500;
3215
+ MAX_SESSION_CHARS = 800;
3216
+ MAX_BRIDGE_CHARS = 48e3;
3217
+ pendingContextBridges = /* @__PURE__ */ new Map();
3007
3218
  }
3008
3219
  });
3009
3220
 
@@ -3114,6 +3325,7 @@ function syncNativeCliFiles() {
3114
3325
  "## System Capabilities",
3115
3326
  "",
3116
3327
  "- To send a file to the user: write [SEND_FILE:/absolute/path] in your response",
3328
+ "- To generate an image: write [GENERATE_IMAGE:detailed prompt] in your response (requires GEMINI_API_KEY)",
3117
3329
  "- To suggest saving a user preference: write [UPDATE_USER:key=value] in your response",
3118
3330
  "- For heartbeat checks: respond with exactly HEARTBEAT_OK if nothing needs attention",
3119
3331
  "- Your working directory is ~/.cc-claw/workspace/",
@@ -3147,29 +3359,8 @@ var init_init = __esm({
3147
3359
  });
3148
3360
 
3149
3361
  // src/bootstrap/loader.ts
3150
- import { readFileSync as readFileSync3, existsSync as existsSync5, readdirSync, statSync as statSync2 } from "fs";
3362
+ import { readFileSync as readFileSync3, existsSync as existsSync5, readdirSync } from "fs";
3151
3363
  import { join as join4 } from "path";
3152
- function loadFileWithCache(path, cache) {
3153
- if (!existsSync5(path)) return null;
3154
- try {
3155
- const stat = statSync2(path);
3156
- if (stat.mtimeMs !== cache.mtime) {
3157
- cache.content = readFileSync3(path, "utf-8").trim();
3158
- cache.mtime = stat.mtimeMs;
3159
- log(`[bootstrap] Loaded ${path} (${cache.content.length} chars)`);
3160
- syncNativeCliFiles();
3161
- }
3162
- return cache.content;
3163
- } catch {
3164
- return null;
3165
- }
3166
- }
3167
- function loadSoul() {
3168
- return loadFileWithCache(SOUL_PATH2, soulCacheObj);
3169
- }
3170
- function loadUser() {
3171
- return loadFileWithCache(USER_PATH2, userCacheObj);
3172
- }
3173
3364
  function searchContext(userMessage) {
3174
3365
  if (!existsSync5(CONTEXT_DIR2)) return null;
3175
3366
  const msgWords = new Set(
@@ -3201,23 +3392,18 @@ function searchContext(userMessage) {
3201
3392
  } catch {
3202
3393
  }
3203
3394
  if (bestMatch && bestMatch.score >= 2) {
3395
+ if (bestMatch.content.length > MAX_CONTEXT_CHARS) {
3396
+ return bestMatch.content.slice(0, MAX_CONTEXT_CHARS) + "\n\u2026(truncated)";
3397
+ }
3204
3398
  return bestMatch.content;
3205
3399
  }
3206
3400
  return null;
3207
3401
  }
3208
- async function assembleBootstrapPrompt(userMessage, tier = "full", chatId) {
3402
+ async function assembleBootstrapPrompt(userMessage, tier = "full", chatId, permMode) {
3209
3403
  const sections = [];
3210
- const soul = loadSoul();
3211
- if (soul) {
3212
- sections.push(`[System instructions \u2014 follow these]
3213
- ${soul}`);
3214
- }
3215
- if (tier === "full" || tier === "heartbeat") {
3216
- const user = loadUser();
3217
- if (user) {
3218
- sections.push(`[User profile]
3219
- ${user}`);
3220
- }
3404
+ syncNativeCliFiles();
3405
+ if (permMode && permMode !== "yolo") {
3406
+ sections.push(buildPermissionNotice(permMode));
3221
3407
  }
3222
3408
  if (tier === "full") {
3223
3409
  const ctx = searchContext(userMessage);
@@ -3226,9 +3412,17 @@ ${user}`);
3226
3412
  ${ctx}`);
3227
3413
  }
3228
3414
  }
3229
- const memory2 = await injectMemoryContext(userMessage);
3230
- if (memory2) {
3231
- sections.push(memory2);
3415
+ if (chatId && tier !== "slim") {
3416
+ const bridge = consumeContextBridge(chatId);
3417
+ if (bridge) {
3418
+ sections.push(bridge);
3419
+ }
3420
+ }
3421
+ if (tier !== "slim") {
3422
+ const memory2 = await injectMemoryContext(userMessage);
3423
+ if (memory2) {
3424
+ sections.push(memory2);
3425
+ }
3232
3426
  }
3233
3427
  if (chatId && (tier === "full" || tier === "heartbeat")) {
3234
3428
  const orchestrationContext = buildOrchestrationContext(chatId);
@@ -3241,6 +3435,12 @@ ${ctx}`);
3241
3435
  log(`[bootstrap] Assembled prompt: tier=${tier}, sections=${sections.length}, totalChars=${result.length}`);
3242
3436
  return result;
3243
3437
  }
3438
+ function buildPermissionNotice(mode) {
3439
+ if (mode === "plan") {
3440
+ return "[Permission notice]\nYou are in restricted mode (plan). You can read and analyze files but CANNOT edit, write, or execute commands.\nIf the user's request requires modifications or execution, include the marker [NEED_PERMISSION] at the end of your response. Briefly explain what you would need to do and why elevated access is required. Do NOT attempt workarounds or refuse the task \u2014 signal the need and the system will ask the user to approve.\n[End permission notice]";
3441
+ }
3442
+ return "[Permission notice]\nYou are in restricted mode (safe). Only a subset of tools are available to you.\nIf the user's request requires tools you don't have access to, include the marker [NEED_PERMISSION] at the end of your response. Briefly explain what you would need to do and why elevated access is required. Do NOT attempt workarounds or refuse the task \u2014 signal the need and the system will ask the user to approve.\n[End permission notice]";
3443
+ }
3244
3444
  function truncateToTokenBudget(text, budget) {
3245
3445
  const charBudget = budget * 4;
3246
3446
  if (text.length <= charBudget) return text;
@@ -3285,7 +3485,7 @@ ${boardText}`);
3285
3485
  return null;
3286
3486
  }
3287
3487
  }
3288
- var SOUL_PATH2, USER_PATH2, CONTEXT_DIR2, soulCacheObj, userCacheObj, ACTIVITY_TOKEN_BUDGET, INBOX_TOKEN_BUDGET, WHITEBOARD_TOKEN_BUDGET;
3488
+ var CONTEXT_DIR2, MAX_CONTEXT_CHARS, ACTIVITY_TOKEN_BUDGET, INBOX_TOKEN_BUDGET, WHITEBOARD_TOKEN_BUDGET;
3289
3489
  var init_loader = __esm({
3290
3490
  "src/bootstrap/loader.ts"() {
3291
3491
  "use strict";
@@ -3296,11 +3496,8 @@ var init_loader = __esm({
3296
3496
  init_store3();
3297
3497
  init_store4();
3298
3498
  init_store();
3299
- SOUL_PATH2 = join4(WORKSPACE_PATH, "SOUL.md");
3300
- USER_PATH2 = join4(WORKSPACE_PATH, "USER.md");
3301
3499
  CONTEXT_DIR2 = join4(WORKSPACE_PATH, "context");
3302
- soulCacheObj = { content: null, mtime: 0 };
3303
- userCacheObj = { content: null, mtime: 0 };
3500
+ MAX_CONTEXT_CHARS = 4e3;
3304
3501
  ACTIVITY_TOKEN_BUDGET = 1500;
3305
3502
  INBOX_TOKEN_BUDGET = 2e3;
3306
3503
  WHITEBOARD_TOKEN_BUDGET = 500;
@@ -3308,66 +3505,62 @@ var init_loader = __esm({
3308
3505
  });
3309
3506
 
3310
3507
  // src/memory/session-log.ts
3311
- function appendToLog(chatId, userMessage, assistantResponse) {
3312
- if (!logs.has(chatId)) logs.set(chatId, []);
3313
- const entries = logs.get(chatId);
3508
+ function appendToLog(chatId, userMessage, assistantResponse, backend2, model2, sessionId) {
3314
3509
  const now = Date.now();
3315
- entries.push({ role: "user", text: userMessage, timestamp: now });
3316
- entries.push({ role: "assistant", text: assistantResponse, timestamp: now });
3317
- const maxEntries = MAX_PAIRS * 2;
3318
- if (entries.length > maxEntries) {
3319
- entries.splice(0, entries.length - maxEntries);
3510
+ appendMessageLog(chatId, "user", userMessage, backend2 ?? null, model2 ?? null, sessionId ?? null);
3511
+ appendMessageLog(chatId, "assistant", assistantResponse, backend2 ?? null, model2 ?? null, sessionId ?? null);
3512
+ const existing = cache.get(chatId) ?? [];
3513
+ existing.push(
3514
+ { role: "user", text: userMessage, timestamp: now },
3515
+ { role: "assistant", text: assistantResponse, timestamp: now }
3516
+ );
3517
+ if (existing.length > CACHE_SIZE * 2) {
3518
+ existing.splice(0, existing.length - CACHE_SIZE * 2);
3320
3519
  }
3520
+ cache.set(chatId, existing);
3321
3521
  }
3322
3522
  function getLog(chatId) {
3323
- return logs.get(chatId) ?? [];
3523
+ const rows = getUnsummarizedLog(chatId);
3524
+ return rows.map((r) => ({
3525
+ role: r.role,
3526
+ text: r.content,
3527
+ timestamp: (/* @__PURE__ */ new Date(r.created_at + (r.created_at.includes("Z") ? "" : "Z"))).getTime()
3528
+ }));
3529
+ }
3530
+ function getMessagePairCount(chatId) {
3531
+ const cached = cache.get(chatId);
3532
+ if (cached && cached.length > 0) {
3533
+ return Math.floor(cached.length / 2);
3534
+ }
3535
+ return Math.floor(getUnsummarizedLog(chatId).length / 2);
3324
3536
  }
3325
3537
  function clearLog(chatId) {
3326
- logs.delete(chatId);
3538
+ markMessageLogSummarized(chatId);
3539
+ cache.delete(chatId);
3327
3540
  }
3328
3541
  function getLoggedChatIds() {
3329
- return [...logs.keys()].filter((id) => (logs.get(id)?.length ?? 0) > 0);
3330
- }
3331
- function getMessagePairCount(chatId) {
3332
- return Math.floor((logs.get(chatId)?.length ?? 0) / 2);
3542
+ return getUnsummarizedChatIds();
3333
3543
  }
3334
- var logs, MAX_PAIRS;
3544
+ var CACHE_SIZE, cache;
3335
3545
  var init_session_log = __esm({
3336
3546
  "src/memory/session-log.ts"() {
3337
3547
  "use strict";
3338
- logs = /* @__PURE__ */ new Map();
3339
- MAX_PAIRS = 100;
3548
+ init_store4();
3549
+ CACHE_SIZE = 20;
3550
+ cache = /* @__PURE__ */ new Map();
3340
3551
  }
3341
3552
  });
3342
3553
 
3343
3554
  // src/memory/summarize.ts
3344
3555
  var summarize_exports = {};
3345
3556
  __export(summarize_exports, {
3557
+ CONTEXT_BRIDGE_PROMPT: () => CONTEXT_BRIDGE_PROMPT,
3346
3558
  summarizeAllPending: () => summarizeAllPending,
3347
- summarizeSession: () => summarizeSession
3559
+ summarizeSession: () => summarizeSession,
3560
+ summarizeWithFallbackChain: () => summarizeWithFallbackChain
3348
3561
  });
3349
3562
  import { spawn } from "child_process";
3350
3563
  import { createInterface } from "readline";
3351
- function resolveSummarizerAdapter(chatId) {
3352
- const config2 = getSummarizer(chatId);
3353
- if (config2.backend === "off") return null;
3354
- let adapter;
3355
- if (config2.backend) {
3356
- adapter = getAdapter(config2.backend);
3357
- } else {
3358
- try {
3359
- adapter = getAdapterForChat(chatId);
3360
- } catch {
3361
- try {
3362
- adapter = getAdapter("claude");
3363
- } catch {
3364
- return null;
3365
- }
3366
- }
3367
- }
3368
- const model2 = config2.model ?? adapter.summarizerModel;
3369
- return { adapter, model: model2 };
3370
- }
3371
3564
  function buildTranscript(entries) {
3372
3565
  const lines = [];
3373
3566
  let totalLen = 0;
@@ -3385,19 +3578,7 @@ function buildTranscript(entries) {
3385
3578
  }
3386
3579
  return lines.join("\n\n");
3387
3580
  }
3388
- async function summarizeSession(chatId) {
3389
- const pairCount = getMessagePairCount(chatId);
3390
- if (pairCount < MIN_PAIRS) {
3391
- clearLog(chatId);
3392
- return false;
3393
- }
3394
- const resolved = resolveSummarizerAdapter(chatId);
3395
- if (!resolved) {
3396
- clearLog(chatId);
3397
- return false;
3398
- }
3399
- const { adapter, model: model2 } = resolved;
3400
- const entries = getLog(chatId);
3581
+ async function attemptSummarize(chatId, adapter, model2, entries) {
3401
3582
  const transcript = buildTranscript(entries);
3402
3583
  const prompt = `${SUMMARIZE_PROMPT}
3403
3584
 
@@ -3416,7 +3597,6 @@ ${transcript}`;
3416
3597
  let inputTokens = 0;
3417
3598
  let outputTokens = 0;
3418
3599
  let cacheReadTokens = 0;
3419
- const SUMMARIZE_TIMEOUT_MS = 6e4;
3420
3600
  await new Promise((resolve) => {
3421
3601
  const proc = spawn(config2.executable, config2.args, {
3422
3602
  env,
@@ -3426,7 +3606,7 @@ ${transcript}`;
3426
3606
  proc.stderr?.resume();
3427
3607
  const rl2 = createInterface({ input: proc.stdout });
3428
3608
  const timeout = setTimeout(() => {
3429
- warn(`[summarize] Timeout after ${SUMMARIZE_TIMEOUT_MS / 1e3}s for chat ${chatId} \u2014 killing process`);
3609
+ warn(`[summarize] Timeout (${adapter.id}:${model2}) for chat ${chatId}`);
3430
3610
  rl2.close();
3431
3611
  proc.kill("SIGTERM");
3432
3612
  setTimeout(() => proc.kill("SIGKILL"), 2e3);
@@ -3441,9 +3621,7 @@ ${transcript}`;
3441
3621
  }
3442
3622
  const events = adapter.parseLine(msg);
3443
3623
  for (const ev of events) {
3444
- if (ev.type === "text" && ev.text) {
3445
- accumulatedText += ev.text;
3446
- }
3624
+ if (ev.type === "text" && ev.text) accumulatedText += ev.text;
3447
3625
  if (ev.type === "usage" && ev.usage) {
3448
3626
  inputTokens += ev.usage.input;
3449
3627
  outputTokens += ev.usage.output;
@@ -3452,9 +3630,9 @@ ${transcript}`;
3452
3630
  if (ev.type === "result") {
3453
3631
  resultText = ev.resultText || accumulatedText;
3454
3632
  if (ev.usage) {
3455
- inputTokens += ev.usage.input;
3456
- outputTokens += ev.usage.output;
3457
- cacheReadTokens += ev.usage.cacheRead;
3633
+ inputTokens = ev.usage.input;
3634
+ outputTokens = ev.usage.output;
3635
+ cacheReadTokens = ev.usage.cacheRead;
3458
3636
  }
3459
3637
  if (adapter.shouldKillOnResult()) {
3460
3638
  rl2.close();
@@ -3475,11 +3653,9 @@ ${transcript}`;
3475
3653
  if (inputTokens + outputTokens > 0) {
3476
3654
  addUsage(chatId, inputTokens, outputTokens, cacheReadTokens, model2);
3477
3655
  }
3656
+ if (!resultText) resultText = accumulatedText;
3478
3657
  if (!resultText) {
3479
- resultText = accumulatedText;
3480
- }
3481
- if (!resultText) {
3482
- warn(`[summarize] ${adapter.displayName} returned empty result for chat ${chatId} \u2014 keeping log for retry`);
3658
+ warn(`[summarize] ${adapter.id}:${model2} returned empty result for chat ${chatId}`);
3483
3659
  return false;
3484
3660
  }
3485
3661
  const summaryMatch = resultText.match(/SUMMARY:\s*(.+?)(?=\nKEY_DETAILS:|\nTOPICS:|$)/s);
@@ -3487,20 +3663,72 @@ ${transcript}`;
3487
3663
  const topicsMatch = resultText.match(/TOPICS:\s*(.+)/);
3488
3664
  let summary = summaryMatch?.[1]?.trim() ?? resultText.trim();
3489
3665
  const keyDetails = keyDetailsMatch?.[1]?.trim();
3490
- if (keyDetails) {
3491
- summary += `
3666
+ if (keyDetails) summary += `
3492
3667
  Key details: ${keyDetails}`;
3493
- }
3494
3668
  const topics = topicsMatch?.[1]?.trim() ?? "";
3495
- saveSessionSummaryWithEmbedding(chatId, summary, topics, pairCount);
3496
- log(`[summarize] Saved session summary for chat ${chatId} via ${adapter.id}:${model2} (${pairCount} pairs, topics: ${topics})`);
3497
- clearLog(chatId);
3669
+ saveSessionSummaryWithEmbedding(chatId, summary, topics, Math.floor(entries.length / 2));
3670
+ log(`[summarize] Saved summary via ${adapter.id}:${model2} for chat ${chatId}`);
3498
3671
  return true;
3499
3672
  } catch (err) {
3500
- warn(`[summarize] Failed for chat ${chatId}: ${errorMessage(err)} \u2014 keeping log for retry`);
3673
+ warn(`[summarize] ${adapter.id}:${model2} failed for ${chatId}: ${errorMessage(err)}`);
3501
3674
  return false;
3502
3675
  }
3503
3676
  }
3677
+ async function summarizeWithFallbackChain(chatId, targetBackendId) {
3678
+ const pairCount = getMessagePairCount(chatId);
3679
+ if (pairCount < MIN_PAIRS) {
3680
+ clearLog(chatId);
3681
+ return false;
3682
+ }
3683
+ const entries = getLog(chatId);
3684
+ if (entries.length === 0) return false;
3685
+ const tried = /* @__PURE__ */ new Set();
3686
+ try {
3687
+ const config2 = getSummarizer(chatId);
3688
+ if (config2.backend !== "off") {
3689
+ const adapter = config2.backend ? getAdapter(config2.backend) : getAdapterForChat(chatId);
3690
+ const model2 = config2.model ?? adapter.summarizerModel;
3691
+ const key = `${adapter.id}:${model2}`;
3692
+ tried.add(key);
3693
+ if (await attemptSummarize(chatId, adapter, model2, entries)) {
3694
+ clearLog(chatId);
3695
+ return true;
3696
+ }
3697
+ }
3698
+ } catch {
3699
+ }
3700
+ if (targetBackendId) {
3701
+ try {
3702
+ const targetAdapter = getAdapter(targetBackendId);
3703
+ const model2 = targetAdapter.summarizerModel;
3704
+ const key = `${targetAdapter.id}:${model2}`;
3705
+ if (!tried.has(key)) {
3706
+ tried.add(key);
3707
+ if (await attemptSummarize(chatId, targetAdapter, model2, entries)) {
3708
+ clearLog(chatId);
3709
+ return true;
3710
+ }
3711
+ }
3712
+ } catch {
3713
+ }
3714
+ }
3715
+ for (const adapter of getAllAdapters()) {
3716
+ const model2 = adapter.summarizerModel;
3717
+ const key = `${adapter.id}:${model2}`;
3718
+ if (!tried.has(key)) {
3719
+ tried.add(key);
3720
+ if (await attemptSummarize(chatId, adapter, model2, entries)) {
3721
+ clearLog(chatId);
3722
+ return true;
3723
+ }
3724
+ }
3725
+ }
3726
+ warn(`[summarize] All fallback attempts failed for chat ${chatId} \u2014 raw log preserved`);
3727
+ return false;
3728
+ }
3729
+ async function summarizeSession(chatId) {
3730
+ return summarizeWithFallbackChain(chatId);
3731
+ }
3504
3732
  async function summarizeAllPending() {
3505
3733
  const chatIds = getLoggedChatIds();
3506
3734
  if (chatIds.length === 0) return;
@@ -3509,7 +3737,7 @@ async function summarizeAllPending() {
3509
3737
  await summarizeSession(chatId);
3510
3738
  }
3511
3739
  }
3512
- var MIN_PAIRS, USER_MSG_LIMIT, AGENT_MSG_LIMIT, TRANSCRIPT_CAP, SUMMARIZE_PROMPT;
3740
+ var MIN_PAIRS, USER_MSG_LIMIT, AGENT_MSG_LIMIT, TRANSCRIPT_CAP, SUMMARIZE_TIMEOUT_MS, SUMMARIZE_PROMPT, CONTEXT_BRIDGE_PROMPT;
3513
3741
  var init_summarize = __esm({
3514
3742
  "src/memory/summarize.ts"() {
3515
3743
  "use strict";
@@ -3521,18 +3749,20 @@ var init_summarize = __esm({
3521
3749
  USER_MSG_LIMIT = 4e3;
3522
3750
  AGENT_MSG_LIMIT = 6e3;
3523
3751
  TRANSCRIPT_CAP = 1e5;
3752
+ SUMMARIZE_TIMEOUT_MS = 6e4;
3524
3753
  SUMMARIZE_PROMPT = `You are summarizing a conversation session for long-term episodic memory. This summary will be injected into future conversations to provide context, so it must contain enough specific detail to be useful weeks or months later.
3525
3754
 
3526
3755
  Instructions:
3527
3756
  1. SUMMARY: Write a detailed summary (5-15 sentences depending on session complexity). Capture:
3528
3757
  - What the user asked for and why
3529
3758
  - What was done, decided, or accomplished
3530
- - Specific details that would be hard to rediscover: names, dates, values, locations, references, sources, outcomes
3531
- - Anything left unfinished or planned for later
3759
+ - Specific details that would be hard to rediscover: names, dates, values, locations, references, sources, decisions, outcomes \u2014 across any domain
3760
+ - The state at session end: what's done, what's in progress, what's planned next
3532
3761
  - Any preferences, corrections, or instructions the user expressed
3533
3762
 
3534
3763
  2. KEY_DETAILS: List the 3-8 most important specific facts as bullet points. These should be concrete and retrievable \u2014 not vague.
3535
3764
  Good: "User's quarterly report deadline is March 15"
3765
+ Good: "Flight booked to NYC on April 3, confirmation #AB1234"
3536
3766
  Bad: "Discussed deadlines"
3537
3767
 
3538
3768
  3. TOPICS: Extract 5-10 specific topic keywords that would help retrieve this session in a future search. Use proper nouns, specific terms, and distinguishing phrases \u2014 NOT generic words like "discussion" or "help".
@@ -3543,6 +3773,14 @@ KEY_DETAILS:
3543
3773
  - <detail 1>
3544
3774
  - <detail 2>
3545
3775
  TOPICS: <specific-keyword1, specific-keyword2, ...>`;
3776
+ CONTEXT_BRIDGE_PROMPT = `You are handing off an active work session to a new AI assistant. Write a concise handoff brief (3-8 sentences) covering:
3777
+ - What the user is actively working on RIGHT NOW
3778
+ - Exact current state: what's done, what's in progress, what's blocked
3779
+ - Specific details needed to continue: names, dates, decisions, references, links, preferences, or any domain-specific context relevant to the work
3780
+ - The immediate next step the assistant should be ready to take
3781
+ - Any critical constraints or preferences expressed by the user
3782
+
3783
+ Be specific. The new assistant has no prior context. Cover any domain \u2014 personal, research, scheduling, content, technical \u2014 whatever applies to this session.`;
3546
3784
  }
3547
3785
  });
3548
3786
 
@@ -3579,20 +3817,20 @@ var init_registry = __esm({
3579
3817
  });
3580
3818
 
3581
3819
  // src/agents/roles.ts
3582
- function buildRoleInstructions(role, task, persona) {
3820
+ function buildRoleInstructions(role, task, persona, includeTools = true) {
3583
3821
  const rolePrompt = persona ?? ROLE_PRESETS[role] ?? ROLE_PRESETS.worker;
3584
3822
  const capitalizedRole = role.charAt(0).toUpperCase() + role.slice(1);
3585
- return [
3823
+ const parts = [
3586
3824
  `## Your Role: ${capitalizedRole}`,
3587
3825
  "",
3588
3826
  rolePrompt,
3589
- "",
3590
- ORCHESTRATOR_TOOLS,
3591
- "",
3592
- "## Task",
3593
- "",
3594
- task
3595
- ].join("\n");
3827
+ ""
3828
+ ];
3829
+ if (includeTools) {
3830
+ parts.push(ORCHESTRATOR_TOOLS, "");
3831
+ }
3832
+ parts.push("## Task", "", task);
3833
+ return parts.join("\n");
3596
3834
  }
3597
3835
  var ROLE_PRESETS, ORCHESTRATOR_TOOLS;
3598
3836
  var init_roles = __esm({
@@ -3660,7 +3898,8 @@ function buildAgentPrompt(opts, runnerSkillPath) {
3660
3898
  }
3661
3899
  parts.push("");
3662
3900
  }
3663
- parts.push(buildRoleInstructions(opts.role ?? "worker", opts.task, opts.persona));
3901
+ const includeTools = opts.includeOrchestratorTools !== false;
3902
+ parts.push(buildRoleInstructions(opts.role ?? "worker", opts.task, opts.persona, includeTools));
3664
3903
  return parts.join("\n");
3665
3904
  }
3666
3905
  function buildSpawnEnv(runner, isSubAgent = false) {
@@ -4243,7 +4482,8 @@ async function startAgent(agentId, chatId, opts) {
4243
4482
  role: opts.role,
4244
4483
  persona: opts.persona,
4245
4484
  extraArgs: mcpExtraArgs.length ? mcpExtraArgs : void 0,
4246
- isSubAgent: true
4485
+ isSubAgent: true,
4486
+ includeOrchestratorTools: process.env.DASHBOARD_ENABLED === "1"
4247
4487
  }, {
4248
4488
  onText: () => {
4249
4489
  updateAgentOutput(db3, agentId);
@@ -4458,7 +4698,7 @@ function startNextQueued(chatId) {
4458
4698
  description: agent.description ?? void 0,
4459
4699
  model: agent.model ?? void 0,
4460
4700
  skills: JSON.parse(agent.skills),
4461
- permMode: agent.permMode === "inherit" ? void 0 : agent.permMode,
4701
+ permMode: agent.permMode === "inherit" ? getMode(chatId) : agent.permMode,
4462
4702
  role: agent.role ?? "worker",
4463
4703
  persona: agent.persona ?? void 0,
4464
4704
  maxRuntimeMs: agent.maxRuntimeMs,
@@ -4964,6 +5204,10 @@ function startDashboard() {
4964
5204
  if (body.cwd && !existsSync8(body.cwd)) {
4965
5205
  return jsonResponse(res, { error: `Directory not found: ${body.cwd}` }, 400);
4966
5206
  }
5207
+ if (!body.permMode || body.permMode === "inherit") {
5208
+ const { getMode: getMode2 } = await Promise.resolve().then(() => (init_store4(), store_exports3));
5209
+ body.permMode = getMode2(body.chatId);
5210
+ }
4967
5211
  const result = await spawnSubAgent(body.chatId, body);
4968
5212
  return jsonResponse(res, result);
4969
5213
  } catch (err) {
@@ -5216,7 +5460,10 @@ function startDashboard() {
5216
5460
  const { getMode: getMode2, getCwd: getCwd2, getModel: getModel2, addUsage: addUsage2, getBackend: getBackend2 } = await Promise.resolve().then(() => (init_store4(), store_exports3));
5217
5461
  const { getAdapterForChat: getAdapterForChat2 } = await Promise.resolve().then(() => (init_backends(), backends_exports));
5218
5462
  const chatId = body.chatId;
5219
- const mode = body.mode ?? getMode2(chatId);
5463
+ const PERM_LEVEL = { plan: 0, safe: 1, yolo: 2 };
5464
+ const storedMode = getMode2(chatId);
5465
+ const requestedMode = body.mode ?? storedMode;
5466
+ const mode = (PERM_LEVEL[requestedMode] ?? 2) <= (PERM_LEVEL[storedMode] ?? 2) ? requestedMode : storedMode;
5220
5467
  const cwd = body.cwd ?? getCwd2(chatId);
5221
5468
  const model2 = body.model ?? getModel2(chatId) ?? (() => {
5222
5469
  try {
@@ -5866,13 +6113,13 @@ function askAgent(chatId, userMessage, opts) {
5866
6113
  return withChatLock(chatId, () => askAgentImpl(chatId, userMessage, opts));
5867
6114
  }
5868
6115
  async function askAgentImpl(chatId, userMessage, opts) {
5869
- const { cwd, onStream, model: model2, permMode, onToolAction, bootstrapTier, timeoutMs } = opts ?? {};
5870
- const adapter = getAdapterForChat(chatId);
5871
- const mode = permMode ?? "yolo";
6116
+ const { cwd, onStream, model: model2, backend: backend2, permMode, onToolAction, bootstrapTier, timeoutMs } = opts ?? {};
6117
+ const adapter = backend2 ? getAdapter(backend2) : getAdapterForChat(chatId);
6118
+ const mode = permMode ?? getMode(chatId);
5872
6119
  const thinkingLevel = getThinkingLevel(chatId);
5873
6120
  const resolvedCwd = cwd ?? WORKSPACE_PATH;
5874
6121
  const tier = bootstrapTier ?? "full";
5875
- const fullPrompt = await assembleBootstrapPrompt(userMessage, tier, chatId);
6122
+ const fullPrompt = await assembleBootstrapPrompt(userMessage, tier, chatId, mode);
5876
6123
  const existingSessionId = getSessionId(chatId);
5877
6124
  const allowedTools = getEnabledTools(chatId);
5878
6125
  const mcpConfigPath = getMcpConfigPath(chatId);
@@ -5933,7 +6180,17 @@ async function askAgentImpl(chatId, userMessage, opts) {
5933
6180
  clearSession(chatId);
5934
6181
  }
5935
6182
  if (result.resultText) {
5936
- appendToLog(chatId, userMessage, result.resultText);
6183
+ appendToLog(chatId, userMessage, result.resultText, adapter.id, model2 ?? null, result.sessionId ?? null);
6184
+ const AUTO_SUMMARIZE_THRESHOLD = 30;
6185
+ const pairCount = getMessagePairCount(chatId);
6186
+ if (pairCount >= AUTO_SUMMARIZE_THRESHOLD) {
6187
+ log(`[agent] Auto-summarizing chat ${chatId} after ${pairCount} turns`);
6188
+ summarizeWithFallbackChain(chatId).then((saved) => {
6189
+ if (saved) opts?.onCompaction?.(chatId);
6190
+ }).catch((err) => {
6191
+ warn(`[agent] Auto-summarize failed for chat ${chatId}: ${err}`);
6192
+ });
6193
+ }
5937
6194
  }
5938
6195
  return {
5939
6196
  text: result.resultText || `(No response from ${adapter.displayName})`,
@@ -6125,6 +6382,9 @@ var init_delivery = __esm({
6125
6382
  // src/scheduler/retry.ts
6126
6383
  function classifyError(err) {
6127
6384
  const msg = err instanceof Error ? err.message : String(err);
6385
+ for (const pattern of EXHAUSTED_PATTERNS) {
6386
+ if (pattern.test(msg)) return "exhausted";
6387
+ }
6128
6388
  for (const pattern of TRANSIENT_PATTERNS) {
6129
6389
  if (pattern.test(msg)) return "transient";
6130
6390
  }
@@ -6138,10 +6398,21 @@ function getBackoffMs(retryCount) {
6138
6398
  const jitter = Math.floor(base * 0.2 * (Math.random() * 2 - 1));
6139
6399
  return base + jitter;
6140
6400
  }
6141
- var TRANSIENT_PATTERNS, PERMANENT_PATTERNS, BACKOFF_MS, MAX_RETRIES, AUTO_PAUSE_THRESHOLD;
6401
+ var EXHAUSTED_PATTERNS, TRANSIENT_PATTERNS, PERMANENT_PATTERNS, BACKOFF_MS, MAX_RETRIES, AUTO_PAUSE_THRESHOLD;
6142
6402
  var init_retry = __esm({
6143
6403
  "src/scheduler/retry.ts"() {
6144
6404
  "use strict";
6405
+ EXHAUSTED_PATTERNS = [
6406
+ /out of.*usage/i,
6407
+ /\d+-hour limit reached/i,
6408
+ /usage limit reached/i,
6409
+ /hit your.*limit/i,
6410
+ /usage.?limit/i,
6411
+ /exceeded your current quota/i,
6412
+ /RESOURCE.?EXHAUSTED/i,
6413
+ /insufficient.?quota/i,
6414
+ /check your plan and billing/i
6415
+ ];
6145
6416
  TRANSIENT_PATTERNS = [
6146
6417
  /rate.?limit/i,
6147
6418
  /too many requests/i,
@@ -6458,23 +6729,50 @@ async function runWithRetry(job, model2, runId, t0) {
6458
6729
  if (job.thinking && job.thinking !== "auto") {
6459
6730
  setThinkingLevel(chatId, job.thinking);
6460
6731
  }
6461
- for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
6462
- try {
6463
- const timeoutMs = job.timeout ? job.timeout * 1e3 : void 0;
6464
- const response = await askAgent(chatId, job.description, { model: model2, bootstrapTier: "slim", timeoutMs });
6465
- return response;
6466
- } catch (err) {
6467
- lastError = err;
6468
- const errorClass = classifyError(err);
6469
- if (errorClass === "permanent" || attempt >= MAX_RETRIES) {
6470
- throw err;
6732
+ const timeoutMs = job.timeout ? job.timeout * 1e3 : void 0;
6733
+ const primaryBackend = resolveJobBackendId(job);
6734
+ const chain = [
6735
+ { backend: primaryBackend, model: model2 },
6736
+ ...job.fallbacks ?? []
6737
+ ];
6738
+ for (let chainIdx = 0; chainIdx < chain.length; chainIdx++) {
6739
+ const { backend: currentBackend, model: currentModel } = chain[chainIdx];
6740
+ const isFallback = chainIdx > 0;
6741
+ if (isFallback) {
6742
+ log(`[scheduler] Job #${job.id} falling back to ${currentBackend}:${currentModel} (fallback ${chainIdx}/${job.fallbacks.length})`);
6743
+ }
6744
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
6745
+ try {
6746
+ const response = await askAgent(chatId, job.description, {
6747
+ model: currentModel,
6748
+ backend: currentBackend,
6749
+ bootstrapTier: "slim",
6750
+ timeoutMs,
6751
+ permMode: job.sessionType === "main" ? getMode(job.chatId) : "yolo"
6752
+ });
6753
+ if (isFallback) {
6754
+ response.text = `[Fallback: ran on ${currentBackend}:${currentModel}]
6755
+
6756
+ ${response.text}`;
6757
+ }
6758
+ return response;
6759
+ } catch (err) {
6760
+ lastError = err;
6761
+ const errorClass = classifyError(err);
6762
+ if (errorClass === "exhausted") {
6763
+ log(`[scheduler] Job #${job.id} backend ${currentBackend} exhausted: ${errorMessage(err)}`);
6764
+ break;
6765
+ }
6766
+ if (errorClass === "permanent" || attempt >= MAX_RETRIES) {
6767
+ throw err;
6768
+ }
6769
+ const backoffMs = getBackoffMs(attempt);
6770
+ log(`[scheduler] Job #${job.id} transient error (attempt ${attempt + 1}/${MAX_RETRIES}), retrying in ${backoffMs / 1e3}s: ${errorMessage(err)}`);
6771
+ await new Promise((r) => setTimeout(r, backoffMs));
6471
6772
  }
6472
- const backoffMs = getBackoffMs(attempt);
6473
- log(`[scheduler] Job #${job.id} transient error (attempt ${attempt + 1}/${MAX_RETRIES}), retrying in ${backoffMs / 1e3}s: ${errorMessage(err)}`);
6474
- await new Promise((r) => setTimeout(r, backoffMs));
6475
6773
  }
6476
6774
  }
6477
- throw lastError ?? new Error("Unknown error");
6775
+ throw lastError ?? new Error("All backends exhausted");
6478
6776
  }
6479
6777
  function resolveJobBackendId(job) {
6480
6778
  return job.backend ?? (() => {
@@ -6578,7 +6876,7 @@ function wrapBackendAdapter(adapter) {
6578
6876
  const config2 = adapter.buildSpawnConfig({
6579
6877
  prompt: opts.prompt,
6580
6878
  sessionId: opts.sessionId,
6581
- permMode: opts.permMode ?? "yolo",
6879
+ permMode: opts.permMode ?? "plan",
6582
6880
  allowedTools: opts.allowedTools ?? [],
6583
6881
  cwd: opts.cwd
6584
6882
  });
@@ -7146,6 +7444,7 @@ var init_telegram2 = __esm({
7146
7444
  // Skills & profile
7147
7445
  { command: "skills", description: "List and invoke skills" },
7148
7446
  { command: "voice", description: "Toggle voice responses" },
7447
+ { command: "imagine", description: "Generate an image from a prompt" },
7149
7448
  { command: "heartbeat", description: "Configure proactive heartbeat" },
7150
7449
  { command: "chats", description: "Manage multi-chat aliases" }
7151
7450
  ]);
@@ -7791,7 +8090,7 @@ async function finalizeProfile(chatId, state, channel) {
7791
8090
  "<!-- Add any additional preferences below this line -->",
7792
8091
  ""
7793
8092
  ].join("\n");
7794
- writeFileSync4(USER_PATH3, content, "utf-8");
8093
+ writeFileSync4(USER_PATH2, content, "utf-8");
7795
8094
  activeProfiles.delete(chatId);
7796
8095
  log(`[profile] User profile saved for chat ${chatId}`);
7797
8096
  await channel.sendText(
@@ -7817,23 +8116,23 @@ function extractUserUpdates(text) {
7817
8116
  return { cleanText, updates };
7818
8117
  }
7819
8118
  function appendToUserProfile(key, value) {
7820
- if (!existsSync11(USER_PATH3)) return;
7821
- const content = readFileSync7(USER_PATH3, "utf-8");
8119
+ if (!existsSync11(USER_PATH2)) return;
8120
+ const content = readFileSync7(USER_PATH2, "utf-8");
7822
8121
  const line = `- **${key}**: ${value}`;
7823
8122
  if (content.includes(line)) return;
7824
8123
  const updated = content.trimEnd() + `
7825
8124
  ${line}
7826
8125
  `;
7827
- writeFileSync4(USER_PATH3, updated, "utf-8");
8126
+ writeFileSync4(USER_PATH2, updated, "utf-8");
7828
8127
  log(`[profile] Appended preference: ${key}=${value}`);
7829
8128
  }
7830
- var USER_PATH3, activeProfiles;
8129
+ var USER_PATH2, activeProfiles;
7831
8130
  var init_profile = __esm({
7832
8131
  "src/bootstrap/profile.ts"() {
7833
8132
  "use strict";
7834
8133
  init_paths();
7835
8134
  init_log();
7836
- USER_PATH3 = join12(WORKSPACE_PATH, "USER.md");
8135
+ USER_PATH2 = join12(WORKSPACE_PATH, "USER.md");
7837
8136
  activeProfiles = /* @__PURE__ */ new Map();
7838
8137
  }
7839
8138
  });
@@ -8024,14 +8323,6 @@ var init_heartbeat = __esm({
8024
8323
  });
8025
8324
 
8026
8325
  // src/format-time.ts
8027
- function formatLocalDate(utcDatetime) {
8028
- const d = parseUtcDatetime(utcDatetime);
8029
- if (!d) return utcDatetime.split("T")[0] ?? utcDatetime.split(" ")[0];
8030
- const year = d.getFullYear();
8031
- const month = String(d.getMonth() + 1).padStart(2, "0");
8032
- const day = String(d.getDate()).padStart(2, "0");
8033
- return `${year}-${month}-${day}`;
8034
- }
8035
8326
  function formatLocalDateTime(utcDatetime) {
8036
8327
  const d = parseUtcDatetime(utcDatetime);
8037
8328
  if (!d) return utcDatetime;
@@ -8069,6 +8360,83 @@ var init_format_time = __esm({
8069
8360
  }
8070
8361
  });
8071
8362
 
8363
+ // src/media/image-gen.ts
8364
+ import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, existsSync as existsSync13 } from "fs";
8365
+ import { join as join14 } from "path";
8366
+ async function generateImage(prompt) {
8367
+ const apiKey = process.env.GEMINI_API_KEY;
8368
+ if (!apiKey) {
8369
+ throw new Error("Image generation requires GEMINI_API_KEY. Configure it in ~/.cc-claw/.env");
8370
+ }
8371
+ log(`[image-gen] Generating image: "${prompt.slice(0, 100)}"`);
8372
+ const response = await fetch(
8373
+ `https://generativelanguage.googleapis.com/v1beta/models/${IMAGE_MODEL}:generateContent?key=${apiKey}`,
8374
+ {
8375
+ method: "POST",
8376
+ headers: { "Content-Type": "application/json" },
8377
+ body: JSON.stringify({
8378
+ contents: [
8379
+ {
8380
+ parts: [{ text: prompt }]
8381
+ }
8382
+ ],
8383
+ generationConfig: {
8384
+ responseModalities: ["TEXT", "IMAGE"]
8385
+ }
8386
+ })
8387
+ }
8388
+ );
8389
+ if (!response.ok) {
8390
+ const errText = await response.text();
8391
+ throw new Error(`Gemini image API error: ${response.status} ${errText.slice(0, 300)}`);
8392
+ }
8393
+ const data = await response.json();
8394
+ const parts = data.candidates?.[0]?.content?.parts;
8395
+ if (!parts || parts.length === 0) {
8396
+ throw new Error("Gemini returned no content for image generation");
8397
+ }
8398
+ let imageData = null;
8399
+ let mimeType = "image/png";
8400
+ let textResponse = null;
8401
+ for (const part of parts) {
8402
+ if (part.inlineData) {
8403
+ imageData = part.inlineData.data;
8404
+ mimeType = part.inlineData.mimeType ?? "image/png";
8405
+ } else if (part.text) {
8406
+ textResponse = part.text;
8407
+ }
8408
+ }
8409
+ if (!imageData) {
8410
+ throw new Error(textResponse ?? "Gemini did not generate an image. The prompt may have been filtered.");
8411
+ }
8412
+ if (!existsSync13(IMAGE_OUTPUT_DIR)) {
8413
+ mkdirSync5(IMAGE_OUTPUT_DIR, { recursive: true });
8414
+ }
8415
+ const ext = mimeType.includes("jpeg") || mimeType.includes("jpg") ? "jpg" : "png";
8416
+ const filename = `img_${Date.now()}.${ext}`;
8417
+ const filePath = join14(IMAGE_OUTPUT_DIR, filename);
8418
+ const buffer = Buffer.from(imageData, "base64");
8419
+ writeFileSync5(filePath, buffer);
8420
+ log(`[image-gen] Saved ${buffer.length} bytes to ${filePath}`);
8421
+ return { filePath, text: textResponse, mimeType };
8422
+ }
8423
+ function isImageGenAvailable() {
8424
+ return !!process.env.GEMINI_API_KEY;
8425
+ }
8426
+ var IMAGE_MODEL, IMAGE_OUTPUT_DIR;
8427
+ var init_image_gen = __esm({
8428
+ "src/media/image-gen.ts"() {
8429
+ "use strict";
8430
+ init_log();
8431
+ IMAGE_MODEL = "gemini-3.1-flash-image-preview";
8432
+ IMAGE_OUTPUT_DIR = join14(
8433
+ process.env.CC_CLAW_HOME ?? join14(process.env.HOME ?? "/tmp", ".cc-claw"),
8434
+ "data",
8435
+ "images"
8436
+ );
8437
+ }
8438
+ });
8439
+
8072
8440
  // src/voice/stt.ts
8073
8441
  import crypto from "crypto";
8074
8442
  import { execFile as execFile2 } from "child_process";
@@ -9137,7 +9505,7 @@ async function handleCommand(msg, channel) {
9137
9505
  case "help":
9138
9506
  await channel.sendText(
9139
9507
  chatId,
9140
- "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/forget <keyword> - Remove a memory\n/voice - Toggle voice responses\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/readonly/plan)\n/verbose - Tool visibility (off/normal/verbose)\n/agents - List active sub-agents\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",
9508
+ "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/forget <keyword> - Remove a memory\n/voice - Toggle voice responses\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/agents - List active sub-agents\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",
9141
9509
  "plain"
9142
9510
  );
9143
9511
  break;
@@ -9197,7 +9565,7 @@ async function handleCommand(msg, channel) {
9197
9565
  toolButtons.push(row);
9198
9566
  }
9199
9567
  toolButtons.push([{ label: "Reset to defaults (all on)", data: "tool:reset" }]);
9200
- const modeNote = currentMode === "readonly" ? "\n\nNote: In read-only mode, tool list is ignored (Read/Glob/Grep only)." : currentMode === "plan" ? "\n\nNote: In plan mode, no tools execute." : currentMode === "yolo" ? "\n\nNote: In YOLO mode, tool list is ignored (all tools allowed)." : "";
9568
+ const modeNote = currentMode === "plan" ? "\n\nNote: In plan mode, tool list is ignored (read-only)." : currentMode === "yolo" ? "\n\nNote: In YOLO mode, tool list is ignored (all tools allowed)." : "";
9201
9569
  await channel.sendKeyboard(
9202
9570
  chatId,
9203
9571
  `Configure allowed tools (mode: ${currentMode})${modeNote}
@@ -9233,7 +9601,15 @@ Tap to toggle:`,
9233
9601
  } else {
9234
9602
  const pairs = getMessagePairCount(chatId);
9235
9603
  if (pairs < 2) {
9236
- await channel.sendText(chatId, "Not enough conversation to summarize (need at least 2 exchanges). Session log is preserved.", "plain");
9604
+ if (getSessionId(chatId)) {
9605
+ await channel.sendText(
9606
+ chatId,
9607
+ "Session log was cleared (auto-compact or service restart). Your conversation history is preserved in episodic memory. Continue chatting normally, or use /newchat to start fresh.",
9608
+ "plain"
9609
+ );
9610
+ } else {
9611
+ await channel.sendText(chatId, "Not enough conversation to summarize (need at least 2 exchanges).", "plain");
9612
+ }
9237
9613
  break;
9238
9614
  }
9239
9615
  await channel.sendText(chatId, `Summarizing current session (${pairs} exchanges)...`, "plain");
@@ -9620,6 +9996,30 @@ ${lines.join("\n")}`, "plain");
9620
9996
  );
9621
9997
  break;
9622
9998
  }
9999
+ case "imagine":
10000
+ case "image": {
10001
+ if (!commandArgs) {
10002
+ await channel.sendText(chatId, "Usage: /imagine <prompt>\nExample: /imagine a cat astronaut on Mars", "plain");
10003
+ return;
10004
+ }
10005
+ if (!isImageGenAvailable()) {
10006
+ await channel.sendText(chatId, "Image generation requires GEMINI_API_KEY. Configure it in ~/.cc-claw/.env", "plain");
10007
+ return;
10008
+ }
10009
+ await channel.sendText(chatId, "\u{1F3A8} Generating image\u2026", "plain");
10010
+ try {
10011
+ const result = await generateImage(commandArgs);
10012
+ const file = await readFile5(result.filePath);
10013
+ const name = result.filePath.split("/").pop() ?? "image.png";
10014
+ await channel.sendFile(chatId, file, name);
10015
+ if (result.text) {
10016
+ await channel.sendText(chatId, result.text, "plain");
10017
+ }
10018
+ } catch (err) {
10019
+ await channel.sendText(chatId, `Image generation failed: ${errorMessage(err)}`, "plain");
10020
+ }
10021
+ break;
10022
+ }
9623
10023
  case "schedule": {
9624
10024
  if (!commandArgs) {
9625
10025
  await channel.sendText(
@@ -9742,20 +10142,38 @@ ${lines.join("\n")}`, "plain");
9742
10142
  break;
9743
10143
  }
9744
10144
  case "history": {
9745
- const historyLimit = commandArgs ? parseInt(commandArgs, 10) || 5 : 5;
9746
- const summaries = listSessionSummaries(historyLimit, chatId);
9747
- if (summaries.length === 0) {
9748
- await channel.sendText(chatId, "No session history yet. History is saved automatically when you use /newchat.", "plain");
9749
- return;
10145
+ const query = commandArgs?.trim() ?? "";
10146
+ if (query) {
10147
+ const results = searchMessageLog(chatId, query, 20);
10148
+ if (results.length === 0) {
10149
+ await channel.sendText(chatId, `No matching history found. Try /memories ${query} for older sessions.`, "plain");
10150
+ break;
10151
+ }
10152
+ const lines = [`\u{1F4CB} History: "${query}"`, "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"];
10153
+ for (const row of results.slice(0, 10)) {
10154
+ const roleLabel = row.role === "user" ? "You" : "Assistant";
10155
+ const date = row.created_at.slice(0, 10);
10156
+ const preview = row.content.slice(0, 300) + (row.content.length > 300 ? "\u2026" : "");
10157
+ lines.push(`[${row.backend ?? "?"} \xB7 ${date}] ${roleLabel}: ${preview}`);
10158
+ }
10159
+ lines.push("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
10160
+ await channel.sendText(chatId, lines.join("\n"), "plain");
10161
+ } else {
10162
+ const rows = getRecentMessageLog(chatId, 20).reverse();
10163
+ if (rows.length === 0) {
10164
+ await channel.sendText(chatId, "No conversation history found.", "plain");
10165
+ break;
10166
+ }
10167
+ const lines = ["\u{1F4CB} Recent conversation (last 10 exchanges):", "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"];
10168
+ for (const row of rows) {
10169
+ const roleLabel = row.role === "user" ? "You" : "Assistant";
10170
+ const date = row.created_at.slice(0, 10);
10171
+ const preview = row.content.slice(0, 300) + (row.content.length > 300 ? "\u2026" : "");
10172
+ lines.push(`[${row.backend ?? "?"} \xB7 ${date}] ${roleLabel}: ${preview}`);
10173
+ }
10174
+ lines.push("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
10175
+ await channel.sendText(chatId, lines.join("\n"), "plain");
9750
10176
  }
9751
- const lines = summaries.map((s) => {
9752
- const date = formatLocalDate(s.created_at);
9753
- const shortSummary = s.summary.length > 200 ? s.summary.slice(0, 200) + "\u2026" : s.summary;
9754
- return `${date} (${s.message_count} msgs)
9755
- ${shortSummary}
9756
- Topics: ${s.topics}`;
9757
- });
9758
- await channel.sendText(chatId, lines.join("\n\n"), "plain");
9759
10177
  break;
9760
10178
  }
9761
10179
  case "skills": {
@@ -9876,8 +10294,8 @@ Use /skills to see it.`, "plain");
9876
10294
  if (!lim.max_input_tokens) continue;
9877
10295
  const u = getBackendUsageInWindow(lim.backend, lim.window);
9878
10296
  const pct = (u.input_tokens / lim.max_input_tokens * 100).toFixed(0);
9879
- const warn2 = u.input_tokens / lim.max_input_tokens >= lim.warn_pct;
9880
- lines.push(` ${lim.backend} (${lim.window}): ${pct}% of ${(lim.max_input_tokens / 1e3).toFixed(0)}K${warn2 ? " \u26A0\uFE0F" : ""}`);
10297
+ const warn3 = u.input_tokens / lim.max_input_tokens >= lim.warn_pct;
10298
+ lines.push(` ${lim.backend} (${lim.window}): ${pct}% of ${(lim.max_input_tokens / 1e3).toFixed(0)}K${warn3 ? " \u26A0\uFE0F" : ""}`);
9881
10299
  }
9882
10300
  }
9883
10301
  await channel.sendText(chatId, lines.join("\n"), "plain");
@@ -10376,8 +10794,17 @@ async function handleText(msg, channel) {
10376
10794
  await channel.sendText(chatId, limitMsg, "plain");
10377
10795
  return;
10378
10796
  }
10379
- if (isChatBusy(chatId)) {
10380
- await channel.sendText(chatId, "I'm currently processing another request. Your message is queued and will be handled next.", "plain");
10797
+ if (isChatBusy(chatId) && !bypassBusyCheck.delete(chatId)) {
10798
+ if (typeof channel.sendKeyboard === "function") {
10799
+ pendingInterrupts.set(chatId, { msg, channel });
10800
+ await channel.sendKeyboard(chatId, "\u23F3 Agent is working on a request\u2026", [
10801
+ [
10802
+ { label: "\u{1F4E5} Queue message", data: `interrupt:queue:${chatId}` },
10803
+ { label: "\u26A1 Send now", data: `interrupt:now:${chatId}` }
10804
+ ]
10805
+ ]);
10806
+ return;
10807
+ }
10381
10808
  }
10382
10809
  let typingActive = true;
10383
10810
  const typingLoop = async () => {
@@ -10395,12 +10822,57 @@ async function handleText(msg, channel) {
10395
10822
  const tMode = getMode(chatId);
10396
10823
  const tVerbose = getVerboseLevel(chatId);
10397
10824
  const tToolCb = tVerbose !== "off" ? makeToolActionCallback(chatId, channel, tVerbose) : void 0;
10398
- const response = await askAgent(chatId, text, { cwd: getCwd(chatId), model: model2, permMode: tMode, onToolAction: tToolCb });
10825
+ const response = await askAgent(chatId, text, {
10826
+ cwd: getCwd(chatId),
10827
+ model: model2,
10828
+ permMode: tMode,
10829
+ onToolAction: tToolCb,
10830
+ onCompaction: (cid) => {
10831
+ channel.sendText(cid, "\u{1F4BE} Context saved to memory.").catch(() => {
10832
+ });
10833
+ }
10834
+ });
10399
10835
  if (response.usage) addUsage(chatId, response.usage.input, response.usage.output, response.usage.cacheRead, model2);
10836
+ if (response.text.includes("[NEED_PERMISSION]") && tMode !== "yolo") {
10837
+ const cleanText = response.text.replace(/\[NEED_PERMISSION\]/g, "").trim();
10838
+ await sendResponse(chatId, channel, cleanText, msg.messageId);
10839
+ const targetMode = determineEscalationTarget(chatId, tMode);
10840
+ storePendingEscalation(chatId, text);
10841
+ if (typeof channel.sendKeyboard === "function") {
10842
+ await channel.sendKeyboard(chatId, `Switch to ${targetMode} mode?`, [
10843
+ [{ label: "Allow", data: `perm:escalate:${targetMode}` }, { label: "Deny", data: "perm:deny" }]
10844
+ ]);
10845
+ }
10846
+ return;
10847
+ }
10400
10848
  await sendResponse(chatId, channel, response.text, msg.messageId);
10401
10849
  } catch (err) {
10402
10850
  error("[router] Error:", err);
10403
10851
  const errMsg = errorMessage(err);
10852
+ const errorClass = classifyError(err);
10853
+ if (errorClass === "exhausted" && typeof channel.sendKeyboard === "function") {
10854
+ const currentBackend = getBackend(chatId) ?? "claude";
10855
+ const { getAllAdapters: getAllAdapters2 } = await Promise.resolve().then(() => (init_backends(), backends_exports));
10856
+ const otherBackends = getAllAdapters2().filter((a) => a.id !== currentBackend);
10857
+ if (otherBackends.length > 0) {
10858
+ pendingFallbackMessages.set(chatId, { msg, channel });
10859
+ const buttons = otherBackends.map((a) => ({
10860
+ label: `\u{1F504} ${a.displayName}`,
10861
+ data: `fallback:${a.id}:${chatId}`
10862
+ }));
10863
+ buttons.push({ label: "\u274C Wait", data: `fallback:wait:${chatId}` });
10864
+ await channel.sendKeyboard(
10865
+ chatId,
10866
+ `\u26A0\uFE0F ${getAdapter(currentBackend).displayName} is out of usage.
10867
+
10868
+ ${errMsg}
10869
+
10870
+ Switch to another backend?`,
10871
+ [buttons]
10872
+ );
10873
+ return;
10874
+ }
10875
+ }
10404
10876
  const userMsg = diagnoseAgentError(errMsg, chatId);
10405
10877
  await channel.sendText(chatId, userMsg, "plain");
10406
10878
  } finally {
@@ -10505,16 +10977,48 @@ async function processFileSends2(chatId, channel, text) {
10505
10977
  }
10506
10978
  return text.replace(fileSendPattern, "").trim();
10507
10979
  }
10980
+ async function processImageGenerations(chatId, channel, text) {
10981
+ const pattern = /\[GENERATE_IMAGE:(.+?)\]/g;
10982
+ const prompts = [];
10983
+ for (const match of text.matchAll(pattern)) {
10984
+ prompts.push(match[1].trim());
10985
+ }
10986
+ if (prompts.length === 0) return text;
10987
+ if (!isImageGenAvailable()) {
10988
+ log("[router] [GENERATE_IMAGE] marker found but GEMINI_API_KEY not set");
10989
+ return text.replace(pattern, "(Image generation unavailable \u2014 GEMINI_API_KEY not configured)").trim();
10990
+ }
10991
+ for (const prompt of prompts) {
10992
+ try {
10993
+ const result = await generateImage(prompt);
10994
+ const file = await readFile5(result.filePath);
10995
+ const name = result.filePath.split("/").pop() ?? "image.png";
10996
+ await channel.sendFile(chatId, file, name);
10997
+ if (result.text) {
10998
+ await channel.sendText(chatId, result.text, "plain");
10999
+ }
11000
+ } catch (err) {
11001
+ error(`[router] Image generation failed for "${prompt.slice(0, 50)}":`, err);
11002
+ await channel.sendText(chatId, `Image generation failed: ${errorMessage(err)}`, "plain");
11003
+ }
11004
+ }
11005
+ return text.replace(pattern, "").trim();
11006
+ }
10508
11007
  async function processReaction(chatId, channel, text, messageId) {
10509
- const reactPattern = /\[REACT:(.+?)\]/;
10510
- const match = text.match(reactPattern);
10511
- if (!match) return text;
10512
- const emoji = match[1].trim();
10513
- if (messageId && typeof channel.reactToMessage === "function" && ALLOWED_REACTION_EMOJIS.has(emoji)) {
10514
- channel.reactToMessage(chatId, messageId, emoji).catch(() => {
10515
- });
11008
+ const reactPatternGlobal = /\[REACT:(.+?)\]/g;
11009
+ if (!reactPatternGlobal.test(text)) return text;
11010
+ let reacted = false;
11011
+ reactPatternGlobal.lastIndex = 0;
11012
+ let match;
11013
+ while ((match = reactPatternGlobal.exec(text)) !== null) {
11014
+ const emoji = match[1].trim();
11015
+ if (!reacted && messageId && typeof channel.reactToMessage === "function" && ALLOWED_REACTION_EMOJIS.has(emoji)) {
11016
+ channel.reactToMessage(chatId, messageId, emoji).catch(() => {
11017
+ });
11018
+ reacted = true;
11019
+ }
10516
11020
  }
10517
- return text.replace(reactPattern, "").trim();
11021
+ return text.replace(/\[REACT:(.+?)\]/g, "").trim();
10518
11022
  }
10519
11023
  async function sendResponse(chatId, channel, text, messageId) {
10520
11024
  text = await processReaction(chatId, channel, text, messageId);
@@ -10526,7 +11030,29 @@ async function sendResponse(chatId, channel, text, messageId) {
10526
11030
  ]);
10527
11031
  }
10528
11032
  }
10529
- const cleanText = await processFileSends2(chatId, channel, afterUpdates);
11033
+ let afterHistory = afterUpdates;
11034
+ const historySearchMatch = afterHistory.match(/\[HISTORY_SEARCH:([^\]]+)\]/);
11035
+ if (historySearchMatch) {
11036
+ afterHistory = afterHistory.replace(/\[HISTORY_SEARCH:[^\]]+\]/g, "").trim();
11037
+ const hsQuery = historySearchMatch[1].trim();
11038
+ const hsResults = searchMessageLog(chatId, hsQuery, 10);
11039
+ if (hsResults.length > 0) {
11040
+ const hsLines = [`\u{1F4CB} History: "${hsQuery}"`, "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"];
11041
+ for (const row of hsResults.slice(0, 8)) {
11042
+ const label2 = row.role === "user" ? "You" : "Assistant";
11043
+ const date = row.created_at.slice(0, 10);
11044
+ hsLines.push(`[${row.backend ?? "?"} \xB7 ${date}] ${label2}: ${row.content.slice(0, 250)}${row.content.length > 250 ? "\u2026" : ""}`);
11045
+ }
11046
+ hsLines.push("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
11047
+ channel.sendText(chatId, hsLines.join("\n")).catch(() => {
11048
+ });
11049
+ } else {
11050
+ channel.sendText(chatId, `No history found for "${hsQuery}". Try /memories ${hsQuery} for older sessions.`).catch(() => {
11051
+ });
11052
+ }
11053
+ }
11054
+ const afterFiles = await processFileSends2(chatId, channel, afterHistory);
11055
+ const cleanText = await processImageGenerations(chatId, channel, afterFiles);
10530
11056
  if (!cleanText) return;
10531
11057
  if (isVoiceEnabled(chatId)) {
10532
11058
  try {
@@ -10551,16 +11077,17 @@ async function sendBackendSwitchConfirmation(chatId, target, channel) {
10551
11077
  await channel.sendText(chatId, `Already using ${targetAdapter.displayName}.`, "plain");
10552
11078
  return;
10553
11079
  }
10554
- const currentLabel = current ? getAdapter(current).displayName : "current backend";
10555
11080
  if (typeof channel.sendKeyboard === "function") {
10556
11081
  await channel.sendKeyboard(
10557
11082
  chatId,
10558
- `\u26A0\uFE0F Switching to ${targetAdapter.displayName} will summarize and reset your current session.
10559
-
10560
- What would you like to do?`,
11083
+ `\u{1F504} Switch to ${targetAdapter.displayName}?
11084
+ Your conversation history is preserved. ${targetAdapter.displayName} will receive a summary of recent context and can access your full history on request.`,
10561
11085
  [
10562
- [{ label: `Stay on ${currentLabel}`, data: "backend_cancel" }],
10563
- [{ label: `Switch to ${targetAdapter.displayName} + summarize`, data: `backend_confirm:${target}` }]
11086
+ [
11087
+ { label: `Switch to ${targetAdapter.displayName}`, data: `backend_confirm:${target}` },
11088
+ { label: `Switch + Clear History`, data: `backend_confirm_clear:${target}` }
11089
+ ],
11090
+ [{ label: "Cancel", data: `backend_cancel:${target}` }]
10564
11091
  ]
10565
11092
  );
10566
11093
  } else {
@@ -10568,21 +11095,21 @@ What would you like to do?`,
10568
11095
  }
10569
11096
  }
10570
11097
  async function doBackendSwitch(chatId, backendId, channel) {
10571
- summarizeSession(chatId).catch(() => {
10572
- });
11098
+ const targetAdapter = getAdapter(backendId);
11099
+ const summarized = await summarizeWithFallbackChain(chatId, backendId);
11100
+ if (summarized) {
11101
+ await channel.sendText(chatId, `\u{1F4BE} Context saved \u2014 ${targetAdapter.displayName} summarized your session.`, "plain");
11102
+ }
11103
+ const bridge = buildContextBridge(chatId);
11104
+ if (bridge) {
11105
+ setPendingContextBridge(chatId, bridge);
11106
+ }
10573
11107
  clearSession(chatId);
10574
11108
  clearModel(chatId);
10575
11109
  clearThinkingLevel(chatId);
10576
11110
  setBackend(chatId, backendId);
10577
- const adapter = getAdapter(backendId);
10578
- logActivity(getDb(), { chatId, source: "telegram", eventType: "config_changed", summary: `Backend switched to ${adapter.displayName}`, detail: { field: "backend", value: backendId } });
10579
- await channel.sendText(
10580
- chatId,
10581
- `Backend switched to ${adapter.displayName}.
10582
- Default model: ${adapter.defaultModel}
10583
- Session reset. Ready!`,
10584
- "plain"
10585
- );
11111
+ logActivity(getDb(), { chatId, source: "telegram", eventType: "config_changed", summary: `Backend switched to ${targetAdapter.displayName}`, detail: { field: "backend", value: backendId } });
11112
+ await channel.sendText(chatId, `\u2705 Switched to ${targetAdapter.displayName}. Ready!`, "plain");
10586
11113
  }
10587
11114
  async function handleCallback(chatId, data, channel) {
10588
11115
  if (data.startsWith("backend:")) {
@@ -10595,14 +11122,17 @@ async function handleCallback(chatId, data, channel) {
10595
11122
  return;
10596
11123
  }
10597
11124
  await sendBackendSwitchConfirmation(chatId, chosen, channel);
11125
+ } else if (data.startsWith("backend_confirm_clear:")) {
11126
+ const target = data.slice(21);
11127
+ if (!getAllBackendIds().includes(target)) return;
11128
+ clearMessageLog(chatId);
11129
+ await doBackendSwitch(chatId, target, channel);
10598
11130
  } else if (data.startsWith("backend_confirm:")) {
10599
11131
  const chosen = data.slice(16);
10600
11132
  if (!getAllBackendIds().includes(chosen)) return;
10601
11133
  await doBackendSwitch(chatId, chosen, channel);
10602
- } else if (data === "backend_cancel") {
10603
- const current = getBackend(chatId);
10604
- const label2 = current ? getAdapter(current).displayName : "current backend";
10605
- await channel.sendText(chatId, `No change. Staying on ${label2}.`, "plain");
11134
+ } else if (data.startsWith("backend_cancel:") || data === "backend_cancel") {
11135
+ await channel.sendText(chatId, "Switch cancelled.", "plain");
10606
11136
  } else if (data.startsWith("model:")) {
10607
11137
  const chosen = data.slice(6);
10608
11138
  let adapter;
@@ -10657,7 +11187,8 @@ Select thinking/effort level:`,
10657
11187
  await channel.sendText(chatId, `Summarizer pinned to ${bk}:${mdl ?? "default"}.`, "plain");
10658
11188
  }
10659
11189
  } else if (data.startsWith("perms:")) {
10660
- const chosen = data.slice(6);
11190
+ let chosen = data.slice(6);
11191
+ if (chosen === "readonly") chosen = "plan";
10661
11192
  if (!PERM_MODES[chosen]) return;
10662
11193
  const previous = getMode(chatId);
10663
11194
  if (chosen === previous) {
@@ -10672,6 +11203,20 @@ Select thinking/effort level:`,
10672
11203
  ${PERM_MODES[chosen]}`,
10673
11204
  "plain"
10674
11205
  );
11206
+ } else if (data.startsWith("perm:escalate:")) {
11207
+ const targetMode = data.slice(14);
11208
+ if (!PERM_MODES[targetMode]) return;
11209
+ setMode(chatId, targetMode);
11210
+ logActivity(getDb(), { chatId, source: "telegram", eventType: "config_changed", summary: `Permission escalated to ${targetMode}`, detail: { field: "permissions", value: targetMode } });
11211
+ await channel.sendText(chatId, `Switched to ${targetMode} mode. Re-processing your request...`, "plain");
11212
+ const pending = getPendingEscalation(chatId);
11213
+ if (pending) {
11214
+ removePendingEscalation(chatId);
11215
+ await handleMessage({ text: pending, chatId, source: "telegram" }, channel);
11216
+ }
11217
+ } else if (data === "perm:deny") {
11218
+ removePendingEscalation(chatId);
11219
+ await channel.sendText(chatId, "Keeping current mode.", "plain");
10675
11220
  } else if (data.startsWith("verbose:")) {
10676
11221
  const chosen = data.slice(8);
10677
11222
  if (!VERBOSE_LEVELS[chosen]) return;
@@ -10746,6 +11291,47 @@ ${PERM_MODES[chosen]}`,
10746
11291
  touchBookmark(chatId, alias);
10747
11292
  logActivity(getDb(), { chatId, source: "telegram", eventType: "config_changed", summary: `Working directory set to ${bookmark.path}`, detail: { field: "cwd", value: bookmark.path } });
10748
11293
  await sendCwdSessionChoice(chatId, bookmark.path, channel);
11294
+ } else if (data.startsWith("interrupt:")) {
11295
+ const parts = data.split(":");
11296
+ const action = parts[1];
11297
+ const targetChatId = parts.slice(2).join(":");
11298
+ const pending = pendingInterrupts.get(targetChatId);
11299
+ if (action === "now" && pending) {
11300
+ pendingInterrupts.delete(targetChatId);
11301
+ stopAgent(targetChatId);
11302
+ await channel.sendText(chatId, "\u26A1 Stopping current task and processing your message\u2026", "plain");
11303
+ await new Promise((r) => setTimeout(r, 500));
11304
+ await handleMessage(pending.msg, pending.channel);
11305
+ } else if (action === "queue" && pending) {
11306
+ pendingInterrupts.delete(targetChatId);
11307
+ bypassBusyCheck.add(targetChatId);
11308
+ await channel.sendText(chatId, "\u{1F4E5} Message queued \u2014 will process after current task.", "plain");
11309
+ handleMessage(pending.msg, pending.channel).catch(() => {
11310
+ });
11311
+ } else {
11312
+ await channel.sendText(chatId, "Message already processed or expired.", "plain");
11313
+ }
11314
+ } else if (data.startsWith("fallback:")) {
11315
+ const parts = data.split(":");
11316
+ const targetBackend = parts[1];
11317
+ const targetChatId = parts[2];
11318
+ const pendingMsg = pendingFallbackMessages.get(targetChatId);
11319
+ if (targetBackend === "wait") {
11320
+ pendingFallbackMessages.delete(targetChatId);
11321
+ await channel.sendText(chatId, "OK \u2014 you can switch manually with /backend when ready.", "plain");
11322
+ } else if (pendingMsg) {
11323
+ pendingFallbackMessages.delete(targetChatId);
11324
+ await summarizeWithFallbackChain(targetChatId, targetBackend);
11325
+ const bridge = buildContextBridge(targetChatId);
11326
+ if (bridge) setPendingContextBridge(targetChatId, bridge);
11327
+ clearSession(targetChatId);
11328
+ setBackend(targetChatId, targetBackend);
11329
+ const adapter = getAdapter(targetBackend);
11330
+ await channel.sendText(chatId, `Switched to ${adapter.displayName}. Resending your message\u2026`, "plain");
11331
+ await handleMessage(pendingMsg.msg, pendingMsg.channel);
11332
+ } else {
11333
+ await channel.sendText(chatId, "Fallback expired. Use /backend to switch manually.", "plain");
11334
+ }
10749
11335
  } else if (data.startsWith("skills:page:")) {
10750
11336
  const page = parseInt(data.slice(12), 10);
10751
11337
  const skills2 = await discoverAllSkills();
@@ -10957,7 +11543,7 @@ Use /skills <page> to navigate (e.g. /skills 2)` : "";
10957
11543
  const header2 = totalPages > 1 ? `${skills2.length} skills (page ${safePage}/${totalPages}). Select one to invoke:` : `${skills2.length} skills available. Select one to invoke:`;
10958
11544
  await channel.sendKeyboard(chatId, header2, buttons);
10959
11545
  }
10960
- var PERM_MODES, VERBOSE_LEVELS, CLI_INSTALL_HINTS, BLOCKED_PATH_PATTERNS2, ALLOWED_REACTION_EMOJIS, SKILLS_PER_PAGE;
11546
+ var PERM_MODES, VERBOSE_LEVELS, pendingInterrupts, bypassBusyCheck, pendingFallbackMessages, CLI_INSTALL_HINTS, BLOCKED_PATH_PATTERNS2, ALLOWED_REACTION_EMOJIS, SKILLS_PER_PAGE;
10961
11547
  var init_router = __esm({
10962
11548
  "src/router.ts"() {
10963
11549
  "use strict";
@@ -10968,9 +11554,13 @@ var init_router = __esm({
10968
11554
  init_log();
10969
11555
  init_format_time();
10970
11556
  init_agent();
11557
+ init_retry();
11558
+ init_image_gen();
10971
11559
  init_stt();
10972
11560
  init_store4();
10973
11561
  init_summarize();
11562
+ init_inject();
11563
+ init_store4();
10974
11564
  init_session_log();
10975
11565
  init_backends();
10976
11566
  init_cron();
@@ -10988,14 +11578,16 @@ var init_router = __esm({
10988
11578
  PERM_MODES = {
10989
11579
  yolo: "YOLO \u2014 all tools, full autopilot",
10990
11580
  safe: "Safe \u2014 only my allowed tools",
10991
- readonly: "Read-only \u2014 Read, Glob, Grep only",
10992
- plan: "Plan \u2014 plan only, no execution"
11581
+ plan: "Plan \u2014 read and analyze only"
10993
11582
  };
10994
11583
  VERBOSE_LEVELS = {
10995
11584
  off: "Off \u2014 no tool visibility",
10996
11585
  normal: "Normal \u2014 summarized actions",
10997
11586
  verbose: "Verbose \u2014 full details"
10998
11587
  };
11588
+ pendingInterrupts = /* @__PURE__ */ new Map();
11589
+ bypassBusyCheck = /* @__PURE__ */ new Set();
11590
+ pendingFallbackMessages = /* @__PURE__ */ new Map();
10999
11591
  CLI_INSTALL_HINTS = {
11000
11592
  claude: "Install: npm install -g @anthropic-ai/claude-code",
11001
11593
  gemini: "Install: npm install -g @anthropic-ai/gemini-cli",
@@ -11090,19 +11682,19 @@ var init_router = __esm({
11090
11682
  });
11091
11683
 
11092
11684
  // src/skills/bootstrap.ts
11093
- import { existsSync as existsSync13 } from "fs";
11685
+ import { existsSync as existsSync14 } from "fs";
11094
11686
  import { readdir as readdir3, readFile as readFile6, writeFile as writeFile3, copyFile } from "fs/promises";
11095
- import { join as join14, dirname as dirname3 } from "path";
11687
+ import { join as join15, dirname as dirname3 } from "path";
11096
11688
  import { fileURLToPath as fileURLToPath3 } from "url";
11097
11689
  async function copyAgentManifestSkills() {
11098
- if (!existsSync13(PKG_SKILLS)) return;
11690
+ if (!existsSync14(PKG_SKILLS)) return;
11099
11691
  try {
11100
11692
  const entries = await readdir3(PKG_SKILLS, { withFileTypes: true });
11101
11693
  for (const entry of entries) {
11102
11694
  if (!entry.isFile() || !entry.name.startsWith("agent-") || !entry.name.endsWith(".md")) continue;
11103
- const src = join14(PKG_SKILLS, entry.name);
11104
- const dest = join14(SKILLS_PATH, entry.name);
11105
- if (existsSync13(dest)) continue;
11695
+ const src = join15(PKG_SKILLS, entry.name);
11696
+ const dest = join15(SKILLS_PATH, entry.name);
11697
+ if (existsSync14(dest)) continue;
11106
11698
  await copyFile(src, dest);
11107
11699
  log(`[skills] Bootstrapped ${entry.name} to ${SKILLS_PATH}`);
11108
11700
  }
@@ -11112,8 +11704,8 @@ async function copyAgentManifestSkills() {
11112
11704
  }
11113
11705
  async function bootstrapSkills() {
11114
11706
  await copyAgentManifestSkills();
11115
- const usmDir = join14(SKILLS_PATH, USM_DIR_NAME);
11116
- if (existsSync13(usmDir)) return;
11707
+ const usmDir = join15(SKILLS_PATH, USM_DIR_NAME);
11708
+ if (existsSync14(usmDir)) return;
11117
11709
  try {
11118
11710
  const entries = await readdir3(SKILLS_PATH);
11119
11711
  const dirs = entries.filter((e) => !e.startsWith("."));
@@ -11135,8 +11727,8 @@ async function bootstrapSkills() {
11135
11727
  }
11136
11728
  }
11137
11729
  async function patchUsmForCcClaw(usmDir) {
11138
- const skillPath = join14(usmDir, "SKILL.md");
11139
- if (!existsSync13(skillPath)) return;
11730
+ const skillPath = join15(usmDir, "SKILL.md");
11731
+ if (!existsSync14(skillPath)) return;
11140
11732
  try {
11141
11733
  let content = await readFile6(skillPath, "utf-8");
11142
11734
  let patched = false;
@@ -11181,8 +11773,8 @@ var init_bootstrap = __esm({
11181
11773
  USM_REPO = "jacob-bd/universal-skills-manager";
11182
11774
  USM_DIR_NAME = "universal-skills-manager";
11183
11775
  CC_CLAW_ECOSYSTEM_PATCH = `| **CC-Claw** | \`~/.cc-claw/workspace/skills/\` | N/A (daemon, no project scope) |`;
11184
- PKG_ROOT = join14(dirname3(fileURLToPath3(import.meta.url)), "..", "..");
11185
- PKG_SKILLS = join14(PKG_ROOT, "skills");
11776
+ PKG_ROOT = join15(dirname3(fileURLToPath3(import.meta.url)), "..", "..");
11777
+ PKG_SKILLS = join15(PKG_ROOT, "skills");
11186
11778
  }
11187
11779
  });
11188
11780
 
@@ -11396,13 +11988,13 @@ __export(ai_skill_exports, {
11396
11988
  generateAiSkill: () => generateAiSkill,
11397
11989
  installAiSkill: () => installAiSkill
11398
11990
  });
11399
- import { existsSync as existsSync14, writeFileSync as writeFileSync5, mkdirSync as mkdirSync5 } from "fs";
11400
- import { join as join15 } from "path";
11991
+ import { existsSync as existsSync15, writeFileSync as writeFileSync6, mkdirSync as mkdirSync6 } from "fs";
11992
+ import { join as join16 } from "path";
11401
11993
  import { homedir as homedir4 } from "os";
11402
11994
  function generateAiSkill() {
11403
11995
  const version = VERSION;
11404
11996
  let systemState = "";
11405
- if (existsSync14(DB_PATH)) {
11997
+ if (existsSync15(DB_PATH)) {
11406
11998
  try {
11407
11999
  const { openDatabaseReadOnly: openDatabaseReadOnly2 } = (init_store4(), __toCommonJS(store_exports3));
11408
12000
  const readDb = openDatabaseReadOnly2();
@@ -11467,11 +12059,10 @@ cc-claw logs --error # Show error log
11467
12059
  \`\`\`bash
11468
12060
  cc-claw backend list --json # Available backends
11469
12061
  cc-claw backend get --json # Active backend
11470
- cc-claw backend set claude # Switch backend
12062
+ cc-claw backend set claude # Switch backend (claude/gemini/codex)
11471
12063
  cc-claw model list --json # Models for active backend
12064
+ cc-claw model get --json # Active model
11472
12065
  cc-claw model set claude-opus-4-6 # Switch model
11473
- cc-claw thinking get --json # Current thinking level
11474
- cc-claw thinking set high # Set thinking level
11475
12066
  \`\`\`
11476
12067
 
11477
12068
  ### Chat
@@ -11491,12 +12082,23 @@ cc-claw memory add "key" "value" # Save a memory (needs daemon)
11491
12082
  cc-claw memory forget "keyword" # Delete memories (needs daemon)
11492
12083
  \`\`\`
11493
12084
 
12085
+ ### Session
12086
+ \`\`\`bash
12087
+ cc-claw session get --json # Current session ID and age
12088
+ cc-claw session new # Clear session (newchat + summarize)
12089
+ \`\`\`
12090
+
11494
12091
  ### Scheduler (cron/schedule are aliases)
11495
12092
  \`\`\`bash
11496
12093
  cc-claw cron list --json # All scheduled jobs
11497
12094
  cc-claw cron health --json # Scheduler health
11498
12095
  cc-claw cron runs --json # Run history
11499
12096
  cc-claw cron create --description "Morning briefing" --cron "0 9 * * *" --backend claude
12097
+ cc-claw cron edit 3 --model gemini-3-flash-preview # Change job model
12098
+ cc-claw cron edit 3 --backend gemini --model gemini-3-flash-preview # Change backend + model
12099
+ cc-claw cron edit 3 --description "New task" --timeout 300 # Edit multiple fields
12100
+ cc-claw cron create --description "Report" --cron "0 9 * * *" --backend claude --model claude-sonnet-4-6 --fallback codex:gpt-5.4 --fallback gemini:gemini-3-flash-preview # With fallback chain
12101
+ cc-claw cron edit 3 --fallback codex:gpt-5.4 --fallback gemini:gemini-3-flash-preview # Set fallbacks
11500
12102
  cc-claw cron cancel 3 # Cancel job #3
11501
12103
  cc-claw cron pause 3 # Pause job
11502
12104
  cc-claw cron resume 3 # Resume job
@@ -11527,7 +12129,14 @@ cc-claw usage cost --json # Session cost estimate
11527
12129
  cc-claw usage cost --all --json # All-time cost by model
11528
12130
  cc-claw usage tokens --json # Backend usage in last 24h
11529
12131
  cc-claw usage limits list --json # Current limits
11530
- cc-claw usage limits set claude daily 500000
12132
+ cc-claw usage limits set claude daily 500000 # Set a limit
12133
+ cc-claw usage limits clear claude daily # Remove a limit
12134
+ \`\`\`
12135
+
12136
+ ### Thinking / Reasoning
12137
+ \`\`\`bash
12138
+ cc-claw thinking get --json # Current thinking level
12139
+ cc-claw thinking set high # Set level (auto/off/low/medium/high/extra_high)
11531
12140
  \`\`\`
11532
12141
 
11533
12142
  ### Permissions & Tools
@@ -11537,10 +12146,71 @@ cc-claw permissions set yolo # Set mode (yolo/safe/readonly/plan)
11537
12146
  cc-claw tools list --json # Tools with enabled/disabled status
11538
12147
  cc-claw tools enable Bash # Enable a tool
11539
12148
  cc-claw tools disable WebFetch # Disable a tool
12149
+ cc-claw tools reset # Reset to defaults (all on)
11540
12150
  cc-claw verbose get --json # Tool visibility level
11541
12151
  cc-claw verbose set verbose # Set level (off/normal/verbose)
11542
12152
  \`\`\`
11543
12153
 
12154
+ ### Working Directory
12155
+ \`\`\`bash
12156
+ cc-claw cwd get --json # Show current working directory
12157
+ cc-claw cwd set /path/to/dir # Set working directory
12158
+ cc-claw cwd clear # Reset to default
12159
+ \`\`\`
12160
+
12161
+ ### Voice
12162
+ \`\`\`bash
12163
+ cc-claw voice get --json # Show voice status
12164
+ cc-claw voice set on # Enable voice responses
12165
+ cc-claw voice set off # Disable voice responses
12166
+ \`\`\`
12167
+
12168
+ ### Image Generation
12169
+ \`\`\`bash
12170
+ # Via Telegram command:
12171
+ /imagine a futuristic city at sunset # Generate image from prompt
12172
+ /image a cat astronaut on Mars # Alias for /imagine
12173
+
12174
+ # Via AI marker in responses:
12175
+ # The AI can write [GENERATE_IMAGE:prompt] in its response to trigger generation
12176
+ # Requires GEMINI_API_KEY in ~/.cc-claw/.env
12177
+ \`\`\`
12178
+
12179
+ ### Heartbeat
12180
+ \`\`\`bash
12181
+ cc-claw heartbeat get --json # Show heartbeat config
12182
+ cc-claw heartbeat set on # Enable proactive awareness
12183
+ cc-claw heartbeat set off # Disable heartbeat
12184
+ cc-claw heartbeat set interval 30m # Set check interval
12185
+ cc-claw heartbeat set hours 9-22 # Set active hours
12186
+ \`\`\`
12187
+
12188
+ ### Summarizer
12189
+ \`\`\`bash
12190
+ cc-claw summarizer get --json # Current summarizer config
12191
+ cc-claw summarizer set auto # Auto (use active backend's summarizer)
12192
+ cc-claw summarizer set off # Disable summarization
12193
+ cc-claw summarizer set claude:claude-haiku-4-5 # Pin specific backend:model
12194
+ \`\`\`
12195
+
12196
+ ### Chat Aliases
12197
+ \`\`\`bash
12198
+ cc-claw chats list --json # Authorized chats and aliases
12199
+ cc-claw chats alias 123456789 "work" # Set alias for a chat
12200
+ cc-claw chats remove-alias "work" # Remove alias
12201
+ \`\`\`
12202
+
12203
+ ### Skills
12204
+ \`\`\`bash
12205
+ cc-claw skills list --json # All skills from all backends
12206
+ cc-claw skills install <github-url> # Install skill from GitHub
12207
+ \`\`\`
12208
+
12209
+ ### MCP Servers
12210
+ \`\`\`bash
12211
+ cc-claw mcps list --json # Registered MCP servers
12212
+ \`\`\`
12213
+
11544
12214
  ### Database
11545
12215
  \`\`\`bash
11546
12216
  cc-claw db stats --json # Row counts, file size, WAL status
@@ -11561,15 +12231,10 @@ cc-claw service uninstall # Remove service
11561
12231
 
11562
12232
  ### Other
11563
12233
  \`\`\`bash
11564
- cc-claw skills list --json # Available skills
11565
- cc-claw mcps list --json # MCP servers
11566
- cc-claw chats list --json # Chat aliases
11567
- cc-claw session get --json # Current session
11568
- cc-claw session new # Clear session
11569
- cc-claw cwd get --json # Working directory
11570
- cc-claw voice get --json # Voice status
11571
- cc-claw heartbeat get --json # Heartbeat config
11572
- cc-claw summarizer get --json # Summarizer config
12234
+ cc-claw setup # Interactive configuration wizard
12235
+ cc-claw tui # Interactive terminal chat
12236
+ cc-claw completion --shell zsh # Generate shell completions (bash/zsh/fish)
12237
+ cc-claw --ai # Generate/install SKILL.md for AI tools
11573
12238
  \`\`\`
11574
12239
 
11575
12240
  ## JSON Output Format
@@ -11625,11 +12290,11 @@ function installAiSkill() {
11625
12290
  const failed = [];
11626
12291
  for (const [backend2, dirs] of Object.entries(BACKEND_SKILL_DIRS2)) {
11627
12292
  for (const dir of dirs) {
11628
- const skillDir = join15(dir, "cc-claw-cli");
11629
- const skillPath = join15(skillDir, "SKILL.md");
12293
+ const skillDir = join16(dir, "cc-claw-cli");
12294
+ const skillPath = join16(skillDir, "SKILL.md");
11630
12295
  try {
11631
- mkdirSync5(skillDir, { recursive: true });
11632
- writeFileSync5(skillPath, skill, "utf-8");
12296
+ mkdirSync6(skillDir, { recursive: true });
12297
+ writeFileSync6(skillPath, skill, "utf-8");
11633
12298
  installed.push(skillPath);
11634
12299
  } catch {
11635
12300
  failed.push(skillPath);
@@ -11645,10 +12310,10 @@ var init_ai_skill = __esm({
11645
12310
  init_paths();
11646
12311
  init_version();
11647
12312
  BACKEND_SKILL_DIRS2 = {
11648
- "cc-claw": [join15(homedir4(), ".cc-claw", "workspace", "skills")],
11649
- claude: [join15(homedir4(), ".claude", "skills")],
11650
- gemini: [join15(homedir4(), ".gemini", "skills")],
11651
- codex: [join15(homedir4(), ".agents", "skills")]
12313
+ "cc-claw": [join16(homedir4(), ".cc-claw", "workspace", "skills")],
12314
+ claude: [join16(homedir4(), ".claude", "skills")],
12315
+ gemini: [join16(homedir4(), ".gemini", "skills")],
12316
+ codex: [join16(homedir4(), ".agents", "skills")]
11652
12317
  };
11653
12318
  }
11654
12319
  });
@@ -11658,21 +12323,21 @@ var index_exports = {};
11658
12323
  __export(index_exports, {
11659
12324
  main: () => main
11660
12325
  });
11661
- import { mkdirSync as mkdirSync6, existsSync as existsSync15, renameSync, statSync as statSync3 } from "fs";
11662
- import { join as join16 } from "path";
12326
+ import { mkdirSync as mkdirSync7, existsSync as existsSync16, renameSync, statSync as statSync2, readFileSync as readFileSync10 } from "fs";
12327
+ import { join as join17 } from "path";
11663
12328
  import dotenv from "dotenv";
11664
12329
  function migrateLayout() {
11665
12330
  const moves = [
11666
- [join16(CC_CLAW_HOME, "cc-claw.db"), join16(DATA_PATH, "cc-claw.db")],
11667
- [join16(CC_CLAW_HOME, "cc-claw.db-shm"), join16(DATA_PATH, "cc-claw.db-shm")],
11668
- [join16(CC_CLAW_HOME, "cc-claw.db-wal"), join16(DATA_PATH, "cc-claw.db-wal")],
11669
- [join16(CC_CLAW_HOME, "cc-claw.log"), join16(LOGS_PATH, "cc-claw.log")],
11670
- [join16(CC_CLAW_HOME, "cc-claw.log.1"), join16(LOGS_PATH, "cc-claw.log.1")],
11671
- [join16(CC_CLAW_HOME, "cc-claw.error.log"), join16(LOGS_PATH, "cc-claw.error.log")],
11672
- [join16(CC_CLAW_HOME, "cc-claw.error.log.1"), join16(LOGS_PATH, "cc-claw.error.log.1")]
12331
+ [join17(CC_CLAW_HOME, "cc-claw.db"), join17(DATA_PATH, "cc-claw.db")],
12332
+ [join17(CC_CLAW_HOME, "cc-claw.db-shm"), join17(DATA_PATH, "cc-claw.db-shm")],
12333
+ [join17(CC_CLAW_HOME, "cc-claw.db-wal"), join17(DATA_PATH, "cc-claw.db-wal")],
12334
+ [join17(CC_CLAW_HOME, "cc-claw.log"), join17(LOGS_PATH, "cc-claw.log")],
12335
+ [join17(CC_CLAW_HOME, "cc-claw.log.1"), join17(LOGS_PATH, "cc-claw.log.1")],
12336
+ [join17(CC_CLAW_HOME, "cc-claw.error.log"), join17(LOGS_PATH, "cc-claw.error.log")],
12337
+ [join17(CC_CLAW_HOME, "cc-claw.error.log.1"), join17(LOGS_PATH, "cc-claw.error.log.1")]
11673
12338
  ];
11674
12339
  for (const [from, to] of moves) {
11675
- if (existsSync15(from) && !existsSync15(to)) {
12340
+ if (existsSync16(from) && !existsSync16(to)) {
11676
12341
  try {
11677
12342
  renameSync(from, to);
11678
12343
  } catch {
@@ -11683,7 +12348,7 @@ function migrateLayout() {
11683
12348
  function rotateLogs() {
11684
12349
  for (const file of [LOG_PATH, ERROR_LOG_PATH]) {
11685
12350
  try {
11686
- const { size } = statSync3(file);
12351
+ const { size } = statSync2(file);
11687
12352
  if (size > LOG_MAX_BYTES) {
11688
12353
  const archivePath = `${file}.1`;
11689
12354
  try {
@@ -11698,14 +12363,21 @@ function rotateLogs() {
11698
12363
  }
11699
12364
  async function main() {
11700
12365
  rotateLogs();
11701
- log("[cc-claw] Starting up...");
12366
+ let version = "unknown";
12367
+ try {
12368
+ const pkgPath = new URL("../package.json", import.meta.url);
12369
+ version = JSON.parse(readFileSync10(pkgPath, "utf-8")).version;
12370
+ } catch {
12371
+ }
12372
+ log(`[cc-claw] Starting v${version}`);
11702
12373
  initDatabase();
12374
+ pruneMessageLog(30, 2e3);
11703
12375
  bootstrapBuiltinMcps(getDb());
11704
- const SUMMARIZE_TIMEOUT_MS = 3e4;
12376
+ const SUMMARIZE_TIMEOUT_MS2 = 3e4;
11705
12377
  try {
11706
12378
  let timer;
11707
12379
  const timeoutPromise = new Promise((_, reject) => {
11708
- timer = setTimeout(() => reject(new Error("timeout")), SUMMARIZE_TIMEOUT_MS);
12380
+ timer = setTimeout(() => reject(new Error("timeout")), SUMMARIZE_TIMEOUT_MS2);
11709
12381
  });
11710
12382
  try {
11711
12383
  await Promise.race([
@@ -11767,11 +12439,11 @@ async function main() {
11767
12439
  bootstrapSkills().catch((err) => error("[cc-claw] Skill bootstrap failed:", err));
11768
12440
  try {
11769
12441
  const { generateAiSkill: generateAiSkill2 } = await Promise.resolve().then(() => (init_ai_skill(), ai_skill_exports));
11770
- const { writeFileSync: writeFileSync8, mkdirSync: mkdirSync10 } = await import("fs");
11771
- const { join: join19 } = await import("path");
11772
- const skillDir = join19(SKILLS_PATH, "cc-claw-cli");
11773
- mkdirSync10(skillDir, { recursive: true });
11774
- writeFileSync8(join19(skillDir, "SKILL.md"), generateAiSkill2(), "utf-8");
12442
+ const { writeFileSync: writeFileSync9, mkdirSync: mkdirSync11 } = await import("fs");
12443
+ const { join: join20 } = await import("path");
12444
+ const skillDir = join20(SKILLS_PATH, "cc-claw-cli");
12445
+ mkdirSync11(skillDir, { recursive: true });
12446
+ writeFileSync9(join20(skillDir, "SKILL.md"), generateAiSkill2(), "utf-8");
11775
12447
  log("[cc-claw] AI skill updated");
11776
12448
  } catch {
11777
12449
  }
@@ -11831,10 +12503,10 @@ var init_index = __esm({
11831
12503
  init_bootstrap2();
11832
12504
  init_health3();
11833
12505
  for (const dir of [CC_CLAW_HOME, DATA_PATH, LOGS_PATH, SKILLS_PATH, RUNNERS_PATH, AGENTS_PATH]) {
11834
- if (!existsSync15(dir)) mkdirSync6(dir, { recursive: true });
12506
+ if (!existsSync16(dir)) mkdirSync7(dir, { recursive: true });
11835
12507
  }
11836
12508
  migrateLayout();
11837
- if (existsSync15(ENV_PATH)) {
12509
+ if (existsSync16(ENV_PATH)) {
11838
12510
  dotenv.config({ path: ENV_PATH });
11839
12511
  } else {
11840
12512
  console.error(`[cc-claw] Config not found at ${ENV_PATH} \u2014 run 'cc-claw setup' first`);
@@ -11855,10 +12527,10 @@ __export(service_exports, {
11855
12527
  serviceStatus: () => serviceStatus,
11856
12528
  uninstallService: () => uninstallService
11857
12529
  });
11858
- import { existsSync as existsSync16, mkdirSync as mkdirSync7, writeFileSync as writeFileSync6, unlinkSync as unlinkSync2 } from "fs";
12530
+ import { existsSync as existsSync17, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7, unlinkSync as unlinkSync2 } from "fs";
11859
12531
  import { execFileSync as execFileSync2, execSync as execSync5 } from "child_process";
11860
12532
  import { homedir as homedir5, platform } from "os";
11861
- import { join as join17, dirname as dirname4 } from "path";
12533
+ import { join as join18, dirname as dirname4 } from "path";
11862
12534
  function resolveExecutable2(name) {
11863
12535
  try {
11864
12536
  return execFileSync2("which", [name], { encoding: "utf-8" }).trim();
@@ -11871,14 +12543,14 @@ function getPathDirs() {
11871
12543
  const home = homedir5();
11872
12544
  const dirs = /* @__PURE__ */ new Set([
11873
12545
  nodeBin,
11874
- join17(home, ".local", "bin"),
12546
+ join18(home, ".local", "bin"),
11875
12547
  "/usr/local/bin",
11876
12548
  "/usr/bin",
11877
12549
  "/bin"
11878
12550
  ]);
11879
12551
  try {
11880
12552
  const prefix = execSync5("npm config get prefix", { encoding: "utf-8" }).trim();
11881
- if (prefix) dirs.add(join17(prefix, "bin"));
12553
+ if (prefix) dirs.add(join18(prefix, "bin"));
11882
12554
  } catch {
11883
12555
  }
11884
12556
  return [...dirs].join(":");
@@ -11933,21 +12605,21 @@ function generatePlist() {
11933
12605
  }
11934
12606
  function installMacOS() {
11935
12607
  const agentsDir = dirname4(PLIST_PATH);
11936
- if (!existsSync16(agentsDir)) mkdirSync7(agentsDir, { recursive: true });
11937
- if (!existsSync16(LOGS_PATH)) mkdirSync7(LOGS_PATH, { recursive: true });
11938
- if (existsSync16(PLIST_PATH)) {
12608
+ if (!existsSync17(agentsDir)) mkdirSync8(agentsDir, { recursive: true });
12609
+ if (!existsSync17(LOGS_PATH)) mkdirSync8(LOGS_PATH, { recursive: true });
12610
+ if (existsSync17(PLIST_PATH)) {
11939
12611
  try {
11940
12612
  execFileSync2("launchctl", ["unload", PLIST_PATH]);
11941
12613
  } catch {
11942
12614
  }
11943
12615
  }
11944
- writeFileSync6(PLIST_PATH, generatePlist());
12616
+ writeFileSync7(PLIST_PATH, generatePlist());
11945
12617
  console.log(` Installed: ${PLIST_PATH}`);
11946
12618
  execFileSync2("launchctl", ["load", PLIST_PATH]);
11947
12619
  console.log(" Service loaded and starting.");
11948
12620
  }
11949
12621
  function uninstallMacOS() {
11950
- if (!existsSync16(PLIST_PATH)) {
12622
+ if (!existsSync17(PLIST_PATH)) {
11951
12623
  console.log(" No service found to uninstall.");
11952
12624
  return;
11953
12625
  }
@@ -11998,9 +12670,9 @@ WantedBy=default.target
11998
12670
  `;
11999
12671
  }
12000
12672
  function installLinux() {
12001
- if (!existsSync16(SYSTEMD_DIR)) mkdirSync7(SYSTEMD_DIR, { recursive: true });
12002
- if (!existsSync16(LOGS_PATH)) mkdirSync7(LOGS_PATH, { recursive: true });
12003
- writeFileSync6(UNIT_PATH, generateUnit());
12673
+ if (!existsSync17(SYSTEMD_DIR)) mkdirSync8(SYSTEMD_DIR, { recursive: true });
12674
+ if (!existsSync17(LOGS_PATH)) mkdirSync8(LOGS_PATH, { recursive: true });
12675
+ writeFileSync7(UNIT_PATH, generateUnit());
12004
12676
  console.log(` Installed: ${UNIT_PATH}`);
12005
12677
  execFileSync2("systemctl", ["--user", "daemon-reload"]);
12006
12678
  execFileSync2("systemctl", ["--user", "enable", "cc-claw"]);
@@ -12008,7 +12680,7 @@ function installLinux() {
12008
12680
  console.log(" Service enabled and started.");
12009
12681
  }
12010
12682
  function uninstallLinux() {
12011
- if (!existsSync16(UNIT_PATH)) {
12683
+ if (!existsSync17(UNIT_PATH)) {
12012
12684
  console.log(" No service found to uninstall.");
12013
12685
  return;
12014
12686
  }
@@ -12033,7 +12705,7 @@ function statusLinux() {
12033
12705
  }
12034
12706
  }
12035
12707
  function installService() {
12036
- if (!existsSync16(join17(CC_CLAW_HOME, ".env"))) {
12708
+ if (!existsSync17(join18(CC_CLAW_HOME, ".env"))) {
12037
12709
  console.error(` Config not found at ${CC_CLAW_HOME}/.env`);
12038
12710
  console.error(" Run 'cc-claw setup' before installing the service.");
12039
12711
  process.exitCode = 1;
@@ -12062,9 +12734,9 @@ var init_service = __esm({
12062
12734
  "use strict";
12063
12735
  init_paths();
12064
12736
  PLIST_LABEL = "com.cc-claw";
12065
- PLIST_PATH = join17(homedir5(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
12066
- SYSTEMD_DIR = join17(homedir5(), ".config", "systemd", "user");
12067
- UNIT_PATH = join17(SYSTEMD_DIR, "cc-claw.service");
12737
+ PLIST_PATH = join18(homedir5(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
12738
+ SYSTEMD_DIR = join18(homedir5(), ".config", "systemd", "user");
12739
+ UNIT_PATH = join18(SYSTEMD_DIR, "cc-claw.service");
12068
12740
  }
12069
12741
  });
12070
12742
 
@@ -12231,13 +12903,13 @@ var init_daemon = __esm({
12231
12903
  });
12232
12904
 
12233
12905
  // src/cli/resolve-chat.ts
12234
- import { readFileSync as readFileSync11 } from "fs";
12906
+ import { readFileSync as readFileSync12 } from "fs";
12235
12907
  function resolveChatId(globalOpts) {
12236
12908
  const explicit = globalOpts.chat;
12237
12909
  if (explicit) return explicit;
12238
12910
  if (_cachedDefault) return _cachedDefault;
12239
12911
  try {
12240
- const content = readFileSync11(ENV_PATH, "utf-8");
12912
+ const content = readFileSync12(ENV_PATH, "utf-8");
12241
12913
  const match = content.match(/^ALLOWED_CHAT_ID=(.+)$/m);
12242
12914
  if (match) {
12243
12915
  _cachedDefault = match[1].split(",")[0].trim();
@@ -12263,12 +12935,12 @@ __export(api_client_exports, {
12263
12935
  apiPost: () => apiPost,
12264
12936
  isDaemonRunning: () => isDaemonRunning
12265
12937
  });
12266
- import { readFileSync as readFileSync12, existsSync as existsSync17 } from "fs";
12938
+ import { readFileSync as readFileSync13, existsSync as existsSync18 } from "fs";
12267
12939
  import { request as httpRequest } from "http";
12268
12940
  function getToken() {
12269
12941
  if (process.env.CC_CLAW_API_TOKEN) return process.env.CC_CLAW_API_TOKEN;
12270
12942
  try {
12271
- if (existsSync17(TOKEN_PATH)) return readFileSync12(TOKEN_PATH, "utf-8").trim();
12943
+ if (existsSync18(TOKEN_PATH)) return readFileSync13(TOKEN_PATH, "utf-8").trim();
12272
12944
  } catch {
12273
12945
  }
12274
12946
  return null;
@@ -12361,7 +13033,7 @@ var status_exports = {};
12361
13033
  __export(status_exports, {
12362
13034
  statusCommand: () => statusCommand
12363
13035
  });
12364
- import { existsSync as existsSync18, statSync as statSync4 } from "fs";
13036
+ import { existsSync as existsSync19, statSync as statSync3 } from "fs";
12365
13037
  async function statusCommand(globalOpts, localOpts) {
12366
13038
  try {
12367
13039
  const { openDatabaseReadOnly: openDatabaseReadOnly2 } = await Promise.resolve().then(() => (init_store4(), store_exports3));
@@ -12401,7 +13073,7 @@ async function statusCommand(globalOpts, localOpts) {
12401
13073
  const cwdRow = readDb.prepare("SELECT cwd FROM chat_cwd WHERE chat_id = ?").get(chatId);
12402
13074
  const voiceRow = readDb.prepare("SELECT enabled FROM chat_voice WHERE chat_id = ?").get(chatId);
12403
13075
  const usageRow = readDb.prepare("SELECT * FROM chat_usage WHERE chat_id = ?").get(chatId);
12404
- const dbStat = existsSync18(DB_PATH) ? statSync4(DB_PATH) : null;
13076
+ const dbStat = existsSync19(DB_PATH) ? statSync3(DB_PATH) : null;
12405
13077
  let daemonRunning = false;
12406
13078
  let daemonInfo = {};
12407
13079
  if (localOpts.deep) {
@@ -12492,12 +13164,12 @@ var doctor_exports = {};
12492
13164
  __export(doctor_exports, {
12493
13165
  doctorCommand: () => doctorCommand
12494
13166
  });
12495
- import { existsSync as existsSync19, statSync as statSync5, accessSync, constants } from "fs";
13167
+ import { existsSync as existsSync20, statSync as statSync4, accessSync, constants } from "fs";
12496
13168
  import { execFileSync as execFileSync3 } from "child_process";
12497
13169
  async function doctorCommand(globalOpts, localOpts) {
12498
13170
  const checks = [];
12499
- if (existsSync19(DB_PATH)) {
12500
- const size = statSync5(DB_PATH).size;
13171
+ if (existsSync20(DB_PATH)) {
13172
+ const size = statSync4(DB_PATH).size;
12501
13173
  checks.push({ name: "Database", status: "ok", message: `${DB_PATH} (${(size / 1024).toFixed(0)}KB)` });
12502
13174
  try {
12503
13175
  const { openDatabaseReadOnly: openDatabaseReadOnly2 } = await Promise.resolve().then(() => (init_store4(), store_exports3));
@@ -12526,7 +13198,7 @@ async function doctorCommand(globalOpts, localOpts) {
12526
13198
  } else {
12527
13199
  checks.push({ name: "Database", status: "error", message: `Not found at ${DB_PATH}`, fix: "cc-claw setup" });
12528
13200
  }
12529
- if (existsSync19(ENV_PATH)) {
13201
+ if (existsSync20(ENV_PATH)) {
12530
13202
  checks.push({ name: "Environment", status: "ok", message: `.env loaded` });
12531
13203
  } else {
12532
13204
  checks.push({ name: "Environment", status: "error", message: "No .env found", fix: "cc-claw setup" });
@@ -12566,7 +13238,7 @@ async function doctorCommand(globalOpts, localOpts) {
12566
13238
  checks.push({ name: "Daemon", status: "warning", message: "could not probe" });
12567
13239
  }
12568
13240
  const tokenPath = `${DATA_PATH}/api-token`;
12569
- if (existsSync19(tokenPath)) {
13241
+ if (existsSync20(tokenPath)) {
12570
13242
  try {
12571
13243
  accessSync(tokenPath, constants.R_OK);
12572
13244
  checks.push({ name: "API token", status: "ok", message: "token file readable" });
@@ -12591,10 +13263,10 @@ async function doctorCommand(globalOpts, localOpts) {
12591
13263
  }
12592
13264
  } catch {
12593
13265
  }
12594
- if (existsSync19(ERROR_LOG_PATH)) {
13266
+ if (existsSync20(ERROR_LOG_PATH)) {
12595
13267
  try {
12596
- const { readFileSync: readFileSync17 } = await import("fs");
12597
- const logContent = readFileSync17(ERROR_LOG_PATH, "utf-8");
13268
+ const { readFileSync: readFileSync18 } = await import("fs");
13269
+ const logContent = readFileSync18(ERROR_LOG_PATH, "utf-8");
12598
13270
  const recentLines = logContent.split("\n").filter(Boolean).slice(-100);
12599
13271
  const last24h = Date.now() - 864e5;
12600
13272
  const recentErrors = recentLines.filter((line) => {
@@ -12710,15 +13382,15 @@ var logs_exports = {};
12710
13382
  __export(logs_exports, {
12711
13383
  logsCommand: () => logsCommand
12712
13384
  });
12713
- import { existsSync as existsSync20, readFileSync as readFileSync13, watchFile as watchFile2, unwatchFile as unwatchFile2 } from "fs";
13385
+ import { existsSync as existsSync21, readFileSync as readFileSync14, watchFile as watchFile2, unwatchFile as unwatchFile2 } from "fs";
12714
13386
  async function logsCommand(opts) {
12715
13387
  const logFile = opts.error ? ERROR_LOG_PATH : LOG_PATH;
12716
- if (!existsSync20(logFile)) {
13388
+ if (!existsSync21(logFile)) {
12717
13389
  outputError("LOG_NOT_FOUND", `Log file not found: ${logFile}`);
12718
13390
  process.exit(1);
12719
13391
  }
12720
13392
  const maxLines = parseInt(opts.lines ?? "100", 10);
12721
- const content = readFileSync13(logFile, "utf-8");
13393
+ const content = readFileSync14(logFile, "utf-8");
12722
13394
  const allLines = content.split("\n");
12723
13395
  const tailLines = allLines.slice(-maxLines);
12724
13396
  console.log(muted(` \u2500\u2500 ${logFile} (last ${tailLines.length} lines) \u2500\u2500`));
@@ -12728,7 +13400,7 @@ async function logsCommand(opts) {
12728
13400
  let lastSize = Buffer.byteLength(content, "utf-8");
12729
13401
  watchFile2(logFile, { interval: 500 }, () => {
12730
13402
  try {
12731
- const newContent = readFileSync13(logFile, "utf-8");
13403
+ const newContent = readFileSync14(logFile, "utf-8");
12732
13404
  const newSize = Buffer.byteLength(newContent, "utf-8");
12733
13405
  if (newSize > lastSize) {
12734
13406
  const newPart = newContent.slice(content.length);
@@ -12761,12 +13433,12 @@ __export(backend_exports, {
12761
13433
  backendList: () => backendList,
12762
13434
  backendSet: () => backendSet
12763
13435
  });
12764
- import { existsSync as existsSync21 } from "fs";
13436
+ import { existsSync as existsSync22 } from "fs";
12765
13437
  async function backendList(globalOpts) {
12766
13438
  const { getAllAdapters: getAllAdapters2 } = await Promise.resolve().then(() => (init_backends(), backends_exports));
12767
13439
  const chatId = resolveChatId(globalOpts);
12768
13440
  let activeBackend = null;
12769
- if (existsSync21(DB_PATH)) {
13441
+ if (existsSync22(DB_PATH)) {
12770
13442
  const { openDatabaseReadOnly: openDatabaseReadOnly2 } = await Promise.resolve().then(() => (init_store4(), store_exports3));
12771
13443
  const readDb = openDatabaseReadOnly2();
12772
13444
  try {
@@ -12797,7 +13469,7 @@ async function backendList(globalOpts) {
12797
13469
  }
12798
13470
  async function backendGet(globalOpts) {
12799
13471
  const chatId = resolveChatId(globalOpts);
12800
- if (!existsSync21(DB_PATH)) {
13472
+ if (!existsSync22(DB_PATH)) {
12801
13473
  outputError("DB_NOT_FOUND", "Database not found. Run cc-claw setup first.");
12802
13474
  process.exit(1);
12803
13475
  }
@@ -12841,13 +13513,13 @@ __export(model_exports, {
12841
13513
  modelList: () => modelList,
12842
13514
  modelSet: () => modelSet
12843
13515
  });
12844
- import { existsSync as existsSync22 } from "fs";
13516
+ import { existsSync as existsSync23 } from "fs";
12845
13517
  async function modelList(globalOpts) {
12846
13518
  const chatId = resolveChatId(globalOpts);
12847
13519
  const { openDatabaseReadOnly: openDatabaseReadOnly2 } = await Promise.resolve().then(() => (init_store4(), store_exports3));
12848
13520
  const { getAdapter: getAdapter2, getAllAdapters: getAllAdapters2 } = await Promise.resolve().then(() => (init_backends(), backends_exports));
12849
13521
  let backendId = "claude";
12850
- if (existsSync22(DB_PATH)) {
13522
+ if (existsSync23(DB_PATH)) {
12851
13523
  const readDb = openDatabaseReadOnly2();
12852
13524
  try {
12853
13525
  const row = readDb.prepare("SELECT backend FROM chat_backend WHERE chat_id = ?").get(chatId);
@@ -12880,7 +13552,7 @@ async function modelList(globalOpts) {
12880
13552
  }
12881
13553
  async function modelGet(globalOpts) {
12882
13554
  const chatId = resolveChatId(globalOpts);
12883
- if (!existsSync22(DB_PATH)) {
13555
+ if (!existsSync23(DB_PATH)) {
12884
13556
  outputError("DB_NOT_FOUND", "Database not found.");
12885
13557
  process.exit(1);
12886
13558
  }
@@ -12924,9 +13596,9 @@ __export(memory_exports, {
12924
13596
  memoryList: () => memoryList,
12925
13597
  memorySearch: () => memorySearch
12926
13598
  });
12927
- import { existsSync as existsSync23 } from "fs";
13599
+ import { existsSync as existsSync24 } from "fs";
12928
13600
  async function memoryList(globalOpts) {
12929
- if (!existsSync23(DB_PATH)) {
13601
+ if (!existsSync24(DB_PATH)) {
12930
13602
  outputError("DB_NOT_FOUND", "Database not found. Run cc-claw setup first.");
12931
13603
  process.exit(1);
12932
13604
  }
@@ -12950,7 +13622,7 @@ async function memoryList(globalOpts) {
12950
13622
  });
12951
13623
  }
12952
13624
  async function memorySearch(globalOpts, query) {
12953
- if (!existsSync23(DB_PATH)) {
13625
+ if (!existsSync24(DB_PATH)) {
12954
13626
  outputError("DB_NOT_FOUND", "Database not found.");
12955
13627
  process.exit(1);
12956
13628
  }
@@ -12972,7 +13644,7 @@ async function memorySearch(globalOpts, query) {
12972
13644
  });
12973
13645
  }
12974
13646
  async function memoryHistory(globalOpts, opts) {
12975
- if (!existsSync23(DB_PATH)) {
13647
+ if (!existsSync24(DB_PATH)) {
12976
13648
  outputError("DB_NOT_FOUND", "Database not found.");
12977
13649
  process.exit(1);
12978
13650
  }
@@ -13020,7 +13692,17 @@ __export(cron_exports2, {
13020
13692
  cronList: () => cronList,
13021
13693
  cronRuns: () => cronRuns
13022
13694
  });
13023
- import { existsSync as existsSync24 } from "fs";
13695
+ import { existsSync as existsSync25 } from "fs";
13696
+ function parseFallbacks(raw) {
13697
+ return raw.slice(0, 3).map((f) => {
13698
+ const [backend2, ...rest] = f.split(":");
13699
+ const model2 = rest.join(":");
13700
+ if (!backend2 || !model2) {
13701
+ throw new Error(`Invalid fallback format "${f}" \u2014 expected backend:model (e.g. gemini:gemini-3-flash-preview)`);
13702
+ }
13703
+ return { backend: backend2, model: model2 };
13704
+ });
13705
+ }
13024
13706
  function parseAndValidateTimeout(raw) {
13025
13707
  if (!raw) return null;
13026
13708
  const val = parseInt(raw, 10);
@@ -13031,7 +13713,7 @@ function parseAndValidateTimeout(raw) {
13031
13713
  return val;
13032
13714
  }
13033
13715
  async function cronList(globalOpts) {
13034
- if (!existsSync24(DB_PATH)) {
13716
+ if (!existsSync25(DB_PATH)) {
13035
13717
  outputError("DB_NOT_FOUND", "Database not found.");
13036
13718
  process.exit(1);
13037
13719
  }
@@ -13052,15 +13734,24 @@ async function cronList(globalOpts) {
13052
13734
  lines.push(` ${statusDot(status)} #${j.id} [${status}] ${schedule2}${tz}`);
13053
13735
  lines.push(` ${j.description}`);
13054
13736
  if (j.backend) lines.push(` Backend: ${j.backend}${j.model ? ` / ${j.model}` : ""}`);
13737
+ if (j.fallbacks) {
13738
+ try {
13739
+ const fb = JSON.parse(j.fallbacks);
13740
+ if (Array.isArray(fb) && fb.length > 0) {
13741
+ lines.push(` Fallbacks: ${fb.map((f) => `${f.backend}:${f.model}`).join(" \u2192 ")}`);
13742
+ }
13743
+ } catch {
13744
+ }
13745
+ }
13055
13746
  if (j.timeout) lines.push(` Timeout: ${j.timeout}s`);
13056
- if (j.next_run_at) lines.push(` Next: ${muted(j.next_run_at)}`);
13747
+ if (j.next_run_at) lines.push(` Next: ${muted(formatLocalDateTime(j.next_run_at))}`);
13057
13748
  lines.push("");
13058
13749
  }
13059
13750
  return lines.join("\n");
13060
13751
  });
13061
13752
  }
13062
13753
  async function cronHealth(globalOpts) {
13063
- if (!existsSync24(DB_PATH)) {
13754
+ if (!existsSync25(DB_PATH)) {
13064
13755
  outputError("DB_NOT_FOUND", "Database not found.");
13065
13756
  process.exit(1);
13066
13757
  }
@@ -13114,6 +13805,7 @@ async function cronCreate(globalOpts, opts) {
13114
13805
  everyMs = unit.startsWith("h") ? num * 36e5 : unit.startsWith("m") ? num * 6e4 : num * 1e3;
13115
13806
  }
13116
13807
  }
13808
+ const fallbacks = opts.fallback?.length ? parseFallbacks(opts.fallback) : void 0;
13117
13809
  const job = insertJob2({
13118
13810
  scheduleType: schedType,
13119
13811
  cron: opts.cron ?? null,
@@ -13125,6 +13817,7 @@ async function cronCreate(globalOpts, opts) {
13125
13817
  model: opts.model ?? null,
13126
13818
  thinking: opts.thinking ?? null,
13127
13819
  timeout,
13820
+ fallbacks,
13128
13821
  sessionType: opts.sessionType ?? "isolated",
13129
13822
  deliveryMode: opts.delivery ?? "announce",
13130
13823
  channel: opts.channel ?? null,
@@ -13209,6 +13902,11 @@ async function cronEdit(globalOpts, id, opts) {
13209
13902
  updates.push("timezone = ?");
13210
13903
  values.push(opts.timezone);
13211
13904
  }
13905
+ if (opts.fallback?.length) {
13906
+ const fallbacks = parseFallbacks(opts.fallback);
13907
+ updates.push("fallbacks = ?");
13908
+ values.push(JSON.stringify(fallbacks));
13909
+ }
13212
13910
  if (updates.length === 0) {
13213
13911
  outputError("NO_CHANGES", "No fields to update. Specify fields with flags (e.g. --description, --cron).");
13214
13912
  process.exit(1);
@@ -13224,7 +13922,7 @@ async function cronEdit(globalOpts, id, opts) {
13224
13922
  }
13225
13923
  }
13226
13924
  async function cronRuns(globalOpts, jobId, opts) {
13227
- if (!existsSync24(DB_PATH)) {
13925
+ if (!existsSync25(DB_PATH)) {
13228
13926
  outputError("DB_NOT_FOUND", "Database not found.");
13229
13927
  process.exit(1);
13230
13928
  }
@@ -13243,7 +13941,7 @@ async function cronRuns(globalOpts, jobId, opts) {
13243
13941
  const lines = ["", divider("Run History"), ""];
13244
13942
  for (const r of list) {
13245
13943
  const duration = r.duration_ms ? ` (${(r.duration_ms / 1e3).toFixed(1)}s)` : "";
13246
- lines.push(` #${r.job_id} [${r.status}] ${r.started_at}${duration}`);
13944
+ lines.push(` #${r.job_id} [${r.status}] ${formatLocalDateTime(r.started_at)}${duration}`);
13247
13945
  if (r.error) lines.push(` Error: ${r.error.slice(0, 100)}`);
13248
13946
  lines.push("");
13249
13947
  }
@@ -13257,6 +13955,7 @@ var init_cron2 = __esm({
13257
13955
  init_paths();
13258
13956
  init_resolve_chat();
13259
13957
  init_types2();
13958
+ init_format_time();
13260
13959
  }
13261
13960
  });
13262
13961
 
@@ -13270,9 +13969,9 @@ __export(agents_exports, {
13270
13969
  runnersList: () => runnersList,
13271
13970
  tasksList: () => tasksList
13272
13971
  });
13273
- import { existsSync as existsSync25 } from "fs";
13972
+ import { existsSync as existsSync26 } from "fs";
13274
13973
  async function agentsList(globalOpts) {
13275
- if (!existsSync25(DB_PATH)) {
13974
+ if (!existsSync26(DB_PATH)) {
13276
13975
  outputError("DB_NOT_FOUND", "Database not found.");
13277
13976
  process.exit(1);
13278
13977
  }
@@ -13303,7 +14002,7 @@ async function agentsList(globalOpts) {
13303
14002
  });
13304
14003
  }
13305
14004
  async function tasksList(globalOpts) {
13306
- if (!existsSync25(DB_PATH)) {
14005
+ if (!existsSync26(DB_PATH)) {
13307
14006
  outputError("DB_NOT_FOUND", "Database not found.");
13308
14007
  process.exit(1);
13309
14008
  }
@@ -13431,18 +14130,18 @@ __export(db_exports, {
13431
14130
  dbPath: () => dbPath,
13432
14131
  dbStats: () => dbStats
13433
14132
  });
13434
- import { existsSync as existsSync26, statSync as statSync6, copyFileSync, mkdirSync as mkdirSync8 } from "fs";
14133
+ import { existsSync as existsSync27, statSync as statSync5, copyFileSync, mkdirSync as mkdirSync9 } from "fs";
13435
14134
  import { dirname as dirname5 } from "path";
13436
14135
  async function dbStats(globalOpts) {
13437
- if (!existsSync26(DB_PATH)) {
14136
+ if (!existsSync27(DB_PATH)) {
13438
14137
  outputError("DB_NOT_FOUND", `Database not found at ${DB_PATH}`);
13439
14138
  process.exit(1);
13440
14139
  }
13441
14140
  const { openDatabaseReadOnly: openDatabaseReadOnly2 } = await Promise.resolve().then(() => (init_store4(), store_exports3));
13442
14141
  const readDb = openDatabaseReadOnly2();
13443
- const mainSize = statSync6(DB_PATH).size;
14142
+ const mainSize = statSync5(DB_PATH).size;
13444
14143
  const walPath = DB_PATH + "-wal";
13445
- const walSize = existsSync26(walPath) ? statSync6(walPath).size : 0;
14144
+ const walSize = existsSync27(walPath) ? statSync5(walPath).size : 0;
13446
14145
  const tableNames = readDb.prepare(
13447
14146
  "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '%_fts%' ORDER BY name"
13448
14147
  ).all();
@@ -13476,17 +14175,17 @@ async function dbPath(globalOpts) {
13476
14175
  output({ path: DB_PATH }, (d) => d.path);
13477
14176
  }
13478
14177
  async function dbBackup(globalOpts, destPath) {
13479
- if (!existsSync26(DB_PATH)) {
14178
+ if (!existsSync27(DB_PATH)) {
13480
14179
  outputError("DB_NOT_FOUND", `Database not found at ${DB_PATH}`);
13481
14180
  process.exit(1);
13482
14181
  }
13483
14182
  const dest = destPath ?? `${DB_PATH}.backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
13484
14183
  try {
13485
- mkdirSync8(dirname5(dest), { recursive: true });
14184
+ mkdirSync9(dirname5(dest), { recursive: true });
13486
14185
  copyFileSync(DB_PATH, dest);
13487
14186
  const walPath = DB_PATH + "-wal";
13488
- if (existsSync26(walPath)) copyFileSync(walPath, dest + "-wal");
13489
- output({ path: dest, sizeBytes: statSync6(dest).size }, (d) => {
14187
+ if (existsSync27(walPath)) copyFileSync(walPath, dest + "-wal");
14188
+ output({ path: dest, sizeBytes: statSync5(dest).size }, (d) => {
13490
14189
  const b = d;
13491
14190
  return `
13492
14191
  ${success("Backup created:")} ${b.path} (${(b.sizeBytes / 1024).toFixed(0)}KB)
@@ -13514,9 +14213,9 @@ __export(usage_exports, {
13514
14213
  usageCost: () => usageCost,
13515
14214
  usageTokens: () => usageTokens
13516
14215
  });
13517
- import { existsSync as existsSync27 } from "fs";
14216
+ import { existsSync as existsSync28 } from "fs";
13518
14217
  function ensureDb() {
13519
- if (!existsSync27(DB_PATH)) {
14218
+ if (!existsSync28(DB_PATH)) {
13520
14219
  outputError("DB_NOT_FOUND", "Database not found. Run cc-claw setup first.");
13521
14220
  process.exit(1);
13522
14221
  }
@@ -13706,9 +14405,9 @@ __export(config_exports, {
13706
14405
  configList: () => configList,
13707
14406
  configSet: () => configSet
13708
14407
  });
13709
- import { existsSync as existsSync28, readFileSync as readFileSync14 } from "fs";
14408
+ import { existsSync as existsSync29, readFileSync as readFileSync15 } from "fs";
13710
14409
  async function configList(globalOpts) {
13711
- if (!existsSync28(DB_PATH)) {
14410
+ if (!existsSync29(DB_PATH)) {
13712
14411
  outputError("DB_NOT_FOUND", "Database not found.");
13713
14412
  process.exit(1);
13714
14413
  }
@@ -13742,7 +14441,7 @@ async function configGet(globalOpts, key) {
13742
14441
  outputError("INVALID_KEY", `Unknown config key "${key}". Valid keys: ${RUNTIME_KEYS.join(", ")}`);
13743
14442
  process.exit(1);
13744
14443
  }
13745
- if (!existsSync28(DB_PATH)) {
14444
+ if (!existsSync29(DB_PATH)) {
13746
14445
  outputError("DB_NOT_FOUND", "Database not found.");
13747
14446
  process.exit(1);
13748
14447
  }
@@ -13788,11 +14487,11 @@ async function configSet(globalOpts, key, value) {
13788
14487
  }
13789
14488
  }
13790
14489
  async function configEnv(_globalOpts) {
13791
- if (!existsSync28(ENV_PATH)) {
14490
+ if (!existsSync29(ENV_PATH)) {
13792
14491
  outputError("ENV_NOT_FOUND", `No .env file at ${ENV_PATH}. Run cc-claw setup.`);
13793
14492
  process.exit(1);
13794
14493
  }
13795
- const content = readFileSync14(ENV_PATH, "utf-8");
14494
+ const content = readFileSync15(ENV_PATH, "utf-8");
13796
14495
  const entries = {};
13797
14496
  const secretPatterns = /TOKEN|KEY|SECRET|PASSWORD|CREDENTIALS/i;
13798
14497
  for (const line of content.split("\n")) {
@@ -13841,9 +14540,9 @@ __export(session_exports, {
13841
14540
  sessionGet: () => sessionGet,
13842
14541
  sessionNew: () => sessionNew
13843
14542
  });
13844
- import { existsSync as existsSync29 } from "fs";
14543
+ import { existsSync as existsSync30 } from "fs";
13845
14544
  async function sessionGet(globalOpts) {
13846
- if (!existsSync29(DB_PATH)) {
14545
+ if (!existsSync30(DB_PATH)) {
13847
14546
  outputError("DB_NOT_FOUND", "Database not found.");
13848
14547
  process.exit(1);
13849
14548
  }
@@ -13904,9 +14603,9 @@ __export(permissions_exports, {
13904
14603
  verboseGet: () => verboseGet,
13905
14604
  verboseSet: () => verboseSet
13906
14605
  });
13907
- import { existsSync as existsSync30 } from "fs";
14606
+ import { existsSync as existsSync31 } from "fs";
13908
14607
  function ensureDb2() {
13909
- if (!existsSync30(DB_PATH)) {
14608
+ if (!existsSync31(DB_PATH)) {
13910
14609
  outputError("DB_NOT_FOUND", "Database not found.");
13911
14610
  process.exit(1);
13912
14611
  }
@@ -13922,7 +14621,8 @@ async function permissionsGet(globalOpts) {
13922
14621
  output({ mode }, (d) => d.mode);
13923
14622
  }
13924
14623
  async function permissionsSet(globalOpts, mode) {
13925
- const valid = ["yolo", "safe", "readonly", "plan"];
14624
+ if (mode === "readonly") mode = "plan";
14625
+ const valid = ["yolo", "safe", "plan"];
13926
14626
  if (!valid.includes(mode)) {
13927
14627
  outputError("INVALID_MODE", `Invalid mode "${mode}". Valid: ${valid.join(", ")}`);
13928
14628
  process.exit(1);
@@ -14052,9 +14752,9 @@ __export(cwd_exports, {
14052
14752
  cwdGet: () => cwdGet,
14053
14753
  cwdSet: () => cwdSet
14054
14754
  });
14055
- import { existsSync as existsSync31 } from "fs";
14755
+ import { existsSync as existsSync32 } from "fs";
14056
14756
  async function cwdGet(globalOpts) {
14057
- if (!existsSync31(DB_PATH)) {
14757
+ if (!existsSync32(DB_PATH)) {
14058
14758
  outputError("DB_NOT_FOUND", "Database not found.");
14059
14759
  process.exit(1);
14060
14760
  }
@@ -14116,9 +14816,9 @@ __export(voice_exports, {
14116
14816
  voiceGet: () => voiceGet,
14117
14817
  voiceSet: () => voiceSet
14118
14818
  });
14119
- import { existsSync as existsSync32 } from "fs";
14819
+ import { existsSync as existsSync33 } from "fs";
14120
14820
  async function voiceGet(globalOpts) {
14121
- if (!existsSync32(DB_PATH)) {
14821
+ if (!existsSync33(DB_PATH)) {
14122
14822
  outputError("DB_NOT_FOUND", "Database not found.");
14123
14823
  process.exit(1);
14124
14824
  }
@@ -14167,9 +14867,9 @@ __export(heartbeat_exports, {
14167
14867
  heartbeatGet: () => heartbeatGet,
14168
14868
  heartbeatSet: () => heartbeatSet
14169
14869
  });
14170
- import { existsSync as existsSync33 } from "fs";
14870
+ import { existsSync as existsSync34 } from "fs";
14171
14871
  async function heartbeatGet(globalOpts) {
14172
- if (!existsSync33(DB_PATH)) {
14872
+ if (!existsSync34(DB_PATH)) {
14173
14873
  outputError("DB_NOT_FOUND", "Database not found.");
14174
14874
  process.exit(1);
14175
14875
  }
@@ -14279,9 +14979,9 @@ __export(chats_exports, {
14279
14979
  chatsList: () => chatsList,
14280
14980
  chatsRemoveAlias: () => chatsRemoveAlias
14281
14981
  });
14282
- import { existsSync as existsSync34 } from "fs";
14982
+ import { existsSync as existsSync35 } from "fs";
14283
14983
  async function chatsList(_globalOpts) {
14284
- if (!existsSync34(DB_PATH)) {
14984
+ if (!existsSync35(DB_PATH)) {
14285
14985
  outputError("DB_NOT_FOUND", "Database not found.");
14286
14986
  process.exit(1);
14287
14987
  }
@@ -14409,9 +15109,9 @@ var mcps_exports = {};
14409
15109
  __export(mcps_exports, {
14410
15110
  mcpsList: () => mcpsList
14411
15111
  });
14412
- import { existsSync as existsSync35 } from "fs";
15112
+ import { existsSync as existsSync36 } from "fs";
14413
15113
  async function mcpsList(_globalOpts) {
14414
- if (!existsSync35(DB_PATH)) {
15114
+ if (!existsSync36(DB_PATH)) {
14415
15115
  outputError("DB_NOT_FOUND", "Database not found.");
14416
15116
  process.exit(1);
14417
15117
  }
@@ -14448,11 +15148,11 @@ __export(chat_exports, {
14448
15148
  chatSend: () => chatSend
14449
15149
  });
14450
15150
  import { request as httpRequest2 } from "http";
14451
- import { readFileSync as readFileSync15, existsSync as existsSync36 } from "fs";
15151
+ import { readFileSync as readFileSync16, existsSync as existsSync37 } from "fs";
14452
15152
  function getToken2() {
14453
15153
  if (process.env.CC_CLAW_API_TOKEN) return process.env.CC_CLAW_API_TOKEN;
14454
15154
  try {
14455
- if (existsSync36(TOKEN_PATH2)) return readFileSync15(TOKEN_PATH2, "utf-8").trim();
15155
+ if (existsSync37(TOKEN_PATH2)) return readFileSync16(TOKEN_PATH2, "utf-8").trim();
14456
15156
  } catch {
14457
15157
  }
14458
15158
  return null;
@@ -14878,10 +15578,10 @@ var init_completion = __esm({
14878
15578
 
14879
15579
  // src/setup.ts
14880
15580
  var setup_exports = {};
14881
- import { existsSync as existsSync37, writeFileSync as writeFileSync7, readFileSync as readFileSync16, copyFileSync as copyFileSync2, mkdirSync as mkdirSync9, statSync as statSync7 } from "fs";
15581
+ import { existsSync as existsSync38, writeFileSync as writeFileSync8, readFileSync as readFileSync17, copyFileSync as copyFileSync2, mkdirSync as mkdirSync10, statSync as statSync6 } from "fs";
14882
15582
  import { execFileSync as execFileSync4 } from "child_process";
14883
15583
  import { createInterface as createInterface5 } from "readline";
14884
- import { join as join18 } from "path";
15584
+ import { join as join19 } from "path";
14885
15585
  function divider2() {
14886
15586
  console.log(dim("\u2500".repeat(55)));
14887
15587
  }
@@ -14955,22 +15655,22 @@ async function setup() {
14955
15655
  }
14956
15656
  console.log("");
14957
15657
  for (const dir of [CC_CLAW_HOME, DATA_PATH, LOGS_PATH, SKILLS_PATH, RUNNERS_PATH, AGENTS_PATH]) {
14958
- if (!existsSync37(dir)) mkdirSync9(dir, { recursive: true });
15658
+ if (!existsSync38(dir)) mkdirSync10(dir, { recursive: true });
14959
15659
  }
14960
15660
  const env = {};
14961
- const envSource = existsSync37(ENV_PATH) ? ENV_PATH : existsSync37(".env") ? ".env" : null;
15661
+ const envSource = existsSync38(ENV_PATH) ? ENV_PATH : existsSync38(".env") ? ".env" : null;
14962
15662
  if (envSource) {
14963
15663
  console.log(yellow(` Found existing config at ${envSource} \u2014 your values will be preserved`));
14964
15664
  console.log(yellow(" unless you enter new ones. Just press Enter to keep existing values.\n"));
14965
- const existing = readFileSync16(envSource, "utf-8");
15665
+ const existing = readFileSync17(envSource, "utf-8");
14966
15666
  for (const line of existing.split("\n")) {
14967
15667
  const match = line.match(/^([^#=]+)=(.*)$/);
14968
15668
  if (match) env[match[1].trim()] = match[2].trim();
14969
15669
  }
14970
15670
  }
14971
- const cwdDb = join18(process.cwd(), "cc-claw.db");
14972
- if (existsSync37(cwdDb) && !existsSync37(DB_PATH)) {
14973
- const { size } = statSync7(cwdDb);
15671
+ const cwdDb = join19(process.cwd(), "cc-claw.db");
15672
+ if (existsSync38(cwdDb) && !existsSync38(DB_PATH)) {
15673
+ const { size } = statSync6(cwdDb);
14974
15674
  console.log(yellow(` Found existing database at ${cwdDb} (${(size / 1024).toFixed(0)}KB)`));
14975
15675
  const migrate = await confirm("Copy database to ~/.cc-claw/? (preserves memories & history)", true);
14976
15676
  if (migrate) {
@@ -15163,7 +15863,7 @@ async function setup() {
15163
15863
  envLines.push("", "# Video Analysis", `GEMINI_API_KEY=${env.GEMINI_API_KEY}`);
15164
15864
  }
15165
15865
  const envContent = envLines.join("\n") + "\n";
15166
- writeFileSync7(ENV_PATH, envContent, { mode: 384 });
15866
+ writeFileSync8(ENV_PATH, envContent, { mode: 384 });
15167
15867
  console.log(green(` Config saved to ${ENV_PATH} (permissions: owner-only)`));
15168
15868
  header(6, TOTAL_STEPS, "Run on Startup (Daemon)");
15169
15869
  console.log(" CC-Claw can run automatically in the background, starting");
@@ -15332,7 +16032,7 @@ function registerCronCommands(cmd) {
15332
16032
  const { cronList: cronList2 } = await Promise.resolve().then(() => (init_cron2(), cron_exports2));
15333
16033
  await cronList2(program.opts());
15334
16034
  });
15335
- cmd.command("create").description("Create a scheduled job").requiredOption("--description <text>", "Job description").option("--prompt <text>", "Agent prompt (defaults to description)").option("--cron <expr>", "Cron expression (e.g. '0 9 * * *')").option("--at <iso8601>", "One-shot time").option("--every <interval>", "Repeat interval (e.g. 30m, 1h)").option("--backend <name>", "Backend for this job").option("--model <name>", "Model for this job").option("--thinking <level>", "Thinking level").option("--timeout <seconds>", "Job timeout in seconds (30-3600)").option("--timezone <tz>", "IANA timezone", "UTC").option("--session-type <type>", "Session type (isolated/main)", "isolated").option("--delivery <mode>", "Delivery mode (announce/webhook/none)", "announce").option("--channel <name>", "Delivery channel").option("--target <id>", "Delivery target").option("--cwd <path>", "Working directory").action(async (opts) => {
16035
+ cmd.command("create").description("Create a scheduled job").requiredOption("--description <text>", "Job description").option("--prompt <text>", "Agent prompt (defaults to description)").option("--cron <expr>", "Cron expression (e.g. '0 9 * * *')").option("--at <iso8601>", "One-shot time").option("--every <interval>", "Repeat interval (e.g. 30m, 1h)").option("--backend <name>", "Backend for this job").option("--model <name>", "Model for this job").option("--thinking <level>", "Thinking level").option("--timeout <seconds>", "Job timeout in seconds (30-3600)").option("--fallback <backend:model>", "Fallback backend:model (repeatable, max 3)", (val, prev) => [...prev, val], []).option("--timezone <tz>", "IANA timezone", "UTC").option("--session-type <type>", "Session type (isolated/main)", "isolated").option("--delivery <mode>", "Delivery mode (announce/webhook/none)", "announce").option("--channel <name>", "Delivery channel").option("--target <id>", "Delivery target").option("--cwd <path>", "Working directory").action(async (opts) => {
15336
16036
  const { cronCreate: cronCreate2 } = await Promise.resolve().then(() => (init_cron2(), cron_exports2));
15337
16037
  await cronCreate2(program.opts(), opts);
15338
16038
  });
@@ -15352,7 +16052,7 @@ function registerCronCommands(cmd) {
15352
16052
  const { cronAction: cronAction2 } = await Promise.resolve().then(() => (init_cron2(), cron_exports2));
15353
16053
  await cronAction2(program.opts(), "run", id);
15354
16054
  });
15355
- cmd.command("edit <id>").description("Edit a job (same flags as create)").option("--description <text>").option("--cron <expr>").option("--at <iso8601>").option("--every <interval>").option("--backend <name>").option("--model <name>").option("--thinking <level>").option("--timeout <seconds>", "Job timeout in seconds (30-3600)").option("--timezone <tz>").action(async (id, opts) => {
16055
+ cmd.command("edit <id>").description("Edit a job (same flags as create)").option("--description <text>").option("--cron <expr>").option("--at <iso8601>").option("--every <interval>").option("--backend <name>").option("--model <name>").option("--thinking <level>").option("--timeout <seconds>", "Job timeout in seconds (30-3600)").option("--fallback <backend:model>", "Fallback backend:model (repeatable, max 3)", (val, prev) => [...prev, val], []).option("--timezone <tz>").action(async (id, opts) => {
15356
16056
  const { cronEdit: cronEdit2 } = await Promise.resolve().then(() => (init_cron2(), cron_exports2));
15357
16057
  await cronEdit2(program.opts(), id, opts);
15358
16058
  });