cc-claw 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +5 -7
  2. package/dist/cli.js +140 -101
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -61,19 +61,17 @@ Config saved to `~/.cc-claw/.env`.
61
61
 
62
62
  ## Run
63
63
 
64
- ```bash
65
- # Background service (recommended)
66
- cc-claw service install
64
+ The setup wizard offers to install CC-Claw as a background service. If you skipped that step:
67
65
 
68
- # Or foreground
69
- cc-claw start
66
+ ```bash
67
+ cc-claw service install # Install + start as background service
70
68
  ```
71
69
 
72
70
  ```bash
73
- cc-claw service start # Start
74
- cc-claw service stop # Stop
75
71
  cc-claw service restart # Restart (pick up config changes)
72
+ cc-claw service stop # Stop the daemon
76
73
  cc-claw service status # Check if running
74
+ cc-claw start # Run in foreground (for debugging)
77
75
  ```
78
76
 
79
77
  ## CLI
package/dist/cli.js CHANGED
@@ -48,7 +48,7 @@ var VERSION;
48
48
  var init_version = __esm({
49
49
  "src/version.ts"() {
50
50
  "use strict";
51
- VERSION = true ? "0.2.3" : (() => {
51
+ VERSION = true ? "0.2.5" : (() => {
52
52
  try {
53
53
  return JSON.parse(readFileSync(join2(process.cwd(), "package.json"), "utf-8")).version ?? "unknown";
54
54
  } catch {
@@ -345,7 +345,7 @@ function claimTask(db3, taskId, agentId) {
345
345
  if (blockedBy.length > 0) {
346
346
  const placeholders = blockedBy.map(() => "?").join(",");
347
347
  const incomplete = db3.prepare(
348
- `SELECT COUNT(*) as count FROM agent_tasks WHERE id IN (${placeholders}) AND status NOT IN ('completed')`
348
+ `SELECT COUNT(*) as count FROM agent_tasks WHERE id IN (${placeholders}) AND status NOT IN ('completed', 'failed', 'abandoned')`
349
349
  ).get(...blockedBy);
350
350
  if (incomplete.count > 0) return false;
351
351
  }
@@ -518,19 +518,18 @@ function cleanupActivity(db3) {
518
518
  if (aged.changes > 0) {
519
519
  log(`[activity] Pruned ${aged.changes} events older than ${RETENTION_DAYS} days`);
520
520
  }
521
- const chatIds = db3.prepare(
522
- "SELECT DISTINCT chatId FROM activity_log"
523
- ).all();
524
- for (const { chatId } of chatIds) {
525
- const excess = db3.prepare(`
526
- DELETE FROM activity_log
527
- WHERE chatId = ? AND id NOT IN (
528
- SELECT id FROM activity_log WHERE chatId = ? ORDER BY createdAt DESC LIMIT ?
529
- )
530
- `).run(chatId, chatId, MAX_ROWS_PER_CHAT);
531
- if (excess.changes > 0) {
532
- log(`[activity] Capped chat ${chatId}: pruned ${excess.changes} excess rows`);
533
- }
521
+ const excess = db3.prepare(`
522
+ DELETE FROM activity_log
523
+ WHERE id NOT IN (
524
+ SELECT id FROM (
525
+ SELECT id, ROW_NUMBER() OVER (PARTITION BY chatId ORDER BY createdAt DESC) AS rn
526
+ FROM activity_log
527
+ ) ranked
528
+ WHERE rn <= ?
529
+ )
530
+ `).run(MAX_ROWS_PER_CHAT);
531
+ if (excess.changes > 0) {
532
+ log(`[activity] Capped activity log: pruned ${excess.changes} excess rows`);
534
533
  }
535
534
  }
536
535
  var RETENTION_DAYS, MAX_ROWS_PER_CHAT;
@@ -1361,7 +1360,7 @@ function searchMemories(queryText, limit = 3) {
1361
1360
  for (const mem of ftsResults) {
1362
1361
  db.prepare(`
1363
1362
  UPDATE memories
1364
- SET salience = MIN(salience + 0.1, 5.0),
1363
+ SET salience = MAX(0, MIN(1.0, salience + 0.1)),
1365
1364
  access_count = access_count + 1,
1366
1365
  last_accessed = datetime('now')
1367
1366
  WHERE id = ?
@@ -1640,7 +1639,7 @@ function searchSessionSummaries(queryText, limit = 3) {
1640
1639
  for (const s of results) {
1641
1640
  db.prepare(`
1642
1641
  UPDATE session_summaries
1643
- SET salience = MIN(salience + 0.1, 5.0)
1642
+ SET salience = MAX(0, MIN(1.0, salience + 0.1))
1644
1643
  WHERE id = ?
1645
1644
  `).run(s.id);
1646
1645
  }
@@ -2233,7 +2232,7 @@ var init_claude = __esm({
2233
2232
  }
2234
2233
  try {
2235
2234
  this._resolvedPath = execSync("which claude", { encoding: "utf-8" }).trim();
2236
- } catch {
2235
+ } catch (_err) {
2237
2236
  const candidates = [
2238
2237
  "/opt/homebrew/bin/claude",
2239
2238
  "/usr/local/bin/claude"
@@ -2712,8 +2711,7 @@ function normalizeFtsScores(items) {
2712
2711
  const ranks = items.map((it) => Math.abs(it._ftsRank ?? 0));
2713
2712
  const maxRank = Math.max(...ranks, 1e-3);
2714
2713
  for (let i = 0; i < items.length; i++) {
2715
- const id = items[i].id;
2716
- scores.set(id, ranks[i] / maxRank);
2714
+ scores.set(items[i].id, ranks[i] / maxRank);
2717
2715
  }
2718
2716
  return scores;
2719
2717
  }
@@ -2929,13 +2927,13 @@ function syncNativeCliFiles() {
2929
2927
  } catch {
2930
2928
  }
2931
2929
  try {
2932
- const soulStat = statSync(SOUL_PATH);
2933
- const userStat = statSync(USER_PATH);
2934
- if (soulStat.mtimeMs === lastSoulMtime && userStat.mtimeMs === lastUserMtime) {
2930
+ const soulMtime = existsSync4(SOUL_PATH) ? statSync(SOUL_PATH).mtimeMs : 0;
2931
+ const userMtime = existsSync4(USER_PATH) ? statSync(USER_PATH).mtimeMs : 0;
2932
+ if (soulMtime === lastSoulMtime && userMtime === lastUserMtime) {
2935
2933
  return;
2936
2934
  }
2937
- lastSoulMtime = soulStat.mtimeMs;
2938
- lastUserMtime = userStat.mtimeMs;
2935
+ lastSoulMtime = soulMtime;
2936
+ lastUserMtime = userMtime;
2939
2937
  } catch {
2940
2938
  }
2941
2939
  const nativeContent = [
@@ -3267,13 +3265,14 @@ ${transcript}`;
3267
3265
  stdio: ["ignore", "pipe", "pipe"],
3268
3266
  ...config2.cwd ? { cwd: config2.cwd } : {}
3269
3267
  });
3268
+ proc.stderr?.resume();
3269
+ const rl2 = createInterface({ input: proc.stdout });
3270
3270
  const timeout = setTimeout(() => {
3271
3271
  warn(`[summarize] Timeout after ${SUMMARIZE_TIMEOUT_MS / 1e3}s for chat ${chatId} \u2014 killing process`);
3272
+ rl2.close();
3272
3273
  proc.kill("SIGTERM");
3273
3274
  setTimeout(() => proc.kill("SIGKILL"), 2e3);
3274
3275
  }, SUMMARIZE_TIMEOUT_MS);
3275
- proc.stderr?.resume();
3276
- const rl2 = createInterface({ input: proc.stdout });
3277
3276
  rl2.on("line", (line) => {
3278
3277
  if (!line.trim()) return;
3279
3278
  let msg;
@@ -3295,9 +3294,9 @@ ${transcript}`;
3295
3294
  if (ev.type === "result") {
3296
3295
  resultText = ev.resultText || accumulatedText;
3297
3296
  if (ev.usage) {
3298
- inputTokens = ev.usage.input;
3299
- outputTokens = ev.usage.output;
3300
- cacheReadTokens = ev.usage.cacheRead;
3297
+ inputTokens += ev.usage.input;
3298
+ outputTokens += ev.usage.output;
3299
+ cacheReadTokens += ev.usage.cacheRead;
3301
3300
  }
3302
3301
  if (adapter.shouldKillOnResult()) {
3303
3302
  rl2.close();
@@ -3482,8 +3481,9 @@ import { spawn as spawn2 } from "child_process";
3482
3481
  import { createInterface as createInterface2 } from "readline";
3483
3482
  import { readFileSync as readFileSync4 } from "fs";
3484
3483
  function stripFrontmatter(text) {
3485
- const match = text.match(/^---\s*\n[\s\S]*?\n---\s*\n?/);
3486
- return match ? text.slice(match[0].length) : text;
3484
+ const match = text.match(/^---\s*\n[\s\S]*?\n---\s*\n?/m);
3485
+ if (!match || match.index !== 0) return text;
3486
+ return text.slice(match[0].length);
3487
3487
  }
3488
3488
  function buildAgentPrompt(opts, runnerSkillPath) {
3489
3489
  const parts = [];
@@ -3782,8 +3782,8 @@ __export(loader_exports, {
3782
3782
  import { readdirSync as readdirSync3, readFileSync as readFileSync5 } from "fs";
3783
3783
  import { join as join6 } from "path";
3784
3784
  function parseFrontmatter(content) {
3785
- const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/);
3786
- if (!match) return { meta: {}, body: content.trim() };
3785
+ const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/m);
3786
+ if (!match || match.index !== 0) return { meta: {}, body: content.trim() };
3787
3787
  const yamlBlock = match[1];
3788
3788
  const body = match[2].trim();
3789
3789
  const meta = {};
@@ -4283,6 +4283,7 @@ function startNextQueued(chatId) {
4283
4283
  maxRuntimeMs: agent.maxRuntimeMs,
4284
4284
  mcps: JSON.parse(agent.mcpsAdded)
4285
4285
  }).catch((err) => {
4286
+ updateAgentStatus(db3, next.id, "failed");
4286
4287
  error(`[orchestrator] Failed to start queued agent ${next.id}:`, err);
4287
4288
  });
4288
4289
  }
@@ -5210,7 +5211,6 @@ var init_server = __esm({
5210
5211
  var agent_exports = {};
5211
5212
  __export(agent_exports, {
5212
5213
  askAgent: () => askAgent,
5213
- askClaude: () => askClaude,
5214
5214
  isAgentActive: () => isAgentActive,
5215
5215
  isChatBusy: () => isChatBusy,
5216
5216
  stopAgent: () => stopAgent
@@ -5310,7 +5310,8 @@ function spawnQuery(adapter, config2, model2, cancelState, onStream, onToolActio
5310
5310
  if (onToolAction && ev.toolName) {
5311
5311
  const toolInput = ev.toolInput ?? {};
5312
5312
  if (ev.toolId) pendingTools.set(ev.toolId, { name: ev.toolName, input: toolInput });
5313
- onToolAction(ev.toolName, toolInput, void 0).catch(() => {
5313
+ onToolAction(ev.toolName, toolInput, void 0).catch((err) => {
5314
+ error("[agent] tool action error:", err);
5314
5315
  });
5315
5316
  }
5316
5317
  break;
@@ -5319,10 +5320,12 @@ function spawnQuery(adapter, config2, model2, cancelState, onStream, onToolActio
5319
5320
  const pending = ev.toolId ? pendingTools.get(ev.toolId) : void 0;
5320
5321
  if (pending) {
5321
5322
  pendingTools.delete(ev.toolId);
5322
- onToolAction(pending.name, pending.input, ev.toolOutput ?? "").catch(() => {
5323
+ onToolAction(pending.name, pending.input, ev.toolOutput ?? "").catch((err) => {
5324
+ error("[agent] tool action error:", err);
5323
5325
  });
5324
5326
  } else if (ev.toolName) {
5325
- onToolAction(ev.toolName, {}, ev.toolOutput ?? "").catch(() => {
5327
+ onToolAction(ev.toolName, {}, ev.toolOutput ?? "").catch((err) => {
5328
+ error("[agent] tool action error:", err);
5326
5329
  });
5327
5330
  }
5328
5331
  }
@@ -5344,7 +5347,10 @@ function spawnQuery(adapter, config2, model2, cancelState, onStream, onToolActio
5344
5347
  cacheRead = ev.usage.cacheRead;
5345
5348
  }
5346
5349
  if (adapter.shouldKillOnResult()) {
5347
- rl2.close();
5350
+ try {
5351
+ rl2.close();
5352
+ } catch {
5353
+ }
5348
5354
  proc.kill("SIGTERM");
5349
5355
  }
5350
5356
  break;
@@ -5466,7 +5472,7 @@ function injectMcpConfig(adapterId, args, mcpConfigPath) {
5466
5472
  if (!flag) return args;
5467
5473
  return [...args, ...flag, mcpConfigPath];
5468
5474
  }
5469
- var __filename2, __dirname2, activeChats, chatLocks, SPAWN_TIMEOUT_MS, MCP_CONFIG_FLAG, askClaude;
5475
+ var __filename2, __dirname2, activeChats, chatLocks, SPAWN_TIMEOUT_MS, MCP_CONFIG_FLAG;
5470
5476
  var init_agent = __esm({
5471
5477
  "src/agent.ts"() {
5472
5478
  "use strict";
@@ -5487,7 +5493,6 @@ var init_agent = __esm({
5487
5493
  MCP_CONFIG_FLAG = {
5488
5494
  claude: ["--mcp-config"]
5489
5495
  };
5490
- askClaude = askAgent;
5491
5496
  }
5492
5497
  });
5493
5498
 
@@ -5917,6 +5922,11 @@ async function executeJob(job) {
5917
5922
  return;
5918
5923
  }
5919
5924
  runningJobs.add(job.id);
5925
+ const timer = activeTimers.get(job.id);
5926
+ if (timer instanceof Cron) {
5927
+ const nextRun = timer.nextRun();
5928
+ if (nextRun) updateJobNextRun(job.id, nextRun.toISOString());
5929
+ }
5920
5930
  const t0 = Date.now();
5921
5931
  const resolvedModel = resolveJobModel(job);
5922
5932
  const runId = insertJobRun(job.id, resolvedModel);
@@ -5942,11 +5952,6 @@ async function executeJob(job) {
5942
5952
  addUsage(job.chatId, response.usage.input, response.usage.output, response.usage.cacheRead, resolvedModel);
5943
5953
  }
5944
5954
  await deliverJobOutput(job, response.text);
5945
- const timer = activeTimers.get(job.id);
5946
- if (timer instanceof Cron) {
5947
- const nextRun = timer.nextRun();
5948
- if (nextRun) updateJobNextRun(job.id, nextRun.toISOString());
5949
- }
5950
5955
  } catch (err) {
5951
5956
  const durationMs = Date.now() - t0;
5952
5957
  const errorClass = classifyError(err);
@@ -6082,9 +6087,9 @@ function writeEnvWrapper(server) {
6082
6087
  mkdirSync4(dir, { recursive: true, mode: 448 });
6083
6088
  const lines = ["#!/bin/sh"];
6084
6089
  for (const [k, v] of Object.entries(server.env ?? {})) {
6085
- lines.push(`export ${k}="${v.replace(/"/g, '\\"')}"`);
6090
+ lines.push(`export ${k}='${v.replace(/'/g, "'\\''")}'`);
6086
6091
  }
6087
- lines.push(`exec ${server.command} ${(server.args ?? []).map((a) => `"${a}"`).join(" ")}`);
6092
+ lines.push(`exec ${server.command} ${(server.args ?? []).map((a) => `'${a.replace(/'/g, "'\\''")}'`).join(" ")}`);
6088
6093
  const path = join8(dir, `wrapper-${server.name}.sh`);
6089
6094
  writeFileSync4(path, lines.join("\n") + "\n", { mode: 448 });
6090
6095
  return path;
@@ -6254,12 +6259,7 @@ function templateReplace(args, vars) {
6254
6259
  }
6255
6260
  function configToRunner(config2) {
6256
6261
  const exePath = resolveExecutable(config2);
6257
- let parseLine;
6258
- if (config2.outputParsing.preset) {
6259
- parseLine = buildGenericParser(config2);
6260
- } else {
6261
- parseLine = buildGenericParser(config2);
6262
- }
6262
+ const parseLine = buildGenericParser(config2);
6263
6263
  return {
6264
6264
  id: config2.id,
6265
6265
  displayName: config2.displayName,
@@ -6355,6 +6355,12 @@ function registerConfigRunners() {
6355
6355
  }
6356
6356
  function watchRunnerConfigs(onChange) {
6357
6357
  if (!existsSync9(RUNNERS_PATH)) return;
6358
+ for (const prev of watchedFiles) {
6359
+ if (!existsSync9(prev)) {
6360
+ unwatchFile(prev);
6361
+ watchedFiles.delete(prev);
6362
+ }
6363
+ }
6358
6364
  const files = readdirSync4(RUNNERS_PATH).filter((f) => f.endsWith(".json"));
6359
6365
  for (const file of files) {
6360
6366
  const fullPath = join9(RUNNERS_PATH, file);
@@ -6521,7 +6527,10 @@ function formatForTelegram(markdown) {
6521
6527
  html = html.replace(/^#{1,6}\s+(.+)$/gm, "<b>$1</b>");
6522
6528
  html = html.replace(/^[-*_]{3,}$/gm, "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
6523
6529
  html = html.replace(/^[\s]*[-*]\s+/gm, "\u2022 ");
6524
- html = html.replace(/\x00CODE(\d+)\x00/g, (_, idx) => codeBlocks[parseInt(idx, 10)]);
6530
+ html = html.replace(/\x00CODE(\d+)\x00/g, (match, idx) => {
6531
+ const block = codeBlocks[parseInt(idx, 10)];
6532
+ return block !== void 0 ? block : match;
6533
+ });
6525
6534
  return html.trim();
6526
6535
  }
6527
6536
  function escapeOutsideCode(text) {
@@ -6561,6 +6570,10 @@ var init_telegram = __esm({
6561
6570
 
6562
6571
  // src/channels/telegram.ts
6563
6572
  import { Bot, InlineKeyboard, InputFile } from "grammy";
6573
+ function numericChatId(chatId) {
6574
+ const raw = chatId.includes(":") ? chatId.split(":").pop() : chatId;
6575
+ return parseInt(raw);
6576
+ }
6564
6577
  var TelegramChannel;
6565
6578
  var init_telegram2 = __esm({
6566
6579
  "src/channels/telegram.ts"() {
@@ -6710,13 +6723,13 @@ var init_telegram2 = __esm({
6710
6723
  await this.bot.stop();
6711
6724
  }
6712
6725
  async sendTyping(chatId) {
6713
- await this.bot.api.sendChatAction(parseInt(chatId), "typing");
6726
+ await this.bot.api.sendChatAction(numericChatId(chatId), "typing");
6714
6727
  }
6715
6728
  async sendText(chatId, text, parseMode) {
6716
6729
  if (parseMode === "plain") {
6717
6730
  const plainChunks = splitMessage(text);
6718
6731
  for (const chunk of plainChunks) {
6719
- await this.bot.api.sendMessage(parseInt(chatId), chunk);
6732
+ await this.bot.api.sendMessage(numericChatId(chatId), chunk);
6720
6733
  }
6721
6734
  return;
6722
6735
  }
@@ -6724,12 +6737,12 @@ var init_telegram2 = __esm({
6724
6737
  const chunks = splitMessage(formatted);
6725
6738
  for (const chunk of chunks) {
6726
6739
  try {
6727
- await this.bot.api.sendMessage(parseInt(chatId), chunk, {
6740
+ await this.bot.api.sendMessage(numericChatId(chatId), chunk, {
6728
6741
  parse_mode: "HTML"
6729
6742
  });
6730
6743
  } catch {
6731
6744
  await this.bot.api.sendMessage(
6732
- parseInt(chatId),
6745
+ numericChatId(chatId),
6733
6746
  chunk.replace(/<[^>]+>/g, "")
6734
6747
  );
6735
6748
  }
@@ -6737,13 +6750,13 @@ var init_telegram2 = __esm({
6737
6750
  }
6738
6751
  async sendVoice(chatId, audioBuffer, fileName) {
6739
6752
  await this.bot.api.sendVoice(
6740
- parseInt(chatId),
6753
+ numericChatId(chatId),
6741
6754
  new InputFile(audioBuffer, fileName ?? "response.ogg")
6742
6755
  );
6743
6756
  }
6744
6757
  async sendFile(chatId, buffer, fileName) {
6745
6758
  await this.bot.api.sendDocument(
6746
- parseInt(chatId),
6759
+ numericChatId(chatId),
6747
6760
  new InputFile(buffer, fileName)
6748
6761
  );
6749
6762
  }
@@ -6755,7 +6768,7 @@ var init_telegram2 = __esm({
6755
6768
  }
6756
6769
  async sendTextReturningId(chatId, text, parseMode) {
6757
6770
  try {
6758
- const msg = await this.bot.api.sendMessage(parseInt(chatId), text);
6771
+ const msg = await this.bot.api.sendMessage(numericChatId(chatId), text);
6759
6772
  return msg.message_id.toString();
6760
6773
  } catch {
6761
6774
  return void 0;
@@ -6764,14 +6777,14 @@ var init_telegram2 = __esm({
6764
6777
  async editText(chatId, messageId, text, parseMode) {
6765
6778
  const formatted = parseMode === "html" ? text : formatForTelegram(text);
6766
6779
  try {
6767
- await this.bot.api.editMessageText(parseInt(chatId), parseInt(messageId), formatted, {
6780
+ await this.bot.api.editMessageText(numericChatId(chatId), parseInt(messageId), formatted, {
6768
6781
  parse_mode: "HTML"
6769
6782
  });
6770
6783
  return true;
6771
6784
  } catch {
6772
6785
  try {
6773
6786
  await this.bot.api.editMessageText(
6774
- parseInt(chatId),
6787
+ numericChatId(chatId),
6775
6788
  parseInt(messageId),
6776
6789
  formatted.replace(/<[^>]+>/g, "")
6777
6790
  );
@@ -6793,13 +6806,13 @@ var init_telegram2 = __esm({
6793
6806
  }
6794
6807
  keyboard.row();
6795
6808
  }
6796
- await this.bot.api.sendMessage(parseInt(chatId), text, {
6809
+ await this.bot.api.sendMessage(numericChatId(chatId), text, {
6797
6810
  reply_markup: keyboard
6798
6811
  });
6799
6812
  }
6800
6813
  async reactToMessage(chatId, messageId, emoji) {
6801
6814
  try {
6802
- await this.bot.api.setMessageReaction(parseInt(chatId), parseInt(messageId), [
6815
+ await this.bot.api.setMessageReaction(numericChatId(chatId), parseInt(messageId), [
6803
6816
  { type: "emoji", emoji }
6804
6817
  ]);
6805
6818
  } catch {
@@ -7093,6 +7106,9 @@ async function installSkillFromGitHub(urlOrShorthand) {
7093
7106
  stdio: "pipe",
7094
7107
  timeout: 3e4
7095
7108
  });
7109
+ if (!existsSync10(join11(tmpDir, ".git"))) {
7110
+ return { success: false, error: "Git clone failed: no .git directory produced" };
7111
+ }
7096
7112
  const searchRoot = subPath ? join11(tmpDir, subPath) : tmpDir;
7097
7113
  const skillDir = await findSkillDir(searchRoot);
7098
7114
  if (!skillDir) {
@@ -7165,7 +7181,12 @@ async function findSkillDir(root) {
7165
7181
  const entries = await readdir2(root, { withFileTypes: true });
7166
7182
  for (const entry of entries) {
7167
7183
  if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
7168
- const subEntries = await readdir2(join11(root, entry.name), { withFileTypes: true });
7184
+ let subEntries;
7185
+ try {
7186
+ subEntries = await readdir2(join11(root, entry.name), { withFileTypes: true });
7187
+ } catch {
7188
+ continue;
7189
+ }
7169
7190
  for (const sub of subEntries) {
7170
7191
  if (!sub.isDirectory() || sub.name.startsWith(".")) continue;
7171
7192
  for (const c of candidates) {
@@ -7371,8 +7392,7 @@ function stopAllHeartbeats() {
7371
7392
  }
7372
7393
  function startAllHeartbeats() {
7373
7394
  try {
7374
- const { getDb: getDb2 } = (init_store4(), __toCommonJS(store_exports2));
7375
- const db3 = getDb2();
7395
+ const db3 = getDb();
7376
7396
  const rows = db3.prepare(
7377
7397
  "SELECT chat_id FROM chat_heartbeat WHERE enabled = 1"
7378
7398
  ).all();
@@ -7610,9 +7630,11 @@ async function macOsTts(text) {
7610
7630
  await execFileAsync2("say", ["-o", tmpAiff, sanitized]);
7611
7631
  await execFileAsync2("ffmpeg", ["-y", "-i", tmpAiff, "-c:a", "libopus", "-b:a", "64k", tmpOgg]);
7612
7632
  const oggBuffer = await readFile4(tmpOgg);
7613
- unlink(tmpAiff).catch(() => {
7633
+ unlink(tmpAiff).catch((err) => {
7634
+ error("[tts] cleanup failed:", err);
7614
7635
  });
7615
- unlink(tmpOgg).catch(() => {
7636
+ unlink(tmpOgg).catch((err) => {
7637
+ error("[tts] cleanup failed:", err);
7616
7638
  });
7617
7639
  return oggBuffer;
7618
7640
  }
@@ -7743,8 +7765,7 @@ async function startWizard(chatId, input, channel) {
7743
7765
  step: "schedule",
7744
7766
  task: parsed?.task || input,
7745
7767
  scheduleType: parsed?.scheduleType,
7746
- cron: parsed?.cron,
7747
- everyMs: parsed?.everyMs
7768
+ cron: parsed?.cron
7748
7769
  };
7749
7770
  if (parsed?.cron) {
7750
7771
  pending.step = "timezone";
@@ -10548,10 +10569,18 @@ async function main() {
10548
10569
  bootstrapBuiltinMcps(getDb());
10549
10570
  const SUMMARIZE_TIMEOUT_MS = 3e4;
10550
10571
  try {
10551
- await Promise.race([
10552
- summarizeAllPending(),
10553
- new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), SUMMARIZE_TIMEOUT_MS))
10554
- ]);
10572
+ let timer;
10573
+ const timeoutPromise = new Promise((_, reject) => {
10574
+ timer = setTimeout(() => reject(new Error("timeout")), SUMMARIZE_TIMEOUT_MS);
10575
+ });
10576
+ try {
10577
+ await Promise.race([
10578
+ summarizeAllPending(),
10579
+ timeoutPromise
10580
+ ]);
10581
+ } finally {
10582
+ clearTimeout(timer);
10583
+ }
10555
10584
  } catch {
10556
10585
  log("[cc-claw] Session summarization skipped (timeout or backend unavailable)");
10557
10586
  }
@@ -10782,7 +10811,7 @@ function uninstallMacOS() {
10782
10811
  }
10783
10812
  function statusMacOS() {
10784
10813
  try {
10785
- const out = execSync5("launchctl list | grep cc-claw", { encoding: "utf-8" }).trim();
10814
+ const out = execSync5("launchctl list | grep cc-claw", { shell: "/bin/sh", encoding: "utf-8" }).trim();
10786
10815
  if (out) {
10787
10816
  const parts = out.split(/\s+/);
10788
10817
  const pid = parts[0];
@@ -11324,7 +11353,9 @@ async function doctorCommand(globalOpts, localOpts) {
11324
11353
  const { openDatabaseReadOnly: openDatabaseReadOnly2 } = await Promise.resolve().then(() => (init_store4(), store_exports2));
11325
11354
  const readDb = openDatabaseReadOnly2();
11326
11355
  const journal = readDb.prepare("PRAGMA journal_mode").get();
11327
- if (journal.journal_mode === "wal") {
11356
+ if (!journal) {
11357
+ checks.push({ name: "WAL mode", status: "warning", message: "could not read journal_mode" });
11358
+ } else if (journal.journal_mode === "wal") {
11328
11359
  checks.push({ name: "WAL mode", status: "ok", message: "enabled" });
11329
11360
  } else {
11330
11361
  checks.push({ name: "WAL mode", status: "warning", message: `${journal.journal_mode} (expected wal)` });
@@ -12128,8 +12159,8 @@ async function agentsSpawn(globalOpts, opts) {
12128
12159
  role: opts.role
12129
12160
  });
12130
12161
  if (res.ok) {
12162
+ const { success: s } = await Promise.resolve().then(() => (init_format(), format_exports));
12131
12163
  output(res.data, (d) => {
12132
- const { success: s } = (init_format(), __toCommonJS(format_exports));
12133
12164
  return `
12134
12165
  ${s(`Agent spawned: ${d.agentId?.slice(0, 8) ?? "?"} (${opts.runner})`)}
12135
12166
  `;
@@ -12393,13 +12424,21 @@ async function usageTokens(globalOpts) {
12393
12424
  const { getAllAdapters: getAllAdapters2, getAllBackendIds: getAllBackendIds2 } = await Promise.resolve().then(() => (init_backends(), backends_exports));
12394
12425
  const readDb = openDatabaseReadOnly2();
12395
12426
  const backendIds = getAllBackendIds2();
12427
+ const adapters2 = getAllAdapters2();
12396
12428
  const cutoff24h = new Date(Date.now() - 864e5).toISOString();
12397
12429
  const data = backendIds.map((bid) => {
12398
- const row = readDb.prepare(`
12399
- SELECT COALESCE(SUM(input_tokens),0) as input_tokens, COALESCE(SUM(output_tokens),0) as output_tokens, COUNT(*) as request_count
12400
- FROM usage_log WHERE model IN (SELECT DISTINCT model FROM usage_log) AND created_at > ?
12401
- `).get(cutoff24h);
12402
- const adapter = getAllAdapters2().find((a) => a.id === bid);
12430
+ const adapter = adapters2.find((a) => a.id === bid);
12431
+ const modelIds = adapter ? Object.keys(adapter.pricing) : [];
12432
+ let row;
12433
+ if (modelIds.length > 0) {
12434
+ const placeholders = modelIds.map(() => "?").join(",");
12435
+ row = readDb.prepare(`
12436
+ SELECT COALESCE(SUM(input_tokens),0) as input_tokens, COALESCE(SUM(output_tokens),0) as output_tokens, COUNT(*) as request_count
12437
+ FROM usage_log WHERE model IN (${placeholders}) AND created_at > ?
12438
+ `).get(...modelIds, cutoff24h);
12439
+ } else {
12440
+ row = { input_tokens: 0, output_tokens: 0, request_count: 0 };
12441
+ }
12403
12442
  return { backend: bid, displayName: adapter?.displayName ?? bid, ...row };
12404
12443
  });
12405
12444
  readDb.close();
@@ -12496,6 +12535,7 @@ async function configList(globalOpts) {
12496
12535
  const config2 = {};
12497
12536
  for (const key of RUNTIME_KEYS) {
12498
12537
  const { table, col } = KEY_TABLE_MAP[key];
12538
+ if (!ALLOWED_TABLES.has(table) || !ALLOWED_COLS.has(col)) continue;
12499
12539
  try {
12500
12540
  const row = readDb.prepare(`SELECT ${col} FROM ${table} WHERE chat_id = ?`).get(chatId);
12501
12541
  config2[key] = row ? row[col] : null;
@@ -12527,6 +12567,10 @@ async function configGet(globalOpts, key) {
12527
12567
  const readDb = openDatabaseReadOnly2();
12528
12568
  const chatId = resolveChatId(globalOpts);
12529
12569
  const { table, col } = KEY_TABLE_MAP[key];
12570
+ if (!ALLOWED_TABLES.has(table) || !ALLOWED_COLS.has(col)) {
12571
+ outputError("INVALID_KEY", `Invalid config mapping for "${key}".`);
12572
+ process.exit(1);
12573
+ }
12530
12574
  let value = null;
12531
12575
  try {
12532
12576
  const row = readDb.prepare(`SELECT ${col} FROM ${table} WHERE chat_id = ?`).get(chatId);
@@ -12585,7 +12629,7 @@ async function configEnv(_globalOpts) {
12585
12629
  return lines.join("\n");
12586
12630
  });
12587
12631
  }
12588
- var RUNTIME_KEYS, KEY_TABLE_MAP;
12632
+ var RUNTIME_KEYS, KEY_TABLE_MAP, ALLOWED_TABLES, ALLOWED_COLS;
12589
12633
  var init_config = __esm({
12590
12634
  "src/cli/commands/config.ts"() {
12591
12635
  "use strict";
@@ -12603,6 +12647,8 @@ var init_config = __esm({
12603
12647
  cwd: { table: "chat_cwd", col: "cwd" },
12604
12648
  voice: { table: "chat_voice", col: "enabled" }
12605
12649
  };
12650
+ ALLOWED_TABLES = new Set(Object.values(KEY_TABLE_MAP).map((v) => v.table));
12651
+ ALLOWED_COLS = new Set(Object.values(KEY_TABLE_MAP).map((v) => v.col));
12606
12652
  }
12607
12653
  });
12608
12654
 
@@ -12676,11 +12722,14 @@ __export(permissions_exports, {
12676
12722
  verboseSet: () => verboseSet
12677
12723
  });
12678
12724
  import { existsSync as existsSync30 } from "fs";
12679
- async function permissionsGet(globalOpts) {
12725
+ function ensureDb2() {
12680
12726
  if (!existsSync30(DB_PATH)) {
12681
12727
  outputError("DB_NOT_FOUND", "Database not found.");
12682
12728
  process.exit(1);
12683
12729
  }
12730
+ }
12731
+ async function permissionsGet(globalOpts) {
12732
+ ensureDb2();
12684
12733
  const { openDatabaseReadOnly: openDatabaseReadOnly2 } = await Promise.resolve().then(() => (init_store4(), store_exports2));
12685
12734
  const readDb = openDatabaseReadOnly2();
12686
12735
  const chatId = resolveChatId(globalOpts);
@@ -12712,10 +12761,7 @@ async function permissionsSet(globalOpts, mode) {
12712
12761
  }
12713
12762
  }
12714
12763
  async function toolsList(globalOpts) {
12715
- if (!existsSync30(DB_PATH)) {
12716
- outputError("DB_NOT_FOUND", "Database not found.");
12717
- process.exit(1);
12718
- }
12764
+ ensureDb2();
12719
12765
  const { openDatabaseReadOnly: openDatabaseReadOnly2 } = await Promise.resolve().then(() => (init_store4(), store_exports2));
12720
12766
  const readDb = openDatabaseReadOnly2();
12721
12767
  const chatId = resolveChatId(globalOpts);
@@ -12776,10 +12822,7 @@ async function toggleTool2(globalOpts, name, enabled) {
12776
12822
  }
12777
12823
  }
12778
12824
  async function verboseGet(globalOpts) {
12779
- if (!existsSync30(DB_PATH)) {
12780
- outputError("DB_NOT_FOUND", "Database not found.");
12781
- process.exit(1);
12782
- }
12825
+ ensureDb2();
12783
12826
  const { openDatabaseReadOnly: openDatabaseReadOnly2 } = await Promise.resolve().then(() => (init_store4(), store_exports2));
12784
12827
  const readDb = openDatabaseReadOnly2();
12785
12828
  const chatId = resolveChatId(globalOpts);
@@ -13682,7 +13725,7 @@ async function requiredInput(prompt, existing) {
13682
13725
  }
13683
13726
  async function validateBotToken(token) {
13684
13727
  try {
13685
- const res = await fetch(`https://api.telegram.org/bot${token}/getMe`);
13728
+ const res = await fetch(`https://api.telegram.org/bot${token}/getMe`, { signal: AbortSignal.timeout(1e4) });
13686
13729
  const data = await res.json();
13687
13730
  if (data.ok && data.result?.username) {
13688
13731
  return { valid: true, botName: data.result.username };
@@ -13895,11 +13938,7 @@ async function setup() {
13895
13938
  console.log(dim(" Voice replies will use macOS text-to-speech as fallback."));
13896
13939
  }
13897
13940
  }
13898
- console.log("");
13899
- if (await confirm("Enable web dashboard? (local status page on port 3141)")) {
13900
- env.DASHBOARD_ENABLED = "1";
13901
- console.log(green(" Dashboard will be available at http://localhost:3141"));
13902
- }
13941
+ env.DASHBOARD_ENABLED = "1";
13903
13942
  console.log("");
13904
13943
  if (await confirm("Enable video analysis? (requires Google Gemini API key)")) {
13905
13944
  console.log(dim(" Get a Gemini key at: https://aistudio.google.com/apikey\n"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-claw",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "CC-Claw: Personal AI assistant on Telegram — multi-backend (Claude, Gemini, Codex), sub-agent orchestration, MCP management",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",