claude-view 0.42.0 → 0.44.0
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/index.js +52 -23
- package/package.json +2 -2
- package/supervise.js +111 -0
package/index.js
CHANGED
|
@@ -6,6 +6,7 @@ const path = require('path')
|
|
|
6
6
|
const os = require('os')
|
|
7
7
|
const https = require('https')
|
|
8
8
|
const zlib = require('zlib')
|
|
9
|
+
const { classifyExit, superviseServer } = require('./supervise')
|
|
9
10
|
|
|
10
11
|
const VERSION = require('./package.json').version
|
|
11
12
|
const REPO = 'tombelieber/claude-view'
|
|
@@ -213,37 +214,65 @@ async function main() {
|
|
|
213
214
|
env.SIDECAR_DIR = sidecarDir
|
|
214
215
|
}
|
|
215
216
|
|
|
216
|
-
// Run the server
|
|
217
|
+
// Run the server under a bounded crash-supervisor (see ./supervise.js).
|
|
217
218
|
//
|
|
218
|
-
//
|
|
219
|
-
// -
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
|
|
219
|
+
// The server is long-running; an abnormal death — a panic=abort SIGABRT, an
|
|
220
|
+
// OS out-of-memory SIGKILL, or a non-zero exit — used to leave the user with
|
|
221
|
+
// a dead app until they manually relaunched. We now restart it on an abnormal
|
|
222
|
+
// exit, bounded to MAX_RESTARTS within RESTART_WINDOW_MS so a hard crash-loop
|
|
223
|
+
// gives up loudly instead of thrashing.
|
|
224
|
+
//
|
|
225
|
+
// Signal handling (intent unchanged):
|
|
226
|
+
// - SIGINT: the terminal delivers it to the whole process group, so the child
|
|
227
|
+
// already receives it. The wrapper ignores it (so Node doesn't exit first)
|
|
228
|
+
// and treats the child's SIGINT death as a deliberate stop — never restart.
|
|
229
|
+
// - SIGTERM/SIGHUP: forward to whichever child is currently running (it may
|
|
230
|
+
// not get them if they were sent to our PID only); also a deliberate stop.
|
|
231
|
+
const MAX_RESTARTS = 5
|
|
232
|
+
const RESTART_WINDOW_MS = 60_000
|
|
225
233
|
|
|
226
234
|
// Prevent Node from exiting on SIGINT before the child does.
|
|
227
235
|
const ignoreSigint = () => {}
|
|
228
236
|
process.on('SIGINT', ignoreSigint)
|
|
229
237
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
+
const supervisor = superviseServer({
|
|
239
|
+
spawn: () => spawn(binaryPath, process.argv.slice(2), { stdio: 'inherit', env }),
|
|
240
|
+
maxRestarts: MAX_RESTARTS,
|
|
241
|
+
windowMs: RESTART_WINDOW_MS,
|
|
242
|
+
now: () => Date.now(),
|
|
243
|
+
log: (msg) => console.error(msg),
|
|
244
|
+
onExit: ({ code, signal }) => {
|
|
245
|
+
// Terminal disposition: restore default SIGINT behavior, then propagate
|
|
246
|
+
// the child's fate faithfully so the parent shell sees the right status.
|
|
247
|
+
process.removeListener('SIGINT', ignoreSigint)
|
|
248
|
+
|
|
249
|
+
// Reaching here with a crash disposition means the supervisor exhausted
|
|
250
|
+
// its restart budget — point the user at the evidence instead of dying
|
|
251
|
+
// silently.
|
|
252
|
+
if (classifyExit(code, signal) === 'crash') {
|
|
253
|
+
const logDir = path.join(os.homedir(), '.claude-view', 'logs')
|
|
254
|
+
console.error(
|
|
255
|
+
`\nCheck ${path.join(logDir, 'crash-*.log')} for a panic backtrace, ` +
|
|
256
|
+
`or your system log for an out-of-memory (jetsam) kill.`,
|
|
257
|
+
)
|
|
258
|
+
}
|
|
238
259
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
260
|
+
if (signal) {
|
|
261
|
+
// Re-signal ourselves so the parent shell sees 128 + signal number.
|
|
262
|
+
process.kill(process.pid, signal)
|
|
263
|
+
} else {
|
|
264
|
+
process.exit(code ?? 1)
|
|
265
|
+
}
|
|
266
|
+
},
|
|
246
267
|
})
|
|
268
|
+
|
|
269
|
+
// Forward deliberate-stop signals to whichever child is currently running.
|
|
270
|
+
for (const sig of ['SIGTERM', 'SIGHUP']) {
|
|
271
|
+
process.on(sig, () => {
|
|
272
|
+
const child = supervisor.getChild()
|
|
273
|
+
if (child) child.kill(sig)
|
|
274
|
+
})
|
|
275
|
+
}
|
|
247
276
|
}
|
|
248
277
|
|
|
249
278
|
main()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-view",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.0",
|
|
4
4
|
"description": "You have 10 Claude sessions running. What are they doing? Live dashboard for Claude Code — monitor every session, track costs, search history, see sub-agents. One command: npx claude-view",
|
|
5
5
|
"bin": {
|
|
6
6
|
"claude-view": "./index.js"
|
|
@@ -57,5 +57,5 @@
|
|
|
57
57
|
"engines": {
|
|
58
58
|
"node": ">=16"
|
|
59
59
|
},
|
|
60
|
-
"files": ["index.js", "README.md"]
|
|
60
|
+
"files": ["index.js", "supervise.js", "README.md"]
|
|
61
61
|
}
|
package/supervise.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Crash-supervisor decision logic for the npx-cli launcher.
|
|
2
|
+
//
|
|
3
|
+
// The launcher spawns the Rust `claude-view` server as a child. Historically
|
|
4
|
+
// the wrapper relayed the child's exit verbatim — so ANY abnormal death of the
|
|
5
|
+
// server (a panic=abort SIGABRT, an OS OOM-kill SIGKILL, a non-zero exit) left
|
|
6
|
+
// the user staring at a dead app until they manually restarted. This module
|
|
7
|
+
// makes the launcher self-heal: restart the server on an abnormal exit, bounded
|
|
8
|
+
// to `maxRestarts` within a rolling `windowMs` so a hard crash-loop gives up
|
|
9
|
+
// loudly instead of thrashing forever.
|
|
10
|
+
//
|
|
11
|
+
// Pure functions, no side effects — the runtime loop in index.js injects the
|
|
12
|
+
// clock and the crash history so the policy is fully testable.
|
|
13
|
+
|
|
14
|
+
// Signals that mean "someone deliberately asked us to stop" — propagate, never
|
|
15
|
+
// restart. Everything else that kills the process (SIGKILL/SIGABRT/SIGSEGV/...)
|
|
16
|
+
// is treated as a crash worth recovering from.
|
|
17
|
+
const USER_STOP_SIGNALS = new Set(['SIGINT', 'SIGTERM', 'SIGHUP'])
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Classify a child exit into one of: 'normal' | 'user-stop' | 'crash'.
|
|
21
|
+
* @param {number|null} code exit code (null when killed by a signal)
|
|
22
|
+
* @param {string|null} signal terminating signal name (null on normal exit)
|
|
23
|
+
*/
|
|
24
|
+
function classifyExit(code, signal) {
|
|
25
|
+
if (signal) {
|
|
26
|
+
return USER_STOP_SIGNALS.has(signal) ? 'user-stop' : 'crash'
|
|
27
|
+
}
|
|
28
|
+
return code === 0 ? 'normal' : 'crash'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Decide whether to restart, given the recent crash history.
|
|
33
|
+
* Only crashes are restartable, and only while fewer than `maxRestarts`
|
|
34
|
+
* crashes have occurred within the trailing `windowMs`.
|
|
35
|
+
* @returns {boolean}
|
|
36
|
+
*/
|
|
37
|
+
function shouldRestart({ exitClass, crashTimes, now, maxRestarts, windowMs }) {
|
|
38
|
+
if (exitClass !== 'crash') return false
|
|
39
|
+
const recent = crashTimes.filter((t) => now - t < windowMs)
|
|
40
|
+
return recent.length < maxRestarts
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Run a child process under a bounded crash-supervisor.
|
|
45
|
+
*
|
|
46
|
+
* Dependency-injected so the restart loop is testable without real processes:
|
|
47
|
+
* - `spawn()` -> returns a child (an EventEmitter emitting 'exit'/'error')
|
|
48
|
+
* - `now()` -> current epoch ms (injected clock)
|
|
49
|
+
* - `log(msg)` -> visible progress/diagnostic line
|
|
50
|
+
* - `onExit({code, signal})` -> terminal disposition (caller maps to process exit)
|
|
51
|
+
*
|
|
52
|
+
* Restarts only abnormal deaths, at most `maxRestarts` within `windowMs`;
|
|
53
|
+
* beyond that it gives up and propagates the last disposition.
|
|
54
|
+
*
|
|
55
|
+
* @returns {{ getChild: () => any, crashCount: () => number }}
|
|
56
|
+
*/
|
|
57
|
+
function superviseServer({ spawn, maxRestarts, windowMs, now, log, onExit }) {
|
|
58
|
+
const crashTimes = []
|
|
59
|
+
let child = null
|
|
60
|
+
|
|
61
|
+
const launch = () => {
|
|
62
|
+
// Per-launch guard: a failed spawn can emit BOTH 'error' and 'exit'; we
|
|
63
|
+
// must act on exactly one of them.
|
|
64
|
+
let settled = false
|
|
65
|
+
const settle = (disposition, restart) => {
|
|
66
|
+
if (settled) return
|
|
67
|
+
settled = true
|
|
68
|
+
if (restart) {
|
|
69
|
+
launch()
|
|
70
|
+
} else {
|
|
71
|
+
onExit(disposition)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
child = spawn()
|
|
76
|
+
|
|
77
|
+
child.on('error', (err) => {
|
|
78
|
+
// Spawn-level failure (binary missing / not executable) is a permanent
|
|
79
|
+
// install problem, not a runtime crash — surface it, don't retry.
|
|
80
|
+
log(`\nFailed to launch claude-view server: ${err.message}`)
|
|
81
|
+
settle({ code: 1, signal: null }, false)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
child.on('exit', (code, signal) => {
|
|
85
|
+
const exitClass = classifyExit(code, signal)
|
|
86
|
+
if (exitClass === 'crash') {
|
|
87
|
+
const t = now()
|
|
88
|
+
if (shouldRestart({ exitClass, crashTimes, now: t, maxRestarts, windowMs })) {
|
|
89
|
+
crashTimes.push(t)
|
|
90
|
+
const reason = signal ? `signal ${signal}` : `exit code ${code}`
|
|
91
|
+
log(
|
|
92
|
+
`\nclaude-view server died (${reason}) — restarting ` +
|
|
93
|
+
`(${crashTimes.length}/${maxRestarts} within ${Math.round(windowMs / 1000)}s)...`,
|
|
94
|
+
)
|
|
95
|
+
settle(null, true)
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
log(
|
|
99
|
+
`\nclaude-view server crashed ${maxRestarts} times within ` +
|
|
100
|
+
`${Math.round(windowMs / 1000)}s — giving up.`,
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
settle({ code, signal }, false)
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
launch()
|
|
108
|
+
return { getChild: () => child, crashCount: () => crashTimes.length }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = { classifyExit, shouldRestart, superviseServer, USER_STOP_SIGNALS }
|