oira666_pi-watcher 0.0.5 → 0.0.7
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 +2 -1
- package/package.json +1 -1
- package/src/index.ts +17 -2
- package/src/runtime-args.ts +37 -2
- package/src/runtime-child.mjs +4 -0
- package/src/runtime-protocol.ts +2 -0
- package/src/runtime.ts +3 -0
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A persistent side-agent conversation for your running [Pi](https://pi.dev) agent.
|
|
4
4
|
|
|
5
|
-
**Package:** `oira666_pi-watcher` · **Version:** `0.0.
|
|
5
|
+
**Package:** `oira666_pi-watcher` · **Version:** `0.0.6` · **License:** MIT
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -27,6 +27,7 @@ pi -e ./src/index.ts
|
|
|
27
27
|
| `/watcher` | Create/resume the side agent and show its conversation. **Sends no prompt.** |
|
|
28
28
|
| `/watcher Why is it retrying?` | Open and send exactly that question. |
|
|
29
29
|
| `/watcher model` | Open the side agent's model picker. Does not prompt either agent. |
|
|
30
|
+
| `/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
31
|
|
|
31
32
|
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
33
|
|
package/package.json
CHANGED
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
|
}
|
|
@@ -103,6 +128,8 @@ export interface PiLocation {
|
|
|
103
128
|
packageDir: string;
|
|
104
129
|
/** Module exporting `main` and `DefaultPackageManager` (bundle index, else unbundled index). */
|
|
105
130
|
entry: string;
|
|
131
|
+
/** Executable Pi CLI for extensions that spawn process.argv[1]. */
|
|
132
|
+
cliEntry: string;
|
|
106
133
|
docsDir?: string;
|
|
107
134
|
}
|
|
108
135
|
|
|
@@ -138,8 +165,16 @@ export function locatePi(options: { piPackageDir?: string; argv?: readonly strin
|
|
|
138
165
|
existsSync(f),
|
|
139
166
|
);
|
|
140
167
|
if (!entry) continue;
|
|
168
|
+
const bin = readJson(join(packageDir, "package.json"))?.bin;
|
|
169
|
+
const piBin = typeof bin === "string" ? bin : (bin as Record<string, unknown> | undefined)?.pi;
|
|
170
|
+
const cliEntry = [
|
|
171
|
+
...(typeof piBin === "string" ? [resolve(packageDir, piBin)] : []),
|
|
172
|
+
join(packageDir, "dist", "bundle", "cli.js"),
|
|
173
|
+
join(packageDir, "dist", "cli.js"),
|
|
174
|
+
].find((f) => existsSync(f));
|
|
175
|
+
if (!cliEntry) continue;
|
|
141
176
|
const docsDir = join(packageDir, "docs");
|
|
142
|
-
return { packageDir, entry, docsDir: existsSync(docsDir) ? docsDir : undefined };
|
|
177
|
+
return { packageDir, entry, cliEntry, docsDir: existsSync(docsDir) ? docsDir : undefined };
|
|
143
178
|
}
|
|
144
179
|
throw new Error(
|
|
145
180
|
`Cannot locate the ${PI_PACKAGE_NAME} package (tried: ${candidates.join(", ") || "none"}). Pass piPackageDir or set PI_PACKAGE_DIR.`,
|
package/src/runtime-child.mjs
CHANGED
|
@@ -42,6 +42,10 @@ async function run() {
|
|
|
42
42
|
setTimeout(() => process.exit(0), 3000).unref();
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
+
// Inherited extensions launch subagents using execPath + argv[1]. Expose the real
|
|
46
|
+
// CLI before importing Pi or loading extensions, not this IPC-only bootstrap.
|
|
47
|
+
process.argv[1] = config.piCliEntry;
|
|
48
|
+
|
|
45
49
|
const pi = await import(pathToFileURL(config.piEntry).href);
|
|
46
50
|
const { DefaultPackageManager, main } = pi;
|
|
47
51
|
if (typeof main !== "function") throw new Error(`pi entry ${config.piEntry} does not export main()`);
|
package/src/runtime-protocol.ts
CHANGED
|
@@ -26,6 +26,8 @@ export interface ChildConfig {
|
|
|
26
26
|
version: 1;
|
|
27
27
|
/** Absolute path of the pi module exporting `main` and `DefaultPackageManager` (bundle/index.js). */
|
|
28
28
|
piEntry: string;
|
|
29
|
+
/** Executable Pi CLI exposed as process.argv[1] in the child. */
|
|
30
|
+
piCliEntry: string;
|
|
29
31
|
cwd: string;
|
|
30
32
|
mainSessionId: string;
|
|
31
33
|
mainSessionFile: string;
|
package/src/runtime.ts
CHANGED
|
@@ -403,6 +403,7 @@ interface Resolved {
|
|
|
403
403
|
childEntry: string;
|
|
404
404
|
piPackageDir: string;
|
|
405
405
|
piEntry: string;
|
|
406
|
+
piCliEntry: string;
|
|
406
407
|
piDocsDir?: string;
|
|
407
408
|
nodePath: string;
|
|
408
409
|
baseArgs: string[];
|
|
@@ -787,6 +788,7 @@ export class WatcherRuntime {
|
|
|
787
788
|
childEntry,
|
|
788
789
|
piPackageDir: pi.packageDir,
|
|
789
790
|
piEntry: pi.entry,
|
|
791
|
+
piCliEntry: pi.cliEntry,
|
|
790
792
|
piDocsDir: pi.docsDir,
|
|
791
793
|
nodePath,
|
|
792
794
|
baseArgs: ["--session-dir", sessionDir, "--session-id", sideSessionId, "--extension", bridgePath],
|
|
@@ -804,6 +806,7 @@ export class WatcherRuntime {
|
|
|
804
806
|
const config: ChildConfig = {
|
|
805
807
|
version: 1,
|
|
806
808
|
piEntry: resolved.piEntry,
|
|
809
|
+
piCliEntry: resolved.piCliEntry,
|
|
807
810
|
cwd: resolved.cwd,
|
|
808
811
|
mainSessionId: options.mainSessionId,
|
|
809
812
|
mainSessionFile: resolved.mainSessionFile,
|