@yusukeshib/pi-babysit 0.3.4 → 0.3.6
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/README.md +32 -34
- package/index.ts +155 -52
- package/package.json +2 -1
- package/search-log.mjs +42 -0
package/README.md
CHANGED
|
@@ -13,8 +13,8 @@ It retires both `@mjakl/pi-processes` (the `process` tool) and the old
|
|
|
13
13
|
pi install npm:@yusukeshib/pi-babysit
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
Then install
|
|
17
|
-
(the extension does **not** auto-install it):
|
|
16
|
+
Then install [`babysit`](https://github.com/yusukeshib/babysit) **0.13.0 or
|
|
17
|
+
newer** (the extension does **not** auto-install it):
|
|
18
18
|
|
|
19
19
|
```sh
|
|
20
20
|
cargo install --git https://github.com/yusukeshib/babysit
|
|
@@ -22,8 +22,8 @@ cargo install --git https://github.com/yusukeshib/babysit
|
|
|
22
22
|
|
|
23
23
|
or grab a prebuilt binary from the
|
|
24
24
|
[releases](https://github.com/yusukeshib/babysit/releases) and put it on your
|
|
25
|
-
`PATH`. If it
|
|
26
|
-
instructions.
|
|
25
|
+
`PATH`. If it is missing or older than 0.13.0, every tool and the `/babysit`
|
|
26
|
+
command fail with upgrade instructions.
|
|
27
27
|
|
|
28
28
|
## The model
|
|
29
29
|
|
|
@@ -50,14 +50,13 @@ programs** (installers, wizards, REPLs): type with `babysit_send`
|
|
|
50
50
|
| Tool | What it does |
|
|
51
51
|
| ---- | ------------ |
|
|
52
52
|
| `babysit_run` | Run any command (`command`, optional `name`/`pty`/`timeout`/`idleTimeout`/`retryOnWorkerDeath`) or start a subagent (`profile: "subagent"`, `task`, optional `agent`/`model`/`tools`). Quick commands return inline; longer ones continue in the background |
|
|
53
|
-
| `babysit_check` | List all sessions,
|
|
53
|
+
| `babysit_check` | List all sessions, inspect one, tail its bounded recent output, or search its raw log with `pattern`; `screen: true` captures TUIs and subagents otherwise show structured live progress |
|
|
54
54
|
| `babysit_send` | Process: type `text` / press `keys` into the PTY. Subagent: steer mid-run, or send a follow-up task when idle (`mode: auto/steer/task`) |
|
|
55
55
|
| `babysit_wait` | Block until done: process exit (or `expect: "regex"` readiness marker), subagent task completion. Multi-wait: `ids` + `mode: "any"\|"all"` |
|
|
56
56
|
| `babysit_kill` | Terminate a session (suppresses the exit notification) |
|
|
57
57
|
|
|
58
|
-
A `tool_call` hook
|
|
59
|
-
|
|
60
|
-
(carried over from pi-processes' `blockBackgroundCommands`).
|
|
58
|
+
A `tool_call` hook blocks shell backgrounding (`… &`, `nohup`, `setsid`,
|
|
59
|
+
`disown`) and redirects all direct `bash` commands to `babysit_run`.
|
|
61
60
|
|
|
62
61
|
## Commands (human)
|
|
63
62
|
|
|
@@ -73,32 +72,29 @@ A minimal widget above the editor shows live counts
|
|
|
73
72
|
`babysit_run`, `babysit_wait`, and automatic completion notifications always
|
|
74
73
|
return lifecycle metadata and the absolute path to the complete `output.log`.
|
|
75
74
|
When the complete output is at most 8 KB it is returned inline; larger output
|
|
76
|
-
stays out of model context. Inspect
|
|
77
|
-
|
|
75
|
+
stays out of model context. Inspect it through the session id without creating
|
|
76
|
+
another shell session:
|
|
78
77
|
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
```text
|
|
79
|
+
babysit_check { id: "cargo-test", lines: 50 }
|
|
80
|
+
babysit_check { id: "cargo-test", pattern: "FAIL|ERROR", lines: 50 }
|
|
82
81
|
```
|
|
83
82
|
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
Tail and search results are capped at 200 lines and clipped to 8 KB. Pattern
|
|
84
|
+
search returns the latest matching lines with line numbers. Do not read a
|
|
85
|
+
potentially large log file in full.
|
|
86
86
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
are explicitly bounded to at most 100 lines. Broad searches, diffs, API calls,
|
|
90
|
-
multiple commands, redirects, and shell wrappers are redirected to
|
|
91
|
-
`babysit_run`. Set `PI_BABYSIT_ALLOW_BASH=1` only as an emergency escape hatch
|
|
92
|
-
to disable this gate.
|
|
87
|
+
All shell commands, including `pwd` and Git, are redirected to `babysit_run`.
|
|
88
|
+
Set `PI_BABYSIT_ALLOW_BASH=1` only as an explicit emergency escape hatch.
|
|
93
89
|
|
|
94
|
-
##
|
|
90
|
+
## Unexpected worker loss
|
|
95
91
|
|
|
96
|
-
If
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
For commands known to be safe and idempotent,
|
|
100
|
-
retry once with a new session id. It is opt-in
|
|
101
|
-
arbitrary command can duplicate side effects.
|
|
92
|
+
If the babysit supervisor disappears without recording an exit, pi-babysit
|
|
93
|
+
normalizes the stale `running` state to `worker-dead` and returns immediately
|
|
94
|
+
instead of hanging. Possible causes include host process cleanup, endpoint
|
|
95
|
+
security, or a supervisor crash. For commands known to be safe and idempotent,
|
|
96
|
+
set `retryOnWorkerDeath: true` to retry once with a new session id. It is opt-in
|
|
97
|
+
because blindly rerunning an arbitrary command can duplicate side effects.
|
|
102
98
|
|
|
103
99
|
## How completion detection works
|
|
104
100
|
|
|
@@ -127,12 +123,14 @@ rule, so a subagent waiting on a long build is never false-killed.
|
|
|
127
123
|
| `PI_BABYSIT_CLI` | `babysit` | babysit binary |
|
|
128
124
|
| `PI_BABYSIT_VIEW_CMD` | bundled `format-stream.mjs` | live-attach pretty printer for subagent JSONL (`""` disables) |
|
|
129
125
|
| `PI_BABYSIT_REAP_AFTER` | `120s` | idle grace before a finished subagent self-exits (`off`/`none`/`0` disables) |
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
`babysit
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
126
|
+
| `PI_BABYSIT_ALLOW_BASH` | unset | set to `1` to bypass direct-Bash redirection (emergency escape hatch) |
|
|
127
|
+
|
|
128
|
+
Requires `babysit` 0.13.0 or newer and `pi` on `PATH`. The extension does **not**
|
|
129
|
+
auto-install `babysit`: if the binary is missing or too old, every tool and the
|
|
130
|
+
`/babysit` command fail with install instructions (`cargo install --git
|
|
131
|
+
https://github.com/yusukeshib/babysit` or a prebuilt release), and a warning is
|
|
132
|
+
shown at session start. Point `$PI_BABYSIT_CLI` at a custom binary path if
|
|
133
|
+
needed.
|
|
136
134
|
|
|
137
135
|
(No tmux dependency — `/babysit` renders inline; take over a live process
|
|
138
136
|
manually with the `babysit attach` command it shows.)
|
package/index.ts
CHANGED
|
@@ -154,27 +154,49 @@ function bs(
|
|
|
154
154
|
// Every session shells out to `babysit`; without it the extension can do
|
|
155
155
|
// nothing. We don't auto-install (that's the user's job) — we fail loudly with
|
|
156
156
|
// install instructions the moment a tool or command is used.
|
|
157
|
-
const
|
|
158
|
-
`
|
|
159
|
-
`Install it, then retry:\n` +
|
|
157
|
+
const INSTALL_STEPS =
|
|
158
|
+
`Install babysit 0.13.0 or newer, then retry:\n` +
|
|
160
159
|
` cargo install --git https://github.com/yusukeshib/babysit\n` +
|
|
161
160
|
`or download a prebuilt binary from https://github.com/yusukeshib/babysit/releases and put it on your PATH.\n` +
|
|
162
161
|
`(Override the binary path with $PI_BABYSIT_CLI.)`;
|
|
162
|
+
const INSTALL_HINT =
|
|
163
|
+
`The \`babysit\` binary was not found (tried "${BABYSIT_BIN}").\n` + INSTALL_STEPS;
|
|
164
|
+
const MIN_BABYSIT_VERSION = [0, 13, 0] as const;
|
|
165
|
+
|
|
166
|
+
export function isSupportedBabysitVersion(output: string): boolean {
|
|
167
|
+
const match = /\b(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?\b/.exec(output);
|
|
168
|
+
if (!match) return false;
|
|
169
|
+
const actual = [Number(match[1]), Number(match[2]), Number(match[3])] as const;
|
|
170
|
+
for (let i = 0; i < MIN_BABYSIT_VERSION.length; i++) {
|
|
171
|
+
if (actual[i] !== MIN_BABYSIT_VERSION[i]) return actual[i] > MIN_BABYSIT_VERSION[i];
|
|
172
|
+
}
|
|
173
|
+
return match[4] === undefined;
|
|
174
|
+
}
|
|
163
175
|
|
|
164
176
|
// Cached preflight — probe `babysit --version` exactly once per process.
|
|
165
|
-
|
|
177
|
+
// undefined = not probed, null = supported, string = actionable error.
|
|
178
|
+
let babysitPreflightError: string | null | undefined;
|
|
166
179
|
async function babysitAvailable(): Promise<boolean> {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
180
|
+
// Cache only success. A missing or outdated binary may be installed while pi
|
|
181
|
+
// stays open, so subsequent tool calls must be able to recover without a restart.
|
|
182
|
+
if (babysitPreflightError === null) return true;
|
|
183
|
+
const r = await bs(["--version"]);
|
|
184
|
+
if (r.code !== 0) {
|
|
185
|
+
babysitPreflightError = INSTALL_HINT;
|
|
186
|
+
} else if (!isSupportedBabysitVersion(r.stdout)) {
|
|
187
|
+
babysitPreflightError =
|
|
188
|
+
`pi-babysit requires babysit 0.13.0 or newer; found ${r.stdout.trim() || "an unknown version"}.\n` +
|
|
189
|
+
INSTALL_STEPS;
|
|
190
|
+
} else {
|
|
191
|
+
babysitPreflightError = null;
|
|
170
192
|
}
|
|
171
|
-
return
|
|
193
|
+
return babysitPreflightError === null;
|
|
172
194
|
}
|
|
173
195
|
|
|
174
196
|
// Throwing form for tool `execute` handlers: a thrown error marks the tool
|
|
175
|
-
// result isError and reports the
|
|
197
|
+
// result isError and reports the preflight error to the model.
|
|
176
198
|
async function requireBabysit(): Promise<void> {
|
|
177
|
-
if (!(await babysitAvailable())) throw new Error(INSTALL_HINT);
|
|
199
|
+
if (!(await babysitAvailable())) throw new Error(babysitPreflightError ?? INSTALL_HINT);
|
|
178
200
|
}
|
|
179
201
|
|
|
180
202
|
// Error-aware: `babysit list` failing is NOT the same as "no sessions" —
|
|
@@ -381,6 +403,68 @@ function clip(s: string, maxBytes = TAIL_MAX_BYTES): string {
|
|
|
381
403
|
return `${head}\n… [${buf.length - maxBytes} bytes elided] …\n${tail}`;
|
|
382
404
|
}
|
|
383
405
|
|
|
406
|
+
async function searchLog(
|
|
407
|
+
id: string,
|
|
408
|
+
pattern: string,
|
|
409
|
+
maxLines: number,
|
|
410
|
+
signal?: AbortSignal,
|
|
411
|
+
): Promise<{ text: string; error?: string }> {
|
|
412
|
+
const file = logPath(id);
|
|
413
|
+
if (!fs.existsSync(file)) return { text: "", error: `Log file is missing: ${file}` };
|
|
414
|
+
if (signal?.aborted) return { text: "", error: "Log search was interrupted." };
|
|
415
|
+
|
|
416
|
+
// Run regex evaluation out of process so catastrophic backtracking or a huge
|
|
417
|
+
// no-newline log cannot freeze or exhaust pi's main Node process. The helper
|
|
418
|
+
// clips each retained line; this parent also enforces a hard wall-clock limit.
|
|
419
|
+
return new Promise((resolve) => {
|
|
420
|
+
const helper = path.join(EXT_DIR, "search-log.mjs");
|
|
421
|
+
const nodeOptions = [process.env.NODE_OPTIONS, "--max-old-space-size=32"]
|
|
422
|
+
.filter(Boolean)
|
|
423
|
+
.join(" ");
|
|
424
|
+
const child = spawn(process.execPath, [helper, file, pattern, String(maxLines)], {
|
|
425
|
+
env: { ...process.env, NODE_OPTIONS: nodeOptions },
|
|
426
|
+
});
|
|
427
|
+
let stdout = "";
|
|
428
|
+
let stderr = "";
|
|
429
|
+
let finished = false;
|
|
430
|
+
let timedOut = false;
|
|
431
|
+
const finish = (result: { text: string; error?: string }) => {
|
|
432
|
+
if (finished) return;
|
|
433
|
+
finished = true;
|
|
434
|
+
clearTimeout(timer);
|
|
435
|
+
signal?.removeEventListener("abort", onAbort);
|
|
436
|
+
resolve(result);
|
|
437
|
+
};
|
|
438
|
+
const onAbort = () => {
|
|
439
|
+
child.kill("SIGTERM");
|
|
440
|
+
finish({ text: "", error: "Log search was interrupted." });
|
|
441
|
+
};
|
|
442
|
+
const timer = setTimeout(() => {
|
|
443
|
+
timedOut = true;
|
|
444
|
+
child.kill("SIGTERM");
|
|
445
|
+
}, 3_000);
|
|
446
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
447
|
+
child.stdout?.on("data", (data) => {
|
|
448
|
+
stdout += data.toString();
|
|
449
|
+
});
|
|
450
|
+
child.stderr?.on("data", (data) => {
|
|
451
|
+
stderr = clip(stderr + data.toString());
|
|
452
|
+
});
|
|
453
|
+
child.on("error", (error) => {
|
|
454
|
+
finish({ text: "", error: `Could not start log search: ${String(error)}` });
|
|
455
|
+
});
|
|
456
|
+
child.on("close", (code) => {
|
|
457
|
+
if (timedOut) {
|
|
458
|
+
finish({ text: "", error: "Log search timed out after 3s; narrow the pattern or log." });
|
|
459
|
+
} else if (code !== 0) {
|
|
460
|
+
finish({ text: "", error: stderr.trim() || `Log search failed (exit ${code ?? "?"}).` });
|
|
461
|
+
} else {
|
|
462
|
+
finish({ text: clip(stdout.trimEnd()) });
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
384
468
|
async function inlineOutput(id: string, status: BsSession): Promise<string> {
|
|
385
469
|
let bytes = status.output_bytes;
|
|
386
470
|
if (bytes == null) {
|
|
@@ -1061,7 +1145,7 @@ async function waitForExit(
|
|
|
1061
1145
|
: ok ? "completed successfully" : `exited with code ${st.exit_code ?? "?"}`) +
|
|
1062
1146
|
`${expectPattern ? ` before /${expectPattern}/ appeared` : ""}.` +
|
|
1063
1147
|
(workerDead
|
|
1064
|
-
? "
|
|
1148
|
+
? " The supervisor disappeared without recording an exit; possible causes include host process cleanup, endpoint security, or a supervisor crash. The command may have started, so retry only if it is safe and idempotent."
|
|
1065
1149
|
: "") +
|
|
1066
1150
|
`\nLog: ${logPath(id)}` + output,
|
|
1067
1151
|
status: st,
|
|
@@ -1079,7 +1163,7 @@ const waitFor = (
|
|
|
1079
1163
|
: waitForExit(id, limitMs, signal, expectPattern);
|
|
1080
1164
|
|
|
1081
1165
|
// ---------------------------------------------------------------------------
|
|
1082
|
-
// bash
|
|
1166
|
+
// direct bash policy
|
|
1083
1167
|
// ---------------------------------------------------------------------------
|
|
1084
1168
|
|
|
1085
1169
|
// Heuristic (no shell AST): catch `... &` backgrounding (not `&&`), nohup,
|
|
@@ -1092,28 +1176,9 @@ function backgroundsItself(command: string): boolean {
|
|
|
1092
1176
|
return false;
|
|
1093
1177
|
}
|
|
1094
1178
|
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
return Math.max(...matches.map((m) => Number(m[1])));
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
/** Commands allowed to bypass babysit: tiny scalar observations and strictly
|
|
1102
|
-
* bounded reads of captured log files. This is deliberately conservative;
|
|
1103
|
-
* uncertain shell syntax belongs in babysit_run where output is capped. */
|
|
1104
|
-
export function isAllowedDirectBash(command: string): boolean {
|
|
1105
|
-
if (process.env.PI_BABYSIT_ALLOW_BASH === "1") return true;
|
|
1106
|
-
const s = command.trim();
|
|
1107
|
-
if (/^(pwd|git status --short|git branch --show-current)$/.test(s)) return true;
|
|
1108
|
-
if (!s || /[;&`\n\r]|\$\(|>|\b(?:bash|sh|zsh)\s+-c\b/.test(s)) return false;
|
|
1109
|
-
if (!/(?:output\.log|\/(?:tmp|var\/tmp)\/[^\s'\"]*\.log)\b/.test(s)) return false;
|
|
1110
|
-
if (/^wc\s+-(?:l|c)\s+/.test(s) && !s.includes("|")) return true;
|
|
1111
|
-
if (!/^(?:tail|head|rg)\b/.test(s)) return false;
|
|
1112
|
-
const limit = boundedLineCount(s);
|
|
1113
|
-
if (limit == null || limit > 100) return false;
|
|
1114
|
-
// rg must feed a bounded head/tail; direct head/tail is already bounded.
|
|
1115
|
-
if (/^rg\b/.test(s) && !/\|\s*(?:head|tail)\b/.test(s)) return false;
|
|
1116
|
-
return true;
|
|
1179
|
+
/** Emergency escape hatch only. All ordinary shell commands go through babysit_run. */
|
|
1180
|
+
export function isAllowedDirectBash(_command: string): boolean {
|
|
1181
|
+
return process.env.PI_BABYSIT_ALLOW_BASH === "1";
|
|
1117
1182
|
}
|
|
1118
1183
|
|
|
1119
1184
|
// ---------------------------------------------------------------------------
|
|
@@ -1165,7 +1230,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1165
1230
|
customType: "pi-babysit-process-end",
|
|
1166
1231
|
content:
|
|
1167
1232
|
`${summary}\nCommand: ${meta.command ?? "?"}\nLog: ${logPath(s.id)}${output}` +
|
|
1168
|
-
`\n\nThis is the automatic process-end notification. Do not call babysit_check just to re-verify. Inspect the log only when needed
|
|
1233
|
+
`\n\nThis is the automatic process-end notification. Do not call babysit_check just to re-verify. Inspect the log only when needed with babysit_check { id: ${JSON.stringify(s.id)}, lines, pattern? }; never read it in full.`,
|
|
1169
1234
|
display: true,
|
|
1170
1235
|
details: {
|
|
1171
1236
|
id: s.id,
|
|
@@ -1282,7 +1347,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
1282
1347
|
}
|
|
1283
1348
|
// Warn early if the binary is missing so the user isn't surprised only when
|
|
1284
1349
|
// a tool later fails. Tools/commands still enforce it via requireBabysit.
|
|
1285
|
-
if (ctx.hasUI && !(await babysitAvailable()))
|
|
1350
|
+
if (ctx.hasUI && !(await babysitAvailable())) {
|
|
1351
|
+
ctx.ui.notify(babysitPreflightError ?? INSTALL_HINT, "warn");
|
|
1352
|
+
}
|
|
1286
1353
|
if (pollTimer) clearInterval(pollTimer);
|
|
1287
1354
|
pollTimer = setInterval(() => {
|
|
1288
1355
|
// Skip if the previous (async) poll hasn't finished, so slow babysit
|
|
@@ -1304,9 +1371,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
1304
1371
|
pollTimer = undefined;
|
|
1305
1372
|
});
|
|
1306
1373
|
|
|
1307
|
-
// Keep unpredictable command output out of model context. Direct bash is
|
|
1308
|
-
// reserved for tiny scalar observations and tightly bounded log inspection;
|
|
1309
|
-
// everything else belongs in babysit_run.
|
|
1310
1374
|
pi.on("tool_call", async (event) => {
|
|
1311
1375
|
if (event.toolName !== "bash") return;
|
|
1312
1376
|
const command = String((event.input as { command?: unknown }).command ?? "");
|
|
@@ -1322,8 +1386,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1322
1386
|
return {
|
|
1323
1387
|
block: true,
|
|
1324
1388
|
reason:
|
|
1325
|
-
"Use babysit_run for
|
|
1326
|
-
"
|
|
1389
|
+
"Use babysit_run for shell commands so output is supervised and captured outside model context. " +
|
|
1390
|
+
"Inspect an existing session log with babysit_check { id, lines, pattern? }. " +
|
|
1327
1391
|
`Retry as babysit_run({ command: ${JSON.stringify(command)} }).`,
|
|
1328
1392
|
};
|
|
1329
1393
|
});
|
|
@@ -1336,11 +1400,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
1336
1400
|
"Run any shell command in a supervised babysit session. Commands that finish within a short " +
|
|
1337
1401
|
"grace period return completion metadata immediately; longer commands continue in the background " +
|
|
1338
1402
|
"and trigger an automatic notification on exit. Complete output is returned inline only when it is " +
|
|
1339
|
-
"small; larger output stays in the log path for bounded inspection with
|
|
1403
|
+
"small; larger output stays in the log path for bounded inspection with babysit_check. " +
|
|
1340
1404
|
"In non-interactive mode (`pi -p`, no UI), process mode blocks until exit because there is no " +
|
|
1341
1405
|
"notification loop. Two modes: (1) `command` — run any shell command, including builds, tests, " +
|
|
1342
1406
|
"dev servers, watchers, and interactive TUIs; you can type into it with babysit_send and read " +
|
|
1343
|
-
"its screen with babysit_check. If
|
|
1407
|
+
"its screen with babysit_check. If a worker disappears during startup without recording an exit, " +
|
|
1344
1408
|
"`retryOnWorkerDeath` can retry one idempotent command once. " +
|
|
1345
1409
|
"(2) `profile: \"subagent\"` + `task` — spawn a pi subagent that works on the task in the " +
|
|
1346
1410
|
"background; poll with babysit_check, steer with babysit_send, block with babysit_wait, " +
|
|
@@ -1349,7 +1413,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1349
1413
|
"Run any shell command with context-safe captured output; quick commands return metadata, longer ones continue in background",
|
|
1350
1414
|
promptGuidelines: [
|
|
1351
1415
|
"Use babysit_run as the default for shell commands, not only long-running work. Small output is returned directly; large stdout/stderr stays out of model context in the returned log path. Give meaningful commands a clear stable `name`.",
|
|
1352
|
-
"Inspect a babysit log
|
|
1416
|
+
"Inspect a babysit log with babysit_check { id, lines, pattern? }; never read or cat a potentially large log file in full.",
|
|
1353
1417
|
"After babysit_run { command } starts a process, end your response immediately so the automatic process-end notification can resume you; NEVER poll with babysit_check or sleep. Set continueAfterStart: true only when you have immediate, specific, non-polling work to do next. Call babysit_wait when you must consume the result inside the current turn (optionally with `expect` to wait for a readiness line like 'listening on').",
|
|
1354
1418
|
"If a babysit worker is killed externally, babysit_run reports it as worker-dead rather than hanging. Set retryOnWorkerDeath: true only for safe, idempotent commands; it retries at most once and may otherwise duplicate side effects.",
|
|
1355
1419
|
"babysit_run gives full PTY control: drive interactive programs (installers, wizards, REPLs) with babysit_send (text or named keys) and read the rendered screen with babysit_check { screen: true }.",
|
|
@@ -1667,10 +1731,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
1667
1731
|
label: "Babysit: check",
|
|
1668
1732
|
description:
|
|
1669
1733
|
"Inspect babysit session(s). Without an id: lists all sessions (processes + subagents). " +
|
|
1670
|
-
"With an id: a process shows state + recent output
|
|
1671
|
-
"
|
|
1672
|
-
"(
|
|
1673
|
-
"a process to end — the exit notification is automatic.",
|
|
1734
|
+
"With an id: a process shows state + recent output, searches its log with `pattern`, " +
|
|
1735
|
+
"or captures the rendered screen with `screen: true`; a subagent shows live progress " +
|
|
1736
|
+
"(or raw log matches with `pattern`). Results are bounded by `lines` and clipped. " +
|
|
1737
|
+
"Do NOT poll this while merely waiting for a process to end — the exit notification is automatic.",
|
|
1674
1738
|
promptSnippet: "Check status/progress of babysit sessions (processes and subagents)",
|
|
1675
1739
|
parameters: Type.Object({
|
|
1676
1740
|
id: Type.Optional(Type.String({ description: "Session id. Omit to list all sessions." })),
|
|
@@ -1678,7 +1742,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
1678
1742
|
Type.Number({ description: "Subagent: how many recent tool calls to show (default 8, max 50)." }),
|
|
1679
1743
|
),
|
|
1680
1744
|
lines: Type.Optional(
|
|
1681
|
-
Type.Number({ description: "
|
|
1745
|
+
Type.Number({ description: "How many tail lines or latest matches to show (default 30, max 200)." }),
|
|
1746
|
+
),
|
|
1747
|
+
pattern: Type.Optional(
|
|
1748
|
+
Type.String({
|
|
1749
|
+
description:
|
|
1750
|
+
"Search this session's raw log with a regular expression; returns the latest bounded matches.",
|
|
1751
|
+
}),
|
|
1682
1752
|
),
|
|
1683
1753
|
screen: Type.Optional(
|
|
1684
1754
|
Type.Boolean({
|
|
@@ -1687,7 +1757,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1687
1757
|
}),
|
|
1688
1758
|
),
|
|
1689
1759
|
}),
|
|
1690
|
-
async execute(_id, params) {
|
|
1760
|
+
async execute(_id, params, signal) {
|
|
1691
1761
|
await requireBabysit();
|
|
1692
1762
|
if (!params.id) {
|
|
1693
1763
|
const { sessions, error } = await listSessions();
|
|
@@ -1722,6 +1792,40 @@ export default function (pi: ExtensionAPI) {
|
|
|
1722
1792
|
};
|
|
1723
1793
|
}
|
|
1724
1794
|
const meta = readMeta(params.id);
|
|
1795
|
+
const nLines = Math.min(Math.max(1, Math.floor(params.lines ?? 30)), 200);
|
|
1796
|
+
if (params.pattern !== undefined) {
|
|
1797
|
+
if (params.screen) {
|
|
1798
|
+
return {
|
|
1799
|
+
content: [{ type: "text", text: "`pattern` and `screen` are mutually exclusive." }],
|
|
1800
|
+
isError: true,
|
|
1801
|
+
details: {},
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
if (params.pattern.length === 0) {
|
|
1805
|
+
return {
|
|
1806
|
+
content: [{ type: "text", text: "`pattern` must not be empty." }],
|
|
1807
|
+
isError: true,
|
|
1808
|
+
details: {},
|
|
1809
|
+
};
|
|
1810
|
+
}
|
|
1811
|
+
const result = await searchLog(params.id, params.pattern, nLines, signal);
|
|
1812
|
+
if (result.error) {
|
|
1813
|
+
return {
|
|
1814
|
+
content: [{ type: "text", text: result.error }],
|
|
1815
|
+
isError: true,
|
|
1816
|
+
details: {},
|
|
1817
|
+
};
|
|
1818
|
+
}
|
|
1819
|
+
const kind = meta?.kind ?? "process";
|
|
1820
|
+
const header = `[${kind}] state=${st.state}\nlog: ${logPath(params.id)}`;
|
|
1821
|
+
const body = result.text
|
|
1822
|
+
? `--- latest matches /${params.pattern}/ ---\n${result.text}`
|
|
1823
|
+
: `(no output matching /${params.pattern}/)`;
|
|
1824
|
+
return {
|
|
1825
|
+
content: [{ type: "text", text: `${header}\n${body}` }],
|
|
1826
|
+
details: { status: st, kind, logPath: logPath(params.id), pattern: params.pattern },
|
|
1827
|
+
};
|
|
1828
|
+
}
|
|
1725
1829
|
|
|
1726
1830
|
// --- process ---
|
|
1727
1831
|
if (meta?.kind !== "subagent") {
|
|
@@ -1740,7 +1844,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
1740
1844
|
const sc = await bs(["screenshot", "-s", params.id, "--trim"]);
|
|
1741
1845
|
parts.push(`--- screen ---\n${clip(sc.stdout.trimEnd()) || "(blank screen)"}`);
|
|
1742
1846
|
} else {
|
|
1743
|
-
const nLines = Math.min(Math.max(1, params.lines ?? 30), 200);
|
|
1744
1847
|
const tail = clip(
|
|
1745
1848
|
(await bs(["log", "-s", params.id, "--tail", String(nLines)])).stdout.trimEnd(),
|
|
1746
1849
|
);
|
|
@@ -2101,7 +2204,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2101
2204
|
description: "Pick a babysit session (↑/↓) to snapshot/inspect",
|
|
2102
2205
|
handler: async (_args, ctx) => {
|
|
2103
2206
|
if (!(await babysitAvailable())) {
|
|
2104
|
-
ctx.ui.notify(INSTALL_HINT, "error");
|
|
2207
|
+
ctx.ui.notify(babysitPreflightError ?? INSTALL_HINT, "error");
|
|
2105
2208
|
return;
|
|
2106
2209
|
}
|
|
2107
2210
|
const sessions = (await listSessions()).sessions.sort((a, b) =>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yusukeshib/pi-babysit",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"description": "Run any shell command and pi subagents under babysit, with context-safe captured output.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"agents.ts",
|
|
24
24
|
"self-reap.ts",
|
|
25
25
|
"format-stream.mjs",
|
|
26
|
+
"search-log.mjs",
|
|
26
27
|
"README.md",
|
|
27
28
|
"LICENSE"
|
|
28
29
|
],
|
package/search-log.mjs
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
|
|
4
|
+
const [file, source, maxLinesRaw] = process.argv.slice(2);
|
|
5
|
+
const maxLines = Math.min(Math.max(1, Number.parseInt(maxLinesRaw ?? "30", 10) || 30), 200);
|
|
6
|
+
const MAX_LINE_BYTES = 4_000;
|
|
7
|
+
|
|
8
|
+
let pattern;
|
|
9
|
+
try {
|
|
10
|
+
pattern = new RegExp(source);
|
|
11
|
+
} catch (error) {
|
|
12
|
+
console.error(`Invalid pattern: ${String(error)}`);
|
|
13
|
+
process.exit(2);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function clipLine(line) {
|
|
17
|
+
const bytes = Buffer.from(line, "utf8");
|
|
18
|
+
if (bytes.length <= MAX_LINE_BYTES) return line;
|
|
19
|
+
const half = Math.floor(MAX_LINE_BYTES / 2);
|
|
20
|
+
const head = bytes.subarray(0, half).toString("utf8").replace(/\uFFFD+$/, "");
|
|
21
|
+
const tail = bytes.subarray(bytes.length - half).toString("utf8").replace(/^\uFFFD+/, "");
|
|
22
|
+
return `${head}… [line clipped] …${tail}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const matches = [];
|
|
26
|
+
let lineNumber = 0;
|
|
27
|
+
try {
|
|
28
|
+
const input = fs.createReadStream(file, { encoding: "utf8" });
|
|
29
|
+
const lines = createInterface({ input, crlfDelay: Infinity });
|
|
30
|
+
for await (const line of lines) {
|
|
31
|
+
lineNumber++;
|
|
32
|
+
pattern.lastIndex = 0;
|
|
33
|
+
if (!pattern.test(line)) continue;
|
|
34
|
+
matches.push(`${lineNumber}:${clipLine(line)}`);
|
|
35
|
+
if (matches.length > maxLines) matches.shift();
|
|
36
|
+
}
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error(`Could not search log: ${String(error)}`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
process.stdout.write(matches.join("\n"));
|