@h-rig/cli 0.0.6-alpha.4 → 0.0.6-alpha.41
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/bin/rig.js +3905 -1193
- package/dist/src/commands/_cli-format.js +369 -0
- package/dist/src/commands/_connection-state.js +1 -3
- package/dist/src/commands/_doctor-checks.js +13 -27
- package/dist/src/commands/_help-catalog.js +445 -0
- package/dist/src/commands/_operator-surface.js +220 -0
- package/dist/src/commands/_operator-view.js +916 -56
- package/dist/src/commands/_parsers.js +0 -2
- package/dist/src/commands/_pi-frontend.js +880 -0
- package/dist/src/commands/_pi-install.js +4 -3
- package/dist/src/commands/_pi-remote-session.js +665 -0
- package/dist/src/commands/_pi-worker-bridge-extension.js +676 -0
- package/dist/src/commands/_policy.js +0 -2
- package/dist/src/commands/_preflight.js +32 -109
- package/dist/src/commands/_run-driver-helpers.js +2 -2
- package/dist/src/commands/_server-client.js +152 -31
- package/dist/src/commands/_snapshot-upload.js +8 -23
- package/dist/src/commands/_task-picker.js +44 -16
- package/dist/src/commands/agent.js +8 -9
- package/dist/src/commands/browser.js +4 -6
- package/dist/src/commands/connect.js +132 -25
- package/dist/src/commands/dist.js +4 -6
- package/dist/src/commands/doctor.js +13 -27
- package/dist/src/commands/github.js +10 -25
- package/dist/src/commands/inbox.js +351 -31
- package/dist/src/commands/init.js +298 -71
- package/dist/src/commands/inspect.js +237 -23
- package/dist/src/commands/inspector.js +2 -4
- package/dist/src/commands/pi.js +168 -0
- package/dist/src/commands/plugin.js +81 -22
- package/dist/src/commands/profile-and-review.js +8 -10
- package/dist/src/commands/queue.js +2 -3
- package/dist/src/commands/remote.js +18 -20
- package/dist/src/commands/repo-git-harness.js +6 -8
- package/dist/src/commands/run.js +1214 -123
- package/dist/src/commands/server.js +222 -33
- package/dist/src/commands/setup.js +17 -37
- package/dist/src/commands/task-report-bug.js +5 -7
- package/dist/src/commands/task-run-driver.js +630 -71
- package/dist/src/commands/task.js +1653 -252
- package/dist/src/commands/test.js +3 -5
- package/dist/src/commands/workspace.js +4 -6
- package/dist/src/commands.js +3884 -1166
- package/dist/src/index.js +3898 -1189
- package/dist/src/launcher.js +5 -3
- package/dist/src/report-bug.js +3 -3
- package/dist/src/runner.js +5 -19
- package/package.json +6 -4
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
// packages/cli/src/commands/_operator-view.ts
|
|
3
|
-
import { createInterface } from "readline";
|
|
4
|
-
|
|
5
2
|
// packages/cli/src/commands/_server-client.ts
|
|
6
|
-
import { spawnSync } from "child_process";
|
|
7
3
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
8
4
|
import { resolve as resolve2 } from "path";
|
|
9
5
|
|
|
@@ -11,8 +7,6 @@ import { resolve as resolve2 } from "path";
|
|
|
11
7
|
import { EventBus } from "@rig/runtime/control-plane/runtime/events";
|
|
12
8
|
import { CliError } from "@rig/runtime/control-plane/errors";
|
|
13
9
|
import { evaluate, loadPolicy, resolveAction } from "@rig/runtime/control-plane/runtime/guard";
|
|
14
|
-
import { PluginManager } from "@rig/runtime/control-plane/runtime/plugins";
|
|
15
|
-
import { loadRuntimeContextFromEnv } from "@rig/runtime/control-plane/runtime/context";
|
|
16
10
|
import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
|
|
17
11
|
import { CliError as CliError2 } from "@rig/runtime/control-plane/errors";
|
|
18
12
|
|
|
@@ -96,13 +90,13 @@ function resolveSelectedConnection(projectRoot, options = {}) {
|
|
|
96
90
|
const global = readGlobalConnections(options);
|
|
97
91
|
const connection = global.connections[repo.selected];
|
|
98
92
|
if (!connection) {
|
|
99
|
-
throw new CliError2(`Selected Rig
|
|
93
|
+
throw new CliError2(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
|
|
100
94
|
}
|
|
101
95
|
return { alias: repo.selected, connection };
|
|
102
96
|
}
|
|
103
97
|
|
|
104
98
|
// packages/cli/src/commands/_server-client.ts
|
|
105
|
-
var
|
|
99
|
+
var scopedGitHubBearerTokens = new Map;
|
|
106
100
|
function cleanToken(value) {
|
|
107
101
|
const trimmed = value?.trim();
|
|
108
102
|
return trimmed ? trimmed : null;
|
|
@@ -119,25 +113,13 @@ function readPrivateRemoteSessionToken(projectRoot) {
|
|
|
119
113
|
}
|
|
120
114
|
}
|
|
121
115
|
function readGitHubBearerTokenForRemote(projectRoot) {
|
|
122
|
-
|
|
123
|
-
|
|
116
|
+
const scopedKey = resolve2(projectRoot);
|
|
117
|
+
if (scopedGitHubBearerTokens.has(scopedKey))
|
|
118
|
+
return scopedGitHubBearerTokens.get(scopedKey) ?? null;
|
|
124
119
|
const privateSession = readPrivateRemoteSessionToken(projectRoot);
|
|
125
|
-
if (privateSession)
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
const envToken = cleanToken(process.env.RIG_GITHUB_TOKEN) ?? cleanToken(process.env.GITHUB_TOKEN) ?? cleanToken(process.env.GH_TOKEN);
|
|
130
|
-
if (envToken) {
|
|
131
|
-
cachedGitHubBearerToken = envToken;
|
|
132
|
-
return cachedGitHubBearerToken;
|
|
133
|
-
}
|
|
134
|
-
const result = spawnSync("gh", ["auth", "token"], {
|
|
135
|
-
encoding: "utf8",
|
|
136
|
-
timeout: 5000,
|
|
137
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
138
|
-
});
|
|
139
|
-
cachedGitHubBearerToken = result.status === 0 ? cleanToken(result.stdout) : null;
|
|
140
|
-
return cachedGitHubBearerToken;
|
|
120
|
+
if (privateSession)
|
|
121
|
+
return privateSession;
|
|
122
|
+
return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
|
|
141
123
|
}
|
|
142
124
|
async function ensureServerForCli(projectRoot) {
|
|
143
125
|
try {
|
|
@@ -218,6 +200,15 @@ async function getRunLogsViaServer(context, runId, options = {}) {
|
|
|
218
200
|
const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
|
|
219
201
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { entries: [] };
|
|
220
202
|
}
|
|
203
|
+
async function getRunTimelineViaServer(context, runId, options = {}) {
|
|
204
|
+
const url = new URL(`http://rig.local/api/runs/${encodeURIComponent(runId)}/timeline`);
|
|
205
|
+
if (options.limit !== undefined)
|
|
206
|
+
url.searchParams.set("limit", String(options.limit));
|
|
207
|
+
if (options.cursor)
|
|
208
|
+
url.searchParams.set("cursor", options.cursor);
|
|
209
|
+
const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
|
|
210
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { entries: [] };
|
|
211
|
+
}
|
|
221
212
|
async function stopRunViaServer(context, runId) {
|
|
222
213
|
const payload = await requestServerJson(context, "/api/runs/stop", {
|
|
223
214
|
method: "POST",
|
|
@@ -234,9 +225,69 @@ async function steerRunViaServer(context, runId, message) {
|
|
|
234
225
|
});
|
|
235
226
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
|
|
236
227
|
}
|
|
228
|
+
async function getRunPiSessionViaServer(context, runId) {
|
|
229
|
+
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi`);
|
|
230
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
231
|
+
}
|
|
232
|
+
async function getRunPiMessagesViaServer(context, runId) {
|
|
233
|
+
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/messages`);
|
|
234
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { messages: [] };
|
|
235
|
+
}
|
|
236
|
+
async function getRunPiStatusViaServer(context, runId) {
|
|
237
|
+
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/status`);
|
|
238
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
239
|
+
}
|
|
240
|
+
async function getRunPiCommandsViaServer(context, runId) {
|
|
241
|
+
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/commands`);
|
|
242
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { commands: [] };
|
|
243
|
+
}
|
|
244
|
+
async function sendRunPiPromptViaServer(context, runId, text, streamingBehavior) {
|
|
245
|
+
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/prompt`, {
|
|
246
|
+
method: "POST",
|
|
247
|
+
headers: { "content-type": "application/json" },
|
|
248
|
+
body: JSON.stringify({ text, streamingBehavior })
|
|
249
|
+
});
|
|
250
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { accepted: true };
|
|
251
|
+
}
|
|
252
|
+
async function sendRunPiShellViaServer(context, runId, text) {
|
|
253
|
+
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/shell`, {
|
|
254
|
+
method: "POST",
|
|
255
|
+
headers: { "content-type": "application/json" },
|
|
256
|
+
body: JSON.stringify({ text })
|
|
257
|
+
});
|
|
258
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { accepted: true };
|
|
259
|
+
}
|
|
260
|
+
async function runRunPiCommandViaServer(context, runId, text) {
|
|
261
|
+
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/commands/run`, {
|
|
262
|
+
method: "POST",
|
|
263
|
+
headers: { "content-type": "application/json" },
|
|
264
|
+
body: JSON.stringify({ text })
|
|
265
|
+
});
|
|
266
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { type: "done" };
|
|
267
|
+
}
|
|
268
|
+
async function respondRunPiExtensionUiViaServer(context, runId, requestId, valueOrCancel) {
|
|
269
|
+
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/extension-ui/respond`, {
|
|
270
|
+
method: "POST",
|
|
271
|
+
headers: { "content-type": "application/json" },
|
|
272
|
+
body: JSON.stringify({ requestId, ...valueOrCancel })
|
|
273
|
+
});
|
|
274
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { accepted: true };
|
|
275
|
+
}
|
|
276
|
+
async function abortRunPiViaServer(context, runId) {
|
|
277
|
+
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/abort`, { method: "POST" });
|
|
278
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { aborted: true };
|
|
279
|
+
}
|
|
280
|
+
async function buildRunPiEventsWebSocketUrl(context, runId) {
|
|
281
|
+
const server = await ensureServerForCli(context.projectRoot);
|
|
282
|
+
const url = new URL(`${server.baseUrl.replace(/\/+$/, "")}/api/runs/${encodeURIComponent(runId)}/pi/events`);
|
|
283
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
284
|
+
if (server.authToken)
|
|
285
|
+
url.searchParams.set("token", server.authToken);
|
|
286
|
+
return url.toString();
|
|
287
|
+
}
|
|
237
288
|
|
|
238
|
-
// packages/cli/src/commands/_operator-
|
|
239
|
-
|
|
289
|
+
// packages/cli/src/commands/_operator-surface.ts
|
|
290
|
+
import { createInterface } from "readline";
|
|
240
291
|
var CANONICAL_STAGES = [
|
|
241
292
|
"Connect",
|
|
242
293
|
"GitHub/task sync",
|
|
@@ -251,18 +302,808 @@ var CANONICAL_STAGES = [
|
|
|
251
302
|
"Merge",
|
|
252
303
|
"Complete"
|
|
253
304
|
];
|
|
305
|
+
function logDetail(log) {
|
|
306
|
+
return typeof log.detail === "string" ? log.detail.trim() : "";
|
|
307
|
+
}
|
|
308
|
+
function parseProviderProtocolLog(title, detail) {
|
|
309
|
+
if (title.trim().toLowerCase() !== "agent output")
|
|
310
|
+
return null;
|
|
311
|
+
if (!detail.startsWith("{") || !detail.endsWith("}"))
|
|
312
|
+
return null;
|
|
313
|
+
try {
|
|
314
|
+
const record = JSON.parse(detail);
|
|
315
|
+
if (!record || typeof record !== "object" || Array.isArray(record))
|
|
316
|
+
return null;
|
|
317
|
+
const type = record.type;
|
|
318
|
+
return typeof type === "string" && [
|
|
319
|
+
"assistant",
|
|
320
|
+
"message_start",
|
|
321
|
+
"message_update",
|
|
322
|
+
"message_end",
|
|
323
|
+
"stream_event",
|
|
324
|
+
"tool_result",
|
|
325
|
+
"tool_execution_start",
|
|
326
|
+
"tool_execution_update",
|
|
327
|
+
"tool_execution_end",
|
|
328
|
+
"turn_start",
|
|
329
|
+
"turn_end"
|
|
330
|
+
].includes(type) ? record : null;
|
|
331
|
+
} catch {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
function renderProviderProtocolLog(record) {
|
|
336
|
+
const type = typeof record.type === "string" ? record.type : "";
|
|
337
|
+
if (type === "tool_execution_start" || type === "tool_execution_update" || type === "tool_execution_end") {
|
|
338
|
+
const toolName = String(record.toolName ?? record.name ?? "tool");
|
|
339
|
+
const status = type === "tool_execution_start" ? "started" : type === "tool_execution_end" ? record.isError === true || record.result && typeof record.result === "object" && !Array.isArray(record.result) && record.result.isError === true ? "failed" : "completed" : "running";
|
|
340
|
+
return `[Pi tool] ${toolName} ${status}`;
|
|
341
|
+
}
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
function entryId(entry, fallback) {
|
|
345
|
+
return typeof entry.id === "string" && entry.id.trim() ? entry.id : fallback;
|
|
346
|
+
}
|
|
254
347
|
function renderOperatorSnapshot(snapshot) {
|
|
255
348
|
const run = snapshot.run.run && typeof snapshot.run.run === "object" ? snapshot.run.run : snapshot.run;
|
|
256
349
|
const runId = String(run.runId ?? run.id ?? "run");
|
|
257
350
|
const status = String(run.status ?? "unknown");
|
|
258
351
|
const logs = snapshot.logs ?? [];
|
|
352
|
+
const latestByStage = new Map;
|
|
353
|
+
for (const log of logs) {
|
|
354
|
+
const title = String(log.title ?? "").toLowerCase();
|
|
355
|
+
const stageName = String(log.stage ?? "").toLowerCase();
|
|
356
|
+
const stage = CANONICAL_STAGES.find((candidate) => candidate.toLowerCase() === title || candidate.toLowerCase() === stageName);
|
|
357
|
+
if (stage)
|
|
358
|
+
latestByStage.set(stage, log);
|
|
359
|
+
}
|
|
259
360
|
const stageLines = CANONICAL_STAGES.flatMap((stage) => {
|
|
260
|
-
const match =
|
|
261
|
-
return match ? [`${stage}: ${String(match.status ?? status)}`] : [];
|
|
361
|
+
const match = latestByStage.get(stage);
|
|
362
|
+
return match ? [`${stage}: ${String(match.status ?? status)}${logDetail(match) ? ` \u2014 ${logDetail(match)}` : ""}`] : [];
|
|
262
363
|
});
|
|
263
364
|
return [`Rig run ${runId}: ${status}`, ...stageLines].join(`
|
|
264
365
|
`);
|
|
265
366
|
}
|
|
367
|
+
function createPiRunStreamRenderer(output = process.stdout) {
|
|
368
|
+
let lastSnapshot = "";
|
|
369
|
+
const assistantTextById = new Map;
|
|
370
|
+
const seenTimeline = new Set;
|
|
371
|
+
const seenLogs = new Set;
|
|
372
|
+
const writeLine = (line) => output.write(`${line}
|
|
373
|
+
`);
|
|
374
|
+
return {
|
|
375
|
+
renderSnapshot(snapshot) {
|
|
376
|
+
const rendered = renderOperatorSnapshot(snapshot);
|
|
377
|
+
if (rendered && rendered !== lastSnapshot) {
|
|
378
|
+
writeLine(rendered);
|
|
379
|
+
lastSnapshot = rendered;
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
renderTimeline(entries) {
|
|
383
|
+
for (const [index, entry] of entries.entries()) {
|
|
384
|
+
const id = entryId(entry, `timeline:${index}:${String(entry.cursor ?? "")}`);
|
|
385
|
+
if (entry.type === "assistant_message" && typeof entry.text === "string") {
|
|
386
|
+
const text = entry.text;
|
|
387
|
+
const previousText = assistantTextById.get(id) ?? "";
|
|
388
|
+
if (!previousText && text.trim()) {
|
|
389
|
+
writeLine("[Pi assistant]");
|
|
390
|
+
}
|
|
391
|
+
if (text.startsWith(previousText)) {
|
|
392
|
+
const delta = text.slice(previousText.length);
|
|
393
|
+
if (delta)
|
|
394
|
+
output.write(delta);
|
|
395
|
+
} else if (text.trim() && text !== previousText) {
|
|
396
|
+
if (previousText)
|
|
397
|
+
writeLine(`
|
|
398
|
+
[Pi assistant]`);
|
|
399
|
+
output.write(text);
|
|
400
|
+
}
|
|
401
|
+
assistantTextById.set(id, text);
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
if (seenTimeline.has(id))
|
|
405
|
+
continue;
|
|
406
|
+
seenTimeline.add(id);
|
|
407
|
+
if (entry.type === "tool_execution_start" || entry.type === "tool_execution_update" || entry.type === "tool_execution_end" || entry.type === "mcp_tool_call") {
|
|
408
|
+
writeLine(`[Pi tool] ${String(entry.toolName ?? entry.name ?? entry.title ?? entry.type)} ${String(entry.status ?? entry.state ?? "")}`.trim());
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
if (entry.type === "timeline_warning") {
|
|
412
|
+
writeLine(`[Rig timeline] ${String(entry.detail ?? entry.message ?? "timeline unavailable")}`);
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
if (entry.type === "action") {
|
|
416
|
+
const text = String(entry.detail ?? entry.message ?? entry.title ?? "").trim();
|
|
417
|
+
if (text)
|
|
418
|
+
writeLine(`[Rig action] ${text}`);
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
if (entry.type === "user_message") {
|
|
422
|
+
const text = String(entry.text ?? entry.message ?? entry.detail ?? "").trim();
|
|
423
|
+
if (text)
|
|
424
|
+
writeLine(`[Operator] ${text}`);
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
const fallback = String(entry.detail ?? entry.message ?? entry.text ?? entry.title ?? "").trim();
|
|
428
|
+
if (fallback)
|
|
429
|
+
writeLine(`[${String(entry.type ?? "timeline")}] ${fallback}`);
|
|
430
|
+
}
|
|
431
|
+
},
|
|
432
|
+
renderLogs(entries) {
|
|
433
|
+
for (const [index, entry] of entries.entries()) {
|
|
434
|
+
const id = entryId(entry, `log:${index}:${String(entry.createdAt ?? "")}:${String(entry.title ?? "")}`);
|
|
435
|
+
if (seenLogs.has(id))
|
|
436
|
+
continue;
|
|
437
|
+
seenLogs.add(id);
|
|
438
|
+
const title = String(entry.title ?? "");
|
|
439
|
+
if (CANONICAL_STAGES.some((stage) => stage.toLowerCase() === title.toLowerCase()))
|
|
440
|
+
continue;
|
|
441
|
+
const detail = logDetail(entry);
|
|
442
|
+
if (!detail)
|
|
443
|
+
continue;
|
|
444
|
+
const protocolRecord = parseProviderProtocolLog(title, detail);
|
|
445
|
+
if (protocolRecord) {
|
|
446
|
+
const protocolLine = renderProviderProtocolLog(protocolRecord);
|
|
447
|
+
if (protocolLine)
|
|
448
|
+
writeLine(protocolLine);
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
writeLine(`[${title || "Rig log"}] ${detail}`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
function createOperatorSurface(options = {}) {
|
|
457
|
+
const input = options.input ?? process.stdin;
|
|
458
|
+
const output = options.output ?? process.stdout;
|
|
459
|
+
const errorOutput = options.errorOutput ?? process.stderr;
|
|
460
|
+
const renderer = createPiRunStreamRenderer(output);
|
|
461
|
+
const writeLine = (line) => output.write(`${line}
|
|
462
|
+
`);
|
|
463
|
+
return {
|
|
464
|
+
mode: "pi-compatible-text",
|
|
465
|
+
...renderer,
|
|
466
|
+
info: writeLine,
|
|
467
|
+
error: (message) => errorOutput.write(`${message}
|
|
468
|
+
`),
|
|
469
|
+
attachCommandInput(handler) {
|
|
470
|
+
if (options.interactive === false || !input.isTTY)
|
|
471
|
+
return null;
|
|
472
|
+
const rl = createInterface({ input, output: process.stdout, terminal: false });
|
|
473
|
+
rl.on("line", (line) => {
|
|
474
|
+
Promise.resolve(handler(line)).catch((error) => writeLine(`Operator command failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
475
|
+
});
|
|
476
|
+
return { close: () => rl.close() };
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// packages/cli/src/commands/_pi-frontend.ts
|
|
482
|
+
import { mkdtempSync, rmSync } from "fs";
|
|
483
|
+
import { tmpdir } from "os";
|
|
484
|
+
import { join } from "path";
|
|
485
|
+
import {
|
|
486
|
+
createAgentSessionFromServices,
|
|
487
|
+
createAgentSessionServices,
|
|
488
|
+
main as runPiMain
|
|
489
|
+
} from "@earendil-works/pi-coding-agent";
|
|
490
|
+
|
|
491
|
+
// packages/cli/src/commands/_pi-remote-session.ts
|
|
492
|
+
import { AgentSession as PiAgentSession } from "@earendil-works/pi-coding-agent";
|
|
493
|
+
var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
|
|
494
|
+
function defaultTransport() {
|
|
495
|
+
return {
|
|
496
|
+
getSession: getRunPiSessionViaServer,
|
|
497
|
+
getMessages: getRunPiMessagesViaServer,
|
|
498
|
+
getStatus: getRunPiStatusViaServer,
|
|
499
|
+
getCommands: getRunPiCommandsViaServer,
|
|
500
|
+
sendPrompt: sendRunPiPromptViaServer,
|
|
501
|
+
sendShell: sendRunPiShellViaServer,
|
|
502
|
+
runCommand: runRunPiCommandViaServer,
|
|
503
|
+
abort: abortRunPiViaServer,
|
|
504
|
+
buildEventsWebSocketUrl: buildRunPiEventsWebSocketUrl
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
function recordOf(value) {
|
|
508
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
509
|
+
}
|
|
510
|
+
function emptyRemoteStatus() {
|
|
511
|
+
return {
|
|
512
|
+
isStreaming: false,
|
|
513
|
+
isCompacting: false,
|
|
514
|
+
isBashRunning: false,
|
|
515
|
+
pendingMessageCount: 0,
|
|
516
|
+
steeringMessages: [],
|
|
517
|
+
followUpMessages: [],
|
|
518
|
+
model: null,
|
|
519
|
+
thinkingLevel: null,
|
|
520
|
+
sessionName: null,
|
|
521
|
+
cwd: null,
|
|
522
|
+
stats: null,
|
|
523
|
+
contextUsage: null
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
function resolveAttachReadyTimeoutMs() {
|
|
527
|
+
const raw = Number.parseInt(process.env.RIG_PI_ATTACH_TIMEOUT_MS ?? "", 10);
|
|
528
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 10 * 60000;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
class RigRemoteSessionController {
|
|
532
|
+
context;
|
|
533
|
+
runId;
|
|
534
|
+
status = emptyRemoteStatus();
|
|
535
|
+
transport;
|
|
536
|
+
hooks = {};
|
|
537
|
+
session = null;
|
|
538
|
+
socket = null;
|
|
539
|
+
closed = false;
|
|
540
|
+
pendingShells = [];
|
|
541
|
+
pendingCompactions = [];
|
|
542
|
+
constructor(input) {
|
|
543
|
+
this.context = input.context;
|
|
544
|
+
this.runId = input.runId;
|
|
545
|
+
this.transport = input.transport ?? defaultTransport();
|
|
546
|
+
}
|
|
547
|
+
ingestEnvelope(envelopeValue) {
|
|
548
|
+
this.applyEnvelope(envelopeValue);
|
|
549
|
+
}
|
|
550
|
+
bindSession(session) {
|
|
551
|
+
this.session = session;
|
|
552
|
+
}
|
|
553
|
+
setUiHooks(hooks) {
|
|
554
|
+
this.hooks = hooks;
|
|
555
|
+
}
|
|
556
|
+
async connect() {
|
|
557
|
+
const ready = await this.waitForReady();
|
|
558
|
+
if (!ready || this.closed)
|
|
559
|
+
return;
|
|
560
|
+
let catchupDone = false;
|
|
561
|
+
const buffered = [];
|
|
562
|
+
const wsUrl = await this.transport.buildEventsWebSocketUrl(this.context, this.runId);
|
|
563
|
+
const socket = new WebSocket(wsUrl);
|
|
564
|
+
this.socket = socket;
|
|
565
|
+
socket.onopen = () => {
|
|
566
|
+
this.hooks.onConnectionChange?.(true);
|
|
567
|
+
this.hooks.onStatusText?.("worker session live");
|
|
568
|
+
};
|
|
569
|
+
socket.onmessage = (message) => {
|
|
570
|
+
try {
|
|
571
|
+
const payload = typeof message.data === "string" ? JSON.parse(message.data) : JSON.parse(Buffer.from(message.data).toString("utf8"));
|
|
572
|
+
if (!catchupDone)
|
|
573
|
+
buffered.push(payload);
|
|
574
|
+
else
|
|
575
|
+
this.applyEnvelope(payload);
|
|
576
|
+
} catch (error) {
|
|
577
|
+
this.hooks.onError?.(`Unparseable worker Pi event: ${error instanceof Error ? error.message : String(error)}`);
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
socket.onerror = () => socket.close();
|
|
581
|
+
socket.onclose = () => {
|
|
582
|
+
this.hooks.onConnectionChange?.(false);
|
|
583
|
+
if (!this.closed)
|
|
584
|
+
this.hooks.onStatusText?.("worker Pi WebSocket disconnected");
|
|
585
|
+
};
|
|
586
|
+
try {
|
|
587
|
+
const [messagesPayload, statusPayload, commandsPayload] = await Promise.all([
|
|
588
|
+
this.transport.getMessages(this.context, this.runId),
|
|
589
|
+
this.transport.getStatus(this.context, this.runId),
|
|
590
|
+
this.transport.getCommands(this.context, this.runId)
|
|
591
|
+
]);
|
|
592
|
+
const messages = Array.isArray(messagesPayload.messages) ? messagesPayload.messages : [];
|
|
593
|
+
this.applyStatusPayload(statusPayload);
|
|
594
|
+
this.session?.replaceRemoteMessages(messages);
|
|
595
|
+
const commands = Array.isArray(commandsPayload.commands) ? commandsPayload.commands : [];
|
|
596
|
+
this.hooks.onCatchUp?.(messages, commands);
|
|
597
|
+
catchupDone = true;
|
|
598
|
+
for (const payload of buffered.splice(0))
|
|
599
|
+
this.applyEnvelope(payload);
|
|
600
|
+
} catch (error) {
|
|
601
|
+
catchupDone = true;
|
|
602
|
+
this.hooks.onError?.(`Worker Pi catch-up failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
close() {
|
|
606
|
+
this.closed = true;
|
|
607
|
+
this.socket?.close();
|
|
608
|
+
for (const shell of this.pendingShells.splice(0))
|
|
609
|
+
shell.reject(new Error("Remote session closed."));
|
|
610
|
+
for (const compaction of this.pendingCompactions.splice(0))
|
|
611
|
+
compaction.reject(new Error("Remote session closed."));
|
|
612
|
+
}
|
|
613
|
+
async sendPrompt(text, streamingBehavior) {
|
|
614
|
+
await this.transport.sendPrompt(this.context, this.runId, text, streamingBehavior);
|
|
615
|
+
}
|
|
616
|
+
async sendCommand(text) {
|
|
617
|
+
const result = await this.transport.runCommand(this.context, this.runId, text);
|
|
618
|
+
return typeof result.message === "string" ? result.message : "worker command accepted";
|
|
619
|
+
}
|
|
620
|
+
async sendShell(text) {
|
|
621
|
+
await this.transport.sendShell(this.context, this.runId, text);
|
|
622
|
+
}
|
|
623
|
+
async abort() {
|
|
624
|
+
await this.transport.abort(this.context, this.runId);
|
|
625
|
+
}
|
|
626
|
+
registerPendingShell(shell) {
|
|
627
|
+
this.pendingShells.push(shell);
|
|
628
|
+
}
|
|
629
|
+
failPendingShell(shell, error) {
|
|
630
|
+
const index = this.pendingShells.indexOf(shell);
|
|
631
|
+
if (index !== -1)
|
|
632
|
+
this.pendingShells.splice(index, 1);
|
|
633
|
+
shell.reject(error);
|
|
634
|
+
}
|
|
635
|
+
registerPendingCompaction(pending) {
|
|
636
|
+
this.pendingCompactions.push(pending);
|
|
637
|
+
}
|
|
638
|
+
async waitForReady() {
|
|
639
|
+
const startedAt = Date.now();
|
|
640
|
+
const deadline = startedAt + resolveAttachReadyTimeoutMs();
|
|
641
|
+
let consecutiveFailures = 0;
|
|
642
|
+
while (!this.closed) {
|
|
643
|
+
let requestFailed = false;
|
|
644
|
+
const session = await this.transport.getSession(this.context, this.runId).catch((error) => {
|
|
645
|
+
requestFailed = true;
|
|
646
|
+
return { ready: false, status: error instanceof Error ? error.message : String(error), retryAfterMs: 1000 };
|
|
647
|
+
});
|
|
648
|
+
if (session.ready !== false)
|
|
649
|
+
return true;
|
|
650
|
+
consecutiveFailures = requestFailed ? consecutiveFailures + 1 : 0;
|
|
651
|
+
const status = String(session.status ?? "starting");
|
|
652
|
+
if (TERMINAL_RUN_STATUSES.has(status.toLowerCase())) {
|
|
653
|
+
this.hooks.onError?.(`Run ended before the worker Pi daemon became ready: ${status}. Inspect with \`rig run show ${this.runId}\`.`);
|
|
654
|
+
return false;
|
|
655
|
+
}
|
|
656
|
+
this.hooks.onStatusText?.(consecutiveFailures >= 5 ? "Rig server unreachable \xB7 retrying \xB7 /detach to exit" : `waiting for worker Pi daemon \xB7 ${status} \xB7 /detach to exit`);
|
|
657
|
+
if (Date.now() >= deadline) {
|
|
658
|
+
this.hooks.onError?.(`Worker Pi daemon did not become ready (last status: ${status}). Set RIG_PI_ATTACH_TIMEOUT_MS to wait longer.`);
|
|
659
|
+
return false;
|
|
660
|
+
}
|
|
661
|
+
await Bun.sleep(typeof session.retryAfterMs === "number" ? session.retryAfterMs : 750);
|
|
662
|
+
}
|
|
663
|
+
return false;
|
|
664
|
+
}
|
|
665
|
+
applyEnvelope(envelopeValue) {
|
|
666
|
+
const envelope = recordOf(envelopeValue);
|
|
667
|
+
if (!envelope)
|
|
668
|
+
return;
|
|
669
|
+
const type = String(envelope.type ?? "");
|
|
670
|
+
if (type === "status.update") {
|
|
671
|
+
this.applyStatusPayload(envelope);
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
if (type === "activity.update") {
|
|
675
|
+
const activity = recordOf(envelope.activity);
|
|
676
|
+
this.hooks.onActivity?.(String(activity?.label ?? ""), activity?.detail ? String(activity.detail) : undefined);
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
if (type === "extension_ui_request") {
|
|
680
|
+
const request = recordOf(envelope.request);
|
|
681
|
+
if (request)
|
|
682
|
+
this.hooks.onExtensionUiRequest?.(request);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
if (type === "pi.ui_event") {
|
|
686
|
+
this.applyShellUiEvent(envelope.event);
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
if (type === "pi.event") {
|
|
690
|
+
this.session?.handleRemoteSessionEvent(envelope.event);
|
|
691
|
+
this.settlePendingCompaction(envelope.event);
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
if (type === "error") {
|
|
695
|
+
this.hooks.onError?.(String(envelope.message ?? envelope.detail ?? "unknown worker error"));
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
applyStatusPayload(payload) {
|
|
699
|
+
const status = recordOf(payload.status) ?? payload;
|
|
700
|
+
const next = this.status;
|
|
701
|
+
next.isStreaming = status.isStreaming === true;
|
|
702
|
+
next.isCompacting = status.isCompacting === true;
|
|
703
|
+
next.isBashRunning = status.isBashRunning === true;
|
|
704
|
+
next.pendingMessageCount = typeof status.pendingMessageCount === "number" ? status.pendingMessageCount : 0;
|
|
705
|
+
next.steeringMessages = Array.isArray(status.steeringMessages) ? status.steeringMessages.map(String) : [];
|
|
706
|
+
next.followUpMessages = Array.isArray(status.followUpMessages) ? status.followUpMessages.map(String) : [];
|
|
707
|
+
next.model = recordOf(status.model) ?? next.model;
|
|
708
|
+
next.thinkingLevel = typeof status.thinkingLevel === "string" ? status.thinkingLevel : next.thinkingLevel;
|
|
709
|
+
next.sessionName = typeof status.sessionName === "string" ? status.sessionName : next.sessionName;
|
|
710
|
+
next.cwd = typeof status.cwd === "string" ? status.cwd : next.cwd;
|
|
711
|
+
next.stats = recordOf(status.stats) ?? next.stats;
|
|
712
|
+
next.contextUsage = recordOf(status.contextUsage) ?? next.contextUsage;
|
|
713
|
+
}
|
|
714
|
+
applyShellUiEvent(value) {
|
|
715
|
+
const event = recordOf(value);
|
|
716
|
+
if (!event)
|
|
717
|
+
return;
|
|
718
|
+
const type = String(event.type ?? "");
|
|
719
|
+
const pending = this.pendingShells[0];
|
|
720
|
+
if (type === "shell.chunk" && pending) {
|
|
721
|
+
pending.sawChunk = true;
|
|
722
|
+
pending.onData(Buffer.from(String(event.chunk ?? "")));
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
if (type === "shell.end" && pending) {
|
|
726
|
+
const output = String(event.output ?? "");
|
|
727
|
+
if (output && !pending.sawChunk)
|
|
728
|
+
pending.onData(Buffer.from(output));
|
|
729
|
+
const index = this.pendingShells.indexOf(pending);
|
|
730
|
+
if (index !== -1)
|
|
731
|
+
this.pendingShells.splice(index, 1);
|
|
732
|
+
pending.resolve({ exitCode: typeof event.exitCode === "number" ? event.exitCode : event.isError === true ? 1 : 0 });
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
settlePendingCompaction(eventValue) {
|
|
736
|
+
const event = recordOf(eventValue);
|
|
737
|
+
if (!event)
|
|
738
|
+
return;
|
|
739
|
+
const type = String(event.type ?? "");
|
|
740
|
+
if (type !== "compaction_end" || this.pendingCompactions.length === 0)
|
|
741
|
+
return;
|
|
742
|
+
const pending = this.pendingCompactions.shift();
|
|
743
|
+
const result = recordOf(event.result);
|
|
744
|
+
if (result)
|
|
745
|
+
pending.resolve(result);
|
|
746
|
+
else if (event.aborted === true)
|
|
747
|
+
pending.reject(new Error("Compaction aborted on the worker."));
|
|
748
|
+
else
|
|
749
|
+
pending.resolve({});
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
class RigRemoteAgentSession extends PiAgentSession {
|
|
754
|
+
remote;
|
|
755
|
+
constructor(config, remote) {
|
|
756
|
+
super(config);
|
|
757
|
+
this.remote = remote;
|
|
758
|
+
remote.bindSession(this);
|
|
759
|
+
}
|
|
760
|
+
handleRemoteSessionEvent(eventValue) {
|
|
761
|
+
const event = recordOf(eventValue);
|
|
762
|
+
if (!event)
|
|
763
|
+
return;
|
|
764
|
+
const type = String(event.type ?? "");
|
|
765
|
+
if ((type === "message_end" || type === "turn_end") && recordOf(event.message)) {
|
|
766
|
+
this.agent.state.messages = [...this.agent.state.messages, event.message];
|
|
767
|
+
}
|
|
768
|
+
this._emit(eventValue);
|
|
769
|
+
}
|
|
770
|
+
replaceRemoteMessages(messages) {
|
|
771
|
+
this.agent.state.messages = messages;
|
|
772
|
+
}
|
|
773
|
+
async prompt(text, options) {
|
|
774
|
+
const trimmed = text.trim();
|
|
775
|
+
if (!trimmed)
|
|
776
|
+
return;
|
|
777
|
+
if (trimmed.startsWith("/")) {
|
|
778
|
+
if (await this._tryExecuteExtensionCommand(trimmed))
|
|
779
|
+
return;
|
|
780
|
+
await this.remote.sendCommand(trimmed);
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
if (trimmed.startsWith("!")) {
|
|
784
|
+
await this.remote.sendShell(trimmed);
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
const behavior = options?.streamingBehavior ?? (this.isStreaming || this.isCompacting ? "steer" : undefined);
|
|
788
|
+
options?.preflightResult?.(true);
|
|
789
|
+
await this.remote.sendPrompt(trimmed, behavior);
|
|
790
|
+
}
|
|
791
|
+
async steer(text) {
|
|
792
|
+
await this.remote.sendPrompt(text, "steer");
|
|
793
|
+
}
|
|
794
|
+
async followUp(text) {
|
|
795
|
+
await this.remote.sendPrompt(text, "followUp");
|
|
796
|
+
}
|
|
797
|
+
async abort() {
|
|
798
|
+
await this.remote.abort();
|
|
799
|
+
}
|
|
800
|
+
async compact(customInstructions) {
|
|
801
|
+
const pending = new Promise((resolve3, reject) => {
|
|
802
|
+
this.remote.registerPendingCompaction({ resolve: resolve3, reject });
|
|
803
|
+
});
|
|
804
|
+
await this.remote.sendCommand(`/compact${customInstructions ? ` ${customInstructions}` : ""}`);
|
|
805
|
+
return pending;
|
|
806
|
+
}
|
|
807
|
+
clearQueue() {
|
|
808
|
+
const cleared = {
|
|
809
|
+
steering: [...this.remote.status.steeringMessages],
|
|
810
|
+
followUp: [...this.remote.status.followUpMessages]
|
|
811
|
+
};
|
|
812
|
+
this.remote.status.steeringMessages = [];
|
|
813
|
+
this.remote.status.followUpMessages = [];
|
|
814
|
+
this.remote.status.pendingMessageCount = 0;
|
|
815
|
+
this.remote.sendCommand("/queue-clear").catch(() => {});
|
|
816
|
+
return cleared;
|
|
817
|
+
}
|
|
818
|
+
get isStreaming() {
|
|
819
|
+
return this.remote.status.isStreaming;
|
|
820
|
+
}
|
|
821
|
+
get isCompacting() {
|
|
822
|
+
return this.remote.status.isCompacting;
|
|
823
|
+
}
|
|
824
|
+
get isBashRunning() {
|
|
825
|
+
return this.remote.status.isBashRunning;
|
|
826
|
+
}
|
|
827
|
+
get pendingMessageCount() {
|
|
828
|
+
return this.remote.status.pendingMessageCount;
|
|
829
|
+
}
|
|
830
|
+
getSteeringMessages() {
|
|
831
|
+
return this.remote.status.steeringMessages;
|
|
832
|
+
}
|
|
833
|
+
getFollowUpMessages() {
|
|
834
|
+
return this.remote.status.followUpMessages;
|
|
835
|
+
}
|
|
836
|
+
get model() {
|
|
837
|
+
return this.remote.status.model ?? super.model;
|
|
838
|
+
}
|
|
839
|
+
async setModel(model) {
|
|
840
|
+
await this.remote.sendCommand(`/model ${model.provider}/${model.id}`);
|
|
841
|
+
this.remote.status.model = model;
|
|
842
|
+
}
|
|
843
|
+
get thinkingLevel() {
|
|
844
|
+
return this.remote.status.thinkingLevel ?? super.thinkingLevel;
|
|
845
|
+
}
|
|
846
|
+
setThinkingLevel(level) {
|
|
847
|
+
this.remote.status.thinkingLevel = level;
|
|
848
|
+
this.remote.sendCommand(`/thinking ${level}`).catch(() => {});
|
|
849
|
+
}
|
|
850
|
+
get sessionName() {
|
|
851
|
+
return this.remote.status.sessionName ?? super.sessionName;
|
|
852
|
+
}
|
|
853
|
+
setSessionName(name) {
|
|
854
|
+
this.remote.status.sessionName = name;
|
|
855
|
+
this.remote.sendCommand(`/name ${name}`).catch(() => {});
|
|
856
|
+
}
|
|
857
|
+
getSessionStats() {
|
|
858
|
+
return this.remote.status.stats ?? super.getSessionStats();
|
|
859
|
+
}
|
|
860
|
+
getContextUsage() {
|
|
861
|
+
return this.remote.status.contextUsage ?? super.getContextUsage();
|
|
862
|
+
}
|
|
863
|
+
dispose() {
|
|
864
|
+
this.remote.close();
|
|
865
|
+
super.dispose();
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
function createRemoteBashOperations(controller, excludeFromContext) {
|
|
869
|
+
return {
|
|
870
|
+
exec(command, _cwd, execOptions) {
|
|
871
|
+
return new Promise((resolve3, reject) => {
|
|
872
|
+
const pending = { onData: execOptions.onData, resolve: resolve3, reject, sawChunk: false };
|
|
873
|
+
const cleanup = () => {
|
|
874
|
+
execOptions.signal?.removeEventListener("abort", onAbort);
|
|
875
|
+
if (timer)
|
|
876
|
+
clearTimeout(timer);
|
|
877
|
+
};
|
|
878
|
+
const onAbort = () => {
|
|
879
|
+
cleanup();
|
|
880
|
+
controller.failPendingShell(pending, new Error("Remote worker shell command aborted locally."));
|
|
881
|
+
};
|
|
882
|
+
const timeoutMs = typeof execOptions.timeout === "number" && execOptions.timeout > 0 ? execOptions.timeout : 0;
|
|
883
|
+
const timer = timeoutMs > 0 ? setTimeout(() => {
|
|
884
|
+
cleanup();
|
|
885
|
+
controller.failPendingShell(pending, new Error(`Remote worker shell command timed out after ${timeoutMs}ms.`));
|
|
886
|
+
}, timeoutMs) : null;
|
|
887
|
+
const wrappedResolve = pending.resolve;
|
|
888
|
+
const wrappedReject = pending.reject;
|
|
889
|
+
pending.resolve = (result) => {
|
|
890
|
+
cleanup();
|
|
891
|
+
wrappedResolve(result);
|
|
892
|
+
};
|
|
893
|
+
pending.reject = (error) => {
|
|
894
|
+
cleanup();
|
|
895
|
+
wrappedReject(error);
|
|
896
|
+
};
|
|
897
|
+
execOptions.signal?.addEventListener("abort", onAbort, { once: true });
|
|
898
|
+
controller.registerPendingShell(pending);
|
|
899
|
+
controller.sendShell(`${excludeFromContext ? "!!" : "!"}${command}`).catch((error) => {
|
|
900
|
+
cleanup();
|
|
901
|
+
controller.failPendingShell(pending, error instanceof Error ? error : new Error(String(error)));
|
|
902
|
+
});
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// packages/cli/src/commands/_pi-worker-bridge-extension.ts
|
|
909
|
+
function recordOf2(value) {
|
|
910
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
911
|
+
}
|
|
912
|
+
function asText(value) {
|
|
913
|
+
if (typeof value === "string")
|
|
914
|
+
return value;
|
|
915
|
+
if (value === null || value === undefined)
|
|
916
|
+
return "";
|
|
917
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
918
|
+
return String(value);
|
|
919
|
+
try {
|
|
920
|
+
return JSON.stringify(value);
|
|
921
|
+
} catch {
|
|
922
|
+
return String(value);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
async function answerExtensionUiRequest(options, ctx, request) {
|
|
926
|
+
const requestId = String(request.requestId ?? request.id ?? `ui-${Date.now()}`);
|
|
927
|
+
const method = String(request.method ?? request.type ?? "input");
|
|
928
|
+
const prompt = asText(request.prompt ?? request.message ?? request.title ?? method);
|
|
929
|
+
const rawOptions = Array.isArray(request.options) ? request.options : Array.isArray(request.items) ? request.items : [];
|
|
930
|
+
const choices = rawOptions.map((option) => {
|
|
931
|
+
const record = recordOf2(option);
|
|
932
|
+
return record ? asText(record.label ?? record.value ?? record.name ?? option) : asText(option);
|
|
933
|
+
}).filter(Boolean);
|
|
934
|
+
try {
|
|
935
|
+
if (method === "confirm") {
|
|
936
|
+
const confirmed = await ctx.ui.confirm("Worker request", prompt);
|
|
937
|
+
await respondRunPiExtensionUiViaServer(options.context, options.runId, requestId, { value: confirmed, confirmed });
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
if (choices.length > 0) {
|
|
941
|
+
const selected = await ctx.ui.select(prompt, choices);
|
|
942
|
+
await respondRunPiExtensionUiViaServer(options.context, options.runId, requestId, selected === undefined ? { cancelled: true } : { value: selected });
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
const value = await ctx.ui.input("Worker request", prompt);
|
|
946
|
+
await respondRunPiExtensionUiViaServer(options.context, options.runId, requestId, value === undefined ? { cancelled: true } : { value });
|
|
947
|
+
} catch (error) {
|
|
948
|
+
ctx.ui.notify(`Failed to answer worker UI request: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
var LOCALLY_OWNED_COMMANDS = new Set(["detach", "quit", "q", "stop", "settings", "model", "thinking", "compact", "session", "name", "reload"]);
|
|
952
|
+
function registerDaemonCommands(pi, options, ctx, commands, registered) {
|
|
953
|
+
for (const command of commands) {
|
|
954
|
+
const record = recordOf2(command);
|
|
955
|
+
const name = typeof record?.name === "string" ? record.name : "";
|
|
956
|
+
const source = typeof record?.source === "string" ? record.source : "worker";
|
|
957
|
+
if (!name || source === "builtin" || registered.has(name) || LOCALLY_OWNED_COMMANDS.has(name))
|
|
958
|
+
continue;
|
|
959
|
+
registered.add(name);
|
|
960
|
+
const description = typeof record?.description === "string" ? record.description : undefined;
|
|
961
|
+
try {
|
|
962
|
+
pi.registerCommand(name, {
|
|
963
|
+
description: `[worker ${source}] ${description ?? ""}`.trim(),
|
|
964
|
+
handler: async (args) => {
|
|
965
|
+
try {
|
|
966
|
+
const message = await options.controller.sendCommand(`/${name}${args ? ` ${args}` : ""}`);
|
|
967
|
+
ctx.ui.notify(message, "info");
|
|
968
|
+
} catch (error) {
|
|
969
|
+
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
});
|
|
973
|
+
} catch {}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
function createRigWorkerPiBridgeExtension(options) {
|
|
977
|
+
return (pi) => {
|
|
978
|
+
const registeredDaemonCommands = new Set;
|
|
979
|
+
pi.registerCommand("detach", {
|
|
980
|
+
description: "Detach from this run; the worker keeps going",
|
|
981
|
+
handler: async (_args, ctx) => {
|
|
982
|
+
ctx.ui.notify("Detached locally; the worker Pi daemon continues.", "info");
|
|
983
|
+
ctx.shutdown();
|
|
984
|
+
}
|
|
985
|
+
});
|
|
986
|
+
pi.registerCommand("stop", {
|
|
987
|
+
description: "Stop the worker Pi run and detach",
|
|
988
|
+
handler: async (_args, ctx) => {
|
|
989
|
+
await options.controller.abort();
|
|
990
|
+
ctx.ui.notify("Stop requested for the worker Pi daemon.", "info");
|
|
991
|
+
ctx.shutdown();
|
|
992
|
+
}
|
|
993
|
+
});
|
|
994
|
+
pi.on("user_bash", (event) => ({
|
|
995
|
+
operations: createRemoteBashOperations(options.controller, event.excludeFromContext === true)
|
|
996
|
+
}));
|
|
997
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
998
|
+
ctx.ui.setTitle("Rig \xB7 worker session");
|
|
999
|
+
ctx.ui.setStatus("rig-worker-pi", "connecting to worker session");
|
|
1000
|
+
ctx.ui.notify(`Attached to Rig run ${options.runId}. Native Pi, remote brain \u2014 /detach exits, /stop cancels the run.`, "info");
|
|
1001
|
+
if (options.initialMessageSent)
|
|
1002
|
+
ctx.ui.notify("Initial message sent to the worker Pi daemon.", "info");
|
|
1003
|
+
const nativeUi = ctx.ui;
|
|
1004
|
+
options.controller.setUiHooks({
|
|
1005
|
+
onStatusText: (text) => ctx.ui.setStatus("rig-worker-pi", text),
|
|
1006
|
+
onActivity: (label, detail) => ctx.ui.setStatus("rig-worker-pi", [label, detail].filter(Boolean).join(" \u2014 ")),
|
|
1007
|
+
onConnectionChange: (connected) => ctx.ui.setStatus("rig-worker-pi", connected ? "worker session live" : "worker session disconnected"),
|
|
1008
|
+
onError: (message) => ctx.ui.notify(message, "error"),
|
|
1009
|
+
onExtensionUiRequest: (request) => {
|
|
1010
|
+
answerExtensionUiRequest(options, ctx, request);
|
|
1011
|
+
},
|
|
1012
|
+
onCatchUp: (messages, commands) => {
|
|
1013
|
+
if (nativeUi.appendSessionMessages)
|
|
1014
|
+
nativeUi.appendSessionMessages(messages);
|
|
1015
|
+
const cwd = options.controller.status.cwd;
|
|
1016
|
+
if (nativeUi.setDisplayCwd && cwd)
|
|
1017
|
+
nativeUi.setDisplayCwd(cwd);
|
|
1018
|
+
registerDaemonCommands(pi, options, ctx, commands, registeredDaemonCommands);
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
1021
|
+
options.controller.connect().catch((error) => {
|
|
1022
|
+
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
1023
|
+
});
|
|
1024
|
+
});
|
|
1025
|
+
pi.on("session_shutdown", () => {
|
|
1026
|
+
options.controller.close();
|
|
1027
|
+
});
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// packages/cli/src/commands/_pi-frontend.ts
|
|
1032
|
+
function setTemporaryEnv(updates) {
|
|
1033
|
+
const previous = new Map;
|
|
1034
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
1035
|
+
previous.set(key, process.env[key]);
|
|
1036
|
+
process.env[key] = value;
|
|
1037
|
+
}
|
|
1038
|
+
return () => {
|
|
1039
|
+
for (const [key, value] of previous) {
|
|
1040
|
+
if (value === undefined)
|
|
1041
|
+
delete process.env[key];
|
|
1042
|
+
else
|
|
1043
|
+
process.env[key] = value;
|
|
1044
|
+
}
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
async function attachRunBundledPiFrontend(context, input) {
|
|
1048
|
+
const tempSessionDir = mkdtempSync(join(tmpdir(), "rig-pi-frontend-sessions-"));
|
|
1049
|
+
const restoreEnv = setTemporaryEnv({
|
|
1050
|
+
PI_CODING_AGENT_SESSION_DIR: tempSessionDir,
|
|
1051
|
+
PI_SKIP_VERSION_CHECK: "1"
|
|
1052
|
+
});
|
|
1053
|
+
const controller = new RigRemoteSessionController({ context, runId: input.runId });
|
|
1054
|
+
const createRemoteRuntime = async ({ cwd, agentDir, sessionManager, sessionStartEvent }) => {
|
|
1055
|
+
const services = await createAgentSessionServices({
|
|
1056
|
+
cwd,
|
|
1057
|
+
agentDir,
|
|
1058
|
+
resourceLoaderOptions: {
|
|
1059
|
+
extensionFactories: [
|
|
1060
|
+
createRigWorkerPiBridgeExtension({
|
|
1061
|
+
context,
|
|
1062
|
+
controller,
|
|
1063
|
+
runId: input.runId,
|
|
1064
|
+
initialMessageSent: input.steered === true
|
|
1065
|
+
})
|
|
1066
|
+
],
|
|
1067
|
+
noContextFiles: true
|
|
1068
|
+
}
|
|
1069
|
+
});
|
|
1070
|
+
const created = await createAgentSessionFromServices({
|
|
1071
|
+
services,
|
|
1072
|
+
sessionManager,
|
|
1073
|
+
sessionStartEvent,
|
|
1074
|
+
noTools: "all",
|
|
1075
|
+
sessionFactory: (config) => new RigRemoteAgentSession(config, controller)
|
|
1076
|
+
});
|
|
1077
|
+
return { ...created, services, diagnostics: services.diagnostics };
|
|
1078
|
+
};
|
|
1079
|
+
let detached = false;
|
|
1080
|
+
try {
|
|
1081
|
+
await runPiMain([], {
|
|
1082
|
+
createRuntimeOverride: () => createRemoteRuntime
|
|
1083
|
+
});
|
|
1084
|
+
detached = true;
|
|
1085
|
+
} finally {
|
|
1086
|
+
restoreEnv();
|
|
1087
|
+
controller.close();
|
|
1088
|
+
rmSync(tempSessionDir, { recursive: true, force: true });
|
|
1089
|
+
}
|
|
1090
|
+
let run = { runId: input.runId, status: "unknown" };
|
|
1091
|
+
try {
|
|
1092
|
+
run = await getRunDetailsViaServer(context, input.runId);
|
|
1093
|
+
} catch {}
|
|
1094
|
+
return {
|
|
1095
|
+
run,
|
|
1096
|
+
logs: [],
|
|
1097
|
+
timeline: [],
|
|
1098
|
+
timelineCursor: null,
|
|
1099
|
+
steered: input.steered === true,
|
|
1100
|
+
detached,
|
|
1101
|
+
rendered: "native bundled Pi frontend with remote worker session runtime"
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// packages/cli/src/commands/_operator-view.ts
|
|
1106
|
+
var TERMINAL_RUN_STATUSES2 = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
|
|
266
1107
|
function runStatusFromPayload(payload) {
|
|
267
1108
|
const run = payload.run && typeof payload.run === "object" && !Array.isArray(payload.run) ? payload.run : payload;
|
|
268
1109
|
return String(run.status ?? "unknown").toLowerCase();
|
|
@@ -284,57 +1125,76 @@ async function applyOperatorCommand(context, input, deps = {}) {
|
|
|
284
1125
|
await (deps.steer ?? steerRunViaServer)(context, input.runId, userMessage);
|
|
285
1126
|
return { action: "continue", message: "Steering message queued." };
|
|
286
1127
|
}
|
|
287
|
-
async function readOperatorSnapshot(context, runId) {
|
|
1128
|
+
async function readOperatorSnapshot(context, runId, options = {}) {
|
|
288
1129
|
const run = await getRunDetailsViaServer(context, runId);
|
|
289
1130
|
const logsPage = await getRunLogsViaServer(context, runId, { limit: 100 });
|
|
290
|
-
const
|
|
291
|
-
|
|
1131
|
+
const timelinePage = await getRunTimelineViaServer(context, runId, { limit: 200, ...options.timelineCursor ? { cursor: options.timelineCursor } : {} }).catch((error) => ({
|
|
1132
|
+
entries: [{
|
|
1133
|
+
id: `timeline-unavailable:${runId}`,
|
|
1134
|
+
type: "timeline_warning",
|
|
1135
|
+
detail: `Selected Rig server did not provide run timeline events: ${error instanceof Error ? error.message : String(error)}`,
|
|
1136
|
+
createdAt: new Date().toISOString()
|
|
1137
|
+
}],
|
|
1138
|
+
nextCursor: options.timelineCursor ?? null
|
|
1139
|
+
}));
|
|
1140
|
+
const logs = Array.isArray(logsPage.entries) ? logsPage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))).toReversed() : [];
|
|
1141
|
+
const timeline = Array.isArray(timelinePage.entries) ? timelinePage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
|
|
1142
|
+
const timelineCursor = typeof timelinePage.nextCursor === "string" ? timelinePage.nextCursor : options.timelineCursor ?? null;
|
|
1143
|
+
return { run, logs, timeline, timelineCursor, rendered: renderOperatorSnapshot({ run, logs, timeline }) };
|
|
292
1144
|
}
|
|
293
1145
|
async function attachRunOperatorView(context, input) {
|
|
294
1146
|
let steered = false;
|
|
295
1147
|
if (input.message?.trim()) {
|
|
296
|
-
await steerRunViaServer(context, input.runId, input.message.trim());
|
|
1148
|
+
await sendRunPiPromptViaServer(context, input.runId, input.message.trim(), "steer").catch(() => steerRunViaServer(context, input.runId, input.message.trim()));
|
|
297
1149
|
steered = true;
|
|
298
1150
|
}
|
|
1151
|
+
if (input.follow && !input.once && input.interactive !== false && context.outputMode === "text" && Boolean(process.stdin.isTTY && process.stdout.isTTY)) {
|
|
1152
|
+
return attachRunBundledPiFrontend(context, {
|
|
1153
|
+
runId: input.runId,
|
|
1154
|
+
steered
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
const surface = createOperatorSurface({ interactive: input.interactive !== false });
|
|
299
1158
|
let snapshot = await readOperatorSnapshot(context, input.runId);
|
|
300
1159
|
if (context.outputMode === "text") {
|
|
301
|
-
|
|
1160
|
+
surface.renderSnapshot(snapshot);
|
|
1161
|
+
surface.renderTimeline(snapshot.timeline);
|
|
1162
|
+
surface.renderLogs(snapshot.logs);
|
|
302
1163
|
if (steered)
|
|
303
|
-
|
|
1164
|
+
surface.info("Message submitted to worker Pi.");
|
|
304
1165
|
}
|
|
305
1166
|
let detached = false;
|
|
306
|
-
let
|
|
1167
|
+
let commandInput = null;
|
|
307
1168
|
if (input.follow && !input.once && context.outputMode === "text") {
|
|
308
1169
|
if (input.interactive !== false && process.stdin.isTTY) {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
}
|
|
319
|
-
}).catch((error) => console.log(`Operator command failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
1170
|
+
surface.info("Controls: /user <message>, /stop, /detach");
|
|
1171
|
+
commandInput = surface.attachCommandInput(async (line) => {
|
|
1172
|
+
const result = await applyOperatorCommand(context, { runId: input.runId, line });
|
|
1173
|
+
if (result.message)
|
|
1174
|
+
surface.info(result.message);
|
|
1175
|
+
if (result.action === "detach" || result.action === "stopped") {
|
|
1176
|
+
detached = true;
|
|
1177
|
+
commandInput?.close();
|
|
1178
|
+
}
|
|
320
1179
|
});
|
|
321
1180
|
}
|
|
322
|
-
let lastRendered = snapshot.rendered;
|
|
323
1181
|
const pollMs = Math.max(250, Math.trunc(input.pollMs ?? 2000));
|
|
324
|
-
|
|
1182
|
+
let timelineCursor = snapshot.timelineCursor;
|
|
1183
|
+
while (!detached && !TERMINAL_RUN_STATUSES2.has(runStatusFromPayload(snapshot.run))) {
|
|
325
1184
|
await Bun.sleep(pollMs);
|
|
326
|
-
snapshot = await readOperatorSnapshot(context, input.runId);
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
1185
|
+
snapshot = await readOperatorSnapshot(context, input.runId, { timelineCursor });
|
|
1186
|
+
timelineCursor = snapshot.timelineCursor;
|
|
1187
|
+
surface.renderSnapshot(snapshot);
|
|
1188
|
+
surface.renderTimeline(snapshot.timeline);
|
|
1189
|
+
surface.renderLogs(snapshot.logs);
|
|
331
1190
|
}
|
|
332
|
-
|
|
1191
|
+
commandInput?.close();
|
|
333
1192
|
}
|
|
334
1193
|
return { ...snapshot, steered, detached };
|
|
335
1194
|
}
|
|
336
1195
|
export {
|
|
337
1196
|
renderOperatorSnapshot,
|
|
1197
|
+
createPiRunStreamRenderer,
|
|
338
1198
|
attachRunOperatorView,
|
|
339
1199
|
applyOperatorCommand
|
|
340
1200
|
};
|