@h-rig/cli 0.0.6-alpha.42 → 0.0.6-alpha.44

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,21 +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
903
  const trimmed = text.trim();
871
904
  if (trimmed.startsWith("/") && await this._tryExecuteExtensionCommand(trimmed))
872
905
  return;
873
- await this.remote.sendPrompt(trimmed, "steer");
906
+ const expanded = await this.expandLocalInput(trimmed);
907
+ if (expanded === null)
908
+ return;
909
+ await this.remote.sendPrompt(expanded, "steer");
874
910
  }
875
911
  async followUp(text) {
876
912
  const trimmed = text.trim();
877
913
  if (trimmed.startsWith("/") && await this._tryExecuteExtensionCommand(trimmed))
878
914
  return;
879
- await this.remote.sendPrompt(trimmed, "followUp");
915
+ const expanded = await this.expandLocalInput(trimmed);
916
+ if (expanded === null)
917
+ return;
918
+ await this.remote.sendPrompt(expanded, "followUp");
880
919
  }
881
920
  async abort() {
882
921
  await this.remote.abort();
@@ -989,6 +1028,9 @@ function createRemoteBashOperations(controller, excludeFromContext) {
989
1028
  };
990
1029
  }
991
1030
 
1031
+ // packages/cli/src/commands/_spinner.ts
1032
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1033
+
992
1034
  // packages/cli/src/commands/_pi-worker-bridge-extension.ts
993
1035
  function recordOf2(value) {
994
1036
  return value && typeof value === "object" && !Array.isArray(value) ? value : null;
@@ -1006,6 +1048,50 @@ function asText(value) {
1006
1048
  return String(value);
1007
1049
  }
1008
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
+ }
1009
1095
  async function answerExtensionUiRequest(options, ctx, request) {
1010
1096
  const requestId = String(request.requestId ?? request.id ?? `ui-${Date.now()}`);
1011
1097
  const method = String(request.method ?? request.type ?? "input");
@@ -1032,7 +1118,7 @@ async function answerExtensionUiRequest(options, ctx, request) {
1032
1118
  ctx.ui.notify(`Failed to answer worker UI request: ${error instanceof Error ? error.message : String(error)}`, "error");
1033
1119
  }
1034
1120
  }
1035
- 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"]);
1036
1122
  function registerDaemonCommands(pi, options, ctx, commands, registered) {
1037
1123
  for (const command of commands) {
1038
1124
  const record = recordOf2(command);
@@ -1060,6 +1146,21 @@ function registerDaemonCommands(pi, options, ctx, commands, registered) {
1060
1146
  function createRigWorkerPiBridgeExtension(options) {
1061
1147
  return (pi) => {
1062
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
+ };
1063
1164
  pi.registerCommand("detach", {
1064
1165
  description: "Detach from this run; the worker keeps going",
1065
1166
  handler: async (_args, ctx) => {
@@ -1075,31 +1176,57 @@ function createRigWorkerPiBridgeExtension(options) {
1075
1176
  ctx.shutdown();
1076
1177
  }
1077
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
+ });
1078
1188
  pi.on("user_bash", (event) => ({
1079
1189
  operations: createRemoteBashOperations(options.controller, event.excludeFromContext === true)
1080
1190
  }));
1081
1191
  pi.on("session_start", async (_event, ctx) => {
1082
- ctx.ui.setTitle("Rig \xB7 worker session");
1083
- ctx.ui.setStatus("rig-worker-pi", "connecting to worker session");
1084
- 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");
1085
1200
  if (options.initialMessageSent)
1086
1201
  ctx.ui.notify("Initial message sent to the worker Pi daemon.", "info");
1087
1202
  const nativeUi = ctx.ui;
1088
1203
  options.controller.setUiHooks({
1089
- onStatusText: (text) => ctx.ui.setStatus("rig-worker-pi", text),
1090
- onActivity: (label, detail) => ctx.ui.setStatus("rig-worker-pi", [label, detail].filter(Boolean).join(" \u2014 ")),
1091
- 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
+ },
1092
1215
  onError: (message) => ctx.ui.notify(message, "error"),
1093
1216
  onExtensionUiRequest: (request) => {
1094
1217
  answerExtensionUiRequest(options, ctx, request);
1095
1218
  },
1096
- onCatchUp: (messages, commands) => {
1219
+ onCatchUp: (messages, commands, capabilities) => {
1097
1220
  if (nativeUi.appendSessionMessages)
1098
1221
  nativeUi.appendSessionMessages(messages);
1099
1222
  const cwd = options.controller.status.cwd;
1100
1223
  if (nativeUi.setDisplayCwd && cwd)
1101
1224
  nativeUi.setDisplayCwd(cwd);
1102
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
+ }
1103
1230
  }
1104
1231
  });
1105
1232
  options.controller.connect().catch((error) => {
@@ -1107,6 +1234,8 @@ function createRigWorkerPiBridgeExtension(options) {
1107
1234
  });
1108
1235
  });
1109
1236
  pi.on("session_shutdown", () => {
1237
+ if (spinnerTimer)
1238
+ clearInterval(spinnerTimer);
1110
1239
  options.controller.close();
1111
1240
  });
1112
1241
  };
@@ -1132,7 +1261,8 @@ async function attachRunBundledPiFrontend(context, input) {
1132
1261
  const tempSessionDir = mkdtempSync(join(tmpdir(), "rig-pi-frontend-sessions-"));
1133
1262
  const restoreEnv = setTemporaryEnv({
1134
1263
  PI_CODING_AGENT_SESSION_DIR: tempSessionDir,
1135
- PI_SKIP_VERSION_CHECK: "1"
1264
+ PI_SKIP_VERSION_CHECK: "1",
1265
+ PI_HIDDEN_COMMANDS: "import,fork,clone,tree,new,resume,trust"
1136
1266
  });
1137
1267
  const controller = new RigRemoteSessionController({ context, runId: input.runId });
1138
1268
  const createRemoteRuntime = async ({ cwd, agentDir, sessionManager, sessionStartEvent }) => {
@@ -1148,6 +1278,9 @@ async function attachRunBundledPiFrontend(context, input) {
1148
1278
  initialMessageSent: input.steered === true
1149
1279
  })
1150
1280
  ],
1281
+ noExtensions: true,
1282
+ noSkills: true,
1283
+ noPromptTemplates: true,
1151
1284
  noContextFiles: true
1152
1285
  }
1153
1286
  });
@@ -1182,7 +1315,7 @@ async function attachRunBundledPiFrontend(context, input) {
1182
1315
  timelineCursor: null,
1183
1316
  steered: input.steered === true,
1184
1317
  detached,
1185
- rendered: "native bundled Pi frontend with remote worker session runtime"
1318
+ rendered: "enriched bundled Pi frontend with remote worker session runtime"
1186
1319
  };
1187
1320
  }
1188
1321