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 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; your Node/pi runtime's requirements also apply. Windows and other CPU architectures are not supported.
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.4",
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.4",
34
- "pi-famulus-linux-arm64": "0.1.4",
35
- "pi-famulus-darwin-x64": "0.1.4",
36
- "pi-famulus-darwin-arm64": "0.1.4"
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": [
@@ -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 type {
22
- AgentToolResult,
23
- BashToolDetails,
24
- ExtensionContext,
25
- ToolDefinition,
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
- const shell = process.env.SHELL && process.env.SHELL.length > 0 ? process.env.SHELL : "/bin/bash";
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, ["-c", params.command], {
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: join(home, "manager.sock"),
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", "pi-famulus")));
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", "pi-famulus");
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, "pi-famulus");
322
+ const candidate = join(dir, MANAGER_FILE_NAME);
287
323
  if (managerCandidateProblem(candidate) === null) return candidate;
288
324
  }
289
325
  return null;
@@ -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.4";
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
- if (existsSync(socketPath)) {
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;
@@ -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 four native optional packages are selected by npm's os/cpu constraints. */
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
- return `pi-famulus-${platform}-${arch}`;
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. */