@h-rig/cli 0.0.6-alpha.6 → 0.0.6-alpha.60

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.
Files changed (50) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/rig.js +4184 -1228
  3. package/dist/src/commands/_cli-format.js +369 -0
  4. package/dist/src/commands/_connection-state.js +12 -6
  5. package/dist/src/commands/_doctor-checks.js +79 -34
  6. package/dist/src/commands/_help-catalog.js +445 -0
  7. package/dist/src/commands/_operator-surface.js +220 -0
  8. package/dist/src/commands/_operator-view.js +1124 -64
  9. package/dist/src/commands/_parsers.js +0 -2
  10. package/dist/src/commands/_pi-frontend.js +1080 -0
  11. package/dist/src/commands/_pi-install.js +4 -3
  12. package/dist/src/commands/_pi-remote-session.js +771 -0
  13. package/dist/src/commands/_pi-worker-bridge-extension.js +834 -0
  14. package/dist/src/commands/_policy.js +0 -2
  15. package/dist/src/commands/_preflight.js +98 -116
  16. package/dist/src/commands/_run-driver-helpers.js +34 -2
  17. package/dist/src/commands/_server-client.js +225 -48
  18. package/dist/src/commands/_snapshot-upload.js +74 -30
  19. package/dist/src/commands/_spinner.js +63 -0
  20. package/dist/src/commands/_task-picker.js +44 -16
  21. package/dist/src/commands/agent.js +8 -9
  22. package/dist/src/commands/browser.js +4 -6
  23. package/dist/src/commands/connect.js +134 -26
  24. package/dist/src/commands/dist.js +4 -6
  25. package/dist/src/commands/doctor.js +79 -34
  26. package/dist/src/commands/github.js +76 -32
  27. package/dist/src/commands/inbox.js +410 -31
  28. package/dist/src/commands/init.js +398 -90
  29. package/dist/src/commands/inspect.js +296 -23
  30. package/dist/src/commands/inspector.js +2 -4
  31. package/dist/src/commands/pi.js +168 -0
  32. package/dist/src/commands/plugin.js +81 -22
  33. package/dist/src/commands/profile-and-review.js +8 -10
  34. package/dist/src/commands/queue.js +2 -3
  35. package/dist/src/commands/remote.js +18 -20
  36. package/dist/src/commands/repo-git-harness.js +6 -8
  37. package/dist/src/commands/run.js +1422 -131
  38. package/dist/src/commands/server.js +280 -40
  39. package/dist/src/commands/setup.js +84 -45
  40. package/dist/src/commands/task-report-bug.js +5 -7
  41. package/dist/src/commands/task-run-driver.js +710 -70
  42. package/dist/src/commands/task.js +1861 -260
  43. package/dist/src/commands/test.js +3 -5
  44. package/dist/src/commands/workspace.js +4 -6
  45. package/dist/src/commands.js +4143 -1181
  46. package/dist/src/index.js +4156 -1203
  47. package/dist/src/launcher.js +5 -3
  48. package/dist/src/report-bug.js +3 -3
  49. package/dist/src/runner.js +5 -19
  50. package/package.json +7 -5
@@ -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
 
@@ -44,6 +38,11 @@ function readJsonFile(path) {
44
38
  throw new CliError2(`Invalid Rig connection state at ${path}: ${error instanceof Error ? error.message : String(error)}`, 1);
45
39
  }
46
40
  }
41
+ function writeJsonFile(path, value) {
42
+ mkdirSync(dirname(path), { recursive: true });
43
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}
44
+ `, "utf8");
45
+ }
47
46
  function normalizeConnection(value) {
48
47
  if (!value || typeof value !== "object" || Array.isArray(value))
49
48
  return null;
@@ -84,25 +83,35 @@ function readRepoConnection(projectRoot) {
84
83
  return {
85
84
  selected,
86
85
  project: typeof record.project === "string" ? record.project : undefined,
87
- linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined
86
+ linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined,
87
+ serverProjectRoot: typeof record.serverProjectRoot === "string" && record.serverProjectRoot.trim() ? record.serverProjectRoot.trim() : undefined
88
88
  };
89
89
  }
90
+ function writeRepoConnection(projectRoot, state) {
91
+ writeJsonFile(resolveRepoConnectionPath(projectRoot), state);
92
+ }
90
93
  function resolveSelectedConnection(projectRoot, options = {}) {
91
94
  const repo = readRepoConnection(projectRoot);
92
95
  if (!repo)
93
96
  return null;
94
97
  if (repo.selected === "local")
95
- return { alias: "local", connection: { kind: "local", mode: "auto" } };
98
+ return { alias: "local", connection: { kind: "local", mode: "auto" }, serverProjectRoot: repo.serverProjectRoot };
96
99
  const global = readGlobalConnections(options);
97
100
  const connection = global.connections[repo.selected];
98
101
  if (!connection) {
99
- throw new CliError2(`Selected Rig connection "${repo.selected}" was not found. Run \`rig connect list\` or \`rig connect use local\`.`, 1);
102
+ throw new CliError2(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
100
103
  }
101
- return { alias: repo.selected, connection };
104
+ return { alias: repo.selected, connection, serverProjectRoot: repo.serverProjectRoot };
105
+ }
106
+ function writeRepoServerProjectRoot(projectRoot, serverProjectRoot) {
107
+ const repo = readRepoConnection(projectRoot);
108
+ if (!repo)
109
+ return;
110
+ writeRepoConnection(projectRoot, { ...repo, serverProjectRoot });
102
111
  }
103
112
 
104
113
  // packages/cli/src/commands/_server-client.ts
105
- var cachedGitHubBearerToken;
114
+ var scopedGitHubBearerTokens = new Map;
106
115
  function cleanToken(value) {
107
116
  const trimmed = value?.trim();
108
117
  return trimmed ? trimmed : null;
@@ -119,41 +128,47 @@ function readPrivateRemoteSessionToken(projectRoot) {
119
128
  }
120
129
  }
121
130
  function readGitHubBearerTokenForRemote(projectRoot) {
122
- if (cachedGitHubBearerToken !== undefined)
123
- return cachedGitHubBearerToken;
131
+ const scopedKey = resolve2(projectRoot);
132
+ if (scopedGitHubBearerTokens.has(scopedKey))
133
+ return scopedGitHubBearerTokens.get(scopedKey) ?? null;
124
134
  const privateSession = readPrivateRemoteSessionToken(projectRoot);
125
- if (privateSession) {
126
- cachedGitHubBearerToken = privateSession;
127
- return cachedGitHubBearerToken;
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;
135
+ if (privateSession)
136
+ return privateSession;
137
+ return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
138
+ }
139
+ function readStoredGitHubAuthToken(projectRoot) {
140
+ const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
141
+ if (!existsSync2(path))
142
+ return null;
143
+ try {
144
+ const parsed = JSON.parse(readFileSync2(path, "utf8"));
145
+ return cleanToken(typeof parsed.token === "string" ? parsed.token : undefined);
146
+ } catch {
147
+ return null;
148
+ }
149
+ }
150
+ function readLocalConnectionFallbackToken(projectRoot) {
151
+ return readGitHubBearerTokenForRemote(projectRoot) ?? cleanToken(process.env.RIG_GITHUB_TOKEN) ?? readStoredGitHubAuthToken(projectRoot);
141
152
  }
142
153
  async function ensureServerForCli(projectRoot) {
143
154
  try {
144
155
  const selected = resolveSelectedConnection(projectRoot);
145
156
  if (selected?.connection.kind === "remote") {
157
+ const authToken = readGitHubBearerTokenForRemote(projectRoot);
158
+ const serverProjectRoot = selected.serverProjectRoot ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
146
159
  return {
147
160
  baseUrl: selected.connection.baseUrl,
148
- authToken: readGitHubBearerTokenForRemote(projectRoot),
149
- connectionKind: "remote"
161
+ authToken,
162
+ connectionKind: "remote",
163
+ serverProjectRoot
150
164
  };
151
165
  }
152
166
  const connection = await ensureLocalRigServerConnection(projectRoot);
153
167
  return {
154
168
  baseUrl: connection.baseUrl,
155
- authToken: connection.authToken,
156
- connectionKind: "local"
169
+ authToken: connection.authToken ?? readLocalConnectionFallbackToken(projectRoot),
170
+ connectionKind: "local",
171
+ serverProjectRoot: resolve2(projectRoot)
157
172
  };
158
173
  } catch (error) {
159
174
  if (error instanceof Error) {
@@ -162,6 +177,29 @@ async function ensureServerForCli(projectRoot) {
162
177
  throw error;
163
178
  }
164
179
  }
180
+ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken) {
181
+ const repo = readRepoConnection(projectRoot);
182
+ const slug = repo?.project?.trim();
183
+ if (!slug)
184
+ return null;
185
+ try {
186
+ const response = await fetch(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`, {
187
+ headers: mergeHeaders(undefined, authToken)
188
+ });
189
+ if (!response.ok)
190
+ return null;
191
+ const payload = await response.json();
192
+ const project = payload.project && typeof payload.project === "object" && !Array.isArray(payload.project) ? payload.project : null;
193
+ const checkouts = Array.isArray(project?.checkouts) ? project.checkouts : [];
194
+ const latestCheckout = [...checkouts].reverse().find((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry) && typeof entry.path === "string"));
195
+ const path = typeof latestCheckout?.path === "string" && latestCheckout.path.trim() ? latestCheckout.path.trim() : null;
196
+ if (path)
197
+ writeRepoServerProjectRoot(projectRoot, path);
198
+ return path;
199
+ } catch {
200
+ return null;
201
+ }
202
+ }
165
203
  function mergeHeaders(headers, authToken) {
166
204
  const merged = new Headers(headers);
167
205
  if (authToken) {
@@ -186,9 +224,12 @@ function diagnosticMessage(payload) {
186
224
  }
187
225
  async function requestServerJson(context, pathname, init = {}) {
188
226
  const server = await ensureServerForCli(context.projectRoot);
227
+ const headers = mergeHeaders(init.headers, server.authToken);
228
+ if (server.serverProjectRoot)
229
+ headers.set("x-rig-project-root", server.serverProjectRoot);
189
230
  const response = await fetch(`${server.baseUrl}${pathname}`, {
190
231
  ...init,
191
- headers: mergeHeaders(init.headers, server.authToken)
232
+ headers
192
233
  });
193
234
  const text = await response.text();
194
235
  const payload = text.trim().length > 0 ? (() => {
@@ -218,6 +259,15 @@ async function getRunLogsViaServer(context, runId, options = {}) {
218
259
  const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
219
260
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { entries: [] };
220
261
  }
262
+ async function getRunTimelineViaServer(context, runId, options = {}) {
263
+ const url = new URL(`http://rig.local/api/runs/${encodeURIComponent(runId)}/timeline`);
264
+ if (options.limit !== undefined)
265
+ url.searchParams.set("limit", String(options.limit));
266
+ if (options.cursor)
267
+ url.searchParams.set("cursor", options.cursor);
268
+ const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
269
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { entries: [] };
270
+ }
221
271
  async function stopRunViaServer(context, runId) {
222
272
  const payload = await requestServerJson(context, "/api/runs/stop", {
223
273
  method: "POST",
@@ -234,9 +284,75 @@ async function steerRunViaServer(context, runId, message) {
234
284
  });
235
285
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
236
286
  }
287
+ async function getRunPiSessionViaServer(context, runId) {
288
+ const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi`);
289
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
290
+ }
291
+ async function getRunPiMessagesViaServer(context, runId) {
292
+ const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/messages`);
293
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { messages: [] };
294
+ }
295
+ async function getRunPiStatusViaServer(context, runId) {
296
+ const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/status`);
297
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
298
+ }
299
+ async function getRunPiCommandsViaServer(context, runId) {
300
+ const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/commands`);
301
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { commands: [] };
302
+ }
303
+ async function getRunPiCapabilitiesViaServer(context, runId) {
304
+ const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/capabilities`);
305
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { capabilities: null };
306
+ }
307
+ async function sendRunPiPromptViaServer(context, runId, text, streamingBehavior) {
308
+ const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/prompt`, {
309
+ method: "POST",
310
+ headers: { "content-type": "application/json" },
311
+ body: JSON.stringify({ text, streamingBehavior })
312
+ });
313
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { accepted: true };
314
+ }
315
+ async function sendRunPiShellViaServer(context, runId, text) {
316
+ const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/shell`, {
317
+ method: "POST",
318
+ headers: { "content-type": "application/json" },
319
+ body: JSON.stringify({ text })
320
+ });
321
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { accepted: true };
322
+ }
323
+ async function runRunPiCommandViaServer(context, runId, text) {
324
+ const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/commands/run`, {
325
+ method: "POST",
326
+ headers: { "content-type": "application/json" },
327
+ body: JSON.stringify({ text })
328
+ });
329
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { type: "done" };
330
+ }
331
+ async function respondRunPiExtensionUiViaServer(context, runId, requestId, valueOrCancel) {
332
+ const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/extension-ui/respond`, {
333
+ method: "POST",
334
+ headers: { "content-type": "application/json" },
335
+ body: JSON.stringify({ requestId, ...valueOrCancel })
336
+ });
337
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { accepted: true };
338
+ }
339
+ async function abortRunPiViaServer(context, runId) {
340
+ const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/abort`, { method: "POST" });
341
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { aborted: true };
342
+ }
343
+ async function buildRunPiEventsWebSocketUrl(context, runId) {
344
+ const server = await ensureServerForCli(context.projectRoot);
345
+ const url = new URL(`${server.baseUrl.replace(/\/+$/, "")}/api/runs/${encodeURIComponent(runId)}/pi/events`);
346
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
347
+ if (server.authToken)
348
+ url.searchParams.set("token", server.authToken);
349
+ if (server.serverProjectRoot)
350
+ url.searchParams.set("rigProjectRoot", server.serverProjectRoot);
351
+ return url.toString();
352
+ }
237
353
 
238
- // packages/cli/src/commands/_operator-view.ts
239
- var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
354
+ // packages/cli/src/commands/_operator-surface.ts
355
+ import { createInterface } from "readline";
240
356
  var CANONICAL_STAGES = [
241
357
  "Connect",
242
358
  "GitHub/task sync",
@@ -251,18 +367,943 @@ var CANONICAL_STAGES = [
251
367
  "Merge",
252
368
  "Complete"
253
369
  ];
370
+ function logDetail(log) {
371
+ return typeof log.detail === "string" ? log.detail.trim() : "";
372
+ }
373
+ function parseProviderProtocolLog(title, detail) {
374
+ if (title.trim().toLowerCase() !== "agent output")
375
+ return null;
376
+ if (!detail.startsWith("{") || !detail.endsWith("}"))
377
+ return null;
378
+ try {
379
+ const record = JSON.parse(detail);
380
+ if (!record || typeof record !== "object" || Array.isArray(record))
381
+ return null;
382
+ const type = record.type;
383
+ return typeof type === "string" && [
384
+ "assistant",
385
+ "message_start",
386
+ "message_update",
387
+ "message_end",
388
+ "stream_event",
389
+ "tool_result",
390
+ "tool_execution_start",
391
+ "tool_execution_update",
392
+ "tool_execution_end",
393
+ "turn_start",
394
+ "turn_end"
395
+ ].includes(type) ? record : null;
396
+ } catch {
397
+ return null;
398
+ }
399
+ }
400
+ function renderProviderProtocolLog(record) {
401
+ const type = typeof record.type === "string" ? record.type : "";
402
+ if (type === "tool_execution_start" || type === "tool_execution_update" || type === "tool_execution_end") {
403
+ const toolName = String(record.toolName ?? record.name ?? "tool");
404
+ 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";
405
+ return `[Pi tool] ${toolName} ${status}`;
406
+ }
407
+ return null;
408
+ }
409
+ function entryId(entry, fallback) {
410
+ return typeof entry.id === "string" && entry.id.trim() ? entry.id : fallback;
411
+ }
254
412
  function renderOperatorSnapshot(snapshot) {
255
413
  const run = snapshot.run.run && typeof snapshot.run.run === "object" ? snapshot.run.run : snapshot.run;
256
414
  const runId = String(run.runId ?? run.id ?? "run");
257
415
  const status = String(run.status ?? "unknown");
258
416
  const logs = snapshot.logs ?? [];
417
+ const latestByStage = new Map;
418
+ for (const log of logs) {
419
+ const title = String(log.title ?? "").toLowerCase();
420
+ const stageName = String(log.stage ?? "").toLowerCase();
421
+ const stage = CANONICAL_STAGES.find((candidate) => candidate.toLowerCase() === title || candidate.toLowerCase() === stageName);
422
+ if (stage)
423
+ latestByStage.set(stage, log);
424
+ }
259
425
  const stageLines = CANONICAL_STAGES.flatMap((stage) => {
260
- const match = logs.find((log) => String(log.title ?? "").toLowerCase() === stage.toLowerCase() || String(log.stage ?? "").toLowerCase() === stage.toLowerCase());
261
- return match ? [`${stage}: ${String(match.status ?? status)}`] : [];
426
+ const match = latestByStage.get(stage);
427
+ return match ? [`${stage}: ${String(match.status ?? status)}${logDetail(match) ? ` \u2014 ${logDetail(match)}` : ""}`] : [];
262
428
  });
263
429
  return [`Rig run ${runId}: ${status}`, ...stageLines].join(`
264
430
  `);
265
431
  }
432
+ function createPiRunStreamRenderer(output = process.stdout) {
433
+ let lastSnapshot = "";
434
+ const assistantTextById = new Map;
435
+ const seenTimeline = new Set;
436
+ const seenLogs = new Set;
437
+ const writeLine = (line) => output.write(`${line}
438
+ `);
439
+ return {
440
+ renderSnapshot(snapshot) {
441
+ const rendered = renderOperatorSnapshot(snapshot);
442
+ if (rendered && rendered !== lastSnapshot) {
443
+ writeLine(rendered);
444
+ lastSnapshot = rendered;
445
+ }
446
+ },
447
+ renderTimeline(entries) {
448
+ for (const [index, entry] of entries.entries()) {
449
+ const id = entryId(entry, `timeline:${index}:${String(entry.cursor ?? "")}`);
450
+ if (entry.type === "assistant_message" && typeof entry.text === "string") {
451
+ const text = entry.text;
452
+ const previousText = assistantTextById.get(id) ?? "";
453
+ if (!previousText && text.trim()) {
454
+ writeLine("[Pi assistant]");
455
+ }
456
+ if (text.startsWith(previousText)) {
457
+ const delta = text.slice(previousText.length);
458
+ if (delta)
459
+ output.write(delta);
460
+ } else if (text.trim() && text !== previousText) {
461
+ if (previousText)
462
+ writeLine(`
463
+ [Pi assistant]`);
464
+ output.write(text);
465
+ }
466
+ assistantTextById.set(id, text);
467
+ continue;
468
+ }
469
+ if (seenTimeline.has(id))
470
+ continue;
471
+ seenTimeline.add(id);
472
+ if (entry.type === "tool_execution_start" || entry.type === "tool_execution_update" || entry.type === "tool_execution_end" || entry.type === "mcp_tool_call") {
473
+ writeLine(`[Pi tool] ${String(entry.toolName ?? entry.name ?? entry.title ?? entry.type)} ${String(entry.status ?? entry.state ?? "")}`.trim());
474
+ continue;
475
+ }
476
+ if (entry.type === "timeline_warning") {
477
+ writeLine(`[Rig timeline] ${String(entry.detail ?? entry.message ?? "timeline unavailable")}`);
478
+ continue;
479
+ }
480
+ if (entry.type === "action") {
481
+ const text = String(entry.detail ?? entry.message ?? entry.title ?? "").trim();
482
+ if (text)
483
+ writeLine(`[Rig action] ${text}`);
484
+ continue;
485
+ }
486
+ if (entry.type === "user_message") {
487
+ const text = String(entry.text ?? entry.message ?? entry.detail ?? "").trim();
488
+ if (text)
489
+ writeLine(`[Operator] ${text}`);
490
+ continue;
491
+ }
492
+ const fallback = String(entry.detail ?? entry.message ?? entry.text ?? entry.title ?? "").trim();
493
+ if (fallback)
494
+ writeLine(`[${String(entry.type ?? "timeline")}] ${fallback}`);
495
+ }
496
+ },
497
+ renderLogs(entries) {
498
+ for (const [index, entry] of entries.entries()) {
499
+ const id = entryId(entry, `log:${index}:${String(entry.createdAt ?? "")}:${String(entry.title ?? "")}`);
500
+ if (seenLogs.has(id))
501
+ continue;
502
+ seenLogs.add(id);
503
+ const title = String(entry.title ?? "");
504
+ if (CANONICAL_STAGES.some((stage) => stage.toLowerCase() === title.toLowerCase()))
505
+ continue;
506
+ const detail = logDetail(entry);
507
+ if (!detail)
508
+ continue;
509
+ const protocolRecord = parseProviderProtocolLog(title, detail);
510
+ if (protocolRecord) {
511
+ const protocolLine = renderProviderProtocolLog(protocolRecord);
512
+ if (protocolLine)
513
+ writeLine(protocolLine);
514
+ continue;
515
+ }
516
+ writeLine(`[${title || "Rig log"}] ${detail}`);
517
+ }
518
+ }
519
+ };
520
+ }
521
+ function createOperatorSurface(options = {}) {
522
+ const input = options.input ?? process.stdin;
523
+ const output = options.output ?? process.stdout;
524
+ const errorOutput = options.errorOutput ?? process.stderr;
525
+ const renderer = createPiRunStreamRenderer(output);
526
+ const writeLine = (line) => output.write(`${line}
527
+ `);
528
+ return {
529
+ mode: "pi-compatible-text",
530
+ ...renderer,
531
+ info: writeLine,
532
+ error: (message) => errorOutput.write(`${message}
533
+ `),
534
+ attachCommandInput(handler) {
535
+ if (options.interactive === false || !input.isTTY)
536
+ return null;
537
+ const rl = createInterface({ input, output: process.stdout, terminal: false });
538
+ rl.on("line", (line) => {
539
+ Promise.resolve(handler(line)).catch((error) => writeLine(`Operator command failed: ${error instanceof Error ? error.message : String(error)}`));
540
+ });
541
+ return { close: () => rl.close() };
542
+ }
543
+ };
544
+ }
545
+
546
+ // packages/cli/src/commands/_pi-frontend.ts
547
+ import { mkdtempSync, rmSync } from "fs";
548
+ import { tmpdir } from "os";
549
+ import { join } from "path";
550
+ import {
551
+ createAgentSessionFromServices,
552
+ createAgentSessionServices,
553
+ main as runPiMain
554
+ } from "@earendil-works/pi-coding-agent";
555
+
556
+ // packages/cli/src/commands/_pi-remote-session.ts
557
+ import { AgentSession as PiAgentSession, expandPromptTemplate } from "@earendil-works/pi-coding-agent";
558
+ var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
559
+ function defaultTransport() {
560
+ return {
561
+ getSession: getRunPiSessionViaServer,
562
+ getMessages: getRunPiMessagesViaServer,
563
+ getStatus: getRunPiStatusViaServer,
564
+ getCommands: getRunPiCommandsViaServer,
565
+ getCapabilities: getRunPiCapabilitiesViaServer,
566
+ sendPrompt: sendRunPiPromptViaServer,
567
+ sendShell: sendRunPiShellViaServer,
568
+ runCommand: runRunPiCommandViaServer,
569
+ abort: abortRunPiViaServer,
570
+ buildEventsWebSocketUrl: buildRunPiEventsWebSocketUrl
571
+ };
572
+ }
573
+ function recordOf(value) {
574
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
575
+ }
576
+ function emptyRemoteStatus() {
577
+ return {
578
+ isStreaming: false,
579
+ isCompacting: false,
580
+ isBashRunning: false,
581
+ pendingMessageCount: 0,
582
+ steeringMessages: [],
583
+ followUpMessages: [],
584
+ model: null,
585
+ thinkingLevel: null,
586
+ sessionName: null,
587
+ cwd: null,
588
+ stats: null,
589
+ contextUsage: null
590
+ };
591
+ }
592
+ function resolveAttachReadyTimeoutMs() {
593
+ const raw = Number.parseInt(process.env.RIG_PI_ATTACH_TIMEOUT_MS ?? "", 10);
594
+ return Number.isFinite(raw) && raw > 0 ? raw : 10 * 60000;
595
+ }
596
+
597
+ class RigRemoteSessionController {
598
+ context;
599
+ runId;
600
+ status = emptyRemoteStatus();
601
+ transport;
602
+ hooks = {};
603
+ session = null;
604
+ socket = null;
605
+ closed = false;
606
+ pendingShells = [];
607
+ pendingCompactions = [];
608
+ constructor(input) {
609
+ this.context = input.context;
610
+ this.runId = input.runId;
611
+ this.transport = input.transport ?? defaultTransport();
612
+ }
613
+ ingestEnvelope(envelopeValue) {
614
+ this.applyEnvelope(envelopeValue);
615
+ }
616
+ bindSession(session) {
617
+ this.session = session;
618
+ }
619
+ setUiHooks(hooks) {
620
+ this.hooks = hooks;
621
+ }
622
+ async connect() {
623
+ const ready = await this.waitForReady();
624
+ if (!ready || this.closed)
625
+ return;
626
+ let catchupDone = false;
627
+ const buffered = [];
628
+ const wsUrl = await this.transport.buildEventsWebSocketUrl(this.context, this.runId);
629
+ const socket = new WebSocket(wsUrl);
630
+ this.socket = socket;
631
+ socket.onopen = () => {
632
+ this.hooks.onConnectionChange?.(true);
633
+ this.hooks.onStatusText?.("worker session live");
634
+ };
635
+ socket.onmessage = (message) => {
636
+ try {
637
+ const payload = typeof message.data === "string" ? JSON.parse(message.data) : JSON.parse(Buffer.from(message.data).toString("utf8"));
638
+ if (!catchupDone)
639
+ buffered.push(payload);
640
+ else
641
+ this.applyEnvelope(payload);
642
+ } catch (error) {
643
+ this.hooks.onError?.(`Unparseable worker Pi event: ${error instanceof Error ? error.message : String(error)}`);
644
+ }
645
+ };
646
+ socket.onerror = () => socket.close();
647
+ socket.onclose = () => {
648
+ this.hooks.onConnectionChange?.(false);
649
+ if (!this.closed)
650
+ this.hooks.onStatusText?.("worker Pi WebSocket disconnected");
651
+ };
652
+ try {
653
+ const [messagesPayload, statusPayload, commandsPayload, capabilitiesPayload] = await Promise.all([
654
+ this.transport.getMessages(this.context, this.runId),
655
+ this.transport.getStatus(this.context, this.runId),
656
+ this.transport.getCommands(this.context, this.runId),
657
+ this.transport.getCapabilities(this.context, this.runId).catch(() => ({ capabilities: null }))
658
+ ]);
659
+ const messages = Array.isArray(messagesPayload.messages) ? messagesPayload.messages : [];
660
+ this.applyStatusPayload(statusPayload);
661
+ this.session?.replaceRemoteMessages(messages);
662
+ const commands = Array.isArray(commandsPayload.commands) ? commandsPayload.commands : [];
663
+ const capabilities = capabilitiesPayload.capabilities && typeof capabilitiesPayload.capabilities === "object" && !Array.isArray(capabilitiesPayload.capabilities) ? capabilitiesPayload.capabilities : null;
664
+ this.hooks.onCatchUp?.(messages, commands, capabilities);
665
+ catchupDone = true;
666
+ for (const payload of buffered.splice(0))
667
+ this.applyEnvelope(payload);
668
+ } catch (error) {
669
+ catchupDone = true;
670
+ this.hooks.onError?.(`Worker Pi catch-up failed: ${error instanceof Error ? error.message : String(error)}`);
671
+ }
672
+ }
673
+ close() {
674
+ this.closed = true;
675
+ this.socket?.close();
676
+ for (const shell of this.pendingShells.splice(0))
677
+ shell.reject(new Error("Remote session closed."));
678
+ for (const compaction of this.pendingCompactions.splice(0))
679
+ compaction.reject(new Error("Remote session closed."));
680
+ }
681
+ async sendPrompt(text, streamingBehavior) {
682
+ await this.transport.sendPrompt(this.context, this.runId, text, streamingBehavior);
683
+ }
684
+ async sendCommand(text) {
685
+ const result = await this.transport.runCommand(this.context, this.runId, text);
686
+ return typeof result.message === "string" ? result.message : "worker command accepted";
687
+ }
688
+ async sendShell(text) {
689
+ await this.transport.sendShell(this.context, this.runId, text);
690
+ }
691
+ async abort() {
692
+ await this.transport.abort(this.context, this.runId);
693
+ }
694
+ registerPendingShell(shell) {
695
+ this.pendingShells.push(shell);
696
+ }
697
+ failPendingShell(shell, error) {
698
+ const index = this.pendingShells.indexOf(shell);
699
+ if (index !== -1)
700
+ this.pendingShells.splice(index, 1);
701
+ shell.reject(error);
702
+ }
703
+ registerPendingCompaction(pending) {
704
+ this.pendingCompactions.push(pending);
705
+ }
706
+ async waitForReady() {
707
+ const startedAt = Date.now();
708
+ const deadline = startedAt + resolveAttachReadyTimeoutMs();
709
+ let consecutiveFailures = 0;
710
+ while (!this.closed) {
711
+ let requestFailed = false;
712
+ const session = await this.transport.getSession(this.context, this.runId).catch((error) => {
713
+ requestFailed = true;
714
+ return { ready: false, status: error instanceof Error ? error.message : String(error), retryAfterMs: 1000 };
715
+ });
716
+ if (session.ready !== false)
717
+ return true;
718
+ consecutiveFailures = requestFailed ? consecutiveFailures + 1 : 0;
719
+ const status = String(session.status ?? "starting");
720
+ if (TERMINAL_RUN_STATUSES.has(status.toLowerCase())) {
721
+ this.hooks.onError?.(`Run ended before the worker Pi daemon became ready: ${status}. Inspect with \`rig run show ${this.runId}\`.`);
722
+ return false;
723
+ }
724
+ 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`);
725
+ if (Date.now() >= deadline) {
726
+ this.hooks.onError?.(`Worker Pi daemon did not become ready (last status: ${status}). Set RIG_PI_ATTACH_TIMEOUT_MS to wait longer.`);
727
+ return false;
728
+ }
729
+ await Bun.sleep(typeof session.retryAfterMs === "number" ? session.retryAfterMs : 750);
730
+ }
731
+ return false;
732
+ }
733
+ applyEnvelope(envelopeValue) {
734
+ const envelope = recordOf(envelopeValue);
735
+ if (!envelope)
736
+ return;
737
+ const type = String(envelope.type ?? "");
738
+ if (type === "status.update") {
739
+ this.applyStatusPayload(envelope);
740
+ return;
741
+ }
742
+ if (type === "activity.update") {
743
+ const activity = recordOf(envelope.activity);
744
+ this.hooks.onActivity?.(String(activity?.label ?? ""), activity?.detail ? String(activity.detail) : undefined);
745
+ return;
746
+ }
747
+ if (type === "extension_ui_request") {
748
+ const request = recordOf(envelope.request);
749
+ if (request)
750
+ this.hooks.onExtensionUiRequest?.(request);
751
+ return;
752
+ }
753
+ if (type === "pi.ui_event") {
754
+ this.applyShellUiEvent(envelope.event);
755
+ return;
756
+ }
757
+ if (type === "pi.event") {
758
+ this.session?.handleRemoteSessionEvent(envelope.event);
759
+ this.settlePendingCompaction(envelope.event);
760
+ return;
761
+ }
762
+ if (type === "error") {
763
+ this.hooks.onError?.(String(envelope.message ?? envelope.detail ?? "unknown worker error"));
764
+ }
765
+ }
766
+ applyStatusPayload(payload) {
767
+ const status = recordOf(payload.status) ?? payload;
768
+ const next = this.status;
769
+ next.isStreaming = status.isStreaming === true;
770
+ next.isCompacting = status.isCompacting === true;
771
+ next.isBashRunning = status.isBashRunning === true;
772
+ next.pendingMessageCount = typeof status.pendingMessageCount === "number" ? status.pendingMessageCount : 0;
773
+ next.steeringMessages = Array.isArray(status.steeringMessages) ? status.steeringMessages.map(String) : [];
774
+ next.followUpMessages = Array.isArray(status.followUpMessages) ? status.followUpMessages.map(String) : [];
775
+ next.model = recordOf(status.model) ?? next.model;
776
+ next.thinkingLevel = typeof status.thinkingLevel === "string" ? status.thinkingLevel : next.thinkingLevel;
777
+ next.sessionName = typeof status.sessionName === "string" ? status.sessionName : next.sessionName;
778
+ next.cwd = typeof status.cwd === "string" ? status.cwd : next.cwd;
779
+ next.stats = recordOf(status.stats) ?? next.stats;
780
+ next.contextUsage = recordOf(status.contextUsage) ?? next.contextUsage;
781
+ }
782
+ applyShellUiEvent(value) {
783
+ const event = recordOf(value);
784
+ if (!event)
785
+ return;
786
+ const type = String(event.type ?? "");
787
+ const pending = this.pendingShells[0];
788
+ if (type === "shell.chunk" && pending) {
789
+ pending.sawChunk = true;
790
+ pending.onData(Buffer.from(String(event.chunk ?? "")));
791
+ return;
792
+ }
793
+ if (type === "shell.end" && pending) {
794
+ const output = String(event.output ?? "");
795
+ if (output && !pending.sawChunk)
796
+ pending.onData(Buffer.from(output));
797
+ const index = this.pendingShells.indexOf(pending);
798
+ if (index !== -1)
799
+ this.pendingShells.splice(index, 1);
800
+ pending.resolve({ exitCode: typeof event.exitCode === "number" ? event.exitCode : event.isError === true ? 1 : 0 });
801
+ }
802
+ }
803
+ settlePendingCompaction(eventValue) {
804
+ const event = recordOf(eventValue);
805
+ if (!event)
806
+ return;
807
+ const type = String(event.type ?? "");
808
+ if (type !== "compaction_end" || this.pendingCompactions.length === 0)
809
+ return;
810
+ const pending = this.pendingCompactions.shift();
811
+ const result = recordOf(event.result);
812
+ if (result)
813
+ pending.resolve(result);
814
+ else if (event.aborted === true)
815
+ pending.reject(new Error("Compaction aborted on the worker."));
816
+ else
817
+ pending.resolve({});
818
+ }
819
+ }
820
+
821
+ class RigRemoteAgentSession extends PiAgentSession {
822
+ remote;
823
+ constructor(config, remote) {
824
+ super(config);
825
+ this.remote = remote;
826
+ remote.bindSession(this);
827
+ }
828
+ handleRemoteSessionEvent(eventValue) {
829
+ const event = recordOf(eventValue);
830
+ if (!event)
831
+ return;
832
+ const type = String(event.type ?? "");
833
+ if ((type === "message_end" || type === "turn_end") && recordOf(event.message)) {
834
+ this.agent.state.messages = [...this.agent.state.messages, event.message];
835
+ }
836
+ this._emit(eventValue);
837
+ }
838
+ replaceRemoteMessages(messages) {
839
+ this.agent.state.messages = messages;
840
+ }
841
+ async expandLocalInput(text) {
842
+ let current = text;
843
+ const runner = this.extensionRunner;
844
+ if (runner.hasHandlers("input")) {
845
+ const result = await runner.emitInput(current, undefined, "interactive");
846
+ if (result.action === "handled")
847
+ return null;
848
+ if (result.action === "transform")
849
+ current = result.text;
850
+ }
851
+ current = this._expandSkillCommand(current);
852
+ current = expandPromptTemplate(current, [...this.promptTemplates]);
853
+ return current;
854
+ }
855
+ async prompt(text, options) {
856
+ const trimmed = text.trim();
857
+ if (!trimmed)
858
+ return;
859
+ if (trimmed.startsWith("/")) {
860
+ if (await this._tryExecuteExtensionCommand(trimmed))
861
+ return;
862
+ const expanded2 = await this.expandLocalInput(trimmed);
863
+ if (expanded2 === null)
864
+ return;
865
+ if (expanded2 !== trimmed) {
866
+ const behavior2 = options?.streamingBehavior ?? (this.isStreaming || this.isCompacting ? "steer" : undefined);
867
+ options?.preflightResult?.(true);
868
+ await this.remote.sendPrompt(expanded2, behavior2);
869
+ return;
870
+ }
871
+ await this.remote.sendCommand(trimmed);
872
+ return;
873
+ }
874
+ if (trimmed.startsWith("!")) {
875
+ await this.remote.sendShell(trimmed);
876
+ return;
877
+ }
878
+ const expanded = await this.expandLocalInput(trimmed);
879
+ if (expanded === null)
880
+ return;
881
+ const behavior = options?.streamingBehavior ?? (this.isStreaming || this.isCompacting ? "steer" : undefined);
882
+ options?.preflightResult?.(true);
883
+ await this.remote.sendPrompt(expanded, behavior);
884
+ }
885
+ async steer(text) {
886
+ const trimmed = text.trim();
887
+ if (trimmed.startsWith("/") && await this._tryExecuteExtensionCommand(trimmed))
888
+ return;
889
+ const expanded = await this.expandLocalInput(trimmed);
890
+ if (expanded === null)
891
+ return;
892
+ await this.remote.sendPrompt(expanded, "steer");
893
+ }
894
+ async followUp(text) {
895
+ const trimmed = text.trim();
896
+ if (trimmed.startsWith("/") && await this._tryExecuteExtensionCommand(trimmed))
897
+ return;
898
+ const expanded = await this.expandLocalInput(trimmed);
899
+ if (expanded === null)
900
+ return;
901
+ await this.remote.sendPrompt(expanded, "followUp");
902
+ }
903
+ async abort() {
904
+ await this.remote.abort();
905
+ }
906
+ async compact(customInstructions) {
907
+ const pending = new Promise((resolve3, reject) => {
908
+ this.remote.registerPendingCompaction({ resolve: resolve3, reject });
909
+ });
910
+ await this.remote.sendCommand(`/compact${customInstructions ? ` ${customInstructions}` : ""}`);
911
+ return pending;
912
+ }
913
+ clearQueue() {
914
+ const cleared = {
915
+ steering: [...this.remote.status.steeringMessages],
916
+ followUp: [...this.remote.status.followUpMessages]
917
+ };
918
+ this.remote.status.steeringMessages = [];
919
+ this.remote.status.followUpMessages = [];
920
+ this.remote.status.pendingMessageCount = 0;
921
+ this.remote.sendCommand("/queue-clear").catch(() => {});
922
+ return cleared;
923
+ }
924
+ get isStreaming() {
925
+ return this.remote.status.isStreaming;
926
+ }
927
+ get isCompacting() {
928
+ return this.remote.status.isCompacting;
929
+ }
930
+ get isBashRunning() {
931
+ return this.remote.status.isBashRunning;
932
+ }
933
+ get pendingMessageCount() {
934
+ return this.remote.status.pendingMessageCount;
935
+ }
936
+ getSteeringMessages() {
937
+ return this.remote.status.steeringMessages;
938
+ }
939
+ getFollowUpMessages() {
940
+ return this.remote.status.followUpMessages;
941
+ }
942
+ get model() {
943
+ return this.remote.status.model ?? super.model;
944
+ }
945
+ async setModel(model) {
946
+ await this.remote.sendCommand(`/model ${model.provider}/${model.id}`);
947
+ this.remote.status.model = model;
948
+ }
949
+ get thinkingLevel() {
950
+ return this.remote.status.thinkingLevel ?? super.thinkingLevel;
951
+ }
952
+ setThinkingLevel(level) {
953
+ this.remote.status.thinkingLevel = level;
954
+ this.remote.sendCommand(`/thinking ${level}`).catch(() => {});
955
+ }
956
+ get sessionName() {
957
+ return this.remote.status.sessionName ?? super.sessionName;
958
+ }
959
+ setSessionName(name) {
960
+ this.remote.status.sessionName = name;
961
+ this.remote.sendCommand(`/name ${name}`).catch(() => {});
962
+ }
963
+ getSessionStats() {
964
+ return this.remote.status.stats ?? super.getSessionStats();
965
+ }
966
+ getContextUsage() {
967
+ return this.remote.status.contextUsage ?? super.getContextUsage();
968
+ }
969
+ dispose() {
970
+ this.remote.close();
971
+ super.dispose();
972
+ }
973
+ }
974
+ function createRemoteBashOperations(controller, excludeFromContext) {
975
+ return {
976
+ exec(command, _cwd, execOptions) {
977
+ return new Promise((resolve3, reject) => {
978
+ const pending = { onData: execOptions.onData, resolve: resolve3, reject, sawChunk: false };
979
+ const cleanup = () => {
980
+ execOptions.signal?.removeEventListener("abort", onAbort);
981
+ if (timer)
982
+ clearTimeout(timer);
983
+ };
984
+ const onAbort = () => {
985
+ cleanup();
986
+ controller.failPendingShell(pending, new Error("Remote worker shell command aborted locally."));
987
+ };
988
+ const timeoutMs = typeof execOptions.timeout === "number" && execOptions.timeout > 0 ? execOptions.timeout : 0;
989
+ const timer = timeoutMs > 0 ? setTimeout(() => {
990
+ cleanup();
991
+ controller.failPendingShell(pending, new Error(`Remote worker shell command timed out after ${timeoutMs}ms.`));
992
+ }, timeoutMs) : null;
993
+ const wrappedResolve = pending.resolve;
994
+ const wrappedReject = pending.reject;
995
+ pending.resolve = (result) => {
996
+ cleanup();
997
+ wrappedResolve(result);
998
+ };
999
+ pending.reject = (error) => {
1000
+ cleanup();
1001
+ wrappedReject(error);
1002
+ };
1003
+ execOptions.signal?.addEventListener("abort", onAbort, { once: true });
1004
+ controller.registerPendingShell(pending);
1005
+ controller.sendShell(`${excludeFromContext ? "!!" : "!"}${command}`).catch((error) => {
1006
+ cleanup();
1007
+ controller.failPendingShell(pending, error instanceof Error ? error : new Error(String(error)));
1008
+ });
1009
+ });
1010
+ }
1011
+ };
1012
+ }
1013
+
1014
+ // packages/cli/src/commands/_spinner.ts
1015
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1016
+
1017
+ // packages/cli/src/commands/_pi-worker-bridge-extension.ts
1018
+ function recordOf2(value) {
1019
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
1020
+ }
1021
+ function asText(value) {
1022
+ if (typeof value === "string")
1023
+ return value;
1024
+ if (value === null || value === undefined)
1025
+ return "";
1026
+ if (typeof value === "number" || typeof value === "boolean")
1027
+ return String(value);
1028
+ try {
1029
+ return JSON.stringify(value);
1030
+ } catch {
1031
+ return String(value);
1032
+ }
1033
+ }
1034
+ function names(value, key = "name") {
1035
+ if (!Array.isArray(value))
1036
+ return [];
1037
+ return value.flatMap((entry) => {
1038
+ if (typeof entry === "string")
1039
+ return [entry];
1040
+ const record = recordOf2(entry);
1041
+ const name = record?.[key];
1042
+ return typeof name === "string" ? [name] : [];
1043
+ });
1044
+ }
1045
+ function renderWorkerCapabilities(capabilities) {
1046
+ const lines = ["Worker session capabilities (in effect for this run)"];
1047
+ const tools = Array.isArray(capabilities.tools) ? capabilities.tools : [];
1048
+ const activeTools = tools.flatMap((tool) => {
1049
+ const record = recordOf2(tool);
1050
+ return record && record.active === true && typeof record.name === "string" ? [record.name] : [];
1051
+ });
1052
+ const inactiveCount = tools.length - activeTools.length;
1053
+ lines.push(` tools ${activeTools.join(", ") || "(none)"}${inactiveCount > 0 ? ` (+${inactiveCount} inactive)` : ""}`);
1054
+ const extensions = Array.isArray(capabilities.extensions) ? capabilities.extensions : [];
1055
+ const extensionLabels = extensions.flatMap((entry) => {
1056
+ const record = recordOf2(entry);
1057
+ if (!record)
1058
+ return [];
1059
+ const path = typeof record.path === "string" ? record.path : "";
1060
+ const short = path.split("/").filter(Boolean).slice(-2).join("/") || path || "extension";
1061
+ return [short];
1062
+ });
1063
+ lines.push(` extensions ${extensionLabels.join(", ") || "(none)"}`);
1064
+ const hookEvents = names(capabilities.hookEvents);
1065
+ const hookList = Array.isArray(capabilities.hookEvents) ? capabilities.hookEvents.map(asText).filter(Boolean) : hookEvents;
1066
+ lines.push(` hooks ${hookList.join(", ") || "(none)"}`);
1067
+ lines.push(` skills ${names(capabilities.skills).join(", ") || "(none)"}`);
1068
+ lines.push(` prompts ${names(capabilities.prompts).join(", ") || "(none)"}`);
1069
+ const model = typeof capabilities.model === "string" ? capabilities.model : "(unknown)";
1070
+ const thinking = typeof capabilities.thinkingLevel === "string" ? capabilities.thinkingLevel : "";
1071
+ const cwd = typeof capabilities.cwd === "string" ? capabilities.cwd : "";
1072
+ lines.push(` model ${model}${thinking ? ` \xB7 ${thinking}` : ""}`);
1073
+ if (cwd)
1074
+ lines.push(` cwd ${cwd}`);
1075
+ lines.push(" (worker commands are in your palette as [worker \u2026] \xB7 /worker shows this again)");
1076
+ return lines;
1077
+ }
1078
+ async function answerExtensionUiRequest(options, ctx, request) {
1079
+ const requestId = String(request.requestId ?? request.id ?? `ui-${Date.now()}`);
1080
+ const method = String(request.method ?? request.type ?? "input");
1081
+ const prompt = asText(request.prompt ?? request.message ?? request.title ?? method);
1082
+ const rawOptions = Array.isArray(request.options) ? request.options : Array.isArray(request.items) ? request.items : [];
1083
+ const choices = rawOptions.map((option) => {
1084
+ const record = recordOf2(option);
1085
+ return record ? asText(record.label ?? record.value ?? record.name ?? option) : asText(option);
1086
+ }).filter(Boolean);
1087
+ try {
1088
+ if (method === "confirm") {
1089
+ const confirmed = await ctx.ui.confirm("Worker request", prompt);
1090
+ await respondRunPiExtensionUiViaServer(options.context, options.runId, requestId, { value: confirmed, confirmed });
1091
+ return;
1092
+ }
1093
+ if (choices.length > 0) {
1094
+ const selected = await ctx.ui.select(prompt, choices);
1095
+ await respondRunPiExtensionUiViaServer(options.context, options.runId, requestId, selected === undefined ? { cancelled: true } : { value: selected });
1096
+ return;
1097
+ }
1098
+ const value = await ctx.ui.input("Worker request", prompt);
1099
+ await respondRunPiExtensionUiViaServer(options.context, options.runId, requestId, value === undefined ? { cancelled: true } : { value });
1100
+ } catch (error) {
1101
+ ctx.ui.notify(`Failed to answer worker UI request: ${error instanceof Error ? error.message : String(error)}`, "error");
1102
+ }
1103
+ }
1104
+ var LOCALLY_OWNED_COMMANDS = new Set(["detach", "quit", "q", "stop", "worker", "settings", "model", "thinking", "compact", "session", "name", "reload"]);
1105
+ function registerDaemonCommands(pi, options, ctx, commands, registered) {
1106
+ for (const command of commands) {
1107
+ const record = recordOf2(command);
1108
+ const name = typeof record?.name === "string" ? record.name : "";
1109
+ const source = typeof record?.source === "string" ? record.source : "worker";
1110
+ if (!name || source === "builtin" || registered.has(name) || LOCALLY_OWNED_COMMANDS.has(name))
1111
+ continue;
1112
+ registered.add(name);
1113
+ const description = typeof record?.description === "string" ? record.description : undefined;
1114
+ try {
1115
+ pi.registerCommand(name, {
1116
+ description: `[worker ${source}] ${description ?? ""}`.trim(),
1117
+ handler: async (args) => {
1118
+ try {
1119
+ const message = await options.controller.sendCommand(`/${name}${args ? ` ${args}` : ""}`);
1120
+ ctx.ui.notify(message, "info");
1121
+ } catch (error) {
1122
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
1123
+ }
1124
+ }
1125
+ });
1126
+ } catch {}
1127
+ }
1128
+ }
1129
+ function createRigWorkerPiBridgeExtension(options) {
1130
+ return (pi) => {
1131
+ const registeredDaemonCommands = new Set;
1132
+ let capabilityLines = null;
1133
+ let statusText = "connecting to worker session";
1134
+ let busy = true;
1135
+ let connected = false;
1136
+ let frame = 0;
1137
+ let spinnerTimer = null;
1138
+ const renderStatus = (ctx) => {
1139
+ const prefix = busy ? `${SPINNER_FRAMES[frame]} ` : connected ? "\u25CF " : "\u25CB ";
1140
+ ctx.ui.setStatus("rig-worker-pi", `${prefix}${statusText}`);
1141
+ };
1142
+ const setStatus = (ctx, text, isBusy) => {
1143
+ statusText = text;
1144
+ busy = isBusy;
1145
+ renderStatus(ctx);
1146
+ };
1147
+ pi.registerCommand("detach", {
1148
+ description: "Detach from this run; the worker keeps going",
1149
+ handler: async (_args, ctx) => {
1150
+ ctx.ui.notify("Detached locally; the worker Pi daemon continues.", "info");
1151
+ ctx.shutdown();
1152
+ }
1153
+ });
1154
+ pi.registerCommand("stop", {
1155
+ description: "Stop the worker Pi run and detach",
1156
+ handler: async (_args, ctx) => {
1157
+ await options.controller.abort();
1158
+ ctx.ui.notify("Stop requested for the worker Pi daemon.", "info");
1159
+ ctx.shutdown();
1160
+ }
1161
+ });
1162
+ pi.registerCommand("worker", {
1163
+ description: "Show the worker session's real capabilities (tools, extensions, hooks, skills)",
1164
+ handler: async (_args, ctx) => {
1165
+ if (capabilityLines)
1166
+ ctx.ui.setWidget("rig-worker-capabilities", capabilityLines, { placement: "aboveEditor" });
1167
+ else
1168
+ ctx.ui.notify("Worker capabilities are not available yet (still connecting).", "info");
1169
+ }
1170
+ });
1171
+ pi.on("user_bash", (event) => ({
1172
+ operations: createRemoteBashOperations(options.controller, event.excludeFromContext === true)
1173
+ }));
1174
+ pi.on("session_start", async (_event, ctx) => {
1175
+ ctx.ui.setTitle("Rig \xB7 enriched bundled Pi");
1176
+ setStatus(ctx, "waiting for worker Pi daemon", true);
1177
+ spinnerTimer = setInterval(() => {
1178
+ frame = (frame + 1) % SPINNER_FRAMES.length;
1179
+ if (busy)
1180
+ renderStatus(ctx);
1181
+ }, 150);
1182
+ ctx.ui.notify(`Enriched bundled Pi \u2014 native UI + Rig layers, worker brain. Attached to run ${options.runId}. /worker shows live capabilities \xB7 /detach exits \xB7 /stop cancels.`, "info");
1183
+ if (options.initialMessageSent)
1184
+ ctx.ui.notify("Initial message sent to the worker Pi daemon.", "info");
1185
+ const nativeUi = ctx.ui;
1186
+ options.controller.setUiHooks({
1187
+ onStatusText: (text) => setStatus(ctx, text, !connected),
1188
+ onActivity: (label, detail) => {
1189
+ const active = label !== "idle" && !/complete|ready/i.test(label);
1190
+ setStatus(ctx, [label, detail].filter(Boolean).join(" \u2014 "), active);
1191
+ if (/agent running|tool:/.test(label))
1192
+ ctx.ui.setWidget("rig-worker-capabilities", undefined);
1193
+ },
1194
+ onConnectionChange: (nextConnected) => {
1195
+ connected = nextConnected;
1196
+ setStatus(ctx, nextConnected ? "worker session live" : "worker session disconnected", false);
1197
+ },
1198
+ onError: (message) => ctx.ui.notify(message, "error"),
1199
+ onExtensionUiRequest: (request) => {
1200
+ answerExtensionUiRequest(options, ctx, request);
1201
+ },
1202
+ onCatchUp: (messages, commands, capabilities) => {
1203
+ if (nativeUi.appendSessionMessages)
1204
+ nativeUi.appendSessionMessages(messages);
1205
+ const cwd = options.controller.status.cwd;
1206
+ if (nativeUi.setDisplayCwd && cwd)
1207
+ nativeUi.setDisplayCwd(cwd);
1208
+ registerDaemonCommands(pi, options, ctx, commands, registeredDaemonCommands);
1209
+ if (capabilities) {
1210
+ capabilityLines = renderWorkerCapabilities(capabilities);
1211
+ ctx.ui.setWidget("rig-worker-capabilities", capabilityLines, { placement: "aboveEditor" });
1212
+ }
1213
+ }
1214
+ });
1215
+ options.controller.connect().catch((error) => {
1216
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
1217
+ });
1218
+ });
1219
+ pi.on("session_shutdown", () => {
1220
+ if (spinnerTimer)
1221
+ clearInterval(spinnerTimer);
1222
+ options.controller.close();
1223
+ });
1224
+ };
1225
+ }
1226
+
1227
+ // packages/cli/src/commands/_pi-frontend.ts
1228
+ function setTemporaryEnv(updates) {
1229
+ const previous = new Map;
1230
+ for (const [key, value] of Object.entries(updates)) {
1231
+ previous.set(key, process.env[key]);
1232
+ process.env[key] = value;
1233
+ }
1234
+ return () => {
1235
+ for (const [key, value] of previous) {
1236
+ if (value === undefined)
1237
+ delete process.env[key];
1238
+ else
1239
+ process.env[key] = value;
1240
+ }
1241
+ };
1242
+ }
1243
+ async function attachRunBundledPiFrontend(context, input) {
1244
+ const tempSessionDir = mkdtempSync(join(tmpdir(), "rig-pi-frontend-sessions-"));
1245
+ const restoreEnv = setTemporaryEnv({
1246
+ PI_CODING_AGENT_SESSION_DIR: tempSessionDir,
1247
+ PI_SKIP_VERSION_CHECK: "1",
1248
+ PI_HIDDEN_COMMANDS: "import,fork,clone,tree,new,resume,trust"
1249
+ });
1250
+ const controller = new RigRemoteSessionController({ context, runId: input.runId });
1251
+ const createRemoteRuntime = async ({ cwd, agentDir, sessionManager, sessionStartEvent }) => {
1252
+ const services = await createAgentSessionServices({
1253
+ cwd,
1254
+ agentDir,
1255
+ resourceLoaderOptions: {
1256
+ extensionFactories: [
1257
+ createRigWorkerPiBridgeExtension({
1258
+ context,
1259
+ controller,
1260
+ runId: input.runId,
1261
+ initialMessageSent: input.steered === true
1262
+ })
1263
+ ],
1264
+ noExtensions: true,
1265
+ noSkills: true,
1266
+ noPromptTemplates: true,
1267
+ noContextFiles: true
1268
+ }
1269
+ });
1270
+ const created = await createAgentSessionFromServices({
1271
+ services,
1272
+ sessionManager,
1273
+ sessionStartEvent,
1274
+ noTools: "all",
1275
+ sessionFactory: (config) => new RigRemoteAgentSession(config, controller)
1276
+ });
1277
+ return { ...created, services, diagnostics: services.diagnostics };
1278
+ };
1279
+ let detached = false;
1280
+ try {
1281
+ await runPiMain([], {
1282
+ createRuntimeOverride: () => createRemoteRuntime
1283
+ });
1284
+ detached = true;
1285
+ } finally {
1286
+ restoreEnv();
1287
+ controller.close();
1288
+ rmSync(tempSessionDir, { recursive: true, force: true });
1289
+ }
1290
+ let run = { runId: input.runId, status: "unknown" };
1291
+ try {
1292
+ run = await getRunDetailsViaServer(context, input.runId);
1293
+ } catch {}
1294
+ return {
1295
+ run,
1296
+ logs: [],
1297
+ timeline: [],
1298
+ timelineCursor: null,
1299
+ steered: input.steered === true,
1300
+ detached,
1301
+ rendered: "enriched bundled Pi frontend with remote worker session runtime"
1302
+ };
1303
+ }
1304
+
1305
+ // packages/cli/src/commands/_operator-view.ts
1306
+ var TERMINAL_RUN_STATUSES2 = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
266
1307
  function runStatusFromPayload(payload) {
267
1308
  const run = payload.run && typeof payload.run === "object" && !Array.isArray(payload.run) ? payload.run : payload;
268
1309
  return String(run.status ?? "unknown").toLowerCase();
@@ -284,57 +1325,76 @@ async function applyOperatorCommand(context, input, deps = {}) {
284
1325
  await (deps.steer ?? steerRunViaServer)(context, input.runId, userMessage);
285
1326
  return { action: "continue", message: "Steering message queued." };
286
1327
  }
287
- async function readOperatorSnapshot(context, runId) {
1328
+ async function readOperatorSnapshot(context, runId, options = {}) {
288
1329
  const run = await getRunDetailsViaServer(context, runId);
289
1330
  const logsPage = await getRunLogsViaServer(context, runId, { limit: 100 });
290
- const entries = Array.isArray(logsPage.entries) ? logsPage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
291
- return { run, logs: entries, rendered: renderOperatorSnapshot({ run, logs: entries }) };
1331
+ const timelinePage = await getRunTimelineViaServer(context, runId, { limit: 200, ...options.timelineCursor ? { cursor: options.timelineCursor } : {} }).catch((error) => ({
1332
+ entries: [{
1333
+ id: `timeline-unavailable:${runId}`,
1334
+ type: "timeline_warning",
1335
+ detail: `Selected Rig server did not provide run timeline events: ${error instanceof Error ? error.message : String(error)}`,
1336
+ createdAt: new Date().toISOString()
1337
+ }],
1338
+ nextCursor: options.timelineCursor ?? null
1339
+ }));
1340
+ const logs = Array.isArray(logsPage.entries) ? logsPage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))).toReversed() : [];
1341
+ const timeline = Array.isArray(timelinePage.entries) ? timelinePage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
1342
+ const timelineCursor = typeof timelinePage.nextCursor === "string" ? timelinePage.nextCursor : options.timelineCursor ?? null;
1343
+ return { run, logs, timeline, timelineCursor, rendered: renderOperatorSnapshot({ run, logs, timeline }) };
292
1344
  }
293
1345
  async function attachRunOperatorView(context, input) {
294
1346
  let steered = false;
295
1347
  if (input.message?.trim()) {
296
- await steerRunViaServer(context, input.runId, input.message.trim());
1348
+ await sendRunPiPromptViaServer(context, input.runId, input.message.trim(), "steer").catch(() => steerRunViaServer(context, input.runId, input.message.trim()));
297
1349
  steered = true;
298
1350
  }
1351
+ if (input.follow && !input.once && input.interactive !== false && context.outputMode === "text" && Boolean(process.stdin.isTTY && process.stdout.isTTY)) {
1352
+ return attachRunBundledPiFrontend(context, {
1353
+ runId: input.runId,
1354
+ steered
1355
+ });
1356
+ }
1357
+ const surface = createOperatorSurface({ interactive: input.interactive !== false });
299
1358
  let snapshot = await readOperatorSnapshot(context, input.runId);
300
1359
  if (context.outputMode === "text") {
301
- console.log(snapshot.rendered);
1360
+ surface.renderSnapshot(snapshot);
1361
+ surface.renderTimeline(snapshot.timeline);
1362
+ surface.renderLogs(snapshot.logs);
302
1363
  if (steered)
303
- console.log("Steering message queued.");
1364
+ surface.info("Message submitted to worker Pi.");
304
1365
  }
305
1366
  let detached = false;
306
- let rl = null;
1367
+ let commandInput = null;
307
1368
  if (input.follow && !input.once && context.outputMode === "text") {
308
1369
  if (input.interactive !== false && process.stdin.isTTY) {
309
- console.log("Controls: /user <message>, /stop, /detach");
310
- rl = createInterface({ input: process.stdin, output: process.stdout, terminal: false });
311
- rl.on("line", (line) => {
312
- applyOperatorCommand(context, { runId: input.runId, line }).then((result) => {
313
- if (result.message)
314
- console.log(result.message);
315
- if (result.action === "detach" || result.action === "stopped") {
316
- detached = true;
317
- rl?.close();
318
- }
319
- }).catch((error) => console.log(`Operator command failed: ${error instanceof Error ? error.message : String(error)}`));
1370
+ surface.info("Controls: /user <message>, /stop, /detach");
1371
+ commandInput = surface.attachCommandInput(async (line) => {
1372
+ const result = await applyOperatorCommand(context, { runId: input.runId, line });
1373
+ if (result.message)
1374
+ surface.info(result.message);
1375
+ if (result.action === "detach" || result.action === "stopped") {
1376
+ detached = true;
1377
+ commandInput?.close();
1378
+ }
320
1379
  });
321
1380
  }
322
- let lastRendered = snapshot.rendered;
323
1381
  const pollMs = Math.max(250, Math.trunc(input.pollMs ?? 2000));
324
- while (!detached && !TERMINAL_RUN_STATUSES.has(runStatusFromPayload(snapshot.run))) {
1382
+ let timelineCursor = snapshot.timelineCursor;
1383
+ while (!detached && !TERMINAL_RUN_STATUSES2.has(runStatusFromPayload(snapshot.run))) {
325
1384
  await Bun.sleep(pollMs);
326
- snapshot = await readOperatorSnapshot(context, input.runId);
327
- if (snapshot.rendered !== lastRendered) {
328
- console.log(snapshot.rendered);
329
- lastRendered = snapshot.rendered;
330
- }
1385
+ snapshot = await readOperatorSnapshot(context, input.runId, { timelineCursor });
1386
+ timelineCursor = snapshot.timelineCursor;
1387
+ surface.renderSnapshot(snapshot);
1388
+ surface.renderTimeline(snapshot.timeline);
1389
+ surface.renderLogs(snapshot.logs);
331
1390
  }
332
- rl?.close();
1391
+ commandInput?.close();
333
1392
  }
334
1393
  return { ...snapshot, steered, detached };
335
1394
  }
336
1395
  export {
337
1396
  renderOperatorSnapshot,
1397
+ createPiRunStreamRenderer,
338
1398
  attachRunOperatorView,
339
1399
  applyOperatorCommand
340
1400
  };