cc-claw 0.2.8 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +548 -54
  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.0" : (() => {
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 = "";
@@ -8278,6 +8347,180 @@ var init_video = __esm({
8278
8347
  }
8279
8348
  });
8280
8349
 
8350
+ // src/shell/guard.ts
8351
+ import { randomUUID as randomUUID2 } from "crypto";
8352
+ function isDestructive(command) {
8353
+ return DESTRUCTIVE_PATTERNS.some((pattern) => pattern.test(command));
8354
+ }
8355
+ function storePendingCommand(command, chatId, raw) {
8356
+ const id = randomUUID2().slice(0, 8);
8357
+ pendingCommands.set(id, { command, chatId, raw, createdAt: Date.now() });
8358
+ setTimeout(() => pendingCommands.delete(id), PENDING_TTL_MS);
8359
+ return id;
8360
+ }
8361
+ function getPendingCommand(id) {
8362
+ return pendingCommands.get(id);
8363
+ }
8364
+ function removePendingCommand(id) {
8365
+ return pendingCommands.delete(id);
8366
+ }
8367
+ var DESTRUCTIVE_PATTERNS, pendingCommands, PENDING_TTL_MS;
8368
+ var init_guard = __esm({
8369
+ "src/shell/guard.ts"() {
8370
+ "use strict";
8371
+ DESTRUCTIVE_PATTERNS = [
8372
+ /\brm\s+(-[a-zA-Z]*[fr][a-zA-Z]*|-[a-zA-Z]*[rf][a-zA-Z]*|--force|--recursive)\b/,
8373
+ /\brm\s+-[a-zA-Z]*\s+\/\s*$/,
8374
+ /\bmkfs\b/,
8375
+ /\bdd\b.*\bof=/,
8376
+ /\b(shutdown|reboot|halt|poweroff)\b/,
8377
+ />\s*\/dev\/sd/,
8378
+ /\bchmod\s+-R\s+777\b/,
8379
+ /\bchown\s+-R\b/,
8380
+ /\bkillall\b/,
8381
+ /\blaunchctl\s+(unload|remove)\b/,
8382
+ /:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;?\s*:/
8383
+ ];
8384
+ pendingCommands = /* @__PURE__ */ new Map();
8385
+ PENDING_TTL_MS = 5 * 60 * 1e3;
8386
+ }
8387
+ });
8388
+
8389
+ // src/shell/exec.ts
8390
+ import { execFile as execFile3 } from "child_process";
8391
+ function executeShell(command, cwd, timeoutMs = SHELL_TIMEOUT_MS) {
8392
+ return new Promise((resolve) => {
8393
+ execFile3(
8394
+ "/bin/zsh",
8395
+ ["-c", command],
8396
+ { cwd, timeout: timeoutMs, maxBuffer: 5 * 1024 * 1024 },
8397
+ (error3, stdout, stderr) => {
8398
+ const output2 = (stdout || "") + (stderr || "");
8399
+ if (error3 && "killed" in error3 && error3.killed) {
8400
+ resolve({
8401
+ output: `Command timed out after ${Math.round(timeoutMs / 1e3)} seconds`,
8402
+ exitCode: 124,
8403
+ timedOut: true
8404
+ });
8405
+ return;
8406
+ }
8407
+ const exitCode = error3 ? typeof error3.code === "number" ? error3.code : 1 : 0;
8408
+ resolve({
8409
+ output: output2,
8410
+ exitCode,
8411
+ timedOut: false
8412
+ });
8413
+ }
8414
+ );
8415
+ });
8416
+ }
8417
+ function formatCodeBlock(command, output2, exitCode) {
8418
+ return `<pre>$ ${command}
8419
+ ${output2}
8420
+ [exit ${exitCode}]</pre>`;
8421
+ }
8422
+ function formatRaw(output2) {
8423
+ return output2.trim();
8424
+ }
8425
+ function shouldSendAsFile(output2) {
8426
+ return output2.length > OUTPUT_MAX_LENGTH;
8427
+ }
8428
+ var SHELL_TIMEOUT_MS, OUTPUT_MAX_LENGTH;
8429
+ var init_exec = __esm({
8430
+ "src/shell/exec.ts"() {
8431
+ "use strict";
8432
+ SHELL_TIMEOUT_MS = 6e4;
8433
+ OUTPUT_MAX_LENGTH = 4e3;
8434
+ }
8435
+ });
8436
+
8437
+ // src/shell/backend-cmd.ts
8438
+ import { execFile as execFile4 } from "child_process";
8439
+ function buildNativeResponse(backendId, command, output2) {
8440
+ const body = output2 || "(no output)";
8441
+ return `<pre>[${backendId}] ${command}
8442
+ ${body}</pre>`;
8443
+ }
8444
+ function extractTextFromNdjson(raw) {
8445
+ const texts = [];
8446
+ for (const line of raw.split("\n")) {
8447
+ if (!line.trim()) continue;
8448
+ try {
8449
+ const obj = JSON.parse(line);
8450
+ if (obj.type === "result" && obj.result) {
8451
+ texts.push(typeof obj.result === "string" ? obj.result : JSON.stringify(obj.result));
8452
+ }
8453
+ if (obj.type === "assistant" && obj.message?.content) {
8454
+ for (const block of obj.message.content) {
8455
+ if (block.type === "text" && block.text) texts.push(block.text);
8456
+ }
8457
+ }
8458
+ if (obj.type === "text" && obj.text) {
8459
+ texts.push(obj.text);
8460
+ }
8461
+ } catch {
8462
+ }
8463
+ }
8464
+ return texts.join("\n").trim();
8465
+ }
8466
+ async function handleBackendCommand(command, chatId, channel) {
8467
+ let adapter;
8468
+ try {
8469
+ adapter = getAdapterForChat(chatId);
8470
+ } catch {
8471
+ await channel.sendText(chatId, "No backend set. Use /backend first.", "plain");
8472
+ return;
8473
+ }
8474
+ const sessionId = getSessionId(chatId);
8475
+ if (!sessionId) {
8476
+ await channel.sendText(chatId, "No active session. Start a conversation first.", "plain");
8477
+ return;
8478
+ }
8479
+ const cwd = getCwd(chatId) ?? `${process.env.HOME ?? "/tmp"}/.cc-claw/workspace`;
8480
+ try {
8481
+ const output2 = await spawnBackendCommand(adapter, command, sessionId, cwd);
8482
+ const formatted = buildNativeResponse(adapter.id, command, output2);
8483
+ await channel.sendText(chatId, formatted, "html");
8484
+ } catch (err) {
8485
+ const msg = err instanceof Error ? err.message : String(err);
8486
+ await channel.sendText(chatId, `Backend command failed: ${msg}`, "plain");
8487
+ }
8488
+ }
8489
+ function spawnBackendCommand(adapter, command, sessionId, cwd) {
8490
+ return new Promise((resolve, reject) => {
8491
+ const config2 = adapter.buildSpawnConfig({
8492
+ prompt: command,
8493
+ sessionId,
8494
+ cwd,
8495
+ permMode: "plan",
8496
+ allowedTools: []
8497
+ });
8498
+ const env = { ...process.env, ...adapter.getEnv() };
8499
+ execFile4(config2.executable, config2.args, {
8500
+ cwd,
8501
+ timeout: BACKEND_CMD_TIMEOUT_MS,
8502
+ env,
8503
+ maxBuffer: 5 * 1024 * 1024
8504
+ }, (error3, stdout, stderr) => {
8505
+ if (error3 && "killed" in error3 && error3.killed) {
8506
+ reject(new Error(`Backend command timed out after ${BACKEND_CMD_TIMEOUT_MS / 1e3} seconds.`));
8507
+ return;
8508
+ }
8509
+ const output2 = extractTextFromNdjson(stdout) || stdout || stderr || "";
8510
+ resolve(output2.trim());
8511
+ });
8512
+ });
8513
+ }
8514
+ var BACKEND_CMD_TIMEOUT_MS;
8515
+ var init_backend_cmd = __esm({
8516
+ "src/shell/backend-cmd.ts"() {
8517
+ "use strict";
8518
+ init_backends();
8519
+ init_store4();
8520
+ BACKEND_CMD_TIMEOUT_MS = 15e3;
8521
+ }
8522
+ });
8523
+
8281
8524
  // src/router.ts
8282
8525
  import { readFile as readFile5, writeFile as writeFile2, unlink as unlink2 } from "fs/promises";
8283
8526
  import { resolve as resolvePath } from "path";
@@ -8367,6 +8610,17 @@ async function handleMessage(msg, channel) {
8367
8610
  }
8368
8611
  }
8369
8612
  }
8613
+ if (msg.type === "text" && msg.text) {
8614
+ const text = msg.text.trim();
8615
+ if (text.startsWith("!!")) {
8616
+ const cmd = text.slice(2).trim();
8617
+ if (cmd) return handleRawShell(cmd, chatId, channel);
8618
+ }
8619
+ if (text.startsWith("!") && text.length > 1) {
8620
+ const cmd = text.slice(1).trim();
8621
+ if (cmd) return handleShell(cmd, chatId, channel);
8622
+ }
8623
+ }
8370
8624
  switch (msg.type) {
8371
8625
  case "command":
8372
8626
  await handleCommand(msg, channel);
@@ -8386,6 +8640,9 @@ async function handleMessage(msg, channel) {
8386
8640
  }
8387
8641
  async function handleCommand(msg, channel) {
8388
8642
  const { chatId, command, commandArgs } = msg;
8643
+ if (command?.startsWith("/")) {
8644
+ return handleBackendCommand(command, chatId, channel);
8645
+ }
8389
8646
  switch (command) {
8390
8647
  case "start":
8391
8648
  case "help":
@@ -8520,15 +8777,7 @@ Tap to toggle:`,
8520
8777
  case "backend": {
8521
8778
  const requestedBackend = (commandArgs ?? "").trim().toLowerCase();
8522
8779
  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");
8780
+ await sendBackendSwitchConfirmation(chatId, requestedBackend, channel);
8532
8781
  break;
8533
8782
  }
8534
8783
  const currentBackend = getBackend(chatId);
@@ -8552,15 +8801,7 @@ Tap to toggle:`,
8552
8801
  case "codex": {
8553
8802
  const backendId = command;
8554
8803
  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");
8804
+ await sendBackendSwitchConfirmation(chatId, backendId, channel);
8564
8805
  } else {
8565
8806
  await channel.sendText(chatId, `Backend "${command}" is not available.`, "plain");
8566
8807
  }
@@ -8733,11 +8974,28 @@ Use /summarizer auto, /summarizer off, or /summarizer <backend>:<model>`, "plain
8733
8974
  case "cwd": {
8734
8975
  if (!commandArgs) {
8735
8976
  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
- );
8977
+ const recents = getRecentBookmarks(chatId, 10);
8978
+ if (recents.length === 0) {
8979
+ await channel.sendText(
8980
+ chatId,
8981
+ current ? `Working directory: ${current}
8982
+
8983
+ No saved bookmarks yet. Set a directory with /cwd <path> to auto-save.` : "No working directory set. Usage: /cwd ~/projects/my-app",
8984
+ "plain"
8985
+ );
8986
+ return;
8987
+ }
8988
+ const text = current ? `Current: ${current}
8989
+
8990
+ Recent directories:` : "Recent directories:";
8991
+ if (typeof channel.sendKeyboard === "function") {
8992
+ const buttons = recents.map((r) => [{ label: r.alias, data: `cwdpick:${r.alias}` }]);
8993
+ await channel.sendKeyboard(chatId, text, buttons);
8994
+ } else {
8995
+ const list = recents.map((r) => ` ${r.alias} \u2192 ${r.path}`).join("\n");
8996
+ await channel.sendText(chatId, `${text}
8997
+ ${list}`, "plain");
8998
+ }
8741
8999
  return;
8742
9000
  }
8743
9001
  if (commandArgs === "reset" || commandArgs === "clear") {
@@ -8745,12 +9003,71 @@ Use /summarizer auto, /summarizer off, or /summarizer <backend>:<model>`, "plain
8745
9003
  await channel.sendText(chatId, "Working directory cleared. Using default.", "plain");
8746
9004
  return;
8747
9005
  }
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");
9006
+ if (commandArgs === "aliases") {
9007
+ const all = getAllBookmarks(chatId);
9008
+ if (all.length === 0) {
9009
+ await channel.sendText(chatId, "No bookmarks saved yet.", "plain");
9010
+ return;
9011
+ }
9012
+ const lines = all.map((b) => ` ${b.manual ? "[manual]" : "[auto]"} ${b.alias} \u2192 ${b.path}`);
9013
+ await channel.sendText(chatId, `Directory bookmarks:
9014
+ ${lines.join("\n")}`, "plain");
9015
+ return;
9016
+ }
9017
+ if (commandArgs.startsWith("unalias ")) {
9018
+ const aliasName = commandArgs.slice(8).trim();
9019
+ if (!aliasName) {
9020
+ await channel.sendText(chatId, "Usage: /cwd unalias <name>", "plain");
9021
+ return;
9022
+ }
9023
+ const deleted = deleteBookmark(chatId, aliasName);
9024
+ await channel.sendText(chatId, deleted ? `Bookmark '${aliasName}' removed.` : `Bookmark '${aliasName}' not found.`, "plain");
9025
+ return;
9026
+ }
9027
+ if (commandArgs.startsWith("alias ")) {
9028
+ const parts = commandArgs.slice(6).trim().split(/\s+/);
9029
+ if (parts.length < 2) {
9030
+ await channel.sendText(chatId, "Usage: /cwd alias <name> <path>", "plain");
9031
+ return;
9032
+ }
9033
+ const [aliasName, ...pathParts] = parts;
9034
+ const aliasPath = pathParts.join(" ").replace(/^~/, process.env.HOME ?? "");
9035
+ upsertBookmark(chatId, aliasName, aliasPath, true);
9036
+ await channel.sendText(chatId, `Bookmark saved: ${aliasName} \u2192 ${aliasPath}`, "plain");
9037
+ return;
9038
+ }
9039
+ const arg = commandArgs;
9040
+ if (arg.startsWith("/") || arg.startsWith("~")) {
9041
+ const resolvedPath = arg.startsWith("~") ? arg.replace("~", process.env.HOME ?? "") : arg;
9042
+ setCwd(chatId, resolvedPath);
9043
+ const basename2 = resolvedPath.split("/").filter(Boolean).pop();
9044
+ if (basename2) upsertBookmark(chatId, basename2, resolvedPath, false);
9045
+ logActivity(getDb(), { chatId, source: "telegram", eventType: "config_changed", summary: `Working directory set to ${resolvedPath}`, detail: { field: "cwd", value: resolvedPath } });
9046
+ await sendCwdSessionChoice(chatId, resolvedPath, channel);
9047
+ return;
9048
+ }
9049
+ const exact = getBookmark(chatId, arg);
9050
+ if (exact) {
9051
+ setCwd(chatId, exact.path);
9052
+ touchBookmark(chatId, arg);
9053
+ logActivity(getDb(), { chatId, source: "telegram", eventType: "config_changed", summary: `Working directory set to ${exact.path}`, detail: { field: "cwd", value: exact.path } });
9054
+ await sendCwdSessionChoice(chatId, exact.path, channel);
9055
+ return;
9056
+ }
9057
+ const matches = findBookmarksByPrefix(chatId, arg);
9058
+ if (matches.length === 1) {
9059
+ setCwd(chatId, matches[0].path);
9060
+ touchBookmark(chatId, matches[0].alias);
9061
+ logActivity(getDb(), { chatId, source: "telegram", eventType: "config_changed", summary: `Working directory set to ${matches[0].path}`, detail: { field: "cwd", value: matches[0].path } });
9062
+ await sendCwdSessionChoice(chatId, matches[0].path, channel);
9063
+ return;
9064
+ }
9065
+ if (matches.length > 1 && typeof channel.sendKeyboard === "function") {
9066
+ const buttons = matches.map((m) => [{ label: `${m.alias} \u2192 ${m.path}`, data: `cwdpick:${m.alias}` }]);
9067
+ await channel.sendKeyboard(chatId, `Multiple matches for "${arg}":`, buttons);
9068
+ return;
9069
+ }
9070
+ await channel.sendText(chatId, `Directory alias '${arg}' not found. Use /cwd aliases to see saved bookmarks.`, "plain");
8754
9071
  break;
8755
9072
  }
8756
9073
  case "memory": {
@@ -9255,7 +9572,7 @@ Use /skills to see it.`, "plain");
9255
9572
  lines.push("", "\u2501\u2501 <b>Built-in</b> \u2501\u2501");
9256
9573
  lines.push(` \u2705 <b>cc-claw</b> <i>Agent orchestrator (spawn, tasks, inbox)</i>`);
9257
9574
  }
9258
- const { execFile: execFile3 } = await import("child_process");
9575
+ const { execFile: execFile5 } = await import("child_process");
9259
9576
  const { homedir: homedir5 } = await import("os");
9260
9577
  const discoveryCwd = homedir5();
9261
9578
  const runnerResults = await Promise.allSettled(
@@ -9264,7 +9581,7 @@ Use /skills to see it.`, "plain");
9264
9581
  if (!listCmd.length) return Promise.resolve({ runner, output: "" });
9265
9582
  const exe = runner.getExecutablePath();
9266
9583
  return new Promise((resolve) => {
9267
- execFile3(exe, listCmd.slice(1), {
9584
+ execFile5(exe, listCmd.slice(1), {
9268
9585
  encoding: "utf-8",
9269
9586
  timeout: 3e4,
9270
9587
  cwd: discoveryCwd,
@@ -9711,31 +10028,65 @@ async function sendResponse(chatId, channel, text) {
9711
10028
  function isImageExt(ext) {
9712
10029
  return ["jpg", "jpeg", "png", "gif", "webp", "bmp", "svg"].includes(ext);
9713
10030
  }
10031
+ async function sendBackendSwitchConfirmation(chatId, target, channel) {
10032
+ const current = getBackend(chatId);
10033
+ const targetAdapter = getAdapter(target);
10034
+ if (current === target) {
10035
+ await channel.sendText(chatId, `Already using ${targetAdapter.displayName}.`, "plain");
10036
+ return;
10037
+ }
10038
+ const currentLabel = current ? getAdapter(current).displayName : "current backend";
10039
+ if (typeof channel.sendKeyboard === "function") {
10040
+ await channel.sendKeyboard(
10041
+ chatId,
10042
+ `\u26A0\uFE0F Switching to ${targetAdapter.displayName} will summarize and reset your current session.
10043
+
10044
+ What would you like to do?`,
10045
+ [
10046
+ [{ label: `Stay on ${currentLabel}`, data: "backend_cancel" }],
10047
+ [{ label: `Switch to ${targetAdapter.displayName} + summarize`, data: `backend_confirm:${target}` }]
10048
+ ]
10049
+ );
10050
+ } else {
10051
+ await doBackendSwitch(chatId, target, channel);
10052
+ }
10053
+ }
10054
+ async function doBackendSwitch(chatId, backendId, channel) {
10055
+ summarizeSession(chatId).catch(() => {
10056
+ });
10057
+ clearSession(chatId);
10058
+ clearModel(chatId);
10059
+ clearThinkingLevel(chatId);
10060
+ setBackend(chatId, backendId);
10061
+ const adapter = getAdapter(backendId);
10062
+ logActivity(getDb(), { chatId, source: "telegram", eventType: "config_changed", summary: `Backend switched to ${adapter.displayName}`, detail: { field: "backend", value: backendId } });
10063
+ await channel.sendText(
10064
+ chatId,
10065
+ `Backend switched to ${adapter.displayName}.
10066
+ Default model: ${adapter.defaultModel}
10067
+ Session reset. Ready!`,
10068
+ "plain"
10069
+ );
10070
+ }
9714
10071
  async function handleCallback(chatId, data, channel) {
9715
10072
  if (data.startsWith("backend:")) {
9716
10073
  const chosen = data.slice(8);
9717
10074
  if (!getAllBackendIds().includes(chosen)) return;
9718
10075
  const previous = getBackend(chatId);
9719
10076
  if (chosen === previous) {
9720
- const adapter2 = getAdapter(chosen);
9721
- await channel.sendText(chatId, `Already using ${adapter2.displayName}.`, "plain");
10077
+ const adapter = getAdapter(chosen);
10078
+ await channel.sendText(chatId, `Already using ${adapter.displayName}.`, "plain");
9722
10079
  return;
9723
10080
  }
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
- );
10081
+ await sendBackendSwitchConfirmation(chatId, chosen, channel);
10082
+ } else if (data.startsWith("backend_confirm:")) {
10083
+ const chosen = data.slice(16);
10084
+ if (!getAllBackendIds().includes(chosen)) return;
10085
+ await doBackendSwitch(chatId, chosen, channel);
10086
+ } else if (data === "backend_cancel") {
10087
+ const current = getBackend(chatId);
10088
+ const label2 = current ? getAdapter(current).displayName : "current backend";
10089
+ await channel.sendText(chatId, `No change. Staying on ${label2}.`, "plain");
9739
10090
  } else if (data.startsWith("model:")) {
9740
10091
  const chosen = data.slice(6);
9741
10092
  let adapter;
@@ -9834,6 +10185,51 @@ ${PERM_MODES[chosen]}`,
9834
10185
  } else {
9835
10186
  await channel.sendText(chatId, "Preference not saved.", "plain");
9836
10187
  }
10188
+ } else if (data.startsWith("shell:")) {
10189
+ const parts = data.split(":");
10190
+ const action = parts[1];
10191
+ const id = parts[2];
10192
+ if (action === "confirm") {
10193
+ const pending = getPendingCommand(id);
10194
+ if (!pending) {
10195
+ await channel.sendText(chatId, "Confirmation expired. Please re-send the command.", "plain");
10196
+ return;
10197
+ }
10198
+ removePendingCommand(id);
10199
+ if (pending.raw) {
10200
+ await handleRawShell(pending.command, pending.chatId, channel, true);
10201
+ } else {
10202
+ await handleShell(pending.command, pending.chatId, channel, true);
10203
+ }
10204
+ } else if (action === "cancel") {
10205
+ removePendingCommand(id);
10206
+ await channel.sendText(chatId, "Command cancelled.", "plain");
10207
+ }
10208
+ } else if (data.startsWith("cwd:")) {
10209
+ const parts = data.split(":");
10210
+ const action = parts[1];
10211
+ const targetChatId = parts.slice(2).join(":");
10212
+ if (action === "keep") {
10213
+ await channel.sendText(chatId, "Session kept. The agent will continue with existing context.", "plain");
10214
+ } else if (action === "summarize") {
10215
+ await summarizeSession(targetChatId);
10216
+ clearSession(targetChatId);
10217
+ await channel.sendText(chatId, "Session summarized and reset. Context preserved in memory.", "plain");
10218
+ } else if (action === "reset") {
10219
+ clearSession(targetChatId);
10220
+ await channel.sendText(chatId, "Session reset. Clean slate.", "plain");
10221
+ }
10222
+ } else if (data.startsWith("cwdpick:")) {
10223
+ const alias = data.slice(8);
10224
+ const bookmark = getBookmark(chatId, alias);
10225
+ if (!bookmark) {
10226
+ await channel.sendText(chatId, `Bookmark '${alias}' no longer exists.`, "plain");
10227
+ return;
10228
+ }
10229
+ setCwd(chatId, bookmark.path);
10230
+ touchBookmark(chatId, alias);
10231
+ logActivity(getDb(), { chatId, source: "telegram", eventType: "config_changed", summary: `Working directory set to ${bookmark.path}`, detail: { field: "cwd", value: bookmark.path } });
10232
+ await sendCwdSessionChoice(chatId, bookmark.path, channel);
9837
10233
  } else if (data.startsWith("skill:")) {
9838
10234
  const parts = data.slice(6).split(":");
9839
10235
  let skillName;
@@ -9871,6 +10267,101 @@ ${PERM_MODES[chosen]}`,
9871
10267
  await sendResponse(chatId, channel, response.text);
9872
10268
  }
9873
10269
  }
10270
+ async function handleShell(command, chatId, channel, skipGuard = false) {
10271
+ if (!skipGuard && isDestructive(command)) {
10272
+ const id = storePendingCommand(command, chatId, false);
10273
+ if (typeof channel.sendKeyboard === "function") {
10274
+ await channel.sendKeyboard(
10275
+ chatId,
10276
+ `\u26A0\uFE0F This command looks potentially destructive.
10277
+
10278
+ Command: ${command}`,
10279
+ [[
10280
+ { label: "Run anyway", data: `shell:confirm:${id}` },
10281
+ { label: "Cancel", data: `shell:cancel:${id}` }
10282
+ ]]
10283
+ );
10284
+ } else {
10285
+ await channel.sendText(chatId, `\u26A0\uFE0F Destructive command blocked: ${command}
10286
+ No keyboard available to confirm.`, "plain");
10287
+ }
10288
+ return;
10289
+ }
10290
+ const cwd = getCwd(chatId) ?? `${process.env.HOME ?? "/tmp"}/.cc-claw/workspace`;
10291
+ const result = await executeShell(command, cwd);
10292
+ logActivity(getDb(), {
10293
+ chatId,
10294
+ source: "telegram",
10295
+ eventType: "shell_command",
10296
+ summary: `Shell: ${command.slice(0, 100)}`,
10297
+ detail: { command, exitCode: result.exitCode, outputLength: result.output.length, timedOut: result.timedOut }
10298
+ });
10299
+ const formatted = formatCodeBlock(command, result.output, result.exitCode);
10300
+ if (shouldSendAsFile(formatted)) {
10301
+ const buffer = Buffer.from(result.output, "utf-8");
10302
+ await channel.sendFile(chatId, buffer, "output.txt", "text/plain");
10303
+ await channel.sendText(chatId, `Output too long (${result.output.length} chars), sent as file.`, "plain");
10304
+ } else {
10305
+ await channel.sendText(chatId, formatted, "html");
10306
+ }
10307
+ }
10308
+ async function handleRawShell(command, chatId, channel, skipGuard = false) {
10309
+ if (!skipGuard && isDestructive(command)) {
10310
+ const id = storePendingCommand(command, chatId, true);
10311
+ if (typeof channel.sendKeyboard === "function") {
10312
+ await channel.sendKeyboard(
10313
+ chatId,
10314
+ `\u26A0\uFE0F This command looks potentially destructive.
10315
+
10316
+ Command: ${command}`,
10317
+ [[
10318
+ { label: "Run anyway", data: `shell:confirm:${id}` },
10319
+ { label: "Cancel", data: `shell:cancel:${id}` }
10320
+ ]]
10321
+ );
10322
+ } else {
10323
+ await channel.sendText(chatId, `\u26A0\uFE0F Destructive command blocked: ${command}
10324
+ No keyboard available to confirm.`, "plain");
10325
+ }
10326
+ return;
10327
+ }
10328
+ const cwd = getCwd(chatId) ?? `${process.env.HOME ?? "/tmp"}/.cc-claw/workspace`;
10329
+ const result = await executeShell(command, cwd);
10330
+ logActivity(getDb(), {
10331
+ chatId,
10332
+ source: "telegram",
10333
+ eventType: "shell_command",
10334
+ summary: `Shell: ${command.slice(0, 100)}`,
10335
+ detail: { command, exitCode: result.exitCode, outputLength: result.output.length, timedOut: result.timedOut }
10336
+ });
10337
+ const formatted = formatRaw(result.output);
10338
+ if (shouldSendAsFile(formatted)) {
10339
+ const buffer = Buffer.from(result.output, "utf-8");
10340
+ await channel.sendFile(chatId, buffer, "output.txt", "text/plain");
10341
+ await channel.sendText(chatId, `Output too long (${result.output.length} chars), sent as file.`, "plain");
10342
+ } else {
10343
+ await channel.sendText(chatId, formatted, "plain");
10344
+ }
10345
+ }
10346
+ async function sendCwdSessionChoice(chatId, path, channel) {
10347
+ if (typeof channel.sendKeyboard === "function") {
10348
+ await channel.sendKeyboard(
10349
+ chatId,
10350
+ `Working directory set to: ${path}
10351
+
10352
+ Changing directories mid-session may confuse the agent.
10353
+ What would you like to do?`,
10354
+ [[
10355
+ { label: "Keep session", data: `cwd:keep:${chatId}` },
10356
+ { label: "Summarize & reset", data: `cwd:summarize:${chatId}` },
10357
+ { label: "Reset session", data: `cwd:reset:${chatId}` }
10358
+ ]]
10359
+ );
10360
+ } else {
10361
+ await channel.sendText(chatId, `Working directory set to: ${path}
10362
+ Session kept.`, "plain");
10363
+ }
10364
+ }
9874
10365
  function parseIntervalToMs(input) {
9875
10366
  const match = input.trim().match(/^(\d+)\s*(m|min|h|hr|hour|s|sec)$/i);
9876
10367
  if (!match) return null;
@@ -9938,6 +10429,9 @@ var init_router = __esm({
9938
10429
  init_registry();
9939
10430
  init_registry2();
9940
10431
  init_store3();
10432
+ init_guard();
10433
+ init_exec();
10434
+ init_backend_cmd();
9941
10435
  PERM_MODES = {
9942
10436
  yolo: "YOLO \u2014 all tools, full autopilot",
9943
10437
  safe: "Safe \u2014 only my allowed tools",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-claw",
3
- "version": "0.2.8",
3
+ "version": "0.3.0",
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",