cc-claw 0.22.7 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +1983 -1293
  2. package/package.json +6 -2
package/dist/cli.js CHANGED
@@ -33,7 +33,7 @@ var VERSION;
33
33
  var init_version = __esm({
34
34
  "src/version.ts"() {
35
35
  "use strict";
36
- VERSION = true ? "0.22.7" : (() => {
36
+ VERSION = true ? "0.23.0" : (() => {
37
37
  try {
38
38
  return JSON.parse(readFileSync(join(process.cwd(), "package.json"), "utf-8")).version ?? "unknown";
39
39
  } catch {
@@ -11793,8 +11793,8 @@ var init_memory = __esm({
11793
11793
  return jsonResponse(res, { success: deleted, mode: "id" });
11794
11794
  }
11795
11795
  if (body.keyword) {
11796
- const { forgetMemory: forgetMemory3 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
11797
- const count = forgetMemory3(body.keyword);
11796
+ const { forgetMemory: forgetMemory4 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
11797
+ const count = forgetMemory4(body.keyword);
11798
11798
  return jsonResponse(res, { success: true, count, mode: "keyword" });
11799
11799
  }
11800
11800
  jsonResponse(res, { error: "Either 'keyword' or 'memoryId' is required" }, 400);
@@ -12189,8 +12189,8 @@ var init_config = __esm({
12189
12189
  handleModelSet = async (req, res) => {
12190
12190
  try {
12191
12191
  const body = JSON.parse(await readBody(req));
12192
- const { setModel: setModel4, clearThinkingLevel: clearThinkingLevel4, clearSession: clearSession3 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
12193
- setModel4(body.chatId, body.model);
12192
+ const { setModel: setModel5, clearThinkingLevel: clearThinkingLevel4, clearSession: clearSession3 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
12193
+ setModel5(body.chatId, body.model);
12194
12194
  clearThinkingLevel4(body.chatId);
12195
12195
  clearSession3(body.chatId);
12196
12196
  logActivity(getDb(), { chatId: body.chatId, source: "cli", eventType: "config_changed", summary: `Model set to ${body.model}`, detail: { field: "model", value: body.model } });
@@ -12202,8 +12202,8 @@ var init_config = __esm({
12202
12202
  handleThinkingSet = async (req, res) => {
12203
12203
  try {
12204
12204
  const body = JSON.parse(await readBody(req));
12205
- const { setThinkingLevel: setThinkingLevel4 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
12206
- setThinkingLevel4(body.chatId, body.level);
12205
+ const { setThinkingLevel: setThinkingLevel5 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
12206
+ setThinkingLevel5(body.chatId, body.level);
12207
12207
  logActivity(getDb(), { chatId: body.chatId, source: "cli", eventType: "config_changed", summary: `Thinking set to ${body.level}`, detail: { field: "thinking", value: body.level } });
12208
12208
  jsonResponse(res, { success: true });
12209
12209
  } catch (err) {
@@ -12244,8 +12244,8 @@ var init_config = __esm({
12244
12244
  handlePermissionsSet = async (req, res) => {
12245
12245
  try {
12246
12246
  const body = JSON.parse(await readBody(req));
12247
- const { setMode: setMode4 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
12248
- setMode4(body.chatId, body.mode);
12247
+ const { setMode: setMode5 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
12248
+ setMode5(body.chatId, body.mode);
12249
12249
  logActivity(getDb(), { chatId: body.chatId, source: "cli", eventType: "config_changed", summary: `Permissions set to ${body.mode}`, detail: { field: "permissions", value: body.mode } });
12250
12250
  jsonResponse(res, { success: true });
12251
12251
  } catch (err) {
@@ -12255,12 +12255,12 @@ var init_config = __esm({
12255
12255
  handleToolsToggle = async (req, res) => {
12256
12256
  try {
12257
12257
  const body = JSON.parse(await readBody(req));
12258
- const { toggleTool: toggleTool5 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
12258
+ const { toggleTool: toggleTool6 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
12259
12259
  if (body.enabled !== void 0) {
12260
12260
  const db3 = getDb();
12261
12261
  db3.prepare("INSERT OR REPLACE INTO chat_tools (chat_id, tool, enabled) VALUES (?, ?, ?)").run(body.chatId, body.tool, body.enabled ? 1 : 0);
12262
12262
  } else {
12263
- toggleTool5(body.chatId, body.tool);
12263
+ toggleTool6(body.chatId, body.tool);
12264
12264
  }
12265
12265
  jsonResponse(res, { success: true });
12266
12266
  } catch (err) {
@@ -12270,8 +12270,8 @@ var init_config = __esm({
12270
12270
  handleToolsReset = async (req, res) => {
12271
12271
  try {
12272
12272
  const body = JSON.parse(await readBody(req));
12273
- const { resetTools: resetTools4 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
12274
- resetTools4(body.chatId);
12273
+ const { resetTools: resetTools5 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
12274
+ resetTools5(body.chatId);
12275
12275
  jsonResponse(res, { success: true });
12276
12276
  } catch (err) {
12277
12277
  jsonResponse(res, { error: errorMessage(err) }, 400);
@@ -12280,8 +12280,8 @@ var init_config = __esm({
12280
12280
  handleVerboseSet = async (req, res) => {
12281
12281
  try {
12282
12282
  const body = JSON.parse(await readBody(req));
12283
- const { setVerboseLevel: setVerboseLevel4 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
12284
- setVerboseLevel4(body.chatId, body.level);
12283
+ const { setVerboseLevel: setVerboseLevel5 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
12284
+ setVerboseLevel5(body.chatId, body.level);
12285
12285
  jsonResponse(res, { success: true });
12286
12286
  } catch (err) {
12287
12287
  jsonResponse(res, { error: errorMessage(err) }, 400);
@@ -12322,8 +12322,8 @@ var init_config = __esm({
12322
12322
  handleHeartbeatSet = async (req, res) => {
12323
12323
  try {
12324
12324
  const body = JSON.parse(await readBody(req));
12325
- const { setHeartbeatConfig: setHeartbeatConfig4 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
12326
- setHeartbeatConfig4(body.chatId, body);
12325
+ const { setHeartbeatConfig: setHeartbeatConfig3 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
12326
+ setHeartbeatConfig3(body.chatId, body);
12327
12327
  jsonResponse(res, { success: true });
12328
12328
  } catch (err) {
12329
12329
  jsonResponse(res, { error: errorMessage(err) }, 400);
@@ -12383,8 +12383,8 @@ var init_config = __esm({
12383
12383
  const db3 = getDb();
12384
12384
  db3.prepare("INSERT OR REPLACE INTO chat_voice (chat_id, enabled) VALUES (?, ?)").run(body.chatId, body.value === "on" || body.value === "1" ? 1 : 0);
12385
12385
  } else if (body.key === "response-style") {
12386
- const { setResponseStyle: setResponseStyle4 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
12387
- setResponseStyle4(body.chatId, body.value);
12386
+ const { setResponseStyle: setResponseStyle5 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
12387
+ setResponseStyle5(body.chatId, body.value);
12388
12388
  } else if (body.key === "backend") {
12389
12389
  const { setBackend: setBackend4, clearSession: clearSession3, clearModel: clearModel4, clearThinkingLevel: clearThinkingLevel4 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
12390
12390
  clearSession3(body.chatId);
@@ -12392,19 +12392,19 @@ var init_config = __esm({
12392
12392
  clearThinkingLevel4(body.chatId);
12393
12393
  setBackend4(body.chatId, body.value);
12394
12394
  } else if (body.key === "model") {
12395
- const { setModel: setModel4, clearThinkingLevel: clearThinkingLevel4, clearSession: clearSession3 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
12396
- setModel4(body.chatId, body.value);
12395
+ const { setModel: setModel5, clearThinkingLevel: clearThinkingLevel4, clearSession: clearSession3 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
12396
+ setModel5(body.chatId, body.value);
12397
12397
  clearThinkingLevel4(body.chatId);
12398
12398
  clearSession3(body.chatId);
12399
12399
  } else if (body.key === "thinking") {
12400
- const { setThinkingLevel: setThinkingLevel4 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
12401
- setThinkingLevel4(body.chatId, body.value);
12400
+ const { setThinkingLevel: setThinkingLevel5 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
12401
+ setThinkingLevel5(body.chatId, body.value);
12402
12402
  } else if (body.key === "mode") {
12403
- const { setMode: setMode4 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
12404
- setMode4(body.chatId, body.value);
12403
+ const { setMode: setMode5 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
12404
+ setMode5(body.chatId, body.value);
12405
12405
  } else if (body.key === "verbose") {
12406
- const { setVerboseLevel: setVerboseLevel4 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
12407
- setVerboseLevel4(body.chatId, body.value);
12406
+ const { setVerboseLevel: setVerboseLevel5 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
12407
+ setVerboseLevel5(body.chatId, body.value);
12408
12408
  } else if (body.key === "summarizer") {
12409
12409
  const { setSummarizer: setSummarizer4, clearSummarizer: clearSummarizer4 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
12410
12410
  if (body.value === "auto") clearSummarizer4(body.chatId);
@@ -15185,7 +15185,7 @@ async function askAgentImpl(chatId, userMessage, opts) {
15185
15185
  const slotSpawnOpts = (() => {
15186
15186
  if (adapter.id !== "gemini") return spawnOpts;
15187
15187
  const geminiAdapter = adapter;
15188
- const { env, slot } = geminiAdapter.resolveSlotEnv(chatId);
15188
+ const { env, slot } = geminiAdapter.resolveSlotEnv(settingsChat);
15189
15189
  if (slot) {
15190
15190
  log(`[agent] rotation=off, using pinned slot: ${slot.label || `slot-${slot.id}`} (${slot.slotType})`);
15191
15191
  return { ...spawnOpts, envOverride: env };
@@ -15239,7 +15239,7 @@ async function askAgentImpl(chatId, userMessage, opts) {
15239
15239
  const retryOpts = (() => {
15240
15240
  if (adapter.id !== "gemini") return spawnOpts;
15241
15241
  const geminiAdapter = adapter;
15242
- const { env, slot } = geminiAdapter.resolveSlotEnv(chatId);
15242
+ const { env, slot } = geminiAdapter.resolveSlotEnv(settingsChat);
15243
15243
  if (slot) return { ...spawnOpts, envOverride: env };
15244
15244
  return spawnOpts;
15245
15245
  })();
@@ -15262,7 +15262,7 @@ async function askAgentImpl(chatId, userMessage, opts) {
15262
15262
  const retryOpts = (() => {
15263
15263
  if (adapter.id !== "gemini") return spawnOpts;
15264
15264
  const geminiAdapter = adapter;
15265
- const { env, slot } = geminiAdapter.resolveSlotEnv(chatId);
15265
+ const { env, slot } = geminiAdapter.resolveSlotEnv(settingsChat);
15266
15266
  if (slot) return { ...spawnOpts, envOverride: env };
15267
15267
  return spawnOpts;
15268
15268
  })();
@@ -15282,7 +15282,7 @@ async function askAgentImpl(chatId, userMessage, opts) {
15282
15282
  const retryOpts = (() => {
15283
15283
  if (adapter.id !== "gemini") return spawnOpts;
15284
15284
  const geminiAdapter = adapter;
15285
- const { env, slot } = geminiAdapter.resolveSlotEnv(chatId);
15285
+ const { env, slot } = geminiAdapter.resolveSlotEnv(settingsChat);
15286
15286
  if (slot) return { ...spawnOpts, envOverride: env };
15287
15287
  return spawnOpts;
15288
15288
  })();
@@ -15303,7 +15303,7 @@ async function askAgentImpl(chatId, userMessage, opts) {
15303
15303
  const retryOpts = (() => {
15304
15304
  if (adapter.id !== "gemini") return spawnOpts;
15305
15305
  const geminiAdapter = adapter;
15306
- const { env, slot } = geminiAdapter.resolveSlotEnv(chatId);
15306
+ const { env, slot } = geminiAdapter.resolveSlotEnv(settingsChat);
15307
15307
  if (slot) return { ...spawnOpts, envOverride: env };
15308
15308
  return spawnOpts;
15309
15309
  })();
@@ -16374,18 +16374,23 @@ function enableHeartbeat(chatId, opts) {
16374
16374
  resumeJob2(existing.id);
16375
16375
  log(`[heartbeat] Resumed job #${existing.id}`);
16376
16376
  }
16377
+ if (!existing.backend) {
16378
+ migrateHeartbeatDefaults(existing);
16379
+ }
16377
16380
  return existing.id;
16378
16381
  }
16379
16382
  const { insertJob: insertJob2 } = (init_jobs(), __toCommonJS(jobs_exports));
16380
16383
  const { startSingleJob: startSingleJob2 } = (init_cron(), __toCommonJS(cron_exports));
16381
16384
  const intervalMs = opts?.intervalMs ?? DEFAULT_INTERVAL_MS;
16385
+ const backend2 = opts?.backend ?? resolveDefaultBackend(chatId);
16386
+ const model2 = opts?.model ?? resolveDefaultModel(backend2);
16382
16387
  const job = insertJob2({
16383
16388
  scheduleType: "every",
16384
16389
  everyMs: intervalMs,
16385
16390
  description: DEFAULT_DESCRIPTION,
16386
16391
  chatId,
16387
- backend: opts?.backend ?? null,
16388
- model: opts?.model ?? null,
16392
+ backend: backend2,
16393
+ model: model2,
16389
16394
  sessionType: "isolated",
16390
16395
  deliveryMode: "announce",
16391
16396
  channel: opts?.channel ?? "telegram",
@@ -16395,9 +16400,36 @@ function enableHeartbeat(chatId, opts) {
16395
16400
  timeout: 120
16396
16401
  });
16397
16402
  startSingleJob2(job);
16398
- log(`[heartbeat] Created job #${job.id} (every ${intervalMs / 6e4}min)`);
16403
+ log(`[heartbeat] Created job #${job.id} (every ${intervalMs / 6e4}min, backend=${backend2}, model=${model2})`);
16399
16404
  return job.id;
16400
16405
  }
16406
+ function resolveDefaultBackend(chatId) {
16407
+ try {
16408
+ const { getAdapterForChat: getAdapterForChat2 } = (init_backends(), __toCommonJS(backends_exports));
16409
+ return getAdapterForChat2(chatId).id;
16410
+ } catch {
16411
+ return "claude";
16412
+ }
16413
+ }
16414
+ function resolveDefaultModel(backend2) {
16415
+ try {
16416
+ const { getAdapter: getAdapter4 } = (init_backends(), __toCommonJS(backends_exports));
16417
+ return getAdapter4(backend2).defaultModel;
16418
+ } catch {
16419
+ return "claude-sonnet-4-6";
16420
+ }
16421
+ }
16422
+ function migrateHeartbeatDefaults(job) {
16423
+ const backend2 = resolveDefaultBackend(job.chatId);
16424
+ const model2 = resolveDefaultModel(backend2);
16425
+ try {
16426
+ const { getDb: getDb2 } = (init_store5(), __toCommonJS(store_exports5));
16427
+ getDb2().prepare("UPDATE jobs SET backend = ?, model = ? WHERE id = ?").run(backend2, model2, job.id);
16428
+ log(`[heartbeat] Migrated job #${job.id}: backend=${backend2}, model=${model2}`);
16429
+ } catch (err) {
16430
+ log(`[heartbeat] Migration failed for job #${job.id}: ${err}`);
16431
+ }
16432
+ }
16401
16433
  function disableHeartbeat() {
16402
16434
  const job = findHeartbeatJob();
16403
16435
  if (!job) return false;
@@ -16431,6 +16463,14 @@ function updateHeartbeatConfig(updates) {
16431
16463
  sets.push("model = ?");
16432
16464
  values.push(updates.model);
16433
16465
  }
16466
+ if (updates.thinking !== void 0) {
16467
+ sets.push("thinking = ?");
16468
+ values.push(updates.thinking);
16469
+ }
16470
+ if (updates.timeout !== void 0) {
16471
+ sets.push("timeout = ?");
16472
+ values.push(updates.timeout);
16473
+ }
16434
16474
  if (updates.target !== void 0) {
16435
16475
  sets.push("target = ?");
16436
16476
  values.push(updates.target);
@@ -16439,11 +16479,19 @@ function updateHeartbeatConfig(updates) {
16439
16479
  sets.push("channel = ?");
16440
16480
  values.push(updates.channel);
16441
16481
  }
16482
+ if (updates.fallbacks !== void 0) {
16483
+ sets.push("fallbacks = ?");
16484
+ values.push(updates.fallbacks);
16485
+ }
16486
+ if (updates.credentialSlotId !== void 0) {
16487
+ sets.push("credential_slot_id = ?");
16488
+ values.push(updates.credentialSlotId);
16489
+ }
16442
16490
  if (sets.length === 0) return false;
16443
16491
  values.push(job.id);
16444
16492
  db3.prepare(`UPDATE jobs SET ${sets.join(", ")} WHERE id = ?`).run(...values);
16445
16493
  if (job.enabled) {
16446
- const { stopJobTimer: stopJobTimer2, startSingleJob: startSingleJob2, listJobs: listJobs3 } = (init_cron(), __toCommonJS(cron_exports));
16494
+ const { stopJobTimer: stopJobTimer2, startSingleJob: startSingleJob2 } = (init_cron(), __toCommonJS(cron_exports));
16447
16495
  const { getJobById: getJobById3 } = (init_store5(), __toCommonJS(store_exports5));
16448
16496
  stopJobTimer2(job.id);
16449
16497
  const refreshed = getJobById3(job.id);
@@ -18695,9 +18743,30 @@ var init_gate = __esm({
18695
18743
  });
18696
18744
 
18697
18745
  // src/voice/stt.ts
18746
+ var stt_exports = {};
18747
+ __export(stt_exports, {
18748
+ ELEVENLABS_VOICES: () => ELEVENLABS_VOICES,
18749
+ GROK_VOICES: () => GROK_VOICES,
18750
+ LOCAL_WHISPER_MODELS: () => LOCAL_WHISPER_MODELS,
18751
+ MACOS_VOICES: () => MACOS_VOICES,
18752
+ downloadWhisperModel: () => downloadWhisperModel,
18753
+ getSttModel: () => getSttModel,
18754
+ getSttProvider: () => getSttProvider,
18755
+ getVoiceConfig: () => getVoiceConfig,
18756
+ isFfmpegAvailable: () => isFfmpegAvailable,
18757
+ isVoiceEnabled: () => isVoiceEnabled,
18758
+ isWhisperCliAvailable: () => isWhisperCliAvailable,
18759
+ isWhisperModelDownloaded: () => isWhisperModelDownloaded,
18760
+ setSttModel: () => setSttModel,
18761
+ setSttProvider: () => setSttProvider,
18762
+ setVoiceProvider: () => setVoiceProvider,
18763
+ synthesizeSpeech: () => synthesizeSpeech,
18764
+ toggleVoice: () => toggleVoice,
18765
+ transcribeAudio: () => transcribeAudio
18766
+ });
18698
18767
  import crypto from "crypto";
18699
18768
  import { execFile as execFile2, execFileSync as execFileSync3 } from "child_process";
18700
- import { readFile as readFile3, unlink as unlink2, mkdir as mkdir2, writeFile } from "fs/promises";
18769
+ import { readFile as readFile3, unlink as unlink2, writeFile } from "fs/promises";
18701
18770
  import { existsSync as existsSync22 } from "fs";
18702
18771
  import { join as join22 } from "path";
18703
18772
  import { promisify as promisify2 } from "util";
@@ -18769,59 +18838,61 @@ function setSttModel(chatId, model2) {
18769
18838
  ON CONFLICT(chat_id) DO UPDATE SET stt_model = ?
18770
18839
  `).run(chatId, model2, model2);
18771
18840
  }
18772
- function isWhisperCliAvailable() {
18773
- if (whisperCliAvailableCache !== null) return whisperCliAvailableCache;
18841
+ function isFfmpegAvailable() {
18842
+ if (ffmpegAvailable !== null) return ffmpegAvailable;
18774
18843
  try {
18775
- execFileSync3("whisper-cli", ["--help"], { stdio: "ignore" });
18776
- whisperCliAvailableCache = true;
18844
+ execFileSync3("ffmpeg", ["-version"], { stdio: "ignore" });
18845
+ ffmpegAvailable = true;
18777
18846
  } catch {
18778
- try {
18779
- execFileSync3("whisper", ["--help"], { stdio: "ignore" });
18780
- whisperCliAvailableCache = true;
18781
- } catch {
18782
- whisperCliAvailableCache = false;
18783
- }
18847
+ ffmpegAvailable = false;
18784
18848
  }
18785
- return whisperCliAvailableCache;
18849
+ return ffmpegAvailable;
18786
18850
  }
18787
- function getWhisperBin() {
18788
- try {
18789
- execFileSync3("whisper-cli", ["--help"], { stdio: "ignore" });
18790
- return "whisper-cli";
18791
- } catch {
18792
- }
18793
- try {
18794
- execFileSync3("whisper", ["--help"], { stdio: "ignore" });
18795
- return "whisper";
18796
- } catch {
18797
- }
18798
- return null;
18799
- }
18800
- function whisperModelPath(model2) {
18801
- return join22(WHISPER_MODELS_PATH, `ggml-${model2}.bin`);
18851
+ function isWhisperCliAvailable() {
18852
+ return true;
18802
18853
  }
18803
18854
  function isWhisperModelDownloaded(model2) {
18804
- return existsSync22(whisperModelPath(model2));
18855
+ const hfId = HF_MODEL_IDS[model2];
18856
+ if (!hfId) return false;
18857
+ const home = process.env.HOME ?? "/tmp";
18858
+ const cacheDir = join22(home, ".cache", "huggingface", "hub", `models--${hfId.replace("/", "--")}`);
18859
+ return existsSync22(cacheDir);
18805
18860
  }
18806
18861
  async function downloadWhisperModel(model2, onProgress) {
18807
- await mkdir2(WHISPER_MODELS_PATH, { recursive: true });
18808
- const dest = whisperModelPath(model2);
18809
- const url = `https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-${model2}.bin`;
18810
18862
  const info = LOCAL_WHISPER_MODELS[model2];
18863
+ const hfId = HF_MODEL_IDS[model2];
18864
+ if (!hfId) throw new Error(`Unknown model: ${model2}`);
18811
18865
  onProgress?.(`\u2B07\uFE0F Downloading Whisper model ${model2} (${info.size})...`);
18812
- log(`[stt] Downloading model ${model2} from ${url}`);
18813
- const response = await fetch(url);
18814
- if (!response.ok) throw new Error(`Failed to download model: ${response.status} ${response.statusText}`);
18815
- const arrayBuffer = await response.arrayBuffer();
18816
- await writeFile(dest, Buffer.from(arrayBuffer));
18817
- log(`[stt] Model ${model2} downloaded to ${dest}`);
18866
+ log(`[stt] Downloading model ${hfId} via @huggingface/transformers`);
18867
+ try {
18868
+ const { pipeline } = await import("@huggingface/transformers");
18869
+ const transcriber = await pipeline("automatic-speech-recognition", hfId);
18870
+ if (cachedPipeline?.model !== model2) {
18871
+ await cachedPipeline?.transcriber?.dispose?.();
18872
+ }
18873
+ cachedPipeline = { model: model2, transcriber };
18874
+ log(`[stt] Model ${model2} downloaded and cached`);
18875
+ onProgress?.(`\u2705 Model ${model2} downloaded and ready!`);
18876
+ } catch (err) {
18877
+ const msg = err instanceof Error ? err.message : String(err);
18878
+ log(`[stt] Model download failed: ${msg}`);
18879
+ throw new Error(`Failed to download model ${model2}: ${msg}`);
18880
+ }
18818
18881
  }
18819
18882
  async function transcribeWithLocalWhisper(audioBuffer, model2, onProgress) {
18820
18883
  ensureFfmpeg();
18821
- const bin = getWhisperBin();
18822
- if (!bin) throw new Error("whisper-cli not found. Install it: brew install whisper-cpp (macOS) or see https://github.com/ggml-org/whisper.cpp");
18823
- if (!isWhisperModelDownloaded(model2)) {
18824
- await downloadWhisperModel(model2, onProgress);
18884
+ const hfId = HF_MODEL_IDS[model2];
18885
+ if (!hfId) throw new Error(`Unknown whisper model: ${model2}`);
18886
+ let transcriber;
18887
+ if (cachedPipeline?.model === model2) {
18888
+ transcriber = cachedPipeline.transcriber;
18889
+ } else {
18890
+ onProgress?.(`\u{1F504} Loading Whisper model ${model2}...`);
18891
+ log(`[stt] Loading pipeline for ${hfId}`);
18892
+ const { pipeline } = await import("@huggingface/transformers");
18893
+ transcriber = await pipeline("automatic-speech-recognition", hfId);
18894
+ if (cachedPipeline) await cachedPipeline.transcriber?.dispose?.();
18895
+ cachedPipeline = { model: model2, transcriber };
18825
18896
  }
18826
18897
  const id = crypto.randomUUID();
18827
18898
  const tmpOgg = `/tmp/cc-claw-stt-${id}.ogg`;
@@ -18829,16 +18900,26 @@ async function transcribeWithLocalWhisper(audioBuffer, model2, onProgress) {
18829
18900
  try {
18830
18901
  await writeFile(tmpOgg, audioBuffer);
18831
18902
  await execFileAsync2("ffmpeg", ["-y", "-i", tmpOgg, "-ar", "16000", "-ac", "1", "-c:a", "pcm_s16le", tmpWav]);
18832
- const modelFile = whisperModelPath(model2);
18833
- const result = await execFileAsync2(bin, ["-m", modelFile, "-f", tmpWav, "-nt", "--output-txt", "-of", `/tmp/cc-claw-stt-${id}`]);
18834
- const txtFile = `/tmp/cc-claw-stt-${id}.txt`;
18835
- let transcript = "";
18836
- if (existsSync22(txtFile)) {
18837
- transcript = (await readFile3(txtFile, "utf-8")).trim();
18838
- unlink2(txtFile).catch(() => {
18839
- });
18840
- } else {
18841
- transcript = (result.stdout ?? "").trim();
18903
+ const wavefileMod = await import("wavefile");
18904
+ const WaveFile = wavefileMod.default?.WaveFile ?? wavefileMod.WaveFile;
18905
+ const wavBuffer = await readFile3(tmpWav);
18906
+ const wav = new WaveFile(wavBuffer);
18907
+ wav.toBitDepth("32f");
18908
+ wav.toSampleRate(16e3);
18909
+ let audioData = wav.getSamples();
18910
+ if (Array.isArray(audioData)) {
18911
+ if (audioData.length > 1) {
18912
+ const SCALING_FACTOR = Math.sqrt(2);
18913
+ for (let i = 0; i < audioData[0].length; ++i) {
18914
+ audioData[0][i] = SCALING_FACTOR * (audioData[0][i] + audioData[1][i]) / 2;
18915
+ }
18916
+ }
18917
+ audioData = audioData[0];
18918
+ }
18919
+ const result = await transcriber(audioData);
18920
+ const transcript = (result?.text ?? "").trim();
18921
+ if (!transcript) {
18922
+ throw new Error("Whisper returned empty transcript. The audio may be too short or silent.");
18842
18923
  }
18843
18924
  return transcript;
18844
18925
  } finally {
@@ -18986,13 +19067,12 @@ async function macOsTts(text, voice2 = "Samantha") {
18986
19067
  });
18987
19068
  return oggBuffer;
18988
19069
  }
18989
- var execFileAsync2, ffmpegAvailable, LOCAL_WHISPER_MODELS, ELEVENLABS_VOICES, GROK_VOICES, MACOS_VOICES, whisperCliAvailableCache;
19070
+ var execFileAsync2, ffmpegAvailable, LOCAL_WHISPER_MODELS, ELEVENLABS_VOICES, GROK_VOICES, MACOS_VOICES, HF_MODEL_IDS, cachedPipeline;
18990
19071
  var init_stt = __esm({
18991
19072
  "src/voice/stt.ts"() {
18992
19073
  "use strict";
18993
19074
  init_log();
18994
19075
  init_store5();
18995
- init_paths();
18996
19076
  execFileAsync2 = promisify2(execFile2);
18997
19077
  ffmpegAvailable = null;
18998
19078
  LOCAL_WHISPER_MODELS = {
@@ -19019,7 +19099,16 @@ var init_stt = __esm({
19019
19099
  "Samantha": { name: "Samantha", gender: "F" },
19020
19100
  "Albert": { name: "Albert", gender: "M" }
19021
19101
  };
19022
- whisperCliAvailableCache = null;
19102
+ HF_MODEL_IDS = {
19103
+ "tiny.en": "Xenova/whisper-tiny.en",
19104
+ "base.en": "Xenova/whisper-base.en",
19105
+ "small.en": "Xenova/whisper-small.en",
19106
+ "small": "Xenova/whisper-small",
19107
+ "medium.en": "Xenova/whisper-medium.en",
19108
+ "medium": "Xenova/whisper-medium",
19109
+ "large-v3-turbo": "Xenova/whisper-large-v3-turbo"
19110
+ };
19111
+ cachedPipeline = null;
19023
19112
  }
19024
19113
  });
19025
19114
 
@@ -19568,13 +19657,13 @@ var init_video = __esm({
19568
19657
 
19569
19658
  // src/router/media.ts
19570
19659
  import { join as join24 } from "path";
19571
- import { mkdir as mkdir3, writeFile as writeFile3, readdir as readdir4, stat as stat3, unlink as unlink4 } from "fs/promises";
19660
+ import { mkdir as mkdir2, writeFile as writeFile3, readdir as readdir4, stat as stat3, unlink as unlink4 } from "fs/promises";
19572
19661
  function getMediaRetentionMs() {
19573
19662
  const hours = parseInt(process.env.MEDIA_RETENTION_HOURS ?? "24", 10);
19574
19663
  return (isNaN(hours) || hours < 1 ? 24 : hours) * 60 * 60 * 1e3;
19575
19664
  }
19576
19665
  async function saveMedia(buffer, prefix, ext) {
19577
- await mkdir3(MEDIA_INCOMING_PATH, { recursive: true });
19666
+ await mkdir2(MEDIA_INCOMING_PATH, { recursive: true });
19578
19667
  const filename = `${prefix}-${Date.now()}.${ext}`;
19579
19668
  const fullPath = join24(MEDIA_INCOMING_PATH, filename);
19580
19669
  await writeFile3(fullPath, buffer);
@@ -19582,7 +19671,7 @@ async function saveMedia(buffer, prefix, ext) {
19582
19671
  }
19583
19672
  async function cleanupOldMedia() {
19584
19673
  try {
19585
- await mkdir3(MEDIA_INCOMING_PATH, { recursive: true });
19674
+ await mkdir2(MEDIA_INCOMING_PATH, { recursive: true });
19586
19675
  const retentionMs = getMediaRetentionMs();
19587
19676
  const retentionHours = Math.round(retentionMs / (60 * 60 * 1e3));
19588
19677
  const files = await readdir4(MEDIA_INCOMING_PATH);
@@ -19652,7 +19741,20 @@ async function handleVoice(msg, channel) {
19652
19741
  await sendResponse(chatId, channel, response.text, msg.messageId);
19653
19742
  } catch (err) {
19654
19743
  error("[router] Voice error:", err);
19655
- await channel.sendText(chatId, `Voice processing error: ${errorMessage(err)}`, { parseMode: "plain" });
19744
+ const raw = errorMessage(err);
19745
+ let userMsg;
19746
+ if (raw.includes("ffmpeg")) {
19747
+ userMsg = "\u274C ffmpeg is not installed \u2014 needed to convert audio.\n\nFix: brew install ffmpeg";
19748
+ } else if (raw.includes("GROQ_API_KEY") || raw.includes("Groq API error")) {
19749
+ userMsg = "\u274C Groq transcription failed. Check your GROQ_API_KEY in ~/.cc-claw/.env, or switch to Local Whisper via /voice.";
19750
+ } else if (raw.includes("empty transcript")) {
19751
+ userMsg = "\u274C Couldn't hear anything in that voice message. Try again with a longer or clearer recording.";
19752
+ } else if (raw.includes("Failed to download model")) {
19753
+ userMsg = "\u274C Couldn't download the Whisper model. Check your internet connection and try again via /voice.";
19754
+ } else {
19755
+ userMsg = `\u274C Voice processing error: ${raw}`;
19756
+ }
19757
+ await channel.sendText(chatId, userMsg, { parseMode: "plain" });
19656
19758
  }
19657
19759
  }
19658
19760
  async function handleMedia(msg, channel) {
@@ -19740,11 +19842,12 @@ Acknowledge receipt. Do NOT analyze the video unless they ask you to.`;
19740
19842
  return;
19741
19843
  }
19742
19844
  const fileBuffer = await channel.downloadFile(fileName);
19743
- const originalName = fileName ?? "file";
19845
+ const originalName = msg.metadata?.originalName ?? fileName ?? "file";
19744
19846
  const ext = originalName.split(".").pop()?.toLowerCase() ?? "";
19847
+ const effectiveExt = ext && ext !== originalName ? ext : msg.mimeType?.split("/")[1]?.replace("plain", "txt").replace("javascript", "js") ?? "";
19745
19848
  let prompt;
19746
19849
  let tempFilePath;
19747
- if (msg.type === "photo" || isImageExt(ext)) {
19850
+ if (msg.type === "photo" || isImageExt(ext) || isImageExt(effectiveExt)) {
19748
19851
  const imgExt = msg.type === "photo" ? "jpg" : ext || "jpg";
19749
19852
  tempFilePath = await saveMedia(fileBuffer, "img", imgExt);
19750
19853
  const allPaths = [tempFilePath];
@@ -19780,7 +19883,7 @@ ${pathList}
19780
19883
 
19781
19884
  Please read all image files and describe what you see.`;
19782
19885
  }
19783
- } else if (isTextExt(ext)) {
19886
+ } else if (isTextExt(ext) || isTextExt(effectiveExt)) {
19784
19887
  const MAX_TEXT_FILE_BYTES = 1048576;
19785
19888
  if (fileBuffer.length > MAX_TEXT_FILE_BYTES) {
19786
19889
  await channel.sendText(chatId, `\u26A0\uFE0F Text file too large for inline processing (${(fileBuffer.length / 1024 / 1024).toFixed(1)}MB, max 1MB). Saving to disk instead.`, { parseMode: "plain" });
@@ -19805,7 +19908,25 @@ ${content}
19805
19908
  \`\`\``;
19806
19909
  }
19807
19910
  } else {
19808
- prompt = `I've shared a file "${originalName}" (${fileBuffer.length} bytes). ${caption || "What can you tell me about it?"}`;
19911
+ const MAX_TEXT_SNIFF = 1048576;
19912
+ const looksLikeText = fileBuffer.length <= MAX_TEXT_SNIFF && !fileBuffer.includes(0);
19913
+ if (looksLikeText) {
19914
+ const content = fileBuffer.toString("utf-8");
19915
+ prompt = caption ? `Here's a file "${originalName}":
19916
+
19917
+ \`\`\`
19918
+ ${content}
19919
+ \`\`\`
19920
+
19921
+ ${caption}` : `Here's a file "${originalName}". Please analyze its contents:
19922
+
19923
+ \`\`\`
19924
+ ${content}
19925
+ \`\`\``;
19926
+ } else {
19927
+ tempFilePath = await saveMedia(fileBuffer, "file", ext || "bin");
19928
+ prompt = `The user shared a binary file "${originalName}" (${fileBuffer.length} bytes), saved at: ${tempFilePath}. ${caption || "What can you tell me about it?"}`;
19929
+ }
19809
19930
  }
19810
19931
  const mediaModel = resolveModel(chatId);
19811
19932
  const mMode = getMode(chatId);
@@ -20179,7 +20300,7 @@ var install_exports = {};
20179
20300
  __export(install_exports, {
20180
20301
  installSkillFromGitHub: () => installSkillFromGitHub
20181
20302
  });
20182
- import { mkdir as mkdir4, readdir as readdir5, readFile as readFile5, cp } from "fs/promises";
20303
+ import { mkdir as mkdir3, readdir as readdir5, readFile as readFile5, cp } from "fs/promises";
20183
20304
  import { existsSync as existsSync24 } from "fs";
20184
20305
  import { join as join25, basename as basename2 } from "path";
20185
20306
  import { execSync as execSync4 } from "child_process";
@@ -20212,7 +20333,7 @@ async function installSkillFromGitHub(urlOrShorthand) {
20212
20333
  if (existsSync24(destDir)) {
20213
20334
  log(`[skill-install] Overwriting existing skill at ${destDir}`);
20214
20335
  }
20215
- await mkdir4(destDir, { recursive: true });
20336
+ await mkdir3(destDir, { recursive: true });
20216
20337
  await cp(skillDir, destDir, { recursive: true });
20217
20338
  let skillName = skillFolderName;
20218
20339
  try {
@@ -20423,6 +20544,53 @@ var init_humanize = __esm({
20423
20544
  });
20424
20545
 
20425
20546
  // src/router/ui.ts
20547
+ var ui_exports = {};
20548
+ __export(ui_exports, {
20549
+ ROTATION_MODE_CONFIRM_LABELS: () => ROTATION_MODE_CONFIRM_LABELS,
20550
+ ROTATION_MODE_LABELS: () => ROTATION_MODE_LABELS,
20551
+ buildAccountSlotKeyboard: () => buildAccountSlotKeyboard,
20552
+ doBackendSwitch: () => doBackendSwitch,
20553
+ getJobScheduleText: () => getJobScheduleText,
20554
+ getJobStatusEmoji: () => getJobStatusEmoji,
20555
+ getJobStatusLabel: () => getJobStatusLabel,
20556
+ handleRotationModeChange: () => handleRotationModeChange,
20557
+ sendBackendAccountPicker: () => sendBackendAccountPicker,
20558
+ sendBackendConfigPanel: () => sendBackendConfigPanel,
20559
+ sendBackendModelPicker: () => sendBackendModelPicker,
20560
+ sendBackendPicker: () => sendBackendPicker,
20561
+ sendBackendSwitchConfirmation: () => sendBackendSwitchConfirmation,
20562
+ sendBackendThinkingPicker: () => sendBackendThinkingPicker,
20563
+ sendCurrentProposal: () => sendCurrentProposal,
20564
+ sendForgetPicker: () => sendForgetPicker,
20565
+ sendHeartbeatEngine: () => sendHeartbeatEngine,
20566
+ sendHeartbeatFallbacks: () => sendHeartbeatFallbacks,
20567
+ sendHeartbeatHours: () => sendHeartbeatHours,
20568
+ sendHeartbeatKeyboard: () => sendHeartbeatKeyboard,
20569
+ sendHeartbeatThinking: () => sendHeartbeatThinking,
20570
+ sendHeartbeatTimeout: () => sendHeartbeatTimeout,
20571
+ sendHeartbeatWatches: () => sendHeartbeatWatches,
20572
+ sendHelpCategories: () => sendHelpCategories,
20573
+ sendHelpCategory: () => sendHelpCategory,
20574
+ sendHistoryView: () => sendHistoryView,
20575
+ sendJobDetail: () => sendJobDetail,
20576
+ sendJobPicker: () => sendJobPicker,
20577
+ sendJobRunsView: () => sendJobRunsView,
20578
+ sendJobsBoard: () => sendJobsBoard,
20579
+ sendMemoryDetail: () => sendMemoryDetail,
20580
+ sendMemoryForgetConfirm: () => sendMemoryForgetConfirm,
20581
+ sendMemoryPage: () => sendMemoryPage,
20582
+ sendModelKeyboard: () => sendModelKeyboard,
20583
+ sendModelSigKeyboard: () => sendModelSigKeyboard,
20584
+ sendPermissionsKeyboard: () => sendPermissionsKeyboard,
20585
+ sendResponseStyleKeyboard: () => sendResponseStyleKeyboard,
20586
+ sendSkillsPage: () => sendSkillsPage,
20587
+ sendThinkingKeyboard: () => sendThinkingKeyboard,
20588
+ sendToolsKeyboard: () => sendToolsKeyboard,
20589
+ sendUnifiedUsage: () => sendUnifiedUsage,
20590
+ sendUsageLimits: () => sendUsageLimits,
20591
+ sendVerboseKeyboard: () => sendVerboseKeyboard,
20592
+ sendVoiceConfigKeyboard: () => sendVoiceConfigKeyboard
20593
+ });
20426
20594
  function buildAccountSlotKeyboard(slots, activeSlotId, rotationMode, callbackPrefix, rotationPrefix) {
20427
20595
  const slotButtons = slots.filter((s) => s.enabled).map((s) => {
20428
20596
  const label2 = s.label || `slot-${s.id}`;
@@ -20470,8 +20638,7 @@ async function handleRotationModeChange(chatId, channel, backendId, mode) {
20470
20638
  const prefix = isGemini ? "" : `${displayName} `;
20471
20639
  await channel.sendText(chatId, `${prefix}Rotation mode set to <b>${ROTATION_MODE_CONFIRM_LABELS[mode]}</b>.`, { parseMode: "html" });
20472
20640
  }
20473
- async function sendHelpCategories(chatId, channel) {
20474
- if (typeof channel.sendKeyboard !== "function") return;
20641
+ async function sendHelpCategories(chatId, channel, messageId) {
20475
20642
  const header2 = `${buildSectionHeader("CC-Claw Commands")}`;
20476
20643
  const cats = Object.keys(HELP_CATEGORIES);
20477
20644
  const rows = [];
@@ -20484,22 +20651,21 @@ async function sendHelpCategories(chatId, channel) {
20484
20651
  }
20485
20652
  rows.push(row);
20486
20653
  }
20487
- await channel.sendKeyboard(chatId, header2, rows);
20654
+ await sendOrEditKeyboard(chatId, channel, messageId, header2, rows);
20488
20655
  }
20489
- async function sendHelpCategory(chatId, category, channel) {
20490
- if (typeof channel.sendKeyboard !== "function") return;
20656
+ async function sendHelpCategory(chatId, category, channel, messageId) {
20491
20657
  const cat = Object.entries(HELP_CATEGORIES).find(([k]) => k.toLowerCase() === category.toLowerCase());
20492
20658
  if (!cat) return;
20493
20659
  const [name, cmds] = cat;
20494
20660
  const header2 = `${buildSectionHeader(`${name} (${cmds.length} commands)`)}`;
20495
20661
  const body = cmds.map((c) => `${c.cmd} \u2014 ${c.desc}`).join("\n");
20496
- await channel.sendKeyboard(chatId, `${header2}
20662
+ await sendOrEditKeyboard(chatId, channel, messageId, `${header2}
20497
20663
 
20498
20664
  ${body}`, [
20499
20665
  [{ label: "\u2190 Back to Categories", data: "help:back" }]
20500
20666
  ]);
20501
20667
  }
20502
- async function sendUnifiedUsage(chatId, channel, view) {
20668
+ async function sendUnifiedUsage(chatId, channel, view, messageId) {
20503
20669
  const { pricing, defaultPricing } = buildUsagePricing();
20504
20670
  const fmt = (n) => `$${n.toFixed(4)}`;
20505
20671
  const fmtK = (n) => `${(n / 1e3).toFixed(0)}K`;
@@ -20555,25 +20721,21 @@ async function sendUnifiedUsage(chatId, channel, view) {
20555
20721
  }
20556
20722
  }
20557
20723
  }
20558
- if (typeof channel.sendKeyboard === "function") {
20559
- const buttons = [
20560
- [
20561
- { label: view === "session" ? "Session \u2713" : "Session", data: "usage:session", ...view === "session" ? { style: "primary" } : {} },
20562
- { label: view === "24h" ? "24h \u2713" : "24h", data: "usage:24h", ...view === "24h" ? { style: "primary" } : {} },
20563
- { label: view === "7d" ? "7d \u2713" : "7d", data: "usage:7d", ...view === "7d" ? { style: "primary" } : {} },
20564
- { label: view === "30d" ? "30d \u2713" : "30d", data: "usage:30d", ...view === "30d" ? { style: "primary" } : {} }
20565
- ],
20566
- [
20567
- { label: view === "model" ? "By Model \u2713" : "By Model", data: "usage:model", ...view === "model" ? { style: "primary" } : {} },
20568
- { label: "Set Limits", data: "usage:limits" }
20569
- ]
20570
- ];
20571
- await channel.sendKeyboard(chatId, lines.join("\n"), buttons);
20572
- } else {
20573
- await channel.sendText(chatId, lines.join("\n"), { parseMode: "plain" });
20574
- }
20724
+ const buttons = [
20725
+ [
20726
+ { label: view === "session" ? "\u2713 Session" : "Session", data: "usage:session", ...view === "session" ? { style: "primary" } : {} },
20727
+ { label: view === "24h" ? "\u2713 24h" : "24h", data: "usage:24h", ...view === "24h" ? { style: "primary" } : {} },
20728
+ { label: view === "7d" ? "\u2713 7d" : "7d", data: "usage:7d", ...view === "7d" ? { style: "primary" } : {} },
20729
+ { label: view === "30d" ? "\u2713 30d" : "30d", data: "usage:30d", ...view === "30d" ? { style: "primary" } : {} }
20730
+ ],
20731
+ [
20732
+ { label: view === "model" ? "\u2713 By Model" : "By Model", data: "usage:model", ...view === "model" ? { style: "primary" } : {} },
20733
+ { label: "Set Limits", data: "usage:limits" }
20734
+ ]
20735
+ ];
20736
+ await sendOrEditKeyboard(chatId, channel, messageId, lines.join("\n"), buttons);
20575
20737
  }
20576
- async function sendUsageLimits(chatId, channel) {
20738
+ async function sendUsageLimits(chatId, channel, messageId) {
20577
20739
  const limits2 = getAllBackendLimits();
20578
20740
  const fmtK = (n) => `${(n / 1e3).toFixed(0)}K`;
20579
20741
  const lines = [buildSectionHeader("Set Usage Limits"), ""];
@@ -20587,27 +20749,19 @@ async function sendUsageLimits(chatId, channel) {
20587
20749
  }
20588
20750
  }
20589
20751
  lines.push("", "Use /limits <backend> <window> <tokens> to set.\nExample: /limits claude daily 500000");
20590
- if (typeof channel.sendKeyboard === "function") {
20591
- const setButtons = getAvailableBackendIds().map((bid) => ({
20592
- label: `Set ${getAdapter(bid).displayName} Limit`,
20593
- data: `usage:limits:set:${bid}`
20594
- }));
20595
- const rows = [];
20596
- for (let i = 0; i < setButtons.length; i += 2) {
20597
- rows.push(setButtons.slice(i, i + 2));
20598
- }
20599
- rows.push([
20600
- { label: "Clear All", data: "usage:limits:clear", style: "danger" }
20601
- ]);
20602
- rows.push([
20603
- { label: "\u2190 Back to Usage", data: "usage:back" }
20604
- ]);
20605
- await channel.sendKeyboard(chatId, lines.join("\n"), rows);
20606
- } else {
20607
- await channel.sendText(chatId, lines.join("\n"), { parseMode: "plain" });
20752
+ const setButtons = getAvailableBackendIds().map((bid) => ({
20753
+ label: `Set ${getAdapter(bid).displayName} Limit`,
20754
+ data: `usage:limits:set:${bid}`
20755
+ }));
20756
+ const rows = [];
20757
+ for (let i = 0; i < setButtons.length; i += 2) {
20758
+ rows.push(setButtons.slice(i, i + 2));
20608
20759
  }
20760
+ rows.push([{ label: "Clear All", data: "usage:limits:clear", style: "danger" }]);
20761
+ rows.push([{ label: "\u2190 Back to Usage", data: "usage:back" }]);
20762
+ await sendOrEditKeyboard(chatId, channel, messageId, lines.join("\n"), rows);
20609
20763
  }
20610
- async function sendHistoryView(chatId, channel, filter) {
20764
+ async function sendHistoryView(chatId, channel, filter, messageId) {
20611
20765
  const limit = filter.limit ?? 10;
20612
20766
  let rows = getRecentMessageLog(chatId, 100).reverse();
20613
20767
  if (filter.backend) {
@@ -20622,7 +20776,9 @@ async function sendHistoryView(chatId, channel, filter) {
20622
20776
  }
20623
20777
  const userMsgs = rows.filter((r) => r.role === "user").slice(0, limit);
20624
20778
  if (userMsgs.length === 0) {
20625
- await channel.sendText(chatId, "No conversation history found.", { parseMode: "plain" });
20779
+ await sendOrEditKeyboard(chatId, channel, messageId, "No conversation history found.", [
20780
+ [[{ label: "Show More (25)", data: "hist:recent:25" }, { label: "Today", data: "hist:today" }, { label: "This Week", data: "hist:week" }]][0]
20781
+ ]);
20626
20782
  return;
20627
20783
  }
20628
20784
  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"];
@@ -20631,28 +20787,28 @@ async function sendHistoryView(chatId, channel, filter) {
20631
20787
  const preview = row.content.slice(0, 80) + (row.content.length > 80 ? "\u2026" : "");
20632
20788
  lines.push(`[${row.backend ?? "?"} \xB7 ${date}] ${preview}`);
20633
20789
  }
20634
- await channel.sendText(chatId, lines.join("\n"), { parseMode: "plain" });
20790
+ lines.push("", "Tip: /history <query> for full-text search");
20635
20791
  const filterRow = [
20636
- { label: "Show More (25)", data: "hist:recent:25" },
20637
- { label: "Today", data: "hist:today" },
20638
- { label: "This Week", data: "hist:week" }
20792
+ { label: filter.limit === 25 ? "\u2713 Show More (25)" : "Show More (25)", data: "hist:recent:25", ...filter.limit === 25 ? { style: "primary" } : {} },
20793
+ { label: filter.period === "today" ? "\u2713 Today" : "Today", data: "hist:today", ...filter.period === "today" ? { style: "primary" } : {} },
20794
+ { label: filter.period === "week" ? "\u2713 This Week" : "This Week", data: "hist:week", ...filter.period === "week" ? { style: "primary" } : {} }
20639
20795
  ];
20640
20796
  const backendRow = getAvailableAdapters().map((a) => ({
20641
- label: a.displayName,
20797
+ label: filter.backend === a.id ? `\u2713 ${a.displayName}` : a.displayName,
20642
20798
  data: `hist:backend:${a.id}`,
20643
- style: "primary"
20799
+ ...filter.backend === a.id ? { style: "primary" } : {}
20644
20800
  }));
20645
- await channel.sendKeyboard(chatId, "Tip: /history <query> for full-text search", [filterRow, backendRow]);
20801
+ await sendOrEditKeyboard(chatId, channel, messageId, lines.join("\n"), [filterRow, backendRow]);
20646
20802
  }
20647
- async function sendVoiceConfigKeyboard(chatId, channel) {
20648
- if (typeof channel.sendKeyboard !== "function") {
20803
+ async function sendVoiceConfigKeyboard(chatId, channel, messageId) {
20804
+ if (typeof channel.sendKeyboard !== "function" && typeof channel.editKeyboard !== "function") {
20649
20805
  await channel.sendText(chatId, "Voice configuration requires an interactive channel (Telegram).", { parseMode: "plain" });
20650
20806
  return;
20651
20807
  }
20652
20808
  const ttsConfig = getVoiceConfig(chatId);
20653
20809
  const sttProvider = getSttProvider(chatId);
20654
20810
  const sttModel = getSttModel(chatId);
20655
- const whisperAvailable = isWhisperCliAvailable();
20811
+ const ffmpegOk = isFfmpegAvailable();
20656
20812
  const buttons = [];
20657
20813
  const groqAvailable = !!process.env.GROQ_API_KEY;
20658
20814
  buttons.push([
@@ -20662,12 +20818,12 @@ async function sendVoiceConfigKeyboard(chatId, channel) {
20662
20818
  ...sttProvider === "groq" ? { style: "primary" } : {}
20663
20819
  },
20664
20820
  {
20665
- label: `${sttProvider === "local-whisper" ? "\u2713 " : ""}\u{1F4BB} Local Whisper${!whisperAvailable ? " (install first)" : ""}`,
20821
+ label: `${sttProvider === "local-whisper" ? "\u2713 " : ""}\u{1F4BB} Local Whisper`,
20666
20822
  data: "vcfg:stt:local-whisper",
20667
20823
  ...sttProvider === "local-whisper" ? { style: "primary" } : {}
20668
20824
  }
20669
20825
  ]);
20670
- if (sttProvider === "local-whisper" || whisperAvailable) {
20826
+ if (sttProvider === "local-whisper") {
20671
20827
  const modelEntries = Object.entries(LOCAL_WHISPER_MODELS);
20672
20828
  for (let i = 0; i < modelEntries.length; i += 2) {
20673
20829
  const row = modelEntries.slice(i, i + 2).map(([id, info]) => {
@@ -20712,14 +20868,158 @@ async function sendVoiceConfigKeyboard(chatId, channel) {
20712
20868
  buttons.push(Object.entries(MACOS_VOICES).map(([id, v]) => ({ label: `${ttsConfig.voiceId === id ? "\u2713 " : ""}${v.name}`, data: `vcfg:v:${id}` })));
20713
20869
  }
20714
20870
  }
20715
- const sttLabel = sttProvider === "groq" ? "Groq (cloud)" : `Local Whisper \xB7 ${sttModel}`;
20871
+ const sttLabel = sttProvider === "groq" ? "Groq (cloud)" : `Local Whisper \xB7 ${sttModel} (on-device)`;
20716
20872
  const ttsLabel = ttsConfig.enabled ? `${ttsConfig.provider === "grok" ? "Grok" : ttsConfig.provider === "macos" ? "macOS" : "ElevenLabs"} replies ON` : "Replies OFF";
20717
- const modelLegend = sttProvider === "local-whisper" || whisperAvailable ? "\n\u25CF = downloaded \u25CB = not yet downloaded" : "";
20873
+ const modelLegend = sttProvider === "local-whisper" ? "\n\u25CF = downloaded \u25CB = tap to download" : "";
20874
+ const prereqWarning = sttProvider === "local-whisper" && !ffmpegOk ? "\n\u26A0\uFE0F ffmpeg not found \u2014 required for audio conversion" : "";
20718
20875
  const header2 = `\u{1F399}\uFE0F Voice Settings
20719
20876
 
20720
20877
  \u{1F3A4} Transcription: ${sttLabel}
20721
- \u{1F50A} Text-to-Speech: ${ttsLabel}${modelLegend}`;
20722
- await channel.sendKeyboard(chatId, header2, buttons);
20878
+ \u{1F50A} Text-to-Speech: ${ttsLabel}${modelLegend}${prereqWarning}`;
20879
+ await sendOrEditKeyboard(chatId, channel, messageId, header2, buttons);
20880
+ }
20881
+ async function sendPermissionsKeyboard(chatId, channel, messageId) {
20882
+ const currentMode = getMode(chatId);
20883
+ const currentExecMode = getExecMode(chatId);
20884
+ const permButtons = Object.entries(PERM_MODES).map(([id, label2]) => [{
20885
+ label: `${id === currentMode ? "\u2713 " : ""}${label2}`,
20886
+ data: `perms:${id}`,
20887
+ ...id === currentMode ? { style: "primary" } : {}
20888
+ }]);
20889
+ const approvalOn = currentExecMode === "approved";
20890
+ permButtons.push([{
20891
+ label: `${approvalOn ? "\u2713 " : ""}\u{1F512} Approve Before Execute: ${approvalOn ? "ON" : "OFF"}`,
20892
+ data: `execmode:${approvalOn ? "yolo" : "approved"}`,
20893
+ ...approvalOn ? { style: "primary" } : {}
20894
+ }]);
20895
+ await sendOrEditKeyboard(chatId, channel, messageId, "\u2699\uFE0F Permission & Execution Settings:", permButtons);
20896
+ }
20897
+ async function sendVerboseKeyboard(chatId, channel, messageId) {
20898
+ const currentVerbose = getVerboseLevel(chatId);
20899
+ const buttons = Object.entries(VERBOSE_LEVELS).map(([id, label2]) => [{
20900
+ label: `${id === currentVerbose ? "\u2713 " : ""}${label2}`,
20901
+ data: `verbose:${id}`,
20902
+ ...id === currentVerbose ? { style: "primary" } : {}
20903
+ }]);
20904
+ await sendOrEditKeyboard(chatId, channel, messageId, "\u{1F441}\uFE0F Tool Visibility:", buttons);
20905
+ }
20906
+ async function sendToolsKeyboard(chatId, channel, messageId) {
20907
+ const toolsMap = getToolsMap(chatId);
20908
+ const currentMode = getMode(chatId);
20909
+ const toolButtons = [];
20910
+ const toolList = [...ALL_TOOLS];
20911
+ for (let i = 0; i < toolList.length; i += 3) {
20912
+ const row = toolList.slice(i, i + 3).map((t) => ({
20913
+ label: `${toolsMap[t] ? "\u2705" : "\u274C"} ${t}`,
20914
+ data: `tool:toggle:${t}`
20915
+ }));
20916
+ toolButtons.push(row);
20917
+ }
20918
+ toolButtons.push([{ label: "Reset to defaults (all on)", data: "tool:reset", style: "danger" }]);
20919
+ const enabledCount = Object.values(toolsMap).filter(Boolean).length;
20920
+ 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)." : "";
20921
+ await sendOrEditKeyboard(
20922
+ chatId,
20923
+ channel,
20924
+ messageId,
20925
+ `\u{1F527} Tools (${enabledCount}/${toolList.length} enabled, mode: ${currentMode})${modeNote}
20926
+ Tap to toggle:`,
20927
+ toolButtons
20928
+ );
20929
+ }
20930
+ async function sendResponseStyleKeyboard(chatId, channel, messageId) {
20931
+ const currentStyle = getResponseStyle(chatId);
20932
+ await sendOrEditKeyboard(chatId, channel, messageId, "\u{1F5E3}\uFE0F AI Response Style:", [
20933
+ [
20934
+ { label: `${currentStyle === "concise" ? "\u2713 " : ""}Concise`, data: "style:concise", ...currentStyle === "concise" ? { style: "primary" } : {} },
20935
+ { label: `${currentStyle === "normal" ? "\u2713 " : ""}Normal`, data: "style:normal", ...currentStyle === "normal" ? { style: "primary" } : {} },
20936
+ { label: `${currentStyle === "detailed" ? "\u2713 " : ""}Detailed`, data: "style:detailed", ...currentStyle === "detailed" ? { style: "primary" } : {} }
20937
+ ]
20938
+ ]);
20939
+ }
20940
+ async function sendModelSigKeyboard(chatId, channel, messageId) {
20941
+ const currentSig = getModelSignature(chatId);
20942
+ await sendOrEditKeyboard(
20943
+ chatId,
20944
+ channel,
20945
+ messageId,
20946
+ `\u{1F9E0} Model Signature: ${currentSig === "on" ? "ON" : "OFF"}
20947
+ Appends model + thinking level to each response.`,
20948
+ [[
20949
+ { label: `${currentSig === "on" ? "\u2713 " : ""}On`, data: "model_sig:on", ...currentSig === "on" ? { style: "success" } : {} },
20950
+ { label: `${currentSig !== "on" ? "\u2713 " : ""}Off`, data: "model_sig:off", ...currentSig !== "on" ? { style: "danger" } : {} }
20951
+ ]]
20952
+ );
20953
+ }
20954
+ async function sendModelKeyboard(chatId, channel, messageId) {
20955
+ let adapter;
20956
+ try {
20957
+ adapter = getAdapterForChat(chatId);
20958
+ } catch {
20959
+ await channel.sendText(chatId, "No backend set. Use /backend first.", { parseMode: "plain" });
20960
+ return;
20961
+ }
20962
+ const models = adapter.availableModels;
20963
+ const current = getModel(chatId) ?? adapter.defaultModel;
20964
+ const isAuto = getModel(chatId) === "auto";
20965
+ const buttons = Object.entries(models).map(([id, info]) => {
20966
+ const tag = info.thinking === "adjustable" ? " \u26A1" : "";
20967
+ return [{
20968
+ label: `${id === current ? "\u2713 " : ""}${info.label}${tag}`,
20969
+ data: `model:${id}`,
20970
+ ...id === current ? { style: "primary" } : {}
20971
+ }];
20972
+ });
20973
+ buttons.unshift([{
20974
+ label: `${isAuto ? "\u2713 " : ""}\u{1F916} Auto (smart routing)`,
20975
+ data: "model:auto",
20976
+ ...isAuto ? { style: "primary" } : {}
20977
+ }]);
20978
+ await sendOrEditKeyboard(chatId, channel, messageId, `Models for ${adapter.displayName}:`, buttons);
20979
+ }
20980
+ async function sendThinkingKeyboard(chatId, channel, messageId, forModelId) {
20981
+ let adapter;
20982
+ try {
20983
+ adapter = getAdapterForChat(chatId);
20984
+ } catch {
20985
+ await channel.sendText(chatId, "No backend set. Use /backend first.", { parseMode: "plain" });
20986
+ return;
20987
+ }
20988
+ const currentModel = forModelId ?? getModel(chatId) ?? adapter.defaultModel;
20989
+ const modelInfo = adapter.availableModels[currentModel];
20990
+ const currentLevel = getThinkingLevel(chatId) || "auto";
20991
+ if (!modelInfo || modelInfo.thinking !== "adjustable" || !modelInfo.thinkingLevels) {
20992
+ await sendOrEditKeyboard(
20993
+ chatId,
20994
+ channel,
20995
+ messageId,
20996
+ `Model ${shortModelName(currentModel)} uses fixed thinking \u2014 no adjustment needed.`,
20997
+ [[{ label: "\u2190 Back to Model", data: "menu:model" }]]
20998
+ );
20999
+ return;
21000
+ }
21001
+ const showThinkingUi = getShowThinkingUi(chatId);
21002
+ const buttons = modelInfo.thinkingLevels.map((level) => [{
21003
+ label: `${level === currentLevel ? "\u2713 " : ""}${level === "auto" ? "Auto" : capitalize(level)}`,
21004
+ data: `thinking:${level}`,
21005
+ ...level === currentLevel ? { style: "primary" } : {}
21006
+ }]);
21007
+ buttons.push([{
21008
+ label: `${showThinkingUi ? "\u2713 " : ""}\u{1F4AD} Show Thinking`,
21009
+ data: "thinking_show_ui:toggle",
21010
+ ...showThinkingUi ? { style: "primary" } : {}
21011
+ }]);
21012
+ await sendOrEditKeyboard(
21013
+ chatId,
21014
+ channel,
21015
+ messageId,
21016
+ `\u{1F4AD} Thinking Level \u2014 ${shortModelName(currentModel)}
21017
+ Current: ${capitalize(currentLevel)}
21018
+ Show thinking tokens: ${showThinkingUi ? "On" : "Off"}${adapter.id !== "claude" ? `
21019
+
21020
+ \u26A0\uFE0F ${adapter.displayName} CLI doesn't stream thinking tokens` : ""}`,
21021
+ buttons
21022
+ );
20723
21023
  }
20724
21024
  async function sendSkillsPage(chatId, channel, skills2, page, messageId) {
20725
21025
  const approved = skills2.filter((s) => s.status === "approved");
@@ -20805,69 +21105,135 @@ async function sendMemoryPage(chatId, channel, page, messageId) {
20805
21105
  await channel.sendText(chatId, "No memories stored yet.", { parseMode: "plain" });
20806
21106
  return;
20807
21107
  }
20808
- const pageSize = 10;
21108
+ const pageSize = 8;
20809
21109
  const totalPages = Math.max(1, Math.ceil(memories.length / pageSize));
20810
21110
  const safePage = Math.max(1, Math.min(page, totalPages));
20811
21111
  const start = (safePage - 1) * pageSize;
20812
21112
  const pageItems = memories.slice(start, start + pageSize);
20813
- const lines = [
20814
- `Memories (${memories.length} total, ${safePage === 1 ? "top " : ""}${pageItems.length} by relevance${totalPages > 1 ? ` \xB7 page ${safePage}/${totalPages}` : ""})`,
20815
- "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501",
20816
- ""
20817
- ];
20818
- for (const [i, m] of pageItems.entries()) {
20819
- lines.push(`${start + i + 1}. ${m.trigger}: ${m.content.slice(0, 60)}${m.content.length > 60 ? "\u2026" : ""}`);
20820
- }
21113
+ const header2 = `\u{1F9E0} Memories (${memories.length} total${totalPages > 1 ? ` \xB7 page ${safePage}/${totalPages}` : ""})`;
20821
21114
  const buttons = [];
20822
- const itemRow = [];
20823
21115
  for (const [i, m] of pageItems.entries()) {
20824
- itemRow.push({ label: `${start + i + 1}`, data: `mem:view:${m.id}` });
20825
- if (itemRow.length === 5) {
20826
- buttons.push([...itemRow]);
20827
- itemRow.length = 0;
20828
- }
21116
+ const num = start + i + 1;
21117
+ const preview = `${m.trigger}: ${m.content}`.slice(0, 32);
21118
+ buttons.push([{
21119
+ label: `${num}. ${preview}${m.content.length > 32 ? "\u2026" : ""}`,
21120
+ data: `mem:view:${m.id}`
21121
+ }]);
20829
21122
  }
20830
- if (itemRow.length > 0) buttons.push([...itemRow]);
20831
21123
  if (totalPages > 1) {
20832
21124
  const navRow = [];
20833
21125
  if (safePage > 1) navRow.push({ label: `\u2190 Page ${safePage - 1}`, data: `mem:page:${safePage - 1}` });
20834
- navRow.push({ label: `${safePage}/${totalPages}`, data: "mem:page:noop" });
21126
+ navRow.push({ label: `${safePage}/${totalPages}`, data: "mem:noop" });
20835
21127
  if (safePage < totalPages) navRow.push({ label: `Page ${safePage + 1} \u2192`, data: `mem:page:${safePage + 1}` });
20836
21128
  buttons.push(navRow);
20837
21129
  }
20838
- const footerRow = [];
20839
- if (memories.length > 50) {
20840
- footerRow.push({ label: "Show All (file)", data: "mem:showall", style: "primary" });
21130
+ const footerRow = [
21131
+ { label: "\u{1F50D} Search", data: "mem:noop", switchInlineQueryCurrentChat: "" }
21132
+ ];
21133
+ if (memories.length > 30) {
21134
+ footerRow.push({ label: "\u{1F4C4} Export All", data: "mem:showall", style: "primary" });
20841
21135
  }
20842
- footerRow.push({ label: "Search: /memory <query>", data: "mem:page:noop" });
20843
21136
  buttons.push(footerRow);
21137
+ await sendOrEditKeyboard(chatId, channel, messageId, header2, buttons);
21138
+ }
21139
+ async function sendMemoryDetail(chatId, memoryId, channel, messageId) {
21140
+ const memory2 = getMemoryById(memoryId);
21141
+ if (!memory2) {
21142
+ await sendOrEditKeyboard(chatId, channel, messageId, "This memory no longer exists.", [
21143
+ [{ label: "\u2190 Back to List", data: "mem:back" }]
21144
+ ]);
21145
+ return;
21146
+ }
21147
+ const lines = [
21148
+ `\u{1F9E0} Memory #${memory2.id}`,
21149
+ buildSectionHeader("", 26),
21150
+ "",
21151
+ `Trigger: ${memory2.trigger}`,
21152
+ `Content: ${memory2.content}`,
21153
+ "",
21154
+ `Salience: ${memory2.salience.toFixed(2)} \xB7 Created: ${memory2.created_at.slice(0, 10)}`
21155
+ ];
21156
+ const buttons = [
21157
+ [
21158
+ { label: "\u270F\uFE0F Edit", data: `mem:edit:${memoryId}`, style: "primary" },
21159
+ { label: "\u{1F5D1} Forget", data: `mem:forget:${memoryId}`, style: "danger" }
21160
+ ],
21161
+ [{ label: "\u2190 Back to List", data: "mem:back" }]
21162
+ ];
20844
21163
  await sendOrEditKeyboard(chatId, channel, messageId, lines.join("\n"), buttons);
20845
21164
  }
20846
- async function sendHeartbeatKeyboard(chatId, channel, messageId) {
21165
+ async function sendMemoryForgetConfirm(chatId, memoryId, channel, messageId) {
21166
+ const memory2 = getMemoryById(memoryId);
21167
+ if (!memory2) {
21168
+ await sendOrEditKeyboard(chatId, channel, messageId, "Memory already deleted.", [
21169
+ [{ label: "\u2190 Back to List", data: "mem:back" }]
21170
+ ]);
21171
+ return;
21172
+ }
21173
+ const lines = [
21174
+ "Delete this memory?",
21175
+ "",
21176
+ `"${memory2.trigger}: ${memory2.content}"`
21177
+ ];
21178
+ const buttons = [
21179
+ [
21180
+ { label: "\u2705 Confirm Delete", data: `mem:forget:confirm:${memoryId}`, style: "danger" },
21181
+ { label: "Cancel", data: `mem:view:${memoryId}` }
21182
+ ]
21183
+ ];
21184
+ await sendOrEditKeyboard(chatId, channel, messageId, lines.join("\n"), buttons);
21185
+ }
21186
+ function getHeartbeatContext(chatId) {
21187
+ const { findHeartbeatJob: findHeartbeatJob2 } = (init_heartbeat2(), __toCommonJS(heartbeat_exports));
21188
+ const job = findHeartbeatJob2();
20847
21189
  const config2 = getHeartbeatConfig(chatId);
20848
- const enabled = config2?.enabled === 1;
20849
- const intervalMs = config2?.intervalMs ?? 18e5;
20850
- const intervalMin = intervalMs / 6e4;
20851
- const activeStart = config2?.activeStart ?? "08:00";
20852
- const activeEnd = config2?.activeEnd ?? "22:00";
20853
21190
  const watches = getActiveWatches(chatId);
20854
- const fallbacks = parseHeartbeatFallbacks(config2?.fallbacks ?? null);
20855
- const backendDisplay = config2?.backend ? capitalize(config2.backend) : "Default";
20856
- const modelDisplay = config2?.model ?? "default";
20857
- const thinkingDisplay = config2?.thinking ?? "off";
20858
- const lastBeat = config2?.lastBeatAt ? config2.lastBeatAt.replace("T", " ").slice(0, 16) : "never";
20859
- const watchNote = watches.length > 0 ? ` \xB7 ${watches.length} watch${watches.length !== 1 ? "es" : ""}` : "";
20860
- const fallbackNote = fallbacks.length > 0 ? `Fallbacks: ${fallbacks.map((f) => `${capitalize(f.backend)}${f.model ? ` (${shortModelName(f.model)})` : ""}`).join(" \u2192 ")}` : "";
20861
- const header2 = [
20862
- `\u{1FAC0} Heartbeat \u2014 ${enabled ? "ON" : "OFF"} \xB7 Every ${intervalMin} min \xB7 ${activeStart}\u2013${activeEnd}${watchNote}`,
20863
- `Backend: ${backendDisplay} \xB7 Model: ${modelDisplay} \xB7 Last: ${lastBeat}`,
20864
- fallbackNote
20865
- ].filter(Boolean).join("\n");
21191
+ return {
21192
+ job,
21193
+ activeStart: config2?.activeStart ?? "08:00",
21194
+ activeEnd: config2?.activeEnd ?? "22:00",
21195
+ lastBeatAt: config2?.lastBeatAt ?? job?.lastRunAt ?? null,
21196
+ watches
21197
+ };
21198
+ }
21199
+ function formatEngine(job) {
21200
+ if (!job) return "Not configured";
21201
+ const backend2 = job.backend ?? "not set";
21202
+ let modelName = job.model ?? "not set";
21203
+ if (job.model) {
21204
+ try {
21205
+ modelName = shortModelName(job.model);
21206
+ } catch {
21207
+ }
21208
+ }
21209
+ return `${capitalize(backend2)} \xB7 ${modelName}`;
21210
+ }
21211
+ async function sendHeartbeatKeyboard(chatId, channel, messageId) {
21212
+ const { job, activeStart, activeEnd, lastBeatAt, watches } = getHeartbeatContext(chatId);
21213
+ const enabled = job ? !!job.enabled : false;
21214
+ const intervalMin = job ? (job.everyMs ?? 18e5) / 6e4 : 30;
21215
+ const fallbacks = job?.fallbacks ?? [];
21216
+ const slotLabel = job?.credentialSlotId ? resolveSlotLabel(job.backend ?? void 0, job.credentialSlotId) : null;
21217
+ const lastBeat = lastBeatAt ? String(lastBeatAt).replace("T", " ").slice(0, 16) : "never";
21218
+ const lines = [
21219
+ "\u{1FAC0} Heartbeat",
21220
+ buildSectionHeader("", 26),
21221
+ "",
21222
+ `Status: ${enabled ? "ON" : "OFF"} \xB7 Every ${intervalMin} min`,
21223
+ `Hours: ${activeStart}\u2013${activeEnd}`,
21224
+ `Engine: ${formatEngine(job)}`
21225
+ ];
21226
+ if (slotLabel) lines.push(`Account: ${slotLabel}`);
21227
+ if (fallbacks.length > 0) {
21228
+ lines.push(`Fallbacks: ${fallbacks.map((f) => `${capitalize(f.backend)}${f.model ? ` (${shortModelName(f.model)})` : ""}`).join(" \u2192 ")}`);
21229
+ }
21230
+ if (job?.thinking && job.thinking !== "auto") lines.push(`Thinking: ${job.thinking}`);
21231
+ if (job?.timeout && job.timeout !== 120) lines.push(`Timeout: ${job.timeout}s`);
21232
+ lines.push(`Last run: ${lastBeat}`);
20866
21233
  const buttons = [];
20867
21234
  buttons.push([
20868
21235
  { label: `${enabled ? "\u2713 " : ""}On`, data: "hb:on", ...enabled ? { style: "success" } : {} },
20869
- { label: `${!enabled ? "\u2713 " : ""}Off`, data: "hb:off", ...!enabled ? { style: "danger" } : {} },
20870
- { label: "\u25B6 Run Now", data: "hb:run", style: "primary" }
21236
+ { label: `${!enabled ? "\u2713 " : ""}Off`, data: "hb:off", ...!enabled ? { style: "danger" } : {} }
20871
21237
  ]);
20872
21238
  const presets = [15, 30, 60, 120];
20873
21239
  buttons.push(presets.map((m) => ({
@@ -20875,63 +21241,225 @@ async function sendHeartbeatKeyboard(chatId, channel, messageId) {
20875
21241
  data: `hb:interval:${m}`,
20876
21242
  ...m === intervalMin ? { style: "primary" } : {}
20877
21243
  })));
20878
- const available = getAvailableBackendIds();
20879
- const backendRow = [
20880
- {
20881
- label: `${!config2?.backend ? "\u2713 " : ""}Default`,
20882
- data: "hb:backend:default",
20883
- ...!config2?.backend ? { style: "primary" } : {}
20884
- },
20885
- ...available.map((bid) => ({
20886
- label: `${config2?.backend === bid ? "\u2713 " : ""}${capitalize(bid)}`,
20887
- data: `hb:backend:${bid}`,
20888
- ...config2?.backend === bid ? { style: "primary" } : {}
20889
- }))
21244
+ buttons.push([
21245
+ { label: `\u23F0 Active Hours`, data: "hb:hours" },
21246
+ { label: "\u25B6 Run Now", data: "hb:run", style: "primary" }
21247
+ ]);
21248
+ buttons.push([
21249
+ { label: "\u{1F9E0} Engine & Account", data: "hb:engine" },
21250
+ { label: `\u{1F517} Fallbacks${fallbacks.length > 0 ? ` (${fallbacks.length})` : ""}`, data: "hb:fallbacks" }
21251
+ ]);
21252
+ const row5 = [
21253
+ { label: `\u{1F441} Watches${watches.length > 0 ? ` (${watches.length})` : ""}`, data: "hb:watches" },
21254
+ { label: "\u23F1 Timeout", data: "hb:timeout" },
21255
+ { label: "\u{1F4AD} Thinking", data: "hb:thinking" }
20890
21256
  ];
20891
- buttons.push(backendRow);
20892
- const targetBackend = config2?.backend ?? getBackend(chatId) ?? "claude";
20893
- try {
20894
- const adapter = getAdapter(targetBackend);
20895
- const models = Object.entries(adapter.availableModels);
20896
- if (models.length > 0) {
20897
- const modelRow = [
20898
- {
20899
- label: `${!config2?.model ? "\u2713 " : ""}Default`,
20900
- data: "hb:model:default",
20901
- ...!config2?.model ? { style: "primary" } : {}
20902
- },
20903
- ...models.slice(0, 4).map(([key]) => ({
20904
- label: `${config2?.model === key ? "\u2713 " : ""}${shortModelName(key)}`,
21257
+ buttons.push(row5);
21258
+ await sendOrEditKeyboard(chatId, channel, messageId, lines.join("\n"), buttons);
21259
+ }
21260
+ async function sendHeartbeatEngine(chatId, channel, messageId) {
21261
+ const { job } = getHeartbeatContext(chatId);
21262
+ const currentBackend = job?.backend ?? null;
21263
+ const currentModel = job?.model ?? null;
21264
+ const slotLabel = job?.credentialSlotId ? resolveSlotLabel(currentBackend ?? void 0, job.credentialSlotId) : "Auto (rotate)";
21265
+ const lines = [
21266
+ "\u{1FAC0} Heartbeat \u203A Engine & Account",
21267
+ buildSectionHeader("", 26),
21268
+ "",
21269
+ `Backend: ${currentBackend ? capitalize(currentBackend) : "\u26A0\uFE0F Not set"}`,
21270
+ `Model: ${currentModel ? shortModelName(currentModel) : "\u26A0\uFE0F Not set"}`,
21271
+ `Account: ${slotLabel}`
21272
+ ];
21273
+ const buttons = [];
21274
+ const available = getAvailableBackendIds();
21275
+ const backendBtns = available.map((bid) => ({
21276
+ label: `${currentBackend === bid ? "\u2713 " : ""}${capitalize(bid)}`,
21277
+ data: `hb:backend:${bid}`,
21278
+ ...currentBackend === bid ? { style: "primary" } : {}
21279
+ }));
21280
+ for (let i = 0; i < backendBtns.length; i += 3) {
21281
+ buttons.push(backendBtns.slice(i, i + 3));
21282
+ }
21283
+ if (currentBackend) {
21284
+ try {
21285
+ const adapter = getAdapter(currentBackend);
21286
+ const models = Object.entries(adapter.availableModels);
21287
+ if (models.length > 0) {
21288
+ buttons.push([{ label: "\u2500\u2500 Model \u2500\u2500", data: "hb:noop" }]);
21289
+ const modelBtns = models.map(([key]) => ({
21290
+ label: `${currentModel === key ? "\u2713 " : ""}${shortModelName(key)}`,
20905
21291
  data: `hb:model:${key}`,
20906
- ...config2?.model === key ? { style: "primary" } : {}
20907
- }))
20908
- ];
20909
- buttons.push(modelRow);
21292
+ ...currentModel === key ? { style: "primary" } : {}
21293
+ }));
21294
+ for (let i = 0; i < modelBtns.length; i += 3) {
21295
+ buttons.push(modelBtns.slice(i, i + 3));
21296
+ }
21297
+ }
21298
+ } catch {
20910
21299
  }
20911
- } catch {
21300
+ const isGemini = currentBackend === "gemini";
21301
+ const slots = isGemini ? getGeminiSlots() : getBackendSlots(currentBackend);
21302
+ const enabledSlots = slots.filter((s) => s.enabled !== 0);
21303
+ if (enabledSlots.length > 0) {
21304
+ buttons.push([{ label: "\u2500\u2500 Account \u2500\u2500", data: "hb:noop" }]);
21305
+ buttons.push([{
21306
+ label: `${!job?.credentialSlotId ? "\u2713 " : ""}Auto (rotate)`,
21307
+ data: "hb:slot:auto",
21308
+ ...!job?.credentialSlotId ? { style: "primary" } : {}
21309
+ }]);
21310
+ for (const s of enabledSlots) {
21311
+ const icon = s.slotType === "api_key" ? "\u{1F511}" : "\u{1F4E7}";
21312
+ const label2 = s.label || s.email || `Slot #${s.id}`;
21313
+ buttons.push([{
21314
+ label: `${job?.credentialSlotId === s.id ? "\u2713 " : ""}${icon} ${label2}`,
21315
+ data: `hb:slot:${s.id}`,
21316
+ ...job?.credentialSlotId === s.id ? { style: "primary" } : {}
21317
+ }]);
21318
+ }
21319
+ }
21320
+ } else {
21321
+ buttons.push([{ label: "\u26A0\uFE0F Pick a backend first", data: "hb:noop" }]);
20912
21322
  }
20913
- const fbCandidates = available.filter((bid) => !fallbacks.some((f) => f.backend === bid) && bid !== config2?.backend);
21323
+ buttons.push([{ label: "\u2190 Back", data: "hb:back" }]);
21324
+ await sendOrEditKeyboard(chatId, channel, messageId, lines.join("\n"), buttons);
21325
+ }
21326
+ async function sendHeartbeatFallbacks(chatId, channel, messageId) {
21327
+ const { job } = getHeartbeatContext(chatId);
21328
+ const fallbacks = job?.fallbacks ?? [];
21329
+ const currentBackend = job?.backend ?? null;
21330
+ const lines = [
21331
+ "\u{1FAC0} Heartbeat \u203A Fallbacks",
21332
+ buildSectionHeader("", 26),
21333
+ "",
21334
+ "If the primary backend is unavailable,",
21335
+ "these fire in order:",
21336
+ ""
21337
+ ];
20914
21338
  if (fallbacks.length > 0) {
20915
- buttons.push([
20916
- { label: `\u{1F504} ${fallbacks.map((f) => capitalize(f.backend)).join(" \u2192 ")}`, data: "hb:noop" },
20917
- { label: "\u2715 Clear", data: "hb:fb:clear", style: "danger" }
20918
- ]);
21339
+ for (let i = 0; i < fallbacks.length; i++) {
21340
+ const f = fallbacks[i];
21341
+ const model2 = f.model ? ` (${shortModelName(f.model)})` : "";
21342
+ lines.push(` ${i + 1}. ${capitalize(f.backend)}${model2}`);
21343
+ }
21344
+ } else {
21345
+ lines.push(" (none configured)");
20919
21346
  }
20920
- if (fbCandidates.length > 0) {
20921
- buttons.push(fbCandidates.slice(0, 4).map((bid) => ({
20922
- label: `+ ${capitalize(bid)} fallback`,
21347
+ const buttons = [];
21348
+ const available = getAvailableBackendIds();
21349
+ const used = new Set([currentBackend, ...fallbacks.map((f) => f.backend)].filter(Boolean));
21350
+ const candidates = available.filter((bid) => !used.has(bid));
21351
+ if (candidates.length > 0) {
21352
+ const addBtns = candidates.map((bid) => ({
21353
+ label: `+ ${capitalize(bid)}`,
20923
21354
  data: `hb:fb:add:${bid}`
20924
- })));
21355
+ }));
21356
+ for (let i = 0; i < addBtns.length; i += 3) {
21357
+ buttons.push(addBtns.slice(i, i + 3));
21358
+ }
20925
21359
  }
20926
- const optionsRow = [];
21360
+ if (fallbacks.length > 0) {
21361
+ buttons.push([{ label: "\u2715 Clear All", data: "hb:fb:clear", style: "danger" }]);
21362
+ }
21363
+ buttons.push([{ label: "\u2190 Back", data: "hb:back" }]);
21364
+ await sendOrEditKeyboard(chatId, channel, messageId, lines.join("\n"), buttons);
21365
+ }
21366
+ async function sendHeartbeatHours(chatId, channel, messageId) {
21367
+ const config2 = getHeartbeatConfig(chatId);
21368
+ const activeStart = config2?.activeStart ?? "08:00";
21369
+ const activeEnd = config2?.activeEnd ?? "22:00";
21370
+ const lines = [
21371
+ "\u{1FAC0} Heartbeat \u203A Active Hours",
21372
+ buildSectionHeader("", 26),
21373
+ "",
21374
+ `Current: ${activeStart}\u2013${activeEnd}`,
21375
+ "Heartbeat only runs during these hours."
21376
+ ];
21377
+ const startHours = ["06:00", "07:00", "08:00", "09:00", "10:00"];
21378
+ const endHours = ["20:00", "21:00", "22:00", "23:00", "00:00"];
21379
+ const buttons = [];
21380
+ buttons.push([{ label: "\u2500\u2500 Start \u2500\u2500", data: "hb:noop" }]);
21381
+ buttons.push(startHours.map((h) => ({
21382
+ label: `${h === activeStart ? "\u2713 " : ""}${h}`,
21383
+ data: `hb:hstart:${h}`,
21384
+ ...h === activeStart ? { style: "primary" } : {}
21385
+ })));
21386
+ buttons.push([{ label: "\u2500\u2500 End \u2500\u2500", data: "hb:noop" }]);
21387
+ buttons.push(endHours.map((h) => ({
21388
+ label: `${h === activeEnd ? "\u2713 " : ""}${h}`,
21389
+ data: `hb:hend:${h}`,
21390
+ ...h === activeEnd ? { style: "primary" } : {}
21391
+ })));
21392
+ buttons.push([{ label: "\u2190 Back", data: "hb:back" }]);
21393
+ await sendOrEditKeyboard(chatId, channel, messageId, lines.join("\n"), buttons);
21394
+ }
21395
+ async function sendHeartbeatTimeout(chatId, channel, messageId) {
21396
+ const { job } = getHeartbeatContext(chatId);
21397
+ const current = job?.timeout ?? 120;
21398
+ const lines = [
21399
+ "\u{1FAC0} Heartbeat \u203A Timeout",
21400
+ buildSectionHeader("", 26),
21401
+ "",
21402
+ `Current: ${current}s (${Math.round(current / 60)} min)`,
21403
+ "Max time per heartbeat check."
21404
+ ];
21405
+ const presets = [60, 120, 300, 600];
21406
+ const buttons = [];
21407
+ buttons.push(presets.map((s) => ({
21408
+ label: `${s === current ? "\u2713 " : ""}${s >= 60 ? `${Math.round(s / 60)} min` : `${s}s`}`,
21409
+ data: `hb:tout:${s}`,
21410
+ ...s === current ? { style: "primary" } : {}
21411
+ })));
21412
+ buttons.push([{ label: "\u2190 Back", data: "hb:back" }]);
21413
+ await sendOrEditKeyboard(chatId, channel, messageId, lines.join("\n"), buttons);
21414
+ }
21415
+ async function sendHeartbeatThinking(chatId, channel, messageId) {
21416
+ const { job } = getHeartbeatContext(chatId);
21417
+ const current = job?.thinking ?? "auto";
21418
+ const lines = [
21419
+ "\u{1FAC0} Heartbeat \u203A Thinking Level",
21420
+ buildSectionHeader("", 26),
21421
+ "",
21422
+ `Current: ${current}`,
21423
+ "Reasoning depth for heartbeat checks."
21424
+ ];
21425
+ const levels = ["auto", "off", "low", "medium", "high"];
21426
+ const buttons = [];
21427
+ buttons.push(levels.slice(0, 3).map((l) => ({
21428
+ label: `${l === current ? "\u2713 " : ""}${capitalize(l)}`,
21429
+ data: `hb:think:${l}`,
21430
+ ...l === current ? { style: "primary" } : {}
21431
+ })));
21432
+ buttons.push(levels.slice(3).map((l) => ({
21433
+ label: `${l === current ? "\u2713 " : ""}${capitalize(l)}`,
21434
+ data: `hb:think:${l}`,
21435
+ ...l === current ? { style: "primary" } : {}
21436
+ })));
21437
+ buttons.push([{ label: "\u2190 Back", data: "hb:back" }]);
21438
+ await sendOrEditKeyboard(chatId, channel, messageId, lines.join("\n"), buttons);
21439
+ }
21440
+ async function sendHeartbeatWatches(chatId, channel, messageId) {
21441
+ const watches = getActiveWatches(chatId);
21442
+ const lines = [
21443
+ "\u{1FAC0} Heartbeat \u203A Watches",
21444
+ buildSectionHeader("", 26),
21445
+ ""
21446
+ ];
20927
21447
  if (watches.length > 0) {
20928
- optionsRow.push({ label: `\u{1F441} Watches (${watches.length})`, data: "hb:watches" });
21448
+ for (const w of watches) {
21449
+ const expiry = w.expiresAt ? ` (until ${w.expiresAt.slice(0, 16)})` : "";
21450
+ lines.push(` ${w.id}. ${w.description}${expiry}`);
21451
+ }
21452
+ lines.push("");
21453
+ lines.push("Remove: /watch remove <id>");
20929
21454
  } else {
20930
- optionsRow.push({ label: "\u{1F441} Add Watch", data: "hb:addwatch" });
21455
+ lines.push(" (no active watches)");
20931
21456
  }
20932
- optionsRow.push({ label: `\u23F0 Hours: ${activeStart}\u2013${activeEnd}`, data: "hb:noop" });
20933
- buttons.push(optionsRow);
20934
- await sendOrEditKeyboard(chatId, channel, messageId, header2, buttons);
21457
+ lines.push("");
21458
+ lines.push("Add: /watch add <description>");
21459
+ lines.push("Example: /watch add Check email for urgent messages");
21460
+ const buttons = [];
21461
+ buttons.push([{ label: "\u2190 Back", data: "hb:back" }]);
21462
+ await sendOrEditKeyboard(chatId, channel, messageId, lines.join("\n"), buttons);
20935
21463
  }
20936
21464
  async function sendForgetPicker(chatId, channel, page, messageId) {
20937
21465
  const memories = listMemories();
@@ -21159,94 +21687,223 @@ async function sendJobPicker(chatId, channel, action) {
21159
21687
  buttons.push([{ label: "Cancel", data: "job:back" }]);
21160
21688
  await channel.sendKeyboard(chatId, `${actionLabel} which job?`, buttons);
21161
21689
  }
21162
- async function sendCurrentProposal(chatId, channel) {
21163
- const { getReviewSession: getReviewSession2, getInsightById: getInsightById2, deleteReviewSession: deleteReviewSession2 } = await Promise.resolve().then(() => (init_store4(), store_exports4));
21690
+ async function sendCurrentProposal(chatId, channel, messageId) {
21691
+ const { getReviewSession: getReviewSession2, getInsightById: getInsightById2, deleteReviewSession: deleteReviewSession2, advanceReviewSession: advanceReviewSession2 } = await Promise.resolve().then(() => (init_store4(), store_exports4));
21164
21692
  const { formatProposalCardWithProgress: formatProposalCardWithProgress2, buildProposalKeyboard: buildProposalKeyboard2, buildReviewCompleteMessage: buildReviewCompleteMessage3 } = await Promise.resolve().then(() => (init_propose(), propose_exports));
21165
21693
  const session2 = getReviewSession2(getDb(), chatId);
21166
21694
  if (!session2) return;
21167
21695
  if (session2.currentIndex >= session2.insightIds.length) {
21168
21696
  const summary = buildReviewCompleteMessage3(session2.results);
21169
21697
  deleteReviewSession2(getDb(), chatId);
21170
- await channel.sendText(chatId, summary, { parseMode: "plain" });
21698
+ await sendOrEditKeyboard(
21699
+ chatId,
21700
+ channel,
21701
+ messageId,
21702
+ summary,
21703
+ [[{ label: "\u2190 Back to Dashboard", data: "evolve:menu" }]]
21704
+ );
21171
21705
  return;
21172
21706
  }
21173
21707
  const insightId = session2.insightIds[session2.currentIndex];
21174
21708
  const insight = getInsightById2(getDb(), insightId);
21175
21709
  if (!insight || insight.status !== "pending") {
21176
- const { advanceReviewSession: advanceReviewSession2 } = await Promise.resolve().then(() => (init_store4(), store_exports4));
21177
21710
  advanceReviewSession2(getDb(), chatId, insightId, "skipped");
21178
- return sendCurrentProposal(chatId, channel);
21711
+ return sendCurrentProposal(chatId, channel, messageId);
21179
21712
  }
21180
21713
  const card = formatProposalCardWithProgress2(insight, session2.currentIndex, session2.insightIds.length);
21181
- const kb = buildProposalKeyboard2(insight.id, insight.category);
21182
- await channel.sendKeyboard(chatId, card, kb);
21714
+ const kb = [
21715
+ ...buildProposalKeyboard2(insight.id, insight.category),
21716
+ [{ label: "\u2190 Dashboard", data: "evolve:menu" }]
21717
+ ];
21718
+ await sendOrEditKeyboard(chatId, channel, messageId, card, kb);
21719
+ }
21720
+ function resolveBackendAccountLabel(chatId, backendId) {
21721
+ if (backendId === "gemini") {
21722
+ const slotId2 = getChatGeminiSlotId(chatId);
21723
+ if (slotId2 == null) return "Auto";
21724
+ const slots2 = getGeminiSlots();
21725
+ const slot2 = slots2.find((s) => s.id === slotId2);
21726
+ return slot2?.label ?? `slot-${slotId2}`;
21727
+ }
21728
+ const slotId = getChatBackendSlotId(chatId, backendId);
21729
+ if (slotId == null) return "Auto";
21730
+ const slots = getBackendSlots(backendId);
21731
+ const slot = slots.find((s) => s.id === slotId);
21732
+ return slot?.label ?? `slot-${slotId}`;
21733
+ }
21734
+ function backendPickerLabel(chatId, backendId, currentBackend) {
21735
+ const adapter = getAdapter(backendId);
21736
+ const model2 = backendId === currentBackend ? getModel(chatId) ?? adapter.defaultModel : adapter.defaultModel;
21737
+ const modelInfo = adapter.availableModels[model2];
21738
+ const modelShort = modelInfo?.label ?? model2;
21739
+ const thinking2 = backendId === currentBackend ? getThinkingLevel(chatId) ?? "auto" : "auto";
21740
+ const account = resolveBackendAccountLabel(chatId, backendId);
21741
+ const active = backendId === currentBackend ? "\u2713 " : "";
21742
+ return `${active}${adapter.displayName} \xB7 ${modelShort} \xB7 ${capitalize(thinking2)} \xB7 ${account}`;
21743
+ }
21744
+ function backendConfigSummary(chatId, backendId, switched) {
21745
+ const adapter = getAdapter(backendId);
21746
+ const model2 = getModel(chatId) ?? adapter.defaultModel;
21747
+ const modelInfo = adapter.availableModels[model2];
21748
+ const modelLabel = modelInfo?.label ?? model2;
21749
+ const thinking2 = getThinkingLevel(chatId) ?? "auto";
21750
+ const account = resolveBackendAccountLabel(chatId, backendId);
21751
+ const header2 = switched ? `\u2705 Switched to ${adapter.displayName}` : `\u2699\uFE0F ${adapter.displayName}`;
21752
+ return `${header2}
21753
+
21754
+ Model: ${modelLabel}
21755
+ Thinking: ${capitalize(thinking2)}
21756
+ Account: ${account}`;
21183
21757
  }
21184
- async function sendBackendSwitchConfirmation(chatId, target, channel) {
21185
- const current = getBackend(chatId);
21186
- const targetAdapter = getAdapter(target);
21187
- if (current === target) {
21188
- await channel.sendText(chatId, `Already using ${targetAdapter.displayName}.`, { parseMode: "plain" });
21758
+ async function sendBackendPicker(chatId, channel, messageId) {
21759
+ if (typeof channel.sendKeyboard !== "function") return;
21760
+ const currentBackend = getBackend(chatId) ?? "";
21761
+ const ids = getAvailableChatBackendIds();
21762
+ const buttons = ids.map((id) => [{
21763
+ label: backendPickerLabel(chatId, id, currentBackend),
21764
+ data: `backend:${id}`,
21765
+ ...id === currentBackend ? { style: "primary" } : {}
21766
+ }]);
21767
+ await sendOrEditKeyboard(chatId, channel, messageId, "Select AI backend:", buttons);
21768
+ }
21769
+ async function sendBackendConfigPanel(chatId, backendId, channel, messageId, switched = false) {
21770
+ if (typeof channel.sendKeyboard !== "function") return;
21771
+ const summary = backendConfigSummary(chatId, backendId, switched);
21772
+ const buttons = [
21773
+ [
21774
+ { label: "Model", data: `bconf:model:${backendId}` },
21775
+ { label: "Thinking", data: `bconf:thinking:${backendId}` },
21776
+ { label: "Account", data: `bconf:account:${backendId}` }
21777
+ ],
21778
+ [{ label: "\u2190 Back to Backends", data: "bconf:back" }]
21779
+ ];
21780
+ await sendOrEditKeyboard(chatId, channel, messageId, summary, buttons);
21781
+ }
21782
+ async function sendBackendModelPicker(chatId, backendId, channel, messageId) {
21783
+ if (typeof channel.sendKeyboard !== "function") return;
21784
+ const adapter = getAdapter(backendId);
21785
+ const currentModel = getModel(chatId) ?? adapter.defaultModel;
21786
+ const summary = backendConfigSummary(chatId, backendId, false);
21787
+ const modelButtons = Object.entries(adapter.availableModels).map(([id, info]) => [{
21788
+ label: `${id === currentModel ? "\u2713 " : ""}${info.label}`,
21789
+ data: `bconf:setmodel:${backendId}:${id}`,
21790
+ ...id === currentModel ? { style: "primary" } : {}
21791
+ }]);
21792
+ modelButtons.push([{ label: "\u2190 Back", data: `bconf:panel:${backendId}` }]);
21793
+ await sendOrEditKeyboard(chatId, channel, messageId, `${summary}
21794
+
21795
+ Pick a model:`, modelButtons);
21796
+ }
21797
+ async function sendBackendThinkingPicker(chatId, backendId, channel, messageId) {
21798
+ if (typeof channel.sendKeyboard !== "function") return;
21799
+ const adapter = getAdapter(backendId);
21800
+ const currentModel = getModel(chatId) ?? adapter.defaultModel;
21801
+ const modelInfo = adapter.availableModels[currentModel];
21802
+ const supportsThinking = modelInfo?.thinking === "adjustable";
21803
+ const summary = backendConfigSummary(chatId, backendId, false);
21804
+ if (!supportsThinking) {
21805
+ await sendOrEditKeyboard(
21806
+ chatId,
21807
+ channel,
21808
+ messageId,
21809
+ `${summary}
21810
+
21811
+ \u26A0\uFE0F Thinking is fixed for ${modelInfo?.label ?? currentModel}.`,
21812
+ [[{ label: "\u2190 Back", data: `bconf:panel:${backendId}` }]]
21813
+ );
21189
21814
  return;
21190
21815
  }
21191
- if (typeof channel.sendKeyboard === "function") {
21192
- await channel.sendKeyboard(
21816
+ const current = getThinkingLevel(chatId) ?? "auto";
21817
+ const levels = [
21818
+ ["auto", "Auto"],
21819
+ ["off", "Off"],
21820
+ ["low", "Low"],
21821
+ ["medium", "Medium"],
21822
+ ["high", "High"],
21823
+ ["extra_high", "Extra High"]
21824
+ ];
21825
+ const buttons = levels.map(([val, label2]) => [{
21826
+ label: `${val === current ? "\u2713 " : ""}${label2}`,
21827
+ data: `bconf:setthinking:${backendId}:${val}`,
21828
+ ...val === current ? { style: "primary" } : {}
21829
+ }]);
21830
+ buttons.push([{ label: "\u2190 Back", data: `bconf:panel:${backendId}` }]);
21831
+ await sendOrEditKeyboard(chatId, channel, messageId, `${summary}
21832
+
21833
+ Pick thinking level:`, buttons);
21834
+ }
21835
+ async function sendBackendAccountPicker(chatId, backendId, channel, messageId) {
21836
+ if (typeof channel.sendKeyboard !== "function") return;
21837
+ const summary = backendConfigSummary(chatId, backendId, false);
21838
+ const isGemini = backendId === "gemini";
21839
+ const slots = isGemini ? getGeminiSlots() : getBackendSlots(backendId);
21840
+ const enabledSlots = slots.filter((s) => s.enabled !== 0 && s.enabled !== false);
21841
+ if (enabledSlots.length === 0) {
21842
+ await sendOrEditKeyboard(
21193
21843
  chatId,
21194
- `\u{1F504} Switch to ${targetAdapter.displayName}?
21195
- Your conversation history is preserved. ${targetAdapter.displayName} will receive a summary of recent context and can access your full history on request.`,
21196
- [
21197
- [
21198
- { label: `Switch to ${targetAdapter.displayName}`, data: `backend_confirm:${target}`, style: "primary" },
21199
- { label: `Switch + Clear History`, data: `backend_confirm_clear:${target}`, style: "primary" }
21200
- ],
21201
- [{ label: "Cancel", data: `backend_cancel:${target}` }]
21202
- ]
21844
+ channel,
21845
+ messageId,
21846
+ `${summary}
21847
+
21848
+ \u26A0\uFE0F No accounts configured. Add one via /accounts.`,
21849
+ [[{ label: "\u2190 Back", data: `bconf:panel:${backendId}` }]]
21203
21850
  );
21204
- } else {
21205
- await doBackendSwitch(chatId, target, channel);
21851
+ return;
21206
21852
  }
21853
+ const activeSlotId = isGemini ? getChatGeminiSlotId(chatId) : getChatBackendSlotId(chatId, backendId);
21854
+ const buttons = enabledSlots.map((s) => {
21855
+ const label2 = s.label ?? `slot-${s.id}`;
21856
+ const icon = (s.slotType ?? s.slot_type) === "oauth" ? "\u{1F464}" : "\u{1F511}";
21857
+ const active = activeSlotId === s.id;
21858
+ return [{
21859
+ label: `${active ? "\u2713 " : ""}${icon} ${label2}`,
21860
+ data: `bconf:setaccount:${backendId}:${s.id}`,
21861
+ ...active ? { style: "primary" } : {}
21862
+ }];
21863
+ });
21864
+ buttons.push([{ label: "\u2190 Auto (rotate)", data: `bconf:setaccount:${backendId}:auto` }]);
21865
+ buttons.push([{ label: "\u2190 Back", data: `bconf:panel:${backendId}` }]);
21866
+ await sendOrEditKeyboard(chatId, channel, messageId, `${summary}
21867
+
21868
+ Pick an account:`, buttons);
21207
21869
  }
21208
- async function doBackendSwitch(chatId, backendId, channel) {
21209
- const targetAdapter = getAdapter(backendId);
21210
- const pairCount = getMessagePairCount(chatId);
21211
- if (pairCount >= 2) {
21212
- await channel.sendText(chatId, `\u23F3 Saving context...`, { parseMode: "plain" });
21213
- }
21214
- const summarized = await summarizeWithFallbackChain(chatId, backendId);
21215
- const bridge = buildContextBridge(chatId);
21216
- if (bridge) {
21217
- setPendingContextBridge(chatId, bridge);
21870
+ async function sendBackendSwitchConfirmation(chatId, target, channel, messageId) {
21871
+ const current = getBackend(chatId);
21872
+ const targetAdapter = getAdapter(target);
21873
+ if (current === target) {
21874
+ await sendBackendConfigPanel(chatId, target, channel, messageId, false);
21875
+ return;
21218
21876
  }
21219
- if (summarized) {
21220
- await channel.sendText(chatId, "\u{1F4BE} Context saved \u2014 session summarized to memory.", { parseMode: "plain" });
21221
- } else if (bridge) {
21222
- await channel.sendText(chatId, "\u{1F4AC} Context preserved.", { parseMode: "plain" });
21877
+ await doBackendSwitch(chatId, target, channel, { messageId });
21878
+ }
21879
+ async function doBackendSwitch(chatId, backendId, channel, opts) {
21880
+ const targetAdapter = getAdapter(backendId);
21881
+ if (!opts?.skipContext) {
21882
+ const pairCount = getMessagePairCount(chatId);
21883
+ if (pairCount >= 2) {
21884
+ if (opts?.messageId && typeof channel.editKeyboard === "function") {
21885
+ await channel.editKeyboard(chatId, opts.messageId, `\u23F3 Saving context before switching to ${targetAdapter.displayName}...`, []);
21886
+ } else {
21887
+ await channel.sendText(chatId, `\u23F3 Saving context...`, { parseMode: "plain" });
21888
+ }
21889
+ }
21890
+ const summarized = await summarizeWithFallbackChain(chatId, backendId);
21891
+ const bridge = buildContextBridge(chatId);
21892
+ if (bridge) setPendingContextBridge(chatId, bridge);
21223
21893
  }
21224
21894
  clearSession(chatId);
21225
21895
  clearModel(chatId);
21226
21896
  clearThinkingLevel(chatId);
21227
- if (backendId !== "gemini") {
21228
- clearChatGeminiSlot(chatId);
21229
- }
21897
+ if (backendId !== "gemini") clearChatGeminiSlot(chatId);
21230
21898
  setBackend(chatId, backendId);
21231
- logActivity(getDb(), { chatId, source: "telegram", eventType: "config_changed", summary: `Backend switched to ${targetAdapter.displayName}`, detail: { field: "backend", value: backendId } });
21232
- if (typeof channel.sendKeyboard === "function") {
21233
- const models = targetAdapter.availableModels;
21234
- const modelButtons = Object.entries(models).map(([id, info]) => {
21235
- const tag = info.thinking === "adjustable" ? " \u26A1" : "";
21236
- return { label: `${info.label}${tag}`, data: `model:${id}` };
21237
- });
21238
- const rows = modelButtons.map((b) => [b]);
21239
- rows.push([{ label: "\u2705 Use Default", data: `backend_dismiss:${backendId}` }]);
21240
- await channel.sendKeyboard(
21241
- chatId,
21242
- `\u2705 Switched to ${targetAdapter.displayName}.
21243
-
21244
- Choose a model (optional):`,
21245
- rows
21246
- );
21247
- } else {
21248
- await channel.sendText(chatId, `\u2705 Switched to ${targetAdapter.displayName}. Ready!`, { parseMode: "plain" });
21249
- }
21899
+ logActivity(getDb(), {
21900
+ chatId,
21901
+ source: "telegram",
21902
+ eventType: "config_changed",
21903
+ summary: `Backend switched to ${targetAdapter.displayName}`,
21904
+ detail: { field: "backend", value: backendId }
21905
+ });
21906
+ await sendBackendConfigPanel(chatId, backendId, channel, opts?.messageId, true);
21250
21907
  }
21251
21908
  var ROTATION_MODE_LABELS, ROTATION_MODE_CONFIRM_LABELS;
21252
21909
  var init_ui = __esm({
@@ -21255,6 +21912,7 @@ var init_ui = __esm({
21255
21912
  init_format();
21256
21913
  init_backends();
21257
21914
  init_store5();
21915
+ init_chat_settings();
21258
21916
  init_summarize();
21259
21917
  init_inject();
21260
21918
  init_session_log();
@@ -21299,237 +21957,285 @@ var init_resolve = __esm({
21299
21957
 
21300
21958
  // src/router/evolve.ts
21301
21959
  import { homedir as homedir6 } from "os";
21302
- async function handleEvolveCommand(chatId, channel) {
21303
- const { getReflectionStatus: getRefStatus, getUnprocessedSignalCount: getUnprocessedSignalCount2, getPendingInsightCount: getPendingInsightCount2, getLastAnalysisTime: getLastAnalysisTime2 } = await Promise.resolve().then(() => (init_store4(), store_exports4));
21960
+ function relativeTime(isoUtc) {
21961
+ const diffMs = Date.now() - (/* @__PURE__ */ new Date(isoUtc + "Z")).getTime();
21962
+ const h = Math.floor(diffMs / 36e5);
21963
+ if (h < 1) return "< 1 hour ago";
21964
+ if (h < 24) return `${h}h ago`;
21965
+ return `${Math.floor(h / 24)}d ago`;
21966
+ }
21967
+ async function getReflectionChatId(chatId) {
21304
21968
  const { getPrimaryChatId: getPrimaryChatId2 } = await Promise.resolve().then(() => (init_resolve(), resolve_exports));
21305
- const reflectionChatId = getPrimaryChatId2() || chatId;
21306
- const isActive = getRefStatus(getDb(), reflectionChatId) === "active";
21307
- if (!isActive) {
21308
- const text = [
21309
- "Self-Learning & Evolution",
21310
- "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501",
21311
- "",
21312
- "Teach your assistant to improve over time.",
21313
- "",
21314
- "When enabled, CC-Claw watches for corrections,",
21315
- "preferences, and frustration in your messages,",
21316
- "then proposes changes to its personality and",
21317
- "behavior files (SOUL.md, USER.md).",
21318
- "",
21319
- "You review and approve every change."
21320
- ].join("\n");
21321
- if (typeof channel.sendKeyboard === "function") {
21322
- const { buildEvolveOnboardingKeyboard: buildEvolveOnboardingKeyboard2 } = await Promise.resolve().then(() => (init_propose(), propose_exports));
21323
- await channel.sendKeyboard(chatId, text, buildEvolveOnboardingKeyboard2());
21324
- } else {
21325
- await channel.sendText(chatId, text + "\n\nUse /evolve enable to activate.", { parseMode: "plain" });
21326
- }
21327
- } else {
21328
- await sendEvolveDashboard(chatId, reflectionChatId, channel);
21329
- }
21969
+ return getPrimaryChatId2() || chatId;
21330
21970
  }
21331
- async function sendEvolveDashboard(chatId, reflectionChatId, channel) {
21971
+ async function buildDashboardView(chatId, reflChatId) {
21332
21972
  const { getUnprocessedSignalCount: getUnprocessedSignalCount2, getPendingInsightCount: getPendingInsightCount2, getLastAnalysisTime: getLastAnalysisTime2, getReflectionModelConfig: getReflectionModelConfig2 } = await Promise.resolve().then(() => (init_store4(), store_exports4));
21333
- const signals = getUnprocessedSignalCount2(getDb(), reflectionChatId);
21334
- const pending = getPendingInsightCount2(getDb(), reflectionChatId);
21335
- const lastTime = getLastAnalysisTime2(getDb(), reflectionChatId);
21336
- let lastText = "never";
21337
- if (lastTime) {
21338
- const diffMs = Date.now() - (/* @__PURE__ */ new Date(lastTime + "Z")).getTime();
21339
- const diffHours = Math.floor(diffMs / 36e5);
21340
- if (diffHours < 1) lastText = "< 1 hour ago";
21341
- else if (diffHours < 24) lastText = `${diffHours}h ago`;
21342
- else lastText = `${Math.floor(diffHours / 24)}d ago`;
21343
- }
21344
- const modelConfig = getReflectionModelConfig2(getDb(), reflectionChatId);
21345
- const modelLabel = modelConfig.mode === "pinned" && modelConfig.backend && modelConfig.model ? `Pinned: ${modelConfig.backend}/${modelConfig.model}` : modelConfig.mode === "cheap" ? "Cheap" : "Auto";
21346
- const dashText = [
21973
+ const signals = getUnprocessedSignalCount2(getDb(), reflChatId);
21974
+ const pending = getPendingInsightCount2(getDb(), reflChatId);
21975
+ const lastTime = getLastAnalysisTime2(getDb(), reflChatId);
21976
+ const lastText = lastTime ? relativeTime(lastTime) : "never";
21977
+ const modelConfig = getReflectionModelConfig2(getDb(), reflChatId);
21978
+ const modelLabel = modelConfig.mode === "pinned" && modelConfig.backend && modelConfig.model ? `${modelConfig.backend}/${modelConfig.model}` : modelConfig.mode === "cheap" ? "Cheap" : "Auto";
21979
+ const text = [
21347
21980
  "Self-Learning & Evolution",
21348
21981
  "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501",
21349
21982
  "",
21350
- `\u2705 Active`,
21983
+ "\u2705 Active",
21351
21984
  `Signals: ${signals} pending \xB7 Proposals: ${pending}`,
21352
21985
  `Last analysis: ${lastText}`,
21353
21986
  `Model: ${modelLabel}`
21354
21987
  ].join("\n");
21355
- if (typeof channel.sendKeyboard === "function") {
21356
- const { buildEvolveMenuKeyboard: buildEvolveMenuKeyboard2 } = await Promise.resolve().then(() => (init_propose(), propose_exports));
21357
- await channel.sendKeyboard(chatId, dashText, buildEvolveMenuKeyboard2({ pendingProposals: pending, unprocessedSignals: signals }));
21988
+ const reviewLabel = pending > 0 ? `Review (${pending})` : "Review";
21989
+ const buttons = [
21990
+ [
21991
+ { label: "\u{1F50D} Analyze Now", data: "evolve:analyze", style: "success" },
21992
+ { label: reviewLabel, data: "evolve:review", ...pending > 0 ? { style: "primary" } : {} }
21993
+ ],
21994
+ [
21995
+ { label: "Stats", data: "evolve:stats" },
21996
+ { label: "History", data: "evolve:history" }
21997
+ ],
21998
+ [
21999
+ { label: "Model", data: "evolve:model" },
22000
+ { label: "Undo", data: "evolve:undo" },
22001
+ { label: "\u23F8 Disable", data: "evolve:toggle", style: "danger" }
22002
+ ]
22003
+ ];
22004
+ return { text, buttons };
22005
+ }
22006
+ async function sendEvolveDashboard(chatId, reflChatId, channel, messageId) {
22007
+ const { text, buttons } = await buildDashboardView(chatId, reflChatId);
22008
+ await sendOrEditKeyboard(chatId, channel, messageId, text, buttons);
22009
+ }
22010
+ function buildOnboardingView() {
22011
+ const text = [
22012
+ "Self-Learning & Evolution",
22013
+ "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501",
22014
+ "",
22015
+ "Teach your assistant to improve over time.",
22016
+ "",
22017
+ "CC-Claw watches for corrections, preferences,",
22018
+ "and frustration in your messages, then proposes",
22019
+ "changes to its personality files (SOUL.md, USER.md).",
22020
+ "",
22021
+ "You review and approve every change."
22022
+ ].join("\n");
22023
+ const buttons = [
22024
+ [{ label: "\u2705 Enable Self-Learning", data: "evolve:toggle", style: "success" }],
22025
+ [{ label: "One-Time Analysis", data: "evolve:analyze" }]
22026
+ ];
22027
+ return { text, buttons };
22028
+ }
22029
+ async function sendCurrentProposalInPlace(chatId, channel, messageId) {
22030
+ const { getReviewSession: getReviewSession2, getInsightById: getInsightById2, deleteReviewSession: deleteReviewSession2, advanceReviewSession: advanceReviewSession2 } = await Promise.resolve().then(() => (init_store4(), store_exports4));
22031
+ const { formatProposalCardWithProgress: formatProposalCardWithProgress2, buildProposalKeyboard: buildProposalKeyboard2, buildReviewCompleteMessage: buildReviewCompleteMessage3 } = await Promise.resolve().then(() => (init_propose(), propose_exports));
22032
+ const reflChatId = await getReflectionChatId(chatId);
22033
+ const session2 = getReviewSession2(getDb(), chatId);
22034
+ if (!session2) {
22035
+ await sendEvolveDashboard(chatId, reflChatId, channel, messageId);
22036
+ return;
22037
+ }
22038
+ if (session2.currentIndex >= session2.insightIds.length) {
22039
+ const summary = buildReviewCompleteMessage3(session2.results);
22040
+ deleteReviewSession2(getDb(), chatId);
22041
+ const buttons = [[{ label: "\u2190 Back to Dashboard", data: "evolve:menu" }]];
22042
+ await sendOrEditKeyboard(chatId, channel, messageId, summary, buttons);
22043
+ return;
22044
+ }
22045
+ const insightId = session2.insightIds[session2.currentIndex];
22046
+ const insight = getInsightById2(getDb(), insightId);
22047
+ if (!insight || insight.status !== "pending") {
22048
+ advanceReviewSession2(getDb(), chatId, insightId, "skipped");
22049
+ return sendCurrentProposalInPlace(chatId, channel, messageId);
22050
+ }
22051
+ const card = formatProposalCardWithProgress2(insight, session2.currentIndex, session2.insightIds.length);
22052
+ const kb = [
22053
+ ...buildProposalKeyboard2(insight.id, insight.category),
22054
+ [{ label: "\u2190 Dashboard", data: "evolve:menu" }]
22055
+ ];
22056
+ await sendOrEditKeyboard(chatId, channel, messageId, card, kb);
22057
+ }
22058
+ async function handleEvolveCommand(chatId, channel) {
22059
+ const { getReflectionStatus: getReflectionStatus2 } = await Promise.resolve().then(() => (init_store4(), store_exports4));
22060
+ const reflChatId = await getReflectionChatId(chatId);
22061
+ const isActive = getReflectionStatus2(getDb(), reflChatId) === "active";
22062
+ if (typeof channel.sendKeyboard !== "function") {
22063
+ await channel.sendText(chatId, isActive ? "Use /evolve buttons to manage self-learning." : "Self-learning disabled. Use /evolve enable.", { parseMode: "plain" });
22064
+ return;
22065
+ }
22066
+ if (!isActive) {
22067
+ const { text, buttons } = buildOnboardingView();
22068
+ await channel.sendKeyboard(chatId, text, buttons);
21358
22069
  } else {
21359
- await channel.sendText(chatId, dashText, { parseMode: "plain" });
22070
+ await sendEvolveDashboard(chatId, reflChatId, channel);
21360
22071
  }
21361
22072
  }
21362
- async function handleEvolveCallback(chatId, data, channel) {
22073
+ async function handleEvolveCallback(chatId, data, channel, messageId) {
21363
22074
  const parts = data.split(":");
21364
22075
  const action = parts[1];
21365
22076
  const idStr = parts[2];
21366
22077
  if (action !== "discuss") {
21367
- const { clearDiscussionMode: clearStale } = await Promise.resolve().then(() => (init_store4(), store_exports4));
21368
- clearStale(chatId);
22078
+ const { clearDiscussionMode: clearDiscussionMode2 } = await Promise.resolve().then(() => (init_store4(), store_exports4));
22079
+ clearDiscussionMode2(chatId);
21369
22080
  }
22081
+ const reflChatId = await getReflectionChatId(chatId);
21370
22082
  switch (action) {
21371
22083
  case "menu": {
21372
- const { getReflectionStatus: getRefStatus } = await Promise.resolve().then(() => (init_store4(), store_exports4));
21373
- const { getPrimaryChatId: getPrimaryChatId2 } = await Promise.resolve().then(() => (init_resolve(), resolve_exports));
21374
- const reflChatId = getPrimaryChatId2() || chatId;
21375
- const menuActive = getRefStatus(getDb(), reflChatId) === "active";
21376
- if (!menuActive) {
21377
- const { buildEvolveOnboardingKeyboard: buildEvolveOnboardingKeyboard2 } = await Promise.resolve().then(() => (init_propose(), propose_exports));
21378
- const text = [
21379
- "Self-Learning & Evolution",
21380
- "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501",
21381
- "",
21382
- "Teach your assistant to improve over time.",
21383
- "",
21384
- "When enabled, CC-Claw watches for corrections,",
21385
- "preferences, and frustration in your messages,",
21386
- "then proposes changes to its personality and",
21387
- "behavior files (SOUL.md, USER.md).",
21388
- "",
21389
- "You review and approve every change."
21390
- ].join("\n");
21391
- await channel.sendKeyboard(chatId, text, buildEvolveOnboardingKeyboard2());
22084
+ const { getReflectionStatus: getReflectionStatus2 } = await Promise.resolve().then(() => (init_store4(), store_exports4));
22085
+ const isActive = getReflectionStatus2(getDb(), reflChatId) === "active";
22086
+ if (!isActive) {
22087
+ const { text, buttons } = buildOnboardingView();
22088
+ await sendOrEditKeyboard(chatId, channel, messageId, text, buttons);
21392
22089
  } else {
21393
- await sendEvolveDashboard(chatId, reflChatId, channel);
22090
+ await sendEvolveDashboard(chatId, reflChatId, channel, messageId);
21394
22091
  }
21395
22092
  break;
21396
22093
  }
21397
22094
  case "analyze": {
21398
22095
  const { runAnalysis: runAnalysis2 } = await Promise.resolve().then(() => (init_analyze(), analyze_exports));
21399
- const { formatNightlySummary: formatNightlySummary2 } = await Promise.resolve().then(() => (init_propose(), propose_exports));
21400
- await channel.sendText(chatId, "Analyzing recent interactions...", { parseMode: "plain" });
22096
+ await sendOrEditKeyboard(
22097
+ chatId,
22098
+ channel,
22099
+ messageId,
22100
+ "\u{1F50D} Analyzing recent interactions...\n\nThis may take a moment.",
22101
+ []
22102
+ );
21401
22103
  try {
21402
22104
  const insights = await runAnalysis2(chatId, { force: true });
21403
22105
  if (insights.length === 0) {
21404
- await channel.sendText(chatId, "No new insights from recent interactions.", { parseMode: "plain" });
22106
+ await sendOrEditKeyboard(
22107
+ chatId,
22108
+ channel,
22109
+ messageId,
22110
+ "\u2705 Analysis complete.\n\nNo new insights from recent interactions.",
22111
+ [[{ label: "\u2190 Back to Dashboard", data: "evolve:menu" }]]
22112
+ );
21405
22113
  } else {
21406
- const nightlyItems = insights.map((ins, i) => ({ id: i + 1, ...ins }));
21407
- await channel.sendText(chatId, formatNightlySummary2(nightlyItems), { parseMode: "plain" });
22114
+ await sendOrEditKeyboard(
22115
+ chatId,
22116
+ channel,
22117
+ messageId,
22118
+ `\u2705 Analysis complete \u2014 ${insights.length} new proposal(s) ready.`,
22119
+ [
22120
+ [{ label: `Review (${insights.length})`, data: "evolve:review", style: "primary" }],
22121
+ [{ label: "\u2190 Back to Dashboard", data: "evolve:menu" }]
22122
+ ]
22123
+ );
21408
22124
  }
21409
22125
  } catch (e) {
21410
- await channel.sendText(chatId, `Analysis failed: ${e}`, { parseMode: "plain" });
22126
+ await sendOrEditKeyboard(
22127
+ chatId,
22128
+ channel,
22129
+ messageId,
22130
+ `\u274C Analysis failed: ${e}`,
22131
+ [[{ label: "\u2190 Back to Dashboard", data: "evolve:menu" }]]
22132
+ );
21411
22133
  }
21412
22134
  break;
21413
22135
  }
21414
22136
  case "review": {
21415
22137
  const { getPendingInsights: getPendingInsights2, createReviewSession: createReviewSession2 } = await Promise.resolve().then(() => (init_store4(), store_exports4));
21416
- const { getPrimaryChatId: getPrimary } = await Promise.resolve().then(() => (init_resolve(), resolve_exports));
21417
- const reflChatId = getPrimary() || chatId;
21418
22138
  const pending = getPendingInsights2(getDb(), reflChatId);
21419
22139
  if (pending.length === 0) {
21420
- await channel.sendText(chatId, "No pending proposals.", { parseMode: "plain" });
21421
- } else {
21422
- const insightIds = pending.map((p) => p.id);
21423
- createReviewSession2(getDb(), chatId, insightIds);
21424
- await channel.sendText(chatId, `${pending.length} proposal(s) ready. Let's review them one by one.`, { parseMode: "plain" });
21425
- await sendCurrentProposal(chatId, channel);
21426
- }
21427
- break;
21428
- }
21429
- case "preview": {
21430
- const { getInsightById: pvIns } = await Promise.resolve().then(() => (init_store4(), store_exports4));
21431
- const pvInsight = pvIns(getDb(), parseInt(idStr, 10));
21432
- if (!pvInsight) {
21433
- await channel.sendText(chatId, "Proposal not found.", { parseMode: "plain" });
21434
- break;
21435
- }
21436
- if (!pvInsight.targetFile || !pvInsight.proposedDiff) {
21437
- await channel.sendText(chatId, "No diff available for this proposal.", { parseMode: "plain" });
21438
- break;
21439
- }
21440
- const { readFileSync: pvRead, existsSync: pvExists } = await import("fs");
21441
- const { join: pvJoin } = await import("path");
21442
- const pvTargetPath = pvJoin(homedir6(), ".cc-claw", pvInsight.targetFile);
21443
- let previewLines;
21444
- if (pvExists(pvTargetPath)) {
21445
- const currentContent = pvRead(pvTargetPath, "utf-8");
21446
- const diffLines = pvInsight.proposedDiff.split("\n");
21447
- const additions = diffLines.filter((l) => l.startsWith("+")).map((l) => l.slice(1).trim());
21448
- const removals = diffLines.filter((l) => l.startsWith("-")).map((l) => l.slice(1).trim());
21449
- previewLines = [
21450
- `\u{1F4CB} Dry-run preview: ${pvInsight.targetFile}`,
21451
- `${"\u2500".repeat(40)}`,
21452
- ""
21453
- ];
21454
- if (removals.length > 0) {
21455
- previewLines.push("Lines to REMOVE:");
21456
- for (const r of removals) {
21457
- previewLines.push(` - ${r}`);
21458
- const idx = currentContent.indexOf(r);
21459
- if (idx >= 0) {
21460
- const lineNum = currentContent.slice(0, idx).split("\n").length;
21461
- previewLines.push(` (line ~${lineNum})`);
21462
- }
21463
- }
21464
- previewLines.push("");
21465
- }
21466
- if (additions.length > 0) {
21467
- previewLines.push("Lines to ADD:");
21468
- for (const a of additions) {
21469
- previewLines.push(` + ${a}`);
21470
- }
21471
- previewLines.push("");
21472
- }
21473
- previewLines.push(`File size: ${currentContent.length} chars`);
22140
+ await sendOrEditKeyboard(
22141
+ chatId,
22142
+ channel,
22143
+ messageId,
22144
+ "No pending proposals.",
22145
+ [[{ label: "\u2190 Back to Dashboard", data: "evolve:menu" }]]
22146
+ );
21474
22147
  } else {
21475
- previewLines = [
21476
- `\u{1F4CB} Dry-run preview: ${pvInsight.targetFile}`,
21477
- `${"\u2500".repeat(40)}`,
21478
- "",
21479
- "(File does not exist yet \u2014 will be created)",
21480
- "",
21481
- "Content to add:",
21482
- pvInsight.proposedDiff
21483
- ];
21484
- }
21485
- await channel.sendText(chatId, previewLines.join("\n"), { parseMode: "plain" });
21486
- break;
21487
- }
21488
- case "diff": {
21489
- const { getInsightById: getInsightById2 } = await Promise.resolve().then(() => (init_store4(), store_exports4));
21490
- const { formatDiffCodeBlock: formatDiffCodeBlock2 } = await Promise.resolve().then(() => (init_propose(), propose_exports));
21491
- const insight = getInsightById2(getDb(), parseInt(idStr, 10));
21492
- if (insight?.proposedDiff) {
21493
- await channel.sendText(chatId, formatDiffCodeBlock2(insight.proposedDiff), { parseMode: "plain" });
22148
+ createReviewSession2(getDb(), chatId, pending.map((p) => p.id));
22149
+ await sendCurrentProposalInPlace(chatId, channel, messageId);
21494
22150
  }
21495
22151
  break;
21496
22152
  }
21497
22153
  case "apply": {
21498
22154
  const { applyInsight: applyInsight2 } = await Promise.resolve().then(() => (init_apply(), apply_exports));
22155
+ const { advanceReviewSession: advanceReviewSession2 } = await Promise.resolve().then(() => (init_store4(), store_exports4));
21499
22156
  const result = await applyInsight2(parseInt(idStr, 10));
21500
- await channel.sendText(chatId, result.message, { parseMode: "plain" });
21501
- const { advanceReviewSession: arAdvance } = await Promise.resolve().then(() => (init_store4(), store_exports4));
21502
- arAdvance(getDb(), chatId, parseInt(idStr, 10), "applied");
21503
- await sendCurrentProposal(chatId, channel);
22157
+ advanceReviewSession2(getDb(), chatId, parseInt(idStr, 10), "applied");
22158
+ await sendOrEditKeyboard(
22159
+ chatId,
22160
+ channel,
22161
+ messageId,
22162
+ `\u2705 ${result.message}`,
22163
+ []
22164
+ );
22165
+ await new Promise((r) => setTimeout(r, 800));
22166
+ await sendCurrentProposalInPlace(chatId, channel, messageId);
21504
22167
  break;
21505
22168
  }
21506
22169
  case "skip": {
21507
- await channel.sendText(chatId, "Skipped \u2014 will show again next review.", { parseMode: "plain" });
21508
- const { advanceReviewSession: skAdvance } = await Promise.resolve().then(() => (init_store4(), store_exports4));
21509
- skAdvance(getDb(), chatId, parseInt(idStr, 10), "skipped");
21510
- await sendCurrentProposal(chatId, channel);
22170
+ const { advanceReviewSession: advanceReviewSession2 } = await Promise.resolve().then(() => (init_store4(), store_exports4));
22171
+ advanceReviewSession2(getDb(), chatId, parseInt(idStr, 10), "skipped");
22172
+ await sendCurrentProposalInPlace(chatId, channel, messageId);
22173
+ break;
22174
+ }
22175
+ case "reject": {
22176
+ const { updateInsightStatus: updateInsightStatus2, advanceReviewSession: advanceReviewSession2 } = await Promise.resolve().then(() => (init_store4(), store_exports4));
22177
+ updateInsightStatus2(getDb(), parseInt(idStr, 10), "rejected");
22178
+ advanceReviewSession2(getDb(), chatId, parseInt(idStr, 10), "rejected");
22179
+ await sendCurrentProposalInPlace(chatId, channel, messageId);
21511
22180
  break;
21512
22181
  }
21513
22182
  case "discuss": {
21514
- const insId = parseInt(idStr, 10);
21515
22183
  const { setDiscussionMode: setDiscussionMode2 } = await Promise.resolve().then(() => (init_store4(), store_exports4));
21516
- setDiscussionMode2(chatId, insId);
21517
- await channel.sendText(chatId, [
21518
- "\u{1F4AC} Discussion mode \u2014 tell me what you'd like to change about this proposal.",
21519
- "",
21520
- 'Examples: "Move this to a skill instead", "Rewrite the diff", "Why this file?", "Make it shorter"',
21521
- "",
21522
- "Type your reply and I'll handle it:"
21523
- ].join("\n"), { parseMode: "plain" });
22184
+ setDiscussionMode2(chatId, parseInt(idStr, 10));
22185
+ await sendOrEditKeyboard(
22186
+ chatId,
22187
+ channel,
22188
+ messageId,
22189
+ [
22190
+ "\u{1F4AC} Discussion mode",
22191
+ "",
22192
+ "Tell me what you'd like to change about this proposal.",
22193
+ "",
22194
+ 'Examples: "Move this to a skill instead", "Make it shorter", "Why this file?"',
22195
+ "",
22196
+ "Type your reply below \u2014 I'll revise and come back with a new proposal."
22197
+ ].join("\n"),
22198
+ [[{ label: "\u2190 Cancel discussion", data: `evolve:review` }]]
22199
+ );
21524
22200
  break;
21525
22201
  }
21526
- case "reject": {
21527
- const { updateInsightStatus: updateInsightStatus2 } = await Promise.resolve().then(() => (init_store4(), store_exports4));
21528
- updateInsightStatus2(getDb(), parseInt(idStr, 10), "rejected");
21529
- await channel.sendText(chatId, "Rejected. Won't propose similar changes.", { parseMode: "plain" });
21530
- const { advanceReviewSession: rjAdvance } = await Promise.resolve().then(() => (init_store4(), store_exports4));
21531
- rjAdvance(getDb(), chatId, parseInt(idStr, 10), "rejected");
21532
- await sendCurrentProposal(chatId, channel);
22202
+ case "preview": {
22203
+ const { getInsightById: getInsightById2 } = await Promise.resolve().then(() => (init_store4(), store_exports4));
22204
+ const insight = getInsightById2(getDb(), parseInt(idStr, 10));
22205
+ if (!insight?.targetFile || !insight?.proposedDiff) {
22206
+ await sendOrEditKeyboard(
22207
+ chatId,
22208
+ channel,
22209
+ messageId,
22210
+ "No diff available for this proposal.",
22211
+ [[{ label: "\u2190 Back", data: `evolve:review` }]]
22212
+ );
22213
+ break;
22214
+ }
22215
+ const { readFileSync: readFileSync33, existsSync: existsSync62 } = await import("fs");
22216
+ const { join: join41 } = await import("path");
22217
+ const targetPath = join41(homedir6(), ".cc-claw", insight.targetFile);
22218
+ let previewText;
22219
+ if (existsSync62(targetPath)) {
22220
+ const current = readFileSync33(targetPath, "utf-8");
22221
+ const diffLines = insight.proposedDiff.split("\n");
22222
+ const additions = diffLines.filter((l) => l.startsWith("+")).map((l) => ` + ${l.slice(1).trim()}`);
22223
+ const removals = diffLines.filter((l) => l.startsWith("-")).map((l) => ` - ${l.slice(1).trim()}`);
22224
+ const lines = [`\u{1F4CB} Preview: ${insight.targetFile}`, ""];
22225
+ if (removals.length) lines.push("Lines to REMOVE:", ...removals, "");
22226
+ if (additions.length) lines.push("Lines to ADD:", ...additions, "");
22227
+ lines.push(`File: ${current.length} chars`);
22228
+ previewText = lines.join("\n");
22229
+ } else {
22230
+ previewText = [`\u{1F4CB} Preview: ${insight.targetFile}`, "", "(File will be created)", "", insight.proposedDiff].join("\n");
22231
+ }
22232
+ await sendOrEditKeyboard(
22233
+ chatId,
22234
+ channel,
22235
+ messageId,
22236
+ previewText,
22237
+ [[{ label: "\u2190 Back to Proposal", data: `evolve:review` }]]
22238
+ );
21533
22239
  break;
21534
22240
  }
21535
22241
  case "stats": {
@@ -21538,46 +22244,49 @@ async function handleEvolveCallback(chatId, data, channel) {
21538
22244
  const { calculateDrift: calculateDrift2 } = await Promise.resolve().then(() => (init_apply(), apply_exports));
21539
22245
  const reportData = buildGrowthReportData2(getDb(), chatId, 30);
21540
22246
  const modelData = buildModelPerformanceData2(getDb(), chatId, 30);
21541
- const metricsForReport = {
22247
+ let report = formatGrowthReport2({
21542
22248
  correctionsBefore: reportData.avgCorrectionsFirstHalf,
21543
22249
  correctionsAfter: reportData.avgCorrectionsSecondHalf,
21544
22250
  praiseRatio: reportData.praiseRatio,
21545
22251
  insightsApplied: reportData.totalInsightsApplied,
21546
22252
  pendingCount: reportData.pendingCount,
21547
- topInsight: reportData.topInsightId != null ? {
21548
- insight: `#${reportData.topInsightId}`,
21549
- effectiveness: reportData.topInsightEffectiveness ?? 0
21550
- } : null
21551
- };
21552
- let report = formatGrowthReport2(metricsForReport, modelData);
22253
+ topInsight: reportData.topInsightId != null ? { insight: `#${reportData.topInsightId}`, effectiveness: reportData.topInsightEffectiveness ?? 0 } : null
22254
+ }, modelData);
21553
22255
  const drift = calculateDrift2(chatId);
21554
22256
  if (drift && (drift.soulDrift > 0.5 || drift.userDrift > 0.5)) {
21555
- report += "\n\nSOUL.md has changed significantly since reflection started.\nTap History in /evolve to review all applied changes.";
22257
+ report += "\n\nSOUL.md has changed significantly since reflection started.";
21556
22258
  }
21557
- await channel.sendText(chatId, report, { parseMode: "plain" });
22259
+ await sendOrEditKeyboard(
22260
+ chatId,
22261
+ channel,
22262
+ messageId,
22263
+ report,
22264
+ [[{ label: "\u2190 Back to Dashboard", data: "evolve:menu" }]]
22265
+ );
21558
22266
  break;
21559
22267
  }
21560
22268
  case "history": {
21561
22269
  const { getAppliedInsights: getAppliedInsights2, getRejectedInsights: getRejectedInsights2 } = await Promise.resolve().then(() => (init_store4(), store_exports4));
21562
22270
  const applied = getAppliedInsights2(getDb(), chatId, 10);
21563
22271
  const rejected = getRejectedInsights2(getDb(), chatId, 10);
21564
- let msg = "Applied Insights:\n";
21565
- if (applied.length === 0) msg += "None yet.\n";
21566
- else applied.forEach((i) => {
21567
- msg += `#${i.id} [${i.category}] ${i.insight}
21568
- `;
21569
- });
21570
- msg += "\nRejected Insights:\n";
21571
- if (rejected.length === 0) msg += "None yet.\n";
21572
- else rejected.forEach((i) => {
21573
- msg += `#${i.id} [${i.category}] ${i.insight}
21574
- `;
21575
- });
21576
- await channel.sendText(chatId, msg, { parseMode: "plain" });
22272
+ const lines = ["Applied Insights:", ""];
22273
+ if (applied.length === 0) lines.push("None yet.");
22274
+ else applied.forEach((i) => lines.push(`#${i.id} [${i.category}] ${i.insight}`));
22275
+ lines.push("", "Rejected Insights:", "");
22276
+ if (rejected.length === 0) lines.push("None yet.");
22277
+ else rejected.forEach((i) => lines.push(`#${i.id} [${i.category}] ${i.insight}`));
22278
+ await sendOrEditKeyboard(
22279
+ chatId,
22280
+ channel,
22281
+ messageId,
22282
+ lines.join("\n"),
22283
+ [[{ label: "\u2190 Back to Dashboard", data: "evolve:menu" }]]
22284
+ );
21577
22285
  break;
21578
22286
  }
21579
22287
  case "toggle": {
21580
22288
  const { getReflectionStatus: getReflectionStatus2, setReflectionStatus: setReflectionStatus2 } = await Promise.resolve().then(() => (init_store4(), store_exports4));
22289
+ const { logActivity: logActivity2 } = await Promise.resolve().then(() => (init_store3(), store_exports3));
21581
22290
  const current = getReflectionStatus2(getDb(), chatId);
21582
22291
  if (current === "frozen") {
21583
22292
  const { readFileSync: readFileSync33, existsSync: existsSync62 } = await import("fs");
@@ -21588,14 +22297,13 @@ async function handleEvolveCallback(chatId, data, channel) {
21588
22297
  const soul = existsSync62(soulPath) ? readFileSync33(soulPath, "utf-8") : "";
21589
22298
  const user = existsSync62(userPath) ? readFileSync33(userPath, "utf-8") : "";
21590
22299
  setReflectionStatus2(getDb(), chatId, "active", soul, user);
21591
- const { logActivity: logActivity2 } = await Promise.resolve().then(() => (init_store3(), store_exports3));
21592
22300
  logActivity2(getDb(), { chatId, source: "telegram", eventType: "reflection_unfrozen", summary: "Reflection enabled" });
21593
- await channel.sendText(chatId, "\u2705 Self-learning enabled. Signal detection is now active.\nCreate a nightly cron job with /schedule to enable automatic analysis.", { parseMode: "plain" });
22301
+ await sendEvolveDashboard(chatId, reflChatId, channel, messageId);
21594
22302
  } else {
21595
22303
  setReflectionStatus2(getDb(), chatId, "frozen", void 0, void 0);
21596
- const { logActivity: logActivity2 } = await Promise.resolve().then(() => (init_store3(), store_exports3));
21597
22304
  logActivity2(getDb(), { chatId, source: "telegram", eventType: "reflection_frozen", summary: "Reflection disabled" });
21598
- await channel.sendText(chatId, "\u26D4 Self-learning disabled. No signals will be collected.", { parseMode: "plain" });
22305
+ const { text, buttons } = buildOnboardingView();
22306
+ await sendOrEditKeyboard(chatId, channel, messageId, text, buttons);
21599
22307
  }
21600
22308
  break;
21601
22309
  }
@@ -21603,62 +22311,123 @@ async function handleEvolveCallback(chatId, data, channel) {
21603
22311
  if (idStr) {
21604
22312
  const { rollbackInsight: rollbackInsight2 } = await Promise.resolve().then(() => (init_apply(), apply_exports));
21605
22313
  const result = await rollbackInsight2(parseInt(idStr, 10));
21606
- await channel.sendText(chatId, result.message, { parseMode: "plain" });
22314
+ await sendOrEditKeyboard(
22315
+ chatId,
22316
+ channel,
22317
+ messageId,
22318
+ result.message,
22319
+ [[{ label: "\u2190 Back to Dashboard", data: "evolve:menu" }]]
22320
+ );
21607
22321
  } else {
21608
22322
  const { getAppliedInsights: getAppliedInsights2 } = await Promise.resolve().then(() => (init_store4(), store_exports4));
21609
- const { buildUndoKeyboard: buildUndoKeyboard2 } = await Promise.resolve().then(() => (init_propose(), propose_exports));
21610
22323
  const applied = getAppliedInsights2(getDb(), chatId, 10);
21611
22324
  if (applied.length === 0) {
21612
- await channel.sendText(chatId, "No applied insights to undo.", { parseMode: "plain" });
22325
+ await sendOrEditKeyboard(
22326
+ chatId,
22327
+ channel,
22328
+ messageId,
22329
+ "No applied insights to undo.",
22330
+ [[{ label: "\u2190 Back to Dashboard", data: "evolve:menu" }]]
22331
+ );
21613
22332
  } else {
21614
- const kb = buildUndoKeyboard2(applied);
21615
- await channel.sendKeyboard(chatId, "Select an insight to undo:", kb);
22333
+ const buttons = [
22334
+ ...applied.map((i) => [{ label: `Undo #${i.id}: ${i.insight}`, data: `evolve:undo:${i.id}`, style: "danger" }]),
22335
+ [{ label: "\u2190 Back to Dashboard", data: "evolve:menu" }]
22336
+ ];
22337
+ await sendOrEditKeyboard(chatId, channel, messageId, "Select an insight to undo:", buttons);
21616
22338
  }
21617
22339
  }
21618
22340
  break;
21619
22341
  }
21620
22342
  case "model": {
22343
+ const { getReflectionModelConfig: getReflectionModelConfig2, setReflectionModelConfig: setReflectionModelConfig2 } = await Promise.resolve().then(() => (init_store4(), store_exports4));
21621
22344
  if (idStr) {
21622
- const { setReflectionModelConfig: setReflectionModelConfig2 } = await Promise.resolve().then(() => (init_store4(), store_exports4));
21623
22345
  setReflectionModelConfig2(getDb(), chatId, idStr);
21624
- await channel.sendText(chatId, `Analysis model set to: ${idStr}`, { parseMode: "plain" });
22346
+ await sendEvolveDashboard(chatId, reflChatId, channel, messageId);
21625
22347
  } else {
21626
- const { getReflectionModelConfig: getReflectionModelConfig2 } = await Promise.resolve().then(() => (init_store4(), store_exports4));
21627
- const { buildModelKeyboard: buildModelKeyboard2 } = await Promise.resolve().then(() => (init_propose(), propose_exports));
21628
22348
  const config2 = getReflectionModelConfig2(getDb(), chatId);
21629
- const kb = buildModelKeyboard2(config2.mode);
21630
- await channel.sendKeyboard(chatId, `Current analysis model: ${config2.mode}`, kb);
22349
+ const modes = [
22350
+ { label: "Auto", mode: "auto" },
22351
+ { label: "Cheap", mode: "cheap" },
22352
+ { label: "Pinned", mode: "pinned" }
22353
+ ];
22354
+ const buttons = [
22355
+ modes.map((m) => ({
22356
+ label: `${m.mode === config2.mode ? "\u2713 " : ""}${m.label}`,
22357
+ data: `evolve:model:${m.mode}`,
22358
+ ...m.mode === config2.mode ? { style: "primary" } : {}
22359
+ })),
22360
+ [{ label: "\u2190 Back to Dashboard", data: "evolve:menu" }]
22361
+ ];
22362
+ await sendOrEditKeyboard(
22363
+ chatId,
22364
+ channel,
22365
+ messageId,
22366
+ `Analysis model: ${config2.mode}`,
22367
+ buttons
22368
+ );
21631
22369
  }
21632
22370
  break;
21633
22371
  }
21634
22372
  }
21635
22373
  }
21636
- async function handleReflectCallback(chatId, data, channel) {
22374
+ async function handleReflectCallback(chatId, data, channel, messageId) {
21637
22375
  const action = data.slice(8);
21638
22376
  if (action === "go") {
21639
22377
  const { runAnalysis: runAnalysis2 } = await Promise.resolve().then(() => (init_analyze(), analyze_exports));
21640
- const { formatNightlySummary: formatNightlySummary2 } = await Promise.resolve().then(() => (init_propose(), propose_exports));
21641
- await channel.sendText(chatId, "Analyzing recent interactions...", { parseMode: "plain" });
22378
+ await sendOrEditKeyboard(
22379
+ chatId,
22380
+ channel,
22381
+ messageId,
22382
+ "\u{1F50D} Analyzing recent interactions...\n\nThis may take a moment.",
22383
+ []
22384
+ );
21642
22385
  try {
21643
22386
  const insights = await runAnalysis2(chatId, { force: true });
21644
22387
  if (insights.length === 0) {
21645
- await channel.sendText(chatId, "No new insights from recent interactions.", { parseMode: "plain" });
22388
+ await sendOrEditKeyboard(
22389
+ chatId,
22390
+ channel,
22391
+ messageId,
22392
+ "\u2705 Analysis complete.\n\nNo new insights from recent interactions.",
22393
+ [[{ label: "Review Dashboard", data: "evolve:menu" }]]
22394
+ );
21646
22395
  } else {
21647
- const items = insights.map((ins, i) => ({ id: i + 1, category: ins.category, insight: ins.insight }));
21648
- await channel.sendText(chatId, formatNightlySummary2(items) + "\n\nUse /evolve to review and apply proposals.", { parseMode: "plain" });
22396
+ await sendOrEditKeyboard(
22397
+ chatId,
22398
+ channel,
22399
+ messageId,
22400
+ `\u2705 Analysis complete \u2014 ${insights.length} new proposal(s) ready.`,
22401
+ [
22402
+ [{ label: `Review (${insights.length})`, data: "evolve:review", style: "primary" }],
22403
+ [{ label: "Dashboard", data: "evolve:menu" }]
22404
+ ]
22405
+ );
21649
22406
  }
21650
22407
  } catch (e) {
21651
- await channel.sendText(chatId, `Analysis failed: ${e}`, { parseMode: "plain" });
22408
+ await sendOrEditKeyboard(
22409
+ chatId,
22410
+ channel,
22411
+ messageId,
22412
+ `\u274C Analysis failed: ${e}`,
22413
+ [[{ label: "Dashboard", data: "evolve:menu" }]]
22414
+ );
21652
22415
  }
21653
22416
  } else if (action === "cancel") {
21654
- await channel.sendText(chatId, "Reflection cancelled.", { parseMode: "plain" });
22417
+ await sendOrEditKeyboard(
22418
+ chatId,
22419
+ channel,
22420
+ messageId,
22421
+ "Reflection cancelled.",
22422
+ [[{ label: "Dashboard", data: "evolve:menu" }]]
22423
+ );
21655
22424
  }
21656
22425
  }
21657
22426
  var init_evolve2 = __esm({
21658
22427
  "src/router/evolve.ts"() {
21659
22428
  "use strict";
21660
22429
  init_store5();
21661
- init_ui();
22430
+ init_helpers();
21662
22431
  }
21663
22432
  });
21664
22433
 
@@ -22304,8 +23073,8 @@ var init_analyze2 = __esm({
22304
23073
  });
22305
23074
 
22306
23075
  // src/optimizer/ui.ts
22307
- var ui_exports = {};
22308
- __export(ui_exports, {
23076
+ var ui_exports2 = {};
23077
+ __export(ui_exports2, {
22309
23078
  buildFindingKeyboard: () => buildFindingKeyboard,
22310
23079
  buildFindingMessage: () => buildFindingMessage,
22311
23080
  buildIdentityAuditKeyboard: () => buildIdentityAuditKeyboard,
@@ -22588,14 +23357,14 @@ __export(optimize_exports, {
22588
23357
  import { readFileSync as readFileSync17, writeFileSync as writeFileSync9, existsSync as existsSync28, readdirSync as readdirSync13, unlinkSync as unlinkSync7 } from "fs";
22589
23358
  import { join as join29, dirname as dirname4 } from "path";
22590
23359
  import { homedir as homedir8 } from "os";
22591
- async function handleOptimizeCommand(chatId, channel, _args) {
23360
+ async function handleOptimizeCommand(chatId, channel, _args, messageId) {
22592
23361
  const { getModelDisplayInfo: getModelDisplayInfo2 } = await Promise.resolve().then(() => (init_analyze2(), analyze_exports2));
22593
23362
  const {
22594
23363
  buildMainMenuMessage: buildMainMenuMessage2,
22595
23364
  buildMainMenuKeyboard: buildMainMenuKeyboard2,
22596
23365
  buildModelRecommendationMessage: buildModelRecommendationMessage2,
22597
23366
  buildModelRecommendationKeyboard: buildModelRecommendationKeyboard2
22598
- } = await Promise.resolve().then(() => (init_ui2(), ui_exports));
23367
+ } = await Promise.resolve().then(() => (init_ui2(), ui_exports2));
22599
23368
  const modelInfo = getModelDisplayInfo2(chatId);
22600
23369
  if (!modelInfo) {
22601
23370
  await channel.sendText(chatId, "No AI backend available. Configure one first.", { parseMode: "plain" });
@@ -22603,105 +23372,106 @@ async function handleOptimizeCommand(chatId, channel, _args) {
22603
23372
  }
22604
23373
  if (modelInfo.isWeak) {
22605
23374
  const msg2 = buildModelRecommendationMessage2(modelInfo.model);
22606
- if (typeof channel.sendKeyboard === "function") {
22607
- await channel.sendKeyboard(chatId, msg2, buildModelRecommendationKeyboard2());
22608
- } else {
22609
- await channel.sendText(chatId, msg2 + "\n\nSwitch to an advanced model for best results.", { parseMode: "plain" });
22610
- }
23375
+ await sendOrEditKeyboard(chatId, channel, messageId, msg2, buildModelRecommendationKeyboard2());
22611
23376
  return;
22612
23377
  }
22613
23378
  const msg = buildMainMenuMessage2(modelInfo.backend, modelInfo.model, modelInfo.thinkingLevel);
22614
- if (typeof channel.sendKeyboard === "function") {
22615
- await channel.sendKeyboard(chatId, msg, buildMainMenuKeyboard2());
22616
- } else {
22617
- await channel.sendText(chatId, msg, { parseMode: "plain" });
22618
- }
23379
+ await sendOrEditKeyboard(chatId, channel, messageId, msg, buildMainMenuKeyboard2());
22619
23380
  }
22620
- async function handleOptimizeCallback(chatId, data, channel) {
23381
+ async function handleOptimizeCallback(chatId, data, channel, messageId) {
22621
23382
  const parts = data.split(":");
22622
23383
  const action = parts[1];
22623
23384
  switch (action) {
22624
23385
  case "identity": {
22625
- await runIdentityAuditFlow(chatId, channel);
23386
+ await runIdentityAuditFlow(chatId, channel, messageId);
22626
23387
  break;
22627
23388
  }
22628
23389
  case "skill-menu": {
22629
- await showSkillPicker(chatId, channel, 0);
23390
+ await showSkillPicker(chatId, channel, 0, messageId);
22630
23391
  break;
22631
23392
  }
22632
23393
  case "skill-page": {
22633
23394
  const page = parseInt(parts[2], 10) || 0;
22634
- await showSkillPicker(chatId, channel, page);
23395
+ await showSkillPicker(chatId, channel, page, messageId);
22635
23396
  break;
22636
23397
  }
22637
23398
  case "skill-pick": {
22638
23399
  const skillName = parts.slice(2).join(":");
22639
- await runSkillAuditFlow(chatId, channel, skillName);
23400
+ await runSkillAuditFlow(chatId, channel, skillName, messageId);
22640
23401
  break;
22641
23402
  }
22642
23403
  case "model-ok": {
22643
23404
  const { getModelDisplayInfo: getModelDisplayInfo2 } = await Promise.resolve().then(() => (init_analyze2(), analyze_exports2));
22644
- const { buildMainMenuMessage: buildMainMenuMessage2, buildMainMenuKeyboard: buildMainMenuKeyboard2 } = await Promise.resolve().then(() => (init_ui2(), ui_exports));
23405
+ const { buildMainMenuMessage: buildMainMenuMessage2, buildMainMenuKeyboard: buildMainMenuKeyboard2 } = await Promise.resolve().then(() => (init_ui2(), ui_exports2));
22645
23406
  const modelInfo = getModelDisplayInfo2(chatId);
22646
23407
  if (!modelInfo) return;
22647
23408
  const msg = buildMainMenuMessage2(modelInfo.backend, modelInfo.model, modelInfo.thinkingLevel);
22648
- if (typeof channel.sendKeyboard === "function") {
22649
- await channel.sendKeyboard(chatId, msg, buildMainMenuKeyboard2());
22650
- }
23409
+ await sendOrEditKeyboard(chatId, channel, messageId, msg, buildMainMenuKeyboard2());
22651
23410
  break;
22652
23411
  }
22653
23412
  case "model-switch": {
22654
- await channel.sendText(chatId, "Use /model or /backend to switch to a more powerful model, then run /optimize again.", { parseMode: "plain" });
23413
+ await sendOrEditKeyboard(
23414
+ chatId,
23415
+ channel,
23416
+ messageId,
23417
+ "Use /model or /backend to switch to a more powerful model, then run /optimize again.",
23418
+ [[{ label: "\u2190 Back", data: "opt:model-ok" }]]
23419
+ );
22655
23420
  break;
22656
23421
  }
22657
23422
  case "review": {
22658
23423
  const index = parseInt(parts[2], 10) || 0;
22659
- await showFinding(chatId, channel, index);
23424
+ const session2 = activeSessions.get(chatId);
23425
+ if (session2 && messageId) session2.messageId = messageId;
23426
+ await showFinding(chatId, channel, index, messageId);
22660
23427
  break;
22661
23428
  }
22662
23429
  case "fix": {
22663
23430
  const fixIndex = parseInt(parts[2], 10) || 0;
22664
- await applyFinding(chatId, channel, fixIndex);
23431
+ const session2 = activeSessions.get(chatId);
23432
+ await applyFinding(chatId, channel, fixIndex, messageId ?? session2?.messageId);
22665
23433
  break;
22666
23434
  }
22667
23435
  case "skip": {
22668
23436
  const skipIndex = parseInt(parts[2], 10) || 0;
22669
- await skipFinding(chatId, channel, skipIndex);
23437
+ const session2 = activeSessions.get(chatId);
23438
+ await skipFinding(chatId, channel, skipIndex, messageId ?? session2?.messageId);
22670
23439
  break;
22671
23440
  }
22672
23441
  case "stop": {
22673
- await stopReview(chatId, channel);
23442
+ const session2 = activeSessions.get(chatId);
23443
+ await stopReview(chatId, channel, messageId ?? session2?.messageId);
22674
23444
  break;
22675
23445
  }
22676
23446
  case "close": {
22677
23447
  activeSessions.delete(chatId);
22678
- await channel.sendText(chatId, "Optimizer closed.", { parseMode: "plain" });
23448
+ if (messageId && channel.editText) {
23449
+ await channel.editText(chatId, messageId, "Optimizer closed.", "plain");
23450
+ } else {
23451
+ await channel.sendText(chatId, "Optimizer closed.", { parseMode: "plain" });
23452
+ }
22679
23453
  break;
22680
23454
  }
22681
23455
  default:
22682
23456
  break;
22683
23457
  }
22684
23458
  }
22685
- async function runIdentityAuditFlow(chatId, channel) {
23459
+ async function runIdentityAuditFlow(chatId, channel, messageId) {
22686
23460
  const { getModelDisplayInfo: getModelDisplayInfo2, runIdentityAudit: runIdentityAudit2 } = await Promise.resolve().then(() => (init_analyze2(), analyze_exports2));
22687
23461
  const {
22688
23462
  buildProgressMessage: buildProgressMessage2,
22689
23463
  buildIdentityAuditSummary: buildIdentityAuditSummary2,
22690
23464
  buildIdentityAuditKeyboard: buildIdentityAuditKeyboard2
22691
- } = await Promise.resolve().then(() => (init_ui2(), ui_exports));
23465
+ } = await Promise.resolve().then(() => (init_ui2(), ui_exports2));
22692
23466
  const modelInfo = getModelDisplayInfo2(chatId);
22693
23467
  if (!modelInfo) return;
22694
- const progressMsgId = typeof channel.sendTextReturningId === "function" ? await channel.sendTextReturningId(
22695
- chatId,
22696
- buildProgressMessage2("identity files", modelInfo.backend, modelInfo.model, modelInfo.thinkingLevel),
22697
- "plain"
22698
- ) : void 0;
22699
- if (!progressMsgId) {
22700
- await channel.sendText(
22701
- chatId,
22702
- buildProgressMessage2("identity files", modelInfo.backend, modelInfo.model, modelInfo.thinkingLevel),
22703
- { parseMode: "plain" }
22704
- );
23468
+ const progressText = buildProgressMessage2("identity files", modelInfo.backend, modelInfo.model, modelInfo.thinkingLevel);
23469
+ let progressMsgId = messageId;
23470
+ if (messageId && channel.editKeyboard) {
23471
+ await channel.editKeyboard(chatId, messageId, progressText + "\n\u23F3 Analyzing...", []);
23472
+ } else {
23473
+ progressMsgId = await channel.sendTextReturningId?.(chatId, progressText, "plain");
23474
+ if (!progressMsgId) await channel.sendText(chatId, progressText, { parseMode: "plain" });
22705
23475
  }
22706
23476
  const startTime = Date.now();
22707
23477
  const progressInterval = setInterval(async () => {
@@ -22709,13 +23479,8 @@ async function runIdentityAuditFlow(chatId, channel) {
22709
23479
  try {
22710
23480
  if (channel.sendTyping) await channel.sendTyping(chatId);
22711
23481
  if (progressMsgId && channel.editText) {
22712
- await channel.editText(
22713
- chatId,
22714
- progressMsgId,
22715
- buildProgressMessage2("identity files", modelInfo.backend, modelInfo.model, modelInfo.thinkingLevel) + `
22716
- \u23F3 Analyzing... (${elapsed}s)`,
22717
- "plain"
22718
- );
23482
+ await channel.editText(chatId, progressMsgId, progressText + `
23483
+ \u23F3 Analyzing... (${elapsed}s)`, "plain");
22719
23484
  }
22720
23485
  } catch {
22721
23486
  }
@@ -22723,62 +23488,43 @@ async function runIdentityAuditFlow(chatId, channel) {
22723
23488
  try {
22724
23489
  const result = await runIdentityAudit2(chatId);
22725
23490
  clearInterval(progressInterval);
22726
- activeSessions.set(chatId, {
22727
- chatId,
22728
- result,
22729
- currentIndex: 0,
22730
- applied: [],
22731
- skipped: []
22732
- });
23491
+ activeSessions.set(chatId, { chatId, result, currentIndex: 0, applied: [], skipped: [], messageId: progressMsgId });
22733
23492
  const summary = buildIdentityAuditSummary2(result);
22734
- if (typeof channel.sendKeyboard === "function") {
22735
- await channel.sendKeyboard(chatId, summary, buildIdentityAuditKeyboard2(result.findings.length));
22736
- } else {
22737
- await channel.sendText(chatId, summary, { parseMode: "plain" });
22738
- }
23493
+ await sendOrEditKeyboard(chatId, channel, progressMsgId, summary, buildIdentityAuditKeyboard2(result.findings.length));
22739
23494
  } catch (e) {
22740
23495
  clearInterval(progressInterval);
22741
- await channel.sendText(chatId, `Identity audit failed: ${e}`, { parseMode: "plain" });
23496
+ await sendOrEditKeyboard(chatId, channel, progressMsgId, `Identity audit failed: ${e}`, [[{ label: "\u2190 Back", data: "opt:model-ok" }]]);
22742
23497
  }
22743
23498
  }
22744
- async function showSkillPicker(chatId, channel, page) {
23499
+ async function showSkillPicker(chatId, channel, page, messageId) {
22745
23500
  const { listCcClawSkills: listCcClawSkills2 } = await Promise.resolve().then(() => (init_analyze2(), analyze_exports2));
22746
- const { buildSkillPickerMessage: buildSkillPickerMessage2, buildSkillPickerKeyboard: buildSkillPickerKeyboard2 } = await Promise.resolve().then(() => (init_ui2(), ui_exports));
23501
+ const { buildSkillPickerMessage: buildSkillPickerMessage2, buildSkillPickerKeyboard: buildSkillPickerKeyboard2 } = await Promise.resolve().then(() => (init_ui2(), ui_exports2));
22747
23502
  const skills2 = listCcClawSkills2();
22748
23503
  if (skills2.length === 0) {
22749
- await channel.sendText(chatId, "No CC-Claw skills found in ~/.cc-claw/workspace/skills/", { parseMode: "plain" });
23504
+ await sendOrEditKeyboard(chatId, channel, messageId, "No CC-Claw skills found in ~/.cc-claw/workspace/skills/", [[{ label: "\u2190 Back", data: "opt:model-ok" }]]);
22750
23505
  return;
22751
23506
  }
22752
23507
  const totalPages = Math.ceil(skills2.length / 6);
22753
23508
  const msg = buildSkillPickerMessage2(page, totalPages);
22754
- if (typeof channel.sendKeyboard === "function") {
22755
- await channel.sendKeyboard(chatId, msg, buildSkillPickerKeyboard2(skills2, page));
22756
- } else {
22757
- const list = skills2.map((s) => `\u2022 ${s.name} \u2014 ${s.description}`).join("\n");
22758
- await channel.sendText(chatId, msg + "\n\n" + list, { parseMode: "plain" });
22759
- }
23509
+ await sendOrEditKeyboard(chatId, channel, messageId, msg, buildSkillPickerKeyboard2(skills2, page));
22760
23510
  }
22761
- async function runSkillAuditFlow(chatId, channel, skillName) {
23511
+ async function runSkillAuditFlow(chatId, channel, skillName, messageId) {
22762
23512
  const { getModelDisplayInfo: getModelDisplayInfo2, runSkillAudit: runSkillAudit2 } = await Promise.resolve().then(() => (init_analyze2(), analyze_exports2));
22763
23513
  const {
22764
23514
  buildProgressMessage: buildProgressMessage2,
22765
23515
  buildSkillAuditSummary: buildSkillAuditSummary2,
22766
23516
  buildSkillAuditKeyboard: buildSkillAuditKeyboard2
22767
- } = await Promise.resolve().then(() => (init_ui2(), ui_exports));
23517
+ } = await Promise.resolve().then(() => (init_ui2(), ui_exports2));
22768
23518
  const modelInfo = getModelDisplayInfo2(chatId);
22769
23519
  if (!modelInfo) return;
22770
23520
  const skillPath = join29(homedir8(), ".cc-claw", "workspace", "skills", skillName, "SKILL.md");
22771
- const progressMsgId = typeof channel.sendTextReturningId === "function" ? await channel.sendTextReturningId(
22772
- chatId,
22773
- buildProgressMessage2(`skill: ${skillName}`, modelInfo.backend, modelInfo.model, modelInfo.thinkingLevel),
22774
- "plain"
22775
- ) : void 0;
22776
- if (!progressMsgId) {
22777
- await channel.sendText(
22778
- chatId,
22779
- buildProgressMessage2(`skill: ${skillName}`, modelInfo.backend, modelInfo.model, modelInfo.thinkingLevel),
22780
- { parseMode: "plain" }
22781
- );
23521
+ const progressText = buildProgressMessage2(`skill: ${skillName}`, modelInfo.backend, modelInfo.model, modelInfo.thinkingLevel);
23522
+ let progressMsgId = messageId;
23523
+ if (messageId && channel.editKeyboard) {
23524
+ await channel.editKeyboard(chatId, messageId, progressText + "\n\u23F3 Analyzing...", []);
23525
+ } else {
23526
+ progressMsgId = await channel.sendTextReturningId?.(chatId, progressText, "plain");
23527
+ if (!progressMsgId) await channel.sendText(chatId, progressText, { parseMode: "plain" });
22782
23528
  }
22783
23529
  const startTime = Date.now();
22784
23530
  const progressInterval = setInterval(async () => {
@@ -22786,13 +23532,8 @@ async function runSkillAuditFlow(chatId, channel, skillName) {
22786
23532
  try {
22787
23533
  if (channel.sendTyping) await channel.sendTyping(chatId);
22788
23534
  if (progressMsgId && channel.editText) {
22789
- await channel.editText(
22790
- chatId,
22791
- progressMsgId,
22792
- buildProgressMessage2(`skill: ${skillName}`, modelInfo.backend, modelInfo.model, modelInfo.thinkingLevel) + `
22793
- \u23F3 Analyzing... (${elapsed}s)`,
22794
- "plain"
22795
- );
23535
+ await channel.editText(chatId, progressMsgId, progressText + `
23536
+ \u23F3 Analyzing... (${elapsed}s)`, "plain");
22796
23537
  }
22797
23538
  } catch {
22798
23539
  }
@@ -22800,66 +23541,47 @@ async function runSkillAuditFlow(chatId, channel, skillName) {
22800
23541
  try {
22801
23542
  const result = await runSkillAudit2(chatId, skillPath);
22802
23543
  clearInterval(progressInterval);
22803
- activeSessions.set(chatId, {
22804
- chatId,
22805
- result,
22806
- currentIndex: 0,
22807
- applied: [],
22808
- skipped: []
22809
- });
23544
+ activeSessions.set(chatId, { chatId, result, currentIndex: 0, applied: [], skipped: [], messageId: progressMsgId });
22810
23545
  const summary = buildSkillAuditSummary2(result);
22811
- if (typeof channel.sendKeyboard === "function") {
22812
- await channel.sendKeyboard(chatId, summary, buildSkillAuditKeyboard2(result.findings.length));
22813
- } else {
22814
- await channel.sendText(chatId, summary, { parseMode: "plain" });
22815
- }
23546
+ await sendOrEditKeyboard(chatId, channel, progressMsgId, summary, buildSkillAuditKeyboard2(result.findings.length));
22816
23547
  } catch (e) {
22817
23548
  clearInterval(progressInterval);
22818
- await channel.sendText(chatId, `Skill audit failed: ${e}`, { parseMode: "plain" });
23549
+ await sendOrEditKeyboard(chatId, channel, progressMsgId, `Skill audit failed: ${e}`, [[{ label: "\u2190 Back", data: "opt:skill-menu" }]]);
22819
23550
  }
22820
23551
  }
22821
- async function showFinding(chatId, channel, index) {
23552
+ async function showFinding(chatId, channel, index, messageId) {
22822
23553
  const session2 = activeSessions.get(chatId);
22823
23554
  if (!session2) {
22824
- await channel.sendText(chatId, "No active optimizer session. Run /optimize first.", { parseMode: "plain" });
23555
+ await sendOrEditKeyboard(chatId, channel, messageId, "No active optimizer session. Run /optimize first.", [[{ label: "Run Optimizer", data: "opt:model-ok" }]]);
22825
23556
  return;
22826
23557
  }
23558
+ const effectiveMsgId = messageId ?? session2.messageId;
22827
23559
  const { findings } = session2.result;
22828
23560
  if (index >= findings.length) {
22829
- await finishReview(chatId, channel);
23561
+ await finishReview(chatId, channel, effectiveMsgId);
22830
23562
  return;
22831
23563
  }
22832
23564
  session2.currentIndex = index;
23565
+ if (effectiveMsgId) session2.messageId = effectiveMsgId;
22833
23566
  const finding = findings[index];
22834
- const { buildFindingMessage: buildFindingMessage2, buildFindingKeyboard: buildFindingKeyboard2 } = await Promise.resolve().then(() => (init_ui2(), ui_exports));
23567
+ const { buildFindingMessage: buildFindingMessage2, buildFindingKeyboard: buildFindingKeyboard2 } = await Promise.resolve().then(() => (init_ui2(), ui_exports2));
22835
23568
  const msg = buildFindingMessage2(finding, index, findings.length);
22836
- if (typeof channel.sendKeyboard === "function") {
22837
- await channel.sendKeyboard(chatId, msg, buildFindingKeyboard2(index, findings.length, !!finding.proposedDiff));
22838
- } else {
22839
- await channel.sendText(chatId, msg, { parseMode: "plain" });
22840
- }
23569
+ await sendOrEditKeyboard(chatId, channel, effectiveMsgId, msg, buildFindingKeyboard2(index, findings.length, !!finding.proposedDiff));
22841
23570
  }
22842
- async function applyFinding(chatId, channel, index) {
23571
+ async function applyFinding(chatId, channel, index, messageId) {
22843
23572
  const session2 = activeSessions.get(chatId);
22844
23573
  if (!session2) return;
23574
+ const effectiveMsgId = messageId ?? session2.messageId;
22845
23575
  const finding = session2.result.findings[index];
22846
23576
  if (!finding || !finding.proposedDiff) {
22847
- await channel.sendText(chatId, "No diff to apply for this finding.", { parseMode: "plain" });
22848
- await showFinding(chatId, channel, index + 1);
23577
+ await showFinding(chatId, channel, index + 1, effectiveMsgId);
22849
23578
  return;
22850
23579
  }
22851
23580
  try {
22852
23581
  const targetPath = resolveTargetFile(finding.location, session2.result.target);
22853
- if (!targetPath) {
22854
- await channel.sendText(chatId, `Cannot determine target file from location: ${finding.location}`, { parseMode: "plain" });
23582
+ if (!targetPath || !existsSync28(targetPath)) {
22855
23583
  session2.skipped.push(index);
22856
- await showFinding(chatId, channel, index + 1);
22857
- return;
22858
- }
22859
- if (!existsSync28(targetPath)) {
22860
- await channel.sendText(chatId, `Target file not found: ${targetPath}`, { parseMode: "plain" });
22861
- session2.skipped.push(index);
22862
- await showFinding(chatId, channel, index + 1);
23584
+ await showFinding(chatId, channel, index + 1, effectiveMsgId);
22863
23585
  return;
22864
23586
  }
22865
23587
  const original = readFileSync17(targetPath, "utf-8");
@@ -22869,13 +23591,7 @@ async function applyFinding(chatId, channel, index) {
22869
23591
  let newContent;
22870
23592
  try {
22871
23593
  const { applyWithAI: applyWithAI2 } = await Promise.resolve().then(() => (init_ai_apply(), ai_apply_exports));
22872
- const aiResult = await applyWithAI2(
22873
- chatId,
22874
- original,
22875
- finding.suggestion || finding.title,
22876
- finding.proposedDiff,
22877
- finding.location.split(":")[0] || "unknown"
22878
- );
23594
+ const aiResult = await applyWithAI2(chatId, original, finding.suggestion || finding.title, finding.proposedDiff, finding.location.split(":")[0] || "unknown");
22879
23595
  if (aiResult.success) {
22880
23596
  newContent = aiResult.newContent;
22881
23597
  } else {
@@ -22889,9 +23605,8 @@ async function applyFinding(chatId, channel, index) {
22889
23605
  newContent = applyDiff2(original, finding.proposedDiff, "replace");
22890
23606
  }
22891
23607
  if (newContent === original) {
22892
- await channel.sendText(chatId, `\u26A0\uFE0F Diff produced no changes. The content may have already been modified.`, { parseMode: "plain" });
22893
23608
  session2.skipped.push(index);
22894
- await showFinding(chatId, channel, index + 1);
23609
+ await showFinding(chatId, channel, index + 1, effectiveMsgId);
22895
23610
  return;
22896
23611
  }
22897
23612
  writeFileSync9(targetPath, newContent, "utf-8");
@@ -22904,34 +23619,32 @@ async function applyFinding(chatId, channel, index) {
22904
23619
  warn("[optimize] syncNativeCliFiles failed:", err instanceof Error ? err.message : err);
22905
23620
  }
22906
23621
  }
22907
- await channel.sendText(chatId, `\u2705 Applied: ${finding.title}`, { parseMode: "plain" });
22908
23622
  } catch (e) {
22909
- const errorMsg = e?.message ?? String(e);
22910
- await channel.sendText(chatId, `\u274C Failed to apply fix: ${errorMsg}`, { parseMode: "plain" });
22911
23623
  session2.skipped.push(index);
22912
23624
  }
22913
- await showFinding(chatId, channel, index + 1);
23625
+ await showFinding(chatId, channel, index + 1, effectiveMsgId);
22914
23626
  }
22915
- async function skipFinding(chatId, channel, index) {
23627
+ async function skipFinding(chatId, channel, index, messageId) {
22916
23628
  const session2 = activeSessions.get(chatId);
22917
23629
  if (!session2) return;
22918
23630
  session2.skipped.push(index);
22919
- await showFinding(chatId, channel, index + 1);
23631
+ await showFinding(chatId, channel, index + 1, messageId ?? session2.messageId);
22920
23632
  }
22921
- async function stopReview(chatId, channel) {
22922
- await finishReview(chatId, channel);
23633
+ async function stopReview(chatId, channel, messageId) {
23634
+ await finishReview(chatId, channel, messageId);
22923
23635
  }
22924
- async function finishReview(chatId, channel) {
23636
+ async function finishReview(chatId, channel, messageId) {
22925
23637
  const session2 = activeSessions.get(chatId);
22926
23638
  if (!session2) return;
22927
- const { buildReviewCompleteMessage: buildReviewCompleteMessage3, buildReviewCompleteKeyboard: buildReviewCompleteKeyboard2 } = await Promise.resolve().then(() => (init_ui2(), ui_exports));
23639
+ const { buildReviewCompleteMessage: buildReviewCompleteMessage3, buildReviewCompleteKeyboard: buildReviewCompleteKeyboard2 } = await Promise.resolve().then(() => (init_ui2(), ui_exports2));
22928
23640
  const msg = buildReviewCompleteMessage3(
22929
23641
  session2.applied.length,
22930
23642
  session2.skipped.length,
22931
23643
  session2.result.findings.length
22932
23644
  );
22933
- if (typeof channel.sendKeyboard === "function") {
22934
- await channel.sendKeyboard(chatId, msg, buildReviewCompleteKeyboard2());
23645
+ const effectiveMsgId = messageId ?? session2.messageId;
23646
+ if (effectiveMsgId) {
23647
+ await sendOrEditKeyboard(chatId, channel, effectiveMsgId, msg, buildReviewCompleteKeyboard2());
22935
23648
  } else {
22936
23649
  await channel.sendText(chatId, msg, { parseMode: "plain" });
22937
23650
  }
@@ -22971,6 +23684,7 @@ var init_optimize = __esm({
22971
23684
  "src/router/optimize.ts"() {
22972
23685
  "use strict";
22973
23686
  init_log();
23687
+ init_helpers();
22974
23688
  activeSessions = /* @__PURE__ */ new Map();
22975
23689
  }
22976
23690
  });
@@ -23196,7 +23910,7 @@ __export(auto_create_exports, {
23196
23910
  storePendingDraft: () => storePendingDraft
23197
23911
  });
23198
23912
  import { join as join30 } from "path";
23199
- import { writeFile as writeFile5, mkdir as mkdir5 } from "fs/promises";
23913
+ import { writeFile as writeFile5, mkdir as mkdir4 } from "fs/promises";
23200
23914
  function isSkillWorthy(signals) {
23201
23915
  const { toolUseCount, tokenOutput, elapsedMs, userMessage } = signals;
23202
23916
  if (toolUseCount < 12) return false;
@@ -23378,7 +24092,7 @@ async function saveSkill(name, content, opts = {}) {
23378
24092
  }
23379
24093
  }
23380
24094
  const dir = join30(SKILLS_PATH, name);
23381
- await mkdir5(dir, { recursive: true });
24095
+ await mkdir4(dir, { recursive: true });
23382
24096
  const filePath = join30(dir, "SKILL.md");
23383
24097
  await writeFile5(filePath, withFrontmatter, "utf-8");
23384
24098
  invalidateSkillCache();
@@ -23558,34 +24272,10 @@ async function handleVoiceConfigCommand(chatId, commandArgs, msg, channel) {
23558
24272
  await sendVoiceConfigKeyboard(chatId, channel);
23559
24273
  }
23560
24274
  async function handleResponseStyleCommand(chatId, commandArgs, msg, channel) {
23561
- const currentStyle = getResponseStyle(chatId);
23562
- if (typeof channel.sendKeyboard === "function") {
23563
- await channel.sendKeyboard(chatId, "\u{1F5E3}\uFE0F AI Response Style:", [
23564
- [
23565
- { label: `${currentStyle === "concise" ? "\u2713 " : ""}Concise`, data: "style:concise", ...currentStyle === "concise" ? { style: "primary" } : {} },
23566
- { label: `${currentStyle === "normal" ? "\u2713 " : ""}Normal`, data: "style:normal", ...currentStyle === "normal" ? { style: "primary" } : {} },
23567
- { label: `${currentStyle === "detailed" ? "\u2713 " : ""}Detailed`, data: "style:detailed", ...currentStyle === "detailed" ? { style: "primary" } : {} }
23568
- ]
23569
- ]);
23570
- } else {
23571
- await channel.sendText(chatId, `Current Response Style: ${currentStyle}`, { parseMode: "plain" });
23572
- }
24275
+ await sendResponseStyleKeyboard(chatId, channel);
23573
24276
  }
23574
24277
  async function handleModelSignatureCommand(chatId, commandArgs, msg, channel) {
23575
- const currentSig = getModelSignature(chatId);
23576
- if (typeof channel.sendKeyboard === "function") {
23577
- await channel.sendKeyboard(chatId, `Model Signature: ${currentSig === "on" ? "ON" : "OFF"}
23578
- Appends model + thinking level to each response.`, [
23579
- [
23580
- { label: currentSig === "on" ? "\u2713 On" : "On", data: "model_sig:on", ...currentSig === "on" ? { style: "success" } : {} },
23581
- { label: currentSig !== "on" ? "\u2713 Off" : "Off", data: "model_sig:off", ...currentSig !== "on" ? { style: "danger" } : {} }
23582
- ]
23583
- ]);
23584
- } else {
23585
- const newSig = currentSig === "on" ? "off" : "on";
23586
- setModelSignature(chatId, newSig);
23587
- await channel.sendText(chatId, newSig === "on" ? "Model signature enabled." : "Model signature disabled.", { parseMode: "plain" });
23588
- }
24278
+ await sendModelSigKeyboard(chatId, channel);
23589
24279
  }
23590
24280
  async function handleRememberCommand(chatId, commandArgs, msg, channel) {
23591
24281
  if (!commandArgs) {
@@ -23604,15 +24294,29 @@ async function handleRememberCommand(chatId, commandArgs, msg, channel) {
23604
24294
  }
23605
24295
  async function handleForgetCommand(chatId, commandArgs, msg, channel) {
23606
24296
  if (!commandArgs) {
23607
- await channel.sendText(chatId, "Usage: /forget <keyword>", { parseMode: "plain" });
24297
+ await sendForgetPicker(chatId, channel, 1);
23608
24298
  return;
23609
24299
  }
23610
- const count = forgetMemory(commandArgs);
23611
- await channel.sendText(
23612
- chatId,
23613
- count > 0 ? `Forgot ${count} memory(ies) matching "${commandArgs}".` : `No memories found matching "${commandArgs}".`,
23614
- { parseMode: "plain" }
24300
+ const keyword = commandArgs.trim();
24301
+ const matching = listMemories().filter(
24302
+ (m) => m.trigger.toLowerCase().includes(keyword.toLowerCase()) || m.content.toLowerCase().includes(keyword.toLowerCase())
23615
24303
  );
24304
+ if (matching.length === 0) {
24305
+ await channel.sendText(chatId, `No memories found matching "${keyword}".`, { parseMode: "plain" });
24306
+ return;
24307
+ }
24308
+ const preview = matching.slice(0, 3).map((m) => `\u2022 ${m.trigger}: ${m.content.slice(0, 40)}`).join("\n");
24309
+ const more = matching.length > 3 ? `
24310
+ \u2026and ${matching.length - 3} more` : "";
24311
+ const confirmText = `Delete ${matching.length} memory(ies) matching "${keyword}"?
24312
+
24313
+ ${preview}${more}`;
24314
+ await sendOrEditKeyboard(chatId, channel, void 0, confirmText, [
24315
+ [
24316
+ { label: `\u{1F5D1} Delete ${matching.length}`, data: `forget:keyword:${keyword}`, style: "danger" },
24317
+ { label: "Cancel", data: "forget:cancel" }
24318
+ ]
24319
+ ]);
23616
24320
  }
23617
24321
  async function handleIntentCommand(chatId, commandArgs, msg, channel) {
23618
24322
  const testMsg = commandArgs?.trim() || "hey";
@@ -23835,72 +24539,13 @@ async function handleMenuCommand(chatId, commandArgs, msg, channel) {
23835
24539
  }
23836
24540
  }
23837
24541
  async function handlePermissionsCommand(chatId, commandArgs, msg, channel) {
23838
- const currentMode = getMode(chatId);
23839
- const currentExecMode = getExecMode(chatId);
23840
- if (typeof channel.sendKeyboard === "function") {
23841
- const permButtons = Object.entries(PERM_MODES).map(([id, label2]) => [{
23842
- label: `${id === currentMode ? "\u2713 " : ""}${label2}`,
23843
- data: `perms:${id}`,
23844
- ...id === currentMode ? { style: "primary" } : {}
23845
- }]);
23846
- const approvalOn = currentExecMode === "approved";
23847
- permButtons.push([{
23848
- label: `${approvalOn ? "\u2713 " : ""}\u{1F512} Approve Before Execute: ${approvalOn ? "ON" : "OFF"}`,
23849
- data: `execmode:${approvalOn ? "yolo" : "approved"}`,
23850
- ...approvalOn ? { style: "primary" } : {}
23851
- }]);
23852
- await channel.sendKeyboard(chatId, "Permission & Execution Settings:", permButtons);
23853
- } else {
23854
- const lines = ["Permission modes:", ""];
23855
- for (const [id, label2] of Object.entries(PERM_MODES)) {
23856
- lines.push(`${id === currentMode ? "\u2713 " : " "}/permissions ${id} \u2014 ${label2}`);
23857
- }
23858
- lines.push("");
23859
- lines.push(`Approve Before Execute: ${currentExecMode === "approved" ? "ON" : "OFF"}`);
23860
- await channel.sendText(chatId, lines.join("\n"), { parseMode: "plain" });
23861
- }
24542
+ await sendPermissionsKeyboard(chatId, channel);
23862
24543
  }
23863
24544
  async function handleVerboseCommand(chatId, commandArgs, msg, channel) {
23864
- const currentVerbose = getVerboseLevel(chatId);
23865
- const buttons = Object.entries(VERBOSE_LEVELS).map(([id, label2]) => [{
23866
- label: `${id === currentVerbose ? "\u2713 " : ""}${label2}`,
23867
- data: `verbose:${id}`,
23868
- ...id === currentVerbose ? { style: "primary" } : {}
23869
- }]);
23870
- if (typeof channel.sendKeyboard === "function") {
23871
- await channel.sendKeyboard(chatId, "Tool visibility level:", buttons);
23872
- } else {
23873
- const lines = Object.entries(VERBOSE_LEVELS).map(
23874
- ([id, label2]) => `${id === currentVerbose ? "\u2713 " : " "}${id} \u2014 ${label2}`
23875
- );
23876
- await channel.sendText(chatId, ["Tool visibility:", "", ...lines].join("\n"), { parseMode: "plain" });
23877
- }
24545
+ await sendVerboseKeyboard(chatId, channel);
23878
24546
  }
23879
24547
  async function handleToolsCommand(chatId, commandArgs, msg, channel) {
23880
- const toolsMap = getToolsMap(chatId);
23881
- const currentMode = getMode(chatId);
23882
- if (typeof channel.sendKeyboard === "function") {
23883
- const toolButtons = [];
23884
- const toolList = [...ALL_TOOLS];
23885
- for (let i = 0; i < toolList.length; i += 3) {
23886
- const row = toolList.slice(i, i + 3).map((t) => ({
23887
- label: `${toolsMap[t] ? "\u2705" : "\u274C"} ${t}`,
23888
- data: `tool:toggle:${t}`
23889
- }));
23890
- toolButtons.push(row);
23891
- }
23892
- toolButtons.push([{ label: "Reset to defaults (all on)", data: "tool:reset", style: "danger" }]);
23893
- 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)." : "";
23894
- await channel.sendKeyboard(
23895
- chatId,
23896
- `Configure allowed tools (mode: ${currentMode})${modeNote}
23897
- Tap to toggle:`,
23898
- toolButtons
23899
- );
23900
- } else {
23901
- const lines = ALL_TOOLS.map((t) => `${toolsMap[t] ? "[on] " : "[off]"} ${t}`);
23902
- await channel.sendText(chatId, ["Allowed tools:", "", ...lines].join("\n"), { parseMode: "plain" });
23903
- }
24548
+ await sendToolsKeyboard(chatId, channel);
23904
24549
  }
23905
24550
  async function handleNewchatCommand(chatId, commandArgs, msg, channel) {
23906
24551
  stopAllSideQuests(chatId);
@@ -23940,14 +24585,22 @@ async function handleNewchatCommand(chatId, commandArgs, msg, channel) {
23940
24585
  const text = `\u2705 New session started. Previous session archived${exchangeCount > 0 ? ` (${exchangeCount} exchanges)` : ""}.
23941
24586
 
23942
24587
  \u{1F9E0} ${backendLabel} \xB7 ${modelLabel}`;
23943
- if (ackMsgId) await channel.editText?.(chatId, ackMsgId, text, "plain");
23944
- const kbMsgId = await channel.sendKeyboard(chatId, text, [
24588
+ const kbButtons = [
23945
24589
  [
23946
24590
  { label: "Switch Backend", data: "menu:backend", style: "primary" },
23947
24591
  { label: "Switch Model", data: "menu:model", style: "primary" }
23948
24592
  ],
23949
24593
  [{ label: "Undo", data: "newchat:undo" }]
23950
- ]);
24594
+ ];
24595
+ let kbMsgId;
24596
+ if (ackMsgId && typeof channel.editKeyboard === "function") {
24597
+ const edited = await channel.editKeyboard(chatId, ackMsgId, text, kbButtons);
24598
+ if (edited) kbMsgId = ackMsgId;
24599
+ }
24600
+ if (!kbMsgId) {
24601
+ if (ackMsgId) await channel.editText?.(chatId, ackMsgId, text, "plain");
24602
+ kbMsgId = await channel.sendKeyboard(chatId, text, kbButtons);
24603
+ }
23951
24604
  if (kbMsgId) {
23952
24605
  const prev = pendingNewchatUndo.get(chatId);
23953
24606
  if (prev) clearTimeout(prev.timer);
@@ -23966,6 +24619,14 @@ async function handleNewchatCommand(chatId, commandArgs, msg, channel) {
23966
24619
  }
23967
24620
  }
23968
24621
  }
24622
+ async function handleClearCommand(chatId, _commandArgs, _msg, channel) {
24623
+ stopAllSideQuests(chatId);
24624
+ clearSession(chatId);
24625
+ clearChatPaidSlots(chatId);
24626
+ setSessionStartedAt(chatId);
24627
+ logActivity(getDb(), { chatId, source: "telegram", eventType: "config_changed", summary: "Session cleared (no summary)", detail: { field: "session", action: "clear" } });
24628
+ await channel.sendText(chatId, "\u{1F9FD} Session cleared. No summary saved.", { parseMode: "plain" });
24629
+ }
23969
24630
  async function handleSummarizeCommand(chatId, commandArgs, msg, channel) {
23970
24631
  if (commandArgs?.toLowerCase() === "all") {
23971
24632
  const pendingIds = getLoggedChatIds();
@@ -24154,25 +24815,13 @@ async function handleStatusCommand(chatId, commandArgs, msg, channel) {
24154
24815
  }
24155
24816
  async function handleBackendCommand2(chatId, commandArgs, msg, channel) {
24156
24817
  const requestedBackend = (commandArgs ?? "").trim().toLowerCase();
24157
- if (requestedBackend && getAllBackendIds().includes(requestedBackend)) {
24818
+ const chatBackendIds = getAvailableChatBackendIds();
24819
+ if (requestedBackend && chatBackendIds.includes(requestedBackend)) {
24158
24820
  await sendBackendSwitchConfirmation(chatId, requestedBackend, channel);
24159
24821
  return;
24160
24822
  }
24161
- const currentBackend = getBackend(chatId);
24162
- const adapters2 = getAvailableAdapters();
24163
- if (typeof channel.sendKeyboard === "function") {
24164
- const buttons = adapters2.map((a) => [{
24165
- label: `${a.id === currentBackend ? "\u2713 " : ""}${a.displayName}`,
24166
- data: `backend:${a.id}`,
24167
- ...a.id === currentBackend ? { style: "primary" } : {}
24168
- }]);
24169
- await channel.sendKeyboard(chatId, "Select AI backend:", buttons);
24170
- } else {
24171
- const lines = adapters2.map(
24172
- (a) => `${a.id === currentBackend ? "\u2713 " : " "}${a.displayName} (/backend ${a.id})`
24173
- );
24174
- await channel.sendText(chatId, ["Available backends:", "", ...lines].join("\n"), { parseMode: "plain" });
24175
- }
24823
+ const { sendBackendPicker: sendBackendPicker2 } = await Promise.resolve().then(() => (init_ui(), ui_exports));
24824
+ await sendBackendPicker2(chatId, channel);
24176
24825
  }
24177
24826
  async function handleBackendShortcutCommand(chatId, commandArgs, msg, channel) {
24178
24827
  const command = msg.command;
@@ -24184,82 +24833,10 @@ async function handleBackendShortcutCommand(chatId, commandArgs, msg, channel) {
24184
24833
  }
24185
24834
  }
24186
24835
  async function handleModelCommand(chatId, commandArgs, msg, channel) {
24187
- let adapter;
24188
- try {
24189
- adapter = getAdapterForChat(chatId);
24190
- } catch {
24191
- await channel.sendText(chatId, "No backend set. Use /backend first.", { parseMode: "plain" });
24192
- return;
24193
- }
24194
- const models = adapter.availableModels;
24195
- const current = getModel(chatId) ?? adapter.defaultModel;
24196
- if (typeof channel.sendKeyboard === "function") {
24197
- const buttons = Object.entries(models).map(([id, info]) => {
24198
- const tag = info.thinking === "adjustable" ? " \u26A1" : "";
24199
- return [{
24200
- label: `${id === current ? "\u2713 " : ""}${info.label}${tag}`,
24201
- data: `model:${id}`,
24202
- ...id === current ? { style: "primary" } : {}
24203
- }];
24204
- });
24205
- const isAuto = getModel(chatId) === "auto";
24206
- buttons.unshift([{
24207
- label: `${isAuto ? "\u2713 " : ""}\u{1F916} Auto (smart routing)`,
24208
- data: "model:auto",
24209
- ...isAuto ? { style: "primary" } : {}
24210
- }]);
24211
- await channel.sendKeyboard(chatId, `Models for ${adapter.displayName}:`, buttons);
24212
- } else {
24213
- const lines = Object.entries(models).map(
24214
- ([id, info]) => `${id === current ? "\u2713 " : " "}${id} \u2014 ${info.label}`
24215
- );
24216
- await channel.sendText(chatId, [`Models (${adapter.displayName}):`, "", ...lines].join("\n"), { parseMode: "plain" });
24217
- }
24836
+ await sendModelKeyboard(chatId, channel);
24218
24837
  }
24219
24838
  async function handleThinkingCommand(chatId, commandArgs, msg, channel) {
24220
- let adapter;
24221
- try {
24222
- adapter = getAdapterForChat(chatId);
24223
- } catch {
24224
- await channel.sendText(chatId, "No backend set. Use /backend first.", { parseMode: "plain" });
24225
- return;
24226
- }
24227
- const currentModel = getModel(chatId) ?? adapter.defaultModel;
24228
- const modelInfo = adapter.availableModels[currentModel];
24229
- const currentLevel = getThinkingLevel(chatId) || "auto";
24230
- if (!modelInfo || modelInfo.thinking !== "adjustable" || !modelInfo.thinkingLevels) {
24231
- await channel.sendText(chatId, `Current model (${shortModelName(currentModel)}) doesn't support adjustable thinking.
24232
- Use /model to pick a model with \u26A1 thinking support.`, { parseMode: "plain" });
24233
- return;
24234
- }
24235
- if (typeof channel.sendKeyboard === "function") {
24236
- const showThinkingUi = getShowThinkingUi(chatId);
24237
- const buttons = modelInfo.thinkingLevels.map((level) => [{
24238
- label: `${level === currentLevel ? "\u2713 " : ""}${level === "auto" ? "Auto" : capitalize(level)}`,
24239
- data: `thinking:${level}`,
24240
- ...level === currentLevel ? { style: "primary" } : {}
24241
- }]);
24242
- buttons.push([{
24243
- label: `${showThinkingUi ? "\u2713 " : ""}\u{1F4AD} Show Thinking`,
24244
- data: "thinking_show_ui:toggle",
24245
- ...showThinkingUi ? { style: "primary" } : {}
24246
- }]);
24247
- await channel.sendKeyboard(
24248
- chatId,
24249
- `\u{1F4AD} Thinking Level \u2014 ${shortModelName(currentModel)}
24250
- Current: ${capitalize(currentLevel)}
24251
- Show thinking tokens: ${showThinkingUi ? "On" : "Off"}${adapter.id !== "claude" ? `
24252
-
24253
- \u26A0\uFE0F ${adapter.displayName} CLI doesn't stream thinking tokens` : ""}`,
24254
- buttons
24255
- );
24256
- } else {
24257
- const showThinkingUi = getShowThinkingUi(chatId);
24258
- await channel.sendText(chatId, `Thinking: ${capitalize(currentLevel)}
24259
- Levels: ${modelInfo.thinkingLevels.join(", ")}
24260
- Show thinking tokens: ${showThinkingUi ? "On" : "Off"}
24261
- Set via callback (keyboard required).`, { parseMode: "plain" });
24262
- }
24839
+ await sendThinkingKeyboard(chatId, channel);
24263
24840
  }
24264
24841
  async function handleUsageCommand(chatId, commandArgs, msg, channel, command) {
24265
24842
  if (commandArgs && command === "limits") {
@@ -25359,6 +25936,9 @@ async function handleCommand(msg, channel) {
25359
25936
  case "newchat":
25360
25937
  await handleNewchatCommand(chatId, commandArgs, msg, channel);
25361
25938
  break;
25939
+ case "clear":
25940
+ await handleClearCommand(chatId, commandArgs, msg, channel);
25941
+ break;
25362
25942
  case "summarize":
25363
25943
  await handleSummarizeCommand(chatId, commandArgs, msg, channel);
25364
25944
  break;
@@ -25565,25 +26145,45 @@ async function handleCallback(chatId, data, channel, messageId) {
25565
26145
  await channel.sendText(chatId, text, { parseMode: "plain" });
25566
26146
  }
25567
26147
  };
25568
- const MENU_COMMANDS = {
25569
- newchat: "newchat",
25570
- status: "status",
25571
- backend: "backend",
25572
- model: "model",
25573
- jobs: "jobs",
25574
- memory: "memory",
25575
- history: "history",
25576
- help: "help",
25577
- permissions: "permissions",
25578
- style: "response_style",
25579
- health: "health"
25580
- };
25581
26148
  if (data.startsWith("menu:")) {
25582
26149
  const action = data.slice(5);
25583
- const cmd = MENU_COMMANDS[action];
25584
- if (cmd) {
25585
- const synth = { chatId, messageId: "", text: "", senderName: "User", type: "command", source: "telegram", command: cmd, commandArgs: "" };
25586
- await handleCommand(synth, channel);
26150
+ switch (action) {
26151
+ case "backend":
26152
+ await sendBackendPicker(chatId, channel, messageId);
26153
+ break;
26154
+ case "model":
26155
+ await sendModelKeyboard(chatId, channel, messageId);
26156
+ break;
26157
+ case "memory":
26158
+ await sendMemoryPage(chatId, channel, 1, messageId);
26159
+ break;
26160
+ case "history":
26161
+ await sendHistoryView(chatId, channel, {}, messageId);
26162
+ break;
26163
+ case "help":
26164
+ await sendHelpCategories(chatId, channel, messageId);
26165
+ break;
26166
+ case "permissions":
26167
+ await sendPermissionsKeyboard(chatId, channel, messageId);
26168
+ break;
26169
+ case "style":
26170
+ await sendResponseStyleKeyboard(chatId, channel, messageId);
26171
+ break;
26172
+ case "jobs":
26173
+ await sendJobsBoard(chatId, channel, 1, messageId);
26174
+ break;
26175
+ default: {
26176
+ const MENU_COMMANDS = {
26177
+ newchat: "newchat",
26178
+ status: "status",
26179
+ health: "health"
26180
+ };
26181
+ const cmd = MENU_COMMANDS[action];
26182
+ if (cmd) {
26183
+ const synth = { chatId, messageId: "", text: "", senderName: "User", type: "command", source: "telegram", command: cmd, commandArgs: "" };
26184
+ await handleCommand(synth, channel);
26185
+ }
26186
+ }
25587
26187
  }
25588
26188
  return;
25589
26189
  }
@@ -25592,32 +26192,79 @@ async function handleCallback(chatId, data, channel, messageId) {
25592
26192
  if (!getAllBackendIds().includes(chosen)) return;
25593
26193
  const previous = getBackend(chatId);
25594
26194
  if (chosen === previous) {
25595
- const adapter = getAdapter(chosen);
25596
- await channel.sendText(chatId, `Already using ${adapter.displayName}.`, { parseMode: "plain" });
26195
+ await sendBackendConfigPanel(chatId, chosen, channel, messageId, false);
25597
26196
  return;
25598
26197
  }
25599
- await sendBackendSwitchConfirmation(chatId, chosen, channel);
26198
+ await doBackendSwitch(chatId, chosen, channel, { messageId });
26199
+ } else if (data.startsWith("bconf:")) {
26200
+ const rest = data.slice(6);
26201
+ if (rest === "back") {
26202
+ await sendBackendPicker(chatId, channel, messageId);
26203
+ } else if (rest.startsWith("panel:")) {
26204
+ const backendId = rest.slice(6);
26205
+ await sendBackendConfigPanel(chatId, backendId, channel, messageId, false);
26206
+ } else if (rest.startsWith("model:")) {
26207
+ const backendId = rest.slice(6);
26208
+ await sendBackendModelPicker(chatId, backendId, channel, messageId);
26209
+ } else if (rest.startsWith("thinking:")) {
26210
+ const backendId = rest.slice(9);
26211
+ await sendBackendThinkingPicker(chatId, backendId, channel, messageId);
26212
+ } else if (rest.startsWith("account:")) {
26213
+ const backendId = rest.slice(8);
26214
+ await sendBackendAccountPicker(chatId, backendId, channel, messageId);
26215
+ } else if (rest.startsWith("setmodel:")) {
26216
+ const parts = rest.slice(9).split(":");
26217
+ const backendId = parts[0];
26218
+ const modelId = parts.slice(1).join(":");
26219
+ setModel(chatId, modelId);
26220
+ await sendBackendConfigPanel(chatId, backendId, channel, messageId, false);
26221
+ } else if (rest.startsWith("setthinking:")) {
26222
+ const parts = rest.slice(12).split(":");
26223
+ const backendId = parts[0];
26224
+ const level = parts[1];
26225
+ setThinkingLevel(chatId, level);
26226
+ await sendBackendConfigPanel(chatId, backendId, channel, messageId, false);
26227
+ } else if (rest.startsWith("setaccount:")) {
26228
+ const parts = rest.slice(11).split(":");
26229
+ const backendId = parts[0];
26230
+ const slotIdStr = parts[1];
26231
+ if (slotIdStr === "auto") {
26232
+ if (backendId === "gemini") {
26233
+ clearChatGeminiSlot(chatId);
26234
+ } else {
26235
+ clearChatBackendSlot(chatId, backendId);
26236
+ }
26237
+ } else {
26238
+ const slotId = parseInt(slotIdStr, 10);
26239
+ if (backendId === "gemini") {
26240
+ pinChatGeminiSlot(chatId, slotId);
26241
+ } else {
26242
+ pinChatBackendSlot(chatId, backendId, slotId);
26243
+ }
26244
+ }
26245
+ await sendBackendConfigPanel(chatId, backendId, channel, messageId, false);
26246
+ }
25600
26247
  } else if (data.startsWith("backend_confirm_clear:")) {
25601
26248
  const target = data.slice("backend_confirm_clear:".length);
25602
26249
  if (!getAllBackendIds().includes(target)) return;
25603
26250
  clearMessageLog(chatId);
25604
- await doBackendSwitch(chatId, target, channel);
26251
+ await doBackendSwitch(chatId, target, channel, { skipContext: true, messageId });
25605
26252
  } else if (data.startsWith("backend_confirm:")) {
25606
26253
  const chosen = data.slice(16);
25607
26254
  if (!getAllBackendIds().includes(chosen)) return;
25608
- await doBackendSwitch(chatId, chosen, channel);
26255
+ await doBackendSwitch(chatId, chosen, channel, { messageId });
25609
26256
  } else if (data.startsWith("backend_cancel:") || data === "backend_cancel") {
25610
- await channel.sendText(chatId, "Switch cancelled.", { parseMode: "plain" });
26257
+ await sendBackendPicker(chatId, channel, messageId);
25611
26258
  } else if (data.startsWith("backend_dismiss:")) {
25612
26259
  const backendId = data.slice(16);
25613
- const adapter = getAdapter(backendId);
25614
- await channel.sendText(chatId, `Using ${adapter.displayName} with default model. Ready!`, { parseMode: "plain" });
26260
+ await sendBackendConfigPanel(chatId, backendId, channel, messageId, false);
25615
26261
  } else if (data.startsWith("model:")) {
25616
26262
  const chosen = data.slice(6);
25617
26263
  if (chosen === "auto") {
25618
26264
  setModel(chatId, "auto");
25619
26265
  clearThinkingLevel(chatId);
25620
- await channel.sendText(chatId, "\u{1F916} Smart routing enabled \u2014 model and thinking level will be chosen per message based on complexity.", { parseMode: "plain" });
26266
+ logActivity(getDb(), { chatId, source: "telegram", eventType: "config_changed", summary: "Smart routing enabled", detail: { field: "model", value: "auto" } });
26267
+ await sendModelKeyboard(chatId, channel, messageId);
25621
26268
  return;
25622
26269
  }
25623
26270
  let adapter;
@@ -25633,45 +26280,19 @@ async function handleCallback(chatId, data, channel, messageId) {
25633
26280
  clearThinkingLevel(chatId);
25634
26281
  logActivity(getDb(), { chatId, source: "telegram", eventType: "config_changed", summary: `Model switched to ${modelInfo.label}`, detail: { field: "model", value: chosen } });
25635
26282
  if (modelInfo.thinking === "adjustable" && modelInfo.thinkingLevels) {
25636
- const thinkingButtons = modelInfo.thinkingLevels.map((level) => [{
25637
- label: level === "auto" ? "Auto (default)" : level.replace("_", " ").replace(/\b\w/g, (c) => c.toUpperCase()),
25638
- data: `thinking:${level}`
25639
- }]);
25640
- if (typeof channel.sendKeyboard === "function") {
25641
- await channel.sendKeyboard(
25642
- chatId,
25643
- `Model set to ${modelInfo.label}. Session continues.
25644
-
25645
- Select thinking/effort level:`,
25646
- thinkingButtons
25647
- );
25648
- } else {
25649
- await channel.sendText(chatId, `Model set to ${modelInfo.label}. Session continues.`, { parseMode: "plain" });
25650
- }
26283
+ await sendThinkingKeyboard(chatId, channel, messageId, chosen);
25651
26284
  } else {
25652
- await channel.sendText(chatId, `Model switched to ${modelInfo.label}. Session continues.`, { parseMode: "plain" });
26285
+ await sendModelKeyboard(chatId, channel, messageId);
25653
26286
  }
25654
26287
  } else if (data.startsWith("thinking:")) {
25655
26288
  const level = data.slice(9);
25656
26289
  setThinkingLevel(chatId, level);
25657
26290
  logActivity(getDb(), { chatId, source: "telegram", eventType: "config_changed", summary: `Thinking level set to ${level}`, detail: { field: "thinking", value: level } });
25658
- const label2 = level === "auto" ? "Auto" : level.replace("_", " ").replace(/\b\w/g, (c) => c.toUpperCase());
25659
- await channel.sendText(chatId, `Thinking level set to: ${label2}`, { parseMode: "plain" });
26291
+ await sendThinkingKeyboard(chatId, channel, messageId);
25660
26292
  } else if (data === "thinking_show_ui:toggle") {
25661
26293
  const newState = toggleShowThinkingUi(chatId);
25662
26294
  logActivity(getDb(), { chatId, source: "telegram", eventType: "config_changed", summary: `Show thinking UI ${newState ? "enabled" : "disabled"}`, detail: { field: "show_thinking_ui", value: String(newState) } });
25663
- let msg;
25664
- if (newState) {
25665
- let backendId;
25666
- try {
25667
- backendId = getAdapterForChat(chatId).id;
25668
- } catch {
25669
- }
25670
- msg = backendId && backendId !== "claude" ? `\u{1F4AD} Thinking tokens enabled \u2014 but ${capitalize(backendId)} CLI doesn't stream them.` : "\u{1F4AD} Thinking tokens will now stream live in the status message.";
25671
- } else {
25672
- msg = "\u{1F4AD} Thinking tokens hidden. Toggle on again via /thinking.";
25673
- }
25674
- await channel.sendText(chatId, msg, { parseMode: "plain" });
26295
+ await sendThinkingKeyboard(chatId, channel, messageId);
25675
26296
  } else if (data.startsWith("debug_log:")) {
25676
26297
  const value = data.slice(10) === "on";
25677
26298
  const { setSessionLogEnabled: setSessionLogEnabled2 } = await Promise.resolve().then(() => (init_chat_settings(), chat_settings_exports));
@@ -25698,19 +26319,9 @@ ${value ? "Full tool inputs/results will be saved to ~/.cc-claw/logs/sessions/"
25698
26319
  let chosen = data.slice(6);
25699
26320
  if (chosen === "readonly") chosen = "plan";
25700
26321
  if (!PERM_MODES[chosen]) return;
25701
- const previous = getMode(chatId);
25702
- if (chosen === previous) {
25703
- await channel.sendText(chatId, `Already in ${chosen} mode.`, { parseMode: "plain" });
25704
- return;
25705
- }
25706
26322
  setMode(chatId, chosen);
25707
26323
  logActivity(getDb(), { chatId, source: "telegram", eventType: "config_changed", summary: `Permission mode set to ${chosen}`, detail: { field: "permissions", value: chosen } });
25708
- await channel.sendText(
25709
- chatId,
25710
- `Permission mode: ${chosen}
25711
- ${PERM_MODES[chosen]}`,
25712
- { parseMode: "plain" }
25713
- );
26324
+ await sendPermissionsKeyboard(chatId, channel, messageId);
25714
26325
  } else if (data.startsWith("perm:escalate:")) {
25715
26326
  const targetMode = data.slice(14);
25716
26327
  if (!PERM_MODES[targetMode]) return;
@@ -25730,33 +26341,20 @@ ${PERM_MODES[chosen]}`,
25730
26341
  const chosen = data.slice(8);
25731
26342
  if (!VERBOSE_LEVELS[chosen]) return;
25732
26343
  setVerboseLevel(chatId, chosen);
25733
- await channel.sendText(chatId, `Tool visibility: ${VERBOSE_LEVELS[chosen]}`, { parseMode: "plain" });
26344
+ await sendVerboseKeyboard(chatId, channel, messageId);
25734
26345
  } else if (data.startsWith("tool:toggle:")) {
25735
26346
  const toolName = data.slice(12);
25736
26347
  if (!ALL_TOOLS.includes(toolName)) return;
25737
- const newState = toggleTool(chatId, toolName);
25738
- await channel.sendText(
25739
- chatId,
25740
- `${newState ? "\u2705" : "\u274C"} ${toolName} ${newState ? "enabled" : "disabled"}`,
25741
- { parseMode: "plain" }
25742
- );
26348
+ toggleTool(chatId, toolName);
26349
+ await sendToolsKeyboard(chatId, channel, messageId);
25743
26350
  } else if (data === "tool:reset") {
25744
26351
  resetTools(chatId);
25745
- await channel.sendText(chatId, "Tool configuration reset \u2014 all tools enabled.", { parseMode: "plain" });
26352
+ await sendToolsKeyboard(chatId, channel, messageId);
25746
26353
  } else if (data.startsWith("style:")) {
25747
26354
  const selectedStyle = data.split(":")[1];
25748
26355
  if (selectedStyle === "concise" || selectedStyle === "normal" || selectedStyle === "detailed") {
25749
26356
  setResponseStyle(chatId, selectedStyle);
25750
- if (typeof channel.sendKeyboard === "function") {
25751
- await channel.sendKeyboard(chatId, "\u{1F5E3}\uFE0F AI Response Style:", [
25752
- [
25753
- { label: `${selectedStyle === "concise" ? "\u2713 " : ""}Concise`, data: "style:concise", ...selectedStyle === "concise" ? { style: "primary" } : {} },
25754
- { label: `${selectedStyle === "normal" ? "\u2713 " : ""}Normal`, data: "style:normal", ...selectedStyle === "normal" ? { style: "primary" } : {} },
25755
- { label: `${selectedStyle === "detailed" ? "\u2713 " : ""}Detailed`, data: "style:detailed", ...selectedStyle === "detailed" ? { style: "primary" } : {} }
25756
- ]
25757
- ]);
25758
- }
25759
- await channel.sendText(chatId, `Response style set to: ${selectedStyle}`, { parseMode: "plain" });
26357
+ await sendResponseStyleKeyboard(chatId, channel, messageId);
25760
26358
  }
25761
26359
  } else if (data.startsWith("agentmode:")) {
25762
26360
  const mode = data.split(":")[1];
@@ -25825,25 +26423,20 @@ ${plan.originalMessage}`;
25825
26423
  const mode = data.split(":")[1];
25826
26424
  if (mode === "approved" || mode === "yolo") {
25827
26425
  setExecMode(chatId, mode);
25828
- const msg = mode === "approved" ? "\u{1F512} Approve Before Execute: <b>ON</b>\nAI will show a plan for your approval before acting." : "\u26A1 Approve Before Execute: <b>OFF</b>\nAI will execute immediately (YOLO mode).";
25829
- await channel.sendText(chatId, msg, { parseMode: "html" });
26426
+ await sendPermissionsKeyboard(chatId, channel, messageId);
25830
26427
  }
25831
26428
  return;
25832
26429
  } else if (data.startsWith("model_sig:")) {
25833
26430
  const value = data.slice(10);
25834
26431
  setModelSignature(chatId, value);
25835
- await channel.sendText(
25836
- chatId,
25837
- value === "on" ? "\u{1F9E0} Model signature enabled. Each response will show the active model and thinking level." : "Model signature disabled.",
25838
- { parseMode: "plain" }
25839
- );
26432
+ await sendModelSigKeyboard(chatId, channel, messageId);
25840
26433
  } else if (data.startsWith("voice:")) {
25841
26434
  const action = data.slice(6);
25842
26435
  if (action === "on" || action === "off") {
25843
26436
  const current = isVoiceEnabled(chatId);
25844
26437
  const desired = action === "on";
25845
26438
  if (current !== desired) toggleVoice(chatId);
25846
- await channel.sendText(chatId, desired ? "\u{1F50A} Voice responses enabled." : "\u{1F507} Voice responses disabled.", { parseMode: "plain" });
26439
+ await sendVoiceConfigKeyboard(chatId, channel, messageId);
25847
26440
  }
25848
26441
  } else if (data.startsWith("vcfg:stt-model:")) {
25849
26442
  const model2 = data.slice(15);
@@ -25855,64 +26448,81 @@ ${plan.originalMessage}`;
25855
26448
  setSttModel(chatId, model2);
25856
26449
  const info = LOCAL_WHISPER_MODELS[model2];
25857
26450
  const downloaded = isWhisperModelDownloaded(model2);
25858
- const notice = downloaded ? `\u2705 Transcription model set to ${model2} (${info.size}, ${info.lang}). Already downloaded.` : `\u2705 Transcription model set to ${model2} (${info.size}, ${info.lang}).
25859
-
25860
- \u2B07\uFE0F Model will be downloaded on your first voice message (~${info.size}). This is a one-time download.`;
25861
- await channel.sendText(chatId, notice, { parseMode: "plain" });
25862
- await sendVoiceConfigKeyboard(chatId, channel);
25863
- } else if (data.startsWith("vcfg:stt:")) {
25864
- const provider = data.slice(9);
25865
- if (provider === "local-whisper") {
25866
- if (!isWhisperCliAvailable()) {
25867
- await channel.sendText(
26451
+ if (downloaded) {
26452
+ await sendVoiceConfigKeyboard(chatId, channel, messageId);
26453
+ } else {
26454
+ if (messageId && channel.editKeyboard) {
26455
+ await channel.editKeyboard(
25868
26456
  chatId,
25869
- "\u26A0\uFE0F Local Whisper requires `whisper-cli` to be installed.\n\nInstall it:\n```\nbrew install whisper-cpp\n```\n(macOS/Linux with Homebrew)\n\nOr download from: https://github.com/ggml-org/whisper.cpp/releases\n\nOnce installed, select Local Whisper here and pick a model.",
25870
- { parseMode: "markdown" }
26457
+ messageId,
26458
+ `\u{1F399}\uFE0F Voice Settings
26459
+
26460
+ \u2B07\uFE0F Downloading ${model2} (${info.size})\u2026
26461
+ This may take a moment.`,
26462
+ []
25871
26463
  );
25872
- return;
26464
+ } else {
26465
+ await channel.sendText(chatId, `\u2B07\uFE0F Downloading ${model2} (${info.size})...`, { parseMode: "plain" });
26466
+ }
26467
+ try {
26468
+ await downloadWhisperModel(model2, async (progressMsg) => {
26469
+ if (messageId && channel.editKeyboard) {
26470
+ await channel.editKeyboard(
26471
+ chatId,
26472
+ messageId,
26473
+ `\u{1F399}\uFE0F Voice Settings
26474
+
26475
+ ${progressMsg}`,
26476
+ []
26477
+ );
26478
+ }
26479
+ });
26480
+ await sendVoiceConfigKeyboard(chatId, channel, messageId);
26481
+ } catch (err) {
26482
+ const errMsg = err instanceof Error ? err.message : String(err);
26483
+ const failMsg = `\u{1F399}\uFE0F Voice Settings
26484
+
26485
+ \u274C Download failed: ${errMsg}
26486
+
26487
+ Tap the model again to retry.`;
26488
+ if (messageId && channel.editKeyboard) {
26489
+ await sendVoiceConfigKeyboard(chatId, channel, messageId);
26490
+ } else {
26491
+ await channel.sendText(chatId, failMsg, { parseMode: "plain" });
26492
+ }
25873
26493
  }
25874
26494
  }
25875
- if (provider === "groq" && !process.env.GROQ_API_KEY) {
25876
- await channel.sendText(
26495
+ } else if (data.startsWith("vcfg:stt:")) {
26496
+ const provider = data.slice(9);
26497
+ if (provider === "local-whisper" && !isFfmpegAvailable()) {
26498
+ await sendOrEditKeyboard(
25877
26499
  chatId,
25878
- "\u26A0\uFE0F Groq requires `GROQ_API_KEY` to be set.\n\nGet a free key at https://console.groq.com/keys, then add it:\n```\necho 'GROQ_API_KEY=your-key' >> ~/.cc-claw/.env\ncc-claw service restart\n```",
25879
- { parseMode: "markdown" }
26500
+ channel,
26501
+ messageId,
26502
+ "\u{1F399}\uFE0F Voice Settings\n\n\u26A0\uFE0F Local Whisper requires ffmpeg to convert audio.\n\nInstall: brew install ffmpeg\n\nThen tap Local Whisper again.",
26503
+ [[{ label: "\u2190 Back", data: "vcfg:stt:groq" }]]
25880
26504
  );
26505
+ return;
26506
+ }
26507
+ if (provider === "groq" && !process.env.GROQ_API_KEY) {
25881
26508
  }
25882
26509
  setSttProvider(chatId, provider);
25883
- const label2 = provider === "groq" ? "Groq (cloud)" : "Local Whisper";
25884
- await channel.sendText(chatId, `\u2705 Transcription provider set to: ${label2}`, { parseMode: "plain" });
25885
- await sendVoiceConfigKeyboard(chatId, channel);
26510
+ await sendVoiceConfigKeyboard(chatId, channel, messageId);
25886
26511
  } else if (data.startsWith("vcfg:")) {
25887
26512
  const parts = data.slice(5).split(":");
25888
26513
  const action = parts[0];
25889
26514
  if (action === "p") {
25890
26515
  const provider = parts[1];
25891
26516
  if (provider === "elevenlabs" || provider === "grok" || provider === "macos") {
25892
- if (provider === "grok" && !process.env.XAI_API_KEY) {
25893
- await channel.sendText(
25894
- chatId,
25895
- "\u26A0\uFE0F Grok requires `XAI_API_KEY` to be set.\n\nAdd it to your config:\n```\necho 'XAI_API_KEY=your-key-here' >> ~/.cc-claw/.env\ncc-claw service restart\n```\n\nGet a key at: https://console.x.ai/team/default/api-keys\n\nSetting Grok as your provider \u2014 it will activate once the key is added.",
25896
- { parseMode: "markdown" }
25897
- );
25898
- }
25899
- if (provider === "elevenlabs" && !process.env.ELEVENLABS_API_KEY) {
25900
- await channel.sendText(
25901
- chatId,
25902
- "\u26A0\uFE0F ElevenLabs requires `ELEVENLABS_API_KEY` to be set.\n\nAdd it to your config:\n```\necho 'ELEVENLABS_API_KEY=your-key-here' >> ~/.cc-claw/.env\ncc-claw service restart\n```\n\nGet a key at: https://elevenlabs.io/api\n\nSetting ElevenLabs as your provider \u2014 it will activate once the key is added.",
25903
- { parseMode: "markdown" }
25904
- );
25905
- }
25906
26517
  const defaultVoice = provider === "grok" ? "eve" : provider === "macos" ? "Samantha" : "21m00Tcm4TlvDq8ikWAM";
25907
26518
  setVoiceProvider(chatId, provider, defaultVoice);
25908
- await sendVoiceConfigKeyboard(chatId, channel);
26519
+ await sendVoiceConfigKeyboard(chatId, channel, messageId);
25909
26520
  }
25910
26521
  } else if (action === "v") {
25911
26522
  const voiceId = parts.slice(1).join(":");
25912
26523
  const config2 = getVoiceConfig(chatId);
25913
26524
  setVoiceProvider(chatId, config2.provider, voiceId);
25914
- const voiceName = config2.provider === "elevenlabs" ? ELEVENLABS_VOICES[voiceId]?.name ?? voiceId : config2.provider === "macos" ? MACOS_VOICES[voiceId]?.name ?? voiceId : voiceId;
25915
- await channel.sendText(chatId, `\u2705 Voice set to: ${voiceName}`, { parseMode: "plain" });
26525
+ await sendVoiceConfigKeyboard(chatId, channel, messageId);
25916
26526
  }
25917
26527
  } else if (data.startsWith("profile:")) {
25918
26528
  await handleProfileCallback(chatId, data, channel);
@@ -25970,14 +26580,14 @@ ${plan.originalMessage}`;
25970
26580
  try {
25971
26581
  const { askAgent: askAgent3 } = await Promise.resolve().then(() => (init_agent(), agent_exports));
25972
26582
  const { BACKEND: BACKEND2 } = await Promise.resolve().then(() => (init_types(), types_exports));
25973
- const { pinChatGeminiSlot: pinChatGeminiSlot4, pinChatBackendSlot: pinChatBackendSlot3 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
26583
+ const { pinChatGeminiSlot: pinChatGeminiSlot5, pinChatBackendSlot: pinChatBackendSlot4 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
25974
26584
  const testChatId = `cron:test:${id}:${Date.now()}`;
25975
26585
  const backend2 = testJob.backend ?? "claude";
25976
26586
  if (testJob.credentialSlotId) {
25977
26587
  if (backend2 === BACKEND2.GEMINI) {
25978
- pinChatGeminiSlot4(testChatId, testJob.credentialSlotId);
26588
+ pinChatGeminiSlot5(testChatId, testJob.credentialSlotId);
25979
26589
  } else {
25980
- pinChatBackendSlot3(testChatId, backend2, testJob.credentialSlotId);
26590
+ pinChatBackendSlot4(testChatId, backend2, testJob.credentialSlotId);
25981
26591
  }
25982
26592
  }
25983
26593
  const t0 = Date.now();
@@ -26305,74 +26915,73 @@ Result: ${task.result.slice(0, 500)}` : ""
26305
26915
  } else if (data.startsWith("grotation:")) {
26306
26916
  const mode = data.split(":")[1];
26307
26917
  await handleRotationModeChange(chatId, channel, "gemini", mode);
26918
+ const slots = getGeminiSlots();
26919
+ const rows = buildAccountSlotKeyboard(slots, getChatGeminiSlotId(chatId), getGeminiRotationMode(), "gslot:", "grotation:");
26920
+ await sendOrEditKeyboard(chatId, channel, messageId, "\u{1F511} Gemini Accounts & Rotation:", rows);
26308
26921
  return;
26309
26922
  } else if (data.startsWith("brotation:")) {
26310
26923
  const parts = data.split(":");
26311
26924
  const backend2 = parts[1];
26312
26925
  const mode = parts[2];
26313
26926
  await handleRotationModeChange(chatId, channel, backend2, mode);
26927
+ const { getBackendRotationMode: getBackendRotationMode2 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
26928
+ const slots = getBackendSlots(backend2);
26929
+ const rows = buildAccountSlotKeyboard(slots, getChatBackendSlotId(chatId, backend2), getBackendRotationMode2(backend2), `bslot:${backend2}:`, `brotation:${backend2}:`);
26930
+ const displayName = backend2 === BACKEND.CLAUDE ? "Claude" : "Codex";
26931
+ await sendOrEditKeyboard(chatId, channel, messageId, `\u{1F511} ${displayName} Accounts & Rotation:`, rows);
26314
26932
  return;
26315
26933
  } else if (data.startsWith("gslot:")) {
26316
26934
  const val = data.split(":")[1];
26317
26935
  if (val === "auto") {
26318
26936
  clearChatGeminiSlot(chatId);
26319
26937
  clearSession(chatId);
26320
- await channel.sendText(chatId, "Gemini slot set to <b>\u{1F504} auto rotation</b>.", { parseMode: "html" });
26321
26938
  } else {
26322
26939
  const slotId = parseInt(val, 10);
26323
- const slots = getGeminiSlots();
26324
- const slot = slots.find((s) => s.id === slotId);
26940
+ const slot = getGeminiSlots().find((s) => s.id === slotId);
26325
26941
  if (slot) {
26326
26942
  pinChatGeminiSlot(chatId, slotId);
26327
26943
  clearSession(chatId);
26328
- const label2 = slot.label || `slot-${slot.id}`;
26329
- const icon = slot.slotType === "oauth" ? "\u{1F468}\u{1F3FD}\u200D\u{1F4BB}" : "\u{1F511}";
26330
- const rotationMode = getGeminiRotationMode();
26331
- const rotationLabels = { off: "off", all: "all slots", accounts: "accounts only", keys: "keys only" };
26332
- const rotationNote = rotationMode === "off" ? "Rotation is off \u2014 only this account will be used." : `Rotation: ${rotationLabels[rotationMode]} (this is the starting slot).`;
26333
- await channel.sendText(chatId, `${icon} Account set to <b>${label2}</b>
26334
- ${rotationNote}`, { parseMode: "html" });
26335
26944
  }
26336
26945
  }
26946
+ const slots = getGeminiSlots();
26947
+ const rows = buildAccountSlotKeyboard(slots, getChatGeminiSlotId(chatId), getGeminiRotationMode(), "gslot:", "grotation:");
26948
+ await sendOrEditKeyboard(chatId, channel, messageId, "\u{1F511} Gemini Accounts & Rotation:", rows);
26337
26949
  return;
26338
26950
  } else if (data === "gopen:accounts") {
26339
26951
  const slots = getGeminiSlots();
26340
26952
  if (slots.length === 0) {
26341
- await channel.sendText(chatId, "No Gemini credentials configured.\nAdd with: <code>cc-claw gemini add-key</code> or <code>cc-claw gemini add-account</code>", { parseMode: "html" });
26342
- return;
26343
- }
26344
- if (typeof channel.sendKeyboard === "function") {
26345
- const rows = buildAccountSlotKeyboard(
26346
- slots,
26347
- getChatGeminiSlotId(chatId),
26348
- getGeminiRotationMode(),
26349
- "gslot:",
26350
- "grotation:"
26953
+ await sendOrEditKeyboard(
26954
+ chatId,
26955
+ channel,
26956
+ messageId,
26957
+ "No Gemini credentials configured.\nAdd with: cc-claw gemini add-key or cc-claw gemini add-account",
26958
+ [[{ label: "\u2190 Back", data: "menu:backend" }]]
26351
26959
  );
26352
- await channel.sendKeyboard(chatId, "Gemini Accounts & Rotation:", rows);
26960
+ return;
26353
26961
  }
26962
+ const rows = buildAccountSlotKeyboard(slots, getChatGeminiSlotId(chatId), getGeminiRotationMode(), "gslot:", "grotation:");
26963
+ await sendOrEditKeyboard(chatId, channel, messageId, "\u{1F511} Gemini Accounts & Rotation:", rows);
26354
26964
  return;
26355
26965
  } else if (data.startsWith("bslot:")) {
26356
26966
  const parts = data.split(":");
26357
26967
  const backend2 = parts[1];
26358
26968
  const val = parts[2];
26359
- const displayName = backend2 === BACKEND.CLAUDE ? "Claude" : "Codex";
26360
26969
  if (val === "auto") {
26361
26970
  clearChatBackendSlot(chatId, backend2);
26362
26971
  clearSession(chatId);
26363
- await channel.sendText(chatId, `${displayName} slot set to <b>\u{1F504} auto rotation</b>.`, { parseMode: "html" });
26364
26972
  } else {
26365
26973
  const slotId = parseInt(val, 10);
26366
- const slots = getBackendSlots(backend2);
26367
- const slot = slots.find((s) => s.id === slotId);
26974
+ const slot = getBackendSlots(backend2).find((s) => s.id === slotId);
26368
26975
  if (slot) {
26369
26976
  pinChatBackendSlot(chatId, backend2, slotId);
26370
26977
  clearSession(chatId);
26371
- const label2 = slot.label || `slot-${slot.id}`;
26372
- const icon = slot.slotType === "oauth" ? "\u{1F468}\u{1F3FD}\u200D\u{1F4BB}" : "\u{1F511}";
26373
- await channel.sendText(chatId, `${icon} ${displayName} account set to <b>${label2}</b>`, { parseMode: "html" });
26374
26978
  }
26375
26979
  }
26980
+ const { getBackendRotationMode: getBackendRotationMode2 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
26981
+ const slots = getBackendSlots(backend2);
26982
+ const rows = buildAccountSlotKeyboard(slots, getChatBackendSlotId(chatId, backend2), getBackendRotationMode2(backend2), `bslot:${backend2}:`, `brotation:${backend2}:`);
26983
+ const displayName = backend2 === BACKEND.CLAUDE ? "Claude" : "Codex";
26984
+ await sendOrEditKeyboard(chatId, channel, messageId, `\u{1F511} ${displayName} Accounts & Rotation:`, rows);
26376
26985
  return;
26377
26986
  } else if (data.startsWith("paidslot:")) {
26378
26987
  const parts = data.split(":");
@@ -26517,10 +27126,10 @@ ${rotationNote}`, { parseMode: "html" });
26517
27126
  await channel.sendText(chatId, "Fallback expired. Use /backend to switch manually.", { parseMode: "plain" });
26518
27127
  }
26519
27128
  } else if (data.startsWith("evolve:")) {
26520
- await handleEvolveCallback(chatId, data, channel);
27129
+ await handleEvolveCallback(chatId, data, channel, messageId);
26521
27130
  return;
26522
27131
  } else if (data.startsWith("reflect:")) {
26523
- await handleReflectCallback(chatId, data, channel);
27132
+ await handleReflectCallback(chatId, data, channel, messageId);
26524
27133
  return;
26525
27134
  } else if (data.startsWith("council:")) {
26526
27135
  const parts = data.split(":");
@@ -26528,45 +27137,54 @@ ${rotationNote}`, { parseMode: "html" });
26528
27137
  if (action === "toggle") {
26529
27138
  const backend2 = parts[2];
26530
27139
  const model2 = parts[3];
26531
- const { getAdapter: getAdapter4 } = await Promise.resolve().then(() => (init_backends(), backends_exports));
26532
- const toggleAdapter = getAdapter4(backend2);
27140
+ const toggleAdapter = getAdapter(backend2);
26533
27141
  const label2 = toggleAdapter.availableModels[model2]?.label ?? model2;
26534
27142
  const { toggleParticipant: toggleParticipant2, buildSelectKeyboard: buildSelectKeyboard2, hasPendingCouncil: hasPendingCouncil2 } = await Promise.resolve().then(() => (init_wizard2(), wizard_exports2));
26535
27143
  if (!hasPendingCouncil2(chatId)) {
26536
- await channel.sendText(chatId, "No council wizard active. Use /council to start.", { parseMode: "plain" });
27144
+ await sendOrEditKeyboard(
27145
+ chatId,
27146
+ channel,
27147
+ messageId,
27148
+ "No council wizard active. Use /council to start.",
27149
+ []
27150
+ );
26537
27151
  return;
26538
27152
  }
26539
27153
  toggleParticipant2(chatId, backend2, model2, label2);
26540
- if (typeof channel.sendKeyboard === "function") {
26541
- const { text, buttons } = buildSelectKeyboard2(chatId);
26542
- await channel.sendKeyboard(chatId, text, buttons);
26543
- }
27154
+ const { text, buttons } = buildSelectKeyboard2(chatId);
27155
+ await sendOrEditKeyboard(chatId, channel, messageId, text, buttons);
26544
27156
  return;
26545
27157
  }
26546
27158
  if (action === "start") {
26547
27159
  const { getCouncilState: getCouncilState2 } = await Promise.resolve().then(() => (init_wizard2(), wizard_exports2));
26548
27160
  const state = getCouncilState2(chatId);
26549
27161
  if (!state || state.selected.size < 2) {
26550
- await channel.sendText(chatId, "Select at least 2 models first.", { parseMode: "plain" });
27162
+ await sendOrEditKeyboard(chatId, channel, messageId, "Select at least 2 models first.", []);
26551
27163
  return;
26552
27164
  }
26553
27165
  state.step = "question";
26554
27166
  const names = [...state.selected.values()].map((p) => p.label).join(", ");
26555
- await channel.sendText(chatId, `\u{1F3DB}\uFE0F Council members: ${names}
27167
+ await sendOrEditKeyboard(
27168
+ chatId,
27169
+ channel,
27170
+ messageId,
27171
+ `\u{1F3DB}\uFE0F Council members: ${names}
26556
27172
 
26557
- Now type the question you want them to debate.`, { parseMode: "plain" });
27173
+ Now type the question you want them to debate.`,
27174
+ [[{ label: "\u2715 Cancel", data: "council:cancel" }]]
27175
+ );
26558
27176
  return;
26559
27177
  }
26560
27178
  if (action === "cancel") {
26561
27179
  const { cancelCouncil: cancelCouncil2 } = await Promise.resolve().then(() => (init_wizard2(), wizard_exports2));
26562
27180
  cancelCouncil2(chatId);
26563
- await channel.sendText(chatId, "Council cancelled.", { parseMode: "plain" });
27181
+ await sendOrEditKeyboard(chatId, channel, messageId, "Council cancelled.", []);
26564
27182
  return;
26565
27183
  }
26566
27184
  if (action === "noop") return;
26567
27185
  return;
26568
27186
  } else if (data.startsWith("opt:")) {
26569
- await handleOptimizeCallback(chatId, data, channel);
27187
+ await handleOptimizeCallback(chatId, data, channel, messageId);
26570
27188
  return;
26571
27189
  } else if (data.startsWith("ollama:")) {
26572
27190
  const { handleOllamaCallback: handleOllamaCallback2 } = await Promise.resolve().then(() => (init_ollama3(), ollama_exports2));
@@ -26648,14 +27266,14 @@ Now type the question you want them to debate.`, { parseMode: "plain" });
26648
27266
  const rest = data.slice(5);
26649
27267
  if (rest.startsWith("recent:")) {
26650
27268
  const limit = parseInt(rest.slice(7), 10) || 25;
26651
- await sendHistoryView(chatId, channel, { limit });
27269
+ await sendHistoryView(chatId, channel, { limit }, messageId);
26652
27270
  } else if (rest === "today") {
26653
- await sendHistoryView(chatId, channel, { period: "today" });
27271
+ await sendHistoryView(chatId, channel, { period: "today" }, messageId);
26654
27272
  } else if (rest === "week") {
26655
- await sendHistoryView(chatId, channel, { period: "week" });
27273
+ await sendHistoryView(chatId, channel, { period: "week" }, messageId);
26656
27274
  } else if (rest.startsWith("backend:")) {
26657
27275
  const backend2 = rest.slice(8);
26658
- await sendHistoryView(chatId, channel, { backend: backend2 });
27276
+ await sendHistoryView(chatId, channel, { backend: backend2 }, messageId);
26659
27277
  }
26660
27278
  return;
26661
27279
  } else if (data.startsWith("forget:")) {
@@ -26667,36 +27285,37 @@ Now type the question you want them to debate.`, { parseMode: "plain" });
26667
27285
  const id = parseInt(rest.slice(5), 10);
26668
27286
  const memory2 = getMemoryById(id);
26669
27287
  if (!memory2) {
26670
- if (typeof channel.sendKeyboard === "function") {
26671
- await channel.sendKeyboard(chatId, "This memory no longer exists.", [
26672
- [{ label: "Refresh List", data: "forget:page:1" }]
26673
- ]);
26674
- } else {
26675
- await channel.sendText(chatId, "This memory no longer exists.", { parseMode: "plain" });
26676
- }
27288
+ await sendOrEditKeyboard(chatId, channel, messageId, "This memory no longer exists.", [
27289
+ [{ label: "Refresh List", data: "forget:page:1" }]
27290
+ ]);
26677
27291
  return;
26678
27292
  }
26679
27293
  const detail = `Delete this memory?
27294
+
26680
27295
  "${memory2.trigger}: ${memory2.content}"
26681
27296
  Salience: ${memory2.salience.toFixed(2)} | Created: ${memory2.created_at.slice(0, 10)}`;
26682
- if (typeof channel.sendKeyboard === "function") {
26683
- await channel.sendKeyboard(chatId, detail, [
26684
- [
26685
- { label: "Confirm Delete", data: `forget:confirm:${id}`, style: "danger" },
26686
- { label: "Cancel", data: "forget:cancel" }
26687
- ]
26688
- ]);
26689
- }
27297
+ await sendOrEditKeyboard(chatId, channel, messageId, detail, [
27298
+ [
27299
+ { label: "\u{1F5D1} Confirm Delete", data: `forget:confirm:${id}`, style: "danger" },
27300
+ { label: "\u2190 Back", data: "forget:page:1" }
27301
+ ]
27302
+ ]);
26690
27303
  } else if (rest.startsWith("confirm:")) {
26691
27304
  const id = parseInt(rest.slice(8), 10);
26692
27305
  const deleted = deleteMemoryById(id);
26693
- await channel.sendText(
26694
- chatId,
26695
- deleted ? "Memory deleted." : "Memory not found \u2014 already deleted.",
26696
- { parseMode: "plain" }
26697
- );
27306
+ const msg = deleted ? "\u2705 Memory deleted." : "Memory not found \u2014 already deleted.";
27307
+ await sendOrEditKeyboard(chatId, channel, messageId, msg, [
27308
+ [{ label: "\u2190 Back to List", data: "forget:page:1" }]
27309
+ ]);
27310
+ } else if (rest.startsWith("keyword:")) {
27311
+ const keyword = rest.slice(8);
27312
+ const count = (await Promise.resolve().then(() => (init_store5(), store_exports5))).forgetMemory(keyword);
27313
+ const msg = count > 0 ? `\u2705 Deleted ${count} memory(ies) matching "${keyword}".` : `No memories found matching "${keyword}".`;
27314
+ await sendOrEditKeyboard(chatId, channel, messageId, msg, [
27315
+ [{ label: "\u2190 Back to List", data: "forget:page:1" }]
27316
+ ]);
26698
27317
  } else if (rest === "cancel") {
26699
- await channel.sendText(chatId, "Cancelled.", { parseMode: "plain" });
27318
+ await sendForgetPicker(chatId, channel, 1, messageId);
26700
27319
  }
26701
27320
  return;
26702
27321
  } else if (data.startsWith("mem:")) {
@@ -26706,60 +27325,17 @@ Salience: ${memory2.salience.toFixed(2)} | Created: ${memory2.created_at.slice(0
26706
27325
  await sendMemoryPage(chatId, channel, page, messageId);
26707
27326
  } else if (rest.startsWith("view:")) {
26708
27327
  const id = parseInt(rest.slice(5), 10);
26709
- const memory2 = getMemoryById(id);
26710
- if (!memory2) {
26711
- if (typeof channel.sendKeyboard === "function") {
26712
- await channel.sendKeyboard(chatId, "This memory no longer exists.", [
26713
- [{ label: "Refresh List", data: "mem:page:1" }]
26714
- ]);
26715
- } else {
26716
- await channel.sendText(chatId, "This memory no longer exists.", { parseMode: "plain" });
26717
- }
26718
- return;
26719
- }
26720
- const detail = [
26721
- buildSectionHeader(`Memory #${memory2.id}`),
26722
- `Trigger: ${memory2.trigger}`,
26723
- `Content: ${memory2.content}`,
26724
- `Salience: ${memory2.salience.toFixed(2)} | Created: ${memory2.created_at.slice(0, 10)}`
26725
- ].join("\n");
26726
- if (typeof channel.sendKeyboard === "function") {
26727
- await channel.sendKeyboard(chatId, detail, [
26728
- [
26729
- { label: "Edit", data: `mem:edit:${id}`, style: "primary" },
26730
- { label: "Forget", data: `mem:forget:${id}`, style: "danger" }
26731
- ],
26732
- [{ label: "Back to List", data: "mem:back" }]
26733
- ]);
26734
- }
27328
+ await sendMemoryDetail(chatId, id, channel, messageId);
26735
27329
  } else if (rest.startsWith("forget:confirm:")) {
26736
27330
  const id = parseInt(rest.slice(15), 10);
26737
27331
  const deleted = deleteMemoryById(id);
26738
- await channel.sendText(
26739
- chatId,
26740
- deleted ? "Memory deleted." : "Memory not found \u2014 already deleted.",
26741
- { parseMode: "plain" }
26742
- );
27332
+ const msg = deleted ? "\u2705 Memory deleted." : "Memory not found \u2014 already deleted.";
27333
+ await sendOrEditKeyboard(chatId, channel, messageId, msg, [
27334
+ [{ label: "\u2190 Back to List", data: "mem:back" }]
27335
+ ]);
26743
27336
  } else if (rest.startsWith("forget:")) {
26744
27337
  const id = parseInt(rest.slice(7), 10);
26745
- const memory2 = getMemoryById(id);
26746
- if (!memory2) {
26747
- await channel.sendText(chatId, "Memory not found.", { parseMode: "plain" });
26748
- return;
26749
- }
26750
- if (typeof channel.sendKeyboard === "function") {
26751
- await channel.sendKeyboard(
26752
- chatId,
26753
- `Delete this memory?
26754
- "${memory2.trigger}: ${memory2.content}"`,
26755
- [
26756
- [
26757
- { label: "Confirm Delete", data: `mem:forget:confirm:${id}`, style: "danger" },
26758
- { label: "Cancel", data: `mem:view:${id}` }
26759
- ]
26760
- ]
26761
- );
26762
- }
27338
+ await sendMemoryForgetConfirm(chatId, id, channel, messageId);
26763
27339
  } else if (rest.startsWith("edit:")) {
26764
27340
  const id = parseInt(rest.slice(5), 10);
26765
27341
  await channel.sendText(chatId, `Type: /memory edit ${id} <new content>`, { parseMode: "plain" });
@@ -26772,6 +27348,7 @@ Salience: ${memory2.salience.toFixed(2)} | Created: ${memory2.created_at.slice(0
26772
27348
  );
26773
27349
  const buffer = Buffer.from(lines.join("\n"), "utf-8");
26774
27350
  await channel.sendFile(chatId, buffer, "memories.txt", "text/plain");
27351
+ } else if (rest === "noop") {
26775
27352
  }
26776
27353
  return;
26777
27354
  } else if (data.startsWith("hb:")) {
@@ -26782,54 +27359,102 @@ Salience: ${memory2.salience.toFixed(2)} | Created: ${memory2.created_at.slice(0
26782
27359
  await sendHeartbeatKeyboard(chatId, channel, messageId);
26783
27360
  } else if (rest === "off") {
26784
27361
  disableHeartbeat2();
26785
- await channel.sendText(chatId, "\u26A0\uFE0F Heartbeat paused. Re-enable with /heartbeat on.", { parseMode: "plain" });
26786
27362
  await sendHeartbeatKeyboard(chatId, channel, messageId);
26787
27363
  } else if (rest.startsWith("interval:")) {
26788
27364
  const min = parseInt(rest.slice(9), 10);
26789
27365
  if (isNaN(min) || min < 1) return;
26790
27366
  updateHeartbeatConfig2({ intervalMs: min * 6e4 });
26791
27367
  await sendHeartbeatKeyboard(chatId, channel, messageId);
26792
- } else if (rest.startsWith("backend:")) {
26793
- const bid = rest.slice(8);
26794
- const newBackend = bid === "default" ? null : bid;
26795
- updateHeartbeatConfig2({ backend: newBackend, model: null });
26796
- updateHeartbeatField(chatId, "backend", newBackend);
26797
- updateHeartbeatField(chatId, "model", null);
26798
- await sendHeartbeatKeyboard(chatId, channel, messageId);
26799
- } else if (rest.startsWith("model:")) {
26800
- const model2 = rest.slice(6);
26801
- const newModel = model2 === "default" ? null : model2;
26802
- updateHeartbeatConfig2({ model: newModel });
26803
- updateHeartbeatField(chatId, "model", newModel);
26804
- await sendHeartbeatKeyboard(chatId, channel, messageId);
26805
- } else if (rest === "thinking") {
26806
- await channel.sendText(chatId, "Thinking level for heartbeat: use /editjob to configure.", { parseMode: "plain" });
26807
27368
  } else if (rest === "run") {
26808
- await channel.sendText(chatId, "\u23F3 Running heartbeat check...", { parseMode: "plain" });
27369
+ if (messageId) {
27370
+ await sendOrEditKeyboard(chatId, channel, messageId, "\u23F3 Running heartbeat check...", []);
27371
+ }
26809
27372
  try {
26810
27373
  await triggerHb();
26811
- await channel.sendText(chatId, "\u2705 Heartbeat check complete.", { parseMode: "plain" });
27374
+ if (messageId) {
27375
+ await sendOrEditKeyboard(chatId, channel, messageId, "\u2705 Heartbeat check complete.", [
27376
+ [{ label: "\u2190 Back to Heartbeat", data: "hb:back" }]
27377
+ ]);
27378
+ } else {
27379
+ await channel.sendText(chatId, "\u2705 Heartbeat check complete.", { parseMode: "plain" });
27380
+ }
26812
27381
  } catch (err) {
26813
- await channel.sendText(chatId, `\u274C Heartbeat failed: ${err instanceof Error ? err.message : String(err)}`, { parseMode: "plain" });
27382
+ const errMsg = err instanceof Error ? err.message : String(err);
27383
+ if (messageId) {
27384
+ await sendOrEditKeyboard(chatId, channel, messageId, `\u274C Heartbeat failed: ${errMsg}`, [
27385
+ [{ label: "\u2190 Back to Heartbeat", data: "hb:back" }]
27386
+ ]);
27387
+ } else {
27388
+ await channel.sendText(chatId, `\u274C Heartbeat failed: ${errMsg}`, { parseMode: "plain" });
27389
+ }
26814
27390
  }
26815
- } else if (rest === "watches" || rest === "addwatch") {
26816
- await channel.sendText(chatId, "Use /watch to manage awareness tasks.\n\nExamples:\n /watch add Check my email for urgent messages\n /watch add Check if Ollama is running\n /watch list\n /watch remove 1", { parseMode: "plain" });
27391
+ } else if (rest === "back") {
27392
+ await sendHeartbeatKeyboard(chatId, channel, messageId);
27393
+ } else if (rest === "engine") {
27394
+ await sendHeartbeatEngine(chatId, channel, messageId);
27395
+ } else if (rest === "fallbacks") {
27396
+ await sendHeartbeatFallbacks(chatId, channel, messageId);
27397
+ } else if (rest === "hours") {
27398
+ await sendHeartbeatHours(chatId, channel, messageId);
27399
+ } else if (rest === "timeout") {
27400
+ await sendHeartbeatTimeout(chatId, channel, messageId);
27401
+ } else if (rest === "thinking") {
27402
+ await sendHeartbeatThinking(chatId, channel, messageId);
27403
+ } else if (rest === "watches") {
27404
+ await sendHeartbeatWatches(chatId, channel, messageId);
27405
+ } else if (rest.startsWith("backend:")) {
27406
+ const bid = rest.slice(8);
27407
+ let defaultModel = null;
27408
+ try {
27409
+ const adapter = getAdapter(bid);
27410
+ defaultModel = adapter.defaultModel;
27411
+ } catch {
27412
+ }
27413
+ updateHeartbeatConfig2({ backend: bid, model: defaultModel, credentialSlotId: null });
27414
+ await sendHeartbeatEngine(chatId, channel, messageId);
27415
+ } else if (rest.startsWith("model:")) {
27416
+ const model2 = rest.slice(6);
27417
+ updateHeartbeatConfig2({ model: model2 });
27418
+ await sendHeartbeatEngine(chatId, channel, messageId);
27419
+ } else if (rest.startsWith("slot:")) {
27420
+ const val = rest.slice(5);
27421
+ const slotId = val === "auto" ? null : parseInt(val, 10);
27422
+ updateHeartbeatConfig2({ credentialSlotId: slotId });
27423
+ await sendHeartbeatEngine(chatId, channel, messageId);
27424
+ } else if (rest.startsWith("hstart:")) {
27425
+ const hour = rest.slice(7);
27426
+ updateHeartbeatField(chatId, "active_start", hour);
27427
+ await sendHeartbeatHours(chatId, channel, messageId);
27428
+ } else if (rest.startsWith("hend:")) {
27429
+ const hour = rest.slice(5);
27430
+ updateHeartbeatField(chatId, "active_end", hour);
27431
+ await sendHeartbeatHours(chatId, channel, messageId);
27432
+ } else if (rest.startsWith("tout:")) {
27433
+ const seconds = parseInt(rest.slice(5), 10);
27434
+ if (!isNaN(seconds)) updateHeartbeatConfig2({ timeout: seconds });
27435
+ await sendHeartbeatTimeout(chatId, channel, messageId);
27436
+ } else if (rest.startsWith("think:")) {
27437
+ const level = rest.slice(6);
27438
+ updateHeartbeatConfig2({ thinking: level === "auto" ? null : level });
27439
+ await sendHeartbeatThinking(chatId, channel, messageId);
26817
27440
  } else if (rest.startsWith("fb:add:")) {
26818
27441
  const bid = rest.slice(7);
26819
27442
  try {
26820
27443
  const adapter = getAdapter(bid);
26821
27444
  const models = Object.entries(adapter.availableModels);
26822
27445
  const rows = [
26823
- [{ label: `Select model for ${capitalize(bid)} fallback:`, data: "hb:noop" }],
26824
- [{ label: "Default", data: `hb:fb:pick:${bid}:default`, style: "primary" }]
27446
+ [{ label: `Select model for ${capitalize(bid)} fallback:`, data: "hb:noop" }]
26825
27447
  ];
27448
+ rows.push([{ label: "Default", data: `hb:fb:pick:${bid}:default`, style: "primary" }]);
26826
27449
  if (models.length > 0) {
26827
- rows.push(models.slice(0, 4).map(([key]) => ({
26828
- label: shortModelName(key),
26829
- data: `hb:fb:pick:${bid}:${key}`
26830
- })));
27450
+ for (let i = 0; i < models.length; i += 3) {
27451
+ rows.push(models.slice(i, i + 3).map(([key]) => ({
27452
+ label: shortModelName(key),
27453
+ data: `hb:fb:pick:${bid}:${key}`
27454
+ })));
27455
+ }
26831
27456
  }
26832
- rows.push([{ label: "\u2190 Back", data: "hb:fb:back" }]);
27457
+ rows.push([{ label: "\u2190 Back", data: "hb:fallbacks" }]);
26833
27458
  await sendOrEditKeyboard(
26834
27459
  chatId,
26835
27460
  channel,
@@ -26838,30 +27463,34 @@ Salience: ${memory2.salience.toFixed(2)} | Created: ${memory2.created_at.slice(0
26838
27463
  rows
26839
27464
  );
26840
27465
  } catch {
26841
- const config2 = getHeartbeatConfig(chatId);
26842
- const current = parseHeartbeatFallbacks(config2?.fallbacks ?? null);
27466
+ const job = findHeartbeatJob2();
27467
+ const current = job?.fallbacks ?? [];
26843
27468
  if (!current.some((f) => f.backend === bid)) {
26844
- current.push({ backend: bid });
26845
- updateHeartbeatField(chatId, "fallbacks", JSON.stringify(current));
27469
+ let fbModel = "";
27470
+ try {
27471
+ fbModel = getAdapter(bid).defaultModel;
27472
+ } catch {
27473
+ }
27474
+ current.push({ backend: bid, model: fbModel });
27475
+ updateHeartbeatConfig2({ fallbacks: JSON.stringify(current) });
26846
27476
  }
26847
- await sendHeartbeatKeyboard(chatId, channel, messageId);
27477
+ await sendHeartbeatFallbacks(chatId, channel, messageId);
26848
27478
  }
26849
27479
  } else if (rest.startsWith("fb:pick:")) {
26850
27480
  const parts = rest.slice(8).split(":");
26851
27481
  const bid = parts[0];
26852
27482
  const model2 = parts.slice(1).join(":");
26853
- const config2 = getHeartbeatConfig(chatId);
26854
- const current = parseHeartbeatFallbacks(config2?.fallbacks ?? null);
27483
+ const job = findHeartbeatJob2();
27484
+ const current = [...job?.fallbacks ?? []];
26855
27485
  if (!current.some((f) => f.backend === bid)) {
26856
- current.push(model2 === "default" ? { backend: bid } : { backend: bid, model: model2 });
26857
- updateHeartbeatField(chatId, "fallbacks", JSON.stringify(current));
27486
+ const resolvedModel = model2 === "default" ? "" : model2;
27487
+ current.push({ backend: bid, model: resolvedModel });
27488
+ updateHeartbeatConfig2({ fallbacks: JSON.stringify(current) });
26858
27489
  }
26859
- await sendHeartbeatKeyboard(chatId, channel, messageId);
26860
- } else if (rest === "fb:back") {
26861
- await sendHeartbeatKeyboard(chatId, channel, messageId);
27490
+ await sendHeartbeatFallbacks(chatId, channel, messageId);
26862
27491
  } else if (rest === "fb:clear") {
26863
- updateHeartbeatField(chatId, "fallbacks", null);
26864
- await sendHeartbeatKeyboard(chatId, channel, messageId);
27492
+ updateHeartbeatConfig2({ fallbacks: null });
27493
+ await sendHeartbeatFallbacks(chatId, channel, messageId);
26865
27494
  } else if (rest === "noop") {
26866
27495
  }
26867
27496
  return;
@@ -26882,32 +27511,37 @@ Salience: ${memory2.salience.toFixed(2)} | Created: ${memory2.created_at.slice(0
26882
27511
  } else if (data.startsWith("help:")) {
26883
27512
  const rest = data.slice(5);
26884
27513
  if (rest === "back") {
26885
- await sendHelpCategories(chatId, channel);
27514
+ await sendHelpCategories(chatId, channel, messageId);
26886
27515
  } else {
26887
- await sendHelpCategory(chatId, rest, channel);
27516
+ await sendHelpCategory(chatId, rest, channel, messageId);
26888
27517
  }
26889
27518
  return;
26890
27519
  } else if (data.startsWith("usage:")) {
26891
27520
  const rest = data.slice(6);
26892
27521
  if (rest === "session" || rest === "24h" || rest === "7d" || rest === "30d" || rest === "model") {
26893
- await sendUnifiedUsage(chatId, channel, rest);
27522
+ await sendUnifiedUsage(chatId, channel, rest, messageId);
26894
27523
  } else if (rest === "limits") {
26895
- await sendUsageLimits(chatId, channel);
27524
+ await sendUsageLimits(chatId, channel, messageId);
26896
27525
  } else if (rest === "back") {
26897
- await sendUnifiedUsage(chatId, channel, "session");
27526
+ await sendUnifiedUsage(chatId, channel, "session", messageId);
26898
27527
  } else if (rest === "limits:clear") {
26899
27528
  for (const bid of getAllBackendIds()) {
26900
27529
  for (const win of ["hourly", "daily", "weekly"]) {
26901
27530
  clearBackendLimit(bid, win);
26902
27531
  }
26903
27532
  }
26904
- await channel.sendText(chatId, "All usage limits cleared.", { parseMode: "plain" });
26905
- await sendUnifiedUsage(chatId, channel, "session");
27533
+ await sendUsageLimits(chatId, channel, messageId);
26906
27534
  } else if (rest.startsWith("limits:set:")) {
26907
27535
  const bid = rest.slice(11);
26908
- await channel.sendText(chatId, `Set limit for ${bid}:
27536
+ await sendOrEditKeyboard(
27537
+ chatId,
27538
+ channel,
27539
+ messageId,
27540
+ `Set ${bid} limit via command:
26909
27541
  /limits ${bid} daily <tokens>
26910
- Example: /limits ${bid} daily 500000`, { parseMode: "plain" });
27542
+ Example: /limits ${bid} daily 500000`,
27543
+ [[{ label: "\u2190 Back to Limits", data: "usage:limits" }]]
27544
+ );
26911
27545
  }
26912
27546
  return;
26913
27547
  } else if (data === "skills:toggle-extract") {
@@ -27078,9 +27712,10 @@ Use /skills to see all available skills.`, { parseMode: "plain" });
27078
27712
  if (messageId) await replaceWithText("\u{1F44C} Noted.");
27079
27713
  return;
27080
27714
  } else if (data === "imp:menu") {
27081
- if (typeof channel.sendKeyboard !== "function") return;
27082
- await channel.sendKeyboard(
27715
+ await sendOrEditKeyboard(
27083
27716
  chatId,
27717
+ channel,
27718
+ messageId,
27084
27719
  "\u{1F4E5} Import Skill\n\nImport a skill from your coding CLIs into CC-Claw.",
27085
27720
  [
27086
27721
  [{ label: "\u{1F50D} From CLI Skills", data: "imp:cli", style: "primary" }],
@@ -27090,10 +27725,12 @@ Use /skills to see all available skills.`, { parseMode: "plain" });
27090
27725
  );
27091
27726
  return;
27092
27727
  } else if (data === "imp:search") {
27093
- await channel.sendText(
27728
+ await sendOrEditKeyboard(
27094
27729
  chatId,
27730
+ channel,
27731
+ messageId,
27095
27732
  "Skill search via Universal Skills Manager is coming in v2.\n\nFor now, use /skill-install <github-url> to install from GitHub.",
27096
- { parseMode: "plain" }
27733
+ [[{ label: "\u2190 Back", data: "imp:menu" }]]
27097
27734
  );
27098
27735
  return;
27099
27736
  } else if (data === "imp:cli" || data.startsWith("imp:cli:")) {
@@ -27103,18 +27740,16 @@ Use /skills to see all available skills.`, { parseMode: "plain" });
27103
27740
  (s) => !s.sources.includes("cc-claw") && s.status === "external"
27104
27741
  );
27105
27742
  if (importable.length === 0) {
27106
- if (typeof channel.sendKeyboard === "function") {
27107
- await channel.sendKeyboard(
27108
- chatId,
27109
- "No new skills found in your coding CLIs.\n\nSkills are scanned from:\n\u2022 ~/.claude/skills/\n\u2022 ~/.gemini/skills/\n\u2022 ~/.codex/skills/\n\u2022 ~/.cursor/skills/",
27110
- [[{ label: "\u2190 Back", data: "imp:menu" }]]
27111
- );
27112
- } else {
27113
- await channel.sendText(chatId, "No new skills found to import.", { parseMode: "plain" });
27114
- }
27743
+ await sendOrEditKeyboard(
27744
+ chatId,
27745
+ channel,
27746
+ messageId,
27747
+ "No new skills found in your coding CLIs.\n\nSkills are scanned from:\n\u2022 ~/.claude/skills/\n\u2022 ~/.gemini/skills/\n\u2022 ~/.codex/skills/\n\u2022 ~/.cursor/skills/",
27748
+ [[{ label: "\u2190 Back", data: "imp:menu" }]]
27749
+ );
27115
27750
  return;
27116
27751
  }
27117
- if (typeof channel.sendKeyboard !== "function") {
27752
+ if (typeof channel.sendKeyboard !== "function" && typeof channel.editKeyboard !== "function") {
27118
27753
  const lines = importable.map((s) => `\u2022 ${s.name} [${s.source}]`);
27119
27754
  await channel.sendText(chatId, `Found ${importable.length} importable skills:
27120
27755
  ${lines.join("\n")}`, { parseMode: "plain" });
@@ -27140,8 +27775,10 @@ ${lines.join("\n")}`, { parseMode: "plain" });
27140
27775
  buttons.push(navRow);
27141
27776
  }
27142
27777
  buttons.push([{ label: "\u2190 Back", data: "imp:menu" }]);
27143
- await channel.sendKeyboard(
27778
+ await sendOrEditKeyboard(
27144
27779
  chatId,
27780
+ channel,
27781
+ messageId,
27145
27782
  `Found ${importable.length} skill${importable.length !== 1 ? "s" : ""} in your coding CLIs:`,
27146
27783
  buttons
27147
27784
  );
@@ -27164,21 +27801,21 @@ ${lines.join("\n")}`, { parseMode: "plain" });
27164
27801
  const preview = raw.length > 1500 ? raw.slice(0, 1500) + "\n...(truncated)" : raw;
27165
27802
  const escaped = preview.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
27166
27803
  const collisionNote = existingCcClaw ? "\n\n\u26A0\uFE0F A skill with this name already exists in CC-Claw. Importing will overwrite it." : "";
27167
- if (typeof channel.sendKeyboard === "function") {
27168
- await channel.sendKeyboard(
27169
- chatId,
27170
- `\u{1F4CB} <b>${skillName}</b> [${source}]${collisionNote}
27804
+ await sendOrEditKeyboard(
27805
+ chatId,
27806
+ channel,
27807
+ messageId,
27808
+ `\u{1F4CB} <b>${skillName}</b> [${source}]${collisionNote}
27171
27809
 
27172
27810
  <pre>${escaped}</pre>`,
27811
+ [
27173
27812
  [
27174
- [
27175
- { label: "\u2705 Import & Approve", data: `imp:go:${source}:${skillName}`, style: "success" },
27176
- { label: "\u{1F527} Import & Optimize", data: `imp:opt:${source}:${skillName}` }
27177
- ],
27178
- [{ label: "\u2190 Back", data: "imp:cli" }]
27179
- ]
27180
- );
27181
- }
27813
+ { label: "\u2705 Import & Approve", data: `imp:go:${source}:${skillName}`, style: "success" },
27814
+ { label: "\u{1F527} Import & Optimize", data: `imp:opt:${source}:${skillName}` }
27815
+ ],
27816
+ [{ label: "\u2190 Back", data: "imp:cli" }]
27817
+ ]
27818
+ );
27182
27819
  return;
27183
27820
  } else if (data.startsWith("imp:go:") || data.startsWith("imp:opt:")) {
27184
27821
  const doApprove = data.startsWith("imp:go:");
@@ -27190,12 +27827,12 @@ ${lines.join("\n")}`, { parseMode: "plain" });
27190
27827
  const allSkills = await discoverAllSkills();
27191
27828
  const skill = allSkills.find((s) => s.name === skillName && s.source === source);
27192
27829
  if (!skill) {
27193
- await channel.sendText(chatId, `Skill "${skillName}" not found.`, { parseMode: "plain" });
27830
+ await sendOrEditKeyboard(chatId, channel, messageId, `Skill "${skillName}" not found.`, [[{ label: "\u2190 Back", data: "imp:cli" }]]);
27194
27831
  return;
27195
27832
  }
27196
27833
  try {
27197
27834
  const { readFile: readFileFs } = await import("fs/promises");
27198
- const { mkdir: mkdir7, writeFile: writeFileFs } = await import("fs/promises");
27835
+ const { mkdir: mkdir6, writeFile: writeFileFs } = await import("fs/promises");
27199
27836
  const { join: join41 } = await import("path");
27200
27837
  const { SKILLS_PATH: SKILLS_PATH2 } = await Promise.resolve().then(() => (init_paths(), paths_exports));
27201
27838
  const { updateFrontmatter: updateFrontmatter2, ensureThreeTierFrontmatter: ensureThreeTierFrontmatter2 } = await Promise.resolve().then(() => (init_frontmatter(), frontmatter_exports));
@@ -27205,30 +27842,32 @@ ${lines.join("\n")}`, { parseMode: "plain" });
27205
27842
  const targetStatus = doApprove ? "approved" : "imported";
27206
27843
  updated = updateFrontmatter2(updated, { status: targetStatus });
27207
27844
  const targetDir = join41(SKILLS_PATH2, skillName);
27208
- await mkdir7(targetDir, { recursive: true });
27845
+ await mkdir6(targetDir, { recursive: true });
27209
27846
  await writeFileFs(join41(targetDir, "SKILL.md"), updated, "utf-8");
27210
27847
  invalidateSkillCache2();
27211
27848
  if (doApprove) {
27212
- await channel.sendText(
27849
+ await sendOrEditKeyboard(
27213
27850
  chatId,
27851
+ channel,
27852
+ messageId,
27214
27853
  `\u2705 Skill "${skillName}" imported and approved.
27215
27854
 
27216
27855
  It's now available in /skills.`,
27217
- { parseMode: "plain" }
27856
+ [[{ label: "\u2190 Skills", data: "skills:page:1" }, { label: "Import Another", data: "imp:cli" }]]
27218
27857
  );
27219
27858
  } else {
27220
- await channel.sendText(
27859
+ await sendOrEditKeyboard(
27221
27860
  chatId,
27222
- `\u{1F4E5} Skill "${skillName}" imported with status: imported.
27223
-
27224
- Opening optimizer...`,
27225
- { parseMode: "plain" }
27861
+ channel,
27862
+ messageId,
27863
+ `\u{1F4E5} Skill "${skillName}" imported. Opening optimizer...`,
27864
+ []
27226
27865
  );
27227
27866
  const { handleOptimizeCallback: routeOptimize } = await Promise.resolve().then(() => (init_optimize(), optimize_exports));
27228
27867
  await routeOptimize(chatId, `opt:skill-pick:${skillName}`, channel);
27229
27868
  }
27230
27869
  } catch (e) {
27231
- await channel.sendText(chatId, `Import failed: ${e.message}`, { parseMode: "plain" });
27870
+ await sendOrEditKeyboard(chatId, channel, messageId, `Import failed: ${e.message}`, [[{ label: "\u2190 Back", data: "imp:cli" }]]);
27232
27871
  }
27233
27872
  return;
27234
27873
  } else if (data === "appr:list" || data.startsWith("appr:list:")) {
@@ -27238,18 +27877,16 @@ Opening optimizer...`,
27238
27877
  (s) => s.sources.includes("cc-claw") && s.status !== "approved"
27239
27878
  );
27240
27879
  if (unapproved.length === 0) {
27241
- if (typeof channel.sendKeyboard === "function") {
27242
- await channel.sendKeyboard(
27243
- chatId,
27244
- "All CC-Claw skills are approved.",
27245
- [[{ label: "\u2190 Back to Skills", data: "skills:page:1" }]]
27246
- );
27247
- } else {
27248
- await channel.sendText(chatId, "All CC-Claw skills are approved.", { parseMode: "plain" });
27249
- }
27880
+ await sendOrEditKeyboard(
27881
+ chatId,
27882
+ channel,
27883
+ messageId,
27884
+ "\u2705 All CC-Claw skills are approved.",
27885
+ [[{ label: "\u2190 Back to Skills", data: "skills:page:1" }]]
27886
+ );
27250
27887
  return;
27251
27888
  }
27252
- if (typeof channel.sendKeyboard !== "function") {
27889
+ if (typeof channel.sendKeyboard !== "function" && typeof channel.editKeyboard !== "function") {
27253
27890
  const lines = unapproved.map((s) => `\u2022 ${s.name} [${s.status}]`);
27254
27891
  await channel.sendText(chatId, `${unapproved.length} skills need approval:
27255
27892
  ${lines.join("\n")}`, { parseMode: "plain" });
@@ -27279,8 +27916,10 @@ ${lines.join("\n")}`, { parseMode: "plain" });
27279
27916
  { label: `\u2705 Approve All (${unapproved.length})`, data: "appr:all", style: "success" }
27280
27917
  ]);
27281
27918
  buttons.push([{ label: "\u2190 Back to Skills", data: "skills:page:1" }]);
27282
- await channel.sendKeyboard(
27919
+ await sendOrEditKeyboard(
27283
27920
  chatId,
27921
+ channel,
27922
+ messageId,
27284
27923
  `${unapproved.length} CC-Claw skill${unapproved.length !== 1 ? "s" : ""} need approval.
27285
27924
 
27286
27925
  Select one to review, or approve all:`,
@@ -27292,11 +27931,11 @@ Select one to review, or approve all:`,
27292
27931
  const allSkills = await discoverAllSkills();
27293
27932
  const skill = allSkills.find((s) => s.name === skillName && s.sources.includes("cc-claw"));
27294
27933
  if (!skill) {
27295
- await channel.sendText(chatId, `Skill "${skillName}" not found.`, { parseMode: "plain" });
27934
+ await sendOrEditKeyboard(chatId, channel, messageId, `Skill "${skillName}" not found.`, [[{ label: "\u2190 Back", data: "appr:list" }]]);
27296
27935
  return;
27297
27936
  }
27298
27937
  if (skill.status === "approved") {
27299
- await channel.sendText(chatId, `"${skillName}" is already approved.`, { parseMode: "plain" });
27938
+ await sendOrEditKeyboard(chatId, channel, messageId, `"${skillName}" is already approved.`, [[{ label: "\u2190 Back", data: "appr:list" }]]);
27300
27939
  return;
27301
27940
  }
27302
27941
  const { readFile: readFileFs } = await import("fs/promises");
@@ -27310,28 +27949,28 @@ Select one to review, or approve all:`,
27310
27949
  \u{1F4CB} Quality check:
27311
27950
  ${formatValidationReport2(validation)}` : "\n\n\u2705 Quality checks passed.";
27312
27951
  const typeLabel = skill.type === "specialist" ? "\u{1F3AF} Specialist" : "\u{1F527} Tool";
27313
- if (typeof channel.sendKeyboard === "function") {
27314
- await channel.sendKeyboard(
27315
- chatId,
27316
- `${typeLabel} <b>${skillName}</b> [${skill.status}]${validationNote}
27952
+ await sendOrEditKeyboard(
27953
+ chatId,
27954
+ channel,
27955
+ messageId,
27956
+ `${typeLabel} <b>${skillName}</b> [${skill.status}]${validationNote}
27317
27957
 
27318
27958
  <pre>${escaped}</pre>`,
27959
+ [
27319
27960
  [
27320
- [
27321
- { label: "\u2705 Approve", data: `appr:do:${skillName}`, style: "success" },
27322
- { label: "\u{1F527} Optimize First", data: `appr:opt:${skillName}` }
27323
- ],
27324
- [{ label: "\u2190 Back", data: "appr:list" }]
27325
- ]
27326
- );
27327
- }
27961
+ { label: "\u2705 Approve", data: `appr:do:${skillName}`, style: "success" },
27962
+ { label: "\u{1F527} Optimize First", data: `appr:opt:${skillName}` }
27963
+ ],
27964
+ [{ label: "\u2190 Back", data: "appr:list" }]
27965
+ ]
27966
+ );
27328
27967
  return;
27329
27968
  } else if (data.startsWith("appr:do:")) {
27330
27969
  const skillName = data.slice(8);
27331
27970
  const allSkills = await discoverAllSkills();
27332
27971
  const skill = allSkills.find((s) => s.name === skillName && s.sources.includes("cc-claw"));
27333
27972
  if (!skill) {
27334
- await channel.sendText(chatId, `Skill "${skillName}" not found.`, { parseMode: "plain" });
27973
+ await sendOrEditKeyboard(chatId, channel, messageId, `Skill "${skillName}" not found.`, [[{ label: "\u2190 Back", data: "appr:list" }]]);
27335
27974
  return;
27336
27975
  }
27337
27976
  try {
@@ -27343,40 +27982,40 @@ ${formatValidationReport2(validation)}` : "\n\n\u2705 Quality checks passed.";
27343
27982
  updated = updateFrontmatter2(updated, { status: "approved" });
27344
27983
  writeFileSync16(skill.filePath, updated, "utf-8");
27345
27984
  invalidateSkillCache2();
27346
- await channel.sendText(chatId, `\u2705 "${skillName}" approved.`, { parseMode: "plain" });
27347
27985
  const refreshedSkills = await discoverAllSkills();
27348
27986
  const stillUnapproved = refreshedSkills.filter(
27349
27987
  (s) => s.sources.includes("cc-claw") && s.status !== "approved"
27350
27988
  );
27351
27989
  if (stillUnapproved.length > 0) {
27352
- if (typeof channel.sendKeyboard === "function") {
27353
- await channel.sendKeyboard(
27354
- chatId,
27355
- `${stillUnapproved.length} more skill${stillUnapproved.length !== 1 ? "s" : ""} to review.`,
27356
- [[
27357
- { label: "Continue Reviewing", data: "appr:list", style: "primary" },
27358
- { label: "\u2190 Done", data: "skills:page:1" }
27359
- ]]
27360
- );
27361
- }
27990
+ await sendOrEditKeyboard(
27991
+ chatId,
27992
+ channel,
27993
+ messageId,
27994
+ `\u2705 "${skillName}" approved.
27995
+
27996
+ ${stillUnapproved.length} more skill${stillUnapproved.length !== 1 ? "s" : ""} to review.`,
27997
+ [[
27998
+ { label: "Continue Reviewing", data: "appr:list", style: "primary" },
27999
+ { label: "\u2190 Done", data: "skills:page:1" }
28000
+ ]]
28001
+ );
27362
28002
  } else {
27363
- if (typeof channel.sendKeyboard === "function") {
27364
- await channel.sendKeyboard(
27365
- chatId,
27366
- "All CC-Claw skills are now approved!",
27367
- [[{ label: "\u2190 Back to Skills", data: "skills:page:1" }]]
27368
- );
27369
- }
28003
+ await sendOrEditKeyboard(
28004
+ chatId,
28005
+ channel,
28006
+ messageId,
28007
+ "\u2705 All CC-Claw skills are now approved!",
28008
+ [[{ label: "\u2190 Back to Skills", data: "skills:page:1" }]]
28009
+ );
27370
28010
  }
27371
28011
  } catch (e) {
27372
- await channel.sendText(chatId, `Approve failed: ${e.message}`, { parseMode: "plain" });
28012
+ await sendOrEditKeyboard(chatId, channel, messageId, `Approve failed: ${e.message}`, [[{ label: "\u2190 Back", data: "appr:list" }]]);
27373
28013
  }
27374
28014
  return;
27375
28015
  } else if (data.startsWith("appr:opt:")) {
27376
28016
  const skillName = data.slice(9);
27377
- await channel.sendText(chatId, `Opening optimizer for "${skillName}"...`, { parseMode: "plain" });
27378
28017
  const { handleOptimizeCallback: routeOptimize } = await Promise.resolve().then(() => (init_optimize(), optimize_exports));
27379
- await routeOptimize(chatId, `opt:skill-pick:${skillName}`, channel);
28018
+ await routeOptimize(chatId, `opt:skill-pick:${skillName}`, channel, messageId);
27380
28019
  return;
27381
28020
  } else if (data === "appr:all") {
27382
28021
  const allSkills = await discoverAllSkills();
@@ -27412,15 +28051,21 @@ ${formatValidationReport2(validation)}` : "\n\n\u2705 Quality checks passed.";
27412
28051
  \u26A0\uFE0F ${issues.length} failed:
27413
28052
  ${issues.join("\n")}`;
27414
28053
  }
27415
- if (typeof channel.sendKeyboard === "function") {
27416
- await channel.sendKeyboard(chatId, msg, [
27417
- [{ label: "\u2190 Back to Skills", data: "skills:page:1" }]
27418
- ]);
27419
- } else {
27420
- await channel.sendText(chatId, msg, { parseMode: "plain" });
27421
- }
28054
+ await sendOrEditKeyboard(
28055
+ chatId,
28056
+ channel,
28057
+ messageId,
28058
+ msg,
28059
+ [[{ label: "\u2190 Back to Skills", data: "skills:page:1" }]]
28060
+ );
27422
28061
  } catch (e) {
27423
- await channel.sendText(chatId, `Bulk approve failed: ${e.message}`, { parseMode: "plain" });
28062
+ await sendOrEditKeyboard(
28063
+ chatId,
28064
+ channel,
28065
+ messageId,
28066
+ `Bulk approve failed: ${e.message}`,
28067
+ [[{ label: "\u2190 Back", data: "appr:list" }]]
28068
+ );
27424
28069
  }
27425
28070
  return;
27426
28071
  } else if (data.startsWith("skill:")) {
@@ -27455,7 +28100,6 @@ ${issues.join("\n")}`;
27455
28100
  var init_callbacks = __esm({
27456
28101
  "src/router/callbacks.ts"() {
27457
28102
  "use strict";
27458
- init_format();
27459
28103
  init_log();
27460
28104
  init_agent();
27461
28105
  init_discover();
@@ -28393,7 +29037,8 @@ Debating: "${question.slice(0, 100)}${question.length > 100 ? "\u2026" : ""}"`,
28393
29037
  }
28394
29038
  responseText += `
28395
29039
 
28396
- \u{1F9E0} [${shortModel} | ${capitalize(thinking2)}${slotTag}] \u23F1\uFE0F ${elapsedSec}s \xB7 \u{1F195}/new`;
29040
+ \u{1F9E0} [${shortModel} | ${capitalize(thinking2)}${slotTag}] \u23F1\uFE0F ${elapsedSec}s
29041
+ \u{1F195} /new | \u{1F9FD} /clear`;
28397
29042
  }
28398
29043
  if (observedSubagents.size > 0) {
28399
29044
  const names = [...observedSubagents].join(", ");
@@ -28876,8 +29521,8 @@ async function runWithRetry(job, model2, runId, t0) {
28876
29521
  }
28877
29522
  for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
28878
29523
  try {
28879
- const { getVerboseLevel: getVerboseLevel3 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
28880
- const vLevel = getVerboseLevel3(chatId);
29524
+ const { getVerboseLevel: getVerboseLevel4 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
29525
+ const vLevel = getVerboseLevel4(chatId);
28881
29526
  let onToolAction;
28882
29527
  if (vLevel !== "off") {
28883
29528
  const { makeToolActionCallback: makeToolActionCallback2 } = await Promise.resolve().then(() => (init_router2(), router_exports));
@@ -29835,7 +30480,7 @@ var init_telegram2 = __esm({
29835
30480
  init_health3();
29836
30481
  init_store5();
29837
30482
  init_telegram_throttle();
29838
- FAST_PATH_COMMANDS = /* @__PURE__ */ new Set(["stop", "status", "new", "newchat"]);
30483
+ FAST_PATH_COMMANDS = /* @__PURE__ */ new Set(["stop", "status", "new", "newchat", "clear"]);
29839
30484
  TelegramChannel = class _TelegramChannel {
29840
30485
  name = "telegram";
29841
30486
  bot;
@@ -29909,6 +30554,7 @@ var init_telegram2 = __esm({
29909
30554
  { command: "status", description: "Session, backend, model, and usage" },
29910
30555
  { command: "new", description: "Start a fresh conversation" },
29911
30556
  { command: "newchat", description: "Start a fresh conversation" },
30557
+ { command: "clear", description: "Clear session without saving (no summary)" },
29912
30558
  { command: "summarize", description: "Save session to memory (or 'all' for pre-restart)" },
29913
30559
  { command: "stop", description: "Cancel the current running task" },
29914
30560
  // Backend & model
@@ -30264,17 +30910,25 @@ var init_telegram2 = __esm({
30264
30910
  }
30265
30911
  }
30266
30912
  }
30267
- async editKeyboard(chatId, messageId, text, buttons) {
30913
+ buildInlineKeyboard(buttons) {
30268
30914
  const keyboard = new InlineKeyboard();
30269
30915
  for (const row of buttons) {
30270
30916
  for (const btn of row) {
30271
- keyboard.text(btn.label, btn.data);
30917
+ if (btn.switchInlineQueryCurrentChat !== void 0) {
30918
+ keyboard.switchInlineCurrent(btn.label, btn.switchInlineQueryCurrentChat);
30919
+ } else {
30920
+ keyboard.text(btn.label, btn.data);
30921
+ }
30272
30922
  if (btn.style === "success") keyboard.success();
30273
30923
  else if (btn.style === "danger") keyboard.danger();
30274
30924
  else if (btn.style === "primary") keyboard.primary();
30275
30925
  }
30276
30926
  keyboard.row();
30277
30927
  }
30928
+ return keyboard;
30929
+ }
30930
+ async editKeyboard(chatId, messageId, text, buttons) {
30931
+ const keyboard = this.buildInlineKeyboard(buttons);
30278
30932
  const formatted = sanitizeForTelegram(formatForTelegram(text));
30279
30933
  try {
30280
30934
  await this.throttle.send(
@@ -30314,16 +30968,7 @@ var init_telegram2 = __esm({
30314
30968
  this.reactionHandlers.push(handler);
30315
30969
  }
30316
30970
  async sendKeyboard(chatId, text, buttons, threadId) {
30317
- const keyboard = new InlineKeyboard();
30318
- for (const row of buttons) {
30319
- for (const btn of row) {
30320
- keyboard.text(btn.label, btn.data);
30321
- if (btn.style === "success") keyboard.success();
30322
- else if (btn.style === "danger") keyboard.danger();
30323
- else if (btn.style === "primary") keyboard.primary();
30324
- }
30325
- keyboard.row();
30326
- }
30971
+ const keyboard = this.buildInlineKeyboard(buttons);
30327
30972
  const MAX_KEYBOARD_TEXT = 4e3;
30328
30973
  const safeText = text.length > MAX_KEYBOARD_TEXT ? text.slice(0, MAX_KEYBOARD_TEXT) + "\n\n\u2026(truncated)" : text;
30329
30974
  const formatted = sanitizeForTelegram(formatForTelegram(safeText));
@@ -30550,6 +31195,9 @@ var init_telegram2 = __esm({
30550
31195
  caption: ctx.message.caption ?? "",
30551
31196
  fileName: ctx.message.document.file_id,
30552
31197
  mimeType: ctx.message.document.mime_type ?? "application/octet-stream",
31198
+ metadata: {
31199
+ originalName: ctx.message.document.file_name
31200
+ },
30553
31201
  chatTitle,
30554
31202
  replyToText,
30555
31203
  forwardedFrom,
@@ -35103,9 +35751,9 @@ async function toolsList(globalOpts) {
35103
35751
  const chatId = resolveChatId2(globalOpts);
35104
35752
  const rows = readDb.prepare("SELECT tool, enabled FROM chat_tools WHERE chat_id = ?").all(chatId);
35105
35753
  readDb.close();
35106
- const ALL_TOOLS3 = ["Read", "Write", "Edit", "Bash", "Glob", "Grep", "WebFetch", "WebSearch", "Agent", "AskUserQuestion"];
35754
+ const ALL_TOOLS4 = ["Read", "Write", "Edit", "Bash", "Glob", "Grep", "WebFetch", "WebSearch", "Agent", "AskUserQuestion"];
35107
35755
  const toolMap = new Map(rows.map((r) => [r.tool, !!r.enabled]));
35108
- const tools2 = ALL_TOOLS3.map((t) => ({ name: t, enabled: toolMap.get(t) ?? true }));
35756
+ const tools2 = ALL_TOOLS4.map((t) => ({ name: t, enabled: toolMap.get(t) ?? true }));
35109
35757
  output(tools2, (d) => {
35110
35758
  const list = d;
35111
35759
  const lines = ["", divider("Tools"), ""];
@@ -35117,10 +35765,10 @@ async function toolsList(globalOpts) {
35117
35765
  });
35118
35766
  }
35119
35767
  async function toolsEnable(globalOpts, name) {
35120
- await toggleTool4(globalOpts, name, true);
35768
+ await toggleTool5(globalOpts, name, true);
35121
35769
  }
35122
35770
  async function toolsDisable(globalOpts, name) {
35123
- await toggleTool4(globalOpts, name, false);
35771
+ await toggleTool5(globalOpts, name, false);
35124
35772
  }
35125
35773
  async function toolsReset(globalOpts) {
35126
35774
  const { isDaemonRunning: isDaemonRunning2, apiPost: apiPost2 } = await Promise.resolve().then(() => (init_api_client(), api_client_exports));
@@ -35139,7 +35787,7 @@ async function toolsReset(globalOpts) {
35139
35787
  process.exit(1);
35140
35788
  }
35141
35789
  }
35142
- async function toggleTool4(globalOpts, name, enabled) {
35790
+ async function toggleTool5(globalOpts, name, enabled) {
35143
35791
  const { isDaemonRunning: isDaemonRunning2, apiPost: apiPost2 } = await Promise.resolve().then(() => (init_api_client(), api_client_exports));
35144
35792
  if (!await isDaemonRunning2()) {
35145
35793
  outputError("DAEMON_OFFLINE", "CC-Claw daemon is not running.\n\n Start it with: cc-claw service start");
@@ -36996,6 +37644,7 @@ async function validateBotToken(token) {
36996
37644
  }
36997
37645
  async function setup() {
36998
37646
  const TOTAL_STEPS = 6;
37647
+ const DRY_RUN = process.env.CC_CLAW_SETUP_DRY_RUN === "1";
36999
37648
  console.log("");
37000
37649
  console.log(bold(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
37001
37650
  console.log(bold(" \u2551 \u2551"));
@@ -37003,6 +37652,10 @@ async function setup() {
37003
37652
  console.log(bold(" \u2551 Personal AI Assistant via Telegram \u2551"));
37004
37653
  console.log(bold(" \u2551 \u2551"));
37005
37654
  console.log(bold(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
37655
+ if (DRY_RUN) {
37656
+ console.log("");
37657
+ console.log(yellow(" \u26A0\uFE0F DRY RUN MODE \u2014 nothing will be saved or modified"));
37658
+ }
37006
37659
  console.log("");
37007
37660
  console.log(" This wizard will walk you through setting up CC-Claw.");
37008
37661
  console.log(" It takes about 5 minutes. Let's go!\n");
@@ -37031,8 +37684,10 @@ async function setup() {
37031
37684
  console.log(yellow(" No backend CLIs found! Install at least one to use CC-Claw."));
37032
37685
  }
37033
37686
  console.log("");
37034
- for (const dir of [CC_CLAW_HOME, DATA_PATH, LOGS_PATH, SKILLS_PATH, RUNNERS_PATH, AGENTS_PATH]) {
37035
- if (!existsSync61(dir)) mkdirSync18(dir, { recursive: true });
37687
+ if (!DRY_RUN) {
37688
+ for (const dir of [CC_CLAW_HOME, DATA_PATH, LOGS_PATH, SKILLS_PATH, RUNNERS_PATH, AGENTS_PATH]) {
37689
+ if (!existsSync61(dir)) mkdirSync18(dir, { recursive: true });
37690
+ }
37036
37691
  }
37037
37692
  const env = {};
37038
37693
  const envSource = existsSync61(ENV_PATH) ? ENV_PATH : existsSync61(".env") ? ".env" : null;
@@ -37051,8 +37706,12 @@ async function setup() {
37051
37706
  console.log(yellow(` Found existing database at ${cwdDb} (${(size / 1024).toFixed(0)}KB)`));
37052
37707
  const migrate = await confirm("Copy database to ~/.cc-claw/? (preserves memories & history)", true);
37053
37708
  if (migrate) {
37054
- copyFileSync5(cwdDb, DB_PATH);
37055
- console.log(green(` Database copied to ${DB_PATH}`));
37709
+ if (DRY_RUN) {
37710
+ console.log(dim(` [dry-run] Would copy database to ${DB_PATH}`));
37711
+ } else {
37712
+ copyFileSync5(cwdDb, DB_PATH);
37713
+ console.log(green(` Database copied to ${DB_PATH}`));
37714
+ }
37056
37715
  }
37057
37716
  console.log("");
37058
37717
  }
@@ -37190,8 +37849,8 @@ async function setup() {
37190
37849
  console.log("");
37191
37850
  console.log(dim(" Choose your transcription provider:\n"));
37192
37851
  console.log(" 1. Local Whisper \u2014 free, works offline, runs on your machine");
37193
- console.log(" Requires: whisper-cli (brew install whisper-cpp on macOS)");
37194
- console.log(" Models downloaded on first use (~75MB\u20131.5GB depending on quality)");
37852
+ console.log(" No external tools needed \u2014 runs natively in Node.js");
37853
+ console.log(" Models downloaded once (~75MB\u20131.5GB depending on quality)");
37195
37854
  console.log("");
37196
37855
  console.log(" 2. Groq \u2014 free cloud API, fast, no local install needed");
37197
37856
  console.log(" Requires: free Groq API key from https://console.groq.com/keys");
@@ -37213,7 +37872,22 @@ async function setup() {
37213
37872
  const modelMap = { "1": "tiny.en", "2": "base.en", "3": "small.en", "4": "small", "5": "medium.en", "6": "medium" };
37214
37873
  env.STT_PROVIDER = "local-whisper";
37215
37874
  env.STT_MODEL = modelMap[modelChoice] ?? "small.en";
37216
- console.log(green(` Local Whisper selected (${env.STT_MODEL}). Model will download on first voice message.`));
37875
+ const selectedModel = env.STT_MODEL;
37876
+ console.log(green(` Local Whisper selected (${selectedModel}).`));
37877
+ if (!DRY_RUN) {
37878
+ console.log(dim(` Model download started in the background. It will be ready by the time you finish setup.`));
37879
+ Promise.resolve().then(() => (init_stt(), stt_exports)).then(({ downloadWhisperModel: downloadWhisperModel2 }) => {
37880
+ downloadWhisperModel2(selectedModel, (msg) => {
37881
+ if (msg.startsWith("\u2705")) console.log(green(` ${msg}`));
37882
+ }).catch((err) => {
37883
+ console.log(yellow(` Background model download failed: ${err.message}`));
37884
+ console.log(dim(` The model will download when you first send a voice message.`));
37885
+ });
37886
+ }).catch(() => {
37887
+ });
37888
+ } else {
37889
+ console.log(dim(` [dry-run] Skipping model download.`));
37890
+ }
37217
37891
  } else if (sttChoice === "2") {
37218
37892
  const groqKey = await requiredInput("Groq API key", env.GROQ_API_KEY);
37219
37893
  env.GROQ_API_KEY = groqKey;
@@ -37293,14 +37967,29 @@ async function setup() {
37293
37967
  envLines.push("", "# Video Analysis", `GEMINI_API_KEY=${env.GEMINI_API_KEY}`);
37294
37968
  }
37295
37969
  const envContent = envLines.join("\n") + "\n";
37296
- writeFileSync15(ENV_PATH, envContent, { mode: 384 });
37297
- console.log(green(` Config saved to ${ENV_PATH} (permissions: owner-only)`));
37970
+ if (DRY_RUN) {
37971
+ console.log(dim(` [dry-run] Would save config to ${ENV_PATH}`));
37972
+ console.log(dim(" [dry-run] Generated config:"));
37973
+ for (const line of envLines) {
37974
+ if (line.includes("TOKEN") || line.includes("KEY") || line.includes("API")) {
37975
+ const masked = line.replace(/=(.+)$/, "=***");
37976
+ console.log(dim(` ${masked}`));
37977
+ } else {
37978
+ console.log(dim(` ${line}`));
37979
+ }
37980
+ }
37981
+ } else {
37982
+ writeFileSync15(ENV_PATH, envContent, { mode: 384 });
37983
+ console.log(green(` Config saved to ${ENV_PATH} (permissions: owner-only)`));
37984
+ }
37298
37985
  header(6, TOTAL_STEPS, "Run on Startup (Daemon)");
37299
37986
  console.log(" CC-Claw can run automatically in the background, starting");
37300
37987
  console.log(" when your Mac boots. No terminal window needed.\n");
37301
37988
  const installDaemon = await confirm("Install CC-Claw as a background service?", true);
37302
37989
  if (installDaemon) {
37303
- try {
37990
+ if (DRY_RUN) {
37991
+ console.log(dim(" [dry-run] Would install background service"));
37992
+ } else try {
37304
37993
  const { installService: installService2 } = await Promise.resolve().then(() => (init_service2(), service_exports2));
37305
37994
  installService2();
37306
37995
  console.log("");
@@ -37322,7 +38011,7 @@ async function setup() {
37322
38011
  }
37323
38012
  console.log("");
37324
38013
  divider2();
37325
- console.log(bold(" Setup Complete!"));
38014
+ console.log(bold(DRY_RUN ? " Dry Run Complete! (nothing was saved)" : " Setup Complete!"));
37326
38015
  divider2();
37327
38016
  console.log("");
37328
38017
  console.log(" " + green("[ok]") + ` Telegram bot: @${env.TELEGRAM_BOT_TOKEN ? "configured" : "missing"}`);
@@ -37964,7 +38653,8 @@ program.command("uninstall", { hidden: true }).description("Remove background se
37964
38653
  const { uninstallService: uninstallService2 } = await Promise.resolve().then(() => (init_service2(), service_exports2));
37965
38654
  uninstallService2();
37966
38655
  });
37967
- program.command("setup").description("Interactive configuration wizard").action(async () => {
38656
+ program.command("setup").description("Interactive configuration wizard").option("--dry-run", "Run the wizard without saving anything (demo/test mode)").action(async (opts) => {
38657
+ if (opts.dryRun) process.env.CC_CLAW_SETUP_DRY_RUN = "1";
37968
38658
  await Promise.resolve().then(() => (init_setup(), setup_exports));
37969
38659
  });
37970
38660
  async function run(argv = process.argv) {