@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 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 fmt(t, idx) {
19
- const mark = t.status === "completed" ? "[x]" : t.status === "in_progress" ? "[~]" : "[ ]";
20
- return `${idx + 1}. ${mark} ${t.content} (${t.id})`;
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[args.indexOf("--status") + 1] && args.includes("--status") ? args[args.indexOf("--status") + 1] : null;
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
- list.forEach((t, i) => console.log(fmt(t, i)));
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
- console.log(`Removed ${id}.`);
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
- return Array.isArray(auth.authorized) && auth.authorized.some((a) => String(a.chatId) === id);
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.log(`Created workspace: ${WORKSPACE}`);
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.log(`Cursor Agent CLI: ${resolvedCursorPath}`);
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.log(`Codex CLI: ${resolvedCodexPath}`);
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,