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/client/assets/index-CxnbrvVk.css +2 -0
- package/dist/client/assets/index-D6NpJLXy.js +88 -0
- package/dist/client/index.html +2 -2
- package/dist/gateway.js +166 -120
- package/dist/neko.js +223 -66
- package/package.json +1 -1
- package/dist/client/assets/index-B1I59uXv.js +0 -88
- package/dist/client/assets/index-U3kl-2BJ.css +0 -2
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({
|
|
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(
|
|
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
|
|
3889
|
+
import { dirname as dirname5, isAbsolute as isAbsolute3, join as join8 } from "path";
|
|
3862
3890
|
async function browseWorkspaces(options) {
|
|
3863
|
-
|
|
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(
|
|
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 =
|
|
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
|
|
4444
|
-
import { basename as basename5, dirname as dirname6, join as
|
|
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
|
|
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
|
|
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 =
|
|
4612
|
+
const file = join9(nekoDir, SERVER_ID_FILENAME);
|
|
4562
4613
|
try {
|
|
4563
|
-
const existing = (await
|
|
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
|
|
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
|
|
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(
|
|
4627
|
-
|
|
4628
|
-
|
|
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(
|
|
4659
|
-
socketPath =
|
|
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})`,
|
|
4662
|
-
const serverId = plan.fileServerId ?? serverIdFromSocketPath2(socketPath) ?? readServerId(
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
5877
|
-
import { dirname as dirname8, join as
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
6125
|
+
const file = join11(nekoDir, SERVER_ID_FILENAME3);
|
|
5976
6126
|
try {
|
|
5977
|
-
const existing = (await
|
|
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
|
|
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/
|
|
6273
|
-
function
|
|
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
|
|
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\
|
|
6364
|
-
--port <n> WebSocket \u76D1\u542C\u7AEF\u53E3\uFF08\u9ED8\u8BA4 8321\
|
|
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
|
|
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
|
-
|
|
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
|
|
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\
|
|
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.
|
|
6583
|
+
console.log("0.2.0");
|
|
6427
6584
|
break;
|
|
6428
6585
|
case undefined:
|
|
6429
6586
|
case "-h":
|
|
6430
6587
|
case "--help":
|
|
6431
|
-
console.log(
|
|
6588
|
+
console.log(USAGE3);
|
|
6432
6589
|
break;
|
|
6433
6590
|
default:
|
|
6434
6591
|
console.error(`\u672A\u77E5\u547D\u4EE4: ${command2}
|
|
6435
6592
|
|
|
6436
|
-
${
|
|
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.
|
|
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": {
|