@piagent/platform 1.27.1 → 1.28.1

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 (70) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +1 -1
  3. package/README.vi.md +1 -1
  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/benchmark.html +1 -1
  13. package/docs-site/commands.html +1 -1
  14. package/docs-site/content/en/index.html +4 -4
  15. package/docs-site/content/en/whats-new.html +12 -0
  16. package/docs-site/content/index.html +4 -4
  17. package/docs-site/content/whats-new.html +12 -0
  18. package/docs-site/context-engine.html +1 -1
  19. package/docs-site/ecosystem.html +1 -1
  20. package/docs-site/en/architecture.html +1 -1
  21. package/docs-site/en/benchmark.html +1 -1
  22. package/docs-site/en/commands.html +1 -1
  23. package/docs-site/en/context-engine.html +1 -1
  24. package/docs-site/en/ecosystem.html +1 -1
  25. package/docs-site/en/index.html +5 -5
  26. package/docs-site/en/mcp-approval.html +1 -1
  27. package/docs-site/en/mcp-auth.html +1 -1
  28. package/docs-site/en/mcp-servers.html +1 -1
  29. package/docs-site/en/mcp.html +1 -1
  30. package/docs-site/en/permissions.html +1 -1
  31. package/docs-site/en/profiles.html +1 -1
  32. package/docs-site/en/quickstart.html +1 -1
  33. package/docs-site/en/runtime.html +1 -1
  34. package/docs-site/en/scope.html +1 -1
  35. package/docs-site/en/security.html +1 -1
  36. package/docs-site/en/team.html +1 -1
  37. package/docs-site/en/whats-new.html +13 -1
  38. package/docs-site/en/windows.html +1 -1
  39. package/docs-site/en/workflows.html +1 -1
  40. package/docs-site/index.html +5 -5
  41. package/docs-site/mcp-approval.html +1 -1
  42. package/docs-site/mcp-auth.html +1 -1
  43. package/docs-site/mcp-servers.html +1 -1
  44. package/docs-site/mcp.html +1 -1
  45. package/docs-site/permissions.html +1 -1
  46. package/docs-site/profiles.html +1 -1
  47. package/docs-site/quickstart.html +1 -1
  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/whats-new.html +13 -1
  53. package/docs-site/windows.html +1 -1
  54. package/docs-site/workflows.html +1 -1
  55. package/evals/private-holdout-v1/public-exposure.v2.json +2 -2
  56. package/package.json +1 -1
  57. package/packages/piagent-core/extensions/company-session-guard.ts +36 -0
  58. package/packages/piagent-core/extensions/piagent-guard.ts +2 -0
  59. package/packages/piagent-core/managed/memory-lessons.mjs +63 -0
  60. package/packages/piagent-core/managed/memory-outbox.mjs +93 -0
  61. package/packages/piagent-core/managed/memory.mjs +246 -93
  62. package/packages/piagent-core/managed/session.mjs +6 -6
  63. package/packages/piagent-core/package.json +1 -1
  64. package/scripts/company-terminal.mjs +12 -0
  65. package/scripts/install-global.sh +11 -0
  66. package/scripts/personal-pi-config.mjs +137 -0
  67. package/scripts/piagent-cli.mjs +8 -0
  68. package/scripts/piagent-studio.mjs +4 -0
  69. package/scripts/team-doctor.sh +4 -0
  70. package/scripts/update-global.mjs +9 -2
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env node
2
+ // What in the member's personal Pi configuration (~/.pi/agent) conflicts with
3
+ // Piagent, and the repair. Personal Pi loads Piagent as a Pi package; an
4
+ // extension path that points into another Piagent copy registers the same
5
+ // tools twice and Pi refuses to start ("Tool … conflicts with …"). Older Agent
6
+ // Watch releases also wrote company providers (agent_watch_*, a direct key
7
+ // read from Agent Watch) and a company default model into personal Pi; with a
8
+ // managed binding (agent-watch-managed.json) the company runs only through
9
+ // `piagent studio`, so those entries are leftovers that send personal Pi to
10
+ // Studio outside the team's Harness, or fail.
11
+ //
12
+ // personal-pi-config.mjs check [--agent-dir <dir>] [--json] exit 1 when something conflicts
13
+ // personal-pi-config.mjs repair [--agent-dir <dir>] backs up, then removes what conflicts
14
+ import fs from "node:fs";
15
+ import os from "node:os";
16
+ import path from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+
19
+ const COMPANY_PROVIDER = /^agent_watch_/;
20
+ const SESSION_HEADER_EXTENSION = "agentwatch-studio-session.mjs";
21
+
22
+ const readJson = (file) => { try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch { return null; } };
23
+ const real = (file) => { try { return fs.realpathSync(file); } catch { return null; } };
24
+
25
+ // The Piagent package a path belongs to (its root), or null.
26
+ export function piagentRootOf(file) {
27
+ let dir = real(file);
28
+ if (!dir) return null;
29
+ if (!fs.statSync(dir).isDirectory()) dir = path.dirname(dir);
30
+ for (; dir !== path.dirname(dir); dir = path.dirname(dir)) {
31
+ const manifest = readJson(path.join(dir, "package.json"));
32
+ if (manifest?.name === "@piagent/platform") return dir;
33
+ }
34
+ return null;
35
+ }
36
+
37
+ const isPiagentSource = (source) => /^(git:github\.com\/Vt-mmm\/piagent|https:\/\/github\.com\/Vt-mmm\/piagent|npm:@piagent\/platform)(@|$)/.test(String(source));
38
+
39
+ // The installed Pi package's version (git:… sources live under <agentDir>/git/…).
40
+ export function packageVersion(agentDir, source) {
41
+ const match = /^git:([^@]+)/.exec(String(source));
42
+ if (match) return readJson(path.join(agentDir, "git", match[1], "package.json"))?.version ?? null;
43
+ const npm = /^npm:(@piagent\/platform)/.exec(String(source));
44
+ if (npm) return readJson(path.join(agentDir, "npm", "node_modules", npm[1], "package.json"))?.version ?? null;
45
+ return null;
46
+ }
47
+
48
+ export function inspect(agentDir) {
49
+ const settingsFile = path.join(agentDir, "settings.json"), modelsFile = path.join(agentDir, "models.json");
50
+ const settings = readJson(settingsFile) ?? {}, models = readJson(modelsFile) ?? {};
51
+ const managed = fs.existsSync(path.join(agentDir, "agent-watch-managed.json"));
52
+ const packages = Array.isArray(settings.packages) ? settings.packages : [];
53
+ const piagentPackages = packages.filter(isPiagentSource);
54
+ const extensions = Array.isArray(settings.extensions) ? settings.extensions : [];
55
+ const findings = [];
56
+
57
+ // Extensions inside a Piagent copy load beside the Pi package: duplicate tools.
58
+ const duplicate = extensions.filter((entry) => piagentRootOf(path.resolve(agentDir, String(entry))) && piagentPackages.length);
59
+ if (duplicate.length) findings.push({ kind: "duplicate-extensions", file: settingsFile, entries: duplicate,
60
+ text: `${duplicate.length} extension(s) point into another Piagent copy while the Piagent Pi package is installed: Pi refuses to start (tools registered twice).` });
61
+ if (piagentPackages.length > 1) findings.push({ kind: "duplicate-packages", file: settingsFile, entries: piagentPackages,
62
+ text: `${piagentPackages.length} Piagent Pi packages are registered; keep one.` });
63
+
64
+ if (managed) {
65
+ const providers = Object.keys(models.providers ?? {}).filter((name) => COMPANY_PROVIDER.test(name));
66
+ if (providers.length) findings.push({ kind: "company-providers", file: modelsFile, entries: providers,
67
+ text: `Old Agent Watch company providers (${providers.join(", ")}) are in personal Pi; company work runs only through \`piagent studio\`.` });
68
+ const defaults = [];
69
+ if (COMPANY_PROVIDER.test(String(settings.defaultProvider ?? ""))) defaults.push(`defaultProvider=${settings.defaultProvider}`);
70
+ for (const entry of Array.isArray(settings.enabledModels) ? settings.enabledModels : []) if (COMPANY_PROVIDER.test(String(entry))) defaults.push(`enabledModels:${entry}`);
71
+ if (defaults.length) findings.push({ kind: "company-defaults", file: settingsFile, entries: defaults,
72
+ text: "Personal Pi's default model or model list still names an old company provider." });
73
+ const header = extensions.filter((entry) => path.basename(String(entry)) === SESSION_HEADER_EXTENSION);
74
+ if (header.length) findings.push({ kind: "company-extension", file: settingsFile, entries: header,
75
+ text: "The old Agent Watch session-header extension is still loaded in personal Pi." });
76
+ }
77
+ return { agentDir, managed, findings, packages: piagentPackages.map((source) => ({ source, version: packageVersion(agentDir, source) })) };
78
+ }
79
+
80
+ function backup(file, stamp) {
81
+ if (!fs.existsSync(file)) return null;
82
+ const copy = `${file}.before-piagent-repair-${stamp}`;
83
+ fs.copyFileSync(file, copy);
84
+ fs.chmodSync(copy, 0o600);
85
+ return copy;
86
+ }
87
+
88
+ function writeJson(file, value) {
89
+ const tmp = `${file}.${process.pid}.tmp`;
90
+ fs.writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
91
+ fs.renameSync(tmp, file);
92
+ }
93
+
94
+ // Removes what inspect() found (duplicate packages are left to the installer,
95
+ // which knows the one to keep). Every changed file is backed up first.
96
+ export function repair(agentDir, now = new Date()) {
97
+ const report = inspect(agentDir);
98
+ const kinds = new Set(report.findings.map((finding) => finding.kind));
99
+ const stamp = now.toISOString().replace(/[:.]/g, "-");
100
+ const changed = [], backups = [];
101
+ const settingsFile = path.join(agentDir, "settings.json"), modelsFile = path.join(agentDir, "models.json");
102
+ if (kinds.has("duplicate-extensions") || kinds.has("company-defaults") || kinds.has("company-extension")) {
103
+ const settings = readJson(settingsFile);
104
+ const drop = new Set(report.findings.filter((f) => f.kind === "duplicate-extensions" || f.kind === "company-extension").flatMap((f) => f.entries));
105
+ if (Array.isArray(settings.extensions)) settings.extensions = settings.extensions.filter((entry) => !drop.has(entry));
106
+ if (Array.isArray(settings.extensions) && !settings.extensions.length) delete settings.extensions;
107
+ if (COMPANY_PROVIDER.test(String(settings.defaultProvider ?? ""))) { delete settings.defaultProvider; delete settings.defaultModel; }
108
+ if (Array.isArray(settings.enabledModels)) settings.enabledModels = settings.enabledModels.filter((entry) => !COMPANY_PROVIDER.test(String(entry)));
109
+ backups.push(backup(settingsFile, stamp)); writeJson(settingsFile, settings); changed.push(settingsFile);
110
+ }
111
+ if (kinds.has("company-providers")) {
112
+ const models = readJson(modelsFile);
113
+ for (const name of Object.keys(models.providers ?? {})) if (COMPANY_PROVIDER.test(name)) delete models.providers[name];
114
+ backups.push(backup(modelsFile, stamp)); writeJson(modelsFile, models); changed.push(modelsFile);
115
+ }
116
+ return { ...report, changed, backups: backups.filter(Boolean), after: inspect(agentDir) };
117
+ }
118
+
119
+ const invoked = process.argv[1] && real(process.argv[1]) === real(fileURLToPath(import.meta.url));
120
+ if (invoked) {
121
+ const args = process.argv.slice(2), command = args[0];
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"));
124
+ if (!["check", "repair"].includes(command)) { console.error("Usage: personal-pi-config.mjs check|repair [--agent-dir <dir>] [--json]"); process.exit(2); }
125
+ const result = command === "repair" ? repair(agentDir) : inspect(agentDir);
126
+ if (args.includes("--json")) console.log(JSON.stringify(result, null, 2));
127
+ else if (command === "repair") {
128
+ for (const finding of result.findings) console.log(`fixed: ${finding.text}`);
129
+ for (const file of result.backups) console.log(`backup: ${file}`);
130
+ if (!result.findings.length) console.log("personal Pi configuration: nothing to repair");
131
+ } else {
132
+ for (const finding of result.findings) console.log(`WARN: ${finding.text}`);
133
+ if (result.findings.length) console.log("Fix: piagent-update (or node scripts/personal-pi-config.mjs repair)");
134
+ else console.log("PASS: personal Pi configuration has no Piagent conflicts");
135
+ }
136
+ process.exit(command === "check" && result.findings.length ? 1 : 0);
137
+ }
@@ -158,8 +158,16 @@ child.once("error", (error) => {
158
158
  process.exit(1);
159
159
  });
160
160
 
161
+ // A command stopped from outside (a script, an editor, a timeout) stops its
162
+ // child too, rather than leaving a company run going in the background. A
163
+ // terminal's Ctrl+C already reaches the whole process group: the launcher just
164
+ // stays until the child has finished.
165
+ for (const signal of ["SIGTERM", "SIGHUP"]) process.on(signal, () => { try { child.kill(signal); } catch { /* already gone */ } });
166
+ process.on("SIGINT", () => {});
167
+
161
168
  child.once("exit", (code, signal) => {
162
169
  if (signal) {
170
+ process.removeAllListeners(signal);
163
171
  process.kill(process.pid, signal);
164
172
  return;
165
173
  }
@@ -162,6 +162,10 @@ try {
162
162
  // (updating Pi breaks the Agent Watch import), no package-update, catalog or
163
163
  // install-telemetry calls. Search tools were provisioned above.
164
164
  process.env.PI_OFFLINE = '1'; process.env.PI_SKIP_VERSION_CHECK = '1';
165
+ // Pi's exit line names `pi --session-dir …`, which would open this company
166
+ // conversation in personal Pi; name the company Terminal instead.
167
+ const write = process.stdout.write.bind(process.stdout), project = /^[\w./~:-]+$/.test(cwd) ? cwd : `'${cwd.replaceAll("'", "'\\''")}'`;
168
+ process.stdout.write = (chunk, ...rest) => write(typeof chunk === 'string' && chunk.includes('To resume this session:') ? `Mở lại cuộc trò chuyện này: piagent studio --project ${project}, rồi /resume\n` : chunk, ...rest);
165
169
  await new api.InteractiveMode(host, { verbose: false }).run();
166
170
  }
167
171
  }
@@ -70,6 +70,10 @@ const warnings = [];
70
70
  const { piagentInstallations } = await import(pathToFileURL(path.join(platformRoot, "scripts/piagent-installations.mjs")).href);
71
71
  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
+ // Personal Pi loading Piagent twice does not start; old company providers send it to Studio outside the Harness.
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`);
73
77
  const { verifyCapabilityLock } = await import(pathToFileURL(path.join(platformRoot, "packages", "piagent-core", "capabilities", "capability-core.js")).href);
74
78
 
75
79
  function exists(rel) {
@@ -21,6 +21,7 @@ import os from "node:os";
21
21
  import path from "node:path";
22
22
  import { spawnSync } from "node:child_process";
23
23
  import { fileURLToPath } from "node:url";
24
+ import { inspect as inspectPersonalPi } from "./personal-pi-config.mjs";
24
25
 
25
26
  const HELPER_PACKAGE = "@piagent/platform";
26
27
  const HOST_PACKAGE = "@earendil-works/pi-coding-agent";
@@ -430,13 +431,19 @@ function main() {
430
431
  console.log("Pi Agent Platform update");
431
432
  console.log(` helper: ${currentHelper} -> ${targetHelper}${helperNeedsChange ? "" : " (already current)"}`);
432
433
  console.log(` host: ${currentHost ?? "not installed"} -> ${targetHost}${hostNeedsChange ? "" : " (already current)"}`);
433
- console.log(` package: reinstalled from v${targetHelper} as a resolved commit${options.package ? "" : " (skipped)"}`);
434
+ // The Pi package can lag the helper (installed from an older tag), and
435
+ // personal Pi can hold conflicting Piagent or old company entries.
436
+ const personal = inspectPersonalPi(process.env.PI_CODING_AGENT_DIR || path.join(os.homedir(), ".pi", "agent"));
437
+ const installedPackages = personal.packages.map((item) => item.version ?? "unknown");
438
+ const packageNeedsChange = options.package && (options.force || installedPackages.length !== 1 || installedPackages[0] !== targetHelper);
439
+ console.log(` package: ${installedPackages.join(", ") || "not installed"} -> v${targetHelper}${options.package ? packageNeedsChange ? " (reinstalled as a resolved commit)" : " (already current)" : " (skipped)"}`);
440
+ for (const finding of personal.findings) console.log(` personal Pi: ${finding.text}`);
434
441
  if (options.project) {
435
442
  console.log(` project: ${path.resolve(options.project)} (${options.migrate ? "migrate + doctor" : "doctor only"})`);
436
443
  }
437
444
 
438
445
  if (options.check) {
439
- const pending = [helperNeedsChange && "helper", hostNeedsChange && "host"].filter(Boolean);
446
+ const pending = [helperNeedsChange && "helper", hostNeedsChange && "host", packageNeedsChange && "package", personal.findings.length && "personal Pi configuration"].filter(Boolean);
440
447
  console.log(pending.length ? `\nUpdate available: ${pending.join(", ")}.` : "\nEverything is on the target release.");
441
448
  return;
442
449
  }