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