@h-rig/cli 0.0.6-alpha.13 → 0.0.6-alpha.15

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,7 +1,5 @@
1
1
  // @bun
2
2
  // packages/cli/src/commands/run.ts
3
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
4
- import { resolve as resolve3 } from "path";
5
3
  import { createInterface as createInterface2 } from "readline/promises";
6
4
 
7
5
  // packages/cli/src/runner.ts
@@ -54,9 +52,7 @@ Usage: ${usage}`);
54
52
  // packages/cli/src/commands/run.ts
55
53
  import {
56
54
  listAuthorityRuns,
57
- readAuthorityRun,
58
- readJsonlFile,
59
- resolveAuthorityRunDir
55
+ readAuthorityRun
60
56
  } from "@rig/runtime/control-plane/authority-files";
61
57
  import {
62
58
  cleanupRunState,
@@ -85,7 +81,6 @@ function parsePositiveInt(value, option, fallback) {
85
81
  }
86
82
 
87
83
  // packages/cli/src/commands/_server-client.ts
88
- import { spawnSync } from "child_process";
89
84
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
90
85
  import { resolve as resolve2 } from "path";
91
86
  import { ensureLocalRigServerConnection } from "@rig/runtime/local-server";
@@ -173,7 +168,7 @@ function resolveSelectedConnection(projectRoot, options = {}) {
173
168
  }
174
169
 
175
170
  // packages/cli/src/commands/_server-client.ts
176
- var cachedGitHubBearerToken;
171
+ var scopedGitHubBearerTokens = new Map;
177
172
  function cleanToken(value) {
178
173
  const trimmed = value?.trim();
179
174
  return trimmed ? trimmed : null;
@@ -190,25 +185,13 @@ function readPrivateRemoteSessionToken(projectRoot) {
190
185
  }
191
186
  }
192
187
  function readGitHubBearerTokenForRemote(projectRoot) {
193
- if (cachedGitHubBearerToken !== undefined)
194
- return cachedGitHubBearerToken;
188
+ const scopedKey = resolve2(projectRoot);
189
+ if (scopedGitHubBearerTokens.has(scopedKey))
190
+ return scopedGitHubBearerTokens.get(scopedKey) ?? null;
195
191
  const privateSession = readPrivateRemoteSessionToken(projectRoot);
196
- if (privateSession) {
197
- cachedGitHubBearerToken = privateSession;
198
- return cachedGitHubBearerToken;
199
- }
200
- const envToken = cleanToken(process.env.RIG_GITHUB_TOKEN) ?? cleanToken(process.env.GITHUB_TOKEN) ?? cleanToken(process.env.GH_TOKEN);
201
- if (envToken) {
202
- cachedGitHubBearerToken = envToken;
203
- return cachedGitHubBearerToken;
204
- }
205
- const result = spawnSync("gh", ["auth", "token"], {
206
- encoding: "utf8",
207
- timeout: 5000,
208
- stdio: ["ignore", "pipe", "ignore"]
209
- });
210
- cachedGitHubBearerToken = result.status === 0 ? cleanToken(result.stdout) : null;
211
- return cachedGitHubBearerToken;
192
+ if (privateSession)
193
+ return privateSession;
194
+ return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
212
195
  }
213
196
  async function ensureServerForCli(projectRoot) {
214
197
  try {
@@ -289,6 +272,15 @@ async function getRunLogsViaServer(context, runId, options = {}) {
289
272
  const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
290
273
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { entries: [] };
291
274
  }
275
+ async function getRunTimelineViaServer(context, runId, options = {}) {
276
+ const url = new URL(`http://rig.local/api/runs/${encodeURIComponent(runId)}/timeline`);
277
+ if (options.limit !== undefined)
278
+ url.searchParams.set("limit", String(options.limit));
279
+ if (options.cursor)
280
+ url.searchParams.set("cursor", options.cursor);
281
+ const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
282
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { entries: [] };
283
+ }
292
284
  async function stopRunViaServer(context, runId) {
293
285
  const payload = await requestServerJson(context, "/api/runs/stop", {
294
286
  method: "POST",
@@ -306,9 +298,8 @@ async function steerRunViaServer(context, runId, message) {
306
298
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
307
299
  }
308
300
 
309
- // packages/cli/src/commands/_operator-view.ts
301
+ // packages/cli/src/commands/_operator-surface.ts
310
302
  import { createInterface } from "readline";
311
- var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
312
303
  var CANONICAL_STAGES = [
313
304
  "Connect",
314
305
  "GitHub/task sync",
@@ -323,18 +314,121 @@ var CANONICAL_STAGES = [
323
314
  "Merge",
324
315
  "Complete"
325
316
  ];
317
+ function logDetail(log) {
318
+ return typeof log.detail === "string" ? log.detail.trim() : "";
319
+ }
320
+ function entryId(entry, fallback) {
321
+ return typeof entry.id === "string" && entry.id.trim() ? entry.id : fallback;
322
+ }
326
323
  function renderOperatorSnapshot(snapshot) {
327
324
  const run = snapshot.run.run && typeof snapshot.run.run === "object" ? snapshot.run.run : snapshot.run;
328
325
  const runId = String(run.runId ?? run.id ?? "run");
329
326
  const status = String(run.status ?? "unknown");
330
327
  const logs = snapshot.logs ?? [];
328
+ const latestByStage = new Map;
329
+ for (const log of logs) {
330
+ const title = String(log.title ?? "").toLowerCase();
331
+ const stageName = String(log.stage ?? "").toLowerCase();
332
+ const stage = CANONICAL_STAGES.find((candidate) => candidate.toLowerCase() === title || candidate.toLowerCase() === stageName);
333
+ if (stage)
334
+ latestByStage.set(stage, log);
335
+ }
331
336
  const stageLines = CANONICAL_STAGES.flatMap((stage) => {
332
- const match = logs.find((log) => String(log.title ?? "").toLowerCase() === stage.toLowerCase() || String(log.stage ?? "").toLowerCase() === stage.toLowerCase());
333
- return match ? [`${stage}: ${String(match.status ?? status)}`] : [];
337
+ const match = latestByStage.get(stage);
338
+ return match ? [`${stage}: ${String(match.status ?? status)}${logDetail(match) ? ` \u2014 ${logDetail(match)}` : ""}`] : [];
334
339
  });
335
340
  return [`Rig run ${runId}: ${status}`, ...stageLines].join(`
336
341
  `);
337
342
  }
343
+ function createPiRunStreamRenderer(output = process.stdout) {
344
+ let lastSnapshot = "";
345
+ const assistantTextById = new Map;
346
+ const seenTimeline = new Set;
347
+ const seenLogs = new Set;
348
+ const writeLine = (line) => output.write(`${line}
349
+ `);
350
+ return {
351
+ renderSnapshot(snapshot) {
352
+ const rendered = renderOperatorSnapshot(snapshot);
353
+ if (rendered && rendered !== lastSnapshot) {
354
+ writeLine(rendered);
355
+ lastSnapshot = rendered;
356
+ }
357
+ },
358
+ renderTimeline(entries) {
359
+ for (const [index, entry] of entries.entries()) {
360
+ const id = entryId(entry, `timeline:${index}:${String(entry.cursor ?? "")}`);
361
+ if (entry.type === "assistant_message" && typeof entry.text === "string") {
362
+ const text = entry.text;
363
+ const previousText = assistantTextById.get(id) ?? "";
364
+ if (text.startsWith(previousText)) {
365
+ const delta = text.slice(previousText.length);
366
+ if (delta)
367
+ output.write(delta);
368
+ } else if (text.trim() && text !== previousText) {
369
+ writeLine(`
370
+ [Pi assistant]`);
371
+ output.write(text);
372
+ }
373
+ assistantTextById.set(id, text);
374
+ continue;
375
+ }
376
+ if (seenTimeline.has(id))
377
+ continue;
378
+ seenTimeline.add(id);
379
+ if (entry.type === "tool_execution_start" || entry.type === "tool_execution_update" || entry.type === "tool_execution_end" || entry.type === "mcp_tool_call") {
380
+ writeLine(`[Pi tool] ${String(entry.toolName ?? entry.name ?? entry.title ?? entry.type)} ${String(entry.status ?? entry.state ?? "")}`.trim());
381
+ continue;
382
+ }
383
+ if (entry.type === "timeline_warning") {
384
+ writeLine(`[Rig timeline] ${String(entry.detail ?? entry.message ?? "timeline unavailable")}`);
385
+ }
386
+ }
387
+ },
388
+ renderLogs(entries) {
389
+ for (const [index, entry] of entries.entries()) {
390
+ const id = entryId(entry, `log:${index}:${String(entry.createdAt ?? "")}:${String(entry.title ?? "")}`);
391
+ if (seenLogs.has(id))
392
+ continue;
393
+ seenLogs.add(id);
394
+ const title = String(entry.title ?? "");
395
+ if (CANONICAL_STAGES.some((stage) => stage.toLowerCase() === title.toLowerCase()))
396
+ continue;
397
+ const detail = logDetail(entry);
398
+ if (!detail)
399
+ continue;
400
+ writeLine(`[${title || "Rig log"}] ${detail}`);
401
+ }
402
+ }
403
+ };
404
+ }
405
+ function createOperatorSurface(options = {}) {
406
+ const input = options.input ?? process.stdin;
407
+ const output = options.output ?? process.stdout;
408
+ const errorOutput = options.errorOutput ?? process.stderr;
409
+ const renderer = createPiRunStreamRenderer(output);
410
+ const writeLine = (line) => output.write(`${line}
411
+ `);
412
+ return {
413
+ mode: "pi-compatible-text",
414
+ ...renderer,
415
+ info: writeLine,
416
+ error: (message) => errorOutput.write(`${message}
417
+ `),
418
+ attachCommandInput(handler) {
419
+ if (options.interactive === false || !input.isTTY)
420
+ return null;
421
+ const rl = createInterface({ input, output: process.stdout, terminal: false });
422
+ rl.on("line", (line) => {
423
+ Promise.resolve(handler(line)).catch((error) => writeLine(`Operator command failed: ${error instanceof Error ? error.message : String(error)}`));
424
+ });
425
+ return { close: () => rl.close() };
426
+ }
427
+ };
428
+ }
429
+
430
+ // packages/cli/src/commands/_operator-view.ts
431
+ var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
338
432
  function runStatusFromPayload(payload) {
339
433
  const run = payload.run && typeof payload.run === "object" && !Array.isArray(payload.run) ? payload.run : payload;
340
434
  return String(run.status ?? "unknown").toLowerCase();
@@ -356,11 +450,22 @@ async function applyOperatorCommand(context, input, deps = {}) {
356
450
  await (deps.steer ?? steerRunViaServer)(context, input.runId, userMessage);
357
451
  return { action: "continue", message: "Steering message queued." };
358
452
  }
359
- async function readOperatorSnapshot(context, runId) {
453
+ async function readOperatorSnapshot(context, runId, options = {}) {
360
454
  const run = await getRunDetailsViaServer(context, runId);
361
455
  const logsPage = await getRunLogsViaServer(context, runId, { limit: 100 });
362
- const entries = Array.isArray(logsPage.entries) ? logsPage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
363
- return { run, logs: entries, rendered: renderOperatorSnapshot({ run, logs: entries }) };
456
+ const timelinePage = await getRunTimelineViaServer(context, runId, { limit: 200, ...options.timelineCursor ? { cursor: options.timelineCursor } : {} }).catch((error) => ({
457
+ entries: [{
458
+ id: `timeline-unavailable:${runId}`,
459
+ type: "timeline_warning",
460
+ detail: `Selected Rig server did not provide run timeline events: ${error instanceof Error ? error.message : String(error)}`,
461
+ createdAt: new Date().toISOString()
462
+ }],
463
+ nextCursor: options.timelineCursor ?? null
464
+ }));
465
+ const logs = Array.isArray(logsPage.entries) ? logsPage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))).toReversed() : [];
466
+ const timeline = Array.isArray(timelinePage.entries) ? timelinePage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
467
+ const timelineCursor = typeof timelinePage.nextCursor === "string" ? timelinePage.nextCursor : options.timelineCursor ?? null;
468
+ return { run, logs, timeline, timelineCursor, rendered: renderOperatorSnapshot({ run, logs, timeline }) };
364
469
  }
365
470
  async function attachRunOperatorView(context, input) {
366
471
  let steered = false;
@@ -368,40 +473,41 @@ async function attachRunOperatorView(context, input) {
368
473
  await steerRunViaServer(context, input.runId, input.message.trim());
369
474
  steered = true;
370
475
  }
476
+ const surface = createOperatorSurface({ interactive: input.interactive !== false });
371
477
  let snapshot = await readOperatorSnapshot(context, input.runId);
372
478
  if (context.outputMode === "text") {
373
- console.log(snapshot.rendered);
479
+ surface.renderSnapshot(snapshot);
480
+ surface.renderTimeline(snapshot.timeline);
481
+ surface.renderLogs(snapshot.logs);
374
482
  if (steered)
375
- console.log("Steering message queued.");
483
+ surface.info("Steering message queued.");
376
484
  }
377
485
  let detached = false;
378
- let rl = null;
486
+ let commandInput = null;
379
487
  if (input.follow && !input.once && context.outputMode === "text") {
380
488
  if (input.interactive !== false && process.stdin.isTTY) {
381
- console.log("Controls: /user <message>, /stop, /detach");
382
- rl = createInterface({ input: process.stdin, output: process.stdout, terminal: false });
383
- rl.on("line", (line) => {
384
- applyOperatorCommand(context, { runId: input.runId, line }).then((result) => {
385
- if (result.message)
386
- console.log(result.message);
387
- if (result.action === "detach" || result.action === "stopped") {
388
- detached = true;
389
- rl?.close();
390
- }
391
- }).catch((error) => console.log(`Operator command failed: ${error instanceof Error ? error.message : String(error)}`));
489
+ surface.info("Controls: /user <message>, /stop, /detach");
490
+ commandInput = surface.attachCommandInput(async (line) => {
491
+ const result = await applyOperatorCommand(context, { runId: input.runId, line });
492
+ if (result.message)
493
+ surface.info(result.message);
494
+ if (result.action === "detach" || result.action === "stopped") {
495
+ detached = true;
496
+ commandInput?.close();
497
+ }
392
498
  });
393
499
  }
394
- let lastRendered = snapshot.rendered;
395
500
  const pollMs = Math.max(250, Math.trunc(input.pollMs ?? 2000));
501
+ let timelineCursor = snapshot.timelineCursor;
396
502
  while (!detached && !TERMINAL_RUN_STATUSES.has(runStatusFromPayload(snapshot.run))) {
397
503
  await Bun.sleep(pollMs);
398
- snapshot = await readOperatorSnapshot(context, input.runId);
399
- if (snapshot.rendered !== lastRendered) {
400
- console.log(snapshot.rendered);
401
- lastRendered = snapshot.rendered;
402
- }
504
+ snapshot = await readOperatorSnapshot(context, input.runId, { timelineCursor });
505
+ timelineCursor = snapshot.timelineCursor;
506
+ surface.renderSnapshot(snapshot);
507
+ surface.renderTimeline(snapshot.timeline);
508
+ surface.renderLogs(snapshot.logs);
403
509
  }
404
- rl?.close();
510
+ commandInput?.close();
405
511
  }
406
512
  return { ...snapshot, steered, detached };
407
513
  }
@@ -575,34 +681,24 @@ async function executeRun(context, args) {
575
681
  if (!run.value) {
576
682
  throw new CliError2("run timeline requires --run <id>.");
577
683
  }
578
- const timelinePath = resolve3(resolveAuthorityRunDir(context.projectRoot, run.value), "timeline.jsonl");
579
- const printEvents = () => {
580
- const events2 = readJsonlFile(timelinePath);
581
- if (context.outputMode === "text") {
582
- for (const event of events2) {
583
- console.log(JSON.stringify(event));
584
- }
585
- }
586
- return events2;
587
- };
588
- const events = printEvents();
684
+ const renderer = createPiRunStreamRenderer();
685
+ let cursor = null;
686
+ const page = await getRunTimelineViaServer(context, run.value, { limit: 500 });
687
+ const events = Array.isArray(page.entries) ? page.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
688
+ cursor = typeof page.nextCursor === "string" ? page.nextCursor : null;
689
+ if (context.outputMode === "text") {
690
+ renderer.renderTimeline(events);
691
+ }
589
692
  if (follow.value && context.outputMode === "text") {
590
- let lastLength = existsSync3(timelinePath) ? readFileSync3(timelinePath, "utf8").length : 0;
591
693
  while (true) {
592
694
  await Bun.sleep(1000);
593
- if (!existsSync3(timelinePath))
594
- continue;
595
- const next = readFileSync3(timelinePath, "utf8");
596
- if (next.length <= lastLength)
597
- continue;
598
- const delta = next.slice(lastLength);
599
- lastLength = next.length;
600
- for (const line of delta.split(/\r?\n/).map((entry) => entry.trim()).filter(Boolean)) {
601
- console.log(line);
602
- }
695
+ const nextPage = await getRunTimelineViaServer(context, run.value, { limit: 500, ...cursor ? { cursor } : {} });
696
+ const nextEvents = Array.isArray(nextPage.entries) ? nextPage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
697
+ cursor = typeof nextPage.nextCursor === "string" ? nextPage.nextCursor : cursor;
698
+ renderer.renderTimeline(nextEvents);
603
699
  }
604
700
  }
605
- return { ok: true, group: "run", command, details: { runId: run.value, events } };
701
+ return { ok: true, group: "run", command, details: { runId: run.value, events, cursor } };
606
702
  }
607
703
  case "attach": {
608
704
  let pending = rest;
@@ -61,7 +61,6 @@ function normalizeRuntimeAdapter(value) {
61
61
  }
62
62
 
63
63
  // packages/cli/src/commands/_server-client.ts
64
- import { spawnSync } from "child_process";
65
64
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
66
65
  import { resolve as resolve2 } from "path";
67
66
  import { ensureLocalRigServerConnection } from "@rig/runtime/local-server";
@@ -149,7 +148,7 @@ function resolveSelectedConnection(projectRoot, options = {}) {
149
148
  }
150
149
 
151
150
  // packages/cli/src/commands/_server-client.ts
152
- var cachedGitHubBearerToken;
151
+ var scopedGitHubBearerTokens = new Map;
153
152
  function cleanToken(value) {
154
153
  const trimmed = value?.trim();
155
154
  return trimmed ? trimmed : null;
@@ -166,25 +165,13 @@ function readPrivateRemoteSessionToken(projectRoot) {
166
165
  }
167
166
  }
168
167
  function readGitHubBearerTokenForRemote(projectRoot) {
169
- if (cachedGitHubBearerToken !== undefined)
170
- return cachedGitHubBearerToken;
168
+ const scopedKey = resolve2(projectRoot);
169
+ if (scopedGitHubBearerTokens.has(scopedKey))
170
+ return scopedGitHubBearerTokens.get(scopedKey) ?? null;
171
171
  const privateSession = readPrivateRemoteSessionToken(projectRoot);
172
- if (privateSession) {
173
- cachedGitHubBearerToken = privateSession;
174
- return cachedGitHubBearerToken;
175
- }
176
- const envToken = cleanToken(process.env.RIG_GITHUB_TOKEN) ?? cleanToken(process.env.GITHUB_TOKEN) ?? cleanToken(process.env.GH_TOKEN);
177
- if (envToken) {
178
- cachedGitHubBearerToken = envToken;
179
- return cachedGitHubBearerToken;
180
- }
181
- const result = spawnSync("gh", ["auth", "token"], {
182
- encoding: "utf8",
183
- timeout: 5000,
184
- stdio: ["ignore", "pipe", "ignore"]
185
- });
186
- cachedGitHubBearerToken = result.status === 0 ? cleanToken(result.stdout) : null;
187
- return cachedGitHubBearerToken;
172
+ if (privateSession)
173
+ return privateSession;
174
+ return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
188
175
  }
189
176
  async function ensureServerForCli(projectRoot) {
190
177
  try {
@@ -258,11 +258,10 @@ function resolveSelectedConnection(projectRoot, options = {}) {
258
258
  }
259
259
 
260
260
  // packages/cli/src/commands/_server-client.ts
261
- import { spawnSync } from "child_process";
262
261
  import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
263
262
  import { resolve as resolve4 } from "path";
264
263
  import { ensureLocalRigServerConnection } from "@rig/runtime/local-server";
265
- var cachedGitHubBearerToken;
264
+ var scopedGitHubBearerTokens = new Map;
266
265
  function cleanToken(value) {
267
266
  const trimmed = value?.trim();
268
267
  return trimmed ? trimmed : null;
@@ -279,25 +278,13 @@ function readPrivateRemoteSessionToken(projectRoot) {
279
278
  }
280
279
  }
281
280
  function readGitHubBearerTokenForRemote(projectRoot) {
282
- if (cachedGitHubBearerToken !== undefined)
283
- return cachedGitHubBearerToken;
281
+ const scopedKey = resolve4(projectRoot);
282
+ if (scopedGitHubBearerTokens.has(scopedKey))
283
+ return scopedGitHubBearerTokens.get(scopedKey) ?? null;
284
284
  const privateSession = readPrivateRemoteSessionToken(projectRoot);
285
- if (privateSession) {
286
- cachedGitHubBearerToken = privateSession;
287
- return cachedGitHubBearerToken;
288
- }
289
- const envToken = cleanToken(process.env.RIG_GITHUB_TOKEN) ?? cleanToken(process.env.GITHUB_TOKEN) ?? cleanToken(process.env.GH_TOKEN);
290
- if (envToken) {
291
- cachedGitHubBearerToken = envToken;
292
- return cachedGitHubBearerToken;
293
- }
294
- const result = spawnSync("gh", ["auth", "token"], {
295
- encoding: "utf8",
296
- timeout: 5000,
297
- stdio: ["ignore", "pipe", "ignore"]
298
- });
299
- cachedGitHubBearerToken = result.status === 0 ? cleanToken(result.stdout) : null;
300
- return cachedGitHubBearerToken;
285
+ if (privateSession)
286
+ return privateSession;
287
+ return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
301
288
  }
302
289
  async function ensureServerForCli(projectRoot) {
303
290
  try {