cc-claw 0.4.4 → 0.4.6

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 +6 -3
  2. package/dist/cli.js +675 -204
  3. package/package.json +1 -1
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.4.4" : (() => {
51
+ VERSION = true ? "0.4.6" : (() => {
52
52
  try {
53
53
  return JSON.parse(readFileSync(join2(process.cwd(), "package.json"), "utf-8")).version ?? "unknown";
54
54
  } catch {
@@ -358,10 +358,10 @@ function claimTask(db3, taskId, agentId) {
358
358
  ).get(...blockedBy);
359
359
  if (incomplete.count > 0) return false;
360
360
  }
361
- db3.prepare(
361
+ const result = db3.prepare(
362
362
  "UPDATE agent_tasks SET assignee = ?, status = 'in_progress' WHERE id = ? AND status = 'pending'"
363
363
  ).run(agentId, taskId);
364
- return true;
364
+ return result.changes > 0;
365
365
  }
366
366
  function sendInboxMessage(db3, opts) {
367
367
  const result = db3.prepare(`
@@ -968,6 +968,7 @@ __export(store_exports3, {
968
968
  getRecentBookmarks: () => getRecentBookmarks,
969
969
  getRecentMemories: () => getRecentMemories,
970
970
  getRecentMessageLog: () => getRecentMessageLog,
971
+ getResponseStyle: () => getResponseStyle,
971
972
  getSessionId: () => getSessionId,
972
973
  getSessionStartedAt: () => getSessionStartedAt,
973
974
  getSessionSummariesWithoutEmbeddings: () => getSessionSummariesWithoutEmbeddings,
@@ -1009,6 +1010,7 @@ __export(store_exports3, {
1009
1010
  setHeartbeatConfig: () => setHeartbeatConfig,
1010
1011
  setMode: () => setMode,
1011
1012
  setModel: () => setModel,
1013
+ setResponseStyle: () => setResponseStyle,
1012
1014
  setSessionId: () => setSessionId,
1013
1015
  setSessionStartedAt: () => setSessionStartedAt,
1014
1016
  setSummarizer: () => setSummarizer,
@@ -1358,9 +1360,19 @@ function initDatabase() {
1358
1360
  db.exec(`
1359
1361
  CREATE TABLE IF NOT EXISTS chat_voice (
1360
1362
  chat_id TEXT PRIMARY KEY,
1361
- enabled INTEGER NOT NULL DEFAULT 0
1363
+ enabled INTEGER NOT NULL DEFAULT 0,
1364
+ provider TEXT DEFAULT 'elevenlabs',
1365
+ voice_id TEXT
1362
1366
  );
1363
1367
  `);
1368
+ try {
1369
+ db.exec(`ALTER TABLE chat_voice ADD COLUMN provider TEXT DEFAULT 'elevenlabs'`);
1370
+ } catch {
1371
+ }
1372
+ try {
1373
+ db.exec(`ALTER TABLE chat_voice ADD COLUMN voice_id TEXT`);
1374
+ } catch {
1375
+ }
1364
1376
  db.exec(`
1365
1377
  CREATE TABLE IF NOT EXISTS backend_limits (
1366
1378
  backend TEXT NOT NULL,
@@ -1468,6 +1480,12 @@ function initDatabase() {
1468
1480
  initMcpTables(db);
1469
1481
  initActivityTable(db);
1470
1482
  initIdentityTables(db);
1483
+ db.exec(`
1484
+ CREATE TABLE IF NOT EXISTS chat_response_style (
1485
+ chat_id TEXT PRIMARY KEY,
1486
+ style TEXT NOT NULL DEFAULT 'normal'
1487
+ );
1488
+ `);
1471
1489
  }
1472
1490
  function getDb() {
1473
1491
  return db;
@@ -1600,6 +1618,17 @@ function applySalienceDecay() {
1600
1618
  db.prepare("DELETE FROM session_summaries WHERE salience < 0.1").run();
1601
1619
  db.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES ('last_salience_decay', ?)").run(now);
1602
1620
  }
1621
+ function getResponseStyle(chatId) {
1622
+ const row = db.prepare("SELECT style FROM chat_response_style WHERE chat_id = ?").get(chatId);
1623
+ return row?.style ?? "normal";
1624
+ }
1625
+ function setResponseStyle(chatId, style) {
1626
+ db.prepare(`
1627
+ INSERT INTO chat_response_style (chat_id, style)
1628
+ VALUES (?, ?)
1629
+ ON CONFLICT(chat_id) DO UPDATE SET style = excluded.style
1630
+ `).run(chatId, style);
1631
+ }
1603
1632
  function getSessionId(chatId) {
1604
1633
  const row = db.prepare(
1605
1634
  "SELECT session_id FROM sessions WHERE chat_id = ?"
@@ -2461,6 +2490,7 @@ var init_store4 = __esm({
2461
2490
  });
2462
2491
 
2463
2492
  // src/env.ts
2493
+ import { homedir as homedir2 } from "os";
2464
2494
  function stripProxyVars(env) {
2465
2495
  for (const key of PROXY_KEYS) delete env[key];
2466
2496
  }
@@ -2469,7 +2499,7 @@ function buildBaseEnv(extraOverrides) {
2469
2499
  for (const [k, v] of Object.entries(process.env)) {
2470
2500
  if (v !== void 0) env[k] = v;
2471
2501
  }
2472
- if (!env.HOME) env.HOME = `/Users/${process.env.USER ?? "unknown"}`;
2502
+ if (!env.HOME) env.HOME = homedir2();
2473
2503
  stripProxyVars(env);
2474
2504
  if (extraOverrides) Object.assign(env, extraOverrides);
2475
2505
  return env;
@@ -3179,8 +3209,9 @@ async function injectMemoryContext(userMessage) {
3179
3209
  seen.add(mem.id);
3180
3210
  combinedMemories.push(mem);
3181
3211
  }
3212
+ if (combinedMemories.length >= FINAL_TOP_K_MEMORIES) break;
3182
3213
  }
3183
- combinedSessions = ftsSessions;
3214
+ combinedSessions = ftsSessions.slice(0, FINAL_TOP_K_SESSIONS);
3184
3215
  }
3185
3216
  if (combinedMemories.length === 0 && combinedSessions.length === 0) return null;
3186
3217
  const lines = [];
@@ -3219,7 +3250,7 @@ var init_inject = __esm({
3219
3250
  });
3220
3251
 
3221
3252
  // src/bootstrap/defaults.ts
3222
- var DEFAULT_SOUL, DEFAULT_USER;
3253
+ var DEFAULT_SOUL, DEFAULT_USER, DEFAULT_EXPERTISE;
3223
3254
  var init_defaults = __esm({
3224
3255
  "src/bootstrap/defaults.ts"() {
3225
3256
  "use strict";
@@ -3256,6 +3287,40 @@ This file is auto-generated. Use /setup-profile to customize, or edit directly.
3256
3287
  - **Timezone**: UTC
3257
3288
  - **Communication style**: concise
3258
3289
  - **Primary use**: general assistant
3290
+ `;
3291
+ DEFAULT_EXPERTISE = `# CC-Claw System Expertise
3292
+
3293
+ You are an expert user and operator of the CC-Claw architecture. Because you are reading this file, the user has likely asked you about a system feature, how the bot operates, its capabilities, or its internal architecture.
3294
+
3295
+ ## 1. Core Architecture & Philosophy
3296
+ - **Multi-Backend System**: You are decoupled from any specific LLM. The user can seamlessly hot-swap your "brain" between Claude (Anthropic), Gemini (Google), and Codex (OpenAI) at any time. You maintain persistent, unified memory across all of them.
3297
+ - **The Daemon**: CC-Claw runs a background daemon (\`cc-claw start\`, \`cc-claw stop\`) that manages the HTTP API, Webhooks, and the Cron Scheduler.
3298
+ - **Database**: All state is stored locally in SQLite (\`~/.cc-claw/data/cc-claw.db\`), ensuring total privacy and fast local access for memory, jobs, and orchestration.
3299
+
3300
+ ## 2. Identity & Context (The "Brain")
3301
+ - **SOUL & USER**: Your personality is defined in \`~/.cc-claw/workspace/SOUL.md\`, and the user's profile is in \`USER.md\`. These are the single source of truth for your behavior.
3302
+ - **On-Demand Context**: The \`context/\` directory holds files like this one. They are injected into your prompt only when triggered by relevant semantic keywords, keeping your context window lean.
3303
+ - **Proactive Memory**: You can autonomously save user facts and preferences to the database by writing \`[UPDATE_USER:key=value]\` in your replies.
3304
+
3305
+ ## 3. Advanced Agent Orchestration
3306
+ - **Sub-Agents**: You are not limited to sequential replies. If a task requires heavy research or coding, you can spawn parallel worker agents using your \`spawn_agent\` MCP tool.
3307
+ - **Supervision**: You manage sub-agents by reading their messages via \`read_inbox\` and sharing data on the orchestration whiteboard via \`set_state\` / \`get_state\`.
3308
+
3309
+ ## 4. Scheduling & Cron (Autonomous Execution)
3310
+ - **Job Management**: You manage scheduled tasks natively. Jobs can route on cron expressions, "every 4h", or "at 09:00".
3311
+ - **Dry Run & Webhooks**: Jobs aren't just for Telegram messages. Using \`--delivery dry_run\`, you can test jobs internally (output is logged to the database but not spammed to chat). You can also route AI output directly to external APIs using \`--delivery webhook\`.
3312
+ - **Auditing**: Every cron execution and its output is permanently logged in the database's \`message_log\` for verifiable auditing.
3313
+
3314
+ ## 5. System Controls & Modes
3315
+ - **Permission Modes**: Command execution is gated. \`safe\` mode requires user approval for mutations, \`yolo\` auto-runs everything without asking, and \`plan\` is strictly read-only for drafting proposals.
3316
+ - **Global Settings**: The user controls your behavior system-wide using commands like \`/response_style\` (concise, normal, detailed) to enforce verbosity constraints, and \`/voice_config\` to select Text-to-Speech voices from premium providers like ElevenLabs, Grok (xAI), or local macOS.
3317
+
3318
+ ## 6. Integrations & MCP
3319
+ - **Skills**: You can load specific workflows from the \`~/.cc-claw/workspace/skills/\` directory.
3320
+ - **Files**: You can send physical files to the user across Telegram by simply writing \`[SEND_FILE:/absolute/path/to/file]\` in your response.
3321
+ - **MCP Ecosystem**: You are deeply natively integrated with Model Context Protocol (MCP) servers (like Perplexity, NotebookLM, Context7) granting you immense external reach.
3322
+
3323
+ If the user asks *how* to do something with CC-Claw, use this expertise to suggest the most native, idiomatic approach available in the architecture.
3259
3324
  `;
3260
3325
  }
3261
3326
  });
@@ -3282,6 +3347,11 @@ function bootstrapWorkspaceFiles() {
3282
3347
  mkdirSync(CONTEXT_DIR, { recursive: true });
3283
3348
  log("[bootstrap] Created context/ directory");
3284
3349
  }
3350
+ const expertisePath = join3(CONTEXT_DIR, "cc-claw-expertise.md");
3351
+ if (!existsSync4(expertisePath)) {
3352
+ writeFileSync(expertisePath, DEFAULT_EXPERTISE, "utf-8");
3353
+ log("[bootstrap] Created default context/cc-claw-expertise.md");
3354
+ }
3285
3355
  syncNativeCliFiles();
3286
3356
  }
3287
3357
  function syncNativeCliFiles() {
@@ -3399,12 +3469,19 @@ function searchContext(userMessage) {
3399
3469
  }
3400
3470
  return null;
3401
3471
  }
3402
- async function assembleBootstrapPrompt(userMessage, tier = "full", chatId, permMode) {
3472
+ async function assembleBootstrapPrompt(userMessage, tier = "full", chatId, permMode, responseStyle) {
3403
3473
  const sections = [];
3404
3474
  syncNativeCliFiles();
3405
3475
  if (permMode && permMode !== "yolo") {
3406
3476
  sections.push(buildPermissionNotice(permMode));
3407
3477
  }
3478
+ if (responseStyle) {
3479
+ if (responseStyle === "concise") {
3480
+ sections.push("[Response Style]\nYou must be as concise and direct as possible. Avoid unnecessary verbosity, pleasantries, or long explanations.");
3481
+ } else if (responseStyle === "detailed") {
3482
+ sections.push("[Response Style]\nYou should be detailed and thorough in your responses. Explain concepts fully and provide comprehensive answers.");
3483
+ }
3484
+ }
3408
3485
  if (tier === "full") {
3409
3486
  const ctx = searchContext(userMessage);
3410
3487
  if (ctx) {
@@ -3925,8 +4002,10 @@ function spawnAgentProcess(runner, opts, callbacks) {
3925
4002
  const child = spawn2(runner.getExecutablePath(), args, {
3926
4003
  env: buildSpawnEnv(runner, opts.isSubAgent),
3927
4004
  cwd: opts.cwd,
3928
- stdio: ["ignore", "pipe", "pipe"]
4005
+ stdio: ["ignore", "pipe", "pipe"],
4006
+ detached: true
3929
4007
  });
4008
+ child.unref();
3930
4009
  let resultText = "";
3931
4010
  let usage2;
3932
4011
  if (child.stdout) {
@@ -3949,7 +4028,14 @@ function spawnAgentProcess(runner, opts, callbacks) {
3949
4028
  if (event.usage) usage2 = event.usage;
3950
4029
  callbacks.onResult?.(resultText, usage2);
3951
4030
  if (runner.shouldKillOnResult()) {
3952
- child.kill("SIGTERM");
4031
+ try {
4032
+ if (child.pid) process.kill(-child.pid, "SIGTERM");
4033
+ } catch {
4034
+ try {
4035
+ child.kill("SIGTERM");
4036
+ } catch {
4037
+ }
4038
+ }
3953
4039
  }
3954
4040
  }
3955
4041
  }
@@ -4029,7 +4115,7 @@ var init_cost = __esm({
4029
4115
  // src/mcps/propagate.ts
4030
4116
  import { execFile } from "child_process";
4031
4117
  import { promisify } from "util";
4032
- import { homedir as homedir2 } from "os";
4118
+ import { homedir as homedir3 } from "os";
4033
4119
  async function discoverExistingMcps(runner) {
4034
4120
  try {
4035
4121
  const listCmd = runner.getMcpListCommand();
@@ -4038,7 +4124,7 @@ async function discoverExistingMcps(runner) {
4038
4124
  const result = await execFileAsync(exe, args, {
4039
4125
  encoding: "utf-8",
4040
4126
  env: runner.getEnv(),
4041
- cwd: homedir2(),
4127
+ cwd: homedir3(),
4042
4128
  timeout: 3e4
4043
4129
  });
4044
4130
  const stdout = typeof result === "string" ? result : Array.isArray(result) ? result[0] : result?.stdout ?? null;
@@ -4553,6 +4639,26 @@ async function startAgent(agentId, chatId, opts) {
4553
4639
  clearTimeout(timeoutTimers.get(agentId));
4554
4640
  timeoutTimers.delete(agentId);
4555
4641
  activeProcesses.delete(agentId);
4642
+ const crashedAgent = getAgent(db3, agentId);
4643
+ if (crashedAgent) {
4644
+ const mcpsCrashed = crashedAgent.mcpsAdded ? JSON.parse(crashedAgent.mcpsAdded) : [];
4645
+ if (mcpsCrashed.length > 0) {
4646
+ const runner2 = getRunner(crashedAgent.runnerId);
4647
+ if (runner2) {
4648
+ const cleanupFn = () => cleanupMcps(runner2, mcpsCrashed, db3, `agent:${agentId}`);
4649
+ if (runner2.capabilities.mcpInjection === "add-remove") {
4650
+ withRunnerLock(runner2.id, cleanupFn).catch((err) => {
4651
+ warn(`[orchestrator] MCP cleanup failed for crashed agent ${agentId.slice(0, 8)}:`, err);
4652
+ });
4653
+ } else {
4654
+ cleanupFn().catch((err) => {
4655
+ warn(`[orchestrator] MCP cleanup failed for crashed agent ${agentId.slice(0, 8)}:`, err);
4656
+ });
4657
+ }
4658
+ }
4659
+ }
4660
+ deleteMcpConfigFile(`cc-claw-${agentId.slice(0, 8)}`);
4661
+ }
4556
4662
  sendInboxMessage(db3, {
4557
4663
  orchestrationId: agent.orchestrationId,
4558
4664
  toAgentId: "main",
@@ -4714,9 +4820,23 @@ function cancelAgent(agentId, reason = "user_cancelled") {
4714
4820
  if (!agent) return false;
4715
4821
  const proc = activeProcesses.get(agentId);
4716
4822
  if (proc) {
4717
- proc.kill("SIGTERM");
4823
+ try {
4824
+ if (proc.pid) process.kill(-proc.pid, "SIGTERM");
4825
+ } catch {
4826
+ try {
4827
+ proc.kill("SIGTERM");
4828
+ } catch {
4829
+ }
4830
+ }
4718
4831
  setTimeout(() => {
4719
- if (!proc.killed) proc.kill("SIGKILL");
4832
+ try {
4833
+ if (proc.pid) process.kill(-proc.pid, "SIGKILL");
4834
+ } catch {
4835
+ try {
4836
+ proc.kill("SIGKILL");
4837
+ } catch {
4838
+ }
4839
+ }
4720
4840
  }, 2e3);
4721
4841
  }
4722
4842
  updateAgentStatus(db3, agentId, "cancelled");
@@ -4761,7 +4881,14 @@ function cancelAllAgents(chatId, reason = "user_cancelled") {
4761
4881
  }
4762
4882
  function shutdownOrchestrator() {
4763
4883
  for (const [agentId, proc] of activeProcesses) {
4764
- proc.kill("SIGTERM");
4884
+ try {
4885
+ if (proc.pid) process.kill(-proc.pid, "SIGTERM");
4886
+ } catch {
4887
+ try {
4888
+ proc.kill("SIGTERM");
4889
+ } catch {
4890
+ }
4891
+ }
4765
4892
  clearTimeout(timeoutTimers.get(agentId));
4766
4893
  }
4767
4894
  activeProcesses.clear();
@@ -5621,8 +5748,8 @@ data: ${JSON.stringify(data)}
5621
5748
  if (url.pathname === "/api/heartbeat/set" && req.method === "POST") {
5622
5749
  try {
5623
5750
  const body = JSON.parse(await readBody(req));
5624
- const { setHeartbeatConfig: setHeartbeatConfig3 } = await Promise.resolve().then(() => (init_store4(), store_exports3));
5625
- setHeartbeatConfig3(body.chatId, body);
5751
+ const { setHeartbeatConfig: setHeartbeatConfig2 } = await Promise.resolve().then(() => (init_store4(), store_exports3));
5752
+ setHeartbeatConfig2(body.chatId, body);
5626
5753
  return jsonResponse(res, { success: true });
5627
5754
  } catch (err) {
5628
5755
  return jsonResponse(res, { error: errorMessage(err) }, 400);
@@ -5671,7 +5798,7 @@ data: ${JSON.stringify(data)}
5671
5798
  if (url.pathname === "/api/config/set" && req.method === "POST") {
5672
5799
  try {
5673
5800
  const body = JSON.parse(await readBody(req));
5674
- const validKeys = ["backend", "model", "thinking", "summarizer", "mode", "verbose", "cwd", "voice"];
5801
+ const validKeys = ["backend", "model", "thinking", "summarizer", "mode", "verbose", "cwd", "voice", "response-style"];
5675
5802
  if (!validKeys.includes(body.key)) {
5676
5803
  return jsonResponse(res, { error: `Invalid config key. Valid: ${validKeys.join(", ")}` }, 400);
5677
5804
  }
@@ -5689,6 +5816,9 @@ data: ${JSON.stringify(data)}
5689
5816
  } else if (body.key === "voice") {
5690
5817
  const db3 = getDb();
5691
5818
  db3.prepare("INSERT OR REPLACE INTO chat_voice (chat_id, enabled) VALUES (?, ?)").run(body.chatId, body.value === "on" || body.value === "1" ? 1 : 0);
5819
+ } else if (body.key === "response-style") {
5820
+ const { setResponseStyle: setResponseStyle2 } = await Promise.resolve().then(() => (init_store4(), store_exports3));
5821
+ setResponseStyle2(body.chatId, body.value);
5692
5822
  } else if (body.key === "backend") {
5693
5823
  const { setBackend: setBackend2, clearSession: clearSession2, clearModel: clearModel2, clearThinkingLevel: clearThinkingLevel2 } = await Promise.resolve().then(() => (init_store4(), store_exports3));
5694
5824
  clearSession2(body.chatId);
@@ -5920,14 +6050,21 @@ var init_server = __esm({
5920
6050
  var agent_exports = {};
5921
6051
  __export(agent_exports, {
5922
6052
  askAgent: () => askAgent,
5923
- isAgentActive: () => isAgentActive,
5924
6053
  isChatBusy: () => isChatBusy,
5925
6054
  stopAgent: () => stopAgent
5926
6055
  });
5927
6056
  import { spawn as spawn4 } from "child_process";
5928
6057
  import { createInterface as createInterface3 } from "readline";
5929
- import { dirname as dirname2 } from "path";
5930
- import { fileURLToPath as fileURLToPath2 } from "url";
6058
+ function killProcessGroup(proc, signal = "SIGTERM") {
6059
+ try {
6060
+ if (proc.pid) process.kill(-proc.pid, signal);
6061
+ } catch {
6062
+ try {
6063
+ proc.kill(signal);
6064
+ } catch {
6065
+ }
6066
+ }
6067
+ }
5931
6068
  function withChatLock(chatId, fn) {
5932
6069
  const prev = chatLocks.get(chatId) ?? Promise.resolve();
5933
6070
  const isBlocked = activeChats.has(chatId);
@@ -5948,14 +6085,13 @@ function stopAgent(chatId) {
5948
6085
  if (!state) return false;
5949
6086
  state.cancelled = true;
5950
6087
  if (state.process) {
5951
- state.process.kill("SIGTERM");
5952
- state.killTimer = setTimeout(() => state.process?.kill("SIGKILL"), 2e3);
6088
+ killProcessGroup(state.process, "SIGTERM");
6089
+ state.killTimer = setTimeout(() => {
6090
+ if (state.process) killProcessGroup(state.process, "SIGKILL");
6091
+ }, 2e3);
5953
6092
  }
5954
6093
  return true;
5955
6094
  }
5956
- function isAgentActive(chatId) {
5957
- return activeChats.has(chatId);
5958
- }
5959
6095
  function spawnQuery(adapter, config2, model2, cancelState, onStream, onToolAction, thinkingLevel, timeoutMs) {
5960
6096
  const effectiveTimeout = timeoutMs ?? SPAWN_TIMEOUT_MS;
5961
6097
  return new Promise((resolve, reject) => {
@@ -5966,21 +6102,18 @@ function spawnQuery(adapter, config2, model2, cancelState, onStream, onToolActio
5966
6102
  const proc = spawn4(config2.executable, finalArgs, {
5967
6103
  env,
5968
6104
  stdio: ["ignore", "pipe", "pipe"],
6105
+ detached: true,
5969
6106
  ...config2.cwd ? { cwd: config2.cwd } : {}
5970
6107
  });
6108
+ proc.unref();
5971
6109
  cancelState.process = proc;
5972
6110
  let timedOut = false;
5973
6111
  let sigkillTimer;
5974
6112
  const spawnTimeout = setTimeout(() => {
5975
6113
  timedOut = true;
5976
6114
  warn(`[agent] Spawn timeout after ${effectiveTimeout / 1e3}s for ${adapter.id} \u2014 killing process`);
5977
- proc.kill("SIGTERM");
5978
- sigkillTimer = setTimeout(() => {
5979
- try {
5980
- proc.kill("SIGKILL");
5981
- } catch {
5982
- }
5983
- }, 3e3);
6115
+ killProcessGroup(proc, "SIGTERM");
6116
+ sigkillTimer = setTimeout(() => killProcessGroup(proc, "SIGKILL"), 3e3);
5984
6117
  }, effectiveTimeout);
5985
6118
  let resultText = "";
5986
6119
  let accumulatedText = "";
@@ -6068,7 +6201,7 @@ function spawnQuery(adapter, config2, model2, cancelState, onStream, onToolActio
6068
6201
  rl2.close();
6069
6202
  } catch {
6070
6203
  }
6071
- proc.kill("SIGTERM");
6204
+ killProcessGroup(proc, "SIGTERM");
6072
6205
  }
6073
6206
  break;
6074
6207
  }
@@ -6116,10 +6249,11 @@ async function askAgentImpl(chatId, userMessage, opts) {
6116
6249
  const { cwd, onStream, model: model2, backend: backend2, permMode, onToolAction, bootstrapTier, timeoutMs } = opts ?? {};
6117
6250
  const adapter = backend2 ? getAdapter(backend2) : getAdapterForChat(chatId);
6118
6251
  const mode = permMode ?? getMode(chatId);
6252
+ const responseStyle = getResponseStyle(chatId);
6119
6253
  const thinkingLevel = getThinkingLevel(chatId);
6120
6254
  const resolvedCwd = cwd ?? WORKSPACE_PATH;
6121
6255
  const tier = bootstrapTier ?? "full";
6122
- const fullPrompt = await assembleBootstrapPrompt(userMessage, tier, chatId, mode);
6256
+ const fullPrompt = await assembleBootstrapPrompt(userMessage, tier, chatId, mode, responseStyle);
6123
6257
  const existingSessionId = getSessionId(chatId);
6124
6258
  const allowedTools = getEnabledTools(chatId);
6125
6259
  const mcpConfigPath = getMcpConfigPath(chatId);
@@ -6209,7 +6343,7 @@ function injectMcpConfig(adapterId, args, mcpConfigPath) {
6209
6343
  if (!flag) return args;
6210
6344
  return [...args, ...flag, mcpConfigPath];
6211
6345
  }
6212
- var __filename2, __dirname2, activeChats, chatLocks, SPAWN_TIMEOUT_MS, MCP_CONFIG_FLAG;
6346
+ var activeChats, chatLocks, SPAWN_TIMEOUT_MS, MCP_CONFIG_FLAG;
6213
6347
  var init_agent = __esm({
6214
6348
  "src/agent.ts"() {
6215
6349
  "use strict";
@@ -6222,8 +6356,6 @@ var init_agent = __esm({
6222
6356
  init_summarize();
6223
6357
  init_server();
6224
6358
  init_mcp_config();
6225
- __filename2 = fileURLToPath2(import.meta.url);
6226
- __dirname2 = dirname2(__filename2);
6227
6359
  activeChats = /* @__PURE__ */ new Map();
6228
6360
  chatLocks = /* @__PURE__ */ new Map();
6229
6361
  SPAWN_TIMEOUT_MS = 10 * 60 * 1e3;
@@ -6280,30 +6412,40 @@ function parseTelegramTarget(target) {
6280
6412
  async function deliverJobOutput(job, responseText) {
6281
6413
  if (job.deliveryMode === "none") {
6282
6414
  log(`[delivery] Job #${job.id}: delivery=none, skipping`);
6283
- return;
6415
+ return true;
6416
+ }
6417
+ if (job.deliveryMode === "dry_run") {
6418
+ log(`[delivery] Job #${job.id}: DRY RUN \u2014 output (${responseText.length} chars):
6419
+ ${responseText.slice(0, 500)}`);
6420
+ appendToLog(job.chatId, `[cron:#${job.id}:dry_run] ${job.description}`, responseText, job.backend ?? "claude", job.model, null);
6421
+ return true;
6284
6422
  }
6285
6423
  try {
6286
6424
  if (job.deliveryMode === "webhook") {
6287
6425
  await deliverWebhook(job, responseText);
6288
- return;
6426
+ appendToLog(job.chatId, `[cron:#${job.id}] ${job.description}`, responseText, job.backend ?? "claude", job.model, null);
6427
+ return true;
6289
6428
  }
6290
6429
  const channelName = job.channel ?? "telegram";
6291
6430
  const channel = registry?.get(channelName);
6292
6431
  if (!channel) {
6293
6432
  error(`[delivery] Job #${job.id}: channel "${channelName}" not found`);
6294
- return;
6433
+ return false;
6295
6434
  }
6296
6435
  const targetChatId = job.target ?? job.chatId;
6297
6436
  const cleanText = await processFileSends(targetChatId, channel, responseText);
6298
- if (!cleanText) return;
6437
+ if (!cleanText) return true;
6299
6438
  if (channelName === "telegram") {
6300
6439
  const parsed = parseTelegramTarget(targetChatId);
6301
- await channel.sendText(parsed.chatId, cleanText);
6440
+ await channel.sendText(parsed.chatId, cleanText, void 0, parsed.threadId);
6302
6441
  } else {
6303
6442
  await channel.sendText(targetChatId, cleanText);
6304
6443
  }
6444
+ appendToLog(job.chatId, `[cron:#${job.id}] ${job.description}`, cleanText, job.backend ?? "claude", job.model, null);
6445
+ return true;
6305
6446
  } catch (err) {
6306
6447
  error(`[delivery] Job #${job.id} delivery failed (non-fatal): ${errorMessage(err)}`);
6448
+ return false;
6307
6449
  }
6308
6450
  }
6309
6451
  async function deliverWebhook(job, responseText) {
@@ -6321,7 +6463,8 @@ async function deliverWebhook(job, responseText) {
6321
6463
  description: job.description,
6322
6464
  text: responseText,
6323
6465
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
6324
- })
6466
+ }),
6467
+ signal: AbortSignal.timeout(3e4)
6325
6468
  });
6326
6469
  if (!resp.ok) {
6327
6470
  error(`[delivery] Webhook POST to ${url} failed: ${resp.status} ${resp.statusText}`);
@@ -6362,6 +6505,7 @@ var init_delivery = __esm({
6362
6505
  "src/scheduler/delivery.ts"() {
6363
6506
  "use strict";
6364
6507
  init_log();
6508
+ init_session_log();
6365
6509
  registry = null;
6366
6510
  BLOCKED_PATH_PATTERNS = [
6367
6511
  /\/\.ssh\//,
@@ -6691,18 +6835,22 @@ async function executeJob(job) {
6691
6835
  return;
6692
6836
  }
6693
6837
  const response = await runWithRetry(job, resolvedModel, runId, t0);
6694
- completeJobRun(runId, "success", {
6695
- usageInput: response.usage?.input,
6696
- usageOutput: response.usage?.output,
6697
- cacheRead: response.usage?.cacheRead,
6698
- durationMs: Date.now() - t0
6699
- });
6838
+ const isEmpty = !response.text || response.text.trim().length === 0 || /^\(No response from/i.test(response.text);
6839
+ const contentStatus = isEmpty ? "no_content" : "success";
6840
+ const durationMs = Date.now() - t0;
6700
6841
  updateJobLastRun(job.id, (/* @__PURE__ */ new Date()).toISOString());
6701
6842
  resetJobFailures(job.id);
6702
6843
  if (response.usage) {
6703
6844
  addUsage(job.chatId, response.usage.input, response.usage.output, response.usage.cacheRead, resolvedModel);
6704
6845
  }
6705
- await deliverJobOutput(job, response.text);
6846
+ const delivered = await deliverJobOutput(job, response.text);
6847
+ const finalStatus = !delivered && contentStatus === "success" ? "delivery_failed" : contentStatus;
6848
+ completeJobRun(runId, finalStatus, {
6849
+ usageInput: response.usage?.input,
6850
+ usageOutput: response.usage?.output,
6851
+ cacheRead: response.usage?.cacheRead,
6852
+ durationMs
6853
+ });
6706
6854
  } catch (err) {
6707
6855
  const durationMs = Date.now() - t0;
6708
6856
  const errorClass = classifyError(err);
@@ -6721,6 +6869,9 @@ async function executeJob(job) {
6721
6869
  }
6722
6870
  } finally {
6723
6871
  runningJobs.delete(job.id);
6872
+ if (job.sessionType === "isolated" && job.thinking && job.thinking !== "auto") {
6873
+ clearThinkingLevel(`cron:${job.id}:${runId}`);
6874
+ }
6724
6875
  }
6725
6876
  }
6726
6877
  async function runWithRetry(job, model2, runId, t0) {
@@ -6788,7 +6939,7 @@ function resolveJobModel(job) {
6788
6939
  const backendId = resolveJobBackendId(job);
6789
6940
  try {
6790
6941
  const adapter = getAdapter(backendId);
6791
- return getModel(job.chatId) ?? adapter.defaultModel;
6942
+ return adapter.defaultModel;
6792
6943
  } catch {
6793
6944
  return "claude-sonnet-4-6";
6794
6945
  }
@@ -6817,7 +6968,7 @@ var init_cron = __esm({
6817
6968
  });
6818
6969
 
6819
6970
  // src/agents/runners/wrap-backend.ts
6820
- import { join as join8 } from "path";
6971
+ import { join as join7 } from "path";
6821
6972
  function buildMcpCommands(backendId) {
6822
6973
  const exe = backendId;
6823
6974
  return {
@@ -6878,7 +7029,8 @@ function wrapBackendAdapter(adapter) {
6878
7029
  sessionId: opts.sessionId,
6879
7030
  permMode: opts.permMode ?? "plan",
6880
7031
  allowedTools: opts.allowedTools ?? [],
6881
- cwd: opts.cwd
7032
+ cwd: opts.cwd,
7033
+ model: opts.model
6882
7034
  });
6883
7035
  return config2.args;
6884
7036
  },
@@ -6907,7 +7059,7 @@ function wrapBackendAdapter(adapter) {
6907
7059
  const configPath = writeMcpConfigFile(server);
6908
7060
  return ["--mcp-config", configPath];
6909
7061
  },
6910
- getSkillPath: () => join8(SKILLS_PATH, `agent-${adapter.id}.md`)
7062
+ getSkillPath: () => join7(SKILLS_PATH, `agent-${adapter.id}.md`)
6911
7063
  };
6912
7064
  }
6913
7065
  var BACKEND_CAPABILITIES;
@@ -6950,7 +7102,7 @@ var init_wrap_backend = __esm({
6950
7102
 
6951
7103
  // src/agents/runners/config-loader.ts
6952
7104
  import { readFileSync as readFileSync6, readdirSync as readdirSync4, existsSync as existsSync9, mkdirSync as mkdirSync4, watchFile, unwatchFile } from "fs";
6953
- import { join as join9 } from "path";
7105
+ import { join as join8 } from "path";
6954
7106
  import { execFileSync } from "child_process";
6955
7107
  function resolveExecutable(config2) {
6956
7108
  if (existsSync9(config2.executable)) return config2.executable;
@@ -7086,7 +7238,7 @@ function configToRunner(config2) {
7086
7238
  prepareMcpInjection() {
7087
7239
  return [];
7088
7240
  },
7089
- getSkillPath: () => join9(SKILLS_PATH, `agent-${config2.id}.md`)
7241
+ getSkillPath: () => join8(SKILLS_PATH, `agent-${config2.id}.md`)
7090
7242
  };
7091
7243
  }
7092
7244
  function loadRunnerConfig(filePath) {
@@ -7106,7 +7258,7 @@ function loadAllRunnerConfigs() {
7106
7258
  const files = readdirSync4(RUNNERS_PATH).filter((f) => f.endsWith(".json"));
7107
7259
  const configs = [];
7108
7260
  for (const file of files) {
7109
- const config2 = loadRunnerConfig(join9(RUNNERS_PATH, file));
7261
+ const config2 = loadRunnerConfig(join8(RUNNERS_PATH, file));
7110
7262
  if (config2) configs.push(config2);
7111
7263
  }
7112
7264
  return configs;
@@ -7136,7 +7288,7 @@ function watchRunnerConfigs(onChange) {
7136
7288
  }
7137
7289
  const files = readdirSync4(RUNNERS_PATH).filter((f) => f.endsWith(".json"));
7138
7290
  for (const file of files) {
7139
- const fullPath = join9(RUNNERS_PATH, file);
7291
+ const fullPath = join8(RUNNERS_PATH, file);
7140
7292
  if (watchedFiles.has(fullPath)) continue;
7141
7293
  watchedFiles.add(fullPath);
7142
7294
  watchFile(fullPath, { interval: 5e3 }, () => {
@@ -7445,6 +7597,8 @@ var init_telegram2 = __esm({
7445
7597
  // Skills & profile
7446
7598
  { command: "skills", description: "List and invoke skills" },
7447
7599
  { command: "voice", description: "Toggle voice responses" },
7600
+ { command: "voice_config", description: "Configure voice provider and voice" },
7601
+ { command: "response_style", description: "Set the AI response style (concise/normal/detailed)" },
7448
7602
  { command: "imagine", description: "Generate an image from a prompt" },
7449
7603
  { command: "heartbeat", description: "Configure proactive heartbeat" },
7450
7604
  { command: "chats", description: "Manage multi-chat aliases" }
@@ -7503,11 +7657,12 @@ var init_telegram2 = __esm({
7503
7657
  async sendTyping(chatId) {
7504
7658
  await this.bot.api.sendChatAction(numericChatId(chatId), "typing");
7505
7659
  }
7506
- async sendText(chatId, text, parseMode) {
7660
+ async sendText(chatId, text, parseMode, threadId) {
7661
+ const threadOpts = threadId ? { message_thread_id: threadId } : {};
7507
7662
  if (parseMode === "plain") {
7508
7663
  const plainChunks = splitMessage(text);
7509
7664
  for (const chunk of plainChunks) {
7510
- await this.bot.api.sendMessage(numericChatId(chatId), chunk);
7665
+ await this.bot.api.sendMessage(numericChatId(chatId), chunk, { ...threadOpts });
7511
7666
  }
7512
7667
  return;
7513
7668
  }
@@ -7516,12 +7671,14 @@ var init_telegram2 = __esm({
7516
7671
  for (const chunk of chunks) {
7517
7672
  try {
7518
7673
  await this.bot.api.sendMessage(numericChatId(chatId), chunk, {
7519
- parse_mode: "HTML"
7674
+ parse_mode: "HTML",
7675
+ ...threadOpts
7520
7676
  });
7521
7677
  } catch {
7522
7678
  await this.bot.api.sendMessage(
7523
7679
  numericChatId(chatId),
7524
- chunk.replace(/<[^>]+>/g, "")
7680
+ chunk.replace(/<[^>]+>/g, ""),
7681
+ { ...threadOpts }
7525
7682
  );
7526
7683
  }
7527
7684
  }
@@ -7546,7 +7703,9 @@ var init_telegram2 = __esm({
7546
7703
  }
7547
7704
  async sendTextReturningId(chatId, text, parseMode) {
7548
7705
  try {
7549
- const msg = await this.bot.api.sendMessage(numericChatId(chatId), text);
7706
+ const formatted = parseMode === "html" ? text : parseMode === "plain" ? text : formatForTelegram(text);
7707
+ const opts = parseMode === "plain" ? {} : { parse_mode: "HTML" };
7708
+ const msg = await this.bot.api.sendMessage(numericChatId(chatId), formatted, opts);
7550
7709
  return msg.message_id.toString();
7551
7710
  } catch {
7552
7711
  return void 0;
@@ -7720,13 +7879,13 @@ __export(discover_exports, {
7720
7879
  });
7721
7880
  import { readdir, readFile as readFile2 } from "fs/promises";
7722
7881
  import { createHash } from "crypto";
7723
- import { homedir as homedir3 } from "os";
7724
- import { join as join10 } from "path";
7882
+ import { homedir as homedir4 } from "os";
7883
+ import { join as join9 } from "path";
7725
7884
  async function discoverAllSkills() {
7726
7885
  const rawSkills = [];
7727
7886
  rawSkills.push(...await scanSkillDir(SKILLS_PATH, "cc-claw"));
7728
7887
  for (const backendId of getAllBackendIds()) {
7729
- const dirs = BACKEND_SKILL_DIRS[backendId] ?? [join10(homedir3(), `.${backendId}`, "skills")];
7888
+ const dirs = BACKEND_SKILL_DIRS[backendId] ?? [join9(homedir4(), `.${backendId}`, "skills")];
7730
7889
  for (const dir of dirs) {
7731
7890
  rawSkills.push(...await scanSkillDir(dir, backendId));
7732
7891
  }
@@ -7746,7 +7905,7 @@ async function scanSkillDir(skillsDir, source) {
7746
7905
  let content;
7747
7906
  let resolvedPath;
7748
7907
  for (const candidate of SKILL_FILE_CANDIDATES) {
7749
- const p = join10(skillsDir, entry.name, candidate);
7908
+ const p = join9(skillsDir, entry.name, candidate);
7750
7909
  try {
7751
7910
  content = await readFile2(p, "utf-8");
7752
7911
  resolvedPath = p;
@@ -7849,11 +8008,11 @@ var init_discover = __esm({
7849
8008
  init_backends();
7850
8009
  SKILL_FILE_CANDIDATES = ["SKILL.md", "skill.md"];
7851
8010
  BACKEND_SKILL_DIRS = {
7852
- claude: [join10(homedir3(), ".claude", "skills")],
7853
- gemini: [join10(homedir3(), ".gemini", "skills")],
8011
+ claude: [join9(homedir4(), ".claude", "skills")],
8012
+ gemini: [join9(homedir4(), ".gemini", "skills")],
7854
8013
  codex: [
7855
- join10(homedir3(), ".agents", "skills"),
7856
- join10(homedir3(), ".codex", "skills")
8014
+ join9(homedir4(), ".agents", "skills"),
8015
+ join9(homedir4(), ".codex", "skills")
7857
8016
  ]
7858
8017
  };
7859
8018
  }
@@ -7866,7 +8025,7 @@ __export(install_exports, {
7866
8025
  });
7867
8026
  import { mkdir, readdir as readdir2, readFile as readFile3, cp } from "fs/promises";
7868
8027
  import { existsSync as existsSync10 } from "fs";
7869
- import { join as join11, basename } from "path";
8028
+ import { join as join10, basename } from "path";
7870
8029
  import { execSync as execSync4 } from "child_process";
7871
8030
  async function installSkillFromGitHub(urlOrShorthand) {
7872
8031
  let repoUrl;
@@ -7877,23 +8036,23 @@ async function installSkillFromGitHub(urlOrShorthand) {
7877
8036
  }
7878
8037
  repoUrl = parsed.cloneUrl;
7879
8038
  subPath = parsed.subPath;
7880
- const tmpDir = join11("/tmp", `cc-claw-skill-${Date.now()}`);
8039
+ const tmpDir = join10("/tmp", `cc-claw-skill-${Date.now()}`);
7881
8040
  try {
7882
8041
  log(`[skill-install] Cloning ${repoUrl} to ${tmpDir}`);
7883
8042
  execSync4(`git clone --depth 1 ${repoUrl} ${tmpDir}`, {
7884
8043
  stdio: "pipe",
7885
8044
  timeout: 3e4
7886
8045
  });
7887
- if (!existsSync10(join11(tmpDir, ".git"))) {
8046
+ if (!existsSync10(join10(tmpDir, ".git"))) {
7888
8047
  return { success: false, error: "Git clone failed: no .git directory produced" };
7889
8048
  }
7890
- const searchRoot = subPath ? join11(tmpDir, subPath) : tmpDir;
8049
+ const searchRoot = subPath ? join10(tmpDir, subPath) : tmpDir;
7891
8050
  const skillDir = await findSkillDir(searchRoot);
7892
8051
  if (!skillDir) {
7893
8052
  return { success: false, error: "No SKILL.md found in the repository." };
7894
8053
  }
7895
8054
  const skillFolderName = basename(skillDir);
7896
- const destDir = join11(SKILLS_PATH, skillFolderName);
8055
+ const destDir = join10(SKILLS_PATH, skillFolderName);
7897
8056
  if (existsSync10(destDir)) {
7898
8057
  log(`[skill-install] Overwriting existing skill at ${destDir}`);
7899
8058
  }
@@ -7901,12 +8060,12 @@ async function installSkillFromGitHub(urlOrShorthand) {
7901
8060
  await cp(skillDir, destDir, { recursive: true });
7902
8061
  let skillName = skillFolderName;
7903
8062
  try {
7904
- const content = await readFile3(join11(destDir, "SKILL.md"), "utf-8");
8063
+ const content = await readFile3(join10(destDir, "SKILL.md"), "utf-8");
7905
8064
  const nameMatch = content.match(/^name:\s*(.+)$/m);
7906
8065
  if (nameMatch) skillName = nameMatch[1].trim().replace(/^["']|["']$/g, "");
7907
8066
  } catch {
7908
8067
  try {
7909
- const content = await readFile3(join11(destDir, "skill.md"), "utf-8");
8068
+ const content = await readFile3(join10(destDir, "skill.md"), "utf-8");
7910
8069
  const nameMatch = content.match(/^name:\s*(.+)$/m);
7911
8070
  if (nameMatch) skillName = nameMatch[1].trim().replace(/^["']|["']$/g, "");
7912
8071
  } catch {
@@ -7941,15 +8100,15 @@ function parseGitHubUrl(input) {
7941
8100
  async function findSkillDir(root) {
7942
8101
  const candidates = ["SKILL.md", "skill.md"];
7943
8102
  for (const c of candidates) {
7944
- if (existsSync10(join11(root, c))) return root;
8103
+ if (existsSync10(join10(root, c))) return root;
7945
8104
  }
7946
8105
  try {
7947
8106
  const entries = await readdir2(root, { withFileTypes: true });
7948
8107
  for (const entry of entries) {
7949
8108
  if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
7950
8109
  for (const c of candidates) {
7951
- if (existsSync10(join11(root, entry.name, c))) {
7952
- return join11(root, entry.name);
8110
+ if (existsSync10(join10(root, entry.name, c))) {
8111
+ return join10(root, entry.name);
7953
8112
  }
7954
8113
  }
7955
8114
  }
@@ -7961,15 +8120,15 @@ async function findSkillDir(root) {
7961
8120
  if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
7962
8121
  let subEntries;
7963
8122
  try {
7964
- subEntries = await readdir2(join11(root, entry.name), { withFileTypes: true });
8123
+ subEntries = await readdir2(join10(root, entry.name), { withFileTypes: true });
7965
8124
  } catch {
7966
8125
  continue;
7967
8126
  }
7968
8127
  for (const sub of subEntries) {
7969
8128
  if (!sub.isDirectory() || sub.name.startsWith(".")) continue;
7970
8129
  for (const c of candidates) {
7971
- if (existsSync10(join11(root, entry.name, sub.name, c))) {
7972
- return join11(root, entry.name, sub.name);
8130
+ if (existsSync10(join10(root, entry.name, sub.name, c))) {
8131
+ return join10(root, entry.name, sub.name);
7973
8132
  }
7974
8133
  }
7975
8134
  }
@@ -7988,7 +8147,7 @@ var init_install = __esm({
7988
8147
 
7989
8148
  // src/bootstrap/profile.ts
7990
8149
  import { readFileSync as readFileSync7, writeFileSync as writeFileSync4, existsSync as existsSync11 } from "fs";
7991
- import { join as join12 } from "path";
8150
+ import { join as join11 } from "path";
7992
8151
  function hasActiveProfile(chatId) {
7993
8152
  return activeProfiles.has(chatId);
7994
8153
  }
@@ -8133,14 +8292,14 @@ var init_profile = __esm({
8133
8292
  "use strict";
8134
8293
  init_paths();
8135
8294
  init_log();
8136
- USER_PATH2 = join12(WORKSPACE_PATH, "USER.md");
8295
+ USER_PATH2 = join11(WORKSPACE_PATH, "USER.md");
8137
8296
  activeProfiles = /* @__PURE__ */ new Map();
8138
8297
  }
8139
8298
  });
8140
8299
 
8141
8300
  // src/bootstrap/heartbeat.ts
8142
8301
  import { readFileSync as readFileSync8, existsSync as existsSync12 } from "fs";
8143
- import { join as join13 } from "path";
8302
+ import { join as join12 } from "path";
8144
8303
  function initHeartbeat(channelReg) {
8145
8304
  registry2 = channelReg;
8146
8305
  }
@@ -8148,8 +8307,18 @@ function startHeartbeatForChat(chatId) {
8148
8307
  stopHeartbeatForChat(chatId);
8149
8308
  const config2 = getHeartbeatConfig(chatId);
8150
8309
  if (!config2 || !config2.enabled) return;
8310
+ let running = false;
8151
8311
  const timer = setInterval(async () => {
8152
- await runHeartbeat(chatId, config2);
8312
+ if (running) {
8313
+ log(`[heartbeat] Skipping tick for ${chatId}: previous heartbeat still running`);
8314
+ return;
8315
+ }
8316
+ running = true;
8317
+ try {
8318
+ await runHeartbeat(chatId, config2);
8319
+ } finally {
8320
+ running = false;
8321
+ }
8153
8322
  }, config2.intervalMs);
8154
8323
  activeTimers2.set(chatId, timer);
8155
8324
  const nextBeat = new Date(Date.now() + config2.intervalMs).toISOString();
@@ -8316,7 +8485,7 @@ var init_heartbeat = __esm({
8316
8485
  init_backends();
8317
8486
  init_health2();
8318
8487
  init_log();
8319
- HEARTBEAT_MD_PATH = join13(WORKSPACE_PATH, "HEARTBEAT.md");
8488
+ HEARTBEAT_MD_PATH = join12(WORKSPACE_PATH, "HEARTBEAT.md");
8320
8489
  HEARTBEAT_OK = "HEARTBEAT_OK";
8321
8490
  registry2 = null;
8322
8491
  activeTimers2 = /* @__PURE__ */ new Map();
@@ -8362,8 +8531,9 @@ var init_format_time = __esm({
8362
8531
  });
8363
8532
 
8364
8533
  // src/media/image-gen.ts
8365
- import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, existsSync as existsSync13 } from "fs";
8366
- import { join as join14 } from "path";
8534
+ import { mkdirSync as mkdirSync5, existsSync as existsSync13 } from "fs";
8535
+ import { writeFile as writeFile2 } from "fs/promises";
8536
+ import { join as join13 } from "path";
8367
8537
  async function generateImage(prompt) {
8368
8538
  const apiKey = process.env.GEMINI_API_KEY;
8369
8539
  if (!apiKey) {
@@ -8415,9 +8585,9 @@ async function generateImage(prompt) {
8415
8585
  }
8416
8586
  const ext = mimeType.includes("jpeg") || mimeType.includes("jpg") ? "jpg" : "png";
8417
8587
  const filename = `img_${Date.now()}.${ext}`;
8418
- const filePath = join14(IMAGE_OUTPUT_DIR, filename);
8588
+ const filePath = join13(IMAGE_OUTPUT_DIR, filename);
8419
8589
  const buffer = Buffer.from(imageData, "base64");
8420
- writeFileSync5(filePath, buffer);
8590
+ await writeFile2(filePath, buffer);
8421
8591
  log(`[image-gen] Saved ${buffer.length} bytes to ${filePath}`);
8422
8592
  return { filePath, text: textResponse, mimeType };
8423
8593
  }
@@ -8430,8 +8600,8 @@ var init_image_gen = __esm({
8430
8600
  "use strict";
8431
8601
  init_log();
8432
8602
  IMAGE_MODEL = "gemini-3.1-flash-image-preview";
8433
- IMAGE_OUTPUT_DIR = join14(
8434
- process.env.CC_CLAW_HOME ?? join14(process.env.HOME ?? "/tmp", ".cc-claw"),
8603
+ IMAGE_OUTPUT_DIR = join13(
8604
+ process.env.CC_CLAW_HOME ?? join13(process.env.HOME ?? "/tmp", ".cc-claw"),
8435
8605
  "data",
8436
8606
  "images"
8437
8607
  );
@@ -8458,6 +8628,23 @@ function toggleVoice(chatId) {
8458
8628
  `).run(chatId, newState, newState);
8459
8629
  return newState === 1;
8460
8630
  }
8631
+ function getVoiceConfig(chatId) {
8632
+ const db3 = getDb();
8633
+ const row = db3.prepare("SELECT enabled, provider, voice_id FROM chat_voice WHERE chat_id = ?").get(chatId);
8634
+ return {
8635
+ enabled: row?.enabled === 1,
8636
+ provider: row?.provider ?? "elevenlabs",
8637
+ voiceId: row?.voice_id ?? null
8638
+ };
8639
+ }
8640
+ function setVoiceProvider(chatId, provider, voiceId) {
8641
+ const db3 = getDb();
8642
+ db3.prepare(`
8643
+ INSERT INTO chat_voice (chat_id, enabled, provider, voice_id)
8644
+ VALUES (?, 1, ?, ?)
8645
+ ON CONFLICT(chat_id) DO UPDATE SET provider = ?, voice_id = ?, enabled = 1
8646
+ `).run(chatId, provider, voiceId, provider, voiceId);
8647
+ }
8461
8648
  async function transcribeAudio(audioBuffer, mimeType = "audio/ogg") {
8462
8649
  const GROQ_API_KEY = process.env.GROQ_API_KEY;
8463
8650
  if (!GROQ_API_KEY) return null;
@@ -8476,25 +8663,48 @@ async function transcribeAudio(audioBuffer, mimeType = "audio/ogg") {
8476
8663
  }
8477
8664
  return (await response.text()).trim();
8478
8665
  }
8479
- async function synthesizeSpeech(text) {
8666
+ async function synthesizeSpeech(text, chatId) {
8480
8667
  const ttsText = text.length > 4e3 ? text.slice(0, 4e3) + "..." : text;
8481
- const elevenLabsKey = process.env.ELEVENLABS_API_KEY;
8482
- if (elevenLabsKey) {
8668
+ const config2 = chatId ? getVoiceConfig(chatId) : null;
8669
+ const provider = config2?.provider ?? "elevenlabs";
8670
+ if (provider === "grok") {
8671
+ const xaiKey = process.env.XAI_API_KEY;
8672
+ if (xaiKey) {
8673
+ try {
8674
+ return await grokTts(ttsText, xaiKey, config2?.voiceId ?? "eve");
8675
+ } catch (err) {
8676
+ error("[tts] Grok failed:", err);
8677
+ }
8678
+ } else {
8679
+ log("[tts] Grok selected but XAI_API_KEY not set, falling back");
8680
+ }
8681
+ }
8682
+ if (provider === "macos") {
8483
8683
  try {
8484
- return await elevenLabsTts(ttsText, elevenLabsKey);
8684
+ return await macOsTts(ttsText, config2?.voiceId ?? "Samantha");
8485
8685
  } catch (err) {
8486
- error("[tts] ElevenLabs failed:", err);
8686
+ error("[tts] macOS TTS failed:", err);
8687
+ }
8688
+ }
8689
+ if (provider === "elevenlabs" || provider === "grok") {
8690
+ const elevenLabsKey = process.env.ELEVENLABS_API_KEY;
8691
+ if (elevenLabsKey) {
8692
+ try {
8693
+ const voiceId = provider === "elevenlabs" && config2?.voiceId ? config2.voiceId : process.env.ELEVENLABS_VOICE_ID ?? "21m00Tcm4TlvDq8ikWAM";
8694
+ return await elevenLabsTts(ttsText, elevenLabsKey, voiceId);
8695
+ } catch (err) {
8696
+ error("[tts] ElevenLabs failed:", err);
8697
+ }
8487
8698
  }
8488
8699
  }
8489
8700
  try {
8490
- return await macOsTts(ttsText);
8701
+ return await macOsTts(ttsText, "Samantha");
8491
8702
  } catch (err) {
8492
8703
  error("[tts] macOS TTS failed:", err);
8493
8704
  }
8494
8705
  return null;
8495
8706
  }
8496
- async function elevenLabsTts(text, apiKey) {
8497
- const voiceId = process.env.ELEVENLABS_VOICE_ID ?? "21m00Tcm4TlvDq8ikWAM";
8707
+ async function elevenLabsTts(text, apiKey, voiceId) {
8498
8708
  const response = await fetch(
8499
8709
  `https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`,
8500
8710
  {
@@ -8515,12 +8725,47 @@ async function elevenLabsTts(text, apiKey) {
8515
8725
  }
8516
8726
  return Buffer.from(await response.arrayBuffer());
8517
8727
  }
8518
- async function macOsTts(text) {
8728
+ async function grokTts(text, apiKey, voiceId) {
8729
+ const response = await fetch("https://api.x.ai/v1/tts", {
8730
+ method: "POST",
8731
+ headers: {
8732
+ "Authorization": `Bearer ${apiKey}`,
8733
+ "Content-Type": "application/json"
8734
+ },
8735
+ body: JSON.stringify({
8736
+ text,
8737
+ voice_id: voiceId,
8738
+ language: "en"
8739
+ })
8740
+ });
8741
+ if (!response.ok) {
8742
+ const errText = await response.text().catch(() => "");
8743
+ throw new Error(`Grok TTS API error: ${response.status} ${errText}`);
8744
+ }
8745
+ const mp3Buffer = Buffer.from(await response.arrayBuffer());
8746
+ return await mp3ToOgg(mp3Buffer);
8747
+ }
8748
+ async function mp3ToOgg(mp3Buffer) {
8749
+ const id = crypto.randomUUID();
8750
+ const tmpMp3 = `/tmp/cc-claw-tts-${id}.mp3`;
8751
+ const tmpOgg = `/tmp/cc-claw-tts-${id}.ogg`;
8752
+ const { writeFile: writeFile5 } = await import("fs/promises");
8753
+ await writeFile5(tmpMp3, mp3Buffer);
8754
+ await execFileAsync2("ffmpeg", ["-y", "-i", tmpMp3, "-c:a", "libopus", "-b:a", "64k", tmpOgg]);
8755
+ const oggBuffer = await readFile4(tmpOgg);
8756
+ unlink(tmpMp3).catch((err) => {
8757
+ error("[tts] cleanup failed:", err);
8758
+ });
8759
+ unlink(tmpOgg).catch((err) => {
8760
+ error("[tts] cleanup failed:", err);
8761
+ });
8762
+ return oggBuffer;
8763
+ }
8764
+ async function macOsTts(text, voice2 = "Samantha") {
8519
8765
  const id = crypto.randomUUID();
8520
8766
  const tmpAiff = `/tmp/cc-claw-tts-${id}.aiff`;
8521
8767
  const tmpOgg = `/tmp/cc-claw-tts-${id}.ogg`;
8522
- const sanitized = text.replace(/['"\\$`]/g, " ");
8523
- await execFileAsync2("say", ["-o", tmpAiff, sanitized]);
8768
+ await execFileAsync2("say", ["-v", voice2, "-o", tmpAiff, text]);
8524
8769
  await execFileAsync2("ffmpeg", ["-y", "-i", tmpAiff, "-c:a", "libopus", "-b:a", "64k", tmpOgg]);
8525
8770
  const oggBuffer = await readFile4(tmpOgg);
8526
8771
  unlink(tmpAiff).catch((err) => {
@@ -8531,13 +8776,28 @@ async function macOsTts(text) {
8531
8776
  });
8532
8777
  return oggBuffer;
8533
8778
  }
8534
- var execFileAsync2;
8779
+ var execFileAsync2, ELEVENLABS_VOICES, GROK_VOICES, MACOS_VOICES;
8535
8780
  var init_stt = __esm({
8536
8781
  "src/voice/stt.ts"() {
8537
8782
  "use strict";
8538
8783
  init_log();
8539
8784
  init_store4();
8540
8785
  execFileAsync2 = promisify2(execFile2);
8786
+ ELEVENLABS_VOICES = {
8787
+ "21m00Tcm4TlvDq8ikWAM": { name: "Rachel", gender: "F" },
8788
+ "EXAVITQu4vr4xnSDxMaL": { name: "Sarah", gender: "F" },
8789
+ "XB0fDUnXU5powFXDhCwa": { name: "Charlotte", gender: "F" },
8790
+ "pFZP5JQG7iQjIQuC4Bku": { name: "Lily", gender: "F" },
8791
+ "pNInz6obpgDQGcFmaJgB": { name: "Adam", gender: "M" },
8792
+ "nPczCjzI2devNBz1zQrb": { name: "Brian", gender: "M" },
8793
+ "onwK4e9ZLuTAKqWW03F9": { name: "Daniel", gender: "M" },
8794
+ "TxGEqnHWrfWFTfGW9XjX": { name: "Josh", gender: "M" }
8795
+ };
8796
+ GROK_VOICES = ["eve", "ara", "rex", "sal", "leo"];
8797
+ MACOS_VOICES = {
8798
+ "Samantha": { name: "Samantha", gender: "F" },
8799
+ "Albert": { name: "Albert", gender: "M" }
8800
+ };
8541
8801
  }
8542
8802
  });
8543
8803
 
@@ -9380,7 +9640,7 @@ var init_backend_cmd = __esm({
9380
9640
  });
9381
9641
 
9382
9642
  // src/router.ts
9383
- import { readFile as readFile5, writeFile as writeFile2, unlink as unlink2 } from "fs/promises";
9643
+ import { readFile as readFile5, writeFile as writeFile3, unlink as unlink2 } from "fs/promises";
9384
9644
  import { resolve as resolvePath } from "path";
9385
9645
  function parseMcpListOutput(output2) {
9386
9646
  const results = [];
@@ -9506,7 +9766,7 @@ async function handleCommand(msg, channel) {
9506
9766
  case "help":
9507
9767
  await channel.sendText(
9508
9768
  chatId,
9509
- "Hey! I'm CC-Claw \u2014 your personal AI assistant on Telegram.\n\nI use AI coding CLIs (Claude, Gemini, Codex) as my brain. Just send me a message to get started.\n\nCommands:\n/backend [name] - Switch AI backend (or /claude /gemini /codex)\n/model - Switch model for active backend\n/summarizer - Configure session summarization model\n/status - Show session, model, backend, and usage\n/cost - Show estimated API cost (use /cost all for all-time)\n/usage - Show usage per backend with limits\n/limits - Configure usage limits per backend\n/newchat - Start a fresh conversation\n/summarize - Save session to memory (without resetting)\n/summarize all - Summarize all pending sessions (pre-restart)\n/cwd <path> - Set working directory\n/cwd - Show current working directory\n/memory - List stored memories\n/remember <text> - Save a memory\n/forget <keyword> - Remove a memory\n/voice - Toggle voice responses\n/imagine <prompt> - Generate an image (or /image)\n/cron <description> - Schedule a task (or /schedule)\n/cron - List scheduled jobs (or /jobs)\n/cron cancel <id> - Cancel a job\n/cron pause <id> - Pause a job\n/cron resume <id> - Resume a job\n/cron run <id> - Trigger a job now\n/cron runs [id] - View run history\n/cron edit <id> - Edit a job\n/cron health - Scheduler health\n/skills - List skills from all backends\n/skill-install <url> - Install a skill from GitHub\n/setup-profile - Set up your user profile\n/chats - List authorized chats and aliases\n/heartbeat - Proactive awareness (on/off/interval/hours)\n/history - List recent session summaries\n/stop - Cancel the current running task\n/tools - Configure which tools the agent can use\n/permissions - Switch permission mode (yolo/safe/plan)\n/verbose - Tool visibility (off/normal/verbose)\n/agents - List active sub-agents\n/tasks - Show task board for current orchestration\n/stopagent <id> - Cancel a specific sub-agent\n/stopall - Cancel all sub-agents in this chat\n/runners - List registered CLI runners\n/mcps - List registered MCP servers\n/help - Show this message",
9769
+ "Hey! I'm CC-Claw \u2014 your personal AI assistant on Telegram.\n\nI use AI coding CLIs (Claude, Gemini, Codex) as my brain. Just send me a message to get started.\n\nCommands:\n/backend [name] - Switch AI backend (or /claude /gemini /codex)\n/model - Switch model for active backend\n/summarizer - Configure session summarization model\n/status - Show session, model, backend, and usage\n/cost - Show estimated API cost (use /cost all for all-time)\n/usage - Show usage per backend with limits\n/limits - Configure usage limits per backend\n/newchat - Start a fresh conversation\n/summarize - Save session to memory (without resetting)\n/summarize all - Summarize all pending sessions (pre-restart)\n/cwd <path> - Set working directory\n/cwd - Show current working directory\n/memory - List stored memories\n/remember <text> - Save a memory\n/forget <keyword> - Remove a memory\n/voice - Toggle voice responses\n/voice_config - Configure voice provider and voice\n/imagine <prompt> - Generate an image (or /image)\n/cron <description> - Schedule a task (or /schedule)\n/cron - List scheduled jobs (or /jobs)\n/cron cancel <id> - Cancel a job\n/cron pause <id> - Pause a job\n/cron resume <id> - Resume a job\n/cron run <id> - Trigger a job now\n/cron runs [id] - View run history\n/cron edit <id> - Edit a job\n/cron health - Scheduler health\n/skills - List skills from all backends\n/skill-install <url> - Install a skill from GitHub\n/setup-profile - Set up your user profile\n/chats - List authorized chats and aliases\n/heartbeat - Proactive awareness (on/off/interval/hours)\n/history - List recent session summaries\n/stop - Cancel the current running task\n/tools - Configure which tools the agent can use\n/permissions - Switch permission mode (yolo/safe/plan)\n/verbose - Tool visibility (off/normal/verbose)\n/agents - List active sub-agents\n/tasks - Show task board for current orchestration\n/stopagent <id> - Cancel a specific sub-agent\n/stopall - Cancel all sub-agents in this chat\n/runners - List registered CLI runners\n/mcps - List registered MCP servers\n/help - Show this message",
9510
9770
  "plain"
9511
9771
  );
9512
9772
  break;
@@ -10000,12 +10260,37 @@ ${lines.join("\n")}`, "plain");
10000
10260
  break;
10001
10261
  }
10002
10262
  case "voice": {
10003
- const enabled = toggleVoice(chatId);
10004
- await channel.sendText(
10005
- chatId,
10006
- enabled ? "Voice responses enabled." : "Voice responses disabled.",
10007
- "plain"
10008
- );
10263
+ const vcEnabled = isVoiceEnabled(chatId);
10264
+ if (typeof channel.sendKeyboard === "function") {
10265
+ await channel.sendKeyboard(chatId, `\u{1F3A7} Voice responses: ${vcEnabled ? "ON" : "OFF"}`, [
10266
+ [
10267
+ { label: `${vcEnabled ? "" : "\u2713 "}\u{1F507} Off`, data: "voice:off" },
10268
+ { label: `${vcEnabled ? "\u2713 " : ""}\u{1F50A} On`, data: "voice:on" }
10269
+ ]
10270
+ ]);
10271
+ } else {
10272
+ const toggled = toggleVoice(chatId);
10273
+ await channel.sendText(chatId, toggled ? "Voice responses enabled." : "Voice responses disabled.", "plain");
10274
+ }
10275
+ break;
10276
+ }
10277
+ case "voice_config": {
10278
+ await sendVoiceConfigKeyboard(chatId, channel);
10279
+ break;
10280
+ }
10281
+ case "response_style": {
10282
+ const currentStyle = getResponseStyle(chatId);
10283
+ if (typeof channel.sendKeyboard === "function") {
10284
+ await channel.sendKeyboard(chatId, "\u{1F5E3}\uFE0F AI Response Style:", [
10285
+ [
10286
+ { label: `${currentStyle === "concise" ? "\u2713 " : ""}Concise`, data: "style:concise" },
10287
+ { label: `${currentStyle === "normal" ? "\u2713 " : ""}Normal`, data: "style:normal" },
10288
+ { label: `${currentStyle === "detailed" ? "\u2713 " : ""}Detailed`, data: "style:detailed" }
10289
+ ]
10290
+ ]);
10291
+ } else {
10292
+ await channel.sendText(chatId, `Current Response Style: ${currentStyle}`, "plain");
10293
+ }
10009
10294
  break;
10010
10295
  }
10011
10296
  case "imagine":
@@ -10306,8 +10591,8 @@ Use /skills to see it.`, "plain");
10306
10591
  if (!lim.max_input_tokens) continue;
10307
10592
  const u = getBackendUsageInWindow(lim.backend, lim.window);
10308
10593
  const pct = (u.input_tokens / lim.max_input_tokens * 100).toFixed(0);
10309
- const warn3 = u.input_tokens / lim.max_input_tokens >= lim.warn_pct;
10310
- lines.push(` ${lim.backend} (${lim.window}): ${pct}% of ${(lim.max_input_tokens / 1e3).toFixed(0)}K${warn3 ? " \u26A0\uFE0F" : ""}`);
10594
+ const warn2 = u.input_tokens / lim.max_input_tokens >= lim.warn_pct;
10595
+ lines.push(` ${lim.backend} (${lim.window}): ${pct}% of ${(lim.max_input_tokens / 1e3).toFixed(0)}K${warn2 ? " \u26A0\uFE0F" : ""}`);
10311
10596
  }
10312
10597
  }
10313
10598
  await channel.sendText(chatId, lines.join("\n"), "plain");
@@ -10507,8 +10792,8 @@ Use /skills to see it.`, "plain");
10507
10792
  lines.push(` \u2705 <b>cc-claw</b> <i>Agent orchestrator (spawn, tasks, inbox)</i>`);
10508
10793
  }
10509
10794
  const { execFile: execFile5 } = await import("child_process");
10510
- const { homedir: homedir6 } = await import("os");
10511
- const discoveryCwd = homedir6();
10795
+ const { homedir: homedir7 } = await import("os");
10796
+ const discoveryCwd = homedir7();
10512
10797
  const runnerResults = await Promise.allSettled(
10513
10798
  getAllRunners().map((runner) => {
10514
10799
  const listCmd = runner.getMcpListCommand();
@@ -10697,24 +10982,44 @@ async function handleMedia(msg, channel) {
10697
10982
  return;
10698
10983
  }
10699
10984
  const videoBuffer = await channel.downloadFile(fileId);
10700
- const geminiPrompt = caption || "Analyze this video and describe what you see in detail.";
10701
- let analysis;
10702
- try {
10703
- const videoMime = msg.metadata?.mimeType ?? msg.mimeType ?? "video/mp4";
10704
- analysis = await analyzeVideo(videoBuffer, geminiPrompt, videoMime);
10705
- } catch (err) {
10706
- analysis = `Video analysis failed: ${errorMessage(err)}`;
10707
- }
10708
- const prompt2 = caption ? `The user sent a video with caption: "${caption}"
10985
+ const videoMime = msg.metadata?.mimeType ?? msg.mimeType ?? "video/mp4";
10986
+ const videoExt = videoMime.split("/")[1]?.replace("quicktime", "mov") || "mp4";
10987
+ const tempVideoPath = `/tmp/cc-claw-video-${Date.now()}.${videoExt}`;
10988
+ await writeFile3(tempVideoPath, videoBuffer);
10989
+ let prompt2;
10990
+ const cleanupVideo = () => unlink2(tempVideoPath).catch(() => {
10991
+ });
10992
+ if (wantsVideoAnalysis(caption)) {
10993
+ const geminiPrompt = caption || "Analyze this video and describe what you see in detail.";
10994
+ let analysis;
10995
+ try {
10996
+ analysis = await analyzeVideo(videoBuffer, geminiPrompt, videoMime);
10997
+ } catch (err) {
10998
+ analysis = `Video analysis failed: ${errorMessage(err)}`;
10999
+ }
11000
+ prompt2 = caption ? `The user sent a video with caption: "${caption}"
10709
11001
 
10710
11002
  Gemini video analysis:
10711
11003
  ${analysis}
10712
11004
 
11005
+ The video is also saved at: ${tempVideoPath}
11006
+
10713
11007
  Respond to the user based on this analysis.` : `The user sent a video. Gemini analyzed it:
10714
11008
 
10715
11009
  ${analysis}
10716
11010
 
11011
+ The video is also saved at: ${tempVideoPath}
11012
+
10717
11013
  Summarize this for the user.`;
11014
+ } else {
11015
+ prompt2 = caption ? `The user sent a video with caption: "${caption}"
11016
+
11017
+ The video has been saved to: ${tempVideoPath}
11018
+
11019
+ Respond to their message. Do NOT analyze the video unless they ask you to.` : `The user sent a video. It has been saved to: ${tempVideoPath}
11020
+
11021
+ Acknowledge receipt. Do NOT analyze the video unless they ask you to.`;
11022
+ }
10718
11023
  const vidModel = resolveModel(chatId);
10719
11024
  const vMode = getMode(chatId);
10720
11025
  const vidVerbose = getVerboseLevel(chatId);
@@ -10722,6 +11027,7 @@ Summarize this for the user.`;
10722
11027
  const response2 = await askAgent(chatId, prompt2, { cwd: getCwd(chatId), model: vidModel, permMode: vMode, onToolAction: vidToolCb });
10723
11028
  if (response2.usage) addUsage(chatId, response2.usage.input, response2.usage.output, response2.usage.cacheRead, vidModel);
10724
11029
  await sendResponse(chatId, channel, response2.text, msg.messageId);
11030
+ cleanupVideo();
10725
11031
  return;
10726
11032
  }
10727
11033
  const fileBuffer = await channel.downloadFile(fileName);
@@ -10732,7 +11038,7 @@ Summarize this for the user.`;
10732
11038
  if (msg.type === "photo" || isImageExt(ext)) {
10733
11039
  const imgExt = msg.type === "photo" ? "jpg" : ext || "jpg";
10734
11040
  tempFilePath = `/tmp/cc-claw-img-${Date.now()}.${imgExt}`;
10735
- await writeFile2(tempFilePath, fileBuffer);
11041
+ await writeFile3(tempFilePath, fileBuffer);
10736
11042
  prompt = caption ? `The user sent an image with caption: "${caption}"
10737
11043
 
10738
11044
  The image has been saved to: ${tempFilePath}
@@ -11068,7 +11374,7 @@ async function sendResponse(chatId, channel, text, messageId) {
11068
11374
  if (!cleanText) return;
11069
11375
  if (isVoiceEnabled(chatId)) {
11070
11376
  try {
11071
- const audioBuffer = await synthesizeSpeech(cleanText);
11377
+ const audioBuffer = await synthesizeSpeech(cleanText, chatId);
11072
11378
  if (audioBuffer) {
11073
11379
  await channel.sendVoice(chatId, audioBuffer);
11074
11380
  return;
@@ -11082,6 +11388,70 @@ async function sendResponse(chatId, channel, text, messageId) {
11082
11388
  function isImageExt(ext) {
11083
11389
  return ["jpg", "jpeg", "png", "gif", "webp", "bmp", "svg"].includes(ext);
11084
11390
  }
11391
+ async function sendVoiceConfigKeyboard(chatId, channel) {
11392
+ if (typeof channel.sendKeyboard !== "function") {
11393
+ await channel.sendText(chatId, "Voice configuration requires an interactive channel (Telegram).", "plain");
11394
+ return;
11395
+ }
11396
+ const config2 = getVoiceConfig(chatId);
11397
+ const currentVoiceName = config2.provider === "elevenlabs" ? ELEVENLABS_VOICES[config2.voiceId ?? ""]?.name ?? "Rachel" : config2.provider === "macos" ? MACOS_VOICES[config2.voiceId ?? ""]?.name ?? "Samantha" : config2.voiceId ?? "eve";
11398
+ const providerLabel = config2.provider === "grok" ? "Grok (xAI)" : config2.provider === "macos" ? "macOS" : "ElevenLabs";
11399
+ const header2 = `\u{1F3A7} Voice Configuration
11400
+ Provider: ${providerLabel}
11401
+ Voice: ${currentVoiceName}
11402
+ Status: ${config2.enabled ? "ON" : "OFF"}`;
11403
+ const buttons = [];
11404
+ buttons.push([
11405
+ { label: `${config2.provider === "elevenlabs" ? "\u2713 " : ""}ElevenLabs`, data: "vcfg:p:elevenlabs" },
11406
+ { label: `${config2.provider === "grok" ? "\u2713 " : ""}Grok`, data: "vcfg:p:grok" },
11407
+ { label: `${config2.provider === "macos" ? "\u2713 " : ""}macOS`, data: "vcfg:p:macos" }
11408
+ ]);
11409
+ if (config2.provider === "elevenlabs") {
11410
+ const entries = Object.entries(ELEVENLABS_VOICES);
11411
+ const female = entries.filter(([, v]) => v.gender === "F");
11412
+ const male = entries.filter(([, v]) => v.gender === "M");
11413
+ buttons.push(female.map(([id, v]) => ({
11414
+ label: `${config2.voiceId === id ? "\u2713 " : ""}${v.name}`,
11415
+ data: `vcfg:v:${id}`
11416
+ })));
11417
+ buttons.push(male.map(([id, v]) => ({
11418
+ label: `${config2.voiceId === id ? "\u2713 " : ""}${v.name}`,
11419
+ data: `vcfg:v:${id}`
11420
+ })));
11421
+ } else if (config2.provider === "grok") {
11422
+ buttons.push(GROK_VOICES.map((v) => ({
11423
+ label: `${config2.voiceId === v ? "\u2713 " : ""}${v.charAt(0).toUpperCase() + v.slice(1)}`,
11424
+ data: `vcfg:v:${v}`
11425
+ })));
11426
+ } else {
11427
+ const entries = Object.entries(MACOS_VOICES);
11428
+ buttons.push(entries.map(([id, v]) => ({
11429
+ label: `${config2.voiceId === id ? "\u2713 " : ""}${v.name}`,
11430
+ data: `vcfg:v:${id}`
11431
+ })));
11432
+ }
11433
+ await channel.sendKeyboard(chatId, header2, buttons);
11434
+ }
11435
+ function wantsVideoAnalysis(caption) {
11436
+ if (!caption) return false;
11437
+ const lower = caption.toLowerCase();
11438
+ const patterns = [
11439
+ /\banalyze\b/,
11440
+ /\banalysis\b/,
11441
+ /\bdescribe\b/,
11442
+ /\bwhat('s| is) (in |happening)/,
11443
+ /\btell me (about|what)/,
11444
+ /\bexplain\b/,
11445
+ /\bsummarize\b/,
11446
+ /\bwhat do you see\b/,
11447
+ /\breview\b/,
11448
+ /\bwatch\b/,
11449
+ /\btranscri/,
11450
+ /\bidentif/,
11451
+ /\bwhat happens\b/
11452
+ ];
11453
+ return patterns.some((p) => p.test(lower));
11454
+ }
11085
11455
  async function sendBackendSwitchConfirmation(chatId, target, channel) {
11086
11456
  const current = getBackend(chatId);
11087
11457
  const targetAdapter = getAdapter(target);
@@ -11227,7 +11597,7 @@ ${PERM_MODES[chosen]}`,
11227
11597
  const pending = getPendingEscalation(chatId);
11228
11598
  if (pending) {
11229
11599
  removePendingEscalation(chatId);
11230
- await handleMessage({ text: pending, chatId, source: "telegram" }, channel);
11600
+ await handleMessage({ text: pending, chatId, source: "telegram", type: "text" }, channel);
11231
11601
  }
11232
11602
  } else if (data === "perm:deny") {
11233
11603
  removePendingEscalation(chatId);
@@ -11315,8 +11685,9 @@ ${PERM_MODES[chosen]}`,
11315
11685
  pendingInterrupts.delete(targetChatId);
11316
11686
  stopAgent(targetChatId);
11317
11687
  await channel.sendText(chatId, "\u26A1 Stopping current task and processing your message\u2026", "plain");
11318
- await new Promise((r) => setTimeout(r, 500));
11319
- await handleMessage(pending.msg, pending.channel);
11688
+ bypassBusyCheck.add(targetChatId);
11689
+ handleMessage(pending.msg, pending.channel).catch(() => {
11690
+ });
11320
11691
  } else if (action === "queue" && pending) {
11321
11692
  pendingInterrupts.delete(targetChatId);
11322
11693
  bypassBusyCheck.add(targetChatId);
@@ -11347,6 +11718,60 @@ ${PERM_MODES[chosen]}`,
11347
11718
  } else {
11348
11719
  await channel.sendText(chatId, "Fallback expired. Use /backend to switch manually.", "plain");
11349
11720
  }
11721
+ } else if (data.startsWith("voice:")) {
11722
+ const action = data.slice(6);
11723
+ if (action === "on" || action === "off") {
11724
+ const current = isVoiceEnabled(chatId);
11725
+ const desired = action === "on";
11726
+ if (current !== desired) toggleVoice(chatId);
11727
+ await channel.sendText(chatId, desired ? "\u{1F50A} Voice responses enabled." : "\u{1F507} Voice responses disabled.", "plain");
11728
+ }
11729
+ } else if (data.startsWith("style:")) {
11730
+ const selectedStyle = data.split(":")[1];
11731
+ if (selectedStyle === "concise" || selectedStyle === "normal" || selectedStyle === "detailed") {
11732
+ setResponseStyle(chatId, selectedStyle);
11733
+ if (typeof channel.sendKeyboard === "function") {
11734
+ await channel.sendKeyboard(chatId, "\u{1F5E3}\uFE0F AI Response Style:", [
11735
+ [
11736
+ { label: `${selectedStyle === "concise" ? "\u2713 " : ""}Concise`, data: "style:concise" },
11737
+ { label: `${selectedStyle === "normal" ? "\u2713 " : ""}Normal`, data: "style:normal" },
11738
+ { label: `${selectedStyle === "detailed" ? "\u2713 " : ""}Detailed`, data: "style:detailed" }
11739
+ ]
11740
+ ]);
11741
+ }
11742
+ await channel.sendText(chatId, `Response style set to: ${selectedStyle}`, "plain");
11743
+ }
11744
+ } else if (data.startsWith("vcfg:")) {
11745
+ const parts = data.slice(5).split(":");
11746
+ const action = parts[0];
11747
+ if (action === "p") {
11748
+ const provider = parts[1];
11749
+ if (provider === "elevenlabs" || provider === "grok" || provider === "macos") {
11750
+ if (provider === "grok" && !process.env.XAI_API_KEY) {
11751
+ await channel.sendText(
11752
+ chatId,
11753
+ "\u26A0\uFE0F Grok requires `XAI_API_KEY` to be set.\n\nAdd it to your config:\n```\necho 'XAI_API_KEY=your-key-here' >> ~/.cc-claw/.env\ncc-claw service restart\n```\n\nGet a key at: https://console.x.ai/team/default/api-keys\n\nSetting Grok as your provider \u2014 it will activate once the key is added.",
11754
+ "markdown"
11755
+ );
11756
+ }
11757
+ if (provider === "elevenlabs" && !process.env.ELEVENLABS_API_KEY) {
11758
+ await channel.sendText(
11759
+ chatId,
11760
+ "\u26A0\uFE0F ElevenLabs requires `ELEVENLABS_API_KEY` to be set.\n\nAdd it to your config:\n```\necho 'ELEVENLABS_API_KEY=your-key-here' >> ~/.cc-claw/.env\ncc-claw service restart\n```\n\nGet a key at: https://elevenlabs.io/api\n\nSetting ElevenLabs as your provider \u2014 it will activate once the key is added.",
11761
+ "markdown"
11762
+ );
11763
+ }
11764
+ const defaultVoice = provider === "grok" ? "eve" : provider === "macos" ? "Samantha" : "21m00Tcm4TlvDq8ikWAM";
11765
+ setVoiceProvider(chatId, provider, defaultVoice);
11766
+ await sendVoiceConfigKeyboard(chatId, channel);
11767
+ }
11768
+ } else if (action === "v") {
11769
+ const voiceId = parts.slice(1).join(":");
11770
+ const config2 = getVoiceConfig(chatId);
11771
+ setVoiceProvider(chatId, config2.provider, voiceId);
11772
+ const voiceName = config2.provider === "elevenlabs" ? ELEVENLABS_VOICES[voiceId]?.name ?? voiceId : config2.provider === "macos" ? MACOS_VOICES[voiceId]?.name ?? voiceId : voiceId;
11773
+ await channel.sendText(chatId, `\u2705 Voice set to: ${voiceName}`, "plain");
11774
+ }
11350
11775
  } else if (data.startsWith("skills:page:")) {
11351
11776
  const page = parseInt(data.slice(12), 10);
11352
11777
  const skills2 = await discoverAllSkills();
@@ -11605,7 +12030,7 @@ var init_router = __esm({
11605
12030
  pendingFallbackMessages = /* @__PURE__ */ new Map();
11606
12031
  CLI_INSTALL_HINTS = {
11607
12032
  claude: "Install: npm install -g @anthropic-ai/claude-code",
11608
- gemini: "Install: npm install -g @anthropic-ai/gemini-cli",
12033
+ gemini: "Install: npm install -g @google/gemini-cli",
11609
12034
  codex: "Install: npm install -g @openai/codex"
11610
12035
  };
11611
12036
  BLOCKED_PATH_PATTERNS2 = [
@@ -11698,17 +12123,17 @@ var init_router = __esm({
11698
12123
 
11699
12124
  // src/skills/bootstrap.ts
11700
12125
  import { existsSync as existsSync14 } from "fs";
11701
- import { readdir as readdir3, readFile as readFile6, writeFile as writeFile3, copyFile } from "fs/promises";
11702
- import { join as join15, dirname as dirname3 } from "path";
11703
- import { fileURLToPath as fileURLToPath3 } from "url";
12126
+ import { readdir as readdir3, readFile as readFile6, writeFile as writeFile4, copyFile } from "fs/promises";
12127
+ import { join as join14, dirname as dirname2 } from "path";
12128
+ import { fileURLToPath as fileURLToPath2 } from "url";
11704
12129
  async function copyAgentManifestSkills() {
11705
12130
  if (!existsSync14(PKG_SKILLS)) return;
11706
12131
  try {
11707
12132
  const entries = await readdir3(PKG_SKILLS, { withFileTypes: true });
11708
12133
  for (const entry of entries) {
11709
12134
  if (!entry.isFile() || !entry.name.startsWith("agent-") || !entry.name.endsWith(".md")) continue;
11710
- const src = join15(PKG_SKILLS, entry.name);
11711
- const dest = join15(SKILLS_PATH, entry.name);
12135
+ const src = join14(PKG_SKILLS, entry.name);
12136
+ const dest = join14(SKILLS_PATH, entry.name);
11712
12137
  if (existsSync14(dest)) continue;
11713
12138
  await copyFile(src, dest);
11714
12139
  log(`[skills] Bootstrapped ${entry.name} to ${SKILLS_PATH}`);
@@ -11719,7 +12144,7 @@ async function copyAgentManifestSkills() {
11719
12144
  }
11720
12145
  async function bootstrapSkills() {
11721
12146
  await copyAgentManifestSkills();
11722
- const usmDir = join15(SKILLS_PATH, USM_DIR_NAME);
12147
+ const usmDir = join14(SKILLS_PATH, USM_DIR_NAME);
11723
12148
  if (existsSync14(usmDir)) return;
11724
12149
  try {
11725
12150
  const entries = await readdir3(SKILLS_PATH);
@@ -11742,7 +12167,7 @@ async function bootstrapSkills() {
11742
12167
  }
11743
12168
  }
11744
12169
  async function patchUsmForCcClaw(usmDir) {
11745
- const skillPath = join15(usmDir, "SKILL.md");
12170
+ const skillPath = join14(usmDir, "SKILL.md");
11746
12171
  if (!existsSync14(skillPath)) return;
11747
12172
  try {
11748
12173
  let content = await readFile6(skillPath, "utf-8");
@@ -11771,7 +12196,7 @@ async function patchUsmForCcClaw(usmDir) {
11771
12196
  }
11772
12197
  }
11773
12198
  if (patched) {
11774
- await writeFile3(skillPath, content, "utf-8");
12199
+ await writeFile4(skillPath, content, "utf-8");
11775
12200
  log("[skills] Patched USM SKILL.md with CC-Claw support");
11776
12201
  }
11777
12202
  } catch (err) {
@@ -11788,8 +12213,8 @@ var init_bootstrap = __esm({
11788
12213
  USM_REPO = "jacob-bd/universal-skills-manager";
11789
12214
  USM_DIR_NAME = "universal-skills-manager";
11790
12215
  CC_CLAW_ECOSYSTEM_PATCH = `| **CC-Claw** | \`~/.cc-claw/workspace/skills/\` | N/A (daemon, no project scope) |`;
11791
- PKG_ROOT = join15(dirname3(fileURLToPath3(import.meta.url)), "..", "..");
11792
- PKG_SKILLS = join15(PKG_ROOT, "skills");
12216
+ PKG_ROOT = join14(dirname2(fileURLToPath2(import.meta.url)), "..", "..");
12217
+ PKG_SKILLS = join14(PKG_ROOT, "skills");
11793
12218
  }
11794
12219
  });
11795
12220
 
@@ -12003,9 +12428,9 @@ __export(ai_skill_exports, {
12003
12428
  generateAiSkill: () => generateAiSkill,
12004
12429
  installAiSkill: () => installAiSkill
12005
12430
  });
12006
- import { existsSync as existsSync15, writeFileSync as writeFileSync6, mkdirSync as mkdirSync6 } from "fs";
12007
- import { join as join16 } from "path";
12008
- import { homedir as homedir4 } from "os";
12431
+ import { existsSync as existsSync15, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6 } from "fs";
12432
+ import { join as join15 } from "path";
12433
+ import { homedir as homedir5 } from "os";
12009
12434
  function generateAiSkill() {
12010
12435
  const version = VERSION;
12011
12436
  let systemState = "";
@@ -12305,11 +12730,11 @@ function installAiSkill() {
12305
12730
  const failed = [];
12306
12731
  for (const [backend2, dirs] of Object.entries(BACKEND_SKILL_DIRS2)) {
12307
12732
  for (const dir of dirs) {
12308
- const skillDir = join16(dir, "cc-claw-cli");
12309
- const skillPath = join16(skillDir, "SKILL.md");
12733
+ const skillDir = join15(dir, "cc-claw-cli");
12734
+ const skillPath = join15(skillDir, "SKILL.md");
12310
12735
  try {
12311
12736
  mkdirSync6(skillDir, { recursive: true });
12312
- writeFileSync6(skillPath, skill, "utf-8");
12737
+ writeFileSync5(skillPath, skill, "utf-8");
12313
12738
  installed.push(skillPath);
12314
12739
  } catch {
12315
12740
  failed.push(skillPath);
@@ -12325,10 +12750,10 @@ var init_ai_skill = __esm({
12325
12750
  init_paths();
12326
12751
  init_version();
12327
12752
  BACKEND_SKILL_DIRS2 = {
12328
- "cc-claw": [join16(homedir4(), ".cc-claw", "workspace", "skills")],
12329
- claude: [join16(homedir4(), ".claude", "skills")],
12330
- gemini: [join16(homedir4(), ".gemini", "skills")],
12331
- codex: [join16(homedir4(), ".agents", "skills")]
12753
+ "cc-claw": [join15(homedir5(), ".cc-claw", "workspace", "skills")],
12754
+ claude: [join15(homedir5(), ".claude", "skills")],
12755
+ gemini: [join15(homedir5(), ".gemini", "skills")],
12756
+ codex: [join15(homedir5(), ".agents", "skills")]
12332
12757
  };
12333
12758
  }
12334
12759
  });
@@ -12339,17 +12764,17 @@ __export(index_exports, {
12339
12764
  main: () => main
12340
12765
  });
12341
12766
  import { mkdirSync as mkdirSync7, existsSync as existsSync16, renameSync, statSync as statSync2, readFileSync as readFileSync10 } from "fs";
12342
- import { join as join17 } from "path";
12767
+ import { join as join16 } from "path";
12343
12768
  import dotenv from "dotenv";
12344
12769
  function migrateLayout() {
12345
12770
  const moves = [
12346
- [join17(CC_CLAW_HOME, "cc-claw.db"), join17(DATA_PATH, "cc-claw.db")],
12347
- [join17(CC_CLAW_HOME, "cc-claw.db-shm"), join17(DATA_PATH, "cc-claw.db-shm")],
12348
- [join17(CC_CLAW_HOME, "cc-claw.db-wal"), join17(DATA_PATH, "cc-claw.db-wal")],
12349
- [join17(CC_CLAW_HOME, "cc-claw.log"), join17(LOGS_PATH, "cc-claw.log")],
12350
- [join17(CC_CLAW_HOME, "cc-claw.log.1"), join17(LOGS_PATH, "cc-claw.log.1")],
12351
- [join17(CC_CLAW_HOME, "cc-claw.error.log"), join17(LOGS_PATH, "cc-claw.error.log")],
12352
- [join17(CC_CLAW_HOME, "cc-claw.error.log.1"), join17(LOGS_PATH, "cc-claw.error.log.1")]
12771
+ [join16(CC_CLAW_HOME, "cc-claw.db"), join16(DATA_PATH, "cc-claw.db")],
12772
+ [join16(CC_CLAW_HOME, "cc-claw.db-shm"), join16(DATA_PATH, "cc-claw.db-shm")],
12773
+ [join16(CC_CLAW_HOME, "cc-claw.db-wal"), join16(DATA_PATH, "cc-claw.db-wal")],
12774
+ [join16(CC_CLAW_HOME, "cc-claw.log"), join16(LOGS_PATH, "cc-claw.log")],
12775
+ [join16(CC_CLAW_HOME, "cc-claw.log.1"), join16(LOGS_PATH, "cc-claw.log.1")],
12776
+ [join16(CC_CLAW_HOME, "cc-claw.error.log"), join16(LOGS_PATH, "cc-claw.error.log")],
12777
+ [join16(CC_CLAW_HOME, "cc-claw.error.log.1"), join16(LOGS_PATH, "cc-claw.error.log.1")]
12353
12778
  ];
12354
12779
  for (const [from, to] of moves) {
12355
12780
  if (existsSync16(from) && !existsSync16(to)) {
@@ -12454,11 +12879,11 @@ async function main() {
12454
12879
  bootstrapSkills().catch((err) => error("[cc-claw] Skill bootstrap failed:", err));
12455
12880
  try {
12456
12881
  const { generateAiSkill: generateAiSkill2 } = await Promise.resolve().then(() => (init_ai_skill(), ai_skill_exports));
12457
- const { writeFileSync: writeFileSync9, mkdirSync: mkdirSync11 } = await import("fs");
12458
- const { join: join20 } = await import("path");
12459
- const skillDir = join20(SKILLS_PATH, "cc-claw-cli");
12882
+ const { writeFileSync: writeFileSync8, mkdirSync: mkdirSync11 } = await import("fs");
12883
+ const { join: join19 } = await import("path");
12884
+ const skillDir = join19(SKILLS_PATH, "cc-claw-cli");
12460
12885
  mkdirSync11(skillDir, { recursive: true });
12461
- writeFileSync9(join20(skillDir, "SKILL.md"), generateAiSkill2(), "utf-8");
12886
+ writeFileSync8(join19(skillDir, "SKILL.md"), generateAiSkill2(), "utf-8");
12462
12887
  log("[cc-claw] AI skill updated");
12463
12888
  } catch {
12464
12889
  }
@@ -12484,7 +12909,13 @@ async function main() {
12484
12909
  stopMonitor();
12485
12910
  shutdownOrchestrator();
12486
12911
  shutdownScheduler();
12487
- await summarizeAllPending();
12912
+ await Promise.race([
12913
+ summarizeAllPending(),
12914
+ new Promise((resolve) => setTimeout(() => {
12915
+ log("[cc-claw] Summarization timed out after 15s, skipping.");
12916
+ resolve();
12917
+ }, 15e3))
12918
+ ]);
12488
12919
  await channelRegistry.stopAll();
12489
12920
  log("[cc-claw] Clean shutdown complete.");
12490
12921
  } catch (err) {
@@ -12542,10 +12973,10 @@ __export(service_exports, {
12542
12973
  serviceStatus: () => serviceStatus,
12543
12974
  uninstallService: () => uninstallService
12544
12975
  });
12545
- import { existsSync as existsSync17, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7, unlinkSync as unlinkSync2 } from "fs";
12976
+ import { existsSync as existsSync17, mkdirSync as mkdirSync8, writeFileSync as writeFileSync6, unlinkSync as unlinkSync2 } from "fs";
12546
12977
  import { execFileSync as execFileSync2, execSync as execSync5 } from "child_process";
12547
- import { homedir as homedir5, platform } from "os";
12548
- import { join as join18, dirname as dirname4 } from "path";
12978
+ import { homedir as homedir6, platform } from "os";
12979
+ import { join as join17, dirname as dirname3 } from "path";
12549
12980
  function resolveExecutable2(name) {
12550
12981
  try {
12551
12982
  return execFileSync2("which", [name], { encoding: "utf-8" }).trim();
@@ -12554,18 +12985,18 @@ function resolveExecutable2(name) {
12554
12985
  }
12555
12986
  }
12556
12987
  function getPathDirs() {
12557
- const nodeBin = dirname4(process.execPath);
12558
- const home = homedir5();
12988
+ const nodeBin = dirname3(process.execPath);
12989
+ const home = homedir6();
12559
12990
  const dirs = /* @__PURE__ */ new Set([
12560
12991
  nodeBin,
12561
- join18(home, ".local", "bin"),
12992
+ join17(home, ".local", "bin"),
12562
12993
  "/usr/local/bin",
12563
12994
  "/usr/bin",
12564
12995
  "/bin"
12565
12996
  ]);
12566
12997
  try {
12567
12998
  const prefix = execSync5("npm config get prefix", { encoding: "utf-8" }).trim();
12568
- if (prefix) dirs.add(join18(prefix, "bin"));
12999
+ if (prefix) dirs.add(join17(prefix, "bin"));
12569
13000
  } catch {
12570
13001
  }
12571
13002
  return [...dirs].join(":");
@@ -12573,7 +13004,7 @@ function getPathDirs() {
12573
13004
  function generatePlist() {
12574
13005
  const ccClawBin = resolveExecutable2("cc-claw");
12575
13006
  const pathDirs = getPathDirs();
12576
- const home = homedir5();
13007
+ const home = homedir6();
12577
13008
  return `<?xml version="1.0" encoding="UTF-8"?>
12578
13009
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
12579
13010
  <plist version="1.0">
@@ -12619,7 +13050,7 @@ function generatePlist() {
12619
13050
  </plist>`;
12620
13051
  }
12621
13052
  function installMacOS() {
12622
- const agentsDir = dirname4(PLIST_PATH);
13053
+ const agentsDir = dirname3(PLIST_PATH);
12623
13054
  if (!existsSync17(agentsDir)) mkdirSync8(agentsDir, { recursive: true });
12624
13055
  if (!existsSync17(LOGS_PATH)) mkdirSync8(LOGS_PATH, { recursive: true });
12625
13056
  if (existsSync17(PLIST_PATH)) {
@@ -12628,7 +13059,7 @@ function installMacOS() {
12628
13059
  } catch {
12629
13060
  }
12630
13061
  }
12631
- writeFileSync7(PLIST_PATH, generatePlist());
13062
+ writeFileSync6(PLIST_PATH, generatePlist());
12632
13063
  console.log(` Installed: ${PLIST_PATH}`);
12633
13064
  execFileSync2("launchctl", ["load", PLIST_PATH]);
12634
13065
  console.log(" Service loaded and starting.");
@@ -12678,7 +13109,7 @@ Restart=on-failure
12678
13109
  RestartSec=10
12679
13110
  WorkingDirectory=${CC_CLAW_HOME}
12680
13111
  Environment=PATH=${pathDirs}
12681
- Environment=HOME=${homedir5()}
13112
+ Environment=HOME=${homedir6()}
12682
13113
 
12683
13114
  [Install]
12684
13115
  WantedBy=default.target
@@ -12687,7 +13118,7 @@ WantedBy=default.target
12687
13118
  function installLinux() {
12688
13119
  if (!existsSync17(SYSTEMD_DIR)) mkdirSync8(SYSTEMD_DIR, { recursive: true });
12689
13120
  if (!existsSync17(LOGS_PATH)) mkdirSync8(LOGS_PATH, { recursive: true });
12690
- writeFileSync7(UNIT_PATH, generateUnit());
13121
+ writeFileSync6(UNIT_PATH, generateUnit());
12691
13122
  console.log(` Installed: ${UNIT_PATH}`);
12692
13123
  execFileSync2("systemctl", ["--user", "daemon-reload"]);
12693
13124
  execFileSync2("systemctl", ["--user", "enable", "cc-claw"]);
@@ -12720,7 +13151,7 @@ function statusLinux() {
12720
13151
  }
12721
13152
  }
12722
13153
  function installService() {
12723
- if (!existsSync17(join18(CC_CLAW_HOME, ".env"))) {
13154
+ if (!existsSync17(join17(CC_CLAW_HOME, ".env"))) {
12724
13155
  console.error(` Config not found at ${CC_CLAW_HOME}/.env`);
12725
13156
  console.error(" Run 'cc-claw setup' before installing the service.");
12726
13157
  process.exitCode = 1;
@@ -12749,9 +13180,9 @@ var init_service = __esm({
12749
13180
  "use strict";
12750
13181
  init_paths();
12751
13182
  PLIST_LABEL = "com.cc-claw";
12752
- PLIST_PATH = join18(homedir5(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
12753
- SYSTEMD_DIR = join18(homedir5(), ".config", "systemd", "user");
12754
- UNIT_PATH = join18(SYSTEMD_DIR, "cc-claw.service");
13183
+ PLIST_PATH = join17(homedir6(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
13184
+ SYSTEMD_DIR = join17(homedir6(), ".config", "systemd", "user");
13185
+ UNIT_PATH = join17(SYSTEMD_DIR, "cc-claw.service");
12755
13186
  }
12756
13187
  });
12757
13188
 
@@ -12975,7 +13406,7 @@ async function apiGet(path) {
12975
13406
  const req = httpRequest(url, {
12976
13407
  method: "GET",
12977
13408
  headers: token ? { "Authorization": `Bearer ${token}` } : {},
12978
- timeout: 1e4
13409
+ timeout: 3e3
12979
13410
  }, (res) => {
12980
13411
  const chunks = [];
12981
13412
  res.on("data", (c) => chunks.push(c));
@@ -13412,15 +13843,14 @@ async function logsCommand(opts) {
13412
13843
  console.log(tailLines.join("\n"));
13413
13844
  if (opts.follow) {
13414
13845
  console.log(muted("\n Following... (Ctrl+C to stop)\n"));
13415
- let lastSize = Buffer.byteLength(content, "utf-8");
13846
+ let lastLength = content.length;
13416
13847
  watchFile2(logFile, { interval: 500 }, () => {
13417
13848
  try {
13418
13849
  const newContent = readFileSync14(logFile, "utf-8");
13419
- const newSize = Buffer.byteLength(newContent, "utf-8");
13420
- if (newSize > lastSize) {
13421
- const newPart = newContent.slice(content.length);
13850
+ if (newContent.length > lastLength) {
13851
+ const newPart = newContent.slice(lastLength);
13422
13852
  process.stdout.write(newPart);
13423
- lastSize = newSize;
13853
+ lastLength = newContent.length;
13424
13854
  }
13425
13855
  } catch {
13426
13856
  }
@@ -13893,8 +14323,17 @@ async function cronEdit(globalOpts, id, opts) {
13893
14323
  values.push(opts.at);
13894
14324
  }
13895
14325
  if (opts.every) {
13896
- updates.push("every_ms = ?, schedule_type = 'every'");
13897
- values.push(opts.every);
14326
+ const m = opts.every.match(/^(\d+)\s*(m|min|h|hr|s|sec)$/i);
14327
+ if (m) {
14328
+ const num = parseInt(m[1], 10);
14329
+ const unit = m[2].toLowerCase();
14330
+ const ms = unit.startsWith("h") ? num * 36e5 : unit.startsWith("m") ? num * 6e4 : num * 1e3;
14331
+ updates.push("every_ms = ?, schedule_type = 'every'");
14332
+ values.push(ms);
14333
+ } else {
14334
+ updates.push("every_ms = ?, schedule_type = 'every'");
14335
+ values.push(parseInt(opts.every, 10) || 0);
14336
+ }
13898
14337
  }
13899
14338
  if (opts.backend) {
13900
14339
  updates.push("backend = ?");
@@ -14146,7 +14585,7 @@ __export(db_exports, {
14146
14585
  dbStats: () => dbStats
14147
14586
  });
14148
14587
  import { existsSync as existsSync27, statSync as statSync5, copyFileSync, mkdirSync as mkdirSync9 } from "fs";
14149
- import { dirname as dirname5 } from "path";
14588
+ import { dirname as dirname4 } from "path";
14150
14589
  async function dbStats(globalOpts) {
14151
14590
  if (!existsSync27(DB_PATH)) {
14152
14591
  outputError("DB_NOT_FOUND", `Database not found at ${DB_PATH}`);
@@ -14196,7 +14635,7 @@ async function dbBackup(globalOpts, destPath) {
14196
14635
  }
14197
14636
  const dest = destPath ?? `${DB_PATH}.backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
14198
14637
  try {
14199
- mkdirSync9(dirname5(dest), { recursive: true });
14638
+ mkdirSync9(dirname4(dest), { recursive: true });
14200
14639
  copyFileSync(DB_PATH, dest);
14201
14640
  const walPath = DB_PATH + "-wal";
14202
14641
  if (existsSync27(walPath)) copyFileSync(walPath, dest + "-wal");
@@ -14533,7 +14972,7 @@ var init_config = __esm({
14533
14972
  init_format();
14534
14973
  init_paths();
14535
14974
  init_resolve_chat();
14536
- RUNTIME_KEYS = ["backend", "model", "thinking", "summarizer", "mode", "verbose", "cwd", "voice"];
14975
+ RUNTIME_KEYS = ["backend", "model", "thinking", "summarizer", "mode", "verbose", "cwd", "voice", "response-style"];
14537
14976
  KEY_TABLE_MAP = {
14538
14977
  backend: { table: "chat_backend", col: "backend" },
14539
14978
  model: { table: "chat_model", col: "model" },
@@ -14542,7 +14981,8 @@ var init_config = __esm({
14542
14981
  mode: { table: "chat_mode", col: "mode" },
14543
14982
  verbose: { table: "chat_verbose", col: "level" },
14544
14983
  cwd: { table: "chat_cwd", col: "cwd" },
14545
- voice: { table: "chat_voice", col: "enabled" }
14984
+ voice: { table: "chat_voice", col: "enabled" },
14985
+ "response-style": { table: "chat_response_style", col: "style" }
14546
14986
  };
14547
14987
  ALLOWED_TABLES = new Set(Object.values(KEY_TABLE_MAP).map((v) => v.table));
14548
14988
  ALLOWED_COLS = new Set(Object.values(KEY_TABLE_MAP).map((v) => v.col));
@@ -15567,7 +16007,7 @@ var init_completion = __esm({
15567
16007
  model: ["list", "get", "set"],
15568
16008
  thinking: ["get", "set"],
15569
16009
  summarizer: ["get", "set"],
15570
- memory: ["list", "search", "add", "forget", "history"],
16010
+ memory: ["list", "search", "history"],
15571
16011
  cron: ["list", "create", "edit", "cancel", "pause", "resume", "run", "runs", "health"],
15572
16012
  schedule: ["list", "create", "edit", "cancel", "pause", "resume", "run", "runs", "health"],
15573
16013
  agents: ["list", "spawn", "cancel", "cancel-all"],
@@ -15585,7 +16025,7 @@ var init_completion = __esm({
15585
16025
  chats: ["list", "alias", "remove-alias"],
15586
16026
  skills: ["list", "install"],
15587
16027
  mcps: ["list"],
15588
- db: ["stats", "path", "backup", "prune"],
16028
+ db: ["stats", "path", "backup"],
15589
16029
  chat: ["send", "stop"]
15590
16030
  };
15591
16031
  }
@@ -15593,10 +16033,10 @@ var init_completion = __esm({
15593
16033
 
15594
16034
  // src/setup.ts
15595
16035
  var setup_exports = {};
15596
- import { existsSync as existsSync38, writeFileSync as writeFileSync8, readFileSync as readFileSync17, copyFileSync as copyFileSync2, mkdirSync as mkdirSync10, statSync as statSync6 } from "fs";
16036
+ import { existsSync as existsSync38, writeFileSync as writeFileSync7, readFileSync as readFileSync17, copyFileSync as copyFileSync2, mkdirSync as mkdirSync10, statSync as statSync6 } from "fs";
15597
16037
  import { execFileSync as execFileSync4 } from "child_process";
15598
16038
  import { createInterface as createInterface5 } from "readline";
15599
- import { join as join19 } from "path";
16039
+ import { join as join18 } from "path";
15600
16040
  function divider2() {
15601
16041
  console.log(dim("\u2500".repeat(55)));
15602
16042
  }
@@ -15683,7 +16123,7 @@ async function setup() {
15683
16123
  if (match) env[match[1].trim()] = match[2].trim();
15684
16124
  }
15685
16125
  }
15686
- const cwdDb = join19(process.cwd(), "cc-claw.db");
16126
+ const cwdDb = join18(process.cwd(), "cc-claw.db");
15687
16127
  if (existsSync38(cwdDb) && !existsSync38(DB_PATH)) {
15688
16128
  const { size } = statSync6(cwdDb);
15689
16129
  console.log(yellow(` Found existing database at ${cwdDb} (${(size / 1024).toFixed(0)}KB)`));
@@ -15827,11 +16267,27 @@ async function setup() {
15827
16267
  env.GROQ_API_KEY = groqKey;
15828
16268
  console.log(green(" Voice transcription (STT) enabled!"));
15829
16269
  console.log("");
15830
- if (await confirm("Enable voice replies? (requires ElevenLabs API key)", false)) {
16270
+ console.log(dim(" Choose a voice reply provider:"));
16271
+ console.log(" 1. ElevenLabs (high-quality, requires API key)");
16272
+ console.log(" 2. Grok / xAI (high-quality, requires API key)");
16273
+ console.log(" 3. macOS (free, uses built-in voices \u2014 Samantha, Albert)");
16274
+ console.log(" 4. Skip voice replies");
16275
+ console.log("");
16276
+ const ttsChoice = await requiredInput("Enter choice (1/2/3/4)", "4");
16277
+ if (ttsChoice === "1") {
15831
16278
  console.log(dim(" Get an ElevenLabs key at: https://elevenlabs.io/api\n"));
15832
16279
  const elevenKey = await requiredInput("ElevenLabs API key", env.ELEVENLABS_API_KEY);
15833
16280
  env.ELEVENLABS_API_KEY = elevenKey;
15834
- console.log(green(" Voice replies (TTS) enabled!"));
16281
+ console.log(green(" Voice replies via ElevenLabs enabled!"));
16282
+ } else if (ttsChoice === "2") {
16283
+ console.log(dim(" Get an xAI key at: https://console.x.ai/team/default/api-keys\n"));
16284
+ const xaiKey = await requiredInput("xAI API key", env.XAI_API_KEY);
16285
+ env.XAI_API_KEY = xaiKey;
16286
+ console.log(green(" Voice replies via Grok enabled!"));
16287
+ console.log(dim(" Use /voice_config in Telegram to select a voice (Eve, Ara, Rex, Sal, Leo)."));
16288
+ } else if (ttsChoice === "3") {
16289
+ console.log(green(" Voice replies via macOS enabled!"));
16290
+ console.log(dim(" Use /voice_config in Telegram to select a voice (Samantha or Albert)."));
15835
16291
  } else {
15836
16292
  console.log(dim(" Voice replies will use macOS text-to-speech as fallback."));
15837
16293
  }
@@ -15870,6 +16326,9 @@ async function setup() {
15870
16326
  if (env.ELEVENLABS_API_KEY) {
15871
16327
  envLines.push(`ELEVENLABS_API_KEY=${env.ELEVENLABS_API_KEY}`);
15872
16328
  }
16329
+ if (env.XAI_API_KEY) {
16330
+ envLines.push(`XAI_API_KEY=${env.XAI_API_KEY}`);
16331
+ }
15873
16332
  }
15874
16333
  if (env.DASHBOARD_ENABLED) {
15875
16334
  envLines.push("", "# Dashboard", `DASHBOARD_ENABLED=${env.DASHBOARD_ENABLED}`);
@@ -15878,7 +16337,7 @@ async function setup() {
15878
16337
  envLines.push("", "# Video Analysis", `GEMINI_API_KEY=${env.GEMINI_API_KEY}`);
15879
16338
  }
15880
16339
  const envContent = envLines.join("\n") + "\n";
15881
- writeFileSync8(ENV_PATH, envContent, { mode: 384 });
16340
+ writeFileSync7(ENV_PATH, envContent, { mode: 384 });
15882
16341
  console.log(green(` Config saved to ${ENV_PATH} (permissions: owner-only)`));
15883
16342
  header(6, TOTAL_STEPS, "Run on Startup (Daemon)");
15884
16343
  console.log(" CC-Claw can run automatically in the background, starting");
@@ -16161,6 +16620,18 @@ config.command("set <key> <value>").description("Set a runtime config value (via
16161
16620
  const { configSet: configSet2 } = await Promise.resolve().then(() => (init_config(), config_exports));
16162
16621
  await configSet2(program.opts(), key, value);
16163
16622
  });
16623
+ config.command("response-style [style]").description("Get or set the AI response style (concise/normal/detailed)").action(async (style) => {
16624
+ const { configGet: configGet2, configSet: configSet2 } = await Promise.resolve().then(() => (init_config(), config_exports));
16625
+ if (style) {
16626
+ if (!["concise", "normal", "detailed"].includes(style)) {
16627
+ console.error("Invalid style. Must be concise, normal, or detailed.");
16628
+ process.exit(1);
16629
+ }
16630
+ await configSet2(program.opts(), "response-style", style);
16631
+ } else {
16632
+ await configGet2(program.opts(), "response-style");
16633
+ }
16634
+ });
16164
16635
  config.command("env").description("Print .env path and values (static config, redacted secrets)").action(async () => {
16165
16636
  const { configEnv: configEnv2 } = await Promise.resolve().then(() => (init_config(), config_exports));
16166
16637
  await configEnv2(program.opts());