@yusukeshib/pi-babysit 0.2.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/LICENSE +21 -0
- package/README.md +105 -0
- package/agents.ts +124 -0
- package/format-stream.mjs +167 -0
- package/index.ts +1881 -0
- package/package.json +43 -0
- package/self-reap.ts +92 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yusuke Shibata
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# pi-babysit
|
|
2
|
+
|
|
3
|
+
A [pi](https://github.com/earendil-works/pi) extension that runs **anything
|
|
4
|
+
long-lived** under [babysit](https://github.com/yusukeshib/babysit)-supervised
|
|
5
|
+
PTY sessions — one substrate for background processes **and** pi subagents.
|
|
6
|
+
It retires both `@mjakl/pi-processes` (the `process` tool) and the old
|
|
7
|
+
`pi-subagent` extension.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
pi install npm:@yusukeshib/pi-babysit
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Then install the [`babysit`](https://github.com/yusukeshib/babysit) binary
|
|
16
|
+
(the extension does **not** auto-install it):
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
cargo install --git https://github.com/yusukeshib/babysit
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
or grab a prebuilt binary from the
|
|
23
|
+
[releases](https://github.com/yusukeshib/babysit/releases) and put it on your
|
|
24
|
+
`PATH`. If it's missing, every tool and the `/babysit` command fail with these
|
|
25
|
+
instructions.
|
|
26
|
+
|
|
27
|
+
## The model
|
|
28
|
+
|
|
29
|
+
Every session is a babysit worker owning a PTY, recording all output to a log,
|
|
30
|
+
reachable from anywhere (`~/.pi-babysit/<pi-session-id>/`). Two kinds:
|
|
31
|
+
|
|
32
|
+
| kind | started by | completion | on completion |
|
|
33
|
+
| ---- | ---------- | ---------- | ------------- |
|
|
34
|
+
| **process** | `babysit_run { command }` | process **exit** | automatic notification message (`triggerTurn`) — the agent may end its turn after starting and is resumed on exit, same contract as the old `process` tool |
|
|
35
|
+
| **subagent** | `babysit_run { profile: "subagent", task }` | `agent_end` in the RPC event stream (process stays alive) | none — the agent polls `babysit_check` or blocks on `babysit_wait`; the idle session accepts follow-up tasks |
|
|
36
|
+
|
|
37
|
+
The **profile is a tool parameter, not a separate tool set**: domain knowledge
|
|
38
|
+
(RPC bookkeeping, per-task byte offsets, parked-turn detection, PTY-safe
|
|
39
|
+
message delivery, spawn validation) lives in code, while the LLM sees one small
|
|
40
|
+
generic surface.
|
|
41
|
+
|
|
42
|
+
Because sessions are real PTYs, the agent can also **drive interactive
|
|
43
|
+
programs** (installers, wizards, REPLs): type with `babysit_send`
|
|
44
|
+
(text or named keys) and read the rendered screen with
|
|
45
|
+
`babysit_check { screen: true }` — a capability neither retired extension had.
|
|
46
|
+
|
|
47
|
+
## Tools (LLM-callable)
|
|
48
|
+
|
|
49
|
+
| Tool | What it does |
|
|
50
|
+
| ---- | ------------ |
|
|
51
|
+
| `babysit_run` | Start a process (`command`, optional `name`/`pty`/`timeout`/`idleTimeout`) or a subagent (`profile: "subagent"`, `task`, optional `agent`/`model`/`tools`). Non-blocking; returns a session id |
|
|
52
|
+
| `babysit_check` | List all sessions, or inspect one: process → state + log tail (or `screen: true` for TUIs); subagent → live progress (turns, recent tool calls, partial answer) |
|
|
53
|
+
| `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`) |
|
|
54
|
+
| `babysit_wait` | Block until done: process exit (or `expect: "regex"` readiness marker), subagent task completion. Multi-wait: `ids` + `mode: "any"\|"all"` |
|
|
55
|
+
| `babysit_kill` | Terminate a session (suppresses the exit notification) |
|
|
56
|
+
|
|
57
|
+
A `tool_call` hook also blocks bash commands that background themselves
|
|
58
|
+
(`… &`, `nohup`, `setsid`, `disown`) and points the agent at `babysit_run`
|
|
59
|
+
(carried over from pi-processes' `blockBackgroundCommands`).
|
|
60
|
+
|
|
61
|
+
## Commands (human)
|
|
62
|
+
|
|
63
|
+
| Command | What it does |
|
|
64
|
+
| ------- | ------------ |
|
|
65
|
+
| `/babysit` | Arrow-key picker over all sessions. Renders an **inline snapshot** (no tmux): running **process** → current rendered screen + recent output + a copy-paste `babysit attach` take-over hint (detach `Ctrl-\ Ctrl-\`); running **subagent** → read-only progress (RPC stdin stays untouchable); finished → summary. Re-run `/babysit` to refresh |
|
|
66
|
+
|
|
67
|
+
A minimal widget above the editor shows live counts
|
|
68
|
+
(`N processes · M subagents working · K idle`).
|
|
69
|
+
|
|
70
|
+
## How completion detection works
|
|
71
|
+
|
|
72
|
+
- **Process**: a 2.5s poller watches for running→exited transitions and injects
|
|
73
|
+
one `pi.sendMessage(…, { triggerTurn: true, deliverAs: "steer" })` per ended
|
|
74
|
+
session (deduped via `meta/<id>.json`). `babysit_kill` and an exit already
|
|
75
|
+
reported by `babysit_wait` suppress the notification.
|
|
76
|
+
- **Subagent**: `babysit_wait` blocks on `babysit expect '"type":"agent_end"'`.
|
|
77
|
+
An `agent_end` whose last message is a **parked** toolResult — a
|
|
78
|
+
`babysit_run { command }` result carrying the `[notify-on-exit]` marker (or
|
|
79
|
+
the legacy `process` tool) — only means "turn parked awaiting a process-exit
|
|
80
|
+
notification; pi resumes on its own", so the wait continues. Any other
|
|
81
|
+
`agent_end` is real completion. Per-task byte offsets scope check/wait to the
|
|
82
|
+
CURRENT task, which is what makes follow-up tasks work.
|
|
83
|
+
|
|
84
|
+
Subagents load `self-reap.ts`, which exits an idle finished subagent after a
|
|
85
|
+
grace window (`PI_BABYSIT_REAP_AFTER`, default 120s) using the same parked-turn
|
|
86
|
+
rule, so a subagent waiting on a long build is never false-killed.
|
|
87
|
+
|
|
88
|
+
## Environment overrides
|
|
89
|
+
|
|
90
|
+
| Var | Default | Purpose |
|
|
91
|
+
| --- | ------- | ------- |
|
|
92
|
+
| `PI_BABYSIT_DIR` | `~/.pi-babysit` | babysit state root (namespaced per pi session) |
|
|
93
|
+
| `PI_BABYSIT_BIN` | `pi` | agent binary for subagents |
|
|
94
|
+
| `PI_BABYSIT_CLI` | `babysit` | babysit binary |
|
|
95
|
+
| `PI_BABYSIT_VIEW_CMD` | bundled `format-stream.mjs` | live-attach pretty printer for subagent JSONL (`""` disables) |
|
|
96
|
+
| `PI_BABYSIT_REAP_AFTER` | `120s` | idle grace before a finished subagent self-exits (`off`/`none`/`0` disables) |
|
|
97
|
+
|
|
98
|
+
Requires `babysit` and `pi` on `PATH`. The extension does **not** auto-install
|
|
99
|
+
`babysit`: if the binary is missing, every tool and the `/babysit` command fail
|
|
100
|
+
with install instructions (`cargo install --git https://github.com/yusukeshib/babysit`
|
|
101
|
+
or a prebuilt release), and a warning is shown at session start. Point
|
|
102
|
+
`$PI_BABYSIT_CLI` at a custom binary path if needed.
|
|
103
|
+
|
|
104
|
+
(No tmux dependency — `/babysit` renders inline; take over a live process
|
|
105
|
+
manually with the `babysit attach` command it shows.)
|
package/agents.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent discovery. Adapted from pi's official subagent example.
|
|
3
|
+
* Loads markdown agent definitions with YAML frontmatter from
|
|
4
|
+
* ~/.pi/agent/agents/*.md (user, always)
|
|
5
|
+
* <project>/.pi/agents/*.md (project, only with scope project|both)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from "node:fs";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import {
|
|
11
|
+
CONFIG_DIR_NAME,
|
|
12
|
+
getAgentDir,
|
|
13
|
+
parseFrontmatter,
|
|
14
|
+
} from "@earendil-works/pi-coding-agent";
|
|
15
|
+
|
|
16
|
+
export type AgentScope = "user" | "project" | "both";
|
|
17
|
+
|
|
18
|
+
export interface AgentConfig {
|
|
19
|
+
name: string;
|
|
20
|
+
description: string;
|
|
21
|
+
tools?: string[];
|
|
22
|
+
model?: string;
|
|
23
|
+
systemPrompt: string;
|
|
24
|
+
source: "user" | "project";
|
|
25
|
+
filePath: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface AgentDiscoveryResult {
|
|
29
|
+
agents: AgentConfig[];
|
|
30
|
+
projectAgentsDir: string | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function loadAgentsFromDir(
|
|
34
|
+
dir: string,
|
|
35
|
+
source: "user" | "project",
|
|
36
|
+
): AgentConfig[] {
|
|
37
|
+
const agents: AgentConfig[] = [];
|
|
38
|
+
if (!fs.existsSync(dir)) return agents;
|
|
39
|
+
|
|
40
|
+
let entries: fs.Dirent[];
|
|
41
|
+
try {
|
|
42
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
43
|
+
} catch {
|
|
44
|
+
return agents;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (const entry of entries) {
|
|
48
|
+
if (!entry.name.endsWith(".md")) continue;
|
|
49
|
+
if (!entry.isFile() && !entry.isSymbolicLink()) continue;
|
|
50
|
+
|
|
51
|
+
const filePath = path.join(dir, entry.name);
|
|
52
|
+
let content: string;
|
|
53
|
+
try {
|
|
54
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
55
|
+
} catch {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const { frontmatter, body } =
|
|
60
|
+
parseFrontmatter<Record<string, string>>(content);
|
|
61
|
+
if (!frontmatter.name || !frontmatter.description) continue;
|
|
62
|
+
|
|
63
|
+
const tools = frontmatter.tools
|
|
64
|
+
?.split(",")
|
|
65
|
+
.map((t) => t.trim())
|
|
66
|
+
.filter(Boolean);
|
|
67
|
+
|
|
68
|
+
agents.push({
|
|
69
|
+
name: frontmatter.name,
|
|
70
|
+
description: frontmatter.description,
|
|
71
|
+
tools: tools && tools.length > 0 ? tools : undefined,
|
|
72
|
+
model: frontmatter.model,
|
|
73
|
+
systemPrompt: body,
|
|
74
|
+
source,
|
|
75
|
+
filePath,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return agents;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isDirectory(p: string): boolean {
|
|
82
|
+
try {
|
|
83
|
+
return fs.statSync(p).isDirectory();
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function findNearestProjectAgentsDir(cwd: string): string | null {
|
|
90
|
+
let currentDir = cwd;
|
|
91
|
+
while (true) {
|
|
92
|
+
const candidate = path.join(currentDir, CONFIG_DIR_NAME, "agents");
|
|
93
|
+
if (isDirectory(candidate)) return candidate;
|
|
94
|
+
const parentDir = path.dirname(currentDir);
|
|
95
|
+
if (parentDir === currentDir) return null;
|
|
96
|
+
currentDir = parentDir;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function discoverAgents(
|
|
101
|
+
cwd: string,
|
|
102
|
+
scope: AgentScope,
|
|
103
|
+
): AgentDiscoveryResult {
|
|
104
|
+
const userDir = path.join(getAgentDir(), "agents");
|
|
105
|
+
const projectAgentsDir = findNearestProjectAgentsDir(cwd);
|
|
106
|
+
|
|
107
|
+
const userAgents = scope === "project" ? [] : loadAgentsFromDir(userDir, "user");
|
|
108
|
+
const projectAgents =
|
|
109
|
+
scope === "user" || !projectAgentsDir
|
|
110
|
+
? []
|
|
111
|
+
: loadAgentsFromDir(projectAgentsDir, "project");
|
|
112
|
+
|
|
113
|
+
const agentMap = new Map<string, AgentConfig>();
|
|
114
|
+
if (scope === "both") {
|
|
115
|
+
for (const a of userAgents) agentMap.set(a.name, a);
|
|
116
|
+
for (const a of projectAgents) agentMap.set(a.name, a);
|
|
117
|
+
} else if (scope === "user") {
|
|
118
|
+
for (const a of userAgents) agentMap.set(a.name, a);
|
|
119
|
+
} else {
|
|
120
|
+
for (const a of projectAgents) agentMap.set(a.name, a);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { agents: Array.from(agentMap.values()), projectAgentsDir };
|
|
124
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* format-stream.mjs — human-readable view for a `pi --mode json` event stream.
|
|
4
|
+
*
|
|
5
|
+
* babysit records the raw JSONL to its log (machine scrapers + `babysit log`
|
|
6
|
+
* stay untouched); this filter only sits on the interactive attach path via
|
|
7
|
+
* `babysit run --view-cmd`. It reads the JSONL on stdin and prints a colored,
|
|
8
|
+
* incrementally-streamed transcript on stdout so a human attaching the tmux
|
|
9
|
+
* pane sees turns, thinking, tool calls and answers instead of a JSON firehose.
|
|
10
|
+
*/
|
|
11
|
+
import * as readline from "node:readline";
|
|
12
|
+
|
|
13
|
+
const C = {
|
|
14
|
+
reset: "\x1b[0m",
|
|
15
|
+
dim: "\x1b[2m",
|
|
16
|
+
bold: "\x1b[1m",
|
|
17
|
+
gray: "\x1b[90m",
|
|
18
|
+
cyan: "\x1b[36m",
|
|
19
|
+
green: "\x1b[32m",
|
|
20
|
+
yellow: "\x1b[33m",
|
|
21
|
+
red: "\x1b[31m",
|
|
22
|
+
blue: "\x1b[34m",
|
|
23
|
+
};
|
|
24
|
+
// The attach client terminal is in raw mode (OPOST off), so a bare "\n"
|
|
25
|
+
// moves down without returning to column 0 and the transcript stair-steps.
|
|
26
|
+
// Normalize every newline (including ones inside model text) to "\r\n".
|
|
27
|
+
const out = (s) => process.stdout.write(String(s).replace(/\r?\n/g, "\r\n"));
|
|
28
|
+
const trunc = (s, n = 72) => {
|
|
29
|
+
s = String(s ?? "");
|
|
30
|
+
return s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function summarizeTool(name, a = {}) {
|
|
34
|
+
switch (name) {
|
|
35
|
+
case "bash":
|
|
36
|
+
return `$ ${trunc(a.command)}`;
|
|
37
|
+
case "read":
|
|
38
|
+
return `read ${trunc(a.file_path ?? a.path)}`;
|
|
39
|
+
case "write":
|
|
40
|
+
return `write ${trunc(a.file_path ?? a.path)}`;
|
|
41
|
+
case "edit":
|
|
42
|
+
return `edit ${trunc(a.file_path ?? a.path)}`;
|
|
43
|
+
case "grep":
|
|
44
|
+
return `grep /${trunc(a.pattern, 40)}/`;
|
|
45
|
+
case "find":
|
|
46
|
+
return `find ${trunc(a.pattern ?? a.path, 40)}`;
|
|
47
|
+
case "ls":
|
|
48
|
+
return `ls ${trunc(a.path)}`;
|
|
49
|
+
default:
|
|
50
|
+
return trunc(JSON.stringify(a), 48);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Join the text of a given block kind ("text" or "thinking") in a message's
|
|
55
|
+
// content array. message_update re-emits the whole (growing) message, so we
|
|
56
|
+
// compare lengths against what we've already printed to append only the delta.
|
|
57
|
+
function blockText(content, kind) {
|
|
58
|
+
if (!Array.isArray(content)) return "";
|
|
59
|
+
let s = "";
|
|
60
|
+
for (const c of content) {
|
|
61
|
+
if (kind === "text" && c.type === "text" && c.text) s += c.text;
|
|
62
|
+
if (kind === "thinking" && c.type === "thinking") s += c.thinking ?? c.text ?? "";
|
|
63
|
+
}
|
|
64
|
+
return s;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const emitted = new Map(); // id -> {think, text, hThink, hText}
|
|
68
|
+
let turn = 0;
|
|
69
|
+
|
|
70
|
+
// Every visual unit (thinking, answer, tool call, error) is a "block": it
|
|
71
|
+
// starts with a marker glyph on its own line. openBlock() is the single
|
|
72
|
+
// place that emits the line break, so spacing stays consistent no matter
|
|
73
|
+
// the event order — one block per line group, no blank lines in between.
|
|
74
|
+
const openBlock = () => {
|
|
75
|
+
out("\n");
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
function streamMessage(msg, fresh = false) {
|
|
79
|
+
if (!msg || msg.role !== "assistant") return;
|
|
80
|
+
const id = msg.id ?? `t${turn}`;
|
|
81
|
+
if (fresh) emitted.delete(id);
|
|
82
|
+
let st = emitted.get(id);
|
|
83
|
+
if (!st) {
|
|
84
|
+
st = { think: 0, text: 0, hThink: false, hText: false };
|
|
85
|
+
emitted.set(id, st);
|
|
86
|
+
}
|
|
87
|
+
const think = blockText(msg.content, "thinking");
|
|
88
|
+
if (think.length > st.think) {
|
|
89
|
+
if (!st.hThink) {
|
|
90
|
+
openBlock();
|
|
91
|
+
out(`${C.gray}[think] `);
|
|
92
|
+
st.hThink = true;
|
|
93
|
+
}
|
|
94
|
+
out(C.gray + think.slice(st.think) + C.reset);
|
|
95
|
+
st.think = think.length;
|
|
96
|
+
}
|
|
97
|
+
const text = blockText(msg.content, "text");
|
|
98
|
+
if (text.length > st.text) {
|
|
99
|
+
if (!st.hText) {
|
|
100
|
+
openBlock();
|
|
101
|
+
out(`${C.green}[text]${C.reset} `);
|
|
102
|
+
st.hText = true;
|
|
103
|
+
}
|
|
104
|
+
out(text.slice(st.text));
|
|
105
|
+
st.text = text.length;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function usageLine(msg) {
|
|
110
|
+
const u = msg?.usage;
|
|
111
|
+
if (!u) return;
|
|
112
|
+
const tok = u.totalTokens ?? u.total_tokens;
|
|
113
|
+
const cost = u.cost?.total;
|
|
114
|
+
const bits = [];
|
|
115
|
+
if (tok != null) bits.push(`${tok} tok`);
|
|
116
|
+
if (cost != null) bits.push(`$${Number(cost).toFixed(4)}`);
|
|
117
|
+
if (msg.stopReason && msg.stopReason !== "stop") bits.push(msg.stopReason);
|
|
118
|
+
if (bits.length) out(`\n${C.gray}${C.dim}[usage] ${bits.join(" \u00b7 ")}${C.reset}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const rl = readline.createInterface({ input: process.stdin });
|
|
122
|
+
rl.on("line", (line) => {
|
|
123
|
+
line = line.trim();
|
|
124
|
+
if (!line.startsWith("{")) return;
|
|
125
|
+
let ev;
|
|
126
|
+
try {
|
|
127
|
+
ev = JSON.parse(line);
|
|
128
|
+
} catch {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
switch (ev.type) {
|
|
132
|
+
case "turn_start":
|
|
133
|
+
// Turn separators are noise in the attach view; message/tool blocks
|
|
134
|
+
// already delimit the flow. `turn` still feeds streamMessage's id fallback.
|
|
135
|
+
turn++;
|
|
136
|
+
break;
|
|
137
|
+
case "message_start":
|
|
138
|
+
streamMessage(ev.message, true);
|
|
139
|
+
break;
|
|
140
|
+
case "message_update":
|
|
141
|
+
streamMessage(ev.message);
|
|
142
|
+
break;
|
|
143
|
+
case "message_end":
|
|
144
|
+
streamMessage(ev.message);
|
|
145
|
+
usageLine(ev.message);
|
|
146
|
+
break;
|
|
147
|
+
case "tool_execution_start":
|
|
148
|
+
openBlock();
|
|
149
|
+
out(
|
|
150
|
+
`${C.cyan}[tool]${C.reset} ${ev.toolName} ${C.dim}${summarizeTool(ev.toolName, ev.args ?? {})}${C.reset}`,
|
|
151
|
+
);
|
|
152
|
+
break;
|
|
153
|
+
case "tool_execution_end": {
|
|
154
|
+
const err = ev.isError || ev.error;
|
|
155
|
+
if (err)
|
|
156
|
+
out(`\n${C.red}[error] ${trunc(ev.error?.message ?? ev.error ?? "tool error", 80)}${C.reset}`);
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
case "error":
|
|
160
|
+
openBlock();
|
|
161
|
+
out(`${C.red}[error] ${trunc(ev.message ?? ev.error ?? JSON.stringify(ev), 120)}${C.reset}`);
|
|
162
|
+
break;
|
|
163
|
+
default:
|
|
164
|
+
break; // session_start/info/shutdown/tree/user/compact: quietly ignored
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
rl.on("close", () => out("\n"));
|