@inetafrica/open-claudia 2.1.1 → 2.2.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/core/actions.js CHANGED
@@ -10,6 +10,9 @@ 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 accessStore = require("./access");
14
+ const peopleStore = require("./people");
15
+ const { vault } = require("./vault-store");
13
16
  const intros = require("./intros");
14
17
  const introFlow = require("./intro-flow");
15
18
  const { finishOnboarding } = require("./onboarding");
@@ -27,8 +30,53 @@ async function handleAction(envelope) {
27
30
  const state = currentState();
28
31
 
29
32
  if (d.startsWith("auth:")) {
30
- if (!isChatOwner(envelope.channelId)) return send("Owner only — auth approvals are restricted.");
31
- const [, action, chatId] = d.split(":");
33
+ if (!isChatOwner(envelope.channelId)) return send("Owner only — auth controls are restricted.");
34
+ const parts = d.split(":");
35
+ const action = parts[1];
36
+
37
+ if (action === "list") {
38
+ const raw = accessStore.listAuthorizedRaw();
39
+ const lines = ["Authorized chats:"];
40
+ for (const a of raw.authorized) {
41
+ const person = peopleStore.findByHandle("telegram", a.chatId) || peopleStore.findByHandle("kazee", a.chatId);
42
+ lines.push(` ${a.chatId}${a.isOwner ? " (owner)" : ""} ${a.name || a.username || ""}${person ? " → " + person.name : " → (no person)"}`);
43
+ }
44
+ if (raw.authorized.length === 0) lines.push(" (none)");
45
+ if (raw.pending.length > 0) {
46
+ lines.push("", "Pending:");
47
+ for (const p of raw.pending) lines.push(` ${p.chatId} ${p.name || p.username || ""}`);
48
+ }
49
+ return send(lines.join("\n"));
50
+ }
51
+
52
+ if (action === "revoke-pick") {
53
+ const raw = accessStore.listAuthorizedRaw();
54
+ const rows = [];
55
+ for (const a of raw.authorized) {
56
+ if (a.isOwner) continue;
57
+ const person = peopleStore.findByHandle("telegram", a.chatId) || peopleStore.findByHandle("kazee", a.chatId);
58
+ const label = `${a.chatId} ${person ? person.name : (a.name || a.username || "")}`.trim();
59
+ rows.push([{ text: `Revoke ${label}`.slice(0, 60), callback_data: `auth:revoke:${a.chatId}` }]);
60
+ }
61
+ if (rows.length === 0) return send("No revocable chats (only owner is authorized).");
62
+ return send("Pick a chat to revoke:", { keyboard: { inline_keyboard: rows } });
63
+ }
64
+
65
+ if (action === "revoke") {
66
+ const chatId = parts.slice(2).join(":");
67
+ if (!chatId) return;
68
+ const person = peopleStore.findByHandle("telegram", chatId) || peopleStore.findByHandle("kazee", chatId);
69
+ if (person && person.isOwner) return send("Refusing to revoke owner chat.");
70
+ if (person) {
71
+ const detected = (person.handles || []).find((h) => String(h.channelId) === String(chatId));
72
+ if (detected) peopleStore.unlinkHandle(person.id, { adapter: detected.adapter, channelId: detected.channelId });
73
+ }
74
+ const r = accessStore.revokeChat(chatId);
75
+ if (!r.ok) return send(`Revoke failed: ${r.reason}`);
76
+ return send(`Revoked ${chatId}${person ? ` (was ${person.name})` : ""}.`);
77
+ }
78
+
79
+ const chatId = parts[2];
32
80
  if (!chatId || !["approve", "deny"].includes(action)) return;
33
81
  const result = action === "approve" ? approveAuthRequest(chatId) : denyAuthRequest(chatId);
34
82
 
@@ -54,6 +102,76 @@ async function handleAction(envelope) {
54
102
  return;
55
103
  }
56
104
 
105
+ if (d.startsWith("vault:")) {
106
+ const action = d.slice("vault:".length);
107
+ if (action === "lock") {
108
+ vault.lock();
109
+ return send("Vault locked.");
110
+ }
111
+ if (action === "cancel") {
112
+ if (!state.pendingVaultUnlock) return send("Nothing to cancel.");
113
+ state.pendingVaultUnlock = false;
114
+ state.pendingVaultAction = null;
115
+ state.pendingVaultUnlockAt = 0;
116
+ return send("Cancelled.");
117
+ }
118
+ if (action === "create") {
119
+ if (!isChatOwner(envelope.channelId)) return send("Owner only.");
120
+ if (vault.exists()) return send("Vault already exists.");
121
+ state.pendingVaultUnlock = true;
122
+ state.pendingVaultUnlockAt = Date.now();
123
+ state.pendingVaultAction = { type: "create" };
124
+ return send("Pick a vault password and send it next.\n(Message will be deleted. Send /vault cancel to abort.)", {
125
+ keyboard: { inline_keyboard: [[{ text: "Cancel", callback_data: "vault:cancel" }]] },
126
+ });
127
+ }
128
+ if (action === "hint") {
129
+ return send([
130
+ "Vault usage:",
131
+ " /vault create — set up the vault (owner, first time)",
132
+ " /vault set <name> <value> — save a credential (your message is deleted)",
133
+ " /vault get <name> — reveal a credential",
134
+ " /vault remove <name> — delete a credential",
135
+ " /vault — list (unlocks if needed)",
136
+ " /vault lock — lock now",
137
+ " /vault cancel — abort a pending password prompt",
138
+ ].join("\n"));
139
+ }
140
+ return;
141
+ }
142
+
143
+ if (d.startsWith("people:")) {
144
+ const rest = d.slice("people:".length);
145
+ if (rest === "hint") {
146
+ return send([
147
+ "People usage:",
148
+ " /people show <name> — details",
149
+ " /people note <name> <text> — add a note",
150
+ " /people add <name> — owner: add person",
151
+ " /people link <name> <adapter> <channel> — owner: link a chat",
152
+ " /people unlink <name> <adapter> <channel> — owner: unlink",
153
+ " /people set-primary <name> <adapter> <channel>",
154
+ " /people remove <name> — owner",
155
+ ].join("\n"));
156
+ }
157
+ if (rest.startsWith("show:")) {
158
+ const id = rest.slice("show:".length);
159
+ const person = peopleStore.findById(id);
160
+ if (!person) return send(`Not found: ${id}`);
161
+ const handles = (person.handles || []).map((h) => ` ${h.adapter}:${h.channelId}${person.primaryChannel && person.primaryChannel.adapter === h.adapter && String(person.primaryChannel.channelId) === String(h.channelId) ? " (primary)" : ""}`).join("\n");
162
+ const notes = (person.notes || []).slice(-5).map((n) => ` - [${(n.at || "").slice(0, 10)}] ${n.text}`).join("\n");
163
+ return send([
164
+ `${person.name}${person.isOwner ? " (owner)" : ""} id=${person.id}`,
165
+ person.bio ? `Bio: ${person.bio}` : null,
166
+ "Handles:",
167
+ handles || " (none)",
168
+ notes ? "Notes:" : null,
169
+ notes || null,
170
+ ].filter((x) => x !== null && x !== "").join("\n"));
171
+ }
172
+ return;
173
+ }
174
+
57
175
  if (d.startsWith("intro:")) {
58
176
  if (!isChatOwner(envelope.channelId)) return send("Owner only — intro approvals are restricted.");
59
177
  const [, action, introId] = d.split(":");
package/core/handlers.js CHANGED
@@ -170,8 +170,15 @@ register({
170
170
  return send("Usage: /auth | /auth list | /auth revoke <chatId>");
171
171
  }
172
172
  if (authorized(env)) {
173
- send("You're already authorized.");
174
- return;
173
+ if (ownerEnv(env)) {
174
+ return send("You're already authorized.", {
175
+ keyboard: { inline_keyboard: [[
176
+ { text: "List authorized", callback_data: "auth:list" },
177
+ { text: "Revoke…", callback_data: "auth:revoke-pick" },
178
+ ]] },
179
+ });
180
+ }
181
+ return send("You're already authorized.");
175
182
  }
176
183
  if (!hasOwner()) {
177
184
  bootstrapOwner({
@@ -870,23 +877,40 @@ register({
870
877
 
871
878
  // ── Vault ──────────────────────────────────────────────────────────
872
879
 
880
+ const PENDING_VAULT_TTL_MS = 90 * 1000;
881
+ const PASSWORD_PROMPT_KB = { inline_keyboard: [[{ text: "Cancel", callback_data: "vault:cancel" }]] };
882
+
883
+ function armVaultPrompt(state, action) {
884
+ state.pendingVaultUnlock = true;
885
+ state.pendingVaultUnlockAt = Date.now();
886
+ state.pendingVaultAction = action;
887
+ }
888
+
873
889
  register({
874
- name: "vault", description: "Manage credentials (password required)", args: "[set|remove|lock] ...",
890
+ name: "vault", description: "Manage credentials (password required)", args: "[create|get|set|remove|lock|cancel] ...",
875
891
  handler: async (env, { tail }) => {
876
892
  if (!authorized(env)) return;
877
893
  const state = currentState();
878
894
 
879
895
  if (!tail) {
880
- if (!vault.exists()) return send("No vault found. Run setup first: node setup.js");
896
+ if (!vault.exists()) {
897
+ if (!ownerEnv(env)) return send("No vault. Ask the owner to run /vault create.");
898
+ return send("No vault yet. Use /vault create to set one up.", {
899
+ keyboard: { inline_keyboard: [[{ text: "Create vault", callback_data: "vault:create" }]] },
900
+ });
901
+ }
881
902
  if (vault.isUnlocked()) {
882
903
  const entries = vault.list();
883
904
  const keys = Object.keys(entries);
884
- if (keys.length === 0) await send("Vault is unlocked but empty.\n\nUse /vault set <name> <value>");
885
- else await send("Vault (unlocked):\n\n" + keys.map((k) => `${k}: ${entries[k]}`).join("\n") + "\n\nLocks automatically in 5 min.");
905
+ const kb = { inline_keyboard: [[
906
+ { text: "Lock now", callback_data: "vault:lock" },
907
+ { text: "How to set/get/remove", callback_data: "vault:hint" },
908
+ ]] };
909
+ if (keys.length === 0) await send("Vault is unlocked but empty.\n\nUse /vault set <name> <value>", { keyboard: kb });
910
+ else await send("Vault (unlocked):\n\n" + keys.map((k) => `${k}: ${entries[k]}`).join("\n") + "\n\nLocks automatically in 5 min.", { keyboard: kb });
886
911
  } else {
887
- state.pendingVaultUnlock = true;
888
- state.pendingVaultAction = { type: "list" };
889
- await send("Vault is locked. Send your vault password.\n(Message will be deleted after reading)");
912
+ armVaultPrompt(state, { type: "list" });
913
+ await send("Vault is locked. Send your vault password.\n(Message will be deleted after reading. Times out in 90s.)", { keyboard: PASSWORD_PROMPT_KB });
890
914
  }
891
915
  return;
892
916
  }
@@ -896,6 +920,21 @@ register({
896
920
  return send("Vault locked.");
897
921
  }
898
922
 
923
+ if (tail === "cancel") {
924
+ if (!state.pendingVaultUnlock) return send("Nothing to cancel.");
925
+ state.pendingVaultUnlock = false;
926
+ state.pendingVaultAction = null;
927
+ state.pendingVaultUnlockAt = 0;
928
+ return send("Cancelled.");
929
+ }
930
+
931
+ if (tail === "create") {
932
+ if (!ownerEnv(env)) return send("Owner only.");
933
+ if (vault.exists()) return send("Vault already exists. Use /vault to unlock, or delete the file first.");
934
+ armVaultPrompt(state, { type: "create" });
935
+ return send("Pick a vault password and send it next.\n(Message will be deleted. Use the button or send /vault cancel to abort.)", { keyboard: PASSWORD_PROMPT_KB });
936
+ }
937
+
899
938
  const setMatch = tail.match(/^set\s+(\S+)\s+(.+)$/);
900
939
  if (setMatch) {
901
940
  await deleteMessage(env.messageId);
@@ -903,9 +942,19 @@ register({
903
942
  vault.set(setMatch[1], setMatch[2].trim());
904
943
  return send(`Saved: ${setMatch[1]}`);
905
944
  }
906
- state.pendingVaultUnlock = true;
907
- state.pendingVaultAction = { type: "set", key: setMatch[1], value: setMatch[2].trim() };
908
- return send("Vault locked. Send password to unlock.");
945
+ armVaultPrompt(state, { type: "set", key: setMatch[1], value: setMatch[2].trim() });
946
+ return send("Vault locked. Send password to unlock.", { keyboard: PASSWORD_PROMPT_KB });
947
+ }
948
+
949
+ const getMatch = tail.match(/^get\s+(\S+)$/);
950
+ if (getMatch) {
951
+ if (vault.isUnlocked()) {
952
+ const value = vault.get(getMatch[1]);
953
+ if (value === null) return send(`No such key: ${getMatch[1]}`);
954
+ return send(`${getMatch[1]}: ${value}\n\n(Visible to anyone who reads this chat — delete the message when done.)`);
955
+ }
956
+ armVaultPrompt(state, { type: "get", key: getMatch[1] });
957
+ return send("Vault locked. Send password to unlock.", { keyboard: PASSWORD_PROMPT_KB });
909
958
  }
910
959
 
911
960
  const removeMatch = tail.match(/^remove\s+(\S+)$/);
@@ -914,15 +963,16 @@ register({
914
963
  vault.remove(removeMatch[1]);
915
964
  return send(`Removed: ${removeMatch[1]}`);
916
965
  }
917
- state.pendingVaultUnlock = true;
918
- state.pendingVaultAction = { type: "remove", key: removeMatch[1] };
919
- return send("Vault locked. Send password to unlock.");
966
+ armVaultPrompt(state, { type: "remove", key: removeMatch[1] });
967
+ return send("Vault locked. Send password to unlock.", { keyboard: PASSWORD_PROMPT_KB });
920
968
  }
921
969
 
922
- send("Usage: /vault | /vault set <name> <value> | /vault remove <name> | /vault lock");
970
+ send("Usage: /vault | /vault create | /vault set <name> <value> | /vault get <name> | /vault remove <name> | /vault lock | /vault cancel");
923
971
  },
924
972
  });
925
973
 
974
+ module.exports.PENDING_VAULT_TTL_MS = PENDING_VAULT_TTL_MS;
975
+
926
976
  // ── Cron ───────────────────────────────────────────────────────────
927
977
 
928
978
  register({
@@ -1052,11 +1102,17 @@ register({
1052
1102
  if (!authorized(env)) return;
1053
1103
  if (!tail || tail === "list") {
1054
1104
  const all = peopleStore.list();
1055
- if (all.length === 0) return send("No people yet.");
1105
+ if (all.length === 0) {
1106
+ return send("No people yet.", ownerEnv(env) ? {
1107
+ keyboard: { inline_keyboard: [[{ text: "How to add/link", callback_data: "people:hint" }]] },
1108
+ } : undefined);
1109
+ }
1056
1110
  const lines = ["Team:"];
1057
1111
  for (const p of all) lines.push(` ${formatPersonShort(p)}`);
1058
- lines.push("", "More: /people show <name>, /people note <name> <text>");
1059
- return send(lines.join("\n"));
1112
+ const rows = [];
1113
+ for (const p of all) rows.push([{ text: `Show ${p.name}`, callback_data: `people:show:${p.id}` }]);
1114
+ if (ownerEnv(env)) rows.push([{ text: "How to note/link/add", callback_data: "people:hint" }]);
1115
+ return send(lines.join("\n"), { keyboard: { inline_keyboard: rows } });
1060
1116
  }
1061
1117
  const showMatch = tail.match(/^show\s+(.+)$/i);
1062
1118
  if (showMatch) {
package/core/router.js CHANGED
@@ -240,18 +240,34 @@ async function handleText(envelope) {
240
240
  }
241
241
 
242
242
  if (state.pendingVaultUnlock) {
243
- const password = envelope.text;
244
- await deleteMessage(envelope.messageId);
245
- const ok = vault.unlock(password);
246
- if (!ok) {
243
+ const { PENDING_VAULT_TTL_MS } = require("./handlers");
244
+ const armedAt = state.pendingVaultUnlockAt || 0;
245
+ if (PENDING_VAULT_TTL_MS && armedAt && Date.now() - armedAt > PENDING_VAULT_TTL_MS) {
247
246
  state.pendingVaultUnlock = false;
248
247
  state.pendingVaultAction = null;
249
- await send("Wrong password.");
248
+ state.pendingVaultUnlockAt = 0;
249
+ await send("Vault prompt timed out. Re-run the command.");
250
250
  return;
251
251
  }
252
+ const password = envelope.text;
253
+ await deleteMessage(envelope.messageId);
252
254
  const action = state.pendingVaultAction;
253
255
  state.pendingVaultUnlock = false;
254
256
  state.pendingVaultAction = null;
257
+ state.pendingVaultUnlockAt = 0;
258
+
259
+ if (action && action.type === "create") {
260
+ if (vault.exists()) { await send("Vault was created in another flow — re-run /vault."); return; }
261
+ vault.create(password);
262
+ await send("Vault created and unlocked.\n\nUse /vault set <name> <value>");
263
+ return;
264
+ }
265
+
266
+ const ok = vault.unlock(password);
267
+ if (!ok) {
268
+ await send("Wrong password.");
269
+ return;
270
+ }
255
271
  if (action.type === "list") {
256
272
  const entries = vault.list();
257
273
  const keys = Object.keys(entries);
@@ -263,6 +279,10 @@ async function handleText(envelope) {
263
279
  } else if (action.type === "remove") {
264
280
  vault.remove(action.key);
265
281
  await send(`Removed: ${action.key}`);
282
+ } else if (action.type === "get") {
283
+ const value = vault.get(action.key);
284
+ if (value === null) await send(`No such key: ${action.key}`);
285
+ else await send(`${action.key}: ${value}\n\n(Visible to anyone who reads this chat — delete the message when done.)`);
266
286
  }
267
287
  return;
268
288
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "2.1.1",
3
+ "version": "2.2.1",
4
4
  "description": "Your always-on AI coding assistant — Claude Code, Cursor Agent, and OpenAI Codex via Telegram or Kazee Chat",
5
5
  "main": "bot.js",
6
6
  "bin": {
package/vault.js CHANGED
@@ -23,7 +23,9 @@ class Vault {
23
23
  // Create a new vault with a password
24
24
  create(password) {
25
25
  this.data = {};
26
+ this._password = password;
26
27
  this._save(password);
28
+ this._resetLockTimer();
27
29
  }
28
30
 
29
31
  // Derive key from password + salt
@@ -125,7 +127,7 @@ class Vault {
125
127
  const entries = {};
126
128
  for (const [k, v] of Object.entries(this.data)) {
127
129
  const val = String(v);
128
- entries[k] = val.length > 8 ? val.slice(0, 4) + "..." + val.slice(-4) : "****";
130
+ entries[k] = val.length >= 4 ? "****" + val.slice(-4) : "****";
129
131
  }
130
132
  return entries;
131
133
  }