cc-claw 0.2.8 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +556 -55
  2. 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.2.8" : (() => {
51
+ VERSION = true ? "0.3.1" : (() => {
52
52
  try {
53
53
  return JSON.parse(readFileSync(join2(process.cwd(), "package.json"), "utf-8")).version ?? "unknown";
54
54
  } catch {
@@ -864,10 +864,13 @@ __export(store_exports2, {
864
864
  clearThinkingLevel: () => clearThinkingLevel,
865
865
  clearUsage: () => clearUsage,
866
866
  completeJobRun: () => completeJobRun,
867
+ deleteBookmark: () => deleteBookmark,
868
+ findBookmarksByPrefix: () => findBookmarksByPrefix,
867
869
  forgetMemory: () => forgetMemory,
868
870
  getActiveJobs: () => getActiveJobs,
869
871
  getActiveWatches: () => getActiveWatches,
870
872
  getAllBackendLimits: () => getAllBackendLimits,
873
+ getAllBookmarks: () => getAllBookmarks,
871
874
  getAllChatAliases: () => getAllChatAliases,
872
875
  getAllJobs: () => getAllJobs,
873
876
  getAllMemoriesWithEmbeddings: () => getAllMemoriesWithEmbeddings,
@@ -876,6 +879,7 @@ __export(store_exports2, {
876
879
  getBackend: () => getBackend,
877
880
  getBackendLimit: () => getBackendLimit,
878
881
  getBackendUsageInWindow: () => getBackendUsageInWindow,
882
+ getBookmark: () => getBookmark,
879
883
  getChatIdByAlias: () => getChatIdByAlias,
880
884
  getChatUsageByModel: () => getChatUsageByModel,
881
885
  getCwd: () => getCwd,
@@ -887,6 +891,7 @@ __export(store_exports2, {
887
891
  getMemoriesWithoutEmbeddings: () => getMemoriesWithoutEmbeddings,
888
892
  getMode: () => getMode,
889
893
  getModel: () => getModel,
894
+ getRecentBookmarks: () => getRecentBookmarks,
890
895
  getRecentMemories: () => getRecentMemories,
891
896
  getSessionId: () => getSessionId,
892
897
  getSessionStartedAt: () => getSessionStartedAt,
@@ -929,13 +934,15 @@ __export(store_exports2, {
929
934
  setThinkingLevel: () => setThinkingLevel,
930
935
  setVerboseLevel: () => setVerboseLevel,
931
936
  toggleTool: () => toggleTool,
937
+ touchBookmark: () => touchBookmark,
932
938
  updateHeartbeatTimestamps: () => updateHeartbeatTimestamps,
933
939
  updateJob: () => updateJob,
934
940
  updateJobEnabled: () => updateJobEnabled,
935
941
  updateJobLastRun: () => updateJobLastRun,
936
942
  updateJobNextRun: () => updateJobNextRun,
937
943
  updateMemoryEmbedding: () => updateMemoryEmbedding,
938
- updateSessionSummaryEmbedding: () => updateSessionSummaryEmbedding
944
+ updateSessionSummaryEmbedding: () => updateSessionSummaryEmbedding,
945
+ upsertBookmark: () => upsertBookmark
939
946
  });
940
947
  import Database from "better-sqlite3";
941
948
  function openDatabaseReadOnly() {
@@ -1297,6 +1304,16 @@ function initDatabase() {
1297
1304
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
1298
1305
  );
1299
1306
  `);
1307
+ db.exec(`
1308
+ CREATE TABLE IF NOT EXISTS cwd_bookmarks (
1309
+ chatId TEXT NOT NULL,
1310
+ alias TEXT NOT NULL,
1311
+ path TEXT NOT NULL,
1312
+ manual INTEGER NOT NULL DEFAULT 0,
1313
+ lastUsed INTEGER NOT NULL DEFAULT 0,
1314
+ PRIMARY KEY (chatId, alias)
1315
+ )
1316
+ `);
1300
1317
  try {
1301
1318
  db.exec("ALTER TABLE memories ADD COLUMN embedding TEXT");
1302
1319
  } catch {
@@ -1491,6 +1508,58 @@ function setCwd(chatId, cwd) {
1491
1508
  function clearCwd(chatId) {
1492
1509
  db.prepare("DELETE FROM chat_cwd WHERE chat_id = ?").run(chatId);
1493
1510
  }
1511
+ function upsertBookmark(chatId, alias, path, manual) {
1512
+ const now = Date.now();
1513
+ const existing = db.prepare(
1514
+ "SELECT manual FROM cwd_bookmarks WHERE chatId = ? AND alias = ?"
1515
+ ).get(chatId, alias);
1516
+ if (existing) {
1517
+ if (existing.manual === 1 && !manual) {
1518
+ return;
1519
+ }
1520
+ db.prepare(
1521
+ "UPDATE cwd_bookmarks SET path = ?, manual = ?, lastUsed = ? WHERE chatId = ? AND alias = ?"
1522
+ ).run(path, manual ? 1 : 0, now, chatId, alias);
1523
+ } else {
1524
+ db.prepare(
1525
+ "INSERT INTO cwd_bookmarks (chatId, alias, path, manual, lastUsed) VALUES (?, ?, ?, ?, ?)"
1526
+ ).run(chatId, alias, path, manual ? 1 : 0, now);
1527
+ }
1528
+ }
1529
+ function touchBookmark(chatId, alias) {
1530
+ db.prepare(
1531
+ "UPDATE cwd_bookmarks SET lastUsed = ? WHERE chatId = ? AND alias = ?"
1532
+ ).run(Date.now(), chatId, alias);
1533
+ }
1534
+ function getBookmark(chatId, alias) {
1535
+ const row = db.prepare(
1536
+ "SELECT alias, path, manual FROM cwd_bookmarks WHERE chatId = ? AND alias = ?"
1537
+ ).get(chatId, alias);
1538
+ return row ? { alias: row.alias, path: row.path, manual: row.manual === 1 } : void 0;
1539
+ }
1540
+ function getRecentBookmarks(chatId, limit = 10) {
1541
+ return db.prepare(
1542
+ "SELECT alias, path FROM cwd_bookmarks WHERE chatId = ? ORDER BY lastUsed DESC LIMIT ?"
1543
+ ).all(chatId, limit);
1544
+ }
1545
+ function findBookmarksByPrefix(chatId, prefix) {
1546
+ return db.prepare(
1547
+ "SELECT alias, path FROM cwd_bookmarks WHERE chatId = ? AND alias LIKE ? || '%'"
1548
+ ).all(chatId, prefix);
1549
+ }
1550
+ function deleteBookmark(chatId, alias) {
1551
+ const result = db.prepare(
1552
+ "DELETE FROM cwd_bookmarks WHERE chatId = ? AND alias = ?"
1553
+ ).run(chatId, alias);
1554
+ return result.changes > 0;
1555
+ }
1556
+ function getAllBookmarks(chatId) {
1557
+ return db.prepare(
1558
+ "SELECT alias, path, manual FROM cwd_bookmarks WHERE chatId = ? ORDER BY alias"
1559
+ ).all(chatId).map(
1560
+ (r) => ({ alias: r.alias, path: r.path, manual: r.manual === 1 })
1561
+ );
1562
+ }
1494
1563
  function getModel(chatId) {
1495
1564
  const row = db.prepare(
1496
1565
  "SELECT model FROM chat_model WHERE chat_id = ?"
@@ -2199,13 +2268,13 @@ var init_claude = __esm({
2199
2268
  displayName = "Claude";
2200
2269
  availableModels = {
2201
2270
  "claude-opus-4-6": {
2202
- label: "Opus 4.6 \u2014 most capable",
2271
+ label: "Opus 4.6 \u2014 most capable, 1M context",
2203
2272
  thinking: "adjustable",
2204
2273
  thinkingLevels: ["auto", "off", "low", "medium", "high"],
2205
2274
  defaultThinkingLevel: "medium"
2206
2275
  },
2207
2276
  "claude-sonnet-4-6": {
2208
- label: "Sonnet 4.6 \u2014 balanced (default)",
2277
+ label: "Sonnet 4.6 \u2014 balanced (default), 1M context",
2209
2278
  thinking: "adjustable",
2210
2279
  thinkingLevels: ["auto", "off", "low", "medium", "high"],
2211
2280
  defaultThinkingLevel: "medium"
@@ -2225,8 +2294,8 @@ var init_claude = __esm({
2225
2294
  "claude-haiku-4-5": { in: 0.8, out: 4, cache: 0.08 }
2226
2295
  };
2227
2296
  contextWindow = {
2228
- "claude-opus-4-6": 2e5,
2229
- "claude-sonnet-4-6": 2e5,
2297
+ "claude-opus-4-6": 1e6,
2298
+ "claude-sonnet-4-6": 1e6,
2230
2299
  "claude-haiku-4-5": 2e5
2231
2300
  };
2232
2301
  _resolvedPath = "";
@@ -4035,6 +4104,9 @@ async function startAgent(agentId, chatId, opts) {
4035
4104
  }
4036
4105
  let mcpExtraArgs = [];
4037
4106
  let orchestratorMcpName = "";
4107
+ if (process.env.DASHBOARD_ENABLED !== "1") {
4108
+ warn(`[orchestrator] DASHBOARD_ENABLED is not set \u2014 agent ${agentId.slice(0, 8)} will NOT have orchestrator MCP tools (set_state, send_message, etc.)`);
4109
+ }
4038
4110
  if (process.env.DASHBOARD_ENABLED === "1") {
4039
4111
  try {
4040
4112
  const { createSubAgentToken: createSubAgentToken2 } = await Promise.resolve().then(() => (init_server(), server_exports));
@@ -4054,6 +4126,9 @@ async function startAgent(agentId, chatId, opts) {
4054
4126
  });
4055
4127
  mcpsAdded = [...mcpsAdded, orchestratorMcpName];
4056
4128
  updateAgentMcpsAdded(db3, agentId, mcpsAdded);
4129
+ if (runner.id === "gemini") {
4130
+ mcpExtraArgs = [`--allowed-mcp-server-names=${orchestratorMcpName}`];
4131
+ }
4057
4132
  log(`[orchestrator] Injected cc-claw MCP via add-remove for agent ${agentId.slice(0, 8)}`);
4058
4133
  }
4059
4134
  } catch (err) {
@@ -8278,6 +8353,180 @@ var init_video = __esm({
8278
8353
  }
8279
8354
  });
8280
8355
 
8356
+ // src/shell/guard.ts
8357
+ import { randomUUID as randomUUID2 } from "crypto";
8358
+ function isDestructive(command) {
8359
+ return DESTRUCTIVE_PATTERNS.some((pattern) => pattern.test(command));
8360
+ }
8361
+ function storePendingCommand(command, chatId, raw) {
8362
+ const id = randomUUID2().slice(0, 8);
8363
+ pendingCommands.set(id, { command, chatId, raw, createdAt: Date.now() });
8364
+ setTimeout(() => pendingCommands.delete(id), PENDING_TTL_MS);
8365
+ return id;
8366
+ }
8367
+ function getPendingCommand(id) {
8368
+ return pendingCommands.get(id);
8369
+ }
8370
+ function removePendingCommand(id) {
8371
+ return pendingCommands.delete(id);
8372
+ }
8373
+ var DESTRUCTIVE_PATTERNS, pendingCommands, PENDING_TTL_MS;
8374
+ var init_guard = __esm({
8375
+ "src/shell/guard.ts"() {
8376
+ "use strict";
8377
+ DESTRUCTIVE_PATTERNS = [
8378
+ /\brm\s+(-[a-zA-Z]*[fr][a-zA-Z]*|-[a-zA-Z]*[rf][a-zA-Z]*|--force|--recursive)\b/,
8379
+ /\brm\s+-[a-zA-Z]*\s+\/\s*$/,
8380
+ /\bmkfs\b/,
8381
+ /\bdd\b.*\bof=/,
8382
+ /\b(shutdown|reboot|halt|poweroff)\b/,
8383
+ />\s*\/dev\/sd/,
8384
+ /\bchmod\s+-R\s+777\b/,
8385
+ /\bchown\s+-R\b/,
8386
+ /\bkillall\b/,
8387
+ /\blaunchctl\s+(unload|remove)\b/,
8388
+ /:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;?\s*:/
8389
+ ];
8390
+ pendingCommands = /* @__PURE__ */ new Map();
8391
+ PENDING_TTL_MS = 5 * 60 * 1e3;
8392
+ }
8393
+ });
8394
+
8395
+ // src/shell/exec.ts
8396
+ import { execFile as execFile3 } from "child_process";
8397
+ function executeShell(command, cwd, timeoutMs = SHELL_TIMEOUT_MS) {
8398
+ return new Promise((resolve) => {
8399
+ execFile3(
8400
+ "/bin/zsh",
8401
+ ["-c", command],
8402
+ { cwd, timeout: timeoutMs, maxBuffer: 5 * 1024 * 1024 },
8403
+ (error3, stdout, stderr) => {
8404
+ const output2 = (stdout || "") + (stderr || "");
8405
+ if (error3 && "killed" in error3 && error3.killed) {
8406
+ resolve({
8407
+ output: `Command timed out after ${Math.round(timeoutMs / 1e3)} seconds`,
8408
+ exitCode: 124,
8409
+ timedOut: true
8410
+ });
8411
+ return;
8412
+ }
8413
+ const exitCode = error3 ? typeof error3.code === "number" ? error3.code : 1 : 0;
8414
+ resolve({
8415
+ output: output2,
8416
+ exitCode,
8417
+ timedOut: false
8418
+ });
8419
+ }
8420
+ );
8421
+ });
8422
+ }
8423
+ function formatCodeBlock(command, output2, exitCode) {
8424
+ return `<pre>$ ${command}
8425
+ ${output2}
8426
+ [exit ${exitCode}]</pre>`;
8427
+ }
8428
+ function formatRaw(output2) {
8429
+ return output2.trim();
8430
+ }
8431
+ function shouldSendAsFile(output2) {
8432
+ return output2.length > OUTPUT_MAX_LENGTH;
8433
+ }
8434
+ var SHELL_TIMEOUT_MS, OUTPUT_MAX_LENGTH;
8435
+ var init_exec = __esm({
8436
+ "src/shell/exec.ts"() {
8437
+ "use strict";
8438
+ SHELL_TIMEOUT_MS = 6e4;
8439
+ OUTPUT_MAX_LENGTH = 4e3;
8440
+ }
8441
+ });
8442
+
8443
+ // src/shell/backend-cmd.ts
8444
+ import { execFile as execFile4 } from "child_process";
8445
+ function buildNativeResponse(backendId, command, output2) {
8446
+ const body = output2 || "(no output)";
8447
+ return `<pre>[${backendId}] ${command}
8448
+ ${body}</pre>`;
8449
+ }
8450
+ function extractTextFromNdjson(raw) {
8451
+ const texts = [];
8452
+ for (const line of raw.split("\n")) {
8453
+ if (!line.trim()) continue;
8454
+ try {
8455
+ const obj = JSON.parse(line);
8456
+ if (obj.type === "result" && obj.result) {
8457
+ texts.push(typeof obj.result === "string" ? obj.result : JSON.stringify(obj.result));
8458
+ }
8459
+ if (obj.type === "assistant" && obj.message?.content) {
8460
+ for (const block of obj.message.content) {
8461
+ if (block.type === "text" && block.text) texts.push(block.text);
8462
+ }
8463
+ }
8464
+ if (obj.type === "text" && obj.text) {
8465
+ texts.push(obj.text);
8466
+ }
8467
+ } catch {
8468
+ }
8469
+ }
8470
+ return texts.join("\n").trim();
8471
+ }
8472
+ async function handleBackendCommand(command, chatId, channel) {
8473
+ let adapter;
8474
+ try {
8475
+ adapter = getAdapterForChat(chatId);
8476
+ } catch {
8477
+ await channel.sendText(chatId, "No backend set. Use /backend first.", "plain");
8478
+ return;
8479
+ }
8480
+ const sessionId = getSessionId(chatId);
8481
+ if (!sessionId) {
8482
+ await channel.sendText(chatId, "No active session. Start a conversation first.", "plain");
8483
+ return;
8484
+ }
8485
+ const cwd = getCwd(chatId) ?? `${process.env.HOME ?? "/tmp"}/.cc-claw/workspace`;
8486
+ try {
8487
+ const output2 = await spawnBackendCommand(adapter, command, sessionId, cwd);
8488
+ const formatted = buildNativeResponse(adapter.id, command, output2);
8489
+ await channel.sendText(chatId, formatted, "html");
8490
+ } catch (err) {
8491
+ const msg = err instanceof Error ? err.message : String(err);
8492
+ await channel.sendText(chatId, `Backend command failed: ${msg}`, "plain");
8493
+ }
8494
+ }
8495
+ function spawnBackendCommand(adapter, command, sessionId, cwd) {
8496
+ return new Promise((resolve, reject) => {
8497
+ const config2 = adapter.buildSpawnConfig({
8498
+ prompt: command,
8499
+ sessionId,
8500
+ cwd,
8501
+ permMode: "plan",
8502
+ allowedTools: []
8503
+ });
8504
+ const env = { ...process.env, ...adapter.getEnv() };
8505
+ execFile4(config2.executable, config2.args, {
8506
+ cwd,
8507
+ timeout: BACKEND_CMD_TIMEOUT_MS,
8508
+ env,
8509
+ maxBuffer: 5 * 1024 * 1024
8510
+ }, (error3, stdout, stderr) => {
8511
+ if (error3 && "killed" in error3 && error3.killed) {
8512
+ reject(new Error(`Backend command timed out after ${BACKEND_CMD_TIMEOUT_MS / 1e3} seconds.`));
8513
+ return;
8514
+ }
8515
+ const output2 = extractTextFromNdjson(stdout) || stdout || stderr || "";
8516
+ resolve(output2.trim());
8517
+ });
8518
+ });
8519
+ }
8520
+ var BACKEND_CMD_TIMEOUT_MS;
8521
+ var init_backend_cmd = __esm({
8522
+ "src/shell/backend-cmd.ts"() {
8523
+ "use strict";
8524
+ init_backends();
8525
+ init_store4();
8526
+ BACKEND_CMD_TIMEOUT_MS = 15e3;
8527
+ }
8528
+ });
8529
+
8281
8530
  // src/router.ts
8282
8531
  import { readFile as readFile5, writeFile as writeFile2, unlink as unlink2 } from "fs/promises";
8283
8532
  import { resolve as resolvePath } from "path";
@@ -8367,6 +8616,17 @@ async function handleMessage(msg, channel) {
8367
8616
  }
8368
8617
  }
8369
8618
  }
8619
+ if (msg.type === "text" && msg.text) {
8620
+ const text = msg.text.trim();
8621
+ if (text.startsWith("!!")) {
8622
+ const cmd = text.slice(2).trim();
8623
+ if (cmd) return handleRawShell(cmd, chatId, channel);
8624
+ }
8625
+ if (text.startsWith("!") && text.length > 1) {
8626
+ const cmd = text.slice(1).trim();
8627
+ if (cmd) return handleShell(cmd, chatId, channel);
8628
+ }
8629
+ }
8370
8630
  switch (msg.type) {
8371
8631
  case "command":
8372
8632
  await handleCommand(msg, channel);
@@ -8386,6 +8646,9 @@ async function handleMessage(msg, channel) {
8386
8646
  }
8387
8647
  async function handleCommand(msg, channel) {
8388
8648
  const { chatId, command, commandArgs } = msg;
8649
+ if (command?.startsWith("/")) {
8650
+ return handleBackendCommand(command, chatId, channel);
8651
+ }
8389
8652
  switch (command) {
8390
8653
  case "start":
8391
8654
  case "help":
@@ -8520,15 +8783,7 @@ Tap to toggle:`,
8520
8783
  case "backend": {
8521
8784
  const requestedBackend = (commandArgs ?? "").trim().toLowerCase();
8522
8785
  if (requestedBackend && getAllBackendIds().includes(requestedBackend)) {
8523
- summarizeSession(chatId).catch(() => {
8524
- });
8525
- clearSession(chatId);
8526
- clearModel(chatId);
8527
- clearThinkingLevel(chatId);
8528
- setBackend(chatId, requestedBackend);
8529
- const adapter = getAdapter(requestedBackend);
8530
- logActivity(getDb(), { chatId, source: "telegram", eventType: "config_changed", summary: `Backend switched to ${adapter.displayName}`, detail: { field: "backend", value: requestedBackend } });
8531
- await channel.sendText(chatId, `Switched to <b>${adapter.displayName}</b>.`, "html");
8786
+ await sendBackendSwitchConfirmation(chatId, requestedBackend, channel);
8532
8787
  break;
8533
8788
  }
8534
8789
  const currentBackend = getBackend(chatId);
@@ -8552,15 +8807,7 @@ Tap to toggle:`,
8552
8807
  case "codex": {
8553
8808
  const backendId = command;
8554
8809
  if (getAllBackendIds().includes(backendId)) {
8555
- summarizeSession(chatId).catch(() => {
8556
- });
8557
- clearSession(chatId);
8558
- clearModel(chatId);
8559
- clearThinkingLevel(chatId);
8560
- setBackend(chatId, backendId);
8561
- const adapter = getAdapter(backendId);
8562
- logActivity(getDb(), { chatId, source: "telegram", eventType: "config_changed", summary: `Backend switched to ${adapter.displayName}`, detail: { field: "backend", value: backendId } });
8563
- await channel.sendText(chatId, `Switched to <b>${adapter.displayName}</b>.`, "html");
8810
+ await sendBackendSwitchConfirmation(chatId, backendId, channel);
8564
8811
  } else {
8565
8812
  await channel.sendText(chatId, `Backend "${command}" is not available.`, "plain");
8566
8813
  }
@@ -8733,11 +8980,28 @@ Use /summarizer auto, /summarizer off, or /summarizer <backend>:<model>`, "plain
8733
8980
  case "cwd": {
8734
8981
  if (!commandArgs) {
8735
8982
  const current = getCwd(chatId);
8736
- await channel.sendText(
8737
- chatId,
8738
- current ? `Working directory: ${current}` : "No working directory set. Usage: /cwd ~/projects/my-app",
8739
- "plain"
8740
- );
8983
+ const recents = getRecentBookmarks(chatId, 10);
8984
+ if (recents.length === 0) {
8985
+ await channel.sendText(
8986
+ chatId,
8987
+ current ? `Working directory: ${current}
8988
+
8989
+ No saved bookmarks yet. Set a directory with /cwd <path> to auto-save.` : "No working directory set. Usage: /cwd ~/projects/my-app",
8990
+ "plain"
8991
+ );
8992
+ return;
8993
+ }
8994
+ const text = current ? `Current: ${current}
8995
+
8996
+ Recent directories:` : "Recent directories:";
8997
+ if (typeof channel.sendKeyboard === "function") {
8998
+ const buttons = recents.map((r) => [{ label: r.alias, data: `cwdpick:${r.alias}` }]);
8999
+ await channel.sendKeyboard(chatId, text, buttons);
9000
+ } else {
9001
+ const list = recents.map((r) => ` ${r.alias} \u2192 ${r.path}`).join("\n");
9002
+ await channel.sendText(chatId, `${text}
9003
+ ${list}`, "plain");
9004
+ }
8741
9005
  return;
8742
9006
  }
8743
9007
  if (commandArgs === "reset" || commandArgs === "clear") {
@@ -8745,12 +9009,71 @@ Use /summarizer auto, /summarizer off, or /summarizer <backend>:<model>`, "plain
8745
9009
  await channel.sendText(chatId, "Working directory cleared. Using default.", "plain");
8746
9010
  return;
8747
9011
  }
8748
- const resolvedPath = commandArgs.startsWith("~") ? commandArgs.replace("~", process.env.HOME ?? "") : commandArgs;
8749
- setCwd(chatId, resolvedPath);
8750
- clearSession(chatId);
8751
- logActivity(getDb(), { chatId, source: "telegram", eventType: "config_changed", summary: `Working directory set to ${resolvedPath}`, detail: { field: "cwd", value: resolvedPath } });
8752
- await channel.sendText(chatId, `Working directory set to: ${resolvedPath}
8753
- Session reset for new context.`, "plain");
9012
+ if (commandArgs === "aliases") {
9013
+ const all = getAllBookmarks(chatId);
9014
+ if (all.length === 0) {
9015
+ await channel.sendText(chatId, "No bookmarks saved yet.", "plain");
9016
+ return;
9017
+ }
9018
+ const lines = all.map((b) => ` ${b.manual ? "[manual]" : "[auto]"} ${b.alias} \u2192 ${b.path}`);
9019
+ await channel.sendText(chatId, `Directory bookmarks:
9020
+ ${lines.join("\n")}`, "plain");
9021
+ return;
9022
+ }
9023
+ if (commandArgs.startsWith("unalias ")) {
9024
+ const aliasName = commandArgs.slice(8).trim();
9025
+ if (!aliasName) {
9026
+ await channel.sendText(chatId, "Usage: /cwd unalias <name>", "plain");
9027
+ return;
9028
+ }
9029
+ const deleted = deleteBookmark(chatId, aliasName);
9030
+ await channel.sendText(chatId, deleted ? `Bookmark '${aliasName}' removed.` : `Bookmark '${aliasName}' not found.`, "plain");
9031
+ return;
9032
+ }
9033
+ if (commandArgs.startsWith("alias ")) {
9034
+ const parts = commandArgs.slice(6).trim().split(/\s+/);
9035
+ if (parts.length < 2) {
9036
+ await channel.sendText(chatId, "Usage: /cwd alias <name> <path>", "plain");
9037
+ return;
9038
+ }
9039
+ const [aliasName, ...pathParts] = parts;
9040
+ const aliasPath = pathParts.join(" ").replace(/^~/, process.env.HOME ?? "");
9041
+ upsertBookmark(chatId, aliasName, aliasPath, true);
9042
+ await channel.sendText(chatId, `Bookmark saved: ${aliasName} \u2192 ${aliasPath}`, "plain");
9043
+ return;
9044
+ }
9045
+ const arg = commandArgs;
9046
+ if (arg.startsWith("/") || arg.startsWith("~")) {
9047
+ const resolvedPath = arg.startsWith("~") ? arg.replace("~", process.env.HOME ?? "") : arg;
9048
+ setCwd(chatId, resolvedPath);
9049
+ const basename2 = resolvedPath.split("/").filter(Boolean).pop();
9050
+ if (basename2) upsertBookmark(chatId, basename2, resolvedPath, false);
9051
+ logActivity(getDb(), { chatId, source: "telegram", eventType: "config_changed", summary: `Working directory set to ${resolvedPath}`, detail: { field: "cwd", value: resolvedPath } });
9052
+ await sendCwdSessionChoice(chatId, resolvedPath, channel);
9053
+ return;
9054
+ }
9055
+ const exact = getBookmark(chatId, arg);
9056
+ if (exact) {
9057
+ setCwd(chatId, exact.path);
9058
+ touchBookmark(chatId, arg);
9059
+ logActivity(getDb(), { chatId, source: "telegram", eventType: "config_changed", summary: `Working directory set to ${exact.path}`, detail: { field: "cwd", value: exact.path } });
9060
+ await sendCwdSessionChoice(chatId, exact.path, channel);
9061
+ return;
9062
+ }
9063
+ const matches = findBookmarksByPrefix(chatId, arg);
9064
+ if (matches.length === 1) {
9065
+ setCwd(chatId, matches[0].path);
9066
+ touchBookmark(chatId, matches[0].alias);
9067
+ logActivity(getDb(), { chatId, source: "telegram", eventType: "config_changed", summary: `Working directory set to ${matches[0].path}`, detail: { field: "cwd", value: matches[0].path } });
9068
+ await sendCwdSessionChoice(chatId, matches[0].path, channel);
9069
+ return;
9070
+ }
9071
+ if (matches.length > 1 && typeof channel.sendKeyboard === "function") {
9072
+ const buttons = matches.map((m) => [{ label: `${m.alias} \u2192 ${m.path}`, data: `cwdpick:${m.alias}` }]);
9073
+ await channel.sendKeyboard(chatId, `Multiple matches for "${arg}":`, buttons);
9074
+ return;
9075
+ }
9076
+ await channel.sendText(chatId, `Directory alias '${arg}' not found. Use /cwd aliases to see saved bookmarks.`, "plain");
8754
9077
  break;
8755
9078
  }
8756
9079
  case "memory": {
@@ -9255,7 +9578,7 @@ Use /skills to see it.`, "plain");
9255
9578
  lines.push("", "\u2501\u2501 <b>Built-in</b> \u2501\u2501");
9256
9579
  lines.push(` \u2705 <b>cc-claw</b> <i>Agent orchestrator (spawn, tasks, inbox)</i>`);
9257
9580
  }
9258
- const { execFile: execFile3 } = await import("child_process");
9581
+ const { execFile: execFile5 } = await import("child_process");
9259
9582
  const { homedir: homedir5 } = await import("os");
9260
9583
  const discoveryCwd = homedir5();
9261
9584
  const runnerResults = await Promise.allSettled(
@@ -9264,7 +9587,7 @@ Use /skills to see it.`, "plain");
9264
9587
  if (!listCmd.length) return Promise.resolve({ runner, output: "" });
9265
9588
  const exe = runner.getExecutablePath();
9266
9589
  return new Promise((resolve) => {
9267
- execFile3(exe, listCmd.slice(1), {
9590
+ execFile5(exe, listCmd.slice(1), {
9268
9591
  encoding: "utf-8",
9269
9592
  timeout: 3e4,
9270
9593
  cwd: discoveryCwd,
@@ -9711,31 +10034,65 @@ async function sendResponse(chatId, channel, text) {
9711
10034
  function isImageExt(ext) {
9712
10035
  return ["jpg", "jpeg", "png", "gif", "webp", "bmp", "svg"].includes(ext);
9713
10036
  }
10037
+ async function sendBackendSwitchConfirmation(chatId, target, channel) {
10038
+ const current = getBackend(chatId);
10039
+ const targetAdapter = getAdapter(target);
10040
+ if (current === target) {
10041
+ await channel.sendText(chatId, `Already using ${targetAdapter.displayName}.`, "plain");
10042
+ return;
10043
+ }
10044
+ const currentLabel = current ? getAdapter(current).displayName : "current backend";
10045
+ if (typeof channel.sendKeyboard === "function") {
10046
+ await channel.sendKeyboard(
10047
+ chatId,
10048
+ `\u26A0\uFE0F Switching to ${targetAdapter.displayName} will summarize and reset your current session.
10049
+
10050
+ What would you like to do?`,
10051
+ [
10052
+ [{ label: `Stay on ${currentLabel}`, data: "backend_cancel" }],
10053
+ [{ label: `Switch to ${targetAdapter.displayName} + summarize`, data: `backend_confirm:${target}` }]
10054
+ ]
10055
+ );
10056
+ } else {
10057
+ await doBackendSwitch(chatId, target, channel);
10058
+ }
10059
+ }
10060
+ async function doBackendSwitch(chatId, backendId, channel) {
10061
+ summarizeSession(chatId).catch(() => {
10062
+ });
10063
+ clearSession(chatId);
10064
+ clearModel(chatId);
10065
+ clearThinkingLevel(chatId);
10066
+ setBackend(chatId, backendId);
10067
+ const adapter = getAdapter(backendId);
10068
+ logActivity(getDb(), { chatId, source: "telegram", eventType: "config_changed", summary: `Backend switched to ${adapter.displayName}`, detail: { field: "backend", value: backendId } });
10069
+ await channel.sendText(
10070
+ chatId,
10071
+ `Backend switched to ${adapter.displayName}.
10072
+ Default model: ${adapter.defaultModel}
10073
+ Session reset. Ready!`,
10074
+ "plain"
10075
+ );
10076
+ }
9714
10077
  async function handleCallback(chatId, data, channel) {
9715
10078
  if (data.startsWith("backend:")) {
9716
10079
  const chosen = data.slice(8);
9717
10080
  if (!getAllBackendIds().includes(chosen)) return;
9718
10081
  const previous = getBackend(chatId);
9719
10082
  if (chosen === previous) {
9720
- const adapter2 = getAdapter(chosen);
9721
- await channel.sendText(chatId, `Already using ${adapter2.displayName}.`, "plain");
10083
+ const adapter = getAdapter(chosen);
10084
+ await channel.sendText(chatId, `Already using ${adapter.displayName}.`, "plain");
9722
10085
  return;
9723
10086
  }
9724
- summarizeSession(chatId).catch(() => {
9725
- });
9726
- clearSession(chatId);
9727
- clearModel(chatId);
9728
- clearThinkingLevel(chatId);
9729
- setBackend(chatId, chosen);
9730
- const adapter = getAdapter(chosen);
9731
- logActivity(getDb(), { chatId, source: "telegram", eventType: "config_changed", summary: `Backend switched to ${adapter.displayName}`, detail: { field: "backend", value: chosen } });
9732
- await channel.sendText(
9733
- chatId,
9734
- `Backend switched to ${adapter.displayName}.
9735
- Default model: ${adapter.defaultModel}
9736
- Session reset. Ready!`,
9737
- "plain"
9738
- );
10087
+ await sendBackendSwitchConfirmation(chatId, chosen, channel);
10088
+ } else if (data.startsWith("backend_confirm:")) {
10089
+ const chosen = data.slice(16);
10090
+ if (!getAllBackendIds().includes(chosen)) return;
10091
+ await doBackendSwitch(chatId, chosen, channel);
10092
+ } else if (data === "backend_cancel") {
10093
+ const current = getBackend(chatId);
10094
+ const label2 = current ? getAdapter(current).displayName : "current backend";
10095
+ await channel.sendText(chatId, `No change. Staying on ${label2}.`, "plain");
9739
10096
  } else if (data.startsWith("model:")) {
9740
10097
  const chosen = data.slice(6);
9741
10098
  let adapter;
@@ -9834,6 +10191,51 @@ ${PERM_MODES[chosen]}`,
9834
10191
  } else {
9835
10192
  await channel.sendText(chatId, "Preference not saved.", "plain");
9836
10193
  }
10194
+ } else if (data.startsWith("shell:")) {
10195
+ const parts = data.split(":");
10196
+ const action = parts[1];
10197
+ const id = parts[2];
10198
+ if (action === "confirm") {
10199
+ const pending = getPendingCommand(id);
10200
+ if (!pending) {
10201
+ await channel.sendText(chatId, "Confirmation expired. Please re-send the command.", "plain");
10202
+ return;
10203
+ }
10204
+ removePendingCommand(id);
10205
+ if (pending.raw) {
10206
+ await handleRawShell(pending.command, pending.chatId, channel, true);
10207
+ } else {
10208
+ await handleShell(pending.command, pending.chatId, channel, true);
10209
+ }
10210
+ } else if (action === "cancel") {
10211
+ removePendingCommand(id);
10212
+ await channel.sendText(chatId, "Command cancelled.", "plain");
10213
+ }
10214
+ } else if (data.startsWith("cwd:")) {
10215
+ const parts = data.split(":");
10216
+ const action = parts[1];
10217
+ const targetChatId = parts.slice(2).join(":");
10218
+ if (action === "keep") {
10219
+ await channel.sendText(chatId, "Session kept. The agent will continue with existing context.", "plain");
10220
+ } else if (action === "summarize") {
10221
+ await summarizeSession(targetChatId);
10222
+ clearSession(targetChatId);
10223
+ await channel.sendText(chatId, "Session summarized and reset. Context preserved in memory.", "plain");
10224
+ } else if (action === "reset") {
10225
+ clearSession(targetChatId);
10226
+ await channel.sendText(chatId, "Session reset. Clean slate.", "plain");
10227
+ }
10228
+ } else if (data.startsWith("cwdpick:")) {
10229
+ const alias = data.slice(8);
10230
+ const bookmark = getBookmark(chatId, alias);
10231
+ if (!bookmark) {
10232
+ await channel.sendText(chatId, `Bookmark '${alias}' no longer exists.`, "plain");
10233
+ return;
10234
+ }
10235
+ setCwd(chatId, bookmark.path);
10236
+ touchBookmark(chatId, alias);
10237
+ logActivity(getDb(), { chatId, source: "telegram", eventType: "config_changed", summary: `Working directory set to ${bookmark.path}`, detail: { field: "cwd", value: bookmark.path } });
10238
+ await sendCwdSessionChoice(chatId, bookmark.path, channel);
9837
10239
  } else if (data.startsWith("skill:")) {
9838
10240
  const parts = data.slice(6).split(":");
9839
10241
  let skillName;
@@ -9871,6 +10273,101 @@ ${PERM_MODES[chosen]}`,
9871
10273
  await sendResponse(chatId, channel, response.text);
9872
10274
  }
9873
10275
  }
10276
+ async function handleShell(command, chatId, channel, skipGuard = false) {
10277
+ if (!skipGuard && isDestructive(command)) {
10278
+ const id = storePendingCommand(command, chatId, false);
10279
+ if (typeof channel.sendKeyboard === "function") {
10280
+ await channel.sendKeyboard(
10281
+ chatId,
10282
+ `\u26A0\uFE0F This command looks potentially destructive.
10283
+
10284
+ Command: ${command}`,
10285
+ [[
10286
+ { label: "Run anyway", data: `shell:confirm:${id}` },
10287
+ { label: "Cancel", data: `shell:cancel:${id}` }
10288
+ ]]
10289
+ );
10290
+ } else {
10291
+ await channel.sendText(chatId, `\u26A0\uFE0F Destructive command blocked: ${command}
10292
+ No keyboard available to confirm.`, "plain");
10293
+ }
10294
+ return;
10295
+ }
10296
+ const cwd = getCwd(chatId) ?? `${process.env.HOME ?? "/tmp"}/.cc-claw/workspace`;
10297
+ const result = await executeShell(command, cwd);
10298
+ logActivity(getDb(), {
10299
+ chatId,
10300
+ source: "telegram",
10301
+ eventType: "shell_command",
10302
+ summary: `Shell: ${command.slice(0, 100)}`,
10303
+ detail: { command, exitCode: result.exitCode, outputLength: result.output.length, timedOut: result.timedOut }
10304
+ });
10305
+ const formatted = formatCodeBlock(command, result.output, result.exitCode);
10306
+ if (shouldSendAsFile(formatted)) {
10307
+ const buffer = Buffer.from(result.output, "utf-8");
10308
+ await channel.sendFile(chatId, buffer, "output.txt", "text/plain");
10309
+ await channel.sendText(chatId, `Output too long (${result.output.length} chars), sent as file.`, "plain");
10310
+ } else {
10311
+ await channel.sendText(chatId, formatted, "html");
10312
+ }
10313
+ }
10314
+ async function handleRawShell(command, chatId, channel, skipGuard = false) {
10315
+ if (!skipGuard && isDestructive(command)) {
10316
+ const id = storePendingCommand(command, chatId, true);
10317
+ if (typeof channel.sendKeyboard === "function") {
10318
+ await channel.sendKeyboard(
10319
+ chatId,
10320
+ `\u26A0\uFE0F This command looks potentially destructive.
10321
+
10322
+ Command: ${command}`,
10323
+ [[
10324
+ { label: "Run anyway", data: `shell:confirm:${id}` },
10325
+ { label: "Cancel", data: `shell:cancel:${id}` }
10326
+ ]]
10327
+ );
10328
+ } else {
10329
+ await channel.sendText(chatId, `\u26A0\uFE0F Destructive command blocked: ${command}
10330
+ No keyboard available to confirm.`, "plain");
10331
+ }
10332
+ return;
10333
+ }
10334
+ const cwd = getCwd(chatId) ?? `${process.env.HOME ?? "/tmp"}/.cc-claw/workspace`;
10335
+ const result = await executeShell(command, cwd);
10336
+ logActivity(getDb(), {
10337
+ chatId,
10338
+ source: "telegram",
10339
+ eventType: "shell_command",
10340
+ summary: `Shell: ${command.slice(0, 100)}`,
10341
+ detail: { command, exitCode: result.exitCode, outputLength: result.output.length, timedOut: result.timedOut }
10342
+ });
10343
+ const formatted = formatRaw(result.output);
10344
+ if (shouldSendAsFile(formatted)) {
10345
+ const buffer = Buffer.from(result.output, "utf-8");
10346
+ await channel.sendFile(chatId, buffer, "output.txt", "text/plain");
10347
+ await channel.sendText(chatId, `Output too long (${result.output.length} chars), sent as file.`, "plain");
10348
+ } else {
10349
+ await channel.sendText(chatId, formatted, "plain");
10350
+ }
10351
+ }
10352
+ async function sendCwdSessionChoice(chatId, path, channel) {
10353
+ if (typeof channel.sendKeyboard === "function") {
10354
+ await channel.sendKeyboard(
10355
+ chatId,
10356
+ `Working directory set to: ${path}
10357
+
10358
+ Changing directories mid-session may confuse the agent.
10359
+ What would you like to do?`,
10360
+ [[
10361
+ { label: "Keep session", data: `cwd:keep:${chatId}` },
10362
+ { label: "Summarize & reset", data: `cwd:summarize:${chatId}` },
10363
+ { label: "Reset session", data: `cwd:reset:${chatId}` }
10364
+ ]]
10365
+ );
10366
+ } else {
10367
+ await channel.sendText(chatId, `Working directory set to: ${path}
10368
+ Session kept.`, "plain");
10369
+ }
10370
+ }
9874
10371
  function parseIntervalToMs(input) {
9875
10372
  const match = input.trim().match(/^(\d+)\s*(m|min|h|hr|hour|s|sec)$/i);
9876
10373
  if (!match) return null;
@@ -9938,6 +10435,9 @@ var init_router = __esm({
9938
10435
  init_registry();
9939
10436
  init_registry2();
9940
10437
  init_store3();
10438
+ init_guard();
10439
+ init_exec();
10440
+ init_backend_cmd();
9941
10441
  PERM_MODES = {
9942
10442
  yolo: "YOLO \u2014 all tools, full autopilot",
9943
10443
  safe: "Safe \u2014 only my allowed tools",
@@ -11520,7 +12020,7 @@ async function doctorCommand(globalOpts, localOpts) {
11520
12020
  const r = d;
11521
12021
  const lines = [
11522
12022
  "",
11523
- box("CC-Claw Doctor"),
12023
+ box(`CC-Claw Doctor v${VERSION}`),
11524
12024
  ""
11525
12025
  ];
11526
12026
  for (const c of r.checks) {
@@ -11550,6 +12050,7 @@ var init_doctor = __esm({
11550
12050
  "use strict";
11551
12051
  init_format();
11552
12052
  init_paths();
12053
+ init_version();
11553
12054
  }
11554
12055
  });
11555
12056
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-claw",
3
- "version": "0.2.8",
3
+ "version": "0.3.1",
4
4
  "description": "CC-Claw: Personal AI assistant on Telegram — multi-backend (Claude, Gemini, Codex), sub-agent orchestration, MCP management",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",