@inetafrica/open-claudia 1.14.7 → 1.14.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 +18 -0
- package/bot.js +87 -28
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v1.14.9
|
|
4
|
+
- Fix `/upgrade`: `latest` was const-scoped to the version-check try
|
|
5
|
+
block, so the install step ran with `latest` undefined and threw
|
|
6
|
+
`ReferenceError: latest is not defined`. Hoist the variable and fall
|
|
7
|
+
back to npm's `latest` dist-tag if `npm view` fails. Regression
|
|
8
|
+
introduced in 745c6cf — broke v1.14.7 and v1.14.8 upgrades.
|
|
9
|
+
|
|
10
|
+
## v1.14.8
|
|
11
|
+
- Fix: `/stop` and the 6h hard-timeout now walk the full descendant
|
|
12
|
+
process tree, not just the Claude CLI's process group. Background
|
|
13
|
+
bashes started with `run_in_background: true` (and other detached
|
|
14
|
+
children) used to survive the kill and keep running indefinitely —
|
|
15
|
+
e.g. a polling `until` loop with a broken grep would burn CPU and
|
|
16
|
+
network for hours after the agent was already dead.
|
|
17
|
+
- Fix: on bot startup, sweep stale `claude login` / `claude setup-token`
|
|
18
|
+
processes older than 30 minutes. These were left over from previous
|
|
19
|
+
bot crashes (blocked on stdin forever) and could hold Keychain locks.
|
|
20
|
+
|
|
3
21
|
## v1.14.0
|
|
4
22
|
- Claude Code auth management from Telegram: `/auth_status`, `/login`, `/setup_token`, `/use_oauth_token`, and `/clear_oauth_token`
|
|
5
23
|
- Claude subprocesses now receive `CLAUDE_CODE_OAUTH_TOKEN` from config/env/vault when available, avoiding launchd/macOS Keychain auth failures
|
package/bot.js
CHANGED
|
@@ -7,16 +7,51 @@ const cron = require("node-cron");
|
|
|
7
7
|
const Vault = require("./vault");
|
|
8
8
|
const CONFIG_DIR = require("./config-dir");
|
|
9
9
|
|
|
10
|
+
// ── Process tree helpers ───────────────────────────────────────────
|
|
11
|
+
// Background tools (e.g. Bash run_in_background, /stop on a dev server)
|
|
12
|
+
// often spawn into their own session/group, so a single PGID kill on the
|
|
13
|
+
// Claude CLI doesn't reach them. We walk the descendant tree explicitly
|
|
14
|
+
// and signal each PID. Without this, runaway poll-loops survive
|
|
15
|
+
// indefinitely after the parent CLI is SIGTERMed.
|
|
16
|
+
function descendantPids(rootPid) {
|
|
17
|
+
const seen = new Set();
|
|
18
|
+
const queue = [Number(rootPid)];
|
|
19
|
+
while (queue.length) {
|
|
20
|
+
const pid = queue.shift();
|
|
21
|
+
if (!pid || seen.has(pid)) continue;
|
|
22
|
+
seen.add(pid);
|
|
23
|
+
try {
|
|
24
|
+
const out = execSync(`pgrep -P ${pid}`, { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
25
|
+
for (const line of out.split("\n")) {
|
|
26
|
+
const child = Number(line.trim());
|
|
27
|
+
if (child) queue.push(child);
|
|
28
|
+
}
|
|
29
|
+
} catch (e) {
|
|
30
|
+
// pgrep exits 1 when no children — ignore.
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
seen.delete(Number(rootPid));
|
|
34
|
+
return [...seen];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function killProcessTree(rootPid, signal = "SIGTERM") {
|
|
38
|
+
if (!rootPid) return;
|
|
39
|
+
const descendants = descendantPids(rootPid);
|
|
40
|
+
// Kill the process group first (covers the common case quickly).
|
|
41
|
+
try { process.kill(-rootPid, signal); } catch (e) {}
|
|
42
|
+
try { process.kill(rootPid, signal); } catch (e) {}
|
|
43
|
+
// Then mop up any descendants that escaped the group (detached
|
|
44
|
+
// children with their own session — e.g. bash run_in_background).
|
|
45
|
+
for (const pid of descendants) {
|
|
46
|
+
try { process.kill(pid, signal); } catch (e) {}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
10
50
|
// ── Graceful shutdown & error handling ─────────────────────────────
|
|
11
51
|
async function gracefulShutdown(signal) {
|
|
12
52
|
console.log(`Received ${signal}, shutting down gracefully...`);
|
|
13
|
-
// Stop any running Claude process
|
|
14
53
|
if (typeof runningProcess !== "undefined" && runningProcess) {
|
|
15
|
-
|
|
16
|
-
process.kill(-runningProcess.pid, "SIGTERM");
|
|
17
|
-
} catch (e) {
|
|
18
|
-
try { runningProcess.kill("SIGTERM"); } catch (e2) {}
|
|
19
|
-
}
|
|
54
|
+
killProcessTree(runningProcess.pid, "SIGTERM");
|
|
20
55
|
}
|
|
21
56
|
// Clean up temp media files older than 1 hour
|
|
22
57
|
try {
|
|
@@ -1178,17 +1213,13 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
1178
1213
|
const startTime = Date.now();
|
|
1179
1214
|
let longRunningNotified = false;
|
|
1180
1215
|
|
|
1181
|
-
// Hard timeout to prevent runaway processes
|
|
1216
|
+
// Hard timeout to prevent runaway processes. We walk the descendant
|
|
1217
|
+
// tree (not just the PGID) so that detached background bashes — like
|
|
1218
|
+
// `run_in_background: true` poll loops — get cleaned up too.
|
|
1182
1219
|
const processTimeout = setTimeout(async () => {
|
|
1183
1220
|
if (runningProcess === proc) {
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
setTimeout(() => {
|
|
1187
|
-
try { process.kill(-proc.pid, "SIGKILL"); } catch (e) {}
|
|
1188
|
-
}, 5000);
|
|
1189
|
-
} catch (e) {
|
|
1190
|
-
try { proc.kill("SIGKILL"); } catch (e2) {}
|
|
1191
|
-
}
|
|
1221
|
+
killProcessTree(proc.pid, "SIGTERM");
|
|
1222
|
+
setTimeout(() => killProcessTree(proc.pid, "SIGKILL"), 5000);
|
|
1192
1223
|
await send(`Task timed out after ${MAX_PROCESS_TIMEOUT / 60000} minutes. Stopped.`);
|
|
1193
1224
|
}
|
|
1194
1225
|
}, MAX_PROCESS_TIMEOUT);
|
|
@@ -1517,9 +1548,12 @@ bot.onText(/\/upgrade$/, async (msg) => {
|
|
|
1517
1548
|
// Change to HOME first — npm install -g replaces the package directory
|
|
1518
1549
|
// which can delete the cwd out from under the running process
|
|
1519
1550
|
try { process.chdir(process.env.HOME || require("os").homedir()); } catch (e) { /* already gone */ }
|
|
1520
|
-
// Check if there's actually a newer version
|
|
1551
|
+
// Check if there's actually a newer version. `latest` is hoisted so
|
|
1552
|
+
// the install step below can reference it; if `npm view` fails we
|
|
1553
|
+
// fall back to npm's `latest` dist-tag.
|
|
1554
|
+
let latest = null;
|
|
1521
1555
|
try {
|
|
1522
|
-
|
|
1556
|
+
latest = execSync("npm view @inetafrica/open-claudia version", {
|
|
1523
1557
|
encoding: "utf-8", timeout: 15000,
|
|
1524
1558
|
env: { ...process.env, PATH: FULL_PATH, HOME: process.env.HOME || require("os").homedir() },
|
|
1525
1559
|
}).trim();
|
|
@@ -1531,8 +1565,9 @@ bot.onText(/\/upgrade$/, async (msg) => {
|
|
|
1531
1565
|
} catch (e) {
|
|
1532
1566
|
await send("Upgrading...");
|
|
1533
1567
|
}
|
|
1568
|
+
const installTarget = latest || "latest";
|
|
1534
1569
|
try {
|
|
1535
|
-
execSync(`npm install -g @inetafrica/open-claudia@${
|
|
1570
|
+
execSync(`npm install -g @inetafrica/open-claudia@${installTarget} 2>&1`, {
|
|
1536
1571
|
encoding: "utf-8", timeout: 120000,
|
|
1537
1572
|
cwd: process.env.HOME || require("os").homedir(),
|
|
1538
1573
|
env: { ...process.env, PATH: FULL_PATH, HOME: process.env.HOME || require("os").homedir() },
|
|
@@ -1697,16 +1732,10 @@ bot.onText(/\/stop/, async (msg) => {
|
|
|
1697
1732
|
if (!isAuthorized(msg)) return;
|
|
1698
1733
|
if (runningProcess) {
|
|
1699
1734
|
const pid = runningProcess.pid;
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
try { runningProcess.kill("SIGTERM"); } catch (e2) {}
|
|
1705
|
-
}
|
|
1706
|
-
// Force kill after 3 seconds if still alive
|
|
1707
|
-
setTimeout(() => {
|
|
1708
|
-
try { process.kill(-pid, "SIGKILL"); } catch (e) {}
|
|
1709
|
-
}, 3000);
|
|
1735
|
+
// Walk the descendant tree so detached children (background bashes,
|
|
1736
|
+
// dev servers) get cleaned up, not just the PGID.
|
|
1737
|
+
killProcessTree(pid, "SIGTERM");
|
|
1738
|
+
setTimeout(() => killProcessTree(pid, "SIGKILL"), 3000);
|
|
1710
1739
|
runningProcess = null;
|
|
1711
1740
|
if (streamInterval) clearTimeout(streamInterval);
|
|
1712
1741
|
messageQueue = [];
|
|
@@ -2206,6 +2235,36 @@ try {
|
|
|
2206
2235
|
console.warn("Setup check skipped:", e.message);
|
|
2207
2236
|
}
|
|
2208
2237
|
|
|
2238
|
+
// Sweep stale `claude login` / `claude setup-token` processes that
|
|
2239
|
+
// survived a previous bot crash. They're blocked on stdin forever and
|
|
2240
|
+
// can hold keychain locks / resources. Anything older than 30 minutes
|
|
2241
|
+
// is assumed to be abandoned.
|
|
2242
|
+
try {
|
|
2243
|
+
const out = execSync(
|
|
2244
|
+
`ps -axo pid,etime,command | awk '/claude (login|setup-token)/ && !/awk/'`,
|
|
2245
|
+
{ encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] },
|
|
2246
|
+
);
|
|
2247
|
+
for (const line of out.split("\n")) {
|
|
2248
|
+
const m = line.trim().match(/^(\d+)\s+(\S+)\s/);
|
|
2249
|
+
if (!m) continue;
|
|
2250
|
+
const pid = Number(m[1]);
|
|
2251
|
+
const elapsed = m[2]; // [DD-]HH:MM:SS or MM:SS
|
|
2252
|
+
const minutes = elapsed.includes("-")
|
|
2253
|
+
? Number(elapsed.split("-")[0]) * 24 * 60 + 60 // any "DD-" form is ≥1 day
|
|
2254
|
+
: elapsed.split(":").length === 3
|
|
2255
|
+
? Number(elapsed.split(":")[0]) * 60 + Number(elapsed.split(":")[1])
|
|
2256
|
+
: Number(elapsed.split(":")[0]);
|
|
2257
|
+
if (minutes >= 30) {
|
|
2258
|
+
try {
|
|
2259
|
+
process.kill(pid, "SIGTERM");
|
|
2260
|
+
console.log(`Swept stale auth process pid=${pid} elapsed=${elapsed}`);
|
|
2261
|
+
} catch (e) {}
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
} catch (e) {
|
|
2265
|
+
// ps/awk not available or no matches — fine.
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2209
2268
|
initCrons();
|
|
2210
2269
|
console.log("Claude Code Telegram bot running");
|
|
2211
2270
|
console.log(`Workspace: ${WORKSPACE}`);
|