@h-rig/cli 0.0.6-alpha.41 → 0.0.6-alpha.43

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.
@@ -0,0 +1,63 @@
1
+ // @bun
2
+ // packages/cli/src/commands/_spinner.ts
3
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
4
+ function createTtySpinner(input) {
5
+ const output = input.output ?? process.stdout;
6
+ const isTty = output.isTTY === true;
7
+ let label = input.label;
8
+ let frame = 0;
9
+ let paused = false;
10
+ let stopped = false;
11
+ let lastPrintedLabel = "";
12
+ const render = () => {
13
+ if (stopped || paused)
14
+ return;
15
+ if (!isTty) {
16
+ if (label !== lastPrintedLabel) {
17
+ output.write(`${label}
18
+ `);
19
+ lastPrintedLabel = label;
20
+ }
21
+ return;
22
+ }
23
+ frame = (frame + 1) % SPINNER_FRAMES.length;
24
+ output.write(`\r\x1B[2K${SPINNER_FRAMES[frame]} ${label}`);
25
+ };
26
+ const clearLine = () => {
27
+ if (isTty)
28
+ output.write("\r\x1B[2K");
29
+ };
30
+ render();
31
+ const timer = isTty ? setInterval(render, input.intervalMs ?? 120) : null;
32
+ return {
33
+ setLabel(next) {
34
+ label = next;
35
+ render();
36
+ },
37
+ pause() {
38
+ paused = true;
39
+ clearLine();
40
+ },
41
+ resume() {
42
+ if (stopped)
43
+ return;
44
+ paused = false;
45
+ render();
46
+ },
47
+ stop(finalLine) {
48
+ if (stopped)
49
+ return;
50
+ stopped = true;
51
+ if (timer)
52
+ clearInterval(timer);
53
+ clearLine();
54
+ if (finalLine)
55
+ output.write(`${finalLine}
56
+ `);
57
+ }
58
+ };
59
+ }
60
+ export {
61
+ createTtySpinner,
62
+ SPINNER_FRAMES
63
+ };
@@ -319,6 +319,10 @@ async function getRunPiCommandsViaServer(context, runId) {
319
319
  const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/commands`);
320
320
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { commands: [] };
321
321
  }
322
+ async function getRunPiCapabilitiesViaServer(context, runId) {
323
+ const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/capabilities`);
324
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { capabilities: null };
325
+ }
322
326
  async function sendRunPiPromptViaServer(context, runId, text, streamingBehavior) {
323
327
  const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/prompt`, {
324
328
  method: "POST",
@@ -567,7 +571,7 @@ import {
567
571
  } from "@earendil-works/pi-coding-agent";
568
572
 
569
573
  // packages/cli/src/commands/_pi-remote-session.ts
570
- import { AgentSession as PiAgentSession } from "@earendil-works/pi-coding-agent";
574
+ import { AgentSession as PiAgentSession, expandPromptTemplate } from "@earendil-works/pi-coding-agent";
571
575
  var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
572
576
  function defaultTransport() {
573
577
  return {
@@ -575,6 +579,7 @@ function defaultTransport() {
575
579
  getMessages: getRunPiMessagesViaServer,
576
580
  getStatus: getRunPiStatusViaServer,
577
581
  getCommands: getRunPiCommandsViaServer,
582
+ getCapabilities: getRunPiCapabilitiesViaServer,
578
583
  sendPrompt: sendRunPiPromptViaServer,
579
584
  sendShell: sendRunPiShellViaServer,
580
585
  runCommand: runRunPiCommandViaServer,
@@ -662,16 +667,18 @@ class RigRemoteSessionController {
662
667
  this.hooks.onStatusText?.("worker Pi WebSocket disconnected");
663
668
  };
664
669
  try {
665
- const [messagesPayload, statusPayload, commandsPayload] = await Promise.all([
670
+ const [messagesPayload, statusPayload, commandsPayload, capabilitiesPayload] = await Promise.all([
666
671
  this.transport.getMessages(this.context, this.runId),
667
672
  this.transport.getStatus(this.context, this.runId),
668
- this.transport.getCommands(this.context, this.runId)
673
+ this.transport.getCommands(this.context, this.runId),
674
+ this.transport.getCapabilities(this.context, this.runId).catch(() => ({ capabilities: null }))
669
675
  ]);
670
676
  const messages = Array.isArray(messagesPayload.messages) ? messagesPayload.messages : [];
671
677
  this.applyStatusPayload(statusPayload);
672
678
  this.session?.replaceRemoteMessages(messages);
673
679
  const commands = Array.isArray(commandsPayload.commands) ? commandsPayload.commands : [];
674
- this.hooks.onCatchUp?.(messages, commands);
680
+ const capabilities = capabilitiesPayload.capabilities && typeof capabilitiesPayload.capabilities === "object" && !Array.isArray(capabilitiesPayload.capabilities) ? capabilitiesPayload.capabilities : null;
681
+ this.hooks.onCatchUp?.(messages, commands, capabilities);
675
682
  catchupDone = true;
676
683
  for (const payload of buffered.splice(0))
677
684
  this.applyEnvelope(payload);
@@ -848,6 +855,20 @@ class RigRemoteAgentSession extends PiAgentSession {
848
855
  replaceRemoteMessages(messages) {
849
856
  this.agent.state.messages = messages;
850
857
  }
858
+ async expandLocalInput(text) {
859
+ let current = text;
860
+ const runner = this.extensionRunner;
861
+ if (runner.hasHandlers("input")) {
862
+ const result = await runner.emitInput(current, undefined, "interactive");
863
+ if (result.action === "handled")
864
+ return null;
865
+ if (result.action === "transform")
866
+ current = result.text;
867
+ }
868
+ current = this._expandSkillCommand(current);
869
+ current = expandPromptTemplate(current, [...this.promptTemplates]);
870
+ return current;
871
+ }
851
872
  async prompt(text, options) {
852
873
  const trimmed = text.trim();
853
874
  if (!trimmed)
@@ -855,6 +876,15 @@ class RigRemoteAgentSession extends PiAgentSession {
855
876
  if (trimmed.startsWith("/")) {
856
877
  if (await this._tryExecuteExtensionCommand(trimmed))
857
878
  return;
879
+ const expanded2 = await this.expandLocalInput(trimmed);
880
+ if (expanded2 === null)
881
+ return;
882
+ if (expanded2 !== trimmed) {
883
+ const behavior2 = options?.streamingBehavior ?? (this.isStreaming || this.isCompacting ? "steer" : undefined);
884
+ options?.preflightResult?.(true);
885
+ await this.remote.sendPrompt(expanded2, behavior2);
886
+ return;
887
+ }
858
888
  await this.remote.sendCommand(trimmed);
859
889
  return;
860
890
  }
@@ -862,15 +892,30 @@ class RigRemoteAgentSession extends PiAgentSession {
862
892
  await this.remote.sendShell(trimmed);
863
893
  return;
864
894
  }
895
+ const expanded = await this.expandLocalInput(trimmed);
896
+ if (expanded === null)
897
+ return;
865
898
  const behavior = options?.streamingBehavior ?? (this.isStreaming || this.isCompacting ? "steer" : undefined);
866
899
  options?.preflightResult?.(true);
867
- await this.remote.sendPrompt(trimmed, behavior);
900
+ await this.remote.sendPrompt(expanded, behavior);
868
901
  }
869
902
  async steer(text) {
870
- await this.remote.sendPrompt(text, "steer");
903
+ const trimmed = text.trim();
904
+ if (trimmed.startsWith("/") && await this._tryExecuteExtensionCommand(trimmed))
905
+ return;
906
+ const expanded = await this.expandLocalInput(trimmed);
907
+ if (expanded === null)
908
+ return;
909
+ await this.remote.sendPrompt(expanded, "steer");
871
910
  }
872
911
  async followUp(text) {
873
- await this.remote.sendPrompt(text, "followUp");
912
+ const trimmed = text.trim();
913
+ if (trimmed.startsWith("/") && await this._tryExecuteExtensionCommand(trimmed))
914
+ return;
915
+ const expanded = await this.expandLocalInput(trimmed);
916
+ if (expanded === null)
917
+ return;
918
+ await this.remote.sendPrompt(expanded, "followUp");
874
919
  }
875
920
  async abort() {
876
921
  await this.remote.abort();
@@ -983,6 +1028,9 @@ function createRemoteBashOperations(controller, excludeFromContext) {
983
1028
  };
984
1029
  }
985
1030
 
1031
+ // packages/cli/src/commands/_spinner.ts
1032
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1033
+
986
1034
  // packages/cli/src/commands/_pi-worker-bridge-extension.ts
987
1035
  function recordOf2(value) {
988
1036
  return value && typeof value === "object" && !Array.isArray(value) ? value : null;
@@ -1000,6 +1048,50 @@ function asText(value) {
1000
1048
  return String(value);
1001
1049
  }
1002
1050
  }
1051
+ function names(value, key = "name") {
1052
+ if (!Array.isArray(value))
1053
+ return [];
1054
+ return value.flatMap((entry) => {
1055
+ if (typeof entry === "string")
1056
+ return [entry];
1057
+ const record = recordOf2(entry);
1058
+ const name = record?.[key];
1059
+ return typeof name === "string" ? [name] : [];
1060
+ });
1061
+ }
1062
+ function renderWorkerCapabilities(capabilities) {
1063
+ const lines = ["Worker session capabilities (in effect for this run)"];
1064
+ const tools = Array.isArray(capabilities.tools) ? capabilities.tools : [];
1065
+ const activeTools = tools.flatMap((tool) => {
1066
+ const record = recordOf2(tool);
1067
+ return record && record.active === true && typeof record.name === "string" ? [record.name] : [];
1068
+ });
1069
+ const inactiveCount = tools.length - activeTools.length;
1070
+ lines.push(` tools ${activeTools.join(", ") || "(none)"}${inactiveCount > 0 ? ` (+${inactiveCount} inactive)` : ""}`);
1071
+ const extensions = Array.isArray(capabilities.extensions) ? capabilities.extensions : [];
1072
+ const extensionLabels = extensions.flatMap((entry) => {
1073
+ const record = recordOf2(entry);
1074
+ if (!record)
1075
+ return [];
1076
+ const path = typeof record.path === "string" ? record.path : "";
1077
+ const short = path.split("/").filter(Boolean).slice(-2).join("/") || path || "extension";
1078
+ return [short];
1079
+ });
1080
+ lines.push(` extensions ${extensionLabels.join(", ") || "(none)"}`);
1081
+ const hookEvents = names(capabilities.hookEvents);
1082
+ const hookList = Array.isArray(capabilities.hookEvents) ? capabilities.hookEvents.map(asText).filter(Boolean) : hookEvents;
1083
+ lines.push(` hooks ${hookList.join(", ") || "(none)"}`);
1084
+ lines.push(` skills ${names(capabilities.skills).join(", ") || "(none)"}`);
1085
+ lines.push(` prompts ${names(capabilities.prompts).join(", ") || "(none)"}`);
1086
+ const model = typeof capabilities.model === "string" ? capabilities.model : "(unknown)";
1087
+ const thinking = typeof capabilities.thinkingLevel === "string" ? capabilities.thinkingLevel : "";
1088
+ const cwd = typeof capabilities.cwd === "string" ? capabilities.cwd : "";
1089
+ lines.push(` model ${model}${thinking ? ` \xB7 ${thinking}` : ""}`);
1090
+ if (cwd)
1091
+ lines.push(` cwd ${cwd}`);
1092
+ lines.push(" (worker commands are in your palette as [worker \u2026] \xB7 /worker shows this again)");
1093
+ return lines;
1094
+ }
1003
1095
  async function answerExtensionUiRequest(options, ctx, request) {
1004
1096
  const requestId = String(request.requestId ?? request.id ?? `ui-${Date.now()}`);
1005
1097
  const method = String(request.method ?? request.type ?? "input");
@@ -1026,7 +1118,7 @@ async function answerExtensionUiRequest(options, ctx, request) {
1026
1118
  ctx.ui.notify(`Failed to answer worker UI request: ${error instanceof Error ? error.message : String(error)}`, "error");
1027
1119
  }
1028
1120
  }
1029
- var LOCALLY_OWNED_COMMANDS = new Set(["detach", "quit", "q", "stop", "settings", "model", "thinking", "compact", "session", "name", "reload"]);
1121
+ var LOCALLY_OWNED_COMMANDS = new Set(["detach", "quit", "q", "stop", "worker", "settings", "model", "thinking", "compact", "session", "name", "reload"]);
1030
1122
  function registerDaemonCommands(pi, options, ctx, commands, registered) {
1031
1123
  for (const command of commands) {
1032
1124
  const record = recordOf2(command);
@@ -1054,6 +1146,21 @@ function registerDaemonCommands(pi, options, ctx, commands, registered) {
1054
1146
  function createRigWorkerPiBridgeExtension(options) {
1055
1147
  return (pi) => {
1056
1148
  const registeredDaemonCommands = new Set;
1149
+ let capabilityLines = null;
1150
+ let statusText = "connecting to worker session";
1151
+ let busy = true;
1152
+ let connected = false;
1153
+ let frame = 0;
1154
+ let spinnerTimer = null;
1155
+ const renderStatus = (ctx) => {
1156
+ const prefix = busy ? `${SPINNER_FRAMES[frame]} ` : connected ? "\u25CF " : "\u25CB ";
1157
+ ctx.ui.setStatus("rig-worker-pi", `${prefix}${statusText}`);
1158
+ };
1159
+ const setStatus = (ctx, text, isBusy) => {
1160
+ statusText = text;
1161
+ busy = isBusy;
1162
+ renderStatus(ctx);
1163
+ };
1057
1164
  pi.registerCommand("detach", {
1058
1165
  description: "Detach from this run; the worker keeps going",
1059
1166
  handler: async (_args, ctx) => {
@@ -1069,31 +1176,57 @@ function createRigWorkerPiBridgeExtension(options) {
1069
1176
  ctx.shutdown();
1070
1177
  }
1071
1178
  });
1179
+ pi.registerCommand("worker", {
1180
+ description: "Show the worker session's real capabilities (tools, extensions, hooks, skills)",
1181
+ handler: async (_args, ctx) => {
1182
+ if (capabilityLines)
1183
+ ctx.ui.setWidget("rig-worker-capabilities", capabilityLines, { placement: "aboveEditor" });
1184
+ else
1185
+ ctx.ui.notify("Worker capabilities are not available yet (still connecting).", "info");
1186
+ }
1187
+ });
1072
1188
  pi.on("user_bash", (event) => ({
1073
1189
  operations: createRemoteBashOperations(options.controller, event.excludeFromContext === true)
1074
1190
  }));
1075
1191
  pi.on("session_start", async (_event, ctx) => {
1076
- ctx.ui.setTitle("Rig \xB7 worker session");
1077
- ctx.ui.setStatus("rig-worker-pi", "connecting to worker session");
1078
- ctx.ui.notify(`Attached to Rig run ${options.runId}. Native Pi, remote brain \u2014 /detach exits, /stop cancels the run.`, "info");
1192
+ ctx.ui.setTitle("Rig \xB7 enriched bundled Pi");
1193
+ setStatus(ctx, "waiting for worker Pi daemon", true);
1194
+ spinnerTimer = setInterval(() => {
1195
+ frame = (frame + 1) % SPINNER_FRAMES.length;
1196
+ if (busy)
1197
+ renderStatus(ctx);
1198
+ }, 150);
1199
+ 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");
1079
1200
  if (options.initialMessageSent)
1080
1201
  ctx.ui.notify("Initial message sent to the worker Pi daemon.", "info");
1081
1202
  const nativeUi = ctx.ui;
1082
1203
  options.controller.setUiHooks({
1083
- onStatusText: (text) => ctx.ui.setStatus("rig-worker-pi", text),
1084
- onActivity: (label, detail) => ctx.ui.setStatus("rig-worker-pi", [label, detail].filter(Boolean).join(" \u2014 ")),
1085
- onConnectionChange: (connected) => ctx.ui.setStatus("rig-worker-pi", connected ? "worker session live" : "worker session disconnected"),
1204
+ onStatusText: (text) => setStatus(ctx, text, !connected),
1205
+ onActivity: (label, detail) => {
1206
+ const active = label !== "idle" && !/complete|ready/i.test(label);
1207
+ setStatus(ctx, [label, detail].filter(Boolean).join(" \u2014 "), active);
1208
+ if (/agent running|tool:/.test(label))
1209
+ ctx.ui.setWidget("rig-worker-capabilities", undefined);
1210
+ },
1211
+ onConnectionChange: (nextConnected) => {
1212
+ connected = nextConnected;
1213
+ setStatus(ctx, nextConnected ? "worker session live" : "worker session disconnected", false);
1214
+ },
1086
1215
  onError: (message) => ctx.ui.notify(message, "error"),
1087
1216
  onExtensionUiRequest: (request) => {
1088
1217
  answerExtensionUiRequest(options, ctx, request);
1089
1218
  },
1090
- onCatchUp: (messages, commands) => {
1219
+ onCatchUp: (messages, commands, capabilities) => {
1091
1220
  if (nativeUi.appendSessionMessages)
1092
1221
  nativeUi.appendSessionMessages(messages);
1093
1222
  const cwd = options.controller.status.cwd;
1094
1223
  if (nativeUi.setDisplayCwd && cwd)
1095
1224
  nativeUi.setDisplayCwd(cwd);
1096
1225
  registerDaemonCommands(pi, options, ctx, commands, registeredDaemonCommands);
1226
+ if (capabilities) {
1227
+ capabilityLines = renderWorkerCapabilities(capabilities);
1228
+ ctx.ui.setWidget("rig-worker-capabilities", capabilityLines, { placement: "aboveEditor" });
1229
+ }
1097
1230
  }
1098
1231
  });
1099
1232
  options.controller.connect().catch((error) => {
@@ -1101,6 +1234,8 @@ function createRigWorkerPiBridgeExtension(options) {
1101
1234
  });
1102
1235
  });
1103
1236
  pi.on("session_shutdown", () => {
1237
+ if (spinnerTimer)
1238
+ clearInterval(spinnerTimer);
1104
1239
  options.controller.close();
1105
1240
  });
1106
1241
  };
@@ -1142,6 +1277,9 @@ async function attachRunBundledPiFrontend(context, input) {
1142
1277
  initialMessageSent: input.steered === true
1143
1278
  })
1144
1279
  ],
1280
+ noExtensions: true,
1281
+ noSkills: true,
1282
+ noPromptTemplates: true,
1145
1283
  noContextFiles: true
1146
1284
  }
1147
1285
  });
@@ -1176,7 +1314,7 @@ async function attachRunBundledPiFrontend(context, input) {
1176
1314
  timelineCursor: null,
1177
1315
  steered: input.steered === true,
1178
1316
  detached,
1179
- rendered: "native bundled Pi frontend with remote worker session runtime"
1317
+ rendered: "enriched bundled Pi frontend with remote worker session runtime"
1180
1318
  };
1181
1319
  }
1182
1320