@inetafrica/open-claudia 2.0.4 → 2.1.0
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/cli.js +23 -0
- package/bin/intros.js +75 -0
- package/bin/people.js +159 -0
- package/bin/recent.js +46 -0
- package/bin/send-to.js +52 -0
- package/bin/task.js +74 -13
- package/core/access.js +10 -1
- package/core/actions.js +34 -0
- package/core/audit.js +23 -0
- package/core/config.js +7 -3
- package/core/intro-flow.js +241 -0
- package/core/intros.js +158 -0
- package/core/loopback.js +243 -3
- package/core/people.js +247 -0
- package/core/relay.js +61 -0
- package/core/router.js +8 -1
- package/core/system-prompt.js +69 -6
- package/core/tasks.js +89 -13
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -267,6 +267,23 @@ switch (command) {
|
|
|
267
267
|
break;
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
+
case "people": {
|
|
271
|
+
require("./people").run(args.slice(1));
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
case "intros": {
|
|
275
|
+
require("./intros").run(args.slice(1));
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
case "send-to": {
|
|
279
|
+
require("./send-to").run(args.slice(1));
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
case "recent": {
|
|
283
|
+
require("./recent").run(args.slice(1));
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
|
|
270
287
|
default:
|
|
271
288
|
console.log(`
|
|
272
289
|
Open Claudia — AI Coding Assistant via Telegram
|
|
@@ -298,6 +315,12 @@ Background work (only inside an active bot-spawned task):
|
|
|
298
315
|
open-claudia task add|list|start|done|remove Per-channel todo (survives restarts)
|
|
299
316
|
open-claudia agent "<prompt>" [--role X] Spawn a throwaway sub-agent for research
|
|
300
317
|
|
|
318
|
+
Multi-user / cross-channel:
|
|
319
|
+
open-claudia people list|show|add|note|link Roster of known team members
|
|
320
|
+
open-claudia intros list|approve|reject Pending introductions from unknown chats
|
|
321
|
+
open-claudia send-to --person "<name>" "<msg>" Relay a message to another team member
|
|
322
|
+
open-claudia recent --person "<name>" [--limit] Read recent activity from another chat
|
|
323
|
+
|
|
301
324
|
Start options:
|
|
302
325
|
--web Also start the web UI
|
|
303
326
|
--quick Skip slow health checks (Claude auth, Telegram API)
|
package/bin/intros.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// open-claudia intros <subcommand>
|
|
2
|
+
// Pending introductions from unknown chats. Owner-only mutations.
|
|
3
|
+
|
|
4
|
+
const { postJson } = require("./loopback-client");
|
|
5
|
+
|
|
6
|
+
const HELP = `
|
|
7
|
+
Pending introductions from unknown chats.
|
|
8
|
+
|
|
9
|
+
open-claudia intros list
|
|
10
|
+
open-claudia intros approve <id>
|
|
11
|
+
open-claudia intros reject <id> [--reason "<...>"]
|
|
12
|
+
|
|
13
|
+
Owner-only: approve, reject.
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
function takeFlag(args, name) {
|
|
17
|
+
const idx = args.indexOf(`--${name}`);
|
|
18
|
+
if (idx < 0) return null;
|
|
19
|
+
const v = args[idx + 1];
|
|
20
|
+
args.splice(idx, 2);
|
|
21
|
+
return v;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function formatIntro(i) {
|
|
25
|
+
const claim = i.claim || {};
|
|
26
|
+
let claimLine = "(no claim yet)";
|
|
27
|
+
if (claim.kind === "existing") claimLine = `claims to be person ${claim.personId}`;
|
|
28
|
+
if (claim.kind === "new") claimLine = `new person "${claim.name}"${claim.bio ? ` — ${claim.bio}` : ""}`;
|
|
29
|
+
return `${i.id} ${i.adapter}:${i.channelId} step=${i.step} ${claimLine}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function runList() {
|
|
33
|
+
try {
|
|
34
|
+
const res = await postJson("intros-list", {});
|
|
35
|
+
const list = res.intros || [];
|
|
36
|
+
if (list.length === 0) { console.log("No pending intros."); process.exit(0); }
|
|
37
|
+
for (const i of list) console.log(formatIntro(i));
|
|
38
|
+
process.exit(0);
|
|
39
|
+
} catch (e) { console.error(e.message); process.exit(1); }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function runApprove(args) {
|
|
43
|
+
const id = args[0];
|
|
44
|
+
if (!id) { console.error("Usage: intros approve <id>"); process.exit(2); }
|
|
45
|
+
try {
|
|
46
|
+
const res = await postJson("intros-approve", { id });
|
|
47
|
+
console.log(`Approved. Person: ${res.person?.name || "?"} (${res.person?.id || "?"}).`);
|
|
48
|
+
process.exit(0);
|
|
49
|
+
} catch (e) { console.error(e.message); process.exit(1); }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function runReject(args) {
|
|
53
|
+
const reason = takeFlag(args, "reason");
|
|
54
|
+
const id = args[0];
|
|
55
|
+
if (!id) { console.error("Usage: intros reject <id> [--reason \"<...>\"]"); process.exit(2); }
|
|
56
|
+
try {
|
|
57
|
+
await postJson("intros-reject", { id, reason });
|
|
58
|
+
console.log(`Rejected ${id}.`);
|
|
59
|
+
process.exit(0);
|
|
60
|
+
} catch (e) { console.error(e.message); process.exit(1); }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function run(args) {
|
|
64
|
+
const sub = args[0];
|
|
65
|
+
const rest = args.slice(1);
|
|
66
|
+
if (!sub || sub === "--help" || sub === "help") { console.log(HELP); process.exit(sub ? 0 : 2); }
|
|
67
|
+
switch (sub) {
|
|
68
|
+
case "list": case "ls": return runList();
|
|
69
|
+
case "approve": return runApprove(rest);
|
|
70
|
+
case "reject": return runReject(rest);
|
|
71
|
+
default: console.error(`Unknown intros subcommand: ${sub}`); console.log(HELP); process.exit(2);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = { run, HELP };
|
package/bin/people.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// open-claudia people <subcommand>
|
|
2
|
+
// Roster of team members the bot knows about. Owner-only mutations;
|
|
3
|
+
// list/show/note are open to any authorized caller.
|
|
4
|
+
|
|
5
|
+
const { postJson } = require("./loopback-client");
|
|
6
|
+
|
|
7
|
+
const HELP = `
|
|
8
|
+
Team roster — humans the bot knows.
|
|
9
|
+
|
|
10
|
+
open-claudia people list
|
|
11
|
+
open-claudia people show <id|name>
|
|
12
|
+
open-claudia people add "<name>" [--bio "<...>"] [--owner]
|
|
13
|
+
open-claudia people note <id|name> "<note>"
|
|
14
|
+
open-claudia people link <id|name> --adapter <kazee|telegram> --channel <id>
|
|
15
|
+
open-claudia people unlink <id|name> --adapter <type> --channel <id>
|
|
16
|
+
open-claudia people set-primary <id|name> --adapter <type> --channel <id>
|
|
17
|
+
open-claudia people remove <id|name>
|
|
18
|
+
|
|
19
|
+
Owner-only: add, link, unlink, set-primary, remove.
|
|
20
|
+
`;
|
|
21
|
+
|
|
22
|
+
function takeFlag(args, name) {
|
|
23
|
+
const idx = args.indexOf(`--${name}`);
|
|
24
|
+
if (idx < 0) return null;
|
|
25
|
+
const v = args[idx + 1];
|
|
26
|
+
args.splice(idx, 2);
|
|
27
|
+
return v;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function hasFlag(args, name) {
|
|
31
|
+
const idx = args.indexOf(`--${name}`);
|
|
32
|
+
if (idx < 0) return false;
|
|
33
|
+
args.splice(idx, 1);
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function formatPerson(p) {
|
|
38
|
+
const handles = (p.handles || []).map((h) => `${h.adapter}:${h.channelId}${p.primaryChannel && p.primaryChannel.adapter === h.adapter && String(p.primaryChannel.channelId) === String(h.channelId) ? " *" : ""}`).join(", ") || "(none)";
|
|
39
|
+
const lines = [
|
|
40
|
+
`${p.name}${p.isOwner ? " (owner)" : ""} [${p.id}]`,
|
|
41
|
+
` Handles: ${handles}`,
|
|
42
|
+
];
|
|
43
|
+
if (p.bio) lines.push(` Bio: ${p.bio}`);
|
|
44
|
+
const notes = p.notes || [];
|
|
45
|
+
if (notes.length) {
|
|
46
|
+
lines.push(" Recent notes:");
|
|
47
|
+
for (const n of notes.slice(-5)) lines.push(` - [${(n.at || "").slice(0, 10)}] ${n.text}`);
|
|
48
|
+
}
|
|
49
|
+
return lines.join("\n");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function runList() {
|
|
53
|
+
try {
|
|
54
|
+
const res = await postJson("people-list", {});
|
|
55
|
+
const list = res.people || [];
|
|
56
|
+
if (list.length === 0) { console.log("No people."); process.exit(0); }
|
|
57
|
+
for (const p of list) console.log(formatPerson(p));
|
|
58
|
+
process.exit(0);
|
|
59
|
+
} catch (e) { console.error(e.message); process.exit(1); }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function runShow(args) {
|
|
63
|
+
const ref = args.join(" ").trim();
|
|
64
|
+
if (!ref) { console.error("Usage: people show <id|name>"); process.exit(2); }
|
|
65
|
+
try {
|
|
66
|
+
const res = await postJson("people-show", { id: ref, name: ref });
|
|
67
|
+
console.log(formatPerson(res.person));
|
|
68
|
+
process.exit(0);
|
|
69
|
+
} catch (e) { console.error(e.message); process.exit(1); }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function runAdd(args) {
|
|
73
|
+
const bio = takeFlag(args, "bio");
|
|
74
|
+
const isOwner = hasFlag(args, "owner");
|
|
75
|
+
const name = args.join(" ").trim();
|
|
76
|
+
if (!name) { console.error("Usage: people add \"<name>\" [--bio \"<...>\"] [--owner]"); process.exit(2); }
|
|
77
|
+
try {
|
|
78
|
+
const res = await postJson("people-add", { name, bio, isOwner });
|
|
79
|
+
console.log(`Added ${res.person.name} (${res.person.id})${isOwner ? " as owner" : ""}.`);
|
|
80
|
+
process.exit(0);
|
|
81
|
+
} catch (e) { console.error(e.message); process.exit(1); }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function runNote(args) {
|
|
85
|
+
const ref = args.shift();
|
|
86
|
+
const text = args.join(" ").trim();
|
|
87
|
+
if (!ref || !text) { console.error("Usage: people note <id|name> \"<note>\""); process.exit(2); }
|
|
88
|
+
try {
|
|
89
|
+
const res = await postJson("people-note", { id: ref, name: ref, text });
|
|
90
|
+
console.log(`Noted [${res.note.at?.slice(0, 19)}]: ${res.note.text}`);
|
|
91
|
+
process.exit(0);
|
|
92
|
+
} catch (e) { console.error(e.message); process.exit(1); }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function runLink(args) {
|
|
96
|
+
const linkAdapter = takeFlag(args, "adapter");
|
|
97
|
+
const linkChannelId = takeFlag(args, "channel");
|
|
98
|
+
const displayName = takeFlag(args, "display");
|
|
99
|
+
const ref = args.join(" ").trim();
|
|
100
|
+
if (!ref || !linkAdapter || !linkChannelId) { console.error("Usage: people link <id|name> --adapter <type> --channel <id>"); process.exit(2); }
|
|
101
|
+
try {
|
|
102
|
+
const res = await postJson("people-link", { id: ref, name: ref, linkAdapter, linkChannelId, displayName });
|
|
103
|
+
console.log(`Linked ${linkAdapter}:${linkChannelId} → ${res.person.name}.`);
|
|
104
|
+
process.exit(0);
|
|
105
|
+
} catch (e) { console.error(e.message); process.exit(1); }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function runUnlink(args) {
|
|
109
|
+
const linkAdapter = takeFlag(args, "adapter");
|
|
110
|
+
const linkChannelId = takeFlag(args, "channel");
|
|
111
|
+
const ref = args.join(" ").trim();
|
|
112
|
+
if (!ref || !linkAdapter || !linkChannelId) { console.error("Usage: people unlink <id|name> --adapter <type> --channel <id>"); process.exit(2); }
|
|
113
|
+
try {
|
|
114
|
+
const res = await postJson("people-unlink", { id: ref, name: ref, linkAdapter, linkChannelId });
|
|
115
|
+
console.log(`Unlinked ${linkAdapter}:${linkChannelId} from ${res.person.name}.`);
|
|
116
|
+
process.exit(0);
|
|
117
|
+
} catch (e) { console.error(e.message); process.exit(1); }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function runSetPrimary(args) {
|
|
121
|
+
const linkAdapter = takeFlag(args, "adapter");
|
|
122
|
+
const linkChannelId = takeFlag(args, "channel");
|
|
123
|
+
const ref = args.join(" ").trim();
|
|
124
|
+
if (!ref || !linkAdapter || !linkChannelId) { console.error("Usage: people set-primary <id|name> --adapter <type> --channel <id>"); process.exit(2); }
|
|
125
|
+
try {
|
|
126
|
+
const res = await postJson("people-set-primary", { id: ref, name: ref, linkAdapter, linkChannelId });
|
|
127
|
+
console.log(`Primary for ${res.person.name} → ${linkAdapter}:${linkChannelId}.`);
|
|
128
|
+
process.exit(0);
|
|
129
|
+
} catch (e) { console.error(e.message); process.exit(1); }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function runRemove(args) {
|
|
133
|
+
const ref = args.join(" ").trim();
|
|
134
|
+
if (!ref) { console.error("Usage: people remove <id|name>"); process.exit(2); }
|
|
135
|
+
try {
|
|
136
|
+
const res = await postJson("people-remove", { id: ref, name: ref });
|
|
137
|
+
console.log(`Removed ${res.removed.name}.`);
|
|
138
|
+
process.exit(0);
|
|
139
|
+
} catch (e) { console.error(e.message); process.exit(1); }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function run(args) {
|
|
143
|
+
const sub = args[0];
|
|
144
|
+
const rest = args.slice(1);
|
|
145
|
+
if (!sub || sub === "--help" || sub === "help") { console.log(HELP); process.exit(sub ? 0 : 2); }
|
|
146
|
+
switch (sub) {
|
|
147
|
+
case "list": case "ls": return runList();
|
|
148
|
+
case "show": return runShow(rest);
|
|
149
|
+
case "add": return runAdd(rest);
|
|
150
|
+
case "note": return runNote(rest);
|
|
151
|
+
case "link": return runLink(rest);
|
|
152
|
+
case "unlink": return runUnlink(rest);
|
|
153
|
+
case "set-primary": return runSetPrimary(rest);
|
|
154
|
+
case "remove": case "rm": return runRemove(rest);
|
|
155
|
+
default: console.error(`Unknown people subcommand: ${sub}`); console.log(HELP); process.exit(2);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = { run, HELP };
|
package/bin/recent.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// open-claudia recent — fetch recent activity from another chat without
|
|
2
|
+
// pulling it into every system prompt. Reads from transcript jsonls.
|
|
3
|
+
|
|
4
|
+
const { postJson } = require("./loopback-client");
|
|
5
|
+
|
|
6
|
+
const HELP = `
|
|
7
|
+
Read recent activity from a chat.
|
|
8
|
+
|
|
9
|
+
open-claudia recent --person "<name>" [--limit 20]
|
|
10
|
+
open-claudia recent --adapter <type> --channel <id> [--limit 20]
|
|
11
|
+
`;
|
|
12
|
+
|
|
13
|
+
function takeFlag(args, name) {
|
|
14
|
+
const idx = args.indexOf(`--${name}`);
|
|
15
|
+
if (idx < 0) return null;
|
|
16
|
+
const v = args[idx + 1];
|
|
17
|
+
args.splice(idx, 2);
|
|
18
|
+
return v;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function run(args) {
|
|
22
|
+
if (args[0] === "--help" || args[0] === "help") { console.log(HELP); process.exit(0); }
|
|
23
|
+
const personName = takeFlag(args, "person");
|
|
24
|
+
const personId = takeFlag(args, "person-id");
|
|
25
|
+
const adapter = takeFlag(args, "adapter");
|
|
26
|
+
const channelId = takeFlag(args, "channel");
|
|
27
|
+
const limit = takeFlag(args, "limit") || 20;
|
|
28
|
+
if (!personName && !personId && !(adapter && channelId)) {
|
|
29
|
+
console.error("Need a target: --person or --adapter/--channel."); process.exit(2);
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const res = await postJson("recent-fetch", { personName, personId, adapter, channelId, limit });
|
|
33
|
+
const entries = res.entries || [];
|
|
34
|
+
if (entries.length === 0) { console.log("(no recent activity)"); process.exit(0); }
|
|
35
|
+
console.log(`Recent on ${res.adapter}:${res.channelId} (${entries.length}):\n`);
|
|
36
|
+
for (const e of entries) {
|
|
37
|
+
const at = (e.at || "").slice(0, 19);
|
|
38
|
+
const role = e.role || e.kind || "?";
|
|
39
|
+
const txt = (e.text || "").replace(/\s+/g, " ").slice(0, 200);
|
|
40
|
+
console.log(`[${at}] ${role}: ${txt}`);
|
|
41
|
+
}
|
|
42
|
+
process.exit(0);
|
|
43
|
+
} catch (e) { console.error(e.message); process.exit(1); }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { run, HELP };
|
package/bin/send-to.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// open-claudia send-to — cross-channel relay primitive. Send a message
|
|
2
|
+
// to another person on their primary channel (or an explicit target).
|
|
3
|
+
|
|
4
|
+
const { postJson } = require("./loopback-client");
|
|
5
|
+
|
|
6
|
+
const HELP = `
|
|
7
|
+
Send a message to another team member or an explicit channel.
|
|
8
|
+
|
|
9
|
+
open-claudia send-to --person "<name>" "<message>"
|
|
10
|
+
open-claudia send-to --person-id <id> "<message>"
|
|
11
|
+
open-claudia send-to --canonical <id> "<message>"
|
|
12
|
+
open-claudia send-to --adapter <type> --channel <id> "<message>"
|
|
13
|
+
|
|
14
|
+
Be honest about provenance — prefix forwarded messages with "From <name>: "
|
|
15
|
+
and autonomous ones with "Open Claudia: ". Every relay is logged.
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
function takeFlag(args, name) {
|
|
19
|
+
const idx = args.indexOf(`--${name}`);
|
|
20
|
+
if (idx < 0) return null;
|
|
21
|
+
const v = args[idx + 1];
|
|
22
|
+
args.splice(idx, 2);
|
|
23
|
+
return v;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function run(args) {
|
|
27
|
+
if (!args.length || args[0] === "--help" || args[0] === "help") {
|
|
28
|
+
console.log(HELP); process.exit(args[0] === "--help" || args[0] === "help" ? 0 : 2);
|
|
29
|
+
}
|
|
30
|
+
const targetPersonName = takeFlag(args, "person");
|
|
31
|
+
const targetPersonId = takeFlag(args, "person-id");
|
|
32
|
+
const targetCanonicalUserId = takeFlag(args, "canonical");
|
|
33
|
+
const targetAdapter = takeFlag(args, "adapter");
|
|
34
|
+
const targetChannelId = takeFlag(args, "channel");
|
|
35
|
+
const text = args.join(" ").trim();
|
|
36
|
+
if (!text) { console.error("Missing message text."); process.exit(2); }
|
|
37
|
+
if (!targetPersonName && !targetPersonId && !targetCanonicalUserId && !(targetAdapter && targetChannelId)) {
|
|
38
|
+
console.error("Need a target: --person, --person-id, --canonical, or --adapter/--channel.");
|
|
39
|
+
process.exit(2);
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const res = await postJson("relay-send", {
|
|
43
|
+
text, targetPersonName, targetPersonId, targetCanonicalUserId, targetAdapter, targetChannelId,
|
|
44
|
+
});
|
|
45
|
+
const to = res.to || {};
|
|
46
|
+
const who = to.person ? `${to.person.name} on ${to.adapter}` : `${to.adapter}:${to.channelId}`;
|
|
47
|
+
console.log(`Sent to ${who}. messageId=${res.messageId || "?"}`);
|
|
48
|
+
process.exit(0);
|
|
49
|
+
} catch (e) { console.error(e.message); process.exit(1); }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = { run, HELP };
|
package/bin/task.js
CHANGED
|
@@ -1,42 +1,101 @@
|
|
|
1
1
|
// open-claudia task <subcommand>
|
|
2
2
|
// Per-channel todo list. Use this for multi-step work so progress
|
|
3
|
-
// survives compactions and restarts.
|
|
3
|
+
// survives compactions and restarts. Supports one level of hierarchy:
|
|
4
|
+
// a parent "plan" task can have child subtasks via --parent.
|
|
4
5
|
|
|
5
6
|
const { postJson } = require("./loopback-client");
|
|
6
7
|
|
|
7
8
|
const HELP = `
|
|
8
|
-
Per-channel todo list.
|
|
9
|
+
Per-channel todo list with optional plan/subtask hierarchy.
|
|
9
10
|
|
|
10
|
-
open-claudia task add "<content>"
|
|
11
|
+
open-claudia task add "<content>" [--parent <id>] [--description "<...>"]
|
|
12
|
+
open-claudia task plan "<title>" "<sub1>" "<sub2>" ... [--description "<...>"]
|
|
11
13
|
open-claudia task list [--status pending|in_progress|completed]
|
|
12
14
|
open-claudia task start <id>
|
|
13
15
|
open-claudia task done <id>
|
|
14
|
-
open-claudia task remove <id>
|
|
15
|
-
open-claudia task clear-completed
|
|
16
|
+
open-claudia task remove <id> # removing a plan removes its subtasks
|
|
17
|
+
open-claudia task clear-completed # only clears plans whose subtasks are all done
|
|
18
|
+
|
|
19
|
+
Use plans for any work with 3+ distinct steps. Mark subtasks in_progress
|
|
20
|
+
as you start them and done as you finish, so a resumed turn or a new
|
|
21
|
+
turn after compaction can pick up where you left off.
|
|
16
22
|
`;
|
|
17
23
|
|
|
18
|
-
function
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
function statusMark(t) {
|
|
25
|
+
return t.status === "completed" ? "[x]" : t.status === "in_progress" ? "[~]" : "[ ]";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function takeFlag(args, name) {
|
|
29
|
+
const idx = args.indexOf(`--${name}`);
|
|
30
|
+
if (idx < 0) return null;
|
|
31
|
+
const v = args[idx + 1];
|
|
32
|
+
args.splice(idx, 2);
|
|
33
|
+
return v;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function renderTree(list) {
|
|
37
|
+
const byParent = new Map();
|
|
38
|
+
const roots = [];
|
|
39
|
+
for (const t of list) {
|
|
40
|
+
if (t.parentId) {
|
|
41
|
+
if (!byParent.has(t.parentId)) byParent.set(t.parentId, []);
|
|
42
|
+
byParent.get(t.parentId).push(t);
|
|
43
|
+
} else {
|
|
44
|
+
roots.push(t);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const lines = [];
|
|
48
|
+
roots.forEach((root, i) => {
|
|
49
|
+
lines.push(`${i + 1}. ${statusMark(root)} ${root.content} (${root.id})`);
|
|
50
|
+
if (root.description) lines.push(` ${root.description}`);
|
|
51
|
+
(byParent.get(root.id) || []).forEach((c) => {
|
|
52
|
+
lines.push(` ${statusMark(c)} ${c.content} (${c.id})`);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
for (const orphan of list) {
|
|
56
|
+
if (orphan.parentId && !roots.find((r) => r.id === orphan.parentId)) {
|
|
57
|
+
lines.push(` ${statusMark(orphan)} ${orphan.content} (${orphan.id}) [orphan]`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return lines.join("\n");
|
|
21
61
|
}
|
|
22
62
|
|
|
23
63
|
async function runAdd(args) {
|
|
64
|
+
const parentId = takeFlag(args, "parent");
|
|
65
|
+
const description = takeFlag(args, "description");
|
|
24
66
|
const content = args.join(" ").trim();
|
|
25
|
-
if (!content) { console.error("Usage: task add \"<content>\""); process.exit(2); }
|
|
67
|
+
if (!content) { console.error("Usage: task add \"<content>\" [--parent <id>] [--description \"<...>\"]"); process.exit(2); }
|
|
26
68
|
try {
|
|
27
|
-
const res = await postJson("task-add", { content });
|
|
69
|
+
const res = await postJson("task-add", { content, parentId, description });
|
|
28
70
|
console.log(`Added task ${res.task.id}.`);
|
|
29
71
|
process.exit(0);
|
|
30
72
|
} catch (e) { console.error(e.message); process.exit(1); }
|
|
31
73
|
}
|
|
32
74
|
|
|
75
|
+
async function runPlan(args) {
|
|
76
|
+
const description = takeFlag(args, "description");
|
|
77
|
+
if (args.length < 2) {
|
|
78
|
+
console.error("Usage: task plan \"<title>\" \"<sub1>\" \"<sub2>\" ... [--description \"<...>\"]");
|
|
79
|
+
process.exit(2);
|
|
80
|
+
}
|
|
81
|
+
const [title, ...subtasks] = args;
|
|
82
|
+
try {
|
|
83
|
+
const res = await postJson("task-plan", { title, subtasks, description });
|
|
84
|
+
const parent = res.plan.parent;
|
|
85
|
+
const kids = res.plan.children || [];
|
|
86
|
+
console.log(`Plan ${parent.id}: ${parent.content}`);
|
|
87
|
+
kids.forEach((c) => console.log(` - ${c.content} (${c.id})`));
|
|
88
|
+
process.exit(0);
|
|
89
|
+
} catch (e) { console.error(e.message); process.exit(1); }
|
|
90
|
+
}
|
|
91
|
+
|
|
33
92
|
async function runList(args) {
|
|
34
|
-
const status = args
|
|
93
|
+
const status = args.includes("--status") ? args[args.indexOf("--status") + 1] : null;
|
|
35
94
|
try {
|
|
36
95
|
const res = await postJson("task-list", status ? { status } : {});
|
|
37
96
|
const list = res.tasks || [];
|
|
38
97
|
if (list.length === 0) { console.log("No tasks."); process.exit(0); }
|
|
39
|
-
|
|
98
|
+
console.log(renderTree(list));
|
|
40
99
|
process.exit(0);
|
|
41
100
|
} catch (e) { console.error(e.message); process.exit(1); }
|
|
42
101
|
}
|
|
@@ -58,7 +117,8 @@ async function runRemove(args) {
|
|
|
58
117
|
try {
|
|
59
118
|
const res = await postJson("task-remove", { id });
|
|
60
119
|
if (!res.ok) { console.error(`Not found: ${id}`); process.exit(1); }
|
|
61
|
-
|
|
120
|
+
const extra = (res.removed && res.removed.alsoRemoved) ? ` (and ${res.removed.alsoRemoved} subtask${res.removed.alsoRemoved === 1 ? "" : "s"})` : "";
|
|
121
|
+
console.log(`Removed ${id}${extra}.`);
|
|
62
122
|
process.exit(0);
|
|
63
123
|
} catch (e) { console.error(e.message); process.exit(1); }
|
|
64
124
|
}
|
|
@@ -77,6 +137,7 @@ async function run(args) {
|
|
|
77
137
|
if (!sub || sub === "--help" || sub === "help") { console.log(HELP); process.exit(sub ? 0 : 2); }
|
|
78
138
|
switch (sub) {
|
|
79
139
|
case "add": return runAdd(rest);
|
|
140
|
+
case "plan": return runPlan(rest);
|
|
80
141
|
case "list": case "ls": return runList(rest);
|
|
81
142
|
case "start": return runUpdate(rest, "in_progress");
|
|
82
143
|
case "done": case "complete": return runUpdate(rest, "completed");
|
package/core/access.js
CHANGED
|
@@ -48,7 +48,16 @@ function isChatAuthorized(chatId) {
|
|
|
48
48
|
if (CHAT_IDS.includes(id)) return true;
|
|
49
49
|
try {
|
|
50
50
|
const auth = JSON.parse(fs.readFileSync(AUTH_FILE, "utf-8"));
|
|
51
|
-
|
|
51
|
+
if (Array.isArray(auth.authorized) && auth.authorized.some((a) => String(a.chatId) === id)) return true;
|
|
52
|
+
} catch (e) {}
|
|
53
|
+
// Also accept any chat that has been linked to a person via an intro.
|
|
54
|
+
try {
|
|
55
|
+
const { currentTransport } = require("./context");
|
|
56
|
+
const transport = currentTransport();
|
|
57
|
+
if (transport) {
|
|
58
|
+
const people = require("./people");
|
|
59
|
+
if (people.findByHandle(transport, id)) return true;
|
|
60
|
+
}
|
|
52
61
|
} catch (e) {}
|
|
53
62
|
return false;
|
|
54
63
|
}
|
package/core/actions.js
CHANGED
|
@@ -10,6 +10,8 @@ const { currentState, resetSettings, resetSessionUsage, saveState } = require(".
|
|
|
10
10
|
const { runClaude, getActiveSessionId } = require("./runner");
|
|
11
11
|
const { listProjects, projectKeyboard, workspacePath } = require("./projects");
|
|
12
12
|
const { isChatOwner, approveAuthRequest, denyAuthRequest, authRequestLabel } = require("./access");
|
|
13
|
+
const intros = require("./intros");
|
|
14
|
+
const introFlow = require("./intro-flow");
|
|
13
15
|
const { finishOnboarding } = require("./onboarding");
|
|
14
16
|
const { startSession } = require("./handlers");
|
|
15
17
|
const jobs = require("./jobs");
|
|
@@ -52,6 +54,38 @@ async function handleAction(envelope) {
|
|
|
52
54
|
return;
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
if (d.startsWith("intro:")) {
|
|
58
|
+
if (!isChatOwner(envelope.channelId)) return send("Owner only — intro approvals are restricted.");
|
|
59
|
+
const [, action, introId] = d.split(":");
|
|
60
|
+
if (!introId || !["approve", "reject"].includes(action)) return;
|
|
61
|
+
const intro = intros.byId(introId);
|
|
62
|
+
if (!intro) return send(`No intro found for ${introId}.`);
|
|
63
|
+
if (intro.step === "approved" || intro.step === "rejected") {
|
|
64
|
+
return send(`Intro ${introId} was already ${intro.step}.`);
|
|
65
|
+
}
|
|
66
|
+
if (action === "approve") {
|
|
67
|
+
try {
|
|
68
|
+
const ownerName = envelope.from?.name || "owner";
|
|
69
|
+
const result = await introFlow.applyApproval(intro, ownerName);
|
|
70
|
+
await introFlow.notifyIntroUser(intro, `You're in. Welcome${result.person ? ", " + result.person.name : ""}. Send /start to begin.`);
|
|
71
|
+
if (envelope.action?.sourceMessageId) {
|
|
72
|
+
await adapter.edit(envelope.channelId, envelope.action.sourceMessageId, `Approved intro ${introId}.`, {});
|
|
73
|
+
} else {
|
|
74
|
+
await send(`Approved intro ${introId}.`);
|
|
75
|
+
}
|
|
76
|
+
} catch (e) { await send(`Approval failed: ${e.message}`); }
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
intros.reject(introId, envelope.from?.name || "owner");
|
|
80
|
+
await introFlow.notifyIntroUser(intro, "Your access request was denied.");
|
|
81
|
+
if (envelope.action?.sourceMessageId) {
|
|
82
|
+
await adapter.edit(envelope.channelId, envelope.action.sourceMessageId, `Rejected intro ${introId}.`, {});
|
|
83
|
+
} else {
|
|
84
|
+
await send(`Rejected intro ${introId}.`);
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
55
89
|
if (d.startsWith("ob:")) { finishOnboarding(d.slice(3)); return; }
|
|
56
90
|
|
|
57
91
|
if (d === "show:projects") { await send("Pick:", { keyboard: projectKeyboard() }); return; }
|
package/core/audit.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Append-only audit log for security-sensitive events: people CRUD,
|
|
2
|
+
// intro decisions, cross-channel sends. One JSON record per line.
|
|
3
|
+
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const { AUDIT_FILE } = require("./config");
|
|
6
|
+
|
|
7
|
+
function log(kind, payload) {
|
|
8
|
+
const entry = { at: new Date().toISOString(), kind, ...payload };
|
|
9
|
+
const line = JSON.stringify(entry) + "\n";
|
|
10
|
+
try { fs.appendFileSync(AUDIT_FILE, line); } catch (e) {}
|
|
11
|
+
return entry;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function tail(n = 50) {
|
|
15
|
+
try {
|
|
16
|
+
const raw = fs.readFileSync(AUDIT_FILE, "utf-8").trim().split("\n");
|
|
17
|
+
return raw.slice(-Math.max(1, n)).map((line) => {
|
|
18
|
+
try { return JSON.parse(line); } catch (e) { return { at: null, kind: "parse-error", raw: line }; }
|
|
19
|
+
});
|
|
20
|
+
} catch (e) { return []; }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = { log, tail };
|
package/core/config.js
CHANGED
|
@@ -62,6 +62,9 @@ const TASKS_DIR = config.TASKS_DIR || path.join(CONFIG_DIR, "tasks");
|
|
|
62
62
|
const VAULT_FILE = config.VAULT_FILE || path.join(CONFIG_DIR, "vault.enc");
|
|
63
63
|
const AUTH_FILE = config.AUTH_FILE || path.join(CONFIG_DIR, "auth.json");
|
|
64
64
|
const IDENTITIES_FILE = config.IDENTITIES_FILE || path.join(CONFIG_DIR, "identities.json");
|
|
65
|
+
const PEOPLE_FILE = config.PEOPLE_FILE || path.join(CONFIG_DIR, "people.json");
|
|
66
|
+
const INTROS_FILE = config.INTROS_FILE || path.join(CONFIG_DIR, "intros.json");
|
|
67
|
+
const AUDIT_FILE = config.AUDIT_FILE || path.join(CONFIG_DIR, "audit.log");
|
|
65
68
|
const STATE_FILE = path.join(CONFIG_DIR, "state.json");
|
|
66
69
|
const SESSIONS_FILE = path.join(CONFIG_DIR, "sessions.json");
|
|
67
70
|
|
|
@@ -78,7 +81,7 @@ if (!CLAUDE_PATH) { console.error("CLAUDE_PATH not set"); process.exit(1); }
|
|
|
78
81
|
if (!fs.existsSync(WORKSPACE)) {
|
|
79
82
|
try {
|
|
80
83
|
fs.mkdirSync(WORKSPACE, { recursive: true });
|
|
81
|
-
console.
|
|
84
|
+
console.error(`Created workspace: ${WORKSPACE}`);
|
|
82
85
|
} catch (e) {
|
|
83
86
|
console.error(`Failed to create workspace: ${e.message}`);
|
|
84
87
|
process.exit(1);
|
|
@@ -99,14 +102,14 @@ if (!resolvedCursorPath) {
|
|
|
99
102
|
try { resolvedCursorPath = execSync("which agent 2>/dev/null", { encoding: "utf-8" }).trim() || null; }
|
|
100
103
|
catch (e) { resolvedCursorPath = null; }
|
|
101
104
|
}
|
|
102
|
-
if (resolvedCursorPath) console.
|
|
105
|
+
if (resolvedCursorPath) console.error(`Cursor Agent CLI: ${resolvedCursorPath}`);
|
|
103
106
|
|
|
104
107
|
let resolvedCodexPath = CODEX_PATH;
|
|
105
108
|
if (!resolvedCodexPath) {
|
|
106
109
|
try { resolvedCodexPath = execSync("which codex 2>/dev/null", { encoding: "utf-8" }).trim() || null; }
|
|
107
110
|
catch (e) { resolvedCodexPath = null; }
|
|
108
111
|
}
|
|
109
|
-
if (resolvedCodexPath) console.
|
|
112
|
+
if (resolvedCodexPath) console.error(`Codex CLI: ${resolvedCodexPath}`);
|
|
110
113
|
|
|
111
114
|
if (!fs.existsSync(TEMP_DIR)) fs.mkdirSync(TEMP_DIR, { recursive: true });
|
|
112
115
|
if (!fs.existsSync(FILES_DIR)) fs.mkdirSync(FILES_DIR, { recursive: true });
|
|
@@ -194,6 +197,7 @@ module.exports = {
|
|
|
194
197
|
TRANSCRIPTS_DIR,
|
|
195
198
|
WHISPER_CLI, WHISPER_MODEL, FFMPEG,
|
|
196
199
|
SOUL_FILE, CRONS_FILE, JOBS_FILE, TASKS_DIR, VAULT_FILE, AUTH_FILE, IDENTITIES_FILE,
|
|
200
|
+
PEOPLE_FILE, INTROS_FILE, AUDIT_FILE,
|
|
197
201
|
STATE_FILE, SESSIONS_FILE,
|
|
198
202
|
TEMP_DIR, FILES_DIR,
|
|
199
203
|
MAX_FILE_SIZE, MAX_VOICE_SIZE, MAX_PROCESS_TIMEOUT,
|