@pontia/pi-client-plugin 0.1.0 → 0.2.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 +17 -25
- package/package.json +30 -23
- package/src/context.ts +0 -104
- package/src/control-socket.ts +456 -0
- package/src/discovery.ts +2 -63
- package/src/events.ts +99 -20
- package/src/index.ts +479 -166
- package/src/live-output.ts +339 -0
- package/src/managed-runtime.ts +5 -1
- package/src/pi-message.ts +14 -4
- package/src/profile.ts +14 -22
- package/src/reporter.ts +62 -32
- package/src/runtime-binding.ts +22 -46
- package/src/session.ts +0 -1
- package/src/values.ts +9 -0
- package/src/workspace.ts +8 -13
- package/src/internal-api.ts +0 -29
package/src/runtime-binding.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { EnvLike } from "./context.js";
|
|
2
|
-
import {
|
|
3
|
-
import { asRecord, optionalString
|
|
2
|
+
import { CONTROL_VERSION, type PiConnection } from "./control-socket.js";
|
|
3
|
+
import { asRecord, optionalString } from "./values.js";
|
|
4
4
|
import type { SessionContext } from "./session.js";
|
|
5
5
|
|
|
6
|
-
export type PiSessionDetails = Pick<
|
|
6
|
+
export type PiSessionDetails = Pick<
|
|
7
|
+
SessionContext,
|
|
8
|
+
"clientSessionKey" | "clientSessionFile" | "clientSessionDir" | "clientCwd"
|
|
9
|
+
>;
|
|
7
10
|
|
|
8
11
|
function callSessionManagerString(sessionManager: unknown, method: string): string | undefined {
|
|
9
12
|
if (!sessionManager || typeof sessionManager !== "object") return undefined;
|
|
@@ -17,7 +20,8 @@ function callSessionManagerString(sessionManager: unknown, method: string): stri
|
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
export function piSessionDetailsFromHookContext(ctx: unknown): PiSessionDetails {
|
|
20
|
-
const sessionManager =
|
|
23
|
+
const sessionManager =
|
|
24
|
+
ctx && typeof ctx === "object" ? (ctx as Record<string, unknown>).sessionManager : undefined;
|
|
21
25
|
return {
|
|
22
26
|
clientSessionKey: callSessionManagerString(sessionManager, "getSessionId"),
|
|
23
27
|
clientSessionFile: callSessionManagerString(sessionManager, "getSessionFile"),
|
|
@@ -26,13 +30,6 @@ export function piSessionDetailsFromHookContext(ctx: unknown): PiSessionDetails
|
|
|
26
30
|
};
|
|
27
31
|
}
|
|
28
32
|
|
|
29
|
-
function agentBindingSessionContextUrl(discoveredBindingUpsertUrl?: string): string | undefined {
|
|
30
|
-
return discoveredBindingUpsertUrl?.replace(
|
|
31
|
-
/\/runtime-bindings\/upsert\/?$/,
|
|
32
|
-
"/agent-bindings/session-context",
|
|
33
|
-
);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
33
|
function tmuxBindingFromEnv(env: EnvLike): { socket_path: string; pane_id: string } | undefined {
|
|
37
34
|
const tmux = optionalString(env.TMUX);
|
|
38
35
|
const paneId = optionalString(env.TMUX_PANE);
|
|
@@ -42,51 +39,40 @@ function tmuxBindingFromEnv(env: EnvLike): { socket_path: string; pane_id: strin
|
|
|
42
39
|
}
|
|
43
40
|
|
|
44
41
|
export async function bindSession(
|
|
45
|
-
|
|
42
|
+
connection: PiConnection,
|
|
46
43
|
env: EnvLike,
|
|
47
|
-
fetchImpl: typeof fetch,
|
|
48
44
|
sessionDetails: PiSessionDetails,
|
|
49
45
|
options: { startKind?: "fork"; parentSessionId?: string; runtimeInstanceId?: string } = {},
|
|
50
46
|
): Promise<SessionContext | undefined> {
|
|
51
47
|
if (!sessionDetails.clientSessionKey) return undefined;
|
|
52
|
-
const discovered = await resolvePontiaConnection({ pontiaHome, fetch: fetchImpl });
|
|
53
|
-
const url = discovered?.bindingUpsertUrl;
|
|
54
|
-
if (!url) return undefined;
|
|
55
|
-
|
|
56
48
|
const tmux = tmuxBindingFromEnv(env);
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
body: JSON.stringify({
|
|
49
|
+
const body = await connection.request("runtime.register", {
|
|
50
|
+
version: CONTROL_VERSION,
|
|
51
|
+
binding: {
|
|
61
52
|
client_type: "pi",
|
|
62
53
|
client_session_key: sessionDetails.clientSessionKey,
|
|
63
54
|
client_session_file: sessionDetails.clientSessionFile,
|
|
64
55
|
client_session_dir: sessionDetails.clientSessionDir,
|
|
65
56
|
client_cwd: sessionDetails.clientCwd,
|
|
66
57
|
launch_cwd: sessionDetails.clientCwd,
|
|
67
|
-
start_command: "pi",
|
|
68
58
|
...(options.startKind ? { start_kind: options.startKind } : {}),
|
|
69
59
|
...(options.parentSessionId ? { parent_session_id: options.parentSessionId } : {}),
|
|
70
60
|
...(options.runtimeInstanceId ? { runtime_instance_id: options.runtimeInstanceId } : {}),
|
|
71
61
|
...(tmux ? { tmux } : {}),
|
|
72
|
-
}
|
|
62
|
+
},
|
|
73
63
|
});
|
|
74
|
-
const body = await parseJsonResponse(response);
|
|
75
|
-
if (!response.ok) throw new Error(`runtime binding upsert failed: ${response.status} ${response.statusText}`);
|
|
76
64
|
|
|
77
65
|
const record = asRecord(body);
|
|
78
66
|
const session = asRecord(record?.session);
|
|
79
67
|
const runtime = asRecord(record?.runtime);
|
|
80
68
|
const sessionId = optionalString(session?.session_id);
|
|
81
69
|
const resolvedRuntimeInstanceId = optionalString(runtime?.runtime_instance_id);
|
|
82
|
-
const internalEventUrl = optionalString(runtime?.internal_event_url) ?? discovered?.internalEventUrl;
|
|
83
70
|
if (!sessionId) throw new Error("runtime binding upsert response missing session.session_id");
|
|
84
|
-
if (!resolvedRuntimeInstanceId)
|
|
85
|
-
|
|
71
|
+
if (!resolvedRuntimeInstanceId)
|
|
72
|
+
throw new Error("runtime binding upsert response missing runtime.runtime_instance_id");
|
|
86
73
|
return {
|
|
87
74
|
sessionId,
|
|
88
75
|
clientType: "pi",
|
|
89
|
-
internalEventUrl,
|
|
90
76
|
runtimeInstanceId: resolvedRuntimeInstanceId,
|
|
91
77
|
...sessionDetails,
|
|
92
78
|
};
|
|
@@ -97,29 +83,20 @@ export interface ExistingPiSessionContext extends SessionContext {
|
|
|
97
83
|
}
|
|
98
84
|
|
|
99
85
|
export async function loadExistingSessionContext(
|
|
100
|
-
|
|
101
|
-
fetchImpl: typeof fetch,
|
|
86
|
+
connection: PiConnection,
|
|
102
87
|
sessionDetails: PiSessionDetails,
|
|
103
88
|
): Promise<ExistingPiSessionContext | undefined> {
|
|
104
89
|
if (!sessionDetails.clientSessionKey) return undefined;
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
url.searchParams.set("client_session_key", sessionDetails.clientSessionKey);
|
|
111
|
-
const response = await fetchImpl(url.toString());
|
|
112
|
-
if (response.status === 404) return undefined;
|
|
113
|
-
const body = await parseJsonResponse(response);
|
|
114
|
-
if (!response.ok) throw new Error(`agent binding session context lookup failed: ${response.status} ${response.statusText}`);
|
|
115
|
-
|
|
116
|
-
const record = asRecord(asRecord(asRecord(body)?.data)?.session_context);
|
|
90
|
+
const body = await connection.request("session.context", {
|
|
91
|
+
client_session_key: sessionDetails.clientSessionKey,
|
|
92
|
+
});
|
|
93
|
+
const record = asRecord(asRecord(body)?.session_context);
|
|
94
|
+
if (!record) return undefined;
|
|
117
95
|
const sessionId = optionalString(record?.session_id);
|
|
118
96
|
const sessionState = optionalString(record?.session_state);
|
|
119
97
|
const clientType = optionalString(record?.client_type);
|
|
120
98
|
const runtimeInstanceId = optionalString(record?.runtime_instance_id);
|
|
121
|
-
|
|
122
|
-
if (!sessionId || !sessionState || clientType !== "pi" || !runtimeInstanceId || !internalEventUrl) {
|
|
99
|
+
if (!sessionId || !sessionState || clientType !== "pi" || !runtimeInstanceId) {
|
|
123
100
|
throw new Error("agent binding session context lookup returned an invalid context");
|
|
124
101
|
}
|
|
125
102
|
return {
|
|
@@ -127,7 +104,6 @@ export async function loadExistingSessionContext(
|
|
|
127
104
|
sessionState,
|
|
128
105
|
clientType: "pi",
|
|
129
106
|
runtimeInstanceId,
|
|
130
|
-
internalEventUrl,
|
|
131
107
|
...sessionDetails,
|
|
132
108
|
};
|
|
133
109
|
}
|
package/src/session.ts
CHANGED
package/src/values.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function optionalString(value: unknown): string | undefined {
|
|
2
|
+
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
6
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
7
|
+
? (value as Record<string, unknown>)
|
|
8
|
+
: undefined;
|
|
9
|
+
}
|
package/src/workspace.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { realpath } from "node:fs/promises";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import { asRecord
|
|
3
|
+
import type { PiConnection } from "./control-socket.js";
|
|
4
|
+
import { asRecord } from "./values.js";
|
|
5
5
|
|
|
6
6
|
async function canonicalPath(path: string): Promise<string> {
|
|
7
7
|
try {
|
|
@@ -11,20 +11,15 @@ async function canonicalPath(path: string): Promise<string> {
|
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
export async function
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export async function isActiveRegisteredWorkspace(pontiaHome: string, fetchImpl: typeof fetch, clientCwd: string | undefined): Promise<boolean | undefined> {
|
|
14
|
+
export async function isActiveRegisteredWorkspace(
|
|
15
|
+
connection: Pick<PiConnection, "request">,
|
|
16
|
+
clientCwd: string | undefined,
|
|
17
|
+
): Promise<boolean> {
|
|
21
18
|
if (!clientCwd) return false;
|
|
22
|
-
const api = await resolveWorkspaceApi(pontiaHome, fetchImpl);
|
|
23
|
-
if (!api) return undefined;
|
|
24
19
|
|
|
25
20
|
const workspacePath = await canonicalPath(clientCwd);
|
|
26
|
-
const body = await
|
|
27
|
-
const workspaces =
|
|
21
|
+
const body = await connection.request("workspaces.list", {});
|
|
22
|
+
const workspaces = asRecord(body)?.workspaces;
|
|
28
23
|
if (!Array.isArray(workspaces)) return false;
|
|
29
24
|
|
|
30
25
|
return workspaces.some((workspace) => {
|
package/src/internal-api.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
export function optionalString(value: unknown): string | undefined {
|
|
2
|
-
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
export function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
6
|
-
return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export async function parseJsonResponse(response: Response): Promise<unknown> {
|
|
10
|
-
const text = await response.text().catch(() => "");
|
|
11
|
-
if (!text) return null;
|
|
12
|
-
try {
|
|
13
|
-
return JSON.parse(text);
|
|
14
|
-
} catch {
|
|
15
|
-
return text;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function responseDataRecord(body: unknown): Record<string, unknown> | undefined {
|
|
20
|
-
const record = asRecord(body);
|
|
21
|
-
return asRecord(record?.data);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export async function fetchJson(fetchImpl: typeof fetch, url: string, token: string): Promise<unknown> {
|
|
25
|
-
const response = await fetchImpl(url, { headers: { Authorization: `Bearer ${token}` } });
|
|
26
|
-
const body = await parseJsonResponse(response);
|
|
27
|
-
if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);
|
|
28
|
-
return body;
|
|
29
|
-
}
|