@inetafrica/open-claudia 1.14.6 → 1.14.8
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 +11 -0
- package/bot-agent.js +8 -3
- package/bot.js +88 -28
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v1.14.8
|
|
4
|
+
- Fix: `/stop` and the 6h hard-timeout now walk the full descendant
|
|
5
|
+
process tree, not just the Claude CLI's process group. Background
|
|
6
|
+
bashes started with `run_in_background: true` (and other detached
|
|
7
|
+
children) used to survive the kill and keep running indefinitely —
|
|
8
|
+
e.g. a polling `until` loop with a broken grep would burn CPU and
|
|
9
|
+
network for hours after the agent was already dead.
|
|
10
|
+
- Fix: on bot startup, sweep stale `claude login` / `claude setup-token`
|
|
11
|
+
processes older than 30 minutes. These were left over from previous
|
|
12
|
+
bot crashes (blocked on stdin forever) and could hold Keychain locks.
|
|
13
|
+
|
|
3
14
|
## v1.14.0
|
|
4
15
|
- Claude Code auth management from Telegram: `/auth_status`, `/login`, `/setup_token`, `/use_oauth_token`, and `/clear_oauth_token`
|
|
5
16
|
- Claude subprocesses now receive `CLAUDE_CODE_OAUTH_TOKEN` from config/env/vault when available, avoiding launchd/macOS Keychain auth failures
|
package/bot-agent.js
CHANGED
|
@@ -1011,12 +1011,17 @@ async function runClaudeAuthCommand(args, label, opts = {}) {
|
|
|
1011
1011
|
pendingClaudeAuthLabel = null;
|
|
1012
1012
|
const token = opts.captureToken ? extractClaudeToken(output) : null;
|
|
1013
1013
|
if (token && !tokenStored) tokenStored = saveClaudeOAuthToken(token);
|
|
1014
|
-
const
|
|
1014
|
+
const cleaned = redactSensitive(stripTerminalControls(output))
|
|
1015
|
+
.replace(/https?:\/\/github\.com\/vadimdemedes\/ink\S*/gi, "")
|
|
1016
|
+
.trim();
|
|
1017
|
+
const isTtyError = /raw mode is not supported|process\.stdin/i.test(output);
|
|
1015
1018
|
if (tokenStored) {
|
|
1016
1019
|
await send(`${label} finished. OAuth token stored for launchd/non-interactive Claude runs.`);
|
|
1017
1020
|
await sendClaudeAuthStatusSummary("Post-auth check:");
|
|
1018
|
-
} else if (
|
|
1019
|
-
await send(`${label}
|
|
1021
|
+
} else if (isTtyError && opts.captureToken) {
|
|
1022
|
+
await send(`${label} cannot complete from this bot — the Claude CLI needs an interactive terminal for setup-token.\n\nRun this in your Mac terminal:\n claude setup-token\n\nThen paste the token here with:\n /use_oauth_token <token>`);
|
|
1023
|
+
} else if (cleaned) {
|
|
1024
|
+
await send(`${label} finished (exit ${code}).\n\n${cleaned.slice(-2500)}`);
|
|
1020
1025
|
await sendClaudeAuthStatusSummary("Post-auth check:");
|
|
1021
1026
|
} else {
|
|
1022
1027
|
await send(`${label} finished (exit ${code}).`);
|
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 {
|
|
@@ -1073,12 +1108,17 @@ async function runClaudeAuthCommand(args, label, opts = {}) {
|
|
|
1073
1108
|
pendingClaudeAuthLabel = null;
|
|
1074
1109
|
const token = opts.captureToken ? extractClaudeToken(output) : null;
|
|
1075
1110
|
if (token && !tokenStored) tokenStored = saveClaudeOAuthToken(token);
|
|
1076
|
-
const
|
|
1111
|
+
const cleaned = redactSensitive(stripTerminalControls(output))
|
|
1112
|
+
.replace(/https?:\/\/github\.com\/vadimdemedes\/ink\S*/gi, "")
|
|
1113
|
+
.trim();
|
|
1114
|
+
const isTtyError = /raw mode is not supported|process\.stdin/i.test(output);
|
|
1077
1115
|
if (tokenStored) {
|
|
1078
1116
|
await send(`${label} finished. OAuth token stored for launchd/non-interactive Claude runs.`);
|
|
1079
1117
|
await sendClaudeAuthStatusSummary("Post-auth check:");
|
|
1080
|
-
} else if (
|
|
1081
|
-
await send(`${label}
|
|
1118
|
+
} else if (isTtyError && opts.captureToken) {
|
|
1119
|
+
await send(`${label} cannot complete from this bot — the Claude CLI needs an interactive terminal for setup-token.\n\nRun this in your Mac terminal:\n claude setup-token\n\nThen paste the token here with:\n /use_oauth_token <token>`);
|
|
1120
|
+
} else if (cleaned) {
|
|
1121
|
+
await send(`${label} finished (exit ${code}).\n\n${cleaned.slice(-2500)}`);
|
|
1082
1122
|
await sendClaudeAuthStatusSummary("Post-auth check:");
|
|
1083
1123
|
} else {
|
|
1084
1124
|
await send(`${label} finished (exit ${code}).`);
|
|
@@ -1173,17 +1213,13 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
1173
1213
|
const startTime = Date.now();
|
|
1174
1214
|
let longRunningNotified = false;
|
|
1175
1215
|
|
|
1176
|
-
// 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.
|
|
1177
1219
|
const processTimeout = setTimeout(async () => {
|
|
1178
1220
|
if (runningProcess === proc) {
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
setTimeout(() => {
|
|
1182
|
-
try { process.kill(-proc.pid, "SIGKILL"); } catch (e) {}
|
|
1183
|
-
}, 5000);
|
|
1184
|
-
} catch (e) {
|
|
1185
|
-
try { proc.kill("SIGKILL"); } catch (e2) {}
|
|
1186
|
-
}
|
|
1221
|
+
killProcessTree(proc.pid, "SIGTERM");
|
|
1222
|
+
setTimeout(() => killProcessTree(proc.pid, "SIGKILL"), 5000);
|
|
1187
1223
|
await send(`Task timed out after ${MAX_PROCESS_TIMEOUT / 60000} minutes. Stopped.`);
|
|
1188
1224
|
}
|
|
1189
1225
|
}, MAX_PROCESS_TIMEOUT);
|
|
@@ -1692,16 +1728,10 @@ bot.onText(/\/stop/, async (msg) => {
|
|
|
1692
1728
|
if (!isAuthorized(msg)) return;
|
|
1693
1729
|
if (runningProcess) {
|
|
1694
1730
|
const pid = runningProcess.pid;
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
try { runningProcess.kill("SIGTERM"); } catch (e2) {}
|
|
1700
|
-
}
|
|
1701
|
-
// Force kill after 3 seconds if still alive
|
|
1702
|
-
setTimeout(() => {
|
|
1703
|
-
try { process.kill(-pid, "SIGKILL"); } catch (e) {}
|
|
1704
|
-
}, 3000);
|
|
1731
|
+
// Walk the descendant tree so detached children (background bashes,
|
|
1732
|
+
// dev servers) get cleaned up, not just the PGID.
|
|
1733
|
+
killProcessTree(pid, "SIGTERM");
|
|
1734
|
+
setTimeout(() => killProcessTree(pid, "SIGKILL"), 3000);
|
|
1705
1735
|
runningProcess = null;
|
|
1706
1736
|
if (streamInterval) clearTimeout(streamInterval);
|
|
1707
1737
|
messageQueue = [];
|
|
@@ -2201,6 +2231,36 @@ try {
|
|
|
2201
2231
|
console.warn("Setup check skipped:", e.message);
|
|
2202
2232
|
}
|
|
2203
2233
|
|
|
2234
|
+
// Sweep stale `claude login` / `claude setup-token` processes that
|
|
2235
|
+
// survived a previous bot crash. They're blocked on stdin forever and
|
|
2236
|
+
// can hold keychain locks / resources. Anything older than 30 minutes
|
|
2237
|
+
// is assumed to be abandoned.
|
|
2238
|
+
try {
|
|
2239
|
+
const out = execSync(
|
|
2240
|
+
`ps -axo pid,etime,command | awk '/claude (login|setup-token)/ && !/awk/'`,
|
|
2241
|
+
{ encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] },
|
|
2242
|
+
);
|
|
2243
|
+
for (const line of out.split("\n")) {
|
|
2244
|
+
const m = line.trim().match(/^(\d+)\s+(\S+)\s/);
|
|
2245
|
+
if (!m) continue;
|
|
2246
|
+
const pid = Number(m[1]);
|
|
2247
|
+
const elapsed = m[2]; // [DD-]HH:MM:SS or MM:SS
|
|
2248
|
+
const minutes = elapsed.includes("-")
|
|
2249
|
+
? Number(elapsed.split("-")[0]) * 24 * 60 + 60 // any "DD-" form is ≥1 day
|
|
2250
|
+
: elapsed.split(":").length === 3
|
|
2251
|
+
? Number(elapsed.split(":")[0]) * 60 + Number(elapsed.split(":")[1])
|
|
2252
|
+
: Number(elapsed.split(":")[0]);
|
|
2253
|
+
if (minutes >= 30) {
|
|
2254
|
+
try {
|
|
2255
|
+
process.kill(pid, "SIGTERM");
|
|
2256
|
+
console.log(`Swept stale auth process pid=${pid} elapsed=${elapsed}`);
|
|
2257
|
+
} catch (e) {}
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
} catch (e) {
|
|
2261
|
+
// ps/awk not available or no matches — fine.
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2204
2264
|
initCrons();
|
|
2205
2265
|
console.log("Claude Code Telegram bot running");
|
|
2206
2266
|
console.log(`Workspace: ${WORKSPACE}`);
|