@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 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 };
@@ -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 { 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);