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 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. Tested against **Pi 1.0.4**; the child launcher depends on that version's package-manager loading interfaces. Standalone compiled Pi binaries are not supported.
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
- npm run check
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
- Tests use an offline scripted provider; no paid model calls are needed. They 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.
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.6",
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-ai": "*",
43
- "@earendil-works/pi-agent-core": "*",
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-ai": "1.0.4",
49
- "@earendil-works/pi-agent-core": "1.0.4",
50
- "@earendil-works/pi-coding-agent": "1.0.4",
51
- "@earendil-works/pi-tui": "1.0.4",
52
- "@types/node": "^22.19.0",
53
- "jiti": "^2.6.1",
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();
@@ -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
  }