@inetafrica/open-claudia 2.14.4 → 2.14.6

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/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## v2.14.6
4
+ - **`/channel add kazee` started from the inline "Add Kazee" button now completes — owner-capture confirm lands on the operator's channel, not on Kazee.** The wizard's owner-confirm prompt (*"Is this you — the owner?"*) is meant to go back to the channel the operator ran `/channel` from, via `channel-wizard.sayToOrigin`, which needs `session.origin` — captured from the envelope passed to `start()`. The typed `/channel add kazee` path (`handlers.js`) passed the envelope; the button path (`actions.js` `chn:add:kazee`) did **not**, so `origin` was null and the prompt fell back to ambient `io.send` scope — which, while processing the operator's inbound Kazee trigger message, is the Kazee room. The Yes/No/Skip buttons then rendered on Kazee, where a tap can never resolve the session: it's keyed by the operator's canonical id, but a Kazee tap resolves to the owner's *unlinked* Kazee user id (the very thing owner-capture is establishing) → `handleAction` finds no session → *"That setup step expired."* `actions.js` now passes the envelope to `wizard.start`, exactly like the typed path.
5
+ - **`/channel remove` and the inline "Remove <id>" button now share one teardown, so they can't drift.** v2.14.5 made the typed `/channel remove` a complete teardown (adapter + `.env` + `people.unlinkAdapterHandles` + `identity.removeTransportIdentities`), but the button (`actions.js` `chn:rm:`) still ran the pre-v2.14.5 version that cleared only `.env` — so removing via the button left exactly the handle/identity residue v2.14.5 was built to clear. The full teardown is now a single `channel-wizard.removeChannel(id)` called by both entry points; callers only guard `telegram` and format `r.summary` / `r.error`. Net −10 lines.
6
+ - **Agent-space / OpenClaw:** no new deps, no new env, no schema change; Docker image builds identically. Both fixes close divergences between the button and typed entry points of the owner-driven `/channel` flow — inert for deployments that never add/remove a channel. Full `npm test` green.
7
+
8
+ ## v2.14.5
9
+ - **`/channel remove <id>` is now a complete teardown — a re-add starts from a genuinely clean slate.** Removal used to clear only the `.env` channel config (`CHANNELS` + the `KAZEE_*` keys), but the wizard's v2.14.4 owner-capture also links the owner's Kazee handle onto their people record (`people.linkHandle`) and mirrors an identity mapping into `identities.json` (`setIdentityMapping`). Those were left behind, so a subsequent `/channel add kazee` stacked on stale handle + identity state. `/channel remove` now also drops every handle for that transport across all people records (new `people.unlinkAdapterHandles`) and removes its identity mappings — channel→canonical links and preferred-channel pointers (new `identity.removeTransportIdentities`) — and clears the previously-orphaned `KAZEE_BOT_USER_ID`.
10
+ - **The cleanup goes through the in-memory stores, so disk and the running process stay consistent.** `identities.json` is loaded once at boot into a shared in-memory object (`identity.js`), and `saveIdentities()` re-serializes that whole object on every write. Because `router.js` calls `autoLinkOwnerChannel` → `setIdentityMapping` → `saveIdentities` on each owner message, any entry lingering in memory is rewritten to disk continuously — so an out-of-band file edit can't stick, and a partial teardown leaves ghosts that reappear. Removing through the in-memory maps (then persisting) means the teardown holds. The `/channel remove` reply now reports what it cleared (e.g. `Removed channel: kazee — cleared 1 handle + identity links`).
11
+ - **Agent-space / OpenClaw:** no new deps, no new env, no schema change; Docker image builds identically. Scoped entirely to the owner-driven `/channel remove` path — inert for deployments that never remove a channel. Full `npm test` green.
12
+
3
13
  ## v2.14.4
4
14
  - **The owner no longer gets asked to double-approve their own Kazee reply — split-brain owner resolution is fixed.** On Kazee a person is identified by `userId` while the room is `channelId` (the chat-document); the two differ. The auth layer already recognised the owner correctly (via `access.matchesTransportOwner` → `currentUserId()`), but the independent relationship guardrail re-derived ownership from `channelId` — which never equals the configured `KAZEE_OWNER_USER_ID` — and so classed the owner as *external*, escalating the owner's own approval a second time. `relationship.speakerFor` now takes an optional third `userId` and keys identity/owner classification off `speakerId = userId || channelId` (the person), while still replying/escalating to `channelId` (the room). The raw per-user id is threaded to every subprocess egress guard via a new `OC_CHANNEL_USER_ID` env var + loopback query/body param (`bin/cli.js`, `bin/entity.js`, `bin/tool.js`, `bin/loopback-client.js`, `core/io.js`, `core/loopback.js`, `core/system-prompt.js`). **On Telegram `userId === channelId` in DMs, so this is a strict no-op** for existing Telegram deployments.
5
15
  - **`/channel add kazee` now links the owner's Kazee handle at capture time.** Owner recognition previously lived only in the env/adapter (`KAZEE_OWNER_USER_ID`); the owner's people record had no Kazee handle, so the guardrail (which resolves through canonical identity) re-derived the operator as external. `finalizeOwner` now calls `people.linkHandle(owner.id, { adapter: "kazee", channelId: ownerUserId, … })` so the wizard's captured owner is recognised the same way the rest of the bot resolves identity — belt-and-suspenders with the `speakerFor` fix above.
package/core/actions.js CHANGED
@@ -488,28 +488,12 @@ async function handleAction(envelope) {
488
488
  if (d.startsWith("chn:")) {
489
489
  if (!isChatOwner(envelope.channelId)) { await send("Owner only."); return; }
490
490
  const wizard = require("./channel-wizard");
491
- const registry = require("./adapter-registry");
492
- if (d === "chn:add:kazee") { await wizard.start(envelope.canonicalUserId, "kazee"); return; }
491
+ if (d === "chn:add:kazee") { await wizard.start(envelope.canonicalUserId, "kazee", envelope); return; }
493
492
  if (d.startsWith("chn:rm:")) {
494
493
  const id = d.slice("chn:rm:".length);
495
494
  if (id === "telegram") { await send("Refusing to remove the Telegram adapter."); return; }
496
- const result = await registry.removeAdapter(id);
497
- if (!result.ok) { await send(result.error); return; }
498
- const { config, saveEnvKey } = require("./config");
499
- const channels = (config.CHANNELS || "telegram").split(",").map((s) => s.trim()).filter(Boolean);
500
- const kept = channels.filter((c) => c !== id && !c.startsWith(id + ":") && c !== id.split(":")[0]);
501
- const next = kept.join(",") || "telegram";
502
- saveEnvKey("CHANNELS", next);
503
- config.CHANNELS = next;
504
- if (id === "kazee") {
505
- saveEnvKey("KAZEE_URL", "");
506
- saveEnvKey("KAZEE_BOT_TOKEN", "");
507
- saveEnvKey("KAZEE_OWNER_USER_ID", "");
508
- config.KAZEE_URL = "";
509
- config.KAZEE_BOT_TOKEN = "";
510
- config.KAZEE_OWNER_USER_ID = "";
511
- }
512
- await send(`Removed channel: ${id}`);
495
+ const r = await wizard.removeChannel(id);
496
+ await send(r.ok ? r.summary : r.error);
513
497
  return;
514
498
  }
515
499
  }
@@ -325,6 +325,37 @@ async function finalizeOwner(canonicalUserId, session, ownerUserId) {
325
325
  );
326
326
  }
327
327
 
328
+ // Full teardown for a live channel, shared by the /channel remove command and
329
+ // the inline "Remove <id>" button so the two entry points can't drift. Removes
330
+ // the adapter, strips its keys from .env (so the change survives restart), and
331
+ // drops its handles + identity mappings through the in-memory stores — a stale
332
+ // handle/link otherwise survives in memory and gets rewritten on the next add,
333
+ // which is what made a re-add feel un-clean. Callers guard "telegram" (their
334
+ // own connection) and format r.summary / r.error for their channel.
335
+ async function removeChannel(id) {
336
+ const result = await registry.removeAdapter(id);
337
+ if (!result.ok) return { ok: false, error: result.error };
338
+ const transport = id.split(":")[0];
339
+ const channels = (config.CHANNELS || "telegram").split(",").map((s) => s.trim()).filter(Boolean);
340
+ const kept = channels.filter((c) => c !== id && !c.startsWith(id + ":") && c !== transport);
341
+ saveEnvKey("CHANNELS", kept.join(",") || "telegram");
342
+ config.CHANNELS = kept.join(",") || "telegram";
343
+ if (transport === "kazee") {
344
+ for (const k of ["KAZEE_URL", "KAZEE_BOT_TOKEN", "KAZEE_OWNER_USER_ID", "KAZEE_BOT_USER_ID"]) {
345
+ saveEnvKey(k, "");
346
+ config[k] = "";
347
+ }
348
+ }
349
+ let unlinked = 0;
350
+ try { unlinked = people.unlinkAdapterHandles(transport).length; } catch (e) {}
351
+ let idsCleared = false;
352
+ try { idsCleared = require("./identity").removeTransportIdentities(transport); } catch (e) {}
353
+ const extra = (unlinked || idsCleared)
354
+ ? ` — cleared ${unlinked} handle${unlinked === 1 ? "" : "s"} + identity links`
355
+ : "";
356
+ return { ok: true, id, transport, unlinked, idsCleared, summary: `Removed channel: ${id}${extra}` };
357
+ }
358
+
328
359
  module.exports = {
329
360
  isAwaiting,
330
361
  getSession,
@@ -334,5 +365,6 @@ module.exports = {
334
365
  handleText,
335
366
  handleAction,
336
367
  tryCaptureOwner,
368
+ removeChannel,
337
369
  validateKazee,
338
370
  };
package/core/handlers.js CHANGED
@@ -1672,23 +1672,8 @@ register({
1672
1672
  if (removeMatch) {
1673
1673
  const id = removeMatch[1];
1674
1674
  if (id === "telegram") return send("Refusing to remove the Telegram adapter from a /channel command — that's your own connection.");
1675
- const result = await registry.removeAdapter(id);
1676
- if (!result.ok) return send(result.error);
1677
- // Strip from .env so the change survives restart.
1678
- const channels = (config.CHANNELS || "telegram").split(",").map((s) => s.trim()).filter(Boolean);
1679
- const kept = channels.filter((c) => c !== id && !c.startsWith(id + ":") && c !== id.split(":")[0]);
1680
- const next = kept.join(",") || "telegram";
1681
- saveEnvKey("CHANNELS", next);
1682
- config.CHANNELS = next;
1683
- if (id === "kazee") {
1684
- saveEnvKey("KAZEE_URL", "");
1685
- saveEnvKey("KAZEE_BOT_TOKEN", "");
1686
- saveEnvKey("KAZEE_OWNER_USER_ID", "");
1687
- config.KAZEE_URL = "";
1688
- config.KAZEE_BOT_TOKEN = "";
1689
- config.KAZEE_OWNER_USER_ID = "";
1690
- }
1691
- return send(`Removed channel: ${id}`);
1675
+ const r = await wizard.removeChannel(id);
1676
+ return send(r.ok ? r.summary : r.error);
1692
1677
  }
1693
1678
 
1694
1679
  send("Usage: /channel | /channel add kazee | /channel remove <id>");
package/core/identity.js CHANGED
@@ -137,6 +137,25 @@ function setIdentityMapping(transport, channelId, canonicalUserId) {
137
137
  };
138
138
  }
139
139
 
140
+ // Remove every identity mapping for a transport — used when a channel is torn
141
+ // down (/channel remove) so no stale channel→canonical link or preferred-channel
142
+ // pointer lingers to be rewritten to disk on the next identity write. Mutates
143
+ // the in-memory map the whole process shares, THEN persists, so memory and disk
144
+ // stay in sync (a disk-only edit would be resurrected by the cached object).
145
+ function removeTransportIdentities(transport) {
146
+ const t = String(transport || "").toLowerCase();
147
+ if (!t) return false;
148
+ let changed = false;
149
+ for (const key of Object.keys(identities.channels)) {
150
+ if (key.slice(0, t.length + 1) === t + ":") { delete identities.channels[key]; changed = true; }
151
+ }
152
+ for (const [userId, pref] of Object.entries(identities.preferred)) {
153
+ if (pref && String(pref.transport || "").toLowerCase() === t) { delete identities.preferred[userId]; changed = true; }
154
+ }
155
+ if (changed) saveIdentities();
156
+ return changed;
157
+ }
158
+
140
159
  module.exports = {
141
160
  identities,
142
161
  normalizeCanonicalUserId,
@@ -149,5 +168,6 @@ module.exports = {
149
168
  canonicalForTelegram,
150
169
  canonicalForStoredUserKey,
151
170
  setIdentityMapping,
171
+ removeTransportIdentities,
152
172
  saveIdentities,
153
173
  };
package/core/people.js CHANGED
@@ -159,6 +159,30 @@ function unlinkHandle(personId, { adapter, channelId }) {
159
159
  return p;
160
160
  }
161
161
 
162
+ // Remove every handle for a given adapter across ALL people records — used when
163
+ // a channel is torn down (/channel remove) so no stranded handle survives to be
164
+ // re-linked or re-mirrored on a later add. primaryChannel falls back to the next
165
+ // remaining handle. Returns the ids of the people that were changed.
166
+ function unlinkAdapterHandles(adapter) {
167
+ const a = String(adapter || "").toLowerCase();
168
+ if (!a) return [];
169
+ const all = load();
170
+ const affected = [];
171
+ for (const p of all) {
172
+ const before = (p.handles || []).length;
173
+ if (!before) continue;
174
+ p.handles = p.handles.filter((h) => h.adapter !== a);
175
+ if (p.handles.length === before) continue;
176
+ if (p.primaryChannel && p.primaryChannel.adapter === a) {
177
+ p.primaryChannel = p.handles[0] ? { adapter: p.handles[0].adapter, channelId: p.handles[0].channelId } : null;
178
+ }
179
+ p.updatedAt = nowIso();
180
+ affected.push(p.id);
181
+ }
182
+ if (affected.length) save(all);
183
+ return affected;
184
+ }
185
+
162
186
  function setPrimary(personId, { adapter, channelId }) {
163
187
  const all = load();
164
188
  const p = all.find((x) => x.id === personId);
@@ -264,7 +288,7 @@ module.exports = {
264
288
  load, save, list, roster,
265
289
  add, update, remove,
266
290
  findById, findByName, findByCanonicalUserId, findByHandle,
267
- linkHandle, unlinkHandle, setPrimary,
291
+ linkHandle, unlinkHandle, unlinkAdapterHandles, setPrimary,
268
292
  note, removeNote,
269
293
  setEntitySlug, resolveEntitySlug,
270
294
  owners, hasOwnerRecord, seedOwnerFromLegacy,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "2.14.4",
3
+ "version": "2.14.6",
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": {