@overmux/pi 0.0.1

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 (70) hide show
  1. package/README.md +118 -0
  2. package/dist/cli.d.ts +1 -0
  3. package/dist/cli.js +40 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/config-2jNv4tol.d.ts +34 -0
  6. package/dist/config-2jNv4tol.d.ts.map +1 -0
  7. package/dist/config.d.ts +2 -0
  8. package/dist/config.js +47 -0
  9. package/dist/config.js.map +1 -0
  10. package/dist/extension.d.ts +19 -0
  11. package/dist/extension.d.ts.map +1 -0
  12. package/dist/extension.js +265 -0
  13. package/dist/extension.js.map +1 -0
  14. package/dist/index.d.ts +10 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +16 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/jsonl-tail.d.ts +13 -0
  19. package/dist/jsonl-tail.d.ts.map +1 -0
  20. package/dist/jsonl-tail.js +83 -0
  21. package/dist/jsonl-tail.js.map +1 -0
  22. package/dist/live-events-ClhFOMGW.js +497 -0
  23. package/dist/live-events-ClhFOMGW.js.map +1 -0
  24. package/dist/live-events-DAmf6RRx.d.ts +107 -0
  25. package/dist/live-events-DAmf6RRx.d.ts.map +1 -0
  26. package/dist/live-events.d.ts +2 -0
  27. package/dist/live-events.js +2 -0
  28. package/dist/notification-DzOd9cRc.d.ts +20 -0
  29. package/dist/notification-DzOd9cRc.d.ts.map +1 -0
  30. package/dist/notification.d.ts +2 -0
  31. package/dist/notification.js +54 -0
  32. package/dist/notification.js.map +1 -0
  33. package/dist/plugin.d.ts +90 -0
  34. package/dist/plugin.d.ts.map +1 -0
  35. package/dist/plugin.js +213 -0
  36. package/dist/plugin.js.map +1 -0
  37. package/dist/projection.d.ts +101 -0
  38. package/dist/projection.d.ts.map +1 -0
  39. package/dist/projection.js +550 -0
  40. package/dist/projection.js.map +1 -0
  41. package/dist/protocol-CsrnSPOv.d.ts +115 -0
  42. package/dist/protocol-CsrnSPOv.d.ts.map +1 -0
  43. package/dist/protocol.d.ts +2 -0
  44. package/dist/protocol.js +365 -0
  45. package/dist/protocol.js.map +1 -0
  46. package/dist/react.d.ts +139 -0
  47. package/dist/react.d.ts.map +1 -0
  48. package/dist/react.js +796 -0
  49. package/dist/react.js.map +1 -0
  50. package/dist/server.d.ts +5 -0
  51. package/dist/server.js +4 -0
  52. package/dist/session-status-CZLTo8Km.d.ts +71 -0
  53. package/dist/session-status-CZLTo8Km.d.ts.map +1 -0
  54. package/dist/styles.css +619 -0
  55. package/docs/index.md +10 -0
  56. package/package.json +115 -0
  57. package/src/cli.ts +59 -0
  58. package/src/config.ts +87 -0
  59. package/src/extension.ts +486 -0
  60. package/src/index.ts +14 -0
  61. package/src/jsonl-tail.ts +110 -0
  62. package/src/live-events.ts +578 -0
  63. package/src/notification.ts +109 -0
  64. package/src/plugin.ts +369 -0
  65. package/src/projection.ts +995 -0
  66. package/src/protocol.ts +805 -0
  67. package/src/react.tsx +1293 -0
  68. package/src/server.ts +15 -0
  69. package/src/session-status.ts +379 -0
  70. package/src/styles.css +619 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import { loadConfigOrDefault } from \"@richardgill/pi-config\";\n\nimport { OvermuxPiConfigSchema } from \"./config.js\";\nimport { overmuxPi } from \"./extension.js\";\n\nexport * from \"./extension.js\";\n\nconst config = loadConfigOrDefault({\n filename: \"overmux-pi.jsonc\",\n schema: OvermuxPiConfigSchema,\n});\n\nexport default (pi: Parameters<typeof overmuxPi>[0]): void =>\n overmuxPi(pi, config);\n"],"mappings":";;;;;;;AAOA,MAAM,SAAS,oBAAoB;CACjC,UAAU;CACV,QAAQ;AACV,CAAC;AAED,IAAA,eAAgB,OACd,UAAU,IAAI,MAAM"}
@@ -0,0 +1,13 @@
1
+ //#region src/jsonl-tail.d.ts
2
+ type JsonlRead = {
3
+ records: unknown[];
4
+ reset: boolean;
5
+ };
6
+ type JsonlTail = {
7
+ read: (path: string) => Promise<JsonlRead>;
8
+ };
9
+ declare const parseCompleteJsonlBytes: (bytes: Buffer) => unknown[];
10
+ declare const createJsonlTail: () => JsonlTail;
11
+ //#endregion
12
+ export { JsonlRead, JsonlTail, createJsonlTail, parseCompleteJsonlBytes };
13
+ //# sourceMappingURL=jsonl-tail.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jsonl-tail.d.ts","names":[],"sources":["../src/jsonl-tail.ts"],"mappings":";KAIY;EAAc;EAAoB;;KAClC;EAAc,OAAO,iBAAiB,QAAQ;;cAE7C,0BAA2B,OAAO;cAkElC,uBAAsB"}
@@ -0,0 +1,83 @@
1
+ import { open, stat } from "node:fs/promises";
2
+ //#region src/jsonl-tail.ts
3
+ const parseCompleteJsonlBytes = (bytes) => {
4
+ const records = [];
5
+ let offset = 0;
6
+ bytes.forEach((byte, index) => {
7
+ if (byte !== 10) return;
8
+ const line = bytes.subarray(offset, index);
9
+ offset = index + 1;
10
+ if (!line.length) return;
11
+ try {
12
+ records.push(JSON.parse(line.toString("utf8")));
13
+ } catch {}
14
+ });
15
+ return records;
16
+ };
17
+ const parseJsonlLines = (bytes) => {
18
+ const newline = bytes.lastIndexOf(10);
19
+ if (newline < 0) return {
20
+ records: [],
21
+ remainder: bytes
22
+ };
23
+ return {
24
+ records: parseCompleteJsonlBytes(bytes.subarray(0, newline + 1)),
25
+ remainder: bytes.subarray(newline + 1)
26
+ };
27
+ };
28
+ const readBytes = async (path, offset, length) => {
29
+ const handle = await open(path, "r");
30
+ try {
31
+ const bytes = Buffer.alloc(length);
32
+ const { bytesRead } = await handle.read(bytes, 0, length, offset);
33
+ return bytes.subarray(0, bytesRead);
34
+ } finally {
35
+ await handle.close();
36
+ }
37
+ };
38
+ const anchorMatches = async (path, cursor, anchor) => {
39
+ if (!anchor.length) return true;
40
+ return (await readBytes(path, cursor - anchor.length, anchor.length)).equals(anchor);
41
+ };
42
+ const nextAnchor = (previous, appended) => Buffer.concat([previous, appended]).subarray(-512);
43
+ const createJsonlTail = () => {
44
+ let anchor = Buffer.alloc(0);
45
+ let cursor = 0;
46
+ let identity;
47
+ let remainder = Buffer.alloc(0);
48
+ return { read: async (path) => {
49
+ const metadata = await stat(path).catch(() => void 0);
50
+ if (!metadata?.isFile()) return {
51
+ records: [],
52
+ reset: false
53
+ };
54
+ const nextIdentity = `${metadata.dev}:${metadata.ino}`;
55
+ const replaced = identity !== void 0 && identity !== nextIdentity;
56
+ const truncated = metadata.size < cursor;
57
+ const rewritten = !replaced && !truncated && !await anchorMatches(path, cursor, anchor);
58
+ const reset = replaced || truncated || rewritten;
59
+ if (reset) {
60
+ anchor = Buffer.alloc(0);
61
+ cursor = 0;
62
+ remainder = Buffer.alloc(0);
63
+ }
64
+ identity = nextIdentity;
65
+ if (metadata.size === cursor) return {
66
+ records: [],
67
+ reset
68
+ };
69
+ const bytes = await readBytes(path, cursor, metadata.size - cursor);
70
+ cursor += bytes.length;
71
+ anchor = nextAnchor(anchor, bytes);
72
+ const parsed = parseJsonlLines(Buffer.concat([remainder, bytes]));
73
+ remainder = parsed.remainder;
74
+ return {
75
+ records: parsed.records,
76
+ reset
77
+ };
78
+ } };
79
+ };
80
+ //#endregion
81
+ export { createJsonlTail, parseCompleteJsonlBytes };
82
+
83
+ //# sourceMappingURL=jsonl-tail.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jsonl-tail.js","names":[],"sources":["../src/jsonl-tail.ts"],"sourcesContent":["import { open, stat } from \"node:fs/promises\";\n\nconst ANCHOR_BYTES = 512;\n\nexport type JsonlRead = { records: unknown[]; reset: boolean };\nexport type JsonlTail = { read: (path: string) => Promise<JsonlRead> };\n\nexport const parseCompleteJsonlBytes = (bytes: Buffer): unknown[] => {\n const records: unknown[] = [];\n let offset = 0;\n bytes.forEach((byte, index) => {\n if (byte !== 10) {\n return;\n }\n const line = bytes.subarray(offset, index);\n offset = index + 1;\n if (!line.length) {\n return;\n }\n try {\n records.push(JSON.parse(line.toString(\"utf8\")) as unknown);\n } catch {}\n });\n return records;\n};\n\nconst parseJsonlLines = (bytes: Buffer) => {\n const newline = bytes.lastIndexOf(10);\n if (newline < 0) {\n return { records: [] as unknown[], remainder: bytes };\n }\n return {\n records: parseCompleteJsonlBytes(bytes.subarray(0, newline + 1)),\n remainder: bytes.subarray(newline + 1),\n };\n};\n\nconst readBytes = async (\n path: string,\n offset: number,\n length: number,\n): Promise<Buffer<ArrayBufferLike>> => {\n const handle = await open(path, \"r\");\n try {\n const bytes = Buffer.alloc(length);\n const { bytesRead } = await handle.read(bytes, 0, length, offset);\n return bytes.subarray(0, bytesRead);\n } finally {\n await handle.close();\n }\n};\n\nconst anchorMatches = async (\n path: string,\n cursor: number,\n anchor: Buffer<ArrayBufferLike>,\n) => {\n if (!anchor.length) {\n return true;\n }\n const committed = await readBytes(\n path,\n cursor - anchor.length,\n anchor.length,\n );\n return committed.equals(anchor);\n};\n\nconst nextAnchor = (\n previous: Buffer<ArrayBufferLike>,\n appended: Buffer<ArrayBufferLike>,\n) => Buffer.concat([previous, appended]).subarray(-ANCHOR_BYTES);\n\nexport const createJsonlTail = (): JsonlTail => {\n let anchor: Buffer<ArrayBufferLike> = Buffer.alloc(0);\n let cursor = 0;\n let identity: string | undefined;\n let remainder: Buffer<ArrayBufferLike> = Buffer.alloc(0);\n\n return {\n read: async (path) => {\n const metadata = await stat(path).catch(() => undefined);\n if (!metadata?.isFile()) {\n return { records: [], reset: false };\n }\n const nextIdentity = `${metadata.dev}:${metadata.ino}`;\n const replaced = identity !== undefined && identity !== nextIdentity;\n const truncated = metadata.size < cursor;\n const rewritten =\n !replaced && !truncated && !(await anchorMatches(path, cursor, anchor));\n const reset = replaced || truncated || rewritten;\n if (reset) {\n anchor = Buffer.alloc(0);\n cursor = 0;\n remainder = Buffer.alloc(0);\n }\n identity = nextIdentity;\n if (metadata.size === cursor) {\n return { records: [], reset };\n }\n\n const bytes = await readBytes(path, cursor, metadata.size - cursor);\n cursor += bytes.length;\n anchor = nextAnchor(anchor, bytes);\n const parsed = parseJsonlLines(Buffer.concat([remainder, bytes]));\n remainder = parsed.remainder;\n return { records: parsed.records, reset };\n },\n };\n};\n"],"mappings":";;AAOA,MAAa,2BAA2B,UAA6B;CACnE,MAAM,UAAqB,CAAC;CAC5B,IAAI,SAAS;CACb,MAAM,SAAS,MAAM,UAAU;EAC7B,IAAI,SAAS,IACX;EAEF,MAAM,OAAO,MAAM,SAAS,QAAQ,KAAK;EACzC,SAAS,QAAQ;EACjB,IAAI,CAAC,KAAK,QACR;EAEF,IAAI;GACF,QAAQ,KAAK,KAAK,MAAM,KAAK,SAAS,MAAM,CAAC,CAAY;EAC3D,QAAQ,CAAC;CACX,CAAC;CACD,OAAO;AACT;AAEA,MAAM,mBAAmB,UAAkB;CACzC,MAAM,UAAU,MAAM,YAAY,EAAE;CACpC,IAAI,UAAU,GACZ,OAAO;EAAE,SAAS,CAAC;EAAgB,WAAW;CAAM;CAEtD,OAAO;EACL,SAAS,wBAAwB,MAAM,SAAS,GAAG,UAAU,CAAC,CAAC;EAC/D,WAAW,MAAM,SAAS,UAAU,CAAC;CACvC;AACF;AAEA,MAAM,YAAY,OAChB,MACA,QACA,WACqC;CACrC,MAAM,SAAS,MAAM,KAAK,MAAM,GAAG;CACnC,IAAI;EACF,MAAM,QAAQ,OAAO,MAAM,MAAM;EACjC,MAAM,EAAE,cAAc,MAAM,OAAO,KAAK,OAAO,GAAG,QAAQ,MAAM;EAChE,OAAO,MAAM,SAAS,GAAG,SAAS;CACpC,UAAU;EACR,MAAM,OAAO,MAAM;CACrB;AACF;AAEA,MAAM,gBAAgB,OACpB,MACA,QACA,WACG;CACH,IAAI,CAAC,OAAO,QACV,OAAO;CAOT,QAAO,MALiB,UACtB,MACA,SAAS,OAAO,QAChB,OAAO,MACT,EAAA,CACiB,OAAO,MAAM;AAChC;AAEA,MAAM,cACJ,UACA,aACG,OAAO,OAAO,CAAC,UAAU,QAAQ,CAAC,CAAC,CAAC,SAAS,IAAa;AAE/D,MAAa,wBAAmC;CAC9C,IAAI,SAAkC,OAAO,MAAM,CAAC;CACpD,IAAI,SAAS;CACb,IAAI;CACJ,IAAI,YAAqC,OAAO,MAAM,CAAC;CAEvD,OAAO,EACL,MAAM,OAAO,SAAS;EACpB,MAAM,WAAW,MAAM,KAAK,IAAI,CAAC,CAAC,YAAY,KAAA,CAAS;EACvD,IAAI,CAAC,UAAU,OAAO,GACpB,OAAO;GAAE,SAAS,CAAC;GAAG,OAAO;EAAM;EAErC,MAAM,eAAe,GAAG,SAAS,IAAI,GAAG,SAAS;EACjD,MAAM,WAAW,aAAa,KAAA,KAAa,aAAa;EACxD,MAAM,YAAY,SAAS,OAAO;EAClC,MAAM,YACJ,CAAC,YAAY,CAAC,aAAa,CAAE,MAAM,cAAc,MAAM,QAAQ,MAAM;EACvE,MAAM,QAAQ,YAAY,aAAa;EACvC,IAAI,OAAO;GACT,SAAS,OAAO,MAAM,CAAC;GACvB,SAAS;GACT,YAAY,OAAO,MAAM,CAAC;EAC5B;EACA,WAAW;EACX,IAAI,SAAS,SAAS,QACpB,OAAO;GAAE,SAAS,CAAC;GAAG;EAAM;EAG9B,MAAM,QAAQ,MAAM,UAAU,MAAM,QAAQ,SAAS,OAAO,MAAM;EAClE,UAAU,MAAM;EAChB,SAAS,WAAW,QAAQ,KAAK;EACjC,MAAM,SAAS,gBAAgB,OAAO,OAAO,CAAC,WAAW,KAAK,CAAC,CAAC;EAChE,YAAY,OAAO;EACnB,OAAO;GAAE,SAAS,OAAO;GAAS;EAAM;CAC1C,EACF;AACF"}
@@ -0,0 +1,497 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { appendFile, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
3
+ import { dirname, join, relative, resolve, sep } from "node:path";
4
+ import { z } from "zod";
5
+ import { hostname } from "node:os";
6
+ import { watch } from "node:fs";
7
+ //#region src/session-status.ts
8
+ const piSessionStatusSchema = z.object({
9
+ version: z.literal(1),
10
+ sessionId: z.string().min(1),
11
+ sessionFile: z.string().min(1),
12
+ streamId: z.string().min(1),
13
+ processInstanceId: z.string().min(1),
14
+ pid: z.number().int().positive(),
15
+ hostname: z.string().min(1),
16
+ tmuxPane: z.string().min(1).optional(),
17
+ startedAt: z.number().int().nonnegative(),
18
+ updatedAt: z.number().int().nonnegative(),
19
+ state: z.enum([
20
+ "idle",
21
+ "busy",
22
+ "stopped"
23
+ ]),
24
+ model: z.object({
25
+ provider: z.string().min(1),
26
+ id: z.string().min(1),
27
+ name: z.string().min(1)
28
+ }).strict().optional(),
29
+ thinkingLevel: z.string().min(1),
30
+ modelOptions: z.array(z.object({
31
+ provider: z.string().min(1),
32
+ id: z.string().min(1),
33
+ name: z.string().min(1)
34
+ }).strict()).max(256).optional(),
35
+ contextUsage: z.object({
36
+ tokens: z.number().nonnegative(),
37
+ contextWindow: z.number().positive(),
38
+ percent: z.number().nonnegative()
39
+ }).strict().optional()
40
+ }).strict();
41
+ const SAFE_PATH_SEGMENT$1 = /^[A-Za-z0-9._-]{1,128}$/;
42
+ const requirePathSegment$1 = (value, label) => {
43
+ if (!SAFE_PATH_SEGMENT$1.test(value)) throw new Error(`Invalid ${label} for Pi session status path`);
44
+ return value;
45
+ };
46
+ const piSessionStatusPath = (rootDir, sessionId, streamId) => join(resolve(rootDir), requirePathSegment$1(sessionId, "session ID"), `${requirePathSegment$1(streamId, "stream ID")}.status.json`);
47
+ const parsePiSessionStatus = (value) => {
48
+ const result = piSessionStatusSchema.safeParse(value);
49
+ return result.success ? result.data : void 0;
50
+ };
51
+ const reportError$1 = (onError, error) => {
52
+ try {
53
+ onError?.(error);
54
+ } catch {}
55
+ };
56
+ const replaceStatusFile = async (state, status) => {
57
+ await mkdir(state.sessionDir, {
58
+ recursive: true,
59
+ mode: 448
60
+ });
61
+ const temporaryPath = `${state.filePath}.${randomUUID()}.tmp`;
62
+ try {
63
+ await writeFile(temporaryPath, `${JSON.stringify(status)}\n`, {
64
+ encoding: "utf8",
65
+ flag: "wx",
66
+ mode: 384
67
+ });
68
+ await rename(temporaryPath, state.filePath);
69
+ } catch (error) {
70
+ await rm(temporaryPath, { force: true }).catch(() => void 0);
71
+ throw error;
72
+ }
73
+ };
74
+ const updateStatus = (state, update) => {
75
+ if (!state.accepting) return state.writeChain;
76
+ const status = piSessionStatusSchema.parse({
77
+ ...state.current,
78
+ ...update,
79
+ updatedAt: Date.now()
80
+ });
81
+ state.current = status;
82
+ state.writeChain = state.writeChain.then(() => replaceStatusFile(state, status)).catch((error) => reportError$1(state.onError, error));
83
+ return state.writeChain;
84
+ };
85
+ const createPiSessionStatusWriter = ({ rootDir, status, onError }) => {
86
+ const filePath = piSessionStatusPath(rootDir, status.sessionId, status.streamId);
87
+ const state = {
88
+ accepting: true,
89
+ current: piSessionStatusSchema.parse({
90
+ ...status,
91
+ updatedAt: status.startedAt,
92
+ version: 1
93
+ }),
94
+ filePath,
95
+ onError,
96
+ sessionDir: join(resolve(rootDir), status.sessionId),
97
+ writeChain: Promise.resolve()
98
+ };
99
+ return {
100
+ filePath,
101
+ streamId: status.streamId,
102
+ update: (update) => updateStatus(state, update),
103
+ close: async () => {
104
+ if (!state.accepting) {
105
+ await state.writeChain;
106
+ return;
107
+ }
108
+ const closing = updateStatus(state, { state: "stopped" });
109
+ state.accepting = false;
110
+ await closing;
111
+ }
112
+ };
113
+ };
114
+ const readStatus = async (filePath, sessionId, streamId) => {
115
+ try {
116
+ const status = parsePiSessionStatus(JSON.parse(await readFile(filePath, "utf8")));
117
+ return status?.sessionId === sessionId && status.streamId === streamId ? status : void 0;
118
+ } catch {
119
+ return;
120
+ }
121
+ };
122
+ const compareRecency = (left, right) => left.updatedAt - right.updatedAt || left.startedAt - right.startedAt || left.streamId.localeCompare(right.streamId);
123
+ const newestBy = (statuses, keyOf) => {
124
+ const newest = /* @__PURE__ */ new Map();
125
+ statuses.forEach((status) => {
126
+ const key = keyOf(status);
127
+ const previous = newest.get(key);
128
+ if (!previous || compareRecency(previous, status) < 0) newest.set(key, status);
129
+ });
130
+ return [...newest.values()];
131
+ };
132
+ const isLocalPidAlive = (pid) => {
133
+ try {
134
+ process.kill(pid, 0);
135
+ return true;
136
+ } catch (error) {
137
+ return error.code === "EPERM";
138
+ }
139
+ };
140
+ const listPiSessionStatuses = async ({ rootDir, hostname: hostname$1 = hostname(), pidAlive = isLocalPidAlive }) => {
141
+ const sessionDirectories = (await readdir(resolve(rootDir), { withFileTypes: true }).catch(() => [])).filter((entry) => entry.isDirectory() && SAFE_PATH_SEGMENT$1.test(entry.name)).map((entry) => entry.name);
142
+ const statuses = (await Promise.all(sessionDirectories.map(async (sessionId) => {
143
+ const sessionDir = join(resolve(rootDir), sessionId);
144
+ const files = (await readdir(sessionDir, { withFileTypes: true }).catch(() => [])).filter((entry) => entry.isFile() && entry.name.endsWith(".status.json") && SAFE_PATH_SEGMENT$1.test(entry.name.slice(0, -12)));
145
+ return Promise.all(files.map((entry) => {
146
+ const streamId = entry.name.slice(0, -12);
147
+ return readStatus(join(sessionDir, entry.name), sessionId, streamId);
148
+ }));
149
+ }))).flatMap((values) => values).filter((status) => status !== void 0);
150
+ const processStatuses = newestBy(statuses, ({ hostname: statusHostname, processInstanceId }) => `${statusHostname}\0${processInstanceId}`).filter((status) => status.state !== "stopped" && status.hostname === hostname$1 && pidAlive(status.pid));
151
+ return newestBy(processStatuses, ({ sessionId }) => sessionId).sort((left, right) => left.sessionId.localeCompare(right.sessionId));
152
+ };
153
+ const createPiSessionStatusSource = ({ rootDir, pollIntervalMs = 250, hostname, pidAlive }) => ({
154
+ list: () => listPiSessionStatuses({
155
+ rootDir,
156
+ hostname,
157
+ pidAlive
158
+ }),
159
+ subscribe: (invalidate, { signal }) => {
160
+ if (signal.aborted) return () => void 0;
161
+ let watcher;
162
+ let scheduled;
163
+ let fingerprint;
164
+ let checkQueue = Promise.resolve();
165
+ const check = () => {
166
+ checkQueue = checkQueue.then(async () => {
167
+ const next = JSON.stringify(await listPiSessionStatuses({
168
+ rootDir,
169
+ hostname,
170
+ pidAlive
171
+ }));
172
+ if (next === fingerprint || signal.aborted) return;
173
+ fingerprint = next;
174
+ invalidate();
175
+ }).catch(() => void 0);
176
+ };
177
+ const schedule = () => {
178
+ if (scheduled || signal.aborted) return;
179
+ scheduled = setTimeout(() => {
180
+ scheduled = void 0;
181
+ check();
182
+ }, 10);
183
+ scheduled.unref();
184
+ };
185
+ try {
186
+ watcher = watch(resolve(rootDir), { recursive: true }, schedule);
187
+ } catch {}
188
+ check();
189
+ const timer = setInterval(schedule, pollIntervalMs);
190
+ timer.unref();
191
+ const dispose = () => {
192
+ signal.removeEventListener("abort", dispose);
193
+ if (scheduled) clearTimeout(scheduled);
194
+ clearInterval(timer);
195
+ watcher?.close();
196
+ };
197
+ signal.addEventListener("abort", dispose, { once: true });
198
+ return dispose;
199
+ }
200
+ });
201
+ //#endregion
202
+ //#region src/live-events.ts
203
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
204
+ const parseLiveEventRecord = (value, sessionId) => {
205
+ if (!isRecord(value) || value.version !== 1) return;
206
+ if (sessionId !== void 0 && value.sessionId !== sessionId) return;
207
+ if (typeof value.sessionId !== "string" || typeof value.processInstanceId !== "string" || typeof value.streamId !== "string" || !Number.isSafeInteger(value.sequence) || value.sequence < 1 || typeof value.timestamp !== "number" || !isRecord(value.event) || typeof value.event.type !== "string") return;
208
+ return value;
209
+ };
210
+ const PROCESS_INSTANCE_KEY = Symbol.for("overmux-pi.live-events.process-instance-id");
211
+ const processGlobals = globalThis;
212
+ const PROCESS_INSTANCE_ID = processGlobals[PROCESS_INSTANCE_KEY] ?? `${process.pid}-${randomUUID()}`;
213
+ processGlobals[PROCESS_INSTANCE_KEY] = PROCESS_INSTANCE_ID;
214
+ const SAFE_PATH_SEGMENT = /^[A-Za-z0-9._-]{1,128}$/;
215
+ const requirePathSegment = (value, label) => {
216
+ if (!SAFE_PATH_SEGMENT.test(value)) throw new Error(`Invalid ${label} for live event path`);
217
+ return value;
218
+ };
219
+ const liveEventSessionDir = (rootDir, sessionId) => join(resolve(rootDir), requirePathSegment(sessionId, "session ID"));
220
+ const liveEventStreamPath = (rootDir, sessionId, streamId) => join(liveEventSessionDir(rootDir, sessionId), `${requirePathSegment(streamId, "stream ID")}.jsonl`);
221
+ const isSameOrWithin = (parent, candidate) => {
222
+ const relativePath = relative(resolve(parent), resolve(candidate));
223
+ return relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${sep}`);
224
+ };
225
+ const assertSeparateFromSessionFile = (rootDir, sessionFile) => {
226
+ if (!sessionFile) return;
227
+ const sessionDir = dirname(sessionFile);
228
+ if (isSameOrWithin(rootDir, sessionDir) || isSameOrWithin(sessionDir, rootDir)) throw new Error("Live events directory must be outside the Pi session directory tree");
229
+ };
230
+ const reportError = (onError, error) => {
231
+ try {
232
+ onError?.(error);
233
+ } catch {}
234
+ };
235
+ const reportWriterError = (state, error) => {
236
+ state.failed = true;
237
+ reportError(state.onError, error);
238
+ };
239
+ const appendLiveEvent = (state, event) => {
240
+ if (!state.accepting || state.failed) return;
241
+ const record = {
242
+ version: 1,
243
+ sessionId: state.sessionId,
244
+ processInstanceId: state.processInstanceId,
245
+ streamId: state.streamId,
246
+ sequence: state.sequence,
247
+ timestamp: Date.now(),
248
+ event
249
+ };
250
+ state.sequence += 1;
251
+ let line;
252
+ try {
253
+ line = `${JSON.stringify(record)}\n`;
254
+ } catch (error) {
255
+ reportWriterError(state, error);
256
+ return;
257
+ }
258
+ state.writeChain = state.writeChain.then(async () => {
259
+ if (!state.directoryReady) {
260
+ await mkdir(state.sessionDir, {
261
+ recursive: true,
262
+ mode: 448
263
+ });
264
+ state.directoryReady = true;
265
+ }
266
+ await appendFile(state.filePath, line, {
267
+ encoding: "utf8",
268
+ mode: 384
269
+ });
270
+ }).catch((error) => reportWriterError(state, error));
271
+ };
272
+ const closeLiveEventWriter = async (state, event) => {
273
+ if (!state.accepting) {
274
+ await state.writeChain;
275
+ return;
276
+ }
277
+ appendLiveEvent(state, event);
278
+ state.accepting = false;
279
+ await state.writeChain;
280
+ };
281
+ const createLiveEventWriter = ({ rootDir, sessionId, processInstanceId = PROCESS_INSTANCE_ID, streamId = randomUUID(), onError }) => {
282
+ const state = {
283
+ filePath: liveEventStreamPath(rootDir, sessionId, streamId),
284
+ sessionDir: liveEventSessionDir(rootDir, sessionId),
285
+ sessionId,
286
+ processInstanceId,
287
+ streamId,
288
+ sequence: 1,
289
+ accepting: true,
290
+ directoryReady: false,
291
+ failed: false,
292
+ writeChain: Promise.resolve(),
293
+ onError
294
+ };
295
+ return {
296
+ filePath: state.filePath,
297
+ streamId: state.streamId,
298
+ append: (event) => appendLiveEvent(state, event),
299
+ close: (event) => closeLiveEventWriter(state, event)
300
+ };
301
+ };
302
+ const compactAssistantMessageEvent = (event) => {
303
+ if (event.type === "start") return { type: event.type };
304
+ if (event.type === "done") return {
305
+ type: event.type,
306
+ reason: event.reason
307
+ };
308
+ if (event.type === "error") {
309
+ const errorMessage = event.error.errorMessage;
310
+ return {
311
+ type: event.type,
312
+ reason: event.reason,
313
+ ...errorMessage ? { errorMessage } : {}
314
+ };
315
+ }
316
+ if (event.type === "text_delta" || event.type === "thinking_delta" || event.type === "toolcall_delta") return {
317
+ type: event.type,
318
+ contentIndex: event.contentIndex,
319
+ delta: event.delta
320
+ };
321
+ return {
322
+ type: event.type,
323
+ contentIndex: event.contentIndex
324
+ };
325
+ };
326
+ const modelIdentity = (model) => ({
327
+ provider: model.provider,
328
+ id: model.id,
329
+ name: model.name
330
+ });
331
+ const modelOptions = (ctx) => {
332
+ const scopedModels = ctx.scopedModels ?? [];
333
+ const availableModels = ctx.modelRegistry?.getAvailable?.() ?? [];
334
+ const models = scopedModels.length ? scopedModels.map(({ model }) => model) : availableModels.length ? availableModels : ctx.modelRegistry?.getAll?.() ?? [];
335
+ return [...new Map(models.map((model) => {
336
+ const identity = modelIdentity(model);
337
+ return [`${identity.provider}\0${identity.id}`, identity];
338
+ })).values()].slice(0, 256);
339
+ };
340
+ const statusContextUsage = (ctx) => {
341
+ const usage = ctx.getContextUsage();
342
+ return usage && usage.tokens !== null && usage.percent !== null ? {
343
+ ...usage,
344
+ tokens: usage.tokens,
345
+ percent: usage.percent
346
+ } : void 0;
347
+ };
348
+ const messageRole = (event) => event.message.role;
349
+ const registerLiveEventCapture = (pi, options) => {
350
+ const state = { messageSequence: 0 };
351
+ const append = (event) => state.writer?.append(event);
352
+ const updateStatus = (ctx, update) => state.statusWriter?.update({
353
+ contextUsage: statusContextUsage(ctx),
354
+ ...update
355
+ });
356
+ pi.on("session_info_changed", (event) => append(event));
357
+ pi.on("session_compact", (event) => append(event));
358
+ pi.on("session_tree", (event) => append(event));
359
+ pi.on("agent_start", (event, ctx) => {
360
+ append(event);
361
+ return updateStatus(ctx, { state: "busy" });
362
+ });
363
+ pi.on("agent_end", (event) => append({ type: event.type }));
364
+ pi.on("agent_settled", (event, ctx) => {
365
+ append(event);
366
+ return updateStatus(ctx, { state: "idle" });
367
+ });
368
+ pi.on("turn_start", (event) => append(event));
369
+ pi.on("turn_end", (event, ctx) => {
370
+ append({
371
+ type: event.type,
372
+ turnIndex: event.turnIndex
373
+ });
374
+ return updateStatus(ctx, {});
375
+ });
376
+ pi.on("message_start", (event) => {
377
+ state.messageSequence += 1;
378
+ const activeMessage = {
379
+ id: `${state.writer?.streamId ?? "inactive"}:${state.messageSequence}`,
380
+ sequence: state.messageSequence,
381
+ role: messageRole(event)
382
+ };
383
+ state.activeMessage = activeMessage;
384
+ append({
385
+ type: event.type,
386
+ messageId: activeMessage.id,
387
+ messageSequence: activeMessage.sequence,
388
+ message: event.message
389
+ });
390
+ });
391
+ pi.on("message_update", (event) => {
392
+ const activeMessage = state.activeMessage;
393
+ if (!activeMessage || activeMessage.role !== "assistant") return;
394
+ append({
395
+ type: event.type,
396
+ messageId: activeMessage.id,
397
+ messageSequence: activeMessage.sequence,
398
+ assistantMessageEvent: compactAssistantMessageEvent(event.assistantMessageEvent)
399
+ });
400
+ });
401
+ pi.on("message_end", (event, ctx) => {
402
+ const activeMessage = state.activeMessage;
403
+ if (!activeMessage) state.messageSequence += 1;
404
+ const identity = activeMessage ?? {
405
+ id: `${state.writer?.streamId ?? "inactive"}:${state.messageSequence}`,
406
+ sequence: state.messageSequence,
407
+ role: messageRole(event)
408
+ };
409
+ append({
410
+ type: event.type,
411
+ messageId: identity.id,
412
+ messageSequence: identity.sequence,
413
+ message: event.message
414
+ });
415
+ state.activeMessage = void 0;
416
+ return updateStatus(ctx, {});
417
+ });
418
+ pi.on("tool_execution_start", (event) => append(event));
419
+ pi.on("tool_execution_end", (event) => append(event));
420
+ pi.on("model_select", (event, ctx) => {
421
+ const model = modelIdentity(event.model);
422
+ append({
423
+ type: event.type,
424
+ source: event.source,
425
+ model,
426
+ ...event.previousModel ? { previousModel: modelIdentity(event.previousModel) } : {}
427
+ });
428
+ return updateStatus(ctx, { model });
429
+ });
430
+ pi.on("thinking_level_select", (event, ctx) => {
431
+ append(event);
432
+ return updateStatus(ctx, { thinkingLevel: event.level });
433
+ });
434
+ return {
435
+ start: async (event, ctx) => {
436
+ state.messageSequence = 0;
437
+ state.activeMessage = void 0;
438
+ if (!options.rootDir) return;
439
+ try {
440
+ const sessionId = ctx.sessionManager.getSessionId();
441
+ const sessionFile = ctx.sessionManager.getSessionFile();
442
+ if (!sessionFile) throw new Error("Pi session has no session file");
443
+ assertSeparateFromSessionFile(options.rootDir, sessionFile);
444
+ state.writer = createLiveEventWriter({
445
+ rootDir: options.rootDir,
446
+ sessionId,
447
+ onError: options.onError
448
+ });
449
+ const startedAt = Date.now();
450
+ const contextUsage = statusContextUsage(ctx);
451
+ const availableModelOptions = modelOptions(ctx);
452
+ state.statusWriter = createPiSessionStatusWriter({
453
+ rootDir: options.rootDir,
454
+ onError: options.onError,
455
+ status: {
456
+ sessionId,
457
+ sessionFile,
458
+ streamId: state.writer.streamId,
459
+ processInstanceId: PROCESS_INSTANCE_ID,
460
+ pid: process.pid,
461
+ hostname: hostname(),
462
+ ...options.env.TMUX_PANE ? { tmuxPane: options.env.TMUX_PANE } : {},
463
+ startedAt,
464
+ state: "idle",
465
+ ...ctx.model ? { model: modelIdentity(ctx.model) } : {},
466
+ ...availableModelOptions.length ? { modelOptions: availableModelOptions } : {},
467
+ thinkingLevel: ctx.thinkingLevel ?? pi.getThinkingLevel(),
468
+ ...contextUsage ? { contextUsage } : {}
469
+ }
470
+ });
471
+ append({
472
+ ...event,
473
+ cwd: ctx.cwd,
474
+ sessionFile,
475
+ pid: process.pid
476
+ });
477
+ await state.statusWriter.update({});
478
+ } catch (error) {
479
+ state.writer = void 0;
480
+ state.statusWriter = void 0;
481
+ reportError(options.onError, error);
482
+ }
483
+ },
484
+ stop: async (event) => {
485
+ const writer = state.writer;
486
+ const statusWriter = state.statusWriter;
487
+ state.writer = void 0;
488
+ state.statusWriter = void 0;
489
+ state.activeMessage = void 0;
490
+ await Promise.all([writer?.close(event), statusWriter?.close()]);
491
+ }
492
+ };
493
+ };
494
+ //#endregion
495
+ export { liveEventStreamPath as a, createPiSessionStatusSource as c, listPiSessionStatuses as d, parsePiSessionStatus as f, liveEventSessionDir as i, createPiSessionStatusWriter as l, piSessionStatusSchema as m, compactAssistantMessageEvent as n, parseLiveEventRecord as o, piSessionStatusPath as p, createLiveEventWriter as r, registerLiveEventCapture as s, PROCESS_INSTANCE_ID as t, isLocalPidAlive as u };
496
+
497
+ //# sourceMappingURL=live-events-ClhFOMGW.js.map