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/neko.js CHANGED
@@ -80,7 +80,7 @@ function updateAction(input) {
80
80
  return "install";
81
81
  return "none";
82
82
  }
83
- var RUNNING_VERSION = "0.4.2", APPLICATION_PROTOCOL = 4, ApplicationProtocolError;
83
+ var RUNNING_VERSION = "0.4.3", APPLICATION_PROTOCOL = 6, ApplicationProtocolError;
84
84
  var init_application_version = __esm(() => {
85
85
  ApplicationProtocolError = class ApplicationProtocolError extends Error {
86
86
  constructor() {
@@ -375,6 +375,182 @@ var init_update = __esm(() => {
375
375
  init_application_version();
376
376
  });
377
377
 
378
+ // packages/core/src/context-limits.ts
379
+ var PERCENT, CONTEXT_FIELD_LIMITS, PRUNING_DEFAULTS;
380
+ var init_context_limits = __esm(() => {
381
+ PERCENT = { kind: "int", min: 1, max: 99 };
382
+ CONTEXT_FIELD_LIMITS = {
383
+ compaction: {
384
+ enabled: { kind: "boolean" },
385
+ triggerPercent: PERCENT,
386
+ keepRecentTokens: { kind: "int", min: 1, max: 1e8 }
387
+ },
388
+ pruning: {
389
+ enabled: { kind: "boolean" },
390
+ triggerPercent: PERCENT,
391
+ keepRecent: { kind: "int", min: 0, max: 1e5 },
392
+ minChars: { kind: "int", min: 1, max: 1e8 },
393
+ minSavePercent: PERCENT
394
+ }
395
+ };
396
+ PRUNING_DEFAULTS = {
397
+ enabled: false,
398
+ triggerPercent: 35,
399
+ keepRecent: 10,
400
+ minChars: 4000,
401
+ minSavePercent: 5
402
+ };
403
+ });
404
+
405
+ // packages/core/src/context-settings.ts
406
+ import { DEFAULT_COMPACTION_POLICY } from "@earendil-works/pi-durable";
407
+ function describe(field) {
408
+ return field.kind === "boolean" ? "a boolean" : `an integer between ${field.min} and ${field.max}`;
409
+ }
410
+ function validateGroups(value, prefix, allowNull) {
411
+ if (!isObject(value))
412
+ throw new Error(`"${prefix}" must be an object.`);
413
+ const out = {};
414
+ for (const key of Object.keys(value)) {
415
+ if (!(key in SCHEMA))
416
+ throw new Error(`"${prefix}.${key}" is not a known setting.`);
417
+ }
418
+ for (const [group, fields] of Object.entries(SCHEMA)) {
419
+ const given = value[group];
420
+ if (given === undefined)
421
+ continue;
422
+ if (!isObject(given))
423
+ throw new Error(`"${prefix}.${group}" must be an object.`);
424
+ const target = {};
425
+ for (const key of Object.keys(given)) {
426
+ if (!(key in fields))
427
+ throw new Error(`"${prefix}.${group}.${key}" is not a known setting.`);
428
+ }
429
+ for (const [name, field] of Object.entries(fields)) {
430
+ const item = given[name];
431
+ if (item === undefined)
432
+ continue;
433
+ if (item === null && allowNull) {
434
+ target[name] = null;
435
+ continue;
436
+ }
437
+ const ok = field.kind === "boolean" ? typeof item === "boolean" : typeof item === "number" && Number.isInteger(item) && item >= field.min && item <= field.max;
438
+ if (!ok)
439
+ throw new Error(`"${prefix}.${group}.${name}" must be ${describe(field)}.`);
440
+ target[name] = item;
441
+ }
442
+ out[group] = target;
443
+ }
444
+ return out;
445
+ }
446
+ function parseContextSettings(value, prefix = "context") {
447
+ return validateGroups(value, prefix, false);
448
+ }
449
+ function parseContextSettingsPatch(value) {
450
+ return validateGroups(value, "context", true);
451
+ }
452
+ function applyContextPatch(saved, patch) {
453
+ const next = {
454
+ compaction: { ...saved.compaction },
455
+ pruning: { ...saved.pruning }
456
+ };
457
+ for (const [key, value] of Object.entries(patch.compaction ?? {})) {
458
+ if (value !== undefined)
459
+ next.compaction[key] = value;
460
+ }
461
+ for (const [key, value] of Object.entries(patch.pruning ?? {})) {
462
+ if (value !== undefined)
463
+ next.pruning[key] = value;
464
+ }
465
+ return next;
466
+ }
467
+ function sanitizeSavedContextSettings(raw) {
468
+ const saved = emptySavedContextSettings();
469
+ if (!isObject(raw))
470
+ return saved;
471
+ for (const [group, fields] of Object.entries(SCHEMA)) {
472
+ const stored = raw[group];
473
+ if (!isObject(stored))
474
+ continue;
475
+ for (const [name, field] of Object.entries(fields)) {
476
+ const item = stored[name];
477
+ const ok = field.kind === "boolean" ? typeof item === "boolean" : typeof item === "number" && Number.isInteger(item) && item >= field.min && item <= field.max;
478
+ if (ok)
479
+ saved[group][name] = item;
480
+ }
481
+ }
482
+ return saved;
483
+ }
484
+ function pick(saved, config, fallback) {
485
+ if (saved !== null && saved !== undefined)
486
+ return { value: saved, source: "saved" };
487
+ if (config !== undefined)
488
+ return { value: config, source: "config" };
489
+ return { value: fallback, source: "default" };
490
+ }
491
+ function resolveContextSettings(config, saved) {
492
+ const cEnabled = pick(saved.compaction.enabled, config?.compaction?.enabled, DEFAULT_COMPACTION_POLICY.enabled);
493
+ const cTrigger = pick(saved.compaction.triggerPercent, config?.compaction?.triggerPercent, null);
494
+ const cKeep = pick(saved.compaction.keepRecentTokens, config?.compaction?.keepRecentTokens, DEFAULT_COMPACTION_POLICY.keepRecentTokens);
495
+ const pEnabled = pick(saved.pruning.enabled, config?.pruning?.enabled, PRUNING_DEFAULTS.enabled);
496
+ const pTrigger = pick(saved.pruning.triggerPercent, config?.pruning?.triggerPercent, PRUNING_DEFAULTS.triggerPercent);
497
+ const pKeep = pick(saved.pruning.keepRecent, config?.pruning?.keepRecent, PRUNING_DEFAULTS.keepRecent);
498
+ const pMin = pick(saved.pruning.minChars, config?.pruning?.minChars, PRUNING_DEFAULTS.minChars);
499
+ const pSave = pick(saved.pruning.minSavePercent, config?.pruning?.minSavePercent, PRUNING_DEFAULTS.minSavePercent);
500
+ return {
501
+ effective: {
502
+ compaction: { enabled: cEnabled.value, triggerPercent: cTrigger.value, keepRecentTokens: cKeep.value },
503
+ pruning: {
504
+ enabled: pEnabled.value,
505
+ triggerPercent: pTrigger.value,
506
+ keepRecent: pKeep.value,
507
+ minChars: pMin.value,
508
+ minSavePercent: pSave.value
509
+ }
510
+ },
511
+ sources: {
512
+ compaction: { enabled: cEnabled.source, triggerPercent: cTrigger.source, keepRecentTokens: cKeep.source },
513
+ pruning: {
514
+ enabled: pEnabled.source,
515
+ triggerPercent: pTrigger.source,
516
+ keepRecent: pKeep.source,
517
+ minChars: pMin.source,
518
+ minSavePercent: pSave.source
519
+ }
520
+ }
521
+ };
522
+ }
523
+ function compactionPolicy(state, windows) {
524
+ const { effective, sources } = state;
525
+ const policy = {};
526
+ if (sources.compaction.enabled !== "default")
527
+ policy.enabled = effective.compaction.enabled;
528
+ if (sources.compaction.keepRecentTokens !== "default")
529
+ policy.keepRecentTokens = effective.compaction.keepRecentTokens;
530
+ const percent = effective.compaction.triggerPercent;
531
+ const window2 = windows.length > 0 ? Math.min(...windows) : 0;
532
+ if (percent !== null && window2 > 0)
533
+ policy.reserveTokens = window2 - Math.floor(window2 * percent / 100);
534
+ return policy;
535
+ }
536
+ var emptySavedContextSettings = () => ({
537
+ compaction: { enabled: null, triggerPercent: null, keepRecentTokens: null },
538
+ pruning: { enabled: null, triggerPercent: null, keepRecent: null, minChars: null, minSavePercent: null }
539
+ }), SCHEMA, isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
540
+ var init_context_settings = __esm(() => {
541
+ init_context_limits();
542
+ SCHEMA = CONTEXT_FIELD_LIMITS;
543
+ });
544
+
545
+ // packages/core/src/search-providers.ts
546
+ function isSearchProvider(value) {
547
+ return typeof value === "string" && SEARCH_PROVIDERS.includes(value);
548
+ }
549
+ var SEARCH_PROVIDERS, DEFAULT_SEARCH_PROVIDER = "tavily";
550
+ var init_search_providers = __esm(() => {
551
+ SEARCH_PROVIDERS = ["tavily", "deepseek"];
552
+ });
553
+
378
554
  // packages/core/src/config.ts
379
555
  import { existsSync, readFileSync } from "fs";
380
556
  import { homedir as homedir2 } from "os";
@@ -489,9 +665,28 @@ function validate(config, source) {
489
665
  throw new Error(`neko config: ${source}: "skills.marketUrl" must be a string.`);
490
666
  }
491
667
  }
668
+ const webSearch = config.webSearch;
669
+ if (webSearch !== undefined) {
670
+ if (typeof webSearch !== "object" || webSearch === null || Array.isArray(webSearch)) {
671
+ throw new Error(`neko config: ${source}: "webSearch" must be an object.`);
672
+ }
673
+ const { provider } = webSearch;
674
+ if (provider !== undefined && !isSearchProvider(provider)) {
675
+ throw new Error(`neko config: ${source}: "webSearch.provider" must be one of ${SEARCH_PROVIDERS.map((item) => `"${item}"`).join(", ")}.`);
676
+ }
677
+ }
678
+ if (config.context !== undefined) {
679
+ try {
680
+ parseContextSettings(config.context);
681
+ } catch (error) {
682
+ throw new Error(`neko config: ${source}: ${error instanceof Error ? error.message : String(error)}`);
683
+ }
684
+ }
492
685
  }
493
686
  var DEFAULT_SKILL_MARKET_URL = "https://skill.kirineko.tech", NEKO_CONFIG_BASENAME = "neko.config.json", ENV_REFERENCE;
494
687
  var init_config = __esm(() => {
688
+ init_context_settings();
689
+ init_search_providers();
495
690
  ENV_REFERENCE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g;
496
691
  });
497
692
 
@@ -1517,6 +1712,132 @@ async function readCompaction(harness, storage, id, context) {
1517
1712
  }
1518
1713
  var init_compaction = () => {};
1519
1714
 
1715
+ // packages/core/src/context-pruning.ts
1716
+ function estimateTokens(text) {
1717
+ const cjk = text.match(CJK)?.length ?? 0;
1718
+ return cjk * TOKENS_PER_CJK_CHAR + (text.length - cjk) * TOKENS_PER_LATIN_CHAR;
1719
+ }
1720
+ function toolResultChars(message) {
1721
+ return resultTexts(message).reduce((total, text) => total + text.length, 0);
1722
+ }
1723
+ function toolResultTokens(message) {
1724
+ return resultTexts(message).reduce((total, text) => total + estimateTokens(text), 0);
1725
+ }
1726
+ function prunedPlaceholder(message, chars) {
1727
+ const { details: _details, ...rest } = message;
1728
+ return {
1729
+ ...rest,
1730
+ content: [
1731
+ {
1732
+ type: "text",
1733
+ text: `[Output of ${message.toolName} omitted from context to save space (${chars} characters). Run the tool again if the content is needed.]`
1734
+ }
1735
+ ]
1736
+ };
1737
+ }
1738
+ function planPrune(entries, settings) {
1739
+ const replaced = new Set;
1740
+ for (const entry of entries)
1741
+ for (const edit of entry.edits ?? [])
1742
+ replaced.add(edit.target);
1743
+ const results = entries.filter(isToolResult);
1744
+ const candidates = results.slice(0, Math.max(0, results.length - settings.keepRecent));
1745
+ const edits = [];
1746
+ const targets = [];
1747
+ let savedChars = 0;
1748
+ let savedTokens = 0;
1749
+ for (const entry of candidates) {
1750
+ if (replaced.has(entry.id))
1751
+ continue;
1752
+ const message = entry.model[0];
1753
+ const chars = toolResultChars(message);
1754
+ if (chars < settings.minChars)
1755
+ continue;
1756
+ const replacement = prunedPlaceholder(message, chars);
1757
+ edits.push({ target: entry.id, action: "replace", messages: [replacement] });
1758
+ targets.push(entry.id);
1759
+ savedChars += chars;
1760
+ savedTokens += toolResultTokens(message);
1761
+ }
1762
+ return edits.length === 0 ? undefined : { edits, targets, savedChars, savedTokens };
1763
+ }
1764
+
1765
+ class ContextPruner {
1766
+ #options;
1767
+ #unsubscribe;
1768
+ #tail = Promise.resolve();
1769
+ #closed = false;
1770
+ constructor(options) {
1771
+ this.#options = options;
1772
+ this.#unsubscribe = options.harness.subscribeCommits(({ changes }) => {
1773
+ if (!options.settings().enabled)
1774
+ return;
1775
+ for (const change of changes) {
1776
+ if (change.type !== "entry" || change.value.kind !== "pi.assistant")
1777
+ continue;
1778
+ const message = change.value.model?.[0];
1779
+ if (message?.role !== "assistant" || message.stopReason === "aborted" || message.stopReason === "error")
1780
+ continue;
1781
+ const usage = message.usage;
1782
+ const tokens = usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
1783
+ if (tokens <= 0)
1784
+ continue;
1785
+ const { conversationId, id } = change.value;
1786
+ this.#enqueue(() => this.#evaluate(conversationId, id, tokens));
1787
+ }
1788
+ });
1789
+ }
1790
+ async flush() {
1791
+ await this.#tail;
1792
+ }
1793
+ close() {
1794
+ this.#closed = true;
1795
+ this.#unsubscribe();
1796
+ }
1797
+ #enqueue(work) {
1798
+ if (this.#closed)
1799
+ return;
1800
+ this.#tail = this.#tail.then(work).catch((error) => {
1801
+ if (!this.#closed)
1802
+ this.#options.report(error);
1803
+ });
1804
+ }
1805
+ async#evaluate(conversationId, entryId, tokens) {
1806
+ const { harness, context } = this.#options;
1807
+ const settings = this.#options.settings();
1808
+ if (!settings.enabled)
1809
+ return;
1810
+ const window2 = await this.#options.windowOf(conversationId);
1811
+ if (window2 <= 0 || tokens / window2 * 100 < settings.triggerPercent)
1812
+ return;
1813
+ const conversation = await harness.conversation(conversationId, context);
1814
+ if (!conversation)
1815
+ return;
1816
+ const view = await conversation.context(context);
1817
+ const plan = planPrune(view.entries, settings);
1818
+ if (!plan)
1819
+ return;
1820
+ if (plan.savedTokens / window2 * 100 < settings.minSavePercent)
1821
+ return;
1822
+ await conversation.submit({
1823
+ type: "write",
1824
+ requestId: `prune:${entryId}`,
1825
+ entry: {
1826
+ kind: PRUNE_ENTRY_KIND,
1827
+ data: { targets: [...plan.targets], savedChars: plan.savedChars },
1828
+ edits: plan.edits
1829
+ }
1830
+ }, context);
1831
+ }
1832
+ }
1833
+ var PRUNE_ENTRY_KIND = "app.neko.context-prune", TOKENS_PER_LATIN_CHAR = 0.3, TOKENS_PER_CJK_CHAR = 0.6, CJK, isToolResult = (entry) => {
1834
+ const message = entry.model?.[0];
1835
+ return entry.kind === "pi.tool-result" && message?.role === "toolResult";
1836
+ }, resultTexts = (message) => message.content.flatMap((block) => block.type === "text" ? [block.text] : []);
1837
+ var init_context_pruning = __esm(() => {
1838
+ CJK = /[\u3000-\u9fff\uac00-\ud7af\uf900-\ufaff\uff00-\uffef]/g;
1839
+ });
1840
+
1520
1841
  // packages/core/src/image-metadata.ts
1521
1842
  import { defineExtension as defineExtension2, hook, ToolTask } from "@earendil-works/pi-durable";
1522
1843
  import sharp from "sharp";
@@ -2176,7 +2497,7 @@ class JobRegistry {
2176
2497
  function reportText(job, tail) {
2177
2498
  const body = reportTail(tail);
2178
2499
  return [
2179
- `[Background job ${job.jobId} "${job.title}" ${describe(job)}]`,
2500
+ `[Background job ${job.jobId} "${job.title}" ${describe2(job)}]`,
2180
2501
  `Command: ${job.command}`,
2181
2502
  body ? `Last output (use the job tool's output action with jobId "${job.jobId}" for more):
2182
2503
  ${body}` : "It produced no output."
@@ -2355,7 +2676,7 @@ class JobManager {
2355
2676
  if (kind === "stop") {
2356
2677
  const jobId = stringField(args.jobId, "jobId");
2357
2678
  const job = await this.stop(io, api.conversationId, jobId, "model", context);
2358
- return this.#text(`Job ${job.jobId} ${describe(job)}.`);
2679
+ return this.#text(`Job ${job.jobId} ${describe2(job)}.`);
2359
2680
  }
2360
2681
  if (kind !== "start")
2361
2682
  throw new Error("Invalid job arguments: action must be start, output, stop or list.");
@@ -2383,7 +2704,7 @@ class JobManager {
2383
2704
  #listText(items) {
2384
2705
  if (!items.length)
2385
2706
  return "No jobs.";
2386
- return items.map((job) => `${job.jobId} [${describe(job)}] "${job.title}" \u2014 ${job.command}`).join(`
2707
+ return items.map((job) => `${job.jobId} [${describe2(job)}] "${job.title}" \u2014 ${job.command}`).join(`
2387
2708
  `);
2388
2709
  }
2389
2710
  async start(io, conversationId, cwd, args, context) {
@@ -2435,7 +2756,7 @@ class JobManager {
2435
2756
  return this.#render(job, read.text, read.offset, read.dropped);
2436
2757
  }
2437
2758
  #render(job, text, offset, dropped) {
2438
- const lines = [`Job ${job.jobId} ${describe(job)}.`, `Title: ${job.title}`, `Command: ${job.command}`];
2759
+ const lines = [`Job ${job.jobId} ${describe2(job)}.`, `Title: ${job.title}`, `Command: ${job.command}`];
2439
2760
  if (dropped > 0)
2440
2761
  lines.push(`(${dropped} bytes of earlier output were dropped)`);
2441
2762
  lines.push(text ? `Output (next offset ${offset}):
@@ -2500,7 +2821,7 @@ ${text}` : `No new output (next offset ${offset}).`);
2500
2821
  this.registry.abortAll();
2501
2822
  }
2502
2823
  }
2503
- var plain = (value) => JSON.parse(JSON.stringify(value)), statusWord, describe = (job) => {
2824
+ var plain = (value) => JSON.parse(JSON.stringify(value)), statusWord, describe2 = (job) => {
2504
2825
  const parts = [statusWord[job.status]];
2505
2826
  if (job.exitCode !== null)
2506
2827
  parts.push(`exit code ${job.exitCode}`);
@@ -3419,21 +3740,22 @@ var init_tool_approvals = __esm(() => {
3419
3740
  });
3420
3741
  });
3421
3742
 
3422
- // packages/core/src/web-search.ts
3423
- import { Type as Type4 } from "@earendil-works/pi-ai";
3424
- import { defineExtension as defineExtension8, defineTool as defineTool4 } from "@earendil-works/pi-durable";
3743
+ // packages/core/src/web-search/http.ts
3425
3744
  function redact(text, key) {
3426
3745
  for (const secret of key ? [key, encodeURIComponent(key)] : [])
3427
3746
  text = text.replaceAll(secret, "[redacted]");
3428
- return text.replace(/tvly-[a-zA-Z0-9_-]+/g, "[redacted]");
3747
+ return text.replace(/\b(?:tvly|sk)-[a-zA-Z0-9_-]+/g, "[redacted]");
3429
3748
  }
3430
3749
  function record(value) {
3431
3750
  return typeof value === "object" && value !== null && !Array.isArray(value);
3432
3751
  }
3433
- async function readBounded(response, signal) {
3752
+ function truncate(text, max) {
3753
+ return text.length > max ? `${text.slice(0, Math.max(0, max - 1))}\u2026` : text;
3754
+ }
3755
+ async function readBounded(response, signal, label) {
3434
3756
  if (Number(response.headers.get("content-length")) > MAX_RESPONSE_BYTES) {
3435
3757
  await response.body?.cancel();
3436
- throw new TavilyError("Tavily response exceeds the 2 MiB limit.");
3758
+ throw new WebToolError(`${label} response exceeds the 2 MiB limit.`);
3437
3759
  }
3438
3760
  if (!response.body)
3439
3761
  return "";
@@ -3454,7 +3776,7 @@ async function readBounded(response, signal) {
3454
3776
  return text + decoder.decode();
3455
3777
  size += chunk.value.byteLength;
3456
3778
  if (size > MAX_RESPONSE_BYTES)
3457
- throw new TavilyError("Tavily response exceeds the 2 MiB limit.");
3779
+ throw new WebToolError(`${label} response exceeds the 2 MiB limit.`);
3458
3780
  text += decoder.decode(chunk.value, { stream: true });
3459
3781
  }
3460
3782
  } finally {
@@ -3463,7 +3785,8 @@ async function readBounded(response, signal) {
3463
3785
  reader.releaseLock();
3464
3786
  }
3465
3787
  }
3466
- async function tavilyPost(url, body, resolveKey, fetchImpl, context) {
3788
+ async function postJson(options) {
3789
+ const { label, context } = options;
3467
3790
  let key;
3468
3791
  const signal = AbortSignal.any([
3469
3792
  AbortSignal.timeout(REQUEST_TIMEOUT_MS),
@@ -3471,68 +3794,190 @@ async function tavilyPost(url, body, resolveKey, fetchImpl, context) {
3471
3794
  ]);
3472
3795
  try {
3473
3796
  signal.throwIfAborted();
3474
- key = await resolveKey();
3797
+ key = await options.resolveKey();
3475
3798
  signal.throwIfAborted();
3476
- const headers = { "content-type": "application/json" };
3477
- if (key)
3478
- headers.authorization = `Bearer ${key}`;
3479
- else
3480
- headers["x-tavily-access-mode"] = "keyless";
3481
- const response = await fetchImpl(url, { method: "POST", headers, body: JSON.stringify(body), signal });
3482
- const text = redact(await readBounded(response, signal), key);
3799
+ const response = await options.fetchImpl(options.url, {
3800
+ method: "POST",
3801
+ headers: { "content-type": "application/json", ...options.headers(key) },
3802
+ body: JSON.stringify(options.body),
3803
+ signal
3804
+ });
3805
+ const text = redact(await readBounded(response, signal, label), key);
3483
3806
  if (!response.ok)
3484
- 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)"}`);
3485
- const data = JSON.parse(text);
3486
- if (!record(data))
3487
- throw new TavilyError("Invalid Tavily response.");
3488
- if (data.error)
3489
- throw new TavilyError(`Tavily request failed: ${truncate(JSON.stringify(data.error), 500)}`);
3490
- return data;
3807
+ throw options.httpError(response.status, text, key);
3808
+ return JSON.parse(text);
3491
3809
  } catch (error) {
3492
3810
  if (context.abortSignal?.aborted)
3493
- throw new TavilyError("Web request cancelled.");
3811
+ throw new WebToolError("Web request cancelled.");
3494
3812
  if (signal.aborted)
3495
- throw new TavilyError("Tavily request timed out. Please retry.");
3496
- if (error instanceof TavilyError)
3813
+ throw new WebToolError(`${label} request timed out. Please retry.`);
3814
+ if (error instanceof WebToolError)
3497
3815
  throw error;
3498
- throw new TavilyError("Tavily request failed. Check the connection and credentials, then retry.");
3816
+ throw new WebToolError(`${label} request failed. Check the connection and credentials, then retry.`);
3499
3817
  }
3500
3818
  }
3819
+ var REQUEST_TIMEOUT_MS = 30000, MAX_RESPONSE_BYTES, WebToolError;
3820
+ var init_http = __esm(() => {
3821
+ MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
3822
+ WebToolError = class WebToolError extends Error {
3823
+ status;
3824
+ constructor(message, status) {
3825
+ super(message);
3826
+ this.status = status;
3827
+ }
3828
+ };
3829
+ });
3830
+
3831
+ // packages/core/src/web-search/deepseek.ts
3832
+ function defaultSleep(ms, signal) {
3833
+ return new Promise((resolve, reject) => {
3834
+ if (signal?.aborted)
3835
+ return reject(signal.reason);
3836
+ const timer = setTimeout(resolve, ms);
3837
+ signal?.addEventListener("abort", () => {
3838
+ clearTimeout(timer);
3839
+ reject(signal.reason);
3840
+ }, { once: true });
3841
+ });
3842
+ }
3843
+ function apiMessage(text) {
3844
+ try {
3845
+ const data = JSON.parse(text);
3846
+ if (record(data) && record(data.error) && typeof data.error.message === "string")
3847
+ return truncate(data.error.message, 300);
3848
+ } catch {}
3849
+ return truncate(text, 300);
3850
+ }
3851
+ function httpError(status, text) {
3852
+ const guidance = "Check the key in /login or the Web settings, or switch the search provider.";
3853
+ if (status === 401)
3854
+ return new WebToolError(`DeepSeek API key is missing or invalid. ${guidance}`, status);
3855
+ if (status === 402)
3856
+ return new WebToolError("DeepSeek account balance is insufficient. Recharge, or switch the search provider to Tavily.", status);
3857
+ if (status === 403)
3858
+ return new WebToolError("DeepSeek blocked this request by its security policy.", status);
3859
+ if (status === 404)
3860
+ return new WebToolError("DeepSeek could not fetch a usable page for this URL.", status);
3861
+ if (status === 429)
3862
+ return new WebToolError("DeepSeek rate limit reached. Retry shortly.", status);
3863
+ if (status === 408 || status >= 500)
3864
+ return new WebToolError(`DeepSeek service error (${status}). Retry shortly.`, status);
3865
+ return new WebToolError(`DeepSeek request failed (${status}): ${apiMessage(text)}`, status);
3866
+ }
3867
+ function dateOnly(value) {
3868
+ return typeof value === "string" && /^\d{4}-\d{2}-\d{2}/.test(value) ? value.slice(0, 10) : undefined;
3869
+ }
3870
+ function createDeepseekProvider(resolveKey, fetchImpl = fetch, options = {}) {
3871
+ const sleep = options.sleep ?? defaultSleep;
3872
+ const requireKey = async () => {
3873
+ const key = await resolveKey();
3874
+ if (!key)
3875
+ throw new WebToolError(DEEPSEEK_MISSING_KEY);
3876
+ return key;
3877
+ };
3878
+ const post = (url, body, context) => postJson({
3879
+ label: "DeepSeek",
3880
+ url,
3881
+ body,
3882
+ fetchImpl,
3883
+ context,
3884
+ resolveKey: requireKey,
3885
+ headers: (key) => ({ authorization: `Bearer ${key}` }),
3886
+ httpError
3887
+ });
3888
+ return {
3889
+ id: "deepseek",
3890
+ async search(query, limit, context) {
3891
+ if (!query.trim())
3892
+ throw new WebToolError("Provide a non-empty query.");
3893
+ if ([...query].length > DEEPSEEK_QUERY_MAX_CHARS)
3894
+ throw new WebToolError(`DeepSeek search queries are limited to ${DEEPSEEK_QUERY_MAX_CHARS} characters. Shorten the query and retry.`);
3895
+ await requireKey();
3896
+ const data = await post(SEARCH_URL, { query, count: limit, need_content: false }, context);
3897
+ if (!record(data) || !Array.isArray(data.results) || !data.results.every(record))
3898
+ throw new WebToolError("Invalid DeepSeek response.");
3899
+ return data.results.slice(0, limit).map((item) => {
3900
+ const date = dateOnly(item.date);
3901
+ return {
3902
+ title: String(item.title ?? "(untitled)"),
3903
+ url: String(item.url ?? ""),
3904
+ text: String(item.summary || item.snippet || ""),
3905
+ ...date ? { date } : {}
3906
+ };
3907
+ });
3908
+ },
3909
+ async extract(urls, context) {
3910
+ await requireKey();
3911
+ const outcomes = new Array(urls.length);
3912
+ let next = 0;
3913
+ let fatal;
3914
+ const fetchOne = async (url) => {
3915
+ for (let attempt = 0;; attempt++) {
3916
+ try {
3917
+ const data = await post(FETCH_URL, { url }, context);
3918
+ if (!record(data) || typeof data.content !== "string")
3919
+ throw new WebToolError("Invalid DeepSeek response.");
3920
+ return { url, text: data.content };
3921
+ } catch (error) {
3922
+ const failure = error instanceof WebToolError ? error : new WebToolError("DeepSeek request failed.");
3923
+ if (context.abortSignal?.aborted || wholeCall(failure.status))
3924
+ throw failure;
3925
+ if (!retryable(failure.status) || attempt >= MAX_RETRIES)
3926
+ return { url, error: failure.message };
3927
+ try {
3928
+ await sleep(BACKOFF_MS[attempt] ?? 750, context.abortSignal);
3929
+ } catch {
3930
+ throw new WebToolError("Web request cancelled.");
3931
+ }
3932
+ }
3933
+ }
3934
+ };
3935
+ const worker = async () => {
3936
+ while (!fatal) {
3937
+ const index = next++;
3938
+ if (index >= urls.length)
3939
+ return;
3940
+ try {
3941
+ outcomes[index] = await fetchOne(urls[index]);
3942
+ } catch (error) {
3943
+ fatal ??= error instanceof WebToolError ? error : new WebToolError("DeepSeek request failed.");
3944
+ }
3945
+ }
3946
+ };
3947
+ await Promise.all(Array.from({ length: Math.min(FETCH_CONCURRENCY, urls.length) }, worker));
3948
+ if (fatal)
3949
+ throw fatal;
3950
+ return outcomes;
3951
+ }
3952
+ };
3953
+ }
3954
+ var SEARCH_URL = "https://api.deepseek.com/v1/tools/search", FETCH_URL = "https://api.deepseek.com/v1/tools/fetch", DEEPSEEK_QUERY_MAX_CHARS = 128, FETCH_CONCURRENCY = 3, MAX_RETRIES = 2, BACKOFF_MS, 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.", retryable = (status) => status !== undefined && (status === 429 || status === 408 || status >= 500), wholeCall = (status) => status === 401 || status === 402;
3955
+ var init_deepseek = __esm(() => {
3956
+ init_http();
3957
+ BACKOFF_MS = [250, 750];
3958
+ });
3959
+
3960
+ // packages/core/src/web-search/index.ts
3961
+ import { Type as Type4 } from "@earendil-works/pi-ai";
3962
+ import { defineExtension as defineExtension8, defineTool as defineTool4 } from "@earendil-works/pi-durable";
3501
3963
  function toolError(error) {
3502
3964
  return {
3503
3965
  output: [
3504
- { type: "text", text: error instanceof TavilyError ? error.message : "Web tool failed. Please retry." }
3966
+ { type: "text", text: error instanceof WebToolError ? error.message : "Web tool failed. Please retry." }
3505
3967
  ],
3506
3968
  isError: true
3507
3969
  };
3508
3970
  }
3509
- function truncate(text, max) {
3510
- return text.length > max ? `${text.slice(0, Math.max(0, max - 1))}\u2026` : text;
3511
- }
3512
- function rows(data, field) {
3513
- const value = data[field];
3514
- if (!Array.isArray(value) || !value.every(record))
3515
- throw new TavilyError("Invalid Tavily response.");
3516
- return value;
3517
- }
3518
3971
  function content(text) {
3519
3972
  return { output: [{ type: "text", text }] };
3520
3973
  }
3521
- function urlKey(value) {
3522
- const text = String(value);
3523
- try {
3524
- return new URL(text).href;
3525
- } catch {
3526
- return text;
3527
- }
3528
- }
3529
- function createWebSearchTools(resolveKey, fetchImpl = fetch) {
3974
+ function createWebSearchTools(resolveProvider) {
3530
3975
  return defineExtension8({
3531
3976
  name: "neko-web-search",
3532
3977
  tools: [
3533
3978
  defineTool4({
3534
3979
  name: "web_search",
3535
- 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.",
3980
+ 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.",
3536
3981
  parameters: Type4.Object({
3537
3982
  query: Type4.String({ minLength: 1, maxLength: 4096, description: "The search query" }),
3538
3983
  maxResults: Type4.Optional(Type4.Number({ description: `Maximum number of results (1-${MAX_RESULTS}, default ${DEFAULT_RESULTS})` }))
@@ -3540,18 +3985,19 @@ function createWebSearchTools(resolveKey, fetchImpl = fetch) {
3540
3985
  async execute({ query, maxResults }, _api, context) {
3541
3986
  try {
3542
3987
  if (!query.trim() || query.length > 4096)
3543
- throw new TavilyError("Provide a non-empty query of at most 4096 characters.");
3988
+ throw new WebToolError("Provide a non-empty query of at most 4096 characters.");
3544
3989
  if (maxResults !== undefined && !Number.isFinite(maxResults))
3545
- throw new TavilyError("maxResults must be a finite number.");
3990
+ throw new WebToolError("maxResults must be a finite number.");
3546
3991
  const limit = Math.max(1, Math.min(MAX_RESULTS, Math.trunc(maxResults ?? DEFAULT_RESULTS)));
3547
- const data = await tavilyPost(SEARCH_URL, { query, max_results: limit, search_depth: "basic" }, resolveKey, fetchImpl, context);
3548
- const results = rows(data, "results").slice(0, limit);
3992
+ const provider = await resolveProvider(context);
3993
+ const results = (await provider.search(query, limit, context)).slice(0, limit);
3549
3994
  const budget = Math.floor(MAX_TOTAL_CHARS / Math.max(1, results.length)) - 2;
3550
3995
  return content(results.map((item, index) => {
3551
- const header = `${index + 1}. ${truncate(String(item.title ?? "(untitled)"), 200)}
3552
- ${truncate(String(item.url ?? ""), 300)}
3553
- `;
3554
- return header + truncate(String(item.content ?? ""), Math.min(MAX_SOURCE_CHARS, budget - header.length));
3996
+ const header = `${index + 1}. ${truncate(item.title, 200)}
3997
+ ${truncate(item.url, 300)}
3998
+ ${item.date ? `Published: ${item.date}
3999
+ ` : ""}`;
4000
+ return header + truncate(item.text, Math.min(MAX_SOURCE_CHARS, budget - header.length));
3555
4001
  }).join(`
3556
4002
 
3557
4003
  `) || "No results.");
@@ -3573,28 +4019,25 @@ ${truncate(String(item.url ?? ""), 300)}
3573
4019
  async execute({ urls }, _api, context) {
3574
4020
  try {
3575
4021
  if (!urls.length || urls.length > MAX_URLS)
3576
- throw new TavilyError(`Provide between 1 and ${MAX_URLS} URLs.`);
4022
+ throw new WebToolError(`Provide between 1 and ${MAX_URLS} URLs.`);
3577
4023
  for (const url of urls) {
3578
4024
  let parsed;
3579
4025
  try {
3580
4026
  parsed = new URL(url);
3581
4027
  } catch {
3582
- throw new TavilyError("Provide valid http(s) URLs.");
4028
+ throw new WebToolError("Provide valid http(s) URLs.");
3583
4029
  }
3584
4030
  if (url.length > 2048 || !["http:", "https:"].includes(parsed.protocol) || parsed.username || parsed.password)
3585
- throw new TavilyError("Provide http(s) URLs of at most 2048 characters without embedded credentials.");
4031
+ throw new WebToolError("Provide http(s) URLs of at most 2048 characters without embedded credentials.");
3586
4032
  }
3587
- const data = await tavilyPost(EXTRACT_URL, { urls, format: "markdown" }, resolveKey, fetchImpl, context);
3588
- const results = rows(data, "results");
3589
- const failed = data.failed_results === undefined ? [] : rows(data, "failed_results");
3590
- const byUrl = (items) => new Map(items.map((item) => [urlKey(item.url), item]));
3591
- const successes = byUrl(results), failures = byUrl(failed);
4033
+ const provider = await resolveProvider(context);
4034
+ const outcomes = await provider.extract(urls, context);
3592
4035
  const budget = Math.floor(MAX_TOTAL_CHARS / urls.length) - 2;
3593
4036
  return content(urls.map((url, index) => {
3594
4037
  const header = `## ${index + 1}. ${truncate(url, 300)}
3595
4038
  `;
3596
- const result = successes.get(urlKey(url)), failure = failures.get(urlKey(url));
3597
- const text = result ? String(result.raw_content ?? "") : `Extraction failed: ${String(failure?.error ?? "No result returned for this URL")}`;
4039
+ const outcome = outcomes[index];
4040
+ const text = outcome && "text" in outcome ? outcome.text : `Extraction failed: ${outcome?.error ?? "No result returned for this URL"}`;
3598
4041
  return header + truncate(text, Math.min(MAX_SOURCE_CHARS, budget - header.length));
3599
4042
  }).join(`
3600
4043
 
@@ -3607,11 +4050,75 @@ ${truncate(String(item.url ?? ""), 300)}
3607
4050
  ]
3608
4051
  });
3609
4052
  }
3610
- var SEARCH_URL = "https://api.tavily.com/search", EXTRACT_URL = "https://api.tavily.com/extract", REQUEST_TIMEOUT_MS = 30000, MAX_RESPONSE_BYTES, MAX_URLS = 20, MAX_RESULTS = 10, DEFAULT_RESULTS = 5, MAX_SOURCE_CHARS = 2000, MAX_TOTAL_CHARS = 12000, TavilyError;
4053
+ var MAX_URLS = 20, MAX_RESULTS = 10, DEFAULT_RESULTS = 5, MAX_SOURCE_CHARS = 2000, MAX_TOTAL_CHARS = 12000;
3611
4054
  var init_web_search = __esm(() => {
3612
- MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
3613
- TavilyError = class TavilyError extends Error {
4055
+ init_http();
4056
+ init_http();
4057
+ });
4058
+
4059
+ // packages/core/src/web-search/tavily.ts
4060
+ function rows(data, field) {
4061
+ const value = data[field];
4062
+ if (!Array.isArray(value) || !value.every(record))
4063
+ throw new WebToolError("Invalid Tavily response.");
4064
+ return value;
4065
+ }
4066
+ function urlKey(value) {
4067
+ const text = String(value);
4068
+ try {
4069
+ return new URL(text).href;
4070
+ } catch {
4071
+ return text;
4072
+ }
4073
+ }
4074
+ function createTavilyProvider(resolveKey, fetchImpl = fetch) {
4075
+ const post = async (url, body, context) => {
4076
+ const data = await postJson({
4077
+ label: "Tavily",
4078
+ url,
4079
+ body,
4080
+ fetchImpl,
4081
+ context,
4082
+ resolveKey,
4083
+ headers: (key) => key ? { authorization: `Bearer ${key}` } : { "x-tavily-access-mode": "keyless" },
4084
+ 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)
4085
+ });
4086
+ if (!record(data))
4087
+ throw new WebToolError("Invalid Tavily response.");
4088
+ if (data.error)
4089
+ throw new WebToolError(`Tavily request failed: ${truncate(JSON.stringify(data.error), 500)}`);
4090
+ return data;
4091
+ };
4092
+ return {
4093
+ id: "tavily",
4094
+ async search(query, limit, context) {
4095
+ const data = await post(SEARCH_URL2, { query, max_results: limit, search_depth: "basic" }, context);
4096
+ return rows(data, "results").slice(0, limit).map((item) => ({
4097
+ title: String(item.title ?? "(untitled)"),
4098
+ url: String(item.url ?? ""),
4099
+ text: String(item.content ?? "")
4100
+ }));
4101
+ },
4102
+ async extract(urls, context) {
4103
+ const data = await post(EXTRACT_URL, { urls, format: "markdown" }, context);
4104
+ const results = rows(data, "results");
4105
+ const failed = data.failed_results === undefined ? [] : rows(data, "failed_results");
4106
+ const byUrl = (items) => new Map(items.map((item) => [urlKey(item.url), item]));
4107
+ const successes = byUrl(results);
4108
+ const failures = byUrl(failed);
4109
+ return urls.map((url) => {
4110
+ const result = successes.get(urlKey(url));
4111
+ if (result)
4112
+ return { url, text: String(result.raw_content ?? "") };
4113
+ const failure = failures.get(urlKey(url));
4114
+ return { url, error: String(failure?.error ?? "No result returned for this URL") };
4115
+ });
4116
+ }
3614
4117
  };
4118
+ }
4119
+ var SEARCH_URL2 = "https://api.tavily.com/search", EXTRACT_URL = "https://api.tavily.com/extract";
4120
+ var init_tavily = __esm(() => {
4121
+ init_http();
3615
4122
  });
3616
4123
 
3617
4124
  // packages/core/src/workspace-files.ts
@@ -4034,12 +4541,21 @@ async function createNekoRuntime(options = {}) {
4034
4541
  registry.install(createCodingTools({ images: await createNodePhotonImages() }));
4035
4542
  registry.install(ImageDetails);
4036
4543
  registry.install(AgentFileSearch);
4037
- registry.install(createWebSearchTools(async () => {
4544
+ const tavily = createTavilyProvider(async () => {
4038
4545
  const saved = await effectiveCredentials.read("tavily");
4039
4546
  if (saved?.type === "api_key" && saved.key)
4040
4547
  return saved.key;
4041
4548
  return process.env.TAVILY_API_KEY || undefined;
4042
- }));
4549
+ });
4550
+ const deepseekApiKey = async () => {
4551
+ const saved = await effectiveCredentials.read("deepseek");
4552
+ if (saved?.type === "api_key" && saved.key)
4553
+ return saved.key;
4554
+ return process.env.DEEPSEEK_API_KEY || undefined;
4555
+ };
4556
+ const deepseek = createDeepseekProvider(deepseekApiKey);
4557
+ let searchProvider = options.searchProvider ?? DEFAULT_SEARCH_PROVIDER;
4558
+ registry.install(createWebSearchTools(async () => searchProvider === "deepseek" ? deepseek : tavily));
4043
4559
  const loadResources = createResourceLoader(options.promptHome);
4044
4560
  const resourceSnapshots = createResourceSnapshots(loadResources, (id, requestId) => storage.submissionByRequest(id, requestId, context));
4045
4561
  registry.install(createNekoPrompt(resourceSnapshots.current));
@@ -4053,9 +4569,16 @@ async function createNekoRuntime(options = {}) {
4053
4569
  let approvals;
4054
4570
  const approvalExtension = createApprovalExtension(() => approvals);
4055
4571
  registry.install(approvalExtension);
4572
+ let savedContext = emptySavedContextSettings();
4573
+ let contextState = resolveContextSettings(options.contextSettings, savedContext);
4056
4574
  const harness = await Harness.open(storage, {
4057
4575
  models,
4058
4576
  registry,
4577
+ settings: {
4578
+ get compaction() {
4579
+ return compactionPolicy(contextState, models.getModels().map((model) => model.contextWindow));
4580
+ }
4581
+ },
4059
4582
  onReport: options.onReport,
4060
4583
  conversationCreated: async (tx, record) => {
4061
4584
  (await tx.doc(SessionMetadataDoc, record.id)).createdAt = Date.now();
@@ -4097,6 +4620,21 @@ async function createNekoRuntime(options = {}) {
4097
4620
  cwd: defaultCwd
4098
4621
  }
4099
4622
  });
4623
+ const savedSearch = (await harness.snapshot(SearchSettingsDoc, conversation.id, context))?.provider;
4624
+ if (isSearchProvider(savedSearch))
4625
+ searchProvider = savedSearch;
4626
+ savedContext = sanitizeSavedContextSettings(await harness.snapshot(ContextSettingsDoc, conversation.id, context));
4627
+ contextState = resolveContextSettings(options.contextSettings, savedContext);
4628
+ const persistContext = async (next) => {
4629
+ await harness.commit(async (tx) => {
4630
+ const doc = await tx.doc(ContextSettingsDoc, conversation.id);
4631
+ doc.compaction = next.compaction;
4632
+ doc.pruning = next.pruning;
4633
+ }, context);
4634
+ savedContext = next;
4635
+ contextState = resolveContextSettings(options.contextSettings, savedContext);
4636
+ return contextState;
4637
+ };
4100
4638
  const registerWorkspace = async (path) => {
4101
4639
  await harness.commit(async (tx) => {
4102
4640
  const doc = await tx.doc(WorkspacesDoc, conversation.id);
@@ -4133,10 +4671,22 @@ async function createNekoRuntime(options = {}) {
4133
4671
  for (const record of existing)
4134
4672
  if (!record.owner && !(await harness.snapshot(ArchivedDoc, record.id, context))?.archived)
4135
4673
  await metadata.restore(record.id);
4674
+ const pruner = new ContextPruner({
4675
+ harness,
4676
+ context,
4677
+ settings: () => contextState.effective.pruning,
4678
+ windowOf: async (id) => {
4679
+ const target = await harness.conversation(id, context);
4680
+ const model = (await target?.agent(context))?.model;
4681
+ return model && models.getModel(model.provider, model.modelId)?.contextWindow || 0;
4682
+ },
4683
+ report: (error) => options.onReport?.(error)
4684
+ });
4136
4685
  let closing;
4137
4686
  const cleanup = () => {
4138
4687
  if (!closing)
4139
4688
  closing = (async () => {
4689
+ pruner.close();
4140
4690
  jobs.shutdown();
4141
4691
  await workspaceFiles.close();
4142
4692
  await metadata.close();
@@ -4503,6 +5053,26 @@ async function createNekoRuntime(options = {}) {
4503
5053
  source: runtimeKeys.has(spec.id) ? "runtime" : savedConfigured ? "saved" : environmentConfigured ? "environment" : spec.keyless ? "keyless" : "none"
4504
5054
  };
4505
5055
  },
5056
+ async contextSettings() {
5057
+ return contextState;
5058
+ },
5059
+ async setContextSettings(patch) {
5060
+ return persistContext(applyContextPatch(savedContext, parseContextSettingsPatch(patch)));
5061
+ },
5062
+ async resetContextSettings() {
5063
+ return persistContext(emptySavedContextSettings());
5064
+ },
5065
+ async searchSettings() {
5066
+ return { selected: searchProvider };
5067
+ },
5068
+ async setSearchProvider(provider) {
5069
+ if (!isSearchProvider(provider))
5070
+ throw new Error("Unknown search provider.");
5071
+ await harness.commit(async (tx) => {
5072
+ (await tx.doc(SearchSettingsDoc, conversation.id)).provider = provider;
5073
+ }, context);
5074
+ searchProvider = provider;
5075
+ },
4506
5076
  async saveCredential(provider, key) {
4507
5077
  const spec = credentialProvider(provider);
4508
5078
  if (typeof key !== "string" || !key.trim() || key.length > 4096 || Array.from(key).some((char) => char.charCodeAt(0) < 32))
@@ -4576,10 +5146,12 @@ async function createNekoRuntime(options = {}) {
4576
5146
  }
4577
5147
  };
4578
5148
  }
4579
- var DEFAULT_MODEL, DEFAULT_STORAGE_PATH, ArchivedDoc, WorkspacesDoc, ActivityDoc;
5149
+ var DEFAULT_MODEL, DEFAULT_STORAGE_PATH, ArchivedDoc, WorkspacesDoc, SearchSettingsDoc, ContextSettingsDoc, ActivityDoc;
4580
5150
  var init_runtime = __esm(() => {
4581
5151
  init_agent_file_search();
4582
5152
  init_compaction();
5153
+ init_context_pruning();
5154
+ init_context_settings();
4583
5155
  init_credentials();
4584
5156
  init_image_metadata();
4585
5157
  init_image_prompt();
@@ -4591,11 +5163,14 @@ var init_runtime = __esm(() => {
4591
5163
  init_invoke();
4592
5164
  init_prompt();
4593
5165
  init_resources();
5166
+ init_search_providers();
4594
5167
  init_session_metadata();
4595
5168
  init_subagent_usage();
4596
5169
  init_subagents();
4597
5170
  init_tool_approvals();
5171
+ init_deepseek();
4598
5172
  init_web_search();
5173
+ init_tavily();
4599
5174
  init_workspace();
4600
5175
  init_workspace_files();
4601
5176
  DEFAULT_MODEL = { provider: "deepseek", modelId: "deepseek-flash" };
@@ -4616,6 +5191,22 @@ var init_runtime = __esm(() => {
4616
5191
  fork: "initial",
4617
5192
  initial: () => ({ items: [] })
4618
5193
  });
5194
+ SearchSettingsDoc = defineDoc5({
5195
+ kind: "app.neko.search-settings",
5196
+ version: 1,
5197
+ scope: "conversation",
5198
+ history: "latest",
5199
+ fork: "initial",
5200
+ initial: () => ({ provider: null })
5201
+ });
5202
+ ContextSettingsDoc = defineDoc5({
5203
+ kind: "app.neko.context-settings",
5204
+ version: 1,
5205
+ scope: "conversation",
5206
+ history: "latest",
5207
+ fork: "initial",
5208
+ initial: emptySavedContextSettings
5209
+ });
4619
5210
  ActivityDoc = defineDoc5({
4620
5211
  kind: "app.neko.activity",
4621
5212
  version: 1,
@@ -4641,12 +5232,14 @@ function mergeTranscriptItems(previous, incoming) {
4641
5232
  // packages/core/src/index.ts
4642
5233
  var init_src = __esm(() => {
4643
5234
  init_config();
5235
+ init_context_settings();
4644
5236
  init_credentials();
4645
5237
  init_images();
4646
5238
  init_jobs();
4647
5239
  init_projection();
4648
5240
  init_frontmatter();
4649
5241
  init_runtime();
5242
+ init_search_providers();
4650
5243
  init_session_metadata();
4651
5244
  init_subagent_usage();
4652
5245
  init_subagents();
@@ -4763,7 +5356,7 @@ function relativeTime(at, now, language = "en") {
4763
5356
  var sessionName = (session, empty = "New session") => session.title || session.firstPromptPreview || empty, messageTime = (session) => session.lastMessageAt ?? session.createdAt, byMessage = (a, b) => messageTime(b) - messageTime(a) || Number(b.id) - Number(a.id), byVisited = (a, b) => b.lastUsedAt - a.lastUsedAt || Number(b.id) - Number(a.id);
4764
5357
 
4765
5358
  // packages/gateway/src/contract.ts
4766
- import { method, remote } from "@earendil-works/chord";
5359
+ import { hostNamed, method, remote } from "@earendil-works/chord";
4767
5360
  import { Type as Type5 } from "typebox";
4768
5361
  function contract(name, def) {
4769
5362
  const variants = Object.fromEntries(Object.entries(def.variants).map(([key, value]) => [
@@ -4773,9 +5366,9 @@ function contract(name, def) {
4773
5366
  methods: Object.fromEntries(Object.entries(value.methods ?? {}).map(([member, [args, result]]) => [member, method(args, result)]))
4774
5367
  }
4775
5368
  ]));
4776
- return remote(name, { ...def, variants });
5369
+ return remote(name, { ...def, host: hostNamed(GATEWAY_HOST), variants });
4777
5370
  }
4778
- var rpc = (args, result) => [args, result], str, num, bool, nil, integer, nullableString, phase, level, thinking, model, change, usage, image, skill, call, DelegationSchema, SubagentSummarySchema, jobStatus, JobSummarySchema, JobOutputSchema, ChatViewSchema, SessionSummarySchema, DirectoryStateSchema, WorkspaceSummarySchema, WorkspaceStateSchema, credentialStatus, CredentialStateSchema, AttachmentSchema, chatInput, NekoSessionDirectory, NekoSessionManagement, resize, inputLimits, modelInfo, NekoModels, imageChunk, NekoSubagent, NekoChat, browse, page, NekoWorkspaces, provider, NekoCredentials, FileCandidatesSchema, NekoWorkspaceFiles, marketSkill, preview, scope, installResult, NekoSkillMarket, ApplicationInfoSchema, NekoApplicationInfo;
5371
+ var GATEWAY_HOST = "neko", rpc = (args, result) => [args, result], str, num, bool, nil, integer, nullableString, phase, level, thinking, model, change, usage, image, skill, call, DelegationSchema, SubagentSummarySchema, jobStatus, JobSummarySchema, JobOutputSchema, ChatViewSchema, SessionSummarySchema, DirectoryStateSchema, WorkspaceSummarySchema, WorkspaceStateSchema, credentialStatus, CredentialStateSchema, AttachmentSchema, chatInput, NekoSessionDirectory, NekoSessionManagement, resize, inputLimits, modelInfo, NekoModels, imageChunk, NekoSubagent, NekoChat, browse, page, NekoWorkspaces, provider, NekoCredentials, SearchSettingsStateSchema, NekoSearchSettings, contextSource, ContextSettingsStateSchema, nullable = (schema) => Type5.Union([schema, nil]), ContextSettingsPatchSchema, NekoContextSettings, FileCandidatesSchema, NekoWorkspaceFiles, marketSkill, preview, scope, installResult, NekoSkillMarket, ApplicationInfoSchema, NekoApplicationInfo;
4779
5372
  var init_contract = __esm(() => {
4780
5373
  str = Type5.String();
4781
5374
  num = Type5.Number();
@@ -5130,6 +5723,77 @@ var init_contract = __esm(() => {
5130
5723
  }
5131
5724
  }
5132
5725
  });
5726
+ SearchSettingsStateSchema = Type5.Object({
5727
+ revision: integer,
5728
+ selected: Type5.Enum(["tavily", "deepseek"])
5729
+ });
5730
+ NekoSearchSettings = contract("neko.search-settings", {
5731
+ version: 1,
5732
+ input: Type5.Object({}, { additionalProperties: false }),
5733
+ variants: {
5734
+ ready: {
5735
+ state: { state: SearchSettingsStateSchema },
5736
+ methods: {
5737
+ set: rpc(Type5.Object({ provider: Type5.Enum(["tavily", "deepseek"]) }, { additionalProperties: false }), nil)
5738
+ }
5739
+ }
5740
+ }
5741
+ });
5742
+ contextSource = Type5.Enum(["default", "config", "saved"]);
5743
+ ContextSettingsStateSchema = Type5.Object({
5744
+ revision: integer,
5745
+ effective: Type5.Object({
5746
+ compaction: Type5.Object({
5747
+ enabled: bool,
5748
+ triggerPercent: Type5.Union([integer, nil]),
5749
+ keepRecentTokens: integer
5750
+ }),
5751
+ pruning: Type5.Object({
5752
+ enabled: bool,
5753
+ triggerPercent: integer,
5754
+ keepRecent: integer,
5755
+ minChars: integer,
5756
+ minSavePercent: integer
5757
+ })
5758
+ }),
5759
+ sources: Type5.Object({
5760
+ compaction: Type5.Object({ enabled: contextSource, triggerPercent: contextSource, keepRecentTokens: contextSource }),
5761
+ pruning: Type5.Object({
5762
+ enabled: contextSource,
5763
+ triggerPercent: contextSource,
5764
+ keepRecent: contextSource,
5765
+ minChars: contextSource,
5766
+ minSavePercent: contextSource
5767
+ })
5768
+ })
5769
+ });
5770
+ ContextSettingsPatchSchema = Type5.Object({
5771
+ compaction: Type5.Optional(Type5.Object({
5772
+ enabled: Type5.Optional(nullable(bool)),
5773
+ triggerPercent: Type5.Optional(nullable(integer)),
5774
+ keepRecentTokens: Type5.Optional(nullable(integer))
5775
+ }, { additionalProperties: false })),
5776
+ pruning: Type5.Optional(Type5.Object({
5777
+ enabled: Type5.Optional(nullable(bool)),
5778
+ triggerPercent: Type5.Optional(nullable(integer)),
5779
+ keepRecent: Type5.Optional(nullable(integer)),
5780
+ minChars: Type5.Optional(nullable(integer)),
5781
+ minSavePercent: Type5.Optional(nullable(integer))
5782
+ }, { additionalProperties: false }))
5783
+ }, { additionalProperties: false });
5784
+ NekoContextSettings = contract("neko.context-settings", {
5785
+ version: 1,
5786
+ input: Type5.Object({}, { additionalProperties: false }),
5787
+ variants: {
5788
+ ready: {
5789
+ state: { state: ContextSettingsStateSchema },
5790
+ methods: {
5791
+ set: rpc(ContextSettingsPatchSchema, nil),
5792
+ reset: rpc(Type5.Object({}, { additionalProperties: false }), nil)
5793
+ }
5794
+ }
5795
+ }
5796
+ });
5133
5797
  FileCandidatesSchema = Type5.Object({
5134
5798
  entries: Type5.Array(Type5.Object({ name: str, path: str, kind: Type5.Enum(["file", "directory"]) }), { maxItems: 100 }),
5135
5799
  limited: bool,
@@ -5183,18 +5847,24 @@ var init_contract = __esm(() => {
5183
5847
  protocolVersion: Type5.Integer({ minimum: 1 }),
5184
5848
  latestVersion: nullableString,
5185
5849
  checkedAt: Type5.Union([integer, nil]),
5186
- checkState: Type5.Enum(["unknown", "checking", "ok", "failed", "development"])
5850
+ checkState: Type5.Enum(["unknown", "checking", "ok", "failed", "development"]),
5851
+ serverId: str
5187
5852
  });
5188
5853
  NekoApplicationInfo = contract("neko.application-info", {
5189
5854
  version: 1,
5190
5855
  input: Type5.Object({}),
5191
- variants: { ready: { state: { info: ApplicationInfoSchema }, methods: { check: rpc(nil, ApplicationInfoSchema) } } }
5856
+ variants: {
5857
+ ready: { state: { info: ApplicationInfoSchema }, methods: { check: rpc(nil, ApplicationInfoSchema) } }
5858
+ }
5192
5859
  });
5193
5860
  });
5194
5861
 
5195
5862
  // packages/gateway/src/link.ts
5196
- import { webSocketLink } from "@earendil-works/chord";
5197
- function boundedWebSocketLink(socket, max = MAX_LINK_BYTES) {
5863
+ import {
5864
+ createHost,
5865
+ webSocketLink
5866
+ } from "@earendil-works/chord";
5867
+ function boundedWebSocketLink(socket, max = MAX_LINK_BYTES, options) {
5198
5868
  let queued = 0, incoming = 0, receiving = false;
5199
5869
  let native;
5200
5870
  const bridge = {
@@ -5210,18 +5880,22 @@ function boundedWebSocketLink(socket, max = MAX_LINK_BYTES) {
5210
5880
  },
5211
5881
  close: (code, reason) => socket.close(code, reason),
5212
5882
  addEventListener(type, handler) {
5213
- if (type === "message")
5214
- socket.addEventListener("message", (event) => {
5215
- const bytes = size(String(event.data));
5883
+ if (type === "message") {
5884
+ const deliver = (data) => {
5885
+ const bytes = size(data);
5216
5886
  if (!receiving)
5217
5887
  incoming += bytes;
5218
5888
  if (bytes > max || incoming > max) {
5219
5889
  native.close("frame limit exceeded");
5220
5890
  return;
5221
5891
  }
5222
- handler(event);
5223
- });
5224
- else
5892
+ handler({ data });
5893
+ };
5894
+ if (socket.on)
5895
+ socket.on("message", (data, isBinary) => deliver(isBinary ? asBytes(data) : new TextDecoder().decode(asBytes(data))));
5896
+ else
5897
+ socket.addEventListener("message", (event) => deliver(typeof event.data === "string" || event.data instanceof Uint8Array ? event.data : String(event.data)));
5898
+ } else
5225
5899
  socket.addEventListener(type, () => {
5226
5900
  if (type === "open")
5227
5901
  queued = 0;
@@ -5233,7 +5907,7 @@ function boundedWebSocketLink(socket, max = MAX_LINK_BYTES) {
5233
5907
  });
5234
5908
  }
5235
5909
  };
5236
- native = webSocketLink(bridge);
5910
+ native = webSocketLink(bridge, options);
5237
5911
  socket.addEventListener("error", () => native.close("socket error"));
5238
5912
  return {
5239
5913
  get closed() {
@@ -5253,7 +5927,7 @@ function boundedWebSocketLink(socket, max = MAX_LINK_BYTES) {
5253
5927
  }
5254
5928
  if (socket.readyState === 0)
5255
5929
  queued += bytes;
5256
- native.send(frame);
5930
+ return native.send(frame);
5257
5931
  },
5258
5932
  receive(handler) {
5259
5933
  receiving = true;
@@ -5264,49 +5938,19 @@ function boundedWebSocketLink(socket, max = MAX_LINK_BYTES) {
5264
5938
  close: (reason) => native.close(reason)
5265
5939
  };
5266
5940
  }
5267
- function identifiedLink(link, expectedName) {
5268
- let receiver;
5269
- let seen = false, pendingBytes = 0;
5270
- const pending = [];
5271
- link.receive((frame) => {
5272
- if (!seen) {
5273
- if (frame.t !== "hello" || frame.name !== expectedName) {
5274
- link.close("serverId mismatch");
5275
- return;
5276
- }
5277
- seen = true;
5278
- }
5279
- if (receiver)
5280
- receiver(frame);
5281
- else {
5282
- pendingBytes += size(JSON.stringify(frame));
5283
- if (pendingBytes > MAX_LINK_BYTES) {
5284
- link.close("receive queue limit exceeded");
5285
- pending.length = 0;
5286
- return;
5287
- }
5288
- pending.push(frame);
5289
- }
5290
- });
5291
- return {
5292
- get closed() {
5293
- return link.closed;
5294
- },
5295
- send: (frame) => link.send(frame),
5296
- receive(handler) {
5297
- receiver = handler;
5298
- pendingBytes = 0;
5299
- for (const frame of pending.splice(0)) {
5300
- if (link.closed)
5301
- break;
5302
- handler(frame);
5303
- }
5941
+ function createClientHost(link, options = {}) {
5942
+ return createHost({
5943
+ kind: "client",
5944
+ self: { kind: "client" },
5945
+ upstream: {
5946
+ link,
5947
+ principal: { kind: "server" },
5948
+ ...options.onProtocolMismatch ? { onProtocolMismatch: options.onProtocolMismatch } : {}
5304
5949
  },
5305
- onClose: (handler) => link.onClose(handler),
5306
- close: (reason) => link.close(reason)
5307
- };
5950
+ onError: () => {}
5951
+ });
5308
5952
  }
5309
- var MAX_LINK_BYTES, size = (text) => new TextEncoder().encode(text).byteLength;
5953
+ var MAX_LINK_BYTES, asBytes = (data) => data instanceof Uint8Array ? data : new Uint8Array(data), size = (data) => typeof data === "string" ? new TextEncoder().encode(data).byteLength : data.byteLength;
5310
5954
  var init_link = __esm(() => {
5311
5955
  MAX_LINK_BYTES = 16 * 1024 * 1024;
5312
5956
  });
@@ -5494,35 +6138,14 @@ class ApprovalQueue {
5494
6138
  }
5495
6139
 
5496
6140
  // packages/gateway/src/client-session.ts
5497
- import { createHost } from "@earendil-works/chord";
5498
- function waitForValue(state, signal) {
5499
- return new Promise((resolve, reject) => {
5500
- let stop = () => {};
5501
- let settled = false;
5502
- const finish = (error, value) => {
5503
- if (settled)
5504
- return;
5505
- settled = true;
5506
- clearTimeout(timer);
5507
- stop();
5508
- signal?.removeEventListener("abort", abort);
5509
- if (error)
5510
- reject(error);
5511
- else
5512
- resolve(value);
5513
- };
5514
- const abort = () => finish(signal?.reason ?? new Error("Cancelled"));
5515
- const timer = setTimeout(() => finish(new Error("Timed out waiting for first snapshot")), 5000);
5516
- stop = state.subscribe((value) => {
5517
- if (value !== undefined)
5518
- finish(undefined, value);
5519
- });
5520
- if (settled)
5521
- stop();
5522
- signal?.addEventListener("abort", abort, { once: true });
5523
- if (signal?.aborted)
5524
- abort();
5525
- });
6141
+ async function assertGatewayIdentity(host, serverId, signal) {
6142
+ const application = await host.get(NekoApplicationInfo, {}, signal ? { signal } : {});
6143
+ try {
6144
+ if (application.info.value.serverId !== serverId)
6145
+ throw new GatewayIdentityError;
6146
+ } finally {
6147
+ application.release();
6148
+ }
5526
6149
  }
5527
6150
  function jobsClient(chat) {
5528
6151
  const supported = () => chat.view.value?.jobs !== undefined;
@@ -5546,16 +6169,37 @@ function jobsClient(chat) {
5546
6169
  };
5547
6170
  }
5548
6171
  function createGatewayConnection(link, serverId) {
5549
- const host = createHost({ kind: "client", upstream: identifiedLink(link, serverId), onError: () => {} });
5550
- const get = (type) => host.get(type, {}, { at: serverId });
6172
+ let protocolMismatch = false;
6173
+ const linkClosed = new AbortController;
6174
+ let closeReason = "";
6175
+ link.onClose((reason) => {
6176
+ closeReason = reason;
6177
+ linkClosed.abort(new Error(`Gateway connection closed: ${reason}`));
6178
+ });
6179
+ const host = createClientHost(link, {
6180
+ onProtocolMismatch: () => {
6181
+ protocolMismatch = true;
6182
+ }
6183
+ });
6184
+ const get = (type) => host.hold(type, {});
5551
6185
  const application = get(NekoApplicationInfo);
5552
6186
  const keep = [application.info.subscribe(() => {})];
5553
6187
  let business;
5554
6188
  let protocolChecked = false;
5555
6189
  function bindBusiness() {
5556
- const directory = get(NekoSessionDirectory), management = get(NekoSessionManagement), models = get(NekoModels), workspaces = get(NekoWorkspaces), workspaceFiles = get(NekoWorkspaceFiles), credentials = get(NekoCredentials), skillMarket = get(NekoSkillMarket);
5557
- keep.push(directory.state.subscribe(() => {}), workspaces.state.subscribe(() => {}), credentials.state.subscribe(() => {}));
5558
- return { directory, management, models, workspaces, workspaceFiles, credentials, skillMarket };
6190
+ const directory = get(NekoSessionDirectory), management = get(NekoSessionManagement), models = get(NekoModels), workspaces = get(NekoWorkspaces), workspaceFiles = get(NekoWorkspaceFiles), credentials = get(NekoCredentials), searchSettings = get(NekoSearchSettings), contextSettings = get(NekoContextSettings), skillMarket = get(NekoSkillMarket);
6191
+ keep.push(directory.state.subscribe(() => {}), workspaces.state.subscribe(() => {}), credentials.state.subscribe(() => {}), searchSettings.state.subscribe(() => {}), contextSettings.state.subscribe(() => {}));
6192
+ return {
6193
+ directory,
6194
+ management,
6195
+ models,
6196
+ workspaces,
6197
+ workspaceFiles,
6198
+ credentials,
6199
+ searchSettings,
6200
+ contextSettings,
6201
+ skillMarket
6202
+ };
5559
6203
  }
5560
6204
  const services = () => {
5561
6205
  if (!protocolChecked)
@@ -5564,6 +6208,37 @@ function createGatewayConnection(link, serverId) {
5564
6208
  business = bindBusiness();
5565
6209
  return business;
5566
6210
  };
6211
+ const checkReady = async (signal) => {
6212
+ await deadline(application.ready, signal);
6213
+ const info = application.info.value;
6214
+ if (info.serverId !== serverId)
6215
+ throw new GatewayIdentityError;
6216
+ if (info.protocolVersion !== APPLICATION_PROTOCOL)
6217
+ throw new ApplicationProtocolError;
6218
+ protocolChecked = true;
6219
+ const {
6220
+ directory,
6221
+ management,
6222
+ models,
6223
+ workspaces,
6224
+ credentials,
6225
+ searchSettings,
6226
+ contextSettings,
6227
+ workspaceFiles,
6228
+ skillMarket
6229
+ } = services();
6230
+ await deadline(Promise.all([
6231
+ directory.ready,
6232
+ management.ready,
6233
+ models.ready,
6234
+ workspaces.ready,
6235
+ credentials.ready,
6236
+ searchSettings.ready,
6237
+ contextSettings.ready,
6238
+ workspaceFiles.ready,
6239
+ skillMarket.ready
6240
+ ]), signal);
6241
+ };
5567
6242
  let chat;
5568
6243
  let attachment;
5569
6244
  let stopView;
@@ -5575,7 +6250,7 @@ function createGatewayConnection(link, serverId) {
5575
6250
  if (!attachment)
5576
6251
  throw new Error("No active session");
5577
6252
  const input = { ...attachment, ...typeof target === "string" ? { agentId: target } : { taskId: target } };
5578
- return subagentClient(() => host.get(NekoSubagent, input, { at: serverId }));
6253
+ return subagentClient(() => host.hold(NekoSubagent, input));
5579
6254
  },
5580
6255
  host,
5581
6256
  link,
@@ -5597,6 +6272,12 @@ function createGatewayConnection(link, serverId) {
5597
6272
  get credentials() {
5598
6273
  return services().credentials;
5599
6274
  },
6275
+ get searchSettings() {
6276
+ return services().searchSettings;
6277
+ },
6278
+ get contextSettings() {
6279
+ return services().contextSettings;
6280
+ },
5600
6281
  get skillMarket() {
5601
6282
  return services().skillMarket;
5602
6283
  },
@@ -5604,26 +6285,13 @@ function createGatewayConnection(link, serverId) {
5604
6285
  return attachment;
5605
6286
  },
5606
6287
  async ready(signal) {
5607
- await deadline(application.ready, signal);
5608
- const info = await waitForValue(application.info, signal);
5609
- if (info.protocolVersion !== APPLICATION_PROTOCOL)
5610
- throw new ApplicationProtocolError;
5611
- protocolChecked = true;
5612
- const { directory, management, models, workspaces, credentials, workspaceFiles, skillMarket } = services();
5613
- await deadline(Promise.all([
5614
- directory.ready,
5615
- management.ready,
5616
- models.ready,
5617
- workspaces.ready,
5618
- credentials.ready,
5619
- workspaceFiles.ready,
5620
- skillMarket.ready
5621
- ]), signal);
5622
- await Promise.all([
5623
- waitForValue(directory.state, signal),
5624
- waitForValue(workspaces.state, signal),
5625
- waitForValue(credentials.state, signal)
5626
- ]);
6288
+ try {
6289
+ await checkReady(signal ? AbortSignal.any([signal, linkClosed.signal]) : linkClosed.signal);
6290
+ } catch (error) {
6291
+ if (protocolMismatch || closeReason.startsWith("protocol"))
6292
+ throw new ApplicationProtocolError;
6293
+ throw error;
6294
+ }
5627
6295
  },
5628
6296
  async attach(sessionId, ctx) {
5629
6297
  const next = await connection.management.attach(sessionId, ctx);
@@ -5632,12 +6300,11 @@ function createGatewayConnection(link, serverId) {
5632
6300
  stopView?.();
5633
6301
  chat?.release();
5634
6302
  attachment = next;
5635
- const current = host.get(NekoChat, next, { at: serverId });
6303
+ const current = host.hold(NekoChat, next);
5636
6304
  chat = current;
5637
6305
  stopView = current.view.subscribe(() => {});
5638
6306
  try {
5639
6307
  await deadline(current.ready, ctx?.signal);
5640
- await waitForValue(current.view, ctx?.signal);
5641
6308
  if (current !== chat)
5642
6309
  throw new Error("Attachment changed");
5643
6310
  return current;
@@ -5697,11 +6364,17 @@ function deadline(work, signal) {
5697
6364
  signal?.removeEventListener("abort", abort);
5698
6365
  });
5699
6366
  }
5700
- var JOBS_UNSUPPORTED = "This gateway does not support background jobs. Restart the gateway to enable them.";
6367
+ var GatewayIdentityError, JOBS_UNSUPPORTED = "This gateway does not support background jobs. Restart the gateway to enable them.";
5701
6368
  var init_client_session = __esm(() => {
5702
6369
  init_application_version();
5703
6370
  init_contract();
5704
6371
  init_link();
6372
+ GatewayIdentityError = class GatewayIdentityError extends Error {
6373
+ constructor() {
6374
+ super("Gateway identity mismatch");
6375
+ this.name = "GatewayIdentityError";
6376
+ }
6377
+ };
5705
6378
  });
5706
6379
 
5707
6380
  // packages/tui/src/theme/palette.ts
@@ -17881,6 +18554,8 @@ var init_labels = __esm(() => {
17881
18554
  { name: "model", description: "Choose a model" },
17882
18555
  { name: "agents", description: "View agent tasks and activity" },
17883
18556
  { name: "jobs", description: "View background jobs" },
18557
+ { name: "search", description: "Choose the web search provider (Tavily/DeepSeek)" },
18558
+ { name: "context", description: "Tune context compaction and tool result pruning" },
17884
18559
  { name: "theme", description: "Choose color theme (auto/light/dark)" },
17885
18560
  { name: "think", description: "Set thinking level (/think high)" },
17886
18561
  { name: "fork", description: "Fork this session" },
@@ -19390,6 +20065,227 @@ var init_welcome = __esm(() => {
19390
20065
  CAT = ["|\\___/|", ") (", "=\\^_^ /=", ")---("];
19391
20066
  });
19392
20067
 
20068
+ // packages/tui/src/context-ui.ts
20069
+ import { matchesKey as matchesKey7 } from "@earendil-works/pi-tui";
20070
+ function format(kind, value) {
20071
+ if (value === null)
20072
+ return "Default";
20073
+ if (typeof value === "boolean")
20074
+ return value ? "On" : "Off";
20075
+ switch (kind) {
20076
+ case "percent":
20077
+ return `${value}%`;
20078
+ case "tokens":
20079
+ return value >= 1000 ? `${Math.round(value / 1000)}k tokens` : `${value} tokens`;
20080
+ case "chars":
20081
+ return value >= 1000 ? `${Math.round(value / 1000)}k chars` : `${value} chars`;
20082
+ default:
20083
+ return String(value);
20084
+ }
20085
+ }
20086
+ function read(state, row) {
20087
+ const effective = state.effective[row.group];
20088
+ const sources = state.sources[row.group];
20089
+ return { value: effective[row.field] ?? null, source: sources[row.field] ?? "default" };
20090
+ }
20091
+ function contextItems(state) {
20092
+ if (!state)
20093
+ return [];
20094
+ const items = [];
20095
+ let group = "";
20096
+ for (const row of CONTEXT_ROWS) {
20097
+ if (row.group !== group) {
20098
+ group = row.group;
20099
+ items.push({
20100
+ value: `header:${group}`,
20101
+ label: group === "compaction" ? "Compaction" : "Tool result pruning",
20102
+ header: true
20103
+ });
20104
+ }
20105
+ const { value, source } = read(state, row);
20106
+ const custom = source !== "default" && value !== null && !row.presets.includes(value);
20107
+ items.push({
20108
+ value: row.key,
20109
+ label: row.label,
20110
+ rightLabel: `${format(row.kind, value)} ${SOURCES[source]}${custom ? " \xB7 Custom" : ""}`,
20111
+ description: row.description
20112
+ });
20113
+ }
20114
+ items.push({
20115
+ value: RESET_VALUE,
20116
+ label: "Reset to defaults",
20117
+ description: "Clear every saved value. Config file values still apply."
20118
+ });
20119
+ return items;
20120
+ }
20121
+
20122
+ class PresetDetail {
20123
+ help = shortcutHints(["\u2191\u2193", "Select"], ["Enter", "Save"], ["Esc", "Back"]);
20124
+ #index = 0;
20125
+ #error = "";
20126
+ #saving = false;
20127
+ #options;
20128
+ #row;
20129
+ #client;
20130
+ #panel;
20131
+ #notify;
20132
+ #changed;
20133
+ constructor(row, client, panel, notify, changed) {
20134
+ this.#row = row;
20135
+ this.#client = client;
20136
+ this.#panel = panel;
20137
+ this.#notify = notify;
20138
+ this.#changed = changed;
20139
+ const state = client.contextSettings.state.value;
20140
+ const current = state ? read(state, row) : { value: null, source: "default" };
20141
+ const options = [null, ...row.presets];
20142
+ const custom = current.source !== "default" && current.value !== null && !row.presets.includes(current.value);
20143
+ if (custom)
20144
+ options.push(current.value);
20145
+ this.#options = options;
20146
+ this.#index = current.source === "saved" || custom ? Math.max(0, options.indexOf(current.value)) : 0;
20147
+ }
20148
+ lines(_width, height) {
20149
+ const state = this.#client.contextSettings.state.value;
20150
+ const current = state ? read(state, this.#row) : undefined;
20151
+ const head = [theme.accent(this.#row.label), theme.dim(this.#row.description)];
20152
+ if (this.#saving)
20153
+ head.push("Saving...");
20154
+ if (this.#error)
20155
+ head.push(theme.error(`Error: ${this.#error}`));
20156
+ const options = this.#options.map((option, i) => {
20157
+ const custom = option !== null && !this.#row.presets.includes(option);
20158
+ const text = `${format(this.#row.kind, option)}${custom ? " (Custom)" : ""}${option === null && current ? ` - now ${SOURCES[current.source]}` : ""}`;
20159
+ return i === this.#index ? theme.selection(`\u276F ${text}`) : ` ${text}`;
20160
+ });
20161
+ const count = Math.max(1, height - head.length);
20162
+ const start = Math.max(0, Math.min(this.#index - Math.floor(count / 2), options.length - count));
20163
+ return [...head, ...options.slice(start, start + count)];
20164
+ }
20165
+ handleKey(data) {
20166
+ if (this.#saving)
20167
+ return true;
20168
+ if (matchesKey7(data, "up"))
20169
+ this.#index = Math.max(0, this.#index - 1);
20170
+ else if (matchesKey7(data, "down"))
20171
+ this.#index = Math.min(this.#options.length - 1, this.#index + 1);
20172
+ else if (matchesKey7(data, "enter")) {
20173
+ const value = this.#options[this.#index] ?? null;
20174
+ this.#saving = true;
20175
+ this.#error = "";
20176
+ const patch = { [this.#row.group]: { [this.#row.field]: value } };
20177
+ this.#client.contextSettings.set(patch).then(() => {
20178
+ this.#saving = false;
20179
+ this.#notify(`${this.#row.label} set to ${format(this.#row.kind, value)}.`);
20180
+ this.#panel.popDetail();
20181
+ }, (error) => {
20182
+ this.#saving = false;
20183
+ this.#error = message(error);
20184
+ this.#changed();
20185
+ });
20186
+ } else
20187
+ return false;
20188
+ return true;
20189
+ }
20190
+ }
20191
+ function openContextPanel(panel, client, notify) {
20192
+ const items = () => contextItems(client.contextSettings.state.value);
20193
+ const stop = client.contextSettings.state.subscribe(() => panel.update(items()));
20194
+ panel.emptyText = CONTEXT_EMPTY;
20195
+ panel.show("Context settings", async () => items(), async (value) => {
20196
+ if (value === RESET_VALUE) {
20197
+ await client.contextSettings.reset({});
20198
+ notify("Context settings reset to defaults.");
20199
+ return;
20200
+ }
20201
+ const row = CONTEXT_ROWS.find((item) => item.key === value);
20202
+ if (!row)
20203
+ return;
20204
+ return new PresetDetail(row, client, panel, notify, () => panel.changed());
20205
+ });
20206
+ return stop;
20207
+ }
20208
+ var CONTEXT_EMPTY = "Context settings are unavailable.", CONTEXT_ROWS, SOURCES, RESET_VALUE = "reset", message = (error) => error instanceof Error ? error.message : String(error);
20209
+ var init_context_ui = __esm(() => {
20210
+ init_shortcut_hints();
20211
+ init_theme();
20212
+ CONTEXT_ROWS = [
20213
+ {
20214
+ key: "compaction.enabled",
20215
+ group: "compaction",
20216
+ field: "enabled",
20217
+ label: "Auto compaction",
20218
+ kind: "bool",
20219
+ description: "Compact automatically near the limit. /compact always works.",
20220
+ presets: [true, false]
20221
+ },
20222
+ {
20223
+ key: "compaction.triggerPercent",
20224
+ group: "compaction",
20225
+ field: "triggerPercent",
20226
+ label: "Compact at",
20227
+ kind: "percent",
20228
+ description: "Compact when context reaches this share of the smallest model window. Default is near the limit.",
20229
+ presets: [30, 40, 50, 60, 70, 80]
20230
+ },
20231
+ {
20232
+ key: "compaction.keepRecentTokens",
20233
+ group: "compaction",
20234
+ field: "keepRecentTokens",
20235
+ label: "Keep recent",
20236
+ kind: "tokens",
20237
+ description: "Recent context a compaction keeps verbatim.",
20238
+ presets: [1e4, 20000, 40000, 80000]
20239
+ },
20240
+ {
20241
+ key: "pruning.enabled",
20242
+ group: "pruning",
20243
+ field: "enabled",
20244
+ label: "Prune tool results",
20245
+ kind: "bool",
20246
+ description: "Replace old, large tool results with a short note in what the model sees. History is kept.",
20247
+ presets: [true, false]
20248
+ },
20249
+ {
20250
+ key: "pruning.triggerPercent",
20251
+ group: "pruning",
20252
+ field: "triggerPercent",
20253
+ label: "Prune at",
20254
+ kind: "percent",
20255
+ description: "Prune when context reaches this share of the model window. Each prune invalidates the prompt cache.",
20256
+ presets: [20, 30, 35, 45, 55]
20257
+ },
20258
+ {
20259
+ key: "pruning.keepRecent",
20260
+ group: "pruning",
20261
+ field: "keepRecent",
20262
+ label: "Keep recent results",
20263
+ kind: "count",
20264
+ description: "Newest tool results that are never pruned.",
20265
+ presets: [5, 10, 20, 40]
20266
+ },
20267
+ {
20268
+ key: "pruning.minChars",
20269
+ group: "pruning",
20270
+ field: "minChars",
20271
+ label: "Min result size",
20272
+ kind: "chars",
20273
+ description: "Results shorter than this are never pruned.",
20274
+ presets: [2000, 4000, 8000, 16000]
20275
+ },
20276
+ {
20277
+ key: "pruning.minSavePercent",
20278
+ group: "pruning",
20279
+ field: "minSavePercent",
20280
+ label: "Min release",
20281
+ kind: "percent",
20282
+ description: "A prune runs only if it frees at least this share of the model window. Each prune invalidates the prompt cache, so a larger share means fewer prunes.",
20283
+ presets: [2, 5, 10, 15]
20284
+ }
20285
+ ];
20286
+ SOURCES = { default: "Default", config: "Config", saved: "Saved" };
20287
+ });
20288
+
19393
20289
  // packages/tui/src/detect-theme.ts
19394
20290
  function detectColorFgBgMode(env = process.env) {
19395
20291
  const bg = env.COLORFGBG?.split(";").at(-1)?.trim();
@@ -19695,7 +20591,7 @@ class UnixLink {
19695
20591
  }
19696
20592
  send(frame) {
19697
20593
  try {
19698
- this.#native.send(frame);
20594
+ return this.#native.send(frame);
19699
20595
  } catch (error) {
19700
20596
  this.#native.close("pending byte limit exceeded");
19701
20597
  throw error;
@@ -19829,7 +20725,7 @@ function createWsListener(options) {
19829
20725
  server.on("error", (error) => options.onError?.(error));
19830
20726
  server.on("connection", (socket) => {
19831
20727
  socket.on("error", (error) => options.onError?.(error));
19832
- accept(boundedWebSocketLink(socket, options.maxPendingBytes));
20728
+ accept(boundedWebSocketLink(socket, options.maxPendingBytes, { acceptPacked: false }));
19833
20729
  });
19834
20730
  },
19835
20731
  async close() {
@@ -19879,6 +20775,25 @@ function isServerId(value) {
19879
20775
  return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
19880
20776
  }
19881
20777
 
20778
+ // packages/gateway/src/principal.ts
20779
+ function gatewayPrincipalKey(principal) {
20780
+ switch (principal.kind) {
20781
+ case "mobile":
20782
+ return `mobile:${principal.deviceId}:${principal.grantRevision}:${principal.connectionId}`;
20783
+ case "local":
20784
+ return `local:${principal.connectionId}`;
20785
+ case "host":
20786
+ return "host";
20787
+ }
20788
+ }
20789
+ function isMobilePrincipal(principal) {
20790
+ return typeof principal === "object" && principal !== null && principal.kind === "mobile";
20791
+ }
20792
+ var HOST_PRINCIPAL;
20793
+ var init_principal = __esm(() => {
20794
+ HOST_PRINCIPAL = { kind: "host" };
20795
+ });
20796
+
19882
20797
  // packages/gateway/src/mobile/store.ts
19883
20798
  import { randomBytes } from "crypto";
19884
20799
  import { chmod as chmod2, mkdir as mkdir5, readFile as readFile6, rename as rename3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
@@ -20189,20 +21104,8 @@ var init_store = __esm(() => {
20189
21104
 
20190
21105
  // packages/gateway/src/mobile/admin.ts
20191
21106
  import { createHash, timingSafeEqual } from "crypto";
20192
- import { ChordError, method as method2, remote as remote2 } from "@earendil-works/chord";
21107
+ import { ChordError } from "@earendil-works/chord";
20193
21108
  import { Type as Type6 } from "typebox";
20194
- function contract2(name, def) {
20195
- const variants = Object.fromEntries(Object.entries(def.variants).map(([key, value]) => [
20196
- key,
20197
- {
20198
- methods: Object.fromEntries(Object.entries(value.methods ?? {}).map(([member, [args, result]]) => [member, method2(args, result)]))
20199
- }
20200
- ]));
20201
- return remote2(name, { ...def, variants });
20202
- }
20203
- function isMobilePrincipal(principal) {
20204
- return typeof principal === "object" && principal !== null && principal.kind === "mobile";
20205
- }
20206
21109
  function digest(value) {
20207
21110
  return createHash("sha256").update(value, "utf8").digest();
20208
21111
  }
@@ -20231,7 +21134,7 @@ function createMobileAdminFactory(deps) {
20231
21134
  throw new ChordError("forbidden", "forbidden");
20232
21135
  };
20233
21136
  return {
20234
- kind: "admin",
21137
+ variant: "admin",
20235
21138
  async state() {
20236
21139
  await guard();
20237
21140
  return deps.mobile.adminState();
@@ -20269,6 +21172,8 @@ function createMobileAdminFactory(deps) {
20269
21172
  }
20270
21173
  var listenerStateSchema, adminDeviceSchema, NekoMobileAdmin;
20271
21174
  var init_admin = __esm(() => {
21175
+ init_contract();
21176
+ init_principal();
20272
21177
  init_store();
20273
21178
  listenerStateSchema = Type6.Union([
20274
21179
  Type6.Object({ status: Type6.Literal("disabled") }),
@@ -20290,54 +21195,42 @@ var init_admin = __esm(() => {
20290
21195
  lastSeenAt: Type6.Integer(),
20291
21196
  online: Type6.Boolean()
20292
21197
  });
20293
- NekoMobileAdmin = contract2("neko.mobile-admin", {
21198
+ NekoMobileAdmin = contract("neko.mobile-admin", {
20294
21199
  version: 1,
20295
21200
  input: Type6.Object({ token: Type6.String() }),
20296
21201
  variants: {
20297
21202
  admin: {
20298
21203
  methods: {
20299
- state: [
20300
- Type6.Null(),
20301
- Type6.Object({
20302
- state: listenerStateSchema,
20303
- candidates: Type6.Array(Type6.String()),
20304
- port: Type6.Integer({ minimum: 1, maximum: 65535 }),
20305
- certificate: Type6.Union([
20306
- Type6.Object({ fingerprint: Type6.String(), expiresAt: Type6.Integer() }),
20307
- Type6.Null()
20308
- ]),
20309
- devices: Type6.Array(adminDeviceSchema),
20310
- lastConnectedAddress: Type6.Union([Type6.String(), Type6.Null()]),
20311
- wsl: Type6.Optional(Type6.Union([
20312
- Type6.Object({ interop: Type6.Boolean(), error: Type6.Union([Type6.String(), Type6.Null()]) }),
20313
- Type6.Null()
20314
- ]))
20315
- })
20316
- ],
20317
- enable: [
20318
- Type6.Object({
20319
- address: Type6.String(),
20320
- port: Type6.Optional(Type6.Integer({ minimum: 1, maximum: 65535 }))
20321
- }),
20322
- Type6.Object({
20323
- status: Type6.Literal("listening"),
20324
- address: Type6.String(),
20325
- port: Type6.Integer({ minimum: 1, maximum: 65535 }),
20326
- bindAddress: Type6.Optional(Type6.String()),
20327
- mode: Type6.Optional(Type6.Union([Type6.Literal("direct"), Type6.Literal("wsl-relay")]))
20328
- })
20329
- ],
20330
- disable: [Type6.Null(), listenerStateSchema],
20331
- createPairing: [Type6.Null(), Type6.Object({ pairingText: Type6.String(), expiresAt: Type6.Integer() })],
20332
- cancelPairing: [Type6.Null(), Type6.Null()],
20333
- revoke: [
20334
- Type6.Object({ deviceId: Type6.String({ minLength: 1 }) }),
20335
- Type6.Object({ revoked: Type6.Boolean() })
20336
- ],
20337
- regenerateCertificate: [
20338
- Type6.Null(),
20339
- Type6.Object({ fingerprint: Type6.String(), expiresAt: Type6.Integer() })
20340
- ]
21204
+ state: rpc(Type6.Null(), Type6.Object({
21205
+ state: listenerStateSchema,
21206
+ candidates: Type6.Array(Type6.String()),
21207
+ port: Type6.Integer({ minimum: 1, maximum: 65535 }),
21208
+ certificate: Type6.Union([
21209
+ Type6.Object({ fingerprint: Type6.String(), expiresAt: Type6.Integer() }),
21210
+ Type6.Null()
21211
+ ]),
21212
+ devices: Type6.Array(adminDeviceSchema),
21213
+ lastConnectedAddress: Type6.Union([Type6.String(), Type6.Null()]),
21214
+ wsl: Type6.Optional(Type6.Union([
21215
+ Type6.Object({ interop: Type6.Boolean(), error: Type6.Union([Type6.String(), Type6.Null()]) }),
21216
+ Type6.Null()
21217
+ ]))
21218
+ })),
21219
+ enable: rpc(Type6.Object({
21220
+ address: Type6.String(),
21221
+ port: Type6.Optional(Type6.Integer({ minimum: 1, maximum: 65535 }))
21222
+ }), Type6.Object({
21223
+ status: Type6.Literal("listening"),
21224
+ address: Type6.String(),
21225
+ port: Type6.Integer({ minimum: 1, maximum: 65535 }),
21226
+ bindAddress: Type6.Optional(Type6.String()),
21227
+ mode: Type6.Optional(Type6.Union([Type6.Literal("direct"), Type6.Literal("wsl-relay")]))
21228
+ })),
21229
+ disable: rpc(Type6.Null(), listenerStateSchema),
21230
+ createPairing: rpc(Type6.Null(), Type6.Object({ pairingText: Type6.String(), expiresAt: Type6.Integer() })),
21231
+ cancelPairing: rpc(Type6.Null(), Type6.Null()),
21232
+ revoke: rpc(Type6.Object({ deviceId: Type6.String({ minLength: 1 }) }), Type6.Object({ revoked: Type6.Boolean() })),
21233
+ regenerateCertificate: rpc(Type6.Null(), Type6.Object({ fingerprint: Type6.String(), expiresAt: Type6.Integer() }))
20341
21234
  }
20342
21235
  }
20343
21236
  }
@@ -20401,7 +21294,7 @@ function buildHelloResultSchema() {
20401
21294
  })
20402
21295
  });
20403
21296
  }
20404
- var MOBILE_LIMITS, MOBILE_API_VERSIONS, MOBILE_CAPABILITIES, MOBILE_CLOSE_REVOKED = 4401, MOBILE_CLOSE_INCOMPATIBLE = 4402, MOBILE_CLOSE_SLOW_CONSUMER = 4408, MobileHelloParams, opt, str2, int, nullable = (schema) => Type7.Union([schema, Type7.Null()]), MobileContentRefSchema, MobileImageRefSchema, MobileToolItemSchema, MobileChatItemSchema, MobileStreamingSchema, MobileUsageSchema, MobileSubagentRoundSchema, MobileAgentSchema, MobileChatStatusSchema, MobileChatSnapshotSchema, MobileWorkspaceSchema, MobileSessionSummarySchema, MobileApprovalSchema, sessionListParams, sessionListResult, subscription, pageResult, pageParams, chunkResult, MOBILE_API_V1_METHODS, patchBody, MOBILE_API_V1_NOTIFICATIONS;
21297
+ var MOBILE_LIMITS, MOBILE_API_VERSIONS, MOBILE_CAPABILITIES, MOBILE_CLOSE_REVOKED = 4401, MOBILE_CLOSE_INCOMPATIBLE = 4402, MOBILE_CLOSE_SLOW_CONSUMER = 4408, MobileHelloParams, opt, str2, int, nullable2 = (schema) => Type7.Union([schema, Type7.Null()]), MobileContentRefSchema, MobileImageRefSchema, MobileToolItemSchema, MobileChatItemSchema, MobileStreamingSchema, MobileUsageSchema, MobileSubagentRoundSchema, MobileAgentSchema, MobileChatStatusSchema, MobileChatSnapshotSchema, MobileWorkspaceSchema, MobileSessionSummarySchema, MobileApprovalSchema, sessionListParams, sessionListResult, subscription, pageResult, pageParams, chunkResult, MOBILE_API_V1_METHODS, patchBody, MOBILE_API_V1_NOTIFICATIONS;
20405
21298
  var init_protocol = __esm(() => {
20406
21299
  init_application_version();
20407
21300
  MOBILE_LIMITS = {
@@ -20520,7 +21413,7 @@ var init_protocol = __esm(() => {
20520
21413
  approvalCount: int,
20521
21414
  readonly: Type7.Boolean(),
20522
21415
  createdAt: Type7.Number(),
20523
- endedAt: nullable(Type7.Number()),
21416
+ endedAt: nullable2(Type7.Number()),
20524
21417
  usage: opt(MobileUsageSchema)
20525
21418
  });
20526
21419
  MobileAgentSchema = Type7.Object({
@@ -20528,18 +21421,18 @@ var init_protocol = __esm(() => {
20528
21421
  role: str2,
20529
21422
  title: str2,
20530
21423
  status: Type7.Union([Type7.Literal("idle"), Type7.Literal("queued"), Type7.Literal("running")]),
20531
- activeRoundId: nullable(int),
21424
+ activeRoundId: nullable2(int),
20532
21425
  lastRoundId: int,
20533
21426
  roundCount: int,
20534
21427
  approvalCount: int
20535
21428
  });
20536
21429
  MobileChatStatusSchema = Type7.Object({
20537
21430
  busy: Type7.Boolean(),
20538
- status: nullable(str2),
21431
+ status: nullable2(str2),
20539
21432
  usage: MobileUsageSchema,
20540
21433
  taskUsage: opt(MobileUsageSchema),
20541
- contextUsage: opt(Type7.Object({ tokens: nullable(Type7.Number()), window: Type7.Number(), percent: nullable(Type7.Number()) })),
20542
- agent: Type7.Object({ model: nullable(str2), thinkingLevel: nullable(str2) }),
21434
+ contextUsage: opt(Type7.Object({ tokens: nullable2(Type7.Number()), window: Type7.Number(), percent: nullable2(Type7.Number()) })),
21435
+ agent: Type7.Object({ model: nullable2(str2), thinkingLevel: nullable2(str2) }),
20543
21436
  subagents: Type7.Array(MobileSubagentRoundSchema),
20544
21437
  agents: Type7.Array(MobileAgentSchema),
20545
21438
  approvalCount: int,
@@ -20549,8 +21442,8 @@ var init_protocol = __esm(() => {
20549
21442
  MobileChatSnapshotSchema = Type7.Object({
20550
21443
  ...MobileChatStatusSchema.properties,
20551
21444
  items: Type7.Array(MobileChatItemSchema),
20552
- historyBefore: nullable(str2),
20553
- streaming: nullable(MobileStreamingSchema)
21445
+ historyBefore: nullable2(str2),
21446
+ streaming: nullable2(MobileStreamingSchema)
20554
21447
  });
20555
21448
  MobileWorkspaceSchema = Type7.Object({ root: str2, name: str2 });
20556
21449
  MobileSessionSummarySchema = Type7.Object({
@@ -20583,7 +21476,7 @@ var init_protocol = __esm(() => {
20583
21476
  nextCursor: opt(str2)
20584
21477
  });
20585
21478
  subscription = { subscriptionId: Type7.String({ minLength: 1 }) };
20586
- pageResult = Type7.Object({ items: Type7.Array(MobileChatItemSchema), historyBefore: nullable(str2) });
21479
+ pageResult = Type7.Object({ items: Type7.Array(MobileChatItemSchema), historyBefore: nullable2(str2) });
20587
21480
  pageParams = Type7.Object({
20588
21481
  ...subscription,
20589
21482
  before: Type7.String({ minLength: 1 }),
@@ -20680,7 +21573,7 @@ var init_protocol = __esm(() => {
20680
21573
  seq: Type7.Integer({ minimum: 1 }),
20681
21574
  upserts: Type7.Array(MobileChatItemSchema),
20682
21575
  status: opt(MobileChatStatusSchema),
20683
- streaming: opt(nullable(MobileStreamingSchema)),
21576
+ streaming: opt(nullable2(MobileStreamingSchema)),
20684
21577
  streamingAppend: opt(Type7.Object({ text: opt(str2), thinking: opt(str2) }))
20685
21578
  };
20686
21579
  MOBILE_API_V1_NOTIFICATIONS = {
@@ -21183,7 +22076,7 @@ function chordToMobileCode(error) {
21183
22076
  return "internal";
21184
22077
  }
21185
22078
  }
21186
- async function remote3(work, invalidAs = "invalid_params") {
22079
+ async function remote2(work, invalidAs = "invalid_params") {
21187
22080
  try {
21188
22081
  return await work();
21189
22082
  } catch (error) {
@@ -21397,14 +22290,17 @@ class MobileAdapter {
21397
22290
  }
21398
22291
  }
21399
22292
  async workspacesList() {
21400
- const state = await waitForValue(this.services().workspaces.state);
22293
+ const { workspaces } = this.services();
22294
+ await workspaces.ready;
22295
+ const state = workspaces.state.value;
21401
22296
  return {
21402
22297
  workspaces: state.workspaces.map((workspace) => ({ root: workspace.path, name: baseName(workspace.path) }))
21403
22298
  };
21404
22299
  }
21405
22300
  async registeredWorkspaceRoots() {
21406
- const state = await waitForValue(this.services().workspaces.state);
21407
- return state.workspaces.map((workspace) => workspace.path);
22301
+ const { workspaces } = this.services();
22302
+ await workspaces.ready;
22303
+ return workspaces.state.value.workspaces.map((workspace) => workspace.path);
21408
22304
  }
21409
22305
  async sessionsList(params) {
21410
22306
  const roots = await this.registeredWorkspaceRoots();
@@ -21413,7 +22309,9 @@ class MobileAdapter {
21413
22309
  const offset = params.cursor === undefined ? 0 : Number.parseInt(params.cursor, 10);
21414
22310
  if (!Number.isInteger(offset) || offset < 0)
21415
22311
  throw new MobileMethodError("invalid_params", "invalid cursor");
21416
- const directory = await waitForValue(this.services().directory.state);
22312
+ const { directory: directoryRef } = this.services();
22313
+ await directoryRef.ready;
22314
+ const directory = directoryRef.state.value;
21417
22315
  let sessions = directory.sessions.filter((session) => session.workspaceRoot === params.workspace);
21418
22316
  if (params.query !== undefined) {
21419
22317
  const query = params.query.toLowerCase();
@@ -21439,7 +22337,7 @@ class MobileAdapter {
21439
22337
  if (!roots.includes(params.workspace))
21440
22338
  throw new MobileMethodError("invalid_params", `unknown workspace: ${params.workspace}`);
21441
22339
  this.assertAuthorized();
21442
- const created = await remote3(() => this.services().management.create({ cwd: params.workspace }));
22340
+ const created = await remote2(() => this.services().management.create({ cwd: params.workspace }));
21443
22341
  return {
21444
22342
  id: created.id,
21445
22343
  workspace: created.workspaceRoot,
@@ -21476,16 +22374,13 @@ class MobileAdapter {
21476
22374
  const generation = ++this.attachGeneration;
21477
22375
  this.closeSubagent();
21478
22376
  this.releaseChat();
21479
- const attachment = await remote3(() => services.management.attach(params.sessionId));
22377
+ const attachment = await remote2(() => services.management.attach(params.sessionId));
21480
22378
  if (generation !== this.attachGeneration || this.disposed)
21481
22379
  throw new MobileMethodError("gone", "superseded by a newer attach");
21482
22380
  const chat = services.openChat(attachment);
21483
22381
  const stopView = chat.view.subscribe(() => {});
21484
22382
  try {
21485
- await remote3(async () => {
21486
- await chat.ready;
21487
- await waitForValue(chat.view);
21488
- });
22383
+ await remote2(() => chat.ready);
21489
22384
  } catch (error) {
21490
22385
  stopView();
21491
22386
  chat.release();
@@ -21547,7 +22442,7 @@ class MobileAdapter {
21547
22442
  this.attachGeneration += 1;
21548
22443
  this.closeSubagent();
21549
22444
  this.releaseChat();
21550
- await remote3(() => this.services().management.detach(null)).catch(() => {
22445
+ await remote2(() => this.services().management.detach(null)).catch(() => {
21551
22446
  return;
21552
22447
  });
21553
22448
  return null;
@@ -21578,7 +22473,7 @@ class MobileAdapter {
21578
22473
  const state = this.requireChat(params);
21579
22474
  const busy = state.chat.view.value?.busy ?? false;
21580
22475
  this.assertAuthorized();
21581
- await remote3(() => state.chat.submit({
22476
+ await remote2(() => state.chat.submit({
21582
22477
  content: params.text,
21583
22478
  requestId: params.requestId,
21584
22479
  ...params.mode === "steer" ? { whenBusy: "steer" } : {}
@@ -21595,7 +22490,7 @@ class MobileAdapter {
21595
22490
  async chatAbort(params) {
21596
22491
  const state = this.requireChat(params);
21597
22492
  this.assertAuthorized();
21598
- await remote3(() => state.chat.abort(null));
22493
+ await remote2(() => state.chat.abort(null));
21599
22494
  return { accepted: true };
21600
22495
  }
21601
22496
  async approvalRecords(state) {
@@ -21674,7 +22569,7 @@ class MobileAdapter {
21674
22569
  this.assertAuthorized();
21675
22570
  const handle = openSubagent({ ...state.attachment, taskId: params.taskId });
21676
22571
  try {
21677
- const status = await remote3(() => handle.decideApproval({
22572
+ const status = await remote2(() => handle.decideApproval({
21678
22573
  id: params.id,
21679
22574
  version: params.version,
21680
22575
  decision: params.decision === "allow" ? "allowed" : "denied"
@@ -21754,7 +22649,7 @@ class MobileAdapter {
21754
22649
  return () => listeners.delete(listener);
21755
22650
  },
21756
22651
  older: async (before) => {
21757
- const page = await remote3(() => client.history({ before }));
22652
+ const page = await remote2(() => client.history({ before }));
21758
22653
  return { items: page.items, next: page.next };
21759
22654
  }
21760
22655
  };
@@ -21798,7 +22693,7 @@ class MobileAdapter {
21798
22693
  this.assertAuthorized();
21799
22694
  const handle = openSubagent({ ...chat.attachment, taskId: params.taskId });
21800
22695
  try {
21801
- await remote3(() => handle.stop({ taskId: params.taskId }));
22696
+ await remote2(() => handle.stop({ taskId: params.taskId }));
21802
22697
  } finally {
21803
22698
  handle.release();
21804
22699
  }
@@ -21812,9 +22707,9 @@ class MobileAdapter {
21812
22707
  const state = this.subagentState;
21813
22708
  if (state === undefined || state.agentId !== params.agentId || state.chatSubscriptionId !== chat.subscriptionId)
21814
22709
  throw new MobileMethodError("gone", "subagent view is no longer open");
21815
- chunk = await remote3(() => state.client.getImage(input));
22710
+ chunk = await remote2(() => state.client.getImage(input));
21816
22711
  } else {
21817
- chunk = await remote3(() => chat.chat.getImage(input));
22712
+ chunk = await remote2(() => chat.chat.getImage(input));
21818
22713
  }
21819
22714
  const bytes = Buffer.from(chunk.data, "base64").subarray(0, MOBILE_LIMITS.chunkBytes);
21820
22715
  const nextOffset = params.offset + bytes.length;
@@ -21864,7 +22759,6 @@ class MobileAdapter {
21864
22759
  var CLOSE_INVALID_JSON = 1003, CLOSE_PROTOCOL_ERROR = 1002, ACTIVE_ROUND, MobileMethodError;
21865
22760
  var init_adapter = __esm(() => {
21866
22761
  init_src();
21867
- init_client_session();
21868
22762
  init_projection2();
21869
22763
  init_protocol();
21870
22764
  ACTIVE_ROUND = new Set(["queued", "running"]);
@@ -39188,7 +40082,7 @@ var init_wsl = __esm(() => {
39188
40082
  import { randomUUID as randomUUID2 } from "crypto";
39189
40083
  import { createServer as createServer2 } from "https";
39190
40084
  import { hostname } from "os";
39191
- import { createHost as createHost2, memoryLinkPair } from "@earendil-works/chord";
40085
+ import { memoryLinkPair } from "@earendil-works/chord";
39192
40086
  import { WebSocketServer as WebSocketServer2 } from "ws";
39193
40087
  function defaultServerName() {
39194
40088
  const name = hostname().trim();
@@ -39220,9 +40114,6 @@ function describeRelayError(error, address, port, bindAddress) {
39220
40114
  return `Windows \u4E2D\u7EE7\u542F\u52A8\u5931\u8D25\uFF1A${error.message}`;
39221
40115
  }
39222
40116
  }
39223
- function isMobilePrincipal2(principal) {
39224
- return typeof principal === "object" && principal !== null && principal.kind === "mobile";
39225
- }
39226
40117
  function toMessage(error) {
39227
40118
  if (error instanceof Error) {
39228
40119
  const code = error.code;
@@ -39401,7 +40292,7 @@ class MobileGateway {
39401
40292
  await store.clearMobileAuthorizations();
39402
40293
  this.certificate = certificate;
39403
40294
  this.pairingValue?.invalidate();
39404
- await this.chord.invalidatePrincipal(isMobilePrincipal2);
40295
+ await this.chord.invalidatePrincipal(isMobilePrincipal);
39405
40296
  for (const connection of [...this.connections.values()])
39406
40297
  connection.close(MOBILE_CLOSE_REVOKED, "revoked");
39407
40298
  const current = this.stateValue;
@@ -39477,11 +40368,7 @@ class MobileGateway {
39477
40368
  store.rememberConnectedAddress(learned).catch((error) => this.onError?.(toError(error)));
39478
40369
  }
39479
40370
  }
39480
- const clientHost = createHost2({
39481
- kind: "client",
39482
- upstream: identifiedLink(clientEnd, this.serverId),
39483
- onError: () => {}
39484
- });
40371
+ const clientHost = createClientHost(clientEnd);
39485
40372
  let servicesValue;
39486
40373
  const adapter = new MobileAdapter({
39487
40374
  socket,
@@ -39493,11 +40380,11 @@ class MobileGateway {
39493
40380
  services: () => {
39494
40381
  if (servicesValue === undefined) {
39495
40382
  servicesValue = {
39496
- directory: clientHost.get(NekoSessionDirectory, {}, { at: this.serverId }),
39497
- management: clientHost.get(NekoSessionManagement, {}, { at: this.serverId }),
39498
- workspaces: clientHost.get(NekoWorkspaces, {}, { at: this.serverId }),
39499
- openChat: (attachment) => clientHost.get(NekoChat, attachment, { at: this.serverId }),
39500
- openSubagent: (input) => clientHost.get(NekoSubagent, input, { at: this.serverId })
40383
+ directory: clientHost.hold(NekoSessionDirectory, {}),
40384
+ management: clientHost.hold(NekoSessionManagement, {}),
40385
+ workspaces: clientHost.hold(NekoWorkspaces, {}),
40386
+ openChat: (attachment) => clientHost.hold(NekoChat, attachment),
40387
+ openSubagent: (input) => clientHost.hold(NekoSubagent, input)
39501
40388
  };
39502
40389
  }
39503
40390
  return servicesValue;
@@ -39521,10 +40408,13 @@ class MobileGateway {
39521
40408
  if (this.notificationCleanup !== undefined)
39522
40409
  return;
39523
40410
  const [serverEnd, clientEnd] = memoryLinkPair();
39524
- this.chord.accept(serverEnd, { principal: { connectionId: "mobile-notification-hub" }, kind: "client" });
39525
- const host = createHost2({ kind: "client", upstream: identifiedLink(clientEnd, this.serverId), onError: () => {} });
39526
- const directory = host.get(NekoSessionDirectory, {}, { at: this.serverId });
39527
- const workspaces = host.get(NekoWorkspaces, {}, { at: this.serverId });
40411
+ this.chord.accept(serverEnd, {
40412
+ principal: { kind: "local", connectionId: "mobile-notification-hub" },
40413
+ kind: "client"
40414
+ });
40415
+ const host = createClientHost(clientEnd);
40416
+ const directory = host.hold(NekoSessionDirectory, {});
40417
+ const workspaces = host.hold(NekoWorkspaces, {});
39528
40418
  const last = { directory: -1, workspaces: -1 };
39529
40419
  const unsubscribe = [
39530
40420
  directory.state.subscribe(() => {
@@ -39808,6 +40698,7 @@ var RELAY_PORT_RETRY_MS = 1e4, RELAY_PORT_RETRY_INTERVAL_MS = 1000, WSL_INTEROP_
39808
40698
  var init_listener = __esm(() => {
39809
40699
  init_contract();
39810
40700
  init_link();
40701
+ init_principal();
39811
40702
  init_adapter();
39812
40703
  init_auth();
39813
40704
  init_certificate();
@@ -39942,15 +40833,7 @@ var init_image_uploads = __esm(() => {
39942
40833
  });
39943
40834
 
39944
40835
  // packages/gateway/src/subagent-host.ts
39945
- import {
39946
- ChordError as ChordError3,
39947
- cell,
39948
- hostPrincipal,
39949
- listener,
39950
- local,
39951
- opaque,
39952
- resource
39953
- } from "@earendil-works/chord";
40836
+ import { ChordError as ChordError3, local, opaque } from "@earendil-works/chord";
39954
40837
  import { UsageDoc as UsageDoc3 } from "@earendil-works/pi-durable";
39955
40838
  import { Type as Type8 } from "typebox";
39956
40839
  async function openReader(runtime, childId, parentId, context, through, historyUsage) {
@@ -40077,31 +40960,27 @@ async function openReader(runtime, childId, parentId, context, through, historyU
40077
40960
  }
40078
40961
  function installSubagentHost(host, runtime, context) {
40079
40962
  host.serve(ChildProjection, (input, _principal, self2) => {
40080
- const { revision } = self2.state({ revision: cell(Type8.Integer(), 0) });
40081
- const { reader } = self2.resources({
40082
- reader: resource({
40083
- rev: 1,
40084
- async open() {
40085
- 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);
40086
- if (!entry)
40087
- throw new ChordError3("gone", "Subagent unavailable");
40088
- const r = await openReader(runtime, entry.childId, input.parentId, context, entry.historyThrough, entry.historyUsage);
40089
- try {
40090
- const initial = await r.refresh();
40091
- await host.root((ctx) => revision.replace(ctx, initial));
40092
- return r;
40093
- } catch (error) {
40094
- r.close();
40095
- throw error;
40096
- }
40097
- },
40098
- close: (r) => r.close()
40099
- })
40963
+ const revision = self2.state("revision", Type8.Integer(), 0);
40964
+ const reader = self2.resource("reader", {
40965
+ version: 1,
40966
+ async open() {
40967
+ 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);
40968
+ if (!entry)
40969
+ throw new ChordError3("gone", "Subagent unavailable");
40970
+ const r = await openReader(runtime, entry.childId, input.parentId, context, entry.historyThrough, entry.historyUsage);
40971
+ try {
40972
+ const initial = await r.refresh();
40973
+ await host.root((ctx) => revision.replace(ctx, initial));
40974
+ return r;
40975
+ } catch (error) {
40976
+ r.close();
40977
+ throw error;
40978
+ }
40979
+ },
40980
+ close: (r) => r.close()
40100
40981
  });
40101
- self2.listen({
40102
- view: listener(reader, (r) => r, async (_event, ctx) => {
40103
- revision.replace(ctx, await reader.value.refresh());
40104
- })
40982
+ self2.on("view", reader, (r) => r, async (_event, ctx) => {
40983
+ revision.replace(ctx, await reader.value.refresh());
40105
40984
  });
40106
40985
  return { revision, reader: self2.expose(reader) };
40107
40986
  });
@@ -40117,23 +40996,24 @@ function installSubagentHost(host, runtime, context) {
40117
40996
  throw new ChordError3("forbidden", "Subagent does not belong to this session");
40118
40997
  return entry;
40119
40998
  };
40120
- const { target } = self2.resources({ target: resource({ rev: 1, deps: [owner], open: check }) });
40121
- const shared = self2.get(ChildProjection, { parentId: Number(input.sessionId), agentId: input.agentId ?? `round-${input.taskId}` }, { principal: hostPrincipal });
40122
- const { authorizedRevision } = self2.state({ authorizedRevision: cell(Type8.Integer(), 0) });
40123
- self2.listen({
40124
- authorizedView: listener(shared.revision, async (value, ctx) => {
40125
- try {
40126
- target.value;
40127
- await check();
40128
- if (value)
40129
- authorizedRevision.replace(ctx, value);
40130
- } catch {
40131
- authorizedRevision.replace(ctx, 0);
40132
- }
40133
- })
40999
+ const target = self2.resource("target", { version: 1, deps: [owner], open: check });
41000
+ const shared = self2.get(ChildProjection, {
41001
+ parentId: Number(input.sessionId),
41002
+ agentId: input.agentId ?? `round-${input.taskId}`
41003
+ });
41004
+ const authorizedRevision = self2.state("authorizedRevision", Type8.Integer(), 0);
41005
+ self2.on("authorizedView", shared.revision, async (value, ctx) => {
41006
+ try {
41007
+ target.value;
41008
+ await check();
41009
+ if (value)
41010
+ authorizedRevision.replace(ctx, value);
41011
+ } catch {
41012
+ authorizedRevision.replace(ctx, 0);
41013
+ }
40134
41014
  });
40135
41015
  return {
40136
- kind: "ready",
41016
+ variant: "ready",
40137
41017
  revision: authorizedRevision,
40138
41018
  chunk: async (request) => {
40139
41019
  await check();
@@ -40193,12 +41073,8 @@ import { open as open3 } from "fs/promises";
40193
41073
  import { basename as basename5, extname } from "path";
40194
41074
  import {
40195
41075
  ChordError as ChordError4,
40196
- cell as cell2,
40197
- hostPrincipal as hostPrincipal2,
40198
- listener as listener2,
40199
41076
  local as local2,
40200
- opaque as opaque2,
40201
- resource as resource2
41077
+ opaque as opaque2
40202
41078
  } from "@earendil-works/chord";
40203
41079
  import { withAbortSignal as withAbortSignal4 } from "@earendil-works/chord/context";
40204
41080
  import { UsageDoc as UsageDoc4 } from "@earendil-works/pi-durable";
@@ -40292,65 +41168,61 @@ async function openProjection(runtime, session, context) {
40292
41168
  function serveSessions(host, runtime, context) {
40293
41169
  installSubagentHost(host, runtime, context);
40294
41170
  host.serve(Owner, (_input, _principal, self2) => {
40295
- const { owner } = self2.resources({
40296
- owner: resource2({
40297
- rev: 1,
40298
- open: () => ({ active: null }),
40299
- close: (value) => {
40300
- value.active = null;
40301
- }
40302
- })
41171
+ const owner = self2.resource("owner", {
41172
+ version: 1,
41173
+ open: () => ({ active: null }),
41174
+ close: (value) => {
41175
+ value.active = null;
41176
+ }
40303
41177
  });
40304
41178
  return { owner: self2.expose(owner) };
40305
41179
  });
40306
41180
  host.serve(Projection, (input, _principal, self2) => {
40307
- const { view } = self2.state({ view: cell2(ChatViewSchema, emptyView) });
40308
- const { projection } = self2.resources({
40309
- projection: resource2({
40310
- rev: 1,
40311
- async open({ signal }) {
40312
- const session = await runtime.getConversation(Number(input.id));
40313
- if (!session)
40314
- throw new ChordError4("gone", "Session unavailable");
40315
- const opened = await openProjection(runtime, session, context);
40316
- try {
40317
- const initial = await opened.project();
40318
- await host.root((ctx) => view.replace(ctx, initial), { signal });
40319
- return opened;
40320
- } catch (error) {
40321
- opened.close();
40322
- throw error;
40323
- }
40324
- },
40325
- close: (value) => value.close()
40326
- })
41181
+ const view = self2.state("view", ChatViewSchema, emptyView);
41182
+ const projection = self2.resource("projection", {
41183
+ version: 1,
41184
+ async open({ signal }) {
41185
+ const session = await runtime.getConversation(Number(input.id));
41186
+ if (!session)
41187
+ throw new ChordError4("gone", "Session unavailable");
41188
+ const opened = await openProjection(runtime, session, context);
41189
+ try {
41190
+ const initial = await opened.project();
41191
+ await host.root((ctx) => view.replace(ctx, initial), { signal });
41192
+ return opened;
41193
+ } catch (error) {
41194
+ opened.close();
41195
+ throw error;
41196
+ }
41197
+ },
41198
+ close: (value) => value.close()
40327
41199
  });
40328
- self2.listen({
40329
- view: listener2(projection, (value) => value, async (_event, ctx) => {
40330
- view.replace(ctx, await projection.value.project());
40331
- })
41200
+ self2.on("view", projection, (value) => value, async (_event, ctx) => {
41201
+ view.replace(ctx, await projection.value.project());
40332
41202
  });
40333
41203
  return { view, reader: self2.expose(projection) };
40334
41204
  });
40335
41205
  host.serve(NekoChat, (input, principal, self2) => {
40336
41206
  const owner = self2.get(Owner, {}, { principal });
40337
- const shared = self2.get(Projection, { id: input.sessionId }, { principal: hostPrincipal2 });
40338
- const { uploads, projection } = self2.resources({
40339
- uploads: resource2({ rev: 1, open: () => new ImageUploads, close: (value) => value.close() }),
40340
- projection: resource2({
40341
- rev: 1,
40342
- deps: [owner],
40343
- async open() {
40344
- if (owner.owner?.active?.attachmentId !== input.attachmentId || owner.owner.active.sessionId !== input.sessionId)
40345
- throw new ChordError4("forbidden", "Attachment does not belong to this connection");
40346
- const session = await runtime.getConversation(Number(input.sessionId));
40347
- if (!session)
40348
- throw new ChordError4("gone", "Session unavailable");
40349
- return {
40350
- session
40351
- };
40352
- }
40353
- })
41207
+ const shared = self2.get(Projection, { id: input.sessionId });
41208
+ const uploads = self2.resource("uploads", {
41209
+ version: 1,
41210
+ open: () => new ImageUploads,
41211
+ close: (value) => value.close()
41212
+ });
41213
+ const projection = self2.resource("projection", {
41214
+ version: 1,
41215
+ deps: [owner],
41216
+ async open() {
41217
+ if (owner.owner?.active?.attachmentId !== input.attachmentId || owner.owner.active.sessionId !== input.sessionId)
41218
+ throw new ChordError4("forbidden", "Attachment does not belong to this connection");
41219
+ const session = await runtime.getConversation(Number(input.sessionId));
41220
+ if (!session)
41221
+ throw new ChordError4("gone", "Session unavailable");
41222
+ return {
41223
+ session
41224
+ };
41225
+ }
40354
41226
  });
40355
41227
  const safeUpload = (action) => {
40356
41228
  try {
@@ -40360,7 +41232,7 @@ function serveSessions(host, runtime, context) {
40360
41232
  }
40361
41233
  };
40362
41234
  const behavior = {
40363
- kind: "ready",
41235
+ variant: "ready",
40364
41236
  view: shared.view,
40365
41237
  draftImage: async ({ id, offset }) => safeUpload(() => uploads.value.read(id, offset)),
40366
41238
  importImage: async (path, ctx) => {
@@ -41224,20 +42096,14 @@ var init_workspaces = __esm(() => {
41224
42096
 
41225
42097
  // packages/gateway/src/server-services.ts
41226
42098
  import { randomUUID as randomUUID4 } from "crypto";
41227
- import {
41228
- ChordError as ChordError5,
41229
- cell as cell3,
41230
- hostPrincipal as hostPrincipal3,
41231
- listener as listener3,
41232
- local as local3,
41233
- method as method3
41234
- } from "@earendil-works/chord";
42099
+ import { ChordError as ChordError5, local as local3, method as method2 } from "@earendil-works/chord";
41235
42100
  import { Type as Type10 } from "typebox";
41236
42101
  async function createNekoServerServices(options) {
41237
42102
  const { runtime, context, host } = options;
41238
42103
  const updates = new UpdateChecker(options.updates);
41239
42104
  await updates.local();
41240
42105
  const updateListeners = new Set;
42106
+ const info = (value) => ({ ...value, serverId: options.serverId });
41241
42107
  let published = JSON.stringify(updates.state), closed = false;
41242
42108
  const publishUpdates = () => {
41243
42109
  if (closed)
@@ -41250,22 +42116,20 @@ async function createNekoServerServices(options) {
41250
42116
  listener(updates.state);
41251
42117
  };
41252
42118
  host.serve(NekoApplicationInfo, (_input, _principal, self2) => {
41253
- const state = self2.state({ info: cell3(ApplicationInfoSchema, updates.state) });
41254
- self2.listen({
41255
- updates: listener3({
41256
- subscribe: (fn) => {
41257
- updateListeners.add(fn);
41258
- return () => updateListeners.delete(fn);
41259
- }
41260
- }, (value, ctx) => state.info.replace(ctx, value))
41261
- });
42119
+ const current = self2.state("info", ApplicationInfoSchema, info(updates.state));
42120
+ self2.on("updates", {
42121
+ subscribe: (fn) => {
42122
+ updateListeners.add(fn);
42123
+ return () => updateListeners.delete(fn);
42124
+ }
42125
+ }, (value, ctx) => current.replace(ctx, info(value)));
41262
42126
  return {
41263
- kind: "ready",
41264
- info: state.info,
42127
+ variant: "ready",
42128
+ info: current,
41265
42129
  check: async () => {
41266
42130
  await updates.check(true);
41267
42131
  publishUpdates();
41268
- return updates.state;
42132
+ return info(updates.state);
41269
42133
  }
41270
42134
  };
41271
42135
  });
@@ -41309,7 +42173,13 @@ async function createNekoServerServices(options) {
41309
42173
  status: info.status
41310
42174
  }));
41311
42175
  const readStatuses = async () => Object.fromEntries(await Promise.all(CREDENTIAL_PROVIDERS.map(async (spec) => [spec.id, await runtime.credentialStatus(spec.id)])));
41312
- const [sessions, workspaces, statuses] = await Promise.all([list(), runtime.listWorkspaces(), readStatuses()]);
42176
+ const [sessions, workspaces, statuses, search, contextSettings0] = await Promise.all([
42177
+ list(),
42178
+ runtime.listWorkspaces(),
42179
+ readStatuses(),
42180
+ runtime.searchSettings(),
42181
+ runtime.contextSettings()
42182
+ ]);
41313
42183
  let mutationTail = Promise.resolve();
41314
42184
  const serialize = (fn) => {
41315
42185
  const result = mutationTail.catch(() => {}).then(fn);
@@ -41317,43 +42187,64 @@ async function createNekoServerServices(options) {
41317
42187
  return result;
41318
42188
  };
41319
42189
  host.serve(ServerState, (_input, _principal, self2) => {
41320
- const state = self2.state({
41321
- directory: cell3(DirectoryStateSchema, { revision: 1, sessions }),
41322
- workspaces: cell3(WorkspaceStateSchema, { revision: 1, workspaces }),
41323
- credentials: cell3(CredentialStateSchema, { revision: 1, statuses })
42190
+ const directory = self2.state("directory", DirectoryStateSchema, { revision: 1, sessions });
42191
+ const workspaceState = self2.state("workspaces", WorkspaceStateSchema, { revision: 1, workspaces });
42192
+ const credentials = self2.state("credentials", CredentialStateSchema, { revision: 1, statuses });
42193
+ const searchSettings = self2.state("searchSettings", SearchSettingsStateSchema, { revision: 1, ...search });
42194
+ const contextSettings = self2.state("contextSettings", ContextSettingsStateSchema, {
42195
+ revision: 1,
42196
+ ...contextSettings0
41324
42197
  });
41325
- self2.listen({
41326
- directory: listener3({ subscribe: runtime.harness.subscribeCommits.bind(runtime.harness) }, async (event, ctx) => {
41327
- if (!event.changes.some((change) => change.type === "conversation" || change.type === "document" && [
41328
- "app.neko.session-metadata",
41329
- "app.neko.activity",
41330
- "app.neko.conversation",
41331
- "app.neko.workspaces",
41332
- "pi.agent"
41333
- ].includes(change.record.kind)))
41334
- return;
41335
- await serialize(async () => {
41336
- const [items, roots] = await Promise.all([list(), runtime.listWorkspaces()]);
41337
- state.directory.replace(ctx, { revision: state.directory.value.revision + 1, sessions: items });
41338
- state.workspaces.replace(ctx, { revision: state.workspaces.value.revision + 1, workspaces: roots });
41339
- });
41340
- })
42198
+ const refreshContextSettings = async (_input, ctx) => {
42199
+ const next = await runtime.contextSettings();
42200
+ const { revision, ...current } = contextSettings.value;
42201
+ if (JSON.stringify(next) !== JSON.stringify(current))
42202
+ contextSettings.replace(ctx, { revision: revision + 1, ...next });
42203
+ return null;
42204
+ };
42205
+ const refreshSearchSettings = async (_input, ctx) => {
42206
+ const next = await runtime.searchSettings();
42207
+ const current = searchSettings.value;
42208
+ if (next.selected !== current.selected)
42209
+ searchSettings.replace(ctx, { revision: current.revision + 1, ...next });
42210
+ return null;
42211
+ };
42212
+ self2.on("directory", { subscribe: runtime.harness.subscribeCommits.bind(runtime.harness) }, async (event, ctx) => {
42213
+ if (!event.changes.some((change) => change.type === "conversation" || change.type === "document" && [
42214
+ "app.neko.session-metadata",
42215
+ "app.neko.activity",
42216
+ "app.neko.conversation",
42217
+ "app.neko.workspaces",
42218
+ "pi.agent"
42219
+ ].includes(change.record.kind)))
42220
+ return;
42221
+ await serialize(async () => {
42222
+ const [items, roots] = await Promise.all([list(), runtime.listWorkspaces()]);
42223
+ directory.replace(ctx, { revision: directory.value.revision + 1, sessions: items });
42224
+ workspaceState.replace(ctx, { revision: workspaceState.value.revision + 1, workspaces: roots });
42225
+ });
41341
42226
  });
41342
42227
  return {
41343
- ...state,
42228
+ directory,
42229
+ workspaces: workspaceState,
42230
+ credentials,
42231
+ searchSettings,
42232
+ contextSettings,
41344
42233
  async refreshCredentials(_input, ctx) {
41345
- state.credentials.replace(ctx, {
41346
- revision: state.credentials.value.revision + 1,
42234
+ credentials.replace(ctx, {
42235
+ revision: credentials.value.revision + 1,
41347
42236
  statuses: await readStatuses()
41348
42237
  });
41349
42238
  return null;
41350
- }
42239
+ },
42240
+ refreshSearchSettings,
42241
+ refreshContextSettings
41351
42242
  };
41352
- }, { eager: { input: {} } });
41353
- const shared = (self2) => self2.get(ServerState, {}, { principal: hostPrincipal3 });
41354
- host.serve(NekoSessionDirectory, (_input, _principal, self2) => ({ kind: "ready", state: shared(self2).directory }));
42243
+ }, { lifetime: "host" });
42244
+ const shared = (self2) => self2.get(ServerState, {});
42245
+ host.serve(NekoSessionDirectory, (_input, _principal, self2) => ({ variant: "ready", state: shared(self2).directory }));
41355
42246
  host.serve(NekoWorkspaces, (_input, _principal, self2) => ({
41356
- kind: "ready",
42247
+ variant: "ready",
41357
42248
  state: shared(self2).workspaces,
41358
42249
  browse: (input) => browseWorkspaces(input),
41359
42250
  add: (input) => serialize(() => runtime.addWorkspace(input.path))
@@ -41361,7 +42252,7 @@ async function createNekoServerServices(options) {
41361
42252
  host.serve(NekoCredentials, (_input, _principal, self2) => {
41362
42253
  const hub = shared(self2);
41363
42254
  return {
41364
- kind: "ready",
42255
+ variant: "ready",
41365
42256
  state: hub.credentials,
41366
42257
  save: async (input, ctx) => {
41367
42258
  await runtime.saveCredential(input.provider, input.key);
@@ -41373,8 +42264,34 @@ async function createNekoServerServices(options) {
41373
42264
  }
41374
42265
  };
41375
42266
  });
42267
+ host.serve(NekoSearchSettings, (_input, _principal, self2) => {
42268
+ const hub = shared(self2);
42269
+ return {
42270
+ variant: "ready",
42271
+ state: hub.searchSettings,
42272
+ set: (input, ctx) => serialize(async () => {
42273
+ await runtime.setSearchProvider(input.provider);
42274
+ return hub.refreshSearchSettings(null, ctx);
42275
+ })
42276
+ };
42277
+ });
42278
+ host.serve(NekoContextSettings, (_input, _principal, self2) => {
42279
+ const hub = shared(self2);
42280
+ return {
42281
+ variant: "ready",
42282
+ state: hub.contextSettings,
42283
+ set: (input, ctx) => serialize(async () => {
42284
+ await runtime.setContextSettings(input);
42285
+ return hub.refreshContextSettings(null, ctx);
42286
+ }),
42287
+ reset: (_input, ctx) => serialize(async () => {
42288
+ await runtime.resetContextSettings();
42289
+ return hub.refreshContextSettings(null, ctx);
42290
+ })
42291
+ };
42292
+ });
41376
42293
  host.serve(NekoModels, () => ({
41377
- kind: "ready",
42294
+ variant: "ready",
41378
42295
  list: async () => [...runtime.listModels()].map((value) => ({
41379
42296
  ...value,
41380
42297
  input: [...value.input],
@@ -41382,7 +42299,7 @@ async function createNekoServerServices(options) {
41382
42299
  }))
41383
42300
  }));
41384
42301
  host.serve(NekoWorkspaceFiles, () => ({
41385
- kind: "ready",
42302
+ variant: "ready",
41386
42303
  search: async (input, ctx) => {
41387
42304
  const roots = await runtime.listWorkspaces();
41388
42305
  if (!roots.some((root) => root.path === input.workspaceRoot))
@@ -41400,7 +42317,7 @@ async function createNekoServerServices(options) {
41400
42317
  return found;
41401
42318
  };
41402
42319
  return {
41403
- kind: "ready",
42320
+ variant: "ready",
41404
42321
  create: (input) => serialize(async () => summary((await runtime.createConversation(input)).id)),
41405
42322
  fork: (id) => serialize(async () => summary((await runtime.forkConversation(toConversationId(id))).id)),
41406
42323
  archive: async (id) => {
@@ -41423,7 +42340,7 @@ async function createNekoServerServices(options) {
41423
42340
  };
41424
42341
  });
41425
42342
  host.serve(NekoSkillMarket, () => ({
41426
- kind: "ready",
42343
+ variant: "ready",
41427
42344
  list: (input) => market.list(input.query).then((skills) => Promise.all(skills.map((skill) => withInstallState(skill, input.workspaceRoot)))).catch(toRemote),
41428
42345
  detail: (input) => market.detail(input.name).then((skill) => withInstallState(skill, input.workspaceRoot)).catch(toRemote),
41429
42346
  preview: (input) => market.preview(input.name).then((value) => ({ ...value, files: [...value.files] })).catch(toRemote),
@@ -41465,7 +42382,11 @@ var init_server_services = __esm(() => {
41465
42382
  directory: DirectoryStateSchema,
41466
42383
  workspaces: WorkspaceStateSchema,
41467
42384
  credentials: CredentialStateSchema,
41468
- refreshCredentials: method3(Type10.Null(), Type10.Null())
42385
+ searchSettings: SearchSettingsStateSchema,
42386
+ contextSettings: ContextSettingsStateSchema,
42387
+ refreshCredentials: method2(Type10.Null(), Type10.Null()),
42388
+ refreshSearchSettings: method2(Type10.Null(), Type10.Null()),
42389
+ refreshContextSettings: method2(Type10.Null(), Type10.Null())
41469
42390
  }
41470
42391
  });
41471
42392
  });
@@ -41474,7 +42395,7 @@ var init_server_services = __esm(() => {
41474
42395
  import { randomUUID as randomUUID5 } from "crypto";
41475
42396
  import { mkdir as mkdir7, readFile as readFile8, writeFile as writeFile6 } from "fs/promises";
41476
42397
  import { basename as basename6, dirname as dirname10, join as join14 } from "path";
41477
- import { createHost as createHost3 } from "@earendil-works/chord";
42398
+ import { createHost as createHost2 } from "@earendil-works/chord";
41478
42399
  import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT4 } from "@earendil-works/chord/context";
41479
42400
  async function startGateway(options = {}) {
41480
42401
  const context = options.context ?? BACKGROUND_CONTEXT4;
@@ -41483,6 +42404,7 @@ async function startGateway(options = {}) {
41483
42404
  const storagePath = options.storagePath ?? fileConfig.storagePath ?? DEFAULT_STORAGE_PATH;
41484
42405
  const host = options.host ?? fileConfig.gateway?.host ?? DEFAULT_HOST;
41485
42406
  const port = options.port !== undefined ? options.port : fileConfig.gateway?.port ?? (process.platform === "win32" ? 0 : DEFAULT_PORT);
42407
+ const searchProvider = options.searchProvider ?? fileConfig.webSearch?.provider;
41486
42408
  const marketUrl = options.marketUrl ?? fileConfig.skills?.marketUrl ?? DEFAULT_SKILL_MARKET_URL;
41487
42409
  const nekoDir = dirname10(storagePath);
41488
42410
  const serverId = options.serverId ?? await loadOrCreateServerId(nekoDir);
@@ -41494,6 +42416,8 @@ async function startGateway(options = {}) {
41494
42416
  ...options.storage !== "memory" ? { storagePath } : {},
41495
42417
  ...options.credentialsPath !== undefined ? { credentialsPath: options.credentialsPath } : {},
41496
42418
  ...options.apiKey !== undefined ? { apiKey: options.apiKey } : {},
42419
+ ...searchProvider !== undefined ? { searchProvider } : {},
42420
+ ...fileConfig.context !== undefined ? { contextSettings: fileConfig.context } : {},
41497
42421
  model,
41498
42422
  thinkingLevel: options.thinkingLevel ?? fileConfig.thinkingLevel ?? "high",
41499
42423
  ...options.cwd !== undefined ? { cwd: options.cwd } : {},
@@ -41524,8 +42448,9 @@ async function startGateway(options = {}) {
41524
42448
  }
41525
42449
  async function bindGateway(runtime, options) {
41526
42450
  const { context, host, port, socketPath, serverId, nekoDir } = options;
41527
- const chord = createHost3({
41528
- name: serverId,
42451
+ const chord = createHost2({
42452
+ name: GATEWAY_HOST,
42453
+ self: HOST_PRINCIPAL,
41529
42454
  dev: true,
41530
42455
  onError: (error) => options.onError?.(error instanceof Error ? error : new Error(String(error))),
41531
42456
  principalKey: gatewayPrincipalKey
@@ -41534,6 +42459,7 @@ async function bindGateway(runtime, options) {
41534
42459
  host: chord,
41535
42460
  runtime,
41536
42461
  context,
42462
+ serverId,
41537
42463
  marketUrl: options.marketUrl,
41538
42464
  ...options.updates ? { updates: options.updates } : {},
41539
42465
  ...options.updateIntervalMs ? { updateIntervalMs: options.updateIntervalMs } : {},
@@ -41549,7 +42475,10 @@ async function bindGateway(runtime, options) {
41549
42475
  }
41550
42476
  try {
41551
42477
  for (const listener of listeners)
41552
- await listener.start((link) => chord.accept(link, { principal: { connectionId: randomUUID5() } }));
42478
+ await listener.start((link) => chord.accept(link, {
42479
+ principal: { kind: "local", connectionId: randomUUID5() },
42480
+ onProtocolMismatch: ({ peer }) => options.onError?.(new Error(`Rejected a client on Chord protocol ${peer ?? "unknown"}; restart or update it.`))
42481
+ }));
41553
42482
  } catch (error) {
41554
42483
  await Promise.allSettled(listeners.map((listener) => listener.close()));
41555
42484
  await chord.close();
@@ -41612,16 +42541,6 @@ async function bindGateway(runtime, options) {
41612
42541
  }).catch((error) => options.onError?.(error instanceof Error ? error : new Error(String(error))));
41613
42542
  return gateway;
41614
42543
  }
41615
- function gatewayPrincipalKey(principal) {
41616
- if (typeof principal === "object" && principal !== null) {
41617
- const record = principal;
41618
- if (record.kind === "mobile" && typeof record.deviceId === "string" && typeof record.grantRevision === "number") {
41619
- const connectionId = typeof record.connectionId === "string" ? record.connectionId : "";
41620
- return `mobile:${record.deviceId}:${record.grantRevision}:${connectionId}`;
41621
- }
41622
- }
41623
- return JSON.stringify(principal) ?? "unknown";
41624
- }
41625
42544
  function toStartupError(error, info) {
41626
42545
  const message = error instanceof Error ? error.message : String(error);
41627
42546
  const existingServerId = info.socketPath === null ? undefined : serverIdFromSocketPath(info.socketPath);
@@ -41661,9 +42580,11 @@ async function loadOrCreateServerId(nekoDir) {
41661
42580
  var DEFAULT_HOST = "127.0.0.1", DEFAULT_PORT = 8321, SERVER_ID_FILENAME = "gateway-server-id";
41662
42581
  var init_gateway = __esm(() => {
41663
42582
  init_src();
42583
+ init_contract();
41664
42584
  init_admin();
41665
42585
  init_listener();
41666
42586
  init_port_file();
42587
+ init_principal();
41667
42588
  init_server_services();
41668
42589
  init_unix_link();
41669
42590
  init_ws_listener();
@@ -41768,6 +42689,7 @@ var DEFAULT_SHUTDOWN_TIMEOUT_MS = 5000;
41768
42689
  // packages/gateway/src/index.ts
41769
42690
  var init_src2 = __esm(() => {
41770
42691
  init_client();
42692
+ init_client_session();
41771
42693
  init_contract();
41772
42694
  init_gateway();
41773
42695
  init_link();
@@ -41838,7 +42760,7 @@ async function connectSpawned(plan, child, outputTail, spawnError) {
41838
42760
  const timeoutMs = plan.spawnTimeoutMs ?? 20000;
41839
42761
  if (plan.platform === "win32") {
41840
42762
  const timeoutAt = Date.now() + timeoutMs;
41841
- const serverId = plan.fileServerId ?? await waitForValue2(() => readServerId(join15(plan.nekoDir, SERVER_ID_FILENAME2)), "gateway serverId file", timeoutMs, child, outputTail, spawnError);
42763
+ const serverId = plan.fileServerId ?? await waitForValue(() => readServerId(join15(plan.nekoDir, SERVER_ID_FILENAME2)), "gateway serverId file", timeoutMs, child, outputTail, spawnError);
41842
42764
  let lastError = new Error("Gateway port file unavailable");
41843
42765
  while (Date.now() < timeoutAt) {
41844
42766
  if (child.exitCode !== null || child.signalCode !== null || spawnError() !== undefined) {
@@ -41875,10 +42797,10 @@ async function connectSpawned(plan, child, outputTail, spawnError) {
41875
42797
  }
41876
42798
  let socketPath = plan.socketPath;
41877
42799
  if (socketPath === undefined) {
41878
- const generated = await waitForValue2(() => readServerId(join15(plan.nekoDir, SERVER_ID_FILENAME2)), "gateway serverId file", timeoutMs, child, outputTail, spawnError);
42800
+ const generated = await waitForValue(() => readServerId(join15(plan.nekoDir, SERVER_ID_FILENAME2)), "gateway serverId file", timeoutMs, child, outputTail, spawnError);
41879
42801
  socketPath = join15(plan.nekoDir, `${generated}.sock`);
41880
42802
  }
41881
- await waitForValue2(() => existsSync3(socketPath) ? true : undefined, `gateway ready (socket: ${socketPath})`, timeoutMs, child, outputTail, spawnError);
42803
+ await waitForValue(() => existsSync3(socketPath) ? true : undefined, `gateway ready (socket: ${socketPath})`, timeoutMs, child, outputTail, spawnError);
41882
42804
  const serverId = plan.fileServerId ?? serverIdFromSocketPath2(socketPath) ?? readServerId(join15(plan.nekoDir, SERVER_ID_FILENAME2));
41883
42805
  if (serverId === undefined) {
41884
42806
  throw new Error(`Cannot determine gateway serverId (socket: ${socketPath})`);
@@ -41975,7 +42897,7 @@ function captureOutput(child) {
41975
42897
  return () => [stdout.trimEnd(), stderr.trimEnd()].filter((part) => part !== "").join(`
41976
42898
  `);
41977
42899
  }
41978
- async function waitForValue2(poll, what, timeoutMs, child, outputTail, spawnError) {
42900
+ async function waitForValue(poll, what, timeoutMs, child, outputTail, spawnError) {
41979
42901
  const start = Date.now();
41980
42902
  for (;; ) {
41981
42903
  const error = spawnError();
@@ -42010,7 +42932,7 @@ var init_gateway2 = __esm(() => {
42010
42932
  });
42011
42933
 
42012
42934
  // packages/tui/src/help-ui.ts
42013
- import { getKeybindings, matchesKey as matchesKey7, TUI_KEYBINDINGS, wrapTextWithAnsi as wrapTextWithAnsi4 } from "@earendil-works/pi-tui";
42935
+ import { getKeybindings, matchesKey as matchesKey8, TUI_KEYBINDINGS, wrapTextWithAnsi as wrapTextWithAnsi4 } from "@earendil-works/pi-tui";
42014
42936
  function helpLines(platform = process.platform) {
42015
42937
  const rows = [];
42016
42938
  const section = (title, hints) => {
@@ -42055,6 +42977,15 @@ function helpLines(platform = process.platform) {
42055
42977
  ["End", "Jump to the newest output"],
42056
42978
  ["\u2190", "Back"]
42057
42979
  ]);
42980
+ section("Web search", [
42981
+ ["/search", "Choose the web search provider"],
42982
+ ["Enter", "Apply the highlighted provider"]
42983
+ ]);
42984
+ section("Context", [
42985
+ ["/context", "Tune compaction and tool result pruning"],
42986
+ ["Enter", "Choose a setting, then a value to save"],
42987
+ ["Esc", "Back"]
42988
+ ]);
42058
42989
  section("Skills", [
42059
42990
  ["U", "Install for user"],
42060
42991
  ["P", "Install for project"],
@@ -42107,13 +43038,13 @@ class HelpDetail {
42107
43038
  this.#offset = Math.max(0, Math.min(this.#max, this.#offset + delta));
42108
43039
  }
42109
43040
  handleKey(data) {
42110
- if (matchesKey7(data, "up"))
43041
+ if (matchesKey8(data, "up"))
42111
43042
  this.scroll(-1);
42112
- else if (matchesKey7(data, "down"))
43043
+ else if (matchesKey8(data, "down"))
42113
43044
  this.scroll(1);
42114
- else if (matchesKey7(data, "pageUp"))
43045
+ else if (matchesKey8(data, "pageUp"))
42115
43046
  this.scroll(-this.#page);
42116
- else if (matchesKey7(data, "pageDown"))
43047
+ else if (matchesKey8(data, "pageDown"))
42117
43048
  this.scroll(this.#page);
42118
43049
  else
42119
43050
  return false;
@@ -42140,7 +43071,7 @@ import { readFile as readFile9, stat as stat5 } from "fs/promises";
42140
43071
  import { homedir as homedir8 } from "os";
42141
43072
  import { basename as basename8, resolve as resolve5 } from "path";
42142
43073
  import { promisify as promisify2 } from "util";
42143
- import { isKeyRelease as isKeyRelease4, isKeyRepeat as isKeyRepeat2, matchesKey as matchesKey8 } from "@earendil-works/pi-tui";
43074
+ import { isKeyRelease as isKeyRelease4, isKeyRepeat as isKeyRepeat2, matchesKey as matchesKey9 } from "@earendil-works/pi-tui";
42144
43075
  function imagePaths(text, platform = process.platform) {
42145
43076
  const paths = [];
42146
43077
  let word = "", quote = "", escaped = false;
@@ -42234,7 +43165,7 @@ end try`]).catch(() => ""));
42234
43165
  return { text: String(await run(tool, wayland ? ["--no-newline"] : ["-selection", "clipboard", "-o"])) };
42235
43166
  }
42236
43167
  function isClipboardShortcut(data, platform = process.platform) {
42237
- return !isKeyRelease4(data) && !isKeyRepeat2(data) && matchesKey8(data, platform === "win32" ? "alt+v" : "ctrl+v");
43168
+ return !isKeyRelease4(data) && !isKeyRepeat2(data) && matchesKey9(data, platform === "win32" ? "alt+v" : "ctrl+v");
42238
43169
  }
42239
43170
  var exec;
42240
43171
  var init_image_input = __esm(() => {
@@ -42279,9 +43210,55 @@ function createAppLayout(parts) {
42279
43210
  var usableSize = (width, height) => width >= 40 && height >= 10;
42280
43211
  var init_layout = () => {};
42281
43212
 
43213
+ // packages/tui/src/search-ui.ts
43214
+ function searchItems(settings, credentials) {
43215
+ if (!settings)
43216
+ return [];
43217
+ const source = (provider) => {
43218
+ const status = credentials?.statuses[provider];
43219
+ return status ? SOURCES2[status.source] : "loading";
43220
+ };
43221
+ const deepseekMissing = credentials?.statuses.deepseek.source === "none";
43222
+ const descriptions = {
43223
+ tavily: `Tavily search (${source("tavily")}). Default`,
43224
+ deepseek: deepseekMissing ? "No DeepSeek key. Run /login to set one before choosing it" : `DeepSeek search and fetch with your DeepSeek API key (${source("deepseek")})`
43225
+ };
43226
+ return SEARCH_PROVIDERS.map((value) => ({
43227
+ value,
43228
+ label: `${NAMES[value]}${settings.selected === value ? " (Current)" : ""}`,
43229
+ description: descriptions[value]
43230
+ }));
43231
+ }
43232
+ function openSearchPanel(panel, client, notify) {
43233
+ const items = () => searchItems(client.searchSettings.state.value, client.credentials.state.value);
43234
+ const refresh = () => panel.update(items());
43235
+ const stops = [client.searchSettings.state.subscribe(refresh), client.credentials.state.subscribe(refresh)];
43236
+ panel.emptyText = SEARCH_EMPTY;
43237
+ panel.show("Web search", async () => items(), async (value) => {
43238
+ await client.searchSettings.set({ provider: value });
43239
+ notify(`Web search provider set to ${NAMES[value]}.`);
43240
+ });
43241
+ return () => {
43242
+ for (const stop of stops)
43243
+ stop();
43244
+ };
43245
+ }
43246
+ var SEARCH_EMPTY = "Search settings are unavailable.", NAMES, SOURCES2;
43247
+ var init_search_ui = __esm(() => {
43248
+ init_search_providers();
43249
+ NAMES = { tavily: "Tavily", deepseek: "DeepSeek" };
43250
+ SOURCES2 = {
43251
+ saved: "saved key",
43252
+ environment: "environment variable",
43253
+ runtime: "runtime key",
43254
+ keyless: "keyless, rate-limited",
43255
+ none: "no key"
43256
+ };
43257
+ });
43258
+
42282
43259
  // packages/tui/src/skill-market-ui.ts
42283
43260
  import { ChordError as ChordError6 } from "@earendil-works/chord";
42284
- import { Markdown as Markdown3, matchesKey as matchesKey9, wrapTextWithAnsi as wrapTextWithAnsi5 } from "@earendil-works/pi-tui";
43261
+ import { Markdown as Markdown3, matchesKey as matchesKey10, wrapTextWithAnsi as wrapTextWithAnsi5 } from "@earendil-works/pi-tui";
42285
43262
 
42286
43263
  class SkillDetailView {
42287
43264
  #skill = null;
@@ -42320,11 +43297,11 @@ class SkillDetailView {
42320
43297
  }
42321
43298
  handleKey(data) {
42322
43299
  if (this.#confirm) {
42323
- if (matchesKey9(data, "y")) {
43300
+ if (matchesKey10(data, "y")) {
42324
43301
  const pending = this.#confirm;
42325
43302
  this.#confirm = null;
42326
43303
  this.run(pending.run);
42327
- } else if (matchesKey9(data, "n")) {
43304
+ } else if (matchesKey10(data, "n")) {
42328
43305
  this.#confirm = null;
42329
43306
  }
42330
43307
  return true;
@@ -42333,17 +43310,17 @@ class SkillDetailView {
42333
43310
  return true;
42334
43311
  const skill = this.#skill;
42335
43312
  const builtinOnly = skill.installed.builtin && !skill.installed.user && !skill.installed.project;
42336
- if (matchesKey9(data, "v")) {
43313
+ if (matchesKey10(data, "v")) {
42337
43314
  this.#openPreview(this.#name);
42338
43315
  return true;
42339
43316
  }
42340
43317
  if (builtinOnly)
42341
43318
  return true;
42342
- if (matchesKey9(data, "u"))
43319
+ if (matchesKey10(data, "u"))
42343
43320
  this.askInstall("user", skill);
42344
- else if (matchesKey9(data, "p"))
43321
+ else if (matchesKey10(data, "p"))
42345
43322
  this.askInstall("project", skill);
42346
- else if (matchesKey9(data, "d"))
43323
+ else if (matchesKey10(data, "d"))
42347
43324
  this.askUninstall(skill);
42348
43325
  else
42349
43326
  return false;
@@ -42468,13 +43445,13 @@ class SkillPreviewView {
42468
43445
  this.#offset = Math.max(0, Math.min(this.#maxOffset, this.#offset + delta));
42469
43446
  }
42470
43447
  handleKey(data) {
42471
- if (matchesKey9(data, "up"))
43448
+ if (matchesKey10(data, "up"))
42472
43449
  this.scroll(-1);
42473
- else if (matchesKey9(data, "down"))
43450
+ else if (matchesKey10(data, "down"))
42474
43451
  this.scroll(1);
42475
- else if (matchesKey9(data, "pageUp"))
43452
+ else if (matchesKey10(data, "pageUp"))
42476
43453
  this.scroll(-this.#pageSize);
42477
- else if (matchesKey9(data, "pageDown"))
43454
+ else if (matchesKey10(data, "pageDown"))
42478
43455
  this.scroll(this.#pageSize);
42479
43456
  else
42480
43457
  return false;
@@ -42526,7 +43503,7 @@ var init_skill_market_ui = __esm(() => {
42526
43503
  });
42527
43504
 
42528
43505
  // packages/tui/src/subagents-ui.ts
42529
- import { matchesKey as matchesKey10 } from "@earendil-works/pi-tui";
43506
+ import { matchesKey as matchesKey11 } from "@earendil-works/pi-tui";
42530
43507
 
42531
43508
  class SubagentDetail {
42532
43509
  get help() {
@@ -42646,13 +43623,13 @@ class SubagentDetail {
42646
43623
  this.history();
42647
43624
  }
42648
43625
  handleKey(data) {
42649
- if (matchesKey10(data, "up")) {
43626
+ if (matchesKey11(data, "up")) {
42650
43627
  this.scroll(-1);
42651
- } else if (matchesKey10(data, "down")) {
43628
+ } else if (matchesKey11(data, "down")) {
42652
43629
  this.scroll(1);
42653
- } else if (matchesKey10(data, "pageUp")) {
43630
+ } else if (matchesKey11(data, "pageUp")) {
42654
43631
  this.scroll(-this.#pageSize);
42655
- } else if (matchesKey10(data, "pageDown")) {
43632
+ } else if (matchesKey11(data, "pageDown")) {
42656
43633
  this.scroll(this.#pageSize);
42657
43634
  } else if (data.toLowerCase() === "e") {
42658
43635
  this.#expanded = !this.#expanded;
@@ -42668,7 +43645,7 @@ class SubagentDetail {
42668
43645
  help: shortcutHints(["Enter", "Stop"], ["Esc", "Back"]),
42669
43646
  lines: () => [theme.warning("Stop this task? Existing changes will be kept.")],
42670
43647
  handleKey: (key) => {
42671
- if (!matchesKey10(key, "enter"))
43648
+ if (!matchesKey11(key, "enter"))
42672
43649
  return false;
42673
43650
  this.#service.stop(stopTaskId === undefined ? null : { taskId: stopTaskId }).catch((e) => {
42674
43651
  this.#error = String(e);
@@ -42799,7 +43776,7 @@ var init_subagents_ui = __esm(() => {
42799
43776
  });
42800
43777
 
42801
43778
  // packages/tui/src/version-ui.ts
42802
- import { matchesKey as matchesKey11, Text as Text9 } from "@earendil-works/pi-tui";
43779
+ import { matchesKey as matchesKey12, Text as Text9 } from "@earendil-works/pi-tui";
42803
43780
  function actionFor(state, installed, runningVersion) {
42804
43781
  const action = updateAction({ gateway: state, frontend: runningVersion, installed });
42805
43782
  return action === "install" && !newer(state?.latestVersion, runningVersion) ? "none" : action;
@@ -42877,9 +43854,9 @@ class VersionDetail {
42877
43854
  return lines.slice(this.#offset, this.#offset + height);
42878
43855
  }
42879
43856
  handleKey(data) {
42880
- if (matchesKey11(data, "up"))
43857
+ if (matchesKey12(data, "up"))
42881
43858
  this.#offset = Math.max(0, this.#offset - 1);
42882
- else if (matchesKey11(data, "down"))
43859
+ else if (matchesKey12(data, "down"))
42883
43860
  this.#offset++;
42884
43861
  else
42885
43862
  return false;
@@ -42944,7 +43921,7 @@ import {
42944
43921
  Container as Container6,
42945
43922
  isKeyRelease as isKeyRelease5,
42946
43923
  Loader,
42947
- matchesKey as matchesKey12,
43924
+ matchesKey as matchesKey13,
42948
43925
  ProcessTerminal,
42949
43926
  ScrollView,
42950
43927
  TuiAltScreen
@@ -42980,6 +43957,8 @@ class NekoTuiApp {
42980
43957
  #chat;
42981
43958
  #agentsPanel = false;
42982
43959
  #jobsPanel = false;
43960
+ #searchStop;
43961
+ #contextStop;
42983
43962
  #agentDetails = new Map;
42984
43963
  #sessionId = "";
42985
43964
  #viewUnsubscribe;
@@ -43021,6 +44000,10 @@ class NekoTuiApp {
43021
44000
  this.#thinkingModel = undefined;
43022
44001
  this.#agentsPanel = false;
43023
44002
  this.#jobsPanel = false;
44003
+ this.#searchStop?.();
44004
+ this.#searchStop = undefined;
44005
+ this.#contextStop?.();
44006
+ this.#contextStop = undefined;
43024
44007
  clearInterval(this.#sessionPanelTimer);
43025
44008
  this.#sessionPanelTimer = undefined;
43026
44009
  this.#ui.setFocus(this.#editor);
@@ -43081,10 +44064,10 @@ class NekoTuiApp {
43081
44064
  this.#ui.addInputListener((data) => {
43082
44065
  if (isKeyRelease5(data))
43083
44066
  return { consume: true };
43084
- if (!matchesKey12(data, "escape") || this.#panel.open || this.#login.open || this.#busyText !== undefined || this.#lastView?.compaction?.active.length)
44067
+ if (!matchesKey13(data, "escape") || this.#panel.open || this.#login.open || this.#busyText !== undefined || this.#lastView?.compaction?.active.length)
43085
44068
  this.#editor.resetEscapeSequence();
43086
44069
  if (!usableSize(this.#terminal.columns, this.#terminal.rows)) {
43087
- if (matchesKey12(data, "ctrl+c"))
44070
+ if (matchesKey13(data, "ctrl+c"))
43088
44071
  this.exit();
43089
44072
  return { consume: true };
43090
44073
  }
@@ -43095,11 +44078,11 @@ class NekoTuiApp {
43095
44078
  }
43096
44079
  if (this.#sessionOperation)
43097
44080
  return { consume: true };
43098
- if (matchesKey12(data, "right") && this.#lastView?.subagents?.length && this.#editor.getText() === "" && !this.#editor.isShowingAutocomplete()) {
44081
+ if (matchesKey13(data, "right") && this.#lastView?.subagents?.length && this.#editor.getText() === "" && !this.#editor.isShowingAutocomplete()) {
43099
44082
  this.#showAgents();
43100
44083
  return { consume: true };
43101
44084
  }
43102
- if (matchesKey12(data, process.platform === "win32" ? "alt+v" : "ctrl+v")) {
44085
+ if (matchesKey13(data, process.platform === "win32" ? "alt+v" : "ctrl+v")) {
43103
44086
  if (isClipboardShortcut(data))
43104
44087
  this.pasteClipboard();
43105
44088
  return { consume: true };
@@ -43116,26 +44099,26 @@ class NekoTuiApp {
43116
44099
  return { consume: true };
43117
44100
  }
43118
44101
  }
43119
- if (matchesKey12(data, "escape") && this.#editor.isShowingAutocomplete()) {
44102
+ if (matchesKey13(data, "escape") && this.#editor.isShowingAutocomplete()) {
43120
44103
  this.#editor.resetEscapeSequence();
43121
44104
  return;
43122
44105
  }
43123
- if (matchesKey12(data, "ctrl+c")) {
44106
+ if (matchesKey13(data, "ctrl+c")) {
43124
44107
  if (this.#busyText !== undefined)
43125
44108
  this.abort();
43126
44109
  else
43127
44110
  this.exit();
43128
44111
  return { consume: true };
43129
44112
  }
43130
- if (matchesKey12(data, "escape") && (this.#busyText !== undefined || this.#lastView?.compaction?.active.some((c) => c.reason === "manual" || c.blocking))) {
44113
+ if (matchesKey13(data, "escape") && (this.#busyText !== undefined || this.#lastView?.compaction?.active.some((c) => c.reason === "manual" || c.blocking))) {
43131
44114
  this.abort();
43132
44115
  return { consume: true };
43133
44116
  }
43134
- if (matchesKey12(data, "ctrl+s")) {
44117
+ if (matchesKey13(data, "ctrl+s")) {
43135
44118
  this.openSessionsOverlay();
43136
44119
  return { consume: true };
43137
44120
  }
43138
- if (this.#busyText !== undefined && matchesKey12(data, "alt+enter")) {
44121
+ if (this.#busyText !== undefined && matchesKey13(data, "alt+enter")) {
43139
44122
  this.submitFollowUp();
43140
44123
  return { consume: true };
43141
44124
  }
@@ -43194,7 +44177,7 @@ class NekoTuiApp {
43194
44177
  const installed = await installedVersion();
43195
44178
  if (this.#exitStarted)
43196
44179
  return;
43197
- const notice = versionNotice(this.#gateway.client.application.info.value ?? null, installed);
44180
+ const notice = versionNotice(this.#gateway.client.application.info.value, installed);
43198
44181
  this.#versionNotice.clear();
43199
44182
  if (notice)
43200
44183
  this.#versionNotice.addChild(new ThemedTruncatedText(() => formatNotice(notice, "info"), 1, 0));
@@ -43242,6 +44225,14 @@ class NekoTuiApp {
43242
44225
  this.#showJobs();
43243
44226
  return;
43244
44227
  }
44228
+ if (trimmed === "/search") {
44229
+ this.#showSearch();
44230
+ return;
44231
+ }
44232
+ if (trimmed === "/context") {
44233
+ this.#showContext();
44234
+ return;
44235
+ }
43245
44236
  const instructions = compactInstructions(trimmed);
43246
44237
  if (instructions !== null) {
43247
44238
  const chat = this.#chat;
@@ -43293,7 +44284,7 @@ class NekoTuiApp {
43293
44284
  if (trimmed === "/version") {
43294
44285
  if (!this.#exitStarted && !this.#panel.open && !this.#login.open) {
43295
44286
  openVersion(this.#panel, async () => ({
43296
- state: this.#gateway.client.application.info.value ?? null,
44287
+ state: this.#gateway.client.application.info.value,
43297
44288
  installed: await installedVersion()
43298
44289
  }), () => this.#gateway.client.application.check(null));
43299
44290
  this.#ui.setFocus(this.#panel);
@@ -43327,7 +44318,7 @@ class NekoTuiApp {
43327
44318
  const chat = this.#chat;
43328
44319
  if (chat === undefined)
43329
44320
  return;
43330
- if (this.#lastView?.agent.model?.provider === "deepseek" && this.#gateway.client.credentials.state.value?.statuses.deepseek.source === "none") {
44321
+ if (this.#lastView?.agent.model?.provider === "deepseek" && this.#gateway.client.credentials.state.value.statuses.deepseek.source === "none") {
43331
44322
  this.#editor.setText(text);
43332
44323
  this.addNotice("Configure DeepSeek with /login before sending.", "warning");
43333
44324
  return;
@@ -43445,7 +44436,7 @@ class NekoTuiApp {
43445
44436
  this.addNotice(messageOf(error), "warning", { wrap: true });
43446
44437
  return;
43447
44438
  }
43448
- const summary = this.#gateway.client.directory.state.value?.sessions.find((s) => s.id === sessionId);
44439
+ const summary = this.#gateway.client.directory.state.value.sessions.find((s) => s.id === sessionId);
43449
44440
  const title = summary ? sessionName(summary, "") : "";
43450
44441
  this.addNotice("Exporting...");
43451
44442
  try {
@@ -43498,8 +44489,7 @@ class NekoTuiApp {
43498
44489
  this.#historyItems = undefined;
43499
44490
  this.#resetTranscript();
43500
44491
  const view = handle.view;
43501
- if (view.value !== undefined)
43502
- this.applyView(view.value);
44492
+ this.applyView(view.value);
43503
44493
  this.#viewUnsubscribe = view.subscribe(() => {
43504
44494
  this.scheduleApply();
43505
44495
  });
@@ -43541,7 +44531,7 @@ class NekoTuiApp {
43541
44531
  async archiveSession() {
43542
44532
  const current = this.#sessionId;
43543
44533
  await this.#gateway.client.management.archive(current);
43544
- const next = recentlyVisited(this.#gateway.client.directory.state.value?.sessions ?? [], this.#options.cwd).find((s) => s.id !== current);
44534
+ const next = recentlyVisited(this.#gateway.client.directory.state.value.sessions, this.#options.cwd).find((s) => s.id !== current);
43545
44535
  if (next)
43546
44536
  await this.switchSession(next.id);
43547
44537
  else
@@ -43550,7 +44540,7 @@ class NekoTuiApp {
43550
44540
  sessionItems() {
43551
44541
  return [
43552
44542
  { value: NEW_SESSION_VALUE, label: "+ New session", description: this.#options.cwd },
43553
- ...sessionPickerItems(this.#gateway.client.directory.state.value?.sessions ?? [], this.#options.cwd, this.#sessionId)
44543
+ ...sessionPickerItems(this.#gateway.client.directory.state.value.sessions, this.#options.cwd, this.#sessionId)
43554
44544
  ];
43555
44545
  }
43556
44546
  openSessionsOverlay() {
@@ -43685,7 +44675,7 @@ class NekoTuiApp {
43685
44675
  this.#panel.pointerSelection = true;
43686
44676
  this.#panel.emptyText = JOBS_EMPTY;
43687
44677
  this.#panel.listKey = (data, value) => {
43688
- if (matchesKey12(data, "left")) {
44678
+ if (matchesKey13(data, "left")) {
43689
44679
  this.#panel.close();
43690
44680
  return true;
43691
44681
  }
@@ -43699,6 +44689,18 @@ class NekoTuiApp {
43699
44689
  this.#panel.show("Jobs", async () => jobItems(jobs.list()), async (value) => new JobDetail(jobs, value, this.#panel, changed).open());
43700
44690
  this.#ui.setFocus(this.#panel);
43701
44691
  }
44692
+ #showSearch() {
44693
+ if (this.#exitStarted || this.#panel.open || this.#login.open)
44694
+ return;
44695
+ this.#searchStop = openSearchPanel(this.#panel, this.#gateway.client, (message) => this.addNotice(message));
44696
+ this.#ui.setFocus(this.#panel);
44697
+ }
44698
+ #showContext() {
44699
+ if (this.#exitStarted || this.#panel.open || this.#login.open)
44700
+ return;
44701
+ this.#contextStop = openContextPanel(this.#panel, this.#gateway.client, (message) => this.addNotice(message));
44702
+ this.#ui.setFocus(this.#panel);
44703
+ }
43702
44704
  #agentDetail(target) {
43703
44705
  const round = typeof target === "number" ? this.#lastView?.subagents?.find((r) => r.taskId === target) : undefined;
43704
44706
  const id = typeof target === "string" ? target : round?.agentId ?? `agent-${round?.childId}`;
@@ -44085,14 +45087,7 @@ async function runTui(options = {}) {
44085
45087
  }
44086
45088
  }
44087
45089
  async function initialSessionId(gateway, cwd) {
44088
- const state = gateway.client.directory.state;
44089
- const start = Date.now();
44090
- while (state.value === undefined) {
44091
- if (Date.now() - start > 5000)
44092
- throw new Error("Timed out loading sessions");
44093
- await new Promise((resolve) => setTimeout(resolve, 25));
44094
- }
44095
- const sessions = state.value?.sessions ?? [];
45090
+ const sessions = gateway.client.directory.state.value.sessions;
44096
45091
  const first = recentlyVisited(sessions, cwd)[0];
44097
45092
  if (first !== undefined)
44098
45093
  return first.id;
@@ -44118,6 +45113,7 @@ var init_tui = __esm(() => {
44118
45113
  init_tool_execution();
44119
45114
  init_user_message();
44120
45115
  init_welcome();
45116
+ init_context_ui();
44121
45117
  init_export_command();
44122
45118
  init_file_completion();
44123
45119
  init_gateway2();
@@ -44126,6 +45122,7 @@ var init_tui = __esm(() => {
44126
45122
  init_jobs_ui();
44127
45123
  init_labels();
44128
45124
  init_layout();
45125
+ init_search_ui();
44129
45126
  init_skill_market_ui();
44130
45127
  init_subagents_ui();
44131
45128
  init_theme();
@@ -44216,7 +45213,7 @@ var USAGE = `\u7528\u6CD5: neko tui [\u9009\u9879]
44216
45213
 
44217
45214
  \u6062\u590D\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\u6700\u8FD1\u4F7F\u7528\u7684\u4F1A\u8BDD\uFF0C\u4E0D\u5B58\u5728\u5219\u521B\u5EFA\u3002
44218
45215
  \u6309\u9700\u62C9\u8D77\u7F51\u5173\uFF0C\u5C5E\u4E3B TUI \u9000\u51FA\u65F6\u968F\u4E4B\u505C\u6B62\u3002
44219
- \u53EF\u7528 /login \u6216 DEEPSEEK_API_KEY \u914D\u7F6E\u51ED\u636E\u3002`;
45216
+ \u53EF\u7528 /login \u6216 DEEPSEEK_API_KEY \u914D\u7F6E\u51ED\u636E\uFF1B/search \u9009\u62E9\u8054\u7F51\u641C\u7D22\u670D\u52A1\uFF08tavily / deepseek\uFF0C\u9ED8\u8BA4 tavily\uFF09\uFF1B/context \u8C03\u6574\u4E0A\u4E0B\u6587\u538B\u7F29\u4E0E\u5DE5\u5177\u7ED3\u679C\u88C1\u526A\u3002`;
44220
45217
  var init_main = __esm(async () => {
44221
45218
  init_tui();
44222
45219
  try {
@@ -44453,7 +45450,7 @@ var init_gateway_spawn = __esm(() => {
44453
45450
 
44454
45451
  // packages/web/src/mobile-admin.ts
44455
45452
  import { randomBytes as randomBytes4, timingSafeEqual as timingSafeEqual3 } from "crypto";
44456
- import { ChordError as ChordError7, createHost as createHost4 } from "@earendil-works/chord";
45453
+ import { ChordError as ChordError7 } from "@earendil-works/chord";
44457
45454
  function createMobileAdminCaller(gateway, nekoDir, timeouts = {}) {
44458
45455
  const serverId = gateway.serverId;
44459
45456
  let tokenPromise;
@@ -44468,7 +45465,7 @@ function createMobileAdminCaller(gateway, nekoDir, timeouts = {}) {
44468
45465
  const token = await readToken();
44469
45466
  signal.throwIfAborted();
44470
45467
  const link = gateway.socketPath !== undefined ? await createUnixLink(gateway.socketPath) : createWsLink(gateway.wsUrl);
44471
- const host = createHost4({ kind: "client", upstream: identifiedLink(link, serverId), onError: () => {} });
45468
+ const host = createClientHost(link);
44472
45469
  const release = () => {
44473
45470
  host.close().catch(() => {
44474
45471
  return;
@@ -44477,7 +45474,8 @@ function createMobileAdminCaller(gateway, nekoDir, timeouts = {}) {
44477
45474
  };
44478
45475
  signal.addEventListener("abort", release, { once: true });
44479
45476
  try {
44480
- const admin = host.get(NekoMobileAdmin, { token }, { at: serverId });
45477
+ await assertGatewayIdentity(host, serverId, signal);
45478
+ const admin = await host.get(NekoMobileAdmin, { token }, { signal });
44481
45479
  const call = admin[method];
44482
45480
  if (call === undefined)
44483
45481
  throw new Error(`unknown mobile admin method: ${method}`);
@@ -45167,7 +46165,8 @@ var GATEWAY_USAGE = `\u7528\u6CD5: neko serve [\u9009\u9879]
45167
46165
  --config <dir> neko.config.json \u67E5\u627E\u8D77\u59CB\u76EE\u5F55\uFF08\u9ED8\u8BA4\u5F53\u524D\u76EE\u5F55\uFF09
45168
46166
  -h, --help \u663E\u793A\u6B64\u5E2E\u52A9
45169
46167
 
45170
- \u53EF\u901A\u8FC7\u524D\u7AEF\u8BBE\u7F6E\u6216 DEEPSEEK_API_KEY \u914D\u7F6E\u51ED\u636E\u3002`;
46168
+ \u53EF\u901A\u8FC7\u524D\u7AEF\u8BBE\u7F6E\u6216 DEEPSEEK_API_KEY / TAVILY_API_KEY \u914D\u7F6E\u51ED\u636E\uFF1B
46169
+ \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`;
45171
46170
 
45172
46171
  // packages/gateway/src/main.ts
45173
46172
  var argv, shutdown2, options, gateway;
@@ -45211,7 +46210,8 @@ var USAGE3 = `\u7528\u6CD5: neko <\u547D\u4EE4> [\u9009\u9879]
45211
46210
  -h, --help \u663E\u793A\u6B64\u5E2E\u52A9
45212
46211
 
45213
46212
  \u5404\u547D\u4EE4\u7684\u9009\u9879\u8BE6\u89C1 neko <\u547D\u4EE4> --help\u3002
45214
- \u53EF\u901A\u8FC7 TUI /login\u3001Web \u8BBE\u7F6E\u9875\u6216 DEEPSEEK_API_KEY / TAVILY_API_KEY \u914D\u7F6E\u51ED\u636E\u3002`;
46213
+ \u53EF\u901A\u8FC7 TUI /login\u3001Web \u8BBE\u7F6E\u9875\u6216 DEEPSEEK_API_KEY / TAVILY_API_KEY \u914D\u7F6E\u51ED\u636E\u3002
46214
+ \u8054\u7F51\u641C\u7D22\u670D\u52A1\uFF08\u9ED8\u8BA4 Tavily\uFF0C\u53EF\u6539\u9009 DeepSeek\uFF09\u53EF\u7528 TUI /search\u3001Web \u8BBE\u7F6E\u9875\u6216\u914D\u7F6E\u6587\u4EF6 webSearch.provider \u9009\u62E9\u3002`;
45215
46215
  var [command2, ...rest] = process.argv.slice(2);
45216
46216
  switch (command2) {
45217
46217
  case "update": {