@inetafrica/open-claudia 2.0.2 → 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 +46 -0
- package/bin/loopback-client.js +99 -0
- package/bin/schedule.js +118 -0
- package/bin/task.js +93 -0
- package/bin/transcript-window.js +202 -0
- package/bot.js +3 -2
- package/core/actions.js +18 -7
- package/core/adapter-registry.js +1 -1
- package/core/config.js +6 -2
- package/core/handlers.js +22 -13
- package/core/jobs.js +99 -0
- package/core/loopback.js +153 -27
- package/core/runner.js +45 -21
- 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
|
@@ -233,6 +233,40 @@ switch (command) {
|
|
|
233
233
|
break;
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
+
case "tw":
|
|
237
|
+
case "transcript-window": {
|
|
238
|
+
require("./transcript-window").run(args.slice(1));
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
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
|
+
|
|
236
270
|
default:
|
|
237
271
|
console.log(`
|
|
238
272
|
Open Claudia — AI Coding Assistant via Telegram
|
|
@@ -252,6 +286,18 @@ Send tools (only work inside an active bot-spawned task):
|
|
|
252
286
|
open-claudia send-photo <path> [caption]
|
|
253
287
|
open-claudia send-voice <path>
|
|
254
288
|
|
|
289
|
+
Memory tools:
|
|
290
|
+
open-claudia transcript-window <pattern> Search project transcript, show hits with context
|
|
291
|
+
(alias: tw; --help for options)
|
|
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
|
+
|
|
255
301
|
Start options:
|
|
256
302
|
--web Also start the web UI
|
|
257
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 };
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// Search the project transcript JSONL for a pattern and print each hit
|
|
2
|
+
// with surrounding turns of context, capped to keep the output bounded.
|
|
3
|
+
//
|
|
4
|
+
// One JSONL line = one user or assistant turn. Output is plain text:
|
|
5
|
+
// match header, separator-delimited context blocks (before, HIT, after).
|
|
6
|
+
//
|
|
7
|
+
// Path resolution:
|
|
8
|
+
// 1. --path <file> flag
|
|
9
|
+
// 2. OC_TRANSCRIPT_PATH env (injected by core/runner.js for bot-spawned tasks)
|
|
10
|
+
// 3. error out — we don't try to guess
|
|
11
|
+
|
|
12
|
+
const fs = require("fs");
|
|
13
|
+
|
|
14
|
+
const DEFAULTS = {
|
|
15
|
+
before: 2,
|
|
16
|
+
after: 2,
|
|
17
|
+
maxTurns: 10,
|
|
18
|
+
maxChars: 1200,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function parseArgs(argv) {
|
|
22
|
+
const out = {
|
|
23
|
+
pattern: null,
|
|
24
|
+
before: DEFAULTS.before,
|
|
25
|
+
after: DEFAULTS.after,
|
|
26
|
+
maxTurns: DEFAULTS.maxTurns,
|
|
27
|
+
maxChars: DEFAULTS.maxChars,
|
|
28
|
+
regex: false,
|
|
29
|
+
path: null,
|
|
30
|
+
json: false,
|
|
31
|
+
help: false,
|
|
32
|
+
};
|
|
33
|
+
for (let i = 0; i < argv.length; i++) {
|
|
34
|
+
const a = argv[i];
|
|
35
|
+
if (a === "-h" || a === "--help") { out.help = true; continue; }
|
|
36
|
+
if (a === "--regex") { out.regex = true; continue; }
|
|
37
|
+
if (a === "--json") { out.json = true; continue; }
|
|
38
|
+
if (a === "--before") { out.before = parseInt(argv[++i], 10); continue; }
|
|
39
|
+
if (a === "--after") { out.after = parseInt(argv[++i], 10); continue; }
|
|
40
|
+
if (a === "--max-turns") { out.maxTurns = parseInt(argv[++i], 10); continue; }
|
|
41
|
+
if (a === "--max-chars") { out.maxChars = parseInt(argv[++i], 10); continue; }
|
|
42
|
+
if (a === "--path") { out.path = argv[++i]; continue; }
|
|
43
|
+
if (a.startsWith("--")) {
|
|
44
|
+
console.error(`Unknown flag: ${a}`);
|
|
45
|
+
process.exit(2);
|
|
46
|
+
}
|
|
47
|
+
if (out.pattern === null) { out.pattern = a; continue; }
|
|
48
|
+
console.error(`Unexpected argument: ${a}`);
|
|
49
|
+
process.exit(2);
|
|
50
|
+
}
|
|
51
|
+
if (!Number.isFinite(out.before) || out.before < 0) out.before = DEFAULTS.before;
|
|
52
|
+
if (!Number.isFinite(out.after) || out.after < 0) out.after = DEFAULTS.after;
|
|
53
|
+
if (!Number.isFinite(out.maxTurns) || out.maxTurns < 1) out.maxTurns = DEFAULTS.maxTurns;
|
|
54
|
+
if (!Number.isFinite(out.maxChars) || out.maxChars < 100) out.maxChars = DEFAULTS.maxChars;
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function printHelp() {
|
|
59
|
+
console.log(`Usage: open-claudia transcript-window <pattern> [options]
|
|
60
|
+
|
|
61
|
+
Search the active project transcript and print each hit with surrounding turns.
|
|
62
|
+
|
|
63
|
+
Options:
|
|
64
|
+
--before N Turns of context before each hit (default ${DEFAULTS.before})
|
|
65
|
+
--after N Turns of context after each hit (default ${DEFAULTS.after})
|
|
66
|
+
--max-turns K Stop after K hits (default ${DEFAULTS.maxTurns})
|
|
67
|
+
--max-chars C Cap per-turn text length (default ${DEFAULTS.maxChars})
|
|
68
|
+
--regex Treat pattern as a regex (default: literal, case-insensitive)
|
|
69
|
+
--path <file> Transcript path (default: OC_TRANSCRIPT_PATH env)
|
|
70
|
+
--json Emit JSONL of matched windows instead of pretty text
|
|
71
|
+
|
|
72
|
+
Exit codes: 0 hits found, 1 no hits, 2 usage error, 3 transcript unreadable.`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function resolveTranscriptPath(flagPath) {
|
|
76
|
+
if (flagPath) return flagPath;
|
|
77
|
+
if (process.env.OC_TRANSCRIPT_PATH) return process.env.OC_TRANSCRIPT_PATH;
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function loadTurns(transcriptPath) {
|
|
82
|
+
const raw = fs.readFileSync(transcriptPath, "utf-8");
|
|
83
|
+
const turns = [];
|
|
84
|
+
let lineNo = 0;
|
|
85
|
+
for (const line of raw.split("\n")) {
|
|
86
|
+
lineNo++;
|
|
87
|
+
if (!line.trim()) continue;
|
|
88
|
+
try {
|
|
89
|
+
const obj = JSON.parse(line);
|
|
90
|
+
turns.push({
|
|
91
|
+
lineNo,
|
|
92
|
+
timestamp: obj.timestamp || "",
|
|
93
|
+
role: obj.role || "",
|
|
94
|
+
text: typeof obj.text === "string" ? obj.text : "",
|
|
95
|
+
sessionId: obj.sessionId || "",
|
|
96
|
+
});
|
|
97
|
+
} catch (e) {
|
|
98
|
+
// skip malformed line
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return turns;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function buildMatcher(pattern, regex) {
|
|
105
|
+
if (regex) {
|
|
106
|
+
try { return new RegExp(pattern); }
|
|
107
|
+
catch (e) {
|
|
108
|
+
console.error(`Invalid regex: ${e.message}`);
|
|
109
|
+
process.exit(2);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const lc = pattern.toLowerCase();
|
|
113
|
+
return { test: (s) => s.toLowerCase().includes(lc) };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function truncate(text, maxChars) {
|
|
117
|
+
if (text.length <= maxChars) return text;
|
|
118
|
+
return text.slice(0, maxChars) + `\n[...truncated, ${text.length - maxChars} more chars]`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function formatTurn(turn, label, maxChars) {
|
|
122
|
+
return [
|
|
123
|
+
`--- ${label} line ${turn.lineNo} ${turn.timestamp} ${turn.role} ---`,
|
|
124
|
+
truncate(turn.text, maxChars),
|
|
125
|
+
].join("\n");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function run(argv) {
|
|
129
|
+
const opts = parseArgs(argv);
|
|
130
|
+
if (opts.help || !opts.pattern) {
|
|
131
|
+
printHelp();
|
|
132
|
+
process.exit(opts.help ? 0 : 2);
|
|
133
|
+
}
|
|
134
|
+
const transcriptPath = resolveTranscriptPath(opts.path);
|
|
135
|
+
if (!transcriptPath) {
|
|
136
|
+
console.error("No transcript path. Pass --path <file>, or run inside an active bot-spawned task (OC_TRANSCRIPT_PATH).");
|
|
137
|
+
process.exit(2);
|
|
138
|
+
}
|
|
139
|
+
if (!fs.existsSync(transcriptPath)) {
|
|
140
|
+
console.error(`Transcript not found: ${transcriptPath}`);
|
|
141
|
+
process.exit(3);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let turns;
|
|
145
|
+
try { turns = loadTurns(transcriptPath); }
|
|
146
|
+
catch (e) {
|
|
147
|
+
console.error(`Failed to read transcript: ${e.message}`);
|
|
148
|
+
process.exit(3);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const matcher = buildMatcher(opts.pattern, opts.regex);
|
|
152
|
+
const hits = [];
|
|
153
|
+
for (let i = 0; i < turns.length; i++) {
|
|
154
|
+
if (matcher.test(turns[i].text)) {
|
|
155
|
+
hits.push(i);
|
|
156
|
+
if (hits.length >= opts.maxTurns) break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (hits.length === 0) {
|
|
161
|
+
console.error(`No matches for ${JSON.stringify(opts.pattern)} in ${transcriptPath}`);
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (opts.json) {
|
|
166
|
+
for (let h = 0; h < hits.length; h++) {
|
|
167
|
+
const idx = hits[h];
|
|
168
|
+
const window = [];
|
|
169
|
+
for (let j = Math.max(0, idx - opts.before); j <= Math.min(turns.length - 1, idx + opts.after); j++) {
|
|
170
|
+
window.push({ ...turns[j], offset: j - idx });
|
|
171
|
+
}
|
|
172
|
+
process.stdout.write(JSON.stringify({ matchIndex: h + 1, matchLine: turns[idx].lineNo, window }) + "\n");
|
|
173
|
+
}
|
|
174
|
+
process.exit(0);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
console.log(`Transcript: ${transcriptPath}`);
|
|
178
|
+
console.log(`Pattern: ${JSON.stringify(opts.pattern)}${opts.regex ? " (regex)" : ""}`);
|
|
179
|
+
console.log(`Hits: ${hits.length} Window: -${opts.before}/+${opts.after} turns Per-turn cap: ${opts.maxChars} chars`);
|
|
180
|
+
console.log("");
|
|
181
|
+
|
|
182
|
+
for (let h = 0; h < hits.length; h++) {
|
|
183
|
+
const idx = hits[h];
|
|
184
|
+
const start = Math.max(0, idx - opts.before);
|
|
185
|
+
const end = Math.min(turns.length - 1, idx + opts.after);
|
|
186
|
+
console.log(`===== match ${h + 1}/${hits.length} line ${turns[idx].lineNo} =====`);
|
|
187
|
+
for (let j = start; j <= end; j++) {
|
|
188
|
+
const offset = j - idx;
|
|
189
|
+
const label = offset === 0 ? "HIT" : (offset < 0 ? `ctx ${offset}` : `ctx +${offset}`);
|
|
190
|
+
console.log(formatTurn(turns[j], label, opts.maxChars));
|
|
191
|
+
}
|
|
192
|
+
console.log("");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
process.exit(0);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
module.exports = { run, parseArgs, loadTurns };
|
|
199
|
+
|
|
200
|
+
if (require.main === module) {
|
|
201
|
+
run(process.argv.slice(2));
|
|
202
|
+
}
|
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);
|