@h-rig/cli 0.0.6-alpha.2 → 0.0.6-alpha.21

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.
@@ -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
 
@@ -102,7 +98,7 @@ function resolveSelectedConnection(projectRoot, options = {}) {
102
98
  }
103
99
 
104
100
  // packages/cli/src/commands/_server-client.ts
105
- var cachedGitHubBearerToken;
101
+ var scopedGitHubBearerTokens = new Map;
106
102
  function cleanToken(value) {
107
103
  const trimmed = value?.trim();
108
104
  return trimmed ? trimmed : null;
@@ -119,25 +115,13 @@ function readPrivateRemoteSessionToken(projectRoot) {
119
115
  }
120
116
  }
121
117
  function readGitHubBearerTokenForRemote(projectRoot) {
122
- if (cachedGitHubBearerToken !== undefined)
123
- return cachedGitHubBearerToken;
118
+ const scopedKey = resolve2(projectRoot);
119
+ if (scopedGitHubBearerTokens.has(scopedKey))
120
+ return scopedGitHubBearerTokens.get(scopedKey) ?? null;
124
121
  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;
122
+ if (privateSession)
123
+ return privateSession;
124
+ return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
141
125
  }
142
126
  async function ensureServerForCli(projectRoot) {
143
127
  try {
@@ -218,6 +202,15 @@ async function getRunLogsViaServer(context, runId, options = {}) {
218
202
  const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
219
203
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { entries: [] };
220
204
  }
205
+ async function getRunTimelineViaServer(context, runId, options = {}) {
206
+ const url = new URL(`http://rig.local/api/runs/${encodeURIComponent(runId)}/timeline`);
207
+ if (options.limit !== undefined)
208
+ url.searchParams.set("limit", String(options.limit));
209
+ if (options.cursor)
210
+ url.searchParams.set("cursor", options.cursor);
211
+ const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
212
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { entries: [] };
213
+ }
221
214
  async function stopRunViaServer(context, runId) {
222
215
  const payload = await requestServerJson(context, "/api/runs/stop", {
223
216
  method: "POST",
@@ -235,8 +228,8 @@ async function steerRunViaServer(context, runId, message) {
235
228
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
236
229
  }
237
230
 
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"]);
231
+ // packages/cli/src/commands/_operator-surface.ts
232
+ import { createInterface } from "readline";
240
233
  var CANONICAL_STAGES = [
241
234
  "Connect",
242
235
  "GitHub/task sync",
@@ -251,18 +244,168 @@ var CANONICAL_STAGES = [
251
244
  "Merge",
252
245
  "Complete"
253
246
  ];
247
+ function logDetail(log) {
248
+ return typeof log.detail === "string" ? log.detail.trim() : "";
249
+ }
250
+ function parseProviderProtocolLog(title, detail) {
251
+ if (title.trim().toLowerCase() !== "agent output")
252
+ return null;
253
+ if (!detail.startsWith("{") || !detail.endsWith("}"))
254
+ return null;
255
+ try {
256
+ const record = JSON.parse(detail);
257
+ if (!record || typeof record !== "object" || Array.isArray(record))
258
+ return null;
259
+ const type = record.type;
260
+ return typeof type === "string" && [
261
+ "assistant",
262
+ "message_start",
263
+ "message_update",
264
+ "message_end",
265
+ "stream_event",
266
+ "tool_result",
267
+ "tool_execution_start",
268
+ "tool_execution_update",
269
+ "tool_execution_end",
270
+ "turn_start",
271
+ "turn_end"
272
+ ].includes(type) ? record : null;
273
+ } catch {
274
+ return null;
275
+ }
276
+ }
277
+ function renderProviderProtocolLog(record) {
278
+ const type = typeof record.type === "string" ? record.type : "";
279
+ if (type === "tool_execution_start" || type === "tool_execution_update" || type === "tool_execution_end") {
280
+ const toolName = String(record.toolName ?? record.name ?? "tool");
281
+ 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";
282
+ return `[Pi tool] ${toolName} ${status}`;
283
+ }
284
+ return null;
285
+ }
286
+ function entryId(entry, fallback) {
287
+ return typeof entry.id === "string" && entry.id.trim() ? entry.id : fallback;
288
+ }
254
289
  function renderOperatorSnapshot(snapshot) {
255
290
  const run = snapshot.run.run && typeof snapshot.run.run === "object" ? snapshot.run.run : snapshot.run;
256
291
  const runId = String(run.runId ?? run.id ?? "run");
257
292
  const status = String(run.status ?? "unknown");
258
293
  const logs = snapshot.logs ?? [];
294
+ const latestByStage = new Map;
295
+ for (const log of logs) {
296
+ const title = String(log.title ?? "").toLowerCase();
297
+ const stageName = String(log.stage ?? "").toLowerCase();
298
+ const stage = CANONICAL_STAGES.find((candidate) => candidate.toLowerCase() === title || candidate.toLowerCase() === stageName);
299
+ if (stage)
300
+ latestByStage.set(stage, log);
301
+ }
259
302
  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)}`] : [];
303
+ const match = latestByStage.get(stage);
304
+ return match ? [`${stage}: ${String(match.status ?? status)}${logDetail(match) ? ` \u2014 ${logDetail(match)}` : ""}`] : [];
262
305
  });
263
306
  return [`Rig run ${runId}: ${status}`, ...stageLines].join(`
264
307
  `);
265
308
  }
309
+ function createPiRunStreamRenderer(output = process.stdout) {
310
+ let lastSnapshot = "";
311
+ const assistantTextById = new Map;
312
+ const seenTimeline = new Set;
313
+ const seenLogs = new Set;
314
+ const writeLine = (line) => output.write(`${line}
315
+ `);
316
+ return {
317
+ renderSnapshot(snapshot) {
318
+ const rendered = renderOperatorSnapshot(snapshot);
319
+ if (rendered && rendered !== lastSnapshot) {
320
+ writeLine(rendered);
321
+ lastSnapshot = rendered;
322
+ }
323
+ },
324
+ renderTimeline(entries) {
325
+ for (const [index, entry] of entries.entries()) {
326
+ const id = entryId(entry, `timeline:${index}:${String(entry.cursor ?? "")}`);
327
+ if (entry.type === "assistant_message" && typeof entry.text === "string") {
328
+ const text = entry.text;
329
+ const previousText = assistantTextById.get(id) ?? "";
330
+ if (!previousText && text.trim()) {
331
+ writeLine("[Pi assistant]");
332
+ }
333
+ if (text.startsWith(previousText)) {
334
+ const delta = text.slice(previousText.length);
335
+ if (delta)
336
+ output.write(delta);
337
+ } else if (text.trim() && text !== previousText) {
338
+ if (previousText)
339
+ writeLine(`
340
+ [Pi assistant]`);
341
+ output.write(text);
342
+ }
343
+ assistantTextById.set(id, text);
344
+ continue;
345
+ }
346
+ if (seenTimeline.has(id))
347
+ continue;
348
+ seenTimeline.add(id);
349
+ if (entry.type === "tool_execution_start" || entry.type === "tool_execution_update" || entry.type === "tool_execution_end" || entry.type === "mcp_tool_call") {
350
+ writeLine(`[Pi tool] ${String(entry.toolName ?? entry.name ?? entry.title ?? entry.type)} ${String(entry.status ?? entry.state ?? "")}`.trim());
351
+ continue;
352
+ }
353
+ if (entry.type === "timeline_warning") {
354
+ writeLine(`[Rig timeline] ${String(entry.detail ?? entry.message ?? "timeline unavailable")}`);
355
+ }
356
+ }
357
+ },
358
+ renderLogs(entries) {
359
+ for (const [index, entry] of entries.entries()) {
360
+ const id = entryId(entry, `log:${index}:${String(entry.createdAt ?? "")}:${String(entry.title ?? "")}`);
361
+ if (seenLogs.has(id))
362
+ continue;
363
+ seenLogs.add(id);
364
+ const title = String(entry.title ?? "");
365
+ if (CANONICAL_STAGES.some((stage) => stage.toLowerCase() === title.toLowerCase()))
366
+ continue;
367
+ const detail = logDetail(entry);
368
+ if (!detail)
369
+ continue;
370
+ const protocolRecord = parseProviderProtocolLog(title, detail);
371
+ if (protocolRecord) {
372
+ const protocolLine = renderProviderProtocolLog(protocolRecord);
373
+ if (protocolLine)
374
+ writeLine(protocolLine);
375
+ continue;
376
+ }
377
+ writeLine(`[${title || "Rig log"}] ${detail}`);
378
+ }
379
+ }
380
+ };
381
+ }
382
+ function createOperatorSurface(options = {}) {
383
+ const input = options.input ?? process.stdin;
384
+ const output = options.output ?? process.stdout;
385
+ const errorOutput = options.errorOutput ?? process.stderr;
386
+ const renderer = createPiRunStreamRenderer(output);
387
+ const writeLine = (line) => output.write(`${line}
388
+ `);
389
+ return {
390
+ mode: "pi-compatible-text",
391
+ ...renderer,
392
+ info: writeLine,
393
+ error: (message) => errorOutput.write(`${message}
394
+ `),
395
+ attachCommandInput(handler) {
396
+ if (options.interactive === false || !input.isTTY)
397
+ return null;
398
+ const rl = createInterface({ input, output: process.stdout, terminal: false });
399
+ rl.on("line", (line) => {
400
+ Promise.resolve(handler(line)).catch((error) => writeLine(`Operator command failed: ${error instanceof Error ? error.message : String(error)}`));
401
+ });
402
+ return { close: () => rl.close() };
403
+ }
404
+ };
405
+ }
406
+
407
+ // packages/cli/src/commands/_operator-view.ts
408
+ var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
266
409
  function runStatusFromPayload(payload) {
267
410
  const run = payload.run && typeof payload.run === "object" && !Array.isArray(payload.run) ? payload.run : payload;
268
411
  return String(run.status ?? "unknown").toLowerCase();
@@ -284,11 +427,22 @@ async function applyOperatorCommand(context, input, deps = {}) {
284
427
  await (deps.steer ?? steerRunViaServer)(context, input.runId, userMessage);
285
428
  return { action: "continue", message: "Steering message queued." };
286
429
  }
287
- async function readOperatorSnapshot(context, runId) {
430
+ async function readOperatorSnapshot(context, runId, options = {}) {
288
431
  const run = await getRunDetailsViaServer(context, runId);
289
432
  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 }) };
433
+ const timelinePage = await getRunTimelineViaServer(context, runId, { limit: 200, ...options.timelineCursor ? { cursor: options.timelineCursor } : {} }).catch((error) => ({
434
+ entries: [{
435
+ id: `timeline-unavailable:${runId}`,
436
+ type: "timeline_warning",
437
+ detail: `Selected Rig server did not provide run timeline events: ${error instanceof Error ? error.message : String(error)}`,
438
+ createdAt: new Date().toISOString()
439
+ }],
440
+ nextCursor: options.timelineCursor ?? null
441
+ }));
442
+ const logs = Array.isArray(logsPage.entries) ? logsPage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))).toReversed() : [];
443
+ const timeline = Array.isArray(timelinePage.entries) ? timelinePage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
444
+ const timelineCursor = typeof timelinePage.nextCursor === "string" ? timelinePage.nextCursor : options.timelineCursor ?? null;
445
+ return { run, logs, timeline, timelineCursor, rendered: renderOperatorSnapshot({ run, logs, timeline }) };
292
446
  }
293
447
  async function attachRunOperatorView(context, input) {
294
448
  let steered = false;
@@ -296,45 +450,47 @@ async function attachRunOperatorView(context, input) {
296
450
  await steerRunViaServer(context, input.runId, input.message.trim());
297
451
  steered = true;
298
452
  }
453
+ const surface = createOperatorSurface({ interactive: input.interactive !== false });
299
454
  let snapshot = await readOperatorSnapshot(context, input.runId);
300
455
  if (context.outputMode === "text") {
301
- console.log(snapshot.rendered);
456
+ surface.renderSnapshot(snapshot);
457
+ surface.renderTimeline(snapshot.timeline);
458
+ surface.renderLogs(snapshot.logs);
302
459
  if (steered)
303
- console.log("Steering message queued.");
460
+ surface.info("Steering message queued.");
304
461
  }
305
462
  let detached = false;
306
- let rl = null;
463
+ let commandInput = null;
307
464
  if (input.follow && !input.once && context.outputMode === "text") {
308
465
  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)}`));
466
+ surface.info("Controls: /user <message>, /stop, /detach");
467
+ commandInput = surface.attachCommandInput(async (line) => {
468
+ const result = await applyOperatorCommand(context, { runId: input.runId, line });
469
+ if (result.message)
470
+ surface.info(result.message);
471
+ if (result.action === "detach" || result.action === "stopped") {
472
+ detached = true;
473
+ commandInput?.close();
474
+ }
320
475
  });
321
476
  }
322
- let lastRendered = snapshot.rendered;
323
477
  const pollMs = Math.max(250, Math.trunc(input.pollMs ?? 2000));
478
+ let timelineCursor = snapshot.timelineCursor;
324
479
  while (!detached && !TERMINAL_RUN_STATUSES.has(runStatusFromPayload(snapshot.run))) {
325
480
  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
- }
481
+ snapshot = await readOperatorSnapshot(context, input.runId, { timelineCursor });
482
+ timelineCursor = snapshot.timelineCursor;
483
+ surface.renderSnapshot(snapshot);
484
+ surface.renderTimeline(snapshot.timeline);
485
+ surface.renderLogs(snapshot.logs);
331
486
  }
332
- rl?.close();
487
+ commandInput?.close();
333
488
  }
334
489
  return { ...snapshot, steered, detached };
335
490
  }
336
491
  export {
337
492
  renderOperatorSnapshot,
493
+ createPiRunStreamRenderer,
338
494
  attachRunOperatorView,
339
495
  applyOperatorCommand
340
496
  };
@@ -3,7 +3,8 @@
3
3
  import { existsSync, readFileSync, rmSync } from "fs";
4
4
  import { homedir } from "os";
5
5
  import { resolve } from "path";
6
- var PI_RIG_PACKAGE_NAME = "@rig/pi-rig";
6
+ var PI_RIG_PACKAGE_NAME = "@h-rig/pi-rig";
7
+ var LEGACY_PI_RIG_PACKAGE_NAME = "@rig/pi-rig";
7
8
  var LEGACY_PI_RIG_MARKER = `// Managed by Rig. Source package: @rig/pi-rig.
8
9
  export { default } from '@rig/pi-rig';
9
10
  `;
@@ -31,7 +32,7 @@ function resolvePiHomeDir(inputHomeDir) {
31
32
  function piListContainsPiRig(output) {
32
33
  return output.split(/\r?\n/).some((line) => {
33
34
  const normalized = line.trim();
34
- return normalized.includes(PI_RIG_PACKAGE_NAME) || /(?:^|[\\/])packages[\\/]pi-rig(?:$|\s)/.test(normalized);
35
+ return normalized.includes(PI_RIG_PACKAGE_NAME) || normalized.includes(LEGACY_PI_RIG_PACKAGE_NAME) || /(?:^|[\\/])packages[\\/]pi-rig(?:$|\s)/.test(normalized);
35
36
  });
36
37
  }
37
38
  async function safeRun(runner, command, options) {
@@ -147,7 +148,7 @@ async function ensureRemotePiRigInstalled(input) {
147
148
  const payload = await input.requestJson("/api/pi-rig/install", {
148
149
  method: "POST",
149
150
  headers: { "content-type": "application/json" },
150
- body: JSON.stringify({ package: "@rig/pi-rig", scope: "global" })
151
+ body: JSON.stringify({ package: PI_RIG_PACKAGE_NAME, scope: "global" })
151
152
  });
152
153
  const record = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
153
154
  const piOk = record.piOk === true || record.ok === true;
@@ -0,0 +1,253 @@
1
+ // @bun
2
+ // packages/cli/src/commands/_pi-session.ts
3
+ import { spawn } from "child_process";
4
+
5
+ // packages/cli/src/runner.ts
6
+ import { EventBus } from "@rig/runtime/control-plane/runtime/events";
7
+ import { CliError } from "@rig/runtime/control-plane/errors";
8
+ import { evaluate, loadPolicy, resolveAction } from "@rig/runtime/control-plane/runtime/guard";
9
+ import { PluginManager } from "@rig/runtime/control-plane/runtime/plugins";
10
+ import { loadRuntimeContextFromEnv } from "@rig/runtime/control-plane/runtime/context";
11
+ import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
12
+ import { CliError as CliError2 } from "@rig/runtime/control-plane/errors";
13
+ function formatCommand(parts) {
14
+ return parts.map((part) => /[^a-zA-Z0-9_./:-]/.test(part) ? JSON.stringify(part) : part).join(" ");
15
+ }
16
+
17
+ // packages/cli/src/commands/_server-client.ts
18
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
19
+ import { resolve as resolve2 } from "path";
20
+ import { ensureLocalRigServerConnection } from "@rig/runtime/local-server";
21
+
22
+ // packages/cli/src/commands/_connection-state.ts
23
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
24
+ import { homedir } from "os";
25
+ import { dirname, resolve } from "path";
26
+ function resolveGlobalConnectionsPath(env = process.env) {
27
+ const explicit = env.RIG_CONNECTIONS_FILE?.trim();
28
+ if (explicit)
29
+ return resolve(explicit);
30
+ const stateDir = env.RIG_GLOBAL_STATE_DIR?.trim();
31
+ if (stateDir)
32
+ return resolve(stateDir, "connections.json");
33
+ return resolve(homedir(), ".rig", "connections.json");
34
+ }
35
+ function resolveRepoConnectionPath(projectRoot) {
36
+ return resolve(projectRoot, ".rig", "state", "connection.json");
37
+ }
38
+ function readJsonFile(path) {
39
+ if (!existsSync(path))
40
+ return null;
41
+ try {
42
+ return JSON.parse(readFileSync(path, "utf8"));
43
+ } catch (error) {
44
+ throw new CliError2(`Invalid Rig connection state at ${path}: ${error instanceof Error ? error.message : String(error)}`, 1);
45
+ }
46
+ }
47
+ function normalizeConnection(value) {
48
+ if (!value || typeof value !== "object" || Array.isArray(value))
49
+ return null;
50
+ const record = value;
51
+ if (record.kind === "local")
52
+ return { kind: "local", mode: "auto" };
53
+ if (record.kind === "remote" && typeof record.baseUrl === "string" && record.baseUrl.trim()) {
54
+ const baseUrl = record.baseUrl.trim().replace(/\/+$/, "");
55
+ return { kind: "remote", baseUrl };
56
+ }
57
+ return null;
58
+ }
59
+ function readGlobalConnections(options = {}) {
60
+ const path = resolveGlobalConnectionsPath(options.env ?? process.env);
61
+ const payload = readJsonFile(path);
62
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
63
+ return { connections: {} };
64
+ }
65
+ const rawConnections = payload.connections;
66
+ const connections = {};
67
+ if (rawConnections && typeof rawConnections === "object" && !Array.isArray(rawConnections)) {
68
+ for (const [alias, raw] of Object.entries(rawConnections)) {
69
+ const connection = normalizeConnection(raw);
70
+ if (connection)
71
+ connections[alias] = connection;
72
+ }
73
+ }
74
+ return { connections };
75
+ }
76
+ function readRepoConnection(projectRoot) {
77
+ const payload = readJsonFile(resolveRepoConnectionPath(projectRoot));
78
+ if (!payload || typeof payload !== "object" || Array.isArray(payload))
79
+ return null;
80
+ const record = payload;
81
+ const selected = typeof record.selected === "string" ? record.selected.trim() : "";
82
+ if (!selected)
83
+ return null;
84
+ return {
85
+ selected,
86
+ project: typeof record.project === "string" ? record.project : undefined,
87
+ linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined
88
+ };
89
+ }
90
+ function resolveSelectedConnection(projectRoot, options = {}) {
91
+ const repo = readRepoConnection(projectRoot);
92
+ if (!repo)
93
+ return null;
94
+ if (repo.selected === "local")
95
+ return { alias: "local", connection: { kind: "local", mode: "auto" } };
96
+ const global = readGlobalConnections(options);
97
+ const connection = global.connections[repo.selected];
98
+ if (!connection) {
99
+ throw new CliError2(`Selected Rig connection "${repo.selected}" was not found. Run \`rig connect list\` or \`rig connect use local\`.`, 1);
100
+ }
101
+ return { alias: repo.selected, connection };
102
+ }
103
+
104
+ // packages/cli/src/commands/_server-client.ts
105
+ var scopedGitHubBearerTokens = new Map;
106
+ function cleanToken(value) {
107
+ const trimmed = value?.trim();
108
+ return trimmed ? trimmed : null;
109
+ }
110
+ function readPrivateRemoteSessionToken(projectRoot) {
111
+ const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
112
+ if (!existsSync2(path))
113
+ return null;
114
+ try {
115
+ const parsed = JSON.parse(readFileSync2(path, "utf8"));
116
+ return cleanToken(typeof parsed.apiSessionToken === "string" ? parsed.apiSessionToken : typeof parsed.sessionToken === "string" ? parsed.sessionToken : undefined);
117
+ } catch {
118
+ return null;
119
+ }
120
+ }
121
+ function readGitHubBearerTokenForRemote(projectRoot) {
122
+ const scopedKey = resolve2(projectRoot);
123
+ if (scopedGitHubBearerTokens.has(scopedKey))
124
+ return scopedGitHubBearerTokens.get(scopedKey) ?? null;
125
+ const privateSession = readPrivateRemoteSessionToken(projectRoot);
126
+ if (privateSession)
127
+ return privateSession;
128
+ return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
129
+ }
130
+ async function ensureServerForCli(projectRoot) {
131
+ try {
132
+ const selected = resolveSelectedConnection(projectRoot);
133
+ if (selected?.connection.kind === "remote") {
134
+ return {
135
+ baseUrl: selected.connection.baseUrl,
136
+ authToken: readGitHubBearerTokenForRemote(projectRoot),
137
+ connectionKind: "remote"
138
+ };
139
+ }
140
+ const connection = await ensureLocalRigServerConnection(projectRoot);
141
+ return {
142
+ baseUrl: connection.baseUrl,
143
+ authToken: connection.authToken,
144
+ connectionKind: "local"
145
+ };
146
+ } catch (error) {
147
+ if (error instanceof Error) {
148
+ throw new CliError2(error.message, 1);
149
+ }
150
+ throw error;
151
+ }
152
+ }
153
+
154
+ // packages/cli/src/commands/_pi-install.ts
155
+ import { existsSync as existsSync3, readFileSync as readFileSync3, rmSync } from "fs";
156
+ import { resolve as resolve3 } from "path";
157
+ var PI_RIG_PACKAGE_NAME = "@h-rig/pi-rig";
158
+ function resolvePiRigPackageSource(projectRoot, exists = existsSync3) {
159
+ const localPackage = resolve3(projectRoot, "packages", "pi-rig");
160
+ if (exists(resolve3(localPackage, "package.json")))
161
+ return localPackage;
162
+ return `npm:${PI_RIG_PACKAGE_NAME}`;
163
+ }
164
+
165
+ // packages/cli/src/commands/_pi-session.ts
166
+ function buildPiRigSessionEnv(input) {
167
+ return {
168
+ RIG_PROJECT_ROOT: input.projectRoot,
169
+ PROJECT_RIG_ROOT: input.projectRoot,
170
+ RIG_RUN_ID: input.runId,
171
+ RIG_SERVER_RUN_ID: input.runId,
172
+ RIG_RUNTIME_ADAPTER: "pi",
173
+ RIG_SERVER_URL: input.serverUrl,
174
+ RIG_SERVER_BASE_URL: input.serverUrl,
175
+ RIG_STEERING_POLL_MS: process.env.RIG_STEERING_POLL_MS?.trim() || "1000",
176
+ RIG_PI_OPERATOR_SESSION: "1",
177
+ ...input.taskId ? { RIG_TASK_ID: input.taskId } : {},
178
+ ...input.authToken ? { RIG_AUTH_TOKEN: input.authToken } : {}
179
+ };
180
+ }
181
+ function shellBinary(name) {
182
+ const explicit = process.env.RIG_PI_BINARY?.trim();
183
+ if (explicit)
184
+ return explicit;
185
+ return Bun.which(name) || name;
186
+ }
187
+ function buildPiRigSessionCommand(input) {
188
+ const configuredExtension = input.extensionSource ?? process.env.RIG_PI_RIG_EXTENSION_SOURCE?.trim();
189
+ const extensionSource = configuredExtension && configuredExtension.length > 0 ? configuredExtension : resolvePiRigPackageSource(input.projectRoot);
190
+ const initialCommand = `/rig attach ${input.runId}`;
191
+ return [
192
+ shellBinary("pi"),
193
+ "--no-extensions",
194
+ "--extension",
195
+ extensionSource,
196
+ initialCommand
197
+ ];
198
+ }
199
+ async function launchPiRigSession(context, input) {
200
+ if (context.outputMode !== "text" || !process.stdin.isTTY || !process.stdout.isTTY) {
201
+ return { launched: false, exitCode: null, command: [] };
202
+ }
203
+ if (process.env.RIG_DISABLE_PI_LAUNCH === "1") {
204
+ return { launched: false, exitCode: null, command: [] };
205
+ }
206
+ const server = await ensureServerForCli(context.projectRoot);
207
+ const command = buildPiRigSessionCommand({ ...input, projectRoot: context.projectRoot });
208
+ const env = {
209
+ ...process.env,
210
+ ...buildPiRigSessionEnv({
211
+ projectRoot: context.projectRoot,
212
+ runId: input.runId,
213
+ taskId: input.taskId,
214
+ serverUrl: server.baseUrl,
215
+ authToken: server.authToken
216
+ })
217
+ };
218
+ process.stdout.write(`Launching Pi for Rig run ${input.runId}\u2026
219
+ `);
220
+ process.stdout.write(`Pi command: ${formatCommand(command)}
221
+ `);
222
+ const launchedAt = Date.now();
223
+ const child = spawn(command[0], command.slice(1), {
224
+ cwd: context.projectRoot,
225
+ env,
226
+ stdio: "inherit"
227
+ });
228
+ const launchError = await new Promise((resolve4) => {
229
+ child.once("error", (error) => {
230
+ resolve4({ error: error.message });
231
+ });
232
+ child.once("close", (code) => resolve4({ code }));
233
+ });
234
+ if ("error" in launchError) {
235
+ process.stderr.write(`Failed to launch Pi; falling back to Rig attach view: ${launchError.error}
236
+ `);
237
+ return { launched: false, exitCode: null, command, error: launchError.error };
238
+ }
239
+ const exitCode = launchError.code;
240
+ const elapsedMs = Date.now() - launchedAt;
241
+ if (typeof exitCode === "number" && exitCode !== 0 && elapsedMs < 5000) {
242
+ const error = `Pi exited during startup with code ${exitCode}.`;
243
+ process.stderr.write(`${error} Falling back to Rig attach view.
244
+ `);
245
+ return { launched: false, exitCode, command, error };
246
+ }
247
+ return { launched: true, exitCode, command };
248
+ }
249
+ export {
250
+ launchPiRigSession,
251
+ buildPiRigSessionEnv,
252
+ buildPiRigSessionCommand
253
+ };