@piagent/platform 1.45.0 → 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 (81) hide show
  1. package/CHANGELOG.md +19 -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 +1 -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 +9 -9
  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 +31 -19
  37. package/docs-site/en/windows.html +1 -1
  38. package/docs-site/en/workflows.html +1 -1
  39. package/docs-site/index.html +9 -9
  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 +39 -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 +31 -19
  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/mcp/mcp-config-layers.js +4 -1
  63. package/packages/piagent-core/package.json +1 -1
  64. package/packages/piagent-webui/extension/piagent-webui.ts +2 -0
  65. package/packages/piagent-webui/gateway/gateway-service.ts +4 -1
  66. package/packages/piagent-webui/gateway/pi-host.ts +2 -2
  67. package/packages/piagent-webui/ownership/profile-state.ts +10 -1
  68. package/packages/piagent-webui/server/benchmark-release-monitor.ts +2 -1
  69. package/scripts/benchmark-runner-support.mjs +2 -1
  70. package/scripts/dashboard-update-job.mjs +4 -2
  71. package/scripts/install-global.sh +113 -5
  72. package/scripts/model-scope-prune.mjs +2 -1
  73. package/scripts/personal-pi-config.mjs +2 -1
  74. package/scripts/personal-pi-restore.mjs +184 -0
  75. package/scripts/pi-usage-history.mjs +2 -1
  76. package/scripts/piagent-cli.mjs +49 -9
  77. package/scripts/piagent-dashboard.mjs +5 -3
  78. package/scripts/setup.sh +2 -6
  79. package/scripts/team-doctor.sh +11 -5
  80. package/scripts/uninstall-global.sh +17 -2
  81. package/scripts/update-global.mjs +5 -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"
@@ -72,8 +72,14 @@ const installations = piagentInstallations();
72
72
  if(installations.length>1) warnings.push("Multiple Piagent installations detected. Use the pinned Watch launcher; other PATHs may run an older build: "+installations.map(i=>`${i.version} [${i.fingerprint}] ${i.root}`).join("; "));
73
73
  // Personal Pi loading Piagent twice does not start; old company providers send it to Studio outside the Harness.
74
74
  const { inspect: inspectPersonalPi } = await import(pathToFileURL(path.join(platformRoot, "scripts/personal-pi-config.mjs")).href);
75
- for (const finding of inspectPersonalPi(process.env.PI_CODING_AGENT_DIR || path.join(process.env.HOME || "", ".pi", "agent")).findings)
76
- (finding.kind === "duplicate-extensions" ? errors : warnings).push(`personal Pi: ${finding.text} Fix: piagent-update`);
75
+ const personalPiDir = path.join(process.env.HOME || "", ".pi", "agent");
76
+ for (const agentDir of new Set([personalPiDir, process.env.PI_CODING_AGENT_DIR || personalPiDir]))
77
+ for (const finding of inspectPersonalPi(agentDir).findings)
78
+ (finding.kind === "duplicate-extensions" ? errors : warnings).push(`${agentDir === personalPiDir ? "personal Pi" : "Piagent home"}: ${finding.text} Fix: piagent-update`);
79
+ // Since 1.46 Piagent runs from its own home; an older install left in personal Pi keeps loading there.
80
+ const { plan: personalPiRestorePlan } = await import(pathToFileURL(path.join(platformRoot, "scripts/personal-pi-restore.mjs")).href);
81
+ if (personalPiRestorePlan({ personalDir: personalPiDir }).legacy)
82
+ warnings.push("personal Pi still loads an older Piagent install (~/.pi/agent). Fix: piagent restore-pi --apply");
77
83
  const { verifyCapabilityLock } = await import(pathToFileURL(path.join(platformRoot, "packages", "piagent-core", "capabilities", "capability-core.js")).href);
78
84
 
79
85
  function exists(rel) {
@@ -271,9 +277,9 @@ if (commandExists("pi")) {
271
277
  piHasMcpAdapter = combined.includes("pi-mcp-adapter");
272
278
  piHasSubagents = combined.includes("pi-subagents");
273
279
  piHasWebAccess = combined.includes("pi-web-access");
274
- if (!piHasMcpAdapter) warnings.push("Pi MCP adapter not found in `pi list`; run setup with --with-mcp or `pi install npm:pi-mcp-adapter@2.15.0`");
275
- if (!piHasSubagents) warnings.push("Pi subagents package not found in `pi list`; run setup with --with-subagents or `pi install npm:pi-subagents@0.38.0`");
276
- if (!piHasWebAccess) warnings.push("Pi web access package not found in `pi list`; run `piagent-update --force` or `pi install npm:pi-web-access@0.17.0` so researcher subagents can browse/fetch external sources");
280
+ if (!piHasMcpAdapter) warnings.push("Pi MCP adapter not found in `pi list`; run `piagent-install --with-mcp` (installs into Piagent's home, not your personal pi)");
281
+ if (!piHasSubagents) warnings.push("Pi subagents package not found in `pi list`; run `piagent-install --with-subagents` if you want them");
282
+ if (!piHasWebAccess) warnings.push("Pi web access package not found in `pi list`; run `piagent-install --with-web-access` so researcher subagents can browse/fetch external sources");
277
283
  }
278
284
 
279
285
  const mcpFiles = [
@@ -9,7 +9,16 @@ set -euo pipefail
9
9
  # deleted.
10
10
 
11
11
  ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
12
- PI_AGENT_DIR="${PI_CODING_AGENT_DIR:-"${HOME}/.pi/agent"}"
12
+ # Piagent's own Pi home (same rule as packages/piagent-core/extensions/agent-home.js).
13
+ PERSONAL_PI_DIR="${HOME}/.pi/agent"
14
+ if [[ -n "${PIAGENT_AGENT_DIR:-}" ]]; then
15
+ PI_AGENT_DIR="$PIAGENT_AGENT_DIR"
16
+ elif [[ -n "${PI_CODING_AGENT_DIR:-}" && "${PI_CODING_AGENT_DIR%/}" != "$PERSONAL_PI_DIR" ]]; then
17
+ PI_AGENT_DIR="$PI_CODING_AGENT_DIR"
18
+ else
19
+ PI_AGENT_DIR="${HOME}/.piagent/agent"
20
+ fi
21
+ export PI_CODING_AGENT_DIR="$PI_AGENT_DIR"
13
22
  GLOBAL_SETTINGS="$PI_AGENT_DIR/settings.json"
14
23
 
15
24
  APPLY=false
@@ -26,7 +35,9 @@ usage() {
26
35
  Usage:
27
36
  piagent-uninstall [--apply] [--with-addons] [--with-host] [--project <path>]
28
37
 
29
- Removes the Pi package this platform installed from Pi's global settings.
38
+ Removes the Pi package this platform installed from Piagent's own Pi home
39
+ (~/.piagent/agent). An install from before 1.46 lives in your personal pi
40
+ (~/.pi/agent): `piagent restore-pi --apply` takes it out of there.
30
41
 
31
42
  (default) Dry run. Reports what would be removed and exits 0.
32
43
  --apply Perform the removal.
@@ -291,6 +302,10 @@ NODE
291
302
  fi
292
303
 
293
304
  echo
305
+ if node "$ROOT/scripts/personal-pi-restore.mjs" --json 2>/dev/null | grep -q '"legacy": true'; then
306
+ echo "Your personal pi ($PERSONAL_PI_DIR) still loads an older Piagent install."
307
+ echo " Take it out with: piagent restore-pi --apply"
308
+ fi
294
309
  if [[ "$APPLY" == true ]]; then
295
310
  echo "Uninstall complete."
296
311
  echo "The npm-global helper is separate and stays until you remove it:"
@@ -22,6 +22,7 @@ import path from "node:path";
22
22
  import { spawnSync } from "node:child_process";
23
23
  import { fileURLToPath } from "node:url";
24
24
  import { inspect as inspectPersonalPi } from "./personal-pi-config.mjs";
25
+ import { personalPiAgentDir, piagentAgentDir } from "../packages/piagent-core/extensions/agent-home.js";
25
26
  import { hostPlan, installPrivateHost, privateHostRoot } from "../packages/piagent-core/managed/pi-host.mjs";
26
27
 
27
28
  const HELPER_PACKAGE = "@piagent/platform";
@@ -438,8 +439,10 @@ function main() {
438
439
  : ` host: ${currentHost ?? "not installed"} -> ${targetHost}${hostNeedsChange ? "" : " (already current)"}`);
439
440
  // The Pi package can lag the helper (installed from an older tag), and
440
441
  // personal Pi can hold conflicting Piagent or old company entries.
441
- const personal = inspectPersonalPi(process.env.PI_CODING_AGENT_DIR || path.join(os.homedir(), ".pi", "agent"));
442
- const installedPackages = personal.packages.map((item) => item.version ?? "unknown");
442
+ // The package lives in Piagent's home; old company entries and an older
443
+ // Piagent install live in the member's personal pi.
444
+ const personal = inspectPersonalPi(personalPiAgentDir()), own = inspectPersonalPi(piagentAgentDir());
445
+ const installedPackages = own.packages.map((item) => item.version ?? "unknown");
443
446
  const packageNeedsChange = options.package && (options.force || installedPackages.length !== 1 || installedPackages[0] !== targetHelper);
444
447
  console.log(` package: ${installedPackages.join(", ") || "not installed"} -> v${targetHelper}${options.package ? packageNeedsChange ? " (reinstalled as a resolved commit)" : " (already current)" : " (skipped)"}`);
445
448
  for (const finding of personal.findings) console.log(` personal Pi: ${finding.text}`);