@piagent/platform 1.43.9 → 1.44.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 (78) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/docs/command-reference-vietnamese.md +8 -2
  3. package/docs/company-dev-commands.md +2 -0
  4. package/docs/distribution-standard.md +6 -6
  5. package/docs/operator-manual-vietnamese.md +7 -7
  6. package/docs/publishing-for-teams.md +2 -0
  7. package/docs/release-install-policy.md +4 -2
  8. package/docs/team-onboarding.md +2 -2
  9. package/docs-site/architecture.html +1 -1
  10. package/docs-site/assets/search-en.json +1 -1
  11. package/docs-site/assets/search-vi.json +1 -1
  12. package/docs-site/benchmark.html +1 -1
  13. package/docs-site/commands.html +1 -1
  14. package/docs-site/context-engine.html +1 -1
  15. package/docs-site/ecosystem.html +1 -1
  16. package/docs-site/en/architecture.html +1 -1
  17. package/docs-site/en/benchmark.html +1 -1
  18. package/docs-site/en/commands.html +1 -1
  19. package/docs-site/en/context-engine.html +1 -1
  20. package/docs-site/en/ecosystem.html +1 -1
  21. package/docs-site/en/index.html +9 -9
  22. package/docs-site/en/mcp-approval.html +1 -1
  23. package/docs-site/en/mcp-auth.html +1 -1
  24. package/docs-site/en/mcp-servers.html +1 -1
  25. package/docs-site/en/mcp.html +1 -1
  26. package/docs-site/en/permissions.html +1 -1
  27. package/docs-site/en/profiles.html +1 -1
  28. package/docs-site/en/quickstart.html +1 -1
  29. package/docs-site/en/runtime.html +1 -1
  30. package/docs-site/en/scope.html +1 -1
  31. package/docs-site/en/security.html +1 -1
  32. package/docs-site/en/team.html +1 -1
  33. package/docs-site/en/update.html +3 -3
  34. package/docs-site/en/whats-new.html +55 -31
  35. package/docs-site/en/windows.html +1 -1
  36. package/docs-site/en/workflows.html +1 -1
  37. package/docs-site/index.html +9 -9
  38. package/docs-site/mcp-approval.html +1 -1
  39. package/docs-site/mcp-auth.html +1 -1
  40. package/docs-site/mcp-servers.html +1 -1
  41. package/docs-site/mcp.html +1 -1
  42. package/docs-site/permissions.html +1 -1
  43. package/docs-site/profiles.html +1 -1
  44. package/docs-site/quickstart.html +1 -1
  45. package/docs-site/releases.json +77 -0
  46. package/docs-site/runtime.html +1 -1
  47. package/docs-site/scope.html +1 -1
  48. package/docs-site/security.html +1 -1
  49. package/docs-site/team.html +1 -1
  50. package/docs-site/update.html +3 -3
  51. package/docs-site/whats-new.html +55 -31
  52. package/docs-site/windows.html +1 -1
  53. package/docs-site/workflows.html +1 -1
  54. package/evals/private-holdout-v1/public-exposure.v2.json +2 -2
  55. package/package.json +4 -1
  56. package/packages/piagent-core/extensions/piagent-guard.ts +3 -0
  57. package/packages/piagent-core/managed/pi-host.mjs +60 -1
  58. package/packages/piagent-core/managed/resource-loader.mjs +3 -2
  59. package/packages/piagent-core/managed/session.mjs +2 -1
  60. package/packages/piagent-core/package.json +1 -1
  61. package/packages/piagent-core/runtime/resources/plugin-skills.mjs +33 -4
  62. package/packages/piagent-core/runtime/terminal/terminal-style.d.mts +7 -0
  63. package/packages/piagent-core/runtime/terminal/terminal-style.mjs +99 -0
  64. package/packages/piagent-core/themes/piagent-contrast.json +78 -0
  65. package/packages/piagent-core/themes/piagent-focus.json +78 -0
  66. package/packages/piagent-core/themes/piagent-ocean.json +78 -0
  67. package/packages/piagent-core/themes/piagent-paper.json +78 -0
  68. package/packages/piagent-webui/gateway/pi-host.ts +13 -1
  69. package/scripts/company-terminal.mjs +6 -2
  70. package/scripts/install-global.sh +13 -1
  71. package/scripts/pi-host.mjs +109 -0
  72. package/scripts/piagent-cli.mjs +17 -6
  73. package/scripts/piagent-dashboard.mjs +2 -2
  74. package/scripts/runtime-advisory-exceptions.json +40 -0
  75. package/scripts/setup.sh +19 -1
  76. package/scripts/skill-sources.mjs +188 -0
  77. package/scripts/uninstall-global.sh +18 -1
  78. package/scripts/update-global.mjs +13 -5
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env node
2
+ // Skill repositories a member adds to Piagent, such as mattpocock/skills.
3
+ //
4
+ // A source is a public GitHub repository of Agent Skills (folders with a
5
+ // SKILL.md). `add` keeps it at one commit under ~/.piagent/skill-sources, so
6
+ // what the agent reads changes only on `update`; the index lists it
7
+ // (~/.piagent/skill-sources.json), and plugin-skills.mjs offers its skills to
8
+ // personal Pi, the company Terminal and the dashboard, as it does a Claude
9
+ // plugin's. A repository with a plugin manifest (.claude-plugin/plugin.json)
10
+ // loads only the skills the manifest publishes. Skills are instructions: a
11
+ // script they bundle runs only through the session's own tools and approvals.
12
+ //
13
+ // piagent skills add <owner/repo> [--ref <branch|tag|commit>]
14
+ // piagent skills list [--json]
15
+ // piagent skills update [<owner/repo>]
16
+ // piagent skills remove <owner/repo>
17
+ import { spawnSync } from 'node:child_process';
18
+ import fs from 'node:fs';
19
+ import os from 'node:os';
20
+ import path from 'node:path';
21
+ import { fileURLToPath } from 'node:url';
22
+ import { skillSourcePlugin, skillSourcesIndex, skillSourcesRoot } from '../packages/piagent-core/runtime/resources/plugin-skills.mjs';
23
+
24
+ // Short names for sources the team recommends.
25
+ export const KNOWN_SOURCES = { mattpocock: 'mattpocock/skills', 'matt-pocock': 'mattpocock/skills' };
26
+
27
+ function usage() {
28
+ return `Usage:
29
+ piagent skills add <owner/repo> [--ref <branch|tag|commit>]
30
+ Add a GitHub skill repository, kept at one commit
31
+ (for example: piagent skills add mattpocock/skills)
32
+ piagent skills list [--json] Skill sources and the skills each one adds
33
+ piagent skills update [<owner/repo>]
34
+ Move a source (or all) to the newest commit of its ref
35
+ piagent skills remove <owner/repo>
36
+ Stop reading a source and delete its copy
37
+
38
+ Skills from a source appear in Pi as /skill:<name>, in the company Terminal and
39
+ in the dashboard, after the next /reload or new conversation.
40
+ `;
41
+ }
42
+
43
+ // owner/repo from what the member typed: owner/repo, a github.com URL, or a known name.
44
+ export function parseRepository(value) {
45
+ const typed = KNOWN_SOURCES[String(value ?? '').trim().toLowerCase()] ?? String(value ?? '').trim();
46
+ const match = /^(?:https:\/\/github\.com\/|git:github\.com\/|github\.com\/)?([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))\/([A-Za-z0-9._-]{1,100}?)(?:\.git)?\/?$/.exec(typed);
47
+ if (!match || match[2] === '.' || match[2] === '..') return null;
48
+ return `${match[1]}/${match[2]}`;
49
+ }
50
+
51
+ const validRef = (ref) => typeof ref === 'string' && /^[A-Za-z0-9][A-Za-z0-9._/-]{0,199}$/.test(ref) && !ref.includes('..');
52
+
53
+ export function readIndex(home = os.homedir()) {
54
+ try {
55
+ const value = JSON.parse(fs.readFileSync(skillSourcesIndex(home), 'utf8'));
56
+ return { schema_version: 1, sources: Array.isArray(value?.sources) ? value.sources : [] };
57
+ } catch { return { schema_version: 1, sources: [] }; }
58
+ }
59
+
60
+ function writeIndex(index, home) {
61
+ const file = skillSourcesIndex(home);
62
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
63
+ const temporary = `${file}.${process.pid}.tmp`;
64
+ fs.writeFileSync(temporary, `${JSON.stringify(index, null, 2)}\n`, { mode: 0o600 });
65
+ fs.renameSync(temporary, file);
66
+ }
67
+
68
+ function git(args, options = {}) {
69
+ const result = spawnSync('git', args, { encoding: 'utf8', timeout: 120_000, env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }, ...options });
70
+ if (result.error) throw new Error(`git could not start: ${result.error.message}`);
71
+ if (result.status !== 0) throw new Error((result.stderr || result.stdout || `git ${args[0]} failed`).trim().split('\n').at(-1));
72
+ return result.stdout.trim();
73
+ }
74
+
75
+ // The repository at `ref` (default branch when none), copied without its
76
+ // history into <root>/github.com/<owner>/<repo>/<commit>. Returns the entry.
77
+ export function fetchSource(repo, { ref, home = os.homedir(), runGit = git } = {}) {
78
+ const url = `https://github.com/${repo}.git`, root = skillSourcesRoot(home);
79
+ const parent = path.join(root, 'github.com', ...repo.split('/'));
80
+ fs.mkdirSync(parent, { recursive: true, mode: 0o700 });
81
+ const staging = fs.mkdtempSync(path.join(parent, '.incoming-'));
82
+ try {
83
+ const checkout = path.join(staging, 'checkout');
84
+ if (ref && /^[0-9a-f]{40}$/i.test(ref)) {
85
+ fs.mkdirSync(checkout);
86
+ runGit(['init', '-q', checkout]);
87
+ runGit(['-C', checkout, 'fetch', '-q', '--depth', '1', url, ref]);
88
+ runGit(['-C', checkout, 'checkout', '-q', 'FETCH_HEAD']);
89
+ } else {
90
+ runGit(['clone', '-q', '--depth', '1', ...(ref ? ['--branch', ref] : []), url, checkout]);
91
+ }
92
+ const commit = runGit(['-C', checkout, 'rev-parse', 'HEAD']);
93
+ if (!/^[0-9a-f]{40}$/.test(commit)) throw new Error('could not read the commit');
94
+ fs.rmSync(path.join(checkout, '.git'), { recursive: true, force: true });
95
+ const target = path.join(parent, commit);
96
+ if (!fs.existsSync(target)) fs.renameSync(checkout, target);
97
+ return { repo, ref: ref ?? null, commit, path: path.relative(root, target), added_at: new Date().toISOString() };
98
+ } finally { fs.rmSync(staging, { recursive: true, force: true }); }
99
+ }
100
+
101
+ // The skills a source adds, as the agent will see them.
102
+ export function sourceSkills(entry, home = os.homedir()) {
103
+ const found = skillSourcePlugin(path.join(skillSourcesRoot(home), entry.path), entry.repo);
104
+ if (!found) return [];
105
+ const names = [];
106
+ const walk = (dir, depth) => {
107
+ if (depth > 4 || names.length >= 400) return;
108
+ if (fs.existsSync(path.join(dir, 'SKILL.md'))) { names.push(path.basename(dir)); return; }
109
+ let entries = [];
110
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
111
+ for (const child of entries) if (child.isDirectory() && !child.name.startsWith('.') && child.name !== 'node_modules') walk(path.join(dir, child.name), depth + 1);
112
+ };
113
+ for (const dir of found.skills) walk(dir, 0);
114
+ return [...new Set(names)].sort();
115
+ }
116
+
117
+ function removeCopy(entry, home) {
118
+ const root = skillSourcesRoot(home), dir = path.resolve(root, entry.path);
119
+ if (dir.startsWith(root + path.sep)) fs.rmSync(dir, { recursive: true, force: true });
120
+ }
121
+
122
+ function add(repo, { ref, home }) {
123
+ const index = readIndex(home), existing = index.sources.find((item) => item.repo === repo);
124
+ const entry = fetchSource(repo, { ref: ref ?? existing?.ref ?? undefined, home });
125
+ const skills = sourceSkills(entry, home);
126
+ if (!skills.length) {
127
+ removeCopy(entry, home);
128
+ throw new Error(`${repo} has no skill (no SKILL.md under the folders it publishes)`);
129
+ }
130
+ if (existing && existing.path !== entry.path) removeCopy(existing, home);
131
+ index.sources = [...index.sources.filter((item) => item.repo !== repo), { ...entry, name: repo }];
132
+ writeIndex(index, home);
133
+ return { entry, skills, replaced: existing?.commit ?? null };
134
+ }
135
+
136
+ function main(argv, home = os.homedir()) {
137
+ const [command, ...rest] = argv;
138
+ const flag = (name) => { const at = rest.indexOf(name); return at >= 0 ? rest[at + 1] : undefined; };
139
+ const positional = rest.filter((value, at) => !value.startsWith('--') && !(at > 0 && rest[at - 1] === '--ref'));
140
+ if ([undefined, 'help', '--help', '-h'].includes(command)) { process.stdout.write(usage()); return 0; }
141
+ try {
142
+ if (command === 'list') {
143
+ const sources = readIndex(home).sources.map((entry) => ({ ...entry, skills: sourceSkills(entry, home) }));
144
+ if (rest.includes('--json')) { process.stdout.write(`${JSON.stringify({ sources }, null, 2)}\n`); return 0; }
145
+ if (!sources.length) { process.stdout.write('No skill source yet. Add one: piagent skills add mattpocock/skills\n'); return 0; }
146
+ for (const entry of sources) process.stdout.write(`${entry.repo} @ ${entry.commit.slice(0, 12)}${entry.ref ? ` (${entry.ref})` : ''}: ${entry.skills.length} skill(s)\n ${entry.skills.join(', ')}\n`);
147
+ return 0;
148
+ }
149
+ if (command === 'add') {
150
+ const repo = parseRepository(positional[0]), ref = flag('--ref');
151
+ if (!repo) throw new Error('name a GitHub repository as owner/repo, for example mattpocock/skills');
152
+ if (ref !== undefined && !validRef(ref)) throw new Error('--ref needs a branch, tag or commit');
153
+ const { entry, skills, replaced } = add(repo, { ref, home });
154
+ process.stdout.write(`${replaced && replaced !== entry.commit ? 'Updated' : 'Added'} ${repo} @ ${entry.commit.slice(0, 12)}: ${skills.length} skill(s)\n ${skills.join(', ')}\n`);
155
+ process.stdout.write('Use them as /skill:<name> after /reload in Pi, or in a new company conversation.\n');
156
+ return 0;
157
+ }
158
+ if (command === 'update') {
159
+ const only = positional[0] ? parseRepository(positional[0]) : null;
160
+ if (positional[0] && !only) throw new Error('name a GitHub repository as owner/repo');
161
+ const targets = readIndex(home).sources.filter((entry) => !only || entry.repo === only);
162
+ if (!targets.length) throw new Error(only ? `${only} is not a skill source` : 'no skill source to update');
163
+ for (const entry of targets) {
164
+ const { entry: next, skills } = add(entry.repo, { ref: entry.ref ?? undefined, home });
165
+ process.stdout.write(`${entry.repo}: ${entry.commit.slice(0, 12)} -> ${next.commit.slice(0, 12)} (${skills.length} skill(s))\n`);
166
+ }
167
+ return 0;
168
+ }
169
+ if (command === 'remove') {
170
+ const repo = parseRepository(positional[0]);
171
+ if (!repo) throw new Error('name a GitHub repository as owner/repo');
172
+ const index = readIndex(home), entry = index.sources.find((item) => item.repo === repo);
173
+ if (!entry) throw new Error(`${repo} is not a skill source`);
174
+ removeCopy(entry, home);
175
+ index.sources = index.sources.filter((item) => item.repo !== repo);
176
+ writeIndex(index, home);
177
+ process.stdout.write(`Removed ${repo}.\n`);
178
+ return 0;
179
+ }
180
+ } catch (error) {
181
+ process.stderr.write(`FAIL: ${error.message}\n`);
182
+ return 1;
183
+ }
184
+ process.stderr.write(usage());
185
+ return 2;
186
+ }
187
+
188
+ if (process.argv[1] && fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) process.exitCode = main(process.argv.slice(2), os.homedir());
@@ -212,7 +212,24 @@ fi
212
212
 
213
213
  if [[ "$WITH_HOST" == true ]]; then
214
214
  echo
215
- if command -v npm >/dev/null 2>&1; then
215
+ # Piagent's private host (kept beside a member's own, newer pi) is Piagent's to remove.
216
+ PRIVATE_HOSTS="${HOME}/.piagent/pi-host"
217
+ if [[ -d "$PRIVATE_HOSTS" ]]; then
218
+ echo "Removing Piagent's private Pi host:"
219
+ run_cmd rm -rf "$PRIVATE_HOSTS"
220
+ fi
221
+ # A pi newer than the pinned release is the member's own: it stays.
222
+ own_pi_version=""
223
+ if command -v pi >/dev/null 2>&1 && [[ "$(pi --version 2>/dev/null || true)" =~ ([0-9]+\.[0-9]+\.[0-9]+([+-][0-9A-Za-z.-]+)?) ]]; then
224
+ own_pi_version="${BASH_REMATCH[1]}"
225
+ fi
226
+ own_pi_plan=""
227
+ if [[ -n "$own_pi_version" ]]; then
228
+ own_pi_plan="$(node --input-type=module -e 'const { hostPlan } = await import(process.argv[1]); process.stdout.write(hostPlan(process.argv[2]));' "$ROOT/packages/piagent-core/managed/pi-host.mjs" "$own_pi_version" 2>/dev/null || true)"
229
+ fi
230
+ if [[ "$own_pi_plan" == "private" ]]; then
231
+ echo "Pi $own_pi_version is your own (newer than the release Piagent pins): left in place."
232
+ elif command -v npm >/dev/null 2>&1; then
216
233
  echo "Uninstalling Pi host:"
217
234
  HOST_PREFIX="$(npm_prefix_override)"
218
235
  if [[ -n "$HOST_PREFIX" ]]; then
@@ -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 { hostPlan, installPrivateHost, privateHostRoot } from "../packages/piagent-core/managed/pi-host.mjs";
25
26
 
26
27
  const HELPER_PACKAGE = "@piagent/platform";
27
28
  const HOST_PACKAGE = "@earendil-works/pi-coding-agent";
@@ -426,11 +427,15 @@ function main() {
426
427
 
427
428
  // npm exec is temporary even when its version already matches the target.
428
429
  const helperNeedsChange = options.force || currentHelper !== targetHelper || isNpxCacheRoot(platformRoot);
429
- const hostNeedsChange = options.host && (options.force || currentHost !== targetHost);
430
+ // A newer pi is the member's own: kept, with the target host installed privately.
431
+ const ownHost = hostPlan(currentHost, targetHost) === "private";
432
+ const hostNeedsChange = options.host && (ownHost ? !privateHostRoot(targetHost) : options.force || currentHost !== targetHost);
430
433
 
431
434
  console.log("Pi Agent Platform update");
432
435
  console.log(` helper: ${currentHelper} -> ${targetHelper}${helperNeedsChange ? "" : " (already current)"}`);
433
- console.log(` host: ${currentHost ?? "not installed"} -> ${targetHost}${hostNeedsChange ? "" : " (already current)"}`);
436
+ console.log(ownHost
437
+ ? ` host: your pi ${currentHost} stays; Piagent's private Pi -> ${targetHost}${hostNeedsChange ? "" : " (already current)"}`
438
+ : ` host: ${currentHost ?? "not installed"} -> ${targetHost}${hostNeedsChange ? "" : " (already current)"}`);
434
439
  // The Pi package can lag the helper (installed from an older tag), and
435
440
  // personal Pi can hold conflicting Piagent or old company entries.
436
441
  const personal = inspectPersonalPi(process.env.PI_CODING_AGENT_DIR || path.join(os.homedir(), ".pi", "agent"));
@@ -473,7 +478,7 @@ function main() {
473
478
  // here rather than trusted from before the move.
474
479
  let effectiveHost = currentHost;
475
480
  let effectiveHostNeedsChange = hostNeedsChange;
476
- if (npmTarget.bin) {
481
+ if (npmTarget.bin && !ownHost) {
477
482
  const activeHost = installedHostVersion();
478
483
  if (activeHost !== currentHost) {
479
484
  effectiveHost = activeHost;
@@ -484,14 +489,17 @@ function main() {
484
489
  }
485
490
 
486
491
  // Refuse an impossible package installation before replacing either global.
487
- if (options.package && !options.host && effectiveHost !== targetHost) {
492
+ if (options.package && !options.host && effectiveHost !== targetHost && !(ownHost && privateHostRoot(targetHost))) {
488
493
  fail(`--no-host cannot install this Pi package: active Pi is ${effectiveHost ?? "missing"}, but v${targetHelper} requires ${targetHost}. Rerun without --no-host.`);
489
494
  }
490
495
 
491
496
  // Host first: piagent-install refuses to run against a host that does not match
492
497
  // the version its package.json pins, so a helper installed ahead of the host
493
498
  // would fail on its very next step.
494
- if (effectiveHostNeedsChange) {
499
+ if (effectiveHostNeedsChange && ownHost) {
500
+ try { installPrivateHost({ version: targetHost, dryRun: options.dryRun, log: (line) => console.log(line.startsWith("DRY RUN") ? line : `run: ${line}`) }); }
501
+ catch { fail(`could not install the private ${HOST_PACKAGE}@${targetHost}`); }
502
+ } else if (effectiveHostNeedsChange) {
495
503
  const args = npmInstallArgs(`${HOST_PACKAGE}@${targetHost}`, npmTarget);
496
504
  describeStep(options.dryRun, "npm", args);
497
505
  if (!options.dryRun && !run("npm", args).ok) fail(`could not install ${HOST_PACKAGE}@${targetHost}`);