@piagent/platform 1.43.6 → 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.
- package/CHANGELOG.md +86 -0
- package/docs/command-reference-vietnamese.md +8 -2
- package/docs/company-dev-commands.md +2 -0
- package/docs/distribution-standard.md +6 -6
- package/docs/operator-manual-vietnamese.md +7 -7
- package/docs/publishing-for-teams.md +5 -0
- package/docs/release-install-policy.md +4 -2
- package/docs/team-onboarding.md +2 -2
- package/docs-site/architecture.html +1 -1
- package/docs-site/assets/search-en.json +1 -1
- package/docs-site/assets/search-vi.json +1 -1
- package/docs-site/benchmark.html +1 -1
- package/docs-site/commands.html +1 -1
- 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 +15 -15
- 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/update.html +3 -3
- package/docs-site/en/whats-new.html +129 -67
- package/docs-site/en/windows.html +1 -1
- package/docs-site/en/workflows.html +1 -1
- package/docs-site/index.html +15 -15
- 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/releases.json +200 -0
- 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/update.html +3 -3
- package/docs-site/whats-new.html +129 -67
- package/docs-site/windows.html +1 -1
- package/docs-site/workflows.html +1 -1
- package/evals/fixtures/piagent-webui/workspace-media-v1.invalid.json +18 -0
- package/evals/fixtures/piagent-webui/workspace-media-v1.valid.json +18 -0
- package/evals/private-holdout-v1/public-exposure.v2.json +2 -2
- package/package.json +4 -1
- package/packages/piagent-core/extensions/piagent-guard.ts +3 -0
- package/packages/piagent-core/extensions/workspace-inspection.ts +15 -0
- package/packages/piagent-core/managed/agent-skills.mjs +2 -2
- package/packages/piagent-core/managed/boundary-queue.mjs +1 -1
- package/packages/piagent-core/managed/dist/workspace/manifest.json +1 -1
- package/packages/piagent-core/managed/dist/workspace/worker.mjs +43 -1
- package/packages/piagent-core/managed/language-environment.mjs +6 -1
- package/packages/piagent-core/managed/pi-host.mjs +60 -1
- package/packages/piagent-core/managed/resource-loader.mjs +3 -2
- package/packages/piagent-core/managed/session.mjs +3 -2
- package/packages/piagent-core/managed/workflow.mjs +19 -4
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/runtime/inspection/workspace-worker.ts +3 -3
- package/packages/piagent-core/runtime/registration/agent-resources.ts +3 -2
- package/packages/piagent-core/runtime/resources/agent-resources.d.mts +3 -2
- package/packages/piagent-core/runtime/resources/agent-resources.mjs +17 -10
- package/packages/piagent-core/runtime/resources/plugin-skills.mjs +156 -0
- package/packages/piagent-core/runtime/session/system-prompt.ts +2 -1
- package/packages/piagent-core/runtime/terminal/terminal-style.d.mts +7 -0
- package/packages/piagent-core/runtime/terminal/terminal-style.mjs +99 -0
- package/packages/piagent-core/themes/piagent-contrast.json +78 -0
- package/packages/piagent-core/themes/piagent-focus.json +78 -0
- package/packages/piagent-core/themes/piagent-ocean.json +78 -0
- package/packages/piagent-core/themes/piagent-paper.json +78 -0
- package/packages/piagent-webui/dist/client/assets/{ActivityPanel-CBbfhN-m.js → ActivityPanel-DxQ3_dyT.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/App-DcaqK-_s.js +5 -0
- package/packages/piagent-webui/dist/client/assets/GatewayApp-5DVdNIyr.js +97 -0
- package/packages/piagent-webui/dist/client/assets/{InspectionApp-B5eKxLEs.js → InspectionApp-CBPHxuqj.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/{LineReview-DZ9ez8kO.js → LineReview-C_0ZpgCH.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/MarkdownMessage-DGt5B-6D.js +29 -0
- package/packages/piagent-webui/dist/client/assets/{SessionAgentWorkspace-N79svKS3.js → SessionAgentWorkspace-DmD9Qt4f.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/SessionInspectorDrawer-CK5ujmH3.js +2 -0
- package/packages/piagent-webui/dist/client/assets/SettingsNavigation-BnorH-l_.js +3 -0
- package/packages/piagent-webui/dist/client/assets/SettingsPage-Dda8GW2a.js +1 -0
- package/packages/piagent-webui/dist/client/assets/{TerminalPanel-CiDM7fY8.js → TerminalPanel-X2389XHl.js} +2 -2
- package/packages/piagent-webui/dist/client/assets/{TerminalView-CR4TO5f1.js → TerminalView-yaIg6RCs.js} +3 -3
- package/packages/piagent-webui/dist/client/assets/{WorkspaceCodeEditor-B5KeO5jf.js → WorkspaceCodeEditor-Dt3dS_QG.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/WorkspaceExplorer-CBHwY4BN.js +5 -0
- package/packages/piagent-webui/dist/client/assets/{WorkspaceQuickOpen-vQ5DqfJl.js → WorkspaceQuickOpen-CJmMBmK8.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/index-BMQo9OEq.js +37 -0
- package/packages/piagent-webui/dist/client/assets/{tab-close-DHDqCxTL.js → tab-close-ITV4Cf4X.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/{transcript-view-model-BCyNr6KF.js → transcript-view-model-Bq0nkxyu.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/{ui-preferences-Dy4qeODT.js → ui-preferences-0waeSO-S.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/{vendor-CA5pzJiI.js → vendor-CSDKB7mU.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/workspace-api-D8sJkiZB.js +1 -0
- package/packages/piagent-webui/dist/client/assets/workspace-navigation-DkVobhwg.js +1 -0
- package/packages/piagent-webui/dist/client/index.html +3 -3
- package/packages/piagent-webui/gateway/native-project-picker.ts +40 -14
- package/packages/piagent-webui/gateway/pi-host.ts +13 -1
- package/packages/piagent-webui/server/workspace-read-routes.ts +3 -3
- package/packages/piagent-webui/server/workspace-reader.ts +2 -2
- package/schemas/piagent-webui/catalog-v1.json +9 -0
- package/schemas/piagent-webui/workspace-media-v1.schema.json +63 -0
- package/scripts/company-terminal.mjs +6 -2
- package/scripts/install-global.sh +13 -1
- package/scripts/pi-host.mjs +109 -0
- package/scripts/piagent-cli.mjs +17 -6
- package/scripts/piagent-dashboard.mjs +2 -2
- package/scripts/runtime-advisory-exceptions.json +40 -0
- package/scripts/setup.sh +19 -1
- package/scripts/skill-sources.mjs +188 -0
- package/scripts/uninstall-global.sh +18 -1
- package/scripts/update-global.mjs +13 -5
- package/packages/piagent-webui/dist/client/assets/App-C633xHZS.js +0 -5
- package/packages/piagent-webui/dist/client/assets/GatewayApp-cQ6cd7fw.js +0 -97
- package/packages/piagent-webui/dist/client/assets/MarkdownMessage-D7bbIWJe.js +0 -29
- package/packages/piagent-webui/dist/client/assets/SessionInspectorDrawer-mZqwulvp.js +0 -2
- package/packages/piagent-webui/dist/client/assets/SettingsNavigation-DzboWgoR.js +0 -3
- package/packages/piagent-webui/dist/client/assets/SettingsPage-DX2QIvRC.js +0 -1
- package/packages/piagent-webui/dist/client/assets/WorkspaceExplorer-C5M5c2uW.js +0 -5
- package/packages/piagent-webui/dist/client/assets/index-CrpbZF1r.js +0 -37
- package/packages/piagent-webui/dist/client/assets/workspace-api-O7auv_td.js +0 -1
- package/packages/piagent-webui/dist/client/assets/workspace-navigation-C8GtShbg.js +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{o as e}from"./bootstrap-CedkNtRG.js";async function t(t,n,r,i){let a=new URLSearchParams({path:r}),o=await fetch(`/api/v1/sessions/${encodeURIComponent(t)}/inspection/workspace/${n}?${a}`,{credentials:`same-origin`,headers:{Accept:`application/json`},signal:i});if(o.status===401&&e(),!o.ok)throw Error(`workspace-read-failed`);return await o.json()}var n=(e,n,r)=>t(e,`tree`,n,r),r=(e,n,r)=>t(e,`file`,n,r),i=(e,n,r)=>t(e,`media`,n,r);async function a(t,n,r){let i=await fetch(`/api/v1/sessions/${encodeURIComponent(t)}/inspection/workspace/paths?${new URLSearchParams({query:n})}`,{credentials:`same-origin`,headers:{Accept:`application/json`},signal:r});if(i.status===401&&e(),!i.ok)throw Error(`workspace-read-failed`);return await i.json()}async function o(t,n,r){let i=await fetch(`/api/v1/sessions/${encodeURIComponent(t)}/inspection/workspace/search?${new URLSearchParams({query:n})}`,{credentials:`same-origin`,headers:{Accept:`application/json`},signal:r});if(i.status===401&&e(),!i.ok)throw Error(`workspace-read-failed`);return await i.json()}export{o as a,n as i,i as n,a as r,r as t};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{r as e}from"./rolldown-runtime-hePW80VL.js";import{Bt as t,Dn as n,Fn as r,It as i}from"./vendor-CSDKB7mU.js";import{i as a,n as o}from"./ui-preferences-0waeSO-S.js";import{h as s}from"./api-BQS4Dtrr.js";var c=e(r(),1);function l(e,t=1){let n=/^(.+?)(?::(\d+)(?::\d+)?)?$/.exec(e.replace(/^@/,``)),r=n?.[1];return!r||r.length>4096||/^[~/]/.test(r)||/[\\\x00-\x1f\x7f:]/.test(r)||r.split(`/`).some(e=>!e||e===`.`||e===`..`)?null:{path:r,line:Math.max(1,Math.min(1e6,Number(n?.[2])||t||1))}}function u(e){let t=e.trim();if(!t||t.length>4096||/^(?:https?|mailto|data|javascript):/i.test(t)||t.startsWith(`#`))return null;if(/^file:/i.test(t))try{t=new URL(t).pathname}catch{return null}try{t=decodeURIComponent(t)}catch{}let n=/#L(\d+)(?:-L?\d+)?$/.exec(t),r=n?Number(n[1]):void 0;if(n&&(t=t.slice(0,n.index)),t=t.replace(/^\.\//,``),t.startsWith(`/`)){let e=/^(\/.+?)(?::(\d+)(?::\d+)?)?$/.exec(t),n=e?.[1]??``;return!n||/[\x00-\x1f\x7f]/.test(n)||n.split(`/`).slice(1).some(e=>!e||e===`.`||e===`..`)?null:{path:n,line:Math.max(1,Math.min(1e6,r??(Number(e?.[2])||1))),absolute:!0}}let i=l(t,r);return i?{...i,absolute:!1}:null}var d=n(),f=`piagent-workspace-open`;async function p(e,t){let n=(await s(e)).roots.find(e=>e.source===`project`&&t.startsWith(`${e.path.replace(/\/+$/,``)}/`));return n?t.slice(n.path.replace(/\/+$/,``).length+1):null}var m=(0,c.createContext)(null);function h({sessionRef:e,onOpen:t,children:n}){let r=n=>{t(),window.dispatchEvent(new CustomEvent(f,{detail:{sessionRef:e,...n}}))};return(0,d.jsx)(m.Provider,{value:e?t=>{if(!(`absolute`in t)||!t.absolute){r({path:t.path,line:t.line});return}p(e,t.path).then(e=>{e&&r({path:e,line:t.line})},()=>void 0)}:null,children:n})}function g({path:e,line:n,children:r}){let i=(0,c.useContext)(m),{locale:s}=a(),u=l(e,n);return!i||!u?r?(0,d.jsx)(d.Fragment,{children:r}):null:(0,d.jsx)(t,{size:`small`,onClick:()=>i(u),sx:{textTransform:`none`,verticalAlign:`baseline`},children:r??o(s,`Mở trong editor`,`Open in editor`)})}function _({link:e,children:t}){let n=(0,c.useContext)(m),{locale:r}=a();return n?(0,d.jsx)(i,{component:`button`,type:`button`,underline:`hover`,onClick:()=>n(e),title:o(r,`Mở ${e.path}`,`Open ${e.path}`),sx:{font:`inherit`,verticalAlign:`baseline`,textAlign:`left`},children:t}):(0,d.jsx)(d.Fragment,{children:t})}function v({children:e}){return(0,d.jsx)(d.Fragment,{children:c.Children.map(e,e=>typeof e==`string`?e.split(/((?<![\w@])@[\w./-]+\.[\w]+(?::\d+)?)/g).map((e,t)=>e.startsWith(`@`)&&l(e)?(0,d.jsx)(g,{path:e,children:e},t):e):e)})}export{h as a,v as i,g as n,u as o,_ as r,l as s,f as t};
|
|
@@ -7,10 +7,10 @@
|
|
|
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-BMQo9OEq.js"></script>
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-hePW80VL.js">
|
|
12
|
-
<link rel="modulepreload" crossorigin href="/assets/vendor-
|
|
13
|
-
<link rel="modulepreload" crossorigin href="/assets/ui-preferences-
|
|
12
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-CSDKB7mU.js">
|
|
13
|
+
<link rel="modulepreload" crossorigin href="/assets/ui-preferences-0waeSO-S.js">
|
|
14
14
|
<link rel="stylesheet" crossorigin href="/assets/index-Dqb2hNan.css">
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|
|
@@ -4,19 +4,31 @@ import path from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
|
|
6
6
|
// `wsl`: the picker is Windows' own, run from WSL; its paths are converted.
|
|
7
|
-
|
|
7
|
+
// `base64`: each line is a UTF-8 path in base64, safe from code pages.
|
|
8
|
+
type PickerSpec = { executable: string; args: string[]; wsl?: boolean; base64?: boolean };
|
|
8
9
|
|
|
9
10
|
// Windows' folder dialog, through PowerShell (also from WSL, where Windows
|
|
10
|
-
// programs run as they are).
|
|
11
|
+
// programs run as they are). The Gateway runs detached, without a console or
|
|
12
|
+
// the foreground: the dialog gets a topmost owner so it opens in front of the
|
|
13
|
+
// browser, and the path is printed as base64 so a non-ASCII folder survives
|
|
14
|
+
// the console's code page. Cancel exits 1; any PowerShell error exits 2.
|
|
11
15
|
const WINDOWS_PICKER = [
|
|
12
|
-
"
|
|
13
|
-
"Add-Type -AssemblyName System.Windows.Forms",
|
|
16
|
+
"$ErrorActionPreference = 'Stop'",
|
|
17
|
+
"try { Add-Type -AssemblyName System.Windows.Forms",
|
|
18
|
+
"[System.Windows.Forms.Application]::EnableVisualStyles()",
|
|
19
|
+
"$owner = New-Object System.Windows.Forms.Form",
|
|
20
|
+
"$owner.TopMost = $true",
|
|
21
|
+
"$owner.ShowInTaskbar = $false",
|
|
14
22
|
"$dialog = New-Object System.Windows.Forms.FolderBrowserDialog",
|
|
15
23
|
"$dialog.Description = 'Choose a project folder for Piagent'",
|
|
16
|
-
"
|
|
24
|
+
"$result = $dialog.ShowDialog($owner)",
|
|
25
|
+
"$owner.Dispose()",
|
|
26
|
+
"if ($result -ne [System.Windows.Forms.DialogResult]::OK) { exit 1 }",
|
|
27
|
+
"[Console]::Out.WriteLine([Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($dialog.SelectedPath)))",
|
|
28
|
+
"exit 0 } catch { [Console]::Error.WriteLine($_.Exception.Message); exit 2 }"
|
|
17
29
|
].join("; ");
|
|
18
30
|
const windowsPicker = (executable: string, wsl = false): PickerSpec =>
|
|
19
|
-
({ executable, args: ["-NoProfile", "-NonInteractive", "-STA", "-Command", WINDOWS_PICKER], ...(wsl ? { wsl } : {}) });
|
|
31
|
+
({ executable, args: ["-NoProfile", "-NonInteractive", "-STA", "-Command", WINDOWS_PICKER], base64: true, ...(wsl ? { wsl } : {}) });
|
|
20
32
|
type PickerEnvironment = Readonly<Record<string, string | undefined>>;
|
|
21
33
|
|
|
22
34
|
function executableOnPath(name: string, environment: PickerEnvironment): string | null {
|
|
@@ -67,35 +79,49 @@ export function nativeProjectPickerAvailable(options: { platform?: NodeJS.Platfo
|
|
|
67
79
|
return resolveNativeProjectPicker(options) !== null;
|
|
68
80
|
}
|
|
69
81
|
|
|
70
|
-
function selectedFolders(stdout: string): string[] {
|
|
82
|
+
function selectedFolders(stdout: string, base64 = false): string[] {
|
|
71
83
|
const selected: string[] = [];
|
|
72
84
|
for (const raw of stdout.split("\n")) {
|
|
73
|
-
|
|
85
|
+
let value = raw.trim(); if (!value) continue;
|
|
86
|
+
if (base64) {
|
|
87
|
+
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(value)) continue;
|
|
88
|
+
value = Buffer.from(value, "base64").toString("utf8");
|
|
89
|
+
}
|
|
74
90
|
let candidate = value;
|
|
75
91
|
if (value.startsWith("file:")) {
|
|
76
92
|
try { candidate = fileURLToPath(value); } catch { continue; }
|
|
77
93
|
}
|
|
78
|
-
if (candidate.includes("\0") || /[\u0001-\u001f\u007f]/.test(candidate) || selected.includes(candidate)) continue;
|
|
94
|
+
if (candidate.includes("\0") || /[\u0001-\u001f\u007f\ufffd]/.test(candidate) || selected.includes(candidate)) continue;
|
|
79
95
|
selected.push(candidate);
|
|
80
96
|
if (selected.length === 32) break;
|
|
81
97
|
}
|
|
82
98
|
return selected;
|
|
83
99
|
}
|
|
84
100
|
|
|
85
|
-
|
|
86
|
-
|
|
101
|
+
type RunPicker = (executable: string, args: string[], options: { timeout: number; maxBuffer: number; windowsHide: boolean },
|
|
102
|
+
callback: (error: Error | null, stdout: string) => void) => unknown;
|
|
103
|
+
type ToWslPath = (windowsPath: string) => string;
|
|
104
|
+
|
|
105
|
+
// The folders the operator chose. `run` and `toWslPath` stand in for the
|
|
106
|
+
// process spawns in tests.
|
|
107
|
+
export function pickNativeProjectFolders(options: { picker?: PickerSpec | null; run?: RunPicker; toWslPath?: ToWslPath } = {}): Promise<string[]> {
|
|
108
|
+
const picker = options.picker === undefined ? resolveNativeProjectPicker() : options.picker;
|
|
87
109
|
if (!picker) return Promise.reject(new Error("native-project-picker-unavailable"));
|
|
110
|
+
const run = options.run ?? (execFile as unknown as RunPicker);
|
|
111
|
+
const toWslPath = options.toWslPath ?? ((folder) => execFileSync("wslpath", ["-u", folder], { encoding: "utf8", timeout: 5000 }).trim());
|
|
88
112
|
return new Promise((resolve, reject) => {
|
|
89
|
-
|
|
113
|
+
// windowsHide: no console window of its own next to the dialog; closing
|
|
114
|
+
// that window used to kill the picker.
|
|
115
|
+
run(picker.executable, picker.args, { timeout: 5 * 60_000, maxBuffer: 64 * 1024, windowsHide: true }, (error, stdout) => {
|
|
90
116
|
if (error) {
|
|
91
117
|
const exitCode = typeof (error as NodeJS.ErrnoException & { code?: unknown }).code === "number"
|
|
92
118
|
? (error as unknown as { code: number }).code : null;
|
|
93
119
|
reject(new Error(exitCode === 1 ? "project-import-cancelled" : "project-import-picker-failed")); return;
|
|
94
120
|
}
|
|
95
|
-
let selected = selectedFolders(stdout.replace(/\r/g, ""));
|
|
121
|
+
let selected = selectedFolders(String(stdout).replace(/\r/g, ""), picker.base64);
|
|
96
122
|
// C:\Users\me\shop, as seen from WSL: /mnt/c/Users/me/shop.
|
|
97
123
|
if (picker.wsl) selected = selected.flatMap((folder) => {
|
|
98
|
-
try { return [
|
|
124
|
+
try { return [toWslPath(folder)]; } catch { return []; }
|
|
99
125
|
});
|
|
100
126
|
if (!selected.length) { reject(new Error("project-import-cancelled")); return; }
|
|
101
127
|
resolve(selected);
|
|
@@ -4,6 +4,7 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
|
+
import { privateHostRoot } from "../../piagent-core/managed/pi-host.mjs";
|
|
7
8
|
|
|
8
9
|
const require = createRequire(import.meta.url);
|
|
9
10
|
|
|
@@ -73,8 +74,19 @@ export function installedPiHostRoot(): string {
|
|
|
73
74
|
throw new Error("pi-host-unavailable");
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
// The host of the pinned release: the installed one when it is that release,
|
|
78
|
+
// else Piagent's private copy, kept beside a member's own newer Pi
|
|
79
|
+
// (packages/piagent-core/managed/pi-host.mjs).
|
|
80
|
+
export function pinnedPiHostRoot(expectedVersion: string): string {
|
|
81
|
+
const own = privateHostRoot(expectedVersion) as string | null;
|
|
82
|
+
let root: string;
|
|
83
|
+
try { root = installedPiHostRoot(); } catch (error) { if (own) return own; throw error; }
|
|
84
|
+
try { if ((JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")) as { version?: string }).version === expectedVersion) return root; } catch { /* unreadable: the private copy, if any */ }
|
|
85
|
+
return own ?? root;
|
|
86
|
+
}
|
|
87
|
+
|
|
76
88
|
export async function loadPinnedPiHost(expectedVersion: string): Promise<any> {
|
|
77
|
-
const root =
|
|
89
|
+
const root = pinnedPiHostRoot(expectedVersion);
|
|
78
90
|
const actual = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")) as { version?: string };
|
|
79
91
|
if (actual.version !== expectedVersion) throw new Error("pi-host-version-mismatch");
|
|
80
92
|
return await import(pathToFileURL(path.join(root, "dist", "index.js")).href);
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { WorkspaceReadError } from "../../piagent-core/security/workspace-file-reader.ts";
|
|
2
2
|
|
|
3
|
-
export type WorkspaceRead = (kind: "tree" | "file" | "paths" | "search", path: string) => unknown | Promise<unknown>;
|
|
3
|
+
export type WorkspaceRead = (kind: "tree" | "file" | "media" | "paths" | "search", path: string) => unknown | Promise<unknown>;
|
|
4
4
|
export type WorkspaceRouteResult = { handled: false } | { handled: true; status: number; value: unknown };
|
|
5
5
|
|
|
6
6
|
// Authentication and company forwarding belong to the session router. This
|
|
7
7
|
// parser accepts only project-relative paths, including an empty tree root.
|
|
8
8
|
export async function routeWorkspaceRead(url: URL, read?: WorkspaceRead): Promise<WorkspaceRouteResult> {
|
|
9
|
-
const match = /^\/api\/v1\/workspace\/(tree|file|paths|search)$/.exec(url.pathname);
|
|
9
|
+
const match = /^\/api\/v1\/workspace\/(tree|file|media|paths|search)$/.exec(url.pathname);
|
|
10
10
|
if (!match) return { handled: false };
|
|
11
11
|
const refused = (status: number, code: string): WorkspaceRouteResult => ({ handled: true, status, value: { error: { code } } });
|
|
12
12
|
if (!read) return refused(503, "workspace-unavailable");
|
|
@@ -19,7 +19,7 @@ export async function routeWorkspaceRead(url: URL, read?: WorkspaceRead): Promis
|
|
|
19
19
|
|| (parameter === "path" && (relative ? relative.split("/").some(part => !part || part === "." || part === "..") : match[1] !== "tree"))) {
|
|
20
20
|
return refused(400, "workspace-path-invalid");
|
|
21
21
|
}
|
|
22
|
-
try { return { handled: true, status: 200, value: await read(match[1] as "tree" | "file" | "paths" | "search", relative) }; }
|
|
22
|
+
try { return { handled: true, status: 200, value: await read(match[1] as "tree" | "file" | "media" | "paths" | "search", relative) }; }
|
|
23
23
|
catch (error) {
|
|
24
24
|
if (error instanceof WorkspaceReadError) return refused(error.code === "changed-during-read" ? 409 : 400, error.code);
|
|
25
25
|
const code = (error as NodeJS.ErrnoException)?.code;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { searchWorkspace } from "../../piagent-core/extensions/workspace-search.ts";
|
|
2
2
|
import { loadProjectContextIndexPolicy } from "../../piagent-core/extensions/workspace-policy.js";
|
|
3
|
-
import { inspectWorkspaceFile, inspectWorkspaceTree, inspectWorkspacePaths } from "../../piagent-core/extensions/workspace-inspection.ts";
|
|
3
|
+
import { inspectWorkspaceFile, inspectWorkspaceMedia, inspectWorkspaceTree, inspectWorkspacePaths } from "../../piagent-core/extensions/workspace-inspection.ts";
|
|
4
4
|
import { inspectWorkspaceEntry } from "../../piagent-core/security/workspace-file-reader.ts";
|
|
5
5
|
import type { WorkspaceRead } from "./workspace-read-routes.ts";
|
|
6
6
|
|
|
@@ -16,7 +16,7 @@ export function personalWorkspaceReader(options: {
|
|
|
16
16
|
} catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; }
|
|
17
17
|
const { policy, profile } = loadProjectContextIndexPolicy(options.packageRoot, options.cwd);
|
|
18
18
|
const context = { policy, profile, readOnly: profile.permissionProfile === "read-only" };
|
|
19
|
-
const value = kind === "search" ? await searchWorkspace(options.cwd, relative, context) : kind === "paths" ? await inspectWorkspacePaths(options.cwd, relative, context) : kind === "tree" ? inspectWorkspaceTree(options.cwd, relative, context) : inspectWorkspaceFile(options.cwd, relative, context);
|
|
19
|
+
const value = kind === "search" ? await searchWorkspace(options.cwd, relative, context) : kind === "paths" ? await inspectWorkspacePaths(options.cwd, relative, context) : kind === "tree" ? inspectWorkspaceTree(options.cwd, relative, context) : kind === "media" ? inspectWorkspaceMedia(options.cwd, relative, context) : inspectWorkspaceFile(options.cwd, relative, context);
|
|
20
20
|
return { schemaVersion: 1, version: `piagent-webui-workspace-${kind}-v1`, generatedAt: new Date().toISOString(),
|
|
21
21
|
identity: await options.identity(), ...value };
|
|
22
22
|
};
|
|
@@ -290,6 +290,15 @@
|
|
|
290
290
|
"supported": true,
|
|
291
291
|
"current": true
|
|
292
292
|
},
|
|
293
|
+
{
|
|
294
|
+
"name": "workspace-media-v1",
|
|
295
|
+
"file": "workspace-media-v1.schema.json",
|
|
296
|
+
"id": "https://piagent.io.vn/schemas/piagent-webui/workspace-media-v1.schema.json",
|
|
297
|
+
"documentVersion": 1,
|
|
298
|
+
"role": "read-model",
|
|
299
|
+
"supported": true,
|
|
300
|
+
"current": true
|
|
301
|
+
},
|
|
293
302
|
{
|
|
294
303
|
"name": "workspace-paths-v1",
|
|
295
304
|
"file": "workspace-paths-v1.schema.json",
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://piagent.io.vn/schemas/piagent-webui/workspace-media-v1.schema.json",
|
|
4
|
+
"title": "Piagent WebUI workspace media v1",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": [
|
|
8
|
+
"schemaVersion",
|
|
9
|
+
"version",
|
|
10
|
+
"generatedAt",
|
|
11
|
+
"identity",
|
|
12
|
+
"path",
|
|
13
|
+
"mime",
|
|
14
|
+
"data",
|
|
15
|
+
"reasonCode"
|
|
16
|
+
],
|
|
17
|
+
"properties": {
|
|
18
|
+
"schemaVersion": {
|
|
19
|
+
"const": 1
|
|
20
|
+
},
|
|
21
|
+
"version": {
|
|
22
|
+
"const": "piagent-webui-workspace-media-v1"
|
|
23
|
+
},
|
|
24
|
+
"generatedAt": {
|
|
25
|
+
"$ref": "common-v1.schema.json#/$defs/timestamp"
|
|
26
|
+
},
|
|
27
|
+
"identity": {
|
|
28
|
+
"$ref": "common-v1.schema.json#/$defs/identity"
|
|
29
|
+
},
|
|
30
|
+
"path": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"maxLength": 4096
|
|
33
|
+
},
|
|
34
|
+
"mime": {
|
|
35
|
+
"enum": [
|
|
36
|
+
"image/png",
|
|
37
|
+
"image/jpeg",
|
|
38
|
+
"image/gif",
|
|
39
|
+
"image/webp",
|
|
40
|
+
"image/bmp",
|
|
41
|
+
"image/x-icon",
|
|
42
|
+
"image/avif",
|
|
43
|
+
null
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
"data": {
|
|
47
|
+
"type": [
|
|
48
|
+
"string",
|
|
49
|
+
"null"
|
|
50
|
+
],
|
|
51
|
+
"maxLength": 11184812,
|
|
52
|
+
"contentEncoding": "base64"
|
|
53
|
+
},
|
|
54
|
+
"reasonCode": {
|
|
55
|
+
"enum": [
|
|
56
|
+
"protected",
|
|
57
|
+
"unsupported",
|
|
58
|
+
"oversized",
|
|
59
|
+
null
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -9,6 +9,7 @@ import { execFile } from 'node:child_process';
|
|
|
9
9
|
import { pathToFileURL } from 'node:url';
|
|
10
10
|
|
|
11
11
|
import { registerTerminalUpdateOffer } from '../packages/piagent-core/runtime/update/terminal-update-offer.mjs';
|
|
12
|
+
import { registerStyleCommand } from '../packages/piagent-core/runtime/terminal/terminal-style.mjs';
|
|
12
13
|
import { parseFailure } from '../packages/piagent-core/runtime/managed-failure.mjs';
|
|
13
14
|
import { checkpointsOf } from '../packages/piagent-core/managed/checkpoints.mjs';
|
|
14
15
|
import { bypassAllowed, effectivePermission } from '../packages/piagent-core/managed/permission.mjs';
|
|
@@ -59,7 +60,7 @@ export function memoryResultLines(text, expanded = false) {
|
|
|
59
60
|
const branchOf = (cwd) => new Promise((resolve) => execFile('git', ['-C', cwd, 'rev-parse', '--abbrev-ref', 'HEAD'], { timeout: 3000 },
|
|
60
61
|
(error, out) => resolve(error ? null : String(out).trim() || null)));
|
|
61
62
|
export const HELP_TEXT = [
|
|
62
|
-
'Cuộc trò chuyện công ty: /undo hoàn tác file lượt trước đã sửa · /bypass on|full|off|status quyền của cuộc trò chuyện · /fast on|off|status (khi Harness cho phép) · /skill:<tên> hoặc /<lệnh> của bạn và của project.',
|
|
63
|
+
'Cuộc trò chuyện công ty: /undo hoàn tác file lượt trước đã sửa · /bypass on|full|off|status quyền của cuộc trò chuyện · /fast on|off|status (khi Harness cho phép) · /style giao diện Terminal · /skill:<tên> hoặc /<lệnh> của bạn và của project.',
|
|
63
64
|
'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.',
|
|
64
65
|
'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.',
|
|
65
66
|
'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.',
|
|
@@ -112,7 +113,7 @@ export function companyCommandText(name, summary = {}) {
|
|
|
112
113
|
case 'login': case 'logout': return 'Cuộc trò chuyện công ty dùng tài khoản AI của team qua Agent Watch: không cần /login hay /logout. Tài khoản và API key cá nhân chỉ dùng trong pi cá nhân.';
|
|
113
114
|
case 'model': case 'scoped-models': return `Model do Harness của team quyết định${main}; Terminal không chọn model. ${ADMIN}`;
|
|
114
115
|
case 'thinking': return `Mức suy luận do Harness của team quyết định${main}. ${ADMIN}`;
|
|
115
|
-
case 'settings': return `Cài đặt của cuộc trò chuyện công ty (compaction, retry…) do Harness quyết định. Phím tắt: /hotkeys. ${ADMIN}`;
|
|
116
|
+
case 'settings': return `Cài đặt của cuộc trò chuyện công ty (compaction, retry…) do Harness quyết định. Giao diện Terminal: /style. Phím tắt: /hotkeys. ${ADMIN}`;
|
|
116
117
|
default: return null;
|
|
117
118
|
}
|
|
118
119
|
}
|
|
@@ -288,6 +289,9 @@ export function companyTerminalExtension({ managed, packageRoot, sdkRoot, bindin
|
|
|
288
289
|
registerTerminalUpdateOffer(pi, { installed, packageRoot, restart: 'piagent studio',
|
|
289
290
|
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 });
|
|
290
291
|
|
|
292
|
+
// The member's pick of look (/style); /settings is not offered here.
|
|
293
|
+
registerStyleCommand(pi, { remember: true });
|
|
294
|
+
|
|
291
295
|
// Ask first or Bypass, kept with the conversation like the dashboard's switch.
|
|
292
296
|
const showPermission = (ctx) => { const mode = managed() ? effectivePermission(managed()) : null; ctx.ui?.setStatus?.(PERMISSION_STATUS, mode && mode !== 'workspace-write' ? PERMISSION_NAME[mode] : undefined); };
|
|
293
297
|
pi.on('session_start', async (_event, ctx) => showPermission(ctx));
|
|
@@ -701,7 +701,19 @@ else
|
|
|
701
701
|
if [[ "$pi_version_output" =~ ([0-9]+\.[0-9]+\.[0-9]+([+-][0-9A-Za-z.-]+)?) ]]; then
|
|
702
702
|
current_pi_version="${BASH_REMATCH[1]}"
|
|
703
703
|
fi
|
|
704
|
-
|
|
704
|
+
pi_host_plan="$(node --input-type=module -e 'const { hostPlan } = await import(process.argv[1]); process.stdout.write(hostPlan(process.argv[2] || null));' "$PLATFORM_ROOT/packages/piagent-core/managed/pi-host.mjs" "$current_pi_version")"
|
|
705
|
+
if [[ "$pi_host_plan" == "private" ]]; then
|
|
706
|
+
# The member's own, newer pi stays: it loads the Piagent package as any Pi
|
|
707
|
+
# package, and company sessions and `piagent pi` run the private pinned host.
|
|
708
|
+
echo "NOTE: your pi $current_pi_version stays as it is; Piagent is qualified on Pi $EXPECTED_PI_VERSION." >&2
|
|
709
|
+
pi_host_args=(ensure --force)
|
|
710
|
+
[[ "$DRY_RUN" == true ]] && pi_host_args+=(--dry-run)
|
|
711
|
+
if ! node "$PLATFORM_ROOT/scripts/pi-host.mjs" "${pi_host_args[@]}" >&2; then
|
|
712
|
+
echo "FAIL: could not install the private Pi $EXPECTED_PI_VERSION." >&2
|
|
713
|
+
exit 1
|
|
714
|
+
fi
|
|
715
|
+
echo "NOTE: run \`piagent pi\` for the qualified release; \`pi\` ($current_pi_version) loads Piagent too, unqualified." >&2
|
|
716
|
+
elif [[ "$current_pi_version" != "$EXPECTED_PI_VERSION" ]]; then
|
|
705
717
|
echo "FAIL: Pi Coding Agent $EXPECTED_PI_VERSION is required; found ${current_pi_version:-${pi_version_output:-unknown}}." >&2
|
|
706
718
|
echo "Install the supported host with: npm install -g --ignore-scripts @earendil-works/pi-coding-agent@$EXPECTED_PI_VERSION" >&2
|
|
707
719
|
exit 1
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The Pi host Piagent runs on, when the member keeps a Pi of their own.
|
|
3
|
+
//
|
|
4
|
+
// Piagent pins one Pi release (package.json peerDependencies). A member who
|
|
5
|
+
// already uses Pi may run a newer one: setup used to replace it with the
|
|
6
|
+
// pinned release. Now theirs stays as it is, and the pinned release is
|
|
7
|
+
// installed privately (packages/piagent-core/managed/pi-host.mjs); company
|
|
8
|
+
// sessions and the dashboard run it, and `piagent pi` opens it for personal use.
|
|
9
|
+
//
|
|
10
|
+
// pi-host.mjs status [--json] the pinned host, the `pi` on PATH, the private copy
|
|
11
|
+
// pi-host.mjs ensure [--dry-run] install the private copy when PATH has another Pi
|
|
12
|
+
// pi-host.mjs root the host root Piagent runs (empty when none)
|
|
13
|
+
// pi-host.mjs run [-- args] run Pi on the pinned host
|
|
14
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
import { PI_HOST_VERSION, hostCli, hostPlan, installPrivateHost, privateHostRoot } from '../packages/piagent-core/managed/pi-host.mjs';
|
|
19
|
+
|
|
20
|
+
const HOST_NAME = '@earendil-works/pi-coding-agent';
|
|
21
|
+
|
|
22
|
+
function usage() {
|
|
23
|
+
return `Usage:
|
|
24
|
+
piagent pi [pi arguments] Run Pi ${PI_HOST_VERSION} (the release Piagent is qualified on)
|
|
25
|
+
piagent pi-host status [--json] Show the pinned host, your own pi and the private copy
|
|
26
|
+
piagent pi-host ensure [--dry-run]
|
|
27
|
+
Install the pinned host privately when your pi is another release
|
|
28
|
+
`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// The `pi` on PATH: its package root and version, or nulls.
|
|
32
|
+
export function pathHost(env = process.env, { probe = true } = {}) {
|
|
33
|
+
for (const dir of (env.PATH ?? '').split(path.delimiter)) {
|
|
34
|
+
if (!path.isAbsolute(dir)) continue;
|
|
35
|
+
let root;
|
|
36
|
+
try { root = path.dirname(fs.realpathSync(path.join(dir, process.platform === 'win32' ? 'pi.cmd' : 'pi'))); } catch { continue; }
|
|
37
|
+
for (let depth = 0; depth < 4; depth += 1, root = path.dirname(root)) {
|
|
38
|
+
try {
|
|
39
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
40
|
+
if (manifest.name === HOST_NAME) return { root, version: manifest.version ?? null };
|
|
41
|
+
} catch { /* keep walking up */ }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (!probe) return { root: null, version: null };
|
|
45
|
+
// A `pi` that is not an npm package (a standalone build): ask it.
|
|
46
|
+
const output = spawnSync('pi', ['--version'], { encoding: 'utf8', timeout: 5_000, shell: process.platform === 'win32' });
|
|
47
|
+
const version = output.status === 0 ? /(\d+\.\d+\.\d+(?:[+-][0-9A-Za-z.-]+)?)/.exec(output.stdout)?.[1] ?? null : null;
|
|
48
|
+
return { root: null, version };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// The host Piagent runs: the `pi` on PATH when it is the pinned release, else
|
|
52
|
+
// the private copy, else (not installed yet) whatever PATH has.
|
|
53
|
+
export function activeHostRoot({ env = process.env, home } = {}) {
|
|
54
|
+
const own = pathHost(env, { probe: false });
|
|
55
|
+
if (own.root && own.version === PI_HOST_VERSION) return own.root;
|
|
56
|
+
return privateHostRoot(PI_HOST_VERSION, home) ?? own.root;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function status({ env = process.env, home } = {}) {
|
|
60
|
+
const own = pathHost(env), privateRoot = privateHostRoot(PI_HOST_VERSION, home);
|
|
61
|
+
return { pinned: PI_HOST_VERSION, own: own.version, ownRoot: own.root, privateRoot, plan: hostPlan(own.version), active: activeHostRoot({ env, home }) };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function describe(report) {
|
|
65
|
+
const lines = [`Piagent runs on Pi ${report.pinned}.`];
|
|
66
|
+
if (!report.own) lines.push('No `pi` on PATH.');
|
|
67
|
+
else if (report.plan === 'current') lines.push(`Your pi is ${report.own}: the same release, used as it is.`);
|
|
68
|
+
else if (report.plan === 'private') lines.push(`Your pi is ${report.own}: kept as it is. Company sessions and \`piagent pi\` use a private Pi ${report.pinned}.`);
|
|
69
|
+
else lines.push(`Your pi is ${report.own}: older than ${report.pinned}; piagent-setup updates it.`);
|
|
70
|
+
lines.push(report.privateRoot ? `Private copy: ${report.privateRoot}` : 'Private copy: not installed');
|
|
71
|
+
return lines.join('\n');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function main(argv) {
|
|
75
|
+
const [command, ...rest] = argv;
|
|
76
|
+
if ([undefined, 'help', '--help', '-h'].includes(command)) { process.stdout.write(usage()); return 0; }
|
|
77
|
+
if (command === 'status') {
|
|
78
|
+
const report = status();
|
|
79
|
+
process.stdout.write(rest.includes('--json') ? `${JSON.stringify(report, null, 2)}\n` : `${describe(report)}\n`);
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
if (command === 'root') { process.stdout.write(`${activeHostRoot() ?? ''}\n`); return 0; }
|
|
83
|
+
if (command === 'ensure') {
|
|
84
|
+
const report = status(), dryRun = rest.includes('--dry-run');
|
|
85
|
+
if (report.plan !== 'private' && !rest.includes('--force')) { process.stdout.write(`${describe(report)}\n`); return 0; }
|
|
86
|
+
try {
|
|
87
|
+
const root = installPrivateHost({ dryRun, log: (line) => process.stdout.write(`${line}\n`) });
|
|
88
|
+
process.stdout.write(`${dryRun ? 'DRY RUN: ' : ''}Pi ${PI_HOST_VERSION} for Piagent: ${root}\n`);
|
|
89
|
+
if (report.own) process.stdout.write(`Your pi ${report.own} is unchanged.\n`);
|
|
90
|
+
return 0;
|
|
91
|
+
} catch (error) { process.stderr.write(`FAIL: ${error.message}\n`); return 1; }
|
|
92
|
+
}
|
|
93
|
+
if (command === 'run') {
|
|
94
|
+
const args = rest[0] === '--' ? rest.slice(1) : rest;
|
|
95
|
+
const root = activeHostRoot();
|
|
96
|
+
if (!root || JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).version !== PI_HOST_VERSION) {
|
|
97
|
+
process.stderr.write(`FAIL: Pi ${PI_HOST_VERSION} is not installed for Piagent. Run: piagent pi-host ensure --force\n`);
|
|
98
|
+
return 1;
|
|
99
|
+
}
|
|
100
|
+
const child = spawn(process.execPath, [hostCli(root), ...args], { stdio: 'inherit' });
|
|
101
|
+
for (const signal of ['SIGTERM', 'SIGHUP']) process.on(signal, () => { try { child.kill(signal); } catch { /* gone */ } });
|
|
102
|
+
process.on('SIGINT', () => {});
|
|
103
|
+
return await new Promise((resolve) => child.once('exit', (code, signal) => resolve(signal ? 1 : code ?? 1)));
|
|
104
|
+
}
|
|
105
|
+
process.stderr.write(usage());
|
|
106
|
+
return 2;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (process.argv[1] && fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) process.exitCode = await main(process.argv.slice(2));
|
package/scripts/piagent-cli.mjs
CHANGED
|
@@ -26,7 +26,9 @@ const scriptByCommand = {
|
|
|
26
26
|
"piagent-context": "scripts/context-engine.mjs",
|
|
27
27
|
"piagent-explain": "scripts/explain-command.mjs",
|
|
28
28
|
"piagent-webui": "scripts/piagent-webui-launcher.mjs",
|
|
29
|
-
"piagent-dashboard": "scripts/piagent-dashboard.mjs"
|
|
29
|
+
"piagent-dashboard": "scripts/piagent-dashboard.mjs",
|
|
30
|
+
"piagent-pi-host": "scripts/pi-host.mjs",
|
|
31
|
+
"piagent-skills": "scripts/skill-sources.mjs"
|
|
30
32
|
};
|
|
31
33
|
|
|
32
34
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -42,6 +44,10 @@ if (invokedAs === "piagent") {
|
|
|
42
44
|
} else if (subcommand === "dashboard") {
|
|
43
45
|
script = "scripts/piagent-dashboard.mjs";
|
|
44
46
|
forwardedArgs = forwardedArgs.slice(1);
|
|
47
|
+
} else if (subcommand === "pi") {
|
|
48
|
+
// Pi itself, on the release Piagent is qualified on, whatever `pi` the member keeps.
|
|
49
|
+
script = "scripts/pi-host.mjs";
|
|
50
|
+
forwardedArgs = ["run", "--", ...forwardedArgs.slice(1)];
|
|
45
51
|
} else if (subcommand === "explain") {
|
|
46
52
|
script = "scripts/explain-command.mjs";
|
|
47
53
|
forwardedArgs = forwardedArgs.slice(1);
|
|
@@ -64,6 +70,9 @@ if (invokedAs === "piagent") {
|
|
|
64
70
|
console.log("Commands:");
|
|
65
71
|
console.log(" dashboard Open and manage the local session hub");
|
|
66
72
|
console.log(" studio Open the managed company agent imported by Agent Watch");
|
|
73
|
+
console.log(" pi Run Pi on the release Piagent is qualified on (your own pi stays as it is)");
|
|
74
|
+
console.log(" pi-host Show or install the Pi host Piagent runs");
|
|
75
|
+
console.log(" skills Add, list or remove skill sources such as mattpocock/skills");
|
|
67
76
|
console.log(" explain Say why the guard would allow or block a shell command");
|
|
68
77
|
console.log(" approve-verification Preview or approve an independent verification plan");
|
|
69
78
|
console.log(" select-verification Preview reusable contracts for exact task criteria");
|
|
@@ -95,8 +104,10 @@ function benchmarkSourceRoot() {
|
|
|
95
104
|
process.exit(1);
|
|
96
105
|
}
|
|
97
106
|
|
|
98
|
-
// The Pi host this member runs: the `pi`
|
|
99
|
-
|
|
107
|
+
// The Pi host this member runs: the `pi` on PATH when it is the pinned release,
|
|
108
|
+
// else Piagent's private copy (a member keeping a newer Pi of their own).
|
|
109
|
+
async function piHost() {
|
|
110
|
+
try { return (await import("./pi-host.mjs")).activeHostRoot(); } catch { /* the `pi` on PATH, below */ }
|
|
100
111
|
for (const dir of (process.env.PATH ?? "").split(path.delimiter)) {
|
|
101
112
|
if (!path.isAbsolute(dir)) continue;
|
|
102
113
|
let root;
|
|
@@ -113,7 +124,7 @@ function piHost() {
|
|
|
113
124
|
// entrypoint, the Node running it and the Pi host. Source checkouts never
|
|
114
125
|
// register, and a failure here never blocks the command. Asking for help or the
|
|
115
126
|
// version changes nothing in the operator's agent directory.
|
|
116
|
-
function recordRuntime() {
|
|
127
|
+
async function recordRuntime() {
|
|
117
128
|
try {
|
|
118
129
|
if (forwardedArgs.some((value) => ["--help", "-h", "--version", "-v"].includes(value))) return;
|
|
119
130
|
if (!packageRoot.split(path.sep).includes("node_modules")) return;
|
|
@@ -129,7 +140,7 @@ function recordRuntime() {
|
|
|
129
140
|
version: JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8")).version,
|
|
130
141
|
entrypoint: fs.realpathSync(path.join(packageRoot, "scripts", "piagent-studio.mjs")),
|
|
131
142
|
node: fs.realpathSync(process.execPath),
|
|
132
|
-
pi_sdk_root: piHost()
|
|
143
|
+
pi_sdk_root: await piHost()
|
|
133
144
|
}, null, 2)}\n`;
|
|
134
145
|
const file = path.join(agentDir, "piagent-runtime.json");
|
|
135
146
|
try { if (fs.readFileSync(file, "utf8") === body) return; } catch { /* first run */ }
|
|
@@ -138,7 +149,7 @@ function recordRuntime() {
|
|
|
138
149
|
fs.renameSync(temporary, file);
|
|
139
150
|
} catch { /* optional: Agent Watch falls back to known install locations */ }
|
|
140
151
|
}
|
|
141
|
-
recordRuntime();
|
|
152
|
+
await recordRuntime();
|
|
142
153
|
|
|
143
154
|
const sourceRoot = invokedAs === "piagent-benchmark" ? benchmarkSourceRoot() : packageRoot;
|
|
144
155
|
const target = path.join(sourceRoot, script);
|
|
@@ -7,7 +7,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
7
7
|
|
|
8
8
|
import { requestGatewayControl } from "../packages/piagent-webui/gateway/control-socket.ts";
|
|
9
9
|
import { browserCommand } from "../packages/piagent-core/runtime/host/browser-opener.mjs";
|
|
10
|
-
import {
|
|
10
|
+
import { pinnedPiHostRoot } from "../packages/piagent-webui/gateway/pi-host.ts";
|
|
11
11
|
import { gatewayProfileState, profileRef, readGatewayDescriptor, readOrCreateCatalogKey,
|
|
12
12
|
removeGatewayDescriptor } from "../packages/piagent-webui/gateway/profile-state.ts";
|
|
13
13
|
import { startPiagentGateway } from "../packages/piagent-webui/gateway/gateway-service.ts";
|
|
@@ -266,7 +266,7 @@ async function main() {
|
|
|
266
266
|
const key = readOrCreateCatalogKey(state), descriptor = readGatewayDescriptor(state);
|
|
267
267
|
let actual = null;
|
|
268
268
|
try {
|
|
269
|
-
const hostRoot =
|
|
269
|
+
const hostRoot = pinnedPiHostRoot(expectedPiVersion);
|
|
270
270
|
actual = JSON.parse(fs.readFileSync(path.join(hostRoot, "package.json"), "utf8")).version;
|
|
271
271
|
} catch { /* reported as a failed check below */ }
|
|
272
272
|
let health = null;
|
|
@@ -24,6 +24,46 @@
|
|
|
24
24
|
"decided": "2026-10-24",
|
|
25
25
|
"until": "2026-11-07",
|
|
26
26
|
"reason": "Subresource Integrity check bypassed with a non-lowercase algorithm token; Pi does not fetch with integrity metadata. Pinned by the Pi host as above; fixed in undici 8.12.2."
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"id": "GHSA-4w7w-5xrc-9h26",
|
|
30
|
+
"package": "undici",
|
|
31
|
+
"node": "node_modules/undici",
|
|
32
|
+
"decided": "2026-10-28",
|
|
33
|
+
"until": "2026-11-07",
|
|
34
|
+
"reason": "Cache poisoning and cross-user disclosure through cross-origin redirects in undici's cache; Pi does not share an HTTP cache between users. Pinned by the Pi host as above (8.10.2 in 1.3.0 and 1.4.0); fixed in undici 8.12.2."
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"id": "GHSA-2m5v-pch6-74hx",
|
|
38
|
+
"package": "undici",
|
|
39
|
+
"node": "node_modules/undici",
|
|
40
|
+
"decided": "2026-10-28",
|
|
41
|
+
"until": "2026-11-07",
|
|
42
|
+
"reason": "TLS certificate validation bypassed when an Agent drops its tls options; affects callers that pass custom tls settings to an undici Agent. Pinned by the Pi host as above; fixed in undici 8.12.2. Owner accepted on 2026-10-28 to keep releases going; revisit as soon as a Pi host carries undici >= 8.12.2."
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"id": "GHSA-qrxm-fqhp-jwvm",
|
|
46
|
+
"package": "undici",
|
|
47
|
+
"node": "node_modules/undici",
|
|
48
|
+
"decided": "2026-10-28",
|
|
49
|
+
"until": "2026-11-07",
|
|
50
|
+
"reason": "Denial of service through a TLS 1.2 certificate-changing renegotiation by a hostile server; no data exposure. Pinned by the Pi host as above; fixed in undici 8.12.2."
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"id": "GHSA-9fm3-97px-rjf6",
|
|
54
|
+
"package": "undici",
|
|
55
|
+
"node": "node_modules/undici",
|
|
56
|
+
"decided": "2026-10-28",
|
|
57
|
+
"until": "2026-11-07",
|
|
58
|
+
"reason": "Denial of service from unbounded automatic PONG buffering in the WebSocket client fed by a hostile server; no data exposure. Pinned by the Pi host as above; fixed in undici 8.12.2."
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"id": "GHSA-55wc-3pg8-jfw6",
|
|
62
|
+
"package": "undici",
|
|
63
|
+
"node": "node_modules/undici",
|
|
64
|
+
"decided": "2026-10-28",
|
|
65
|
+
"until": "2026-11-07",
|
|
66
|
+
"reason": "Denial of service from an unhandled exception in the decompress interceptor; no data exposure. Pinned by the Pi host as above; fixed in undici 8.12.2."
|
|
27
67
|
}
|
|
28
68
|
]
|
|
29
69
|
}
|
package/scripts/setup.sh
CHANGED
|
@@ -80,6 +80,7 @@ CONFIGURE_MODEL_SCOPE=true
|
|
|
80
80
|
MODEL_SCOPE_PRESET="full"
|
|
81
81
|
DEFAULT_MODEL="openai-codex/gpt-6-sol:high"
|
|
82
82
|
AUTO_INSTALL_PI=true
|
|
83
|
+
PI_HOST_PRIVATE=false
|
|
83
84
|
FORCE_PROFILE=false
|
|
84
85
|
FORCE_SETTINGS=false
|
|
85
86
|
SKIP_AGENTS=false
|
|
@@ -347,6 +348,19 @@ ensure_pi_cli() {
|
|
|
347
348
|
if [[ "$current_version" == "$expected_pi_version" ]]; then
|
|
348
349
|
return
|
|
349
350
|
fi
|
|
351
|
+
# A newer pi is the member's own: it stays, and the pinned host is
|
|
352
|
+
# installed privately beside it (scripts/pi-host.mjs).
|
|
353
|
+
if [[ -n "$current_version" ]] && [[ "$(node -e 'const [a,b]=process.argv.slice(1).map(v=>v.split(/[+-]/)[0].split(".").map(Number)); for (let i=0;i<3;i++) if (a[i]!==b[i]) { process.stdout.write(a[i]>b[i]?"newer":"older"); process.exit(0); } process.stdout.write("same");' "$current_version" "$expected_pi_version")" == "newer" ]]; then
|
|
354
|
+
echo "Pi CLI $current_version is your own and stays as it is; Piagent runs on a private Pi $expected_pi_version."
|
|
355
|
+
local ensure_args=(ensure --force)
|
|
356
|
+
[[ "$DRY_RUN" == true ]] && ensure_args+=(--dry-run)
|
|
357
|
+
if ! node "$PLATFORM_ROOT/scripts/pi-host.mjs" "${ensure_args[@]}"; then
|
|
358
|
+
echo "FAIL: could not install the private Pi $expected_pi_version." >&2
|
|
359
|
+
exit 1
|
|
360
|
+
fi
|
|
361
|
+
PI_HOST_PRIVATE=true
|
|
362
|
+
return
|
|
363
|
+
fi
|
|
350
364
|
if [[ "$AUTO_INSTALL_PI" == false ]]; then
|
|
351
365
|
echo "FAIL: Pi Coding Agent $expected_pi_version is required; found ${current_version:-${current_output:-unknown}}. Remove --no-install-pi or upgrade it manually." >&2
|
|
352
366
|
exit 1
|
|
@@ -490,7 +504,11 @@ echo "Next:"
|
|
|
490
504
|
if [[ "$DO_PROJECT" == true ]]; then
|
|
491
505
|
echo " cd \"$PROJECT_PATH\""
|
|
492
506
|
fi
|
|
493
|
-
|
|
507
|
+
if [[ "$PI_HOST_PRIVATE" == true ]]; then
|
|
508
|
+
echo " piagent pi # Pi $(node -p 'require(process.argv[1]).peerDependencies["@earendil-works/pi-coding-agent"]' "$PLATFORM_ROOT/package.json"), the release Piagent is qualified on; your own pi also loads Piagent"
|
|
509
|
+
else
|
|
510
|
+
echo " pi"
|
|
511
|
+
fi
|
|
494
512
|
echo " /login # first time only"
|
|
495
513
|
if [[ "$DO_PROJECT" == true ]]; then
|
|
496
514
|
echo " /model # or Ctrl+L: select provider/model from Pi selector"
|