cc-claw 0.3.3 → 0.3.5

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 +274 -78
  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.5" : (() => {
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}
@@ -9606,20 +9761,8 @@ Topics: ${s.topics}`;
9606
9761
  await channel.sendText(chatId, "No skills found. Install skills with /skill-install <github-url> or place them in ~/.cc-claw/workspace/skills/", "plain");
9607
9762
  return;
9608
9763
  }
9609
- if (typeof channel.sendKeyboard === "function") {
9610
- const buttons = skills2.map((s) => {
9611
- const tags = s.sources.join(", ");
9612
- return [{ label: `${s.name} [${tags}]`, data: `skill:${s.source}:${s.name}` }];
9613
- });
9614
- await channel.sendKeyboard(chatId, `${skills2.length} skills available. Select one to invoke:`, buttons);
9615
- } else {
9616
- const lines = skills2.map((s) => {
9617
- const tags = s.sources.join(", ");
9618
- const desc = s.description ? ` \u2014 ${s.description.slice(0, 50)}` : "";
9619
- return `\u2022 ${s.name} [${tags}]${desc}`;
9620
- });
9621
- await channel.sendText(chatId, ["Available skills:", "", ...lines].join("\n"), "plain");
9622
- }
9764
+ const page = commandArgs ? parseInt(commandArgs, 10) || 1 : 1;
9765
+ await sendSkillsPage(chatId, channel, skills2, page);
9623
9766
  break;
9624
9767
  }
9625
9768
  case "skill-install": {
@@ -10047,7 +10190,7 @@ Use /skills to see it.`, "plain");
10047
10190
  }
10048
10191
  const runLines = cronRuns2.map((r) => {
10049
10192
  const dur = r.durationMs ? ` (${(r.durationMs / 1e3).toFixed(1)}s)` : "";
10050
- return `#${r.jobId} [${r.status}] ${r.startedAt}${dur}`;
10193
+ return `#${r.jobId} [${r.status}] ${formatLocalDateTime(r.startedAt)}${dur}`;
10051
10194
  });
10052
10195
  await channel.sendText(chatId, runLines.join("\n\n"), "plain");
10053
10196
  break;
@@ -10096,7 +10239,7 @@ async function handleVoice(msg, channel) {
10096
10239
  const vModel = resolveModel(chatId);
10097
10240
  const vVerbose = getVerboseLevel(chatId);
10098
10241
  const vToolCb = vVerbose !== "off" ? makeToolActionCallback(chatId, channel, vVerbose) : void 0;
10099
- const response = await askAgent(chatId, transcript, getCwd(chatId), void 0, vModel, mode, vToolCb);
10242
+ const response = await askAgent(chatId, transcript, { cwd: getCwd(chatId), model: vModel, permMode: mode, onToolAction: vToolCb });
10100
10243
  if (response.usage) addUsage(chatId, response.usage.input, response.usage.output, response.usage.cacheRead, vModel);
10101
10244
  await sendResponse(chatId, channel, response.text);
10102
10245
  } catch (err) {
@@ -10143,7 +10286,7 @@ Summarize this for the user.`;
10143
10286
  const vMode = getMode(chatId);
10144
10287
  const vidVerbose = getVerboseLevel(chatId);
10145
10288
  const vidToolCb = vidVerbose !== "off" ? makeToolActionCallback(chatId, channel, vidVerbose) : void 0;
10146
- const response2 = await askAgent(chatId, prompt2, getCwd(chatId), void 0, vidModel, vMode, vidToolCb);
10289
+ const response2 = await askAgent(chatId, prompt2, { cwd: getCwd(chatId), model: vidModel, permMode: vMode, onToolAction: vidToolCb });
10147
10290
  if (response2.usage) addUsage(chatId, response2.usage.input, response2.usage.output, response2.usage.cacheRead, vidModel);
10148
10291
  await sendResponse(chatId, channel, response2.text);
10149
10292
  return;
@@ -10184,7 +10327,7 @@ ${content}
10184
10327
  const mMode = getMode(chatId);
10185
10328
  const mVerbose = getVerboseLevel(chatId);
10186
10329
  const mToolCb = mVerbose !== "off" ? makeToolActionCallback(chatId, channel, mVerbose) : void 0;
10187
- const response = await askAgent(chatId, prompt, getCwd(chatId), void 0, mediaModel, mMode, mToolCb);
10330
+ const response = await askAgent(chatId, prompt, { cwd: getCwd(chatId), model: mediaModel, permMode: mMode, onToolAction: mToolCb });
10188
10331
  if (response.usage) addUsage(chatId, response.usage.input, response.usage.output, response.usage.cacheRead, mediaModel);
10189
10332
  await sendResponse(chatId, channel, response.text);
10190
10333
  if (tempFilePath) {
@@ -10249,7 +10392,7 @@ async function handleText(msg, channel) {
10249
10392
  const tMode = getMode(chatId);
10250
10393
  const tVerbose = getVerboseLevel(chatId);
10251
10394
  const tToolCb = tVerbose !== "off" ? makeToolActionCallback(chatId, channel, tVerbose) : void 0;
10252
- const response = await askAgent(chatId, text, getCwd(chatId), void 0, model2, tMode, tToolCb);
10395
+ const response = await askAgent(chatId, text, { cwd: getCwd(chatId), model: model2, permMode: tMode, onToolAction: tToolCb });
10253
10396
  if (response.usage) addUsage(chatId, response.usage.input, response.usage.output, response.usage.cacheRead, model2);
10254
10397
  await sendResponse(chatId, channel, response.text);
10255
10398
  } catch (err) {
@@ -10588,6 +10731,10 @@ ${PERM_MODES[chosen]}`,
10588
10731
  touchBookmark(chatId, alias);
10589
10732
  logActivity(getDb(), { chatId, source: "telegram", eventType: "config_changed", summary: `Working directory set to ${bookmark.path}`, detail: { field: "cwd", value: bookmark.path } });
10590
10733
  await sendCwdSessionChoice(chatId, bookmark.path, channel);
10734
+ } else if (data.startsWith("skills:page:")) {
10735
+ const page = parseInt(data.slice(12), 10);
10736
+ const skills2 = await discoverAllSkills();
10737
+ await sendSkillsPage(chatId, channel, skills2, page);
10591
10738
  } else if (data.startsWith("skill:")) {
10592
10739
  const parts = data.slice(6).split(":");
10593
10740
  let skillName;
@@ -10620,7 +10767,7 @@ ${PERM_MODES[chosen]}`,
10620
10767
  const sMode = getMode(chatId);
10621
10768
  const sVerbose = getVerboseLevel(chatId);
10622
10769
  const sToolCb = sVerbose !== "off" ? makeToolActionCallback(chatId, channel, sVerbose) : void 0;
10623
- const response = await askAgent(chatId, skillContent, getCwd(chatId), void 0, skillModel, sMode, sToolCb);
10770
+ const response = await askAgent(chatId, skillContent, { cwd: getCwd(chatId), model: skillModel, permMode: sMode, onToolAction: sToolCb });
10624
10771
  if (response.usage) addUsage(chatId, response.usage.input, response.usage.output, response.usage.cacheRead, skillModel);
10625
10772
  await sendResponse(chatId, channel, response.text);
10626
10773
  }
@@ -10764,7 +10911,38 @@ function isTextExt(ext) {
10764
10911
  "log"
10765
10912
  ].includes(ext);
10766
10913
  }
10767
- var PERM_MODES, VERBOSE_LEVELS, CLI_INSTALL_HINTS, BLOCKED_PATH_PATTERNS2;
10914
+ async function sendSkillsPage(chatId, channel, skills2, page) {
10915
+ const totalPages = Math.ceil(skills2.length / SKILLS_PER_PAGE);
10916
+ const safePage = Math.max(1, Math.min(page, totalPages));
10917
+ const start = (safePage - 1) * SKILLS_PER_PAGE;
10918
+ const pageSkills = skills2.slice(start, start + SKILLS_PER_PAGE);
10919
+ if (typeof channel.sendKeyboard !== "function") {
10920
+ const lines = pageSkills.map((s) => {
10921
+ const tags = s.sources.join(", ");
10922
+ const desc = s.description ? ` \u2014 ${s.description.slice(0, 50)}` : "";
10923
+ return `\u2022 ${s.name} [${tags}]${desc}`;
10924
+ });
10925
+ const header3 = totalPages > 1 ? `Skills (page ${safePage}/${totalPages}, ${skills2.length} total):` : "Available skills:";
10926
+ const footer = totalPages > 1 ? `
10927
+ Use /skills <page> to navigate (e.g. /skills 2)` : "";
10928
+ await channel.sendText(chatId, [header3, "", ...lines, footer].join("\n"), "plain");
10929
+ return;
10930
+ }
10931
+ const buttons = pageSkills.map((s) => {
10932
+ const tags = s.sources.join(", ");
10933
+ return [{ label: `${s.name} [${tags}]`, data: `skill:${s.source}:${s.name}` }];
10934
+ });
10935
+ if (totalPages > 1) {
10936
+ const navRow = [];
10937
+ if (safePage > 1) navRow.push({ label: `\u2190 Page ${safePage - 1}`, data: `skills:page:${safePage - 1}` });
10938
+ navRow.push({ label: `${safePage}/${totalPages}`, data: "skills:page:noop" });
10939
+ if (safePage < totalPages) navRow.push({ label: `Page ${safePage + 1} \u2192`, data: `skills:page:${safePage + 1}` });
10940
+ buttons.push(navRow);
10941
+ }
10942
+ const header2 = totalPages > 1 ? `${skills2.length} skills (page ${safePage}/${totalPages}). Select one to invoke:` : `${skills2.length} skills available. Select one to invoke:`;
10943
+ await channel.sendKeyboard(chatId, header2, buttons);
10944
+ }
10945
+ var PERM_MODES, VERBOSE_LEVELS, CLI_INSTALL_HINTS, BLOCKED_PATH_PATTERNS2, SKILLS_PER_PAGE;
10768
10946
  var init_router = __esm({
10769
10947
  "src/router.ts"() {
10770
10948
  "use strict";
@@ -10773,10 +10951,12 @@ var init_router = __esm({
10773
10951
  init_profile();
10774
10952
  init_heartbeat();
10775
10953
  init_log();
10954
+ init_format_time();
10776
10955
  init_agent();
10777
10956
  init_stt();
10778
10957
  init_store4();
10779
10958
  init_summarize();
10959
+ init_session_log();
10780
10960
  init_backends();
10781
10961
  init_cron();
10782
10962
  init_wizard();
@@ -10819,6 +10999,7 @@ var init_router = __esm({
10819
10999
  /\/etc\/shadow$/,
10820
11000
  /\/etc\/passwd$/
10821
11001
  ];
11002
+ SKILLS_PER_PAGE = 25;
10822
11003
  }
10823
11004
  });
10824
11005
 
@@ -12420,7 +12601,9 @@ async function doctorCommand(globalOpts, localOpts) {
12420
12601
  lines.push(` ${parts.join(", ")}`);
12421
12602
  const fixable = r.checks.filter((c) => c.fix);
12422
12603
  if (fixable.length > 0 && !localOpts.fix) {
12423
- lines.push(muted(" Run cc-claw doctor --fix to attempt repairs."));
12604
+ for (const f of fixable) {
12605
+ lines.push(muted(` Fix: ${f.fix}`));
12606
+ }
12424
12607
  }
12425
12608
  }
12426
12609
  lines.push("");
@@ -12723,7 +12906,7 @@ async function memoryHistory(globalOpts, opts) {
12723
12906
  `;
12724
12907
  const lines = ["", divider(`Session History (${sums.length})`), ""];
12725
12908
  for (const s of sums) {
12726
- const date = s.created_at.split("T")[0] ?? s.created_at.split(" ")[0];
12909
+ const date = formatLocalDateTime(s.created_at);
12727
12910
  lines.push(` ${date} (${s.message_count} msgs)`);
12728
12911
  lines.push(` ${s.summary}`);
12729
12912
  lines.push(` Topics: ${muted(s.topics)}`);
@@ -12738,6 +12921,7 @@ var init_memory = __esm({
12738
12921
  init_format();
12739
12922
  init_paths();
12740
12923
  init_resolve_chat();
12924
+ init_format_time();
12741
12925
  }
12742
12926
  });
12743
12927
 
@@ -12752,6 +12936,15 @@ __export(cron_exports2, {
12752
12936
  cronRuns: () => cronRuns
12753
12937
  });
12754
12938
  import { existsSync as existsSync24 } from "fs";
12939
+ function parseAndValidateTimeout(raw) {
12940
+ if (!raw) return null;
12941
+ const val = parseInt(raw, 10);
12942
+ if (isNaN(val) || val < TIMEOUT_MIN_SECONDS || val > TIMEOUT_MAX_SECONDS) {
12943
+ outputError("INVALID_TIMEOUT", `Timeout must be between ${TIMEOUT_MIN_SECONDS} and ${TIMEOUT_MAX_SECONDS} seconds (got: ${raw})`);
12944
+ process.exit(1);
12945
+ }
12946
+ return val;
12947
+ }
12755
12948
  async function cronList(globalOpts) {
12756
12949
  if (!existsSync24(DB_PATH)) {
12757
12950
  outputError("DB_NOT_FOUND", "Database not found.");
@@ -12774,6 +12967,7 @@ async function cronList(globalOpts) {
12774
12967
  lines.push(` ${statusDot(status)} #${j.id} [${status}] ${schedule2}${tz}`);
12775
12968
  lines.push(` ${j.description}`);
12776
12969
  if (j.backend) lines.push(` Backend: ${j.backend}${j.model ? ` / ${j.model}` : ""}`);
12970
+ if (j.timeout) lines.push(` Timeout: ${j.timeout}s`);
12777
12971
  if (j.next_run_at) lines.push(` Next: ${muted(j.next_run_at)}`);
12778
12972
  lines.push("");
12779
12973
  }
@@ -12810,7 +13004,7 @@ async function cronHealth(globalOpts) {
12810
13004
  });
12811
13005
  }
12812
13006
  async function cronCreate(globalOpts, opts) {
12813
- const { isDaemonRunning: isDaemonRunning2, apiPost: apiPost2 } = await Promise.resolve().then(() => (init_api_client(), api_client_exports));
13007
+ const { isDaemonRunning: isDaemonRunning2 } = await Promise.resolve().then(() => (init_api_client(), api_client_exports));
12814
13008
  if (!await isDaemonRunning2()) {
12815
13009
  outputError("DAEMON_OFFLINE", "CC-Claw daemon is not running.\n\n Start it with: cc-claw service start");
12816
13010
  process.exit(1);
@@ -12821,10 +13015,9 @@ async function cronCreate(globalOpts, opts) {
12821
13015
  }
12822
13016
  const chatId = resolveChatId(globalOpts);
12823
13017
  const { success: successFmt } = await Promise.resolve().then(() => (init_format(), format_exports));
13018
+ const timeout = parseAndValidateTimeout(opts.timeout);
12824
13019
  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();
13020
+ const { insertJob: insertJob2 } = await Promise.resolve().then(() => (init_store4(), store_exports3));
12828
13021
  const schedType = opts.cron ? "cron" : opts.at ? "at" : "every";
12829
13022
  let everyMs = null;
12830
13023
  if (opts.every) {
@@ -12835,28 +13028,25 @@ async function cronCreate(globalOpts, opts) {
12835
13028
  everyMs = unit.startsWith("h") ? num * 36e5 : unit.startsWith("m") ? num * 6e4 : num * 1e3;
12836
13029
  }
12837
13030
  }
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,
13031
+ const job = insertJob2({
13032
+ scheduleType: schedType,
13033
+ cron: opts.cron ?? null,
13034
+ atTime: opts.at ?? null,
12845
13035
  everyMs,
12846
- opts.description,
13036
+ description: opts.description,
12847
13037
  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.`)}
13038
+ backend: opts.backend ?? null,
13039
+ model: opts.model ?? null,
13040
+ thinking: opts.thinking ?? null,
13041
+ timeout,
13042
+ sessionType: opts.sessionType ?? "isolated",
13043
+ deliveryMode: opts.delivery ?? "announce",
13044
+ channel: opts.channel ?? null,
13045
+ target: opts.target ?? null,
13046
+ timezone: opts.timezone ?? "UTC"
13047
+ });
13048
+ output({ id: job.id, success: true }, () => `
13049
+ ${successFmt(`Job #${job.id} created.`)}
12860
13050
  `);
12861
13051
  } catch (err) {
12862
13052
  outputError("CREATE_FAILED", err.message);
@@ -12923,6 +13113,11 @@ async function cronEdit(globalOpts, id, opts) {
12923
13113
  updates.push("thinking = ?");
12924
13114
  values.push(opts.thinking);
12925
13115
  }
13116
+ if (opts.timeout) {
13117
+ const timeout = parseAndValidateTimeout(opts.timeout);
13118
+ updates.push("timeout = ?");
13119
+ values.push(timeout);
13120
+ }
12926
13121
  if (opts.timezone) {
12927
13122
  updates.push("timezone = ?");
12928
13123
  values.push(opts.timezone);
@@ -12974,6 +13169,7 @@ var init_cron2 = __esm({
12974
13169
  init_format();
12975
13170
  init_paths();
12976
13171
  init_resolve_chat();
13172
+ init_types2();
12977
13173
  }
12978
13174
  });
12979
13175
 
@@ -15049,7 +15245,7 @@ function registerCronCommands(cmd) {
15049
15245
  const { cronList: cronList2 } = await Promise.resolve().then(() => (init_cron2(), cron_exports2));
15050
15246
  await cronList2(program.opts());
15051
15247
  });
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) => {
15248
+ 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
15249
  const { cronCreate: cronCreate2 } = await Promise.resolve().then(() => (init_cron2(), cron_exports2));
15054
15250
  await cronCreate2(program.opts(), opts);
15055
15251
  });
@@ -15069,7 +15265,7 @@ function registerCronCommands(cmd) {
15069
15265
  const { cronAction: cronAction2 } = await Promise.resolve().then(() => (init_cron2(), cron_exports2));
15070
15266
  await cronAction2(program.opts(), "run", id);
15071
15267
  });
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) => {
15268
+ 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
15269
  const { cronEdit: cronEdit2 } = await Promise.resolve().then(() => (init_cron2(), cron_exports2));
15074
15270
  await cronEdit2(program.opts(), id, opts);
15075
15271
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-claw",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
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",