gnosys 5.12.0 → 5.12.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +48 -7
- package/dist/index.js +199 -14
- package/dist/lib/addCommand.js +0 -1
- package/dist/lib/archive.js +10 -6
- package/dist/lib/askCommand.js +1 -1
- package/dist/lib/attachCommand.d.ts +17 -0
- package/dist/lib/attachCommand.js +66 -0
- package/dist/lib/attachments.d.ts +43 -2
- package/dist/lib/attachments.js +81 -2
- package/dist/lib/chat/choose.js +2 -2
- package/dist/lib/clientReadOverlay.js +3 -0
- package/dist/lib/config.d.ts +1 -48
- package/dist/lib/configCommand.js +2 -2
- package/dist/lib/db.d.ts +16 -1
- package/dist/lib/db.js +248 -133
- package/dist/lib/dbWrite.d.ts +1 -1
- package/dist/lib/dearchiveCommand.js +1 -1
- package/dist/lib/docxExtract.js +1 -1
- package/dist/lib/dream.d.ts +8 -0
- package/dist/lib/dream.js +35 -1
- package/dist/lib/dreamLogCommand.js +1 -1
- package/dist/lib/dreamRunLog.d.ts +1 -1
- package/dist/lib/dreamRunLog.js +26 -4
- package/dist/lib/embeddings.js +0 -3
- package/dist/lib/exportProject.d.ts +3 -2
- package/dist/lib/exportProject.js +2 -1
- package/dist/lib/federated.js +1 -1
- package/dist/lib/ftsQuery.d.ts +25 -0
- package/dist/lib/ftsQuery.js +42 -0
- package/dist/lib/hybridSearch.d.ts +9 -0
- package/dist/lib/hybridSearch.js +14 -0
- package/dist/lib/hybridSearchCommand.js +6 -1
- package/dist/lib/importProject.js +2 -1
- package/dist/lib/llm.js +1 -1
- package/dist/lib/lock.d.ts +1 -1
- package/dist/lib/lock.js +5 -3
- package/dist/lib/migrate.js +0 -1
- package/dist/lib/multimodalIngest.js +1 -1
- package/dist/lib/platform.d.ts +0 -6
- package/dist/lib/platform.js +0 -28
- package/dist/lib/readCommand.js +11 -10
- package/dist/lib/remoteWizard.d.ts +1 -1
- package/dist/lib/remoteWizard.js +4 -4
- package/dist/lib/resolver.d.ts +17 -0
- package/dist/lib/resolver.js +43 -17
- package/dist/lib/rulesGen.d.ts +8 -0
- package/dist/lib/rulesGen.js +16 -0
- package/dist/lib/search.d.ts +0 -2
- package/dist/lib/search.js +33 -27
- package/dist/lib/semanticSearchCommand.js +1 -1
- package/dist/lib/setup/sections/providers.js +56 -4
- package/dist/lib/setup/sections/routing.js +42 -5
- package/dist/lib/setup/sections/taskRoutingEditor.d.ts +1 -5
- package/dist/lib/setup/sections/taskRoutingEditor.js +0 -10
- package/dist/lib/setup/ui/header.js +0 -1
- package/dist/lib/setup/ui/status.d.ts +0 -1
- package/dist/lib/setup/ui/status.js +0 -2
- package/dist/lib/setup.d.ts +0 -15
- package/dist/lib/setup.js +13 -158
- package/dist/lib/staleCommand.js +2 -2
- package/dist/lib/syncClient.d.ts +0 -6
- package/dist/lib/syncClient.js +36 -14
- package/dist/lib/syncDoctorCommand.js +2 -2
- package/dist/lib/syncIngest.d.ts +11 -0
- package/dist/lib/syncIngest.js +24 -1
- package/dist/lib/syncIngestStartup.js +2 -2
- package/dist/lib/syncSnapshot.d.ts +2 -0
- package/dist/lib/syncSnapshot.js +4 -0
- package/dist/lib/syncStaging.d.ts +0 -2
- package/dist/lib/syncStaging.js +0 -2
- package/dist/lib/updateCommand.js +1 -1
- package/dist/lib/webBuildCommand.js +1 -1
- package/dist/lib/webIndex.js +0 -1
- package/dist/lib/webIngestCommand.js +1 -1
- package/dist/sandbox/client.js +1 -1
- package/dist/sandbox/manager.js +1 -14
- package/dist/sandbox/server.js +3 -5
- package/package.json +5 -2
package/dist/lib/setup.js
CHANGED
|
@@ -271,16 +271,6 @@ export async function fetchDynamicModels() {
|
|
|
271
271
|
return {};
|
|
272
272
|
}
|
|
273
273
|
}
|
|
274
|
-
/**
|
|
275
|
-
* Get model tiers for a provider — tries dynamic first, falls back to hardcoded.
|
|
276
|
-
*/
|
|
277
|
-
async function getModelTiers(provider) {
|
|
278
|
-
const dynamic = await fetchDynamicModels();
|
|
279
|
-
if (dynamic[provider] && dynamic[provider].length > 0) {
|
|
280
|
-
return dynamic[provider];
|
|
281
|
-
}
|
|
282
|
-
return PROVIDER_TIERS[provider] ?? [];
|
|
283
|
-
}
|
|
284
274
|
// ─── Provider display names and env var mapping ─────────────────────────────
|
|
285
275
|
const PROVIDER_DISPLAY = {
|
|
286
276
|
anthropic: "Anthropic (Claude)",
|
|
@@ -675,7 +665,7 @@ export async function detectIDEs(projectDir) {
|
|
|
675
665
|
*/
|
|
676
666
|
export function upsertGrokMcpBlock(existing, name, entry) {
|
|
677
667
|
// Drop mistaken v5.9.4 `[mcp.<name>]` sections so we don't leave dead config.
|
|
678
|
-
|
|
668
|
+
const content = removeTomlSection(existing, `[mcp.${name}]`);
|
|
679
669
|
const sectionHeader = `[mcp_servers.${name}]`;
|
|
680
670
|
const lines = content.split("\n");
|
|
681
671
|
const headerIdx = lines.findIndex((line) => line.trim() === sectionHeader);
|
|
@@ -779,7 +769,7 @@ export async function setupIDE(ide, projectDir) {
|
|
|
779
769
|
// 1. Strip legacy hand-written sections in ~/.codex/config.toml.
|
|
780
770
|
const userCodexConfig = path.join(os.homedir(), ".codex", "config.toml");
|
|
781
771
|
try {
|
|
782
|
-
|
|
772
|
+
const existing = await fs.readFile(userCodexConfig, "utf-8");
|
|
783
773
|
const cleaned = removeTomlSection(removeTomlSection(existing, "[mcp.gnosys]"), "[gnosys]");
|
|
784
774
|
if (cleaned !== existing) {
|
|
785
775
|
await fs.writeFile(userCodexConfig, cleaned, "utf-8");
|
|
@@ -796,7 +786,7 @@ export async function setupIDE(ide, projectDir) {
|
|
|
796
786
|
let alreadyCorrect = false;
|
|
797
787
|
try {
|
|
798
788
|
const existing = runCli("codex", ["mcp", "get", "gnosys"], { allowFailure: true });
|
|
799
|
-
if (existing
|
|
789
|
+
if (existing?.includes(gnosysCmd) && !existing.includes(" serve")) {
|
|
800
790
|
alreadyCorrect = true;
|
|
801
791
|
}
|
|
802
792
|
else if (existing) {
|
|
@@ -1439,8 +1429,7 @@ export async function runSetup(opts) {
|
|
|
1439
1429
|
const gnomeIdx = hasSecret ? idx++ : -1;
|
|
1440
1430
|
const envIdx = idx++;
|
|
1441
1431
|
const dotenvIdx = idx++;
|
|
1442
|
-
|
|
1443
|
-
// skipIdx is unused as a variable but documents the last index
|
|
1432
|
+
idx++; // skip entry consumes the last index (value itself unused)
|
|
1444
1433
|
if (keyChoice === keychainIdx) {
|
|
1445
1434
|
// macOS Keychain — reuse existing key if available, otherwise ask
|
|
1446
1435
|
console.log();
|
|
@@ -1974,60 +1963,6 @@ export async function runSetup(opts) {
|
|
|
1974
1963
|
throw err;
|
|
1975
1964
|
}
|
|
1976
1965
|
}
|
|
1977
|
-
/**
|
|
1978
|
-
* Update ONLY `llm.defaultProvider` in gnosys.json. Used by the summary
|
|
1979
|
-
* panel row 1 ("provider") so it stops dragging the user into the full
|
|
1980
|
-
* model picker — that's row 2's job.
|
|
1981
|
-
*
|
|
1982
|
-
* v5.9.4 Bug 4 — before this split, both summary rows routed through
|
|
1983
|
-
* `runModelsSetup`, leaving no way to swap provider without also choosing
|
|
1984
|
-
* a new model. Now row 1 picks a provider, row 2 picks a model.
|
|
1985
|
-
*/
|
|
1986
|
-
export async function runProviderOnlySetup(opts = {}) {
|
|
1987
|
-
const projectDir = opts.directory ? path.resolve(opts.directory) : process.cwd();
|
|
1988
|
-
const ownsRl = !opts.rl;
|
|
1989
|
-
const rl = opts.rl ?? createInterface({ input: stdin, output: stdout });
|
|
1990
|
-
try {
|
|
1991
|
-
const { Header } = await import("./setup/ui/header.js");
|
|
1992
|
-
const { Title } = await import("./setup/ui/title.js");
|
|
1993
|
-
const { Spinner } = await import("./setup/ui/spinner.js");
|
|
1994
|
-
const { printStatus } = await import("./setup/ui/status.js");
|
|
1995
|
-
console.log();
|
|
1996
|
-
console.log(Header(["gnosys", "setup", "provider"]));
|
|
1997
|
-
console.log();
|
|
1998
|
-
console.log(Title("Default provider", "pick the LLM provider — model stays as configured"));
|
|
1999
|
-
console.log();
|
|
2000
|
-
const existingConfig = await loadExistingConfig(projectDir);
|
|
2001
|
-
const currentProvider = existingConfig?.llm.defaultProvider;
|
|
2002
|
-
const pricingSpin = Spinner("fetching latest pricing from openrouter…");
|
|
2003
|
-
const fetchStart = Date.now();
|
|
2004
|
-
const dynamicModels = await fetchDynamicModels();
|
|
2005
|
-
const fetchMs = Date.now() - fetchStart;
|
|
2006
|
-
if (Object.keys(dynamicModels).length > 0) {
|
|
2007
|
-
pricingSpin.ok("pricing loaded", `${fetchMs} ms`);
|
|
2008
|
-
}
|
|
2009
|
-
else {
|
|
2010
|
-
pricingSpin.fail("pricing fetch failed", "using bundled tiers");
|
|
2011
|
-
}
|
|
2012
|
-
console.log();
|
|
2013
|
-
const provider = await pickProvider(rl, dynamicModels, "Choose your LLM provider", currentProvider);
|
|
2014
|
-
if (!provider || provider === currentProvider) {
|
|
2015
|
-
printStatus("warn", "no change · provider unchanged");
|
|
2016
|
-
return;
|
|
2017
|
-
}
|
|
2018
|
-
const storePath = ensureActiveStorePath(projectDir);
|
|
2019
|
-
const existingLlm = existingConfig?.llm ?? {};
|
|
2020
|
-
await updateConfig(storePath, {
|
|
2021
|
-
llm: { ...existingLlm, defaultProvider: provider },
|
|
2022
|
-
});
|
|
2023
|
-
printStatus("ok", `default provider · ${provider}`, `${storePath}/gnosys.json`);
|
|
2024
|
-
printStatus("progress", "model unchanged", "use row 2 to swap the model");
|
|
2025
|
-
}
|
|
2026
|
-
finally {
|
|
2027
|
-
if (ownsRl)
|
|
2028
|
-
rl.close();
|
|
2029
|
-
}
|
|
2030
|
-
}
|
|
2031
1966
|
/**
|
|
2032
1967
|
* Build API key requirements from the selected task set only (§3.7).
|
|
2033
1968
|
* One global key per distinct cloud provider in the selection.
|
|
@@ -2142,7 +2077,6 @@ export async function promptKeyDestinationAndPersist(opts) {
|
|
|
2142
2077
|
(await askChoice(opts.rl, "Where should I store this key?", options));
|
|
2143
2078
|
const secureIdx = 0;
|
|
2144
2079
|
const dotenvIdx = 1;
|
|
2145
|
-
const noneIdx = 2;
|
|
2146
2080
|
if (choice === secureIdx) {
|
|
2147
2081
|
if (storeApiKeySecret(opts.service, opts.key, opts.provider)) {
|
|
2148
2082
|
const store = process.platform === "darwin" ? "macOS Keychain" : "GNOME Keyring";
|
|
@@ -2571,84 +2505,6 @@ async function runModelsTaskRoutingSetup(ctx) {
|
|
|
2571
2505
|
await updateConfig(storePath, updatePayload);
|
|
2572
2506
|
printStatus("ok", `saved task routing · ${storePath}/gnosys.json`);
|
|
2573
2507
|
}
|
|
2574
|
-
/**
|
|
2575
|
-
* Lightweight model-management command. Supports three operations:
|
|
2576
|
-
* --list: print available models for the current provider
|
|
2577
|
-
* --refresh: clear the OpenRouter cache and re-fetch
|
|
2578
|
-
* --set X: update the default model in gnosys.json (no prompts)
|
|
2579
|
-
*/
|
|
2580
|
-
async function runModelsCommand(opts = {}) {
|
|
2581
|
-
const projectDir = opts.directory ? path.resolve(opts.directory) : process.cwd();
|
|
2582
|
-
const existingConfig = await loadExistingConfig(projectDir);
|
|
2583
|
-
const currentProvider = existingConfig?.llm.defaultProvider;
|
|
2584
|
-
if (opts.refresh) {
|
|
2585
|
-
const cacheFile = path.join(os.homedir(), ".config", "gnosys", "models-cache.json");
|
|
2586
|
-
try {
|
|
2587
|
-
await fs.unlink(cacheFile);
|
|
2588
|
-
console.log(`${CHECK} Cache cleared.`);
|
|
2589
|
-
}
|
|
2590
|
-
catch {
|
|
2591
|
-
console.log(`${DIM}No cache to clear.${RESET}`);
|
|
2592
|
-
}
|
|
2593
|
-
}
|
|
2594
|
-
if (opts.list) {
|
|
2595
|
-
if (!currentProvider) {
|
|
2596
|
-
console.log(`${WARN} No provider configured. Run 'gnosys setup' first.`);
|
|
2597
|
-
return;
|
|
2598
|
-
}
|
|
2599
|
-
console.log();
|
|
2600
|
-
console.log(`${BOLD}Available models for ${currentProvider}:${RESET}`);
|
|
2601
|
-
console.log();
|
|
2602
|
-
const dynamicModels = await fetchDynamicModels();
|
|
2603
|
-
const tiers = dynamicModels[currentProvider] ?? PROVIDER_TIERS[currentProvider] ?? [];
|
|
2604
|
-
if (tiers.length === 0) {
|
|
2605
|
-
console.log(` ${DIM}No models in catalog. Try '--refresh' or use a custom model name.${RESET}`);
|
|
2606
|
-
return;
|
|
2607
|
-
}
|
|
2608
|
-
for (const t of tiers) {
|
|
2609
|
-
const rec = t.recommended ? ` ${CYAN}<- recommended${RESET}` : "";
|
|
2610
|
-
const price = t.input === 0 && t.output === 0
|
|
2611
|
-
? "free"
|
|
2612
|
-
: `$${t.input.toFixed(2)}–$${t.output.toFixed(2)}/M`;
|
|
2613
|
-
console.log(` ${t.name.padEnd(24)} ${t.model.padEnd(40)} ${DIM}${price}${RESET}${rec}`);
|
|
2614
|
-
}
|
|
2615
|
-
return;
|
|
2616
|
-
}
|
|
2617
|
-
if (opts.set) {
|
|
2618
|
-
if (!currentProvider) {
|
|
2619
|
-
console.log(`${WARN} No provider configured. Run 'gnosys setup' first.`);
|
|
2620
|
-
return;
|
|
2621
|
-
}
|
|
2622
|
-
// v5.9.4 Bug 10 — unified store resolution.
|
|
2623
|
-
const storePath = ensureActiveStorePath(projectDir);
|
|
2624
|
-
const existingProviderConfig = existingConfig?.llm?.[currentProvider];
|
|
2625
|
-
const providerConfigBase = (typeof existingProviderConfig === "object" && existingProviderConfig !== null)
|
|
2626
|
-
? existingProviderConfig
|
|
2627
|
-
: {};
|
|
2628
|
-
await updateConfig(storePath, {
|
|
2629
|
-
llm: {
|
|
2630
|
-
...(existingConfig?.llm ?? {}),
|
|
2631
|
-
defaultProvider: currentProvider,
|
|
2632
|
-
[currentProvider]: { ...providerConfigBase, model: opts.set },
|
|
2633
|
-
},
|
|
2634
|
-
});
|
|
2635
|
-
console.log(`${CHECK} Default model set to ${GREEN}${opts.set}${RESET} for ${currentProvider}.`);
|
|
2636
|
-
return;
|
|
2637
|
-
}
|
|
2638
|
-
// No flags: show current config
|
|
2639
|
-
if (!currentProvider) {
|
|
2640
|
-
console.log(`${WARN} No provider configured. Run 'gnosys setup' first.`);
|
|
2641
|
-
return;
|
|
2642
|
-
}
|
|
2643
|
-
const currentModel = existingConfig
|
|
2644
|
-
? getProviderModel(existingConfig, existingConfig.llm.defaultProvider)
|
|
2645
|
-
: "";
|
|
2646
|
-
console.log();
|
|
2647
|
-
console.log(`Provider: ${GREEN}${currentProvider}${RESET}`);
|
|
2648
|
-
console.log(`Model: ${GREEN}${currentModel}${RESET}`);
|
|
2649
|
-
console.log();
|
|
2650
|
-
console.log(`${DIM}Use '--list' to see options, '--set <model>' to change, '--refresh' to update catalog.${RESET}`);
|
|
2651
|
-
}
|
|
2652
2508
|
/**
|
|
2653
2509
|
* Walks the user through configuring dream mode. Handles:
|
|
2654
2510
|
* - enable/disable
|
|
@@ -2857,23 +2713,23 @@ export async function runDreamSetup(opts = {}) {
|
|
|
2857
2713
|
let discoverRelationships = dDiscover;
|
|
2858
2714
|
if (editChoice === "e") {
|
|
2859
2715
|
const idleAns = await askInput(rl, "idle minutes before triggering", { default: String(dIdle) });
|
|
2860
|
-
idleMinutes = Math.max(1, parseInt(idleAns) || dIdle);
|
|
2716
|
+
idleMinutes = Math.max(1, parseInt(idleAns, 10) || dIdle);
|
|
2861
2717
|
const runtimeAns = await askInput(rl, "max runtime minutes", { default: String(dRuntime) });
|
|
2862
|
-
maxRuntimeMinutes = Math.max(1, parseInt(runtimeAns) || dRuntime);
|
|
2718
|
+
maxRuntimeMinutes = Math.max(1, parseInt(runtimeAns, 10) || dRuntime);
|
|
2863
2719
|
const minMemAns = await askInput(rl, "minimum memories before activating", { default: String(dMinMem) });
|
|
2864
|
-
minMemories = Math.max(1, parseInt(minMemAns) || dMinMem);
|
|
2720
|
+
minMemories = Math.max(1, parseInt(minMemAns, 10) || dMinMem);
|
|
2865
2721
|
const scheduleStartAns = await askInput(rl, "night window start hour (0-23)", { default: String(dScheduleStart) });
|
|
2866
|
-
scheduleStartHour = Math.min(23, Math.max(0, parseInt(scheduleStartAns) || dScheduleStart));
|
|
2722
|
+
scheduleStartHour = Math.min(23, Math.max(0, parseInt(scheduleStartAns, 10) || dScheduleStart));
|
|
2867
2723
|
const scheduleEndAns = await askInput(rl, "night window end hour (0-23)", { default: String(dScheduleEnd) });
|
|
2868
|
-
scheduleEndHour = Math.min(23, Math.max(0, parseInt(scheduleEndAns) || dScheduleEnd));
|
|
2724
|
+
scheduleEndHour = Math.min(23, Math.max(0, parseInt(scheduleEndAns, 10) || dScheduleEnd));
|
|
2869
2725
|
const systemIdleAns = await askInput(rl, "real machine idle minutes required", { default: String(dSystemIdle) });
|
|
2870
|
-
systemIdleMinutes = Math.max(1, parseInt(systemIdleAns) || dSystemIdle);
|
|
2726
|
+
systemIdleMinutes = Math.max(1, parseInt(systemIdleAns, 10) || dSystemIdle);
|
|
2871
2727
|
const minNewAns = await askInput(rl, "minimum changed memories before dreaming", { default: String(dMinNewMemories) });
|
|
2872
|
-
minNewMemoriesToDream = Math.max(0, parseInt(minNewAns) || dMinNewMemories);
|
|
2728
|
+
minNewMemoriesToDream = Math.max(0, parseInt(minNewAns, 10) || dMinNewMemories);
|
|
2873
2729
|
const minHoursAns = await askInput(rl, "minimum hours between successful dreams", { default: String(dMinHours) });
|
|
2874
|
-
minHoursBetweenRuns = Math.max(0, parseInt(minHoursAns) || dMinHours);
|
|
2730
|
+
minHoursBetweenRuns = Math.max(0, parseInt(minHoursAns, 10) || dMinHours);
|
|
2875
2731
|
const maxCallsAns = await askInput(rl, "maximum LLM calls per dream run", { default: String(dMaxCalls) });
|
|
2876
|
-
maxLLMCallsPerRun = Math.max(0, parseInt(maxCallsAns) || dMaxCalls);
|
|
2732
|
+
maxLLMCallsPerRun = Math.max(0, parseInt(maxCallsAns, 10) || dMaxCalls);
|
|
2877
2733
|
selfCritique = await askYesNo(rl, "self-critique (rule + LLM-based review flagging)", dSelfCritique);
|
|
2878
2734
|
generateSummaries = await askYesNo(rl, "generate summaries (LLM)", dGenSummaries);
|
|
2879
2735
|
discoverRelationships = await askYesNo(rl, "discover relationships (LLM)", dDiscover);
|
|
@@ -3009,4 +2865,3 @@ export async function getApiKeyForProvider(provider, opts) {
|
|
|
3009
2865
|
}
|
|
3010
2866
|
return readFirstInChain(provider) ?? "";
|
|
3011
2867
|
}
|
|
3012
|
-
export { getApiKeyForProviderFromConfig } from "./apiKeyVault.js";
|
package/dist/lib/staleCommand.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export async function runStaleCommand(getResolver, opts) {
|
|
2
2
|
const resolver = await getResolver();
|
|
3
|
-
const threshold = parseInt(opts.days);
|
|
3
|
+
const threshold = parseInt(opts.days, 10);
|
|
4
4
|
const cutoff = new Date();
|
|
5
5
|
cutoff.setDate(cutoff.getDate() - threshold);
|
|
6
6
|
const cutoffStr = cutoff.toISOString().split("T")[0];
|
|
@@ -18,7 +18,7 @@ export async function runStaleCommand(getResolver, opts) {
|
|
|
18
18
|
b.frontmatter.modified;
|
|
19
19
|
return (aDate || "").localeCompare(bDate || "");
|
|
20
20
|
})
|
|
21
|
-
.slice(0, parseInt(opts.limit));
|
|
21
|
+
.slice(0, parseInt(opts.limit, 10));
|
|
22
22
|
if (stale.length === 0) {
|
|
23
23
|
console.log(`No memories older than ${threshold} days.`);
|
|
24
24
|
return;
|
package/dist/lib/syncClient.d.ts
CHANGED
|
@@ -33,15 +33,9 @@ export interface V13SyncStatus {
|
|
|
33
33
|
}
|
|
34
34
|
export declare function isMasterReachable(masterPath: string): boolean;
|
|
35
35
|
export declare function countClientWaitingStaging(masterPath: string, machineId: string): number;
|
|
36
|
-
/** Client should hide snapshot reads when master is unreachable (v13 offline rule). */
|
|
37
|
-
export declare function shouldHideSnapshotReads(masterPath: string): boolean;
|
|
38
36
|
export declare function listClientReceipts(masterPath: string, machineId: string): IngestReceipt[];
|
|
39
37
|
export declare function getIngestedUlids(masterPath: string, machineId: string): Set<string>;
|
|
40
38
|
export declare function openClientReadContext(localDb: GnosysDB, masterPath: string, machineId: string): ClientReadContext;
|
|
41
39
|
/** Release snapshot/master DB handles opened by openClientReadContext. */
|
|
42
40
|
export declare function closeClientReadContext(ctx: ClientReadContext): void;
|
|
43
|
-
/**
|
|
44
|
-
* @deprecated Use openClientReadContext — returns only the read DB without overlay or cleanup.
|
|
45
|
-
*/
|
|
46
|
-
export declare function openClientReadDb(localDb: GnosysDB, masterPath: string): GnosysDB;
|
|
47
41
|
export declare function getV13SyncStatus(localDb: GnosysDB): V13SyncStatus;
|
package/dist/lib/syncClient.js
CHANGED
|
@@ -6,7 +6,7 @@ import path from "path";
|
|
|
6
6
|
import { GnosysDB } from "./db.js";
|
|
7
7
|
import { readMachineConfig, getMachineId } from "./machineConfig.js";
|
|
8
8
|
import { getConfiguredRemotePath } from "./remote.js";
|
|
9
|
-
import { clientSnapshotStore, formatSnapshotAge, getClientAcceptedManifest, } from "./syncSnapshot.js";
|
|
9
|
+
import { acceptClientSnapshot, clientSnapshotStore, compareSnapshotVersion, formatSnapshotAge, getClientAcceptedManifest, getMasterManifest, } from "./syncSnapshot.js";
|
|
10
10
|
import { countFailedStagingFiles, machineStagingDir, stagingRoot } from "./syncStaging.js";
|
|
11
11
|
import { renderClientSyncStatusLines, } from "./setup/remoteRender.js";
|
|
12
12
|
const REACHABILITY_TTL_MS = 30_000;
|
|
@@ -53,10 +53,6 @@ export function countClientWaitingStaging(masterPath, machineId) {
|
|
|
53
53
|
return 0;
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
-
/** Client should hide snapshot reads when master is unreachable (v13 offline rule). */
|
|
57
|
-
export function shouldHideSnapshotReads(masterPath) {
|
|
58
|
-
return !isMasterReachable(masterPath);
|
|
59
|
-
}
|
|
60
56
|
export function listClientReceipts(masterPath, machineId) {
|
|
61
57
|
const dir = path.join(stagingRoot(masterPath), machineId, "receipts");
|
|
62
58
|
if (!existsSync(dir))
|
|
@@ -92,7 +88,39 @@ export function openClientReadContext(localDb, masterPath, machineId) {
|
|
|
92
88
|
const reachable = isMasterReachable(masterPath);
|
|
93
89
|
const ingestedUlids = getIngestedUlids(masterPath, machineId);
|
|
94
90
|
const pendingAdds = localDb.listActivePendingAdds().filter((p) => !ingestedUlids.has(p.id));
|
|
91
|
+
const store = clientSnapshotStore(masterPath);
|
|
92
|
+
const snapPath = path.join(store, "gnosys.db");
|
|
95
93
|
if (reachable) {
|
|
94
|
+
// v13 completion (5.12.x): clients must not open the live gnosys.db over
|
|
95
|
+
// the network (concurrent master writes + network FS = torn-page hazard,
|
|
96
|
+
// DESIGN.md "The Simple Rule"). Refresh the verified local snapshot copy
|
|
97
|
+
// when the master has published a newer one, then read the local copy.
|
|
98
|
+
const manifest = getMasterManifest(masterPath);
|
|
99
|
+
if (manifest) {
|
|
100
|
+
const accepted = getClientAcceptedManifest(masterPath);
|
|
101
|
+
if (compareSnapshotVersion(accepted, manifest)) {
|
|
102
|
+
// Best effort — on checksum/copy failure we fall through to whatever
|
|
103
|
+
// local snapshot (or live-DB fallback) is still available.
|
|
104
|
+
acceptClientSnapshot(masterPath, manifest);
|
|
105
|
+
}
|
|
106
|
+
if (existsSync(snapPath)) {
|
|
107
|
+
const snapDb = new GnosysDB(store);
|
|
108
|
+
if (snapDb.isAvailable()) {
|
|
109
|
+
return {
|
|
110
|
+
db: snapDb,
|
|
111
|
+
localDb,
|
|
112
|
+
pendingOverlay: pendingAdds,
|
|
113
|
+
source: "snapshot",
|
|
114
|
+
masterReachable: true,
|
|
115
|
+
ownsReadDb: true,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
snapDb.close();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Compatibility fallback: master has never published a snapshot (older
|
|
122
|
+
// master version, or first sweep still pending) — open the live DB as
|
|
123
|
+
// before. Disappears once the master runs one publish-enabled sweep.
|
|
96
124
|
const masterDb = new GnosysDB(masterPath);
|
|
97
125
|
if (masterDb.isAvailable()) {
|
|
98
126
|
return {
|
|
@@ -106,8 +134,9 @@ export function openClientReadContext(localDb, masterPath, machineId) {
|
|
|
106
134
|
}
|
|
107
135
|
masterDb.close();
|
|
108
136
|
}
|
|
109
|
-
|
|
110
|
-
|
|
137
|
+
// Offline (soft rule, signed off 2026-06-11): the last-accepted snapshot
|
|
138
|
+
// stays readable — immutable + checksummed, and offline clients are
|
|
139
|
+
// add-only, so staleness is the only risk. Status surfaces snapshotAge.
|
|
111
140
|
if (existsSync(snapPath)) {
|
|
112
141
|
const snapDb = new GnosysDB(store);
|
|
113
142
|
if (snapDb.isAvailable()) {
|
|
@@ -137,13 +166,6 @@ export function closeClientReadContext(ctx) {
|
|
|
137
166
|
ctx.db.close();
|
|
138
167
|
}
|
|
139
168
|
}
|
|
140
|
-
/**
|
|
141
|
-
* @deprecated Use openClientReadContext — returns only the read DB without overlay or cleanup.
|
|
142
|
-
*/
|
|
143
|
-
export function openClientReadDb(localDb, masterPath) {
|
|
144
|
-
const ctx = openClientReadContext(localDb, masterPath, getMachineId());
|
|
145
|
-
return ctx.db;
|
|
146
|
-
}
|
|
147
169
|
export function getV13SyncStatus(localDb) {
|
|
148
170
|
const mc = readMachineConfig();
|
|
149
171
|
const masterPath = getConfiguredRemotePath(localDb);
|
|
@@ -2,7 +2,7 @@ import { GnosysDB } from "./db.js";
|
|
|
2
2
|
import { getConfiguredRemotePath } from "./remote.js";
|
|
3
3
|
import { readMachineConfig } from "./machineConfig.js";
|
|
4
4
|
import { getV13SyncStatus } from "./syncClient.js";
|
|
5
|
-
import {
|
|
5
|
+
import { runMasterIngestSweepAndPublish } from "./syncIngest.js";
|
|
6
6
|
import { getSyncIngestTimerStatus } from "./syncIngestTimer.js";
|
|
7
7
|
import { quarantineStaleTmpFiles, stagingRoot } from "./syncStaging.js";
|
|
8
8
|
import { existsSync, readdirSync } from "fs";
|
|
@@ -48,7 +48,7 @@ export async function runSyncDoctorCommand(opts) {
|
|
|
48
48
|
quarantineStaleTmpFiles(masterPath, id);
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
-
ingestResult =
|
|
51
|
+
ingestResult = await runMasterIngestSweepAndPublish(masterPath, {
|
|
52
52
|
quiet: opts.quiet || !!opts.json,
|
|
53
53
|
});
|
|
54
54
|
if (ingestResult.errors.length > 0) {
|
package/dist/lib/syncIngest.d.ts
CHANGED
|
@@ -17,3 +17,14 @@ export interface IngestSweepOptions {
|
|
|
17
17
|
* Always-on cheap ingest sweep (agent start + timer). Single-writer lock on local disk.
|
|
18
18
|
*/
|
|
19
19
|
export declare function runMasterIngestSweep(masterPath: string, opts?: IngestSweepOptions): IngestSweepResult;
|
|
20
|
+
/**
|
|
21
|
+
* v13 completion (5.12.x): sweep, then publish a fresh immutable snapshot
|
|
22
|
+
* when the sweep changed the DB (or none has ever been published). Clients
|
|
23
|
+
* read the published snapshot via a verified local copy instead of opening
|
|
24
|
+
* the live gnosys.db over the network — the hazard the design forbids.
|
|
25
|
+
*
|
|
26
|
+
* Kept separate from runMasterIngestSweep: publishMasterSnapshot acquires
|
|
27
|
+
* the same master-ingest lock, so it must run after the sweep releases it,
|
|
28
|
+
* and the sweep itself stays synchronous for existing callers.
|
|
29
|
+
*/
|
|
30
|
+
export declare function runMasterIngestSweepAndPublish(masterPath: string, opts?: IngestSweepOptions): Promise<IngestSweepResult>;
|
package/dist/lib/syncIngest.js
CHANGED
|
@@ -8,7 +8,7 @@ import { getGnosysHome } from "./paths.js";
|
|
|
8
8
|
import { ensureMachineConfig } from "./machineConfig.js";
|
|
9
9
|
import { listPendingStagingQueue, observeStagingFile, parseStagedFile, quarantineStagingFile, quarantineStaleTmpFiles, stagingRoot, verifyMemoryExistsInDb, } from "./syncStaging.js";
|
|
10
10
|
import { assertMasterLeaseHeld, readMasterMarker, touchMasterMarkerHeartbeat, validateLeaseEpochBeforeWrite, } from "./masterLease.js";
|
|
11
|
-
import { acquireWriteLockSync } from "./syncLock.js";
|
|
11
|
+
import { acquireWriteLockSync, } from "./syncLock.js";
|
|
12
12
|
const INGEST_LOCK_NAME = "master-ingest.lock";
|
|
13
13
|
function payloadToMemory(p, now) {
|
|
14
14
|
const contentHash = fnv1a(`${p.title}\n${p.content}`);
|
|
@@ -150,3 +150,26 @@ export function runMasterIngestSweep(masterPath, opts) {
|
|
|
150
150
|
}
|
|
151
151
|
return result;
|
|
152
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* v13 completion (5.12.x): sweep, then publish a fresh immutable snapshot
|
|
155
|
+
* when the sweep changed the DB (or none has ever been published). Clients
|
|
156
|
+
* read the published snapshot via a verified local copy instead of opening
|
|
157
|
+
* the live gnosys.db over the network — the hazard the design forbids.
|
|
158
|
+
*
|
|
159
|
+
* Kept separate from runMasterIngestSweep: publishMasterSnapshot acquires
|
|
160
|
+
* the same master-ingest lock, so it must run after the sweep releases it,
|
|
161
|
+
* and the sweep itself stays synchronous for existing callers.
|
|
162
|
+
*/
|
|
163
|
+
export async function runMasterIngestSweepAndPublish(masterPath, opts) {
|
|
164
|
+
const result = runMasterIngestSweep(masterPath, opts);
|
|
165
|
+
try {
|
|
166
|
+
const { getMasterManifest, publishMasterSnapshot } = await import("./syncSnapshot.js");
|
|
167
|
+
if (result.ingested > 0 || !getMasterManifest(masterPath)) {
|
|
168
|
+
await publishMasterSnapshot(masterPath);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
result.errors.push(`snapshot publish: ${err instanceof Error ? err.message : String(err)}`);
|
|
173
|
+
}
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { GnosysDB } from "./db.js";
|
|
2
2
|
import { readMachineConfig } from "./machineConfig.js";
|
|
3
3
|
import { getConfiguredRemotePath } from "./remote.js";
|
|
4
|
-
import {
|
|
4
|
+
import { runMasterIngestSweepAndPublish } from "./syncIngest.js";
|
|
5
5
|
/**
|
|
6
6
|
* Run one ingest sweep on MCP server startup (master role only).
|
|
7
7
|
* Non-blocking — callers should fire-and-forget.
|
|
@@ -22,7 +22,7 @@ export async function maybeRunStartupIngestSweep() {
|
|
|
22
22
|
}
|
|
23
23
|
if (!masterPath)
|
|
24
24
|
return;
|
|
25
|
-
const result =
|
|
25
|
+
const result = await runMasterIngestSweepAndPublish(masterPath, { quiet: true });
|
|
26
26
|
if (result.errors.length > 0) {
|
|
27
27
|
console.error(`[sync] Startup ingest sweep: ${result.ingested} ingested, ${result.errors.length} error(s)`);
|
|
28
28
|
}
|
|
@@ -11,6 +11,8 @@ export interface SnapshotManifestFile {
|
|
|
11
11
|
}
|
|
12
12
|
export declare function masterSnapshotsDir(masterPath: string): string;
|
|
13
13
|
export declare function clientSnapshotStore(masterPath: string): string;
|
|
14
|
+
/** Read the master's currently published snapshot manifest (null if none). */
|
|
15
|
+
export declare function getMasterManifest(masterPath: string): SnapshotManifestFile | null;
|
|
14
16
|
/** Publish a new immutable snapshot on the master (re-validates lease epoch first). */
|
|
15
17
|
export declare function publishMasterSnapshot(masterPath: string): Promise<SnapshotManifestFile | null>;
|
|
16
18
|
export declare function compareSnapshotVersion(accepted: {
|
package/dist/lib/syncSnapshot.js
CHANGED
|
@@ -34,6 +34,10 @@ function readManifestFile(masterPath) {
|
|
|
34
34
|
return null;
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
+
/** Read the master's currently published snapshot manifest (null if none). */
|
|
38
|
+
export function getMasterManifest(masterPath) {
|
|
39
|
+
return readManifestFile(masterPath);
|
|
40
|
+
}
|
|
37
41
|
function writeManifestFile(masterPath, manifest) {
|
|
38
42
|
const dir = masterSnapshotsDir(masterPath);
|
|
39
43
|
mkdirSync(dir, { recursive: true });
|
|
@@ -34,8 +34,6 @@ export type StagedFileParseResult = {
|
|
|
34
34
|
};
|
|
35
35
|
export declare function stagingRoot(masterPath: string): string;
|
|
36
36
|
export declare function machineStagingDir(masterPath: string, machineId: string): string;
|
|
37
|
-
/** Alias used by remoteWizard and tests. */
|
|
38
|
-
export declare const stagingDirForMachine: typeof machineStagingDir;
|
|
39
37
|
export declare function clientPresencePath(masterPath: string, machineId: string): string;
|
|
40
38
|
export declare function failedQuarantineDir(masterPath: string, machineId: string): string;
|
|
41
39
|
export declare function buildStagingFileName(memoryUlid: string, unixMs?: number): string;
|
package/dist/lib/syncStaging.js
CHANGED
|
@@ -21,8 +21,6 @@ export function stagingRoot(masterPath) {
|
|
|
21
21
|
export function machineStagingDir(masterPath, machineId) {
|
|
22
22
|
return path.join(stagingRoot(masterPath), machineId);
|
|
23
23
|
}
|
|
24
|
-
/** Alias used by remoteWizard and tests. */
|
|
25
|
-
export const stagingDirForMachine = machineStagingDir;
|
|
26
24
|
export function clientPresencePath(masterPath, machineId) {
|
|
27
25
|
return path.join(machineStagingDir(masterPath, machineId), PRESENCE_FILE);
|
|
28
26
|
}
|
|
@@ -18,7 +18,7 @@ export async function runUpdateCommand(getResolver, memoryPath, opts) {
|
|
|
18
18
|
else {
|
|
19
19
|
const resolver = await getResolver();
|
|
20
20
|
const memory = await resolver.readMemory(memoryPath);
|
|
21
|
-
if (!memory
|
|
21
|
+
if (!memory?.frontmatter.id) {
|
|
22
22
|
console.error(`Memory not found: ${memoryPath}`);
|
|
23
23
|
centralDb?.close();
|
|
24
24
|
process.exit(1);
|
|
@@ -20,7 +20,7 @@ export async function runWebBuildCommand(getWebStorePath, opts) {
|
|
|
20
20
|
categories: webConfig.categories,
|
|
21
21
|
llmEnrich: opts.llm ? webConfig.llmEnrich : false,
|
|
22
22
|
prune: opts.prune || webConfig.prune,
|
|
23
|
-
concurrency: parseInt(opts.concurrency) || webConfig.concurrency,
|
|
23
|
+
concurrency: parseInt(opts.concurrency, 10) || webConfig.concurrency,
|
|
24
24
|
crawlDelayMs: webConfig.crawlDelayMs,
|
|
25
25
|
dryRun: opts.dryRun,
|
|
26
26
|
}, gnosysConfig);
|
package/dist/lib/webIndex.js
CHANGED
|
@@ -115,7 +115,6 @@ export function buildIndexSync(knowledgeDir, options = {}) {
|
|
|
115
115
|
const id = typeof fm.id === "string" ? fm.id : path.basename(filePath, ".md");
|
|
116
116
|
const category = typeof fm.category === "string" ? fm.category : "general";
|
|
117
117
|
const created = typeof fm.created === "string" ? fm.created : null;
|
|
118
|
-
const docIndex = documents.length;
|
|
119
118
|
documents.push({
|
|
120
119
|
id,
|
|
121
120
|
path: relativePath,
|
|
@@ -17,7 +17,7 @@ export async function runWebIngestCommand(getWebStorePath, opts) {
|
|
|
17
17
|
categories: webConfig.categories,
|
|
18
18
|
llmEnrich: opts.llm ? webConfig.llmEnrich : false,
|
|
19
19
|
prune: opts.prune || webConfig.prune,
|
|
20
|
-
concurrency: parseInt(opts.concurrency) || webConfig.concurrency,
|
|
20
|
+
concurrency: parseInt(opts.concurrency, 10) || webConfig.concurrency,
|
|
21
21
|
crawlDelayMs: webConfig.crawlDelayMs,
|
|
22
22
|
dryRun: opts.dryRun,
|
|
23
23
|
verbose: opts.verbose,
|
package/dist/sandbox/client.js
CHANGED
package/dist/sandbox/manager.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Manages the lifecycle of the background sandbox server process.
|
|
5
5
|
* Handles start, stop, status, and auto-start on helper import.
|
|
6
6
|
*/
|
|
7
|
-
import { spawn } from "child_process";
|
|
7
|
+
import { spawn, } from "child_process";
|
|
8
8
|
import fs from "fs";
|
|
9
9
|
import path from "path";
|
|
10
10
|
import { fileURLToPath } from "url";
|
|
@@ -204,16 +204,3 @@ export async function sandboxStatus() {
|
|
|
204
204
|
};
|
|
205
205
|
}
|
|
206
206
|
}
|
|
207
|
-
/**
|
|
208
|
-
* Ensure the sandbox is running (auto-start if needed).
|
|
209
|
-
* Used by the helper library to transparently start the sandbox.
|
|
210
|
-
*/
|
|
211
|
-
async function ensureSandbox(opts) {
|
|
212
|
-
const client = new SandboxClient();
|
|
213
|
-
if (await client.isRunning()) {
|
|
214
|
-
return client;
|
|
215
|
-
}
|
|
216
|
-
// Auto-start
|
|
217
|
-
await startSandbox({ dbPath: opts?.dbPath, wait: true });
|
|
218
|
-
return client;
|
|
219
|
-
}
|
package/dist/sandbox/server.js
CHANGED
|
@@ -13,10 +13,10 @@ import fs from "fs";
|
|
|
13
13
|
import path from "path";
|
|
14
14
|
import { GnosysDB } from "../lib/db.js";
|
|
15
15
|
import { federatedSearch } from "../lib/federated.js";
|
|
16
|
-
import { setPreference, getPreference, getAllPreferences, deletePreference, searchPreferences } from "../lib/preferences.js";
|
|
16
|
+
import { setPreference, getPreference, getAllPreferences, deletePreference, searchPreferences, } from "../lib/preferences.js";
|
|
17
17
|
import { GnosysDreamEngine, DreamScheduler, DEFAULT_DREAM_CONFIG } from "../lib/dream.js";
|
|
18
18
|
import { DEFAULT_CONFIG } from "../lib/config.js";
|
|
19
|
-
import { generateRulesBlock } from "../lib/rulesGen.js";
|
|
19
|
+
import { generateRulesBlock, } from "../lib/rulesGen.js";
|
|
20
20
|
import { getSandboxDir as getSandboxDirImpl } from "../lib/paths.js";
|
|
21
21
|
import { logError, logWarn } from "../lib/log.js";
|
|
22
22
|
// ─── Socket + PID paths ─────────────────────────────────────────────────
|
|
@@ -62,8 +62,6 @@ export function initDreamMode(db, config, dreamConfig) {
|
|
|
62
62
|
const originalStart = scheduler.start.bind(scheduler);
|
|
63
63
|
scheduler.start = () => {
|
|
64
64
|
originalStart();
|
|
65
|
-
// Override the internal check interval to track state
|
|
66
|
-
const CHECK_INTERVAL = 60_000;
|
|
67
65
|
const origCheckIdle = scheduler.checkIdle;
|
|
68
66
|
if (origCheckIdle) {
|
|
69
67
|
scheduler.checkIdle = async () => {
|
|
@@ -552,7 +550,7 @@ export function startServer(dbPath) {
|
|
|
552
550
|
}
|
|
553
551
|
// Open database (with retry for network shares — Dropbox, iCloud, NAS)
|
|
554
552
|
const dbDir = dbPath || GnosysDB.getCentralDbDir();
|
|
555
|
-
const isNetworkPath = dbPath
|
|
553
|
+
const isNetworkPath = !!dbPath;
|
|
556
554
|
const db = new GnosysDB(dbDir, isNetworkPath ? { retries: 5, retryDelayMs: 1000 } : undefined);
|
|
557
555
|
if (!db.isAvailable()) {
|
|
558
556
|
logError(new Error("Failed to open GnosysDB"), { module: "sandbox", op: "openDb", hint: "Install it with: npm install better-sqlite3" });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gnosys",
|
|
3
|
-
"version": "5.12.
|
|
3
|
+
"version": "5.12.3",
|
|
4
4
|
"description": "Gnosys — Persistent Memory for AI Agents. Sandbox-first runtime, central SQLite brain, federated search, Dream Mode, Web Knowledge Base, Obsidian export.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"scripts": {
|
|
22
22
|
"prebuild": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
23
|
-
"build": "tsc",
|
|
23
|
+
"build": "tsc -p tsconfig.build.json",
|
|
24
24
|
"postbuild": "node -e \"const fs=require('fs');for (const f of ['dist/cli.js','dist/index.js']) { try { fs.chmodSync(f,0o755); } catch {} }\"",
|
|
25
25
|
"prebuild:publish": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
26
26
|
"build:publish": "tsc -p tsconfig.publish.json",
|
|
@@ -32,6 +32,8 @@
|
|
|
32
32
|
"test:coverage": "vitest run --coverage",
|
|
33
33
|
"lint": "biome check src/",
|
|
34
34
|
"lint:fix": "biome check --write src/",
|
|
35
|
+
"typecheck": "tsc --noEmit",
|
|
36
|
+
"knip": "knip",
|
|
35
37
|
"docs:mcp-tools": "node scripts/gen-mcp-tools.mjs --write",
|
|
36
38
|
"docs:cli": "node scripts/gen-cli-docs.mjs --write",
|
|
37
39
|
"postinstall": "node dist/postinstall.js || true",
|
|
@@ -66,6 +68,7 @@
|
|
|
66
68
|
"@types/react": "^19.2.14",
|
|
67
69
|
"@types/turndown": "^5.0.6",
|
|
68
70
|
"@vitest/coverage-v8": "^4.1.0",
|
|
71
|
+
"knip": "^6.16.1",
|
|
69
72
|
"tsx": "^4.19.0",
|
|
70
73
|
"typescript": "^5.7.0",
|
|
71
74
|
"vitest": "^4.0.18"
|