@pontia/pi-client-plugin 0.2.0 → 0.2.2

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.
@@ -1,9 +1,12 @@
1
1
  import type { EnvLike } from "./context.js";
2
- import { resolvePontiaConnection } from "./discovery.js";
3
- import { asRecord, optionalString, parseJsonResponse } from "./internal-api.js";
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<SessionContext, "clientSessionKey" | "clientSessionFile" | "clientSessionDir" | "clientCwd">;
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 = ctx && typeof ctx === "object" ? (ctx as Record<string, unknown>).sessionManager : undefined;
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
- pontiaHome: string,
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 response = await fetchImpl(url, {
58
- method: "POST",
59
- headers: { "Content-Type": "application/json" },
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) throw new Error("runtime binding upsert response missing runtime.runtime_instance_id");
85
- if (!internalEventUrl) throw new Error("runtime binding upsert response missing runtime.internal_event_url");
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
- pontiaHome: string,
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 discovered = await resolvePontiaConnection({ pontiaHome, fetch: fetchImpl });
106
- const baseUrl = agentBindingSessionContextUrl(discovered?.bindingUpsertUrl);
107
- if (!baseUrl) return undefined;
108
- const url = new URL(baseUrl);
109
- url.searchParams.set("client_type", "pi");
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
- const internalEventUrl = optionalString(record?.internal_event_url) ?? discovered?.internalEventUrl;
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
@@ -1,7 +1,6 @@
1
1
  export interface SessionContext {
2
2
  sessionId: string;
3
3
  clientType: "pi";
4
- internalEventUrl: string;
5
4
  runtimeInstanceId: string;
6
5
  clientSessionKey?: string;
7
6
  clientSessionFile?: string;
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 { resolvePontiaConnection } from "./discovery.js";
4
- import { asRecord, fetchJson, responseDataRecord } from "./internal-api.js";
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 resolveWorkspaceApi(pontiaHome: string, fetchImpl: typeof fetch): Promise<{ externalApiUrl: string; externalApiToken: string } | undefined> {
15
- const discovered = await resolvePontiaConnection({ pontiaHome, fetch: fetchImpl });
16
- if (!discovered?.externalApiToken) return undefined;
17
- return { externalApiUrl: discovered.externalApiUrl, externalApiToken: discovered.externalApiToken };
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 fetchJson(fetchImpl, `${api.externalApiUrl}/workspaces`, api.externalApiToken);
27
- const workspaces = responseDataRecord(body)?.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) => {
@@ -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
- }