neko-harness 0.4.2 → 0.4.4
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-BjLs6qQU.css +2 -0
- package/dist/client/assets/index-BqLuRtzq.js +391 -0
- package/dist/client/index.html +2 -2
- package/dist/gateway.js +1880 -1082
- package/dist/neko.js +1925 -551
- 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";
|
|
@@ -17065,6 +17256,50 @@ function savedDefaultImagePrompt(content, value) {
|
|
|
17065
17256
|
import { defineEntry } from "@earendil-works/pi-durable";
|
|
17066
17257
|
var NekoInput = defineEntry("app.neko.input");
|
|
17067
17258
|
|
|
17259
|
+
// packages/core/src/queued.ts
|
|
17260
|
+
var QUEUED_PREVIEW_CHARS = 300;
|
|
17261
|
+
var textOf = (content) => {
|
|
17262
|
+
if (typeof content === "string")
|
|
17263
|
+
return content;
|
|
17264
|
+
if (!Array.isArray(content))
|
|
17265
|
+
return "";
|
|
17266
|
+
return content.flatMap((b) => b && b.type === "text" && typeof b.text === "string" ? [b.text] : []).join("");
|
|
17267
|
+
};
|
|
17268
|
+
var clip = (text) => {
|
|
17269
|
+
const chars = [...text.replace(/\s+/g, " ").trim()];
|
|
17270
|
+
return chars.length > QUEUED_PREVIEW_CHARS ? `${chars.slice(0, QUEUED_PREVIEW_CHARS - 1).join("")}\u2026` : chars.join("");
|
|
17271
|
+
};
|
|
17272
|
+
function queuedUserInput(raw) {
|
|
17273
|
+
const item = raw;
|
|
17274
|
+
if (item.mode !== "steer" && item.mode !== "followUp")
|
|
17275
|
+
return;
|
|
17276
|
+
if (item.entry?.kind !== NekoInput.kind)
|
|
17277
|
+
return;
|
|
17278
|
+
const data = item.entry.data;
|
|
17279
|
+
if (!data || data.jobReport || data.subagentRoundId)
|
|
17280
|
+
return;
|
|
17281
|
+
const defaultPrompt = Array.isArray(item.content) ? savedDefaultImagePrompt(item.content, data.defaultImagePrompt) : undefined;
|
|
17282
|
+
const content = defaultPrompt && Array.isArray(item.content) ? item.content.slice(1) : item.content;
|
|
17283
|
+
const skill = data.skill ? { name: data.skill.name, args: data.skill.args } : undefined;
|
|
17284
|
+
return {
|
|
17285
|
+
id: item.id,
|
|
17286
|
+
mode: item.mode,
|
|
17287
|
+
text: skill ? `/skill:${skill.name}${skill.args ? ` ${skill.args}` : ""}` : textOf(content),
|
|
17288
|
+
images: data.images?.length ?? 0,
|
|
17289
|
+
...skill ? { skill } : {}
|
|
17290
|
+
};
|
|
17291
|
+
}
|
|
17292
|
+
function projectQueued(view) {
|
|
17293
|
+
const items = view.docs["pi.inbox"]?.items ?? [];
|
|
17294
|
+
return items.flatMap((raw) => {
|
|
17295
|
+
const input = queuedUserInput(raw);
|
|
17296
|
+
if (!input)
|
|
17297
|
+
return [];
|
|
17298
|
+
const { text, ...rest } = input;
|
|
17299
|
+
return [{ ...rest, preview: clip(text) }];
|
|
17300
|
+
});
|
|
17301
|
+
}
|
|
17302
|
+
|
|
17068
17303
|
// packages/core/src/workspace.ts
|
|
17069
17304
|
import { accessSync, constants, realpathSync, statSync } from "fs";
|
|
17070
17305
|
import { isAbsolute, resolve as resolve2 } from "path";
|
|
@@ -17385,6 +17620,7 @@ function projectChatView(view, modelLabel, options = {}) {
|
|
|
17385
17620
|
},
|
|
17386
17621
|
cwd: savedWorkspace(agent?.cwd),
|
|
17387
17622
|
busy: live.run !== undefined,
|
|
17623
|
+
queued: projectQueued(view),
|
|
17388
17624
|
status,
|
|
17389
17625
|
usage: {
|
|
17390
17626
|
input: usage.input,
|
|
@@ -17530,6 +17766,7 @@ import {
|
|
|
17530
17766
|
createRegistry,
|
|
17531
17767
|
defineDoc as defineDoc5,
|
|
17532
17768
|
Harness,
|
|
17769
|
+
InboxDoc as InboxDoc2,
|
|
17533
17770
|
LiveDoc as importLiveDoc,
|
|
17534
17771
|
MemoryStorage,
|
|
17535
17772
|
UsageDoc as UsageDoc2,
|
|
@@ -17728,6 +17965,134 @@ async function readCompaction(harness, storage, id, context) {
|
|
|
17728
17965
|
return { taskId: task.id, phase, at: task.endedAt ?? task.startedAt ?? 0 };
|
|
17729
17966
|
}
|
|
17730
17967
|
|
|
17968
|
+
// packages/core/src/context-pruning.ts
|
|
17969
|
+
var PRUNE_ENTRY_KIND = "app.neko.context-prune";
|
|
17970
|
+
var TOKENS_PER_LATIN_CHAR = 0.3;
|
|
17971
|
+
var TOKENS_PER_CJK_CHAR = 0.6;
|
|
17972
|
+
var CJK = /[\u3000-\u9fff\uac00-\ud7af\uf900-\ufaff\uff00-\uffef]/g;
|
|
17973
|
+
function estimateTokens(text) {
|
|
17974
|
+
const cjk = text.match(CJK)?.length ?? 0;
|
|
17975
|
+
return cjk * TOKENS_PER_CJK_CHAR + (text.length - cjk) * TOKENS_PER_LATIN_CHAR;
|
|
17976
|
+
}
|
|
17977
|
+
var isToolResult = (entry) => {
|
|
17978
|
+
const message = entry.model?.[0];
|
|
17979
|
+
return entry.kind === "pi.tool-result" && message?.role === "toolResult";
|
|
17980
|
+
};
|
|
17981
|
+
var resultTexts = (message) => message.content.flatMap((block) => block.type === "text" ? [block.text] : []);
|
|
17982
|
+
function toolResultChars(message) {
|
|
17983
|
+
return resultTexts(message).reduce((total, text) => total + text.length, 0);
|
|
17984
|
+
}
|
|
17985
|
+
function toolResultTokens(message) {
|
|
17986
|
+
return resultTexts(message).reduce((total, text) => total + estimateTokens(text), 0);
|
|
17987
|
+
}
|
|
17988
|
+
function prunedPlaceholder(message, chars) {
|
|
17989
|
+
const { details: _details, ...rest } = message;
|
|
17990
|
+
return {
|
|
17991
|
+
...rest,
|
|
17992
|
+
content: [
|
|
17993
|
+
{
|
|
17994
|
+
type: "text",
|
|
17995
|
+
text: `[Output of ${message.toolName} omitted from context to save space (${chars} characters). Run the tool again if the content is needed.]`
|
|
17996
|
+
}
|
|
17997
|
+
]
|
|
17998
|
+
};
|
|
17999
|
+
}
|
|
18000
|
+
function planPrune(entries, settings) {
|
|
18001
|
+
const replaced = new Set;
|
|
18002
|
+
for (const entry of entries)
|
|
18003
|
+
for (const edit of entry.edits ?? [])
|
|
18004
|
+
replaced.add(edit.target);
|
|
18005
|
+
const results = entries.filter(isToolResult);
|
|
18006
|
+
const candidates = results.slice(0, Math.max(0, results.length - settings.keepRecent));
|
|
18007
|
+
const edits = [];
|
|
18008
|
+
const targets = [];
|
|
18009
|
+
let savedChars = 0;
|
|
18010
|
+
let savedTokens = 0;
|
|
18011
|
+
for (const entry of candidates) {
|
|
18012
|
+
if (replaced.has(entry.id))
|
|
18013
|
+
continue;
|
|
18014
|
+
const message = entry.model[0];
|
|
18015
|
+
const chars = toolResultChars(message);
|
|
18016
|
+
if (chars < settings.minChars)
|
|
18017
|
+
continue;
|
|
18018
|
+
const replacement = prunedPlaceholder(message, chars);
|
|
18019
|
+
edits.push({ target: entry.id, action: "replace", messages: [replacement] });
|
|
18020
|
+
targets.push(entry.id);
|
|
18021
|
+
savedChars += chars;
|
|
18022
|
+
savedTokens += toolResultTokens(message);
|
|
18023
|
+
}
|
|
18024
|
+
return edits.length === 0 ? undefined : { edits, targets, savedChars, savedTokens };
|
|
18025
|
+
}
|
|
18026
|
+
|
|
18027
|
+
class ContextPruner {
|
|
18028
|
+
#options;
|
|
18029
|
+
#unsubscribe;
|
|
18030
|
+
#tail = Promise.resolve();
|
|
18031
|
+
#closed = false;
|
|
18032
|
+
constructor(options) {
|
|
18033
|
+
this.#options = options;
|
|
18034
|
+
this.#unsubscribe = options.harness.subscribeCommits(({ changes }) => {
|
|
18035
|
+
if (!options.settings().enabled)
|
|
18036
|
+
return;
|
|
18037
|
+
for (const change of changes) {
|
|
18038
|
+
if (change.type !== "entry" || change.value.kind !== "pi.assistant")
|
|
18039
|
+
continue;
|
|
18040
|
+
const message = change.value.model?.[0];
|
|
18041
|
+
if (message?.role !== "assistant" || message.stopReason === "aborted" || message.stopReason === "error")
|
|
18042
|
+
continue;
|
|
18043
|
+
const usage = message.usage;
|
|
18044
|
+
const tokens = usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
|
|
18045
|
+
if (tokens <= 0)
|
|
18046
|
+
continue;
|
|
18047
|
+
const { conversationId, id } = change.value;
|
|
18048
|
+
this.#enqueue(() => this.#evaluate(conversationId, id, tokens));
|
|
18049
|
+
}
|
|
18050
|
+
});
|
|
18051
|
+
}
|
|
18052
|
+
async flush() {
|
|
18053
|
+
await this.#tail;
|
|
18054
|
+
}
|
|
18055
|
+
close() {
|
|
18056
|
+
this.#closed = true;
|
|
18057
|
+
this.#unsubscribe();
|
|
18058
|
+
}
|
|
18059
|
+
#enqueue(work) {
|
|
18060
|
+
if (this.#closed)
|
|
18061
|
+
return;
|
|
18062
|
+
this.#tail = this.#tail.then(work).catch((error) => {
|
|
18063
|
+
if (!this.#closed)
|
|
18064
|
+
this.#options.report(error);
|
|
18065
|
+
});
|
|
18066
|
+
}
|
|
18067
|
+
async#evaluate(conversationId, entryId, tokens) {
|
|
18068
|
+
const { harness, context } = this.#options;
|
|
18069
|
+
const settings = this.#options.settings();
|
|
18070
|
+
if (!settings.enabled)
|
|
18071
|
+
return;
|
|
18072
|
+
const window2 = await this.#options.windowOf(conversationId);
|
|
18073
|
+
if (window2 <= 0 || tokens / window2 * 100 < settings.triggerPercent)
|
|
18074
|
+
return;
|
|
18075
|
+
const conversation = await harness.conversation(conversationId, context);
|
|
18076
|
+
if (!conversation)
|
|
18077
|
+
return;
|
|
18078
|
+
const view = await conversation.context(context);
|
|
18079
|
+
const plan = planPrune(view.entries, settings);
|
|
18080
|
+
if (!plan)
|
|
18081
|
+
return;
|
|
18082
|
+
if (plan.savedTokens / window2 * 100 < settings.minSavePercent)
|
|
18083
|
+
return;
|
|
18084
|
+
await conversation.submit({
|
|
18085
|
+
type: "write",
|
|
18086
|
+
requestId: `prune:${entryId}`,
|
|
18087
|
+
entry: {
|
|
18088
|
+
kind: PRUNE_ENTRY_KIND,
|
|
18089
|
+
data: { targets: [...plan.targets], savedChars: plan.savedChars },
|
|
18090
|
+
edits: plan.edits
|
|
18091
|
+
}
|
|
18092
|
+
}, context);
|
|
18093
|
+
}
|
|
18094
|
+
}
|
|
18095
|
+
|
|
17731
18096
|
// packages/core/src/image-metadata.ts
|
|
17732
18097
|
import { defineExtension as defineExtension2, hook, ToolTask } from "@earendil-works/pi-durable";
|
|
17733
18098
|
import sharp from "sharp";
|
|
@@ -18387,7 +18752,7 @@ var statusWord = {
|
|
|
18387
18752
|
stopped: "stopped",
|
|
18388
18753
|
lost: "lost"
|
|
18389
18754
|
};
|
|
18390
|
-
var
|
|
18755
|
+
var describe2 = (job) => {
|
|
18391
18756
|
const parts = [statusWord[job.status]];
|
|
18392
18757
|
if (job.exitCode !== null)
|
|
18393
18758
|
parts.push(`exit code ${job.exitCode}`);
|
|
@@ -18402,7 +18767,7 @@ var describe = (job) => {
|
|
|
18402
18767
|
function reportText(job, tail) {
|
|
18403
18768
|
const body = reportTail(tail);
|
|
18404
18769
|
return [
|
|
18405
|
-
`[Background job ${job.jobId} "${job.title}" ${
|
|
18770
|
+
`[Background job ${job.jobId} "${job.title}" ${describe2(job)}]`,
|
|
18406
18771
|
`Command: ${job.command}`,
|
|
18407
18772
|
body ? `Last output (use the job tool's output action with jobId "${job.jobId}" for more):
|
|
18408
18773
|
${body}` : "It produced no output."
|
|
@@ -18607,7 +18972,7 @@ class JobManager {
|
|
|
18607
18972
|
if (kind === "stop") {
|
|
18608
18973
|
const jobId = stringField(args.jobId, "jobId");
|
|
18609
18974
|
const job = await this.stop(io, api.conversationId, jobId, "model", context);
|
|
18610
|
-
return this.#text(`Job ${job.jobId} ${
|
|
18975
|
+
return this.#text(`Job ${job.jobId} ${describe2(job)}.`);
|
|
18611
18976
|
}
|
|
18612
18977
|
if (kind !== "start")
|
|
18613
18978
|
throw new Error("Invalid job arguments: action must be start, output, stop or list.");
|
|
@@ -18635,7 +19000,7 @@ class JobManager {
|
|
|
18635
19000
|
#listText(items) {
|
|
18636
19001
|
if (!items.length)
|
|
18637
19002
|
return "No jobs.";
|
|
18638
|
-
return items.map((job) => `${job.jobId} [${
|
|
19003
|
+
return items.map((job) => `${job.jobId} [${describe2(job)}] "${job.title}" \u2014 ${job.command}`).join(`
|
|
18639
19004
|
`);
|
|
18640
19005
|
}
|
|
18641
19006
|
async start(io, conversationId, cwd, args, context) {
|
|
@@ -18687,7 +19052,7 @@ class JobManager {
|
|
|
18687
19052
|
return this.#render(job, read.text, read.offset, read.dropped);
|
|
18688
19053
|
}
|
|
18689
19054
|
#render(job, text, offset, dropped) {
|
|
18690
|
-
const lines = [`Job ${job.jobId} ${
|
|
19055
|
+
const lines = [`Job ${job.jobId} ${describe2(job)}.`, `Title: ${job.title}`, `Command: ${job.command}`];
|
|
18691
19056
|
if (dropped > 0)
|
|
18692
19057
|
lines.push(`(${dropped} bytes of earlier output were dropped)`);
|
|
18693
19058
|
lines.push(text ? `Output (next offset ${offset}):
|
|
@@ -19096,6 +19461,14 @@ var createResourceSnapshots = (load, submission) => {
|
|
|
19096
19461
|
discard(id, requestId) {
|
|
19097
19462
|
conversations.get(id)?.pending.delete(requestId);
|
|
19098
19463
|
},
|
|
19464
|
+
move(id, from, to) {
|
|
19465
|
+
const pending = conversations.get(id)?.pending;
|
|
19466
|
+
const snapshot = pending?.get(from);
|
|
19467
|
+
if (!pending || !snapshot)
|
|
19468
|
+
return;
|
|
19469
|
+
pending.delete(from);
|
|
19470
|
+
pending.set(to, snapshot);
|
|
19471
|
+
},
|
|
19099
19472
|
forget(id) {
|
|
19100
19473
|
conversations.delete(id);
|
|
19101
19474
|
}
|
|
@@ -19621,33 +19994,32 @@ async function prepareApprovalConversations(harness, ids, extension, context) {
|
|
|
19621
19994
|
}
|
|
19622
19995
|
}
|
|
19623
19996
|
|
|
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";
|
|
19997
|
+
// packages/core/src/web-search/http.ts
|
|
19629
19998
|
var REQUEST_TIMEOUT_MS = 30000;
|
|
19630
19999
|
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
20000
|
|
|
19637
|
-
class
|
|
20001
|
+
class WebToolError extends Error {
|
|
20002
|
+
status;
|
|
20003
|
+
constructor(message, status) {
|
|
20004
|
+
super(message);
|
|
20005
|
+
this.status = status;
|
|
20006
|
+
}
|
|
19638
20007
|
}
|
|
19639
20008
|
function redact(text, key) {
|
|
19640
20009
|
for (const secret of key ? [key, encodeURIComponent(key)] : [])
|
|
19641
20010
|
text = text.replaceAll(secret, "[redacted]");
|
|
19642
|
-
return text.replace(
|
|
20011
|
+
return text.replace(/\b(?:tvly|sk)-[a-zA-Z0-9_-]+/g, "[redacted]");
|
|
19643
20012
|
}
|
|
19644
20013
|
function record(value) {
|
|
19645
20014
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
19646
20015
|
}
|
|
19647
|
-
|
|
20016
|
+
function truncate(text, max) {
|
|
20017
|
+
return text.length > max ? `${text.slice(0, Math.max(0, max - 1))}\u2026` : text;
|
|
20018
|
+
}
|
|
20019
|
+
async function readBounded(response, signal, label) {
|
|
19648
20020
|
if (Number(response.headers.get("content-length")) > MAX_RESPONSE_BYTES) {
|
|
19649
20021
|
await response.body?.cancel();
|
|
19650
|
-
throw new
|
|
20022
|
+
throw new WebToolError(`${label} response exceeds the 2 MiB limit.`);
|
|
19651
20023
|
}
|
|
19652
20024
|
if (!response.body)
|
|
19653
20025
|
return "";
|
|
@@ -19668,7 +20040,7 @@ async function readBounded(response, signal) {
|
|
|
19668
20040
|
return text + decoder.decode();
|
|
19669
20041
|
size += chunk.value.byteLength;
|
|
19670
20042
|
if (size > MAX_RESPONSE_BYTES)
|
|
19671
|
-
throw new
|
|
20043
|
+
throw new WebToolError(`${label} response exceeds the 2 MiB limit.`);
|
|
19672
20044
|
text += decoder.decode(chunk.value, { stream: true });
|
|
19673
20045
|
}
|
|
19674
20046
|
} finally {
|
|
@@ -19677,7 +20049,8 @@ async function readBounded(response, signal) {
|
|
|
19677
20049
|
reader.releaseLock();
|
|
19678
20050
|
}
|
|
19679
20051
|
}
|
|
19680
|
-
async function
|
|
20052
|
+
async function postJson(options) {
|
|
20053
|
+
const { label, context } = options;
|
|
19681
20054
|
let key;
|
|
19682
20055
|
const signal = AbortSignal.any([
|
|
19683
20056
|
AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
@@ -19685,68 +20058,188 @@ async function tavilyPost(url, body, resolveKey, fetchImpl, context) {
|
|
|
19685
20058
|
]);
|
|
19686
20059
|
try {
|
|
19687
20060
|
signal.throwIfAborted();
|
|
19688
|
-
key = await resolveKey();
|
|
20061
|
+
key = await options.resolveKey();
|
|
19689
20062
|
signal.throwIfAborted();
|
|
19690
|
-
const
|
|
19691
|
-
|
|
19692
|
-
headers
|
|
19693
|
-
|
|
19694
|
-
|
|
19695
|
-
|
|
19696
|
-
const text = redact(await readBounded(response, signal), key);
|
|
20063
|
+
const response = await options.fetchImpl(options.url, {
|
|
20064
|
+
method: "POST",
|
|
20065
|
+
headers: { "content-type": "application/json", ...options.headers(key) },
|
|
20066
|
+
body: JSON.stringify(options.body),
|
|
20067
|
+
signal
|
|
20068
|
+
});
|
|
20069
|
+
const text = redact(await readBounded(response, signal, label), key);
|
|
19697
20070
|
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;
|
|
20071
|
+
throw options.httpError(response.status, text, key);
|
|
20072
|
+
return JSON.parse(text);
|
|
19705
20073
|
} catch (error) {
|
|
19706
20074
|
if (context.abortSignal?.aborted)
|
|
19707
|
-
throw new
|
|
20075
|
+
throw new WebToolError("Web request cancelled.");
|
|
19708
20076
|
if (signal.aborted)
|
|
19709
|
-
throw new
|
|
19710
|
-
if (error instanceof
|
|
20077
|
+
throw new WebToolError(`${label} request timed out. Please retry.`);
|
|
20078
|
+
if (error instanceof WebToolError)
|
|
19711
20079
|
throw error;
|
|
19712
|
-
throw new
|
|
20080
|
+
throw new WebToolError(`${label} request failed. Check the connection and credentials, then retry.`);
|
|
19713
20081
|
}
|
|
19714
20082
|
}
|
|
20083
|
+
|
|
20084
|
+
// packages/core/src/web-search/deepseek.ts
|
|
20085
|
+
var SEARCH_URL = "https://api.deepseek.com/v1/tools/search";
|
|
20086
|
+
var FETCH_URL = "https://api.deepseek.com/v1/tools/fetch";
|
|
20087
|
+
var DEEPSEEK_QUERY_MAX_CHARS = 128;
|
|
20088
|
+
var FETCH_CONCURRENCY = 3;
|
|
20089
|
+
var MAX_RETRIES = 2;
|
|
20090
|
+
var BACKOFF_MS = [250, 750];
|
|
20091
|
+
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.";
|
|
20092
|
+
function defaultSleep(ms, signal) {
|
|
20093
|
+
return new Promise((resolve, reject) => {
|
|
20094
|
+
if (signal?.aborted)
|
|
20095
|
+
return reject(signal.reason);
|
|
20096
|
+
const timer = setTimeout(resolve, ms);
|
|
20097
|
+
signal?.addEventListener("abort", () => {
|
|
20098
|
+
clearTimeout(timer);
|
|
20099
|
+
reject(signal.reason);
|
|
20100
|
+
}, { once: true });
|
|
20101
|
+
});
|
|
20102
|
+
}
|
|
20103
|
+
function apiMessage(text) {
|
|
20104
|
+
try {
|
|
20105
|
+
const data = JSON.parse(text);
|
|
20106
|
+
if (record(data) && record(data.error) && typeof data.error.message === "string")
|
|
20107
|
+
return truncate(data.error.message, 300);
|
|
20108
|
+
} catch {}
|
|
20109
|
+
return truncate(text, 300);
|
|
20110
|
+
}
|
|
20111
|
+
function httpError(status, text) {
|
|
20112
|
+
const guidance = "Check the key in /login or the Web settings, or switch the search provider.";
|
|
20113
|
+
if (status === 401)
|
|
20114
|
+
return new WebToolError(`DeepSeek API key is missing or invalid. ${guidance}`, status);
|
|
20115
|
+
if (status === 402)
|
|
20116
|
+
return new WebToolError("DeepSeek account balance is insufficient. Recharge, or switch the search provider to Tavily.", status);
|
|
20117
|
+
if (status === 403)
|
|
20118
|
+
return new WebToolError("DeepSeek blocked this request by its security policy.", status);
|
|
20119
|
+
if (status === 404)
|
|
20120
|
+
return new WebToolError("DeepSeek could not fetch a usable page for this URL.", status);
|
|
20121
|
+
if (status === 429)
|
|
20122
|
+
return new WebToolError("DeepSeek rate limit reached. Retry shortly.", status);
|
|
20123
|
+
if (status === 408 || status >= 500)
|
|
20124
|
+
return new WebToolError(`DeepSeek service error (${status}). Retry shortly.`, status);
|
|
20125
|
+
return new WebToolError(`DeepSeek request failed (${status}): ${apiMessage(text)}`, status);
|
|
20126
|
+
}
|
|
20127
|
+
var retryable = (status) => status !== undefined && (status === 429 || status === 408 || status >= 500);
|
|
20128
|
+
var wholeCall = (status) => status === 401 || status === 402;
|
|
20129
|
+
function dateOnly(value) {
|
|
20130
|
+
return typeof value === "string" && /^\d{4}-\d{2}-\d{2}/.test(value) ? value.slice(0, 10) : undefined;
|
|
20131
|
+
}
|
|
20132
|
+
function createDeepseekProvider(resolveKey, fetchImpl = fetch, options = {}) {
|
|
20133
|
+
const sleep = options.sleep ?? defaultSleep;
|
|
20134
|
+
const requireKey = async () => {
|
|
20135
|
+
const key = await resolveKey();
|
|
20136
|
+
if (!key)
|
|
20137
|
+
throw new WebToolError(DEEPSEEK_MISSING_KEY);
|
|
20138
|
+
return key;
|
|
20139
|
+
};
|
|
20140
|
+
const post = (url, body, context) => postJson({
|
|
20141
|
+
label: "DeepSeek",
|
|
20142
|
+
url,
|
|
20143
|
+
body,
|
|
20144
|
+
fetchImpl,
|
|
20145
|
+
context,
|
|
20146
|
+
resolveKey: requireKey,
|
|
20147
|
+
headers: (key) => ({ authorization: `Bearer ${key}` }),
|
|
20148
|
+
httpError
|
|
20149
|
+
});
|
|
20150
|
+
return {
|
|
20151
|
+
id: "deepseek",
|
|
20152
|
+
async search(query, limit, context) {
|
|
20153
|
+
if (!query.trim())
|
|
20154
|
+
throw new WebToolError("Provide a non-empty query.");
|
|
20155
|
+
if ([...query].length > DEEPSEEK_QUERY_MAX_CHARS)
|
|
20156
|
+
throw new WebToolError(`DeepSeek search queries are limited to ${DEEPSEEK_QUERY_MAX_CHARS} characters. Shorten the query and retry.`);
|
|
20157
|
+
await requireKey();
|
|
20158
|
+
const data = await post(SEARCH_URL, { query, count: limit, need_content: false }, context);
|
|
20159
|
+
if (!record(data) || !Array.isArray(data.results) || !data.results.every(record))
|
|
20160
|
+
throw new WebToolError("Invalid DeepSeek response.");
|
|
20161
|
+
return data.results.slice(0, limit).map((item) => {
|
|
20162
|
+
const date = dateOnly(item.date);
|
|
20163
|
+
return {
|
|
20164
|
+
title: String(item.title ?? "(untitled)"),
|
|
20165
|
+
url: String(item.url ?? ""),
|
|
20166
|
+
text: String(item.summary || item.snippet || ""),
|
|
20167
|
+
...date ? { date } : {}
|
|
20168
|
+
};
|
|
20169
|
+
});
|
|
20170
|
+
},
|
|
20171
|
+
async extract(urls, context) {
|
|
20172
|
+
await requireKey();
|
|
20173
|
+
const outcomes = new Array(urls.length);
|
|
20174
|
+
let next = 0;
|
|
20175
|
+
let fatal;
|
|
20176
|
+
const fetchOne = async (url) => {
|
|
20177
|
+
for (let attempt = 0;; attempt++) {
|
|
20178
|
+
try {
|
|
20179
|
+
const data = await post(FETCH_URL, { url }, context);
|
|
20180
|
+
if (!record(data) || typeof data.content !== "string")
|
|
20181
|
+
throw new WebToolError("Invalid DeepSeek response.");
|
|
20182
|
+
return { url, text: data.content };
|
|
20183
|
+
} catch (error) {
|
|
20184
|
+
const failure = error instanceof WebToolError ? error : new WebToolError("DeepSeek request failed.");
|
|
20185
|
+
if (context.abortSignal?.aborted || wholeCall(failure.status))
|
|
20186
|
+
throw failure;
|
|
20187
|
+
if (!retryable(failure.status) || attempt >= MAX_RETRIES)
|
|
20188
|
+
return { url, error: failure.message };
|
|
20189
|
+
try {
|
|
20190
|
+
await sleep(BACKOFF_MS[attempt] ?? 750, context.abortSignal);
|
|
20191
|
+
} catch {
|
|
20192
|
+
throw new WebToolError("Web request cancelled.");
|
|
20193
|
+
}
|
|
20194
|
+
}
|
|
20195
|
+
}
|
|
20196
|
+
};
|
|
20197
|
+
const worker = async () => {
|
|
20198
|
+
while (!fatal) {
|
|
20199
|
+
const index = next++;
|
|
20200
|
+
if (index >= urls.length)
|
|
20201
|
+
return;
|
|
20202
|
+
try {
|
|
20203
|
+
outcomes[index] = await fetchOne(urls[index]);
|
|
20204
|
+
} catch (error) {
|
|
20205
|
+
fatal ??= error instanceof WebToolError ? error : new WebToolError("DeepSeek request failed.");
|
|
20206
|
+
}
|
|
20207
|
+
}
|
|
20208
|
+
};
|
|
20209
|
+
await Promise.all(Array.from({ length: Math.min(FETCH_CONCURRENCY, urls.length) }, worker));
|
|
20210
|
+
if (fatal)
|
|
20211
|
+
throw fatal;
|
|
20212
|
+
return outcomes;
|
|
20213
|
+
}
|
|
20214
|
+
};
|
|
20215
|
+
}
|
|
20216
|
+
|
|
20217
|
+
// packages/core/src/web-search/index.ts
|
|
20218
|
+
import { Type as Type4 } from "@earendil-works/pi-ai";
|
|
20219
|
+
import { defineExtension as defineExtension8, defineTool as defineTool4 } from "@earendil-works/pi-durable";
|
|
20220
|
+
var MAX_URLS = 20;
|
|
20221
|
+
var MAX_RESULTS = 10;
|
|
20222
|
+
var DEFAULT_RESULTS = 5;
|
|
20223
|
+
var MAX_SOURCE_CHARS = 2000;
|
|
20224
|
+
var MAX_TOTAL_CHARS = 12000;
|
|
19715
20225
|
function toolError(error) {
|
|
19716
20226
|
return {
|
|
19717
20227
|
output: [
|
|
19718
|
-
{ type: "text", text: error instanceof
|
|
20228
|
+
{ type: "text", text: error instanceof WebToolError ? error.message : "Web tool failed. Please retry." }
|
|
19719
20229
|
],
|
|
19720
20230
|
isError: true
|
|
19721
20231
|
};
|
|
19722
20232
|
}
|
|
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
20233
|
function content(text) {
|
|
19733
20234
|
return { output: [{ type: "text", text }] };
|
|
19734
20235
|
}
|
|
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) {
|
|
20236
|
+
function createWebSearchTools(resolveProvider) {
|
|
19744
20237
|
return defineExtension8({
|
|
19745
20238
|
name: "neko-web-search",
|
|
19746
20239
|
tools: [
|
|
19747
20240
|
defineTool4({
|
|
19748
20241
|
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.",
|
|
20242
|
+
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
20243
|
parameters: Type4.Object({
|
|
19751
20244
|
query: Type4.String({ minLength: 1, maxLength: 4096, description: "The search query" }),
|
|
19752
20245
|
maxResults: Type4.Optional(Type4.Number({ description: `Maximum number of results (1-${MAX_RESULTS}, default ${DEFAULT_RESULTS})` }))
|
|
@@ -19754,18 +20247,19 @@ function createWebSearchTools(resolveKey, fetchImpl = fetch) {
|
|
|
19754
20247
|
async execute({ query, maxResults }, _api, context) {
|
|
19755
20248
|
try {
|
|
19756
20249
|
if (!query.trim() || query.length > 4096)
|
|
19757
|
-
throw new
|
|
20250
|
+
throw new WebToolError("Provide a non-empty query of at most 4096 characters.");
|
|
19758
20251
|
if (maxResults !== undefined && !Number.isFinite(maxResults))
|
|
19759
|
-
throw new
|
|
20252
|
+
throw new WebToolError("maxResults must be a finite number.");
|
|
19760
20253
|
const limit = Math.max(1, Math.min(MAX_RESULTS, Math.trunc(maxResults ?? DEFAULT_RESULTS)));
|
|
19761
|
-
const
|
|
19762
|
-
const results =
|
|
20254
|
+
const provider = await resolveProvider(context);
|
|
20255
|
+
const results = (await provider.search(query, limit, context)).slice(0, limit);
|
|
19763
20256
|
const budget = Math.floor(MAX_TOTAL_CHARS / Math.max(1, results.length)) - 2;
|
|
19764
20257
|
return content(results.map((item, index) => {
|
|
19765
|
-
const header = `${index + 1}. ${truncate(
|
|
19766
|
-
${truncate(
|
|
19767
|
-
|
|
19768
|
-
|
|
20258
|
+
const header = `${index + 1}. ${truncate(item.title, 200)}
|
|
20259
|
+
${truncate(item.url, 300)}
|
|
20260
|
+
${item.date ? `Published: ${item.date}
|
|
20261
|
+
` : ""}`;
|
|
20262
|
+
return header + truncate(item.text, Math.min(MAX_SOURCE_CHARS, budget - header.length));
|
|
19769
20263
|
}).join(`
|
|
19770
20264
|
|
|
19771
20265
|
`) || "No results.");
|
|
@@ -19787,28 +20281,25 @@ ${truncate(String(item.url ?? ""), 300)}
|
|
|
19787
20281
|
async execute({ urls }, _api, context) {
|
|
19788
20282
|
try {
|
|
19789
20283
|
if (!urls.length || urls.length > MAX_URLS)
|
|
19790
|
-
throw new
|
|
20284
|
+
throw new WebToolError(`Provide between 1 and ${MAX_URLS} URLs.`);
|
|
19791
20285
|
for (const url of urls) {
|
|
19792
20286
|
let parsed;
|
|
19793
20287
|
try {
|
|
19794
20288
|
parsed = new URL(url);
|
|
19795
20289
|
} catch {
|
|
19796
|
-
throw new
|
|
20290
|
+
throw new WebToolError("Provide valid http(s) URLs.");
|
|
19797
20291
|
}
|
|
19798
20292
|
if (url.length > 2048 || !["http:", "https:"].includes(parsed.protocol) || parsed.username || parsed.password)
|
|
19799
|
-
throw new
|
|
20293
|
+
throw new WebToolError("Provide http(s) URLs of at most 2048 characters without embedded credentials.");
|
|
19800
20294
|
}
|
|
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);
|
|
20295
|
+
const provider = await resolveProvider(context);
|
|
20296
|
+
const outcomes = await provider.extract(urls, context);
|
|
19806
20297
|
const budget = Math.floor(MAX_TOTAL_CHARS / urls.length) - 2;
|
|
19807
20298
|
return content(urls.map((url, index) => {
|
|
19808
20299
|
const header = `## ${index + 1}. ${truncate(url, 300)}
|
|
19809
20300
|
`;
|
|
19810
|
-
const
|
|
19811
|
-
const text =
|
|
20301
|
+
const outcome = outcomes[index];
|
|
20302
|
+
const text = outcome && "text" in outcome ? outcome.text : `Extraction failed: ${outcome?.error ?? "No result returned for this URL"}`;
|
|
19812
20303
|
return header + truncate(text, Math.min(MAX_SOURCE_CHARS, budget - header.length));
|
|
19813
20304
|
}).join(`
|
|
19814
20305
|
|
|
@@ -19822,6 +20313,69 @@ ${truncate(String(item.url ?? ""), 300)}
|
|
|
19822
20313
|
});
|
|
19823
20314
|
}
|
|
19824
20315
|
|
|
20316
|
+
// packages/core/src/web-search/tavily.ts
|
|
20317
|
+
var SEARCH_URL2 = "https://api.tavily.com/search";
|
|
20318
|
+
var EXTRACT_URL = "https://api.tavily.com/extract";
|
|
20319
|
+
function rows(data, field) {
|
|
20320
|
+
const value = data[field];
|
|
20321
|
+
if (!Array.isArray(value) || !value.every(record))
|
|
20322
|
+
throw new WebToolError("Invalid Tavily response.");
|
|
20323
|
+
return value;
|
|
20324
|
+
}
|
|
20325
|
+
function urlKey(value) {
|
|
20326
|
+
const text = String(value);
|
|
20327
|
+
try {
|
|
20328
|
+
return new URL(text).href;
|
|
20329
|
+
} catch {
|
|
20330
|
+
return text;
|
|
20331
|
+
}
|
|
20332
|
+
}
|
|
20333
|
+
function createTavilyProvider(resolveKey, fetchImpl = fetch) {
|
|
20334
|
+
const post = async (url, body, context) => {
|
|
20335
|
+
const data = await postJson({
|
|
20336
|
+
label: "Tavily",
|
|
20337
|
+
url,
|
|
20338
|
+
body,
|
|
20339
|
+
fetchImpl,
|
|
20340
|
+
context,
|
|
20341
|
+
resolveKey,
|
|
20342
|
+
headers: (key) => key ? { authorization: `Bearer ${key}` } : { "x-tavily-access-mode": "keyless" },
|
|
20343
|
+
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)
|
|
20344
|
+
});
|
|
20345
|
+
if (!record(data))
|
|
20346
|
+
throw new WebToolError("Invalid Tavily response.");
|
|
20347
|
+
if (data.error)
|
|
20348
|
+
throw new WebToolError(`Tavily request failed: ${truncate(JSON.stringify(data.error), 500)}`);
|
|
20349
|
+
return data;
|
|
20350
|
+
};
|
|
20351
|
+
return {
|
|
20352
|
+
id: "tavily",
|
|
20353
|
+
async search(query, limit, context) {
|
|
20354
|
+
const data = await post(SEARCH_URL2, { query, max_results: limit, search_depth: "basic" }, context);
|
|
20355
|
+
return rows(data, "results").slice(0, limit).map((item) => ({
|
|
20356
|
+
title: String(item.title ?? "(untitled)"),
|
|
20357
|
+
url: String(item.url ?? ""),
|
|
20358
|
+
text: String(item.content ?? "")
|
|
20359
|
+
}));
|
|
20360
|
+
},
|
|
20361
|
+
async extract(urls, context) {
|
|
20362
|
+
const data = await post(EXTRACT_URL, { urls, format: "markdown" }, context);
|
|
20363
|
+
const results = rows(data, "results");
|
|
20364
|
+
const failed = data.failed_results === undefined ? [] : rows(data, "failed_results");
|
|
20365
|
+
const byUrl = (items) => new Map(items.map((item) => [urlKey(item.url), item]));
|
|
20366
|
+
const successes = byUrl(results);
|
|
20367
|
+
const failures = byUrl(failed);
|
|
20368
|
+
return urls.map((url) => {
|
|
20369
|
+
const result = successes.get(urlKey(url));
|
|
20370
|
+
if (result)
|
|
20371
|
+
return { url, text: String(result.raw_content ?? "") };
|
|
20372
|
+
const failure = failures.get(urlKey(url));
|
|
20373
|
+
return { url, error: String(failure?.error ?? "No result returned for this URL") };
|
|
20374
|
+
});
|
|
20375
|
+
}
|
|
20376
|
+
};
|
|
20377
|
+
}
|
|
20378
|
+
|
|
19825
20379
|
// packages/core/src/workspace-files.ts
|
|
19826
20380
|
import { realpath as realpath2, stat } from "fs/promises";
|
|
19827
20381
|
import { basename as basename2, isAbsolute as isAbsolute2, relative, resolve as resolve3, sep } from "path";
|
|
@@ -20201,6 +20755,22 @@ var WorkspacesDoc = defineDoc5({
|
|
|
20201
20755
|
fork: "initial",
|
|
20202
20756
|
initial: () => ({ items: [] })
|
|
20203
20757
|
});
|
|
20758
|
+
var SearchSettingsDoc = defineDoc5({
|
|
20759
|
+
kind: "app.neko.search-settings",
|
|
20760
|
+
version: 1,
|
|
20761
|
+
scope: "conversation",
|
|
20762
|
+
history: "latest",
|
|
20763
|
+
fork: "initial",
|
|
20764
|
+
initial: () => ({ provider: null })
|
|
20765
|
+
});
|
|
20766
|
+
var ContextSettingsDoc = defineDoc5({
|
|
20767
|
+
kind: "app.neko.context-settings",
|
|
20768
|
+
version: 1,
|
|
20769
|
+
scope: "conversation",
|
|
20770
|
+
history: "latest",
|
|
20771
|
+
fork: "initial",
|
|
20772
|
+
initial: emptySavedContextSettings
|
|
20773
|
+
});
|
|
20204
20774
|
var ActivityDoc = defineDoc5({
|
|
20205
20775
|
kind: "app.neko.activity",
|
|
20206
20776
|
version: 1,
|
|
@@ -20247,12 +20817,21 @@ async function createNekoRuntime(options = {}) {
|
|
|
20247
20817
|
registry.install(createCodingTools({ images: await createNodePhotonImages() }));
|
|
20248
20818
|
registry.install(ImageDetails);
|
|
20249
20819
|
registry.install(AgentFileSearch);
|
|
20250
|
-
|
|
20820
|
+
const tavily = createTavilyProvider(async () => {
|
|
20251
20821
|
const saved = await effectiveCredentials.read("tavily");
|
|
20252
20822
|
if (saved?.type === "api_key" && saved.key)
|
|
20253
20823
|
return saved.key;
|
|
20254
20824
|
return process.env.TAVILY_API_KEY || undefined;
|
|
20255
|
-
})
|
|
20825
|
+
});
|
|
20826
|
+
const deepseekApiKey = async () => {
|
|
20827
|
+
const saved = await effectiveCredentials.read("deepseek");
|
|
20828
|
+
if (saved?.type === "api_key" && saved.key)
|
|
20829
|
+
return saved.key;
|
|
20830
|
+
return process.env.DEEPSEEK_API_KEY || undefined;
|
|
20831
|
+
};
|
|
20832
|
+
const deepseek = createDeepseekProvider(deepseekApiKey);
|
|
20833
|
+
let searchProvider = options.searchProvider ?? DEFAULT_SEARCH_PROVIDER;
|
|
20834
|
+
registry.install(createWebSearchTools(async () => searchProvider === "deepseek" ? deepseek : tavily));
|
|
20256
20835
|
const loadResources = createResourceLoader(options.promptHome);
|
|
20257
20836
|
const resourceSnapshots = createResourceSnapshots(loadResources, (id, requestId) => storage.submissionByRequest(id, requestId, context));
|
|
20258
20837
|
registry.install(createNekoPrompt(resourceSnapshots.current));
|
|
@@ -20266,9 +20845,16 @@ async function createNekoRuntime(options = {}) {
|
|
|
20266
20845
|
let approvals;
|
|
20267
20846
|
const approvalExtension = createApprovalExtension(() => approvals);
|
|
20268
20847
|
registry.install(approvalExtension);
|
|
20848
|
+
let savedContext = emptySavedContextSettings();
|
|
20849
|
+
let contextState = resolveContextSettings(options.contextSettings, savedContext);
|
|
20269
20850
|
const harness = await Harness.open(storage, {
|
|
20270
20851
|
models,
|
|
20271
20852
|
registry,
|
|
20853
|
+
settings: {
|
|
20854
|
+
get compaction() {
|
|
20855
|
+
return compactionPolicy(contextState, models.getModels().map((model) => model.contextWindow));
|
|
20856
|
+
}
|
|
20857
|
+
},
|
|
20272
20858
|
onReport: options.onReport,
|
|
20273
20859
|
conversationCreated: async (tx, record) => {
|
|
20274
20860
|
(await tx.doc(SessionMetadataDoc, record.id)).createdAt = Date.now();
|
|
@@ -20310,6 +20896,21 @@ async function createNekoRuntime(options = {}) {
|
|
|
20310
20896
|
cwd: defaultCwd
|
|
20311
20897
|
}
|
|
20312
20898
|
});
|
|
20899
|
+
const savedSearch = (await harness.snapshot(SearchSettingsDoc, conversation.id, context))?.provider;
|
|
20900
|
+
if (isSearchProvider(savedSearch))
|
|
20901
|
+
searchProvider = savedSearch;
|
|
20902
|
+
savedContext = sanitizeSavedContextSettings(await harness.snapshot(ContextSettingsDoc, conversation.id, context));
|
|
20903
|
+
contextState = resolveContextSettings(options.contextSettings, savedContext);
|
|
20904
|
+
const persistContext = async (next) => {
|
|
20905
|
+
await harness.commit(async (tx) => {
|
|
20906
|
+
const doc = await tx.doc(ContextSettingsDoc, conversation.id);
|
|
20907
|
+
doc.compaction = next.compaction;
|
|
20908
|
+
doc.pruning = next.pruning;
|
|
20909
|
+
}, context);
|
|
20910
|
+
savedContext = next;
|
|
20911
|
+
contextState = resolveContextSettings(options.contextSettings, savedContext);
|
|
20912
|
+
return contextState;
|
|
20913
|
+
};
|
|
20313
20914
|
const registerWorkspace = async (path) => {
|
|
20314
20915
|
await harness.commit(async (tx) => {
|
|
20315
20916
|
const doc = await tx.doc(WorkspacesDoc, conversation.id);
|
|
@@ -20346,10 +20947,22 @@ async function createNekoRuntime(options = {}) {
|
|
|
20346
20947
|
for (const record of existing)
|
|
20347
20948
|
if (!record.owner && !(await harness.snapshot(ArchivedDoc, record.id, context))?.archived)
|
|
20348
20949
|
await metadata.restore(record.id);
|
|
20950
|
+
const pruner = new ContextPruner({
|
|
20951
|
+
harness,
|
|
20952
|
+
context,
|
|
20953
|
+
settings: () => contextState.effective.pruning,
|
|
20954
|
+
windowOf: async (id) => {
|
|
20955
|
+
const target = await harness.conversation(id, context);
|
|
20956
|
+
const model = (await target?.agent(context))?.model;
|
|
20957
|
+
return model && models.getModel(model.provider, model.modelId)?.contextWindow || 0;
|
|
20958
|
+
},
|
|
20959
|
+
report: (error) => options.onReport?.(error)
|
|
20960
|
+
});
|
|
20349
20961
|
let closing;
|
|
20350
20962
|
const cleanup = () => {
|
|
20351
20963
|
if (!closing)
|
|
20352
20964
|
closing = (async () => {
|
|
20965
|
+
pruner.close();
|
|
20353
20966
|
jobs.shutdown();
|
|
20354
20967
|
await workspaceFiles.close();
|
|
20355
20968
|
await metadata.close();
|
|
@@ -20506,6 +21119,57 @@ async function createNekoRuntime(options = {}) {
|
|
|
20506
21119
|
},
|
|
20507
21120
|
abort() {
|
|
20508
21121
|
return target.abort(context);
|
|
21122
|
+
},
|
|
21123
|
+
async withdraw(submissionId) {
|
|
21124
|
+
const id = submissionId;
|
|
21125
|
+
const inbox = await harness.snapshot(InboxDoc2, target.id, context);
|
|
21126
|
+
const item = inbox?.items.find((i) => i.id === id);
|
|
21127
|
+
const input = item ? queuedUserInput(item) : undefined;
|
|
21128
|
+
if (item && !input)
|
|
21129
|
+
return { result: "unknown" };
|
|
21130
|
+
const requestId = (await (await harness.submission(id, context))?.status(context))?.requestId;
|
|
21131
|
+
const outcome = await harness.abortSubmission(id, context, target.id);
|
|
21132
|
+
if (outcome !== "aborted") {
|
|
21133
|
+
return { result: outcome === "already_placed" ? "placed" : outcome === "settled" ? "settled" : "unknown" };
|
|
21134
|
+
}
|
|
21135
|
+
if (requestId !== undefined)
|
|
21136
|
+
resourceSnapshots.discard(target.id, requestId);
|
|
21137
|
+
await metadata.reconcile(target.id);
|
|
21138
|
+
return { result: "withdrawn", ...input ? { text: input.text, images: input.images } : {} };
|
|
21139
|
+
},
|
|
21140
|
+
async steer(submissionId) {
|
|
21141
|
+
const id = submissionId;
|
|
21142
|
+
const inbox = await harness.snapshot(InboxDoc2, target.id, context);
|
|
21143
|
+
const item = inbox?.items.find((i) => i.id === id);
|
|
21144
|
+
const input = item ? queuedUserInput(item) : undefined;
|
|
21145
|
+
if (item && (!input || item.mode === "write"))
|
|
21146
|
+
return { result: "unknown" };
|
|
21147
|
+
if (item && input?.mode === "steer")
|
|
21148
|
+
return { result: "steered", id: submissionId };
|
|
21149
|
+
const requestId = (await (await harness.submission(id, context))?.status(context))?.requestId;
|
|
21150
|
+
const outcome = await harness.abortSubmission(id, context, target.id);
|
|
21151
|
+
if (outcome !== "aborted" || !item || !input) {
|
|
21152
|
+
return { result: outcome === "already_placed" ? "placed" : outcome === "settled" ? "settled" : "unknown" };
|
|
21153
|
+
}
|
|
21154
|
+
const steerRequestId = requestId !== undefined ? `steer:${requestId}` : crypto.randomUUID();
|
|
21155
|
+
try {
|
|
21156
|
+
const next = await target.submit({
|
|
21157
|
+
type: "input",
|
|
21158
|
+
content: item.content,
|
|
21159
|
+
...item.entry ? { entry: item.entry } : {},
|
|
21160
|
+
requestId: steerRequestId,
|
|
21161
|
+
whenBusy: "steer"
|
|
21162
|
+
}, context);
|
|
21163
|
+
if (requestId !== undefined)
|
|
21164
|
+
resourceSnapshots.move(target.id, requestId, steerRequestId);
|
|
21165
|
+
await metadata.reconcile(target.id);
|
|
21166
|
+
return { result: "steered", id: next.id };
|
|
21167
|
+
} catch {
|
|
21168
|
+
if (requestId !== undefined)
|
|
21169
|
+
resourceSnapshots.discard(target.id, requestId);
|
|
21170
|
+
await metadata.reconcile(target.id);
|
|
21171
|
+
return { result: "withdrawn", text: input.text, images: input.images };
|
|
21172
|
+
}
|
|
20509
21173
|
}
|
|
20510
21174
|
});
|
|
20511
21175
|
const rootSession = makeSession(conversation);
|
|
@@ -20716,8 +21380,28 @@ async function createNekoRuntime(options = {}) {
|
|
|
20716
21380
|
source: runtimeKeys.has(spec.id) ? "runtime" : savedConfigured ? "saved" : environmentConfigured ? "environment" : spec.keyless ? "keyless" : "none"
|
|
20717
21381
|
};
|
|
20718
21382
|
},
|
|
20719
|
-
async
|
|
20720
|
-
|
|
21383
|
+
async contextSettings() {
|
|
21384
|
+
return contextState;
|
|
21385
|
+
},
|
|
21386
|
+
async setContextSettings(patch) {
|
|
21387
|
+
return persistContext(applyContextPatch(savedContext, parseContextSettingsPatch(patch)));
|
|
21388
|
+
},
|
|
21389
|
+
async resetContextSettings() {
|
|
21390
|
+
return persistContext(emptySavedContextSettings());
|
|
21391
|
+
},
|
|
21392
|
+
async searchSettings() {
|
|
21393
|
+
return { selected: searchProvider };
|
|
21394
|
+
},
|
|
21395
|
+
async setSearchProvider(provider) {
|
|
21396
|
+
if (!isSearchProvider(provider))
|
|
21397
|
+
throw new Error("Unknown search provider.");
|
|
21398
|
+
await harness.commit(async (tx) => {
|
|
21399
|
+
(await tx.doc(SearchSettingsDoc, conversation.id)).provider = provider;
|
|
21400
|
+
}, context);
|
|
21401
|
+
searchProvider = provider;
|
|
21402
|
+
},
|
|
21403
|
+
async saveCredential(provider, key) {
|
|
21404
|
+
const spec = credentialProvider(provider);
|
|
20721
21405
|
if (typeof key !== "string" || !key.trim() || key.length > 4096 || Array.from(key).some((char) => char.charCodeAt(0) < 32))
|
|
20722
21406
|
throw new Error("Enter a valid API key.");
|
|
20723
21407
|
try {
|
|
@@ -20789,506 +21473,34 @@ async function createNekoRuntime(options = {}) {
|
|
|
20789
21473
|
}
|
|
20790
21474
|
};
|
|
20791
21475
|
}
|
|
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";
|
|
21476
|
+
// packages/gateway/src/contract.ts
|
|
21477
|
+
import { hostNamed, method, remote } from "@earendil-works/chord";
|
|
20800
21478
|
import { Type as Type5 } from "typebox";
|
|
20801
|
-
|
|
20802
|
-
|
|
20803
|
-
|
|
20804
|
-
|
|
20805
|
-
|
|
20806
|
-
|
|
20807
|
-
|
|
20808
|
-
|
|
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";
|
|
21268
|
-
var rpc = (args, result) => [args, result];
|
|
21269
|
-
function contract2(name, def) {
|
|
21270
|
-
const variants = Object.fromEntries(Object.entries(def.variants).map(([key, value]) => [
|
|
21271
|
-
key,
|
|
21272
|
-
{
|
|
21273
|
-
...value,
|
|
21274
|
-
methods: Object.fromEntries(Object.entries(value.methods ?? {}).map(([member, [args, result]]) => [member, method2(args, result)]))
|
|
21479
|
+
var GATEWAY_HOST = "neko";
|
|
21480
|
+
var rpc = (args, result) => [args, result];
|
|
21481
|
+
function contract(name, def) {
|
|
21482
|
+
const variants = Object.fromEntries(Object.entries(def.variants).map(([key, value]) => [
|
|
21483
|
+
key,
|
|
21484
|
+
{
|
|
21485
|
+
...value,
|
|
21486
|
+
methods: Object.fromEntries(Object.entries(value.methods ?? {}).map(([member, [args, result]]) => [member, method(args, result)]))
|
|
21275
21487
|
}
|
|
21276
21488
|
]));
|
|
21277
|
-
return
|
|
21489
|
+
return remote(name, { ...def, host: hostNamed(GATEWAY_HOST), variants });
|
|
21278
21490
|
}
|
|
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 =
|
|
21491
|
+
var str = Type5.String();
|
|
21492
|
+
var num = Type5.Number();
|
|
21493
|
+
var bool = Type5.Boolean();
|
|
21494
|
+
var nil = Type5.Null();
|
|
21495
|
+
var integer = Type5.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER });
|
|
21496
|
+
var nullableString = Type5.Union([str, nil]);
|
|
21497
|
+
var phase = Type5.Enum(["pending", "running", "done"]);
|
|
21498
|
+
var level = Type5.Enum(["project", "user", "builtin"]);
|
|
21499
|
+
var thinking = Type5.Enum(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
21500
|
+
var model = Type5.Object({ provider: str, modelId: str });
|
|
21501
|
+
var change = Type5.Object({ model: Type5.Optional(model), thinkingLevel: Type5.Optional(thinking) }, { additionalProperties: false });
|
|
21502
|
+
var usage = Type5.Object({ input: num, output: num, cacheRead: num, cacheWrite: num, cost: num });
|
|
21503
|
+
var image = Type5.Object({
|
|
21292
21504
|
name: str,
|
|
21293
21505
|
mimeType: str,
|
|
21294
21506
|
width: integer,
|
|
@@ -21297,8 +21509,8 @@ var image = Type6.Object({
|
|
|
21297
21509
|
entryId: integer,
|
|
21298
21510
|
contentIndex: integer
|
|
21299
21511
|
});
|
|
21300
|
-
var skill =
|
|
21301
|
-
var call =
|
|
21512
|
+
var skill = Type5.Object({ name: str, location: str, body: str, args: str });
|
|
21513
|
+
var call = Type5.Object({
|
|
21302
21514
|
callId: str,
|
|
21303
21515
|
name: str,
|
|
21304
21516
|
summary: str,
|
|
@@ -21306,116 +21518,135 @@ var call = Type6.Object({
|
|
|
21306
21518
|
phase,
|
|
21307
21519
|
output: nullableString,
|
|
21308
21520
|
diff: nullableString,
|
|
21309
|
-
diffAdded:
|
|
21310
|
-
diffRemoved:
|
|
21311
|
-
diffTruncated:
|
|
21521
|
+
diffAdded: Type5.Union([num, nil]),
|
|
21522
|
+
diffRemoved: Type5.Union([num, nil]),
|
|
21523
|
+
diffTruncated: Type5.Optional(bool)
|
|
21312
21524
|
});
|
|
21313
|
-
var DelegationSchema =
|
|
21525
|
+
var DelegationSchema = Type5.Object({
|
|
21314
21526
|
parentId: integer,
|
|
21315
|
-
agentId:
|
|
21316
|
-
round:
|
|
21317
|
-
runnerId:
|
|
21318
|
-
inputEntryId:
|
|
21319
|
-
answerEntryId:
|
|
21320
|
-
roundUsage:
|
|
21527
|
+
agentId: Type5.Optional(str),
|
|
21528
|
+
round: Type5.Optional(integer),
|
|
21529
|
+
runnerId: Type5.Optional(integer),
|
|
21530
|
+
inputEntryId: Type5.Optional(integer),
|
|
21531
|
+
answerEntryId: Type5.Optional(integer),
|
|
21532
|
+
roundUsage: Type5.Optional(Type5.Union([usage, nil])),
|
|
21321
21533
|
taskId: integer,
|
|
21322
21534
|
callId: str,
|
|
21323
21535
|
childId: integer,
|
|
21324
|
-
role:
|
|
21536
|
+
role: Type5.Enum(["explore", "plan", "coder"]),
|
|
21325
21537
|
title: str,
|
|
21326
21538
|
createdAt: num,
|
|
21327
|
-
startedAt:
|
|
21328
|
-
endedAt:
|
|
21329
|
-
status:
|
|
21539
|
+
startedAt: Type5.Union([num, nil]),
|
|
21540
|
+
endedAt: Type5.Union([num, nil]),
|
|
21541
|
+
status: Type5.Enum(["queued", "running", "completed", "failed", "cancelled"]),
|
|
21330
21542
|
report: nullableString,
|
|
21331
|
-
approvalCount:
|
|
21332
|
-
historyThrough:
|
|
21333
|
-
historyUsage:
|
|
21543
|
+
approvalCount: Type5.Optional(integer),
|
|
21544
|
+
historyThrough: Type5.Optional(integer),
|
|
21545
|
+
historyUsage: Type5.Optional(usage)
|
|
21334
21546
|
});
|
|
21335
|
-
var SubagentSummarySchema =
|
|
21547
|
+
var SubagentSummarySchema = Type5.Object({
|
|
21336
21548
|
agentId: str,
|
|
21337
21549
|
parentId: integer,
|
|
21338
21550
|
childId: integer,
|
|
21339
|
-
role:
|
|
21551
|
+
role: Type5.Enum(["explore", "plan", "coder"]),
|
|
21340
21552
|
title: str,
|
|
21341
|
-
status:
|
|
21342
|
-
activeRoundId:
|
|
21553
|
+
status: Type5.Enum(["idle", "queued", "running"]),
|
|
21554
|
+
activeRoundId: Type5.Union([integer, nil]),
|
|
21343
21555
|
lastRoundId: integer,
|
|
21344
|
-
lastReport:
|
|
21556
|
+
lastReport: Type5.Optional(nullableString),
|
|
21345
21557
|
roundCount: integer,
|
|
21346
21558
|
taskTitle: str,
|
|
21347
21559
|
approvalCount: integer,
|
|
21348
21560
|
createdAt: num,
|
|
21349
|
-
historyThrough:
|
|
21350
|
-
historyUsage:
|
|
21561
|
+
historyThrough: Type5.Optional(integer),
|
|
21562
|
+
historyUsage: Type5.Optional(usage)
|
|
21351
21563
|
});
|
|
21352
|
-
var jobStatus =
|
|
21353
|
-
var JobSummarySchema =
|
|
21564
|
+
var jobStatus = Type5.Enum(["running", "exited", "failed", "stopped", "lost"]);
|
|
21565
|
+
var JobSummarySchema = Type5.Object({
|
|
21354
21566
|
jobId: str,
|
|
21355
21567
|
title: str,
|
|
21356
21568
|
command: str,
|
|
21357
21569
|
cwd: str,
|
|
21358
21570
|
status: jobStatus,
|
|
21359
21571
|
startedAt: num,
|
|
21360
|
-
endedAt:
|
|
21361
|
-
exitCode:
|
|
21362
|
-
reason:
|
|
21363
|
-
timeoutSeconds:
|
|
21572
|
+
endedAt: Type5.Union([num, nil]),
|
|
21573
|
+
exitCode: Type5.Union([num, nil]),
|
|
21574
|
+
reason: Type5.Optional(Type5.Enum(["timeout", "spawn-error", "restart", "aborted"])),
|
|
21575
|
+
timeoutSeconds: Type5.Optional(num),
|
|
21364
21576
|
outputBytes: integer
|
|
21365
21577
|
});
|
|
21366
|
-
var JobOutputSchema =
|
|
21578
|
+
var JobOutputSchema = Type5.Object({
|
|
21367
21579
|
job: JobSummarySchema,
|
|
21368
21580
|
text: str,
|
|
21369
21581
|
offset: integer,
|
|
21370
21582
|
truncated: bool,
|
|
21371
21583
|
dropped: integer
|
|
21372
21584
|
});
|
|
21373
|
-
var
|
|
21374
|
-
|
|
21375
|
-
|
|
21376
|
-
|
|
21377
|
-
|
|
21378
|
-
|
|
21379
|
-
|
|
21585
|
+
var QueuedInputSchema = Type5.Object({
|
|
21586
|
+
id: integer,
|
|
21587
|
+
mode: Type5.Enum(["steer", "followUp"]),
|
|
21588
|
+
preview: str,
|
|
21589
|
+
images: integer,
|
|
21590
|
+
skill: Type5.Optional(Type5.Object({ name: str, args: str }))
|
|
21591
|
+
});
|
|
21592
|
+
var WithdrawResultSchema = Type5.Object({
|
|
21593
|
+
result: Type5.Enum(["withdrawn", "placed", "settled", "unknown"]),
|
|
21594
|
+
text: Type5.Optional(str),
|
|
21595
|
+
images: Type5.Optional(integer)
|
|
21596
|
+
});
|
|
21597
|
+
var SteerResultSchema = Type5.Object({
|
|
21598
|
+
result: Type5.Enum(["steered", "withdrawn", "placed", "settled", "unknown"]),
|
|
21599
|
+
id: Type5.Optional(integer),
|
|
21600
|
+
text: Type5.Optional(str),
|
|
21601
|
+
images: Type5.Optional(integer)
|
|
21602
|
+
});
|
|
21603
|
+
var ChatViewSchema = Type5.Object({
|
|
21604
|
+
historyBefore: Type5.Optional(Type5.Union([integer, nil])),
|
|
21605
|
+
agents: Type5.Optional(Type5.Array(SubagentSummarySchema)),
|
|
21606
|
+
jobs: Type5.Optional(Type5.Array(JobSummarySchema)),
|
|
21607
|
+
queued: Type5.Optional(Type5.Array(QueuedInputSchema)),
|
|
21608
|
+
rounds: Type5.Optional(Type5.Array(DelegationSchema)),
|
|
21609
|
+
subagents: Type5.Optional(Type5.Array(DelegationSchema)),
|
|
21610
|
+
taskUsage: Type5.Optional(usage),
|
|
21380
21611
|
model: str,
|
|
21381
|
-
agent:
|
|
21612
|
+
agent: Type5.Object({ model: Type5.Union([model, nil]), thinkingLevel: Type5.Union([thinking, nil]) }),
|
|
21382
21613
|
cwd: str,
|
|
21383
21614
|
busy: bool,
|
|
21384
21615
|
status: nullableString,
|
|
21385
21616
|
usage,
|
|
21386
|
-
contextUsage:
|
|
21387
|
-
compaction:
|
|
21388
|
-
failure:
|
|
21389
|
-
active:
|
|
21617
|
+
contextUsage: Type5.Optional(Type5.Object({ tokens: Type5.Union([num, nil]), window: num, percent: Type5.Union([num, nil]) })),
|
|
21618
|
+
compaction: Type5.Optional(Type5.Object({
|
|
21619
|
+
failure: Type5.Optional(Type5.Object({ taskId: integer, reason: Type5.Enum(["manual", "threshold", "overflow"]), error: str })),
|
|
21620
|
+
active: Type5.Array(Type5.Object({
|
|
21390
21621
|
taskId: integer,
|
|
21391
|
-
reason:
|
|
21622
|
+
reason: Type5.Enum(["manual", "threshold", "overflow"]),
|
|
21392
21623
|
blocking: bool,
|
|
21393
21624
|
attempt: integer,
|
|
21394
|
-
retry:
|
|
21625
|
+
retry: Type5.Optional(Type5.Object({ at: num, error: str }))
|
|
21395
21626
|
})),
|
|
21396
|
-
latest:
|
|
21627
|
+
latest: Type5.Union([
|
|
21397
21628
|
nil,
|
|
21398
|
-
|
|
21629
|
+
Type5.Object({
|
|
21399
21630
|
taskId: integer,
|
|
21400
|
-
phase:
|
|
21631
|
+
phase: Type5.Enum(["summarizing", "waiting-to-apply", "applied", "nothing", "skipped", "cancelled", "failed"]),
|
|
21401
21632
|
at: num
|
|
21402
21633
|
})
|
|
21403
21634
|
])
|
|
21404
21635
|
})),
|
|
21405
|
-
items:
|
|
21406
|
-
|
|
21407
|
-
type:
|
|
21408
|
-
order:
|
|
21636
|
+
items: Type5.Array(Type5.Union([
|
|
21637
|
+
Type5.Object({
|
|
21638
|
+
type: Type5.Literal("user"),
|
|
21639
|
+
order: Type5.Optional(num),
|
|
21409
21640
|
key: str,
|
|
21410
21641
|
id: integer,
|
|
21411
21642
|
text: str,
|
|
21412
|
-
images:
|
|
21413
|
-
skill:
|
|
21414
|
-
defaultImagePrompt:
|
|
21643
|
+
images: Type5.Optional(Type5.Array(image)),
|
|
21644
|
+
skill: Type5.Optional(skill),
|
|
21645
|
+
defaultImagePrompt: Type5.Optional(Type5.Object({ version: Type5.Integer({ minimum: 1 }), text: str }))
|
|
21415
21646
|
}),
|
|
21416
|
-
|
|
21417
|
-
type:
|
|
21418
|
-
order:
|
|
21647
|
+
Type5.Object({
|
|
21648
|
+
type: Type5.Literal("assistant"),
|
|
21649
|
+
order: Type5.Optional(num),
|
|
21419
21650
|
key: str,
|
|
21420
21651
|
id: integer,
|
|
21421
21652
|
thinking: str,
|
|
@@ -21424,11 +21655,11 @@ var ChatViewSchema = Type6.Object({
|
|
|
21424
21655
|
errorMessage: nullableString,
|
|
21425
21656
|
note: nullableString
|
|
21426
21657
|
}),
|
|
21427
|
-
|
|
21428
|
-
type:
|
|
21429
|
-
order:
|
|
21658
|
+
Type5.Object({
|
|
21659
|
+
type: Type5.Literal("tool"),
|
|
21660
|
+
order: Type5.Optional(num),
|
|
21430
21661
|
key: str,
|
|
21431
|
-
images:
|
|
21662
|
+
images: Type5.Optional(Type5.Array(image)),
|
|
21432
21663
|
callId: str,
|
|
21433
21664
|
name: str,
|
|
21434
21665
|
summary: str,
|
|
@@ -21437,61 +21668,61 @@ var ChatViewSchema = Type6.Object({
|
|
|
21437
21668
|
isError: bool,
|
|
21438
21669
|
output: nullableString,
|
|
21439
21670
|
result: nullableString,
|
|
21440
|
-
durationMs:
|
|
21671
|
+
durationMs: Type5.Union([num, nil]),
|
|
21441
21672
|
note: nullableString,
|
|
21442
21673
|
diff: nullableString,
|
|
21443
|
-
diffAdded:
|
|
21444
|
-
diffRemoved:
|
|
21445
|
-
diffTruncated:
|
|
21674
|
+
diffAdded: Type5.Union([num, nil]),
|
|
21675
|
+
diffRemoved: Type5.Union([num, nil]),
|
|
21676
|
+
diffTruncated: Type5.Optional(bool)
|
|
21446
21677
|
}),
|
|
21447
|
-
|
|
21448
|
-
type:
|
|
21449
|
-
order:
|
|
21678
|
+
Type5.Object({
|
|
21679
|
+
type: Type5.Literal("system"),
|
|
21680
|
+
order: Type5.Optional(num),
|
|
21450
21681
|
key: str,
|
|
21451
21682
|
id: integer,
|
|
21452
21683
|
text: str,
|
|
21453
|
-
jobId:
|
|
21684
|
+
jobId: Type5.Optional(str)
|
|
21454
21685
|
})
|
|
21455
21686
|
])),
|
|
21456
|
-
streaming:
|
|
21687
|
+
streaming: Type5.Union([nil, Type5.Object({ thinking: str, text: str, toolCalls: Type5.Array(call) })])
|
|
21457
21688
|
});
|
|
21458
|
-
var SessionSummarySchema =
|
|
21689
|
+
var SessionSummarySchema = Type5.Object({
|
|
21459
21690
|
workspaceRoot: str,
|
|
21460
21691
|
lastUsedAt: num,
|
|
21461
21692
|
title: str,
|
|
21462
21693
|
firstPromptPreview: str,
|
|
21463
21694
|
createdAt: num,
|
|
21464
|
-
lastMessageAt:
|
|
21465
|
-
model:
|
|
21466
|
-
status:
|
|
21695
|
+
lastMessageAt: Type5.Union([num, nil]),
|
|
21696
|
+
model: Type5.Union([model, nil]),
|
|
21697
|
+
status: Type5.Enum(["idle", "running", "error"]),
|
|
21467
21698
|
id: str
|
|
21468
21699
|
});
|
|
21469
|
-
var DirectoryStateSchema =
|
|
21470
|
-
var WorkspaceSummarySchema =
|
|
21471
|
-
var WorkspaceStateSchema =
|
|
21472
|
-
var credentialStatus =
|
|
21700
|
+
var DirectoryStateSchema = Type5.Object({ revision: integer, sessions: Type5.Array(SessionSummarySchema) });
|
|
21701
|
+
var WorkspaceSummarySchema = Type5.Object({ path: str, createdAt: num });
|
|
21702
|
+
var WorkspaceStateSchema = Type5.Object({ revision: integer, workspaces: Type5.Array(WorkspaceSummarySchema) });
|
|
21703
|
+
var credentialStatus = Type5.Object({
|
|
21473
21704
|
savedConfigured: bool,
|
|
21474
21705
|
environmentConfigured: bool,
|
|
21475
|
-
source:
|
|
21706
|
+
source: Type5.Enum(["saved", "environment", "runtime", "keyless", "none"])
|
|
21476
21707
|
});
|
|
21477
|
-
var CredentialStateSchema =
|
|
21708
|
+
var CredentialStateSchema = Type5.Object({
|
|
21478
21709
|
revision: integer,
|
|
21479
|
-
statuses:
|
|
21710
|
+
statuses: Type5.Object({ deepseek: credentialStatus, tavily: credentialStatus })
|
|
21480
21711
|
});
|
|
21481
|
-
var AttachmentSchema =
|
|
21482
|
-
var chatInput =
|
|
21483
|
-
var NekoSessionDirectory =
|
|
21712
|
+
var AttachmentSchema = Type5.Object({ sessionId: str, attachmentId: str });
|
|
21713
|
+
var chatInput = Type5.Object({ sessionId: str, attachmentId: str }, { additionalProperties: false });
|
|
21714
|
+
var NekoSessionDirectory = contract("neko.session-directory", {
|
|
21484
21715
|
version: 1,
|
|
21485
|
-
input:
|
|
21716
|
+
input: Type5.Object({}, { additionalProperties: false }),
|
|
21486
21717
|
variants: { ready: { state: { state: DirectoryStateSchema } } }
|
|
21487
21718
|
});
|
|
21488
|
-
var NekoSessionManagement =
|
|
21719
|
+
var NekoSessionManagement = contract("neko.session-management", {
|
|
21489
21720
|
version: 1,
|
|
21490
|
-
input:
|
|
21721
|
+
input: Type5.Object({}, { additionalProperties: false }),
|
|
21491
21722
|
variants: {
|
|
21492
21723
|
ready: {
|
|
21493
21724
|
methods: {
|
|
21494
|
-
create: rpc(
|
|
21725
|
+
create: rpc(Type5.Object({ ...change.properties, cwd: Type5.Optional(str) }, { additionalProperties: false }), SessionSummarySchema),
|
|
21495
21726
|
archive: rpc(str, nil),
|
|
21496
21727
|
attach: rpc(str, AttachmentSchema),
|
|
21497
21728
|
detach: rpc(nil, nil),
|
|
@@ -21500,30 +21731,30 @@ var NekoSessionManagement = contract2("neko.session-management", {
|
|
|
21500
21731
|
}
|
|
21501
21732
|
}
|
|
21502
21733
|
});
|
|
21503
|
-
var resize =
|
|
21504
|
-
maxWidth:
|
|
21505
|
-
maxHeight:
|
|
21506
|
-
maxBytes:
|
|
21507
|
-
jpegQuality:
|
|
21734
|
+
var resize = Type5.Object({
|
|
21735
|
+
maxWidth: Type5.Optional(integer),
|
|
21736
|
+
maxHeight: Type5.Optional(integer),
|
|
21737
|
+
maxBytes: Type5.Optional(integer),
|
|
21738
|
+
jpegQuality: Type5.Optional(integer)
|
|
21508
21739
|
});
|
|
21509
|
-
var inputLimits =
|
|
21510
|
-
maxRequestBytes:
|
|
21511
|
-
images:
|
|
21512
|
-
resize:
|
|
21513
|
-
maxPerMessage:
|
|
21514
|
-
maxPerRequest:
|
|
21740
|
+
var inputLimits = Type5.Object({
|
|
21741
|
+
maxRequestBytes: Type5.Optional(integer),
|
|
21742
|
+
images: Type5.Optional(Type5.Object({
|
|
21743
|
+
resize: Type5.Optional(resize),
|
|
21744
|
+
maxPerMessage: Type5.Optional(integer),
|
|
21745
|
+
maxPerRequest: Type5.Optional(integer)
|
|
21515
21746
|
}))
|
|
21516
21747
|
});
|
|
21517
|
-
var modelInfo =
|
|
21748
|
+
var modelInfo = Type5.Object({
|
|
21518
21749
|
provider: str,
|
|
21519
21750
|
modelId: str,
|
|
21520
21751
|
name: str,
|
|
21521
21752
|
contextWindow: num,
|
|
21522
|
-
input:
|
|
21523
|
-
inputLimits:
|
|
21753
|
+
input: Type5.Array(str),
|
|
21754
|
+
inputLimits: Type5.Optional(inputLimits),
|
|
21524
21755
|
reasoning: bool,
|
|
21525
|
-
supportedThinkingLevels:
|
|
21526
|
-
thinkingLevelAdjustments:
|
|
21756
|
+
supportedThinkingLevels: Type5.Array(thinking),
|
|
21757
|
+
thinkingLevelAdjustments: Type5.Object({
|
|
21527
21758
|
off: thinking,
|
|
21528
21759
|
minimal: thinking,
|
|
21529
21760
|
low: thinking,
|
|
@@ -21533,168 +21764,714 @@ var modelInfo = Type6.Object({
|
|
|
21533
21764
|
max: thinking
|
|
21534
21765
|
})
|
|
21535
21766
|
});
|
|
21536
|
-
var NekoModels =
|
|
21767
|
+
var NekoModels = contract("neko.models", {
|
|
21537
21768
|
version: 1,
|
|
21538
|
-
input:
|
|
21539
|
-
variants: { ready: { methods: { list: rpc(nil,
|
|
21769
|
+
input: Type5.Object({}, { additionalProperties: false }),
|
|
21770
|
+
variants: { ready: { methods: { list: rpc(nil, Type5.Array(modelInfo)) } } }
|
|
21540
21771
|
});
|
|
21541
|
-
var imageChunk =
|
|
21542
|
-
var NekoSubagent =
|
|
21772
|
+
var imageChunk = Type5.Object({ data: str, total: integer, mimeType: str });
|
|
21773
|
+
var NekoSubagent = contract("neko.subagent", {
|
|
21543
21774
|
version: 1,
|
|
21544
|
-
input:
|
|
21775
|
+
input: Type5.Object({
|
|
21545
21776
|
sessionId: str,
|
|
21546
21777
|
attachmentId: str,
|
|
21547
|
-
taskId:
|
|
21548
|
-
agentId:
|
|
21778
|
+
taskId: Type5.Optional(integer),
|
|
21779
|
+
agentId: Type5.Optional(str)
|
|
21549
21780
|
}),
|
|
21550
21781
|
variants: {
|
|
21551
21782
|
ready: {
|
|
21552
21783
|
state: { revision: integer },
|
|
21553
21784
|
methods: {
|
|
21554
|
-
chunk: rpc(
|
|
21555
|
-
kind:
|
|
21556
|
-
before:
|
|
21557
|
-
token:
|
|
21785
|
+
chunk: rpc(Type5.Object({
|
|
21786
|
+
kind: Type5.Enum(["view", "history"]),
|
|
21787
|
+
before: Type5.Optional(integer),
|
|
21788
|
+
token: Type5.Optional(integer),
|
|
21558
21789
|
offset: integer
|
|
21559
|
-
}),
|
|
21560
|
-
getImage: rpc(
|
|
21561
|
-
decideApproval: rpc(
|
|
21562
|
-
stop: rpc(
|
|
21790
|
+
}), Type5.Object({ token: integer, data: str, next: Type5.Union([integer, nil]) })),
|
|
21791
|
+
getImage: rpc(Type5.Object({ entryId: integer, contentIndex: integer, offset: integer }), imageChunk),
|
|
21792
|
+
decideApproval: rpc(Type5.Object({ id: integer, version: str, decision: Type5.Enum(["allowed", "denied"]) }), Type5.Enum(["pending", "allowed", "denied", "cancelled"])),
|
|
21793
|
+
stop: rpc(Type5.Union([nil, Type5.Object({ taskId: integer })]), nil)
|
|
21563
21794
|
}
|
|
21564
21795
|
}
|
|
21565
21796
|
}
|
|
21566
21797
|
});
|
|
21567
|
-
var NekoChat =
|
|
21798
|
+
var NekoChat = contract("neko.chat", {
|
|
21568
21799
|
version: 1,
|
|
21569
21800
|
input: chatInput,
|
|
21570
21801
|
variants: {
|
|
21571
21802
|
ready: {
|
|
21572
21803
|
state: { view: ChatViewSchema },
|
|
21573
21804
|
methods: {
|
|
21574
|
-
submit: rpc(
|
|
21805
|
+
submit: rpc(Type5.Object({
|
|
21575
21806
|
content: str,
|
|
21576
|
-
images:
|
|
21577
|
-
requestId:
|
|
21578
|
-
whenBusy:
|
|
21807
|
+
images: Type5.Optional(Type5.Array(str, { maxItems: 4 })),
|
|
21808
|
+
requestId: Type5.Optional(str),
|
|
21809
|
+
whenBusy: Type5.Optional(Type5.Enum(["steer", "followUp"]))
|
|
21579
21810
|
}, { additionalProperties: false }), nil),
|
|
21580
21811
|
abort: rpc(nil, nil),
|
|
21812
|
+
withdraw: rpc(Type5.Object({ id: integer }, { additionalProperties: false }), WithdrawResultSchema),
|
|
21813
|
+
steer: rpc(Type5.Object({ id: integer }, { additionalProperties: false }), SteerResultSchema),
|
|
21581
21814
|
usage: rpc(nil, usage),
|
|
21582
|
-
skills: rpc(nil,
|
|
21583
|
-
jobOutput: rpc(
|
|
21584
|
-
jobStop: rpc(
|
|
21815
|
+
skills: rpc(nil, Type5.Array(Type5.Object({ name: str, description: str, location: str, level }))),
|
|
21816
|
+
jobOutput: rpc(Type5.Object({ jobId: str, since: Type5.Optional(integer) }, { additionalProperties: false }), JobOutputSchema),
|
|
21817
|
+
jobStop: rpc(Type5.Object({ jobId: str }, { additionalProperties: false }), JobSummarySchema),
|
|
21585
21818
|
configure: rpc(change, nil),
|
|
21586
|
-
compact: rpc(
|
|
21587
|
-
importImage: rpc(str,
|
|
21588
|
-
draftImage: rpc(
|
|
21589
|
-
beginImage: rpc(
|
|
21590
|
-
appendImage: rpc(
|
|
21819
|
+
compact: rpc(Type5.Object({ instructions: Type5.Optional(str) }, { additionalProperties: false }), nil),
|
|
21820
|
+
importImage: rpc(str, Type5.Object({ id: str, name: str })),
|
|
21821
|
+
draftImage: rpc(Type5.Object({ id: str, offset: integer }, { additionalProperties: false }), imageChunk),
|
|
21822
|
+
beginImage: rpc(Type5.Object({ name: str, mimeType: str, size: Type5.Integer({ minimum: 1, maximum: 20971520 }) }, { additionalProperties: false }), str),
|
|
21823
|
+
appendImage: rpc(Type5.Object({ id: str, offset: integer, data: Type5.String({ maxLength: 350000 }) }, { additionalProperties: false }), nil),
|
|
21591
21824
|
finishImage: rpc(str, nil),
|
|
21592
21825
|
releaseImage: rpc(str, nil),
|
|
21593
|
-
getImage: rpc(
|
|
21826
|
+
getImage: rpc(Type5.Object({ entryId: integer, contentIndex: integer, offset: integer }, { additionalProperties: false }), imageChunk)
|
|
21594
21827
|
}
|
|
21595
21828
|
}
|
|
21596
21829
|
}
|
|
21597
21830
|
});
|
|
21598
|
-
var browse =
|
|
21599
|
-
var page =
|
|
21831
|
+
var browse = Type5.Object({ path: str, query: Type5.Optional(str), showHidden: Type5.Optional(bool), cursor: Type5.Optional(str) }, { additionalProperties: false });
|
|
21832
|
+
var page = Type5.Object({
|
|
21600
21833
|
path: str,
|
|
21601
21834
|
parent: nullableString,
|
|
21602
|
-
entries:
|
|
21835
|
+
entries: Type5.Array(Type5.Object({ name: str, path: str, error: Type5.Optional(str) })),
|
|
21603
21836
|
next: nullableString
|
|
21604
21837
|
});
|
|
21605
|
-
var NekoWorkspaces =
|
|
21838
|
+
var NekoWorkspaces = contract("neko.workspaces", {
|
|
21606
21839
|
version: 1,
|
|
21607
|
-
input:
|
|
21840
|
+
input: Type5.Object({}, { additionalProperties: false }),
|
|
21608
21841
|
variants: {
|
|
21609
21842
|
ready: {
|
|
21610
21843
|
state: { state: WorkspaceStateSchema },
|
|
21611
21844
|
methods: {
|
|
21612
|
-
add: rpc(
|
|
21845
|
+
add: rpc(Type5.Object({ path: str }, { additionalProperties: false }), WorkspaceSummarySchema),
|
|
21613
21846
|
browse: rpc(browse, page)
|
|
21614
21847
|
}
|
|
21615
21848
|
}
|
|
21616
21849
|
}
|
|
21617
21850
|
});
|
|
21618
|
-
var provider =
|
|
21619
|
-
var NekoCredentials =
|
|
21851
|
+
var provider = Type5.Enum(["deepseek", "tavily"]);
|
|
21852
|
+
var NekoCredentials = contract("neko.credentials", {
|
|
21620
21853
|
version: 1,
|
|
21621
|
-
input:
|
|
21854
|
+
input: Type5.Object({}, { additionalProperties: false }),
|
|
21622
21855
|
variants: {
|
|
21623
21856
|
ready: {
|
|
21624
21857
|
state: { state: CredentialStateSchema },
|
|
21625
21858
|
methods: {
|
|
21626
|
-
save: rpc(
|
|
21627
|
-
remove: rpc(
|
|
21859
|
+
save: rpc(Type5.Object({ provider, key: Type5.String({ minLength: 1, maxLength: 8192 }) }, { additionalProperties: false }), nil),
|
|
21860
|
+
remove: rpc(Type5.Object({ provider }, { additionalProperties: false }), nil)
|
|
21861
|
+
}
|
|
21862
|
+
}
|
|
21863
|
+
}
|
|
21864
|
+
});
|
|
21865
|
+
var SearchSettingsStateSchema = Type5.Object({
|
|
21866
|
+
revision: integer,
|
|
21867
|
+
selected: Type5.Enum(["tavily", "deepseek"])
|
|
21868
|
+
});
|
|
21869
|
+
var NekoSearchSettings = contract("neko.search-settings", {
|
|
21870
|
+
version: 1,
|
|
21871
|
+
input: Type5.Object({}, { additionalProperties: false }),
|
|
21872
|
+
variants: {
|
|
21873
|
+
ready: {
|
|
21874
|
+
state: { state: SearchSettingsStateSchema },
|
|
21875
|
+
methods: {
|
|
21876
|
+
set: rpc(Type5.Object({ provider: Type5.Enum(["tavily", "deepseek"]) }, { additionalProperties: false }), nil)
|
|
21628
21877
|
}
|
|
21629
21878
|
}
|
|
21630
21879
|
}
|
|
21631
21880
|
});
|
|
21632
|
-
var
|
|
21633
|
-
|
|
21881
|
+
var contextSource = Type5.Enum(["default", "config", "saved"]);
|
|
21882
|
+
var ContextSettingsStateSchema = Type5.Object({
|
|
21883
|
+
revision: integer,
|
|
21884
|
+
effective: Type5.Object({
|
|
21885
|
+
compaction: Type5.Object({
|
|
21886
|
+
enabled: bool,
|
|
21887
|
+
triggerPercent: Type5.Union([integer, nil]),
|
|
21888
|
+
keepRecentTokens: integer
|
|
21889
|
+
}),
|
|
21890
|
+
pruning: Type5.Object({
|
|
21891
|
+
enabled: bool,
|
|
21892
|
+
triggerPercent: integer,
|
|
21893
|
+
keepRecent: integer,
|
|
21894
|
+
minChars: integer,
|
|
21895
|
+
minSavePercent: integer
|
|
21896
|
+
})
|
|
21897
|
+
}),
|
|
21898
|
+
sources: Type5.Object({
|
|
21899
|
+
compaction: Type5.Object({ enabled: contextSource, triggerPercent: contextSource, keepRecentTokens: contextSource }),
|
|
21900
|
+
pruning: Type5.Object({
|
|
21901
|
+
enabled: contextSource,
|
|
21902
|
+
triggerPercent: contextSource,
|
|
21903
|
+
keepRecent: contextSource,
|
|
21904
|
+
minChars: contextSource,
|
|
21905
|
+
minSavePercent: contextSource
|
|
21906
|
+
})
|
|
21907
|
+
})
|
|
21908
|
+
});
|
|
21909
|
+
var nullable = (schema) => Type5.Union([schema, nil]);
|
|
21910
|
+
var ContextSettingsPatchSchema = Type5.Object({
|
|
21911
|
+
compaction: Type5.Optional(Type5.Object({
|
|
21912
|
+
enabled: Type5.Optional(nullable(bool)),
|
|
21913
|
+
triggerPercent: Type5.Optional(nullable(integer)),
|
|
21914
|
+
keepRecentTokens: Type5.Optional(nullable(integer))
|
|
21915
|
+
}, { additionalProperties: false })),
|
|
21916
|
+
pruning: Type5.Optional(Type5.Object({
|
|
21917
|
+
enabled: Type5.Optional(nullable(bool)),
|
|
21918
|
+
triggerPercent: Type5.Optional(nullable(integer)),
|
|
21919
|
+
keepRecent: Type5.Optional(nullable(integer)),
|
|
21920
|
+
minChars: Type5.Optional(nullable(integer)),
|
|
21921
|
+
minSavePercent: Type5.Optional(nullable(integer))
|
|
21922
|
+
}, { additionalProperties: false }))
|
|
21923
|
+
}, { additionalProperties: false });
|
|
21924
|
+
var NekoContextSettings = contract("neko.context-settings", {
|
|
21925
|
+
version: 1,
|
|
21926
|
+
input: Type5.Object({}, { additionalProperties: false }),
|
|
21927
|
+
variants: {
|
|
21928
|
+
ready: {
|
|
21929
|
+
state: { state: ContextSettingsStateSchema },
|
|
21930
|
+
methods: {
|
|
21931
|
+
set: rpc(ContextSettingsPatchSchema, nil),
|
|
21932
|
+
reset: rpc(Type5.Object({}, { additionalProperties: false }), nil)
|
|
21933
|
+
}
|
|
21934
|
+
}
|
|
21935
|
+
}
|
|
21936
|
+
});
|
|
21937
|
+
var FileCandidatesSchema = Type5.Object({
|
|
21938
|
+
entries: Type5.Array(Type5.Object({ name: str, path: str, kind: Type5.Enum(["file", "directory"]) }), { maxItems: 100 }),
|
|
21634
21939
|
limited: bool,
|
|
21635
21940
|
incomplete: bool
|
|
21636
21941
|
});
|
|
21637
|
-
var NekoWorkspaceFiles =
|
|
21942
|
+
var NekoWorkspaceFiles = contract("neko.workspace-files", {
|
|
21943
|
+
version: 1,
|
|
21944
|
+
input: Type5.Object({}, { additionalProperties: false }),
|
|
21945
|
+
variants: {
|
|
21946
|
+
ready: {
|
|
21947
|
+
methods: {
|
|
21948
|
+
search: rpc(Type5.Object({ workspaceRoot: str, query: str }, { additionalProperties: false }), FileCandidatesSchema)
|
|
21949
|
+
}
|
|
21950
|
+
}
|
|
21951
|
+
}
|
|
21952
|
+
});
|
|
21953
|
+
var marketSkill = Type5.Object({
|
|
21954
|
+
name: str,
|
|
21955
|
+
description: str,
|
|
21956
|
+
category: str,
|
|
21957
|
+
source: str,
|
|
21958
|
+
license: str,
|
|
21959
|
+
version: str,
|
|
21960
|
+
sha256: str,
|
|
21961
|
+
sizeBytes: integer,
|
|
21962
|
+
uploadedAt: str,
|
|
21963
|
+
installed: Type5.Object({ project: bool, user: bool, builtin: bool }),
|
|
21964
|
+
activeLevel: Type5.Union([level, nil])
|
|
21965
|
+
});
|
|
21966
|
+
var preview = Type5.Object({ body: str, files: Type5.Array(str) });
|
|
21967
|
+
var scope = Type5.Enum(["user", "project"]);
|
|
21968
|
+
var installResult = Type5.Object({ name: str, scope, path: str, shadowedBy: Type5.Optional(scope) });
|
|
21969
|
+
var NekoSkillMarket = contract("neko.skill-market", {
|
|
21638
21970
|
version: 1,
|
|
21639
|
-
input:
|
|
21971
|
+
input: Type5.Object({}, { additionalProperties: false }),
|
|
21640
21972
|
variants: {
|
|
21641
21973
|
ready: {
|
|
21642
21974
|
methods: {
|
|
21643
|
-
|
|
21975
|
+
list: rpc(Type5.Object({ query: Type5.Optional(str), workspaceRoot: Type5.Optional(str) }, { additionalProperties: false }), Type5.Array(marketSkill)),
|
|
21976
|
+
detail: rpc(Type5.Object({ name: str, workspaceRoot: Type5.Optional(str) }, { additionalProperties: false }), marketSkill),
|
|
21977
|
+
preview: rpc(Type5.Object({ name: str }, { additionalProperties: false }), preview),
|
|
21978
|
+
install: rpc(Type5.Object({ name: str, scope, workspaceRoot: Type5.Optional(str), overwrite: Type5.Optional(bool) }, { additionalProperties: false }), installResult),
|
|
21979
|
+
uninstall: rpc(Type5.Object({ name: str, scope, workspaceRoot: Type5.Optional(str) }, { additionalProperties: false }), nil)
|
|
21644
21980
|
}
|
|
21645
21981
|
}
|
|
21646
21982
|
}
|
|
21647
21983
|
});
|
|
21648
|
-
var
|
|
21649
|
-
|
|
21650
|
-
|
|
21651
|
-
|
|
21652
|
-
|
|
21653
|
-
|
|
21654
|
-
|
|
21655
|
-
|
|
21656
|
-
|
|
21657
|
-
|
|
21658
|
-
|
|
21659
|
-
|
|
21660
|
-
|
|
21661
|
-
|
|
21662
|
-
|
|
21663
|
-
|
|
21664
|
-
|
|
21984
|
+
var ApplicationInfoSchema = Type5.Object({
|
|
21985
|
+
runningVersion: str,
|
|
21986
|
+
installedVersion: nullableString,
|
|
21987
|
+
protocolVersion: Type5.Integer({ minimum: 1 }),
|
|
21988
|
+
latestVersion: nullableString,
|
|
21989
|
+
checkedAt: Type5.Union([integer, nil]),
|
|
21990
|
+
checkState: Type5.Enum(["unknown", "checking", "ok", "failed", "development"]),
|
|
21991
|
+
serverId: str
|
|
21992
|
+
});
|
|
21993
|
+
var NekoApplicationInfo = contract("neko.application-info", {
|
|
21994
|
+
version: 1,
|
|
21995
|
+
input: Type5.Object({}),
|
|
21996
|
+
variants: {
|
|
21997
|
+
ready: { state: { info: ApplicationInfoSchema }, methods: { check: rpc(nil, ApplicationInfoSchema) } }
|
|
21998
|
+
}
|
|
21999
|
+
});
|
|
22000
|
+
|
|
22001
|
+
// packages/gateway/src/identity.ts
|
|
22002
|
+
function isServerId(value) {
|
|
22003
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
22004
|
+
}
|
|
22005
|
+
|
|
22006
|
+
// packages/gateway/src/mobile/admin.ts
|
|
22007
|
+
import { createHash, timingSafeEqual } from "crypto";
|
|
22008
|
+
import { ChordError } from "@earendil-works/chord";
|
|
22009
|
+
import { Type as Type6 } from "typebox";
|
|
22010
|
+
|
|
22011
|
+
// packages/gateway/src/principal.ts
|
|
22012
|
+
var HOST_PRINCIPAL = { kind: "host" };
|
|
22013
|
+
function gatewayPrincipalKey(principal) {
|
|
22014
|
+
switch (principal.kind) {
|
|
22015
|
+
case "mobile":
|
|
22016
|
+
return `mobile:${principal.deviceId}:${principal.grantRevision}:${principal.connectionId}`;
|
|
22017
|
+
case "local":
|
|
22018
|
+
return `local:${principal.connectionId}`;
|
|
22019
|
+
case "host":
|
|
22020
|
+
return "host";
|
|
22021
|
+
}
|
|
22022
|
+
}
|
|
22023
|
+
function isMobilePrincipal(principal) {
|
|
22024
|
+
return typeof principal === "object" && principal !== null && principal.kind === "mobile";
|
|
22025
|
+
}
|
|
22026
|
+
|
|
22027
|
+
// packages/gateway/src/mobile/store.ts
|
|
22028
|
+
import { randomBytes } from "crypto";
|
|
22029
|
+
import { chmod as chmod2, mkdir as mkdir2, readFile as readFile4, rename as rename2, rm, writeFile } from "fs/promises";
|
|
22030
|
+
import { join as join6 } from "path";
|
|
22031
|
+
var MOBILE_STATE_FORMAT_VERSION = 1;
|
|
22032
|
+
var MOBILE_DEFAULT_PORT = 8323;
|
|
22033
|
+
var MOBILE_ADMIN_TOKEN_BYTES = 32;
|
|
22034
|
+
var POSIX = process.platform !== "win32";
|
|
22035
|
+
var ADMIN_TOKEN_PATTERN = /^[A-Za-z0-9_-]{43}$/;
|
|
22036
|
+
var CREDENTIAL_HASH_PATTERN = /^[0-9a-fA-F]{64}$/;
|
|
22037
|
+
function defaultState() {
|
|
22038
|
+
return {
|
|
22039
|
+
formatVersion: MOBILE_STATE_FORMAT_VERSION,
|
|
22040
|
+
enabled: false,
|
|
22041
|
+
address: null,
|
|
22042
|
+
port: MOBILE_DEFAULT_PORT,
|
|
22043
|
+
devices: [],
|
|
22044
|
+
receipts: [],
|
|
22045
|
+
lastConnectedAddress: null
|
|
22046
|
+
};
|
|
22047
|
+
}
|
|
22048
|
+
function freezeState(state) {
|
|
22049
|
+
for (const device of state.devices)
|
|
22050
|
+
Object.freeze(device);
|
|
22051
|
+
Object.freeze(state.devices);
|
|
22052
|
+
for (const receipt of state.receipts)
|
|
22053
|
+
Object.freeze(receipt);
|
|
22054
|
+
Object.freeze(state.receipts);
|
|
22055
|
+
return Object.freeze(state);
|
|
22056
|
+
}
|
|
22057
|
+
function isNotFound(error) {
|
|
22058
|
+
return error?.code === "ENOENT";
|
|
22059
|
+
}
|
|
22060
|
+
function invalidState(statePath, detail) {
|
|
22061
|
+
return new Error(`Invalid mobile state file ${statePath}: ${detail}`);
|
|
22062
|
+
}
|
|
22063
|
+
function readString(value, what, statePath) {
|
|
22064
|
+
if (typeof value !== "string")
|
|
22065
|
+
throw invalidState(statePath, `${what} must be a string`);
|
|
22066
|
+
return value;
|
|
22067
|
+
}
|
|
22068
|
+
function readNonEmptyString(value, what, statePath) {
|
|
22069
|
+
const text = readString(value, what, statePath);
|
|
22070
|
+
if (text === "")
|
|
22071
|
+
throw invalidState(statePath, `${what} must be a non-empty string`);
|
|
22072
|
+
return text;
|
|
22073
|
+
}
|
|
22074
|
+
function readCredentialHash(value, what, statePath) {
|
|
22075
|
+
const text = readString(value, what, statePath);
|
|
22076
|
+
if (!CREDENTIAL_HASH_PATTERN.test(text))
|
|
22077
|
+
throw invalidState(statePath, `${what} must be a 64-character hex string`);
|
|
22078
|
+
return text;
|
|
22079
|
+
}
|
|
22080
|
+
function readRevision(value, what, statePath) {
|
|
22081
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0)
|
|
22082
|
+
throw invalidState(statePath, `${what} must be a non-negative integer`);
|
|
22083
|
+
return value;
|
|
22084
|
+
}
|
|
22085
|
+
function readTimestamp(value, what, statePath) {
|
|
22086
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
22087
|
+
throw invalidState(statePath, `${what} must be a finite number`);
|
|
22088
|
+
return value;
|
|
22089
|
+
}
|
|
22090
|
+
function parseDevice(raw, index, statePath) {
|
|
22091
|
+
const what = `devices[${index}]`;
|
|
22092
|
+
if (typeof raw !== "object" || raw === null)
|
|
22093
|
+
throw invalidState(statePath, `${what} must be an object`);
|
|
22094
|
+
const record = raw;
|
|
22095
|
+
return {
|
|
22096
|
+
deviceId: readNonEmptyString(record.deviceId, `${what}.deviceId`, statePath),
|
|
22097
|
+
name: readString(record.name, `${what}.name`, statePath),
|
|
22098
|
+
credentialHash: readCredentialHash(record.credentialHash, `${what}.credentialHash`, statePath),
|
|
22099
|
+
grantRevision: readRevision(record.grantRevision, `${what}.grantRevision`, statePath),
|
|
22100
|
+
pairedAt: readTimestamp(record.pairedAt, `${what}.pairedAt`, statePath),
|
|
22101
|
+
lastSeenAt: readTimestamp(record.lastSeenAt, `${what}.lastSeenAt`, statePath)
|
|
22102
|
+
};
|
|
22103
|
+
}
|
|
22104
|
+
function parseReceipt(raw, index, statePath) {
|
|
22105
|
+
const what = `receipts[${index}]`;
|
|
22106
|
+
if (typeof raw !== "object" || raw === null)
|
|
22107
|
+
throw invalidState(statePath, `${what} must be an object`);
|
|
22108
|
+
const record = raw;
|
|
22109
|
+
return {
|
|
22110
|
+
pairRequestId: readNonEmptyString(record.pairRequestId, `${what}.pairRequestId`, statePath),
|
|
22111
|
+
deviceId: readNonEmptyString(record.deviceId, `${what}.deviceId`, statePath),
|
|
22112
|
+
credentialHash: readCredentialHash(record.credentialHash, `${what}.credentialHash`, statePath),
|
|
22113
|
+
grantRevision: readRevision(record.grantRevision, `${what}.grantRevision`, statePath),
|
|
22114
|
+
expiresAt: readTimestamp(record.expiresAt, `${what}.expiresAt`, statePath)
|
|
22115
|
+
};
|
|
22116
|
+
}
|
|
22117
|
+
function parseState(raw, statePath) {
|
|
22118
|
+
if (typeof raw !== "object" || raw === null)
|
|
22119
|
+
throw invalidState(statePath, "root must be an object");
|
|
22120
|
+
const record = raw;
|
|
22121
|
+
if (record.formatVersion !== MOBILE_STATE_FORMAT_VERSION)
|
|
22122
|
+
throw invalidState(statePath, `unsupported formatVersion (expected ${MOBILE_STATE_FORMAT_VERSION})`);
|
|
22123
|
+
if (typeof record.enabled !== "boolean")
|
|
22124
|
+
throw invalidState(statePath, "enabled must be a boolean");
|
|
22125
|
+
if (record.address !== null && typeof record.address !== "string")
|
|
22126
|
+
throw invalidState(statePath, "address must be a string or null");
|
|
22127
|
+
const port = record.port;
|
|
22128
|
+
if (typeof port !== "number" || !Number.isInteger(port) || port < 1 || port > 65535)
|
|
22129
|
+
throw invalidState(statePath, "port must be an integer between 1 and 65535");
|
|
22130
|
+
if (!Array.isArray(record.devices))
|
|
22131
|
+
throw invalidState(statePath, "devices must be an array");
|
|
22132
|
+
if (!Array.isArray(record.receipts))
|
|
22133
|
+
throw invalidState(statePath, "receipts must be an array");
|
|
22134
|
+
const lastConnectedAddress = record.lastConnectedAddress === undefined || record.lastConnectedAddress === null ? null : readString(record.lastConnectedAddress, "lastConnectedAddress", statePath);
|
|
22135
|
+
return {
|
|
22136
|
+
formatVersion: MOBILE_STATE_FORMAT_VERSION,
|
|
22137
|
+
enabled: record.enabled,
|
|
22138
|
+
address: record.address,
|
|
22139
|
+
port,
|
|
22140
|
+
devices: record.devices.map((device, index) => parseDevice(device, index, statePath)),
|
|
22141
|
+
receipts: record.receipts.map((receipt, index) => parseReceipt(receipt, index, statePath)),
|
|
22142
|
+
lastConnectedAddress
|
|
22143
|
+
};
|
|
22144
|
+
}
|
|
22145
|
+
function assertPort(port) {
|
|
22146
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
22147
|
+
throw new Error("port must be an integer between 1 and 65535");
|
|
22148
|
+
return port;
|
|
22149
|
+
}
|
|
22150
|
+
var tempCounter = 0;
|
|
22151
|
+
async function writeFileAtomic(filePath, contents, mode) {
|
|
22152
|
+
const tempPath = `${filePath}.tmp-${process.pid}-${++tempCounter}`;
|
|
22153
|
+
try {
|
|
22154
|
+
await writeFile(tempPath, contents, { mode });
|
|
22155
|
+
if (POSIX)
|
|
22156
|
+
await chmod2(tempPath, mode);
|
|
22157
|
+
await rename2(tempPath, filePath);
|
|
22158
|
+
} finally {
|
|
22159
|
+
await rm(tempPath, { force: true });
|
|
22160
|
+
}
|
|
22161
|
+
}
|
|
22162
|
+
async function writeSecretFile(filePath, contents) {
|
|
22163
|
+
await writeFileAtomic(filePath, contents, 384);
|
|
22164
|
+
}
|
|
22165
|
+
async function readSecretFile(filePath) {
|
|
22166
|
+
try {
|
|
22167
|
+
return await readFile4(filePath, "utf8");
|
|
22168
|
+
} catch (error) {
|
|
22169
|
+
if (isNotFound(error))
|
|
22170
|
+
return;
|
|
22171
|
+
throw error;
|
|
22172
|
+
}
|
|
22173
|
+
}
|
|
22174
|
+
async function loadState(statePath) {
|
|
22175
|
+
let raw;
|
|
22176
|
+
try {
|
|
22177
|
+
raw = await readFile4(statePath, "utf8");
|
|
22178
|
+
} catch (error) {
|
|
22179
|
+
if (isNotFound(error))
|
|
22180
|
+
return freezeState(defaultState());
|
|
22181
|
+
throw error;
|
|
22182
|
+
}
|
|
22183
|
+
let parsed;
|
|
22184
|
+
try {
|
|
22185
|
+
parsed = JSON.parse(raw);
|
|
22186
|
+
} catch {
|
|
22187
|
+
throw invalidState(statePath, "not valid JSON");
|
|
22188
|
+
}
|
|
22189
|
+
return freezeState(parseState(parsed, statePath));
|
|
22190
|
+
}
|
|
22191
|
+
|
|
22192
|
+
class MobileStore {
|
|
22193
|
+
mobileDir;
|
|
22194
|
+
tlsDir;
|
|
22195
|
+
statePath;
|
|
22196
|
+
adminTokenPath;
|
|
22197
|
+
current;
|
|
22198
|
+
queue = Promise.resolve();
|
|
22199
|
+
constructor(mobileDir, initial) {
|
|
22200
|
+
this.mobileDir = mobileDir;
|
|
22201
|
+
this.tlsDir = join6(mobileDir, "tls");
|
|
22202
|
+
this.statePath = join6(mobileDir, "state.json");
|
|
22203
|
+
this.adminTokenPath = join6(mobileDir, "admin-token");
|
|
22204
|
+
this.current = initial;
|
|
22205
|
+
}
|
|
22206
|
+
static async open(nekoDir) {
|
|
22207
|
+
const mobileDir = join6(nekoDir, "mobile");
|
|
22208
|
+
const tlsDir = join6(mobileDir, "tls");
|
|
22209
|
+
await mkdir2(mobileDir, { recursive: true, mode: 448 });
|
|
22210
|
+
await mkdir2(tlsDir, { recursive: true, mode: 448 });
|
|
22211
|
+
if (POSIX) {
|
|
22212
|
+
await chmod2(mobileDir, 448);
|
|
22213
|
+
await chmod2(tlsDir, 448);
|
|
22214
|
+
}
|
|
22215
|
+
return new MobileStore(mobileDir, await loadState(join6(mobileDir, "state.json")));
|
|
22216
|
+
}
|
|
22217
|
+
getState() {
|
|
22218
|
+
return this.current;
|
|
22219
|
+
}
|
|
22220
|
+
updateConfig(patch) {
|
|
22221
|
+
return this.mutate((state) => ({
|
|
22222
|
+
...state,
|
|
22223
|
+
...patch.enabled !== undefined ? { enabled: patch.enabled } : {},
|
|
22224
|
+
...patch.address !== undefined ? { address: patch.address } : {},
|
|
22225
|
+
...patch.port !== undefined ? { port: assertPort(patch.port) } : {}
|
|
22226
|
+
}));
|
|
22227
|
+
}
|
|
22228
|
+
upsertDevice(device) {
|
|
22229
|
+
return this.mutate((state) => ({
|
|
22230
|
+
...state,
|
|
22231
|
+
devices: state.devices.some((d) => d.deviceId === device.deviceId) ? state.devices.map((d) => d.deviceId === device.deviceId ? device : d) : [...state.devices, device]
|
|
22232
|
+
}));
|
|
22233
|
+
}
|
|
22234
|
+
removeDevice(deviceId) {
|
|
22235
|
+
return this.mutate((state) => ({
|
|
22236
|
+
...state,
|
|
22237
|
+
devices: state.devices.filter((d) => d.deviceId !== deviceId)
|
|
22238
|
+
}));
|
|
22239
|
+
}
|
|
22240
|
+
completePairing(input) {
|
|
22241
|
+
return this.mutate((state) => {
|
|
22242
|
+
const now = Date.now();
|
|
22243
|
+
const existing = state.devices.find((d) => d.deviceId === input.deviceId);
|
|
22244
|
+
const grantRevision = existing === undefined ? 1 : existing.grantRevision + 1;
|
|
22245
|
+
const device = {
|
|
22246
|
+
deviceId: input.deviceId,
|
|
22247
|
+
name: input.name,
|
|
22248
|
+
credentialHash: input.credentialHash,
|
|
22249
|
+
grantRevision,
|
|
22250
|
+
pairedAt: input.pairedAt,
|
|
22251
|
+
lastSeenAt: input.pairedAt
|
|
22252
|
+
};
|
|
22253
|
+
const receipt = {
|
|
22254
|
+
pairRequestId: input.pairRequestId,
|
|
22255
|
+
deviceId: input.deviceId,
|
|
22256
|
+
credentialHash: input.credentialHash,
|
|
22257
|
+
grantRevision,
|
|
22258
|
+
expiresAt: input.receiptExpiresAt
|
|
22259
|
+
};
|
|
22260
|
+
return {
|
|
22261
|
+
...state,
|
|
22262
|
+
devices: existing === undefined ? [...state.devices, device] : state.devices.map((d) => d.deviceId === input.deviceId ? device : d),
|
|
22263
|
+
receipts: [
|
|
22264
|
+
...state.receipts.filter((r) => r.expiresAt > now && r.pairRequestId !== input.pairRequestId),
|
|
22265
|
+
receipt
|
|
22266
|
+
]
|
|
22267
|
+
};
|
|
22268
|
+
});
|
|
22269
|
+
}
|
|
22270
|
+
putReceipt(receipt) {
|
|
22271
|
+
return this.mutate((state) => ({
|
|
22272
|
+
...state,
|
|
22273
|
+
receipts: state.receipts.some((r) => r.pairRequestId === receipt.pairRequestId) ? state.receipts.map((r) => r.pairRequestId === receipt.pairRequestId ? receipt : r) : [...state.receipts, receipt]
|
|
22274
|
+
}));
|
|
22275
|
+
}
|
|
22276
|
+
removeReceipt(pairRequestId) {
|
|
22277
|
+
return this.mutate((state) => ({
|
|
22278
|
+
...state,
|
|
22279
|
+
receipts: state.receipts.filter((r) => r.pairRequestId !== pairRequestId)
|
|
22280
|
+
}));
|
|
22281
|
+
}
|
|
22282
|
+
touchDevice(deviceId, lastSeenAt) {
|
|
22283
|
+
return this.mutate((state) => {
|
|
22284
|
+
const existing = state.devices.find((d) => d.deviceId === deviceId);
|
|
22285
|
+
if (existing === undefined)
|
|
22286
|
+
return state;
|
|
22287
|
+
return {
|
|
22288
|
+
...state,
|
|
22289
|
+
devices: state.devices.map((d) => d.deviceId === deviceId ? { ...d, lastSeenAt } : d)
|
|
22290
|
+
};
|
|
22291
|
+
});
|
|
22292
|
+
}
|
|
22293
|
+
rememberConnectedAddress(address) {
|
|
22294
|
+
if (this.current.lastConnectedAddress === address)
|
|
22295
|
+
return Promise.resolve(this.current);
|
|
22296
|
+
return this.mutate((state) => ({ ...state, lastConnectedAddress: address }));
|
|
22297
|
+
}
|
|
22298
|
+
clearMobileAuthorizations() {
|
|
22299
|
+
return this.mutate((state) => ({ ...state, devices: [], receipts: [] }));
|
|
22300
|
+
}
|
|
22301
|
+
ensureAdminToken() {
|
|
22302
|
+
return this.enqueue(async () => {
|
|
22303
|
+
const existing = await readSecretFile(this.adminTokenPath);
|
|
22304
|
+
if (existing !== undefined) {
|
|
22305
|
+
const token = existing.trim();
|
|
22306
|
+
if (ADMIN_TOKEN_PATTERN.test(token))
|
|
22307
|
+
return token;
|
|
22308
|
+
throw new Error(`Invalid mobile admin token file ${this.adminTokenPath}: expected a base64url token`);
|
|
22309
|
+
}
|
|
22310
|
+
const token = randomBytes(MOBILE_ADMIN_TOKEN_BYTES).toString("base64url");
|
|
22311
|
+
await writeSecretFile(this.adminTokenPath, `${token}
|
|
22312
|
+
`);
|
|
22313
|
+
return token;
|
|
22314
|
+
});
|
|
22315
|
+
}
|
|
22316
|
+
mutate(recipe) {
|
|
22317
|
+
return this.enqueue(async () => {
|
|
22318
|
+
const next = freezeState(recipe(this.current));
|
|
22319
|
+
await writeFileAtomic(this.statePath, `${JSON.stringify(next, null, "\t")}
|
|
22320
|
+
`, 384);
|
|
22321
|
+
this.current = next;
|
|
22322
|
+
return next;
|
|
22323
|
+
});
|
|
22324
|
+
}
|
|
22325
|
+
enqueue(task) {
|
|
22326
|
+
const run = this.queue.then(task);
|
|
22327
|
+
this.queue = run.then(() => {
|
|
22328
|
+
return;
|
|
22329
|
+
}, () => {
|
|
22330
|
+
return;
|
|
22331
|
+
});
|
|
22332
|
+
return run;
|
|
22333
|
+
}
|
|
22334
|
+
}
|
|
22335
|
+
|
|
22336
|
+
// packages/gateway/src/mobile/admin.ts
|
|
22337
|
+
var listenerStateSchema = Type6.Union([
|
|
22338
|
+
Type6.Object({ status: Type6.Literal("disabled") }),
|
|
22339
|
+
Type6.Object({ status: Type6.Literal("starting") }),
|
|
22340
|
+
Type6.Object({
|
|
22341
|
+
status: Type6.Literal("listening"),
|
|
22342
|
+
address: Type6.String(),
|
|
22343
|
+
port: Type6.Integer({ minimum: 1, maximum: 65535 }),
|
|
22344
|
+
bindAddress: Type6.Optional(Type6.String()),
|
|
22345
|
+
mode: Type6.Optional(Type6.Union([Type6.Literal("direct"), Type6.Literal("wsl-relay")]))
|
|
22346
|
+
}),
|
|
22347
|
+
Type6.Object({ status: Type6.Literal("error"), error: Type6.String() }),
|
|
22348
|
+
Type6.Object({ status: Type6.Literal("stopping") })
|
|
22349
|
+
]);
|
|
22350
|
+
var adminDeviceSchema = Type6.Object({
|
|
22351
|
+
deviceId: Type6.String(),
|
|
22352
|
+
name: Type6.String(),
|
|
22353
|
+
pairedAt: Type6.Integer(),
|
|
22354
|
+
lastSeenAt: Type6.Integer(),
|
|
22355
|
+
online: Type6.Boolean()
|
|
22356
|
+
});
|
|
22357
|
+
var NekoMobileAdmin = contract("neko.mobile-admin", {
|
|
21665
22358
|
version: 1,
|
|
21666
|
-
input: Type6.Object({
|
|
22359
|
+
input: Type6.Object({ token: Type6.String() }),
|
|
21667
22360
|
variants: {
|
|
21668
|
-
|
|
22361
|
+
admin: {
|
|
21669
22362
|
methods: {
|
|
21670
|
-
|
|
21671
|
-
|
|
21672
|
-
|
|
21673
|
-
|
|
21674
|
-
|
|
22363
|
+
state: rpc(Type6.Null(), Type6.Object({
|
|
22364
|
+
state: listenerStateSchema,
|
|
22365
|
+
candidates: Type6.Array(Type6.String()),
|
|
22366
|
+
port: Type6.Integer({ minimum: 1, maximum: 65535 }),
|
|
22367
|
+
certificate: Type6.Union([
|
|
22368
|
+
Type6.Object({ fingerprint: Type6.String(), expiresAt: Type6.Integer() }),
|
|
22369
|
+
Type6.Null()
|
|
22370
|
+
]),
|
|
22371
|
+
devices: Type6.Array(adminDeviceSchema),
|
|
22372
|
+
lastConnectedAddress: Type6.Union([Type6.String(), Type6.Null()]),
|
|
22373
|
+
wsl: Type6.Optional(Type6.Union([
|
|
22374
|
+
Type6.Object({ interop: Type6.Boolean(), error: Type6.Union([Type6.String(), Type6.Null()]) }),
|
|
22375
|
+
Type6.Null()
|
|
22376
|
+
]))
|
|
22377
|
+
})),
|
|
22378
|
+
enable: rpc(Type6.Object({
|
|
22379
|
+
address: Type6.String(),
|
|
22380
|
+
port: Type6.Optional(Type6.Integer({ minimum: 1, maximum: 65535 }))
|
|
22381
|
+
}), Type6.Object({
|
|
22382
|
+
status: Type6.Literal("listening"),
|
|
22383
|
+
address: Type6.String(),
|
|
22384
|
+
port: Type6.Integer({ minimum: 1, maximum: 65535 }),
|
|
22385
|
+
bindAddress: Type6.Optional(Type6.String()),
|
|
22386
|
+
mode: Type6.Optional(Type6.Union([Type6.Literal("direct"), Type6.Literal("wsl-relay")]))
|
|
22387
|
+
})),
|
|
22388
|
+
disable: rpc(Type6.Null(), listenerStateSchema),
|
|
22389
|
+
createPairing: rpc(Type6.Null(), Type6.Object({ pairingText: Type6.String(), expiresAt: Type6.Integer() })),
|
|
22390
|
+
cancelPairing: rpc(Type6.Null(), Type6.Null()),
|
|
22391
|
+
revoke: rpc(Type6.Object({ deviceId: Type6.String({ minLength: 1 }) }), Type6.Object({ revoked: Type6.Boolean() })),
|
|
22392
|
+
regenerateCertificate: rpc(Type6.Null(), Type6.Object({ fingerprint: Type6.String(), expiresAt: Type6.Integer() }))
|
|
21675
22393
|
}
|
|
21676
22394
|
}
|
|
21677
22395
|
}
|
|
21678
22396
|
});
|
|
21679
|
-
|
|
21680
|
-
|
|
21681
|
-
|
|
21682
|
-
|
|
21683
|
-
|
|
21684
|
-
|
|
21685
|
-
|
|
21686
|
-
|
|
21687
|
-
|
|
21688
|
-
|
|
21689
|
-
|
|
21690
|
-
|
|
21691
|
-
|
|
22397
|
+
function digest(value) {
|
|
22398
|
+
return createHash("sha256").update(value, "utf8").digest();
|
|
22399
|
+
}
|
|
22400
|
+
async function tokenMatches(token, store) {
|
|
22401
|
+
if (store === undefined)
|
|
22402
|
+
return false;
|
|
22403
|
+
let adminToken;
|
|
22404
|
+
try {
|
|
22405
|
+
adminToken = await store.ensureAdminToken();
|
|
22406
|
+
} catch {
|
|
22407
|
+
return false;
|
|
22408
|
+
}
|
|
22409
|
+
const expected = digest(adminToken);
|
|
22410
|
+
const provided = digest(token);
|
|
22411
|
+
return expected.length === provided.length && timingSafeEqual(expected, provided);
|
|
22412
|
+
}
|
|
22413
|
+
function createMobileAdminFactory(deps) {
|
|
22414
|
+
return (input, principal) => {
|
|
22415
|
+
if (isMobilePrincipal(principal))
|
|
22416
|
+
throw new ChordError("forbidden", "mobile principals cannot administer pairing");
|
|
22417
|
+
const guard = async () => {
|
|
22418
|
+
if (!await tokenMatches(input.token, deps.store))
|
|
22419
|
+
throw new ChordError("forbidden", "forbidden");
|
|
22420
|
+
};
|
|
22421
|
+
return {
|
|
22422
|
+
variant: "admin",
|
|
22423
|
+
async state() {
|
|
22424
|
+
await guard();
|
|
22425
|
+
return deps.mobile.adminState();
|
|
22426
|
+
},
|
|
22427
|
+
async enable(params) {
|
|
22428
|
+
await guard();
|
|
22429
|
+
const state = await deps.mobile.enable({ address: params.address, port: params.port });
|
|
22430
|
+
if (state.status !== "listening")
|
|
22431
|
+
throw new ChordError("unavailable", `mobile listener failed: ${state.status}`);
|
|
22432
|
+
return state;
|
|
22433
|
+
},
|
|
22434
|
+
async disable() {
|
|
22435
|
+
await guard();
|
|
22436
|
+
return deps.mobile.disable();
|
|
22437
|
+
},
|
|
22438
|
+
async createPairing() {
|
|
22439
|
+
await guard();
|
|
22440
|
+
return deps.mobile.createPairing();
|
|
22441
|
+
},
|
|
22442
|
+
async cancelPairing() {
|
|
22443
|
+
await guard();
|
|
22444
|
+
deps.mobile.cancelPairing();
|
|
22445
|
+
return null;
|
|
22446
|
+
},
|
|
22447
|
+
async revoke(params) {
|
|
22448
|
+
await guard();
|
|
22449
|
+
return { revoked: await deps.mobile.revoke(params.deviceId) };
|
|
22450
|
+
},
|
|
22451
|
+
async regenerateCertificate() {
|
|
22452
|
+
await guard();
|
|
22453
|
+
return deps.mobile.regenerateCertificate();
|
|
22454
|
+
}
|
|
22455
|
+
};
|
|
22456
|
+
};
|
|
22457
|
+
}
|
|
22458
|
+
|
|
22459
|
+
// packages/gateway/src/mobile/listener.ts
|
|
22460
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
22461
|
+
import { createServer } from "https";
|
|
22462
|
+
import { hostname } from "os";
|
|
22463
|
+
import { memoryLinkPair } from "@earendil-works/chord";
|
|
22464
|
+
import { WebSocketServer } from "ws";
|
|
21692
22465
|
|
|
21693
22466
|
// packages/gateway/src/link.ts
|
|
21694
|
-
import {
|
|
22467
|
+
import {
|
|
22468
|
+
createHost,
|
|
22469
|
+
webSocketLink
|
|
22470
|
+
} from "@earendil-works/chord";
|
|
21695
22471
|
var MAX_LINK_BYTES = 16 * 1024 * 1024;
|
|
21696
|
-
var
|
|
21697
|
-
|
|
22472
|
+
var asBytes = (data) => data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
22473
|
+
var size = (data) => typeof data === "string" ? new TextEncoder().encode(data).byteLength : data.byteLength;
|
|
22474
|
+
function boundedWebSocketLink(socket, max = MAX_LINK_BYTES, options) {
|
|
21698
22475
|
let queued = 0, incoming = 0, receiving = false;
|
|
21699
22476
|
let native;
|
|
21700
22477
|
const bridge = {
|
|
@@ -21710,18 +22487,22 @@ function boundedWebSocketLink(socket, max = MAX_LINK_BYTES) {
|
|
|
21710
22487
|
},
|
|
21711
22488
|
close: (code, reason) => socket.close(code, reason),
|
|
21712
22489
|
addEventListener(type, handler) {
|
|
21713
|
-
if (type === "message")
|
|
21714
|
-
|
|
21715
|
-
const bytes = size(
|
|
22490
|
+
if (type === "message") {
|
|
22491
|
+
const deliver = (data) => {
|
|
22492
|
+
const bytes = size(data);
|
|
21716
22493
|
if (!receiving)
|
|
21717
22494
|
incoming += bytes;
|
|
21718
22495
|
if (bytes > max || incoming > max) {
|
|
21719
22496
|
native.close("frame limit exceeded");
|
|
21720
22497
|
return;
|
|
21721
22498
|
}
|
|
21722
|
-
handler(
|
|
21723
|
-
}
|
|
21724
|
-
|
|
22499
|
+
handler({ data });
|
|
22500
|
+
};
|
|
22501
|
+
if (socket.on)
|
|
22502
|
+
socket.on("message", (data, isBinary) => deliver(isBinary ? asBytes(data) : new TextDecoder().decode(asBytes(data))));
|
|
22503
|
+
else
|
|
22504
|
+
socket.addEventListener("message", (event) => deliver(typeof event.data === "string" || event.data instanceof Uint8Array ? event.data : String(event.data)));
|
|
22505
|
+
} else
|
|
21725
22506
|
socket.addEventListener(type, () => {
|
|
21726
22507
|
if (type === "open")
|
|
21727
22508
|
queued = 0;
|
|
@@ -21733,7 +22514,7 @@ function boundedWebSocketLink(socket, max = MAX_LINK_BYTES) {
|
|
|
21733
22514
|
});
|
|
21734
22515
|
}
|
|
21735
22516
|
};
|
|
21736
|
-
native = webSocketLink(bridge);
|
|
22517
|
+
native = webSocketLink(bridge, options);
|
|
21737
22518
|
socket.addEventListener("error", () => native.close("socket error"));
|
|
21738
22519
|
return {
|
|
21739
22520
|
get closed() {
|
|
@@ -21753,7 +22534,7 @@ function boundedWebSocketLink(socket, max = MAX_LINK_BYTES) {
|
|
|
21753
22534
|
}
|
|
21754
22535
|
if (socket.readyState === 0)
|
|
21755
22536
|
queued += bytes;
|
|
21756
|
-
native.send(frame);
|
|
22537
|
+
return native.send(frame);
|
|
21757
22538
|
},
|
|
21758
22539
|
receive(handler) {
|
|
21759
22540
|
receiving = true;
|
|
@@ -21764,47 +22545,17 @@ function boundedWebSocketLink(socket, max = MAX_LINK_BYTES) {
|
|
|
21764
22545
|
close: (reason) => native.close(reason)
|
|
21765
22546
|
};
|
|
21766
22547
|
}
|
|
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
|
-
}
|
|
22548
|
+
function createClientHost(link, options = {}) {
|
|
22549
|
+
return createHost({
|
|
22550
|
+
kind: "client",
|
|
22551
|
+
self: { kind: "client" },
|
|
22552
|
+
upstream: {
|
|
22553
|
+
link,
|
|
22554
|
+
principal: { kind: "server" },
|
|
22555
|
+
...options.onProtocolMismatch ? { onProtocolMismatch: options.onProtocolMismatch } : {}
|
|
21804
22556
|
},
|
|
21805
|
-
|
|
21806
|
-
|
|
21807
|
-
};
|
|
22557
|
+
onError: () => {}
|
|
22558
|
+
});
|
|
21808
22559
|
}
|
|
21809
22560
|
|
|
21810
22561
|
// packages/gateway/src/mobile/adapter.ts
|
|
@@ -21817,16 +22568,6 @@ var byMessage = (a, b) => messageTime(b) - messageTime(a) || Number(b.id) - Numb
|
|
|
21817
22568
|
// packages/gateway/src/mobile/adapter.ts
|
|
21818
22569
|
import { Check as Check2 } from "typebox/value";
|
|
21819
22570
|
|
|
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
22571
|
// packages/gateway/src/subagent-client.ts
|
|
21831
22572
|
function subagentClient(connect) {
|
|
21832
22573
|
let value;
|
|
@@ -21967,40 +22708,16 @@ function subagentClient(connect) {
|
|
|
21967
22708
|
};
|
|
21968
22709
|
}
|
|
21969
22710
|
|
|
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
22711
|
// packages/gateway/src/mobile/projection.ts
|
|
22002
22712
|
import { createHash as createHash2 } from "crypto";
|
|
22003
22713
|
|
|
22714
|
+
// packages/core/src/application-version.ts
|
|
22715
|
+
var RUNNING_VERSION = "0.4.4";
|
|
22716
|
+
var APPLICATION_PROTOCOL = 6;
|
|
22717
|
+
function stableVersion(value) {
|
|
22718
|
+
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)));
|
|
22719
|
+
}
|
|
22720
|
+
|
|
22004
22721
|
// packages/gateway/src/mobile/protocol.ts
|
|
22005
22722
|
import { Type as Type7 } from "typebox";
|
|
22006
22723
|
import { Check } from "typebox/value";
|
|
@@ -22072,7 +22789,7 @@ function buildHelloResult(serverId, serverName, apiVersion) {
|
|
|
22072
22789
|
var opt = Type7.Optional;
|
|
22073
22790
|
var str2 = Type7.String();
|
|
22074
22791
|
var int = Type7.Integer({ minimum: 0 });
|
|
22075
|
-
var
|
|
22792
|
+
var nullable2 = (schema) => Type7.Union([schema, Type7.Null()]);
|
|
22076
22793
|
var MobileContentRefSchema = Type7.Object({
|
|
22077
22794
|
sessionId: str2,
|
|
22078
22795
|
agentId: opt(str2),
|
|
@@ -22161,7 +22878,7 @@ var MobileSubagentRoundSchema = Type7.Object({
|
|
|
22161
22878
|
approvalCount: int,
|
|
22162
22879
|
readonly: Type7.Boolean(),
|
|
22163
22880
|
createdAt: Type7.Number(),
|
|
22164
|
-
endedAt:
|
|
22881
|
+
endedAt: nullable2(Type7.Number()),
|
|
22165
22882
|
usage: opt(MobileUsageSchema)
|
|
22166
22883
|
});
|
|
22167
22884
|
var MobileAgentSchema = Type7.Object({
|
|
@@ -22169,18 +22886,25 @@ var MobileAgentSchema = Type7.Object({
|
|
|
22169
22886
|
role: str2,
|
|
22170
22887
|
title: str2,
|
|
22171
22888
|
status: Type7.Union([Type7.Literal("idle"), Type7.Literal("queued"), Type7.Literal("running")]),
|
|
22172
|
-
activeRoundId:
|
|
22889
|
+
activeRoundId: nullable2(int),
|
|
22173
22890
|
lastRoundId: int,
|
|
22174
22891
|
roundCount: int,
|
|
22175
22892
|
approvalCount: int
|
|
22176
22893
|
});
|
|
22894
|
+
var MobileQueuedSchema = Type7.Object({
|
|
22895
|
+
id: int,
|
|
22896
|
+
mode: Type7.Union([Type7.Literal("steer"), Type7.Literal("followUp")]),
|
|
22897
|
+
preview: Type7.String(),
|
|
22898
|
+
images: int
|
|
22899
|
+
});
|
|
22177
22900
|
var MobileChatStatusSchema = Type7.Object({
|
|
22178
22901
|
busy: Type7.Boolean(),
|
|
22179
|
-
|
|
22902
|
+
queued: opt(Type7.Array(MobileQueuedSchema)),
|
|
22903
|
+
status: nullable2(str2),
|
|
22180
22904
|
usage: MobileUsageSchema,
|
|
22181
22905
|
taskUsage: opt(MobileUsageSchema),
|
|
22182
|
-
contextUsage: opt(Type7.Object({ tokens:
|
|
22183
|
-
agent: Type7.Object({ model:
|
|
22906
|
+
contextUsage: opt(Type7.Object({ tokens: nullable2(Type7.Number()), window: Type7.Number(), percent: nullable2(Type7.Number()) })),
|
|
22907
|
+
agent: Type7.Object({ model: nullable2(str2), thinkingLevel: nullable2(str2) }),
|
|
22184
22908
|
subagents: Type7.Array(MobileSubagentRoundSchema),
|
|
22185
22909
|
agents: Type7.Array(MobileAgentSchema),
|
|
22186
22910
|
approvalCount: int,
|
|
@@ -22190,8 +22914,8 @@ var MobileChatStatusSchema = Type7.Object({
|
|
|
22190
22914
|
var MobileChatSnapshotSchema = Type7.Object({
|
|
22191
22915
|
...MobileChatStatusSchema.properties,
|
|
22192
22916
|
items: Type7.Array(MobileChatItemSchema),
|
|
22193
|
-
historyBefore:
|
|
22194
|
-
streaming:
|
|
22917
|
+
historyBefore: nullable2(str2),
|
|
22918
|
+
streaming: nullable2(MobileStreamingSchema)
|
|
22195
22919
|
});
|
|
22196
22920
|
var MobileWorkspaceSchema = Type7.Object({ root: str2, name: str2 });
|
|
22197
22921
|
var MobileSessionSummarySchema = Type7.Object({
|
|
@@ -22224,7 +22948,7 @@ var sessionListResult = Type7.Object({
|
|
|
22224
22948
|
nextCursor: opt(str2)
|
|
22225
22949
|
});
|
|
22226
22950
|
var subscription = { subscriptionId: Type7.String({ minLength: 1 }) };
|
|
22227
|
-
var pageResult = Type7.Object({ items: Type7.Array(MobileChatItemSchema), historyBefore:
|
|
22951
|
+
var pageResult = Type7.Object({ items: Type7.Array(MobileChatItemSchema), historyBefore: nullable2(str2) });
|
|
22228
22952
|
var pageParams = Type7.Object({
|
|
22229
22953
|
...subscription,
|
|
22230
22954
|
before: Type7.String({ minLength: 1 }),
|
|
@@ -22259,6 +22983,34 @@ var MOBILE_API_V1_METHODS = {
|
|
|
22259
22983
|
params: Type7.Object({ ...subscription, requestId: Type7.String({ minLength: 1, maxLength: 128 }) }),
|
|
22260
22984
|
result: Type7.Object({ status: Type7.Union([Type7.Literal("accepted"), Type7.Literal("unknown")]) })
|
|
22261
22985
|
},
|
|
22986
|
+
"chat.steer": {
|
|
22987
|
+
params: Type7.Object({ ...subscription, id: int }),
|
|
22988
|
+
result: Type7.Object({
|
|
22989
|
+
result: Type7.Union([
|
|
22990
|
+
Type7.Literal("steered"),
|
|
22991
|
+
Type7.Literal("withdrawn"),
|
|
22992
|
+
Type7.Literal("placed"),
|
|
22993
|
+
Type7.Literal("settled"),
|
|
22994
|
+
Type7.Literal("unknown")
|
|
22995
|
+
]),
|
|
22996
|
+
id: opt(int),
|
|
22997
|
+
text: opt(Type7.String()),
|
|
22998
|
+
images: opt(int)
|
|
22999
|
+
})
|
|
23000
|
+
},
|
|
23001
|
+
"chat.withdraw": {
|
|
23002
|
+
params: Type7.Object({ ...subscription, id: int }),
|
|
23003
|
+
result: Type7.Object({
|
|
23004
|
+
result: Type7.Union([
|
|
23005
|
+
Type7.Literal("withdrawn"),
|
|
23006
|
+
Type7.Literal("placed"),
|
|
23007
|
+
Type7.Literal("settled"),
|
|
23008
|
+
Type7.Literal("unknown")
|
|
23009
|
+
]),
|
|
23010
|
+
text: opt(Type7.String()),
|
|
23011
|
+
images: opt(int)
|
|
23012
|
+
})
|
|
23013
|
+
},
|
|
22262
23014
|
"chat.abort": { params: Type7.Object(subscription), result: Type7.Object({ accepted: Type7.Boolean() }) },
|
|
22263
23015
|
"subagents.open": {
|
|
22264
23016
|
params: Type7.Object({ ...subscription, agentId: opt(Type7.String({ minLength: 1 })), taskId: opt(int) }),
|
|
@@ -22338,7 +23090,7 @@ var patchBody = {
|
|
|
22338
23090
|
seq: Type7.Integer({ minimum: 1 }),
|
|
22339
23091
|
upserts: Type7.Array(MobileChatItemSchema),
|
|
22340
23092
|
status: opt(MobileChatStatusSchema),
|
|
22341
|
-
streaming: opt(
|
|
23093
|
+
streaming: opt(nullable2(MobileStreamingSchema)),
|
|
22342
23094
|
streamingAppend: opt(Type7.Object({ text: opt(str2), thinking: opt(str2) }))
|
|
22343
23095
|
};
|
|
22344
23096
|
var MOBILE_API_V1_NOTIFICATIONS = {
|
|
@@ -22546,6 +23298,9 @@ function mapStatus(view, options = {}) {
|
|
|
22546
23298
|
const latest = view.compaction?.latest;
|
|
22547
23299
|
return {
|
|
22548
23300
|
busy: view.busy,
|
|
23301
|
+
...view.queued === undefined ? {} : {
|
|
23302
|
+
queued: view.queued.map(({ id, mode, preview, images }) => ({ id, mode, preview, images }))
|
|
23303
|
+
},
|
|
22549
23304
|
status: view.status,
|
|
22550
23305
|
usage: mapUsage(view.usage),
|
|
22551
23306
|
...view.taskUsage === undefined ? {} : { taskUsage: mapUsage(view.taskUsage) },
|
|
@@ -22850,7 +23605,7 @@ function chordToMobileCode(error) {
|
|
|
22850
23605
|
return "internal";
|
|
22851
23606
|
}
|
|
22852
23607
|
}
|
|
22853
|
-
async function
|
|
23608
|
+
async function remote2(work, invalidAs = "invalid_params") {
|
|
22854
23609
|
try {
|
|
22855
23610
|
return await work();
|
|
22856
23611
|
} catch (error) {
|
|
@@ -23032,6 +23787,10 @@ class MobileAdapter {
|
|
|
23032
23787
|
return this.chatSubmit(params);
|
|
23033
23788
|
case "chat.submissionStatus":
|
|
23034
23789
|
return this.chatSubmissionStatus(params);
|
|
23790
|
+
case "chat.steer":
|
|
23791
|
+
return this.chatSteer(params);
|
|
23792
|
+
case "chat.withdraw":
|
|
23793
|
+
return this.chatWithdraw(params);
|
|
23035
23794
|
case "chat.abort":
|
|
23036
23795
|
return this.chatAbort(params);
|
|
23037
23796
|
case "subagents.open":
|
|
@@ -23064,14 +23823,17 @@ class MobileAdapter {
|
|
|
23064
23823
|
}
|
|
23065
23824
|
}
|
|
23066
23825
|
async workspacesList() {
|
|
23067
|
-
const
|
|
23826
|
+
const { workspaces } = this.services();
|
|
23827
|
+
await workspaces.ready;
|
|
23828
|
+
const state = workspaces.state.value;
|
|
23068
23829
|
return {
|
|
23069
23830
|
workspaces: state.workspaces.map((workspace) => ({ root: workspace.path, name: baseName(workspace.path) }))
|
|
23070
23831
|
};
|
|
23071
23832
|
}
|
|
23072
23833
|
async registeredWorkspaceRoots() {
|
|
23073
|
-
const
|
|
23074
|
-
|
|
23834
|
+
const { workspaces } = this.services();
|
|
23835
|
+
await workspaces.ready;
|
|
23836
|
+
return workspaces.state.value.workspaces.map((workspace) => workspace.path);
|
|
23075
23837
|
}
|
|
23076
23838
|
async sessionsList(params) {
|
|
23077
23839
|
const roots = await this.registeredWorkspaceRoots();
|
|
@@ -23080,7 +23842,9 @@ class MobileAdapter {
|
|
|
23080
23842
|
const offset = params.cursor === undefined ? 0 : Number.parseInt(params.cursor, 10);
|
|
23081
23843
|
if (!Number.isInteger(offset) || offset < 0)
|
|
23082
23844
|
throw new MobileMethodError("invalid_params", "invalid cursor");
|
|
23083
|
-
const directory =
|
|
23845
|
+
const { directory: directoryRef } = this.services();
|
|
23846
|
+
await directoryRef.ready;
|
|
23847
|
+
const directory = directoryRef.state.value;
|
|
23084
23848
|
let sessions = directory.sessions.filter((session) => session.workspaceRoot === params.workspace);
|
|
23085
23849
|
if (params.query !== undefined) {
|
|
23086
23850
|
const query = params.query.toLowerCase();
|
|
@@ -23106,7 +23870,7 @@ class MobileAdapter {
|
|
|
23106
23870
|
if (!roots.includes(params.workspace))
|
|
23107
23871
|
throw new MobileMethodError("invalid_params", `unknown workspace: ${params.workspace}`);
|
|
23108
23872
|
this.assertAuthorized();
|
|
23109
|
-
const created = await
|
|
23873
|
+
const created = await remote2(() => this.services().management.create({ cwd: params.workspace }));
|
|
23110
23874
|
return {
|
|
23111
23875
|
id: created.id,
|
|
23112
23876
|
workspace: created.workspaceRoot,
|
|
@@ -23143,16 +23907,13 @@ class MobileAdapter {
|
|
|
23143
23907
|
const generation = ++this.attachGeneration;
|
|
23144
23908
|
this.closeSubagent();
|
|
23145
23909
|
this.releaseChat();
|
|
23146
|
-
const attachment = await
|
|
23910
|
+
const attachment = await remote2(() => services.management.attach(params.sessionId));
|
|
23147
23911
|
if (generation !== this.attachGeneration || this.disposed)
|
|
23148
23912
|
throw new MobileMethodError("gone", "superseded by a newer attach");
|
|
23149
23913
|
const chat = services.openChat(attachment);
|
|
23150
23914
|
const stopView = chat.view.subscribe(() => {});
|
|
23151
23915
|
try {
|
|
23152
|
-
await
|
|
23153
|
-
await chat.ready;
|
|
23154
|
-
await waitForValue(chat.view);
|
|
23155
|
-
});
|
|
23916
|
+
await remote2(() => chat.ready);
|
|
23156
23917
|
} catch (error) {
|
|
23157
23918
|
stopView();
|
|
23158
23919
|
chat.release();
|
|
@@ -23214,7 +23975,7 @@ class MobileAdapter {
|
|
|
23214
23975
|
this.attachGeneration += 1;
|
|
23215
23976
|
this.closeSubagent();
|
|
23216
23977
|
this.releaseChat();
|
|
23217
|
-
await
|
|
23978
|
+
await remote2(() => this.services().management.detach(null)).catch(() => {
|
|
23218
23979
|
return;
|
|
23219
23980
|
});
|
|
23220
23981
|
return null;
|
|
@@ -23245,7 +24006,7 @@ class MobileAdapter {
|
|
|
23245
24006
|
const state = this.requireChat(params);
|
|
23246
24007
|
const busy = state.chat.view.value?.busy ?? false;
|
|
23247
24008
|
this.assertAuthorized();
|
|
23248
|
-
await
|
|
24009
|
+
await remote2(() => state.chat.submit({
|
|
23249
24010
|
content: params.text,
|
|
23250
24011
|
requestId: params.requestId,
|
|
23251
24012
|
...params.mode === "steer" ? { whenBusy: "steer" } : {}
|
|
@@ -23259,10 +24020,20 @@ class MobileAdapter {
|
|
|
23259
24020
|
const accepted = await this.runtime.submissionAccepted(state.sessionId, params.requestId);
|
|
23260
24021
|
return { status: accepted ? "accepted" : "unknown" };
|
|
23261
24022
|
}
|
|
24023
|
+
async chatSteer(params) {
|
|
24024
|
+
const state = this.requireChat(params);
|
|
24025
|
+
this.assertAuthorized();
|
|
24026
|
+
return remote2(() => state.chat.steer({ id: params.id }), "rejected");
|
|
24027
|
+
}
|
|
24028
|
+
async chatWithdraw(params) {
|
|
24029
|
+
const state = this.requireChat(params);
|
|
24030
|
+
this.assertAuthorized();
|
|
24031
|
+
return remote2(() => state.chat.withdraw({ id: params.id }), "rejected");
|
|
24032
|
+
}
|
|
23262
24033
|
async chatAbort(params) {
|
|
23263
24034
|
const state = this.requireChat(params);
|
|
23264
24035
|
this.assertAuthorized();
|
|
23265
|
-
await
|
|
24036
|
+
await remote2(() => state.chat.abort(null));
|
|
23266
24037
|
return { accepted: true };
|
|
23267
24038
|
}
|
|
23268
24039
|
async approvalRecords(state) {
|
|
@@ -23341,7 +24112,7 @@ class MobileAdapter {
|
|
|
23341
24112
|
this.assertAuthorized();
|
|
23342
24113
|
const handle = openSubagent({ ...state.attachment, taskId: params.taskId });
|
|
23343
24114
|
try {
|
|
23344
|
-
const status = await
|
|
24115
|
+
const status = await remote2(() => handle.decideApproval({
|
|
23345
24116
|
id: params.id,
|
|
23346
24117
|
version: params.version,
|
|
23347
24118
|
decision: params.decision === "allow" ? "allowed" : "denied"
|
|
@@ -23421,7 +24192,7 @@ class MobileAdapter {
|
|
|
23421
24192
|
return () => listeners.delete(listener);
|
|
23422
24193
|
},
|
|
23423
24194
|
older: async (before) => {
|
|
23424
|
-
const page = await
|
|
24195
|
+
const page = await remote2(() => client.history({ before }));
|
|
23425
24196
|
return { items: page.items, next: page.next };
|
|
23426
24197
|
}
|
|
23427
24198
|
};
|
|
@@ -23465,7 +24236,7 @@ class MobileAdapter {
|
|
|
23465
24236
|
this.assertAuthorized();
|
|
23466
24237
|
const handle = openSubagent({ ...chat.attachment, taskId: params.taskId });
|
|
23467
24238
|
try {
|
|
23468
|
-
await
|
|
24239
|
+
await remote2(() => handle.stop({ taskId: params.taskId }));
|
|
23469
24240
|
} finally {
|
|
23470
24241
|
handle.release();
|
|
23471
24242
|
}
|
|
@@ -23479,9 +24250,9 @@ class MobileAdapter {
|
|
|
23479
24250
|
const state = this.subagentState;
|
|
23480
24251
|
if (state === undefined || state.agentId !== params.agentId || state.chatSubscriptionId !== chat.subscriptionId)
|
|
23481
24252
|
throw new MobileMethodError("gone", "subagent view is no longer open");
|
|
23482
|
-
chunk = await
|
|
24253
|
+
chunk = await remote2(() => state.client.getImage(input));
|
|
23483
24254
|
} else {
|
|
23484
|
-
chunk = await
|
|
24255
|
+
chunk = await remote2(() => chat.chat.getImage(input));
|
|
23485
24256
|
}
|
|
23486
24257
|
const bytes = Buffer.from(chunk.data, "base64").subarray(0, MOBILE_LIMITS.chunkBytes);
|
|
23487
24258
|
const nextOffset = params.offset + bytes.length;
|
|
@@ -24412,9 +25183,6 @@ function describeRelayError(error, address, port, bindAddress) {
|
|
|
24412
25183
|
return `Windows \u4E2D\u7EE7\u542F\u52A8\u5931\u8D25\uFF1A${error.message}`;
|
|
24413
25184
|
}
|
|
24414
25185
|
}
|
|
24415
|
-
function isMobilePrincipal2(principal) {
|
|
24416
|
-
return typeof principal === "object" && principal !== null && principal.kind === "mobile";
|
|
24417
|
-
}
|
|
24418
25186
|
function toMessage(error) {
|
|
24419
25187
|
if (error instanceof Error) {
|
|
24420
25188
|
const code = error.code;
|
|
@@ -24593,7 +25361,7 @@ class MobileGateway {
|
|
|
24593
25361
|
await store.clearMobileAuthorizations();
|
|
24594
25362
|
this.certificate = certificate;
|
|
24595
25363
|
this.pairingValue?.invalidate();
|
|
24596
|
-
await this.chord.invalidatePrincipal(
|
|
25364
|
+
await this.chord.invalidatePrincipal(isMobilePrincipal);
|
|
24597
25365
|
for (const connection of [...this.connections.values()])
|
|
24598
25366
|
connection.close(MOBILE_CLOSE_REVOKED, "revoked");
|
|
24599
25367
|
const current = this.stateValue;
|
|
@@ -24669,11 +25437,7 @@ class MobileGateway {
|
|
|
24669
25437
|
store.rememberConnectedAddress(learned).catch((error) => this.onError?.(toError(error)));
|
|
24670
25438
|
}
|
|
24671
25439
|
}
|
|
24672
|
-
const clientHost =
|
|
24673
|
-
kind: "client",
|
|
24674
|
-
upstream: identifiedLink(clientEnd, this.serverId),
|
|
24675
|
-
onError: () => {}
|
|
24676
|
-
});
|
|
25440
|
+
const clientHost = createClientHost(clientEnd);
|
|
24677
25441
|
let servicesValue;
|
|
24678
25442
|
const adapter = new MobileAdapter({
|
|
24679
25443
|
socket,
|
|
@@ -24685,11 +25449,11 @@ class MobileGateway {
|
|
|
24685
25449
|
services: () => {
|
|
24686
25450
|
if (servicesValue === undefined) {
|
|
24687
25451
|
servicesValue = {
|
|
24688
|
-
directory: clientHost.
|
|
24689
|
-
management: clientHost.
|
|
24690
|
-
workspaces: clientHost.
|
|
24691
|
-
openChat: (attachment) => clientHost.
|
|
24692
|
-
openSubagent: (input) => clientHost.
|
|
25452
|
+
directory: clientHost.hold(NekoSessionDirectory, {}),
|
|
25453
|
+
management: clientHost.hold(NekoSessionManagement, {}),
|
|
25454
|
+
workspaces: clientHost.hold(NekoWorkspaces, {}),
|
|
25455
|
+
openChat: (attachment) => clientHost.hold(NekoChat, attachment),
|
|
25456
|
+
openSubagent: (input) => clientHost.hold(NekoSubagent, input)
|
|
24693
25457
|
};
|
|
24694
25458
|
}
|
|
24695
25459
|
return servicesValue;
|
|
@@ -24713,10 +25477,13 @@ class MobileGateway {
|
|
|
24713
25477
|
if (this.notificationCleanup !== undefined)
|
|
24714
25478
|
return;
|
|
24715
25479
|
const [serverEnd, clientEnd] = memoryLinkPair();
|
|
24716
|
-
this.chord.accept(serverEnd, {
|
|
24717
|
-
|
|
24718
|
-
|
|
24719
|
-
|
|
25480
|
+
this.chord.accept(serverEnd, {
|
|
25481
|
+
principal: { kind: "local", connectionId: "mobile-notification-hub" },
|
|
25482
|
+
kind: "client"
|
|
25483
|
+
});
|
|
25484
|
+
const host = createClientHost(clientEnd);
|
|
25485
|
+
const directory = host.hold(NekoSessionDirectory, {});
|
|
25486
|
+
const workspaces = host.hold(NekoWorkspaces, {});
|
|
24720
25487
|
const last = { directory: -1, workspaces: -1 };
|
|
24721
25488
|
const unsubscribe = [
|
|
24722
25489
|
directory.state.subscribe(() => {
|
|
@@ -25013,14 +25780,7 @@ async function removeGatewayPortFile(nekoDir, serverId) {
|
|
|
25013
25780
|
|
|
25014
25781
|
// packages/gateway/src/server-services.ts
|
|
25015
25782
|
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";
|
|
25783
|
+
import { ChordError as ChordError5, local as local3, method as method2 } from "@earendil-works/chord";
|
|
25024
25784
|
|
|
25025
25785
|
// packages/core/src/application-updates.ts
|
|
25026
25786
|
import { mkdir as mkdir3, readFile as readFile6, rename as rename3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
|
|
@@ -25177,12 +25937,8 @@ import { open as open2 } from "fs/promises";
|
|
|
25177
25937
|
import { basename as basename3, extname } from "path";
|
|
25178
25938
|
import {
|
|
25179
25939
|
ChordError as ChordError4,
|
|
25180
|
-
cell as cell2,
|
|
25181
|
-
hostPrincipal as hostPrincipal2,
|
|
25182
|
-
listener as listener2,
|
|
25183
25940
|
local as local2,
|
|
25184
|
-
opaque as opaque2
|
|
25185
|
-
resource as resource2
|
|
25941
|
+
opaque as opaque2
|
|
25186
25942
|
} from "@earendil-works/chord";
|
|
25187
25943
|
import { withAbortSignal as withAbortSignal4 } from "@earendil-works/chord/context";
|
|
25188
25944
|
import { UsageDoc as UsageDoc4 } from "@earendil-works/pi-durable";
|
|
@@ -25283,15 +26039,7 @@ class ImageUploads {
|
|
|
25283
26039
|
}
|
|
25284
26040
|
|
|
25285
26041
|
// 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";
|
|
26042
|
+
import { ChordError as ChordError3, local, opaque } from "@earendil-works/chord";
|
|
25295
26043
|
import { UsageDoc as UsageDoc3 } from "@earendil-works/pi-durable";
|
|
25296
26044
|
import { Type as Type8 } from "typebox";
|
|
25297
26045
|
async function openReader(runtime, childId, parentId, context, through, historyUsage) {
|
|
@@ -25426,31 +26174,27 @@ var ChildProjection = local("neko.subagent-projection", {
|
|
|
25426
26174
|
});
|
|
25427
26175
|
function installSubagentHost(host, runtime, context) {
|
|
25428
26176
|
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
|
-
})
|
|
26177
|
+
const revision = self2.state("revision", Type8.Integer(), 0);
|
|
26178
|
+
const reader = self2.resource("reader", {
|
|
26179
|
+
version: 1,
|
|
26180
|
+
async open() {
|
|
26181
|
+
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);
|
|
26182
|
+
if (!entry)
|
|
26183
|
+
throw new ChordError3("gone", "Subagent unavailable");
|
|
26184
|
+
const r = await openReader(runtime, entry.childId, input.parentId, context, entry.historyThrough, entry.historyUsage);
|
|
26185
|
+
try {
|
|
26186
|
+
const initial = await r.refresh();
|
|
26187
|
+
await host.root((ctx) => revision.replace(ctx, initial));
|
|
26188
|
+
return r;
|
|
26189
|
+
} catch (error) {
|
|
26190
|
+
r.close();
|
|
26191
|
+
throw error;
|
|
26192
|
+
}
|
|
26193
|
+
},
|
|
26194
|
+
close: (r) => r.close()
|
|
25449
26195
|
});
|
|
25450
|
-
self2.
|
|
25451
|
-
|
|
25452
|
-
revision.replace(ctx, await reader.value.refresh());
|
|
25453
|
-
})
|
|
26196
|
+
self2.on("view", reader, (r) => r, async (_event, ctx) => {
|
|
26197
|
+
revision.replace(ctx, await reader.value.refresh());
|
|
25454
26198
|
});
|
|
25455
26199
|
return { revision, reader: self2.expose(reader) };
|
|
25456
26200
|
});
|
|
@@ -25466,23 +26210,24 @@ function installSubagentHost(host, runtime, context) {
|
|
|
25466
26210
|
throw new ChordError3("forbidden", "Subagent does not belong to this session");
|
|
25467
26211
|
return entry;
|
|
25468
26212
|
};
|
|
25469
|
-
const
|
|
25470
|
-
const shared = self2.get(ChildProjection, {
|
|
25471
|
-
|
|
25472
|
-
|
|
25473
|
-
|
|
25474
|
-
|
|
25475
|
-
|
|
25476
|
-
|
|
25477
|
-
|
|
25478
|
-
|
|
25479
|
-
|
|
25480
|
-
authorizedRevision.replace(ctx,
|
|
25481
|
-
|
|
25482
|
-
|
|
26213
|
+
const target = self2.resource("target", { version: 1, deps: [owner], open: check });
|
|
26214
|
+
const shared = self2.get(ChildProjection, {
|
|
26215
|
+
parentId: Number(input.sessionId),
|
|
26216
|
+
agentId: input.agentId ?? `round-${input.taskId}`
|
|
26217
|
+
});
|
|
26218
|
+
const authorizedRevision = self2.state("authorizedRevision", Type8.Integer(), 0);
|
|
26219
|
+
self2.on("authorizedView", shared.revision, async (value, ctx) => {
|
|
26220
|
+
try {
|
|
26221
|
+
target.value;
|
|
26222
|
+
await check();
|
|
26223
|
+
if (value)
|
|
26224
|
+
authorizedRevision.replace(ctx, value);
|
|
26225
|
+
} catch {
|
|
26226
|
+
authorizedRevision.replace(ctx, 0);
|
|
26227
|
+
}
|
|
25483
26228
|
});
|
|
25484
26229
|
return {
|
|
25485
|
-
|
|
26230
|
+
variant: "ready",
|
|
25486
26231
|
revision: authorizedRevision,
|
|
25487
26232
|
chunk: async (request) => {
|
|
25488
26233
|
await check();
|
|
@@ -25542,6 +26287,7 @@ var emptyView = {
|
|
|
25542
26287
|
agent: { model: null, thinkingLevel: null },
|
|
25543
26288
|
cwd: "",
|
|
25544
26289
|
busy: false,
|
|
26290
|
+
queued: [],
|
|
25545
26291
|
status: null,
|
|
25546
26292
|
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 },
|
|
25547
26293
|
items: [],
|
|
@@ -25638,65 +26384,61 @@ async function openProjection(runtime, session, context) {
|
|
|
25638
26384
|
function serveSessions(host, runtime, context) {
|
|
25639
26385
|
installSubagentHost(host, runtime, context);
|
|
25640
26386
|
host.serve(Owner, (_input, _principal, self2) => {
|
|
25641
|
-
const
|
|
25642
|
-
|
|
25643
|
-
|
|
25644
|
-
|
|
25645
|
-
|
|
25646
|
-
|
|
25647
|
-
}
|
|
25648
|
-
})
|
|
26387
|
+
const owner = self2.resource("owner", {
|
|
26388
|
+
version: 1,
|
|
26389
|
+
open: () => ({ active: null }),
|
|
26390
|
+
close: (value) => {
|
|
26391
|
+
value.active = null;
|
|
26392
|
+
}
|
|
25649
26393
|
});
|
|
25650
26394
|
return { owner: self2.expose(owner) };
|
|
25651
26395
|
});
|
|
25652
26396
|
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
|
-
})
|
|
26397
|
+
const view = self2.state("view", ChatViewSchema, emptyView);
|
|
26398
|
+
const projection = self2.resource("projection", {
|
|
26399
|
+
version: 1,
|
|
26400
|
+
async open({ signal }) {
|
|
26401
|
+
const session = await runtime.getConversation(Number(input.id));
|
|
26402
|
+
if (!session)
|
|
26403
|
+
throw new ChordError4("gone", "Session unavailable");
|
|
26404
|
+
const opened = await openProjection(runtime, session, context);
|
|
26405
|
+
try {
|
|
26406
|
+
const initial = await opened.project();
|
|
26407
|
+
await host.root((ctx) => view.replace(ctx, initial), { signal });
|
|
26408
|
+
return opened;
|
|
26409
|
+
} catch (error) {
|
|
26410
|
+
opened.close();
|
|
26411
|
+
throw error;
|
|
26412
|
+
}
|
|
26413
|
+
},
|
|
26414
|
+
close: (value) => value.close()
|
|
25673
26415
|
});
|
|
25674
|
-
self2.
|
|
25675
|
-
view
|
|
25676
|
-
view.replace(ctx, await projection.value.project());
|
|
25677
|
-
})
|
|
26416
|
+
self2.on("view", projection, (value) => value, async (_event, ctx) => {
|
|
26417
|
+
view.replace(ctx, await projection.value.project());
|
|
25678
26418
|
});
|
|
25679
26419
|
return { view, reader: self2.expose(projection) };
|
|
25680
26420
|
});
|
|
25681
26421
|
host.serve(NekoChat, (input, principal, self2) => {
|
|
25682
26422
|
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
|
-
|
|
26423
|
+
const shared = self2.get(Projection, { id: input.sessionId });
|
|
26424
|
+
const uploads = self2.resource("uploads", {
|
|
26425
|
+
version: 1,
|
|
26426
|
+
open: () => new ImageUploads,
|
|
26427
|
+
close: (value) => value.close()
|
|
26428
|
+
});
|
|
26429
|
+
const projection = self2.resource("projection", {
|
|
26430
|
+
version: 1,
|
|
26431
|
+
deps: [owner],
|
|
26432
|
+
async open() {
|
|
26433
|
+
if (owner.owner?.active?.attachmentId !== input.attachmentId || owner.owner.active.sessionId !== input.sessionId)
|
|
26434
|
+
throw new ChordError4("forbidden", "Attachment does not belong to this connection");
|
|
26435
|
+
const session = await runtime.getConversation(Number(input.sessionId));
|
|
26436
|
+
if (!session)
|
|
26437
|
+
throw new ChordError4("gone", "Session unavailable");
|
|
26438
|
+
return {
|
|
26439
|
+
session
|
|
26440
|
+
};
|
|
26441
|
+
}
|
|
25700
26442
|
});
|
|
25701
26443
|
const safeUpload = (action) => {
|
|
25702
26444
|
try {
|
|
@@ -25706,7 +26448,7 @@ function serveSessions(host, runtime, context) {
|
|
|
25706
26448
|
}
|
|
25707
26449
|
};
|
|
25708
26450
|
const behavior = {
|
|
25709
|
-
|
|
26451
|
+
variant: "ready",
|
|
25710
26452
|
view: shared.view,
|
|
25711
26453
|
draftImage: async ({ id, offset }) => safeUpload(() => uploads.value.read(id, offset)),
|
|
25712
26454
|
importImage: async (path, ctx) => {
|
|
@@ -25813,6 +26555,8 @@ function serveSessions(host, runtime, context) {
|
|
|
25813
26555
|
await projection.value.session.abort();
|
|
25814
26556
|
return null;
|
|
25815
26557
|
},
|
|
26558
|
+
withdraw: ({ id }) => projection.value.session.withdraw(id),
|
|
26559
|
+
steer: ({ id }) => projection.value.session.steer(id),
|
|
25816
26560
|
skills: () => runtime.listSkills(projection.value.session.id),
|
|
25817
26561
|
jobOutput: async ({ jobId, since }) => {
|
|
25818
26562
|
try {
|
|
@@ -26568,7 +27312,11 @@ var ServerState = local3("neko.server-state", {
|
|
|
26568
27312
|
directory: DirectoryStateSchema,
|
|
26569
27313
|
workspaces: WorkspaceStateSchema,
|
|
26570
27314
|
credentials: CredentialStateSchema,
|
|
26571
|
-
|
|
27315
|
+
searchSettings: SearchSettingsStateSchema,
|
|
27316
|
+
contextSettings: ContextSettingsStateSchema,
|
|
27317
|
+
refreshCredentials: method2(Type10.Null(), Type10.Null()),
|
|
27318
|
+
refreshSearchSettings: method2(Type10.Null(), Type10.Null()),
|
|
27319
|
+
refreshContextSettings: method2(Type10.Null(), Type10.Null())
|
|
26572
27320
|
}
|
|
26573
27321
|
});
|
|
26574
27322
|
async function createNekoServerServices(options) {
|
|
@@ -26576,6 +27324,7 @@ async function createNekoServerServices(options) {
|
|
|
26576
27324
|
const updates = new UpdateChecker(options.updates);
|
|
26577
27325
|
await updates.local();
|
|
26578
27326
|
const updateListeners = new Set;
|
|
27327
|
+
const info = (value) => ({ ...value, serverId: options.serverId });
|
|
26579
27328
|
let published = JSON.stringify(updates.state), closed = false;
|
|
26580
27329
|
const publishUpdates = () => {
|
|
26581
27330
|
if (closed)
|
|
@@ -26588,22 +27337,20 @@ async function createNekoServerServices(options) {
|
|
|
26588
27337
|
listener(updates.state);
|
|
26589
27338
|
};
|
|
26590
27339
|
host.serve(NekoApplicationInfo, (_input, _principal, self2) => {
|
|
26591
|
-
const
|
|
26592
|
-
self2.
|
|
26593
|
-
|
|
26594
|
-
|
|
26595
|
-
|
|
26596
|
-
|
|
26597
|
-
|
|
26598
|
-
}, (value, ctx) => state.info.replace(ctx, value))
|
|
26599
|
-
});
|
|
27340
|
+
const current = self2.state("info", ApplicationInfoSchema, info(updates.state));
|
|
27341
|
+
self2.on("updates", {
|
|
27342
|
+
subscribe: (fn) => {
|
|
27343
|
+
updateListeners.add(fn);
|
|
27344
|
+
return () => updateListeners.delete(fn);
|
|
27345
|
+
}
|
|
27346
|
+
}, (value, ctx) => current.replace(ctx, info(value)));
|
|
26600
27347
|
return {
|
|
26601
|
-
|
|
26602
|
-
info:
|
|
27348
|
+
variant: "ready",
|
|
27349
|
+
info: current,
|
|
26603
27350
|
check: async () => {
|
|
26604
27351
|
await updates.check(true);
|
|
26605
27352
|
publishUpdates();
|
|
26606
|
-
return updates.state;
|
|
27353
|
+
return info(updates.state);
|
|
26607
27354
|
}
|
|
26608
27355
|
};
|
|
26609
27356
|
});
|
|
@@ -26647,7 +27394,13 @@ async function createNekoServerServices(options) {
|
|
|
26647
27394
|
status: info.status
|
|
26648
27395
|
}));
|
|
26649
27396
|
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([
|
|
27397
|
+
const [sessions, workspaces, statuses, search, contextSettings0] = await Promise.all([
|
|
27398
|
+
list(),
|
|
27399
|
+
runtime.listWorkspaces(),
|
|
27400
|
+
readStatuses(),
|
|
27401
|
+
runtime.searchSettings(),
|
|
27402
|
+
runtime.contextSettings()
|
|
27403
|
+
]);
|
|
26651
27404
|
let mutationTail = Promise.resolve();
|
|
26652
27405
|
const serialize = (fn) => {
|
|
26653
27406
|
const result = mutationTail.catch(() => {}).then(fn);
|
|
@@ -26655,43 +27408,64 @@ async function createNekoServerServices(options) {
|
|
|
26655
27408
|
return result;
|
|
26656
27409
|
};
|
|
26657
27410
|
host.serve(ServerState, (_input, _principal, self2) => {
|
|
26658
|
-
const
|
|
26659
|
-
|
|
26660
|
-
|
|
26661
|
-
|
|
27411
|
+
const directory = self2.state("directory", DirectoryStateSchema, { revision: 1, sessions });
|
|
27412
|
+
const workspaceState = self2.state("workspaces", WorkspaceStateSchema, { revision: 1, workspaces });
|
|
27413
|
+
const credentials = self2.state("credentials", CredentialStateSchema, { revision: 1, statuses });
|
|
27414
|
+
const searchSettings = self2.state("searchSettings", SearchSettingsStateSchema, { revision: 1, ...search });
|
|
27415
|
+
const contextSettings = self2.state("contextSettings", ContextSettingsStateSchema, {
|
|
27416
|
+
revision: 1,
|
|
27417
|
+
...contextSettings0
|
|
26662
27418
|
});
|
|
26663
|
-
|
|
26664
|
-
|
|
26665
|
-
|
|
26666
|
-
|
|
26667
|
-
|
|
26668
|
-
|
|
26669
|
-
|
|
26670
|
-
|
|
26671
|
-
|
|
26672
|
-
|
|
26673
|
-
|
|
26674
|
-
|
|
26675
|
-
|
|
26676
|
-
|
|
26677
|
-
|
|
26678
|
-
|
|
27419
|
+
const refreshContextSettings = async (_input, ctx) => {
|
|
27420
|
+
const next = await runtime.contextSettings();
|
|
27421
|
+
const { revision, ...current } = contextSettings.value;
|
|
27422
|
+
if (JSON.stringify(next) !== JSON.stringify(current))
|
|
27423
|
+
contextSettings.replace(ctx, { revision: revision + 1, ...next });
|
|
27424
|
+
return null;
|
|
27425
|
+
};
|
|
27426
|
+
const refreshSearchSettings = async (_input, ctx) => {
|
|
27427
|
+
const next = await runtime.searchSettings();
|
|
27428
|
+
const current = searchSettings.value;
|
|
27429
|
+
if (next.selected !== current.selected)
|
|
27430
|
+
searchSettings.replace(ctx, { revision: current.revision + 1, ...next });
|
|
27431
|
+
return null;
|
|
27432
|
+
};
|
|
27433
|
+
self2.on("directory", { subscribe: runtime.harness.subscribeCommits.bind(runtime.harness) }, async (event, ctx) => {
|
|
27434
|
+
if (!event.changes.some((change) => change.type === "conversation" || change.type === "document" && [
|
|
27435
|
+
"app.neko.session-metadata",
|
|
27436
|
+
"app.neko.activity",
|
|
27437
|
+
"app.neko.conversation",
|
|
27438
|
+
"app.neko.workspaces",
|
|
27439
|
+
"pi.agent"
|
|
27440
|
+
].includes(change.record.kind)))
|
|
27441
|
+
return;
|
|
27442
|
+
await serialize(async () => {
|
|
27443
|
+
const [items, roots] = await Promise.all([list(), runtime.listWorkspaces()]);
|
|
27444
|
+
directory.replace(ctx, { revision: directory.value.revision + 1, sessions: items });
|
|
27445
|
+
workspaceState.replace(ctx, { revision: workspaceState.value.revision + 1, workspaces: roots });
|
|
27446
|
+
});
|
|
26679
27447
|
});
|
|
26680
27448
|
return {
|
|
26681
|
-
|
|
27449
|
+
directory,
|
|
27450
|
+
workspaces: workspaceState,
|
|
27451
|
+
credentials,
|
|
27452
|
+
searchSettings,
|
|
27453
|
+
contextSettings,
|
|
26682
27454
|
async refreshCredentials(_input, ctx) {
|
|
26683
|
-
|
|
26684
|
-
revision:
|
|
27455
|
+
credentials.replace(ctx, {
|
|
27456
|
+
revision: credentials.value.revision + 1,
|
|
26685
27457
|
statuses: await readStatuses()
|
|
26686
27458
|
});
|
|
26687
27459
|
return null;
|
|
26688
|
-
}
|
|
27460
|
+
},
|
|
27461
|
+
refreshSearchSettings,
|
|
27462
|
+
refreshContextSettings
|
|
26689
27463
|
};
|
|
26690
|
-
}, {
|
|
26691
|
-
const shared = (self2) => self2.get(ServerState, {}
|
|
26692
|
-
host.serve(NekoSessionDirectory, (_input, _principal, self2) => ({
|
|
27464
|
+
}, { lifetime: "host" });
|
|
27465
|
+
const shared = (self2) => self2.get(ServerState, {});
|
|
27466
|
+
host.serve(NekoSessionDirectory, (_input, _principal, self2) => ({ variant: "ready", state: shared(self2).directory }));
|
|
26693
27467
|
host.serve(NekoWorkspaces, (_input, _principal, self2) => ({
|
|
26694
|
-
|
|
27468
|
+
variant: "ready",
|
|
26695
27469
|
state: shared(self2).workspaces,
|
|
26696
27470
|
browse: (input) => browseWorkspaces(input),
|
|
26697
27471
|
add: (input) => serialize(() => runtime.addWorkspace(input.path))
|
|
@@ -26699,7 +27473,7 @@ async function createNekoServerServices(options) {
|
|
|
26699
27473
|
host.serve(NekoCredentials, (_input, _principal, self2) => {
|
|
26700
27474
|
const hub = shared(self2);
|
|
26701
27475
|
return {
|
|
26702
|
-
|
|
27476
|
+
variant: "ready",
|
|
26703
27477
|
state: hub.credentials,
|
|
26704
27478
|
save: async (input, ctx) => {
|
|
26705
27479
|
await runtime.saveCredential(input.provider, input.key);
|
|
@@ -26711,8 +27485,34 @@ async function createNekoServerServices(options) {
|
|
|
26711
27485
|
}
|
|
26712
27486
|
};
|
|
26713
27487
|
});
|
|
27488
|
+
host.serve(NekoSearchSettings, (_input, _principal, self2) => {
|
|
27489
|
+
const hub = shared(self2);
|
|
27490
|
+
return {
|
|
27491
|
+
variant: "ready",
|
|
27492
|
+
state: hub.searchSettings,
|
|
27493
|
+
set: (input, ctx) => serialize(async () => {
|
|
27494
|
+
await runtime.setSearchProvider(input.provider);
|
|
27495
|
+
return hub.refreshSearchSettings(null, ctx);
|
|
27496
|
+
})
|
|
27497
|
+
};
|
|
27498
|
+
});
|
|
27499
|
+
host.serve(NekoContextSettings, (_input, _principal, self2) => {
|
|
27500
|
+
const hub = shared(self2);
|
|
27501
|
+
return {
|
|
27502
|
+
variant: "ready",
|
|
27503
|
+
state: hub.contextSettings,
|
|
27504
|
+
set: (input, ctx) => serialize(async () => {
|
|
27505
|
+
await runtime.setContextSettings(input);
|
|
27506
|
+
return hub.refreshContextSettings(null, ctx);
|
|
27507
|
+
}),
|
|
27508
|
+
reset: (_input, ctx) => serialize(async () => {
|
|
27509
|
+
await runtime.resetContextSettings();
|
|
27510
|
+
return hub.refreshContextSettings(null, ctx);
|
|
27511
|
+
})
|
|
27512
|
+
};
|
|
27513
|
+
});
|
|
26714
27514
|
host.serve(NekoModels, () => ({
|
|
26715
|
-
|
|
27515
|
+
variant: "ready",
|
|
26716
27516
|
list: async () => [...runtime.listModels()].map((value) => ({
|
|
26717
27517
|
...value,
|
|
26718
27518
|
input: [...value.input],
|
|
@@ -26720,7 +27520,7 @@ async function createNekoServerServices(options) {
|
|
|
26720
27520
|
}))
|
|
26721
27521
|
}));
|
|
26722
27522
|
host.serve(NekoWorkspaceFiles, () => ({
|
|
26723
|
-
|
|
27523
|
+
variant: "ready",
|
|
26724
27524
|
search: async (input, ctx) => {
|
|
26725
27525
|
const roots = await runtime.listWorkspaces();
|
|
26726
27526
|
if (!roots.some((root) => root.path === input.workspaceRoot))
|
|
@@ -26738,7 +27538,7 @@ async function createNekoServerServices(options) {
|
|
|
26738
27538
|
return found;
|
|
26739
27539
|
};
|
|
26740
27540
|
return {
|
|
26741
|
-
|
|
27541
|
+
variant: "ready",
|
|
26742
27542
|
create: (input) => serialize(async () => summary((await runtime.createConversation(input)).id)),
|
|
26743
27543
|
fork: (id) => serialize(async () => summary((await runtime.forkConversation(toConversationId(id))).id)),
|
|
26744
27544
|
archive: async (id) => {
|
|
@@ -26761,7 +27561,7 @@ async function createNekoServerServices(options) {
|
|
|
26761
27561
|
};
|
|
26762
27562
|
});
|
|
26763
27563
|
host.serve(NekoSkillMarket, () => ({
|
|
26764
|
-
|
|
27564
|
+
variant: "ready",
|
|
26765
27565
|
list: (input) => market.list(input.query).then((skills) => Promise.all(skills.map((skill) => withInstallState(skill, input.workspaceRoot)))).catch(toRemote),
|
|
26766
27566
|
detail: (input) => market.detail(input.name).then((skill) => withInstallState(skill, input.workspaceRoot)).catch(toRemote),
|
|
26767
27567
|
preview: (input) => market.preview(input.name).then((value) => ({ ...value, files: [...value.files] })).catch(toRemote),
|
|
@@ -26876,7 +27676,7 @@ class UnixLink {
|
|
|
26876
27676
|
}
|
|
26877
27677
|
send(frame) {
|
|
26878
27678
|
try {
|
|
26879
|
-
this.#native.send(frame);
|
|
27679
|
+
return this.#native.send(frame);
|
|
26880
27680
|
} catch (error) {
|
|
26881
27681
|
this.#native.close("pending byte limit exceeded");
|
|
26882
27682
|
throw error;
|
|
@@ -26985,7 +27785,7 @@ function createWsListener(options) {
|
|
|
26985
27785
|
server.on("error", (error) => options.onError?.(error));
|
|
26986
27786
|
server.on("connection", (socket) => {
|
|
26987
27787
|
socket.on("error", (error) => options.onError?.(error));
|
|
26988
|
-
accept(boundedWebSocketLink(socket, options.maxPendingBytes));
|
|
27788
|
+
accept(boundedWebSocketLink(socket, options.maxPendingBytes, { acceptPacked: false }));
|
|
26989
27789
|
});
|
|
26990
27790
|
},
|
|
26991
27791
|
async close() {
|
|
@@ -27011,6 +27811,7 @@ async function startGateway(options = {}) {
|
|
|
27011
27811
|
const storagePath = options.storagePath ?? fileConfig.storagePath ?? DEFAULT_STORAGE_PATH;
|
|
27012
27812
|
const host = options.host ?? fileConfig.gateway?.host ?? DEFAULT_HOST;
|
|
27013
27813
|
const port = options.port !== undefined ? options.port : fileConfig.gateway?.port ?? (process.platform === "win32" ? 0 : DEFAULT_PORT);
|
|
27814
|
+
const searchProvider = options.searchProvider ?? fileConfig.webSearch?.provider;
|
|
27014
27815
|
const marketUrl = options.marketUrl ?? fileConfig.skills?.marketUrl ?? DEFAULT_SKILL_MARKET_URL;
|
|
27015
27816
|
const nekoDir = dirname9(storagePath);
|
|
27016
27817
|
const serverId = options.serverId ?? await loadOrCreateServerId(nekoDir);
|
|
@@ -27022,6 +27823,8 @@ async function startGateway(options = {}) {
|
|
|
27022
27823
|
...options.storage !== "memory" ? { storagePath } : {},
|
|
27023
27824
|
...options.credentialsPath !== undefined ? { credentialsPath: options.credentialsPath } : {},
|
|
27024
27825
|
...options.apiKey !== undefined ? { apiKey: options.apiKey } : {},
|
|
27826
|
+
...searchProvider !== undefined ? { searchProvider } : {},
|
|
27827
|
+
...fileConfig.context !== undefined ? { contextSettings: fileConfig.context } : {},
|
|
27025
27828
|
model,
|
|
27026
27829
|
thinkingLevel: options.thinkingLevel ?? fileConfig.thinkingLevel ?? "high",
|
|
27027
27830
|
...options.cwd !== undefined ? { cwd: options.cwd } : {},
|
|
@@ -27052,8 +27855,9 @@ async function startGateway(options = {}) {
|
|
|
27052
27855
|
}
|
|
27053
27856
|
async function bindGateway(runtime, options) {
|
|
27054
27857
|
const { context, host, port, socketPath, serverId, nekoDir } = options;
|
|
27055
|
-
const chord =
|
|
27056
|
-
name:
|
|
27858
|
+
const chord = createHost2({
|
|
27859
|
+
name: GATEWAY_HOST,
|
|
27860
|
+
self: HOST_PRINCIPAL,
|
|
27057
27861
|
dev: true,
|
|
27058
27862
|
onError: (error) => options.onError?.(error instanceof Error ? error : new Error(String(error))),
|
|
27059
27863
|
principalKey: gatewayPrincipalKey
|
|
@@ -27062,6 +27866,7 @@ async function bindGateway(runtime, options) {
|
|
|
27062
27866
|
host: chord,
|
|
27063
27867
|
runtime,
|
|
27064
27868
|
context,
|
|
27869
|
+
serverId,
|
|
27065
27870
|
marketUrl: options.marketUrl,
|
|
27066
27871
|
...options.updates ? { updates: options.updates } : {},
|
|
27067
27872
|
...options.updateIntervalMs ? { updateIntervalMs: options.updateIntervalMs } : {},
|
|
@@ -27077,7 +27882,10 @@ async function bindGateway(runtime, options) {
|
|
|
27077
27882
|
}
|
|
27078
27883
|
try {
|
|
27079
27884
|
for (const listener of listeners)
|
|
27080
|
-
await listener.start((link) => chord.accept(link, {
|
|
27885
|
+
await listener.start((link) => chord.accept(link, {
|
|
27886
|
+
principal: { kind: "local", connectionId: randomUUID5() },
|
|
27887
|
+
onProtocolMismatch: ({ peer }) => options.onError?.(new Error(`Rejected a client on Chord protocol ${peer ?? "unknown"}; restart or update it.`))
|
|
27888
|
+
}));
|
|
27081
27889
|
} catch (error) {
|
|
27082
27890
|
await Promise.allSettled(listeners.map((listener) => listener.close()));
|
|
27083
27891
|
await chord.close();
|
|
@@ -27140,16 +27948,6 @@ async function bindGateway(runtime, options) {
|
|
|
27140
27948
|
}).catch((error) => options.onError?.(error instanceof Error ? error : new Error(String(error))));
|
|
27141
27949
|
return gateway;
|
|
27142
27950
|
}
|
|
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
27951
|
function toStartupError(error, info) {
|
|
27154
27952
|
const message = error instanceof Error ? error.message : String(error);
|
|
27155
27953
|
const existingServerId = info.socketPath === null ? undefined : serverIdFromSocketPath(info.socketPath);
|