@inetafrica/open-claudia 2.0.3 → 2.0.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.
- package/bin/agent.js +60 -0
- package/bin/cli.js +36 -0
- package/bin/loopback-client.js +99 -0
- package/bin/schedule.js +118 -0
- package/bin/task.js +93 -0
- package/bot.js +3 -2
- package/core/actions.js +18 -7
- package/core/adapter-registry.js +1 -1
- package/core/config.js +3 -1
- package/core/handlers.js +22 -13
- package/core/jobs.js +99 -0
- package/core/loopback.js +153 -27
- package/core/runner.js +12 -0
- package/core/scheduler.js +249 -0
- package/core/subagent.js +109 -0
- package/core/system-prompt.js +21 -0
- package/core/tasks.js +92 -0
- package/package.json +1 -1
- package/core/cron.js +0 -77
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 };
|
package/bin/schedule.js
ADDED
|
@@ -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,93 @@
|
|
|
1
|
+
// open-claudia task <subcommand>
|
|
2
|
+
// Per-channel todo list. Use this for multi-step work so progress
|
|
3
|
+
// survives compactions and restarts.
|
|
4
|
+
|
|
5
|
+
const { postJson } = require("./loopback-client");
|
|
6
|
+
|
|
7
|
+
const HELP = `
|
|
8
|
+
Per-channel todo list.
|
|
9
|
+
|
|
10
|
+
open-claudia task add "<content>"
|
|
11
|
+
open-claudia task list [--status pending|in_progress|completed]
|
|
12
|
+
open-claudia task start <id>
|
|
13
|
+
open-claudia task done <id>
|
|
14
|
+
open-claudia task remove <id>
|
|
15
|
+
open-claudia task clear-completed
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
function fmt(t, idx) {
|
|
19
|
+
const mark = t.status === "completed" ? "[x]" : t.status === "in_progress" ? "[~]" : "[ ]";
|
|
20
|
+
return `${idx + 1}. ${mark} ${t.content} (${t.id})`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function runAdd(args) {
|
|
24
|
+
const content = args.join(" ").trim();
|
|
25
|
+
if (!content) { console.error("Usage: task add \"<content>\""); process.exit(2); }
|
|
26
|
+
try {
|
|
27
|
+
const res = await postJson("task-add", { content });
|
|
28
|
+
console.log(`Added task ${res.task.id}.`);
|
|
29
|
+
process.exit(0);
|
|
30
|
+
} catch (e) { console.error(e.message); process.exit(1); }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function runList(args) {
|
|
34
|
+
const status = args[args.indexOf("--status") + 1] && args.includes("--status") ? args[args.indexOf("--status") + 1] : null;
|
|
35
|
+
try {
|
|
36
|
+
const res = await postJson("task-list", status ? { status } : {});
|
|
37
|
+
const list = res.tasks || [];
|
|
38
|
+
if (list.length === 0) { console.log("No tasks."); process.exit(0); }
|
|
39
|
+
list.forEach((t, i) => console.log(fmt(t, i)));
|
|
40
|
+
process.exit(0);
|
|
41
|
+
} catch (e) { console.error(e.message); process.exit(1); }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function runUpdate(args, status) {
|
|
45
|
+
const id = args[0];
|
|
46
|
+
if (!id) { console.error(`Usage: task ${status === "in_progress" ? "start" : "done"} <id>`); process.exit(2); }
|
|
47
|
+
try {
|
|
48
|
+
const res = await postJson("task-update", { id, status });
|
|
49
|
+
if (!res.ok) { console.error(`Not found: ${id}`); process.exit(1); }
|
|
50
|
+
console.log(`Task ${id} → ${status}.`);
|
|
51
|
+
process.exit(0);
|
|
52
|
+
} catch (e) { console.error(e.message); process.exit(1); }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function runRemove(args) {
|
|
56
|
+
const id = args[0];
|
|
57
|
+
if (!id) { console.error("Usage: task remove <id>"); process.exit(2); }
|
|
58
|
+
try {
|
|
59
|
+
const res = await postJson("task-remove", { id });
|
|
60
|
+
if (!res.ok) { console.error(`Not found: ${id}`); process.exit(1); }
|
|
61
|
+
console.log(`Removed ${id}.`);
|
|
62
|
+
process.exit(0);
|
|
63
|
+
} catch (e) { console.error(e.message); process.exit(1); }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function runClearCompleted() {
|
|
67
|
+
try {
|
|
68
|
+
await postJson("task-clear-completed", {});
|
|
69
|
+
console.log("Cleared completed tasks.");
|
|
70
|
+
process.exit(0);
|
|
71
|
+
} catch (e) { console.error(e.message); process.exit(1); }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function run(args) {
|
|
75
|
+
const sub = args[0];
|
|
76
|
+
const rest = args.slice(1);
|
|
77
|
+
if (!sub || sub === "--help" || sub === "help") { console.log(HELP); process.exit(sub ? 0 : 2); }
|
|
78
|
+
switch (sub) {
|
|
79
|
+
case "add": return runAdd(rest);
|
|
80
|
+
case "list": case "ls": return runList(rest);
|
|
81
|
+
case "start": return runUpdate(rest, "in_progress");
|
|
82
|
+
case "done": case "complete": return runUpdate(rest, "completed");
|
|
83
|
+
case "pending": return runUpdate(rest, "pending");
|
|
84
|
+
case "remove": case "rm": return runRemove(rest);
|
|
85
|
+
case "clear-completed": case "clear": return runClearCompleted();
|
|
86
|
+
default:
|
|
87
|
+
console.error(`Unknown task subcommand: ${sub}`);
|
|
88
|
+
console.log(HELP);
|
|
89
|
+
process.exit(2);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
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 {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
144
|
-
|
|
145
|
-
|
|
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
|
}
|
package/core/adapter-registry.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// remember to re-wire everything.
|
|
9
9
|
|
|
10
10
|
const { loadChannels } = require("./config");
|
|
11
|
-
const { setAdapters } = require("./
|
|
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");
|
|
@@ -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
|
-
|
|
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 =
|
|
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
|
-
|
|
919
|
-
|
|
920
|
-
|
|
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 =
|
|
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 =
|
|
929
|
-
|
|
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 <#>');
|