@bcmyguest/openmem 0.0.0-stage → 0.8.0

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 (230) hide show
  1. package/.agents/skills/openmem/SKILL.md +34 -0
  2. package/LICENSE +190 -0
  3. package/README.md +140 -2
  4. package/bin/hindsight-project-index +530 -0
  5. package/cli/coding-agent/access-policy.mjs +38 -0
  6. package/cli/coding-agent/agents.mjs +34 -0
  7. package/cli/coding-agent/bank-from-cwd.mjs +140 -0
  8. package/cli/coding-agent/capabilities.mjs +34 -0
  9. package/cli/coding-agent/context-store.mjs +81 -0
  10. package/cli/coding-agent/context-trim.mjs +897 -0
  11. package/cli/coding-agent/file-io.mjs +72 -0
  12. package/cli/coding-agent/hook-harnesses.mjs +88 -0
  13. package/cli/coding-agent/hook-runner.mjs +284 -0
  14. package/cli/coding-agent/install-access.mjs +119 -0
  15. package/cli/coding-agent/install-context-proxy.mjs +333 -0
  16. package/cli/coding-agent/install-rtk.mjs +60 -0
  17. package/cli/coding-agent/install-shim.mjs +201 -0
  18. package/cli/coding-agent/opencode-config.mjs +107 -0
  19. package/cli/coding-agent/opencode-plugin.mjs +132 -0
  20. package/cli/coding-agent/opencode2-plugin/index.js +299 -0
  21. package/cli/coding-agent/recall-frame.mjs +29 -0
  22. package/cli/coding-agent/remove-upstream-access.mjs +122 -0
  23. package/cli/coding-agent/runtime.mjs +40 -0
  24. package/cli/coding-agent/session-db.mjs +626 -0
  25. package/cli/coding-agent/session-facade.mjs +86 -0
  26. package/cli/coding-agent/toml-edit.mjs +161 -0
  27. package/cli/cognee-mcp.mjs +260 -0
  28. package/cli/context-management.mjs +254 -0
  29. package/cli/feed-sync.mjs +27 -0
  30. package/cli/install-coding-agent.mjs +298 -0
  31. package/cli/install-memory-mcp.mjs +426 -0
  32. package/cli/is-main.mjs +18 -0
  33. package/cli/mcp/context-sql-runner.mjs +58 -0
  34. package/cli/mcp/context-store.mjs +670 -0
  35. package/cli/mcp/context.mjs +61 -0
  36. package/cli/mcp/export/cognee-dataset-name.mjs +12 -0
  37. package/cli/mcp/export/cognee.mjs +119 -0
  38. package/cli/mcp/export/hindsight.mjs +48 -0
  39. package/cli/mcp/export/index.mjs +26 -0
  40. package/cli/mcp/export/mem0.mjs +31 -0
  41. package/cli/mcp/knowledge.mjs +333 -0
  42. package/cli/mcp/llm-synthesize.mjs +50 -0
  43. package/cli/mcp/mem0-scope-id.mjs +22 -0
  44. package/cli/mcp/memory-facade.mjs +521 -0
  45. package/cli/mcp/memory-reflect-consolidate.mjs +120 -0
  46. package/cli/mcp/notebooklm-banks.mjs +21 -0
  47. package/cli/mcp/notebooklm-hindsight-rest.mjs +45 -0
  48. package/cli/mcp/notebooklm-provider.mjs +225 -0
  49. package/cli/mcp/pmm-records.mjs +23 -0
  50. package/cli/mcp/portability.mjs +158 -0
  51. package/cli/mcp/stdio.mjs +65 -0
  52. package/cli/mcp/unified-mcp-install.mjs +65 -0
  53. package/cli/mem0-mcp.mjs +108 -0
  54. package/cli/notebooklm-mcp.mjs +236 -0
  55. package/cli/openmem-mcp.mjs +38 -0
  56. package/cli/openmem.mjs +222 -0
  57. package/cli/roots.mjs +85 -0
  58. package/cli/schedule.mjs +176 -0
  59. package/cli/stack.mjs +438 -0
  60. package/cli/switch-memory-provider.mjs +358 -0
  61. package/cli/wiring-report.mjs +125 -0
  62. package/compose.bench.yaml +12 -0
  63. package/compose.yaml +227 -0
  64. package/config/coding-agent.json +13 -0
  65. package/config/hindsight.env.example +26 -0
  66. package/config/index.yml +54 -0
  67. package/config/litellm.lemond.yaml +40 -0
  68. package/config/litellm.ollama.yaml +33 -0
  69. package/config/notebooklm.yml +33 -0
  70. package/config/openmem.toml.example +124 -0
  71. package/docs/OPENMEM.md +23 -0
  72. package/docs/architecture.md +245 -0
  73. package/docs/assets/README.md +10 -0
  74. package/docs/assets/setup-demo.gif +0 -0
  75. package/docs/coding-agent-hooks.md +75 -0
  76. package/docs/coding-agent-scopes.md +22 -0
  77. package/docs/coding-agents.md +71 -0
  78. package/docs/cognee.md +37 -0
  79. package/docs/compatibility.md +80 -0
  80. package/docs/concepts.md +135 -0
  81. package/docs/context-management.md +255 -0
  82. package/docs/context-proxy.md +115 -0
  83. package/docs/context-research.md +215 -0
  84. package/docs/context-store.md +277 -0
  85. package/docs/hindsight.md +77 -0
  86. package/docs/index-sources-roadmap.md +38 -0
  87. package/docs/index-sources.md +39 -0
  88. package/docs/indexer-sinks.md +122 -0
  89. package/docs/indexing.md +57 -0
  90. package/docs/manual-stack-setup.md +74 -0
  91. package/docs/mcp-install-guide.md +4 -0
  92. package/docs/mcp-setup.md +104 -0
  93. package/docs/mem0.md +141 -0
  94. package/docs/memory-portability.md +73 -0
  95. package/docs/memory-providers.md +246 -0
  96. package/docs/meridian-litellm.md +198 -0
  97. package/docs/model-configuration.md +106 -0
  98. package/docs/notebooklm.md +83 -0
  99. package/docs/npx-install.md +172 -0
  100. package/docs/openmem-config.md +194 -0
  101. package/docs/provider-wizard-hooks.md +222 -0
  102. package/docs/quickstart.md +107 -0
  103. package/docs/setup-wizard.md +117 -0
  104. package/docs/sources-feeds.md +39 -0
  105. package/docs/sources-readwise.md +23 -0
  106. package/docs/sources-zotero.md +22 -0
  107. package/glama.json +4 -0
  108. package/indexer/Dockerfile +38 -0
  109. package/indexer/code_outline.py +146 -0
  110. package/indexer/feed_sync.py +107 -0
  111. package/indexer/index_config.py +290 -0
  112. package/indexer/pytest.ini +5 -0
  113. package/indexer/sinks/__init__.py +148 -0
  114. package/indexer/sinks/_http.py +44 -0
  115. package/indexer/sinks/cognee.py +329 -0
  116. package/indexer/sinks/hindsight.py +107 -0
  117. package/indexer/sinks/mem0.py +247 -0
  118. package/indexer/watch.py +240 -0
  119. package/package.json +96 -4
  120. package/providers/_template/compose.yaml +52 -0
  121. package/providers/cognee/OPERATOR.md +82 -0
  122. package/providers/cognee/compose.yaml +89 -0
  123. package/providers/cognee/lib/clack.mjs +157 -0
  124. package/providers/cognee/lib/coding-agent.mjs +26 -0
  125. package/providers/cognee/lib/cognee-mcp-install.mjs +153 -0
  126. package/providers/cognee/lib/config.mjs +98 -0
  127. package/providers/cognee/lib/env-file.mjs +194 -0
  128. package/providers/cognee/lib/env-setup.mjs +83 -0
  129. package/providers/cognee/lib/llm-env.mjs +382 -0
  130. package/providers/cognee/lib/ports.mjs +25 -0
  131. package/providers/cognee/lib/smoke.mjs +246 -0
  132. package/providers/cognee/lib/stack-env.mjs +134 -0
  133. package/providers/cognee/lib/state.mjs +101 -0
  134. package/providers/cognee/lib/summary.mjs +56 -0
  135. package/providers/cognee/lib/validate.mjs +97 -0
  136. package/providers/cognee/manifest.json +56 -0
  137. package/providers/cognee/wizard.mjs +134 -0
  138. package/providers/graphiti/manifest.json +15 -0
  139. package/providers/hindsight/lib/clack.mjs +347 -0
  140. package/providers/hindsight/lib/coding-agent.mjs +204 -0
  141. package/providers/hindsight/lib/config.mjs +79 -0
  142. package/providers/hindsight/lib/env-setup.mjs +72 -0
  143. package/providers/hindsight/lib/litellm-routes.mjs +196 -0
  144. package/providers/hindsight/lib/meridian.mjs +307 -0
  145. package/providers/hindsight/lib/model-backend.mjs +243 -0
  146. package/providers/hindsight/lib/notebooklm-mcp-install.mjs +127 -0
  147. package/providers/hindsight/lib/ollama-compose.mjs +51 -0
  148. package/providers/hindsight/lib/ports.mjs +30 -0
  149. package/providers/hindsight/lib/probe.mjs +54 -0
  150. package/providers/hindsight/lib/stack-env.mjs +125 -0
  151. package/providers/hindsight/lib/state.mjs +80 -0
  152. package/providers/hindsight/lib/summary.mjs +57 -0
  153. package/providers/hindsight/lib/validate.mjs +81 -0
  154. package/providers/hindsight/manifest.json +58 -0
  155. package/providers/hindsight/wizard.mjs +150 -0
  156. package/providers/letta/manifest.json +29 -0
  157. package/providers/mem0/OPERATOR.md +143 -0
  158. package/providers/mem0/compose.yaml +79 -0
  159. package/providers/mem0/lib/coding-agent.mjs +58 -0
  160. package/providers/mem0/lib/config.mjs +45 -0
  161. package/providers/mem0/lib/configure.mjs +201 -0
  162. package/providers/mem0/lib/dotenv.mjs +71 -0
  163. package/providers/mem0/lib/env-file.mjs +110 -0
  164. package/providers/mem0/lib/http.mjs +69 -0
  165. package/providers/mem0/lib/litellm.mjs +457 -0
  166. package/providers/mem0/lib/mcp-install.mjs +165 -0
  167. package/providers/mem0/lib/secret.mjs +30 -0
  168. package/providers/mem0/lib/smoke.mjs +179 -0
  169. package/providers/mem0/lib/stack-env.mjs +255 -0
  170. package/providers/mem0/lib/state.mjs +67 -0
  171. package/providers/mem0/lib/summary.mjs +88 -0
  172. package/providers/mem0/lib/url.mjs +106 -0
  173. package/providers/mem0/lib/validate.mjs +158 -0
  174. package/providers/mem0/manifest.json +47 -0
  175. package/providers/mem0/wizard.mjs +146 -0
  176. package/providers/zep/manifest.json +29 -0
  177. package/proxy/config.yaml +33 -0
  178. package/proxy/openmem_context/__init__.py +6 -0
  179. package/proxy/openmem_context/adapters.py +504 -0
  180. package/proxy/openmem_context/compress.py +53 -0
  181. package/proxy/openmem_context/core.py +606 -0
  182. package/proxy/openmem_context/embed.py +212 -0
  183. package/proxy/openmem_context/recall.py +49 -0
  184. package/proxy/openmem_context/session.py +70 -0
  185. package/proxy/openmem_context/sessiondb.py +276 -0
  186. package/proxy/openmem_context/settings.py +134 -0
  187. package/proxy/openmem_context/store.py +129 -0
  188. package/proxy/openmem_context/tokens.py +123 -0
  189. package/proxy/openmem_context/ttl.py +78 -0
  190. package/proxy/openmem_context_hook.py +34 -0
  191. package/scripts/check-pack.mjs +219 -0
  192. package/scripts/gen-masks.mjs +83 -0
  193. package/scripts/gen-masks.sh +4 -0
  194. package/scripts/promotion-provider-smoke.mjs +34 -0
  195. package/scripts/run-setup.sh +30 -0
  196. package/scripts/setup.mjs +107 -0
  197. package/scripts/setup.sh +5 -0
  198. package/scripts/spin-up.sh +45 -0
  199. package/scripts/wizard.sh +5 -0
  200. package/server.json +34 -0
  201. package/sync/Dockerfile +27 -0
  202. package/sync/memory_backend.py +87 -0
  203. package/sync/notebooklm_sync.py +664 -0
  204. package/wizard/bin/pm.mjs +97 -0
  205. package/wizard/bin/setup.mjs +33 -0
  206. package/wizard/lib/compose-version.mjs +32 -0
  207. package/wizard/lib/compose.mjs +70 -0
  208. package/wizard/lib/context-step.mjs +140 -0
  209. package/wizard/lib/curl.mjs +103 -0
  210. package/wizard/lib/docker-build.mjs +148 -0
  211. package/wizard/lib/exec.mjs +41 -0
  212. package/wizard/lib/indexer.mjs +6 -0
  213. package/wizard/lib/litellm-env.mjs +55 -0
  214. package/wizard/lib/load-machine-env.mjs +4 -0
  215. package/wizard/lib/log.mjs +32 -0
  216. package/wizard/lib/notebooklm-agent-access.mjs +92 -0
  217. package/wizard/lib/notebooklm.mjs +214 -0
  218. package/wizard/lib/parse-args.mjs +87 -0
  219. package/wizard/lib/paths.mjs +39 -0
  220. package/wizard/lib/pm-config.mjs +371 -0
  221. package/wizard/lib/ports.mjs +94 -0
  222. package/wizard/lib/prompts.mjs +85 -0
  223. package/wizard/lib/provider-wizard/helpers.mjs +98 -0
  224. package/wizard/lib/provider-wizard/run-steps.mjs +129 -0
  225. package/wizard/lib/providers/registry.mjs +262 -0
  226. package/wizard/lib/run-clack.mjs +108 -0
  227. package/wizard/lib/run-wizard.mjs +750 -0
  228. package/wizard/lib/validate-setup.mjs +62 -0
  229. package/wizard/lib/wizard-action.mjs +34 -0
  230. package/wizard/lib/wizard-conf.mjs +71 -0
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Step "9. Coding-agent plugin", Hindsight half: ensure ~/.hindsight/coding-agent.json
3
+ * exists (seeded from the tracked template when missing) and check that its optInPaths
4
+ * cover the indexed roots. Machine-local keys such as notebooklmAccess must live in a
5
+ * real file — never in the repo template — so we do not replace a materialised config
6
+ * with a symlink (that used to wipe notebooklmAccess.enabled after step 6).
7
+ * The meta wizard installs the plugin package itself from the manifest.
8
+ */
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+ import { realpathSync } from "node:fs";
12
+ import { installNotebooklmMcp } from "./notebooklm-mcp-install.mjs";
13
+ import { setNotebooklmAccessEnabled } from "../../../wizard/lib/notebooklm-agent-access.mjs";
14
+ import { ensureState } from "./state.mjs";
15
+
16
+ /** @typedef {import("../../../wizard/lib/provider-wizard/helpers.mjs").ProviderWizardHelpers} Helpers */
17
+
18
+ function realpathSafe(p) {
19
+ try {
20
+ return realpathSync(p);
21
+ } catch {
22
+ return path.resolve(p);
23
+ }
24
+ }
25
+
26
+ /** @param {string} filePath */
27
+ function readJsonSafe(filePath) {
28
+ try {
29
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ /** True when notebooklm.yml opts the sync service in. */
36
+ function notebooklmSyncEnabled(ctx) {
37
+ const yml = ctx.notebookFile || path.join(ctx.home, ".config/hindsight/notebooklm.yml");
38
+ if (!fs.existsSync(yml)) return false;
39
+ return /^enabled:\s*true\b/m.test(fs.readFileSync(yml, "utf8"));
40
+ }
41
+
42
+ /**
43
+ * Seed or keep a real coding-agent.json. Never leave (or create) a symlink to the shipped
44
+ * template — machine keys such as notebooklmAccess must not write into the repo.
45
+ *
46
+ * @param {Record<string, any>} ctx
47
+ * @param {Helpers} h
48
+ */
49
+ async function linkAgentConfig(ctx, h) {
50
+ const rel = ctx.memoryProvider.codingAgent?.configTemplate;
51
+ if (!rel) return;
52
+ const target = path.join(ctx.repoRoot, rel);
53
+ let stat;
54
+ try {
55
+ stat = fs.lstatSync(ctx.agentJson);
56
+ } catch {
57
+ stat = null;
58
+ }
59
+ if (stat?.isSymbolicLink()) {
60
+ // Copy current contents into a regular file so later writes never hit the template.
61
+ if (ctx.dry) {
62
+ h.info(`[dry-run] materialize ${ctx.agentJson} (symlink → regular file)`);
63
+ return;
64
+ }
65
+ const raw = fs.readFileSync(ctx.agentJson, "utf8");
66
+ fs.unlinkSync(ctx.agentJson);
67
+ fs.mkdirSync(path.dirname(ctx.agentJson), { recursive: true });
68
+ fs.writeFileSync(ctx.agentJson, raw.endsWith("\n") ? raw : `${raw}\n`);
69
+ h.ok(`materialized ${ctx.agentJson} (was symlink → regular file)`);
70
+ return;
71
+ }
72
+ if (stat?.isFile()) {
73
+ // Keep the real file. Replacing it with a symlink to the shipped template drops
74
+ // notebooklmAccess.enabled and breaks the NotebookLM MCP on the next Cursor start.
75
+ h.ok(`${ctx.agentJson} is a machine-local file (not replaced with a symlink)`);
76
+ return;
77
+ }
78
+ // Missing: copy the template into a real file so later notebooklmAccess writes never
79
+ // land in the repo.
80
+ if (ctx.dry) {
81
+ h.info(`[dry-run] create ${ctx.agentJson} from ${rel} (regular file, not a symlink)`);
82
+ return;
83
+ }
84
+ fs.mkdirSync(path.dirname(ctx.agentJson), { recursive: true });
85
+ const seeded = readJsonSafe(target) || {
86
+ serverMode: "self-hosted",
87
+ apiUrl: "http://127.0.0.1:8888",
88
+ optInOnly: true,
89
+ optInPaths: [],
90
+ mapPathToBank: {},
91
+ notebooklmAccess: { enabled: false, banks: ["*"] },
92
+ };
93
+ fs.writeFileSync(ctx.agentJson, `${JSON.stringify(seeded, null, 2)}\n`);
94
+ h.ok(`created ${ctx.agentJson} from ${rel}`);
95
+ }
96
+
97
+ /**
98
+ * If NotebookLM sync is on, keep notebooklmAccess.enabled and the Cursor MCP entry.
99
+ * Materialises a symlink into a real file first so the flag never writes into the template.
100
+ *
101
+ * @param {Record<string, any>} ctx
102
+ * @param {Helpers} h
103
+ */
104
+ async function ensureNotebooklmAccess(ctx, h) {
105
+ if (!notebooklmSyncEnabled(ctx)) return;
106
+ const templatePath = path.join(ctx.repoRoot, "config/coding-agent.json");
107
+ if (ctx.dry) {
108
+ h.info(`[dry-run] keep notebooklmAccess.enabled=true (NotebookLM sync is on)`);
109
+ return;
110
+ }
111
+ const result = setNotebooklmAccessEnabled(ctx.agentJson, templatePath, true);
112
+ h.ok(
113
+ `notebooklmAccess.enabled=true in ${ctx.agentJson}` +
114
+ (result.action === "materialized" ? " (materialized symlink → regular file)" : ""),
115
+ );
116
+ await installNotebooklmMcp(
117
+ { home: ctx.home, repoRoot: ctx.repoRoot, agentJson: ctx.agentJson, dry: false },
118
+ h,
119
+ );
120
+ }
121
+
122
+ /**
123
+ * @param {Record<string, any>} ctx
124
+ * @param {Helpers} h
125
+ * @returns {string[]}
126
+ */
127
+ function indexRoots(ctx, h) {
128
+ const source = fs.existsSync(ctx.indexFile)
129
+ ? ctx.indexFile
130
+ : path.join(ctx.repoRoot, "config/index.yml");
131
+ const roots = [];
132
+ for (const line of fs.readFileSync(source, "utf8").split("\n")) {
133
+ const m = line.match(/^\s*- path:\s*"?([^"]+)"?/);
134
+ if (m) roots.push(h.expandHome(m[1].trim(), ctx.home));
135
+ }
136
+ return roots;
137
+ }
138
+
139
+ /**
140
+ * @param {Record<string, any>} ctx
141
+ * @param {Helpers} h
142
+ */
143
+ async function checkOptIn(ctx, h) {
144
+ // Prefer the live agent config (real file or symlink target). Never write
145
+ // machine paths into the shipped template unless that is what agentJson resolves to.
146
+ let target = ctx.agentJson;
147
+ try {
148
+ if (fs.lstatSync(ctx.agentJson).isSymbolicLink()) {
149
+ target = realpathSafe(fs.readlinkSync(ctx.agentJson));
150
+ }
151
+ } catch {
152
+ target = path.join(ctx.repoRoot, "config/coding-agent.json");
153
+ }
154
+ if (!fs.existsSync(target)) {
155
+ target = path.join(ctx.repoRoot, "config/coding-agent.json");
156
+ }
157
+ const cfg = JSON.parse(fs.readFileSync(target, "utf8"));
158
+ if (!Array.isArray(cfg.optInPaths)) cfg.optInPaths = [];
159
+ const opt = cfg.optInPaths.map((p) => path.resolve(h.expandHome(p, ctx.home)));
160
+ const missing = [];
161
+ for (const root of indexRoots(ctx, h)) {
162
+ const abs = path.resolve(root);
163
+ const covered = opt.some((p) => abs === p || abs.startsWith(`${p.replace(/\/$/, "")}/`));
164
+ if (!covered) missing.push(root);
165
+ }
166
+ if (!missing.length) {
167
+ h.ok(`optInPaths in ${target} covers the indexed roots`);
168
+ return;
169
+ }
170
+ h.warn("optInPaths does not cover:");
171
+ for (const m of missing) h.info(` ${m}`);
172
+ if (ctx.dry) {
173
+ h.info(`[dry-run] offer to add these to optInPaths in ${target}`);
174
+ return;
175
+ }
176
+ const tracked = path.join(ctx.repoRoot, "config/coding-agent.json");
177
+ const prompt =
178
+ realpathSafe(target) === realpathSafe(tracked)
179
+ ? "Add them to optInPaths in the tracked file config/coding-agent.json (shows in git status)?"
180
+ : `Add them to optInPaths in ${target}?`;
181
+ if (await h.confirm(ctx, prompt, "n")) {
182
+ for (const root of missing) {
183
+ if (!cfg.optInPaths.includes(root)) cfg.optInPaths.push(root);
184
+ }
185
+ fs.writeFileSync(target, `${JSON.stringify(cfg, null, 2)}\n`);
186
+ h.ok(`updated ${target}`);
187
+ }
188
+ }
189
+
190
+ /**
191
+ * @param {Record<string, any>} ctx
192
+ * @param {Helpers} h
193
+ */
194
+ export async function setupCodingAgent(ctx, h) {
195
+ ensureState(ctx);
196
+ await linkAgentConfig(ctx, h);
197
+ await checkOptIn(ctx, h);
198
+ // Step 6 may have set notebooklmAccess on a real file; linkAgentConfig used to
199
+ // symlink over it. Re-assert after linking, then register the MCP.
200
+ await ensureNotebooklmAccess(ctx, h);
201
+ if (!notebooklmSyncEnabled(ctx)) {
202
+ await installNotebooklmMcp(ctx, h);
203
+ }
204
+ }
@@ -0,0 +1,79 @@
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
+ const meridian = /** @type {Record<string, unknown>} */ (model.meridian || {});
16
+ return {
17
+ apiPort: stack.api_port != null ? String(stack.api_port) : "",
18
+ cpPort: stack.control_plane_port != null ? String(stack.control_plane_port) : "",
19
+ modelBackend: String(model.backend || ""),
20
+ modelRetain: String(model.retain || ""),
21
+ modelReflect: String(model.reflect || ""),
22
+ modelApiBase: String(model.api_base || ""),
23
+ lemondHost: String(lemond.host || ""),
24
+ ollamaHost: String(ollama.host || ""),
25
+ meridianHost: String(meridian.host || ""),
26
+ };
27
+ }
28
+
29
+ /**
30
+ * Apply config defaults into process.env when env/CLI did not set them.
31
+ * @param {Record<string, unknown>} cfg
32
+ */
33
+ export function applyConfigEnv(cfg) {
34
+ const d = defaultsFromConfig(cfg);
35
+ if (d.apiPort && !process.env.HINDSIGHT_PORT) process.env.HINDSIGHT_PORT = d.apiPort;
36
+ if (d.cpPort && !process.env.HINDSIGHT_CP_PORT) process.env.HINDSIGHT_CP_PORT = d.cpPort;
37
+ if (d.modelBackend && !process.env.PM_BACKEND) process.env.PM_BACKEND = d.modelBackend;
38
+ if (d.modelRetain && !process.env.PM_DEFAULT_MODEL) {
39
+ process.env.PM_DEFAULT_MODEL = d.modelRetain;
40
+ }
41
+ if (d.modelReflect && !process.env.PM_REFLECT_MODEL) {
42
+ process.env.PM_REFLECT_MODEL = d.modelReflect;
43
+ }
44
+ if (d.modelApiBase && !process.env.MEMORY_API_BASE) {
45
+ process.env.MEMORY_API_BASE = d.modelApiBase;
46
+ }
47
+ if (d.lemondHost && !process.env.LEMOND_BASE) process.env.LEMOND_BASE = d.lemondHost;
48
+ if (d.meridianHost && !process.env.MERIDIAN_BASE) process.env.MERIDIAN_BASE = d.meridianHost;
49
+ }
50
+
51
+ /**
52
+ * Tables merged into config.toml when the wizard saves it.
53
+ * @param {Record<string, any>} ctx
54
+ * @returns {Record<string, Record<string, unknown>>}
55
+ */
56
+ export function configToml(ctx) {
57
+ const model = {
58
+ backend: ctx.backend,
59
+ retain: ctx.model,
60
+ reflect: ctx.reflectModel || "",
61
+ api_base: ctx.apiBase || "",
62
+ };
63
+ if (ctx.backend === "lemond" || ctx.lemondBase) {
64
+ model.lemond = { host: ctx.lemondBase };
65
+ }
66
+ if (ctx.backend === "ollama" || ctx.ollamaBase) {
67
+ model.ollama = { host: ctx.ollamaBase };
68
+ }
69
+ if (ctx.backend === "meridian" && ctx.meridianBase) {
70
+ model.meridian = { host: ctx.meridianBase };
71
+ }
72
+ return {
73
+ stack: {
74
+ api_port: Number(ctx.portApi || 8888),
75
+ control_plane_port: Number(ctx.portCp || 9999),
76
+ },
77
+ model,
78
+ };
79
+ }
@@ -0,0 +1,72 @@
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
+ // Own-route backends run from litellm.yaml; setup.mjs only knows lemond and ollama.
34
+ if (backend === "custom" || backend === "meridian") backend = "lemond";
35
+ /** @type {Record<string, string>} */
36
+ const env = { MEMORY_BACKEND: backend };
37
+ if (ctx.ollamaInCompose) env.MEMORY_OLLAMA_IN_COMPOSE = "1";
38
+ return {
39
+ description:
40
+ "scripts/setup.sh: write .env, litellm.env (mode 600 when new), compose.override.yaml",
41
+ env,
42
+ };
43
+ }
44
+
45
+ /**
46
+ * @param {Record<string, any>} ctx
47
+ * @param {Helpers} h
48
+ */
49
+ export async function setupEnv(ctx, h) {
50
+ ensureState(ctx);
51
+ const example = path.join(ctx.repoRoot, "config/hindsight.env.example");
52
+ if (fs.existsSync(ctx.envFile)) h.ok(`kept ${ctx.envFile}`);
53
+ else if (ctx.dry) h.info(`[dry-run] create ${ctx.envFile} from config/hindsight.env.example (mode 600)`);
54
+ else {
55
+ fs.copyFileSync(example, ctx.envFile);
56
+ fs.chmodSync(ctx.envFile, 0o600);
57
+ h.ok(`created ${ctx.envFile} (mode 600)`);
58
+ }
59
+ await h.runStackSetup(ctx);
60
+ if (ctx.dry) {
61
+ h.info(
62
+ `[dry-run] run setup.sh --migrate-env unless ${ctx.envFile} already uses the LiteLLM proxy and master key`,
63
+ );
64
+ } else if (llmWired(ctx, h)) h.ok("Hindsight calls the LiteLLM proxy with the master key");
65
+ else if (await h.confirm(ctx, `Point ${ctx.envFile} at the LiteLLM proxy now (backs up the file, prints no values)?`, "y")) {
66
+ await h.runStackSetup(ctx, {
67
+ description: "migrate hindsight.env",
68
+ args: ["--migrate-env"],
69
+ env: {},
70
+ });
71
+ } else h.warn("Hindsight is not wired to the LiteLLM proxy; the stack will not work until you migrate");
72
+ }
@@ -0,0 +1,196 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { configToml } from "./config.mjs";
4
+ import { renderMeridianRoutes } from "./meridian.mjs";
5
+ import { OLLAMA_HOST_API_BASE } from "./state.mjs";
6
+
7
+ /** @typedef {import("../../../wizard/lib/provider-wizard/helpers.mjs").ProviderWizardHelpers} Helpers */
8
+
9
+ /** Shipped litellm.<backend>.yaml api_base for the host-side model server. */
10
+ function shippedApiBase(backend) {
11
+ if (backend === "ollama") return OLLAMA_HOST_API_BASE;
12
+ if (backend === "lemond") return "http://host.docker.internal:13305/v1";
13
+ return "";
14
+ }
15
+
16
+ /**
17
+ * @param {Record<string, any>} ctx
18
+ * @param {string} alias
19
+ * @param {string} litellmModel
20
+ */
21
+ function renderRoute(ctx, alias, litellmModel) {
22
+ let out = ` - model_name: ${alias}\n litellm_params:\n model: ${litellmModel}\n api_base: ${ctx.apiBase}\n`;
23
+ if (ctx.backend === "ollama") {
24
+ out += " extra_body:\n think: false\n";
25
+ } else {
26
+ out += " api_key: os.environ/LEMOND_API_KEY\n extra_body:\n chat_template_kwargs:\n enable_thinking: false\n";
27
+ }
28
+ return out;
29
+ }
30
+
31
+ /**
32
+ * @param {Record<string, any>} ctx
33
+ * @param {string} litellmModel
34
+ */
35
+ function renderOverride(ctx, litellmModel) {
36
+ return (
37
+ "# Written by ./setup. Replaces config/litellm.*.yaml while this file exists.\n" +
38
+ "# Delete it, or rerun ./setup with the shipped model, to use the shipped routes again.\n" +
39
+ "# Thinking is off for all three aliases. Non-ollama servers get chat_template_kwargs; a server that rejects it needs a different route.\n" +
40
+ "model_list:\n" +
41
+ renderRoute(ctx, "memory-retain", litellmModel) +
42
+ renderRoute(ctx, "memory-reflect", litellmModel) +
43
+ renderRoute(ctx, "memory-consolidate", litellmModel) +
44
+ "general_settings:\n master_key: os.environ/LITELLM_MASTER_KEY\n"
45
+ );
46
+ }
47
+
48
+ /**
49
+ * @param {Record<string, any>} ctx
50
+ * @param {string} litellmModel
51
+ * @param {string} reflectLitellmModel
52
+ */
53
+ function renderShipped(ctx, litellmModel, reflectLitellmModel) {
54
+ const src = path.join(ctx.repoRoot, "config", `litellm.${ctx.backend}.yaml`);
55
+ const lines = fs.readFileSync(src, "utf8").split("\n");
56
+ const out = [
57
+ `# Written by ./setup from config/litellm.${ctx.backend}.yaml. Replaces the shipped routes while this file exists.`,
58
+ "# Delete it, or rerun ./setup with the shipped models, to use the shipped routes again.",
59
+ ];
60
+ let name = "";
61
+ for (const line of lines) {
62
+ const mn = line.match(/^ - model_name: (\S+)/);
63
+ if (mn) name = mn[1];
64
+ if (/^ api_base: /.test(line)) {
65
+ out.push(` api_base: ${ctx.apiBase}`);
66
+ continue;
67
+ }
68
+ if (/^ model: /.test(line)) {
69
+ if (name !== "memory-reflect") out.push(` model: ${litellmModel}`);
70
+ else if (reflectLitellmModel) out.push(` model: ${reflectLitellmModel}`);
71
+ else out.push(line);
72
+ continue;
73
+ }
74
+ out.push(line);
75
+ }
76
+ return `${out.join("\n")}\n`;
77
+ }
78
+
79
+ /**
80
+ * @param {Record<string, any>} ctx
81
+ * @returns {string}
82
+ */
83
+ export function litellmConfig(ctx) {
84
+ if (ctx.backend === "meridian") return renderMeridianRoutes(ctx);
85
+ let prefix = "openai/";
86
+ if (ctx.backend === "ollama") prefix = "ollama_chat/";
87
+ if (ctx.backend === "custom") return renderOverride(ctx, `${prefix}${ctx.model}`);
88
+ let reflect = "";
89
+ if (ctx.reflectModel) reflect = `${prefix}${ctx.reflectModel}`;
90
+ return renderShipped(ctx, `${prefix}${ctx.model}`, reflect);
91
+ }
92
+
93
+ /**
94
+ * @param {Record<string, any>} ctx
95
+ * @param {Helpers} h
96
+ */
97
+ export async function removeOverrideIfAsked(ctx, h) {
98
+ if (!fs.existsSync(ctx.litellmOverride)) return;
99
+ if (ctx.dry) {
100
+ h.info(`[dry-run] move ${ctx.litellmOverride} aside so the shipped ${ctx.backend} routes apply`);
101
+ return;
102
+ }
103
+ if (!(await h.confirm(ctx, `Move ${ctx.litellmOverride} aside so the shipped ${ctx.backend} routes apply?`, "y"))) {
104
+ h.warn("kept the override; it still takes precedence over the shipped routes");
105
+ return;
106
+ }
107
+ const bak = `${ctx.litellmOverride}.bak-${timestamp()}`;
108
+ fs.renameSync(ctx.litellmOverride, bak);
109
+ h.ok("moved the override aside");
110
+ }
111
+
112
+ /**
113
+ * @param {Record<string, any>} ctx
114
+ * @param {Helpers} h
115
+ */
116
+ export async function writeOverride(ctx, h) {
117
+ if (ctx.dry && ctx.backend === "meridian") {
118
+ const backup = fs.existsSync(ctx.litellmOverride) ? " (backs up the existing file)" : "";
119
+ h.info(
120
+ `[dry-run] write ${ctx.litellmOverride}${backup}: Meridian routes, retain and consolidate -> anthropic/${ctx.model}, reflect -> anthropic/${ctx.reflectModel || ctx.model}, at ${ctx.apiBase}`,
121
+ );
122
+ return;
123
+ }
124
+ if (ctx.dry) {
125
+ h.info(
126
+ `[dry-run] write ${ctx.litellmOverride} from config/litellm.${ctx.backend}.yaml: retain and consolidate -> ${ctx.model}, reflect -> ${ctx.reflectModel || "shipped"}, at ${ctx.apiBase}`,
127
+ );
128
+ return;
129
+ }
130
+ h.ensureConfigDir(ctx);
131
+ const content = litellmConfig(ctx);
132
+ const tmp = path.join(ctx.hsDir, `.litellm.yaml.${process.pid}`);
133
+ fs.writeFileSync(tmp, content, { mode: 0o644 });
134
+ if (fs.existsSync(ctx.litellmOverride)) {
135
+ const existing = fs.readFileSync(ctx.litellmOverride);
136
+ if (Buffer.compare(existing, Buffer.from(content)) === 0) {
137
+ fs.unlinkSync(tmp);
138
+ h.ok(`${ctx.litellmOverride} already has this route`);
139
+ return;
140
+ }
141
+ if (!(await h.confirm(ctx, `Replace ${ctx.litellmOverride} (the old file is kept as .bak)?`, "y"))) {
142
+ fs.unlinkSync(tmp);
143
+ h.warn("kept the existing override; it still takes precedence");
144
+ return;
145
+ }
146
+ fs.copyFileSync(ctx.litellmOverride, `${ctx.litellmOverride}.bak-${timestamp()}`);
147
+ }
148
+ fs.renameSync(tmp, ctx.litellmOverride);
149
+ h.ok(`wrote ${ctx.litellmOverride}`);
150
+ }
151
+
152
+ /**
153
+ * @param {Record<string, any>} ctx
154
+ * @param {Helpers} h
155
+ */
156
+ export async function applyRoute(ctx, h) {
157
+ let shipped = "";
158
+ if (ctx.backend === "lemond") shipped = ctx.lemondDefaultModel;
159
+ if (ctx.backend === "ollama") shipped = ctx.ollamaDefaultModel;
160
+ const defaultBase = shippedApiBase(ctx.backend);
161
+ const apiMatchesShipped = !ctx.apiBase || ctx.apiBase === defaultBase;
162
+ const ownRoute = ctx.backend === "custom" || ctx.backend === "meridian";
163
+ if (!ownRoute && ctx.model === shipped && !ctx.reflectModel && apiMatchesShipped) {
164
+ await removeOverrideIfAsked(ctx, h);
165
+ } else {
166
+ await writeOverride(ctx, h);
167
+ }
168
+ }
169
+
170
+ function timestamp() {
171
+ const d = new Date();
172
+ const p = (n) => String(n).padStart(2, "0");
173
+ return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
174
+ }
175
+
176
+ /**
177
+ * Record the backend choice in wizard.conf (and config.toml).
178
+ * @param {Record<string, any>} ctx
179
+ * @param {Helpers} h
180
+ */
181
+ export function saveConf(ctx, h) {
182
+ if (ctx.dry) {
183
+ h.info(`[dry-run] record backend ${ctx.backend} in ${ctx.wizardConf}`);
184
+ return;
185
+ }
186
+ /** @type {Record<string, string>} */
187
+ const keys = {
188
+ MEMORY_BACKEND: ctx.backend,
189
+ MEMORY_MODEL: ctx.model,
190
+ MEMORY_API_BASE: ctx.apiBase,
191
+ MEMORY_REFLECT_MODEL: ctx.reflectModel,
192
+ MEMORY_OLLAMA_IN_COMPOSE: ctx.ollamaInCompose ? "1" : "0",
193
+ };
194
+ if (ctx.backend === "meridian") keys.MEMORY_MERIDIAN_URL = ctx.meridianBase;
195
+ h.saveWizardConf(ctx, keys, { pmSections: configToml(ctx) });
196
+ }