@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 +62 -0
- package/bin/cli.js +32 -2
- 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/core/access.js +42 -1
- package/core/actions.js +34 -0
- package/core/audit.js +40 -0
- package/core/config.js +4 -0
- package/core/handlers.js +181 -3
- package/core/intro-flow.js +241 -0
- package/core/intros.js +158 -0
- package/core/loopback.js +259 -0
- package/core/people.js +247 -0
- package/core/relay.js +61 -0
- package/core/router.js +8 -1
- package/core/system-prompt.js +57 -2
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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,
|