@hyperdreamer/pi-webui 1.10.7 → 1.11.0-beta.10

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.
Files changed (92) hide show
  1. package/README.md +17 -2
  2. package/dist/cli.js +265 -32
  3. package/dist/cli.js.map +1 -1
  4. package/dist/client/assets/{CodeViewer-BIb2Wx8W.js → CodeViewer-CAf_j27u.js} +1 -1
  5. package/dist/client/assets/{UnifiedDiffViewer-DpWiGlQo.js → UnifiedDiffViewer-wBjEJWa1.js} +1 -1
  6. package/dist/client/assets/{index-DX583DEv.js → index-5EtDb9nj.js} +1197 -476
  7. package/dist/client/index.html +1 -1
  8. package/dist/config.js +52 -11
  9. package/dist/config.js.map +1 -1
  10. package/dist/pi-webui-plugins/workspace-memory/pi-webui-plugin.js +3 -2
  11. package/dist/pi-webui-plugins/workspace-tasks/pi-webui-plugin.js +1 -0
  12. package/dist/plugin-api.d.ts +30 -0
  13. package/dist/server/app.js +6 -1
  14. package/dist/server/app.js.map +1 -1
  15. package/dist/server/realtime/sessionEventHub.js +40 -5
  16. package/dist/server/realtime/sessionEventHub.js.map +1 -1
  17. package/dist/server/realtime/sessionStatusCoalescer.js +137 -0
  18. package/dist/server/realtime/sessionStatusCoalescer.js.map +1 -0
  19. package/dist/server/sessiond/sessionProxyRoutes.js +2 -0
  20. package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -1
  21. package/dist/server/sessiond.js +57 -5
  22. package/dist/server/sessiond.js.map +1 -1
  23. package/dist/server/sessions/modelPolicyCapability.js +88 -0
  24. package/dist/server/sessions/modelPolicyCapability.js.map +1 -0
  25. package/dist/server/sessions/modelPolicyTool.js +31 -0
  26. package/dist/server/sessions/modelPolicyTool.js.map +1 -0
  27. package/dist/server/sessions/modelTierRegistry.js +14 -0
  28. package/dist/server/sessions/modelTierRegistry.js.map +1 -1
  29. package/dist/server/sessions/modelTierSettingsRoutes.js +36 -0
  30. package/dist/server/sessions/modelTierSettingsRoutes.js.map +1 -0
  31. package/dist/server/sessions/modelTierSettingsService.js +100 -0
  32. package/dist/server/sessions/modelTierSettingsService.js.map +1 -0
  33. package/dist/server/sessions/piSessionService.js +1643 -433
  34. package/dist/server/sessions/piSessionService.js.map +1 -1
  35. package/dist/server/sessions/sessionDefaultsRoutes.js +39 -0
  36. package/dist/server/sessions/sessionDefaultsRoutes.js.map +1 -1
  37. package/dist/server/sessions/sessionDefaultsService.js +36 -6
  38. package/dist/server/sessions/sessionDefaultsService.js.map +1 -1
  39. package/dist/server/sessions/sessionMetadataStore.js +107 -13
  40. package/dist/server/sessions/sessionMetadataStore.js.map +1 -1
  41. package/dist/server/sessions/sessionModelPolicy.js +118 -0
  42. package/dist/server/sessions/sessionModelPolicy.js.map +1 -0
  43. package/dist/server/sessions/sessionReorder.js +117 -0
  44. package/dist/server/sessions/sessionReorder.js.map +1 -0
  45. package/dist/server/sessions/sessionRouteFastifyOptions.js +8 -0
  46. package/dist/server/sessions/sessionRouteFastifyOptions.js.map +1 -0
  47. package/dist/server/sessions/sessionRoutes.js +208 -3
  48. package/dist/server/sessions/sessionRoutes.js.map +1 -1
  49. package/dist/server/sessions/starterModelPolicyPreferenceStore.js +164 -0
  50. package/dist/server/sessions/starterModelPolicyPreferenceStore.js.map +1 -0
  51. package/dist/server/sessions/utilityModelExtension.js +199 -0
  52. package/dist/server/sessions/utilityModelExtension.js.map +1 -0
  53. package/dist/server/sessions/utilityModelResolver.js +77 -0
  54. package/dist/server/sessions/utilityModelResolver.js.map +1 -0
  55. package/dist/server/sessions/utilityModelSettingsRoutes.js +62 -0
  56. package/dist/server/sessions/utilityModelSettingsRoutes.js.map +1 -0
  57. package/dist/server/sessions/utilityModelSettingsService.js +101 -0
  58. package/dist/server/sessions/utilityModelSettingsService.js.map +1 -0
  59. package/dist/server/skills/optionalSkillInstall.js +69 -0
  60. package/dist/server/skills/optionalSkillInstall.js.map +1 -0
  61. package/dist/server/skills/optionalSkillInstaller.js +148 -0
  62. package/dist/server/skills/optionalSkillInstaller.js.map +1 -0
  63. package/dist/shared/apiTypes.d.ts +129 -3
  64. package/dist/shared/apiTypes.js +10 -0
  65. package/dist/shared/apiTypes.js.map +1 -1
  66. package/dist/shared/capabilities.js +15 -0
  67. package/dist/shared/capabilities.js.map +1 -1
  68. package/dist/shared/federatedRoutes.js +7 -0
  69. package/dist/shared/federatedRoutes.js.map +1 -1
  70. package/docs/assets/pi-webui-desktop.png +0 -0
  71. package/docs/config.md +61 -3
  72. package/docs/plugins.md +93 -16
  73. package/optional-skills/deterministic-subagent-driven-development/SKILL.md +224 -0
  74. package/optional-skills/deterministic-subagent-driven-development/pi-webui-skill.json +28 -0
  75. package/optional-skills/deterministic-subagent-driven-development/prompts/final-reviewer.md +132 -0
  76. package/optional-skills/deterministic-subagent-driven-development/prompts/implementer.md +101 -0
  77. package/optional-skills/deterministic-subagent-driven-development/prompts/re-reviewer.md +60 -0
  78. package/optional-skills/deterministic-subagent-driven-development/prompts/task-reviewer.md +80 -0
  79. package/optional-skills/deterministic-subagent-driven-development/references/capability-contract.md +174 -0
  80. package/optional-skills/deterministic-subagent-driven-development/references/plan-contract.md +268 -0
  81. package/optional-skills/deterministic-subagent-driven-development/references/state-machine.md +177 -0
  82. package/optional-skills/deterministic-subagent-driven-development/scripts/lib/manifest.mjs +258 -0
  83. package/optional-skills/deterministic-subagent-driven-development/scripts/lib/plan-policy.mjs +350 -0
  84. package/optional-skills/deterministic-subagent-driven-development/scripts/lib/prompt-renderer.mjs +290 -0
  85. package/optional-skills/deterministic-subagent-driven-development/scripts/lib/state-machine.mjs +1264 -0
  86. package/optional-skills/deterministic-subagent-driven-development/scripts/lib/state-store.mjs +532 -0
  87. package/optional-skills/deterministic-subagent-driven-development/scripts/sdd-state +3 -0
  88. package/optional-skills/deterministic-subagent-driven-development/scripts/sdd-state.mjs +349 -0
  89. package/optional-skills/deterministic-writing-plans/SKILL.md +232 -0
  90. package/optional-skills/deterministic-writing-plans/references/grammar.md +84 -0
  91. package/optional-skills/deterministic-writing-plans/templates/plan-skeleton.md +143 -0
  92. package/package.json +13 -9
package/README.md CHANGED
@@ -11,7 +11,7 @@ Run agents where your code, tools, credentials, and build caches live. Supervise
11
11
 
12
12
  ![PI WEBUI](docs/assets/pi-webui-banner.png)
13
13
 
14
- ![PI WEBUI desktop screenshot](docs/assets/pi-webui-desktop.png)
14
+ ![PI WEBUI desktop collage showing workspace projects, live session activity, system information, and memory](docs/assets/pi-webui-desktop.png)
15
15
 
16
16
  ## Why PI WEBUI?
17
17
 
@@ -36,7 +36,7 @@ Requirements:
36
36
 
37
37
  - Node.js 22.19.0 or newer
38
38
  - npm
39
- - Pi Coding Agent `>=0.82.1 <0.83`, configured for your user
39
+ - Pi Coding Agent `>=0.83.0 <0.84`, configured for your user
40
40
  - git and the development tools your agents need
41
41
 
42
42
  Install PI WEBUI from npm or clone from GitHub:
@@ -117,6 +117,21 @@ Project-local PI WEBUI config lives at:
117
117
 
118
118
  Common configuration includes host/port, path access, uploads, PI WEBUI plugin enablement, shortcuts, and session daemon options. In Settings, machine-affecting config targets the selected machine; gateway host/port/allowed-hosts, remote machine registration, tokens, and keyboard shortcuts stay local.
119
119
 
120
+ ## Extra skills
121
+
122
+ PI WEBUI ships two opt-in agent skills for deterministic plan authoring and
123
+ subagent-driven execution. Nothing installs or activates them by default:
124
+
125
+ ```bash
126
+ pi-webui install-extra --dry-run # show what would change
127
+ pi-webui install-extra # install, after confirming
128
+ ```
129
+
130
+ They install as `writing-plans` and `subagent-driven-development`, **replacing**
131
+ any existing skills with those names. The command explains the risk, asks for
132
+ confirmation, and backs up both skills and the skill lock file first. See
133
+ [docs/optional-skills.md](https://github.com/hyperdreamer/pi-webui/blob/main/docs/optional-skills.md).
134
+
120
135
  ## Development
121
136
 
122
137
  `start.sh` (a thin wrapper around `npm run dev`) starts the complete development stack. To run its processes separately:
package/dist/cli.js CHANGED
@@ -1,19 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawnSync } from "node:child_process";
3
- import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
3
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
4
4
  import { mkdir, rm, writeFile } from "node:fs/promises";
5
5
  import { homedir, userInfo } from "node:os";
6
6
  import { basename, dirname, join, resolve } from "node:path";
7
+ import { createInterface } from "node:readline/promises";
7
8
  import { fileURLToPath } from "node:url";
8
- import { DEFAULT_PORT, defaultPiWebUiConfigPath, defaultPiWebUiDataDir, effectivePiWebUiConfig, examplePiWebUiConfig } from "./config.js";
9
- import { packageVersion, printPiWebUiVersionReport } from "./piWebUiVersionReport.js";
10
- import { checkNodePtyDarwinSpawnHelper, formatNodePtyDarwinSpawnHelperCheck } from "./server/diagnostics/nodePtySpawnHelper.js";
11
- import { checkNodePtyNativeModule, formatNodePtyNativeModuleCheck } from "./server/diagnostics/nodePtyNativeModule.js";
9
+ import { DEFAULT_PORT, defaultPiWebUiConfigPath, defaultPiWebUiDataDir, effectivePiWebUiConfig, examplePiWebUiConfig, } from "./config.js";
10
+ import { packageVersion, printPiWebUiVersionReport, } from "./piWebUiVersionReport.js";
11
+ import { installOptionalSkills, } from "./server/skills/optionalSkillInstaller.js";
12
+ import { checkNodePtyDarwinSpawnHelper, formatNodePtyDarwinSpawnHelperCheck, } from "./server/diagnostics/nodePtySpawnHelper.js";
13
+ import { checkNodePtyNativeModule, formatNodePtyNativeModuleCheck, } from "./server/diagnostics/nodePtyNativeModule.js";
12
14
  import { installNativeServiceCandidate, nativeServiceInstallFailureNeedsPathAdvice, } from "./nativeServices/serviceInstall.js";
13
15
  import { minimumSupportedNodeVersion, nativeServiceManagerRefs, productionNativeServiceIds, } from "./nativeServices/servicePlan.js";
14
16
  import { formatNativeServiceDoctorResult, inferInstalledNativeServiceMode, inspectInstalledDevelopmentServiceInput, inspectInstalledProductionServiceContext, runNativeServiceDoctor, } from "./nativeServices/serviceDoctor.js";
15
17
  import { createNativeServiceAuthoritativeProbe, nativeServicePrerequisiteShellCheck, } from "./nativeServices/serviceProbe.js";
16
- import { renderLaunchdPlist, renderSystemdUnit } from "./nativeServices/serviceRendering.js";
18
+ import { renderLaunchdPlist, renderSystemdUnit, } from "./nativeServices/serviceRendering.js";
17
19
  const PI_WEBUI_PACKAGE_NAME = "@hyperdreamer/pi-webui";
18
20
  const systemdServiceDir = join(homedir(), ".config", "systemd", "user");
19
21
  const launchdServiceDir = join(homedir(), "Library", "LaunchAgents");
@@ -87,18 +89,31 @@ function capture(command, args) {
87
89
  const result = spawnSync(command, args, { encoding: "utf8" });
88
90
  const errorMessage = result.error instanceof Error ? result.error.message : "";
89
91
  const stderr = outputText(result.stderr);
90
- return { status: result.status ?? 1, stdout: outputText(result.stdout), stderr: stderr === "" ? errorMessage : stderr };
92
+ return {
93
+ status: result.status ?? 1,
94
+ stdout: outputText(result.stdout),
95
+ stderr: stderr === "" ? errorMessage : stderr,
96
+ };
91
97
  }
92
98
  function runQuiet(command, args) {
93
99
  return capture(command, args).status;
94
100
  }
95
101
  function hasCommand(command) {
96
- return capture("/usr/bin/env", ["sh", "-c", `command -v ${shellSingleQuote(command)}`]).status === 0;
102
+ return (capture("/usr/bin/env", [
103
+ "sh",
104
+ "-c",
105
+ `command -v ${shellSingleQuote(command)}`,
106
+ ]).status === 0);
97
107
  }
98
108
  function isLingerEnabled() {
99
109
  if (!hasCommand("loginctl"))
100
110
  return undefined;
101
- const result = capture("loginctl", ["show-user", userInfo().username, "-p", "Linger"]);
111
+ const result = capture("loginctl", [
112
+ "show-user",
113
+ userInfo().username,
114
+ "-p",
115
+ "Linger",
116
+ ]);
102
117
  if (result.status !== 0)
103
118
  return undefined;
104
119
  const value = result.stdout.trim();
@@ -109,7 +124,11 @@ function isLingerEnabled() {
109
124
  return undefined;
110
125
  }
111
126
  function parseInstallOptions(args) {
112
- const options = { host: "127.0.0.1", port: String(DEFAULT_PORT), mode: "production" };
127
+ const options = {
128
+ host: "127.0.0.1",
129
+ port: String(DEFAULT_PORT),
130
+ mode: "production",
131
+ };
113
132
  for (let i = 0; i < args.length; i += 1) {
114
133
  const arg = args[i];
115
134
  if (arg === undefined)
@@ -196,7 +215,9 @@ function serviceShellCommand(command, cwd) {
196
215
  return ["/usr/bin/env", detectServiceShell().executable, "-lc", fullCommand];
197
216
  }
198
217
  function serviceShellQuote(value) {
199
- return detectServiceShell().name === "fish" ? fishSingleQuote(value) : shellSingleQuote(value);
218
+ return detectServiceShell().name === "fish"
219
+ ? fishSingleQuote(value)
220
+ : shellSingleQuote(value);
200
221
  }
201
222
  function describeServiceShell() {
202
223
  const shell = detectServiceShell();
@@ -205,7 +226,9 @@ function describeServiceShell() {
205
226
  ? "could not detect a supported login shell; using bash"
206
227
  : `detected ${shell.detectedExecutable}; using bash because PI WEBUI currently supports bash, zsh, and fish`;
207
228
  }
208
- return shell.detectedExecutable === null ? shell.name : `${shell.name} (${shell.detectedExecutable})`;
229
+ return shell.detectedExecutable === null
230
+ ? shell.name
231
+ : `${shell.name} (${shell.detectedExecutable})`;
209
232
  }
210
233
  function configEnvironment(options, configPath) {
211
234
  return options.config === undefined ? {} : { PI_WEBUI_CONFIG: configPath };
@@ -252,7 +275,9 @@ function launchdLogPath(ref) {
252
275
  return join(logDir, ref.logName);
253
276
  }
254
277
  function installConfigPath(options) {
255
- return options.config === undefined ? defaultPiWebUiConfigPath() : resolve(options.config);
278
+ return options.config === undefined
279
+ ? defaultPiWebUiConfigPath()
280
+ : resolve(options.config);
256
281
  }
257
282
  async function writeInitialConfig(options, configPath) {
258
283
  await mkdir(dirname(configPath), { recursive: true });
@@ -267,13 +292,17 @@ function launchdPlistPath(ref) {
267
292
  return join(launchdServiceDir, ref.launchdPlistName);
268
293
  }
269
294
  function serviceFilePath(backend, ref) {
270
- return backend.kind === "systemd" ? systemdServicePath(ref) : launchdPlistPath(ref);
295
+ return backend.kind === "systemd"
296
+ ? systemdServicePath(ref)
297
+ : launchdPlistPath(ref);
271
298
  }
272
299
  function serviceFileExists(backend, ref) {
273
300
  return existsSync(serviceFilePath(backend, ref));
274
301
  }
275
302
  function installedServiceIds(backend) {
276
- return new Set(allServiceRefs().filter((ref) => serviceFileExists(backend, ref)).map((ref) => ref.id));
303
+ return new Set(allServiceRefs()
304
+ .filter((ref) => serviceFileExists(backend, ref))
305
+ .map((ref) => ref.id));
277
306
  }
278
307
  function installedServiceRefs(backend) {
279
308
  const installed = startOrder(allServiceRefs().filter((ref) => serviceFileExists(backend, ref)));
@@ -302,13 +331,15 @@ function launchdServiceTarget(ref) {
302
331
  return `${launchdDomain()}/${ref.launchdLabel}`;
303
332
  }
304
333
  function launchdIsLoaded(ref) {
305
- return capture("launchctl", ["print", launchdServiceTarget(ref)]).status === 0;
334
+ return (capture("launchctl", ["print", launchdServiceTarget(ref)]).status === 0);
306
335
  }
307
336
  function launchdBootout(ref) {
308
337
  runQuiet("launchctl", ["bootout", launchdServiceTarget(ref)]);
309
338
  }
310
339
  function launchdBootstrap(ref) {
311
- run("launchctl", ["bootstrap", launchdDomain(), launchdPlistPath(ref)], { check: true });
340
+ run("launchctl", ["bootstrap", launchdDomain(), launchdPlistPath(ref)], {
341
+ check: true,
342
+ });
312
343
  run("launchctl", ["enable", launchdServiceTarget(ref)], { check: true });
313
344
  }
314
345
  function launchdStart(ref) {
@@ -406,14 +437,24 @@ function makeServiceRuntimeStatus(ref, health, detail, target, filePath, pid) {
406
437
  }
407
438
  function firstOutputLine(...values) {
408
439
  for (const value of values) {
409
- const line = value.trim().split("\n").find((candidate) => candidate.trim() !== "");
440
+ const line = value
441
+ .trim()
442
+ .split("\n")
443
+ .find((candidate) => candidate.trim() !== "");
410
444
  if (line !== undefined)
411
445
  return line.trim();
412
446
  }
413
447
  return undefined;
414
448
  }
415
449
  function systemdMainPid(ref) {
416
- const result = capture("systemctl", ["--user", "--no-pager", "show", ref.systemdName, "--property=MainPID", "--value"]);
450
+ const result = capture("systemctl", [
451
+ "--user",
452
+ "--no-pager",
453
+ "show",
454
+ ref.systemdName,
455
+ "--property=MainPID",
456
+ "--value",
457
+ ]);
417
458
  if (result.status !== 0)
418
459
  return undefined;
419
460
  const value = result.stdout.trim();
@@ -424,7 +465,12 @@ function systemdRuntimeStatus(backend, ref) {
424
465
  const filePath = serviceFilePath(backend, ref);
425
466
  if (!serviceFileExists(backend, ref))
426
467
  return makeServiceRuntimeStatus(ref, "not-installed", "not installed", target, filePath);
427
- const result = capture("systemctl", ["--user", "--no-pager", "is-active", target]);
468
+ const result = capture("systemctl", [
469
+ "--user",
470
+ "--no-pager",
471
+ "is-active",
472
+ target,
473
+ ]);
428
474
  const state = firstOutputLine(result.stdout, result.stderr) ?? "unknown";
429
475
  if (result.status === 0 && state === "active")
430
476
  return makeServiceRuntimeStatus(ref, "running", "running", target, filePath, systemdMainPid(ref));
@@ -440,7 +486,9 @@ export function launchdRuntimeDetails(output) {
440
486
  const lastExitCode = parseLaunchdField(output, "last exit code");
441
487
  const detail = state === "running"
442
488
  ? "running"
443
- : lastExitCode === undefined ? state : `${state} (last exit code ${lastExitCode})`;
489
+ : lastExitCode === undefined
490
+ ? state
491
+ : `${state} (last exit code ${lastExitCode})`;
444
492
  return { state, detail, pid };
445
493
  }
446
494
  function launchdRuntimeStatus(backend, ref) {
@@ -453,11 +501,17 @@ function launchdRuntimeStatus(backend, ref) {
453
501
  return makeServiceRuntimeStatus(ref, "stopped", firstOutputLine(result.stderr, result.stdout) ?? "not loaded", target, filePath);
454
502
  }
455
503
  const details = launchdRuntimeDetails(result.stdout);
456
- const health = details.state === "running" ? "running" : details.state === "unknown" ? "unknown" : "stopped";
504
+ const health = details.state === "running"
505
+ ? "running"
506
+ : details.state === "unknown"
507
+ ? "unknown"
508
+ : "stopped";
457
509
  return makeServiceRuntimeStatus(ref, health, details.detail, target, filePath, details.pid);
458
510
  }
459
511
  function runtimeStatus(backend, ref) {
460
- return backend.kind === "systemd" ? systemdRuntimeStatus(backend, ref) : launchdRuntimeStatus(backend, ref);
512
+ return backend.kind === "systemd"
513
+ ? systemdRuntimeStatus(backend, ref)
514
+ : launchdRuntimeStatus(backend, ref);
461
515
  }
462
516
  function printServiceStatus(status) {
463
517
  const icon = status.health === "running" ? "✓" : "✗";
@@ -611,7 +665,11 @@ async function uninstall() {
611
665
  console.log(`PI WEBUI ${backend.label} removed. Production and development service files were removed; config and data were left in place.`);
612
666
  }
613
667
  function systemdServiceAction(action, refs) {
614
- const orderedRefs = action === "stop" ? stopOrder(refs) : action === "restart" ? restartOrder(refs) : startOrder(refs);
668
+ const orderedRefs = action === "stop"
669
+ ? stopOrder(refs)
670
+ : action === "restart"
671
+ ? restartOrder(refs)
672
+ : startOrder(refs);
615
673
  run("systemctl", ["--user", action, ...orderedRefs.map((ref) => ref.systemdName)], { check: true });
616
674
  }
617
675
  function launchdServiceAction(action, refs) {
@@ -649,7 +707,11 @@ function logs() {
649
707
  const backend = requireServiceBackend("pi-webui logs");
650
708
  const refs = installedServiceRefs(backend);
651
709
  if (backend.kind === "systemd") {
652
- run("journalctl", ["--user", ...refs.flatMap((ref) => ["-u", ref.systemdName]), "-f"]);
710
+ run("journalctl", [
711
+ "--user",
712
+ ...refs.flatMap((ref) => ["-u", ref.systemdName]),
713
+ "-f",
714
+ ]);
653
715
  return;
654
716
  }
655
717
  run("tail", ["-F", ...refs.map((ref) => launchdLogPath(ref))]);
@@ -684,9 +746,18 @@ function generalDoctorChecks() {
684
746
  const shell = serviceShellLabel();
685
747
  const agentCommand = agentCommandForChecks();
686
748
  return [
687
- [`Caller login ${shell} can find node >= ${minimumSupportedNodeVersion}`, serviceShellCommand(nodeVersionCheck())],
688
- [`Caller login ${shell} can find npm`, serviceShellCommand(commandWithVersionCheck("npm"))],
689
- [`Caller login ${shell} can find ${agentCommand}`, serviceShellCommand(commandWithVersionCheck(agentCommand))],
749
+ [
750
+ `Caller login ${shell} can find node >= ${minimumSupportedNodeVersion}`,
751
+ serviceShellCommand(nodeVersionCheck()),
752
+ ],
753
+ [
754
+ `Caller login ${shell} can find npm`,
755
+ serviceShellCommand(commandWithVersionCheck("npm")),
756
+ ],
757
+ [
758
+ `Caller login ${shell} can find ${agentCommand}`,
759
+ serviceShellCommand(commandWithVersionCheck(agentCommand)),
760
+ ],
690
761
  ];
691
762
  }
692
763
  function runChecks(checks) {
@@ -715,7 +786,12 @@ function printCheckOutput(output) {
715
786
  }
716
787
  function optionalDoctorChecks() {
717
788
  const shell = serviceShellLabel();
718
- return [[`Caller login ${shell} can find optional ripgrep (rg)`, serviceShellCommand(commandCheck("rg"))]];
789
+ return [
790
+ [
791
+ `Caller login ${shell} can find optional ripgrep (rg)`,
792
+ serviceShellCommand(commandCheck("rg")),
793
+ ],
794
+ ];
719
795
  }
720
796
  function printOptionalDoctorChecks() {
721
797
  let missingOptionalTool = false;
@@ -859,7 +935,9 @@ async function doctor() {
859
935
  printOptionalDoctorChecks();
860
936
  console.log("\nNative terminal runtime readiness:");
861
937
  const nodePtyNativeModuleOk = printNodePtyNativeModuleCheck();
862
- const nodePtySpawnHelperOk = nodePtyNativeModuleOk ? printNodePtyDarwinSpawnHelperCheck() : true;
938
+ const nodePtySpawnHelperOk = nodePtyNativeModuleOk
939
+ ? printNodePtyDarwinSpawnHelperCheck()
940
+ : true;
863
941
  let nativeServiceReport = null;
864
942
  if (backend !== undefined) {
865
943
  console.log("\nNative service plan checks (service-manager context):");
@@ -889,7 +967,8 @@ async function doctor() {
889
967
  const pathFailure = !generalReadinessOk || nativeServiceReport?.pathAdviceRecommended === true;
890
968
  if (pathFailure) {
891
969
  console.log("\nIf a command works in your terminal but fails in the service-manager check, compare the caller and manager contexts above.");
892
- const adviceShell = nativeServiceReport?.pathAdviceRecommended === true && nativeServiceReport.adviceShell !== null
970
+ const adviceShell = nativeServiceReport?.pathAdviceRecommended === true &&
971
+ nativeServiceReport.adviceShell !== null
893
972
  ? nativeServiceReport.adviceShell
894
973
  : detectServiceShell();
895
974
  printPathSetupAdvice(adviceShell);
@@ -915,12 +994,161 @@ function printNodePtyDarwinSpawnHelperCheck() {
915
994
  function isRecord(value) {
916
995
  return typeof value === "object" && value !== null && !Array.isArray(value);
917
996
  }
997
+ function optionalSkillsSourceRoot() {
998
+ return join(packageRootPath(), "optional-skills");
999
+ }
1000
+ function globalSkillsRootPath() {
1001
+ return join(homedir(), ".pi", "agent", "skills");
1002
+ }
1003
+ function skillLockFilePath() {
1004
+ const xdgStateHome = process.env["XDG_STATE_HOME"];
1005
+ return xdgStateHome !== undefined && xdgStateHome !== ""
1006
+ ? join(xdgStateHome, "skills", ".skill-lock.json")
1007
+ : join(homedir(), ".agents", ".skill-lock.json");
1008
+ }
1009
+ function createInstallerIo() {
1010
+ return {
1011
+ exists: (path) => existsSync(path),
1012
+ listFiles: (dir) => listFilesRecursively(dir),
1013
+ readFile: (path) => readFileSync(path, "utf8"),
1014
+ writeFile: (path, content) => {
1015
+ writeFileSync(path, content);
1016
+ },
1017
+ copyFile: (from, to) => {
1018
+ mkdirSync(dirname(to), { recursive: true });
1019
+ copyFileSync(from, to);
1020
+ },
1021
+ makeDir: (path) => {
1022
+ mkdirSync(path, { recursive: true });
1023
+ },
1024
+ movePath: (from, to) => {
1025
+ mkdirSync(dirname(to), { recursive: true });
1026
+ renameSync(from, to);
1027
+ },
1028
+ removePath: (path) => {
1029
+ rmSync(path, { recursive: true, force: true });
1030
+ },
1031
+ regenerateManifest: (skillDir) => {
1032
+ const manifest = join(skillDir, "pi-webui-skill.json");
1033
+ if (!existsSync(manifest))
1034
+ return;
1035
+ const result = spawnSync(process.execPath, [
1036
+ join(skillDir, "scripts", "sdd-state.mjs"),
1037
+ "manifest-create",
1038
+ "--source-root",
1039
+ skillDir,
1040
+ "--package-json",
1041
+ join(packageRootPath(), "package.json"),
1042
+ "--output",
1043
+ manifest,
1044
+ ], { encoding: "utf8", stdio: "pipe" });
1045
+ if (result.status !== 0) {
1046
+ throw new Error(`failed to regenerate the runtime manifest for ${skillDir}: ${result.stderr.trim()}`);
1047
+ }
1048
+ },
1049
+ };
1050
+ }
1051
+ function listFilesRecursively(dir, prefix = "") {
1052
+ const found = [];
1053
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
1054
+ const relative = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
1055
+ if (entry.isDirectory())
1056
+ found.push(...listFilesRecursively(join(dir, entry.name), relative));
1057
+ else
1058
+ found.push(relative);
1059
+ }
1060
+ return found;
1061
+ }
1062
+ async function confirmOptionalSkillInstall() {
1063
+ if (!process.stdin.isTTY) {
1064
+ throw new Error("pi-webui install-extra needs an interactive terminal to confirm. Re-run it directly, or pass --yes to skip the prompt.");
1065
+ }
1066
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1067
+ try {
1068
+ const answer = await rl.question("Proceed? [y/N] ");
1069
+ return /^y(es)?$/iu.test(answer.trim());
1070
+ }
1071
+ finally {
1072
+ rl.close();
1073
+ }
1074
+ }
1075
+ function printOptionalSkillRisk(skillsRoot, lockPath) {
1076
+ console.log(`PI WEBUI extra skills install
1077
+
1078
+ This installs two opt-in agent skills into:
1079
+ ${skillsRoot}
1080
+
1081
+ They install under the names 'writing-plans' and
1082
+ 'subagent-driven-development', because other skills such as
1083
+ 'brainstorming' route to those names. Any existing skill with either name
1084
+ WILL BE REPLACED.
1085
+
1086
+ What this changes:
1087
+ - replaces ${join(skillsRoot, "writing-plans")}
1088
+ - replaces ${join(skillsRoot, "subagent-driven-development")}
1089
+ - removes those two entries from ${lockPath}
1090
+
1091
+ That directory is not version controlled, so a timestamped backup of both
1092
+ skills and the lock file is taken first, and its path is printed at the end.
1093
+ Helper scripts the controller inherits are carried over from the replaced
1094
+ skill.
1095
+ `);
1096
+ }
1097
+ async function installExtra(args) {
1098
+ const dryRun = args.includes("--dry-run");
1099
+ const assumeYes = args.includes("--yes") || args.includes("-y");
1100
+ for (const arg of args) {
1101
+ if (arg !== "--dry-run" && arg !== "--yes" && arg !== "-y") {
1102
+ throw new Error(`Unknown install-extra option: ${arg}`);
1103
+ }
1104
+ }
1105
+ const sourceRoot = optionalSkillsSourceRoot();
1106
+ if (!existsSync(sourceRoot)) {
1107
+ throw new Error(`This build does not ship optional skills (${sourceRoot} is missing). Install from a repository checkout instead.`);
1108
+ }
1109
+ const skillsRoot = globalSkillsRootPath();
1110
+ const lockPath = skillLockFilePath();
1111
+ printOptionalSkillRisk(skillsRoot, lockPath);
1112
+ if (dryRun)
1113
+ console.log("Dry run: nothing will be written.\n");
1114
+ else if (!assumeYes && !(await confirmOptionalSkillInstall())) {
1115
+ console.log("Aborted. Nothing was changed.");
1116
+ return;
1117
+ }
1118
+ const backupRoot = join(homedir(), ".pi", "agent", `skills-backup-${timestampSlug()}`);
1119
+ const report = installOptionalSkills(createInstallerIo(), {
1120
+ sourceRoot,
1121
+ skillsRoot,
1122
+ backupRoot,
1123
+ lockPath,
1124
+ dryRun,
1125
+ });
1126
+ console.log(dryRun ? "\nWould install:" : "\nInstalled:");
1127
+ for (const name of report.installed)
1128
+ console.log(` ${name}`);
1129
+ if (report.lockEntriesRemoved.length > 0) {
1130
+ console.log(dryRun ? "Would remove lock entries:" : "Removed lock entries:");
1131
+ for (const name of report.lockEntriesRemoved)
1132
+ console.log(` ${name}`);
1133
+ }
1134
+ if (dryRun)
1135
+ return;
1136
+ console.log(`\nBackup: ${report.backupRoot}`);
1137
+ console.log(`Restore with:\n rm -rf ${report.installed
1138
+ .map((n) => join(skillsRoot, n))
1139
+ .join(" ")}\n cp -a ${report.backupRoot}/* ${skillsRoot}/`);
1140
+ console.log("\nType /reload in each idle PI WEBUI session to pick up the new skills.");
1141
+ }
1142
+ function timestampSlug() {
1143
+ return new Date().toISOString().replaceAll(/[:.]/gu, "-").slice(0, 19);
1144
+ }
918
1145
  function help() {
919
1146
  console.log(`PI WEBUI
920
1147
 
921
1148
  Usage:
922
1149
  pi-webui install [--dev] [--host 127.0.0.1] [--port ${String(DEFAULT_PORT)}] [--config ~/.config/pi-webui/config.json]
923
1150
  pi-webui uninstall
1151
+ pi-webui install-extra [--dry-run] [--yes]
924
1152
  pi-webui start|stop|restart|status|logs
925
1153
  pi-webui doctor
926
1154
  pi-webui version
@@ -939,8 +1167,13 @@ async function main() {
939
1167
  await install(args);
940
1168
  else if (command === "uninstall")
941
1169
  await uninstall();
942
- else if (command === "start" || command === "stop" || command === "restart" || command === "status")
1170
+ else if (command === "start" ||
1171
+ command === "stop" ||
1172
+ command === "restart" ||
1173
+ command === "status")
943
1174
  serviceAction(command);
1175
+ else if (command === "install-extra")
1176
+ await installExtra(args);
944
1177
  else if (command === "logs")
945
1178
  logs();
946
1179
  else if (command === "doctor")