cc-claw 0.8.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +3 -2
  2. package/dist/cli.js +2009 -505
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -38,13 +38,14 @@ __export(paths_exports, {
38
38
  IDENTITY_PATH: () => IDENTITY_PATH,
39
39
  LOGS_PATH: () => LOGS_PATH,
40
40
  LOG_PATH: () => LOG_PATH,
41
+ MEDIA_PATH: () => MEDIA_PATH,
41
42
  RUNNERS_PATH: () => RUNNERS_PATH,
42
43
  SKILLS_PATH: () => SKILLS_PATH,
43
44
  WORKSPACE_PATH: () => WORKSPACE_PATH
44
45
  });
45
46
  import { homedir } from "os";
46
47
  import { join } from "path";
47
- var CC_CLAW_HOME, ENV_PATH, DATA_PATH, DB_PATH, LOGS_PATH, LOG_PATH, ERROR_LOG_PATH, IDENTITY_PATH, WORKSPACE_PATH, SKILLS_PATH, RUNNERS_PATH, AGENTS_PATH;
48
+ var CC_CLAW_HOME, ENV_PATH, DATA_PATH, DB_PATH, LOGS_PATH, LOG_PATH, ERROR_LOG_PATH, IDENTITY_PATH, WORKSPACE_PATH, SKILLS_PATH, RUNNERS_PATH, AGENTS_PATH, MEDIA_PATH;
48
49
  var init_paths = __esm({
49
50
  "src/paths.ts"() {
50
51
  "use strict";
@@ -60,6 +61,7 @@ var init_paths = __esm({
60
61
  SKILLS_PATH = join(WORKSPACE_PATH, "skills");
61
62
  RUNNERS_PATH = join(CC_CLAW_HOME, "runners");
62
63
  AGENTS_PATH = join(CC_CLAW_HOME, "agents");
64
+ MEDIA_PATH = join(CC_CLAW_HOME, "media");
63
65
  }
64
66
  });
65
67
 
@@ -70,7 +72,7 @@ var VERSION;
70
72
  var init_version = __esm({
71
73
  "src/version.ts"() {
72
74
  "use strict";
73
- VERSION = true ? "0.8.0" : (() => {
75
+ VERSION = true ? "0.9.1" : (() => {
74
76
  try {
75
77
  return JSON.parse(readFileSync(join2(process.cwd(), "package.json"), "utf-8")).version ?? "unknown";
76
78
  } catch {
@@ -683,6 +685,7 @@ __export(store_exports4, {
683
685
  getConflictingInsights: () => getConflictingInsights,
684
686
  getGrowthMetrics: () => getGrowthMetrics,
685
687
  getInsightById: () => getInsightById,
688
+ getLastAnalysisTime: () => getLastAnalysisTime,
686
689
  getPendingInsightCount: () => getPendingInsightCount,
687
690
  getPendingInsights: () => getPendingInsights,
688
691
  getRecommendations: () => getRecommendations,
@@ -691,6 +694,7 @@ __export(store_exports4, {
691
694
  getRejectedInsights: () => getRejectedInsights,
692
695
  getSignalCountForChat: () => getSignalCountForChat,
693
696
  getSignalsForPeriod: () => getSignalsForPeriod,
697
+ getUnprocessedSignalCount: () => getUnprocessedSignalCount,
694
698
  getUnprocessedSignals: () => getUnprocessedSignals,
695
699
  initReflectionTables: () => initReflectionTables,
696
700
  logInsight: () => logInsight,
@@ -713,8 +717,8 @@ function cleanupBackupFiles(ccClawHome) {
713
717
  if (!file.includes(".bak.")) continue;
714
718
  const fullPath = join3(identityDir, file);
715
719
  try {
716
- const stat = statSync(fullPath);
717
- if (stat.mtimeMs < thirtyDaysAgo) unlinkSync(fullPath);
720
+ const stat2 = statSync(fullPath);
721
+ if (stat2.mtimeMs < thirtyDaysAgo) unlinkSync(fullPath);
718
722
  } catch {
719
723
  }
720
724
  }
@@ -841,6 +845,20 @@ function getSignalsForPeriod(db3, chatId, period) {
841
845
  ORDER BY created_at ASC
842
846
  `).all(chatId, period);
843
847
  }
848
+ function getUnprocessedSignalCount(db3, chatId) {
849
+ const row = db3.prepare(`
850
+ SELECT COUNT(*) as count FROM feedback_signals
851
+ WHERE chatId = ? AND processedAt IS NULL
852
+ `).get(chatId);
853
+ return row.count;
854
+ }
855
+ function getLastAnalysisTime(db3, chatId) {
856
+ const row = db3.prepare(`
857
+ SELECT MAX(processedAt) as lastTime FROM feedback_signals
858
+ WHERE chatId = ? AND processedAt IS NOT NULL
859
+ `).get(chatId);
860
+ return row.lastTime;
861
+ }
844
862
  function getReflectionStatus(db3, chatId) {
845
863
  const row = db3.prepare("SELECT status FROM chat_reflection WHERE chatId = ?").get(chatId);
846
864
  return row?.status ?? "frozen";
@@ -1280,6 +1298,8 @@ __export(store_exports5, {
1280
1298
  clearUsage: () => clearUsage,
1281
1299
  completeJobRun: () => completeJobRun,
1282
1300
  deleteBookmark: () => deleteBookmark,
1301
+ deleteMemoryById: () => deleteMemoryById,
1302
+ deleteSessionSummary: () => deleteSessionSummary,
1283
1303
  determineEscalationTarget: () => determineEscalationTarget,
1284
1304
  findBookmarksByPrefix: () => findBookmarksByPrefix,
1285
1305
  forgetMemory: () => forgetMemory,
@@ -1310,6 +1330,7 @@ __export(store_exports5, {
1310
1330
  getJobById: () => getJobById,
1311
1331
  getJobRuns: () => getJobRuns,
1312
1332
  getMemoriesWithoutEmbeddings: () => getMemoriesWithoutEmbeddings,
1333
+ getMemoryById: () => getMemoryById,
1313
1334
  getMode: () => getMode,
1314
1335
  getModel: () => getModel,
1315
1336
  getModelSignature: () => getModelSignature,
@@ -1977,6 +1998,13 @@ function forgetMemory(keyword) {
1977
1998
  ).run(`%${escaped}%`, `%${escaped}%`);
1978
1999
  return result.changes;
1979
2000
  }
2001
+ function getMemoryById(id) {
2002
+ return db.prepare("SELECT * FROM memories WHERE id = ?").get(id);
2003
+ }
2004
+ function deleteMemoryById(id) {
2005
+ const result = db.prepare("DELETE FROM memories WHERE id = ?").run(id);
2006
+ return result.changes > 0;
2007
+ }
1980
2008
  function applySalienceDecay() {
1981
2009
  db.exec("CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)");
1982
2010
  const lastDecayRow = db.prepare("SELECT value FROM meta WHERE key = 'last_salience_decay'").get();
@@ -2276,6 +2304,10 @@ function saveSessionSummary(chatId, summary, topics, messageCount, embedding) {
2276
2304
  function updateSessionSummaryEmbedding(id, embedding) {
2277
2305
  db.prepare("UPDATE session_summaries SET embedding = ? WHERE id = ?").run(embedding, id);
2278
2306
  }
2307
+ function deleteSessionSummary(id) {
2308
+ const result = db.prepare("DELETE FROM session_summaries WHERE id = ?").run(id);
2309
+ return result.changes > 0;
2310
+ }
2279
2311
  function getSessionSummariesWithoutEmbeddings(limit = 100) {
2280
2312
  return db.prepare(
2281
2313
  "SELECT * FROM session_summaries WHERE embedding IS NULL ORDER BY id LIMIT ?"
@@ -3258,6 +3290,12 @@ var init_gemini = __esm({
3258
3290
  thinkingLevels: ["low", "high"],
3259
3291
  defaultThinkingLevel: "high"
3260
3292
  },
3293
+ "gemini-2.5-pro": {
3294
+ label: "Gemini 2.5 Pro \u2014 stable, 1M context",
3295
+ thinking: "adjustable",
3296
+ thinkingLevels: ["low", "high"],
3297
+ defaultThinkingLevel: "high"
3298
+ },
3261
3299
  "gemini-3-flash-preview": {
3262
3300
  label: "Gemini 3 Flash \u2014 fast",
3263
3301
  thinking: "adjustable",
@@ -3275,11 +3313,13 @@ var init_gemini = __esm({
3275
3313
  summarizerModel = "gemini-3.1-flash-lite-preview";
3276
3314
  pricing = {
3277
3315
  "gemini-3.1-pro-preview": { in: 1.25, out: 10, cache: 0.32 },
3316
+ "gemini-2.5-pro": { in: 1.25, out: 10, cache: 0.32 },
3278
3317
  "gemini-3-flash-preview": { in: 0.15, out: 0.6, cache: 0.04 },
3279
3318
  "gemini-3.1-flash-lite-preview": { in: 0.04, out: 0.15, cache: 0.01 }
3280
3319
  };
3281
3320
  contextWindow = {
3282
3321
  "gemini-3.1-pro-preview": 1e6,
3322
+ "gemini-2.5-pro": 1e6,
3283
3323
  "gemini-3-flash-preview": 1e6,
3284
3324
  "gemini-3.1-flash-lite-preview": 1e6
3285
3325
  };
@@ -3828,6 +3868,7 @@ You are a proactive personal AI assistant running inside a Telegram bot powered
3828
3868
  - **Files**: To send a file to the user, write \`[SEND_FILE:/absolute/path/to/file]\` in your response \u2014 the system uploads it automatically.
3829
3869
  - **Heartbeat**: You periodically wake up to check on things and alert the user proactively.
3830
3870
  - **Sub-Agents**: You can spawn sub-agents using different coding CLIs to work on tasks in parallel. Use the orchestration MCP tools (spawn_agent, send_message, read_inbox, etc.).
3871
+ - **Interactive UI**: Most commands show interactive keyboards in Telegram. Use /menu for quick access to common actions.
3831
3872
  - **MCP Tools**: You have access to MCP servers for external tools and data sources. Use /mcp to see what's available.
3832
3873
  - **Self-Learning**: The system tracks your corrections, preferences, and feedback.
3833
3874
  Use /evolve to manage everything: analyze, review proposals, see stats, and more.
@@ -3883,6 +3924,7 @@ You are an expert user and operator of the CC-Claw architecture. Because you are
3883
3924
  ## 5. System Controls & Modes
3884
3925
  - **Permission Modes**: Command execution is gated. \`safe\` mode requires user approval for mutations, \`yolo\` auto-runs everything without asking, and \`plan\` is strictly read-only for drafting proposals.
3885
3926
  - **Global Settings**: The user controls your behavior system-wide using commands like \`/response_style\` (concise, normal, detailed) to enforce verbosity constraints, and \`/voice_config\` to select Text-to-Speech voices from premium providers like ElevenLabs, Grok (xAI), or local macOS.
3927
+ - **Telegram UX**: Most slash commands use interactive inline keyboards with colored buttons. \`/menu\` (or \`/m\`) is the home screen. \`/usage\` unifies cost, limits, and usage tracking. Inline mode (\`@botname query\`) searches memories from any chat.
3886
3928
 
3887
3929
  ## 6. Integrations & MCP
3888
3930
  - **Skills**: You can load specific workflows from the \`~/.cc-claw/workspace/skills/\` directory.
@@ -3956,6 +3998,10 @@ function bootstrapWorkspaceFiles() {
3956
3998
  mkdirSync(CONTEXT_DIR, { recursive: true });
3957
3999
  log("[bootstrap] Created context/ directory");
3958
4000
  }
4001
+ if (!existsSync5(MEDIA_PATH)) {
4002
+ mkdirSync(MEDIA_PATH, { recursive: true });
4003
+ log("[bootstrap] Created media/ directory");
4004
+ }
3959
4005
  const expertisePath = join6(CONTEXT_DIR, "cc-claw-expertise.md");
3960
4006
  if (!existsSync5(expertisePath)) {
3961
4007
  writeFileSync(expertisePath, DEFAULT_EXPERTISE, "utf-8");
@@ -4165,6 +4211,11 @@ ${ctx}`);
4165
4211
  } catch {
4166
4212
  }
4167
4213
  }
4214
+ if (tier !== "slim" && tier !== "heartbeat") {
4215
+ sections.push(
4216
+ "[React] Start your response with [REACT:emoji] \u2014 one emoji from the Telegram allowed set matching the message tone. Examples: \u{1F914} question, \u{1FAE1} task/request, \u{1F525} exciting, \u{1F923} funny, \u{1F622} sad, \u{1F4AF} agree, \u{1F389} celebrate, \u{1F468}\u200D\u{1F4BB} coding, \u{1F92F} mind-blown, \u{1F440} interesting, \u{1F91D} thanks, \u{1F60E} chill."
4217
+ );
4218
+ }
4168
4219
  sections.push(userMessage);
4169
4220
  const result = sections.join("\n\n");
4170
4221
  log(`[bootstrap] Assembled prompt: tier=${tier}, sections=${sections.length}, totalChars=${result.length}`);
@@ -7485,11 +7536,11 @@ data: ${JSON.stringify(data)}
7485
7536
  const body = JSON.parse(await readBody(req));
7486
7537
  const { setReflectionStatus: setReflectionStatus2 } = await Promise.resolve().then(() => (init_store4(), store_exports4));
7487
7538
  const { existsSync: fileExists, readFileSync: fileRead } = await import("fs");
7488
- const { join: join25 } = await import("path");
7539
+ const { join: join26 } = await import("path");
7489
7540
  const { CC_CLAW_HOME: home } = await Promise.resolve().then(() => (init_paths(), paths_exports));
7490
7541
  const chatId = body.chatId || (process.env.ALLOWED_CHAT_ID ?? "").split(",")[0]?.trim() || "default";
7491
- const soulPath = join25(home, "identity/SOUL.md");
7492
- const userPath = join25(home, "identity/USER.md");
7542
+ const soulPath = join26(home, "identity/SOUL.md");
7543
+ const userPath = join26(home, "identity/USER.md");
7493
7544
  const soul = fileExists(soulPath) ? fileRead(soulPath, "utf-8") : "";
7494
7545
  const user = fileExists(userPath) ? fileRead(userPath, "utf-8") : "";
7495
7546
  setReflectionStatus2(getDb(), chatId, "active", soul, user);
@@ -9526,6 +9577,7 @@ var init_telegram2 = __esm({
9526
9577
  "use strict";
9527
9578
  init_telegram();
9528
9579
  init_log();
9580
+ init_store5();
9529
9581
  TelegramChannel = class {
9530
9582
  name = "telegram";
9531
9583
  bot;
@@ -9566,12 +9618,15 @@ var init_telegram2 = __esm({
9566
9618
  voice: true,
9567
9619
  files: true,
9568
9620
  typing: true,
9569
- richFormatting: true
9621
+ richFormatting: true,
9622
+ buttonStyles: true
9570
9623
  };
9571
9624
  }
9572
9625
  async start(handler) {
9573
9626
  await this.bot.api.setMyCommands([
9574
9627
  // Core
9628
+ { command: "menu", description: "Home screen \u2014 quick-access keyboard" },
9629
+ { command: "m", description: "Home screen (alias for /menu)" },
9575
9630
  { command: "help", description: "Show available commands" },
9576
9631
  { command: "status", description: "Session, backend, model, and usage" },
9577
9632
  { command: "new", description: "Start a fresh conversation" },
@@ -9684,6 +9739,12 @@ var init_telegram2 = __esm({
9684
9739
  }
9685
9740
  }
9686
9741
  });
9742
+ this.bot.on("inline_query", (ctx) => {
9743
+ if (!this.isAuthorized(ctx.from.id.toString())) return;
9744
+ this.handleInlineQuery(ctx).catch((err) => {
9745
+ error("[telegram] Inline query error:", err);
9746
+ });
9747
+ });
9687
9748
  this.bot.catch((err) => {
9688
9749
  error("[telegram] Unhandled error:", err);
9689
9750
  });
@@ -9792,19 +9853,28 @@ var init_telegram2 = __esm({
9792
9853
  for (const row of buttons) {
9793
9854
  for (const btn of row) {
9794
9855
  keyboard.text(btn.label, btn.data);
9856
+ if (btn.style === "success") keyboard.success();
9857
+ else if (btn.style === "danger") keyboard.danger();
9858
+ else if (btn.style === "primary") keyboard.primary();
9795
9859
  }
9796
9860
  keyboard.row();
9797
9861
  }
9798
- await this.bot.api.sendMessage(numericChatId(chatId), text, {
9799
- reply_markup: keyboard
9800
- });
9862
+ try {
9863
+ const msg = await this.bot.api.sendMessage(numericChatId(chatId), text, {
9864
+ reply_markup: keyboard
9865
+ });
9866
+ return msg.message_id.toString();
9867
+ } catch {
9868
+ return void 0;
9869
+ }
9801
9870
  }
9802
9871
  async reactToMessage(chatId, messageId, emoji) {
9803
9872
  try {
9804
9873
  await this.bot.api.setMessageReaction(numericChatId(chatId), parseInt(messageId), [
9805
9874
  { type: "emoji", emoji }
9806
9875
  ]);
9807
- } catch {
9876
+ } catch (err) {
9877
+ log(`[telegram] reactToMessage failed (chat=${chatId} msg=${messageId}): ${err}`);
9808
9878
  }
9809
9879
  }
9810
9880
  /** Get the underlying Grammy Bot instance (for scheduler, etc.) */
@@ -9812,6 +9882,57 @@ var init_telegram2 = __esm({
9812
9882
  return this.bot;
9813
9883
  }
9814
9884
  // ── Private helpers ──────────────────────────────────────
9885
+ async handleInlineQuery(ctx) {
9886
+ const query = ctx.inlineQuery?.query?.trim() ?? "";
9887
+ const results = [];
9888
+ if (query.length < 2) {
9889
+ const recent = getRecentMemories(10);
9890
+ for (const mem of recent) {
9891
+ results.push({
9892
+ type: "article",
9893
+ id: `mem-${mem.id}`,
9894
+ title: mem.trigger,
9895
+ description: mem.content.slice(0, 100),
9896
+ input_message_content: {
9897
+ message_text: mem.content.slice(0, 4096)
9898
+ }
9899
+ });
9900
+ }
9901
+ } else {
9902
+ const memories = searchMemories(query, 5);
9903
+ for (const mem of memories) {
9904
+ results.push({
9905
+ type: "article",
9906
+ id: `mem-${mem.id}`,
9907
+ title: mem.trigger,
9908
+ description: mem.content.slice(0, 100),
9909
+ input_message_content: {
9910
+ message_text: mem.content.slice(0, 4096)
9911
+ }
9912
+ });
9913
+ }
9914
+ const historyLimit = 10 - results.length;
9915
+ if (historyLimit > 0) {
9916
+ const messages = searchMessageLog(this.primaryChatId, query, historyLimit);
9917
+ for (const msg of messages) {
9918
+ const date = msg.created_at?.slice(0, 10) ?? "";
9919
+ const backend2 = msg.backend ?? "?";
9920
+ const role = msg.role === "user" ? "You" : "Assistant";
9921
+ const preview = msg.content.slice(0, 60).replace(/\n/g, " ");
9922
+ results.push({
9923
+ type: "article",
9924
+ id: `msg-${msg.id}`,
9925
+ title: `[${backend2} \xB7 ${date}] ${role}: ${preview}`,
9926
+ description: msg.content.slice(0, 100).replace(/\n/g, " "),
9927
+ input_message_content: {
9928
+ message_text: msg.content.slice(0, 4096)
9929
+ }
9930
+ });
9931
+ }
9932
+ }
9933
+ }
9934
+ await ctx.answerInlineQuery(results.slice(0, 10), { cache_time: 30 });
9935
+ }
9815
9936
  trackAgentMessage(messageId, chatId) {
9816
9937
  if (this.agentMessageIds.size >= 1e4) {
9817
9938
  this.agentMessageIds.clear();
@@ -10576,6 +10697,17 @@ var init_heartbeat = __esm({
10576
10697
  }
10577
10698
  });
10578
10699
 
10700
+ // src/ui/format.ts
10701
+ function buildSectionHeader(label2, width = 26) {
10702
+ const inner = `\u2501\u2501 ${label2} `;
10703
+ return inner + "\u2501".repeat(Math.max(0, width - inner.length));
10704
+ }
10705
+ var init_format = __esm({
10706
+ "src/ui/format.ts"() {
10707
+ "use strict";
10708
+ }
10709
+ });
10710
+
10579
10711
  // src/format-time.ts
10580
10712
  function formatLocalDateTime(utcDatetime) {
10581
10713
  const d = parseUtcDatetime(utcDatetime);
@@ -11627,6 +11759,85 @@ var init_wizard = __esm({
11627
11759
  }
11628
11760
  });
11629
11761
 
11762
+ // src/scheduler/humanize.ts
11763
+ function formatTime(hour, minute) {
11764
+ const h = hour % 12 || 12;
11765
+ const ampm = hour < 12 ? "AM" : "PM";
11766
+ return minute === 0 ? `${h}:00 ${ampm}` : `${h}:${String(minute).padStart(2, "0")} ${ampm}`;
11767
+ }
11768
+ function humanizeCron(expr, _tz) {
11769
+ if (expr.startsWith("every:")) {
11770
+ const ms = parseInt(expr.slice(6), 10);
11771
+ if (isNaN(ms)) return expr;
11772
+ const minutes = Math.round(ms / 6e4);
11773
+ if (minutes < 60) return `Every ${minutes} minute${minutes !== 1 ? "s" : ""}`;
11774
+ const hours = Math.round(minutes / 60);
11775
+ if (hours < 24) return `Every ${hours} hour${hours !== 1 ? "s" : ""}`;
11776
+ const days = Math.round(hours / 24);
11777
+ return `Every ${days} day${days !== 1 ? "s" : ""}`;
11778
+ }
11779
+ if (expr.startsWith("at:")) {
11780
+ const dateStr = expr.slice(3);
11781
+ try {
11782
+ const date = new Date(dateStr);
11783
+ const month2 = date.toLocaleString("en-US", { month: "short" });
11784
+ const day = date.getDate();
11785
+ const year = date.getFullYear();
11786
+ return `Once on ${month2} ${day}, ${year}`;
11787
+ } catch {
11788
+ return expr;
11789
+ }
11790
+ }
11791
+ const parts = expr.trim().split(/\s+/);
11792
+ if (parts.length !== 5) return expr;
11793
+ const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
11794
+ if (minute === "*" && hour === "*" && dayOfMonth === "*" && month === "*" && dayOfWeek === "*") {
11795
+ return "Every minute";
11796
+ }
11797
+ if (minute.startsWith("*/") && hour === "*" && dayOfMonth === "*" && month === "*" && dayOfWeek === "*") {
11798
+ const n = parseInt(minute.slice(2), 10);
11799
+ return `Every ${n} minute${n !== 1 ? "s" : ""}`;
11800
+ }
11801
+ if (!minute.includes("*") && !minute.includes("/") && hour === "*" && dayOfMonth === "*" && month === "*" && dayOfWeek === "*") {
11802
+ const m = parseInt(minute, 10);
11803
+ return m === 0 ? "Every hour" : `Every hour at :${String(m).padStart(2, "0")}`;
11804
+ }
11805
+ if (minute === "0" && hour.startsWith("*/") && dayOfMonth === "*" && month === "*" && dayOfWeek === "*") {
11806
+ const n = parseInt(hour.slice(2), 10);
11807
+ return `Every ${n} hour${n !== 1 ? "s" : ""}`;
11808
+ }
11809
+ const hasSpecificTime = !minute.includes("*") && !minute.includes("/") && !hour.includes("*") && !hour.includes("/");
11810
+ if (hasSpecificTime) {
11811
+ const h = parseInt(hour, 10);
11812
+ const m = parseInt(minute, 10);
11813
+ const timeStr = formatTime(h, m);
11814
+ if (dayOfMonth === "*" && month === "*" && dayOfWeek === "*") {
11815
+ return `Every day at ${timeStr}`;
11816
+ }
11817
+ if (dayOfMonth === "*" && month === "*" && dayOfWeek !== "*") {
11818
+ const dayNames = dayOfWeek.split(",").map((d) => {
11819
+ const idx = parseInt(d, 10);
11820
+ return DAYS[idx] ?? d;
11821
+ });
11822
+ if (dayNames.length === 1) return `Every ${dayNames[0]} at ${timeStr}`;
11823
+ return `Every ${dayNames.join(", ")} at ${timeStr}`;
11824
+ }
11825
+ if (dayOfMonth !== "*" && month === "*" && dayOfWeek === "*") {
11826
+ const d = parseInt(dayOfMonth, 10);
11827
+ const suffix = d === 1 || d === 21 || d === 31 ? "st" : d === 2 || d === 22 ? "nd" : d === 3 || d === 23 ? "rd" : "th";
11828
+ return `${d}${suffix} of every month at ${timeStr}`;
11829
+ }
11830
+ }
11831
+ return expr;
11832
+ }
11833
+ var DAYS;
11834
+ var init_humanize = __esm({
11835
+ "src/scheduler/humanize.ts"() {
11836
+ "use strict";
11837
+ DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
11838
+ }
11839
+ });
11840
+
11630
11841
  // src/media/video.ts
11631
11842
  async function analyzeVideo(videoBuffer, prompt, mimeType = "video/mp4") {
11632
11843
  const GEMINI_API_KEY = process.env.GEMINI_API_KEY;
@@ -11903,6 +12114,48 @@ var init_backend_cmd = __esm({
11903
12114
  }
11904
12115
  });
11905
12116
 
12117
+ // src/ui/pagination.ts
12118
+ function buildPaginatedKeyboard(opts) {
12119
+ const { items, page, callbackPrefix, renderItem, headerText, footerButtons } = opts;
12120
+ const pageSize = opts.pageSize ?? DEFAULT_PAGE_SIZE;
12121
+ const totalPages = Math.max(1, Math.ceil(items.length / pageSize));
12122
+ const safePage = Math.max(1, Math.min(page, totalPages));
12123
+ const start = (safePage - 1) * pageSize;
12124
+ const pageItems = items.slice(start, start + pageSize);
12125
+ const text = headerText(safePage, totalPages, items.length);
12126
+ const buttons = [];
12127
+ for (const [i, item] of pageItems.entries()) {
12128
+ const rendered = renderItem(item, start + i);
12129
+ buttons.push([rendered]);
12130
+ }
12131
+ if (totalPages > 1) {
12132
+ const navRow = [];
12133
+ if (safePage > 1) navRow.push({ label: `\u2190 Page ${safePage - 1}`, data: `${callbackPrefix}page:${safePage - 1}` });
12134
+ navRow.push({ label: `${safePage}/${totalPages}`, data: `${callbackPrefix}page:noop` });
12135
+ if (safePage < totalPages) navRow.push({ label: `Page ${safePage + 1} \u2192`, data: `${callbackPrefix}page:${safePage + 1}` });
12136
+ buttons.push(navRow);
12137
+ }
12138
+ if (footerButtons && footerButtons.length > 0) {
12139
+ buttons.push(footerButtons);
12140
+ }
12141
+ return { text, buttons };
12142
+ }
12143
+ function isPaginationCallback(data, prefix) {
12144
+ const pagePrefix = `${prefix}page:`;
12145
+ if (!data.startsWith(pagePrefix)) return null;
12146
+ const val = data.slice(pagePrefix.length);
12147
+ if (val === "noop") return null;
12148
+ const page = parseInt(val, 10);
12149
+ return isNaN(page) ? null : page;
12150
+ }
12151
+ var DEFAULT_PAGE_SIZE;
12152
+ var init_pagination = __esm({
12153
+ "src/ui/pagination.ts"() {
12154
+ "use strict";
12155
+ DEFAULT_PAGE_SIZE = 10;
12156
+ }
12157
+ });
12158
+
11906
12159
  // src/reflection/propose.ts
11907
12160
  var propose_exports = {};
11908
12161
  __export(propose_exports, {
@@ -11972,40 +12225,40 @@ function buildProposalKeyboard(insightId, category) {
11972
12225
  if (category === "codebase") {
11973
12226
  return [
11974
12227
  [
11975
- { text: "Show Diff", callback_data: `evolve:diff:${insightId}` },
11976
- { text: "Skip", callback_data: `evolve:skip:${insightId}` },
11977
- { text: "Reject", callback_data: `evolve:reject:${insightId}` }
12228
+ { label: "Show Diff", data: `evolve:diff:${insightId}`, style: "primary" },
12229
+ { label: "Skip", data: `evolve:skip:${insightId}` },
12230
+ { label: "Reject", data: `evolve:reject:${insightId}`, style: "danger" }
11978
12231
  ]
11979
12232
  ];
11980
12233
  }
11981
12234
  return [
11982
12235
  [
11983
- { text: "Apply", callback_data: `evolve:apply:${insightId}` },
11984
- { text: "Skip", callback_data: `evolve:skip:${insightId}` },
11985
- { text: "Reject", callback_data: `evolve:reject:${insightId}` }
12236
+ { label: "Apply", data: `evolve:apply:${insightId}`, style: "success" },
12237
+ { label: "Skip", data: `evolve:skip:${insightId}` },
12238
+ { label: "Reject", data: `evolve:reject:${insightId}`, style: "danger" }
11986
12239
  ]
11987
12240
  ];
11988
12241
  }
11989
12242
  function buildEvolveMenuKeyboard() {
11990
12243
  return [
11991
12244
  [
11992
- { text: "Analyze Now", callback_data: "evolve:analyze" },
11993
- { text: "Review Proposals", callback_data: "evolve:review" }
12245
+ { label: "Analyze Now", data: "evolve:analyze", style: "success" },
12246
+ { label: "Review Proposals", data: "evolve:review" }
11994
12247
  ],
11995
12248
  [
11996
- { text: "Stats", callback_data: "evolve:stats" },
11997
- { text: "History", callback_data: "evolve:history" }
12249
+ { label: "Stats", data: "evolve:stats" },
12250
+ { label: "History", data: "evolve:history" }
11998
12251
  ],
11999
12252
  [
12000
- { text: "On/Off", callback_data: "evolve:toggle" },
12001
- { text: "Undo", callback_data: "evolve:undo" },
12002
- { text: "Model", callback_data: "evolve:model" }
12253
+ { label: "On/Off", data: "evolve:toggle" },
12254
+ { label: "Undo", data: "evolve:undo", style: "danger" },
12255
+ { label: "Model", data: "evolve:model" }
12003
12256
  ]
12004
12257
  ];
12005
12258
  }
12006
12259
  function buildUndoKeyboard(insights) {
12007
12260
  return insights.map((ins) => [
12008
- { text: `Undo #${ins.id}: ${ins.insight}`, callback_data: `evolve:undo:${ins.id}` }
12261
+ { label: `Undo #${ins.id}: ${ins.insight}`, data: `evolve:undo:${ins.id}`, style: "danger" }
12009
12262
  ]);
12010
12263
  }
12011
12264
  function buildModelKeyboard(currentMode) {
@@ -12016,8 +12269,9 @@ function buildModelKeyboard(currentMode) {
12016
12269
  ];
12017
12270
  return [
12018
12271
  modes.map(({ label: label2, mode }) => ({
12019
- text: mode === currentMode ? `[${label2}]` : label2,
12020
- callback_data: `evolve:model:${mode}`
12272
+ label: mode === currentMode ? `[${label2}]` : label2,
12273
+ data: `evolve:model:${mode}`,
12274
+ ...mode === currentMode ? { style: "primary" } : {}
12021
12275
  }))
12022
12276
  ];
12023
12277
  }
@@ -12028,8 +12282,8 @@ var init_propose = __esm({
12028
12282
  });
12029
12283
 
12030
12284
  // src/router.ts
12031
- import { readFile as readFile5, writeFile as writeFile3, unlink as unlink2 } from "fs/promises";
12032
- import { resolve as resolvePath } from "path";
12285
+ import { readFile as readFile5, writeFile as writeFile3, unlink as unlink2, mkdir as mkdir2, readdir as readdir3, stat } from "fs/promises";
12286
+ import { resolve as resolvePath, join as join19 } from "path";
12033
12287
  function parseMcpListOutput(output2) {
12034
12288
  const results = [];
12035
12289
  const seen = /* @__PURE__ */ new Set();
@@ -12082,6 +12336,7 @@ function formatModelShort(modelId) {
12082
12336
  "claude-sonnet-4-6": "Sonnet 4.6",
12083
12337
  "claude-haiku-4-5": "Haiku 4.5",
12084
12338
  "gemini-3.1-pro-preview": "Gemini Pro 3.1",
12339
+ "gemini-2.5-pro": "Gemini 2.5 Pro",
12085
12340
  "gemini-3-flash-preview": "Gemini 3 Flash",
12086
12341
  "gpt-5.4": "GPT-5.4",
12087
12342
  "gpt-5.3-codex": "GPT-5.3 Codex",
@@ -12108,22 +12363,208 @@ function resolveModel(chatId) {
12108
12363
  return "unknown";
12109
12364
  }
12110
12365
  }
12366
+ async function sendHelpCategories(chatId, channel) {
12367
+ if (typeof channel.sendKeyboard !== "function") return;
12368
+ const header2 = `${buildSectionHeader("CC-Claw Commands")}`;
12369
+ const cats = Object.keys(HELP_CATEGORIES);
12370
+ const rows = [];
12371
+ for (let i = 0; i < cats.length; i += 2) {
12372
+ const row = [
12373
+ { label: cats[i], data: `help:${cats[i].toLowerCase()}`, style: "primary" }
12374
+ ];
12375
+ if (cats[i + 1]) {
12376
+ row.push({ label: cats[i + 1], data: `help:${cats[i + 1].toLowerCase()}`, style: "primary" });
12377
+ }
12378
+ rows.push(row);
12379
+ }
12380
+ await channel.sendKeyboard(chatId, header2, rows);
12381
+ }
12382
+ async function sendHelpCategory(chatId, category, channel) {
12383
+ if (typeof channel.sendKeyboard !== "function") return;
12384
+ const cat = Object.entries(HELP_CATEGORIES).find(([k]) => k.toLowerCase() === category.toLowerCase());
12385
+ if (!cat) return;
12386
+ const [name, cmds] = cat;
12387
+ const header2 = `${buildSectionHeader(`${name} (${cmds.length} commands)`)}`;
12388
+ const body = cmds.map((c) => `${c.cmd} \u2014 ${c.desc}`).join("\n");
12389
+ await channel.sendKeyboard(chatId, `${header2}
12390
+
12391
+ ${body}`, [
12392
+ [{ label: "\u2190 Back to Categories", data: "help:back" }]
12393
+ ]);
12394
+ }
12395
+ function buildUsagePricing() {
12396
+ const pricing = {};
12397
+ for (const a of getAllAdapters()) Object.assign(pricing, a.pricing);
12398
+ return { pricing, defaultPricing: { in: 3, out: 15, cache: 0.3 } };
12399
+ }
12400
+ function computeCost(tokens, model2, pricing, defaultPricing) {
12401
+ const p = pricing[model2] ?? defaultPricing;
12402
+ return tokens.input_tokens / 1e6 * p.in + tokens.output_tokens / 1e6 * p.out + tokens.cache_read_tokens / 1e6 * p.cache;
12403
+ }
12404
+ async function sendUnifiedUsage(chatId, channel, view) {
12405
+ const { pricing, defaultPricing } = buildUsagePricing();
12406
+ const fmt = (n) => `$${n.toFixed(4)}`;
12407
+ const fmtK = (n) => `${(n / 1e3).toFixed(0)}K`;
12408
+ const lines = [buildSectionHeader("Usage & Costs"), ""];
12409
+ if (view === "session") {
12410
+ const sessionStart = getSessionStartedAt(chatId);
12411
+ const chatModelUsage = getChatUsageByModel(chatId, sessionStart);
12412
+ let sessionTokens = 0;
12413
+ let sessionCost = 0;
12414
+ for (const u of chatModelUsage) {
12415
+ sessionTokens += u.input_tokens + u.output_tokens;
12416
+ sessionCost += computeCost(u, u.model, pricing, defaultPricing);
12417
+ }
12418
+ lines.push(`Session: ${fmtK(sessionTokens)} tokens \xB7 ~${fmt(sessionCost)}`);
12419
+ lines.push("");
12420
+ for (const bid of getAllBackendIds()) {
12421
+ const u = getBackendUsageInWindow(bid, "daily");
12422
+ const a = getAdapter(bid);
12423
+ const limit = getAllBackendLimits().find((l) => l.backend === bid && l.window === "daily");
12424
+ if (limit?.max_input_tokens) {
12425
+ lines.push(`${a.displayName}: ${fmtK(u.input_tokens)} / ${fmtK(limit.max_input_tokens)} limit (24h)`);
12426
+ } else {
12427
+ lines.push(`${a.displayName}: ${fmtK(u.input_tokens + u.output_tokens)} / no limit`);
12428
+ }
12429
+ }
12430
+ } else if (view === "model") {
12431
+ const allUsage = getAllTimeUsage();
12432
+ if (allUsage.length === 0) {
12433
+ lines.push("No usage history recorded yet.");
12434
+ } else {
12435
+ let grandTotal = 0;
12436
+ for (const u of allUsage) {
12437
+ const cost = computeCost(u, u.model, pricing, defaultPricing);
12438
+ grandTotal += cost;
12439
+ lines.push(`${formatModelShort(u.model)}: ${fmtK(u.input_tokens + u.output_tokens)} \xB7 ${fmt(cost)}`);
12440
+ }
12441
+ lines.push("", `Total: ${fmt(grandTotal)}`);
12442
+ }
12443
+ } else if (view === "limits") {
12444
+ await sendUsageLimits(chatId, channel);
12445
+ return;
12446
+ } else {
12447
+ const windowType = USAGE_WINDOW_MAP[view] ?? "daily";
12448
+ const windowLabel = view;
12449
+ for (const bid of getAllBackendIds()) {
12450
+ const u = view === "30d" ? getBackendUsageInWindow(bid, "weekly") : getBackendUsageInWindow(bid, windowType);
12451
+ const a = getAdapter(bid);
12452
+ const limit = getAllBackendLimits().find((l) => l.backend === bid && l.window === windowType);
12453
+ if (limit?.max_input_tokens) {
12454
+ lines.push(`${a.displayName}: ${fmtK(u.input_tokens)} / ${fmtK(limit.max_input_tokens)} limit (${windowLabel})`);
12455
+ } else {
12456
+ lines.push(`${a.displayName}: ${fmtK(u.input_tokens + u.output_tokens)} (${windowLabel}) \xB7 no limit`);
12457
+ }
12458
+ }
12459
+ }
12460
+ if (typeof channel.sendKeyboard === "function") {
12461
+ const buttons = [
12462
+ [
12463
+ { label: view === "session" ? "Session \u2713" : "Session", data: "usage:session", ...view === "session" ? { style: "primary" } : {} },
12464
+ { label: view === "24h" ? "24h \u2713" : "24h", data: "usage:24h", ...view === "24h" ? { style: "primary" } : {} },
12465
+ { label: view === "7d" ? "7d \u2713" : "7d", data: "usage:7d", ...view === "7d" ? { style: "primary" } : {} },
12466
+ { label: view === "30d" ? "30d \u2713" : "30d", data: "usage:30d", ...view === "30d" ? { style: "primary" } : {} }
12467
+ ],
12468
+ [
12469
+ { label: view === "model" ? "By Model \u2713" : "By Model", data: "usage:model", ...view === "model" ? { style: "primary" } : {} },
12470
+ { label: "Set Limits", data: "usage:limits" }
12471
+ ]
12472
+ ];
12473
+ await channel.sendKeyboard(chatId, lines.join("\n"), buttons);
12474
+ } else {
12475
+ await channel.sendText(chatId, lines.join("\n"), "plain");
12476
+ }
12477
+ }
12478
+ async function sendUsageLimits(chatId, channel) {
12479
+ const limits2 = getAllBackendLimits();
12480
+ const fmtK = (n) => `${(n / 1e3).toFixed(0)}K`;
12481
+ const lines = [buildSectionHeader("Set Usage Limits"), ""];
12482
+ for (const bid of getAllBackendIds()) {
12483
+ const a = getAdapter(bid);
12484
+ const lim = limits2.find((l) => l.backend === bid);
12485
+ if (lim?.max_input_tokens) {
12486
+ lines.push(`${a.displayName}: ${fmtK(lim.max_input_tokens)} tokens / ${lim.window}`);
12487
+ } else {
12488
+ lines.push(`${a.displayName}: none`);
12489
+ }
12490
+ }
12491
+ lines.push("", "Use /limits <backend> <window> <tokens> to set.\nExample: /limits claude daily 500000");
12492
+ if (typeof channel.sendKeyboard === "function") {
12493
+ const setButtons = getAllBackendIds().map((bid) => ({
12494
+ label: `Set ${getAdapter(bid).displayName} Limit`,
12495
+ data: `usage:limits:set:${bid}`
12496
+ }));
12497
+ const rows = [];
12498
+ for (let i = 0; i < setButtons.length; i += 2) {
12499
+ rows.push(setButtons.slice(i, i + 2));
12500
+ }
12501
+ rows.push([
12502
+ { label: "Clear All", data: "usage:limits:clear", style: "danger" }
12503
+ ]);
12504
+ rows.push([
12505
+ { label: "\u2190 Back to Usage", data: "usage:back" }
12506
+ ]);
12507
+ await channel.sendKeyboard(chatId, lines.join("\n"), rows);
12508
+ } else {
12509
+ await channel.sendText(chatId, lines.join("\n"), "plain");
12510
+ }
12511
+ }
12512
+ function getMediaRetentionMs() {
12513
+ const hours = parseInt(process.env.MEDIA_RETENTION_HOURS ?? "24", 10);
12514
+ return (isNaN(hours) || hours < 1 ? 24 : hours) * 60 * 60 * 1e3;
12515
+ }
12516
+ async function saveMedia(buffer, prefix, ext) {
12517
+ await mkdir2(MEDIA_INCOMING_PATH, { recursive: true });
12518
+ const filename = `${prefix}-${Date.now()}.${ext}`;
12519
+ const fullPath = join19(MEDIA_INCOMING_PATH, filename);
12520
+ await writeFile3(fullPath, buffer);
12521
+ return fullPath;
12522
+ }
12523
+ async function cleanupOldMedia() {
12524
+ try {
12525
+ await mkdir2(MEDIA_INCOMING_PATH, { recursive: true });
12526
+ const retentionMs = getMediaRetentionMs();
12527
+ const retentionHours = Math.round(retentionMs / (60 * 60 * 1e3));
12528
+ const files = await readdir3(MEDIA_INCOMING_PATH);
12529
+ const now = Date.now();
12530
+ let removed = 0;
12531
+ for (const file of files) {
12532
+ try {
12533
+ const filePath = join19(MEDIA_INCOMING_PATH, file);
12534
+ const s = await stat(filePath);
12535
+ if (now - s.mtimeMs > retentionMs) {
12536
+ await unlink2(filePath);
12537
+ removed++;
12538
+ }
12539
+ } catch {
12540
+ }
12541
+ }
12542
+ if (removed > 0) log(`[media] Cleaned up ${removed} file(s) older than ${retentionHours}h from incoming/`);
12543
+ } catch {
12544
+ }
12545
+ }
12111
12546
  function pickReactionEmoji(msg) {
12112
12547
  switch (msg.type) {
12113
12548
  case "voice":
12114
- return "\u{1F3A7}";
12549
+ return "\u{1F917}";
12115
12550
  case "photo":
12116
12551
  return "\u{1F440}";
12117
12552
  case "video":
12118
- return "\u{1F3AC}";
12553
+ return "\u{1F440}";
12119
12554
  case "document":
12120
- return "\u{1F4C4}";
12555
+ return "\u{1F913}";
12121
12556
  case "command":
12122
12557
  return "\u{1FAE1}";
12123
12558
  default:
12124
12559
  return "\u{1F468}\u200D\u{1F4BB}";
12125
12560
  }
12126
12561
  }
12562
+ function pickFallbackEmoji(userMessage) {
12563
+ for (const { pattern, emoji } of TONE_PATTERNS) {
12564
+ if (pattern.test(userMessage)) return emoji;
12565
+ }
12566
+ return "\u{1F44D}";
12567
+ }
12127
12568
  async function handleResponseExhaustion(responseText, chatId, msg, channel) {
12128
12569
  const raw = responseText.replace(/\n\n🧠 \[.+$/, "").trim();
12129
12570
  if (raw.length > 300 || !isExhaustedMessage(raw)) return false;
@@ -12134,7 +12575,8 @@ async function handleResponseExhaustion(responseText, chatId, msg, channel) {
12134
12575
  pendingFallbackMessages.set(chatId, { msg, channel, agentMode: msg.agentMode });
12135
12576
  const buttons = otherBackends.map((a) => ({
12136
12577
  label: `\u{1F504} ${a.displayName}`,
12137
- data: `fallback:${a.id}:${chatId}`
12578
+ data: `fallback:${a.id}:${chatId}`,
12579
+ style: "primary"
12138
12580
  }));
12139
12581
  buttons.push({ label: "\u274C Wait", data: `fallback:wait:${chatId}` });
12140
12582
  await channel.sendKeyboard(
@@ -12201,13 +12643,40 @@ async function handleCommand(msg, channel) {
12201
12643
  }
12202
12644
  switch (command) {
12203
12645
  case "start":
12204
- case "help":
12205
- await channel.sendText(
12206
- chatId,
12207
- "Hey! I'm CC-Claw \u2014 your personal AI assistant on Telegram.\n\nI use AI coding CLIs (Claude, Gemini, Codex) as my brain. Just send me a message to get started.\n\nCommands:\n/backend [name] - Switch AI backend (or /claude /gemini /codex)\n/model - Switch model for active backend\n/gemini_accounts - Manage Gemini credentials & rotation\n/summarizer - Configure session summarization model\n/status - Show session, model, backend, and usage\n/cost - Show estimated API cost (use /cost all for all-time)\n/usage - Show usage per backend with limits\n/limits - Configure usage limits per backend\n/newchat - Start a fresh conversation\n/summarize - Save session to memory (without resetting)\n/summarize all - Summarize all pending sessions (pre-restart)\n/cwd <path> - Set working directory\n/cwd - Show current working directory\n/memory - List stored memories\n/remember <text> - Save a memory\n/forget <keyword> - Remove a memory\n/voice - Toggle voice responses\n/voice_config - Configure voice provider and voice\n/imagine <prompt> - Generate an image (or /image)\n/cron <description> - Schedule a task (or /schedule)\n/cron - List scheduled jobs (or /jobs)\n/cron cancel <id> - Cancel a job\n/cron pause <id> - Pause a job\n/cron resume <id> - Resume a job\n/cron run <id> - Trigger a job now\n/cron runs [id] - View run history\n/cron edit <id> - Edit a job\n/cron health - Scheduler health\n/skills - List skills from all backends\n/skill-install <url> - Install a skill from GitHub\n/setup-profile - Set up your user profile\n/chats - List authorized chats and aliases\n/heartbeat - Proactive awareness (on/off/interval/hours)\n/history - List recent session summaries\n/stop - Cancel the current running task\n/tools - Configure which tools the agent can use\n/permissions - Switch permission mode (yolo/safe/plan)\n/verbose - Tool visibility (off/normal/verbose)\n/model_signature - Toggle model+thinking signature on responses\n/intent <msg> - Test intent classifier (chat vs agentic)\n/agents - List active sub-agents\n/agents mode [auto|native|claw] - Set agent mode (native vs orchestrated)\n/agents history - Native sub-agent activity (24h)\n/tasks - Show task board for current orchestration\n/stopagent <id> - Cancel a specific sub-agent\n/stopall - Cancel all sub-agents in this chat\n/runners - List registered CLI runners\n/mcps - List registered MCP servers\n/evolve - Self-learning & evolution controls\n/help - Show this message",
12208
- "plain"
12209
- );
12646
+ case "help": {
12647
+ if (typeof channel.sendKeyboard === "function") {
12648
+ await sendHelpCategories(chatId, channel);
12649
+ } else {
12650
+ await channel.sendText(
12651
+ chatId,
12652
+ "CC-Claw Commands\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n\n" + Object.entries(HELP_CATEGORIES).map(
12653
+ ([cat, cmds]) => `${cat}:
12654
+ ${cmds.map((c) => ` ${c.cmd} \u2014 ${c.desc}`).join("\n")}`
12655
+ ).join("\n\n"),
12656
+ "plain"
12657
+ );
12658
+ }
12659
+ break;
12660
+ }
12661
+ case "menu":
12662
+ case "m": {
12663
+ if (typeof channel.sendKeyboard === "function") {
12664
+ const header2 = `\u{1F43E} ${buildSectionHeader("CC-Claw")}`;
12665
+ await channel.sendKeyboard(chatId, header2, [
12666
+ [{ label: "New Chat", data: "menu:newchat" }, { label: "Status", data: "menu:status" }],
12667
+ [{ label: "Backend", data: "menu:backend" }, { label: "Model", data: "menu:model" }],
12668
+ [{ label: "Jobs", data: "menu:jobs" }, { label: "Memory", data: "menu:memory" }],
12669
+ [{ label: "History", data: "menu:history" }, { label: "Help", data: "menu:help" }]
12670
+ ]);
12671
+ } else {
12672
+ await channel.sendText(
12673
+ chatId,
12674
+ "CC-Claw Menu:\n/newchat \xB7 /status \xB7 /backend \xB7 /model\n/jobs \xB7 /memory \xB7 /history \xB7 /help",
12675
+ "plain"
12676
+ );
12677
+ }
12210
12678
  break;
12679
+ }
12211
12680
  case "stop": {
12212
12681
  const stopped = stopAgent(chatId);
12213
12682
  await channel.sendText(
@@ -12215,6 +12684,11 @@ async function handleCommand(msg, channel) {
12215
12684
  stopped ? "Stopping current task..." : "Nothing is running.",
12216
12685
  "plain"
12217
12686
  );
12687
+ if (stopped && typeof channel.sendKeyboard === "function") {
12688
+ await channel.sendKeyboard(chatId, "", [
12689
+ [{ label: "\u{1F195} New Chat", data: "menu:newchat" }]
12690
+ ]);
12691
+ }
12218
12692
  break;
12219
12693
  }
12220
12694
  case "permissions": {
@@ -12222,7 +12696,8 @@ async function handleCommand(msg, channel) {
12222
12696
  if (typeof channel.sendKeyboard === "function") {
12223
12697
  const buttons = Object.entries(PERM_MODES).map(([id, label2]) => [{
12224
12698
  label: `${id === currentMode ? "\u2713 " : ""}${label2}`,
12225
- data: `perms:${id}`
12699
+ data: `perms:${id}`,
12700
+ ...id === currentMode ? { style: "primary" } : {}
12226
12701
  }]);
12227
12702
  await channel.sendKeyboard(chatId, "Select permission mode:", buttons);
12228
12703
  } else {
@@ -12238,7 +12713,8 @@ async function handleCommand(msg, channel) {
12238
12713
  const currentVerbose = getVerboseLevel(chatId);
12239
12714
  const buttons = Object.entries(VERBOSE_LEVELS).map(([id, label2]) => [{
12240
12715
  label: `${id === currentVerbose ? "\u2713 " : ""}${label2}`,
12241
- data: `verbose:${id}`
12716
+ data: `verbose:${id}`,
12717
+ ...id === currentVerbose ? { style: "primary" } : {}
12242
12718
  }]);
12243
12719
  if (typeof channel.sendKeyboard === "function") {
12244
12720
  await channel.sendKeyboard(chatId, "Tool visibility level:", buttons);
@@ -12263,7 +12739,7 @@ async function handleCommand(msg, channel) {
12263
12739
  }));
12264
12740
  toolButtons.push(row);
12265
12741
  }
12266
- toolButtons.push([{ label: "Reset to defaults (all on)", data: "tool:reset" }]);
12742
+ toolButtons.push([{ label: "Reset to defaults (all on)", data: "tool:reset", style: "danger" }]);
12267
12743
  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)." : "";
12268
12744
  await channel.sendKeyboard(
12269
12745
  chatId,
@@ -12279,13 +12755,31 @@ Tap to toggle:`,
12279
12755
  }
12280
12756
  case "new":
12281
12757
  case "newchat": {
12758
+ const oldSessionId = getSessionId(chatId);
12759
+ const exchangeCount = getMessagePairCount(chatId);
12282
12760
  const summarized = await summarizeSession(chatId);
12283
12761
  clearSession(chatId);
12284
12762
  clearChatGeminiSlot(chatId);
12285
12763
  setSessionStartedAt(chatId);
12286
12764
  logActivity(getDb(), { chatId, source: "telegram", eventType: "config_changed", summary: "New session started", detail: { field: "session", action: "reset", summarized } });
12287
- const msg2 = summarized ? "Session summarized and saved. Fresh conversation started!" : "Fresh conversation started. What's on your mind?";
12288
- await channel.sendText(chatId, msg2, "plain");
12765
+ if (typeof channel.sendKeyboard === "function" && oldSessionId) {
12766
+ const msg2 = `\u2705 New session started. Previous session archived${exchangeCount > 0 ? ` (${exchangeCount} exchanges)` : ""}.`;
12767
+ const kbMsgId = await channel.sendKeyboard(chatId, msg2, [
12768
+ [{ label: "Undo", data: "newchat:undo" }]
12769
+ ]);
12770
+ if (kbMsgId) {
12771
+ const prev = pendingNewchatUndo.get(chatId);
12772
+ if (prev) clearTimeout(prev.timer);
12773
+ const timer = setTimeout(async () => {
12774
+ pendingNewchatUndo.delete(chatId);
12775
+ await channel.editText?.(chatId, kbMsgId, msg2, "plain");
12776
+ }, 3e4);
12777
+ pendingNewchatUndo.set(chatId, { oldSessionId, timer, msgId: kbMsgId });
12778
+ }
12779
+ } else {
12780
+ const msg2 = summarized ? "Session summarized and saved. Fresh conversation started!" : "Fresh conversation started. What's on your mind?";
12781
+ await channel.sendText(chatId, msg2, "plain");
12782
+ }
12289
12783
  break;
12290
12784
  }
12291
12785
  case "summarize": {
@@ -12312,12 +12806,51 @@ Tap to toggle:`,
12312
12806
  }
12313
12807
  break;
12314
12808
  }
12315
- await channel.sendText(chatId, `Summarizing current session (${pairs} exchanges)...`, "plain");
12809
+ const progressMsgId = await channel.sendTextReturningId?.(chatId, `Summarizing ${pairs} exchanges...`, "plain");
12810
+ if (!progressMsgId) {
12811
+ await channel.sendText(chatId, `Summarizing ${pairs} exchanges...`, "plain");
12812
+ }
12316
12813
  const success2 = await summarizeSession(chatId);
12317
12814
  if (success2) {
12318
- await channel.sendText(chatId, "Session summarized and saved to memory. Session continues (use /newchat to also reset).", "plain");
12815
+ const recentSummary = listSessionSummaries(1, chatId);
12816
+ const summaryId = recentSummary[0]?.id;
12817
+ const pendingIds = getLoggedChatIds().filter((id) => id !== chatId);
12818
+ const doneText = `\u2705 Session saved to memory (${pairs} exchanges).`;
12819
+ if (typeof channel.sendKeyboard === "function") {
12820
+ if (progressMsgId) {
12821
+ await channel.editText?.(chatId, progressMsgId, doneText, "plain");
12822
+ }
12823
+ const buttons = [];
12824
+ const row = [];
12825
+ if (pendingIds.length > 0) {
12826
+ row.push({ label: `Summarize All (${pendingIds.length} pending)`, data: "summ:all", style: "primary" });
12827
+ }
12828
+ if (summaryId) {
12829
+ row.push({ label: "Undo", data: "summ:undo" });
12830
+ }
12831
+ if (row.length > 0) {
12832
+ buttons.push(row);
12833
+ const kbMsgId = await channel.sendKeyboard(chatId, progressMsgId ? "" : doneText, buttons);
12834
+ if (summaryId && kbMsgId) {
12835
+ const prev = pendingSummaryUndo.get(chatId);
12836
+ if (prev) clearTimeout(prev.timer);
12837
+ const timer = setTimeout(async () => {
12838
+ pendingSummaryUndo.delete(chatId);
12839
+ await channel.editText?.(chatId, kbMsgId, `\u2705 Session saved to memory (${pairs} exchanges).`, "plain");
12840
+ }, 3e4);
12841
+ pendingSummaryUndo.set(chatId, { summaryId, timer, msgId: kbMsgId });
12842
+ }
12843
+ }
12844
+ } else {
12845
+ await channel.sendText(chatId, doneText, "plain");
12846
+ }
12319
12847
  } else {
12320
- await channel.sendText(chatId, "Summarization failed. Session log preserved for retry.", "plain");
12848
+ const failText = "Summarization failed. Session log preserved for retry.";
12849
+ if (progressMsgId) {
12850
+ await channel.editText?.(chatId, progressMsgId, failText, "plain");
12851
+ } else {
12852
+ await channel.sendText(chatId, failText, "plain");
12853
+ }
12321
12854
  }
12322
12855
  }
12323
12856
  break;
@@ -12366,7 +12899,6 @@ Tap to toggle:`,
12366
12899
  }
12367
12900
  const iStats = getIntentStats();
12368
12901
  const iTotal = iStats.chat + iStats.agentic;
12369
- const intentLine = iTotal > 0 ? `\u26A1 ${iStats.chat} chat \xB7 ${iStats.agentic} agentic (${(iStats.chat / iTotal * 100).toFixed(0)}% fast-path)` : `\u26A1 No messages classified yet`;
12370
12902
  let geminiSlotInfo = "";
12371
12903
  if ((backendId ?? "claude") === "gemini") {
12372
12904
  try {
@@ -12379,30 +12911,46 @@ Tap to toggle:`,
12379
12911
  } catch {
12380
12912
  }
12381
12913
  }
12914
+ const iQuick = iStats.chat;
12915
+ const iDeep = iStats.agentic;
12916
+ const intentLine = iTotal > 0 ? `\u26A1 ${iTotal} messages: ${iQuick} quick, ${iDeep} deep` : `\u26A1 No messages classified yet`;
12382
12917
  const lines = [
12383
12918
  `\u{1F43E} CC-Claw v${VERSION}`,
12384
12919
  `\u23F1 Uptime: ${uptimeStr}`,
12385
12920
  ``,
12386
- `\u2501\u2501 Engine \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`,
12921
+ buildSectionHeader("Engine"),
12387
12922
  `\u{1F9E0} ${adapter?.displayName ?? backendId ?? "not set"} \xB7 ${formatModelShort(model2)}${geminiSlotInfo}`,
12388
12923
  `\u{1F4AD} Think: ${thinking2} \xB7 Mode: ${mode}`,
12389
12924
  `\u{1F916} Agents: ${getAgentMode(chatId)}`,
12390
12925
  `\u{1F507} Voice: ${voice2 ? "on" : "off"} \xB7 Sig: ${modelSig}`,
12391
12926
  ``,
12392
- `\u2501\u2501 Session \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`,
12927
+ buildSectionHeader("Session"),
12393
12928
  `\u{1F4CB} ${sessionId ? sessionId.slice(0, 12) + "..." : "no active session"}`,
12394
12929
  `\u{1F4C1} ${cwd ?? "default workspace"}`,
12395
12930
  `\u{1F4D0} Context: ${ctxBar} ${usedK}K/${maxK}K (${contextPct.toFixed(1)}%)`,
12396
12931
  ``,
12397
- `\u2501\u2501 Usage \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`,
12932
+ buildSectionHeader("Usage"),
12398
12933
  `\u{1F4E8} ${usage2.request_count} requests \xB7 ${(usage2.input_tokens / 1e3).toFixed(0)}K in \xB7 ${(usage2.output_tokens / 1e3).toFixed(0)}K out`,
12399
12934
  `\u{1F4BE} ${(usage2.cache_read_tokens / 1e3).toFixed(0)}K cached`,
12400
12935
  ...limitsLine ? [limitsLine] : [],
12401
12936
  ``,
12402
- `\u2501\u2501 Intelligence \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`,
12937
+ buildSectionHeader("Activity"),
12403
12938
  intentLine
12404
12939
  ];
12405
- await channel.sendText(chatId, lines.join("\n"), "plain");
12940
+ if (typeof channel.sendKeyboard === "function") {
12941
+ await channel.sendKeyboard(chatId, lines.join("\n"), [
12942
+ [
12943
+ { label: "Switch Backend", data: "menu:backend", style: "primary" },
12944
+ { label: "Switch Model", data: "menu:model", style: "primary" }
12945
+ ],
12946
+ [
12947
+ { label: "Change Mode", data: "menu:permissions" },
12948
+ { label: "Change Style", data: "menu:style" }
12949
+ ]
12950
+ ]);
12951
+ } else {
12952
+ await channel.sendText(chatId, lines.join("\n"), "plain");
12953
+ }
12406
12954
  break;
12407
12955
  }
12408
12956
  case "backend": {
@@ -12416,7 +12964,8 @@ Tap to toggle:`,
12416
12964
  if (typeof channel.sendKeyboard === "function") {
12417
12965
  const buttons = adapters2.map((a) => [{
12418
12966
  label: `${a.id === currentBackend ? "\u2713 " : ""}${a.displayName}`,
12419
- data: `backend:${a.id}`
12967
+ data: `backend:${a.id}`,
12968
+ ...a.id === currentBackend ? { style: "primary" } : {}
12420
12969
  }]);
12421
12970
  await channel.sendKeyboard(chatId, "Select AI backend:", buttons);
12422
12971
  } else {
@@ -12502,7 +13051,8 @@ Use: /gemini_accounts <name> to pin`, "plain");
12502
13051
  const tag = info.thinking === "adjustable" ? " \u26A1" : "";
12503
13052
  return [{
12504
13053
  label: `${id === current ? "\u2713 " : ""}${info.label}${tag}`,
12505
- data: `model:${id}`
13054
+ data: `model:${id}`,
13055
+ ...id === current ? { style: "primary" } : {}
12506
13056
  }];
12507
13057
  });
12508
13058
  await channel.sendKeyboard(chatId, `Models for ${adapter.displayName}:`, buttons);
@@ -12524,15 +13074,18 @@ Use: /gemini_accounts <name> to pin`, "plain");
12524
13074
  const current = getSummarizer(chatId);
12525
13075
  const currentLabel = current.backend === "off" ? "Off" : current.backend ? `${current.backend}:${current.model ?? "default"}` : `Auto (${adapter?.summarizerModel ?? "default"})`;
12526
13076
  if (typeof channel.sendKeyboard === "function") {
13077
+ const isAuto = !current.backend && current.backend !== "off";
13078
+ const isOff = current.backend === "off";
12527
13079
  const buttons = [
12528
- [{ label: `Auto (use active backend)${!current.backend && current.backend !== "off" ? " \u2713" : ""}`, data: "summarizer:auto" }],
12529
- [{ label: `Off (disable)${current.backend === "off" ? " \u2713" : ""}`, data: "summarizer:off" }]
13080
+ [{ label: `Auto (use active backend)${isAuto ? " \u2713" : ""}`, data: "summarizer:auto", ...isAuto ? { style: "primary" } : {} }],
13081
+ [{ label: `Off (disable)${isOff ? " \u2713" : ""}`, data: "summarizer:off", ...isOff ? { style: "primary" } : {} }]
12530
13082
  ];
12531
13083
  for (const a of getAllAdapters()) {
12532
13084
  const pinned = current.backend === a.id;
12533
13085
  buttons.push([{
12534
13086
  label: `${pinned ? "\u2713 " : ""}${a.displayName}: ${a.summarizerModel}`,
12535
- data: `summarizer:${a.id}:${a.summarizerModel}`
13087
+ data: `summarizer:${a.id}:${a.summarizerModel}`,
13088
+ ...pinned ? { style: "primary" } : {}
12536
13089
  }]);
12537
13090
  }
12538
13091
  await channel.sendKeyboard(chatId, `Session summarizer (current: ${currentLabel}):`, buttons);
@@ -12543,112 +13096,37 @@ Use /summarizer auto, /summarizer off, or /summarizer <backend>:<model>`, "plain
12543
13096
  }
12544
13097
  break;
12545
13098
  }
12546
- case "cost": {
12547
- const pricing = {};
12548
- for (const a of getAllAdapters()) Object.assign(pricing, a.pricing);
12549
- const defaultPricing = { in: 3, out: 15, cache: 0.3 };
12550
- const fmt = (n) => `$${n.toFixed(4)}`;
12551
- if (commandArgs === "all") {
12552
- const allUsage = getAllTimeUsage();
12553
- if (allUsage.length === 0) {
12554
- await channel.sendText(chatId, "No usage history recorded yet.", "plain");
12555
- break;
12556
- }
12557
- const lines = ["All-time cost breakdown by model:", ""];
12558
- let grandTotal = 0;
12559
- let grandRequests = 0;
12560
- for (const u of allUsage) {
12561
- const p = pricing[u.model] ?? defaultPricing;
12562
- const costIn = u.input_tokens / 1e6 * p.in;
12563
- const costOut = u.output_tokens / 1e6 * p.out;
12564
- const costCache = u.cache_read_tokens / 1e6 * p.cache;
12565
- const modelTotal = costIn + costOut + costCache;
12566
- grandTotal += modelTotal;
12567
- grandRequests += u.request_count;
12568
- lines.push(
12569
- `${u.model}:`,
12570
- ` Requests: ${u.request_count.toLocaleString()}`,
12571
- ` Input: ${u.input_tokens.toLocaleString()} \u2192 ${fmt(costIn)}`,
12572
- ` Output: ${u.output_tokens.toLocaleString()} \u2192 ${fmt(costOut)}`,
12573
- ` Cache read: ${u.cache_read_tokens.toLocaleString()} \u2192 ${fmt(costCache)}`,
12574
- ` Subtotal: ${fmt(modelTotal)}`,
12575
- ""
12576
- );
12577
- }
12578
- lines.push(
12579
- `Grand total: ${fmt(grandTotal)} (${grandRequests.toLocaleString()} requests)`,
12580
- "",
12581
- "Estimates based on API pricing. Actual costs may differ if you're on a subscription plan."
12582
- );
12583
- await channel.sendText(chatId, lines.join("\n"), "plain");
12584
- } else {
12585
- const sessionStart = getSessionStartedAt(chatId);
12586
- const chatModelUsage = getChatUsageByModel(chatId, sessionStart);
12587
- if (chatModelUsage.length > 0) {
12588
- const sessionLabel = sessionStart ? "this session" : "this chat";
12589
- const lines = [`Cost estimate for ${sessionLabel}:`, ""];
12590
- let grandTotal = 0;
12591
- let grandRequests = 0;
12592
- for (const u of chatModelUsage) {
12593
- const p = pricing[u.model] ?? defaultPricing;
12594
- const costIn = u.input_tokens / 1e6 * p.in;
12595
- const costOut = u.output_tokens / 1e6 * p.out;
12596
- const costCache = u.cache_read_tokens / 1e6 * p.cache;
12597
- const modelTotal = costIn + costOut + costCache;
12598
- grandTotal += modelTotal;
12599
- grandRequests += u.request_count;
12600
- lines.push(
12601
- `${u.model}:`,
12602
- ` Requests: ${u.request_count.toLocaleString()}`,
12603
- ` Input: ${u.input_tokens.toLocaleString()} \u2192 ${fmt(costIn)}`,
12604
- ` Output: ${u.output_tokens.toLocaleString()} \u2192 ${fmt(costOut)}`,
12605
- ` Cache read: ${u.cache_read_tokens.toLocaleString()} \u2192 ${fmt(costCache)}`,
12606
- ` Subtotal: ${fmt(modelTotal)}`,
12607
- ""
12608
- );
13099
+ case "cost":
13100
+ case "limits":
13101
+ case "usage": {
13102
+ if (commandArgs && command === "limits") {
13103
+ const parts = commandArgs.split(/\s+/);
13104
+ if (parts.length >= 3) {
13105
+ const [bk, win, val] = parts;
13106
+ if (!getAllBackendIds().includes(bk)) {
13107
+ await channel.sendText(chatId, `Unknown backend: ${bk}. Available: ${getAllBackendIds().join(", ")}`, "plain");
13108
+ break;
12609
13109
  }
12610
- lines.push(
12611
- `Total est.: ${fmt(grandTotal)} (${grandRequests.toLocaleString()} requests)`,
12612
- "",
12613
- "Use /cost all for all-time totals across all chats.",
12614
- "Estimates based on API pricing. Actual costs may differ if you're on a subscription plan."
12615
- );
12616
- await channel.sendText(chatId, lines.join("\n"), "plain");
12617
- } else if (sessionStart) {
12618
- await channel.sendText(chatId, "No usage recorded this session yet.\nUse /cost all for all-time totals.", "plain");
12619
- } else {
12620
- const usage2 = getUsage(chatId);
12621
- if (usage2.request_count === 0) {
12622
- await channel.sendText(chatId, "No usage recorded for this chat yet.", "plain");
13110
+ if (!["hourly", "daily", "weekly"].includes(win)) {
13111
+ await channel.sendText(chatId, "Window must be: hourly, daily, or weekly", "plain");
13112
+ break;
13113
+ }
13114
+ if (val === "off") {
13115
+ clearBackendLimit(bk, win);
13116
+ await channel.sendText(chatId, `Limit removed for ${bk} (${win}).`, "plain");
12623
13117
  } else {
12624
- let costModel;
12625
- try {
12626
- costModel = getModel(chatId) ?? getAdapterForChat(chatId).defaultModel;
12627
- } catch {
12628
- costModel = getModel(chatId) ?? "unknown";
13118
+ const tokens = parseInt(val, 10);
13119
+ if (isNaN(tokens) || tokens <= 0) {
13120
+ await channel.sendText(chatId, "Token limit must be a positive number.", "plain");
13121
+ break;
12629
13122
  }
12630
- const p = pricing[costModel] ?? defaultPricing;
12631
- const costIn = usage2.input_tokens / 1e6 * p.in;
12632
- const costOut = usage2.output_tokens / 1e6 * p.out;
12633
- const costCache = usage2.cache_read_tokens / 1e6 * p.cache;
12634
- const totalCost = costIn + costOut + costCache;
12635
- const lines = [
12636
- `Cost estimate for this chat (${costModel}):`,
12637
- "",
12638
- `Requests: ${usage2.request_count}`,
12639
- `Input: ${usage2.input_tokens.toLocaleString()} tokens \u2192 ${fmt(costIn)}`,
12640
- `Output: ${usage2.output_tokens.toLocaleString()} tokens \u2192 ${fmt(costOut)}`,
12641
- `Cache read: ${usage2.cache_read_tokens.toLocaleString()} tokens \u2192 ${fmt(costCache)}`,
12642
- "",
12643
- `Total est.: ${fmt(totalCost)}`,
12644
- "",
12645
- "Per-model breakdown unavailable (legacy data).",
12646
- "Estimates based on API pricing. Actual costs may differ if you're on a subscription plan."
12647
- ];
12648
- await channel.sendText(chatId, lines.join("\n"), "plain");
13123
+ setBackendLimit(bk, win, tokens);
13124
+ await channel.sendText(chatId, `Limit set: ${bk} (${win}) = ${(tokens / 1e3).toFixed(0)}K input tokens`, "plain");
12649
13125
  }
13126
+ break;
12650
13127
  }
12651
13128
  }
13129
+ await sendUnifiedUsage(chatId, channel, "session");
12652
13130
  break;
12653
13131
  }
12654
13132
  case "cwd": {
@@ -12751,15 +13229,37 @@ ${lines.join("\n")}`, "plain");
12751
13229
  break;
12752
13230
  }
12753
13231
  case "memory": {
13232
+ if (commandArgs?.startsWith("edit ")) {
13233
+ const editMatch = commandArgs.match(/^edit\s+(\d+)\s+(.+)/);
13234
+ if (!editMatch) {
13235
+ await channel.sendText(chatId, "Usage: /memory edit <id> <new content>", "plain");
13236
+ break;
13237
+ }
13238
+ const editId = parseInt(editMatch[1], 10);
13239
+ const newContent = editMatch[2];
13240
+ const mem = getMemoryById(editId);
13241
+ if (!mem) {
13242
+ await channel.sendText(chatId, "Memory not found.", "plain");
13243
+ break;
13244
+ }
13245
+ deleteMemoryById(editId);
13246
+ saveMemoryWithEmbedding(mem.trigger, newContent, mem.type);
13247
+ await channel.sendText(chatId, `Memory #${editId} updated.`, "plain");
13248
+ break;
13249
+ }
12754
13250
  const memories = listMemories();
12755
13251
  if (memories.length === 0) {
12756
13252
  await channel.sendText(chatId, "No memories stored yet.", "plain");
12757
13253
  return;
12758
13254
  }
12759
- const lines = memories.map(
12760
- (m) => `- ${m.trigger}: ${m.content} (salience: ${m.salience.toFixed(2)})`
12761
- );
12762
- await channel.sendText(chatId, lines.join("\n"), "plain");
13255
+ if (typeof channel.sendKeyboard === "function") {
13256
+ await sendMemoryPage(chatId, channel, 1);
13257
+ } else {
13258
+ const lines = memories.map(
13259
+ (m) => `- ${m.trigger}: ${m.content} (salience: ${m.salience.toFixed(2)})`
13260
+ );
13261
+ await channel.sendText(chatId, lines.join("\n"), "plain");
13262
+ }
12763
13263
  break;
12764
13264
  }
12765
13265
  case "remember": {
@@ -12771,6 +13271,11 @@ ${lines.join("\n")}`, "plain");
12771
13271
  const trigger = content.split(/\s+/).slice(0, 3).join(" ");
12772
13272
  saveMemoryWithEmbedding(trigger, content, "semantic");
12773
13273
  await channel.sendText(chatId, "Got it, I'll remember that.", "plain");
13274
+ if (typeof channel.sendKeyboard === "function") {
13275
+ await channel.sendKeyboard(chatId, "", [
13276
+ [{ label: "\u{1F4DA} View Memories", data: "menu:memory" }]
13277
+ ]);
13278
+ }
12774
13279
  break;
12775
13280
  }
12776
13281
  case "forget": {
@@ -12791,8 +13296,8 @@ ${lines.join("\n")}`, "plain");
12791
13296
  if (typeof channel.sendKeyboard === "function") {
12792
13297
  await channel.sendKeyboard(chatId, `\u{1F3A7} Voice responses: ${vcEnabled ? "ON" : "OFF"}`, [
12793
13298
  [
12794
- { label: `${vcEnabled ? "" : "\u2713 "}\u{1F507} Off`, data: "voice:off" },
12795
- { label: `${vcEnabled ? "\u2713 " : ""}\u{1F50A} On`, data: "voice:on" }
13299
+ { label: `${vcEnabled ? "" : "\u2713 "}\u{1F507} Off`, data: "voice:off", ...!vcEnabled ? { style: "danger" } : {} },
13300
+ { label: `${vcEnabled ? "\u2713 " : ""}\u{1F50A} On`, data: "voice:on", ...vcEnabled ? { style: "success" } : {} }
12796
13301
  ]
12797
13302
  ]);
12798
13303
  } else {
@@ -12810,9 +13315,9 @@ ${lines.join("\n")}`, "plain");
12810
13315
  if (typeof channel.sendKeyboard === "function") {
12811
13316
  await channel.sendKeyboard(chatId, "\u{1F5E3}\uFE0F AI Response Style:", [
12812
13317
  [
12813
- { label: `${currentStyle === "concise" ? "\u2713 " : ""}Concise`, data: "style:concise" },
12814
- { label: `${currentStyle === "normal" ? "\u2713 " : ""}Normal`, data: "style:normal" },
12815
- { label: `${currentStyle === "detailed" ? "\u2713 " : ""}Detailed`, data: "style:detailed" }
13318
+ { label: `${currentStyle === "concise" ? "\u2713 " : ""}Concise`, data: "style:concise", ...currentStyle === "concise" ? { style: "primary" } : {} },
13319
+ { label: `${currentStyle === "normal" ? "\u2713 " : ""}Normal`, data: "style:normal", ...currentStyle === "normal" ? { style: "primary" } : {} },
13320
+ { label: `${currentStyle === "detailed" ? "\u2713 " : ""}Detailed`, data: "style:detailed", ...currentStyle === "detailed" ? { style: "primary" } : {} }
12816
13321
  ]
12817
13322
  ]);
12818
13323
  } else {
@@ -12826,8 +13331,8 @@ ${lines.join("\n")}`, "plain");
12826
13331
  await channel.sendKeyboard(chatId, `Model Signature: ${currentSig === "on" ? "ON" : "OFF"}
12827
13332
  Appends model + thinking level to each response.`, [
12828
13333
  [
12829
- { label: currentSig === "on" ? "\u2713 On" : "On", data: "model_sig:on" },
12830
- { label: currentSig !== "on" ? "\u2713 Off" : "Off", data: "model_sig:off" }
13334
+ { label: currentSig === "on" ? "\u2713 On" : "On", data: "model_sig:on", ...currentSig === "on" ? { style: "success" } : {} },
13335
+ { label: currentSig !== "on" ? "\u2713 Off" : "Off", data: "model_sig:off", ...currentSig !== "on" ? { style: "danger" } : {} }
12831
13336
  ]
12832
13337
  ]);
12833
13338
  } else {
@@ -12874,32 +13379,12 @@ Appends model + thinking level to each response.`, [
12874
13379
  break;
12875
13380
  }
12876
13381
  case "jobs": {
12877
- const jobs = listJobs();
12878
- if (jobs.length === 0) {
12879
- await channel.sendText(chatId, "No scheduled jobs.", "plain");
12880
- return;
12881
- }
12882
- const lines = jobs.map((j) => {
12883
- const status = !j.active ? "cancelled" : !j.enabled ? "paused" : "active";
12884
- const schedule2 = j.cron ?? (j.atTime ? `at ${j.atTime}` : j.everyMs ? `every ${j.everyMs / 1e3}s` : "?");
12885
- const tz = j.timezone !== "UTC" ? ` (${j.timezone})` : "";
12886
- const backend2 = j.backend ?? "default";
12887
- const model2 = j.model ?? "default";
12888
- const failures = j.consecutiveFailures > 0 ? ` [${j.consecutiveFailures} failures]` : "";
12889
- const lastRun = j.lastRunAt ? `
12890
- Last: ${j.lastRunAt}` : "";
12891
- const nextRun = j.nextRunAt ? `
12892
- Next: ${j.nextRunAt}` : "";
12893
- return `#${j.id} [${status}] ${schedule2}${tz}
12894
- ${j.description}
12895
- Backend: ${backend2} / ${model2}${failures}${lastRun}${nextRun}`;
12896
- });
12897
- await channel.sendText(chatId, lines.join("\n\n"), "plain");
13382
+ await sendJobsBoard(chatId, channel, 1);
12898
13383
  break;
12899
13384
  }
12900
13385
  case "cancel": {
12901
13386
  if (!commandArgs) {
12902
- await channel.sendText(chatId, "Usage: /cancel <job-id>", "plain");
13387
+ await sendJobPicker(chatId, channel, "cancel");
12903
13388
  return;
12904
13389
  }
12905
13390
  const id = parseInt(commandArgs, 10);
@@ -12913,7 +13398,7 @@ Appends model + thinking level to each response.`, [
12913
13398
  }
12914
13399
  case "pause": {
12915
13400
  if (!commandArgs) {
12916
- await channel.sendText(chatId, "Usage: /pause <job-id>", "plain");
13401
+ await sendJobPicker(chatId, channel, "pause");
12917
13402
  return;
12918
13403
  }
12919
13404
  const pauseId = parseInt(commandArgs, 10);
@@ -12927,7 +13412,7 @@ Appends model + thinking level to each response.`, [
12927
13412
  }
12928
13413
  case "resume": {
12929
13414
  if (!commandArgs) {
12930
- await channel.sendText(chatId, "Usage: /resume <job-id>", "plain");
13415
+ await sendJobPicker(chatId, channel, "resume");
12931
13416
  return;
12932
13417
  }
12933
13418
  const resumeId = parseInt(commandArgs, 10);
@@ -12941,7 +13426,7 @@ Appends model + thinking level to each response.`, [
12941
13426
  }
12942
13427
  case "run": {
12943
13428
  if (!commandArgs) {
12944
- await channel.sendText(chatId, "Usage: /run <job-id>", "plain");
13429
+ await sendJobPicker(chatId, channel, "run");
12945
13430
  return;
12946
13431
  }
12947
13432
  const runJobId = parseInt(commandArgs, 10);
@@ -12952,20 +13437,24 @@ Appends model + thinking level to each response.`, [
12952
13437
  }
12953
13438
  case "runs": {
12954
13439
  const runsJobId = commandArgs ? parseInt(commandArgs, 10) : void 0;
12955
- const runs = getJobRuns(runsJobId, 10);
12956
- if (runs.length === 0) {
12957
- await channel.sendText(chatId, runsJobId ? `No runs for job #${runsJobId}.` : "No run history yet.", "plain");
12958
- return;
12959
- }
12960
- const lines = runs.map((r) => {
12961
- const duration = r.durationMs ? ` (${(r.durationMs / 1e3).toFixed(1)}s)` : "";
12962
- const error3 = r.error ? `
13440
+ if (runsJobId) {
13441
+ await sendJobRunsView(chatId, runsJobId, channel, 1);
13442
+ } else {
13443
+ const runs = getJobRuns(void 0, 10);
13444
+ if (runs.length === 0) {
13445
+ await channel.sendText(chatId, "No run history yet.", "plain");
13446
+ return;
13447
+ }
13448
+ const lines = runs.map((r) => {
13449
+ const duration = r.durationMs ? ` (${(r.durationMs / 1e3).toFixed(1)}s)` : "";
13450
+ const error3 = r.error ? `
12963
13451
  Error: ${r.error.slice(0, 100)}` : "";
12964
- const usage2 = r.usageInput ? `
13452
+ const usage2 = r.usageInput ? `
12965
13453
  Tokens: ${r.usageInput}in / ${r.usageOutput}out` : "";
12966
- return `#${r.jobId} [${r.status}] ${formatLocalDateTime(r.startedAt)}${duration}${error3}${usage2}`;
12967
- });
12968
- await channel.sendText(chatId, lines.join("\n\n"), "plain");
13454
+ return `#${r.jobId} [${r.status}] ${formatLocalDateTime(r.startedAt)}${duration}${error3}${usage2}`;
13455
+ });
13456
+ await channel.sendText(chatId, lines.join("\n\n"), "plain");
13457
+ }
12969
13458
  break;
12970
13459
  }
12971
13460
  case "editjob": {
@@ -12980,6 +13469,14 @@ Appends model + thinking level to each response.`, [
12980
13469
  case "health": {
12981
13470
  const report = getHealthReport();
12982
13471
  await channel.sendText(chatId, formatHealthReport(report), "plain");
13472
+ if (typeof channel.sendKeyboard === "function") {
13473
+ await channel.sendKeyboard(chatId, "", [
13474
+ [
13475
+ { label: "\u{1F504} Refresh", data: "menu:health" },
13476
+ { label: "\u{1F4CB} View Jobs", data: "menu:jobs" }
13477
+ ]
13478
+ ]);
13479
+ }
12983
13480
  break;
12984
13481
  }
12985
13482
  case "history": {
@@ -13000,20 +13497,24 @@ Appends model + thinking level to each response.`, [
13000
13497
  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");
13001
13498
  await channel.sendText(chatId, lines.join("\n"), "plain");
13002
13499
  } else {
13003
- const rows = getRecentMessageLog(chatId, 20).reverse();
13004
- if (rows.length === 0) {
13005
- await channel.sendText(chatId, "No conversation history found.", "plain");
13006
- break;
13007
- }
13008
- 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"];
13009
- for (const row of rows) {
13010
- const roleLabel = row.role === "user" ? "You" : "Assistant";
13011
- const date = row.created_at.slice(0, 10);
13012
- const preview = row.content.slice(0, 300) + (row.content.length > 300 ? "\u2026" : "");
13013
- lines.push(`[${row.backend ?? "?"} \xB7 ${date}] ${roleLabel}: ${preview}`);
13500
+ if (typeof channel.sendKeyboard === "function") {
13501
+ await sendHistoryView(chatId, channel, {});
13502
+ } else {
13503
+ const rows = getRecentMessageLog(chatId, 20).reverse();
13504
+ if (rows.length === 0) {
13505
+ await channel.sendText(chatId, "No conversation history found.", "plain");
13506
+ break;
13507
+ }
13508
+ 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"];
13509
+ for (const row of rows) {
13510
+ const roleLabel = row.role === "user" ? "You" : "Assistant";
13511
+ const date = row.created_at.slice(0, 10);
13512
+ const preview = row.content.slice(0, 300) + (row.content.length > 300 ? "\u2026" : "");
13513
+ lines.push(`[${row.backend ?? "?"} \xB7 ${date}] ${roleLabel}: ${preview}`);
13514
+ }
13515
+ 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");
13516
+ await channel.sendText(chatId, lines.join("\n"), "plain");
13014
13517
  }
13015
- 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");
13016
- await channel.sendText(chatId, lines.join("\n"), "plain");
13017
13518
  }
13018
13519
  break;
13019
13520
  }
@@ -13049,11 +13550,34 @@ Use /skills to see it.`, "plain");
13049
13550
  case "chats": {
13050
13551
  if (!commandArgs) {
13051
13552
  const aliases = getAllChatAliases();
13052
- if (aliases.length === 0) {
13053
- await channel.sendText(chatId, "No chat aliases configured yet.\nUsage: /chats name <chat_id> <alias>\n\nGroup chats are auto-discovered when the bot receives a message from them.", "plain");
13054
- } else {
13055
- const lines = ["Authorized chats:", "", ...aliases.map((a) => ` ${a.alias} -> ${a.chatId}`)];
13056
- await channel.sendText(chatId, lines.join("\n"), "plain");
13553
+ if (typeof channel.sendKeyboard === "function") {
13554
+ if (aliases.length === 0) {
13555
+ await channel.sendKeyboard(
13556
+ chatId,
13557
+ "Authorized Chats\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n\nNo chat aliases configured yet.\nGroup chats are auto-discovered when the bot receives a message.",
13558
+ [[{ label: "Refresh", data: "chats:refresh", style: "primary" }]]
13559
+ );
13560
+ } else {
13561
+ const lines = aliases.map(
13562
+ (a) => `${a.alias.padEnd(12)} | ${a.chatId}${a.chatId === chatId ? " [Active]" : ""}`
13563
+ );
13564
+ const text = ["Authorized Chats", "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501", "", ...lines].join("\n");
13565
+ const buttons = [];
13566
+ const actionRow = [];
13567
+ if (aliases.length > 0) {
13568
+ actionRow.push({ label: "Remove", data: "chats:remove", style: "danger" });
13569
+ }
13570
+ actionRow.push({ label: "Refresh", data: "chats:refresh", style: "primary" });
13571
+ buttons.push(actionRow);
13572
+ await channel.sendKeyboard(chatId, text, buttons);
13573
+ }
13574
+ } else {
13575
+ if (aliases.length === 0) {
13576
+ await channel.sendText(chatId, "No chat aliases configured yet.\nUsage: /chats name <chat_id> <alias>\n\nGroup chats are auto-discovered when the bot receives a message from them.", "plain");
13577
+ } else {
13578
+ const lines = ["Authorized chats:", "", ...aliases.map((a) => ` ${a.alias} -> ${a.chatId}`)];
13579
+ await channel.sendText(chatId, lines.join("\n"), "plain");
13580
+ }
13057
13581
  }
13058
13582
  break;
13059
13583
  }
@@ -13072,117 +13596,79 @@ Use /skills to see it.`, "plain");
13072
13596
  break;
13073
13597
  }
13074
13598
  case "heartbeat": {
13075
- if (!commandArgs) {
13076
- await channel.sendText(chatId, formatHeartbeatStatus(chatId), "plain");
13077
- break;
13078
- }
13079
- const { action, value } = parseHeartbeatCommand(chatId, commandArgs);
13080
- switch (action) {
13081
- case "on": {
13082
- setHeartbeatConfig(chatId, { enabled: true });
13083
- startHeartbeatForChat(chatId);
13084
- await channel.sendText(chatId, "Heartbeat enabled. I'll check in periodically.", "plain");
13085
- break;
13086
- }
13087
- case "off": {
13088
- setHeartbeatConfig(chatId, { enabled: false });
13089
- stopHeartbeatForChat(chatId);
13090
- await channel.sendText(chatId, "Heartbeat disabled.", "plain");
13091
- break;
13092
- }
13093
- case "interval": {
13094
- const ms = parseIntervalToMs(value ?? "30m");
13095
- if (!ms || ms < 6e4) {
13096
- await channel.sendText(chatId, "Invalid interval. Use e.g. 15m, 30m, 1h, 2h", "plain");
13599
+ if (commandArgs) {
13600
+ const { action, value } = parseHeartbeatCommand(chatId, commandArgs);
13601
+ switch (action) {
13602
+ case "on": {
13603
+ setHeartbeatConfig(chatId, { enabled: true });
13604
+ startHeartbeatForChat(chatId);
13605
+ if (typeof channel.sendKeyboard === "function") {
13606
+ await sendHeartbeatKeyboard(chatId, channel);
13607
+ } else {
13608
+ await channel.sendText(chatId, "Heartbeat enabled. I'll check in periodically.", "plain");
13609
+ }
13097
13610
  break;
13098
13611
  }
13099
- setHeartbeatConfig(chatId, { intervalMs: ms });
13100
- stopHeartbeatForChat(chatId);
13101
- const hbConf = getHeartbeatConfig(chatId);
13102
- if (hbConf?.enabled) startHeartbeatForChat(chatId);
13103
- await channel.sendText(chatId, `Heartbeat interval set to ${ms / 6e4} minutes.`, "plain");
13104
- break;
13105
- }
13106
- case "hours": {
13107
- const hourMatch = (value ?? "").match(/^(\d{1,2})-(\d{1,2})$/);
13108
- if (!hourMatch) {
13109
- await channel.sendText(chatId, "Usage: /heartbeat hours 8-22", "plain");
13612
+ case "off": {
13613
+ setHeartbeatConfig(chatId, { enabled: false });
13614
+ stopHeartbeatForChat(chatId);
13615
+ if (typeof channel.sendKeyboard === "function") {
13616
+ await sendHeartbeatKeyboard(chatId, channel);
13617
+ } else {
13618
+ await channel.sendText(chatId, "Heartbeat disabled.", "plain");
13619
+ }
13110
13620
  break;
13111
13621
  }
13112
- const start = `${hourMatch[1].padStart(2, "0")}:00`;
13113
- const end = `${hourMatch[2].padStart(2, "0")}:00`;
13114
- setHeartbeatConfig(chatId, { activeStart: start, activeEnd: end });
13115
- await channel.sendText(chatId, `Active hours: ${start} - ${end}`, "plain");
13116
- break;
13117
- }
13118
- default:
13119
- await channel.sendText(chatId, formatHeartbeatStatus(chatId), "plain");
13120
- }
13121
- break;
13122
- }
13123
- case "usage": {
13124
- const backendIds = getAllBackendIds();
13125
- const lines = ["Backend usage (last 24h):", ""];
13126
- for (const bid of backendIds) {
13127
- const u = getBackendUsageInWindow(bid, "daily");
13128
- const a = getAdapter(bid);
13129
- lines.push(` ${a.displayName}: ${(u.input_tokens / 1e3).toFixed(1)}K in / ${(u.output_tokens / 1e3).toFixed(1)}K out (${u.request_count} requests)`);
13130
- }
13131
- const limits2 = getAllBackendLimits();
13132
- if (limits2.length > 0) {
13133
- lines.push("", "Limits:");
13134
- for (const lim of limits2) {
13135
- if (!lim.max_input_tokens) continue;
13136
- const u = getBackendUsageInWindow(lim.backend, lim.window);
13137
- const pct2 = (u.input_tokens / lim.max_input_tokens * 100).toFixed(0);
13138
- const warn2 = u.input_tokens / lim.max_input_tokens >= lim.warn_pct;
13139
- lines.push(` ${lim.backend} (${lim.window}): ${pct2}% of ${(lim.max_input_tokens / 1e3).toFixed(0)}K${warn2 ? " \u26A0\uFE0F" : ""}`);
13140
- }
13141
- }
13142
- await channel.sendText(chatId, lines.join("\n"), "plain");
13143
- break;
13144
- }
13145
- case "limits": {
13146
- if (!commandArgs) {
13147
- const limits2 = getAllBackendLimits();
13148
- if (limits2.length === 0) {
13149
- await channel.sendText(chatId, "No usage limits set.\n\nUsage: /limits <backend> <window> <tokens>\nExample: /limits claude daily 500000\nTo remove: /limits claude daily off", "plain");
13150
- } else {
13151
- const lines = limits2.map(
13152
- (l) => `${l.backend} (${l.window}): ${l.max_input_tokens ? `${(l.max_input_tokens / 1e3).toFixed(0)}K input tokens` : "no limit"}`
13153
- );
13154
- await channel.sendText(chatId, ["Current limits:", "", ...lines].join("\n"), "plain");
13622
+ case "interval": {
13623
+ const ms = parseIntervalToMs(value ?? "30m");
13624
+ if (!ms || ms < 6e4) {
13625
+ await channel.sendText(chatId, "Invalid interval. Use e.g. 15m, 30m, 1h, 2h", "plain");
13626
+ break;
13627
+ }
13628
+ setHeartbeatConfig(chatId, { intervalMs: ms });
13629
+ stopHeartbeatForChat(chatId);
13630
+ const hbConf = getHeartbeatConfig(chatId);
13631
+ if (hbConf?.enabled) startHeartbeatForChat(chatId);
13632
+ if (typeof channel.sendKeyboard === "function") {
13633
+ await sendHeartbeatKeyboard(chatId, channel);
13634
+ } else {
13635
+ await channel.sendText(chatId, `Heartbeat interval set to ${ms / 6e4} minutes.`, "plain");
13636
+ }
13637
+ break;
13638
+ }
13639
+ case "hours": {
13640
+ const hourMatch = (value ?? "").match(/^(\d{1,2})-(\d{1,2})$/);
13641
+ if (!hourMatch) {
13642
+ await channel.sendText(chatId, "Usage: /heartbeat hours 8-22", "plain");
13643
+ break;
13644
+ }
13645
+ const hbStart = `${hourMatch[1].padStart(2, "0")}:00`;
13646
+ const hbEnd = `${hourMatch[2].padStart(2, "0")}:00`;
13647
+ setHeartbeatConfig(chatId, { activeStart: hbStart, activeEnd: hbEnd });
13648
+ if (typeof channel.sendKeyboard === "function") {
13649
+ await sendHeartbeatKeyboard(chatId, channel);
13650
+ } else {
13651
+ await channel.sendText(chatId, `Active hours: ${hbStart} - ${hbEnd}`, "plain");
13652
+ }
13653
+ break;
13654
+ }
13655
+ default:
13656
+ if (typeof channel.sendKeyboard === "function") {
13657
+ await sendHeartbeatKeyboard(chatId, channel);
13658
+ } else {
13659
+ await channel.sendText(chatId, formatHeartbeatStatus(chatId), "plain");
13660
+ }
13155
13661
  }
13156
13662
  break;
13157
13663
  }
13158
- const parts = commandArgs.split(/\s+/);
13159
- if (parts.length < 3) {
13160
- await channel.sendText(chatId, "Usage: /limits <backend> <window> <tokens|off>\nExample: /limits claude daily 500000", "plain");
13161
- break;
13162
- }
13163
- const [bk, win, val] = parts;
13164
- if (!getAllBackendIds().includes(bk)) {
13165
- await channel.sendText(chatId, `Unknown backend: ${bk}. Available: ${getAllBackendIds().join(", ")}`, "plain");
13166
- break;
13167
- }
13168
- if (!["hourly", "daily", "weekly"].includes(win)) {
13169
- await channel.sendText(chatId, "Window must be: hourly, daily, or weekly", "plain");
13170
- break;
13171
- }
13172
- if (val === "off") {
13173
- clearBackendLimit(bk, win);
13174
- await channel.sendText(chatId, `Limit removed for ${bk} (${win}).`, "plain");
13664
+ if (typeof channel.sendKeyboard === "function") {
13665
+ await sendHeartbeatKeyboard(chatId, channel);
13175
13666
  } else {
13176
- const tokens = parseInt(val, 10);
13177
- if (isNaN(tokens) || tokens <= 0) {
13178
- await channel.sendText(chatId, "Token limit must be a positive number.", "plain");
13179
- break;
13180
- }
13181
- setBackendLimit(bk, win, tokens);
13182
- await channel.sendText(chatId, `Limit set: ${bk} (${win}) = ${(tokens / 1e3).toFixed(0)}K input tokens`, "plain");
13667
+ await channel.sendText(chatId, formatHeartbeatStatus(chatId), "plain");
13183
13668
  }
13184
13669
  break;
13185
13670
  }
13671
+ /* usage + limits handled above in unified "cost"/"limits"/"usage" case */
13186
13672
  case "agents": {
13187
13673
  if (commandArgs?.startsWith("mode")) {
13188
13674
  const modeArg = commandArgs.slice(5).trim().toLowerCase();
@@ -13200,9 +13686,9 @@ Use /skills to see it.`, "plain");
13200
13686
 
13201
13687
  Choose a mode:`, [
13202
13688
  [
13203
- { label: "Auto (recommended)", data: "agentmode:auto" },
13204
- { label: "Native (fast)", data: "agentmode:native" },
13205
- { label: "Orchestrated", data: "agentmode:claw" }
13689
+ { label: "Auto (recommended)", data: "agentmode:auto", ...current === "auto" ? { style: "primary" } : {} },
13690
+ { label: "Native (fast)", data: "agentmode:native", ...current === "native" ? { style: "primary" } : {} },
13691
+ { label: "Orchestrated", data: "agentmode:claw", ...current === "claw" ? { style: "primary" } : {} }
13206
13692
  ]
13207
13693
  ]);
13208
13694
  } else {
@@ -13280,6 +13766,18 @@ ${lines2.join("\n")}`, "html");
13280
13766
  }
13281
13767
  lines.push(`Total: ${agents2.length} agent(s) (${runningCount} running, ${queuedCount} queued)`);
13282
13768
  await channel.sendText(chatId, lines.join("\n"), "html");
13769
+ if (typeof channel.sendKeyboard === "function") {
13770
+ const agentButtons = [];
13771
+ for (const a of agents2) {
13772
+ const shortId = a.id.slice(0, 8);
13773
+ const taskLabel = a.task ? a.task.slice(0, 20) : a.status;
13774
+ agentButtons.push([
13775
+ { label: `\u{1F6D1} Stop ${shortId}: ${taskLabel}`, data: `agents:stop:${shortId}`, style: "danger" }
13776
+ ]);
13777
+ }
13778
+ agentButtons.push([{ label: "\u{1F4CB} View Task Board", data: "agents:tasks" }]);
13779
+ await channel.sendKeyboard(chatId, "Agent actions:", agentButtons);
13780
+ }
13283
13781
  break;
13284
13782
  }
13285
13783
  case "tasks": {
@@ -13322,11 +13820,56 @@ ${lines2.join("\n")}`, "html");
13322
13820
  lines.push("No tasks yet.");
13323
13821
  }
13324
13822
  await channel.sendText(chatId, lines.join("\n").trimEnd(), "html");
13823
+ if (typeof channel.sendKeyboard === "function" && tasks.length > 0) {
13824
+ const taskButtons = [];
13825
+ const viewable = tasks.filter((t) => t.status === "pending" || t.status === "in_progress");
13826
+ for (const t of viewable.slice(0, 10)) {
13827
+ const statusIcon = t.status === "in_progress" ? "\u{1F504}" : "\u{1F4CB}";
13828
+ taskButtons.push([
13829
+ { label: `${statusIcon} #${t.id}: ${t.subject.slice(0, 30)}`, data: `tasks:view:${t.id}`, style: "primary" }
13830
+ ]);
13831
+ }
13832
+ if (taskButtons.length > 0) {
13833
+ await channel.sendKeyboard(chatId, "View task details:", taskButtons);
13834
+ }
13835
+ }
13325
13836
  break;
13326
13837
  }
13327
13838
  case "stopagent": {
13328
13839
  if (!commandArgs) {
13329
- await channel.sendText(chatId, "Usage: /stopagent <id>\nUse first 8 chars of agent ID or full ID.", "plain");
13840
+ const db4 = getDb();
13841
+ const agents3 = listActiveAgents(db4);
13842
+ if (agents3.length === 0) {
13843
+ await channel.sendText(chatId, "No active agents to stop.", "plain");
13844
+ break;
13845
+ }
13846
+ if (typeof channel.sendKeyboard === "function") {
13847
+ const STATUS_EMOJI_STOP = { running: "\u{1F7E2}", queued: "\u{1F7E1}", starting: "\u{1F535}", idle: "\u26AA" };
13848
+ const agentLines = agents3.map((a) => {
13849
+ const emoji = STATUS_EMOJI_STOP[a.status] ?? "\u26AA";
13850
+ const shortId = a.id.slice(0, 8);
13851
+ const taskLabel = a.task ? a.task.slice(0, 40) : "no task";
13852
+ const runtimeMs = Date.now() - (/* @__PURE__ */ new Date(a.createdAt + "Z")).getTime();
13853
+ const mins = Math.floor(runtimeMs / 6e4);
13854
+ const runtime = mins < 1 ? "<1m" : `${mins}m`;
13855
+ return `${emoji} ${shortId} \u2014 "${taskLabel}" (${a.status}, ${runtime})`;
13856
+ });
13857
+ const buttons = agents3.map((a) => {
13858
+ const shortId = a.id.slice(0, 8);
13859
+ const taskLabel = a.task ? a.task.slice(0, 20) : a.status;
13860
+ return [{ label: `${shortId}: ${taskLabel}`, data: `stopagent:pick:${shortId}`, style: "danger" }];
13861
+ });
13862
+ buttons.push([
13863
+ { label: "\u{1F6D1} Stop All", data: "stopagent:all", style: "danger" },
13864
+ { label: "Cancel", data: "stopagent:cancel" }
13865
+ ]);
13866
+ await channel.sendKeyboard(chatId, `Stop which agent?
13867
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
13868
+
13869
+ ${agentLines.join("\n")}`, buttons);
13870
+ } else {
13871
+ await channel.sendText(chatId, "Usage: /stopagent <id>\nUse first 8 chars of agent ID or full ID.", "plain");
13872
+ }
13330
13873
  break;
13331
13874
  }
13332
13875
  const db3 = getDb();
@@ -13344,6 +13887,14 @@ ${lines2.join("\n")}`, "html");
13344
13887
  case "stopall": {
13345
13888
  const count = cancelAllAgents(chatId);
13346
13889
  await channel.sendText(chatId, count > 0 ? `Cancelled ${count} agent(s).` : "No active agents to cancel.", "plain");
13890
+ if (count > 0 && typeof channel.sendKeyboard === "function") {
13891
+ await channel.sendKeyboard(chatId, "", [
13892
+ [
13893
+ { label: "\u{1F4CB} View Task Board", data: "agents:tasks" },
13894
+ { label: "\u{1F195} New Chat", data: "menu:newchat" }
13895
+ ]
13896
+ ]);
13897
+ }
13347
13898
  break;
13348
13899
  }
13349
13900
  case "runners": {
@@ -13431,19 +13982,7 @@ ${lines2.join("\n")}`, "html");
13431
13982
  }
13432
13983
  case "cron": {
13433
13984
  if (!commandArgs) {
13434
- const cronJobs = listJobs();
13435
- if (cronJobs.length === 0) {
13436
- await channel.sendText(chatId, "No scheduled jobs.\n\nCreate one: /cron <description> or /schedule <description>", "plain");
13437
- return;
13438
- }
13439
- const cronLines = cronJobs.map((j) => {
13440
- const status = !j.active ? "cancelled" : !j.enabled ? "paused" : "active";
13441
- const sched = j.cron ?? (j.atTime ? `at ${j.atTime}` : j.everyMs ? `every ${j.everyMs / 1e3}s` : "?");
13442
- const tz = j.timezone !== "UTC" ? ` (${j.timezone})` : "";
13443
- return `#${j.id} [${status}] ${sched}${tz}
13444
- ${j.description}`;
13445
- });
13446
- await channel.sendText(chatId, cronLines.join("\n\n"), "plain");
13985
+ await sendJobsBoard(chatId, channel, 1);
13447
13986
  return;
13448
13987
  }
13449
13988
  const cronParts = commandArgs.split(/\s+/);
@@ -13452,7 +13991,7 @@ ${lines2.join("\n")}`, "html");
13452
13991
  switch (cronSub) {
13453
13992
  case "cancel": {
13454
13993
  if (!cronSubArgs) {
13455
- await channel.sendText(chatId, "Usage: /cron cancel <id>", "plain");
13994
+ await sendJobPicker(chatId, channel, "cancel");
13456
13995
  return;
13457
13996
  }
13458
13997
  const id = parseInt(cronSubArgs, 10);
@@ -13462,7 +14001,7 @@ ${lines2.join("\n")}`, "html");
13462
14001
  }
13463
14002
  case "pause": {
13464
14003
  if (!cronSubArgs) {
13465
- await channel.sendText(chatId, "Usage: /cron pause <id>", "plain");
14004
+ await sendJobPicker(chatId, channel, "pause");
13466
14005
  return;
13467
14006
  }
13468
14007
  const pauseId = parseInt(cronSubArgs, 10);
@@ -13472,7 +14011,7 @@ ${lines2.join("\n")}`, "html");
13472
14011
  }
13473
14012
  case "resume": {
13474
14013
  if (!cronSubArgs) {
13475
- await channel.sendText(chatId, "Usage: /cron resume <id>", "plain");
14014
+ await sendJobPicker(chatId, channel, "resume");
13476
14015
  return;
13477
14016
  }
13478
14017
  const resumeId = parseInt(cronSubArgs, 10);
@@ -13482,7 +14021,7 @@ ${lines2.join("\n")}`, "html");
13482
14021
  }
13483
14022
  case "run": {
13484
14023
  if (!cronSubArgs) {
13485
- await channel.sendText(chatId, "Usage: /cron run <id>", "plain");
14024
+ await sendJobPicker(chatId, channel, "run");
13486
14025
  return;
13487
14026
  }
13488
14027
  const runId = parseInt(cronSubArgs, 10);
@@ -13493,16 +14032,20 @@ ${lines2.join("\n")}`, "html");
13493
14032
  }
13494
14033
  case "runs": {
13495
14034
  const runsId = cronSubArgs ? parseInt(cronSubArgs, 10) : void 0;
13496
- const cronRuns2 = getJobRuns(runsId, 10);
13497
- if (cronRuns2.length === 0) {
13498
- await channel.sendText(chatId, runsId ? `No runs for job #${runsId}.` : "No run history.", "plain");
13499
- return;
14035
+ if (runsId) {
14036
+ await sendJobRunsView(chatId, runsId, channel, 1);
14037
+ } else {
14038
+ const cronRuns2 = getJobRuns(void 0, 10);
14039
+ if (cronRuns2.length === 0) {
14040
+ await channel.sendText(chatId, "No run history.", "plain");
14041
+ return;
14042
+ }
14043
+ const runLines = cronRuns2.map((r) => {
14044
+ const dur = r.durationMs ? ` (${(r.durationMs / 1e3).toFixed(1)}s)` : "";
14045
+ return `#${r.jobId} [${r.status}] ${formatLocalDateTime(r.startedAt)}${dur}`;
14046
+ });
14047
+ await channel.sendText(chatId, runLines.join("\n\n"), "plain");
13500
14048
  }
13501
- const runLines = cronRuns2.map((r) => {
13502
- const dur = r.durationMs ? ` (${(r.durationMs / 1e3).toFixed(1)}s)` : "";
13503
- return `#${r.jobId} [${r.status}] ${formatLocalDateTime(r.startedAt)}${dur}`;
13504
- });
13505
- await channel.sendText(chatId, runLines.join("\n\n"), "plain");
13506
14049
  break;
13507
14050
  }
13508
14051
  case "edit": {
@@ -13538,19 +14081,50 @@ Message: "${testMsg}"`, "plain");
13538
14081
  break;
13539
14082
  }
13540
14083
  case "reflect": {
13541
- const { runAnalysis: runAnalysis2 } = await Promise.resolve().then(() => (init_analyze(), analyze_exports));
13542
- const { formatNightlySummary: formatNightlySummary2 } = await Promise.resolve().then(() => (init_propose(), propose_exports));
13543
- await channel.sendText(chatId, "Analyzing recent interactions...", "plain");
13544
- try {
13545
- const insights = await runAnalysis2(chatId);
13546
- if (insights.length === 0) {
13547
- await channel.sendText(chatId, "No new insights from recent interactions.", "plain");
13548
- } else {
13549
- const items = insights.map((ins, i) => ({ id: i + 1, category: ins.category, insight: ins.insight }));
13550
- await channel.sendText(chatId, formatNightlySummary2(items), "plain");
14084
+ if (typeof channel.sendKeyboard === "function") {
14085
+ const { getUnprocessedSignalCount: getUnprocessedSignalCount2, getLastAnalysisTime: getLastAnalysisTime2 } = await Promise.resolve().then(() => (init_store4(), store_exports4));
14086
+ const signalCount = getUnprocessedSignalCount2(getDb(), chatId);
14087
+ const lastTime = getLastAnalysisTime2(getDb(), chatId);
14088
+ let lastAnalysisText = "never";
14089
+ if (lastTime) {
14090
+ const diffMs = Date.now() - (/* @__PURE__ */ new Date(lastTime + "Z")).getTime();
14091
+ const diffHours = Math.floor(diffMs / 36e5);
14092
+ if (diffHours < 1) lastAnalysisText = "less than an hour ago";
14093
+ else if (diffHours < 24) lastAnalysisText = `${diffHours} hour${diffHours === 1 ? "" : "s"} ago`;
14094
+ else {
14095
+ const diffDays = Math.floor(diffHours / 24);
14096
+ lastAnalysisText = `${diffDays} day${diffDays === 1 ? "" : "s"} ago`;
14097
+ }
14098
+ }
14099
+ const text = [
14100
+ "Reflection Analysis",
14101
+ "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501",
14102
+ "",
14103
+ "This will analyze recent interactions for",
14104
+ "improvement insights. Uses the summarizer model.",
14105
+ "",
14106
+ `Unprocessed signals: ${signalCount}`,
14107
+ `Last analysis: ${lastAnalysisText}`
14108
+ ].join("\n");
14109
+ await channel.sendKeyboard(chatId, text, [[
14110
+ { label: "Analyze Now", data: "reflect:go", style: "success" },
14111
+ { label: "Cancel", data: "reflect:cancel" }
14112
+ ]]);
14113
+ } else {
14114
+ const { runAnalysis: runAnalysis2 } = await Promise.resolve().then(() => (init_analyze(), analyze_exports));
14115
+ const { formatNightlySummary: formatNightlySummary2 } = await Promise.resolve().then(() => (init_propose(), propose_exports));
14116
+ await channel.sendText(chatId, "Analyzing recent interactions...", "plain");
14117
+ try {
14118
+ const insights = await runAnalysis2(chatId);
14119
+ if (insights.length === 0) {
14120
+ await channel.sendText(chatId, "No new insights from recent interactions.", "plain");
14121
+ } else {
14122
+ const items = insights.map((ins, i) => ({ id: i + 1, category: ins.category, insight: ins.insight }));
14123
+ await channel.sendText(chatId, formatNightlySummary2(items), "plain");
14124
+ }
14125
+ } catch (e) {
14126
+ await channel.sendText(chatId, `Analysis failed: ${e}`, "plain");
13551
14127
  }
13552
- } catch (e) {
13553
- await channel.sendText(chatId, `Analysis failed: ${e}`, "plain");
13554
14128
  }
13555
14129
  break;
13556
14130
  }
@@ -13582,7 +14156,8 @@ async function handleVoice(msg, channel) {
13582
14156
  const response = await askAgent(chatId, transcript, { cwd: getCwd(chatId), model: vModel, permMode: mode, onToolAction: vToolCb });
13583
14157
  if (response.usage) addUsage(chatId, response.usage.input, response.usage.output, response.usage.cacheRead, vModel);
13584
14158
  if (await handleResponseExhaustion(response.text, chatId, msg, channel)) return;
13585
- await sendResponse(chatId, channel, response.text, msg.messageId);
14159
+ const voiceResponse = ensureReaction(response.text, transcript);
14160
+ await sendResponse(chatId, channel, voiceResponse, msg.messageId);
13586
14161
  } catch (err) {
13587
14162
  error("[router] Voice error:", err);
13588
14163
  await channel.sendText(chatId, `Voice processing error: ${errorMessage(err)}`, "plain");
@@ -13607,11 +14182,8 @@ async function handleMedia(msg, channel) {
13607
14182
  const videoBuffer = await channel.downloadFile(fileId);
13608
14183
  const videoMime = msg.metadata?.mimeType ?? msg.mimeType ?? "video/mp4";
13609
14184
  const videoExt = videoMime.split("/")[1]?.replace("quicktime", "mov") || "mp4";
13610
- const tempVideoPath = `/tmp/cc-claw-video-${Date.now()}.${videoExt}`;
13611
- await writeFile3(tempVideoPath, videoBuffer);
14185
+ const videoPath = await saveMedia(videoBuffer, "video", videoExt);
13612
14186
  let prompt2;
13613
- const cleanupVideo = () => unlink2(tempVideoPath).catch(() => {
13614
- });
13615
14187
  if (wantsVideoAnalysis(caption)) {
13616
14188
  const geminiPrompt = caption || "Analyze this video and describe what you see in detail.";
13617
14189
  let analysis;
@@ -13625,21 +14197,21 @@ async function handleMedia(msg, channel) {
13625
14197
  Gemini video analysis:
13626
14198
  ${analysis}
13627
14199
 
13628
- The video is also saved at: ${tempVideoPath}
14200
+ The video is also saved at: ${videoPath}
13629
14201
 
13630
14202
  Respond to the user based on this analysis.` : `The user sent a video. Gemini analyzed it:
13631
14203
 
13632
14204
  ${analysis}
13633
14205
 
13634
- The video is also saved at: ${tempVideoPath}
14206
+ The video is also saved at: ${videoPath}
13635
14207
 
13636
14208
  Summarize this for the user.`;
13637
14209
  } else {
13638
14210
  prompt2 = caption ? `The user sent a video with caption: "${caption}"
13639
14211
 
13640
- The video has been saved to: ${tempVideoPath}
14212
+ The video has been saved to: ${videoPath}
13641
14213
 
13642
- Respond to their message. Do NOT analyze the video unless they ask you to.` : `The user sent a video. It has been saved to: ${tempVideoPath}
14214
+ Respond to their message. Do NOT analyze the video unless they ask you to.` : `The user sent a video. It has been saved to: ${videoPath}
13643
14215
 
13644
14216
  Acknowledge receipt. Do NOT analyze the video unless they ask you to.`;
13645
14217
  }
@@ -13649,12 +14221,9 @@ Acknowledge receipt. Do NOT analyze the video unless they ask you to.`;
13649
14221
  const vidToolCb = vidVerbose !== "off" ? makeToolActionCallback(chatId, channel, vidVerbose) : void 0;
13650
14222
  const response2 = await askAgent(chatId, prompt2, { cwd: getCwd(chatId), model: vidModel, permMode: vMode, onToolAction: vidToolCb });
13651
14223
  if (response2.usage) addUsage(chatId, response2.usage.input, response2.usage.output, response2.usage.cacheRead, vidModel);
13652
- if (await handleResponseExhaustion(response2.text, chatId, msg, channel)) {
13653
- cleanupVideo();
13654
- return;
13655
- }
13656
- await sendResponse(chatId, channel, response2.text, msg.messageId);
13657
- cleanupVideo();
14224
+ if (await handleResponseExhaustion(response2.text, chatId, msg, channel)) return;
14225
+ const vidResponse = ensureReaction(response2.text, caption || "video");
14226
+ await sendResponse(chatId, channel, vidResponse, msg.messageId);
13658
14227
  return;
13659
14228
  }
13660
14229
  const fileBuffer = await channel.downloadFile(fileName);
@@ -13664,8 +14233,7 @@ Acknowledge receipt. Do NOT analyze the video unless they ask you to.`;
13664
14233
  let tempFilePath;
13665
14234
  if (msg.type === "photo" || isImageExt(ext)) {
13666
14235
  const imgExt = msg.type === "photo" ? "jpg" : ext || "jpg";
13667
- tempFilePath = `/tmp/cc-claw-img-${Date.now()}.${imgExt}`;
13668
- await writeFile3(tempFilePath, fileBuffer);
14236
+ tempFilePath = await saveMedia(fileBuffer, "img", imgExt);
13669
14237
  prompt = caption ? `The user sent an image with caption: "${caption}"
13670
14238
 
13671
14239
  The image has been saved to: ${tempFilePath}
@@ -13695,16 +14263,9 @@ ${content}
13695
14263
  const mToolCb = mVerbose !== "off" ? makeToolActionCallback(chatId, channel, mVerbose) : void 0;
13696
14264
  const response = await askAgent(chatId, prompt, { cwd: getCwd(chatId), model: mediaModel, permMode: mMode, onToolAction: mToolCb });
13697
14265
  if (response.usage) addUsage(chatId, response.usage.input, response.usage.output, response.usage.cacheRead, mediaModel);
13698
- if (await handleResponseExhaustion(response.text, chatId, msg, channel)) {
13699
- if (tempFilePath) unlink2(tempFilePath).catch(() => {
13700
- });
13701
- return;
13702
- }
13703
- await sendResponse(chatId, channel, response.text, msg.messageId);
13704
- if (tempFilePath) {
13705
- unlink2(tempFilePath).catch(() => {
13706
- });
13707
- }
14266
+ if (await handleResponseExhaustion(response.text, chatId, msg, channel)) return;
14267
+ const mediaResponse = ensureReaction(response.text, caption || "file");
14268
+ await sendResponse(chatId, channel, mediaResponse, msg.messageId);
13708
14269
  } catch (err) {
13709
14270
  error("[router] Media error:", err);
13710
14271
  await channel.sendText(chatId, `Error processing file: ${errorMessage(err)}`, "plain");
@@ -13860,12 +14421,13 @@ async function handleText(msg, channel) {
13860
14421
  storePendingEscalation(chatId, text);
13861
14422
  if (typeof channel.sendKeyboard === "function") {
13862
14423
  await channel.sendKeyboard(chatId, `Switch to ${targetMode} mode?`, [
13863
- [{ label: "Allow", data: `perm:escalate:${targetMode}` }, { label: "Deny", data: "perm:deny" }]
14424
+ [{ label: "Allow", data: `perm:escalate:${targetMode}`, style: "success" }, { label: "Deny", data: "perm:deny", style: "danger" }]
13864
14425
  ]);
13865
14426
  }
13866
14427
  return;
13867
14428
  }
13868
14429
  if (await handleResponseExhaustion(responseText, chatId, msg, channel)) return;
14430
+ responseText = ensureReaction(responseText, cleanText || text);
13869
14431
  await sendResponse(chatId, channel, responseText, msg.messageId);
13870
14432
  try {
13871
14433
  const { detectAndLogSignals: detectAndLogSignals2 } = await Promise.resolve().then(() => (init_detect(), detect_exports));
@@ -13886,7 +14448,7 @@ async function handleText(msg, channel) {
13886
14448
  const modeLabel = mode === "accounts" ? "\u{1F468}\u{1F3FD}\u200D\u{1F4BB} Only" : mode === "keys" ? "\u{1F511} Only" : mode;
13887
14449
  if (typeof channel.sendKeyboard === "function") {
13888
14450
  const rows = [
13889
- [{ label: "Claude", data: "backend:claude" }, { label: "Codex", data: "backend:codex" }],
14451
+ [{ label: "Claude", data: "backend:claude", style: "primary" }, { label: "Codex", data: "backend:codex", style: "primary" }],
13890
14452
  [{ label: "\u2699\uFE0F Change rotation mode", data: "gopen:accounts" }]
13891
14453
  ];
13892
14454
  await channel.sendKeyboard(chatId, `\u26A0\uFE0F No eligible slots for rotation mode (${modeLabel}). Switch backend?`, rows);
@@ -13898,7 +14460,7 @@ async function handleText(msg, channel) {
13898
14460
  const timeLeft = errMsg.split("|")[1] || "?";
13899
14461
  if (typeof channel.sendKeyboard === "function") {
13900
14462
  const rows = [
13901
- [{ label: "Claude", data: "backend:claude" }, { label: "Codex", data: "backend:codex" }],
14463
+ [{ label: "Claude", data: "backend:claude", style: "primary" }, { label: "Codex", data: "backend:codex", style: "primary" }],
13902
14464
  [{ label: "\u2699\uFE0F Change rotation mode", data: "gopen:accounts" }]
13903
14465
  ];
13904
14466
  await channel.sendKeyboard(chatId, `\u26A0\uFE0F All eligible slots in cooldown. Next available in ~${timeLeft}. Switch backend?`, rows);
@@ -14074,6 +14636,11 @@ async function processImageGenerations(chatId, channel, text) {
14074
14636
  }
14075
14637
  return text.replace(pattern, "").trim();
14076
14638
  }
14639
+ function ensureReaction(responseText, userMessage) {
14640
+ if (/\[REACT:.+?\]/.test(responseText)) return responseText;
14641
+ const emoji = pickFallbackEmoji(userMessage);
14642
+ return `[REACT:${emoji}]${responseText}`;
14643
+ }
14077
14644
  async function processReaction(chatId, channel, text, messageId) {
14078
14645
  const reactPatternGlobal = /\[REACT:(.+?)\]/g;
14079
14646
  if (!reactPatternGlobal.test(text)) return text;
@@ -14083,7 +14650,8 @@ async function processReaction(chatId, channel, text, messageId) {
14083
14650
  while ((match = reactPatternGlobal.exec(text)) !== null) {
14084
14651
  const emoji = match[1].trim();
14085
14652
  if (!reacted && messageId && typeof channel.reactToMessage === "function" && ALLOWED_REACTION_EMOJIS.has(emoji)) {
14086
- channel.reactToMessage(chatId, messageId, emoji).catch(() => {
14653
+ channel.reactToMessage(chatId, messageId, emoji).catch((err) => {
14654
+ log(`[reaction] Failed for chat=${chatId}: ${err}`);
14087
14655
  });
14088
14656
  reacted = true;
14089
14657
  }
@@ -14096,7 +14664,7 @@ async function sendResponse(chatId, channel, text, messageId) {
14096
14664
  for (const { key, value } of updates) {
14097
14665
  if (typeof channel.sendKeyboard === "function") {
14098
14666
  await channel.sendKeyboard(chatId, `Save preference? ${key}: ${value}`, [
14099
- [{ label: "Yes", data: `userupdate:yes:${key}:${value}` }, { label: "No", data: "userupdate:no" }]
14667
+ [{ label: "Yes", data: `userupdate:yes:${key}:${value}`, style: "success" }, { label: "No", data: "userupdate:no" }]
14100
14668
  ]);
14101
14669
  }
14102
14670
  }
@@ -14218,8 +14786,8 @@ async function sendBackendSwitchConfirmation(chatId, target, channel) {
14218
14786
  Your conversation history is preserved. ${targetAdapter.displayName} will receive a summary of recent context and can access your full history on request.`,
14219
14787
  [
14220
14788
  [
14221
- { label: `Switch to ${targetAdapter.displayName}`, data: `backend_confirm:${target}` },
14222
- { label: `Switch + Clear History`, data: `backend_confirm_clear:${target}` }
14789
+ { label: `Switch to ${targetAdapter.displayName}`, data: `backend_confirm:${target}`, style: "primary" },
14790
+ { label: `Switch + Clear History`, data: `backend_confirm_clear:${target}`, style: "primary" }
14223
14791
  ],
14224
14792
  [{ label: "Cancel", data: `backend_cancel:${target}` }]
14225
14793
  ]
@@ -14252,7 +14820,397 @@ async function doBackendSwitch(chatId, backendId, channel) {
14252
14820
  logActivity(getDb(), { chatId, source: "telegram", eventType: "config_changed", summary: `Backend switched to ${targetAdapter.displayName}`, detail: { field: "backend", value: backendId } });
14253
14821
  await channel.sendText(chatId, `\u2705 Switched to ${targetAdapter.displayName}. Ready!`, "plain");
14254
14822
  }
14823
+ async function sendHistoryView(chatId, channel, filter) {
14824
+ const limit = filter.limit ?? 10;
14825
+ let rows = getRecentMessageLog(chatId, 100).reverse();
14826
+ if (filter.backend) {
14827
+ rows = rows.filter((r) => r.backend === filter.backend);
14828
+ }
14829
+ if (filter.period === "today") {
14830
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
14831
+ rows = rows.filter((r) => r.created_at.slice(0, 10) === today);
14832
+ } else if (filter.period === "week") {
14833
+ const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1e3).toISOString();
14834
+ rows = rows.filter((r) => r.created_at >= weekAgo);
14835
+ }
14836
+ const userMsgs = rows.filter((r) => r.role === "user").slice(0, limit);
14837
+ if (userMsgs.length === 0) {
14838
+ await channel.sendText(chatId, "No conversation history found.", "plain");
14839
+ return;
14840
+ }
14841
+ const lines = [`\u{1F4CB} Recent Conversation (last ${userMsgs.length})`, "\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"];
14842
+ for (const row of userMsgs) {
14843
+ const date = row.created_at.slice(0, 10);
14844
+ const preview = row.content.slice(0, 80) + (row.content.length > 80 ? "\u2026" : "");
14845
+ lines.push(`[${row.backend ?? "?"} \xB7 ${date}] ${preview}`);
14846
+ }
14847
+ await channel.sendText(chatId, lines.join("\n"), "plain");
14848
+ const filterRow = [
14849
+ { label: "Show More (25)", data: "hist:recent:25" },
14850
+ { label: "Today", data: "hist:today" },
14851
+ { label: "This Week", data: "hist:week" }
14852
+ ];
14853
+ const backendRow = getAllAdapters().map((a) => ({
14854
+ label: a.displayName,
14855
+ data: `hist:backend:${a.id}`,
14856
+ style: "primary"
14857
+ }));
14858
+ await channel.sendKeyboard(chatId, "Tip: /history <query> for full-text search", [filterRow, backendRow]);
14859
+ }
14860
+ async function sendMemoryPage(chatId, channel, page) {
14861
+ const memories = listMemories();
14862
+ if (memories.length === 0) {
14863
+ await channel.sendText(chatId, "No memories stored yet.", "plain");
14864
+ return;
14865
+ }
14866
+ const pageSize = 10;
14867
+ const totalPages = Math.max(1, Math.ceil(memories.length / pageSize));
14868
+ const safePage = Math.max(1, Math.min(page, totalPages));
14869
+ const start = (safePage - 1) * pageSize;
14870
+ const pageItems = memories.slice(start, start + pageSize);
14871
+ const lines = [
14872
+ `Memories (${memories.length} total, ${safePage === 1 ? "top " : ""}${pageItems.length} by relevance${totalPages > 1 ? ` \xB7 page ${safePage}/${totalPages}` : ""})`,
14873
+ "\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",
14874
+ ""
14875
+ ];
14876
+ for (const [i, m] of pageItems.entries()) {
14877
+ lines.push(`${start + i + 1}. ${m.trigger}: ${m.content.slice(0, 60)}${m.content.length > 60 ? "\u2026" : ""}`);
14878
+ }
14879
+ const buttons = [];
14880
+ const itemRow = [];
14881
+ for (const [i, m] of pageItems.entries()) {
14882
+ itemRow.push({ label: `${start + i + 1}`, data: `mem:view:${m.id}` });
14883
+ if (itemRow.length === 5) {
14884
+ buttons.push([...itemRow]);
14885
+ itemRow.length = 0;
14886
+ }
14887
+ }
14888
+ if (itemRow.length > 0) buttons.push([...itemRow]);
14889
+ if (totalPages > 1) {
14890
+ const navRow = [];
14891
+ if (safePage > 1) navRow.push({ label: `\u2190 Page ${safePage - 1}`, data: `mem:page:${safePage - 1}` });
14892
+ navRow.push({ label: `${safePage}/${totalPages}`, data: "mem:page:noop" });
14893
+ if (safePage < totalPages) navRow.push({ label: `Page ${safePage + 1} \u2192`, data: `mem:page:${safePage + 1}` });
14894
+ buttons.push(navRow);
14895
+ }
14896
+ const footerRow = [];
14897
+ if (memories.length > 50) {
14898
+ footerRow.push({ label: "Show All (file)", data: "mem:showall", style: "primary" });
14899
+ }
14900
+ footerRow.push({ label: "Search: /memory <query>", data: "mem:page:noop" });
14901
+ buttons.push(footerRow);
14902
+ await channel.sendKeyboard(chatId, lines.join("\n"), buttons);
14903
+ }
14904
+ async function sendHeartbeatKeyboard(chatId, channel) {
14905
+ const config2 = getHeartbeatConfig(chatId);
14906
+ const enabled = config2?.enabled === 1;
14907
+ const intervalMs = config2?.intervalMs ?? 18e5;
14908
+ const intervalMin = intervalMs / 6e4;
14909
+ const activeStart = config2?.activeStart ?? "08:00";
14910
+ const activeEnd = config2?.activeEnd ?? "22:00";
14911
+ const lines = [
14912
+ buildSectionHeader("Heartbeat"),
14913
+ "CC-Claw periodically wakes up to check on",
14914
+ "things and alert you proactively \u2014 even when",
14915
+ "you haven't sent a message.",
14916
+ "",
14917
+ `Status: ${enabled ? "ON" : "OFF"}`,
14918
+ `Interval: every ${intervalMin} min`,
14919
+ `Active hours: ${activeStart}-${activeEnd}`
14920
+ ];
14921
+ const presets = [15, 30, 60, 120];
14922
+ const intervalRow = presets.map((m) => ({
14923
+ label: `${m === intervalMin ? "\u2713 " : ""}${m} min`,
14924
+ data: `hb:interval:${m}`,
14925
+ ...m === intervalMin ? { style: "primary" } : {}
14926
+ }));
14927
+ await channel.sendKeyboard(chatId, lines.join("\n"), [
14928
+ [
14929
+ { label: `${enabled ? "\u2713 " : ""}On`, data: "hb:on", ...enabled ? { style: "success" } : {} },
14930
+ { label: `${!enabled ? "\u2713 " : ""}Off`, data: "hb:off", ...!enabled ? { style: "danger" } : {} }
14931
+ ],
14932
+ intervalRow,
14933
+ [{ label: "Active Hours: /heartbeat hours <start>-<end>", data: "hb:noop" }]
14934
+ ]);
14935
+ }
14936
+ async function sendForgetPicker(chatId, channel, page) {
14937
+ const memories = listMemories();
14938
+ if (memories.length === 0) {
14939
+ await channel.sendText(chatId, "No memories to forget.", "plain");
14940
+ return;
14941
+ }
14942
+ const { text, buttons } = buildPaginatedKeyboard({
14943
+ items: memories,
14944
+ page,
14945
+ pageSize: 10,
14946
+ callbackPrefix: "forget:",
14947
+ renderItem: (m) => {
14948
+ const preview = `${m.trigger}: ${m.content}`.slice(0, 30);
14949
+ return { label: preview, data: `forget:pick:${m.id}` };
14950
+ },
14951
+ headerText: (p, tp, total) => `Which memory to forget? (${total} total${tp > 1 ? `, page ${p}/${tp}` : ""})`,
14952
+ footerButtons: [{ label: "Cancel", data: "forget:cancel" }]
14953
+ });
14954
+ await channel.sendKeyboard(chatId, text, buttons);
14955
+ }
14956
+ function getJobScheduleText(job) {
14957
+ if (job.cron) return humanizeCron(job.cron);
14958
+ if (job.everyMs) return humanizeCron(`every:${job.everyMs}`);
14959
+ if (job.atTime) return humanizeCron(`at:${job.atTime}`);
14960
+ return "?";
14961
+ }
14962
+ function getJobStatusEmoji(job) {
14963
+ if (!job.active) return "\u{1F6AB}";
14964
+ if (!job.enabled) return "\u23F8";
14965
+ return "\u25B6\uFE0F";
14966
+ }
14967
+ function getJobStatusLabel(job) {
14968
+ if (!job.active) return "cancelled";
14969
+ if (!job.enabled) return "paused";
14970
+ return "active";
14971
+ }
14972
+ async function sendJobsBoard(chatId, channel, page) {
14973
+ const jobs = listJobs();
14974
+ if (jobs.length === 0) {
14975
+ await channel.sendText(chatId, "No scheduled jobs.\n\nCreate one: /schedule <description>", "plain");
14976
+ return;
14977
+ }
14978
+ if (typeof channel.sendKeyboard !== "function") {
14979
+ const lines = jobs.map((j) => {
14980
+ const status = getJobStatusLabel(j);
14981
+ const schedule2 = getJobScheduleText(j);
14982
+ const tz = j.timezone !== "UTC" ? ` (${j.timezone})` : "";
14983
+ return `#${j.id} [${status}] ${schedule2}${tz}
14984
+ ${j.description}`;
14985
+ });
14986
+ await channel.sendText(chatId, lines.join("\n\n"), "plain");
14987
+ return;
14988
+ }
14989
+ const headerLines = [];
14990
+ for (const [i, j] of jobs.entries()) {
14991
+ const emoji = getJobStatusEmoji(j);
14992
+ const schedule2 = getJobScheduleText(j);
14993
+ const desc = j.description.length > 20 ? j.description.slice(0, 20) + "\u2026" : j.description;
14994
+ headerLines.push(`${i + 1}. ${desc} | ${schedule2} | ${emoji} ${getJobStatusLabel(j)}`);
14995
+ }
14996
+ const { text, buttons } = buildPaginatedKeyboard({
14997
+ items: jobs,
14998
+ page,
14999
+ pageSize: 10,
15000
+ callbackPrefix: "job:",
15001
+ renderItem: (j) => ({
15002
+ label: `#${j.id}: ${j.description.slice(0, 30)}`,
15003
+ data: `job:view:${j.id}`,
15004
+ style: "primary"
15005
+ }),
15006
+ headerText: (p, tp, total) => {
15007
+ const pageJobs = jobs.slice((p - 1) * 10, p * 10);
15008
+ const lines = [`Scheduled Jobs (${total})`, buildSectionHeader("", 20)];
15009
+ for (const j of pageJobs) {
15010
+ const emoji = getJobStatusEmoji(j);
15011
+ const schedule2 = getJobScheduleText(j);
15012
+ const desc = j.description.length > 24 ? j.description.slice(0, 24) + "\u2026" : j.description;
15013
+ lines.push(`${emoji} ${desc} \xB7 ${schedule2}`);
15014
+ }
15015
+ lines.push("");
15016
+ lines.push("Tap a job to view details:");
15017
+ return lines.join("\n");
15018
+ },
15019
+ footerButtons: []
15020
+ });
15021
+ await channel.sendKeyboard(chatId, text, buttons);
15022
+ }
15023
+ async function sendJobDetail(chatId, jobId, channel) {
15024
+ const job = getJobById(jobId);
15025
+ if (!job) {
15026
+ if (typeof channel.sendKeyboard === "function") {
15027
+ await channel.sendKeyboard(chatId, "This job no longer exists.", [
15028
+ [{ label: "Refresh List", data: "job:back" }]
15029
+ ]);
15030
+ } else {
15031
+ await channel.sendText(chatId, "Job not found.", "plain");
15032
+ }
15033
+ return;
15034
+ }
15035
+ const schedule2 = getJobScheduleText(job);
15036
+ const tz = job.timezone !== "UTC" ? ` (${job.timezone})` : "";
15037
+ const backend2 = job.backend ?? "default";
15038
+ const model2 = job.model ?? "default";
15039
+ const lastRun = job.lastRunAt ? formatLocalDateTime(job.lastRunAt) : "never";
15040
+ const nextRun = job.nextRunAt ? formatLocalDateTime(job.nextRunAt) : "\u2014";
15041
+ const status = getJobStatusLabel(job);
15042
+ const failures = job.consecutiveFailures > 0 ? `
15043
+ \u26A0\uFE0F ${job.consecutiveFailures} consecutive failures` : "";
15044
+ const runs = getJobRuns(jobId, 1);
15045
+ const lastRunStatus = runs.length > 0 ? runs[0].status : null;
15046
+ const lines = [
15047
+ `Job #${job.id}: ${job.description}`,
15048
+ buildSectionHeader("", 22),
15049
+ `Runs: ${schedule2}${tz}`,
15050
+ `Backend: ${backend2} | Model: ${model2}`,
15051
+ `Last run: ${lastRun}${lastRunStatus ? ` (${lastRunStatus})` : ""}`,
15052
+ `Next run: ${nextRun}`,
15053
+ `Status: ${status}${failures}`
15054
+ ];
15055
+ const text = lines.join("\n");
15056
+ if (typeof channel.sendKeyboard !== "function") {
15057
+ await channel.sendText(chatId, text, "plain");
15058
+ return;
15059
+ }
15060
+ const actionRow1 = [
15061
+ { label: "Run Now", data: `job:run:${job.id}`, style: "success" }
15062
+ ];
15063
+ if (job.active && job.enabled) {
15064
+ actionRow1.push({ label: "Pause", data: `job:pause:${job.id}` });
15065
+ } else if (job.active && !job.enabled) {
15066
+ actionRow1.push({ label: "Resume", data: `job:resume:${job.id}` });
15067
+ }
15068
+ actionRow1.push({ label: "Edit", data: `job:edit:${job.id}` });
15069
+ const actionRow2 = [
15070
+ { label: "View Runs", data: `job:runs:${job.id}`, style: "primary" }
15071
+ ];
15072
+ if (job.active) {
15073
+ actionRow2.push({ label: "Cancel Job", data: `job:cancel:${job.id}`, style: "danger" });
15074
+ }
15075
+ const buttons = [actionRow1, actionRow2, [{ label: "\u2190 Back to List", data: "job:back" }]];
15076
+ await channel.sendKeyboard(chatId, text, buttons);
15077
+ }
15078
+ async function sendJobRunsView(chatId, jobId, channel, page) {
15079
+ const runs = getJobRuns(jobId, 50);
15080
+ if (runs.length === 0) {
15081
+ const msg = `No runs for job #${jobId}.`;
15082
+ if (typeof channel.sendKeyboard === "function") {
15083
+ await channel.sendKeyboard(chatId, msg, [
15084
+ [{ label: "\u2190 Back to Job", data: `job:view:${jobId}` }]
15085
+ ]);
15086
+ } else {
15087
+ await channel.sendText(chatId, msg, "plain");
15088
+ }
15089
+ return;
15090
+ }
15091
+ if (typeof channel.sendKeyboard !== "function") {
15092
+ const lines = runs.slice(0, 10).map((r) => {
15093
+ const duration = r.durationMs ? ` (${(r.durationMs / 1e3).toFixed(1)}s)` : "";
15094
+ const error3 = r.error ? `
15095
+ Error: ${r.error.slice(0, 100)}` : "";
15096
+ return `#${r.jobId} [${r.status}] ${formatLocalDateTime(r.startedAt)}${duration}${error3}`;
15097
+ });
15098
+ await channel.sendText(chatId, lines.join("\n\n"), "plain");
15099
+ return;
15100
+ }
15101
+ const statusEmoji = {
15102
+ success: "\u2705",
15103
+ failed: "\u274C",
15104
+ running: "\u{1F504}",
15105
+ skipped: "\u23ED",
15106
+ no_content: "\u{1F4ED}",
15107
+ delivery_failed: "\u{1F4EA}"
15108
+ };
15109
+ const { text, buttons } = buildPaginatedKeyboard({
15110
+ items: runs,
15111
+ page,
15112
+ pageSize: 10,
15113
+ callbackPrefix: `job:runs:${jobId}:`,
15114
+ renderItem: (r) => {
15115
+ const emoji = statusEmoji[r.status] ?? "\u2753";
15116
+ const duration = r.durationMs ? ` ${(r.durationMs / 1e3).toFixed(1)}s` : "";
15117
+ return {
15118
+ label: `${emoji} ${formatLocalDateTime(r.startedAt)}${duration}`,
15119
+ data: `job:runs:${jobId}:noop`
15120
+ };
15121
+ },
15122
+ headerText: (p, tp, total) => {
15123
+ const pageRuns = runs.slice((p - 1) * 10, p * 10);
15124
+ const lines = [`Runs for Job #${jobId} (${total} total)`, buildSectionHeader("", 22)];
15125
+ for (const r of pageRuns) {
15126
+ const emoji = statusEmoji[r.status] ?? "\u2753";
15127
+ const duration = r.durationMs ? ` (${(r.durationMs / 1e3).toFixed(1)}s)` : "";
15128
+ const error3 = r.error ? `
15129
+ \u2514 ${r.error.slice(0, 80)}` : "";
15130
+ const usage2 = r.usageInput ? `
15131
+ \u2514 Tokens: ${r.usageInput}in / ${r.usageOutput}out` : "";
15132
+ lines.push(`${emoji} ${formatLocalDateTime(r.startedAt)}${duration}${error3}${usage2}`);
15133
+ }
15134
+ return lines.join("\n");
15135
+ },
15136
+ footerButtons: [{ label: "\u2190 Back to Job", data: `job:view:${jobId}` }]
15137
+ });
15138
+ await channel.sendKeyboard(chatId, text, buttons);
15139
+ }
15140
+ async function sendJobPicker(chatId, channel, action) {
15141
+ const allJobs = listJobs();
15142
+ const jobs = action === "resume" ? allJobs.filter((j) => j.active && !j.enabled) : action === "pause" ? allJobs.filter((j) => j.active && j.enabled) : allJobs.filter((j) => j.active);
15143
+ if (jobs.length === 0) {
15144
+ const msg = action === "resume" ? "No paused jobs." : action === "pause" ? "No active jobs to pause." : "No active jobs.";
15145
+ await channel.sendText(chatId, msg, "plain");
15146
+ return;
15147
+ }
15148
+ if (typeof channel.sendKeyboard !== "function") {
15149
+ await channel.sendText(chatId, `Usage: /${action} <job-id>`, "plain");
15150
+ return;
15151
+ }
15152
+ const actionLabel = action.charAt(0).toUpperCase() + action.slice(1);
15153
+ const style = action === "run" ? "success" : action === "cancel" ? "danger" : void 0;
15154
+ const buttons = jobs.map((j) => [{
15155
+ label: `#${j.id}: ${j.description.slice(0, 30)}`,
15156
+ data: action === "cancel" ? `job:cancel:${j.id}` : `job:${action}:${j.id}`,
15157
+ ...style ? { style } : {}
15158
+ }]);
15159
+ buttons.push([{ label: "Cancel", data: "job:back" }]);
15160
+ await channel.sendKeyboard(chatId, `${actionLabel} which job?`, buttons);
15161
+ }
14255
15162
  async function handleCallback(chatId, data, channel) {
15163
+ if (data.startsWith("menu:")) {
15164
+ const action = data.slice(5);
15165
+ const synth = { chatId, messageId: "", text: "", senderName: "User", type: "command", source: "telegram", command: "", commandArgs: "" };
15166
+ switch (action) {
15167
+ case "newchat":
15168
+ synth.command = "newchat";
15169
+ await handleCommand(synth, channel);
15170
+ break;
15171
+ case "status":
15172
+ synth.command = "status";
15173
+ await handleCommand(synth, channel);
15174
+ break;
15175
+ case "backend":
15176
+ synth.command = "backend";
15177
+ await handleCommand(synth, channel);
15178
+ break;
15179
+ case "model":
15180
+ synth.command = "model";
15181
+ await handleCommand(synth, channel);
15182
+ break;
15183
+ case "jobs":
15184
+ synth.command = "jobs";
15185
+ await handleCommand(synth, channel);
15186
+ break;
15187
+ case "memory":
15188
+ synth.command = "memory";
15189
+ await handleCommand(synth, channel);
15190
+ break;
15191
+ case "history":
15192
+ synth.command = "history";
15193
+ await handleCommand(synth, channel);
15194
+ break;
15195
+ case "help":
15196
+ synth.command = "help";
15197
+ await handleCommand(synth, channel);
15198
+ break;
15199
+ case "permissions":
15200
+ synth.command = "permissions";
15201
+ await handleCommand(synth, channel);
15202
+ break;
15203
+ case "style":
15204
+ synth.command = "response_style";
15205
+ await handleCommand(synth, channel);
15206
+ break;
15207
+ case "health":
15208
+ synth.command = "health";
15209
+ await handleCommand(synth, channel);
15210
+ break;
15211
+ }
15212
+ return;
15213
+ }
14256
15214
  if (data.startsWith("backend:")) {
14257
15215
  const chosen = data.slice(8);
14258
15216
  if (!getAllBackendIds().includes(chosen)) return;
@@ -14493,9 +15451,9 @@ ${PERM_MODES[chosen]}`,
14493
15451
  if (typeof channel.sendKeyboard === "function") {
14494
15452
  await channel.sendKeyboard(chatId, "\u{1F5E3}\uFE0F AI Response Style:", [
14495
15453
  [
14496
- { label: `${selectedStyle === "concise" ? "\u2713 " : ""}Concise`, data: "style:concise" },
14497
- { label: `${selectedStyle === "normal" ? "\u2713 " : ""}Normal`, data: "style:normal" },
14498
- { label: `${selectedStyle === "detailed" ? "\u2713 " : ""}Detailed`, data: "style:detailed" }
15454
+ { label: `${selectedStyle === "concise" ? "\u2713 " : ""}Concise`, data: "style:concise", ...selectedStyle === "concise" ? { style: "primary" } : {} },
15455
+ { label: `${selectedStyle === "normal" ? "\u2713 " : ""}Normal`, data: "style:normal", ...selectedStyle === "normal" ? { style: "primary" } : {} },
15456
+ { label: `${selectedStyle === "detailed" ? "\u2713 " : ""}Detailed`, data: "style:detailed", ...selectedStyle === "detailed" ? { style: "primary" } : {} }
14499
15457
  ]
14500
15458
  ]);
14501
15459
  }
@@ -14513,6 +15471,78 @@ ${PERM_MODES[chosen]}`,
14513
15471
  await channel.sendText(chatId, `Agent mode set to <b>${mode}</b>. Session cleared.`, "html");
14514
15472
  }
14515
15473
  return;
15474
+ } else if (data.startsWith("agents:")) {
15475
+ const rest = data.slice(7);
15476
+ if (rest.startsWith("stop:")) {
15477
+ const shortId = rest.slice(5);
15478
+ const db3 = getDb();
15479
+ const agents2 = listActiveAgents(db3);
15480
+ const match = agents2.find((a) => a.id.startsWith(shortId));
15481
+ if (!match) {
15482
+ await channel.sendText(chatId, `Agent ${shortId} not found or already stopped. Use /agents to refresh.`, "plain");
15483
+ return;
15484
+ }
15485
+ const ok = cancelAgent(match.id);
15486
+ await channel.sendText(chatId, ok ? `Agent ${match.id.slice(0, 8)} cancelled.` : "Could not cancel agent.", "plain");
15487
+ } else if (rest === "tasks") {
15488
+ const synth = { chatId, messageId: "", text: "", senderName: "User", type: "command", source: "telegram", command: "tasks", commandArgs: "" };
15489
+ await handleCommand(synth, channel);
15490
+ }
15491
+ return;
15492
+ } else if (data.startsWith("stopagent:")) {
15493
+ const rest = data.slice(10);
15494
+ if (rest.startsWith("pick:")) {
15495
+ const shortId = rest.slice(5);
15496
+ const db3 = getDb();
15497
+ const agents2 = listActiveAgents(db3);
15498
+ const match = agents2.find((a) => a.id.startsWith(shortId));
15499
+ if (!match) {
15500
+ await channel.sendText(chatId, `Agent ${shortId} not found or already stopped.`, "plain");
15501
+ return;
15502
+ }
15503
+ const ok = cancelAgent(match.id);
15504
+ await channel.sendText(chatId, ok ? `Agent ${match.id.slice(0, 8)} cancelled.` : "Could not cancel agent.", "plain");
15505
+ } else if (rest === "all") {
15506
+ const count = cancelAllAgents(chatId);
15507
+ await channel.sendText(chatId, count > 0 ? `Cancelled ${count} agent(s).` : "No active agents to cancel.", "plain");
15508
+ } else if (rest === "cancel") {
15509
+ await channel.sendText(chatId, "Cancelled.", "plain");
15510
+ }
15511
+ return;
15512
+ } else if (data.startsWith("tasks:")) {
15513
+ const rest = data.slice(6);
15514
+ if (rest.startsWith("view:")) {
15515
+ const taskId = parseInt(rest.slice(5), 10);
15516
+ const db3 = getDb();
15517
+ const task = getTask(db3, taskId);
15518
+ if (!task) {
15519
+ await channel.sendText(chatId, "Task not found or outdated. Use /tasks to refresh.", "plain");
15520
+ return;
15521
+ }
15522
+ const STATUS_EMOJI_TASK = {
15523
+ pending: "\u{1F4CB}",
15524
+ in_progress: "\u{1F504}",
15525
+ completed: "\u2705",
15526
+ failed: "\u274C",
15527
+ abandoned: "\u{1F6AB}"
15528
+ };
15529
+ const emoji = STATUS_EMOJI_TASK[task.status] ?? "\u{1F4CB}";
15530
+ const assignee = task.assignee ? `Assignee: ${task.assignee.slice(0, 8)}` : "Unassigned";
15531
+ const lines = [
15532
+ `${emoji} Task #${task.id}: ${task.subject}`,
15533
+ `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`,
15534
+ `Status: ${task.status}`,
15535
+ assignee,
15536
+ `Created: ${task.createdAt}`,
15537
+ task.completedAt ? `Completed: ${task.completedAt}` : "",
15538
+ "",
15539
+ task.description,
15540
+ task.result ? `
15541
+ Result: ${task.result.slice(0, 500)}` : ""
15542
+ ].filter(Boolean);
15543
+ await channel.sendText(chatId, lines.join("\n"), "plain");
15544
+ }
15545
+ return;
14516
15546
  } else if (data.startsWith("grotation:")) {
14517
15547
  const mode = data.split(":")[1];
14518
15548
  const validModes = ["off", "all", "accounts", "keys"];
@@ -14766,10 +15796,10 @@ ${PERM_MODES[chosen]}`,
14766
15796
  const current = getReflectionStatus2(getDb2(), chatId);
14767
15797
  if (current === "frozen") {
14768
15798
  const { readFileSync: readFileSync21, existsSync: existsSync43 } = await import("fs");
14769
- const { join: join25 } = await import("path");
15799
+ const { join: join26 } = await import("path");
14770
15800
  const { CC_CLAW_HOME: CC_CLAW_HOME3 } = await Promise.resolve().then(() => (init_paths(), paths_exports));
14771
- const soulPath = join25(CC_CLAW_HOME3, "identity/SOUL.md");
14772
- const userPath = join25(CC_CLAW_HOME3, "identity/USER.md");
15801
+ const soulPath = join26(CC_CLAW_HOME3, "identity/SOUL.md");
15802
+ const userPath = join26(CC_CLAW_HOME3, "identity/USER.md");
14773
15803
  const soul = existsSync43(soulPath) ? readFileSync21(soulPath, "utf-8") : "";
14774
15804
  const user = existsSync43(userPath) ? readFileSync21(userPath, "utf-8") : "";
14775
15805
  setReflectionStatus2(getDb2(), chatId, "active", soul, user);
@@ -14821,6 +15851,378 @@ ${PERM_MODES[chosen]}`,
14821
15851
  }
14822
15852
  }
14823
15853
  return;
15854
+ } else if (data.startsWith("summ:")) {
15855
+ const action = data.slice(5);
15856
+ if (action === "all") {
15857
+ const pendingIds = getLoggedChatIds();
15858
+ if (pendingIds.length === 0) {
15859
+ await channel.sendText(chatId, "No pending sessions to summarize.", "plain");
15860
+ return;
15861
+ }
15862
+ await channel.sendText(chatId, `Summarizing ${pendingIds.length} pending session(s)...`, "plain");
15863
+ await summarizeAllPending();
15864
+ await channel.sendText(chatId, `Done. ${pendingIds.length} session(s) summarized and saved to memory.`, "plain");
15865
+ } else if (action === "undo") {
15866
+ const pending = pendingSummaryUndo.get(chatId);
15867
+ if (!pending) {
15868
+ await channel.sendText(chatId, "Undo window expired.", "plain");
15869
+ return;
15870
+ }
15871
+ clearTimeout(pending.timer);
15872
+ pendingSummaryUndo.delete(chatId);
15873
+ const deleted = deleteSessionSummary(pending.summaryId);
15874
+ if (deleted) {
15875
+ await channel.editText?.(chatId, pending.msgId, "\u21A9\uFE0F Summary undone. Session log restored.", "plain");
15876
+ } else {
15877
+ await channel.editText?.(chatId, pending.msgId, "Could not undo \u2014 summary not found.", "plain");
15878
+ }
15879
+ }
15880
+ return;
15881
+ } else if (data.startsWith("reflect:")) {
15882
+ const action = data.slice(8);
15883
+ if (action === "go") {
15884
+ const { runAnalysis: runAnalysis2 } = await Promise.resolve().then(() => (init_analyze(), analyze_exports));
15885
+ const { formatNightlySummary: formatNightlySummary2 } = await Promise.resolve().then(() => (init_propose(), propose_exports));
15886
+ await channel.sendText(chatId, "Analyzing recent interactions...", "plain");
15887
+ try {
15888
+ const insights = await runAnalysis2(chatId);
15889
+ if (insights.length === 0) {
15890
+ await channel.sendText(chatId, "No new insights from recent interactions.", "plain");
15891
+ } else {
15892
+ const items = insights.map((ins, i) => ({ id: i + 1, category: ins.category, insight: ins.insight }));
15893
+ await channel.sendText(chatId, formatNightlySummary2(items) + "\n\nUse /evolve to review and apply proposals.", "plain");
15894
+ }
15895
+ } catch (e) {
15896
+ await channel.sendText(chatId, `Analysis failed: ${e}`, "plain");
15897
+ }
15898
+ } else if (action === "cancel") {
15899
+ await channel.sendText(chatId, "Reflection cancelled.", "plain");
15900
+ }
15901
+ return;
15902
+ } else if (data.startsWith("chats:")) {
15903
+ const action = data.slice(6);
15904
+ if (action === "refresh") {
15905
+ const aliases = getAllChatAliases();
15906
+ if (aliases.length === 0) {
15907
+ await channel.sendKeyboard?.(
15908
+ chatId,
15909
+ "Authorized Chats\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n\nNo chat aliases configured yet.",
15910
+ [[{ label: "Refresh", data: "chats:refresh", style: "primary" }]]
15911
+ );
15912
+ } else {
15913
+ const lines = aliases.map(
15914
+ (a) => `${a.alias.padEnd(12)} | ${a.chatId}${a.chatId === chatId ? " [Active]" : ""}`
15915
+ );
15916
+ const text = ["Authorized Chats", "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501", "", ...lines].join("\n");
15917
+ const buttons = [];
15918
+ const actionRow = [];
15919
+ if (aliases.length > 0) {
15920
+ actionRow.push({ label: "Remove", data: "chats:remove", style: "danger" });
15921
+ }
15922
+ actionRow.push({ label: "Refresh", data: "chats:refresh", style: "primary" });
15923
+ buttons.push(actionRow);
15924
+ await channel.sendKeyboard?.(chatId, text, buttons);
15925
+ }
15926
+ } else if (action === "remove") {
15927
+ const aliases = getAllChatAliases();
15928
+ if (aliases.length === 0) {
15929
+ await channel.sendText(chatId, "No aliases to remove.", "plain");
15930
+ return;
15931
+ }
15932
+ const aliasButtons = aliases.map((a, i) => [{ label: a.alias, data: `chats:remove:${i}` }]);
15933
+ aliasButtons.push([{ label: "Cancel", data: "chats:refresh" }]);
15934
+ await channel.sendKeyboard?.(chatId, "Remove which alias?", aliasButtons);
15935
+ } else if (action.startsWith("remove:")) {
15936
+ const idx = parseInt(action.slice(7), 10);
15937
+ const aliases = getAllChatAliases();
15938
+ if (isNaN(idx) || idx < 0 || idx >= aliases.length) {
15939
+ await channel.sendText(chatId, "This data is outdated. Use /chats to refresh.", "plain");
15940
+ return;
15941
+ }
15942
+ const alias = aliases[idx].alias;
15943
+ const removed = removeChatAlias(alias);
15944
+ await channel.sendText(chatId, removed ? `Alias "${alias}" removed.` : `Alias "${alias}" not found.`, "plain");
15945
+ }
15946
+ return;
15947
+ } else if (data.startsWith("hist:")) {
15948
+ const rest = data.slice(5);
15949
+ if (rest.startsWith("recent:")) {
15950
+ const limit = parseInt(rest.slice(7), 10) || 25;
15951
+ await sendHistoryView(chatId, channel, { limit });
15952
+ } else if (rest === "today") {
15953
+ await sendHistoryView(chatId, channel, { period: "today" });
15954
+ } else if (rest === "week") {
15955
+ await sendHistoryView(chatId, channel, { period: "week" });
15956
+ } else if (rest.startsWith("backend:")) {
15957
+ const backend2 = rest.slice(8);
15958
+ await sendHistoryView(chatId, channel, { backend: backend2 });
15959
+ }
15960
+ return;
15961
+ } else if (data.startsWith("forget:")) {
15962
+ const rest = data.slice(7);
15963
+ const page = isPaginationCallback(data, "forget:");
15964
+ if (page !== null) {
15965
+ await sendForgetPicker(chatId, channel, page);
15966
+ } else if (rest.startsWith("pick:")) {
15967
+ const id = parseInt(rest.slice(5), 10);
15968
+ const memory2 = getMemoryById(id);
15969
+ if (!memory2) {
15970
+ if (typeof channel.sendKeyboard === "function") {
15971
+ await channel.sendKeyboard(chatId, "This memory no longer exists.", [
15972
+ [{ label: "Refresh List", data: "forget:page:1" }]
15973
+ ]);
15974
+ } else {
15975
+ await channel.sendText(chatId, "This memory no longer exists.", "plain");
15976
+ }
15977
+ return;
15978
+ }
15979
+ const detail = `Delete this memory?
15980
+ "${memory2.trigger}: ${memory2.content}"
15981
+ Salience: ${memory2.salience.toFixed(2)} | Created: ${memory2.created_at.slice(0, 10)}`;
15982
+ if (typeof channel.sendKeyboard === "function") {
15983
+ await channel.sendKeyboard(chatId, detail, [
15984
+ [
15985
+ { label: "Confirm Delete", data: `forget:confirm:${id}`, style: "danger" },
15986
+ { label: "Cancel", data: "forget:cancel" }
15987
+ ]
15988
+ ]);
15989
+ }
15990
+ } else if (rest.startsWith("confirm:")) {
15991
+ const id = parseInt(rest.slice(8), 10);
15992
+ const deleted = deleteMemoryById(id);
15993
+ await channel.sendText(
15994
+ chatId,
15995
+ deleted ? "Memory deleted." : "Memory not found \u2014 already deleted.",
15996
+ "plain"
15997
+ );
15998
+ } else if (rest === "cancel") {
15999
+ await channel.sendText(chatId, "Cancelled.", "plain");
16000
+ }
16001
+ return;
16002
+ } else if (data.startsWith("mem:")) {
16003
+ const rest = data.slice(4);
16004
+ const page = isPaginationCallback(data, "mem:");
16005
+ if (page !== null) {
16006
+ await sendMemoryPage(chatId, channel, page);
16007
+ } else if (rest.startsWith("view:")) {
16008
+ const id = parseInt(rest.slice(5), 10);
16009
+ const memory2 = getMemoryById(id);
16010
+ if (!memory2) {
16011
+ if (typeof channel.sendKeyboard === "function") {
16012
+ await channel.sendKeyboard(chatId, "This memory no longer exists.", [
16013
+ [{ label: "Refresh List", data: "mem:page:1" }]
16014
+ ]);
16015
+ } else {
16016
+ await channel.sendText(chatId, "This memory no longer exists.", "plain");
16017
+ }
16018
+ return;
16019
+ }
16020
+ const detail = [
16021
+ buildSectionHeader(`Memory #${memory2.id}`),
16022
+ `Trigger: ${memory2.trigger}`,
16023
+ `Content: ${memory2.content}`,
16024
+ `Salience: ${memory2.salience.toFixed(2)} | Created: ${memory2.created_at.slice(0, 10)}`
16025
+ ].join("\n");
16026
+ if (typeof channel.sendKeyboard === "function") {
16027
+ await channel.sendKeyboard(chatId, detail, [
16028
+ [
16029
+ { label: "Edit", data: `mem:edit:${id}`, style: "primary" },
16030
+ { label: "Forget", data: `mem:forget:${id}`, style: "danger" }
16031
+ ],
16032
+ [{ label: "Back to List", data: "mem:back" }]
16033
+ ]);
16034
+ }
16035
+ } else if (rest.startsWith("forget:confirm:")) {
16036
+ const id = parseInt(rest.slice(15), 10);
16037
+ const deleted = deleteMemoryById(id);
16038
+ await channel.sendText(
16039
+ chatId,
16040
+ deleted ? "Memory deleted." : "Memory not found \u2014 already deleted.",
16041
+ "plain"
16042
+ );
16043
+ } else if (rest.startsWith("forget:")) {
16044
+ const id = parseInt(rest.slice(7), 10);
16045
+ const memory2 = getMemoryById(id);
16046
+ if (!memory2) {
16047
+ await channel.sendText(chatId, "Memory not found.", "plain");
16048
+ return;
16049
+ }
16050
+ if (typeof channel.sendKeyboard === "function") {
16051
+ await channel.sendKeyboard(
16052
+ chatId,
16053
+ `Delete this memory?
16054
+ "${memory2.trigger}: ${memory2.content}"`,
16055
+ [
16056
+ [
16057
+ { label: "Confirm Delete", data: `mem:forget:confirm:${id}`, style: "danger" },
16058
+ { label: "Cancel", data: `mem:view:${id}` }
16059
+ ]
16060
+ ]
16061
+ );
16062
+ }
16063
+ } else if (rest.startsWith("edit:")) {
16064
+ const id = parseInt(rest.slice(5), 10);
16065
+ await channel.sendText(chatId, `Type: /memory edit ${id} <new content>`, "plain");
16066
+ } else if (rest === "back") {
16067
+ await sendMemoryPage(chatId, channel, 1);
16068
+ } else if (rest === "showall") {
16069
+ const memories = listMemories();
16070
+ const lines = memories.map(
16071
+ (m, i) => `${i + 1}. [${m.salience.toFixed(2)}] ${m.trigger}: ${m.content}`
16072
+ );
16073
+ const buffer = Buffer.from(lines.join("\n"), "utf-8");
16074
+ await channel.sendFile(chatId, buffer, "memories.txt", "text/plain");
16075
+ }
16076
+ return;
16077
+ } else if (data.startsWith("hb:")) {
16078
+ const rest = data.slice(3);
16079
+ if (rest === "on") {
16080
+ setHeartbeatConfig(chatId, { enabled: true });
16081
+ startHeartbeatForChat(chatId);
16082
+ await sendHeartbeatKeyboard(chatId, channel);
16083
+ } else if (rest === "off") {
16084
+ setHeartbeatConfig(chatId, { enabled: false });
16085
+ stopHeartbeatForChat(chatId);
16086
+ await sendHeartbeatKeyboard(chatId, channel);
16087
+ } else if (rest.startsWith("interval:")) {
16088
+ const min = parseInt(rest.slice(9), 10);
16089
+ if (isNaN(min) || min < 1) return;
16090
+ const ms = min * 6e4;
16091
+ setHeartbeatConfig(chatId, { intervalMs: ms });
16092
+ stopHeartbeatForChat(chatId);
16093
+ const hbConf = getHeartbeatConfig(chatId);
16094
+ if (hbConf?.enabled) startHeartbeatForChat(chatId);
16095
+ await sendHeartbeatKeyboard(chatId, channel);
16096
+ }
16097
+ return;
16098
+ } else if (data.startsWith("newchat:")) {
16099
+ const rest = data.slice(8);
16100
+ if (rest === "undo") {
16101
+ const pending = pendingNewchatUndo.get(chatId);
16102
+ if (!pending) {
16103
+ await channel.sendText(chatId, "Undo window expired.", "plain");
16104
+ return;
16105
+ }
16106
+ clearTimeout(pending.timer);
16107
+ pendingNewchatUndo.delete(chatId);
16108
+ setSessionId(chatId, pending.oldSessionId);
16109
+ await channel.editText?.(chatId, pending.msgId, "\u21A9\uFE0F Previous session restored.", "plain");
16110
+ }
16111
+ return;
16112
+ } else if (data.startsWith("help:")) {
16113
+ const rest = data.slice(5);
16114
+ if (rest === "back") {
16115
+ await sendHelpCategories(chatId, channel);
16116
+ } else {
16117
+ await sendHelpCategory(chatId, rest, channel);
16118
+ }
16119
+ return;
16120
+ } else if (data.startsWith("usage:")) {
16121
+ const rest = data.slice(6);
16122
+ if (rest === "session" || rest === "24h" || rest === "7d" || rest === "30d" || rest === "model") {
16123
+ await sendUnifiedUsage(chatId, channel, rest);
16124
+ } else if (rest === "limits") {
16125
+ await sendUsageLimits(chatId, channel);
16126
+ } else if (rest === "back") {
16127
+ await sendUnifiedUsage(chatId, channel, "session");
16128
+ } else if (rest === "limits:clear") {
16129
+ for (const bid of getAllBackendIds()) {
16130
+ for (const win of ["hourly", "daily", "weekly"]) {
16131
+ clearBackendLimit(bid, win);
16132
+ }
16133
+ }
16134
+ await channel.sendText(chatId, "All usage limits cleared.", "plain");
16135
+ await sendUnifiedUsage(chatId, channel, "session");
16136
+ } else if (rest.startsWith("limits:set:")) {
16137
+ const bid = rest.slice(11);
16138
+ await channel.sendText(chatId, `Set limit for ${bid}:
16139
+ /limits ${bid} daily <tokens>
16140
+ Example: /limits ${bid} daily 500000`, "plain");
16141
+ }
16142
+ return;
16143
+ } else if (data.startsWith("job:")) {
16144
+ const rest = data.slice(4);
16145
+ if (rest === "back") {
16146
+ await sendJobsBoard(chatId, channel, 1);
16147
+ } else if (rest.startsWith("view:")) {
16148
+ const id = parseInt(rest.slice(5), 10);
16149
+ await sendJobDetail(chatId, id, channel);
16150
+ } else if (rest.startsWith("run:")) {
16151
+ const id = parseInt(rest.slice(4), 10);
16152
+ await channel.sendText(chatId, `Triggering job #${id}...`, "plain");
16153
+ const result = await triggerJob(id);
16154
+ if (typeof channel.sendKeyboard === "function") {
16155
+ await channel.sendKeyboard(chatId, result, [
16156
+ [{ label: "\u2190 Back to Job", data: `job:view:${id}` }]
16157
+ ]);
16158
+ } else {
16159
+ await channel.sendText(chatId, result, "plain");
16160
+ }
16161
+ } else if (rest.startsWith("pause:")) {
16162
+ const id = parseInt(rest.slice(6), 10);
16163
+ const paused = pauseJob(id);
16164
+ if (paused) {
16165
+ await sendJobDetail(chatId, id, channel);
16166
+ } else {
16167
+ await channel.sendText(chatId, `Job #${id} not found.`, "plain");
16168
+ }
16169
+ } else if (rest.startsWith("resume:")) {
16170
+ const id = parseInt(rest.slice(7), 10);
16171
+ const resumed = resumeJob(id);
16172
+ if (resumed) {
16173
+ await sendJobDetail(chatId, id, channel);
16174
+ } else {
16175
+ await channel.sendText(chatId, `Job #${id} not found.`, "plain");
16176
+ }
16177
+ } else if (rest.startsWith("cancel:confirm:")) {
16178
+ const id = parseInt(rest.slice(15), 10);
16179
+ const cancelled = cancelJob(id);
16180
+ if (cancelled) {
16181
+ await channel.sendText(chatId, `Job #${id} cancelled.`, "plain");
16182
+ await sendJobsBoard(chatId, channel, 1);
16183
+ } else {
16184
+ await channel.sendText(chatId, `Job #${id} not found.`, "plain");
16185
+ }
16186
+ } else if (rest.startsWith("cancel:")) {
16187
+ const id = parseInt(rest.slice(7), 10);
16188
+ const job = getJobById(id);
16189
+ if (!job) {
16190
+ await channel.sendText(chatId, `Job #${id} not found.`, "plain");
16191
+ return;
16192
+ }
16193
+ if (typeof channel.sendKeyboard === "function") {
16194
+ await channel.sendKeyboard(
16195
+ chatId,
16196
+ `Cancel job #${id}: ${job.description}?
16197
+
16198
+ This cannot be undone.`,
16199
+ [
16200
+ [
16201
+ { label: "Confirm Cancel", data: `job:cancel:confirm:${id}`, style: "danger" },
16202
+ { label: "\u2190 Back", data: `job:view:${id}` }
16203
+ ]
16204
+ ]
16205
+ );
16206
+ }
16207
+ } else if (rest.startsWith("runs:")) {
16208
+ const parts = rest.slice(5).split(":");
16209
+ const jobId = parseInt(parts[0], 10);
16210
+ const page = isPaginationCallback(data, `job:runs:${jobId}:`);
16211
+ if (page !== null) {
16212
+ await sendJobRunsView(chatId, jobId, channel, page);
16213
+ } else {
16214
+ await sendJobRunsView(chatId, jobId, channel, 1);
16215
+ }
16216
+ } else if (rest.startsWith("edit:")) {
16217
+ const id = parseInt(rest.slice(5), 10);
16218
+ await startEditWizard(chatId, id, channel);
16219
+ } else {
16220
+ const page = isPaginationCallback(data, "job:");
16221
+ if (page !== null) {
16222
+ await sendJobsBoard(chatId, channel, page);
16223
+ }
16224
+ }
16225
+ return;
14824
16226
  }
14825
16227
  }
14826
16228
  async function handleShell(command, chatId, channel, skipGuard = false) {
@@ -14833,7 +16235,7 @@ async function handleShell(command, chatId, channel, skipGuard = false) {
14833
16235
 
14834
16236
  Command: ${command}`,
14835
16237
  [[
14836
- { label: "Run anyway", data: `shell:confirm:${id}` },
16238
+ { label: "Run anyway", data: `shell:confirm:${id}`, style: "danger" },
14837
16239
  { label: "Cancel", data: `shell:cancel:${id}` }
14838
16240
  ]]
14839
16241
  );
@@ -14871,7 +16273,7 @@ async function handleRawShell(command, chatId, channel, skipGuard = false) {
14871
16273
 
14872
16274
  Command: ${command}`,
14873
16275
  [[
14874
- { label: "Run anyway", data: `shell:confirm:${id}` },
16276
+ { label: "Run anyway", data: `shell:confirm:${id}`, style: "danger" },
14875
16277
  { label: "Cancel", data: `shell:cancel:${id}` }
14876
16278
  ]]
14877
16279
  );
@@ -14993,14 +16395,16 @@ Use /skills <page> to navigate (e.g. /skills 2)` : "";
14993
16395
  const header2 = totalPages > 1 ? `${skills2.length} skills (page ${safePage}/${totalPages}). Select one to invoke:` : `${skills2.length} skills available. Select one to invoke:`;
14994
16396
  await channel.sendKeyboard(chatId, header2, buttons);
14995
16397
  }
14996
- var PERM_MODES, VERBOSE_LEVELS, pendingInterrupts, bypassBusyCheck, pendingFallbackMessages, dashboardClawWarnings, CLI_INSTALL_HINTS, BLOCKED_PATH_PATTERNS2, ALLOWED_REACTION_EMOJIS, SKILLS_PER_PAGE;
16398
+ var PERM_MODES, VERBOSE_LEVELS, HELP_CATEGORIES, USAGE_WINDOW_MAP, MEDIA_INCOMING_PATH, TONE_PATTERNS, pendingInterrupts, bypassBusyCheck, pendingFallbackMessages, dashboardClawWarnings, pendingSummaryUndo, pendingNewchatUndo, CLI_INSTALL_HINTS, BLOCKED_PATH_PATTERNS2, ALLOWED_REACTION_EMOJIS, SKILLS_PER_PAGE;
14997
16399
  var init_router = __esm({
14998
16400
  "src/router.ts"() {
14999
16401
  "use strict";
16402
+ init_paths();
15000
16403
  init_discover();
15001
16404
  init_install();
15002
16405
  init_profile();
15003
16406
  init_heartbeat();
16407
+ init_format();
15004
16408
  init_log();
15005
16409
  init_format_time();
15006
16410
  init_agent();
@@ -15019,6 +16423,7 @@ var init_router = __esm({
15019
16423
  init_cron();
15020
16424
  init_wizard();
15021
16425
  init_health2();
16426
+ init_humanize();
15022
16427
  init_video();
15023
16428
  init_store();
15024
16429
  init_orchestrator();
@@ -15029,6 +16434,7 @@ var init_router = __esm({
15029
16434
  init_guard();
15030
16435
  init_exec();
15031
16436
  init_backend_cmd();
16437
+ init_pagination();
15032
16438
  PERM_MODES = {
15033
16439
  yolo: "YOLO \u2014 all tools, full autopilot",
15034
16440
  safe: "Safe \u2014 only my allowed tools",
@@ -15039,10 +16445,98 @@ var init_router = __esm({
15039
16445
  normal: "Normal \u2014 summarized actions",
15040
16446
  verbose: "Verbose \u2014 full details"
15041
16447
  };
16448
+ HELP_CATEGORIES = {
16449
+ Talk: [
16450
+ { cmd: "/newchat", desc: "Start a fresh conversation" },
16451
+ { cmd: "/stop", desc: "Cancel the current running task" },
16452
+ { cmd: "/summarize", desc: "Save session to memory" },
16453
+ { cmd: "/history", desc: "Browse conversation history" }
16454
+ ],
16455
+ Brain: [
16456
+ { cmd: "/backend", desc: "Switch AI backend" },
16457
+ { cmd: "/model", desc: "Switch model" },
16458
+ { cmd: "/permissions", desc: "Set permission mode" },
16459
+ { cmd: "/response_style", desc: "Set response verbosity" },
16460
+ { cmd: "/model_signature", desc: "Toggle model signature" }
16461
+ ],
16462
+ Memory: [
16463
+ { cmd: "/memory", desc: "Browse stored memories" },
16464
+ { cmd: "/remember", desc: "Save a new memory" },
16465
+ { cmd: "/forget", desc: "Remove a memory" },
16466
+ { cmd: "/summarizer", desc: "Configure summarizer" }
16467
+ ],
16468
+ Schedule: [
16469
+ { cmd: "/cron", desc: "Schedule a task (aka /schedule)" },
16470
+ { cmd: "/jobs", desc: "List scheduled jobs" },
16471
+ { cmd: "/editjob", desc: "Edit a job" },
16472
+ { cmd: "/health", desc: "Scheduler health" }
16473
+ ],
16474
+ Agents: [
16475
+ { cmd: "/agents", desc: "List active sub-agents" },
16476
+ { cmd: "/tasks", desc: "Show task board" },
16477
+ { cmd: "/stopagent", desc: "Cancel a sub-agent" },
16478
+ { cmd: "/runners", desc: "List CLI runners" }
16479
+ ],
16480
+ Settings: [
16481
+ { cmd: "/voice", desc: "Toggle voice responses" },
16482
+ { cmd: "/voice_config", desc: "Configure voice provider" },
16483
+ { cmd: "/cwd", desc: "Set working directory" },
16484
+ { cmd: "/tools", desc: "Configure allowed tools" },
16485
+ { cmd: "/verbose", desc: "Tool visibility level" },
16486
+ { cmd: "/heartbeat", desc: "Proactive awareness" },
16487
+ { cmd: "/chats", desc: "Manage authorized chats" },
16488
+ { cmd: "/setup-profile", desc: "Set up user profile" }
16489
+ ],
16490
+ Usage: [
16491
+ { cmd: "/usage", desc: "Costs, limits & token usage" }
16492
+ ],
16493
+ System: [
16494
+ { cmd: "/status", desc: "System status overview" },
16495
+ { cmd: "/skills", desc: "List skills" },
16496
+ { cmd: "/skill-install", desc: "Install skill from GitHub" },
16497
+ { cmd: "/mcps", desc: "List MCP servers" },
16498
+ { cmd: "/mcp", desc: "Manage MCP servers" },
16499
+ { cmd: "/evolve", desc: "Self-learning controls" },
16500
+ { cmd: "/intent", desc: "Test intent classifier" }
16501
+ ]
16502
+ };
16503
+ USAGE_WINDOW_MAP = { "24h": "daily", "7d": "weekly" };
16504
+ MEDIA_INCOMING_PATH = join19(MEDIA_PATH, "incoming");
16505
+ TONE_PATTERNS = [
16506
+ // Humor / laughter
16507
+ { pattern: /\b(lol|lmao|rofl|haha|hehe|😂|🤣|funny|hilarious|joke)\b/i, emoji: "\u{1F923}" },
16508
+ // Sadness / empathy
16509
+ { pattern: /\b(sad|upset|depressed|heartbroken|miss|grief|mourning|sorry for|RIP)\b/i, emoji: "\u{1F622}" },
16510
+ // Frustration / anger
16511
+ { pattern: /\b(angry|furious|pissed|annoyed|hate|wtf|ugh|damn|broken again|still broken)\b/i, emoji: "\u{1F621}" },
16512
+ // Excitement / celebration
16513
+ { pattern: /\b(amazing|awesome|incredible|congrats|celebrate|woohoo|yay|🎉|shipped|launched)\b/i, emoji: "\u{1F389}" },
16514
+ // Mind-blown / surprise
16515
+ { pattern: /\b(wow|insane|mind.?blown|unbelievable|no way|holy|crazy|wild)\b/i, emoji: "\u{1F92F}" },
16516
+ // Gratitude / thanks
16517
+ { pattern: /\b(thanks|thank you|appreciate|grateful|cheers|thx)\b/i, emoji: "\u{1F91D}" },
16518
+ // Agreement / strong positive
16519
+ { pattern: /\b(exactly|perfect|nailed it|spot on|100%|absolutely|yes!)\b/i, emoji: "\u{1F4AF}" },
16520
+ // Questions / curiosity
16521
+ { pattern: /\?\s*$/, emoji: "\u{1F914}" },
16522
+ { pattern: /\b(what|how|why|when|where|can you|could you|do you know)\b.*\?/i, emoji: "\u{1F914}" },
16523
+ // Task / request / command
16524
+ { pattern: /\b(please|can you|could you|go ahead|do it|make it|fix|build|create|deploy|run|send|update|add|remove|change|set up|configure)\b/i, emoji: "\u{1FAE1}" },
16525
+ // Coding / technical
16526
+ { pattern: /\b(code|bug|error|function|api|database|git|commit|merge|refactor|test|debug)\b/i, emoji: "\u{1F468}\u200D\u{1F4BB}" },
16527
+ // Interesting / looking at something
16528
+ { pattern: /\b(look at|check out|see this|interesting|curious|what do you think)\b/i, emoji: "\u{1F440}" },
16529
+ // Love / strong like
16530
+ { pattern: /\b(love|❤|heart|beautiful|gorgeous)\b/i, emoji: "\u{1F60D}" },
16531
+ // Chill / casual greeting
16532
+ { pattern: /^(hey|hi|hello|sup|yo|what'?s up|howdy)\b/i, emoji: "\u{1F60E}" }
16533
+ ];
15042
16534
  pendingInterrupts = /* @__PURE__ */ new Map();
15043
16535
  bypassBusyCheck = /* @__PURE__ */ new Set();
15044
16536
  pendingFallbackMessages = /* @__PURE__ */ new Map();
15045
16537
  dashboardClawWarnings = /* @__PURE__ */ new Map();
16538
+ pendingSummaryUndo = /* @__PURE__ */ new Map();
16539
+ pendingNewchatUndo = /* @__PURE__ */ new Map();
15046
16540
  CLI_INSTALL_HINTS = {
15047
16541
  claude: "Install: npm install -g @anthropic-ai/claude-code",
15048
16542
  gemini: "Install: npm install -g @google/gemini-cli",
@@ -15138,17 +16632,17 @@ var init_router = __esm({
15138
16632
 
15139
16633
  // src/skills/bootstrap.ts
15140
16634
  import { existsSync as existsSync16 } from "fs";
15141
- import { readdir as readdir3, readFile as readFile6, writeFile as writeFile4, copyFile } from "fs/promises";
15142
- import { join as join19, dirname as dirname3 } from "path";
16635
+ import { readdir as readdir4, readFile as readFile6, writeFile as writeFile4, copyFile } from "fs/promises";
16636
+ import { join as join20, dirname as dirname3 } from "path";
15143
16637
  import { fileURLToPath as fileURLToPath2 } from "url";
15144
16638
  async function copyAgentManifestSkills() {
15145
16639
  if (!existsSync16(PKG_SKILLS)) return;
15146
16640
  try {
15147
- const entries = await readdir3(PKG_SKILLS, { withFileTypes: true });
16641
+ const entries = await readdir4(PKG_SKILLS, { withFileTypes: true });
15148
16642
  for (const entry of entries) {
15149
16643
  if (!entry.isFile() || !entry.name.startsWith("agent-") || !entry.name.endsWith(".md")) continue;
15150
- const src = join19(PKG_SKILLS, entry.name);
15151
- const dest = join19(SKILLS_PATH, entry.name);
16644
+ const src = join20(PKG_SKILLS, entry.name);
16645
+ const dest = join20(SKILLS_PATH, entry.name);
15152
16646
  if (existsSync16(dest)) continue;
15153
16647
  await copyFile(src, dest);
15154
16648
  log(`[skills] Bootstrapped ${entry.name} to ${SKILLS_PATH}`);
@@ -15159,10 +16653,10 @@ async function copyAgentManifestSkills() {
15159
16653
  }
15160
16654
  async function bootstrapSkills() {
15161
16655
  await copyAgentManifestSkills();
15162
- const usmDir = join19(SKILLS_PATH, USM_DIR_NAME);
16656
+ const usmDir = join20(SKILLS_PATH, USM_DIR_NAME);
15163
16657
  if (existsSync16(usmDir)) return;
15164
16658
  try {
15165
- const entries = await readdir3(SKILLS_PATH);
16659
+ const entries = await readdir4(SKILLS_PATH);
15166
16660
  const dirs = entries.filter((e) => !e.startsWith("."));
15167
16661
  if (dirs.length > 0) return;
15168
16662
  } catch {
@@ -15182,7 +16676,7 @@ async function bootstrapSkills() {
15182
16676
  }
15183
16677
  }
15184
16678
  async function patchUsmForCcClaw(usmDir) {
15185
- const skillPath = join19(usmDir, "SKILL.md");
16679
+ const skillPath = join20(usmDir, "SKILL.md");
15186
16680
  if (!existsSync16(skillPath)) return;
15187
16681
  try {
15188
16682
  let content = await readFile6(skillPath, "utf-8");
@@ -15228,8 +16722,8 @@ var init_bootstrap = __esm({
15228
16722
  USM_REPO = "jacob-bd/universal-skills-manager";
15229
16723
  USM_DIR_NAME = "universal-skills-manager";
15230
16724
  CC_CLAW_ECOSYSTEM_PATCH = `| **CC-Claw** | \`~/.cc-claw/workspace/skills/\` | N/A (daemon, no project scope) |`;
15231
- PKG_ROOT = join19(dirname3(fileURLToPath2(import.meta.url)), "..", "..");
15232
- PKG_SKILLS = join19(PKG_ROOT, "skills");
16725
+ PKG_ROOT = join20(dirname3(fileURLToPath2(import.meta.url)), "..", "..");
16726
+ PKG_SKILLS = join20(PKG_ROOT, "skills");
15233
16727
  }
15234
16728
  });
15235
16729
 
@@ -15444,7 +16938,7 @@ __export(ai_skill_exports, {
15444
16938
  installAiSkill: () => installAiSkill
15445
16939
  });
15446
16940
  import { existsSync as existsSync17, writeFileSync as writeFileSync6, mkdirSync as mkdirSync7 } from "fs";
15447
- import { join as join20 } from "path";
16941
+ import { join as join21 } from "path";
15448
16942
  import { homedir as homedir5 } from "os";
15449
16943
  function generateAiSkill() {
15450
16944
  const version = VERSION;
@@ -15498,6 +16992,12 @@ Use the CC-Claw CLI when you need to:
15498
16992
  - Every command supports \`--json\` for machine-parseable output
15499
16993
  - Use \`--chat <id>\` to target a specific chat context
15500
16994
 
16995
+ ## Telegram Quick Reference
16996
+
16997
+ - \`/menu\` (\`/m\`) \u2014 Home screen keyboard with quick access to all major features
16998
+ - \`/usage\` \u2014 Unified cost, limits, and usage view (\`/cost\` and \`/limits\` are aliases)
16999
+ - \`@botname <query>\` \u2014 Inline mode: memory/history search from any Telegram chat (requires \`/setinline\` in BotFather)
17000
+
15501
17001
  ## Command Reference
15502
17002
 
15503
17003
  ### Diagnostics
@@ -15745,8 +17245,8 @@ function installAiSkill() {
15745
17245
  const failed = [];
15746
17246
  for (const [backend2, dirs] of Object.entries(BACKEND_SKILL_DIRS2)) {
15747
17247
  for (const dir of dirs) {
15748
- const skillDir = join20(dir, "cc-claw-cli");
15749
- const skillPath = join20(skillDir, "SKILL.md");
17248
+ const skillDir = join21(dir, "cc-claw-cli");
17249
+ const skillPath = join21(skillDir, "SKILL.md");
15750
17250
  try {
15751
17251
  mkdirSync7(skillDir, { recursive: true });
15752
17252
  writeFileSync6(skillPath, skill, "utf-8");
@@ -15765,10 +17265,10 @@ var init_ai_skill = __esm({
15765
17265
  init_paths();
15766
17266
  init_version();
15767
17267
  BACKEND_SKILL_DIRS2 = {
15768
- "cc-claw": [join20(homedir5(), ".cc-claw", "workspace", "skills")],
15769
- claude: [join20(homedir5(), ".claude", "skills")],
15770
- gemini: [join20(homedir5(), ".gemini", "skills")],
15771
- codex: [join20(homedir5(), ".agents", "skills")]
17268
+ "cc-claw": [join21(homedir5(), ".cc-claw", "workspace", "skills")],
17269
+ claude: [join21(homedir5(), ".claude", "skills")],
17270
+ gemini: [join21(homedir5(), ".gemini", "skills")],
17271
+ codex: [join21(homedir5(), ".agents", "skills")]
15772
17272
  };
15773
17273
  }
15774
17274
  });
@@ -15779,17 +17279,17 @@ __export(index_exports, {
15779
17279
  main: () => main
15780
17280
  });
15781
17281
  import { mkdirSync as mkdirSync8, existsSync as existsSync18, renameSync, statSync as statSync3, readFileSync as readFileSync12 } from "fs";
15782
- import { join as join21 } from "path";
17282
+ import { join as join22 } from "path";
15783
17283
  import dotenv from "dotenv";
15784
17284
  function migrateLayout() {
15785
17285
  const moves = [
15786
- [join21(CC_CLAW_HOME, "cc-claw.db"), join21(DATA_PATH, "cc-claw.db")],
15787
- [join21(CC_CLAW_HOME, "cc-claw.db-shm"), join21(DATA_PATH, "cc-claw.db-shm")],
15788
- [join21(CC_CLAW_HOME, "cc-claw.db-wal"), join21(DATA_PATH, "cc-claw.db-wal")],
15789
- [join21(CC_CLAW_HOME, "cc-claw.log"), join21(LOGS_PATH, "cc-claw.log")],
15790
- [join21(CC_CLAW_HOME, "cc-claw.log.1"), join21(LOGS_PATH, "cc-claw.log.1")],
15791
- [join21(CC_CLAW_HOME, "cc-claw.error.log"), join21(LOGS_PATH, "cc-claw.error.log")],
15792
- [join21(CC_CLAW_HOME, "cc-claw.error.log.1"), join21(LOGS_PATH, "cc-claw.error.log.1")]
17286
+ [join22(CC_CLAW_HOME, "cc-claw.db"), join22(DATA_PATH, "cc-claw.db")],
17287
+ [join22(CC_CLAW_HOME, "cc-claw.db-shm"), join22(DATA_PATH, "cc-claw.db-shm")],
17288
+ [join22(CC_CLAW_HOME, "cc-claw.db-wal"), join22(DATA_PATH, "cc-claw.db-wal")],
17289
+ [join22(CC_CLAW_HOME, "cc-claw.log"), join22(LOGS_PATH, "cc-claw.log")],
17290
+ [join22(CC_CLAW_HOME, "cc-claw.log.1"), join22(LOGS_PATH, "cc-claw.log.1")],
17291
+ [join22(CC_CLAW_HOME, "cc-claw.error.log"), join22(LOGS_PATH, "cc-claw.error.log")],
17292
+ [join22(CC_CLAW_HOME, "cc-claw.error.log.1"), join22(LOGS_PATH, "cc-claw.error.log.1")]
15793
17293
  ];
15794
17294
  for (const [from, to] of moves) {
15795
17295
  if (existsSync18(from) && !existsSync18(to)) {
@@ -15880,11 +17380,9 @@ async function main() {
15880
17380
  initOrchestrator();
15881
17381
  let notifyQueue = Promise.resolve();
15882
17382
  setNotifyCallback((chatId, message) => {
15883
- const primaryChatId = (process.env.ALLOWED_CHAT_ID ?? "").split(",")[0]?.trim();
15884
- const targetChatId = primaryChatId || chatId;
15885
17383
  notifyQueue = notifyQueue.then(async () => {
15886
17384
  for (const ch of channelRegistry.list()) {
15887
- await ch.sendText(targetChatId, `\u{1F916} ${message}`, "plain").catch(() => {
17385
+ await ch.sendText(chatId, `\u{1F916} ${message}`, "plain").catch(() => {
15888
17386
  });
15889
17387
  }
15890
17388
  await new Promise((r) => setTimeout(r, 300));
@@ -15920,10 +17418,10 @@ async function main() {
15920
17418
  try {
15921
17419
  const { generateAiSkill: generateAiSkill2 } = await Promise.resolve().then(() => (init_ai_skill(), ai_skill_exports));
15922
17420
  const { writeFileSync: writeFileSync10, mkdirSync: mkdirSync13 } = await import("fs");
15923
- const { join: join25 } = await import("path");
15924
- const skillDir = join25(SKILLS_PATH, "cc-claw-cli");
17421
+ const { join: join26 } = await import("path");
17422
+ const skillDir = join26(SKILLS_PATH, "cc-claw-cli");
15925
17423
  mkdirSync13(skillDir, { recursive: true });
15926
- writeFileSync10(join25(skillDir, "SKILL.md"), generateAiSkill2(), "utf-8");
17424
+ writeFileSync10(join26(skillDir, "SKILL.md"), generateAiSkill2(), "utf-8");
15927
17425
  log("[cc-claw] AI skill updated");
15928
17426
  } catch {
15929
17427
  }
@@ -15940,6 +17438,8 @@ async function main() {
15940
17438
  log(`[orchestrator] Agent ${agent.id.slice(0, 8)} idle \u2014 nudging`);
15941
17439
  }
15942
17440
  });
17441
+ cleanupOldMedia().catch(() => {
17442
+ });
15943
17443
  log("[cc-claw] Ready!");
15944
17444
  const shutdown = async (signal) => {
15945
17445
  log(`[cc-claw] Received ${signal}, shutting down...`);
@@ -16117,7 +17617,7 @@ __export(service_exports, {
16117
17617
  import { existsSync as existsSync20, mkdirSync as mkdirSync9, writeFileSync as writeFileSync7, unlinkSync as unlinkSync4 } from "fs";
16118
17618
  import { execFileSync as execFileSync2, execSync as execSync5 } from "child_process";
16119
17619
  import { homedir as homedir6, platform } from "os";
16120
- import { join as join22, dirname as dirname4 } from "path";
17620
+ import { join as join23, dirname as dirname4 } from "path";
16121
17621
  function resolveExecutable2(name) {
16122
17622
  try {
16123
17623
  return execFileSync2("which", [name], { encoding: "utf-8" }).trim();
@@ -16130,14 +17630,14 @@ function getPathDirs() {
16130
17630
  const home = homedir6();
16131
17631
  const dirs = /* @__PURE__ */ new Set([
16132
17632
  nodeBin,
16133
- join22(home, ".local", "bin"),
17633
+ join23(home, ".local", "bin"),
16134
17634
  "/usr/local/bin",
16135
17635
  "/usr/bin",
16136
17636
  "/bin"
16137
17637
  ]);
16138
17638
  try {
16139
17639
  const prefix = execSync5("npm config get prefix", { encoding: "utf-8" }).trim();
16140
- if (prefix) dirs.add(join22(prefix, "bin"));
17640
+ if (prefix) dirs.add(join23(prefix, "bin"));
16141
17641
  } catch {
16142
17642
  }
16143
17643
  return [...dirs].join(":");
@@ -16312,7 +17812,7 @@ function statusLinux() {
16312
17812
  }
16313
17813
  }
16314
17814
  function installService() {
16315
- if (!existsSync20(join22(CC_CLAW_HOME, ".env"))) {
17815
+ if (!existsSync20(join23(CC_CLAW_HOME, ".env"))) {
16316
17816
  console.error(` Config not found at ${CC_CLAW_HOME}/.env`);
16317
17817
  console.error(" Run 'cc-claw setup' before installing the service.");
16318
17818
  process.exitCode = 1;
@@ -16341,9 +17841,9 @@ var init_service = __esm({
16341
17841
  "use strict";
16342
17842
  init_paths();
16343
17843
  PLIST_LABEL = "com.cc-claw";
16344
- PLIST_PATH = join22(homedir6(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
16345
- SYSTEMD_DIR = join22(homedir6(), ".config", "systemd", "user");
16346
- UNIT_PATH = join22(SYSTEMD_DIR, "cc-claw.service");
17844
+ PLIST_PATH = join23(homedir6(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
17845
+ SYSTEMD_DIR = join23(homedir6(), ".config", "systemd", "user");
17846
+ UNIT_PATH = join23(SYSTEMD_DIR, "cc-claw.service");
16347
17847
  }
16348
17848
  });
16349
17849
 
@@ -16440,7 +17940,7 @@ function outputError(code, message) {
16440
17940
  }
16441
17941
  }
16442
17942
  var isJsonMode;
16443
- var init_format = __esm({
17943
+ var init_format2 = __esm({
16444
17944
  "src/cli/format.ts"() {
16445
17945
  "use strict";
16446
17946
  isJsonMode = () => process.argv.includes("--json") || !process.stdout.isTTY && !process.argv.includes("--human");
@@ -16505,7 +18005,7 @@ async function restartService() {
16505
18005
  var init_daemon = __esm({
16506
18006
  "src/cli/commands/daemon.ts"() {
16507
18007
  "use strict";
16508
- init_format();
18008
+ init_format2();
16509
18009
  }
16510
18010
  });
16511
18011
 
@@ -16659,7 +18159,7 @@ function formatUptime2(seconds) {
16659
18159
  var init_status = __esm({
16660
18160
  "src/cli/commands/status.ts"() {
16661
18161
  "use strict";
16662
- init_format();
18162
+ init_format2();
16663
18163
  init_paths();
16664
18164
  init_version();
16665
18165
  init_resolve_chat();
@@ -16888,7 +18388,7 @@ function formatUptime3(seconds) {
16888
18388
  var init_doctor = __esm({
16889
18389
  "src/cli/commands/doctor.ts"() {
16890
18390
  "use strict";
16891
- init_format();
18391
+ init_format2();
16892
18392
  init_paths();
16893
18393
  init_version();
16894
18394
  }
@@ -16938,7 +18438,7 @@ var init_logs = __esm({
16938
18438
  "src/cli/commands/logs.ts"() {
16939
18439
  "use strict";
16940
18440
  init_paths();
16941
- init_format();
18441
+ init_format2();
16942
18442
  }
16943
18443
  });
16944
18444
 
@@ -16955,7 +18455,7 @@ __export(gemini_exports, {
16955
18455
  geminiRotation: () => geminiRotation
16956
18456
  });
16957
18457
  import { existsSync as existsSync24, mkdirSync as mkdirSync10, writeFileSync as writeFileSync8, readFileSync as readFileSync17, chmodSync } from "fs";
16958
- import { join as join23 } from "path";
18458
+ import { join as join24 } from "path";
16959
18459
  import { createInterface as createInterface5 } from "readline";
16960
18460
  function requireDb() {
16961
18461
  if (!existsSync24(DB_PATH)) {
@@ -16983,7 +18483,7 @@ async function resolveSlotId(idOrLabel) {
16983
18483
  function resolveOAuthEmail(configHome) {
16984
18484
  if (!configHome) return null;
16985
18485
  try {
16986
- const accountsPath = join23(configHome, ".gemini", "google_accounts.json");
18486
+ const accountsPath = join24(configHome, ".gemini", "google_accounts.json");
16987
18487
  if (!existsSync24(accountsPath)) return null;
16988
18488
  const accounts = JSON.parse(readFileSync17(accountsPath, "utf-8"));
16989
18489
  return accounts.active || null;
@@ -17067,14 +18567,14 @@ async function geminiAddKey(globalOpts, opts) {
17067
18567
  }
17068
18568
  async function geminiAddAccount(globalOpts, opts) {
17069
18569
  await requireWriteDb();
17070
- const slotsDir = join23(CC_CLAW_HOME, "gemini-slots");
18570
+ const slotsDir = join24(CC_CLAW_HOME, "gemini-slots");
17071
18571
  if (!existsSync24(slotsDir)) mkdirSync10(slotsDir, { recursive: true });
17072
18572
  const { addGeminiSlot: addGeminiSlot2 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
17073
18573
  const tempId = Date.now();
17074
- const slotDir = join23(slotsDir, `slot-${tempId}`);
18574
+ const slotDir = join24(slotsDir, `slot-${tempId}`);
17075
18575
  mkdirSync10(slotDir, { recursive: true, mode: 448 });
17076
- mkdirSync10(join23(slotDir, ".gemini"), { recursive: true });
17077
- writeFileSync8(join23(slotDir, ".gemini", "settings.json"), JSON.stringify({
18576
+ mkdirSync10(join24(slotDir, ".gemini"), { recursive: true });
18577
+ writeFileSync8(join24(slotDir, ".gemini", "settings.json"), JSON.stringify({
17078
18578
  security: { auth: { selectedType: "oauth-personal" } }
17079
18579
  }, null, 2));
17080
18580
  console.log("");
@@ -17091,7 +18591,7 @@ async function geminiAddAccount(globalOpts, opts) {
17091
18591
  });
17092
18592
  } catch {
17093
18593
  }
17094
- const oauthPath = join23(slotDir, ".gemini", "oauth_creds.json");
18594
+ const oauthPath = join24(slotDir, ".gemini", "oauth_creds.json");
17095
18595
  if (!existsSync24(oauthPath)) {
17096
18596
  console.log(error2("\n No OAuth credentials found. Sign-in may have failed."));
17097
18597
  console.log(" The slot directory is preserved at: " + slotDir);
@@ -17100,7 +18600,7 @@ async function geminiAddAccount(globalOpts, opts) {
17100
18600
  }
17101
18601
  let accountEmail = "unknown";
17102
18602
  try {
17103
- const accounts = JSON.parse(__require("fs").readFileSync(join23(slotDir, ".gemini", "google_accounts.json"), "utf-8"));
18603
+ const accounts = JSON.parse(__require("fs").readFileSync(join24(slotDir, ".gemini", "google_accounts.json"), "utf-8"));
17104
18604
  accountEmail = accounts.active || accountEmail;
17105
18605
  } catch {
17106
18606
  }
@@ -17193,7 +18693,7 @@ var dbInitialized;
17193
18693
  var init_gemini2 = __esm({
17194
18694
  "src/cli/commands/gemini.ts"() {
17195
18695
  "use strict";
17196
- init_format();
18696
+ init_format2();
17197
18697
  init_paths();
17198
18698
  dbInitialized = false;
17199
18699
  }
@@ -17273,7 +18773,7 @@ async function backendSet(globalOpts, name) {
17273
18773
  var init_backend = __esm({
17274
18774
  "src/cli/commands/backend.ts"() {
17275
18775
  "use strict";
17276
- init_format();
18776
+ init_format2();
17277
18777
  init_paths();
17278
18778
  init_resolve_chat();
17279
18779
  }
@@ -17356,7 +18856,7 @@ async function modelSet(globalOpts, name) {
17356
18856
  var init_model = __esm({
17357
18857
  "src/cli/commands/model.ts"() {
17358
18858
  "use strict";
17359
- init_format();
18859
+ init_format2();
17360
18860
  init_paths();
17361
18861
  init_resolve_chat();
17362
18862
  }
@@ -17448,7 +18948,7 @@ async function memoryHistory(globalOpts, opts) {
17448
18948
  var init_memory = __esm({
17449
18949
  "src/cli/commands/memory.ts"() {
17450
18950
  "use strict";
17451
- init_format();
18951
+ init_format2();
17452
18952
  init_paths();
17453
18953
  init_resolve_chat();
17454
18954
  init_format_time();
@@ -17563,7 +19063,7 @@ async function cronCreate(globalOpts, opts) {
17563
19063
  process.exit(1);
17564
19064
  }
17565
19065
  const chatId = resolveChatId(globalOpts);
17566
- const { success: successFmt } = await Promise.resolve().then(() => (init_format(), format_exports));
19066
+ const { success: successFmt } = await Promise.resolve().then(() => (init_format2(), format_exports));
17567
19067
  const timeout = parseAndValidateTimeout(opts.timeout);
17568
19068
  try {
17569
19069
  const { initDatabase: initDatabase2, insertJob: insertJob2 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
@@ -17612,7 +19112,7 @@ async function cronAction(globalOpts, action, id) {
17612
19112
  process.exit(1);
17613
19113
  }
17614
19114
  const jobId = parseInt(id, 10);
17615
- const { success: successFmt } = await Promise.resolve().then(() => (init_format(), format_exports));
19115
+ const { success: successFmt } = await Promise.resolve().then(() => (init_format2(), format_exports));
17616
19116
  const res = await apiPost2(`/api/cron/${action}`, { id: jobId });
17617
19117
  if (res.ok) {
17618
19118
  const labels = { cancel: "cancelled", pause: "paused", resume: "resumed", run: "triggered" };
@@ -17631,7 +19131,7 @@ async function cronEdit(globalOpts, id, opts) {
17631
19131
  process.exit(1);
17632
19132
  }
17633
19133
  const jobId = parseInt(id, 10);
17634
- const { success: successFmt } = await Promise.resolve().then(() => (init_format(), format_exports));
19134
+ const { success: successFmt } = await Promise.resolve().then(() => (init_format2(), format_exports));
17635
19135
  try {
17636
19136
  const { initDatabase: initDatabase2, getDb: getDb2 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
17637
19137
  initDatabase2();
@@ -17733,7 +19233,7 @@ async function cronRuns(globalOpts, jobId, opts) {
17733
19233
  var init_cron2 = __esm({
17734
19234
  "src/cli/commands/cron.ts"() {
17735
19235
  "use strict";
17736
- init_format();
19236
+ init_format2();
17737
19237
  init_paths();
17738
19238
  init_resolve_chat();
17739
19239
  init_types2();
@@ -17823,7 +19323,7 @@ async function agentsSpawn(globalOpts, opts) {
17823
19323
  role: opts.role
17824
19324
  });
17825
19325
  if (res.ok) {
17826
- const { success: s } = await Promise.resolve().then(() => (init_format(), format_exports));
19326
+ const { success: s } = await Promise.resolve().then(() => (init_format2(), format_exports));
17827
19327
  output(res.data, (d) => {
17828
19328
  return `
17829
19329
  ${s(`Agent spawned: ${d.agentId?.slice(0, 8) ?? "?"} (${opts.runner})`)}
@@ -17842,7 +19342,7 @@ async function agentsCancel(globalOpts, id) {
17842
19342
  }
17843
19343
  const res = await apiPost2("/api/orchestrator/cancel", { agentId: id });
17844
19344
  if (res.ok) {
17845
- const { success: s } = await Promise.resolve().then(() => (init_format(), format_exports));
19345
+ const { success: s } = await Promise.resolve().then(() => (init_format2(), format_exports));
17846
19346
  output({ success: true }, () => `
17847
19347
  ${s(`Agent ${id.slice(0, 8)} cancelled.`)}
17848
19348
  `);
@@ -17860,7 +19360,7 @@ async function agentsCancelAll(globalOpts) {
17860
19360
  const chatId = resolveChatId(globalOpts);
17861
19361
  const res = await apiPost2("/api/orchestrator/cancel-all", { chatId });
17862
19362
  if (res.ok) {
17863
- const { success: s } = await Promise.resolve().then(() => (init_format(), format_exports));
19363
+ const { success: s } = await Promise.resolve().then(() => (init_format2(), format_exports));
17864
19364
  const count = res.data?.cancelled ?? 0;
17865
19365
  output({ cancelled: count }, () => `
17866
19366
  ${s(count > 0 ? `Cancelled ${count} agent(s).` : "No active agents.")}
@@ -17899,7 +19399,7 @@ async function runnersList(globalOpts) {
17899
19399
  var init_agents = __esm({
17900
19400
  "src/cli/commands/agents.ts"() {
17901
19401
  "use strict";
17902
- init_format();
19402
+ init_format2();
17903
19403
  init_paths();
17904
19404
  init_resolve_chat();
17905
19405
  }
@@ -17981,7 +19481,7 @@ async function dbBackup(globalOpts, destPath) {
17981
19481
  var init_db = __esm({
17982
19482
  "src/cli/commands/db.ts"() {
17983
19483
  "use strict";
17984
- init_format();
19484
+ init_format2();
17985
19485
  init_paths();
17986
19486
  }
17987
19487
  });
@@ -18144,7 +19644,7 @@ async function limitsSet(globalOpts, backend2, window, tokens) {
18144
19644
  }
18145
19645
  const res = await apiPost2("/api/limits/set", { backend: backend2, window, maxInputTokens: parseInt(tokens, 10) });
18146
19646
  if (res.ok) {
18147
- const { success: success2 } = await Promise.resolve().then(() => (init_format(), format_exports));
19647
+ const { success: success2 } = await Promise.resolve().then(() => (init_format2(), format_exports));
18148
19648
  output({ success: true }, () => `
18149
19649
  ${success2(`Limit set: ${backend2} (${window}) = ${(parseInt(tokens, 10) / 1e3).toFixed(0)}K input tokens`)}
18150
19650
  `);
@@ -18161,7 +19661,7 @@ async function limitsClear(globalOpts, backend2, window) {
18161
19661
  }
18162
19662
  const res = await apiPost2("/api/limits/clear", { backend: backend2, window });
18163
19663
  if (res.ok) {
18164
- const { success: success2 } = await Promise.resolve().then(() => (init_format(), format_exports));
19664
+ const { success: success2 } = await Promise.resolve().then(() => (init_format2(), format_exports));
18165
19665
  output({ success: true }, () => `
18166
19666
  ${success2(`Limit removed for ${backend2} (${window}).`)}
18167
19667
  `);
@@ -18173,7 +19673,7 @@ async function limitsClear(globalOpts, backend2, window) {
18173
19673
  var init_usage = __esm({
18174
19674
  "src/cli/commands/usage.ts"() {
18175
19675
  "use strict";
18176
- init_format();
19676
+ init_format2();
18177
19677
  init_paths();
18178
19678
  init_resolve_chat();
18179
19679
  }
@@ -18297,7 +19797,7 @@ var RUNTIME_KEYS, KEY_TABLE_MAP, ALLOWED_TABLES, ALLOWED_COLS;
18297
19797
  var init_config = __esm({
18298
19798
  "src/cli/commands/config.ts"() {
18299
19799
  "use strict";
18300
- init_format();
19800
+ init_format2();
18301
19801
  init_paths();
18302
19802
  init_resolve_chat();
18303
19803
  RUNTIME_KEYS = ["backend", "model", "thinking", "summarizer", "mode", "verbose", "cwd", "voice", "response-style"];
@@ -18368,7 +19868,7 @@ async function sessionNew(globalOpts) {
18368
19868
  var init_session = __esm({
18369
19869
  "src/cli/commands/session.ts"() {
18370
19870
  "use strict";
18371
- init_format();
19871
+ init_format2();
18372
19872
  init_paths();
18373
19873
  init_resolve_chat();
18374
19874
  }
@@ -18522,7 +20022,7 @@ async function verboseSet(globalOpts, level) {
18522
20022
  var init_permissions = __esm({
18523
20023
  "src/cli/commands/permissions.ts"() {
18524
20024
  "use strict";
18525
- init_format();
20025
+ init_format2();
18526
20026
  init_paths();
18527
20027
  init_resolve_chat();
18528
20028
  }
@@ -18587,7 +20087,7 @@ async function cwdClear(globalOpts) {
18587
20087
  var init_cwd = __esm({
18588
20088
  "src/cli/commands/cwd.ts"() {
18589
20089
  "use strict";
18590
- init_format();
20090
+ init_format2();
18591
20091
  init_paths();
18592
20092
  init_resolve_chat();
18593
20093
  }
@@ -18638,7 +20138,7 @@ async function voiceSet(globalOpts, value) {
18638
20138
  var init_voice = __esm({
18639
20139
  "src/cli/commands/voice.ts"() {
18640
20140
  "use strict";
18641
- init_format();
20141
+ init_format2();
18642
20142
  init_paths();
18643
20143
  init_resolve_chat();
18644
20144
  }
@@ -18749,7 +20249,7 @@ async function heartbeatSet(globalOpts, subcommand, value) {
18749
20249
  var init_heartbeat2 = __esm({
18750
20250
  "src/cli/commands/heartbeat.ts"() {
18751
20251
  "use strict";
18752
- init_format();
20252
+ init_format2();
18753
20253
  init_paths();
18754
20254
  init_resolve_chat();
18755
20255
  }
@@ -18821,7 +20321,7 @@ async function chatsRemoveAlias(_globalOpts, name) {
18821
20321
  var init_chats = __esm({
18822
20322
  "src/cli/commands/chats.ts"() {
18823
20323
  "use strict";
18824
- init_format();
20324
+ init_format2();
18825
20325
  init_paths();
18826
20326
  }
18827
20327
  });
@@ -18863,7 +20363,7 @@ async function skillsList(_globalOpts) {
18863
20363
  }
18864
20364
  async function skillsInstall(_globalOpts, url) {
18865
20365
  const { isDaemonRunning: isDaemonRunning2 } = await Promise.resolve().then(() => (init_api_client(), api_client_exports));
18866
- const { success: successFmt } = await Promise.resolve().then(() => (init_format(), format_exports));
20366
+ const { success: successFmt } = await Promise.resolve().then(() => (init_format2(), format_exports));
18867
20367
  try {
18868
20368
  const { installSkillFromGitHub: installSkillFromGitHub2 } = await Promise.resolve().then(() => (init_install(), install_exports));
18869
20369
  const result = await installSkillFromGitHub2(url);
@@ -18883,7 +20383,7 @@ async function skillsInstall(_globalOpts, url) {
18883
20383
  var init_skills = __esm({
18884
20384
  "src/cli/commands/skills.ts"() {
18885
20385
  "use strict";
18886
- init_format();
20386
+ init_format2();
18887
20387
  }
18888
20388
  });
18889
20389
 
@@ -18920,7 +20420,7 @@ async function mcpsList(_globalOpts) {
18920
20420
  var init_mcps = __esm({
18921
20421
  "src/cli/commands/mcps.ts"() {
18922
20422
  "use strict";
18923
- init_format();
20423
+ init_format2();
18924
20424
  init_paths();
18925
20425
  }
18926
20426
  });
@@ -19059,7 +20559,7 @@ var TOKEN_PATH2, DEFAULT_PORT2;
19059
20559
  var init_chat = __esm({
19060
20560
  "src/cli/commands/chat.ts"() {
19061
20561
  "use strict";
19062
- init_format();
20562
+ init_format2();
19063
20563
  init_paths();
19064
20564
  init_resolve_chat();
19065
20565
  TOKEN_PATH2 = `${DATA_PATH}/api-token`;
@@ -19204,7 +20704,7 @@ async function tuiCommand(globalOpts, cmdOpts) {
19204
20704
  var init_tui = __esm({
19205
20705
  "src/cli/commands/tui.ts"() {
19206
20706
  "use strict";
19207
- init_format();
20707
+ init_format2();
19208
20708
  init_resolve_chat();
19209
20709
  }
19210
20710
  });
@@ -19730,7 +21230,7 @@ async function evolveHistory(globalOpts, opts) {
19730
21230
  var init_evolve = __esm({
19731
21231
  "src/cli/commands/evolve.ts"() {
19732
21232
  "use strict";
19733
- init_format();
21233
+ init_format2();
19734
21234
  init_paths();
19735
21235
  init_resolve_chat();
19736
21236
  init_format_time();
@@ -19742,7 +21242,7 @@ var setup_exports = {};
19742
21242
  import { existsSync as existsSync42, writeFileSync as writeFileSync9, readFileSync as readFileSync20, copyFileSync as copyFileSync3, mkdirSync as mkdirSync12, statSync as statSync7 } from "fs";
19743
21243
  import { execFileSync as execFileSync4 } from "child_process";
19744
21244
  import { createInterface as createInterface7 } from "readline";
19745
- import { join as join24 } from "path";
21245
+ import { join as join25 } from "path";
19746
21246
  function divider2() {
19747
21247
  console.log(dim("\u2500".repeat(55)));
19748
21248
  }
@@ -19829,7 +21329,7 @@ async function setup() {
19829
21329
  if (match) env[match[1].trim()] = match[2].trim();
19830
21330
  }
19831
21331
  }
19832
- const cwdDb = join24(process.cwd(), "cc-claw.db");
21332
+ const cwdDb = join25(process.cwd(), "cc-claw.db");
19833
21333
  if (existsSync42(cwdDb) && !existsSync42(DB_PATH)) {
19834
21334
  const { size } = statSync7(cwdDb);
19835
21335
  console.log(yellow(` Found existing database at ${cwdDb} (${(size / 1024).toFixed(0)}KB)`));
@@ -19855,6 +21355,10 @@ async function setup() {
19855
21355
  console.log(cyan(" 3. Choose a display name (e.g., 'CC-Claw')"));
19856
21356
  console.log(cyan(" 4. Choose a username (must end in 'bot', e.g., 'cc_claw_bot')"));
19857
21357
  console.log(cyan(" 5. BotFather will give you a token \u2014 copy it\n"));
21358
+ console.log(cyan(" 6. (Optional) Enable inline search \u2014 lets you search memories"));
21359
+ console.log(cyan(" from any chat by typing @your_bot username"));
21360
+ console.log(cyan(" Send /setinline to @BotFather, select your bot, then send:"));
21361
+ console.log(cyan(" Search memories, history, and jobs\n"));
19858
21362
  console.log(dim(" The token looks like: 123456789:ABCdefGHIjklMNOpqrsTUVwxyz\n"));
19859
21363
  await ask(dim(" Press Enter when you're ready to paste the token..."));
19860
21364
  console.log("");
@@ -20454,7 +21958,7 @@ heartbeat.command("set <subcommand> [value]").description("Configure heartbeat (
20454
21958
  var summarizer = program.command("summarizer").description("Session summarizer config");
20455
21959
  summarizer.command("get").description("Current summarizer config").action(async () => {
20456
21960
  if (!(await import("fs")).existsSync(DB_PATH)) {
20457
- const { outputError: outputError3 } = await Promise.resolve().then(() => (init_format(), format_exports));
21961
+ const { outputError: outputError3 } = await Promise.resolve().then(() => (init_format2(), format_exports));
20458
21962
  outputError3("DB_NOT_FOUND", "Database not found.");
20459
21963
  process.exit(1);
20460
21964
  }
@@ -20464,25 +21968,25 @@ summarizer.command("get").description("Current summarizer config").action(async
20464
21968
  const row = readDb.prepare("SELECT backend, model FROM chat_summarizer WHERE chat_id = ?").get(chatId);
20465
21969
  readDb.close();
20466
21970
  const value = row?.backend === "off" ? "off" : row?.backend ? `${row.backend}:${row.model ?? "default"}` : "auto";
20467
- const { output: out } = await Promise.resolve().then(() => (init_format(), format_exports));
21971
+ const { output: out } = await Promise.resolve().then(() => (init_format2(), format_exports));
20468
21972
  out({ value }, (d) => d.value);
20469
21973
  });
20470
21974
  summarizer.command("set <value>").description("Set summarizer (auto/off/<backend>:<model>)").action(async (value) => {
20471
21975
  const { isDaemonRunning: isDaemonRunning2, apiPost: apiPost2 } = await Promise.resolve().then(() => (init_api_client(), api_client_exports));
20472
21976
  if (!await isDaemonRunning2()) {
20473
- const { outputError: outputError3 } = await Promise.resolve().then(() => (init_format(), format_exports));
21977
+ const { outputError: outputError3 } = await Promise.resolve().then(() => (init_format2(), format_exports));
20474
21978
  outputError3("DAEMON_OFFLINE", "Daemon not running.");
20475
21979
  process.exit(1);
20476
21980
  }
20477
21981
  const chatId = program.opts().chat ?? "default";
20478
21982
  const res = await apiPost2("/api/summarizer/set", { chatId, value });
20479
21983
  if (res.ok) {
20480
- const { output: out, success: s } = await Promise.resolve().then(() => (init_format(), format_exports));
21984
+ const { output: out, success: s } = await Promise.resolve().then(() => (init_format2(), format_exports));
20481
21985
  out({ success: true }, () => `
20482
21986
  ${s(`Summarizer: ${value}`)}
20483
21987
  `);
20484
21988
  } else {
20485
- const { outputError: outputError3 } = await Promise.resolve().then(() => (init_format(), format_exports));
21989
+ const { outputError: outputError3 } = await Promise.resolve().then(() => (init_format2(), format_exports));
20486
21990
  outputError3("SET_FAILED", `Failed: ${JSON.stringify(res.data)}`);
20487
21991
  process.exit(1);
20488
21992
  }
@@ -20490,7 +21994,7 @@ summarizer.command("set <value>").description("Set summarizer (auto/off/<backend
20490
21994
  var thinking = program.command("thinking").description("Thinking/reasoning level");
20491
21995
  thinking.command("get").description("Current thinking level").action(async () => {
20492
21996
  if (!(await import("fs")).existsSync(DB_PATH)) {
20493
- const { outputError: outputError3 } = await Promise.resolve().then(() => (init_format(), format_exports));
21997
+ const { outputError: outputError3 } = await Promise.resolve().then(() => (init_format2(), format_exports));
20494
21998
  outputError3("DB_NOT_FOUND", "Database not found.");
20495
21999
  process.exit(1);
20496
22000
  }
@@ -20499,25 +22003,25 @@ thinking.command("get").description("Current thinking level").action(async () =>
20499
22003
  const chatId = program.opts().chat ?? "default";
20500
22004
  const row = readDb.prepare("SELECT level FROM chat_thinking WHERE chat_id = ?").get(chatId);
20501
22005
  readDb.close();
20502
- const { output: out } = await Promise.resolve().then(() => (init_format(), format_exports));
22006
+ const { output: out } = await Promise.resolve().then(() => (init_format2(), format_exports));
20503
22007
  out({ level: row?.level ?? "auto" }, (d) => d.level);
20504
22008
  });
20505
22009
  thinking.command("set <level>").description("Set thinking level").action(async (level) => {
20506
22010
  const { isDaemonRunning: isDaemonRunning2, apiPost: apiPost2 } = await Promise.resolve().then(() => (init_api_client(), api_client_exports));
20507
22011
  if (!await isDaemonRunning2()) {
20508
- const { outputError: outputError3 } = await Promise.resolve().then(() => (init_format(), format_exports));
22012
+ const { outputError: outputError3 } = await Promise.resolve().then(() => (init_format2(), format_exports));
20509
22013
  outputError3("DAEMON_OFFLINE", "Daemon not running.");
20510
22014
  process.exit(1);
20511
22015
  }
20512
22016
  const chatId = program.opts().chat ?? "default";
20513
22017
  const res = await apiPost2("/api/thinking/set", { chatId, level });
20514
22018
  if (res.ok) {
20515
- const { output: out, success: s } = await Promise.resolve().then(() => (init_format(), format_exports));
22019
+ const { output: out, success: s } = await Promise.resolve().then(() => (init_format2(), format_exports));
20516
22020
  out({ success: true }, () => `
20517
22021
  ${s(`Thinking: ${level}`)}
20518
22022
  `);
20519
22023
  } else {
20520
- const { outputError: outputError3 } = await Promise.resolve().then(() => (init_format(), format_exports));
22024
+ const { outputError: outputError3 } = await Promise.resolve().then(() => (init_format2(), format_exports));
20521
22025
  outputError3("SET_FAILED", `Failed: ${JSON.stringify(res.data)}`);
20522
22026
  process.exit(1);
20523
22027
  }
@@ -20557,19 +22061,19 @@ chat.command("send <message>").description("Send a message and get a response").
20557
22061
  chat.command("stop").description("Cancel the current running task").action(async () => {
20558
22062
  const { isDaemonRunning: isDaemonRunning2, apiPost: apiPost2 } = await Promise.resolve().then(() => (init_api_client(), api_client_exports));
20559
22063
  if (!await isDaemonRunning2()) {
20560
- const { outputError: outputError3 } = await Promise.resolve().then(() => (init_format(), format_exports));
22064
+ const { outputError: outputError3 } = await Promise.resolve().then(() => (init_format2(), format_exports));
20561
22065
  outputError3("DAEMON_OFFLINE", "Daemon not running.");
20562
22066
  process.exit(1);
20563
22067
  }
20564
22068
  const chatId = program.opts().chat ?? "default";
20565
22069
  const res = await apiPost2("/api/chat/stop", { chatId });
20566
22070
  if (res.ok) {
20567
- const { output: out, success: s } = await Promise.resolve().then(() => (init_format(), format_exports));
22071
+ const { output: out, success: s } = await Promise.resolve().then(() => (init_format2(), format_exports));
20568
22072
  out({ success: true }, () => `
20569
22073
  ${s("Stopping current task...")}
20570
22074
  `);
20571
22075
  } else {
20572
- const { outputError: outputError3 } = await Promise.resolve().then(() => (init_format(), format_exports));
22076
+ const { outputError: outputError3 } = await Promise.resolve().then(() => (init_format2(), format_exports));
20573
22077
  outputError3("STOP_FAILED", "Nothing is running.");
20574
22078
  process.exit(1);
20575
22079
  }