pi-famulus 0.1.4 → 0.1.6
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/README.md +4 -1
- package/bin/pi-famulus.js +1 -1
- package/package.json +8 -6
- package/src/bash-override.ts +23 -8
- package/src/config.ts +43 -7
- package/src/manager-client.ts +3 -2
- package/src/native-manager.js +8 -3
package/README.md
CHANGED
|
@@ -20,8 +20,9 @@ Supported native packages:
|
|
|
20
20
|
| Linux | arm64 | `pi-famulus-linux-arm64` |
|
|
21
21
|
| macOS | Intel x64 | `pi-famulus-darwin-x64` |
|
|
22
22
|
| macOS | Apple Silicon arm64 | `pi-famulus-darwin-arm64` |
|
|
23
|
+
| Windows | x64 | `pi-famulus-win-x64` |
|
|
23
24
|
|
|
24
|
-
Linux binaries are statically linked with musl. macOS binaries target macOS 13 or newer;
|
|
25
|
+
Linux binaries are statically linked with musl. macOS binaries target macOS 13 or newer; Windows x64 binaries use MSVC with the CRT linked statically. Windows ARM64 is not published until CI produces that tarball. Your Node/pi runtime's requirements also apply. In-place manager upgrade remains Unix-only.
|
|
25
26
|
|
|
26
27
|
The npm package also exposes the `pi-famulus` CLI (`npx pi-famulus --help`). The extension's binary search order is:
|
|
27
28
|
|
|
@@ -31,6 +32,8 @@ The npm package also exposes the `pi-famulus` CLI (`npx pi-famulus --help`). The
|
|
|
31
32
|
4. Executable `~/.pi/agent/pi-famulus/bin/pi-famulus` (or the equivalent beneath `PI_FAMULUS_HOME`).
|
|
32
33
|
5. Executable `pi-famulus` on `PATH`.
|
|
33
34
|
|
|
35
|
+
On Windows the file name in steps 4 and 5 (and inside the native package) is `pi-famulus.exe`.
|
|
36
|
+
|
|
34
37
|
Configuration and runtime state remain in `~/.pi/agent/pi-famulus`. Installing/updating the npm package does not move that state or copy a binary into the shared home. The npm CLI uses the package's native executable; use the extension configuration above when intentionally running a separately built manager.
|
|
35
38
|
|
|
36
39
|
If optional dependencies were omitted, reinstall with them enabled. The CLI reports an actionable error; the extension can still discover an explicitly installed manager and otherwise enters degraded mode. An arbitrary different-version native npm package is never substituted.
|
package/bin/pi-famulus.js
CHANGED
|
@@ -7,7 +7,7 @@ if (!binary) {
|
|
|
7
7
|
const name = nativePackageName(process.platform, process.arch);
|
|
8
8
|
console.error(name
|
|
9
9
|
? `pi-famulus: missing usable ${name}. Reinstall pi-famulus with optional dependencies enabled (npm install --include=optional pi-famulus).`
|
|
10
|
-
: `pi-famulus: unsupported ${process.platform}/${process.arch}; only Linux/macOS x64/arm64 are supported.`);
|
|
10
|
+
: `pi-famulus: unsupported ${process.platform}/${process.arch}; only Linux/macOS/Windows x64/arm64 are supported.`);
|
|
11
11
|
process.exit(1);
|
|
12
12
|
}
|
|
13
13
|
const child = spawn(binary, process.argv.slice(2), { stdio: "inherit" });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-famulus",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Subagents, monitoring, and auto-backgrounding bash for pi",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
},
|
|
16
16
|
"os": [
|
|
17
17
|
"linux",
|
|
18
|
-
"darwin"
|
|
18
|
+
"darwin",
|
|
19
|
+
"win32"
|
|
19
20
|
],
|
|
20
21
|
"cpu": [
|
|
21
22
|
"x64",
|
|
@@ -30,10 +31,11 @@
|
|
|
30
31
|
"pi-famulus": "./bin/pi-famulus.js"
|
|
31
32
|
},
|
|
32
33
|
"optionalDependencies": {
|
|
33
|
-
"pi-famulus-linux-x64": "0.1.
|
|
34
|
-
"pi-famulus-linux-arm64": "0.1.
|
|
35
|
-
"pi-famulus-darwin-x64": "0.1.
|
|
36
|
-
"pi-famulus-darwin-arm64": "0.1.
|
|
34
|
+
"pi-famulus-linux-x64": "0.1.6",
|
|
35
|
+
"pi-famulus-linux-arm64": "0.1.6",
|
|
36
|
+
"pi-famulus-darwin-x64": "0.1.6",
|
|
37
|
+
"pi-famulus-darwin-arm64": "0.1.6",
|
|
38
|
+
"pi-famulus-win-x64": "0.1.6"
|
|
37
39
|
},
|
|
38
40
|
"pi": {
|
|
39
41
|
"extensions": [
|
package/src/bash-override.ts
CHANGED
|
@@ -18,11 +18,12 @@ import { writeFileSync } from "node:fs";
|
|
|
18
18
|
import { tmpdir } from "node:os";
|
|
19
19
|
import { join } from "node:path";
|
|
20
20
|
import { Type, type Static } from "typebox";
|
|
21
|
-
import
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
import {
|
|
22
|
+
getShellConfig,
|
|
23
|
+
type AgentToolResult,
|
|
24
|
+
type BashToolDetails,
|
|
25
|
+
type ExtensionContext,
|
|
26
|
+
type ToolDefinition,
|
|
26
27
|
} from "@earendil-works/pi-coding-agent";
|
|
27
28
|
import { taskOutputPath, type FamulusConfig } from "./config";
|
|
28
29
|
import { realClock, type Clock, type ClockTimer } from "./clock";
|
|
@@ -166,15 +167,29 @@ async function executeLocal(
|
|
|
166
167
|
): Promise<AgentToolResult<FamulusBashDetails | undefined>> {
|
|
167
168
|
const timeoutMs = resolveTimeoutMs(params.timeout);
|
|
168
169
|
const startedAtMs = clock.now();
|
|
169
|
-
|
|
170
|
+
// pi's own choice (Git Bash on Windows, /bin/bash on Unix): commands are
|
|
171
|
+
// written for pi's bash tool, not for whatever $SHELL is.
|
|
172
|
+
let shell: ReturnType<typeof getShellConfig>;
|
|
173
|
+
try {
|
|
174
|
+
shell = getShellConfig();
|
|
175
|
+
} catch (err) {
|
|
176
|
+
throw new Error(`Failed to start shell: ${(err as Error).message}`);
|
|
177
|
+
}
|
|
178
|
+
const viaStdin = shell.commandTransport === "stdin";
|
|
170
179
|
|
|
171
180
|
const output = await new Promise<{ text: string; exitCode: number | null; signal: string | null; timedOut: boolean; aborted: boolean }>(
|
|
172
181
|
(resolve, reject) => {
|
|
173
|
-
const child = spawn(shell, [
|
|
182
|
+
const child = spawn(shell.shell, viaStdin ? shell.args : [...shell.args, params.command], {
|
|
174
183
|
cwd: ctx.cwd,
|
|
175
184
|
env: process.env,
|
|
176
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
185
|
+
stdio: [viaStdin ? "pipe" : "ignore", "pipe", "pipe"],
|
|
186
|
+
windowsHide: true,
|
|
177
187
|
});
|
|
188
|
+
if (viaStdin) {
|
|
189
|
+
// Swallow EPIPE if the shell exits before consuming stdin (pi's runner does the same).
|
|
190
|
+
child.stdin?.on("error", () => {});
|
|
191
|
+
child.stdin?.end(params.command);
|
|
192
|
+
}
|
|
178
193
|
const chunks: Buffer[] = [];
|
|
179
194
|
let settled = false;
|
|
180
195
|
let timedOut = false;
|
package/src/config.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { accessSync, constants, readFileSync, statSync } from "node:fs";
|
|
8
8
|
import { homedir } from "node:os";
|
|
9
|
-
import { delimiter, join } from "node:path";
|
|
9
|
+
import { delimiter, join, win32 } from "node:path";
|
|
10
10
|
import { nativePackageName, resolveNativeManagerPath } from "./native-manager.js";
|
|
11
11
|
|
|
12
12
|
export interface FamulusConfig {
|
|
@@ -141,11 +141,43 @@ export function getFamulusHome(env: NodeJS.ProcessEnv = process.env): string {
|
|
|
141
141
|
return join(homedir(), ".pi", "agent", "pi-famulus");
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
/** FNV-1a 64-bit over UTF-8 bytes — must match manager `sys::fnv1a64`. */
|
|
145
|
+
export function fnv1a64(input: string): string {
|
|
146
|
+
let h = 0xcbf29ce484222325n;
|
|
147
|
+
for (const byte of Buffer.from(input, "utf8")) {
|
|
148
|
+
h ^= BigInt(byte);
|
|
149
|
+
h = (h * 0x0100000001b3n) & 0xffffffffffffffffn;
|
|
150
|
+
}
|
|
151
|
+
return h.toString(16);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Named-pipe identity of a home — must match manager `lifecycle::windows_pipe_ident`.
|
|
156
|
+
* The home is made absolute lexically (`path.win32.resolve` against `cwd`):
|
|
157
|
+
* `.` / `..` and repeated separators collapse, symlinks and junctions are
|
|
158
|
+
* not expanded. One spelling per directory (backslashes, no trailing
|
|
159
|
+
* separator except a drive root, lower case). The same relative home from
|
|
160
|
+
* two working directories must not share a pipe.
|
|
161
|
+
*/
|
|
162
|
+
export function windowsHomeKey(home: string, cwd: string = process.cwd()): string {
|
|
163
|
+
let s = win32.resolve(cwd, home);
|
|
164
|
+
while (s.length > 3 && s.endsWith("\\")) s = s.slice(0, -1);
|
|
165
|
+
return s.toLowerCase();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function windowsPipeName(home: string, cwd: string = process.cwd()): string {
|
|
169
|
+
return `\\\\.\\pipe\\pi-famulus-${fnv1a64(windowsHomeKey(home, cwd))}`;
|
|
170
|
+
}
|
|
171
|
+
|
|
144
172
|
/** Well-known paths inside the pi-famulus home directory (design doc §3.1). */
|
|
145
|
-
export function famulusPaths(home: string) {
|
|
173
|
+
export function famulusPaths(home: string, platform: NodeJS.Platform = process.platform) {
|
|
174
|
+
const socket =
|
|
175
|
+
platform === "win32"
|
|
176
|
+
? windowsPipeName(home)
|
|
177
|
+
: join(home, "manager.sock");
|
|
146
178
|
return {
|
|
147
179
|
home,
|
|
148
|
-
socket
|
|
180
|
+
socket,
|
|
149
181
|
pidFile: join(home, "manager.pid"),
|
|
150
182
|
spawnLock: join(home, "manager.spawn.lock"),
|
|
151
183
|
log: join(home, "manager.log"),
|
|
@@ -225,6 +257,9 @@ export function loadConfig(home: string = getFamulusHome()): FamulusConfig {
|
|
|
225
257
|
return config;
|
|
226
258
|
}
|
|
227
259
|
|
|
260
|
+
/** File name of the manager in <home>/bin and on PATH: Windows only starts `.exe` images. */
|
|
261
|
+
export const MANAGER_FILE_NAME = process.platform === "win32" ? "pi-famulus.exe" : "pi-famulus";
|
|
262
|
+
|
|
228
263
|
/** Return why a candidate is unusable, or null for an executable regular file. */
|
|
229
264
|
function managerCandidateProblem(path: string): string | null {
|
|
230
265
|
try {
|
|
@@ -260,14 +295,15 @@ export function describeManagerSearch(
|
|
|
260
295
|
tried.push(nativeName
|
|
261
296
|
? `npm ${nativeName}${native ? ` ${native}` : " (missing or unusable, ignored)"}`
|
|
262
297
|
: `npm native manager (unsupported ${process.platform}/${process.arch})`);
|
|
263
|
-
tried.push(describeCandidate(join(home, "bin",
|
|
298
|
+
tried.push(describeCandidate(join(home, "bin", MANAGER_FILE_NAME)));
|
|
264
299
|
tried.push("pi-famulus on PATH");
|
|
265
300
|
return tried.join("; ");
|
|
266
301
|
}
|
|
267
302
|
|
|
268
303
|
/**
|
|
269
304
|
* Resolve an executable regular pi-famulus binary, or null if none is usable.
|
|
270
|
-
* Priority: config.managerPath > PI_FAMULUS_MANAGER_PATH env > npm native package > <home>/bin/pi-famulus > PATH
|
|
305
|
+
* Priority: config.managerPath > PI_FAMULUS_MANAGER_PATH env > npm native package > <home>/bin/pi-famulus > PATH
|
|
306
|
+
* (`pi-famulus.exe` on Windows).
|
|
271
307
|
*/
|
|
272
308
|
export function resolveManagerPath(
|
|
273
309
|
config: FamulusConfig,
|
|
@@ -279,11 +315,11 @@ export function resolveManagerPath(
|
|
|
279
315
|
if (envPath && managerCandidateProblem(envPath) === null) return envPath;
|
|
280
316
|
const native = resolveNativeManagerPath();
|
|
281
317
|
if (native) return native;
|
|
282
|
-
const bundled = join(home, "bin",
|
|
318
|
+
const bundled = join(home, "bin", MANAGER_FILE_NAME);
|
|
283
319
|
if (managerCandidateProblem(bundled) === null) return bundled;
|
|
284
320
|
for (const dir of (env.PATH ?? "").split(delimiter)) {
|
|
285
321
|
if (!dir) continue;
|
|
286
|
-
const candidate = join(dir,
|
|
322
|
+
const candidate = join(dir, MANAGER_FILE_NAME);
|
|
287
323
|
if (managerCandidateProblem(candidate) === null) return candidate;
|
|
288
324
|
}
|
|
289
325
|
return null;
|
package/src/manager-client.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { famulusPaths } from "./config";
|
|
|
18
18
|
import { realClock, type Clock, type ClockTimer } from "./clock";
|
|
19
19
|
|
|
20
20
|
const MAX_FRAME_BYTES = 4 * 1024 * 1024; // 4 MiB (§3.3)
|
|
21
|
-
const EXTENSION_VERSION = "0.1.
|
|
21
|
+
const EXTENSION_VERSION = "0.1.6";
|
|
22
22
|
// 5: speaks per-kind agent admission and queued acquire.
|
|
23
23
|
const PROTOCOL = 5;
|
|
24
24
|
const HELLO_TIMEOUT_MS = 5000;
|
|
@@ -631,7 +631,8 @@ export class ManagerClient {
|
|
|
631
631
|
const deadline = this.now() + timeoutMs;
|
|
632
632
|
for (;;) {
|
|
633
633
|
this.checkStartupOpen();
|
|
634
|
-
|
|
634
|
+
// A named pipe is not a file: existsSync does not see it, connecting does.
|
|
635
|
+
if (process.platform === "win32" || existsSync(socketPath)) {
|
|
635
636
|
const ok = await this.openSocket(socketPath, Math.min(SOCKET_READY_POLL_MS, Math.max(0, deadline - this.now())))
|
|
636
637
|
.then((probe) => { probe.destroy(); return true; }, () => false);
|
|
637
638
|
if (ok) return;
|
package/src/native-manager.js
CHANGED
|
@@ -14,10 +14,15 @@ const resolutionErrorCodes = new Set([
|
|
|
14
14
|
const packageVersion = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
15
15
|
const resolveInstalled = createRequire(import.meta.url).resolve;
|
|
16
16
|
|
|
17
|
-
/** The
|
|
17
|
+
/** The native optional packages are selected by npm's os/cpu constraints. */
|
|
18
18
|
export function nativePackageName(platform, arch) {
|
|
19
|
-
if (!["linux", "darwin"].includes(platform) || !["x64", "arm64"].includes(arch)) return null;
|
|
20
|
-
|
|
19
|
+
if (!["linux", "darwin", "win32"].includes(platform) || !["x64", "arm64"].includes(arch)) return null;
|
|
20
|
+
// Windows ARM64 has no release producer yet; manual binary lookup still works.
|
|
21
|
+
if (platform === "win32" && arch === "arm64") return null;
|
|
22
|
+
// npm package names use `win` (not `win32`) after unscoped `*-win32-*` names
|
|
23
|
+
// were blocked by registry spam detection on first publish.
|
|
24
|
+
const packageOs = platform === "win32" ? "win" : platform;
|
|
25
|
+
return `pi-famulus-${packageOs}-${arch}`;
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
/** Resolve the exact-version optional package without loading native executable bytes. */
|