cc-claw 0.3.3 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/dist/cli.js +235 -63
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -124,6 +124,7 @@ Read commands work offline (direct DB access). Write commands require the daemon
124
124
  | `/help` | All commands |
125
125
  | `/status` | Backend, model, session, usage |
126
126
  | `/newchat` | Start fresh (summarizes current session) |
127
+ | `/summarize` | Save session to memory without resetting |
127
128
  | `/stop` | Cancel running task |
128
129
 
129
130
  ### Backend & Model
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.3.3" : (() => {
51
+ VERSION = true ? "0.3.4" : (() => {
52
52
  try {
53
53
  return JSON.parse(readFileSync(join2(process.cwd(), "package.json"), "utf-8")).version ?? "unknown";
54
54
  } catch {
@@ -1143,6 +1143,7 @@ function initDatabase() {
1143
1143
  backend TEXT,
1144
1144
  model TEXT,
1145
1145
  thinking TEXT,
1146
+ timeout INTEGER,
1146
1147
  session_type TEXT NOT NULL DEFAULT 'isolated',
1147
1148
  channel TEXT,
1148
1149
  target TEXT,
@@ -1177,6 +1178,7 @@ function initDatabase() {
1177
1178
  backend TEXT,
1178
1179
  model TEXT,
1179
1180
  thinking TEXT,
1181
+ timeout INTEGER,
1180
1182
  session_type TEXT NOT NULL DEFAULT 'isolated',
1181
1183
  channel TEXT,
1182
1184
  target TEXT,
@@ -1257,6 +1259,10 @@ function initDatabase() {
1257
1259
  db.exec("ALTER TABLE jobs ADD COLUMN consecutive_failures INTEGER NOT NULL DEFAULT 0");
1258
1260
  } catch {
1259
1261
  }
1262
+ try {
1263
+ db.exec("ALTER TABLE jobs ADD COLUMN timeout INTEGER");
1264
+ } catch {
1265
+ }
1260
1266
  }
1261
1267
  } catch {
1262
1268
  }
@@ -1949,8 +1955,8 @@ function getBackendUsageInWindow(backend2, windowType) {
1949
1955
  function insertJob(params) {
1950
1956
  const result = db.prepare(`
1951
1957
  INSERT INTO jobs (schedule_type, cron, at_time, every_ms, description, chat_id,
1952
- backend, model, thinking, session_type, channel, target, delivery_mode, timezone)
1953
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1958
+ backend, model, thinking, timeout, session_type, channel, target, delivery_mode, timezone)
1959
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1954
1960
  `).run(
1955
1961
  params.scheduleType,
1956
1962
  params.cron ?? null,
@@ -1961,6 +1967,7 @@ function insertJob(params) {
1961
1967
  params.backend ?? null,
1962
1968
  params.model ?? null,
1963
1969
  params.thinking ?? null,
1970
+ params.timeout ?? null,
1964
1971
  params.sessionType ?? "isolated",
1965
1972
  params.channel ?? null,
1966
1973
  params.target ?? null,
@@ -2012,6 +2019,7 @@ function updateJob(id, fields) {
2012
2019
  backend: "backend",
2013
2020
  model: "model",
2014
2021
  thinking: "thinking",
2022
+ timeout: "timeout",
2015
2023
  sessionType: "session_type",
2016
2024
  channel: "channel",
2017
2025
  target: "target",
@@ -2275,7 +2283,7 @@ var init_store4 = __esm({
2275
2283
  ALL_TOOLS = ["Read", "Glob", "Grep", "Bash", "Write", "Edit", "WebFetch", "WebSearch", "Agent", "AskUserQuestion"];
2276
2284
  JOB_SELECT = `
2277
2285
  SELECT id, schedule_type as scheduleType, cron, at_time as atTime, every_ms as everyMs,
2278
- description, chat_id as chatId, backend, model, thinking,
2286
+ description, chat_id as chatId, backend, model, thinking, timeout,
2279
2287
  session_type as sessionType, channel, target, delivery_mode as deliveryMode,
2280
2288
  timezone, enabled, active, created_at as createdAt, last_run_at as lastRunAt,
2281
2289
  next_run_at as nextRunAt, consecutive_failures as consecutiveFailures
@@ -5230,9 +5238,14 @@ data: ${JSON.stringify(data)}
5230
5238
  `);
5231
5239
  };
5232
5240
  try {
5233
- const response = await askAgent2(chatId, body.message, cwd, (partial) => {
5234
- sendSSE("text", partial);
5235
- }, model2, mode);
5241
+ const response = await askAgent2(chatId, body.message, {
5242
+ cwd,
5243
+ onStream: (partial) => {
5244
+ sendSSE("text", partial);
5245
+ },
5246
+ model: model2,
5247
+ permMode: mode
5248
+ });
5236
5249
  if (response.usage) addUsage2(chatId, response.usage.input, response.usage.output, response.usage.cacheRead, model2 ?? "unknown");
5237
5250
  sendSSE("done", JSON.stringify({ text: response.text, usage: response.usage }));
5238
5251
  res.end();
@@ -5241,7 +5254,7 @@ data: ${JSON.stringify(data)}
5241
5254
  res.end();
5242
5255
  }
5243
5256
  } else {
5244
- const response = await askAgent2(chatId, body.message, cwd, void 0, model2, mode);
5257
+ const response = await askAgent2(chatId, body.message, { cwd, model: model2, permMode: mode });
5245
5258
  if (response.usage) addUsage2(chatId, response.usage.input, response.usage.output, response.usage.cacheRead, model2 ?? "unknown");
5246
5259
  return jsonResponse(res, { text: response.text, usage: response.usage, sessionId: response.sessionId });
5247
5260
  }
@@ -5696,24 +5709,32 @@ function stopAgent(chatId) {
5696
5709
  function isAgentActive(chatId) {
5697
5710
  return activeChats.has(chatId);
5698
5711
  }
5699
- function spawnQuery(adapter, config2, model2, cancelState, onStream, onToolAction, thinkingLevel) {
5712
+ function spawnQuery(adapter, config2, model2, cancelState, onStream, onToolAction, thinkingLevel, timeoutMs) {
5713
+ const effectiveTimeout = timeoutMs ?? SPAWN_TIMEOUT_MS;
5700
5714
  return new Promise((resolve, reject) => {
5701
5715
  const thinkingConfig = thinkingLevel && thinkingLevel !== "auto" ? adapter.applyThinkingConfig(thinkingLevel, model2) : void 0;
5702
5716
  const env = adapter.getEnv(thinkingConfig?.envOverrides);
5703
5717
  const finalArgs = thinkingConfig?.extraArgs ? [...config2.args, ...thinkingConfig.extraArgs] : config2.args;
5704
- log(`[agent:spawn] backend=${adapter.id} exe=${config2.executable} model=${model2} cwd=${config2.cwd ?? "(inherited)"}`);
5718
+ log(`[agent:spawn] backend=${adapter.id} exe=${config2.executable} model=${model2} timeout=${effectiveTimeout / 1e3}s cwd=${config2.cwd ?? "(inherited)"}`);
5705
5719
  const proc = spawn4(config2.executable, finalArgs, {
5706
5720
  env,
5707
5721
  stdio: ["ignore", "pipe", "pipe"],
5708
5722
  ...config2.cwd ? { cwd: config2.cwd } : {}
5709
5723
  });
5710
5724
  cancelState.process = proc;
5725
+ let timedOut = false;
5726
+ let sigkillTimer;
5711
5727
  const spawnTimeout = setTimeout(() => {
5712
- warn(`[agent] Spawn timeout after ${SPAWN_TIMEOUT_MS / 1e3}s for ${adapter.id} \u2014 killing process`);
5713
- cancelState.cancelled = true;
5728
+ timedOut = true;
5729
+ warn(`[agent] Spawn timeout after ${effectiveTimeout / 1e3}s for ${adapter.id} \u2014 killing process`);
5714
5730
  proc.kill("SIGTERM");
5715
- setTimeout(() => proc.kill("SIGKILL"), 3e3);
5716
- }, SPAWN_TIMEOUT_MS);
5731
+ sigkillTimer = setTimeout(() => {
5732
+ try {
5733
+ proc.kill("SIGKILL");
5734
+ } catch {
5735
+ }
5736
+ }, 3e3);
5737
+ }, effectiveTimeout);
5717
5738
  let resultText = "";
5718
5739
  let accumulatedText = "";
5719
5740
  let sessionId;
@@ -5808,12 +5829,17 @@ function spawnQuery(adapter, config2, model2, cancelState, onStream, onToolActio
5808
5829
  });
5809
5830
  proc.on("error", (err) => {
5810
5831
  clearTimeout(spawnTimeout);
5832
+ if (sigkillTimer) clearTimeout(sigkillTimer);
5811
5833
  rl2.close();
5812
5834
  cancelState.process = void 0;
5813
5835
  reject(err);
5814
5836
  });
5815
5837
  proc.on("close", (code, signal) => {
5816
5838
  clearTimeout(spawnTimeout);
5839
+ if (sigkillTimer) {
5840
+ clearTimeout(sigkillTimer);
5841
+ sigkillTimer = void 0;
5842
+ }
5817
5843
  if (cancelState.killTimer) {
5818
5844
  clearTimeout(cancelState.killTimer);
5819
5845
  cancelState.killTimer = void 0;
@@ -5823,6 +5849,10 @@ function spawnQuery(adapter, config2, model2, cancelState, onStream, onToolActio
5823
5849
  const stderr = Buffer.concat(stderrChunks).toString().trim();
5824
5850
  if (stderr) warn(`[agent] stderr: ${stderr.slice(0, 500)}`);
5825
5851
  }
5852
+ if (timedOut) {
5853
+ reject(new Error(`Spawn timeout after ${effectiveTimeout / 1e3}s`));
5854
+ return;
5855
+ }
5826
5856
  if (code && code !== 0 && !cancelState.cancelled && !resultText) {
5827
5857
  const stderr = Buffer.concat(stderrChunks).toString().trim();
5828
5858
  reject(new Error(`CLI exited with code ${code}${stderr ? `: ${stderr.slice(0, 500)}` : ""}`));
@@ -5832,10 +5862,11 @@ function spawnQuery(adapter, config2, model2, cancelState, onStream, onToolActio
5832
5862
  });
5833
5863
  });
5834
5864
  }
5835
- function askAgent(chatId, userMessage, cwd, onStream, model2, permMode, onToolAction, bootstrapTier) {
5836
- return withChatLock(chatId, () => askAgentImpl(chatId, userMessage, cwd, onStream, model2, permMode, onToolAction, bootstrapTier));
5865
+ function askAgent(chatId, userMessage, opts) {
5866
+ return withChatLock(chatId, () => askAgentImpl(chatId, userMessage, opts));
5837
5867
  }
5838
- async function askAgentImpl(chatId, userMessage, cwd, onStream, model2, permMode, onToolAction, bootstrapTier) {
5868
+ async function askAgentImpl(chatId, userMessage, opts) {
5869
+ const { cwd, onStream, model: model2, permMode, onToolAction, bootstrapTier, timeoutMs } = opts ?? {};
5839
5870
  const adapter = getAdapterForChat(chatId);
5840
5871
  const mode = permMode ?? "yolo";
5841
5872
  const thinkingLevel = getThinkingLevel(chatId);
@@ -5876,13 +5907,13 @@ async function askAgentImpl(chatId, userMessage, cwd, onStream, model2, permMode
5876
5907
  const resolvedModel = model2 ?? adapter.defaultModel;
5877
5908
  let result = { resultText: "", sessionId: void 0, input: 0, output: 0, cacheRead: 0, sawToolEvents: false, sawResultEvent: false };
5878
5909
  try {
5879
- result = await spawnQuery(adapter, configWithSession, resolvedModel, cancelState, onStream, onToolAction, thinkingLevel);
5910
+ result = await spawnQuery(adapter, configWithSession, resolvedModel, cancelState, onStream, onToolAction, thinkingLevel, timeoutMs);
5880
5911
  const wasEmptyResponse = !result.resultText && !result.sawToolEvents && !result.sawResultEvent;
5881
5912
  if (wasEmptyResponse && !cancelState.cancelled && existingSessionId) {
5882
5913
  warn(`[agent] No result with session ${existingSessionId} for chat ${chatId} \u2014 retrying fresh`);
5883
5914
  await summarizeSession(chatId);
5884
5915
  clearSession(chatId);
5885
- result = await spawnQuery(adapter, baseConfig, resolvedModel, cancelState, onStream, onToolAction, thinkingLevel);
5916
+ result = await spawnQuery(adapter, baseConfig, resolvedModel, cancelState, onStream, onToolAction, thinkingLevel, timeoutMs);
5886
5917
  }
5887
5918
  } finally {
5888
5919
  activeChats.delete(chatId);
@@ -6429,7 +6460,8 @@ async function runWithRetry(job, model2, runId, t0) {
6429
6460
  }
6430
6461
  for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
6431
6462
  try {
6432
- const response = await askAgent(chatId, job.description, void 0, void 0, model2, void 0, void 0, "slim");
6463
+ const timeoutMs = job.timeout ? job.timeout * 1e3 : void 0;
6464
+ const response = await askAgent(chatId, job.description, { model: model2, bootstrapTier: "slim", timeoutMs });
6433
6465
  return response;
6434
6466
  } catch (err) {
6435
6467
  lastError = err;
@@ -7069,6 +7101,7 @@ var init_telegram2 = __esm({
7069
7101
  { command: "help", description: "Show available commands" },
7070
7102
  { command: "status", description: "Session, backend, model, and usage" },
7071
7103
  { command: "newchat", description: "Start a fresh conversation" },
7104
+ { command: "summarize", description: "Save session to memory (or 'all' for pre-restart)" },
7072
7105
  { command: "stop", description: "Cancel the current running task" },
7073
7106
  // Backend & model
7074
7107
  { command: "backend", description: "Switch AI backend (Claude/Gemini/Codex)" },
@@ -7856,7 +7889,7 @@ async function runHeartbeat(chatId, config2) {
7856
7889
  cleanExpiredWatches();
7857
7890
  const prompt = assembleHeartbeatPrompt(chatId);
7858
7891
  try {
7859
- const response = await askAgent(chatId, prompt, void 0, void 0, void 0, void 0, void 0, "heartbeat");
7892
+ const response = await askAgent(chatId, prompt, { bootstrapTier: "heartbeat" });
7860
7893
  if (response.usage) {
7861
7894
  let heartbeatModel;
7862
7895
  try {
@@ -7988,6 +8021,52 @@ var init_heartbeat = __esm({
7988
8021
  }
7989
8022
  });
7990
8023
 
8024
+ // src/format-time.ts
8025
+ function formatLocalDate(utcDatetime) {
8026
+ const d = parseUtcDatetime(utcDatetime);
8027
+ if (!d) return utcDatetime.split("T")[0] ?? utcDatetime.split(" ")[0];
8028
+ const year = d.getFullYear();
8029
+ const month = String(d.getMonth() + 1).padStart(2, "0");
8030
+ const day = String(d.getDate()).padStart(2, "0");
8031
+ return `${year}-${month}-${day}`;
8032
+ }
8033
+ function formatLocalDateTime(utcDatetime) {
8034
+ const d = parseUtcDatetime(utcDatetime);
8035
+ if (!d) return utcDatetime;
8036
+ const year = d.getFullYear();
8037
+ const month = String(d.getMonth() + 1).padStart(2, "0");
8038
+ const day = String(d.getDate()).padStart(2, "0");
8039
+ const hours = String(d.getHours()).padStart(2, "0");
8040
+ const minutes = String(d.getMinutes()).padStart(2, "0");
8041
+ const tzAbbr = getTimezoneAbbr(d);
8042
+ return `${year}-${month}-${day} ${hours}:${minutes} ${tzAbbr}`;
8043
+ }
8044
+ function parseUtcDatetime(utcDatetime) {
8045
+ if (!utcDatetime) return null;
8046
+ const normalized = utcDatetime.includes("T") ? utcDatetime : utcDatetime.replace(" ", "T");
8047
+ const withZ = normalized.endsWith("Z") ? normalized : normalized + "Z";
8048
+ const d = new Date(withZ);
8049
+ return isNaN(d.getTime()) ? null : d;
8050
+ }
8051
+ function getTimezoneAbbr(date) {
8052
+ try {
8053
+ const parts = new Intl.DateTimeFormat("en-US", {
8054
+ timeZone: systemTimezone,
8055
+ timeZoneName: "short"
8056
+ }).formatToParts(date);
8057
+ return parts.find((p) => p.type === "timeZoneName")?.value ?? "";
8058
+ } catch {
8059
+ return "";
8060
+ }
8061
+ }
8062
+ var systemTimezone;
8063
+ var init_format_time = __esm({
8064
+ "src/format-time.ts"() {
8065
+ "use strict";
8066
+ systemTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
8067
+ }
8068
+ });
8069
+
7991
8070
  // src/voice/stt.ts
7992
8071
  import crypto from "crypto";
7993
8072
  import { execFile as execFile2 } from "child_process";
@@ -8092,10 +8171,13 @@ var init_stt = __esm({
8092
8171
  });
8093
8172
 
8094
8173
  // src/scheduler/types.ts
8095
- var COMMON_TIMEZONES;
8174
+ var TIMEOUT_MIN_SECONDS, TIMEOUT_MAX_SECONDS, TIMEOUT_DEFAULT_SECONDS, COMMON_TIMEZONES;
8096
8175
  var init_types2 = __esm({
8097
8176
  "src/scheduler/types.ts"() {
8098
8177
  "use strict";
8178
+ TIMEOUT_MIN_SECONDS = 30;
8179
+ TIMEOUT_MAX_SECONDS = 3600;
8180
+ TIMEOUT_DEFAULT_SECONDS = 600;
8099
8181
  COMMON_TIMEZONES = {
8100
8182
  "US/Eastern": "America/New_York",
8101
8183
  "US/Central": "America/Chicago",
@@ -8311,8 +8393,23 @@ async function handleWizardText(chatId, text, channel) {
8311
8393
  case "thinking": {
8312
8394
  const level = text.trim().toLowerCase();
8313
8395
  pending.thinking = level || "auto";
8314
- pending.step = "session";
8315
- await promptSession(chatId, channel);
8396
+ pending.step = "timeout";
8397
+ await promptTimeout(chatId, channel);
8398
+ break;
8399
+ }
8400
+ case "timeout": {
8401
+ const val = parseInt(text.trim(), 10);
8402
+ if (text.trim().toLowerCase() === "default") {
8403
+ pending.timeout = void 0;
8404
+ pending.step = "session";
8405
+ await promptSession(chatId, channel);
8406
+ } else if (isNaN(val) || val < TIMEOUT_MIN_SECONDS || val > TIMEOUT_MAX_SECONDS) {
8407
+ await channel.sendText(chatId, `Invalid timeout. Enter a value between ${TIMEOUT_MIN_SECONDS} and ${TIMEOUT_MAX_SECONDS} seconds, or "default" for ${TIMEOUT_DEFAULT_SECONDS}s.`, "plain");
8408
+ } else {
8409
+ pending.timeout = val;
8410
+ pending.step = "session";
8411
+ await promptSession(chatId, channel);
8412
+ }
8316
8413
  break;
8317
8414
  }
8318
8415
  case "session": {
@@ -8390,7 +8487,18 @@ async function handleWizardCallback(chatId, data, channel) {
8390
8487
  await promptThinking(chatId, channel);
8391
8488
  } else if (data.startsWith("sched:thinking:")) {
8392
8489
  pending.thinking = data.slice(15);
8490
+ pending.step = "timeout";
8491
+ await promptTimeout(chatId, channel);
8492
+ } else if (data.startsWith("sched:timeout:")) {
8493
+ const val = data.slice(14);
8494
+ if (val === "default") {
8495
+ pending.timeout = void 0;
8496
+ } else {
8497
+ pending.timeout = parseInt(val, 10);
8498
+ }
8393
8499
  pending.step = "session";
8500
+ const label2 = pending.timeout ? `${pending.timeout}s (${Math.round(pending.timeout / 60)} min)` : `Default (${TIMEOUT_DEFAULT_SECONDS}s)`;
8501
+ await channel.sendText(chatId, `Timeout: ${label2}`, "plain");
8394
8502
  await promptSession(chatId, channel);
8395
8503
  } else if (data.startsWith("sched:session:")) {
8396
8504
  pending.sessionType = data.slice(14);
@@ -8484,10 +8592,27 @@ async function promptThinking(chatId, channel) {
8484
8592
  await channel.sendKeyboard(chatId, "Thinking/effort level for this job?", buttons);
8485
8593
  } else {
8486
8594
  pending.thinking = "auto";
8487
- pending.step = "session";
8488
- await promptSession(chatId, channel);
8595
+ pending.step = "timeout";
8596
+ await promptTimeout(chatId, channel);
8489
8597
  }
8490
8598
  }
8599
+ async function promptTimeout(chatId, channel) {
8600
+ if (typeof channel.sendKeyboard !== "function") {
8601
+ await channel.sendText(chatId, `Job timeout in seconds (${TIMEOUT_MIN_SECONDS}-${TIMEOUT_MAX_SECONDS}), or "default" for ${TIMEOUT_DEFAULT_SECONDS}s:`, "plain");
8602
+ return;
8603
+ }
8604
+ await channel.sendKeyboard(chatId, "Maximum runtime for this job?", [
8605
+ [
8606
+ { label: `5 min (300s)`, data: "sched:timeout:300" },
8607
+ { label: `10 min (600s)`, data: "sched:timeout:600" }
8608
+ ],
8609
+ [
8610
+ { label: `15 min (900s)`, data: "sched:timeout:900" },
8611
+ { label: `30 min (1800s)`, data: "sched:timeout:1800" }
8612
+ ],
8613
+ [{ label: `Default (${TIMEOUT_DEFAULT_SECONDS}s)`, data: "sched:timeout:default" }]
8614
+ ]);
8615
+ }
8491
8616
  async function promptSession(chatId, channel) {
8492
8617
  if (typeof channel.sendKeyboard !== "function") {
8493
8618
  await channel.sendText(chatId, "Session type: isolated (fresh, no history) or main (uses conversation context)?", "plain");
@@ -8523,6 +8648,7 @@ async function promptConfirm(chatId, channel) {
8523
8648
  const pending = pendingJobs.get(chatId);
8524
8649
  if (!pending) return;
8525
8650
  const backendName = pending.backend ? getAdapter(pending.backend).displayName : "default";
8651
+ const timeoutLabel = pending.timeout ? `${pending.timeout}s (${Math.round(pending.timeout / 60)} min)` : `Default (${TIMEOUT_DEFAULT_SECONDS}s)`;
8526
8652
  const lines = [
8527
8653
  "Job configuration:",
8528
8654
  "",
@@ -8532,6 +8658,7 @@ async function promptConfirm(chatId, channel) {
8532
8658
  ` Backend: ${backendName}`,
8533
8659
  ` Model: ${pending.model ?? "default"}`,
8534
8660
  ` Thinking: ${pending.thinking ?? "auto"}`,
8661
+ ` Timeout: ${timeoutLabel}`,
8535
8662
  ` Session: ${pending.sessionType ?? "isolated"}`,
8536
8663
  ` Delivery: ${pending.deliveryMode ?? "announce"}`
8537
8664
  ];
@@ -8562,6 +8689,7 @@ async function finalizeJob(chatId, channel) {
8562
8689
  backend: pending.backend ?? null,
8563
8690
  model: pending.model ?? null,
8564
8691
  thinking: pending.thinking ?? null,
8692
+ timeout: pending.timeout ?? null,
8565
8693
  sessionType: pending.sessionType ?? "isolated",
8566
8694
  channel: pending.channel ?? null,
8567
8695
  target: pending.target ?? null,
@@ -8622,6 +8750,7 @@ async function startEditWizard(chatId, jobId, channel) {
8622
8750
  backend: job.backend,
8623
8751
  model: job.model ?? void 0,
8624
8752
  thinking: job.thinking ?? void 0,
8753
+ timeout: job.timeout ?? void 0,
8625
8754
  sessionType: job.sessionType,
8626
8755
  deliveryMode: job.deliveryMode,
8627
8756
  channel: job.channel ?? void 0,
@@ -9006,7 +9135,7 @@ async function handleCommand(msg, channel) {
9006
9135
  case "help":
9007
9136
  await channel.sendText(
9008
9137
  chatId,
9009
- "Hey! I'm CC-Claw \u2014 your personal AI assistant on Telegram.\n\nI use AI coding CLIs (Claude, Gemini, Codex) as my brain. Just send me a message to get started.\n\nCommands:\n/backend [name] - Switch AI backend (or /claude /gemini /codex)\n/model - Switch model for active backend\n/summarizer - Configure session summarization model\n/status - Show session, model, backend, and usage\n/cost - Show estimated API cost (use /cost all for all-time)\n/usage - Show usage per backend with limits\n/limits - Configure usage limits per backend\n/newchat - Start a fresh conversation\n/cwd <path> - Set working directory\n/cwd - Show current working directory\n/memory - List stored memories\n/forget <keyword> - Remove a memory\n/voice - Toggle voice responses\n/cron <description> - Schedule a task (or /schedule)\n/cron - List scheduled jobs (or /jobs)\n/cron cancel <id> - Cancel a job\n/cron pause <id> - Pause a job\n/cron resume <id> - Resume a job\n/cron run <id> - Trigger a job now\n/cron runs [id] - View run history\n/cron edit <id> - Edit a job\n/cron health - Scheduler health\n/skills - List skills from all backends\n/skill-install <url> - Install a skill from GitHub\n/setup-profile - Set up your user profile\n/chats - List authorized chats and aliases\n/heartbeat - Proactive awareness (on/off/interval/hours)\n/history - List recent session summaries\n/stop - Cancel the current running task\n/tools - Configure which tools the agent can use\n/permissions - Switch permission mode (yolo/safe/readonly/plan)\n/verbose - Tool visibility (off/normal/verbose)\n/agents - List active sub-agents\n/tasks - Show task board for current orchestration\n/stopagent <id> - Cancel a specific sub-agent\n/stopall - Cancel all sub-agents in this chat\n/runners - List registered CLI runners\n/mcps - List registered MCP servers\n/help - Show this message",
9138
+ "Hey! I'm CC-Claw \u2014 your personal AI assistant on Telegram.\n\nI use AI coding CLIs (Claude, Gemini, Codex) as my brain. Just send me a message to get started.\n\nCommands:\n/backend [name] - Switch AI backend (or /claude /gemini /codex)\n/model - Switch model for active backend\n/summarizer - Configure session summarization model\n/status - Show session, model, backend, and usage\n/cost - Show estimated API cost (use /cost all for all-time)\n/usage - Show usage per backend with limits\n/limits - Configure usage limits per backend\n/newchat - Start a fresh conversation\n/summarize - Save session to memory (without resetting)\n/summarize all - Summarize all pending sessions (pre-restart)\n/cwd <path> - Set working directory\n/cwd - Show current working directory\n/memory - List stored memories\n/forget <keyword> - Remove a memory\n/voice - Toggle voice responses\n/cron <description> - Schedule a task (or /schedule)\n/cron - List scheduled jobs (or /jobs)\n/cron cancel <id> - Cancel a job\n/cron pause <id> - Pause a job\n/cron resume <id> - Resume a job\n/cron run <id> - Trigger a job now\n/cron runs [id] - View run history\n/cron edit <id> - Edit a job\n/cron health - Scheduler health\n/skills - List skills from all backends\n/skill-install <url> - Install a skill from GitHub\n/setup-profile - Set up your user profile\n/chats - List authorized chats and aliases\n/heartbeat - Proactive awareness (on/off/interval/hours)\n/history - List recent session summaries\n/stop - Cancel the current running task\n/tools - Configure which tools the agent can use\n/permissions - Switch permission mode (yolo/safe/readonly/plan)\n/verbose - Tool visibility (off/normal/verbose)\n/agents - List active sub-agents\n/tasks - Show task board for current orchestration\n/stopagent <id> - Cancel a specific sub-agent\n/stopall - Cancel all sub-agents in this chat\n/runners - List registered CLI runners\n/mcps - List registered MCP servers\n/help - Show this message",
9010
9139
  "plain"
9011
9140
  );
9012
9141
  break;
@@ -9088,6 +9217,32 @@ Tap to toggle:`,
9088
9217
  await channel.sendText(chatId, msg2, "plain");
9089
9218
  break;
9090
9219
  }
9220
+ case "summarize": {
9221
+ if (commandArgs?.toLowerCase() === "all") {
9222
+ const pendingIds = getLoggedChatIds();
9223
+ if (pendingIds.length === 0) {
9224
+ await channel.sendText(chatId, "No pending sessions to summarize.", "plain");
9225
+ break;
9226
+ }
9227
+ await channel.sendText(chatId, `Summarizing ${pendingIds.length} pending session(s)...`, "plain");
9228
+ await summarizeAllPending();
9229
+ await channel.sendText(chatId, `Done. ${pendingIds.length} session(s) summarized and saved to memory.`, "plain");
9230
+ } else {
9231
+ const pairs = getMessagePairCount(chatId);
9232
+ if (pairs < 2) {
9233
+ await channel.sendText(chatId, "Not enough conversation to summarize (need at least 2 exchanges). Session log is preserved.", "plain");
9234
+ break;
9235
+ }
9236
+ await channel.sendText(chatId, `Summarizing current session (${pairs} exchanges)...`, "plain");
9237
+ const success2 = await summarizeSession(chatId);
9238
+ if (success2) {
9239
+ await channel.sendText(chatId, "Session summarized and saved to memory. Session continues (use /newchat to also reset).", "plain");
9240
+ } else {
9241
+ await channel.sendText(chatId, "Summarization failed. Session log preserved for retry.", "plain");
9242
+ }
9243
+ }
9244
+ break;
9245
+ }
9091
9246
  case "status": {
9092
9247
  const sessionId = getSessionId(chatId);
9093
9248
  const cwd = getCwd(chatId);
@@ -9564,7 +9719,7 @@ ${lines.join("\n")}`, "plain");
9564
9719
  Error: ${r.error.slice(0, 100)}` : "";
9565
9720
  const usage2 = r.usageInput ? `
9566
9721
  Tokens: ${r.usageInput}in / ${r.usageOutput}out` : "";
9567
- return `#${r.jobId} [${r.status}] ${r.startedAt}${duration}${error3}${usage2}`;
9722
+ return `#${r.jobId} [${r.status}] ${formatLocalDateTime(r.startedAt)}${duration}${error3}${usage2}`;
9568
9723
  });
9569
9724
  await channel.sendText(chatId, lines.join("\n\n"), "plain");
9570
9725
  break;
@@ -9591,7 +9746,7 @@ ${lines.join("\n")}`, "plain");
9591
9746
  return;
9592
9747
  }
9593
9748
  const lines = summaries.map((s) => {
9594
- const date = s.created_at.split("T")[0] ?? s.created_at.split(" ")[0];
9749
+ const date = formatLocalDate(s.created_at);
9595
9750
  const shortSummary = s.summary.length > 200 ? s.summary.slice(0, 200) + "\u2026" : s.summary;
9596
9751
  return `${date} (${s.message_count} msgs)
9597
9752
  ${shortSummary}
@@ -10047,7 +10202,7 @@ Use /skills to see it.`, "plain");
10047
10202
  }
10048
10203
  const runLines = cronRuns2.map((r) => {
10049
10204
  const dur = r.durationMs ? ` (${(r.durationMs / 1e3).toFixed(1)}s)` : "";
10050
- return `#${r.jobId} [${r.status}] ${r.startedAt}${dur}`;
10205
+ return `#${r.jobId} [${r.status}] ${formatLocalDateTime(r.startedAt)}${dur}`;
10051
10206
  });
10052
10207
  await channel.sendText(chatId, runLines.join("\n\n"), "plain");
10053
10208
  break;
@@ -10096,7 +10251,7 @@ async function handleVoice(msg, channel) {
10096
10251
  const vModel = resolveModel(chatId);
10097
10252
  const vVerbose = getVerboseLevel(chatId);
10098
10253
  const vToolCb = vVerbose !== "off" ? makeToolActionCallback(chatId, channel, vVerbose) : void 0;
10099
- const response = await askAgent(chatId, transcript, getCwd(chatId), void 0, vModel, mode, vToolCb);
10254
+ const response = await askAgent(chatId, transcript, { cwd: getCwd(chatId), model: vModel, permMode: mode, onToolAction: vToolCb });
10100
10255
  if (response.usage) addUsage(chatId, response.usage.input, response.usage.output, response.usage.cacheRead, vModel);
10101
10256
  await sendResponse(chatId, channel, response.text);
10102
10257
  } catch (err) {
@@ -10143,7 +10298,7 @@ Summarize this for the user.`;
10143
10298
  const vMode = getMode(chatId);
10144
10299
  const vidVerbose = getVerboseLevel(chatId);
10145
10300
  const vidToolCb = vidVerbose !== "off" ? makeToolActionCallback(chatId, channel, vidVerbose) : void 0;
10146
- const response2 = await askAgent(chatId, prompt2, getCwd(chatId), void 0, vidModel, vMode, vidToolCb);
10301
+ const response2 = await askAgent(chatId, prompt2, { cwd: getCwd(chatId), model: vidModel, permMode: vMode, onToolAction: vidToolCb });
10147
10302
  if (response2.usage) addUsage(chatId, response2.usage.input, response2.usage.output, response2.usage.cacheRead, vidModel);
10148
10303
  await sendResponse(chatId, channel, response2.text);
10149
10304
  return;
@@ -10184,7 +10339,7 @@ ${content}
10184
10339
  const mMode = getMode(chatId);
10185
10340
  const mVerbose = getVerboseLevel(chatId);
10186
10341
  const mToolCb = mVerbose !== "off" ? makeToolActionCallback(chatId, channel, mVerbose) : void 0;
10187
- const response = await askAgent(chatId, prompt, getCwd(chatId), void 0, mediaModel, mMode, mToolCb);
10342
+ const response = await askAgent(chatId, prompt, { cwd: getCwd(chatId), model: mediaModel, permMode: mMode, onToolAction: mToolCb });
10188
10343
  if (response.usage) addUsage(chatId, response.usage.input, response.usage.output, response.usage.cacheRead, mediaModel);
10189
10344
  await sendResponse(chatId, channel, response.text);
10190
10345
  if (tempFilePath) {
@@ -10249,7 +10404,7 @@ async function handleText(msg, channel) {
10249
10404
  const tMode = getMode(chatId);
10250
10405
  const tVerbose = getVerboseLevel(chatId);
10251
10406
  const tToolCb = tVerbose !== "off" ? makeToolActionCallback(chatId, channel, tVerbose) : void 0;
10252
- const response = await askAgent(chatId, text, getCwd(chatId), void 0, model2, tMode, tToolCb);
10407
+ const response = await askAgent(chatId, text, { cwd: getCwd(chatId), model: model2, permMode: tMode, onToolAction: tToolCb });
10253
10408
  if (response.usage) addUsage(chatId, response.usage.input, response.usage.output, response.usage.cacheRead, model2);
10254
10409
  await sendResponse(chatId, channel, response.text);
10255
10410
  } catch (err) {
@@ -10620,7 +10775,7 @@ ${PERM_MODES[chosen]}`,
10620
10775
  const sMode = getMode(chatId);
10621
10776
  const sVerbose = getVerboseLevel(chatId);
10622
10777
  const sToolCb = sVerbose !== "off" ? makeToolActionCallback(chatId, channel, sVerbose) : void 0;
10623
- const response = await askAgent(chatId, skillContent, getCwd(chatId), void 0, skillModel, sMode, sToolCb);
10778
+ const response = await askAgent(chatId, skillContent, { cwd: getCwd(chatId), model: skillModel, permMode: sMode, onToolAction: sToolCb });
10624
10779
  if (response.usage) addUsage(chatId, response.usage.input, response.usage.output, response.usage.cacheRead, skillModel);
10625
10780
  await sendResponse(chatId, channel, response.text);
10626
10781
  }
@@ -10773,10 +10928,12 @@ var init_router = __esm({
10773
10928
  init_profile();
10774
10929
  init_heartbeat();
10775
10930
  init_log();
10931
+ init_format_time();
10776
10932
  init_agent();
10777
10933
  init_stt();
10778
10934
  init_store4();
10779
10935
  init_summarize();
10936
+ init_session_log();
10780
10937
  init_backends();
10781
10938
  init_cron();
10782
10939
  init_wizard();
@@ -12420,7 +12577,9 @@ async function doctorCommand(globalOpts, localOpts) {
12420
12577
  lines.push(` ${parts.join(", ")}`);
12421
12578
  const fixable = r.checks.filter((c) => c.fix);
12422
12579
  if (fixable.length > 0 && !localOpts.fix) {
12423
- lines.push(muted(" Run cc-claw doctor --fix to attempt repairs."));
12580
+ for (const f of fixable) {
12581
+ lines.push(muted(` Fix: ${f.fix}`));
12582
+ }
12424
12583
  }
12425
12584
  }
12426
12585
  lines.push("");
@@ -12723,7 +12882,7 @@ async function memoryHistory(globalOpts, opts) {
12723
12882
  `;
12724
12883
  const lines = ["", divider(`Session History (${sums.length})`), ""];
12725
12884
  for (const s of sums) {
12726
- const date = s.created_at.split("T")[0] ?? s.created_at.split(" ")[0];
12885
+ const date = formatLocalDateTime(s.created_at);
12727
12886
  lines.push(` ${date} (${s.message_count} msgs)`);
12728
12887
  lines.push(` ${s.summary}`);
12729
12888
  lines.push(` Topics: ${muted(s.topics)}`);
@@ -12738,6 +12897,7 @@ var init_memory = __esm({
12738
12897
  init_format();
12739
12898
  init_paths();
12740
12899
  init_resolve_chat();
12900
+ init_format_time();
12741
12901
  }
12742
12902
  });
12743
12903
 
@@ -12752,6 +12912,15 @@ __export(cron_exports2, {
12752
12912
  cronRuns: () => cronRuns
12753
12913
  });
12754
12914
  import { existsSync as existsSync24 } from "fs";
12915
+ function parseAndValidateTimeout(raw) {
12916
+ if (!raw) return null;
12917
+ const val = parseInt(raw, 10);
12918
+ if (isNaN(val) || val < TIMEOUT_MIN_SECONDS || val > TIMEOUT_MAX_SECONDS) {
12919
+ outputError("INVALID_TIMEOUT", `Timeout must be between ${TIMEOUT_MIN_SECONDS} and ${TIMEOUT_MAX_SECONDS} seconds (got: ${raw})`);
12920
+ process.exit(1);
12921
+ }
12922
+ return val;
12923
+ }
12755
12924
  async function cronList(globalOpts) {
12756
12925
  if (!existsSync24(DB_PATH)) {
12757
12926
  outputError("DB_NOT_FOUND", "Database not found.");
@@ -12774,6 +12943,7 @@ async function cronList(globalOpts) {
12774
12943
  lines.push(` ${statusDot(status)} #${j.id} [${status}] ${schedule2}${tz}`);
12775
12944
  lines.push(` ${j.description}`);
12776
12945
  if (j.backend) lines.push(` Backend: ${j.backend}${j.model ? ` / ${j.model}` : ""}`);
12946
+ if (j.timeout) lines.push(` Timeout: ${j.timeout}s`);
12777
12947
  if (j.next_run_at) lines.push(` Next: ${muted(j.next_run_at)}`);
12778
12948
  lines.push("");
12779
12949
  }
@@ -12810,7 +12980,7 @@ async function cronHealth(globalOpts) {
12810
12980
  });
12811
12981
  }
12812
12982
  async function cronCreate(globalOpts, opts) {
12813
- const { isDaemonRunning: isDaemonRunning2, apiPost: apiPost2 } = await Promise.resolve().then(() => (init_api_client(), api_client_exports));
12983
+ const { isDaemonRunning: isDaemonRunning2 } = await Promise.resolve().then(() => (init_api_client(), api_client_exports));
12814
12984
  if (!await isDaemonRunning2()) {
12815
12985
  outputError("DAEMON_OFFLINE", "CC-Claw daemon is not running.\n\n Start it with: cc-claw service start");
12816
12986
  process.exit(1);
@@ -12821,10 +12991,9 @@ async function cronCreate(globalOpts, opts) {
12821
12991
  }
12822
12992
  const chatId = resolveChatId(globalOpts);
12823
12993
  const { success: successFmt } = await Promise.resolve().then(() => (init_format(), format_exports));
12994
+ const timeout = parseAndValidateTimeout(opts.timeout);
12824
12995
  try {
12825
- const { openDatabaseReadOnly: openDatabaseReadOnly2 } = await Promise.resolve().then(() => (init_store4(), store_exports3));
12826
- const { getDb: getDb2 } = await Promise.resolve().then(() => (init_store4(), store_exports3));
12827
- const db3 = getDb2();
12996
+ const { insertJob: insertJob2 } = await Promise.resolve().then(() => (init_store4(), store_exports3));
12828
12997
  const schedType = opts.cron ? "cron" : opts.at ? "at" : "every";
12829
12998
  let everyMs = null;
12830
12999
  if (opts.every) {
@@ -12835,28 +13004,25 @@ async function cronCreate(globalOpts, opts) {
12835
13004
  everyMs = unit.startsWith("h") ? num * 36e5 : unit.startsWith("m") ? num * 6e4 : num * 1e3;
12836
13005
  }
12837
13006
  }
12838
- const result = db3.prepare(`
12839
- INSERT INTO jobs (schedule_type, cron, at_time, every_ms, description, chat_id, backend, model, thinking, session_type, delivery_mode, channel, target, timezone)
12840
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
12841
- `).run(
12842
- schedType,
12843
- opts.cron ?? null,
12844
- opts.at ?? null,
13007
+ const job = insertJob2({
13008
+ scheduleType: schedType,
13009
+ cron: opts.cron ?? null,
13010
+ atTime: opts.at ?? null,
12845
13011
  everyMs,
12846
- opts.description,
13012
+ description: opts.description,
12847
13013
  chatId,
12848
- opts.backend ?? null,
12849
- opts.model ?? null,
12850
- opts.thinking ?? null,
12851
- opts.sessionType ?? "isolated",
12852
- opts.delivery ?? "announce",
12853
- opts.channel ?? null,
12854
- opts.target ?? null,
12855
- opts.timezone ?? "UTC"
12856
- );
12857
- const jobId = Number(result.lastInsertRowid);
12858
- output({ id: jobId, success: true }, () => `
12859
- ${successFmt(`Job #${jobId} created.`)}
13014
+ backend: opts.backend ?? null,
13015
+ model: opts.model ?? null,
13016
+ thinking: opts.thinking ?? null,
13017
+ timeout,
13018
+ sessionType: opts.sessionType ?? "isolated",
13019
+ deliveryMode: opts.delivery ?? "announce",
13020
+ channel: opts.channel ?? null,
13021
+ target: opts.target ?? null,
13022
+ timezone: opts.timezone ?? "UTC"
13023
+ });
13024
+ output({ id: job.id, success: true }, () => `
13025
+ ${successFmt(`Job #${job.id} created.`)}
12860
13026
  `);
12861
13027
  } catch (err) {
12862
13028
  outputError("CREATE_FAILED", err.message);
@@ -12923,6 +13089,11 @@ async function cronEdit(globalOpts, id, opts) {
12923
13089
  updates.push("thinking = ?");
12924
13090
  values.push(opts.thinking);
12925
13091
  }
13092
+ if (opts.timeout) {
13093
+ const timeout = parseAndValidateTimeout(opts.timeout);
13094
+ updates.push("timeout = ?");
13095
+ values.push(timeout);
13096
+ }
12926
13097
  if (opts.timezone) {
12927
13098
  updates.push("timezone = ?");
12928
13099
  values.push(opts.timezone);
@@ -12974,6 +13145,7 @@ var init_cron2 = __esm({
12974
13145
  init_format();
12975
13146
  init_paths();
12976
13147
  init_resolve_chat();
13148
+ init_types2();
12977
13149
  }
12978
13150
  });
12979
13151
 
@@ -15049,7 +15221,7 @@ function registerCronCommands(cmd) {
15049
15221
  const { cronList: cronList2 } = await Promise.resolve().then(() => (init_cron2(), cron_exports2));
15050
15222
  await cronList2(program.opts());
15051
15223
  });
15052
- cmd.command("create").description("Create a scheduled job").requiredOption("--description <text>", "Job description").option("--prompt <text>", "Agent prompt (defaults to description)").option("--cron <expr>", "Cron expression (e.g. '0 9 * * *')").option("--at <iso8601>", "One-shot time").option("--every <interval>", "Repeat interval (e.g. 30m, 1h)").option("--backend <name>", "Backend for this job").option("--model <name>", "Model for this job").option("--thinking <level>", "Thinking level").option("--timezone <tz>", "IANA timezone", "UTC").option("--session-type <type>", "Session type (isolated/main)", "isolated").option("--delivery <mode>", "Delivery mode (announce/webhook/none)", "announce").option("--channel <name>", "Delivery channel").option("--target <id>", "Delivery target").option("--cwd <path>", "Working directory").action(async (opts) => {
15224
+ cmd.command("create").description("Create a scheduled job").requiredOption("--description <text>", "Job description").option("--prompt <text>", "Agent prompt (defaults to description)").option("--cron <expr>", "Cron expression (e.g. '0 9 * * *')").option("--at <iso8601>", "One-shot time").option("--every <interval>", "Repeat interval (e.g. 30m, 1h)").option("--backend <name>", "Backend for this job").option("--model <name>", "Model for this job").option("--thinking <level>", "Thinking level").option("--timeout <seconds>", "Job timeout in seconds (30-3600)").option("--timezone <tz>", "IANA timezone", "UTC").option("--session-type <type>", "Session type (isolated/main)", "isolated").option("--delivery <mode>", "Delivery mode (announce/webhook/none)", "announce").option("--channel <name>", "Delivery channel").option("--target <id>", "Delivery target").option("--cwd <path>", "Working directory").action(async (opts) => {
15053
15225
  const { cronCreate: cronCreate2 } = await Promise.resolve().then(() => (init_cron2(), cron_exports2));
15054
15226
  await cronCreate2(program.opts(), opts);
15055
15227
  });
@@ -15069,7 +15241,7 @@ function registerCronCommands(cmd) {
15069
15241
  const { cronAction: cronAction2 } = await Promise.resolve().then(() => (init_cron2(), cron_exports2));
15070
15242
  await cronAction2(program.opts(), "run", id);
15071
15243
  });
15072
- cmd.command("edit <id>").description("Edit a job (same flags as create)").option("--description <text>").option("--cron <expr>").option("--at <iso8601>").option("--every <interval>").option("--backend <name>").option("--model <name>").option("--thinking <level>").option("--timezone <tz>").action(async (id, opts) => {
15244
+ cmd.command("edit <id>").description("Edit a job (same flags as create)").option("--description <text>").option("--cron <expr>").option("--at <iso8601>").option("--every <interval>").option("--backend <name>").option("--model <name>").option("--thinking <level>").option("--timeout <seconds>", "Job timeout in seconds (30-3600)").option("--timezone <tz>").action(async (id, opts) => {
15073
15245
  const { cronEdit: cronEdit2 } = await Promise.resolve().then(() => (init_cron2(), cron_exports2));
15074
15246
  await cronEdit2(program.opts(), id, opts);
15075
15247
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-claw",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
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",