cc-claw 0.3.11 → 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.
@@ -72,7 +72,7 @@ Typical workflow:
72
72
  description: z.string().optional().describe("Short description of what this agent does"),
73
73
  model: z.string().optional().describe("Model override for the sub-agent"),
74
74
  skills: z.array(z.string()).optional().describe("Skills to inject into the sub-agent"),
75
- permMode: z.string().optional().describe("Permission mode: yolo, safe, readonly, plan (defaults to chat's mode)"),
75
+ permMode: z.string().optional().describe("Permission mode: yolo, safe, plan (defaults to chat's mode)"),
76
76
  role: z.string().optional().describe("Agent role. Presets: worker, reviewer, planner, researcher, writer, analyst, debugger, critic, synthesizer. Custom roles accepted as labels."),
77
77
  persona: z.string().optional().describe("Custom system prompt for the agent. Replaces the role's default prompt while preserving orchestrator tools and task."),
78
78
  allowedTools: z.array(z.string()).optional().describe("Restrict agent to specific tools (Claude-only). E.g., ['Read', 'Grep', 'Glob']"),
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.11" : (() => {
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,
@@ -1395,6 +1409,52 @@ function initDatabase() {
1395
1409
  PRIMARY KEY (chatId, alias)
1396
1410
  )
1397
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
+ `);
1398
1458
  try {
1399
1459
  db.exec("ALTER TABLE memories ADD COLUMN embedding TEXT");
1400
1460
  } catch {
@@ -1674,7 +1734,8 @@ function getMode(chatId) {
1674
1734
  const row = db.prepare(
1675
1735
  "SELECT mode FROM chat_mode WHERE chat_id = ?"
1676
1736
  ).get(chatId);
1677
- return row?.mode ?? "yolo";
1737
+ const raw = row?.mode ?? "yolo";
1738
+ return raw === "readonly" ? "plan" : raw;
1678
1739
  }
1679
1740
  function setMode(chatId, mode) {
1680
1741
  db.prepare(`
@@ -1683,6 +1744,30 @@ function setMode(chatId, mode) {
1683
1744
  ON CONFLICT(chat_id) DO UPDATE SET mode = ?
1684
1745
  `).run(chatId, mode, mode);
1685
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
+ }
1686
1771
  function getUsage(chatId) {
1687
1772
  const row = db.prepare(
1688
1773
  "SELECT input_tokens, output_tokens, cache_read_tokens, request_count, last_input_tokens, last_cache_read_tokens FROM chat_usage WHERE chat_id = ?"
@@ -2166,7 +2251,77 @@ function getAllChatAliases() {
2166
2251
  function removeChatAlias(alias) {
2167
2252
  return db.prepare("DELETE FROM chat_aliases WHERE alias = ?").run(alias.toLowerCase()).changes > 0;
2168
2253
  }
2169
- 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;
2170
2325
  var init_store4 = __esm({
2171
2326
  "src/memory/store.ts"() {
2172
2327
  "use strict";
@@ -2291,6 +2446,8 @@ var init_store4 = __esm({
2291
2446
  "near",
2292
2447
  "or"
2293
2448
  ]);
2449
+ pendingEscalations = /* @__PURE__ */ new Map();
2450
+ ESCALATION_TTL_MS = 5 * 60 * 1e3;
2294
2451
  ALL_TOOLS = ["Read", "Glob", "Grep", "Bash", "Write", "Edit", "WebFetch", "WebSearch", "Agent", "AskUserQuestion"];
2295
2452
  JOB_SELECT = `
2296
2453
  SELECT id, schedule_type as scheduleType, cron, at_time as atTime, every_ms as everyMs,
@@ -2444,15 +2601,14 @@ var init_claude = __esm({
2444
2601
  switch (opts.permMode) {
2445
2602
  case "plan":
2446
2603
  args.push("--permission-mode", "plan");
2447
- break;
2448
- case "readonly":
2449
- args.push("--permission-mode", "bypassPermissions");
2450
- args.push("--allowedTools", "Read,Glob,Grep");
2604
+ args.push("--strict-mcp-config");
2451
2605
  break;
2452
2606
  case "safe":
2453
2607
  args.push("--permission-mode", "bypassPermissions");
2454
2608
  if (opts.allowedTools.length > 0) {
2455
- args.push("--allowedTools", opts.allowedTools.join(","));
2609
+ args.push("--tools", opts.allowedTools.join(","));
2610
+ } else {
2611
+ args.push("--tools", "Read,Glob,Grep");
2456
2612
  }
2457
2613
  break;
2458
2614
  case "yolo":
@@ -2611,11 +2767,10 @@ var init_gemini = __esm({
2611
2767
  if (opts.model) args.push("-m", opts.model);
2612
2768
  switch (opts.permMode) {
2613
2769
  case "plan":
2614
- case "readonly":
2615
2770
  args.push("--approval-mode", "plan");
2616
2771
  break;
2617
2772
  case "safe":
2618
- args.push("--approval-mode", "auto_edit");
2773
+ args.push("--approval-mode", "plan");
2619
2774
  break;
2620
2775
  case "yolo":
2621
2776
  default:
@@ -2697,21 +2852,34 @@ var init_codex = __esm({
2697
2852
  thinking: "adjustable",
2698
2853
  thinkingLevels: ["low", "medium", "high", "extra_high"],
2699
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"
2700
2867
  }
2701
2868
  };
2702
2869
  defaultModel = "gpt-5.4";
2703
- // Internal-only model for summarization — not shown in /model picker
2704
2870
  summarizerModel = "gpt-5.1-codex-mini";
2705
2871
  pricing = {
2706
2872
  "gpt-5.4": { in: 2.5, out: 10, cache: 0.63 },
2707
2873
  "gpt-5.3-codex": { in: 2, out: 8, cache: 0.5 },
2708
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 },
2709
2876
  "gpt-5.1-codex-mini": { in: 0.3, out: 1.2, cache: 0.08 }
2710
2877
  };
2711
2878
  contextWindow = {
2712
2879
  "gpt-5.4": 2e5,
2713
2880
  "gpt-5.3-codex": 2e5,
2714
2881
  "gpt-5.2-codex": 2e5,
2882
+ "gpt-5.1-codex-max": 4e5,
2715
2883
  "gpt-5.1-codex-mini": 2e5
2716
2884
  };
2717
2885
  // Codex has no single result event with final text -- the caller (spawnQuery)
@@ -2753,11 +2921,10 @@ var init_codex = __esm({
2753
2921
  if (opts.model) args.push("-m", opts.model);
2754
2922
  switch (opts.permMode) {
2755
2923
  case "plan":
2756
- case "readonly":
2757
2924
  args.push("--sandbox", "read-only");
2758
2925
  break;
2759
2926
  case "safe":
2760
- args.push("--full-auto");
2927
+ args.push("--sandbox", "read-only");
2761
2928
  break;
2762
2929
  case "yolo":
2763
2930
  default:
@@ -2915,6 +3082,31 @@ function mergeAndScore(allItems, vectorScores, ftsScores, getDays, decayRate, to
2915
3082
  results.sort((a, b) => b.score - a.score);
2916
3083
  return results.slice(0, topK);
2917
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
+ }
2918
3110
  async function injectMemoryContext(userMessage) {
2919
3111
  const provider = resolveProvider();
2920
3112
  const useVectors = !!provider;
@@ -3007,7 +3199,7 @@ async function injectMemoryContext(userMessage) {
3007
3199
  ${lines.join("\n")}
3008
3200
  [End memory context]`;
3009
3201
  }
3010
- 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;
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;
3011
3203
  var init_inject = __esm({
3012
3204
  "src/memory/inject.ts"() {
3013
3205
  "use strict";
@@ -3021,6 +3213,8 @@ var init_inject = __esm({
3021
3213
  FINAL_TOP_K_SESSIONS = 3;
3022
3214
  MAX_MEMORY_CHARS = 500;
3023
3215
  MAX_SESSION_CHARS = 800;
3216
+ MAX_BRIDGE_CHARS = 48e3;
3217
+ pendingContextBridges = /* @__PURE__ */ new Map();
3024
3218
  }
3025
3219
  });
3026
3220
 
@@ -3205,9 +3399,12 @@ function searchContext(userMessage) {
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
3404
  syncNativeCliFiles();
3405
+ if (permMode && permMode !== "yolo") {
3406
+ sections.push(buildPermissionNotice(permMode));
3407
+ }
3211
3408
  if (tier === "full") {
3212
3409
  const ctx = searchContext(userMessage);
3213
3410
  if (ctx) {
@@ -3215,6 +3412,12 @@ async function assembleBootstrapPrompt(userMessage, tier = "full", chatId) {
3215
3412
  ${ctx}`);
3216
3413
  }
3217
3414
  }
3415
+ if (chatId && tier !== "slim") {
3416
+ const bridge = consumeContextBridge(chatId);
3417
+ if (bridge) {
3418
+ sections.push(bridge);
3419
+ }
3420
+ }
3218
3421
  if (tier !== "slim") {
3219
3422
  const memory2 = await injectMemoryContext(userMessage);
3220
3423
  if (memory2) {
@@ -3232,6 +3435,12 @@ ${ctx}`);
3232
3435
  log(`[bootstrap] Assembled prompt: tier=${tier}, sections=${sections.length}, totalChars=${result.length}`);
3233
3436
  return result;
3234
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
+ }
3235
3444
  function truncateToTokenBudget(text, budget) {
3236
3445
  const charBudget = budget * 4;
3237
3446
  if (text.length <= charBudget) return text;
@@ -3296,66 +3505,62 @@ var init_loader = __esm({
3296
3505
  });
3297
3506
 
3298
3507
  // src/memory/session-log.ts
3299
- function appendToLog(chatId, userMessage, assistantResponse) {
3300
- if (!logs.has(chatId)) logs.set(chatId, []);
3301
- const entries = logs.get(chatId);
3508
+ function appendToLog(chatId, userMessage, assistantResponse, backend2, model2, sessionId) {
3302
3509
  const now = Date.now();
3303
- entries.push({ role: "user", text: userMessage, timestamp: now });
3304
- entries.push({ role: "assistant", text: assistantResponse, timestamp: now });
3305
- const maxEntries = MAX_PAIRS * 2;
3306
- if (entries.length > maxEntries) {
3307
- 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);
3308
3519
  }
3520
+ cache.set(chatId, existing);
3309
3521
  }
3310
3522
  function getLog(chatId) {
3311
- 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);
3312
3536
  }
3313
3537
  function clearLog(chatId) {
3314
- logs.delete(chatId);
3538
+ markMessageLogSummarized(chatId);
3539
+ cache.delete(chatId);
3315
3540
  }
3316
3541
  function getLoggedChatIds() {
3317
- return [...logs.keys()].filter((id) => (logs.get(id)?.length ?? 0) > 0);
3542
+ return getUnsummarizedChatIds();
3318
3543
  }
3319
- function getMessagePairCount(chatId) {
3320
- return Math.floor((logs.get(chatId)?.length ?? 0) / 2);
3321
- }
3322
- var logs, MAX_PAIRS;
3544
+ var CACHE_SIZE, cache;
3323
3545
  var init_session_log = __esm({
3324
3546
  "src/memory/session-log.ts"() {
3325
3547
  "use strict";
3326
- logs = /* @__PURE__ */ new Map();
3327
- MAX_PAIRS = 100;
3548
+ init_store4();
3549
+ CACHE_SIZE = 20;
3550
+ cache = /* @__PURE__ */ new Map();
3328
3551
  }
3329
3552
  });
3330
3553
 
3331
3554
  // src/memory/summarize.ts
3332
3555
  var summarize_exports = {};
3333
3556
  __export(summarize_exports, {
3557
+ CONTEXT_BRIDGE_PROMPT: () => CONTEXT_BRIDGE_PROMPT,
3334
3558
  summarizeAllPending: () => summarizeAllPending,
3335
- summarizeSession: () => summarizeSession
3559
+ summarizeSession: () => summarizeSession,
3560
+ summarizeWithFallbackChain: () => summarizeWithFallbackChain
3336
3561
  });
3337
3562
  import { spawn } from "child_process";
3338
3563
  import { createInterface } from "readline";
3339
- function resolveSummarizerAdapter(chatId) {
3340
- const config2 = getSummarizer(chatId);
3341
- if (config2.backend === "off") return null;
3342
- let adapter;
3343
- if (config2.backend) {
3344
- adapter = getAdapter(config2.backend);
3345
- } else {
3346
- try {
3347
- adapter = getAdapterForChat(chatId);
3348
- } catch {
3349
- try {
3350
- adapter = getAdapter("claude");
3351
- } catch {
3352
- return null;
3353
- }
3354
- }
3355
- }
3356
- const model2 = config2.model ?? adapter.summarizerModel;
3357
- return { adapter, model: model2 };
3358
- }
3359
3564
  function buildTranscript(entries) {
3360
3565
  const lines = [];
3361
3566
  let totalLen = 0;
@@ -3373,19 +3578,7 @@ function buildTranscript(entries) {
3373
3578
  }
3374
3579
  return lines.join("\n\n");
3375
3580
  }
3376
- async function summarizeSession(chatId) {
3377
- const pairCount = getMessagePairCount(chatId);
3378
- if (pairCount < MIN_PAIRS) {
3379
- clearLog(chatId);
3380
- return false;
3381
- }
3382
- const resolved = resolveSummarizerAdapter(chatId);
3383
- if (!resolved) {
3384
- clearLog(chatId);
3385
- return false;
3386
- }
3387
- const { adapter, model: model2 } = resolved;
3388
- const entries = getLog(chatId);
3581
+ async function attemptSummarize(chatId, adapter, model2, entries) {
3389
3582
  const transcript = buildTranscript(entries);
3390
3583
  const prompt = `${SUMMARIZE_PROMPT}
3391
3584
 
@@ -3404,7 +3597,6 @@ ${transcript}`;
3404
3597
  let inputTokens = 0;
3405
3598
  let outputTokens = 0;
3406
3599
  let cacheReadTokens = 0;
3407
- const SUMMARIZE_TIMEOUT_MS = 6e4;
3408
3600
  await new Promise((resolve) => {
3409
3601
  const proc = spawn(config2.executable, config2.args, {
3410
3602
  env,
@@ -3414,7 +3606,7 @@ ${transcript}`;
3414
3606
  proc.stderr?.resume();
3415
3607
  const rl2 = createInterface({ input: proc.stdout });
3416
3608
  const timeout = setTimeout(() => {
3417
- 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}`);
3418
3610
  rl2.close();
3419
3611
  proc.kill("SIGTERM");
3420
3612
  setTimeout(() => proc.kill("SIGKILL"), 2e3);
@@ -3429,9 +3621,7 @@ ${transcript}`;
3429
3621
  }
3430
3622
  const events = adapter.parseLine(msg);
3431
3623
  for (const ev of events) {
3432
- if (ev.type === "text" && ev.text) {
3433
- accumulatedText += ev.text;
3434
- }
3624
+ if (ev.type === "text" && ev.text) accumulatedText += ev.text;
3435
3625
  if (ev.type === "usage" && ev.usage) {
3436
3626
  inputTokens += ev.usage.input;
3437
3627
  outputTokens += ev.usage.output;
@@ -3463,11 +3653,9 @@ ${transcript}`;
3463
3653
  if (inputTokens + outputTokens > 0) {
3464
3654
  addUsage(chatId, inputTokens, outputTokens, cacheReadTokens, model2);
3465
3655
  }
3656
+ if (!resultText) resultText = accumulatedText;
3466
3657
  if (!resultText) {
3467
- resultText = accumulatedText;
3468
- }
3469
- if (!resultText) {
3470
- 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}`);
3471
3659
  return false;
3472
3660
  }
3473
3661
  const summaryMatch = resultText.match(/SUMMARY:\s*(.+?)(?=\nKEY_DETAILS:|\nTOPICS:|$)/s);
@@ -3475,20 +3663,72 @@ ${transcript}`;
3475
3663
  const topicsMatch = resultText.match(/TOPICS:\s*(.+)/);
3476
3664
  let summary = summaryMatch?.[1]?.trim() ?? resultText.trim();
3477
3665
  const keyDetails = keyDetailsMatch?.[1]?.trim();
3478
- if (keyDetails) {
3479
- summary += `
3666
+ if (keyDetails) summary += `
3480
3667
  Key details: ${keyDetails}`;
3481
- }
3482
3668
  const topics = topicsMatch?.[1]?.trim() ?? "";
3483
- saveSessionSummaryWithEmbedding(chatId, summary, topics, pairCount);
3484
- log(`[summarize] Saved session summary for chat ${chatId} via ${adapter.id}:${model2} (${pairCount} pairs, topics: ${topics})`);
3485
- clearLog(chatId);
3669
+ saveSessionSummaryWithEmbedding(chatId, summary, topics, Math.floor(entries.length / 2));
3670
+ log(`[summarize] Saved summary via ${adapter.id}:${model2} for chat ${chatId}`);
3486
3671
  return true;
3487
3672
  } catch (err) {
3488
- warn(`[summarize] Failed for chat ${chatId}: ${errorMessage(err)} \u2014 keeping log for retry`);
3673
+ warn(`[summarize] ${adapter.id}:${model2} failed for ${chatId}: ${errorMessage(err)}`);
3489
3674
  return false;
3490
3675
  }
3491
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
+ }
3492
3732
  async function summarizeAllPending() {
3493
3733
  const chatIds = getLoggedChatIds();
3494
3734
  if (chatIds.length === 0) return;
@@ -3497,7 +3737,7 @@ async function summarizeAllPending() {
3497
3737
  await summarizeSession(chatId);
3498
3738
  }
3499
3739
  }
3500
- 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;
3501
3741
  var init_summarize = __esm({
3502
3742
  "src/memory/summarize.ts"() {
3503
3743
  "use strict";
@@ -3509,18 +3749,20 @@ var init_summarize = __esm({
3509
3749
  USER_MSG_LIMIT = 4e3;
3510
3750
  AGENT_MSG_LIMIT = 6e3;
3511
3751
  TRANSCRIPT_CAP = 1e5;
3752
+ SUMMARIZE_TIMEOUT_MS = 6e4;
3512
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.
3513
3754
 
3514
3755
  Instructions:
3515
3756
  1. SUMMARY: Write a detailed summary (5-15 sentences depending on session complexity). Capture:
3516
3757
  - What the user asked for and why
3517
3758
  - What was done, decided, or accomplished
3518
- - Specific details that would be hard to rediscover: names, dates, values, locations, references, sources, outcomes
3519
- - 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
3520
3761
  - Any preferences, corrections, or instructions the user expressed
3521
3762
 
3522
3763
  2. KEY_DETAILS: List the 3-8 most important specific facts as bullet points. These should be concrete and retrievable \u2014 not vague.
3523
3764
  Good: "User's quarterly report deadline is March 15"
3765
+ Good: "Flight booked to NYC on April 3, confirmation #AB1234"
3524
3766
  Bad: "Discussed deadlines"
3525
3767
 
3526
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".
@@ -3531,6 +3773,14 @@ KEY_DETAILS:
3531
3773
  - <detail 1>
3532
3774
  - <detail 2>
3533
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.`;
3534
3784
  }
3535
3785
  });
3536
3786
 
@@ -4448,7 +4698,7 @@ function startNextQueued(chatId) {
4448
4698
  description: agent.description ?? void 0,
4449
4699
  model: agent.model ?? void 0,
4450
4700
  skills: JSON.parse(agent.skills),
4451
- permMode: agent.permMode === "inherit" ? void 0 : agent.permMode,
4701
+ permMode: agent.permMode === "inherit" ? getMode(chatId) : agent.permMode,
4452
4702
  role: agent.role ?? "worker",
4453
4703
  persona: agent.persona ?? void 0,
4454
4704
  maxRuntimeMs: agent.maxRuntimeMs,
@@ -4954,6 +5204,10 @@ function startDashboard() {
4954
5204
  if (body.cwd && !existsSync8(body.cwd)) {
4955
5205
  return jsonResponse(res, { error: `Directory not found: ${body.cwd}` }, 400);
4956
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
+ }
4957
5211
  const result = await spawnSubAgent(body.chatId, body);
4958
5212
  return jsonResponse(res, result);
4959
5213
  } catch (err) {
@@ -5206,7 +5460,10 @@ function startDashboard() {
5206
5460
  const { getMode: getMode2, getCwd: getCwd2, getModel: getModel2, addUsage: addUsage2, getBackend: getBackend2 } = await Promise.resolve().then(() => (init_store4(), store_exports3));
5207
5461
  const { getAdapterForChat: getAdapterForChat2 } = await Promise.resolve().then(() => (init_backends(), backends_exports));
5208
5462
  const chatId = body.chatId;
5209
- 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;
5210
5467
  const cwd = body.cwd ?? getCwd2(chatId);
5211
5468
  const model2 = body.model ?? getModel2(chatId) ?? (() => {
5212
5469
  try {
@@ -5858,11 +6115,11 @@ function askAgent(chatId, userMessage, opts) {
5858
6115
  async function askAgentImpl(chatId, userMessage, opts) {
5859
6116
  const { cwd, onStream, model: model2, backend: backend2, permMode, onToolAction, bootstrapTier, timeoutMs } = opts ?? {};
5860
6117
  const adapter = backend2 ? getAdapter(backend2) : getAdapterForChat(chatId);
5861
- const mode = permMode ?? "yolo";
6118
+ const mode = permMode ?? getMode(chatId);
5862
6119
  const thinkingLevel = getThinkingLevel(chatId);
5863
6120
  const resolvedCwd = cwd ?? WORKSPACE_PATH;
5864
6121
  const tier = bootstrapTier ?? "full";
5865
- const fullPrompt = await assembleBootstrapPrompt(userMessage, tier, chatId);
6122
+ const fullPrompt = await assembleBootstrapPrompt(userMessage, tier, chatId, mode);
5866
6123
  const existingSessionId = getSessionId(chatId);
5867
6124
  const allowedTools = getEnabledTools(chatId);
5868
6125
  const mcpConfigPath = getMcpConfigPath(chatId);
@@ -5923,12 +6180,14 @@ async function askAgentImpl(chatId, userMessage, opts) {
5923
6180
  clearSession(chatId);
5924
6181
  }
5925
6182
  if (result.resultText) {
5926
- appendToLog(chatId, userMessage, result.resultText);
6183
+ appendToLog(chatId, userMessage, result.resultText, adapter.id, model2 ?? null, result.sessionId ?? null);
5927
6184
  const AUTO_SUMMARIZE_THRESHOLD = 30;
5928
6185
  const pairCount = getMessagePairCount(chatId);
5929
6186
  if (pairCount >= AUTO_SUMMARIZE_THRESHOLD) {
5930
6187
  log(`[agent] Auto-summarizing chat ${chatId} after ${pairCount} turns`);
5931
- summarizeSession(chatId).catch((err) => {
6188
+ summarizeWithFallbackChain(chatId).then((saved) => {
6189
+ if (saved) opts?.onCompaction?.(chatId);
6190
+ }).catch((err) => {
5932
6191
  warn(`[agent] Auto-summarize failed for chat ${chatId}: ${err}`);
5933
6192
  });
5934
6193
  }
@@ -6488,7 +6747,8 @@ async function runWithRetry(job, model2, runId, t0) {
6488
6747
  model: currentModel,
6489
6748
  backend: currentBackend,
6490
6749
  bootstrapTier: "slim",
6491
- timeoutMs
6750
+ timeoutMs,
6751
+ permMode: job.sessionType === "main" ? getMode(job.chatId) : "yolo"
6492
6752
  });
6493
6753
  if (isFallback) {
6494
6754
  response.text = `[Fallback: ran on ${currentBackend}:${currentModel}]
@@ -6616,7 +6876,7 @@ function wrapBackendAdapter(adapter) {
6616
6876
  const config2 = adapter.buildSpawnConfig({
6617
6877
  prompt: opts.prompt,
6618
6878
  sessionId: opts.sessionId,
6619
- permMode: opts.permMode ?? "yolo",
6879
+ permMode: opts.permMode ?? "plan",
6620
6880
  allowedTools: opts.allowedTools ?? [],
6621
6881
  cwd: opts.cwd
6622
6882
  });
@@ -8063,14 +8323,6 @@ var init_heartbeat = __esm({
8063
8323
  });
8064
8324
 
8065
8325
  // src/format-time.ts
8066
- function formatLocalDate(utcDatetime) {
8067
- const d = parseUtcDatetime(utcDatetime);
8068
- if (!d) return utcDatetime.split("T")[0] ?? utcDatetime.split(" ")[0];
8069
- const year = d.getFullYear();
8070
- const month = String(d.getMonth() + 1).padStart(2, "0");
8071
- const day = String(d.getDate()).padStart(2, "0");
8072
- return `${year}-${month}-${day}`;
8073
- }
8074
8326
  function formatLocalDateTime(utcDatetime) {
8075
8327
  const d = parseUtcDatetime(utcDatetime);
8076
8328
  if (!d) return utcDatetime;
@@ -9253,7 +9505,7 @@ async function handleCommand(msg, channel) {
9253
9505
  case "help":
9254
9506
  await channel.sendText(
9255
9507
  chatId,
9256
- "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/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",
9257
9509
  "plain"
9258
9510
  );
9259
9511
  break;
@@ -9313,7 +9565,7 @@ async function handleCommand(msg, channel) {
9313
9565
  toolButtons.push(row);
9314
9566
  }
9315
9567
  toolButtons.push([{ label: "Reset to defaults (all on)", data: "tool:reset" }]);
9316
- 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)." : "";
9317
9569
  await channel.sendKeyboard(
9318
9570
  chatId,
9319
9571
  `Configure allowed tools (mode: ${currentMode})${modeNote}
@@ -9349,7 +9601,15 @@ Tap to toggle:`,
9349
9601
  } else {
9350
9602
  const pairs = getMessagePairCount(chatId);
9351
9603
  if (pairs < 2) {
9352
- 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
+ }
9353
9613
  break;
9354
9614
  }
9355
9615
  await channel.sendText(chatId, `Summarizing current session (${pairs} exchanges)...`, "plain");
@@ -9882,20 +10142,38 @@ ${lines.join("\n")}`, "plain");
9882
10142
  break;
9883
10143
  }
9884
10144
  case "history": {
9885
- const historyLimit = commandArgs ? parseInt(commandArgs, 10) || 5 : 5;
9886
- const summaries = listSessionSummaries(historyLimit, chatId);
9887
- if (summaries.length === 0) {
9888
- await channel.sendText(chatId, "No session history yet. History is saved automatically when you use /newchat.", "plain");
9889
- 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");
9890
10176
  }
9891
- const lines = summaries.map((s) => {
9892
- const date = formatLocalDate(s.created_at);
9893
- const shortSummary = s.summary.length > 200 ? s.summary.slice(0, 200) + "\u2026" : s.summary;
9894
- return `${date} (${s.message_count} msgs)
9895
- ${shortSummary}
9896
- Topics: ${s.topics}`;
9897
- });
9898
- await channel.sendText(chatId, lines.join("\n\n"), "plain");
9899
10177
  break;
9900
10178
  }
9901
10179
  case "skills": {
@@ -10516,7 +10794,7 @@ async function handleText(msg, channel) {
10516
10794
  await channel.sendText(chatId, limitMsg, "plain");
10517
10795
  return;
10518
10796
  }
10519
- if (isChatBusy(chatId)) {
10797
+ if (isChatBusy(chatId) && !bypassBusyCheck.delete(chatId)) {
10520
10798
  if (typeof channel.sendKeyboard === "function") {
10521
10799
  pendingInterrupts.set(chatId, { msg, channel });
10522
10800
  await channel.sendKeyboard(chatId, "\u23F3 Agent is working on a request\u2026", [
@@ -10544,8 +10822,29 @@ async function handleText(msg, channel) {
10544
10822
  const tMode = getMode(chatId);
10545
10823
  const tVerbose = getVerboseLevel(chatId);
10546
10824
  const tToolCb = tVerbose !== "off" ? makeToolActionCallback(chatId, channel, tVerbose) : void 0;
10547
- 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
+ });
10548
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
+ }
10549
10848
  await sendResponse(chatId, channel, response.text, msg.messageId);
10550
10849
  } catch (err) {
10551
10850
  error("[router] Error:", err);
@@ -10731,7 +11030,28 @@ async function sendResponse(chatId, channel, text, messageId) {
10731
11030
  ]);
10732
11031
  }
10733
11032
  }
10734
- const afterFiles = 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);
10735
11055
  const cleanText = await processImageGenerations(chatId, channel, afterFiles);
10736
11056
  if (!cleanText) return;
10737
11057
  if (isVoiceEnabled(chatId)) {
@@ -10757,16 +11077,17 @@ async function sendBackendSwitchConfirmation(chatId, target, channel) {
10757
11077
  await channel.sendText(chatId, `Already using ${targetAdapter.displayName}.`, "plain");
10758
11078
  return;
10759
11079
  }
10760
- const currentLabel = current ? getAdapter(current).displayName : "current backend";
10761
11080
  if (typeof channel.sendKeyboard === "function") {
10762
11081
  await channel.sendKeyboard(
10763
11082
  chatId,
10764
- `\u26A0\uFE0F Switching to ${targetAdapter.displayName} will summarize and reset your current session.
10765
-
10766
- 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.`,
10767
11085
  [
10768
- [{ label: `Stay on ${currentLabel}`, data: "backend_cancel" }],
10769
- [{ 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}` }]
10770
11091
  ]
10771
11092
  );
10772
11093
  } else {
@@ -10774,21 +11095,21 @@ What would you like to do?`,
10774
11095
  }
10775
11096
  }
10776
11097
  async function doBackendSwitch(chatId, backendId, channel) {
10777
- summarizeSession(chatId).catch(() => {
10778
- });
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
+ }
10779
11107
  clearSession(chatId);
10780
11108
  clearModel(chatId);
10781
11109
  clearThinkingLevel(chatId);
10782
11110
  setBackend(chatId, backendId);
10783
- const adapter = getAdapter(backendId);
10784
- logActivity(getDb(), { chatId, source: "telegram", eventType: "config_changed", summary: `Backend switched to ${adapter.displayName}`, detail: { field: "backend", value: backendId } });
10785
- await channel.sendText(
10786
- chatId,
10787
- `Backend switched to ${adapter.displayName}.
10788
- Default model: ${adapter.defaultModel}
10789
- Session reset. Ready!`,
10790
- "plain"
10791
- );
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");
10792
11113
  }
10793
11114
  async function handleCallback(chatId, data, channel) {
10794
11115
  if (data.startsWith("backend:")) {
@@ -10801,14 +11122,17 @@ async function handleCallback(chatId, data, channel) {
10801
11122
  return;
10802
11123
  }
10803
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);
10804
11130
  } else if (data.startsWith("backend_confirm:")) {
10805
11131
  const chosen = data.slice(16);
10806
11132
  if (!getAllBackendIds().includes(chosen)) return;
10807
11133
  await doBackendSwitch(chatId, chosen, channel);
10808
- } else if (data === "backend_cancel") {
10809
- const current = getBackend(chatId);
10810
- const label2 = current ? getAdapter(current).displayName : "current backend";
10811
- 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");
10812
11136
  } else if (data.startsWith("model:")) {
10813
11137
  const chosen = data.slice(6);
10814
11138
  let adapter;
@@ -10863,7 +11187,8 @@ Select thinking/effort level:`,
10863
11187
  await channel.sendText(chatId, `Summarizer pinned to ${bk}:${mdl ?? "default"}.`, "plain");
10864
11188
  }
10865
11189
  } else if (data.startsWith("perms:")) {
10866
- const chosen = data.slice(6);
11190
+ let chosen = data.slice(6);
11191
+ if (chosen === "readonly") chosen = "plan";
10867
11192
  if (!PERM_MODES[chosen]) return;
10868
11193
  const previous = getMode(chatId);
10869
11194
  if (chosen === previous) {
@@ -10878,6 +11203,20 @@ Select thinking/effort level:`,
10878
11203
  ${PERM_MODES[chosen]}`,
10879
11204
  "plain"
10880
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");
10881
11220
  } else if (data.startsWith("verbose:")) {
10882
11221
  const chosen = data.slice(8);
10883
11222
  if (!VERBOSE_LEVELS[chosen]) return;
@@ -10965,6 +11304,7 @@ ${PERM_MODES[chosen]}`,
10965
11304
  await handleMessage(pending.msg, pending.channel);
10966
11305
  } else if (action === "queue" && pending) {
10967
11306
  pendingInterrupts.delete(targetChatId);
11307
+ bypassBusyCheck.add(targetChatId);
10968
11308
  await channel.sendText(chatId, "\u{1F4E5} Message queued \u2014 will process after current task.", "plain");
10969
11309
  handleMessage(pending.msg, pending.channel).catch(() => {
10970
11310
  });
@@ -10981,8 +11321,9 @@ ${PERM_MODES[chosen]}`,
10981
11321
  await channel.sendText(chatId, "OK \u2014 you can switch manually with /backend when ready.", "plain");
10982
11322
  } else if (pendingMsg) {
10983
11323
  pendingFallbackMessages.delete(targetChatId);
10984
- const { summarizeSession: summarizeSession2 } = await Promise.resolve().then(() => (init_summarize(), summarize_exports));
10985
- await summarizeSession2(targetChatId);
11324
+ await summarizeWithFallbackChain(targetChatId, targetBackend);
11325
+ const bridge = buildContextBridge(targetChatId);
11326
+ if (bridge) setPendingContextBridge(targetChatId, bridge);
10986
11327
  clearSession(targetChatId);
10987
11328
  setBackend(targetChatId, targetBackend);
10988
11329
  const adapter = getAdapter(targetBackend);
@@ -11202,7 +11543,7 @@ Use /skills <page> to navigate (e.g. /skills 2)` : "";
11202
11543
  const header2 = totalPages > 1 ? `${skills2.length} skills (page ${safePage}/${totalPages}). Select one to invoke:` : `${skills2.length} skills available. Select one to invoke:`;
11203
11544
  await channel.sendKeyboard(chatId, header2, buttons);
11204
11545
  }
11205
- var PERM_MODES, VERBOSE_LEVELS, pendingInterrupts, pendingFallbackMessages, 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;
11206
11547
  var init_router = __esm({
11207
11548
  "src/router.ts"() {
11208
11549
  "use strict";
@@ -11218,6 +11559,8 @@ var init_router = __esm({
11218
11559
  init_stt();
11219
11560
  init_store4();
11220
11561
  init_summarize();
11562
+ init_inject();
11563
+ init_store4();
11221
11564
  init_session_log();
11222
11565
  init_backends();
11223
11566
  init_cron();
@@ -11235,8 +11578,7 @@ var init_router = __esm({
11235
11578
  PERM_MODES = {
11236
11579
  yolo: "YOLO \u2014 all tools, full autopilot",
11237
11580
  safe: "Safe \u2014 only my allowed tools",
11238
- readonly: "Read-only \u2014 Read, Glob, Grep only",
11239
- plan: "Plan \u2014 plan only, no execution"
11581
+ plan: "Plan \u2014 read and analyze only"
11240
11582
  };
11241
11583
  VERBOSE_LEVELS = {
11242
11584
  off: "Off \u2014 no tool visibility",
@@ -11244,6 +11586,7 @@ var init_router = __esm({
11244
11586
  verbose: "Verbose \u2014 full details"
11245
11587
  };
11246
11588
  pendingInterrupts = /* @__PURE__ */ new Map();
11589
+ bypassBusyCheck = /* @__PURE__ */ new Set();
11247
11590
  pendingFallbackMessages = /* @__PURE__ */ new Map();
11248
11591
  CLI_INSTALL_HINTS = {
11249
11592
  claude: "Install: npm install -g @anthropic-ai/claude-code",
@@ -11980,7 +12323,7 @@ var index_exports = {};
11980
12323
  __export(index_exports, {
11981
12324
  main: () => main
11982
12325
  });
11983
- import { mkdirSync as mkdirSync7, existsSync as existsSync16, renameSync, statSync as statSync2 } from "fs";
12326
+ import { mkdirSync as mkdirSync7, existsSync as existsSync16, renameSync, statSync as statSync2, readFileSync as readFileSync10 } from "fs";
11984
12327
  import { join as join17 } from "path";
11985
12328
  import dotenv from "dotenv";
11986
12329
  function migrateLayout() {
@@ -12020,14 +12363,21 @@ function rotateLogs() {
12020
12363
  }
12021
12364
  async function main() {
12022
12365
  rotateLogs();
12023
- 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}`);
12024
12373
  initDatabase();
12374
+ pruneMessageLog(30, 2e3);
12025
12375
  bootstrapBuiltinMcps(getDb());
12026
- const SUMMARIZE_TIMEOUT_MS = 3e4;
12376
+ const SUMMARIZE_TIMEOUT_MS2 = 3e4;
12027
12377
  try {
12028
12378
  let timer;
12029
12379
  const timeoutPromise = new Promise((_, reject) => {
12030
- timer = setTimeout(() => reject(new Error("timeout")), SUMMARIZE_TIMEOUT_MS);
12380
+ timer = setTimeout(() => reject(new Error("timeout")), SUMMARIZE_TIMEOUT_MS2);
12031
12381
  });
12032
12382
  try {
12033
12383
  await Promise.race([
@@ -12553,13 +12903,13 @@ var init_daemon = __esm({
12553
12903
  });
12554
12904
 
12555
12905
  // src/cli/resolve-chat.ts
12556
- import { readFileSync as readFileSync11 } from "fs";
12906
+ import { readFileSync as readFileSync12 } from "fs";
12557
12907
  function resolveChatId(globalOpts) {
12558
12908
  const explicit = globalOpts.chat;
12559
12909
  if (explicit) return explicit;
12560
12910
  if (_cachedDefault) return _cachedDefault;
12561
12911
  try {
12562
- const content = readFileSync11(ENV_PATH, "utf-8");
12912
+ const content = readFileSync12(ENV_PATH, "utf-8");
12563
12913
  const match = content.match(/^ALLOWED_CHAT_ID=(.+)$/m);
12564
12914
  if (match) {
12565
12915
  _cachedDefault = match[1].split(",")[0].trim();
@@ -12585,12 +12935,12 @@ __export(api_client_exports, {
12585
12935
  apiPost: () => apiPost,
12586
12936
  isDaemonRunning: () => isDaemonRunning
12587
12937
  });
12588
- import { readFileSync as readFileSync12, existsSync as existsSync18 } from "fs";
12938
+ import { readFileSync as readFileSync13, existsSync as existsSync18 } from "fs";
12589
12939
  import { request as httpRequest } from "http";
12590
12940
  function getToken() {
12591
12941
  if (process.env.CC_CLAW_API_TOKEN) return process.env.CC_CLAW_API_TOKEN;
12592
12942
  try {
12593
- if (existsSync18(TOKEN_PATH)) return readFileSync12(TOKEN_PATH, "utf-8").trim();
12943
+ if (existsSync18(TOKEN_PATH)) return readFileSync13(TOKEN_PATH, "utf-8").trim();
12594
12944
  } catch {
12595
12945
  }
12596
12946
  return null;
@@ -12915,8 +13265,8 @@ async function doctorCommand(globalOpts, localOpts) {
12915
13265
  }
12916
13266
  if (existsSync20(ERROR_LOG_PATH)) {
12917
13267
  try {
12918
- const { readFileSync: readFileSync17 } = await import("fs");
12919
- const logContent = readFileSync17(ERROR_LOG_PATH, "utf-8");
13268
+ const { readFileSync: readFileSync18 } = await import("fs");
13269
+ const logContent = readFileSync18(ERROR_LOG_PATH, "utf-8");
12920
13270
  const recentLines = logContent.split("\n").filter(Boolean).slice(-100);
12921
13271
  const last24h = Date.now() - 864e5;
12922
13272
  const recentErrors = recentLines.filter((line) => {
@@ -13032,7 +13382,7 @@ var logs_exports = {};
13032
13382
  __export(logs_exports, {
13033
13383
  logsCommand: () => logsCommand
13034
13384
  });
13035
- import { existsSync as existsSync21, 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";
13036
13386
  async function logsCommand(opts) {
13037
13387
  const logFile = opts.error ? ERROR_LOG_PATH : LOG_PATH;
13038
13388
  if (!existsSync21(logFile)) {
@@ -13040,7 +13390,7 @@ async function logsCommand(opts) {
13040
13390
  process.exit(1);
13041
13391
  }
13042
13392
  const maxLines = parseInt(opts.lines ?? "100", 10);
13043
- const content = readFileSync13(logFile, "utf-8");
13393
+ const content = readFileSync14(logFile, "utf-8");
13044
13394
  const allLines = content.split("\n");
13045
13395
  const tailLines = allLines.slice(-maxLines);
13046
13396
  console.log(muted(` \u2500\u2500 ${logFile} (last ${tailLines.length} lines) \u2500\u2500`));
@@ -13050,7 +13400,7 @@ async function logsCommand(opts) {
13050
13400
  let lastSize = Buffer.byteLength(content, "utf-8");
13051
13401
  watchFile2(logFile, { interval: 500 }, () => {
13052
13402
  try {
13053
- const newContent = readFileSync13(logFile, "utf-8");
13403
+ const newContent = readFileSync14(logFile, "utf-8");
13054
13404
  const newSize = Buffer.byteLength(newContent, "utf-8");
13055
13405
  if (newSize > lastSize) {
13056
13406
  const newPart = newContent.slice(content.length);
@@ -14055,7 +14405,7 @@ __export(config_exports, {
14055
14405
  configList: () => configList,
14056
14406
  configSet: () => configSet
14057
14407
  });
14058
- import { existsSync as existsSync29, readFileSync as readFileSync14 } from "fs";
14408
+ import { existsSync as existsSync29, readFileSync as readFileSync15 } from "fs";
14059
14409
  async function configList(globalOpts) {
14060
14410
  if (!existsSync29(DB_PATH)) {
14061
14411
  outputError("DB_NOT_FOUND", "Database not found.");
@@ -14141,7 +14491,7 @@ async function configEnv(_globalOpts) {
14141
14491
  outputError("ENV_NOT_FOUND", `No .env file at ${ENV_PATH}. Run cc-claw setup.`);
14142
14492
  process.exit(1);
14143
14493
  }
14144
- const content = readFileSync14(ENV_PATH, "utf-8");
14494
+ const content = readFileSync15(ENV_PATH, "utf-8");
14145
14495
  const entries = {};
14146
14496
  const secretPatterns = /TOKEN|KEY|SECRET|PASSWORD|CREDENTIALS/i;
14147
14497
  for (const line of content.split("\n")) {
@@ -14271,7 +14621,8 @@ async function permissionsGet(globalOpts) {
14271
14621
  output({ mode }, (d) => d.mode);
14272
14622
  }
14273
14623
  async function permissionsSet(globalOpts, mode) {
14274
- const valid = ["yolo", "safe", "readonly", "plan"];
14624
+ if (mode === "readonly") mode = "plan";
14625
+ const valid = ["yolo", "safe", "plan"];
14275
14626
  if (!valid.includes(mode)) {
14276
14627
  outputError("INVALID_MODE", `Invalid mode "${mode}". Valid: ${valid.join(", ")}`);
14277
14628
  process.exit(1);
@@ -14797,11 +15148,11 @@ __export(chat_exports, {
14797
15148
  chatSend: () => chatSend
14798
15149
  });
14799
15150
  import { request as httpRequest2 } from "http";
14800
- import { readFileSync as readFileSync15, existsSync as existsSync37 } from "fs";
15151
+ import { readFileSync as readFileSync16, existsSync as existsSync37 } from "fs";
14801
15152
  function getToken2() {
14802
15153
  if (process.env.CC_CLAW_API_TOKEN) return process.env.CC_CLAW_API_TOKEN;
14803
15154
  try {
14804
- if (existsSync37(TOKEN_PATH2)) return readFileSync15(TOKEN_PATH2, "utf-8").trim();
15155
+ if (existsSync37(TOKEN_PATH2)) return readFileSync16(TOKEN_PATH2, "utf-8").trim();
14805
15156
  } catch {
14806
15157
  }
14807
15158
  return null;
@@ -15227,7 +15578,7 @@ var init_completion = __esm({
15227
15578
 
15228
15579
  // src/setup.ts
15229
15580
  var setup_exports = {};
15230
- import { existsSync as existsSync38, writeFileSync as writeFileSync8, readFileSync as readFileSync16, copyFileSync as copyFileSync2, mkdirSync as mkdirSync10, statSync as statSync6 } from "fs";
15581
+ import { existsSync as existsSync38, writeFileSync as writeFileSync8, readFileSync as readFileSync17, copyFileSync as copyFileSync2, mkdirSync as mkdirSync10, statSync as statSync6 } from "fs";
15231
15582
  import { execFileSync as execFileSync4 } from "child_process";
15232
15583
  import { createInterface as createInterface5 } from "readline";
15233
15584
  import { join as join19 } from "path";
@@ -15311,7 +15662,7 @@ async function setup() {
15311
15662
  if (envSource) {
15312
15663
  console.log(yellow(` Found existing config at ${envSource} \u2014 your values will be preserved`));
15313
15664
  console.log(yellow(" unless you enter new ones. Just press Enter to keep existing values.\n"));
15314
- const existing = readFileSync16(envSource, "utf-8");
15665
+ const existing = readFileSync17(envSource, "utf-8");
15315
15666
  for (const line of existing.split("\n")) {
15316
15667
  const match = line.match(/^([^#=]+)=(.*)$/);
15317
15668
  if (match) env[match[1].trim()] = match[2].trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-claw",
3
- "version": "0.3.11",
3
+ "version": "0.4.0",
4
4
  "description": "CC-Claw: Personal AI assistant on Telegram — multi-backend (Claude, Gemini, Codex), sub-agent orchestration, MCP management",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",