@bcmyguest/openmem 0.0.0-stage → 0.6.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.
Files changed (191) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +136 -2
  3. package/bin/hindsight-project-index +530 -0
  4. package/cli/coding-agent/access-policy.mjs +38 -0
  5. package/cli/coding-agent/agents.mjs +34 -0
  6. package/cli/coding-agent/bank-from-cwd.mjs +140 -0
  7. package/cli/coding-agent/capabilities.mjs +23 -0
  8. package/cli/coding-agent/hook-harnesses.mjs +87 -0
  9. package/cli/coding-agent/hook-runner.mjs +260 -0
  10. package/cli/coding-agent/install-access.mjs +93 -0
  11. package/cli/coding-agent/install-shim.mjs +97 -0
  12. package/cli/coding-agent/opencode-config.mjs +91 -0
  13. package/cli/coding-agent/opencode-plugin.mjs +96 -0
  14. package/cli/coding-agent/runtime.mjs +39 -0
  15. package/cli/coding-agent/session-facade.mjs +62 -0
  16. package/cli/cognee-mcp.mjs +260 -0
  17. package/cli/feed-sync.mjs +11 -0
  18. package/cli/install-coding-agent.mjs +165 -0
  19. package/cli/install-memory-mcp.mjs +426 -0
  20. package/cli/is-main.mjs +18 -0
  21. package/cli/mcp/context.mjs +61 -0
  22. package/cli/mcp/export/cognee-dataset-name.mjs +12 -0
  23. package/cli/mcp/export/cognee.mjs +119 -0
  24. package/cli/mcp/export/hindsight.mjs +48 -0
  25. package/cli/mcp/export/index.mjs +26 -0
  26. package/cli/mcp/export/mem0.mjs +31 -0
  27. package/cli/mcp/llm-synthesize.mjs +50 -0
  28. package/cli/mcp/mem0-scope-id.mjs +22 -0
  29. package/cli/mcp/memory-facade.mjs +455 -0
  30. package/cli/mcp/memory-reflect-consolidate.mjs +120 -0
  31. package/cli/mcp/notebooklm-banks.mjs +21 -0
  32. package/cli/mcp/notebooklm-hindsight-rest.mjs +45 -0
  33. package/cli/mcp/notebooklm-provider.mjs +225 -0
  34. package/cli/mcp/pmm-records.mjs +23 -0
  35. package/cli/mcp/portability.mjs +158 -0
  36. package/cli/mcp/stdio.mjs +65 -0
  37. package/cli/mcp/unified-mcp-install.mjs +65 -0
  38. package/cli/mem0-mcp.mjs +108 -0
  39. package/cli/notebooklm-mcp.mjs +236 -0
  40. package/cli/openmem-mcp.mjs +35 -0
  41. package/cli/openmem.mjs +205 -0
  42. package/cli/roots.mjs +85 -0
  43. package/cli/stack.mjs +436 -0
  44. package/cli/switch-memory-provider.mjs +358 -0
  45. package/cli/wiring-report.mjs +125 -0
  46. package/compose.bench.yaml +12 -0
  47. package/compose.yaml +164 -0
  48. package/config/coding-agent.json +13 -0
  49. package/config/hindsight.env.example +24 -0
  50. package/config/index.yml +54 -0
  51. package/config/litellm.lemond.yaml +40 -0
  52. package/config/litellm.ollama.yaml +33 -0
  53. package/config/notebooklm.yml +33 -0
  54. package/config/openmem.toml.example +62 -0
  55. package/docs/OPENMEM.md +23 -0
  56. package/docs/architecture.md +219 -0
  57. package/docs/assets/README.md +10 -0
  58. package/docs/assets/setup-demo.gif +0 -0
  59. package/docs/coding-agent-hooks.md +51 -0
  60. package/docs/coding-agent-scopes.md +22 -0
  61. package/docs/coding-agents.md +51 -0
  62. package/docs/cognee.md +37 -0
  63. package/docs/concepts.md +112 -0
  64. package/docs/hindsight.md +74 -0
  65. package/docs/index-sources-roadmap.md +38 -0
  66. package/docs/index-sources.md +39 -0
  67. package/docs/indexer-sinks.md +122 -0
  68. package/docs/indexing.md +57 -0
  69. package/docs/manual-stack-setup.md +74 -0
  70. package/docs/mcp-install-guide.md +4 -0
  71. package/docs/mcp-setup.md +101 -0
  72. package/docs/mem0.md +103 -0
  73. package/docs/memory-portability.md +73 -0
  74. package/docs/memory-providers.md +246 -0
  75. package/docs/model-configuration.md +84 -0
  76. package/docs/notebooklm.md +83 -0
  77. package/docs/npx-install.md +170 -0
  78. package/docs/openmem-config.md +194 -0
  79. package/docs/provider-wizard-hooks.md +222 -0
  80. package/docs/quickstart.md +101 -0
  81. package/docs/setup-wizard.md +116 -0
  82. package/docs/sources-feeds.md +52 -0
  83. package/docs/sources-readwise.md +23 -0
  84. package/docs/sources-zotero.md +22 -0
  85. package/glama.json +4 -0
  86. package/indexer/Dockerfile +38 -0
  87. package/indexer/code_outline.py +146 -0
  88. package/indexer/feed_sync.py +107 -0
  89. package/indexer/index_config.py +290 -0
  90. package/indexer/pytest.ini +5 -0
  91. package/indexer/sinks/__init__.py +148 -0
  92. package/indexer/sinks/_http.py +44 -0
  93. package/indexer/sinks/cognee.py +329 -0
  94. package/indexer/sinks/hindsight.py +107 -0
  95. package/indexer/sinks/mem0.py +247 -0
  96. package/indexer/watch.py +240 -0
  97. package/package.json +95 -4
  98. package/providers/_template/compose.yaml +52 -0
  99. package/providers/cognee/OPERATOR.md +82 -0
  100. package/providers/cognee/compose.yaml +89 -0
  101. package/providers/cognee/lib/clack.mjs +157 -0
  102. package/providers/cognee/lib/coding-agent.mjs +26 -0
  103. package/providers/cognee/lib/cognee-mcp-install.mjs +153 -0
  104. package/providers/cognee/lib/config.mjs +98 -0
  105. package/providers/cognee/lib/env-file.mjs +194 -0
  106. package/providers/cognee/lib/env-setup.mjs +83 -0
  107. package/providers/cognee/lib/llm-env.mjs +382 -0
  108. package/providers/cognee/lib/ports.mjs +25 -0
  109. package/providers/cognee/lib/smoke.mjs +246 -0
  110. package/providers/cognee/lib/stack-env.mjs +134 -0
  111. package/providers/cognee/lib/state.mjs +101 -0
  112. package/providers/cognee/lib/summary.mjs +56 -0
  113. package/providers/cognee/lib/validate.mjs +97 -0
  114. package/providers/cognee/manifest.json +56 -0
  115. package/providers/cognee/wizard.mjs +134 -0
  116. package/providers/graphiti/manifest.json +15 -0
  117. package/providers/hindsight/lib/clack.mjs +245 -0
  118. package/providers/hindsight/lib/coding-agent.mjs +204 -0
  119. package/providers/hindsight/lib/config.mjs +73 -0
  120. package/providers/hindsight/lib/env-setup.mjs +71 -0
  121. package/providers/hindsight/lib/litellm-routes.mjs +187 -0
  122. package/providers/hindsight/lib/model-backend.mjs +214 -0
  123. package/providers/hindsight/lib/notebooklm-mcp-install.mjs +127 -0
  124. package/providers/hindsight/lib/ollama-compose.mjs +51 -0
  125. package/providers/hindsight/lib/ports.mjs +30 -0
  126. package/providers/hindsight/lib/probe.mjs +33 -0
  127. package/providers/hindsight/lib/stack-env.mjs +132 -0
  128. package/providers/hindsight/lib/state.mjs +75 -0
  129. package/providers/hindsight/lib/summary.mjs +43 -0
  130. package/providers/hindsight/lib/validate.mjs +75 -0
  131. package/providers/hindsight/manifest.json +58 -0
  132. package/providers/hindsight/wizard.mjs +149 -0
  133. package/providers/letta/manifest.json +29 -0
  134. package/providers/mem0/OPERATOR.md +124 -0
  135. package/providers/mem0/compose.yaml +57 -0
  136. package/providers/mem0/lib/coding-agent.mjs +58 -0
  137. package/providers/mem0/lib/config.mjs +45 -0
  138. package/providers/mem0/lib/configure.mjs +135 -0
  139. package/providers/mem0/lib/dotenv.mjs +71 -0
  140. package/providers/mem0/lib/env-file.mjs +110 -0
  141. package/providers/mem0/lib/http.mjs +69 -0
  142. package/providers/mem0/lib/mcp-install.mjs +165 -0
  143. package/providers/mem0/lib/secret.mjs +30 -0
  144. package/providers/mem0/lib/smoke.mjs +179 -0
  145. package/providers/mem0/lib/stack-env.mjs +216 -0
  146. package/providers/mem0/lib/state.mjs +67 -0
  147. package/providers/mem0/lib/summary.mjs +60 -0
  148. package/providers/mem0/lib/url.mjs +106 -0
  149. package/providers/mem0/lib/validate.mjs +158 -0
  150. package/providers/mem0/manifest.json +47 -0
  151. package/providers/mem0/wizard.mjs +133 -0
  152. package/providers/zep/manifest.json +29 -0
  153. package/scripts/check-pack.mjs +218 -0
  154. package/scripts/gen-masks.mjs +83 -0
  155. package/scripts/gen-masks.sh +4 -0
  156. package/scripts/promotion-provider-smoke.mjs +34 -0
  157. package/scripts/run-setup.sh +30 -0
  158. package/scripts/setup.mjs +94 -0
  159. package/scripts/setup.sh +5 -0
  160. package/scripts/spin-up.sh +45 -0
  161. package/scripts/wizard.sh +5 -0
  162. package/server.json +34 -0
  163. package/skills/openmem/SKILL.md +29 -0
  164. package/sync/Dockerfile +27 -0
  165. package/sync/memory_backend.py +87 -0
  166. package/sync/notebooklm_sync.py +664 -0
  167. package/wizard/bin/pm.mjs +97 -0
  168. package/wizard/bin/setup.mjs +33 -0
  169. package/wizard/lib/compose-version.mjs +32 -0
  170. package/wizard/lib/compose.mjs +70 -0
  171. package/wizard/lib/curl.mjs +103 -0
  172. package/wizard/lib/docker-build.mjs +148 -0
  173. package/wizard/lib/exec.mjs +41 -0
  174. package/wizard/lib/indexer.mjs +6 -0
  175. package/wizard/lib/load-machine-env.mjs +4 -0
  176. package/wizard/lib/log.mjs +32 -0
  177. package/wizard/lib/notebooklm-agent-access.mjs +92 -0
  178. package/wizard/lib/notebooklm.mjs +214 -0
  179. package/wizard/lib/parse-args.mjs +87 -0
  180. package/wizard/lib/paths.mjs +39 -0
  181. package/wizard/lib/pm-config.mjs +163 -0
  182. package/wizard/lib/ports.mjs +94 -0
  183. package/wizard/lib/prompts.mjs +57 -0
  184. package/wizard/lib/provider-wizard/helpers.mjs +98 -0
  185. package/wizard/lib/provider-wizard/run-steps.mjs +129 -0
  186. package/wizard/lib/providers/registry.mjs +262 -0
  187. package/wizard/lib/run-clack.mjs +108 -0
  188. package/wizard/lib/run-wizard.mjs +755 -0
  189. package/wizard/lib/validate-setup.mjs +62 -0
  190. package/wizard/lib/wizard-action.mjs +34 -0
  191. package/wizard/lib/wizard-conf.mjs +71 -0
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Hindsight's slice of ~/.config/personal-memory/config.toml: [stack] ports and the
3
+ * [model] table. The meta wizard owns [memory], [stack].project, [wizard] and [tasks].
4
+ */
5
+
6
+ /**
7
+ * Flat Hindsight defaults from a merged TOML config.
8
+ * @param {Record<string, unknown>} cfg
9
+ */
10
+ export function defaultsFromConfig(cfg) {
11
+ const stack = /** @type {Record<string, unknown>} */ (cfg.stack || {});
12
+ const model = /** @type {Record<string, unknown>} */ (cfg.model || {});
13
+ const lemond = /** @type {Record<string, unknown>} */ (model.lemond || {});
14
+ const ollama = /** @type {Record<string, unknown>} */ (model.ollama || {});
15
+ return {
16
+ apiPort: stack.api_port != null ? String(stack.api_port) : "",
17
+ cpPort: stack.control_plane_port != null ? String(stack.control_plane_port) : "",
18
+ modelBackend: String(model.backend || ""),
19
+ modelRetain: String(model.retain || ""),
20
+ modelReflect: String(model.reflect || ""),
21
+ modelApiBase: String(model.api_base || ""),
22
+ lemondHost: String(lemond.host || ""),
23
+ ollamaHost: String(ollama.host || ""),
24
+ };
25
+ }
26
+
27
+ /**
28
+ * Apply config defaults into process.env when env/CLI did not set them.
29
+ * @param {Record<string, unknown>} cfg
30
+ */
31
+ export function applyConfigEnv(cfg) {
32
+ const d = defaultsFromConfig(cfg);
33
+ if (d.apiPort && !process.env.HINDSIGHT_PORT) process.env.HINDSIGHT_PORT = d.apiPort;
34
+ if (d.cpPort && !process.env.HINDSIGHT_CP_PORT) process.env.HINDSIGHT_CP_PORT = d.cpPort;
35
+ if (d.modelBackend && !process.env.PM_BACKEND) process.env.PM_BACKEND = d.modelBackend;
36
+ if (d.modelRetain && !process.env.PM_DEFAULT_MODEL) {
37
+ process.env.PM_DEFAULT_MODEL = d.modelRetain;
38
+ }
39
+ if (d.modelReflect && !process.env.PM_REFLECT_MODEL) {
40
+ process.env.PM_REFLECT_MODEL = d.modelReflect;
41
+ }
42
+ if (d.modelApiBase && !process.env.MEMORY_API_BASE) {
43
+ process.env.MEMORY_API_BASE = d.modelApiBase;
44
+ }
45
+ if (d.lemondHost && !process.env.LEMOND_BASE) process.env.LEMOND_BASE = d.lemondHost;
46
+ }
47
+
48
+ /**
49
+ * Tables merged into config.toml when the wizard saves it.
50
+ * @param {Record<string, any>} ctx
51
+ * @returns {Record<string, Record<string, unknown>>}
52
+ */
53
+ export function configToml(ctx) {
54
+ const model = {
55
+ backend: ctx.backend,
56
+ retain: ctx.model,
57
+ reflect: ctx.reflectModel || "",
58
+ api_base: ctx.apiBase || "",
59
+ };
60
+ if (ctx.backend === "lemond" || ctx.lemondBase) {
61
+ model.lemond = { host: ctx.lemondBase };
62
+ }
63
+ if (ctx.backend === "ollama" || ctx.ollamaBase) {
64
+ model.ollama = { host: ctx.ollamaBase };
65
+ }
66
+ return {
67
+ stack: {
68
+ api_port: Number(ctx.portApi || 8888),
69
+ control_plane_port: Number(ctx.portCp || 9999),
70
+ },
71
+ model,
72
+ };
73
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Step "4. Secrets and env", Hindsight half: ~/.config/hindsight/hindsight.env from the
3
+ * shipped example, then the stack setup script (litellm.env, .env, compose.override.yaml),
4
+ * then wiring hindsight.env to the LiteLLM proxy.
5
+ */
6
+ import fs from "node:fs";
7
+ import path from "node:path";
8
+ import { ensureState } from "./state.mjs";
9
+
10
+ /** @typedef {import("../../../wizard/lib/provider-wizard/helpers.mjs").ProviderWizardHelpers} Helpers */
11
+
12
+ /**
13
+ * True when hindsight.env points Hindsight at the LiteLLM proxy with the master key.
14
+ * @param {Record<string, any>} ctx
15
+ * @param {Helpers} h
16
+ */
17
+ export function llmWired(ctx, h) {
18
+ if (h.envValue(ctx.envFile, "HINDSIGHT_API_LLM_BASE_URL") !== "http://litellm:4000/v1") {
19
+ return false;
20
+ }
21
+ const key = h.envValue(ctx.envFile, "HINDSIGHT_API_LLM_API_KEY");
22
+ const master = h.envValue(ctx.litellmEnv, "LITELLM_MASTER_KEY");
23
+ return Boolean(key && master && key === master);
24
+ }
25
+
26
+ /**
27
+ * Arguments for the meta wizard's stack setup script.
28
+ * @param {Record<string, any>} ctx
29
+ */
30
+ export function stackEnv(ctx) {
31
+ ensureState(ctx);
32
+ let backend = ctx.backend;
33
+ if (backend === "custom") backend = "lemond";
34
+ /** @type {Record<string, string>} */
35
+ const env = { MEMORY_BACKEND: backend };
36
+ if (ctx.ollamaInCompose) env.MEMORY_OLLAMA_IN_COMPOSE = "1";
37
+ return {
38
+ description:
39
+ "scripts/setup.sh: write .env, litellm.env (mode 600 when new), compose.override.yaml",
40
+ env,
41
+ };
42
+ }
43
+
44
+ /**
45
+ * @param {Record<string, any>} ctx
46
+ * @param {Helpers} h
47
+ */
48
+ export async function setupEnv(ctx, h) {
49
+ ensureState(ctx);
50
+ const example = path.join(ctx.repoRoot, "config/hindsight.env.example");
51
+ if (fs.existsSync(ctx.envFile)) h.ok(`kept ${ctx.envFile}`);
52
+ else if (ctx.dry) h.info(`[dry-run] create ${ctx.envFile} from config/hindsight.env.example (mode 600)`);
53
+ else {
54
+ fs.copyFileSync(example, ctx.envFile);
55
+ fs.chmodSync(ctx.envFile, 0o600);
56
+ h.ok(`created ${ctx.envFile} (mode 600)`);
57
+ }
58
+ await h.runStackSetup(ctx);
59
+ if (ctx.dry) {
60
+ h.info(
61
+ `[dry-run] run setup.sh --migrate-env unless ${ctx.envFile} already uses the LiteLLM proxy and master key`,
62
+ );
63
+ } else if (llmWired(ctx, h)) h.ok("Hindsight calls the LiteLLM proxy with the master key");
64
+ else if (await h.confirm(ctx, `Point ${ctx.envFile} at the LiteLLM proxy now (backs up the file, prints no values)?`, "y")) {
65
+ await h.runStackSetup(ctx, {
66
+ description: "migrate hindsight.env",
67
+ args: ["--migrate-env"],
68
+ env: {},
69
+ });
70
+ } else h.warn("Hindsight is not wired to the LiteLLM proxy; the stack will not work until you migrate");
71
+ }
@@ -0,0 +1,187 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { configToml } from "./config.mjs";
4
+ import { OLLAMA_HOST_API_BASE } from "./state.mjs";
5
+
6
+ /** @typedef {import("../../../wizard/lib/provider-wizard/helpers.mjs").ProviderWizardHelpers} Helpers */
7
+
8
+ /** Shipped litellm.<backend>.yaml api_base for the host-side model server. */
9
+ function shippedApiBase(backend) {
10
+ if (backend === "ollama") return OLLAMA_HOST_API_BASE;
11
+ if (backend === "lemond") return "http://host.docker.internal:13305/v1";
12
+ return "";
13
+ }
14
+
15
+ /**
16
+ * @param {Record<string, any>} ctx
17
+ * @param {string} alias
18
+ * @param {string} litellmModel
19
+ */
20
+ function renderRoute(ctx, alias, litellmModel) {
21
+ let out = ` - model_name: ${alias}\n litellm_params:\n model: ${litellmModel}\n api_base: ${ctx.apiBase}\n`;
22
+ if (ctx.backend === "ollama") {
23
+ out += " extra_body:\n think: false\n";
24
+ } else {
25
+ out += " api_key: os.environ/LEMOND_API_KEY\n extra_body:\n chat_template_kwargs:\n enable_thinking: false\n";
26
+ }
27
+ return out;
28
+ }
29
+
30
+ /**
31
+ * @param {Record<string, any>} ctx
32
+ * @param {string} litellmModel
33
+ */
34
+ function renderOverride(ctx, litellmModel) {
35
+ return (
36
+ "# Written by ./setup. Replaces config/litellm.*.yaml while this file exists.\n" +
37
+ "# Delete it, or rerun ./setup with the shipped model, to use the shipped routes again.\n" +
38
+ "# Thinking is off for all three aliases. Non-ollama servers get chat_template_kwargs; a server that rejects it needs a different route.\n" +
39
+ "model_list:\n" +
40
+ renderRoute(ctx, "memory-retain", litellmModel) +
41
+ renderRoute(ctx, "memory-reflect", litellmModel) +
42
+ renderRoute(ctx, "memory-consolidate", litellmModel) +
43
+ "general_settings:\n master_key: os.environ/LITELLM_MASTER_KEY\n"
44
+ );
45
+ }
46
+
47
+ /**
48
+ * @param {Record<string, any>} ctx
49
+ * @param {string} litellmModel
50
+ * @param {string} reflectLitellmModel
51
+ */
52
+ function renderShipped(ctx, litellmModel, reflectLitellmModel) {
53
+ const src = path.join(ctx.repoRoot, "config", `litellm.${ctx.backend}.yaml`);
54
+ const lines = fs.readFileSync(src, "utf8").split("\n");
55
+ const out = [
56
+ `# Written by ./setup from config/litellm.${ctx.backend}.yaml. Replaces the shipped routes while this file exists.`,
57
+ "# Delete it, or rerun ./setup with the shipped models, to use the shipped routes again.",
58
+ ];
59
+ let name = "";
60
+ for (const line of lines) {
61
+ const mn = line.match(/^ - model_name: (\S+)/);
62
+ if (mn) name = mn[1];
63
+ if (/^ api_base: /.test(line)) {
64
+ out.push(` api_base: ${ctx.apiBase}`);
65
+ continue;
66
+ }
67
+ if (/^ model: /.test(line)) {
68
+ if (name !== "memory-reflect") out.push(` model: ${litellmModel}`);
69
+ else if (reflectLitellmModel) out.push(` model: ${reflectLitellmModel}`);
70
+ else out.push(line);
71
+ continue;
72
+ }
73
+ out.push(line);
74
+ }
75
+ return `${out.join("\n")}\n`;
76
+ }
77
+
78
+ /**
79
+ * @param {Record<string, any>} ctx
80
+ * @returns {string}
81
+ */
82
+ export function litellmConfig(ctx) {
83
+ let prefix = "openai/";
84
+ if (ctx.backend === "ollama") prefix = "ollama_chat/";
85
+ if (ctx.backend === "custom") return renderOverride(ctx, `${prefix}${ctx.model}`);
86
+ let reflect = "";
87
+ if (ctx.reflectModel) reflect = `${prefix}${ctx.reflectModel}`;
88
+ return renderShipped(ctx, `${prefix}${ctx.model}`, reflect);
89
+ }
90
+
91
+ /**
92
+ * @param {Record<string, any>} ctx
93
+ * @param {Helpers} h
94
+ */
95
+ export async function removeOverrideIfAsked(ctx, h) {
96
+ if (!fs.existsSync(ctx.litellmOverride)) return;
97
+ if (ctx.dry) {
98
+ h.info(`[dry-run] move ${ctx.litellmOverride} aside so the shipped ${ctx.backend} routes apply`);
99
+ return;
100
+ }
101
+ if (!(await h.confirm(ctx, `Move ${ctx.litellmOverride} aside so the shipped ${ctx.backend} routes apply?`, "y"))) {
102
+ h.warn("kept the override; it still takes precedence over the shipped routes");
103
+ return;
104
+ }
105
+ const bak = `${ctx.litellmOverride}.bak-${timestamp()}`;
106
+ fs.renameSync(ctx.litellmOverride, bak);
107
+ h.ok("moved the override aside");
108
+ }
109
+
110
+ /**
111
+ * @param {Record<string, any>} ctx
112
+ * @param {Helpers} h
113
+ */
114
+ export async function writeOverride(ctx, h) {
115
+ if (ctx.dry) {
116
+ h.info(
117
+ `[dry-run] write ${ctx.litellmOverride} from config/litellm.${ctx.backend}.yaml: retain and consolidate -> ${ctx.model}, reflect -> ${ctx.reflectModel || "shipped"}, at ${ctx.apiBase}`,
118
+ );
119
+ return;
120
+ }
121
+ h.ensureConfigDir(ctx);
122
+ const content = litellmConfig(ctx);
123
+ const tmp = path.join(ctx.hsDir, `.litellm.yaml.${process.pid}`);
124
+ fs.writeFileSync(tmp, content, { mode: 0o644 });
125
+ if (fs.existsSync(ctx.litellmOverride)) {
126
+ const existing = fs.readFileSync(ctx.litellmOverride);
127
+ if (Buffer.compare(existing, Buffer.from(content)) === 0) {
128
+ fs.unlinkSync(tmp);
129
+ h.ok(`${ctx.litellmOverride} already has this route`);
130
+ return;
131
+ }
132
+ if (!(await h.confirm(ctx, `Replace ${ctx.litellmOverride} (the old file is kept as .bak)?`, "y"))) {
133
+ fs.unlinkSync(tmp);
134
+ h.warn("kept the existing override; it still takes precedence");
135
+ return;
136
+ }
137
+ fs.copyFileSync(ctx.litellmOverride, `${ctx.litellmOverride}.bak-${timestamp()}`);
138
+ }
139
+ fs.renameSync(tmp, ctx.litellmOverride);
140
+ h.ok(`wrote ${ctx.litellmOverride}`);
141
+ }
142
+
143
+ /**
144
+ * @param {Record<string, any>} ctx
145
+ * @param {Helpers} h
146
+ */
147
+ export async function applyRoute(ctx, h) {
148
+ let shipped = "";
149
+ if (ctx.backend === "lemond") shipped = ctx.lemondDefaultModel;
150
+ if (ctx.backend === "ollama") shipped = ctx.ollamaDefaultModel;
151
+ const defaultBase = shippedApiBase(ctx.backend);
152
+ const apiMatchesShipped = !ctx.apiBase || ctx.apiBase === defaultBase;
153
+ if (ctx.backend !== "custom" && ctx.model === shipped && !ctx.reflectModel && apiMatchesShipped) {
154
+ await removeOverrideIfAsked(ctx, h);
155
+ } else {
156
+ await writeOverride(ctx, h);
157
+ }
158
+ }
159
+
160
+ function timestamp() {
161
+ const d = new Date();
162
+ const p = (n) => String(n).padStart(2, "0");
163
+ return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
164
+ }
165
+
166
+ /**
167
+ * Record the backend choice in wizard.conf (and config.toml).
168
+ * @param {Record<string, any>} ctx
169
+ * @param {Helpers} h
170
+ */
171
+ export function saveConf(ctx, h) {
172
+ if (ctx.dry) {
173
+ h.info(`[dry-run] record backend ${ctx.backend} in ${ctx.wizardConf}`);
174
+ return;
175
+ }
176
+ h.saveWizardConf(
177
+ ctx,
178
+ {
179
+ MEMORY_BACKEND: ctx.backend,
180
+ MEMORY_MODEL: ctx.model,
181
+ MEMORY_API_BASE: ctx.apiBase,
182
+ MEMORY_REFLECT_MODEL: ctx.reflectModel,
183
+ MEMORY_OLLAMA_IN_COMPOSE: ctx.ollamaInCompose ? "1" : "0",
184
+ },
185
+ { pmSections: configToml(ctx) },
186
+ );
187
+ }
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Hindsight model routing: how the LiteLLM proxy in front of Hindsight reaches an LLM
3
+ * (lemond, Ollama or a custom OpenAI-compatible URL), plus the reflect-model question and
4
+ * `lemonade pull`. The meta wizard knows none of this; it calls configureModelRouting.
5
+ */
6
+ import { applyRoute, litellmConfig, saveConf } from "./litellm-routes.mjs";
7
+ import { ollamaModelNames, openAiModelIds } from "./probe.mjs";
8
+ import {
9
+ backendDefault,
10
+ ensureState,
11
+ OLLAMA_COMPOSE_API_BASE,
12
+ OLLAMA_HOST_API_BASE,
13
+ storedOr,
14
+ } from "./state.mjs";
15
+
16
+ /** @typedef {import("../../../wizard/lib/provider-wizard/helpers.mjs").ProviderWizardHelpers} Helpers */
17
+
18
+ /**
19
+ * @param {Record<string, any>} ctx
20
+ * @param {Helpers} h
21
+ */
22
+ async function pickBackend(ctx, h) {
23
+ const def = backendDefault(ctx, h);
24
+ if (ctx.yes) {
25
+ ctx.backend = def;
26
+ return;
27
+ }
28
+ h.info("Pick a backend first; only that server is contacted.");
29
+ h.info("1) lemond 2) ollama 3) custom OpenAI-compatible URL");
30
+ const defNum = def === "lemond" ? "1" : def === "ollama" ? "2" : "3";
31
+ const choice = await h.ask(ctx, "Model backend (1-3)", defNum);
32
+ switch (choice) {
33
+ case "1":
34
+ ctx.backend = "lemond";
35
+ break;
36
+ case "2":
37
+ ctx.backend = "ollama";
38
+ break;
39
+ case "3":
40
+ ctx.backend = "custom";
41
+ break;
42
+ default:
43
+ h.die("choose 1, 2 or 3");
44
+ }
45
+ }
46
+
47
+ /** @param {Record<string, any>} ctx @param {Helpers} h */
48
+ function probeLemond(ctx, h) {
49
+ const body = h.curlJson(`${ctx.lemondBase}/v1/models`);
50
+ ctx.lemondJson = body;
51
+ ctx.lemondNames = openAiModelIds(typeof body === "object" ? body : null);
52
+ }
53
+
54
+ /** @param {Record<string, any>} ctx @param {Helpers} h */
55
+ function probeOllama(ctx, h) {
56
+ const body = h.curlJson(`${ctx.ollamaBase}/api/tags`);
57
+ ctx.ollamaJson = body;
58
+ ctx.ollamaNames = ollamaModelNames(body);
59
+ }
60
+
61
+ /** @param {string[]} names @param {Helpers} h */
62
+ function showModels(names, h) {
63
+ for (const n of names) h.info(` ${n}`);
64
+ }
65
+
66
+ /** @param {Record<string, any>} ctx @param {Helpers} h */
67
+ async function askReflectModel(ctx, h) {
68
+ const defaultAnswer =
69
+ process.env.PM_REFLECT_MODEL ||
70
+ storedOr(ctx, h, ctx.backend, "MEMORY_REFLECT_MODEL", "");
71
+ const answer = await h.ask(
72
+ ctx,
73
+ "Separate reflect model (type shipped for the shipped reflect route)",
74
+ defaultAnswer,
75
+ );
76
+ if (answer === "" || answer === "shipped") ctx.reflectModel = "";
77
+ else ctx.reflectModel = answer;
78
+ }
79
+
80
+ /** @param {Record<string, any>} ctx @param {Helpers} h */
81
+ async function pullLemondModels(ctx, h) {
82
+ if (!ctx.lemondJson) return;
83
+ const names = new Set();
84
+ for (const line of litellmConfig(ctx).split("\n")) {
85
+ const m = line.match(/^\s*model: openai\/(.+)/);
86
+ if (m) names.add(m[1]);
87
+ }
88
+ for (const name of names) {
89
+ if (!name || ctx.lemondNames.includes(name)) continue;
90
+ h.warn(`${name} is not downloaded in lemond`);
91
+ if (await h.confirm(ctx, `Run lemonade pull ${name} now?`, "y")) {
92
+ if (h.commandExists("lemonade")) {
93
+ await h.act(ctx, `lemonade pull ${name}`, "lemonade", ["pull", name]);
94
+ } else h.warn(`lemonade is not on PATH: run 'lemonade pull ${name}' on this host`);
95
+ }
96
+ }
97
+ }
98
+
99
+ /** @param {Record<string, any>} ctx @param {Helpers} h */
100
+ async function chooseLemond(ctx, h) {
101
+ ctx.apiBase = "http://host.docker.internal:13305/v1";
102
+ const defaultModel = storedOr(
103
+ ctx,
104
+ h,
105
+ "lemond",
106
+ "MEMORY_MODEL",
107
+ process.env.PM_DEFAULT_MODEL || ctx.lemondDefaultModel,
108
+ );
109
+ if (!ctx.lemondJson) {
110
+ h.warn(`lemond is not answering on ${ctx.lemondBase}; the model list is unavailable`);
111
+ } else if (!ctx.lemondNames.includes(ctx.lemondDefaultModel)) {
112
+ h.warn(`${ctx.lemondDefaultModel} is not listed by lemond`);
113
+ }
114
+ ctx.model = await h.ask(ctx, "lemond model id", defaultModel);
115
+ await askReflectModel(ctx, h);
116
+ await pullLemondModels(ctx, h);
117
+ }
118
+
119
+ /** @param {Record<string, any>} ctx @param {Helpers} h */
120
+ async function chooseOllama(ctx, h) {
121
+ const defaultModel = storedOr(
122
+ ctx,
123
+ h,
124
+ "ollama",
125
+ "MEMORY_MODEL",
126
+ process.env.PM_DEFAULT_MODEL || ctx.ollamaDefaultModel,
127
+ );
128
+ if (ctx.ollamaJson) {
129
+ ctx.ollamaInCompose = false;
130
+ ctx.apiBase = OLLAMA_HOST_API_BASE;
131
+ } else {
132
+ h.warn(`Ollama is not answering on ${ctx.ollamaBase}`);
133
+ const useDocker = await h.confirm(
134
+ ctx,
135
+ "Ollama is not on this host. Run it in Docker (compose profile ollama)?",
136
+ "y",
137
+ );
138
+ ctx.ollamaInCompose = useDocker;
139
+ ctx.apiBase = useDocker ? OLLAMA_COMPOSE_API_BASE : OLLAMA_HOST_API_BASE;
140
+ if (useDocker) {
141
+ h.info("LiteLLM will call http://ollama:11434; the wizard pulls the model after the stack starts.");
142
+ }
143
+ }
144
+ ctx.model = await h.ask(ctx, "Ollama model", defaultModel);
145
+ await askReflectModel(ctx, h);
146
+ }
147
+
148
+ /** @param {Record<string, any>} ctx @param {Helpers} h */
149
+ async function chooseCustom(ctx, h) {
150
+ const defaultBase = storedOr(ctx, h, "custom", "MEMORY_API_BASE", ctx.customDefaultBase);
151
+ const defaultModel = storedOr(ctx, h, "custom", "MEMORY_MODEL", ctx.lemondDefaultModel);
152
+ h.info("The litellm container calls this URL. Host services use host.docker.internal.");
153
+ h.info("Examples: lemond http://host.docker.internal:13305/v1 ; Meridian http://host.docker.internal:3456/v1");
154
+ ctx.apiBase = await h.ask(ctx, "OpenAI-compatible base URL", defaultBase);
155
+ ctx.model = await h.ask(ctx, "Model id the server serves", defaultModel);
156
+ await askReflectModel(ctx, h);
157
+ }
158
+
159
+ /**
160
+ * Step "3. Model backend": choose lemond, Ollama or a custom URL, write the LiteLLM route
161
+ * override when the choice differs from the shipped routes, and record it in wizard.conf.
162
+ * With the Clack UI done (ctx.skipBackendStep) the answers come from PM_BACKEND,
163
+ * PM_DEFAULT_MODEL, MEMORY_API_BASE and PM_REFLECT_MODEL instead.
164
+ *
165
+ * @param {Record<string, any>} ctx
166
+ * @param {Helpers} h
167
+ */
168
+ export async function chooseModelBackend(ctx, h) {
169
+ ensureState(ctx);
170
+ h.step("3. Model backend");
171
+ if (ctx.skipBackendStep) {
172
+ if (!process.env.PM_BACKEND) h.die("PM_BACKEND missing with WIZARD_SKIP_BACKEND_STEP");
173
+ if (!process.env.PM_DEFAULT_MODEL) h.die("PM_DEFAULT_MODEL missing with WIZARD_SKIP_BACKEND_STEP");
174
+ ctx.backend = process.env.PM_BACKEND;
175
+ ctx.model = process.env.PM_DEFAULT_MODEL;
176
+ ctx.apiBase = process.env.MEMORY_API_BASE || "";
177
+ ctx.reflectModel = process.env.PM_REFLECT_MODEL || "";
178
+ ctx.ollamaInCompose = process.env.MEMORY_OLLAMA_IN_COMPOSE === "1";
179
+ if (ctx.backend === "ollama" && !ctx.apiBase) {
180
+ ctx.apiBase = ctx.ollamaInCompose ? OLLAMA_COMPOSE_API_BASE : OLLAMA_HOST_API_BASE;
181
+ }
182
+ h.ok(`backend ${ctx.backend}, model ${ctx.model} (setup UI)`);
183
+ await applyRoute(ctx, h);
184
+ saveConf(ctx, h);
185
+ return;
186
+ }
187
+ await pickBackend(ctx, h);
188
+ switch (ctx.backend) {
189
+ case "lemond":
190
+ probeLemond(ctx, h);
191
+ if (ctx.lemondJson) {
192
+ h.ok(`lemond answers on ${ctx.lemondBase}`);
193
+ showModels(ctx.lemondNames, h);
194
+ } else h.info(`lemond: not answering on ${ctx.lemondBase}`);
195
+ await chooseLemond(ctx, h);
196
+ break;
197
+ case "ollama":
198
+ probeOllama(ctx, h);
199
+ if (ctx.ollamaJson) {
200
+ h.ok(`Ollama answers on ${ctx.ollamaBase}`);
201
+ showModels(ctx.ollamaNames, h);
202
+ } else h.info(`Ollama: not answering on ${ctx.ollamaBase}`);
203
+ await chooseOllama(ctx, h);
204
+ break;
205
+ case "custom":
206
+ await chooseCustom(ctx, h);
207
+ break;
208
+ default:
209
+ h.die(`unknown backend ${ctx.backend}`);
210
+ }
211
+ h.ok(`backend ${ctx.backend}, model ${ctx.model}`);
212
+ await applyRoute(ctx, h);
213
+ saveConf(ctx, h);
214
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * When notebooklmAccess.enabled, merge a "notebooklm" stdio MCP into ~/.cursor/mcp.json.
3
+ * Never overwrites existing "hindsight" or "codegraph" entries. Dry-run safe.
4
+ */
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import { resolveAccess, loadCodingAgentConfig } from "../../../cli/notebooklm-mcp.mjs";
8
+
9
+ /** @typedef {import("../../../wizard/lib/provider-wizard/helpers.mjs").ProviderWizardHelpers} Helpers */
10
+
11
+ /** @param {string} repoRoot */
12
+ export function notebooklmMcpScriptPath(repoRoot) {
13
+ return path.resolve(repoRoot, "cli/notebooklm-mcp.mjs");
14
+ }
15
+
16
+ /**
17
+ * @param {Record<string, any>} mcp
18
+ * @param {string} scriptPath absolute path to cli/notebooklm-mcp.mjs
19
+ * @param {{ agentJson?: string }} [opts]
20
+ * @returns {{ mcp: Record<string, any>, changed: boolean, skipped?: string }}
21
+ */
22
+ export function mergeNotebooklmMcpServer(mcp, scriptPath, opts = {}) {
23
+ const out = { ...mcp };
24
+ const servers = { ...(mcp.mcpServers && typeof mcp.mcpServers === "object" ? mcp.mcpServers : {}) };
25
+ // Absolute node + explicit config path: Cursor Agent's PATH may resolve a different
26
+ // `node`, and a symlink ~/.hindsight/coding-agent.json → shipped template used to
27
+ // make the process exit before handshake ("Connection closed").
28
+ const nodeBin = process.execPath;
29
+ const agentJson =
30
+ opts.agentJson ||
31
+ process.env.HINDSIGHT_CODING_AGENT_JSON ||
32
+ path.join(process.env.HOME || "", ".hindsight", "coding-agent.json");
33
+ const next = {
34
+ type: "stdio",
35
+ command: nodeBin,
36
+ args: [scriptPath],
37
+ env: {
38
+ HINDSIGHT_CODING_AGENT_JSON: agentJson,
39
+ },
40
+ };
41
+ const prev = servers.notebooklm;
42
+ const same =
43
+ prev &&
44
+ prev.command === next.command &&
45
+ Array.isArray(prev.args) &&
46
+ prev.args.length === 1 &&
47
+ prev.args[0] === scriptPath &&
48
+ prev.type === "stdio" &&
49
+ prev.env?.HINDSIGHT_CODING_AGENT_JSON === next.env.HINDSIGHT_CODING_AGENT_JSON;
50
+ if (same) return { mcp: { ...out, mcpServers: servers }, changed: false, skipped: "already configured" };
51
+ servers.notebooklm = next;
52
+ // Preserve sibling servers by reference identity of keys we did not touch.
53
+ return { mcp: { ...out, mcpServers: servers }, changed: true };
54
+ }
55
+
56
+ /**
57
+ * @param {Record<string, any>} ctx
58
+ * @param {Helpers} h
59
+ */
60
+ export async function installNotebooklmMcp(ctx, h) {
61
+ const agentPath = ctx.agentJson || path.join(ctx.home, ".hindsight", "coding-agent.json");
62
+ const tracked = path.join(ctx.repoRoot, "config/coding-agent.json");
63
+ let cfg = null;
64
+ for (const p of [agentPath, tracked]) {
65
+ try {
66
+ if (fs.existsSync(p)) {
67
+ cfg = loadCodingAgentConfig(p);
68
+ break;
69
+ }
70
+ } catch {
71
+ /* try next */
72
+ }
73
+ }
74
+ if (!cfg) {
75
+ h.info("notebooklm MCP: no coding-agent.json yet; skip Cursor mcp.json merge");
76
+ return;
77
+ }
78
+ const access = resolveAccess(cfg);
79
+ if (!access.enabled) {
80
+ h.info("notebooklmAccess.enabled is false; skip registering notebooklm MCP in ~/.cursor/mcp.json");
81
+ return;
82
+ }
83
+
84
+ const script = notebooklmMcpScriptPath(ctx.repoRoot);
85
+ if (!fs.existsSync(script)) {
86
+ h.warn(`notebooklm MCP script missing: ${script}`);
87
+ return;
88
+ }
89
+
90
+ const mcpPath = path.join(ctx.home, ".cursor", "mcp.json");
91
+ let existing = {};
92
+ if (fs.existsSync(mcpPath)) {
93
+ try {
94
+ existing = JSON.parse(fs.readFileSync(mcpPath, "utf8"));
95
+ } catch (err) {
96
+ h.warn(`cannot parse ${mcpPath}: ${err instanceof Error ? err.message : err}`);
97
+ return;
98
+ }
99
+ }
100
+
101
+ // Guard: never clobber unrelated servers (merge only adds/updates "notebooklm").
102
+ const beforeHindsight = existing.mcpServers?.hindsight;
103
+ const beforeCodegraph = existing.mcpServers?.codegraph;
104
+ const { mcp, changed, skipped } = mergeNotebooklmMcpServer(existing, script, {
105
+ agentJson: path.resolve(agentPath),
106
+ });
107
+ if (beforeHindsight && mcp.mcpServers?.hindsight !== beforeHindsight) {
108
+ h.warn("refusing to alter hindsight MCP entry");
109
+ return;
110
+ }
111
+ if (beforeCodegraph && mcp.mcpServers?.codegraph !== beforeCodegraph) {
112
+ h.warn("refusing to alter codegraph MCP entry");
113
+ return;
114
+ }
115
+
116
+ if (!changed) {
117
+ h.ok(`notebooklm MCP already in ${mcpPath}${skipped ? ` (${skipped})` : ""}`);
118
+ return;
119
+ }
120
+ if (ctx.dry) {
121
+ h.info(`[dry-run] merge notebooklm MCP into ${mcpPath} → node ${script}`);
122
+ return;
123
+ }
124
+ fs.mkdirSync(path.dirname(mcpPath), { recursive: true });
125
+ fs.writeFileSync(mcpPath, `${JSON.stringify(mcp, null, 2)}\n`);
126
+ h.ok(`registered notebooklm MCP in ${mcpPath}`);
127
+ }