@inetafrica/open-claudia 2.0.3 → 2.0.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.
package/bin/agent.js ADDED
@@ -0,0 +1,60 @@
1
+ // open-claudia agent "<prompt>" [--role "<role>"] [--json] [--cwd <dir>]
2
+ // Spawn a fresh, throwaway Claude sub-agent for focused research.
3
+ // No session resume; the parent agent captures stdout and decides what
4
+ // to do with the result.
5
+
6
+ const path = require("path");
7
+
8
+ const HELP = `
9
+ Spawn a fresh Claude sub-agent. Output goes to stdout.
10
+
11
+ open-claudia agent "<prompt>" [--role "<role>"] [--cwd <dir>] [--json] [--timeout <s>]
12
+
13
+ The sub-agent has read-only research tools (Read, Glob, Grep, Bash for read-only
14
+ commands) but no access to your chat session or the send-* CLIs.
15
+ `;
16
+
17
+ function takeFlag(args, name) {
18
+ const idx = args.indexOf(`--${name}`);
19
+ if (idx < 0) return null;
20
+ const v = args[idx + 1];
21
+ args.splice(idx, 2);
22
+ return v;
23
+ }
24
+
25
+ function hasFlag(args, name) {
26
+ const idx = args.indexOf(`--${name}`);
27
+ if (idx < 0) return false;
28
+ args.splice(idx, 1);
29
+ return true;
30
+ }
31
+
32
+ async function run(args) {
33
+ if (args.length === 0 || args[0] === "--help" || args[0] === "help") {
34
+ console.log(HELP); process.exit(args[0] === "--help" || args[0] === "help" ? 0 : 2);
35
+ }
36
+ const rest = args.slice();
37
+ const role = takeFlag(rest, "role");
38
+ const cwd = takeFlag(rest, "cwd") || process.cwd();
39
+ const timeoutS = takeFlag(rest, "timeout");
40
+ const json = hasFlag(rest, "json");
41
+ const prompt = rest.join(" ").trim();
42
+ if (!prompt) { console.error("Missing prompt."); process.exit(2); }
43
+
44
+ const { spawnSubagent } = require(path.join(__dirname, "..", "core", "subagent"));
45
+ const channelId = process.env.OC_CHANNEL_ID || null;
46
+ try {
47
+ const res = await spawnSubagent(prompt, {
48
+ cwd,
49
+ role,
50
+ channelId,
51
+ json,
52
+ timeoutMs: timeoutS ? Number(timeoutS) * 1000 : undefined,
53
+ });
54
+ if (res.text) process.stdout.write(res.text + (res.text.endsWith("\n") ? "" : "\n"));
55
+ if (res.truncated) process.stderr.write("(output truncated to 64KB)\n");
56
+ process.exit(0);
57
+ } catch (e) { console.error(`agent failed: ${e.message}`); process.exit(1); }
58
+ }
59
+
60
+ module.exports = { run, HELP };
package/bin/cli.js CHANGED
@@ -239,6 +239,34 @@ switch (command) {
239
239
  break;
240
240
  }
241
241
 
242
+ case "schedule-wakeup": {
243
+ require("./schedule").runScheduleWakeup(args.slice(1));
244
+ break;
245
+ }
246
+ case "cron-add": {
247
+ require("./schedule").runCronAdd(args.slice(1));
248
+ break;
249
+ }
250
+ case "cron-list":
251
+ case "job-list": {
252
+ require("./schedule").runCronList();
253
+ break;
254
+ }
255
+ case "cron-remove": {
256
+ require("./schedule").runCronRemove(args.slice(1));
257
+ break;
258
+ }
259
+
260
+ case "task": {
261
+ require("./task").run(args.slice(1));
262
+ break;
263
+ }
264
+
265
+ case "agent": {
266
+ require("./agent").run(args.slice(1));
267
+ break;
268
+ }
269
+
242
270
  default:
243
271
  console.log(`
244
272
  Open Claudia — AI Coding Assistant via Telegram
@@ -262,6 +290,14 @@ Memory tools:
262
290
  open-claudia transcript-window <pattern> Search project transcript, show hits with context
263
291
  (alias: tw; --help for options)
264
292
 
293
+ Background work (only inside an active bot-spawned task):
294
+ open-claudia schedule-wakeup <when> "<prompt>" One-shot future wake-up; resumes session
295
+ open-claudia cron-add "<sched>" "<prompt>" Recurring cron job
296
+ open-claudia cron-list List all wakeups + crons on this channel
297
+ open-claudia cron-remove <id> Remove a scheduled job
298
+ open-claudia task add|list|start|done|remove Per-channel todo (survives restarts)
299
+ open-claudia agent "<prompt>" [--role X] Spawn a throwaway sub-agent for research
300
+
265
301
  Start options:
266
302
  --web Also start the web UI
267
303
  --quick Skip slow health checks (Claude auth, Telegram API)
@@ -0,0 +1,99 @@
1
+ // Shared loopback HTTP client used by every subprocess CLI that needs
2
+ // to talk back to the running bot. Resolves the URL+token from env
3
+ // (OC_SEND_URL / OC_SEND_TOKEN) with a fallback to scanning the
4
+ // loopback info file directory written by the bot at startup.
5
+
6
+ const fs = require("fs");
7
+ const path = require("path");
8
+ const http = require("http");
9
+ const configDir = require(path.join(__dirname, "..", "config-dir"));
10
+
11
+ function readLoopbackInfo() {
12
+ try {
13
+ const dir = path.join(configDir, "loopback");
14
+ if (!fs.existsSync(dir)) return null;
15
+ const entries = fs.readdirSync(dir).filter((f) => f.endsWith(".json"));
16
+ let newest = null;
17
+ for (const f of entries) {
18
+ const full = path.join(dir, f);
19
+ const stat = fs.statSync(full);
20
+ if (!newest || stat.mtimeMs > newest.mtimeMs) newest = { full, mtimeMs: stat.mtimeMs };
21
+ }
22
+ if (!newest) return null;
23
+ return JSON.parse(fs.readFileSync(newest.full, "utf-8"));
24
+ } catch (e) { return null; }
25
+ }
26
+
27
+ function context() {
28
+ let sendUrl = process.env.OC_SEND_URL;
29
+ let sendToken = process.env.OC_SEND_TOKEN;
30
+ if (!sendUrl || !sendToken) {
31
+ const info = readLoopbackInfo();
32
+ if (info) { sendUrl = info.url; sendToken = info.token; }
33
+ }
34
+ return {
35
+ sendUrl,
36
+ sendToken,
37
+ channelId: process.env.OC_CHANNEL_ID,
38
+ adapterId: process.env.OC_CHANNEL_ADAPTER,
39
+ canonicalUserId: process.env.OC_CANONICAL_USER_ID || null,
40
+ sessionId: process.env.OC_LAST_SESSION_ID || null,
41
+ sessionKey: process.env.OC_LAST_SESSION_KEY || null,
42
+ };
43
+ }
44
+
45
+ function requireContext() {
46
+ const ctx = context();
47
+ if (!ctx.sendUrl || !ctx.sendToken || !ctx.channelId || !ctx.adapterId) {
48
+ console.error("No active chat context. This command only works from inside a bot-spawned task.");
49
+ process.exit(2);
50
+ }
51
+ return ctx;
52
+ }
53
+
54
+ function postJson(kind, body) {
55
+ const ctx = requireContext();
56
+ const u = new URL(ctx.sendUrl);
57
+ const payload = JSON.stringify({
58
+ channelId: ctx.channelId,
59
+ adapter: ctx.adapterId,
60
+ canonicalUserId: ctx.canonicalUserId,
61
+ sessionId: ctx.sessionId,
62
+ sessionKey: ctx.sessionKey,
63
+ ...body,
64
+ });
65
+ return new Promise((resolve, reject) => {
66
+ const req = http.request({
67
+ method: "POST",
68
+ hostname: u.hostname,
69
+ port: u.port,
70
+ path: `/${kind}`,
71
+ headers: {
72
+ Authorization: `Bearer ${ctx.sendToken}`,
73
+ "Content-Type": "application/json",
74
+ "Content-Length": Buffer.byteLength(payload),
75
+ },
76
+ }, (res) => {
77
+ let out = "";
78
+ res.on("data", (c) => { out += c; });
79
+ res.on("end", () => {
80
+ if (res.statusCode >= 200 && res.statusCode < 300) {
81
+ try { resolve(JSON.parse(out)); }
82
+ catch (e) { resolve({ ok: true }); }
83
+ } else {
84
+ let msg = `loopback ${res.statusCode}: ${out}`;
85
+ try {
86
+ const parsed = JSON.parse(out);
87
+ if (parsed && parsed.error) msg = `loopback ${res.statusCode}: ${parsed.error}`;
88
+ } catch (e) {}
89
+ reject(new Error(msg));
90
+ }
91
+ });
92
+ });
93
+ req.on("error", (e) => reject(new Error(`loopback request failed: ${e.message}`)));
94
+ req.write(payload);
95
+ req.end();
96
+ });
97
+ }
98
+
99
+ module.exports = { context, requireContext, postJson, readLoopbackInfo };
@@ -0,0 +1,118 @@
1
+ // open-claudia schedule-wakeup / cron-add / cron-list / cron-remove
2
+ // CLI surface for the agent to manage its own background work.
3
+
4
+ const { postJson } = require("./loopback-client");
5
+
6
+ const HELP = `
7
+ Background scheduling — manage wakeups and crons.
8
+
9
+ Wakeups (one-shot, resumes the same conversation):
10
+ open-claudia schedule-wakeup <when> "<prompt>" [--label "<label>"]
11
+ <when>: duration (e.g. 30s, 5m, 2h, 1d) OR an ISO datetime (e.g. 2026-05-16T15:00:00Z)
12
+
13
+ Crons (recurring):
14
+ open-claudia cron-add "<5-field cron>" "<prompt>" [--label "<label>"] [--project <name>]
15
+ open-claudia cron-list # all wakeups + crons for this channel
16
+ open-claudia cron-remove <id> # remove by id from cron-list
17
+
18
+ Each fire wakes the agent up in the same channel and resumes the Claude session
19
+ that was active when the schedule was created (so context carries over).
20
+ `;
21
+
22
+ const DUR = /^(\d+)\s*(s|sec|secs|m|min|mins|h|hr|hrs|d|day|days)$/i;
23
+
24
+ function parseWhen(s) {
25
+ if (!s) return null;
26
+ const m = String(s).trim().match(DUR);
27
+ if (m) {
28
+ const n = parseInt(m[1], 10);
29
+ const unit = m[2].toLowerCase();
30
+ const ms = unit.startsWith("s") ? n * 1000
31
+ : unit.startsWith("m") ? n * 60 * 1000
32
+ : unit.startsWith("h") ? n * 60 * 60 * 1000
33
+ : n * 24 * 60 * 60 * 1000;
34
+ return Date.now() + ms;
35
+ }
36
+ const ts = Date.parse(String(s));
37
+ if (Number.isFinite(ts)) return ts;
38
+ return null;
39
+ }
40
+
41
+ function takeFlag(args, name) {
42
+ const idx = args.indexOf(`--${name}`);
43
+ if (idx < 0) return null;
44
+ const v = args[idx + 1];
45
+ args.splice(idx, 2);
46
+ return v;
47
+ }
48
+
49
+ function fmtJob(j) {
50
+ const when = j.kind === "wakeup"
51
+ ? `fires ${new Date(j.fireAt).toISOString()}`
52
+ : `schedule "${j.schedule}"`;
53
+ const proj = j.project ? ` [${j.project}]` : "";
54
+ const src = j.source ? ` (${j.source})` : "";
55
+ return `${j.id} ${j.kind} ${j.label || "(no label)"}${proj}${src} ${when}`;
56
+ }
57
+
58
+ async function runScheduleWakeup(args) {
59
+ if (args.length < 2 || args[0] === "--help") { console.log(HELP); process.exit(args[0] === "--help" ? 0 : 2); }
60
+ const rest = args.slice();
61
+ const label = takeFlag(rest, "label");
62
+ const project = takeFlag(rest, "project");
63
+ const when = parseWhen(rest[0]);
64
+ if (!when) {
65
+ console.error(`Could not parse <when>: "${rest[0]}". Use a duration (5m, 2h, 1d) or ISO datetime.`);
66
+ process.exit(2);
67
+ }
68
+ const prompt = rest.slice(1).join(" ");
69
+ if (!prompt) { console.error("Missing prompt."); process.exit(2); }
70
+ try {
71
+ const res = await postJson("schedule-wakeup", { fireAt: when, prompt, label: label || prompt.slice(0, 60), project });
72
+ console.log(`Scheduled wakeup ${res.job.id} for ${new Date(when).toISOString()}.`);
73
+ process.exit(0);
74
+ } catch (e) { console.error(e.message); process.exit(1); }
75
+ }
76
+
77
+ async function runCronAdd(args) {
78
+ if (args.length < 2 || args[0] === "--help") { console.log(HELP); process.exit(args[0] === "--help" ? 0 : 2); }
79
+ const rest = args.slice();
80
+ const label = takeFlag(rest, "label");
81
+ const project = takeFlag(rest, "project");
82
+ const schedule = rest[0];
83
+ const prompt = rest.slice(1).join(" ");
84
+ if (!schedule || !prompt) { console.error("Usage: cron-add \"<schedule>\" \"<prompt>\""); process.exit(2); }
85
+ try {
86
+ const res = await postJson("cron-add", { schedule, prompt, label: label || prompt.slice(0, 60), project });
87
+ console.log(`Added cron ${res.job.id} (${schedule}).`);
88
+ process.exit(0);
89
+ } catch (e) { console.error(e.message); process.exit(1); }
90
+ }
91
+
92
+ async function runCronList() {
93
+ try {
94
+ const res = await postJson("job-list", {});
95
+ const list = res.jobs || [];
96
+ if (list.length === 0) { console.log("No jobs."); process.exit(0); }
97
+ for (const j of list) console.log(fmtJob(j));
98
+ process.exit(0);
99
+ } catch (e) { console.error(e.message); process.exit(1); }
100
+ }
101
+
102
+ async function runCronRemove(args) {
103
+ const id = args[0];
104
+ if (!id) { console.error("Usage: cron-remove <id>"); process.exit(2); }
105
+ try {
106
+ const res = await postJson("cron-remove", { id });
107
+ console.log(res.ok ? `Removed ${id}.` : `Not found: ${id}.`);
108
+ process.exit(res.ok ? 0 : 1);
109
+ } catch (e) { console.error(e.message); process.exit(1); }
110
+ }
111
+
112
+ module.exports = {
113
+ HELP,
114
+ runScheduleWakeup,
115
+ runCronAdd,
116
+ runCronList,
117
+ runCronRemove,
118
+ };
package/bin/task.js ADDED
@@ -0,0 +1,154 @@
1
+ // open-claudia task <subcommand>
2
+ // Per-channel todo list. Use this for multi-step work so progress
3
+ // survives compactions and restarts. Supports one level of hierarchy:
4
+ // a parent "plan" task can have child subtasks via --parent.
5
+
6
+ const { postJson } = require("./loopback-client");
7
+
8
+ const HELP = `
9
+ Per-channel todo list with optional plan/subtask hierarchy.
10
+
11
+ open-claudia task add "<content>" [--parent <id>] [--description "<...>"]
12
+ open-claudia task plan "<title>" "<sub1>" "<sub2>" ... [--description "<...>"]
13
+ open-claudia task list [--status pending|in_progress|completed]
14
+ open-claudia task start <id>
15
+ open-claudia task done <id>
16
+ open-claudia task remove <id> # removing a plan removes its subtasks
17
+ open-claudia task clear-completed # only clears plans whose subtasks are all done
18
+
19
+ Use plans for any work with 3+ distinct steps. Mark subtasks in_progress
20
+ as you start them and done as you finish, so a resumed turn or a new
21
+ turn after compaction can pick up where you left off.
22
+ `;
23
+
24
+ function statusMark(t) {
25
+ return t.status === "completed" ? "[x]" : t.status === "in_progress" ? "[~]" : "[ ]";
26
+ }
27
+
28
+ function takeFlag(args, name) {
29
+ const idx = args.indexOf(`--${name}`);
30
+ if (idx < 0) return null;
31
+ const v = args[idx + 1];
32
+ args.splice(idx, 2);
33
+ return v;
34
+ }
35
+
36
+ function renderTree(list) {
37
+ const byParent = new Map();
38
+ const roots = [];
39
+ for (const t of list) {
40
+ if (t.parentId) {
41
+ if (!byParent.has(t.parentId)) byParent.set(t.parentId, []);
42
+ byParent.get(t.parentId).push(t);
43
+ } else {
44
+ roots.push(t);
45
+ }
46
+ }
47
+ const lines = [];
48
+ roots.forEach((root, i) => {
49
+ lines.push(`${i + 1}. ${statusMark(root)} ${root.content} (${root.id})`);
50
+ if (root.description) lines.push(` ${root.description}`);
51
+ (byParent.get(root.id) || []).forEach((c) => {
52
+ lines.push(` ${statusMark(c)} ${c.content} (${c.id})`);
53
+ });
54
+ });
55
+ for (const orphan of list) {
56
+ if (orphan.parentId && !roots.find((r) => r.id === orphan.parentId)) {
57
+ lines.push(` ${statusMark(orphan)} ${orphan.content} (${orphan.id}) [orphan]`);
58
+ }
59
+ }
60
+ return lines.join("\n");
61
+ }
62
+
63
+ async function runAdd(args) {
64
+ const parentId = takeFlag(args, "parent");
65
+ const description = takeFlag(args, "description");
66
+ const content = args.join(" ").trim();
67
+ if (!content) { console.error("Usage: task add \"<content>\" [--parent <id>] [--description \"<...>\"]"); process.exit(2); }
68
+ try {
69
+ const res = await postJson("task-add", { content, parentId, description });
70
+ console.log(`Added task ${res.task.id}.`);
71
+ process.exit(0);
72
+ } catch (e) { console.error(e.message); process.exit(1); }
73
+ }
74
+
75
+ async function runPlan(args) {
76
+ const description = takeFlag(args, "description");
77
+ if (args.length < 2) {
78
+ console.error("Usage: task plan \"<title>\" \"<sub1>\" \"<sub2>\" ... [--description \"<...>\"]");
79
+ process.exit(2);
80
+ }
81
+ const [title, ...subtasks] = args;
82
+ try {
83
+ const res = await postJson("task-plan", { title, subtasks, description });
84
+ const parent = res.plan.parent;
85
+ const kids = res.plan.children || [];
86
+ console.log(`Plan ${parent.id}: ${parent.content}`);
87
+ kids.forEach((c) => console.log(` - ${c.content} (${c.id})`));
88
+ process.exit(0);
89
+ } catch (e) { console.error(e.message); process.exit(1); }
90
+ }
91
+
92
+ async function runList(args) {
93
+ const status = args.includes("--status") ? args[args.indexOf("--status") + 1] : null;
94
+ try {
95
+ const res = await postJson("task-list", status ? { status } : {});
96
+ const list = res.tasks || [];
97
+ if (list.length === 0) { console.log("No tasks."); process.exit(0); }
98
+ console.log(renderTree(list));
99
+ process.exit(0);
100
+ } catch (e) { console.error(e.message); process.exit(1); }
101
+ }
102
+
103
+ async function runUpdate(args, status) {
104
+ const id = args[0];
105
+ if (!id) { console.error(`Usage: task ${status === "in_progress" ? "start" : "done"} <id>`); process.exit(2); }
106
+ try {
107
+ const res = await postJson("task-update", { id, status });
108
+ if (!res.ok) { console.error(`Not found: ${id}`); process.exit(1); }
109
+ console.log(`Task ${id} → ${status}.`);
110
+ process.exit(0);
111
+ } catch (e) { console.error(e.message); process.exit(1); }
112
+ }
113
+
114
+ async function runRemove(args) {
115
+ const id = args[0];
116
+ if (!id) { console.error("Usage: task remove <id>"); process.exit(2); }
117
+ try {
118
+ const res = await postJson("task-remove", { id });
119
+ if (!res.ok) { console.error(`Not found: ${id}`); process.exit(1); }
120
+ const extra = (res.removed && res.removed.alsoRemoved) ? ` (and ${res.removed.alsoRemoved} subtask${res.removed.alsoRemoved === 1 ? "" : "s"})` : "";
121
+ console.log(`Removed ${id}${extra}.`);
122
+ process.exit(0);
123
+ } catch (e) { console.error(e.message); process.exit(1); }
124
+ }
125
+
126
+ async function runClearCompleted() {
127
+ try {
128
+ await postJson("task-clear-completed", {});
129
+ console.log("Cleared completed tasks.");
130
+ process.exit(0);
131
+ } catch (e) { console.error(e.message); process.exit(1); }
132
+ }
133
+
134
+ async function run(args) {
135
+ const sub = args[0];
136
+ const rest = args.slice(1);
137
+ if (!sub || sub === "--help" || sub === "help") { console.log(HELP); process.exit(sub ? 0 : 2); }
138
+ switch (sub) {
139
+ case "add": return runAdd(rest);
140
+ case "plan": return runPlan(rest);
141
+ case "list": case "ls": return runList(rest);
142
+ case "start": return runUpdate(rest, "in_progress");
143
+ case "done": case "complete": return runUpdate(rest, "completed");
144
+ case "pending": return runUpdate(rest, "pending");
145
+ case "remove": case "rm": return runRemove(rest);
146
+ case "clear-completed": case "clear": return runClearCompleted();
147
+ default:
148
+ console.error(`Unknown task subcommand: ${sub}`);
149
+ console.log(HELP);
150
+ process.exit(2);
151
+ }
152
+ }
153
+
154
+ module.exports = { run, HELP };
package/bot.js CHANGED
@@ -13,7 +13,7 @@ const { execSync } = require("child_process");
13
13
  const { config, BOT_DIR, CONFIG_DIR, SOUL_FILE, VAULT_FILE, CHAT_ID } = require("./core/config");
14
14
  const { vault } = require("./core/vault-store");
15
15
  const { isOnboarded } = require("./core/onboarding");
16
- const { initCrons } = require("./core/cron");
16
+ const { initScheduler, stopAll: stopScheduler } = require("./core/scheduler");
17
17
  const { onMessage, onAction } = require("./core/router");
18
18
  const { publicCommands } = require("./core/commands");
19
19
  const registry = require("./core/adapter-registry");
@@ -43,6 +43,7 @@ async function gracefulShutdown(signal) {
43
43
  }
44
44
  }
45
45
  for (const a of adapters) { try { await a.stop(); } catch (e) {} }
46
+ try { stopScheduler(); } catch (e) {}
46
47
  try { loopback.stop(); } catch (e) {}
47
48
  try {
48
49
  const mediaDir = path.join(CONFIG_DIR, "media");
@@ -159,7 +160,7 @@ setInterval(checkForUpdates, 5 * 60 * 1000);
159
160
  }
160
161
  } catch (e) {}
161
162
 
162
- initCrons();
163
+ initScheduler(adapters);
163
164
 
164
165
  try {
165
166
  const lb = await loopback.start(registry);
package/core/actions.js CHANGED
@@ -12,7 +12,8 @@ const { listProjects, projectKeyboard, workspacePath } = require("./projects");
12
12
  const { isChatOwner, approveAuthRequest, denyAuthRequest, authRequestLabel } = require("./access");
13
13
  const { finishOnboarding } = require("./onboarding");
14
14
  const { startSession } = require("./handlers");
15
- const { activeCrons, loadCrons, saveCrons, scheduleCron } = require("./cron");
15
+ const jobs = require("./jobs");
16
+ const scheduler = require("./scheduler");
16
17
 
17
18
  async function handleAction(envelope) {
18
19
  const adapter = envelope.adapter;
@@ -134,15 +135,25 @@ async function handleAction(envelope) {
134
135
  };
135
136
  const p = presets[d.slice(3)];
136
137
  if (!p) return;
137
- const c = { id: `cron_${Date.now()}`, ...p, project: state.currentSession.name };
138
- const list = loadCrons(); list.push(c); saveCrons(list); scheduleCron(c);
139
- await send(`Added: ${c.label} for ${state.currentSession.name}`);
138
+ scheduler.addJob({
139
+ kind: "cron",
140
+ adapter: envelope.adapter.id,
141
+ adapterType: envelope.adapter.type,
142
+ channelId: String(envelope.channelId),
143
+ canonicalUserId: envelope.canonicalUserId,
144
+ project: state.currentSession.name,
145
+ prompt: p.prompt,
146
+ label: p.label,
147
+ source: "user",
148
+ schedule: p.schedule,
149
+ });
150
+ await send(`Added: ${p.label} for ${state.currentSession.name}`);
140
151
  return;
141
152
  }
142
153
  if (d === "cp:clear") {
143
- for (const [, v] of activeCrons) v.task.stop();
144
- activeCrons.clear();
145
- saveCrons([]);
154
+ for (const j of jobs.listForChannel(envelope.adapter.id, envelope.channelId)) {
155
+ if (j.kind === "cron" && j.source === "user") scheduler.removeJob(j.id);
156
+ }
146
157
  await send("All crons cleared.");
147
158
  return;
148
159
  }
@@ -8,7 +8,7 @@
8
8
  // remember to re-wire everything.
9
9
 
10
10
  const { loadChannels } = require("./config");
11
- const { setAdapters } = require("./cron");
11
+ const { setAdapters } = require("./scheduler");
12
12
 
13
13
  const { TelegramAdapter } = require("../channels/telegram/adapter");
14
14
  const { KazeeAdapter } = require("../channels/kazee/adapter");
package/core/config.js CHANGED
@@ -57,6 +57,8 @@ const WHISPER_MODEL = config.WHISPER_MODEL || "";
57
57
  const FFMPEG = config.FFMPEG || "";
58
58
  const SOUL_FILE = config.SOUL_FILE || path.join(CONFIG_DIR, "soul.md");
59
59
  const CRONS_FILE = config.CRONS_FILE || path.join(CONFIG_DIR, "crons.json");
60
+ const JOBS_FILE = config.JOBS_FILE || path.join(CONFIG_DIR, "jobs.json");
61
+ const TASKS_DIR = config.TASKS_DIR || path.join(CONFIG_DIR, "tasks");
60
62
  const VAULT_FILE = config.VAULT_FILE || path.join(CONFIG_DIR, "vault.enc");
61
63
  const AUTH_FILE = config.AUTH_FILE || path.join(CONFIG_DIR, "auth.json");
62
64
  const IDENTITIES_FILE = config.IDENTITIES_FILE || path.join(CONFIG_DIR, "identities.json");
@@ -76,7 +78,7 @@ if (!CLAUDE_PATH) { console.error("CLAUDE_PATH not set"); process.exit(1); }
76
78
  if (!fs.existsSync(WORKSPACE)) {
77
79
  try {
78
80
  fs.mkdirSync(WORKSPACE, { recursive: true });
79
- console.log(`Created workspace: ${WORKSPACE}`);
81
+ console.error(`Created workspace: ${WORKSPACE}`);
80
82
  } catch (e) {
81
83
  console.error(`Failed to create workspace: ${e.message}`);
82
84
  process.exit(1);
@@ -97,14 +99,14 @@ if (!resolvedCursorPath) {
97
99
  try { resolvedCursorPath = execSync("which agent 2>/dev/null", { encoding: "utf-8" }).trim() || null; }
98
100
  catch (e) { resolvedCursorPath = null; }
99
101
  }
100
- if (resolvedCursorPath) console.log(`Cursor Agent CLI: ${resolvedCursorPath}`);
102
+ if (resolvedCursorPath) console.error(`Cursor Agent CLI: ${resolvedCursorPath}`);
101
103
 
102
104
  let resolvedCodexPath = CODEX_PATH;
103
105
  if (!resolvedCodexPath) {
104
106
  try { resolvedCodexPath = execSync("which codex 2>/dev/null", { encoding: "utf-8" }).trim() || null; }
105
107
  catch (e) { resolvedCodexPath = null; }
106
108
  }
107
- if (resolvedCodexPath) console.log(`Codex CLI: ${resolvedCodexPath}`);
109
+ if (resolvedCodexPath) console.error(`Codex CLI: ${resolvedCodexPath}`);
108
110
 
109
111
  if (!fs.existsSync(TEMP_DIR)) fs.mkdirSync(TEMP_DIR, { recursive: true });
110
112
  if (!fs.existsSync(FILES_DIR)) fs.mkdirSync(FILES_DIR, { recursive: true });
@@ -191,7 +193,7 @@ module.exports = {
191
193
  TRANSCRIPT_MAX_ENTRY_CHARS,
192
194
  TRANSCRIPTS_DIR,
193
195
  WHISPER_CLI, WHISPER_MODEL, FFMPEG,
194
- SOUL_FILE, CRONS_FILE, VAULT_FILE, AUTH_FILE, IDENTITIES_FILE,
196
+ SOUL_FILE, CRONS_FILE, JOBS_FILE, TASKS_DIR, VAULT_FILE, AUTH_FILE, IDENTITIES_FILE,
195
197
  STATE_FILE, SESSIONS_FILE,
196
198
  TEMP_DIR, FILES_DIR,
197
199
  MAX_FILE_SIZE, MAX_VOICE_SIZE, MAX_PROCESS_TIMEOUT,
package/core/handlers.js CHANGED
@@ -14,16 +14,15 @@ const {
14
14
  const { register } = require("./commands");
15
15
  const { send, deleteMessage } = require("./io");
16
16
  const { currentState, resetSettings, resetSessionUsage, saveState, getProjectSessions, getLastProjectSession, recordSession, linkIdentity } = require("./state");
17
- const { canonicalForChannel, normalizeCanonicalUserId, channelKey, identities } = require("./identity");
17
+ const { canonicalForChannel, canonicalForTelegram, normalizeCanonicalUserId, channelKey, identities } = require("./identity");
18
18
  const { isChatAuthorized, isChatOwner, recordPendingAuthRequest, authRequestLabel, hasOwner, bootstrapOwner } = require("./access");
19
19
  const { isOnboarded, startOnboarding } = require("./onboarding");
20
20
  const { listProjects, findProject, projectKeyboard, workspacePath } = require("./projects");
21
21
  const { vault } = require("./vault-store");
22
22
  const { redactSensitive } = require("./redact");
23
23
  const { runDoctorChecks, formatDoctorReport } = require("./doctor");
24
- const {
25
- loadCrons, saveCrons, scheduleCron, activeCrons,
26
- } = require("./cron");
24
+ const jobs = require("./jobs");
25
+ const scheduler = require("./scheduler");
27
26
  const {
28
27
  runClaude, compactActiveSession, getActiveSessionId,
29
28
  } = require("./runner");
@@ -897,8 +896,9 @@ register({
897
896
  name: "cron", description: "Manage scheduled tasks", args: "[add|remove ...]",
898
897
  handler: async (env, { tail }) => {
899
898
  if (!authorized(env)) return;
899
+ const channelJobs = () => jobs.listForChannel(env.adapter.id, env.channelId).filter((j) => j.kind === "cron");
900
900
  if (!tail) {
901
- const list = loadCrons();
901
+ const list = channelJobs();
902
902
  if (list.length === 0) {
903
903
  return send("No crons.\n\nAdd: /cron add \"<schedule>\" <project> \"<prompt>\"\n\nOr pick a preset:", {
904
904
  keyboard: { inline_keyboard: [
@@ -907,7 +907,7 @@ register({
907
907
  ] },
908
908
  });
909
909
  }
910
- return send("Crons:\n\n" + list.map((c, i) => `${i + 1}. ${c.label} (${c.schedule}) — ${c.project}`).join("\n") + "\n\nRemove: /cron remove <#>");
910
+ return send("Crons:\n\n" + list.map((c, i) => `${i + 1}. ${c.label} (${c.schedule}) — ${c.project || ""}`).join("\n") + "\n\nRemove: /cron remove <#>");
911
911
  }
912
912
 
913
913
  const addMatch = tail.match(/^add\s+"(.+)"\s+(\S+)\s+"(.+)"$/);
@@ -915,19 +915,28 @@ register({
915
915
  if (!cronLib.validate(addMatch[1])) return send("Invalid cron schedule.");
916
916
  const proj = findProject(addMatch[2]);
917
917
  if (!proj || Array.isArray(proj)) return send("Project not found.");
918
- const c = { id: `cron_${Date.now()}`, schedule: addMatch[1], project: proj, prompt: addMatch[3], label: addMatch[3].slice(0, 50) };
919
- const list = loadCrons(); list.push(c); saveCrons(list); scheduleCron(c);
920
- return send(`Added: ${c.label} (${c.schedule}) for ${proj}`);
918
+ scheduler.addJob({
919
+ kind: "cron",
920
+ adapter: env.adapter.id,
921
+ adapterType: env.adapter.type,
922
+ channelId: String(env.channelId),
923
+ canonicalUserId: env.canonicalUserId,
924
+ project: proj,
925
+ prompt: addMatch[3],
926
+ label: addMatch[3].slice(0, 50),
927
+ source: "user",
928
+ schedule: addMatch[1],
929
+ });
930
+ return send(`Added: ${addMatch[3].slice(0, 50)} (${addMatch[1]}) for ${proj}`);
921
931
  }
922
932
 
923
933
  const removeMatch = tail.match(/^remove\s+(\d+)$/);
924
934
  if (removeMatch) {
925
- const list = loadCrons();
935
+ const list = channelJobs();
926
936
  const idx = parseInt(removeMatch[1], 10) - 1;
927
937
  if (idx < 0 || idx >= list.length) return send("Invalid number.");
928
- const removed = list.splice(idx, 1)[0]; saveCrons(list);
929
- if (activeCrons.has(removed.id)) { activeCrons.get(removed.id).task.stop(); activeCrons.delete(removed.id); }
930
- return send(`Removed: ${removed.label}`);
938
+ const removed = scheduler.removeJob(list[idx].id);
939
+ return send(`Removed: ${removed ? removed.label : "(unknown)"}`);
931
940
  }
932
941
 
933
942
  send('Usage: /cron | /cron add "<schedule>" <project> "<prompt>" | /cron remove <#>');