@inetafrica/open-claudia 2.2.2 → 2.2.4

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,13 @@
1
1
  # Changelog
2
2
 
3
+ ## v2.2.4
4
+ - Bearer auth on `/api/*`: when `BOT_CONTROL_TOKEN` is set in the env, requests with `Authorization: Bearer <token>` are accepted in addition to the existing cookie session. Behaviour is unchanged when the env var is not set, so local installs are not affected.
5
+ - `/upgrade` detects an AgentSpace-managed pod (both `AGENTSPACE_POD_TOKEN` and `AGENTSPACE_API_URL` set) and delegates to `POST /pods/self/upgrade` on the control plane instead of running `npm install -g` locally. The control plane rolls the deployment with a fresh image pull. Local installs fall through to the existing npm upgrade path.
6
+ - Password change in the web UI fires a fire-and-forget `POST /pods/self/password-changed` callback so the control plane can flag the pod as having a user-set password and refuse to leak the stale initial value.
7
+
8
+ ## v2.2.3
9
+ - Queue drain now batches: when multiple messages are received while a task is running, they're delivered as one combined follow-up turn instead of N isolated turns. The model sees them together (numbered, with HH:MM:SS queue timestamps), in context of what it just finished, so it can plan across them. A single queued message still delivers as before — no behavior change for the common case.
10
+
3
11
  ## v2.0.1
4
12
  - Kazee owner detection: `envelope.channelId` is the chat-document id, but `KAZEE_OWNER_USER_ID` is the Kazee user id. `isChatOwner`/`isChatAuthorized` now also short-circuit when the inbound user id matches the configured transport owner, so the owner running `/auth` (or anything else) on a fresh Kazee install is recognized immediately instead of being queued as a non-owner request.
5
13
  - `chatContext` now carries `userId` and `transport` in addition to `chatId`; new `currentUserId()` / `currentTransport()` exports.
package/core/handlers.js CHANGED
@@ -301,10 +301,55 @@ register({
301
301
  },
302
302
  });
303
303
 
304
+ async function requestAgentSpaceUpgrade() {
305
+ const apiUrl = process.env.AGENTSPACE_API_URL;
306
+ const token = process.env.AGENTSPACE_POD_TOKEN;
307
+ if (!apiUrl || !token) return null;
308
+ const u = new URL("/pods/self/upgrade", apiUrl);
309
+ const lib = u.protocol === "https:" ? require("https") : require("http");
310
+ return new Promise((resolve) => {
311
+ const req = lib.request({
312
+ method: "POST",
313
+ hostname: u.hostname,
314
+ port: u.port || (u.protocol === "https:" ? 443 : 80),
315
+ path: u.pathname + u.search,
316
+ headers: {
317
+ "Authorization": `Bearer ${token}`,
318
+ "Content-Type": "application/json",
319
+ "Content-Length": "0",
320
+ },
321
+ }, (res) => {
322
+ let data = "";
323
+ res.on("data", (c) => { data += c; });
324
+ res.on("end", () => resolve({ status: res.statusCode, body: data }));
325
+ });
326
+ req.on("error", (e) => resolve({ status: 0, body: String(e.message || e) }));
327
+ req.setTimeout(15000, () => { req.destroy(new Error("timeout")); });
328
+ req.end();
329
+ });
330
+ }
331
+
304
332
  register({
305
333
  name: "upgrade", description: "Upgrade and restart", ownerOnly: true,
306
334
  handler: async (env) => {
307
335
  if (!ownerEnv(env)) return;
336
+ // Container-managed upgrade: if AgentSpace injected pod-self credentials,
337
+ // delegate to the control plane so the deployment can be rolled with a
338
+ // fresh image pull. The local npm path can't do that.
339
+ if (process.env.AGENTSPACE_POD_TOKEN && process.env.AGENTSPACE_API_URL) {
340
+ try {
341
+ const result = await requestAgentSpaceUpgrade();
342
+ if (result && (result.status === 202 || result.status === 200)) {
343
+ await send("Upgrade requested — AgentSpace will pull latest image and restart, see you in a minute.");
344
+ return;
345
+ }
346
+ await send(`Upgrade request failed (status ${result?.status || "?"}). ${(result?.body || "").slice(0, 300)}`);
347
+ return;
348
+ } catch (e) {
349
+ await send(`Upgrade request error: ${e.message || e}`);
350
+ return;
351
+ }
352
+ }
308
353
  try { process.chdir(process.env.HOME || require("os").homedir()); } catch (e) {}
309
354
  let latest = null;
310
355
  try {
package/core/runner.js CHANGED
@@ -390,7 +390,7 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
390
390
  const { settings } = state;
391
391
 
392
392
  if (state.runningProcess) {
393
- state.messageQueue.push({ prompt, replyToMsgId, opts });
393
+ state.messageQueue.push({ prompt, replyToMsgId, opts, queuedAt: Date.now() });
394
394
  await send(state.isCompacting ? "Compacting context, will pick this up next…" : "Queued.", { replyTo: replyToMsgId });
395
395
  return;
396
396
  }
@@ -676,8 +676,27 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
676
676
  }
677
677
 
678
678
  if (state.messageQueue.length > 0 && state.currentSession) {
679
- const next = state.messageQueue.shift();
680
- await runClaude(next.prompt, state.currentSession.dir, next.replyToMsgId, next.opts);
679
+ const drained = state.messageQueue.splice(0);
680
+ if (drained.length === 1) {
681
+ const only = drained[0];
682
+ await runClaude(only.prompt, state.currentSession.dir, only.replyToMsgId, only.opts);
683
+ } else {
684
+ const fmtTime = (ts) => {
685
+ const d = new Date(ts);
686
+ const hh = String(d.getHours()).padStart(2, "0");
687
+ const mm = String(d.getMinutes()).padStart(2, "0");
688
+ const ss = String(d.getSeconds()).padStart(2, "0");
689
+ return `${hh}:${mm}:${ss}`;
690
+ };
691
+ const numbered = drained.map((m, i) => `[${i + 1}] (${fmtTime(m.queuedAt || Date.now())}) ${m.prompt}`).join("\n\n");
692
+ const batched =
693
+ `While you were working the user sent these ${drained.length} follow-up messages (oldest first):\n\n` +
694
+ `${numbered}\n\n` +
695
+ `Treat each as a distinct follow-up request. Add them to your plan and handle them; ` +
696
+ `if any contradicts an earlier one, prefer the newer one and call out the conflict.`;
697
+ const last = drained[drained.length - 1];
698
+ await runClaude(batched, state.currentSession.dir, last.replyToMsgId, last.opts);
699
+ }
681
700
  }
682
701
  }));
683
702
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "2.2.2",
3
+ "version": "2.2.4",
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/web.js CHANGED
@@ -48,9 +48,48 @@ function validatePasswordComplexity(pw) {
48
48
  function setPassword(newPassword) {
49
49
  fs.writeFileSync(WEB_PASSWORD_FILE, newPassword);
50
50
  fs.writeFileSync(PASSWORD_CHANGED_FILE, new Date().toISOString());
51
+ notifyAgentSpacePasswordChanged();
52
+ }
53
+
54
+ function notifyAgentSpacePasswordChanged() {
55
+ const apiUrl = process.env.AGENTSPACE_API_URL;
56
+ const token = process.env.AGENTSPACE_POD_TOKEN;
57
+ if (!apiUrl || !token) return;
58
+ let u;
59
+ try { u = new URL("/pods/self/password-changed", apiUrl); } catch (e) { return; }
60
+ const lib = u.protocol === "https:" ? require("https") : require("http");
61
+ const req = lib.request({
62
+ method: "POST",
63
+ hostname: u.hostname,
64
+ port: u.port || (u.protocol === "https:" ? 443 : 80),
65
+ path: u.pathname + u.search,
66
+ headers: {
67
+ "Authorization": `Bearer ${token}`,
68
+ "Content-Type": "application/json",
69
+ "Content-Length": "0",
70
+ },
71
+ });
72
+ req.on("error", () => {});
73
+ req.setTimeout(5000, () => { try { req.destroy(); } catch (e) {} });
74
+ req.end();
75
+ }
76
+
77
+ function checkBearerAuth(req) {
78
+ const expected = process.env.BOT_CONTROL_TOKEN;
79
+ if (!expected) return false;
80
+ const header = req.headers.authorization || "";
81
+ if (!header.startsWith("Bearer ")) return false;
82
+ const presented = header.slice(7).trim();
83
+ if (presented.length !== expected.length) return false;
84
+ try {
85
+ return crypto.timingSafeEqual(Buffer.from(presented), Buffer.from(expected));
86
+ } catch (e) {
87
+ return false;
88
+ }
51
89
  }
52
90
 
53
91
  function checkAuth(req) {
92
+ if (checkBearerAuth(req)) return true;
54
93
  const cookie = (req.headers.cookie || "").split(";").find((c) => c.trim().startsWith("oc_session="));
55
94
  if (!cookie) return false;
56
95
  const token = cookie.split("=")[1]?.trim();