@h-rig/cli 0.0.6-alpha.34 → 0.0.6-alpha.36

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,168 @@
1
+ // @bun
2
+ // packages/cli/src/commands/pi.ts
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
4
+ import { homedir } from "os";
5
+ import { dirname, resolve } from "path";
6
+
7
+ // packages/cli/src/runner.ts
8
+ import { EventBus } from "@rig/runtime/control-plane/runtime/events";
9
+ import { CliError } from "@rig/runtime/control-plane/errors";
10
+ import { evaluate, loadPolicy, resolveAction } from "@rig/runtime/control-plane/runtime/guard";
11
+ import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
12
+ import { CliError as CliError2 } from "@rig/runtime/control-plane/errors";
13
+ function requireNoExtraArgs(args, usage) {
14
+ if (args.length > 0) {
15
+ throw new CliError(`Unexpected arguments: ${args.join(" ")}
16
+ Usage: ${usage}`);
17
+ }
18
+ }
19
+
20
+ // packages/cli/src/commands/pi.ts
21
+ function settingsPath(root) {
22
+ return resolve(root, ".pi", "settings.json");
23
+ }
24
+ function userSettingsPath() {
25
+ return resolve(homedir(), ".pi", "agent", "settings.json");
26
+ }
27
+ function readJson(path, fallback) {
28
+ if (!existsSync(path))
29
+ return fallback;
30
+ try {
31
+ return JSON.parse(readFileSync(path, "utf-8"));
32
+ } catch {
33
+ return fallback;
34
+ }
35
+ }
36
+ function packageKey(entry) {
37
+ if (typeof entry === "string")
38
+ return entry;
39
+ if (entry && typeof entry === "object" && typeof entry.source === "string") {
40
+ return entry.source;
41
+ }
42
+ return JSON.stringify(entry);
43
+ }
44
+ function writeSettings(path, settings) {
45
+ mkdirSync(dirname(path), { recursive: true });
46
+ writeFileSync(path, `${JSON.stringify(settings, null, 2)}
47
+ `, "utf-8");
48
+ }
49
+ async function searchNpmForPiExtensions(term) {
50
+ const query = encodeURIComponent(term ? `${term} pi extension` : "pi extension");
51
+ const url = `https://registry.npmjs.org/-/v1/search?text=${query}&size=20`;
52
+ const response = await fetch(url);
53
+ if (!response.ok) {
54
+ throw new CliError2(`npm registry search failed (${response.status}).`, 2);
55
+ }
56
+ const payload = await response.json();
57
+ const results = [];
58
+ for (const entry of payload.objects ?? []) {
59
+ const pkg = entry.package;
60
+ if (!pkg?.name)
61
+ continue;
62
+ const keywords = (pkg.keywords ?? []).map((k) => k.toLowerCase());
63
+ const piLike = pkg.name.startsWith("pi-") || pkg.name.includes("-pi") || keywords.includes("pi") || keywords.includes("pi-extension") || (pkg.description ?? "").toLowerCase().includes("pi extension") || (pkg.description ?? "").toLowerCase().includes("pi coding agent");
64
+ if (!piLike)
65
+ continue;
66
+ results.push({ name: pkg.name, version: pkg.version ?? "", description: pkg.description ?? "" });
67
+ }
68
+ return results;
69
+ }
70
+ async function executePi(context, args) {
71
+ const [command = "list", ...rest] = args;
72
+ const projectSettingsPath = settingsPath(context.projectRoot);
73
+ const managedRecordPath = resolve(context.projectRoot, ".rig", "state", "pi-managed-packages.json");
74
+ switch (command) {
75
+ case "list": {
76
+ requireNoExtraArgs(rest, "rig pi list");
77
+ const project = readJson(projectSettingsPath, {});
78
+ const managed = new Set(readJson(managedRecordPath, []));
79
+ const user = readJson(userSettingsPath(), {});
80
+ const projectPackages = (Array.isArray(project.packages) ? project.packages : []).map((entry) => ({
81
+ source: packageKey(entry),
82
+ managedByRigConfig: managed.has(packageKey(entry))
83
+ }));
84
+ const userPackages = (Array.isArray(user.packages) ? user.packages : []).map(packageKey);
85
+ if (context.outputMode === "text") {
86
+ console.log("Project Pi packages (.pi/settings.json):");
87
+ if (projectPackages.length === 0)
88
+ console.log(" (none)");
89
+ for (const pkg of projectPackages) {
90
+ console.log(` ${pkg.source}${pkg.managedByRigConfig ? " [from rig.config runtime.pi.packages]" : ""}`);
91
+ }
92
+ console.log("User Pi packages (~/.pi/agent/settings.json):");
93
+ if (userPackages.length === 0)
94
+ console.log(" (none)");
95
+ for (const pkg of userPackages)
96
+ console.log(` ${pkg}`);
97
+ console.log("Add more: `rig pi add <npm-package>` \xB7 discover: `rig pi search <term>`");
98
+ }
99
+ return { ok: true, group: "pi", command, details: { projectPackages, userPackages } };
100
+ }
101
+ case "add": {
102
+ const [source, ...extra] = rest;
103
+ requireNoExtraArgs(extra, "rig pi add <package-source>");
104
+ if (!source) {
105
+ throw new CliError2("Usage: rig pi add <package-source> (npm name, name@version, or git URL)", 2);
106
+ }
107
+ const settings = readJson(projectSettingsPath, {});
108
+ const packages = Array.isArray(settings.packages) ? settings.packages : [];
109
+ if (packages.some((entry) => packageKey(entry) === source)) {
110
+ throw new CliError2(`"${source}" is already in ${projectSettingsPath}.`, 2);
111
+ }
112
+ writeSettings(projectSettingsPath, { ...settings, packages: [...packages, source] });
113
+ if (context.outputMode === "text") {
114
+ console.log(`Added ${source} to ${projectSettingsPath}.`);
115
+ console.log("Pi installs missing packages automatically at the next session start (local and worker).");
116
+ }
117
+ return { ok: true, group: "pi", command, details: { source, settingsPath: projectSettingsPath } };
118
+ }
119
+ case "remove": {
120
+ const [source, ...extra] = rest;
121
+ requireNoExtraArgs(extra, "rig pi remove <package-source>");
122
+ if (!source) {
123
+ throw new CliError2("Usage: rig pi remove <package-source>", 2);
124
+ }
125
+ const managed = new Set(readJson(managedRecordPath, []));
126
+ if (managed.has(source)) {
127
+ throw new CliError2(`"${source}" is managed by rig.config.ts (runtime.pi.packages); remove it there instead.`, 2);
128
+ }
129
+ const settings = readJson(projectSettingsPath, {});
130
+ const packages = Array.isArray(settings.packages) ? settings.packages : [];
131
+ const next = packages.filter((entry) => packageKey(entry) !== source);
132
+ if (next.length === packages.length) {
133
+ throw new CliError2(`"${source}" is not in ${projectSettingsPath}.`, 2);
134
+ }
135
+ const nextSettings = { ...settings };
136
+ if (next.length > 0)
137
+ nextSettings.packages = next;
138
+ else
139
+ delete nextSettings.packages;
140
+ writeSettings(projectSettingsPath, nextSettings);
141
+ if (context.outputMode === "text") {
142
+ console.log(`Removed ${source} from ${projectSettingsPath}.`);
143
+ }
144
+ return { ok: true, group: "pi", command, details: { source } };
145
+ }
146
+ case "search": {
147
+ const term = rest.join(" ").trim();
148
+ const results = await searchNpmForPiExtensions(term);
149
+ if (context.outputMode === "text") {
150
+ if (results.length === 0) {
151
+ console.log(`No Pi extension packages found on npm${term ? ` for "${term}"` : ""}.`);
152
+ } else {
153
+ console.log(`Pi extension packages on npm${term ? ` matching "${term}"` : ""}:`);
154
+ for (const pkg of results) {
155
+ console.log(` ${pkg.name}@${pkg.version} ${pkg.description.slice(0, 80)}`);
156
+ }
157
+ console.log("Install one: `rig pi add <name>`");
158
+ }
159
+ }
160
+ return { ok: true, group: "pi", command, details: { term, results } };
161
+ }
162
+ default:
163
+ throw new CliError2(`Unknown pi command: ${command}. Use list|add|remove|search.`);
164
+ }
165
+ }
166
+ export {
167
+ executePi
168
+ };
@@ -880,7 +880,38 @@ function parseWsPayload(message) {
880
880
  return JSON.parse(message.data);
881
881
  return JSON.parse(Buffer.from(message.data).toString("utf8"));
882
882
  }
883
- async function connectWorkerStream(options, ctx, state) {
883
+ var BRIDGE_LOCAL_COMMANDS = new Set(["detach", "quit", "q", "stop"]);
884
+ function registerDaemonCommandsNatively(pi, options, ctx, state, commands, registered) {
885
+ for (const command of commands) {
886
+ const record = recordOf(command);
887
+ const name = typeof record?.name === "string" ? record.name : "";
888
+ if (!name || registered.has(name) || BRIDGE_LOCAL_COMMANDS.has(name))
889
+ continue;
890
+ registered.add(name);
891
+ const description = typeof record?.description === "string" ? record.description : undefined;
892
+ const source = typeof record?.source === "string" ? record.source : "worker";
893
+ try {
894
+ pi.registerCommand(name, {
895
+ description: `[worker ${source}] ${description ?? ""}`.trim(),
896
+ handler: async (args) => {
897
+ const text = `/${name}${args ? ` ${args}` : ""}`;
898
+ appendTranscript(state, "You", text);
899
+ try {
900
+ const result = await runRunPiCommandViaServer(options.context, options.runId, text);
901
+ const message = typeof result.message === "string" ? result.message : "worker command accepted";
902
+ appendTranscript(state, "System", message);
903
+ if (state.nativeStream)
904
+ ctx.ui.notify(message, "info");
905
+ } catch (error) {
906
+ reportBridgeError(ctx, state, error instanceof Error ? error.message : String(error));
907
+ }
908
+ updatePiUi(ctx, state);
909
+ }
910
+ });
911
+ } catch {}
912
+ }
913
+ }
914
+ async function connectWorkerStream(options, pi, ctx, state, registeredDaemonCommands) {
884
915
  const ready = await waitForWorkerReady(options, ctx, state);
885
916
  if (!ready)
886
917
  return;
@@ -935,6 +966,7 @@ async function connectWorkerStream(options, ctx, state) {
935
966
  const record = recordOf(command);
936
967
  return typeof record?.name === "string" ? [`/${record.name}`] : [];
937
968
  });
969
+ registerDaemonCommandsNatively(pi, options, ctx, state, commands, registeredDaemonCommands);
938
970
  catchupDone = true;
939
971
  for (const payload of buffered.splice(0))
940
972
  applyEnvelope(ctx, state, payload);
@@ -1062,6 +1094,7 @@ function createRigWorkerPiBridgeExtension(options) {
1062
1094
  };
1063
1095
  if (options.initialMessageSent)
1064
1096
  appendTranscript(state, "System", "Initial message sent to worker Pi daemon.");
1097
+ const registeredDaemonCommands = new Set;
1065
1098
  let nativePiUiContextAvailable = false;
1066
1099
  pi.on("user_bash", (event) => {
1067
1100
  state.nativeStream = Boolean(state.nativeStream || nativePiUiContextAvailable);
@@ -1094,7 +1127,7 @@ function createRigWorkerPiBridgeExtension(options) {
1094
1127
  });
1095
1128
  return { consume: true };
1096
1129
  });
1097
- connectWorkerStream(options, ctx, state).catch((error) => {
1130
+ connectWorkerStream(options, pi, ctx, state, registeredDaemonCommands).catch((error) => {
1098
1131
  appendTranscript(state, "Error", error instanceof Error ? error.message : String(error));
1099
1132
  updatePiUi(ctx, state);
1100
1133
  });
@@ -1141,8 +1174,6 @@ async function attachRunBundledPiFrontend(context, input) {
1141
1174
  "--no-tools",
1142
1175
  "--no-builtin-tools",
1143
1176
  "--no-skills",
1144
- "--no-prompt-templates",
1145
- "--no-themes",
1146
1177
  "--no-context-files",
1147
1178
  "--no-approve"
1148
1179
  ], {
@@ -1810,6 +1841,33 @@ async function executeRun(context, args) {
1810
1841
  }
1811
1842
  return { ok: true, group: "run", command, details: restarted };
1812
1843
  }
1844
+ case "steer": {
1845
+ const runOption = takeOption(rest, "--run");
1846
+ const messageOption = takeOption(runOption.rest, "--message");
1847
+ const shortMessageOption = takeOption(messageOption.rest, "-m");
1848
+ const positionalRunId = shortMessageOption.rest.length > 0 ? shortMessageOption.rest[0] : undefined;
1849
+ const extra = positionalRunId ? shortMessageOption.rest.slice(1) : shortMessageOption.rest;
1850
+ requireNoExtraArgs(extra, "rig run steer [<run-id>|--run <id>] --message <text>");
1851
+ const runId = runOption.value ?? positionalRunId;
1852
+ const message = messageOption.value ?? shortMessageOption.value;
1853
+ if (!runId) {
1854
+ throw new CliError2("run steer requires a run id (positional or --run <id>).", 2);
1855
+ }
1856
+ if (!message?.trim()) {
1857
+ throw new CliError2("run steer requires --message <text>.", 2);
1858
+ }
1859
+ if (context.dryRun) {
1860
+ if (context.outputMode === "text") {
1861
+ console.log(`[dry-run] rig run steer ${runId} --message ${JSON.stringify(message)}`);
1862
+ }
1863
+ return { ok: true, group: "run", command, details: { runId, dryRun: true } };
1864
+ }
1865
+ await steerRunViaServer(context, runId, message.trim());
1866
+ if (context.outputMode === "text") {
1867
+ console.log(`Steering message queued for ${runId}.`);
1868
+ }
1869
+ return { ok: true, group: "run", command, details: { runId, queued: true } };
1870
+ }
1813
1871
  case "stop": {
1814
1872
  const runOption = takeOption(rest, "--run");
1815
1873
  const positionalRunId = runOption.rest.length > 0 ? runOption.rest[0] : undefined;
@@ -32,6 +32,9 @@ Usage: ${usage}`);
32
32
  }
33
33
  }
34
34
 
35
+ // packages/cli/src/commands/server.ts
36
+ import { resolveRigServerCommand } from "@rig/runtime/local-server";
37
+
35
38
  // packages/cli/src/commands/_authority-runs.ts
36
39
  import {
37
40
  readAuthorityRun,
@@ -483,7 +486,8 @@ async function executeServer(context, args, options) {
483
486
  const authTokenResult = takeOption(pending, "--auth-token");
484
487
  pending = authTokenResult.rest;
485
488
  requireNoExtraArgs(pending, "rig server start [--host <host>] [--port <n>] [--poll-ms <n>] [--auth-token <token>]");
486
- const commandParts = ["rig-server", "start"];
489
+ const serverCommand = resolveRigServerCommand(context.projectRoot);
490
+ const commandParts = [serverCommand.command, ...serverCommand.commandArgs, "start"];
487
491
  if (hostResult.value) {
488
492
  commandParts.push("--host", hostResult.value);
489
493
  }
@@ -506,7 +510,8 @@ async function executeServer(context, args, options) {
506
510
  const eventResult = takeOption(pending, "--event");
507
511
  pending = eventResult.rest;
508
512
  requireNoExtraArgs(pending, "rig server notify-test [--event <type>]");
509
- const commandParts = ["rig-server", "notify-test"];
513
+ const serverCommand = resolveRigServerCommand(context.projectRoot);
514
+ const commandParts = [serverCommand.command, ...serverCommand.commandArgs, "notify-test"];
510
515
  if (eventResult.value) {
511
516
  commandParts.push("--event", eventResult.value);
512
517
  }
@@ -1283,7 +1283,38 @@ function parseWsPayload(message2) {
1283
1283
  return JSON.parse(message2.data);
1284
1284
  return JSON.parse(Buffer.from(message2.data).toString("utf8"));
1285
1285
  }
1286
- async function connectWorkerStream(options, ctx, state) {
1286
+ var BRIDGE_LOCAL_COMMANDS = new Set(["detach", "quit", "q", "stop"]);
1287
+ function registerDaemonCommandsNatively(pi, options, ctx, state, commands, registered) {
1288
+ for (const command of commands) {
1289
+ const record = recordOf(command);
1290
+ const name = typeof record?.name === "string" ? record.name : "";
1291
+ if (!name || registered.has(name) || BRIDGE_LOCAL_COMMANDS.has(name))
1292
+ continue;
1293
+ registered.add(name);
1294
+ const description = typeof record?.description === "string" ? record.description : undefined;
1295
+ const source = typeof record?.source === "string" ? record.source : "worker";
1296
+ try {
1297
+ pi.registerCommand(name, {
1298
+ description: `[worker ${source}] ${description ?? ""}`.trim(),
1299
+ handler: async (args) => {
1300
+ const text = `/${name}${args ? ` ${args}` : ""}`;
1301
+ appendTranscript(state, "You", text);
1302
+ try {
1303
+ const result = await runRunPiCommandViaServer(options.context, options.runId, text);
1304
+ const message2 = typeof result.message === "string" ? result.message : "worker command accepted";
1305
+ appendTranscript(state, "System", message2);
1306
+ if (state.nativeStream)
1307
+ ctx.ui.notify(message2, "info");
1308
+ } catch (error) {
1309
+ reportBridgeError(ctx, state, error instanceof Error ? error.message : String(error));
1310
+ }
1311
+ updatePiUi(ctx, state);
1312
+ }
1313
+ });
1314
+ } catch {}
1315
+ }
1316
+ }
1317
+ async function connectWorkerStream(options, pi, ctx, state, registeredDaemonCommands) {
1287
1318
  const ready = await waitForWorkerReady(options, ctx, state);
1288
1319
  if (!ready)
1289
1320
  return;
@@ -1338,6 +1369,7 @@ async function connectWorkerStream(options, ctx, state) {
1338
1369
  const record = recordOf(command);
1339
1370
  return typeof record?.name === "string" ? [`/${record.name}`] : [];
1340
1371
  });
1372
+ registerDaemonCommandsNatively(pi, options, ctx, state, commands, registeredDaemonCommands);
1341
1373
  catchupDone = true;
1342
1374
  for (const payload of buffered.splice(0))
1343
1375
  applyEnvelope(ctx, state, payload);
@@ -1465,6 +1497,7 @@ function createRigWorkerPiBridgeExtension(options) {
1465
1497
  };
1466
1498
  if (options.initialMessageSent)
1467
1499
  appendTranscript(state, "System", "Initial message sent to worker Pi daemon.");
1500
+ const registeredDaemonCommands = new Set;
1468
1501
  let nativePiUiContextAvailable = false;
1469
1502
  pi.on("user_bash", (event) => {
1470
1503
  state.nativeStream = Boolean(state.nativeStream || nativePiUiContextAvailable);
@@ -1497,7 +1530,7 @@ function createRigWorkerPiBridgeExtension(options) {
1497
1530
  });
1498
1531
  return { consume: true };
1499
1532
  });
1500
- connectWorkerStream(options, ctx, state).catch((error) => {
1533
+ connectWorkerStream(options, pi, ctx, state, registeredDaemonCommands).catch((error) => {
1501
1534
  appendTranscript(state, "Error", error instanceof Error ? error.message : String(error));
1502
1535
  updatePiUi(ctx, state);
1503
1536
  });
@@ -1544,8 +1577,6 @@ async function attachRunBundledPiFrontend(context, input) {
1544
1577
  "--no-tools",
1545
1578
  "--no-builtin-tools",
1546
1579
  "--no-skills",
1547
- "--no-prompt-templates",
1548
- "--no-themes",
1549
1580
  "--no-context-files",
1550
1581
  "--no-approve"
1551
1582
  ], {
@@ -1951,6 +1982,7 @@ var PRIMARY_GROUPS = [
1951
1982
  { command: "show <id>|--run <id> [--raw]", description: "Show a human run summary; --raw prints the full payload.", primary: true },
1952
1983
  { command: "attach <run-id>|--run <id> [--follow]", description: "Attach to the run; --follow launches native bundled Pi for live Pi runs.", primary: true },
1953
1984
  { command: "stop [<run-id>|--run <id>]", description: "Request stop for one run or local active runs.", primary: true },
1985
+ { command: "steer <run-id> --message <text>", description: "Queue a steering message into a live worker without attaching." },
1954
1986
  { command: "timeline --run <id> [--follow]", description: "Stream raw run timeline events." },
1955
1987
  { command: "resume", description: "Resume the most recent interrupted local run." },
1956
1988
  { command: "restart", description: "Restart the most recent local run from a clean runtime." },
@@ -2052,6 +2084,19 @@ var ADVANCED_GROUPS = [
2052
2084
  { command: "hp-next <dev|check|e2e|reset>", description: "Drive the hp-next browser test harness." }
2053
2085
  ]
2054
2086
  },
2087
+ {
2088
+ name: "pi",
2089
+ summary: "Manage Pi extension packages for this project (community extensions from npm/git).",
2090
+ usage: ["rig pi <list|add|remove|search> [args]"],
2091
+ commands: [
2092
+ { command: "list", description: "Show project and user Pi extension packages." },
2093
+ { command: "add <source>", description: "Add an npm/git Pi extension to .pi/settings.json (auto-installs at next session)." },
2094
+ { command: "remove <source>", description: "Remove an operator-added Pi extension." },
2095
+ { command: "search [term]", description: "Discover Pi extension packages on the npm registry." }
2096
+ ],
2097
+ examples: ["rig pi search subagents", "rig pi add pi-subagents", "rig pi list"],
2098
+ next: ["Config-managed extensions: declare `runtime: { pi: { packages: [...] } }` in rig.config.ts \u2014 workers pick them up automatically."]
2099
+ },
2055
2100
  {
2056
2101
  name: "plugin",
2057
2102
  summary: "Plugin listing, validation, and plugin-contributed commands.",