@inetafrica/open-claudia 2.2.7 → 2.2.9

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,12 @@
1
1
  # Changelog
2
2
 
3
+ ## v2.2.9
4
+ - Startup self-heal for grandfathered AgentSpace pods: if `.password-changed` exists on the PVC but AgentSpace was never notified (because the pod changed its password before `AGENTSPACE_API_URL` was injected, or before the `/pods/self/password-changed` callback shipped), the web server fires the callback once on startup and writes a `.password-changed-notified` sentinel so subsequent restarts don't retry. `notifyAgentSpacePasswordChanged` now returns a status so `setPassword` can also drop the sentinel after a successful live notification. Pre-v2.2.x grandfathered pods that never wrote the marker file still need manual `webPasswordUserSet=true` in Mongo.
5
+
6
+ ## v2.2.8
7
+ - `/upgrade` now works on docker containers that run our baked-in `/app` source (the common self-host layout). Previously the handler fell through to `npm install -g`, which hit `EACCES` because the runtime user is uid 1001 and couldn't write the global node_modules — and even when it could, the bot reads from `/app`, not the global root, so the new version was never picked up. The new branch detects the `/app` layout, `npm pack`s the latest tarball, overlays it onto `/app`, runs `npm install --omit=dev`, and exits so the orchestrator restarts the container on the new source. AgentSpace pods (which have `AGENTSPACE_POD_TOKEN` + `AGENTSPACE_API_URL`) still go through the control plane; nothing else changes for them or for npm-global installs.
8
+ - Dockerfile: `chown -R claudia:claudia /app` after the build steps so the runtime user can overlay new source during the in-place `/upgrade` above. Previously `/app` and its `node_modules` were root-owned because the COPY and `npm ci` ran as root before `USER 1001`.
9
+
3
10
  ## v2.2.7
4
11
  - Docker image now ships `git`, `jq`, `python3`, `python3-pip`, and `build-essential` so spawned coding agents don't fall back to curling random binaries into userspace when a basic tool is missing.
5
12
  - Symlink `/app/bin/cli.js` to `/usr/local/bin/open-claudia` so the CLI (used by agents for `send-file`, `task`, etc.) is on PATH from any cwd. Previously agents had to extract the packaged tgz to find it.
package/Dockerfile CHANGED
@@ -44,6 +44,9 @@ RUN chmod -R a+rX /app
44
44
  # Expose the open-claudia CLI on PATH so spawned agents can send files, manage tasks, etc.
45
45
  RUN chmod +x /app/bin/cli.js && ln -s /app/bin/cli.js /usr/local/bin/open-claudia
46
46
 
47
+ # Let the runtime user overlay new source into /app for in-place /upgrade.
48
+ RUN chown -R claudia:claudia /app
49
+
47
50
  # Entrypoint auto-configures from env vars on first run
48
51
  COPY docker-entrypoint.sh /usr/local/bin/
49
52
  RUN chmod +x /usr/local/bin/docker-entrypoint.sh
package/core/handlers.js CHANGED
@@ -329,6 +329,41 @@ async function requestAgentSpaceUpgrade() {
329
329
  });
330
330
  }
331
331
 
332
+ // True when the running bot's source lives at /app (docker image layout from
333
+ // our Dockerfile). In that case `npm install -g` can't actually upgrade us:
334
+ // the process reads code from /app, not from the global npm root, and uid
335
+ // 1001 can't write to /usr/local/lib/node_modules anyway. Detect this and
336
+ // run an in-place tarball overlay instead.
337
+ function isDockerAppLayout() {
338
+ try {
339
+ const pkgPath = path.resolve(path.join(__dirname, "..", "package.json"));
340
+ return pkgPath === "/app/package.json";
341
+ } catch (e) {
342
+ return false;
343
+ }
344
+ }
345
+
346
+ function inPlaceTarballUpgrade(targetVersion) {
347
+ const tmpDir = `/tmp/oc-upgrade-${process.pid}-${Date.now()}`;
348
+ fs.mkdirSync(tmpDir, { recursive: true });
349
+ const env = { ...process.env, PATH: FULL_PATH, HOME: process.env.HOME || require("os").homedir() };
350
+ const packOutput = execSync(`npm pack @inetafrica/open-claudia@${targetVersion}`, {
351
+ encoding: "utf-8", cwd: tmpDir, timeout: 120000, env,
352
+ }).trim();
353
+ // Last non-empty line of `npm pack` stdout is the tgz filename.
354
+ const tgzName = packOutput.split("\n").map((s) => s.trim()).filter(Boolean).pop();
355
+ const tgzPath = path.join(tmpDir, tgzName);
356
+ execSync(`tar -xzf "${tgzPath}" -C "${tmpDir}"`, { encoding: "utf-8", timeout: 60000 });
357
+ // npm pack tarballs always extract into a top-level "package/" directory.
358
+ // cp -a preserves perms and symlinks; the trailing /. copies contents only.
359
+ execSync(`cp -a "${tmpDir}/package/." /app/`, { encoding: "utf-8", timeout: 60000 });
360
+ try { fs.chmodSync("/app/bin/cli.js", 0o755); } catch (e) {}
361
+ execSync(`npm install --omit=dev --no-audit --no-fund`, {
362
+ encoding: "utf-8", cwd: "/app", timeout: 240000, env,
363
+ });
364
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (e) {}
365
+ }
366
+
332
367
  register({
333
368
  name: "upgrade", description: "Upgrade and restart", ownerOnly: true,
334
369
  handler: async (env) => {
@@ -350,6 +385,55 @@ register({
350
385
  return;
351
386
  }
352
387
  }
388
+ // Docker /app layout: bot runs from baked-in source, not from the global
389
+ // npm root, and as a non-root user. Do an in-place tarball overlay and
390
+ // exit; whatever orchestrates the container (k8s, docker restart=always)
391
+ // brings us back on the new source.
392
+ if (isDockerAppLayout()) {
393
+ try { process.chdir("/tmp"); } catch (e) {}
394
+ let latest = null;
395
+ try {
396
+ latest = execSync("npm view @inetafrica/open-claudia version", {
397
+ encoding: "utf-8", timeout: 15000,
398
+ env: { ...process.env, PATH: FULL_PATH, HOME: process.env.HOME || require("os").homedir() },
399
+ }).trim();
400
+ } catch (e) {
401
+ await send(`Upgrade failed: could not query npm registry (${(e.message || String(e)).slice(0, 200)}).`);
402
+ return;
403
+ }
404
+ if (latest === CURRENT_VERSION) {
405
+ await send(`Already on the latest version (v${CURRENT_VERSION}).`);
406
+ return;
407
+ }
408
+ await send(`Upgrading v${CURRENT_VERSION} → v${latest} (in-place tarball overlay into /app)...`);
409
+ try {
410
+ inPlaceTarballUpgrade(latest);
411
+ } catch (e) {
412
+ const errOutput = (e.stdout || e.stderr || e.message || String(e)).slice(-700);
413
+ await send(`Upgrade failed:\n${errOutput}`);
414
+ return;
415
+ }
416
+ let newVersion = latest;
417
+ let whatsNew = "";
418
+ try {
419
+ newVersion = JSON.parse(fs.readFileSync("/app/package.json", "utf-8")).version;
420
+ const changelog = fs.readFileSync("/app/CHANGELOG.md", "utf-8");
421
+ let versionHeader = `## v${newVersion}`;
422
+ let start = changelog.indexOf(versionHeader);
423
+ if (start < 0) { versionHeader = `## ${newVersion}`; start = changelog.indexOf(versionHeader); }
424
+ if (start >= 0) {
425
+ const afterHeader = changelog.slice(start + versionHeader.length);
426
+ const nextVersion = afterHeader.indexOf("\n## ");
427
+ const section = nextVersion >= 0 ? afterHeader.slice(0, nextVersion) : afterHeader;
428
+ whatsNew = section.trim();
429
+ }
430
+ } catch (e) {}
431
+ const tailNote = "Source refreshed in /app. If this release also changed the Dockerfile (apt packages, env, base image), the host still needs a docker pull + recreate for those.";
432
+ const msg = `Installed v${newVersion}.${whatsNew ? `\n\nWhat's new:\n${whatsNew}` : ""}\n\n${tailNote}\n\nRestarting...`;
433
+ await send(msg.length > 3900 ? msg.slice(0, 3900) : msg);
434
+ setTimeout(() => process.exit(0), 2000);
435
+ return;
436
+ }
353
437
  try { process.chdir(process.env.HOME || require("os").homedir()); } catch (e) {}
354
438
  let latest = null;
355
439
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "2.2.7",
3
+ "version": "2.2.9",
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
@@ -31,6 +31,10 @@ function getPassword() {
31
31
  }
32
32
 
33
33
  const PASSWORD_CHANGED_FILE = path.join(CONFIG_DIR, ".password-changed");
34
+ // Sentinel: written once AgentSpace has been notified about the user-set
35
+ // password. Used to keep the startup self-heal idempotent for grandfathered
36
+ // pods that changed their password before AGENTSPACE_API_URL was injected.
37
+ const PASSWORD_CHANGED_NOTIFIED_FILE = path.join(CONFIG_DIR, ".password-changed-notified");
34
38
 
35
39
  function isPasswordChanged() {
36
40
  return fs.existsSync(PASSWORD_CHANGED_FILE);
@@ -48,30 +52,56 @@ function validatePasswordComplexity(pw) {
48
52
  function setPassword(newPassword) {
49
53
  fs.writeFileSync(WEB_PASSWORD_FILE, newPassword);
50
54
  fs.writeFileSync(PASSWORD_CHANGED_FILE, new Date().toISOString());
51
- notifyAgentSpacePasswordChanged();
55
+ notifyAgentSpacePasswordChanged().then((r) => {
56
+ if (r && r.ok) {
57
+ try { fs.writeFileSync(PASSWORD_CHANGED_NOTIFIED_FILE, new Date().toISOString()); } catch (e) {}
58
+ }
59
+ }).catch(() => {});
52
60
  }
53
61
 
54
62
  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
- },
63
+ return new Promise((resolve) => {
64
+ const apiUrl = process.env.AGENTSPACE_API_URL;
65
+ const token = process.env.AGENTSPACE_POD_TOKEN;
66
+ if (!apiUrl || !token) return resolve({ ok: false, reason: "missing-env" });
67
+ let u;
68
+ try { u = new URL("/pods/self/password-changed", apiUrl); } catch (e) { return resolve({ ok: false, reason: "bad-url" }); }
69
+ const lib = u.protocol === "https:" ? require("https") : require("http");
70
+ const req = lib.request({
71
+ method: "POST",
72
+ hostname: u.hostname,
73
+ port: u.port || (u.protocol === "https:" ? 443 : 80),
74
+ path: u.pathname + u.search,
75
+ headers: {
76
+ "Authorization": `Bearer ${token}`,
77
+ "Content-Type": "application/json",
78
+ "Content-Length": "0",
79
+ },
80
+ }, (res) => {
81
+ res.on("data", () => {});
82
+ res.on("end", () => resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, status: res.statusCode }));
83
+ });
84
+ req.on("error", (e) => resolve({ ok: false, reason: String(e.message || e) }));
85
+ req.setTimeout(5000, () => { try { req.destroy(); } catch (e) {} resolve({ ok: false, reason: "timeout" }); });
86
+ req.end();
71
87
  });
72
- req.on("error", () => {});
73
- req.setTimeout(5000, () => { try { req.destroy(); } catch (e) {} });
74
- req.end();
88
+ }
89
+
90
+ // Startup self-heal for grandfathered pods. If the pod changed its web
91
+ // password before AGENTSPACE_API_URL was injected (or before the callback
92
+ // existed), the marker file is on disk but AgentSpace still thinks the
93
+ // initial password is valid. Fire the callback once on startup; on success
94
+ // write a sentinel so we don't repeat it on every restart.
95
+ function reconcileGrandfatheredPasswordChange() {
96
+ if (!isPasswordChanged()) return;
97
+ if (fs.existsSync(PASSWORD_CHANGED_NOTIFIED_FILE)) return;
98
+ if (!process.env.AGENTSPACE_API_URL || !process.env.AGENTSPACE_POD_TOKEN) return;
99
+ notifyAgentSpacePasswordChanged().then((r) => {
100
+ if (r && r.ok) {
101
+ try { fs.writeFileSync(PASSWORD_CHANGED_NOTIFIED_FILE, new Date().toISOString()); } catch (e) {}
102
+ console.log("[web] Notified AgentSpace of pre-existing user-set password.");
103
+ }
104
+ }).catch(() => {});
75
105
  }
76
106
 
77
107
  function checkBearerAuth(req) {
@@ -741,6 +771,7 @@ function startWebServer() {
741
771
  server.listen(PORT, () => {
742
772
  console.log(`Web UI running on http://localhost:${PORT}`);
743
773
  console.log("Admin password configured.");
774
+ reconcileGrandfatheredPasswordChange();
744
775
  });
745
776
 
746
777
  return server;