neko-harness 0.4.2 → 0.4.3

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
@@ -16503,7 +16503,8 @@ var GATEWAY_USAGE = `\u7528\u6CD5: neko serve [\u9009\u9879]
16503
16503
  --config <dir> neko.config.json \u67E5\u627E\u8D77\u59CB\u76EE\u5F55\uFF08\u9ED8\u8BA4\u5F53\u524D\u76EE\u5F55\uFF09
16504
16504
  -h, --help \u663E\u793A\u6B64\u5E2E\u52A9
16505
16505
 
16506
- \u53EF\u901A\u8FC7\u524D\u7AEF\u8BBE\u7F6E\u6216 DEEPSEEK_API_KEY \u914D\u7F6E\u51ED\u636E\u3002`;
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
16508
  function parseGatewayArgs(argv) {
16508
16509
  let port;
16509
16510
  const options = {};
@@ -16594,13 +16595,186 @@ function parseGatewayArgs(argv) {
16594
16595
  import { randomUUID as randomUUID5 } from "crypto";
16595
16596
  import { mkdir as mkdir6, readFile as readFile7, writeFile as writeFile5 } from "fs/promises";
16596
16597
  import { basename as basename4, dirname as dirname9, join as join13 } from "path";
16597
- import { createHost as createHost3 } from "@earendil-works/chord";
16598
+ import { createHost as createHost2 } from "@earendil-works/chord";
16598
16599
  import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT4 } from "@earendil-works/chord/context";
16599
16600
 
16600
16601
  // packages/core/src/config.ts
16601
16602
  import { existsSync, readFileSync } from "fs";
16602
16603
  import { homedir } from "os";
16603
16604
  import { dirname, join, resolve } from "path";
16605
+
16606
+ // packages/core/src/context-settings.ts
16607
+ import { DEFAULT_COMPACTION_POLICY } from "@earendil-works/pi-durable";
16608
+
16609
+ // packages/core/src/context-limits.ts
16610
+ var PERCENT = { kind: "int", min: 1, max: 99 };
16611
+ var CONTEXT_FIELD_LIMITS = {
16612
+ compaction: {
16613
+ enabled: { kind: "boolean" },
16614
+ triggerPercent: PERCENT,
16615
+ keepRecentTokens: { kind: "int", min: 1, max: 1e8 }
16616
+ },
16617
+ pruning: {
16618
+ enabled: { kind: "boolean" },
16619
+ triggerPercent: PERCENT,
16620
+ keepRecent: { kind: "int", min: 0, max: 1e5 },
16621
+ minChars: { kind: "int", min: 1, max: 1e8 },
16622
+ minSavePercent: PERCENT
16623
+ }
16624
+ };
16625
+ var PRUNING_DEFAULTS = {
16626
+ enabled: false,
16627
+ triggerPercent: 35,
16628
+ keepRecent: 10,
16629
+ minChars: 4000,
16630
+ minSavePercent: 5
16631
+ };
16632
+
16633
+ // packages/core/src/context-settings.ts
16634
+ var emptySavedContextSettings = () => ({
16635
+ compaction: { enabled: null, triggerPercent: null, keepRecentTokens: null },
16636
+ pruning: { enabled: null, triggerPercent: null, keepRecent: null, minChars: null, minSavePercent: null }
16637
+ });
16638
+ var SCHEMA = CONTEXT_FIELD_LIMITS;
16639
+ var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
16640
+ function describe(field) {
16641
+ return field.kind === "boolean" ? "a boolean" : `an integer between ${field.min} and ${field.max}`;
16642
+ }
16643
+ function validateGroups(value, prefix, allowNull) {
16644
+ if (!isObject(value))
16645
+ throw new Error(`"${prefix}" must be an object.`);
16646
+ const out = {};
16647
+ for (const key of Object.keys(value)) {
16648
+ if (!(key in SCHEMA))
16649
+ throw new Error(`"${prefix}.${key}" is not a known setting.`);
16650
+ }
16651
+ for (const [group, fields] of Object.entries(SCHEMA)) {
16652
+ const given = value[group];
16653
+ if (given === undefined)
16654
+ continue;
16655
+ if (!isObject(given))
16656
+ throw new Error(`"${prefix}.${group}" must be an object.`);
16657
+ const target = {};
16658
+ for (const key of Object.keys(given)) {
16659
+ if (!(key in fields))
16660
+ throw new Error(`"${prefix}.${group}.${key}" is not a known setting.`);
16661
+ }
16662
+ for (const [name, field] of Object.entries(fields)) {
16663
+ const item = given[name];
16664
+ if (item === undefined)
16665
+ continue;
16666
+ if (item === null && allowNull) {
16667
+ target[name] = null;
16668
+ continue;
16669
+ }
16670
+ const ok = field.kind === "boolean" ? typeof item === "boolean" : typeof item === "number" && Number.isInteger(item) && item >= field.min && item <= field.max;
16671
+ if (!ok)
16672
+ throw new Error(`"${prefix}.${group}.${name}" must be ${describe(field)}.`);
16673
+ target[name] = item;
16674
+ }
16675
+ out[group] = target;
16676
+ }
16677
+ return out;
16678
+ }
16679
+ function parseContextSettings(value, prefix = "context") {
16680
+ return validateGroups(value, prefix, false);
16681
+ }
16682
+ function parseContextSettingsPatch(value) {
16683
+ return validateGroups(value, "context", true);
16684
+ }
16685
+ function applyContextPatch(saved, patch) {
16686
+ const next = {
16687
+ compaction: { ...saved.compaction },
16688
+ pruning: { ...saved.pruning }
16689
+ };
16690
+ for (const [key, value] of Object.entries(patch.compaction ?? {})) {
16691
+ if (value !== undefined)
16692
+ next.compaction[key] = value;
16693
+ }
16694
+ for (const [key, value] of Object.entries(patch.pruning ?? {})) {
16695
+ if (value !== undefined)
16696
+ next.pruning[key] = value;
16697
+ }
16698
+ return next;
16699
+ }
16700
+ function sanitizeSavedContextSettings(raw) {
16701
+ const saved = emptySavedContextSettings();
16702
+ if (!isObject(raw))
16703
+ return saved;
16704
+ for (const [group, fields] of Object.entries(SCHEMA)) {
16705
+ const stored = raw[group];
16706
+ if (!isObject(stored))
16707
+ continue;
16708
+ for (const [name, field] of Object.entries(fields)) {
16709
+ const item = stored[name];
16710
+ const ok = field.kind === "boolean" ? typeof item === "boolean" : typeof item === "number" && Number.isInteger(item) && item >= field.min && item <= field.max;
16711
+ if (ok)
16712
+ saved[group][name] = item;
16713
+ }
16714
+ }
16715
+ return saved;
16716
+ }
16717
+ function pick(saved, config, fallback) {
16718
+ if (saved !== null && saved !== undefined)
16719
+ return { value: saved, source: "saved" };
16720
+ if (config !== undefined)
16721
+ return { value: config, source: "config" };
16722
+ return { value: fallback, source: "default" };
16723
+ }
16724
+ function resolveContextSettings(config, saved) {
16725
+ const cEnabled = pick(saved.compaction.enabled, config?.compaction?.enabled, DEFAULT_COMPACTION_POLICY.enabled);
16726
+ const cTrigger = pick(saved.compaction.triggerPercent, config?.compaction?.triggerPercent, null);
16727
+ const cKeep = pick(saved.compaction.keepRecentTokens, config?.compaction?.keepRecentTokens, DEFAULT_COMPACTION_POLICY.keepRecentTokens);
16728
+ const pEnabled = pick(saved.pruning.enabled, config?.pruning?.enabled, PRUNING_DEFAULTS.enabled);
16729
+ const pTrigger = pick(saved.pruning.triggerPercent, config?.pruning?.triggerPercent, PRUNING_DEFAULTS.triggerPercent);
16730
+ const pKeep = pick(saved.pruning.keepRecent, config?.pruning?.keepRecent, PRUNING_DEFAULTS.keepRecent);
16731
+ const pMin = pick(saved.pruning.minChars, config?.pruning?.minChars, PRUNING_DEFAULTS.minChars);
16732
+ const pSave = pick(saved.pruning.minSavePercent, config?.pruning?.minSavePercent, PRUNING_DEFAULTS.minSavePercent);
16733
+ return {
16734
+ effective: {
16735
+ compaction: { enabled: cEnabled.value, triggerPercent: cTrigger.value, keepRecentTokens: cKeep.value },
16736
+ pruning: {
16737
+ enabled: pEnabled.value,
16738
+ triggerPercent: pTrigger.value,
16739
+ keepRecent: pKeep.value,
16740
+ minChars: pMin.value,
16741
+ minSavePercent: pSave.value
16742
+ }
16743
+ },
16744
+ sources: {
16745
+ compaction: { enabled: cEnabled.source, triggerPercent: cTrigger.source, keepRecentTokens: cKeep.source },
16746
+ pruning: {
16747
+ enabled: pEnabled.source,
16748
+ triggerPercent: pTrigger.source,
16749
+ keepRecent: pKeep.source,
16750
+ minChars: pMin.source,
16751
+ minSavePercent: pSave.source
16752
+ }
16753
+ }
16754
+ };
16755
+ }
16756
+ function compactionPolicy(state, windows) {
16757
+ const { effective, sources } = state;
16758
+ const policy = {};
16759
+ if (sources.compaction.enabled !== "default")
16760
+ policy.enabled = effective.compaction.enabled;
16761
+ if (sources.compaction.keepRecentTokens !== "default")
16762
+ policy.keepRecentTokens = effective.compaction.keepRecentTokens;
16763
+ const percent = effective.compaction.triggerPercent;
16764
+ const window2 = windows.length > 0 ? Math.min(...windows) : 0;
16765
+ if (percent !== null && window2 > 0)
16766
+ policy.reserveTokens = window2 - Math.floor(window2 * percent / 100);
16767
+ return policy;
16768
+ }
16769
+
16770
+ // packages/core/src/search-providers.ts
16771
+ var SEARCH_PROVIDERS = ["tavily", "deepseek"];
16772
+ var DEFAULT_SEARCH_PROVIDER = "tavily";
16773
+ function isSearchProvider(value) {
16774
+ return typeof value === "string" && SEARCH_PROVIDERS.includes(value);
16775
+ }
16776
+
16777
+ // packages/core/src/config.ts
16604
16778
  var DEFAULT_SKILL_MARKET_URL = "https://skill.kirineko.tech";
16605
16779
  var NEKO_CONFIG_BASENAME = "neko.config.json";
16606
16780
  function findNekoConfigFile(startDir = process.cwd()) {
@@ -16714,6 +16888,23 @@ function validate(config, source) {
16714
16888
  throw new Error(`neko config: ${source}: "skills.marketUrl" must be a string.`);
16715
16889
  }
16716
16890
  }
16891
+ const webSearch = config.webSearch;
16892
+ if (webSearch !== undefined) {
16893
+ if (typeof webSearch !== "object" || webSearch === null || Array.isArray(webSearch)) {
16894
+ throw new Error(`neko config: ${source}: "webSearch" must be an object.`);
16895
+ }
16896
+ const { provider } = webSearch;
16897
+ if (provider !== undefined && !isSearchProvider(provider)) {
16898
+ throw new Error(`neko config: ${source}: "webSearch.provider" must be one of ${SEARCH_PROVIDERS.map((item) => `"${item}"`).join(", ")}.`);
16899
+ }
16900
+ }
16901
+ if (config.context !== undefined) {
16902
+ try {
16903
+ parseContextSettings(config.context);
16904
+ } catch (error) {
16905
+ throw new Error(`neko config: ${source}: ${error instanceof Error ? error.message : String(error)}`);
16906
+ }
16907
+ }
16717
16908
  }
16718
16909
  // packages/core/src/credentials.ts
16719
16910
  import { chmod, mkdir, open, readFile, rename, unlink } from "fs/promises";
@@ -17728,6 +17919,134 @@ async function readCompaction(harness, storage, id, context) {
17728
17919
  return { taskId: task.id, phase, at: task.endedAt ?? task.startedAt ?? 0 };
17729
17920
  }
17730
17921
 
17922
+ // packages/core/src/context-pruning.ts
17923
+ var PRUNE_ENTRY_KIND = "app.neko.context-prune";
17924
+ var TOKENS_PER_LATIN_CHAR = 0.3;
17925
+ var TOKENS_PER_CJK_CHAR = 0.6;
17926
+ var CJK = /[\u3000-\u9fff\uac00-\ud7af\uf900-\ufaff\uff00-\uffef]/g;
17927
+ function estimateTokens(text) {
17928
+ const cjk = text.match(CJK)?.length ?? 0;
17929
+ return cjk * TOKENS_PER_CJK_CHAR + (text.length - cjk) * TOKENS_PER_LATIN_CHAR;
17930
+ }
17931
+ var isToolResult = (entry) => {
17932
+ const message = entry.model?.[0];
17933
+ return entry.kind === "pi.tool-result" && message?.role === "toolResult";
17934
+ };
17935
+ var resultTexts = (message) => message.content.flatMap((block) => block.type === "text" ? [block.text] : []);
17936
+ function toolResultChars(message) {
17937
+ return resultTexts(message).reduce((total, text) => total + text.length, 0);
17938
+ }
17939
+ function toolResultTokens(message) {
17940
+ return resultTexts(message).reduce((total, text) => total + estimateTokens(text), 0);
17941
+ }
17942
+ function prunedPlaceholder(message, chars) {
17943
+ const { details: _details, ...rest } = message;
17944
+ return {
17945
+ ...rest,
17946
+ content: [
17947
+ {
17948
+ type: "text",
17949
+ text: `[Output of ${message.toolName} omitted from context to save space (${chars} characters). Run the tool again if the content is needed.]`
17950
+ }
17951
+ ]
17952
+ };
17953
+ }
17954
+ function planPrune(entries, settings) {
17955
+ const replaced = new Set;
17956
+ for (const entry of entries)
17957
+ for (const edit of entry.edits ?? [])
17958
+ replaced.add(edit.target);
17959
+ const results = entries.filter(isToolResult);
17960
+ const candidates = results.slice(0, Math.max(0, results.length - settings.keepRecent));
17961
+ const edits = [];
17962
+ const targets = [];
17963
+ let savedChars = 0;
17964
+ let savedTokens = 0;
17965
+ for (const entry of candidates) {
17966
+ if (replaced.has(entry.id))
17967
+ continue;
17968
+ const message = entry.model[0];
17969
+ const chars = toolResultChars(message);
17970
+ if (chars < settings.minChars)
17971
+ continue;
17972
+ const replacement = prunedPlaceholder(message, chars);
17973
+ edits.push({ target: entry.id, action: "replace", messages: [replacement] });
17974
+ targets.push(entry.id);
17975
+ savedChars += chars;
17976
+ savedTokens += toolResultTokens(message);
17977
+ }
17978
+ return edits.length === 0 ? undefined : { edits, targets, savedChars, savedTokens };
17979
+ }
17980
+
17981
+ class ContextPruner {
17982
+ #options;
17983
+ #unsubscribe;
17984
+ #tail = Promise.resolve();
17985
+ #closed = false;
17986
+ constructor(options) {
17987
+ this.#options = options;
17988
+ this.#unsubscribe = options.harness.subscribeCommits(({ changes }) => {
17989
+ if (!options.settings().enabled)
17990
+ return;
17991
+ for (const change of changes) {
17992
+ if (change.type !== "entry" || change.value.kind !== "pi.assistant")
17993
+ continue;
17994
+ const message = change.value.model?.[0];
17995
+ if (message?.role !== "assistant" || message.stopReason === "aborted" || message.stopReason === "error")
17996
+ continue;
17997
+ const usage = message.usage;
17998
+ const tokens = usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
17999
+ if (tokens <= 0)
18000
+ continue;
18001
+ const { conversationId, id } = change.value;
18002
+ this.#enqueue(() => this.#evaluate(conversationId, id, tokens));
18003
+ }
18004
+ });
18005
+ }
18006
+ async flush() {
18007
+ await this.#tail;
18008
+ }
18009
+ close() {
18010
+ this.#closed = true;
18011
+ this.#unsubscribe();
18012
+ }
18013
+ #enqueue(work) {
18014
+ if (this.#closed)
18015
+ return;
18016
+ this.#tail = this.#tail.then(work).catch((error) => {
18017
+ if (!this.#closed)
18018
+ this.#options.report(error);
18019
+ });
18020
+ }
18021
+ async#evaluate(conversationId, entryId, tokens) {
18022
+ const { harness, context } = this.#options;
18023
+ const settings = this.#options.settings();
18024
+ if (!settings.enabled)
18025
+ return;
18026
+ const window2 = await this.#options.windowOf(conversationId);
18027
+ if (window2 <= 0 || tokens / window2 * 100 < settings.triggerPercent)
18028
+ return;
18029
+ const conversation = await harness.conversation(conversationId, context);
18030
+ if (!conversation)
18031
+ return;
18032
+ const view = await conversation.context(context);
18033
+ const plan = planPrune(view.entries, settings);
18034
+ if (!plan)
18035
+ return;
18036
+ if (plan.savedTokens / window2 * 100 < settings.minSavePercent)
18037
+ return;
18038
+ await conversation.submit({
18039
+ type: "write",
18040
+ requestId: `prune:${entryId}`,
18041
+ entry: {
18042
+ kind: PRUNE_ENTRY_KIND,
18043
+ data: { targets: [...plan.targets], savedChars: plan.savedChars },
18044
+ edits: plan.edits
18045
+ }
18046
+ }, context);
18047
+ }
18048
+ }
18049
+
17731
18050
  // packages/core/src/image-metadata.ts
17732
18051
  import { defineExtension as defineExtension2, hook, ToolTask } from "@earendil-works/pi-durable";
17733
18052
  import sharp from "sharp";
@@ -18387,7 +18706,7 @@ var statusWord = {
18387
18706
  stopped: "stopped",
18388
18707
  lost: "lost"
18389
18708
  };
18390
- var describe = (job) => {
18709
+ var describe2 = (job) => {
18391
18710
  const parts = [statusWord[job.status]];
18392
18711
  if (job.exitCode !== null)
18393
18712
  parts.push(`exit code ${job.exitCode}`);
@@ -18402,7 +18721,7 @@ var describe = (job) => {
18402
18721
  function reportText(job, tail) {
18403
18722
  const body = reportTail(tail);
18404
18723
  return [
18405
- `[Background job ${job.jobId} "${job.title}" ${describe(job)}]`,
18724
+ `[Background job ${job.jobId} "${job.title}" ${describe2(job)}]`,
18406
18725
  `Command: ${job.command}`,
18407
18726
  body ? `Last output (use the job tool's output action with jobId "${job.jobId}" for more):
18408
18727
  ${body}` : "It produced no output."
@@ -18607,7 +18926,7 @@ class JobManager {
18607
18926
  if (kind === "stop") {
18608
18927
  const jobId = stringField(args.jobId, "jobId");
18609
18928
  const job = await this.stop(io, api.conversationId, jobId, "model", context);
18610
- return this.#text(`Job ${job.jobId} ${describe(job)}.`);
18929
+ return this.#text(`Job ${job.jobId} ${describe2(job)}.`);
18611
18930
  }
18612
18931
  if (kind !== "start")
18613
18932
  throw new Error("Invalid job arguments: action must be start, output, stop or list.");
@@ -18635,7 +18954,7 @@ class JobManager {
18635
18954
  #listText(items) {
18636
18955
  if (!items.length)
18637
18956
  return "No jobs.";
18638
- return items.map((job) => `${job.jobId} [${describe(job)}] "${job.title}" \u2014 ${job.command}`).join(`
18957
+ return items.map((job) => `${job.jobId} [${describe2(job)}] "${job.title}" \u2014 ${job.command}`).join(`
18639
18958
  `);
18640
18959
  }
18641
18960
  async start(io, conversationId, cwd, args, context) {
@@ -18687,7 +19006,7 @@ class JobManager {
18687
19006
  return this.#render(job, read.text, read.offset, read.dropped);
18688
19007
  }
18689
19008
  #render(job, text, offset, dropped) {
18690
- const lines = [`Job ${job.jobId} ${describe(job)}.`, `Title: ${job.title}`, `Command: ${job.command}`];
19009
+ const lines = [`Job ${job.jobId} ${describe2(job)}.`, `Title: ${job.title}`, `Command: ${job.command}`];
18691
19010
  if (dropped > 0)
18692
19011
  lines.push(`(${dropped} bytes of earlier output were dropped)`);
18693
19012
  lines.push(text ? `Output (next offset ${offset}):
@@ -19621,33 +19940,32 @@ async function prepareApprovalConversations(harness, ids, extension, context) {
19621
19940
  }
19622
19941
  }
19623
19942
 
19624
- // packages/core/src/web-search.ts
19625
- import { Type as Type4 } from "@earendil-works/pi-ai";
19626
- import { defineExtension as defineExtension8, defineTool as defineTool4 } from "@earendil-works/pi-durable";
19627
- var SEARCH_URL = "https://api.tavily.com/search";
19628
- var EXTRACT_URL = "https://api.tavily.com/extract";
19943
+ // packages/core/src/web-search/http.ts
19629
19944
  var REQUEST_TIMEOUT_MS = 30000;
19630
19945
  var MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
19631
- var MAX_URLS = 20;
19632
- var MAX_RESULTS = 10;
19633
- var DEFAULT_RESULTS = 5;
19634
- var MAX_SOURCE_CHARS = 2000;
19635
- var MAX_TOTAL_CHARS = 12000;
19636
19946
 
19637
- class TavilyError extends Error {
19947
+ class WebToolError extends Error {
19948
+ status;
19949
+ constructor(message, status) {
19950
+ super(message);
19951
+ this.status = status;
19952
+ }
19638
19953
  }
19639
19954
  function redact(text, key) {
19640
19955
  for (const secret of key ? [key, encodeURIComponent(key)] : [])
19641
19956
  text = text.replaceAll(secret, "[redacted]");
19642
- return text.replace(/tvly-[a-zA-Z0-9_-]+/g, "[redacted]");
19957
+ return text.replace(/\b(?:tvly|sk)-[a-zA-Z0-9_-]+/g, "[redacted]");
19643
19958
  }
19644
19959
  function record(value) {
19645
19960
  return typeof value === "object" && value !== null && !Array.isArray(value);
19646
19961
  }
19647
- async function readBounded(response, signal) {
19962
+ function truncate(text, max) {
19963
+ return text.length > max ? `${text.slice(0, Math.max(0, max - 1))}\u2026` : text;
19964
+ }
19965
+ async function readBounded(response, signal, label) {
19648
19966
  if (Number(response.headers.get("content-length")) > MAX_RESPONSE_BYTES) {
19649
19967
  await response.body?.cancel();
19650
- throw new TavilyError("Tavily response exceeds the 2 MiB limit.");
19968
+ throw new WebToolError(`${label} response exceeds the 2 MiB limit.`);
19651
19969
  }
19652
19970
  if (!response.body)
19653
19971
  return "";
@@ -19668,7 +19986,7 @@ async function readBounded(response, signal) {
19668
19986
  return text + decoder.decode();
19669
19987
  size += chunk.value.byteLength;
19670
19988
  if (size > MAX_RESPONSE_BYTES)
19671
- throw new TavilyError("Tavily response exceeds the 2 MiB limit.");
19989
+ throw new WebToolError(`${label} response exceeds the 2 MiB limit.`);
19672
19990
  text += decoder.decode(chunk.value, { stream: true });
19673
19991
  }
19674
19992
  } finally {
@@ -19677,7 +19995,8 @@ async function readBounded(response, signal) {
19677
19995
  reader.releaseLock();
19678
19996
  }
19679
19997
  }
19680
- async function tavilyPost(url, body, resolveKey, fetchImpl, context) {
19998
+ async function postJson(options) {
19999
+ const { label, context } = options;
19681
20000
  let key;
19682
20001
  const signal = AbortSignal.any([
19683
20002
  AbortSignal.timeout(REQUEST_TIMEOUT_MS),
@@ -19685,68 +20004,188 @@ async function tavilyPost(url, body, resolveKey, fetchImpl, context) {
19685
20004
  ]);
19686
20005
  try {
19687
20006
  signal.throwIfAborted();
19688
- key = await resolveKey();
20007
+ key = await options.resolveKey();
19689
20008
  signal.throwIfAborted();
19690
- const headers = { "content-type": "application/json" };
19691
- if (key)
19692
- headers.authorization = `Bearer ${key}`;
19693
- else
19694
- headers["x-tavily-access-mode"] = "keyless";
19695
- const response = await fetchImpl(url, { method: "POST", headers, body: JSON.stringify(body), signal });
19696
- const text = redact(await readBounded(response, signal), key);
20009
+ const response = await options.fetchImpl(options.url, {
20010
+ method: "POST",
20011
+ headers: { "content-type": "application/json", ...options.headers(key) },
20012
+ body: JSON.stringify(options.body),
20013
+ signal
20014
+ });
20015
+ const text = redact(await readBounded(response, signal, label), key);
19697
20016
  if (!response.ok)
19698
- throw new TavilyError(`Tavily request failed (${response.status}): ${truncate(text, 500)}${key ? "" : " (keyless rate limits apply; save a Tavily API key in settings for higher limits)"}`);
19699
- const data = JSON.parse(text);
19700
- if (!record(data))
19701
- throw new TavilyError("Invalid Tavily response.");
19702
- if (data.error)
19703
- throw new TavilyError(`Tavily request failed: ${truncate(JSON.stringify(data.error), 500)}`);
19704
- return data;
20017
+ throw options.httpError(response.status, text, key);
20018
+ return JSON.parse(text);
19705
20019
  } catch (error) {
19706
20020
  if (context.abortSignal?.aborted)
19707
- throw new TavilyError("Web request cancelled.");
20021
+ throw new WebToolError("Web request cancelled.");
19708
20022
  if (signal.aborted)
19709
- throw new TavilyError("Tavily request timed out. Please retry.");
19710
- if (error instanceof TavilyError)
20023
+ throw new WebToolError(`${label} request timed out. Please retry.`);
20024
+ if (error instanceof WebToolError)
19711
20025
  throw error;
19712
- throw new TavilyError("Tavily request failed. Check the connection and credentials, then retry.");
20026
+ throw new WebToolError(`${label} request failed. Check the connection and credentials, then retry.`);
19713
20027
  }
19714
20028
  }
20029
+
20030
+ // packages/core/src/web-search/deepseek.ts
20031
+ var SEARCH_URL = "https://api.deepseek.com/v1/tools/search";
20032
+ var FETCH_URL = "https://api.deepseek.com/v1/tools/fetch";
20033
+ var DEEPSEEK_QUERY_MAX_CHARS = 128;
20034
+ var FETCH_CONCURRENCY = 3;
20035
+ var MAX_RETRIES = 2;
20036
+ var BACKOFF_MS = [250, 750];
20037
+ var DEEPSEEK_MISSING_KEY = "DeepSeek API key is not configured. Save one with /login or in the Web settings, or switch the search provider to Tavily.";
20038
+ function defaultSleep(ms, signal) {
20039
+ return new Promise((resolve, reject) => {
20040
+ if (signal?.aborted)
20041
+ return reject(signal.reason);
20042
+ const timer = setTimeout(resolve, ms);
20043
+ signal?.addEventListener("abort", () => {
20044
+ clearTimeout(timer);
20045
+ reject(signal.reason);
20046
+ }, { once: true });
20047
+ });
20048
+ }
20049
+ function apiMessage(text) {
20050
+ try {
20051
+ const data = JSON.parse(text);
20052
+ if (record(data) && record(data.error) && typeof data.error.message === "string")
20053
+ return truncate(data.error.message, 300);
20054
+ } catch {}
20055
+ return truncate(text, 300);
20056
+ }
20057
+ function httpError(status, text) {
20058
+ const guidance = "Check the key in /login or the Web settings, or switch the search provider.";
20059
+ if (status === 401)
20060
+ return new WebToolError(`DeepSeek API key is missing or invalid. ${guidance}`, status);
20061
+ if (status === 402)
20062
+ return new WebToolError("DeepSeek account balance is insufficient. Recharge, or switch the search provider to Tavily.", status);
20063
+ if (status === 403)
20064
+ return new WebToolError("DeepSeek blocked this request by its security policy.", status);
20065
+ if (status === 404)
20066
+ return new WebToolError("DeepSeek could not fetch a usable page for this URL.", status);
20067
+ if (status === 429)
20068
+ return new WebToolError("DeepSeek rate limit reached. Retry shortly.", status);
20069
+ if (status === 408 || status >= 500)
20070
+ return new WebToolError(`DeepSeek service error (${status}). Retry shortly.`, status);
20071
+ return new WebToolError(`DeepSeek request failed (${status}): ${apiMessage(text)}`, status);
20072
+ }
20073
+ var retryable = (status) => status !== undefined && (status === 429 || status === 408 || status >= 500);
20074
+ var wholeCall = (status) => status === 401 || status === 402;
20075
+ function dateOnly(value) {
20076
+ return typeof value === "string" && /^\d{4}-\d{2}-\d{2}/.test(value) ? value.slice(0, 10) : undefined;
20077
+ }
20078
+ function createDeepseekProvider(resolveKey, fetchImpl = fetch, options = {}) {
20079
+ const sleep = options.sleep ?? defaultSleep;
20080
+ const requireKey = async () => {
20081
+ const key = await resolveKey();
20082
+ if (!key)
20083
+ throw new WebToolError(DEEPSEEK_MISSING_KEY);
20084
+ return key;
20085
+ };
20086
+ const post = (url, body, context) => postJson({
20087
+ label: "DeepSeek",
20088
+ url,
20089
+ body,
20090
+ fetchImpl,
20091
+ context,
20092
+ resolveKey: requireKey,
20093
+ headers: (key) => ({ authorization: `Bearer ${key}` }),
20094
+ httpError
20095
+ });
20096
+ return {
20097
+ id: "deepseek",
20098
+ async search(query, limit, context) {
20099
+ if (!query.trim())
20100
+ throw new WebToolError("Provide a non-empty query.");
20101
+ if ([...query].length > DEEPSEEK_QUERY_MAX_CHARS)
20102
+ throw new WebToolError(`DeepSeek search queries are limited to ${DEEPSEEK_QUERY_MAX_CHARS} characters. Shorten the query and retry.`);
20103
+ await requireKey();
20104
+ const data = await post(SEARCH_URL, { query, count: limit, need_content: false }, context);
20105
+ if (!record(data) || !Array.isArray(data.results) || !data.results.every(record))
20106
+ throw new WebToolError("Invalid DeepSeek response.");
20107
+ return data.results.slice(0, limit).map((item) => {
20108
+ const date = dateOnly(item.date);
20109
+ return {
20110
+ title: String(item.title ?? "(untitled)"),
20111
+ url: String(item.url ?? ""),
20112
+ text: String(item.summary || item.snippet || ""),
20113
+ ...date ? { date } : {}
20114
+ };
20115
+ });
20116
+ },
20117
+ async extract(urls, context) {
20118
+ await requireKey();
20119
+ const outcomes = new Array(urls.length);
20120
+ let next = 0;
20121
+ let fatal;
20122
+ const fetchOne = async (url) => {
20123
+ for (let attempt = 0;; attempt++) {
20124
+ try {
20125
+ const data = await post(FETCH_URL, { url }, context);
20126
+ if (!record(data) || typeof data.content !== "string")
20127
+ throw new WebToolError("Invalid DeepSeek response.");
20128
+ return { url, text: data.content };
20129
+ } catch (error) {
20130
+ const failure = error instanceof WebToolError ? error : new WebToolError("DeepSeek request failed.");
20131
+ if (context.abortSignal?.aborted || wholeCall(failure.status))
20132
+ throw failure;
20133
+ if (!retryable(failure.status) || attempt >= MAX_RETRIES)
20134
+ return { url, error: failure.message };
20135
+ try {
20136
+ await sleep(BACKOFF_MS[attempt] ?? 750, context.abortSignal);
20137
+ } catch {
20138
+ throw new WebToolError("Web request cancelled.");
20139
+ }
20140
+ }
20141
+ }
20142
+ };
20143
+ const worker = async () => {
20144
+ while (!fatal) {
20145
+ const index = next++;
20146
+ if (index >= urls.length)
20147
+ return;
20148
+ try {
20149
+ outcomes[index] = await fetchOne(urls[index]);
20150
+ } catch (error) {
20151
+ fatal ??= error instanceof WebToolError ? error : new WebToolError("DeepSeek request failed.");
20152
+ }
20153
+ }
20154
+ };
20155
+ await Promise.all(Array.from({ length: Math.min(FETCH_CONCURRENCY, urls.length) }, worker));
20156
+ if (fatal)
20157
+ throw fatal;
20158
+ return outcomes;
20159
+ }
20160
+ };
20161
+ }
20162
+
20163
+ // packages/core/src/web-search/index.ts
20164
+ import { Type as Type4 } from "@earendil-works/pi-ai";
20165
+ import { defineExtension as defineExtension8, defineTool as defineTool4 } from "@earendil-works/pi-durable";
20166
+ var MAX_URLS = 20;
20167
+ var MAX_RESULTS = 10;
20168
+ var DEFAULT_RESULTS = 5;
20169
+ var MAX_SOURCE_CHARS = 2000;
20170
+ var MAX_TOTAL_CHARS = 12000;
19715
20171
  function toolError(error) {
19716
20172
  return {
19717
20173
  output: [
19718
- { type: "text", text: error instanceof TavilyError ? error.message : "Web tool failed. Please retry." }
20174
+ { type: "text", text: error instanceof WebToolError ? error.message : "Web tool failed. Please retry." }
19719
20175
  ],
19720
20176
  isError: true
19721
20177
  };
19722
20178
  }
19723
- function truncate(text, max) {
19724
- return text.length > max ? `${text.slice(0, Math.max(0, max - 1))}\u2026` : text;
19725
- }
19726
- function rows(data, field) {
19727
- const value = data[field];
19728
- if (!Array.isArray(value) || !value.every(record))
19729
- throw new TavilyError("Invalid Tavily response.");
19730
- return value;
19731
- }
19732
20179
  function content(text) {
19733
20180
  return { output: [{ type: "text", text }] };
19734
20181
  }
19735
- function urlKey(value) {
19736
- const text = String(value);
19737
- try {
19738
- return new URL(text).href;
19739
- } catch {
19740
- return text;
19741
- }
19742
- }
19743
- function createWebSearchTools(resolveKey, fetchImpl = fetch) {
20182
+ function createWebSearchTools(resolveProvider) {
19744
20183
  return defineExtension8({
19745
20184
  name: "neko-web-search",
19746
20185
  tools: [
19747
20186
  defineTool4({
19748
20187
  name: "web_search",
19749
- description: "Search the live web for current or unknown information. Returns ranked sources (title, URL, bounded excerpts). Use only when up-to-date or external information is needed; use web_extract to read a known URL.",
20188
+ description: "Search the live web for current or unknown information. Returns ranked sources (title, URL, bounded excerpts). Keep the query short and focused (ideally under 128 characters). Use only when up-to-date or external information is needed; use web_extract to read a known URL.",
19750
20189
  parameters: Type4.Object({
19751
20190
  query: Type4.String({ minLength: 1, maxLength: 4096, description: "The search query" }),
19752
20191
  maxResults: Type4.Optional(Type4.Number({ description: `Maximum number of results (1-${MAX_RESULTS}, default ${DEFAULT_RESULTS})` }))
@@ -19754,18 +20193,19 @@ function createWebSearchTools(resolveKey, fetchImpl = fetch) {
19754
20193
  async execute({ query, maxResults }, _api, context) {
19755
20194
  try {
19756
20195
  if (!query.trim() || query.length > 4096)
19757
- throw new TavilyError("Provide a non-empty query of at most 4096 characters.");
20196
+ throw new WebToolError("Provide a non-empty query of at most 4096 characters.");
19758
20197
  if (maxResults !== undefined && !Number.isFinite(maxResults))
19759
- throw new TavilyError("maxResults must be a finite number.");
20198
+ throw new WebToolError("maxResults must be a finite number.");
19760
20199
  const limit = Math.max(1, Math.min(MAX_RESULTS, Math.trunc(maxResults ?? DEFAULT_RESULTS)));
19761
- const data = await tavilyPost(SEARCH_URL, { query, max_results: limit, search_depth: "basic" }, resolveKey, fetchImpl, context);
19762
- const results = rows(data, "results").slice(0, limit);
20200
+ const provider = await resolveProvider(context);
20201
+ const results = (await provider.search(query, limit, context)).slice(0, limit);
19763
20202
  const budget = Math.floor(MAX_TOTAL_CHARS / Math.max(1, results.length)) - 2;
19764
20203
  return content(results.map((item, index) => {
19765
- const header = `${index + 1}. ${truncate(String(item.title ?? "(untitled)"), 200)}
19766
- ${truncate(String(item.url ?? ""), 300)}
19767
- `;
19768
- return header + truncate(String(item.content ?? ""), Math.min(MAX_SOURCE_CHARS, budget - header.length));
20204
+ const header = `${index + 1}. ${truncate(item.title, 200)}
20205
+ ${truncate(item.url, 300)}
20206
+ ${item.date ? `Published: ${item.date}
20207
+ ` : ""}`;
20208
+ return header + truncate(item.text, Math.min(MAX_SOURCE_CHARS, budget - header.length));
19769
20209
  }).join(`
19770
20210
 
19771
20211
  `) || "No results.");
@@ -19787,28 +20227,25 @@ ${truncate(String(item.url ?? ""), 300)}
19787
20227
  async execute({ urls }, _api, context) {
19788
20228
  try {
19789
20229
  if (!urls.length || urls.length > MAX_URLS)
19790
- throw new TavilyError(`Provide between 1 and ${MAX_URLS} URLs.`);
20230
+ throw new WebToolError(`Provide between 1 and ${MAX_URLS} URLs.`);
19791
20231
  for (const url of urls) {
19792
20232
  let parsed;
19793
20233
  try {
19794
20234
  parsed = new URL(url);
19795
20235
  } catch {
19796
- throw new TavilyError("Provide valid http(s) URLs.");
20236
+ throw new WebToolError("Provide valid http(s) URLs.");
19797
20237
  }
19798
20238
  if (url.length > 2048 || !["http:", "https:"].includes(parsed.protocol) || parsed.username || parsed.password)
19799
- throw new TavilyError("Provide http(s) URLs of at most 2048 characters without embedded credentials.");
20239
+ throw new WebToolError("Provide http(s) URLs of at most 2048 characters without embedded credentials.");
19800
20240
  }
19801
- const data = await tavilyPost(EXTRACT_URL, { urls, format: "markdown" }, resolveKey, fetchImpl, context);
19802
- const results = rows(data, "results");
19803
- const failed = data.failed_results === undefined ? [] : rows(data, "failed_results");
19804
- const byUrl = (items) => new Map(items.map((item) => [urlKey(item.url), item]));
19805
- const successes = byUrl(results), failures = byUrl(failed);
20241
+ const provider = await resolveProvider(context);
20242
+ const outcomes = await provider.extract(urls, context);
19806
20243
  const budget = Math.floor(MAX_TOTAL_CHARS / urls.length) - 2;
19807
20244
  return content(urls.map((url, index) => {
19808
20245
  const header = `## ${index + 1}. ${truncate(url, 300)}
19809
20246
  `;
19810
- const result = successes.get(urlKey(url)), failure = failures.get(urlKey(url));
19811
- const text = result ? String(result.raw_content ?? "") : `Extraction failed: ${String(failure?.error ?? "No result returned for this URL")}`;
20247
+ const outcome = outcomes[index];
20248
+ const text = outcome && "text" in outcome ? outcome.text : `Extraction failed: ${outcome?.error ?? "No result returned for this URL"}`;
19812
20249
  return header + truncate(text, Math.min(MAX_SOURCE_CHARS, budget - header.length));
19813
20250
  }).join(`
19814
20251
 
@@ -19822,6 +20259,69 @@ ${truncate(String(item.url ?? ""), 300)}
19822
20259
  });
19823
20260
  }
19824
20261
 
20262
+ // packages/core/src/web-search/tavily.ts
20263
+ var SEARCH_URL2 = "https://api.tavily.com/search";
20264
+ var EXTRACT_URL = "https://api.tavily.com/extract";
20265
+ function rows(data, field) {
20266
+ const value = data[field];
20267
+ if (!Array.isArray(value) || !value.every(record))
20268
+ throw new WebToolError("Invalid Tavily response.");
20269
+ return value;
20270
+ }
20271
+ function urlKey(value) {
20272
+ const text = String(value);
20273
+ try {
20274
+ return new URL(text).href;
20275
+ } catch {
20276
+ return text;
20277
+ }
20278
+ }
20279
+ function createTavilyProvider(resolveKey, fetchImpl = fetch) {
20280
+ const post = async (url, body, context) => {
20281
+ const data = await postJson({
20282
+ label: "Tavily",
20283
+ url,
20284
+ body,
20285
+ fetchImpl,
20286
+ context,
20287
+ resolveKey,
20288
+ headers: (key) => key ? { authorization: `Bearer ${key}` } : { "x-tavily-access-mode": "keyless" },
20289
+ httpError: (status, text, key) => new WebToolError(`Tavily request failed (${status}): ${truncate(text, 500)}${key ? "" : " (keyless rate limits apply; save a Tavily API key in settings for higher limits)"}`, status)
20290
+ });
20291
+ if (!record(data))
20292
+ throw new WebToolError("Invalid Tavily response.");
20293
+ if (data.error)
20294
+ throw new WebToolError(`Tavily request failed: ${truncate(JSON.stringify(data.error), 500)}`);
20295
+ return data;
20296
+ };
20297
+ return {
20298
+ id: "tavily",
20299
+ async search(query, limit, context) {
20300
+ const data = await post(SEARCH_URL2, { query, max_results: limit, search_depth: "basic" }, context);
20301
+ return rows(data, "results").slice(0, limit).map((item) => ({
20302
+ title: String(item.title ?? "(untitled)"),
20303
+ url: String(item.url ?? ""),
20304
+ text: String(item.content ?? "")
20305
+ }));
20306
+ },
20307
+ async extract(urls, context) {
20308
+ const data = await post(EXTRACT_URL, { urls, format: "markdown" }, context);
20309
+ const results = rows(data, "results");
20310
+ const failed = data.failed_results === undefined ? [] : rows(data, "failed_results");
20311
+ const byUrl = (items) => new Map(items.map((item) => [urlKey(item.url), item]));
20312
+ const successes = byUrl(results);
20313
+ const failures = byUrl(failed);
20314
+ return urls.map((url) => {
20315
+ const result = successes.get(urlKey(url));
20316
+ if (result)
20317
+ return { url, text: String(result.raw_content ?? "") };
20318
+ const failure = failures.get(urlKey(url));
20319
+ return { url, error: String(failure?.error ?? "No result returned for this URL") };
20320
+ });
20321
+ }
20322
+ };
20323
+ }
20324
+
19825
20325
  // packages/core/src/workspace-files.ts
19826
20326
  import { realpath as realpath2, stat } from "fs/promises";
19827
20327
  import { basename as basename2, isAbsolute as isAbsolute2, relative, resolve as resolve3, sep } from "path";
@@ -20201,6 +20701,22 @@ var WorkspacesDoc = defineDoc5({
20201
20701
  fork: "initial",
20202
20702
  initial: () => ({ items: [] })
20203
20703
  });
20704
+ var SearchSettingsDoc = defineDoc5({
20705
+ kind: "app.neko.search-settings",
20706
+ version: 1,
20707
+ scope: "conversation",
20708
+ history: "latest",
20709
+ fork: "initial",
20710
+ initial: () => ({ provider: null })
20711
+ });
20712
+ var ContextSettingsDoc = defineDoc5({
20713
+ kind: "app.neko.context-settings",
20714
+ version: 1,
20715
+ scope: "conversation",
20716
+ history: "latest",
20717
+ fork: "initial",
20718
+ initial: emptySavedContextSettings
20719
+ });
20204
20720
  var ActivityDoc = defineDoc5({
20205
20721
  kind: "app.neko.activity",
20206
20722
  version: 1,
@@ -20247,12 +20763,21 @@ async function createNekoRuntime(options = {}) {
20247
20763
  registry.install(createCodingTools({ images: await createNodePhotonImages() }));
20248
20764
  registry.install(ImageDetails);
20249
20765
  registry.install(AgentFileSearch);
20250
- registry.install(createWebSearchTools(async () => {
20766
+ const tavily = createTavilyProvider(async () => {
20251
20767
  const saved = await effectiveCredentials.read("tavily");
20252
20768
  if (saved?.type === "api_key" && saved.key)
20253
20769
  return saved.key;
20254
20770
  return process.env.TAVILY_API_KEY || undefined;
20255
- }));
20771
+ });
20772
+ const deepseekApiKey = async () => {
20773
+ const saved = await effectiveCredentials.read("deepseek");
20774
+ if (saved?.type === "api_key" && saved.key)
20775
+ return saved.key;
20776
+ return process.env.DEEPSEEK_API_KEY || undefined;
20777
+ };
20778
+ const deepseek = createDeepseekProvider(deepseekApiKey);
20779
+ let searchProvider = options.searchProvider ?? DEFAULT_SEARCH_PROVIDER;
20780
+ registry.install(createWebSearchTools(async () => searchProvider === "deepseek" ? deepseek : tavily));
20256
20781
  const loadResources = createResourceLoader(options.promptHome);
20257
20782
  const resourceSnapshots = createResourceSnapshots(loadResources, (id, requestId) => storage.submissionByRequest(id, requestId, context));
20258
20783
  registry.install(createNekoPrompt(resourceSnapshots.current));
@@ -20266,9 +20791,16 @@ async function createNekoRuntime(options = {}) {
20266
20791
  let approvals;
20267
20792
  const approvalExtension = createApprovalExtension(() => approvals);
20268
20793
  registry.install(approvalExtension);
20794
+ let savedContext = emptySavedContextSettings();
20795
+ let contextState = resolveContextSettings(options.contextSettings, savedContext);
20269
20796
  const harness = await Harness.open(storage, {
20270
20797
  models,
20271
20798
  registry,
20799
+ settings: {
20800
+ get compaction() {
20801
+ return compactionPolicy(contextState, models.getModels().map((model) => model.contextWindow));
20802
+ }
20803
+ },
20272
20804
  onReport: options.onReport,
20273
20805
  conversationCreated: async (tx, record) => {
20274
20806
  (await tx.doc(SessionMetadataDoc, record.id)).createdAt = Date.now();
@@ -20310,6 +20842,21 @@ async function createNekoRuntime(options = {}) {
20310
20842
  cwd: defaultCwd
20311
20843
  }
20312
20844
  });
20845
+ const savedSearch = (await harness.snapshot(SearchSettingsDoc, conversation.id, context))?.provider;
20846
+ if (isSearchProvider(savedSearch))
20847
+ searchProvider = savedSearch;
20848
+ savedContext = sanitizeSavedContextSettings(await harness.snapshot(ContextSettingsDoc, conversation.id, context));
20849
+ contextState = resolveContextSettings(options.contextSettings, savedContext);
20850
+ const persistContext = async (next) => {
20851
+ await harness.commit(async (tx) => {
20852
+ const doc = await tx.doc(ContextSettingsDoc, conversation.id);
20853
+ doc.compaction = next.compaction;
20854
+ doc.pruning = next.pruning;
20855
+ }, context);
20856
+ savedContext = next;
20857
+ contextState = resolveContextSettings(options.contextSettings, savedContext);
20858
+ return contextState;
20859
+ };
20313
20860
  const registerWorkspace = async (path) => {
20314
20861
  await harness.commit(async (tx) => {
20315
20862
  const doc = await tx.doc(WorkspacesDoc, conversation.id);
@@ -20346,10 +20893,22 @@ async function createNekoRuntime(options = {}) {
20346
20893
  for (const record of existing)
20347
20894
  if (!record.owner && !(await harness.snapshot(ArchivedDoc, record.id, context))?.archived)
20348
20895
  await metadata.restore(record.id);
20896
+ const pruner = new ContextPruner({
20897
+ harness,
20898
+ context,
20899
+ settings: () => contextState.effective.pruning,
20900
+ windowOf: async (id) => {
20901
+ const target = await harness.conversation(id, context);
20902
+ const model = (await target?.agent(context))?.model;
20903
+ return model && models.getModel(model.provider, model.modelId)?.contextWindow || 0;
20904
+ },
20905
+ report: (error) => options.onReport?.(error)
20906
+ });
20349
20907
  let closing;
20350
20908
  const cleanup = () => {
20351
20909
  if (!closing)
20352
20910
  closing = (async () => {
20911
+ pruner.close();
20353
20912
  jobs.shutdown();
20354
20913
  await workspaceFiles.close();
20355
20914
  await metadata.close();
@@ -20716,6 +21275,26 @@ async function createNekoRuntime(options = {}) {
20716
21275
  source: runtimeKeys.has(spec.id) ? "runtime" : savedConfigured ? "saved" : environmentConfigured ? "environment" : spec.keyless ? "keyless" : "none"
20717
21276
  };
20718
21277
  },
21278
+ async contextSettings() {
21279
+ return contextState;
21280
+ },
21281
+ async setContextSettings(patch) {
21282
+ return persistContext(applyContextPatch(savedContext, parseContextSettingsPatch(patch)));
21283
+ },
21284
+ async resetContextSettings() {
21285
+ return persistContext(emptySavedContextSettings());
21286
+ },
21287
+ async searchSettings() {
21288
+ return { selected: searchProvider };
21289
+ },
21290
+ async setSearchProvider(provider) {
21291
+ if (!isSearchProvider(provider))
21292
+ throw new Error("Unknown search provider.");
21293
+ await harness.commit(async (tx) => {
21294
+ (await tx.doc(SearchSettingsDoc, conversation.id)).provider = provider;
21295
+ }, context);
21296
+ searchProvider = provider;
21297
+ },
20719
21298
  async saveCredential(provider, key) {
20720
21299
  const spec = credentialProvider(provider);
20721
21300
  if (typeof key !== "string" || !key.trim() || key.length > 4096 || Array.from(key).some((char) => char.charCodeAt(0) < 32))
@@ -20789,506 +21368,34 @@ async function createNekoRuntime(options = {}) {
20789
21368
  }
20790
21369
  };
20791
21370
  }
20792
- // packages/gateway/src/identity.ts
20793
- function isServerId(value) {
20794
- return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
20795
- }
20796
-
20797
- // packages/gateway/src/mobile/admin.ts
20798
- import { createHash, timingSafeEqual } from "crypto";
20799
- import { ChordError, method, remote } from "@earendil-works/chord";
21371
+ // packages/gateway/src/contract.ts
21372
+ import { hostNamed, method, remote } from "@earendil-works/chord";
20800
21373
  import { Type as Type5 } from "typebox";
20801
-
20802
- // packages/gateway/src/mobile/store.ts
20803
- import { randomBytes } from "crypto";
20804
- import { chmod as chmod2, mkdir as mkdir2, readFile as readFile4, rename as rename2, rm, writeFile } from "fs/promises";
20805
- import { join as join6 } from "path";
20806
- var MOBILE_STATE_FORMAT_VERSION = 1;
20807
- var MOBILE_DEFAULT_PORT = 8323;
20808
- var MOBILE_ADMIN_TOKEN_BYTES = 32;
20809
- var POSIX = process.platform !== "win32";
20810
- var ADMIN_TOKEN_PATTERN = /^[A-Za-z0-9_-]{43}$/;
20811
- var CREDENTIAL_HASH_PATTERN = /^[0-9a-fA-F]{64}$/;
20812
- function defaultState() {
20813
- return {
20814
- formatVersion: MOBILE_STATE_FORMAT_VERSION,
20815
- enabled: false,
20816
- address: null,
20817
- port: MOBILE_DEFAULT_PORT,
20818
- devices: [],
20819
- receipts: [],
20820
- lastConnectedAddress: null
20821
- };
20822
- }
20823
- function freezeState(state) {
20824
- for (const device of state.devices)
20825
- Object.freeze(device);
20826
- Object.freeze(state.devices);
20827
- for (const receipt of state.receipts)
20828
- Object.freeze(receipt);
20829
- Object.freeze(state.receipts);
20830
- return Object.freeze(state);
20831
- }
20832
- function isNotFound(error) {
20833
- return error?.code === "ENOENT";
20834
- }
20835
- function invalidState(statePath, detail) {
20836
- return new Error(`Invalid mobile state file ${statePath}: ${detail}`);
20837
- }
20838
- function readString(value, what, statePath) {
20839
- if (typeof value !== "string")
20840
- throw invalidState(statePath, `${what} must be a string`);
20841
- return value;
20842
- }
20843
- function readNonEmptyString(value, what, statePath) {
20844
- const text = readString(value, what, statePath);
20845
- if (text === "")
20846
- throw invalidState(statePath, `${what} must be a non-empty string`);
20847
- return text;
20848
- }
20849
- function readCredentialHash(value, what, statePath) {
20850
- const text = readString(value, what, statePath);
20851
- if (!CREDENTIAL_HASH_PATTERN.test(text))
20852
- throw invalidState(statePath, `${what} must be a 64-character hex string`);
20853
- return text;
20854
- }
20855
- function readRevision(value, what, statePath) {
20856
- if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0)
20857
- throw invalidState(statePath, `${what} must be a non-negative integer`);
20858
- return value;
20859
- }
20860
- function readTimestamp(value, what, statePath) {
20861
- if (typeof value !== "number" || !Number.isFinite(value))
20862
- throw invalidState(statePath, `${what} must be a finite number`);
20863
- return value;
20864
- }
20865
- function parseDevice(raw, index, statePath) {
20866
- const what = `devices[${index}]`;
20867
- if (typeof raw !== "object" || raw === null)
20868
- throw invalidState(statePath, `${what} must be an object`);
20869
- const record = raw;
20870
- return {
20871
- deviceId: readNonEmptyString(record.deviceId, `${what}.deviceId`, statePath),
20872
- name: readString(record.name, `${what}.name`, statePath),
20873
- credentialHash: readCredentialHash(record.credentialHash, `${what}.credentialHash`, statePath),
20874
- grantRevision: readRevision(record.grantRevision, `${what}.grantRevision`, statePath),
20875
- pairedAt: readTimestamp(record.pairedAt, `${what}.pairedAt`, statePath),
20876
- lastSeenAt: readTimestamp(record.lastSeenAt, `${what}.lastSeenAt`, statePath)
20877
- };
20878
- }
20879
- function parseReceipt(raw, index, statePath) {
20880
- const what = `receipts[${index}]`;
20881
- if (typeof raw !== "object" || raw === null)
20882
- throw invalidState(statePath, `${what} must be an object`);
20883
- const record = raw;
20884
- return {
20885
- pairRequestId: readNonEmptyString(record.pairRequestId, `${what}.pairRequestId`, statePath),
20886
- deviceId: readNonEmptyString(record.deviceId, `${what}.deviceId`, statePath),
20887
- credentialHash: readCredentialHash(record.credentialHash, `${what}.credentialHash`, statePath),
20888
- grantRevision: readRevision(record.grantRevision, `${what}.grantRevision`, statePath),
20889
- expiresAt: readTimestamp(record.expiresAt, `${what}.expiresAt`, statePath)
20890
- };
20891
- }
20892
- function parseState(raw, statePath) {
20893
- if (typeof raw !== "object" || raw === null)
20894
- throw invalidState(statePath, "root must be an object");
20895
- const record = raw;
20896
- if (record.formatVersion !== MOBILE_STATE_FORMAT_VERSION)
20897
- throw invalidState(statePath, `unsupported formatVersion (expected ${MOBILE_STATE_FORMAT_VERSION})`);
20898
- if (typeof record.enabled !== "boolean")
20899
- throw invalidState(statePath, "enabled must be a boolean");
20900
- if (record.address !== null && typeof record.address !== "string")
20901
- throw invalidState(statePath, "address must be a string or null");
20902
- const port = record.port;
20903
- if (typeof port !== "number" || !Number.isInteger(port) || port < 1 || port > 65535)
20904
- throw invalidState(statePath, "port must be an integer between 1 and 65535");
20905
- if (!Array.isArray(record.devices))
20906
- throw invalidState(statePath, "devices must be an array");
20907
- if (!Array.isArray(record.receipts))
20908
- throw invalidState(statePath, "receipts must be an array");
20909
- const lastConnectedAddress = record.lastConnectedAddress === undefined || record.lastConnectedAddress === null ? null : readString(record.lastConnectedAddress, "lastConnectedAddress", statePath);
20910
- return {
20911
- formatVersion: MOBILE_STATE_FORMAT_VERSION,
20912
- enabled: record.enabled,
20913
- address: record.address,
20914
- port,
20915
- devices: record.devices.map((device, index) => parseDevice(device, index, statePath)),
20916
- receipts: record.receipts.map((receipt, index) => parseReceipt(receipt, index, statePath)),
20917
- lastConnectedAddress
20918
- };
20919
- }
20920
- function assertPort(port) {
20921
- if (!Number.isInteger(port) || port < 1 || port > 65535)
20922
- throw new Error("port must be an integer between 1 and 65535");
20923
- return port;
20924
- }
20925
- var tempCounter = 0;
20926
- async function writeFileAtomic(filePath, contents, mode) {
20927
- const tempPath = `${filePath}.tmp-${process.pid}-${++tempCounter}`;
20928
- try {
20929
- await writeFile(tempPath, contents, { mode });
20930
- if (POSIX)
20931
- await chmod2(tempPath, mode);
20932
- await rename2(tempPath, filePath);
20933
- } finally {
20934
- await rm(tempPath, { force: true });
20935
- }
20936
- }
20937
- async function writeSecretFile(filePath, contents) {
20938
- await writeFileAtomic(filePath, contents, 384);
20939
- }
20940
- async function readSecretFile(filePath) {
20941
- try {
20942
- return await readFile4(filePath, "utf8");
20943
- } catch (error) {
20944
- if (isNotFound(error))
20945
- return;
20946
- throw error;
20947
- }
20948
- }
20949
- async function loadState(statePath) {
20950
- let raw;
20951
- try {
20952
- raw = await readFile4(statePath, "utf8");
20953
- } catch (error) {
20954
- if (isNotFound(error))
20955
- return freezeState(defaultState());
20956
- throw error;
20957
- }
20958
- let parsed;
20959
- try {
20960
- parsed = JSON.parse(raw);
20961
- } catch {
20962
- throw invalidState(statePath, "not valid JSON");
20963
- }
20964
- return freezeState(parseState(parsed, statePath));
20965
- }
20966
-
20967
- class MobileStore {
20968
- mobileDir;
20969
- tlsDir;
20970
- statePath;
20971
- adminTokenPath;
20972
- current;
20973
- queue = Promise.resolve();
20974
- constructor(mobileDir, initial) {
20975
- this.mobileDir = mobileDir;
20976
- this.tlsDir = join6(mobileDir, "tls");
20977
- this.statePath = join6(mobileDir, "state.json");
20978
- this.adminTokenPath = join6(mobileDir, "admin-token");
20979
- this.current = initial;
20980
- }
20981
- static async open(nekoDir) {
20982
- const mobileDir = join6(nekoDir, "mobile");
20983
- const tlsDir = join6(mobileDir, "tls");
20984
- await mkdir2(mobileDir, { recursive: true, mode: 448 });
20985
- await mkdir2(tlsDir, { recursive: true, mode: 448 });
20986
- if (POSIX) {
20987
- await chmod2(mobileDir, 448);
20988
- await chmod2(tlsDir, 448);
20989
- }
20990
- return new MobileStore(mobileDir, await loadState(join6(mobileDir, "state.json")));
20991
- }
20992
- getState() {
20993
- return this.current;
20994
- }
20995
- updateConfig(patch) {
20996
- return this.mutate((state) => ({
20997
- ...state,
20998
- ...patch.enabled !== undefined ? { enabled: patch.enabled } : {},
20999
- ...patch.address !== undefined ? { address: patch.address } : {},
21000
- ...patch.port !== undefined ? { port: assertPort(patch.port) } : {}
21001
- }));
21002
- }
21003
- upsertDevice(device) {
21004
- return this.mutate((state) => ({
21005
- ...state,
21006
- devices: state.devices.some((d) => d.deviceId === device.deviceId) ? state.devices.map((d) => d.deviceId === device.deviceId ? device : d) : [...state.devices, device]
21007
- }));
21008
- }
21009
- removeDevice(deviceId) {
21010
- return this.mutate((state) => ({
21011
- ...state,
21012
- devices: state.devices.filter((d) => d.deviceId !== deviceId)
21013
- }));
21014
- }
21015
- completePairing(input) {
21016
- return this.mutate((state) => {
21017
- const now = Date.now();
21018
- const existing = state.devices.find((d) => d.deviceId === input.deviceId);
21019
- const grantRevision = existing === undefined ? 1 : existing.grantRevision + 1;
21020
- const device = {
21021
- deviceId: input.deviceId,
21022
- name: input.name,
21023
- credentialHash: input.credentialHash,
21024
- grantRevision,
21025
- pairedAt: input.pairedAt,
21026
- lastSeenAt: input.pairedAt
21027
- };
21028
- const receipt = {
21029
- pairRequestId: input.pairRequestId,
21030
- deviceId: input.deviceId,
21031
- credentialHash: input.credentialHash,
21032
- grantRevision,
21033
- expiresAt: input.receiptExpiresAt
21034
- };
21035
- return {
21036
- ...state,
21037
- devices: existing === undefined ? [...state.devices, device] : state.devices.map((d) => d.deviceId === input.deviceId ? device : d),
21038
- receipts: [
21039
- ...state.receipts.filter((r) => r.expiresAt > now && r.pairRequestId !== input.pairRequestId),
21040
- receipt
21041
- ]
21042
- };
21043
- });
21044
- }
21045
- putReceipt(receipt) {
21046
- return this.mutate((state) => ({
21047
- ...state,
21048
- receipts: state.receipts.some((r) => r.pairRequestId === receipt.pairRequestId) ? state.receipts.map((r) => r.pairRequestId === receipt.pairRequestId ? receipt : r) : [...state.receipts, receipt]
21049
- }));
21050
- }
21051
- removeReceipt(pairRequestId) {
21052
- return this.mutate((state) => ({
21053
- ...state,
21054
- receipts: state.receipts.filter((r) => r.pairRequestId !== pairRequestId)
21055
- }));
21056
- }
21057
- touchDevice(deviceId, lastSeenAt) {
21058
- return this.mutate((state) => {
21059
- const existing = state.devices.find((d) => d.deviceId === deviceId);
21060
- if (existing === undefined)
21061
- return state;
21062
- return {
21063
- ...state,
21064
- devices: state.devices.map((d) => d.deviceId === deviceId ? { ...d, lastSeenAt } : d)
21065
- };
21066
- });
21067
- }
21068
- rememberConnectedAddress(address) {
21069
- if (this.current.lastConnectedAddress === address)
21070
- return Promise.resolve(this.current);
21071
- return this.mutate((state) => ({ ...state, lastConnectedAddress: address }));
21072
- }
21073
- clearMobileAuthorizations() {
21074
- return this.mutate((state) => ({ ...state, devices: [], receipts: [] }));
21075
- }
21076
- ensureAdminToken() {
21077
- return this.enqueue(async () => {
21078
- const existing = await readSecretFile(this.adminTokenPath);
21079
- if (existing !== undefined) {
21080
- const token = existing.trim();
21081
- if (ADMIN_TOKEN_PATTERN.test(token))
21082
- return token;
21083
- throw new Error(`Invalid mobile admin token file ${this.adminTokenPath}: expected a base64url token`);
21084
- }
21085
- const token = randomBytes(MOBILE_ADMIN_TOKEN_BYTES).toString("base64url");
21086
- await writeSecretFile(this.adminTokenPath, `${token}
21087
- `);
21088
- return token;
21089
- });
21090
- }
21091
- mutate(recipe) {
21092
- return this.enqueue(async () => {
21093
- const next = freezeState(recipe(this.current));
21094
- await writeFileAtomic(this.statePath, `${JSON.stringify(next, null, "\t")}
21095
- `, 384);
21096
- this.current = next;
21097
- return next;
21098
- });
21099
- }
21100
- enqueue(task) {
21101
- const run = this.queue.then(task);
21102
- this.queue = run.then(() => {
21103
- return;
21104
- }, () => {
21105
- return;
21106
- });
21107
- return run;
21108
- }
21109
- }
21110
-
21111
- // packages/gateway/src/mobile/admin.ts
21112
- function contract(name, def) {
21113
- const variants = Object.fromEntries(Object.entries(def.variants).map(([key, value]) => [
21114
- key,
21115
- {
21116
- methods: Object.fromEntries(Object.entries(value.methods ?? {}).map(([member, [args, result]]) => [member, method(args, result)]))
21117
- }
21118
- ]));
21119
- return remote(name, { ...def, variants });
21120
- }
21121
- var listenerStateSchema = Type5.Union([
21122
- Type5.Object({ status: Type5.Literal("disabled") }),
21123
- Type5.Object({ status: Type5.Literal("starting") }),
21124
- Type5.Object({
21125
- status: Type5.Literal("listening"),
21126
- address: Type5.String(),
21127
- port: Type5.Integer({ minimum: 1, maximum: 65535 }),
21128
- bindAddress: Type5.Optional(Type5.String()),
21129
- mode: Type5.Optional(Type5.Union([Type5.Literal("direct"), Type5.Literal("wsl-relay")]))
21130
- }),
21131
- Type5.Object({ status: Type5.Literal("error"), error: Type5.String() }),
21132
- Type5.Object({ status: Type5.Literal("stopping") })
21133
- ]);
21134
- var adminDeviceSchema = Type5.Object({
21135
- deviceId: Type5.String(),
21136
- name: Type5.String(),
21137
- pairedAt: Type5.Integer(),
21138
- lastSeenAt: Type5.Integer(),
21139
- online: Type5.Boolean()
21140
- });
21141
- var NekoMobileAdmin = contract("neko.mobile-admin", {
21142
- version: 1,
21143
- input: Type5.Object({ token: Type5.String() }),
21144
- variants: {
21145
- admin: {
21146
- methods: {
21147
- state: [
21148
- Type5.Null(),
21149
- Type5.Object({
21150
- state: listenerStateSchema,
21151
- candidates: Type5.Array(Type5.String()),
21152
- port: Type5.Integer({ minimum: 1, maximum: 65535 }),
21153
- certificate: Type5.Union([
21154
- Type5.Object({ fingerprint: Type5.String(), expiresAt: Type5.Integer() }),
21155
- Type5.Null()
21156
- ]),
21157
- devices: Type5.Array(adminDeviceSchema),
21158
- lastConnectedAddress: Type5.Union([Type5.String(), Type5.Null()]),
21159
- wsl: Type5.Optional(Type5.Union([
21160
- Type5.Object({ interop: Type5.Boolean(), error: Type5.Union([Type5.String(), Type5.Null()]) }),
21161
- Type5.Null()
21162
- ]))
21163
- })
21164
- ],
21165
- enable: [
21166
- Type5.Object({
21167
- address: Type5.String(),
21168
- port: Type5.Optional(Type5.Integer({ minimum: 1, maximum: 65535 }))
21169
- }),
21170
- Type5.Object({
21171
- status: Type5.Literal("listening"),
21172
- address: Type5.String(),
21173
- port: Type5.Integer({ minimum: 1, maximum: 65535 }),
21174
- bindAddress: Type5.Optional(Type5.String()),
21175
- mode: Type5.Optional(Type5.Union([Type5.Literal("direct"), Type5.Literal("wsl-relay")]))
21176
- })
21177
- ],
21178
- disable: [Type5.Null(), listenerStateSchema],
21179
- createPairing: [Type5.Null(), Type5.Object({ pairingText: Type5.String(), expiresAt: Type5.Integer() })],
21180
- cancelPairing: [Type5.Null(), Type5.Null()],
21181
- revoke: [
21182
- Type5.Object({ deviceId: Type5.String({ minLength: 1 }) }),
21183
- Type5.Object({ revoked: Type5.Boolean() })
21184
- ],
21185
- regenerateCertificate: [
21186
- Type5.Null(),
21187
- Type5.Object({ fingerprint: Type5.String(), expiresAt: Type5.Integer() })
21188
- ]
21189
- }
21190
- }
21191
- }
21192
- });
21193
- function isMobilePrincipal(principal) {
21194
- return typeof principal === "object" && principal !== null && principal.kind === "mobile";
21195
- }
21196
- function digest(value) {
21197
- return createHash("sha256").update(value, "utf8").digest();
21198
- }
21199
- async function tokenMatches(token, store) {
21200
- if (store === undefined)
21201
- return false;
21202
- let adminToken;
21203
- try {
21204
- adminToken = await store.ensureAdminToken();
21205
- } catch {
21206
- return false;
21207
- }
21208
- const expected = digest(adminToken);
21209
- const provided = digest(token);
21210
- return expected.length === provided.length && timingSafeEqual(expected, provided);
21211
- }
21212
- function createMobileAdminFactory(deps) {
21213
- return (input, principal) => {
21214
- if (isMobilePrincipal(principal))
21215
- throw new ChordError("forbidden", "mobile principals cannot administer pairing");
21216
- const guard = async () => {
21217
- if (!await tokenMatches(input.token, deps.store))
21218
- throw new ChordError("forbidden", "forbidden");
21219
- };
21220
- return {
21221
- kind: "admin",
21222
- async state() {
21223
- await guard();
21224
- return deps.mobile.adminState();
21225
- },
21226
- async enable(params) {
21227
- await guard();
21228
- const state = await deps.mobile.enable({ address: params.address, port: params.port });
21229
- if (state.status !== "listening")
21230
- throw new ChordError("unavailable", `mobile listener failed: ${state.status}`);
21231
- return state;
21232
- },
21233
- async disable() {
21234
- await guard();
21235
- return deps.mobile.disable();
21236
- },
21237
- async createPairing() {
21238
- await guard();
21239
- return deps.mobile.createPairing();
21240
- },
21241
- async cancelPairing() {
21242
- await guard();
21243
- deps.mobile.cancelPairing();
21244
- return null;
21245
- },
21246
- async revoke(params) {
21247
- await guard();
21248
- return { revoked: await deps.mobile.revoke(params.deviceId) };
21249
- },
21250
- async regenerateCertificate() {
21251
- await guard();
21252
- return deps.mobile.regenerateCertificate();
21253
- }
21254
- };
21255
- };
21256
- }
21257
-
21258
- // packages/gateway/src/mobile/listener.ts
21259
- import { randomUUID as randomUUID2 } from "crypto";
21260
- import { createServer } from "https";
21261
- import { hostname } from "os";
21262
- import { createHost as createHost2, memoryLinkPair } from "@earendil-works/chord";
21263
- import { WebSocketServer } from "ws";
21264
-
21265
- // packages/gateway/src/contract.ts
21266
- import { method as method2, remote as remote2 } from "@earendil-works/chord";
21267
- import { Type as Type6 } from "typebox";
21374
+ var GATEWAY_HOST = "neko";
21268
21375
  var rpc = (args, result) => [args, result];
21269
- function contract2(name, def) {
21376
+ function contract(name, def) {
21270
21377
  const variants = Object.fromEntries(Object.entries(def.variants).map(([key, value]) => [
21271
21378
  key,
21272
21379
  {
21273
21380
  ...value,
21274
- methods: Object.fromEntries(Object.entries(value.methods ?? {}).map(([member, [args, result]]) => [member, method2(args, result)]))
21381
+ methods: Object.fromEntries(Object.entries(value.methods ?? {}).map(([member, [args, result]]) => [member, method(args, result)]))
21275
21382
  }
21276
21383
  ]));
21277
- return remote2(name, { ...def, variants });
21384
+ return remote(name, { ...def, host: hostNamed(GATEWAY_HOST), variants });
21278
21385
  }
21279
- var str = Type6.String();
21280
- var num = Type6.Number();
21281
- var bool = Type6.Boolean();
21282
- var nil = Type6.Null();
21283
- var integer = Type6.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER });
21284
- var nullableString = Type6.Union([str, nil]);
21285
- var phase = Type6.Enum(["pending", "running", "done"]);
21286
- var level = Type6.Enum(["project", "user", "builtin"]);
21287
- var thinking = Type6.Enum(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
21288
- var model = Type6.Object({ provider: str, modelId: str });
21289
- var change = Type6.Object({ model: Type6.Optional(model), thinkingLevel: Type6.Optional(thinking) }, { additionalProperties: false });
21290
- var usage = Type6.Object({ input: num, output: num, cacheRead: num, cacheWrite: num, cost: num });
21291
- var image = Type6.Object({
21386
+ var str = Type5.String();
21387
+ var num = Type5.Number();
21388
+ var bool = Type5.Boolean();
21389
+ var nil = Type5.Null();
21390
+ var integer = Type5.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER });
21391
+ var nullableString = Type5.Union([str, nil]);
21392
+ var phase = Type5.Enum(["pending", "running", "done"]);
21393
+ var level = Type5.Enum(["project", "user", "builtin"]);
21394
+ var thinking = Type5.Enum(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
21395
+ var model = Type5.Object({ provider: str, modelId: str });
21396
+ var change = Type5.Object({ model: Type5.Optional(model), thinkingLevel: Type5.Optional(thinking) }, { additionalProperties: false });
21397
+ var usage = Type5.Object({ input: num, output: num, cacheRead: num, cacheWrite: num, cost: num });
21398
+ var image = Type5.Object({
21292
21399
  name: str,
21293
21400
  mimeType: str,
21294
21401
  width: integer,
@@ -21297,8 +21404,8 @@ var image = Type6.Object({
21297
21404
  entryId: integer,
21298
21405
  contentIndex: integer
21299
21406
  });
21300
- var skill = Type6.Object({ name: str, location: str, body: str, args: str });
21301
- var call = Type6.Object({
21407
+ var skill = Type5.Object({ name: str, location: str, body: str, args: str });
21408
+ var call = Type5.Object({
21302
21409
  callId: str,
21303
21410
  name: str,
21304
21411
  summary: str,
@@ -21306,116 +21413,116 @@ var call = Type6.Object({
21306
21413
  phase,
21307
21414
  output: nullableString,
21308
21415
  diff: nullableString,
21309
- diffAdded: Type6.Union([num, nil]),
21310
- diffRemoved: Type6.Union([num, nil]),
21311
- diffTruncated: Type6.Optional(bool)
21416
+ diffAdded: Type5.Union([num, nil]),
21417
+ diffRemoved: Type5.Union([num, nil]),
21418
+ diffTruncated: Type5.Optional(bool)
21312
21419
  });
21313
- var DelegationSchema = Type6.Object({
21420
+ var DelegationSchema = Type5.Object({
21314
21421
  parentId: integer,
21315
- agentId: Type6.Optional(str),
21316
- round: Type6.Optional(integer),
21317
- runnerId: Type6.Optional(integer),
21318
- inputEntryId: Type6.Optional(integer),
21319
- answerEntryId: Type6.Optional(integer),
21320
- roundUsage: Type6.Optional(Type6.Union([usage, nil])),
21422
+ agentId: Type5.Optional(str),
21423
+ round: Type5.Optional(integer),
21424
+ runnerId: Type5.Optional(integer),
21425
+ inputEntryId: Type5.Optional(integer),
21426
+ answerEntryId: Type5.Optional(integer),
21427
+ roundUsage: Type5.Optional(Type5.Union([usage, nil])),
21321
21428
  taskId: integer,
21322
21429
  callId: str,
21323
21430
  childId: integer,
21324
- role: Type6.Enum(["explore", "plan", "coder"]),
21431
+ role: Type5.Enum(["explore", "plan", "coder"]),
21325
21432
  title: str,
21326
21433
  createdAt: num,
21327
- startedAt: Type6.Union([num, nil]),
21328
- endedAt: Type6.Union([num, nil]),
21329
- status: Type6.Enum(["queued", "running", "completed", "failed", "cancelled"]),
21434
+ startedAt: Type5.Union([num, nil]),
21435
+ endedAt: Type5.Union([num, nil]),
21436
+ status: Type5.Enum(["queued", "running", "completed", "failed", "cancelled"]),
21330
21437
  report: nullableString,
21331
- approvalCount: Type6.Optional(integer),
21332
- historyThrough: Type6.Optional(integer),
21333
- historyUsage: Type6.Optional(usage)
21438
+ approvalCount: Type5.Optional(integer),
21439
+ historyThrough: Type5.Optional(integer),
21440
+ historyUsage: Type5.Optional(usage)
21334
21441
  });
21335
- var SubagentSummarySchema = Type6.Object({
21442
+ var SubagentSummarySchema = Type5.Object({
21336
21443
  agentId: str,
21337
21444
  parentId: integer,
21338
21445
  childId: integer,
21339
- role: Type6.Enum(["explore", "plan", "coder"]),
21446
+ role: Type5.Enum(["explore", "plan", "coder"]),
21340
21447
  title: str,
21341
- status: Type6.Enum(["idle", "queued", "running"]),
21342
- activeRoundId: Type6.Union([integer, nil]),
21448
+ status: Type5.Enum(["idle", "queued", "running"]),
21449
+ activeRoundId: Type5.Union([integer, nil]),
21343
21450
  lastRoundId: integer,
21344
- lastReport: Type6.Optional(nullableString),
21451
+ lastReport: Type5.Optional(nullableString),
21345
21452
  roundCount: integer,
21346
21453
  taskTitle: str,
21347
21454
  approvalCount: integer,
21348
21455
  createdAt: num,
21349
- historyThrough: Type6.Optional(integer),
21350
- historyUsage: Type6.Optional(usage)
21456
+ historyThrough: Type5.Optional(integer),
21457
+ historyUsage: Type5.Optional(usage)
21351
21458
  });
21352
- var jobStatus = Type6.Enum(["running", "exited", "failed", "stopped", "lost"]);
21353
- var JobSummarySchema = Type6.Object({
21459
+ var jobStatus = Type5.Enum(["running", "exited", "failed", "stopped", "lost"]);
21460
+ var JobSummarySchema = Type5.Object({
21354
21461
  jobId: str,
21355
21462
  title: str,
21356
21463
  command: str,
21357
21464
  cwd: str,
21358
21465
  status: jobStatus,
21359
21466
  startedAt: num,
21360
- endedAt: Type6.Union([num, nil]),
21361
- exitCode: Type6.Union([num, nil]),
21362
- reason: Type6.Optional(Type6.Enum(["timeout", "spawn-error", "restart", "aborted"])),
21363
- timeoutSeconds: Type6.Optional(num),
21467
+ endedAt: Type5.Union([num, nil]),
21468
+ exitCode: Type5.Union([num, nil]),
21469
+ reason: Type5.Optional(Type5.Enum(["timeout", "spawn-error", "restart", "aborted"])),
21470
+ timeoutSeconds: Type5.Optional(num),
21364
21471
  outputBytes: integer
21365
21472
  });
21366
- var JobOutputSchema = Type6.Object({
21473
+ var JobOutputSchema = Type5.Object({
21367
21474
  job: JobSummarySchema,
21368
21475
  text: str,
21369
21476
  offset: integer,
21370
21477
  truncated: bool,
21371
21478
  dropped: integer
21372
21479
  });
21373
- var ChatViewSchema = Type6.Object({
21374
- historyBefore: Type6.Optional(Type6.Union([integer, nil])),
21375
- agents: Type6.Optional(Type6.Array(SubagentSummarySchema)),
21376
- jobs: Type6.Optional(Type6.Array(JobSummarySchema)),
21377
- rounds: Type6.Optional(Type6.Array(DelegationSchema)),
21378
- subagents: Type6.Optional(Type6.Array(DelegationSchema)),
21379
- taskUsage: Type6.Optional(usage),
21480
+ var ChatViewSchema = Type5.Object({
21481
+ historyBefore: Type5.Optional(Type5.Union([integer, nil])),
21482
+ agents: Type5.Optional(Type5.Array(SubagentSummarySchema)),
21483
+ jobs: Type5.Optional(Type5.Array(JobSummarySchema)),
21484
+ rounds: Type5.Optional(Type5.Array(DelegationSchema)),
21485
+ subagents: Type5.Optional(Type5.Array(DelegationSchema)),
21486
+ taskUsage: Type5.Optional(usage),
21380
21487
  model: str,
21381
- agent: Type6.Object({ model: Type6.Union([model, nil]), thinkingLevel: Type6.Union([thinking, nil]) }),
21488
+ agent: Type5.Object({ model: Type5.Union([model, nil]), thinkingLevel: Type5.Union([thinking, nil]) }),
21382
21489
  cwd: str,
21383
21490
  busy: bool,
21384
21491
  status: nullableString,
21385
21492
  usage,
21386
- contextUsage: Type6.Optional(Type6.Object({ tokens: Type6.Union([num, nil]), window: num, percent: Type6.Union([num, nil]) })),
21387
- compaction: Type6.Optional(Type6.Object({
21388
- failure: Type6.Optional(Type6.Object({ taskId: integer, reason: Type6.Enum(["manual", "threshold", "overflow"]), error: str })),
21389
- active: Type6.Array(Type6.Object({
21493
+ contextUsage: Type5.Optional(Type5.Object({ tokens: Type5.Union([num, nil]), window: num, percent: Type5.Union([num, nil]) })),
21494
+ compaction: Type5.Optional(Type5.Object({
21495
+ failure: Type5.Optional(Type5.Object({ taskId: integer, reason: Type5.Enum(["manual", "threshold", "overflow"]), error: str })),
21496
+ active: Type5.Array(Type5.Object({
21390
21497
  taskId: integer,
21391
- reason: Type6.Enum(["manual", "threshold", "overflow"]),
21498
+ reason: Type5.Enum(["manual", "threshold", "overflow"]),
21392
21499
  blocking: bool,
21393
21500
  attempt: integer,
21394
- retry: Type6.Optional(Type6.Object({ at: num, error: str }))
21501
+ retry: Type5.Optional(Type5.Object({ at: num, error: str }))
21395
21502
  })),
21396
- latest: Type6.Union([
21503
+ latest: Type5.Union([
21397
21504
  nil,
21398
- Type6.Object({
21505
+ Type5.Object({
21399
21506
  taskId: integer,
21400
- phase: Type6.Enum(["summarizing", "waiting-to-apply", "applied", "nothing", "skipped", "cancelled", "failed"]),
21507
+ phase: Type5.Enum(["summarizing", "waiting-to-apply", "applied", "nothing", "skipped", "cancelled", "failed"]),
21401
21508
  at: num
21402
21509
  })
21403
21510
  ])
21404
21511
  })),
21405
- items: Type6.Array(Type6.Union([
21406
- Type6.Object({
21407
- type: Type6.Literal("user"),
21408
- order: Type6.Optional(num),
21512
+ items: Type5.Array(Type5.Union([
21513
+ Type5.Object({
21514
+ type: Type5.Literal("user"),
21515
+ order: Type5.Optional(num),
21409
21516
  key: str,
21410
21517
  id: integer,
21411
21518
  text: str,
21412
- images: Type6.Optional(Type6.Array(image)),
21413
- skill: Type6.Optional(skill),
21414
- defaultImagePrompt: Type6.Optional(Type6.Object({ version: Type6.Integer({ minimum: 1 }), text: str }))
21519
+ images: Type5.Optional(Type5.Array(image)),
21520
+ skill: Type5.Optional(skill),
21521
+ defaultImagePrompt: Type5.Optional(Type5.Object({ version: Type5.Integer({ minimum: 1 }), text: str }))
21415
21522
  }),
21416
- Type6.Object({
21417
- type: Type6.Literal("assistant"),
21418
- order: Type6.Optional(num),
21523
+ Type5.Object({
21524
+ type: Type5.Literal("assistant"),
21525
+ order: Type5.Optional(num),
21419
21526
  key: str,
21420
21527
  id: integer,
21421
21528
  thinking: str,
@@ -21424,11 +21531,11 @@ var ChatViewSchema = Type6.Object({
21424
21531
  errorMessage: nullableString,
21425
21532
  note: nullableString
21426
21533
  }),
21427
- Type6.Object({
21428
- type: Type6.Literal("tool"),
21429
- order: Type6.Optional(num),
21534
+ Type5.Object({
21535
+ type: Type5.Literal("tool"),
21536
+ order: Type5.Optional(num),
21430
21537
  key: str,
21431
- images: Type6.Optional(Type6.Array(image)),
21538
+ images: Type5.Optional(Type5.Array(image)),
21432
21539
  callId: str,
21433
21540
  name: str,
21434
21541
  summary: str,
@@ -21437,61 +21544,61 @@ var ChatViewSchema = Type6.Object({
21437
21544
  isError: bool,
21438
21545
  output: nullableString,
21439
21546
  result: nullableString,
21440
- durationMs: Type6.Union([num, nil]),
21547
+ durationMs: Type5.Union([num, nil]),
21441
21548
  note: nullableString,
21442
21549
  diff: nullableString,
21443
- diffAdded: Type6.Union([num, nil]),
21444
- diffRemoved: Type6.Union([num, nil]),
21445
- diffTruncated: Type6.Optional(bool)
21550
+ diffAdded: Type5.Union([num, nil]),
21551
+ diffRemoved: Type5.Union([num, nil]),
21552
+ diffTruncated: Type5.Optional(bool)
21446
21553
  }),
21447
- Type6.Object({
21448
- type: Type6.Literal("system"),
21449
- order: Type6.Optional(num),
21554
+ Type5.Object({
21555
+ type: Type5.Literal("system"),
21556
+ order: Type5.Optional(num),
21450
21557
  key: str,
21451
21558
  id: integer,
21452
21559
  text: str,
21453
- jobId: Type6.Optional(str)
21560
+ jobId: Type5.Optional(str)
21454
21561
  })
21455
21562
  ])),
21456
- streaming: Type6.Union([nil, Type6.Object({ thinking: str, text: str, toolCalls: Type6.Array(call) })])
21563
+ streaming: Type5.Union([nil, Type5.Object({ thinking: str, text: str, toolCalls: Type5.Array(call) })])
21457
21564
  });
21458
- var SessionSummarySchema = Type6.Object({
21565
+ var SessionSummarySchema = Type5.Object({
21459
21566
  workspaceRoot: str,
21460
21567
  lastUsedAt: num,
21461
21568
  title: str,
21462
21569
  firstPromptPreview: str,
21463
21570
  createdAt: num,
21464
- lastMessageAt: Type6.Union([num, nil]),
21465
- model: Type6.Union([model, nil]),
21466
- status: Type6.Enum(["idle", "running", "error"]),
21571
+ lastMessageAt: Type5.Union([num, nil]),
21572
+ model: Type5.Union([model, nil]),
21573
+ status: Type5.Enum(["idle", "running", "error"]),
21467
21574
  id: str
21468
21575
  });
21469
- var DirectoryStateSchema = Type6.Object({ revision: integer, sessions: Type6.Array(SessionSummarySchema) });
21470
- var WorkspaceSummarySchema = Type6.Object({ path: str, createdAt: num });
21471
- var WorkspaceStateSchema = Type6.Object({ revision: integer, workspaces: Type6.Array(WorkspaceSummarySchema) });
21472
- var credentialStatus = Type6.Object({
21576
+ var DirectoryStateSchema = Type5.Object({ revision: integer, sessions: Type5.Array(SessionSummarySchema) });
21577
+ var WorkspaceSummarySchema = Type5.Object({ path: str, createdAt: num });
21578
+ var WorkspaceStateSchema = Type5.Object({ revision: integer, workspaces: Type5.Array(WorkspaceSummarySchema) });
21579
+ var credentialStatus = Type5.Object({
21473
21580
  savedConfigured: bool,
21474
21581
  environmentConfigured: bool,
21475
- source: Type6.Enum(["saved", "environment", "runtime", "keyless", "none"])
21582
+ source: Type5.Enum(["saved", "environment", "runtime", "keyless", "none"])
21476
21583
  });
21477
- var CredentialStateSchema = Type6.Object({
21584
+ var CredentialStateSchema = Type5.Object({
21478
21585
  revision: integer,
21479
- statuses: Type6.Object({ deepseek: credentialStatus, tavily: credentialStatus })
21586
+ statuses: Type5.Object({ deepseek: credentialStatus, tavily: credentialStatus })
21480
21587
  });
21481
- var AttachmentSchema = Type6.Object({ sessionId: str, attachmentId: str });
21482
- var chatInput = Type6.Object({ sessionId: str, attachmentId: str }, { additionalProperties: false });
21483
- var NekoSessionDirectory = contract2("neko.session-directory", {
21588
+ var AttachmentSchema = Type5.Object({ sessionId: str, attachmentId: str });
21589
+ var chatInput = Type5.Object({ sessionId: str, attachmentId: str }, { additionalProperties: false });
21590
+ var NekoSessionDirectory = contract("neko.session-directory", {
21484
21591
  version: 1,
21485
- input: Type6.Object({}, { additionalProperties: false }),
21592
+ input: Type5.Object({}, { additionalProperties: false }),
21486
21593
  variants: { ready: { state: { state: DirectoryStateSchema } } }
21487
21594
  });
21488
- var NekoSessionManagement = contract2("neko.session-management", {
21595
+ var NekoSessionManagement = contract("neko.session-management", {
21489
21596
  version: 1,
21490
- input: Type6.Object({}, { additionalProperties: false }),
21597
+ input: Type5.Object({}, { additionalProperties: false }),
21491
21598
  variants: {
21492
21599
  ready: {
21493
21600
  methods: {
21494
- create: rpc(Type6.Object({ ...change.properties, cwd: Type6.Optional(str) }, { additionalProperties: false }), SessionSummarySchema),
21601
+ create: rpc(Type5.Object({ ...change.properties, cwd: Type5.Optional(str) }, { additionalProperties: false }), SessionSummarySchema),
21495
21602
  archive: rpc(str, nil),
21496
21603
  attach: rpc(str, AttachmentSchema),
21497
21604
  detach: rpc(nil, nil),
@@ -21499,202 +21606,746 @@ var NekoSessionManagement = contract2("neko.session-management", {
21499
21606
  }
21500
21607
  }
21501
21608
  }
21502
- });
21503
- var resize = Type6.Object({
21504
- maxWidth: Type6.Optional(integer),
21505
- maxHeight: Type6.Optional(integer),
21506
- maxBytes: Type6.Optional(integer),
21507
- jpegQuality: Type6.Optional(integer)
21508
- });
21509
- var inputLimits = Type6.Object({
21510
- maxRequestBytes: Type6.Optional(integer),
21511
- images: Type6.Optional(Type6.Object({
21512
- resize: Type6.Optional(resize),
21513
- maxPerMessage: Type6.Optional(integer),
21514
- maxPerRequest: Type6.Optional(integer)
21515
- }))
21516
- });
21517
- var modelInfo = Type6.Object({
21518
- provider: str,
21519
- modelId: str,
21520
- name: str,
21521
- contextWindow: num,
21522
- input: Type6.Array(str),
21523
- inputLimits: Type6.Optional(inputLimits),
21524
- reasoning: bool,
21525
- supportedThinkingLevels: Type6.Array(thinking),
21526
- thinkingLevelAdjustments: Type6.Object({
21527
- off: thinking,
21528
- minimal: thinking,
21529
- low: thinking,
21530
- medium: thinking,
21531
- high: thinking,
21532
- xhigh: thinking,
21533
- max: thinking
21534
- })
21535
- });
21536
- var NekoModels = contract2("neko.models", {
21537
- version: 1,
21538
- input: Type6.Object({}, { additionalProperties: false }),
21539
- variants: { ready: { methods: { list: rpc(nil, Type6.Array(modelInfo)) } } }
21540
- });
21541
- var imageChunk = Type6.Object({ data: str, total: integer, mimeType: str });
21542
- var NekoSubagent = contract2("neko.subagent", {
21543
- version: 1,
21544
- input: Type6.Object({
21545
- sessionId: str,
21546
- attachmentId: str,
21547
- taskId: Type6.Optional(integer),
21548
- agentId: Type6.Optional(str)
21549
- }),
21550
- variants: {
21551
- ready: {
21552
- state: { revision: integer },
21553
- methods: {
21554
- chunk: rpc(Type6.Object({
21555
- kind: Type6.Enum(["view", "history"]),
21556
- before: Type6.Optional(integer),
21557
- token: Type6.Optional(integer),
21558
- offset: integer
21559
- }), Type6.Object({ token: integer, data: str, next: Type6.Union([integer, nil]) })),
21560
- getImage: rpc(Type6.Object({ entryId: integer, contentIndex: integer, offset: integer }), imageChunk),
21561
- decideApproval: rpc(Type6.Object({ id: integer, version: str, decision: Type6.Enum(["allowed", "denied"]) }), Type6.Enum(["pending", "allowed", "denied", "cancelled"])),
21562
- stop: rpc(Type6.Union([nil, Type6.Object({ taskId: integer })]), nil)
21563
- }
21564
- }
21609
+ });
21610
+ var resize = Type5.Object({
21611
+ maxWidth: Type5.Optional(integer),
21612
+ maxHeight: Type5.Optional(integer),
21613
+ maxBytes: Type5.Optional(integer),
21614
+ jpegQuality: Type5.Optional(integer)
21615
+ });
21616
+ var inputLimits = Type5.Object({
21617
+ maxRequestBytes: Type5.Optional(integer),
21618
+ images: Type5.Optional(Type5.Object({
21619
+ resize: Type5.Optional(resize),
21620
+ maxPerMessage: Type5.Optional(integer),
21621
+ maxPerRequest: Type5.Optional(integer)
21622
+ }))
21623
+ });
21624
+ var modelInfo = Type5.Object({
21625
+ provider: str,
21626
+ modelId: str,
21627
+ name: str,
21628
+ contextWindow: num,
21629
+ input: Type5.Array(str),
21630
+ inputLimits: Type5.Optional(inputLimits),
21631
+ reasoning: bool,
21632
+ supportedThinkingLevels: Type5.Array(thinking),
21633
+ thinkingLevelAdjustments: Type5.Object({
21634
+ off: thinking,
21635
+ minimal: thinking,
21636
+ low: thinking,
21637
+ medium: thinking,
21638
+ high: thinking,
21639
+ xhigh: thinking,
21640
+ max: thinking
21641
+ })
21642
+ });
21643
+ var NekoModels = contract("neko.models", {
21644
+ version: 1,
21645
+ input: Type5.Object({}, { additionalProperties: false }),
21646
+ variants: { ready: { methods: { list: rpc(nil, Type5.Array(modelInfo)) } } }
21647
+ });
21648
+ var imageChunk = Type5.Object({ data: str, total: integer, mimeType: str });
21649
+ var NekoSubagent = contract("neko.subagent", {
21650
+ version: 1,
21651
+ input: Type5.Object({
21652
+ sessionId: str,
21653
+ attachmentId: str,
21654
+ taskId: Type5.Optional(integer),
21655
+ agentId: Type5.Optional(str)
21656
+ }),
21657
+ variants: {
21658
+ ready: {
21659
+ state: { revision: integer },
21660
+ methods: {
21661
+ chunk: rpc(Type5.Object({
21662
+ kind: Type5.Enum(["view", "history"]),
21663
+ before: Type5.Optional(integer),
21664
+ token: Type5.Optional(integer),
21665
+ offset: integer
21666
+ }), Type5.Object({ token: integer, data: str, next: Type5.Union([integer, nil]) })),
21667
+ getImage: rpc(Type5.Object({ entryId: integer, contentIndex: integer, offset: integer }), imageChunk),
21668
+ decideApproval: rpc(Type5.Object({ id: integer, version: str, decision: Type5.Enum(["allowed", "denied"]) }), Type5.Enum(["pending", "allowed", "denied", "cancelled"])),
21669
+ stop: rpc(Type5.Union([nil, Type5.Object({ taskId: integer })]), nil)
21670
+ }
21671
+ }
21672
+ }
21673
+ });
21674
+ var NekoChat = contract("neko.chat", {
21675
+ version: 1,
21676
+ input: chatInput,
21677
+ variants: {
21678
+ ready: {
21679
+ state: { view: ChatViewSchema },
21680
+ methods: {
21681
+ submit: rpc(Type5.Object({
21682
+ content: str,
21683
+ images: Type5.Optional(Type5.Array(str, { maxItems: 4 })),
21684
+ requestId: Type5.Optional(str),
21685
+ whenBusy: Type5.Optional(Type5.Enum(["steer", "followUp"]))
21686
+ }, { additionalProperties: false }), nil),
21687
+ abort: rpc(nil, nil),
21688
+ usage: rpc(nil, usage),
21689
+ skills: rpc(nil, Type5.Array(Type5.Object({ name: str, description: str, location: str, level }))),
21690
+ jobOutput: rpc(Type5.Object({ jobId: str, since: Type5.Optional(integer) }, { additionalProperties: false }), JobOutputSchema),
21691
+ jobStop: rpc(Type5.Object({ jobId: str }, { additionalProperties: false }), JobSummarySchema),
21692
+ configure: rpc(change, nil),
21693
+ compact: rpc(Type5.Object({ instructions: Type5.Optional(str) }, { additionalProperties: false }), nil),
21694
+ importImage: rpc(str, Type5.Object({ id: str, name: str })),
21695
+ draftImage: rpc(Type5.Object({ id: str, offset: integer }, { additionalProperties: false }), imageChunk),
21696
+ beginImage: rpc(Type5.Object({ name: str, mimeType: str, size: Type5.Integer({ minimum: 1, maximum: 20971520 }) }, { additionalProperties: false }), str),
21697
+ appendImage: rpc(Type5.Object({ id: str, offset: integer, data: Type5.String({ maxLength: 350000 }) }, { additionalProperties: false }), nil),
21698
+ finishImage: rpc(str, nil),
21699
+ releaseImage: rpc(str, nil),
21700
+ getImage: rpc(Type5.Object({ entryId: integer, contentIndex: integer, offset: integer }, { additionalProperties: false }), imageChunk)
21701
+ }
21702
+ }
21703
+ }
21704
+ });
21705
+ var browse = Type5.Object({ path: str, query: Type5.Optional(str), showHidden: Type5.Optional(bool), cursor: Type5.Optional(str) }, { additionalProperties: false });
21706
+ var page = Type5.Object({
21707
+ path: str,
21708
+ parent: nullableString,
21709
+ entries: Type5.Array(Type5.Object({ name: str, path: str, error: Type5.Optional(str) })),
21710
+ next: nullableString
21711
+ });
21712
+ var NekoWorkspaces = contract("neko.workspaces", {
21713
+ version: 1,
21714
+ input: Type5.Object({}, { additionalProperties: false }),
21715
+ variants: {
21716
+ ready: {
21717
+ state: { state: WorkspaceStateSchema },
21718
+ methods: {
21719
+ add: rpc(Type5.Object({ path: str }, { additionalProperties: false }), WorkspaceSummarySchema),
21720
+ browse: rpc(browse, page)
21721
+ }
21722
+ }
21723
+ }
21724
+ });
21725
+ var provider = Type5.Enum(["deepseek", "tavily"]);
21726
+ var NekoCredentials = contract("neko.credentials", {
21727
+ version: 1,
21728
+ input: Type5.Object({}, { additionalProperties: false }),
21729
+ variants: {
21730
+ ready: {
21731
+ state: { state: CredentialStateSchema },
21732
+ methods: {
21733
+ save: rpc(Type5.Object({ provider, key: Type5.String({ minLength: 1, maxLength: 8192 }) }, { additionalProperties: false }), nil),
21734
+ remove: rpc(Type5.Object({ provider }, { additionalProperties: false }), nil)
21735
+ }
21736
+ }
21737
+ }
21738
+ });
21739
+ var SearchSettingsStateSchema = Type5.Object({
21740
+ revision: integer,
21741
+ selected: Type5.Enum(["tavily", "deepseek"])
21742
+ });
21743
+ var NekoSearchSettings = contract("neko.search-settings", {
21744
+ version: 1,
21745
+ input: Type5.Object({}, { additionalProperties: false }),
21746
+ variants: {
21747
+ ready: {
21748
+ state: { state: SearchSettingsStateSchema },
21749
+ methods: {
21750
+ set: rpc(Type5.Object({ provider: Type5.Enum(["tavily", "deepseek"]) }, { additionalProperties: false }), nil)
21751
+ }
21752
+ }
21753
+ }
21754
+ });
21755
+ var contextSource = Type5.Enum(["default", "config", "saved"]);
21756
+ var ContextSettingsStateSchema = Type5.Object({
21757
+ revision: integer,
21758
+ effective: Type5.Object({
21759
+ compaction: Type5.Object({
21760
+ enabled: bool,
21761
+ triggerPercent: Type5.Union([integer, nil]),
21762
+ keepRecentTokens: integer
21763
+ }),
21764
+ pruning: Type5.Object({
21765
+ enabled: bool,
21766
+ triggerPercent: integer,
21767
+ keepRecent: integer,
21768
+ minChars: integer,
21769
+ minSavePercent: integer
21770
+ })
21771
+ }),
21772
+ sources: Type5.Object({
21773
+ compaction: Type5.Object({ enabled: contextSource, triggerPercent: contextSource, keepRecentTokens: contextSource }),
21774
+ pruning: Type5.Object({
21775
+ enabled: contextSource,
21776
+ triggerPercent: contextSource,
21777
+ keepRecent: contextSource,
21778
+ minChars: contextSource,
21779
+ minSavePercent: contextSource
21780
+ })
21781
+ })
21782
+ });
21783
+ var nullable = (schema) => Type5.Union([schema, nil]);
21784
+ var ContextSettingsPatchSchema = Type5.Object({
21785
+ compaction: Type5.Optional(Type5.Object({
21786
+ enabled: Type5.Optional(nullable(bool)),
21787
+ triggerPercent: Type5.Optional(nullable(integer)),
21788
+ keepRecentTokens: Type5.Optional(nullable(integer))
21789
+ }, { additionalProperties: false })),
21790
+ pruning: Type5.Optional(Type5.Object({
21791
+ enabled: Type5.Optional(nullable(bool)),
21792
+ triggerPercent: Type5.Optional(nullable(integer)),
21793
+ keepRecent: Type5.Optional(nullable(integer)),
21794
+ minChars: Type5.Optional(nullable(integer)),
21795
+ minSavePercent: Type5.Optional(nullable(integer))
21796
+ }, { additionalProperties: false }))
21797
+ }, { additionalProperties: false });
21798
+ var NekoContextSettings = contract("neko.context-settings", {
21799
+ version: 1,
21800
+ input: Type5.Object({}, { additionalProperties: false }),
21801
+ variants: {
21802
+ ready: {
21803
+ state: { state: ContextSettingsStateSchema },
21804
+ methods: {
21805
+ set: rpc(ContextSettingsPatchSchema, nil),
21806
+ reset: rpc(Type5.Object({}, { additionalProperties: false }), nil)
21807
+ }
21808
+ }
21809
+ }
21810
+ });
21811
+ var FileCandidatesSchema = Type5.Object({
21812
+ entries: Type5.Array(Type5.Object({ name: str, path: str, kind: Type5.Enum(["file", "directory"]) }), { maxItems: 100 }),
21813
+ limited: bool,
21814
+ incomplete: bool
21815
+ });
21816
+ var NekoWorkspaceFiles = contract("neko.workspace-files", {
21817
+ version: 1,
21818
+ input: Type5.Object({}, { additionalProperties: false }),
21819
+ variants: {
21820
+ ready: {
21821
+ methods: {
21822
+ search: rpc(Type5.Object({ workspaceRoot: str, query: str }, { additionalProperties: false }), FileCandidatesSchema)
21823
+ }
21824
+ }
21825
+ }
21826
+ });
21827
+ var marketSkill = Type5.Object({
21828
+ name: str,
21829
+ description: str,
21830
+ category: str,
21831
+ source: str,
21832
+ license: str,
21833
+ version: str,
21834
+ sha256: str,
21835
+ sizeBytes: integer,
21836
+ uploadedAt: str,
21837
+ installed: Type5.Object({ project: bool, user: bool, builtin: bool }),
21838
+ activeLevel: Type5.Union([level, nil])
21839
+ });
21840
+ var preview = Type5.Object({ body: str, files: Type5.Array(str) });
21841
+ var scope = Type5.Enum(["user", "project"]);
21842
+ var installResult = Type5.Object({ name: str, scope, path: str, shadowedBy: Type5.Optional(scope) });
21843
+ var NekoSkillMarket = contract("neko.skill-market", {
21844
+ version: 1,
21845
+ input: Type5.Object({}, { additionalProperties: false }),
21846
+ variants: {
21847
+ ready: {
21848
+ methods: {
21849
+ list: rpc(Type5.Object({ query: Type5.Optional(str), workspaceRoot: Type5.Optional(str) }, { additionalProperties: false }), Type5.Array(marketSkill)),
21850
+ detail: rpc(Type5.Object({ name: str, workspaceRoot: Type5.Optional(str) }, { additionalProperties: false }), marketSkill),
21851
+ preview: rpc(Type5.Object({ name: str }, { additionalProperties: false }), preview),
21852
+ install: rpc(Type5.Object({ name: str, scope, workspaceRoot: Type5.Optional(str), overwrite: Type5.Optional(bool) }, { additionalProperties: false }), installResult),
21853
+ uninstall: rpc(Type5.Object({ name: str, scope, workspaceRoot: Type5.Optional(str) }, { additionalProperties: false }), nil)
21854
+ }
21855
+ }
21856
+ }
21857
+ });
21858
+ var ApplicationInfoSchema = Type5.Object({
21859
+ runningVersion: str,
21860
+ installedVersion: nullableString,
21861
+ protocolVersion: Type5.Integer({ minimum: 1 }),
21862
+ latestVersion: nullableString,
21863
+ checkedAt: Type5.Union([integer, nil]),
21864
+ checkState: Type5.Enum(["unknown", "checking", "ok", "failed", "development"]),
21865
+ serverId: str
21866
+ });
21867
+ var NekoApplicationInfo = contract("neko.application-info", {
21868
+ version: 1,
21869
+ input: Type5.Object({}),
21870
+ variants: {
21871
+ ready: { state: { info: ApplicationInfoSchema }, methods: { check: rpc(nil, ApplicationInfoSchema) } }
21872
+ }
21873
+ });
21874
+
21875
+ // packages/gateway/src/identity.ts
21876
+ function isServerId(value) {
21877
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
21878
+ }
21879
+
21880
+ // packages/gateway/src/mobile/admin.ts
21881
+ import { createHash, timingSafeEqual } from "crypto";
21882
+ import { ChordError } from "@earendil-works/chord";
21883
+ import { Type as Type6 } from "typebox";
21884
+
21885
+ // packages/gateway/src/principal.ts
21886
+ var HOST_PRINCIPAL = { kind: "host" };
21887
+ function gatewayPrincipalKey(principal) {
21888
+ switch (principal.kind) {
21889
+ case "mobile":
21890
+ return `mobile:${principal.deviceId}:${principal.grantRevision}:${principal.connectionId}`;
21891
+ case "local":
21892
+ return `local:${principal.connectionId}`;
21893
+ case "host":
21894
+ return "host";
21895
+ }
21896
+ }
21897
+ function isMobilePrincipal(principal) {
21898
+ return typeof principal === "object" && principal !== null && principal.kind === "mobile";
21899
+ }
21900
+
21901
+ // packages/gateway/src/mobile/store.ts
21902
+ import { randomBytes } from "crypto";
21903
+ import { chmod as chmod2, mkdir as mkdir2, readFile as readFile4, rename as rename2, rm, writeFile } from "fs/promises";
21904
+ import { join as join6 } from "path";
21905
+ var MOBILE_STATE_FORMAT_VERSION = 1;
21906
+ var MOBILE_DEFAULT_PORT = 8323;
21907
+ var MOBILE_ADMIN_TOKEN_BYTES = 32;
21908
+ var POSIX = process.platform !== "win32";
21909
+ var ADMIN_TOKEN_PATTERN = /^[A-Za-z0-9_-]{43}$/;
21910
+ var CREDENTIAL_HASH_PATTERN = /^[0-9a-fA-F]{64}$/;
21911
+ function defaultState() {
21912
+ return {
21913
+ formatVersion: MOBILE_STATE_FORMAT_VERSION,
21914
+ enabled: false,
21915
+ address: null,
21916
+ port: MOBILE_DEFAULT_PORT,
21917
+ devices: [],
21918
+ receipts: [],
21919
+ lastConnectedAddress: null
21920
+ };
21921
+ }
21922
+ function freezeState(state) {
21923
+ for (const device of state.devices)
21924
+ Object.freeze(device);
21925
+ Object.freeze(state.devices);
21926
+ for (const receipt of state.receipts)
21927
+ Object.freeze(receipt);
21928
+ Object.freeze(state.receipts);
21929
+ return Object.freeze(state);
21930
+ }
21931
+ function isNotFound(error) {
21932
+ return error?.code === "ENOENT";
21933
+ }
21934
+ function invalidState(statePath, detail) {
21935
+ return new Error(`Invalid mobile state file ${statePath}: ${detail}`);
21936
+ }
21937
+ function readString(value, what, statePath) {
21938
+ if (typeof value !== "string")
21939
+ throw invalidState(statePath, `${what} must be a string`);
21940
+ return value;
21941
+ }
21942
+ function readNonEmptyString(value, what, statePath) {
21943
+ const text = readString(value, what, statePath);
21944
+ if (text === "")
21945
+ throw invalidState(statePath, `${what} must be a non-empty string`);
21946
+ return text;
21947
+ }
21948
+ function readCredentialHash(value, what, statePath) {
21949
+ const text = readString(value, what, statePath);
21950
+ if (!CREDENTIAL_HASH_PATTERN.test(text))
21951
+ throw invalidState(statePath, `${what} must be a 64-character hex string`);
21952
+ return text;
21953
+ }
21954
+ function readRevision(value, what, statePath) {
21955
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0)
21956
+ throw invalidState(statePath, `${what} must be a non-negative integer`);
21957
+ return value;
21958
+ }
21959
+ function readTimestamp(value, what, statePath) {
21960
+ if (typeof value !== "number" || !Number.isFinite(value))
21961
+ throw invalidState(statePath, `${what} must be a finite number`);
21962
+ return value;
21963
+ }
21964
+ function parseDevice(raw, index, statePath) {
21965
+ const what = `devices[${index}]`;
21966
+ if (typeof raw !== "object" || raw === null)
21967
+ throw invalidState(statePath, `${what} must be an object`);
21968
+ const record = raw;
21969
+ return {
21970
+ deviceId: readNonEmptyString(record.deviceId, `${what}.deviceId`, statePath),
21971
+ name: readString(record.name, `${what}.name`, statePath),
21972
+ credentialHash: readCredentialHash(record.credentialHash, `${what}.credentialHash`, statePath),
21973
+ grantRevision: readRevision(record.grantRevision, `${what}.grantRevision`, statePath),
21974
+ pairedAt: readTimestamp(record.pairedAt, `${what}.pairedAt`, statePath),
21975
+ lastSeenAt: readTimestamp(record.lastSeenAt, `${what}.lastSeenAt`, statePath)
21976
+ };
21977
+ }
21978
+ function parseReceipt(raw, index, statePath) {
21979
+ const what = `receipts[${index}]`;
21980
+ if (typeof raw !== "object" || raw === null)
21981
+ throw invalidState(statePath, `${what} must be an object`);
21982
+ const record = raw;
21983
+ return {
21984
+ pairRequestId: readNonEmptyString(record.pairRequestId, `${what}.pairRequestId`, statePath),
21985
+ deviceId: readNonEmptyString(record.deviceId, `${what}.deviceId`, statePath),
21986
+ credentialHash: readCredentialHash(record.credentialHash, `${what}.credentialHash`, statePath),
21987
+ grantRevision: readRevision(record.grantRevision, `${what}.grantRevision`, statePath),
21988
+ expiresAt: readTimestamp(record.expiresAt, `${what}.expiresAt`, statePath)
21989
+ };
21990
+ }
21991
+ function parseState(raw, statePath) {
21992
+ if (typeof raw !== "object" || raw === null)
21993
+ throw invalidState(statePath, "root must be an object");
21994
+ const record = raw;
21995
+ if (record.formatVersion !== MOBILE_STATE_FORMAT_VERSION)
21996
+ throw invalidState(statePath, `unsupported formatVersion (expected ${MOBILE_STATE_FORMAT_VERSION})`);
21997
+ if (typeof record.enabled !== "boolean")
21998
+ throw invalidState(statePath, "enabled must be a boolean");
21999
+ if (record.address !== null && typeof record.address !== "string")
22000
+ throw invalidState(statePath, "address must be a string or null");
22001
+ const port = record.port;
22002
+ if (typeof port !== "number" || !Number.isInteger(port) || port < 1 || port > 65535)
22003
+ throw invalidState(statePath, "port must be an integer between 1 and 65535");
22004
+ if (!Array.isArray(record.devices))
22005
+ throw invalidState(statePath, "devices must be an array");
22006
+ if (!Array.isArray(record.receipts))
22007
+ throw invalidState(statePath, "receipts must be an array");
22008
+ const lastConnectedAddress = record.lastConnectedAddress === undefined || record.lastConnectedAddress === null ? null : readString(record.lastConnectedAddress, "lastConnectedAddress", statePath);
22009
+ return {
22010
+ formatVersion: MOBILE_STATE_FORMAT_VERSION,
22011
+ enabled: record.enabled,
22012
+ address: record.address,
22013
+ port,
22014
+ devices: record.devices.map((device, index) => parseDevice(device, index, statePath)),
22015
+ receipts: record.receipts.map((receipt, index) => parseReceipt(receipt, index, statePath)),
22016
+ lastConnectedAddress
22017
+ };
22018
+ }
22019
+ function assertPort(port) {
22020
+ if (!Number.isInteger(port) || port < 1 || port > 65535)
22021
+ throw new Error("port must be an integer between 1 and 65535");
22022
+ return port;
22023
+ }
22024
+ var tempCounter = 0;
22025
+ async function writeFileAtomic(filePath, contents, mode) {
22026
+ const tempPath = `${filePath}.tmp-${process.pid}-${++tempCounter}`;
22027
+ try {
22028
+ await writeFile(tempPath, contents, { mode });
22029
+ if (POSIX)
22030
+ await chmod2(tempPath, mode);
22031
+ await rename2(tempPath, filePath);
22032
+ } finally {
22033
+ await rm(tempPath, { force: true });
22034
+ }
22035
+ }
22036
+ async function writeSecretFile(filePath, contents) {
22037
+ await writeFileAtomic(filePath, contents, 384);
22038
+ }
22039
+ async function readSecretFile(filePath) {
22040
+ try {
22041
+ return await readFile4(filePath, "utf8");
22042
+ } catch (error) {
22043
+ if (isNotFound(error))
22044
+ return;
22045
+ throw error;
21565
22046
  }
21566
- });
21567
- var NekoChat = contract2("neko.chat", {
21568
- version: 1,
21569
- input: chatInput,
21570
- variants: {
21571
- ready: {
21572
- state: { view: ChatViewSchema },
21573
- methods: {
21574
- submit: rpc(Type6.Object({
21575
- content: str,
21576
- images: Type6.Optional(Type6.Array(str, { maxItems: 4 })),
21577
- requestId: Type6.Optional(str),
21578
- whenBusy: Type6.Optional(Type6.Enum(["steer", "followUp"]))
21579
- }, { additionalProperties: false }), nil),
21580
- abort: rpc(nil, nil),
21581
- usage: rpc(nil, usage),
21582
- skills: rpc(nil, Type6.Array(Type6.Object({ name: str, description: str, location: str, level }))),
21583
- jobOutput: rpc(Type6.Object({ jobId: str, since: Type6.Optional(integer) }, { additionalProperties: false }), JobOutputSchema),
21584
- jobStop: rpc(Type6.Object({ jobId: str }, { additionalProperties: false }), JobSummarySchema),
21585
- configure: rpc(change, nil),
21586
- compact: rpc(Type6.Object({ instructions: Type6.Optional(str) }, { additionalProperties: false }), nil),
21587
- importImage: rpc(str, Type6.Object({ id: str, name: str })),
21588
- draftImage: rpc(Type6.Object({ id: str, offset: integer }, { additionalProperties: false }), imageChunk),
21589
- beginImage: rpc(Type6.Object({ name: str, mimeType: str, size: Type6.Integer({ minimum: 1, maximum: 20971520 }) }, { additionalProperties: false }), str),
21590
- appendImage: rpc(Type6.Object({ id: str, offset: integer, data: Type6.String({ maxLength: 350000 }) }, { additionalProperties: false }), nil),
21591
- finishImage: rpc(str, nil),
21592
- releaseImage: rpc(str, nil),
21593
- getImage: rpc(Type6.Object({ entryId: integer, contentIndex: integer, offset: integer }, { additionalProperties: false }), imageChunk)
21594
- }
22047
+ }
22048
+ async function loadState(statePath) {
22049
+ let raw;
22050
+ try {
22051
+ raw = await readFile4(statePath, "utf8");
22052
+ } catch (error) {
22053
+ if (isNotFound(error))
22054
+ return freezeState(defaultState());
22055
+ throw error;
22056
+ }
22057
+ let parsed;
22058
+ try {
22059
+ parsed = JSON.parse(raw);
22060
+ } catch {
22061
+ throw invalidState(statePath, "not valid JSON");
22062
+ }
22063
+ return freezeState(parseState(parsed, statePath));
22064
+ }
22065
+
22066
+ class MobileStore {
22067
+ mobileDir;
22068
+ tlsDir;
22069
+ statePath;
22070
+ adminTokenPath;
22071
+ current;
22072
+ queue = Promise.resolve();
22073
+ constructor(mobileDir, initial) {
22074
+ this.mobileDir = mobileDir;
22075
+ this.tlsDir = join6(mobileDir, "tls");
22076
+ this.statePath = join6(mobileDir, "state.json");
22077
+ this.adminTokenPath = join6(mobileDir, "admin-token");
22078
+ this.current = initial;
22079
+ }
22080
+ static async open(nekoDir) {
22081
+ const mobileDir = join6(nekoDir, "mobile");
22082
+ const tlsDir = join6(mobileDir, "tls");
22083
+ await mkdir2(mobileDir, { recursive: true, mode: 448 });
22084
+ await mkdir2(tlsDir, { recursive: true, mode: 448 });
22085
+ if (POSIX) {
22086
+ await chmod2(mobileDir, 448);
22087
+ await chmod2(tlsDir, 448);
21595
22088
  }
22089
+ return new MobileStore(mobileDir, await loadState(join6(mobileDir, "state.json")));
21596
22090
  }
21597
- });
21598
- var browse = Type6.Object({ path: str, query: Type6.Optional(str), showHidden: Type6.Optional(bool), cursor: Type6.Optional(str) }, { additionalProperties: false });
21599
- var page = Type6.Object({
21600
- path: str,
21601
- parent: nullableString,
21602
- entries: Type6.Array(Type6.Object({ name: str, path: str, error: Type6.Optional(str) })),
21603
- next: nullableString
21604
- });
21605
- var NekoWorkspaces = contract2("neko.workspaces", {
21606
- version: 1,
21607
- input: Type6.Object({}, { additionalProperties: false }),
21608
- variants: {
21609
- ready: {
21610
- state: { state: WorkspaceStateSchema },
21611
- methods: {
21612
- add: rpc(Type6.Object({ path: str }, { additionalProperties: false }), WorkspaceSummarySchema),
21613
- browse: rpc(browse, page)
22091
+ getState() {
22092
+ return this.current;
22093
+ }
22094
+ updateConfig(patch) {
22095
+ return this.mutate((state) => ({
22096
+ ...state,
22097
+ ...patch.enabled !== undefined ? { enabled: patch.enabled } : {},
22098
+ ...patch.address !== undefined ? { address: patch.address } : {},
22099
+ ...patch.port !== undefined ? { port: assertPort(patch.port) } : {}
22100
+ }));
22101
+ }
22102
+ upsertDevice(device) {
22103
+ return this.mutate((state) => ({
22104
+ ...state,
22105
+ devices: state.devices.some((d) => d.deviceId === device.deviceId) ? state.devices.map((d) => d.deviceId === device.deviceId ? device : d) : [...state.devices, device]
22106
+ }));
22107
+ }
22108
+ removeDevice(deviceId) {
22109
+ return this.mutate((state) => ({
22110
+ ...state,
22111
+ devices: state.devices.filter((d) => d.deviceId !== deviceId)
22112
+ }));
22113
+ }
22114
+ completePairing(input) {
22115
+ return this.mutate((state) => {
22116
+ const now = Date.now();
22117
+ const existing = state.devices.find((d) => d.deviceId === input.deviceId);
22118
+ const grantRevision = existing === undefined ? 1 : existing.grantRevision + 1;
22119
+ const device = {
22120
+ deviceId: input.deviceId,
22121
+ name: input.name,
22122
+ credentialHash: input.credentialHash,
22123
+ grantRevision,
22124
+ pairedAt: input.pairedAt,
22125
+ lastSeenAt: input.pairedAt
22126
+ };
22127
+ const receipt = {
22128
+ pairRequestId: input.pairRequestId,
22129
+ deviceId: input.deviceId,
22130
+ credentialHash: input.credentialHash,
22131
+ grantRevision,
22132
+ expiresAt: input.receiptExpiresAt
22133
+ };
22134
+ return {
22135
+ ...state,
22136
+ devices: existing === undefined ? [...state.devices, device] : state.devices.map((d) => d.deviceId === input.deviceId ? device : d),
22137
+ receipts: [
22138
+ ...state.receipts.filter((r) => r.expiresAt > now && r.pairRequestId !== input.pairRequestId),
22139
+ receipt
22140
+ ]
22141
+ };
22142
+ });
22143
+ }
22144
+ putReceipt(receipt) {
22145
+ return this.mutate((state) => ({
22146
+ ...state,
22147
+ receipts: state.receipts.some((r) => r.pairRequestId === receipt.pairRequestId) ? state.receipts.map((r) => r.pairRequestId === receipt.pairRequestId ? receipt : r) : [...state.receipts, receipt]
22148
+ }));
22149
+ }
22150
+ removeReceipt(pairRequestId) {
22151
+ return this.mutate((state) => ({
22152
+ ...state,
22153
+ receipts: state.receipts.filter((r) => r.pairRequestId !== pairRequestId)
22154
+ }));
22155
+ }
22156
+ touchDevice(deviceId, lastSeenAt) {
22157
+ return this.mutate((state) => {
22158
+ const existing = state.devices.find((d) => d.deviceId === deviceId);
22159
+ if (existing === undefined)
22160
+ return state;
22161
+ return {
22162
+ ...state,
22163
+ devices: state.devices.map((d) => d.deviceId === deviceId ? { ...d, lastSeenAt } : d)
22164
+ };
22165
+ });
22166
+ }
22167
+ rememberConnectedAddress(address) {
22168
+ if (this.current.lastConnectedAddress === address)
22169
+ return Promise.resolve(this.current);
22170
+ return this.mutate((state) => ({ ...state, lastConnectedAddress: address }));
22171
+ }
22172
+ clearMobileAuthorizations() {
22173
+ return this.mutate((state) => ({ ...state, devices: [], receipts: [] }));
22174
+ }
22175
+ ensureAdminToken() {
22176
+ return this.enqueue(async () => {
22177
+ const existing = await readSecretFile(this.adminTokenPath);
22178
+ if (existing !== undefined) {
22179
+ const token = existing.trim();
22180
+ if (ADMIN_TOKEN_PATTERN.test(token))
22181
+ return token;
22182
+ throw new Error(`Invalid mobile admin token file ${this.adminTokenPath}: expected a base64url token`);
21614
22183
  }
21615
- }
22184
+ const token = randomBytes(MOBILE_ADMIN_TOKEN_BYTES).toString("base64url");
22185
+ await writeSecretFile(this.adminTokenPath, `${token}
22186
+ `);
22187
+ return token;
22188
+ });
22189
+ }
22190
+ mutate(recipe) {
22191
+ return this.enqueue(async () => {
22192
+ const next = freezeState(recipe(this.current));
22193
+ await writeFileAtomic(this.statePath, `${JSON.stringify(next, null, "\t")}
22194
+ `, 384);
22195
+ this.current = next;
22196
+ return next;
22197
+ });
22198
+ }
22199
+ enqueue(task) {
22200
+ const run = this.queue.then(task);
22201
+ this.queue = run.then(() => {
22202
+ return;
22203
+ }, () => {
22204
+ return;
22205
+ });
22206
+ return run;
21616
22207
  }
22208
+ }
22209
+
22210
+ // packages/gateway/src/mobile/admin.ts
22211
+ var listenerStateSchema = Type6.Union([
22212
+ Type6.Object({ status: Type6.Literal("disabled") }),
22213
+ Type6.Object({ status: Type6.Literal("starting") }),
22214
+ Type6.Object({
22215
+ status: Type6.Literal("listening"),
22216
+ address: Type6.String(),
22217
+ port: Type6.Integer({ minimum: 1, maximum: 65535 }),
22218
+ bindAddress: Type6.Optional(Type6.String()),
22219
+ mode: Type6.Optional(Type6.Union([Type6.Literal("direct"), Type6.Literal("wsl-relay")]))
22220
+ }),
22221
+ Type6.Object({ status: Type6.Literal("error"), error: Type6.String() }),
22222
+ Type6.Object({ status: Type6.Literal("stopping") })
22223
+ ]);
22224
+ var adminDeviceSchema = Type6.Object({
22225
+ deviceId: Type6.String(),
22226
+ name: Type6.String(),
22227
+ pairedAt: Type6.Integer(),
22228
+ lastSeenAt: Type6.Integer(),
22229
+ online: Type6.Boolean()
21617
22230
  });
21618
- var provider = Type6.Enum(["deepseek", "tavily"]);
21619
- var NekoCredentials = contract2("neko.credentials", {
22231
+ var NekoMobileAdmin = contract("neko.mobile-admin", {
21620
22232
  version: 1,
21621
- input: Type6.Object({}, { additionalProperties: false }),
22233
+ input: Type6.Object({ token: Type6.String() }),
21622
22234
  variants: {
21623
- ready: {
21624
- state: { state: CredentialStateSchema },
22235
+ admin: {
21625
22236
  methods: {
21626
- save: rpc(Type6.Object({ provider, key: Type6.String({ minLength: 1, maxLength: 8192 }) }, { additionalProperties: false }), nil),
21627
- remove: rpc(Type6.Object({ provider }, { additionalProperties: false }), nil)
22237
+ state: rpc(Type6.Null(), Type6.Object({
22238
+ state: listenerStateSchema,
22239
+ candidates: Type6.Array(Type6.String()),
22240
+ port: Type6.Integer({ minimum: 1, maximum: 65535 }),
22241
+ certificate: Type6.Union([
22242
+ Type6.Object({ fingerprint: Type6.String(), expiresAt: Type6.Integer() }),
22243
+ Type6.Null()
22244
+ ]),
22245
+ devices: Type6.Array(adminDeviceSchema),
22246
+ lastConnectedAddress: Type6.Union([Type6.String(), Type6.Null()]),
22247
+ wsl: Type6.Optional(Type6.Union([
22248
+ Type6.Object({ interop: Type6.Boolean(), error: Type6.Union([Type6.String(), Type6.Null()]) }),
22249
+ Type6.Null()
22250
+ ]))
22251
+ })),
22252
+ enable: rpc(Type6.Object({
22253
+ address: Type6.String(),
22254
+ port: Type6.Optional(Type6.Integer({ minimum: 1, maximum: 65535 }))
22255
+ }), Type6.Object({
22256
+ status: Type6.Literal("listening"),
22257
+ address: Type6.String(),
22258
+ port: Type6.Integer({ minimum: 1, maximum: 65535 }),
22259
+ bindAddress: Type6.Optional(Type6.String()),
22260
+ mode: Type6.Optional(Type6.Union([Type6.Literal("direct"), Type6.Literal("wsl-relay")]))
22261
+ })),
22262
+ disable: rpc(Type6.Null(), listenerStateSchema),
22263
+ createPairing: rpc(Type6.Null(), Type6.Object({ pairingText: Type6.String(), expiresAt: Type6.Integer() })),
22264
+ cancelPairing: rpc(Type6.Null(), Type6.Null()),
22265
+ revoke: rpc(Type6.Object({ deviceId: Type6.String({ minLength: 1 }) }), Type6.Object({ revoked: Type6.Boolean() })),
22266
+ regenerateCertificate: rpc(Type6.Null(), Type6.Object({ fingerprint: Type6.String(), expiresAt: Type6.Integer() }))
21628
22267
  }
21629
22268
  }
21630
22269
  }
21631
22270
  });
21632
- var FileCandidatesSchema = Type6.Object({
21633
- entries: Type6.Array(Type6.Object({ name: str, path: str, kind: Type6.Enum(["file", "directory"]) }), { maxItems: 100 }),
21634
- limited: bool,
21635
- incomplete: bool
21636
- });
21637
- var NekoWorkspaceFiles = contract2("neko.workspace-files", {
21638
- version: 1,
21639
- input: Type6.Object({}, { additionalProperties: false }),
21640
- variants: {
21641
- ready: {
21642
- methods: {
21643
- search: rpc(Type6.Object({ workspaceRoot: str, query: str }, { additionalProperties: false }), FileCandidatesSchema)
21644
- }
21645
- }
22271
+ function digest(value) {
22272
+ return createHash("sha256").update(value, "utf8").digest();
22273
+ }
22274
+ async function tokenMatches(token, store) {
22275
+ if (store === undefined)
22276
+ return false;
22277
+ let adminToken;
22278
+ try {
22279
+ adminToken = await store.ensureAdminToken();
22280
+ } catch {
22281
+ return false;
21646
22282
  }
21647
- });
21648
- var marketSkill = Type6.Object({
21649
- name: str,
21650
- description: str,
21651
- category: str,
21652
- source: str,
21653
- license: str,
21654
- version: str,
21655
- sha256: str,
21656
- sizeBytes: integer,
21657
- uploadedAt: str,
21658
- installed: Type6.Object({ project: bool, user: bool, builtin: bool }),
21659
- activeLevel: Type6.Union([level, nil])
21660
- });
21661
- var preview = Type6.Object({ body: str, files: Type6.Array(str) });
21662
- var scope = Type6.Enum(["user", "project"]);
21663
- var installResult = Type6.Object({ name: str, scope, path: str, shadowedBy: Type6.Optional(scope) });
21664
- var NekoSkillMarket = contract2("neko.skill-market", {
21665
- version: 1,
21666
- input: Type6.Object({}, { additionalProperties: false }),
21667
- variants: {
21668
- ready: {
21669
- methods: {
21670
- list: rpc(Type6.Object({ query: Type6.Optional(str), workspaceRoot: Type6.Optional(str) }, { additionalProperties: false }), Type6.Array(marketSkill)),
21671
- detail: rpc(Type6.Object({ name: str, workspaceRoot: Type6.Optional(str) }, { additionalProperties: false }), marketSkill),
21672
- preview: rpc(Type6.Object({ name: str }, { additionalProperties: false }), preview),
21673
- install: rpc(Type6.Object({ name: str, scope, workspaceRoot: Type6.Optional(str), overwrite: Type6.Optional(bool) }, { additionalProperties: false }), installResult),
21674
- uninstall: rpc(Type6.Object({ name: str, scope, workspaceRoot: Type6.Optional(str) }, { additionalProperties: false }), nil)
22283
+ const expected = digest(adminToken);
22284
+ const provided = digest(token);
22285
+ return expected.length === provided.length && timingSafeEqual(expected, provided);
22286
+ }
22287
+ function createMobileAdminFactory(deps) {
22288
+ return (input, principal) => {
22289
+ if (isMobilePrincipal(principal))
22290
+ throw new ChordError("forbidden", "mobile principals cannot administer pairing");
22291
+ const guard = async () => {
22292
+ if (!await tokenMatches(input.token, deps.store))
22293
+ throw new ChordError("forbidden", "forbidden");
22294
+ };
22295
+ return {
22296
+ variant: "admin",
22297
+ async state() {
22298
+ await guard();
22299
+ return deps.mobile.adminState();
22300
+ },
22301
+ async enable(params) {
22302
+ await guard();
22303
+ const state = await deps.mobile.enable({ address: params.address, port: params.port });
22304
+ if (state.status !== "listening")
22305
+ throw new ChordError("unavailable", `mobile listener failed: ${state.status}`);
22306
+ return state;
22307
+ },
22308
+ async disable() {
22309
+ await guard();
22310
+ return deps.mobile.disable();
22311
+ },
22312
+ async createPairing() {
22313
+ await guard();
22314
+ return deps.mobile.createPairing();
22315
+ },
22316
+ async cancelPairing() {
22317
+ await guard();
22318
+ deps.mobile.cancelPairing();
22319
+ return null;
22320
+ },
22321
+ async revoke(params) {
22322
+ await guard();
22323
+ return { revoked: await deps.mobile.revoke(params.deviceId) };
22324
+ },
22325
+ async regenerateCertificate() {
22326
+ await guard();
22327
+ return deps.mobile.regenerateCertificate();
21675
22328
  }
21676
- }
21677
- }
21678
- });
21679
- var ApplicationInfoSchema = Type6.Object({
21680
- runningVersion: str,
21681
- installedVersion: nullableString,
21682
- protocolVersion: Type6.Integer({ minimum: 1 }),
21683
- latestVersion: nullableString,
21684
- checkedAt: Type6.Union([integer, nil]),
21685
- checkState: Type6.Enum(["unknown", "checking", "ok", "failed", "development"])
21686
- });
21687
- var NekoApplicationInfo = contract2("neko.application-info", {
21688
- version: 1,
21689
- input: Type6.Object({}),
21690
- variants: { ready: { state: { info: ApplicationInfoSchema }, methods: { check: rpc(nil, ApplicationInfoSchema) } } }
21691
- });
22329
+ };
22330
+ };
22331
+ }
22332
+
22333
+ // packages/gateway/src/mobile/listener.ts
22334
+ import { randomUUID as randomUUID2 } from "crypto";
22335
+ import { createServer } from "https";
22336
+ import { hostname } from "os";
22337
+ import { memoryLinkPair } from "@earendil-works/chord";
22338
+ import { WebSocketServer } from "ws";
21692
22339
 
21693
22340
  // packages/gateway/src/link.ts
21694
- import { webSocketLink } from "@earendil-works/chord";
22341
+ import {
22342
+ createHost,
22343
+ webSocketLink
22344
+ } from "@earendil-works/chord";
21695
22345
  var MAX_LINK_BYTES = 16 * 1024 * 1024;
21696
- var size = (text) => new TextEncoder().encode(text).byteLength;
21697
- function boundedWebSocketLink(socket, max = MAX_LINK_BYTES) {
22346
+ var asBytes = (data) => data instanceof Uint8Array ? data : new Uint8Array(data);
22347
+ var size = (data) => typeof data === "string" ? new TextEncoder().encode(data).byteLength : data.byteLength;
22348
+ function boundedWebSocketLink(socket, max = MAX_LINK_BYTES, options) {
21698
22349
  let queued = 0, incoming = 0, receiving = false;
21699
22350
  let native;
21700
22351
  const bridge = {
@@ -21710,18 +22361,22 @@ function boundedWebSocketLink(socket, max = MAX_LINK_BYTES) {
21710
22361
  },
21711
22362
  close: (code, reason) => socket.close(code, reason),
21712
22363
  addEventListener(type, handler) {
21713
- if (type === "message")
21714
- socket.addEventListener("message", (event) => {
21715
- const bytes = size(String(event.data));
22364
+ if (type === "message") {
22365
+ const deliver = (data) => {
22366
+ const bytes = size(data);
21716
22367
  if (!receiving)
21717
22368
  incoming += bytes;
21718
22369
  if (bytes > max || incoming > max) {
21719
22370
  native.close("frame limit exceeded");
21720
22371
  return;
21721
22372
  }
21722
- handler(event);
21723
- });
21724
- else
22373
+ handler({ data });
22374
+ };
22375
+ if (socket.on)
22376
+ socket.on("message", (data, isBinary) => deliver(isBinary ? asBytes(data) : new TextDecoder().decode(asBytes(data))));
22377
+ else
22378
+ socket.addEventListener("message", (event) => deliver(typeof event.data === "string" || event.data instanceof Uint8Array ? event.data : String(event.data)));
22379
+ } else
21725
22380
  socket.addEventListener(type, () => {
21726
22381
  if (type === "open")
21727
22382
  queued = 0;
@@ -21733,7 +22388,7 @@ function boundedWebSocketLink(socket, max = MAX_LINK_BYTES) {
21733
22388
  });
21734
22389
  }
21735
22390
  };
21736
- native = webSocketLink(bridge);
22391
+ native = webSocketLink(bridge, options);
21737
22392
  socket.addEventListener("error", () => native.close("socket error"));
21738
22393
  return {
21739
22394
  get closed() {
@@ -21753,7 +22408,7 @@ function boundedWebSocketLink(socket, max = MAX_LINK_BYTES) {
21753
22408
  }
21754
22409
  if (socket.readyState === 0)
21755
22410
  queued += bytes;
21756
- native.send(frame);
22411
+ return native.send(frame);
21757
22412
  },
21758
22413
  receive(handler) {
21759
22414
  receiving = true;
@@ -21764,47 +22419,17 @@ function boundedWebSocketLink(socket, max = MAX_LINK_BYTES) {
21764
22419
  close: (reason) => native.close(reason)
21765
22420
  };
21766
22421
  }
21767
- function identifiedLink(link, expectedName) {
21768
- let receiver;
21769
- let seen = false, pendingBytes = 0;
21770
- const pending = [];
21771
- link.receive((frame) => {
21772
- if (!seen) {
21773
- if (frame.t !== "hello" || frame.name !== expectedName) {
21774
- link.close("serverId mismatch");
21775
- return;
21776
- }
21777
- seen = true;
21778
- }
21779
- if (receiver)
21780
- receiver(frame);
21781
- else {
21782
- pendingBytes += size(JSON.stringify(frame));
21783
- if (pendingBytes > MAX_LINK_BYTES) {
21784
- link.close("receive queue limit exceeded");
21785
- pending.length = 0;
21786
- return;
21787
- }
21788
- pending.push(frame);
21789
- }
21790
- });
21791
- return {
21792
- get closed() {
21793
- return link.closed;
21794
- },
21795
- send: (frame) => link.send(frame),
21796
- receive(handler) {
21797
- receiver = handler;
21798
- pendingBytes = 0;
21799
- for (const frame of pending.splice(0)) {
21800
- if (link.closed)
21801
- break;
21802
- handler(frame);
21803
- }
22422
+ function createClientHost(link, options = {}) {
22423
+ return createHost({
22424
+ kind: "client",
22425
+ self: { kind: "client" },
22426
+ upstream: {
22427
+ link,
22428
+ principal: { kind: "server" },
22429
+ ...options.onProtocolMismatch ? { onProtocolMismatch: options.onProtocolMismatch } : {}
21804
22430
  },
21805
- onClose: (handler) => link.onClose(handler),
21806
- close: (reason) => link.close(reason)
21807
- };
22431
+ onError: () => {}
22432
+ });
21808
22433
  }
21809
22434
 
21810
22435
  // packages/gateway/src/mobile/adapter.ts
@@ -21817,16 +22442,6 @@ var byMessage = (a, b) => messageTime(b) - messageTime(a) || Number(b.id) - Numb
21817
22442
  // packages/gateway/src/mobile/adapter.ts
21818
22443
  import { Check as Check2 } from "typebox/value";
21819
22444
 
21820
- // packages/gateway/src/client-session.ts
21821
- import { createHost } from "@earendil-works/chord";
21822
-
21823
- // packages/core/src/application-version.ts
21824
- var RUNNING_VERSION = "0.4.2";
21825
- var APPLICATION_PROTOCOL = 4;
21826
- function stableVersion(value) {
21827
- 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)));
21828
- }
21829
-
21830
22445
  // packages/gateway/src/subagent-client.ts
21831
22446
  function subagentClient(connect) {
21832
22447
  let value;
@@ -21967,40 +22582,16 @@ function subagentClient(connect) {
21967
22582
  };
21968
22583
  }
21969
22584
 
21970
- // packages/gateway/src/client-session.ts
21971
- function waitForValue(state, signal) {
21972
- return new Promise((resolve, reject) => {
21973
- let stop = () => {};
21974
- let settled = false;
21975
- const finish = (error, value) => {
21976
- if (settled)
21977
- return;
21978
- settled = true;
21979
- clearTimeout(timer);
21980
- stop();
21981
- signal?.removeEventListener("abort", abort);
21982
- if (error)
21983
- reject(error);
21984
- else
21985
- resolve(value);
21986
- };
21987
- const abort = () => finish(signal?.reason ?? new Error("Cancelled"));
21988
- const timer = setTimeout(() => finish(new Error("Timed out waiting for first snapshot")), 5000);
21989
- stop = state.subscribe((value) => {
21990
- if (value !== undefined)
21991
- finish(undefined, value);
21992
- });
21993
- if (settled)
21994
- stop();
21995
- signal?.addEventListener("abort", abort, { once: true });
21996
- if (signal?.aborted)
21997
- abort();
21998
- });
21999
- }
22000
-
22001
22585
  // packages/gateway/src/mobile/projection.ts
22002
22586
  import { createHash as createHash2 } from "crypto";
22003
22587
 
22588
+ // packages/core/src/application-version.ts
22589
+ var RUNNING_VERSION = "0.4.3";
22590
+ var APPLICATION_PROTOCOL = 6;
22591
+ function stableVersion(value) {
22592
+ 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)));
22593
+ }
22594
+
22004
22595
  // packages/gateway/src/mobile/protocol.ts
22005
22596
  import { Type as Type7 } from "typebox";
22006
22597
  import { Check } from "typebox/value";
@@ -22072,7 +22663,7 @@ function buildHelloResult(serverId, serverName, apiVersion) {
22072
22663
  var opt = Type7.Optional;
22073
22664
  var str2 = Type7.String();
22074
22665
  var int = Type7.Integer({ minimum: 0 });
22075
- var nullable = (schema) => Type7.Union([schema, Type7.Null()]);
22666
+ var nullable2 = (schema) => Type7.Union([schema, Type7.Null()]);
22076
22667
  var MobileContentRefSchema = Type7.Object({
22077
22668
  sessionId: str2,
22078
22669
  agentId: opt(str2),
@@ -22161,7 +22752,7 @@ var MobileSubagentRoundSchema = Type7.Object({
22161
22752
  approvalCount: int,
22162
22753
  readonly: Type7.Boolean(),
22163
22754
  createdAt: Type7.Number(),
22164
- endedAt: nullable(Type7.Number()),
22755
+ endedAt: nullable2(Type7.Number()),
22165
22756
  usage: opt(MobileUsageSchema)
22166
22757
  });
22167
22758
  var MobileAgentSchema = Type7.Object({
@@ -22169,18 +22760,18 @@ var MobileAgentSchema = Type7.Object({
22169
22760
  role: str2,
22170
22761
  title: str2,
22171
22762
  status: Type7.Union([Type7.Literal("idle"), Type7.Literal("queued"), Type7.Literal("running")]),
22172
- activeRoundId: nullable(int),
22763
+ activeRoundId: nullable2(int),
22173
22764
  lastRoundId: int,
22174
22765
  roundCount: int,
22175
22766
  approvalCount: int
22176
22767
  });
22177
22768
  var MobileChatStatusSchema = Type7.Object({
22178
22769
  busy: Type7.Boolean(),
22179
- status: nullable(str2),
22770
+ status: nullable2(str2),
22180
22771
  usage: MobileUsageSchema,
22181
22772
  taskUsage: opt(MobileUsageSchema),
22182
- contextUsage: opt(Type7.Object({ tokens: nullable(Type7.Number()), window: Type7.Number(), percent: nullable(Type7.Number()) })),
22183
- agent: Type7.Object({ model: nullable(str2), thinkingLevel: nullable(str2) }),
22773
+ contextUsage: opt(Type7.Object({ tokens: nullable2(Type7.Number()), window: Type7.Number(), percent: nullable2(Type7.Number()) })),
22774
+ agent: Type7.Object({ model: nullable2(str2), thinkingLevel: nullable2(str2) }),
22184
22775
  subagents: Type7.Array(MobileSubagentRoundSchema),
22185
22776
  agents: Type7.Array(MobileAgentSchema),
22186
22777
  approvalCount: int,
@@ -22190,8 +22781,8 @@ var MobileChatStatusSchema = Type7.Object({
22190
22781
  var MobileChatSnapshotSchema = Type7.Object({
22191
22782
  ...MobileChatStatusSchema.properties,
22192
22783
  items: Type7.Array(MobileChatItemSchema),
22193
- historyBefore: nullable(str2),
22194
- streaming: nullable(MobileStreamingSchema)
22784
+ historyBefore: nullable2(str2),
22785
+ streaming: nullable2(MobileStreamingSchema)
22195
22786
  });
22196
22787
  var MobileWorkspaceSchema = Type7.Object({ root: str2, name: str2 });
22197
22788
  var MobileSessionSummarySchema = Type7.Object({
@@ -22224,7 +22815,7 @@ var sessionListResult = Type7.Object({
22224
22815
  nextCursor: opt(str2)
22225
22816
  });
22226
22817
  var subscription = { subscriptionId: Type7.String({ minLength: 1 }) };
22227
- var pageResult = Type7.Object({ items: Type7.Array(MobileChatItemSchema), historyBefore: nullable(str2) });
22818
+ var pageResult = Type7.Object({ items: Type7.Array(MobileChatItemSchema), historyBefore: nullable2(str2) });
22228
22819
  var pageParams = Type7.Object({
22229
22820
  ...subscription,
22230
22821
  before: Type7.String({ minLength: 1 }),
@@ -22338,7 +22929,7 @@ var patchBody = {
22338
22929
  seq: Type7.Integer({ minimum: 1 }),
22339
22930
  upserts: Type7.Array(MobileChatItemSchema),
22340
22931
  status: opt(MobileChatStatusSchema),
22341
- streaming: opt(nullable(MobileStreamingSchema)),
22932
+ streaming: opt(nullable2(MobileStreamingSchema)),
22342
22933
  streamingAppend: opt(Type7.Object({ text: opt(str2), thinking: opt(str2) }))
22343
22934
  };
22344
22935
  var MOBILE_API_V1_NOTIFICATIONS = {
@@ -22850,7 +23441,7 @@ function chordToMobileCode(error) {
22850
23441
  return "internal";
22851
23442
  }
22852
23443
  }
22853
- async function remote3(work, invalidAs = "invalid_params") {
23444
+ async function remote2(work, invalidAs = "invalid_params") {
22854
23445
  try {
22855
23446
  return await work();
22856
23447
  } catch (error) {
@@ -23064,14 +23655,17 @@ class MobileAdapter {
23064
23655
  }
23065
23656
  }
23066
23657
  async workspacesList() {
23067
- const state = await waitForValue(this.services().workspaces.state);
23658
+ const { workspaces } = this.services();
23659
+ await workspaces.ready;
23660
+ const state = workspaces.state.value;
23068
23661
  return {
23069
23662
  workspaces: state.workspaces.map((workspace) => ({ root: workspace.path, name: baseName(workspace.path) }))
23070
23663
  };
23071
23664
  }
23072
23665
  async registeredWorkspaceRoots() {
23073
- const state = await waitForValue(this.services().workspaces.state);
23074
- return state.workspaces.map((workspace) => workspace.path);
23666
+ const { workspaces } = this.services();
23667
+ await workspaces.ready;
23668
+ return workspaces.state.value.workspaces.map((workspace) => workspace.path);
23075
23669
  }
23076
23670
  async sessionsList(params) {
23077
23671
  const roots = await this.registeredWorkspaceRoots();
@@ -23080,7 +23674,9 @@ class MobileAdapter {
23080
23674
  const offset = params.cursor === undefined ? 0 : Number.parseInt(params.cursor, 10);
23081
23675
  if (!Number.isInteger(offset) || offset < 0)
23082
23676
  throw new MobileMethodError("invalid_params", "invalid cursor");
23083
- const directory = await waitForValue(this.services().directory.state);
23677
+ const { directory: directoryRef } = this.services();
23678
+ await directoryRef.ready;
23679
+ const directory = directoryRef.state.value;
23084
23680
  let sessions = directory.sessions.filter((session) => session.workspaceRoot === params.workspace);
23085
23681
  if (params.query !== undefined) {
23086
23682
  const query = params.query.toLowerCase();
@@ -23106,7 +23702,7 @@ class MobileAdapter {
23106
23702
  if (!roots.includes(params.workspace))
23107
23703
  throw new MobileMethodError("invalid_params", `unknown workspace: ${params.workspace}`);
23108
23704
  this.assertAuthorized();
23109
- const created = await remote3(() => this.services().management.create({ cwd: params.workspace }));
23705
+ const created = await remote2(() => this.services().management.create({ cwd: params.workspace }));
23110
23706
  return {
23111
23707
  id: created.id,
23112
23708
  workspace: created.workspaceRoot,
@@ -23143,16 +23739,13 @@ class MobileAdapter {
23143
23739
  const generation = ++this.attachGeneration;
23144
23740
  this.closeSubagent();
23145
23741
  this.releaseChat();
23146
- const attachment = await remote3(() => services.management.attach(params.sessionId));
23742
+ const attachment = await remote2(() => services.management.attach(params.sessionId));
23147
23743
  if (generation !== this.attachGeneration || this.disposed)
23148
23744
  throw new MobileMethodError("gone", "superseded by a newer attach");
23149
23745
  const chat = services.openChat(attachment);
23150
23746
  const stopView = chat.view.subscribe(() => {});
23151
23747
  try {
23152
- await remote3(async () => {
23153
- await chat.ready;
23154
- await waitForValue(chat.view);
23155
- });
23748
+ await remote2(() => chat.ready);
23156
23749
  } catch (error) {
23157
23750
  stopView();
23158
23751
  chat.release();
@@ -23214,7 +23807,7 @@ class MobileAdapter {
23214
23807
  this.attachGeneration += 1;
23215
23808
  this.closeSubagent();
23216
23809
  this.releaseChat();
23217
- await remote3(() => this.services().management.detach(null)).catch(() => {
23810
+ await remote2(() => this.services().management.detach(null)).catch(() => {
23218
23811
  return;
23219
23812
  });
23220
23813
  return null;
@@ -23245,7 +23838,7 @@ class MobileAdapter {
23245
23838
  const state = this.requireChat(params);
23246
23839
  const busy = state.chat.view.value?.busy ?? false;
23247
23840
  this.assertAuthorized();
23248
- await remote3(() => state.chat.submit({
23841
+ await remote2(() => state.chat.submit({
23249
23842
  content: params.text,
23250
23843
  requestId: params.requestId,
23251
23844
  ...params.mode === "steer" ? { whenBusy: "steer" } : {}
@@ -23262,7 +23855,7 @@ class MobileAdapter {
23262
23855
  async chatAbort(params) {
23263
23856
  const state = this.requireChat(params);
23264
23857
  this.assertAuthorized();
23265
- await remote3(() => state.chat.abort(null));
23858
+ await remote2(() => state.chat.abort(null));
23266
23859
  return { accepted: true };
23267
23860
  }
23268
23861
  async approvalRecords(state) {
@@ -23341,7 +23934,7 @@ class MobileAdapter {
23341
23934
  this.assertAuthorized();
23342
23935
  const handle = openSubagent({ ...state.attachment, taskId: params.taskId });
23343
23936
  try {
23344
- const status = await remote3(() => handle.decideApproval({
23937
+ const status = await remote2(() => handle.decideApproval({
23345
23938
  id: params.id,
23346
23939
  version: params.version,
23347
23940
  decision: params.decision === "allow" ? "allowed" : "denied"
@@ -23421,7 +24014,7 @@ class MobileAdapter {
23421
24014
  return () => listeners.delete(listener);
23422
24015
  },
23423
24016
  older: async (before) => {
23424
- const page = await remote3(() => client.history({ before }));
24017
+ const page = await remote2(() => client.history({ before }));
23425
24018
  return { items: page.items, next: page.next };
23426
24019
  }
23427
24020
  };
@@ -23465,7 +24058,7 @@ class MobileAdapter {
23465
24058
  this.assertAuthorized();
23466
24059
  const handle = openSubagent({ ...chat.attachment, taskId: params.taskId });
23467
24060
  try {
23468
- await remote3(() => handle.stop({ taskId: params.taskId }));
24061
+ await remote2(() => handle.stop({ taskId: params.taskId }));
23469
24062
  } finally {
23470
24063
  handle.release();
23471
24064
  }
@@ -23479,9 +24072,9 @@ class MobileAdapter {
23479
24072
  const state = this.subagentState;
23480
24073
  if (state === undefined || state.agentId !== params.agentId || state.chatSubscriptionId !== chat.subscriptionId)
23481
24074
  throw new MobileMethodError("gone", "subagent view is no longer open");
23482
- chunk = await remote3(() => state.client.getImage(input));
24075
+ chunk = await remote2(() => state.client.getImage(input));
23483
24076
  } else {
23484
- chunk = await remote3(() => chat.chat.getImage(input));
24077
+ chunk = await remote2(() => chat.chat.getImage(input));
23485
24078
  }
23486
24079
  const bytes = Buffer.from(chunk.data, "base64").subarray(0, MOBILE_LIMITS.chunkBytes);
23487
24080
  const nextOffset = params.offset + bytes.length;
@@ -24412,9 +25005,6 @@ function describeRelayError(error, address, port, bindAddress) {
24412
25005
  return `Windows \u4E2D\u7EE7\u542F\u52A8\u5931\u8D25\uFF1A${error.message}`;
24413
25006
  }
24414
25007
  }
24415
- function isMobilePrincipal2(principal) {
24416
- return typeof principal === "object" && principal !== null && principal.kind === "mobile";
24417
- }
24418
25008
  function toMessage(error) {
24419
25009
  if (error instanceof Error) {
24420
25010
  const code = error.code;
@@ -24593,7 +25183,7 @@ class MobileGateway {
24593
25183
  await store.clearMobileAuthorizations();
24594
25184
  this.certificate = certificate;
24595
25185
  this.pairingValue?.invalidate();
24596
- await this.chord.invalidatePrincipal(isMobilePrincipal2);
25186
+ await this.chord.invalidatePrincipal(isMobilePrincipal);
24597
25187
  for (const connection of [...this.connections.values()])
24598
25188
  connection.close(MOBILE_CLOSE_REVOKED, "revoked");
24599
25189
  const current = this.stateValue;
@@ -24669,11 +25259,7 @@ class MobileGateway {
24669
25259
  store.rememberConnectedAddress(learned).catch((error) => this.onError?.(toError(error)));
24670
25260
  }
24671
25261
  }
24672
- const clientHost = createHost2({
24673
- kind: "client",
24674
- upstream: identifiedLink(clientEnd, this.serverId),
24675
- onError: () => {}
24676
- });
25262
+ const clientHost = createClientHost(clientEnd);
24677
25263
  let servicesValue;
24678
25264
  const adapter = new MobileAdapter({
24679
25265
  socket,
@@ -24685,11 +25271,11 @@ class MobileGateway {
24685
25271
  services: () => {
24686
25272
  if (servicesValue === undefined) {
24687
25273
  servicesValue = {
24688
- directory: clientHost.get(NekoSessionDirectory, {}, { at: this.serverId }),
24689
- management: clientHost.get(NekoSessionManagement, {}, { at: this.serverId }),
24690
- workspaces: clientHost.get(NekoWorkspaces, {}, { at: this.serverId }),
24691
- openChat: (attachment) => clientHost.get(NekoChat, attachment, { at: this.serverId }),
24692
- openSubagent: (input) => clientHost.get(NekoSubagent, input, { at: this.serverId })
25274
+ directory: clientHost.hold(NekoSessionDirectory, {}),
25275
+ management: clientHost.hold(NekoSessionManagement, {}),
25276
+ workspaces: clientHost.hold(NekoWorkspaces, {}),
25277
+ openChat: (attachment) => clientHost.hold(NekoChat, attachment),
25278
+ openSubagent: (input) => clientHost.hold(NekoSubagent, input)
24693
25279
  };
24694
25280
  }
24695
25281
  return servicesValue;
@@ -24713,10 +25299,13 @@ class MobileGateway {
24713
25299
  if (this.notificationCleanup !== undefined)
24714
25300
  return;
24715
25301
  const [serverEnd, clientEnd] = memoryLinkPair();
24716
- this.chord.accept(serverEnd, { principal: { connectionId: "mobile-notification-hub" }, kind: "client" });
24717
- const host = createHost2({ kind: "client", upstream: identifiedLink(clientEnd, this.serverId), onError: () => {} });
24718
- const directory = host.get(NekoSessionDirectory, {}, { at: this.serverId });
24719
- const workspaces = host.get(NekoWorkspaces, {}, { at: this.serverId });
25302
+ this.chord.accept(serverEnd, {
25303
+ principal: { kind: "local", connectionId: "mobile-notification-hub" },
25304
+ kind: "client"
25305
+ });
25306
+ const host = createClientHost(clientEnd);
25307
+ const directory = host.hold(NekoSessionDirectory, {});
25308
+ const workspaces = host.hold(NekoWorkspaces, {});
24720
25309
  const last = { directory: -1, workspaces: -1 };
24721
25310
  const unsubscribe = [
24722
25311
  directory.state.subscribe(() => {
@@ -25013,14 +25602,7 @@ async function removeGatewayPortFile(nekoDir, serverId) {
25013
25602
 
25014
25603
  // packages/gateway/src/server-services.ts
25015
25604
  import { randomUUID as randomUUID4 } from "crypto";
25016
- import {
25017
- ChordError as ChordError5,
25018
- cell as cell3,
25019
- hostPrincipal as hostPrincipal3,
25020
- listener as listener3,
25021
- local as local3,
25022
- method as method3
25023
- } from "@earendil-works/chord";
25605
+ import { ChordError as ChordError5, local as local3, method as method2 } from "@earendil-works/chord";
25024
25606
 
25025
25607
  // packages/core/src/application-updates.ts
25026
25608
  import { mkdir as mkdir3, readFile as readFile6, rename as rename3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
@@ -25177,12 +25759,8 @@ import { open as open2 } from "fs/promises";
25177
25759
  import { basename as basename3, extname } from "path";
25178
25760
  import {
25179
25761
  ChordError as ChordError4,
25180
- cell as cell2,
25181
- hostPrincipal as hostPrincipal2,
25182
- listener as listener2,
25183
25762
  local as local2,
25184
- opaque as opaque2,
25185
- resource as resource2
25763
+ opaque as opaque2
25186
25764
  } from "@earendil-works/chord";
25187
25765
  import { withAbortSignal as withAbortSignal4 } from "@earendil-works/chord/context";
25188
25766
  import { UsageDoc as UsageDoc4 } from "@earendil-works/pi-durable";
@@ -25283,15 +25861,7 @@ class ImageUploads {
25283
25861
  }
25284
25862
 
25285
25863
  // packages/gateway/src/subagent-host.ts
25286
- import {
25287
- ChordError as ChordError3,
25288
- cell,
25289
- hostPrincipal,
25290
- listener,
25291
- local,
25292
- opaque,
25293
- resource
25294
- } from "@earendil-works/chord";
25864
+ import { ChordError as ChordError3, local, opaque } from "@earendil-works/chord";
25295
25865
  import { UsageDoc as UsageDoc3 } from "@earendil-works/pi-durable";
25296
25866
  import { Type as Type8 } from "typebox";
25297
25867
  async function openReader(runtime, childId, parentId, context, through, historyUsage) {
@@ -25426,31 +25996,27 @@ var ChildProjection = local("neko.subagent-projection", {
25426
25996
  });
25427
25997
  function installSubagentHost(host, runtime, context) {
25428
25998
  host.serve(ChildProjection, (input, _principal, self2) => {
25429
- const { revision } = self2.state({ revision: cell(Type8.Integer(), 0) });
25430
- const { reader } = self2.resources({
25431
- reader: resource({
25432
- rev: 1,
25433
- async open() {
25434
- const entry = (await listDelegations(runtime.harness, input.parentId, context)).find((d) => input.agentId.startsWith("round-") ? d.taskId === Number(input.agentId.slice(6)) : (d.agentId ?? agentIdFor(d.childId)) === input.agentId);
25435
- if (!entry)
25436
- throw new ChordError3("gone", "Subagent unavailable");
25437
- const r = await openReader(runtime, entry.childId, input.parentId, context, entry.historyThrough, entry.historyUsage);
25438
- try {
25439
- const initial = await r.refresh();
25440
- await host.root((ctx) => revision.replace(ctx, initial));
25441
- return r;
25442
- } catch (error) {
25443
- r.close();
25444
- throw error;
25445
- }
25446
- },
25447
- close: (r) => r.close()
25448
- })
25999
+ const revision = self2.state("revision", Type8.Integer(), 0);
26000
+ const reader = self2.resource("reader", {
26001
+ version: 1,
26002
+ async open() {
26003
+ const entry = (await listDelegations(runtime.harness, input.parentId, context)).find((d) => input.agentId.startsWith("round-") ? d.taskId === Number(input.agentId.slice(6)) : (d.agentId ?? agentIdFor(d.childId)) === input.agentId);
26004
+ if (!entry)
26005
+ throw new ChordError3("gone", "Subagent unavailable");
26006
+ const r = await openReader(runtime, entry.childId, input.parentId, context, entry.historyThrough, entry.historyUsage);
26007
+ try {
26008
+ const initial = await r.refresh();
26009
+ await host.root((ctx) => revision.replace(ctx, initial));
26010
+ return r;
26011
+ } catch (error) {
26012
+ r.close();
26013
+ throw error;
26014
+ }
26015
+ },
26016
+ close: (r) => r.close()
25449
26017
  });
25450
- self2.listen({
25451
- view: listener(reader, (r) => r, async (_event, ctx) => {
25452
- revision.replace(ctx, await reader.value.refresh());
25453
- })
26018
+ self2.on("view", reader, (r) => r, async (_event, ctx) => {
26019
+ revision.replace(ctx, await reader.value.refresh());
25454
26020
  });
25455
26021
  return { revision, reader: self2.expose(reader) };
25456
26022
  });
@@ -25466,23 +26032,24 @@ function installSubagentHost(host, runtime, context) {
25466
26032
  throw new ChordError3("forbidden", "Subagent does not belong to this session");
25467
26033
  return entry;
25468
26034
  };
25469
- const { target } = self2.resources({ target: resource({ rev: 1, deps: [owner], open: check }) });
25470
- const shared = self2.get(ChildProjection, { parentId: Number(input.sessionId), agentId: input.agentId ?? `round-${input.taskId}` }, { principal: hostPrincipal });
25471
- const { authorizedRevision } = self2.state({ authorizedRevision: cell(Type8.Integer(), 0) });
25472
- self2.listen({
25473
- authorizedView: listener(shared.revision, async (value, ctx) => {
25474
- try {
25475
- target.value;
25476
- await check();
25477
- if (value)
25478
- authorizedRevision.replace(ctx, value);
25479
- } catch {
25480
- authorizedRevision.replace(ctx, 0);
25481
- }
25482
- })
26035
+ const target = self2.resource("target", { version: 1, deps: [owner], open: check });
26036
+ const shared = self2.get(ChildProjection, {
26037
+ parentId: Number(input.sessionId),
26038
+ agentId: input.agentId ?? `round-${input.taskId}`
26039
+ });
26040
+ const authorizedRevision = self2.state("authorizedRevision", Type8.Integer(), 0);
26041
+ self2.on("authorizedView", shared.revision, async (value, ctx) => {
26042
+ try {
26043
+ target.value;
26044
+ await check();
26045
+ if (value)
26046
+ authorizedRevision.replace(ctx, value);
26047
+ } catch {
26048
+ authorizedRevision.replace(ctx, 0);
26049
+ }
25483
26050
  });
25484
26051
  return {
25485
- kind: "ready",
26052
+ variant: "ready",
25486
26053
  revision: authorizedRevision,
25487
26054
  chunk: async (request) => {
25488
26055
  await check();
@@ -25638,65 +26205,61 @@ async function openProjection(runtime, session, context) {
25638
26205
  function serveSessions(host, runtime, context) {
25639
26206
  installSubagentHost(host, runtime, context);
25640
26207
  host.serve(Owner, (_input, _principal, self2) => {
25641
- const { owner } = self2.resources({
25642
- owner: resource2({
25643
- rev: 1,
25644
- open: () => ({ active: null }),
25645
- close: (value) => {
25646
- value.active = null;
25647
- }
25648
- })
26208
+ const owner = self2.resource("owner", {
26209
+ version: 1,
26210
+ open: () => ({ active: null }),
26211
+ close: (value) => {
26212
+ value.active = null;
26213
+ }
25649
26214
  });
25650
26215
  return { owner: self2.expose(owner) };
25651
26216
  });
25652
26217
  host.serve(Projection, (input, _principal, self2) => {
25653
- const { view } = self2.state({ view: cell2(ChatViewSchema, emptyView) });
25654
- const { projection } = self2.resources({
25655
- projection: resource2({
25656
- rev: 1,
25657
- async open({ signal }) {
25658
- const session = await runtime.getConversation(Number(input.id));
25659
- if (!session)
25660
- throw new ChordError4("gone", "Session unavailable");
25661
- const opened = await openProjection(runtime, session, context);
25662
- try {
25663
- const initial = await opened.project();
25664
- await host.root((ctx) => view.replace(ctx, initial), { signal });
25665
- return opened;
25666
- } catch (error) {
25667
- opened.close();
25668
- throw error;
25669
- }
25670
- },
25671
- close: (value) => value.close()
25672
- })
26218
+ const view = self2.state("view", ChatViewSchema, emptyView);
26219
+ const projection = self2.resource("projection", {
26220
+ version: 1,
26221
+ async open({ signal }) {
26222
+ const session = await runtime.getConversation(Number(input.id));
26223
+ if (!session)
26224
+ throw new ChordError4("gone", "Session unavailable");
26225
+ const opened = await openProjection(runtime, session, context);
26226
+ try {
26227
+ const initial = await opened.project();
26228
+ await host.root((ctx) => view.replace(ctx, initial), { signal });
26229
+ return opened;
26230
+ } catch (error) {
26231
+ opened.close();
26232
+ throw error;
26233
+ }
26234
+ },
26235
+ close: (value) => value.close()
25673
26236
  });
25674
- self2.listen({
25675
- view: listener2(projection, (value) => value, async (_event, ctx) => {
25676
- view.replace(ctx, await projection.value.project());
25677
- })
26237
+ self2.on("view", projection, (value) => value, async (_event, ctx) => {
26238
+ view.replace(ctx, await projection.value.project());
25678
26239
  });
25679
26240
  return { view, reader: self2.expose(projection) };
25680
26241
  });
25681
26242
  host.serve(NekoChat, (input, principal, self2) => {
25682
26243
  const owner = self2.get(Owner, {}, { principal });
25683
- const shared = self2.get(Projection, { id: input.sessionId }, { principal: hostPrincipal2 });
25684
- const { uploads, projection } = self2.resources({
25685
- uploads: resource2({ rev: 1, open: () => new ImageUploads, close: (value) => value.close() }),
25686
- projection: resource2({
25687
- rev: 1,
25688
- deps: [owner],
25689
- async open() {
25690
- if (owner.owner?.active?.attachmentId !== input.attachmentId || owner.owner.active.sessionId !== input.sessionId)
25691
- throw new ChordError4("forbidden", "Attachment does not belong to this connection");
25692
- const session = await runtime.getConversation(Number(input.sessionId));
25693
- if (!session)
25694
- throw new ChordError4("gone", "Session unavailable");
25695
- return {
25696
- session
25697
- };
25698
- }
25699
- })
26244
+ const shared = self2.get(Projection, { id: input.sessionId });
26245
+ const uploads = self2.resource("uploads", {
26246
+ version: 1,
26247
+ open: () => new ImageUploads,
26248
+ close: (value) => value.close()
26249
+ });
26250
+ const projection = self2.resource("projection", {
26251
+ version: 1,
26252
+ deps: [owner],
26253
+ async open() {
26254
+ if (owner.owner?.active?.attachmentId !== input.attachmentId || owner.owner.active.sessionId !== input.sessionId)
26255
+ throw new ChordError4("forbidden", "Attachment does not belong to this connection");
26256
+ const session = await runtime.getConversation(Number(input.sessionId));
26257
+ if (!session)
26258
+ throw new ChordError4("gone", "Session unavailable");
26259
+ return {
26260
+ session
26261
+ };
26262
+ }
25700
26263
  });
25701
26264
  const safeUpload = (action) => {
25702
26265
  try {
@@ -25706,7 +26269,7 @@ function serveSessions(host, runtime, context) {
25706
26269
  }
25707
26270
  };
25708
26271
  const behavior = {
25709
- kind: "ready",
26272
+ variant: "ready",
25710
26273
  view: shared.view,
25711
26274
  draftImage: async ({ id, offset }) => safeUpload(() => uploads.value.read(id, offset)),
25712
26275
  importImage: async (path, ctx) => {
@@ -26568,7 +27131,11 @@ var ServerState = local3("neko.server-state", {
26568
27131
  directory: DirectoryStateSchema,
26569
27132
  workspaces: WorkspaceStateSchema,
26570
27133
  credentials: CredentialStateSchema,
26571
- refreshCredentials: method3(Type10.Null(), Type10.Null())
27134
+ searchSettings: SearchSettingsStateSchema,
27135
+ contextSettings: ContextSettingsStateSchema,
27136
+ refreshCredentials: method2(Type10.Null(), Type10.Null()),
27137
+ refreshSearchSettings: method2(Type10.Null(), Type10.Null()),
27138
+ refreshContextSettings: method2(Type10.Null(), Type10.Null())
26572
27139
  }
26573
27140
  });
26574
27141
  async function createNekoServerServices(options) {
@@ -26576,6 +27143,7 @@ async function createNekoServerServices(options) {
26576
27143
  const updates = new UpdateChecker(options.updates);
26577
27144
  await updates.local();
26578
27145
  const updateListeners = new Set;
27146
+ const info = (value) => ({ ...value, serverId: options.serverId });
26579
27147
  let published = JSON.stringify(updates.state), closed = false;
26580
27148
  const publishUpdates = () => {
26581
27149
  if (closed)
@@ -26588,22 +27156,20 @@ async function createNekoServerServices(options) {
26588
27156
  listener(updates.state);
26589
27157
  };
26590
27158
  host.serve(NekoApplicationInfo, (_input, _principal, self2) => {
26591
- const state = self2.state({ info: cell3(ApplicationInfoSchema, updates.state) });
26592
- self2.listen({
26593
- updates: listener3({
26594
- subscribe: (fn) => {
26595
- updateListeners.add(fn);
26596
- return () => updateListeners.delete(fn);
26597
- }
26598
- }, (value, ctx) => state.info.replace(ctx, value))
26599
- });
27159
+ const current = self2.state("info", ApplicationInfoSchema, info(updates.state));
27160
+ self2.on("updates", {
27161
+ subscribe: (fn) => {
27162
+ updateListeners.add(fn);
27163
+ return () => updateListeners.delete(fn);
27164
+ }
27165
+ }, (value, ctx) => current.replace(ctx, info(value)));
26600
27166
  return {
26601
- kind: "ready",
26602
- info: state.info,
27167
+ variant: "ready",
27168
+ info: current,
26603
27169
  check: async () => {
26604
27170
  await updates.check(true);
26605
27171
  publishUpdates();
26606
- return updates.state;
27172
+ return info(updates.state);
26607
27173
  }
26608
27174
  };
26609
27175
  });
@@ -26647,7 +27213,13 @@ async function createNekoServerServices(options) {
26647
27213
  status: info.status
26648
27214
  }));
26649
27215
  const readStatuses = async () => Object.fromEntries(await Promise.all(CREDENTIAL_PROVIDERS.map(async (spec) => [spec.id, await runtime.credentialStatus(spec.id)])));
26650
- const [sessions, workspaces, statuses] = await Promise.all([list(), runtime.listWorkspaces(), readStatuses()]);
27216
+ const [sessions, workspaces, statuses, search, contextSettings0] = await Promise.all([
27217
+ list(),
27218
+ runtime.listWorkspaces(),
27219
+ readStatuses(),
27220
+ runtime.searchSettings(),
27221
+ runtime.contextSettings()
27222
+ ]);
26651
27223
  let mutationTail = Promise.resolve();
26652
27224
  const serialize = (fn) => {
26653
27225
  const result = mutationTail.catch(() => {}).then(fn);
@@ -26655,43 +27227,64 @@ async function createNekoServerServices(options) {
26655
27227
  return result;
26656
27228
  };
26657
27229
  host.serve(ServerState, (_input, _principal, self2) => {
26658
- const state = self2.state({
26659
- directory: cell3(DirectoryStateSchema, { revision: 1, sessions }),
26660
- workspaces: cell3(WorkspaceStateSchema, { revision: 1, workspaces }),
26661
- credentials: cell3(CredentialStateSchema, { revision: 1, statuses })
27230
+ const directory = self2.state("directory", DirectoryStateSchema, { revision: 1, sessions });
27231
+ const workspaceState = self2.state("workspaces", WorkspaceStateSchema, { revision: 1, workspaces });
27232
+ const credentials = self2.state("credentials", CredentialStateSchema, { revision: 1, statuses });
27233
+ const searchSettings = self2.state("searchSettings", SearchSettingsStateSchema, { revision: 1, ...search });
27234
+ const contextSettings = self2.state("contextSettings", ContextSettingsStateSchema, {
27235
+ revision: 1,
27236
+ ...contextSettings0
26662
27237
  });
26663
- self2.listen({
26664
- directory: listener3({ subscribe: runtime.harness.subscribeCommits.bind(runtime.harness) }, async (event, ctx) => {
26665
- if (!event.changes.some((change) => change.type === "conversation" || change.type === "document" && [
26666
- "app.neko.session-metadata",
26667
- "app.neko.activity",
26668
- "app.neko.conversation",
26669
- "app.neko.workspaces",
26670
- "pi.agent"
26671
- ].includes(change.record.kind)))
26672
- return;
26673
- await serialize(async () => {
26674
- const [items, roots] = await Promise.all([list(), runtime.listWorkspaces()]);
26675
- state.directory.replace(ctx, { revision: state.directory.value.revision + 1, sessions: items });
26676
- state.workspaces.replace(ctx, { revision: state.workspaces.value.revision + 1, workspaces: roots });
26677
- });
26678
- })
27238
+ const refreshContextSettings = async (_input, ctx) => {
27239
+ const next = await runtime.contextSettings();
27240
+ const { revision, ...current } = contextSettings.value;
27241
+ if (JSON.stringify(next) !== JSON.stringify(current))
27242
+ contextSettings.replace(ctx, { revision: revision + 1, ...next });
27243
+ return null;
27244
+ };
27245
+ const refreshSearchSettings = async (_input, ctx) => {
27246
+ const next = await runtime.searchSettings();
27247
+ const current = searchSettings.value;
27248
+ if (next.selected !== current.selected)
27249
+ searchSettings.replace(ctx, { revision: current.revision + 1, ...next });
27250
+ return null;
27251
+ };
27252
+ self2.on("directory", { subscribe: runtime.harness.subscribeCommits.bind(runtime.harness) }, async (event, ctx) => {
27253
+ if (!event.changes.some((change) => change.type === "conversation" || change.type === "document" && [
27254
+ "app.neko.session-metadata",
27255
+ "app.neko.activity",
27256
+ "app.neko.conversation",
27257
+ "app.neko.workspaces",
27258
+ "pi.agent"
27259
+ ].includes(change.record.kind)))
27260
+ return;
27261
+ await serialize(async () => {
27262
+ const [items, roots] = await Promise.all([list(), runtime.listWorkspaces()]);
27263
+ directory.replace(ctx, { revision: directory.value.revision + 1, sessions: items });
27264
+ workspaceState.replace(ctx, { revision: workspaceState.value.revision + 1, workspaces: roots });
27265
+ });
26679
27266
  });
26680
27267
  return {
26681
- ...state,
27268
+ directory,
27269
+ workspaces: workspaceState,
27270
+ credentials,
27271
+ searchSettings,
27272
+ contextSettings,
26682
27273
  async refreshCredentials(_input, ctx) {
26683
- state.credentials.replace(ctx, {
26684
- revision: state.credentials.value.revision + 1,
27274
+ credentials.replace(ctx, {
27275
+ revision: credentials.value.revision + 1,
26685
27276
  statuses: await readStatuses()
26686
27277
  });
26687
27278
  return null;
26688
- }
27279
+ },
27280
+ refreshSearchSettings,
27281
+ refreshContextSettings
26689
27282
  };
26690
- }, { eager: { input: {} } });
26691
- const shared = (self2) => self2.get(ServerState, {}, { principal: hostPrincipal3 });
26692
- host.serve(NekoSessionDirectory, (_input, _principal, self2) => ({ kind: "ready", state: shared(self2).directory }));
27283
+ }, { lifetime: "host" });
27284
+ const shared = (self2) => self2.get(ServerState, {});
27285
+ host.serve(NekoSessionDirectory, (_input, _principal, self2) => ({ variant: "ready", state: shared(self2).directory }));
26693
27286
  host.serve(NekoWorkspaces, (_input, _principal, self2) => ({
26694
- kind: "ready",
27287
+ variant: "ready",
26695
27288
  state: shared(self2).workspaces,
26696
27289
  browse: (input) => browseWorkspaces(input),
26697
27290
  add: (input) => serialize(() => runtime.addWorkspace(input.path))
@@ -26699,7 +27292,7 @@ async function createNekoServerServices(options) {
26699
27292
  host.serve(NekoCredentials, (_input, _principal, self2) => {
26700
27293
  const hub = shared(self2);
26701
27294
  return {
26702
- kind: "ready",
27295
+ variant: "ready",
26703
27296
  state: hub.credentials,
26704
27297
  save: async (input, ctx) => {
26705
27298
  await runtime.saveCredential(input.provider, input.key);
@@ -26711,8 +27304,34 @@ async function createNekoServerServices(options) {
26711
27304
  }
26712
27305
  };
26713
27306
  });
27307
+ host.serve(NekoSearchSettings, (_input, _principal, self2) => {
27308
+ const hub = shared(self2);
27309
+ return {
27310
+ variant: "ready",
27311
+ state: hub.searchSettings,
27312
+ set: (input, ctx) => serialize(async () => {
27313
+ await runtime.setSearchProvider(input.provider);
27314
+ return hub.refreshSearchSettings(null, ctx);
27315
+ })
27316
+ };
27317
+ });
27318
+ host.serve(NekoContextSettings, (_input, _principal, self2) => {
27319
+ const hub = shared(self2);
27320
+ return {
27321
+ variant: "ready",
27322
+ state: hub.contextSettings,
27323
+ set: (input, ctx) => serialize(async () => {
27324
+ await runtime.setContextSettings(input);
27325
+ return hub.refreshContextSettings(null, ctx);
27326
+ }),
27327
+ reset: (_input, ctx) => serialize(async () => {
27328
+ await runtime.resetContextSettings();
27329
+ return hub.refreshContextSettings(null, ctx);
27330
+ })
27331
+ };
27332
+ });
26714
27333
  host.serve(NekoModels, () => ({
26715
- kind: "ready",
27334
+ variant: "ready",
26716
27335
  list: async () => [...runtime.listModels()].map((value) => ({
26717
27336
  ...value,
26718
27337
  input: [...value.input],
@@ -26720,7 +27339,7 @@ async function createNekoServerServices(options) {
26720
27339
  }))
26721
27340
  }));
26722
27341
  host.serve(NekoWorkspaceFiles, () => ({
26723
- kind: "ready",
27342
+ variant: "ready",
26724
27343
  search: async (input, ctx) => {
26725
27344
  const roots = await runtime.listWorkspaces();
26726
27345
  if (!roots.some((root) => root.path === input.workspaceRoot))
@@ -26738,7 +27357,7 @@ async function createNekoServerServices(options) {
26738
27357
  return found;
26739
27358
  };
26740
27359
  return {
26741
- kind: "ready",
27360
+ variant: "ready",
26742
27361
  create: (input) => serialize(async () => summary((await runtime.createConversation(input)).id)),
26743
27362
  fork: (id) => serialize(async () => summary((await runtime.forkConversation(toConversationId(id))).id)),
26744
27363
  archive: async (id) => {
@@ -26761,7 +27380,7 @@ async function createNekoServerServices(options) {
26761
27380
  };
26762
27381
  });
26763
27382
  host.serve(NekoSkillMarket, () => ({
26764
- kind: "ready",
27383
+ variant: "ready",
26765
27384
  list: (input) => market.list(input.query).then((skills) => Promise.all(skills.map((skill) => withInstallState(skill, input.workspaceRoot)))).catch(toRemote),
26766
27385
  detail: (input) => market.detail(input.name).then((skill) => withInstallState(skill, input.workspaceRoot)).catch(toRemote),
26767
27386
  preview: (input) => market.preview(input.name).then((value) => ({ ...value, files: [...value.files] })).catch(toRemote),
@@ -26876,7 +27495,7 @@ class UnixLink {
26876
27495
  }
26877
27496
  send(frame) {
26878
27497
  try {
26879
- this.#native.send(frame);
27498
+ return this.#native.send(frame);
26880
27499
  } catch (error) {
26881
27500
  this.#native.close("pending byte limit exceeded");
26882
27501
  throw error;
@@ -26985,7 +27604,7 @@ function createWsListener(options) {
26985
27604
  server.on("error", (error) => options.onError?.(error));
26986
27605
  server.on("connection", (socket) => {
26987
27606
  socket.on("error", (error) => options.onError?.(error));
26988
- accept(boundedWebSocketLink(socket, options.maxPendingBytes));
27607
+ accept(boundedWebSocketLink(socket, options.maxPendingBytes, { acceptPacked: false }));
26989
27608
  });
26990
27609
  },
26991
27610
  async close() {
@@ -27011,6 +27630,7 @@ async function startGateway(options = {}) {
27011
27630
  const storagePath = options.storagePath ?? fileConfig.storagePath ?? DEFAULT_STORAGE_PATH;
27012
27631
  const host = options.host ?? fileConfig.gateway?.host ?? DEFAULT_HOST;
27013
27632
  const port = options.port !== undefined ? options.port : fileConfig.gateway?.port ?? (process.platform === "win32" ? 0 : DEFAULT_PORT);
27633
+ const searchProvider = options.searchProvider ?? fileConfig.webSearch?.provider;
27014
27634
  const marketUrl = options.marketUrl ?? fileConfig.skills?.marketUrl ?? DEFAULT_SKILL_MARKET_URL;
27015
27635
  const nekoDir = dirname9(storagePath);
27016
27636
  const serverId = options.serverId ?? await loadOrCreateServerId(nekoDir);
@@ -27022,6 +27642,8 @@ async function startGateway(options = {}) {
27022
27642
  ...options.storage !== "memory" ? { storagePath } : {},
27023
27643
  ...options.credentialsPath !== undefined ? { credentialsPath: options.credentialsPath } : {},
27024
27644
  ...options.apiKey !== undefined ? { apiKey: options.apiKey } : {},
27645
+ ...searchProvider !== undefined ? { searchProvider } : {},
27646
+ ...fileConfig.context !== undefined ? { contextSettings: fileConfig.context } : {},
27025
27647
  model,
27026
27648
  thinkingLevel: options.thinkingLevel ?? fileConfig.thinkingLevel ?? "high",
27027
27649
  ...options.cwd !== undefined ? { cwd: options.cwd } : {},
@@ -27052,8 +27674,9 @@ async function startGateway(options = {}) {
27052
27674
  }
27053
27675
  async function bindGateway(runtime, options) {
27054
27676
  const { context, host, port, socketPath, serverId, nekoDir } = options;
27055
- const chord = createHost3({
27056
- name: serverId,
27677
+ const chord = createHost2({
27678
+ name: GATEWAY_HOST,
27679
+ self: HOST_PRINCIPAL,
27057
27680
  dev: true,
27058
27681
  onError: (error) => options.onError?.(error instanceof Error ? error : new Error(String(error))),
27059
27682
  principalKey: gatewayPrincipalKey
@@ -27062,6 +27685,7 @@ async function bindGateway(runtime, options) {
27062
27685
  host: chord,
27063
27686
  runtime,
27064
27687
  context,
27688
+ serverId,
27065
27689
  marketUrl: options.marketUrl,
27066
27690
  ...options.updates ? { updates: options.updates } : {},
27067
27691
  ...options.updateIntervalMs ? { updateIntervalMs: options.updateIntervalMs } : {},
@@ -27077,7 +27701,10 @@ async function bindGateway(runtime, options) {
27077
27701
  }
27078
27702
  try {
27079
27703
  for (const listener of listeners)
27080
- await listener.start((link) => chord.accept(link, { principal: { connectionId: randomUUID5() } }));
27704
+ await listener.start((link) => chord.accept(link, {
27705
+ principal: { kind: "local", connectionId: randomUUID5() },
27706
+ onProtocolMismatch: ({ peer }) => options.onError?.(new Error(`Rejected a client on Chord protocol ${peer ?? "unknown"}; restart or update it.`))
27707
+ }));
27081
27708
  } catch (error) {
27082
27709
  await Promise.allSettled(listeners.map((listener) => listener.close()));
27083
27710
  await chord.close();
@@ -27140,16 +27767,6 @@ async function bindGateway(runtime, options) {
27140
27767
  }).catch((error) => options.onError?.(error instanceof Error ? error : new Error(String(error))));
27141
27768
  return gateway;
27142
27769
  }
27143
- function gatewayPrincipalKey(principal) {
27144
- if (typeof principal === "object" && principal !== null) {
27145
- const record = principal;
27146
- if (record.kind === "mobile" && typeof record.deviceId === "string" && typeof record.grantRevision === "number") {
27147
- const connectionId = typeof record.connectionId === "string" ? record.connectionId : "";
27148
- return `mobile:${record.deviceId}:${record.grantRevision}:${connectionId}`;
27149
- }
27150
- }
27151
- return JSON.stringify(principal) ?? "unknown";
27152
- }
27153
27770
  function toStartupError(error, info) {
27154
27771
  const message = error instanceof Error ? error.message : String(error);
27155
27772
  const existingServerId = info.socketPath === null ? undefined : serverIdFromSocketPath(info.socketPath);