@h-rig/pi-rig 0.0.6-alpha.77 → 0.0.6-alpha.79

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,186 @@
1
+ import { RIG_PROTOCOL_VERSION } from "@rig/contracts";
2
+ export { RIG_PROTOCOL_VERSION };
3
+ export type RigExtensionContext = {
4
+ readonly active: boolean;
5
+ readonly runId?: string;
6
+ readonly taskId?: string;
7
+ readonly serverUrl?: string;
8
+ readonly projectRoot?: string;
9
+ readonly authToken?: string;
10
+ readonly steeringPollMs?: number;
11
+ readonly operatorSession?: boolean;
12
+ };
13
+ export type RigBridgeFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
14
+ /**
15
+ * Result of the client–server protocol handshake performed at extension init.
16
+ *
17
+ * - "compatible": the server reports the same RIG_PROTOCOL_VERSION.
18
+ * - "mismatch": the server reports a different (or no) protocol version; the
19
+ * bridge must disable itself instead of failing on a later request.
20
+ * - "indeterminate": the server could not be reached, so compatibility is
21
+ * unknown; the bridge stays enabled and normal error handling applies.
22
+ */
23
+ export type RigProtocolCheck = {
24
+ readonly status: "compatible" | "mismatch" | "indeterminate";
25
+ readonly serverProtocolVersion: number | null;
26
+ readonly message: string | null;
27
+ };
28
+ export declare function createRigContextFromEnv(env?: Record<string, string | undefined>): RigExtensionContext;
29
+ /** Upper bound on any single bridge request. Observed remote servers answer
30
+ * in 0.5–25s under load; without a bound, one wedged fetch freezes every
31
+ * gate-dependent surface (commands, tools, steering) for the session. */
32
+ export declare const BRIDGE_REQUEST_TIMEOUT_MS = 30000;
33
+ /** Tighter bound for the protocol handshake: a timeout maps to
34
+ * "indeterminate" (bridge stays enabled, handshake retries on the next
35
+ * bridge call), so failing fast here only shortens the blind window. */
36
+ export declare const PROTOCOL_CHECK_TIMEOUT_MS = 10000;
37
+ export declare class RigBridgeClient {
38
+ readonly context: RigExtensionContext;
39
+ readonly fetchImpl: RigBridgeFetch;
40
+ constructor(input: {
41
+ context: RigExtensionContext;
42
+ fetchImpl?: RigBridgeFetch;
43
+ });
44
+ request(pathname: string, init?: RequestInit, timeoutMs?: number): Promise<unknown>;
45
+ status(timeoutMs?: number): Promise<Record<string, unknown>>;
46
+ /**
47
+ * Validate that this pi-rig speaks the same protocol as the Rig server.
48
+ * Reads `protocolVersion` from the server bootstrap/status payload; servers
49
+ * that predate the handshake report none and count as v0 (mismatch).
50
+ */
51
+ checkProtocolCompatibility(): Promise<RigProtocolCheck>;
52
+ listTasks(): Promise<Array<Record<string, unknown>>>;
53
+ runTask(taskId?: string): Promise<{
54
+ runId: string;
55
+ }>;
56
+ attach(runId?: string | undefined): Promise<Record<string, unknown>>;
57
+ runLogs(runId?: string | undefined, limit?: number): Promise<Array<Record<string, unknown>>>;
58
+ runTimeline(runId?: string | undefined, limit?: number): Promise<Array<Record<string, unknown>>>;
59
+ steer(message: string, runId?: string | undefined): Promise<Record<string, unknown>>;
60
+ stop(runId?: string | undefined): Promise<Record<string, unknown>>;
61
+ pollSteering(runId?: string | undefined): Promise<Array<Record<string, unknown>>>;
62
+ consumeSteering(runId?: string | undefined): Promise<Array<Record<string, unknown>>>;
63
+ emitEvent(event: Record<string, unknown>): Promise<Record<string, unknown>>;
64
+ readTaskMetadata(taskId?: string | undefined): Promise<Record<string, unknown>>;
65
+ private piProxy;
66
+ /** The worker session's slash commands (name + description). */
67
+ workerCommands(runId?: string | undefined): Promise<Array<Record<string, unknown>>>;
68
+ /** Run one of the worker session's slash commands. */
69
+ workerRunCommand(command: string, args: string, runId?: string | undefined): Promise<Record<string, unknown>>;
70
+ /** Execute a shell command inside the WORKER's workspace. */
71
+ workerShell(command: string, runId?: string | undefined): Promise<Record<string, unknown>>;
72
+ /** Abort the worker session's current turn. */
73
+ workerAbort(runId?: string | undefined): Promise<Record<string, unknown>>;
74
+ /** The worker session's real capabilities (tools, extensions, hooks, skills, model, cwd). */
75
+ workerCapabilities(runId?: string | undefined): Promise<Record<string, unknown>>;
76
+ /** Answer a worker extension-UI request (select/confirm/input) via the server proxy. */
77
+ workerRespondExtensionUi(requestId: string, response: Record<string, unknown>, runId?: string | undefined): Promise<Record<string, unknown>>;
78
+ /** The run's full Pi session transcript (jsonl) — the cockpit's source of truth. */
79
+ fetchRunSessionFile(runId?: string | undefined): Promise<{
80
+ fileName: string;
81
+ content: string;
82
+ } | null>;
83
+ }
84
+ export type RigWebSocketEvent = {
85
+ readonly data?: unknown;
86
+ };
87
+ /**
88
+ * Minimal WebSocket surface the bridge needs. Matches the global
89
+ * `WebSocket` available in Bun and Node >= 22; tests inject fakes.
90
+ */
91
+ export type RigWebSocketLike = {
92
+ addEventListener(type: "open" | "message" | "close" | "error", listener: (event: RigWebSocketEvent) => void): void;
93
+ send(data: string): void;
94
+ close(): void;
95
+ };
96
+ export type RigWebSocketFactory = (url: string) => RigWebSocketLike;
97
+ export type RigBridgeSocketHandlers = {
98
+ /** Steering message pushed for this context's run (already runId-filtered). */
99
+ readonly onSteeringMessage?: (message: Record<string, unknown>) => void;
100
+ /** Any engine event on the `rig.event` channel (NOT runId-filtered). */
101
+ readonly onRigEvent?: (event: Record<string, unknown>) => void;
102
+ readonly onSnapshotInvalidated?: () => void;
103
+ readonly onConnect?: () => void;
104
+ readonly onDisconnect?: () => void;
105
+ };
106
+ export declare function buildRigWebSocketUrl(serverUrl: string, authToken?: string): string;
107
+ /**
108
+ * Reconnecting WebSocket subscriber for Rig server push channels.
109
+ *
110
+ * - Connects to the same host as the HTTP base URL (`ws://`/`wss://`,
111
+ * token as query param).
112
+ * - Reconnects with exponential backoff (1s doubling to 30s); `connected`
113
+ * lets callers gate their HTTP-polling fallback on socket health.
114
+ * - Steering delivery is confirmed over the same socket via the
115
+ * `rig.ackRunSteering` RPC method.
116
+ */
117
+ export declare class RigBridgeSocket {
118
+ private readonly context;
119
+ private readonly handlers;
120
+ private readonly factory;
121
+ private readonly reconnectBaseMs;
122
+ private readonly reconnectMaxMs;
123
+ private socket;
124
+ private connectedFlag;
125
+ private closed;
126
+ private started;
127
+ private attempt;
128
+ private reconnectTimer;
129
+ private ackSequence;
130
+ constructor(input: {
131
+ readonly context: RigExtensionContext;
132
+ readonly handlers?: RigBridgeSocketHandlers;
133
+ readonly webSocketFactory?: RigWebSocketFactory;
134
+ readonly reconnectBaseMs?: number;
135
+ readonly reconnectMaxMs?: number;
136
+ });
137
+ get connected(): boolean;
138
+ /** Returns false when no server URL or WebSocket implementation exists. */
139
+ start(): boolean;
140
+ close(): void;
141
+ /** Confirm steering delivery over the socket. Fire-and-forget: on failure the
142
+ * polling fallback re-delivers and the caller's dedupe set absorbs it. */
143
+ ackSteering(runId: string, ids: readonly string[]): void;
144
+ private connect;
145
+ private scheduleReconnect;
146
+ private handleMessage;
147
+ }
148
+ /** WS URL for the server's worker-session event proxy
149
+ * (`/api/runs/:id/pi/events` → daemon events). Token rides the query param
150
+ * (the upgrade handler reads it); rigProjectRoot scopes multi-root servers. */
151
+ export declare function buildRunPiEventsWebSocketUrl(context: RigExtensionContext): string | null;
152
+ /**
153
+ * Reconnecting subscriber for the worker session's event stream (the same
154
+ * frames the worker daemon emits: `pi.event` envelopes, `status.update`,
155
+ * `activity.update`). This is the live-mirror transport: the operator
156
+ * console renders worker turns from these frames.
157
+ *
158
+ * The proxy returns 202 while the session is still starting and the run may
159
+ * outlive several daemon generations, so reconnection uses the same backoff
160
+ * discipline as RigBridgeSocket.
161
+ */
162
+ export declare class RigWorkerEventsSocket {
163
+ private readonly context;
164
+ private readonly handlers;
165
+ private readonly factory;
166
+ private readonly reconnectBaseMs;
167
+ private readonly reconnectMaxMs;
168
+ private socket;
169
+ private connectedFlag;
170
+ private closed;
171
+ private started;
172
+ private attempt;
173
+ private reconnectTimer;
174
+ constructor(input: {
175
+ readonly context: RigExtensionContext;
176
+ readonly handlers?: RigWorkerEventsSocket["handlers"];
177
+ readonly webSocketFactory?: RigWebSocketFactory;
178
+ readonly reconnectBaseMs?: number;
179
+ readonly reconnectMaxMs?: number;
180
+ });
181
+ get connected(): boolean;
182
+ start(): boolean;
183
+ close(): void;
184
+ private connect;
185
+ private scheduleReconnect;
186
+ }
@@ -266,10 +266,11 @@ class RigBridgeClient {
266
266
  return commands.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)));
267
267
  }
268
268
  async workerRunCommand(command, args, runId = this.context.runId) {
269
+ const text = `/${command}${args.trim() ? ` ${args.trim()}` : ""}`;
269
270
  const payload = await this.piProxy("commands/run", {
270
271
  method: "POST",
271
272
  headers: { "content-type": "application/json" },
272
- body: JSON.stringify({ command, args })
273
+ body: JSON.stringify({ text })
273
274
  }, runId);
274
275
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
275
276
  }
@@ -277,7 +278,7 @@ class RigBridgeClient {
277
278
  const payload = await this.piProxy("shell", {
278
279
  method: "POST",
279
280
  headers: { "content-type": "application/json" },
280
- body: JSON.stringify({ command })
281
+ body: JSON.stringify({ text: command })
281
282
  }, runId);
282
283
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
283
284
  }
@@ -285,6 +286,32 @@ class RigBridgeClient {
285
286
  const payload = await this.piProxy("abort", { method: "POST" }, runId);
286
287
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
287
288
  }
289
+ async workerCapabilities(runId = this.context.runId) {
290
+ const payload = await this.piProxy("capabilities", undefined, runId);
291
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
292
+ }
293
+ async workerRespondExtensionUi(requestId, response, runId = this.context.runId) {
294
+ const payload = await this.piProxy("extension-ui/respond", {
295
+ method: "POST",
296
+ headers: { "content-type": "application/json" },
297
+ body: JSON.stringify({ requestId, ...response })
298
+ }, runId);
299
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
300
+ }
301
+ async fetchRunSessionFile(runId = this.context.runId) {
302
+ if (!runId)
303
+ return null;
304
+ const payload = await this.request(`/api/runs/${encodeURIComponent(runId)}/session-file`);
305
+ if (!payload || typeof payload !== "object" || Array.isArray(payload))
306
+ return null;
307
+ const record = payload;
308
+ if (record.ok !== true || typeof record.content !== "string" || !record.content.trim())
309
+ return null;
310
+ return {
311
+ fileName: typeof record.fileName === "string" && record.fileName.trim() ? record.fileName : `rig-run-${runId}.jsonl`,
312
+ content: record.content
313
+ };
314
+ }
288
315
  }
289
316
  function buildRigWebSocketUrl(serverUrl, authToken) {
290
317
  const url = new URL(serverUrl);
@@ -0,0 +1,10 @@
1
+ import type { RigExtensionContext, RigBridgeClient } from "./client";
2
+ export type RigCommandDefinition = {
3
+ readonly description: string;
4
+ readonly handler: (args: string, ctx: unknown) => Promise<void>;
5
+ };
6
+ export declare function createRigSlashCommands(input: {
7
+ readonly context: RigExtensionContext;
8
+ readonly client: RigBridgeClient;
9
+ readonly notify?: (message: string, level?: "info" | "error") => void;
10
+ }): Record<string, RigCommandDefinition>;
@@ -0,0 +1,59 @@
1
+ import { RigBridgeClient, type RigExtensionContext, type RigProtocolCheck, type RigWebSocketFactory } from "./client";
2
+ export type MinimalPiApi = {
3
+ registerCommand?: (name: string, command: {
4
+ description?: string;
5
+ handler: (args: string, ctx: unknown) => Promise<void>;
6
+ }) => void;
7
+ registerTool?: (tool: {
8
+ name: string;
9
+ } & Record<string, unknown>) => void;
10
+ on?: (eventName: string, handler: (event: unknown, ctx: unknown) => unknown) => void;
11
+ sendUserMessage?: (content: string, options?: {
12
+ deliverAs?: "steer" | "followUp" | "nextTurn";
13
+ triggerTurn?: boolean;
14
+ }) => void | Promise<void>;
15
+ /** Stock Pi custom-message injection: rendered in the transcript without
16
+ * triggering a turn. The operator console's live worker mirror rides this. */
17
+ sendMessage?: (message: {
18
+ customType: string;
19
+ content: string;
20
+ display?: boolean;
21
+ details?: unknown;
22
+ }, options?: {
23
+ triggerTurn?: boolean;
24
+ }) => void | Promise<void>;
25
+ /** Stock custom-message renderer registration: the live mirror renders
26
+ * worker turns through Pi's own components via this seam. */
27
+ registerMessageRenderer?: (customType: string, renderer: (message: unknown, options: {
28
+ expanded: boolean;
29
+ }, theme: unknown) => unknown) => void;
30
+ };
31
+ export type PiRigExtensionState = RigExtensionContext & {
32
+ readonly client: RigBridgeClient;
33
+ /** Test seam: lets the suite fake the push socket. Defaults to the global WebSocket. */
34
+ readonly webSocketFactory?: RigWebSocketFactory;
35
+ };
36
+ export declare function createPiRigExtensionState(input?: {
37
+ readonly env?: Record<string, string | undefined>;
38
+ readonly fetchImpl?: typeof fetch;
39
+ readonly webSocketFactory?: RigWebSocketFactory;
40
+ }): PiRigExtensionState;
41
+ export type PiRigBridgeGate = {
42
+ readonly allowed: boolean;
43
+ readonly message: string | null;
44
+ /** Handshake outcome: "compatible" means the server speaks this protocol
45
+ * version and therefore supports the WS push surface. */
46
+ readonly status: RigProtocolCheck["status"];
47
+ };
48
+ export type PiRigBridgeGateCheck = (ctx: unknown) => Promise<PiRigBridgeGate>;
49
+ /** Live refresh control handed to the operator widget by the WS bridge:
50
+ * while the socket is up, pushes (rig.event / snapshotInvalidated) drive the
51
+ * widget instead of the 1s status poll. */
52
+ export type OperatorLiveRefresh = {
53
+ isConnected(): boolean;
54
+ /** Returns true (and resets) when a push arrived since the last check. */
55
+ consumePushTrigger(): boolean;
56
+ };
57
+ export default function createPiRigExtension(pi: MinimalPiApi, options?: {
58
+ state?: PiRigExtensionState;
59
+ }): void;