@piagent/platform 1.0.1 → 1.1.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.
- package/CHANGELOG.md +52 -23
- package/README.md +78 -333
- package/docs/command-reference-vietnamese.md +47 -4
- package/docs/distribution-standard.md +9 -9
- package/docs/herdr-workflow.md +22 -1
- package/docs/mcp-and-tools.md +27 -3
- package/docs/operator-manual-vietnamese.md +16 -12
- package/docs/publishing-for-teams.md +2 -1
- package/docs/quickstart-vietnamese.md +55 -222
- package/docs/release-install-policy.md +54 -10
- package/docs/security-threat-model.md +1 -1
- package/docs/task-lifecycle-vietnamese.md +139 -0
- package/docs/team-onboarding.md +22 -7
- package/docs-site/index.html +43 -65
- package/package.json +7 -5
- package/packages/piagent-core/README.md +2 -1
- package/packages/piagent-core/extensions/document-intake.ts +436 -0
- package/packages/piagent-core/extensions/guard-types.ts +1 -0
- package/packages/piagent-core/extensions/piagent-guard.ts +120 -0
- package/packages/piagent-core/package.json +2 -2
- package/schemas/project-profile.schema.json +8 -0
- package/scripts/check-published-site.mjs +189 -0
- package/scripts/configure-mcp.sh +34 -1
- package/scripts/init-project.sh +19 -0
- package/scripts/install-global.sh +26 -8
- package/scripts/piagent-cli.mjs +1 -0
- package/scripts/setup.sh +14 -0
- package/scripts/team-doctor.sh +1 -1
- package/scripts/uninstall-global.sh +231 -0
- package/scripts/verify-local.sh +131 -72
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Checks that the published documentation site serves the released version from
|
|
4
|
+
// every address it resolves to.
|
|
5
|
+
//
|
|
6
|
+
// Opening the site in a browser is not this check. A domain with several A
|
|
7
|
+
// records answers from whichever one the resolver picked that second, so one
|
|
8
|
+
// address can be dead for a week while every manual look happens to land on a
|
|
9
|
+
// healthy one. This resolves the name, then asks each address directly with the
|
|
10
|
+
// right Host header and SNI, and reports them separately. A partial outage has
|
|
11
|
+
// to read as a failure, not as an average.
|
|
12
|
+
//
|
|
13
|
+
// A redirect from a pinned address back to the same host is reported rather
|
|
14
|
+
// than followed: that is the shape of a loop, and following it would only
|
|
15
|
+
// re-resolve the name and hide which address caused it.
|
|
16
|
+
|
|
17
|
+
import dns from "node:dns/promises";
|
|
18
|
+
import https from "node:https";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import fs from "node:fs";
|
|
21
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
22
|
+
|
|
23
|
+
const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
24
|
+
const REQUEST_TIMEOUT_MS = 15_000;
|
|
25
|
+
const MAX_BODY_BYTES = 4 * 1024 * 1024;
|
|
26
|
+
|
|
27
|
+
function fail(message) {
|
|
28
|
+
process.stderr.write(`FAIL: ${message}\n`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseArguments(values) {
|
|
33
|
+
const options = { hosts: [], expect: null };
|
|
34
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
35
|
+
const option = values[index];
|
|
36
|
+
if (option === "-h" || option === "--help") {
|
|
37
|
+
process.stdout.write("Usage: node scripts/check-published-site.mjs [--host <name>]... [--expect <text>]\n");
|
|
38
|
+
process.stdout.write("Verify every resolved address of the docs site serves the released version.\n");
|
|
39
|
+
process.stdout.write("Defaults to piagent.io.vn and www.piagent.io.vn, expecting the root package version.\n");
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
const value = values[index + 1];
|
|
43
|
+
if (option === "--host") {
|
|
44
|
+
if (!value || value.startsWith("--")) fail("--host requires a value");
|
|
45
|
+
if (!/^[a-z0-9.-]{1,253}$/i.test(value)) fail(`--host value ${JSON.stringify(value)} is not a hostname`);
|
|
46
|
+
options.hosts.push(value);
|
|
47
|
+
index += 1;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (option === "--expect") {
|
|
51
|
+
if (!value || value.startsWith("--")) fail("--expect requires a value");
|
|
52
|
+
if (options.expect !== null) fail("duplicate option --expect");
|
|
53
|
+
options.expect = value;
|
|
54
|
+
index += 1;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
fail(`unknown option ${option}`);
|
|
58
|
+
}
|
|
59
|
+
if (options.hosts.length === 0) options.hosts = ["piagent.io.vn", "www.piagent.io.vn"];
|
|
60
|
+
if (options.expect === null) {
|
|
61
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(repositoryRoot, "package.json"), "utf8"));
|
|
62
|
+
options.expect = manifest.version;
|
|
63
|
+
}
|
|
64
|
+
return options;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function resolveAddresses(host) {
|
|
68
|
+
const addresses = [];
|
|
69
|
+
for (const [family, resolver] of [[4, dns.resolve4], [6, dns.resolve6]]) {
|
|
70
|
+
try {
|
|
71
|
+
for (const address of await resolver(host)) addresses.push({ address, family });
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (error?.code !== "ENODATA" && error?.code !== "ENOTFOUND") throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return addresses;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function request(host, { address, family }) {
|
|
80
|
+
return new Promise((resolve) => {
|
|
81
|
+
const call = https.request(
|
|
82
|
+
{
|
|
83
|
+
host: address,
|
|
84
|
+
family,
|
|
85
|
+
servername: host,
|
|
86
|
+
port: 443,
|
|
87
|
+
path: "/",
|
|
88
|
+
method: "GET",
|
|
89
|
+
headers: { Host: host, "User-Agent": "piagent-site-check" },
|
|
90
|
+
timeout: REQUEST_TIMEOUT_MS
|
|
91
|
+
},
|
|
92
|
+
(response) => {
|
|
93
|
+
let body = "";
|
|
94
|
+
let bytes = 0;
|
|
95
|
+
response.setEncoding("utf8");
|
|
96
|
+
response.on("data", (chunk) => {
|
|
97
|
+
bytes += Buffer.byteLength(chunk, "utf8");
|
|
98
|
+
if (bytes > MAX_BODY_BYTES) {
|
|
99
|
+
response.destroy();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
body += chunk;
|
|
103
|
+
});
|
|
104
|
+
response.on("end", () => resolve({ status: response.statusCode, location: response.headers.location, body }));
|
|
105
|
+
response.on("error", (error) => resolve({ error: error.message, code: error.code }));
|
|
106
|
+
}
|
|
107
|
+
);
|
|
108
|
+
call.on("timeout", () => {
|
|
109
|
+
call.destroy();
|
|
110
|
+
resolve({ error: `no response within ${REQUEST_TIMEOUT_MS}ms` });
|
|
111
|
+
});
|
|
112
|
+
call.on("error", (error) => resolve({ error: error.message, code: error.code }));
|
|
113
|
+
call.end();
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// A redirect is only acceptable when it leaves this host for another name we are
|
|
118
|
+
// also checking. Pointing back at itself is the loop; pointing off the domain
|
|
119
|
+
// means the released page is not what this address serves.
|
|
120
|
+
export function judge(host, hosts, expect, result, family) {
|
|
121
|
+
// A machine with no route to the address family cannot say anything about the
|
|
122
|
+
// address. Calling that a site failure would be a false positive, and a gate
|
|
123
|
+
// that cries wolf gets ignored, so it is reported as unverified instead.
|
|
124
|
+
if (result.code === "ENETUNREACH" || result.code === "EHOSTUNREACH") {
|
|
125
|
+
return { unverified: `no route from this machine to IPv${family}: ${result.error}` };
|
|
126
|
+
}
|
|
127
|
+
if (result.error) return `unreachable: ${result.error}`;
|
|
128
|
+
if (result.status >= 300 && result.status < 400) {
|
|
129
|
+
let target;
|
|
130
|
+
try {
|
|
131
|
+
target = new URL(result.location ?? "", `https://${host}/`);
|
|
132
|
+
} catch {
|
|
133
|
+
return `HTTP ${result.status} with an unreadable Location header`;
|
|
134
|
+
}
|
|
135
|
+
if (target.hostname === host) return `HTTP ${result.status} redirecting to itself at ${target.href}`;
|
|
136
|
+
if (!hosts.includes(target.hostname)) return `HTTP ${result.status} redirecting off the site to ${target.href}`;
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
if (result.status !== 200) return `HTTP ${result.status}`;
|
|
140
|
+
if (!result.body.includes(expect)) return `HTTP 200 but the page does not mention ${expect}`;
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function main() {
|
|
145
|
+
const options = parseArguments(process.argv.slice(2));
|
|
146
|
+
const failures = [];
|
|
147
|
+
const unverified = [];
|
|
148
|
+
let checked = 0;
|
|
149
|
+
|
|
150
|
+
for (const host of options.hosts) {
|
|
151
|
+
let addresses;
|
|
152
|
+
try {
|
|
153
|
+
addresses = await resolveAddresses(host);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
fail(`${host} did not resolve: ${error instanceof Error ? error.message : String(error)}`);
|
|
156
|
+
}
|
|
157
|
+
if (addresses.length === 0) fail(`${host} resolved to no addresses`);
|
|
158
|
+
for (const entry of addresses) {
|
|
159
|
+
const verdict = judge(host, options.hosts, options.expect, await request(host, entry), entry.family);
|
|
160
|
+
checked += 1;
|
|
161
|
+
if (verdict?.unverified) {
|
|
162
|
+
unverified.push(`${host} at ${entry.address}: ${verdict.unverified}`);
|
|
163
|
+
process.stdout.write(` ???? ${host} ${entry.address} — ${verdict.unverified}\n`);
|
|
164
|
+
} else if (verdict) {
|
|
165
|
+
failures.push(`${host} at ${entry.address}: ${verdict}`);
|
|
166
|
+
process.stdout.write(` FAIL ${host} ${entry.address} — ${verdict}\n`);
|
|
167
|
+
} else {
|
|
168
|
+
process.stdout.write(` ok ${host} ${entry.address}\n`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
for (const entry of unverified) process.stdout.write(`UNVERIFIED: ${entry}\n`);
|
|
174
|
+
|
|
175
|
+
if (failures.length > 0) {
|
|
176
|
+
process.stderr.write(`FAIL: ${failures.length} of ${checked} addresses do not serve ${options.expect}\n`);
|
|
177
|
+
for (const failure of failures) process.stderr.write(` ${failure}\n`);
|
|
178
|
+
process.stderr.write("Each address is a real path users take. Fix the DNS records or the deployment behind them.\n");
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const verified = checked - unverified.length;
|
|
183
|
+
const note = unverified.length > 0 ? ` (${unverified.length} not reachable from this machine)` : "";
|
|
184
|
+
process.stdout.write(`PASS: ${verified} of ${checked} addresses serve ${options.expect}${note}\n`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
188
|
+
await main();
|
|
189
|
+
}
|
package/scripts/configure-mcp.sh
CHANGED
|
@@ -211,6 +211,22 @@ const servers = {
|
|
|
211
211
|
}
|
|
212
212
|
};
|
|
213
213
|
|
|
214
|
+
// Writing a server definition does not make it usable. What each one still
|
|
215
|
+
// needs from the operator is stated here so it can be printed at the moment the
|
|
216
|
+
// definition is written, rather than living only in a description string that
|
|
217
|
+
// shows up under --list.
|
|
218
|
+
const prerequisites = {
|
|
219
|
+
context7: ["optional: export CONTEXT7_API_KEY for a higher documentation quota"],
|
|
220
|
+
"chrome-devtools": ["a local Chrome installation"],
|
|
221
|
+
github: [
|
|
222
|
+
"Docker running",
|
|
223
|
+
"export GITHUB_PERSONAL_ACCESS_TOKEN with the scopes you intend to use"
|
|
224
|
+
],
|
|
225
|
+
playwright: ["browsers installed on first use (npx playwright install)"],
|
|
226
|
+
figma: ["Figma OAuth, completed from the Pi MCP panel on first use"],
|
|
227
|
+
"figma-desktop": ["Figma desktop running with Dev Mode MCP enabled"]
|
|
228
|
+
};
|
|
229
|
+
|
|
214
230
|
const presets = {
|
|
215
231
|
minimal: [],
|
|
216
232
|
docs: ["context7"],
|
|
@@ -227,7 +243,10 @@ const presets = {
|
|
|
227
243
|
if (listOnly) {
|
|
228
244
|
console.log(JSON.stringify({
|
|
229
245
|
presets,
|
|
230
|
-
servers: Object.fromEntries(Object.entries(servers).map(([name, value]) => [
|
|
246
|
+
servers: Object.fromEntries(Object.entries(servers).map(([name, value]) => [
|
|
247
|
+
name,
|
|
248
|
+
{ description: value.description, requires: prerequisites[name] ?? [] }
|
|
249
|
+
]))
|
|
231
250
|
}, null, 2));
|
|
232
251
|
process.exit(0);
|
|
233
252
|
}
|
|
@@ -290,6 +309,19 @@ const report = {
|
|
|
290
309
|
serverCount: Object.keys(current.mcpServers).length
|
|
291
310
|
};
|
|
292
311
|
|
|
312
|
+
// stdout stays machine-readable; the operator note goes to stderr.
|
|
313
|
+
function reportPrerequisites() {
|
|
314
|
+
const lines = [];
|
|
315
|
+
for (const name of presets[presetName]) {
|
|
316
|
+
for (const item of prerequisites[name] ?? []) lines.push(` ${name}: ${item}`);
|
|
317
|
+
}
|
|
318
|
+
if (lines.length === 0) return;
|
|
319
|
+
console.error("");
|
|
320
|
+
console.error("Server definitions are written. Each still needs this before it can connect:");
|
|
321
|
+
for (const line of lines) console.error(line);
|
|
322
|
+
console.error("Servers connect lazily, so nothing above is checked until the first call.");
|
|
323
|
+
}
|
|
324
|
+
|
|
293
325
|
if (dryRun) {
|
|
294
326
|
console.log(JSON.stringify(report, null, 2));
|
|
295
327
|
console.log(output);
|
|
@@ -297,5 +329,6 @@ if (dryRun) {
|
|
|
297
329
|
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
298
330
|
fs.writeFileSync(configPath, output);
|
|
299
331
|
console.log(JSON.stringify(report, null, 2));
|
|
332
|
+
reportPrerequisites();
|
|
300
333
|
}
|
|
301
334
|
NODE
|
package/scripts/init-project.sh
CHANGED
|
@@ -238,6 +238,20 @@ NODE
|
|
|
238
238
|
fi
|
|
239
239
|
fi
|
|
240
240
|
|
|
241
|
+
# Running from an installed package rather than a checkout. The name and
|
|
242
|
+
# version on disk are exactly what a teammate resolves from the registry, so
|
|
243
|
+
# they are a portable source; the install path is not, and project settings
|
|
244
|
+
# are meant to be committed.
|
|
245
|
+
if [[ "$PLATFORM_ROOT" == */node_modules/* ]]; then
|
|
246
|
+
local installed_name installed_version
|
|
247
|
+
installed_name="$(node -e 'const fs=require("node:fs"); process.stdout.write(JSON.parse(fs.readFileSync(process.argv[1], "utf8")).name ?? "");' "$PLATFORM_ROOT/package.json")"
|
|
248
|
+
installed_version="$(node -e 'const fs=require("node:fs"); process.stdout.write(JSON.parse(fs.readFileSync(process.argv[1], "utf8")).version ?? "");' "$PLATFORM_ROOT/package.json")"
|
|
249
|
+
if [[ -n "$installed_name" && "$installed_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([+-][0-9A-Za-z.-]+)?$ ]]; then
|
|
250
|
+
printf 'npm:%s@%s\n' "$installed_name" "$installed_version"
|
|
251
|
+
return
|
|
252
|
+
fi
|
|
253
|
+
fi
|
|
254
|
+
|
|
241
255
|
echo "WARN: No exact package source provided." >&2
|
|
242
256
|
echo "WARN: .pi/settings.json will use local platform path; do not commit it for team use." >&2
|
|
243
257
|
printf '%s\n' "$PLATFORM_ROOT"
|
|
@@ -365,5 +379,10 @@ echo " cd \"$PROJECT_PATH\""
|
|
|
365
379
|
echo " pi"
|
|
366
380
|
echo " /login # first time only"
|
|
367
381
|
echo " /onboard-project # first project-read after model selection"
|
|
382
|
+
echo " /mcp # MCP servers come from the global install, not from this project"
|
|
368
383
|
echo " /context-index # inspect compact project context index"
|
|
369
384
|
echo " /memory-policy # inspect project memory policy when needed"
|
|
385
|
+
echo
|
|
386
|
+
echo "Note: .mcp.json and .pi/mcp.json are written empty on purpose. They exist so this"
|
|
387
|
+
echo "project can add its own servers later; the shared baseline lives in the global"
|
|
388
|
+
echo "config written by piagent-setup. An empty file here does not mean MCP is missing."
|
|
@@ -4,7 +4,7 @@ set -euo pipefail
|
|
|
4
4
|
usage() {
|
|
5
5
|
cat <<'USAGE'
|
|
6
6
|
Usage:
|
|
7
|
-
scripts/install-global.sh [--stable|--dev|--local|--channel <stable|dev|local>] [--version <tag>] [--resolve-tag] [--package-source <source>] [--dry-run] [--with-mcp] [--mcp-preset <preset>] [--with-subagents] [--subagents-preset <preset>] [--with-web-access] [--with-herdr] [--model-scope <preset>]
|
|
7
|
+
scripts/install-global.sh [--stable|--dev|--local|--channel <stable|dev|local>] [--version <tag>] [--resolve-tag] [--package-source <source>] [--dry-run] [--with-mcp|--no-mcp] [--mcp-preset <preset>] [--with-subagents] [--subagents-preset <preset>] [--with-web-access] [--with-herdr] [--model-scope <preset>]
|
|
8
8
|
|
|
9
9
|
Purpose:
|
|
10
10
|
Install the piagent Pi package into the current user's global Pi settings.
|
|
@@ -15,7 +15,7 @@ Package source examples:
|
|
|
15
15
|
# Installs the stable tag after resolving it to a commit SHA.
|
|
16
16
|
|
|
17
17
|
# Pin a specific release:
|
|
18
|
-
scripts/install-global.sh --version v1.0
|
|
18
|
+
scripts/install-global.sh --version v1.1.0 --resolve-tag
|
|
19
19
|
|
|
20
20
|
# Preview the planned install/update without changing user config:
|
|
21
21
|
scripts/install-global.sh --stable --dry-run
|
|
@@ -35,7 +35,10 @@ Notes:
|
|
|
35
35
|
- OAuth is intentionally not automated. Run `pi` then `/login`.
|
|
36
36
|
- Model scope is configured with Pi's native `enabledModels` so users choose via `/model`, Ctrl+L, `/scoped-models`, and Ctrl+P.
|
|
37
37
|
- Herdr integration is optional and modifies user-level Herdr/Pi config.
|
|
38
|
-
- MCP
|
|
38
|
+
- MCP is installed by default with the core preset: Context7 docs, Chrome DevTools, GitHub.
|
|
39
|
+
Skip it with --no-mcp. Writing the config does not start or authenticate anything:
|
|
40
|
+
the GitHub server additionally needs Docker running and GITHUB_PERSONAL_ACCESS_TOKEN
|
|
41
|
+
exported, and Chrome DevTools needs a local Chrome.
|
|
39
42
|
- Subagents preset defaults to safe: compact tool description, bounded concurrency/depth.
|
|
40
43
|
- Web access is optional. Install it only when you want the builtin `researcher` subagent to browse/fetch web sources inside Pi.
|
|
41
44
|
USAGE
|
|
@@ -50,8 +53,9 @@ DEFAULT_REPO_REMOTE_URL="https://github.com/Vt-mmm/piagent.git"
|
|
|
50
53
|
PACKAGE_SOURCE="${PIAGENT_PACKAGE_SOURCE:-}"
|
|
51
54
|
PACKAGE_VERSION="${PIAGENT_PACKAGE_VERSION:-}"
|
|
52
55
|
RELEASE_CHANNEL="${PIAGENT_RELEASE_CHANNEL:-}"
|
|
53
|
-
WITH_MCP=
|
|
56
|
+
WITH_MCP=true
|
|
54
57
|
MCP_PRESET="core"
|
|
58
|
+
MCP_PRESET_EXPLICIT=false
|
|
55
59
|
WITH_SUBAGENTS=false
|
|
56
60
|
SUBAGENTS_PRESET="safe"
|
|
57
61
|
SUBAGENTS_MODEL_SCOPE="none"
|
|
@@ -123,7 +127,7 @@ detect_runtime_surface() {
|
|
|
123
127
|
printf 'native-windows/%s (not release-gated; terminal helpers require Bash semantics)' "$arch_name"
|
|
124
128
|
;;
|
|
125
129
|
*)
|
|
126
|
-
printf '%s/%s (outside v1.0
|
|
130
|
+
printf '%s/%s (outside v1.1.0 release matrix)' "$os_name" "$arch_name"
|
|
127
131
|
;;
|
|
128
132
|
esac
|
|
129
133
|
}
|
|
@@ -131,7 +135,7 @@ detect_runtime_surface() {
|
|
|
131
135
|
warn_runtime_surface_if_needed() {
|
|
132
136
|
local surface="$1"
|
|
133
137
|
case "$surface" in
|
|
134
|
-
*"supported target"*|*"experimental"*|*"not release-gated"*|*"outside v1.0
|
|
138
|
+
*"supported target"*|*"experimental"*|*"not release-gated"*|*"outside v1.1.0 release matrix"*)
|
|
135
139
|
echo "WARN: runtime surface is $surface." >&2
|
|
136
140
|
echo "WARN: For team rollout, run piagent-doctor plus the project smoke/verify suite on this machine before relying on it." >&2
|
|
137
141
|
;;
|
|
@@ -275,9 +279,14 @@ while [[ $# -gt 0 ]]; do
|
|
|
275
279
|
WITH_MCP=true
|
|
276
280
|
shift
|
|
277
281
|
;;
|
|
282
|
+
--no-mcp)
|
|
283
|
+
WITH_MCP=false
|
|
284
|
+
shift
|
|
285
|
+
;;
|
|
278
286
|
--mcp-preset)
|
|
279
287
|
require_value "$1" "${2:-}"
|
|
280
288
|
MCP_PRESET="$2"
|
|
289
|
+
MCP_PRESET_EXPLICIT=true
|
|
281
290
|
shift 2
|
|
282
291
|
;;
|
|
283
292
|
--with-subagents)
|
|
@@ -328,6 +337,13 @@ while [[ $# -gt 0 ]]; do
|
|
|
328
337
|
esac
|
|
329
338
|
done
|
|
330
339
|
|
|
340
|
+
# A preset chosen alongside --no-mcp used to be accepted and then quietly
|
|
341
|
+
# dropped, which reads as "the preset was applied".
|
|
342
|
+
if [[ "$WITH_MCP" == false && "$MCP_PRESET_EXPLICIT" == true ]]; then
|
|
343
|
+
echo "FAIL: --mcp-preset has no effect with --no-mcp. Drop one of them." >&2
|
|
344
|
+
exit 2
|
|
345
|
+
fi
|
|
346
|
+
|
|
331
347
|
require_node_version
|
|
332
348
|
CURRENT_RUNTIME_SURFACE="$(detect_runtime_surface)"
|
|
333
349
|
warn_runtime_surface_if_needed "$CURRENT_RUNTIME_SURFACE"
|
|
@@ -438,7 +454,7 @@ if ! command -v pi >/dev/null 2>&1; then
|
|
|
438
454
|
else
|
|
439
455
|
echo "FAIL: pi is not on PATH." >&2
|
|
440
456
|
echo "Install Pi first, then rerun this script." >&2
|
|
441
|
-
echo "Expected command on npm-based installs: npm install -g --ignore-scripts @earendil-works/pi-coding-agent@0.
|
|
457
|
+
echo "Expected command on npm-based installs: npm install -g --ignore-scripts @earendil-works/pi-coding-agent@0.82.0" >&2
|
|
442
458
|
exit 1
|
|
443
459
|
fi
|
|
444
460
|
else
|
|
@@ -527,6 +543,8 @@ echo " pi"
|
|
|
527
543
|
echo " /login"
|
|
528
544
|
echo " /model # or Ctrl+L: select provider/model from Pi selector"
|
|
529
545
|
echo " /scoped-models # optional: edit Ctrl+P model cycle scope"
|
|
530
|
-
|
|
546
|
+
if [[ "$WITH_MCP" == true ]]; then
|
|
547
|
+
echo " /mcp # inspect MCP servers; authenticate Figma/GitHub only when needed"
|
|
548
|
+
fi
|
|
531
549
|
echo " /subagents-doctor"
|
|
532
550
|
echo " /task Implement <task> # parent may auto-delegate scout/planner/reviewer"
|
package/scripts/piagent-cli.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
const scriptByCommand = {
|
|
7
7
|
"piagent-setup": "scripts/setup.sh",
|
|
8
8
|
"piagent-install": "scripts/install-global.sh",
|
|
9
|
+
"piagent-uninstall": "scripts/uninstall-global.sh",
|
|
9
10
|
"piagent-init": "scripts/init-project.sh",
|
|
10
11
|
"piagent-doctor": "scripts/team-doctor.sh",
|
|
11
12
|
"piagent-benchmark": "scripts/quality-benchmark.sh",
|
package/scripts/setup.sh
CHANGED
|
@@ -244,6 +244,20 @@ NODE
|
|
|
244
244
|
fi
|
|
245
245
|
fi
|
|
246
246
|
|
|
247
|
+
# Running from an installed package rather than a checkout. The name and
|
|
248
|
+
# version on disk are exactly what a teammate resolves from the registry, so
|
|
249
|
+
# they are a portable source; the install path is not, and project settings
|
|
250
|
+
# are meant to be committed.
|
|
251
|
+
if [[ "$PLATFORM_ROOT" == */node_modules/* ]]; then
|
|
252
|
+
local installed_name installed_version
|
|
253
|
+
installed_name="$(node -e 'const fs=require("node:fs"); process.stdout.write(JSON.parse(fs.readFileSync(process.argv[1], "utf8")).name ?? "");' "$PLATFORM_ROOT/package.json")"
|
|
254
|
+
installed_version="$(node -e 'const fs=require("node:fs"); process.stdout.write(JSON.parse(fs.readFileSync(process.argv[1], "utf8")).version ?? "");' "$PLATFORM_ROOT/package.json")"
|
|
255
|
+
if [[ -n "$installed_name" && "$installed_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([+-][0-9A-Za-z.-]+)?$ ]]; then
|
|
256
|
+
printf 'npm:%s@%s\n' "$installed_name" "$installed_version"
|
|
257
|
+
return
|
|
258
|
+
fi
|
|
259
|
+
fi
|
|
260
|
+
|
|
247
261
|
echo "WARN: No exact package source provided." >&2
|
|
248
262
|
echo "WARN: using local platform path; do not commit project .pi/settings.json with this value for team rollout." >&2
|
|
249
263
|
printf '%s\n' "$PLATFORM_ROOT"
|
package/scripts/team-doctor.sh
CHANGED
|
@@ -137,7 +137,7 @@ function runtimeMatrixStatus() {
|
|
|
137
137
|
surface: `${platform}/${arch}`,
|
|
138
138
|
status: "outside-release-matrix",
|
|
139
139
|
teamRolloutReady: false,
|
|
140
|
-
note: "This runtime is outside the documented v1.0
|
|
140
|
+
note: "This runtime is outside the documented v1.1.0 support matrix."
|
|
141
141
|
};
|
|
142
142
|
}
|
|
143
143
|
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Removes what this platform installed, and nothing else.
|
|
5
|
+
#
|
|
6
|
+
# Uninstall is reported before it is performed, for the same reason install is:
|
|
7
|
+
# the two touch shared Pi state that other tools also write to. Anything this
|
|
8
|
+
# script cannot prove it installed is listed for the operator instead of being
|
|
9
|
+
# deleted.
|
|
10
|
+
|
|
11
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
12
|
+
PI_AGENT_DIR="${PI_CODING_AGENT_DIR:-"${HOME}/.pi/agent"}"
|
|
13
|
+
GLOBAL_SETTINGS="$PI_AGENT_DIR/settings.json"
|
|
14
|
+
|
|
15
|
+
APPLY=false
|
|
16
|
+
WITH_ADDONS=false
|
|
17
|
+
WITH_HOST=false
|
|
18
|
+
PROJECT_PATH=""
|
|
19
|
+
|
|
20
|
+
PLATFORM_PACKAGE="@piagent/platform"
|
|
21
|
+
ADDON_PACKAGES=("pi-mcp-adapter" "pi-subagents" "pi-web-access")
|
|
22
|
+
PI_HOST_PACKAGE="@earendil-works/pi-coding-agent"
|
|
23
|
+
|
|
24
|
+
usage() {
|
|
25
|
+
cat <<'USAGE'
|
|
26
|
+
Usage:
|
|
27
|
+
piagent-uninstall [--apply] [--with-addons] [--with-host] [--project <path>]
|
|
28
|
+
|
|
29
|
+
Removes the Pi package this platform installed from Pi's global settings.
|
|
30
|
+
|
|
31
|
+
(default) Dry run. Reports what would be removed and exits 0.
|
|
32
|
+
--apply Perform the removal.
|
|
33
|
+
--with-addons Also remove pi-mcp-adapter, pi-subagents, and pi-web-access.
|
|
34
|
+
--with-host Also uninstall the global Pi Coding Agent host.
|
|
35
|
+
--project <path> Also remove this platform's state from a project.
|
|
36
|
+
|
|
37
|
+
Never removed, at any flag combination: credentials, trust decisions, sessions,
|
|
38
|
+
todos, and project memory. Those are operator data, not platform state.
|
|
39
|
+
USAGE
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
while [[ $# -gt 0 ]]; do
|
|
43
|
+
case "$1" in
|
|
44
|
+
--apply) APPLY=true; shift ;;
|
|
45
|
+
--with-addons) WITH_ADDONS=true; shift ;;
|
|
46
|
+
--with-host) WITH_HOST=true; shift ;;
|
|
47
|
+
--project)
|
|
48
|
+
if [[ $# -lt 2 || -z "${2:-}" ]]; then
|
|
49
|
+
echo "Missing value for --project" >&2
|
|
50
|
+
exit 2
|
|
51
|
+
fi
|
|
52
|
+
PROJECT_PATH="$2"
|
|
53
|
+
shift 2
|
|
54
|
+
;;
|
|
55
|
+
-h|--help) usage; exit 0 ;;
|
|
56
|
+
*)
|
|
57
|
+
echo "Unknown argument: $1" >&2
|
|
58
|
+
usage >&2
|
|
59
|
+
exit 2
|
|
60
|
+
;;
|
|
61
|
+
esac
|
|
62
|
+
done
|
|
63
|
+
|
|
64
|
+
print_cmd() {
|
|
65
|
+
printf '+'
|
|
66
|
+
for arg in "$@"; do printf ' %q' "$arg"; done
|
|
67
|
+
printf '\n'
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
run_cmd() {
|
|
71
|
+
print_cmd "$@"
|
|
72
|
+
if [[ "$APPLY" == true ]]; then
|
|
73
|
+
"$@"
|
|
74
|
+
fi
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# Reads the package sources Pi has registered, so removal targets what is
|
|
78
|
+
# actually installed rather than what this version happens to install today. An
|
|
79
|
+
# older release registered a different source shape, and it still has to come
|
|
80
|
+
# out.
|
|
81
|
+
platform_sources() {
|
|
82
|
+
[[ -f "$GLOBAL_SETTINGS" ]] || return 0
|
|
83
|
+
node --input-type=module - "$GLOBAL_SETTINGS" <<'NODE'
|
|
84
|
+
import fs from "node:fs";
|
|
85
|
+
|
|
86
|
+
let settings;
|
|
87
|
+
try {
|
|
88
|
+
settings = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
89
|
+
} catch {
|
|
90
|
+
process.exit(0);
|
|
91
|
+
}
|
|
92
|
+
const packages = Array.isArray(settings.packages) ? settings.packages : [];
|
|
93
|
+
// Every shape this platform has ever been installed as: the scoped npm
|
|
94
|
+
// package, the Git source, and a local checkout path.
|
|
95
|
+
const owned = /(@piagent\/platform|[/:]piagent(@|$|#)|Vt-mmm\/piagent|pi-company-platform)/;
|
|
96
|
+
for (const entry of packages) {
|
|
97
|
+
if (typeof entry === "string" && owned.test(entry)) process.stdout.write(`${entry}\n`);
|
|
98
|
+
}
|
|
99
|
+
NODE
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
addon_sources() {
|
|
103
|
+
[[ -f "$GLOBAL_SETTINGS" ]] || return 0
|
|
104
|
+
node --input-type=module - "$GLOBAL_SETTINGS" "${ADDON_PACKAGES[@]}" <<'NODE'
|
|
105
|
+
import fs from "node:fs";
|
|
106
|
+
|
|
107
|
+
let settings;
|
|
108
|
+
try {
|
|
109
|
+
settings = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
110
|
+
} catch {
|
|
111
|
+
process.exit(0);
|
|
112
|
+
}
|
|
113
|
+
const names = process.argv.slice(3);
|
|
114
|
+
const packages = Array.isArray(settings.packages) ? settings.packages : [];
|
|
115
|
+
for (const entry of packages) {
|
|
116
|
+
if (typeof entry !== "string") continue;
|
|
117
|
+
if (names.some((name) => entry.includes(name))) process.stdout.write(`${entry}\n`);
|
|
118
|
+
}
|
|
119
|
+
NODE
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
echo "Pi Agent Platform uninstall"
|
|
123
|
+
echo " mode: $([[ "$APPLY" == true ]] && echo apply || echo "dry run")"
|
|
124
|
+
echo " settings: $GLOBAL_SETTINGS"
|
|
125
|
+
echo " addons: $WITH_ADDONS"
|
|
126
|
+
echo " host: $WITH_HOST"
|
|
127
|
+
[[ -n "$PROJECT_PATH" ]] && echo " project: $PROJECT_PATH"
|
|
128
|
+
echo
|
|
129
|
+
|
|
130
|
+
if ! command -v pi >/dev/null 2>&1; then
|
|
131
|
+
echo "WARN: pi is not on PATH; skipping Pi package removal." >&2
|
|
132
|
+
echo "WARN: reinstall the host or remove entries from $GLOBAL_SETTINGS by hand." >&2
|
|
133
|
+
else
|
|
134
|
+
found=false
|
|
135
|
+
while IFS= read -r source; do
|
|
136
|
+
[[ -z "$source" ]] && continue
|
|
137
|
+
found=true
|
|
138
|
+
echo "Removing platform package:"
|
|
139
|
+
run_cmd pi remove "$source"
|
|
140
|
+
done < <(platform_sources)
|
|
141
|
+
if [[ "$found" == false ]]; then
|
|
142
|
+
echo "No platform package found in Pi settings; nothing to remove."
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
if [[ "$WITH_ADDONS" == true ]]; then
|
|
146
|
+
addon_found=false
|
|
147
|
+
while IFS= read -r source; do
|
|
148
|
+
[[ -z "$source" ]] && continue
|
|
149
|
+
addon_found=true
|
|
150
|
+
echo "Removing add-on:"
|
|
151
|
+
run_cmd pi remove "$source"
|
|
152
|
+
done < <(addon_sources)
|
|
153
|
+
if [[ "$addon_found" == false ]]; then
|
|
154
|
+
echo "No pinned add-on found in Pi settings."
|
|
155
|
+
fi
|
|
156
|
+
# The MCP baseline and subagent config are shared files that the operator
|
|
157
|
+
# may have edited after install, so they are reported rather than reset.
|
|
158
|
+
# Install writes the shared global scope; the Pi-global file is the override
|
|
159
|
+
# scope and is listed too, because an operator who used it will look there.
|
|
160
|
+
echo "Left in place, review by hand if no longer wanted:"
|
|
161
|
+
echo " ${XDG_CONFIG_HOME:-"${HOME}/.config"}/mcp/mcp.json # written by install"
|
|
162
|
+
echo " $PI_AGENT_DIR/mcp.json # Pi-global override scope, only if you created it"
|
|
163
|
+
echo " $PI_AGENT_DIR/extensions/subagent/config.json"
|
|
164
|
+
fi
|
|
165
|
+
fi
|
|
166
|
+
|
|
167
|
+
if [[ "$WITH_HOST" == true ]]; then
|
|
168
|
+
echo
|
|
169
|
+
if command -v npm >/dev/null 2>&1; then
|
|
170
|
+
echo "Uninstalling Pi host:"
|
|
171
|
+
run_cmd npm uninstall -g "$PI_HOST_PACKAGE"
|
|
172
|
+
else
|
|
173
|
+
echo "WARN: npm is not on PATH; cannot uninstall $PI_HOST_PACKAGE." >&2
|
|
174
|
+
fi
|
|
175
|
+
fi
|
|
176
|
+
|
|
177
|
+
if [[ -n "$PROJECT_PATH" ]]; then
|
|
178
|
+
echo
|
|
179
|
+
if [[ ! -d "$PROJECT_PATH" ]]; then
|
|
180
|
+
echo "FAIL: project path does not exist: $PROJECT_PATH" >&2
|
|
181
|
+
exit 1
|
|
182
|
+
fi
|
|
183
|
+
PROJECT_PATH="$(cd "$PROJECT_PATH" && pwd)"
|
|
184
|
+
echo "Project state in $PROJECT_PATH:"
|
|
185
|
+
|
|
186
|
+
# Files this platform alone writes and owns.
|
|
187
|
+
for relative in ".pi/piagent-profile.json" ".pi/piagent-profile.lock.json"; do
|
|
188
|
+
[[ -e "$PROJECT_PATH/$relative" ]] && run_cmd rm -f "$PROJECT_PATH/$relative"
|
|
189
|
+
done
|
|
190
|
+
if [[ -d "$PROJECT_PATH/.pi/piagent-state" ]]; then
|
|
191
|
+
run_cmd rm -rf "$PROJECT_PATH/.pi/piagent-state"
|
|
192
|
+
fi
|
|
193
|
+
|
|
194
|
+
# settings.json belongs to Pi, not to this platform. Only the package entry
|
|
195
|
+
# pointing here comes out; everything else the operator configured stays.
|
|
196
|
+
if [[ -f "$PROJECT_PATH/.pi/settings.json" ]]; then
|
|
197
|
+
echo "+ drop platform package entry from $PROJECT_PATH/.pi/settings.json"
|
|
198
|
+
if [[ "$APPLY" == true ]]; then
|
|
199
|
+
node --input-type=module - "$PROJECT_PATH/.pi/settings.json" <<'NODE'
|
|
200
|
+
import fs from "node:fs";
|
|
201
|
+
|
|
202
|
+
const target = process.argv[2];
|
|
203
|
+
const settings = JSON.parse(fs.readFileSync(target, "utf8"));
|
|
204
|
+
if (Array.isArray(settings.packages)) {
|
|
205
|
+
const owned = /(@piagent\/platform|[/:]piagent(@|$|#)|Vt-mmm\/piagent|pi-company-platform)/;
|
|
206
|
+
const kept = settings.packages.filter((entry) => !(typeof entry === "string" && owned.test(entry)));
|
|
207
|
+
if (kept.length > 0) settings.packages = kept;
|
|
208
|
+
else delete settings.packages;
|
|
209
|
+
fs.writeFileSync(target, `${JSON.stringify(settings, null, 2)}\n`);
|
|
210
|
+
}
|
|
211
|
+
NODE
|
|
212
|
+
fi
|
|
213
|
+
fi
|
|
214
|
+
|
|
215
|
+
# Written from templates at init, then edited by whoever used the project.
|
|
216
|
+
# Deleting them would take the operator's work with them.
|
|
217
|
+
echo "Left in place because they carry project content:"
|
|
218
|
+
for relative in "AGENTS.md" "REVIEW_GUIDELINES.md" ".mcp.json" ".pi/settings.json" ".pi/mcp.json" \
|
|
219
|
+
".pi/project-context.md" ".pi/context-index.json" ".pi/tech-stack.json" ".pi/tech-context" ".pi/memory"; do
|
|
220
|
+
[[ -e "$PROJECT_PATH/$relative" ]] && echo " $relative"
|
|
221
|
+
done
|
|
222
|
+
fi
|
|
223
|
+
|
|
224
|
+
echo
|
|
225
|
+
if [[ "$APPLY" == true ]]; then
|
|
226
|
+
echo "Uninstall complete."
|
|
227
|
+
echo "The npm-global helper is separate and stays until you remove it:"
|
|
228
|
+
echo " npm uninstall -g $PLATFORM_PACKAGE"
|
|
229
|
+
else
|
|
230
|
+
echo "Dry run only. Nothing was removed. Re-run with --apply to perform it."
|
|
231
|
+
fi
|