@piagent/platform 1.12.1 → 1.13.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 +33 -0
- package/README.md +3 -3
- package/README.vi.md +5 -1
- package/docs/command-reference-vietnamese.md +2 -2
- package/docs/distribution-standard.md +6 -6
- package/docs/en/README.md +1 -0
- package/docs/en/windows.md +66 -0
- package/docs/languages.json +13 -4
- package/docs/operator-manual-vietnamese.md +7 -7
- package/docs/publishing-for-teams.md +1 -0
- package/docs/release-install-policy.md +2 -2
- package/docs/team-onboarding.md +2 -2
- package/docs/vi/README.md +1 -0
- package/docs/vi/windows.md +66 -0
- 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 +13 -0
- package/docs-site/content/index.html +4 -4
- package/docs-site/content/whats-new.html +13 -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 +14 -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 +14 -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/piagent-guard.ts +3 -1
- package/packages/piagent-core/extensions/shell-write-targets.js +2 -1
- package/packages/piagent-core/extensions/source-checkout-paths.ts +4 -3
- package/packages/piagent-core/extensions/task-journal.js +2 -1
- package/packages/piagent-core/extensions/task-state.js +3 -3
- package/packages/piagent-core/managed/broker-client.mjs +2 -1
- package/packages/piagent-core/managed/doctor.mjs +5 -3
- package/packages/piagent-core/managed/language-environment.mjs +16 -6
- package/packages/piagent-core/managed/linux-sandbox.mjs +81 -0
- package/packages/piagent-core/managed/session.mjs +4 -1
- package/packages/piagent-core/managed/store.d.mts +2 -0
- package/packages/piagent-core/managed/store.mjs +12 -0
- package/packages/piagent-core/managed/tool-boundary.mjs +41 -14
- package/packages/piagent-core/managed/toolchain.mjs +5 -2
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/runtime/host/browser-opener.d.mts +2 -0
- package/packages/piagent-core/runtime/host/browser-opener.mjs +20 -0
- package/packages/piagent-core/runtime/input/chat-images.ts +11 -4
- package/packages/piagent-core/runtime/resources/agent-resources.mjs +4 -4
- package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-FWfOWZjV.js → MarkdownMessage-D8wcWkJg.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/{index-DFKHLVYk.js → index-DWL_wVw7.js} +3 -3
- package/packages/piagent-webui/dist/client/index.html +1 -1
- package/packages/piagent-webui/extension/piagent-webui.ts +6 -5
- package/packages/piagent-webui/gateway/control-socket.ts +14 -2
- package/packages/piagent-webui/gateway/managed-launch.ts +8 -6
- package/packages/piagent-webui/gateway/native-project-picker.ts +28 -3
- package/packages/piagent-webui/gateway/path-suggestions.ts +9 -3
- package/packages/piagent-webui/gateway/pi-host.ts +11 -3
- package/scripts/dashboard-update-job.mjs +3 -2
- package/scripts/piagent-cli.mjs +2 -2
- package/scripts/piagent-dashboard.mjs +7 -7
- package/scripts/piagent-studio.mjs +4 -4
- package/scripts/piagent-webui-launcher.mjs +4 -3
|
@@ -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-DWL_wVw7.js"></script>
|
|
11
11
|
<link rel="stylesheet" crossorigin href="/assets/index-Z91z6SlN.css">
|
|
12
12
|
</head>
|
|
13
13
|
<body>
|
|
@@ -3,7 +3,7 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
7
|
|
|
8
8
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
|
|
@@ -31,6 +31,7 @@ import { SourceMutationController } from "./source-mutation-controller.ts";
|
|
|
31
31
|
import { SourceRevertController } from "./source-revert-controller.ts";
|
|
32
32
|
import { SourceOpenController } from "./source-open-controller.ts";
|
|
33
33
|
import { VSCodeHandoff } from "./vscode-handoff.ts";
|
|
34
|
+
import { browserCommand } from "../../piagent-core/runtime/host/browser-opener.mjs";
|
|
34
35
|
import { TerminalSessionAdapter } from "./terminal-session-adapter.ts";
|
|
35
36
|
import { isGatewayRuntimeContext } from "../ownership/gateway-runtime-context.ts";
|
|
36
37
|
|
|
@@ -124,9 +125,9 @@ function removeControlDirectory(directory: string): void {
|
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
function openLocalUrl(url: string): boolean {
|
|
127
|
-
const
|
|
128
|
-
if (!
|
|
129
|
-
try { const child = spawn(command,
|
|
128
|
+
const opener = browserCommand(url);
|
|
129
|
+
if (!opener) return false;
|
|
130
|
+
try { const child = spawn(opener.command, opener.args, { detached: true, stdio: "ignore", ...opener.options }); child.unref(); return true; }
|
|
130
131
|
catch { return false; }
|
|
131
132
|
}
|
|
132
133
|
|
|
@@ -342,7 +343,7 @@ export default function piagentWebUiExtension(pi: ExtensionAPI): void {
|
|
|
342
343
|
fs.chmodSync(socketDirectory, 0o700);
|
|
343
344
|
const socket = path.join(socketDirectory, "control.sock");
|
|
344
345
|
const controlToken = randomBytes(32).toString("base64url");
|
|
345
|
-
const loader = path.join(PLATFORM_ROOT, "scripts", "register-typescript-loader.mjs");
|
|
346
|
+
const loader = pathToFileURL(path.join(PLATFORM_ROOT, "scripts", "register-typescript-loader.mjs")).href;
|
|
346
347
|
const main = path.join(PACKAGE_ROOT, "server", "sidecar-main.ts");
|
|
347
348
|
const child = spawn(process.execPath, ["--disable-warning=ExperimentalWarning", "--import", loader, main], {
|
|
348
349
|
cwd: ctx.cwd, env: { PATH: process.env.PATH ?? "", TMPDIR: process.env.TMPDIR ?? os.tmpdir(), NO_COLOR: "1" },
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import net from "node:net";
|
|
3
|
+
import path from "node:path";
|
|
3
4
|
|
|
4
5
|
const MAX_MESSAGE_BYTES = 4_096;
|
|
5
6
|
// The folder list grows with every project the company runtime has seen.
|
|
@@ -60,7 +61,8 @@ export async function startGatewayControlSocket(options: {
|
|
|
60
61
|
socketPath: string;
|
|
61
62
|
handle(request: GatewayControlRequest): Promise<GatewayControlResponse> | GatewayControlResponse;
|
|
62
63
|
}): Promise<{ close(): Promise<void> }> {
|
|
63
|
-
|
|
64
|
+
const windows = process.platform === "win32";
|
|
65
|
+
if (!windows) await removeStaleSocket(options.socketPath);
|
|
64
66
|
const server = net.createServer((socket) => {
|
|
65
67
|
socket.setEncoding("utf8");
|
|
66
68
|
let body = "", finished = false;
|
|
@@ -79,7 +81,7 @@ export async function startGatewayControlSocket(options: {
|
|
|
79
81
|
const register = parsed?.action === "project.register";
|
|
80
82
|
if (!parsed || !["health", "issue-launch-url", "stop", "project.paths", "project.register"].includes(String(parsed.action))
|
|
81
83
|
|| Object.keys(parsed).some((key) => key !== "action" && !(register && key === "cwd"))
|
|
82
|
-
|| register && (typeof parsed.cwd !== "string" || !parsed.cwd
|
|
84
|
+
|| register && (typeof parsed.cwd !== "string" || !path.isAbsolute(parsed.cwd) || parsed.cwd.length > 4096)) throw new Error("invalid");
|
|
83
85
|
request = parsed as GatewayControlRequest;
|
|
84
86
|
} catch { socket.end(`${JSON.stringify({ ok: false, error: "invalid-request" })}\n`); return; }
|
|
85
87
|
try { socket.end(`${JSON.stringify(await options.handle(request))}\n`); }
|
|
@@ -88,6 +90,16 @@ export async function startGatewayControlSocket(options: {
|
|
|
88
90
|
});
|
|
89
91
|
socket.once("error", () => { socket.destroy(); });
|
|
90
92
|
});
|
|
93
|
+
// A Windows pipe is not a file: there is no stale one to remove and nothing
|
|
94
|
+
// to link. Its name stays taken while a Gateway listens on it.
|
|
95
|
+
if (windows) {
|
|
96
|
+
await new Promise<void>((resolve, reject) => {
|
|
97
|
+
server.once("error", (error: NodeJS.ErrnoException) => reject(error.code === "EADDRINUSE" ? new Error("gateway-already-running") : error));
|
|
98
|
+
server.listen(options.socketPath, resolve);
|
|
99
|
+
});
|
|
100
|
+
let closed: Promise<void> | null = null;
|
|
101
|
+
return { close: () => closed ??= new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve())) };
|
|
102
|
+
}
|
|
91
103
|
// Closing a server removes the path it was bound to, whoever holds that path
|
|
92
104
|
// by then: a Gateway that exited after another had taken the path removed the
|
|
93
105
|
// running Gateway's socket, which could no longer be stopped or asked for a
|
|
@@ -6,7 +6,7 @@ import path from "node:path";
|
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { requestGatewayControl } from "./control-socket.ts";
|
|
8
8
|
import { gatewayProfileState } from "./profile-state.ts";
|
|
9
|
-
import { storedSlot } from "../../piagent-core/managed/store.mjs";
|
|
9
|
+
import { agentWatchDataDirectory, interopEnvironment, storedSlot } from "../../piagent-core/managed/store.mjs";
|
|
10
10
|
|
|
11
11
|
// The personal dashboard shows company sessions but never runs them: it starts
|
|
12
12
|
// the pinned managed entrypoint (its own process and Gateway, `--serve`) the
|
|
@@ -15,7 +15,7 @@ import { storedSlot } from "../../piagent-core/managed/store.mjs";
|
|
|
15
15
|
export type ManagedLaunchStatus = { schemaVersion: 1; version: "piagent-managed-launch-status-v1"; available: boolean; model: "agent-watch-auto" };
|
|
16
16
|
type Binding = { file: string; profile: string; node: string; entrypoint: string; broker: string; brokerSha256: string };
|
|
17
17
|
|
|
18
|
-
const CLEAN_ENV = { PATH: "/usr/bin:/bin", LANG: "en_US.UTF-8", TERM: "xterm-256color" };
|
|
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
20
|
const sha256 = (file: string) => createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
|
21
21
|
const HEX64 = /^[a-f0-9]{64}$/;
|
|
@@ -51,7 +51,7 @@ export function managedLaunchStatus(agentDir = DEFAULT_AGENT_DIR): ManagedLaunch
|
|
|
51
51
|
// A key's slot points at its member's conversation store once it has been
|
|
52
52
|
// launched (store.mjs); until then the slot's own folder is asked.
|
|
53
53
|
function controlSocket(home: string, profile: string): string {
|
|
54
|
-
const root = path.join(home, "
|
|
54
|
+
const root = path.join(agentWatchDataDirectory(home), "ManagedSessions");
|
|
55
55
|
return gatewayProfileState(path.join(root, storedSlot(root, profile) ?? profile)).controlSocket;
|
|
56
56
|
}
|
|
57
57
|
|
|
@@ -66,7 +66,9 @@ async function running(socket: string): Promise<{ packageVersion?: unknown } | "
|
|
|
66
66
|
|
|
67
67
|
// Same foreground step as Agent Watch: macOS may ask once to let this build of
|
|
68
68
|
// the broker read its Keychain item. Nothing secret comes back. Exit 67 means
|
|
69
|
-
// the imported key is no longer connected in Agent Watch.
|
|
69
|
+
// the imported key is no longer connected in Agent Watch. From WSL the broker is
|
|
70
|
+
// Agent Watch for Windows, whose key has no prompt: any other failure means the
|
|
71
|
+
// key cannot be read on this computer.
|
|
70
72
|
async function authorize(pinned: Binding): Promise<void> {
|
|
71
73
|
if (fs.realpathSync(pinned.broker) !== pinned.broker || sha256(pinned.broker) !== pinned.brokerSha256) throw new Error("managed-launch-binding-changed");
|
|
72
74
|
const code = await new Promise<number | null>((resolve) => {
|
|
@@ -75,7 +77,7 @@ async function authorize(pinned: Binding): Promise<void> {
|
|
|
75
77
|
child.once("exit", (exit) => resolve(exit));
|
|
76
78
|
});
|
|
77
79
|
if (code === 67) throw new Error("managed-profile-disconnected");
|
|
78
|
-
if (code !== 0) throw new Error("managed-keychain-approval-required");
|
|
80
|
+
if (code !== 0) throw new Error(process.platform === "linux" ? "managed-key-unavailable" : "managed-keychain-approval-required");
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
// The control socket of a running company Gateway, without launching one.
|
|
@@ -117,7 +119,7 @@ async function start(agentDir: string, home: string, timeoutMs: number): Promise
|
|
|
117
119
|
await authorize(pinned);
|
|
118
120
|
// Startup failures are reported on stderr as `Agent Watch: managed-…`; the
|
|
119
121
|
// log is private and removed once the process is ready or has exited.
|
|
120
|
-
const directory = path.join(home, "
|
|
122
|
+
const directory = path.join(agentWatchDataDirectory(home), "ManagedLaunch");
|
|
121
123
|
fs.mkdirSync(directory, { recursive: true, mode: 0o700 }); fs.chmodSync(directory, 0o700);
|
|
122
124
|
const log = path.join(directory, `launch-${process.pid}-${Date.now()}.log`);
|
|
123
125
|
const fd = fs.openSync(log, "wx", 0o600);
|
|
@@ -1,9 +1,22 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
1
|
+
import { execFile, execFileSync } from "node:child_process";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
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
|
+
type PickerSpec = { executable: string; args: string[]; wsl?: boolean };
|
|
8
|
+
|
|
9
|
+
// Windows' folder dialog, through PowerShell (also from WSL, where Windows
|
|
10
|
+
// programs run as they are). It prints the chosen folder; cancel exits 1.
|
|
11
|
+
const WINDOWS_PICKER = [
|
|
12
|
+
"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8",
|
|
13
|
+
"Add-Type -AssemblyName System.Windows.Forms",
|
|
14
|
+
"$dialog = New-Object System.Windows.Forms.FolderBrowserDialog",
|
|
15
|
+
"$dialog.Description = 'Choose a project folder for Piagent'",
|
|
16
|
+
"if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { [Console]::Out.WriteLine($dialog.SelectedPath); exit 0 } else { exit 1 }"
|
|
17
|
+
].join("; ");
|
|
18
|
+
const windowsPicker = (executable: string, wsl = false): PickerSpec =>
|
|
19
|
+
({ executable, args: ["-NoProfile", "-NonInteractive", "-STA", "-Command", WINDOWS_PICKER], ...(wsl ? { wsl } : {}) });
|
|
7
20
|
type PickerEnvironment = Readonly<Record<string, string | undefined>>;
|
|
8
21
|
|
|
9
22
|
function executableOnPath(name: string, environment: PickerEnvironment): string | null {
|
|
@@ -32,6 +45,14 @@ export function resolveNativeProjectPicker(options: { platform?: NodeJS.Platform
|
|
|
32
45
|
].join("\n");
|
|
33
46
|
return { executable: "/usr/bin/osascript", args: ["-e", script] };
|
|
34
47
|
}
|
|
48
|
+
if (platform === "win32") {
|
|
49
|
+
const executable = path.win32.join(environment.SystemRoot ?? "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
50
|
+
return fs.existsSync(executable) ? windowsPicker(executable) : null;
|
|
51
|
+
}
|
|
52
|
+
if (platform === "linux" && environment.WSL_DISTRO_NAME) {
|
|
53
|
+
const executable = executableOnPath("powershell.exe", environment);
|
|
54
|
+
if (executable) return windowsPicker(executable, true);
|
|
55
|
+
}
|
|
35
56
|
if (platform !== "linux" || !(environment.DISPLAY || environment.WAYLAND_DISPLAY)) return null;
|
|
36
57
|
for (const name of ["zenity", "qarma"]) {
|
|
37
58
|
const executable = executableOnPath(name, environment);
|
|
@@ -71,7 +92,11 @@ export function pickNativeProjectFolders(): Promise<string[]> {
|
|
|
71
92
|
? (error as unknown as { code: number }).code : null;
|
|
72
93
|
reject(new Error(exitCode === 1 ? "project-import-cancelled" : "project-import-picker-failed")); return;
|
|
73
94
|
}
|
|
74
|
-
|
|
95
|
+
let selected = selectedFolders(stdout.replace(/\r/g, ""));
|
|
96
|
+
// C:\Users\me\shop, as seen from WSL: /mnt/c/Users/me/shop.
|
|
97
|
+
if (picker.wsl) selected = selected.flatMap((folder) => {
|
|
98
|
+
try { return [execFileSync("wslpath", ["-u", folder], { encoding: "utf8", timeout: 5000 }).trim()]; } catch { return []; }
|
|
99
|
+
});
|
|
75
100
|
if (!selected.length) { reject(new Error("project-import-cancelled")); return; }
|
|
76
101
|
resolve(selected);
|
|
77
102
|
});
|
|
@@ -24,12 +24,18 @@ const either = (value: string) => value.normalize("NFC") === value.normalize("NF
|
|
|
24
24
|
: `(?:${escape(value.normalize("NFC"))}|${escape(value.normalize("NFD"))})`;
|
|
25
25
|
const inside = (file: string, root: string) => file === root || file.startsWith(root + path.sep);
|
|
26
26
|
|
|
27
|
+
// fd where it is commonly installed, then on PATH (Debian and Ubuntu name it
|
|
28
|
+
// fdfind; Windows adds .exe).
|
|
27
29
|
export function findFd(home = os.homedir()): string | null {
|
|
28
|
-
|
|
30
|
+
const names = process.platform === "win32" ? ["fd.exe"] : ["fd", "fdfind"];
|
|
31
|
+
const onPath = String(process.env.PATH ?? "").split(path.delimiter).filter(Boolean).flatMap((dir) => names.map((name) => path.join(dir, name)));
|
|
32
|
+
for (const candidate of ["/opt/homebrew/bin/fd", "/usr/local/bin/fd", path.join(home, ".pi/agent/bin", names[0]), ...onPath]) {
|
|
29
33
|
try { fs.accessSync(candidate, fs.constants.X_OK); return candidate; } catch { /* next */ }
|
|
30
34
|
}
|
|
31
35
|
return null;
|
|
32
36
|
}
|
|
37
|
+
// macOS: the system's git, always there; elsewhere git from PATH.
|
|
38
|
+
const GIT = process.platform === "darwin" ? "/usr/bin/git" : "git";
|
|
33
39
|
|
|
34
40
|
// Higher is better: the name itself, then its start, then anywhere in it,
|
|
35
41
|
// then anywhere in the path; a folder before a file with the same score.
|
|
@@ -59,7 +65,7 @@ async function listed(folder: string, signal?: AbortSignal): Promise<Entry[]> {
|
|
|
59
65
|
}
|
|
60
66
|
} catch { /* unreadable */ }
|
|
61
67
|
if (!entries.length) return entries;
|
|
62
|
-
const ignored = new Set((await run(
|
|
68
|
+
const ignored = new Set((await run(GIT, ["-C", folder, "check-ignore", "-z", "--stdin"], signal, entries.map((entry) => `${entry.path}\0`).join("")))
|
|
63
69
|
.split("\0").filter(Boolean));
|
|
64
70
|
return ignored.size ? entries.filter((entry) => !ignored.has(entry.path)) : entries;
|
|
65
71
|
}
|
|
@@ -75,7 +81,7 @@ async function searched(folder: string, query: string, options: { fd: string | n
|
|
|
75
81
|
.map((line) => ({ path: line.replace(/^\.\//, "").replace(/\/$/, ""), directory: line.endsWith("/") }));
|
|
76
82
|
}
|
|
77
83
|
if (!options.atRoot) return [];
|
|
78
|
-
const files = (await run(
|
|
84
|
+
const files = (await run(GIT, ["-C", folder, "ls-files", "--cached", "--others", "--exclude-standard", "-z"], options.signal))
|
|
79
85
|
.split("\0").filter(Boolean).slice(0, 50_000);
|
|
80
86
|
const folders = new Set<string>();
|
|
81
87
|
for (const file of files) for (let dir = path.posix.dirname(file); dir !== "."; dir = path.posix.dirname(dir)) folders.add(dir);
|
|
@@ -29,8 +29,12 @@ export function installedPiHostRoot(): string {
|
|
|
29
29
|
// The operator installation is normally global.
|
|
30
30
|
}
|
|
31
31
|
try {
|
|
32
|
-
const
|
|
33
|
-
const
|
|
32
|
+
const windows = process.platform === "win32";
|
|
33
|
+
const executable = execFileSync(windows ? "where" : "which", ["pi"], { encoding: "utf8", timeout: 2_000, stdio: ["ignore", "pipe", "ignore"] }).trim().split(/\r?\n/)[0];
|
|
34
|
+
// npm on Windows puts the pi.cmd shim in the global prefix and the package
|
|
35
|
+
// in node_modules beside it.
|
|
36
|
+
const found = packageRootFrom(fs.realpathSync(executable))
|
|
37
|
+
?? (windows ? packageRootFrom(path.join(path.dirname(executable), "node_modules", "@earendil-works", "pi-coding-agent", "package.json")) : null);
|
|
34
38
|
if (found) return found;
|
|
35
39
|
} catch {
|
|
36
40
|
// A launcher with a short PATH (Agent Watch, launchd) cannot see `pi`.
|
|
@@ -42,8 +46,12 @@ export function installedPiHostRoot(): string {
|
|
|
42
46
|
const agentDir = process.env.PI_CODING_AGENT_DIR || path.join(home, ".pi", "agent");
|
|
43
47
|
recorded = JSON.parse(fs.readFileSync(path.join(agentDir, "piagent-runtime.json"), "utf8")).pi_sdk_root ?? null;
|
|
44
48
|
} catch { /* no record yet */ }
|
|
49
|
+
// npm's default Windows prefixes (%APPDATA%\npm, or beside node.exe) have no lib folder.
|
|
50
|
+
const appData = process.env.APPDATA ?? path.join(home, "AppData", "Roaming");
|
|
51
|
+
const windowsHosts = process.platform !== "win32" ? []
|
|
52
|
+
: [path.join(appData, "npm"), path.dirname(process.execPath)].map((prefix) => path.join(prefix, "node_modules", "@earendil-works", "pi-coding-agent"));
|
|
45
53
|
for (const candidate of [recorded, path.join(home, ".pi", "npm-global", hostPackage), path.join(home, ".local", hostPackage),
|
|
46
|
-
path.join("/opt/homebrew", hostPackage), path.join("/usr/local", hostPackage)]) {
|
|
54
|
+
path.join("/opt/homebrew", hostPackage), path.join("/usr/local", hostPackage), ...windowsHosts]) {
|
|
47
55
|
if (!candidate) continue;
|
|
48
56
|
const found = packageRootFrom(path.join(candidate, "package.json"));
|
|
49
57
|
if (found) return found;
|
|
@@ -13,7 +13,7 @@ import fs from "node:fs";
|
|
|
13
13
|
import os from "node:os";
|
|
14
14
|
import path from "node:path";
|
|
15
15
|
import { spawnSync } from "node:child_process";
|
|
16
|
-
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
17
17
|
|
|
18
18
|
const RELEASE = /^(0|[1-9]\d{0,8})\.(0|[1-9]\d{0,8})\.(0|[1-9]\d{0,8})$/;
|
|
19
19
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -70,7 +70,8 @@ try {
|
|
|
70
70
|
fs.writeSync(log, "\nRestarting the dashboard on the new release\n");
|
|
71
71
|
// Like the \`piagent\` command, with the package's TypeScript loader: an npm
|
|
72
72
|
// install sits under node_modules, where Node does not strip types itself.
|
|
73
|
-
|
|
73
|
+
// --import takes a URL: a Windows path (C:\...) reads as a URL scheme.
|
|
74
|
+
const loader = pathToFileURL(path.join(packageRoot, "scripts", "register-typescript-loader.mjs")).href;
|
|
74
75
|
const restart = spawnSync(process.execPath, ["--disable-warning=ExperimentalWarning", "--import", loader, dashboard, "restart", "--json"],
|
|
75
76
|
{ stdio: ["ignore", "ignore", log], env, timeout: 3 * 60 * 1000 });
|
|
76
77
|
finish("succeeded", { installed, ...(bindingChanged ? { bindingChanged } : {}), ...(restart.status === 0 ? {} : { reason: "dashboard-restart-failed" }) });
|
package/scripts/piagent-cli.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { spawn } from "node:child_process";
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
7
|
|
|
8
8
|
const scriptByCommand = {
|
|
9
9
|
"piagent-setup": "scripts/setup.sh",
|
|
@@ -140,7 +140,7 @@ const sourceRoot = invokedAs === "piagent-benchmark" ? benchmarkSourceRoot() : p
|
|
|
140
140
|
const target = path.join(sourceRoot, script);
|
|
141
141
|
const runner = target.endsWith(".mjs") ? process.execPath : "bash";
|
|
142
142
|
const runnerArgs = target.endsWith(".mjs")
|
|
143
|
-
? ["--disable-warning=ExperimentalWarning", "--import", path.join(sourceRoot, "scripts", "register-typescript-loader.mjs"), target]
|
|
143
|
+
? ["--disable-warning=ExperimentalWarning", "--import", pathToFileURL(path.join(sourceRoot, "scripts", "register-typescript-loader.mjs")).href, target]
|
|
144
144
|
: [target];
|
|
145
145
|
const child = spawn(runner, [...runnerArgs, ...forwardedArgs], {
|
|
146
146
|
cwd: process.cwd(),
|
|
@@ -3,9 +3,10 @@ import { spawn, spawnSync } from "node:child_process";
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
7
|
|
|
8
8
|
import { requestGatewayControl } from "../packages/piagent-webui/gateway/control-socket.ts";
|
|
9
|
+
import { browserCommand } from "../packages/piagent-core/runtime/host/browser-opener.mjs";
|
|
9
10
|
import { installedPiHostRoot } from "../packages/piagent-webui/gateway/pi-host.ts";
|
|
10
11
|
import { gatewayProfileState, profileRef, readGatewayDescriptor, readOrCreateCatalogKey,
|
|
11
12
|
removeGatewayDescriptor } from "../packages/piagent-webui/gateway/profile-state.ts";
|
|
@@ -32,11 +33,10 @@ function output(value) {
|
|
|
32
33
|
|
|
33
34
|
function openBrowser(url) {
|
|
34
35
|
if (noOpen) return;
|
|
35
|
-
const opener =
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (result.status !== 0) process.stderr.write(`Dashboard is ready at ${url}\n`);
|
|
36
|
+
const opener = browserCommand(url);
|
|
37
|
+
const result = opener ? spawnSync(opener.command, opener.args, { stdio: "ignore", ...opener.options }) : null;
|
|
38
|
+
// explorer.exe (WSL) exits 1 even when it opened the page.
|
|
39
|
+
if (!result || result.error || (result.status !== 0 && !/explorer\.exe$/.test(opener.command))) process.stderr.write(`Dashboard is ready at ${url}\n`);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
function processAlive(pid) {
|
|
@@ -170,7 +170,7 @@ async function ensureStarted() {
|
|
|
170
170
|
writeGatewayErrorLog(state);
|
|
171
171
|
const child = spawn(process.execPath, [
|
|
172
172
|
"--disable-warning=ExperimentalWarning",
|
|
173
|
-
"--import", path.join(packageRoot, "scripts", "register-typescript-loader.mjs"),
|
|
173
|
+
"--import", pathToFileURL(path.join(packageRoot, "scripts", "register-typescript-loader.mjs")).href,
|
|
174
174
|
fileURLToPath(import.meta.url),
|
|
175
175
|
"serve",
|
|
176
176
|
...(agentDir ? ["--agent-dir", agentDir] : [])
|
|
@@ -6,7 +6,7 @@ import { createHash } from 'node:crypto';
|
|
|
6
6
|
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
|
-
import { resolveStore, storedSlot } from '../packages/piagent-core/managed/store.mjs';
|
|
9
|
+
import { agentWatchDataDirectory, interopEnvironment, resolveStore, storedSlot } from '../packages/piagent-core/managed/store.mjs';
|
|
10
10
|
|
|
11
11
|
const digest = file => createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
12
12
|
// Node's experimental-feature notices (type stripping, SQLite) are not
|
|
@@ -23,7 +23,7 @@ for (let i = 0; i < args.length; i++) {
|
|
|
23
23
|
const home = os.homedir();
|
|
24
24
|
// Do this before importing the SDK: it must not discover personal keys, Node
|
|
25
25
|
// preload hooks, proxy overrides or provider endpoints from an ambient shell.
|
|
26
|
-
const clean = { PATH: '/usr/bin:/bin', HOME: home, LANG: 'en_US.UTF-8', TERM: process.env.TERM || 'xterm-256color' };
|
|
26
|
+
const clean = { PATH: '/usr/bin:/bin', HOME: home, LANG: 'en_US.UTF-8', TERM: process.env.TERM || 'xterm-256color', ...interopEnvironment() };
|
|
27
27
|
for (const key of Object.keys(process.env)) delete process.env[key];
|
|
28
28
|
Object.assign(process.env, clean);
|
|
29
29
|
let broker, current, gateway, hold;
|
|
@@ -53,7 +53,7 @@ try {
|
|
|
53
53
|
}
|
|
54
54
|
// Conversations live in one store per member on this machine, whichever key
|
|
55
55
|
// Agent Watch holds: a key's slot learns its member once, then points there.
|
|
56
|
-
const root = path.join(home, '
|
|
56
|
+
const root = path.join(agentWatchDataDirectory(home), 'ManagedSessions');
|
|
57
57
|
let store = storedSlot(root, config.profile_id);
|
|
58
58
|
if (!store) {
|
|
59
59
|
const asked = new ManagedBrokerClient({ executable: config.broker, profileID: config.profile_id });
|
|
@@ -150,6 +150,6 @@ try {
|
|
|
150
150
|
// Never print JSON payloads or startup objects that can contain role tokens.
|
|
151
151
|
const code = error instanceof Error && /^managed-[a-z:_-]+$/.test(error.message) ? error.message : 'managed-launch-failed';
|
|
152
152
|
const hint = code === 'managed-session-open-elsewhere' ? 'This conversation is running in the Piagent WebUI; continue it there, or start a new one here.'
|
|
153
|
-
: 'Refresh the Studio import or check Keychain access.';
|
|
153
|
+
: process.platform === 'linux' ? 'In Agent Watch for Windows, choose Connect Piagent in WSL again.' : 'Refresh the Studio import or check Keychain access.';
|
|
154
154
|
process.stderr.write(`Agent Watch: ${code}. ${hint}\n`); process.exitCode = 1;
|
|
155
155
|
} finally { await gateway?.close(); if (current) await current.dispose(); else await broker?.dispose(); hold?.release(); }
|
|
@@ -4,6 +4,7 @@ import fs from "node:fs";
|
|
|
4
4
|
import net from "node:net";
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
|
+
import { browserCommand } from "../packages/piagent-core/runtime/host/browser-opener.mjs";
|
|
7
8
|
|
|
8
9
|
const REF = /^[A-Za-z0-9][A-Za-z0-9._~-]{0,159}$/;
|
|
9
10
|
|
|
@@ -83,9 +84,9 @@ function requestLaunch(descriptor) {
|
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
function openUrl(url) {
|
|
86
|
-
const
|
|
87
|
-
if (!
|
|
88
|
-
const child = spawn(command,
|
|
87
|
+
const opener = browserCommand(url);
|
|
88
|
+
if (!opener) return false;
|
|
89
|
+
const child = spawn(opener.command, opener.args, { detached: true, stdio: "ignore", ...opener.options });
|
|
89
90
|
child.unref(); return true;
|
|
90
91
|
}
|
|
91
92
|
|