@piagent/platform 1.28.0 → 1.28.2
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 +31 -0
- package/README.md +1 -1
- package/README.vi.md +1 -1
- package/docs/command-reference-vietnamese.md +2 -2
- package/docs/distribution-standard.md +6 -6
- package/docs/languages.json +2 -2
- package/docs/operator-manual-vietnamese.md +7 -7
- package/docs/publishing-for-teams.md +2 -0
- package/docs/release-install-policy.md +2 -2
- package/docs/team-onboarding.md +2 -2
- package/docs-site/architecture.html +1 -1
- package/docs-site/benchmark.html +1 -1
- package/docs-site/commands.html +1 -1
- package/docs-site/content/en/index.html +4 -4
- package/docs-site/content/en/whats-new.html +12 -0
- package/docs-site/content/index.html +4 -4
- package/docs-site/content/whats-new.html +12 -0
- package/docs-site/context-engine.html +1 -1
- package/docs-site/ecosystem.html +1 -1
- package/docs-site/en/architecture.html +1 -1
- package/docs-site/en/benchmark.html +1 -1
- package/docs-site/en/commands.html +1 -1
- package/docs-site/en/context-engine.html +1 -1
- package/docs-site/en/ecosystem.html +1 -1
- package/docs-site/en/index.html +5 -5
- package/docs-site/en/mcp-approval.html +1 -1
- package/docs-site/en/mcp-auth.html +1 -1
- package/docs-site/en/mcp-servers.html +1 -1
- package/docs-site/en/mcp.html +1 -1
- package/docs-site/en/permissions.html +1 -1
- package/docs-site/en/profiles.html +1 -1
- package/docs-site/en/quickstart.html +1 -1
- package/docs-site/en/runtime.html +1 -1
- package/docs-site/en/scope.html +1 -1
- package/docs-site/en/security.html +1 -1
- package/docs-site/en/team.html +1 -1
- package/docs-site/en/whats-new.html +13 -1
- package/docs-site/en/windows.html +1 -1
- package/docs-site/en/workflows.html +1 -1
- package/docs-site/index.html +5 -5
- package/docs-site/mcp-approval.html +1 -1
- package/docs-site/mcp-auth.html +1 -1
- package/docs-site/mcp-servers.html +1 -1
- package/docs-site/mcp.html +1 -1
- package/docs-site/permissions.html +1 -1
- package/docs-site/profiles.html +1 -1
- package/docs-site/quickstart.html +1 -1
- package/docs-site/runtime.html +1 -1
- package/docs-site/scope.html +1 -1
- package/docs-site/security.html +1 -1
- package/docs-site/team.html +1 -1
- package/docs-site/whats-new.html +13 -1
- package/docs-site/windows.html +1 -1
- package/docs-site/workflows.html +1 -1
- package/evals/private-holdout-v1/public-exposure.v2.json +2 -2
- package/package.json +1 -1
- package/packages/piagent-core/extensions/company-session-guard.ts +36 -0
- package/packages/piagent-core/extensions/piagent-guard.ts +2 -0
- package/packages/piagent-core/managed/binding-wait.mjs +66 -0
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-BpNzOP6f.js → MarkdownMessage-BKK5r1UP.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/{index-DWkb1mpZ.js → index-8YNd9KDA.js} +4 -4
- package/packages/piagent-webui/dist/client/index.html +1 -1
- package/packages/piagent-webui/gateway/managed-launch.ts +6 -1
- package/packages/piagent-webui/shared/company-copy.ts +2 -0
- package/scripts/company-terminal.mjs +13 -1
- package/scripts/install-global.sh +11 -0
- package/scripts/personal-pi-config.mjs +137 -0
- package/scripts/piagent-cli.mjs +8 -0
- package/scripts/piagent-studio.mjs +25 -6
- package/scripts/team-doctor.sh +4 -0
- package/scripts/update-global.mjs +9 -2
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<meta name="csp-nonce" content="__PIAGENT_CSP_NONCE__" />
|
|
8
8
|
<meta name="piagent-webui-mode" content="__PIAGENT_WEBUI_MODE__" />
|
|
9
9
|
<title>Piagent WebUI</title>
|
|
10
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
+
<script type="module" crossorigin src="/assets/index-8YNd9KDA.js"></script>
|
|
11
11
|
<link rel="stylesheet" crossorigin href="/assets/index-B5XOKQy-.css">
|
|
12
12
|
</head>
|
|
13
13
|
<body>
|
|
@@ -17,6 +17,7 @@ type Binding = { file: string; profile: string; node: string; entrypoint: string
|
|
|
17
17
|
|
|
18
18
|
const CLEAN_ENV = { PATH: "/usr/bin:/bin", LANG: "en_US.UTF-8", TERM: "xterm-256color", ...interopEnvironment() };
|
|
19
19
|
const READY = /^Agent Watch Auto: ready$/m;
|
|
20
|
+
const BINDING_WAIT = /\[managed-binding-wait\]$/m, BINDING_WAIT_MS = 95_000;
|
|
20
21
|
const sha256 = (file: string) => createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
|
21
22
|
const HEX64 = /^[a-f0-9]{64}$/;
|
|
22
23
|
|
|
@@ -130,8 +131,12 @@ async function start(agentDir: string, home: string, timeoutMs: number): Promise
|
|
|
130
131
|
child.once("exit", () => { exited = true; }); child.once("error", () => { exited = true; }); child.unref();
|
|
131
132
|
} finally { fs.closeSync(fd); }
|
|
132
133
|
try {
|
|
133
|
-
|
|
134
|
+
// Right after an update the entrypoint waits (up to 90 s) for Agent Watch
|
|
135
|
+
// to write the new binding; give it that long before timing out.
|
|
136
|
+
let limit = timeoutMs;
|
|
137
|
+
for (const started = Date.now(); Date.now() - started < limit;) {
|
|
134
138
|
const text = fs.readFileSync(log, "utf8");
|
|
139
|
+
if (limit === timeoutMs && BINDING_WAIT.test(text)) limit = timeoutMs + BINDING_WAIT_MS;
|
|
135
140
|
// The launch may just have pointed this slot at the member's store.
|
|
136
141
|
if (READY.test(text)) return controlSocket(home, pinned.profile);
|
|
137
142
|
if (exited) throw new Error(text.match(/Agent Watch: (managed-[a-z:_-]+)\./)?.[1] ?? "managed-launch-failed");
|
|
@@ -12,6 +12,8 @@ export const LAUNCH_REASONS: Record<string, [string, string]> = {
|
|
|
12
12
|
"managed-key-unavailable": ["Agent Watch không đọc được key công ty trên máy này. Mở Agent Watch, dán lại mã kết nối, bấm Kết nối Piagent trong WSL rồi kết nối lại.", "Agent Watch cannot read the company key on this computer. Open Agent Watch, paste the connection code again, choose Connect Piagent in WSL, then connect again."],
|
|
13
13
|
"managed-profile-disconnected": ["Cấu hình Piagent đang thuộc key công ty đã ngắt kết nối. Nhập lại cấu hình cho Piagent trong Agent Watch (trên Windows: Kết nối Piagent trong WSL).", "The Piagent configuration belongs to a disconnected company key. Import it again in Agent Watch (on Windows: Connect Piagent in WSL)."],
|
|
14
14
|
"managed-launch-binding-changed": ["Agent Watch hoặc Piagent vừa cập nhật. Nhập lại cấu hình cho Piagent trong Agent Watch (trên Windows: Kết nối Piagent trong WSL).", "Agent Watch or Piagent changed. Import the Piagent configuration again in Agent Watch (on Windows: Connect Piagent in WSL)."],
|
|
15
|
+
"managed-binding-update-pending": ["Agent Watch đang chạy nhưng chưa cập nhật cấu hình cho bản Piagent mới. Mở Agent Watch → Studio, bấm Áp dụng cho Piagent rồi mở lại.", "Agent Watch is running but has not updated the configuration for the new Piagent yet. Open Agent Watch → Studio, apply for Piagent, then open it again."],
|
|
16
|
+
"managed-binding-watch-not-running": ["Piagent vừa cập nhật nhưng Agent Watch không chạy nên chưa cập nhật cấu hình. Mở Agent Watch, chờ vài giây rồi mở lại.", "Piagent was updated but Agent Watch is not running, so the configuration was not updated. Open Agent Watch, wait a few seconds, then open it again."],
|
|
15
17
|
"managed-launch-version-mismatch": ["Agent Watch đang dùng một bản Piagent khác với dashboard này. Mở Agent Watch → Studio, bấm Áp dụng cho Piagent rồi kết nối lại.", "Agent Watch points at another Piagent install than this dashboard. Open Agent Watch → Studio, apply for Piagent, then connect again."],
|
|
16
18
|
"managed-launch-stale-runtime": ["Phiên công ty của bản Piagent cũ chưa dừng được. Thử kết nối lại sau ít giây.", "The company runtime from the previous Piagent did not stop yet. Connect again in a few seconds."],
|
|
17
19
|
"managed-launch-timeout": ["Phiên công ty khởi động quá lâu. Kết nối lại.", "The company runtime took too long to start. Connect again."],
|
|
@@ -15,6 +15,12 @@ import { headlineText, outcomeText, processItems } from '../packages/piagent-web
|
|
|
15
15
|
import { turnEndCopy, turnEndOf } from '../packages/piagent-webui/shared/turn-end.ts';
|
|
16
16
|
|
|
17
17
|
const LOCALE = 'vi';
|
|
18
|
+
export const HELP_TEXT = [
|
|
19
|
+
'Cuộc trò chuyện công ty: /undo hoàn tác file lượt trước đã sửa · /bypass on|off|status quyền lệnh cần internet · /fast on|off|status (khi Harness cho phép) · /skill:<tên> hoặc /<lệnh> của bạn và của project.',
|
|
20
|
+
'Pi: /session thông tin cuộc trò chuyện · /resume mở cuộc trò chuyện công ty khác · /new cuộc mới · /compact nén ngữ cảnh · /tree, /fork nhánh · /name đặt tên · /copy, /export · /hotkeys toàn bộ phím tắt · /quit thoát.',
|
|
21
|
+
'Phím: ! <lệnh> chạy shell · Esc dừng lượt đang chạy · Ctrl+C xoá ô nhập, hai lần để thoát · Ctrl+O xem thêm.',
|
|
22
|
+
'Mở lại sau: piagent studio --project <thư mục>, rồi /resume. Đừng mở cuộc trò chuyện công ty bằng pi cá nhân.',
|
|
23
|
+
].join('\n');
|
|
18
24
|
const PERMISSION_STATUS = 'agent-watch-permission';
|
|
19
25
|
const ASK = 'Hỏi trước (mặc định)', BYPASS = 'Bypass: chỉ hỏi việc bắt buộc';
|
|
20
26
|
const PERMISSION_TEXT = {
|
|
@@ -62,7 +68,7 @@ export function companyTerminalExtension({ managed, packageRoot, sdkRoot, bindin
|
|
|
62
68
|
};
|
|
63
69
|
|
|
64
70
|
registerTerminalUpdateOffer(pi, { installed, packageRoot, restart: 'piagent studio',
|
|
65
|
-
afterUpdate: () => bindingChanged() ? 'Bản mới đổi trình khởi chạy công ty:
|
|
71
|
+
afterUpdate: () => bindingChanged() ? 'Bản mới đổi trình khởi chạy công ty: Agent Watch tự cập nhật cấu hình (cần Agent Watch đang chạy); piagent studio chờ việc này khi mở lại.' : null });
|
|
66
72
|
|
|
67
73
|
// Ask first or Bypass, kept with the conversation like the dashboard's switch.
|
|
68
74
|
const showPermission = (ctx) => ctx.ui?.setStatus?.(PERMISSION_STATUS, managed()?.permission === 'trusted-full-access' ? 'Bypass' : undefined);
|
|
@@ -117,6 +123,12 @@ export function companyTerminalExtension({ managed, packageRoot, sdkRoot, bindin
|
|
|
117
123
|
: 'Fast đang tắt: các lượt chạy tốc độ thường.', 'info');
|
|
118
124
|
} });
|
|
119
125
|
|
|
126
|
+
// /help is answered here, as on the dashboard: no model request (Pi has no
|
|
127
|
+
// /help of its own, so the text would otherwise go to the agent).
|
|
128
|
+
pi.registerCommand('help', {
|
|
129
|
+
description: 'Lệnh và phím tắt của Terminal công ty (không gửi model)',
|
|
130
|
+
handler: async (_args, ctx) => ctx.ui.notify(HELP_TEXT, 'info') });
|
|
131
|
+
|
|
120
132
|
// The Harness's notes carry English for the agent; the member reads them in Vietnamese.
|
|
121
133
|
pi.registerMessageRenderer('agent-watch-process', (message, _options, theme) => {
|
|
122
134
|
const lines = processLines(message.details);
|
|
@@ -781,6 +781,17 @@ if [[ "$WITH_HERDR" == true ]]; then
|
|
|
781
781
|
fi
|
|
782
782
|
fi
|
|
783
783
|
|
|
784
|
+
# Personal Pi must not load Piagent twice (an extension path into another
|
|
785
|
+
# Piagent copy beside the package) or keep old Agent Watch company providers:
|
|
786
|
+
# Pi refuses to start, or personal Pi reaches Studio outside the Harness.
|
|
787
|
+
echo
|
|
788
|
+
echo "Personal Pi configuration:"
|
|
789
|
+
if [[ "$DRY_RUN" == true ]]; then
|
|
790
|
+
print_cmd node "$PLATFORM_ROOT/scripts/personal-pi-config.mjs" repair
|
|
791
|
+
else
|
|
792
|
+
node "$PLATFORM_ROOT/scripts/personal-pi-config.mjs" repair || echo "WARN: could not check the personal Pi configuration; run: node \"$PLATFORM_ROOT/scripts/personal-pi-config.mjs\" check" >&2
|
|
793
|
+
fi
|
|
794
|
+
|
|
784
795
|
echo
|
|
785
796
|
echo "Installed packages:"
|
|
786
797
|
if [[ "$DRY_RUN" == true ]]; then
|
|
@@ -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
|
+
}
|
package/scripts/piagent-cli.mjs
CHANGED
|
@@ -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
|
}
|
|
@@ -7,6 +7,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
7
7
|
import { ManagedBrokerClient } from '../packages/piagent-core/managed/broker-client.mjs';
|
|
8
8
|
import { ManagedSession } from '../packages/piagent-core/managed/session.mjs';
|
|
9
9
|
import { agentWatchDataDirectory, interopEnvironment, resolveStore, storedSlot } from '../packages/piagent-core/managed/store.mjs';
|
|
10
|
+
import { agentWatchRunning, awaitRebind, bindingMismatch, installedAfterBinding } from '../packages/piagent-core/managed/binding-wait.mjs';
|
|
10
11
|
|
|
11
12
|
const digest = file => createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
12
13
|
// Node's experimental-feature notices (type stripping, SQLite) are not
|
|
@@ -30,18 +31,32 @@ Object.assign(process.env, clean);
|
|
|
30
31
|
let broker, current, gateway, hold;
|
|
31
32
|
try {
|
|
32
33
|
const configPath = fs.realpathSync(flags.get('--config') ?? path.join(home,'.pi/agent/agent-watch-managed.json'));
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
const readConfig = () => {
|
|
35
|
+
if (!fs.statSync(configPath).isFile() || fs.statSync(configPath).size > 16384) throw Error('managed-config-invalid');
|
|
36
|
+
return JSON.parse(fs.readFileSync(configPath));
|
|
37
|
+
};
|
|
38
|
+
let config = readConfig();
|
|
35
39
|
// Agent Watch rewrites this when the key or its harness changes.
|
|
36
40
|
const bindingRevision = () => { try { return String(JSON.parse(fs.readFileSync(configPath, 'utf8')).configuration_revision ?? '') || null; } catch { return null; } };
|
|
37
41
|
// --serve backs the personal dashboard: no browser, no project of its own.
|
|
38
42
|
// Its startup probe runs in an empty private folder that is never listed.
|
|
39
43
|
const serve = flags.has('--serve');
|
|
40
44
|
if (serve && (flags.has('--project') || flags.has('--web') || flags.has('--check') || flags.has('--prompt'))) throw Error('managed-launch-arguments');
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
const running = { self: fs.realpathSync(fileURLToPath(import.meta.url)), node: fs.realpathSync(process.execPath) };
|
|
46
|
+
const mismatch = bindingMismatch(config, running);
|
|
47
|
+
// Right after a Piagent update only the launcher's hash differs until Agent
|
|
48
|
+
// Watch writes the new binding: wait for it. Nothing runs before every
|
|
49
|
+
// pinned path and hash matches again.
|
|
50
|
+
if (mismatch === 'entrypoint' && installedAfterBinding(configPath, config.entrypoint)) {
|
|
51
|
+
const watch = () => agentWatchRunning(config.broker);
|
|
52
|
+
const rebound = await awaitRebind({ running: watch,
|
|
53
|
+
current: () => { try { const next = readConfig(); return bindingMismatch(next, running) === null ? next : null; } catch { return null; } },
|
|
54
|
+
waiting: state => process.stderr.write(state === false
|
|
55
|
+
? 'Piagent vừa cập nhật nhưng Agent Watch không chạy. Mở Agent Watch; Piagent tự tiếp tục khi cấu hình được cập nhật… [managed-binding-wait]\n'
|
|
56
|
+
: 'Agent Watch đang cập nhật cấu hình cho bản Piagent mới… [managed-binding-wait]\n') });
|
|
57
|
+
if (!rebound) throw Error(watch() === false ? 'managed-binding-watch-not-running' : 'managed-binding-update-pending');
|
|
58
|
+
config = rebound;
|
|
59
|
+
} else if (mismatch) throw Error('managed-launch-binding-changed');
|
|
45
60
|
// What company sessions need on this machine, checked with the real sandbox
|
|
46
61
|
// and broker; no conversation, no model request.
|
|
47
62
|
if (flags.has('--doctor')) {
|
|
@@ -162,6 +177,10 @@ try {
|
|
|
162
177
|
// (updating Pi breaks the Agent Watch import), no package-update, catalog or
|
|
163
178
|
// install-telemetry calls. Search tools were provisioned above.
|
|
164
179
|
process.env.PI_OFFLINE = '1'; process.env.PI_SKIP_VERSION_CHECK = '1';
|
|
180
|
+
// Pi's exit line names `pi --session-dir …`, which would open this company
|
|
181
|
+
// conversation in personal Pi; name the company Terminal instead.
|
|
182
|
+
const write = process.stdout.write.bind(process.stdout), project = /^[\w./~:-]+$/.test(cwd) ? cwd : `'${cwd.replaceAll("'", "'\\''")}'`;
|
|
183
|
+
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
184
|
await new api.InteractiveMode(host, { verbose: false }).run();
|
|
166
185
|
}
|
|
167
186
|
}
|
package/scripts/team-doctor.sh
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|