@inetafrica/open-claudia 2.0.5 → 2.1.1

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/auth.js ADDED
@@ -0,0 +1,62 @@
1
+ // open-claudia auth <subcommand>
2
+ // Owner-only view of the raw auth.json store and direct deauth. Use
3
+ // `open-claudia people` for richer roster operations. This is the
4
+ // low-level escape hatch for legacy entries that don't have a person
5
+ // record yet.
6
+
7
+ const { postJson } = require("./loopback-client");
8
+
9
+ const HELP = `
10
+ Raw authorization store.
11
+
12
+ open-claudia auth list
13
+ open-claudia auth revoke <chatId>
14
+
15
+ Owner-only. Revoking also unlinks the chat from any matching person.
16
+ Owner entries are protected — refuse to self-deauth.
17
+ `;
18
+
19
+ async function runList() {
20
+ try {
21
+ const res = await postJson("auth-list", {});
22
+ const authorized = res.authorized || [];
23
+ const pending = res.pending || [];
24
+ if (authorized.length === 0 && pending.length === 0) { console.log("No authorized or pending chats."); process.exit(0); }
25
+ if (authorized.length) {
26
+ console.log("Authorized:");
27
+ for (const a of authorized) {
28
+ const owner = a.isOwner ? " (owner)" : "";
29
+ const person = a.personName ? ` → ${a.personName}` : " → (no person record)";
30
+ console.log(` ${a.chatId}${owner} ${a.name || a.username || ""}${person}`);
31
+ }
32
+ }
33
+ if (pending.length) {
34
+ console.log("\nPending:");
35
+ for (const p of pending) console.log(` ${p.chatId} ${p.name || p.username || ""} (requested ${p.requestedAt})`);
36
+ }
37
+ process.exit(0);
38
+ } catch (e) { console.error(e.message); process.exit(1); }
39
+ }
40
+
41
+ async function runRevoke(args) {
42
+ const chatId = args[0];
43
+ if (!chatId) { console.error("Usage: auth revoke <chatId>"); process.exit(2); }
44
+ try {
45
+ const res = await postJson("auth-revoke", { chatId });
46
+ console.log(`Revoked ${chatId}${res.personId ? ` (was linked to person ${res.personId})` : ""}.`);
47
+ process.exit(0);
48
+ } catch (e) { console.error(e.message); process.exit(1); }
49
+ }
50
+
51
+ async function run(args) {
52
+ const sub = args[0];
53
+ const rest = args.slice(1);
54
+ if (!sub || sub === "--help" || sub === "help") { console.log(HELP); process.exit(sub ? 0 : 2); }
55
+ switch (sub) {
56
+ case "list": case "ls": return runList();
57
+ case "revoke": case "rm": return runRevoke(rest);
58
+ default: console.error(`Unknown auth subcommand: ${sub}`); console.log(HELP); process.exit(2);
59
+ }
60
+ }
61
+
62
+ module.exports = { run, HELP };
package/bin/cli.js CHANGED
@@ -188,11 +188,17 @@ switch (command) {
188
188
  break;
189
189
  }
190
190
 
191
- case "auth":
192
- // Pass through to setup.js auth mode
191
+ case "auth": {
192
+ const sub = args[1];
193
+ if (["list", "ls", "revoke", "rm"].includes(sub)) {
194
+ require("./auth").run(args.slice(1));
195
+ break;
196
+ }
197
+ // Legacy: open-claudia auth (no subcommand) → setup.js auth mode
193
198
  process.argv = [process.argv[0], process.argv[1], "--auth"];
194
199
  require(path.join(botDir, "setup.js"));
195
200
  break;
201
+ }
196
202
 
197
203
  case "stop": {
198
204
  const pids = findBotProcesses();
@@ -267,6 +273,23 @@ switch (command) {
267
273
  break;
268
274
  }
269
275
 
276
+ case "people": {
277
+ require("./people").run(args.slice(1));
278
+ break;
279
+ }
280
+ case "intros": {
281
+ require("./intros").run(args.slice(1));
282
+ break;
283
+ }
284
+ case "send-to": {
285
+ require("./send-to").run(args.slice(1));
286
+ break;
287
+ }
288
+ case "recent": {
289
+ require("./recent").run(args.slice(1));
290
+ break;
291
+ }
292
+
270
293
  default:
271
294
  console.log(`
272
295
  Open Claudia — AI Coding Assistant via Telegram
@@ -298,6 +321,13 @@ Background work (only inside an active bot-spawned task):
298
321
  open-claudia task add|list|start|done|remove Per-channel todo (survives restarts)
299
322
  open-claudia agent "<prompt>" [--role X] Spawn a throwaway sub-agent for research
300
323
 
324
+ Multi-user / cross-channel:
325
+ open-claudia people list|show|add|note|link Roster of known team members
326
+ open-claudia intros list|approve|reject Pending introductions from unknown chats
327
+ open-claudia auth list|revoke <chatId> Raw authorization view + direct deauth
328
+ open-claudia send-to --person "<name>" "<msg>" Relay a message to another team member
329
+ open-claudia recent --person "<name>" [--limit] Read recent activity from another chat
330
+
301
331
  Start options:
302
332
  --web Also start the web UI
303
333
  --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/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
  }
@@ -125,6 +134,36 @@ function recordPendingAuthRequest({ chatId, name, username }) {
125
134
  return { status: "queued" };
126
135
  }
127
136
 
137
+ // Remove a chatId from the legacy auth.json. Owners are never removed
138
+ // this way to avoid self-locking; the caller has to handle owner
139
+ // demotion explicitly. Returns { ok, reason }.
140
+ function revokeChat(chatId) {
141
+ const id = String(chatId || "");
142
+ if (!id) return { ok: false, reason: "missing_id" };
143
+ const auth = loadAuth();
144
+ const before = auth.authorized.length;
145
+ auth.authorized = auth.authorized.filter((a) => {
146
+ if (String(a.chatId) !== id) return true;
147
+ if (a.isOwner === true) return true;
148
+ return false;
149
+ });
150
+ auth.pending = auth.pending.filter((p) => String(p.chatId) !== id);
151
+ if (auth.authorized.length === before) {
152
+ if (auth.pending.length === 0) return { ok: false, reason: "not_found" };
153
+ }
154
+ saveAuth(auth);
155
+ updateAuthorizedChatEnv(auth);
156
+ return { ok: true };
157
+ }
158
+
159
+ function listAuthorizedRaw() {
160
+ const auth = loadAuth();
161
+ return {
162
+ authorized: auth.authorized.slice(),
163
+ pending: auth.pending.slice(),
164
+ };
165
+ }
166
+
128
167
  function hasOwner() {
129
168
  if (CHAT_ID) return true;
130
169
  try {
@@ -164,4 +203,6 @@ module.exports = {
164
203
  updateAuthorizedChatEnv,
165
204
  hasOwner,
166
205
  bootstrapOwner,
206
+ revokeChat,
207
+ listAuthorizedRaw,
167
208
  };
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,40 @@
1
+ // Append-only audit log for security-sensitive events: people CRUD,
2
+ // intro decisions, cross-channel sends. One JSON record per line.
3
+ // Auto-rotates when the file exceeds MAX_BYTES — the current file is
4
+ // renamed to <name>.1 (overwriting any previous archive) and a fresh
5
+ // file starts.
6
+
7
+ const fs = require("fs");
8
+ const { AUDIT_FILE } = require("./config");
9
+
10
+ const MAX_BYTES = 2 * 1024 * 1024; // 2 MB; rotate at this size
11
+
12
+ function rotateIfNeeded() {
13
+ try {
14
+ const stat = fs.statSync(AUDIT_FILE);
15
+ if (stat.size > MAX_BYTES) {
16
+ try { fs.renameSync(AUDIT_FILE, `${AUDIT_FILE}.1`); } catch (e) {}
17
+ }
18
+ } catch (e) {} // file may not exist yet
19
+ }
20
+
21
+ function log(kind, payload) {
22
+ const entry = { at: new Date().toISOString(), kind, ...payload };
23
+ const line = JSON.stringify(entry) + "\n";
24
+ try {
25
+ rotateIfNeeded();
26
+ fs.appendFileSync(AUDIT_FILE, line);
27
+ } catch (e) {}
28
+ return entry;
29
+ }
30
+
31
+ function tail(n = 50) {
32
+ try {
33
+ const raw = fs.readFileSync(AUDIT_FILE, "utf-8").trim().split("\n");
34
+ return raw.slice(-Math.max(1, n)).map((line) => {
35
+ try { return JSON.parse(line); } catch (e) { return { at: null, kind: "parse-error", raw: line }; }
36
+ });
37
+ } catch (e) { return []; }
38
+ }
39
+
40
+ module.exports = { log, tail, MAX_BYTES };
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
 
@@ -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,