@pontia/pi-client-plugin 0.1.0

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.
@@ -0,0 +1,87 @@
1
+ function textFromContent(content: unknown): string | undefined {
2
+ if (typeof content === "string") return content;
3
+ if (!Array.isArray(content)) return undefined;
4
+
5
+ const text = content
6
+ .map((part) => {
7
+ if (!part || typeof part !== "object") return "";
8
+ const record = part as Record<string, unknown>;
9
+ return record.type === "text" && typeof record.text === "string" ? record.text : "";
10
+ })
11
+ .join("");
12
+
13
+ return text.length > 0 ? text : undefined;
14
+ }
15
+
16
+ export function assistantTextFromMessage(message: unknown): string | undefined {
17
+ if (!message || typeof message !== "object") return undefined;
18
+ const record = message as Record<string, unknown>;
19
+ if (record.role !== "assistant") return undefined;
20
+ return textFromContent(record.content);
21
+ }
22
+
23
+ export function assistantDeltaFromEvent(event: unknown): string | undefined {
24
+ if (!event || typeof event !== "object") return undefined;
25
+ const record = event as Record<string, unknown>;
26
+ const streamEvent = record.assistantMessageEvent;
27
+ if (streamEvent && typeof streamEvent === "object") {
28
+ const streamRecord = streamEvent as Record<string, unknown>;
29
+ for (const key of ["text_delta", "textDelta", "delta", "text"] as const) {
30
+ const value = streamRecord[key];
31
+ if (typeof value === "string" && value.length > 0) return value;
32
+ }
33
+ }
34
+ return undefined;
35
+ }
36
+
37
+ const transcriptBoundaryStreamEventTypes = new Set([
38
+ "thinking_start",
39
+ "thinking_end",
40
+ "text_start",
41
+ "text_end",
42
+ "toolcall_start",
43
+ "toolcall_end",
44
+ ]);
45
+
46
+ export function isTranscriptBoundaryMessageUpdate(event: unknown): boolean {
47
+ if (!event || typeof event !== "object") return false;
48
+ const streamEvent = (event as Record<string, unknown>).assistantMessageEvent;
49
+ if (!streamEvent || typeof streamEvent !== "object") return false;
50
+ const type = (streamEvent as Record<string, unknown>).type;
51
+ return typeof type === "string" && transcriptBoundaryStreamEventTypes.has(type);
52
+ }
53
+
54
+ interface PiAgentEndEventLike {
55
+ messages: unknown[];
56
+ }
57
+
58
+ function lastAssistantMessageFromAgentEnd(event: PiAgentEndEventLike): Record<string, unknown> | undefined {
59
+ for (let index = event.messages.length - 1; index >= 0; index -= 1) {
60
+ const message = event.messages[index];
61
+ if (message && typeof message === "object" && (message as Record<string, unknown>).role === "assistant") {
62
+ return message as Record<string, unknown>;
63
+ }
64
+ }
65
+ return undefined;
66
+ }
67
+
68
+ export function agentEndWasInterrupted(event: PiAgentEndEventLike): boolean {
69
+ return lastAssistantMessageFromAgentEnd(event)?.stopReason === "aborted";
70
+ }
71
+
72
+ export function errorMessageFromAgentEnd(event: PiAgentEndEventLike): string | undefined {
73
+ const assistant = lastAssistantMessageFromAgentEnd(event);
74
+ if (assistant?.stopReason !== "error") return undefined;
75
+ return typeof assistant.errorMessage === "string" && assistant.errorMessage.length > 0
76
+ ? assistant.errorMessage
77
+ : "pi agent reported an error";
78
+ }
79
+
80
+ export function lastAssistantTextFromMessages(messages: unknown): string | undefined {
81
+ if (!Array.isArray(messages)) return undefined;
82
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
83
+ const text = assistantTextFromMessage(messages[index]);
84
+ if (text) return text;
85
+ }
86
+ return undefined;
87
+ }
package/src/profile.ts ADDED
@@ -0,0 +1,29 @@
1
+ import { resolvePontiaConnection } from "./discovery.js";
2
+ import { fetchJson, optionalString, responseDataRecord } from "./internal-api.js";
3
+
4
+ export async function loadProfileSystemPrompt(
5
+ pontiaHome: string,
6
+ fetchImpl: typeof fetch,
7
+ sessionId?: string,
8
+ ): Promise<string | undefined> {
9
+ const connection = await resolvePontiaConnection({ pontiaHome, fetch: fetchImpl });
10
+ const baseUrl = connection?.externalApiUrl;
11
+ const token = connection?.externalApiToken;
12
+ if (!baseUrl || !token || !sessionId) return undefined;
13
+
14
+ const sessionBody = await fetchJson(fetchImpl, `${baseUrl}/sessions/${encodeURIComponent(sessionId)}`, token);
15
+ const session = responseDataRecord(sessionBody)?.session;
16
+ if (!session || typeof session !== "object" || Array.isArray(session)) return undefined;
17
+ const sessionRecord = session as Record<string, unknown>;
18
+ const profileId = optionalString(sessionRecord.execution_profile_id);
19
+ const profileVersion = optionalString(sessionRecord.execution_profile_version);
20
+ if (!profileId) return undefined;
21
+
22
+ const profileUrl = profileVersion
23
+ ? `${baseUrl}/agent-profiles/${encodeURIComponent(profileId)}/versions/${encodeURIComponent(profileVersion)}`
24
+ : `${baseUrl}/agent-profiles/${encodeURIComponent(profileId)}`;
25
+ const profileBody = await fetchJson(fetchImpl, profileUrl, token);
26
+ const profile = responseDataRecord(profileBody)?.agent_profile;
27
+ if (!profile || typeof profile !== "object" || Array.isArray(profile)) return undefined;
28
+ return optionalString((profile as Record<string, unknown>).system_prompt_template);
29
+ }
@@ -0,0 +1,67 @@
1
+ import { appendDiagnostic } from "./diagnostics.js";
2
+ import type { InternalEvent } from "./events.js";
3
+
4
+ type FetchLike = typeof fetch;
5
+
6
+ export interface EventReportResult {
7
+ accepted: boolean;
8
+ eventId?: string;
9
+ turnId?: string;
10
+ }
11
+
12
+ export interface EventReporterOptions {
13
+ fetch?: FetchLike;
14
+ logFile: string;
15
+ }
16
+
17
+ export class EventReporter {
18
+ private readonly fetchImpl: FetchLike;
19
+ private readonly logFile: string;
20
+
21
+ constructor(options: EventReporterOptions) {
22
+ this.fetchImpl = options.fetch ?? fetch;
23
+ this.logFile = options.logFile;
24
+ }
25
+
26
+ async report(context: { internalEventUrl: string }, event: InternalEvent): Promise<EventReportResult> {
27
+ try {
28
+ const response = await this.fetchImpl(context.internalEventUrl, {
29
+ method: "POST",
30
+ headers: { "Content-Type": "application/json" },
31
+ body: JSON.stringify(event),
32
+ });
33
+
34
+ const body = await response.text().catch(() => "");
35
+ if (!response.ok) {
36
+ await appendDiagnostic(this.logFile, {
37
+ level: "error",
38
+ code: "internal_event_post_failed",
39
+ message: `Internal Event API rejected ${event.type}: ${response.status} ${response.statusText}`,
40
+ details: { event_type: event.type, status: response.status, body },
41
+ });
42
+ return { accepted: false };
43
+ }
44
+
45
+ let responseBody: unknown;
46
+ try {
47
+ responseBody = body ? JSON.parse(body) : null;
48
+ } catch {
49
+ responseBody = null;
50
+ }
51
+ const record = responseBody && typeof responseBody === "object" ? responseBody as Record<string, unknown> : undefined;
52
+ return {
53
+ accepted: true,
54
+ eventId: typeof record?.event_id === "string" ? record.event_id : undefined,
55
+ turnId: typeof record?.turn_id === "string" ? record.turn_id : undefined,
56
+ };
57
+ } catch (error) {
58
+ await appendDiagnostic(this.logFile, {
59
+ level: "error",
60
+ code: "internal_event_post_exception",
61
+ message: `Internal Event API POST failed for ${event.type}`,
62
+ details: { event_type: event.type, error: error instanceof Error ? error.message : String(error) },
63
+ });
64
+ return { accepted: false };
65
+ }
66
+ }
67
+ }
@@ -0,0 +1,133 @@
1
+ import type { EnvLike } from "./context.js";
2
+ import { resolvePontiaConnection } from "./discovery.js";
3
+ import { asRecord, optionalString, parseJsonResponse } from "./internal-api.js";
4
+ import type { SessionContext } from "./session.js";
5
+
6
+ export type PiSessionDetails = Pick<SessionContext, "clientSessionKey" | "clientSessionFile" | "clientSessionDir" | "clientCwd">;
7
+
8
+ function callSessionManagerString(sessionManager: unknown, method: string): string | undefined {
9
+ if (!sessionManager || typeof sessionManager !== "object") return undefined;
10
+ const fn = (sessionManager as Record<string, unknown>)[method];
11
+ if (typeof fn !== "function") return undefined;
12
+ try {
13
+ return optionalString(fn.call(sessionManager));
14
+ } catch {
15
+ return undefined;
16
+ }
17
+ }
18
+
19
+ export function piSessionDetailsFromHookContext(ctx: unknown): PiSessionDetails {
20
+ const sessionManager = ctx && typeof ctx === "object" ? (ctx as Record<string, unknown>).sessionManager : undefined;
21
+ return {
22
+ clientSessionKey: callSessionManagerString(sessionManager, "getSessionId"),
23
+ clientSessionFile: callSessionManagerString(sessionManager, "getSessionFile"),
24
+ clientSessionDir: callSessionManagerString(sessionManager, "getSessionDir"),
25
+ clientCwd: callSessionManagerString(sessionManager, "getCwd"),
26
+ };
27
+ }
28
+
29
+ function agentBindingSessionContextUrl(discoveredBindingUpsertUrl?: string): string | undefined {
30
+ return discoveredBindingUpsertUrl?.replace(
31
+ /\/runtime-bindings\/upsert\/?$/,
32
+ "/agent-bindings/session-context",
33
+ );
34
+ }
35
+
36
+ function tmuxBindingFromEnv(env: EnvLike): { socket_path: string; pane_id: string } | undefined {
37
+ const tmux = optionalString(env.TMUX);
38
+ const paneId = optionalString(env.TMUX_PANE);
39
+ const socketPath = optionalString(tmux?.split(",", 1)[0]);
40
+ if (!socketPath || !paneId) return undefined;
41
+ return { socket_path: socketPath, pane_id: paneId };
42
+ }
43
+
44
+ export async function bindSession(
45
+ pontiaHome: string,
46
+ env: EnvLike,
47
+ fetchImpl: typeof fetch,
48
+ sessionDetails: PiSessionDetails,
49
+ options: { startKind?: "fork"; parentSessionId?: string; runtimeInstanceId?: string } = {},
50
+ ): Promise<SessionContext | undefined> {
51
+ 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
+ const tmux = tmuxBindingFromEnv(env);
57
+ const response = await fetchImpl(url, {
58
+ method: "POST",
59
+ headers: { "Content-Type": "application/json" },
60
+ body: JSON.stringify({
61
+ client_type: "pi",
62
+ client_session_key: sessionDetails.clientSessionKey,
63
+ client_session_file: sessionDetails.clientSessionFile,
64
+ client_session_dir: sessionDetails.clientSessionDir,
65
+ client_cwd: sessionDetails.clientCwd,
66
+ launch_cwd: sessionDetails.clientCwd,
67
+ start_command: "pi",
68
+ ...(options.startKind ? { start_kind: options.startKind } : {}),
69
+ ...(options.parentSessionId ? { parent_session_id: options.parentSessionId } : {}),
70
+ ...(options.runtimeInstanceId ? { runtime_instance_id: options.runtimeInstanceId } : {}),
71
+ ...(tmux ? { tmux } : {}),
72
+ }),
73
+ });
74
+ const body = await parseJsonResponse(response);
75
+ if (!response.ok) throw new Error(`runtime binding upsert failed: ${response.status} ${response.statusText}`);
76
+
77
+ const record = asRecord(body);
78
+ const session = asRecord(record?.session);
79
+ const runtime = asRecord(record?.runtime);
80
+ const sessionId = optionalString(session?.session_id);
81
+ const resolvedRuntimeInstanceId = optionalString(runtime?.runtime_instance_id);
82
+ const internalEventUrl = optionalString(runtime?.internal_event_url) ?? discovered?.internalEventUrl;
83
+ 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");
86
+ return {
87
+ sessionId,
88
+ clientType: "pi",
89
+ internalEventUrl,
90
+ runtimeInstanceId: resolvedRuntimeInstanceId,
91
+ ...sessionDetails,
92
+ };
93
+ }
94
+
95
+ export interface ExistingPiSessionContext extends SessionContext {
96
+ sessionState: string;
97
+ }
98
+
99
+ export async function loadExistingSessionContext(
100
+ pontiaHome: string,
101
+ fetchImpl: typeof fetch,
102
+ sessionDetails: PiSessionDetails,
103
+ ): Promise<ExistingPiSessionContext | undefined> {
104
+ 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);
117
+ const sessionId = optionalString(record?.session_id);
118
+ const sessionState = optionalString(record?.session_state);
119
+ const clientType = optionalString(record?.client_type);
120
+ 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) {
123
+ throw new Error("agent binding session context lookup returned an invalid context");
124
+ }
125
+ return {
126
+ sessionId,
127
+ sessionState,
128
+ clientType: "pi",
129
+ runtimeInstanceId,
130
+ internalEventUrl,
131
+ ...sessionDetails,
132
+ };
133
+ }
package/src/session.ts ADDED
@@ -0,0 +1,10 @@
1
+ export interface SessionContext {
2
+ sessionId: string;
3
+ clientType: "pi";
4
+ internalEventUrl: string;
5
+ runtimeInstanceId: string;
6
+ clientSessionKey?: string;
7
+ clientSessionFile?: string;
8
+ clientSessionDir?: string;
9
+ clientCwd?: string;
10
+ }
@@ -0,0 +1,34 @@
1
+ import { realpath } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ import { resolvePontiaConnection } from "./discovery.js";
4
+ import { asRecord, fetchJson, responseDataRecord } from "./internal-api.js";
5
+
6
+ async function canonicalPath(path: string): Promise<string> {
7
+ try {
8
+ return await realpath(path);
9
+ } catch {
10
+ return resolve(path);
11
+ }
12
+ }
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> {
21
+ if (!clientCwd) return false;
22
+ const api = await resolveWorkspaceApi(pontiaHome, fetchImpl);
23
+ if (!api) return undefined;
24
+
25
+ const workspacePath = await canonicalPath(clientCwd);
26
+ const body = await fetchJson(fetchImpl, `${api.externalApiUrl}/workspaces`, api.externalApiToken);
27
+ const workspaces = responseDataRecord(body)?.workspaces;
28
+ if (!Array.isArray(workspaces)) return false;
29
+
30
+ return workspaces.some((workspace) => {
31
+ const record = asRecord(workspace);
32
+ return record?.state === "active" && record.canonical_path === workspacePath;
33
+ });
34
+ }