@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.
- package/README.md +118 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +40 -0
- package/dist/cli.js.map +1 -0
- package/dist/config-2jNv4tol.d.ts +34 -0
- package/dist/config-2jNv4tol.d.ts.map +1 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +47 -0
- package/dist/config.js.map +1 -0
- package/dist/extension.d.ts +19 -0
- package/dist/extension.d.ts.map +1 -0
- package/dist/extension.js +265 -0
- package/dist/extension.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/jsonl-tail.d.ts +13 -0
- package/dist/jsonl-tail.d.ts.map +1 -0
- package/dist/jsonl-tail.js +83 -0
- package/dist/jsonl-tail.js.map +1 -0
- package/dist/live-events-ClhFOMGW.js +497 -0
- package/dist/live-events-ClhFOMGW.js.map +1 -0
- package/dist/live-events-DAmf6RRx.d.ts +107 -0
- package/dist/live-events-DAmf6RRx.d.ts.map +1 -0
- package/dist/live-events.d.ts +2 -0
- package/dist/live-events.js +2 -0
- package/dist/notification-DzOd9cRc.d.ts +20 -0
- package/dist/notification-DzOd9cRc.d.ts.map +1 -0
- package/dist/notification.d.ts +2 -0
- package/dist/notification.js +54 -0
- package/dist/notification.js.map +1 -0
- package/dist/plugin.d.ts +90 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +213 -0
- package/dist/plugin.js.map +1 -0
- package/dist/projection.d.ts +101 -0
- package/dist/projection.d.ts.map +1 -0
- package/dist/projection.js +550 -0
- package/dist/projection.js.map +1 -0
- package/dist/protocol-CsrnSPOv.d.ts +115 -0
- package/dist/protocol-CsrnSPOv.d.ts.map +1 -0
- package/dist/protocol.d.ts +2 -0
- package/dist/protocol.js +365 -0
- package/dist/protocol.js.map +1 -0
- package/dist/react.d.ts +139 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +796 -0
- package/dist/react.js.map +1 -0
- package/dist/server.d.ts +5 -0
- package/dist/server.js +4 -0
- package/dist/session-status-CZLTo8Km.d.ts +71 -0
- package/dist/session-status-CZLTo8Km.d.ts.map +1 -0
- package/dist/styles.css +619 -0
- package/docs/index.md +10 -0
- package/package.json +115 -0
- package/src/cli.ts +59 -0
- package/src/config.ts +87 -0
- package/src/extension.ts +486 -0
- package/src/index.ts +14 -0
- package/src/jsonl-tail.ts +110 -0
- package/src/live-events.ts +578 -0
- package/src/notification.ts +109 -0
- package/src/plugin.ts +369 -0
- package/src/projection.ts +995 -0
- package/src/protocol.ts +805 -0
- package/src/react.tsx +1293 -0
- package/src/server.ts +15 -0
- package/src/session-status.ts +379 -0
- 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
|