cc-claw 0.2.7 → 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 +565 -58
  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.7" : (() => {
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 {
@@ -90,6 +90,7 @@ var init_log = __esm({
90
90
  var store_exports = {};
91
91
  __export(store_exports, {
92
92
  claimTask: () => claimTask,
93
+ cleanupStaleAgents: () => cleanupStaleAgents,
93
94
  clearState: () => clearState,
94
95
  createAgent: () => createAgent,
95
96
  createOrchestration: () => createOrchestration,
@@ -273,6 +274,14 @@ function listActiveAgents(db3) {
273
274
  function listQueuedAgents(db3) {
274
275
  return db3.prepare("SELECT * FROM agents WHERE status = 'queued' ORDER BY createdAt").all();
275
276
  }
277
+ function cleanupStaleAgents(db3) {
278
+ const result = db3.prepare(`
279
+ UPDATE agents SET status = 'failed', completedAt = datetime('now'),
280
+ resultSummary = 'Marked stale after service restart'
281
+ WHERE status IN ('queued', 'starting', 'running', 'idle')
282
+ `).run();
283
+ return result.changes;
284
+ }
276
285
  function getNextQueuedAgent(db3, orchestrationId) {
277
286
  return db3.prepare(
278
287
  "SELECT * FROM agents WHERE orchestrationId = ? AND status = 'queued' ORDER BY createdAt LIMIT 1"
@@ -855,10 +864,13 @@ __export(store_exports2, {
855
864
  clearThinkingLevel: () => clearThinkingLevel,
856
865
  clearUsage: () => clearUsage,
857
866
  completeJobRun: () => completeJobRun,
867
+ deleteBookmark: () => deleteBookmark,
868
+ findBookmarksByPrefix: () => findBookmarksByPrefix,
858
869
  forgetMemory: () => forgetMemory,
859
870
  getActiveJobs: () => getActiveJobs,
860
871
  getActiveWatches: () => getActiveWatches,
861
872
  getAllBackendLimits: () => getAllBackendLimits,
873
+ getAllBookmarks: () => getAllBookmarks,
862
874
  getAllChatAliases: () => getAllChatAliases,
863
875
  getAllJobs: () => getAllJobs,
864
876
  getAllMemoriesWithEmbeddings: () => getAllMemoriesWithEmbeddings,
@@ -867,6 +879,7 @@ __export(store_exports2, {
867
879
  getBackend: () => getBackend,
868
880
  getBackendLimit: () => getBackendLimit,
869
881
  getBackendUsageInWindow: () => getBackendUsageInWindow,
882
+ getBookmark: () => getBookmark,
870
883
  getChatIdByAlias: () => getChatIdByAlias,
871
884
  getChatUsageByModel: () => getChatUsageByModel,
872
885
  getCwd: () => getCwd,
@@ -878,6 +891,7 @@ __export(store_exports2, {
878
891
  getMemoriesWithoutEmbeddings: () => getMemoriesWithoutEmbeddings,
879
892
  getMode: () => getMode,
880
893
  getModel: () => getModel,
894
+ getRecentBookmarks: () => getRecentBookmarks,
881
895
  getRecentMemories: () => getRecentMemories,
882
896
  getSessionId: () => getSessionId,
883
897
  getSessionStartedAt: () => getSessionStartedAt,
@@ -920,13 +934,15 @@ __export(store_exports2, {
920
934
  setThinkingLevel: () => setThinkingLevel,
921
935
  setVerboseLevel: () => setVerboseLevel,
922
936
  toggleTool: () => toggleTool,
937
+ touchBookmark: () => touchBookmark,
923
938
  updateHeartbeatTimestamps: () => updateHeartbeatTimestamps,
924
939
  updateJob: () => updateJob,
925
940
  updateJobEnabled: () => updateJobEnabled,
926
941
  updateJobLastRun: () => updateJobLastRun,
927
942
  updateJobNextRun: () => updateJobNextRun,
928
943
  updateMemoryEmbedding: () => updateMemoryEmbedding,
929
- updateSessionSummaryEmbedding: () => updateSessionSummaryEmbedding
944
+ updateSessionSummaryEmbedding: () => updateSessionSummaryEmbedding,
945
+ upsertBookmark: () => upsertBookmark
930
946
  });
931
947
  import Database from "better-sqlite3";
932
948
  function openDatabaseReadOnly() {
@@ -1288,6 +1304,16 @@ function initDatabase() {
1288
1304
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
1289
1305
  );
1290
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
+ `);
1291
1317
  try {
1292
1318
  db.exec("ALTER TABLE memories ADD COLUMN embedding TEXT");
1293
1319
  } catch {
@@ -1482,6 +1508,58 @@ function setCwd(chatId, cwd) {
1482
1508
  function clearCwd(chatId) {
1483
1509
  db.prepare("DELETE FROM chat_cwd WHERE chat_id = ?").run(chatId);
1484
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
+ }
1485
1563
  function getModel(chatId) {
1486
1564
  const row = db.prepare(
1487
1565
  "SELECT model FROM chat_model WHERE chat_id = ?"
@@ -2190,13 +2268,13 @@ var init_claude = __esm({
2190
2268
  displayName = "Claude";
2191
2269
  availableModels = {
2192
2270
  "claude-opus-4-6": {
2193
- label: "Opus 4.6 \u2014 most capable",
2271
+ label: "Opus 4.6 \u2014 most capable, 1M context",
2194
2272
  thinking: "adjustable",
2195
2273
  thinkingLevels: ["auto", "off", "low", "medium", "high"],
2196
2274
  defaultThinkingLevel: "medium"
2197
2275
  },
2198
2276
  "claude-sonnet-4-6": {
2199
- label: "Sonnet 4.6 \u2014 balanced (default)",
2277
+ label: "Sonnet 4.6 \u2014 balanced (default), 1M context",
2200
2278
  thinking: "adjustable",
2201
2279
  thinkingLevels: ["auto", "off", "low", "medium", "high"],
2202
2280
  defaultThinkingLevel: "medium"
@@ -2216,8 +2294,8 @@ var init_claude = __esm({
2216
2294
  "claude-haiku-4-5": { in: 0.8, out: 4, cache: 0.08 }
2217
2295
  };
2218
2296
  contextWindow = {
2219
- "claude-opus-4-6": 2e5,
2220
- "claude-sonnet-4-6": 2e5,
2297
+ "claude-opus-4-6": 1e6,
2298
+ "claude-sonnet-4-6": 1e6,
2221
2299
  "claude-haiku-4-5": 2e5
2222
2300
  };
2223
2301
  _resolvedPath = "";
@@ -4351,6 +4429,11 @@ function shutdownOrchestrator() {
4351
4429
  }
4352
4430
  function initOrchestrator() {
4353
4431
  cleanupOrphanedMcpConfigs();
4432
+ const db3 = getDb();
4433
+ const staleCount = cleanupStaleAgents(db3);
4434
+ if (staleCount > 0) {
4435
+ log(`[orchestrator] Cleared ${staleCount} stale agent(s) from previous run`);
4436
+ }
4354
4437
  log("[orchestrator] Initialized");
4355
4438
  }
4356
4439
  var activeProcesses, timeoutTimers, runnerLocks, notifyCallback;
@@ -8264,6 +8347,180 @@ var init_video = __esm({
8264
8347
  }
8265
8348
  });
8266
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
+
8267
8524
  // src/router.ts
8268
8525
  import { readFile as readFile5, writeFile as writeFile2, unlink as unlink2 } from "fs/promises";
8269
8526
  import { resolve as resolvePath } from "path";
@@ -8353,6 +8610,17 @@ async function handleMessage(msg, channel) {
8353
8610
  }
8354
8611
  }
8355
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
+ }
8356
8624
  switch (msg.type) {
8357
8625
  case "command":
8358
8626
  await handleCommand(msg, channel);
@@ -8372,6 +8640,9 @@ async function handleMessage(msg, channel) {
8372
8640
  }
8373
8641
  async function handleCommand(msg, channel) {
8374
8642
  const { chatId, command, commandArgs } = msg;
8643
+ if (command?.startsWith("/")) {
8644
+ return handleBackendCommand(command, chatId, channel);
8645
+ }
8375
8646
  switch (command) {
8376
8647
  case "start":
8377
8648
  case "help":
@@ -8506,15 +8777,7 @@ Tap to toggle:`,
8506
8777
  case "backend": {
8507
8778
  const requestedBackend = (commandArgs ?? "").trim().toLowerCase();
8508
8779
  if (requestedBackend && getAllBackendIds().includes(requestedBackend)) {
8509
- summarizeSession(chatId).catch(() => {
8510
- });
8511
- clearSession(chatId);
8512
- clearModel(chatId);
8513
- clearThinkingLevel(chatId);
8514
- setBackend(chatId, requestedBackend);
8515
- const adapter = getAdapter(requestedBackend);
8516
- logActivity(getDb(), { chatId, source: "telegram", eventType: "config_changed", summary: `Backend switched to ${adapter.displayName}`, detail: { field: "backend", value: requestedBackend } });
8517
- await channel.sendText(chatId, `Switched to <b>${adapter.displayName}</b>.`, "html");
8780
+ await sendBackendSwitchConfirmation(chatId, requestedBackend, channel);
8518
8781
  break;
8519
8782
  }
8520
8783
  const currentBackend = getBackend(chatId);
@@ -8538,15 +8801,7 @@ Tap to toggle:`,
8538
8801
  case "codex": {
8539
8802
  const backendId = command;
8540
8803
  if (getAllBackendIds().includes(backendId)) {
8541
- summarizeSession(chatId).catch(() => {
8542
- });
8543
- clearSession(chatId);
8544
- clearModel(chatId);
8545
- clearThinkingLevel(chatId);
8546
- setBackend(chatId, backendId);
8547
- const adapter = getAdapter(backendId);
8548
- logActivity(getDb(), { chatId, source: "telegram", eventType: "config_changed", summary: `Backend switched to ${adapter.displayName}`, detail: { field: "backend", value: backendId } });
8549
- await channel.sendText(chatId, `Switched to <b>${adapter.displayName}</b>.`, "html");
8804
+ await sendBackendSwitchConfirmation(chatId, backendId, channel);
8550
8805
  } else {
8551
8806
  await channel.sendText(chatId, `Backend "${command}" is not available.`, "plain");
8552
8807
  }
@@ -8719,11 +8974,28 @@ Use /summarizer auto, /summarizer off, or /summarizer <backend>:<model>`, "plain
8719
8974
  case "cwd": {
8720
8975
  if (!commandArgs) {
8721
8976
  const current = getCwd(chatId);
8722
- await channel.sendText(
8723
- chatId,
8724
- current ? `Working directory: ${current}` : "No working directory set. Usage: /cwd ~/projects/my-app",
8725
- "plain"
8726
- );
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
+ }
8727
8999
  return;
8728
9000
  }
8729
9001
  if (commandArgs === "reset" || commandArgs === "clear") {
@@ -8731,12 +9003,71 @@ Use /summarizer auto, /summarizer off, or /summarizer <backend>:<model>`, "plain
8731
9003
  await channel.sendText(chatId, "Working directory cleared. Using default.", "plain");
8732
9004
  return;
8733
9005
  }
8734
- const resolvedPath = commandArgs.startsWith("~") ? commandArgs.replace("~", process.env.HOME ?? "") : commandArgs;
8735
- setCwd(chatId, resolvedPath);
8736
- clearSession(chatId);
8737
- logActivity(getDb(), { chatId, source: "telegram", eventType: "config_changed", summary: `Working directory set to ${resolvedPath}`, detail: { field: "cwd", value: resolvedPath } });
8738
- await channel.sendText(chatId, `Working directory set to: ${resolvedPath}
8739
- 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");
8740
9071
  break;
8741
9072
  }
8742
9073
  case "memory": {
@@ -9241,7 +9572,7 @@ Use /skills to see it.`, "plain");
9241
9572
  lines.push("", "\u2501\u2501 <b>Built-in</b> \u2501\u2501");
9242
9573
  lines.push(` \u2705 <b>cc-claw</b> <i>Agent orchestrator (spawn, tasks, inbox)</i>`);
9243
9574
  }
9244
- const { execFile: execFile3 } = await import("child_process");
9575
+ const { execFile: execFile5 } = await import("child_process");
9245
9576
  const { homedir: homedir5 } = await import("os");
9246
9577
  const discoveryCwd = homedir5();
9247
9578
  const runnerResults = await Promise.allSettled(
@@ -9250,7 +9581,7 @@ Use /skills to see it.`, "plain");
9250
9581
  if (!listCmd.length) return Promise.resolve({ runner, output: "" });
9251
9582
  const exe = runner.getExecutablePath();
9252
9583
  return new Promise((resolve) => {
9253
- execFile3(exe, listCmd.slice(1), {
9584
+ execFile5(exe, listCmd.slice(1), {
9254
9585
  encoding: "utf-8",
9255
9586
  timeout: 3e4,
9256
9587
  cwd: discoveryCwd,
@@ -9697,31 +10028,65 @@ async function sendResponse(chatId, channel, text) {
9697
10028
  function isImageExt(ext) {
9698
10029
  return ["jpg", "jpeg", "png", "gif", "webp", "bmp", "svg"].includes(ext);
9699
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
+ }
9700
10071
  async function handleCallback(chatId, data, channel) {
9701
10072
  if (data.startsWith("backend:")) {
9702
10073
  const chosen = data.slice(8);
9703
10074
  if (!getAllBackendIds().includes(chosen)) return;
9704
10075
  const previous = getBackend(chatId);
9705
10076
  if (chosen === previous) {
9706
- const adapter2 = getAdapter(chosen);
9707
- await channel.sendText(chatId, `Already using ${adapter2.displayName}.`, "plain");
10077
+ const adapter = getAdapter(chosen);
10078
+ await channel.sendText(chatId, `Already using ${adapter.displayName}.`, "plain");
9708
10079
  return;
9709
10080
  }
9710
- summarizeSession(chatId).catch(() => {
9711
- });
9712
- clearSession(chatId);
9713
- clearModel(chatId);
9714
- clearThinkingLevel(chatId);
9715
- setBackend(chatId, chosen);
9716
- const adapter = getAdapter(chosen);
9717
- logActivity(getDb(), { chatId, source: "telegram", eventType: "config_changed", summary: `Backend switched to ${adapter.displayName}`, detail: { field: "backend", value: chosen } });
9718
- await channel.sendText(
9719
- chatId,
9720
- `Backend switched to ${adapter.displayName}.
9721
- Default model: ${adapter.defaultModel}
9722
- Session reset. Ready!`,
9723
- "plain"
9724
- );
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");
9725
10090
  } else if (data.startsWith("model:")) {
9726
10091
  const chosen = data.slice(6);
9727
10092
  let adapter;
@@ -9735,7 +10100,6 @@ Session reset. Ready!`,
9735
10100
  if (!modelInfo) return;
9736
10101
  setModel(chatId, chosen);
9737
10102
  clearThinkingLevel(chatId);
9738
- clearSession(chatId);
9739
10103
  logActivity(getDb(), { chatId, source: "telegram", eventType: "config_changed", summary: `Model switched to ${modelInfo.label}`, detail: { field: "model", value: chosen } });
9740
10104
  if (modelInfo.thinking === "adjustable" && modelInfo.thinkingLevels) {
9741
10105
  const thinkingButtons = modelInfo.thinkingLevels.map((level) => [{
@@ -9745,16 +10109,16 @@ Session reset. Ready!`,
9745
10109
  if (typeof channel.sendKeyboard === "function") {
9746
10110
  await channel.sendKeyboard(
9747
10111
  chatId,
9748
- `Model set to ${modelInfo.label}.
10112
+ `Model set to ${modelInfo.label}. Session continues.
9749
10113
 
9750
10114
  Select thinking/effort level:`,
9751
10115
  thinkingButtons
9752
10116
  );
9753
10117
  } else {
9754
- await channel.sendText(chatId, `Model set to ${modelInfo.label}. Session reset.`, "plain");
10118
+ await channel.sendText(chatId, `Model set to ${modelInfo.label}. Session continues.`, "plain");
9755
10119
  }
9756
10120
  } else {
9757
- await channel.sendText(chatId, `Model switched to ${modelInfo.label}. Session reset.`, "plain");
10121
+ await channel.sendText(chatId, `Model switched to ${modelInfo.label}. Session continues.`, "plain");
9758
10122
  }
9759
10123
  } else if (data.startsWith("thinking:")) {
9760
10124
  const level = data.slice(9);
@@ -9821,6 +10185,51 @@ ${PERM_MODES[chosen]}`,
9821
10185
  } else {
9822
10186
  await channel.sendText(chatId, "Preference not saved.", "plain");
9823
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);
9824
10233
  } else if (data.startsWith("skill:")) {
9825
10234
  const parts = data.slice(6).split(":");
9826
10235
  let skillName;
@@ -9858,6 +10267,101 @@ ${PERM_MODES[chosen]}`,
9858
10267
  await sendResponse(chatId, channel, response.text);
9859
10268
  }
9860
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
+ }
9861
10365
  function parseIntervalToMs(input) {
9862
10366
  const match = input.trim().match(/^(\d+)\s*(m|min|h|hr|hour|s|sec)$/i);
9863
10367
  if (!match) return null;
@@ -9925,6 +10429,9 @@ var init_router = __esm({
9925
10429
  init_registry();
9926
10430
  init_registry2();
9927
10431
  init_store3();
10432
+ init_guard();
10433
+ init_exec();
10434
+ init_backend_cmd();
9928
10435
  PERM_MODES = {
9929
10436
  yolo: "YOLO \u2014 all tools, full autopilot",
9930
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.7",
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",