@tduyng/pi-shell 0.0.3 → 0.0.4

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.
Files changed (2) hide show
  1. package/README.md +27 -47
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,64 +1,39 @@
1
1
  # @tduyng/pi-shell
2
2
 
3
- Park a long command in the background and get woken when it matters. One `bg` tool covers start, read, kill, and list.
3
+ Background processes with one tool: start, read new bytes, kill, list. The turn moves on, and an exiting process wakes the session instead of being polled.
4
4
 
5
- ## What it costs
6
-
7
- `bg` is 197 tokens a request for the whole lifecycle, where four tools would have cost four schemas.
8
-
9
- The saving is in the wait, not the schema. A test suite, a build, or a dev server run for minutes and then return a log nobody reads whole. This tool holds the bytes and hands the model the tail it asked for, so a five minute build does not become a five minute stall in the transcript.
5
+ A foreground command blocks the turn. `&` plus `sleep` is a guess, and the log has to be read from the top to find one line.
10
6
 
11
7
  ## The tool
12
8
 
13
- One tool with an `action` field. Each task runs in the current working directory with a fresh shell, like pi's own bash. There is no persistent shell session.
14
-
15
- ### start
16
-
17
- Spawns a detached command and returns `task_id` (an 8-character UUID prefix), `pid`, and the absolute log `path`. stdout and stderr stream to `<agentDir>/shell-logs/<taskId>.log`. The task runs silently until you ask about it. An optional `timeout` caps it at 600 seconds, with no timeout by default.
18
-
19
- ### output
20
-
21
- Reads new bytes since the last read, through a byte cursor.
22
-
23
- | `wait` | Behaviour |
24
- | --- | --- |
25
- | `now` | Return whatever is available |
26
- | `next_write` | Park the agent until the first new byte, then wake it |
27
- | `exit` | Park the agent until the process ends, then deliver the verdict and the unread tail |
28
-
29
- A wake read does not advance the cursor, so a later `output` still sees every byte from the stored offset.
30
-
31
- ### kill
32
-
33
- SIGTERM to the process group, polled every 50ms for up to a second, then SIGKILL. Takes a `task_id`, a raw `pid`, or a `port` resolved through `lsof -ti`.
34
-
35
- ### status
36
-
37
- Every task with id, pid, state (running, exited, killed), cursor offset, and the first 80 characters of the command.
38
-
39
- ## Waking only when asked
40
-
41
- Only a task the model parked on, through `wait: "exit"` or `wait: "next_write"`, injects a message when it finishes. A burst of exits coalesces into one message on a byte budget: 8 KB total by default, at least 1 KB per task. The wake carries the exit verdict, the unread tail, and the log path, so the model gets the result without a read-back.
9
+ ```text
10
+ bg({action: "start", command: "bun run dev"})
11
+ bg({action: "output", task_id: "t1", wait: "next_write"})
12
+ bg({action: "kill", port: 3000})
13
+ bg({action: "status"})
14
+ ```
42
15
 
43
- The wake fires from the `onExit` callback through `queueMicrotask` rather than polling. A wake that lands mid-turn goes to the follow-up queue and is delivered at the next `agent_settled`.
16
+ `output` waits three ways: `now` returns what is there, `next_write` returns on the next line written, and `exit` returns when the process ends. `kill` takes a `task_id`, a `pid`, or a `port`, whichever is known. Logs land in `<agent-dir>/shell-logs/`, and the start result names the path.
44
17
 
45
- A task parked on `wait: "exit"` also gets a silence check. Every 30 seconds it reads the log size, and after 10 minutes with no new bytes the agent is woken once with the elapsed time and the log path. It fires again only after output resumes and stops again. No model call runs for the check.
18
+ One tool with an action field instead of four tools, because four tools would cost four schemas in every request.
46
19
 
47
- ## Reading a log without drowning in it
20
+ ## Cursor reads
48
21
 
49
- Large output is cut to a head and a tail, 8 KB total by default, with the exact omitted byte count between them. Each side reads up to 16 KB, split on UTF-8 sequence boundaries so a character is never cut mid-byte. The full log stays on disk at the path every result shows.
22
+ `output` takes a byte offset and returns only new bytes. A build that prints 200,000 lines costs the 40 that matter, and a line already read is never re-billed.
50
23
 
51
- A `tool_result` handler annotates the two cases where an exit code cannot say what happened: exit 1 from `grep`, `rg`, or `find` means no matches, and a command killed by its timeout did not finish cleanly. Each note stops the model retrying a search that already succeeded.
24
+ The task list is rebuilt from session entries after a reload, so a task whose pid is gone is reported as gone.
52
25
 
53
- pi already reports a signal-killed command as a failure: its bash backend returns `128 + signal` and raises when a command ends with neither an exit code nor a signal. `shell-process.test.ts` runs `kill -9` against pi's own bash tool and asserts a nonzero result in that family, so a pi release that reintroduces the silent success fails here.
26
+ ## The exit code pi reports wrong
54
27
 
55
- ## Footer
28
+ A `tool_result` handler patches pi's silent signal-death bug. A child killed by a signal reports `code === null`, which pi reads as success, so a command that was killed looks like a command that passed. The handler reports `128+n` and adds a note for a benign exit.
56
29
 
57
- A dot and the live count while any task runs, for example `2 bg running`. It clears when the last one exits.
30
+ ## Settings
58
31
 
59
- ## Lifecycle
32
+ | Variable | Default | Effect |
33
+ | --- | --- | --- |
34
+ | `SHELL` | unset | `0`, `false`, `no` or `off` disables the extension |
60
35
 
61
- Each task is in-memory and dies with the pi process. The session records the command, log path, and start time, so the model can still read the log after a reload, even though the process handle is gone. Detached grandchildren that inherit the shell's stdout keep writing after the shell exits, and the log stream closes only when both pipes close, since ending it on the first close would drop data the second one still delivers.
36
+ `SHELL` is the variable a login shell sets to its own path, so the switch only fires on the four falsey spellings. The name is a compromise: any other name would break profiles that already set it.
62
37
 
63
38
  ## Install
64
39
 
@@ -68,6 +43,11 @@ pi install npm:@tduyng/pi-shell
68
43
 
69
44
  Local checkout: `pi install /path/to/dotfiles/pi/agent/extensions/pi-shell`
70
45
 
71
- ## Configure
46
+ `kill` by port uses `lsof`, which is on macOS and most Linux images. Kill by id or pid needs nothing.
47
+
48
+ ## Limits
72
49
 
73
- `SHELL`, set to `0`, `false`, `no`, or `off`, disables the extension.
50
+ - A start can set a timeout up to 600 seconds. The default is no timeout.
51
+ - Logs are kept 7 days, with a 512 MB cap across the log directory.
52
+ - The `output` view returns at most 8 KB per call. Use `from_offset` to continue.
53
+ - A wake is sent only when the session is idle, so a process that exits mid-turn shows up at the next settle.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tduyng/pi-shell",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "One bg tool for background processes, woken on exit, with cursor reads that never re-read bytes.",
5
5
  "keywords": [
6
6
  "pi",