neko-harness 0.1.1 → 0.2.1

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.
@@ -9,8 +9,8 @@
9
9
  href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' rx='26' fill='%23ffb7c5'/%3E%3Ctext x='50' y='70' font-size='58' text-anchor='middle' fill='%23ffffff' font-family='sans-serif' font-weight='bold'%3En%3C/text%3E%3C/svg%3E"
10
10
  />
11
11
  <title>neko harness</title>
12
- <script type="module" crossorigin src="/assets/index-B1I59uXv.js"></script>
13
- <link rel="stylesheet" crossorigin href="/assets/index-U3kl-2BJ.css">
12
+ <script type="module" crossorigin src="/assets/index-D6NpJLXy.js"></script>
13
+ <link rel="stylesheet" crossorigin href="/assets/index-CxnbrvVk.css">
14
14
  </head>
15
15
  <body>
16
16
  <div id="root"></div>
package/dist/gateway.js CHANGED
@@ -1,10 +1,113 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
3
 
4
+ // packages/gateway/src/cli-args.ts
5
+ var GATEWAY_USAGE = `\u7528\u6CD5: main.ts [\u9009\u9879]
6
+
7
+ \u9009\u9879:
8
+ --host <ip> WebSocket \u76D1\u542C\u5730\u5740\uFF08\u9ED8\u8BA4 127.0.0.1\uFF0C\u4EC5\u672C\u673A\uFF09
9
+ --port <n> WebSocket \u76D1\u542C\u7AEF\u53E3\uFF08\u9ED8\u8BA4 8321\uFF0CWindows \u9ED8\u8BA4\u968F\u673A\uFF1B0/null \u7981\u7528 WS\uFF09
10
+ --socket-path <path> Unix socket \u8DEF\u5F84\uFF08\u9ED8\u8BA4 <\u6570\u636E\u76EE\u5F55>/<serverId>.sock\uFF1BWindows \u9ED8\u8BA4\u7981\u7528\uFF09
11
+ --no-socket \u7981\u7528 Unix socket \u4F20\u8F93\uFF08\u4EC5 WebSocket\uFF09
12
+ --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
13
+ --cwd <dir> \u4F1A\u8BDD\u5DE5\u5177\u7684\u5DE5\u4F5C\u76EE\u5F55\uFF08\u9ED8\u8BA4\u5F53\u524D\u76EE\u5F55\uFF09
14
+ --model <ref> \u6A21\u578B\uFF0C\u683C\u5F0F provider/modelId\uFF08\u9ED8\u8BA4 deepseek/deepseek-flash\uFF09
15
+ --storage <path> SQLite \u5B58\u50A8\u6587\u4EF6\uFF08\u9ED8\u8BA4 ~/.neko/neko.sqlite\uFF09
16
+ --credentials <path> \u7528\u6237\u51ED\u636E\u6587\u4EF6\uFF08\u9ED8\u8BA4 ~/.neko/credentials.json\uFF09
17
+ --config <dir> neko.config.json \u67E5\u627E\u8D77\u59CB\u76EE\u5F55\uFF08\u9ED8\u8BA4\u5F53\u524D\u76EE\u5F55\uFF09
18
+ -h, --help \u663E\u793A\u6B64\u5E2E\u52A9
19
+
20
+ \u53EF\u901A\u8FC7\u524D\u7AEF\u8BBE\u7F6E\u6216 DEEPSEEK_API_KEY \u914D\u7F6E\u51ED\u636E\u3002`;
21
+ function parseGatewayArgs(argv) {
22
+ let port;
23
+ const options = {};
24
+ for (let i = 0;i < argv.length; i++) {
25
+ const arg = argv[i];
26
+ const next = argv[i + 1];
27
+ switch (arg) {
28
+ case "--host":
29
+ if (next === undefined)
30
+ throw new Error("--host \u9700\u8981\u53C2\u6570");
31
+ options.host = next;
32
+ i++;
33
+ break;
34
+ case "--port": {
35
+ if (next === undefined)
36
+ throw new Error("--port \u9700\u8981\u53C2\u6570");
37
+ if (next === "0" || next === "null") {
38
+ port = null;
39
+ } else {
40
+ const parsed = Number(next);
41
+ if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65535) {
42
+ throw new Error(`--port \u9700\u8981 0-65535 \u7684\u6574\u6570: ${next}`);
43
+ }
44
+ port = parsed;
45
+ }
46
+ i++;
47
+ break;
48
+ }
49
+ case "--socket-path":
50
+ if (next === undefined)
51
+ throw new Error("--socket-path \u9700\u8981\u53C2\u6570");
52
+ options.socketPath = next;
53
+ i++;
54
+ break;
55
+ case "--no-socket":
56
+ options.socketPath = null;
57
+ break;
58
+ case "--server-id":
59
+ if (next === undefined)
60
+ throw new Error("--server-id \u9700\u8981\u53C2\u6570");
61
+ options.serverId = next;
62
+ i++;
63
+ break;
64
+ case "--cwd":
65
+ if (next === undefined)
66
+ throw new Error("--cwd \u9700\u8981\u53C2\u6570");
67
+ options.cwd = next;
68
+ i++;
69
+ break;
70
+ case "--model": {
71
+ if (next === undefined)
72
+ throw new Error("--model \u9700\u8981\u53C2\u6570");
73
+ const slash = next.indexOf("/");
74
+ if (slash <= 0 || slash === next.length - 1)
75
+ throw new Error(`--model \u683C\u5F0F\u4E3A provider/modelId: ${next}`);
76
+ options.model = { provider: next.slice(0, slash), modelId: next.slice(slash + 1) };
77
+ i++;
78
+ break;
79
+ }
80
+ case "--credentials":
81
+ if (next === undefined)
82
+ throw new Error("--credentials \u9700\u8981\u53C2\u6570");
83
+ options.credentialsPath = next;
84
+ i++;
85
+ break;
86
+ case "--storage":
87
+ if (next === undefined)
88
+ throw new Error("--storage \u9700\u8981\u53C2\u6570");
89
+ options.storagePath = next;
90
+ i++;
91
+ break;
92
+ case "--config":
93
+ if (next === undefined)
94
+ throw new Error("--config \u9700\u8981\u53C2\u6570");
95
+ options.configDir = next;
96
+ i++;
97
+ break;
98
+ default:
99
+ throw new Error(`\u672A\u77E5\u9009\u9879: ${arg}`);
100
+ }
101
+ }
102
+ if (port !== undefined)
103
+ options.port = port;
104
+ return options;
105
+ }
106
+
4
107
  // packages/gateway/src/gateway.ts
5
108
  import { randomUUID } from "crypto";
6
- import { mkdir as mkdir2, readFile as readFile4, writeFile } from "fs/promises";
7
- import { basename as basename4, dirname as dirname6, join as join8 } from "path";
109
+ import { mkdir as mkdir2, readFile as readFile5, writeFile as writeFile2 } from "fs/promises";
110
+ import { basename as basename4, dirname as dirname6, join as join9 } from "path";
8
111
  import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT2 } from "@earendil-works/chord/context";
9
112
  import { isServerId } from "@earendil-works/pi-protocol";
10
113
  import { Server, SessionNotFoundError } from "@earendil-works/pi-server";
@@ -217,8 +320,8 @@ function validate(config, source) {
217
320
  if (port !== undefined && (!Number.isInteger(port) || port < 0 || port > 65535)) {
218
321
  throw new Error(`neko config: ${source}: "gateway.port" must be an integer between 0 and 65535.`);
219
322
  }
220
- if (socketPath !== undefined && typeof socketPath !== "string") {
221
- throw new Error(`neko config: ${source}: "gateway.socketPath" must be a string.`);
323
+ if (socketPath !== undefined && socketPath !== null && typeof socketPath !== "string") {
324
+ throw new Error(`neko config: ${source}: "gateway.socketPath" must be a string or null.`);
222
325
  }
223
326
  }
224
327
  const theme = config.theme;
@@ -1897,7 +2000,10 @@ async function createNekoRuntime(options = {}) {
1897
2000
  conversationCreated: async (tx, record) => {
1898
2001
  (await tx.doc(SessionMetadataDoc, record.id)).createdAt = Date.now();
1899
2002
  },
1900
- env: ({ cwd }) => new NodeExecutionEnv({ cwd: executionWorkspace(cwd) })
2003
+ env: ({ cwd }) => new NodeExecutionEnv({
2004
+ cwd: executionWorkspace(cwd),
2005
+ ...process.platform === "win32" ? { shellEnv: { PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" } } : {}
2006
+ })
1901
2007
  }, context);
1902
2008
  const scanConversations = () => harness.commit(async (tx) => {
1903
2009
  const records = [];
@@ -2323,9 +2429,24 @@ async function createNekoRuntime(options = {}) {
2323
2429
  }
2324
2430
  };
2325
2431
  }
2432
+ // packages/gateway/src/port-file.ts
2433
+ import { readFile as readFile4, rm, writeFile } from "fs/promises";
2434
+ import { join as join6 } from "path";
2435
+ function gatewayPortFilePath(nekoDir, serverId) {
2436
+ return join6(nekoDir, `${serverId}.port`);
2437
+ }
2438
+ async function writeGatewayPortFile(nekoDir, serverId, wsUrl) {
2439
+ await writeFile(gatewayPortFilePath(nekoDir, serverId), `${wsUrl}
2440
+ `, { mode: 384 });
2441
+ }
2442
+ async function removeGatewayPortFile(nekoDir, serverId) {
2443
+ await rm(gatewayPortFilePath(nekoDir, serverId), { force: true });
2444
+ }
2445
+
2326
2446
  // packages/gateway/src/server-services.ts
2327
2447
  import {
2328
2448
  createRemoteServiceEndpoint,
2449
+ RemoteServiceError,
2329
2450
  RemoteServiceProvider,
2330
2451
  replicatedState
2331
2452
  } from "@earendil-works/chord";
@@ -2334,13 +2455,20 @@ import {
2334
2455
  import { spawn } from "child_process";
2335
2456
  import { accessSync as accessSync2, constants as constants2 } from "fs";
2336
2457
  import { realpath, stat } from "fs/promises";
2337
- import { basename as basename2, delimiter, isAbsolute as isAbsolute2, join as join6, relative, resolve as resolve4, sep } from "path";
2458
+ import { basename as basename2, delimiter, isAbsolute as isAbsolute2, join as join7, relative, resolve as resolve4, sep } from "path";
2459
+
2460
+ class MissingFileSearchDependencyError extends Error {
2461
+ constructor() {
2462
+ super("Install fd (or fdfind) to search workspace files.");
2463
+ this.name = "MissingFileSearchDependencyError";
2464
+ }
2465
+ }
2338
2466
  function findFd() {
2339
2467
  for (const name of process.platform === "win32" ? ["fd.exe", "fdfind.exe"] : ["fd", "fdfind"]) {
2340
2468
  for (const directory of (process.env.PATH ?? "").split(delimiter)) {
2341
2469
  if (!directory)
2342
2470
  continue;
2343
- const path = join6(directory, name);
2471
+ const path = join7(directory, name);
2344
2472
  try {
2345
2473
  accessSync2(path, constants2.X_OK);
2346
2474
  return path;
@@ -2364,7 +2492,7 @@ async function workspaceTarget(root, path) {
2364
2492
  }
2365
2493
  async function searchWorkspaceFiles(root, query, signal, fd = findFd()) {
2366
2494
  if (!fd)
2367
- throw new Error("Install fd (or fdfind) to search workspace files.");
2495
+ throw new MissingFileSearchDependencyError;
2368
2496
  query = relativeQuery(query);
2369
2497
  const slash = query.lastIndexOf("/");
2370
2498
  const prefix = slash < 0 ? "" : query.slice(0, slash + 1);
@@ -2388,6 +2516,8 @@ async function searchWorkspaceFiles(root, query, signal, fd = findFd()) {
2388
2516
  "--max-results",
2389
2517
  "101",
2390
2518
  "--print0",
2519
+ "--path-separator",
2520
+ "/",
2391
2521
  "--color",
2392
2522
  "never"
2393
2523
  ];
@@ -2456,14 +2586,38 @@ var NekoWorkspaces = defineService("neko.workspaces");
2456
2586
  var NekoCredentials = defineService("neko.credentials");
2457
2587
  var NekoWorkspaceFiles = defineService("neko.workspace-files");
2458
2588
 
2589
+ // packages/gateway/src/file-search-help.ts
2590
+ function fileSearchInstallHelp(platform = process.platform) {
2591
+ const docs = "https://github.com/sharkdp/fd#installation";
2592
+ let install;
2593
+ switch (platform) {
2594
+ case "win32":
2595
+ install = "\u8BF7\u6267\u884C\uFF1Awinget install sharkdp.fd\u3002";
2596
+ break;
2597
+ case "darwin":
2598
+ install = "\u8BF7\u6267\u884C\uFF1Abrew install fd\u3002";
2599
+ break;
2600
+ case "linux":
2601
+ install = `Debian/Ubuntu \u8BF7\u6267\u884C\uFF1Asudo apt install fd-find\u3002\u5176\u4ED6\u53D1\u884C\u7248\u8BF7\u53C2\u8003\uFF1A${docs} \u3002`;
2602
+ break;
2603
+ default:
2604
+ install = `\u5B89\u88C5\u65B9\u6CD5\u8BF7\u53C2\u8003\uFF1A${docs} \u3002`;
2605
+ }
2606
+ return `\u672A\u627E\u5230 fd \u6216 fdfind\uFF0C\u6682\u65F6\u65E0\u6CD5\u4F7F\u7528 @ \u6587\u4EF6\u8865\u5168\u3002\u8BF7\u5728\u8FD0\u884C neko \u7684\u7535\u8111\u4E0A\u5B89\u88C5\u3002${install}\u5B89\u88C5\u540E\u91CD\u65B0\u6253\u5F00\u7EC8\u7AEF\u5E76\u91CD\u542F neko\u3002`;
2607
+ }
2608
+
2459
2609
  // packages/gateway/src/workspaces.ts
2460
2610
  import { constants as constants3 } from "fs";
2461
2611
  import { access, readdir as readdir2, stat as stat2 } from "fs/promises";
2462
- import { dirname as dirname5, isAbsolute as isAbsolute3, join as join7 } from "path";
2612
+ import { dirname as dirname5, isAbsolute as isAbsolute3, join as join8 } from "path";
2463
2613
  async function browseWorkspaces(options) {
2464
- if (!isAbsolute3(options.path))
2614
+ const requested = options.path.replace(/^\/+([A-Za-z]:[\\/])/, "$1");
2615
+ if (process.platform === "win32" && (requested === "/" || requested === "\\")) {
2616
+ return { path: "/", parent: null, entries: await listDrives(), next: null };
2617
+ }
2618
+ if (!isAbsolute3(requested))
2465
2619
  throw new Error("\u8BF7\u9009\u62E9\u5B8C\u6574\u7684\u7EDD\u5BF9\u76EE\u5F55\u8DEF\u5F84");
2466
- const path = accessibleWorkspace(options.path);
2620
+ const path = accessibleWorkspace(requested);
2467
2621
  const query = options.query?.toLowerCase() ?? "";
2468
2622
  const key = JSON.stringify([path, query, options.showHidden === true]);
2469
2623
  let offset = 0;
@@ -2482,7 +2636,7 @@ async function browseWorkspaces(options) {
2482
2636
  let next = offset;
2483
2637
  for (;next < entries.length && directories.length < 100; next++) {
2484
2638
  const entry = entries[next];
2485
- const target = join7(path, entry.name);
2639
+ const target = join8(path, entry.name);
2486
2640
  if (!entry.isDirectory() && !entry.isSymbolicLink())
2487
2641
  continue;
2488
2642
  try {
@@ -2501,6 +2655,17 @@ async function browseWorkspaces(options) {
2501
2655
  next: next < entries.length ? JSON.stringify({ key, offset: next }) : null
2502
2656
  };
2503
2657
  }
2658
+ async function listDrives() {
2659
+ const drives = [];
2660
+ for (const letter of "ABCDEFGHIJKLMNOPQRSTUVWXYZ") {
2661
+ const root = `${letter}:\\`;
2662
+ try {
2663
+ await access(root);
2664
+ drives.push({ name: `${letter}:`, path: root });
2665
+ } catch {}
2666
+ }
2667
+ return drives;
2668
+ }
2504
2669
 
2505
2670
  // packages/gateway/src/server-services.ts
2506
2671
  async function createNekoServerServices(options) {
@@ -2598,6 +2763,10 @@ async function createNekoServerServices(options) {
2598
2763
  if (!roots.some((w) => w.path === input.workspaceRoot))
2599
2764
  throw new Error("Unknown workspace.");
2600
2765
  return await searchWorkspaceFiles(input.workspaceRoot, input.query, AbortSignal.any([controller.signal, ...ctx.abortSignal ? [ctx.abortSignal] : []]));
2766
+ } catch (error) {
2767
+ if (error instanceof MissingFileSearchDependencyError)
2768
+ throw new RemoteServiceError("service_not_allowed", fileSearchInstallHelp());
2769
+ throw error;
2601
2770
  } finally {
2602
2771
  fileQueries--;
2603
2772
  if (fileQuery === controller)
@@ -2688,7 +2857,7 @@ import { open as open2 } from "fs/promises";
2688
2857
  import { basename as basename3, extname } from "path";
2689
2858
  import {
2690
2859
  createRemoteServiceEndpoint as createRemoteServiceEndpoint2,
2691
- RemoteServiceError,
2860
+ RemoteServiceError as RemoteServiceError2,
2692
2861
  RemoteServiceProvider as RemoteServiceProvider2,
2693
2862
  replicatedState as replicatedState2
2694
2863
  } from "@earendil-works/chord";
@@ -2856,7 +3025,7 @@ async function openNekoSessionHandle(options) {
2856
3025
  try {
2857
3026
  return action();
2858
3027
  } catch (error) {
2859
- throw new RemoteServiceError("service_invalid_value", error instanceof Error ? error.message : "Image upload failed");
3028
+ throw new RemoteServiceError2("service_invalid_value", error instanceof Error ? error.message : "Image upload failed");
2860
3029
  }
2861
3030
  };
2862
3031
  const createProvider = () => {
@@ -2913,7 +3082,7 @@ async function openNekoSessionHandle(options) {
2913
3082
  try {
2914
3083
  await session.compact(instructions);
2915
3084
  } catch (error) {
2916
- throw new RemoteServiceError("service_invalid_value", error instanceof Error ? error.message : "Cannot compact");
3085
+ throw new RemoteServiceError2("service_invalid_value", error instanceof Error ? error.message : "Cannot compact");
2917
3086
  }
2918
3087
  },
2919
3088
  beginImage: async (input) => safeUpload(() => uploads.begin(input)),
@@ -2922,15 +3091,15 @@ async function openNekoSessionHandle(options) {
2922
3091
  releaseImage: async (id) => uploads.release(id),
2923
3092
  getImage: async (input) => {
2924
3093
  if (!Number.isSafeInteger(input.offset) || input.offset < 0 || !Number.isSafeInteger(input.contentIndex) || input.contentIndex < 0)
2925
- throw new RemoteServiceError("service_invalid_value", "Invalid image range");
3094
+ throw new RemoteServiceError2("service_invalid_value", "Invalid image range");
2926
3095
  const entry = history.get(input.entryId);
2927
3096
  const content = entry?.model?.[0]?.content;
2928
3097
  const image = Array.isArray(content) ? content[input.contentIndex] : undefined;
2929
3098
  if (image?.type !== "image")
2930
- throw new RemoteServiceError("service_invalid_value", "Image is not part of this conversation");
3099
+ throw new RemoteServiceError2("service_invalid_value", "Image is not part of this conversation");
2931
3100
  const bytes = Buffer.from(image.data, "base64");
2932
3101
  if (input.offset > bytes.length)
2933
- throw new RemoteServiceError("service_invalid_value", "Invalid image range");
3102
+ throw new RemoteServiceError2("service_invalid_value", "Invalid image range");
2934
3103
  return {
2935
3104
  data: bytes.subarray(input.offset, input.offset + IMAGE_CHUNK_BYTES).toString("base64"),
2936
3105
  total: bytes.length,
@@ -2950,8 +3119,8 @@ async function openNekoSessionHandle(options) {
2950
3119
  uploads.release(id);
2951
3120
  } catch (error) {
2952
3121
  if (error instanceof Error && error.message.startsWith("DEEPSEEK_KEY_REQUIRED:"))
2953
- throw new RemoteServiceError("service_invalid_value", "DEEPSEEK_KEY_REQUIRED: Configure DeepSeek in /login or Web settings.");
2954
- throw new RemoteServiceError("service_invalid_value", error instanceof Error ? error.message : "Cannot submit message");
3122
+ throw new RemoteServiceError2("service_invalid_value", "DEEPSEEK_KEY_REQUIRED: Configure DeepSeek in /login or Web settings.");
3123
+ throw new RemoteServiceError2("service_invalid_value", error instanceof Error ? error.message : "Cannot submit message");
2955
3124
  }
2956
3125
  await onActivity();
2957
3126
  },
@@ -2970,7 +3139,7 @@ async function openNekoSessionHandle(options) {
2970
3139
  configure: async (change) => {
2971
3140
  const applied = await runtime.configureConversation(session.id, change);
2972
3141
  if (!applied) {
2973
- throw new RemoteServiceError("service_invalid_value", `Session ${String(session.id)} is unavailable or archived`);
3142
+ throw new RemoteServiceError2("service_invalid_value", `Session ${String(session.id)} is unavailable or archived`);
2974
3143
  }
2975
3144
  }
2976
3145
  });
@@ -3123,10 +3292,10 @@ async function startGateway(options = {}) {
3123
3292
  const model = options.model ?? fileConfig.defaultModel ?? DEFAULT_MODEL;
3124
3293
  const storagePath = options.storagePath ?? fileConfig.storagePath ?? DEFAULT_STORAGE_PATH;
3125
3294
  const host = options.host ?? fileConfig.gateway?.host ?? DEFAULT_HOST;
3126
- const port = options.port !== undefined ? options.port : fileConfig.gateway?.port ?? DEFAULT_PORT;
3295
+ const port = options.port !== undefined ? options.port : fileConfig.gateway?.port ?? (process.platform === "win32" ? 0 : DEFAULT_PORT);
3127
3296
  const nekoDir = dirname6(storagePath);
3128
3297
  const serverId = options.serverId ?? await loadOrCreateServerId(nekoDir);
3129
- const socketPath = options.socketPath !== undefined ? options.socketPath : fileConfig.gateway?.socketPath ?? join8(nekoDir, `${serverId}.sock`);
3298
+ const socketPath = options.socketPath !== undefined ? options.socketPath : fileConfig.gateway?.socketPath !== undefined ? fileConfig.gateway.socketPath : process.platform === "win32" ? null : join9(nekoDir, `${serverId}.sock`);
3130
3299
  const runtime = await createNekoRuntime({
3131
3300
  context,
3132
3301
  storage: options.storage ?? "sqlite",
@@ -3147,6 +3316,7 @@ async function startGateway(options = {}) {
3147
3316
  port,
3148
3317
  socketPath,
3149
3318
  serverId,
3319
+ nekoDir,
3150
3320
  model,
3151
3321
  onError: options.onError
3152
3322
  });
@@ -3156,7 +3326,7 @@ async function startGateway(options = {}) {
3156
3326
  }
3157
3327
  }
3158
3328
  async function bindGateway(runtime, options) {
3159
- const { context, host, port, socketPath, serverId, model } = options;
3329
+ const { context, host, port, socketPath, serverId, nekoDir, model } = options;
3160
3330
  const modelLabel = `${model.provider}/${model.modelId}`;
3161
3331
  const serverServices = await createNekoServerServices({ runtime, context });
3162
3332
  let wsListener;
@@ -3189,6 +3359,10 @@ async function bindGateway(runtime, options) {
3189
3359
  await serverServices.dispose().catch(() => {});
3190
3360
  throw toStartupError(error, { socketPath, port, host, serverId });
3191
3361
  }
3362
+ const portFile = process.platform === "win32" && wsListener !== undefined ? { nekoDir, serverId } : undefined;
3363
+ if (portFile !== undefined && wsListener !== undefined) {
3364
+ await writeGatewayPortFile(portFile.nekoDir, portFile.serverId, `ws://${host}:${wsListener.port}`);
3365
+ }
3192
3366
  let closed = false;
3193
3367
  return {
3194
3368
  serverId,
@@ -3200,6 +3374,8 @@ async function bindGateway(runtime, options) {
3200
3374
  if (closed)
3201
3375
  return;
3202
3376
  closed = true;
3377
+ if (portFile !== undefined)
3378
+ await removeGatewayPortFile(portFile.nekoDir, portFile.serverId).catch(() => {});
3203
3379
  await server.close();
3204
3380
  await serverServices.dispose();
3205
3381
  await runtime.close();
@@ -3209,7 +3385,8 @@ async function bindGateway(runtime, options) {
3209
3385
  function toStartupError(error, info) {
3210
3386
  const message = error instanceof Error ? error.message : String(error);
3211
3387
  const existingServerId = info.socketPath === null ? undefined : serverIdFromSocketPath(info.socketPath);
3212
- 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`;
3388
+ 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";
3389
+ const connectHint = `\u5982\u9700\u8FDE\u63A5\u5DF2\u6709\u5B9E\u4F8B\uFF1A${existingAddress}\uFF0C\u6216\u505C\u6389\u5360\u7528\u65B9\u540E\u91CD\u8BD5\u3002`;
3213
3390
  if (message.includes("already running")) {
3214
3391
  return new Error(`neko gateway \u542F\u52A8\u5931\u8D25\uFF1A${message}
3215
3392
  \u8BE5\u5730\u5740\u5DF2\u88AB\u4E00\u4E2A\u6D3B\u8DC3\u7F51\u5173\u5360\u7528\uFF0C\u4E0D\u80FD\u542F\u52A8\u7B2C\u4E8C\u4E2A\u5B9E\u4F8B\u3002
@@ -3229,123 +3406,26 @@ function serverIdFromSocketPath(socketPath) {
3229
3406
  return isServerId(candidate) ? candidate : undefined;
3230
3407
  }
3231
3408
  async function loadOrCreateServerId(nekoDir) {
3232
- const file = join8(nekoDir, SERVER_ID_FILENAME);
3409
+ const file = join9(nekoDir, SERVER_ID_FILENAME);
3233
3410
  try {
3234
- const existing = (await readFile4(file, "utf8")).trim();
3411
+ const existing = (await readFile5(file, "utf8")).trim();
3235
3412
  if (existing !== "")
3236
3413
  return existing;
3237
3414
  } catch {}
3238
3415
  const created = randomUUID();
3239
3416
  await mkdir2(nekoDir, { recursive: true, mode: 448 });
3240
- await writeFile(file, `${created}
3417
+ await writeFile2(file, `${created}
3241
3418
  `, { mode: 384 });
3242
3419
  return created;
3243
3420
  }
3244
3421
 
3245
3422
  // packages/gateway/src/main.ts
3246
- var USAGE = `\u7528\u6CD5: main.ts [\u9009\u9879]
3247
-
3248
- \u9009\u9879:
3249
- --host <ip> WebSocket \u76D1\u542C\u5730\u5740\uFF08\u9ED8\u8BA4 127.0.0.1\uFF0C\u4EC5\u672C\u673A\uFF1B0/null \u7981\u7528 WS\uFF09
3250
- --port <n> WebSocket \u76D1\u542C\u7AEF\u53E3\uFF08\u9ED8\u8BA4 8321\uFF1B0 = \u968F\u673A\u5206\u914D\uFF09
3251
- --socket-path <path> Unix socket \u8DEF\u5F84\uFF08\u9ED8\u8BA4 <\u6570\u636E\u76EE\u5F55>/<serverId>.sock\uFF09
3252
- --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
3253
- --cwd <dir> \u4F1A\u8BDD\u5DE5\u5177\u7684\u5DE5\u4F5C\u76EE\u5F55\uFF08\u9ED8\u8BA4\u5F53\u524D\u76EE\u5F55\uFF09
3254
- --model <ref> \u6A21\u578B\uFF0C\u683C\u5F0F provider/modelId\uFF08\u9ED8\u8BA4 deepseek/deepseek-flash\uFF09
3255
- --storage <path> SQLite \u5B58\u50A8\u6587\u4EF6\uFF08\u9ED8\u8BA4 ~/.neko/neko.sqlite\uFF09
3256
- --credentials <path> \u7528\u6237\u51ED\u636E\u6587\u4EF6\uFF08\u9ED8\u8BA4 ~/.neko/credentials.json\uFF09
3257
- --config <dir> neko.config.json \u67E5\u627E\u8D77\u59CB\u76EE\u5F55\uFF08\u9ED8\u8BA4\u5F53\u524D\u76EE\u5F55\uFF09
3258
- -h, --help \u663E\u793A\u6B64\u5E2E\u52A9
3259
-
3260
- \u53EF\u901A\u8FC7\u524D\u7AEF\u8BBE\u7F6E\u6216 DEEPSEEK_API_KEY \u914D\u7F6E\u51ED\u636E\u3002`;
3261
- function parseArgs(argv) {
3262
- let port;
3263
- const options = {};
3264
- for (let i = 0;i < argv.length; i++) {
3265
- const arg = argv[i];
3266
- const next = argv[i + 1];
3267
- switch (arg) {
3268
- case "--host":
3269
- if (next === undefined)
3270
- throw new Error("--host \u9700\u8981\u53C2\u6570");
3271
- options.host = next;
3272
- i++;
3273
- break;
3274
- case "--port": {
3275
- if (next === undefined)
3276
- throw new Error("--port \u9700\u8981\u53C2\u6570");
3277
- if (next === "0" || next === "null") {
3278
- port = null;
3279
- } else {
3280
- const parsed = Number(next);
3281
- if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65535) {
3282
- throw new Error(`--port \u9700\u8981 0-65535 \u7684\u6574\u6570: ${next}`);
3283
- }
3284
- port = parsed;
3285
- }
3286
- i++;
3287
- break;
3288
- }
3289
- case "--socket-path":
3290
- if (next === undefined)
3291
- throw new Error("--socket-path \u9700\u8981\u53C2\u6570");
3292
- options.socketPath = next;
3293
- i++;
3294
- break;
3295
- case "--server-id":
3296
- if (next === undefined)
3297
- throw new Error("--server-id \u9700\u8981\u53C2\u6570");
3298
- options.serverId = next;
3299
- i++;
3300
- break;
3301
- case "--cwd":
3302
- if (next === undefined)
3303
- throw new Error("--cwd \u9700\u8981\u53C2\u6570");
3304
- options.cwd = next;
3305
- i++;
3306
- break;
3307
- case "--model": {
3308
- if (next === undefined)
3309
- throw new Error("--model \u9700\u8981\u53C2\u6570");
3310
- const slash = next.indexOf("/");
3311
- if (slash <= 0 || slash === next.length - 1)
3312
- throw new Error(`--model \u683C\u5F0F\u4E3A provider/modelId: ${next}`);
3313
- options.model = { provider: next.slice(0, slash), modelId: next.slice(slash + 1) };
3314
- i++;
3315
- break;
3316
- }
3317
- case "--credentials":
3318
- if (next === undefined)
3319
- throw new Error("--credentials \u9700\u8981\u53C2\u6570");
3320
- options.credentialsPath = next;
3321
- i++;
3322
- break;
3323
- case "--storage":
3324
- if (next === undefined)
3325
- throw new Error("--storage \u9700\u8981\u53C2\u6570");
3326
- options.storagePath = next;
3327
- i++;
3328
- break;
3329
- case "--config":
3330
- if (next === undefined)
3331
- throw new Error("--config \u9700\u8981\u53C2\u6570");
3332
- options.configDir = next;
3333
- i++;
3334
- break;
3335
- case "-h":
3336
- case "--help":
3337
- console.log(USAGE);
3338
- process.exit(0);
3339
- break;
3340
- default:
3341
- throw new Error(`\u672A\u77E5\u9009\u9879: ${arg}`);
3342
- }
3343
- }
3344
- if (port !== undefined)
3345
- options.port = port;
3346
- return options;
3423
+ var argv = process.argv.slice(2);
3424
+ if (argv.includes("-h") || argv.includes("--help")) {
3425
+ console.log(GATEWAY_USAGE);
3426
+ process.exit(0);
3347
3427
  }
3348
- var options = parseArgs(process.argv.slice(2));
3428
+ var options = parseGatewayArgs(argv);
3349
3429
  var gateway = await startGateway({
3350
3430
  ...options,
3351
3431
  onError: (error) => console.error("gateway error:", error.message)