@h-rig/pi-rig 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.
@@ -79,8 +79,9 @@ function createRigContextFromEnv(env = process.env) {
79
79
  const discovered = discoverRigContext(env);
80
80
  const serverUrl = env.RIG_SERVER_URL ?? env.RIG_SERVER_BASE_URL ?? discovered.serverUrl;
81
81
  const projectRoot = env.RIG_PROJECT_ROOT ?? env.PROJECT_RIG_ROOT ?? discovered.projectRoot;
82
- const authToken = env.RIG_AUTH_TOKEN ?? env.RIG_GITHUB_TOKEN ?? env.GITHUB_TOKEN ?? env.GH_TOKEN ?? discovered.authToken;
82
+ const authToken = env.RIG_AUTH_TOKEN ?? env.RIG_SERVER_AUTH_TOKEN ?? discovered.authToken;
83
83
  const steeringPollMs = cleanNonNegativeInteger(env.RIG_STEERING_POLL_MS);
84
+ const operatorSession = env.RIG_PI_OPERATOR_SESSION === "1" || env.RIG_PI_OPERATOR_SESSION === "true";
84
85
  const active = Boolean(runId || taskId || serverUrl || projectRoot);
85
86
  if (!active)
86
87
  return { active: false };
@@ -91,7 +92,8 @@ function createRigContextFromEnv(env = process.env) {
91
92
  ...serverUrl ? { serverUrl } : {},
92
93
  ...projectRoot ? { projectRoot } : {},
93
94
  ...authToken ? { authToken } : {},
94
- ...steeringPollMs !== null ? { steeringPollMs } : {}
95
+ ...steeringPollMs !== null ? { steeringPollMs } : {},
96
+ ...operatorSession ? { operatorSession } : {}
95
97
  };
96
98
  }
97
99
  function joinUrl(baseUrl, pathname) {
@@ -157,6 +159,20 @@ class RigBridgeClient {
157
159
  const payload = await this.request(`/api/runs/${encodeURIComponent(runId)}`);
158
160
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { runId };
159
161
  }
162
+ async runLogs(runId = this.context.runId, limit = 20) {
163
+ if (!runId)
164
+ throw new Error("runId is required");
165
+ const payload = await this.request(`/api/runs/${encodeURIComponent(runId)}/logs?limit=${encodeURIComponent(String(limit))}`);
166
+ const entries = payload && typeof payload === "object" && !Array.isArray(payload) ? payload.entries : null;
167
+ return Array.isArray(entries) ? entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
168
+ }
169
+ async runTimeline(runId = this.context.runId, limit = 20) {
170
+ if (!runId)
171
+ throw new Error("runId is required");
172
+ const payload = await this.request(`/api/runs/${encodeURIComponent(runId)}/timeline?limit=${encodeURIComponent(String(limit))}`);
173
+ const entries = payload && typeof payload === "object" && !Array.isArray(payload) ? payload.entries : null;
174
+ return Array.isArray(entries) ? entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
175
+ }
160
176
  async steer(message, runId = this.context.runId) {
161
177
  if (!runId)
162
178
  throw new Error("runId is required");
@@ -167,6 +183,16 @@ class RigBridgeClient {
167
183
  });
168
184
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
169
185
  }
186
+ async stop(runId = this.context.runId) {
187
+ if (!runId)
188
+ throw new Error("runId is required");
189
+ const payload = await this.request("/api/runs/stop", {
190
+ method: "POST",
191
+ headers: { "content-type": "application/json" },
192
+ body: JSON.stringify({ runId })
193
+ });
194
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true, runId };
195
+ }
170
196
  async pollSteering(runId = this.context.runId) {
171
197
  if (!runId)
172
198
  return [];
@@ -1,5 +1,13 @@
1
1
  // @bun
2
2
  // packages/pi-rig/src/commands.ts
3
+ function runRecordFromPayload(payload) {
4
+ return payload.run && typeof payload.run === "object" && !Array.isArray(payload.run) ? payload.run : payload;
5
+ }
6
+ function formatEntry(entry) {
7
+ const type = String(entry.type ?? entry.title ?? "event");
8
+ const text = typeof entry.text === "string" ? entry.text : typeof entry.detail === "string" ? entry.detail : typeof entry.message === "string" ? entry.message : JSON.stringify(entry);
9
+ return `${type}: ${text}`;
10
+ }
3
11
  function createRigSlashCommands(input) {
4
12
  const notify = input.notify ?? (() => {});
5
13
  async function handleRig(args) {
@@ -28,18 +36,40 @@ function createRigSlashCommands(input) {
28
36
  }
29
37
  if (first === "attach") {
30
38
  const run = await input.client.attach(second);
31
- const runRecord = run.run && typeof run.run === "object" ? run.run : run;
39
+ const runRecord = runRecordFromPayload(run);
32
40
  notify(`Attached to ${String(runRecord.runId ?? second ?? input.context.runId ?? "run")}: ${String(runRecord.status ?? "unknown")}`, "info");
33
41
  return;
34
42
  }
35
- notify("Usage: /rig status | /rig task list | /rig task run [id] | /rig attach [run-id]", "error");
43
+ if (first === "timeline" || first === "logs") {
44
+ const runId = second || input.context.runId;
45
+ const entries = first === "timeline" ? await input.client.runTimeline(runId, 20) : await input.client.runLogs(runId, 20);
46
+ notify(entries.length > 0 ? entries.slice(-10).map(formatEntry).join(`
47
+ `) : `No ${first} entries for ${runId ?? "run"}.`, "info");
48
+ return;
49
+ }
50
+ if (first === "steer") {
51
+ const message = args.trim().slice("steer".length).trim();
52
+ if (!message) {
53
+ notify("Usage: /rig steer <message>", "error");
54
+ return;
55
+ }
56
+ await input.client.steer(message);
57
+ notify("Rig steering message queued.", "info");
58
+ return;
59
+ }
60
+ if (first === "stop") {
61
+ await input.client.stop(second || input.context.runId);
62
+ notify("Rig stop requested.", "info");
63
+ return;
64
+ }
65
+ notify("Usage: /rig status | /rig task list | /rig task run [id] | /rig attach [run-id] | /rig timeline [run-id] | /rig logs [run-id] | /rig steer <message> | /rig stop [run-id]", "error");
36
66
  } catch (error) {
37
67
  notify(error instanceof Error ? error.message : String(error), "error");
38
68
  }
39
69
  }
40
70
  return {
41
71
  rig: {
42
- description: "Rig control-plane commands: status, task list, task run, attach",
72
+ description: "Rig control-plane commands: status, task list, task run, attach, timeline, logs, steer, stop",
43
73
  handler: handleRig
44
74
  }
45
75
  };
package/dist/src/index.js CHANGED
@@ -79,8 +79,9 @@ function createRigContextFromEnv(env = process.env) {
79
79
  const discovered = discoverRigContext(env);
80
80
  const serverUrl = env.RIG_SERVER_URL ?? env.RIG_SERVER_BASE_URL ?? discovered.serverUrl;
81
81
  const projectRoot = env.RIG_PROJECT_ROOT ?? env.PROJECT_RIG_ROOT ?? discovered.projectRoot;
82
- const authToken = env.RIG_AUTH_TOKEN ?? env.RIG_GITHUB_TOKEN ?? env.GITHUB_TOKEN ?? env.GH_TOKEN ?? discovered.authToken;
82
+ const authToken = env.RIG_AUTH_TOKEN ?? env.RIG_SERVER_AUTH_TOKEN ?? discovered.authToken;
83
83
  const steeringPollMs = cleanNonNegativeInteger(env.RIG_STEERING_POLL_MS);
84
+ const operatorSession = env.RIG_PI_OPERATOR_SESSION === "1" || env.RIG_PI_OPERATOR_SESSION === "true";
84
85
  const active = Boolean(runId || taskId || serverUrl || projectRoot);
85
86
  if (!active)
86
87
  return { active: false };
@@ -91,7 +92,8 @@ function createRigContextFromEnv(env = process.env) {
91
92
  ...serverUrl ? { serverUrl } : {},
92
93
  ...projectRoot ? { projectRoot } : {},
93
94
  ...authToken ? { authToken } : {},
94
- ...steeringPollMs !== null ? { steeringPollMs } : {}
95
+ ...steeringPollMs !== null ? { steeringPollMs } : {},
96
+ ...operatorSession ? { operatorSession } : {}
95
97
  };
96
98
  }
97
99
  function joinUrl(baseUrl, pathname) {
@@ -157,6 +159,20 @@ class RigBridgeClient {
157
159
  const payload = await this.request(`/api/runs/${encodeURIComponent(runId)}`);
158
160
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { runId };
159
161
  }
162
+ async runLogs(runId = this.context.runId, limit = 20) {
163
+ if (!runId)
164
+ throw new Error("runId is required");
165
+ const payload = await this.request(`/api/runs/${encodeURIComponent(runId)}/logs?limit=${encodeURIComponent(String(limit))}`);
166
+ const entries = payload && typeof payload === "object" && !Array.isArray(payload) ? payload.entries : null;
167
+ return Array.isArray(entries) ? entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
168
+ }
169
+ async runTimeline(runId = this.context.runId, limit = 20) {
170
+ if (!runId)
171
+ throw new Error("runId is required");
172
+ const payload = await this.request(`/api/runs/${encodeURIComponent(runId)}/timeline?limit=${encodeURIComponent(String(limit))}`);
173
+ const entries = payload && typeof payload === "object" && !Array.isArray(payload) ? payload.entries : null;
174
+ return Array.isArray(entries) ? entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
175
+ }
160
176
  async steer(message, runId = this.context.runId) {
161
177
  if (!runId)
162
178
  throw new Error("runId is required");
@@ -167,6 +183,16 @@ class RigBridgeClient {
167
183
  });
168
184
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
169
185
  }
186
+ async stop(runId = this.context.runId) {
187
+ if (!runId)
188
+ throw new Error("runId is required");
189
+ const payload = await this.request("/api/runs/stop", {
190
+ method: "POST",
191
+ headers: { "content-type": "application/json" },
192
+ body: JSON.stringify({ runId })
193
+ });
194
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true, runId };
195
+ }
170
196
  async pollSteering(runId = this.context.runId) {
171
197
  if (!runId)
172
198
  return [];
@@ -201,6 +227,14 @@ class RigBridgeClient {
201
227
  }
202
228
 
203
229
  // packages/pi-rig/src/commands.ts
230
+ function runRecordFromPayload(payload) {
231
+ return payload.run && typeof payload.run === "object" && !Array.isArray(payload.run) ? payload.run : payload;
232
+ }
233
+ function formatEntry(entry) {
234
+ const type = String(entry.type ?? entry.title ?? "event");
235
+ const text = typeof entry.text === "string" ? entry.text : typeof entry.detail === "string" ? entry.detail : typeof entry.message === "string" ? entry.message : JSON.stringify(entry);
236
+ return `${type}: ${text}`;
237
+ }
204
238
  function createRigSlashCommands(input) {
205
239
  const notify = input.notify ?? (() => {});
206
240
  async function handleRig(args) {
@@ -229,18 +263,40 @@ function createRigSlashCommands(input) {
229
263
  }
230
264
  if (first === "attach") {
231
265
  const run = await input.client.attach(second);
232
- const runRecord = run.run && typeof run.run === "object" ? run.run : run;
266
+ const runRecord = runRecordFromPayload(run);
233
267
  notify(`Attached to ${String(runRecord.runId ?? second ?? input.context.runId ?? "run")}: ${String(runRecord.status ?? "unknown")}`, "info");
234
268
  return;
235
269
  }
236
- notify("Usage: /rig status | /rig task list | /rig task run [id] | /rig attach [run-id]", "error");
270
+ if (first === "timeline" || first === "logs") {
271
+ const runId = second || input.context.runId;
272
+ const entries = first === "timeline" ? await input.client.runTimeline(runId, 20) : await input.client.runLogs(runId, 20);
273
+ notify(entries.length > 0 ? entries.slice(-10).map(formatEntry).join(`
274
+ `) : `No ${first} entries for ${runId ?? "run"}.`, "info");
275
+ return;
276
+ }
277
+ if (first === "steer") {
278
+ const message = args.trim().slice("steer".length).trim();
279
+ if (!message) {
280
+ notify("Usage: /rig steer <message>", "error");
281
+ return;
282
+ }
283
+ await input.client.steer(message);
284
+ notify("Rig steering message queued.", "info");
285
+ return;
286
+ }
287
+ if (first === "stop") {
288
+ await input.client.stop(second || input.context.runId);
289
+ notify("Rig stop requested.", "info");
290
+ return;
291
+ }
292
+ notify("Usage: /rig status | /rig task list | /rig task run [id] | /rig attach [run-id] | /rig timeline [run-id] | /rig logs [run-id] | /rig steer <message> | /rig stop [run-id]", "error");
237
293
  } catch (error) {
238
294
  notify(error instanceof Error ? error.message : String(error), "error");
239
295
  }
240
296
  }
241
297
  return {
242
298
  rig: {
243
- description: "Rig control-plane commands: status, task list, task run, attach",
299
+ description: "Rig control-plane commands: status, task list, task run, attach, timeline, logs, steer, stop",
244
300
  handler: handleRig
245
301
  }
246
302
  };
@@ -318,6 +374,35 @@ function notify(ctx, message, level = "info") {
318
374
  notifyFn.call(ui, message, level);
319
375
  }
320
376
  }
377
+ function setWidget(ctx, id, lines) {
378
+ const ui = ctx && typeof ctx === "object" ? ctx.ui : null;
379
+ const setWidgetFn = ui && typeof ui === "object" ? ui.setWidget : null;
380
+ if (typeof setWidgetFn === "function") {
381
+ setWidgetFn.call(ui, id, lines);
382
+ }
383
+ }
384
+ function setStatus(ctx, id, text) {
385
+ const ui = ctx && typeof ctx === "object" ? ctx.ui : null;
386
+ const setStatusFn = ui && typeof ui === "object" ? ui.setStatus : null;
387
+ if (typeof setStatusFn === "function") {
388
+ setStatusFn.call(ui, id, text);
389
+ }
390
+ }
391
+ function setFooter(ctx, line) {
392
+ const ui = ctx && typeof ctx === "object" ? ctx.ui : null;
393
+ const setFooterFn = ui && typeof ui === "object" ? ui.setFooter : null;
394
+ if (typeof setFooterFn !== "function")
395
+ return;
396
+ setFooterFn.call(ui, () => ({
397
+ render(width) {
398
+ const max = Math.max(0, Math.trunc(width));
399
+ if (max === 0)
400
+ return [""];
401
+ return [line.length > max ? `${line.slice(0, Math.max(0, max - 1))}\u2026` : line];
402
+ },
403
+ invalidate() {}
404
+ }));
405
+ }
321
406
  function steeringText(message) {
322
407
  const text = typeof message.message === "string" ? message.message.trim() : "";
323
408
  if (!text)
@@ -327,7 +412,7 @@ function steeringText(message) {
327
412
  ${text}`;
328
413
  }
329
414
  async function consumeQueuedSteering(pi, state, ctx) {
330
- if (!state.active || !state.runId || typeof pi.sendUserMessage !== "function")
415
+ if (state.operatorSession || !state.active || !state.runId || typeof pi.sendUserMessage !== "function")
331
416
  return;
332
417
  try {
333
418
  const messages = await state.client.consumeSteering(state.runId);
@@ -344,8 +429,86 @@ async function consumeQueuedSteering(pi, state, ctx) {
344
429
  notify(ctx, `Rig steering sync failed: ${error instanceof Error ? error.message : String(error)}`, "error");
345
430
  }
346
431
  }
432
+ function inputText(event) {
433
+ if (!event || typeof event !== "object" || Array.isArray(event))
434
+ return null;
435
+ const text = event.text;
436
+ return typeof text === "string" && text.trim() ? text.trim() : null;
437
+ }
438
+ async function handleOperatorInput(event, state, ctx) {
439
+ if (!state.operatorSession || !state.active || !state.runId)
440
+ return;
441
+ const text = inputText(event);
442
+ if (!text || text.startsWith("/"))
443
+ return;
444
+ try {
445
+ await state.client.steer(text, state.runId);
446
+ notify(ctx, "Rig steering message queued.");
447
+ } catch (error) {
448
+ notify(ctx, `Rig steering failed: ${error instanceof Error ? error.message : String(error)}`, "error");
449
+ }
450
+ return { action: "handled" };
451
+ }
452
+ function shortEntry(entry) {
453
+ const type = String(entry.type ?? entry.title ?? "event");
454
+ const text = typeof entry.text === "string" ? entry.text : typeof entry.detail === "string" ? entry.detail : typeof entry.message === "string" ? entry.message : "";
455
+ return `${type}: ${text}`.slice(0, 160);
456
+ }
457
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
458
+ function runLocation(run) {
459
+ const worktree = typeof run.worktreePath === "string" && run.worktreePath.trim() ? run.worktreePath.trim() : null;
460
+ const projectRoot = typeof run.projectRoot === "string" && run.projectRoot.trim() ? run.projectRoot.trim() : null;
461
+ return worktree ?? projectRoot ?? "remote/local worker workspace";
462
+ }
463
+ function runPayload(payload) {
464
+ return payload.run && typeof payload.run === "object" && !Array.isArray(payload.run) ? payload.run : payload;
465
+ }
466
+ function startOperatorRunWidget(state, ctx) {
467
+ if (!state.operatorSession || !state.active || !state.runId)
468
+ return;
469
+ let inFlight = false;
470
+ let frame = 0;
471
+ const refresh = async () => {
472
+ if (inFlight)
473
+ return;
474
+ inFlight = true;
475
+ const spinner = SPINNER_FRAMES[frame++ % SPINNER_FRAMES.length] ?? "\u2022";
476
+ try {
477
+ const [runPayloadRecord, timeline] = await Promise.all([
478
+ state.client.attach(state.runId),
479
+ state.client.runTimeline(state.runId, 8).catch(() => [])
480
+ ]);
481
+ const run = runPayload(runPayloadRecord);
482
+ const status = String(run.status ?? "unknown");
483
+ const header = `${spinner} Rig ${String(run.runId ?? state.runId)} \xB7 ${status}`;
484
+ const location = runLocation(run);
485
+ const detail = typeof run.statusDetail === "string" && run.statusDetail.trim() ? run.statusDetail.trim() : String(run.title ?? run.taskId ?? "");
486
+ const lines = [
487
+ header,
488
+ `worker: ${location}`.slice(0, 200),
489
+ ...detail ? [detail.slice(0, 160)] : [],
490
+ ...timeline.slice(-5).map(shortEntry)
491
+ ];
492
+ setStatus(ctx, "rig", `${spinner} Rig ${status}`);
493
+ setFooter(ctx, `${spinner} Rig ${String(run.runId ?? state.runId)} \xB7 ${status} \xB7 worker ${location}`);
494
+ setWidget(ctx, "rig-run", lines);
495
+ } catch (error) {
496
+ const message = error instanceof Error ? error.message : String(error);
497
+ setStatus(ctx, "rig", `${spinner} Rig unavailable`);
498
+ setFooter(ctx, `${spinner} Rig ${state.runId} \xB7 unavailable \xB7 ${message}`);
499
+ setWidget(ctx, "rig-run", [`${spinner} Rig ${state.runId} \xB7 unavailable`, message]);
500
+ } finally {
501
+ inFlight = false;
502
+ }
503
+ };
504
+ refresh();
505
+ const timer = setInterval(() => void refresh(), 1000);
506
+ if (typeof timer.unref === "function") {
507
+ timer.unref();
508
+ }
509
+ }
347
510
  function startLiveSteeringPoll(pi, state, ctx) {
348
- if (!state.active || !state.runId || typeof pi.sendUserMessage !== "function")
511
+ if (state.operatorSession || !state.active || !state.runId || typeof pi.sendUserMessage !== "function")
349
512
  return;
350
513
  const intervalMs = state.steeringPollMs ?? 1000;
351
514
  if (intervalMs <= 0)
@@ -389,14 +552,12 @@ function createPiRigExtension(pi, options = {}) {
389
552
  }
390
553
  startLiveSteeringPoll(pi, state, globalThis);
391
554
  }
555
+ pi.on?.("input", async (event, ctx) => handleOperatorInput(event, state, ctx));
392
556
  pi.on?.("session_start", async (_event, ctx) => {
393
557
  if (!state.active || !state.runId)
394
558
  return;
395
- const ui = ctx && typeof ctx === "object" ? ctx.ui : null;
396
- const setStatus = ui && typeof ui === "object" ? ui.setStatus : null;
397
- if (typeof setStatus === "function") {
398
- setStatus.call(ui, "rig", `Rig ${state.runId}`);
399
- }
559
+ setStatus(ctx, "rig", `Rig ${state.runId}`);
560
+ startOperatorRunWidget(state, ctx);
400
561
  await consumeQueuedSteering(pi, state, ctx);
401
562
  });
402
563
  pi.on?.("turn_end", async (_event, ctx) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@h-rig/pi-rig",
3
- "version": "0.0.6-alpha.6",
3
+ "version": "0.0.6-alpha.60",
4
4
  "type": "module",
5
5
  "description": "Rig package",
6
6
  "license": "UNLICENSED",
@@ -33,7 +33,7 @@
33
33
  ]
34
34
  },
35
35
  "peerDependencies": {
36
- "@earendil-works/pi-coding-agent": "*",
36
+ "@earendil-works/pi-coding-agent": "npm:@h-rig/pi-coding-agent@0.0.6-alpha.60",
37
37
  "typebox": "*"
38
38
  }
39
39
  }