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/gateway.js CHANGED
@@ -16497,14 +16497,17 @@ var GATEWAY_USAGE = `\u7528\u6CD5: neko serve [\u9009\u9879]
16497
16497
  --no-socket \u7981\u7528 Unix socket \u4F20\u8F93\uFF08\u4EC5 WebSocket\uFF09
16498
16498
  --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
16499
16499
  --cwd <dir> \u4F1A\u8BDD\u5DE5\u5177\u7684\u5DE5\u4F5C\u76EE\u5F55\uFF08\u9ED8\u8BA4\u5F53\u524D\u76EE\u5F55\uFF09
16500
- --model <ref> \u6A21\u578B\uFF0C\u683C\u5F0F provider/modelId\uFF08\u9ED8\u8BA4 deepseek/deepseek-flash\uFF09
16500
+ --model <ref> \u6A21\u578B\uFF0C\u683C\u5F0F provider/modelId\uFF08modelId \u53EF\u542B /\uFF0C\u9ED8\u8BA4 deepseek/deepseek-flash\uFF09
16501
16501
  --storage <path> SQLite \u5B58\u50A8\u6587\u4EF6\uFF08\u9ED8\u8BA4 ~/.neko/neko.sqlite\uFF09
16502
16502
  --credentials <path> \u7528\u6237\u51ED\u636E\u6587\u4EF6\uFF08\u9ED8\u8BA4 ~/.neko/credentials.json\uFF09
16503
16503
  --config <dir> neko.config.json \u67E5\u627E\u8D77\u59CB\u76EE\u5F55\uFF08\u9ED8\u8BA4\u5F53\u524D\u76EE\u5F55\uFF09
16504
+ --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
16504
16505
  -h, --help \u663E\u793A\u6B64\u5E2E\u52A9
16505
16506
 
16506
- \u53EF\u901A\u8FC7\u524D\u7AEF\u8BBE\u7F6E\u6216 DEEPSEEK_API_KEY / TAVILY_API_KEY \u914D\u7F6E\u51ED\u636E\uFF1B
16507
- \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`;
16507
+ \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
16508
+ \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
16509
+ \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
16510
+ \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`;
16508
16511
  function parseGatewayArgs(argv) {
16509
16512
  let port;
16510
16513
  const options = {};
@@ -16582,6 +16585,9 @@ function parseGatewayArgs(argv) {
16582
16585
  options.configDir = next;
16583
16586
  i++;
16584
16587
  break;
16588
+ case "--stop-on-stdin":
16589
+ options.stopOnStdin = true;
16590
+ break;
16585
16591
  default:
16586
16592
  throw new Error(`\u672A\u77E5\u9009\u9879: ${arg}`);
16587
16593
  }
@@ -16888,6 +16894,16 @@ function validate(config, source) {
16888
16894
  throw new Error(`neko config: ${source}: "skills.marketUrl" must be a string.`);
16889
16895
  }
16890
16896
  }
16897
+ const shell = config.shell;
16898
+ if (shell !== undefined) {
16899
+ if (typeof shell !== "object" || shell === null || Array.isArray(shell)) {
16900
+ throw new Error(`neko config: ${source}: "shell" must be an object.`);
16901
+ }
16902
+ const { path } = shell;
16903
+ if (path !== undefined && typeof path !== "string") {
16904
+ throw new Error(`neko config: ${source}: "shell.path" must be a string.`);
16905
+ }
16906
+ }
16891
16907
  const webSearch = config.webSearch;
16892
16908
  if (webSearch !== undefined) {
16893
16909
  if (typeof webSearch !== "object" || webSearch === null || Array.isArray(webSearch)) {
@@ -16907,30 +16923,113 @@ function validate(config, source) {
16907
16923
  }
16908
16924
  }
16909
16925
  // packages/core/src/credentials.ts
16910
- import { chmod, mkdir, open, readFile, rename, unlink } from "fs/promises";
16926
+ import { chmod, mkdir, open, readFile, stat, unlink } from "fs/promises";
16911
16927
  import { homedir as homedir2 } from "os";
16912
16928
  import { dirname as dirname2, join as join2 } from "path";
16913
16929
 
16930
+ // packages/core/src/rename-with-retry.ts
16931
+ import { rename } from "fs/promises";
16932
+ var TRANSIENT = new Set(["EPERM", "EBUSY", "EACCES"]);
16933
+ var FIRST_DELAY_MS = 10;
16934
+ var MAX_TOTAL_MS = 1000;
16935
+ async function renameWithRetry(from, to, deps = {}) {
16936
+ const doRename = deps.rename ?? rename;
16937
+ const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
16938
+ let delay = FIRST_DELAY_MS;
16939
+ let waited = 0;
16940
+ for (;; ) {
16941
+ try {
16942
+ await doRename(from, to);
16943
+ return;
16944
+ } catch (error) {
16945
+ const code = error.code;
16946
+ if (typeof code !== "string" || !TRANSIENT.has(code) || waited >= MAX_TOTAL_MS)
16947
+ throw error;
16948
+ const step = Math.min(delay, MAX_TOTAL_MS - waited);
16949
+ await sleep(step);
16950
+ waited += step;
16951
+ delay *= 2;
16952
+ }
16953
+ }
16954
+ }
16955
+
16914
16956
  // packages/core/src/credential-providers.ts
16957
+ var model = (id, name, envVar, signupUrl) => ({ id, kind: "model", name, envVar, signupUrl, keyless: false });
16958
+ var MODEL_PROVIDERS = [
16959
+ model("deepseek", "DeepSeek", "DEEPSEEK_API_KEY", "https://platform.deepseek.com/api_keys"),
16960
+ model("openrouter", "OpenRouter", "OPENROUTER_API_KEY", "https://openrouter.ai/keys"),
16961
+ model("opencode-go", "OpenCode Go", "OPENCODE_API_KEY", "https://opencode.ai/auth"),
16962
+ model("opencode", "OpenCode Zen", "OPENCODE_API_KEY", "https://opencode.ai/auth"),
16963
+ model("openai", "OpenAI", "OPENAI_API_KEY", "https://platform.openai.com/api-keys"),
16964
+ model("google", "Google", "GEMINI_API_KEY", "https://aistudio.google.com/apikey"),
16965
+ model("groq", "Groq", "GROQ_API_KEY", "https://console.groq.com/keys"),
16966
+ model("mistral", "Mistral", "MISTRAL_API_KEY", "https://console.mistral.ai/api-keys"),
16967
+ model("xai", "xAI", "XAI_API_KEY", "https://console.x.ai"),
16968
+ model("together", "Together", "TOGETHER_API_KEY", "https://api.together.ai/settings/api-keys"),
16969
+ model("fireworks", "Fireworks", "FIREWORKS_API_KEY", "https://fireworks.ai/account/api-keys"),
16970
+ model("cerebras", "Cerebras", "CEREBRAS_API_KEY", "https://cloud.cerebras.ai"),
16971
+ model("nvidia", "NVIDIA", "NVIDIA_API_KEY", "https://build.nvidia.com/settings/api-keys"),
16972
+ model("huggingface", "Hugging Face", "HF_TOKEN", "https://huggingface.co/settings/tokens"),
16973
+ model("vercel-ai-gateway", "Vercel AI Gateway", "AI_GATEWAY_API_KEY", "https://vercel.com/ai-gateway"),
16974
+ model("moonshotai", "Moonshot AI", "MOONSHOT_API_KEY", "https://platform.moonshot.ai/console/api-keys"),
16975
+ model("moonshotai-cn", "Moonshot AI CN", "MOONSHOT_API_KEY", "https://platform.moonshot.cn/console/api-keys"),
16976
+ model("minimax", "MiniMax", "MINIMAX_API_KEY", "https://platform.minimax.io"),
16977
+ model("minimax-cn", "MiniMax CN", "MINIMAX_CN_API_KEY", "https://platform.minimaxi.com"),
16978
+ model("zai", "Z.AI", "ZAI_API_KEY", "https://z.ai/manage-apikey/apikey-list"),
16979
+ model("zai-coding-cn", "Z.AI Coding CN", "ZAI_CODING_CN_API_KEY", "https://bigmodel.cn/usercenter/proj-mgmt/apikeys"),
16980
+ model("qwen-token-plan", "Qwen Token Plan", "QWEN_TOKEN_PLAN_API_KEY", "https://modelstudio.console.alibabacloud.com"),
16981
+ model("qwen-token-plan-cn", "Qwen Token Plan CN", "QWEN_TOKEN_PLAN_CN_API_KEY", "https://bailian.console.aliyun.com"),
16982
+ model("qwen-token-plan-individual", "Qwen Token Plan Individual", "QWEN_TOKEN_PLAN_API_KEY", "https://modelstudio.console.alibabacloud.com"),
16983
+ model("xiaomi", "Xiaomi MiMo", "XIAOMI_API_KEY", "https://platform.xiaomimimo.com"),
16984
+ model("xiaomi-token-plan-cn", "Xiaomi Token Plan CN", "XIAOMI_TOKEN_PLAN_CN_API_KEY", "https://platform.xiaomimimo.com"),
16985
+ model("xiaomi-token-plan-ams", "Xiaomi Token Plan AMS", "XIAOMI_TOKEN_PLAN_AMS_API_KEY", "https://platform.xiaomimimo.com"),
16986
+ model("xiaomi-token-plan-sgp", "Xiaomi Token Plan SGP", "XIAOMI_TOKEN_PLAN_SGP_API_KEY", "https://platform.xiaomimimo.com"),
16987
+ model("baseten", "Baseten", "BASETEN_API_KEY", "https://app.baseten.co/settings/api_keys"),
16988
+ model("ant-ling", "Ant Ling", "ANT_LING_API_KEY", "https://ling.tbox.cn"),
16989
+ model("kimi-coding", "Kimi For Coding", "KIMI_API_KEY", "https://www.kimi.com/code/console"),
16990
+ model("meta", "Meta", "META_API_KEY", "https://llama.developer.meta.com")
16991
+ ];
16992
+ var SEARCH_CREDENTIAL_PROVIDERS = [
16993
+ {
16994
+ id: "tavily",
16995
+ kind: "search",
16996
+ name: "Tavily",
16997
+ envVar: "TAVILY_API_KEY",
16998
+ signupUrl: "https://app.tavily.com",
16999
+ keyless: true
17000
+ }
17001
+ ];
16915
17002
  var CREDENTIAL_PROVIDERS = [
16916
- { id: "deepseek", envVar: "DEEPSEEK_API_KEY", signupUrl: "https://platform.deepseek.com/api_keys", keyless: false },
16917
- { id: "tavily", envVar: "TAVILY_API_KEY", signupUrl: "https://app.tavily.com", keyless: true }
17003
+ ...MODEL_PROVIDERS,
17004
+ ...SEARCH_CREDENTIAL_PROVIDERS
16918
17005
  ];
17006
+ var DEFAULT_ENABLED_PROVIDERS = ["deepseek"];
17007
+ var LOCKED_PROVIDERS = ["deepseek"];
17008
+ function isModelProvider(id) {
17009
+ return MODEL_PROVIDERS.some((item) => item.id === id);
17010
+ }
16919
17011
  function credentialProvider(id) {
16920
17012
  const spec = CREDENTIAL_PROVIDERS.find((item) => item.id === id);
16921
17013
  if (!spec)
16922
17014
  throw new Error(`Unknown credential provider: ${id}`);
16923
17015
  return spec;
16924
17016
  }
17017
+ function normalizeEnabledProviders(saved) {
17018
+ const wanted = new Set([...LOCKED_PROVIDERS, ...saved ?? DEFAULT_ENABLED_PROVIDERS]);
17019
+ return MODEL_PROVIDERS.map((item) => item.id).filter((id) => wanted.has(id));
17020
+ }
16925
17021
 
16926
17022
  // packages/core/src/credentials.ts
16927
17023
  import { InMemoryCredentialStore } from "@earendil-works/pi-ai";
17024
+ var STALE_LOCK_MS = 30000;
16928
17025
  var DEFAULT_CREDENTIALS_PATH = join2(homedir2(), ".neko", "credentials.json");
16929
17026
 
16930
17027
  class FileCredentialStore {
16931
17028
  path;
16932
- constructor(path = DEFAULT_CREDENTIALS_PATH) {
17029
+ #renameDeps;
17030
+ constructor(path = DEFAULT_CREDENTIALS_PATH, renameDeps = {}) {
16933
17031
  this.path = path;
17032
+ this.#renameDeps = renameDeps;
16934
17033
  }
16935
17034
  async#read() {
16936
17035
  try {
@@ -16960,6 +17059,7 @@ class FileCredentialStore {
16960
17059
  await mkdir(dirname2(this.path), { recursive: true, mode: 448 });
16961
17060
  const lockPath = `${this.path}.lock`;
16962
17061
  let lock;
17062
+ let reclaimed = false;
16963
17063
  for (let attempt = 0;; attempt++) {
16964
17064
  try {
16965
17065
  lock = await open(lockPath, "wx", 384);
@@ -16967,6 +17067,16 @@ class FileCredentialStore {
16967
17067
  } catch (error) {
16968
17068
  if (error.code !== "EEXIST" || attempt >= 100)
16969
17069
  throw new Error("Credential storage is busy or unavailable.");
17070
+ if (!reclaimed) {
17071
+ const info = await stat(lockPath).catch(() => {
17072
+ return;
17073
+ });
17074
+ if (info && Date.now() - info.mtimeMs > STALE_LOCK_MS) {
17075
+ reclaimed = true;
17076
+ await unlink(lockPath).catch(() => {});
17077
+ continue;
17078
+ }
17079
+ }
16970
17080
  await new Promise((resolve) => setTimeout(resolve, 25));
16971
17081
  }
16972
17082
  }
@@ -16988,7 +17098,7 @@ class FileCredentialStore {
16988
17098
  } finally {
16989
17099
  await file.close();
16990
17100
  }
16991
- await rename(temporary, this.path);
17101
+ await renameWithRetry(temporary, this.path, this.#renameDeps);
16992
17102
  return result;
16993
17103
  } catch (error) {
16994
17104
  if (callbackFailed)
@@ -17815,7 +17925,7 @@ var structuredOutputSchema = Type.Union([
17815
17925
  next: Type.Union([Type.Integer(), Type.Null()])
17816
17926
  }, { additionalProperties: false })
17817
17927
  ]);
17818
- function createSubagentExtension(prepare, discard) {
17928
+ function createSubagentExtension(prepare, discard, guard) {
17819
17929
  const finish = async (tx, input, status, report) => {
17820
17930
  const round = (await tx.doc(DelegationsDoc, input.parentId)).items.find((r) => r.taskId === input.taskId);
17821
17931
  round.status = status;
@@ -17959,6 +18069,8 @@ function createSubagentExtension(prepare, discard) {
17959
18069
  if (!args.task.trim() || !args.title.trim() || Buffer.byteLength(args.task) > 32768)
17960
18070
  throw new Error("Task/title must be nonempty; task limit is 32 KiB.");
17961
18071
  const agent = await api.agent(context);
18072
+ if (agent.model)
18073
+ await guard?.(agent.model);
17962
18074
  const round = await api.commit(async (tx) => {
17963
18075
  const rounds = await refreshDelegations(tx, api.conversationId);
17964
18076
  const receipt = rounds.find((r) => r.taskId === api.taskId);
@@ -18868,7 +18980,7 @@ import { homedir as homedir4 } from "os";
18868
18980
  import { join as join5 } from "path";
18869
18981
  import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT3 } from "@earendil-works/chord/context";
18870
18982
  import { clampThinkingLevel, createModels, getSupportedThinkingLevels as getSupportedThinkingLevels2 } from "@earendil-works/pi-ai/models";
18871
- import { deepseekProvider } from "@earendil-works/pi-ai/providers/deepseek";
18983
+ import { builtinProviders } from "@earendil-works/pi-ai/providers/all";
18872
18984
  import {
18873
18985
  AssistantEntry as AssistantEntry2,
18874
18986
  createRegistry,
@@ -19011,7 +19123,7 @@ var SearchText = defineTool2({
19011
19123
  complete = false;
19012
19124
  continue;
19013
19125
  }
19014
- const lines = new TextDecoder("utf-8", { fatal: true }).decode(data).split(/\r?\n/);
19126
+ const lines = new TextDecoder("utf-8", { fatal: true }).decode(data).replace(/^\uFEFF/, "").split(/\r?\n/);
19015
19127
  for (let i = 0;i < lines.length; i++) {
19016
19128
  ctx.abortSignal?.throwIfAborted();
19017
19129
  const value = lines[i];
@@ -19444,8 +19556,11 @@ class JobManager {
19444
19556
  outcome = { status: "failed", exitCode: null, reason: "timeout" };
19445
19557
  else if (result.error.code === "aborted")
19446
19558
  outcome = { status: "stopped", exitCode: null, reason: "aborted" };
19447
- else
19559
+ else {
19560
+ entry.buffer.append(`${result.error.message}
19561
+ `);
19448
19562
  outcome = { status: "failed", exitCode: null, reason: "spawn-error" };
19563
+ }
19449
19564
  }
19450
19565
  if (this.#closing) {
19451
19566
  await new Promise((resolve) => {
@@ -19842,10 +19957,15 @@ var TOOL_GUIDELINES = {
19842
19957
  ]
19843
19958
  };
19844
19959
  var GENERIC_RULES = ["Be concise in your responses", "Show file paths clearly when working with files"];
19845
- var buildRules = (toolNames) => {
19960
+ var 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.";
19961
+ var toolRules = (name, platform) => {
19962
+ const rules = TOOL_GUIDELINES[name] ?? [];
19963
+ return platform === "win32" && (name === "bash" || name === "job") ? [...rules, WINDOWS_SHELL_RULE] : rules;
19964
+ };
19965
+ var buildRules = (toolNames, platform) => {
19846
19966
  const rules = [];
19847
19967
  const seen = new Set;
19848
- for (const rule of [...toolNames.flatMap((name) => TOOL_GUIDELINES[name] ?? []), ...GENERIC_RULES]) {
19968
+ for (const rule of [...toolNames.flatMap((name) => toolRules(name, platform)), ...GENERIC_RULES]) {
19849
19969
  if (seen.has(rule))
19850
19970
  continue;
19851
19971
  seen.add(rule);
@@ -19862,7 +19982,7 @@ ${content}
19862
19982
  ].join(`
19863
19983
 
19864
19984
  `);
19865
- var createNekoPrompt = (loadResources) => {
19985
+ var createNekoPrompt = (loadResources, platform = process.platform) => {
19866
19986
  const built = new WeakMap;
19867
19987
  const build = async (input, context) => {
19868
19988
  const cwd = input.agent.cwd;
@@ -19870,7 +19990,7 @@ var createNekoPrompt = (loadResources) => {
19870
19990
  const resources = cwd ? await loadResources(input.conversationId, cwd) : { contextFiles: [], skills: [] };
19871
19991
  const sections = {
19872
19992
  preamble: role ? ROLE_PROMPTS[role] + SUBAGENT_DELIVERY : PREAMBLE,
19873
- rules: buildRules(input.agent.tools.map((tool) => tool.name)),
19993
+ rules: buildRules(input.agent.tools.map((tool) => tool.name), platform),
19874
19994
  project_context: renderProjectContext(resources.contextFiles),
19875
19995
  skills: formatSkillsForPrompt(resources.skills) || undefined,
19876
19996
  cwd: cwd?.replace(/\\/g, "/")
@@ -19918,7 +20038,7 @@ var readResources = async (workspace, home, builtinDir, report, invocationName)
19918
20038
  const contextFiles = [];
19919
20039
  for (const path of [userAgentsFile(home), workspaceAgentsFile(workspace)]) {
19920
20040
  try {
19921
- const content = await readFile3(path, "utf8");
20041
+ const content = (await readFile3(path, "utf8")).replace(/^\uFEFF/, "");
19922
20042
  if (Buffer.byteLength(content, "utf8") > 64 * 1024)
19923
20043
  report({
19924
20044
  path,
@@ -20443,7 +20563,7 @@ function mergeUsage(base, extra) {
20443
20563
  }
20444
20564
  return result;
20445
20565
  }
20446
- function namingTask() {
20566
+ function namingTask(canUse) {
20447
20567
  let active = 0;
20448
20568
  const waits = new Set;
20449
20569
  return defineTask3({
@@ -20480,7 +20600,7 @@ function namingTask() {
20480
20600
  try {
20481
20601
  const saved = await runtime.snapshot(SessionMetadataDoc, runtime.conversationId, context);
20482
20602
  const model = saved?.model && runtime.models.getModel(saved.model.provider, saved.model.modelId);
20483
- if (!saved || !model || saved.attempts >= 2 || !getSupportedThinkingLevels(model).includes("off")) {
20603
+ if (!saved || !model || saved.attempts >= 2 || !getSupportedThinkingLevels(model).includes("off") || !await canUse(model.provider, model.id)) {
20484
20604
  await runtime.commit(async (tx) => {
20485
20605
  (await tx.doc(SessionMetadataDoc, runtime.conversationId)).naming = "fallback";
20486
20606
  return { status: "terminal", outcome: { status: "completed", result: null } };
@@ -20560,12 +20680,13 @@ class SessionMetadataManager {
20560
20680
  #known = new Set;
20561
20681
  #unsubscribe;
20562
20682
  #live = new Map;
20563
- task = namingTask();
20683
+ task;
20564
20684
  harness;
20565
20685
  storage;
20566
20686
  context;
20567
20687
  report;
20568
- constructor(harness, storage, context, registry, report = console.error) {
20688
+ constructor(harness, storage, context, registry, report = console.error, canUseModel = async () => true) {
20689
+ this.task = namingTask(canUseModel);
20569
20690
  this.harness = harness;
20570
20691
  this.storage = storage;
20571
20692
  this.context = context;
@@ -21374,11 +21495,152 @@ function createTavilyProvider(resolveKey, fetchImpl = fetch) {
21374
21495
  };
21375
21496
  }
21376
21497
 
21498
+ // packages/core/src/windows-shell.ts
21499
+ import { spawnSync } from "child_process";
21500
+ import { statSync as statSync2 } from "fs";
21501
+ import { win32 } from "path";
21502
+ function isWslBashPath(path) {
21503
+ const normalized = path.replace(/\//g, "\\").toLowerCase();
21504
+ return /^[a-z]:\\windows\\(?:system32|sysnative)\\bash\.exe$/.test(normalized) || /\\microsoft\\windowsapps\\(?:[^\\]+\\)*bash\.exe$/.test(normalized);
21505
+ }
21506
+ function envValue(env, name) {
21507
+ const wanted = name.toLowerCase();
21508
+ for (const [key, value] of Object.entries(env))
21509
+ if (key.toLowerCase() === wanted && value)
21510
+ return value;
21511
+ return;
21512
+ }
21513
+ function bashBesideGit(gitExe) {
21514
+ const dir = win32.dirname(gitExe);
21515
+ const parent = win32.dirname(dir);
21516
+ const name = win32.basename(dir).toLowerCase();
21517
+ if (name === "cmd")
21518
+ return win32.join(parent, "bin", "bash.exe");
21519
+ if (name === "bin") {
21520
+ const root = win32.basename(parent).toLowerCase() === "mingw64" ? win32.dirname(parent) : parent;
21521
+ return win32.join(root, "bin", "bash.exe");
21522
+ }
21523
+ return;
21524
+ }
21525
+ function configProblem(path, kind) {
21526
+ if (isWslBashPath(path))
21527
+ return "it is WSL's bash, which is not supported (commands must run in Git Bash)";
21528
+ if (kind === "missing")
21529
+ return "the file does not exist";
21530
+ if (kind === "directory")
21531
+ return "it is a directory, not bash.exe";
21532
+ return;
21533
+ }
21534
+ function resolveWindowsShell(deps, configPath) {
21535
+ if (configPath !== undefined && configPath !== "") {
21536
+ const problem = configProblem(configPath, deps.kind(configPath));
21537
+ if (problem !== undefined) {
21538
+ return {
21539
+ kind: "missing",
21540
+ reason: "invalid_config",
21541
+ detail: `shell.path "${configPath}" is invalid: ${problem}.`,
21542
+ searched: [configPath]
21543
+ };
21544
+ }
21545
+ return { kind: "found", path: configPath, source: "config" };
21546
+ }
21547
+ const searched = [];
21548
+ const check = (path) => {
21549
+ if (!searched.includes(path))
21550
+ searched.push(path);
21551
+ return !isWslBashPath(path) && deps.kind(path) === "file";
21552
+ };
21553
+ const roots = [
21554
+ envValue(deps.env, "ProgramFiles"),
21555
+ envValue(deps.env, "ProgramFiles(x86)"),
21556
+ (() => {
21557
+ const local = envValue(deps.env, "LOCALAPPDATA");
21558
+ return local === undefined ? undefined : win32.join(local, "Programs");
21559
+ })()
21560
+ ];
21561
+ for (const root of roots) {
21562
+ if (root === undefined)
21563
+ continue;
21564
+ const candidate = win32.join(root, "Git", "bin", "bash.exe");
21565
+ if (check(candidate))
21566
+ return { kind: "found", path: candidate, source: "auto" };
21567
+ }
21568
+ for (const gitExe of deps.where("git.exe")) {
21569
+ const candidate = bashBesideGit(gitExe);
21570
+ if (candidate !== undefined && check(candidate))
21571
+ return { kind: "found", path: candidate, source: "auto" };
21572
+ }
21573
+ for (const candidate of deps.where("bash.exe")) {
21574
+ if (check(candidate))
21575
+ return { kind: "found", path: candidate, source: "auto" };
21576
+ }
21577
+ return { kind: "missing", reason: "not_found", detail: "No Git Bash was found.", searched };
21578
+ }
21579
+ function summarizeWindowsShell(status) {
21580
+ return status.kind === "found" ? { status: "ready", path: status.path, source: status.source } : { status: "missing", reason: status.reason, detail: status.detail };
21581
+ }
21582
+ function windowsShellMessage(status) {
21583
+ const lines = status.reason === "invalid_config" ? [
21584
+ `Cannot run shell commands: ${status.detail}`,
21585
+ "Fix shell.path in neko.config.json, or remove it to auto-detect Git Bash."
21586
+ ] : [
21587
+ "Cannot run shell commands: Git Bash was not found. neko runs commands in Git for Windows' bash, not PowerShell, cmd or WSL.",
21588
+ "Install Git for Windows (https://git-scm.com/download/win), or set shell.path in neko.config.json to its bash.exe."
21589
+ ];
21590
+ if (status.searched.length > 0)
21591
+ lines.push("Checked:", ...status.searched.map((path) => ` ${path}`));
21592
+ lines.push("File tools (read, write, edit) still work.");
21593
+ return lines.join(`
21594
+ `);
21595
+ }
21596
+ function systemWindowsShellDeps() {
21597
+ return {
21598
+ env: process.env,
21599
+ kind(path) {
21600
+ try {
21601
+ return statSync2(path).isDirectory() ? "directory" : "file";
21602
+ } catch {
21603
+ return "missing";
21604
+ }
21605
+ },
21606
+ where(program) {
21607
+ const result = spawnSync("where", [program], { encoding: "utf8", timeout: 5000, windowsHide: true });
21608
+ if (result.status !== 0 || typeof result.stdout !== "string")
21609
+ return [];
21610
+ return result.stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => line !== "");
21611
+ }
21612
+ };
21613
+ }
21614
+ var WINDOWS_SHELL_RETRY_MS = 30000;
21615
+ function createWindowsShellResolver(options) {
21616
+ const deps = options?.deps ?? systemWindowsShellDeps();
21617
+ const now = options?.now ?? Date.now;
21618
+ let cached;
21619
+ return {
21620
+ status(call) {
21621
+ if (cached !== undefined && call?.refresh !== true) {
21622
+ if (cached.status.kind === "found") {
21623
+ if (deps.kind(cached.status.path) === "file")
21624
+ return cached.status;
21625
+ } else if (now() - cached.at < WINDOWS_SHELL_RETRY_MS) {
21626
+ return cached.status;
21627
+ }
21628
+ }
21629
+ cached = { status: resolveWindowsShell(deps, options?.configPath), at: now() };
21630
+ return cached.status;
21631
+ }
21632
+ };
21633
+ }
21634
+
21377
21635
  // packages/core/src/workspace-files.ts
21378
- import { open as open2, realpath as realpath2, stat } from "fs/promises";
21636
+ import { open as open2, realpath as realpath2, stat as stat2 } from "fs/promises";
21379
21637
  import { basename as basename2, isAbsolute as isAbsolute3, relative as relative2, resolve as resolve4, sep as sep2 } from "path";
21380
21638
  import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT2, withAbortSignal as withAbortSignal3 } from "@earendil-works/chord/context";
21381
- import { getOrThrow as getOrThrow2 } from "@earendil-works/pi-durable/env";
21639
+ import {
21640
+ ExecutionError,
21641
+ err,
21642
+ getOrThrow as getOrThrow2
21643
+ } from "@earendil-works/pi-durable/env";
21382
21644
  import { Connection, LocalExecutionEnv, localDaemon } from "@earendil-works/pi-env";
21383
21645
  var BROWSE_LIMITS = { children: 1000, readBytes: 512 * 1024 };
21384
21646
  var ENV_TEMPLATE = /\.(example|sample|template)$/;
@@ -21416,6 +21678,19 @@ var FILE_CACHE_LIMITS = {
21416
21678
  concurrent: 2
21417
21679
  };
21418
21680
 
21681
+ class ShellUnavailableEnv extends LocalExecutionEnv {
21682
+ #message;
21683
+ constructor(options, message) {
21684
+ super(options);
21685
+ this.#message = message;
21686
+ }
21687
+ async exec(command, options, context) {
21688
+ if (typeof command === "string")
21689
+ return err(new ExecutionError("shell_unavailable", this.#message));
21690
+ return super.exec(command, options, context);
21691
+ }
21692
+ }
21693
+
21419
21694
  class WorkspaceFiles {
21420
21695
  #connection;
21421
21696
  #starting;
@@ -21430,8 +21705,10 @@ class WorkspaceFiles {
21430
21705
  #waiting = new Set;
21431
21706
  #timer;
21432
21707
  limits;
21433
- constructor(limits = FILE_CACHE_LIMITS) {
21708
+ #shell;
21709
+ constructor(limits = FILE_CACHE_LIMITS, shell) {
21434
21710
  this.limits = limits;
21711
+ this.#shell = shell;
21435
21712
  this.#timer = setInterval(() => {
21436
21713
  for (const [key, item] of this.#cache)
21437
21714
  if (!item.waiters && Date.now() - item.used >= this.limits.idleMs)
@@ -21450,20 +21727,31 @@ class WorkspaceFiles {
21450
21727
  queued: this.#waiting.size
21451
21728
  };
21452
21729
  }
21453
- env(cwd) {
21730
+ shellStatus(options) {
21731
+ return this.#shell?.status(options);
21732
+ }
21733
+ #open() {
21454
21734
  if (this.#closed)
21455
21735
  throw new Error("File environment closed");
21456
21736
  this.#connection ??= new Connection({ command: [localDaemon()] });
21457
- return new LocalExecutionEnv({
21458
- connection: this.#connection,
21737
+ return this.#connection;
21738
+ }
21739
+ env(cwd) {
21740
+ const options = {
21741
+ connection: this.#open(),
21459
21742
  cwd,
21460
21743
  ...process.platform === "win32" ? { shellEnv: { PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" } } : {}
21461
- });
21744
+ };
21745
+ const shell = this.#shell?.status();
21746
+ if (shell === undefined)
21747
+ return new LocalExecutionEnv(options);
21748
+ if (shell.kind === "found")
21749
+ return new LocalExecutionEnv({ ...options, shellPath: shell.path });
21750
+ return new ShellUnavailableEnv(options, windowsShellMessage(shell));
21462
21751
  }
21463
21752
  async#ready() {
21464
21753
  if (!this.#starting) {
21465
- this.env(process.cwd());
21466
- const connection = this.#connection;
21754
+ const connection = this.#open();
21467
21755
  let timer;
21468
21756
  const work = Promise.race([
21469
21757
  connection.info().then(() => connection.session()),
@@ -21540,7 +21828,7 @@ class WorkspaceFiles {
21540
21828
  { path: item.root, recursive: true, exclude: { names: [".git"] } }
21541
21829
  ];
21542
21830
  const git = resolve4(item.root, ".git");
21543
- if (await stat(git).then(() => true, () => false))
21831
+ if (await stat2(git).then(() => true, () => false))
21544
21832
  targets.push({ path: git, recursive: true, exclude: { names: ["objects", "logs", "refs"] } });
21545
21833
  const result = await env.watch(targets, (change) => {
21546
21834
  for (const user of current.users) {
@@ -21648,7 +21936,7 @@ class WorkspaceFiles {
21648
21936
  const slash = query.lastIndexOf("/"), prefix = slash < 0 ? "" : query.slice(0, slash + 1), needle = query.slice(slash + 1);
21649
21937
  root = await realpath2(root);
21650
21938
  const base = await workspaceTarget(root, prefix);
21651
- if (!(await stat(base)).isDirectory())
21939
+ if (!(await stat2(base)).isDirectory())
21652
21940
  throw new Error("Workspace directory is unavailable.");
21653
21941
  return this.#withListing(root, base, signal, async (listing) => {
21654
21942
  const pattern = new RegExp([...needle].map((c) => c.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*"), /[A-Z]/.test(needle) ? "" : "i");
@@ -21678,7 +21966,7 @@ class WorkspaceFiles {
21678
21966
  const clean = dir === "" ? "" : relativeQuery(dir).replace(/\/+$/, "");
21679
21967
  root = await realpath2(root);
21680
21968
  const target = await workspaceTarget(root, clean);
21681
- if (!(await stat(target)).isDirectory())
21969
+ if (!(await stat2(target)).isDirectory())
21682
21970
  throw new Error("Not a directory.");
21683
21971
  const prefix = clean === "" ? "" : `${clean}/`;
21684
21972
  return this.#withListing(root, root, signal, async (listing) => {
@@ -21720,7 +22008,7 @@ class WorkspaceFiles {
21720
22008
  throw new Error("File not found.");
21721
22009
  throw error;
21722
22010
  }
21723
- const info = await stat(target);
22011
+ const info = await stat2(target);
21724
22012
  if (!info.isFile())
21725
22013
  throw new Error("Not a file.");
21726
22014
  const base = { path: clean, size: info.size, mtimeMs: info.mtimeMs };
@@ -21852,6 +22140,22 @@ var SearchSettingsDoc = defineDoc7({
21852
22140
  fork: "initial",
21853
22141
  initial: () => ({ provider: null })
21854
22142
  });
22143
+ var ProviderSettingsDoc = defineDoc7({
22144
+ kind: "app.neko.provider-settings",
22145
+ version: 1,
22146
+ scope: "conversation",
22147
+ history: "latest",
22148
+ fork: "initial",
22149
+ initial: () => ({ enabled: null })
22150
+ });
22151
+ var DefaultModelDoc = defineDoc7({
22152
+ kind: "app.neko.default-model",
22153
+ version: 1,
22154
+ scope: "conversation",
22155
+ history: "latest",
22156
+ fork: "initial",
22157
+ initial: () => ({ model: null })
22158
+ });
21855
22159
  var ContextSettingsDoc = defineDoc7({
21856
22160
  kind: "app.neko.context-settings",
21857
22161
  version: 1,
@@ -21872,6 +22176,7 @@ async function createNekoRuntime(options = {}) {
21872
22176
  const context = options.context ?? BACKGROUND_CONTEXT3;
21873
22177
  const defaultCwd = normalizeWorkspace(options.cwd);
21874
22178
  const model = options.model ?? DEFAULT_MODEL;
22179
+ const modelOverride = options.modelOverride;
21875
22180
  const providers = options.providers ?? [];
21876
22181
  const credentials = options.credentials ?? (options.storage === "memory" && !options.credentialsPath ? new InMemoryCredentialStore : new FileCredentialStore(options.credentialsPath));
21877
22182
  const runtimeKeys = new Map;
@@ -21891,16 +22196,47 @@ async function createNekoRuntime(options = {}) {
21891
22196
  delete: (id) => credentials.delete(id)
21892
22197
  } : credentials;
21893
22198
  const models = createModels({ credentials: effectiveCredentials });
21894
- models.setProvider(deepseekProvider());
22199
+ for (const provider of builtinProviders())
22200
+ if (isModelProvider(provider.id))
22201
+ models.setProvider(provider);
21895
22202
  for (const provider of providers)
21896
22203
  models.setProvider(provider);
22204
+ let enabled = new Set(normalizeEnabledProviders(null));
22205
+ const guardProvider = async (ref) => {
22206
+ if (!ref || !isModelProvider(ref.provider))
22207
+ return;
22208
+ const spec = credentialProvider(ref.provider);
22209
+ if (!enabled.has(ref.provider))
22210
+ throw new Error(`PROVIDER_DISABLED: ${spec.name} is not enabled. Enable it in /login or Web settings, or switch model.`);
22211
+ if (!await models.checkAuth(ref.provider))
22212
+ throw new Error(`PROVIDER_KEY_REQUIRED: Configure a ${spec.name} API key in /login or Web settings.`);
22213
+ };
21897
22214
  const resolveLevel = (ref, level) => {
21898
22215
  const info = models.getModel(ref.provider, ref.modelId);
21899
22216
  if (!info)
21900
22217
  throw new Error("Unknown model");
21901
22218
  return clampThinkingLevel(info, level);
21902
22219
  };
21903
- const workspaceFiles = new WorkspaceFiles;
22220
+ let modelInfoCache;
22221
+ const allModels = () => {
22222
+ modelInfoCache ??= models.getModels().map((model) => ({
22223
+ provider: model.provider,
22224
+ modelId: model.id,
22225
+ name: model.name,
22226
+ contextWindow: model.contextWindow,
22227
+ input: model.input,
22228
+ ...model.inputLimits ? { inputLimits: model.inputLimits } : {},
22229
+ reasoning: model.reasoning,
22230
+ supportedThinkingLevels: getSupportedThinkingLevels2(model),
22231
+ thinkingLevelAdjustments: Object.fromEntries(["off", "minimal", "low", "medium", "high", "xhigh", "max"].map((level) => [
22232
+ level,
22233
+ clampThinkingLevel(model, level)
22234
+ ]))
22235
+ }));
22236
+ return modelInfoCache;
22237
+ };
22238
+ const shellResolver = options.shellResolver ?? (process.platform === "win32" ? createWindowsShellResolver({ configPath: options.shellPath }) : undefined);
22239
+ const workspaceFiles = new WorkspaceFiles(undefined, shellResolver);
21904
22240
  const storage = options.storage === "memory" ? new MemoryStorage : await openNodeSqliteStorage(options.storagePath ?? DEFAULT_STORAGE_PATH);
21905
22241
  const registry = createRegistry();
21906
22242
  registry.install(createCodingTools({ images: await createNodePhotonImages() }));
@@ -21926,7 +22262,7 @@ async function createNekoRuntime(options = {}) {
21926
22262
  registry.install(createNekoPrompt(resourceSnapshots.current));
21927
22263
  registry.install(createSubagentExtension(async (id, requestId, cwd) => {
21928
22264
  resourceSnapshots.stage(id, requestId, cwd, await loadResources(cwd));
21929
- }, resourceSnapshots.discard));
22265
+ }, resourceSnapshots.discard, (ref) => guardProvider(ref)));
21930
22266
  const jobs = new JobManager;
21931
22267
  registry.install(jobs.extension);
21932
22268
  registry.install(createTodoExtension());
@@ -21944,7 +22280,7 @@ async function createNekoRuntime(options = {}) {
21944
22280
  registry,
21945
22281
  settings: {
21946
22282
  get compaction() {
21947
- return compactionPolicy(contextState, models.getModels().map((model) => model.contextWindow));
22283
+ return compactionPolicy(contextState, allModels().filter((model) => !isModelProvider(model.provider) || enabled.has(model.provider)).map((model) => model.contextWindow));
21948
22284
  }
21949
22285
  },
21950
22286
  onReport: options.onReport,
@@ -21986,13 +22322,15 @@ async function createNekoRuntime(options = {}) {
21986
22322
  throw error;
21987
22323
  }
21988
22324
  await questions.reconcileAll();
22325
+ const rootModel = modelOverride ?? model;
21989
22326
  const conversation = await harness.root(context, {
21990
22327
  agent: {
21991
- model,
21992
- thinkingLevel: resolveLevel(model, options.thinkingLevel ?? "high"),
22328
+ model: rootModel,
22329
+ thinkingLevel: resolveLevel(rootModel, options.thinkingLevel ?? "high"),
21993
22330
  cwd: defaultCwd
21994
22331
  }
21995
22332
  });
22333
+ enabled = new Set(normalizeEnabledProviders((await harness.snapshot(ProviderSettingsDoc, conversation.id, context))?.enabled));
21996
22334
  const savedSearch = (await harness.snapshot(SearchSettingsDoc, conversation.id, context))?.provider;
21997
22335
  if (isSearchProvider(savedSearch))
21998
22336
  searchProvider = savedSearch;
@@ -22038,7 +22376,7 @@ async function createNekoRuntime(options = {}) {
22038
22376
  };
22039
22377
  if (existing.length === 0)
22040
22378
  await touchConversation(conversation.id);
22041
- const metadata = new SessionMetadataManager(harness, storage, context, registry, options.onReport);
22379
+ const metadata = new SessionMetadataManager(harness, storage, context, registry, options.onReport, (provider, modelId) => guardProvider({ provider, modelId }).then(() => true, () => false));
22042
22380
  if (existing.length === 0)
22043
22381
  await metadata.initialize(conversation.id);
22044
22382
  for (const record of existing)
@@ -22099,9 +22437,8 @@ async function createNekoRuntime(options = {}) {
22099
22437
  data.skill = skill;
22100
22438
  });
22101
22439
  }
22102
- if (!duplicate && agent.model?.provider === "deepseek" && !await models.checkAuth("deepseek")) {
22103
- throw new Error("DEEPSEEK_KEY_REQUIRED: Configure a DeepSeek API key in /login or Web settings.");
22104
- }
22440
+ if (!duplicate)
22441
+ await guardProvider(agent.model);
22105
22442
  if (!duplicate && agent.model) {
22106
22443
  const resolved = models.getModel(agent.model.provider, agent.model.modelId);
22107
22444
  if (resolved)
@@ -22233,8 +22570,7 @@ async function createNekoRuntime(options = {}) {
22233
22570
  },
22234
22571
  async compact(instructions) {
22235
22572
  const agent = await target.agent(context);
22236
- if (agent.model?.provider === "deepseek" && !await models.checkAuth("deepseek"))
22237
- throw new Error("DEEPSEEK_KEY_REQUIRED: Configure DeepSeek in /login or Web settings.");
22573
+ await guardProvider(agent.model);
22238
22574
  await admit(target.id, async () => {
22239
22575
  const live = await harness.snapshot(importLiveDoc, target.id, context);
22240
22576
  if (live?.compactions?.length || (await readCompaction(harness, storage, target.id, context))?.phase === "waiting-to-apply")
@@ -22410,6 +22746,10 @@ async function createNekoRuntime(options = {}) {
22410
22746
  questions,
22411
22747
  closed: harness.closed,
22412
22748
  workspaceFiles,
22749
+ shellStatus(call) {
22750
+ const status = workspaceFiles.shellStatus(call);
22751
+ return status === undefined ? undefined : summarizeWindowsShell(status);
22752
+ },
22413
22753
  conversation,
22414
22754
  root: rootSession,
22415
22755
  submit: rootSession.submit,
@@ -22460,11 +22800,13 @@ async function createNekoRuntime(options = {}) {
22460
22800
  },
22461
22801
  async createConversation(createOptions) {
22462
22802
  const workspace = await addWorkspace(createOptions?.cwd ?? defaultCwd);
22803
+ const savedDefault = (await harness.snapshot(DefaultModelDoc, conversation.id, context))?.model ?? null;
22804
+ const effective = createOptions?.model ?? modelOverride ?? savedDefault ?? model;
22463
22805
  const created = await harness.createConversation({
22464
22806
  ownership: { kind: "ownerless" },
22465
22807
  agent: {
22466
- model: createOptions?.model ?? model,
22467
- thinkingLevel: resolveLevel(createOptions?.model ?? model, createOptions?.thinkingLevel ?? options.thinkingLevel ?? "high"),
22808
+ model: effective,
22809
+ thinkingLevel: resolveLevel(effective, createOptions?.thinkingLevel ?? options.thinkingLevel ?? "high"),
22468
22810
  cwd: workspace.path
22469
22811
  }
22470
22812
  }, context);
@@ -22494,6 +22836,8 @@ async function createNekoRuntime(options = {}) {
22494
22836
  const targetModel = ref && models.getModel(ref.provider, ref.modelId);
22495
22837
  if (!targetModel)
22496
22838
  throw new Error("Unknown model");
22839
+ if (change.model && isModelProvider(ref.provider) && !enabled.has(ref.provider))
22840
+ throw new Error(`PROVIDER_DISABLED: ${credentialProvider(ref.provider).name} is not enabled.`);
22497
22841
  await session.conversation.configure({ model: ref, thinkingLevel: clampThinkingLevel(targetModel, change.thinkingLevel ?? current.thinkingLevel) }, context);
22498
22842
  return true;
22499
22843
  });
@@ -22548,20 +22892,62 @@ async function createNekoRuntime(options = {}) {
22548
22892
  }
22549
22893
  },
22550
22894
  listModels() {
22551
- return models.getModels().map((model) => ({
22552
- provider: model.provider,
22553
- modelId: model.id,
22554
- name: model.name,
22555
- contextWindow: model.contextWindow,
22556
- input: model.input,
22557
- ...model.inputLimits ? { inputLimits: model.inputLimits } : {},
22558
- reasoning: model.reasoning,
22559
- supportedThinkingLevels: getSupportedThinkingLevels2(model),
22560
- thinkingLevelAdjustments: Object.fromEntries(["off", "minimal", "low", "medium", "high", "xhigh", "max"].map((level) => [
22561
- level,
22562
- clampThinkingLevel(model, level)
22563
- ]))
22564
- }));
22895
+ return allModels();
22896
+ },
22897
+ async listSelectableModels() {
22898
+ const availability = new Map;
22899
+ const result = [];
22900
+ for (const info of allModels()) {
22901
+ const managed = isModelProvider(info.provider);
22902
+ if (managed && !enabled.has(info.provider))
22903
+ continue;
22904
+ if (!availability.has(info.provider))
22905
+ availability.set(info.provider, !managed || Boolean(await models.checkAuth(info.provider)));
22906
+ result.push({ ...info, available: availability.get(info.provider) ?? false });
22907
+ }
22908
+ return result;
22909
+ },
22910
+ enabledProviders() {
22911
+ return MODEL_PROVIDERS.map((item) => item.id).filter((id) => enabled.has(id));
22912
+ },
22913
+ async setProviderEnabled(provider, value) {
22914
+ if (!isModelProvider(provider))
22915
+ throw new Error("Unknown model provider.");
22916
+ if (typeof value !== "boolean")
22917
+ throw new Error("Enabled must be a boolean.");
22918
+ if (!value && LOCKED_PROVIDERS.includes(provider))
22919
+ throw new Error("DeepSeek is always enabled.");
22920
+ const next = new Set(enabled);
22921
+ if (value)
22922
+ next.add(provider);
22923
+ else
22924
+ next.delete(provider);
22925
+ const list = normalizeEnabledProviders([...next]);
22926
+ await harness.commit(async (tx) => {
22927
+ (await tx.doc(ProviderSettingsDoc, conversation.id)).enabled = list;
22928
+ }, context);
22929
+ enabled = new Set(list);
22930
+ },
22931
+ async defaultModel() {
22932
+ const saved = (await harness.snapshot(DefaultModelDoc, conversation.id, context))?.model ?? null;
22933
+ if (modelOverride)
22934
+ return { effective: modelOverride, source: "cli" };
22935
+ if (saved)
22936
+ return { effective: saved, source: "saved" };
22937
+ if (options.model)
22938
+ return { effective: model, source: "config" };
22939
+ return { effective: model, source: "default" };
22940
+ },
22941
+ async setDefaultModel(ref) {
22942
+ if (ref !== null) {
22943
+ if (!models.getModel(ref.provider, ref.modelId))
22944
+ throw new Error("Unknown model");
22945
+ if (isModelProvider(ref.provider) && !enabled.has(ref.provider))
22946
+ throw new Error(`PROVIDER_DISABLED: ${credentialProvider(ref.provider).name} is not enabled.`);
22947
+ }
22948
+ await harness.commit(async (tx) => {
22949
+ (await tx.doc(DefaultModelDoc, conversation.id)).model = ref;
22950
+ }, context);
22565
22951
  },
22566
22952
  async getAgent(id) {
22567
22953
  const session = await sessionFor(id);
@@ -22626,8 +23012,8 @@ var nullableString = Type7.Union([str, nil]);
22626
23012
  var phase = Type7.Enum(["pending", "running", "done"]);
22627
23013
  var level = Type7.Enum(["project", "user", "builtin"]);
22628
23014
  var thinking = Type7.Enum(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
22629
- var model = Type7.Object({ provider: str, modelId: str });
22630
- var change = Type7.Object({ model: Type7.Optional(model), thinkingLevel: Type7.Optional(thinking) }, { additionalProperties: false });
23015
+ var model2 = Type7.Object({ provider: str, modelId: str });
23016
+ var change = Type7.Object({ model: Type7.Optional(model2), thinkingLevel: Type7.Optional(thinking) }, { additionalProperties: false });
22631
23017
  var usage = Type7.Object({ input: num, output: num, cacheRead: num, cacheWrite: num, cost: num });
22632
23018
  var image = Type7.Object({
22633
23019
  name: str,
@@ -22865,7 +23251,7 @@ var ChatViewSchema = Type7.Object({
22865
23251
  subagents: Type7.Optional(Type7.Array(DelegationSchema)),
22866
23252
  taskUsage: Type7.Optional(usage),
22867
23253
  model: str,
22868
- agent: Type7.Object({ model: Type7.Union([model, nil]), thinkingLevel: Type7.Union([thinking, nil]) }),
23254
+ agent: Type7.Object({ model: Type7.Union([model2, nil]), thinkingLevel: Type7.Union([thinking, nil]) }),
22869
23255
  cwd: str,
22870
23256
  busy: bool,
22871
23257
  status: nullableString,
@@ -22950,7 +23336,7 @@ var SessionSummarySchema = Type7.Object({
22950
23336
  firstPromptPreview: str,
22951
23337
  createdAt: num,
22952
23338
  lastMessageAt: Type7.Union([num, nil]),
22953
- model: Type7.Union([model, nil]),
23339
+ model: Type7.Union([model2, nil]),
22954
23340
  status: Type7.Enum(["idle", "running", "error"]),
22955
23341
  awaitingAnswer: Type7.Optional(bool),
22956
23342
  id: str
@@ -22961,11 +23347,12 @@ var WorkspaceStateSchema = Type7.Object({ revision: integer, workspaces: Type7.A
22961
23347
  var credentialStatus = Type7.Object({
22962
23348
  savedConfigured: bool,
22963
23349
  environmentConfigured: bool,
22964
- source: Type7.Enum(["saved", "environment", "runtime", "keyless", "none"])
23350
+ source: Type7.Enum(["saved", "environment", "runtime", "keyless", "none"]),
23351
+ enabled: Type7.Optional(bool)
22965
23352
  });
22966
23353
  var CredentialStateSchema = Type7.Object({
22967
23354
  revision: integer,
22968
- statuses: Type7.Object({ deepseek: credentialStatus, tavily: credentialStatus })
23355
+ statuses: Type7.Record(str, credentialStatus)
22969
23356
  });
22970
23357
  var AttachmentSchema = Type7.Object({ sessionId: str, attachmentId: str });
22971
23358
  var chatInput = Type7.Object({ sessionId: str, attachmentId: str }, { additionalProperties: false });
@@ -23007,6 +23394,7 @@ var modelInfo = Type7.Object({
23007
23394
  provider: str,
23008
23395
  modelId: str,
23009
23396
  name: str,
23397
+ available: bool,
23010
23398
  contextWindow: num,
23011
23399
  input: Type7.Array(str),
23012
23400
  inputLimits: Type7.Optional(inputLimits),
@@ -23112,16 +23500,17 @@ var NekoWorkspaces = contract("neko.workspaces", {
23112
23500
  }
23113
23501
  }
23114
23502
  });
23115
- var provider = Type7.Enum(["deepseek", "tavily"]);
23503
+ var provider = Type7.String({ minLength: 1, maxLength: 64 });
23116
23504
  var NekoCredentials = contract("neko.credentials", {
23117
- version: 1,
23505
+ version: 2,
23118
23506
  input: Type7.Object({}, { additionalProperties: false }),
23119
23507
  variants: {
23120
23508
  ready: {
23121
23509
  state: { state: CredentialStateSchema },
23122
23510
  methods: {
23123
23511
  save: rpc(Type7.Object({ provider, key: Type7.String({ minLength: 1, maxLength: 8192 }) }, { additionalProperties: false }), nil),
23124
- remove: rpc(Type7.Object({ provider }, { additionalProperties: false }), nil)
23512
+ remove: rpc(Type7.Object({ provider }, { additionalProperties: false }), nil),
23513
+ setEnabled: rpc(Type7.Object({ provider, enabled: bool }, { additionalProperties: false }), nil)
23125
23514
  }
23126
23515
  }
23127
23516
  }
@@ -23198,6 +23587,24 @@ var NekoContextSettings = contract("neko.context-settings", {
23198
23587
  }
23199
23588
  }
23200
23589
  });
23590
+ var defaultModelSource = Type7.Enum(["cli", "saved", "config", "default"]);
23591
+ var DefaultModelStateSchema = Type7.Object({
23592
+ effective: model2,
23593
+ source: defaultModelSource
23594
+ });
23595
+ var NekoDefaultModel = contract("neko.default-model", {
23596
+ version: 1,
23597
+ input: Type7.Object({}, { additionalProperties: false }),
23598
+ variants: {
23599
+ ready: {
23600
+ state: { state: DefaultModelStateSchema },
23601
+ methods: {
23602
+ set: rpc(model2, nil),
23603
+ clear: rpc(nil, nil)
23604
+ }
23605
+ }
23606
+ }
23607
+ });
23201
23608
  var FileCandidatesSchema = Type7.Object({
23202
23609
  entries: Type7.Array(Type7.Object({ name: str, path: str, kind: Type7.Enum(["file", "directory"]) }), { maxItems: 100 }),
23203
23610
  limited: bool,
@@ -23245,6 +23652,14 @@ var NekoSkillMarket = contract("neko.skill-market", {
23245
23652
  }
23246
23653
  }
23247
23654
  });
23655
+ var ShellStatusSchema = Type7.Union([
23656
+ Type7.Object({ status: Type7.Literal("ready"), path: str, source: Type7.Enum(["config", "auto"]) }),
23657
+ Type7.Object({
23658
+ status: Type7.Literal("missing"),
23659
+ reason: Type7.Enum(["not_found", "invalid_config"]),
23660
+ detail: Type7.Optional(str)
23661
+ })
23662
+ ]);
23248
23663
  var ApplicationInfoSchema = Type7.Object({
23249
23664
  runningVersion: str,
23250
23665
  installedVersion: nullableString,
@@ -23252,7 +23667,8 @@ var ApplicationInfoSchema = Type7.Object({
23252
23667
  latestVersion: nullableString,
23253
23668
  checkedAt: Type7.Union([integer, nil]),
23254
23669
  checkState: Type7.Enum(["unknown", "checking", "ok", "failed", "development"]),
23255
- serverId: str
23670
+ serverId: str,
23671
+ shell: Type7.Optional(ShellStatusSchema)
23256
23672
  });
23257
23673
  var NekoApplicationInfo = contract("neko.application-info", {
23258
23674
  version: 1,
@@ -23290,7 +23706,7 @@ function isMobilePrincipal(principal) {
23290
23706
 
23291
23707
  // packages/gateway/src/mobile/store.ts
23292
23708
  import { randomBytes } from "crypto";
23293
- import { chmod as chmod2, mkdir as mkdir2, readFile as readFile4, rename as rename2, rm, writeFile } from "fs/promises";
23709
+ import { chmod as chmod2, mkdir as mkdir2, readFile as readFile4, rm, writeFile } from "fs/promises";
23294
23710
  import { join as join6 } from "path";
23295
23711
  var MOBILE_STATE_FORMAT_VERSION = 1;
23296
23712
  var MOBILE_DEFAULT_PORT = 8323;
@@ -23418,9 +23834,10 @@ async function writeFileAtomic(filePath, contents, mode) {
23418
23834
  await writeFile(tempPath, contents, { mode });
23419
23835
  if (POSIX)
23420
23836
  await chmod2(tempPath, mode);
23421
- await rename2(tempPath, filePath);
23422
- } finally {
23837
+ await renameWithRetry(tempPath, filePath);
23838
+ } catch (error) {
23423
23839
  await rm(tempPath, { force: true });
23840
+ throw error;
23424
23841
  }
23425
23842
  }
23426
23843
  async function writeSecretFile(filePath, contents) {
@@ -23976,8 +24393,8 @@ function subagentClient(connect) {
23976
24393
  import { createHash as createHash2 } from "crypto";
23977
24394
 
23978
24395
  // packages/core/src/application-version.ts
23979
- var RUNNING_VERSION = "0.4.5";
23980
- var APPLICATION_PROTOCOL = 6;
24396
+ var RUNNING_VERSION = "0.4.7";
24397
+ var APPLICATION_PROTOCOL = 8;
23981
24398
  function stableVersion(value) {
23982
24399
  return typeof value === "string" && /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(value) && value.split(".").every((n) => Number.isSafeInteger(Number(n)));
23983
24400
  }
@@ -26626,6 +27043,10 @@ function toMessage(error) {
26626
27043
  }
26627
27044
  return String(error);
26628
27045
  }
27046
+ function normalizeLocalAddress(address) {
27047
+ const mapped = address?.startsWith("::ffff:") ? address.slice("::ffff:".length) : undefined;
27048
+ return mapped !== undefined && isIpv4Literal(mapped) ? mapped : address;
27049
+ }
26629
27050
 
26630
27051
  class MobileGateway {
26631
27052
  serverId;
@@ -26954,11 +27375,12 @@ class MobileGateway {
26954
27375
  connection.sendNotification(method, params);
26955
27376
  }
26956
27377
  advertisedAddress(localAddress) {
27378
+ const normalized = normalizeLocalAddress(localAddress);
26957
27379
  const state = this.stateValue;
26958
- if (state.status === "listening" && state.mode === "wsl-relay" && localAddress === state.bindAddress) {
27380
+ if (state.status === "listening" && state.mode === "wsl-relay" && normalized === state.bindAddress) {
26959
27381
  return state.address;
26960
27382
  }
26961
- return localAddress;
27383
+ return normalized;
26962
27384
  }
26963
27385
  requireStore() {
26964
27386
  if (this.storeHandle === undefined)
@@ -27219,7 +27641,7 @@ import { randomUUID as randomUUID5 } from "crypto";
27219
27641
  import { ChordError as ChordError5, local as local3, method as method2 } from "@earendil-works/chord";
27220
27642
 
27221
27643
  // packages/core/src/application-updates.ts
27222
- import { mkdir as mkdir3, readFile as readFile6, rename as rename3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
27644
+ import { mkdir as mkdir3, readFile as readFile6, rm as rm3, writeFile as writeFile3 } from "fs/promises";
27223
27645
  import { homedir as homedir5 } from "os";
27224
27646
  import { dirname as dirname5, join as join10 } from "path";
27225
27647
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -27349,7 +27771,7 @@ class UpdateChecker {
27349
27771
  try {
27350
27772
  await mkdir3(dirname5(this.#cachePath), { recursive: true });
27351
27773
  await writeFile3(temporary, JSON.stringify({ latestVersion: value.version, checkedAt: now }), { mode: 384 });
27352
- await rename3(temporary, this.#cachePath);
27774
+ await renameWithRetry(temporary, this.#cachePath);
27353
27775
  } catch {} finally {
27354
27776
  await rm3(temporary, { force: true }).catch(() => {});
27355
27777
  }
@@ -27995,8 +28417,6 @@ function serveSessions(host, runtime, context) {
27995
28417
  for (const id of input.images ?? [])
27996
28418
  uploads.value.release(id);
27997
28419
  } catch (error) {
27998
- if (error instanceof Error && error.message.startsWith("DEEPSEEK_KEY_REQUIRED:"))
27999
- throw new ChordError4("invalid", "DEEPSEEK_KEY_REQUIRED: Configure DeepSeek in /login or Web settings.");
28000
28420
  throw new ChordError4("invalid", error instanceof Error ? error.message : "Cannot submit message");
28001
28421
  }
28002
28422
  return null;
@@ -28085,7 +28505,7 @@ function serveSessions(host, runtime, context) {
28085
28505
 
28086
28506
  // packages/gateway/src/skill-market.ts
28087
28507
  import { createHash as createHash6, randomUUID as randomUUID4 } from "crypto";
28088
- import { mkdir as mkdir4, readdir as readdir2, rename as rename4, rm as rm4, stat as stat2, writeFile as writeFile4 } from "fs/promises";
28508
+ import { mkdir as mkdir4, readdir as readdir2, rm as rm4, stat as stat3, writeFile as writeFile4 } from "fs/promises";
28089
28509
  import { homedir as homedir6 } from "os";
28090
28510
  import { dirname as dirname6, join as join11 } from "path";
28091
28511
 
@@ -28230,11 +28650,11 @@ var ec = [
28230
28650
  "stream finishing",
28231
28651
  "invalid zip data"
28232
28652
  ];
28233
- var err = function(ind, msg, nt) {
28653
+ var err2 = function(ind, msg, nt) {
28234
28654
  var e = new Error(msg || ec[ind]);
28235
28655
  e.code = ind;
28236
28656
  if (Error.captureStackTrace)
28237
- Error.captureStackTrace(e, err);
28657
+ Error.captureStackTrace(e, err2);
28238
28658
  if (!nt)
28239
28659
  throw e;
28240
28660
  return e;
@@ -28267,7 +28687,7 @@ var inflt = function(dat, st, buf, dict) {
28267
28687
  var s = shft(pos) + 4, l = dat[s - 4] | dat[s - 3] << 8, t = s + l;
28268
28688
  if (t > sl) {
28269
28689
  if (noSt)
28270
- err(0);
28690
+ err2(0);
28271
28691
  break;
28272
28692
  }
28273
28693
  if (resize)
@@ -28313,10 +28733,10 @@ var inflt = function(dat, st, buf, dict) {
28313
28733
  lm = hMap(lt, lbt, 1);
28314
28734
  dm = hMap(dt, dbt, 1);
28315
28735
  } else
28316
- err(1);
28736
+ err2(1);
28317
28737
  if (pos > tbts) {
28318
28738
  if (noSt)
28319
- err(0);
28739
+ err2(0);
28320
28740
  break;
28321
28741
  }
28322
28742
  }
@@ -28329,11 +28749,11 @@ var inflt = function(dat, st, buf, dict) {
28329
28749
  pos += c & 15;
28330
28750
  if (pos > tbts) {
28331
28751
  if (noSt)
28332
- err(0);
28752
+ err2(0);
28333
28753
  break;
28334
28754
  }
28335
28755
  if (!c)
28336
- err(2);
28756
+ err2(2);
28337
28757
  if (sym < 256)
28338
28758
  buf[bt++] = sym;
28339
28759
  else if (sym == 256) {
@@ -28348,7 +28768,7 @@ var inflt = function(dat, st, buf, dict) {
28348
28768
  }
28349
28769
  var d = dm[bits16(dat, pos) & dms], dsym = d >> 4;
28350
28770
  if (!d)
28351
- err(3);
28771
+ err2(3);
28352
28772
  pos += d & 15;
28353
28773
  var dt = fd[dsym];
28354
28774
  if (dsym > 3) {
@@ -28357,7 +28777,7 @@ var inflt = function(dat, st, buf, dict) {
28357
28777
  }
28358
28778
  if (pos > tbts) {
28359
28779
  if (noSt)
28360
- err(0);
28780
+ err2(0);
28361
28781
  break;
28362
28782
  }
28363
28783
  if (resize)
@@ -28366,7 +28786,7 @@ var inflt = function(dat, st, buf, dict) {
28366
28786
  if (bt < dt) {
28367
28787
  var shift = dl - dt, dend = Math.min(dt, end);
28368
28788
  if (shift + bt < 0)
28369
- err(3);
28789
+ err2(3);
28370
28790
  for (;bt < dend; ++bt)
28371
28791
  buf[bt] = dict[shift + bt];
28372
28792
  }
@@ -28426,7 +28846,7 @@ function strFromU8(dat, latin1) {
28426
28846
  } else {
28427
28847
  var _a = dutf8(dat), { s, r } = _a;
28428
28848
  if (r.length)
28429
- err(8);
28849
+ err2(8);
28430
28850
  return s;
28431
28851
  }
28432
28852
  }
@@ -28453,7 +28873,7 @@ var z64hs = function(d, b, l, z, sc, su, off) {
28453
28873
  }
28454
28874
  }
28455
28875
  if (z < 2)
28456
- err(13);
28876
+ err2(13);
28457
28877
  }
28458
28878
  return [sc, su, off, 0];
28459
28879
  };
@@ -28462,7 +28882,7 @@ function unzipSync(data, opts) {
28462
28882
  var e = data.length - 22;
28463
28883
  for (;b4(data, e) != 101010256; --e) {
28464
28884
  if (!e || data.length - e > 65558)
28465
- err(13);
28885
+ err2(13);
28466
28886
  }
28467
28887
  var c = b2(data, e + 8);
28468
28888
  if (!c)
@@ -28492,7 +28912,7 @@ function unzipSync(data, opts) {
28492
28912
  else if (c_2 == 8)
28493
28913
  files[fn] = inflateSync(data.subarray(b, b + sc), { out: new u8(su) });
28494
28914
  else
28495
- err(14, "unknown compression type " + c_2);
28915
+ err2(14, "unknown compression type " + c_2);
28496
28916
  }
28497
28917
  }
28498
28918
  return files;
@@ -28533,6 +28953,7 @@ function createSkillMarketClient(options) {
28533
28953
  const home = options.home ?? homedir6();
28534
28954
  const fetchImpl = options.fetchImpl ?? fetch;
28535
28955
  const now = options.now ?? Date.now;
28956
+ const placeRename = options.renameImpl;
28536
28957
  let base;
28537
28958
  try {
28538
28959
  base = new URL(options.marketUrl);
@@ -28637,6 +29058,8 @@ function createSkillMarketClient(options) {
28637
29058
  throw new SkillMarketError("market_unavailable", "Project-scope install needs a workspaceRoot");
28638
29059
  return projectSkillsDir(workspaceRoot);
28639
29060
  };
29061
+ const BACKUP_PREFIX = ".bak-";
29062
+ const UUID_LENGTH = 36;
28640
29063
  const sweepTempDirs = async (skillsDir) => {
28641
29064
  const parent = dirname6(skillsDir);
28642
29065
  let names = [];
@@ -28646,11 +29069,26 @@ function createSkillMarketClient(options) {
28646
29069
  return;
28647
29070
  }
28648
29071
  await Promise.all(names.filter((n) => n.startsWith(".tmp-")).map((n) => rm4(join11(parent, n), { recursive: true, force: true }).catch(() => {})));
29072
+ for (const n of names.filter((n) => n.startsWith(BACKUP_PREFIX))) {
29073
+ const backup = join11(parent, n);
29074
+ const skill = n.slice(BACKUP_PREFIX.length + UUID_LENGTH + 1);
29075
+ if (skill === "" || skill.includes("/") || skill.includes("\\") || skill === "." || skill === "..")
29076
+ continue;
29077
+ const target = join11(skillsDir, skill);
29078
+ if (await stat3(target).catch(() => {
29079
+ return;
29080
+ }) === undefined) {
29081
+ await mkdir4(skillsDir, { recursive: true }).catch(() => {});
29082
+ await renameWithRetry(backup, target).catch(() => {});
29083
+ } else {
29084
+ await rm4(backup, { recursive: true, force: true }).catch(() => {});
29085
+ }
29086
+ }
28649
29087
  };
28650
29088
  const install = async ({ name, scope, workspaceRoot, overwrite }) => {
28651
29089
  const skillsDir = skillsDirFor(scope, workspaceRoot);
28652
29090
  const target = join11(skillsDir, name);
28653
- const exists = await stat2(target).catch(() => {
29091
+ const exists = await stat3(target).catch(() => {
28654
29092
  return;
28655
29093
  }) !== undefined;
28656
29094
  if (exists && overwrite !== true)
@@ -28665,17 +29103,27 @@ function createSkillMarketClient(options) {
28665
29103
  await mkdir4(dirname6(dest), { recursive: true });
28666
29104
  await writeFile4(dest, bytes);
28667
29105
  }
28668
- if (exists)
28669
- await rm4(target, { recursive: true, force: true });
28670
29106
  await mkdir4(skillsDir, { recursive: true });
28671
- await rename4(temp, target);
29107
+ if (exists) {
29108
+ const backup = join11(dirname6(skillsDir), `${BACKUP_PREFIX}${randomUUID4()}-${name}`);
29109
+ await renameWithRetry(target, backup);
29110
+ try {
29111
+ await renameWithRetry(temp, target, placeRename ? { rename: placeRename } : {});
29112
+ } catch (error) {
29113
+ await renameWithRetry(backup, target).catch(() => {});
29114
+ throw error;
29115
+ }
29116
+ await rm4(backup, { recursive: true, force: true }).catch(() => {});
29117
+ } else {
29118
+ await renameWithRetry(temp, target, placeRename ? { rename: placeRename } : {});
29119
+ }
28672
29120
  } finally {
28673
29121
  await rm4(temp, { recursive: true, force: true }).catch(() => {});
28674
29122
  }
28675
29123
  let shadowedBy;
28676
29124
  if (scope === "user" && workspaceRoot !== undefined && workspaceRoot !== "") {
28677
29125
  const projectDir = join11(projectSkillsDir(workspaceRoot), name);
28678
- if (await stat2(projectDir).catch(() => {
29126
+ if (await stat3(projectDir).catch(() => {
28679
29127
  return;
28680
29128
  }) !== undefined)
28681
29129
  shadowedBy = "project";
@@ -28685,7 +29133,7 @@ function createSkillMarketClient(options) {
28685
29133
  const uninstall = async ({ name, scope, workspaceRoot }) => {
28686
29134
  const skillsDir = skillsDirFor(scope, workspaceRoot);
28687
29135
  const target = join11(skillsDir, name);
28688
- if (await stat2(target).catch(() => {
29136
+ if (await stat3(target).catch(() => {
28689
29137
  return;
28690
29138
  }) === undefined)
28691
29139
  throw new SkillMarketError("skill_not_installed", `Skill ${name} is not installed at ${scope} level`);
@@ -28719,7 +29167,7 @@ function createSkillMarketClient(options) {
28719
29167
 
28720
29168
  // packages/gateway/src/workspaces.ts
28721
29169
  import { constants as constants3 } from "fs";
28722
- import { access, readdir as readdir3, stat as stat3 } from "fs/promises";
29170
+ import { access, readdir as readdir3, stat as stat4 } from "fs/promises";
28723
29171
  import { dirname as dirname7, isAbsolute as isAbsolute4, join as join12 } from "path";
28724
29172
  async function browseWorkspaces(options) {
28725
29173
  const requested = options.path.replace(/^\/+([A-Za-z]:[\\/])/, "$1");
@@ -28751,7 +29199,7 @@ async function browseWorkspaces(options) {
28751
29199
  if (!entry.isDirectory() && !entry.isSymbolicLink())
28752
29200
  continue;
28753
29201
  try {
28754
- if (!(await stat3(target)).isDirectory())
29202
+ if (!(await stat4(target)).isDirectory())
28755
29203
  continue;
28756
29204
  await access(target, constants3.R_OK | constants3.X_OK);
28757
29205
  directories.push({ name: entry.name, path: target });
@@ -28788,9 +29236,11 @@ var ServerState = local3("neko.server-state", {
28788
29236
  credentials: CredentialStateSchema,
28789
29237
  searchSettings: SearchSettingsStateSchema,
28790
29238
  contextSettings: ContextSettingsStateSchema,
29239
+ defaultModel: DefaultModelStateSchema,
28791
29240
  refreshCredentials: method2(Type12.Null(), Type12.Null()),
28792
29241
  refreshSearchSettings: method2(Type12.Null(), Type12.Null()),
28793
- refreshContextSettings: method2(Type12.Null(), Type12.Null())
29242
+ refreshContextSettings: method2(Type12.Null(), Type12.Null()),
29243
+ refreshDefaultModel: method2(Type12.Null(), Type12.Null())
28794
29244
  }
28795
29245
  });
28796
29246
  async function createNekoServerServices(options) {
@@ -28798,12 +29248,15 @@ async function createNekoServerServices(options) {
28798
29248
  const updates = new UpdateChecker(options.updates);
28799
29249
  await updates.local();
28800
29250
  const updateListeners = new Set;
28801
- const info = (value) => ({ ...value, serverId: options.serverId });
28802
- let published = JSON.stringify(updates.state), closed = false;
29251
+ const info = (value) => {
29252
+ const shell = runtime.shellStatus();
29253
+ return { ...value, serverId: options.serverId, ...shell === undefined ? {} : { shell } };
29254
+ };
29255
+ let published = JSON.stringify(info(updates.state)), closed = false;
28803
29256
  const publishUpdates = () => {
28804
29257
  if (closed)
28805
29258
  return;
28806
- const next = JSON.stringify(updates.state);
29259
+ const next = JSON.stringify(info(updates.state));
28807
29260
  if (next === published)
28808
29261
  return;
28809
29262
  published = next;
@@ -28822,6 +29275,7 @@ async function createNekoServerServices(options) {
28822
29275
  variant: "ready",
28823
29276
  info: current,
28824
29277
  check: async () => {
29278
+ runtime.shellStatus({ refresh: true });
28825
29279
  await updates.check(true);
28826
29280
  publishUpdates();
28827
29281
  return info(updates.state);
@@ -28868,13 +29322,20 @@ async function createNekoServerServices(options) {
28868
29322
  status: info.status,
28869
29323
  awaitingAnswer: info.awaitingAnswer
28870
29324
  }));
28871
- const readStatuses = async () => Object.fromEntries(await Promise.all(CREDENTIAL_PROVIDERS.map(async (spec) => [spec.id, await runtime.credentialStatus(spec.id)])));
28872
- const [sessions, workspaces, statuses, search, contextSettings0] = await Promise.all([
29325
+ const readStatuses = async () => {
29326
+ const enabled = new Set(runtime.enabledProviders());
29327
+ return Object.fromEntries(await Promise.all(CREDENTIAL_PROVIDERS.map(async (spec) => {
29328
+ const status = await runtime.credentialStatus(spec.id);
29329
+ return [spec.id, spec.kind === "model" ? { ...status, enabled: enabled.has(spec.id) } : status];
29330
+ })));
29331
+ };
29332
+ const [sessions, workspaces, statuses, search, contextSettings0, defaultModel0] = await Promise.all([
28873
29333
  list(),
28874
29334
  runtime.listWorkspaces(),
28875
29335
  readStatuses(),
28876
29336
  runtime.searchSettings(),
28877
- runtime.contextSettings()
29337
+ runtime.contextSettings(),
29338
+ runtime.defaultModel()
28878
29339
  ]);
28879
29340
  let mutationTail = Promise.resolve();
28880
29341
  const serialize = (fn) => {
@@ -28891,6 +29352,7 @@ async function createNekoServerServices(options) {
28891
29352
  revision: 1,
28892
29353
  ...contextSettings0
28893
29354
  });
29355
+ const defaultModel = self2.state("defaultModel", DefaultModelStateSchema, defaultModel0);
28894
29356
  const refreshContextSettings = async (_input, ctx) => {
28895
29357
  const next = await runtime.contextSettings();
28896
29358
  const { revision, ...current } = contextSettings.value;
@@ -28898,6 +29360,12 @@ async function createNekoServerServices(options) {
28898
29360
  contextSettings.replace(ctx, { revision: revision + 1, ...next });
28899
29361
  return null;
28900
29362
  };
29363
+ const refreshDefaultModel = async (_input, ctx) => {
29364
+ const next = await runtime.defaultModel();
29365
+ if (JSON.stringify(next) !== JSON.stringify(defaultModel.value))
29366
+ defaultModel.replace(ctx, next);
29367
+ return null;
29368
+ };
28901
29369
  const refreshSearchSettings = async (_input, ctx) => {
28902
29370
  const next = await runtime.searchSettings();
28903
29371
  const current = searchSettings.value;
@@ -28926,6 +29394,7 @@ async function createNekoServerServices(options) {
28926
29394
  credentials,
28927
29395
  searchSettings,
28928
29396
  contextSettings,
29397
+ defaultModel,
28929
29398
  async refreshCredentials(_input, ctx) {
28930
29399
  credentials.replace(ctx, {
28931
29400
  revision: credentials.value.revision + 1,
@@ -28934,7 +29403,8 @@ async function createNekoServerServices(options) {
28934
29403
  return null;
28935
29404
  },
28936
29405
  refreshSearchSettings,
28937
- refreshContextSettings
29406
+ refreshContextSettings,
29407
+ refreshDefaultModel
28938
29408
  };
28939
29409
  }, { lifetime: "host" });
28940
29410
  const shared = (self2) => self2.get(ServerState, {});
@@ -28957,7 +29427,17 @@ async function createNekoServerServices(options) {
28957
29427
  remove: async (input, ctx) => {
28958
29428
  await runtime.removeCredential(input.provider);
28959
29429
  return hub.refreshCredentials(null, ctx);
28960
- }
29430
+ },
29431
+ setEnabled: (input, ctx) => serialize(async () => {
29432
+ if (!isModelProvider(input.provider))
29433
+ throw new ChordError5("invalid", "Unknown model provider.");
29434
+ try {
29435
+ await runtime.setProviderEnabled(input.provider, input.enabled);
29436
+ } catch (error) {
29437
+ throw new ChordError5("invalid", error instanceof Error ? error.message : "Cannot change provider.");
29438
+ }
29439
+ return hub.refreshCredentials(null, ctx);
29440
+ })
28961
29441
  };
28962
29442
  });
28963
29443
  host.serve(NekoSearchSettings, (_input, _principal, self2) => {
@@ -28986,9 +29466,24 @@ async function createNekoServerServices(options) {
28986
29466
  })
28987
29467
  };
28988
29468
  });
29469
+ host.serve(NekoDefaultModel, (_input, _principal, self2) => {
29470
+ const hub = shared(self2);
29471
+ return {
29472
+ variant: "ready",
29473
+ state: hub.defaultModel,
29474
+ set: (input, ctx) => serialize(async () => {
29475
+ await runtime.setDefaultModel(input);
29476
+ return hub.refreshDefaultModel(null, ctx);
29477
+ }),
29478
+ clear: (_input, ctx) => serialize(async () => {
29479
+ await runtime.setDefaultModel(null);
29480
+ return hub.refreshDefaultModel(null, ctx);
29481
+ })
29482
+ };
29483
+ });
28989
29484
  host.serve(NekoModels, () => ({
28990
29485
  variant: "ready",
28991
- list: async () => [...runtime.listModels()].map((value) => ({
29486
+ list: async () => (await runtime.listSelectableModels()).map((value) => ({
28992
29487
  ...value,
28993
29488
  input: [...value.input],
28994
29489
  supportedThinkingLevels: [...value.supportedThinkingLevels]
@@ -29282,7 +29777,6 @@ var SERVER_ID_FILENAME = "gateway-server-id";
29282
29777
  async function startGateway(options = {}) {
29283
29778
  const context = options.context ?? BACKGROUND_CONTEXT4;
29284
29779
  const fileConfig = loadNekoConfig(options.configDir);
29285
- const model = options.model ?? fileConfig.defaultModel ?? DEFAULT_MODEL;
29286
29780
  const storagePath = options.storagePath ?? fileConfig.storagePath ?? DEFAULT_STORAGE_PATH;
29287
29781
  const host = options.host ?? fileConfig.gateway?.host ?? DEFAULT_HOST;
29288
29782
  const port = options.port !== undefined ? options.port : fileConfig.gateway?.port ?? (process.platform === "win32" ? 0 : DEFAULT_PORT);
@@ -29300,12 +29794,15 @@ async function startGateway(options = {}) {
29300
29794
  ...options.apiKey !== undefined ? { apiKey: options.apiKey } : {},
29301
29795
  ...searchProvider !== undefined ? { searchProvider } : {},
29302
29796
  ...fileConfig.context !== undefined ? { contextSettings: fileConfig.context } : {},
29303
- model,
29797
+ ...fileConfig.defaultModel !== undefined ? { model: fileConfig.defaultModel } : {},
29798
+ ...options.model !== undefined ? { modelOverride: options.model } : {},
29304
29799
  thinkingLevel: options.thinkingLevel ?? fileConfig.thinkingLevel ?? "high",
29305
29800
  ...options.cwd !== undefined ? { cwd: options.cwd } : {},
29306
29801
  ...options.providers !== undefined ? { providers: options.providers } : {},
29307
29802
  ...options.extensions !== undefined ? { extensions: options.extensions } : {},
29308
- ...options.promptHome !== undefined ? { promptHome: options.promptHome } : {}
29803
+ ...options.promptHome !== undefined ? { promptHome: options.promptHome } : {},
29804
+ ...fileConfig.shell?.path !== undefined ? { shellPath: fileConfig.shell.path } : {},
29805
+ ...options.shellResolver !== undefined ? { shellResolver: options.shellResolver } : {}
29309
29806
  });
29310
29807
  try {
29311
29808
  return await bindGateway(runtime, {
@@ -29315,12 +29812,12 @@ async function startGateway(options = {}) {
29315
29812
  socketPath,
29316
29813
  serverId,
29317
29814
  nekoDir,
29318
- model,
29319
29815
  marketUrl,
29320
29816
  promptHome: options.promptHome,
29321
29817
  updates: options.updates,
29322
29818
  updateIntervalMs: options.updateIntervalMs,
29323
29819
  mobileServerName: options.mobileServerName,
29820
+ mobileNetwork: options.mobileNetwork,
29324
29821
  onError: options.onError
29325
29822
  });
29326
29823
  } catch (error) {
@@ -29355,12 +29852,13 @@ async function bindGateway(runtime, options) {
29355
29852
  wsListener = createWsListener({ host, port, onError: options.onError });
29356
29853
  listeners.push(wsListener);
29357
29854
  }
29855
+ const acceptLocal = (link) => chord.accept(link, {
29856
+ principal: { kind: "local", connectionId: randomUUID6() },
29857
+ onProtocolMismatch: ({ peer }) => options.onError?.(new Error(`Rejected a client on Chord protocol ${peer ?? "unknown"}; restart or update it.`))
29858
+ });
29358
29859
  try {
29359
29860
  for (const listener of listeners)
29360
- await listener.start((link) => chord.accept(link, {
29361
- principal: { kind: "local", connectionId: randomUUID6() },
29362
- onProtocolMismatch: ({ peer }) => options.onError?.(new Error(`Rejected a client on Chord protocol ${peer ?? "unknown"}; restart or update it.`))
29363
- }));
29861
+ await listener.start(acceptLocal);
29364
29862
  } catch (error) {
29365
29863
  await Promise.allSettled(listeners.map((listener) => listener.close()));
29366
29864
  await chord.close();
@@ -29376,6 +29874,7 @@ async function bindGateway(runtime, options) {
29376
29874
  serverId,
29377
29875
  serverName: options.mobileServerName,
29378
29876
  onError: options.onError,
29877
+ network: options.mobileNetwork,
29379
29878
  chord: {
29380
29879
  accept: (link, acceptOptions) => chord.accept(link, acceptOptions),
29381
29880
  invalidatePrincipal: (match) => chord.invalidatePrincipal(match)
@@ -29400,6 +29899,9 @@ async function bindGateway(runtime, options) {
29400
29899
  port: wsListener?.port,
29401
29900
  runtime,
29402
29901
  mobile,
29902
+ accept(link) {
29903
+ acceptLocal(link);
29904
+ },
29403
29905
  async close() {
29404
29906
  if (closing)
29405
29907
  return closing;
@@ -29485,14 +29987,15 @@ function installShutdown(options = {}) {
29485
29987
  exit(1);
29486
29988
  });
29487
29989
  };
29488
- const onSignal = () => {
29990
+ const begin = (reason) => {
29489
29991
  if (controller.signal.aborted) {
29490
29992
  options.log?.("\u518D\u6B21\u6536\u5230\u7EC8\u6B62\u4FE1\u53F7\uFF0C\u5F3A\u5236\u9000\u51FA\u3002");
29491
29993
  exit(130);
29492
29994
  return;
29493
29995
  }
29494
- options.log?.(`
29495
- \u6B63\u5728\u5173\u95ED\u2026`);
29996
+ options.log?.(reason === undefined ? `
29997
+ \u6B63\u5728\u5173\u95ED\u2026` : `
29998
+ \u6B63\u5728\u5173\u95ED\uFF08${reason}\uFF09\u2026`);
29496
29999
  controller.abort();
29497
30000
  timer = setTimeout(() => {
29498
30001
  options.log?.("\u5173\u95ED\u8D85\u65F6\uFF0C\u5F3A\u5236\u9000\u51FA\u3002");
@@ -29501,6 +30004,7 @@ function installShutdown(options = {}) {
29501
30004
  timer.unref?.();
29502
30005
  runCleanup();
29503
30006
  };
30007
+ const onSignal = () => begin(undefined);
29504
30008
  const installed = [];
29505
30009
  for (const signal of shutdownSignals(proc.platform)) {
29506
30010
  try {
@@ -29515,6 +30019,9 @@ function installShutdown(options = {}) {
29515
30019
  if (controller.signal.aborted)
29516
30020
  runCleanup();
29517
30021
  },
30022
+ trigger(reason) {
30023
+ begin(reason);
30024
+ },
29518
30025
  dispose() {
29519
30026
  clearTimeout(timer);
29520
30027
  for (const signal of installed)
@@ -29545,3 +30052,15 @@ if (gateway.wsUrl !== undefined)
29545
30052
  console.log(` WebSocket: ${gateway.wsUrl}`);
29546
30053
  console.log("\u6309 Ctrl+C \u505C\u6B62\u3002");
29547
30054
  shutdown.setCleanup(() => gateway.close());
30055
+ if (options.stopOnStdin === true) {
30056
+ let triggered = false;
30057
+ const onStdinEnd = () => {
30058
+ if (triggered)
30059
+ return;
30060
+ triggered = true;
30061
+ shutdown.trigger("stdin \u5DF2\u5173\u95ED");
30062
+ };
30063
+ process.stdin.once("end", onStdinEnd);
30064
+ process.stdin.once("close", onStdinEnd);
30065
+ process.stdin.resume();
30066
+ }