gnosys 5.9.0 → 5.9.1

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/index.d.ts CHANGED
@@ -4,5 +4,10 @@
4
4
  * Exposes memory operations as MCP tools that any agent can call.
5
5
  * Supports layered stores: project (auto-discovered), personal, global, optional.
6
6
  */
7
- export {};
7
+ /**
8
+ * Returns once the heavy deps have been loaded and module-level vars
9
+ * (ingestion, hybridSearch, askEngine) are populated. Handlers that
10
+ * need any of these should `await ensureHeavyDeps()` first.
11
+ */
12
+ export declare function ensureHeavyDeps(): Promise<void>;
8
13
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;GAIG"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;GAIG;AAg/GH;;;;GAIG;AACH,wBAAgB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAE/C"}
package/dist/index.js CHANGED
@@ -43,26 +43,17 @@ import { z } from "zod";
43
43
  import fs from "fs/promises";
44
44
  import { GnosysSearch } from "./lib/search.js";
45
45
  import { GnosysTagRegistry } from "./lib/tags.js";
46
- import { performImport, formatImportSummary, estimateDuration } from "./lib/import.js";
47
- import { GnosysIngestion } from "./lib/ingest.js";
48
46
  import { GnosysResolver } from "./lib/resolver.js";
49
47
  import { applyLens } from "./lib/lensing.js";
50
48
  import { getFileHistory, rollbackToCommit, hasGitHistory } from "./lib/history.js";
51
49
  import { groupByPeriod, computeStats } from "./lib/timeline.js";
52
50
  import { buildLinkGraph, getBacklinks, getOutgoingLinks, formatGraphSummary } from "./lib/wikilinks.js";
53
- import { bootstrap } from "./lib/bootstrap.js";
54
51
  import { loadConfig, DEFAULT_CONFIG } from "./lib/config.js";
55
- import { GnosysEmbeddings } from "./lib/embeddings.js";
56
- import { GnosysHybridSearch } from "./lib/hybridSearch.js";
57
- import { GnosysAsk } from "./lib/ask.js";
58
52
  import { getLLMProvider } from "./lib/llm.js";
59
- import { GnosysMaintenanceEngine, formatMaintenanceReport } from "./lib/maintenance.js";
60
53
  import { recall, formatRecall } from "./lib/recall.js";
61
54
  import { initAudit, readAuditLog, formatAuditTimeline } from "./lib/audit.js";
62
55
  import { GnosysDB } from "./lib/db.js";
63
56
  import { syncMemoryToDb, syncUpdateToDb, syncDearchiveToDb, syncReinforcementToDb, auditToDb } from "./lib/dbWrite.js";
64
- import { GnosysDreamEngine, DreamScheduler, formatDreamReport } from "./lib/dream.js";
65
- import { GnosysExporter, formatExportReport } from "./lib/export.js";
66
57
  import { createProjectIdentity, readProjectIdentity } from "./lib/projectIdentity.js";
67
58
  import { setPreference, getPreference, getAllPreferences, deletePreference } from "./lib/preferences.js";
68
59
  import { syncRules, generateRulesBlock } from "./lib/rulesGen.js";
@@ -503,7 +494,11 @@ server.tool("gnosys_add", "Add a new memory. Accepts raw text — an LLM structu
503
494
  isError: true,
504
495
  };
505
496
  }
506
- // Note: ingestion remains module-level since it's heavy and project-agnostic
497
+ // v5.9.1 (#100): ingestion is constructed in the background after
498
+ // server.connect() responds to the MCP handshake. Wait for that to
499
+ // finish before proceeding — first call after a fresh MCP spawn
500
+ // may pause here briefly.
501
+ await ensureHeavyDeps();
507
502
  if (!ingestion) {
508
503
  return {
509
504
  content: [
@@ -796,6 +791,7 @@ server.tool("gnosys_init", "Initialize Gnosys in a project directory. Creates .g
796
791
  search = new GnosysSearch(writeTarget.store.getStorePath());
797
792
  tagRegistry = new GnosysTagRegistry(writeTarget.store.getStorePath());
798
793
  await tagRegistry.load();
794
+ const { GnosysIngestion } = await import("./lib/ingest.js");
799
795
  ingestion = new GnosysIngestion(writeTarget.store, tagRegistry);
800
796
  await reindexAllStores();
801
797
  }
@@ -876,6 +872,7 @@ server.tool("gnosys_migrate", "Migrate a Gnosys store (.gnosys/) from one direct
876
872
  search = new GnosysSearch(writeTarget.store.getStorePath());
877
873
  tagRegistry = new GnosysTagRegistry(writeTarget.store.getStorePath());
878
874
  await tagRegistry.load();
875
+ const { GnosysIngestion } = await import("./lib/ingest.js");
879
876
  ingestion = new GnosysIngestion(writeTarget.store, tagRegistry);
880
877
  await reindexAllStores();
881
878
  }
@@ -1057,6 +1054,11 @@ server.tool("gnosys_commit_context", "Pre-compaction memory sweep. Call this bef
1057
1054
  // happens against `ctx.config` (merged project+global) below; if that
1058
1055
  // can't find a provider, getLLMProvider() surfaces a provider-specific
1059
1056
  // error message.
1057
+ //
1058
+ // v5.9.1 (#100): wait for the background heavy-init to populate
1059
+ // `ingestion` if it hasn't yet (first call after a fresh MCP spawn
1060
+ // may pause here while @huggingface/transformers etc. load).
1061
+ await ensureHeavyDeps();
1060
1062
  if (!ingestion) {
1061
1063
  return {
1062
1064
  content: [{ type: "text", text: "Ingestion module not initialized." }],
@@ -1471,6 +1473,8 @@ server.tool("gnosys_bootstrap", "Batch-import existing documents from a director
1471
1473
  return { content: [{ type: "text", text: "No writable store found." }], isError: true };
1472
1474
  }
1473
1475
  try {
1476
+ // v5.9.1 (#100): bootstrap pulls in heavy file-walking deps — load lazily.
1477
+ const { bootstrap } = await import("./lib/bootstrap.js");
1474
1478
  const result = await bootstrap(writeTarget.store, {
1475
1479
  sourceDir,
1476
1480
  patterns,
@@ -1549,6 +1553,8 @@ server.tool("gnosys_import", "Bulk import structured data (CSV, JSON, JSONL) int
1549
1553
  }
1550
1554
  const effectiveMode = mode || "structured";
1551
1555
  try {
1556
+ // v5.9.1 (#100): import.js pulls mammoth + pdf-parse + turndown.
1557
+ const { performImport, formatImportSummary, estimateDuration } = await import("./lib/import.js");
1552
1558
  const result = await performImport(writeTarget.store, ingestion, {
1553
1559
  format: format,
1554
1560
  data,
@@ -1599,6 +1605,8 @@ server.tool("gnosys_hybrid_search", "Search memories using hybrid keyword + sema
1599
1605
  }, async ({ query, limit, mode, projectRoot }) => {
1600
1606
  // Note: hybridSearch is module-level (heavy) and not scoped per project
1601
1607
  (projectRoot); // quiets unused warning if any
1608
+ // v5.9.1 (#100): wait for the background heavy-init to finish.
1609
+ await ensureHeavyDeps();
1602
1610
  if (!hybridSearch) {
1603
1611
  return {
1604
1612
  content: [{ type: "text", text: "Hybrid search not initialized. No stores found." }],
@@ -1619,6 +1627,8 @@ server.tool("gnosys_hybrid_search", "Search memories using hybrid keyword + sema
1619
1627
  // Use default resolver here since hybridSearch operates across all stores
1620
1628
  const writeTarget = resolver.getWriteTarget();
1621
1629
  if (writeTarget) {
1630
+ // v5.9.1 (#100): lazy-load the maintenance module here too.
1631
+ const { GnosysMaintenanceEngine } = await import("./lib/maintenance.js");
1622
1632
  GnosysMaintenanceEngine.reinforceBatch(writeTarget.store, results.map((r) => r.relativePath)).catch(() => { }); // Fire-and-forget
1623
1633
  }
1624
1634
  const embCount = hybridSearch.embeddingCount();
@@ -1646,6 +1656,7 @@ server.tool("gnosys_semantic_search", "Search memories using semantic similarity
1646
1656
  }, async ({ query, limit, projectRoot }) => {
1647
1657
  // Note: hybridSearch is module-level (heavy) and not scoped per project
1648
1658
  (projectRoot); // quiets unused warning if any
1659
+ await ensureHeavyDeps();
1649
1660
  if (!hybridSearch) {
1650
1661
  return {
1651
1662
  content: [{ type: "text", text: "Search not initialized. No stores found." }],
@@ -1677,6 +1688,7 @@ server.tool("gnosys_semantic_search", "Search memories using semantic similarity
1677
1688
  server.tool("gnosys_reindex", "Rebuild all semantic embeddings from every memory file. Downloads the embedding model (~80 MB) on first run. Required before hybrid/semantic search can be used. Safe to re-run — fully regenerates the index.", { projectRoot: projectRootParam }, async ({ projectRoot }) => {
1678
1689
  // Note: reindex operates on all stores, projectRoot is for API consistency
1679
1690
  (projectRoot); // quiets unused warning if any
1691
+ await ensureHeavyDeps();
1680
1692
  if (!hybridSearch) {
1681
1693
  return {
1682
1694
  content: [{ type: "text", text: "No stores found. Initialize a store with gnosys_init first." }],
@@ -1712,6 +1724,7 @@ server.tool("gnosys_ask", "Ask a natural-language question and get a synthesized
1712
1724
  }, async ({ question, limit, mode, projectRoot }) => {
1713
1725
  // Note: askEngine is module-level (heavy) and not scoped per project
1714
1726
  (projectRoot); // quiets unused warning if any
1727
+ await ensureHeavyDeps();
1715
1728
  if (!askEngine) {
1716
1729
  return {
1717
1730
  content: [{ type: "text", text: "Ask engine not initialized. Ensure stores exist and an LLM provider is configured." }],
@@ -1726,6 +1739,8 @@ server.tool("gnosys_ask", "Ask a natural-language question and get a synthesized
1726
1739
  // Reinforce used memories (best-effort, non-blocking)
1727
1740
  const writeTarget = resolver.getWriteTarget();
1728
1741
  if (writeTarget && result.sources.length > 0) {
1742
+ // v5.9.1 (#100): lazy-load the maintenance module here too.
1743
+ const { GnosysMaintenanceEngine } = await import("./lib/maintenance.js");
1729
1744
  GnosysMaintenanceEngine.reinforceBatch(writeTarget.store, result.sources.map((s) => s.relativePath)).catch(() => { }); // Fire-and-forget
1730
1745
  }
1731
1746
  const sourcesText = result.sources.length > 0
@@ -1765,6 +1780,9 @@ server.tool("gnosys_maintain", "Run vault maintenance: detect duplicate memories
1765
1780
  }, async ({ dryRun, autoApply, projectRoot }) => {
1766
1781
  const ctx = await resolveToolContext(projectRoot);
1767
1782
  try {
1783
+ // v5.9.1 (#100): maintenance engine pulls LLM machinery for the
1784
+ // discoverRelationships / generateSummaries phases — load lazily.
1785
+ const { GnosysMaintenanceEngine, formatMaintenanceReport } = await import("./lib/maintenance.js");
1768
1786
  const engine = new GnosysMaintenanceEngine(ctx.resolver, ctx.config);
1769
1787
  const report = await engine.maintain({
1770
1788
  dryRun: dryRun ?? true,
@@ -1893,6 +1911,8 @@ server.tool("gnosys_dream", "Run a Dream Mode cycle — idle-time consolidation
1893
1911
  provider: ctx.config?.dream?.provider || "ollama",
1894
1912
  model: ctx.config?.dream?.model,
1895
1913
  };
1914
+ // v5.9.1 (#100): dream engine pulls LLM provider machinery — load lazily.
1915
+ const { GnosysDreamEngine, formatDreamReport } = await import("./lib/dream.js");
1896
1916
  const engine = new GnosysDreamEngine(ctx.centralDb, ctx.config || DEFAULT_CONFIG, dreamConfig);
1897
1917
  const report = await engine.dream((phase, detail) => {
1898
1918
  console.error(`[dream:${phase}] ${detail}`);
@@ -1927,6 +1947,8 @@ server.tool("gnosys_export", "Export gnosys.db to Obsidian-compatible vault —
1927
1947
  ],
1928
1948
  };
1929
1949
  }
1950
+ // v5.9.1 (#100): exporter pulls obsidian-flavored markdown gen — lazy.
1951
+ const { GnosysExporter, formatExportReport } = await import("./lib/export.js");
1930
1952
  const exporter = new GnosysExporter(ctx.centralDb);
1931
1953
  const report = await exporter.export({
1932
1954
  targetDir: params.targetDir,
@@ -2780,6 +2802,113 @@ This marks the conversation checkpoint so the next /gnosys-memorize only process
2780
2802
  ],
2781
2803
  };
2782
2804
  });
2805
+ // ─── Heavy module initialization (deferred) ───────────────────────────────
2806
+ //
2807
+ // v5.9.1 (#100). Constructs GnosysIngestion / GnosysEmbeddings /
2808
+ // GnosysHybridSearch / GnosysAsk / GnosysDreamEngine. These modules pull
2809
+ // in @huggingface/transformers (80MB), mammoth, pdf-parse, turndown, and
2810
+ // LLM provider SDKs — together ~24s of import time on cold disk. By
2811
+ // running them AFTER server.connect() responds to the MCP handshake, we
2812
+ // avoid the client-side timeout (Grok Build = 10s default).
2813
+ let heavyDepsReadyResolve = null;
2814
+ const heavyDepsReady = new Promise((resolve) => {
2815
+ heavyDepsReadyResolve = resolve;
2816
+ });
2817
+ /**
2818
+ * Returns once the heavy deps have been loaded and module-level vars
2819
+ * (ingestion, hybridSearch, askEngine) are populated. Handlers that
2820
+ * need any of these should `await ensureHeavyDeps()` first.
2821
+ */
2822
+ export function ensureHeavyDeps() {
2823
+ return heavyDepsReady;
2824
+ }
2825
+ async function initHeavyDeps() {
2826
+ const writeTarget = resolver.getWriteTarget();
2827
+ if (!writeTarget || !tagRegistry || !search) {
2828
+ heavyDepsReadyResolve?.();
2829
+ return;
2830
+ }
2831
+ // Ingestion (used by gnosys_add, gnosys_commit_context).
2832
+ const { GnosysIngestion } = await import("./lib/ingest.js");
2833
+ ingestion = new GnosysIngestion(writeTarget.store, tagRegistry, config);
2834
+ console.error(`LLM ingestion: ${ingestion.isLLMAvailable ? `enabled (${ingestion.providerName})` : "disabled (configure LLM provider)"}`);
2835
+ // Hybrid search + ask (used by gnosys_hybrid_search / gnosys_ask).
2836
+ const { GnosysEmbeddings } = await import("./lib/embeddings.js");
2837
+ const { GnosysHybridSearch } = await import("./lib/hybridSearch.js");
2838
+ const { GnosysAsk } = await import("./lib/ask.js");
2839
+ const embeddings = new GnosysEmbeddings(writeTarget.store.getStorePath());
2840
+ hybridSearch = new GnosysHybridSearch(search, embeddings, resolver, writeTarget.store.getStorePath(), gnosysDb || undefined);
2841
+ askEngine = new GnosysAsk(hybridSearch, config, resolver, writeTarget.store.getStorePath());
2842
+ const embCount = embeddings.hasEmbeddings() ? embeddings.count() : 0;
2843
+ console.error(`Hybrid search: ${embCount > 0 ? `ready (${embCount} embeddings)` : "available (run gnosys_reindex to build embeddings)"}`);
2844
+ console.error(`Ask engine: ${askEngine.isLLMAvailable ? `ready (${askEngine.providerName}/${askEngine.modelName})` : "disabled (configure LLM provider)"}`);
2845
+ // Dream mode (only constructed if enabled; designation gate inside start()).
2846
+ if (gnosysDb && config.dream?.enabled) {
2847
+ const { GnosysDreamEngine, DreamScheduler } = await import("./lib/dream.js");
2848
+ const dreamEngine = new GnosysDreamEngine(gnosysDb, config, config.dream);
2849
+ dreamScheduler = new DreamScheduler(dreamEngine, config.dream);
2850
+ // Layer 3: probe the dream provider if this machine is the dream node.
2851
+ try {
2852
+ const designated = gnosysDb.getDreamMachineId();
2853
+ const localId = gnosysDb.getMeta("machine_id");
2854
+ if (designated && designated === localId) {
2855
+ const dreamProvider = config.dream.provider || "ollama";
2856
+ const dreamModel = config.dream.model || "(default)";
2857
+ const { validateModel } = await import("./lib/modelValidation.js");
2858
+ const envVarName = `GNOSYS_${dreamProvider.toUpperCase()}_KEY`;
2859
+ let apiKey = process.env[envVarName] || "";
2860
+ if (!apiKey && process.platform === "darwin" && dreamProvider !== "ollama" && dreamProvider !== "lmstudio") {
2861
+ try {
2862
+ const { execSync } = await import("child_process");
2863
+ apiKey = execSync(`security find-generic-password -a "$USER" -s "${envVarName}" -w 2>/dev/null`, {
2864
+ stdio: "pipe", encoding: "utf-8", timeout: 2000,
2865
+ }).trim();
2866
+ }
2867
+ catch {
2868
+ // No key in keychain
2869
+ }
2870
+ }
2871
+ if (!apiKey && dreamProvider !== "ollama" && dreamProvider !== "lmstudio") {
2872
+ process.stderr.write(`gnosys: dream provider '${dreamProvider}/${dreamModel}' has no API key configured.\n` +
2873
+ ` This machine is designated to dream, but the LLM cannot be called.\n` +
2874
+ ` Run 'gnosys setup dream' to reconfigure.\n`);
2875
+ }
2876
+ else {
2877
+ const result = await Promise.race([
2878
+ validateModel(dreamProvider, dreamModel, apiKey),
2879
+ new Promise((resolve) => setTimeout(() => resolve({ ok: false, error: "probe timeout (5s)" }), 5000)),
2880
+ ]);
2881
+ if (!result.ok) {
2882
+ process.stderr.write(`gnosys: dream provider '${dreamProvider}/${dreamModel}' is unreachable at startup.\n` +
2883
+ ` This machine is designated to dream, but the LLM cannot be called.\n` +
2884
+ ` Error: ${("error" in result && result.error) || "unknown"}\n` +
2885
+ ` Run 'gnosys setup dream' to reconfigure.\n`);
2886
+ }
2887
+ }
2888
+ }
2889
+ }
2890
+ catch {
2891
+ // Probe failed — non-fatal. Continue with scheduler start.
2892
+ }
2893
+ dreamScheduler.start();
2894
+ const designated = gnosysDb.getDreamMachineId();
2895
+ const localId = gnosysDb.getMeta("machine_id");
2896
+ if (!designated) {
2897
+ console.error(`Dream Mode: enabled but no machine designated. Run 'gnosys setup dream' on the machine you want to host dreams.`);
2898
+ }
2899
+ else if (designated !== localId) {
2900
+ console.error(`Dream Mode: enabled — designated to '${designated}'. This machine (${localId || "?"}) will not dream.`);
2901
+ }
2902
+ else {
2903
+ console.error(`Dream Mode: enabled on this machine (idle ${config.dream.idleMinutes}min, max ${config.dream.maxRuntimeMinutes}min)`);
2904
+ }
2905
+ }
2906
+ else {
2907
+ console.error(`Dream Mode: disabled (run 'gnosys setup dream' to configure)`);
2908
+ }
2909
+ console.error("Gnosys MCP: heavy modules ready");
2910
+ heavyDepsReadyResolve?.();
2911
+ }
2783
2912
  // ─── Start the server ────────────────────────────────────────────────────
2784
2913
  async function main() {
2785
2914
  // v5.7.1 (#15): start the upgrade-marker watcher BEFORE anything else.
@@ -2810,7 +2939,12 @@ async function main() {
2810
2939
  console.error("Gnosys MCP server starting.");
2811
2940
  console.error("Active stores:");
2812
2941
  console.error(resolver.getSummary());
2813
- // Initialize search from the first writable store
2942
+ // Initialize search from the first writable store. Everything in this
2943
+ // block is FAST — opening the search index + tag registry + loading
2944
+ // gnosys.json. The slow stuff (LLM providers, transformers embeddings,
2945
+ // pdf/docx parsers) is deferred to `initHeavyDeps()` below so the MCP
2946
+ // server can answer the `initialize` handshake within the client's
2947
+ // timeout (Grok Build is 10s, Claude Code is ~15s).
2814
2948
  const writeTarget = resolver.getWriteTarget();
2815
2949
  if (writeTarget) {
2816
2950
  search = new GnosysSearch(writeTarget.store.getStorePath());
@@ -2823,7 +2957,6 @@ async function main() {
2823
2957
  catch (err) {
2824
2958
  console.error(`Warning: Failed to load gnosys.json: ${err instanceof Error ? err.message : err}`);
2825
2959
  }
2826
- ingestion = new GnosysIngestion(writeTarget.store, tagRegistry, config);
2827
2960
  // Initialize audit logging
2828
2961
  initAudit(writeTarget.store.getStorePath());
2829
2962
  // Build search index across all stores
@@ -2831,91 +2964,22 @@ async function main() {
2831
2964
  // v5.2: gnosysDb now points to the central DB (sole source of truth).
2832
2965
  // No local project DB is created or opened.
2833
2966
  gnosysDb = centralDb;
2834
- // Initialize hybrid search + ask engine (embeddings loaded lazily)
2835
- const embeddings = new GnosysEmbeddings(writeTarget.store.getStorePath());
2836
- hybridSearch = new GnosysHybridSearch(search, embeddings, resolver, writeTarget.store.getStorePath(), gnosysDb || undefined);
2837
- askEngine = new GnosysAsk(hybridSearch, config, resolver, writeTarget.store.getStorePath());
2838
- const embCount = embeddings.hasEmbeddings() ? embeddings.count() : 0;
2839
- console.error(`LLM ingestion: ${ingestion.isLLMAvailable ? `enabled (${ingestion.providerName})` : "disabled (configure LLM provider)"}`);
2840
- console.error(`Hybrid search: ${embCount > 0 ? `ready (${embCount} embeddings)` : "available (run gnosys_reindex to build embeddings)"}`);
2841
- console.error(`Ask engine: ${askEngine.isLLMAvailable ? `ready (${askEngine.providerName}/${askEngine.modelName})` : "disabled (configure LLM provider)"}`);
2842
- // v2.0: Initialize Dream Mode (idle-time consolidation)
2843
- // v5.4.2: Designation gate inside DreamScheduler.start() — only the
2844
- // machine designated via `gnosys setup dream` arms the timer. Other
2845
- // machines no-op silently. Layer 3 startup probe surfaces a stderr
2846
- // warning if this machine is designated but the dream provider is
2847
- // unreachable.
2848
- if (gnosysDb && config.dream?.enabled) {
2849
- const dreamEngine = new GnosysDreamEngine(gnosysDb, config, config.dream);
2850
- dreamScheduler = new DreamScheduler(dreamEngine, config.dream);
2851
- // Layer 3: probe the dream provider if this machine is the dream node.
2852
- // Done before scheduler.start() so users see the warning immediately
2853
- // alongside other startup output.
2854
- try {
2855
- const designated = gnosysDb.getDreamMachineId();
2856
- const localId = gnosysDb.getMeta("machine_id");
2857
- if (designated && designated === localId) {
2858
- // Quick reachability probe — 5s timeout to avoid blocking startup.
2859
- const dreamProvider = config.dream.provider || "ollama";
2860
- const dreamModel = config.dream.model || "(default)";
2861
- const { validateModel } = await import("./lib/modelValidation.js");
2862
- // Resolve API key from env or keychain (mirroring resolveApiKey precedence).
2863
- const envVarName = `GNOSYS_${dreamProvider.toUpperCase()}_KEY`;
2864
- let apiKey = process.env[envVarName] || "";
2865
- if (!apiKey && process.platform === "darwin" && dreamProvider !== "ollama" && dreamProvider !== "lmstudio") {
2866
- try {
2867
- const { execSync } = await import("child_process");
2868
- apiKey = execSync(`security find-generic-password -a "$USER" -s "${envVarName}" -w 2>/dev/null`, {
2869
- stdio: "pipe", encoding: "utf-8", timeout: 2000,
2870
- }).trim();
2871
- }
2872
- catch {
2873
- // No key in keychain
2874
- }
2875
- }
2876
- // Skip the network probe if there's clearly no key configured for a
2877
- // remote provider — surface the obvious config gap instead.
2878
- if (!apiKey && dreamProvider !== "ollama" && dreamProvider !== "lmstudio") {
2879
- process.stderr.write(`gnosys: dream provider '${dreamProvider}/${dreamModel}' has no API key configured.\n` +
2880
- ` This machine is designated to dream, but the LLM cannot be called.\n` +
2881
- ` Run 'gnosys setup dream' to reconfigure.\n`);
2882
- }
2883
- else {
2884
- const result = await Promise.race([
2885
- validateModel(dreamProvider, dreamModel, apiKey),
2886
- new Promise((resolve) => setTimeout(() => resolve({ ok: false, error: "probe timeout (5s)" }), 5000)),
2887
- ]);
2888
- if (!result.ok) {
2889
- process.stderr.write(`gnosys: dream provider '${dreamProvider}/${dreamModel}' is unreachable at startup.\n` +
2890
- ` This machine is designated to dream, but the LLM cannot be called.\n` +
2891
- ` Error: ${("error" in result && result.error) || "unknown"}\n` +
2892
- ` Run 'gnosys setup dream' to reconfigure.\n`);
2893
- }
2894
- }
2895
- }
2896
- }
2897
- catch {
2898
- // Probe failed — non-fatal. Continue with scheduler start.
2899
- }
2900
- dreamScheduler.start();
2901
- const designated = gnosysDb.getDreamMachineId();
2902
- const localId = gnosysDb.getMeta("machine_id");
2903
- if (!designated) {
2904
- console.error(`Dream Mode: enabled but no machine designated. Run 'gnosys setup dream' on the machine you want to host dreams.`);
2905
- }
2906
- else if (designated !== localId) {
2907
- console.error(`Dream Mode: enabled — designated to '${designated}'. This machine (${localId || "?"}) will not dream.`);
2908
- }
2909
- else {
2910
- console.error(`Dream Mode: enabled on this machine (idle ${config.dream.idleMinutes}min, max ${config.dream.maxRuntimeMinutes}min)`);
2911
- }
2912
- }
2913
- else {
2914
- console.error(`Dream Mode: disabled (run 'gnosys setup dream' to configure)`);
2915
- }
2967
+ console.error("Gnosys MCP: light init complete; LLM/embeddings/dream loading in background…");
2916
2968
  }
2969
+ // v5.9.1 (#100): connect EARLY so the MCP `initialize` handshake responds
2970
+ // before we incur the ~24s heavy-import cost. After connect, kick off
2971
+ // heavy module initialization in the background. Handlers that use the
2972
+ // module-level `ingestion` / `hybridSearch` / `askEngine` vars guard
2973
+ // against null and either await readiness or surface a clear error.
2917
2974
  const transport = new StdioServerTransport();
2918
2975
  await server.connect(transport);
2976
+ console.error("Gnosys MCP: handshake ready (heavy modules still loading)");
2977
+ // Kick off heavy deps; fire-and-forget. Errors here are non-fatal —
2978
+ // they're logged to stderr and the affected handlers will report the
2979
+ // failure on first use.
2980
+ void initHeavyDeps().catch((err) => {
2981
+ console.error(`Gnosys MCP: heavy-init failed — ${err instanceof Error ? err.message : err}`);
2982
+ });
2919
2983
  // ─── MCP Roots Support (multi-project awareness) ───────────────────────
2920
2984
  // After connecting, request workspace roots from the host. This lets us
2921
2985
  // discover .gnosys stores in all open projects, not just the cwd.