@h-rig/pi-rig 0.0.6-alpha.9 → 0.0.6-alpha.90
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/dist/src/client.d.ts +186 -0
- package/dist/src/client.js +425 -7
- package/dist/src/commands.d.ts +10 -0
- package/dist/src/commands.js +48 -3
- package/dist/src/index.d.ts +59 -0
- package/dist/src/index.js +1292 -34
- package/dist/src/live-mirror.d.ts +46 -0
- package/dist/src/live-mirror.js +223 -0
- package/dist/src/tools.d.ts +19 -0
- package/package.json +10 -2
|
@@ -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
|
+
}
|
package/dist/src/client.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { existsSync, readFileSync } from "fs";
|
|
4
4
|
import { homedir } from "os";
|
|
5
5
|
import { dirname, resolve } from "path";
|
|
6
|
+
import { RIG_PROTOCOL_VERSION, RIG_WS_CHANNELS, RIG_WS_METHODS } from "@rig/contracts";
|
|
6
7
|
function cleanString(value) {
|
|
7
8
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
8
9
|
}
|
|
@@ -79,8 +80,9 @@ function createRigContextFromEnv(env = process.env) {
|
|
|
79
80
|
const discovered = discoverRigContext(env);
|
|
80
81
|
const serverUrl = env.RIG_SERVER_URL ?? env.RIG_SERVER_BASE_URL ?? discovered.serverUrl;
|
|
81
82
|
const projectRoot = env.RIG_PROJECT_ROOT ?? env.PROJECT_RIG_ROOT ?? discovered.projectRoot;
|
|
82
|
-
const authToken = env.RIG_AUTH_TOKEN ?? env.
|
|
83
|
+
const authToken = env.RIG_AUTH_TOKEN ?? env.RIG_SERVER_AUTH_TOKEN ?? discovered.authToken;
|
|
83
84
|
const steeringPollMs = cleanNonNegativeInteger(env.RIG_STEERING_POLL_MS);
|
|
85
|
+
const operatorSession = env.RIG_PI_OPERATOR_SESSION === "1" || env.RIG_PI_OPERATOR_SESSION === "true";
|
|
84
86
|
const active = Boolean(runId || taskId || serverUrl || projectRoot);
|
|
85
87
|
if (!active)
|
|
86
88
|
return { active: false };
|
|
@@ -91,7 +93,8 @@ function createRigContextFromEnv(env = process.env) {
|
|
|
91
93
|
...serverUrl ? { serverUrl } : {},
|
|
92
94
|
...projectRoot ? { projectRoot } : {},
|
|
93
95
|
...authToken ? { authToken } : {},
|
|
94
|
-
...steeringPollMs !== null ? { steeringPollMs } : {}
|
|
96
|
+
...steeringPollMs !== null ? { steeringPollMs } : {},
|
|
97
|
+
...operatorSession ? { operatorSession } : {}
|
|
95
98
|
};
|
|
96
99
|
}
|
|
97
100
|
function joinUrl(baseUrl, pathname) {
|
|
@@ -116,6 +119,8 @@ function requireServerUrl(context) {
|
|
|
116
119
|
}
|
|
117
120
|
return context.serverUrl;
|
|
118
121
|
}
|
|
122
|
+
var BRIDGE_REQUEST_TIMEOUT_MS = 30000;
|
|
123
|
+
var PROTOCOL_CHECK_TIMEOUT_MS = 1e4;
|
|
119
124
|
|
|
120
125
|
class RigBridgeClient {
|
|
121
126
|
context;
|
|
@@ -124,18 +129,46 @@ class RigBridgeClient {
|
|
|
124
129
|
this.context = input.context;
|
|
125
130
|
this.fetchImpl = input.fetchImpl ?? fetch;
|
|
126
131
|
}
|
|
127
|
-
async request(pathname, init) {
|
|
132
|
+
async request(pathname, init, timeoutMs = BRIDGE_REQUEST_TIMEOUT_MS) {
|
|
128
133
|
const headers = new Headers(init?.headers);
|
|
129
134
|
if (this.context.authToken && !headers.has("authorization")) {
|
|
130
135
|
headers.set("authorization", `Bearer ${this.context.authToken}`);
|
|
131
136
|
}
|
|
132
|
-
|
|
137
|
+
if (this.context.projectRoot && !headers.has("x-rig-project-root")) {
|
|
138
|
+
headers.set("x-rig-project-root", this.context.projectRoot);
|
|
139
|
+
}
|
|
140
|
+
const signal = init?.signal ?? (timeoutMs > 0 ? AbortSignal.timeout(timeoutMs) : undefined);
|
|
141
|
+
const response = await this.fetchImpl(joinUrl(requireServerUrl(this.context), pathname), { ...init, headers, signal });
|
|
133
142
|
return readJsonResponse(response);
|
|
134
143
|
}
|
|
135
|
-
async status() {
|
|
136
|
-
const payload = await this.request("/api/server/status");
|
|
144
|
+
async status(timeoutMs) {
|
|
145
|
+
const payload = await this.request("/api/server/status", undefined, timeoutMs);
|
|
137
146
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
138
147
|
}
|
|
148
|
+
async checkProtocolCompatibility() {
|
|
149
|
+
let payload;
|
|
150
|
+
try {
|
|
151
|
+
payload = await this.status(PROTOCOL_CHECK_TIMEOUT_MS);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
return {
|
|
154
|
+
status: "indeterminate",
|
|
155
|
+
serverProtocolVersion: null,
|
|
156
|
+
message: `Rig server protocol check failed: ${error instanceof Error ? error.message : String(error)}`
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const raw = payload.protocolVersion;
|
|
160
|
+
const serverProtocolVersion = typeof raw === "number" && Number.isInteger(raw) && raw >= 0 ? raw : null;
|
|
161
|
+
if (serverProtocolVersion === RIG_PROTOCOL_VERSION) {
|
|
162
|
+
return { status: "compatible", serverProtocolVersion, message: null };
|
|
163
|
+
}
|
|
164
|
+
const serverLabel = serverProtocolVersion === null ? "v0 (no protocolVersion reported)" : `v${serverProtocolVersion}`;
|
|
165
|
+
const updateHint = (serverProtocolVersion ?? 0) < RIG_PROTOCOL_VERSION ? "update the Rig server (upgrade @h-rig/cli / @h-rig/server and restart it)" : "update pi-rig (upgrade @h-rig/pi-rig, or reinstall the extension from this server)";
|
|
166
|
+
return {
|
|
167
|
+
status: "mismatch",
|
|
168
|
+
serverProtocolVersion,
|
|
169
|
+
message: `Rig server speaks protocol ${serverLabel}, this pi-rig speaks v${RIG_PROTOCOL_VERSION} \u2014 ${updateHint}. The Rig bridge is disabled for this session.`
|
|
170
|
+
};
|
|
171
|
+
}
|
|
139
172
|
async listTasks() {
|
|
140
173
|
const payload = await this.request("/api/workspace/tasks");
|
|
141
174
|
return Array.isArray(payload) ? payload.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
|
|
@@ -157,6 +190,20 @@ class RigBridgeClient {
|
|
|
157
190
|
const payload = await this.request(`/api/runs/${encodeURIComponent(runId)}`);
|
|
158
191
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { runId };
|
|
159
192
|
}
|
|
193
|
+
async runLogs(runId = this.context.runId, limit = 20) {
|
|
194
|
+
if (!runId)
|
|
195
|
+
throw new Error("runId is required");
|
|
196
|
+
const payload = await this.request(`/api/runs/${encodeURIComponent(runId)}/logs?limit=${encodeURIComponent(String(limit))}`);
|
|
197
|
+
const entries = payload && typeof payload === "object" && !Array.isArray(payload) ? payload.entries : null;
|
|
198
|
+
return Array.isArray(entries) ? entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
|
|
199
|
+
}
|
|
200
|
+
async runTimeline(runId = this.context.runId, limit = 20) {
|
|
201
|
+
if (!runId)
|
|
202
|
+
throw new Error("runId is required");
|
|
203
|
+
const payload = await this.request(`/api/runs/${encodeURIComponent(runId)}/timeline?limit=${encodeURIComponent(String(limit))}`);
|
|
204
|
+
const entries = payload && typeof payload === "object" && !Array.isArray(payload) ? payload.entries : null;
|
|
205
|
+
return Array.isArray(entries) ? entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
|
|
206
|
+
}
|
|
160
207
|
async steer(message, runId = this.context.runId) {
|
|
161
208
|
if (!runId)
|
|
162
209
|
throw new Error("runId is required");
|
|
@@ -167,6 +214,16 @@ class RigBridgeClient {
|
|
|
167
214
|
});
|
|
168
215
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
|
|
169
216
|
}
|
|
217
|
+
async stop(runId = this.context.runId) {
|
|
218
|
+
if (!runId)
|
|
219
|
+
throw new Error("runId is required");
|
|
220
|
+
const payload = await this.request("/api/runs/stop", {
|
|
221
|
+
method: "POST",
|
|
222
|
+
headers: { "content-type": "application/json" },
|
|
223
|
+
body: JSON.stringify({ runId })
|
|
224
|
+
});
|
|
225
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true, runId };
|
|
226
|
+
}
|
|
170
227
|
async pollSteering(runId = this.context.runId) {
|
|
171
228
|
if (!runId)
|
|
172
229
|
return [];
|
|
@@ -198,8 +255,369 @@ class RigBridgeClient {
|
|
|
198
255
|
const payload = await this.request(`/api/workspace/tasks/${encodeURIComponent(taskId)}`);
|
|
199
256
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
200
257
|
}
|
|
258
|
+
async piProxy(action, init, runId = this.context.runId) {
|
|
259
|
+
if (!runId)
|
|
260
|
+
throw new Error("runId is required");
|
|
261
|
+
return this.request(`/api/runs/${encodeURIComponent(runId)}/pi/${action}`, init);
|
|
262
|
+
}
|
|
263
|
+
async workerCommands(runId = this.context.runId) {
|
|
264
|
+
const payload = await this.piProxy("commands", undefined, runId);
|
|
265
|
+
const commands = Array.isArray(payload) ? payload : payload && typeof payload === "object" && !Array.isArray(payload) && Array.isArray(payload.commands) ? payload.commands : [];
|
|
266
|
+
return commands.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)));
|
|
267
|
+
}
|
|
268
|
+
async workerRunCommand(command, args, runId = this.context.runId) {
|
|
269
|
+
const text = `/${command}${args.trim() ? ` ${args.trim()}` : ""}`;
|
|
270
|
+
const payload = await this.piProxy("commands/run", {
|
|
271
|
+
method: "POST",
|
|
272
|
+
headers: { "content-type": "application/json" },
|
|
273
|
+
body: JSON.stringify({ text })
|
|
274
|
+
}, runId);
|
|
275
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
|
|
276
|
+
}
|
|
277
|
+
async workerShell(command, runId = this.context.runId) {
|
|
278
|
+
const payload = await this.piProxy("shell", {
|
|
279
|
+
method: "POST",
|
|
280
|
+
headers: { "content-type": "application/json" },
|
|
281
|
+
body: JSON.stringify({ text: command })
|
|
282
|
+
}, runId);
|
|
283
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
|
|
284
|
+
}
|
|
285
|
+
async workerAbort(runId = this.context.runId) {
|
|
286
|
+
const payload = await this.piProxy("abort", { method: "POST" }, runId);
|
|
287
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
|
|
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
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function buildRigWebSocketUrl(serverUrl, authToken) {
|
|
317
|
+
const url = new URL(serverUrl);
|
|
318
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
319
|
+
if (authToken) {
|
|
320
|
+
url.searchParams.set("token", authToken);
|
|
321
|
+
}
|
|
322
|
+
return url.toString();
|
|
323
|
+
}
|
|
324
|
+
function defaultWebSocketFactory() {
|
|
325
|
+
const ctor = globalThis.WebSocket;
|
|
326
|
+
if (typeof ctor !== "function")
|
|
327
|
+
return null;
|
|
328
|
+
return (url) => new ctor(url);
|
|
329
|
+
}
|
|
330
|
+
function webSocketEventText(data) {
|
|
331
|
+
if (typeof data === "string")
|
|
332
|
+
return data;
|
|
333
|
+
if (data instanceof Uint8Array)
|
|
334
|
+
return new TextDecoder().decode(data);
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
class RigBridgeSocket {
|
|
339
|
+
context;
|
|
340
|
+
handlers;
|
|
341
|
+
factory;
|
|
342
|
+
reconnectBaseMs;
|
|
343
|
+
reconnectMaxMs;
|
|
344
|
+
socket = null;
|
|
345
|
+
connectedFlag = false;
|
|
346
|
+
closed = false;
|
|
347
|
+
started = false;
|
|
348
|
+
attempt = 0;
|
|
349
|
+
reconnectTimer = null;
|
|
350
|
+
ackSequence = 0;
|
|
351
|
+
constructor(input) {
|
|
352
|
+
this.context = input.context;
|
|
353
|
+
this.handlers = input.handlers ?? {};
|
|
354
|
+
this.factory = input.webSocketFactory ?? defaultWebSocketFactory();
|
|
355
|
+
this.reconnectBaseMs = input.reconnectBaseMs ?? 1000;
|
|
356
|
+
this.reconnectMaxMs = input.reconnectMaxMs ?? 30000;
|
|
357
|
+
}
|
|
358
|
+
get connected() {
|
|
359
|
+
return this.connectedFlag;
|
|
360
|
+
}
|
|
361
|
+
start() {
|
|
362
|
+
if (this.closed)
|
|
363
|
+
return false;
|
|
364
|
+
if (this.started)
|
|
365
|
+
return true;
|
|
366
|
+
if (!this.context.serverUrl || !this.factory)
|
|
367
|
+
return false;
|
|
368
|
+
this.started = true;
|
|
369
|
+
this.connect();
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
close() {
|
|
373
|
+
this.closed = true;
|
|
374
|
+
this.connectedFlag = false;
|
|
375
|
+
if (this.reconnectTimer) {
|
|
376
|
+
clearTimeout(this.reconnectTimer);
|
|
377
|
+
this.reconnectTimer = null;
|
|
378
|
+
}
|
|
379
|
+
try {
|
|
380
|
+
this.socket?.close();
|
|
381
|
+
} catch {}
|
|
382
|
+
this.socket = null;
|
|
383
|
+
}
|
|
384
|
+
ackSteering(runId, ids) {
|
|
385
|
+
if (!this.connectedFlag || !this.socket || ids.length === 0)
|
|
386
|
+
return;
|
|
387
|
+
try {
|
|
388
|
+
this.socket.send(JSON.stringify({
|
|
389
|
+
id: `pi-rig-steer-ack-${++this.ackSequence}`,
|
|
390
|
+
body: { _tag: RIG_WS_METHODS.ackRunSteering, runId, ids }
|
|
391
|
+
}));
|
|
392
|
+
} catch {}
|
|
393
|
+
}
|
|
394
|
+
connect() {
|
|
395
|
+
if (this.closed || !this.context.serverUrl || !this.factory)
|
|
396
|
+
return;
|
|
397
|
+
let socket;
|
|
398
|
+
try {
|
|
399
|
+
socket = this.factory(buildRigWebSocketUrl(this.context.serverUrl, this.context.authToken));
|
|
400
|
+
} catch {
|
|
401
|
+
this.scheduleReconnect();
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
this.socket = socket;
|
|
405
|
+
let gone = false;
|
|
406
|
+
socket.addEventListener("open", () => {
|
|
407
|
+
if (this.closed || gone)
|
|
408
|
+
return;
|
|
409
|
+
this.attempt = 0;
|
|
410
|
+
this.connectedFlag = true;
|
|
411
|
+
this.handlers.onConnect?.();
|
|
412
|
+
});
|
|
413
|
+
const onGone = () => {
|
|
414
|
+
if (gone)
|
|
415
|
+
return;
|
|
416
|
+
gone = true;
|
|
417
|
+
const wasConnected = this.connectedFlag;
|
|
418
|
+
this.connectedFlag = false;
|
|
419
|
+
try {
|
|
420
|
+
socket.close();
|
|
421
|
+
} catch {}
|
|
422
|
+
if (this.socket === socket)
|
|
423
|
+
this.socket = null;
|
|
424
|
+
if (this.closed)
|
|
425
|
+
return;
|
|
426
|
+
if (wasConnected)
|
|
427
|
+
this.handlers.onDisconnect?.();
|
|
428
|
+
this.scheduleReconnect();
|
|
429
|
+
};
|
|
430
|
+
socket.addEventListener("close", onGone);
|
|
431
|
+
socket.addEventListener("error", onGone);
|
|
432
|
+
socket.addEventListener("message", (event) => {
|
|
433
|
+
if (!this.closed)
|
|
434
|
+
this.handleMessage(event);
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
scheduleReconnect() {
|
|
438
|
+
if (this.closed || this.reconnectTimer)
|
|
439
|
+
return;
|
|
440
|
+
const delay = Math.min(this.reconnectBaseMs * 2 ** this.attempt, this.reconnectMaxMs);
|
|
441
|
+
this.attempt += 1;
|
|
442
|
+
const timer = setTimeout(() => {
|
|
443
|
+
this.reconnectTimer = null;
|
|
444
|
+
this.connect();
|
|
445
|
+
}, delay);
|
|
446
|
+
this.reconnectTimer = timer;
|
|
447
|
+
if (typeof timer.unref === "function") {
|
|
448
|
+
timer.unref();
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
handleMessage(event) {
|
|
452
|
+
const text = webSocketEventText(event.data);
|
|
453
|
+
if (!text)
|
|
454
|
+
return;
|
|
455
|
+
let parsed;
|
|
456
|
+
try {
|
|
457
|
+
parsed = JSON.parse(text);
|
|
458
|
+
} catch {
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
462
|
+
return;
|
|
463
|
+
const record = parsed;
|
|
464
|
+
if (record.type !== "push" || typeof record.channel !== "string")
|
|
465
|
+
return;
|
|
466
|
+
const data = record.data && typeof record.data === "object" && !Array.isArray(record.data) ? record.data : null;
|
|
467
|
+
if (record.channel === RIG_WS_CHANNELS.runSteering) {
|
|
468
|
+
if (!data || !this.context.runId || data.runId !== this.context.runId)
|
|
469
|
+
return;
|
|
470
|
+
const message = data.message && typeof data.message === "object" && !Array.isArray(data.message) ? data.message : null;
|
|
471
|
+
if (message)
|
|
472
|
+
this.handlers.onSteeringMessage?.(message);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
if (record.channel === RIG_WS_CHANNELS.event) {
|
|
476
|
+
if (data)
|
|
477
|
+
this.handlers.onRigEvent?.(data);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
if (record.channel === RIG_WS_CHANNELS.snapshotInvalidated) {
|
|
481
|
+
this.handlers.onSnapshotInvalidated?.();
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
function buildRunPiEventsWebSocketUrl(context) {
|
|
486
|
+
if (!context.serverUrl || !context.runId)
|
|
487
|
+
return null;
|
|
488
|
+
const url = new URL(`${context.serverUrl.replace(/\/+$/, "")}/api/runs/${encodeURIComponent(context.runId)}/pi/events`);
|
|
489
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
490
|
+
if (context.authToken)
|
|
491
|
+
url.searchParams.set("token", context.authToken);
|
|
492
|
+
if (context.projectRoot)
|
|
493
|
+
url.searchParams.set("rigProjectRoot", context.projectRoot);
|
|
494
|
+
return url.toString();
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
class RigWorkerEventsSocket {
|
|
498
|
+
context;
|
|
499
|
+
handlers;
|
|
500
|
+
factory;
|
|
501
|
+
reconnectBaseMs;
|
|
502
|
+
reconnectMaxMs;
|
|
503
|
+
socket = null;
|
|
504
|
+
connectedFlag = false;
|
|
505
|
+
closed = false;
|
|
506
|
+
started = false;
|
|
507
|
+
attempt = 0;
|
|
508
|
+
reconnectTimer = null;
|
|
509
|
+
constructor(input) {
|
|
510
|
+
this.context = input.context;
|
|
511
|
+
this.handlers = input.handlers ?? {};
|
|
512
|
+
this.factory = input.webSocketFactory ?? defaultWebSocketFactory();
|
|
513
|
+
this.reconnectBaseMs = input.reconnectBaseMs ?? 1000;
|
|
514
|
+
this.reconnectMaxMs = input.reconnectMaxMs ?? 30000;
|
|
515
|
+
}
|
|
516
|
+
get connected() {
|
|
517
|
+
return this.connectedFlag;
|
|
518
|
+
}
|
|
519
|
+
start() {
|
|
520
|
+
if (this.closed)
|
|
521
|
+
return false;
|
|
522
|
+
if (this.started)
|
|
523
|
+
return true;
|
|
524
|
+
if (!buildRunPiEventsWebSocketUrl(this.context) || !this.factory)
|
|
525
|
+
return false;
|
|
526
|
+
this.started = true;
|
|
527
|
+
this.connect();
|
|
528
|
+
return true;
|
|
529
|
+
}
|
|
530
|
+
close() {
|
|
531
|
+
this.closed = true;
|
|
532
|
+
this.connectedFlag = false;
|
|
533
|
+
if (this.reconnectTimer) {
|
|
534
|
+
clearTimeout(this.reconnectTimer);
|
|
535
|
+
this.reconnectTimer = null;
|
|
536
|
+
}
|
|
537
|
+
try {
|
|
538
|
+
this.socket?.close();
|
|
539
|
+
} catch {}
|
|
540
|
+
this.socket = null;
|
|
541
|
+
}
|
|
542
|
+
connect() {
|
|
543
|
+
const target = buildRunPiEventsWebSocketUrl(this.context);
|
|
544
|
+
if (this.closed || !target || !this.factory)
|
|
545
|
+
return;
|
|
546
|
+
let socket;
|
|
547
|
+
try {
|
|
548
|
+
socket = this.factory(target);
|
|
549
|
+
} catch {
|
|
550
|
+
this.scheduleReconnect();
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
this.socket = socket;
|
|
554
|
+
let gone = false;
|
|
555
|
+
socket.addEventListener("open", () => {
|
|
556
|
+
if (this.closed || gone)
|
|
557
|
+
return;
|
|
558
|
+
this.attempt = 0;
|
|
559
|
+
this.connectedFlag = true;
|
|
560
|
+
this.handlers.onConnect?.();
|
|
561
|
+
});
|
|
562
|
+
const onGone = () => {
|
|
563
|
+
if (gone)
|
|
564
|
+
return;
|
|
565
|
+
gone = true;
|
|
566
|
+
const wasConnected = this.connectedFlag;
|
|
567
|
+
this.connectedFlag = false;
|
|
568
|
+
try {
|
|
569
|
+
socket.close();
|
|
570
|
+
} catch {}
|
|
571
|
+
if (this.socket === socket)
|
|
572
|
+
this.socket = null;
|
|
573
|
+
if (this.closed)
|
|
574
|
+
return;
|
|
575
|
+
if (wasConnected)
|
|
576
|
+
this.handlers.onDisconnect?.();
|
|
577
|
+
this.scheduleReconnect();
|
|
578
|
+
};
|
|
579
|
+
socket.addEventListener("close", onGone);
|
|
580
|
+
socket.addEventListener("error", onGone);
|
|
581
|
+
socket.addEventListener("message", (event) => {
|
|
582
|
+
if (this.closed)
|
|
583
|
+
return;
|
|
584
|
+
const text = webSocketEventText(event.data);
|
|
585
|
+
if (!text)
|
|
586
|
+
return;
|
|
587
|
+
let parsed;
|
|
588
|
+
try {
|
|
589
|
+
parsed = JSON.parse(text);
|
|
590
|
+
} catch {
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
594
|
+
this.handlers.onFrame?.(parsed);
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
scheduleReconnect() {
|
|
599
|
+
if (this.closed || this.reconnectTimer)
|
|
600
|
+
return;
|
|
601
|
+
const delay = Math.min(this.reconnectBaseMs * 2 ** this.attempt, this.reconnectMaxMs);
|
|
602
|
+
this.attempt += 1;
|
|
603
|
+
const timer = setTimeout(() => {
|
|
604
|
+
this.reconnectTimer = null;
|
|
605
|
+
this.connect();
|
|
606
|
+
}, delay);
|
|
607
|
+
this.reconnectTimer = timer;
|
|
608
|
+
if (typeof timer.unref === "function") {
|
|
609
|
+
timer.unref();
|
|
610
|
+
}
|
|
611
|
+
}
|
|
201
612
|
}
|
|
202
613
|
export {
|
|
203
614
|
createRigContextFromEnv,
|
|
204
|
-
|
|
615
|
+
buildRunPiEventsWebSocketUrl,
|
|
616
|
+
buildRigWebSocketUrl,
|
|
617
|
+
RigWorkerEventsSocket,
|
|
618
|
+
RigBridgeSocket,
|
|
619
|
+
RigBridgeClient,
|
|
620
|
+
RIG_PROTOCOL_VERSION,
|
|
621
|
+
PROTOCOL_CHECK_TIMEOUT_MS,
|
|
622
|
+
BRIDGE_REQUEST_TIMEOUT_MS
|
|
205
623
|
};
|
|
@@ -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>;
|