cc-claw 0.19.2 → 0.19.3

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 +1460 -1171
  2. package/package.json +1 -1
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.19.2" : (() => {
36
+ VERSION = true ? "0.19.3" : (() => {
37
37
  try {
38
38
  return JSON.parse(readFileSync(join(process.cwd(), "package.json"), "utf-8")).version ?? "unknown";
39
39
  } catch {
@@ -8470,6 +8470,17 @@ function buildContextPrefix(msg) {
8470
8470
  }
8471
8471
  return parts.length > 0 ? parts.join("\n") + "\n\n" : "";
8472
8472
  }
8473
+ async function sendOrEditKeyboard(chatId, channel, messageId, text, buttons) {
8474
+ if (messageId && typeof channel.editKeyboard === "function") {
8475
+ const ok = await channel.editKeyboard(chatId, messageId, text, buttons);
8476
+ if (ok) return messageId;
8477
+ }
8478
+ if (typeof channel.sendKeyboard === "function") {
8479
+ return channel.sendKeyboard(chatId, text, buttons);
8480
+ }
8481
+ await channel.sendText(chatId, text, { parseMode: "plain" });
8482
+ return void 0;
8483
+ }
8473
8484
  var TONE_PATTERNS, ALLOWED_REACTION_EMOJIS, BLOCKED_PATH_PATTERNS, CLI_INSTALL_HINTS, PERM_MODES, VERBOSE_LEVELS, HELP_CATEGORIES, USAGE_WINDOW_MAP, SKILLS_PER_PAGE, MAX_SIDE_QUESTS;
8474
8485
  var init_helpers = __esm({
8475
8486
  "src/router/helpers.ts"() {
@@ -15314,160 +15325,568 @@ var init_wizard = __esm({
15314
15325
  }
15315
15326
  });
15316
15327
 
15317
- // src/agents/classify.ts
15318
- function classifyAgentIntent(message) {
15319
- const NOT_DETECTED = { detected: false, suggestedMode: "native" };
15320
- for (const pat of NEGATIVE_PATTERNS) {
15321
- if (pat.test(message)) return NOT_DETECTED;
15322
- }
15323
- for (const pat of CLAW_SIGNAL_PATTERNS) {
15324
- if (pat.test(message)) {
15325
- return { detected: true, suggestedMode: "claw", reason: "cross-backend or inter-agent communication detected" };
15326
- }
15327
- }
15328
- for (const pat of AGENT_SIGNAL_PATTERNS) {
15329
- if (pat.test(message)) {
15330
- return { detected: true, suggestedMode: "native" };
15331
- }
15328
+ // src/router/ollama.ts
15329
+ var ollama_exports2 = {};
15330
+ __export(ollama_exports2, {
15331
+ handleOllamaCallback: () => handleOllamaCallback,
15332
+ handleOllamaCommand: () => handleOllamaCommand,
15333
+ handleOllamaWizardText: () => handleOllamaWizardText,
15334
+ hasOllamaWizard: () => hasOllamaWizard
15335
+ });
15336
+ function hasOllamaWizard(chatId) {
15337
+ return pendingAdds.has(chatId);
15338
+ }
15339
+ function cancelOllamaWizard(chatId) {
15340
+ pendingAdds.delete(chatId);
15341
+ const timer = wizardTimers2.get(chatId);
15342
+ if (timer) {
15343
+ clearTimeout(timer);
15344
+ wizardTimers2.delete(chatId);
15332
15345
  }
15333
- return NOT_DETECTED;
15334
15346
  }
15335
- var AGENT_SIGNAL_PATTERNS, CLAW_SIGNAL_PATTERNS, NEGATIVE_PATTERNS;
15336
- var init_classify2 = __esm({
15337
- "src/agents/classify.ts"() {
15338
- "use strict";
15339
- AGENT_SIGNAL_PATTERNS = [
15340
- /\bspawn\s+(?:\d+\s+)?agents?\b/i,
15341
- /\bsub-?agents?\b/i,
15342
- /\bparallel\s+agents?\b/i,
15343
- /\bfan\s+out\b/i,
15344
- /\bdispatch\s+agents?\b/i,
15345
- /\bmulti-?agent\b/i
15346
- ];
15347
- CLAW_SIGNAL_PATTERNS = [
15348
- /\breview\s+each\s+other/i,
15349
- /\bdiscuss\s+between\b/i,
15350
- /\bcross-?backend\b/i,
15351
- /\bagent\s+team\b/i,
15352
- /\bagent\s+inbox\b/i,
15353
- /\bagent\s+whiteboard\b/i,
15354
- /\bagents?\s+.*debate\b/i,
15355
- /\bclaude\b.*\bagents?\b.*\bgemini\b/i,
15356
- /\bgemini\b.*\bagents?\b.*\bclaude\b/i,
15357
- /\bcodex\b.*\bagents?\b.*\bclaude\b/i,
15358
- /\bclaude\b.*\bagents?\b.*\bcodex\b/i,
15359
- /\bgemini\b.*\bagents?\b.*\bcodex\b/i,
15360
- /\bcodex\b.*\bagents?\b.*\bgemini\b/i
15361
- ];
15362
- NEGATIVE_PATTERNS = [
15363
- /\bhow\s+does\s+.*\bwork\b/i,
15364
- /\bwhat\s+is\s+.*\bfunction\b/i,
15365
- /\bexplain\s+.*\bagent/i,
15366
- /src\/agents?\//
15367
- ];
15347
+ function resetWizardTimeout2(chatId) {
15348
+ const existing = wizardTimers2.get(chatId);
15349
+ if (existing) clearTimeout(existing);
15350
+ wizardTimers2.set(chatId, setTimeout(() => {
15351
+ pendingAdds.delete(chatId);
15352
+ wizardTimers2.delete(chatId);
15353
+ log(`[ollama-wizard] Auto-cancelled stale wizard for chat ${chatId}`);
15354
+ }, WIZARD_TIMEOUT_MS2));
15355
+ }
15356
+ async function startAddWizard(chatId, channel) {
15357
+ cancelOllamaWizard(chatId);
15358
+ pendingAdds.set(chatId, { step: "name" });
15359
+ resetWizardTimeout2(chatId);
15360
+ await channel.sendText(chatId, '\u{1F999} Add Ollama Server\n\nWhat should this server be called?\n(A short name like "local", "mac-studio", "gpu-box")', { parseMode: "plain" });
15361
+ }
15362
+ async function handleOllamaWizardText(chatId, text, channel) {
15363
+ const pending = pendingAdds.get(chatId);
15364
+ if (!pending) return;
15365
+ const lower = text.toLowerCase().trim();
15366
+ if (lower === "cancel") {
15367
+ cancelOllamaWizard(chatId);
15368
+ await channel.sendText(chatId, "Server setup cancelled.", { parseMode: "plain" });
15369
+ return;
15368
15370
  }
15369
- });
15370
-
15371
- // src/router/session-log.ts
15372
- var session_log_exports2 = {};
15373
- __export(session_log_exports2, {
15374
- SessionLogFile: () => SessionLogFile,
15375
- cleanupSessionLogs: () => cleanupSessionLogs,
15376
- getRetentionDays: () => getRetentionDays,
15377
- listSessionLogs: () => listSessionLogs,
15378
- startSessionLogCleanupTimer: () => startSessionLogCleanupTimer,
15379
- tailSessionLog: () => tailSessionLog
15380
- });
15381
- import { existsSync as existsSync15, mkdirSync as mkdirSync8, appendFileSync, readdirSync as readdirSync9, unlinkSync as unlinkSync6, statSync as statSync6, createReadStream } from "fs";
15382
- import { join as join15, basename } from "path";
15383
- import { createInterface as createInterface6 } from "readline";
15384
- function getRetentionDays() {
15385
- const env = process.env.SESSION_LOG_RETENTION_DAYS;
15386
- if (env) {
15387
- const n = parseInt(env, 10);
15388
- if (!isNaN(n) && n > 0) return n;
15371
+ if (lower === "confirm" && pending.step === "confirm") {
15372
+ return finalizeAddWizard(chatId, channel);
15389
15373
  }
15390
- return DEFAULT_RETENTION_DAYS;
15391
- }
15392
- function cleanupSessionLogs(retentionDays) {
15393
- const days = retentionDays ?? getRetentionDays();
15394
- if (!existsSync15(SESSION_LOGS_PATH)) return 0;
15395
- const cutoff = Date.now() - days * 24 * 60 * 60 * 1e3;
15396
- let cleaned = 0;
15397
- try {
15398
- for (const file of readdirSync9(SESSION_LOGS_PATH)) {
15399
- if (!file.startsWith("session-") || !file.endsWith(".log")) continue;
15400
- const filePath = join15(SESSION_LOGS_PATH, file);
15401
- try {
15402
- const { mtimeMs } = statSync6(filePath);
15403
- if (mtimeMs < cutoff) {
15404
- unlinkSync6(filePath);
15405
- cleaned++;
15406
- }
15407
- } catch {
15374
+ resetWizardTimeout2(chatId);
15375
+ switch (pending.step) {
15376
+ case "name": {
15377
+ const name = text.trim().replace(/\s+/g, "-").toLowerCase();
15378
+ if (!name || name.length > 30) {
15379
+ await channel.sendText(chatId, "Name should be 1-30 characters. Try again:", { parseMode: "plain" });
15380
+ return;
15408
15381
  }
15382
+ const { OllamaStore } = await Promise.resolve().then(() => (init_ollama(), ollama_exports));
15383
+ if (OllamaStore.getServer(name)) {
15384
+ await channel.sendText(chatId, `Server "${name}" already exists. Pick a different name:`, { parseMode: "plain" });
15385
+ return;
15386
+ }
15387
+ pending.name = name;
15388
+ pending.step = "host";
15389
+ await channel.sendText(chatId, `Server name: ${name}
15390
+
15391
+ Enter the host or IP address:
15392
+ (e.g. 192.168.1.100, mac-studio.local, 10.0.0.5)`, { parseMode: "plain" });
15393
+ break;
15409
15394
  }
15410
- } catch (err) {
15411
- log(`[session-log] Cleanup failed: ${err}`);
15395
+ case "host": {
15396
+ const host = text.trim();
15397
+ if (!host || host.includes(" ")) {
15398
+ await channel.sendText(chatId, "Invalid host. Enter an IP address or hostname (no spaces):", { parseMode: "plain" });
15399
+ return;
15400
+ }
15401
+ pending.host = host;
15402
+ pending.step = "port";
15403
+ await promptPort(chatId, channel);
15404
+ break;
15405
+ }
15406
+ case "port": {
15407
+ const val = parseInt(text.trim(), 10);
15408
+ if (isNaN(val) || val < 1 || val > 65535) {
15409
+ await channel.sendText(chatId, "Invalid port (1-65535). Try again or tap Default:", { parseMode: "plain" });
15410
+ return;
15411
+ }
15412
+ pending.port = val;
15413
+ pending.step = "confirm";
15414
+ await promptAddConfirm(chatId, channel);
15415
+ break;
15416
+ }
15417
+ default:
15418
+ await channel.sendText(chatId, "Use the buttons to confirm, edit, or cancel.", { parseMode: "plain" });
15412
15419
  }
15413
- if (cleaned > 0) {
15414
- log(`[session-log] Cleaned ${cleaned} session log(s) older than ${days} day(s)`);
15420
+ }
15421
+ async function promptPort(chatId, channel) {
15422
+ if (typeof channel.sendKeyboard === "function") {
15423
+ await channel.sendKeyboard(chatId, "Which port is Ollama running on?", [
15424
+ [{ label: "Default (11434)", data: "ollama:wizard_port:11434" }],
15425
+ [{ label: "Custom (443)", data: "ollama:wizard_port:443" }]
15426
+ ]);
15427
+ await channel.sendText(chatId, "Or type a custom port number:", { parseMode: "plain" });
15428
+ } else {
15429
+ await channel.sendText(chatId, "Enter the port (default: 11434):", { parseMode: "plain" });
15415
15430
  }
15416
- return cleaned;
15417
15431
  }
15418
- function startSessionLogCleanupTimer() {
15419
- const timer = setInterval(() => {
15420
- cleanupSessionLogs();
15421
- }, 24 * 60 * 60 * 1e3);
15422
- timer.unref();
15423
- return timer;
15432
+ async function promptAddConfirm(chatId, channel) {
15433
+ const pending = pendingAdds.get(chatId);
15434
+ if (!pending) return;
15435
+ const lines = [
15436
+ "\u{1F999} Add Server \u2014 Confirm",
15437
+ "",
15438
+ ` Name: ${pending.name}`,
15439
+ ` Host: ${pending.host}`,
15440
+ ` Port: ${pending.port ?? 11434}`
15441
+ ];
15442
+ if (typeof channel.sendKeyboard === "function") {
15443
+ await channel.sendKeyboard(chatId, lines.join("\n"), [
15444
+ [
15445
+ { label: "\u2705 Add", data: "ollama:wizard_confirm", style: "success" },
15446
+ { label: "\u270F\uFE0F Start Over", data: "ollama:wizard_restart" },
15447
+ { label: "\u274C Cancel", data: "ollama:wizard_cancel" }
15448
+ ]
15449
+ ]);
15450
+ } else {
15451
+ await channel.sendText(chatId, lines.join("\n") + "\n\nSend 'confirm' to add or 'cancel' to abort.", { parseMode: "plain" });
15452
+ }
15424
15453
  }
15425
- function listSessionLogs() {
15426
- if (!existsSync15(SESSION_LOGS_PATH)) return [];
15427
- const logs = [];
15428
- for (const file of readdirSync9(SESSION_LOGS_PATH)) {
15429
- if (!file.startsWith("session-") || !file.endsWith(".log")) continue;
15430
- const filePath = join15(SESSION_LOGS_PATH, file);
15431
- try {
15432
- const stat3 = statSync6(filePath);
15433
- const match = file.match(/^session-(.+?)-(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2})\.log$/);
15434
- logs.push({
15435
- filename: file,
15436
- filePath,
15437
- chatId: match?.[1] ?? "unknown",
15438
- timestamp: match?.[2]?.replace(/-/g, (m, i) => i > 9 ? ":" : m) ?? "unknown",
15439
- sizeBytes: stat3.size,
15440
- modifiedAt: stat3.mtime
15441
- });
15442
- } catch {
15443
- }
15454
+ async function finalizeAddWizard(chatId, channel) {
15455
+ const pending = pendingAdds.get(chatId);
15456
+ if (!pending?.name || !pending?.host) return;
15457
+ cancelOllamaWizard(chatId);
15458
+ await handleAdd(chatId, channel, pending.name, pending.host, pending.port);
15459
+ }
15460
+ async function handleOllamaCommand(chatId, commandArgs, channel) {
15461
+ const [sub, ...rest] = (commandArgs || "").trim().split(/\s+/);
15462
+ switch (sub) {
15463
+ case "models":
15464
+ return sendModelList(chatId, channel, rest[0]);
15465
+ case "health":
15466
+ return sendHealthCheck(chatId, channel);
15467
+ case "discover":
15468
+ return sendDiscover(chatId, channel, rest[0]);
15469
+ case "add":
15470
+ if (rest.length >= 2) return handleAdd(chatId, channel, rest[0], rest[1], rest[2] ? parseInt(rest[2], 10) : void 0);
15471
+ if (rest.length === 1) return handleAdd(chatId, channel, rest[0], rest[0]);
15472
+ return startAddWizard(chatId, channel);
15473
+ case "remove":
15474
+ if (rest[0]) return sendRemoveConfirm(chatId, channel, rest[0]);
15475
+ await channel.sendText(chatId, "Usage: /ollama remove <server-name>", { parseMode: "plain" });
15476
+ return;
15477
+ default:
15478
+ return sendOllamaDashboard(chatId, channel);
15444
15479
  }
15445
- logs.sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime());
15446
- return logs;
15447
15480
  }
15448
- async function* tailSessionLog(filePath, lines = 50) {
15449
- if (!existsSync15(filePath)) {
15450
- yield `File not found: ${filePath}`;
15481
+ async function sendOllamaDashboard(chatId, channel) {
15482
+ const { OllamaStore } = await Promise.resolve().then(() => (init_ollama(), ollama_exports));
15483
+ const servers = OllamaStore.listServers();
15484
+ if (servers.length === 0) {
15485
+ const text = [
15486
+ "<b>\u{1F999} Ollama</b>",
15487
+ "",
15488
+ "No servers configured.",
15489
+ "",
15490
+ "Add your first server:",
15491
+ " <code>/ollama add &lt;name&gt; &lt;host&gt; [port]</code>",
15492
+ "",
15493
+ "Example:",
15494
+ " <code>/ollama add local 192.168.1.100</code>"
15495
+ ].join("\n");
15496
+ if (typeof channel.sendKeyboard === "function") {
15497
+ await channel.sendKeyboard(chatId, text, [
15498
+ [{ label: "\u{1F4D6} Help", data: "ollama:help" }]
15499
+ ]);
15500
+ } else {
15501
+ await channel.sendText(chatId, text, { parseMode: "html" });
15502
+ }
15451
15503
  return;
15452
15504
  }
15453
- const allLines = [];
15454
- const rl2 = createInterface6({
15455
- input: createReadStream(filePath, { encoding: "utf-8" }),
15456
- crlfDelay: Infinity
15457
- });
15458
- for await (const line of rl2) {
15459
- allLines.push(line);
15505
+ const lines = ["<b>\u{1F999} Ollama Servers</b>", ""];
15506
+ for (const s of servers) {
15507
+ const dot = s.status === "online" ? "\u{1F7E2}" : "\u{1F534}";
15508
+ const modelCount = OllamaStore.listModels(s.id).length;
15509
+ lines.push(`${dot} <b>${s.name}</b> <code>${s.host}:${s.port}</code>`);
15510
+ lines.push(` ${modelCount} model${modelCount !== 1 ? "s" : ""} \xB7 ${s.status}`);
15460
15511
  }
15461
- const start = Math.max(0, allLines.length - lines);
15462
- for (let i = start; i < allLines.length; i++) {
15463
- yield allLines[i];
15512
+ const onlineCount = servers.filter((s) => s.status === "online").length;
15513
+ lines.push("", `${onlineCount}/${servers.length} online`);
15514
+ if (typeof channel.sendKeyboard === "function") {
15515
+ const buttons = [
15516
+ [
15517
+ { label: "\u{1F50D} Discover", data: "ollama:discover" },
15518
+ { label: "\u{1F49A} Health", data: "ollama:health", style: "success" }
15519
+ ],
15520
+ [
15521
+ { label: "\u{1F4CB} Models", data: "ollama:models" },
15522
+ { label: "\u2795 Add", data: "ollama:add_prompt" }
15523
+ ]
15524
+ ];
15525
+ if (servers.length <= 4) {
15526
+ const removeRow = servers.map((s) => ({
15527
+ label: `\u{1F5D1} ${s.name}`,
15528
+ data: `ollama:remove_confirm:${s.name}`,
15529
+ style: "danger"
15530
+ }));
15531
+ buttons.push(removeRow);
15532
+ }
15533
+ await channel.sendKeyboard(chatId, lines.join("\n"), buttons);
15534
+ } else {
15535
+ await channel.sendText(chatId, lines.join("\n"), { parseMode: "html" });
15464
15536
  }
15465
15537
  }
15466
- var DEFAULT_RETENTION_DAYS, SessionLogFile;
15467
- var init_session_log2 = __esm({
15468
- "src/router/session-log.ts"() {
15469
- "use strict";
15470
- init_paths();
15538
+ async function handleAdd(chatId, channel, name, host, port) {
15539
+ const { OllamaStore, OllamaClient, OllamaService } = await Promise.resolve().then(() => (init_ollama(), ollama_exports));
15540
+ const existing = OllamaStore.getServer(name);
15541
+ if (existing) {
15542
+ await channel.sendText(chatId, `Server "${name}" already exists. Remove it first.`, { parseMode: "plain" });
15543
+ return;
15544
+ }
15545
+ const actualPort = port ?? 11434;
15546
+ await channel.sendText(chatId, `Adding ${name} (${host}:${actualPort})...`, { parseMode: "plain" });
15547
+ const online = await OllamaClient.ping(`http://${host}:${actualPort}`, { timeoutMs: 5e3 });
15548
+ const server = OllamaStore.addServer(name, host, actualPort, null);
15549
+ OllamaStore.updateServerStatus(server.id, online ? "online" : "offline");
15550
+ if (!online) {
15551
+ await channel.sendText(chatId, `\u26A0\uFE0F Server "${name}" added but not responding. Check connectivity.`, { parseMode: "plain" });
15552
+ return;
15553
+ }
15554
+ const models = await OllamaService.discoverModels(name);
15555
+ const lines = [
15556
+ `\u2705 Added "${name}" (${host}:${actualPort})`,
15557
+ "",
15558
+ `Found ${models.length} model(s):`
15559
+ ];
15560
+ for (const m of models) {
15561
+ lines.push(` \u2022 ${m.name}${m.parameterSize ? ` (${m.parameterSize})` : ""}`);
15562
+ }
15563
+ await channel.sendText(chatId, lines.join("\n"), { parseMode: "plain" });
15564
+ }
15565
+ async function sendModelList(chatId, channel, serverName) {
15566
+ const { OllamaStore } = await Promise.resolve().then(() => (init_ollama(), ollama_exports));
15567
+ let models;
15568
+ if (serverName) {
15569
+ const server = OllamaStore.getServer(serverName);
15570
+ if (!server) {
15571
+ await channel.sendText(chatId, `Server "${serverName}" not found.`, { parseMode: "plain" });
15572
+ return;
15573
+ }
15574
+ models = OllamaStore.listModels(server.id);
15575
+ } else {
15576
+ models = OllamaStore.getAvailableModels();
15577
+ }
15578
+ if (models.length === 0) {
15579
+ await channel.sendText(chatId, "No models discovered. Run /ollama discover first.", { parseMode: "plain" });
15580
+ return;
15581
+ }
15582
+ const lines = [
15583
+ `<b>\u{1F9E0} Ollama Models</b>${serverName ? ` (${serverName})` : ""}`,
15584
+ ""
15585
+ ];
15586
+ for (const m of models) {
15587
+ const sizeGB = m.sizeBytes > 0 ? `${(m.sizeBytes / 1e9).toFixed(1)}GB` : "";
15588
+ const ctxK = m.contextWindow ? `${Math.round(m.contextWindow / 1e3)}K ctx` : "";
15589
+ const meta = [m.parameterSize, m.quantization, sizeGB, ctxK].filter(Boolean).join(" \xB7 ");
15590
+ lines.push(` \u2022 <b>${m.name}</b>`);
15591
+ if (meta) lines.push(` <i>${meta}</i>`);
15592
+ }
15593
+ lines.push("", `${models.length} model${models.length !== 1 ? "s" : ""} total`);
15594
+ await channel.sendText(chatId, lines.join("\n"), { parseMode: "html" });
15595
+ }
15596
+ async function sendDiscover(chatId, channel, serverName) {
15597
+ await channel.sendText(chatId, "\u{1F50D} Discovering models...", { parseMode: "plain" });
15598
+ try {
15599
+ const { OllamaService } = await Promise.resolve().then(() => (init_ollama(), ollama_exports));
15600
+ const models = await OllamaService.discoverModels(serverName);
15601
+ if (models.length === 0) {
15602
+ await channel.sendText(chatId, "No models found. Check server connectivity.", { parseMode: "plain" });
15603
+ return;
15604
+ }
15605
+ const lines = [`\u2705 Found ${models.length} model(s):`, ""];
15606
+ for (const m of models) {
15607
+ const meta = [m.parameterSize, m.contextWindow ? `${Math.round(m.contextWindow / 1e3)}K ctx` : ""].filter(Boolean).join(" \xB7 ");
15608
+ lines.push(` \u2022 ${m.name}${meta ? ` (${meta})` : ""}`);
15609
+ }
15610
+ await channel.sendText(chatId, lines.join("\n"), { parseMode: "plain" });
15611
+ } catch (err) {
15612
+ await channel.sendText(chatId, `Discovery failed: ${err instanceof Error ? err.message : String(err)}`, { parseMode: "plain" });
15613
+ }
15614
+ }
15615
+ async function sendHealthCheck(chatId, channel) {
15616
+ await channel.sendText(chatId, "\u{1F49A} Pinging servers...", { parseMode: "plain" });
15617
+ try {
15618
+ const { OllamaService } = await Promise.resolve().then(() => (init_ollama(), ollama_exports));
15619
+ const results = await OllamaService.healthCheck();
15620
+ if (results.length === 0) {
15621
+ await channel.sendText(chatId, "No servers configured.", { parseMode: "plain" });
15622
+ return;
15623
+ }
15624
+ const lines = ["Health Check Results:", ""];
15625
+ for (const s of results) {
15626
+ const dot = s.status === "online" ? "\u{1F7E2}" : "\u{1F534}";
15627
+ lines.push(`${dot} ${s.name} (${s.host}:${s.port}) \u2014 ${s.status}`);
15628
+ }
15629
+ const online = results.filter((s) => s.status === "online").length;
15630
+ lines.push("", `${online}/${results.length} online`);
15631
+ await channel.sendText(chatId, lines.join("\n"), { parseMode: "plain" });
15632
+ } catch (err) {
15633
+ await channel.sendText(chatId, `Health check failed: ${err instanceof Error ? err.message : String(err)}`, { parseMode: "plain" });
15634
+ }
15635
+ }
15636
+ async function sendRemoveConfirm(chatId, channel, name) {
15637
+ const { OllamaStore } = await Promise.resolve().then(() => (init_ollama(), ollama_exports));
15638
+ const server = OllamaStore.getServer(name);
15639
+ if (!server) {
15640
+ await channel.sendText(chatId, `Server "${name}" not found.`, { parseMode: "plain" });
15641
+ return;
15642
+ }
15643
+ const modelCount = OllamaStore.listModels(server.id).length;
15644
+ const text = [
15645
+ `Remove server <b>${name}</b>?`,
15646
+ "",
15647
+ `Host: ${server.host}:${server.port}`,
15648
+ `Status: ${server.status}`,
15649
+ `Models: ${modelCount}`,
15650
+ "",
15651
+ "This will also remove all cached model data."
15652
+ ].join("\n");
15653
+ if (typeof channel.sendKeyboard === "function") {
15654
+ await channel.sendKeyboard(chatId, text, [
15655
+ [
15656
+ { label: "\u2705 Confirm", data: `ollama:remove:${name}`, style: "danger" },
15657
+ { label: "\u274C Cancel", data: "ollama:dashboard" }
15658
+ ]
15659
+ ]);
15660
+ } else {
15661
+ await channel.sendText(chatId, text + "\n\nUse /ollama remove <name> to confirm.", { parseMode: "html" });
15662
+ }
15663
+ }
15664
+ async function handleOllamaCallback(chatId, data, channel) {
15665
+ const parts = data.split(":");
15666
+ const action = parts[1];
15667
+ switch (action) {
15668
+ case "dashboard":
15669
+ return sendOllamaDashboard(chatId, channel);
15670
+ case "models":
15671
+ return sendModelList(chatId, channel);
15672
+ case "discover":
15673
+ return sendDiscover(chatId, channel);
15674
+ case "health":
15675
+ return sendHealthCheck(chatId, channel);
15676
+ case "remove_confirm": {
15677
+ const name = parts.slice(2).join(":");
15678
+ return sendRemoveConfirm(chatId, channel, name);
15679
+ }
15680
+ case "remove": {
15681
+ const name = parts.slice(2).join(":");
15682
+ const { OllamaStore } = await Promise.resolve().then(() => (init_ollama(), ollama_exports));
15683
+ const removed = OllamaStore.removeServer(name);
15684
+ if (removed) {
15685
+ await channel.sendText(chatId, `\u2705 Removed server "${name}"`, { parseMode: "plain" });
15686
+ } else {
15687
+ await channel.sendText(chatId, `Server "${name}" not found.`, { parseMode: "plain" });
15688
+ }
15689
+ return sendOllamaDashboard(chatId, channel);
15690
+ }
15691
+ case "add_prompt":
15692
+ return startAddWizard(chatId, channel);
15693
+ case "wizard_port": {
15694
+ const pending = pendingAdds.get(chatId);
15695
+ if (!pending) return;
15696
+ pending.port = parseInt(parts[2], 10) || 11434;
15697
+ pending.step = "confirm";
15698
+ await channel.sendText(chatId, `Port: ${pending.port}`, { parseMode: "plain" });
15699
+ return promptAddConfirm(chatId, channel);
15700
+ }
15701
+ case "wizard_confirm":
15702
+ return finalizeAddWizard(chatId, channel);
15703
+ case "wizard_restart":
15704
+ return startAddWizard(chatId, channel);
15705
+ case "wizard_cancel":
15706
+ cancelOllamaWizard(chatId);
15707
+ await channel.sendText(chatId, "Server setup cancelled.", { parseMode: "plain" });
15708
+ return;
15709
+ case "help":
15710
+ await channel.sendText(chatId, [
15711
+ "\u{1F999} Ollama Commands:",
15712
+ "",
15713
+ "/ollama \u2014 Server dashboard",
15714
+ "/ollama add <name> <host> [port] \u2014 Add server",
15715
+ "/ollama remove <name> \u2014 Remove server",
15716
+ "/ollama models [server] \u2014 List models",
15717
+ "/ollama discover [server] \u2014 Discover models",
15718
+ "/ollama health \u2014 Ping all servers"
15719
+ ].join("\n"), { parseMode: "plain" });
15720
+ return;
15721
+ default:
15722
+ await channel.sendText(chatId, "Unknown action.", { parseMode: "plain" });
15723
+ }
15724
+ }
15725
+ var pendingAdds, wizardTimers2, WIZARD_TIMEOUT_MS2;
15726
+ var init_ollama3 = __esm({
15727
+ "src/router/ollama.ts"() {
15728
+ "use strict";
15729
+ init_log();
15730
+ pendingAdds = /* @__PURE__ */ new Map();
15731
+ wizardTimers2 = /* @__PURE__ */ new Map();
15732
+ WIZARD_TIMEOUT_MS2 = 5 * 60 * 1e3;
15733
+ }
15734
+ });
15735
+
15736
+ // src/agents/classify.ts
15737
+ function classifyAgentIntent(message) {
15738
+ const NOT_DETECTED = { detected: false, suggestedMode: "native" };
15739
+ for (const pat of NEGATIVE_PATTERNS) {
15740
+ if (pat.test(message)) return NOT_DETECTED;
15741
+ }
15742
+ for (const pat of CLAW_SIGNAL_PATTERNS) {
15743
+ if (pat.test(message)) {
15744
+ return { detected: true, suggestedMode: "claw", reason: "cross-backend or inter-agent communication detected" };
15745
+ }
15746
+ }
15747
+ for (const pat of AGENT_SIGNAL_PATTERNS) {
15748
+ if (pat.test(message)) {
15749
+ return { detected: true, suggestedMode: "native" };
15750
+ }
15751
+ }
15752
+ return NOT_DETECTED;
15753
+ }
15754
+ var AGENT_SIGNAL_PATTERNS, CLAW_SIGNAL_PATTERNS, NEGATIVE_PATTERNS;
15755
+ var init_classify2 = __esm({
15756
+ "src/agents/classify.ts"() {
15757
+ "use strict";
15758
+ AGENT_SIGNAL_PATTERNS = [
15759
+ /\bspawn\s+(?:\d+\s+)?agents?\b/i,
15760
+ /\bsub-?agents?\b/i,
15761
+ /\bparallel\s+agents?\b/i,
15762
+ /\bfan\s+out\b/i,
15763
+ /\bdispatch\s+agents?\b/i,
15764
+ /\bmulti-?agent\b/i
15765
+ ];
15766
+ CLAW_SIGNAL_PATTERNS = [
15767
+ /\breview\s+each\s+other/i,
15768
+ /\bdiscuss\s+between\b/i,
15769
+ /\bcross-?backend\b/i,
15770
+ /\bagent\s+team\b/i,
15771
+ /\bagent\s+inbox\b/i,
15772
+ /\bagent\s+whiteboard\b/i,
15773
+ /\bagents?\s+.*debate\b/i,
15774
+ /\bclaude\b.*\bagents?\b.*\bgemini\b/i,
15775
+ /\bgemini\b.*\bagents?\b.*\bclaude\b/i,
15776
+ /\bcodex\b.*\bagents?\b.*\bclaude\b/i,
15777
+ /\bclaude\b.*\bagents?\b.*\bcodex\b/i,
15778
+ /\bgemini\b.*\bagents?\b.*\bcodex\b/i,
15779
+ /\bcodex\b.*\bagents?\b.*\bgemini\b/i
15780
+ ];
15781
+ NEGATIVE_PATTERNS = [
15782
+ /\bhow\s+does\s+.*\bwork\b/i,
15783
+ /\bwhat\s+is\s+.*\bfunction\b/i,
15784
+ /\bexplain\s+.*\bagent/i,
15785
+ /src\/agents?\//
15786
+ ];
15787
+ }
15788
+ });
15789
+
15790
+ // src/router/session-log.ts
15791
+ var session_log_exports2 = {};
15792
+ __export(session_log_exports2, {
15793
+ SessionLogFile: () => SessionLogFile,
15794
+ cleanupSessionLogs: () => cleanupSessionLogs,
15795
+ getRetentionDays: () => getRetentionDays,
15796
+ listSessionLogs: () => listSessionLogs,
15797
+ startSessionLogCleanupTimer: () => startSessionLogCleanupTimer,
15798
+ tailSessionLog: () => tailSessionLog
15799
+ });
15800
+ import { existsSync as existsSync15, mkdirSync as mkdirSync8, appendFileSync, readdirSync as readdirSync9, unlinkSync as unlinkSync6, statSync as statSync6, createReadStream } from "fs";
15801
+ import { join as join15, basename } from "path";
15802
+ import { createInterface as createInterface6 } from "readline";
15803
+ function getRetentionDays() {
15804
+ const env = process.env.SESSION_LOG_RETENTION_DAYS;
15805
+ if (env) {
15806
+ const n = parseInt(env, 10);
15807
+ if (!isNaN(n) && n > 0) return n;
15808
+ }
15809
+ return DEFAULT_RETENTION_DAYS;
15810
+ }
15811
+ function cleanupSessionLogs(retentionDays) {
15812
+ const days = retentionDays ?? getRetentionDays();
15813
+ if (!existsSync15(SESSION_LOGS_PATH)) return 0;
15814
+ const cutoff = Date.now() - days * 24 * 60 * 60 * 1e3;
15815
+ let cleaned = 0;
15816
+ try {
15817
+ for (const file of readdirSync9(SESSION_LOGS_PATH)) {
15818
+ if (!file.startsWith("session-") || !file.endsWith(".log")) continue;
15819
+ const filePath = join15(SESSION_LOGS_PATH, file);
15820
+ try {
15821
+ const { mtimeMs } = statSync6(filePath);
15822
+ if (mtimeMs < cutoff) {
15823
+ unlinkSync6(filePath);
15824
+ cleaned++;
15825
+ }
15826
+ } catch {
15827
+ }
15828
+ }
15829
+ } catch (err) {
15830
+ log(`[session-log] Cleanup failed: ${err}`);
15831
+ }
15832
+ if (cleaned > 0) {
15833
+ log(`[session-log] Cleaned ${cleaned} session log(s) older than ${days} day(s)`);
15834
+ }
15835
+ return cleaned;
15836
+ }
15837
+ function startSessionLogCleanupTimer() {
15838
+ const timer = setInterval(() => {
15839
+ cleanupSessionLogs();
15840
+ }, 24 * 60 * 60 * 1e3);
15841
+ timer.unref();
15842
+ return timer;
15843
+ }
15844
+ function listSessionLogs() {
15845
+ if (!existsSync15(SESSION_LOGS_PATH)) return [];
15846
+ const logs = [];
15847
+ for (const file of readdirSync9(SESSION_LOGS_PATH)) {
15848
+ if (!file.startsWith("session-") || !file.endsWith(".log")) continue;
15849
+ const filePath = join15(SESSION_LOGS_PATH, file);
15850
+ try {
15851
+ const stat3 = statSync6(filePath);
15852
+ const match = file.match(/^session-(.+?)-(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2})\.log$/);
15853
+ logs.push({
15854
+ filename: file,
15855
+ filePath,
15856
+ chatId: match?.[1] ?? "unknown",
15857
+ timestamp: match?.[2]?.replace(/-/g, (m, i) => i > 9 ? ":" : m) ?? "unknown",
15858
+ sizeBytes: stat3.size,
15859
+ modifiedAt: stat3.mtime
15860
+ });
15861
+ } catch {
15862
+ }
15863
+ }
15864
+ logs.sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime());
15865
+ return logs;
15866
+ }
15867
+ async function* tailSessionLog(filePath, lines = 50) {
15868
+ if (!existsSync15(filePath)) {
15869
+ yield `File not found: ${filePath}`;
15870
+ return;
15871
+ }
15872
+ const allLines = [];
15873
+ const rl2 = createInterface6({
15874
+ input: createReadStream(filePath, { encoding: "utf-8" }),
15875
+ crlfDelay: Infinity
15876
+ });
15877
+ for await (const line of rl2) {
15878
+ allLines.push(line);
15879
+ }
15880
+ const start = Math.max(0, allLines.length - lines);
15881
+ for (let i = start; i < allLines.length; i++) {
15882
+ yield allLines[i];
15883
+ }
15884
+ }
15885
+ var DEFAULT_RETENTION_DAYS, SessionLogFile;
15886
+ var init_session_log2 = __esm({
15887
+ "src/router/session-log.ts"() {
15888
+ "use strict";
15889
+ init_paths();
15471
15890
  init_log();
15472
15891
  DEFAULT_RETENTION_DAYS = 7;
15473
15892
  SessionLogFile = class {
@@ -18060,7 +18479,7 @@ Status: ${config2.enabled ? "ON" : "OFF"}`;
18060
18479
  }
18061
18480
  await channel.sendKeyboard(chatId, header2, buttons);
18062
18481
  }
18063
- async function sendSkillsPage(chatId, channel, skills2, page) {
18482
+ async function sendSkillsPage(chatId, channel, skills2, page, messageId) {
18064
18483
  const totalPages = Math.ceil(skills2.length / SKILLS_PER_PAGE);
18065
18484
  const safePage = Math.max(1, Math.min(page, totalPages));
18066
18485
  const start = (safePage - 1) * SKILLS_PER_PAGE;
@@ -18089,9 +18508,9 @@ Use /skills <page> to navigate (e.g. /skills 2)` : "";
18089
18508
  buttons.push(navRow);
18090
18509
  }
18091
18510
  const header2 = totalPages > 1 ? `${skills2.length} skills (page ${safePage}/${totalPages}). Select one to invoke:` : `${skills2.length} skills available. Select one to invoke:`;
18092
- await channel.sendKeyboard(chatId, header2, buttons);
18511
+ await sendOrEditKeyboard(chatId, channel, messageId, header2, buttons);
18093
18512
  }
18094
- async function sendMemoryPage(chatId, channel, page) {
18513
+ async function sendMemoryPage(chatId, channel, page, messageId) {
18095
18514
  const memories = listMemories();
18096
18515
  if (memories.length === 0) {
18097
18516
  await channel.sendText(chatId, "No memories stored yet.", { parseMode: "plain" });
@@ -18133,7 +18552,7 @@ async function sendMemoryPage(chatId, channel, page) {
18133
18552
  }
18134
18553
  footerRow.push({ label: "Search: /memory <query>", data: "mem:page:noop" });
18135
18554
  buttons.push(footerRow);
18136
- await channel.sendKeyboard(chatId, lines.join("\n"), buttons);
18555
+ await sendOrEditKeyboard(chatId, channel, messageId, lines.join("\n"), buttons);
18137
18556
  }
18138
18557
  async function sendHeartbeatKeyboard(chatId, channel) {
18139
18558
  const config2 = getHeartbeatConfig(chatId);
@@ -18167,7 +18586,7 @@ async function sendHeartbeatKeyboard(chatId, channel) {
18167
18586
  [{ label: "Active Hours: /heartbeat hours <start>-<end>", data: "hb:noop" }]
18168
18587
  ]);
18169
18588
  }
18170
- async function sendForgetPicker(chatId, channel, page) {
18589
+ async function sendForgetPicker(chatId, channel, page, messageId) {
18171
18590
  const memories = listMemories();
18172
18591
  if (memories.length === 0) {
18173
18592
  await channel.sendText(chatId, "No memories to forget.", { parseMode: "plain" });
@@ -18185,7 +18604,7 @@ async function sendForgetPicker(chatId, channel, page) {
18185
18604
  headerText: (p, tp, total) => `Which memory to forget? (${total} total${tp > 1 ? `, page ${p}/${tp}` : ""})`,
18186
18605
  footerButtons: [{ label: "Cancel", data: "forget:cancel" }]
18187
18606
  });
18188
- await channel.sendKeyboard(chatId, text, buttons);
18607
+ await sendOrEditKeyboard(chatId, channel, messageId, text, buttons);
18189
18608
  }
18190
18609
  function getJobScheduleText(job) {
18191
18610
  if (job.cron) return humanizeCron(job.cron);
@@ -18203,7 +18622,7 @@ function getJobStatusLabel(job) {
18203
18622
  if (!job.enabled) return "paused";
18204
18623
  return "active";
18205
18624
  }
18206
- async function sendJobsBoard(chatId, channel, page) {
18625
+ async function sendJobsBoard(chatId, channel, page, messageId) {
18207
18626
  const jobs = listJobs();
18208
18627
  if (jobs.length === 0) {
18209
18628
  await channel.sendText(chatId, "No scheduled jobs.\n\nCreate one: /schedule <description>", { parseMode: "plain" });
@@ -18245,9 +18664,9 @@ async function sendJobsBoard(chatId, channel, page) {
18245
18664
  },
18246
18665
  footerButtons: []
18247
18666
  });
18248
- await channel.sendKeyboard(chatId, text, buttons);
18667
+ await sendOrEditKeyboard(chatId, channel, messageId, text, buttons);
18249
18668
  }
18250
- async function sendJobDetail(chatId, jobId, channel) {
18669
+ async function sendJobDetail(chatId, jobId, channel, messageId) {
18251
18670
  const job = getJobById(jobId);
18252
18671
  if (!job) {
18253
18672
  if (typeof channel.sendKeyboard === "function") {
@@ -18301,9 +18720,9 @@ async function sendJobDetail(chatId, jobId, channel) {
18301
18720
  actionRow2.push({ label: "Cancel Job", data: `job:cancel:${job.id}`, style: "danger" });
18302
18721
  }
18303
18722
  const buttons = [actionRow1, actionRow2, [{ label: "\u2190 Back to List", data: "job:back" }]];
18304
- await channel.sendKeyboard(chatId, text, buttons);
18723
+ await sendOrEditKeyboard(chatId, channel, messageId, text, buttons);
18305
18724
  }
18306
- async function sendJobRunsView(chatId, jobId, channel, page) {
18725
+ async function sendJobRunsView(chatId, jobId, channel, page, messageId) {
18307
18726
  const runs = getJobRuns(jobId, 50);
18308
18727
  if (runs.length === 0) {
18309
18728
  const msg = `No runs for job #${jobId}.`;
@@ -18363,7 +18782,7 @@ async function sendJobRunsView(chatId, jobId, channel, page) {
18363
18782
  },
18364
18783
  footerButtons: [{ label: "\u2190 Back to Job", data: `job:view:${jobId}` }]
18365
18784
  });
18366
- await channel.sendKeyboard(chatId, text, buttons);
18785
+ await sendOrEditKeyboard(chatId, channel, messageId, text, buttons);
18367
18786
  }
18368
18787
  async function sendJobPicker(chatId, channel, action) {
18369
18788
  const allJobs = listJobs();
@@ -18807,13 +19226,13 @@ async function handleEvolveCallback(chatId, data, channel) {
18807
19226
  const { getReflectionStatus: getReflectionStatus2, setReflectionStatus: setReflectionStatus2 } = await Promise.resolve().then(() => (init_store4(), store_exports4));
18808
19227
  const current = getReflectionStatus2(getDb(), chatId);
18809
19228
  if (current === "frozen") {
18810
- const { readFileSync: readFileSync27, existsSync: existsSync56 } = await import("fs");
19229
+ const { readFileSync: readFileSync28, existsSync: existsSync56 } = await import("fs");
18811
19230
  const { join: join35 } = await import("path");
18812
19231
  const { CC_CLAW_HOME: CC_CLAW_HOME3 } = await Promise.resolve().then(() => (init_paths(), paths_exports));
18813
19232
  const soulPath = join35(CC_CLAW_HOME3, "identity/SOUL.md");
18814
19233
  const userPath = join35(CC_CLAW_HOME3, "identity/USER.md");
18815
- const soul = existsSync56(soulPath) ? readFileSync27(soulPath, "utf-8") : "";
18816
- const user = existsSync56(userPath) ? readFileSync27(userPath, "utf-8") : "";
19234
+ const soul = existsSync56(soulPath) ? readFileSync28(soulPath, "utf-8") : "";
19235
+ const user = existsSync56(userPath) ? readFileSync28(userPath, "utf-8") : "";
18817
19236
  setReflectionStatus2(getDb(), chatId, "active", soul, user);
18818
19237
  const { logActivity: logActivity2 } = await Promise.resolve().then(() => (init_store3(), store_exports3));
18819
19238
  logActivity2(getDb(), { chatId, source: "telegram", eventType: "reflection_unfrozen", summary: "Reflection enabled" });
@@ -20907,1081 +21326,809 @@ async function handleMemoryCommand(chatId, commandArgs, msg, channel) {
20907
21326
  await channel.sendText(chatId, `Memory #${editId} updated.`, { parseMode: "plain" });
20908
21327
  return;
20909
21328
  }
20910
- const memories = listMemories();
20911
- if (memories.length === 0) {
20912
- await channel.sendText(chatId, "No memories stored yet.", { parseMode: "plain" });
20913
- return;
20914
- }
20915
- if (typeof channel.sendKeyboard === "function") {
20916
- await sendMemoryPage(chatId, channel, 1);
20917
- } else {
20918
- const lines = memories.map(
20919
- (m) => `- ${m.trigger}: ${m.content} (salience: ${m.salience.toFixed(2)})`
20920
- );
20921
- await channel.sendText(chatId, lines.join("\n"), { parseMode: "plain" });
20922
- }
20923
- }
20924
- async function handleImagineCommand(chatId, commandArgs, msg, channel) {
20925
- if (!commandArgs) {
20926
- await channel.sendText(chatId, "Usage: /imagine <prompt>\nExample: /imagine a cat astronaut on Mars", { parseMode: "plain" });
20927
- return;
20928
- }
20929
- if (!isImageGenAvailable()) {
20930
- await channel.sendText(chatId, "Image generation requires GEMINI_API_KEY. Configure it in ~/.cc-claw/.env", { parseMode: "plain" });
20931
- return;
20932
- }
20933
- await channel.sendText(chatId, "\u{1F3A8} Generating image\u2026", { parseMode: "plain" });
20934
- try {
20935
- const result = await generateImage(commandArgs);
20936
- const file = await readFile6(result.filePath);
20937
- const name = result.filePath.split("/").pop() ?? "image.png";
20938
- await channel.sendFile(chatId, file, name);
20939
- if (result.text) {
20940
- await channel.sendText(chatId, result.text, { parseMode: "plain" });
20941
- }
20942
- } catch (err) {
20943
- await channel.sendText(chatId, `Image generation failed: ${errorMessage(err)}`, { parseMode: "plain" });
20944
- }
20945
- }
20946
- async function handleRunsCommand(chatId, commandArgs, msg, channel) {
20947
- const runsJobId = commandArgs ? parseInt(commandArgs, 10) : void 0;
20948
- if (runsJobId) {
20949
- await sendJobRunsView(chatId, runsJobId, channel, 1);
20950
- } else {
20951
- const runs = getJobRuns(void 0, 10);
20952
- if (runs.length === 0) {
20953
- await channel.sendText(chatId, "No run history yet.", { parseMode: "plain" });
20954
- return;
20955
- }
20956
- const lines = runs.map((r) => {
20957
- const duration = r.durationMs ? ` (${(r.durationMs / 1e3).toFixed(1)}s)` : "";
20958
- const error3 = r.error ? `
20959
- Error: ${r.error.slice(0, 100)}` : "";
20960
- const usage2 = r.usageInput ? `
20961
- Tokens: ${r.usageInput}in / ${r.usageOutput}out` : "";
20962
- return `#${r.jobId} [${r.status}] ${formatLocalDateTime(r.startedAt)}${duration}${error3}${usage2}`;
20963
- });
20964
- await channel.sendText(chatId, lines.join("\n\n"), { parseMode: "plain" });
20965
- }
20966
- }
20967
- async function handleSkillsCommand(chatId, commandArgs, msg, channel) {
20968
- const skills2 = await discoverAllSkills();
20969
- if (skills2.length === 0) {
20970
- await channel.sendText(chatId, "No skills found. Install skills with /skill-install <github-url> or place them in ~/.cc-claw/workspace/skills/", { parseMode: "plain" });
20971
- return;
20972
- }
20973
- const page = commandArgs ? parseInt(commandArgs, 10) || 1 : 1;
20974
- await sendSkillsPage(chatId, channel, skills2, page);
20975
- }
20976
- async function handleChatsCommand(chatId, commandArgs, msg, channel) {
20977
- if (!commandArgs) {
20978
- const aliases = getAllChatAliases();
20979
- if (typeof channel.sendKeyboard === "function") {
20980
- if (aliases.length === 0) {
20981
- await channel.sendKeyboard(
20982
- chatId,
20983
- "Authorized Chats\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n\nNo chat aliases configured yet.\nGroup chats are auto-discovered when the bot receives a message.",
20984
- [[{ label: "Refresh", data: "chats:refresh", style: "primary" }]]
20985
- );
20986
- } else {
20987
- const lines = aliases.map(
20988
- (a) => `${a.alias.padEnd(12)} | ${a.chatId}${a.chatId === chatId ? " [Active]" : ""}`
20989
- );
20990
- const text = ["Authorized Chats", "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501", "", ...lines].join("\n");
20991
- const buttons = [];
20992
- const actionRow = [];
20993
- if (aliases.length > 0) {
20994
- actionRow.push({ label: "Remove", data: "chats:remove", style: "danger" });
20995
- }
20996
- actionRow.push({ label: "Refresh", data: "chats:refresh", style: "primary" });
20997
- buttons.push(actionRow);
20998
- await channel.sendKeyboard(chatId, text, buttons);
20999
- }
21000
- } else {
21001
- if (aliases.length === 0) {
21002
- await channel.sendText(chatId, "No chat aliases configured yet.\nUsage: /chats name <chat_id> <alias>\n\nGroup chats are auto-discovered when the bot receives a message from them.", { parseMode: "plain" });
21003
- } else {
21004
- const lines = ["Authorized chats:", "", ...aliases.map((a) => ` ${a.alias} -> ${a.chatId}`)];
21005
- await channel.sendText(chatId, lines.join("\n"), { parseMode: "plain" });
21006
- }
21007
- }
21008
- return;
21009
- }
21010
- const parts = commandArgs.split(/\s+/);
21011
- if (parts[0] === "name" && parts.length >= 3) {
21012
- const targetChatId = parts[1];
21013
- const alias = parts.slice(2).join("-").toLowerCase();
21014
- setChatAlias(alias, targetChatId);
21015
- await channel.sendText(chatId, `Alias set: ${alias} -> ${targetChatId}`, { parseMode: "plain" });
21016
- } else if (parts[0] === "remove" && parts[1]) {
21017
- const removed = removeChatAlias(parts[1]);
21018
- await channel.sendText(chatId, removed ? `Alias "${parts[1]}" removed.` : `Alias "${parts[1]}" not found.`, { parseMode: "plain" });
21019
- } else {
21020
- await channel.sendText(chatId, "Usage:\n /chats \u2014 list all aliases\n /chats name <chat_id> <alias> \u2014 assign alias\n /chats remove <alias> \u2014 remove alias", { parseMode: "plain" });
21021
- }
21022
- }
21023
- async function handleCwdCommand(chatId, commandArgs, msg, channel) {
21024
- if (!commandArgs) {
21025
- const current = getCwd(chatId);
21026
- const recents = getRecentBookmarks(chatId, 10);
21027
- if (recents.length === 0) {
21028
- await channel.sendText(
21029
- chatId,
21030
- current ? `Working directory: ${current}
21031
-
21032
- No saved bookmarks yet. Set a directory with /cwd <path> to auto-save.` : "No working directory set. Usage: /cwd ~/projects/my-app",
21033
- { parseMode: "plain" }
21034
- );
21035
- return;
21036
- }
21037
- const text = current ? `Current: ${current}
21038
-
21039
- Recent directories:` : "Recent directories:";
21040
- if (typeof channel.sendKeyboard === "function") {
21041
- const buttons = recents.map((r) => [{ label: r.alias, data: `cwdpick:${r.alias}` }]);
21042
- await channel.sendKeyboard(chatId, text, buttons);
21043
- } else {
21044
- const list = recents.map((r) => ` ${r.alias} \u2192 ${r.path}`).join("\n");
21045
- await channel.sendText(chatId, `${text}
21046
- ${list}`, { parseMode: "plain" });
21047
- }
21048
- return;
21049
- }
21050
- if (commandArgs === "reset" || commandArgs === "clear") {
21051
- clearCwd(chatId);
21052
- await channel.sendText(chatId, "Working directory cleared. Using default.", { parseMode: "plain" });
21053
- return;
21054
- }
21055
- if (commandArgs === "aliases") {
21056
- const all = getAllBookmarks(chatId);
21057
- if (all.length === 0) {
21058
- await channel.sendText(chatId, "No bookmarks saved yet.", { parseMode: "plain" });
21059
- return;
21060
- }
21061
- const lines = all.map((b) => ` ${b.manual ? "[manual]" : "[auto]"} ${b.alias} \u2192 ${b.path}`);
21062
- await channel.sendText(chatId, `Directory bookmarks:
21063
- ${lines.join("\n")}`, { parseMode: "plain" });
21064
- return;
21065
- }
21066
- if (commandArgs.startsWith("unalias ")) {
21067
- const aliasName = commandArgs.slice(8).trim();
21068
- if (!aliasName) {
21069
- await channel.sendText(chatId, "Usage: /cwd unalias <name>", { parseMode: "plain" });
21070
- return;
21071
- }
21072
- const deleted = deleteBookmark(chatId, aliasName);
21073
- await channel.sendText(chatId, deleted ? `Bookmark '${aliasName}' removed.` : `Bookmark '${aliasName}' not found.`, { parseMode: "plain" });
21074
- return;
21075
- }
21076
- if (commandArgs.startsWith("alias ")) {
21077
- const parts = commandArgs.slice(6).trim().split(/\s+/);
21078
- if (parts.length < 2) {
21079
- await channel.sendText(chatId, "Usage: /cwd alias <name> <path>", { parseMode: "plain" });
21080
- return;
21081
- }
21082
- const [aliasName, ...pathParts] = parts;
21083
- const aliasPath = pathParts.join(" ").replace(/^~/, process.env.HOME ?? "");
21084
- upsertBookmark(chatId, aliasName, aliasPath, true);
21085
- await channel.sendText(chatId, `Bookmark saved: ${aliasName} \u2192 ${aliasPath}`, { parseMode: "plain" });
21086
- return;
21087
- }
21088
- const arg = commandArgs;
21089
- if (arg.startsWith("/") || arg.startsWith("~")) {
21090
- const resolvedPath = arg.startsWith("~") ? arg.replace("~", process.env.HOME ?? "") : arg;
21091
- setCwd(chatId, resolvedPath);
21092
- const basename4 = resolvedPath.split("/").filter(Boolean).pop();
21093
- if (basename4) upsertBookmark(chatId, basename4, resolvedPath, false);
21094
- logActivity(getDb(), { chatId, source: "telegram", eventType: "config_changed", summary: `Working directory set to ${resolvedPath}`, detail: { field: "cwd", value: resolvedPath } });
21095
- await sendCwdSessionChoice(chatId, resolvedPath, channel);
21096
- return;
21097
- }
21098
- const exact = getBookmark(chatId, arg);
21099
- if (exact) {
21100
- setCwd(chatId, exact.path);
21101
- touchBookmark(chatId, arg);
21102
- logActivity(getDb(), { chatId, source: "telegram", eventType: "config_changed", summary: `Working directory set to ${exact.path}`, detail: { field: "cwd", value: exact.path } });
21103
- await sendCwdSessionChoice(chatId, exact.path, channel);
21329
+ const memories = listMemories();
21330
+ if (memories.length === 0) {
21331
+ await channel.sendText(chatId, "No memories stored yet.", { parseMode: "plain" });
21104
21332
  return;
21105
21333
  }
21106
- const matches = findBookmarksByPrefix(chatId, arg);
21107
- if (matches.length === 1) {
21108
- setCwd(chatId, matches[0].path);
21109
- touchBookmark(chatId, matches[0].alias);
21110
- logActivity(getDb(), { chatId, source: "telegram", eventType: "config_changed", summary: `Working directory set to ${matches[0].path}`, detail: { field: "cwd", value: matches[0].path } });
21111
- await sendCwdSessionChoice(chatId, matches[0].path, channel);
21334
+ if (typeof channel.sendKeyboard === "function") {
21335
+ await sendMemoryPage(chatId, channel, 1);
21336
+ } else {
21337
+ const lines = memories.map(
21338
+ (m) => `- ${m.trigger}: ${m.content} (salience: ${m.salience.toFixed(2)})`
21339
+ );
21340
+ await channel.sendText(chatId, lines.join("\n"), { parseMode: "plain" });
21341
+ }
21342
+ }
21343
+ async function handleImagineCommand(chatId, commandArgs, msg, channel) {
21344
+ if (!commandArgs) {
21345
+ await channel.sendText(chatId, "Usage: /imagine <prompt>\nExample: /imagine a cat astronaut on Mars", { parseMode: "plain" });
21112
21346
  return;
21113
21347
  }
21114
- if (matches.length > 1 && typeof channel.sendKeyboard === "function") {
21115
- const buttons = matches.map((m) => [{ label: `${m.alias} \u2192 ${m.path}`, data: `cwdpick:${m.alias}` }]);
21116
- await channel.sendKeyboard(chatId, `Multiple matches for "${arg}":`, buttons);
21348
+ if (!isImageGenAvailable()) {
21349
+ await channel.sendText(chatId, "Image generation requires GEMINI_API_KEY. Configure it in ~/.cc-claw/.env", { parseMode: "plain" });
21117
21350
  return;
21118
21351
  }
21119
- await channel.sendText(chatId, `Directory alias '${arg}' not found. Use /cwd aliases to see saved bookmarks.`, { parseMode: "plain" });
21120
- }
21121
- async function handleSummarizerCommand(chatId, commandArgs, msg, channel) {
21122
- let adapter;
21352
+ await channel.sendText(chatId, "\u{1F3A8} Generating image\u2026", { parseMode: "plain" });
21123
21353
  try {
21124
- adapter = getAdapterForChat(chatId);
21125
- } catch {
21126
- adapter = null;
21127
- }
21128
- const current = getSummarizer(chatId);
21129
- const currentLabel = current.backend === "off" ? "Off" : current.backend ? `${current.backend}:${current.model ?? "default"}` : `Auto (${adapter?.summarizerModel ?? "default"})`;
21130
- if (typeof channel.sendKeyboard === "function") {
21131
- const isAuto = !current.backend && current.backend !== "off";
21132
- const isOff = current.backend === "off";
21133
- const buttons = [
21134
- [{ label: `Auto (use active backend)${isAuto ? " \u2713" : ""}`, data: "summarizer:auto", ...isAuto ? { style: "primary" } : {} }],
21135
- [{ label: `Off (disable)${isOff ? " \u2713" : ""}`, data: "summarizer:off", ...isOff ? { style: "primary" } : {} }]
21136
- ];
21137
- for (const a of getAvailableAdapters()) {
21138
- const pinned = current.backend === a.id;
21139
- buttons.push([{
21140
- label: `${pinned ? "\u2713 " : ""}${a.displayName}: ${a.summarizerModel}`,
21141
- data: `summarizer:${a.id}:${a.summarizerModel}`,
21142
- ...pinned ? { style: "primary" } : {}
21143
- }]);
21354
+ const result = await generateImage(commandArgs);
21355
+ const file = await readFile6(result.filePath);
21356
+ const name = result.filePath.split("/").pop() ?? "image.png";
21357
+ await channel.sendFile(chatId, file, name);
21358
+ if (result.text) {
21359
+ await channel.sendText(chatId, result.text, { parseMode: "plain" });
21144
21360
  }
21145
- await channel.sendKeyboard(chatId, `Session summarizer (current: ${currentLabel}):`, buttons);
21146
- } else {
21147
- await channel.sendText(chatId, `Summarizer: ${currentLabel}
21148
-
21149
- Use /summarizer auto, /summarizer off, or /summarizer <backend>:<model>`, { parseMode: "plain" });
21361
+ } catch (err) {
21362
+ await channel.sendText(chatId, `Image generation failed: ${errorMessage(err)}`, { parseMode: "plain" });
21150
21363
  }
21151
21364
  }
21152
- async function handleGeminiAccountsCommand(chatId, commandArgs, msg, channel) {
21153
- const slots = getGeminiSlots();
21154
- if (slots.length === 0) {
21155
- 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" });
21156
- return;
21157
- }
21158
- if (typeof channel.sendKeyboard === "function") {
21159
- const rows = buildAccountSlotKeyboard(
21160
- slots,
21161
- getChatGeminiSlotId(chatId),
21162
- getGeminiRotationMode(),
21163
- "gslot:",
21164
- "grotation:"
21165
- );
21166
- await channel.sendKeyboard(chatId, "Gemini Accounts & Rotation:", rows);
21365
+ async function handleRunsCommand(chatId, commandArgs, msg, channel) {
21366
+ const runsJobId = commandArgs ? parseInt(commandArgs, 10) : void 0;
21367
+ if (runsJobId) {
21368
+ await sendJobRunsView(chatId, runsJobId, channel, 1);
21167
21369
  } else {
21168
- const currentMode = getGeminiRotationMode();
21169
- const list = slots.filter((s) => s.enabled).map((s) => {
21170
- const icon = s.slotType === "oauth" ? "\u{1F468}\u{1F3FD}\u200D\u{1F4BB}" : "\u{1F511}";
21171
- return `${icon} ${s.label || `slot-${s.id}`} (#${s.id})`;
21172
- }).join("\n");
21173
- await channel.sendText(chatId, `Slots:
21174
- ${list}
21175
-
21176
- Rotation mode: ${currentMode}
21177
- Use: /gemini_accounts <name> to pin`, { parseMode: "plain" });
21370
+ const runs = getJobRuns(void 0, 10);
21371
+ if (runs.length === 0) {
21372
+ await channel.sendText(chatId, "No run history yet.", { parseMode: "plain" });
21373
+ return;
21374
+ }
21375
+ const lines = runs.map((r) => {
21376
+ const duration = r.durationMs ? ` (${(r.durationMs / 1e3).toFixed(1)}s)` : "";
21377
+ const error3 = r.error ? `
21378
+ Error: ${r.error.slice(0, 100)}` : "";
21379
+ const usage2 = r.usageInput ? `
21380
+ Tokens: ${r.usageInput}in / ${r.usageOutput}out` : "";
21381
+ return `#${r.jobId} [${r.status}] ${formatLocalDateTime(r.startedAt)}${duration}${error3}${usage2}`;
21382
+ });
21383
+ await channel.sendText(chatId, lines.join("\n\n"), { parseMode: "plain" });
21178
21384
  }
21179
21385
  }
21180
- async function handleBackendAccountsCommand(chatId, commandArgs, msg, channel) {
21181
- const command = msg.command;
21182
- const slotBackend = command === "claude_accounts" ? "claude" : "codex";
21183
- const slotDisplayName = command === "claude_accounts" ? "Claude" : "Codex";
21184
- const slots = getBackendSlots(slotBackend);
21185
- if (slots.length === 0) {
21186
- await channel.sendText(chatId, `No ${slotDisplayName} credentials configured.
21187
- Add with: <code>cc-claw ${slotBackend} add-key</code>`, { parseMode: "html" });
21386
+ async function handleSkillsCommand(chatId, commandArgs, msg, channel) {
21387
+ const skills2 = await discoverAllSkills();
21388
+ if (skills2.length === 0) {
21389
+ await channel.sendText(chatId, "No skills found. Install skills with /skill-install <github-url> or place them in ~/.cc-claw/workspace/skills/", { parseMode: "plain" });
21188
21390
  return;
21189
21391
  }
21190
- if (typeof channel.sendKeyboard === "function") {
21191
- const rows = buildAccountSlotKeyboard(
21192
- slots,
21193
- getChatBackendSlotId(chatId, slotBackend),
21194
- getBackendRotationMode(slotBackend),
21195
- `bslot:${slotBackend}:`,
21196
- `brotation:${slotBackend}:`
21197
- );
21198
- await channel.sendKeyboard(chatId, `${slotDisplayName} Accounts & Rotation:`, rows);
21199
- } else {
21200
- const currentMode = getBackendRotationMode(slotBackend);
21201
- const list = slots.filter((s) => s.enabled).map((s) => {
21202
- const icon = s.slotType === "oauth" ? "\u{1F468}\u{1F3FD}\u200D\u{1F4BB}" : "\u{1F511}";
21203
- return `${icon} ${s.label || `slot-${s.id}`} (#${s.id})`;
21204
- }).join("\n");
21205
- await channel.sendText(chatId, `${slotDisplayName} Slots:
21206
- ${list}
21207
-
21208
- Rotation mode: ${currentMode}
21209
- Use: /${command} <name> to pin`, { parseMode: "plain" });
21210
- }
21211
- }
21212
- async function handleDebugCommand(chatId, commandArgs, msg, channel) {
21213
- const { getSessionLogEnabled: getSessionLogEnabled2, toggleSessionLogEnabled: toggleSessionLogEnabled2 } = await Promise.resolve().then(() => (init_chat_settings(), chat_settings_exports));
21214
- const current = getSessionLogEnabled2(chatId);
21215
- if (commandArgs === "on" || commandArgs === "off") {
21216
- const { setSessionLogEnabled: setSessionLogEnabled2 } = await Promise.resolve().then(() => (init_chat_settings(), chat_settings_exports));
21217
- const value = commandArgs === "on";
21218
- setSessionLogEnabled2(chatId, value);
21219
- await channel.sendText(chatId, `\u{1F52C} Session debug logging: ${value ? "ON" : "OFF"}
21220
-
21221
- ${value ? "Full tool inputs/results will be saved to ~/.cc-claw/logs/sessions/\nUse 'cc-claw logs session list' or 'cc-claw logs session tail' to inspect." : "Session logs disabled."}`, { parseMode: "plain" });
21222
- } else if (typeof channel.sendKeyboard === "function") {
21223
- await channel.sendKeyboard(
21224
- chatId,
21225
- `\u{1F52C} Session Debug Logging
21226
-
21227
- Records full, untruncated tool inputs and results to disk for debugging.
21228
- Logs: ~/.cc-claw/logs/sessions/
21229
- Retention: ${process.env.SESSION_LOG_RETENTION_DAYS ?? "7"} day(s)
21230
-
21231
- Currently: ${current ? "ON" : "OFF"}`,
21232
- [[
21233
- { label: current ? "\u2713 ON" : "ON", data: "debug_log:on", ...current ? { style: "primary" } : {} },
21234
- { label: !current ? "\u2713 OFF" : "OFF", data: "debug_log:off", ...!current ? { style: "primary" } : {} }
21235
- ]]
21236
- );
21237
- } else {
21238
- const next = toggleSessionLogEnabled2(chatId);
21239
- await channel.sendText(chatId, `\u{1F52C} Session debug logging: ${next ? "ON" : "OFF"}`, { parseMode: "plain" });
21240
- }
21392
+ const page = commandArgs ? parseInt(commandArgs, 10) || 1 : 1;
21393
+ await sendSkillsPage(chatId, channel, skills2, page);
21241
21394
  }
21242
- async function handleHeartbeatCommand(chatId, commandArgs, msg, channel) {
21243
- if (commandArgs) {
21244
- const { action, value } = parseHeartbeatCommand(chatId, commandArgs);
21245
- switch (action) {
21246
- case "on": {
21247
- setHeartbeatConfig(chatId, { enabled: true });
21248
- startHeartbeatForChat(chatId);
21249
- if (typeof channel.sendKeyboard === "function") {
21250
- await sendHeartbeatKeyboard(chatId, channel);
21251
- } else {
21252
- await channel.sendText(chatId, "Heartbeat enabled. I'll check in periodically.", { parseMode: "plain" });
21253
- }
21254
- return;
21255
- }
21256
- case "off": {
21257
- setHeartbeatConfig(chatId, { enabled: false });
21258
- stopHeartbeatForChat(chatId);
21259
- if (typeof channel.sendKeyboard === "function") {
21260
- await sendHeartbeatKeyboard(chatId, channel);
21261
- } else {
21262
- await channel.sendText(chatId, "Heartbeat disabled.", { parseMode: "plain" });
21263
- }
21264
- return;
21265
- }
21266
- case "interval": {
21267
- const ms = parseIntervalToMs(value ?? "30m");
21268
- if (!ms || ms < 6e4) {
21269
- await channel.sendText(chatId, "Invalid interval. Use e.g. 15m, 30m, 1h, 2h", { parseMode: "plain" });
21270
- return;
21271
- }
21272
- setHeartbeatConfig(chatId, { intervalMs: ms });
21273
- stopHeartbeatForChat(chatId);
21274
- const hbConf = getHeartbeatConfig(chatId);
21275
- if (hbConf?.enabled) startHeartbeatForChat(chatId);
21276
- if (typeof channel.sendKeyboard === "function") {
21277
- await sendHeartbeatKeyboard(chatId, channel);
21278
- } else {
21279
- await channel.sendText(chatId, `Heartbeat interval set to ${ms / 6e4} minutes.`, { parseMode: "plain" });
21395
+ async function handleChatsCommand(chatId, commandArgs, msg, channel) {
21396
+ if (!commandArgs) {
21397
+ const aliases = getAllChatAliases();
21398
+ if (typeof channel.sendKeyboard === "function") {
21399
+ if (aliases.length === 0) {
21400
+ await channel.sendKeyboard(
21401
+ chatId,
21402
+ "Authorized Chats\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n\nNo chat aliases configured yet.\nGroup chats are auto-discovered when the bot receives a message.",
21403
+ [[{ label: "Refresh", data: "chats:refresh", style: "primary" }]]
21404
+ );
21405
+ } else {
21406
+ const lines = aliases.map(
21407
+ (a) => `${a.alias.padEnd(12)} | ${a.chatId}${a.chatId === chatId ? " [Active]" : ""}`
21408
+ );
21409
+ const text = ["Authorized Chats", "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501", "", ...lines].join("\n");
21410
+ const buttons = [];
21411
+ const actionRow = [];
21412
+ if (aliases.length > 0) {
21413
+ actionRow.push({ label: "Remove", data: "chats:remove", style: "danger" });
21280
21414
  }
21281
- return;
21415
+ actionRow.push({ label: "Refresh", data: "chats:refresh", style: "primary" });
21416
+ buttons.push(actionRow);
21417
+ await channel.sendKeyboard(chatId, text, buttons);
21282
21418
  }
21283
- case "hours": {
21284
- const hourMatch = (value ?? "").match(/^(\d{1,2})-(\d{1,2})$/);
21285
- if (!hourMatch) {
21286
- await channel.sendText(chatId, "Usage: /heartbeat hours 8-22", { parseMode: "plain" });
21287
- return;
21288
- }
21289
- const hbStart = `${hourMatch[1].padStart(2, "0")}:00`;
21290
- const hbEnd = `${hourMatch[2].padStart(2, "0")}:00`;
21291
- setHeartbeatConfig(chatId, { activeStart: hbStart, activeEnd: hbEnd });
21292
- if (typeof channel.sendKeyboard === "function") {
21293
- await sendHeartbeatKeyboard(chatId, channel);
21294
- } else {
21295
- await channel.sendText(chatId, `Active hours: ${hbStart} - ${hbEnd}`, { parseMode: "plain" });
21296
- }
21297
- return;
21419
+ } else {
21420
+ if (aliases.length === 0) {
21421
+ await channel.sendText(chatId, "No chat aliases configured yet.\nUsage: /chats name <chat_id> <alias>\n\nGroup chats are auto-discovered when the bot receives a message from them.", { parseMode: "plain" });
21422
+ } else {
21423
+ const lines = ["Authorized chats:", "", ...aliases.map((a) => ` ${a.alias} -> ${a.chatId}`)];
21424
+ await channel.sendText(chatId, lines.join("\n"), { parseMode: "plain" });
21298
21425
  }
21299
- default:
21300
- if (typeof channel.sendKeyboard === "function") {
21301
- await sendHeartbeatKeyboard(chatId, channel);
21302
- } else {
21303
- await channel.sendText(chatId, formatHeartbeatStatus(chatId), { parseMode: "plain" });
21304
- }
21305
- return;
21306
21426
  }
21427
+ return;
21307
21428
  }
21308
- if (typeof channel.sendKeyboard === "function") {
21309
- await sendHeartbeatKeyboard(chatId, channel);
21429
+ const parts = commandArgs.split(/\s+/);
21430
+ if (parts[0] === "name" && parts.length >= 3) {
21431
+ const targetChatId = parts[1];
21432
+ const alias = parts.slice(2).join("-").toLowerCase();
21433
+ setChatAlias(alias, targetChatId);
21434
+ await channel.sendText(chatId, `Alias set: ${alias} -> ${targetChatId}`, { parseMode: "plain" });
21435
+ } else if (parts[0] === "remove" && parts[1]) {
21436
+ const removed = removeChatAlias(parts[1]);
21437
+ await channel.sendText(chatId, removed ? `Alias "${parts[1]}" removed.` : `Alias "${parts[1]}" not found.`, { parseMode: "plain" });
21310
21438
  } else {
21311
- await channel.sendText(chatId, formatHeartbeatStatus(chatId), { parseMode: "plain" });
21439
+ await channel.sendText(chatId, "Usage:\n /chats \u2014 list all aliases\n /chats name <chat_id> <alias> \u2014 assign alias\n /chats remove <alias> \u2014 remove alias", { parseMode: "plain" });
21312
21440
  }
21313
21441
  }
21314
- async function handleAgentsCommand(chatId, commandArgs, msg, channel) {
21315
- if (commandArgs?.startsWith("mode")) {
21316
- const modeArg = commandArgs.slice(5).trim().toLowerCase();
21317
- if (modeArg === "native" || modeArg === "claw" || modeArg === "auto") {
21318
- setAgentMode(chatId, modeArg);
21319
- try {
21320
- await summarizeSession(chatId);
21321
- } catch {
21322
- }
21323
- clearSession(chatId);
21324
- await channel.sendText(chatId, `Agent mode set to <b>${modeArg}</b>. Session cleared.`, { parseMode: "html" });
21325
- } else if (typeof channel.sendKeyboard === "function") {
21326
- const current = getAgentMode(chatId);
21327
- await channel.sendKeyboard(chatId, `Agent mode: <b>${current}</b>
21442
+ async function handleCwdCommand(chatId, commandArgs, msg, channel) {
21443
+ if (!commandArgs) {
21444
+ const current = getCwd(chatId);
21445
+ const recents = getRecentBookmarks(chatId, 10);
21446
+ if (recents.length === 0) {
21447
+ await channel.sendText(
21448
+ chatId,
21449
+ current ? `Working directory: ${current}
21328
21450
 
21329
- Choose a mode:`, [
21330
- [
21331
- { label: "Auto (recommended)", data: "agentmode:auto", ...current === "auto" ? { style: "primary" } : {} },
21332
- { label: "Native (fast)", data: "agentmode:native", ...current === "native" ? { style: "primary" } : {} },
21333
- { label: "Orchestrated", data: "agentmode:claw", ...current === "claw" ? { style: "primary" } : {} }
21334
- ]
21335
- ]);
21336
- } else {
21337
- const current = getAgentMode(chatId);
21338
- await channel.sendText(chatId, `Agent mode: ${current}. Use /agents mode <auto|native|claw> to change.`, { parseMode: "plain" });
21451
+ No saved bookmarks yet. Set a directory with /cwd <path> to auto-save.` : "No working directory set. Usage: /cwd ~/projects/my-app",
21452
+ { parseMode: "plain" }
21453
+ );
21454
+ return;
21339
21455
  }
21340
- return;
21341
- }
21342
- if (commandArgs?.startsWith("history")) {
21343
- const db4 = getDb();
21344
- const events = db4.prepare(`
21345
- SELECT summary, detail, createdAt FROM activity_log
21346
- WHERE chatId = ? AND eventType = 'native_subagent'
21347
- AND createdAt > datetime('now', '-1 day')
21348
- ORDER BY createdAt DESC LIMIT 20
21349
- `).all(chatId);
21350
- if (events.length === 0) {
21351
- await channel.sendText(chatId, "No native sub-agent activity in the last 24h.", { parseMode: "plain" });
21352
- } else {
21353
- const lines2 = events.map((e) => {
21354
- const d = e.detail ? JSON.parse(e.detail) : {};
21355
- return `\u2022 ${e.summary} (${d.backend ?? "?"}, ${e.createdAt})`;
21356
- });
21357
- await channel.sendText(chatId, `<b>Native agent history (24h)</b>
21456
+ const text = current ? `Current: ${current}
21358
21457
 
21359
- ${lines2.join("\n")}`, { parseMode: "html" });
21458
+ Recent directories:` : "Recent directories:";
21459
+ if (typeof channel.sendKeyboard === "function") {
21460
+ const buttons = recents.map((r) => [{ label: r.alias, data: `cwdpick:${r.alias}` }]);
21461
+ await channel.sendKeyboard(chatId, text, buttons);
21462
+ } else {
21463
+ const list = recents.map((r) => ` ${r.alias} \u2192 ${r.path}`).join("\n");
21464
+ await channel.sendText(chatId, `${text}
21465
+ ${list}`, { parseMode: "plain" });
21360
21466
  }
21361
21467
  return;
21362
21468
  }
21363
- const db3 = getDb();
21364
- const agents2 = listActiveAgents(db3);
21365
- const STATUS_EMOJI = {
21366
- running: "\u{1F7E2}",
21367
- queued: "\u{1F7E1}",
21368
- starting: "\u{1F535}",
21369
- idle: "\u26AA",
21370
- failed: "\u274C",
21371
- cancelled: "\u{1F6AB}"
21372
- };
21373
- const runners2 = getAllRunners();
21374
- const runnerMap = new Map(runners2.map((r) => [r.id, r]));
21375
- if (agents2.length === 0) {
21376
- await channel.sendText(chatId, "<b>Active Agents</b>\n\nNo active agents.", { parseMode: "html" });
21469
+ if (commandArgs === "reset" || commandArgs === "clear") {
21470
+ clearCwd(chatId);
21471
+ await channel.sendText(chatId, "Working directory cleared. Using default.", { parseMode: "plain" });
21377
21472
  return;
21378
21473
  }
21379
- const { getAllAdapters: getAllAdaptersImport } = await Promise.resolve().then(() => (init_backends(), backends_exports));
21380
- const defaultModelMap = /* @__PURE__ */ new Map();
21381
- for (const adapterItem of getAllAdaptersImport()) {
21382
- defaultModelMap.set(adapterItem.id, adapterItem.defaultModel);
21383
- }
21384
- const lines = ["<b>Active Agents</b>", ""];
21385
- let runningCount = 0;
21386
- let queuedCount = 0;
21387
- for (const a of agents2) {
21388
- const emoji = STATUS_EMOJI[a.status] ?? "\u26AA";
21389
- const runner = runnerMap.get(a.runnerId);
21390
- const displayName = runner?.displayName ?? a.runnerId;
21391
- const modelId = a.model ?? defaultModelMap.get(a.runnerId) ?? "";
21392
- const modelLabel = shortModelName(modelId);
21393
- const shortId = a.id.slice(0, 8);
21394
- if (a.status === "running") runningCount++;
21395
- if (a.status === "queued") queuedCount++;
21396
- const agentLabel = a.name ?? shortId;
21397
- const header2 = modelLabel ? `${emoji} ${agentLabel} (${displayName} | ${modelLabel}) \u2014 ${a.status}` : `${emoji} ${agentLabel} (${displayName}) \u2014 ${a.status}`;
21398
- lines.push(header2);
21399
- if (a.name) lines.push(` ID: ${shortId}`);
21400
- if (a.description) lines.push(` ${a.description}`);
21401
- if (a.task) lines.push(` Task: ${a.task.slice(0, 200)}${a.task.length > 200 ? "\u2026" : ""}`);
21402
- if (a.tokenInput > 0 || a.tokenOutput > 0) {
21403
- const inK = (a.tokenInput / 1e3).toFixed(1);
21404
- const outK = (a.tokenOutput / 1e3).toFixed(1);
21405
- lines.push(` Tokens: ${inK}k in / ${outK}k out`);
21406
- }
21407
- lines.push("");
21408
- }
21409
- lines.push(`Total: ${agents2.length} agent(s) (${runningCount} running, ${queuedCount} queued)`);
21410
- await channel.sendText(chatId, lines.join("\n"), { parseMode: "html" });
21411
- if (typeof channel.sendKeyboard === "function") {
21412
- const agentButtons = [];
21413
- for (const a of agents2) {
21414
- const shortId = a.id.slice(0, 8);
21415
- const taskLabel = a.task ? a.task.slice(0, 20) : a.status;
21416
- agentButtons.push([
21417
- { label: `\u{1F6D1} Stop ${shortId}: ${taskLabel}`, data: `agents:stop:${shortId}`, style: "danger" }
21418
- ]);
21474
+ if (commandArgs === "aliases") {
21475
+ const all = getAllBookmarks(chatId);
21476
+ if (all.length === 0) {
21477
+ await channel.sendText(chatId, "No bookmarks saved yet.", { parseMode: "plain" });
21478
+ return;
21419
21479
  }
21420
- agentButtons.push([{ label: "\u{1F4CB} View Task Board", data: "agents:tasks" }]);
21421
- await channel.sendKeyboard(chatId, "Agent actions:", agentButtons);
21422
- }
21423
- }
21424
- async function handleTasksCommand(chatId, commandArgs, msg, channel) {
21425
- const db3 = getDb();
21426
- const orch = getActiveOrchestration(db3, chatId);
21427
- if (!orch) {
21428
- await channel.sendText(chatId, "<b>Task Board</b>\n\nNo active orchestration. Start a task to create one.", { parseMode: "html" });
21480
+ const lines = all.map((b) => ` ${b.manual ? "[manual]" : "[auto]"} ${b.alias} \u2192 ${b.path}`);
21481
+ await channel.sendText(chatId, `Directory bookmarks:
21482
+ ${lines.join("\n")}`, { parseMode: "plain" });
21429
21483
  return;
21430
21484
  }
21431
- const tasks = listTasksByOrchestration(db3, orch.id);
21432
- const byStatus = {
21433
- pending: [],
21434
- in_progress: [],
21435
- completed: [],
21436
- failed: [],
21437
- abandoned: []
21438
- };
21439
- for (const t of tasks) {
21440
- if (byStatus[t.status]) byStatus[t.status].push(t);
21441
- }
21442
- const lines = ["<b>Task Board</b>", ""];
21443
- const sections = [
21444
- { label: "Pending", emoji: "\u{1F4CB}", status: "pending" },
21445
- { label: "In Progress", emoji: "\u{1F504}", status: "in_progress" },
21446
- { label: "Completed", emoji: "\u2705", status: "completed" },
21447
- { label: "Failed", emoji: "\u274C", status: "failed" },
21448
- { label: "Abandoned", emoji: "\u{1F6AB}", status: "abandoned" }
21449
- ];
21450
- for (const { label: label2, emoji, status } of sections) {
21451
- const list = byStatus[status];
21452
- if (list.length === 0) continue;
21453
- lines.push(`${emoji} ${label2}:`);
21454
- for (const t of list) {
21455
- const assignee = t.assignee ? ` (\u2192 ${t.assignee.slice(0, 8)})` : "";
21456
- lines.push(` #${t.id}: ${t.subject}${assignee}`);
21457
- }
21458
- lines.push("");
21459
- }
21460
- if (lines.length === 2) {
21461
- lines.push("No tasks yet.");
21462
- }
21463
- await channel.sendText(chatId, lines.join("\n").trimEnd(), { parseMode: "html" });
21464
- if (typeof channel.sendKeyboard === "function" && tasks.length > 0) {
21465
- const taskButtons = [];
21466
- const viewable = tasks.filter((t) => t.status === "pending" || t.status === "in_progress");
21467
- for (const t of viewable.slice(0, 10)) {
21468
- const statusIcon = t.status === "in_progress" ? "\u{1F504}" : "\u{1F4CB}";
21469
- taskButtons.push([
21470
- { label: `${statusIcon} #${t.id}: ${t.subject.slice(0, 30)}`, data: `tasks:view:${t.id}`, style: "primary" }
21471
- ]);
21472
- }
21473
- if (taskButtons.length > 0) {
21474
- await channel.sendKeyboard(chatId, "View task details:", taskButtons);
21485
+ if (commandArgs.startsWith("unalias ")) {
21486
+ const aliasName = commandArgs.slice(8).trim();
21487
+ if (!aliasName) {
21488
+ await channel.sendText(chatId, "Usage: /cwd unalias <name>", { parseMode: "plain" });
21489
+ return;
21475
21490
  }
21491
+ const deleted = deleteBookmark(chatId, aliasName);
21492
+ await channel.sendText(chatId, deleted ? `Bookmark '${aliasName}' removed.` : `Bookmark '${aliasName}' not found.`, { parseMode: "plain" });
21493
+ return;
21476
21494
  }
21477
- }
21478
- async function handleStopagentCommand(chatId, commandArgs, msg, channel) {
21479
- if (!commandArgs) {
21480
- const db4 = getDb();
21481
- const agents3 = listActiveAgents(db4);
21482
- if (agents3.length === 0) {
21483
- await channel.sendText(chatId, "No active agents to stop.", { parseMode: "plain" });
21495
+ if (commandArgs.startsWith("alias ")) {
21496
+ const parts = commandArgs.slice(6).trim().split(/\s+/);
21497
+ if (parts.length < 2) {
21498
+ await channel.sendText(chatId, "Usage: /cwd alias <name> <path>", { parseMode: "plain" });
21484
21499
  return;
21485
21500
  }
21486
- if (typeof channel.sendKeyboard === "function") {
21487
- const STATUS_EMOJI_STOP = { running: "\u{1F7E2}", queued: "\u{1F7E1}", starting: "\u{1F535}", idle: "\u26AA" };
21488
- const agentLines = agents3.map((a) => {
21489
- const emoji = STATUS_EMOJI_STOP[a.status] ?? "\u26AA";
21490
- const shortId = a.id.slice(0, 8);
21491
- const taskLabel = a.task ? a.task.slice(0, 40) : "no task";
21492
- const runtimeMs = Date.now() - (/* @__PURE__ */ new Date(a.createdAt + "Z")).getTime();
21493
- const mins = Math.floor(runtimeMs / 6e4);
21494
- const runtime = mins < 1 ? "<1m" : `${mins}m`;
21495
- return `${emoji} ${shortId} \u2014 "${taskLabel}" (${a.status}, ${runtime})`;
21496
- });
21497
- const buttons = agents3.map((a) => {
21498
- const shortId = a.id.slice(0, 8);
21499
- const taskLabel = a.task ? a.task.slice(0, 20) : a.status;
21500
- return [{ label: `${shortId}: ${taskLabel}`, data: `stopagent:pick:${shortId}`, style: "danger" }];
21501
- });
21502
- buttons.push([
21503
- { label: "\u{1F6D1} Stop All", data: "stopagent:all", style: "danger" },
21504
- { label: "Cancel", data: "stopagent:cancel" }
21505
- ]);
21506
- await channel.sendKeyboard(chatId, `Stop which agent?
21507
- \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
21508
-
21509
- ${agentLines.join("\n")}`, buttons);
21510
- } else {
21511
- await channel.sendText(chatId, "Usage: /stopagent <id>\nUse first 8 chars of agent ID or full ID.", { parseMode: "plain" });
21512
- }
21501
+ const [aliasName, ...pathParts] = parts;
21502
+ const aliasPath = pathParts.join(" ").replace(/^~/, process.env.HOME ?? "");
21503
+ upsertBookmark(chatId, aliasName, aliasPath, true);
21504
+ await channel.sendText(chatId, `Bookmark saved: ${aliasName} \u2192 ${aliasPath}`, { parseMode: "plain" });
21513
21505
  return;
21514
21506
  }
21515
- const db3 = getDb();
21516
- const agents2 = listActiveAgents(db3);
21517
- const id = commandArgs.trim();
21518
- const match = agents2.find((a) => a.id === id || a.id.startsWith(id));
21519
- if (!match) {
21520
- await channel.sendText(chatId, `No active agent found matching "${id}". Use /agents to list.`, { parseMode: "plain" });
21507
+ const arg = commandArgs;
21508
+ if (arg.startsWith("/") || arg.startsWith("~")) {
21509
+ const resolvedPath = arg.startsWith("~") ? arg.replace("~", process.env.HOME ?? "") : arg;
21510
+ setCwd(chatId, resolvedPath);
21511
+ const basename4 = resolvedPath.split("/").filter(Boolean).pop();
21512
+ if (basename4) upsertBookmark(chatId, basename4, resolvedPath, false);
21513
+ logActivity(getDb(), { chatId, source: "telegram", eventType: "config_changed", summary: `Working directory set to ${resolvedPath}`, detail: { field: "cwd", value: resolvedPath } });
21514
+ await sendCwdSessionChoice(chatId, resolvedPath, channel);
21521
21515
  return;
21522
21516
  }
21523
- const ok = cancelAgent(match.id);
21524
- await channel.sendText(chatId, ok ? `Agent ${match.id.slice(0, 8)} cancelled.` : "Could not cancel agent.", { parseMode: "plain" });
21525
- }
21526
- async function handleMcpCommand(chatId, commandArgs, msg, channel) {
21527
- const db3 = getDb();
21528
- const lines = ["<b>\u{1F50C} MCP Servers</b>"];
21529
- let totalCount = 0;
21530
- let connectedCount = 0;
21531
- const centralMcps = listRegisteredMcps(db3);
21532
- if (centralMcps.length > 0) {
21533
- lines.push("", "\u2501\u2501 <b>CC-Claw</b> \u2501\u2501");
21534
- for (const m of centralMcps) {
21535
- totalCount++;
21536
- connectedCount++;
21537
- const autoTag = m.enabledByDefault ? " \u{1F4CC}" : "";
21538
- const desc = m.description ? ` <i>${m.description}</i>` : "";
21539
- lines.push(` \u2705 <b>${m.name}</b>${autoTag}${desc}`);
21540
- }
21517
+ const exact = getBookmark(chatId, arg);
21518
+ if (exact) {
21519
+ setCwd(chatId, exact.path);
21520
+ touchBookmark(chatId, arg);
21521
+ logActivity(getDb(), { chatId, source: "telegram", eventType: "config_changed", summary: `Working directory set to ${exact.path}`, detail: { field: "cwd", value: exact.path } });
21522
+ await sendCwdSessionChoice(chatId, exact.path, channel);
21523
+ return;
21541
21524
  }
21542
- if (process.env.DASHBOARD_ENABLED === "1") {
21543
- totalCount++;
21544
- connectedCount++;
21545
- lines.push("", "\u2501\u2501 <b>Built-in</b> \u2501\u2501");
21546
- lines.push(` \u2705 <b>cc-claw</b> <i>Agent orchestrator (spawn, tasks, inbox)</i>`);
21525
+ const matches = findBookmarksByPrefix(chatId, arg);
21526
+ if (matches.length === 1) {
21527
+ setCwd(chatId, matches[0].path);
21528
+ touchBookmark(chatId, matches[0].alias);
21529
+ logActivity(getDb(), { chatId, source: "telegram", eventType: "config_changed", summary: `Working directory set to ${matches[0].path}`, detail: { field: "cwd", value: matches[0].path } });
21530
+ await sendCwdSessionChoice(chatId, matches[0].path, channel);
21531
+ return;
21547
21532
  }
21548
- const { execFile: execFile5 } = await import("child_process");
21549
- const { homedir: homedirImport } = await import("os");
21550
- const discoveryCwd = homedirImport();
21551
- const runnerResults = await Promise.allSettled(
21552
- getAllRunners().map((runner) => {
21553
- const listCmd = runner.getMcpListCommand();
21554
- if (!listCmd.length) return Promise.resolve({ runner, output: "" });
21555
- const exe = runner.getExecutablePath();
21556
- return new Promise((resolve) => {
21557
- execFile5(exe, listCmd.slice(1), {
21558
- encoding: "utf-8",
21559
- timeout: 3e4,
21560
- cwd: discoveryCwd,
21561
- env: runner.getEnv()
21562
- }, (_err, stdout, stderr) => {
21563
- const combined = ((stdout ?? "") + "\n" + (stderr ?? "")).trim();
21564
- resolve({ runner, output: combined });
21565
- });
21566
- });
21567
- })
21568
- );
21569
- for (const result of runnerResults) {
21570
- if (result.status !== "fulfilled") continue;
21571
- const { runner, output: output2 } = result.value;
21572
- if (!runner.getMcpListCommand().length) continue;
21573
- const parsed = parseMcpListOutput(output2);
21574
- lines.push("", `\u2501\u2501 <b>${runner.displayName}</b> \u2501\u2501`);
21575
- if (parsed.length === 0) {
21576
- lines.push(" <i>No servers configured</i>");
21577
- continue;
21578
- }
21579
- for (const srv of parsed) {
21580
- totalCount++;
21581
- if (srv.connected) connectedCount++;
21582
- const icon = srv.connected ? "\u2705" : "\u274C";
21583
- lines.push(` ${icon} <b>${srv.name}</b>`);
21533
+ if (matches.length > 1 && typeof channel.sendKeyboard === "function") {
21534
+ const buttons = matches.map((m) => [{ label: `${m.alias} \u2192 ${m.path}`, data: `cwdpick:${m.alias}` }]);
21535
+ await channel.sendKeyboard(chatId, `Multiple matches for "${arg}":`, buttons);
21536
+ return;
21537
+ }
21538
+ await channel.sendText(chatId, `Directory alias '${arg}' not found. Use /cwd aliases to see saved bookmarks.`, { parseMode: "plain" });
21539
+ }
21540
+ async function handleSummarizerCommand(chatId, commandArgs, msg, channel) {
21541
+ let adapter;
21542
+ try {
21543
+ adapter = getAdapterForChat(chatId);
21544
+ } catch {
21545
+ adapter = null;
21546
+ }
21547
+ const current = getSummarizer(chatId);
21548
+ const currentLabel = current.backend === "off" ? "Off" : current.backend ? `${current.backend}:${current.model ?? "default"}` : `Auto (${adapter?.summarizerModel ?? "default"})`;
21549
+ if (typeof channel.sendKeyboard === "function") {
21550
+ const isAuto = !current.backend && current.backend !== "off";
21551
+ const isOff = current.backend === "off";
21552
+ const buttons = [
21553
+ [{ label: `Auto (use active backend)${isAuto ? " \u2713" : ""}`, data: "summarizer:auto", ...isAuto ? { style: "primary" } : {} }],
21554
+ [{ label: `Off (disable)${isOff ? " \u2713" : ""}`, data: "summarizer:off", ...isOff ? { style: "primary" } : {} }]
21555
+ ];
21556
+ for (const a of getAvailableAdapters()) {
21557
+ const pinned = current.backend === a.id;
21558
+ buttons.push([{
21559
+ label: `${pinned ? "\u2713 " : ""}${a.displayName}: ${a.summarizerModel}`,
21560
+ data: `summarizer:${a.id}:${a.summarizerModel}`,
21561
+ ...pinned ? { style: "primary" } : {}
21562
+ }]);
21584
21563
  }
21564
+ await channel.sendKeyboard(chatId, `Session summarizer (current: ${currentLabel}):`, buttons);
21565
+ } else {
21566
+ await channel.sendText(chatId, `Summarizer: ${currentLabel}
21567
+
21568
+ Use /summarizer auto, /summarizer off, or /summarizer <backend>:<model>`, { parseMode: "plain" });
21585
21569
  }
21586
- if (totalCount === 0) {
21587
- lines.push("", "<i>No MCP servers found.</i>");
21570
+ }
21571
+ async function handleGeminiAccountsCommand(chatId, commandArgs, msg, channel) {
21572
+ const slots = getGeminiSlots();
21573
+ if (slots.length === 0) {
21574
+ 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" });
21575
+ return;
21576
+ }
21577
+ if (typeof channel.sendKeyboard === "function") {
21578
+ const rows = buildAccountSlotKeyboard(
21579
+ slots,
21580
+ getChatGeminiSlotId(chatId),
21581
+ getGeminiRotationMode(),
21582
+ "gslot:",
21583
+ "grotation:"
21584
+ );
21585
+ await channel.sendKeyboard(chatId, "Gemini Accounts & Rotation:", rows);
21588
21586
  } else {
21589
- lines.splice(1, 0, `<i>${connectedCount}/${totalCount} connected</i>`);
21587
+ const currentMode = getGeminiRotationMode();
21588
+ const list = slots.filter((s) => s.enabled).map((s) => {
21589
+ const icon = s.slotType === "oauth" ? "\u{1F468}\u{1F3FD}\u200D\u{1F4BB}" : "\u{1F511}";
21590
+ return `${icon} ${s.label || `slot-${s.id}`} (#${s.id})`;
21591
+ }).join("\n");
21592
+ await channel.sendText(chatId, `Slots:
21593
+ ${list}
21594
+
21595
+ Rotation mode: ${currentMode}
21596
+ Use: /gemini_accounts <name> to pin`, { parseMode: "plain" });
21590
21597
  }
21591
- await channel.sendText(chatId, lines.join("\n"), { parseMode: "html" });
21592
21598
  }
21593
- async function handleCronCommand(chatId, commandArgs, msg, channel) {
21594
- if (!commandArgs) {
21595
- await sendJobsBoard(chatId, channel, 1);
21599
+ async function handleBackendAccountsCommand(chatId, commandArgs, msg, channel) {
21600
+ const command = msg.command;
21601
+ const slotBackend = command === "claude_accounts" ? "claude" : "codex";
21602
+ const slotDisplayName = command === "claude_accounts" ? "Claude" : "Codex";
21603
+ const slots = getBackendSlots(slotBackend);
21604
+ if (slots.length === 0) {
21605
+ await channel.sendText(chatId, `No ${slotDisplayName} credentials configured.
21606
+ Add with: <code>cc-claw ${slotBackend} add-key</code>`, { parseMode: "html" });
21596
21607
  return;
21597
21608
  }
21598
- const cronParts = commandArgs.split(/\s+/);
21599
- const cronSub = cronParts[0].toLowerCase();
21600
- const cronSubArgs = cronParts.slice(1).join(" ");
21601
- switch (cronSub) {
21602
- case "cancel": {
21603
- if (!cronSubArgs) {
21604
- await sendJobPicker(chatId, channel, "cancel");
21605
- return;
21606
- }
21607
- const id = parseInt(cronSubArgs, 10);
21608
- const ok = cancelJob(id);
21609
- await channel.sendText(chatId, ok ? `Job #${id} cancelled.` : `Job #${id} not found.`, { parseMode: "plain" });
21610
- break;
21611
- }
21612
- case "pause": {
21613
- if (!cronSubArgs) {
21614
- await sendJobPicker(chatId, channel, "pause");
21609
+ if (typeof channel.sendKeyboard === "function") {
21610
+ const rows = buildAccountSlotKeyboard(
21611
+ slots,
21612
+ getChatBackendSlotId(chatId, slotBackend),
21613
+ getBackendRotationMode(slotBackend),
21614
+ `bslot:${slotBackend}:`,
21615
+ `brotation:${slotBackend}:`
21616
+ );
21617
+ await channel.sendKeyboard(chatId, `${slotDisplayName} Accounts & Rotation:`, rows);
21618
+ } else {
21619
+ const currentMode = getBackendRotationMode(slotBackend);
21620
+ const list = slots.filter((s) => s.enabled).map((s) => {
21621
+ const icon = s.slotType === "oauth" ? "\u{1F468}\u{1F3FD}\u200D\u{1F4BB}" : "\u{1F511}";
21622
+ return `${icon} ${s.label || `slot-${s.id}`} (#${s.id})`;
21623
+ }).join("\n");
21624
+ await channel.sendText(chatId, `${slotDisplayName} Slots:
21625
+ ${list}
21626
+
21627
+ Rotation mode: ${currentMode}
21628
+ Use: /${command} <name> to pin`, { parseMode: "plain" });
21629
+ }
21630
+ }
21631
+ async function handleDebugCommand(chatId, commandArgs, msg, channel) {
21632
+ const { getSessionLogEnabled: getSessionLogEnabled2, toggleSessionLogEnabled: toggleSessionLogEnabled2 } = await Promise.resolve().then(() => (init_chat_settings(), chat_settings_exports));
21633
+ const current = getSessionLogEnabled2(chatId);
21634
+ if (commandArgs === "on" || commandArgs === "off") {
21635
+ const { setSessionLogEnabled: setSessionLogEnabled2 } = await Promise.resolve().then(() => (init_chat_settings(), chat_settings_exports));
21636
+ const value = commandArgs === "on";
21637
+ setSessionLogEnabled2(chatId, value);
21638
+ await channel.sendText(chatId, `\u{1F52C} Session debug logging: ${value ? "ON" : "OFF"}
21639
+
21640
+ ${value ? "Full tool inputs/results will be saved to ~/.cc-claw/logs/sessions/\nUse 'cc-claw logs session list' or 'cc-claw logs session tail' to inspect." : "Session logs disabled."}`, { parseMode: "plain" });
21641
+ } else if (typeof channel.sendKeyboard === "function") {
21642
+ await channel.sendKeyboard(
21643
+ chatId,
21644
+ `\u{1F52C} Session Debug Logging
21645
+
21646
+ Records full, untruncated tool inputs and results to disk for debugging.
21647
+ Logs: ~/.cc-claw/logs/sessions/
21648
+ Retention: ${process.env.SESSION_LOG_RETENTION_DAYS ?? "7"} day(s)
21649
+
21650
+ Currently: ${current ? "ON" : "OFF"}`,
21651
+ [[
21652
+ { label: current ? "\u2713 ON" : "ON", data: "debug_log:on", ...current ? { style: "primary" } : {} },
21653
+ { label: !current ? "\u2713 OFF" : "OFF", data: "debug_log:off", ...!current ? { style: "primary" } : {} }
21654
+ ]]
21655
+ );
21656
+ } else {
21657
+ const next = toggleSessionLogEnabled2(chatId);
21658
+ await channel.sendText(chatId, `\u{1F52C} Session debug logging: ${next ? "ON" : "OFF"}`, { parseMode: "plain" });
21659
+ }
21660
+ }
21661
+ async function handleHeartbeatCommand(chatId, commandArgs, msg, channel) {
21662
+ if (commandArgs) {
21663
+ const { action, value } = parseHeartbeatCommand(chatId, commandArgs);
21664
+ switch (action) {
21665
+ case "on": {
21666
+ setHeartbeatConfig(chatId, { enabled: true });
21667
+ startHeartbeatForChat(chatId);
21668
+ if (typeof channel.sendKeyboard === "function") {
21669
+ await sendHeartbeatKeyboard(chatId, channel);
21670
+ } else {
21671
+ await channel.sendText(chatId, "Heartbeat enabled. I'll check in periodically.", { parseMode: "plain" });
21672
+ }
21615
21673
  return;
21616
21674
  }
21617
- const pauseId = parseInt(cronSubArgs, 10);
21618
- const paused = pauseJob(pauseId);
21619
- await channel.sendText(chatId, paused ? `Job #${pauseId} paused.` : `Job #${pauseId} not found.`, { parseMode: "plain" });
21620
- break;
21621
- }
21622
- case "resume": {
21623
- if (!cronSubArgs) {
21624
- await sendJobPicker(chatId, channel, "resume");
21675
+ case "off": {
21676
+ setHeartbeatConfig(chatId, { enabled: false });
21677
+ stopHeartbeatForChat(chatId);
21678
+ if (typeof channel.sendKeyboard === "function") {
21679
+ await sendHeartbeatKeyboard(chatId, channel);
21680
+ } else {
21681
+ await channel.sendText(chatId, "Heartbeat disabled.", { parseMode: "plain" });
21682
+ }
21625
21683
  return;
21626
21684
  }
21627
- const resumeId = parseInt(cronSubArgs, 10);
21628
- const resumed = resumeJob(resumeId);
21629
- await channel.sendText(chatId, resumed ? `Job #${resumeId} resumed.` : `Job #${resumeId} not found.`, { parseMode: "plain" });
21630
- break;
21631
- }
21632
- case "run": {
21633
- if (!cronSubArgs) {
21634
- await sendJobPicker(chatId, channel, "run");
21685
+ case "interval": {
21686
+ const ms = parseIntervalToMs(value ?? "30m");
21687
+ if (!ms || ms < 6e4) {
21688
+ await channel.sendText(chatId, "Invalid interval. Use e.g. 15m, 30m, 1h, 2h", { parseMode: "plain" });
21689
+ return;
21690
+ }
21691
+ setHeartbeatConfig(chatId, { intervalMs: ms });
21692
+ stopHeartbeatForChat(chatId);
21693
+ const hbConf = getHeartbeatConfig(chatId);
21694
+ if (hbConf?.enabled) startHeartbeatForChat(chatId);
21695
+ if (typeof channel.sendKeyboard === "function") {
21696
+ await sendHeartbeatKeyboard(chatId, channel);
21697
+ } else {
21698
+ await channel.sendText(chatId, `Heartbeat interval set to ${ms / 6e4} minutes.`, { parseMode: "plain" });
21699
+ }
21635
21700
  return;
21636
21701
  }
21637
- const runId = parseInt(cronSubArgs, 10);
21638
- await channel.sendText(chatId, `Triggering job #${runId}...`, { parseMode: "plain" });
21639
- const runResult = await triggerJob(runId);
21640
- await channel.sendText(chatId, runResult, { parseMode: "plain" });
21641
- break;
21642
- }
21643
- case "runs": {
21644
- const runsId = cronSubArgs ? parseInt(cronSubArgs, 10) : void 0;
21645
- if (runsId) {
21646
- await sendJobRunsView(chatId, runsId, channel, 1);
21647
- } else {
21648
- const cronRuns2 = getJobRuns(void 0, 10);
21649
- if (cronRuns2.length === 0) {
21650
- await channel.sendText(chatId, "No run history.", { parseMode: "plain" });
21702
+ case "hours": {
21703
+ const hourMatch = (value ?? "").match(/^(\d{1,2})-(\d{1,2})$/);
21704
+ if (!hourMatch) {
21705
+ await channel.sendText(chatId, "Usage: /heartbeat hours 8-22", { parseMode: "plain" });
21651
21706
  return;
21652
21707
  }
21653
- const runLines = cronRuns2.map((r) => {
21654
- const dur = r.durationMs ? ` (${(r.durationMs / 1e3).toFixed(1)}s)` : "";
21655
- return `#${r.jobId} [${r.status}] ${formatLocalDateTime(r.startedAt)}${dur}`;
21656
- });
21657
- await channel.sendText(chatId, runLines.join("\n\n"), { parseMode: "plain" });
21658
- }
21659
- break;
21660
- }
21661
- case "edit": {
21662
- if (!cronSubArgs) {
21663
- await channel.sendText(chatId, "Usage: /cron edit <id>", { parseMode: "plain" });
21708
+ const hbStart = `${hourMatch[1].padStart(2, "0")}:00`;
21709
+ const hbEnd = `${hourMatch[2].padStart(2, "0")}:00`;
21710
+ setHeartbeatConfig(chatId, { activeStart: hbStart, activeEnd: hbEnd });
21711
+ if (typeof channel.sendKeyboard === "function") {
21712
+ await sendHeartbeatKeyboard(chatId, channel);
21713
+ } else {
21714
+ await channel.sendText(chatId, `Active hours: ${hbStart} - ${hbEnd}`, { parseMode: "plain" });
21715
+ }
21664
21716
  return;
21665
21717
  }
21666
- const editId = parseInt(cronSubArgs, 10);
21667
- await startEditWizard(chatId, editId, channel);
21668
- break;
21669
- }
21670
- case "health": {
21671
- const report = getHealthReport();
21672
- await channel.sendText(chatId, formatHealthReport(report), { parseMode: "plain" });
21673
- break;
21718
+ default:
21719
+ if (typeof channel.sendKeyboard === "function") {
21720
+ await sendHeartbeatKeyboard(chatId, channel);
21721
+ } else {
21722
+ await channel.sendText(chatId, formatHeartbeatStatus(chatId), { parseMode: "plain" });
21723
+ }
21724
+ return;
21674
21725
  }
21675
- default:
21676
- await startWizard(chatId, commandArgs, channel);
21677
- }
21678
- }
21679
- var init_command_handlers = __esm({
21680
- "src/router/command-handlers.ts"() {
21681
- "use strict";
21682
- init_format();
21683
- init_log();
21684
- init_format_time();
21685
- init_version();
21686
- init_image_gen();
21687
- init_stt();
21688
- init_agent();
21689
- init_classify();
21690
- init_install();
21691
- init_profile();
21692
- init_heartbeat2();
21693
- init_discover();
21694
- init_store5();
21695
- init_summarize();
21696
- init_session_log();
21697
- init_backends();
21698
- init_cron();
21699
- init_wizard();
21700
- init_health2();
21701
- init_store();
21702
- init_orchestrator();
21703
- init_registry();
21704
- init_registry2();
21705
- init_types();
21706
- init_store3();
21707
- init_helpers();
21708
- init_shell();
21709
- init_ui();
21710
- init_state();
21711
- init_evolve2();
21712
- init_optimize();
21713
21726
  }
21714
- });
21715
-
21716
- // src/router/ollama.ts
21717
- var ollama_exports2 = {};
21718
- __export(ollama_exports2, {
21719
- handleOllamaCallback: () => handleOllamaCallback,
21720
- handleOllamaCommand: () => handleOllamaCommand
21721
- });
21722
- async function handleOllamaCommand(chatId, commandArgs, channel) {
21723
- const [sub, ...rest] = (commandArgs || "").trim().split(/\s+/);
21724
- switch (sub) {
21725
- case "models":
21726
- return sendModelList(chatId, channel, rest[0]);
21727
- case "health":
21728
- return sendHealthCheck(chatId, channel);
21729
- case "discover":
21730
- return sendDiscover(chatId, channel, rest[0]);
21731
- case "add":
21732
- if (rest.length >= 2) return handleAdd(chatId, channel, rest[0], rest[1], rest[2] ? parseInt(rest[2], 10) : void 0);
21733
- if (rest.length === 1) return handleAdd(chatId, channel, rest[0], rest[0]);
21734
- await channel.sendText(chatId, "Usage: /ollama add <name> <host> [port]", { parseMode: "plain" });
21735
- return;
21736
- case "remove":
21737
- if (rest[0]) return sendRemoveConfirm(chatId, channel, rest[0]);
21738
- await channel.sendText(chatId, "Usage: /ollama remove <server-name>", { parseMode: "plain" });
21739
- return;
21740
- default:
21741
- return sendOllamaDashboard(chatId, channel);
21727
+ if (typeof channel.sendKeyboard === "function") {
21728
+ await sendHeartbeatKeyboard(chatId, channel);
21729
+ } else {
21730
+ await channel.sendText(chatId, formatHeartbeatStatus(chatId), { parseMode: "plain" });
21742
21731
  }
21743
21732
  }
21744
- async function sendOllamaDashboard(chatId, channel) {
21745
- const { OllamaStore } = await Promise.resolve().then(() => (init_ollama(), ollama_exports));
21746
- const servers = OllamaStore.listServers();
21747
- if (servers.length === 0) {
21748
- const text = [
21749
- "<b>\u{1F999} Ollama</b>",
21750
- "",
21751
- "No servers configured.",
21752
- "",
21753
- "Add your first server:",
21754
- " <code>/ollama add &lt;name&gt; &lt;host&gt; [port]</code>",
21755
- "",
21756
- "Example:",
21757
- " <code>/ollama add local 192.168.1.100</code>"
21758
- ].join("\n");
21759
- if (typeof channel.sendKeyboard === "function") {
21760
- await channel.sendKeyboard(chatId, text, [
21761
- [{ label: "\u{1F4D6} Help", data: "ollama:help" }]
21733
+ async function handleAgentsCommand(chatId, commandArgs, msg, channel) {
21734
+ if (commandArgs?.startsWith("mode")) {
21735
+ const modeArg = commandArgs.slice(5).trim().toLowerCase();
21736
+ if (modeArg === "native" || modeArg === "claw" || modeArg === "auto") {
21737
+ setAgentMode(chatId, modeArg);
21738
+ try {
21739
+ await summarizeSession(chatId);
21740
+ } catch {
21741
+ }
21742
+ clearSession(chatId);
21743
+ await channel.sendText(chatId, `Agent mode set to <b>${modeArg}</b>. Session cleared.`, { parseMode: "html" });
21744
+ } else if (typeof channel.sendKeyboard === "function") {
21745
+ const current = getAgentMode(chatId);
21746
+ await channel.sendKeyboard(chatId, `Agent mode: <b>${current}</b>
21747
+
21748
+ Choose a mode:`, [
21749
+ [
21750
+ { label: "Auto (recommended)", data: "agentmode:auto", ...current === "auto" ? { style: "primary" } : {} },
21751
+ { label: "Native (fast)", data: "agentmode:native", ...current === "native" ? { style: "primary" } : {} },
21752
+ { label: "Orchestrated", data: "agentmode:claw", ...current === "claw" ? { style: "primary" } : {} }
21753
+ ]
21762
21754
  ]);
21763
21755
  } else {
21764
- await channel.sendText(chatId, text, { parseMode: "html" });
21756
+ const current = getAgentMode(chatId);
21757
+ await channel.sendText(chatId, `Agent mode: ${current}. Use /agents mode <auto|native|claw> to change.`, { parseMode: "plain" });
21765
21758
  }
21766
21759
  return;
21767
21760
  }
21768
- const lines = ["<b>\u{1F999} Ollama Servers</b>", ""];
21769
- for (const s of servers) {
21770
- const dot = s.status === "online" ? "\u{1F7E2}" : "\u{1F534}";
21771
- const modelCount = OllamaStore.listModels(s.id).length;
21772
- lines.push(`${dot} <b>${s.name}</b> <code>${s.host}:${s.port}</code>`);
21773
- lines.push(` ${modelCount} model${modelCount !== 1 ? "s" : ""} \xB7 ${s.status}`);
21761
+ if (commandArgs?.startsWith("history")) {
21762
+ const db4 = getDb();
21763
+ const events = db4.prepare(`
21764
+ SELECT summary, detail, createdAt FROM activity_log
21765
+ WHERE chatId = ? AND eventType = 'native_subagent'
21766
+ AND createdAt > datetime('now', '-1 day')
21767
+ ORDER BY createdAt DESC LIMIT 20
21768
+ `).all(chatId);
21769
+ if (events.length === 0) {
21770
+ await channel.sendText(chatId, "No native sub-agent activity in the last 24h.", { parseMode: "plain" });
21771
+ } else {
21772
+ const lines2 = events.map((e) => {
21773
+ const d = e.detail ? JSON.parse(e.detail) : {};
21774
+ return `\u2022 ${e.summary} (${d.backend ?? "?"}, ${e.createdAt})`;
21775
+ });
21776
+ await channel.sendText(chatId, `<b>Native agent history (24h)</b>
21777
+
21778
+ ${lines2.join("\n")}`, { parseMode: "html" });
21779
+ }
21780
+ return;
21781
+ }
21782
+ const db3 = getDb();
21783
+ const agents2 = listActiveAgents(db3);
21784
+ const STATUS_EMOJI = {
21785
+ running: "\u{1F7E2}",
21786
+ queued: "\u{1F7E1}",
21787
+ starting: "\u{1F535}",
21788
+ idle: "\u26AA",
21789
+ failed: "\u274C",
21790
+ cancelled: "\u{1F6AB}"
21791
+ };
21792
+ const runners2 = getAllRunners();
21793
+ const runnerMap = new Map(runners2.map((r) => [r.id, r]));
21794
+ if (agents2.length === 0) {
21795
+ await channel.sendText(chatId, "<b>Active Agents</b>\n\nNo active agents.", { parseMode: "html" });
21796
+ return;
21797
+ }
21798
+ const { getAllAdapters: getAllAdaptersImport } = await Promise.resolve().then(() => (init_backends(), backends_exports));
21799
+ const defaultModelMap = /* @__PURE__ */ new Map();
21800
+ for (const adapterItem of getAllAdaptersImport()) {
21801
+ defaultModelMap.set(adapterItem.id, adapterItem.defaultModel);
21802
+ }
21803
+ const lines = ["<b>Active Agents</b>", ""];
21804
+ let runningCount = 0;
21805
+ let queuedCount = 0;
21806
+ for (const a of agents2) {
21807
+ const emoji = STATUS_EMOJI[a.status] ?? "\u26AA";
21808
+ const runner = runnerMap.get(a.runnerId);
21809
+ const displayName = runner?.displayName ?? a.runnerId;
21810
+ const modelId = a.model ?? defaultModelMap.get(a.runnerId) ?? "";
21811
+ const modelLabel = shortModelName(modelId);
21812
+ const shortId = a.id.slice(0, 8);
21813
+ if (a.status === "running") runningCount++;
21814
+ if (a.status === "queued") queuedCount++;
21815
+ const agentLabel = a.name ?? shortId;
21816
+ const header2 = modelLabel ? `${emoji} ${agentLabel} (${displayName} | ${modelLabel}) \u2014 ${a.status}` : `${emoji} ${agentLabel} (${displayName}) \u2014 ${a.status}`;
21817
+ lines.push(header2);
21818
+ if (a.name) lines.push(` ID: ${shortId}`);
21819
+ if (a.description) lines.push(` ${a.description}`);
21820
+ if (a.task) lines.push(` Task: ${a.task.slice(0, 200)}${a.task.length > 200 ? "\u2026" : ""}`);
21821
+ if (a.tokenInput > 0 || a.tokenOutput > 0) {
21822
+ const inK = (a.tokenInput / 1e3).toFixed(1);
21823
+ const outK = (a.tokenOutput / 1e3).toFixed(1);
21824
+ lines.push(` Tokens: ${inK}k in / ${outK}k out`);
21825
+ }
21826
+ lines.push("");
21774
21827
  }
21775
- const onlineCount = servers.filter((s) => s.status === "online").length;
21776
- lines.push("", `${onlineCount}/${servers.length} online`);
21828
+ lines.push(`Total: ${agents2.length} agent(s) (${runningCount} running, ${queuedCount} queued)`);
21829
+ await channel.sendText(chatId, lines.join("\n"), { parseMode: "html" });
21777
21830
  if (typeof channel.sendKeyboard === "function") {
21778
- const buttons = [
21779
- [
21780
- { label: "\u{1F50D} Discover", data: "ollama:discover" },
21781
- { label: "\u{1F49A} Health", data: "ollama:health", style: "success" }
21782
- ],
21783
- [
21784
- { label: "\u{1F4CB} Models", data: "ollama:models" },
21785
- { label: "\u2795 Add", data: "ollama:add_prompt" }
21786
- ]
21787
- ];
21788
- if (servers.length <= 4) {
21789
- const removeRow = servers.map((s) => ({
21790
- label: `\u{1F5D1} ${s.name}`,
21791
- data: `ollama:remove_confirm:${s.name}`,
21792
- style: "danger"
21793
- }));
21794
- buttons.push(removeRow);
21831
+ const agentButtons = [];
21832
+ for (const a of agents2) {
21833
+ const shortId = a.id.slice(0, 8);
21834
+ const taskLabel = a.task ? a.task.slice(0, 20) : a.status;
21835
+ agentButtons.push([
21836
+ { label: `\u{1F6D1} Stop ${shortId}: ${taskLabel}`, data: `agents:stop:${shortId}`, style: "danger" }
21837
+ ]);
21795
21838
  }
21796
- await channel.sendKeyboard(chatId, lines.join("\n"), buttons);
21797
- } else {
21798
- await channel.sendText(chatId, lines.join("\n"), { parseMode: "html" });
21839
+ agentButtons.push([{ label: "\u{1F4CB} View Task Board", data: "agents:tasks" }]);
21840
+ await channel.sendKeyboard(chatId, "Agent actions:", agentButtons);
21799
21841
  }
21800
21842
  }
21801
- async function handleAdd(chatId, channel, name, host, port) {
21802
- const { OllamaStore, OllamaClient, OllamaService } = await Promise.resolve().then(() => (init_ollama(), ollama_exports));
21803
- const existing = OllamaStore.getServer(name);
21804
- if (existing) {
21805
- await channel.sendText(chatId, `Server "${name}" already exists. Remove it first.`, { parseMode: "plain" });
21843
+ async function handleTasksCommand(chatId, commandArgs, msg, channel) {
21844
+ const db3 = getDb();
21845
+ const orch = getActiveOrchestration(db3, chatId);
21846
+ if (!orch) {
21847
+ await channel.sendText(chatId, "<b>Task Board</b>\n\nNo active orchestration. Start a task to create one.", { parseMode: "html" });
21806
21848
  return;
21807
21849
  }
21808
- const actualPort = port ?? 11434;
21809
- await channel.sendText(chatId, `Adding ${name} (${host}:${actualPort})...`, { parseMode: "plain" });
21810
- const online = await OllamaClient.ping(`http://${host}:${actualPort}`, { timeoutMs: 5e3 });
21811
- const server = OllamaStore.addServer(name, host, actualPort, null);
21812
- OllamaStore.updateServerStatus(server.id, online ? "online" : "offline");
21813
- if (!online) {
21814
- await channel.sendText(chatId, `\u26A0\uFE0F Server "${name}" added but not responding. Check connectivity.`, { parseMode: "plain" });
21815
- return;
21850
+ const tasks = listTasksByOrchestration(db3, orch.id);
21851
+ const byStatus = {
21852
+ pending: [],
21853
+ in_progress: [],
21854
+ completed: [],
21855
+ failed: [],
21856
+ abandoned: []
21857
+ };
21858
+ for (const t of tasks) {
21859
+ if (byStatus[t.status]) byStatus[t.status].push(t);
21816
21860
  }
21817
- const models = await OllamaService.discoverModels(name);
21818
- const lines = [
21819
- `\u2705 Added "${name}" (${host}:${actualPort})`,
21820
- "",
21821
- `Found ${models.length} model(s):`
21861
+ const lines = ["<b>Task Board</b>", ""];
21862
+ const sections = [
21863
+ { label: "Pending", emoji: "\u{1F4CB}", status: "pending" },
21864
+ { label: "In Progress", emoji: "\u{1F504}", status: "in_progress" },
21865
+ { label: "Completed", emoji: "\u2705", status: "completed" },
21866
+ { label: "Failed", emoji: "\u274C", status: "failed" },
21867
+ { label: "Abandoned", emoji: "\u{1F6AB}", status: "abandoned" }
21822
21868
  ];
21823
- for (const m of models) {
21824
- lines.push(` \u2022 ${m.name}${m.parameterSize ? ` (${m.parameterSize})` : ""}`);
21825
- }
21826
- await channel.sendText(chatId, lines.join("\n"), { parseMode: "plain" });
21827
- }
21828
- async function sendModelList(chatId, channel, serverName) {
21829
- const { OllamaStore } = await Promise.resolve().then(() => (init_ollama(), ollama_exports));
21830
- let models;
21831
- if (serverName) {
21832
- const server = OllamaStore.getServer(serverName);
21833
- if (!server) {
21834
- await channel.sendText(chatId, `Server "${serverName}" not found.`, { parseMode: "plain" });
21835
- return;
21869
+ for (const { label: label2, emoji, status } of sections) {
21870
+ const list = byStatus[status];
21871
+ if (list.length === 0) continue;
21872
+ lines.push(`${emoji} ${label2}:`);
21873
+ for (const t of list) {
21874
+ const assignee = t.assignee ? ` (\u2192 ${t.assignee.slice(0, 8)})` : "";
21875
+ lines.push(` #${t.id}: ${t.subject}${assignee}`);
21836
21876
  }
21837
- models = OllamaStore.listModels(server.id);
21838
- } else {
21839
- models = OllamaStore.getAvailableModels();
21877
+ lines.push("");
21840
21878
  }
21841
- if (models.length === 0) {
21842
- await channel.sendText(chatId, "No models discovered. Run /ollama discover first.", { parseMode: "plain" });
21843
- return;
21879
+ if (lines.length === 2) {
21880
+ lines.push("No tasks yet.");
21844
21881
  }
21845
- const lines = [
21846
- `<b>\u{1F9E0} Ollama Models</b>${serverName ? ` (${serverName})` : ""}`,
21847
- ""
21848
- ];
21849
- for (const m of models) {
21850
- const sizeGB = m.sizeBytes > 0 ? `${(m.sizeBytes / 1e9).toFixed(1)}GB` : "";
21851
- const ctxK = m.contextWindow ? `${Math.round(m.contextWindow / 1e3)}K ctx` : "";
21852
- const meta = [m.parameterSize, m.quantization, sizeGB, ctxK].filter(Boolean).join(" \xB7 ");
21853
- lines.push(` \u2022 <b>${m.name}</b>`);
21854
- if (meta) lines.push(` <i>${meta}</i>`);
21882
+ await channel.sendText(chatId, lines.join("\n").trimEnd(), { parseMode: "html" });
21883
+ if (typeof channel.sendKeyboard === "function" && tasks.length > 0) {
21884
+ const taskButtons = [];
21885
+ const viewable = tasks.filter((t) => t.status === "pending" || t.status === "in_progress");
21886
+ for (const t of viewable.slice(0, 10)) {
21887
+ const statusIcon = t.status === "in_progress" ? "\u{1F504}" : "\u{1F4CB}";
21888
+ taskButtons.push([
21889
+ { label: `${statusIcon} #${t.id}: ${t.subject.slice(0, 30)}`, data: `tasks:view:${t.id}`, style: "primary" }
21890
+ ]);
21891
+ }
21892
+ if (taskButtons.length > 0) {
21893
+ await channel.sendKeyboard(chatId, "View task details:", taskButtons);
21894
+ }
21855
21895
  }
21856
- lines.push("", `${models.length} model${models.length !== 1 ? "s" : ""} total`);
21857
- await channel.sendText(chatId, lines.join("\n"), { parseMode: "html" });
21858
21896
  }
21859
- async function sendDiscover(chatId, channel, serverName) {
21860
- await channel.sendText(chatId, "\u{1F50D} Discovering models...", { parseMode: "plain" });
21861
- try {
21862
- const { OllamaService } = await Promise.resolve().then(() => (init_ollama(), ollama_exports));
21863
- const models = await OllamaService.discoverModels(serverName);
21864
- if (models.length === 0) {
21865
- await channel.sendText(chatId, "No models found. Check server connectivity.", { parseMode: "plain" });
21897
+ async function handleStopagentCommand(chatId, commandArgs, msg, channel) {
21898
+ if (!commandArgs) {
21899
+ const db4 = getDb();
21900
+ const agents3 = listActiveAgents(db4);
21901
+ if (agents3.length === 0) {
21902
+ await channel.sendText(chatId, "No active agents to stop.", { parseMode: "plain" });
21866
21903
  return;
21867
21904
  }
21868
- const lines = [`\u2705 Found ${models.length} model(s):`, ""];
21869
- for (const m of models) {
21870
- const meta = [m.parameterSize, m.contextWindow ? `${Math.round(m.contextWindow / 1e3)}K ctx` : ""].filter(Boolean).join(" \xB7 ");
21871
- lines.push(` \u2022 ${m.name}${meta ? ` (${meta})` : ""}`);
21905
+ if (typeof channel.sendKeyboard === "function") {
21906
+ const STATUS_EMOJI_STOP = { running: "\u{1F7E2}", queued: "\u{1F7E1}", starting: "\u{1F535}", idle: "\u26AA" };
21907
+ const agentLines = agents3.map((a) => {
21908
+ const emoji = STATUS_EMOJI_STOP[a.status] ?? "\u26AA";
21909
+ const shortId = a.id.slice(0, 8);
21910
+ const taskLabel = a.task ? a.task.slice(0, 40) : "no task";
21911
+ const runtimeMs = Date.now() - (/* @__PURE__ */ new Date(a.createdAt + "Z")).getTime();
21912
+ const mins = Math.floor(runtimeMs / 6e4);
21913
+ const runtime = mins < 1 ? "<1m" : `${mins}m`;
21914
+ return `${emoji} ${shortId} \u2014 "${taskLabel}" (${a.status}, ${runtime})`;
21915
+ });
21916
+ const buttons = agents3.map((a) => {
21917
+ const shortId = a.id.slice(0, 8);
21918
+ const taskLabel = a.task ? a.task.slice(0, 20) : a.status;
21919
+ return [{ label: `${shortId}: ${taskLabel}`, data: `stopagent:pick:${shortId}`, style: "danger" }];
21920
+ });
21921
+ buttons.push([
21922
+ { label: "\u{1F6D1} Stop All", data: "stopagent:all", style: "danger" },
21923
+ { label: "Cancel", data: "stopagent:cancel" }
21924
+ ]);
21925
+ await channel.sendKeyboard(chatId, `Stop which agent?
21926
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
21927
+
21928
+ ${agentLines.join("\n")}`, buttons);
21929
+ } else {
21930
+ await channel.sendText(chatId, "Usage: /stopagent <id>\nUse first 8 chars of agent ID or full ID.", { parseMode: "plain" });
21872
21931
  }
21873
- await channel.sendText(chatId, lines.join("\n"), { parseMode: "plain" });
21874
- } catch (err) {
21875
- await channel.sendText(chatId, `Discovery failed: ${err instanceof Error ? err.message : String(err)}`, { parseMode: "plain" });
21932
+ return;
21933
+ }
21934
+ const db3 = getDb();
21935
+ const agents2 = listActiveAgents(db3);
21936
+ const id = commandArgs.trim();
21937
+ const match = agents2.find((a) => a.id === id || a.id.startsWith(id));
21938
+ if (!match) {
21939
+ await channel.sendText(chatId, `No active agent found matching "${id}". Use /agents to list.`, { parseMode: "plain" });
21940
+ return;
21876
21941
  }
21942
+ const ok = cancelAgent(match.id);
21943
+ await channel.sendText(chatId, ok ? `Agent ${match.id.slice(0, 8)} cancelled.` : "Could not cancel agent.", { parseMode: "plain" });
21877
21944
  }
21878
- async function sendHealthCheck(chatId, channel) {
21879
- await channel.sendText(chatId, "\u{1F49A} Pinging servers...", { parseMode: "plain" });
21880
- try {
21881
- const { OllamaService } = await Promise.resolve().then(() => (init_ollama(), ollama_exports));
21882
- const results = await OllamaService.healthCheck();
21883
- if (results.length === 0) {
21884
- await channel.sendText(chatId, "No servers configured.", { parseMode: "plain" });
21885
- return;
21945
+ async function handleMcpCommand(chatId, commandArgs, msg, channel) {
21946
+ const db3 = getDb();
21947
+ const lines = ["<b>\u{1F50C} MCP Servers</b>"];
21948
+ let totalCount = 0;
21949
+ let connectedCount = 0;
21950
+ const centralMcps = listRegisteredMcps(db3);
21951
+ if (centralMcps.length > 0) {
21952
+ lines.push("", "\u2501\u2501 <b>CC-Claw</b> \u2501\u2501");
21953
+ for (const m of centralMcps) {
21954
+ totalCount++;
21955
+ connectedCount++;
21956
+ const autoTag = m.enabledByDefault ? " \u{1F4CC}" : "";
21957
+ const desc = m.description ? ` <i>${m.description}</i>` : "";
21958
+ lines.push(` \u2705 <b>${m.name}</b>${autoTag}${desc}`);
21886
21959
  }
21887
- const lines = ["Health Check Results:", ""];
21888
- for (const s of results) {
21889
- const dot = s.status === "online" ? "\u{1F7E2}" : "\u{1F534}";
21890
- lines.push(`${dot} ${s.name} (${s.host}:${s.port}) \u2014 ${s.status}`);
21960
+ }
21961
+ if (process.env.DASHBOARD_ENABLED === "1") {
21962
+ totalCount++;
21963
+ connectedCount++;
21964
+ lines.push("", "\u2501\u2501 <b>Built-in</b> \u2501\u2501");
21965
+ lines.push(` \u2705 <b>cc-claw</b> <i>Agent orchestrator (spawn, tasks, inbox)</i>`);
21966
+ }
21967
+ const { execFile: execFile5 } = await import("child_process");
21968
+ const { homedir: homedirImport } = await import("os");
21969
+ const discoveryCwd = homedirImport();
21970
+ const runnerResults = await Promise.allSettled(
21971
+ getAllRunners().map((runner) => {
21972
+ const listCmd = runner.getMcpListCommand();
21973
+ if (!listCmd.length) return Promise.resolve({ runner, output: "" });
21974
+ const exe = runner.getExecutablePath();
21975
+ return new Promise((resolve) => {
21976
+ execFile5(exe, listCmd.slice(1), {
21977
+ encoding: "utf-8",
21978
+ timeout: 3e4,
21979
+ cwd: discoveryCwd,
21980
+ env: runner.getEnv()
21981
+ }, (_err, stdout, stderr) => {
21982
+ const combined = ((stdout ?? "") + "\n" + (stderr ?? "")).trim();
21983
+ resolve({ runner, output: combined });
21984
+ });
21985
+ });
21986
+ })
21987
+ );
21988
+ for (const result of runnerResults) {
21989
+ if (result.status !== "fulfilled") continue;
21990
+ const { runner, output: output2 } = result.value;
21991
+ if (!runner.getMcpListCommand().length) continue;
21992
+ const parsed = parseMcpListOutput(output2);
21993
+ lines.push("", `\u2501\u2501 <b>${runner.displayName}</b> \u2501\u2501`);
21994
+ if (parsed.length === 0) {
21995
+ lines.push(" <i>No servers configured</i>");
21996
+ continue;
21997
+ }
21998
+ for (const srv of parsed) {
21999
+ totalCount++;
22000
+ if (srv.connected) connectedCount++;
22001
+ const icon = srv.connected ? "\u2705" : "\u274C";
22002
+ lines.push(` ${icon} <b>${srv.name}</b>`);
21891
22003
  }
21892
- const online = results.filter((s) => s.status === "online").length;
21893
- lines.push("", `${online}/${results.length} online`);
21894
- await channel.sendText(chatId, lines.join("\n"), { parseMode: "plain" });
21895
- } catch (err) {
21896
- await channel.sendText(chatId, `Health check failed: ${err instanceof Error ? err.message : String(err)}`, { parseMode: "plain" });
21897
- }
21898
- }
21899
- async function sendRemoveConfirm(chatId, channel, name) {
21900
- const { OllamaStore } = await Promise.resolve().then(() => (init_ollama(), ollama_exports));
21901
- const server = OllamaStore.getServer(name);
21902
- if (!server) {
21903
- await channel.sendText(chatId, `Server "${name}" not found.`, { parseMode: "plain" });
21904
- return;
21905
22004
  }
21906
- const modelCount = OllamaStore.listModels(server.id).length;
21907
- const text = [
21908
- `Remove server <b>${name}</b>?`,
21909
- "",
21910
- `Host: ${server.host}:${server.port}`,
21911
- `Status: ${server.status}`,
21912
- `Models: ${modelCount}`,
21913
- "",
21914
- "This will also remove all cached model data."
21915
- ].join("\n");
21916
- if (typeof channel.sendKeyboard === "function") {
21917
- await channel.sendKeyboard(chatId, text, [
21918
- [
21919
- { label: "\u2705 Confirm", data: `ollama:remove:${name}`, style: "danger" },
21920
- { label: "\u274C Cancel", data: "ollama:dashboard" }
21921
- ]
21922
- ]);
22005
+ if (totalCount === 0) {
22006
+ lines.push("", "<i>No MCP servers found.</i>");
21923
22007
  } else {
21924
- await channel.sendText(chatId, text + "\n\nUse /ollama remove <name> to confirm.", { parseMode: "html" });
22008
+ lines.splice(1, 0, `<i>${connectedCount}/${totalCount} connected</i>`);
21925
22009
  }
22010
+ await channel.sendText(chatId, lines.join("\n"), { parseMode: "html" });
21926
22011
  }
21927
- async function handleOllamaCallback(chatId, data, channel) {
21928
- const parts = data.split(":");
21929
- const action = parts[1];
21930
- switch (action) {
21931
- case "dashboard":
21932
- return sendOllamaDashboard(chatId, channel);
21933
- case "models":
21934
- return sendModelList(chatId, channel);
21935
- case "discover":
21936
- return sendDiscover(chatId, channel);
21937
- case "health":
21938
- return sendHealthCheck(chatId, channel);
21939
- case "remove_confirm": {
21940
- const name = parts.slice(2).join(":");
21941
- return sendRemoveConfirm(chatId, channel, name);
22012
+ async function handleCronCommand(chatId, commandArgs, msg, channel) {
22013
+ if (!commandArgs) {
22014
+ await sendJobsBoard(chatId, channel, 1);
22015
+ return;
22016
+ }
22017
+ const cronParts = commandArgs.split(/\s+/);
22018
+ const cronSub = cronParts[0].toLowerCase();
22019
+ const cronSubArgs = cronParts.slice(1).join(" ");
22020
+ switch (cronSub) {
22021
+ case "cancel": {
22022
+ if (!cronSubArgs) {
22023
+ await sendJobPicker(chatId, channel, "cancel");
22024
+ return;
22025
+ }
22026
+ const id = parseInt(cronSubArgs, 10);
22027
+ const ok = cancelJob(id);
22028
+ await channel.sendText(chatId, ok ? `Job #${id} cancelled.` : `Job #${id} not found.`, { parseMode: "plain" });
22029
+ break;
21942
22030
  }
21943
- case "remove": {
21944
- const name = parts.slice(2).join(":");
21945
- const { OllamaStore } = await Promise.resolve().then(() => (init_ollama(), ollama_exports));
21946
- const removed = OllamaStore.removeServer(name);
21947
- if (removed) {
21948
- await channel.sendText(chatId, `\u2705 Removed server "${name}"`, { parseMode: "plain" });
22031
+ case "pause": {
22032
+ if (!cronSubArgs) {
22033
+ await sendJobPicker(chatId, channel, "pause");
22034
+ return;
22035
+ }
22036
+ const pauseId = parseInt(cronSubArgs, 10);
22037
+ const paused = pauseJob(pauseId);
22038
+ await channel.sendText(chatId, paused ? `Job #${pauseId} paused.` : `Job #${pauseId} not found.`, { parseMode: "plain" });
22039
+ break;
22040
+ }
22041
+ case "resume": {
22042
+ if (!cronSubArgs) {
22043
+ await sendJobPicker(chatId, channel, "resume");
22044
+ return;
22045
+ }
22046
+ const resumeId = parseInt(cronSubArgs, 10);
22047
+ const resumed = resumeJob(resumeId);
22048
+ await channel.sendText(chatId, resumed ? `Job #${resumeId} resumed.` : `Job #${resumeId} not found.`, { parseMode: "plain" });
22049
+ break;
22050
+ }
22051
+ case "run": {
22052
+ if (!cronSubArgs) {
22053
+ await sendJobPicker(chatId, channel, "run");
22054
+ return;
22055
+ }
22056
+ const runId = parseInt(cronSubArgs, 10);
22057
+ await channel.sendText(chatId, `Triggering job #${runId}...`, { parseMode: "plain" });
22058
+ const runResult = await triggerJob(runId);
22059
+ await channel.sendText(chatId, runResult, { parseMode: "plain" });
22060
+ break;
22061
+ }
22062
+ case "runs": {
22063
+ const runsId = cronSubArgs ? parseInt(cronSubArgs, 10) : void 0;
22064
+ if (runsId) {
22065
+ await sendJobRunsView(chatId, runsId, channel, 1);
21949
22066
  } else {
21950
- await channel.sendText(chatId, `Server "${name}" not found.`, { parseMode: "plain" });
22067
+ const cronRuns2 = getJobRuns(void 0, 10);
22068
+ if (cronRuns2.length === 0) {
22069
+ await channel.sendText(chatId, "No run history.", { parseMode: "plain" });
22070
+ return;
22071
+ }
22072
+ const runLines = cronRuns2.map((r) => {
22073
+ const dur = r.durationMs ? ` (${(r.durationMs / 1e3).toFixed(1)}s)` : "";
22074
+ return `#${r.jobId} [${r.status}] ${formatLocalDateTime(r.startedAt)}${dur}`;
22075
+ });
22076
+ await channel.sendText(chatId, runLines.join("\n\n"), { parseMode: "plain" });
21951
22077
  }
21952
- return sendOllamaDashboard(chatId, channel);
22078
+ break;
22079
+ }
22080
+ case "edit": {
22081
+ if (!cronSubArgs) {
22082
+ await channel.sendText(chatId, "Usage: /cron edit <id>", { parseMode: "plain" });
22083
+ return;
22084
+ }
22085
+ const editId = parseInt(cronSubArgs, 10);
22086
+ await startEditWizard(chatId, editId, channel);
22087
+ break;
22088
+ }
22089
+ case "health": {
22090
+ const report = getHealthReport();
22091
+ await channel.sendText(chatId, formatHealthReport(report), { parseMode: "plain" });
22092
+ break;
21953
22093
  }
21954
- case "add_prompt":
21955
- await channel.sendText(chatId, [
21956
- "To add a server, send:",
21957
- "",
21958
- " /ollama add <name> <host> [port]",
21959
- "",
21960
- "Examples:",
21961
- " /ollama add local 192.168.1.100",
21962
- " /ollama add mac-studio 10.0.0.5 11434",
21963
- " /ollama add cloud api.ollama.example.com 443"
21964
- ].join("\n"), { parseMode: "plain" });
21965
- return;
21966
- case "help":
21967
- await channel.sendText(chatId, [
21968
- "\u{1F999} Ollama Commands:",
21969
- "",
21970
- "/ollama \u2014 Server dashboard",
21971
- "/ollama add <name> <host> [port] \u2014 Add server",
21972
- "/ollama remove <name> \u2014 Remove server",
21973
- "/ollama models [server] \u2014 List models",
21974
- "/ollama discover [server] \u2014 Discover models",
21975
- "/ollama health \u2014 Ping all servers"
21976
- ].join("\n"), { parseMode: "plain" });
21977
- return;
21978
22094
  default:
21979
- await channel.sendText(chatId, "Unknown action.", { parseMode: "plain" });
22095
+ await startWizard(chatId, commandArgs, channel);
21980
22096
  }
21981
22097
  }
21982
- var init_ollama3 = __esm({
21983
- "src/router/ollama.ts"() {
22098
+ var init_command_handlers = __esm({
22099
+ "src/router/command-handlers.ts"() {
21984
22100
  "use strict";
22101
+ init_format();
22102
+ init_log();
22103
+ init_format_time();
22104
+ init_version();
22105
+ init_image_gen();
22106
+ init_stt();
22107
+ init_agent();
22108
+ init_classify();
22109
+ init_install();
22110
+ init_profile();
22111
+ init_heartbeat2();
22112
+ init_discover();
22113
+ init_store5();
22114
+ init_summarize();
22115
+ init_session_log();
22116
+ init_backends();
22117
+ init_cron();
22118
+ init_wizard();
22119
+ init_health2();
22120
+ init_store();
22121
+ init_orchestrator();
22122
+ init_registry();
22123
+ init_registry2();
22124
+ init_types();
22125
+ init_store3();
22126
+ init_helpers();
22127
+ init_shell();
22128
+ init_ui();
22129
+ init_state();
22130
+ init_evolve2();
22131
+ init_optimize();
21985
22132
  }
21986
22133
  });
21987
22134
 
@@ -22525,7 +22672,7 @@ ${plan.originalMessage}`;
22525
22672
  await sendJobsBoard(chatId, channel, 1);
22526
22673
  } else if (rest.startsWith("view:")) {
22527
22674
  const id = parseInt(rest.slice(5), 10);
22528
- await sendJobDetail(chatId, id, channel);
22675
+ await sendJobDetail(chatId, id, channel, messageId);
22529
22676
  } else if (rest.startsWith("run:")) {
22530
22677
  const id = parseInt(rest.slice(4), 10);
22531
22678
  await channel.sendText(chatId, `Triggering job #${id}...`, { parseMode: "plain" });
@@ -22541,7 +22688,7 @@ ${plan.originalMessage}`;
22541
22688
  const id = parseInt(rest.slice(6), 10);
22542
22689
  const paused = pauseJob(id);
22543
22690
  if (paused) {
22544
- await sendJobDetail(chatId, id, channel);
22691
+ await sendJobDetail(chatId, id, channel, messageId);
22545
22692
  } else {
22546
22693
  await channel.sendText(chatId, `Job #${id} not found.`, { parseMode: "plain" });
22547
22694
  }
@@ -22549,7 +22696,7 @@ ${plan.originalMessage}`;
22549
22696
  const id = parseInt(rest.slice(7), 10);
22550
22697
  const resumed = resumeJob(id);
22551
22698
  if (resumed) {
22552
- await sendJobDetail(chatId, id, channel);
22699
+ await sendJobDetail(chatId, id, channel, messageId);
22553
22700
  } else {
22554
22701
  await channel.sendText(chatId, `Job #${id} not found.`, { parseMode: "plain" });
22555
22702
  }
@@ -22588,17 +22735,19 @@ This cannot be undone.`,
22588
22735
  const jobId = parseInt(parts[0], 10);
22589
22736
  const page = isPaginationCallback(data, `job:runs:${jobId}:`);
22590
22737
  if (page !== null) {
22591
- await sendJobRunsView(chatId, jobId, channel, page);
22738
+ await sendJobRunsView(chatId, jobId, channel, page, messageId);
22592
22739
  } else {
22593
- await sendJobRunsView(chatId, jobId, channel, 1);
22740
+ await sendJobRunsView(chatId, jobId, channel, 1, messageId);
22594
22741
  }
22595
22742
  } else if (rest.startsWith("edit:")) {
22596
22743
  const id = parseInt(rest.slice(5), 10);
22597
22744
  await startEditWizard(chatId, id, channel);
22745
+ } else if (rest === "back") {
22746
+ await sendJobsBoard(chatId, channel, 1, messageId);
22598
22747
  } else {
22599
22748
  const page = isPaginationCallback(data, "job:");
22600
22749
  if (page !== null) {
22601
- await sendJobsBoard(chatId, channel, page);
22750
+ await sendJobsBoard(chatId, channel, page, messageId);
22602
22751
  }
22603
22752
  }
22604
22753
  return;
@@ -22991,7 +23140,7 @@ ${rotationNote}`, { parseMode: "html" });
22991
23140
  const rest = data.slice(7);
22992
23141
  const page = isPaginationCallback(data, "forget:");
22993
23142
  if (page !== null) {
22994
- await sendForgetPicker(chatId, channel, page);
23143
+ await sendForgetPicker(chatId, channel, page, messageId);
22995
23144
  } else if (rest.startsWith("pick:")) {
22996
23145
  const id = parseInt(rest.slice(5), 10);
22997
23146
  const memory2 = getMemoryById(id);
@@ -23032,7 +23181,7 @@ Salience: ${memory2.salience.toFixed(2)} | Created: ${memory2.created_at.slice(0
23032
23181
  const rest = data.slice(4);
23033
23182
  const page = isPaginationCallback(data, "mem:");
23034
23183
  if (page !== null) {
23035
- await sendMemoryPage(chatId, channel, page);
23184
+ await sendMemoryPage(chatId, channel, page, messageId);
23036
23185
  } else if (rest.startsWith("view:")) {
23037
23186
  const id = parseInt(rest.slice(5), 10);
23038
23187
  const memory2 = getMemoryById(id);
@@ -23093,7 +23242,7 @@ Salience: ${memory2.salience.toFixed(2)} | Created: ${memory2.created_at.slice(0
23093
23242
  const id = parseInt(rest.slice(5), 10);
23094
23243
  await channel.sendText(chatId, `Type: /memory edit ${id} <new content>`, { parseMode: "plain" });
23095
23244
  } else if (rest === "back") {
23096
- await sendMemoryPage(chatId, channel, 1);
23245
+ await sendMemoryPage(chatId, channel, 1, messageId);
23097
23246
  } else if (rest === "showall") {
23098
23247
  const memories = listMemories();
23099
23248
  const lines = memories.map(
@@ -23172,7 +23321,7 @@ Example: /limits ${bid} daily 500000`, { parseMode: "plain" });
23172
23321
  } else if (data.startsWith("skills:page:")) {
23173
23322
  const page = parseInt(data.slice(12), 10);
23174
23323
  const skills2 = await discoverAllSkills();
23175
- await sendSkillsPage(chatId, channel, skills2, page);
23324
+ await sendSkillsPage(chatId, channel, skills2, page, messageId);
23176
23325
  } else if (data.startsWith("skill:")) {
23177
23326
  const parts = data.slice(6).split(":");
23178
23327
  let skillName;
@@ -23347,6 +23496,10 @@ async function handleText(msg, channel) {
23347
23496
  await handleWizardText(chatId, text, channel);
23348
23497
  return;
23349
23498
  }
23499
+ if (hasOllamaWizard(chatId)) {
23500
+ await handleOllamaWizardText(chatId, text, channel);
23501
+ return;
23502
+ }
23350
23503
  const rememberMatch = text.match(/^remember\s+(?:that\s+)?(.+)/i);
23351
23504
  if (rememberMatch) {
23352
23505
  const content = rememberMatch[1];
@@ -23807,6 +23960,7 @@ var init_router2 = __esm({
23807
23960
  init_store5();
23808
23961
  init_backends();
23809
23962
  init_wizard();
23963
+ init_ollama3();
23810
23964
  init_classify2();
23811
23965
  init_session_log2();
23812
23966
  init_live_status();
@@ -25283,6 +25437,46 @@ var init_telegram2 = __esm({
25283
25437
  }
25284
25438
  }
25285
25439
  }
25440
+ async editKeyboard(chatId, messageId, text, buttons) {
25441
+ const keyboard = new InlineKeyboard();
25442
+ for (const row of buttons) {
25443
+ for (const btn of row) {
25444
+ keyboard.text(btn.label, btn.data);
25445
+ if (btn.style === "success") keyboard.success();
25446
+ else if (btn.style === "danger") keyboard.danger();
25447
+ else if (btn.style === "primary") keyboard.primary();
25448
+ }
25449
+ keyboard.row();
25450
+ }
25451
+ const formatted = sanitizeForTelegram(formatForTelegram(text));
25452
+ try {
25453
+ await withRetry(
25454
+ "editKeyboard:html",
25455
+ () => this.bot.api.editMessageText(numericChatId(chatId), parseInt(messageId), formatted, {
25456
+ parse_mode: "HTML",
25457
+ reply_markup: keyboard
25458
+ })
25459
+ );
25460
+ return true;
25461
+ } catch (err) {
25462
+ if (isRateLimitError(err)) return false;
25463
+ try {
25464
+ await withRetry(
25465
+ "editKeyboard:plain",
25466
+ () => this.bot.api.editMessageText(
25467
+ numericChatId(chatId),
25468
+ parseInt(messageId),
25469
+ formatted.replace(/<[^>]+>/g, ""),
25470
+ { reply_markup: keyboard }
25471
+ )
25472
+ );
25473
+ return true;
25474
+ } catch (err2) {
25475
+ warn("[telegram] editKeyboard failed:", err2 instanceof Error ? err2.message : err2);
25476
+ return false;
25477
+ }
25478
+ }
25479
+ }
25286
25480
  /** Register a handler for inline keyboard callback queries */
25287
25481
  onCallback(handler) {
25288
25482
  this.callbackHandlers.push(handler);
@@ -27338,9 +27532,10 @@ var init_status = __esm({
27338
27532
  // src/cli/commands/doctor.ts
27339
27533
  var doctor_exports = {};
27340
27534
  __export(doctor_exports, {
27341
- doctorCommand: () => doctorCommand
27535
+ doctorCommand: () => doctorCommand,
27536
+ doctorErrors: () => doctorErrors
27342
27537
  });
27343
- import { existsSync as existsSync30, statSync as statSync10, accessSync, constants } from "fs";
27538
+ import { existsSync as existsSync30, statSync as statSync10, accessSync, readFileSync as readFileSync20, constants } from "fs";
27344
27539
  import { execFileSync as execFileSync4 } from "child_process";
27345
27540
  async function doctorCommand(globalOpts, localOpts) {
27346
27541
  const checks = [];
@@ -27456,8 +27651,8 @@ async function doctorCommand(globalOpts, localOpts) {
27456
27651
  }
27457
27652
  if (existsSync30(ERROR_LOG_PATH)) {
27458
27653
  try {
27459
- const { readFileSync: readFileSync27 } = await import("fs");
27460
- const logContent = readFileSync27(ERROR_LOG_PATH, "utf-8");
27654
+ const { readFileSync: readFileSync28 } = await import("fs");
27655
+ const logContent = readFileSync28(ERROR_LOG_PATH, "utf-8");
27461
27656
  const recentLines = logContent.split("\n").filter(Boolean).slice(-500);
27462
27657
  const last24h = Date.now() - 864e5;
27463
27658
  const recentErrors = recentLines.filter((line) => {
@@ -27469,29 +27664,38 @@ async function doctorCommand(globalOpts, localOpts) {
27469
27664
  let rate429 = 0;
27470
27665
  let contentSilence = 0;
27471
27666
  let spawnTimeout = 0;
27472
- let other = 0;
27667
+ const otherLines = [];
27473
27668
  for (const line of recentErrors) {
27474
27669
  if (/429|rate.?limit/i.test(line)) rate429++;
27475
27670
  else if (/content silence/i.test(line)) contentSilence++;
27476
27671
  else if (/spawn timeout|timeout after \d+s/i.test(line)) spawnTimeout++;
27477
- else other++;
27672
+ else otherLines.push(line);
27478
27673
  }
27479
- const logFix = "cc-claw doctor --fix to clear stale errors";
27674
+ const viewFix = "cc-claw logs --error to view details";
27675
+ const clearFix = "cc-claw doctor --fix to clear stale errors";
27480
27676
  if (rate429 > 10) {
27481
- checks.push({ name: "Telegram rate limits", status: "error", message: `${rate429} rate-limit (429) errors in last 24h \u2014 message delivery blocked`, fix: logFix });
27677
+ checks.push({ name: "Telegram rate limits", status: "error", message: `${rate429} rate-limit (429) errors in last 24h \u2014 message delivery blocked`, fix: `${viewFix}, ${clearFix}` });
27482
27678
  } else if (rate429 > 0) {
27483
- checks.push({ name: "Telegram rate limits", status: "warning", message: `${rate429} rate-limit (429) errors in last 24h`, fix: logFix });
27679
+ checks.push({ name: "Telegram rate limits", status: "warning", message: `${rate429} rate-limit (429) errors in last 24h`, fix: `${viewFix}, ${clearFix}` });
27484
27680
  }
27485
27681
  if (contentSilence > 0) {
27486
- checks.push({ name: "Content silence", status: "warning", message: `${contentSilence} agent silence timeout(s) in last 24h \u2014 API went unresponsive`, fix: logFix });
27682
+ checks.push({ name: "Content silence", status: "warning", message: `${contentSilence} agent silence timeout(s) in last 24h \u2014 API went unresponsive`, fix: `${viewFix}, ${clearFix}` });
27487
27683
  }
27488
27684
  if (spawnTimeout > 0) {
27489
- checks.push({ name: "Spawn timeouts", status: "warning", message: `${spawnTimeout} backend timeout(s) in last 24h`, fix: logFix });
27490
- }
27491
- if (other > 0) {
27492
- checks.push({ name: "Other errors", status: "warning", message: `${other} other error(s) in last 24h`, fix: logFix });
27685
+ checks.push({ name: "Spawn timeouts", status: "warning", message: `${spawnTimeout} backend timeout(s) in last 24h`, fix: `${viewFix}, ${clearFix}` });
27686
+ }
27687
+ if (otherLines.length > 0) {
27688
+ const summaries = /* @__PURE__ */ new Map();
27689
+ for (const line of otherLines) {
27690
+ const body = line.replace(/^\[\d{4}-\d{2}-\d{2}\s+[\d:+-]+\]\s*/, "").slice(0, 120);
27691
+ const key = body.slice(0, 80);
27692
+ summaries.set(key, (summaries.get(key) ?? 0) + 1);
27693
+ }
27694
+ const topErrors = [...summaries.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([msg, count]) => count > 1 ? `${msg}\u2026 (\xD7${count})` : `${msg}\u2026`).join("\n ");
27695
+ checks.push({ name: "Other errors", status: "warning", message: `${otherLines.length} error(s) in last 24h:
27696
+ ${topErrors}`, fix: `${viewFix}, ${clearFix}` });
27493
27697
  }
27494
- if (rate429 === 0 && contentSilence === 0 && spawnTimeout === 0 && other === 0) {
27698
+ if (rate429 === 0 && contentSilence === 0 && spawnTimeout === 0 && otherLines.length === 0) {
27495
27699
  checks.push({ name: "Recent errors", status: "ok", message: "none in last 24h" });
27496
27700
  }
27497
27701
  } else {
@@ -27631,6 +27835,86 @@ async function doctorCommand(globalOpts, localOpts) {
27631
27835
  });
27632
27836
  process.exit(errors > 0 ? 1 : warnings > 0 ? 2 : 0);
27633
27837
  }
27838
+ function getRecentErrors() {
27839
+ if (!existsSync30(ERROR_LOG_PATH)) return null;
27840
+ const logContent = readFileSync20(ERROR_LOG_PATH, "utf-8");
27841
+ const allLines = logContent.split("\n").filter(Boolean).slice(-500);
27842
+ const last24h = Date.now() - 864e5;
27843
+ const lines = allLines.filter((line) => {
27844
+ const match = line.match(/^\[(\d{4}-\d{2}-\d{2})\s+([\d:+-]+)/);
27845
+ if (match) return (/* @__PURE__ */ new Date(`${match[1]}T${match[2]}`)).getTime() > last24h;
27846
+ return false;
27847
+ });
27848
+ if (lines.length === 0) return null;
27849
+ const classified = { rate429: [], contentSilence: [], spawnTimeout: [], other: [] };
27850
+ for (const line of lines) {
27851
+ if (/429|rate.?limit/i.test(line)) classified.rate429.push(line);
27852
+ else if (/content silence/i.test(line)) classified.contentSilence.push(line);
27853
+ else if (/spawn timeout|timeout after \d+s/i.test(line)) classified.spawnTimeout.push(line);
27854
+ else classified.other.push(line);
27855
+ }
27856
+ return { lines, classified };
27857
+ }
27858
+ function stripTimestamp(line) {
27859
+ const match = line.match(/^\[(\d{4}-\d{2}-\d{2})\s+([\d:+-]+)\]\s*(.*)/);
27860
+ if (match) return { time: match[2].replace(/[-+]\d{4}$/, ""), body: match[3] };
27861
+ return { time: "", body: line };
27862
+ }
27863
+ async function doctorErrors(globalOpts) {
27864
+ const result = getRecentErrors();
27865
+ if (!result) {
27866
+ output({ errors: [] }, () => `
27867
+ ${success("No errors in last 24h.")}
27868
+ `);
27869
+ return;
27870
+ }
27871
+ const { classified } = result;
27872
+ const total = classified.rate429.length + classified.contentSilence.length + classified.spawnTimeout.length + classified.other.length;
27873
+ output({ total, classified }, () => {
27874
+ const lines = [
27875
+ "",
27876
+ box("Recent Errors (last 24h)"),
27877
+ ""
27878
+ ];
27879
+ const renderCategory = (label2, items, maxShow) => {
27880
+ if (items.length === 0) return;
27881
+ const deduped = /* @__PURE__ */ new Map();
27882
+ for (const line of items) {
27883
+ const { time, body } = stripTimestamp(line);
27884
+ const key = body.slice(0, 120);
27885
+ const existing = deduped.get(key);
27886
+ if (existing) {
27887
+ existing.count++;
27888
+ existing.lastTime = time;
27889
+ } else {
27890
+ deduped.set(key, { count: 1, lastTime: time });
27891
+ }
27892
+ }
27893
+ lines.push(` ${warnMark()} ${label2} (${items.length})`);
27894
+ const entries = [...deduped.entries()].sort((a, b) => b[1].count - a[1].count).slice(0, maxShow);
27895
+ for (const [msg, { count, lastTime }] of entries) {
27896
+ const timeStr = lastTime ? `[${lastTime}] ` : "";
27897
+ const countStr = count > 1 ? muted(` (\xD7${count})`) : "";
27898
+ lines.push(` ${timeStr}${msg.slice(0, 100)}${countStr}`);
27899
+ }
27900
+ if (deduped.size > maxShow) {
27901
+ lines.push(muted(` \u2026 and ${deduped.size - maxShow} more unique error(s)`));
27902
+ }
27903
+ lines.push("");
27904
+ };
27905
+ renderCategory("Telegram rate limits", classified.rate429, 5);
27906
+ renderCategory("Content silence timeouts", classified.contentSilence, 5);
27907
+ renderCategory("Spawn timeouts", classified.spawnTimeout, 5);
27908
+ renderCategory("Other errors", classified.other, 10);
27909
+ if (total === 0) {
27910
+ lines.push(` ${success("No errors in last 24h.")}`);
27911
+ } else {
27912
+ lines.push(muted(` ${total} total error(s). Run cc-claw doctor --fix to clear.`));
27913
+ }
27914
+ lines.push("");
27915
+ return lines.join("\n");
27916
+ });
27917
+ }
27634
27918
  function formatUptime3(seconds) {
27635
27919
  seconds = Math.floor(seconds);
27636
27920
  if (seconds < 60) return `${seconds}s`;
@@ -27652,7 +27936,7 @@ var logs_exports = {};
27652
27936
  __export(logs_exports, {
27653
27937
  logsCommand: () => logsCommand
27654
27938
  });
27655
- import { existsSync as existsSync31, readFileSync as readFileSync20, watchFile as watchFile2, unwatchFile as unwatchFile2 } from "fs";
27939
+ import { existsSync as existsSync31, readFileSync as readFileSync21, watchFile as watchFile2, unwatchFile as unwatchFile2 } from "fs";
27656
27940
  async function logsCommand(opts) {
27657
27941
  const logFile = opts.error ? ERROR_LOG_PATH : LOG_PATH;
27658
27942
  if (!existsSync31(logFile)) {
@@ -27660,7 +27944,7 @@ async function logsCommand(opts) {
27660
27944
  process.exit(1);
27661
27945
  }
27662
27946
  const maxLines = parseInt(opts.lines ?? "100", 10);
27663
- const content = readFileSync20(logFile, "utf-8");
27947
+ const content = readFileSync21(logFile, "utf-8");
27664
27948
  const allLines = content.split("\n");
27665
27949
  const tailLines = allLines.slice(-maxLines);
27666
27950
  console.log(muted(` \u2500\u2500 ${logFile} (last ${tailLines.length} lines) \u2500\u2500`));
@@ -27670,7 +27954,7 @@ async function logsCommand(opts) {
27670
27954
  let lastLength = content.length;
27671
27955
  watchFile2(logFile, { interval: 500 }, () => {
27672
27956
  try {
27673
- const newContent = readFileSync20(logFile, "utf-8");
27957
+ const newContent = readFileSync21(logFile, "utf-8");
27674
27958
  if (newContent.length > lastLength) {
27675
27959
  const newPart = newContent.slice(lastLength);
27676
27960
  process.stdout.write(newPart);
@@ -27702,7 +27986,7 @@ __export(session_logs_exports, {
27702
27986
  sessionLogsList: () => sessionLogsList,
27703
27987
  sessionLogsTail: () => sessionLogsTail
27704
27988
  });
27705
- import { readFileSync as readFileSync21, watchFile as watchFile3, unwatchFile as unwatchFile3 } from "fs";
27989
+ import { readFileSync as readFileSync22, watchFile as watchFile3, unwatchFile as unwatchFile3 } from "fs";
27706
27990
  async function sessionLogsList(opts) {
27707
27991
  const logs = listSessionLogs();
27708
27992
  if (logs.length === 0) {
@@ -27759,12 +28043,12 @@ async function sessionLogsTail(opts) {
27759
28043
  console.log(muted("\n Following... (Ctrl+C to stop)\n"));
27760
28044
  let lastLength = 0;
27761
28045
  try {
27762
- lastLength = readFileSync21(targetPath, "utf-8").length;
28046
+ lastLength = readFileSync22(targetPath, "utf-8").length;
27763
28047
  } catch {
27764
28048
  }
27765
28049
  watchFile3(targetPath, { interval: 500 }, () => {
27766
28050
  try {
27767
- const content = readFileSync21(targetPath, "utf-8");
28051
+ const content = readFileSync22(targetPath, "utf-8");
27768
28052
  if (content.length > lastLength) {
27769
28053
  process.stdout.write(content.slice(lastLength));
27770
28054
  lastLength = content.length;
@@ -27806,7 +28090,7 @@ __export(gemini_exports, {
27806
28090
  geminiReorder: () => geminiReorder,
27807
28091
  geminiRotation: () => geminiRotation
27808
28092
  });
27809
- import { existsSync as existsSync33, mkdirSync as mkdirSync14, writeFileSync as writeFileSync10, readFileSync as readFileSync22, chmodSync } from "fs";
28093
+ import { existsSync as existsSync33, mkdirSync as mkdirSync14, writeFileSync as writeFileSync10, readFileSync as readFileSync23, chmodSync } from "fs";
27810
28094
  import { join as join31 } from "path";
27811
28095
  import { createInterface as createInterface8 } from "readline";
27812
28096
  function requireDb() {
@@ -27837,7 +28121,7 @@ function resolveOAuthEmail(configHome) {
27837
28121
  try {
27838
28122
  const accountsPath = join31(configHome, ".gemini", "google_accounts.json");
27839
28123
  if (!existsSync33(accountsPath)) return null;
27840
- const accounts = JSON.parse(readFileSync22(accountsPath, "utf-8"));
28124
+ const accounts = JSON.parse(readFileSync23(accountsPath, "utf-8"));
27841
28125
  return accounts.active || null;
27842
28126
  } catch {
27843
28127
  return null;
@@ -28071,7 +28355,7 @@ __export(backend_cmd_factory_exports, {
28071
28355
  makeReorder: () => makeReorder,
28072
28356
  registerBackendSlotCommands: () => registerBackendSlotCommands
28073
28357
  });
28074
- import { existsSync as existsSync34, mkdirSync as mkdirSync15, readFileSync as readFileSync23 } from "fs";
28358
+ import { existsSync as existsSync34, mkdirSync as mkdirSync15, readFileSync as readFileSync24 } from "fs";
28075
28359
  import { join as join32 } from "path";
28076
28360
  import { createInterface as createInterface9 } from "readline";
28077
28361
  function requireDb2() {
@@ -28343,7 +28627,7 @@ var init_backend_cmd_factory = __esm({
28343
28627
  const claudeJsonNested = join32(slotDir, ".claude", ".claude.json");
28344
28628
  if (existsSync34(claudeJson)) {
28345
28629
  try {
28346
- const data = JSON.parse(readFileSync23(claudeJson, "utf-8"));
28630
+ const data = JSON.parse(readFileSync24(claudeJson, "utf-8"));
28347
28631
  return Boolean(data.oauthAccount);
28348
28632
  } catch {
28349
28633
  return false;
@@ -28351,7 +28635,7 @@ var init_backend_cmd_factory = __esm({
28351
28635
  }
28352
28636
  if (existsSync34(claudeJsonNested)) {
28353
28637
  try {
28354
- const data = JSON.parse(readFileSync23(claudeJsonNested, "utf-8"));
28638
+ const data = JSON.parse(readFileSync24(claudeJsonNested, "utf-8"));
28355
28639
  return Boolean(data.oauthAccount);
28356
28640
  } catch {
28357
28641
  return false;
@@ -28374,7 +28658,7 @@ var init_backend_cmd_factory = __esm({
28374
28658
  try {
28375
28659
  const claudeJson = join32(slotDir, ".claude.json");
28376
28660
  if (existsSync34(claudeJson)) {
28377
- const data = JSON.parse(readFileSync23(claudeJson, "utf-8"));
28661
+ const data = JSON.parse(readFileSync24(claudeJson, "utf-8"));
28378
28662
  if (data.oauthAccount?.emailAddress) return data.oauthAccount.emailAddress;
28379
28663
  }
28380
28664
  } catch {
@@ -28393,7 +28677,7 @@ var init_backend_cmd_factory = __esm({
28393
28677
  },
28394
28678
  extractLabel: (slotDir) => {
28395
28679
  try {
28396
- const authData = JSON.parse(readFileSync23(join32(slotDir, "auth.json"), "utf-8"));
28680
+ const authData = JSON.parse(readFileSync24(join32(slotDir, "auth.json"), "utf-8"));
28397
28681
  if (authData.email) return authData.email;
28398
28682
  if (authData.account_name) return authData.account_name;
28399
28683
  if (authData.user?.email) return authData.user.email;
@@ -29638,7 +29922,7 @@ __export(config_exports2, {
29638
29922
  configList: () => configList,
29639
29923
  configSet: () => configSet
29640
29924
  });
29641
- import { existsSync as existsSync43, readFileSync as readFileSync24 } from "fs";
29925
+ import { existsSync as existsSync43, readFileSync as readFileSync25 } from "fs";
29642
29926
  async function configList(globalOpts) {
29643
29927
  if (!existsSync43(DB_PATH)) {
29644
29928
  outputError("DB_NOT_FOUND", "Database not found.");
@@ -29724,7 +30008,7 @@ async function configEnv(_globalOpts) {
29724
30008
  outputError("ENV_NOT_FOUND", `No .env file at ${ENV_PATH}. Run cc-claw setup.`);
29725
30009
  process.exit(1);
29726
30010
  }
29727
- const content = readFileSync24(ENV_PATH, "utf-8");
30011
+ const content = readFileSync25(ENV_PATH, "utf-8");
29728
30012
  const entries = {};
29729
30013
  const secretPatterns = /TOKEN|KEY|SECRET|PASSWORD|CREDENTIALS/i;
29730
30014
  for (const line of content.split("\n")) {
@@ -30473,11 +30757,11 @@ __export(chat_exports2, {
30473
30757
  chatSend: () => chatSend
30474
30758
  });
30475
30759
  import { request as httpRequest2 } from "http";
30476
- import { readFileSync as readFileSync25, existsSync as existsSync53 } from "fs";
30760
+ import { readFileSync as readFileSync26, existsSync as existsSync53 } from "fs";
30477
30761
  function getToken2() {
30478
30762
  if (process.env.CC_CLAW_API_TOKEN) return process.env.CC_CLAW_API_TOKEN;
30479
30763
  try {
30480
- if (existsSync53(TOKEN_PATH2)) return readFileSync25(TOKEN_PATH2, "utf-8").trim();
30764
+ if (existsSync53(TOKEN_PATH2)) return readFileSync26(TOKEN_PATH2, "utf-8").trim();
30481
30765
  } catch {
30482
30766
  }
30483
30767
  return null;
@@ -31423,7 +31707,7 @@ var init_optimize2 = __esm({
31423
31707
 
31424
31708
  // src/setup.ts
31425
31709
  var setup_exports = {};
31426
- import { existsSync as existsSync55, writeFileSync as writeFileSync12, readFileSync as readFileSync26, copyFileSync as copyFileSync4, mkdirSync as mkdirSync18, statSync as statSync12 } from "fs";
31710
+ import { existsSync as existsSync55, writeFileSync as writeFileSync12, readFileSync as readFileSync27, copyFileSync as copyFileSync4, mkdirSync as mkdirSync18, statSync as statSync12 } from "fs";
31427
31711
  import { execFileSync as execFileSync5 } from "child_process";
31428
31712
  import { createInterface as createInterface11 } from "readline";
31429
31713
  import { join as join34 } from "path";
@@ -31508,7 +31792,7 @@ async function setup() {
31508
31792
  if (envSource) {
31509
31793
  console.log(yellow(` Found existing config at ${envSource} \u2014 your values will be preserved`));
31510
31794
  console.log(yellow(" unless you enter new ones. Just press Enter to keep existing values.\n"));
31511
- const existing = readFileSync26(envSource, "utf-8");
31795
+ const existing = readFileSync27(envSource, "utf-8");
31512
31796
  for (const line of existing.split("\n")) {
31513
31797
  const match = line.match(/^([^#=]+)=(.*)$/);
31514
31798
  if (match) env[match[1].trim()] = match[2].trim();
@@ -31849,9 +32133,14 @@ program.command("status").description("Comprehensive system status").option("--d
31849
32133
  const { statusCommand: statusCommand2 } = await Promise.resolve().then(() => (init_status(), status_exports));
31850
32134
  await statusCommand2(program.opts(), opts);
31851
32135
  });
31852
- program.command("doctor").description("Diagnose common issues and suggest fixes").option("--fix", "Auto-apply safe fixes").action(async (opts) => {
31853
- const { doctorCommand: doctorCommand2 } = await Promise.resolve().then(() => (init_doctor(), doctor_exports));
31854
- await doctorCommand2(program.opts(), opts);
32136
+ program.command("doctor").description("Diagnose common issues and suggest fixes").option("--fix", "Auto-apply safe fixes").argument("[subcommand]", "Sub-command: errors \u2014 show recent errors classified").action(async (subcommand, opts) => {
32137
+ if (subcommand === "errors") {
32138
+ const { doctorErrors: doctorErrors2 } = await Promise.resolve().then(() => (init_doctor(), doctor_exports));
32139
+ await doctorErrors2(program.opts());
32140
+ } else {
32141
+ const { doctorCommand: doctorCommand2 } = await Promise.resolve().then(() => (init_doctor(), doctor_exports));
32142
+ await doctorCommand2(program.opts(), opts);
32143
+ }
31855
32144
  });
31856
32145
  program.command("logs").description("Tail daemon logs").option("-f, --follow", "Follow mode (like tail -f)").option("--error", "Show error log instead of stdout").option("--lines <n>", "Number of lines", "100").action(async (opts) => {
31857
32146
  const { logsCommand: logsCommand2 } = await Promise.resolve().then(() => (init_logs(), logs_exports));