neko-harness 0.4.5 → 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.5", APPLICATION_PROTOCOL = 6, 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)) {
@@ -691,30 +729,85 @@ var init_config = __esm(() => {
691
729
  });
692
730
 
693
731
  // packages/core/src/credential-providers.ts
732
+ function isModelProvider(id) {
733
+ return MODEL_PROVIDERS.some((item) => item.id === id);
734
+ }
694
735
  function credentialProvider(id) {
695
736
  const spec = CREDENTIAL_PROVIDERS.find((item) => item.id === id);
696
737
  if (!spec)
697
738
  throw new Error(`Unknown credential provider: ${id}`);
698
739
  return spec;
699
740
  }
700
- var CREDENTIAL_PROVIDERS;
741
+ function normalizeEnabledProviders(saved) {
742
+ const wanted = new Set([...LOCKED_PROVIDERS, ...saved ?? DEFAULT_ENABLED_PROVIDERS]);
743
+ return MODEL_PROVIDERS.map((item) => item.id).filter((id) => wanted.has(id));
744
+ }
745
+ var model = (id, name, envVar, signupUrl) => ({ id, kind: "model", name, envVar, signupUrl, keyless: false }), MODEL_PROVIDERS, SEARCH_CREDENTIAL_PROVIDERS, CREDENTIAL_PROVIDERS, DEFAULT_ENABLED_PROVIDERS, LOCKED_PROVIDERS;
701
746
  var init_credential_providers = __esm(() => {
747
+ MODEL_PROVIDERS = [
748
+ model("deepseek", "DeepSeek", "DEEPSEEK_API_KEY", "https://platform.deepseek.com/api_keys"),
749
+ model("openrouter", "OpenRouter", "OPENROUTER_API_KEY", "https://openrouter.ai/keys"),
750
+ model("opencode-go", "OpenCode Go", "OPENCODE_API_KEY", "https://opencode.ai/auth"),
751
+ model("opencode", "OpenCode Zen", "OPENCODE_API_KEY", "https://opencode.ai/auth"),
752
+ model("openai", "OpenAI", "OPENAI_API_KEY", "https://platform.openai.com/api-keys"),
753
+ model("google", "Google", "GEMINI_API_KEY", "https://aistudio.google.com/apikey"),
754
+ model("groq", "Groq", "GROQ_API_KEY", "https://console.groq.com/keys"),
755
+ model("mistral", "Mistral", "MISTRAL_API_KEY", "https://console.mistral.ai/api-keys"),
756
+ model("xai", "xAI", "XAI_API_KEY", "https://console.x.ai"),
757
+ model("together", "Together", "TOGETHER_API_KEY", "https://api.together.ai/settings/api-keys"),
758
+ model("fireworks", "Fireworks", "FIREWORKS_API_KEY", "https://fireworks.ai/account/api-keys"),
759
+ model("cerebras", "Cerebras", "CEREBRAS_API_KEY", "https://cloud.cerebras.ai"),
760
+ model("nvidia", "NVIDIA", "NVIDIA_API_KEY", "https://build.nvidia.com/settings/api-keys"),
761
+ model("huggingface", "Hugging Face", "HF_TOKEN", "https://huggingface.co/settings/tokens"),
762
+ model("vercel-ai-gateway", "Vercel AI Gateway", "AI_GATEWAY_API_KEY", "https://vercel.com/ai-gateway"),
763
+ model("moonshotai", "Moonshot AI", "MOONSHOT_API_KEY", "https://platform.moonshot.ai/console/api-keys"),
764
+ model("moonshotai-cn", "Moonshot AI CN", "MOONSHOT_API_KEY", "https://platform.moonshot.cn/console/api-keys"),
765
+ model("minimax", "MiniMax", "MINIMAX_API_KEY", "https://platform.minimax.io"),
766
+ model("minimax-cn", "MiniMax CN", "MINIMAX_CN_API_KEY", "https://platform.minimaxi.com"),
767
+ model("zai", "Z.AI", "ZAI_API_KEY", "https://z.ai/manage-apikey/apikey-list"),
768
+ model("zai-coding-cn", "Z.AI Coding CN", "ZAI_CODING_CN_API_KEY", "https://bigmodel.cn/usercenter/proj-mgmt/apikeys"),
769
+ model("qwen-token-plan", "Qwen Token Plan", "QWEN_TOKEN_PLAN_API_KEY", "https://modelstudio.console.alibabacloud.com"),
770
+ model("qwen-token-plan-cn", "Qwen Token Plan CN", "QWEN_TOKEN_PLAN_CN_API_KEY", "https://bailian.console.aliyun.com"),
771
+ model("qwen-token-plan-individual", "Qwen Token Plan Individual", "QWEN_TOKEN_PLAN_API_KEY", "https://modelstudio.console.alibabacloud.com"),
772
+ model("xiaomi", "Xiaomi MiMo", "XIAOMI_API_KEY", "https://platform.xiaomimimo.com"),
773
+ model("xiaomi-token-plan-cn", "Xiaomi Token Plan CN", "XIAOMI_TOKEN_PLAN_CN_API_KEY", "https://platform.xiaomimimo.com"),
774
+ model("xiaomi-token-plan-ams", "Xiaomi Token Plan AMS", "XIAOMI_TOKEN_PLAN_AMS_API_KEY", "https://platform.xiaomimimo.com"),
775
+ model("xiaomi-token-plan-sgp", "Xiaomi Token Plan SGP", "XIAOMI_TOKEN_PLAN_SGP_API_KEY", "https://platform.xiaomimimo.com"),
776
+ model("baseten", "Baseten", "BASETEN_API_KEY", "https://app.baseten.co/settings/api_keys"),
777
+ model("ant-ling", "Ant Ling", "ANT_LING_API_KEY", "https://ling.tbox.cn"),
778
+ model("kimi-coding", "Kimi For Coding", "KIMI_API_KEY", "https://www.kimi.com/code/console"),
779
+ model("meta", "Meta", "META_API_KEY", "https://llama.developer.meta.com")
780
+ ];
781
+ SEARCH_CREDENTIAL_PROVIDERS = [
782
+ {
783
+ id: "tavily",
784
+ kind: "search",
785
+ name: "Tavily",
786
+ envVar: "TAVILY_API_KEY",
787
+ signupUrl: "https://app.tavily.com",
788
+ keyless: true
789
+ }
790
+ ];
702
791
  CREDENTIAL_PROVIDERS = [
703
- { id: "deepseek", envVar: "DEEPSEEK_API_KEY", signupUrl: "https://platform.deepseek.com/api_keys", keyless: false },
704
- { id: "tavily", envVar: "TAVILY_API_KEY", signupUrl: "https://app.tavily.com", keyless: true }
792
+ ...MODEL_PROVIDERS,
793
+ ...SEARCH_CREDENTIAL_PROVIDERS
705
794
  ];
795
+ DEFAULT_ENABLED_PROVIDERS = ["deepseek"];
796
+ LOCKED_PROVIDERS = ["deepseek"];
706
797
  });
707
798
 
708
799
  // packages/core/src/credentials.ts
709
- 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";
710
801
  import { homedir as homedir3 } from "os";
711
802
  import { dirname as dirname4, join as join4 } from "path";
712
803
  import { InMemoryCredentialStore } from "@earendil-works/pi-ai";
713
804
 
714
805
  class FileCredentialStore {
715
806
  path;
716
- constructor(path = DEFAULT_CREDENTIALS_PATH) {
807
+ #renameDeps;
808
+ constructor(path = DEFAULT_CREDENTIALS_PATH, renameDeps = {}) {
717
809
  this.path = path;
810
+ this.#renameDeps = renameDeps;
718
811
  }
719
812
  async#read() {
720
813
  try {
@@ -744,6 +837,7 @@ class FileCredentialStore {
744
837
  await mkdir3(dirname4(this.path), { recursive: true, mode: 448 });
745
838
  const lockPath = `${this.path}.lock`;
746
839
  let lock;
840
+ let reclaimed = false;
747
841
  for (let attempt = 0;; attempt++) {
748
842
  try {
749
843
  lock = await open(lockPath, "wx", 384);
@@ -751,6 +845,16 @@ class FileCredentialStore {
751
845
  } catch (error) {
752
846
  if (error.code !== "EEXIST" || attempt >= 100)
753
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
+ }
754
858
  await new Promise((resolve) => setTimeout(resolve, 25));
755
859
  }
756
860
  }
@@ -772,7 +876,7 @@ class FileCredentialStore {
772
876
  } finally {
773
877
  await file.close();
774
878
  }
775
- await rename2(temporary, this.path);
879
+ await renameWithRetry(temporary, this.path, this.#renameDeps);
776
880
  return result;
777
881
  } catch (error) {
778
882
  if (callbackFailed)
@@ -798,8 +902,9 @@ class FileCredentialStore {
798
902
  });
799
903
  }
800
904
  }
801
- var DEFAULT_CREDENTIALS_PATH;
905
+ var STALE_LOCK_MS = 30000, DEFAULT_CREDENTIALS_PATH;
802
906
  var init_credentials = __esm(() => {
907
+ init_rename_with_retry();
803
908
  init_credential_providers();
804
909
  DEFAULT_CREDENTIALS_PATH = join4(homedir3(), ".neko", "credentials.json");
805
910
  });
@@ -1521,7 +1626,7 @@ async function acquireSlot(tx, round, cwd) {
1521
1626
  slots.items.push({ taskId: round.taskId, parentId: round.parentId, cwd, coder: round.role === "coder" });
1522
1627
  return true;
1523
1628
  }
1524
- function createSubagentExtension(prepare, discard) {
1629
+ function createSubagentExtension(prepare, discard, guard) {
1525
1630
  const finish = async (tx, input, status, report) => {
1526
1631
  const round = (await tx.doc(DelegationsDoc, input.parentId)).items.find((r) => r.taskId === input.taskId);
1527
1632
  round.status = status;
@@ -1665,6 +1770,8 @@ function createSubagentExtension(prepare, discard) {
1665
1770
  if (!args.task.trim() || !args.title.trim() || Buffer.byteLength(args.task) > 32768)
1666
1771
  throw new Error("Task/title must be nonempty; task limit is 32 KiB.");
1667
1772
  const agent = await api.agent(context);
1773
+ if (agent.model)
1774
+ await guard?.(agent.model);
1668
1775
  const round = await api.commit(async (tx) => {
1669
1776
  const rounds = await refreshDelegations(tx, api.conversationId);
1670
1777
  const receipt = rounds.find((r) => r.taskId === api.taskId);
@@ -2842,7 +2949,7 @@ var init_agent_file_search = __esm(() => {
2842
2949
  complete = false;
2843
2950
  continue;
2844
2951
  }
2845
- 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/);
2846
2953
  for (let i = 0;i < lines.length; i++) {
2847
2954
  ctx.abortSignal?.throwIfAborted();
2848
2955
  const value = lines[i];
@@ -3232,8 +3339,11 @@ class JobManager {
3232
3339
  outcome = { status: "failed", exitCode: null, reason: "timeout" };
3233
3340
  else if (result.error.code === "aborted")
3234
3341
  outcome = { status: "stopped", exitCode: null, reason: "aborted" };
3235
- else
3342
+ else {
3343
+ entry.buffer.append(`${result.error.message}
3344
+ `);
3236
3345
  outcome = { status: "failed", exitCode: null, reason: "spawn-error" };
3346
+ }
3237
3347
  }
3238
3348
  if (this.#closing) {
3239
3349
  await new Promise((resolve) => {
@@ -3641,10 +3751,13 @@ var init_invoke = __esm(() => {
3641
3751
 
3642
3752
  // packages/core/src/prompt/prompt.ts
3643
3753
  import { defineExtension as defineExtension5, section } from "@earendil-works/pi-durable";
3644
- 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) => {
3645
3758
  const rules = [];
3646
3759
  const seen = new Set;
3647
- 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]) {
3648
3761
  if (seen.has(rule))
3649
3762
  continue;
3650
3763
  seen.add(rule);
@@ -3659,7 +3772,7 @@ ${content}
3659
3772
  </project_instructions>`)
3660
3773
  ].join(`
3661
3774
 
3662
- `), createNekoPrompt = (loadResources) => {
3775
+ `), createNekoPrompt = (loadResources, platform = process.platform) => {
3663
3776
  const built = new WeakMap;
3664
3777
  const build = async (input, context) => {
3665
3778
  const cwd = input.agent.cwd;
@@ -3667,7 +3780,7 @@ ${content}
3667
3780
  const resources = cwd ? await loadResources(input.conversationId, cwd) : { contextFiles: [], skills: [] };
3668
3781
  const sections = {
3669
3782
  preamble: role ? ROLE_PROMPTS[role] + SUBAGENT_DELIVERY : PREAMBLE,
3670
- rules: buildRules(input.agent.tools.map((tool) => tool.name)),
3783
+ rules: buildRules(input.agent.tools.map((tool) => tool.name), platform),
3671
3784
  project_context: renderProjectContext(resources.contextFiles),
3672
3785
  skills: formatSkillsForPrompt(resources.skills) || undefined,
3673
3786
  cwd: cwd?.replace(/\\/g, "/")
@@ -3746,7 +3859,7 @@ var userAgentsFile = (home) => join6(home, ".agents", "AGENTS.md"), userSkillsDi
3746
3859
  const contextFiles = [];
3747
3860
  for (const path of [userAgentsFile(home), workspaceAgentsFile(workspace)]) {
3748
3861
  try {
3749
- const content = await readFile5(path, "utf8");
3862
+ const content = (await readFile5(path, "utf8")).replace(/^\uFEFF/, "");
3750
3863
  if (Buffer.byteLength(content, "utf8") > 64 * 1024)
3751
3864
  report({
3752
3865
  path,
@@ -4250,7 +4363,7 @@ function mergeUsage(base, extra) {
4250
4363
  }
4251
4364
  return result;
4252
4365
  }
4253
- function namingTask() {
4366
+ function namingTask(canUse) {
4254
4367
  let active = 0;
4255
4368
  const waits = new Set;
4256
4369
  return defineTask3({
@@ -4287,7 +4400,7 @@ function namingTask() {
4287
4400
  try {
4288
4401
  const saved = await runtime.snapshot(SessionMetadataDoc, runtime.conversationId, context);
4289
4402
  const model = saved?.model && runtime.models.getModel(saved.model.provider, saved.model.modelId);
4290
- if (!saved || !model || saved.attempts >= 2 || !getSupportedThinkingLevels(model).includes("off")) {
4403
+ if (!saved || !model || saved.attempts >= 2 || !getSupportedThinkingLevels(model).includes("off") || !await canUse(model.provider, model.id)) {
4291
4404
  await runtime.commit(async (tx) => {
4292
4405
  (await tx.doc(SessionMetadataDoc, runtime.conversationId)).naming = "fallback";
4293
4406
  return { status: "terminal", outcome: { status: "completed", result: null } };
@@ -4367,12 +4480,13 @@ class SessionMetadataManager {
4367
4480
  #known = new Set;
4368
4481
  #unsubscribe;
4369
4482
  #live = new Map;
4370
- task = namingTask();
4483
+ task;
4371
4484
  harness;
4372
4485
  storage;
4373
4486
  context;
4374
4487
  report;
4375
- constructor(harness, storage, context, registry, report = console.error) {
4488
+ constructor(harness, storage, context, registry, report = console.error, canUseModel = async () => true) {
4489
+ this.task = namingTask(canUseModel);
4376
4490
  this.harness = harness;
4377
4491
  this.storage = storage;
4378
4492
  this.context = context;
@@ -5215,11 +5329,153 @@ var init_tavily = __esm(() => {
5215
5329
  init_http();
5216
5330
  });
5217
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
+
5218
5470
  // packages/core/src/workspace-files.ts
5219
- 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";
5220
5472
  import { basename as basename3, isAbsolute as isAbsolute3, relative as relative2, resolve as resolve4, sep as sep2 } from "path";
5221
5473
  import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT2, withAbortSignal as withAbortSignal3 } from "@earendil-works/chord/context";
5222
- 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";
5223
5479
  import { Connection, LocalExecutionEnv, localDaemon } from "@earendil-works/pi-env";
5224
5480
  function isProtectedPath(path) {
5225
5481
  const name = basename3(path).toLowerCase();
@@ -5257,8 +5513,10 @@ class WorkspaceFiles {
5257
5513
  #waiting = new Set;
5258
5514
  #timer;
5259
5515
  limits;
5260
- constructor(limits = FILE_CACHE_LIMITS) {
5516
+ #shell;
5517
+ constructor(limits = FILE_CACHE_LIMITS, shell) {
5261
5518
  this.limits = limits;
5519
+ this.#shell = shell;
5262
5520
  this.#timer = setInterval(() => {
5263
5521
  for (const [key, item] of this.#cache)
5264
5522
  if (!item.waiters && Date.now() - item.used >= this.limits.idleMs)
@@ -5277,20 +5535,31 @@ class WorkspaceFiles {
5277
5535
  queued: this.#waiting.size
5278
5536
  };
5279
5537
  }
5280
- env(cwd) {
5538
+ shellStatus(options) {
5539
+ return this.#shell?.status(options);
5540
+ }
5541
+ #open() {
5281
5542
  if (this.#closed)
5282
5543
  throw new Error("File environment closed");
5283
5544
  this.#connection ??= new Connection({ command: [localDaemon()] });
5284
- return new LocalExecutionEnv({
5285
- connection: this.#connection,
5545
+ return this.#connection;
5546
+ }
5547
+ env(cwd) {
5548
+ const options = {
5549
+ connection: this.#open(),
5286
5550
  cwd,
5287
5551
  ...process.platform === "win32" ? { shellEnv: { PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" } } : {}
5288
- });
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));
5289
5559
  }
5290
5560
  async#ready() {
5291
5561
  if (!this.#starting) {
5292
- this.env(process.cwd());
5293
- const connection = this.#connection;
5562
+ const connection = this.#open();
5294
5563
  let timer;
5295
5564
  const work = Promise.race([
5296
5565
  connection.info().then(() => connection.session()),
@@ -5367,7 +5636,7 @@ class WorkspaceFiles {
5367
5636
  { path: item.root, recursive: true, exclude: { names: [".git"] } }
5368
5637
  ];
5369
5638
  const git = resolve4(item.root, ".git");
5370
- if (await stat2(git).then(() => true, () => false))
5639
+ if (await stat3(git).then(() => true, () => false))
5371
5640
  targets.push({ path: git, recursive: true, exclude: { names: ["objects", "logs", "refs"] } });
5372
5641
  const result = await env.watch(targets, (change) => {
5373
5642
  for (const user of current.users) {
@@ -5475,7 +5744,7 @@ class WorkspaceFiles {
5475
5744
  const slash = query.lastIndexOf("/"), prefix = slash < 0 ? "" : query.slice(0, slash + 1), needle = query.slice(slash + 1);
5476
5745
  root = await realpath3(root);
5477
5746
  const base = await workspaceTarget(root, prefix);
5478
- if (!(await stat2(base)).isDirectory())
5747
+ if (!(await stat3(base)).isDirectory())
5479
5748
  throw new Error("Workspace directory is unavailable.");
5480
5749
  return this.#withListing(root, base, signal, async (listing) => {
5481
5750
  const pattern = new RegExp([...needle].map((c) => c.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*"), /[A-Z]/.test(needle) ? "" : "i");
@@ -5505,7 +5774,7 @@ class WorkspaceFiles {
5505
5774
  const clean = dir === "" ? "" : relativeQuery(dir).replace(/\/+$/, "");
5506
5775
  root = await realpath3(root);
5507
5776
  const target = await workspaceTarget(root, clean);
5508
- if (!(await stat2(target)).isDirectory())
5777
+ if (!(await stat3(target)).isDirectory())
5509
5778
  throw new Error("Not a directory.");
5510
5779
  const prefix = clean === "" ? "" : `${clean}/`;
5511
5780
  return this.#withListing(root, root, signal, async (listing) => {
@@ -5547,7 +5816,7 @@ class WorkspaceFiles {
5547
5816
  throw new Error("File not found.");
5548
5817
  throw error;
5549
5818
  }
5550
- const info = await stat2(target);
5819
+ const info = await stat3(target);
5551
5820
  if (!info.isFile())
5552
5821
  throw new Error("Not a file.");
5553
5822
  const base = { path: clean, size: info.size, mtimeMs: info.mtimeMs };
@@ -5650,8 +5919,9 @@ function cancellable(work, signal) {
5650
5919
  abort();
5651
5920
  });
5652
5921
  }
5653
- var BROWSE_LIMITS, ENV_TEMPLATE, FILE_CACHE_LIMITS;
5922
+ var BROWSE_LIMITS, ENV_TEMPLATE, FILE_CACHE_LIMITS, ShellUnavailableEnv;
5654
5923
  var init_workspace_files = __esm(() => {
5924
+ init_windows_shell();
5655
5925
  BROWSE_LIMITS = { children: 1000, readBytes: 512 * 1024 };
5656
5926
  ENV_TEMPLATE = /\.(example|sample|template)$/;
5657
5927
  FILE_CACHE_LIMITS = {
@@ -5666,6 +5936,18 @@ var init_workspace_files = __esm(() => {
5666
5936
  idleMs: 300000,
5667
5937
  concurrent: 2
5668
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
+ };
5669
5951
  });
5670
5952
 
5671
5953
  // packages/core/src/runtime.ts
@@ -5673,7 +5955,7 @@ import { homedir as homedir5 } from "os";
5673
5955
  import { join as join7 } from "path";
5674
5956
  import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT3 } from "@earendil-works/chord/context";
5675
5957
  import { clampThinkingLevel, createModels, getSupportedThinkingLevels as getSupportedThinkingLevels2 } from "@earendil-works/pi-ai/models";
5676
- import { deepseekProvider } from "@earendil-works/pi-ai/providers/deepseek";
5958
+ import { builtinProviders } from "@earendil-works/pi-ai/providers/all";
5677
5959
  import {
5678
5960
  AssistantEntry as AssistantEntry2,
5679
5961
  createRegistry,
@@ -5692,6 +5974,7 @@ async function createNekoRuntime(options = {}) {
5692
5974
  const context = options.context ?? BACKGROUND_CONTEXT3;
5693
5975
  const defaultCwd = normalizeWorkspace(options.cwd);
5694
5976
  const model = options.model ?? DEFAULT_MODEL;
5977
+ const modelOverride = options.modelOverride;
5695
5978
  const providers = options.providers ?? [];
5696
5979
  const credentials = options.credentials ?? (options.storage === "memory" && !options.credentialsPath ? new InMemoryCredentialStore : new FileCredentialStore(options.credentialsPath));
5697
5980
  const runtimeKeys = new Map;
@@ -5711,16 +5994,47 @@ async function createNekoRuntime(options = {}) {
5711
5994
  delete: (id) => credentials.delete(id)
5712
5995
  } : credentials;
5713
5996
  const models = createModels({ credentials: effectiveCredentials });
5714
- models.setProvider(deepseekProvider());
5997
+ for (const provider of builtinProviders())
5998
+ if (isModelProvider(provider.id))
5999
+ models.setProvider(provider);
5715
6000
  for (const provider of providers)
5716
6001
  models.setProvider(provider);
6002
+ let enabled = new Set(normalizeEnabledProviders(null));
6003
+ const guardProvider = async (ref) => {
6004
+ if (!ref || !isModelProvider(ref.provider))
6005
+ return;
6006
+ const spec = credentialProvider(ref.provider);
6007
+ if (!enabled.has(ref.provider))
6008
+ throw new Error(`PROVIDER_DISABLED: ${spec.name} is not enabled. Enable it in /login or Web settings, or switch model.`);
6009
+ if (!await models.checkAuth(ref.provider))
6010
+ throw new Error(`PROVIDER_KEY_REQUIRED: Configure a ${spec.name} API key in /login or Web settings.`);
6011
+ };
5717
6012
  const resolveLevel = (ref, level) => {
5718
6013
  const info = models.getModel(ref.provider, ref.modelId);
5719
6014
  if (!info)
5720
6015
  throw new Error("Unknown model");
5721
6016
  return clampThinkingLevel(info, level);
5722
6017
  };
5723
- const workspaceFiles = new WorkspaceFiles;
6018
+ let modelInfoCache;
6019
+ const allModels = () => {
6020
+ modelInfoCache ??= models.getModels().map((model) => ({
6021
+ provider: model.provider,
6022
+ modelId: model.id,
6023
+ name: model.name,
6024
+ contextWindow: model.contextWindow,
6025
+ input: model.input,
6026
+ ...model.inputLimits ? { inputLimits: model.inputLimits } : {},
6027
+ reasoning: model.reasoning,
6028
+ supportedThinkingLevels: getSupportedThinkingLevels2(model),
6029
+ thinkingLevelAdjustments: Object.fromEntries(["off", "minimal", "low", "medium", "high", "xhigh", "max"].map((level) => [
6030
+ level,
6031
+ clampThinkingLevel(model, level)
6032
+ ]))
6033
+ }));
6034
+ return modelInfoCache;
6035
+ };
6036
+ const shellResolver = options.shellResolver ?? (process.platform === "win32" ? createWindowsShellResolver({ configPath: options.shellPath }) : undefined);
6037
+ const workspaceFiles = new WorkspaceFiles(undefined, shellResolver);
5724
6038
  const storage = options.storage === "memory" ? new MemoryStorage : await openNodeSqliteStorage(options.storagePath ?? DEFAULT_STORAGE_PATH);
5725
6039
  const registry = createRegistry();
5726
6040
  registry.install(createCodingTools({ images: await createNodePhotonImages() }));
@@ -5746,7 +6060,7 @@ async function createNekoRuntime(options = {}) {
5746
6060
  registry.install(createNekoPrompt(resourceSnapshots.current));
5747
6061
  registry.install(createSubagentExtension(async (id, requestId, cwd) => {
5748
6062
  resourceSnapshots.stage(id, requestId, cwd, await loadResources(cwd));
5749
- }, resourceSnapshots.discard));
6063
+ }, resourceSnapshots.discard, (ref) => guardProvider(ref)));
5750
6064
  const jobs = new JobManager;
5751
6065
  registry.install(jobs.extension);
5752
6066
  registry.install(createTodoExtension());
@@ -5764,7 +6078,7 @@ async function createNekoRuntime(options = {}) {
5764
6078
  registry,
5765
6079
  settings: {
5766
6080
  get compaction() {
5767
- return compactionPolicy(contextState, models.getModels().map((model) => model.contextWindow));
6081
+ return compactionPolicy(contextState, allModels().filter((model) => !isModelProvider(model.provider) || enabled.has(model.provider)).map((model) => model.contextWindow));
5768
6082
  }
5769
6083
  },
5770
6084
  onReport: options.onReport,
@@ -5806,13 +6120,15 @@ async function createNekoRuntime(options = {}) {
5806
6120
  throw error;
5807
6121
  }
5808
6122
  await questions.reconcileAll();
6123
+ const rootModel = modelOverride ?? model;
5809
6124
  const conversation = await harness.root(context, {
5810
6125
  agent: {
5811
- model,
5812
- thinkingLevel: resolveLevel(model, options.thinkingLevel ?? "high"),
6126
+ model: rootModel,
6127
+ thinkingLevel: resolveLevel(rootModel, options.thinkingLevel ?? "high"),
5813
6128
  cwd: defaultCwd
5814
6129
  }
5815
6130
  });
6131
+ enabled = new Set(normalizeEnabledProviders((await harness.snapshot(ProviderSettingsDoc, conversation.id, context))?.enabled));
5816
6132
  const savedSearch = (await harness.snapshot(SearchSettingsDoc, conversation.id, context))?.provider;
5817
6133
  if (isSearchProvider(savedSearch))
5818
6134
  searchProvider = savedSearch;
@@ -5858,7 +6174,7 @@ async function createNekoRuntime(options = {}) {
5858
6174
  };
5859
6175
  if (existing.length === 0)
5860
6176
  await touchConversation(conversation.id);
5861
- const metadata = new SessionMetadataManager(harness, storage, context, registry, options.onReport);
6177
+ const metadata = new SessionMetadataManager(harness, storage, context, registry, options.onReport, (provider, modelId) => guardProvider({ provider, modelId }).then(() => true, () => false));
5862
6178
  if (existing.length === 0)
5863
6179
  await metadata.initialize(conversation.id);
5864
6180
  for (const record of existing)
@@ -5919,9 +6235,8 @@ async function createNekoRuntime(options = {}) {
5919
6235
  data.skill = skill;
5920
6236
  });
5921
6237
  }
5922
- if (!duplicate && agent.model?.provider === "deepseek" && !await models.checkAuth("deepseek")) {
5923
- throw new Error("DEEPSEEK_KEY_REQUIRED: Configure a DeepSeek API key in /login or Web settings.");
5924
- }
6238
+ if (!duplicate)
6239
+ await guardProvider(agent.model);
5925
6240
  if (!duplicate && agent.model) {
5926
6241
  const resolved = models.getModel(agent.model.provider, agent.model.modelId);
5927
6242
  if (resolved)
@@ -6053,8 +6368,7 @@ async function createNekoRuntime(options = {}) {
6053
6368
  },
6054
6369
  async compact(instructions) {
6055
6370
  const agent = await target.agent(context);
6056
- if (agent.model?.provider === "deepseek" && !await models.checkAuth("deepseek"))
6057
- throw new Error("DEEPSEEK_KEY_REQUIRED: Configure DeepSeek in /login or Web settings.");
6371
+ await guardProvider(agent.model);
6058
6372
  await admit(target.id, async () => {
6059
6373
  const live = await harness.snapshot(importLiveDoc, target.id, context);
6060
6374
  if (live?.compactions?.length || (await readCompaction(harness, storage, target.id, context))?.phase === "waiting-to-apply")
@@ -6230,6 +6544,10 @@ async function createNekoRuntime(options = {}) {
6230
6544
  questions,
6231
6545
  closed: harness.closed,
6232
6546
  workspaceFiles,
6547
+ shellStatus(call) {
6548
+ const status = workspaceFiles.shellStatus(call);
6549
+ return status === undefined ? undefined : summarizeWindowsShell(status);
6550
+ },
6233
6551
  conversation,
6234
6552
  root: rootSession,
6235
6553
  submit: rootSession.submit,
@@ -6280,11 +6598,13 @@ async function createNekoRuntime(options = {}) {
6280
6598
  },
6281
6599
  async createConversation(createOptions) {
6282
6600
  const workspace = await addWorkspace(createOptions?.cwd ?? defaultCwd);
6601
+ const savedDefault = (await harness.snapshot(DefaultModelDoc, conversation.id, context))?.model ?? null;
6602
+ const effective = createOptions?.model ?? modelOverride ?? savedDefault ?? model;
6283
6603
  const created = await harness.createConversation({
6284
6604
  ownership: { kind: "ownerless" },
6285
6605
  agent: {
6286
- model: createOptions?.model ?? model,
6287
- thinkingLevel: resolveLevel(createOptions?.model ?? model, createOptions?.thinkingLevel ?? options.thinkingLevel ?? "high"),
6606
+ model: effective,
6607
+ thinkingLevel: resolveLevel(effective, createOptions?.thinkingLevel ?? options.thinkingLevel ?? "high"),
6288
6608
  cwd: workspace.path
6289
6609
  }
6290
6610
  }, context);
@@ -6314,6 +6634,8 @@ async function createNekoRuntime(options = {}) {
6314
6634
  const targetModel = ref && models.getModel(ref.provider, ref.modelId);
6315
6635
  if (!targetModel)
6316
6636
  throw new Error("Unknown model");
6637
+ if (change.model && isModelProvider(ref.provider) && !enabled.has(ref.provider))
6638
+ throw new Error(`PROVIDER_DISABLED: ${credentialProvider(ref.provider).name} is not enabled.`);
6317
6639
  await session.conversation.configure({ model: ref, thinkingLevel: clampThinkingLevel(targetModel, change.thinkingLevel ?? current.thinkingLevel) }, context);
6318
6640
  return true;
6319
6641
  });
@@ -6368,20 +6690,62 @@ async function createNekoRuntime(options = {}) {
6368
6690
  }
6369
6691
  },
6370
6692
  listModels() {
6371
- return models.getModels().map((model) => ({
6372
- provider: model.provider,
6373
- modelId: model.id,
6374
- name: model.name,
6375
- contextWindow: model.contextWindow,
6376
- input: model.input,
6377
- ...model.inputLimits ? { inputLimits: model.inputLimits } : {},
6378
- reasoning: model.reasoning,
6379
- supportedThinkingLevels: getSupportedThinkingLevels2(model),
6380
- thinkingLevelAdjustments: Object.fromEntries(["off", "minimal", "low", "medium", "high", "xhigh", "max"].map((level) => [
6381
- level,
6382
- clampThinkingLevel(model, level)
6383
- ]))
6384
- }));
6693
+ return allModels();
6694
+ },
6695
+ async listSelectableModels() {
6696
+ const availability = new Map;
6697
+ const result = [];
6698
+ for (const info of allModels()) {
6699
+ const managed = isModelProvider(info.provider);
6700
+ if (managed && !enabled.has(info.provider))
6701
+ continue;
6702
+ if (!availability.has(info.provider))
6703
+ availability.set(info.provider, !managed || Boolean(await models.checkAuth(info.provider)));
6704
+ result.push({ ...info, available: availability.get(info.provider) ?? false });
6705
+ }
6706
+ return result;
6707
+ },
6708
+ enabledProviders() {
6709
+ return MODEL_PROVIDERS.map((item) => item.id).filter((id) => enabled.has(id));
6710
+ },
6711
+ async setProviderEnabled(provider, value) {
6712
+ if (!isModelProvider(provider))
6713
+ throw new Error("Unknown model provider.");
6714
+ if (typeof value !== "boolean")
6715
+ throw new Error("Enabled must be a boolean.");
6716
+ if (!value && LOCKED_PROVIDERS.includes(provider))
6717
+ throw new Error("DeepSeek is always enabled.");
6718
+ const next = new Set(enabled);
6719
+ if (value)
6720
+ next.add(provider);
6721
+ else
6722
+ next.delete(provider);
6723
+ const list = normalizeEnabledProviders([...next]);
6724
+ await harness.commit(async (tx) => {
6725
+ (await tx.doc(ProviderSettingsDoc, conversation.id)).enabled = list;
6726
+ }, context);
6727
+ enabled = new Set(list);
6728
+ },
6729
+ async defaultModel() {
6730
+ const saved = (await harness.snapshot(DefaultModelDoc, conversation.id, context))?.model ?? null;
6731
+ if (modelOverride)
6732
+ return { effective: modelOverride, source: "cli" };
6733
+ if (saved)
6734
+ return { effective: saved, source: "saved" };
6735
+ if (options.model)
6736
+ return { effective: model, source: "config" };
6737
+ return { effective: model, source: "default" };
6738
+ },
6739
+ async setDefaultModel(ref) {
6740
+ if (ref !== null) {
6741
+ if (!models.getModel(ref.provider, ref.modelId))
6742
+ throw new Error("Unknown model");
6743
+ if (isModelProvider(ref.provider) && !enabled.has(ref.provider))
6744
+ throw new Error(`PROVIDER_DISABLED: ${credentialProvider(ref.provider).name} is not enabled.`);
6745
+ }
6746
+ await harness.commit(async (tx) => {
6747
+ (await tx.doc(DefaultModelDoc, conversation.id)).model = ref;
6748
+ }, context);
6385
6749
  },
6386
6750
  async getAgent(id) {
6387
6751
  const session = await sessionFor(id);
@@ -6422,7 +6786,7 @@ async function createNekoRuntime(options = {}) {
6422
6786
  }
6423
6787
  };
6424
6788
  }
6425
- var DEFAULT_MODEL, DEFAULT_STORAGE_PATH, neverAborted = () => new AbortController().signal, ArchivedDoc, WorkspacesDoc, SearchSettingsDoc, ContextSettingsDoc, ActivityDoc;
6789
+ var DEFAULT_MODEL, DEFAULT_STORAGE_PATH, neverAborted = () => new AbortController().signal, ArchivedDoc, WorkspacesDoc, SearchSettingsDoc, ProviderSettingsDoc, DefaultModelDoc, ContextSettingsDoc, ActivityDoc;
6426
6790
  var init_runtime = __esm(() => {
6427
6791
  init_agent_file_search();
6428
6792
  init_compaction();
@@ -6451,6 +6815,7 @@ var init_runtime = __esm(() => {
6451
6815
  init_deepseek();
6452
6816
  init_web_search();
6453
6817
  init_tavily();
6818
+ init_windows_shell();
6454
6819
  init_workspace();
6455
6820
  init_workspace_files();
6456
6821
  DEFAULT_MODEL = { provider: "deepseek", modelId: "deepseek-flash" };
@@ -6479,6 +6844,22 @@ var init_runtime = __esm(() => {
6479
6844
  fork: "initial",
6480
6845
  initial: () => ({ provider: null })
6481
6846
  });
6847
+ ProviderSettingsDoc = defineDoc7({
6848
+ kind: "app.neko.provider-settings",
6849
+ version: 1,
6850
+ scope: "conversation",
6851
+ history: "latest",
6852
+ fork: "initial",
6853
+ initial: () => ({ enabled: null })
6854
+ });
6855
+ DefaultModelDoc = defineDoc7({
6856
+ kind: "app.neko.default-model",
6857
+ version: 1,
6858
+ scope: "conversation",
6859
+ history: "latest",
6860
+ fork: "initial",
6861
+ initial: () => ({ model: null })
6862
+ });
6482
6863
  ContextSettingsDoc = defineDoc7({
6483
6864
  kind: "app.neko.context-settings",
6484
6865
  version: 1,
@@ -6521,6 +6902,7 @@ var init_src = __esm(() => {
6521
6902
  init_projection();
6522
6903
  init_frontmatter();
6523
6904
  init_queued();
6905
+ init_rename_with_retry();
6524
6906
  init_runtime();
6525
6907
  init_search_providers();
6526
6908
  init_session_metadata();
@@ -6529,6 +6911,7 @@ var init_src = __esm(() => {
6529
6911
  init_todos();
6530
6912
  init_tool_approvals();
6531
6913
  init_user_questions();
6914
+ init_windows_shell();
6532
6915
  init_workspace();
6533
6916
  init_workspace_files();
6534
6917
  });
@@ -6654,7 +7037,7 @@ function contract(name, def) {
6654
7037
  ]));
6655
7038
  return remote(name, { ...def, host: hostNamed(GATEWAY_HOST), variants });
6656
7039
  }
6657
- var GATEWAY_HOST = "neko", rpc = (args, result) => [args, result], str, num, bool, nil, integer, nullableString, phase, level, thinking, model, 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, 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;
6658
7041
  var init_contract = __esm(() => {
6659
7042
  init_interactions();
6660
7043
  str = Type7.String();
@@ -6666,8 +7049,8 @@ var init_contract = __esm(() => {
6666
7049
  phase = Type7.Enum(["pending", "running", "done"]);
6667
7050
  level = Type7.Enum(["project", "user", "builtin"]);
6668
7051
  thinking = Type7.Enum(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
6669
- model = Type7.Object({ provider: str, modelId: str });
6670
- change = Type7.Object({ model: Type7.Optional(model), thinkingLevel: Type7.Optional(thinking) }, { additionalProperties: false });
7052
+ model2 = Type7.Object({ provider: str, modelId: str });
7053
+ change = Type7.Object({ model: Type7.Optional(model2), thinkingLevel: Type7.Optional(thinking) }, { additionalProperties: false });
6671
7054
  usage = Type7.Object({ input: num, output: num, cacheRead: num, cacheWrite: num, cost: num });
6672
7055
  image = Type7.Object({
6673
7056
  name: str,
@@ -6905,7 +7288,7 @@ var init_contract = __esm(() => {
6905
7288
  subagents: Type7.Optional(Type7.Array(DelegationSchema)),
6906
7289
  taskUsage: Type7.Optional(usage),
6907
7290
  model: str,
6908
- agent: Type7.Object({ model: Type7.Union([model, nil]), thinkingLevel: Type7.Union([thinking, nil]) }),
7291
+ agent: Type7.Object({ model: Type7.Union([model2, nil]), thinkingLevel: Type7.Union([thinking, nil]) }),
6909
7292
  cwd: str,
6910
7293
  busy: bool,
6911
7294
  status: nullableString,
@@ -6990,7 +7373,7 @@ var init_contract = __esm(() => {
6990
7373
  firstPromptPreview: str,
6991
7374
  createdAt: num,
6992
7375
  lastMessageAt: Type7.Union([num, nil]),
6993
- model: Type7.Union([model, nil]),
7376
+ model: Type7.Union([model2, nil]),
6994
7377
  status: Type7.Enum(["idle", "running", "error"]),
6995
7378
  awaitingAnswer: Type7.Optional(bool),
6996
7379
  id: str
@@ -7001,11 +7384,12 @@ var init_contract = __esm(() => {
7001
7384
  credentialStatus = Type7.Object({
7002
7385
  savedConfigured: bool,
7003
7386
  environmentConfigured: bool,
7004
- source: Type7.Enum(["saved", "environment", "runtime", "keyless", "none"])
7387
+ source: Type7.Enum(["saved", "environment", "runtime", "keyless", "none"]),
7388
+ enabled: Type7.Optional(bool)
7005
7389
  });
7006
7390
  CredentialStateSchema = Type7.Object({
7007
7391
  revision: integer,
7008
- statuses: Type7.Object({ deepseek: credentialStatus, tavily: credentialStatus })
7392
+ statuses: Type7.Record(str, credentialStatus)
7009
7393
  });
7010
7394
  AttachmentSchema = Type7.Object({ sessionId: str, attachmentId: str });
7011
7395
  chatInput = Type7.Object({ sessionId: str, attachmentId: str }, { additionalProperties: false });
@@ -7047,6 +7431,7 @@ var init_contract = __esm(() => {
7047
7431
  provider: str,
7048
7432
  modelId: str,
7049
7433
  name: str,
7434
+ available: bool,
7050
7435
  contextWindow: num,
7051
7436
  input: Type7.Array(str),
7052
7437
  inputLimits: Type7.Optional(inputLimits),
@@ -7152,16 +7537,17 @@ var init_contract = __esm(() => {
7152
7537
  }
7153
7538
  }
7154
7539
  });
7155
- provider = Type7.Enum(["deepseek", "tavily"]);
7540
+ provider = Type7.String({ minLength: 1, maxLength: 64 });
7156
7541
  NekoCredentials = contract("neko.credentials", {
7157
- version: 1,
7542
+ version: 2,
7158
7543
  input: Type7.Object({}, { additionalProperties: false }),
7159
7544
  variants: {
7160
7545
  ready: {
7161
7546
  state: { state: CredentialStateSchema },
7162
7547
  methods: {
7163
7548
  save: rpc(Type7.Object({ provider, key: Type7.String({ minLength: 1, maxLength: 8192 }) }, { additionalProperties: false }), nil),
7164
- remove: rpc(Type7.Object({ provider }, { additionalProperties: false }), nil)
7549
+ remove: rpc(Type7.Object({ provider }, { additionalProperties: false }), nil),
7550
+ setEnabled: rpc(Type7.Object({ provider, enabled: bool }, { additionalProperties: false }), nil)
7165
7551
  }
7166
7552
  }
7167
7553
  }
@@ -7237,6 +7623,24 @@ var init_contract = __esm(() => {
7237
7623
  }
7238
7624
  }
7239
7625
  });
7626
+ defaultModelSource = Type7.Enum(["cli", "saved", "config", "default"]);
7627
+ DefaultModelStateSchema = Type7.Object({
7628
+ effective: model2,
7629
+ source: defaultModelSource
7630
+ });
7631
+ NekoDefaultModel = contract("neko.default-model", {
7632
+ version: 1,
7633
+ input: Type7.Object({}, { additionalProperties: false }),
7634
+ variants: {
7635
+ ready: {
7636
+ state: { state: DefaultModelStateSchema },
7637
+ methods: {
7638
+ set: rpc(model2, nil),
7639
+ clear: rpc(nil, nil)
7640
+ }
7641
+ }
7642
+ }
7643
+ });
7240
7644
  FileCandidatesSchema = Type7.Object({
7241
7645
  entries: Type7.Array(Type7.Object({ name: str, path: str, kind: Type7.Enum(["file", "directory"]) }), { maxItems: 100 }),
7242
7646
  limited: bool,
@@ -7284,6 +7688,14 @@ var init_contract = __esm(() => {
7284
7688
  }
7285
7689
  }
7286
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
+ ]);
7287
7699
  ApplicationInfoSchema = Type7.Object({
7288
7700
  runningVersion: str,
7289
7701
  installedVersion: nullableString,
@@ -7291,7 +7703,8 @@ var init_contract = __esm(() => {
7291
7703
  latestVersion: nullableString,
7292
7704
  checkedAt: Type7.Union([integer, nil]),
7293
7705
  checkState: Type7.Enum(["unknown", "checking", "ok", "failed", "development"]),
7294
- serverId: str
7706
+ serverId: str,
7707
+ shell: Type7.Optional(ShellStatusSchema)
7295
7708
  });
7296
7709
  NekoApplicationInfo = contract("neko.application-info", {
7297
7710
  version: 1,
@@ -7672,8 +8085,8 @@ function createGatewayConnection(link, serverId) {
7672
8085
  let business;
7673
8086
  let protocolChecked = false;
7674
8087
  function bindBusiness() {
7675
- const directory = get(NekoSessionDirectory), management = get(NekoSessionManagement), models = get(NekoModels), workspaces = get(NekoWorkspaces), workspaceFiles = get(NekoWorkspaceFiles), credentials = get(NekoCredentials), searchSettings = get(NekoSearchSettings), contextSettings = get(NekoContextSettings), skillMarket = get(NekoSkillMarket);
7676
- keep.push(directory.state.subscribe(() => {}), workspaces.state.subscribe(() => {}), credentials.state.subscribe(() => {}), searchSettings.state.subscribe(() => {}), contextSettings.state.subscribe(() => {}));
8088
+ const directory = get(NekoSessionDirectory), management = get(NekoSessionManagement), models = get(NekoModels), workspaces = get(NekoWorkspaces), workspaceFiles = get(NekoWorkspaceFiles), credentials = get(NekoCredentials), searchSettings = get(NekoSearchSettings), contextSettings = get(NekoContextSettings), defaultModel = get(NekoDefaultModel), skillMarket = get(NekoSkillMarket);
8089
+ keep.push(directory.state.subscribe(() => {}), workspaces.state.subscribe(() => {}), credentials.state.subscribe(() => {}), searchSettings.state.subscribe(() => {}), contextSettings.state.subscribe(() => {}), defaultModel.state.subscribe(() => {}));
7677
8090
  return {
7678
8091
  directory,
7679
8092
  management,
@@ -7683,6 +8096,7 @@ function createGatewayConnection(link, serverId) {
7683
8096
  credentials,
7684
8097
  searchSettings,
7685
8098
  contextSettings,
8099
+ defaultModel,
7686
8100
  skillMarket
7687
8101
  };
7688
8102
  }
@@ -7709,6 +8123,7 @@ function createGatewayConnection(link, serverId) {
7709
8123
  credentials,
7710
8124
  searchSettings,
7711
8125
  contextSettings,
8126
+ defaultModel,
7712
8127
  workspaceFiles,
7713
8128
  skillMarket
7714
8129
  } = services();
@@ -7720,6 +8135,7 @@ function createGatewayConnection(link, serverId) {
7720
8135
  credentials.ready,
7721
8136
  searchSettings.ready,
7722
8137
  contextSettings.ready,
8138
+ defaultModel.ready,
7723
8139
  workspaceFiles.ready,
7724
8140
  skillMarket.ready
7725
8141
  ]), signal);
@@ -7763,6 +8179,9 @@ function createGatewayConnection(link, serverId) {
7763
8179
  get contextSettings() {
7764
8180
  return services().contextSettings;
7765
8181
  },
8182
+ get defaultModel() {
8183
+ return services().defaultModel;
8184
+ },
7766
8185
  get skillMarket() {
7767
8186
  return services().skillMarket;
7768
8187
  },
@@ -20022,6 +20441,10 @@ function formatTokens(tokens) {
20022
20441
  return `${(tokens / 1000).toFixed(1)}k`;
20023
20442
  return String(tokens);
20024
20443
  }
20444
+ function modelDisplayName(model) {
20445
+ const slash = model.indexOf("/");
20446
+ return slash < 0 || slash === model.length - 1 ? model : model.slice(slash + 1);
20447
+ }
20025
20448
 
20026
20449
  // packages/tui/src/labels.ts
20027
20450
  var commands, labels;
@@ -20036,13 +20459,13 @@ var init_labels = __esm(() => {
20036
20459
  { name: "exit", description: "Exit neko TUI" },
20037
20460
  { name: "new", description: "Create a new session" },
20038
20461
  { name: "sessions", description: "Switch or create a session" },
20039
- { name: "login", description: "Configure DeepSeek API key" },
20462
+ { name: "login", description: "Manage model providers and API keys" },
20040
20463
  { name: "model", description: "Choose a model" },
20041
20464
  { name: "agents", description: "View agent tasks and activity" },
20042
20465
  { name: "jobs", description: "View background jobs" },
20043
20466
  { name: "todos", description: "View the assistant's todo list" },
20044
20467
  { name: "question", description: "Answer the assistant's question (Ctrl+O)" },
20045
- { name: "search", description: "Choose the web search provider (Tavily/DeepSeek)" },
20468
+ { name: "search", description: "Choose the web search provider (Tavily/DeepSeek); K sets its key" },
20046
20469
  { name: "context", description: "Tune context compaction and tool result pruning" },
20047
20470
  { name: "theme", description: "Choose color theme (auto/light/dark)" },
20048
20471
  { name: "think", description: "Set thinking level (/think high)" },
@@ -20111,7 +20534,7 @@ class StatusFooter {
20111
20534
  const statusText = truncateToWidth2(s.compaction ?? s.busy ?? labels.ready, Math.min(28, Math.floor(space / 2)));
20112
20535
  const tone = this.#tone();
20113
20536
  const statusStyled = tone === "error" ? theme.error(statusText) : tone === "warning" ? theme.warning(statusText) : tone === "accent" ? theme.accent(statusText) : theme.ok(statusText);
20114
- const modelShort = s.model.split("/").at(-1) ?? s.model;
20537
+ const modelShort = modelDisplayName(s.model);
20115
20538
  const identity = [
20116
20539
  { text: basename4(s.cwd) || s.cwd, slot: theme.muted },
20117
20540
  { text: modelShort, slot: theme.model },
@@ -20660,16 +21083,21 @@ import {
20660
21083
  isKeyRelease as isKeyRelease2,
20661
21084
  matchesKey as matchesKey5,
20662
21085
  renderFakeCursor,
20663
- truncateToWidth as truncateToWidth3
21086
+ truncateToWidth as truncateToWidth3,
21087
+ visibleWidth as visibleWidth3
20664
21088
  } from "@earendil-works/pi-tui";
20665
21089
 
20666
21090
  class LoginPanel {
20667
21091
  height = 10;
20668
21092
  focused = false;
20669
21093
  open = false;
20670
- #input = new Input;
21094
+ #level = "list";
21095
+ #filter = new Input({ prompt: "\u276F ", placeholder: "Type to filter" });
21096
+ #index = 0;
21097
+ #inputs = new Map;
20671
21098
  #state;
20672
21099
  #provider = "deepseek";
21100
+ #group = "model";
20673
21101
  #message = "";
20674
21102
  #busy = false;
20675
21103
  #confirmingRemove = false;
@@ -20680,24 +21108,35 @@ class LoginPanel {
20680
21108
  #closed;
20681
21109
  #save;
20682
21110
  #remove;
21111
+ #toggle;
20683
21112
  #autoCloseMs;
20684
21113
  constructor(options) {
20685
21114
  this.#changed = options.changed;
20686
21115
  this.#closed = options.closed;
20687
21116
  this.#save = options.save;
20688
21117
  this.#remove = options.remove;
21118
+ this.#toggle = options.toggle;
20689
21119
  this.#autoCloseMs = options.autoCloseMs ?? AUTO_CLOSE_MS;
20690
21120
  }
20691
- show(state, subscribe) {
21121
+ show(state, subscribe, initial) {
20692
21122
  this.#generation++;
20693
21123
  this.#clearCloseTimer();
20694
21124
  this.open = true;
21125
+ this.#level = "list";
20695
21126
  this.#busy = false;
20696
21127
  this.#confirmingRemove = false;
20697
21128
  this.#message = "";
20698
- this.#input = new Input;
21129
+ this.#inputs.clear();
21130
+ this.#filter = new Input({ prompt: "\u276F ", placeholder: "Type to filter" });
21131
+ this.#index = 0;
20699
21132
  this.#provider = "deepseek";
21133
+ this.#group = "model";
20700
21134
  this.#state = state;
21135
+ if (initial) {
21136
+ this.#provider = initial.provider;
21137
+ this.#group = initial.group;
21138
+ this.#level = "key";
21139
+ }
20701
21140
  this.#unsubscribe?.();
20702
21141
  this.#unsubscribe = subscribe((value) => {
20703
21142
  if (this.open) {
@@ -20712,26 +21151,96 @@ class LoginPanel {
20712
21151
  this.#clearCloseTimer();
20713
21152
  this.open = false;
20714
21153
  this.#confirmingRemove = false;
20715
- this.#input = new Input;
21154
+ this.#inputs.clear();
21155
+ this.#filter = new Input({ prompt: "\u276F ", placeholder: "Type to filter" });
20716
21156
  this.#unsubscribe?.();
20717
21157
  this.#unsubscribe = undefined;
20718
21158
  this.#closed();
20719
21159
  this.#changed();
20720
21160
  }
20721
21161
  invalidate() {}
21162
+ get #input() {
21163
+ let input = this.#inputs.get(this.#provider);
21164
+ if (!input) {
21165
+ input = new Input;
21166
+ this.#inputs.set(this.#provider, input);
21167
+ }
21168
+ return input;
21169
+ }
21170
+ get #status() {
21171
+ return this.#state?.statuses[this.#provider];
21172
+ }
21173
+ #visible() {
21174
+ const query = compact(this.#filter.getValue());
21175
+ const all = listRows();
21176
+ if (query === "")
21177
+ return all;
21178
+ return all.filter(({ spec }) => compact(`${spec.name} ${spec.id} ${spec.envVar}`).includes(query));
21179
+ }
21180
+ #isEnabled(spec) {
21181
+ return isLocked(spec.id) || this.#state?.statuses[spec.id]?.enabled === true;
21182
+ }
20722
21183
  handleInput(data) {
20723
21184
  if (isKeyRelease2(data))
20724
21185
  return;
20725
- if (matchesKey5(data, "escape") || matchesKey5(data, "ctrl+c")) {
21186
+ if (matchesKey5(data, "ctrl+c")) {
21187
+ this.close();
21188
+ return;
21189
+ }
21190
+ if (this.#level === "list")
21191
+ this.#handleList(data);
21192
+ else
21193
+ this.#handleKey(data);
21194
+ this.#changed();
21195
+ }
21196
+ #handleList(data) {
21197
+ if (matchesKey5(data, "escape")) {
20726
21198
  this.close();
20727
21199
  return;
20728
21200
  }
21201
+ if (this.#busy)
21202
+ return;
21203
+ const items = this.#visible();
21204
+ if (matchesKey5(data, "up") || matchesKey5(data, "down")) {
21205
+ const delta = matchesKey5(data, "up") ? -1 : 1;
21206
+ this.#index = Math.max(0, Math.min(items.length - 1, this.#index + delta));
21207
+ this.#message = "";
21208
+ } else if (matchesKey5(data, "space")) {
21209
+ const row = items[this.#index];
21210
+ if (row)
21211
+ this.#toggleEnabled(row);
21212
+ } else if (matchesKey5(data, "enter")) {
21213
+ const row = items[this.#index];
21214
+ if (!row)
21215
+ return;
21216
+ this.#provider = row.spec.id;
21217
+ this.#group = row.group;
21218
+ this.#level = "key";
21219
+ this.#message = "";
21220
+ } else {
21221
+ const before = this.#filter.getValue();
21222
+ this.#filter.handleInput(data);
21223
+ if (this.#filter.getValue() !== before) {
21224
+ this.#index = 0;
21225
+ this.#message = "";
21226
+ }
21227
+ }
21228
+ }
21229
+ #handleKey(data) {
21230
+ if (matchesKey5(data, "escape")) {
21231
+ if (this.#busy || this.#closeTimer !== undefined)
21232
+ this.close();
21233
+ else
21234
+ this.#backToList();
21235
+ return;
21236
+ }
20729
21237
  if (this.#busy)
20730
21238
  return;
20731
21239
  const confirming = this.#confirmingRemove;
20732
21240
  this.#confirmingRemove = false;
20733
21241
  if (confirming && !matchesKey5(data, "ctrl+r"))
20734
21242
  this.#message = "";
21243
+ const input = this.#input;
20735
21244
  if (matchesKey5(data, "enter"))
20736
21245
  this.#apply(false);
20737
21246
  else if (matchesKey5(data, "ctrl+r") && this.#status?.savedConfigured) {
@@ -20741,23 +21250,52 @@ class LoginPanel {
20741
21250
  this.#confirmingRemove = true;
20742
21251
  this.#message = "Press Ctrl+R again to remove the saved key.";
20743
21252
  }
20744
- } else if (matchesKey5(data, "tab") || (matchesKey5(data, "left") || matchesKey5(data, "right")) && this.#input.getValue() === "")
20745
- this.#switch();
21253
+ } else if (matchesKey5(data, "tab"))
21254
+ this.#backToList();
20746
21255
  else {
20747
- this.#input.handleInput(data);
20748
- if (this.#input.getValue().length > 4096)
20749
- this.#input.setValue(this.#input.getValue().slice(0, 4096));
21256
+ input.handleInput(data);
21257
+ if (input.getValue().length > 4096)
21258
+ input.setValue(input.getValue().slice(0, 4096));
20750
21259
  }
20751
- this.#changed();
20752
- }
20753
- get #status() {
20754
- return this.#state?.statuses[this.#provider];
20755
21260
  }
20756
- #switch() {
20757
- const index = CREDENTIAL_PROVIDERS.findIndex((spec) => spec.id === this.#provider);
20758
- this.#provider = CREDENTIAL_PROVIDERS[(index + 1) % CREDENTIAL_PROVIDERS.length].id;
21261
+ #backToList() {
21262
+ const index = this.#visible().findIndex((row) => row.spec.id === this.#provider && row.group === this.#group);
21263
+ if (index >= 0)
21264
+ this.#index = index;
21265
+ this.#level = "list";
21266
+ this.#confirmingRemove = false;
20759
21267
  this.#message = "";
20760
21268
  }
21269
+ async#toggleEnabled({ spec, group }) {
21270
+ if (group === "search") {
21271
+ this.#message = isModelProvider(spec.id) ? `${spec.name} shares its key with the model provider and has no enable switch here.` : `${spec.name} is a web search credential and has no enable switch.`;
21272
+ return;
21273
+ }
21274
+ if (isLocked(spec.id)) {
21275
+ this.#message = `${spec.name} is always enabled.`;
21276
+ return;
21277
+ }
21278
+ if (!isModelProvider(spec.id))
21279
+ return;
21280
+ const enable = !this.#isEnabled(spec);
21281
+ const generation = this.#generation;
21282
+ this.#busy = true;
21283
+ this.#message = enable ? "Enabling..." : "Disabling...";
21284
+ this.#changed();
21285
+ try {
21286
+ await this.#toggle(spec.id, enable);
21287
+ if (generation === this.#generation)
21288
+ this.#message = enable ? `${spec.name} enabled. Press Enter to add a key.` : `${spec.name} disabled.`;
21289
+ } catch (error) {
21290
+ if (generation === this.#generation)
21291
+ this.#message = error instanceof Error ? error.message : "Could not update the provider. Try again.";
21292
+ } finally {
21293
+ if (generation === this.#generation) {
21294
+ this.#busy = false;
21295
+ this.#changed();
21296
+ }
21297
+ }
21298
+ }
20761
21299
  #clearCloseTimer() {
20762
21300
  if (this.#closeTimer !== undefined)
20763
21301
  clearTimeout(this.#closeTimer);
@@ -20796,7 +21334,7 @@ class LoginPanel {
20796
21334
  else
20797
21335
  await this.#save(provider, key);
20798
21336
  if (generation === this.#generation) {
20799
- this.#input = new Input;
21337
+ this.#inputs.set(provider, new Input);
20800
21338
  this.#message = remove ? "Saved key removed." : "Saved. Used by the next request.";
20801
21339
  this.#scheduleAutoClose(generation);
20802
21340
  }
@@ -20810,34 +21348,100 @@ class LoginPanel {
20810
21348
  }
20811
21349
  }
20812
21350
  }
21351
+ #frame(rows, help, width) {
21352
+ const body = Math.max(0, this.height - 1);
21353
+ while (rows.length < body)
21354
+ rows.push("");
21355
+ return [...rows.slice(0, body), help].map((line) => theme.panel(` ${truncateToWidth3(line, Math.max(1, width - 2), "...", true)} `));
21356
+ }
20813
21357
  render(width) {
21358
+ return this.#level === "list" ? this.#renderList(width) : this.#renderKey(width);
21359
+ }
21360
+ #renderList(width) {
21361
+ const w = Math.max(1, width - 2);
21362
+ const items = this.#visible();
21363
+ this.#index = Math.max(0, Math.min(this.#index, items.length - 1));
21364
+ const selected = items[this.#index];
21365
+ const rows = [theme.accent(style.bold("Model providers and API keys"))];
21366
+ if (this.height >= 4) {
21367
+ this.#filter.focused = this.focused;
21368
+ rows.push(...this.#filter.render(w).slice(0, 1));
21369
+ }
21370
+ const showStatus = this.height >= 6;
21371
+ const entries = [];
21372
+ let group;
21373
+ for (const row of items) {
21374
+ const next = row.group === "search" ? "Web search" : "Model providers";
21375
+ if (next !== group)
21376
+ entries.push({ header: next });
21377
+ group = next;
21378
+ entries.push({ row });
21379
+ }
21380
+ const count = Math.max(1, this.height - rows.length - 1 - (showStatus ? 1 : 0));
21381
+ const at = entries.findIndex((entry) => ("row" in entry) && entry.row === selected);
21382
+ const start = Math.max(0, Math.min(at - Math.floor(count / 2), entries.length - count));
21383
+ for (const entry of entries.slice(start, start + count)) {
21384
+ if ("header" in entry) {
21385
+ rows.push(theme.dim(` ${entry.header}`));
21386
+ continue;
21387
+ }
21388
+ const { spec, group: rowGroup } = entry.row;
21389
+ const status = this.#state?.statuses[spec.id];
21390
+ const enabled = this.#isEnabled(spec);
21391
+ const modelRow = rowGroup === "model";
21392
+ const mark = modelRow ? enabled ? "[x]" : "[ ]" : " ";
21393
+ const right = status ? `${status.source} \xB7 env ${status.environmentConfigured ? "set" : "unset"}` : "loading";
21394
+ const leftWidth = Math.max(1, w - 3 - visibleWidth3(right));
21395
+ const left = truncateToWidth3(`${mark} ${spec.name}${modelRow && isLocked(spec.id) ? " (always on)" : ""}`, leftWidth, "...");
21396
+ const label = `${left}${" ".repeat(Math.max(1, leftWidth - visibleWidth3(left) + 1))}${right}`;
21397
+ if (entry.row === selected)
21398
+ rows.push(theme.selection(`\u276F ${label}`));
21399
+ else
21400
+ rows.push(modelRow && !enabled ? theme.dim(` ${label}`) : ` ${label}`);
21401
+ }
21402
+ if (items.length === 0)
21403
+ rows.push("No matching providers");
21404
+ if (showStatus) {
21405
+ while (rows.length < this.height - 2)
21406
+ rows.push("");
21407
+ const hint = selected ? selected.group === "search" && isModelProvider(selected.spec.id) ? `Same key as the ${selected.spec.name} model provider. Environment variable: ${selected.spec.envVar}` : `Environment variable: ${selected.spec.envVar}` : "";
21408
+ rows.push(this.#message || (hint ? theme.dim(hint) : ""));
21409
+ }
21410
+ const help = this.#busy ? shortcutHints(["Esc", "Close"]) : shortcutHints(["\u2191\u2193", "Select"], ["Space", "Toggle"], ["Enter", "Key"], ["Esc", "Close"]);
21411
+ return this.#frame(rows, help, width);
21412
+ }
21413
+ #renderKey(width) {
20814
21414
  const state = this.#status;
20815
- const spec = credentialProvider(this.#provider);
20816
- const tabs = CREDENTIAL_PROVIDERS.map((item) => item.id === this.#provider ? `[${item.id}]` : ` ${item.id} `).join(" ");
21415
+ const spec = CREDENTIAL_PROVIDERS.find((item) => item.id === this.#provider) ?? CREDENTIAL_PROVIDERS[0];
20817
21416
  const source = state?.source === "keyless" ? "keyless (rate-limited)" : state?.source ?? "Loading...";
20818
21417
  const keyLength = this.#input.getValue().length;
20819
21418
  const marker = this.focused ? CURSOR_MARKER2 : "";
20820
21419
  const keyRow = keyLength > 0 ? `API Key: ********${marker}${this.focused ? renderFakeCursor(" ") : ""} (${keyLength} characters)` : `API Key: ${marker}${this.focused ? `${renderFakeCursor("(")}paste key)` : "(paste key)"}`;
20821
- const rows = [
20822
- `Provider: ${tabs}`,
20823
- `Environment: ${state ? state.environmentConfigured ? "Configured" : "Not configured" : "Loading..."}`,
20824
- `Active source: ${source}`,
20825
- keyRow,
20826
- this.#message || (spec.keyless ? "Without a key, Tavily runs in rate-limited keyless mode." : "Saved key overrides environment."),
20827
- spec.signupUrl
20828
- ];
20829
- const switchHint = keyLength > 0 ? ["Tab", "Switch"] : ["Left/Right/Tab", "Switch"];
20830
- const help = this.#busy ? shortcutHints(["Esc", "Close"]) : this.#confirmingRemove ? shortcutHints(["Ctrl+R", "Confirm remove"], ["Esc", "Cancel"]) : state?.savedConfigured ? shortcutHints(["Enter", "Save"], ["Ctrl+R", "Remove"], switchHint, ["Esc", "Cancel"]) : shortcutHints(["Enter", "Save"], switchHint, ["Esc", "Cancel"]);
20831
- while (rows.length < this.height - 1)
20832
- rows.push("");
20833
- return [...rows.slice(0, Math.max(0, this.height - 1)), help].map((line) => theme.panel(` ${truncateToWidth3(line, Math.max(1, width - 2), "...", true)} `));
20834
- }
20835
- }
20836
- var AUTO_CLOSE_MS = 800;
21420
+ const tag = this.#group === "search" ? isModelProvider(spec.id) ? "web search, same key as model provider" : "web search" : this.#isEnabled(spec) ? "enabled" : "disabled";
21421
+ const defaultMessage = spec.keyless ? `Without a key, ${spec.name} runs in rate-limited keyless mode.` : this.#group === "model" && !this.#isEnabled(spec) ? "Provider is disabled. Esc, then Space enables it." : "Saved key overrides environment.";
21422
+ const provider = `Provider: ${spec.name} (${tag})`;
21423
+ const message = this.#message || defaultMessage;
21424
+ const environment = `Environment: ${spec.envVar} ${state ? state.environmentConfigured ? "Configured" : "Not configured" : "Loading..."}`;
21425
+ const sourceRow = `Active source: ${source}`;
21426
+ const rows = this.height - 1 >= 6 ? [provider, environment, sourceRow, keyRow, message, spec.signupUrl] : [provider, keyRow, message, environment, sourceRow, spec.signupUrl];
21427
+ const help = this.#busy ? shortcutHints(["Esc", "Close"]) : this.#confirmingRemove ? shortcutHints(["Ctrl+R", "Confirm remove"], ["Esc", "Back"]) : state?.savedConfigured ? shortcutHints(["Enter", "Save"], ["Ctrl+R", "Remove"], ["Tab", "Providers"], ["Esc", "Back"]) : shortcutHints(["Enter", "Save"], ["Tab", "Providers"], ["Esc", "Back"]);
21428
+ return this.#frame(rows, help, width);
21429
+ }
21430
+ }
21431
+ var AUTO_CLOSE_MS = 800, isLocked = (id) => LOCKED_PROVIDERS.includes(id), SEARCH_ALIASES, listRows = () => {
21432
+ const all = CREDENTIAL_PROVIDERS.map((spec) => ({ spec, group: spec.kind === "search" ? "search" : "model" }));
21433
+ for (const id of SEARCH_ALIASES) {
21434
+ const spec = CREDENTIAL_PROVIDERS.find((item) => item.id === id);
21435
+ if (spec)
21436
+ all.push({ spec, group: "search" });
21437
+ }
21438
+ return all;
21439
+ }, compact = (value) => value.toLowerCase().replace(/\s+/g, "");
20837
21440
  var init_login = __esm(() => {
20838
21441
  init_src();
20839
21442
  init_theme();
20840
21443
  init_shortcut_hints();
21444
+ SEARCH_ALIASES = ["deepseek"];
20841
21445
  });
20842
21446
 
20843
21447
  // packages/tui/src/components/notice.ts
@@ -20861,7 +21465,7 @@ import {
20861
21465
  isKeyRelease as isKeyRelease3,
20862
21466
  matchesKey as matchesKey6,
20863
21467
  truncateToWidth as truncateToWidth4,
20864
- visibleWidth as visibleWidth3,
21468
+ visibleWidth as visibleWidth4,
20865
21469
  wrapTextWithAnsi as wrapTextWithAnsi3
20866
21470
  } from "@earendil-works/pi-tui";
20867
21471
 
@@ -20869,7 +21473,9 @@ class PickerPanel {
20869
21473
  pointerSelection = false;
20870
21474
  agentNavigation = false;
20871
21475
  emptyText;
21476
+ groupedFilter = false;
20872
21477
  listKey;
21478
+ help;
20873
21479
  search = new Input2({ prompt: "\u276F ", placeholder: "Type to filter" });
20874
21480
  #items = [];
20875
21481
  #index = 0;
@@ -20877,6 +21483,7 @@ class PickerPanel {
20877
21483
  #generation = 0;
20878
21484
  #state = "closed";
20879
21485
  #error = "";
21486
+ #hint = "";
20880
21487
  #title = "";
20881
21488
  #load;
20882
21489
  #pick;
@@ -20896,6 +21503,9 @@ class PickerPanel {
20896
21503
  set focused(value) {
20897
21504
  this.search.focused = value;
20898
21505
  }
21506
+ set hint(value) {
21507
+ this.#hint = value;
21508
+ }
20899
21509
  invalidate() {}
20900
21510
  async show(title, load, pick) {
20901
21511
  if (this.open)
@@ -20939,6 +21549,9 @@ class PickerPanel {
20939
21549
  this.pointerSelection = false;
20940
21550
  this.agentNavigation = false;
20941
21551
  this.emptyText = undefined;
21552
+ this.groupedFilter = false;
21553
+ this.help = undefined;
21554
+ this.#hint = "";
20942
21555
  this.listKey = undefined;
20943
21556
  ++this.#generation;
20944
21557
  for (const detail of this.#details)
@@ -20969,9 +21582,23 @@ class PickerPanel {
20969
21582
  #details = [];
20970
21583
  #filtered() {
20971
21584
  const query = this.search.getValue().toLowerCase();
20972
- if (query !== "")
21585
+ if (query === "")
21586
+ return this.#items;
21587
+ if (!this.groupedFilter)
20973
21588
  return this.#items.filter((item) => !item.header && this.#matches(item, query));
20974
- return this.#items;
21589
+ const result = [];
21590
+ let header;
21591
+ for (const item of this.#items) {
21592
+ if (item.header)
21593
+ header = item;
21594
+ else if (this.#matches(item, query)) {
21595
+ if (header)
21596
+ result.push(header);
21597
+ header = undefined;
21598
+ result.push(item);
21599
+ }
21600
+ }
21601
+ return result;
20975
21602
  }
20976
21603
  #matches(item, query) {
20977
21604
  return `${item.label} ${item.description ?? ""} ${item.searchText ?? ""}`.toLowerCase().includes(query);
@@ -21019,6 +21646,7 @@ class PickerPanel {
21019
21646
  }
21020
21647
  if (this.#state !== "ready")
21021
21648
  return;
21649
+ this.#hint = "";
21022
21650
  const items = this.#filtered();
21023
21651
  if (this.listKey?.(data, items[this.#index]?.value)) {
21024
21652
  this.changed();
@@ -21032,6 +21660,11 @@ class PickerPanel {
21032
21660
  const item = items[this.#index];
21033
21661
  if (!item || item.header)
21034
21662
  return;
21663
+ if (item.blockedHint) {
21664
+ this.#hint = item.blockedHint;
21665
+ this.changed();
21666
+ return;
21667
+ }
21035
21668
  const generation = this.#generation;
21036
21669
  this.#state = "applying";
21037
21670
  this.#pick(item.value).then((result) => {
@@ -21112,8 +21745,9 @@ class PickerPanel {
21112
21745
  }
21113
21746
  if (height >= 4)
21114
21747
  rows.push(...this.search.render(w).slice(0, 1));
21115
- const help = this.#state === "error" ? labels.retry : this.#state === "ready" ? this.agentNavigation ? shortcutHints(["\u2191\u2193", "Select"], ["\u2192/Enter", "Open"], ["\u2190", "Main"]) : labels.select : labels.close;
21116
- const available = Math.max(1, height - rows.length - 1);
21748
+ const help = this.#state === "error" ? labels.retry : this.#state === "ready" ? this.agentNavigation ? shortcutHints(["\u2191\u2193", "Select"], ["\u2192/Enter", "Open"], ["\u2190", "Main"]) : this.help ?? labels.select : labels.close;
21749
+ const hint = this.#state === "ready" && height >= 5 ? this.#hint : "";
21750
+ const available = Math.max(1, height - rows.length - 1 - (hint ? 1 : 0));
21117
21751
  if (this.#state !== "ready")
21118
21752
  rows.push(this.#state === "error" ? theme.error(`Error: ${this.#error}`) : this.#state === "applying" ? labels.applying : labels.loading);
21119
21753
  else {
@@ -21125,7 +21759,7 @@ class PickerPanel {
21125
21759
  this.#index = first;
21126
21760
  }
21127
21761
  const selected = items[this.#index];
21128
- const details = height >= 9 && selected?.description ? wrapTextWithAnsi3(selected.description, w).slice(0, 2) : [];
21762
+ const details = height >= 9 && !hint && selected?.description ? wrapTextWithAnsi3(selected.description, w).slice(0, 2) : [];
21129
21763
  const count = Math.max(1, available - details.length);
21130
21764
  const start = Math.max(0, Math.min(this.#index - Math.floor(count / 2), items.length - count));
21131
21765
  for (const [i, item] of items.slice(start, start + count).entries()) {
@@ -21134,9 +21768,9 @@ class PickerPanel {
21134
21768
  continue;
21135
21769
  }
21136
21770
  const right = item.rightLabel ?? "";
21137
- const leftWidth = Math.max(1, w - 3 - visibleWidth3(right));
21771
+ const leftWidth = Math.max(1, w - 3 - visibleWidth4(right));
21138
21772
  const left = truncateToWidth4(item.label, leftWidth, "...");
21139
- const label = right ? `${left}${" ".repeat(Math.max(1, leftWidth - visibleWidth3(left) + 1))}${right}` : left;
21773
+ const label = right ? `${left}${" ".repeat(Math.max(1, leftWidth - visibleWidth4(left) + 1))}${right}` : left;
21140
21774
  const rendered = start + i === this.#index ? theme.selection(`\u276F ${label}`) : ` ${label}`;
21141
21775
  if (this.pointerSelection)
21142
21776
  this.#pointerRows.set(rows.length, start + i);
@@ -21144,9 +21778,11 @@ class PickerPanel {
21144
21778
  }
21145
21779
  if (items.length === 0)
21146
21780
  rows.push(this.emptyText !== undefined && this.#items.length === 0 ? this.emptyText : labels.noResults);
21147
- while (rows.length < height - 1 - details.length)
21781
+ while (rows.length < height - 1 - details.length - (hint ? 1 : 0))
21148
21782
  rows.push("");
21149
21783
  rows.push(...details.map((line) => theme.dim(line)));
21784
+ if (hint)
21785
+ rows.push(theme.warning(hint));
21150
21786
  }
21151
21787
  while (rows.length < height - 1)
21152
21788
  rows.push("");
@@ -21899,10 +22535,151 @@ function followTerminalTheme(ui, apply) {
21899
22535
  }
21900
22536
  var THEME_QUERY_TIMEOUT_MS = 100;
21901
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
+
21902
22680
  // packages/tui/src/export-command.ts
21903
22681
  import { open as open3, unlink as unlink2 } from "fs/promises";
21904
- import { homedir as homedir6 } from "os";
21905
- import { resolve as resolve5 } from "path";
22682
+ import { resolve as resolve6 } from "path";
21906
22683
  function parseExportArgs(input) {
21907
22684
  const words = [];
21908
22685
  let word = "";
@@ -21952,7 +22729,7 @@ async function exportToFile(input) {
21952
22729
  const format = input.path === undefined ? "markdown" : exportFormatFromPath(input.path);
21953
22730
  if (format === null)
21954
22731
  throw new Error("Unsupported export format: use a .md, .html or .json path");
21955
- 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));
21956
22733
  let handle;
21957
22734
  try {
21958
22735
  handle = await open3(target, "wx");
@@ -21981,6 +22758,7 @@ async function exportToFile(input) {
21981
22758
  var EXPORT_USAGE = "Usage: /export [path.md|path.html|path.json] [--thinking]";
21982
22759
  var init_export_command = __esm(() => {
21983
22760
  init_conversation_export();
22761
+ init_image_input();
21984
22762
  });
21985
22763
 
21986
22764
  // packages/core/src/file-mentions.ts
@@ -22290,7 +23068,11 @@ var init_ws_listener = __esm(() => {
22290
23068
  async function connectNekoGateway(address, signal) {
22291
23069
  signal?.throwIfAborted();
22292
23070
  const link = address.transport === "unix" ? await createUnixLink(address.socketPath, signal) : createWsLink(address.url, signal);
22293
- 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);
22294
23076
  const controller = new AbortController;
22295
23077
  const abort = () => controller.abort(signal?.reason);
22296
23078
  signal?.addEventListener("abort", abort, { once: true });
@@ -22339,8 +23121,8 @@ var init_principal = __esm(() => {
22339
23121
 
22340
23122
  // packages/gateway/src/mobile/store.ts
22341
23123
  import { randomBytes } from "crypto";
22342
- import { chmod as chmod2, mkdir as mkdir5, readFile as readFile6, rename as rename3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
22343
- 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";
22344
23126
  function defaultState() {
22345
23127
  return {
22346
23128
  formatVersion: MOBILE_STATE_FORMAT_VERSION,
@@ -22460,9 +23242,10 @@ async function writeFileAtomic(filePath, contents, mode) {
22460
23242
  await writeFile3(tempPath, contents, { mode });
22461
23243
  if (POSIX)
22462
23244
  await chmod2(tempPath, mode);
22463
- await rename3(tempPath, filePath);
22464
- } finally {
23245
+ await renameWithRetry(tempPath, filePath);
23246
+ } catch (error) {
22465
23247
  await rm3(tempPath, { force: true });
23248
+ throw error;
22466
23249
  }
22467
23250
  }
22468
23251
  async function writeSecretFile(filePath, contents) {
@@ -22470,7 +23253,7 @@ async function writeSecretFile(filePath, contents) {
22470
23253
  }
22471
23254
  async function readSecretFile(filePath) {
22472
23255
  try {
22473
- return await readFile6(filePath, "utf8");
23256
+ return await readFile7(filePath, "utf8");
22474
23257
  } catch (error) {
22475
23258
  if (isNotFound(error))
22476
23259
  return;
@@ -22480,7 +23263,7 @@ async function readSecretFile(filePath) {
22480
23263
  async function loadState(statePath) {
22481
23264
  let raw;
22482
23265
  try {
22483
- raw = await readFile6(statePath, "utf8");
23266
+ raw = await readFile7(statePath, "utf8");
22484
23267
  } catch (error) {
22485
23268
  if (isNotFound(error))
22486
23269
  return freezeState(defaultState());
@@ -22504,21 +23287,21 @@ class MobileStore {
22504
23287
  queue = Promise.resolve();
22505
23288
  constructor(mobileDir, initial) {
22506
23289
  this.mobileDir = mobileDir;
22507
- this.tlsDir = join8(mobileDir, "tls");
22508
- this.statePath = join8(mobileDir, "state.json");
22509
- 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");
22510
23293
  this.current = initial;
22511
23294
  }
22512
23295
  static async open(nekoDir) {
22513
- const mobileDir = join8(nekoDir, "mobile");
22514
- const tlsDir = join8(mobileDir, "tls");
23296
+ const mobileDir = join9(nekoDir, "mobile");
23297
+ const tlsDir = join9(mobileDir, "tls");
22515
23298
  await mkdir5(mobileDir, { recursive: true, mode: 448 });
22516
23299
  await mkdir5(tlsDir, { recursive: true, mode: 448 });
22517
23300
  if (POSIX) {
22518
23301
  await chmod2(mobileDir, 448);
22519
23302
  await chmod2(tlsDir, 448);
22520
23303
  }
22521
- return new MobileStore(mobileDir, await loadState(join8(mobileDir, "state.json")));
23304
+ return new MobileStore(mobileDir, await loadState(join9(mobileDir, "state.json")));
22522
23305
  }
22523
23306
  getState() {
22524
23307
  return this.current;
@@ -22640,6 +23423,7 @@ class MobileStore {
22640
23423
  }
22641
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;
22642
23425
  var init_store = __esm(() => {
23426
+ init_src();
22643
23427
  POSIX = process.platform !== "win32";
22644
23428
  ADMIN_TOKEN_PATTERN = /^[A-Za-z0-9_-]{43}$/;
22645
23429
  CREDENTIAL_HASH_PATTERN = /^[0-9a-fA-F]{64}$/;
@@ -41082,7 +41866,7 @@ import {
41082
41866
  randomBytes as randomBytes2,
41083
41867
  X509Certificate
41084
41868
  } from "crypto";
41085
- import { join as join9 } from "path";
41869
+ import { join as join10 } from "path";
41086
41870
  function mobileCommonName(serverId) {
41087
41871
  return `neko-${serverId.slice(0, 8).toLowerCase()}.invalid`;
41088
41872
  }
@@ -41174,8 +41958,8 @@ class MobileCertificate {
41174
41958
  this.current = initial;
41175
41959
  }
41176
41960
  static async load(store, serverId) {
41177
- const certPath = join9(store.tlsDir, TLS_CERT_FILE);
41178
- 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);
41179
41963
  const certPem = await readSecretFile(certPath);
41180
41964
  const keyPem = await readSecretFile(keyPath);
41181
41965
  if (certPem === undefined && keyPem === undefined) {
@@ -41705,10 +42489,10 @@ var init_relay = __esm(() => {
41705
42489
  });
41706
42490
 
41707
42491
  // packages/gateway/src/mobile/wsl.ts
41708
- import { execFile as execFile2 } from "child_process";
41709
- 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";
41710
42494
  import { networkInterfaces as networkInterfaces2 } from "os";
41711
- import { delimiter, join as join10 } from "path";
42495
+ import { delimiter, join as join11 } from "path";
41712
42496
  function detectWsl(overrides = {}) {
41713
42497
  const deps = { ...defaultProbeDeps, ...overrides };
41714
42498
  if (deps.platform !== "linux")
@@ -41719,7 +42503,7 @@ function detectWsl(overrides = {}) {
41719
42503
  if (!inWsl)
41720
42504
  return;
41721
42505
  const candidates = [
41722
- ...(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")),
41723
42507
  POWERSHELL_FALLBACK
41724
42508
  ];
41725
42509
  return { powershell: candidates.find((candidate) => deps.isExecutable(candidate)) };
@@ -41808,7 +42592,7 @@ function primaryWslAddress(interfaces = networkInterfaces2()) {
41808
42592
  return eth0?.address ?? wslOwnAddresses(interfaces)[0];
41809
42593
  }
41810
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) => {
41811
- 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));
41812
42596
  });
41813
42597
  var init_wsl = __esm(() => {
41814
42598
  init_network();
@@ -41817,7 +42601,7 @@ var init_wsl = __esm(() => {
41817
42601
  env: process.env,
41818
42602
  readProcVersion() {
41819
42603
  try {
41820
- return readFileSync2("/proc/version", "utf8");
42604
+ return readFileSync3("/proc/version", "utf8");
41821
42605
  } catch {
41822
42606
  return;
41823
42607
  }
@@ -41889,6 +42673,10 @@ function toMessage(error) {
41889
42673
  }
41890
42674
  return String(error);
41891
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
+ }
41892
42680
 
41893
42681
  class MobileGateway {
41894
42682
  serverId;
@@ -42217,11 +43005,12 @@ class MobileGateway {
42217
43005
  connection.sendNotification(method, params);
42218
43006
  }
42219
43007
  advertisedAddress(localAddress) {
43008
+ const normalized = normalizeLocalAddress(localAddress);
42220
43009
  const state = this.stateValue;
42221
- if (state.status === "listening" && state.mode === "wsl-relay" && localAddress === state.bindAddress) {
43010
+ if (state.status === "listening" && state.mode === "wsl-relay" && normalized === state.bindAddress) {
42222
43011
  return state.address;
42223
43012
  }
42224
- return localAddress;
43013
+ return normalized;
42225
43014
  }
42226
43015
  requireStore() {
42227
43016
  if (this.storeHandle === undefined)
@@ -42479,10 +43268,10 @@ var init_listener = __esm(() => {
42479
43268
  });
42480
43269
 
42481
43270
  // packages/gateway/src/port-file.ts
42482
- import { readFile as readFile7, rm as rm4, writeFile as writeFile4 } from "fs/promises";
42483
- 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";
42484
43273
  function gatewayPortFilePath(nekoDir, serverId) {
42485
- return join11(nekoDir, `${serverId}.port`);
43274
+ return join12(nekoDir, `${serverId}.port`);
42486
43275
  }
42487
43276
  async function writeGatewayPortFile(nekoDir, serverId, wsUrl) {
42488
43277
  await writeFile4(gatewayPortFilePath(nekoDir, serverId), `${wsUrl}
@@ -42491,7 +43280,7 @@ async function writeGatewayPortFile(nekoDir, serverId, wsUrl) {
42491
43280
  async function readGatewayPortFile(nekoDir, serverId) {
42492
43281
  let content;
42493
43282
  try {
42494
- content = await readFile7(gatewayPortFilePath(nekoDir, serverId), "utf8");
43283
+ content = await readFile8(gatewayPortFilePath(nekoDir, serverId), "utf8");
42495
43284
  } catch {
42496
43285
  return;
42497
43286
  }
@@ -42838,7 +43627,7 @@ var init_subagent_host = __esm(() => {
42838
43627
 
42839
43628
  // packages/gateway/src/session-host.ts
42840
43629
  import { open as open4 } from "fs/promises";
42841
- import { basename as basename5, extname } from "path";
43630
+ import { basename as basename6, extname } from "path";
42842
43631
  import {
42843
43632
  ChordError as ChordError4,
42844
43633
  local as local2,
@@ -43033,7 +43822,7 @@ function serveSessions(host, runtime, context) {
43033
43822
  if (!info.isFile())
43034
43823
  throw new Error("Image must be a file");
43035
43824
  ctx.signal?.throwIfAborted();
43036
- id = uploads.value.begin({ name: basename5(target), mimeType, size: info.size });
43825
+ id = uploads.value.begin({ name: basename6(target), mimeType, size: info.size });
43037
43826
  let offset = 0;
43038
43827
  while (offset < info.size) {
43039
43828
  ctx.signal?.throwIfAborted();
@@ -43050,9 +43839,9 @@ function serveSessions(host, runtime, context) {
43050
43839
  ctx.signal?.throwIfAborted();
43051
43840
  uploads.value.finish(id);
43052
43841
  const raw = uploads.value.resolve([id])[0];
43053
- 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);
43054
43843
  ctx.signal?.throwIfAborted();
43055
- return { id, name: basename5(target) };
43844
+ return { id, name: basename6(target) };
43056
43845
  } catch (error) {
43057
43846
  if (id)
43058
43847
  uploads.value.release(id);
@@ -43111,8 +43900,6 @@ function serveSessions(host, runtime, context) {
43111
43900
  for (const id of input.images ?? [])
43112
43901
  uploads.value.release(id);
43113
43902
  } catch (error) {
43114
- if (error instanceof Error && error.message.startsWith("DEEPSEEK_KEY_REQUIRED:"))
43115
- throw new ChordError4("invalid", "DEEPSEEK_KEY_REQUIRED: Configure DeepSeek in /login or Web settings.");
43116
43903
  throw new ChordError4("invalid", error instanceof Error ? error.message : "Cannot submit message");
43117
43904
  }
43118
43905
  return null;
@@ -43248,7 +44035,7 @@ function strFromU8(dat, latin1) {
43248
44035
  } else {
43249
44036
  var _a = dutf8(dat), { s, r } = _a;
43250
44037
  if (r.length)
43251
- err(8);
44038
+ err2(8);
43252
44039
  return s;
43253
44040
  }
43254
44041
  }
@@ -43257,7 +44044,7 @@ function unzipSync(data, opts) {
43257
44044
  var e = data.length - 22;
43258
44045
  for (;b4(data, e) != 101010256; --e) {
43259
44046
  if (!e || data.length - e > 65558)
43260
- err(13);
44047
+ err2(13);
43261
44048
  }
43262
44049
  var c = b2(data, e + 8);
43263
44050
  if (!c)
@@ -43287,7 +44074,7 @@ function unzipSync(data, opts) {
43287
44074
  else if (c_2 == 8)
43288
44075
  files[fn] = inflateSync(data.subarray(b, b + sc), { out: new u8(su) });
43289
44076
  else
43290
- err(14, "unknown compression type " + c_2);
44077
+ err2(14, "unknown compression type " + c_2);
43291
44078
  }
43292
44079
  }
43293
44080
  return files;
@@ -43360,11 +44147,11 @@ var require2, _a, Worker2, isMarkedAsUntransferable, u8, u16, i32, fleb, fdeb, c
43360
44147
  if (e == null || e > v.length)
43361
44148
  e = v.length;
43362
44149
  return new u8(v.subarray(s, e));
43363
- }, ec, err = function(ind, msg, nt) {
44150
+ }, ec, err2 = function(ind, msg, nt) {
43364
44151
  var e = new Error(msg || ec[ind]);
43365
44152
  e.code = ind;
43366
44153
  if (Error.captureStackTrace)
43367
- Error.captureStackTrace(e, err);
44154
+ Error.captureStackTrace(e, err2);
43368
44155
  if (!nt)
43369
44156
  throw e;
43370
44157
  return e;
@@ -43396,7 +44183,7 @@ var require2, _a, Worker2, isMarkedAsUntransferable, u8, u16, i32, fleb, fdeb, c
43396
44183
  var s = shft(pos) + 4, l = dat[s - 4] | dat[s - 3] << 8, t = s + l;
43397
44184
  if (t > sl) {
43398
44185
  if (noSt)
43399
- err(0);
44186
+ err2(0);
43400
44187
  break;
43401
44188
  }
43402
44189
  if (resize)
@@ -43442,10 +44229,10 @@ var require2, _a, Worker2, isMarkedAsUntransferable, u8, u16, i32, fleb, fdeb, c
43442
44229
  lm = hMap(lt, lbt, 1);
43443
44230
  dm = hMap(dt, dbt, 1);
43444
44231
  } else
43445
- err(1);
44232
+ err2(1);
43446
44233
  if (pos > tbts) {
43447
44234
  if (noSt)
43448
- err(0);
44235
+ err2(0);
43449
44236
  break;
43450
44237
  }
43451
44238
  }
@@ -43458,11 +44245,11 @@ var require2, _a, Worker2, isMarkedAsUntransferable, u8, u16, i32, fleb, fdeb, c
43458
44245
  pos += c & 15;
43459
44246
  if (pos > tbts) {
43460
44247
  if (noSt)
43461
- err(0);
44248
+ err2(0);
43462
44249
  break;
43463
44250
  }
43464
44251
  if (!c)
43465
- err(2);
44252
+ err2(2);
43466
44253
  if (sym < 256)
43467
44254
  buf[bt++] = sym;
43468
44255
  else if (sym == 256) {
@@ -43477,7 +44264,7 @@ var require2, _a, Worker2, isMarkedAsUntransferable, u8, u16, i32, fleb, fdeb, c
43477
44264
  }
43478
44265
  var d = dm[bits16(dat, pos) & dms], dsym = d >> 4;
43479
44266
  if (!d)
43480
- err(3);
44267
+ err2(3);
43481
44268
  pos += d & 15;
43482
44269
  var dt = fd[dsym];
43483
44270
  if (dsym > 3) {
@@ -43486,7 +44273,7 @@ var require2, _a, Worker2, isMarkedAsUntransferable, u8, u16, i32, fleb, fdeb, c
43486
44273
  }
43487
44274
  if (pos > tbts) {
43488
44275
  if (noSt)
43489
- err(0);
44276
+ err2(0);
43490
44277
  break;
43491
44278
  }
43492
44279
  if (resize)
@@ -43495,7 +44282,7 @@ var require2, _a, Worker2, isMarkedAsUntransferable, u8, u16, i32, fleb, fdeb, c
43495
44282
  if (bt < dt) {
43496
44283
  var shift = dl - dt, dend = Math.min(dt, end);
43497
44284
  if (shift + bt < 0)
43498
- err(3);
44285
+ err2(3);
43499
44286
  for (;bt < dend; ++bt)
43500
44287
  buf[bt] = dict[shift + bt];
43501
44288
  }
@@ -43550,7 +44337,7 @@ var require2, _a, Worker2, isMarkedAsUntransferable, u8, u16, i32, fleb, fdeb, c
43550
44337
  }
43551
44338
  }
43552
44339
  if (z < 2)
43553
- err(13);
44340
+ err2(13);
43554
44341
  }
43555
44342
  return [sc, su, off, 0];
43556
44343
  };
@@ -43619,13 +44406,14 @@ var init_esm = __esm(() => {
43619
44406
 
43620
44407
  // packages/gateway/src/skill-market.ts
43621
44408
  import { createHash as createHash6, randomUUID as randomUUID4 } from "crypto";
43622
- 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";
43623
44410
  import { homedir as homedir7 } from "os";
43624
- import { dirname as dirname8, join as join12 } from "path";
44411
+ import { dirname as dirname8, join as join13 } from "path";
43625
44412
  function createSkillMarketClient(options) {
43626
44413
  const home = options.home ?? homedir7();
43627
44414
  const fetchImpl = options.fetchImpl ?? fetch;
43628
44415
  const now = options.now ?? Date.now;
44416
+ const placeRename = options.renameImpl;
43629
44417
  let base;
43630
44418
  try {
43631
44419
  base = new URL(options.marketUrl);
@@ -43730,6 +44518,8 @@ function createSkillMarketClient(options) {
43730
44518
  throw new SkillMarketError("market_unavailable", "Project-scope install needs a workspaceRoot");
43731
44519
  return projectSkillsDir(workspaceRoot);
43732
44520
  };
44521
+ const BACKUP_PREFIX = ".bak-";
44522
+ const UUID_LENGTH = 36;
43733
44523
  const sweepTempDirs = async (skillsDir) => {
43734
44524
  const parent = dirname8(skillsDir);
43735
44525
  let names = [];
@@ -43738,12 +44528,27 @@ function createSkillMarketClient(options) {
43738
44528
  } catch {
43739
44529
  return;
43740
44530
  }
43741
- 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
+ }
43742
44547
  };
43743
44548
  const install = async ({ name, scope, workspaceRoot, overwrite }) => {
43744
44549
  const skillsDir = skillsDirFor(scope, workspaceRoot);
43745
- const target = join12(skillsDir, name);
43746
- const exists = await stat3(target).catch(() => {
44550
+ const target = join13(skillsDir, name);
44551
+ const exists = await stat5(target).catch(() => {
43747
44552
  return;
43748
44553
  }) !== undefined;
43749
44554
  if (exists && overwrite !== true)
@@ -43751,24 +44556,34 @@ function createSkillMarketClient(options) {
43751
44556
  const skill = await detail(name);
43752
44557
  const files = unpack(skill, await download(skill));
43753
44558
  await sweepTempDirs(skillsDir);
43754
- const temp = join12(dirname8(skillsDir), `.tmp-${randomUUID4()}`);
44559
+ const temp = join13(dirname8(skillsDir), `.tmp-${randomUUID4()}`);
43755
44560
  try {
43756
44561
  for (const [relative, bytes] of files) {
43757
- const dest = join12(temp, ...relative.split("/"));
44562
+ const dest = join13(temp, ...relative.split("/"));
43758
44563
  await mkdir6(dirname8(dest), { recursive: true });
43759
44564
  await writeFile5(dest, bytes);
43760
44565
  }
43761
- if (exists)
43762
- await rm5(target, { recursive: true, force: true });
43763
44566
  await mkdir6(skillsDir, { recursive: true });
43764
- 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
+ }
43765
44580
  } finally {
43766
44581
  await rm5(temp, { recursive: true, force: true }).catch(() => {});
43767
44582
  }
43768
44583
  let shadowedBy;
43769
44584
  if (scope === "user" && workspaceRoot !== undefined && workspaceRoot !== "") {
43770
- const projectDir = join12(projectSkillsDir(workspaceRoot), name);
43771
- if (await stat3(projectDir).catch(() => {
44585
+ const projectDir = join13(projectSkillsDir(workspaceRoot), name);
44586
+ if (await stat5(projectDir).catch(() => {
43772
44587
  return;
43773
44588
  }) !== undefined)
43774
44589
  shadowedBy = "project";
@@ -43777,8 +44592,8 @@ function createSkillMarketClient(options) {
43777
44592
  };
43778
44593
  const uninstall = async ({ name, scope, workspaceRoot }) => {
43779
44594
  const skillsDir = skillsDirFor(scope, workspaceRoot);
43780
- const target = join12(skillsDir, name);
43781
- if (await stat3(target).catch(() => {
44595
+ const target = join13(skillsDir, name);
44596
+ if (await stat5(target).catch(() => {
43782
44597
  return;
43783
44598
  }) === undefined)
43784
44599
  throw new SkillMarketError("skill_not_installed", `Skill ${name} is not installed at ${scope} level`);
@@ -43809,7 +44624,7 @@ function createSkillMarketClient(options) {
43809
44624
  uninstall
43810
44625
  };
43811
44626
  }
43812
- 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) => {
43813
44628
  const row = raw ?? {};
43814
44629
  const metadata = row.metadata;
43815
44630
  const source = metadata !== null && typeof metadata === "object" ? metadata.source : undefined;
@@ -43841,8 +44656,8 @@ var init_skill_market = __esm(() => {
43841
44656
 
43842
44657
  // packages/gateway/src/workspaces.ts
43843
44658
  import { constants as constants3 } from "fs";
43844
- import { access, readdir as readdir3, stat as stat4 } from "fs/promises";
43845
- 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";
43846
44661
  async function browseWorkspaces(options) {
43847
44662
  const requested = options.path.replace(/^\/+([A-Za-z]:[\\/])/, "$1");
43848
44663
  if (process.platform === "win32" && (requested === "/" || requested === "\\")) {
@@ -43869,11 +44684,11 @@ async function browseWorkspaces(options) {
43869
44684
  let next = offset;
43870
44685
  for (;next < entries.length && directories.length < 100; next++) {
43871
44686
  const entry = entries[next];
43872
- const target = join13(path, entry.name);
44687
+ const target = join14(path, entry.name);
43873
44688
  if (!entry.isDirectory() && !entry.isSymbolicLink())
43874
44689
  continue;
43875
44690
  try {
43876
- if (!(await stat4(target)).isDirectory())
44691
+ if (!(await stat6(target)).isDirectory())
43877
44692
  continue;
43878
44693
  await access(target, constants3.R_OK | constants3.X_OK);
43879
44694
  directories.push({ name: entry.name, path: target });
@@ -43912,12 +44727,15 @@ async function createNekoServerServices(options) {
43912
44727
  const updates = new UpdateChecker(options.updates);
43913
44728
  await updates.local();
43914
44729
  const updateListeners = new Set;
43915
- const info = (value) => ({ ...value, serverId: options.serverId });
43916
- 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;
43917
44735
  const publishUpdates = () => {
43918
44736
  if (closed)
43919
44737
  return;
43920
- const next = JSON.stringify(updates.state);
44738
+ const next = JSON.stringify(info(updates.state));
43921
44739
  if (next === published)
43922
44740
  return;
43923
44741
  published = next;
@@ -43936,6 +44754,7 @@ async function createNekoServerServices(options) {
43936
44754
  variant: "ready",
43937
44755
  info: current,
43938
44756
  check: async () => {
44757
+ runtime.shellStatus({ refresh: true });
43939
44758
  await updates.check(true);
43940
44759
  publishUpdates();
43941
44760
  return info(updates.state);
@@ -43982,13 +44801,20 @@ async function createNekoServerServices(options) {
43982
44801
  status: info.status,
43983
44802
  awaitingAnswer: info.awaitingAnswer
43984
44803
  }));
43985
- const readStatuses = async () => Object.fromEntries(await Promise.all(CREDENTIAL_PROVIDERS.map(async (spec) => [spec.id, await runtime.credentialStatus(spec.id)])));
43986
- const [sessions, workspaces, statuses, search, contextSettings0] = await Promise.all([
44804
+ const readStatuses = async () => {
44805
+ const enabled = new Set(runtime.enabledProviders());
44806
+ return Object.fromEntries(await Promise.all(CREDENTIAL_PROVIDERS.map(async (spec) => {
44807
+ const status = await runtime.credentialStatus(spec.id);
44808
+ return [spec.id, spec.kind === "model" ? { ...status, enabled: enabled.has(spec.id) } : status];
44809
+ })));
44810
+ };
44811
+ const [sessions, workspaces, statuses, search, contextSettings0, defaultModel0] = await Promise.all([
43987
44812
  list(),
43988
44813
  runtime.listWorkspaces(),
43989
44814
  readStatuses(),
43990
44815
  runtime.searchSettings(),
43991
- runtime.contextSettings()
44816
+ runtime.contextSettings(),
44817
+ runtime.defaultModel()
43992
44818
  ]);
43993
44819
  let mutationTail = Promise.resolve();
43994
44820
  const serialize = (fn) => {
@@ -44005,6 +44831,7 @@ async function createNekoServerServices(options) {
44005
44831
  revision: 1,
44006
44832
  ...contextSettings0
44007
44833
  });
44834
+ const defaultModel = self2.state("defaultModel", DefaultModelStateSchema, defaultModel0);
44008
44835
  const refreshContextSettings = async (_input, ctx) => {
44009
44836
  const next = await runtime.contextSettings();
44010
44837
  const { revision, ...current } = contextSettings.value;
@@ -44012,6 +44839,12 @@ async function createNekoServerServices(options) {
44012
44839
  contextSettings.replace(ctx, { revision: revision + 1, ...next });
44013
44840
  return null;
44014
44841
  };
44842
+ const refreshDefaultModel = async (_input, ctx) => {
44843
+ const next = await runtime.defaultModel();
44844
+ if (JSON.stringify(next) !== JSON.stringify(defaultModel.value))
44845
+ defaultModel.replace(ctx, next);
44846
+ return null;
44847
+ };
44015
44848
  const refreshSearchSettings = async (_input, ctx) => {
44016
44849
  const next = await runtime.searchSettings();
44017
44850
  const current = searchSettings.value;
@@ -44040,6 +44873,7 @@ async function createNekoServerServices(options) {
44040
44873
  credentials,
44041
44874
  searchSettings,
44042
44875
  contextSettings,
44876
+ defaultModel,
44043
44877
  async refreshCredentials(_input, ctx) {
44044
44878
  credentials.replace(ctx, {
44045
44879
  revision: credentials.value.revision + 1,
@@ -44048,7 +44882,8 @@ async function createNekoServerServices(options) {
44048
44882
  return null;
44049
44883
  },
44050
44884
  refreshSearchSettings,
44051
- refreshContextSettings
44885
+ refreshContextSettings,
44886
+ refreshDefaultModel
44052
44887
  };
44053
44888
  }, { lifetime: "host" });
44054
44889
  const shared = (self2) => self2.get(ServerState, {});
@@ -44071,7 +44906,17 @@ async function createNekoServerServices(options) {
44071
44906
  remove: async (input, ctx) => {
44072
44907
  await runtime.removeCredential(input.provider);
44073
44908
  return hub.refreshCredentials(null, ctx);
44074
- }
44909
+ },
44910
+ setEnabled: (input, ctx) => serialize(async () => {
44911
+ if (!isModelProvider(input.provider))
44912
+ throw new ChordError5("invalid", "Unknown model provider.");
44913
+ try {
44914
+ await runtime.setProviderEnabled(input.provider, input.enabled);
44915
+ } catch (error) {
44916
+ throw new ChordError5("invalid", error instanceof Error ? error.message : "Cannot change provider.");
44917
+ }
44918
+ return hub.refreshCredentials(null, ctx);
44919
+ })
44075
44920
  };
44076
44921
  });
44077
44922
  host.serve(NekoSearchSettings, (_input, _principal, self2) => {
@@ -44100,9 +44945,24 @@ async function createNekoServerServices(options) {
44100
44945
  })
44101
44946
  };
44102
44947
  });
44948
+ host.serve(NekoDefaultModel, (_input, _principal, self2) => {
44949
+ const hub = shared(self2);
44950
+ return {
44951
+ variant: "ready",
44952
+ state: hub.defaultModel,
44953
+ set: (input, ctx) => serialize(async () => {
44954
+ await runtime.setDefaultModel(input);
44955
+ return hub.refreshDefaultModel(null, ctx);
44956
+ }),
44957
+ clear: (_input, ctx) => serialize(async () => {
44958
+ await runtime.setDefaultModel(null);
44959
+ return hub.refreshDefaultModel(null, ctx);
44960
+ })
44961
+ };
44962
+ });
44103
44963
  host.serve(NekoModels, () => ({
44104
44964
  variant: "ready",
44105
- list: async () => [...runtime.listModels()].map((value) => ({
44965
+ list: async () => (await runtime.listSelectableModels()).map((value) => ({
44106
44966
  ...value,
44107
44967
  input: [...value.input],
44108
44968
  supportedThinkingLevels: [...value.supportedThinkingLevels]
@@ -44194,23 +45054,24 @@ var init_server_services = __esm(() => {
44194
45054
  credentials: CredentialStateSchema,
44195
45055
  searchSettings: SearchSettingsStateSchema,
44196
45056
  contextSettings: ContextSettingsStateSchema,
45057
+ defaultModel: DefaultModelStateSchema,
44197
45058
  refreshCredentials: method2(Type12.Null(), Type12.Null()),
44198
45059
  refreshSearchSettings: method2(Type12.Null(), Type12.Null()),
44199
- refreshContextSettings: method2(Type12.Null(), Type12.Null())
45060
+ refreshContextSettings: method2(Type12.Null(), Type12.Null()),
45061
+ refreshDefaultModel: method2(Type12.Null(), Type12.Null())
44200
45062
  }
44201
45063
  });
44202
45064
  });
44203
45065
 
44204
45066
  // packages/gateway/src/gateway.ts
44205
45067
  import { randomUUID as randomUUID6 } from "crypto";
44206
- import { mkdir as mkdir7, readFile as readFile8, writeFile as writeFile6 } from "fs/promises";
44207
- 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";
44208
45070
  import { createHost as createHost2 } from "@earendil-works/chord";
44209
45071
  import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT4 } from "@earendil-works/chord/context";
44210
45072
  async function startGateway(options = {}) {
44211
45073
  const context = options.context ?? BACKGROUND_CONTEXT4;
44212
45074
  const fileConfig = loadNekoConfig(options.configDir);
44213
- const model = options.model ?? fileConfig.defaultModel ?? DEFAULT_MODEL;
44214
45075
  const storagePath = options.storagePath ?? fileConfig.storagePath ?? DEFAULT_STORAGE_PATH;
44215
45076
  const host = options.host ?? fileConfig.gateway?.host ?? DEFAULT_HOST;
44216
45077
  const port = options.port !== undefined ? options.port : fileConfig.gateway?.port ?? (process.platform === "win32" ? 0 : DEFAULT_PORT);
@@ -44218,7 +45079,7 @@ async function startGateway(options = {}) {
44218
45079
  const marketUrl = options.marketUrl ?? fileConfig.skills?.marketUrl ?? DEFAULT_SKILL_MARKET_URL;
44219
45080
  const nekoDir = dirname10(storagePath);
44220
45081
  const serverId = options.serverId ?? await loadOrCreateServerId(nekoDir);
44221
- 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`);
44222
45083
  const runtime = await createNekoRuntime({
44223
45084
  context,
44224
45085
  onReport: (error) => options.onError?.(error instanceof Error ? error : new Error(String(error))),
@@ -44228,12 +45089,15 @@ async function startGateway(options = {}) {
44228
45089
  ...options.apiKey !== undefined ? { apiKey: options.apiKey } : {},
44229
45090
  ...searchProvider !== undefined ? { searchProvider } : {},
44230
45091
  ...fileConfig.context !== undefined ? { contextSettings: fileConfig.context } : {},
44231
- model,
45092
+ ...fileConfig.defaultModel !== undefined ? { model: fileConfig.defaultModel } : {},
45093
+ ...options.model !== undefined ? { modelOverride: options.model } : {},
44232
45094
  thinkingLevel: options.thinkingLevel ?? fileConfig.thinkingLevel ?? "high",
44233
45095
  ...options.cwd !== undefined ? { cwd: options.cwd } : {},
44234
45096
  ...options.providers !== undefined ? { providers: options.providers } : {},
44235
45097
  ...options.extensions !== undefined ? { extensions: options.extensions } : {},
44236
- ...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 } : {}
44237
45101
  });
44238
45102
  try {
44239
45103
  return await bindGateway(runtime, {
@@ -44243,12 +45107,12 @@ async function startGateway(options = {}) {
44243
45107
  socketPath,
44244
45108
  serverId,
44245
45109
  nekoDir,
44246
- model,
44247
45110
  marketUrl,
44248
45111
  promptHome: options.promptHome,
44249
45112
  updates: options.updates,
44250
45113
  updateIntervalMs: options.updateIntervalMs,
44251
45114
  mobileServerName: options.mobileServerName,
45115
+ mobileNetwork: options.mobileNetwork,
44252
45116
  onError: options.onError
44253
45117
  });
44254
45118
  } catch (error) {
@@ -44283,12 +45147,13 @@ async function bindGateway(runtime, options) {
44283
45147
  wsListener = createWsListener({ host, port, onError: options.onError });
44284
45148
  listeners.push(wsListener);
44285
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
+ });
44286
45154
  try {
44287
45155
  for (const listener of listeners)
44288
- await listener.start((link) => chord.accept(link, {
44289
- principal: { kind: "local", connectionId: randomUUID6() },
44290
- onProtocolMismatch: ({ peer }) => options.onError?.(new Error(`Rejected a client on Chord protocol ${peer ?? "unknown"}; restart or update it.`))
44291
- }));
45156
+ await listener.start(acceptLocal);
44292
45157
  } catch (error) {
44293
45158
  await Promise.allSettled(listeners.map((listener) => listener.close()));
44294
45159
  await chord.close();
@@ -44304,6 +45169,7 @@ async function bindGateway(runtime, options) {
44304
45169
  serverId,
44305
45170
  serverName: options.mobileServerName,
44306
45171
  onError: options.onError,
45172
+ network: options.mobileNetwork,
44307
45173
  chord: {
44308
45174
  accept: (link, acceptOptions) => chord.accept(link, acceptOptions),
44309
45175
  invalidatePrincipal: (match) => chord.invalidatePrincipal(match)
@@ -44328,6 +45194,9 @@ async function bindGateway(runtime, options) {
44328
45194
  port: wsListener?.port,
44329
45195
  runtime,
44330
45196
  mobile,
45197
+ accept(link) {
45198
+ acceptLocal(link);
45199
+ },
44331
45200
  async close() {
44332
45201
  if (closing)
44333
45202
  return closing;
@@ -44368,16 +45237,16 @@ ${connectHint}`);
44368
45237
  return error instanceof Error ? error : new Error(message);
44369
45238
  }
44370
45239
  function serverIdFromSocketPath(socketPath) {
44371
- const name = basename6(socketPath);
45240
+ const name = basename7(socketPath);
44372
45241
  if (!name.endsWith(".sock"))
44373
45242
  return;
44374
45243
  const candidate = name.slice(0, -".sock".length);
44375
45244
  return isServerId(candidate) ? candidate : undefined;
44376
45245
  }
44377
45246
  async function loadOrCreateServerId(nekoDir) {
44378
- const file = join14(nekoDir, SERVER_ID_FILENAME);
45247
+ const file = join15(nekoDir, SERVER_ID_FILENAME);
44379
45248
  try {
44380
- const existing = (await readFile8(file, "utf8")).trim();
45249
+ const existing = (await readFile9(file, "utf8")).trim();
44381
45250
  if (existing !== "")
44382
45251
  return existing;
44383
45252
  } catch {}
@@ -44416,10 +45285,11 @@ function gatewayCommand(args) {
44416
45285
  async function stopGatewayProcess(child, options = {}) {
44417
45286
  if (child.pid === undefined || child.exitCode !== null || child.signalCode !== null)
44418
45287
  return;
45288
+ const viaStdin = options.viaStdin === true && child.stdin != null;
44419
45289
  await new Promise((resolve) => {
44420
45290
  let forcedTimer;
44421
45291
  const timer = setTimeout(() => {
44422
- child.kill("SIGKILL");
45292
+ child.kill(viaStdin ? undefined : "SIGKILL");
44423
45293
  forcedTimer = setTimeout(resolve, options.forcedMs ?? FORCED_STOP_MS);
44424
45294
  }, options.gracefulMs ?? GRACEFUL_STOP_MS);
44425
45295
  child.once("exit", () => {
@@ -44427,7 +45297,10 @@ async function stopGatewayProcess(child, options = {}) {
44427
45297
  clearTimeout(forcedTimer);
44428
45298
  resolve();
44429
45299
  });
44430
- child.kill("SIGTERM");
45300
+ if (viaStdin)
45301
+ child.stdin?.end();
45302
+ else
45303
+ child.kill("SIGTERM");
44431
45304
  });
44432
45305
  }
44433
45306
  var GRACEFUL_STOP_MS = 3000, FORCED_STOP_MS = 2000;
@@ -44457,14 +45330,15 @@ function installShutdown(options = {}) {
44457
45330
  exit(1);
44458
45331
  });
44459
45332
  };
44460
- const onSignal = () => {
45333
+ const begin = (reason) => {
44461
45334
  if (controller.signal.aborted) {
44462
45335
  options.log?.("\u518D\u6B21\u6536\u5230\u7EC8\u6B62\u4FE1\u53F7\uFF0C\u5F3A\u5236\u9000\u51FA\u3002");
44463
45336
  exit(130);
44464
45337
  return;
44465
45338
  }
44466
- options.log?.(`
44467
- \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`);
44468
45342
  controller.abort();
44469
45343
  timer = setTimeout(() => {
44470
45344
  options.log?.("\u5173\u95ED\u8D85\u65F6\uFF0C\u5F3A\u5236\u9000\u51FA\u3002");
@@ -44473,6 +45347,7 @@ function installShutdown(options = {}) {
44473
45347
  timer.unref?.();
44474
45348
  runCleanup();
44475
45349
  };
45350
+ const onSignal = () => begin(undefined);
44476
45351
  const installed = [];
44477
45352
  for (const signal of shutdownSignals(proc.platform)) {
44478
45353
  try {
@@ -44487,6 +45362,9 @@ function installShutdown(options = {}) {
44487
45362
  if (controller.signal.aborted)
44488
45363
  runCleanup();
44489
45364
  },
45365
+ trigger(reason) {
45366
+ begin(reason);
45367
+ },
44490
45368
  dispose() {
44491
45369
  clearTimeout(timer);
44492
45370
  for (const signal of installed)
@@ -44512,13 +45390,13 @@ var init_src2 = __esm(() => {
44512
45390
 
44513
45391
  // packages/tui/src/gateway.ts
44514
45392
  import { spawn as spawn3 } from "child_process";
44515
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
44516
- 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";
44517
45395
  async function connectToGateway(options = {}) {
44518
45396
  const config = loadNekoConfig(options.configDir);
44519
45397
  const storagePath = options.storagePath ?? config.storagePath ?? DEFAULT_STORAGE_PATH;
44520
45398
  const nekoDir = dirname11(storagePath);
44521
- const fileServerId = readServerId(join15(nekoDir, SERVER_ID_FILENAME2));
45399
+ const fileServerId = readServerId(join16(nekoDir, SERVER_ID_FILENAME2));
44522
45400
  const platform = options.platform ?? process.platform;
44523
45401
  if (platform === "win32") {
44524
45402
  if (fileServerId !== undefined) {
@@ -44535,8 +45413,8 @@ async function connectToGateway(options = {}) {
44535
45413
  }
44536
45414
  return spawnAndConnect({ ...options, platform, storagePath, socketPath: undefined, nekoDir, fileServerId });
44537
45415
  }
44538
- const socketPath = options.socketPath ?? config.gateway?.socketPath ?? (fileServerId !== undefined ? join15(nekoDir, `${fileServerId}.sock`) : undefined);
44539
- 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));
44540
45418
  if (socketPath !== undefined && existsSync3(socketPath)) {
44541
45419
  const serverId = serverIdFor(socketPath);
44542
45420
  if (serverId !== undefined) {
@@ -44553,7 +45431,9 @@ async function connectToGateway(options = {}) {
44553
45431
  }
44554
45432
  async function spawnAndConnect(plan) {
44555
45433
  const command = gatewayCommand(gatewayArgs(plan));
44556
- 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
+ });
44557
45437
  const outputTail = captureOutput(child);
44558
45438
  let spawnError;
44559
45439
  child.on("error", (error) => {
@@ -44562,7 +45442,7 @@ async function spawnAndConnect(plan) {
44562
45442
  try {
44563
45443
  return await connectSpawned(plan, child, outputTail, () => spawnError);
44564
45444
  } catch (error) {
44565
- await stopGatewayProcess(child);
45445
+ await stopGatewayProcess(child, { viaStdin: plan.platform === "win32" });
44566
45446
  throw error;
44567
45447
  }
44568
45448
  }
@@ -44570,7 +45450,7 @@ async function connectSpawned(plan, child, outputTail, spawnError) {
44570
45450
  const timeoutMs = plan.spawnTimeoutMs ?? 20000;
44571
45451
  if (plan.platform === "win32") {
44572
45452
  const timeoutAt = Date.now() + timeoutMs;
44573
- 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);
44574
45454
  let lastError = new Error("Gateway port file unavailable");
44575
45455
  while (Date.now() < timeoutAt) {
44576
45456
  if (child.exitCode !== null || child.signalCode !== null || spawnError() !== undefined) {
@@ -44591,7 +45471,7 @@ async function connectSpawned(plan, child, outputTail, spawnError) {
44591
45471
  spawned: true,
44592
45472
  async close() {
44593
45473
  await client.close().catch(() => {});
44594
- await stopGatewayProcess(child);
45474
+ await stopGatewayProcess(child, { viaStdin: plan.platform === "win32" });
44595
45475
  await removeGatewayPortFile(plan.nekoDir, serverId).catch(() => {});
44596
45476
  }
44597
45477
  };
@@ -44607,11 +45487,11 @@ async function connectSpawned(plan, child, outputTail, spawnError) {
44607
45487
  }
44608
45488
  let socketPath = plan.socketPath;
44609
45489
  if (socketPath === undefined) {
44610
- const generated = await waitForValue(() => readServerId(join15(plan.nekoDir, SERVER_ID_FILENAME2)), "gateway serverId file", timeoutMs, child, outputTail, spawnError);
44611
- 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`);
44612
45492
  }
44613
45493
  await waitForValue(() => existsSync3(socketPath) ? true : undefined, `gateway ready (socket: ${socketPath})`, timeoutMs, child, outputTail, spawnError);
44614
- 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));
44615
45495
  if (serverId === undefined) {
44616
45496
  throw new Error(`Cannot determine gateway serverId (socket: ${socketPath})`);
44617
45497
  }
@@ -44628,7 +45508,7 @@ async function connectSpawned(plan, child, outputTail, spawnError) {
44628
45508
  spawned: true,
44629
45509
  async close() {
44630
45510
  await client.close().catch(() => {});
44631
- await stopGatewayProcess(child);
45511
+ await stopGatewayProcess(child, { viaStdin: plan.platform === "win32" });
44632
45512
  }
44633
45513
  };
44634
45514
  } catch (error) {
@@ -44645,6 +45525,7 @@ function gatewayArgs(plan) {
44645
45525
  if (plan.platform === "win32") {
44646
45526
  return [
44647
45527
  "--no-socket",
45528
+ "--stop-on-stdin",
44648
45529
  "--storage",
44649
45530
  plan.storagePath,
44650
45531
  ...plan.credentialsPath ? ["--credentials", plan.credentialsPath] : [],
@@ -44663,14 +45544,14 @@ function gatewayArgs(plan) {
44663
45544
  }
44664
45545
  function readServerId(file) {
44665
45546
  try {
44666
- const value = readFileSync3(file, "utf8").trim();
45547
+ const value = readFileSync4(file, "utf8").trim();
44667
45548
  return isServerId(value) ? value : undefined;
44668
45549
  } catch {
44669
45550
  return;
44670
45551
  }
44671
45552
  }
44672
45553
  function serverIdFromSocketPath2(socketPath) {
44673
- const name = basename7(socketPath);
45554
+ const name = basename8(socketPath);
44674
45555
  if (!name.endsWith(".sock"))
44675
45556
  return;
44676
45557
  const candidate = name.slice(0, -".sock".length);
@@ -44742,8 +45623,8 @@ var init_gateway2 = __esm(() => {
44742
45623
  });
44743
45624
 
44744
45625
  // packages/tui/src/help-ui.ts
44745
- import { getKeybindings, matchesKey as matchesKey8, TUI_KEYBINDINGS, wrapTextWithAnsi as wrapTextWithAnsi4 } from "@earendil-works/pi-tui";
44746
- 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()) {
44747
45628
  const rows = [];
44748
45629
  const section = (title, hints) => {
44749
45630
  rows.push(theme.accent(title), ...hints.map(([key, description]) => shortcutHints([key, description])), "");
@@ -44756,7 +45637,7 @@ function helpLines(platform = process.platform) {
44756
45637
  ["Ctrl+C", "Stop while running; exit when idle"],
44757
45638
  ["Ctrl+S", "Open sessions"],
44758
45639
  ["Ctrl+O", "Open or close the assistant's question"],
44759
- [platform === "win32" ? "Alt+V" : "Ctrl+V", "Paste clipboard image or text"],
45640
+ [keyLabel(clipboardShortcutKey(platform, wsl)), "Paste clipboard image or text"],
44760
45641
  ["/", "Complete commands"],
44761
45642
  ["@", "Complete workspace files"],
44762
45643
  ["/skill:", "Complete installed skills"]
@@ -44815,7 +45696,8 @@ function helpLines(platform = process.platform) {
44815
45696
  ]);
44816
45697
  section("Web search", [
44817
45698
  ["/search", "Choose the web search provider"],
44818
- ["Enter", "Apply the highlighted provider"]
45699
+ ["Enter", "Apply the highlighted provider"],
45700
+ ["K", "Set the highlighted provider's API key (opens /login)"]
44819
45701
  ]);
44820
45702
  section("Context", [
44821
45703
  ["/context", "Tune compaction and tool result pruning"],
@@ -44874,13 +45756,13 @@ class HelpDetail {
44874
45756
  this.#offset = Math.max(0, Math.min(this.#max, this.#offset + delta));
44875
45757
  }
44876
45758
  handleKey(data) {
44877
- if (matchesKey8(data, "up"))
45759
+ if (matchesKey9(data, "up"))
44878
45760
  this.scroll(-1);
44879
- else if (matchesKey8(data, "down"))
45761
+ else if (matchesKey9(data, "down"))
44880
45762
  this.scroll(1);
44881
- else if (matchesKey8(data, "pageUp"))
45763
+ else if (matchesKey9(data, "pageUp"))
44882
45764
  this.scroll(-this.#page);
44883
- else if (matchesKey8(data, "pageDown"))
45765
+ else if (matchesKey9(data, "pageDown"))
44884
45766
  this.scroll(this.#page);
44885
45767
  else
44886
45768
  return false;
@@ -44897,117 +45779,11 @@ class HelpDetail {
44897
45779
  var keyLabel = (key) => key.split("+").map((part) => ({ escape: "Esc", pageUp: "PgUp", pageDown: "PgDn" })[part] ?? part[0].toUpperCase() + part.slice(1)).join("+");
44898
45780
  var init_help_ui = __esm(() => {
44899
45781
  init_shortcut_hints();
45782
+ init_image_input();
44900
45783
  init_labels();
44901
45784
  init_theme();
44902
45785
  });
44903
45786
 
44904
- // packages/tui/src/image-input.ts
44905
- import { execFile as execFile3 } from "child_process";
44906
- import { readFile as readFile9, stat as stat5 } from "fs/promises";
44907
- import { homedir as homedir8 } from "os";
44908
- import { basename as basename8, resolve as resolve6 } from "path";
44909
- import { promisify as promisify2 } from "util";
44910
- import { isKeyRelease as isKeyRelease4, isKeyRepeat as isKeyRepeat2, matchesKey as matchesKey9 } from "@earendil-works/pi-tui";
44911
- function imagePaths(text, platform = process.platform) {
44912
- const paths = [];
44913
- let word = "", quote = "", escaped = false;
44914
- for (const c of text.trim()) {
44915
- if (escaped) {
44916
- word += c;
44917
- escaped = false;
44918
- } else if (c === "\\" && quote !== "'" && platform !== "win32") {
44919
- escaped = true;
44920
- } else if (quote) {
44921
- if (c === quote)
44922
- quote = "";
44923
- else
44924
- word += c;
44925
- } else if (c === '"' || c === "'")
44926
- quote = c;
44927
- else if (/\s/.test(c)) {
44928
- if (word) {
44929
- paths.push(word);
44930
- word = "";
44931
- }
44932
- } else
44933
- word += c;
44934
- }
44935
- if (quote || escaped)
44936
- return null;
44937
- if (word)
44938
- paths.push(word);
44939
- return paths.length && paths.every((p) => /\.(png|jpe?g|webp)$/i.test(p)) ? paths : null;
44940
- }
44941
- async function readImageFile(path, cwd) {
44942
- const full = resolve6(cwd, path.startsWith("~/") ? homedir8() + path.slice(1) : path);
44943
- const info = await stat5(full);
44944
- if (!info.isFile() || info.size > 20 * 1024 * 1024)
44945
- throw new Error("Image must be a file no larger than 20 MiB");
44946
- const bytes = await readFile9(full);
44947
- const after = await stat5(full);
44948
- if (bytes.length !== info.size || after.size !== info.size || after.mtimeMs !== info.mtimeMs)
44949
- throw new Error("Image changed while reading");
44950
- const mimeType = /\.png$/i.test(full) ? "image/png" : /\.webp$/i.test(full) ? "image/webp" : "image/jpeg";
44951
- return { name: basename8(full), mimeType, bytes };
44952
- }
44953
- async function command(file, args, encoding = "utf8") {
44954
- return (await exec(file, args, { timeout: 2000, maxBuffer: 30 * 1024 * 1024, encoding })).stdout;
44955
- }
44956
- async function clipboard(options = {}) {
44957
- const run = options.command ?? command;
44958
- const platform = options.platform ?? process.platform;
44959
- if (platform === "darwin") {
44960
- const files = await run("osascript", [
44961
- "-e",
44962
- `try
44963
- set fs to the clipboard as \xABclass furl\xBB
44964
- return POSIX path of fs
44965
- on error
44966
- return ""
44967
- end try`
44968
- ]).catch(() => "");
44969
- if (String(files).trim())
44970
- return /\.(png|jpe?g|webp)$/i.test(String(files).trim()) ? { paths: [String(files).trim()] } : { text: String(files).trim() };
44971
- const image = String(await run("osascript", ["-e", `try
44972
- return the clipboard as \xABclass PNGf\xBB
44973
- on error
44974
- return ""
44975
- end try`]).catch(() => ""));
44976
- const hex = /\u00ABdata PNGf([0-9A-Fa-f]+)\u00BB/.exec(image)?.[1];
44977
- if (hex)
44978
- return { image: { name: "Clipboard.png", mimeType: "image/png", bytes: Buffer.from(hex, "hex") } };
44979
- return { text: String(await run("pbpaste", [])) };
44980
- }
44981
- if (platform === "win32") {
44982
- const value = String(await run("powershell.exe", [
44983
- "-NoProfile",
44984
- "-STA",
44985
- "-Command",
44986
- '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}'
44987
- ]));
44988
- if (value.startsWith("IMAGE:"))
44989
- return {
44990
- image: { name: "Clipboard.png", mimeType: "image/png", bytes: Buffer.from(value.slice(6).trim(), "base64") }
44991
- };
44992
- return { text: value };
44993
- }
44994
- const wayland = options.wayland ?? !!process.env.WAYLAND_DISPLAY;
44995
- const tool = wayland ? "wl-paste" : "xclip";
44996
- try {
44997
- const bytes = await run(tool, wayland ? ["--type", "image/png"] : ["-selection", "clipboard", "-t", "image/png", "-o"], "buffer");
44998
- if (bytes.length)
44999
- return { image: { name: "Clipboard.png", mimeType: "image/png", bytes: Buffer.from(bytes) } };
45000
- } catch {}
45001
- return { text: String(await run(tool, wayland ? ["--no-newline"] : ["-selection", "clipboard", "-o"])) };
45002
- }
45003
- function isClipboardShortcut(data, platform = process.platform) {
45004
- return !isKeyRelease4(data) && !isKeyRepeat2(data) && matchesKey9(data, platform === "win32" ? "alt+v" : "ctrl+v");
45005
- }
45006
- var exec;
45007
- var init_image_input = __esm(() => {
45008
- exec = promisify2(execFile3);
45009
- });
45010
-
45011
45787
  // packages/tui/src/question-dock.ts
45012
45788
  import {
45013
45789
  Input as Input3,
@@ -45015,7 +45791,7 @@ import {
45015
45791
  isKeyRepeat as isKeyRepeat3,
45016
45792
  matchesKey as matchesKey10,
45017
45793
  truncateToWidth as truncateToWidth6,
45018
- visibleWidth as visibleWidth4,
45794
+ visibleWidth as visibleWidth5,
45019
45795
  wrapTextWithAnsi as wrapTextWithAnsi5
45020
45796
  } from "@earendil-works/pi-tui";
45021
45797
  function settlementText(record, mine) {
@@ -45494,7 +46270,7 @@ class QuestionDock {
45494
46270
  `\u2753 \u7B49\u5F85\u56DE\u7B54 (${n}) \xB7 Ctrl+O \u6216 /question`,
45495
46271
  "\u2753 \u7B49\u5F85\u56DE\u7B54 \xB7 /question"
45496
46272
  ];
45497
- const text = candidates.find((candidate) => visibleWidth4(candidate) <= width - 1) ?? candidates.at(-1) ?? "";
46273
+ const text = candidates.find((candidate) => visibleWidth5(candidate) <= width - 1) ?? candidates.at(-1) ?? "";
45498
46274
  return theme.warning(truncateToWidth6(` ${text}`, width));
45499
46275
  }
45500
46276
  #panel(entries, width) {
@@ -45539,7 +46315,7 @@ class QuestionDock {
45539
46315
  ["", maxScroll > 0 ? range : ""],
45540
46316
  ["", ""]
45541
46317
  ].map(([a, b]) => title + theme.dim([a, b].filter(Boolean).map((part) => ` \xB7 ${part}`).join("")));
45542
- const rows = [headers.find((header) => visibleWidth4(header) <= width) ?? headers.at(-1) ?? title];
46318
+ const rows = [headers.find((header) => visibleWidth5(header) <= width) ?? headers.at(-1) ?? title];
45543
46319
  for (const line of bodyLines.slice(scroll, scroll + bodyRows))
45544
46320
  rows.push(`${theme.dim("\u2502 ")}${line}`);
45545
46321
  if (form) {
@@ -45619,7 +46395,7 @@ class QuestionDock {
45619
46395
  ];
45620
46396
  }
45621
46397
  const lines = candidates.map((parts) => ` ${shortcutHints(...parts)}`);
45622
- return lines.find((line) => visibleWidth4(line) <= width) ?? truncateToWidth6(lines.at(-1) ?? "", width);
46398
+ return lines.find((line) => visibleWidth5(line) <= width) ?? truncateToWidth6(lines.at(-1) ?? "", width);
45623
46399
  }
45624
46400
  }
45625
46401
  var QUESTION_MAX_ROWS = 14, QUESTION_MIN_ROWS = 6, SKIP_LABEL = "\u8DF3\u8FC7\u6B64\u95EE\u9898", QUESTION_KEY = "ctrl+o", canAutoOpen = (focus) => focus.online && focus.usable && focus.editorText === "" && !focus.autocomplete && !focus.overlayOpen && !focus.approvalFocused && !focus.sessionOperation && !focus.queueSelecting, keyOf = (session, id, version) => `${session}\x00${id}\x00${version}`, messageOf = (error) => error instanceof Error ? error.message : String(error);
@@ -45759,6 +46535,95 @@ var init_input_router = __esm(() => {
45759
46535
  init_question_dock();
45760
46536
  });
45761
46537
 
46538
+ // packages/tui/src/provider-ui.ts
46539
+ function providerSendBlock(model, credentials) {
46540
+ const status = model ? credentials?.statuses[model.provider] : undefined;
46541
+ if (!model || !status)
46542
+ return;
46543
+ const name = providerName(model.provider);
46544
+ if (status.enabled === false)
46545
+ return `${name} is disabled. Enable it with /login or switch model.`;
46546
+ if (status.source === "none")
46547
+ return `Configure ${name} with /login before sending.`;
46548
+ return;
46549
+ }
46550
+ function modelItems(models, current, defaultModel) {
46551
+ const currentKey = current ? modelKey(current) : undefined;
46552
+ const defaultKey = defaultModel ? modelKey(defaultModel) : undefined;
46553
+ const order = new Map(MODEL_PROVIDERS.map((spec, index) => [spec.id, index + 1]));
46554
+ const rank = (provider) => provider === current?.provider ? 0 : order.get(provider) ?? order.size + 1;
46555
+ const providers = [...new Set(models.map((model) => model.provider))].sort((a, b) => rank(a) - rank(b) || a.localeCompare(b));
46556
+ const items = [];
46557
+ for (const provider of providers) {
46558
+ const group = models.filter((model) => model.provider === provider).sort((a, b) => Number(modelKey(b) === currentKey) - Number(modelKey(a) === currentKey) || a.name.localeCompare(b.name));
46559
+ const name = providerName(provider);
46560
+ const available = group.some((model) => model.available);
46561
+ items.push({
46562
+ value: `header:${provider}`,
46563
+ label: `${name}${available ? "" : " \xB7 needs key"}`,
46564
+ header: true
46565
+ });
46566
+ for (const model of group) {
46567
+ const key = modelKey(model);
46568
+ const image = model.input.includes("image");
46569
+ const tags = [
46570
+ ...key === defaultKey ? ["default"] : [],
46571
+ ...model.available ? [] : ["needs key"],
46572
+ formatTokens(model.contextWindow),
46573
+ ...model.reasoning ? ["Thinking"] : [],
46574
+ ...image ? ["Images"] : []
46575
+ ];
46576
+ items.push({
46577
+ value: key,
46578
+ label: `${key}${key === currentKey ? " (Current)" : ""}${key === defaultKey ? " \u2605" : ""}`,
46579
+ rightLabel: tags.join(" \xB7 "),
46580
+ searchText: `${model.name} ${name}`,
46581
+ description: `${model.name} | ${formatTokens(model.contextWindow)} context${model.reasoning ? " | Thinking" : ""} | ${image ? "Images" : "Text only"}`,
46582
+ ...model.available ? {} : { blockedHint: needsKeyHint(provider) }
46583
+ });
46584
+ }
46585
+ }
46586
+ return items;
46587
+ }
46588
+ function openModelPanel(panel, client, current, applyModel, notify) {
46589
+ let models = [];
46590
+ const currentKey = current ? modelKey(current) : null;
46591
+ const effectiveDefault = () => client.defaultModel.state.value?.effective ?? null;
46592
+ const items = () => modelItems(models, current, effectiveDefault());
46593
+ const stop = client.defaultModel.state.subscribe(() => panel.update(items()));
46594
+ panel.groupedFilter = true;
46595
+ panel.help = shortcutHints(["\u2191\u2193", "Select"], ["Enter", "Open"], ["D", "Toggle default"], ["Esc", "Close"]);
46596
+ panel.listKey = (data, value) => {
46597
+ if (data.toLowerCase() !== "d" || panel.search.getValue() !== "" || value === undefined)
46598
+ return false;
46599
+ const model = models.find((m) => modelKey(m) === value);
46600
+ if (!model)
46601
+ return false;
46602
+ if (!model.available) {
46603
+ panel.hint = needsKeyHint(model.provider);
46604
+ return true;
46605
+ }
46606
+ const effective = effectiveDefault();
46607
+ const toggle = effective !== null && modelKey(effective) === value ? client.defaultModel.clear(null) : client.defaultModel.set({ provider: model.provider, modelId: model.modelId });
46608
+ toggle.catch((error) => notify(error instanceof Error ? error.message : String(error)));
46609
+ return true;
46610
+ };
46611
+ panel.show("Models", async () => {
46612
+ models = await client.models.list(null);
46613
+ return items();
46614
+ }, async (value) => {
46615
+ const selected = models.find((m) => modelKey(m) === value);
46616
+ if (selected?.available && value !== currentKey)
46617
+ await applyModel(selected);
46618
+ });
46619
+ return stop;
46620
+ }
46621
+ var modelKey = (model) => `${model.provider}/${model.modelId}`, providerName = (id) => CREDENTIAL_PROVIDERS.find((spec) => spec.id === id)?.name ?? id, isProviderRejection = (message) => /^(PROVIDER_KEY_REQUIRED|PROVIDER_DISABLED):/.test(message), needsKeyHint = (provider) => `${providerName(provider)} needs a key. Use /login to configure it.`;
46622
+ var init_provider_ui = __esm(() => {
46623
+ init_src();
46624
+ init_shortcut_hints();
46625
+ });
46626
+
45762
46627
  // packages/tui/src/queue-ui.ts
45763
46628
  import { matchesKey as matchesKey12, truncateToWidth as truncateToWidth7 } from "@earendil-works/pi-tui";
45764
46629
 
@@ -45866,10 +46731,10 @@ function searchItems(settings, credentials) {
45866
46731
  const status = credentials?.statuses[provider];
45867
46732
  return status ? SOURCES2[status.source] : "loading";
45868
46733
  };
45869
- const deepseekMissing = credentials?.statuses.deepseek.source === "none";
46734
+ const deepseekMissing = credentials?.statuses.deepseek?.source === "none";
45870
46735
  const descriptions = {
45871
46736
  tavily: `Tavily search (${source("tavily")}). Default`,
45872
- deepseek: deepseekMissing ? "No DeepSeek key. Run /login to set one before choosing it" : `DeepSeek search and fetch with your DeepSeek API key (${source("deepseek")})`
46737
+ deepseek: deepseekMissing ? "No DeepSeek key. Press k (or use /login) to set one before choosing it" : `DeepSeek search and fetch with your DeepSeek API key (${source("deepseek")})`
45873
46738
  };
45874
46739
  return SEARCH_PROVIDERS.map((value) => ({
45875
46740
  value,
@@ -45877,11 +46742,23 @@ function searchItems(settings, credentials) {
45877
46742
  description: descriptions[value]
45878
46743
  }));
45879
46744
  }
45880
- function openSearchPanel(panel, client, notify) {
46745
+ function openSearchPanel(panel, client, notify, openKey) {
45881
46746
  const items = () => searchItems(client.searchSettings.state.value, client.credentials.state.value);
45882
46747
  const refresh = () => panel.update(items());
45883
46748
  const stops = [client.searchSettings.state.subscribe(refresh), client.credentials.state.subscribe(refresh)];
45884
46749
  panel.emptyText = SEARCH_EMPTY;
46750
+ if (openKey) {
46751
+ panel.help = shortcutHints(["\u2191\u2193", "Select"], ["Enter", "Choose"], ["K", "Set key"], ["Esc", "Close"]);
46752
+ panel.listKey = (data, value) => {
46753
+ if (data.toLowerCase() !== "k" || panel.search.getValue() !== "" || value === undefined)
46754
+ return false;
46755
+ if (!SEARCH_PROVIDERS.includes(value))
46756
+ return false;
46757
+ panel.close();
46758
+ openKey(value);
46759
+ return true;
46760
+ };
46761
+ }
45885
46762
  panel.show("Web search", async () => items(), async (value) => {
45886
46763
  await client.searchSettings.set({ provider: value });
45887
46764
  notify(`Web search provider set to ${NAMES[value]}.`);
@@ -45894,6 +46771,7 @@ function openSearchPanel(panel, client, notify) {
45894
46771
  var SEARCH_EMPTY = "Search settings are unavailable.", NAMES, SOURCES2;
45895
46772
  var init_search_ui = __esm(() => {
45896
46773
  init_search_providers();
46774
+ init_shortcut_hints();
45897
46775
  NAMES = { tavily: "Tavily", deepseek: "DeepSeek" };
45898
46776
  SOURCES2 = {
45899
46777
  saved: "saved key",
@@ -45904,6 +46782,31 @@ var init_search_ui = __esm(() => {
45904
46782
  };
45905
46783
  });
45906
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
+
45907
46810
  // packages/tui/src/skill-market-ui.ts
45908
46811
  import { ChordError as ChordError6 } from "@earendil-works/chord";
45909
46812
  import { Markdown as Markdown3, matchesKey as matchesKey13, wrapTextWithAnsi as wrapTextWithAnsi6 } from "@earendil-works/pi-tui";
@@ -46424,7 +47327,7 @@ var init_subagents_ui = __esm(() => {
46424
47327
  });
46425
47328
 
46426
47329
  // packages/tui/src/todos-ui.ts
46427
- import { matchesKey as matchesKey15, truncateToWidth as truncateToWidth8, visibleWidth as visibleWidth5, wrapTextWithAnsi as wrapTextWithAnsi7 } from "@earendil-works/pi-tui";
47330
+ import { matchesKey as matchesKey15, truncateToWidth as truncateToWidth8, visibleWidth as visibleWidth6, wrapTextWithAnsi as wrapTextWithAnsi7 } from "@earendil-works/pi-tui";
46428
47331
  function todoProgressText(list) {
46429
47332
  if (!list || list.items.length === 0)
46430
47333
  return;
@@ -46487,7 +47390,7 @@ class TodosDetail {
46487
47390
  rows.push(row === 0 ? styled(item.status, glyph, `${number}${line}`) : theme.dim(`${indent}${line}`));
46488
47391
  });
46489
47392
  });
46490
- const legend = height >= 5 && visibleWidth5(LEGEND) <= width ? [theme.dim(LEGEND)] : [];
47393
+ const legend = height >= 5 && visibleWidth6(LEGEND) <= width ? [theme.dim(LEGEND)] : [];
46491
47394
  const pinned = offline.length + 1 + legend.length;
46492
47395
  const room = Math.max(1, height - pinned);
46493
47396
  this.#max = Math.max(0, rows.length - room);
@@ -46686,6 +47589,7 @@ class NekoTuiApp {
46686
47589
  #notices = new Container6;
46687
47590
  #versionNotice = new Container6;
46688
47591
  #versionStop;
47592
+ #shellStop;
46689
47593
  #versionTimer;
46690
47594
  #terminal = new ProcessTerminal;
46691
47595
  #panel;
@@ -46715,6 +47619,7 @@ class NekoTuiApp {
46715
47619
  #queueRow = new QueueRow;
46716
47620
  #searchStop;
46717
47621
  #contextStop;
47622
+ #modelStop;
46718
47623
  #agentDetails = new Map;
46719
47624
  #sessionId = "";
46720
47625
  #viewUnsubscribe;
@@ -46773,6 +47678,8 @@ class NekoTuiApp {
46773
47678
  this.#searchStop = undefined;
46774
47679
  this.#contextStop?.();
46775
47680
  this.#contextStop = undefined;
47681
+ this.#modelStop?.();
47682
+ this.#modelStop = undefined;
46776
47683
  clearInterval(this.#sessionPanelTimer);
46777
47684
  this.#sessionPanelTimer = undefined;
46778
47685
  this.#ui.setFocus(this.#question.open ? this.#question : this.#editor);
@@ -46801,6 +47708,9 @@ class NekoTuiApp {
46801
47708
  },
46802
47709
  remove: async (provider) => {
46803
47710
  await this.#gateway.client.credentials.remove({ provider });
47711
+ },
47712
+ toggle: async (provider, enabled) => {
47713
+ await this.#gateway.client.credentials.setEnabled({ provider, enabled });
46804
47714
  }
46805
47715
  });
46806
47716
  const complete = fileCompletion(this.#commands, options.cwd ?? process.cwd(), (text) => this.addNotice(text), (query, signal) => this.#gateway.client.host.root((ctx) => this.#gateway.client.workspaceFiles.search({ workspaceRoot: this.#lastView?.cwd ?? options.cwd ?? process.cwd(), query }, ctx), { signal }));
@@ -46849,7 +47759,7 @@ class NekoTuiApp {
46849
47759
  showAgents: () => this.#showAgents(),
46850
47760
  toggleQuestion: () => this.#toggleQuestion(),
46851
47761
  clipboardKey: (data) => {
46852
- if (!matchesKey17(data, process.platform === "win32" ? "alt+v" : "ctrl+v"))
47762
+ if (!matchesKey17(data, clipboardShortcutKey()))
46853
47763
  return false;
46854
47764
  if (isClipboardShortcut(data))
46855
47765
  this.pasteClipboard();
@@ -46938,15 +47848,20 @@ class NekoTuiApp {
46938
47848
  this.#ui.requestRender();
46939
47849
  };
46940
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);
46941
47854
  this.#versionTimer = setInterval(() => void refreshVersion(), 30000);
46942
47855
  refreshVersion();
46943
47856
  await this.switchSession(initialSessionId);
46944
47857
  this.#ui.start();
46945
47858
  this.#ui.requestRender(true);
47859
+ checkShell();
46946
47860
  loadAllHighlightLanguages();
46947
47861
  this.#startThemeDetection();
46948
47862
  await this.#exited;
46949
47863
  this.#versionStop?.();
47864
+ this.#shellStop?.();
46950
47865
  clearInterval(this.#versionTimer);
46951
47866
  this.#viewUnsubscribe?.();
46952
47867
  this.#directoryUnsubscribe?.();
@@ -47003,7 +47918,7 @@ class NekoTuiApp {
47003
47918
  chat.compact({ instructions }).catch((e) => {
47004
47919
  if (session === this.#sessionId) {
47005
47920
  this.#editor.setText(text);
47006
- this.addNotice(messageOf2(e), "error");
47921
+ this.addNotice(messageOf2(e), isProviderRejection(messageOf2(e)) ? "warning" : "error");
47007
47922
  }
47008
47923
  });
47009
47924
  return;
@@ -47017,14 +47932,7 @@ class NekoTuiApp {
47017
47932
  return;
47018
47933
  }
47019
47934
  if (trimmed === "/login") {
47020
- if (!this.#panel.open && !this.#login.open) {
47021
- const service = this.#gateway.client.credentials;
47022
- this.#login.show(service.state.value, (fn) => service.state.subscribe((value) => {
47023
- if (value)
47024
- fn(value);
47025
- }));
47026
- this.#ui.setFocus(this.#login);
47027
- }
47935
+ this.#showLogin();
47028
47936
  return;
47029
47937
  }
47030
47938
  if (trimmed.startsWith("/login ")) {
@@ -47080,9 +47988,10 @@ class NekoTuiApp {
47080
47988
  const chat = this.#chat;
47081
47989
  if (chat === undefined)
47082
47990
  return;
47083
- if (this.#lastView?.agent.model?.provider === "deepseek" && this.#gateway.client.credentials.state.value.statuses.deepseek.source === "none") {
47991
+ const blocked = providerSendBlock(this.#lastView?.agent.model, this.#gateway.client.credentials.state.value);
47992
+ if (blocked) {
47084
47993
  this.#editor.setText(text);
47085
- this.addNotice("Configure DeepSeek with /login before sending.", "warning");
47994
+ this.addNotice(blocked, "warning");
47086
47995
  return;
47087
47996
  }
47088
47997
  this.sendImages(text);
@@ -47163,8 +48072,10 @@ class NekoTuiApp {
47163
48072
  this.#imageDrafts.get(session)?.delete(ref);
47164
48073
  this.#retryRequests.delete(session);
47165
48074
  } catch (e) {
47166
- if (session === this.#sessionId)
47167
- this.addNotice(messageOf2(e), "error");
48075
+ if (session === this.#sessionId) {
48076
+ const message = messageOf2(e);
48077
+ this.addNotice(message, isProviderRejection(message) ? "warning" : "error");
48078
+ }
47168
48079
  } finally {
47169
48080
  for (const id of ids)
47170
48081
  await chat.releaseImage(id).catch(() => {});
@@ -47345,22 +48256,10 @@ class NekoTuiApp {
47345
48256
  return;
47346
48257
  const chat = this.#chat;
47347
48258
  const current = this.#lastView?.agent.model;
47348
- const currentKey = current ? `${current.provider}/${current.modelId}` : null;
47349
- let models = [];
47350
- const ready = this.#panel.show("Models", async () => {
47351
- models = await this.#gateway.client.models.list(null);
47352
- return [...models].sort((a, b) => Number(keyOf2(b) === currentKey) - Number(keyOf2(a) === currentKey)).map((model) => ({
47353
- value: keyOf2(model),
47354
- label: `${model.name}${keyOf2(model) === currentKey ? " (Current)" : ""}`,
47355
- description: `${keyOf2(model)} | ${formatContextWindow(model.contextWindow)} context${model.reasoning ? " | Thinking" : ""} | ${model.input.includes("image") ? "Images" : "Text only"}`
47356
- }));
47357
- }, async (value) => {
47358
- const selected = models.find((m) => keyOf2(m) === value);
47359
- if (selected && value !== currentKey)
47360
- await chat?.configure({ model: { provider: selected.provider, modelId: selected.modelId } });
47361
- });
48259
+ this.#modelStop = openModelPanel(this.#panel, this.#gateway.client, current, async (selected) => {
48260
+ await chat?.configure({ model: { provider: selected.provider, modelId: selected.modelId } });
48261
+ }, (message) => this.addNotice(message, "error"));
47362
48262
  this.#ui.setFocus(this.#panel);
47363
- await ready;
47364
48263
  }
47365
48264
  async openThemeOverlay() {
47366
48265
  if (this.#exitStarted || this.#panel.open || this.#login.open)
@@ -47402,8 +48301,12 @@ class NekoTuiApp {
47402
48301
  return;
47403
48302
  try {
47404
48303
  const model = (await this.#gateway.client.models.list(null)).find((m) => m.provider === ref.provider && m.modelId === ref.modelId);
47405
- if (chat !== this.#chat || this.#exitStarted || this.#panel.open || this.#login.open || !model)
48304
+ if (chat !== this.#chat || this.#exitStarted || this.#panel.open || this.#login.open)
48305
+ return;
48306
+ if (!model) {
48307
+ this.addNotice("The current model is unavailable. Choose another with /model.", "warning");
47406
48308
  return;
48309
+ }
47407
48310
  if (raw) {
47408
48311
  const level = model.supportedThinkingLevels.find((value) => value === raw);
47409
48312
  if (!level) {
@@ -47562,10 +48465,20 @@ class NekoTuiApp {
47562
48465
  });
47563
48466
  return { consume: true };
47564
48467
  }
48468
+ #showLogin(initial) {
48469
+ if (this.#panel.open || this.#login.open)
48470
+ return;
48471
+ const service = this.#gateway.client.credentials;
48472
+ this.#login.show(service.state.value, (fn) => service.state.subscribe((value) => {
48473
+ if (value)
48474
+ fn(value);
48475
+ }), initial);
48476
+ this.#ui.setFocus(this.#login);
48477
+ }
47565
48478
  #showSearch() {
47566
48479
  if (this.#exitStarted || this.#panel.open || this.#login.open)
47567
48480
  return;
47568
- this.#searchStop = openSearchPanel(this.#panel, this.#gateway.client, (message) => this.addNotice(message));
48481
+ this.#searchStop = openSearchPanel(this.#panel, this.#gateway.client, (message) => this.addNotice(message), (provider) => this.#showLogin({ provider, group: "search" }));
47569
48482
  this.#ui.setFocus(this.#panel);
47570
48483
  }
47571
48484
  #showContext() {
@@ -47655,7 +48568,8 @@ class NekoTuiApp {
47655
48568
  if (this.#imageWarningModel !== imageModel && view.items.some((i) => (i.type === "user" || i.type === "tool") && i.images?.length)) {
47656
48569
  this.#imageWarningModel = imageModel;
47657
48570
  this.#gateway.client.models.list(null).then((models) => {
47658
- if (this.#imageWarningModel === imageModel && !models.find((m) => m.provider === view.agent.model?.provider && m.modelId === view.agent.model?.modelId)?.input.includes("image"))
48571
+ const model = models.find((m) => m.provider === view.agent.model?.provider && m.modelId === view.agent.model?.modelId);
48572
+ if (this.#imageWarningModel === imageModel && model && !model.input.includes("image"))
47659
48573
  this.addNotice("Text-only model: previous images are unavailable to the model; written analysis is retained.", "warning");
47660
48574
  }).catch(() => {});
47661
48575
  }
@@ -47849,7 +48763,7 @@ class NekoTuiApp {
47849
48763
  #syncWelcome(view) {
47850
48764
  const empty = view.items.length === 0 && view.streaming === null && !view.busy;
47851
48765
  if (empty) {
47852
- const modelShort = view.model.split("/").at(-1) ?? view.model;
48766
+ const modelShort = modelDisplayName(view.model);
47853
48767
  this.#welcome.setModelLine(view.agent.thinkingLevel !== null ? `${modelShort} \xB7 Think ${view.agent.thinkingLevel}` : modelShort);
47854
48768
  if (!this.#welcomeAttached) {
47855
48769
  this.#imageTranscript.addChild(this.#welcome);
@@ -47957,12 +48871,6 @@ function hiddenCommands(view) {
47957
48871
  hidden.add("question");
47958
48872
  return hidden;
47959
48873
  }
47960
- function keyOf2(model) {
47961
- return `${model.provider}/${model.modelId}`;
47962
- }
47963
- function formatContextWindow(tokens) {
47964
- return formatTokens(tokens);
47965
- }
47966
48874
  async function runTui(options = {}) {
47967
48875
  options = { ...options, cwd: normalizeWorkspace(options.cwd) };
47968
48876
  const gateway = await connectToGateway({
@@ -47986,7 +48894,7 @@ async function initialSessionId(gateway, cwd) {
47986
48894
  return first.id;
47987
48895
  return (await gateway.client.management.create({ ...cwd !== undefined ? { cwd } : {} })).id;
47988
48896
  }
47989
- var EXIT_COMMANDS, SESSIONS_COMMANDS, NEW_SESSION_VALUE = "__new__";
48897
+ var EXIT_COMMANDS, SESSIONS_COMMANDS, NEW_SESSION_VALUE = "__new__", keyOf2;
47990
48898
  var init_tui = __esm(() => {
47991
48899
  init_src();
47992
48900
  init_application_updates();
@@ -48016,6 +48924,7 @@ var init_tui = __esm(() => {
48016
48924
  init_jobs_ui();
48017
48925
  init_labels();
48018
48926
  init_layout();
48927
+ init_provider_ui();
48019
48928
  init_question_dock();
48020
48929
  init_queue_ui();
48021
48930
  init_search_ui();
@@ -48027,6 +48936,7 @@ var init_tui = __esm(() => {
48027
48936
  init_workspace2();
48028
48937
  EXIT_COMMANDS = new Set(["/exit", "/quit"]);
48029
48938
  SESSIONS_COMMANDS = new Set(["/sessions", "/session"]);
48939
+ keyOf2 = modelKey;
48030
48940
  });
48031
48941
 
48032
48942
  // packages/tui/src/main.ts
@@ -48110,7 +49020,7 @@ var USAGE = `\u7528\u6CD5: neko tui [\u9009\u9879]
48110
49020
 
48111
49021
  \u6062\u590D\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\u6700\u8FD1\u4F7F\u7528\u7684\u4F1A\u8BDD\uFF0C\u4E0D\u5B58\u5728\u5219\u521B\u5EFA\u3002
48112
49022
  \u6309\u9700\u62C9\u8D77\u7F51\u5173\uFF0C\u5C5E\u4E3B TUI \u9000\u51FA\u65F6\u968F\u4E4B\u505C\u6B62\u3002
48113
- \u53EF\u7528 /login \u6216 DEEPSEEK_API_KEY \u914D\u7F6E\u51ED\u636E\uFF1B/search \u9009\u62E9\u8054\u7F51\u641C\u7D22\u670D\u52A1\uFF08tavily / deepseek\uFF0C\u9ED8\u8BA4 tavily\uFF09\uFF1B/context \u8C03\u6574\u4E0A\u4E0B\u6587\u538B\u7F29\u4E0E\u5DE5\u5177\u7ED3\u679C\u88C1\u526A\u3002`;
49023
+ \u53EF\u7528 /login \u6216 DEEPSEEK_API_KEY \u7B49\u73AF\u5883\u53D8\u91CF\u914D\u7F6E\u51ED\u636E\uFF08\u9664 DeepSeek \u5916\u7684 provider \u9700\u5148\u5728 /login \u542F\u7528\uFF09\uFF1B/search \u9009\u62E9\u8054\u7F51\u641C\u7D22\u670D\u52A1\uFF08tavily / deepseek\uFF0C\u9ED8\u8BA4 tavily\uFF09\uFF1B/context \u8C03\u6574\u4E0A\u4E0B\u6587\u538B\u7F29\u4E0E\u5DE5\u5177\u7ED3\u679C\u88C1\u526A\uFF1B/model \u4E2D\u53EF\u628A\u53EF\u9009\u6A21\u578B\u300C\u8BBE\u4E3A\u9ED8\u8BA4\u300D\uFF0C\u4F18\u5148\u7EA7\u4F4E\u4E8E --model\u3001\u9AD8\u4E8E\u914D\u7F6E\u6587\u4EF6 defaultModel\u3002`;
48114
49024
  var init_main = __esm(async () => {
48115
49025
  init_tui();
48116
49026
  try {
@@ -48142,7 +49052,7 @@ var init_open_browser = () => {};
48142
49052
  import { spawn as spawn4 } from "child_process";
48143
49053
  import { randomUUID as randomUUID7 } from "crypto";
48144
49054
  import { mkdir as mkdir8, readFile as readFile10, writeFile as writeFile7 } from "fs/promises";
48145
- import { dirname as dirname12, join as join16 } from "path";
49055
+ import { dirname as dirname12, join as join17 } from "path";
48146
49056
  async function ensureNekoGateway(options) {
48147
49057
  const timeoutMs = options.spawnTimeoutMs ?? 30000;
48148
49058
  const timeoutAt = Date.now() + timeoutMs;
@@ -48184,11 +49094,11 @@ async function ensureNekoGateway(options) {
48184
49094
  }
48185
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)}`);
48186
49096
  } catch (error) {
48187
- await stopGatewayProcess(child);
49097
+ await stopSpawnedGateway(child);
48188
49098
  throw error;
48189
49099
  }
48190
49100
  }
48191
- const socketPath = options.socketPath ?? join16(nekoDir, `${serverId}.sock`);
49101
+ const socketPath = options.socketPath ?? join17(nekoDir, `${serverId}.sock`);
48192
49102
  const host = options.host ?? "127.0.0.1";
48193
49103
  const port = options.port ?? 8321;
48194
49104
  const wsUrl = `ws://${host.includes(":") && !host.startsWith("[") ? `[${host}]` : host}:${port}`;
@@ -48227,15 +49137,18 @@ async function ensureNekoGateway(options) {
48227
49137
  }
48228
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)}`);
48229
49139
  } catch (error) {
48230
- await stopGatewayProcess(child);
49140
+ await stopSpawnedGateway(child);
48231
49141
  throw error;
48232
49142
  }
48233
49143
  }
49144
+ function stopSpawnedGateway(child) {
49145
+ return stopGatewayProcess(child, { viaStdin: process.platform === "win32" });
49146
+ }
48234
49147
  function spawnGatewayProcess(options, socketPath, serverId) {
48235
49148
  const args = [
48236
49149
  "--host",
48237
49150
  options.host ?? "127.0.0.1",
48238
- ...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 ?? ""],
48239
49152
  "--server-id",
48240
49153
  serverId,
48241
49154
  "--storage",
@@ -48245,7 +49158,10 @@ function spawnGatewayProcess(options, socketPath, serverId) {
48245
49158
  ...options.model !== undefined ? ["--model", `${options.model.provider}/${options.model.modelId}`] : []
48246
49159
  ];
48247
49160
  const command = gatewayCommand(args);
48248
- 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
+ });
48249
49165
  }
48250
49166
  function pipeChildLogs(child, onLog) {
48251
49167
  if (onLog === undefined)
@@ -48302,7 +49218,7 @@ function makeHandle(serverId, socketPath, wsUrl, spawned, nekoDir) {
48302
49218
  spawned,
48303
49219
  async close() {
48304
49220
  if (spawned !== null) {
48305
- await stopGatewayProcess(spawned);
49221
+ await stopSpawnedGateway(spawned);
48306
49222
  if (process.platform === "win32")
48307
49223
  await removeGatewayPortFile(nekoDir, serverId).catch(() => {});
48308
49224
  }
@@ -48310,7 +49226,7 @@ function makeHandle(serverId, socketPath, wsUrl, spawned, nekoDir) {
48310
49226
  };
48311
49227
  }
48312
49228
  async function loadOrCreateServerId2(nekoDir) {
48313
- const file = join16(nekoDir, SERVER_ID_FILENAME3);
49229
+ const file = join17(nekoDir, SERVER_ID_FILENAME3);
48314
49230
  try {
48315
49231
  const existing = (await readFile10(file, "utf8")).trim();
48316
49232
  if (existing !== "")
@@ -48635,9 +49551,9 @@ var init_mobile_admin = __esm(() => {
48635
49551
 
48636
49552
  // packages/web/src/open-target.ts
48637
49553
  import { spawn as spawn5 } from "child_process";
48638
- import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
48639
- import { homedir as homedir9 } from "os";
48640
- 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";
48641
49557
  function isOpenTargetId(value) {
48642
49558
  return value === "vscode" || value === "file-manager";
48643
49559
  }
@@ -48656,9 +49572,9 @@ function systemOpenHost() {
48656
49572
  const platform = process.platform;
48657
49573
  return {
48658
49574
  platform,
48659
- wsl: detectWsl2(platform, process.env, () => readFileSync4("/proc/version", "utf8")),
49575
+ wsl: detectWsl2(platform, process.env, () => readFileSync5("/proc/version", "utf8")),
48660
49576
  env: process.env,
48661
- home: homedir9(),
49577
+ home: homedir8(),
48662
49578
  exists: existsSync4
48663
49579
  };
48664
49580
  }
@@ -48671,7 +49587,7 @@ function fileManagerLabel(host) {
48671
49587
  }
48672
49588
  function findOnPath(host, names) {
48673
49589
  const windows = host.platform === "win32";
48674
- const pathApi = windows ? win32 : posix;
49590
+ const pathApi = windows ? win322 : posix;
48675
49591
  const raw = host.env[windows ? "Path" : "PATH"] ?? host.env.PATH ?? host.env.Path ?? "";
48676
49592
  for (const entry of raw.split(windows ? ";" : ":")) {
48677
49593
  if (entry === "")
@@ -48687,7 +49603,7 @@ function findOnPath(host, names) {
48687
49603
  function windowsCodeExecutable(host) {
48688
49604
  const shim = findOnPath(host, ["code.cmd"]);
48689
49605
  if (shim !== undefined) {
48690
- const exe = win32.join(win32.dirname(win32.dirname(shim)), "Code.exe");
49606
+ const exe = win322.join(win322.dirname(win322.dirname(shim)), "Code.exe");
48691
49607
  if (host.exists(exe))
48692
49608
  return exe;
48693
49609
  }
@@ -48698,7 +49614,7 @@ function windowsCodeExecutable(host) {
48698
49614
  const base = host.env[variable];
48699
49615
  if (base === undefined || base === "")
48700
49616
  continue;
48701
- const exe = win32.join(base, suffix);
49617
+ const exe = win322.join(base, suffix);
48702
49618
  if (host.exists(exe))
48703
49619
  return exe;
48704
49620
  }
@@ -48743,8 +49659,7 @@ function spawnDetached(spec) {
48743
49659
  const child = spawn5(spec.command, [...spec.args], {
48744
49660
  cwd: spec.cwd,
48745
49661
  detached: true,
48746
- stdio: "ignore",
48747
- windowsHide: true
49662
+ stdio: "ignore"
48748
49663
  });
48749
49664
  const timer = setTimeout(() => reject(new OpenError("launch_failed", "\u542F\u52A8\u8D85\u65F6")), LAUNCH_TIMEOUT_MS);
48750
49665
  child.once("error", (error) => {
@@ -48785,7 +49700,7 @@ var init_open_target = __esm(() => {
48785
49700
  });
48786
49701
 
48787
49702
  // packages/web/src/open-api.ts
48788
- import { realpath as realpath4, stat as stat6 } from "fs/promises";
49703
+ import { realpath as realpath4, stat as stat7 } from "fs/promises";
48789
49704
  import { isAbsolute as isAbsolute5 } from "path";
48790
49705
  async function validateDirectory(value) {
48791
49706
  if (typeof value !== "string" || value === "" || value.length > MAX_PATH_LENGTH)
@@ -48794,7 +49709,7 @@ async function validateDirectory(value) {
48794
49709
  return;
48795
49710
  try {
48796
49711
  const resolved = await realpath4(value);
48797
- return (await stat6(resolved)).isDirectory() ? resolved : undefined;
49712
+ return (await stat7(resolved)).isDirectory() ? resolved : undefined;
48798
49713
  } catch {
48799
49714
  return;
48800
49715
  }
@@ -48856,7 +49771,7 @@ var init_open_api = __esm(() => {
48856
49771
 
48857
49772
  // packages/web/src/server.ts
48858
49773
  import { createReadStream } from "fs";
48859
- import { stat as stat7 } from "fs/promises";
49774
+ import { stat as stat8 } from "fs/promises";
48860
49775
  import { createServer as createServer3 } from "http";
48861
49776
  import { dirname as dirname13, extname as extname2, resolve as resolve7, sep as sep3 } from "path";
48862
49777
  import { fileURLToPath as fileURLToPath5 } from "url";
@@ -48988,18 +49903,18 @@ async function serveStatic(pathname, method, response, staticDir) {
48988
49903
  return;
48989
49904
  }
48990
49905
  let file = candidate;
48991
- let stats = await stat7(file).catch(() => {
49906
+ let stats = await stat8(file).catch(() => {
48992
49907
  return;
48993
49908
  });
48994
49909
  if (stats?.isDirectory()) {
48995
49910
  file = resolve7(file, "index.html");
48996
- stats = await stat7(file).catch(() => {
49911
+ stats = await stat8(file).catch(() => {
48997
49912
  return;
48998
49913
  });
48999
49914
  }
49000
49915
  if (stats === undefined && extname2(candidate) === "") {
49001
49916
  file = resolve7(staticDir, "index.html");
49002
- stats = await stat7(file).catch(() => {
49917
+ stats = await stat8(file).catch(() => {
49003
49918
  return;
49004
49919
  });
49005
49920
  }
@@ -49154,11 +50069,13 @@ var USAGE2 = `\u7528\u6CD5: neko web [\u9009\u9879]
49154
50069
  --server-id <id> \u7F51\u5173\u903B\u8F91\u8EAB\u4EFD\uFF08\u9ED8\u8BA4\u8BFB\u53D6/\u751F\u6210 <\u6570\u636E\u76EE\u5F55>/gateway-server-id\uFF09
49155
50070
  --storage <path> SQLite \u5B58\u50A8\u6587\u4EF6\uFF08\u9ED8\u8BA4 ~/.neko/neko.sqlite\uFF09
49156
50071
  --cwd <dir> \u4F1A\u8BDD\u5DE5\u5177\u7684\u5DE5\u4F5C\u76EE\u5F55\uFF08\u9ED8\u8BA4\u5F53\u524D\u76EE\u5F55\uFF09
49157
- --model <ref> \u6A21\u578B\uFF0C\u683C\u5F0F provider/modelId\uFF08\u9ED8\u8BA4 deepseek/deepseek-flash\uFF09
50072
+ --model <ref> \u6A21\u578B\uFF0C\u683C\u5F0F provider/modelId\uFF08modelId \u53EF\u542B /\uFF0C\u9ED8\u8BA4 deepseek/deepseek-flash\uFF09
49158
50073
  --no-open \u4E0D\u81EA\u52A8\u6253\u5F00\u6D4F\u89C8\u5668\uFF08\u9002\u7528\u4E8E\u8FDC\u7A0B\u6216\u65E0\u684C\u9762\u73AF\u5883\uFF09
49159
50074
  -h, --help \u663E\u793A\u6B64\u5E2E\u52A9
49160
50075
 
49161
- \u53EF\u5728 Web \u8BBE\u7F6E\u9875\u914D\u7F6E Key\uFF1B\u7F51\u5173\u5B50\u8FDB\u7A0B\u4E5F\u4F1A\u7EE7\u627F DEEPSEEK_API_KEY\u3002`, server, shutdown;
50076
+ \u53EF\u5728 Web \u8BBE\u7F6E\u9875\u300C\u6A21\u578B\u670D\u52A1\u300D\u542F\u7528 provider\uFF08\u5982 OpenRouter\u3001OpenCode Go\uFF09\u5E76\u914D\u7F6E Key\uFF0C\u8054\u7F51\u641C\u7D22\u5728\u300C\u8054\u7F51\u641C\u7D22\u300D\u9875\u8BBE\u7F6E\uFF1B
50077
+ \u6A21\u578B\u9009\u62E9\u5668\u4E2D\u53EF\u628A\u53EF\u9009\u6A21\u578B\u300C\u8BBE\u4E3A\u9ED8\u8BA4\u300D\uFF0C\u4F18\u5148\u7EA7\u4F4E\u4E8E --model\u3001\u9AD8\u4E8E\u914D\u7F6E\u6587\u4EF6 defaultModel\uFF1B
50078
+ \u7F51\u5173\u5B50\u8FDB\u7A0B\u4E5F\u4F1A\u7EE7\u627F DEEPSEEK_API_KEY / OPENROUTER_API_KEY / OPENCODE_API_KEY \u7B49\u73AF\u5883\u53D8\u91CF\u3002`, server, shutdown;
49162
50079
  var init_main2 = __esm(async () => {
49163
50080
  init_src2();
49164
50081
  init_open_browser();
@@ -49284,6 +50201,9 @@ function parseGatewayArgs(argv) {
49284
50201
  options.configDir = next;
49285
50202
  i++;
49286
50203
  break;
50204
+ case "--stop-on-stdin":
50205
+ options.stopOnStdin = true;
50206
+ break;
49287
50207
  default:
49288
50208
  throw new Error(`\u672A\u77E5\u9009\u9879: ${arg}`);
49289
50209
  }
@@ -49301,14 +50221,17 @@ var GATEWAY_USAGE = `\u7528\u6CD5: neko serve [\u9009\u9879]
49301
50221
  --no-socket \u7981\u7528 Unix socket \u4F20\u8F93\uFF08\u4EC5 WebSocket\uFF09
49302
50222
  --server-id <id> \u903B\u8F91\u670D\u52A1\u5668\u8EAB\u4EFD\uFF08\u9ED8\u8BA4\u8BFB\u53D6/\u751F\u6210 <\u6570\u636E\u76EE\u5F55>/gateway-server-id\uFF09
49303
50223
  --cwd <dir> \u4F1A\u8BDD\u5DE5\u5177\u7684\u5DE5\u4F5C\u76EE\u5F55\uFF08\u9ED8\u8BA4\u5F53\u524D\u76EE\u5F55\uFF09
49304
- --model <ref> \u6A21\u578B\uFF0C\u683C\u5F0F provider/modelId\uFF08\u9ED8\u8BA4 deepseek/deepseek-flash\uFF09
50224
+ --model <ref> \u6A21\u578B\uFF0C\u683C\u5F0F provider/modelId\uFF08modelId \u53EF\u542B /\uFF0C\u9ED8\u8BA4 deepseek/deepseek-flash\uFF09
49305
50225
  --storage <path> SQLite \u5B58\u50A8\u6587\u4EF6\uFF08\u9ED8\u8BA4 ~/.neko/neko.sqlite\uFF09
49306
50226
  --credentials <path> \u7528\u6237\u51ED\u636E\u6587\u4EF6\uFF08\u9ED8\u8BA4 ~/.neko/credentials.json\uFF09
49307
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
49308
50229
  -h, --help \u663E\u793A\u6B64\u5E2E\u52A9
49309
50230
 
49310
- \u53EF\u901A\u8FC7\u524D\u7AEF\u8BBE\u7F6E\u6216 DEEPSEEK_API_KEY / TAVILY_API_KEY \u914D\u7F6E\u51ED\u636E\uFF1B
49311
- \u8054\u7F51\u641C\u7D22\u670D\u52A1\u53EF\u5728\u524D\u7AEF\u8BBE\u7F6E\uFF0C\u6216\u7528\u914D\u7F6E\u6587\u4EF6 webSearch.provider\uFF08tavily / deepseek\uFF0C\u9ED8\u8BA4 tavily\uFF09\u3002`;
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
50232
+ \u9664 DeepSeek \u5916\u7684\u6A21\u578B provider \u9700\u5148\u5728\u524D\u7AEF\u542F\u7528\uFF0C--model \u6307\u5411\u672A\u542F\u7528\u7684 provider \u65F6\u63D0\u4EA4\u4F1A\u88AB\u62D2\u7EDD\uFF1B
50233
+ \u8054\u7F51\u641C\u7D22\u670D\u52A1\u53EF\u5728\u524D\u7AEF\u8BBE\u7F6E\uFF0C\u6216\u7528\u914D\u7F6E\u6587\u4EF6 webSearch.provider\uFF08tavily / deepseek\uFF0C\u9ED8\u8BA4 tavily\uFF09\uFF1B
50234
+ \u9ED8\u8BA4\u6A21\u578B\u8FD8\u53EF\u5728 TUI /model \u6216 Web \u6A21\u578B\u9009\u62E9\u5668\u4E2D\u300C\u8BBE\u4E3A\u9ED8\u8BA4\u300D\uFF0C\u4F18\u5148\u7EA7\u4F4E\u4E8E --model\u3001\u9AD8\u4E8E\u914D\u7F6E\u6587\u4EF6 defaultModel\u3002`;
49312
50235
 
49313
50236
  // packages/gateway/src/main.ts
49314
50237
  var argv, shutdown2, options, gateway;
@@ -49335,6 +50258,18 @@ var init_main3 = __esm(async () => {
49335
50258
  console.log(` WebSocket: ${gateway.wsUrl}`);
49336
50259
  console.log("\u6309 Ctrl+C \u505C\u6B62\u3002");
49337
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
+ }
49338
50273
  });
49339
50274
 
49340
50275
  // packages/cli/src/main.ts
@@ -49352,8 +50287,10 @@ var USAGE3 = `\u7528\u6CD5: neko <\u547D\u4EE4> [\u9009\u9879]
49352
50287
  -h, --help \u663E\u793A\u6B64\u5E2E\u52A9
49353
50288
 
49354
50289
  \u5404\u547D\u4EE4\u7684\u9009\u9879\u8BE6\u89C1 neko <\u547D\u4EE4> --help\u3002
49355
- \u53EF\u901A\u8FC7 TUI /login\u3001Web \u8BBE\u7F6E\u9875\u6216 DEEPSEEK_API_KEY / TAVILY_API_KEY \u914D\u7F6E\u51ED\u636E\u3002
49356
- \u8054\u7F51\u641C\u7D22\u670D\u52A1\uFF08\u9ED8\u8BA4 Tavily\uFF0C\u53EF\u6539\u9009 DeepSeek\uFF09\u53EF\u7528 TUI /search\u3001Web \u8BBE\u7F6E\u9875\u6216\u914D\u7F6E\u6587\u4EF6 webSearch.provider \u9009\u62E9\u3002`;
50290
+ \u53EF\u901A\u8FC7 TUI /login\u3001Web \u8BBE\u7F6E\u9875\u6216\u73AF\u5883\u53D8\u91CF\uFF08DEEPSEEK_API_KEY / OPENROUTER_API_KEY / OPENCODE_API_KEY / TAVILY_API_KEY \u7B49\uFF09\u914D\u7F6E\u51ED\u636E\uFF1B
50291
+ \u9664 DeepSeek \u5916\u7684\u6A21\u578B provider\uFF08\u5982 OpenRouter\u3001OpenCode Go\uFF09\u9700\u5148\u5728 /login \u6216 Web \u8BBE\u7F6E\u9875\u300C\u6A21\u578B\u670D\u52A1\u300D\u542F\u7528\uFF0C--model \u683C\u5F0F\u4E3A provider/modelId\u3002
50292
+ \u8054\u7F51\u641C\u7D22\u670D\u52A1\uFF08\u9ED8\u8BA4 Tavily\uFF0C\u53EF\u6539\u9009 DeepSeek\uFF09\u53EF\u7528 TUI /search\u3001Web \u8BBE\u7F6E\u9875\u300C\u8054\u7F51\u641C\u7D22\u300D\u6216\u914D\u7F6E\u6587\u4EF6 webSearch.provider \u9009\u62E9\u3002
50293
+ \u9ED8\u8BA4\u6A21\u578B\u53EF\u5728 TUI /model \u6216 Web \u6A21\u578B\u9009\u62E9\u5668\u300C\u8BBE\u4E3A\u9ED8\u8BA4\u300D\uFF0C\u4F18\u5148\u7EA7\u4F4E\u4E8E --model\u3001\u9AD8\u4E8E\u914D\u7F6E\u6587\u4EF6 defaultModel\u3002`;
49357
50294
  var [command2, ...rest] = process.argv.slice(2);
49358
50295
  switch (command2) {
49359
50296
  case "update": {