@piagent/platform 1.1.9 → 1.2.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 (81) hide show
  1. package/CHANGELOG.md +175 -1
  2. package/README.md +2 -2
  3. package/docs/command-reference-vietnamese.md +19 -5
  4. package/docs/distribution-standard.md +7 -7
  5. package/docs/mcp-and-tools.md +154 -2
  6. package/docs/model-options.md +10 -8
  7. package/docs/operator-manual-vietnamese.md +25 -13
  8. package/docs/publishing-for-teams.md +2 -0
  9. package/docs/quickstart-vietnamese.md +2 -2
  10. package/docs/release-install-policy.md +6 -6
  11. package/docs/security-threat-model.md +4 -3
  12. package/docs/team-onboarding.md +4 -4
  13. package/docs/usage-observability.md +29 -0
  14. package/docs/vercel-docs-site.md +34 -4
  15. package/docs-site/architecture.html +152 -0
  16. package/docs-site/assets/docs.css +1001 -0
  17. package/docs-site/assets/docs.js +84 -0
  18. package/docs-site/commands.html +203 -0
  19. package/docs-site/content/architecture.html +29 -0
  20. package/docs-site/content/commands.html +80 -0
  21. package/docs-site/content/index.html +80 -0
  22. package/docs-site/content/mcp-approval.html +133 -0
  23. package/docs-site/content/mcp-auth.html +76 -0
  24. package/docs-site/content/mcp-servers.html +111 -0
  25. package/docs-site/content/mcp.html +120 -0
  26. package/docs-site/content/permissions.html +54 -0
  27. package/docs-site/content/profiles.html +68 -0
  28. package/docs-site/content/quickstart.html +144 -0
  29. package/docs-site/content/runtime.html +45 -0
  30. package/docs-site/content/scope.html +71 -0
  31. package/docs-site/content/security.html +51 -0
  32. package/docs-site/content/team.html +23 -0
  33. package/docs-site/content/workflows.html +47 -0
  34. package/docs-site/index.html +131 -1534
  35. package/docs-site/mcp-approval.html +256 -0
  36. package/docs-site/mcp-auth.html +199 -0
  37. package/docs-site/mcp-servers.html +234 -0
  38. package/docs-site/mcp.html +243 -0
  39. package/docs-site/permissions.html +177 -0
  40. package/docs-site/profiles.html +191 -0
  41. package/docs-site/quickstart.html +267 -0
  42. package/docs-site/runtime.html +168 -0
  43. package/docs-site/scope.html +194 -0
  44. package/docs-site/security.html +174 -0
  45. package/docs-site/team.html +142 -0
  46. package/docs-site/workflows.html +170 -0
  47. package/package.json +4 -2
  48. package/packages/piagent-core/capabilities/capability-core.js +58 -5
  49. package/packages/piagent-core/capabilities/capability-sources.js +73 -12
  50. package/packages/piagent-core/extensions/document-intake.ts +5 -5
  51. package/packages/piagent-core/extensions/piagent-guard.ts +432 -2
  52. package/packages/piagent-core/extensions/policy-core.js +1586 -98
  53. package/packages/piagent-core/mcp/mcp-approval-store.js +216 -0
  54. package/packages/piagent-core/mcp/mcp-auth-readiness.js +225 -0
  55. package/packages/piagent-core/mcp/mcp-command-actions.js +400 -0
  56. package/packages/piagent-core/mcp/mcp-config-layers.js +466 -0
  57. package/packages/piagent-core/mcp/mcp-import-kinds.js +240 -0
  58. package/packages/piagent-core/mcp/mcp-server-catalog.js +150 -0
  59. package/packages/piagent-core/mcp/mcp-server-entry.js +293 -0
  60. package/packages/piagent-core/mcp/mcp-session-view.js +236 -0
  61. package/packages/piagent-core/mcp/mcp-tool-naming.js +90 -0
  62. package/packages/piagent-core/package.json +7 -3
  63. package/packages/piagent-core/prompts/model-options.md +8 -8
  64. package/scripts/build-docs-site.mjs +421 -0
  65. package/scripts/configure-model-scope.sh +21 -5
  66. package/scripts/configure-subagents.sh +18 -5
  67. package/scripts/init-project.sh +15 -8
  68. package/scripts/install-global.sh +5 -5
  69. package/scripts/mcp-manage.mjs +656 -0
  70. package/scripts/pi-model-catalog.sh +11 -1
  71. package/scripts/pi-session-stats.sh +27 -1
  72. package/scripts/pi-usage-history.mjs +567 -0
  73. package/scripts/piagent-cli.mjs +1 -1
  74. package/scripts/preview-docs-site.mjs +88 -0
  75. package/scripts/quality-benchmark.sh +37 -9
  76. package/scripts/setup.sh +23 -15
  77. package/scripts/team-doctor.sh +18 -2
  78. package/scripts/uninstall-global.sh +10 -4
  79. package/scripts/verify-local.sh +65 -19
  80. package/scripts/verify-release-identity.mjs +37 -2
  81. package/scripts/configure-mcp.sh +0 -349
@@ -0,0 +1,236 @@
1
+ import {
2
+ SCOPES,
3
+ collectServers,
4
+ repositoryImportDeclarations,
5
+ unreadableLayers,
6
+ unverifiableMcpConfig
7
+ } from "./mcp-config-layers.js";
8
+ import { approvalState } from "./mcp-approval-store.js";
9
+ import { maskServerEntry, maskUrlCredentials } from "./mcp-server-entry.js";
10
+ import { evaluateServerReadiness } from "./mcp-auth-readiness.js";
11
+ import { CATALOG_PREREQUISITES } from "./mcp-server-catalog.js";
12
+
13
+ // What the terminal CLI and the in-session `/piagent-mcp` command both show.
14
+ //
15
+ // The same question gets asked from two places — a shell prompt and a Pi
16
+ // session — and two answers that disagree about whether a server is usable is
17
+ // worse than either answer alone. So the reading, the states and the wording
18
+ // live here once, and both surfaces print what this returns.
19
+ //
20
+ // Read-only by construction: nothing here writes a config or an approval.
21
+
22
+ /** @typedef {import("./mcp-auth-readiness.js").ReadinessState} ReadinessState */
23
+
24
+ export class McpViewError extends Error {}
25
+
26
+ /**
27
+ * The stdio command line, or the remote URL, shortened for a table cell.
28
+ * @param {Record<string, unknown>} entry
29
+ * @param {number} [limit]
30
+ * @returns {string}
31
+ */
32
+ export function describeTarget(entry, limit = 56) {
33
+ // Masked here as well as in the detail view. This is the string that goes in
34
+ // the `list` table, which is the output most likely to be screenshotted.
35
+ if (typeof entry.url === "string") return String(maskUrlCredentials(entry.url));
36
+ const argv = [entry.command, ...(Array.isArray(entry.args) ? entry.args : [])].filter((item) => typeof item === "string");
37
+ const text = argv.join(" ");
38
+ return text.length > limit ? `${text.slice(0, limit - 3)}...` : text;
39
+ }
40
+
41
+ /**
42
+ * Every server visible from a project, with its readiness resolved.
43
+ * @param {{projectPath: string, scopes?: string[]}} options
44
+ */
45
+ export function collectServerRows(options) {
46
+ const { projectPath, scopes } = options;
47
+ if (scopes) {
48
+ for (const scope of scopes) {
49
+ if (!SCOPES.includes(scope)) throw new McpViewError(`unsupported scope: ${scope}`);
50
+ }
51
+ }
52
+ return collectServers({ projectPath, scopes }).map((server) => ({
53
+ ...server,
54
+ transport: typeof server.entry.url === "string" ? "http" : "stdio",
55
+ target: describeTarget(server.entry),
56
+ readiness: evaluateServerReadiness(server, { projectPath })
57
+ }));
58
+ }
59
+
60
+ /**
61
+ * Servers that will not answer a tool call. `oauth` is excluded: it may already
62
+ * be signed in, and this cannot tell.
63
+ * @param {ReturnType<typeof collectServerRows>} rows
64
+ */
65
+ export function blockedRows(rows) {
66
+ return rows.filter((row) => row.readiness.state !== "ready" && row.readiness.state !== "oauth");
67
+ }
68
+
69
+ /**
70
+ * Everything any surface needs to answer "what is MCP doing here right now".
71
+ *
72
+ * The three surfaces — the guard, `/piagent-mcp`, and the terminal CLI — used to
73
+ * assemble this themselves out of four separate calls, and each one picked a
74
+ * different subset. That is how `list` came to print "No MCP servers configured"
75
+ * and exit 0 in a session where the guard was refusing every tool call: the
76
+ * listing was reading rows, and the thing that made the answer wrong was not a
77
+ * row. Everything that decides whether MCP works is gathered here once.
78
+ *
79
+ * `blockingConfig` is the part that outranks per-server state. While any of it
80
+ * stands, no server is usable whatever its own readiness says, so a caller that
81
+ * reports readiness without reporting this is giving an answer that is locally
82
+ * true and globally wrong.
83
+ *
84
+ * @param {{projectPath: string, scopes?: string[]}} options
85
+ */
86
+ export function sessionMcpState(options) {
87
+ const { projectPath, scopes } = options;
88
+ const rows = collectServerRows({ projectPath, scopes });
89
+ const blockingConfig = unverifiableMcpConfig({ projectPath });
90
+ return {
91
+ rows,
92
+ blocked: blockedRows(rows),
93
+ blockingConfig,
94
+ unreadable: unreadableLayers({ projectPath }),
95
+ imports: repositoryImportDeclarations({ projectPath }),
96
+ // One boolean rather than each caller re-deriving the rule. `list --json`
97
+ // read `servers: []` as success while this was true.
98
+ usable: blockingConfig.length === 0
99
+ };
100
+ }
101
+
102
+ /**
103
+ * One named server, resolved to a single scope. `listHint` is the caller's own
104
+ * way of listing servers, so the error names a command the reader can actually
105
+ * run from where they are.
106
+ *
107
+ * @param {{projectPath: string, name: string, scope?: string, listHint?: string, scopeHint?: string}} options
108
+ */
109
+ export function describeServer(options) {
110
+ const { projectPath, name, scope, listHint, scopeHint = "--scope" } = options;
111
+ const matches = collectServerRows({ projectPath, scopes: scope ? [scope] : undefined })
112
+ .filter((row) => row.name === name);
113
+ if (matches.length === 0) {
114
+ throw new McpViewError(scope
115
+ ? `no server named ${name} in ${scope} scope`
116
+ : `no server named ${name}.${listHint ? ` Run \`${listHint}\` to see what is configured.` : ""}`);
117
+ }
118
+ if (matches.length > 1) {
119
+ throw new McpViewError(
120
+ `${name} is defined in more than one scope (${matches.map((row) => row.scope).join(", ")}). ` +
121
+ `Pass ${scopeHint} to say which one.`
122
+ );
123
+ }
124
+ const row = matches[0];
125
+ // `requiresApproval`, not the scope. A server imported into a `global` config
126
+ // from a repository-relative kind carries the scope of the file that declared
127
+ // the import and still needs a decision here, so keying off the scope showed
128
+ // no approval line for exactly the servers whose approval is least obvious.
129
+ const approval = row.requiresApproval
130
+ ? approvalState({ projectPath, name, entry: row.entry })
131
+ : undefined;
132
+ return { ...row, masked: maskServerEntry(row.entry), approval };
133
+ }
134
+
135
+ /**
136
+ * Readiness remedies are written as terminal commands, because that is where
137
+ * every one of them can be run. Inside a session the same step is a slash
138
+ * command, and telling a reader to shell out for something they can do where
139
+ * they are standing is how a surface ends up feeling harder than it is.
140
+ *
141
+ * @param {string | undefined} remedy
142
+ * @param {boolean} inSession
143
+ * @returns {string | undefined}
144
+ */
145
+ export function remedyFor(remedy, inSession) {
146
+ if (!remedy || !inSession) return remedy;
147
+ return remedy.replace(/(^|[\s`])piagent-mcp /g, "$1/piagent-mcp ");
148
+ }
149
+
150
+ /** Left-aligned columns sized to their contents. */
151
+ function table(headers, rows) {
152
+ const widths = headers.map((header, i) => Math.max(header.length, ...rows.map((cells) => cells[i].length)));
153
+ const line = (cells) => cells.map((cell, i) => cell.padEnd(widths[i])).join(" ").trimEnd();
154
+ return [line(headers), ...rows.map(line)].join("\n");
155
+ }
156
+
157
+ /**
158
+ * @param {ReturnType<typeof collectServerRows>} rows
159
+ * @returns {string}
160
+ */
161
+ export function formatServerTable(rows) {
162
+ if (rows.length === 0) return "";
163
+ return table(
164
+ ["NAME", "SCOPE", "TRANSPORT", "TARGET", "STATE"],
165
+ rows.map((row) => [row.name, row.scope, row.transport, row.target, row.readiness.state])
166
+ );
167
+ }
168
+
169
+ /** Fields worth showing; the rest of an entry is noise in a status view. */
170
+ const DETAIL_FIELDS = ["url", "command", "args", "env", "headers", "auth", "bearerTokenEnvVar"];
171
+
172
+ /**
173
+ * @param {ReturnType<typeof describeServer>} detail
174
+ * @param {{inSession?: boolean}} [options]
175
+ * @returns {string[]}
176
+ */
177
+ export function formatServerDetail(detail, options = {}) {
178
+ const lines = [
179
+ detail.name,
180
+ ` scope: ${detail.scope}`,
181
+ ` file: ${detail.file}`,
182
+ // Only for a server that came through `imports`. Without it the `scope` and
183
+ // `file` lines read as a config this tool can edit, and every write command
184
+ // suggested against them fails or edits the wrong file.
185
+ ...(detail.origin?.startsWith("import:")
186
+ ? [` origin: ${detail.origin} (declared by the ${detail.scope} config; edit ${detail.file} directly)`]
187
+ : []),
188
+ ` transport: ${detail.transport}`
189
+ ];
190
+ for (const key of DETAIL_FIELDS) {
191
+ const item = detail.masked[key];
192
+ if (item === undefined) continue;
193
+ lines.push(` ${key}: ${typeof item === "string" ? item : JSON.stringify(item)}`);
194
+ }
195
+ lines.push(` state: ${detail.readiness.state} (${detail.readiness.detail})`);
196
+ const next = remedyFor(detail.readiness.remedy, options.inSession === true);
197
+ if (next) lines.push(` next: ${next}`);
198
+ if (detail.approval) lines.push(` approval: ${detail.approval.state} (${detail.approval.digest})`);
199
+ return lines;
200
+ }
201
+
202
+ /**
203
+ * The readiness report, one line per server plus the remedy for anything stuck.
204
+ * `dockerRunning` is passed in rather than probed: the probe spawns a process
205
+ * and only the caller knows whether it can afford that.
206
+ *
207
+ * @param {ReturnType<typeof collectServerRows>} rows
208
+ * @param {{dockerRunning?: boolean, rerun?: string, inSession?: boolean}} [options]
209
+ * @returns {{lines: string[], problems: number}}
210
+ */
211
+ export function formatDoctorReport(rows, options = {}) {
212
+ if (rows.length === 0) return { lines: ["No MCP servers configured."], problems: 0 };
213
+
214
+ const lines = [];
215
+ let problems = 0;
216
+ for (const row of rows) {
217
+ const usable = row.readiness.state === "ready" || row.readiness.state === "oauth";
218
+ if (!usable) problems += 1;
219
+ lines.push(`${usable ? "OK " : "NEED"} ${row.name} (${row.scope}): ${row.readiness.detail}`);
220
+ const remedy = remedyFor(row.readiness.remedy, options.inSession === true);
221
+ if (remedy) lines.push(` -> ${remedy}`);
222
+ }
223
+ if (options.dockerRunning === false) {
224
+ problems += 1;
225
+ lines.push("NEED docker: installed but the daemon is not responding");
226
+ lines.push(` -> start Docker Desktop, then re-run ${options.rerun ?? "the check"}`);
227
+ }
228
+ const prerequisites = rows.flatMap((row) => (CATALOG_PREREQUISITES[row.name] ?? []).map((item) => ` ${row.name}: ${item}`));
229
+ if (prerequisites.length > 0) lines.push("", "Pinned servers also expect:", ...prerequisites);
230
+ return { lines, problems };
231
+ }
232
+
233
+ /** True when any configured server runs through the docker CLI. */
234
+ export function usesDocker(rows) {
235
+ return rows.some((row) => row.entry.command === "docker");
236
+ }
@@ -0,0 +1,90 @@
1
+ // How the adapter names the tools it exposes, mirrored here so a tool call can
2
+ // be traced back to the server behind it.
3
+ //
4
+ // The adapter builds a prefix from the server name and joins it to the tool name
5
+ // with an underscore. With the default `toolPrefix: "server"` a server called
6
+ // `github` exposes `github_create_issue`. There is no `mcp__` anywhere in that
7
+ // scheme, which is why a gate written against `mcp__<server>__<tool>` never
8
+ // fired: it was watching for a name shape nothing produces.
9
+ //
10
+ // `none` is the case with no answer. It strips the prefix entirely, so the tool
11
+ // arrives under whatever name the server gave it and carries no evidence of
12
+ // where it came from. Attribution is impossible by construction, which makes the
13
+ // combination of that setting with `directTools` in a repository-carried config
14
+ // something to refuse rather than something to parse.
15
+
16
+ /** Prefix modes the adapter accepts. */
17
+ export const TOOL_PREFIX_MODES = new Set(["server", "none", "short"]);
18
+
19
+ /**
20
+ * The prefix a server's tools carry under one mode, matching the adapter's own
21
+ * derivation. An empty string means the mode erases the origin.
22
+ * @param {string} serverName
23
+ * @param {string} mode
24
+ * @returns {string}
25
+ */
26
+ export function serverPrefix(serverName, mode) {
27
+ if (mode === "none") return "";
28
+ if (mode === "short") {
29
+ const short = serverName.replace(/-?mcp$/i, "").replace(/-/g, "_");
30
+ return short || "mcp";
31
+ }
32
+ return serverName.replace(/-/g, "_");
33
+ }
34
+
35
+ /**
36
+ * Every prefix a server's tools could arrive under. All prefixing modes are
37
+ * covered rather than just the configured one, because the setting lives in the
38
+ * same file as the servers: a repository that wants a different mode can simply
39
+ * declare it, so trusting the declared value to decide what to inspect would
40
+ * hand the choice back to the thing being gated.
41
+ * @param {string} serverName
42
+ * @returns {string[]}
43
+ */
44
+ export function serverPrefixes(serverName) {
45
+ const prefixes = new Set();
46
+ for (const mode of ["server", "short"]) {
47
+ const prefix = serverPrefix(serverName, mode);
48
+ if (prefix) prefixes.add(prefix);
49
+ }
50
+ return [...prefixes];
51
+ }
52
+
53
+ /**
54
+ * Which of the given servers exposes this tool, or undefined when the name
55
+ * carries no prefix belonging to any of them.
56
+ *
57
+ * The longest match wins. Servers named `db` and `db_admin` both prefix
58
+ * `db_admin_query`, and answering `db` there would check the wrong server's
59
+ * approval — the more specific prefix is the one that actually produced it.
60
+ *
61
+ * @param {string} toolName
62
+ * @param {Iterable<string>} serverNames
63
+ * @returns {string|undefined}
64
+ */
65
+ export function attributeDirectTool(toolName, serverNames) {
66
+ if (typeof toolName !== "string" || toolName.length === 0) return undefined;
67
+ let best;
68
+ let bestLength = 0;
69
+ for (const name of serverNames) {
70
+ for (const prefix of serverPrefixes(name)) {
71
+ if (prefix.length <= bestLength) continue;
72
+ if (!toolName.startsWith(`${prefix}_`)) continue;
73
+ best = name;
74
+ bestLength = prefix.length;
75
+ }
76
+ }
77
+ return best;
78
+ }
79
+
80
+ /**
81
+ * Whether a config's settings put tools on the model under names that cannot be
82
+ * traced to a server. Both halves are required: without `directTools` every call
83
+ * goes through the proxy, which names its server explicitly.
84
+ * @param {Record<string, unknown>} settings
85
+ * @returns {boolean}
86
+ */
87
+ export function erasesToolOrigin(settings) {
88
+ if (!settings || typeof settings !== "object") return false;
89
+ return settings.directTools === true && settings.toolPrefix === "none";
90
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piagent/core",
3
- "version": "1.1.9",
3
+ "version": "1.2.1",
4
4
  "type": "module",
5
5
  "private": true,
6
6
  "license": "MIT",
@@ -37,8 +37,12 @@
37
37
  "typebox": "1.1.38"
38
38
  },
39
39
  "peerDependenciesMeta": {
40
- "@earendil-works/pi-coding-agent": { "optional": true },
41
- "typebox": { "optional": true }
40
+ "@earendil-works/pi-coding-agent": {
41
+ "optional": true
42
+ },
43
+ "typebox": {
44
+ "optional": true
45
+ }
42
46
  },
43
47
  "engines": {
44
48
  "node": ">=22.19.0"
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Explain native Pi model selector/scope options for Codex and Claude
2
+ description: Explain native Pi model selector/scope options across model families
3
3
  argument-hint: "[task type, budget, or provider preference]"
4
4
  ---
5
5
 
@@ -18,15 +18,15 @@ Mandatory flow:
18
18
  - `/scoped-models` for cycle scope;
19
19
  - Ctrl+P / Shift+Ctrl+P for model cycling;
20
20
  - Shift+Tab for thinking level.
21
- 4. Include both OpenAI Codex and Claude/Anthropic options unless the user explicitly restricts provider.
22
- 5. Do not limit the answer to one or two legacy examples. Include the current latest-family options for Codex 5.5/5.6 and Claude Sonnet/Opus/Fable where relevant.
21
+ 4. Include both model families — the OpenAI Codex models and the Anthropic Claude models — unless the user explicitly restricts the provider. Name them as model families, never as another tool.
22
+ 5. Do not limit the answer to one or two legacy examples. Include the current latest-family options: the Codex 5.5/5.6 models and the Claude Sonnet/Opus/Fable models, where relevant.
23
23
  6. If the user needs available model versions, recommend `piagent-models`, `/model`, or `pi --list-models`.
24
24
  7. Explain thinking levels as task effort, not as quality guarantee.
25
25
  8. Do not claim token/cost savings without benchmark evidence.
26
26
 
27
27
  Current latest-family catalog to consider:
28
28
 
29
- OpenAI Codex:
29
+ OpenAI Codex models (provider `openai-codex`):
30
30
 
31
31
  - `openai-codex/gpt-5.3-codex-spark`
32
32
  - `openai-codex/gpt-5.4-mini`
@@ -36,7 +36,7 @@ OpenAI Codex:
36
36
  - `openai-codex/gpt-5.6-sol`
37
37
  - `openai-codex/gpt-5.6-terra`
38
38
 
39
- Claude/Anthropic:
39
+ Anthropic Claude models (provider `anthropic`):
40
40
 
41
41
  - `anthropic/claude-haiku-4-5`
42
42
  - `anthropic/claude-sonnet-4-5`
@@ -50,7 +50,7 @@ Claude/Anthropic:
50
50
 
51
51
  Default selector scope seeded by setup:
52
52
 
53
- | Preset | OpenAI Codex | Claude/Anthropic | Use when |
53
+ | Preset | OpenAI Codex model | Anthropic Claude model | Use when |
54
54
  |---|---|---|---|
55
55
  | Fast scout | `openai-codex/gpt-5.4-mini:low` | `anthropic/claude-haiku-4-5:low` | quick reads, simple Q&A |
56
56
  | Balanced implement | `openai-codex/gpt-5.5:medium` | `anthropic/claude-sonnet-5:medium` | normal source tasks |
@@ -63,6 +63,6 @@ Output:
63
63
  - Current model/context if available.
64
64
  - How to open/select/change model in Pi UI.
65
65
  - What `enabledModels`/scope contains.
66
- - Codex family options.
67
- - Claude family options.
66
+ - Codex model family options.
67
+ - Claude model family options.
68
68
  - How to inspect/re-apply model scope from terminal.