neko-harness 0.1.0 → 0.2.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/dist/neko.js CHANGED
@@ -221,8 +221,8 @@ function validate(config, source) {
221
221
  if (port !== undefined && (!Number.isInteger(port) || port < 0 || port > 65535)) {
222
222
  throw new Error(`neko config: ${source}: "gateway.port" must be an integer between 0 and 65535.`);
223
223
  }
224
- if (socketPath !== undefined && typeof socketPath !== "string") {
225
- throw new Error(`neko config: ${source}: "gateway.socketPath" must be a string.`);
224
+ if (socketPath !== undefined && socketPath !== null && typeof socketPath !== "string") {
225
+ throw new Error(`neko config: ${source}: "gateway.socketPath" must be a string or null.`);
226
226
  }
227
227
  }
228
228
  const theme = config.theme;
@@ -1897,7 +1897,10 @@ async function createNekoRuntime(options = {}) {
1897
1897
  conversationCreated: async (tx, record) => {
1898
1898
  (await tx.doc(SessionMetadataDoc, record.id)).createdAt = Date.now();
1899
1899
  },
1900
- env: ({ cwd }) => new NodeExecutionEnv({ cwd: executionWorkspace(cwd) })
1900
+ env: ({ cwd }) => new NodeExecutionEnv({
1901
+ cwd: executionWorkspace(cwd),
1902
+ ...process.platform === "win32" ? { shellEnv: { PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" } } : {}
1903
+ })
1901
1904
  }, context);
1902
1905
  const scanConversations = () => harness.commit(async (tx) => {
1903
1906
  const records = [];
@@ -3582,7 +3585,7 @@ function fileCompletion(commands, root, notice) {
3582
3585
  const items = [];
3583
3586
  for (const item of result.items) {
3584
3587
  const path = item.value.slice(1).replace(/^"|"$/g, "").replace(/\/$/, "");
3585
- if (path.split("/").includes(".git"))
3588
+ if (path.split(/[\\/]/).includes(".git"))
3586
3589
  continue;
3587
3590
  try {
3588
3591
  await workspaceTarget(root, path);
@@ -3855,14 +3858,43 @@ var init_client = __esm(() => {
3855
3858
  init_ws_listener();
3856
3859
  });
3857
3860
 
3861
+ // packages/gateway/src/port-file.ts
3862
+ import { readFile as readFile4, rm, writeFile } from "fs/promises";
3863
+ import { join as join7 } from "path";
3864
+ function gatewayPortFilePath(nekoDir, serverId) {
3865
+ return join7(nekoDir, `${serverId}.port`);
3866
+ }
3867
+ async function writeGatewayPortFile(nekoDir, serverId, wsUrl) {
3868
+ await writeFile(gatewayPortFilePath(nekoDir, serverId), `${wsUrl}
3869
+ `, { mode: 384 });
3870
+ }
3871
+ async function readGatewayPortFile(nekoDir, serverId) {
3872
+ let content;
3873
+ try {
3874
+ content = await readFile4(gatewayPortFilePath(nekoDir, serverId), "utf8");
3875
+ } catch {
3876
+ return;
3877
+ }
3878
+ const url = content.trim();
3879
+ return url.startsWith("ws://") || url.startsWith("wss://") ? url : undefined;
3880
+ }
3881
+ async function removeGatewayPortFile(nekoDir, serverId) {
3882
+ await rm(gatewayPortFilePath(nekoDir, serverId), { force: true });
3883
+ }
3884
+ var init_port_file = () => {};
3885
+
3858
3886
  // packages/gateway/src/workspaces.ts
3859
3887
  import { constants as constants3 } from "fs";
3860
3888
  import { access, readdir as readdir2, stat as stat2 } from "fs/promises";
3861
- import { dirname as dirname5, isAbsolute as isAbsolute3, join as join7 } from "path";
3889
+ import { dirname as dirname5, isAbsolute as isAbsolute3, join as join8 } from "path";
3862
3890
  async function browseWorkspaces(options) {
3863
- if (!isAbsolute3(options.path))
3891
+ const requested = options.path.replace(/^\/+([A-Za-z]:[\\/])/, "$1");
3892
+ if (process.platform === "win32" && (requested === "/" || requested === "\\")) {
3893
+ return { path: "/", parent: null, entries: await listDrives(), next: null };
3894
+ }
3895
+ if (!isAbsolute3(requested))
3864
3896
  throw new Error("\u8BF7\u9009\u62E9\u5B8C\u6574\u7684\u7EDD\u5BF9\u76EE\u5F55\u8DEF\u5F84");
3865
- const path = accessibleWorkspace(options.path);
3897
+ const path = accessibleWorkspace(requested);
3866
3898
  const query = options.query?.toLowerCase() ?? "";
3867
3899
  const key = JSON.stringify([path, query, options.showHidden === true]);
3868
3900
  let offset = 0;
@@ -3881,7 +3913,7 @@ async function browseWorkspaces(options) {
3881
3913
  let next = offset;
3882
3914
  for (;next < entries.length && directories.length < 100; next++) {
3883
3915
  const entry = entries[next];
3884
- const target = join7(path, entry.name);
3916
+ const target = join8(path, entry.name);
3885
3917
  if (!entry.isDirectory() && !entry.isSymbolicLink())
3886
3918
  continue;
3887
3919
  try {
@@ -3900,6 +3932,17 @@ async function browseWorkspaces(options) {
3900
3932
  next: next < entries.length ? JSON.stringify({ key, offset: next }) : null
3901
3933
  };
3902
3934
  }
3935
+ async function listDrives() {
3936
+ const drives = [];
3937
+ for (const letter of "ABCDEFGHIJKLMNOPQRSTUVWXYZ") {
3938
+ const root = `${letter}:\\`;
3939
+ try {
3940
+ await access(root);
3941
+ drives.push({ name: `${letter}:`, path: root });
3942
+ } catch {}
3943
+ }
3944
+ return drives;
3945
+ }
3903
3946
  var init_workspaces = __esm(() => {
3904
3947
  init_src();
3905
3948
  });
@@ -4440,8 +4483,8 @@ var init_session_host = __esm(() => {
4440
4483
 
4441
4484
  // packages/gateway/src/gateway.ts
4442
4485
  import { randomUUID } from "crypto";
4443
- import { mkdir as mkdir2, readFile as readFile4, writeFile } from "fs/promises";
4444
- import { basename as basename5, dirname as dirname6, join as join8 } from "path";
4486
+ import { mkdir as mkdir2, readFile as readFile5, writeFile as writeFile2 } from "fs/promises";
4487
+ import { basename as basename5, dirname as dirname6, join as join9 } from "path";
4445
4488
  import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT3 } from "@earendil-works/chord/context";
4446
4489
  import { isServerId } from "@earendil-works/pi-protocol";
4447
4490
  import { Server, SessionNotFoundError } from "@earendil-works/pi-server";
@@ -4452,10 +4495,10 @@ async function startGateway(options = {}) {
4452
4495
  const model = options.model ?? fileConfig.defaultModel ?? DEFAULT_MODEL;
4453
4496
  const storagePath = options.storagePath ?? fileConfig.storagePath ?? DEFAULT_STORAGE_PATH;
4454
4497
  const host = options.host ?? fileConfig.gateway?.host ?? DEFAULT_HOST;
4455
- const port = options.port !== undefined ? options.port : fileConfig.gateway?.port ?? DEFAULT_PORT;
4498
+ const port = options.port !== undefined ? options.port : fileConfig.gateway?.port ?? (process.platform === "win32" ? 0 : DEFAULT_PORT);
4456
4499
  const nekoDir = dirname6(storagePath);
4457
4500
  const serverId = options.serverId ?? await loadOrCreateServerId(nekoDir);
4458
- const socketPath = options.socketPath !== undefined ? options.socketPath : fileConfig.gateway?.socketPath ?? join8(nekoDir, `${serverId}.sock`);
4501
+ const socketPath = options.socketPath !== undefined ? options.socketPath : fileConfig.gateway?.socketPath !== undefined ? fileConfig.gateway.socketPath : process.platform === "win32" ? null : join9(nekoDir, `${serverId}.sock`);
4459
4502
  const runtime = await createNekoRuntime({
4460
4503
  context,
4461
4504
  storage: options.storage ?? "sqlite",
@@ -4476,6 +4519,7 @@ async function startGateway(options = {}) {
4476
4519
  port,
4477
4520
  socketPath,
4478
4521
  serverId,
4522
+ nekoDir,
4479
4523
  model,
4480
4524
  onError: options.onError
4481
4525
  });
@@ -4485,7 +4529,7 @@ async function startGateway(options = {}) {
4485
4529
  }
4486
4530
  }
4487
4531
  async function bindGateway(runtime, options) {
4488
- const { context, host, port, socketPath, serverId, model } = options;
4532
+ const { context, host, port, socketPath, serverId, nekoDir, model } = options;
4489
4533
  const modelLabel = `${model.provider}/${model.modelId}`;
4490
4534
  const serverServices = await createNekoServerServices({ runtime, context });
4491
4535
  let wsListener;
@@ -4518,6 +4562,10 @@ async function bindGateway(runtime, options) {
4518
4562
  await serverServices.dispose().catch(() => {});
4519
4563
  throw toStartupError(error, { socketPath, port, host, serverId });
4520
4564
  }
4565
+ const portFile = process.platform === "win32" && wsListener !== undefined ? { nekoDir, serverId } : undefined;
4566
+ if (portFile !== undefined && wsListener !== undefined) {
4567
+ await writeGatewayPortFile(portFile.nekoDir, portFile.serverId, `ws://${host}:${wsListener.port}`);
4568
+ }
4521
4569
  let closed = false;
4522
4570
  return {
4523
4571
  serverId,
@@ -4529,6 +4577,8 @@ async function bindGateway(runtime, options) {
4529
4577
  if (closed)
4530
4578
  return;
4531
4579
  closed = true;
4580
+ if (portFile !== undefined)
4581
+ await removeGatewayPortFile(portFile.nekoDir, portFile.serverId).catch(() => {});
4532
4582
  await server.close();
4533
4583
  await serverServices.dispose();
4534
4584
  await runtime.close();
@@ -4538,7 +4588,8 @@ async function bindGateway(runtime, options) {
4538
4588
  function toStartupError(error, info) {
4539
4589
  const message = error instanceof Error ? error.message : String(error);
4540
4590
  const existingServerId = info.socketPath === null ? undefined : serverIdFromSocketPath(info.socketPath);
4541
- const connectHint = existingServerId !== undefined ? `\u5982\u9700\u8FDE\u63A5\u5DF2\u6709\u5B9E\u4F8B\uFF1AUnix socket ${info.socketPath}\uFF08serverId: ${existingServerId}\uFF09\uFF0C\u6216\u505C\u6389\u5360\u7528\u65B9\u540E\u91CD\u8BD5\u3002` : `\u5982\u9700\u8FDE\u63A5\u5DF2\u6709\u5B9E\u4F8B\uFF1AUnix socket ${info.socketPath ?? "\uFF08\u672A\u542F\u7528\uFF09"}\uFF0C\u6216\u505C\u6389\u5360\u7528\u65B9\u540E\u91CD\u8BD5\u3002`;
4591
+ const existingAddress = info.socketPath !== null ? `Unix socket ${info.socketPath}${existingServerId !== undefined ? `\uFF08serverId: ${existingServerId}\uFF09` : ""}` : process.platform === "win32" ? `\u7AEF\u53E3\u6587\u4EF6 <\u6570\u636E\u76EE\u5F55>/${info.serverId}.port \u4E2D\u7684 ws \u5730\u5740` : "Unix socket\uFF08\u672A\u542F\u7528\uFF09";
4592
+ const connectHint = `\u5982\u9700\u8FDE\u63A5\u5DF2\u6709\u5B9E\u4F8B\uFF1A${existingAddress}\uFF0C\u6216\u505C\u6389\u5360\u7528\u65B9\u540E\u91CD\u8BD5\u3002`;
4542
4593
  if (message.includes("already running")) {
4543
4594
  return new Error(`neko gateway \u542F\u52A8\u5931\u8D25\uFF1A${message}
4544
4595
  \u8BE5\u5730\u5740\u5DF2\u88AB\u4E00\u4E2A\u6D3B\u8DC3\u7F51\u5173\u5360\u7528\uFF0C\u4E0D\u80FD\u542F\u52A8\u7B2C\u4E8C\u4E2A\u5B9E\u4F8B\u3002
@@ -4558,21 +4609,22 @@ function serverIdFromSocketPath(socketPath) {
4558
4609
  return isServerId(candidate) ? candidate : undefined;
4559
4610
  }
4560
4611
  async function loadOrCreateServerId(nekoDir) {
4561
- const file = join8(nekoDir, SERVER_ID_FILENAME);
4612
+ const file = join9(nekoDir, SERVER_ID_FILENAME);
4562
4613
  try {
4563
- const existing = (await readFile4(file, "utf8")).trim();
4614
+ const existing = (await readFile5(file, "utf8")).trim();
4564
4615
  if (existing !== "")
4565
4616
  return existing;
4566
4617
  } catch {}
4567
4618
  const created = randomUUID();
4568
4619
  await mkdir2(nekoDir, { recursive: true, mode: 448 });
4569
- await writeFile(file, `${created}
4620
+ await writeFile2(file, `${created}
4570
4621
  `, { mode: 384 });
4571
4622
  return created;
4572
4623
  }
4573
4624
  var DEFAULT_HOST = "127.0.0.1", DEFAULT_PORT = 8321, SERVER_ID_FILENAME = "gateway-server-id";
4574
4625
  var init_gateway = __esm(() => {
4575
4626
  init_src();
4627
+ init_port_file();
4576
4628
  init_server_services();
4577
4629
  init_session_host();
4578
4630
  init_ws_listener();
@@ -4610,6 +4662,7 @@ var init_src2 = __esm(() => {
4610
4662
  init_client();
4611
4663
  init_contract();
4612
4664
  init_gateway();
4665
+ init_port_file();
4613
4666
  init_process();
4614
4667
  init_ws_listener();
4615
4668
  });
@@ -4617,15 +4670,27 @@ var init_src2 = __esm(() => {
4617
4670
  // packages/tui/src/gateway.ts
4618
4671
  import { spawn as spawn2 } from "child_process";
4619
4672
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
4620
- import { basename as basename6, dirname as dirname7, join as join9 } from "path";
4673
+ import { basename as basename6, dirname as dirname7, join as join10 } from "path";
4621
4674
  import { isServerId as isServerId2 } from "@earendil-works/pi-protocol";
4622
4675
  async function connectToGateway(options = {}) {
4623
4676
  const config = loadNekoConfig(options.configDir);
4624
4677
  const storagePath = options.storagePath ?? config.storagePath ?? DEFAULT_STORAGE_PATH;
4625
4678
  const nekoDir = dirname7(storagePath);
4626
- const fileServerId = readServerId(join9(nekoDir, SERVER_ID_FILENAME2));
4627
- const socketPath = options.socketPath ?? config.gateway?.socketPath ?? (fileServerId !== undefined ? join9(nekoDir, `${fileServerId}.sock`) : undefined);
4628
- const serverIdFor = (path) => fileServerId ?? serverIdFromSocketPath2(path) ?? readServerId(join9(nekoDir, SERVER_ID_FILENAME2));
4679
+ const fileServerId = readServerId(join10(nekoDir, SERVER_ID_FILENAME2));
4680
+ if (process.platform === "win32") {
4681
+ if (fileServerId !== undefined) {
4682
+ const wsUrl = await readGatewayPortFile(nekoDir, fileServerId);
4683
+ if (wsUrl !== undefined) {
4684
+ try {
4685
+ const client = await connectNekoGateway({ transport: "ws", url: wsUrl, serverId: fileServerId });
4686
+ return { client, socketPath: undefined, serverId: fileServerId, spawned: false, close: () => client.close() };
4687
+ } catch {}
4688
+ }
4689
+ }
4690
+ return spawnAndConnect({ ...options, storagePath, socketPath: undefined, nekoDir, fileServerId });
4691
+ }
4692
+ const socketPath = options.socketPath ?? config.gateway?.socketPath ?? (fileServerId !== undefined ? join10(nekoDir, `${fileServerId}.sock`) : undefined);
4693
+ const serverIdFor = (path) => fileServerId ?? serverIdFromSocketPath2(path) ?? readServerId(join10(nekoDir, SERVER_ID_FILENAME2));
4629
4694
  if (socketPath !== undefined && existsSync2(socketPath)) {
4630
4695
  const serverId = serverIdFor(socketPath);
4631
4696
  if (serverId !== undefined) {
@@ -4653,13 +4718,42 @@ async function spawnAndConnect(plan) {
4653
4718
  }
4654
4719
  }
4655
4720
  async function connectSpawned(plan, child, outputTail, spawnError) {
4721
+ const timeoutMs = plan.spawnTimeoutMs ?? 20000;
4722
+ if (process.platform === "win32") {
4723
+ const serverId = plan.fileServerId ?? await waitForValue(() => readServerId(join10(plan.nekoDir, SERVER_ID_FILENAME2)), "gateway serverId file", timeoutMs, child, outputTail, spawnError);
4724
+ const wsUrl = await waitForValue(() => readGatewayPortFile(plan.nekoDir, serverId), "gateway ready (port file)", timeoutMs, child, outputTail, spawnError);
4725
+ let lastError;
4726
+ for (let attempt = 0;attempt < CONNECT_RETRIES; attempt++) {
4727
+ if (child.exitCode !== null || spawnError() !== undefined)
4728
+ break;
4729
+ try {
4730
+ const client = await connectNekoGateway({ transport: "ws", url: wsUrl, serverId });
4731
+ return {
4732
+ client,
4733
+ socketPath: undefined,
4734
+ serverId,
4735
+ spawned: true,
4736
+ async close() {
4737
+ await client.close().catch(() => {});
4738
+ await stopGatewayProcess(child);
4739
+ await removeGatewayPortFile(plan.nekoDir, serverId).catch(() => {});
4740
+ }
4741
+ };
4742
+ } catch (error) {
4743
+ lastError = error;
4744
+ await sleep(CONNECT_RETRY_MS);
4745
+ }
4746
+ }
4747
+ throw new Error(`Gateway connection failed: ${lastError instanceof Error ? lastError.message : String(lastError)}
4748
+ ${outputTail()}`);
4749
+ }
4656
4750
  let socketPath = plan.socketPath;
4657
4751
  if (socketPath === undefined) {
4658
- const generated = await waitForValue(() => readServerId(join9(plan.nekoDir, SERVER_ID_FILENAME2)), "gateway serverId file", plan.spawnTimeoutMs ?? 20000, child, outputTail, spawnError);
4659
- socketPath = join9(plan.nekoDir, `${generated}.sock`);
4752
+ const generated = await waitForValue(() => readServerId(join10(plan.nekoDir, SERVER_ID_FILENAME2)), "gateway serverId file", timeoutMs, child, outputTail, spawnError);
4753
+ socketPath = join10(plan.nekoDir, `${generated}.sock`);
4660
4754
  }
4661
- await waitForValue(() => existsSync2(socketPath) ? true : undefined, `gateway ready (socket: ${socketPath})`, plan.spawnTimeoutMs ?? 20000, child, outputTail, spawnError);
4662
- const serverId = plan.fileServerId ?? serverIdFromSocketPath2(socketPath) ?? readServerId(join9(plan.nekoDir, SERVER_ID_FILENAME2));
4755
+ await waitForValue(() => existsSync2(socketPath) ? true : undefined, `gateway ready (socket: ${socketPath})`, timeoutMs, child, outputTail, spawnError);
4756
+ const serverId = plan.fileServerId ?? serverIdFromSocketPath2(socketPath) ?? readServerId(join10(plan.nekoDir, SERVER_ID_FILENAME2));
4663
4757
  if (serverId === undefined) {
4664
4758
  throw new Error(`Cannot determine gateway serverId (socket: ${socketPath})`);
4665
4759
  }
@@ -4688,6 +4782,16 @@ async function connectSpawned(plan, child, outputTail, spawnError) {
4688
4782
  ${outputTail()}`);
4689
4783
  }
4690
4784
  function gatewayArgs(plan) {
4785
+ if (process.platform === "win32") {
4786
+ return [
4787
+ "--no-socket",
4788
+ "--storage",
4789
+ plan.storagePath,
4790
+ ...plan.credentialsPath ? ["--credentials", plan.credentialsPath] : [],
4791
+ ...plan.configDir ? ["--cwd", plan.configDir, "--config", plan.configDir] : [],
4792
+ ...plan.model !== undefined ? ["--model", `${plan.model.provider}/${plan.model.modelId}`] : []
4793
+ ];
4794
+ }
4691
4795
  return [
4692
4796
  "--port",
4693
4797
  "0",
@@ -4732,7 +4836,7 @@ async function waitForValue(poll, what, timeoutMs, child, outputTail, spawnError
4732
4836
  const error = spawnError();
4733
4837
  if (error !== undefined)
4734
4838
  throw new Error(`Gateway process failed to start: ${error.message}${formatTail(outputTail())}`);
4735
- const value = poll();
4839
+ const value = await poll();
4736
4840
  if (value !== undefined)
4737
4841
  return value;
4738
4842
  if (child.exitCode !== null) {
@@ -4761,19 +4865,19 @@ var init_gateway2 = __esm(() => {
4761
4865
 
4762
4866
  // packages/tui/src/image-input.ts
4763
4867
  import { execFile } from "child_process";
4764
- import { readFile as readFile5, stat as stat3 } from "fs/promises";
4868
+ import { readFile as readFile6, stat as stat3 } from "fs/promises";
4765
4869
  import { homedir as homedir5 } from "os";
4766
4870
  import { basename as basename7, resolve as resolve5 } from "path";
4767
4871
  import { promisify } from "util";
4768
4872
  import { isKeyRelease as isKeyRelease4, isKeyRepeat as isKeyRepeat2, matchesKey as matchesKey4 } from "@earendil-works/pi-tui";
4769
- function imagePaths(text) {
4873
+ function imagePaths(text, platform = process.platform) {
4770
4874
  const paths = [];
4771
4875
  let word = "", quote = "", escaped = false;
4772
4876
  for (const c of text.trim()) {
4773
4877
  if (escaped) {
4774
4878
  word += c;
4775
4879
  escaped = false;
4776
- } else if (c === "\\" && quote !== "'") {
4880
+ } else if (c === "\\" && quote !== "'" && platform !== "win32") {
4777
4881
  escaped = true;
4778
4882
  } else if (quote) {
4779
4883
  if (c === quote)
@@ -4801,7 +4905,7 @@ async function readImageFile(path, cwd) {
4801
4905
  const info = await stat3(full);
4802
4906
  if (!info.isFile() || info.size > 20 * 1024 * 1024)
4803
4907
  throw new Error("Image must be a file no larger than 20 MiB");
4804
- const bytes = await readFile5(full);
4908
+ const bytes = await readFile6(full);
4805
4909
  const after = await stat3(full);
4806
4910
  if (bytes.length !== info.size || after.size !== info.size || after.mtimeMs !== info.mtimeMs)
4807
4911
  throw new Error("Image changed while reading");
@@ -5206,7 +5310,7 @@ class NekoTuiApp {
5206
5310
  this.sendImages(text, "followUp");
5207
5311
  }
5208
5312
  async addImagePaths(paths) {
5209
- return paths.map((path) => this.addImage(readImageFile(path, this.#options.cwd ?? process.cwd()), path.split("/").at(-1) ?? "Image")).join(" ");
5313
+ return paths.map((path) => this.addImage(readImageFile(path, this.#options.cwd ?? process.cwd()), path.split(/[\\/]/).at(-1) ?? "Image")).join(" ");
5210
5314
  }
5211
5315
  addImage(file, name) {
5212
5316
  const ref = `[Image ${++this.#imageCounter}: ${name.replace(/[\]\r\n]/g, "")}]`;
@@ -5836,7 +5940,7 @@ Options:
5836
5940
  --cwd <dir> Workspace (default: launch directory; not an access sandbox)
5837
5941
  --model <ref> provider/modelId for a spawned gateway
5838
5942
  --storage <path> SQLite storage (default: ~/.neko/neko.sqlite)
5839
- --socket-path <path> Gateway Unix socket
5943
+ --socket-path <path> Gateway Unix socket\uFF08POSIX\uFF1BWindows \u7ECF\u7AEF\u53E3\u6587\u4EF6\u53D1\u73B0 ws \u5730\u5740\uFF09
5840
5944
  --theme <mode> light / dark / auto (overrides NEKO_THEME)
5841
5945
  -h, --help Show help
5842
5946
 
@@ -5873,16 +5977,53 @@ var init_open_browser = () => {};
5873
5977
  // packages/web/src/gateway-spawn.ts
5874
5978
  import { spawn as spawn3 } from "child_process";
5875
5979
  import { randomUUID as randomUUID2 } from "crypto";
5876
- import { mkdir as mkdir3, readFile as readFile6, writeFile as writeFile2 } from "fs/promises";
5877
- import { dirname as dirname8, join as join10 } from "path";
5980
+ import { mkdir as mkdir3, readFile as readFile7, writeFile as writeFile3 } from "fs/promises";
5981
+ import { dirname as dirname8, join as join11 } from "path";
5878
5982
  async function ensureNekoGateway(options) {
5879
5983
  const nekoDir = dirname8(options.storagePath);
5880
5984
  const serverId = options.serverId ?? await loadOrCreateServerId2(nekoDir);
5881
- const socketPath = options.socketPath ?? join10(nekoDir, `${serverId}.sock`);
5985
+ if (process.platform === "win32") {
5986
+ const discovered = await readGatewayPortFile(nekoDir, serverId);
5987
+ if (discovered !== undefined && await probeWs(discovered, serverId)) {
5988
+ return makeHandle(serverId, undefined, discovered, null, nekoDir);
5989
+ }
5990
+ const child = spawnGatewayProcess(options, null, serverId);
5991
+ pipeChildLogs(child, options.onLog);
5992
+ let spawnError;
5993
+ child.on("error", (error) => {
5994
+ spawnError = error;
5995
+ });
5996
+ try {
5997
+ const timeoutAt = Date.now() + (options.spawnTimeoutMs ?? 30000);
5998
+ let lastError = new Error("gateway \u672A\u5C31\u7EEA");
5999
+ while (Date.now() < timeoutAt) {
6000
+ if (spawnError !== undefined)
6001
+ throw spawnError;
6002
+ if (child.exitCode !== null || child.signalCode !== null) {
6003
+ throw new Error(`neko gateway \u5B50\u8FDB\u7A0B\u63D0\u524D\u9000\u51FA\uFF08code ${child.exitCode}\uFF09\uFF0C\u8BE6\u89C1\u4E0A\u65B9\u65E5\u5FD7\u3002\u7AEF\u53E3\u6587\u4EF6\u53EF\u80FD\u88AB\u5176\u4ED6\u5B9E\u4F8B\u5360\u7528\u3002`);
6004
+ }
6005
+ const wsUrl = await readGatewayPortFile(nekoDir, serverId);
6006
+ if (wsUrl !== undefined) {
6007
+ try {
6008
+ if (await probeWs(wsUrl, serverId))
6009
+ return makeHandle(serverId, undefined, wsUrl, child, nekoDir);
6010
+ } catch (error) {
6011
+ lastError = error;
6012
+ }
6013
+ }
6014
+ await sleep2(PROBE_INTERVAL_MS);
6015
+ }
6016
+ throw new Error(`\u7B49\u5F85 neko gateway \u5C31\u7EEA\u8D85\u65F6\uFF08${options.spawnTimeoutMs ?? 30000}ms\uFF09\uFF1A${lastError instanceof Error ? lastError.message : String(lastError)}`);
6017
+ } catch (error) {
6018
+ await stopGatewayProcess(child);
6019
+ throw error;
6020
+ }
6021
+ }
6022
+ const socketPath = options.socketPath ?? join11(nekoDir, `${serverId}.sock`);
5882
6023
  const host = options.host ?? "127.0.0.1";
5883
6024
  const port = options.port ?? 8321;
5884
6025
  if (await probeGateway(socketPath, serverId)) {
5885
- return makeHandle(serverId, socketPath, host, port, null);
6026
+ return makeHandle(serverId, socketPath, `ws://${host}:${port}`, null, nekoDir);
5886
6027
  }
5887
6028
  const child = spawnGatewayProcess(options, socketPath, serverId);
5888
6029
  pipeChildLogs(child, options.onLog);
@@ -5901,7 +6042,7 @@ async function ensureNekoGateway(options) {
5901
6042
  }
5902
6043
  try {
5903
6044
  if (await probeGateway(socketPath, serverId))
5904
- return makeHandle(serverId, socketPath, host, port, child);
6045
+ return makeHandle(serverId, socketPath, `ws://${host}:${port}`, child, nekoDir);
5905
6046
  } catch (error) {
5906
6047
  lastError = error;
5907
6048
  }
@@ -5917,10 +6058,7 @@ function spawnGatewayProcess(options, socketPath, serverId) {
5917
6058
  const args = [
5918
6059
  "--host",
5919
6060
  options.host ?? "127.0.0.1",
5920
- "--port",
5921
- String(options.port ?? 8321),
5922
- "--socket-path",
5923
- socketPath,
6061
+ ...process.platform === "win32" ? ["--no-socket"] : ["--port", String(options.port ?? 8321), "--socket-path", socketPath ?? ""],
5924
6062
  "--server-id",
5925
6063
  serverId,
5926
6064
  "--storage",
@@ -5959,28 +6097,40 @@ async function probeGateway(socketPath, serverId) {
5959
6097
  return false;
5960
6098
  }
5961
6099
  }
5962
- function makeHandle(serverId, socketPath, host, port, spawned) {
6100
+ async function probeWs(wsUrl, serverId) {
6101
+ try {
6102
+ const probe = await connectNekoGateway({ transport: "ws", url: wsUrl, serverId });
6103
+ await probe.close();
6104
+ return true;
6105
+ } catch {
6106
+ return false;
6107
+ }
6108
+ }
6109
+ function makeHandle(serverId, socketPath, wsUrl, spawned, nekoDir) {
5963
6110
  return {
5964
6111
  serverId,
5965
6112
  socketPath,
5966
- wsUrl: `ws://${host}:${port}`,
6113
+ wsUrl,
5967
6114
  spawned,
5968
6115
  async close() {
5969
- if (spawned !== null)
6116
+ if (spawned !== null) {
5970
6117
  await stopGatewayProcess(spawned);
6118
+ if (process.platform === "win32")
6119
+ await removeGatewayPortFile(nekoDir, serverId).catch(() => {});
6120
+ }
5971
6121
  }
5972
6122
  };
5973
6123
  }
5974
6124
  async function loadOrCreateServerId2(nekoDir) {
5975
- const file = join10(nekoDir, SERVER_ID_FILENAME3);
6125
+ const file = join11(nekoDir, SERVER_ID_FILENAME3);
5976
6126
  try {
5977
- const existing = (await readFile6(file, "utf8")).trim();
6127
+ const existing = (await readFile7(file, "utf8")).trim();
5978
6128
  if (existing !== "")
5979
6129
  return existing;
5980
6130
  } catch {}
5981
6131
  const created = randomUUID2();
5982
6132
  await mkdir3(nekoDir, { recursive: true, mode: 448 });
5983
- await writeFile2(file, `${created}
6133
+ await writeFile3(file, `${created}
5984
6134
  `, { mode: 384 });
5985
6135
  return created;
5986
6136
  }
@@ -6215,7 +6365,7 @@ var USAGE2 = `\u7528\u6CD5: main.ts [\u9009\u9879]
6215
6365
  --port <n> \u9759\u6001 HTTP \u7AEF\u53E3\uFF08\u9ED8\u8BA4 8322\uFF09
6216
6366
  --host <ip> \u9759\u6001 HTTP \u5730\u5740\uFF08\u9ED8\u8BA4 127.0.0.1\uFF09
6217
6367
  --gateway-port <n> \u7F51\u5173 WebSocket \u7AEF\u53E3\uFF08\u9ED8\u8BA4 8321\uFF09
6218
- --socket-path <path> \u7F51\u5173 Unix socket \u8DEF\u5F84\uFF08\u9ED8\u8BA4 <\u6570\u636E\u76EE\u5F55>/<serverId>.sock\uFF09
6368
+ --socket-path <path> \u7F51\u5173 Unix socket \u8DEF\u5F84\uFF08\u9ED8\u8BA4 <\u6570\u636E\u76EE\u5F55>/<serverId>.sock\uFF1B\u4EC5 POSIX\uFF09
6219
6369
  --server-id <id> \u7F51\u5173\u903B\u8F91\u8EAB\u4EFD\uFF08\u9ED8\u8BA4\u8BFB\u53D6/\u751F\u6210 <\u6570\u636E\u76EE\u5F55>/gateway-server-id\uFF09
6220
6370
  --storage <path> SQLite \u5B58\u50A8\u6587\u4EF6\uFF08\u9ED8\u8BA4 ~/.neko/neko.sqlite\uFF09
6221
6371
  --cwd <dir> \u4F1A\u8BDD\u5DE5\u5177\u7684\u5DE5\u4F5C\u76EE\u5F55\uFF08\u9ED8\u8BA4\u5F53\u524D\u76EE\u5F55\uFF09
@@ -6269,8 +6419,8 @@ var init_main2 = __esm(async () => {
6269
6419
  }
6270
6420
  });
6271
6421
 
6272
- // packages/gateway/src/main.ts
6273
- function parseArgs3(argv) {
6422
+ // packages/gateway/src/cli-args.ts
6423
+ function parseGatewayArgs(argv) {
6274
6424
  let port;
6275
6425
  const options = {};
6276
6426
  for (let i = 0;i < argv.length; i++) {
@@ -6304,6 +6454,9 @@ function parseArgs3(argv) {
6304
6454
  options.socketPath = next;
6305
6455
  i++;
6306
6456
  break;
6457
+ case "--no-socket":
6458
+ options.socketPath = null;
6459
+ break;
6307
6460
  case "--server-id":
6308
6461
  if (next === undefined)
6309
6462
  throw new Error("--server-id \u9700\u8981\u53C2\u6570");
@@ -6344,11 +6497,6 @@ function parseArgs3(argv) {
6344
6497
  options.configDir = next;
6345
6498
  i++;
6346
6499
  break;
6347
- case "-h":
6348
- case "--help":
6349
- console.log(USAGE3);
6350
- process.exit(0);
6351
- break;
6352
6500
  default:
6353
6501
  throw new Error(`\u672A\u77E5\u9009\u9879: ${arg}`);
6354
6502
  }
@@ -6357,12 +6505,13 @@ function parseArgs3(argv) {
6357
6505
  options.port = port;
6358
6506
  return options;
6359
6507
  }
6360
- var USAGE3 = `\u7528\u6CD5: main.ts [\u9009\u9879]
6508
+ var GATEWAY_USAGE = `\u7528\u6CD5: main.ts [\u9009\u9879]
6361
6509
 
6362
6510
  \u9009\u9879:
6363
- --host <ip> WebSocket \u76D1\u542C\u5730\u5740\uFF08\u9ED8\u8BA4 127.0.0.1\uFF0C\u4EC5\u672C\u673A\uFF1B0/null \u7981\u7528 WS\uFF09
6364
- --port <n> WebSocket \u76D1\u542C\u7AEF\u53E3\uFF08\u9ED8\u8BA4 8321\uFF1B0 = \u968F\u673A\u5206\u914D\uFF09
6365
- --socket-path <path> Unix socket \u8DEF\u5F84\uFF08\u9ED8\u8BA4 <\u6570\u636E\u76EE\u5F55>/<serverId>.sock\uFF09
6511
+ --host <ip> WebSocket \u76D1\u542C\u5730\u5740\uFF08\u9ED8\u8BA4 127.0.0.1\uFF0C\u4EC5\u672C\u673A\uFF09
6512
+ --port <n> WebSocket \u76D1\u542C\u7AEF\u53E3\uFF08\u9ED8\u8BA4 8321\uFF0CWindows \u9ED8\u8BA4\u968F\u673A\uFF1B0/null \u7981\u7528 WS\uFF09
6513
+ --socket-path <path> Unix socket \u8DEF\u5F84\uFF08\u9ED8\u8BA4 <\u6570\u636E\u76EE\u5F55>/<serverId>.sock\uFF1BWindows \u9ED8\u8BA4\u7981\u7528\uFF09
6514
+ --no-socket \u7981\u7528 Unix socket \u4F20\u8F93\uFF08\u4EC5 WebSocket\uFF09
6366
6515
  --server-id <id> \u903B\u8F91\u670D\u52A1\u5668\u8EAB\u4EFD\uFF08\u9ED8\u8BA4\u8BFB\u53D6/\u751F\u6210 <\u6570\u636E\u76EE\u5F55>/gateway-server-id\uFF09
6367
6516
  --cwd <dir> \u4F1A\u8BDD\u5DE5\u5177\u7684\u5DE5\u4F5C\u76EE\u5F55\uFF08\u9ED8\u8BA4\u5F53\u524D\u76EE\u5F55\uFF09
6368
6517
  --model <ref> \u6A21\u578B\uFF0C\u683C\u5F0F provider/modelId\uFF08\u9ED8\u8BA4 deepseek/deepseek-flash\uFF09
@@ -6371,7 +6520,10 @@ var USAGE3 = `\u7528\u6CD5: main.ts [\u9009\u9879]
6371
6520
  --config <dir> neko.config.json \u67E5\u627E\u8D77\u59CB\u76EE\u5F55\uFF08\u9ED8\u8BA4\u5F53\u524D\u76EE\u5F55\uFF09
6372
6521
  -h, --help \u663E\u793A\u6B64\u5E2E\u52A9
6373
6522
 
6374
- \u53EF\u901A\u8FC7\u524D\u7AEF\u8BBE\u7F6E\u6216 DEEPSEEK_API_KEY \u914D\u7F6E\u51ED\u636E\u3002`, options, gateway, stopping = false, stop = () => {
6523
+ \u53EF\u901A\u8FC7\u524D\u7AEF\u8BBE\u7F6E\u6216 DEEPSEEK_API_KEY \u914D\u7F6E\u51ED\u636E\u3002`;
6524
+
6525
+ // packages/gateway/src/main.ts
6526
+ var argv, options, gateway, stopping = false, stop = () => {
6375
6527
  if (stopping)
6376
6528
  return;
6377
6529
  stopping = true;
@@ -6382,7 +6534,12 @@ var USAGE3 = `\u7528\u6CD5: main.ts [\u9009\u9879]
6382
6534
  };
6383
6535
  var init_main3 = __esm(async () => {
6384
6536
  init_gateway();
6385
- options = parseArgs3(process.argv.slice(2));
6537
+ argv = process.argv.slice(2);
6538
+ if (argv.includes("-h") || argv.includes("--help")) {
6539
+ console.log(GATEWAY_USAGE);
6540
+ process.exit(0);
6541
+ }
6542
+ options = parseGatewayArgs(argv);
6386
6543
  gateway = await startGateway({
6387
6544
  ...options,
6388
6545
  onError: (error) => console.error("gateway error:", error.message)
@@ -6398,12 +6555,12 @@ var init_main3 = __esm(async () => {
6398
6555
  });
6399
6556
 
6400
6557
  // packages/cli/src/main.ts
6401
- var USAGE4 = `\u7528\u6CD5: neko <command> [options]
6558
+ var USAGE3 = `\u7528\u6CD5: neko <command> [options]
6402
6559
 
6403
6560
  \u547D\u4EE4:
6404
6561
  tui \u542F\u52A8\u7EC8\u7AEF\u4EA4\u4E92\u754C\u9762\uFF08\u81EA\u52A8\u62C9\u8D77 gateway\uFF09
6405
6562
  web \u542F\u52A8 Web UI \u9759\u6001\u6258\u7BA1\uFF08\u81EA\u52A8\u62C9\u8D77 gateway\uFF09
6406
- serve \u4EC5\u542F\u52A8 gateway\uFF08Unix socket + WebSocket\uFF09
6563
+ serve \u4EC5\u542F\u52A8 gateway\uFF08WebSocket\uFF1BPOSIX \u53E6\u6709 Unix socket\uFF09
6407
6564
 
6408
6565
  \u5404\u547D\u4EE4\u7684\u9009\u9879\u900F\u4F20\uFF0C\u8BE6\u89C1 neko <command> --help\u3002
6409
6566
  \u53EF\u901A\u8FC7 TUI /login\u3001Web \u8BBE\u7F6E\u9875\u6216 DEEPSEEK_API_KEY / TAVILY_API_KEY \u914D\u7F6E\u51ED\u636E\u3002`;
@@ -6423,16 +6580,16 @@ switch (command2) {
6423
6580
  }
6424
6581
  case "--version":
6425
6582
  case "-v":
6426
- console.log("0.1.0");
6583
+ console.log("0.2.0");
6427
6584
  break;
6428
6585
  case undefined:
6429
6586
  case "-h":
6430
6587
  case "--help":
6431
- console.log(USAGE4);
6588
+ console.log(USAGE3);
6432
6589
  break;
6433
6590
  default:
6434
6591
  console.error(`\u672A\u77E5\u547D\u4EE4: ${command2}
6435
6592
 
6436
- ${USAGE4}`);
6593
+ ${USAGE3}`);
6437
6594
  process.exitCode = 1;
6438
6595
  }
package/package.json CHANGED
@@ -24,7 +24,7 @@
24
24
  },
25
25
  "packageManager": "bun@1.4.2",
26
26
  "trustedDependencies": [],
27
- "version": "0.1.0",
27
+ "version": "0.2.0",
28
28
  "description": "A local AI coding assistant with terminal and web interfaces, powered by Pi and Bun.",
29
29
  "license": "MIT",
30
30
  "repository": {