@yusukeshib/pi-babysit 0.3.3 → 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 +301 -68
- 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
|
@@ -36,9 +36,9 @@ import * as fs from "node:fs";
|
|
|
36
36
|
import * as os from "node:os";
|
|
37
37
|
import * as path from "node:path";
|
|
38
38
|
import { fileURLToPath } from "node:url";
|
|
39
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
39
|
+
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
40
40
|
import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
|
|
41
|
-
import { Box, Markdown, Text } from "@earendil-works/pi-tui";
|
|
41
|
+
import { Box, Container, Markdown, Text } from "@earendil-works/pi-tui";
|
|
42
42
|
import { Type } from "typebox";
|
|
43
43
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
44
44
|
import { type AgentConfig, type AgentScope, discoverAgents } from "./agents";
|
|
@@ -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
|
// ---------------------------------------------------------------------------
|
|
@@ -1146,6 +1211,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
1146
1211
|
meta.notified = true;
|
|
1147
1212
|
writeMeta(s.id, meta);
|
|
1148
1213
|
const ok = s.exit_code === 0;
|
|
1214
|
+
const status: DisplayStatus = ok
|
|
1215
|
+
? "success"
|
|
1216
|
+
: s.state === "dead" || s.exit_code == null
|
|
1217
|
+
? "terminated"
|
|
1218
|
+
: "failed";
|
|
1149
1219
|
const output = await inlineOutput(s.id, s);
|
|
1150
1220
|
const runtime = meta.startedAt
|
|
1151
1221
|
? `${Math.round((Date.now() - meta.startedAt) / 1000)}s`
|
|
@@ -1160,9 +1230,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
1160
1230
|
customType: "pi-babysit-process-end",
|
|
1161
1231
|
content:
|
|
1162
1232
|
`${summary}\nCommand: ${meta.command ?? "?"}\nLog: ${logPath(s.id)}${output}` +
|
|
1163
|
-
`\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.`,
|
|
1164
1234
|
display: true,
|
|
1165
|
-
details: {
|
|
1235
|
+
details: {
|
|
1236
|
+
id: s.id,
|
|
1237
|
+
exitCode: s.exit_code,
|
|
1238
|
+
success: ok,
|
|
1239
|
+
status,
|
|
1240
|
+
runtime,
|
|
1241
|
+
logPath: logPath(s.id),
|
|
1242
|
+
},
|
|
1166
1243
|
},
|
|
1167
1244
|
{ triggerTurn: true, deliverAs: "steer" },
|
|
1168
1245
|
);
|
|
@@ -1198,23 +1275,63 @@ export default function (pi: ExtensionAPI) {
|
|
|
1198
1275
|
ctx.ui.setWidget("pi-babysit", lines, { placement: "belowEditor" });
|
|
1199
1276
|
};
|
|
1200
1277
|
|
|
1201
|
-
|
|
1202
|
-
|
|
1278
|
+
type DisplayStatus = "started" | "running" | "idle" | "success" | "failed" | "terminated";
|
|
1279
|
+
const renderStatus = (status: DisplayStatus, theme: Theme, prefix?: string): string => {
|
|
1280
|
+
const labels: Record<
|
|
1281
|
+
DisplayStatus,
|
|
1282
|
+
{ icon: string; text: string; color: "accent" | "warning" | "success" | "error" }
|
|
1283
|
+
> = {
|
|
1284
|
+
started: { icon: "", text: "STARTED", color: "accent" },
|
|
1285
|
+
running: { icon: "", text: "RUNNING", color: "accent" },
|
|
1286
|
+
idle: { icon: "", text: "IDLE", color: "warning" },
|
|
1287
|
+
success: { icon: "", text: "SUCCESS", color: "success" },
|
|
1288
|
+
failed: { icon: "", text: "FAILED", color: "error" },
|
|
1289
|
+
terminated: { icon: "", text: "TERMINATED", color: "error" },
|
|
1290
|
+
};
|
|
1291
|
+
const label = labels[status];
|
|
1292
|
+
const text = prefix ? `${prefix} ${label.text}` : label.text;
|
|
1293
|
+
const decorated = label.icon ? `${label.icon} ${text}` : text;
|
|
1294
|
+
return theme.fg(label.color, theme.bold(decorated));
|
|
1295
|
+
};
|
|
1296
|
+
const outcomeStatus = (outcome: WaitOutcome): DisplayStatus =>
|
|
1297
|
+
outcome.ok
|
|
1298
|
+
? "success"
|
|
1299
|
+
: outcome.status &&
|
|
1300
|
+
(outcome.status.state === "dead" || outcome.status.exit_code == null)
|
|
1301
|
+
? "terminated"
|
|
1302
|
+
: "failed";
|
|
1303
|
+
|
|
1304
|
+
// Render snapshots and subagent answers INLINE in the transcript as formatted
|
|
1305
|
+
// markdown, with a semantic status label that remains readable on any theme.
|
|
1203
1306
|
pi.registerMessageRenderer("pi-babysit-result", (message, _opts, theme) => {
|
|
1204
|
-
const d = (message.details ?? {}) as {
|
|
1307
|
+
const d = (message.details ?? {}) as {
|
|
1308
|
+
title?: string;
|
|
1309
|
+
body?: string;
|
|
1310
|
+
status?: DisplayStatus;
|
|
1311
|
+
};
|
|
1205
1312
|
const body =
|
|
1206
1313
|
d.body ?? (typeof message.content === "string" ? message.content : "");
|
|
1207
|
-
const box = new Box(1, 0, (t) => theme.bg("
|
|
1314
|
+
const box = new Box(1, 0, (t) => theme.bg("toolSuccessBg", t));
|
|
1315
|
+
if (d.status) box.addChild(new Text(renderStatus(d.status, theme), 0, 0));
|
|
1208
1316
|
if (d.title) box.addChild(new Text(theme.fg("accent", d.title), 0, 0));
|
|
1209
1317
|
box.addChild(new Markdown(body, 0, 0, getMarkdownTheme()));
|
|
1210
1318
|
return box;
|
|
1211
1319
|
});
|
|
1212
1320
|
|
|
1213
|
-
// Process-end notification rendering
|
|
1321
|
+
// Process-end notification rendering with a colored lifecycle label. Keep the
|
|
1322
|
+
// box background subtle: coloring a potentially large log excerpt is noisy.
|
|
1214
1323
|
pi.registerMessageRenderer("pi-babysit-process-end", (message, _opts, theme) => {
|
|
1215
1324
|
const content = typeof message.content === "string" ? message.content : "";
|
|
1216
|
-
const
|
|
1217
|
-
|
|
1325
|
+
const d = (message.details ?? {}) as {
|
|
1326
|
+
status?: DisplayStatus;
|
|
1327
|
+
success?: boolean;
|
|
1328
|
+
exitCode?: number | null;
|
|
1329
|
+
};
|
|
1330
|
+
const status =
|
|
1331
|
+
d.status ?? (d.success ? "success" : d.exitCode == null ? "terminated" : "failed");
|
|
1332
|
+
const box = new Box(1, 1, (t) => theme.bg("toolSuccessBg", t));
|
|
1333
|
+
box.addChild(new Text(renderStatus(status, theme, "babysit_run"), 0, 0));
|
|
1334
|
+
box.addChild(new Text(theme.fg("toolOutput", content), 0, 0));
|
|
1218
1335
|
return box;
|
|
1219
1336
|
});
|
|
1220
1337
|
|
|
@@ -1230,7 +1347,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
1230
1347
|
}
|
|
1231
1348
|
// Warn early if the binary is missing so the user isn't surprised only when
|
|
1232
1349
|
// a tool later fails. Tools/commands still enforce it via requireBabysit.
|
|
1233
|
-
if (ctx.hasUI && !(await babysitAvailable()))
|
|
1350
|
+
if (ctx.hasUI && !(await babysitAvailable())) {
|
|
1351
|
+
ctx.ui.notify(babysitPreflightError ?? INSTALL_HINT, "warn");
|
|
1352
|
+
}
|
|
1234
1353
|
if (pollTimer) clearInterval(pollTimer);
|
|
1235
1354
|
pollTimer = setInterval(() => {
|
|
1236
1355
|
// Skip if the previous (async) poll hasn't finished, so slow babysit
|
|
@@ -1252,9 +1371,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
1252
1371
|
pollTimer = undefined;
|
|
1253
1372
|
});
|
|
1254
1373
|
|
|
1255
|
-
// Keep unpredictable command output out of model context. Direct bash is
|
|
1256
|
-
// reserved for tiny scalar observations and tightly bounded log inspection;
|
|
1257
|
-
// everything else belongs in babysit_run.
|
|
1258
1374
|
pi.on("tool_call", async (event) => {
|
|
1259
1375
|
if (event.toolName !== "bash") return;
|
|
1260
1376
|
const command = String((event.input as { command?: unknown }).command ?? "");
|
|
@@ -1270,8 +1386,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1270
1386
|
return {
|
|
1271
1387
|
block: true,
|
|
1272
1388
|
reason:
|
|
1273
|
-
"Use babysit_run for
|
|
1274
|
-
"
|
|
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? }. " +
|
|
1275
1391
|
`Retry as babysit_run({ command: ${JSON.stringify(command)} }).`,
|
|
1276
1392
|
};
|
|
1277
1393
|
});
|
|
@@ -1284,11 +1400,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
1284
1400
|
"Run any shell command in a supervised babysit session. Commands that finish within a short " +
|
|
1285
1401
|
"grace period return completion metadata immediately; longer commands continue in the background " +
|
|
1286
1402
|
"and trigger an automatic notification on exit. Complete output is returned inline only when it is " +
|
|
1287
|
-
"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. " +
|
|
1288
1404
|
"In non-interactive mode (`pi -p`, no UI), process mode blocks until exit because there is no " +
|
|
1289
1405
|
"notification loop. Two modes: (1) `command` — run any shell command, including builds, tests, " +
|
|
1290
1406
|
"dev servers, watchers, and interactive TUIs; you can type into it with babysit_send and read " +
|
|
1291
|
-
"its screen with babysit_check. If
|
|
1407
|
+
"its screen with babysit_check. If a worker disappears during startup without recording an exit, " +
|
|
1292
1408
|
"`retryOnWorkerDeath` can retry one idempotent command once. " +
|
|
1293
1409
|
"(2) `profile: \"subagent\"` + `task` — spawn a pi subagent that works on the task in the " +
|
|
1294
1410
|
"background; poll with babysit_check, steer with babysit_send, block with babysit_wait, " +
|
|
@@ -1297,7 +1413,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1297
1413
|
"Run any shell command with context-safe captured output; quick commands return metadata, longer ones continue in background",
|
|
1298
1414
|
promptGuidelines: [
|
|
1299
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`.",
|
|
1300
|
-
"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.",
|
|
1301
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').",
|
|
1302
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.",
|
|
1303
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 }.",
|
|
@@ -1433,7 +1549,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
1433
1549
|
return {
|
|
1434
1550
|
content: [{ type: "text", text: `${retried ? "Retried once after external worker death.\n" : ""}${outcome.text}` }],
|
|
1435
1551
|
isError: !outcome.ok,
|
|
1436
|
-
details: {
|
|
1552
|
+
details: {
|
|
1553
|
+
id: res.id,
|
|
1554
|
+
kind: "process",
|
|
1555
|
+
command: params.command,
|
|
1556
|
+
logPath: logPath(res.id),
|
|
1557
|
+
retried,
|
|
1558
|
+
status: outcomeStatus(outcome),
|
|
1559
|
+
},
|
|
1437
1560
|
};
|
|
1438
1561
|
}
|
|
1439
1562
|
|
|
@@ -1462,7 +1585,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
1462
1585
|
return {
|
|
1463
1586
|
content: [{ type: "text", text: `${retried ? "Retried once after external worker death.\n" : ""}${outcome.text}` }],
|
|
1464
1587
|
isError: !outcome.ok,
|
|
1465
|
-
details: {
|
|
1588
|
+
details: {
|
|
1589
|
+
id: res.id,
|
|
1590
|
+
kind: "process",
|
|
1591
|
+
command: params.command,
|
|
1592
|
+
logPath: logPath(res.id),
|
|
1593
|
+
retried,
|
|
1594
|
+
status: outcomeStatus(outcome),
|
|
1595
|
+
},
|
|
1466
1596
|
};
|
|
1467
1597
|
}
|
|
1468
1598
|
|
|
@@ -1482,7 +1612,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
1482
1612
|
`Human can watch/take over: /babysit`,
|
|
1483
1613
|
},
|
|
1484
1614
|
],
|
|
1485
|
-
details: {
|
|
1615
|
+
details: {
|
|
1616
|
+
id: res.id,
|
|
1617
|
+
kind: "process",
|
|
1618
|
+
command: params.command,
|
|
1619
|
+
logPath: logPath(res.id),
|
|
1620
|
+
retried,
|
|
1621
|
+
status: "started" satisfies DisplayStatus,
|
|
1622
|
+
},
|
|
1486
1623
|
// Do not return `terminate: true` here. In RPC/subagent hosts that hint
|
|
1487
1624
|
// can shut down the hosting pi worker, whose process-tree cleanup then
|
|
1488
1625
|
// kills the otherwise detached babysit supervisor and closes its PTY
|
|
@@ -1542,8 +1679,49 @@ export default function (pi: ExtensionAPI) {
|
|
|
1542
1679
|
`Human can watch/steer: /babysit (pick ${res.id})`,
|
|
1543
1680
|
},
|
|
1544
1681
|
],
|
|
1545
|
-
details: {
|
|
1682
|
+
details: {
|
|
1683
|
+
id: res.id,
|
|
1684
|
+
kind: "subagent",
|
|
1685
|
+
agent: agent?.name,
|
|
1686
|
+
model: res.model,
|
|
1687
|
+
task: params.task,
|
|
1688
|
+
status: "started" satisfies DisplayStatus,
|
|
1689
|
+
},
|
|
1690
|
+
};
|
|
1691
|
+
},
|
|
1692
|
+
// The result label already includes "babysit_run"; suppress the default
|
|
1693
|
+
// call header so the tool name is not shown twice in adjacent lines.
|
|
1694
|
+
renderCall() {
|
|
1695
|
+
return new Container();
|
|
1696
|
+
},
|
|
1697
|
+
renderResult(result, { isPartial }, theme, context) {
|
|
1698
|
+
const details = (result.details ?? {}) as {
|
|
1699
|
+
kind?: "process" | "subagent";
|
|
1700
|
+
status?: DisplayStatus;
|
|
1546
1701
|
};
|
|
1702
|
+
const content = result.content
|
|
1703
|
+
.filter((item): item is { type: "text"; text: string } => item.type === "text")
|
|
1704
|
+
.map((item) => item.text)
|
|
1705
|
+
.join("\n");
|
|
1706
|
+
// A vanished supervisor must never be presented as success, even if an
|
|
1707
|
+
// older/stale result omitted status details or the host did not preserve
|
|
1708
|
+
// the custom isError field. The textual diagnosis is part of our stable
|
|
1709
|
+
// tool contract, so give it precedence over all fallback classification.
|
|
1710
|
+
const workerDead =
|
|
1711
|
+
content.includes("worker-dead") ||
|
|
1712
|
+
content.includes("babysit supervisor disappeared");
|
|
1713
|
+
const status: DisplayStatus = isPartial
|
|
1714
|
+
? "running"
|
|
1715
|
+
: workerDead
|
|
1716
|
+
? "terminated"
|
|
1717
|
+
: details.status ??
|
|
1718
|
+
(context.isError
|
|
1719
|
+
? "failed"
|
|
1720
|
+
: details.kind === "subagent" || content.includes(NOTIFY_MARKER)
|
|
1721
|
+
? "started"
|
|
1722
|
+
: "success");
|
|
1723
|
+
const label = renderStatus(status, theme, "babysit_run");
|
|
1724
|
+
return new Text(content ? `${label}\n${theme.fg("toolOutput", content)}` : label, 0, 0);
|
|
1547
1725
|
},
|
|
1548
1726
|
});
|
|
1549
1727
|
|
|
@@ -1553,10 +1731,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
1553
1731
|
label: "Babysit: check",
|
|
1554
1732
|
description:
|
|
1555
1733
|
"Inspect babysit session(s). Without an id: lists all sessions (processes + subagents). " +
|
|
1556
|
-
"With an id: a process shows state + recent output
|
|
1557
|
-
"
|
|
1558
|
-
"(
|
|
1559
|
-
"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.",
|
|
1560
1738
|
promptSnippet: "Check status/progress of babysit sessions (processes and subagents)",
|
|
1561
1739
|
parameters: Type.Object({
|
|
1562
1740
|
id: Type.Optional(Type.String({ description: "Session id. Omit to list all sessions." })),
|
|
@@ -1564,7 +1742,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
1564
1742
|
Type.Number({ description: "Subagent: how many recent tool calls to show (default 8, max 50)." }),
|
|
1565
1743
|
),
|
|
1566
1744
|
lines: Type.Optional(
|
|
1567
|
-
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
|
+
}),
|
|
1568
1752
|
),
|
|
1569
1753
|
screen: Type.Optional(
|
|
1570
1754
|
Type.Boolean({
|
|
@@ -1573,7 +1757,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1573
1757
|
}),
|
|
1574
1758
|
),
|
|
1575
1759
|
}),
|
|
1576
|
-
async execute(_id, params) {
|
|
1760
|
+
async execute(_id, params, signal) {
|
|
1577
1761
|
await requireBabysit();
|
|
1578
1762
|
if (!params.id) {
|
|
1579
1763
|
const { sessions, error } = await listSessions();
|
|
@@ -1608,6 +1792,40 @@ export default function (pi: ExtensionAPI) {
|
|
|
1608
1792
|
};
|
|
1609
1793
|
}
|
|
1610
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
|
+
}
|
|
1611
1829
|
|
|
1612
1830
|
// --- process ---
|
|
1613
1831
|
if (meta?.kind !== "subagent") {
|
|
@@ -1626,7 +1844,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
1626
1844
|
const sc = await bs(["screenshot", "-s", params.id, "--trim"]);
|
|
1627
1845
|
parts.push(`--- screen ---\n${clip(sc.stdout.trimEnd()) || "(blank screen)"}`);
|
|
1628
1846
|
} else {
|
|
1629
|
-
const nLines = Math.min(Math.max(1, params.lines ?? 30), 200);
|
|
1630
1847
|
const tail = clip(
|
|
1631
1848
|
(await bs(["log", "-s", params.id, "--tail", String(nLines)])).stdout.trimEnd(),
|
|
1632
1849
|
);
|
|
@@ -1987,7 +2204,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1987
2204
|
description: "Pick a babysit session (↑/↓) to snapshot/inspect",
|
|
1988
2205
|
handler: async (_args, ctx) => {
|
|
1989
2206
|
if (!(await babysitAvailable())) {
|
|
1990
|
-
ctx.ui.notify(INSTALL_HINT, "error");
|
|
2207
|
+
ctx.ui.notify(babysitPreflightError ?? INSTALL_HINT, "error");
|
|
1991
2208
|
return;
|
|
1992
2209
|
}
|
|
1993
2210
|
const sessions = (await listSessions()).sessions.sort((a, b) =>
|
|
@@ -2044,12 +2261,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
2044
2261
|
`${picked.id} ${picked.state}${elapsedSuffix}` +
|
|
2045
2262
|
(picked.exit_code != null ? ` (exit=${picked.exit_code})` : "") +
|
|
2046
2263
|
` ${stats}`;
|
|
2264
|
+
const status: DisplayStatus = prog.errorMsg
|
|
2265
|
+
? "failed"
|
|
2266
|
+
: prog.running || prog.waitingOnProcess
|
|
2267
|
+
? "running"
|
|
2268
|
+
: prog.done
|
|
2269
|
+
? "idle"
|
|
2270
|
+
: picked.exit_code === 0
|
|
2271
|
+
? "success"
|
|
2272
|
+
: "terminated";
|
|
2047
2273
|
if (ctx.hasUI) {
|
|
2048
2274
|
pi.sendMessage({
|
|
2049
2275
|
customType: "pi-babysit-result",
|
|
2050
2276
|
content: title,
|
|
2051
2277
|
display: true,
|
|
2052
|
-
details: { title, body },
|
|
2278
|
+
details: { title, body, status },
|
|
2053
2279
|
});
|
|
2054
2280
|
} else {
|
|
2055
2281
|
ctx.ui.notify(`${title}\n\n${body}`, "info");
|
|
@@ -2078,12 +2304,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
2078
2304
|
(running
|
|
2079
2305
|
? `\n\n_Take over in your own terminal:_ \`${attachCmd(picked.id)}\` _(detach: Ctrl-\\ Ctrl-\\)._ Re-run \`/babysit\` to refresh this snapshot.`
|
|
2080
2306
|
: "");
|
|
2307
|
+
const status: DisplayStatus = running
|
|
2308
|
+
? "running"
|
|
2309
|
+
: picked.exit_code === 0
|
|
2310
|
+
? "success"
|
|
2311
|
+
: picked.state === "dead" || picked.exit_code == null
|
|
2312
|
+
? "terminated"
|
|
2313
|
+
: "failed";
|
|
2081
2314
|
if (ctx.hasUI) {
|
|
2082
2315
|
pi.sendMessage({
|
|
2083
2316
|
customType: "pi-babysit-result",
|
|
2084
2317
|
content: title,
|
|
2085
2318
|
display: true,
|
|
2086
|
-
details: { title, body },
|
|
2319
|
+
details: { title, body, status },
|
|
2087
2320
|
});
|
|
2088
2321
|
} else {
|
|
2089
2322
|
ctx.ui.notify(`${title}\n\n${body}`, "info");
|
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"));
|