@inetafrica/open-claudia 2.6.28 → 2.6.30

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,35 @@
1
1
  # Changelog
2
2
 
3
+ ## v2.6.30
4
+ - **Cluster self-management for AgentSpace pods.** When running as an AgentSpace pod, the bot can inspect and manage its own deployment through the backend broker — `/cluster status|logs [n]|restart|start|stop|scale <0|1>|sync` (owner-gated) for humans, and `open-claudia cluster …` for the agent. Both are thin clients over `core/cluster-client.js`, which posts to the broker's capability-gated `/pods/self/cluster` endpoint with the pod's bearer token; the bot holds no Kubernetes credentials. `brokerConfigured()` requires both `AGENTSPACE_API_URL` and `AGENTSPACE_POD_TOKEN`, so an unconfigured local install safely replies "not available" while a provisioned pod reaches the broker. Also tightens the `core/pack-guard.js` exfil heuristic: the broad "URL co-occurring with a sensitive word" rule (which false-positived on legitimate infra notes) is replaced by two precise carriers — a send/store/upload verb pointed at a URL/address, and a credential attached directly to a URL as a query/fragment param.
5
+
6
+ ## v2.6.29
7
+ - **Kazee channel hardening.** `send()` now retries up to 3× with 1s/2s backoff (skipping 4xx), `edit()` normalizes text the same way as `send()`, inbound handling is async and detects ogg/opus voice notes (promoting them from "audio" to "voice" so the router dispatches correctly), reply-to context best-effort fetches the replied-to message before emitting the envelope, and `format.js` converts stray Telegram HTML tags (`<b>`, `<code>`, `<pre>`, `<i>`, `<s>`, `<a>`) to Markdown as a safety net.
8
+
9
+ ## v2.6.28
10
+ - **Usage: report true peak context, not the inflated per-turn sum.** The result event re-counted the cached prefix once per tool round-trip, so `contextTokens` ballooned into the millions and the "Context this turn" alert fired every turn. It now tracks the peak single-call prefix (input + cache_read + cache_creation) and reports that as `contextTokens`; the summed value is kept as `billedContextTokens` for cost attribution.
11
+
12
+ ## v2.6.27
13
+ - **Tasks: recency-ranked injection, staleness footer, and dream hygiene.** Open-task injection now ranks by activity (max of root and child `updatedAt`) rather than status — the most-recently-worked render in full, colder ones render title-only with a `task get <id>` pointer (new accessor + loopback endpoint + CLI sub), and anything untouched >14d (`OPEN_CLAUDIA_TASK_STALE_DAYS`) collapses into a capped one-line stale footer. The dream pass gains a task-hygiene step that surfaces the stalest tasks in the morning report — flag-and-ask only; it never edits or deletes tasks.
14
+
15
+ ## v2.6.26
16
+ - **Memory: threat-scan guard + FTS-miss instrumentation.** `core/pack-guard.js` scans reviewer/dream content before write (override/exfil/base64 heuristics; strict on the auto-injected Stance/Procedure/State/description, lenient on Journal) and is wired into the pack reviewer and dream merge/umbrella/retag — a hit skips the write and announces one line. `logRecall` now records FTS-miss rescues (packs the judge kept that FTS scored ~0) into `recall-stats.json`, so a vector backend becomes a data-driven decision.
17
+
18
+ ## v2.6.25
19
+ - **Memory: provenance tracking for pack writes.** A per-pack `.provenance.json` sidecar records which origin (user / reviewer / dream) last authored each section; `updatePack`/`createPack` take an origin, and an automated writer never silently overwrites a user-authored Stance. An absent sidecar defaults to "user" (protect-by-default).
20
+
21
+ ## v2.6.24
22
+ - **Memory: pack usage telemetry + lifecycle retirement.** Packs gain `usage_count` telemetry (bumped on each use alongside `last_used`), a dream archive action retires cold packs (idle ≥30d, used ≤3×) into `.archived` with safety guards, and `pack archive`/`restore`/`archived` CLI commands are added; `/skills` surfaces the archived count. Also adds a post-dream chat summary (`DREAM_SUMMARY`), toggled with the new `/dreamsummary on|off` command (default on); when off, the nightly consolidation runs silently.
23
+
24
+ ## v2.6.23
25
+ - **Progressive pack disclosure on by default.** Injects Stance + State teasers with on-demand Procedure/Journal and widens the pack match limit to 6. Override with `PACK_PROGRESSIVE=off`.
26
+
27
+ ## v2.6.22
28
+ - **Wire `/skills` and `/learn` into the context-pack store.** Both previously read from / wrote to the empty legacy `~/.claude/skills` dir; now `/skills` lists packs with show/remove, and `/learn` folds work into a matching pack's Procedure plus a dated Journal line.
29
+
30
+ ## v2.6.21
31
+ - **CI: allow tests to run without bot env.** The test suite no longer requires bot environment variables to be present.
32
+
3
33
  ## v2.6.20
4
34
  - **AgentSpace pod durability.** `/upgrade` and password-change callbacks now preserve the `/api` base path expected by AgentSpace, CLI paths are resolved to concrete executables at startup, and runs repair a stale/missing session workspace before spawning Claude, Cursor, or Codex. This prevents misleading `spawn ... ENOENT` failures when the saved session cwd no longer exists.
5
35
 
package/README.md CHANGED
@@ -47,6 +47,7 @@ Send text, voice notes, screenshots, and files from your phone. Your chosen AI a
47
47
  - **Token economy** — byte-stable system prompt for maximum Anthropic prompt-cache hits; dynamic state rides each message instead
48
48
  - **Web UI** — optional browser UI for setup and config (`open-claudia start --web`)
49
49
  - **Auto-updates** — checks npm every 5 minutes, upgrade with `/upgrade`
50
+ - **Cluster self-management** — when running as an AgentSpace pod, the bot can inspect and manage its own deployment (`/cluster`, or `open-claudia cluster …` for the agent) through the broker, which authenticates, capability-checks, and audits every operation
50
51
  - **Requirements doctor** — `/doctor` checks CLI installs, auth, voice tools, and writable paths
51
52
  - **Cross-platform** — macOS, Linux, and Windows
52
53
 
@@ -187,6 +188,7 @@ When you select a project, the last conversation is automatically resumed. Tap "
187
188
  | `/learn [<hint>]` | Capture the last piece of work into the matching context pack |
188
189
  | `/skills [show\|remove <name>]` | List, show, or remove legacy learned skills |
189
190
  | `/soul` | View/edit assistant identity and personality |
191
+ | `/dreamsummary [on\|off]` | Toggle the post-dream memory summary in chat |
190
192
 
191
193
  ### Automation
192
194
 
@@ -235,9 +237,23 @@ Tokens are redacted from chat output and logs.
235
237
  | `/version` | Show current running version |
236
238
  | `/upgrade` | Upgrade to latest version and restart (includes a post-upgrade doctor summary) |
237
239
  | `/restart` | Restart the bot |
240
+ | `/dashboard` | Get a one-time login link to the web dashboard |
238
241
  | `/stop` | Cancel a running task |
239
242
  | `/help` | Show all commands |
240
243
 
244
+ ### Cluster control
245
+
246
+ Available only when the bot runs as an AgentSpace-provisioned pod (the broker credentials `AGENTSPACE_API_URL` and `AGENTSPACE_POD_TOKEN` are present). Off this network the command replies that it isn't available and does nothing. Owner-gated; every operation is authenticated, capability-checked, and audited by the broker, and confined to the bot's own deployment.
247
+
248
+ | Command | Description |
249
+ |---------|-------------|
250
+ | `/cluster status` | Deployment readiness/phase |
251
+ | `/cluster logs [n]` | Tail the last n container log lines |
252
+ | `/cluster restart` | Rollout restart (pulls latest image) |
253
+ | `/cluster start` / `/cluster stop` | Scale to 1 / 0 replicas |
254
+ | `/cluster scale <0\|1>` | Stop (0) or start (1) |
255
+ | `/cluster sync` | Re-apply the deployment manifest |
256
+
241
257
  ## Memory & Long-Term Context
242
258
 
243
259
  Open Claudia layers three memory systems on top of the backend's native sessions:
package/bin/cli.js CHANGED
@@ -274,6 +274,11 @@ switch (command) {
274
274
  break;
275
275
  }
276
276
 
277
+ case "cluster": {
278
+ require("./cluster").run(args.slice(1));
279
+ break;
280
+ }
281
+
277
282
  case "pack": {
278
283
  require("./pack").run(args.slice(1));
279
284
  break;
@@ -347,6 +352,9 @@ Background work (only inside an active bot-spawned task):
347
352
  open-claudia task add|list|start|done|remove Per-channel todo (survives restarts)
348
353
  open-claudia agent "<prompt>" [--role X] Spawn a throwaway sub-agent for research
349
354
 
355
+ Cluster control (self-management of this bot's own deployment via the broker):
356
+ open-claudia cluster status|logs|restart|start|stop|scale <0|1>|sync
357
+
350
358
  Multi-user / cross-channel:
351
359
  open-claudia people list|show|add|note|link Roster of known team members
352
360
  open-claudia intros list|approve|reject Pending introductions from unknown chats
package/bin/cluster.js ADDED
@@ -0,0 +1,75 @@
1
+ // open-claudia cluster <operation>
2
+ //
3
+ // Self-management of this bot's own Kubernetes deployment, mediated by the
4
+ // AgentSpace broker. The bot has no direct cluster credentials — every command
5
+ // here is a request the broker authenticates, capability-checks, audits, and
6
+ // executes against the pod's own namespace.
7
+
8
+ const { clusterRequest } = require("../core/cluster-client");
9
+
10
+ const HELP = `
11
+ Cluster control for this bot's own deployment (via the AgentSpace broker).
12
+
13
+ open-claudia cluster status Show deployment readiness/phase
14
+ open-claudia cluster logs [n] Tail the last n container log lines (default 100, max 1000)
15
+ open-claudia cluster restart Rollout restart (pulls latest image)
16
+ open-claudia cluster start Scale to 1 replica
17
+ open-claudia cluster stop Scale to 0 replicas
18
+ open-claudia cluster scale <0|1> Stop (0) or start (1)
19
+ open-claudia cluster sync Re-apply the deployment manifest
20
+
21
+ All operations are confined to this pod's own namespace and gated by the
22
+ capabilities the broker has granted this bot.
23
+ `;
24
+
25
+ function ok(line) { if (line) console.log(line); process.exit(0); }
26
+ function fail(line) { if (line) console.error(line); process.exit(1); }
27
+
28
+ // Render a broker response into a human line + correct exit code.
29
+ function report(res, successLine) {
30
+ if (res.status >= 200 && res.status < 300) {
31
+ ok(typeof successLine === "function" ? successLine(res.json || {}) : successLine);
32
+ }
33
+ const detail = res.json && res.json.error ? res.json.error : (res.body || "").slice(0, 400);
34
+ if (res.status === 0) fail(`Broker request failed: ${detail}`);
35
+ fail(`Broker ${res.status}: ${detail}`);
36
+ }
37
+
38
+ async function run(args) {
39
+ const op = (args[0] || "").toLowerCase();
40
+ const rest = args.slice(1);
41
+ if (!op || op === "help" || op === "--help") { console.log(HELP); process.exit(op ? 0 : 2); }
42
+
43
+ switch (op) {
44
+ case "status": {
45
+ const res = await clusterRequest("status");
46
+ return report(res, (j) =>
47
+ `Deployment: ${j.phase || "?"}${j.ready ? " (ready)" : ""}${j.message ? ` — ${j.message}` : ""}`);
48
+ }
49
+ case "logs": {
50
+ const tailLines = rest[0] ? parseInt(rest[0], 10) : undefined;
51
+ if (rest[0] && Number.isNaN(tailLines)) fail("Usage: cluster logs [n] (n = number of lines)");
52
+ const res = await clusterRequest("logs", tailLines ? { tailLines } : {});
53
+ return report(res, (j) => (j.logs || "(no logs)"));
54
+ }
55
+ case "restart":
56
+ return report(await clusterRequest("restart"), (j) => j.message || "Restart requested.");
57
+ case "start":
58
+ return report(await clusterRequest("start"), (j) => j.message || "Started.");
59
+ case "stop":
60
+ return report(await clusterRequest("stop"), (j) => j.message || "Stopped.");
61
+ case "scale": {
62
+ const n = rest[0];
63
+ if (n !== "0" && n !== "1") fail("Usage: cluster scale <0|1>");
64
+ return report(await clusterRequest("scale", { replicas: parseInt(n, 10) }), (j) => j.message || `Scaled to ${n}.`);
65
+ }
66
+ case "sync":
67
+ return report(await clusterRequest("sync"), (j) => j.message || "Reconciled.");
68
+ default:
69
+ console.error(`Unknown cluster operation: ${op}`);
70
+ console.log(HELP);
71
+ process.exit(2);
72
+ }
73
+ }
74
+
75
+ module.exports = { run, HELP };
@@ -12,6 +12,7 @@ const { TEMP_DIR, FILES_DIR } = require("../../core/config");
12
12
  const { canonicalForChannel } = require("../../core/identity");
13
13
  const { inlineKeyboardToPortable } = require("../types");
14
14
  const { createKazeeSocket } = require("./socket");
15
+ const { normalize } = require("./format");
15
16
 
16
17
  class KazeeAdapter {
17
18
  constructor({ id = "kazee", url, token, ownerUserId, botUserId }) {
@@ -98,7 +99,7 @@ class KazeeAdapter {
98
99
  });
99
100
  }
100
101
 
101
- _handleInboundMessage(msg) {
102
+ async _handleInboundMessage(msg) {
102
103
  if (!msg) return;
103
104
  const senderId = String(msg.sender?._id || msg.sender?.id || msg.senderId || "");
104
105
  if (this.botUserId && senderId === String(this.botUserId)) return; // ignore our own echoes
@@ -169,6 +170,37 @@ class KazeeAdapter {
169
170
  }
170
171
  }
171
172
 
173
+ // Voice/audio distinction: chat-central has no "voice" type, so voice
174
+ // notes arrive as "audio". Detect them by mimeType (ogg/opus) and
175
+ // promote to "voice" so the router can transcribe them correctly.
176
+ if (envelope.type === "audio" && envelope.media?.length) {
177
+ const mime = (envelope.media[0].mimeType || "").toLowerCase();
178
+ if (mime.includes("ogg") || mime.includes("opus")) {
179
+ envelope.type = "voice";
180
+ envelope.media[0].type = "voice";
181
+ }
182
+ }
183
+
184
+ // Reply-to context: fetch the replied-to message content so the router
185
+ // has the same context Telegram provides. Best-effort — failure is silent.
186
+ if (msg.replyTo) {
187
+ try {
188
+ const data = await this._request("GET", `/message/${encodeURIComponent(String(msg.replyTo))}`);
189
+ const m = data?.message || data;
190
+ if (m && (m._id || m.id)) {
191
+ envelope.reply = {
192
+ text: m.content || "",
193
+ from: {
194
+ id: String(m.sender?._id || m.sender?.id || ""),
195
+ name: m.sender?.name || m.sender?.displayName || "",
196
+ username: m.sender?.username || "",
197
+ },
198
+ messageId: m._id || m.id,
199
+ };
200
+ }
201
+ } catch (e) { /* best-effort — envelope is still valid without reply context */ }
202
+ }
203
+
172
204
  this._emit("message", envelope);
173
205
  }
174
206
 
@@ -184,24 +216,42 @@ class KazeeAdapter {
184
216
  const body = {
185
217
  chat_id: channelId,
186
218
  sender: this.botUserId,
187
- content: text,
219
+ content: normalize(text),
188
220
  type: buttons ? "interactive" : "text",
189
221
  findMeCode: `oc-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
190
222
  };
191
223
  if (opts.replyTo) body.replyTo = opts.replyTo;
192
224
  if (buttons) body.interactive = { buttons };
193
- try {
194
- const res = await this._request("POST", `/chat/${encodeURIComponent(channelId)}/message`, body);
195
- return res?.message?._id || res?.messageId || res?._id || null;
196
- } catch (e) {
197
- console.error("Kazee send error:", e.message);
198
- return null;
225
+
226
+ for (let attempt = 0; attempt < 3; attempt++) {
227
+ try {
228
+ const res = await this._request("POST", `/chat/${encodeURIComponent(channelId)}/message`, body);
229
+ return res?.message?._id || res?.messageId || res?._id || null;
230
+ } catch (e) {
231
+ const errMsg = e.message || "";
232
+ // Don't retry on client errors (4xx) — they won't self-heal.
233
+ const statusMatch = errMsg.match(/→ (\d{3})/);
234
+ const status = statusMatch ? parseInt(statusMatch[1], 10) : 0;
235
+ if (status >= 400 && status < 500) {
236
+ console.error("Kazee send error (not retrying):", errMsg);
237
+ return null;
238
+ }
239
+ if (attempt < 2) {
240
+ const delay = (attempt + 1) * 1000;
241
+ console.error(`Kazee send error (attempt ${attempt + 1}/3, retrying in ${delay}ms):`, errMsg);
242
+ await new Promise((r) => setTimeout(r, delay));
243
+ continue;
244
+ }
245
+ console.error("Kazee send error (exhausted retries):", errMsg);
246
+ return null;
247
+ }
199
248
  }
249
+ return null;
200
250
  }
201
251
 
202
252
  async edit(channelId, messageId, text, opts = {}) {
203
253
  const buttons = this._normalizeKeyboard(opts.keyboard);
204
- const body = { content: text };
254
+ const body = { content: normalize(text) };
205
255
  if (buttons) {
206
256
  body.type = "interactive";
207
257
  body.interactive = { buttons };
@@ -1,9 +1,27 @@
1
- // Kazee uses standard markdown end-to-end. Telegram-flavoured asterisks
2
- // and underscores still render fine, so this is mostly a hook for future
3
- // divergence (image embeds, code-block fences, etc.).
1
+ // Kazee renders standard markdown end-to-end. This module normalizes text
2
+ // before sending: converts any stray Telegram HTML tags (which the model may
3
+ // accidentally emit) into their markdown equivalents so they render correctly
4
+ // instead of appearing as raw tag noise.
5
+
6
+ function htmlToMarkdown(text) {
7
+ return String(text || "")
8
+ .replace(/<pre><code[^>]*>([\s\S]*?)<\/code><\/pre>/gi, (_, code) => "```\n" + code + "\n```")
9
+ .replace(/<pre>([\s\S]*?)<\/pre>/gi, (_, code) => "```\n" + code + "\n```")
10
+ .replace(/<code>([\s\S]*?)<\/code>/gi, "`$1`")
11
+ .replace(/<b>([\s\S]*?)<\/b>/gi, "**$1**")
12
+ .replace(/<strong>([\s\S]*?)<\/strong>/gi, "**$1**")
13
+ .replace(/<i>([\s\S]*?)<\/i>/gi, "_$1_")
14
+ .replace(/<em>([\s\S]*?)<\/em>/gi, "_$1_")
15
+ .replace(/<s>([\s\S]*?)<\/s>/gi, "~~$1~~")
16
+ .replace(/<strike>([\s\S]*?)<\/strike>/gi, "~~$1~~")
17
+ .replace(/<del>([\s\S]*?)<\/del>/gi, "~~$1~~")
18
+ .replace(/<a href="([^"]+)">([\s\S]*?)<\/a>/gi, "[$2]($1)")
19
+ .replace(/<[^>]+>/g, "")
20
+ .replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#(\d+);/g, (_, n) => String.fromCodePoint(Number(n)));
21
+ }
4
22
 
5
23
  function normalize(text) {
6
- return String(text || "");
24
+ return htmlToMarkdown(String(text || ""));
7
25
  }
8
26
 
9
27
  module.exports = { normalize };
@@ -0,0 +1,79 @@
1
+ // Thin client for the AgentSpace cluster-control broker.
2
+ //
3
+ // Both entry points use this single implementation:
4
+ // - bin/cluster.js (the `open-claudia cluster ...` CLI, driven by the agent)
5
+ // - core/handlers.js (the `/cluster` Telegram slash command, driven by a human)
6
+ //
7
+ // The bot holds no Kubernetes credentials. It authenticates to the broker as
8
+ // itself with the pod-self token; the broker enforces capabilities, policy and
9
+ // audit and performs the actual cluster operation on the pod's own namespace.
10
+
11
+ const CLUSTER_OPERATIONS = ["status", "logs", "restart", "start", "stop", "scale", "sync"];
12
+
13
+ function brokerConfigured() {
14
+ return !!(process.env.AGENTSPACE_API_URL && process.env.AGENTSPACE_POD_TOKEN);
15
+ }
16
+
17
+ // Build the absolute URL to a pods/self/* endpoint, mirroring the path-base
18
+ // logic used by the upgrade flow in core/handlers.js.
19
+ function endpoint(pathSuffix) {
20
+ const apiUrl = process.env.AGENTSPACE_API_URL;
21
+ const base = new URL(apiUrl);
22
+ if (base.pathname === "/") base.pathname = "/api/";
23
+ if (!base.pathname.endsWith("/")) base.pathname += "/";
24
+ return new URL(pathSuffix, base);
25
+ }
26
+
27
+ // POST a cluster operation to the broker. Resolves to { status, body, json }.
28
+ // Never rejects on HTTP/transport errors — surfaces them via status 0 so callers
29
+ // can format a single error path.
30
+ function clusterRequest(operation, params = {}) {
31
+ return new Promise((resolve) => {
32
+ if (!brokerConfigured()) {
33
+ resolve({
34
+ status: 0,
35
+ body: "AgentSpace broker not configured (AGENTSPACE_API_URL / AGENTSPACE_POD_TOKEN missing). This only works inside a pod provisioned by AgentSpace.",
36
+ json: null,
37
+ });
38
+ return;
39
+ }
40
+
41
+ let u;
42
+ try {
43
+ u = endpoint("pods/self/cluster");
44
+ } catch (e) {
45
+ resolve({ status: 0, body: `Bad AGENTSPACE_API_URL: ${e.message || e}`, json: null });
46
+ return;
47
+ }
48
+
49
+ const payload = JSON.stringify({ operation, ...params });
50
+ const lib = u.protocol === "https:" ? require("https") : require("http");
51
+ const req = lib.request(
52
+ {
53
+ method: "POST",
54
+ hostname: u.hostname,
55
+ port: u.port || (u.protocol === "https:" ? 443 : 80),
56
+ path: u.pathname + u.search,
57
+ headers: {
58
+ Authorization: `Bearer ${process.env.AGENTSPACE_POD_TOKEN}`,
59
+ "Content-Type": "application/json",
60
+ "Content-Length": Buffer.byteLength(payload),
61
+ },
62
+ },
63
+ (res) => {
64
+ let data = "";
65
+ res.on("data", (c) => { data += c; });
66
+ res.on("end", () => {
67
+ let json = null;
68
+ try { json = JSON.parse(data); } catch (e) {}
69
+ resolve({ status: res.statusCode, body: data, json });
70
+ });
71
+ }
72
+ );
73
+ req.on("error", (e) => resolve({ status: 0, body: String(e.message || e), json: null }));
74
+ req.setTimeout(20000, () => { req.destroy(new Error("timeout")); });
75
+ req.end(payload);
76
+ });
77
+ }
78
+
79
+ module.exports = { clusterRequest, brokerConfigured, CLUSTER_OPERATIONS };
package/core/handlers.js CHANGED
@@ -525,6 +525,52 @@ register({
525
525
  },
526
526
  });
527
527
 
528
+ register({
529
+ name: "cluster", description: "Control this bot's own deployment (status/logs/restart/start/stop/scale/sync)",
530
+ args: "<status|logs|restart|start|stop|scale 0|1|sync>", ownerOnly: true,
531
+ handler: async (env, { tail }) => {
532
+ if (!ownerEnv(env)) return;
533
+ const { clusterRequest, brokerConfigured } = require("./cluster-client");
534
+ if (!brokerConfigured()) {
535
+ return send("Cluster control isn't available — this bot isn't running under AgentSpace (no broker credentials).");
536
+ }
537
+ const parts = (tail || "").trim().split(/\s+/).filter(Boolean);
538
+ const op = (parts[0] || "").toLowerCase();
539
+ const allowed = ["status", "logs", "restart", "start", "stop", "scale", "sync"];
540
+ if (!op || !allowed.includes(op)) {
541
+ return send("Usage: /cluster <status | logs [n] | restart | start | stop | scale 0|1 | sync>");
542
+ }
543
+ const params = {};
544
+ if (op === "logs" && parts[1]) {
545
+ const n = parseInt(parts[1], 10);
546
+ if (!Number.isNaN(n)) params.tailLines = n;
547
+ }
548
+ if (op === "scale") {
549
+ const n = parts[1];
550
+ if (n !== "0" && n !== "1") return send("Usage: /cluster scale <0|1>");
551
+ params.replicas = parseInt(n, 10);
552
+ }
553
+ try {
554
+ const res = await clusterRequest(op, params);
555
+ if (res.status >= 200 && res.status < 300) {
556
+ const j = res.json || {};
557
+ if (op === "status") {
558
+ return send(`Deployment: ${j.phase || "?"}${j.ready ? " (ready)" : ""}${j.message ? ` — ${j.message}` : ""}`);
559
+ }
560
+ if (op === "logs") {
561
+ const logs = j.logs || "(no logs)";
562
+ return send(logs.length > 3500 ? logs.slice(-3500) : logs);
563
+ }
564
+ return send(j.message || "Done.");
565
+ }
566
+ const detail = (res.json && res.json.error) ? res.json.error : (res.body || "").slice(0, 300);
567
+ return send(res.status === 0 ? `Cluster request failed: ${detail}` : `Cluster ${res.status}: ${detail}`);
568
+ } catch (e) {
569
+ return send(`Cluster error: ${e.message || e}`);
570
+ }
571
+ },
572
+ });
573
+
528
574
  register({
529
575
  name: "projects", description: "Browse all workspace projects",
530
576
  handler: async (env) => { if (authorized(env)) send("Pick:", { keyboard: projectKeyboard() }); },
@@ -29,8 +29,15 @@ const OVERRIDE_PATTERNS = [
29
29
  // Exfiltration: a send/post/upload verb pointed at a URL or address. Scanned in
30
30
  // every section, including Journal.
31
31
  const EXFIL_PATTERNS = [
32
- /\b(send|post|upload|exfiltrate|forward|transmit|leak|email|curl|wget|fetch)\b[^.\n]{0,60}\b(to|at|into)\b[^.\n]{0,40}(https?:\/\/|ftp:\/\/|[\w.-]+@[\w.-]+)/i,
33
- /\b(https?:\/\/)[^\s)]+[^\s)]*\b.*\b(api\s*key|token|password|secret|credential|env)/i,
32
+ // A send/store/upload verb pointed at a URL or address.
33
+ /\b(send|post|upload|exfiltrate|forward|transmit|leak|email|curl|wget|fetch|store|save|drop|put|copy)\b[^.\n]{0,60}\b(to|at|into)\b[^.\n]{0,40}(https?:\/\/|ftp:\/\/|[\w.-]+@[\w.-]+)/i,
34
+ // A credential embedded directly in a URL as a query/fragment param
35
+ // (e.g. https://evil/?token=…): the classic verbless exfil carrier. Prose that
36
+ // merely mentions a URL near the word "token" no longer trips — the credential
37
+ // must actually be attached to the URL with =/:, which legitimate infra/ops
38
+ // notes don't do. This replaces a broad URL+keyword co-occurrence rule that
39
+ // false-positived on benign sentences like "the API at <url> uses a token".
40
+ /(https?:\/\/)[^\s)]*?\b(api_?key|access_?token|token|password|secret|credential)\b\s*[=:]/i,
34
41
  /\bcurl\b[^\n]*\b(https?:\/\/)/i,
35
42
  ];
36
43
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "2.6.28",
3
+ "version": "2.6.30",
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": {