@piagent/platform 1.44.4 → 1.46.0

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 (89) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +5 -3
  3. package/README.vi.md +5 -3
  4. package/docs/command-reference-vietnamese.md +2 -2
  5. package/docs/distribution-standard.md +6 -6
  6. package/docs/languages.json +2 -2
  7. package/docs/operator-manual-vietnamese.md +7 -7
  8. package/docs/publishing-for-teams.md +2 -0
  9. package/docs/release-install-policy.md +2 -2
  10. package/docs/team-onboarding.md +2 -2
  11. package/docs-site/architecture.html +1 -1
  12. package/docs-site/assets/search-en.json +1 -1
  13. package/docs-site/assets/search-vi.json +1 -1
  14. package/docs-site/benchmark.html +1 -1
  15. package/docs-site/commands.html +1 -1
  16. package/docs-site/context-engine.html +1 -1
  17. package/docs-site/ecosystem.html +1 -1
  18. package/docs-site/en/architecture.html +1 -1
  19. package/docs-site/en/benchmark.html +1 -1
  20. package/docs-site/en/commands.html +1 -1
  21. package/docs-site/en/context-engine.html +1 -1
  22. package/docs-site/en/ecosystem.html +1 -1
  23. package/docs-site/en/index.html +15 -15
  24. package/docs-site/en/mcp-approval.html +1 -1
  25. package/docs-site/en/mcp-auth.html +1 -1
  26. package/docs-site/en/mcp-servers.html +1 -1
  27. package/docs-site/en/mcp.html +1 -1
  28. package/docs-site/en/permissions.html +1 -1
  29. package/docs-site/en/profiles.html +1 -1
  30. package/docs-site/en/quickstart.html +1 -1
  31. package/docs-site/en/runtime.html +1 -1
  32. package/docs-site/en/scope.html +1 -1
  33. package/docs-site/en/security.html +1 -1
  34. package/docs-site/en/team.html +1 -1
  35. package/docs-site/en/update.html +3 -3
  36. package/docs-site/en/whats-new.html +58 -35
  37. package/docs-site/en/windows.html +1 -1
  38. package/docs-site/en/workflows.html +1 -1
  39. package/docs-site/index.html +15 -15
  40. package/docs-site/mcp-approval.html +1 -1
  41. package/docs-site/mcp-auth.html +1 -1
  42. package/docs-site/mcp-servers.html +1 -1
  43. package/docs-site/mcp.html +1 -1
  44. package/docs-site/permissions.html +1 -1
  45. package/docs-site/profiles.html +1 -1
  46. package/docs-site/quickstart.html +1 -1
  47. package/docs-site/releases.json +75 -0
  48. package/docs-site/runtime.html +1 -1
  49. package/docs-site/scope.html +1 -1
  50. package/docs-site/security.html +1 -1
  51. package/docs-site/team.html +1 -1
  52. package/docs-site/update.html +3 -3
  53. package/docs-site/whats-new.html +58 -35
  54. package/docs-site/windows.html +1 -1
  55. package/docs-site/workflows.html +1 -1
  56. package/evals/private-holdout-v1/public-exposure.v2.json +2 -2
  57. package/package.json +1 -1
  58. package/packages/piagent-core/benchmark/benchmark-bootstrap.js +3 -1
  59. package/packages/piagent-core/extensions/agent-home.d.ts +4 -0
  60. package/packages/piagent-core/extensions/agent-home.js +52 -0
  61. package/packages/piagent-core/extensions/piagent-guard.ts +5 -1
  62. package/packages/piagent-core/extensions/working-tree-snapshot-worker.mjs +9 -0
  63. package/packages/piagent-core/extensions/workspace-evidence-roots.js +28 -2
  64. package/packages/piagent-core/mcp/mcp-config-layers.js +4 -1
  65. package/packages/piagent-core/package.json +1 -1
  66. package/packages/piagent-core/runtime/hooks/input-hook.ts +17 -3
  67. package/packages/piagent-core/runtime/input/chat-images.ts +66 -10
  68. package/packages/piagent-core/runtime/input/file-arguments.ts +102 -0
  69. package/packages/piagent-core/runtime/inspection/webui-snapshot.ts +2 -6
  70. package/packages/piagent-core/runtime/inspection/working-tree-off-thread.ts +52 -0
  71. package/packages/piagent-core/runtime/solver/runtime-features.ts +15 -3
  72. package/packages/piagent-webui/extension/piagent-webui.ts +2 -0
  73. package/packages/piagent-webui/gateway/gateway-service.ts +4 -1
  74. package/packages/piagent-webui/gateway/pi-host.ts +2 -2
  75. package/packages/piagent-webui/ownership/profile-state.ts +10 -1
  76. package/packages/piagent-webui/server/benchmark-release-monitor.ts +2 -1
  77. package/scripts/benchmark-runner-support.mjs +2 -1
  78. package/scripts/dashboard-update-job.mjs +4 -2
  79. package/scripts/install-global.sh +113 -5
  80. package/scripts/model-scope-prune.mjs +2 -1
  81. package/scripts/personal-pi-config.mjs +2 -1
  82. package/scripts/personal-pi-restore.mjs +184 -0
  83. package/scripts/pi-usage-history.mjs +2 -1
  84. package/scripts/piagent-cli.mjs +49 -9
  85. package/scripts/piagent-dashboard.mjs +5 -3
  86. package/scripts/setup.sh +2 -6
  87. package/scripts/team-doctor.sh +11 -5
  88. package/scripts/uninstall-global.sh +17 -2
  89. package/scripts/update-global.mjs +5 -2
@@ -23,6 +23,9 @@ Options:
23
23
  --with-web-access | --no-web-access
24
24
  Install pi-web-access for the researcher subagent (default: install).
25
25
  --with-herdr Also configure Herdr integration.
26
+ --copy-login | --no-copy-login Copy (or not) your personal pi login into Piagent's home.
27
+ Default: ask in a terminal; without one, copy only when
28
+ moving an older install out of your personal pi.
26
29
  --model-scope <full|codex|claude>
27
30
  Model families to enable in Pi (default: full). A member who
28
31
  already has a default model and model list keeps them; an
@@ -36,7 +39,13 @@ Options:
36
39
  -h, --help Print this help and exit.
37
40
 
38
41
  Purpose:
39
- Install the piagent Pi package into the current user's global Pi settings.
42
+ Install Piagent into its own Pi home (~/.piagent/agent; PIAGENT_AGENT_DIR overrides).
43
+ Your personal `pi` (~/.pi/agent) stays stock: open Piagent with `piagent`.
44
+ An install from an older release that lives in ~/.pi/agent is moved out of it.
45
+
46
+ Run in a terminal, a first install asks which add-ons to install and whether
47
+ to use your pi login; flags answer those questions up front, and a run
48
+ without a terminal (CI, the dashboard updater) takes the defaults.
40
49
 
41
50
  Package source examples:
42
51
  # Recommended stable install: exact current release tag
@@ -61,7 +70,8 @@ Package source examples:
61
70
  Notes:
62
71
  - `currentRelease` in output is the version of the terminal helper executing this command.
63
72
  - Use exactly one package selector. The first CLI selector overrides environment defaults; a second selector fails closed.
64
- - OAuth is intentionally not automated. Run `pi` then `/login`.
73
+ - OAuth is intentionally not automated. Run `piagent` then `/login`, or let the
74
+ installer copy the login your personal pi already has.
65
75
  - Model scope is configured with Pi's native `enabledModels` so users choose via `/model`, Ctrl+L, `/scoped-models`, and Ctrl+P.
66
76
  - Herdr integration is optional and modifies user-level Herdr/Pi config.
67
77
  - MCP is installed by default with the core preset: Context7 docs, Chrome DevTools, GitHub.
@@ -76,6 +86,21 @@ USAGE
76
86
  }
77
87
 
78
88
  PLATFORM_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
89
+ # Piagent's own Pi home (packages/piagent-core/extensions/agent-home.js has the
90
+ # same rule). Every `pi` call below works there, never in the personal home.
91
+ PERSONAL_PI_DIR="${HOME}/.pi/agent"
92
+ if [[ -n "${PIAGENT_AGENT_DIR:-}" ]]; then
93
+ PIAGENT_HOME="$PIAGENT_AGENT_DIR"
94
+ elif [[ -n "${PI_CODING_AGENT_DIR:-}" && "${PI_CODING_AGENT_DIR%/}" != "$PERSONAL_PI_DIR" ]]; then
95
+ PIAGENT_HOME="$PI_CODING_AGENT_DIR"
96
+ else
97
+ PIAGENT_HOME="${HOME}/.piagent/agent"
98
+ fi
99
+ export PI_CODING_AGENT_DIR="$PIAGENT_HOME"
100
+ MCP_EXPLICIT=false
101
+ WEB_ACCESS_EXPLICIT=false
102
+ MODEL_SCOPE_EXPLICIT=false
103
+ COPY_LOGIN=""
79
104
  CURRENT_RELEASE_TAG=""
80
105
  EXPECTED_PI_VERSION=""
81
106
  EXPECTED_RELEASE_COMMIT="${PIAGENT_EXPECTED_RELEASE_COMMIT:-}"
@@ -486,10 +511,12 @@ while [[ $# -gt 0 ]]; do
486
511
  ;;
487
512
  --with-mcp)
488
513
  WITH_MCP=true
514
+ MCP_EXPLICIT=true
489
515
  shift
490
516
  ;;
491
517
  --no-mcp)
492
518
  WITH_MCP=false
519
+ MCP_EXPLICIT=true
493
520
  shift
494
521
  ;;
495
522
  --mcp-preset)
@@ -520,10 +547,12 @@ while [[ $# -gt 0 ]]; do
520
547
  ;;
521
548
  --with-web-access)
522
549
  WITH_WEB_ACCESS=true
550
+ WEB_ACCESS_EXPLICIT=true
523
551
  shift
524
552
  ;;
525
553
  --no-web-access)
526
554
  WITH_WEB_ACCESS=false
555
+ WEB_ACCESS_EXPLICIT=true
527
556
  shift
528
557
  ;;
529
558
  --with-herdr)
@@ -533,6 +562,7 @@ while [[ $# -gt 0 ]]; do
533
562
  --model-scope)
534
563
  require_value "$1" "${2:-}"
535
564
  MODEL_SCOPE_PRESET="$2"
565
+ MODEL_SCOPE_EXPLICIT=true
536
566
  shift 2
537
567
  ;;
538
568
  --default-model)
@@ -546,6 +576,15 @@ while [[ $# -gt 0 ]]; do
546
576
  ;;
547
577
  --no-model-scope)
548
578
  CONFIGURE_MODEL_SCOPE=false
579
+ MODEL_SCOPE_EXPLICIT=true
580
+ shift
581
+ ;;
582
+ --copy-login)
583
+ COPY_LOGIN=true
584
+ shift
585
+ ;;
586
+ --no-copy-login)
587
+ COPY_LOGIN=false
549
588
  shift
550
589
  ;;
551
590
  -h|--help)
@@ -569,6 +608,54 @@ fi
569
608
 
570
609
  require_node_version
571
610
 
611
+ # What personal Pi holds from an older Piagent release (packages, login).
612
+ LEGACY_REPORT="$(node "$PLATFORM_ROOT/scripts/personal-pi-restore.mjs" --json 2>/dev/null || echo '{}')"
613
+ legacy_field() {
614
+ node -e 'const r = JSON.parse(process.argv[1] || "{}"); const v = process.argv[2].split(".").reduce((o, k) => o?.[k], r); process.stdout.write(Array.isArray(v) ? v.join("\n") : String(v ?? ""));' "$LEGACY_REPORT" "$1"
615
+ }
616
+ LEGACY_INSTALL="$(legacy_field legacy)"
617
+ LEGACY_SUBAGENTS=false
618
+ if legacy_field remove | grep -q '^npm:pi-subagents@'; then LEGACY_SUBAGENTS=true; fi
619
+ FIRST_INSTALL=true
620
+ if [[ -f "$PIAGENT_HOME/settings.json" ]] && grep -qE '@piagent/platform|Vt-mmm/piagent' "$PIAGENT_HOME/settings.json" 2>/dev/null; then
621
+ FIRST_INSTALL=false
622
+ fi
623
+
624
+ # Ask, rather than decide for the member, what goes into Piagent's home: only on
625
+ # a first install (an update keeps what is there), only in a terminal, and never
626
+ # for a question a flag already answered.
627
+ ask_yes_no() {
628
+ local question="$1" default="$2" answer hint="[Y/n]"
629
+ [[ "$default" == false ]] && hint="[y/N]"
630
+ read -r -p "$question $hint " answer </dev/tty || answer=""
631
+ case "$answer" in
632
+ [Yy]*) echo true ;;
633
+ [Nn]*) echo false ;;
634
+ *) echo "$default" ;;
635
+ esac
636
+ }
637
+ if [[ "$FIRST_INSTALL" == true && "$DRY_RUN" == false && -t 0 && -t 1 && -z "${PIAGENT_INSTALL_DEFAULTS:-}" ]]; then
638
+ echo "Piagent installs into its own Pi home: $PIAGENT_HOME"
639
+ echo "Your personal pi ($PERSONAL_PI_DIR) is not changed by these choices."
640
+ [[ "$MCP_EXPLICIT" == true ]] || WITH_MCP="$(ask_yes_no "Install the MCP adapter (pi-mcp-adapter, servers: $MCP_PRESET)?" "$WITH_MCP")"
641
+ if [[ "$SUBAGENTS_EXPLICIT" == false ]]; then
642
+ WITH_SUBAGENTS="$(ask_yes_no "Install pi-subagents (optional parallel helpers; company sessions use their own)?" "$LEGACY_SUBAGENTS")"
643
+ SUBAGENTS_EXPLICIT=true
644
+ fi
645
+ [[ "$WEB_ACCESS_EXPLICIT" == true ]] || WITH_WEB_ACCESS="$(ask_yes_no "Install pi-web-access (web search and fetch for the researcher subagent)?" "$WITH_WEB_ACCESS")"
646
+ [[ "$MODEL_SCOPE_EXPLICIT" == true ]] || CONFIGURE_MODEL_SCOPE="$(ask_yes_no "Set Piagent's model list and default model ($DEFAULT_MODEL)?" "$CONFIGURE_MODEL_SCOPE")"
647
+ if [[ -z "$COPY_LOGIN" && "$(legacy_field loginAvailable)" == true ]]; then
648
+ COPY_LOGIN="$(ask_yes_no "Use the login your personal pi already has (copies auth.json once)?" true)"
649
+ fi
650
+ echo
651
+ fi
652
+ if [[ -z "$COPY_LOGIN" ]]; then
653
+ COPY_LOGIN="$LEGACY_INSTALL"
654
+ fi
655
+ if [[ "$LEGACY_SUBAGENTS" == true && "$SUBAGENTS_EXPLICIT" == false ]]; then
656
+ WITH_SUBAGENTS=true
657
+ fi
658
+
572
659
  # The preset is only read at the very last step, after the platform package and
573
660
  # the MCP adapter are already installed. A name with a typo therefore failed
574
661
  # after two installs had landed, leaving a half-configured machine, and the
@@ -736,10 +823,25 @@ if [[ -n "$RESOLVED_PACKAGE_COMMIT" ]]; then
736
823
  fi
737
824
  echo " source: $PACKAGE_SOURCE"
738
825
  sync_local_helper
826
+ echo " home: $PIAGENT_HOME (your personal pi stays in $PERSONAL_PI_DIR)"
827
+ run_cmd mkdir -p "$PIAGENT_HOME"
739
828
  echo "Removing previous Pi Agent Platform package registrations:"
740
829
  remove_existing_piagent_platform_sources
741
830
  run_cmd pi install "$PACKAGE_SOURCE"
742
831
 
832
+ # An older release installed Piagent into personal Pi. Move it out: carry the
833
+ # member's model choices (and the login, when agreed) to Piagent's home, then
834
+ # leave personal Pi stock. Backups of every changed file stay beside it.
835
+ if [[ "$LEGACY_INSTALL" == true ]]; then
836
+ echo "Moving the older Piagent install out of your personal pi:"
837
+ restore_args=("$PLATFORM_ROOT/scripts/personal-pi-restore.mjs" --apply)
838
+ [[ "$COPY_LOGIN" == true ]] && restore_args+=(--copy-login)
839
+ run_cmd node "${restore_args[@]}"
840
+ elif [[ "$COPY_LOGIN" == true && "$(legacy_field loginAvailable)" == true ]]; then
841
+ echo "Copying your pi login to Piagent's home:"
842
+ run_cmd node -e 'const fs = require("node:fs"); fs.copyFileSync(process.argv[1], process.argv[2], fs.constants.COPYFILE_EXCL); fs.chmodSync(process.argv[2], 0o600);' "$PERSONAL_PI_DIR/auth.json" "$PIAGENT_HOME/auth.json"
843
+ fi
844
+
743
845
  if [[ "$WITH_MCP" == true ]]; then
744
846
  echo "Installing Pi MCP adapter:"
745
847
  if [[ "$DRY_RUN" == false ]]; then
@@ -747,6 +849,9 @@ if [[ "$WITH_MCP" == true ]]; then
747
849
  fi
748
850
  remove_existing_npm_package_sources "pi-mcp-adapter"
749
851
  run_cmd pi install "$PI_MCP_ADAPTER_SOURCE"
852
+ # Pi's built-in `mcp` extension registers the same /mcp command as the
853
+ # adapter; Piagent's home turns the built-in off so only one loads.
854
+ run_cmd node -e 'const fs = require("node:fs"); const f = process.argv[1]; const s = JSON.parse(fs.readFileSync(f, "utf8")); const e = Array.isArray(s.extensions) ? s.extensions : []; if (!e.includes("-builtin:mcp")) { s.extensions = [...e, "-builtin:mcp"]; fs.writeFileSync(f, JSON.stringify(s, null, 2) + "\n"); }' "$PIAGENT_HOME/settings.json"
750
855
  if [[ "$DRY_RUN" == false ]] && command -v pi-mcp-adapter >/dev/null 2>&1; then
751
856
  pi-mcp-adapter init || true
752
857
  fi
@@ -802,10 +907,13 @@ if [[ "$DRY_RUN" == true ]]; then
802
907
  print_cmd node "$PLATFORM_ROOT/scripts/personal-pi-config.mjs" repair
803
908
  else
804
909
  node "$PLATFORM_ROOT/scripts/personal-pi-config.mjs" repair || echo "WARN: could not check the personal Pi configuration; run: node \"$PLATFORM_ROOT/scripts/personal-pi-config.mjs\" check" >&2
910
+ node "$PLATFORM_ROOT/scripts/personal-pi-config.mjs" repair --agent-dir "$PIAGENT_HOME" >/dev/null || true
805
911
  fi
806
912
 
807
913
  echo
808
- echo "Installed packages:"
914
+ echo "Open Piagent with: piagent (your own pi in $PERSONAL_PI_DIR stays stock)"
915
+ echo
916
+ echo "Installed packages in $PIAGENT_HOME:"
809
917
  if [[ "$DRY_RUN" == true ]]; then
810
918
  print_cmd pi list
811
919
  else
@@ -814,8 +922,8 @@ fi
814
922
 
815
923
  echo
816
924
  echo "Next:"
817
- echo " pi"
818
- echo " /login"
925
+ echo " piagent # Piagent in this terminal; plain \`pi\` stays your own"
926
+ echo " /login # unless the login of your pi was copied"
819
927
  echo " /model # or Ctrl+L: select provider/model from Pi selector"
820
928
  echo " /scoped-models # optional: edit Ctrl+P model cycle scope"
821
929
  if [[ "$WITH_MCP" == true ]]; then
@@ -10,6 +10,7 @@ import fs from "node:fs";
10
10
  import path from "node:path";
11
11
  import { execFileSync } from "node:child_process";
12
12
  import { pathToFileURL } from "node:url";
13
+ import { piagentAgentDir } from "../packages/piagent-core/extensions/agent-home.js";
13
14
 
14
15
  // The Pi install `pi` runs from.
15
16
  export function piPackageRoot(environment = process.env) {
@@ -55,7 +56,7 @@ export function pruneSettings(settingsPath, stale) {
55
56
 
56
57
  if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(new URL(import.meta.url).pathname)) {
57
58
  const args = process.argv.slice(2), at = args.indexOf("--settings");
58
- const agentDir = process.env.PI_CODING_AGENT_DIR || path.join(process.env.HOME ?? "", ".pi", "agent");
59
+ const agentDir = piagentAgentDir(process.env, process.env.HOME ?? "");
59
60
  const settingsPath = at >= 0 && args[at + 1] ? path.resolve(args[at + 1]) : path.join(agentDir, "settings.json");
60
61
  const json = args.includes("--json"), fix = args.includes("--fix");
61
62
  const out = (value, text) => process.stdout.write(json ? `${JSON.stringify(value)}\n` : `${text}\n`);
@@ -120,7 +120,8 @@ const invoked = process.argv[1] && real(process.argv[1]) === real(fileURLToPath(
120
120
  if (invoked) {
121
121
  const args = process.argv.slice(2), command = args[0];
122
122
  const at = args.indexOf("--agent-dir");
123
- const agentDir = at >= 0 ? path.resolve(args[at + 1]) : (process.env.PI_CODING_AGENT_DIR || path.join(os.homedir(), ".pi", "agent"));
123
+ // The member's personal pi, whatever home Piagent itself runs from.
124
+ const agentDir = at >= 0 ? path.resolve(args[at + 1]) : path.join(os.homedir(), ".pi", "agent");
124
125
  if (!["check", "repair"].includes(command)) { console.error("Usage: personal-pi-config.mjs check|repair [--agent-dir <dir>] [--json]"); process.exit(2); }
125
126
  const result = command === "repair" ? repair(agentDir) : inspect(agentDir);
126
127
  if (args.includes("--json")) console.log(JSON.stringify(result, null, 2));
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env node
2
+ // Takes Piagent out of the member's personal Pi (~/.pi/agent) and leaves it as
3
+ // stock Pi. Releases before 1.46 installed Piagent, its add-ons, a model scope
4
+ // and a subagent baseline there, so every plain `pi` loaded them. Piagent now
5
+ // runs from its own home (~/.piagent/agent, opened with `piagent`).
6
+ //
7
+ // personal-pi-restore.mjs [--apply] [--copy-login] [--json]
8
+ //
9
+ // Without --apply nothing changes: it reports what it would do. With --apply:
10
+ // - carries the member's model choices, custom models and trust decisions to
11
+ // Piagent's home when that home has none yet (the login only with
12
+ // --copy-login: credentials are copied when the member says so);
13
+ // - removes the Piagent package and the exact-pinned add-ons Piagent
14
+ // installed (pi-mcp-adapter, pi-subagents, pi-web-access) from personal Pi;
15
+ // - removes the subagent baseline and the model list Piagent wrote, when the
16
+ // list still holds only models Piagent offered.
17
+ // Every changed file is backed up first. Sessions, the login and anything the
18
+ // member added stay in personal Pi.
19
+ import { spawnSync } from "node:child_process";
20
+ import fs from "node:fs";
21
+ import path from "node:path";
22
+ import { fileURLToPath } from "node:url";
23
+ import { personalPiAgentDir, piagentAgentDir } from "../packages/piagent-core/extensions/agent-home.js";
24
+
25
+ const ADDONS = ["pi-mcp-adapter", "pi-subagents", "pi-web-access"];
26
+ const MODEL_KEYS = ["defaultProvider", "defaultModel", "defaultThinkingLevel", "enabledModels"];
27
+ const CARRIED_FILES = ["models.json", "trust.json", "piagent-model-scope.json"];
28
+ const isPiagentSource = (source) => /(@piagent\/platform|Vt-mmm\/piagent|pi-company-platform)/.test(source);
29
+ // Piagent always installed add-ons at an exact version; an unpinned entry is the member's own.
30
+ const isPinnedAddon = (source) => ADDONS.some((name) => source.startsWith(`npm:${name}@`));
31
+
32
+ const readJson = (file) => { try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch { return null; } };
33
+ const sourceOf = (entry) => (typeof entry === "string" ? entry : typeof entry?.source === "string" ? entry.source : "");
34
+
35
+ function isLocalPiagent(agentDir, source) {
36
+ if (/^(npm|git|https?):/.test(source)) return false;
37
+ return readJson(path.join(path.resolve(agentDir, source), "package.json"))?.name === "@piagent/platform";
38
+ }
39
+
40
+ export function plan({ personalDir = personalPiAgentDir(), piagentDir = piagentAgentDir(), copyLogin = false } = {}) {
41
+ const settings = readJson(path.join(personalDir, "settings.json")) ?? {};
42
+ const packages = (Array.isArray(settings.packages) ? settings.packages : []).map(sourceOf).filter(Boolean);
43
+ const remove = packages.filter((source) => isPiagentSource(source) || isLocalPiagent(personalDir, source) || isPinnedAddon(source));
44
+ const kept = packages.filter((source) => !remove.includes(source) && ADDONS.some((name) => source.includes(name)));
45
+ const offered = readJson(path.join(personalDir, "piagent-model-scope.json"))?.offered;
46
+ const models = Array.isArray(settings.enabledModels) ? settings.enabledModels : [];
47
+ const piagentModelList = Array.isArray(offered) && models.length > 0 && models.every((model) => offered.includes(model));
48
+ const subagentConfig = path.join(personalDir, "extensions", "subagent", "config.json");
49
+ const target = readJson(path.join(piagentDir, "settings.json")) ?? {};
50
+ const carry = {
51
+ settings: MODEL_KEYS.filter((key) => settings[key] !== undefined && target[key] === undefined),
52
+ files: CARRIED_FILES.filter((name) => fs.existsSync(path.join(personalDir, name)) && !fs.existsSync(path.join(piagentDir, name))),
53
+ login: copyLogin && fs.existsSync(path.join(personalDir, "auth.json")) && !fs.existsSync(path.join(piagentDir, "auth.json"))
54
+ };
55
+ const cleanup = {
56
+ subagentsSetting: settings.subagents !== undefined && remove.some((source) => source.startsWith("npm:pi-subagents@")),
57
+ subagentConfig: fs.existsSync(subagentConfig) && remove.some((source) => source.startsWith("npm:pi-subagents@")),
58
+ enabledModels: piagentModelList,
59
+ modelScopeRecord: fs.existsSync(path.join(personalDir, "piagent-model-scope.json"))
60
+ };
61
+ const loginAvailable = fs.existsSync(path.join(personalDir, "auth.json")) && !fs.existsSync(path.join(piagentDir, "auth.json"));
62
+ const legacy = remove.some((source) => isPiagentSource(source) || isLocalPiagent(personalDir, source));
63
+ return { personalDir, piagentDir, legacy, remove, kept, carry, cleanup, loginAvailable };
64
+ }
65
+
66
+ function backup(file, stamp) {
67
+ if (!fs.existsSync(file)) return null;
68
+ const copy = `${file}.before-piagent-restore-${stamp}`;
69
+ fs.copyFileSync(file, copy);
70
+ fs.chmodSync(copy, 0o600);
71
+ return copy;
72
+ }
73
+
74
+ function writeJson(file, value) {
75
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
76
+ const tmp = `${file}.${process.pid}.tmp`;
77
+ fs.writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
78
+ fs.renameSync(tmp, file);
79
+ }
80
+
81
+ function copyPrivate(from, to) {
82
+ fs.mkdirSync(path.dirname(to), { recursive: true, mode: 0o700 });
83
+ fs.copyFileSync(from, to, fs.constants.COPYFILE_EXCL);
84
+ fs.chmodSync(to, 0o600);
85
+ }
86
+
87
+ // `pi remove` also deletes the package's files; when Pi is missing or refuses,
88
+ // the entry still leaves settings.json so personal Pi stops loading it.
89
+ function removePackage(personalDir, source) {
90
+ const env = { ...process.env, PI_CODING_AGENT_DIR: personalDir };
91
+ delete env.PIAGENT_ACTIVE;
92
+ delete env.PIAGENT_AGENT_DIR;
93
+ const result = spawnSync("pi", ["remove", source], { env, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], shell: process.platform === "win32" });
94
+ if (result.status === 0) return "pi remove";
95
+ const file = path.join(personalDir, "settings.json"), settings = readJson(file);
96
+ if (settings && Array.isArray(settings.packages)) {
97
+ settings.packages = settings.packages.filter((entry) => sourceOf(entry) !== source);
98
+ writeJson(file, settings);
99
+ }
100
+ return "settings.json";
101
+ }
102
+
103
+ export function apply(options = {}, now = new Date()) {
104
+ const report = plan(options);
105
+ const { personalDir, piagentDir, carry, cleanup } = report;
106
+ const stamp = now.toISOString().replace(/[:.]/g, "-");
107
+ const backups = [], done = [];
108
+ const personalSettingsFile = path.join(personalDir, "settings.json");
109
+ const anything = report.remove.length || Object.values(cleanup).some(Boolean);
110
+ if (anything) backups.push(backup(personalSettingsFile, stamp));
111
+
112
+ // Carry first, so nothing the member chose is lost if a removal fails.
113
+ if (carry.settings.length) {
114
+ const from = readJson(personalSettingsFile) ?? {}, targetFile = path.join(piagentDir, "settings.json");
115
+ const target = readJson(targetFile) ?? {};
116
+ for (const key of carry.settings) target[key] = from[key];
117
+ writeJson(targetFile, target);
118
+ done.push(`carried ${carry.settings.join(", ")} to ${targetFile}`);
119
+ }
120
+ for (const name of carry.files) {
121
+ copyPrivate(path.join(personalDir, name), path.join(piagentDir, name));
122
+ done.push(`carried ${name} to ${piagentDir}`);
123
+ }
124
+ if (carry.login) {
125
+ copyPrivate(path.join(personalDir, "auth.json"), path.join(piagentDir, "auth.json"));
126
+ done.push(`copied the pi login to ${piagentDir} (personal pi keeps its own)`);
127
+ }
128
+
129
+ for (const source of report.remove) done.push(`removed ${source} from personal pi (${removePackage(personalDir, source)})`);
130
+
131
+ if (cleanup.subagentsSetting || cleanup.enabledModels) {
132
+ const settings = readJson(personalSettingsFile) ?? {};
133
+ if (cleanup.subagentsSetting) { delete settings.subagents; done.push("removed the subagents setting Piagent wrote"); }
134
+ if (cleanup.enabledModels) { delete settings.enabledModels; done.push("removed the model list Piagent wrote"); }
135
+ writeJson(personalSettingsFile, settings);
136
+ }
137
+ if (cleanup.subagentConfig) {
138
+ const file = path.join(personalDir, "extensions", "subagent", "config.json");
139
+ fs.renameSync(file, `${file}.before-piagent-restore-${stamp}`);
140
+ backups.push(`${file}.before-piagent-restore-${stamp}`);
141
+ done.push("set aside the subagent baseline Piagent wrote");
142
+ }
143
+ if (cleanup.modelScopeRecord) {
144
+ fs.rmSync(path.join(personalDir, "piagent-model-scope.json"), { force: true });
145
+ done.push("removed Piagent's model-scope record from personal pi");
146
+ }
147
+ return { ...report, done, backups: backups.filter(Boolean) };
148
+ }
149
+
150
+ function describe(report, applied) {
151
+ const lines = [`Personal pi: ${report.personalDir}`, `Piagent home: ${report.piagentDir}`];
152
+ if (applied) {
153
+ lines.push(...(report.done.length ? report.done.map((line) => ` ${line}`) : [" nothing to change: personal pi has no Piagent entries"]));
154
+ for (const file of report.backups) lines.push(` backup: ${file}`);
155
+ } else {
156
+ if (!report.remove.length && !Object.values(report.cleanup).some(Boolean)) lines.push(" nothing to change: personal pi has no Piagent entries");
157
+ for (const source of report.remove) lines.push(` would remove ${source}`);
158
+ if (report.cleanup.subagentsSetting) lines.push(" would remove the subagents setting Piagent wrote");
159
+ if (report.cleanup.subagentConfig) lines.push(" would set aside extensions/subagent/config.json");
160
+ if (report.cleanup.enabledModels) lines.push(" would remove the model list Piagent wrote");
161
+ if (report.carry.settings.length || report.carry.files.length) lines.push(` would carry ${[...report.carry.settings, ...report.carry.files].join(", ")} to Piagent's home`);
162
+ if (report.carry.login) lines.push(" would copy the pi login to Piagent's home");
163
+ else if (report.loginAvailable) lines.push(" login stays personal; add --copy-login to use it in Piagent too (else run `piagent`, then /login)");
164
+ }
165
+ for (const source of report.kept) lines.push(` kept ${source} (not a version Piagent pinned; remove it with pi remove if you do not want it)`);
166
+ if (!applied) lines.push("Dry run. Re-run with --apply to make these changes.");
167
+ return lines.join("\n");
168
+ }
169
+
170
+ const real = (file) => { try { return fs.realpathSync(file); } catch { return null; } };
171
+ const invoked = process.argv[1] && real(process.argv[1]) === real(fileURLToPath(import.meta.url));
172
+ if (invoked) {
173
+ const args = process.argv.slice(2);
174
+ if (args.includes("--help") || args.includes("-h")) {
175
+ console.log("Usage: piagent restore-pi [--apply] [--copy-login] [--json]\n\nTakes an older Piagent install out of your personal pi (~/.pi/agent) and leaves it stock.\nWithout --apply it only reports. --copy-login also copies your pi login into Piagent's home.");
176
+ process.exit(0);
177
+ }
178
+ const unknown = args.filter((value) => !["--apply", "--copy-login", "--json"].includes(value));
179
+ if (unknown.length) { console.error(`Unknown argument: ${unknown[0]}`); process.exit(2); }
180
+ const options = { copyLogin: args.includes("--copy-login") };
181
+ const applied = args.includes("--apply");
182
+ const report = applied ? apply(options) : plan(options);
183
+ console.log(args.includes("--json") ? JSON.stringify(report, null, 2) : describe(report, applied));
184
+ }
@@ -3,6 +3,7 @@ import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { pathToFileURL } from "node:url";
6
+ import { piagentAgentDir } from "../packages/piagent-core/extensions/agent-home.js";
6
7
 
7
8
  const USAGE = `Usage:
8
9
  scripts/pi-usage-history.mjs [project-path] [options]
@@ -63,7 +64,7 @@ function parseArgs(argv) {
63
64
  includeSubagents: true,
64
65
  limit: 20,
65
66
  sessionsDir: process.env.PI_CODING_AGENT_SESSION_DIR
66
- || path.join(process.env.PI_CODING_AGENT_DIR || path.join(os.homedir(), ".pi", "agent"), "sessions"),
67
+ || path.join(piagentAgentDir(), "sessions"),
67
68
  projectPath: undefined,
68
69
  since: undefined,
69
70
  until: undefined
@@ -28,7 +28,8 @@ const scriptByCommand = {
28
28
  "piagent-webui": "scripts/piagent-webui-launcher.mjs",
29
29
  "piagent-dashboard": "scripts/piagent-dashboard.mjs",
30
30
  "piagent-pi-host": "scripts/pi-host.mjs",
31
- "piagent-skills": "scripts/skill-sources.mjs"
31
+ "piagent-skills": "scripts/skill-sources.mjs",
32
+ "piagent-restore-pi": "scripts/personal-pi-restore.mjs"
32
33
  };
33
34
 
34
35
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
@@ -36,9 +37,28 @@ const invokedAs = path.basename(process.argv[1] ?? "");
36
37
  let forwardedArgs = process.argv.slice(2);
37
38
  let script = scriptByCommand[invokedAs];
38
39
 
40
+ // Piagent's Pi lives in its own home so the member's `pi` (~/.pi/agent) stays
41
+ // stock; Agent Watch's files stay in ~/.pi/agent. Same rule as
42
+ // packages/piagent-core/extensions/agent-home.js, inlined because this
43
+ // launcher runs before anything else of the package resolves.
44
+ const personalPiDir = path.resolve(os.homedir(), ".pi", "agent");
45
+ function piagentHome() {
46
+ const own = process.env.PIAGENT_AGENT_DIR?.trim();
47
+ if (own) return path.resolve(own);
48
+ const configured = process.env.PI_CODING_AGENT_DIR?.trim();
49
+ if (configured && path.resolve(configured) !== personalPiDir) return path.resolve(configured);
50
+ return path.resolve(os.homedir(), ".piagent", "agent");
51
+ }
52
+ let terminal = false;
53
+
39
54
  if (invokedAs === "piagent") {
40
55
  const subcommand = forwardedArgs[0];
41
- if (subcommand === "studio") {
56
+ if (subcommand === undefined || (subcommand.startsWith("-") && !["--help", "-h"].includes(subcommand))) {
57
+ // `piagent` on its own (or with Pi's flags, `piagent -c`) opens Piagent here.
58
+ script = "scripts/pi-host.mjs";
59
+ forwardedArgs = ["run", "--", ...forwardedArgs];
60
+ terminal = true;
61
+ } else if (subcommand === "studio") {
42
62
  script = "scripts/piagent-studio.mjs";
43
63
  forwardedArgs = forwardedArgs.slice(1);
44
64
  } else if (subcommand === "dashboard") {
@@ -48,6 +68,7 @@ if (invokedAs === "piagent") {
48
68
  // Pi itself, on the release Piagent is qualified on, whatever `pi` the member keeps.
49
69
  script = "scripts/pi-host.mjs";
50
70
  forwardedArgs = ["run", "--", ...forwardedArgs.slice(1)];
71
+ terminal = true;
51
72
  } else if (subcommand === "explain") {
52
73
  script = "scripts/explain-command.mjs";
53
74
  forwardedArgs = forwardedArgs.slice(1);
@@ -61,16 +82,18 @@ if (invokedAs === "piagent") {
61
82
  // `piagent doctor` is `piagent-doctor`: members type it both ways.
62
83
  script = scriptByCommand[`piagent-${subcommand}`];
63
84
  forwardedArgs = forwardedArgs.slice(1);
64
- } else if ([undefined, "help", "--help", "-h"].includes(subcommand)) {
85
+ } else if (["help", "--help", "-h"].includes(subcommand)) {
65
86
  // Subcommands are listed, their flags are not. Restating them here meant a
66
87
  // second copy that drifts: this line still advertised the dashboard without
67
88
  // `--repair` after that flag shipped. Each command states its own surface.
68
- console.log("Usage: piagent <command> [options]");
89
+ console.log("Usage: piagent [pi options] Open Piagent in this terminal (its own Pi home, ~/.piagent/agent)");
90
+ console.log(" piagent <command> [options]");
69
91
  console.log("");
70
92
  console.log("Commands:");
71
93
  console.log(" dashboard Open and manage the local session hub");
72
94
  console.log(" studio Open the managed company agent imported by Agent Watch");
73
- console.log(" pi Run Pi on the release Piagent is qualified on (your own pi stays as it is)");
95
+ console.log(" pi Same as `piagent`: Pi on the release Piagent is qualified on, in Piagent's home");
96
+ console.log(" restore-pi Take an older Piagent install out of your own pi (~/.pi/agent)");
74
97
  console.log(" pi-host Show or install the Pi host Piagent runs");
75
98
  console.log(" skills Add, list or remove skill sources such as mattpocock/skills");
76
99
  console.log(" explain Say why the guard would allow or block a shell command");
@@ -91,7 +114,7 @@ if (!script) {
91
114
  function benchmarkSourceRoot() {
92
115
  if (fs.existsSync(path.join(packageRoot, ".git"))) return packageRoot;
93
116
  if (process.argv.slice(2).some((value) => value === "--help" || value === "-h")) return packageRoot;
94
- const agentRoot = path.resolve(process.env.PI_CODING_AGENT_DIR || path.join(os.homedir(), ".pi", "agent"));
117
+ const agentRoot = piagentHome();
95
118
  const installed = path.join(agentRoot, "git", "github.com", "Vt-mmm", "piagent");
96
119
  try {
97
120
  const helperVersion = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8")).version;
@@ -133,8 +156,9 @@ async function recordRuntime() {
133
156
  // an entrypoint that no longer exists.
134
157
  const osTemp = fs.realpathSync.native(os.tmpdir()), installRoot = fs.realpathSync.native(packageRoot);
135
158
  if (installRoot === osTemp || installRoot.startsWith(`${osTemp}${path.sep}`)) return;
136
- const agentDir = path.resolve(process.env.PI_CODING_AGENT_DIR || path.join(os.homedir(), ".pi", "agent"));
137
- if (!fs.statSync(agentDir).isDirectory()) return;
159
+ // Agent Watch reads the record from the personal Pi folder, not Piagent's home.
160
+ const agentDir = personalPiDir;
161
+ fs.mkdirSync(agentDir, { recursive: true, mode: 0o700 });
138
162
  const body = `${JSON.stringify({
139
163
  schema_version: 1,
140
164
  version: JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8")).version,
@@ -151,6 +175,21 @@ async function recordRuntime() {
151
175
  }
152
176
  await recordRuntime();
153
177
 
178
+ if (terminal) {
179
+ let packages = [];
180
+ try { packages = JSON.parse(fs.readFileSync(path.join(piagentHome(), "settings.json"), "utf8")).packages ?? []; } catch { /* not installed yet */ }
181
+ const isPiagent = (entry) => {
182
+ const source = String(typeof entry === "string" ? entry : entry?.source ?? "");
183
+ if (/@piagent\/platform|Vt-mmm\/piagent/.test(source)) return true;
184
+ try { return JSON.parse(fs.readFileSync(path.join(path.resolve(piagentHome(), source), "package.json"), "utf8")).name === "@piagent/platform"; } catch { return false; }
185
+ };
186
+ if (!packages.some(isPiagent)) {
187
+ console.error(`Piagent is not installed in ${piagentHome()} yet. Run: piagent-install --stable`);
188
+ console.error("An install from before 1.46 lives in your own pi; piagent-install moves it, or run: piagent restore-pi");
189
+ process.exit(1);
190
+ }
191
+ }
192
+
154
193
  const sourceRoot = invokedAs === "piagent-benchmark" ? benchmarkSourceRoot() : packageRoot;
155
194
  const target = path.join(sourceRoot, script);
156
195
  const runner = target.endsWith(".mjs") ? process.execPath : "bash";
@@ -159,7 +198,8 @@ const runnerArgs = target.endsWith(".mjs")
159
198
  : [target];
160
199
  const child = spawn(runner, [...runnerArgs, ...forwardedArgs], {
161
200
  cwd: process.cwd(),
162
- env: script === "scripts/piagent-studio.mjs" ? { PATH: "/usr/bin:/bin", HOME: os.homedir(), TERM: process.env.TERM || "xterm-256color", LANG: "en_US.UTF-8" } : process.env,
201
+ env: script === "scripts/piagent-studio.mjs" ? { PATH: "/usr/bin:/bin", HOME: os.homedir(), TERM: process.env.TERM || "xterm-256color", LANG: "en_US.UTF-8" }
202
+ : { ...process.env, PI_CODING_AGENT_DIR: piagentHome(), ...(terminal ? { PIAGENT_ACTIVE: "1" } : {}) },
163
203
  stdio: "inherit"
164
204
  });
165
205
 
@@ -12,6 +12,7 @@ import { gatewayProfileState, profileRef, readGatewayDescriptor, readOrCreateCat
12
12
  removeGatewayDescriptor } from "../packages/piagent-webui/gateway/profile-state.ts";
13
13
  import { startPiagentGateway } from "../packages/piagent-webui/gateway/gateway-service.ts";
14
14
  import { agentWatchCompanyConnector } from "../packages/piagent-webui/gateway/managed-launch.ts";
15
+ import { agentWatchControlDir, piagentAgentDir } from "../packages/piagent-core/extensions/agent-home.js";
15
16
 
16
17
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
17
18
  const packageJson = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
@@ -177,7 +178,7 @@ async function ensureStarted() {
177
178
  }
178
179
  const existing = await currentLaunchUrl() ?? await waitForRunningGateway();
179
180
  if (existing) return existing;
180
- const effectiveAgentDir = path.resolve(agentDir ?? process.env.PI_CODING_AGENT_DIR ?? path.join(os.homedir(), ".pi", "agent"));
181
+ const effectiveAgentDir = path.resolve(agentDir ?? piagentAgentDir());
181
182
  const state = gatewayProfileState(effectiveAgentDir);
182
183
  writeGatewayErrorLog(state);
183
184
  const child = spawn(process.execPath, [
@@ -210,8 +211,9 @@ async function ensureStarted() {
210
211
 
211
212
  async function serve() {
212
213
  // Company sessions come from the Agent Watch binding in this Pi folder.
213
- const piAgentDir = path.resolve(agentDir ?? process.env.PI_CODING_AGENT_DIR ?? path.join(os.homedir(), ".pi", "agent"));
214
- const gateway = await startPiagentGateway({ packageRoot, expectedPiVersion, agentDir, company: agentWatchCompanyConnector(piAgentDir) });
214
+ // Agent Watch writes the company binding into the personal Pi folder.
215
+ const bindingDir = agentDir ? path.resolve(agentDir) : agentWatchControlDir();
216
+ const gateway = await startPiagentGateway({ packageRoot, expectedPiVersion, agentDir: agentDir ?? piagentAgentDir(), company: agentWatchCompanyConnector(bindingDir) });
215
217
  const shutdown = () => { void gateway.close(); };
216
218
  process.once("SIGINT", shutdown);
217
219
  process.once("SIGTERM", shutdown);
package/scripts/setup.sh CHANGED
@@ -504,12 +504,8 @@ echo "Next:"
504
504
  if [[ "$DO_PROJECT" == true ]]; then
505
505
  echo " cd \"$PROJECT_PATH\""
506
506
  fi
507
- if [[ "$PI_HOST_PRIVATE" == true ]]; then
508
- echo " piagent pi # Pi $(node -p 'require(process.argv[1]).peerDependencies["@earendil-works/pi-coding-agent"]' "$PLATFORM_ROOT/package.json"), the release Piagent is qualified on; your own pi also loads Piagent"
509
- else
510
- echo " pi"
511
- fi
512
- echo " /login # first time only"
507
+ echo " piagent # Piagent in its own Pi home; plain \`pi\` stays your own, stock"
508
+ echo " /login # first time only, unless the login of your pi was copied"
513
509
  if [[ "$DO_PROJECT" == true ]]; then
514
510
  echo " /model # or Ctrl+L: select provider/model from Pi selector"
515
511
  echo " /scoped-models # optional: edit Ctrl+P model cycle scope"