claude-bulkhead 0.2.2 → 0.3.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/CHANGELOG.md +51 -0
- package/README.es.md +142 -50
- package/README.ja.md +142 -50
- package/README.ko.md +143 -51
- package/README.md +142 -50
- package/README.pt-BR.md +142 -50
- package/README.zh-CN.md +154 -62
- package/hooks/hooks.json +68 -8
- package/package.json +5 -2
- package/src/agent-hook.js +162 -0
- package/src/agent-setup.js +256 -0
- package/src/agents.js +89 -0
- package/src/allow-cli.js +65 -0
- package/src/allow-hint.js +62 -0
- package/src/allow-once.js +152 -0
- package/src/auto-queue.js +93 -0
- package/src/bash-guards.js +36 -0
- package/src/basics.js +46 -27
- package/src/checkout-guard.js +342 -74
- package/src/cli-util.js +13 -1
- package/src/cli.js +40 -4
- package/src/config-check.js +21 -4
- package/src/config-cli.js +75 -10
- package/src/config.js +106 -9
- package/src/dev-servers.js +361 -61
- package/src/disk-guard.js +8 -8
- package/src/doctor-enforcement.js +273 -0
- package/src/doctor.js +19 -9
- package/src/edit-guard.js +196 -0
- package/src/events.js +30 -5
- package/src/explain.js +251 -0
- package/src/git-info.js +55 -0
- package/src/gitargs.js +82 -0
- package/src/hook.js +38 -6
- package/src/kill-extras.js +219 -0
- package/src/kill-guard.js +228 -77
- package/src/kill-owners.js +116 -0
- package/src/log-cli.js +15 -10
- package/src/mcp.js +131 -0
- package/src/memory-guard.js +126 -24
- package/src/memory.js +21 -2
- package/src/notices.js +95 -0
- package/src/orphan-reaper.js +58 -11
- package/src/port-cli.js +78 -3
- package/src/port-guard.js +80 -68
- package/src/port-kills.js +198 -0
- package/src/pressure.js +75 -0
- package/src/processes.js +44 -12
- package/src/scan-report.js +166 -0
- package/src/scan.js +87 -18
- package/src/secrets-guard.js +301 -0
- package/src/self-allow.js +73 -0
- package/src/session-info.js +121 -0
- package/src/session-state.js +176 -0
- package/src/sessions.js +109 -20
- package/src/shell.js +279 -19
- package/src/slot-cli.js +92 -21
- package/src/slots.js +61 -13
- package/src/stable-port.js +61 -0
- package/src/status.js +55 -17
- package/src/statusline.js +45 -16
- package/src/subagent-guard.js +24 -0
- package/src/top.js +36 -11
- package/src/wait-guard.js +96 -32
- package/src/worktree-cli.js +454 -0
- package/src/worktree-copy.js +82 -0
package/src/top.js
CHANGED
|
@@ -5,7 +5,10 @@ const { stateDir } = require('./hook')
|
|
|
5
5
|
const { tailEvents } = require('./events')
|
|
6
6
|
const { limits } = require('./slots')
|
|
7
7
|
const { size, when } = require('./status')
|
|
8
|
-
const { useColor, paint,
|
|
8
|
+
const { useColor, paint, fit, rejectArgs, numberOption } = require('./cli-util')
|
|
9
|
+
const { homeShort, fitPath, stateLabel, needsUserFirst, branchLabel, mcpLine } = require('./session-info')
|
|
10
|
+
|
|
11
|
+
const shortPath = (p, home) => homeShort(p, home) || '(unknown)'
|
|
9
12
|
|
|
10
13
|
const USAGE = `usage: bulkhead top [--interval=SECONDS] [--once]
|
|
11
14
|
|
|
@@ -43,9 +46,11 @@ const cell = (s, width, right = false) => {
|
|
|
43
46
|
// project column shrinks. Widths are fixed except the project and ports columns.
|
|
44
47
|
const COLUMNS = [
|
|
45
48
|
{ key: 'pid', title: 'PID', width: 7 },
|
|
49
|
+
{ key: 'state', title: 'STATE', width: 20 },
|
|
46
50
|
{ key: 'started', title: 'STARTED', width: 12, drop: 2 },
|
|
47
|
-
{ key: 'project', title: 'PROJECT', width:
|
|
48
|
-
{ key: '
|
|
51
|
+
{ key: 'project', title: 'PROJECT', width: 20, flex: true },
|
|
52
|
+
{ key: 'branch', title: 'BRANCH', width: 18, drop: 5 },
|
|
53
|
+
{ key: 'rss', title: 'RSS', width: 7, right: true, drop: 6 },
|
|
49
54
|
{ key: 'bar', title: '', width: 10, drop: 1 },
|
|
50
55
|
{ key: 'procs', title: 'PROCS', width: 5, right: true, drop: 4 },
|
|
51
56
|
{ key: 'ports', title: 'PORTS', width: 6, grow: 22, drop: 3 },
|
|
@@ -54,11 +59,14 @@ const COLUMNS = [
|
|
|
54
59
|
function layout(width) {
|
|
55
60
|
let cols = COLUMNS.slice()
|
|
56
61
|
const total = cs => cs.reduce((n, c) => n + c.width, 0) + (cs.length - 1) * 2
|
|
57
|
-
for (const d of [1, 2, 3, 4]) {
|
|
62
|
+
for (const d of [1, 2, 3, 4, 5, 6]) {
|
|
58
63
|
if (total(cols) <= width) break
|
|
59
64
|
cols = cols.filter(c => c.drop !== d)
|
|
60
65
|
}
|
|
61
66
|
cols = cols.map(c => ({ ...c }))
|
|
67
|
+
// the state text shrinks (needs perm…) before the project column is squeezed out
|
|
68
|
+
const state = cols.find(c => c.key === 'state')
|
|
69
|
+
if (state && total(cols) > width) state.width = Math.max(7, state.width - (total(cols) - width))
|
|
62
70
|
let spare = width - total(cols)
|
|
63
71
|
const ports = cols.find(c => c.key === 'ports')
|
|
64
72
|
if (ports && spare > 0) {
|
|
@@ -112,24 +120,31 @@ function frame(view, { width = 100, height = 40, color = false, home = os.homedi
|
|
|
112
120
|
lines.push('')
|
|
113
121
|
|
|
114
122
|
// sessions
|
|
115
|
-
const sessions = st.sessions
|
|
123
|
+
const sessions = needsUserFirst(st.sessions)
|
|
116
124
|
const cols = layout(width)
|
|
117
|
-
|
|
125
|
+
const needy = sessions.filter(s => s.state === 'permission' || s.state === 'waiting').length
|
|
126
|
+
lines.push(c('bold', fit(`SESSIONS ${sessions.length}`, width)) + (needy && width >= 24 + 12 ? c('yellow', ` ${needy} waiting for you`) : ''))
|
|
118
127
|
lines.push(c('dim', cols.map(col => cell(col.title, col.width, col.right)).join(' ').trimEnd()))
|
|
119
128
|
const maxRss = Math.max(1, ...sessions.map(s => s.rss))
|
|
120
129
|
|
|
121
130
|
// what has to fit below the session rows
|
|
122
131
|
const slotRows = st.slots.length + (st.queue || []).length
|
|
123
132
|
const events = view.events || []
|
|
124
|
-
const
|
|
133
|
+
const dupes = (st.mcp || []).filter(g => g.duplicate)
|
|
134
|
+
const below = 2 + (slotRows ? slotRows + 1 : 1) + 2 + Math.max(1, events.length) + (dupes.length ? dupes.length + 2 : 0)
|
|
125
135
|
const room = Math.max(1, height - lines.length - below)
|
|
126
136
|
const shown = sessions.length > room ? sessions.slice(0, Math.max(0, room - 1)) : sessions
|
|
127
137
|
for (const s of shown) {
|
|
128
138
|
const parts = cols.map(col => {
|
|
129
139
|
switch (col.key) {
|
|
130
140
|
case 'pid': return cell(s.pid, col.width)
|
|
141
|
+
case 'state': return c(s.state === 'permission' ? 'red' : s.state === 'waiting' ? 'yellow' : s.state === 'working' ? 'green' : 'dim', cell(stateLabel(s, now), col.width))
|
|
131
142
|
case 'started': return cell(when(s.start, now), col.width)
|
|
132
|
-
case '
|
|
143
|
+
case 'branch': return cell(branchLabel(s), col.width)
|
|
144
|
+
case 'project': {
|
|
145
|
+
const tag = s.agent && s.agent !== 'claude' ? `[${s.agent}] ` : ''
|
|
146
|
+
return cell(tag + fitPath(shortPath(s.cwd, home), Math.max(4, col.width - tag.length)), col.width)
|
|
147
|
+
}
|
|
133
148
|
case 'rss': return cell(size(s.rss), col.width, true)
|
|
134
149
|
case 'bar': return c('cyan', meter(s.rss / maxRss, col.width))
|
|
135
150
|
case 'procs': return cell(s.children == null ? '-' : s.children, col.width, true)
|
|
@@ -142,6 +157,11 @@ function frame(view, { width = 100, height = 40, color = false, home = os.homedi
|
|
|
142
157
|
if (shown.length < sessions.length) lines.push(c('dim', fit(`... ${sessions.length - shown.length} more`, width)))
|
|
143
158
|
if (!sessions.length) lines.push(c('dim', 'no Claude sessions found'))
|
|
144
159
|
lines.push('')
|
|
160
|
+
if (dupes.length) {
|
|
161
|
+
lines.push(c('bold', 'DUPLICATE MCP SERVERS'))
|
|
162
|
+
for (const g of dupes) lines.push(c('yellow', fit(`${mcpLine(g, size)} sessions ${g.sessions.join(', ')}`, width)))
|
|
163
|
+
lines.push('')
|
|
164
|
+
}
|
|
145
165
|
|
|
146
166
|
// slots and queue
|
|
147
167
|
const queue = st.queue || []
|
|
@@ -296,10 +316,15 @@ function snapshot({ stdout = process.stdout, env = process.env } = {}) {
|
|
|
296
316
|
function main(args, { stdin = process.stdin, stdout = process.stdout, env = process.env } = {}) {
|
|
297
317
|
if (args.includes('--help') || args.includes('-h')) return stdout.write(USAGE + '\n')
|
|
298
318
|
if (rejectArgs('top', args, { flags: ['once'], valued: ['interval'] }, process.stderr, USAGE)) return
|
|
319
|
+
const every = numberOption(args, 'interval', { min: 0.5, inclusive: true })
|
|
320
|
+
if (every.error) {
|
|
321
|
+
process.stderr.write(`bulkhead top: ${every.error}\n`)
|
|
322
|
+
process.exitCode = 1
|
|
323
|
+
return
|
|
324
|
+
}
|
|
299
325
|
if (args.includes('--once') || !stdout.isTTY || !stdin.isTTY) return snapshot({ stdout, env })
|
|
300
|
-
const
|
|
301
|
-
const interval = sec >= 0.5 ? sec * 1000 : defaultInterval()
|
|
326
|
+
const interval = every.value ? every.value * 1000 : defaultInterval()
|
|
302
327
|
return live({ stdin, stdout, env, interval }).then(() => process.exit(0))
|
|
303
328
|
}
|
|
304
329
|
|
|
305
|
-
module.exports = { main, frame, layout, live, snapshot, workerScanner, defaultInterval, ALT_ON, ALT_OFF, SHOW }
|
|
330
|
+
module.exports = { main, frame, layout, live, snapshot, workerScanner, defaultInterval, ALT_ON, ALT_OFF, SHOW, USAGE }
|
package/src/wait-guard.js
CHANGED
|
@@ -52,6 +52,43 @@ function follows(argv) {
|
|
|
52
52
|
return false
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
// Code with its string literals and comments blanked: a sleep inside a quoted file body or a
|
|
56
|
+
// comment is text, not a wait. lang is 'python', 'js' or 'secs' (ruby, perl, php).
|
|
57
|
+
function blankText(code, lang) {
|
|
58
|
+
code = String(code).slice(0, 200000)
|
|
59
|
+
let out = ''
|
|
60
|
+
let i = 0
|
|
61
|
+
const n = code.length
|
|
62
|
+
while (i < n) {
|
|
63
|
+
const c = code[i]
|
|
64
|
+
if ((c === '#' && lang !== 'js') || (c === '/' && code[i + 1] === '/' && lang === 'js')) {
|
|
65
|
+
const nl = code.indexOf('\n', i)
|
|
66
|
+
i = nl < 0 ? n : nl
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
69
|
+
if (c === '/' && code[i + 1] === '*' && lang === 'js') {
|
|
70
|
+
const end = code.indexOf('*/', i + 2)
|
|
71
|
+
i = end < 0 ? n : end + 2
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
74
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
75
|
+
const q = lang === 'python' && code.startsWith(c.repeat(3), i) ? c.repeat(3) : c
|
|
76
|
+
i += q.length
|
|
77
|
+
while (i < n && !code.startsWith(q, i)) {
|
|
78
|
+
if (code[i] === '\\') i++
|
|
79
|
+
else if (q.length === 1 && q !== '`' && code[i] === '\n') break
|
|
80
|
+
i++
|
|
81
|
+
}
|
|
82
|
+
i += q.length
|
|
83
|
+
out += '""'
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
out += c
|
|
87
|
+
i++
|
|
88
|
+
}
|
|
89
|
+
return out
|
|
90
|
+
}
|
|
91
|
+
|
|
55
92
|
// A loop inside an interpreter's code: Infinity for while loops, the count for range(N),
|
|
56
93
|
// N.times or for (i = 0; i < N; ...), and 1 for no loop.
|
|
57
94
|
function codeLoops(code) {
|
|
@@ -63,28 +100,34 @@ function codeLoops(code) {
|
|
|
63
100
|
return best
|
|
64
101
|
}
|
|
65
102
|
|
|
66
|
-
// Worst-case wait in seconds of python -c, node -e, ruby -e, perl -e or php -r code
|
|
67
|
-
// sleep(300)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
103
|
+
// Worst-case wait in seconds of python -c, node -e, ruby -e, perl -e or php -r code, or of a
|
|
104
|
+
// python or node heredoc: time.sleep(300) and asyncio.sleep(300) wait seconds, sleep(500) and
|
|
105
|
+
// setTimeout(f, 500) in JavaScript wait milliseconds. Strings that code hands to a shell
|
|
106
|
+
// (os.system('sleep 300'), execSync("sleep 300")) are shell waits.
|
|
107
|
+
function codeWait(code, lang = 'secs') {
|
|
108
|
+
const text = blankText(code, lang)
|
|
109
|
+
const s = shell.embeddedShell(code).flatMap(sleeps)
|
|
110
|
+
const unit = lang === 'js' ? 1000 : 1
|
|
111
|
+
for (const m of text.matchAll(/\bsleep\s*\(?\s*(\d+(?:\.\d+)?)\s*\)?/g)) s.push(Number(m[1]) / unit)
|
|
112
|
+
for (const m of text.matchAll(/\bsetTimeout\s*\(([^;]{0,300}?),\s*(\d+(?:\.\d+)?)\s*\)/g)) s.push(Number(m[2]) / 1000)
|
|
72
113
|
if (!s.length) return 0
|
|
73
|
-
const loops = codeLoops(
|
|
114
|
+
const loops = codeLoops(text)
|
|
74
115
|
return loops > 1 ? loops * s.reduce((a, b) => Math.max(a, b), 0) : s.reduce((a, b) => a + b, 0)
|
|
75
116
|
}
|
|
76
117
|
|
|
118
|
+
const INTERPRETER_WORD = /\b(?:python[\d.]*|py|pythonw|node|nodejs|deno|bun|ruby|perl|php|lua)\b/
|
|
77
119
|
const INTERPRETER = /^(?:python[\d.]*|py|pythonw|node|deno|bun|ruby|perl|php)$/
|
|
78
|
-
const INTERPRETER_HEREDOC = /(?:^|[\s;&|(])(?:python[\d.]*|py|node|deno|bun|ruby|perl|php)(?:\.exe)?(?:\s+-)?\s*$/
|
|
79
120
|
const CODE_FLAG = { python: /^-c$/, node: /^(?:-e|--eval|-p|--print)$/, ruby: /^-e$/, perl: /^-[Ee]$/, php: /^-r$/ }
|
|
121
|
+
const langOf = b => (/^py/.test(b) ? 'python' : /^(?:node|nodejs|deno|bun)$/.test(b) ? 'js' : 'secs')
|
|
80
122
|
|
|
123
|
+
// { code, lang } of an interpreter command line, or null.
|
|
81
124
|
function codeOf(argv) {
|
|
82
125
|
const b = shell.bin(argv[0])
|
|
83
126
|
if (!INTERPRETER.test(b)) return null
|
|
84
127
|
const kind = /^py/.test(b) ? 'python' : /^(?:node|deno|bun)$/.test(b) ? 'node' : b
|
|
85
|
-
if (/^(?:deno|bun)$/.test(b) && argv[1] === 'eval') return argv[2]
|
|
128
|
+
if (/^(?:deno|bun)$/.test(b) && argv[1] === 'eval') return argv[2] ? { code: argv[2], lang: 'js' } : null
|
|
86
129
|
const i = argv.findIndex((a, k) => k > 0 && CODE_FLAG[kind].test(a))
|
|
87
|
-
return i > 0 ? argv[i + 1]
|
|
130
|
+
return i > 0 && argv[i + 1] ? { code: argv[i + 1], lang: langOf(b) } : null
|
|
88
131
|
}
|
|
89
132
|
|
|
90
133
|
// Inert commands only print or search text: echo "sleep 600", git log -S "sleep 300".
|
|
@@ -106,7 +149,7 @@ function scan(text, mode) {
|
|
|
106
149
|
const detached = s.remote && argv.some(v => v === '-d' || v === '--detach')
|
|
107
150
|
if (INERT.test(shell.bin(argv[0])) || BACKGROUND.test(argv[0]) || detached) { cut.push([s.argv[0].s, c.e]); continue }
|
|
108
151
|
const src = codeOf(argv)
|
|
109
|
-
if (src) code.push(codeWait(src))
|
|
152
|
+
if (src) code.push(codeWait(src.code, src.lang))
|
|
110
153
|
if (follows(argv)) follow = true
|
|
111
154
|
}
|
|
112
155
|
if (!cut.length) return { text, code, follow }
|
|
@@ -160,6 +203,13 @@ function hasLoop(cmd) {
|
|
|
160
203
|
/ForEach-Object|\|\s*%\s*\{/i.test(cmd)
|
|
161
204
|
}
|
|
162
205
|
|
|
206
|
+
// Is the end of this text inside a for, while or until loop that has not reached its done?
|
|
207
|
+
function inLoop(before) {
|
|
208
|
+
const opened = (before.match(/\b(?:for|while|until)\b/g) || []).length
|
|
209
|
+
const closed = (before.match(/\bdone\b/g) || []).length
|
|
210
|
+
return opened > closed
|
|
211
|
+
}
|
|
212
|
+
|
|
163
213
|
const QUOTED = /"[^"]*"|'[^']*'/g
|
|
164
214
|
|
|
165
215
|
const OPENERS = new Set(['do', 'if', 'case'])
|
|
@@ -170,7 +220,9 @@ const CLOSERS = new Set(['done', 'fi', 'esac'])
|
|
|
170
220
|
// backgrounds both. Quotes, loops, groups and subshells are kept whole.
|
|
171
221
|
function topLevelLists(cmd) {
|
|
172
222
|
const out = []
|
|
173
|
-
|
|
223
|
+
// what is open: 'p' parenthesis, 'b' brace, 'w' loop, if or case. A `)` that ends a case pattern
|
|
224
|
+
// (haeon) cmd;;) has no ( to close and must not close the case.
|
|
225
|
+
const open = []
|
|
174
226
|
let start = 0
|
|
175
227
|
let quote = null
|
|
176
228
|
const push = (end, op) => {
|
|
@@ -186,16 +238,19 @@ function topLevelLists(cmd) {
|
|
|
186
238
|
}
|
|
187
239
|
if (c === '\\') { i++; continue }
|
|
188
240
|
if (c === '"' || c === "'") { quote = c; continue }
|
|
189
|
-
if (c === '(' || c === '{') {
|
|
190
|
-
if (c === ')' || c === '}') {
|
|
241
|
+
if (c === '(' || c === '{') { open.push(c === '(' ? 'p' : 'b'); continue }
|
|
242
|
+
if (c === ')' || c === '}') {
|
|
243
|
+
if (open[open.length - 1] === (c === ')' ? 'p' : 'b')) open.pop()
|
|
244
|
+
continue
|
|
245
|
+
}
|
|
191
246
|
if (/[a-z]/i.test(c) && (i === 0 || /[\s;&|]/.test(cmd[i - 1]))) {
|
|
192
247
|
const word = /^[\w.-]+/.exec(cmd.slice(i, i + 64))[0]
|
|
193
|
-
if (OPENERS.has(word))
|
|
194
|
-
else if (CLOSERS.has(word)
|
|
248
|
+
if (OPENERS.has(word)) open.push('w')
|
|
249
|
+
else if (CLOSERS.has(word) && open[open.length - 1] === 'w') open.pop()
|
|
195
250
|
i += word.length - 1
|
|
196
251
|
continue
|
|
197
252
|
}
|
|
198
|
-
if (
|
|
253
|
+
if (open.length) continue
|
|
199
254
|
if (c === ';' || c === '\n') push(i, c)
|
|
200
255
|
else if (c === '&') {
|
|
201
256
|
const prev = cmd[i - 1]
|
|
@@ -221,22 +276,31 @@ function dropBackgrounded(cmd) {
|
|
|
221
276
|
|
|
222
277
|
// Worst-case wait in seconds, capped by the tool timeout. Returns null when within the limit.
|
|
223
278
|
function judge(cmd, timeoutMs, maxSec = 180, mode = 'sh') {
|
|
224
|
-
const
|
|
279
|
+
const raw = String(cmd || '')
|
|
280
|
+
// a heredoc that a shell runs, here or over ssh, is shell text; one that python or node reads is code
|
|
281
|
+
const opened = shell.unwrap(shell.prepare(raw, mode, ['shell', 'remote']), mode, 0, { remote: true })
|
|
225
282
|
const { text, code, follow } = scan(dropBackgrounded(opened), mode)
|
|
226
|
-
// python3 - <<'EOF' ... EOF: the body is the interpreter's code
|
|
227
|
-
|
|
228
|
-
|
|
283
|
+
// python3 - <<'EOF' ... EOF: the body is the interpreter's code, in its own language. Its waits
|
|
284
|
+
// are added to the shell's, not multiplied by a shell loop that ended before it.
|
|
285
|
+
let after = 0
|
|
286
|
+
if (raw.includes('<<') && INTERPRETER_WORD.test(raw)) {
|
|
287
|
+
for (const d of shell.heredocs(raw, (prefix, rest) => shell.consumerOf(prefix, rest, mode).kind === 'interp')) {
|
|
288
|
+
const w = codeWait(d.body, langOf(shell.consumerOf(d.prefix, d.rest, mode).bin))
|
|
289
|
+
if (inLoop(raw.slice(0, d.start))) code.push(w)
|
|
290
|
+
else after += w
|
|
291
|
+
}
|
|
229
292
|
}
|
|
230
293
|
// Quoted text is an argument, not shell code: a `for (` inside node -e "..." is not a loop.
|
|
231
294
|
const bare = text.replace(QUOTED, '_')
|
|
232
295
|
const s = sleeps(bare).concat(code.filter(n => n > 0))
|
|
233
296
|
if (follow) s.push(Infinity)
|
|
234
|
-
if (!s.length) return null
|
|
297
|
+
if (!s.length && !after) return null
|
|
235
298
|
const wrapped = bare.match(/\b(?:g?timeout)\s+(?:-\w+\s+)*(\d+)s?\b/)
|
|
236
299
|
if (wrapped && Number(wrapped[1]) <= maxSec) return null
|
|
237
300
|
const cap = Number(timeoutMs) > 0 ? Number(timeoutMs) / 1000 : DEFAULT_TOOL_TIMEOUT_S
|
|
238
301
|
const loop = hasLoop(bare)
|
|
239
|
-
const
|
|
302
|
+
const inShell = !s.length ? 0 : loop ? loopCount(bare) * s.reduce((a, b) => Math.max(a, b), 0) : s.reduce((a, b) => a + b, 0)
|
|
303
|
+
const worst = inShell + after
|
|
240
304
|
const est = Math.min(worst, cap)
|
|
241
305
|
return est > maxSec ? { est, maxSec, rule: follow && !loop ? 'follow' : loop ? 'poll-loop' : 'long-sleep' } : null
|
|
242
306
|
}
|
|
@@ -250,14 +314,14 @@ function message({ est, maxSec }) {
|
|
|
250
314
|
'For a one-off check, skip the sleep and read the output file now.'
|
|
251
315
|
}
|
|
252
316
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
return blocked('wait-guard', verdict.rule, input, `${message(verdict)}\n${ASK_USER}`)
|
|
260
|
-
})
|
|
317
|
+
function handle(input) {
|
|
318
|
+
const ti = input.tool_input || {}
|
|
319
|
+
if (ti.run_in_background || typeof ti.command !== 'string') return
|
|
320
|
+
const verdict = judge(ti.command, ti.timeout, setting('waitGuard.limitSeconds', 180), shell.modeFor(input))
|
|
321
|
+
if (!verdict) return
|
|
322
|
+
return blocked('wait-guard', verdict.rule, input, `${message(verdict)}\n${ASK_USER}`)
|
|
261
323
|
}
|
|
262
324
|
|
|
263
|
-
|
|
325
|
+
if (require.main === module) runHook('wait-guard', handle)
|
|
326
|
+
|
|
327
|
+
module.exports = { handle, judge, message, topLevelLists, dropBackgrounded }
|