@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,214 @@
1
+ /**
2
+ * NotebookLM opt-in during setup: install the CLI if needed, run `notebooklm login`
3
+ * (browser) when there is no valid session, then inject NOTEBOOKLM_HOME so host
4
+ * commands and wizard.conf agree with the compose bind mount (~/.config/notebooklm).
5
+ * After sync is enabled, optionally opt in coding-agent MCP access (notebooklm_list_banks /
6
+ * notebooklm_search).
7
+ */
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+ import { spawnSync } from "node:child_process";
11
+ import { step, info, ok, warn } from "./log.mjs";
12
+ import { act, commandExists } from "./exec.mjs";
13
+ import { confirm } from "./prompts.mjs";
14
+ import { upsertWizardConf } from "./wizard-conf.mjs";
15
+ import { setNotebooklmAccessEnabled } from "./notebooklm-agent-access.mjs";
16
+ import { installNotebooklmMcp } from "../../providers/hindsight/lib/notebooklm-mcp-install.mjs";
17
+
18
+ /** @typedef {import("./run-wizard.mjs").WizardContext} WizardContext */
19
+
20
+ const MCP_CONFIRM =
21
+ "Allow coding agents to query NotebookLM banks via MCP (notebooklm_list_banks / notebooklm_search)?";
22
+
23
+ /** @param {WizardContext} ctx */
24
+ function codingAgentPaths(ctx) {
25
+ const agentPath = ctx.agentJson || path.join(ctx.home, ".hindsight", "coding-agent.json");
26
+ const templatePath = path.join(ctx.repoRoot, "config/coding-agent.json");
27
+ return { agentPath, templatePath };
28
+ }
29
+
30
+ /**
31
+ * After NotebookLM sync was enabled this run: opt-in MCP access (default yes).
32
+ * @param {WizardContext} ctx
33
+ */
34
+ export async function offerNotebooklmMcpAccess(ctx) {
35
+ const allow = await confirm(ctx, MCP_CONFIRM, "y");
36
+ const { agentPath, templatePath } = codingAgentPaths(ctx);
37
+ const h = { info, ok, warn };
38
+
39
+ if (!allow) {
40
+ if (ctx.dry) {
41
+ info(`[dry-run] leave notebooklmAccess.enabled false; skip MCP merge (${agentPath})`);
42
+ return false;
43
+ }
44
+ if (fs.existsSync(agentPath)) {
45
+ try {
46
+ const st = fs.lstatSync(agentPath);
47
+ if (st.isFile() || st.isSymbolicLink()) {
48
+ setNotebooklmAccessEnabled(agentPath, templatePath, false);
49
+ ok(`notebooklmAccess.enabled=false in ${agentPath}`);
50
+ }
51
+ } catch (err) {
52
+ warn(`could not clear notebooklmAccess.enabled: ${err instanceof Error ? err.message : err}`);
53
+ }
54
+ } else {
55
+ info("notebooklmAccess left disabled (no coding-agent.json yet)");
56
+ }
57
+ return false;
58
+ }
59
+
60
+ if (ctx.dry) {
61
+ const plan = setNotebooklmAccessEnabled(agentPath, templatePath, true, { dry: true });
62
+ info(
63
+ `[dry-run] ${plan.action} ${agentPath} with notebooklmAccess.enabled=true; merge notebooklm MCP into ~/.cursor/mcp.json`,
64
+ );
65
+ return true;
66
+ }
67
+
68
+ const result = setNotebooklmAccessEnabled(agentPath, templatePath, true);
69
+ ok(
70
+ `notebooklmAccess.enabled=true in ${agentPath}` +
71
+ (result.action === "materialized" ? " (copied from symlink → regular file)" : ""),
72
+ );
73
+ await installNotebooklmMcp(
74
+ { home: ctx.home, repoRoot: ctx.repoRoot, agentJson: agentPath, dry: false },
75
+ h,
76
+ );
77
+ ok("Restart Cursor Agent so it picks up the notebooklm MCP");
78
+ return true;
79
+ }
80
+
81
+ /** Host path the compose service bind-mounts as /data/notebooklm. */
82
+ export function notebooklmHome(ctx) {
83
+ return path.join(ctx.home, ".config/notebooklm");
84
+ }
85
+
86
+ /** Env for every host `notebooklm` child: NOTEBOOKLM_HOME points at the shared cookie dir. */
87
+ export function notebooklmEnv(ctx) {
88
+ return { ...process.env, NOTEBOOKLM_HOME: notebooklmHome(ctx) };
89
+ }
90
+
91
+ /**
92
+ * Persist NOTEBOOKLM_HOME into the process env and wizard.conf (compose already mounts the path).
93
+ * @param {WizardContext} ctx
94
+ */
95
+ export function injectNotebooklmHome(ctx) {
96
+ const home = notebooklmHome(ctx);
97
+ process.env.NOTEBOOKLM_HOME = home;
98
+ upsertWizardConf(ctx, { NOTEBOOKLM_HOME: home });
99
+ ok(`NOTEBOOKLM_HOME=${home} (process env + wizard.conf)`);
100
+ }
101
+
102
+ /**
103
+ * @param {WizardContext} ctx
104
+ * @returns {boolean}
105
+ */
106
+ export function notebooklmAuthOk(ctx) {
107
+ if (!commandExists("notebooklm")) return false;
108
+ const check = spawnSync("notebooklm", ["auth", "check", "--test", "--json"], {
109
+ env: notebooklmEnv(ctx),
110
+ stdio: "ignore",
111
+ });
112
+ return check.status === 0;
113
+ }
114
+
115
+ /**
116
+ * Install notebooklm-py + Playwright Chromium via uv when the CLI is missing.
117
+ * @param {WizardContext} ctx
118
+ * @returns {Promise<boolean>} true when `notebooklm` is on PATH afterwards
119
+ */
120
+ export async function ensureNotebooklmCli(ctx) {
121
+ if (commandExists("notebooklm")) return true;
122
+ if (!commandExists("uv")) {
123
+ warn("uv is not on PATH; install uv, then rerun setup to enable NotebookLM");
124
+ return false;
125
+ }
126
+ if (ctx.dry) {
127
+ info(`[dry-run] uv tool install ${ctx.notebooklmPkg} && playwright install chromium`);
128
+ return false;
129
+ }
130
+ info(`installing ${ctx.notebooklmPkg} (includes Playwright Chromium)`);
131
+ await act(ctx, `uv tool install ${ctx.notebooklmPkg}`, "uv", ["tool", "install", ctx.notebooklmPkg]);
132
+ await act(
133
+ ctx,
134
+ "playwright install chromium for notebooklm-py",
135
+ "uv",
136
+ ["tool", "run", "--from", ctx.notebooklmPkg, "python", "-m", "playwright", "install", "chromium"],
137
+ );
138
+ if (!commandExists("notebooklm")) {
139
+ warn("notebooklm still not on PATH after install (check ~/.local/bin)");
140
+ return false;
141
+ }
142
+ return true;
143
+ }
144
+
145
+ /**
146
+ * Open the Google sign-in browser via notebooklm-py. Stdio is inherited so the
147
+ * operator can complete login; no separate "are you done?" prompt.
148
+ * @param {WizardContext} ctx
149
+ * @returns {Promise<boolean>}
150
+ */
151
+ export async function runNotebooklmLogin(ctx) {
152
+ if (ctx.dry) {
153
+ info("[dry-run] notebooklm login (opens a browser; NOTEBOOKLM_HOME set)");
154
+ return false;
155
+ }
156
+ if (ctx.yes) {
157
+ warn("skipping notebooklm login under --yes (needs a browser); log in interactively or rerun without --yes");
158
+ return false;
159
+ }
160
+ if (!process.stdin.isTTY) {
161
+ warn("skipping notebooklm login (no TTY); run setup interactively once");
162
+ return false;
163
+ }
164
+ fs.mkdirSync(notebooklmHome(ctx), { recursive: true, mode: 0o700 });
165
+ info("opening Google sign-in for NotebookLM (complete it in the browser window)…");
166
+ const r = spawnSync("notebooklm", ["login"], {
167
+ env: notebooklmEnv(ctx),
168
+ stdio: "inherit",
169
+ });
170
+ return r.status === 0;
171
+ }
172
+
173
+ /**
174
+ * @param {WizardContext} ctx
175
+ * @param {(c: WizardContext) => void} enableNotebooklm
176
+ */
177
+ export async function setupNotebooklm(ctx, enableNotebooklm) {
178
+ step("6. NotebookLM sync (optional, default no)");
179
+ if (!(await confirm(ctx, "Enable the NotebookLM sync? It stores a Google session cookie on this machine", "n"))) {
180
+ info("skipped; the service stays off");
181
+ return;
182
+ }
183
+ if (ctx.dry) {
184
+ info("[dry-run] install notebooklm-py if needed, login if auth fails, inject NOTEBOOKLM_HOME, enable notebooklm.yml");
185
+ await offerNotebooklmMcpAccess(ctx);
186
+ return;
187
+ }
188
+
189
+ fs.mkdirSync(notebooklmHome(ctx), { recursive: true, mode: 0o700 });
190
+ injectNotebooklmHome(ctx);
191
+
192
+ if (!(await ensureNotebooklmCli(ctx))) return;
193
+
194
+ if (notebooklmAuthOk(ctx)) {
195
+ ok("NotebookLM session already valid");
196
+ enableNotebooklm(ctx);
197
+ await offerNotebooklmMcpAccess(ctx);
198
+ return;
199
+ }
200
+
201
+ info("no valid NotebookLM session yet; starting login…");
202
+ const loggedIn = await runNotebooklmLogin(ctx);
203
+ if (!loggedIn) {
204
+ warn(`login did not finish; ${ctx.notebookFile} is unchanged. Rerun setup when ready`);
205
+ return;
206
+ }
207
+ if (!notebooklmAuthOk(ctx)) {
208
+ warn(`auth check failed after login; ${ctx.notebookFile} is unchanged`);
209
+ return;
210
+ }
211
+ ok("NotebookLM login ok");
212
+ enableNotebooklm(ctx);
213
+ await offerNotebooklmMcpAccess(ctx);
214
+ }
@@ -0,0 +1,87 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import { die } from "./log.mjs";
4
+ import { parseActionFromArgv } from "./wizard-action.mjs";
5
+
6
+ const HELP = `Setup wizard: takes a fresh clone to a running memory stack for the selected provider. Symlinked as ./setup at the repo root.
7
+ Re-runnable: shows current values as defaults and asks before it replaces config or secrets.
8
+
9
+ ./setup interactive (@clack/prompts UI, then the rest)
10
+ ./setup --yes accept every default, no prompts
11
+ ./setup --dry-run [--yes] print the actions; read-only checks run, nothing is written or started
12
+ ./setup --yes --no-smoke skip the smoke retain/recall (for scripted re-runs)
13
+ ./setup --rebuild rebuild the images even when their sources are unchanged
14
+ ./setup --up [--yes] start the stack using existing wizard.conf
15
+ ./setup --down [--yes] stop the stack (docker compose down)
16
+
17
+ PM_PROJECT sets the compose project name (default: openmem). MEMORY_PROVIDER selects the
18
+ memory plugin (default: hindsight). PM_WIZARD_ACTION=configure|up|down. Each provider documents its
19
+ own environment variables (model backend, ports) in providers/<id>/. PM_STACK_ROOT and PM_PACKAGE_ROOT
20
+ split the mutable stack directory from the read-only code directory. The last line is
21
+ "wizard: changed=N": files and containers this run altered.
22
+ Test against a scratch HOME:
23
+ HOME=/tmp/wiz-home PM_PROJECT=pm-wizard-test ./setup --yes`;
24
+
25
+ /**
26
+ * @param {string} repoRoot
27
+ * @param {string[]} argv
28
+ * @returns {import("./run-wizard.mjs").WizardContext}
29
+ */
30
+ export function parseWizardFlags(repoRoot, argv) {
31
+ const home = process.env.HOME || os.homedir();
32
+
33
+ const ctx = {
34
+ repoRoot,
35
+ // Read-only code root (wizard/, providers/, scripts/); the stack root is repoRoot.
36
+ packageRoot: process.env.PM_PACKAGE_ROOT ? path.resolve(process.env.PM_PACKAGE_ROOT) : repoRoot,
37
+ home,
38
+ project: process.env.PM_PROJECT || "openmem",
39
+ // Shared per-machine config dir: wizard.conf, index.yml, notebooklm.yml. The name predates
40
+ // provider plugins and stays fixed so existing machines keep their config.
41
+ hsDir: path.join(home, ".config/hindsight"),
42
+ indexFile: path.join(home, ".config/hindsight/index.yml"),
43
+ notebookFile: path.join(home, ".config/hindsight/notebooklm.yml"),
44
+ wizardConf: path.join(home, ".config/hindsight/wizard.conf"),
45
+ smokeBank: "wizard-smoke",
46
+ buildHashFile: path.join(repoRoot, ".build-hash"),
47
+ notebooklmPkg: "notebooklm-py[browser]==0.8.5",
48
+ yes: false,
49
+ dry: false,
50
+ smoke: true,
51
+ rebuild: false,
52
+ // True when the Clack UI already asked the provider's questions (WIZARD_SKIP_BACKEND_STEP=1).
53
+ skipBackendStep: process.env.WIZARD_SKIP_BACKEND_STEP === "1",
54
+ action: parseActionFromArgv(argv),
55
+ providerId: "",
56
+ };
57
+
58
+ for (const arg of argv) {
59
+ switch (arg) {
60
+ case "--up":
61
+ case "--down":
62
+ break;
63
+ case "--yes":
64
+ case "-y":
65
+ ctx.yes = true;
66
+ break;
67
+ case "--dry-run":
68
+ ctx.dry = true;
69
+ break;
70
+ case "--no-smoke":
71
+ ctx.smoke = false;
72
+ break;
73
+ case "--rebuild":
74
+ ctx.rebuild = true;
75
+ break;
76
+ case "-h":
77
+ case "--help":
78
+ console.log(HELP);
79
+ process.exit(0);
80
+ break;
81
+ default:
82
+ die(`unknown option: ${arg} (see --help)`);
83
+ }
84
+ }
85
+
86
+ return ctx;
87
+ }
@@ -0,0 +1,39 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+
5
+ export function expandHome(p, home = process.env.HOME || os.homedir()) {
6
+ if (p === "~") return home;
7
+ if (p.startsWith("~/")) return path.join(home, p.slice(2));
8
+ return p;
9
+ }
10
+
11
+ export function envValue(filePath, key) {
12
+ if (!fs.existsSync(filePath)) return "";
13
+ const prefix = `${key}=`;
14
+ for (const line of fs.readFileSync(filePath, "utf8").split("\n")) {
15
+ if (line.startsWith(prefix)) return line.slice(prefix.length);
16
+ }
17
+ return "";
18
+ }
19
+
20
+ export function ensureHsDir(ctx) {
21
+ if (ctx.dry) return;
22
+ fs.mkdirSync(ctx.hsDir, { recursive: true, mode: 0o700 });
23
+ }
24
+
25
+ export function realpathSafe(p) {
26
+ try {
27
+ return fs.realpathSync(p);
28
+ } catch {
29
+ return path.resolve(p);
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Mount point for the shared indexer's state. The directory name predates provider
35
+ * plugins and stays fixed so existing state keeps working.
36
+ */
37
+ export function indexerStateDir(ctx) {
38
+ return path.join(ctx.home, ".cache", "hindsight-project-index");
39
+ }
@@ -0,0 +1,163 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import TOML from "@iarna/toml";
4
+ import { DEFAULT_PROVIDER_ID } from "./providers/registry.mjs";
5
+
6
+ /** @param {string} home */
7
+ export function userConfigPath(home) {
8
+ return path.join(home, ".config/personal-memory/config.toml");
9
+ }
10
+
11
+ /** @param {string} repoRoot */
12
+ export function projectConfigPaths(repoRoot) {
13
+ return [
14
+ path.join(repoRoot, "openmem.toml"),
15
+ path.join(repoRoot, "config/openmem.toml"),
16
+ // Pre-rebrand filenames still load if present.
17
+ path.join(repoRoot, "personal-memory.toml"),
18
+ path.join(repoRoot, "config/personal-memory.toml"),
19
+ ];
20
+ }
21
+
22
+ /**
23
+ * @param {string} file
24
+ * @returns {Record<string, unknown>}
25
+ */
26
+ function readTomlFile(file) {
27
+ if (!fs.existsSync(file)) return {};
28
+ try {
29
+ return TOML.parse(fs.readFileSync(file, "utf8"));
30
+ } catch (e) {
31
+ throw new Error(`${file}: ${/** @type {Error} */ (e).message}`);
32
+ }
33
+ }
34
+
35
+ /** @param {Record<string, unknown>} base @param {Record<string, unknown>} over */
36
+ function deepMerge(base, over) {
37
+ const out = { ...base };
38
+ for (const [k, v] of Object.entries(over)) {
39
+ if (v && typeof v === "object" && !Array.isArray(v) && typeof out[k] === "object") {
40
+ out[k] = deepMerge(/** @type {Record<string, unknown>} */ (out[k]), /** @type {Record<string, unknown>} */ (v));
41
+ } else out[k] = v;
42
+ }
43
+ return out;
44
+ }
45
+
46
+ /**
47
+ * @param {string} home
48
+ * @param {string} repoRoot
49
+ * @returns {{ merged: Record<string, unknown>, userPath: string, wroteFrom: string[] }}
50
+ */
51
+ export function loadPmConfig(home, repoRoot) {
52
+ let merged = {};
53
+ const wroteFrom = [];
54
+ for (const p of projectConfigPaths(repoRoot)) {
55
+ if (fs.existsSync(p)) {
56
+ merged = deepMerge(merged, readTomlFile(p));
57
+ wroteFrom.push(p);
58
+ }
59
+ }
60
+ const userPath = userConfigPath(home);
61
+ if (fs.existsSync(userPath)) {
62
+ merged = deepMerge(merged, readTomlFile(userPath));
63
+ wroteFrom.push(userPath);
64
+ }
65
+ return { merged, userPath, wroteFrom };
66
+ }
67
+
68
+ /**
69
+ * Flat defaults for the wizard (--yes / Clack initialValue). Provider-specific tables
70
+ * ([model], provider ports) are read by the provider's own hooks from `cfg`.
71
+ * @param {Record<string, unknown>} cfg
72
+ */
73
+ export function defaultsFromPmConfig(cfg) {
74
+ const memory = /** @type {Record<string, unknown>} */ (cfg.memory || {});
75
+ const stack = /** @type {Record<string, unknown>} */ (cfg.stack || {});
76
+ const wizard = /** @type {Record<string, unknown>} */ (cfg.wizard || {});
77
+
78
+ return {
79
+ memoryProvider: String(memory.provider || ""),
80
+ stackProject: String(stack.project || ""),
81
+ wizardAction: String(wizard.action || ""),
82
+ };
83
+ }
84
+
85
+ /**
86
+ * @param {import("./run-wizard.mjs").WizardContext} ctx
87
+ * @param {Record<string, Record<string, unknown>>} [sections] provider-owned tables:
88
+ * `stack` keys are merged into [stack]; `memory` sub-tables (for example `memory.mem0`) are
89
+ * merged into [memory], and `provider` there is always the active provider id; every other
90
+ * key becomes its own top-level table
91
+ * @returns {string}
92
+ */
93
+ export function buildPmConfigToml(ctx, sections = {}) {
94
+ const { stack: stackExtra = {}, memory: memoryExtra = {}, ...rest } = sections;
95
+ const doc = {
96
+ memory: { ...memoryExtra, provider: ctx.memoryProvider?.id || DEFAULT_PROVIDER_ID },
97
+ stack: { project: ctx.project, ...stackExtra },
98
+ wizard: { action: ctx.action || "configure" },
99
+ ...rest,
100
+ tasks: {
101
+ configure: { description: "Secrets, index sources, build images, compose up" },
102
+ up: { description: "Start stack using existing config" },
103
+ down: { description: "docker compose down for this project" },
104
+ doctor: { description: "Validate memory setup on this machine" },
105
+ },
106
+ };
107
+ return `# Written by ./setup — mise-like config for openmem.\n# Docs: docs/openmem-config.md\n${TOML.stringify(doc)}`;
108
+ }
109
+
110
+ /**
111
+ * @param {import("./run-wizard.mjs").WizardContext} ctx
112
+ * @param {Record<string, Record<string, unknown>>} [sections] see buildPmConfigToml
113
+ */
114
+ export function syncPmConfig(ctx, sections = {}) {
115
+ if (ctx.dry) return;
116
+ const dir = path.dirname(userConfigPath(ctx.home));
117
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
118
+ const file = userConfigPath(ctx.home);
119
+ fs.writeFileSync(file, buildPmConfigToml(ctx, sections), { mode: 0o600 });
120
+ }
121
+
122
+ /**
123
+ * Minimal in-place update of `memory.provider` in the user config.toml when the file
124
+ * already exists. Does not create the file (unlike syncPmConfig).
125
+ *
126
+ * @param {string} home
127
+ * @param {string} providerId
128
+ * @param {{ dry?: boolean }} [opts]
129
+ * @returns {"updated"|"skipped"|"dry-run"}
130
+ */
131
+ export function updatePmConfigProvider(home, providerId, opts = {}) {
132
+ const file = userConfigPath(home);
133
+ if (!fs.existsSync(file)) return "skipped";
134
+ if (opts.dry) return "dry-run";
135
+ const doc = readTomlFile(file);
136
+ const memory = {
137
+ ...(doc.memory && typeof doc.memory === "object" && !Array.isArray(doc.memory)
138
+ ? /** @type {Record<string, unknown>} */ (doc.memory)
139
+ : {}),
140
+ provider: providerId,
141
+ };
142
+ doc.memory = memory;
143
+ const header =
144
+ "# Written by openmem — mise-like config.\n# Docs: docs/openmem-config.md\n";
145
+ fs.writeFileSync(file, `${header}${TOML.stringify(doc)}`, { mode: 0o600 });
146
+ return "updated";
147
+ }
148
+
149
+ /**
150
+ * Apply config defaults into process.env when env/CLI did not set them. Only the
151
+ * provider-neutral keys live here; a provider maps its own tables in its `init` hook.
152
+ * @param {Record<string, unknown>} cfg
153
+ */
154
+ export function applyPmConfigEnv(cfg) {
155
+ const d = defaultsFromPmConfig(cfg);
156
+ if (d.memoryProvider && !process.env.MEMORY_PROVIDER) {
157
+ process.env.MEMORY_PROVIDER = d.memoryProvider;
158
+ }
159
+ if (d.stackProject && !process.env.PM_PROJECT) process.env.PM_PROJECT = d.stackProject;
160
+ if (d.wizardAction && !process.env.PM_WIZARD_ACTION) {
161
+ process.env.PM_WIZARD_ACTION = d.wizardAction;
162
+ }
163
+ }
@@ -0,0 +1,94 @@
1
+ import net from "node:net";
2
+ import path from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+ import { envValue } from "./paths.mjs";
5
+ import { ok, warn, die } from "./log.mjs";
6
+ import { confirm } from "./prompts.mjs";
7
+
8
+ /**
9
+ * @param {number} port
10
+ * @returns {Promise<boolean>}
11
+ */
12
+ export function portFree(port) {
13
+ return new Promise((resolve) => {
14
+ const server = net.createServer();
15
+ server.unref();
16
+ server.on("error", () => resolve(false));
17
+ server.listen({ port, host: "127.0.0.1", exclusive: true }, () => {
18
+ server.close(() => resolve(true));
19
+ });
20
+ });
21
+ }
22
+
23
+ /**
24
+ * Keep a host port when it is free (or held by this compose project); otherwise offer
25
+ * the next free one.
26
+ * @param {import("./run-wizard.mjs").WizardContext} ctx
27
+ * @param {string} label
28
+ * @param {number} port
29
+ * @param {{ envVars?: string[] }} [opts] env var names that override the ports, for messages
30
+ * @returns {Promise<number>}
31
+ */
32
+ export async function choosePort(ctx, label, port, opts = {}) {
33
+ const envVars = opts.envVars || [];
34
+ if (await portFree(port)) {
35
+ ok(`${label} port ${port} is free`);
36
+ return port;
37
+ }
38
+ let owner = "";
39
+ const ps = spawnSync(
40
+ "docker",
41
+ ["ps", "--filter", `publish=${port}`, "--format", "{{.Label \"com.docker.compose.project\"}}"],
42
+ { encoding: "utf8" },
43
+ );
44
+ if (ps.status === 0 && ps.stdout) {
45
+ owner = ps.stdout.split("\n").find((l) => l.trim())?.trim() || "";
46
+ }
47
+ if (owner === ctx.project) {
48
+ ok(`${label} port ${port} is held by this project (${ctx.project})`);
49
+ return port;
50
+ }
51
+ if (owner) warn(`${label} port ${port} is held by compose project '${owner}'`);
52
+ else warn(`${label} port ${port} is held by another process`);
53
+
54
+ let alt = 0;
55
+ for (let i = 1; i <= 50; i++) {
56
+ if (await portFree(port + i)) {
57
+ alt = port + i;
58
+ break;
59
+ }
60
+ }
61
+ if (!alt) {
62
+ die(
63
+ `no free port near ${port}${envVars.length ? `; set ${envVars.join(" or ")} and` : ";"} rerun`,
64
+ );
65
+ }
66
+ if (await confirm(ctx, `Use port ${alt} for the ${label} instead (saved to ${path.join(ctx.repoRoot, ".env")})?`, "y")) {
67
+ return alt;
68
+ }
69
+ die(`free port ${port}${envVars.length ? ` (or set ${envVars.join(" / ")})` : ""} and rerun ./setup`);
70
+ }
71
+
72
+ /**
73
+ * Resolve one host port from the active provider's manifest (`compose.hostPortEnv`,
74
+ * `compose.defaultHostPort`): process env, then the stack .env, then the default.
75
+ * Exports the chosen port in process.env under the manifest's variable name.
76
+ * @param {import("./run-wizard.mjs").WizardContext} ctx
77
+ * @param {string} [label]
78
+ * @returns {Promise<number>}
79
+ */
80
+ export async function resolveHostPort(ctx, label) {
81
+ const compose = ctx.memoryProvider?.compose || {};
82
+ const envName = compose.hostPortEnv;
83
+ if (!envName) die(`provider ${ctx.memoryProvider?.id} declares no compose.hostPortEnv`);
84
+ const wanted =
85
+ process.env[envName] ||
86
+ envValue(path.join(ctx.repoRoot, ".env"), envName) ||
87
+ String(compose.defaultHostPort ?? 8888);
88
+ if (!/^\d+$/.test(wanted)) die(`${envName} must be a port number`);
89
+ const port = await choosePort(ctx, label || `${ctx.memoryProvider.label} API`, Number(wanted), {
90
+ envVars: [envName],
91
+ });
92
+ process.env[envName] = String(port);
93
+ return port;
94
+ }
@@ -0,0 +1,57 @@
1
+ import readline from "node:readline/promises";
2
+ import { stdin as input, stdout as output } from "node:process";
3
+
4
+ /**
5
+ * @param {import("./run-wizard.mjs").WizardContext} ctx
6
+ * @param {string} prompt
7
+ * @param {string} defaultValue
8
+ */
9
+ export async function ask(ctx, prompt, defaultValue) {
10
+ if (ctx.yes) return defaultValue;
11
+ if (!process.stdin.isTTY) return defaultValue;
12
+ try {
13
+ const clack = await import("@clack/prompts");
14
+ const v = await clack.text({
15
+ message: prompt,
16
+ placeholder: defaultValue,
17
+ defaultValue,
18
+ });
19
+ if (clack.isCancel(v)) process.exit(0);
20
+ const s = String(v ?? "").trim();
21
+ return s || defaultValue;
22
+ } catch {
23
+ const rl = readline.createInterface({ input, output });
24
+ const line = await rl.question(` ${prompt} [${defaultValue}]: `);
25
+ rl.close();
26
+ const s = line.trim();
27
+ return s || defaultValue;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * @param {import("./run-wizard.mjs").WizardContext} ctx
33
+ * @param {string} prompt
34
+ * @param {"y"|"n"} defaultYN
35
+ */
36
+ export async function confirm(ctx, prompt, defaultYN) {
37
+ if (ctx.yes) return defaultYN === "y";
38
+ if (!process.stdin.isTTY) return defaultYN === "y";
39
+ try {
40
+ const clack = await import("@clack/prompts");
41
+ const v = await clack.confirm({
42
+ message: prompt,
43
+ active: "yes",
44
+ inactive: "no",
45
+ initialValue: defaultYN === "y",
46
+ });
47
+ if (clack.isCancel(v)) process.exit(0);
48
+ return Boolean(v);
49
+ } catch {
50
+ const hint = defaultYN === "y" ? "Y/n" : "y/N";
51
+ const rl = readline.createInterface({ input, output });
52
+ const line = await rl.question(` ${prompt} [${hint}]: `);
53
+ rl.close();
54
+ const reply = (line.trim() || defaultYN).toLowerCase();
55
+ return reply === "y" || reply === "yes";
56
+ }
57
+ }