oira666_pi-watcher 0.0.6 → 0.0.8
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/EVERGREEN.md +59 -0
- package/README.md +13 -4
- package/package.json +14 -12
- package/src/index.ts +17 -2
- package/src/runtime-args.ts +26 -1
package/EVERGREEN.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
## Summary
|
|
2
|
+
- Watcher is a persistent, independent conversation for observing and steering a running main agent.
|
|
3
|
+
- Opening, toggling, copying, and picking a model must not prompt or stop either agent.
|
|
4
|
+
- Public touchpoints: extension commands/events/UI, session metadata, tool registration, and Pi RPC.
|
|
5
|
+
- The child wraps exported package-manager implementation methods before extensions load.
|
|
6
|
+
- Main history is read-only input; steering requires human approval and is refused during compaction.
|
|
7
|
+
- Main risks are CLI/resource-loading drift, session identity, lifecycle races, and overlay focus.
|
|
8
|
+
|
|
9
|
+
## Intent
|
|
10
|
+
Give the human a side conversation without interrupting or duplicating the main agent's work.
|
|
11
|
+
One side session belongs to each main session; reopening and restarting preserve its history/model.
|
|
12
|
+
Observe the supplied main JSONL path and bounded live-tool previews, not source or verification work
|
|
13
|
+
unless explicitly requested. This is a prompt policy, not a sandbox. Never change main model defaults
|
|
14
|
+
or its status bar. Closing the panel leaves the side conversation running.
|
|
15
|
+
|
|
16
|
+
## Pi integration (public APIs)
|
|
17
|
+
`src/index.ts` registers `/watcher`, Alt+W, session/agent/tool/compaction handlers, and approved
|
|
18
|
+
`sendUserMessage(..., { deliverAs })`. It reads session ID/file/leaf, idle state, and project trust.
|
|
19
|
+
The overlay uses `ctx.ui.custom`, native dialogs, clipboard helpers, themes, and pi-tui components.
|
|
20
|
+
The child registers `live_main_tools`/`steer_main`, adds prompt sections in `before_agent_start`,
|
|
21
|
+
and persists its main-session link using `appendEntry`/session entries.
|
|
22
|
+
RPC supplies prompts, state/history/models, queue clearing, abort, extension dialogs, and settlement.
|
|
23
|
+
The package manifest exposes `src/index.ts`; credentials/settings/resources follow normal Pi loading.
|
|
24
|
+
|
|
25
|
+
## Internal Pi APIs
|
|
26
|
+
Internal/undocumented coupling is present: `runtime-child.mjs` calls exported CLI `main()` and
|
|
27
|
+
patches `DefaultPackageManager.prototype.resolve` and `resolveExtensionSources`, relying on the
|
|
28
|
+
resolved extension-resource shape (`enabled`, path, source metadata) before imports occur.
|
|
29
|
+
`runtime-args.ts` discovers npm package layouts (`dist/bundle/index.js`, `dist/index.js`, CLI entry).
|
|
30
|
+
There are no direct private Pi module imports or private Pi instance-field accesses in runtime code.
|
|
31
|
+
The `__PI_WATCHER_CHILD__` global and Node IPC protocol belong to Watcher, not Pi.
|
|
32
|
+
Standalone binaries alone cannot supply the importable child runtime.
|
|
33
|
+
|
|
34
|
+
## Weak points
|
|
35
|
+
Resource/CLI changes can break exclusions, inherited flags, or nested subagent launches.
|
|
36
|
+
Session replacement, reload, startup cancellation, approval, and child exit must release locks/listeners.
|
|
37
|
+
RPC message framing, event names, model persistence, and compaction delivery boundaries matter.
|
|
38
|
+
Inherited extensions can start work/processes; exclusion is essential and permissions remain inherited.
|
|
39
|
+
UI tests simulate terminal components; e2e does not drive an actual interactive PTY or clipboard.
|
|
40
|
+
|
|
41
|
+
## Tests
|
|
42
|
+
`npm run test:evergreen` runs `npm test` (latest Pi dependencies, TypeScript check, unit/integration),
|
|
43
|
+
then `test/e2e/watcher.test.ts`. No lint script is configured.
|
|
44
|
+
Unit tests cover bounded live state, prompt identity/policy, CLI filtering, JSONL transport, and UI.
|
|
45
|
+
`runtime-integration.test.ts` uses real RPC children and an offline provider to protect prompt-free
|
|
46
|
+
startup, inheritance/exclusions, nested Pi launches, isolated models, resume, approval refusal,
|
|
47
|
+
session guards, locks, shutdown, and recovery. `extension.test.cjs` exercises command → overlay →
|
|
48
|
+
RPC → approval → main delivery, including compaction refusal and shutdown cancellation.
|
|
49
|
+
E2E launches `$EVERGREEN_PI_BIN` (or `pi` on PATH) with stdin ignored and uses
|
|
50
|
+
`$EVERGREEN_E2E_MODEL` (required) for real main/side model calls. It verifies reading actual main
|
|
51
|
+
history, live-tool access, unchanged main history, and exact side-history restoration after restart.
|
|
52
|
+
It copies configured auth/models into a temporary agent directory; credentials are never committed.
|
|
53
|
+
The child uses the latest modular npm Pi from devDependencies, even with a standalone global host.
|
|
54
|
+
|
|
55
|
+
## Relevant changelog topics
|
|
56
|
+
Watch extension loading/package-manager resources, CLI entry/layout/flags, trust and built-in MCP;
|
|
57
|
+
RPC framing/dialogs/settlement; session JSONL/resume/tools and model persistence; steering/compaction;
|
|
58
|
+
TUI key parsing, overlay focus/disposal, and clipboard support. Pi 1.4.0 includes relevant fixes for
|
|
59
|
+
concurrent dialogs and resumed extension-tool activation. Tested against Pi 1.4.0.
|
package/README.md
CHANGED
|
@@ -10,7 +10,9 @@ A persistent side-agent conversation for your running [Pi](https://pi.dev) agent
|
|
|
10
10
|
pi install npm:oira666_pi-watcher
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
Then restart Pi or run `/reload`. Requires Node.js **22.19+** and the modular Pi installation.
|
|
13
|
+
Then restart Pi or run `/reload`. Requires Node.js **22.19+** and the modular Pi installation. The child launcher uses Pi's package-manager loading interfaces. Standalone compiled Pi binaries are not supported.
|
|
14
|
+
|
|
15
|
+
Tested against Pi 1.4.0.
|
|
14
16
|
|
|
15
17
|
For local development:
|
|
16
18
|
|
|
@@ -27,6 +29,7 @@ pi -e ./src/index.ts
|
|
|
27
29
|
| `/watcher` | Create/resume the side agent and show its conversation. **Sends no prompt.** |
|
|
28
30
|
| `/watcher Why is it retrying?` | Open and send exactly that question. |
|
|
29
31
|
| `/watcher model` | Open the side agent's model picker. Does not prompt either agent. |
|
|
32
|
+
| `/watcher command` | Copy `pi --session <side session file>` to the clipboard so you can open the watcher conversation in another terminal (works from any directory). |
|
|
30
33
|
|
|
31
34
|
These are human-facing TUI controls, not tools exposed to the main agent. They work while the main agent is running, including during compaction.
|
|
32
35
|
|
|
@@ -130,11 +133,17 @@ Closing only the overlay leaves the side agent alive, including any inherited MC
|
|
|
130
133
|
|
|
131
134
|
```sh
|
|
132
135
|
npm ci
|
|
133
|
-
|
|
134
|
-
npm test
|
|
136
|
+
mkdir -p tmp
|
|
137
|
+
TMPDIR="$PWD/tmp" npm test
|
|
135
138
|
npm pack --dry-run --ignore-scripts
|
|
136
139
|
```
|
|
137
140
|
|
|
138
|
-
|
|
141
|
+
`npm test` installs the direct Pi development dependencies at `@latest` with install scripts disabled, then runs `npm run check` and the tests. Tests target only the latest npm Pi release. Before each release, update the tested version above and the Pi peer dependency baseline after all checks pass. No lint command is configured.
|
|
142
|
+
|
|
143
|
+
`npm test` uses an offline scripted provider; no paid model calls are needed. These tests exercise the real RPC child, extension inheritance/exclusion, session resume, models, steering, main-extension integration, transport limits, live-tool cleanup, and overlay keyboard/rendering behavior.
|
|
144
|
+
|
|
145
|
+
For Evergreen, set `EVERGREEN_E2E_MODEL` to a configured `provider/model` and run `npm run test:evergreen`. It runs the suite above plus a real-model e2e test using `EVERGREEN_PI_BIN` (default: `pi` on PATH), with stdin ignored. The e2e host is the global Pi; its Watcher child uses the latest modular npm Pi development dependency. This test makes model calls, requires configured credentials, and checks main-history observation, live-tool access, read-only main history, and side-session restoration. See [EVERGREEN.md](EVERGREEN.md) for compatibility risks and coverage.
|
|
146
|
+
|
|
147
|
+
These are automated tests, not a manual terminal smoke test. README snippets are not separately executed.
|
|
139
148
|
|
|
140
149
|
The implementation is in `src/index.ts` (integration), `src/runtime*` (isolated child and RPC/IPC bridge), `src/live-tools.ts`, `src/config.ts`, and `src/ui*` (overlay).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oira666_pi-watcher",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "A persistent side-agent conversation for observing and steering your running Pi agent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"files": [
|
|
26
26
|
"src/",
|
|
27
27
|
"README.md",
|
|
28
|
+
"EVERGREEN.md",
|
|
28
29
|
"LICENSE",
|
|
29
30
|
"watcher-settings.example.json"
|
|
30
31
|
],
|
|
@@ -35,22 +36,23 @@
|
|
|
35
36
|
},
|
|
36
37
|
"scripts": {
|
|
37
38
|
"check": "tsc --noEmit",
|
|
39
|
+
"pretest": "npm install @earendil-works/pi-ai@latest @earendil-works/pi-agent-core@latest @earendil-works/pi-coding-agent@latest @earendil-works/pi-tui@latest --save-dev --ignore-scripts && npm run check",
|
|
38
40
|
"test": "node --test test/*.test.ts test/*.test.cjs",
|
|
41
|
+
"test:evergreen": "node test/evergreen.mjs",
|
|
39
42
|
"prepack": "npm run check && npm test"
|
|
40
43
|
},
|
|
41
44
|
"peerDependencies": {
|
|
42
|
-
"@earendil-works/pi-
|
|
43
|
-
"@earendil-works/pi-
|
|
44
|
-
"@earendil-works/pi-coding-agent": "
|
|
45
|
-
"@earendil-works/pi-tui": "
|
|
45
|
+
"@earendil-works/pi-agent-core": ">=1.4.0",
|
|
46
|
+
"@earendil-works/pi-ai": ">=1.4.0",
|
|
47
|
+
"@earendil-works/pi-coding-agent": ">=1.4.0",
|
|
48
|
+
"@earendil-works/pi-tui": ">=1.4.0"
|
|
46
49
|
},
|
|
47
50
|
"devDependencies": {
|
|
48
|
-
"@earendil-works/pi-
|
|
49
|
-
"@earendil-works/pi-
|
|
50
|
-
"@earendil-works/pi-coding-agent": "1.0
|
|
51
|
-
"@earendil-works/pi-tui": "1.0
|
|
52
|
-
"@types/node": "^
|
|
53
|
-
"
|
|
54
|
-
"typescript": "^5.9.3"
|
|
51
|
+
"@earendil-works/pi-agent-core": "^1.4.0",
|
|
52
|
+
"@earendil-works/pi-ai": "^1.4.0",
|
|
53
|
+
"@earendil-works/pi-coding-agent": "^1.4.0",
|
|
54
|
+
"@earendil-works/pi-tui": "^1.4.0",
|
|
55
|
+
"@types/node": "^26.6.5",
|
|
56
|
+
"typescript": "^7.0.2"
|
|
55
57
|
}
|
|
56
58
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { getAgentDir, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import { copyToClipboard, getAgentDir, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { loadSettings } from "./config.ts";
|
|
3
3
|
import { LiveTools } from "./live-tools.ts";
|
|
4
4
|
import {
|
|
5
5
|
isWatcherChildProcess, messagesToTranscript, WatcherRuntime,
|
|
6
6
|
type UiDialogRequest, type UiDialogResponse, type WatcherRuntimeEvent,
|
|
7
7
|
} from "./runtime.ts";
|
|
8
|
+
import { deriveSideSessionId, findSideSessionFile, formatSideSessionCommand } from "./runtime-args.ts";
|
|
8
9
|
import { openWatcherUI, WatcherStore, type WatcherController } from "./ui.ts";
|
|
9
10
|
|
|
10
11
|
interface Side {
|
|
@@ -283,10 +284,24 @@ export default function watcher(pi: ExtensionAPI): void {
|
|
|
283
284
|
},
|
|
284
285
|
});
|
|
285
286
|
|
|
287
|
+
/** Copy `pi --session <side session file>` (opens it from any directory) and echo it dimmed. */
|
|
288
|
+
async function copySessionCommand(ctx: ExtensionContext): Promise<void> {
|
|
289
|
+
try {
|
|
290
|
+
const id = ctx.sessionManager.getSessionId();
|
|
291
|
+
const running = side && side.id === id ? side.runtime.info?.sideSessionFile : undefined;
|
|
292
|
+
const file = running ?? findSideSessionFile(loadSettings(getAgentDir()).sessionDir, deriveSideSessionId(id));
|
|
293
|
+
if (!file) { ctx.ui.notify("Watcher: no side session yet for this session. Open /watcher first.", "warning"); return; }
|
|
294
|
+
const command = formatSideSessionCommand(file);
|
|
295
|
+
await copyToClipboard(command);
|
|
296
|
+
ctx.ui.notify(`command copied: ${command}`, "info");
|
|
297
|
+
} catch (error) { ctx.ui.notify(`Watcher: could not copy command: ${errorText(error)}`, "error"); }
|
|
298
|
+
}
|
|
299
|
+
|
|
286
300
|
pi.registerCommand("watcher", {
|
|
287
|
-
description: "Open the side agent; /watcher <question> to ask; /watcher model to choose its model",
|
|
301
|
+
description: "Open the side agent; /watcher <question> to ask; /watcher model to choose its model; /watcher command to copy a `pi --session` command for another terminal",
|
|
288
302
|
handler: async (args, ctx) => {
|
|
289
303
|
if (ctx.mode !== "tui") { ctx.ui.notify("/watcher is a human-only interactive terminal command.", "warning"); return; }
|
|
304
|
+
if (args.trim() === "command") { await copySessionCommand(ctx); return; }
|
|
290
305
|
try {
|
|
291
306
|
const s = await ensure(ctx);
|
|
292
307
|
const question = args.trim();
|
package/src/runtime-args.ts
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* main-session header verification, pi package discovery.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { closeSync, existsSync, openSync, readFileSync, readSync, realpathSync } from "node:fs";
|
|
6
|
+
import { closeSync, existsSync, openSync, readdirSync, readFileSync, readSync, realpathSync } from "node:fs";
|
|
7
|
+
import { homedir } from "node:os";
|
|
7
8
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
8
9
|
import { BRIDGE_TOOL_NAMES } from "./runtime-protocol.ts";
|
|
9
10
|
|
|
@@ -21,6 +22,30 @@ export function deriveSideSessionId(mainSessionId: string): string {
|
|
|
21
22
|
return `watcher-${cleaned}`.slice(0, 200).replace(/[^A-Za-z0-9]+$/, "");
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
/**
|
|
26
|
+
* One-line shell command that opens the side session in another terminal: `pi --session <file>`.
|
|
27
|
+
* A file path (not an id) opens that exact session from any directory; pi switches to the
|
|
28
|
+
* session's cwd. The home directory is shown as `~` when that stays shell-safe, else single-quoted.
|
|
29
|
+
*/
|
|
30
|
+
export function formatSideSessionCommand(sessionFile: string, home = homedir()): string {
|
|
31
|
+
const safe = /^[A-Za-z0-9_@%+=:,./-]+$/;
|
|
32
|
+
let path = sessionFile;
|
|
33
|
+
if (home && path.startsWith(`${home}/`) && safe.test(path.slice(home.length))) path = `~${path.slice(home.length)}`;
|
|
34
|
+
else if (!safe.test(path)) path = `'${path.replace(/'/g, `'\\''`)}'`;
|
|
35
|
+
return `pi --session ${path}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Existing side session file `<timestamp>_<id>.jsonl` in a flat session dir, newest first. */
|
|
39
|
+
export function findSideSessionFile(sessionDir: string, sideSessionId: string): string | undefined {
|
|
40
|
+
try {
|
|
41
|
+
const suffix = `_${sideSessionId}.jsonl`;
|
|
42
|
+
const hit = readdirSync(sessionDir).filter((f) => f.endsWith(suffix)).sort().pop();
|
|
43
|
+
return hit ? join(sessionDir, hit) : undefined;
|
|
44
|
+
} catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
24
49
|
export function isValidSessionId(id: string): boolean {
|
|
25
50
|
return /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/.test(id);
|
|
26
51
|
}
|