neko-harness 0.4.6 → 0.4.7

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
@@ -80,7 +80,7 @@ function updateAction(input) {
80
80
  return "install";
81
81
  return "none";
82
82
  }
83
- var RUNNING_VERSION = "0.4.6", APPLICATION_PROTOCOL = 8, ApplicationProtocolError;
83
+ var RUNNING_VERSION = "0.4.7", APPLICATION_PROTOCOL = 8, ApplicationProtocolError;
84
84
  var init_application_version = __esm(() => {
85
85
  ApplicationProtocolError = class ApplicationProtocolError extends Error {
86
86
  constructor() {
@@ -90,8 +90,35 @@ var init_application_version = __esm(() => {
90
90
  };
91
91
  });
92
92
 
93
+ // packages/core/src/rename-with-retry.ts
94
+ import { rename } from "fs/promises";
95
+ async function renameWithRetry(from, to, deps = {}) {
96
+ const doRename = deps.rename ?? rename;
97
+ const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
98
+ let delay = FIRST_DELAY_MS;
99
+ let waited = 0;
100
+ for (;; ) {
101
+ try {
102
+ await doRename(from, to);
103
+ return;
104
+ } catch (error) {
105
+ const code = error.code;
106
+ if (typeof code !== "string" || !TRANSIENT.has(code) || waited >= MAX_TOTAL_MS)
107
+ throw error;
108
+ const step = Math.min(delay, MAX_TOTAL_MS - waited);
109
+ await sleep(step);
110
+ waited += step;
111
+ delay *= 2;
112
+ }
113
+ }
114
+ }
115
+ var TRANSIENT, FIRST_DELAY_MS = 10, MAX_TOTAL_MS = 1000;
116
+ var init_rename_with_retry = __esm(() => {
117
+ TRANSIENT = new Set(["EPERM", "EBUSY", "EACCES"]);
118
+ });
119
+
93
120
  // packages/core/src/application-updates.ts
94
- import { mkdir, readFile, rename, rm, writeFile } from "fs/promises";
121
+ import { mkdir, readFile, rm, writeFile } from "fs/promises";
95
122
  import { homedir } from "os";
96
123
  import { dirname, join } from "path";
97
124
  import { fileURLToPath } from "url";
@@ -220,7 +247,7 @@ class UpdateChecker {
220
247
  try {
221
248
  await mkdir(dirname(this.#cachePath), { recursive: true });
222
249
  await writeFile(temporary, JSON.stringify({ latestVersion: value.version, checkedAt: now }), { mode: 384 });
223
- await rename(temporary, this.#cachePath);
250
+ await renameWithRetry(temporary, this.#cachePath);
224
251
  } catch {} finally {
225
252
  await rm(temporary, { force: true }).catch(() => {});
226
253
  }
@@ -238,6 +265,7 @@ class UpdateChecker {
238
265
  var INSTALL_MANIFEST;
239
266
  var init_application_updates = __esm(() => {
240
267
  init_application_version();
268
+ init_rename_with_retry();
241
269
  INSTALL_MANIFEST = stableVersion(RUNNING_VERSION) ? fileURLToPath(new URL("../package.json", import.meta.url)) : undefined;
242
270
  });
243
271
 
@@ -665,6 +693,16 @@ function validate(config, source) {
665
693
  throw new Error(`neko config: ${source}: "skills.marketUrl" must be a string.`);
666
694
  }
667
695
  }
696
+ const shell = config.shell;
697
+ if (shell !== undefined) {
698
+ if (typeof shell !== "object" || shell === null || Array.isArray(shell)) {
699
+ throw new Error(`neko config: ${source}: "shell" must be an object.`);
700
+ }
701
+ const { path } = shell;
702
+ if (path !== undefined && typeof path !== "string") {
703
+ throw new Error(`neko config: ${source}: "shell.path" must be a string.`);
704
+ }
705
+ }
668
706
  const webSearch = config.webSearch;
669
707
  if (webSearch !== undefined) {
670
708
  if (typeof webSearch !== "object" || webSearch === null || Array.isArray(webSearch)) {
@@ -759,15 +797,17 @@ var init_credential_providers = __esm(() => {
759
797
  });
760
798
 
761
799
  // packages/core/src/credentials.ts
762
- import { chmod, mkdir as mkdir3, open, readFile as readFile3, rename as rename2, unlink } from "fs/promises";
800
+ import { chmod, mkdir as mkdir3, open, readFile as readFile3, stat as stat2, unlink } from "fs/promises";
763
801
  import { homedir as homedir3 } from "os";
764
802
  import { dirname as dirname4, join as join4 } from "path";
765
803
  import { InMemoryCredentialStore } from "@earendil-works/pi-ai";
766
804
 
767
805
  class FileCredentialStore {
768
806
  path;
769
- constructor(path = DEFAULT_CREDENTIALS_PATH) {
807
+ #renameDeps;
808
+ constructor(path = DEFAULT_CREDENTIALS_PATH, renameDeps = {}) {
770
809
  this.path = path;
810
+ this.#renameDeps = renameDeps;
771
811
  }
772
812
  async#read() {
773
813
  try {
@@ -797,6 +837,7 @@ class FileCredentialStore {
797
837
  await mkdir3(dirname4(this.path), { recursive: true, mode: 448 });
798
838
  const lockPath = `${this.path}.lock`;
799
839
  let lock;
840
+ let reclaimed = false;
800
841
  for (let attempt = 0;; attempt++) {
801
842
  try {
802
843
  lock = await open(lockPath, "wx", 384);
@@ -804,6 +845,16 @@ class FileCredentialStore {
804
845
  } catch (error) {
805
846
  if (error.code !== "EEXIST" || attempt >= 100)
806
847
  throw new Error("Credential storage is busy or unavailable.");
848
+ if (!reclaimed) {
849
+ const info = await stat2(lockPath).catch(() => {
850
+ return;
851
+ });
852
+ if (info && Date.now() - info.mtimeMs > STALE_LOCK_MS) {
853
+ reclaimed = true;
854
+ await unlink(lockPath).catch(() => {});
855
+ continue;
856
+ }
857
+ }
807
858
  await new Promise((resolve) => setTimeout(resolve, 25));
808
859
  }
809
860
  }
@@ -825,7 +876,7 @@ class FileCredentialStore {
825
876
  } finally {
826
877
  await file.close();
827
878
  }
828
- await rename2(temporary, this.path);
879
+ await renameWithRetry(temporary, this.path, this.#renameDeps);
829
880
  return result;
830
881
  } catch (error) {
831
882
  if (callbackFailed)
@@ -851,8 +902,9 @@ class FileCredentialStore {
851
902
  });
852
903
  }
853
904
  }
854
- var DEFAULT_CREDENTIALS_PATH;
905
+ var STALE_LOCK_MS = 30000, DEFAULT_CREDENTIALS_PATH;
855
906
  var init_credentials = __esm(() => {
907
+ init_rename_with_retry();
856
908
  init_credential_providers();
857
909
  DEFAULT_CREDENTIALS_PATH = join4(homedir3(), ".neko", "credentials.json");
858
910
  });
@@ -2897,7 +2949,7 @@ var init_agent_file_search = __esm(() => {
2897
2949
  complete = false;
2898
2950
  continue;
2899
2951
  }
2900
- const lines = new TextDecoder("utf-8", { fatal: true }).decode(data).split(/\r?\n/);
2952
+ const lines = new TextDecoder("utf-8", { fatal: true }).decode(data).replace(/^\uFEFF/, "").split(/\r?\n/);
2901
2953
  for (let i = 0;i < lines.length; i++) {
2902
2954
  ctx.abortSignal?.throwIfAborted();
2903
2955
  const value = lines[i];
@@ -3287,8 +3339,11 @@ class JobManager {
3287
3339
  outcome = { status: "failed", exitCode: null, reason: "timeout" };
3288
3340
  else if (result.error.code === "aborted")
3289
3341
  outcome = { status: "stopped", exitCode: null, reason: "aborted" };
3290
- else
3342
+ else {
3343
+ entry.buffer.append(`${result.error.message}
3344
+ `);
3291
3345
  outcome = { status: "failed", exitCode: null, reason: "spawn-error" };
3346
+ }
3292
3347
  }
3293
3348
  if (this.#closing) {
3294
3349
  await new Promise((resolve) => {
@@ -3696,10 +3751,13 @@ var init_invoke = __esm(() => {
3696
3751
 
3697
3752
  // packages/core/src/prompt/prompt.ts
3698
3753
  import { defineExtension as defineExtension5, section } from "@earendil-works/pi-durable";
3699
- var KEYS, PREAMBLE = "You are an expert coding assistant operating inside neko, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.", TOOL_GUIDELINES, GENERIC_RULES, buildRules = (toolNames) => {
3754
+ var KEYS, PREAMBLE = "You are an expert coding assistant operating inside neko, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.", TOOL_GUIDELINES, GENERIC_RULES, WINDOWS_SHELL_RULE = "On Windows, commands run in Git Bash (not PowerShell, cmd or WSL); write paths with forward slashes, e.g. D:/code or /d/code.", toolRules = (name, platform) => {
3755
+ const rules = TOOL_GUIDELINES[name] ?? [];
3756
+ return platform === "win32" && (name === "bash" || name === "job") ? [...rules, WINDOWS_SHELL_RULE] : rules;
3757
+ }, buildRules = (toolNames, platform) => {
3700
3758
  const rules = [];
3701
3759
  const seen = new Set;
3702
- for (const rule of [...toolNames.flatMap((name) => TOOL_GUIDELINES[name] ?? []), ...GENERIC_RULES]) {
3760
+ for (const rule of [...toolNames.flatMap((name) => toolRules(name, platform)), ...GENERIC_RULES]) {
3703
3761
  if (seen.has(rule))
3704
3762
  continue;
3705
3763
  seen.add(rule);
@@ -3714,7 +3772,7 @@ ${content}
3714
3772
  </project_instructions>`)
3715
3773
  ].join(`
3716
3774
 
3717
- `), createNekoPrompt = (loadResources) => {
3775
+ `), createNekoPrompt = (loadResources, platform = process.platform) => {
3718
3776
  const built = new WeakMap;
3719
3777
  const build = async (input, context) => {
3720
3778
  const cwd = input.agent.cwd;
@@ -3722,7 +3780,7 @@ ${content}
3722
3780
  const resources = cwd ? await loadResources(input.conversationId, cwd) : { contextFiles: [], skills: [] };
3723
3781
  const sections = {
3724
3782
  preamble: role ? ROLE_PROMPTS[role] + SUBAGENT_DELIVERY : PREAMBLE,
3725
- rules: buildRules(input.agent.tools.map((tool) => tool.name)),
3783
+ rules: buildRules(input.agent.tools.map((tool) => tool.name), platform),
3726
3784
  project_context: renderProjectContext(resources.contextFiles),
3727
3785
  skills: formatSkillsForPrompt(resources.skills) || undefined,
3728
3786
  cwd: cwd?.replace(/\\/g, "/")
@@ -3801,7 +3859,7 @@ var userAgentsFile = (home) => join6(home, ".agents", "AGENTS.md"), userSkillsDi
3801
3859
  const contextFiles = [];
3802
3860
  for (const path of [userAgentsFile(home), workspaceAgentsFile(workspace)]) {
3803
3861
  try {
3804
- const content = await readFile5(path, "utf8");
3862
+ const content = (await readFile5(path, "utf8")).replace(/^\uFEFF/, "");
3805
3863
  if (Buffer.byteLength(content, "utf8") > 64 * 1024)
3806
3864
  report({
3807
3865
  path,
@@ -5271,11 +5329,153 @@ var init_tavily = __esm(() => {
5271
5329
  init_http();
5272
5330
  });
5273
5331
 
5332
+ // packages/core/src/windows-shell.ts
5333
+ import { spawnSync } from "child_process";
5334
+ import { statSync as statSync2 } from "fs";
5335
+ import { win32 } from "path";
5336
+ function isWslBashPath(path) {
5337
+ const normalized = path.replace(/\//g, "\\").toLowerCase();
5338
+ return /^[a-z]:\\windows\\(?:system32|sysnative)\\bash\.exe$/.test(normalized) || /\\microsoft\\windowsapps\\(?:[^\\]+\\)*bash\.exe$/.test(normalized);
5339
+ }
5340
+ function envValue(env, name) {
5341
+ const wanted = name.toLowerCase();
5342
+ for (const [key, value] of Object.entries(env))
5343
+ if (key.toLowerCase() === wanted && value)
5344
+ return value;
5345
+ return;
5346
+ }
5347
+ function bashBesideGit(gitExe) {
5348
+ const dir = win32.dirname(gitExe);
5349
+ const parent = win32.dirname(dir);
5350
+ const name = win32.basename(dir).toLowerCase();
5351
+ if (name === "cmd")
5352
+ return win32.join(parent, "bin", "bash.exe");
5353
+ if (name === "bin") {
5354
+ const root = win32.basename(parent).toLowerCase() === "mingw64" ? win32.dirname(parent) : parent;
5355
+ return win32.join(root, "bin", "bash.exe");
5356
+ }
5357
+ return;
5358
+ }
5359
+ function configProblem(path, kind) {
5360
+ if (isWslBashPath(path))
5361
+ return "it is WSL's bash, which is not supported (commands must run in Git Bash)";
5362
+ if (kind === "missing")
5363
+ return "the file does not exist";
5364
+ if (kind === "directory")
5365
+ return "it is a directory, not bash.exe";
5366
+ return;
5367
+ }
5368
+ function resolveWindowsShell(deps, configPath) {
5369
+ if (configPath !== undefined && configPath !== "") {
5370
+ const problem = configProblem(configPath, deps.kind(configPath));
5371
+ if (problem !== undefined) {
5372
+ return {
5373
+ kind: "missing",
5374
+ reason: "invalid_config",
5375
+ detail: `shell.path "${configPath}" is invalid: ${problem}.`,
5376
+ searched: [configPath]
5377
+ };
5378
+ }
5379
+ return { kind: "found", path: configPath, source: "config" };
5380
+ }
5381
+ const searched = [];
5382
+ const check = (path) => {
5383
+ if (!searched.includes(path))
5384
+ searched.push(path);
5385
+ return !isWslBashPath(path) && deps.kind(path) === "file";
5386
+ };
5387
+ const roots = [
5388
+ envValue(deps.env, "ProgramFiles"),
5389
+ envValue(deps.env, "ProgramFiles(x86)"),
5390
+ (() => {
5391
+ const local = envValue(deps.env, "LOCALAPPDATA");
5392
+ return local === undefined ? undefined : win32.join(local, "Programs");
5393
+ })()
5394
+ ];
5395
+ for (const root of roots) {
5396
+ if (root === undefined)
5397
+ continue;
5398
+ const candidate = win32.join(root, "Git", "bin", "bash.exe");
5399
+ if (check(candidate))
5400
+ return { kind: "found", path: candidate, source: "auto" };
5401
+ }
5402
+ for (const gitExe of deps.where("git.exe")) {
5403
+ const candidate = bashBesideGit(gitExe);
5404
+ if (candidate !== undefined && check(candidate))
5405
+ return { kind: "found", path: candidate, source: "auto" };
5406
+ }
5407
+ for (const candidate of deps.where("bash.exe")) {
5408
+ if (check(candidate))
5409
+ return { kind: "found", path: candidate, source: "auto" };
5410
+ }
5411
+ return { kind: "missing", reason: "not_found", detail: "No Git Bash was found.", searched };
5412
+ }
5413
+ function summarizeWindowsShell(status) {
5414
+ return status.kind === "found" ? { status: "ready", path: status.path, source: status.source } : { status: "missing", reason: status.reason, detail: status.detail };
5415
+ }
5416
+ function windowsShellMessage(status) {
5417
+ const lines = status.reason === "invalid_config" ? [
5418
+ `Cannot run shell commands: ${status.detail}`,
5419
+ "Fix shell.path in neko.config.json, or remove it to auto-detect Git Bash."
5420
+ ] : [
5421
+ "Cannot run shell commands: Git Bash was not found. neko runs commands in Git for Windows' bash, not PowerShell, cmd or WSL.",
5422
+ "Install Git for Windows (https://git-scm.com/download/win), or set shell.path in neko.config.json to its bash.exe."
5423
+ ];
5424
+ if (status.searched.length > 0)
5425
+ lines.push("Checked:", ...status.searched.map((path) => ` ${path}`));
5426
+ lines.push("File tools (read, write, edit) still work.");
5427
+ return lines.join(`
5428
+ `);
5429
+ }
5430
+ function systemWindowsShellDeps() {
5431
+ return {
5432
+ env: process.env,
5433
+ kind(path) {
5434
+ try {
5435
+ return statSync2(path).isDirectory() ? "directory" : "file";
5436
+ } catch {
5437
+ return "missing";
5438
+ }
5439
+ },
5440
+ where(program) {
5441
+ const result = spawnSync("where", [program], { encoding: "utf8", timeout: 5000, windowsHide: true });
5442
+ if (result.status !== 0 || typeof result.stdout !== "string")
5443
+ return [];
5444
+ return result.stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => line !== "");
5445
+ }
5446
+ };
5447
+ }
5448
+ function createWindowsShellResolver(options) {
5449
+ const deps = options?.deps ?? systemWindowsShellDeps();
5450
+ const now = options?.now ?? Date.now;
5451
+ let cached;
5452
+ return {
5453
+ status(call) {
5454
+ if (cached !== undefined && call?.refresh !== true) {
5455
+ if (cached.status.kind === "found") {
5456
+ if (deps.kind(cached.status.path) === "file")
5457
+ return cached.status;
5458
+ } else if (now() - cached.at < WINDOWS_SHELL_RETRY_MS) {
5459
+ return cached.status;
5460
+ }
5461
+ }
5462
+ cached = { status: resolveWindowsShell(deps, options?.configPath), at: now() };
5463
+ return cached.status;
5464
+ }
5465
+ };
5466
+ }
5467
+ var WINDOWS_SHELL_RETRY_MS = 30000;
5468
+ var init_windows_shell = () => {};
5469
+
5274
5470
  // packages/core/src/workspace-files.ts
5275
- import { open as open2, realpath as realpath3, stat as stat2 } from "fs/promises";
5471
+ import { open as open2, realpath as realpath3, stat as stat3 } from "fs/promises";
5276
5472
  import { basename as basename3, isAbsolute as isAbsolute3, relative as relative2, resolve as resolve4, sep as sep2 } from "path";
5277
5473
  import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT2, withAbortSignal as withAbortSignal3 } from "@earendil-works/chord/context";
5278
- import { getOrThrow as getOrThrow2 } from "@earendil-works/pi-durable/env";
5474
+ import {
5475
+ ExecutionError,
5476
+ err,
5477
+ getOrThrow as getOrThrow2
5478
+ } from "@earendil-works/pi-durable/env";
5279
5479
  import { Connection, LocalExecutionEnv, localDaemon } from "@earendil-works/pi-env";
5280
5480
  function isProtectedPath(path) {
5281
5481
  const name = basename3(path).toLowerCase();
@@ -5313,8 +5513,10 @@ class WorkspaceFiles {
5313
5513
  #waiting = new Set;
5314
5514
  #timer;
5315
5515
  limits;
5316
- constructor(limits = FILE_CACHE_LIMITS) {
5516
+ #shell;
5517
+ constructor(limits = FILE_CACHE_LIMITS, shell) {
5317
5518
  this.limits = limits;
5519
+ this.#shell = shell;
5318
5520
  this.#timer = setInterval(() => {
5319
5521
  for (const [key, item] of this.#cache)
5320
5522
  if (!item.waiters && Date.now() - item.used >= this.limits.idleMs)
@@ -5333,20 +5535,31 @@ class WorkspaceFiles {
5333
5535
  queued: this.#waiting.size
5334
5536
  };
5335
5537
  }
5336
- env(cwd) {
5538
+ shellStatus(options) {
5539
+ return this.#shell?.status(options);
5540
+ }
5541
+ #open() {
5337
5542
  if (this.#closed)
5338
5543
  throw new Error("File environment closed");
5339
5544
  this.#connection ??= new Connection({ command: [localDaemon()] });
5340
- return new LocalExecutionEnv({
5341
- connection: this.#connection,
5545
+ return this.#connection;
5546
+ }
5547
+ env(cwd) {
5548
+ const options = {
5549
+ connection: this.#open(),
5342
5550
  cwd,
5343
5551
  ...process.platform === "win32" ? { shellEnv: { PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" } } : {}
5344
- });
5552
+ };
5553
+ const shell = this.#shell?.status();
5554
+ if (shell === undefined)
5555
+ return new LocalExecutionEnv(options);
5556
+ if (shell.kind === "found")
5557
+ return new LocalExecutionEnv({ ...options, shellPath: shell.path });
5558
+ return new ShellUnavailableEnv(options, windowsShellMessage(shell));
5345
5559
  }
5346
5560
  async#ready() {
5347
5561
  if (!this.#starting) {
5348
- this.env(process.cwd());
5349
- const connection = this.#connection;
5562
+ const connection = this.#open();
5350
5563
  let timer;
5351
5564
  const work = Promise.race([
5352
5565
  connection.info().then(() => connection.session()),
@@ -5423,7 +5636,7 @@ class WorkspaceFiles {
5423
5636
  { path: item.root, recursive: true, exclude: { names: [".git"] } }
5424
5637
  ];
5425
5638
  const git = resolve4(item.root, ".git");
5426
- if (await stat2(git).then(() => true, () => false))
5639
+ if (await stat3(git).then(() => true, () => false))
5427
5640
  targets.push({ path: git, recursive: true, exclude: { names: ["objects", "logs", "refs"] } });
5428
5641
  const result = await env.watch(targets, (change) => {
5429
5642
  for (const user of current.users) {
@@ -5531,7 +5744,7 @@ class WorkspaceFiles {
5531
5744
  const slash = query.lastIndexOf("/"), prefix = slash < 0 ? "" : query.slice(0, slash + 1), needle = query.slice(slash + 1);
5532
5745
  root = await realpath3(root);
5533
5746
  const base = await workspaceTarget(root, prefix);
5534
- if (!(await stat2(base)).isDirectory())
5747
+ if (!(await stat3(base)).isDirectory())
5535
5748
  throw new Error("Workspace directory is unavailable.");
5536
5749
  return this.#withListing(root, base, signal, async (listing) => {
5537
5750
  const pattern = new RegExp([...needle].map((c) => c.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*"), /[A-Z]/.test(needle) ? "" : "i");
@@ -5561,7 +5774,7 @@ class WorkspaceFiles {
5561
5774
  const clean = dir === "" ? "" : relativeQuery(dir).replace(/\/+$/, "");
5562
5775
  root = await realpath3(root);
5563
5776
  const target = await workspaceTarget(root, clean);
5564
- if (!(await stat2(target)).isDirectory())
5777
+ if (!(await stat3(target)).isDirectory())
5565
5778
  throw new Error("Not a directory.");
5566
5779
  const prefix = clean === "" ? "" : `${clean}/`;
5567
5780
  return this.#withListing(root, root, signal, async (listing) => {
@@ -5603,7 +5816,7 @@ class WorkspaceFiles {
5603
5816
  throw new Error("File not found.");
5604
5817
  throw error;
5605
5818
  }
5606
- const info = await stat2(target);
5819
+ const info = await stat3(target);
5607
5820
  if (!info.isFile())
5608
5821
  throw new Error("Not a file.");
5609
5822
  const base = { path: clean, size: info.size, mtimeMs: info.mtimeMs };
@@ -5706,8 +5919,9 @@ function cancellable(work, signal) {
5706
5919
  abort();
5707
5920
  });
5708
5921
  }
5709
- var BROWSE_LIMITS, ENV_TEMPLATE, FILE_CACHE_LIMITS;
5922
+ var BROWSE_LIMITS, ENV_TEMPLATE, FILE_CACHE_LIMITS, ShellUnavailableEnv;
5710
5923
  var init_workspace_files = __esm(() => {
5924
+ init_windows_shell();
5711
5925
  BROWSE_LIMITS = { children: 1000, readBytes: 512 * 1024 };
5712
5926
  ENV_TEMPLATE = /\.(example|sample|template)$/;
5713
5927
  FILE_CACHE_LIMITS = {
@@ -5722,6 +5936,18 @@ var init_workspace_files = __esm(() => {
5722
5936
  idleMs: 300000,
5723
5937
  concurrent: 2
5724
5938
  };
5939
+ ShellUnavailableEnv = class ShellUnavailableEnv extends LocalExecutionEnv {
5940
+ #message;
5941
+ constructor(options, message) {
5942
+ super(options);
5943
+ this.#message = message;
5944
+ }
5945
+ async exec(command, options, context) {
5946
+ if (typeof command === "string")
5947
+ return err(new ExecutionError("shell_unavailable", this.#message));
5948
+ return super.exec(command, options, context);
5949
+ }
5950
+ };
5725
5951
  });
5726
5952
 
5727
5953
  // packages/core/src/runtime.ts
@@ -5807,7 +6033,8 @@ async function createNekoRuntime(options = {}) {
5807
6033
  }));
5808
6034
  return modelInfoCache;
5809
6035
  };
5810
- const workspaceFiles = new WorkspaceFiles;
6036
+ const shellResolver = options.shellResolver ?? (process.platform === "win32" ? createWindowsShellResolver({ configPath: options.shellPath }) : undefined);
6037
+ const workspaceFiles = new WorkspaceFiles(undefined, shellResolver);
5811
6038
  const storage = options.storage === "memory" ? new MemoryStorage : await openNodeSqliteStorage(options.storagePath ?? DEFAULT_STORAGE_PATH);
5812
6039
  const registry = createRegistry();
5813
6040
  registry.install(createCodingTools({ images: await createNodePhotonImages() }));
@@ -6317,6 +6544,10 @@ async function createNekoRuntime(options = {}) {
6317
6544
  questions,
6318
6545
  closed: harness.closed,
6319
6546
  workspaceFiles,
6547
+ shellStatus(call) {
6548
+ const status = workspaceFiles.shellStatus(call);
6549
+ return status === undefined ? undefined : summarizeWindowsShell(status);
6550
+ },
6320
6551
  conversation,
6321
6552
  root: rootSession,
6322
6553
  submit: rootSession.submit,
@@ -6584,6 +6815,7 @@ var init_runtime = __esm(() => {
6584
6815
  init_deepseek();
6585
6816
  init_web_search();
6586
6817
  init_tavily();
6818
+ init_windows_shell();
6587
6819
  init_workspace();
6588
6820
  init_workspace_files();
6589
6821
  DEFAULT_MODEL = { provider: "deepseek", modelId: "deepseek-flash" };
@@ -6670,6 +6902,7 @@ var init_src = __esm(() => {
6670
6902
  init_projection();
6671
6903
  init_frontmatter();
6672
6904
  init_queued();
6905
+ init_rename_with_retry();
6673
6906
  init_runtime();
6674
6907
  init_search_providers();
6675
6908
  init_session_metadata();
@@ -6678,6 +6911,7 @@ var init_src = __esm(() => {
6678
6911
  init_todos();
6679
6912
  init_tool_approvals();
6680
6913
  init_user_questions();
6914
+ init_windows_shell();
6681
6915
  init_workspace();
6682
6916
  init_workspace_files();
6683
6917
  });
@@ -6803,7 +7037,7 @@ function contract(name, def) {
6803
7037
  ]));
6804
7038
  return remote(name, { ...def, host: hostNamed(GATEWAY_HOST), variants });
6805
7039
  }
6806
- var GATEWAY_HOST = "neko", rpc = (args, result) => [args, result], str, num, bool, nil, integer, nullableString, phase, level, thinking, model2, change, usage, image, skill, call, DelegationSchema, SubagentSummarySchema, jobStatus, JobSummarySchema, JobOutputSchema, QueuedInputSchema, WithdrawResultSchema, SteerResultSchema, TodoItemSchema, TodoListSchema, QuestionOptionSchema, questionStatus, QuestionSummarySchema, WorkspaceEntrySchema, WorkspaceDirSchema, WorkspaceFileSchema, ChangeSummarySchema, ChangedFileSchema, ChangeListSchema, ChangeOpSchema, ChangeDetailSchema, QuestionListSchema, QuestionRecordSchema, QuestionResponseSchema, ToolInteractionSchema, ChatViewSchema, SessionSummarySchema, DirectoryStateSchema, WorkspaceSummarySchema, WorkspaceStateSchema, credentialStatus, CredentialStateSchema, AttachmentSchema, chatInput, NekoSessionDirectory, NekoSessionManagement, resize, inputLimits, modelInfo, NekoModels, imageChunk, NekoSubagent, NekoChat, browse, page, NekoWorkspaces, provider, NekoCredentials, SearchSettingsStateSchema, NekoSearchSettings, contextSource, ContextSettingsStateSchema, nullable = (schema) => Type7.Union([schema, nil]), ContextSettingsPatchSchema, NekoContextSettings, defaultModelSource, DefaultModelStateSchema, NekoDefaultModel, FileCandidatesSchema, NekoWorkspaceFiles, marketSkill, preview, scope, installResult, NekoSkillMarket, ApplicationInfoSchema, NekoApplicationInfo;
7040
+ var GATEWAY_HOST = "neko", rpc = (args, result) => [args, result], str, num, bool, nil, integer, nullableString, phase, level, thinking, model2, change, usage, image, skill, call, DelegationSchema, SubagentSummarySchema, jobStatus, JobSummarySchema, JobOutputSchema, QueuedInputSchema, WithdrawResultSchema, SteerResultSchema, TodoItemSchema, TodoListSchema, QuestionOptionSchema, questionStatus, QuestionSummarySchema, WorkspaceEntrySchema, WorkspaceDirSchema, WorkspaceFileSchema, ChangeSummarySchema, ChangedFileSchema, ChangeListSchema, ChangeOpSchema, ChangeDetailSchema, QuestionListSchema, QuestionRecordSchema, QuestionResponseSchema, ToolInteractionSchema, ChatViewSchema, SessionSummarySchema, DirectoryStateSchema, WorkspaceSummarySchema, WorkspaceStateSchema, credentialStatus, CredentialStateSchema, AttachmentSchema, chatInput, NekoSessionDirectory, NekoSessionManagement, resize, inputLimits, modelInfo, NekoModels, imageChunk, NekoSubagent, NekoChat, browse, page, NekoWorkspaces, provider, NekoCredentials, SearchSettingsStateSchema, NekoSearchSettings, contextSource, ContextSettingsStateSchema, nullable = (schema) => Type7.Union([schema, nil]), ContextSettingsPatchSchema, NekoContextSettings, defaultModelSource, DefaultModelStateSchema, NekoDefaultModel, FileCandidatesSchema, NekoWorkspaceFiles, marketSkill, preview, scope, installResult, NekoSkillMarket, ShellStatusSchema, ApplicationInfoSchema, NekoApplicationInfo;
6807
7041
  var init_contract = __esm(() => {
6808
7042
  init_interactions();
6809
7043
  str = Type7.String();
@@ -7454,6 +7688,14 @@ var init_contract = __esm(() => {
7454
7688
  }
7455
7689
  }
7456
7690
  });
7691
+ ShellStatusSchema = Type7.Union([
7692
+ Type7.Object({ status: Type7.Literal("ready"), path: str, source: Type7.Enum(["config", "auto"]) }),
7693
+ Type7.Object({
7694
+ status: Type7.Literal("missing"),
7695
+ reason: Type7.Enum(["not_found", "invalid_config"]),
7696
+ detail: Type7.Optional(str)
7697
+ })
7698
+ ]);
7457
7699
  ApplicationInfoSchema = Type7.Object({
7458
7700
  runningVersion: str,
7459
7701
  installedVersion: nullableString,
@@ -7461,7 +7703,8 @@ var init_contract = __esm(() => {
7461
7703
  latestVersion: nullableString,
7462
7704
  checkedAt: Type7.Union([integer, nil]),
7463
7705
  checkState: Type7.Enum(["unknown", "checking", "ok", "failed", "development"]),
7464
- serverId: str
7706
+ serverId: str,
7707
+ shell: Type7.Optional(ShellStatusSchema)
7465
7708
  });
7466
7709
  NekoApplicationInfo = contract("neko.application-info", {
7467
7710
  version: 1,
@@ -22292,10 +22535,151 @@ function followTerminalTheme(ui, apply) {
22292
22535
  }
22293
22536
  var THEME_QUERY_TIMEOUT_MS = 100;
22294
22537
 
22538
+ // packages/tui/src/image-input.ts
22539
+ import { execFile as execFile2 } from "child_process";
22540
+ import { readFileSync as readFileSync2 } from "fs";
22541
+ import { readFile as readFile6, stat as stat4 } from "fs/promises";
22542
+ import { homedir as homedir6 } from "os";
22543
+ import { basename as basename5, join as join8, resolve as resolve5 } from "path";
22544
+ import { promisify as promisify2 } from "util";
22545
+ import { isKeyRelease as isKeyRelease4, isKeyRepeat as isKeyRepeat2, matchesKey as matchesKey8 } from "@earendil-works/pi-tui";
22546
+ function imagePaths(text, platform = process.platform) {
22547
+ const paths = [];
22548
+ let word = "", quote = "", escaped = false;
22549
+ for (const c of text.trim()) {
22550
+ if (escaped) {
22551
+ word += c;
22552
+ escaped = false;
22553
+ } else if (c === "\\" && quote !== "'" && platform !== "win32") {
22554
+ escaped = true;
22555
+ } else if (quote) {
22556
+ if (c === quote)
22557
+ quote = "";
22558
+ else
22559
+ word += c;
22560
+ } else if (c === '"' || c === "'")
22561
+ quote = c;
22562
+ else if (/\s/.test(c)) {
22563
+ if (word) {
22564
+ paths.push(word);
22565
+ word = "";
22566
+ }
22567
+ } else
22568
+ word += c;
22569
+ }
22570
+ if (quote || escaped)
22571
+ return null;
22572
+ if (word)
22573
+ paths.push(word);
22574
+ return paths.length && paths.every((p) => /\.(png|jpe?g|webp)$/i.test(p)) ? paths : null;
22575
+ }
22576
+ function expandHome(path, platform = process.platform) {
22577
+ if (path.startsWith("~/") || platform === "win32" && path.startsWith("~\\"))
22578
+ return join8(homedir6(), path.slice(2));
22579
+ return path;
22580
+ }
22581
+ async function readImageFile(path, cwd, platform = process.platform) {
22582
+ const full = resolve5(cwd, expandHome(path, platform));
22583
+ const info = await stat4(full);
22584
+ if (!info.isFile() || info.size > 20 * 1024 * 1024)
22585
+ throw new Error("Image must be a file no larger than 20 MiB");
22586
+ const bytes = await readFile6(full);
22587
+ const after = await stat4(full);
22588
+ if (bytes.length !== info.size || after.size !== info.size || after.mtimeMs !== info.mtimeMs)
22589
+ throw new Error("Image changed while reading");
22590
+ const mimeType = /\.png$/i.test(full) ? "image/png" : /\.webp$/i.test(full) ? "image/webp" : "image/jpeg";
22591
+ return { name: basename5(full), mimeType, bytes };
22592
+ }
22593
+ async function command(file, args, encoding = "utf8", timeout = 2000) {
22594
+ return (await exec(file, args, { timeout, maxBuffer: 30 * 1024 * 1024, encoding })).stdout;
22595
+ }
22596
+ async function clipboard(options = {}) {
22597
+ const run = options.command ?? command;
22598
+ const platform = options.platform ?? process.platform;
22599
+ if (platform === "darwin") {
22600
+ const files = await run("osascript", [
22601
+ "-e",
22602
+ `try
22603
+ set fs to the clipboard as \xABclass furl\xBB
22604
+ return POSIX path of fs
22605
+ on error
22606
+ return ""
22607
+ end try`
22608
+ ]).catch(() => "");
22609
+ if (String(files).trim())
22610
+ return /\.(png|jpe?g|webp)$/i.test(String(files).trim()) ? { paths: [String(files).trim()] } : { text: String(files).trim() };
22611
+ const image = String(await run("osascript", ["-e", `try
22612
+ return the clipboard as \xABclass PNGf\xBB
22613
+ on error
22614
+ return ""
22615
+ end try`]).catch(() => ""));
22616
+ const hex = /\u00ABdata PNGf([0-9A-Fa-f]+)\u00BB/.exec(image)?.[1];
22617
+ if (hex)
22618
+ return { image: { name: "Clipboard.png", mimeType: "image/png", bytes: Buffer.from(hex, "hex") } };
22619
+ return { text: String(await run("pbpaste", [])) };
22620
+ }
22621
+ if (platform === "win32")
22622
+ return windowsClipboard(run);
22623
+ let windowsError;
22624
+ if (platform === "linux" && (options.wsl ?? runningInWsl())) {
22625
+ try {
22626
+ return await windowsClipboard(run);
22627
+ } catch (error) {
22628
+ windowsError = error;
22629
+ }
22630
+ }
22631
+ const wayland = options.wayland ?? !!process.env.WAYLAND_DISPLAY;
22632
+ const tool = wayland ? "wl-paste" : "xclip";
22633
+ try {
22634
+ const bytes = await run(tool, wayland ? ["--type", "image/png"] : ["-selection", "clipboard", "-t", "image/png", "-o"], "buffer");
22635
+ if (bytes.length)
22636
+ return { image: { name: "Clipboard.png", mimeType: "image/png", bytes: Buffer.from(bytes) } };
22637
+ } catch {}
22638
+ try {
22639
+ return { text: String(await run(tool, wayland ? ["--no-newline"] : ["-selection", "clipboard", "-o"])) };
22640
+ } catch (error) {
22641
+ throw windowsError ?? error;
22642
+ }
22643
+ }
22644
+ async function windowsClipboard(run) {
22645
+ const value = String(await run("powershell.exe", ["-NoProfile", "-STA", "-Command", WINDOWS_CLIPBOARD_SCRIPT], "utf8", 8000));
22646
+ if (value.startsWith("IMAGE:"))
22647
+ return {
22648
+ image: { name: "Clipboard.png", mimeType: "image/png", bytes: Buffer.from(value.slice(6).trim(), "base64") }
22649
+ };
22650
+ return { text: value.replace(/\r?\n$/, "") };
22651
+ }
22652
+ function runningInWsl() {
22653
+ wslHost ??= process.platform === "linux" && (Boolean(process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP || process.env.WSLENV) || /microsoft|wsl/i.test(readFileOrEmpty("/proc/version")));
22654
+ return wslHost;
22655
+ }
22656
+ function readFileOrEmpty(path) {
22657
+ try {
22658
+ return readFileSync2(path, "utf8");
22659
+ } catch {
22660
+ return "";
22661
+ }
22662
+ }
22663
+ function clipboardShortcutKey(platform = process.platform, wsl = runningInWsl()) {
22664
+ return platform === "win32" || platform === "linux" && wsl ? "alt+v" : "ctrl+v";
22665
+ }
22666
+ function isClipboardShortcut(data, platform = process.platform, wsl = runningInWsl()) {
22667
+ return !isKeyRelease4(data) && !isKeyRepeat2(data) && matchesKey8(data, clipboardShortcutKey(platform, wsl));
22668
+ }
22669
+ var exec, WINDOWS_CLIPBOARD_SCRIPT, wslHost;
22670
+ var init_image_input = __esm(() => {
22671
+ exec = promisify2(execFile2);
22672
+ WINDOWS_CLIPBOARD_SCRIPT = [
22673
+ "try{[Console]::OutputEncoding=[Text.UTF8Encoding]::new($false)}catch{}",
22674
+ "Add-Type -AssemblyName System.Windows.Forms",
22675
+ "$i=[Windows.Forms.Clipboard]::GetImage()",
22676
+ 'if($i){$m=New-Object IO.MemoryStream; $i.Save($m,[Drawing.Imaging.ImageFormat]::Png); "IMAGE:"+[Convert]::ToBase64String($m.ToArray());$i.Dispose();$m.Dispose()}else{Get-Clipboard -Raw}'
22677
+ ].join("; ");
22678
+ });
22679
+
22295
22680
  // packages/tui/src/export-command.ts
22296
22681
  import { open as open3, unlink as unlink2 } from "fs/promises";
22297
- import { homedir as homedir6 } from "os";
22298
- import { resolve as resolve5 } from "path";
22682
+ import { resolve as resolve6 } from "path";
22299
22683
  function parseExportArgs(input) {
22300
22684
  const words = [];
22301
22685
  let word = "";
@@ -22345,7 +22729,7 @@ async function exportToFile(input) {
22345
22729
  const format = input.path === undefined ? "markdown" : exportFormatFromPath(input.path);
22346
22730
  if (format === null)
22347
22731
  throw new Error("Unsupported export format: use a .md, .html or .json path");
22348
- const target = resolve5(input.cwd, input.path === undefined ? exportFileName(input.source.title, format, input.source.exportedAt) : input.path.startsWith("~/") ? homedir6() + input.path.slice(1) : input.path);
22732
+ const target = resolve6(input.cwd, input.path === undefined ? exportFileName(input.source.title, format, input.source.exportedAt) : expandHome(input.path, input.platform));
22349
22733
  let handle;
22350
22734
  try {
22351
22735
  handle = await open3(target, "wx");
@@ -22374,6 +22758,7 @@ async function exportToFile(input) {
22374
22758
  var EXPORT_USAGE = "Usage: /export [path.md|path.html|path.json] [--thinking]";
22375
22759
  var init_export_command = __esm(() => {
22376
22760
  init_conversation_export();
22761
+ init_image_input();
22377
22762
  });
22378
22763
 
22379
22764
  // packages/core/src/file-mentions.ts
@@ -22683,7 +23068,11 @@ var init_ws_listener = __esm(() => {
22683
23068
  async function connectNekoGateway(address, signal) {
22684
23069
  signal?.throwIfAborted();
22685
23070
  const link = address.transport === "unix" ? await createUnixLink(address.socketPath, signal) : createWsLink(address.url, signal);
22686
- const connection = createGatewayConnection(link, address.serverId);
23071
+ return connectNekoGatewayLink(link, address.serverId, signal);
23072
+ }
23073
+ async function connectNekoGatewayLink(link, serverId, signal) {
23074
+ signal?.throwIfAborted();
23075
+ const connection = createGatewayConnection(link, serverId);
22687
23076
  const controller = new AbortController;
22688
23077
  const abort = () => controller.abort(signal?.reason);
22689
23078
  signal?.addEventListener("abort", abort, { once: true });
@@ -22732,8 +23121,8 @@ var init_principal = __esm(() => {
22732
23121
 
22733
23122
  // packages/gateway/src/mobile/store.ts
22734
23123
  import { randomBytes } from "crypto";
22735
- import { chmod as chmod2, mkdir as mkdir5, readFile as readFile6, rename as rename3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
22736
- import { join as join8 } from "path";
23124
+ import { chmod as chmod2, mkdir as mkdir5, readFile as readFile7, rm as rm3, writeFile as writeFile3 } from "fs/promises";
23125
+ import { join as join9 } from "path";
22737
23126
  function defaultState() {
22738
23127
  return {
22739
23128
  formatVersion: MOBILE_STATE_FORMAT_VERSION,
@@ -22853,9 +23242,10 @@ async function writeFileAtomic(filePath, contents, mode) {
22853
23242
  await writeFile3(tempPath, contents, { mode });
22854
23243
  if (POSIX)
22855
23244
  await chmod2(tempPath, mode);
22856
- await rename3(tempPath, filePath);
22857
- } finally {
23245
+ await renameWithRetry(tempPath, filePath);
23246
+ } catch (error) {
22858
23247
  await rm3(tempPath, { force: true });
23248
+ throw error;
22859
23249
  }
22860
23250
  }
22861
23251
  async function writeSecretFile(filePath, contents) {
@@ -22863,7 +23253,7 @@ async function writeSecretFile(filePath, contents) {
22863
23253
  }
22864
23254
  async function readSecretFile(filePath) {
22865
23255
  try {
22866
- return await readFile6(filePath, "utf8");
23256
+ return await readFile7(filePath, "utf8");
22867
23257
  } catch (error) {
22868
23258
  if (isNotFound(error))
22869
23259
  return;
@@ -22873,7 +23263,7 @@ async function readSecretFile(filePath) {
22873
23263
  async function loadState(statePath) {
22874
23264
  let raw;
22875
23265
  try {
22876
- raw = await readFile6(statePath, "utf8");
23266
+ raw = await readFile7(statePath, "utf8");
22877
23267
  } catch (error) {
22878
23268
  if (isNotFound(error))
22879
23269
  return freezeState(defaultState());
@@ -22897,21 +23287,21 @@ class MobileStore {
22897
23287
  queue = Promise.resolve();
22898
23288
  constructor(mobileDir, initial) {
22899
23289
  this.mobileDir = mobileDir;
22900
- this.tlsDir = join8(mobileDir, "tls");
22901
- this.statePath = join8(mobileDir, "state.json");
22902
- this.adminTokenPath = join8(mobileDir, "admin-token");
23290
+ this.tlsDir = join9(mobileDir, "tls");
23291
+ this.statePath = join9(mobileDir, "state.json");
23292
+ this.adminTokenPath = join9(mobileDir, "admin-token");
22903
23293
  this.current = initial;
22904
23294
  }
22905
23295
  static async open(nekoDir) {
22906
- const mobileDir = join8(nekoDir, "mobile");
22907
- const tlsDir = join8(mobileDir, "tls");
23296
+ const mobileDir = join9(nekoDir, "mobile");
23297
+ const tlsDir = join9(mobileDir, "tls");
22908
23298
  await mkdir5(mobileDir, { recursive: true, mode: 448 });
22909
23299
  await mkdir5(tlsDir, { recursive: true, mode: 448 });
22910
23300
  if (POSIX) {
22911
23301
  await chmod2(mobileDir, 448);
22912
23302
  await chmod2(tlsDir, 448);
22913
23303
  }
22914
- return new MobileStore(mobileDir, await loadState(join8(mobileDir, "state.json")));
23304
+ return new MobileStore(mobileDir, await loadState(join9(mobileDir, "state.json")));
22915
23305
  }
22916
23306
  getState() {
22917
23307
  return this.current;
@@ -23033,6 +23423,7 @@ class MobileStore {
23033
23423
  }
23034
23424
  var MOBILE_STATE_FORMAT_VERSION = 1, MOBILE_DEFAULT_PORT = 8323, MOBILE_ADMIN_TOKEN_BYTES = 32, POSIX, ADMIN_TOKEN_PATTERN, CREDENTIAL_HASH_PATTERN, tempCounter = 0;
23035
23425
  var init_store = __esm(() => {
23426
+ init_src();
23036
23427
  POSIX = process.platform !== "win32";
23037
23428
  ADMIN_TOKEN_PATTERN = /^[A-Za-z0-9_-]{43}$/;
23038
23429
  CREDENTIAL_HASH_PATTERN = /^[0-9a-fA-F]{64}$/;
@@ -41475,7 +41866,7 @@ import {
41475
41866
  randomBytes as randomBytes2,
41476
41867
  X509Certificate
41477
41868
  } from "crypto";
41478
- import { join as join9 } from "path";
41869
+ import { join as join10 } from "path";
41479
41870
  function mobileCommonName(serverId) {
41480
41871
  return `neko-${serverId.slice(0, 8).toLowerCase()}.invalid`;
41481
41872
  }
@@ -41567,8 +41958,8 @@ class MobileCertificate {
41567
41958
  this.current = initial;
41568
41959
  }
41569
41960
  static async load(store, serverId) {
41570
- const certPath = join9(store.tlsDir, TLS_CERT_FILE);
41571
- const keyPath = join9(store.tlsDir, TLS_KEY_FILE);
41961
+ const certPath = join10(store.tlsDir, TLS_CERT_FILE);
41962
+ const keyPath = join10(store.tlsDir, TLS_KEY_FILE);
41572
41963
  const certPem = await readSecretFile(certPath);
41573
41964
  const keyPem = await readSecretFile(keyPath);
41574
41965
  if (certPem === undefined && keyPem === undefined) {
@@ -42098,10 +42489,10 @@ var init_relay = __esm(() => {
42098
42489
  });
42099
42490
 
42100
42491
  // packages/gateway/src/mobile/wsl.ts
42101
- import { execFile as execFile2 } from "child_process";
42102
- import { accessSync as accessSync2, constants as constants2, readFileSync as readFileSync2 } from "fs";
42492
+ import { execFile as execFile3 } from "child_process";
42493
+ import { accessSync as accessSync2, constants as constants2, readFileSync as readFileSync3 } from "fs";
42103
42494
  import { networkInterfaces as networkInterfaces2 } from "os";
42104
- import { delimiter, join as join10 } from "path";
42495
+ import { delimiter, join as join11 } from "path";
42105
42496
  function detectWsl(overrides = {}) {
42106
42497
  const deps = { ...defaultProbeDeps, ...overrides };
42107
42498
  if (deps.platform !== "linux")
@@ -42112,7 +42503,7 @@ function detectWsl(overrides = {}) {
42112
42503
  if (!inWsl)
42113
42504
  return;
42114
42505
  const candidates = [
42115
- ...(env.PATH ?? "").split(delimiter).filter((dir) => dir !== "").map((dir) => join10(dir, "powershell.exe")),
42506
+ ...(env.PATH ?? "").split(delimiter).filter((dir) => dir !== "").map((dir) => join11(dir, "powershell.exe")),
42116
42507
  POWERSHELL_FALLBACK
42117
42508
  ];
42118
42509
  return { powershell: candidates.find((candidate) => deps.isExecutable(candidate)) };
@@ -42201,7 +42592,7 @@ function primaryWslAddress(interfaces = networkInterfaces2()) {
42201
42592
  return eth0?.address ?? wslOwnAddresses(interfaces)[0];
42202
42593
  }
42203
42594
  var POWERSHELL_FALLBACK = "/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe", defaultProbeDeps, WINDOWS_ADDRESSES_SCRIPT, WSL_ADAPTER_PATTERN, runPowerShell = (powershell, script, timeoutMs) => new Promise((resolve, reject) => {
42204
- execFile2(powershell, ["-NoProfile", "-NonInteractive", "-Command", script], { timeout: timeoutMs, windowsHide: true, maxBuffer: 1024 * 1024, encoding: "utf8" }, (error, stdout) => error ? reject(error) : resolve(stdout));
42595
+ execFile3(powershell, ["-NoProfile", "-NonInteractive", "-Command", script], { timeout: timeoutMs, windowsHide: true, maxBuffer: 1024 * 1024, encoding: "utf8" }, (error, stdout) => error ? reject(error) : resolve(stdout));
42205
42596
  });
42206
42597
  var init_wsl = __esm(() => {
42207
42598
  init_network();
@@ -42210,7 +42601,7 @@ var init_wsl = __esm(() => {
42210
42601
  env: process.env,
42211
42602
  readProcVersion() {
42212
42603
  try {
42213
- return readFileSync2("/proc/version", "utf8");
42604
+ return readFileSync3("/proc/version", "utf8");
42214
42605
  } catch {
42215
42606
  return;
42216
42607
  }
@@ -42282,6 +42673,10 @@ function toMessage(error) {
42282
42673
  }
42283
42674
  return String(error);
42284
42675
  }
42676
+ function normalizeLocalAddress(address) {
42677
+ const mapped = address?.startsWith("::ffff:") ? address.slice("::ffff:".length) : undefined;
42678
+ return mapped !== undefined && isIpv4Literal(mapped) ? mapped : address;
42679
+ }
42285
42680
 
42286
42681
  class MobileGateway {
42287
42682
  serverId;
@@ -42610,11 +43005,12 @@ class MobileGateway {
42610
43005
  connection.sendNotification(method, params);
42611
43006
  }
42612
43007
  advertisedAddress(localAddress) {
43008
+ const normalized = normalizeLocalAddress(localAddress);
42613
43009
  const state = this.stateValue;
42614
- if (state.status === "listening" && state.mode === "wsl-relay" && localAddress === state.bindAddress) {
43010
+ if (state.status === "listening" && state.mode === "wsl-relay" && normalized === state.bindAddress) {
42615
43011
  return state.address;
42616
43012
  }
42617
- return localAddress;
43013
+ return normalized;
42618
43014
  }
42619
43015
  requireStore() {
42620
43016
  if (this.storeHandle === undefined)
@@ -42872,10 +43268,10 @@ var init_listener = __esm(() => {
42872
43268
  });
42873
43269
 
42874
43270
  // packages/gateway/src/port-file.ts
42875
- import { readFile as readFile7, rm as rm4, writeFile as writeFile4 } from "fs/promises";
42876
- import { join as join11 } from "path";
43271
+ import { readFile as readFile8, rm as rm4, writeFile as writeFile4 } from "fs/promises";
43272
+ import { join as join12 } from "path";
42877
43273
  function gatewayPortFilePath(nekoDir, serverId) {
42878
- return join11(nekoDir, `${serverId}.port`);
43274
+ return join12(nekoDir, `${serverId}.port`);
42879
43275
  }
42880
43276
  async function writeGatewayPortFile(nekoDir, serverId, wsUrl) {
42881
43277
  await writeFile4(gatewayPortFilePath(nekoDir, serverId), `${wsUrl}
@@ -42884,7 +43280,7 @@ async function writeGatewayPortFile(nekoDir, serverId, wsUrl) {
42884
43280
  async function readGatewayPortFile(nekoDir, serverId) {
42885
43281
  let content;
42886
43282
  try {
42887
- content = await readFile7(gatewayPortFilePath(nekoDir, serverId), "utf8");
43283
+ content = await readFile8(gatewayPortFilePath(nekoDir, serverId), "utf8");
42888
43284
  } catch {
42889
43285
  return;
42890
43286
  }
@@ -43231,7 +43627,7 @@ var init_subagent_host = __esm(() => {
43231
43627
 
43232
43628
  // packages/gateway/src/session-host.ts
43233
43629
  import { open as open4 } from "fs/promises";
43234
- import { basename as basename5, extname } from "path";
43630
+ import { basename as basename6, extname } from "path";
43235
43631
  import {
43236
43632
  ChordError as ChordError4,
43237
43633
  local as local2,
@@ -43426,7 +43822,7 @@ function serveSessions(host, runtime, context) {
43426
43822
  if (!info.isFile())
43427
43823
  throw new Error("Image must be a file");
43428
43824
  ctx.signal?.throwIfAborted();
43429
- id = uploads.value.begin({ name: basename5(target), mimeType, size: info.size });
43825
+ id = uploads.value.begin({ name: basename6(target), mimeType, size: info.size });
43430
43826
  let offset = 0;
43431
43827
  while (offset < info.size) {
43432
43828
  ctx.signal?.throwIfAborted();
@@ -43443,9 +43839,9 @@ function serveSessions(host, runtime, context) {
43443
43839
  ctx.signal?.throwIfAborted();
43444
43840
  uploads.value.finish(id);
43445
43841
  const raw = uploads.value.resolve([id])[0];
43446
- await prepareImage(Buffer.from(raw.content.data, "base64"), mimeType, basename5(target), model);
43842
+ await prepareImage(Buffer.from(raw.content.data, "base64"), mimeType, basename6(target), model);
43447
43843
  ctx.signal?.throwIfAborted();
43448
- return { id, name: basename5(target) };
43844
+ return { id, name: basename6(target) };
43449
43845
  } catch (error) {
43450
43846
  if (id)
43451
43847
  uploads.value.release(id);
@@ -43639,7 +44035,7 @@ function strFromU8(dat, latin1) {
43639
44035
  } else {
43640
44036
  var _a = dutf8(dat), { s, r } = _a;
43641
44037
  if (r.length)
43642
- err(8);
44038
+ err2(8);
43643
44039
  return s;
43644
44040
  }
43645
44041
  }
@@ -43648,7 +44044,7 @@ function unzipSync(data, opts) {
43648
44044
  var e = data.length - 22;
43649
44045
  for (;b4(data, e) != 101010256; --e) {
43650
44046
  if (!e || data.length - e > 65558)
43651
- err(13);
44047
+ err2(13);
43652
44048
  }
43653
44049
  var c = b2(data, e + 8);
43654
44050
  if (!c)
@@ -43678,7 +44074,7 @@ function unzipSync(data, opts) {
43678
44074
  else if (c_2 == 8)
43679
44075
  files[fn] = inflateSync(data.subarray(b, b + sc), { out: new u8(su) });
43680
44076
  else
43681
- err(14, "unknown compression type " + c_2);
44077
+ err2(14, "unknown compression type " + c_2);
43682
44078
  }
43683
44079
  }
43684
44080
  return files;
@@ -43751,11 +44147,11 @@ var require2, _a, Worker2, isMarkedAsUntransferable, u8, u16, i32, fleb, fdeb, c
43751
44147
  if (e == null || e > v.length)
43752
44148
  e = v.length;
43753
44149
  return new u8(v.subarray(s, e));
43754
- }, ec, err = function(ind, msg, nt) {
44150
+ }, ec, err2 = function(ind, msg, nt) {
43755
44151
  var e = new Error(msg || ec[ind]);
43756
44152
  e.code = ind;
43757
44153
  if (Error.captureStackTrace)
43758
- Error.captureStackTrace(e, err);
44154
+ Error.captureStackTrace(e, err2);
43759
44155
  if (!nt)
43760
44156
  throw e;
43761
44157
  return e;
@@ -43787,7 +44183,7 @@ var require2, _a, Worker2, isMarkedAsUntransferable, u8, u16, i32, fleb, fdeb, c
43787
44183
  var s = shft(pos) + 4, l = dat[s - 4] | dat[s - 3] << 8, t = s + l;
43788
44184
  if (t > sl) {
43789
44185
  if (noSt)
43790
- err(0);
44186
+ err2(0);
43791
44187
  break;
43792
44188
  }
43793
44189
  if (resize)
@@ -43833,10 +44229,10 @@ var require2, _a, Worker2, isMarkedAsUntransferable, u8, u16, i32, fleb, fdeb, c
43833
44229
  lm = hMap(lt, lbt, 1);
43834
44230
  dm = hMap(dt, dbt, 1);
43835
44231
  } else
43836
- err(1);
44232
+ err2(1);
43837
44233
  if (pos > tbts) {
43838
44234
  if (noSt)
43839
- err(0);
44235
+ err2(0);
43840
44236
  break;
43841
44237
  }
43842
44238
  }
@@ -43849,11 +44245,11 @@ var require2, _a, Worker2, isMarkedAsUntransferable, u8, u16, i32, fleb, fdeb, c
43849
44245
  pos += c & 15;
43850
44246
  if (pos > tbts) {
43851
44247
  if (noSt)
43852
- err(0);
44248
+ err2(0);
43853
44249
  break;
43854
44250
  }
43855
44251
  if (!c)
43856
- err(2);
44252
+ err2(2);
43857
44253
  if (sym < 256)
43858
44254
  buf[bt++] = sym;
43859
44255
  else if (sym == 256) {
@@ -43868,7 +44264,7 @@ var require2, _a, Worker2, isMarkedAsUntransferable, u8, u16, i32, fleb, fdeb, c
43868
44264
  }
43869
44265
  var d = dm[bits16(dat, pos) & dms], dsym = d >> 4;
43870
44266
  if (!d)
43871
- err(3);
44267
+ err2(3);
43872
44268
  pos += d & 15;
43873
44269
  var dt = fd[dsym];
43874
44270
  if (dsym > 3) {
@@ -43877,7 +44273,7 @@ var require2, _a, Worker2, isMarkedAsUntransferable, u8, u16, i32, fleb, fdeb, c
43877
44273
  }
43878
44274
  if (pos > tbts) {
43879
44275
  if (noSt)
43880
- err(0);
44276
+ err2(0);
43881
44277
  break;
43882
44278
  }
43883
44279
  if (resize)
@@ -43886,7 +44282,7 @@ var require2, _a, Worker2, isMarkedAsUntransferable, u8, u16, i32, fleb, fdeb, c
43886
44282
  if (bt < dt) {
43887
44283
  var shift = dl - dt, dend = Math.min(dt, end);
43888
44284
  if (shift + bt < 0)
43889
- err(3);
44285
+ err2(3);
43890
44286
  for (;bt < dend; ++bt)
43891
44287
  buf[bt] = dict[shift + bt];
43892
44288
  }
@@ -43941,7 +44337,7 @@ var require2, _a, Worker2, isMarkedAsUntransferable, u8, u16, i32, fleb, fdeb, c
43941
44337
  }
43942
44338
  }
43943
44339
  if (z < 2)
43944
- err(13);
44340
+ err2(13);
43945
44341
  }
43946
44342
  return [sc, su, off, 0];
43947
44343
  };
@@ -44010,13 +44406,14 @@ var init_esm = __esm(() => {
44010
44406
 
44011
44407
  // packages/gateway/src/skill-market.ts
44012
44408
  import { createHash as createHash6, randomUUID as randomUUID4 } from "crypto";
44013
- import { mkdir as mkdir6, readdir as readdir2, rename as rename4, rm as rm5, stat as stat3, writeFile as writeFile5 } from "fs/promises";
44409
+ import { mkdir as mkdir6, readdir as readdir2, rm as rm5, stat as stat5, writeFile as writeFile5 } from "fs/promises";
44014
44410
  import { homedir as homedir7 } from "os";
44015
- import { dirname as dirname8, join as join12 } from "path";
44411
+ import { dirname as dirname8, join as join13 } from "path";
44016
44412
  function createSkillMarketClient(options) {
44017
44413
  const home = options.home ?? homedir7();
44018
44414
  const fetchImpl = options.fetchImpl ?? fetch;
44019
44415
  const now = options.now ?? Date.now;
44416
+ const placeRename = options.renameImpl;
44020
44417
  let base;
44021
44418
  try {
44022
44419
  base = new URL(options.marketUrl);
@@ -44121,6 +44518,8 @@ function createSkillMarketClient(options) {
44121
44518
  throw new SkillMarketError("market_unavailable", "Project-scope install needs a workspaceRoot");
44122
44519
  return projectSkillsDir(workspaceRoot);
44123
44520
  };
44521
+ const BACKUP_PREFIX = ".bak-";
44522
+ const UUID_LENGTH = 36;
44124
44523
  const sweepTempDirs = async (skillsDir) => {
44125
44524
  const parent = dirname8(skillsDir);
44126
44525
  let names = [];
@@ -44129,12 +44528,27 @@ function createSkillMarketClient(options) {
44129
44528
  } catch {
44130
44529
  return;
44131
44530
  }
44132
- await Promise.all(names.filter((n) => n.startsWith(".tmp-")).map((n) => rm5(join12(parent, n), { recursive: true, force: true }).catch(() => {})));
44531
+ await Promise.all(names.filter((n) => n.startsWith(".tmp-")).map((n) => rm5(join13(parent, n), { recursive: true, force: true }).catch(() => {})));
44532
+ for (const n of names.filter((n) => n.startsWith(BACKUP_PREFIX))) {
44533
+ const backup = join13(parent, n);
44534
+ const skill = n.slice(BACKUP_PREFIX.length + UUID_LENGTH + 1);
44535
+ if (skill === "" || skill.includes("/") || skill.includes("\\") || skill === "." || skill === "..")
44536
+ continue;
44537
+ const target = join13(skillsDir, skill);
44538
+ if (await stat5(target).catch(() => {
44539
+ return;
44540
+ }) === undefined) {
44541
+ await mkdir6(skillsDir, { recursive: true }).catch(() => {});
44542
+ await renameWithRetry(backup, target).catch(() => {});
44543
+ } else {
44544
+ await rm5(backup, { recursive: true, force: true }).catch(() => {});
44545
+ }
44546
+ }
44133
44547
  };
44134
44548
  const install = async ({ name, scope, workspaceRoot, overwrite }) => {
44135
44549
  const skillsDir = skillsDirFor(scope, workspaceRoot);
44136
- const target = join12(skillsDir, name);
44137
- const exists = await stat3(target).catch(() => {
44550
+ const target = join13(skillsDir, name);
44551
+ const exists = await stat5(target).catch(() => {
44138
44552
  return;
44139
44553
  }) !== undefined;
44140
44554
  if (exists && overwrite !== true)
@@ -44142,24 +44556,34 @@ function createSkillMarketClient(options) {
44142
44556
  const skill = await detail(name);
44143
44557
  const files = unpack(skill, await download(skill));
44144
44558
  await sweepTempDirs(skillsDir);
44145
- const temp = join12(dirname8(skillsDir), `.tmp-${randomUUID4()}`);
44559
+ const temp = join13(dirname8(skillsDir), `.tmp-${randomUUID4()}`);
44146
44560
  try {
44147
44561
  for (const [relative, bytes] of files) {
44148
- const dest = join12(temp, ...relative.split("/"));
44562
+ const dest = join13(temp, ...relative.split("/"));
44149
44563
  await mkdir6(dirname8(dest), { recursive: true });
44150
44564
  await writeFile5(dest, bytes);
44151
44565
  }
44152
- if (exists)
44153
- await rm5(target, { recursive: true, force: true });
44154
44566
  await mkdir6(skillsDir, { recursive: true });
44155
- await rename4(temp, target);
44567
+ if (exists) {
44568
+ const backup = join13(dirname8(skillsDir), `${BACKUP_PREFIX}${randomUUID4()}-${name}`);
44569
+ await renameWithRetry(target, backup);
44570
+ try {
44571
+ await renameWithRetry(temp, target, placeRename ? { rename: placeRename } : {});
44572
+ } catch (error) {
44573
+ await renameWithRetry(backup, target).catch(() => {});
44574
+ throw error;
44575
+ }
44576
+ await rm5(backup, { recursive: true, force: true }).catch(() => {});
44577
+ } else {
44578
+ await renameWithRetry(temp, target, placeRename ? { rename: placeRename } : {});
44579
+ }
44156
44580
  } finally {
44157
44581
  await rm5(temp, { recursive: true, force: true }).catch(() => {});
44158
44582
  }
44159
44583
  let shadowedBy;
44160
44584
  if (scope === "user" && workspaceRoot !== undefined && workspaceRoot !== "") {
44161
- const projectDir = join12(projectSkillsDir(workspaceRoot), name);
44162
- if (await stat3(projectDir).catch(() => {
44585
+ const projectDir = join13(projectSkillsDir(workspaceRoot), name);
44586
+ if (await stat5(projectDir).catch(() => {
44163
44587
  return;
44164
44588
  }) !== undefined)
44165
44589
  shadowedBy = "project";
@@ -44168,8 +44592,8 @@ function createSkillMarketClient(options) {
44168
44592
  };
44169
44593
  const uninstall = async ({ name, scope, workspaceRoot }) => {
44170
44594
  const skillsDir = skillsDirFor(scope, workspaceRoot);
44171
- const target = join12(skillsDir, name);
44172
- if (await stat3(target).catch(() => {
44595
+ const target = join13(skillsDir, name);
44596
+ if (await stat5(target).catch(() => {
44173
44597
  return;
44174
44598
  }) === undefined)
44175
44599
  throw new SkillMarketError("skill_not_installed", `Skill ${name} is not installed at ${scope} level`);
@@ -44200,7 +44624,7 @@ function createSkillMarketClient(options) {
44200
44624
  uninstall
44201
44625
  };
44202
44626
  }
44203
- var SkillMarketError, CATALOG_TTL_MS = 60000, ZIP_CACHE_MAX = 16, MAX_PACKAGE_BYTES, userSkillsDir2 = (home) => join12(home, ".agents", "skills"), projectSkillsDir = (workspaceRoot) => join12(workspaceRoot, ".agents", "skills"), activeLevelOf = (state) => state.project ? "project" : state.user ? "user" : state.builtin ? "builtin" : null, normalizeEntry = (raw) => {
44627
+ var SkillMarketError, CATALOG_TTL_MS = 60000, ZIP_CACHE_MAX = 16, MAX_PACKAGE_BYTES, userSkillsDir2 = (home) => join13(home, ".agents", "skills"), projectSkillsDir = (workspaceRoot) => join13(workspaceRoot, ".agents", "skills"), activeLevelOf = (state) => state.project ? "project" : state.user ? "user" : state.builtin ? "builtin" : null, normalizeEntry = (raw) => {
44204
44628
  const row = raw ?? {};
44205
44629
  const metadata = row.metadata;
44206
44630
  const source = metadata !== null && typeof metadata === "object" ? metadata.source : undefined;
@@ -44232,8 +44656,8 @@ var init_skill_market = __esm(() => {
44232
44656
 
44233
44657
  // packages/gateway/src/workspaces.ts
44234
44658
  import { constants as constants3 } from "fs";
44235
- import { access, readdir as readdir3, stat as stat4 } from "fs/promises";
44236
- import { dirname as dirname9, isAbsolute as isAbsolute4, join as join13 } from "path";
44659
+ import { access, readdir as readdir3, stat as stat6 } from "fs/promises";
44660
+ import { dirname as dirname9, isAbsolute as isAbsolute4, join as join14 } from "path";
44237
44661
  async function browseWorkspaces(options) {
44238
44662
  const requested = options.path.replace(/^\/+([A-Za-z]:[\\/])/, "$1");
44239
44663
  if (process.platform === "win32" && (requested === "/" || requested === "\\")) {
@@ -44260,11 +44684,11 @@ async function browseWorkspaces(options) {
44260
44684
  let next = offset;
44261
44685
  for (;next < entries.length && directories.length < 100; next++) {
44262
44686
  const entry = entries[next];
44263
- const target = join13(path, entry.name);
44687
+ const target = join14(path, entry.name);
44264
44688
  if (!entry.isDirectory() && !entry.isSymbolicLink())
44265
44689
  continue;
44266
44690
  try {
44267
- if (!(await stat4(target)).isDirectory())
44691
+ if (!(await stat6(target)).isDirectory())
44268
44692
  continue;
44269
44693
  await access(target, constants3.R_OK | constants3.X_OK);
44270
44694
  directories.push({ name: entry.name, path: target });
@@ -44303,12 +44727,15 @@ async function createNekoServerServices(options) {
44303
44727
  const updates = new UpdateChecker(options.updates);
44304
44728
  await updates.local();
44305
44729
  const updateListeners = new Set;
44306
- const info = (value) => ({ ...value, serverId: options.serverId });
44307
- let published = JSON.stringify(updates.state), closed = false;
44730
+ const info = (value) => {
44731
+ const shell = runtime.shellStatus();
44732
+ return { ...value, serverId: options.serverId, ...shell === undefined ? {} : { shell } };
44733
+ };
44734
+ let published = JSON.stringify(info(updates.state)), closed = false;
44308
44735
  const publishUpdates = () => {
44309
44736
  if (closed)
44310
44737
  return;
44311
- const next = JSON.stringify(updates.state);
44738
+ const next = JSON.stringify(info(updates.state));
44312
44739
  if (next === published)
44313
44740
  return;
44314
44741
  published = next;
@@ -44327,6 +44754,7 @@ async function createNekoServerServices(options) {
44327
44754
  variant: "ready",
44328
44755
  info: current,
44329
44756
  check: async () => {
44757
+ runtime.shellStatus({ refresh: true });
44330
44758
  await updates.check(true);
44331
44759
  publishUpdates();
44332
44760
  return info(updates.state);
@@ -44637,8 +45065,8 @@ var init_server_services = __esm(() => {
44637
45065
 
44638
45066
  // packages/gateway/src/gateway.ts
44639
45067
  import { randomUUID as randomUUID6 } from "crypto";
44640
- import { mkdir as mkdir7, readFile as readFile8, writeFile as writeFile6 } from "fs/promises";
44641
- import { basename as basename6, dirname as dirname10, join as join14 } from "path";
45068
+ import { mkdir as mkdir7, readFile as readFile9, writeFile as writeFile6 } from "fs/promises";
45069
+ import { basename as basename7, dirname as dirname10, join as join15 } from "path";
44642
45070
  import { createHost as createHost2 } from "@earendil-works/chord";
44643
45071
  import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT4 } from "@earendil-works/chord/context";
44644
45072
  async function startGateway(options = {}) {
@@ -44651,7 +45079,7 @@ async function startGateway(options = {}) {
44651
45079
  const marketUrl = options.marketUrl ?? fileConfig.skills?.marketUrl ?? DEFAULT_SKILL_MARKET_URL;
44652
45080
  const nekoDir = dirname10(storagePath);
44653
45081
  const serverId = options.serverId ?? await loadOrCreateServerId(nekoDir);
44654
- const socketPath = options.socketPath !== undefined ? options.socketPath : fileConfig.gateway?.socketPath !== undefined ? fileConfig.gateway.socketPath : process.platform === "win32" ? null : join14(nekoDir, `${serverId}.sock`);
45082
+ const socketPath = options.socketPath !== undefined ? options.socketPath : fileConfig.gateway?.socketPath !== undefined ? fileConfig.gateway.socketPath : process.platform === "win32" ? null : join15(nekoDir, `${serverId}.sock`);
44655
45083
  const runtime = await createNekoRuntime({
44656
45084
  context,
44657
45085
  onReport: (error) => options.onError?.(error instanceof Error ? error : new Error(String(error))),
@@ -44667,7 +45095,9 @@ async function startGateway(options = {}) {
44667
45095
  ...options.cwd !== undefined ? { cwd: options.cwd } : {},
44668
45096
  ...options.providers !== undefined ? { providers: options.providers } : {},
44669
45097
  ...options.extensions !== undefined ? { extensions: options.extensions } : {},
44670
- ...options.promptHome !== undefined ? { promptHome: options.promptHome } : {}
45098
+ ...options.promptHome !== undefined ? { promptHome: options.promptHome } : {},
45099
+ ...fileConfig.shell?.path !== undefined ? { shellPath: fileConfig.shell.path } : {},
45100
+ ...options.shellResolver !== undefined ? { shellResolver: options.shellResolver } : {}
44671
45101
  });
44672
45102
  try {
44673
45103
  return await bindGateway(runtime, {
@@ -44682,6 +45112,7 @@ async function startGateway(options = {}) {
44682
45112
  updates: options.updates,
44683
45113
  updateIntervalMs: options.updateIntervalMs,
44684
45114
  mobileServerName: options.mobileServerName,
45115
+ mobileNetwork: options.mobileNetwork,
44685
45116
  onError: options.onError
44686
45117
  });
44687
45118
  } catch (error) {
@@ -44716,12 +45147,13 @@ async function bindGateway(runtime, options) {
44716
45147
  wsListener = createWsListener({ host, port, onError: options.onError });
44717
45148
  listeners.push(wsListener);
44718
45149
  }
45150
+ const acceptLocal = (link) => chord.accept(link, {
45151
+ principal: { kind: "local", connectionId: randomUUID6() },
45152
+ onProtocolMismatch: ({ peer }) => options.onError?.(new Error(`Rejected a client on Chord protocol ${peer ?? "unknown"}; restart or update it.`))
45153
+ });
44719
45154
  try {
44720
45155
  for (const listener of listeners)
44721
- await listener.start((link) => chord.accept(link, {
44722
- principal: { kind: "local", connectionId: randomUUID6() },
44723
- onProtocolMismatch: ({ peer }) => options.onError?.(new Error(`Rejected a client on Chord protocol ${peer ?? "unknown"}; restart or update it.`))
44724
- }));
45156
+ await listener.start(acceptLocal);
44725
45157
  } catch (error) {
44726
45158
  await Promise.allSettled(listeners.map((listener) => listener.close()));
44727
45159
  await chord.close();
@@ -44737,6 +45169,7 @@ async function bindGateway(runtime, options) {
44737
45169
  serverId,
44738
45170
  serverName: options.mobileServerName,
44739
45171
  onError: options.onError,
45172
+ network: options.mobileNetwork,
44740
45173
  chord: {
44741
45174
  accept: (link, acceptOptions) => chord.accept(link, acceptOptions),
44742
45175
  invalidatePrincipal: (match) => chord.invalidatePrincipal(match)
@@ -44761,6 +45194,9 @@ async function bindGateway(runtime, options) {
44761
45194
  port: wsListener?.port,
44762
45195
  runtime,
44763
45196
  mobile,
45197
+ accept(link) {
45198
+ acceptLocal(link);
45199
+ },
44764
45200
  async close() {
44765
45201
  if (closing)
44766
45202
  return closing;
@@ -44801,16 +45237,16 @@ ${connectHint}`);
44801
45237
  return error instanceof Error ? error : new Error(message);
44802
45238
  }
44803
45239
  function serverIdFromSocketPath(socketPath) {
44804
- const name = basename6(socketPath);
45240
+ const name = basename7(socketPath);
44805
45241
  if (!name.endsWith(".sock"))
44806
45242
  return;
44807
45243
  const candidate = name.slice(0, -".sock".length);
44808
45244
  return isServerId(candidate) ? candidate : undefined;
44809
45245
  }
44810
45246
  async function loadOrCreateServerId(nekoDir) {
44811
- const file = join14(nekoDir, SERVER_ID_FILENAME);
45247
+ const file = join15(nekoDir, SERVER_ID_FILENAME);
44812
45248
  try {
44813
- const existing = (await readFile8(file, "utf8")).trim();
45249
+ const existing = (await readFile9(file, "utf8")).trim();
44814
45250
  if (existing !== "")
44815
45251
  return existing;
44816
45252
  } catch {}
@@ -44849,10 +45285,11 @@ function gatewayCommand(args) {
44849
45285
  async function stopGatewayProcess(child, options = {}) {
44850
45286
  if (child.pid === undefined || child.exitCode !== null || child.signalCode !== null)
44851
45287
  return;
45288
+ const viaStdin = options.viaStdin === true && child.stdin != null;
44852
45289
  await new Promise((resolve) => {
44853
45290
  let forcedTimer;
44854
45291
  const timer = setTimeout(() => {
44855
- child.kill("SIGKILL");
45292
+ child.kill(viaStdin ? undefined : "SIGKILL");
44856
45293
  forcedTimer = setTimeout(resolve, options.forcedMs ?? FORCED_STOP_MS);
44857
45294
  }, options.gracefulMs ?? GRACEFUL_STOP_MS);
44858
45295
  child.once("exit", () => {
@@ -44860,7 +45297,10 @@ async function stopGatewayProcess(child, options = {}) {
44860
45297
  clearTimeout(forcedTimer);
44861
45298
  resolve();
44862
45299
  });
44863
- child.kill("SIGTERM");
45300
+ if (viaStdin)
45301
+ child.stdin?.end();
45302
+ else
45303
+ child.kill("SIGTERM");
44864
45304
  });
44865
45305
  }
44866
45306
  var GRACEFUL_STOP_MS = 3000, FORCED_STOP_MS = 2000;
@@ -44890,14 +45330,15 @@ function installShutdown(options = {}) {
44890
45330
  exit(1);
44891
45331
  });
44892
45332
  };
44893
- const onSignal = () => {
45333
+ const begin = (reason) => {
44894
45334
  if (controller.signal.aborted) {
44895
45335
  options.log?.("\u518D\u6B21\u6536\u5230\u7EC8\u6B62\u4FE1\u53F7\uFF0C\u5F3A\u5236\u9000\u51FA\u3002");
44896
45336
  exit(130);
44897
45337
  return;
44898
45338
  }
44899
- options.log?.(`
44900
- \u6B63\u5728\u5173\u95ED\u2026`);
45339
+ options.log?.(reason === undefined ? `
45340
+ \u6B63\u5728\u5173\u95ED\u2026` : `
45341
+ \u6B63\u5728\u5173\u95ED\uFF08${reason}\uFF09\u2026`);
44901
45342
  controller.abort();
44902
45343
  timer = setTimeout(() => {
44903
45344
  options.log?.("\u5173\u95ED\u8D85\u65F6\uFF0C\u5F3A\u5236\u9000\u51FA\u3002");
@@ -44906,6 +45347,7 @@ function installShutdown(options = {}) {
44906
45347
  timer.unref?.();
44907
45348
  runCleanup();
44908
45349
  };
45350
+ const onSignal = () => begin(undefined);
44909
45351
  const installed = [];
44910
45352
  for (const signal of shutdownSignals(proc.platform)) {
44911
45353
  try {
@@ -44920,6 +45362,9 @@ function installShutdown(options = {}) {
44920
45362
  if (controller.signal.aborted)
44921
45363
  runCleanup();
44922
45364
  },
45365
+ trigger(reason) {
45366
+ begin(reason);
45367
+ },
44923
45368
  dispose() {
44924
45369
  clearTimeout(timer);
44925
45370
  for (const signal of installed)
@@ -44945,13 +45390,13 @@ var init_src2 = __esm(() => {
44945
45390
 
44946
45391
  // packages/tui/src/gateway.ts
44947
45392
  import { spawn as spawn3 } from "child_process";
44948
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
44949
- import { basename as basename7, dirname as dirname11, join as join15 } from "path";
45393
+ import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
45394
+ import { basename as basename8, dirname as dirname11, join as join16 } from "path";
44950
45395
  async function connectToGateway(options = {}) {
44951
45396
  const config = loadNekoConfig(options.configDir);
44952
45397
  const storagePath = options.storagePath ?? config.storagePath ?? DEFAULT_STORAGE_PATH;
44953
45398
  const nekoDir = dirname11(storagePath);
44954
- const fileServerId = readServerId(join15(nekoDir, SERVER_ID_FILENAME2));
45399
+ const fileServerId = readServerId(join16(nekoDir, SERVER_ID_FILENAME2));
44955
45400
  const platform = options.platform ?? process.platform;
44956
45401
  if (platform === "win32") {
44957
45402
  if (fileServerId !== undefined) {
@@ -44968,8 +45413,8 @@ async function connectToGateway(options = {}) {
44968
45413
  }
44969
45414
  return spawnAndConnect({ ...options, platform, storagePath, socketPath: undefined, nekoDir, fileServerId });
44970
45415
  }
44971
- const socketPath = options.socketPath ?? config.gateway?.socketPath ?? (fileServerId !== undefined ? join15(nekoDir, `${fileServerId}.sock`) : undefined);
44972
- const serverIdFor = (path) => fileServerId ?? serverIdFromSocketPath2(path) ?? readServerId(join15(nekoDir, SERVER_ID_FILENAME2));
45416
+ const socketPath = options.socketPath ?? config.gateway?.socketPath ?? (fileServerId !== undefined ? join16(nekoDir, `${fileServerId}.sock`) : undefined);
45417
+ const serverIdFor = (path) => fileServerId ?? serverIdFromSocketPath2(path) ?? readServerId(join16(nekoDir, SERVER_ID_FILENAME2));
44973
45418
  if (socketPath !== undefined && existsSync3(socketPath)) {
44974
45419
  const serverId = serverIdFor(socketPath);
44975
45420
  if (serverId !== undefined) {
@@ -44986,7 +45431,9 @@ async function connectToGateway(options = {}) {
44986
45431
  }
44987
45432
  async function spawnAndConnect(plan) {
44988
45433
  const command = gatewayCommand(gatewayArgs(plan));
44989
- const child = spawn3(command.binary, command.args, { stdio: ["ignore", "pipe", "pipe"] });
45434
+ const child = spawn3(command.binary, command.args, {
45435
+ stdio: [plan.platform === "win32" ? "pipe" : "ignore", "pipe", "pipe"]
45436
+ });
44990
45437
  const outputTail = captureOutput(child);
44991
45438
  let spawnError;
44992
45439
  child.on("error", (error) => {
@@ -44995,7 +45442,7 @@ async function spawnAndConnect(plan) {
44995
45442
  try {
44996
45443
  return await connectSpawned(plan, child, outputTail, () => spawnError);
44997
45444
  } catch (error) {
44998
- await stopGatewayProcess(child);
45445
+ await stopGatewayProcess(child, { viaStdin: plan.platform === "win32" });
44999
45446
  throw error;
45000
45447
  }
45001
45448
  }
@@ -45003,7 +45450,7 @@ async function connectSpawned(plan, child, outputTail, spawnError) {
45003
45450
  const timeoutMs = plan.spawnTimeoutMs ?? 20000;
45004
45451
  if (plan.platform === "win32") {
45005
45452
  const timeoutAt = Date.now() + timeoutMs;
45006
- const serverId = plan.fileServerId ?? await waitForValue(() => readServerId(join15(plan.nekoDir, SERVER_ID_FILENAME2)), "gateway serverId file", timeoutMs, child, outputTail, spawnError);
45453
+ const serverId = plan.fileServerId ?? await waitForValue(() => readServerId(join16(plan.nekoDir, SERVER_ID_FILENAME2)), "gateway serverId file", timeoutMs, child, outputTail, spawnError);
45007
45454
  let lastError = new Error("Gateway port file unavailable");
45008
45455
  while (Date.now() < timeoutAt) {
45009
45456
  if (child.exitCode !== null || child.signalCode !== null || spawnError() !== undefined) {
@@ -45024,7 +45471,7 @@ async function connectSpawned(plan, child, outputTail, spawnError) {
45024
45471
  spawned: true,
45025
45472
  async close() {
45026
45473
  await client.close().catch(() => {});
45027
- await stopGatewayProcess(child);
45474
+ await stopGatewayProcess(child, { viaStdin: plan.platform === "win32" });
45028
45475
  await removeGatewayPortFile(plan.nekoDir, serverId).catch(() => {});
45029
45476
  }
45030
45477
  };
@@ -45040,11 +45487,11 @@ async function connectSpawned(plan, child, outputTail, spawnError) {
45040
45487
  }
45041
45488
  let socketPath = plan.socketPath;
45042
45489
  if (socketPath === undefined) {
45043
- const generated = await waitForValue(() => readServerId(join15(plan.nekoDir, SERVER_ID_FILENAME2)), "gateway serverId file", timeoutMs, child, outputTail, spawnError);
45044
- socketPath = join15(plan.nekoDir, `${generated}.sock`);
45490
+ const generated = await waitForValue(() => readServerId(join16(plan.nekoDir, SERVER_ID_FILENAME2)), "gateway serverId file", timeoutMs, child, outputTail, spawnError);
45491
+ socketPath = join16(plan.nekoDir, `${generated}.sock`);
45045
45492
  }
45046
45493
  await waitForValue(() => existsSync3(socketPath) ? true : undefined, `gateway ready (socket: ${socketPath})`, timeoutMs, child, outputTail, spawnError);
45047
- const serverId = plan.fileServerId ?? serverIdFromSocketPath2(socketPath) ?? readServerId(join15(plan.nekoDir, SERVER_ID_FILENAME2));
45494
+ const serverId = plan.fileServerId ?? serverIdFromSocketPath2(socketPath) ?? readServerId(join16(plan.nekoDir, SERVER_ID_FILENAME2));
45048
45495
  if (serverId === undefined) {
45049
45496
  throw new Error(`Cannot determine gateway serverId (socket: ${socketPath})`);
45050
45497
  }
@@ -45061,7 +45508,7 @@ async function connectSpawned(plan, child, outputTail, spawnError) {
45061
45508
  spawned: true,
45062
45509
  async close() {
45063
45510
  await client.close().catch(() => {});
45064
- await stopGatewayProcess(child);
45511
+ await stopGatewayProcess(child, { viaStdin: plan.platform === "win32" });
45065
45512
  }
45066
45513
  };
45067
45514
  } catch (error) {
@@ -45078,6 +45525,7 @@ function gatewayArgs(plan) {
45078
45525
  if (plan.platform === "win32") {
45079
45526
  return [
45080
45527
  "--no-socket",
45528
+ "--stop-on-stdin",
45081
45529
  "--storage",
45082
45530
  plan.storagePath,
45083
45531
  ...plan.credentialsPath ? ["--credentials", plan.credentialsPath] : [],
@@ -45096,14 +45544,14 @@ function gatewayArgs(plan) {
45096
45544
  }
45097
45545
  function readServerId(file) {
45098
45546
  try {
45099
- const value = readFileSync3(file, "utf8").trim();
45547
+ const value = readFileSync4(file, "utf8").trim();
45100
45548
  return isServerId(value) ? value : undefined;
45101
45549
  } catch {
45102
45550
  return;
45103
45551
  }
45104
45552
  }
45105
45553
  function serverIdFromSocketPath2(socketPath) {
45106
- const name = basename7(socketPath);
45554
+ const name = basename8(socketPath);
45107
45555
  if (!name.endsWith(".sock"))
45108
45556
  return;
45109
45557
  const candidate = name.slice(0, -".sock".length);
@@ -45175,8 +45623,8 @@ var init_gateway2 = __esm(() => {
45175
45623
  });
45176
45624
 
45177
45625
  // packages/tui/src/help-ui.ts
45178
- import { getKeybindings, matchesKey as matchesKey8, TUI_KEYBINDINGS, wrapTextWithAnsi as wrapTextWithAnsi4 } from "@earendil-works/pi-tui";
45179
- function helpLines(platform = process.platform) {
45626
+ import { getKeybindings, matchesKey as matchesKey9, TUI_KEYBINDINGS, wrapTextWithAnsi as wrapTextWithAnsi4 } from "@earendil-works/pi-tui";
45627
+ function helpLines(platform = process.platform, wsl = runningInWsl()) {
45180
45628
  const rows = [];
45181
45629
  const section = (title, hints) => {
45182
45630
  rows.push(theme.accent(title), ...hints.map(([key, description]) => shortcutHints([key, description])), "");
@@ -45189,7 +45637,7 @@ function helpLines(platform = process.platform) {
45189
45637
  ["Ctrl+C", "Stop while running; exit when idle"],
45190
45638
  ["Ctrl+S", "Open sessions"],
45191
45639
  ["Ctrl+O", "Open or close the assistant's question"],
45192
- [platform === "win32" ? "Alt+V" : "Ctrl+V", "Paste clipboard image or text"],
45640
+ [keyLabel(clipboardShortcutKey(platform, wsl)), "Paste clipboard image or text"],
45193
45641
  ["/", "Complete commands"],
45194
45642
  ["@", "Complete workspace files"],
45195
45643
  ["/skill:", "Complete installed skills"]
@@ -45308,13 +45756,13 @@ class HelpDetail {
45308
45756
  this.#offset = Math.max(0, Math.min(this.#max, this.#offset + delta));
45309
45757
  }
45310
45758
  handleKey(data) {
45311
- if (matchesKey8(data, "up"))
45759
+ if (matchesKey9(data, "up"))
45312
45760
  this.scroll(-1);
45313
- else if (matchesKey8(data, "down"))
45761
+ else if (matchesKey9(data, "down"))
45314
45762
  this.scroll(1);
45315
- else if (matchesKey8(data, "pageUp"))
45763
+ else if (matchesKey9(data, "pageUp"))
45316
45764
  this.scroll(-this.#page);
45317
- else if (matchesKey8(data, "pageDown"))
45765
+ else if (matchesKey9(data, "pageDown"))
45318
45766
  this.scroll(this.#page);
45319
45767
  else
45320
45768
  return false;
@@ -45331,117 +45779,11 @@ class HelpDetail {
45331
45779
  var keyLabel = (key) => key.split("+").map((part) => ({ escape: "Esc", pageUp: "PgUp", pageDown: "PgDn" })[part] ?? part[0].toUpperCase() + part.slice(1)).join("+");
45332
45780
  var init_help_ui = __esm(() => {
45333
45781
  init_shortcut_hints();
45782
+ init_image_input();
45334
45783
  init_labels();
45335
45784
  init_theme();
45336
45785
  });
45337
45786
 
45338
- // packages/tui/src/image-input.ts
45339
- import { execFile as execFile3 } from "child_process";
45340
- import { readFile as readFile9, stat as stat5 } from "fs/promises";
45341
- import { homedir as homedir8 } from "os";
45342
- import { basename as basename8, resolve as resolve6 } from "path";
45343
- import { promisify as promisify2 } from "util";
45344
- import { isKeyRelease as isKeyRelease4, isKeyRepeat as isKeyRepeat2, matchesKey as matchesKey9 } from "@earendil-works/pi-tui";
45345
- function imagePaths(text, platform = process.platform) {
45346
- const paths = [];
45347
- let word = "", quote = "", escaped = false;
45348
- for (const c of text.trim()) {
45349
- if (escaped) {
45350
- word += c;
45351
- escaped = false;
45352
- } else if (c === "\\" && quote !== "'" && platform !== "win32") {
45353
- escaped = true;
45354
- } else if (quote) {
45355
- if (c === quote)
45356
- quote = "";
45357
- else
45358
- word += c;
45359
- } else if (c === '"' || c === "'")
45360
- quote = c;
45361
- else if (/\s/.test(c)) {
45362
- if (word) {
45363
- paths.push(word);
45364
- word = "";
45365
- }
45366
- } else
45367
- word += c;
45368
- }
45369
- if (quote || escaped)
45370
- return null;
45371
- if (word)
45372
- paths.push(word);
45373
- return paths.length && paths.every((p) => /\.(png|jpe?g|webp)$/i.test(p)) ? paths : null;
45374
- }
45375
- async function readImageFile(path, cwd) {
45376
- const full = resolve6(cwd, path.startsWith("~/") ? homedir8() + path.slice(1) : path);
45377
- const info = await stat5(full);
45378
- if (!info.isFile() || info.size > 20 * 1024 * 1024)
45379
- throw new Error("Image must be a file no larger than 20 MiB");
45380
- const bytes = await readFile9(full);
45381
- const after = await stat5(full);
45382
- if (bytes.length !== info.size || after.size !== info.size || after.mtimeMs !== info.mtimeMs)
45383
- throw new Error("Image changed while reading");
45384
- const mimeType = /\.png$/i.test(full) ? "image/png" : /\.webp$/i.test(full) ? "image/webp" : "image/jpeg";
45385
- return { name: basename8(full), mimeType, bytes };
45386
- }
45387
- async function command(file, args, encoding = "utf8") {
45388
- return (await exec(file, args, { timeout: 2000, maxBuffer: 30 * 1024 * 1024, encoding })).stdout;
45389
- }
45390
- async function clipboard(options = {}) {
45391
- const run = options.command ?? command;
45392
- const platform = options.platform ?? process.platform;
45393
- if (platform === "darwin") {
45394
- const files = await run("osascript", [
45395
- "-e",
45396
- `try
45397
- set fs to the clipboard as \xABclass furl\xBB
45398
- return POSIX path of fs
45399
- on error
45400
- return ""
45401
- end try`
45402
- ]).catch(() => "");
45403
- if (String(files).trim())
45404
- return /\.(png|jpe?g|webp)$/i.test(String(files).trim()) ? { paths: [String(files).trim()] } : { text: String(files).trim() };
45405
- const image = String(await run("osascript", ["-e", `try
45406
- return the clipboard as \xABclass PNGf\xBB
45407
- on error
45408
- return ""
45409
- end try`]).catch(() => ""));
45410
- const hex = /\u00ABdata PNGf([0-9A-Fa-f]+)\u00BB/.exec(image)?.[1];
45411
- if (hex)
45412
- return { image: { name: "Clipboard.png", mimeType: "image/png", bytes: Buffer.from(hex, "hex") } };
45413
- return { text: String(await run("pbpaste", [])) };
45414
- }
45415
- if (platform === "win32") {
45416
- const value = String(await run("powershell.exe", [
45417
- "-NoProfile",
45418
- "-STA",
45419
- "-Command",
45420
- 'Add-Type -AssemblyName System.Windows.Forms; $i=[Windows.Forms.Clipboard]::GetImage(); if($i){$m=New-Object IO.MemoryStream; $i.Save($m,[Drawing.Imaging.ImageFormat]::Png); "IMAGE:"+[Convert]::ToBase64String($m.ToArray());$i.Dispose();$m.Dispose()}else{Get-Clipboard -Raw}'
45421
- ]));
45422
- if (value.startsWith("IMAGE:"))
45423
- return {
45424
- image: { name: "Clipboard.png", mimeType: "image/png", bytes: Buffer.from(value.slice(6).trim(), "base64") }
45425
- };
45426
- return { text: value };
45427
- }
45428
- const wayland = options.wayland ?? !!process.env.WAYLAND_DISPLAY;
45429
- const tool = wayland ? "wl-paste" : "xclip";
45430
- try {
45431
- const bytes = await run(tool, wayland ? ["--type", "image/png"] : ["-selection", "clipboard", "-t", "image/png", "-o"], "buffer");
45432
- if (bytes.length)
45433
- return { image: { name: "Clipboard.png", mimeType: "image/png", bytes: Buffer.from(bytes) } };
45434
- } catch {}
45435
- return { text: String(await run(tool, wayland ? ["--no-newline"] : ["-selection", "clipboard", "-o"])) };
45436
- }
45437
- function isClipboardShortcut(data, platform = process.platform) {
45438
- return !isKeyRelease4(data) && !isKeyRepeat2(data) && matchesKey9(data, platform === "win32" ? "alt+v" : "ctrl+v");
45439
- }
45440
- var exec;
45441
- var init_image_input = __esm(() => {
45442
- exec = promisify2(execFile3);
45443
- });
45444
-
45445
45787
  // packages/tui/src/question-dock.ts
45446
45788
  import {
45447
45789
  Input as Input3,
@@ -46440,6 +46782,31 @@ var init_search_ui = __esm(() => {
46440
46782
  };
46441
46783
  });
46442
46784
 
46785
+ // packages/tui/src/shell-notice.ts
46786
+ function shellNoticeText(shell) {
46787
+ if (shell.status !== "missing")
46788
+ return;
46789
+ if (shell.reason === "invalid_config")
46790
+ return `${shell.detail ?? "shell.path is invalid."} Bash and background jobs are unavailable. Fix shell.path in neko.config.json.`;
46791
+ return "Git Bash not found: bash and background jobs are unavailable. Install Git for Windows (https://git-scm.com/download/win) or set shell.path in neko.config.json.";
46792
+ }
46793
+ function createShellNotifier(show) {
46794
+ let last;
46795
+ return (shell) => {
46796
+ if (shell === undefined)
46797
+ return;
46798
+ const text = shellNoticeText(shell);
46799
+ if (text === undefined) {
46800
+ last = undefined;
46801
+ return;
46802
+ }
46803
+ if (text === last)
46804
+ return;
46805
+ last = text;
46806
+ show(text);
46807
+ };
46808
+ }
46809
+
46443
46810
  // packages/tui/src/skill-market-ui.ts
46444
46811
  import { ChordError as ChordError6 } from "@earendil-works/chord";
46445
46812
  import { Markdown as Markdown3, matchesKey as matchesKey13, wrapTextWithAnsi as wrapTextWithAnsi6 } from "@earendil-works/pi-tui";
@@ -47222,6 +47589,7 @@ class NekoTuiApp {
47222
47589
  #notices = new Container6;
47223
47590
  #versionNotice = new Container6;
47224
47591
  #versionStop;
47592
+ #shellStop;
47225
47593
  #versionTimer;
47226
47594
  #terminal = new ProcessTerminal;
47227
47595
  #panel;
@@ -47391,7 +47759,7 @@ class NekoTuiApp {
47391
47759
  showAgents: () => this.#showAgents(),
47392
47760
  toggleQuestion: () => this.#toggleQuestion(),
47393
47761
  clipboardKey: (data) => {
47394
- if (!matchesKey17(data, process.platform === "win32" ? "alt+v" : "ctrl+v"))
47762
+ if (!matchesKey17(data, clipboardShortcutKey()))
47395
47763
  return false;
47396
47764
  if (isClipboardShortcut(data))
47397
47765
  this.pasteClipboard();
@@ -47480,15 +47848,20 @@ class NekoTuiApp {
47480
47848
  this.#ui.requestRender();
47481
47849
  };
47482
47850
  this.#versionStop = this.#gateway.client.application.info.subscribe(() => void refreshVersion());
47851
+ const notifyShell = createShellNotifier((text) => this.addNotice(text, "warning", { wrap: true, ttlMs: 20000 }));
47852
+ const checkShell = () => notifyShell(this.#gateway.client.application.info.value?.shell);
47853
+ this.#shellStop = this.#gateway.client.application.info.subscribe(checkShell);
47483
47854
  this.#versionTimer = setInterval(() => void refreshVersion(), 30000);
47484
47855
  refreshVersion();
47485
47856
  await this.switchSession(initialSessionId);
47486
47857
  this.#ui.start();
47487
47858
  this.#ui.requestRender(true);
47859
+ checkShell();
47488
47860
  loadAllHighlightLanguages();
47489
47861
  this.#startThemeDetection();
47490
47862
  await this.#exited;
47491
47863
  this.#versionStop?.();
47864
+ this.#shellStop?.();
47492
47865
  clearInterval(this.#versionTimer);
47493
47866
  this.#viewUnsubscribe?.();
47494
47867
  this.#directoryUnsubscribe?.();
@@ -48679,7 +49052,7 @@ var init_open_browser = () => {};
48679
49052
  import { spawn as spawn4 } from "child_process";
48680
49053
  import { randomUUID as randomUUID7 } from "crypto";
48681
49054
  import { mkdir as mkdir8, readFile as readFile10, writeFile as writeFile7 } from "fs/promises";
48682
- import { dirname as dirname12, join as join16 } from "path";
49055
+ import { dirname as dirname12, join as join17 } from "path";
48683
49056
  async function ensureNekoGateway(options) {
48684
49057
  const timeoutMs = options.spawnTimeoutMs ?? 30000;
48685
49058
  const timeoutAt = Date.now() + timeoutMs;
@@ -48721,11 +49094,11 @@ async function ensureNekoGateway(options) {
48721
49094
  }
48722
49095
  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)}`);
48723
49096
  } catch (error) {
48724
- await stopGatewayProcess(child);
49097
+ await stopSpawnedGateway(child);
48725
49098
  throw error;
48726
49099
  }
48727
49100
  }
48728
- const socketPath = options.socketPath ?? join16(nekoDir, `${serverId}.sock`);
49101
+ const socketPath = options.socketPath ?? join17(nekoDir, `${serverId}.sock`);
48729
49102
  const host = options.host ?? "127.0.0.1";
48730
49103
  const port = options.port ?? 8321;
48731
49104
  const wsUrl = `ws://${host.includes(":") && !host.startsWith("[") ? `[${host}]` : host}:${port}`;
@@ -48764,15 +49137,18 @@ async function ensureNekoGateway(options) {
48764
49137
  }
48765
49138
  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)}`);
48766
49139
  } catch (error) {
48767
- await stopGatewayProcess(child);
49140
+ await stopSpawnedGateway(child);
48768
49141
  throw error;
48769
49142
  }
48770
49143
  }
49144
+ function stopSpawnedGateway(child) {
49145
+ return stopGatewayProcess(child, { viaStdin: process.platform === "win32" });
49146
+ }
48771
49147
  function spawnGatewayProcess(options, socketPath, serverId) {
48772
49148
  const args = [
48773
49149
  "--host",
48774
49150
  options.host ?? "127.0.0.1",
48775
- ...process.platform === "win32" ? ["--no-socket"] : ["--port", String(options.port ?? 8321), "--socket-path", socketPath ?? ""],
49151
+ ...process.platform === "win32" ? ["--no-socket", "--stop-on-stdin"] : ["--port", String(options.port ?? 8321), "--socket-path", socketPath ?? ""],
48776
49152
  "--server-id",
48777
49153
  serverId,
48778
49154
  "--storage",
@@ -48782,7 +49158,10 @@ function spawnGatewayProcess(options, socketPath, serverId) {
48782
49158
  ...options.model !== undefined ? ["--model", `${options.model.provider}/${options.model.modelId}`] : []
48783
49159
  ];
48784
49160
  const command = gatewayCommand(args);
48785
- return spawn4(command.binary, command.args, { env: process.env, stdio: ["ignore", "pipe", "pipe"] });
49161
+ return spawn4(command.binary, command.args, {
49162
+ env: process.env,
49163
+ stdio: [process.platform === "win32" ? "pipe" : "ignore", "pipe", "pipe"]
49164
+ });
48786
49165
  }
48787
49166
  function pipeChildLogs(child, onLog) {
48788
49167
  if (onLog === undefined)
@@ -48839,7 +49218,7 @@ function makeHandle(serverId, socketPath, wsUrl, spawned, nekoDir) {
48839
49218
  spawned,
48840
49219
  async close() {
48841
49220
  if (spawned !== null) {
48842
- await stopGatewayProcess(spawned);
49221
+ await stopSpawnedGateway(spawned);
48843
49222
  if (process.platform === "win32")
48844
49223
  await removeGatewayPortFile(nekoDir, serverId).catch(() => {});
48845
49224
  }
@@ -48847,7 +49226,7 @@ function makeHandle(serverId, socketPath, wsUrl, spawned, nekoDir) {
48847
49226
  };
48848
49227
  }
48849
49228
  async function loadOrCreateServerId2(nekoDir) {
48850
- const file = join16(nekoDir, SERVER_ID_FILENAME3);
49229
+ const file = join17(nekoDir, SERVER_ID_FILENAME3);
48851
49230
  try {
48852
49231
  const existing = (await readFile10(file, "utf8")).trim();
48853
49232
  if (existing !== "")
@@ -49172,9 +49551,9 @@ var init_mobile_admin = __esm(() => {
49172
49551
 
49173
49552
  // packages/web/src/open-target.ts
49174
49553
  import { spawn as spawn5 } from "child_process";
49175
- import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
49176
- import { homedir as homedir9 } from "os";
49177
- import { posix, win32 } from "path";
49554
+ import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
49555
+ import { homedir as homedir8 } from "os";
49556
+ import { posix, win32 as win322 } from "path";
49178
49557
  function isOpenTargetId(value) {
49179
49558
  return value === "vscode" || value === "file-manager";
49180
49559
  }
@@ -49193,9 +49572,9 @@ function systemOpenHost() {
49193
49572
  const platform = process.platform;
49194
49573
  return {
49195
49574
  platform,
49196
- wsl: detectWsl2(platform, process.env, () => readFileSync4("/proc/version", "utf8")),
49575
+ wsl: detectWsl2(platform, process.env, () => readFileSync5("/proc/version", "utf8")),
49197
49576
  env: process.env,
49198
- home: homedir9(),
49577
+ home: homedir8(),
49199
49578
  exists: existsSync4
49200
49579
  };
49201
49580
  }
@@ -49208,7 +49587,7 @@ function fileManagerLabel(host) {
49208
49587
  }
49209
49588
  function findOnPath(host, names) {
49210
49589
  const windows = host.platform === "win32";
49211
- const pathApi = windows ? win32 : posix;
49590
+ const pathApi = windows ? win322 : posix;
49212
49591
  const raw = host.env[windows ? "Path" : "PATH"] ?? host.env.PATH ?? host.env.Path ?? "";
49213
49592
  for (const entry of raw.split(windows ? ";" : ":")) {
49214
49593
  if (entry === "")
@@ -49224,7 +49603,7 @@ function findOnPath(host, names) {
49224
49603
  function windowsCodeExecutable(host) {
49225
49604
  const shim = findOnPath(host, ["code.cmd"]);
49226
49605
  if (shim !== undefined) {
49227
- const exe = win32.join(win32.dirname(win32.dirname(shim)), "Code.exe");
49606
+ const exe = win322.join(win322.dirname(win322.dirname(shim)), "Code.exe");
49228
49607
  if (host.exists(exe))
49229
49608
  return exe;
49230
49609
  }
@@ -49235,7 +49614,7 @@ function windowsCodeExecutable(host) {
49235
49614
  const base = host.env[variable];
49236
49615
  if (base === undefined || base === "")
49237
49616
  continue;
49238
- const exe = win32.join(base, suffix);
49617
+ const exe = win322.join(base, suffix);
49239
49618
  if (host.exists(exe))
49240
49619
  return exe;
49241
49620
  }
@@ -49280,8 +49659,7 @@ function spawnDetached(spec) {
49280
49659
  const child = spawn5(spec.command, [...spec.args], {
49281
49660
  cwd: spec.cwd,
49282
49661
  detached: true,
49283
- stdio: "ignore",
49284
- windowsHide: true
49662
+ stdio: "ignore"
49285
49663
  });
49286
49664
  const timer = setTimeout(() => reject(new OpenError("launch_failed", "\u542F\u52A8\u8D85\u65F6")), LAUNCH_TIMEOUT_MS);
49287
49665
  child.once("error", (error) => {
@@ -49322,7 +49700,7 @@ var init_open_target = __esm(() => {
49322
49700
  });
49323
49701
 
49324
49702
  // packages/web/src/open-api.ts
49325
- import { realpath as realpath4, stat as stat6 } from "fs/promises";
49703
+ import { realpath as realpath4, stat as stat7 } from "fs/promises";
49326
49704
  import { isAbsolute as isAbsolute5 } from "path";
49327
49705
  async function validateDirectory(value) {
49328
49706
  if (typeof value !== "string" || value === "" || value.length > MAX_PATH_LENGTH)
@@ -49331,7 +49709,7 @@ async function validateDirectory(value) {
49331
49709
  return;
49332
49710
  try {
49333
49711
  const resolved = await realpath4(value);
49334
- return (await stat6(resolved)).isDirectory() ? resolved : undefined;
49712
+ return (await stat7(resolved)).isDirectory() ? resolved : undefined;
49335
49713
  } catch {
49336
49714
  return;
49337
49715
  }
@@ -49393,7 +49771,7 @@ var init_open_api = __esm(() => {
49393
49771
 
49394
49772
  // packages/web/src/server.ts
49395
49773
  import { createReadStream } from "fs";
49396
- import { stat as stat7 } from "fs/promises";
49774
+ import { stat as stat8 } from "fs/promises";
49397
49775
  import { createServer as createServer3 } from "http";
49398
49776
  import { dirname as dirname13, extname as extname2, resolve as resolve7, sep as sep3 } from "path";
49399
49777
  import { fileURLToPath as fileURLToPath5 } from "url";
@@ -49525,18 +49903,18 @@ async function serveStatic(pathname, method, response, staticDir) {
49525
49903
  return;
49526
49904
  }
49527
49905
  let file = candidate;
49528
- let stats = await stat7(file).catch(() => {
49906
+ let stats = await stat8(file).catch(() => {
49529
49907
  return;
49530
49908
  });
49531
49909
  if (stats?.isDirectory()) {
49532
49910
  file = resolve7(file, "index.html");
49533
- stats = await stat7(file).catch(() => {
49911
+ stats = await stat8(file).catch(() => {
49534
49912
  return;
49535
49913
  });
49536
49914
  }
49537
49915
  if (stats === undefined && extname2(candidate) === "") {
49538
49916
  file = resolve7(staticDir, "index.html");
49539
- stats = await stat7(file).catch(() => {
49917
+ stats = await stat8(file).catch(() => {
49540
49918
  return;
49541
49919
  });
49542
49920
  }
@@ -49823,6 +50201,9 @@ function parseGatewayArgs(argv) {
49823
50201
  options.configDir = next;
49824
50202
  i++;
49825
50203
  break;
50204
+ case "--stop-on-stdin":
50205
+ options.stopOnStdin = true;
50206
+ break;
49826
50207
  default:
49827
50208
  throw new Error(`\u672A\u77E5\u9009\u9879: ${arg}`);
49828
50209
  }
@@ -49844,6 +50225,7 @@ var GATEWAY_USAGE = `\u7528\u6CD5: neko serve [\u9009\u9879]
49844
50225
  --storage <path> SQLite \u5B58\u50A8\u6587\u4EF6\uFF08\u9ED8\u8BA4 ~/.neko/neko.sqlite\uFF09
49845
50226
  --credentials <path> \u7528\u6237\u51ED\u636E\u6587\u4EF6\uFF08\u9ED8\u8BA4 ~/.neko/credentials.json\uFF09
49846
50227
  --config <dir> neko.config.json \u67E5\u627E\u8D77\u59CB\u76EE\u5F55\uFF08\u9ED8\u8BA4\u5F53\u524D\u76EE\u5F55\uFF09
50228
+ --stop-on-stdin \u6807\u51C6\u8F93\u5165\u7ED3\u675F\u65F6\u5173\u505C\u7F51\u5173\uFF08\u4F9B Windows \u4E0A\u62C9\u8D77\u7F51\u5173\u7684\u524D\u7AEF\u4F7F\u7528\uFF09
49847
50229
  -h, --help \u663E\u793A\u6B64\u5E2E\u52A9
49848
50230
 
49849
50231
  \u53EF\u901A\u8FC7\u524D\u7AEF\u8BBE\u7F6E\u6216\u73AF\u5883\u53D8\u91CF\uFF08DEEPSEEK_API_KEY / OPENROUTER_API_KEY / OPENCODE_API_KEY / TAVILY_API_KEY \u7B49\uFF09\u914D\u7F6E\u51ED\u636E\uFF1B
@@ -49876,6 +50258,18 @@ var init_main3 = __esm(async () => {
49876
50258
  console.log(` WebSocket: ${gateway.wsUrl}`);
49877
50259
  console.log("\u6309 Ctrl+C \u505C\u6B62\u3002");
49878
50260
  shutdown2.setCleanup(() => gateway.close());
50261
+ if (options.stopOnStdin === true) {
50262
+ let triggered = false;
50263
+ const onStdinEnd = () => {
50264
+ if (triggered)
50265
+ return;
50266
+ triggered = true;
50267
+ shutdown2.trigger("stdin \u5DF2\u5173\u95ED");
50268
+ };
50269
+ process.stdin.once("end", onStdinEnd);
50270
+ process.stdin.once("close", onStdinEnd);
50271
+ process.stdin.resume();
50272
+ }
49879
50273
  });
49880
50274
 
49881
50275
  // packages/cli/src/main.ts