@neta-art/cohub-cli 10.3.0 → 10.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,7 @@ import { NATIVE_INGEST_MAX_TURNS, SETTLED_TURN_STATUSES } from "@neta-art/cohub"
4
4
  import { acceptsDirectory, acceptsTranscript, readSessionHeader, readTranscript, transcriptRoots } from "./adapters.js";
5
5
  import { nativeTurnId } from "./identity.js";
6
6
  import { ensurePlainCodexRollout } from "./transcript.js";
7
+ import { containsNativePath } from "./paths.js";
7
8
  const missing = (error) => error?.code === "ENOENT";
8
9
  /** Transcripts read in parallel per lane. The live lane never waits behind a backfill. */
9
10
  const LIVE_CONCURRENCY = 2;
@@ -39,9 +40,8 @@ const BOUNDARY_OVERLAP = Math.max(...Object.values(BOUNDARIES).flat().map((token
39
40
  const IMPORT_FAILURES_KEPT = 100;
40
41
  /** Headers are read this many at a time; each costs one small read. */
41
42
  const HEADER_CONCURRENCY = 16;
42
- const within = (root, cwd) => cwd === root || cwd.startsWith(`${root}/`);
43
43
  /** Whether a session in `cwd` belongs to the directory bound at `root`. */
44
- export const owns = (root, cwd, nested = []) => Boolean(cwd) && within(root, cwd) && !nested.some((inner) => within(inner, cwd));
44
+ export const owns = (root, cwd, nested = []) => Boolean(cwd) && containsNativePath(root, cwd) && !nested.some((inner) => containsNativePath(inner, cwd));
45
45
  /** Every transcript of the bound directory and its subdirectories, newest first. */
46
46
  export async function discoverTranscripts(cwd, harnesses, options = {}) {
47
47
  const found = [];
@@ -144,7 +144,7 @@ export class NativeIngest {
144
144
  return owns(this.options.cwd, cwd, this.nested);
145
145
  }
146
146
  exclude(nested) {
147
- this.nested = nested.filter((root) => root !== this.options.cwd && within(this.options.cwd, root));
147
+ this.nested = nested.filter((root) => !containsNativePath(root, this.options.cwd) && containsNativePath(this.options.cwd, root));
148
148
  }
149
149
  configure(harnesses, enabledAt) {
150
150
  const added = harnesses.filter((harness) => !this.harnesses.includes(harness));
@@ -560,7 +560,7 @@ export class NativeIngest {
560
560
  }
561
561
  harnessFor(path) {
562
562
  for (const harness of this.harnesses) {
563
- if (transcriptRoots(harness).some((root) => path.startsWith(`${root}/`)) && acceptsTranscript(path, harness, this.options.cwd))
563
+ if (transcriptRoots(harness).some((root) => containsNativePath(root, path)) && acceptsTranscript(path, harness, this.options.cwd))
564
564
  return harness;
565
565
  }
566
566
  return null;
@@ -0,0 +1,2 @@
1
+ /** Whether a native path is the root itself or one of its descendants. */
2
+ export declare function containsNativePath(root: string, path: string, platform?: NodeJS.Platform): boolean;
@@ -0,0 +1,7 @@
1
+ import { posix, win32 } from "node:path";
2
+ /** Whether a native path is the root itself or one of its descendants. */
3
+ export function containsNativePath(root, path, platform = process.platform) {
4
+ const paths = platform === "win32" ? win32 : posix;
5
+ const relative = paths.relative(root, path);
6
+ return relative === "" || relative !== ".." && !relative.startsWith(`..${paths.sep}`) && !paths.isAbsolute(relative);
7
+ }
@@ -1,6 +1,6 @@
1
1
  // Cohub control bridge for Pi. Self-contained: Node built-ins only, so it installs as one file.
2
2
  import { createHash } from "node:crypto";
3
- import { existsSync, realpathSync } from "node:fs";
3
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
4
4
  import { connect } from "node:net";
5
5
  import { homedir } from "node:os";
6
6
  import { dirname, join } from "node:path";
@@ -103,7 +103,17 @@ export default function cohub(pi) {
103
103
  const path = locateSocket(ctx.cwd);
104
104
  if (!path)
105
105
  return;
106
- const current = connect(path);
106
+ // On Windows the socket file names the Runtime's private named pipe.
107
+ let endpoint = path;
108
+ if (process.platform === "win32") {
109
+ try {
110
+ endpoint = readFileSync(path, "utf8").trim();
111
+ }
112
+ catch {
113
+ return;
114
+ }
115
+ }
116
+ const current = connect(endpoint);
107
117
  socket = current;
108
118
  buffer = "";
109
119
  current.on("connect", () => send({
@@ -1,5 +1,6 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import { createServer } from "node:net";
2
- import { chmod, mkdir, rm } from "node:fs/promises";
3
+ import { chmod, mkdir, rename, rm, writeFile } from "node:fs/promises";
3
4
  import { dirname } from "node:path";
4
5
  import { COHUB_PI_PROTOCOL } from "./pi-extension.js";
5
6
  const MAX_LINE_BYTES = 8 * 1024 * 1024;
@@ -68,17 +69,33 @@ export class PiLink {
68
69
  }
69
70
  async listen(signal) {
70
71
  const { socketPath } = this.options;
72
+ const windows = process.platform === "win32";
73
+ // Windows named pipes share one global namespace: listen on a random name and
74
+ // publish it at socketPath, so another account cannot pre-claim the endpoint.
75
+ const endpoint = windows ? `\\\\.\\pipe\\cohub-pi-${randomUUID()}` : socketPath;
71
76
  // The socket accepts commands that drive the user's agent: only this user may reach it.
72
77
  await mkdir(dirname(socketPath), { recursive: true, mode: 0o700 });
73
- if (process.platform !== "win32")
78
+ if (!windows)
74
79
  await chmod(dirname(socketPath), 0o700);
75
80
  await rm(socketPath, { force: true });
76
81
  const server = createServer((socket) => this.accept(socket));
77
82
  await new Promise((resolve, reject) => {
78
83
  server.once("error", reject);
79
- server.listen(socketPath, () => { server.removeListener("error", reject); resolve(); });
84
+ server.listen(endpoint, () => { server.removeListener("error", reject); resolve(); });
80
85
  });
81
- if (process.platform !== "win32")
86
+ if (windows) {
87
+ const temporary = `${socketPath}.${randomUUID()}.tmp`;
88
+ try {
89
+ await writeFile(temporary, endpoint, { mode: 0o600 });
90
+ await rename(temporary, socketPath);
91
+ }
92
+ catch (error) {
93
+ server.close();
94
+ await rm(temporary, { force: true });
95
+ throw error;
96
+ }
97
+ }
98
+ else
82
99
  await chmod(socketPath, 0o600);
83
100
  this.server = server;
84
101
  signal.addEventListener("abort", () => {
@@ -1,8 +1,11 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
+ import { harnessCommand } from "../harness.js";
3
4
  /** A harness binary's own version, or `null` when it cannot say. */
4
5
  export async function harnessVersion(binary) {
5
- const { stdout } = await promisify(execFile)(binary, ["--version"], { encoding: "utf8", timeout: 15_000 }).catch(() => ({ stdout: "" }));
6
+ const { stdout } = await Promise.resolve()
7
+ .then(() => promisify(execFile)(...harnessCommand(binary, ["--version"], process.cwd()), { encoding: "utf8", timeout: 15_000, windowsHide: true }))
8
+ .catch(() => ({ stdout: "" }));
6
9
  const match = /(\d+)\.(\d+)\.(\d+)/.exec(stdout);
7
10
  return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
8
11
  }
@@ -1,7 +1,9 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { randomUUID } from "node:crypto";
3
+ import { createServer } from "node:net";
3
4
  import { createInterface } from "node:readline";
4
5
  import { setTimeout as delay } from "node:timers/promises";
6
+ import { PassThrough } from "node:stream";
5
7
  import { resolveWebsocketUrl } from "@neta-art/cohub";
6
8
  import { AuthRequiredError, resolveAccessToken } from "../auth.js";
7
9
  import { createClient } from "../client.js";
@@ -22,6 +24,24 @@ export function sandboxOutputLevel(value, stream) {
22
24
  return level;
23
25
  return stream === "stderr" ? "warn" : "debug";
24
26
  }
27
+ /**
28
+ * Windows cannot hand sandboxd an extra inherited pipe, so control frames use a
29
+ * named pipe instead. Only the first client is accepted; the name is unguessable.
30
+ */
31
+ async function listenWindowsControlPipe() {
32
+ const path = `\\\\.\\pipe\\cohub-sandboxd-${randomUUID()}`;
33
+ const stream = new PassThrough();
34
+ const server = createServer((socket) => {
35
+ server.close();
36
+ socket.on("error", () => stream.end());
37
+ socket.pipe(stream);
38
+ });
39
+ await new Promise((resolve, reject) => {
40
+ server.once("error", reject);
41
+ server.listen(path, () => { server.removeListener("error", reject); resolve(); });
42
+ });
43
+ return { path, stream, close: () => { server.close(); stream.end(); } };
44
+ }
25
45
  export async function runRuntime(config, onState, externalSignal, onDiagnostic) {
26
46
  const controller = new AbortController();
27
47
  const signal = externalSignal ? AbortSignal.any([externalSignal, controller.signal]) : controller.signal;
@@ -119,12 +139,16 @@ export async function runRuntime(config, onState, externalSignal, onDiagnostic)
119
139
  let probeTimer;
120
140
  let managed = false;
121
141
  let childStopped = false;
142
+ let controlPipe = null;
122
143
  try {
123
144
  const initialToken = await token();
124
145
  signal.throwIfAborted();
146
+ if (process.platform === "win32")
147
+ controlPipe = await listenWindowsControlPipe();
125
148
  const bridge = spawn(binary, ["--local", "--space", config.spaceId, "--root", config.root, "--relay", process.env.COHUB_RELAY_URL?.trim() || relay.toString()], {
126
- stdio: ["pipe", "pipe", "pipe", "pipe"],
127
- env: { ...process.env, COHUB_RELAY_TOKEN: initialToken, COHUB_RUNTIME_ID: status.runtimeId, COHUB_LOG_FORMAT: "json", COHUB_RUNTIME_MANAGED: "1", COHUB_DISPLAY: config.display ?? "" },
149
+ stdio: controlPipe ? ["pipe", "pipe", "pipe"] : ["pipe", "pipe", "pipe", "pipe"],
150
+ windowsHide: true,
151
+ env: { ...process.env, COHUB_RELAY_TOKEN: initialToken, COHUB_RUNTIME_ID: status.runtimeId, COHUB_LOG_FORMAT: "json", COHUB_RUNTIME_MANAGED: "1", COHUB_DISPLAY: config.display ?? "", ...(controlPipe ? { COHUB_RUNTIME_CONTROL_PIPE: controlPipe.path } : {}) },
128
152
  });
129
153
  bridge.stdin?.on("error", () => undefined);
130
154
  const closed = new Promise((resolve) => {
@@ -153,7 +177,7 @@ export async function runRuntime(config, onState, externalSignal, onDiagnostic)
153
177
  diagnostics.log(sandboxOutputLevel(parsed?.level, streamName), "sandboxd.log", { ...data, message: parsed?.msg ?? line }, { component: "sandboxd" });
154
178
  });
155
179
  }
156
- const control = bridge.stdio[3];
180
+ const control = controlPipe ? controlPipe.stream : bridge.stdio[3];
157
181
  const events = createInterface({ input: control });
158
182
  events.on("line", (line) => {
159
183
  try {
@@ -202,7 +226,11 @@ export async function runRuntime(config, onState, externalSignal, onDiagnostic)
202
226
  };
203
227
  probeTimer = setTimeout(() => void probe(), 1000);
204
228
  const terminate = () => {
205
- bridge.kill("SIGTERM");
229
+ // Windows has no SIGTERM; closing the managed stdin lets sandboxd stop gracefully.
230
+ if (process.platform === "win32")
231
+ bridge.stdin?.end();
232
+ else
233
+ bridge.kill("SIGTERM");
206
234
  const timeout = setTimeout(() => bridge.kill("SIGKILL"), 3000);
207
235
  void closed.finally(() => clearTimeout(timeout));
208
236
  };
@@ -218,6 +246,7 @@ export async function runRuntime(config, onState, externalSignal, onDiagnostic)
218
246
  }
219
247
  finally {
220
248
  childStopped = true;
249
+ controlPipe?.close();
221
250
  clearTimeout(refreshTimer);
222
251
  clearTimeout(probeTimer);
223
252
  update({ workspaceConnected: false });
@@ -1,30 +1,13 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { UPLOAD_MAX_BATCH_BYTES, UPLOAD_MAX_BATCH_FILES, } from "@neta-art/cohub";
2
+ import { batchUploadEntries } from "@neta-art/cohub";
3
3
  import { putLocalFile } from "../http-put.js";
4
4
  import { createLimiter } from "./limit.js";
5
5
  const PUT_CONCURRENCY = 4;
6
- function batches(entries) {
7
- const result = [];
8
- let batch = [];
9
- let bytes = 0;
10
- for (const entry of entries) {
11
- if (batch.length > 0 && (batch.length >= UPLOAD_MAX_BATCH_FILES || bytes + entry.size > UPLOAD_MAX_BATCH_BYTES)) {
12
- result.push(batch);
13
- batch = [];
14
- bytes = 0;
15
- }
16
- batch.push(entry);
17
- bytes += entry.size;
18
- }
19
- if (batch.length > 0)
20
- result.push(batch);
21
- return result;
22
- }
23
6
  export async function uploadLocalFiles(files, entries, options = {}) {
24
7
  const uploaded = [];
25
8
  const uploadIds = [];
26
9
  const limit = createLimiter(PUT_CONCURRENCY);
27
- for (const batch of batches(entries)) {
10
+ for (const batch of batchUploadEntries(entries, (entry) => entry.size)) {
28
11
  const byId = new Map(batch.map((entry) => [randomUUID(), entry]));
29
12
  const plan = await files.createUpload({
30
13
  destination: { kind: "workspace", targetDir: options.targetDir },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neta-art/cohub-cli",
3
- "version": "10.3.0",
3
+ "version": "10.4.2",
4
4
  "description": "CLI for Cohub — spaces, sessions, and agent collaboration.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -18,8 +18,8 @@
18
18
  "NOTICE"
19
19
  ],
20
20
  "dependencies": {
21
- "@neta-art/cohub": "11.3.0",
22
- "@neta-art/generation": "^0.1.35",
21
+ "@neta-art/cohub": "11.3.2",
22
+ "@neta-art/generation": "^0.1.38",
23
23
  "commander": "^15.0.0",
24
24
  "sharp": "^0.35.4",
25
25
  "ws": "^8.21.3"