@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.
@@ -6,9 +6,10 @@ const fs = require("fs");
6
6
  const path = require("path");
7
7
  const { SOUL_FILE, CRONS_FILE, VAULT_FILE, FILES_DIR, BOT_DIR, WHISPER_CLI, FFMPEG } = require("./config");
8
8
  const { currentState } = require("./state");
9
- const { currentAdapter } = require("./context");
9
+ const { currentAdapter, currentChannelId } = require("./context");
10
10
  const { vault } = require("./vault-store");
11
11
  const { transcriptPointerNote } = require("./transcripts");
12
+ const tasksStore = require("./tasks");
12
13
 
13
14
  function loadSoul() {
14
15
  try { return fs.readFileSync(SOUL_FILE, "utf-8"); }
@@ -20,10 +21,22 @@ function buildSystemPrompt() {
20
21
  const soul = loadSoul();
21
22
  const hasVoice = WHISPER_CLI && FFMPEG;
22
23
  const adapter = currentAdapter();
24
+ const channelId = currentChannelId();
23
25
  const channelLabel = adapter
24
26
  ? (adapter.type === "kazee" ? "Kazee Chat" : adapter.type === "telegram" ? "Telegram" : adapter.type)
25
27
  : "Telegram";
26
28
 
29
+ let pendingTasksBlock = "";
30
+ if (adapter && channelId) {
31
+ try {
32
+ const pending = tasksStore.pendingSummary(adapter.id, channelId);
33
+ if (pending.length > 0) {
34
+ const tree = tasksStore.formatTree(adapter.id, channelId, { showIds: true });
35
+ pendingTasksBlock = `\n## Pending tasks\nYou may be resuming prior work. Before starting anything new, review these and decide whether to continue, update, or abandon them. Mark a subtask in_progress when you actually start it and completed when it's done.\n\n${tree}\n`;
36
+ }
37
+ } catch (e) {}
38
+ }
39
+
27
40
  return `
28
41
  ${soul}
29
42
 
@@ -43,7 +56,7 @@ ${soul}
43
56
  - Received user files directory: ${FILES_DIR}
44
57
 
45
58
  ${transcriptPointerNote(state)}
46
-
59
+ ${pendingTasksBlock}
47
60
  ## Delivery
48
61
  Reply normally in your final answer. To send a file, image, or voice clip back to the current chat, run the bot CLI from inside this task — channel context is already in the env:
49
62
  - \`open-claudia send-file <path> [caption]\` — any document/binary
@@ -51,6 +64,34 @@ Reply normally in your final answer. To send a file, image, or voice clip back t
51
64
  - \`open-claudia send-voice <path>\` — ogg/opus voice note
52
65
  Never print or embed bot tokens in prompts, commands, logs, or messages.
53
66
 
67
+ ## Background work
68
+ You can persist work across turns and wake yourself up later. These survive bot restarts.
69
+
70
+ Wake-ups and crons (real schedulers, not hallucinations — use them instead of saying "I'll check back later"):
71
+ - \`open-claudia schedule-wakeup <when> "<prompt>"\` — one-shot. \`<when>\` is a duration like \`30s\`/\`5m\`/\`2h\`/\`1d\` or an ISO datetime. When it fires you wake up in this same conversation with the prompt as if the user typed it.
72
+ - \`open-claudia cron-add "<5-field cron>" "<prompt>"\` — recurring.
73
+ - \`open-claudia cron-list\` — list all wakeups + crons on this channel.
74
+ - \`open-claudia cron-remove <id>\` — cancel one.
75
+
76
+ Persistent todo list with plans + subtasks (per channel; survives compaction and restart).
77
+
78
+ For any work with 3+ distinct steps you MUST create a plan before starting. The plan is a parent task whose children are the steps. As you work, mark each subtask in_progress when you begin and completed when done — this is how a resumed turn (after compaction, restart, or wakeup) sees where you left off. If pending tasks already exist when you start a turn they will be shown under "## Pending tasks" above; check them first.
79
+
80
+ - \`open-claudia task plan "<plan title>" "<step 1>" "<step 2>" "<step 3>" [--description "<why / success criteria>"]\` — atomic create
81
+ - \`open-claudia task add "<content>" [--parent <plan-id>] [--description "..."]\` — add a single task or subtask
82
+ - \`open-claudia task list\` — renders as a tree
83
+ - \`open-claudia task start <id>\` / \`task done <id>\` / \`task remove <id>\` (removing a plan removes its subtasks)
84
+ - \`open-claudia task clear-completed\` — only clears plans whose subtasks are all done
85
+
86
+ For one-off work under 3 steps, skip the plan and just track it in your own response.
87
+
88
+ Sub-agents (spawn a fresh throwaway Claude for focused research — output comes back on stdout):
89
+ - \`open-claudia agent "<prompt>" [--role "<role>"]\`
90
+ - Use when a side question would pollute this conversation, or to fan out independent lookups.
91
+ - The sub-agent has Read/Glob/Grep/Bash but no access to your chat session or send-* tools.
92
+
93
+ If you tell the user "I'll check back in N minutes" or "I'll run this every morning", you MUST schedule it with one of the commands above in the same turn. Otherwise nothing happens.
94
+
54
95
  ## Guidelines
55
96
  - Keep responses concise — many users are on mobile.
56
97
  - Markdown: *bold*, _italic_, \`code\`, \`\`\`code blocks\`\`\` work on both Telegram and Kazee. Skip headers (#) and links [text](url).
package/core/tasks.js ADDED
@@ -0,0 +1,168 @@
1
+ // Per-channel persistent todo list. The agent uses this to track
2
+ // multi-step work that should survive a turn, a compaction, or a
3
+ // restart. Scoped by adapter+channel so Telegram and Kazee see
4
+ // separate lists.
5
+ //
6
+ // Tasks support a single level of hierarchy via parentId: a top-level
7
+ // "plan" task can have child subtasks. This lets the agent break a big
8
+ // piece of work into a plan whose progress is visible at a glance.
9
+
10
+ const fs = require("fs");
11
+ const path = require("path");
12
+ const { TASKS_DIR } = require("./config");
13
+
14
+ function safe(s) {
15
+ return String(s || "").replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 80);
16
+ }
17
+
18
+ function filePathFor(adapter, channelId) {
19
+ fs.mkdirSync(TASKS_DIR, { recursive: true });
20
+ return path.join(TASKS_DIR, `${safe(adapter)}-${safe(channelId)}.json`);
21
+ }
22
+
23
+ function load(adapter, channelId) {
24
+ try {
25
+ const raw = JSON.parse(fs.readFileSync(filePathFor(adapter, channelId), "utf-8"));
26
+ return Array.isArray(raw) ? raw : [];
27
+ } catch (e) { return []; }
28
+ }
29
+
30
+ function save(adapter, channelId, list) {
31
+ const f = filePathFor(adapter, channelId);
32
+ const tmp = `${f}.tmp`;
33
+ fs.writeFileSync(tmp, JSON.stringify(list, null, 2));
34
+ fs.renameSync(tmp, f);
35
+ }
36
+
37
+ function nextId() {
38
+ return `task_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
39
+ }
40
+
41
+ function add(adapter, channelId, content, opts = {}) {
42
+ const list = load(adapter, channelId);
43
+ const parentId = opts.parentId || null;
44
+ if (parentId) {
45
+ const parent = list.find((t) => t.id === parentId);
46
+ if (!parent) throw new Error(`parent task not found: ${parentId}`);
47
+ if (parent.parentId) throw new Error(`nested subtasks not supported: ${parentId} is already a subtask`);
48
+ }
49
+ const t = {
50
+ id: nextId(),
51
+ content: String(content || "").trim(),
52
+ status: "pending",
53
+ parentId,
54
+ description: opts.description ? String(opts.description) : null,
55
+ createdAt: Date.now(),
56
+ updatedAt: Date.now(),
57
+ };
58
+ list.push(t);
59
+ save(adapter, channelId, list);
60
+ return t;
61
+ }
62
+
63
+ function plan(adapter, channelId, title, subtaskContents, opts = {}) {
64
+ const parent = add(adapter, channelId, title, { description: opts.description || null });
65
+ const children = [];
66
+ for (const c of subtaskContents || []) {
67
+ if (!String(c || "").trim()) continue;
68
+ children.push(add(adapter, channelId, c, { parentId: parent.id }));
69
+ }
70
+ return { parent, children };
71
+ }
72
+
73
+ function update(adapter, channelId, id, patch) {
74
+ const list = load(adapter, channelId);
75
+ const idx = list.findIndex((t) => t.id === id);
76
+ if (idx < 0) return null;
77
+ list[idx] = { ...list[idx], ...patch, updatedAt: Date.now() };
78
+ save(adapter, channelId, list);
79
+ return list[idx];
80
+ }
81
+
82
+ function remove(adapter, channelId, id) {
83
+ const list = load(adapter, channelId);
84
+ const target = list.find((t) => t.id === id);
85
+ if (!target) return null;
86
+ const ids = new Set([id]);
87
+ if (!target.parentId) {
88
+ for (const t of list) if (t.parentId === id) ids.add(t.id);
89
+ }
90
+ const next = list.filter((t) => !ids.has(t.id));
91
+ save(adapter, channelId, next);
92
+ return { removed: target, alsoRemoved: ids.size - 1 };
93
+ }
94
+
95
+ function clearCompleted(adapter, channelId) {
96
+ const all = load(adapter, channelId);
97
+ const drop = new Set();
98
+ for (const t of all) {
99
+ if (t.status !== "completed") continue;
100
+ if (t.parentId) { drop.add(t.id); continue; }
101
+ const kids = all.filter((c) => c.parentId === t.id);
102
+ if (kids.length === 0 || kids.every((c) => c.status === "completed")) {
103
+ drop.add(t.id);
104
+ for (const c of kids) drop.add(c.id);
105
+ }
106
+ }
107
+ const remaining = all.filter((t) => !drop.has(t.id));
108
+ save(adapter, channelId, remaining);
109
+ return remaining;
110
+ }
111
+
112
+ function list(adapter, channelId, opts = {}) {
113
+ const all = load(adapter, channelId);
114
+ if (opts.status) return all.filter((t) => t.status === opts.status);
115
+ return all;
116
+ }
117
+
118
+ function tree(adapter, channelId) {
119
+ const all = load(adapter, channelId);
120
+ const byParent = new Map();
121
+ const roots = [];
122
+ for (const t of all) {
123
+ if (t.parentId) {
124
+ if (!byParent.has(t.parentId)) byParent.set(t.parentId, []);
125
+ byParent.get(t.parentId).push(t);
126
+ } else {
127
+ roots.push(t);
128
+ }
129
+ }
130
+ return roots.map((r) => ({ ...r, children: byParent.get(r.id) || [] }));
131
+ }
132
+
133
+ function pendingSummary(adapter, channelId) {
134
+ const all = load(adapter, channelId);
135
+ return all.filter((t) => t.status === "pending" || t.status === "in_progress");
136
+ }
137
+
138
+ function statusMark(t) {
139
+ return t.status === "completed" ? "[x]" : t.status === "in_progress" ? "[~]" : "[ ]";
140
+ }
141
+
142
+ function format(t, idx) {
143
+ const num = typeof idx === "number" ? `${idx + 1}. ` : "";
144
+ return `${num}${statusMark(t)} ${t.content}`;
145
+ }
146
+
147
+ function formatTree(adapter, channelId, opts = {}) {
148
+ const showIds = opts.showIds !== false;
149
+ const t = tree(adapter, channelId);
150
+ const lines = [];
151
+ t.forEach((root, i) => {
152
+ const head = `${i + 1}. ${statusMark(root)} ${root.content}` + (showIds ? ` (${root.id})` : "");
153
+ lines.push(head);
154
+ if (root.description) lines.push(` ${root.description}`);
155
+ root.children.forEach((child) => {
156
+ lines.push(` ${statusMark(child)} ${child.content}` + (showIds ? ` (${child.id})` : ""));
157
+ });
158
+ });
159
+ return lines.join("\n");
160
+ }
161
+
162
+ module.exports = {
163
+ filePathFor,
164
+ load, save,
165
+ add, plan, update, remove, list, tree, clearCompleted,
166
+ pendingSummary,
167
+ format, formatTree, statusMark,
168
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "2.0.3",
3
+ "version": "2.0.5",
4
4
  "description": "Your always-on AI coding assistant — Claude Code, Cursor Agent, and OpenAI Codex via Telegram or Kazee Chat",
5
5
  "main": "bot.js",
6
6
  "bin": {
package/core/cron.js DELETED
@@ -1,77 +0,0 @@
1
- // Cron loader and scheduler. Cron jobs fire from a timer (no inbound
2
- // message, so no chatContext) — we resolve the owner's preferred
3
- // channel from identities and bind the run to that adapter.
4
-
5
- const fs = require("fs");
6
- const path = require("path");
7
- const cron = require("node-cron");
8
- const { CRONS_FILE, WORKSPACE, CHAT_ID } = require("./config");
9
- const { runInChat } = require("./context");
10
- const { canonicalForTelegram, identities } = require("./identity");
11
- const { runClaudeSilent } = require("./runner");
12
-
13
- const activeCrons = new Map();
14
- let adaptersById = new Map();
15
-
16
- function setAdapters(adapters) {
17
- adaptersById = new Map();
18
- for (const a of adapters) adaptersById.set(a.type, a);
19
- }
20
-
21
- function loadCrons() {
22
- try { return JSON.parse(fs.readFileSync(CRONS_FILE, "utf-8")); }
23
- catch (e) { return []; }
24
- }
25
-
26
- function saveCrons(list) {
27
- fs.writeFileSync(CRONS_FILE, JSON.stringify(list, null, 2));
28
- }
29
-
30
- function ownerDispatchTarget() {
31
- // Prefer an explicit owner mapping; fall back to legacy Telegram CHAT_ID.
32
- const ownerId = canonicalForTelegram(CHAT_ID || "");
33
- const preferred = identities.preferred[ownerId];
34
- if (preferred && preferred.transport && adaptersById.get(preferred.transport)) {
35
- return {
36
- adapter: adaptersById.get(preferred.transport),
37
- channelId: String(preferred.channelId),
38
- canonicalUserId: ownerId,
39
- };
40
- }
41
- const tg = adaptersById.get("telegram");
42
- if (tg && CHAT_ID) {
43
- return { adapter: tg, channelId: String(CHAT_ID), canonicalUserId: ownerId };
44
- }
45
- return null;
46
- }
47
-
48
- function scheduleCron(c) {
49
- const cwd = path.join(WORKSPACE, c.project);
50
- if (activeCrons.has(c.id)) activeCrons.get(c.id).task.stop();
51
- const task = cron.schedule(c.schedule, () => {
52
- const target = ownerDispatchTarget();
53
- if (!target) {
54
- console.error(`Cron ${c.label}: no adapter available to deliver result`);
55
- return;
56
- }
57
- runInChat(target, () => runClaudeSilent(c.prompt, cwd, c.label));
58
- });
59
- activeCrons.set(c.id, { task, config: c });
60
- }
61
-
62
- function initCrons() {
63
- for (const c of loadCrons()) {
64
- try { scheduleCron(c); }
65
- catch (e) { console.error("Cron error:", e.message); }
66
- }
67
- console.log(`Loaded ${loadCrons().length} cron(s)`);
68
- }
69
-
70
- module.exports = {
71
- activeCrons,
72
- setAdapters,
73
- loadCrons,
74
- saveCrons,
75
- scheduleCron,
76
- initCrons,
77
- };