@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,755 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { spawnSync } from "node:child_process";
5
+ import { parseWizardFlags } from "./parse-args.mjs";
6
+ import { step, info, ok, warn, die, problem } from "./log.mjs";
7
+ import { COMPOSE_TOO_OLD_HINT, composeVersionOk, parseComposeVersion } from "./compose-version.mjs";
8
+ import { envValue, ensureHsDir, expandHome, indexerStateDir } from "./paths.mjs";
9
+ import { curlGet, curlRequest, curlRequestProgress } from "./curl.mjs";
10
+ import { ask, confirm } from "./prompts.mjs";
11
+ import { act, actWithEnv, commandExists } from "./exec.mjs";
12
+ import { buildService, snapshot } from "./docker-build.mjs";
13
+ import { composeArgs, composeCommandText, composeSpawnOptions } from "./compose.mjs";
14
+ import { INDEXER_BIN } from "./indexer.mjs";
15
+ import {
16
+ DEFAULT_PROVIDER_ID,
17
+ expandPath,
18
+ listPlannedProviders,
19
+ listSelectableProviders,
20
+ providerApiBase,
21
+ providerDefaultId,
22
+ resolveProvider,
23
+ } from "./providers/registry.mjs";
24
+ import { validateMemorySetup } from "./validate-setup.mjs";
25
+ import { actionLabel, parseActionFromArgv } from "./wizard-action.mjs";
26
+ import { applyPmConfigEnv, loadPmConfig, syncPmConfig } from "./pm-config.mjs";
27
+ import { writeWizardConf } from "./wizard-conf.mjs";
28
+ import { setupNotebooklm as runNotebooklmSetup } from "./notebooklm.mjs";
29
+ import { createHelpers } from "./provider-wizard/helpers.mjs";
30
+ import {
31
+ PROVIDER_PHASES,
32
+ callProviderHook,
33
+ runProviderPhase,
34
+ } from "./provider-wizard/run-steps.mjs";
35
+
36
+ /**
37
+ * The meta wizard. It owns the provider-neutral steps (preflight, provider choice, shared
38
+ * index.yml, NotebookLM, compose build/up/down, health, generic smoke, validation, config
39
+ * sync) and calls the active provider's hooks for everything provider-specific. Provider
40
+ * knowledge (model backends, secret files, ports, summary lines) lives in
41
+ * providers/<id>/; see docs/provider-wizard-hooks.md. A provider without a hook for a
42
+ * phase gets a clean no-op.
43
+ */
44
+
45
+ /** @typedef {ReturnType<parseWizardFlags>} WizardContext & { memoryProvider?: import("./providers/registry.mjs").ProviderManifest } */
46
+
47
+ function confGet(ctx, key) {
48
+ return envValue(ctx.wizardConf, key);
49
+ }
50
+
51
+ /** Helpers handed to provider hooks; the few that need meta state are overridden here. */
52
+ function providerHelpers() {
53
+ return createHelpers({
54
+ runStackSetup: (ctx, opts) => runSetupScript(ctx, opts),
55
+ apiBase: (ctx) => resolveApiBase(ctx),
56
+ });
57
+ }
58
+
59
+ /**
60
+ * The provider's API base URL: its `apiBase` hook, else the manifest's port settings.
61
+ * @param {WizardContext} ctx
62
+ * @returns {Promise<string>}
63
+ */
64
+ async function resolveApiBase(ctx) {
65
+ const hook = await callProviderHook(ctx, PROVIDER_PHASES.apiBase, providerHelpers());
66
+ if (hook.ran && hook.value) return String(hook.value);
67
+ return providerApiBase(ctx, ctx.memoryProvider);
68
+ }
69
+
70
+ /** Run the provider's quiet `init` hook (context defaults, TOML mapping). */
71
+ async function initProvider(ctx) {
72
+ await runProviderPhase(ctx, PROVIDER_PHASES.init, providerHelpers());
73
+ }
74
+
75
+ /**
76
+ * Optional compose profiles beyond the provider's main profile.
77
+ * Providers may already have set `ctx.extraComposeProfiles` (hydrate / beforeStack).
78
+ * NotebookLM is meta-owned; composeSpawnOptions overrides COMPOSE_PROFILES to match
79
+ * the `--profile` list, so every needed profile must be listed on the compose command.
80
+ */
81
+ function syncExtraProfiles(ctx) {
82
+ const extra = [...(ctx.extraComposeProfiles || [])];
83
+ if (fs.existsSync(ctx.notebookFile)) {
84
+ const text = fs.readFileSync(ctx.notebookFile, "utf8");
85
+ if (/^enabled:\s*true\b/m.test(text) && !extra.includes("notebooklm")) {
86
+ extra.push("notebooklm");
87
+ }
88
+ }
89
+ ctx.extraComposeProfiles = extra;
90
+ }
91
+
92
+ async function hydrateFromWizardConf(ctx) {
93
+ if (!fs.existsSync(ctx.wizardConf)) return false;
94
+ const provider = confGet(ctx, "MEMORY_PROVIDER") || DEFAULT_PROVIDER_ID;
95
+ ctx.providerId = provider;
96
+ ctx.memoryProvider = resolveProvider(provider);
97
+ const nlmHome = confGet(ctx, "NOTEBOOKLM_HOME");
98
+ if (nlmHome) process.env.NOTEBOOKLM_HOME = nlmHome;
99
+ await runProviderPhase(ctx, PROVIDER_PHASES.hydrate, providerHelpers());
100
+ syncExtraProfiles(ctx);
101
+ return true;
102
+ }
103
+
104
+ async function chooseProvider(ctx) {
105
+ step("2. Memory provider");
106
+ const def = providerDefaultId(ctx);
107
+ const selectable = listSelectableProviders();
108
+ if (ctx.skipBackendStep && process.env.MEMORY_PROVIDER) {
109
+ ctx.providerId = process.env.MEMORY_PROVIDER;
110
+ ctx.memoryProvider = resolveProvider(ctx.providerId);
111
+ ok(`memory provider ${ctx.memoryProvider.label} (setup UI)`);
112
+ return;
113
+ }
114
+ if (ctx.yes) {
115
+ ctx.providerId = selectable.some((m) => m.id === def) ? def : DEFAULT_PROVIDER_ID;
116
+ } else {
117
+ info("Supported providers:");
118
+ selectable.forEach((m, i) => info(` ${i + 1}) ${m.id} — ${m.label} (${m.status})`));
119
+ for (const m of listPlannedProviders()) {
120
+ info(` -) ${m.id} — planned (use ./setup when implemented)`);
121
+ }
122
+ const defIdx = String(selectable.findIndex((m) => m.id === def) + 1 || 1);
123
+ const choice = await ask(ctx, "Memory provider (number)", defIdx);
124
+ const idx = Number(choice) - 1;
125
+ if (idx < 0 || idx >= selectable.length) die("pick a listed provider");
126
+ ctx.providerId = selectable[idx].id;
127
+ }
128
+ const previous = confGet(ctx, "MEMORY_PROVIDER");
129
+ ctx.memoryProvider = resolveProvider(ctx.providerId);
130
+ if (previous && previous !== ctx.providerId && !ctx.yes) {
131
+ if (
132
+ !(await confirm(
133
+ ctx,
134
+ `Switch memory provider from ${previous} to ${ctx.providerId}? Existing data may not migrate automatically.`,
135
+ "n",
136
+ ))
137
+ ) {
138
+ ctx.providerId = previous;
139
+ ctx.memoryProvider = resolveProvider(previous);
140
+ }
141
+ }
142
+ ok(`memory provider ${ctx.memoryProvider.label}`);
143
+ }
144
+
145
+ /**
146
+ * Run scripts/setup.mjs from the code root. It writes the stack .env and
147
+ * compose.override.yaml in the stack root, then calls the provider's `prepareStackEnv`.
148
+ * Defaults for the description and environment come from the provider's `stackEnv` hook.
149
+ *
150
+ * @param {WizardContext} ctx
151
+ * @param {{ description?: string, args?: string[], env?: Record<string, string> }} [opts]
152
+ */
153
+ async function runSetupScript(ctx, opts = {}) {
154
+ const hook = await callProviderHook(ctx, PROVIDER_PHASES.stackEnv, providerHelpers());
155
+ const spec = (hook.ran && hook.value) || {};
156
+ const description =
157
+ opts.description ?? spec.description ?? "scripts/setup.sh: write .env, compose.override.yaml";
158
+ const env = {
159
+ ...(opts.env ?? spec.env ?? {}),
160
+ MEMORY_PROVIDER: ctx.memoryProvider.id,
161
+ PM_STACK_ROOT: ctx.repoRoot,
162
+ PM_PACKAGE_ROOT: ctx.packageRoot,
163
+ };
164
+ await actWithEnv(
165
+ ctx,
166
+ description,
167
+ process.execPath,
168
+ [path.join(ctx.packageRoot, "scripts", "setup.mjs"), ...(opts.args || [])],
169
+ env,
170
+ );
171
+ }
172
+
173
+ async function setupEnv(ctx) {
174
+ step("4. Secrets and env");
175
+ ensureHsDir(ctx);
176
+ const stateDir = indexerStateDir(ctx);
177
+ if (fs.existsSync(stateDir)) ok(`kept ${stateDir} (indexer state mount point)`);
178
+ else if (ctx.dry) info(`[dry-run] create ${stateDir} (indexer state mount point)`);
179
+ else {
180
+ fs.mkdirSync(stateDir, { recursive: true });
181
+ ok(`created ${stateDir} (indexer state mount point)`);
182
+ }
183
+ // A provider's setupEnv hook owns this step and calls helpers.runStackSetup itself.
184
+ const handled = await runProviderPhase(ctx, PROVIDER_PHASES.setupEnv, providerHelpers());
185
+ if (!handled) await runSetupScript(ctx);
186
+ }
187
+
188
+ function yamlList(key, csv) {
189
+ const items = csv
190
+ .split(",")
191
+ .map((s) => s.trim())
192
+ .filter(Boolean);
193
+ if (!items.length) return `${key}: []\n`;
194
+ return `${key}:\n${items.map((i) => ` - "${i}"`).join("\n")}\n`;
195
+ }
196
+
197
+ /**
198
+ * @param {{ roots: { path: string, depth: number }[], reposCsv: string, vaultPathsCsv: string, excludesCsv: string }} opts
199
+ */
200
+ function renderIndex(opts) {
201
+ const rootsYaml = opts.roots
202
+ .map((r) => ` - path: "${r.path}"\n depth: ${r.depth}`)
203
+ .join("\n");
204
+ const vaultItems = opts.vaultPathsCsv
205
+ .split(",")
206
+ .map((s) => s.trim())
207
+ .filter(Boolean);
208
+ const vaultPathsYaml = vaultItems.length
209
+ ? ` paths:\n${vaultItems.map((i) => ` - "${i}"`).join("\n")}\n`
210
+ : " paths: []\n";
211
+ return (
212
+ "# Written by ./setup. Replaces config/index.yml on this machine.\n" +
213
+ "# Keys omitted here take built-in values; see README \"Source config: index.yml\".\n" +
214
+ `roots:\n${rootsYaml}\n` +
215
+ yamlList("repos", opts.reposCsv) +
216
+ "vaults:\n auto: true\n" +
217
+ vaultPathsYaml +
218
+ yamlList("exclude", opts.excludesCsv)
219
+ );
220
+ }
221
+
222
+ function findVaults(root, depth) {
223
+ const out = [];
224
+ const max = depth + 1;
225
+ const walk = (dir, d) => {
226
+ if (d > max) return;
227
+ let entries;
228
+ try {
229
+ entries = fs.readdirSync(dir, { withFileTypes: true });
230
+ } catch {
231
+ return;
232
+ }
233
+ for (const ent of entries) {
234
+ if (!ent.isDirectory()) continue;
235
+ const p = path.join(dir, ent.name);
236
+ if (ent.name === ".obsidian") {
237
+ out.push(dir);
238
+ continue;
239
+ }
240
+ walk(p, d + 1);
241
+ }
242
+ };
243
+ walk(root, 0);
244
+ return out;
245
+ }
246
+
247
+ async function configureIndex(ctx) {
248
+ step("5. Sources to index");
249
+ if (fs.existsSync(ctx.indexFile)) {
250
+ info(`found ${ctx.indexFile}`);
251
+ if (!(await confirm(ctx, "Replace it with new answers?", "n"))) {
252
+ ok(`kept ${ctx.indexFile}`);
253
+ return;
254
+ }
255
+ }
256
+ info("Scan roots cover git repos and Obsidian vaults (.obsidian/). Add notes/papers/bookmarks paths or feeds in index.yml for other markdown trees ([docs/index-sources.md](https://github.com/bcmyguest/openmem/blob/main/docs/index-sources.md)).");
257
+ const rootsCsv = await ask(
258
+ ctx,
259
+ "Root folder(s) to scan, comma-separated",
260
+ "~",
261
+ );
262
+ const depth = await ask(ctx, "Depth below each root", "3");
263
+ const reposCsv = await ask(ctx, "Extra repository paths, comma-separated (empty for none)", "");
264
+ const vaultPathsCsv = await ask(
265
+ ctx,
266
+ "Extra Obsidian vault paths, comma-separated (empty for auto-discover only)",
267
+ "",
268
+ );
269
+ const excludesCsv = await ask(
270
+ ctx,
271
+ "Exclude glob patterns on absolute paths, comma-separated (empty for none)",
272
+ "",
273
+ );
274
+ const rootPaths = rootsCsv
275
+ .split(",")
276
+ .map((s) => s.trim())
277
+ .filter(Boolean);
278
+ if (!rootPaths.length) die("at least one scan root is required");
279
+ const depthN = Number(depth);
280
+ if (!Number.isFinite(depthN) || depthN < 1) die("depth must be a positive integer");
281
+ if (ctx.dry) {
282
+ info(
283
+ `[dry-run] look for Obsidian vaults under ${rootPaths.join(", ")}, validate the config, then write ${ctx.indexFile}`,
284
+ );
285
+ return;
286
+ }
287
+ /** @type {string[]} */
288
+ const discovered = [];
289
+ for (const root of rootPaths) {
290
+ const expanded = expandHome(root, ctx.home);
291
+ for (const v of findVaults(expanded, depthN)) {
292
+ if (!discovered.includes(v)) discovered.push(v);
293
+ }
294
+ }
295
+ if (discovered.length) {
296
+ info("Obsidian vaults found (auto-discovered under scan roots):");
297
+ for (const v of discovered) info(` ${v}`);
298
+ } else {
299
+ info(`no Obsidian vaults found under ${rootPaths.join(", ")} within depth ${depthN}`);
300
+ info("tip: put the vault under a scan root, or list it under Extra Obsidian vault paths (for example ~/work-notes/work)");
301
+ }
302
+
303
+ const content = renderIndex({
304
+ roots: rootPaths.map((path) => ({ path, depth: depthN })),
305
+ reposCsv,
306
+ vaultPathsCsv,
307
+ excludesCsv,
308
+ });
309
+ const tmp = path.join(os.tmpdir(), `wizard-index-${process.pid}.yml`);
310
+ fs.writeFileSync(tmp, content);
311
+ await buildService(ctx, "indexer", "openmem-indexer:local");
312
+ const listingR = spawnSync(
313
+ "docker",
314
+ composeArgs(
315
+ ctx,
316
+ "run",
317
+ "--rm",
318
+ "--no-deps",
319
+ "-T",
320
+ "-v",
321
+ `${tmp}:/tmp/wizard-index.yml:ro`,
322
+ "indexer",
323
+ INDEXER_BIN,
324
+ "--config",
325
+ "/tmp/wizard-index.yml",
326
+ "--list",
327
+ ),
328
+ { encoding: "utf8", ...composeSpawnOptions(ctx) },
329
+ );
330
+ fs.unlinkSync(tmp);
331
+ if (listingR.status !== 0) die("the indexer rejected the config (message above)");
332
+ const listing = listingR.stdout || "";
333
+ const count = listing.split("\n").filter((l) => /^(repo|vault)\t/.test(l)).length;
334
+ info(`the indexer selects ${count} source(s):`);
335
+ for (const line of listing.split("\n").slice(0, 40)) {
336
+ if (line) info(` ${line}`);
337
+ }
338
+ if (!(await confirm(ctx, `Write this config to ${ctx.indexFile}?`, "y"))) {
339
+ warn("not written");
340
+ return;
341
+ }
342
+ ensureHsDir(ctx);
343
+ if (fs.existsSync(ctx.indexFile)) {
344
+ fs.copyFileSync(ctx.indexFile, `${ctx.indexFile}.bak-${Date.now()}`);
345
+ }
346
+ fs.writeFileSync(ctx.indexFile, content, { mode: 0o644 });
347
+ ok(`wrote ${ctx.indexFile}`);
348
+ }
349
+
350
+ function enableNotebooklm(ctx) {
351
+ ensureHsDir(ctx);
352
+ if (fs.existsSync(ctx.notebookFile)) {
353
+ let text = fs.readFileSync(ctx.notebookFile, "utf8");
354
+ if (/^enabled:/m.test(text)) text = text.replace(/^enabled:.*/m, "enabled: true");
355
+ else text += "\nenabled: true\n";
356
+ fs.writeFileSync(ctx.notebookFile, text);
357
+ } else {
358
+ fs.writeFileSync(
359
+ ctx.notebookFile,
360
+ "# Per-machine overrides (changed keys only); see config/notebooklm.yml.\nenabled: true\n",
361
+ );
362
+ }
363
+ ok(`enabled in ${ctx.notebookFile}; stack start will include --profile notebooklm`);
364
+ syncExtraProfiles(ctx);
365
+ }
366
+
367
+ async function setupNotebooklm(ctx) {
368
+ await runNotebooklmSetup(ctx, enableNotebooklm);
369
+ }
370
+
371
+ async function waitHealth(ctx) {
372
+ const p = ctx.memoryProvider;
373
+ const base = await resolveApiBase(ctx);
374
+ const healthPath = p.compose.healthPath || "/health";
375
+ if (ctx.dry) {
376
+ info(`[dry-run] wait for ${base}${healthPath}`);
377
+ return;
378
+ }
379
+ info(`waiting for ${p.label} at ${base}${healthPath}…`);
380
+ for (let i = 0; i < 90; i++) {
381
+ if (curlGet(`${base}${healthPath}`)) {
382
+ ok(`${p.label} is healthy at ${base}`);
383
+ return;
384
+ }
385
+ if (i > 0 && i % 5 === 0) info(`… still waiting for health (${i * 4}s)`);
386
+ await new Promise((r) => setTimeout(r, 4000));
387
+ }
388
+ if (p.compose.healthService) {
389
+ die(
390
+ `${p.label} is not healthy after about 6 minutes: docker compose -p ${ctx.project} logs ${p.compose.healthService}`,
391
+ );
392
+ }
393
+ die(
394
+ `${p.label} is not healthy after about 6 minutes at ${base}${healthPath}: check the external API is running and reachable`,
395
+ );
396
+ }
397
+
398
+ async function deleteSmokeBank(ctx, base) {
399
+ const p = ctx.memoryProvider;
400
+ const bank = p.wizard.smokeBank || ctx.smokeBank;
401
+ const delPath = expandPath(p, bank, p.wizard.deletePath);
402
+ curlRequest(p.wizard.deleteMethod || "DELETE", `${base}${delPath}`);
403
+ }
404
+
405
+ /** Expand {compose}/{project}/{api}/{controlPlane}/{logs} in a smoke hint string. */
406
+ function expandSmokeHint(ctx, template, logs, apiBase) {
407
+ if (!template) return "";
408
+ const api = apiBase || `http://127.0.0.1:${ctx.port || 8888}`;
409
+ const cp = `http://127.0.0.1:${ctx.portCp || 9999}`;
410
+ return template
411
+ .replaceAll("{compose}", composeCommandText(ctx))
412
+ .replaceAll("{project}", ctx.project)
413
+ .replaceAll("{api}", api)
414
+ .replaceAll("{controlPlane}", cp)
415
+ .replaceAll("{logs}", logs);
416
+ }
417
+
418
+ /**
419
+ * Smoke test. A provider's `smoke` hook replaces the manifest's HTTP template (for
420
+ * flows that are asynchronous, multipart or not bank-shaped); otherwise the template in
421
+ * manifest.wizard (retain, recall, delete) runs.
422
+ *
423
+ * Soft-fails on timeout/mismatch: prints fix hints and continues setup (stack is up).
424
+ */
425
+ async function smokeTest(ctx) {
426
+ const handled = await runProviderPhase(ctx, PROVIDER_PHASES.smoke, providerHelpers());
427
+ if (handled) return;
428
+ const p = ctx.memoryProvider;
429
+ const bank = p.wizard.smokeBank || ctx.smokeBank;
430
+ const base = await resolveApiBase(ctx);
431
+ if (ctx.dry) {
432
+ info(`[dry-run] retain and recall one fact in bank ${bank}, then delete the bank`);
433
+ return;
434
+ }
435
+ if (!p.wizard.retainPath || !p.wizard.recallPath) {
436
+ info(`smoke test skipped (${p.label} has no wizard smoke steps yet)`);
437
+ return;
438
+ }
439
+ const logServices = (p.compose.logServices?.length ? p.compose.logServices : null) ||
440
+ (p.compose.healthService ? [p.compose.healthService] : []);
441
+ const logs = composeCommandText(ctx, "logs", ...logServices);
442
+ const retainTimeout = Number(p.wizard.smokeRetainTimeoutSec) > 0 ? Number(p.wizard.smokeRetainTimeoutSec) : 90;
443
+ const recallTimeout = Number(p.wizard.smokeRecallTimeoutSec) > 0 ? Number(p.wizard.smokeRecallTimeoutSec) : 60;
444
+ const api = base;
445
+ const cp = `http://127.0.0.1:${ctx.portCp || 9999}`;
446
+ info(`smoke test: retain and recall in bank ${bank} (retain ≤${retainTimeout}s, recall ≤${recallTimeout}s)`);
447
+ info(` watch: control plane ${cp} · API ${api}/docs · logs: ${logs}`);
448
+ const tick = (label) => ({ elapsed, timeoutSec }) =>
449
+ info(`… still ${label} (${elapsed}s / ${timeoutSec}s; UI ${cp})`);
450
+ const retainPath = expandPath(p, bank, p.wizard.retainPath);
451
+ info("retain: posting smoke fact (sync; waits on the model)…");
452
+ const retain = await curlRequestProgress(
453
+ p.wizard.retainMethod || "POST",
454
+ `${base}${retainPath}`,
455
+ p.wizard.retainBody,
456
+ retainTimeout,
457
+ { onTick: tick("retaining") },
458
+ );
459
+ if (!retain) {
460
+ await deleteSmokeBank(ctx, base);
461
+ const hint =
462
+ expandSmokeHint(ctx, p.wizard.retainHint || p.wizard.recallHint, logs, base) ||
463
+ `check model route and LLM queue (${logs}); open ${cp}`;
464
+ warn(`smoke retain timed out or failed — continuing setup. ${hint}`);
465
+ ctx.smokeFailed = true;
466
+ return;
467
+ }
468
+ ok("retain ok");
469
+ const recallPath = expandPath(p, bank, p.wizard.recallPath);
470
+ info("recall: asking for the smoke fact…");
471
+ const recall = await curlRequestProgress(
472
+ p.wizard.recallMethod || "POST",
473
+ `${base}${recallPath}`,
474
+ p.wizard.recallBody,
475
+ recallTimeout,
476
+ { onTick: tick("recalling") },
477
+ );
478
+ info("cleanup: deleting smoke bank…");
479
+ await deleteSmokeBank(ctx, base);
480
+ const needle = p.wizard.recallMustMatch || "heron";
481
+ if (!new RegExp(needle, "i").test(recall)) {
482
+ const hint =
483
+ expandSmokeHint(ctx, p.wizard.recallHint, logs, base) ||
484
+ `recall empty; ${logs}; open ${cp}`;
485
+ warn(`smoke recall did not return the fact — continuing setup. ${hint}`);
486
+ ctx.smokeFailed = true;
487
+ return;
488
+ }
489
+ ok(`retain and recall worked; bank ${bank} deleted`);
490
+ }
491
+
492
+ async function stackDown(ctx) {
493
+ step("Stop the stack");
494
+ info(`action: ${actionLabel("down")}`);
495
+ const args = composeArgs(ctx, "down");
496
+ await act(ctx, `docker ${args.join(" ")}`, "docker", args, composeSpawnOptions(ctx));
497
+ ok(`compose project ${ctx.project} stopped`);
498
+ }
499
+
500
+ async function startStack(ctx) {
501
+ step("7. Start the stack");
502
+ syncExtraProfiles(ctx);
503
+ await runSetupScript(ctx);
504
+ await buildService(ctx, "indexer", "openmem-indexer:local");
505
+ const wantNotebooklm = (ctx.extraComposeProfiles || []).includes("notebooklm");
506
+ if (wantNotebooklm) {
507
+ await buildService(ctx, "notebooklm", "openmem-notebooklm:local");
508
+ }
509
+ // NotebookLM sync shares the stack LLM with smoke retain. Bring the core stack
510
+ // up first (omit notebooklm profile), run smoke, then start NotebookLM.
511
+ const profilesForSmoke = (ctx.extraComposeProfiles || []).filter((p) => p !== "notebooklm");
512
+ const savedExtras = ctx.extraComposeProfiles;
513
+ ctx.extraComposeProfiles = profilesForSmoke;
514
+ const up = composeArgs(ctx, "up", "-d", "--no-build");
515
+ await act(ctx, `docker ${up.join(" ")}`, "docker", up, composeSpawnOptions(ctx));
516
+ await waitHealth(ctx);
517
+ // After health, before smoke (provider may pull models or warm caches).
518
+ await runProviderPhase(ctx, PROVIDER_PHASES.afterStackHealthy, providerHelpers());
519
+ if (ctx.smoke) await smokeTest(ctx);
520
+ else info("smoke test skipped (--no-smoke)");
521
+ ctx.extraComposeProfiles = savedExtras;
522
+ if (wantNotebooklm) {
523
+ info("starting NotebookLM sync (after smoke — avoids contending for the LLM)…");
524
+ const nlmUp = composeArgs(ctx, "up", "-d", "--no-build", "notebooklm");
525
+ await act(ctx, `docker ${nlmUp.join(" ")}`, "docker", nlmUp, composeSpawnOptions(ctx));
526
+ ok("NotebookLM sync started");
527
+ }
528
+ }
529
+
530
+ /**
531
+ * PATH binary → coding-agent plugin harness id.
532
+ * Binary names often differ from harness ids (`claude` → `claude-code`,
533
+ * `agent` / `cursor-agent` → `cursor-cli`). Detection mirrors the upstream
534
+ * installer's `detect` checks where practical.
535
+ */
536
+ const CODING_AGENT_HARNESS = {
537
+ claude: "claude-code",
538
+ codex: "codex",
539
+ opencode: "opencode",
540
+ // Cursor Agent CLI: `agent` (current) and `cursor-agent` (legacy / alias).
541
+ agent: "cursor-cli",
542
+ "cursor-agent": "cursor-cli",
543
+ };
544
+
545
+ /** @returns {string[]} unique harness ids for agents detected on this machine */
546
+ function detectedCodingHarnesses() {
547
+ const found = new Set();
548
+ for (const [bin, harness] of Object.entries(CODING_AGENT_HARNESS)) {
549
+ if (commandExists(bin)) found.add(harness);
550
+ }
551
+ // Upstream cursor-cli also detects ~/.cursor even without the CLI on PATH.
552
+ const cursorHome = path.join(process.env.HOME || os.homedir(), ".cursor");
553
+ if (fs.existsSync(cursorHome)) found.add("cursor-cli");
554
+ return [...found];
555
+ }
556
+
557
+ async function setupCodingAgents(ctx) {
558
+ step("8. Coding-agent plugin (optional)");
559
+ const pkg = ctx.memoryProvider.codingAgent?.pluginPackage;
560
+ const harnesses = detectedCodingHarnesses();
561
+ // Link config/coding-agent.json first so the installer can adopt self-hosted settings
562
+ // (and mem0/Cognee hooks can print their non-plugin guidance).
563
+ await runProviderPhase(ctx, PROVIDER_PHASES.setupCodingAgent, providerHelpers());
564
+ if (!pkg) {
565
+ info(`no coding-agent plugin defined for ${ctx.memoryProvider.label}; step skipped`);
566
+ return;
567
+ }
568
+ if (!harnesses.length) {
569
+ info("no supported coding-agent CLIs on PATH (claude, codex, opencode, agent/cursor-agent): plugin skipped");
570
+ return;
571
+ }
572
+ const apiUrl = await resolveApiBase(ctx);
573
+ // Scripted install: --server/--api-url skip the "Where should memory live?" prompt
574
+ // (--server/--api-url skip the installer's memory-location prompt.)
575
+ const installArgs = [
576
+ ...harnesses,
577
+ "--server",
578
+ "self-hosted",
579
+ "--api-url",
580
+ apiUrl,
581
+ ];
582
+ info(`detected harnesses: ${harnesses.join(" ")} → self-hosted ${apiUrl}`);
583
+ if (await confirm(ctx, `Install the ${ctx.memoryProvider.label} plugin for them now (npx)?`, "n")) {
584
+ await act(ctx, `npx --yes ${pkg} install ${installArgs.join(" ")}`, "npx", [
585
+ "--yes",
586
+ pkg,
587
+ "install",
588
+ ...installArgs,
589
+ ]);
590
+ ok("restart Cursor Agent (or your coding agent) so it picks up the new plugin wiring");
591
+ } else {
592
+ info(`skipped; later: npx --yes ${pkg} install ${installArgs.join(" ")}`);
593
+ }
594
+ }
595
+
596
+ async function assertMemorySetup(ctx) {
597
+ step("9. Validate memory setup");
598
+ ctx.validationErrors = [];
599
+ ctx.validationChecks = [];
600
+ await runProviderPhase(ctx, PROVIDER_PHASES.extendValidation, providerHelpers());
601
+ const result = validateMemorySetup(ctx);
602
+ result.checks.push(...ctx.validationChecks);
603
+ if (ctx.validationErrors?.length) {
604
+ for (const e of ctx.validationErrors) result.errors.push(e);
605
+ result.ok = result.errors.length === 0;
606
+ }
607
+ for (const c of result.checks) info(`check: ${c}`);
608
+ if (result.ok) {
609
+ ok("memory setup validation passed");
610
+ return;
611
+ }
612
+ for (const e of result.errors) warn(e);
613
+ if (ctx.dry) {
614
+ info("[dry-run] validation errors ignored");
615
+ return;
616
+ }
617
+ die("memory setup validation failed (fix the items above and rerun ./setup)");
618
+ }
619
+
620
+ /** config.toml tables the provider owns (empty for a provider without the hook). */
621
+ async function providerConfigSections(ctx) {
622
+ const hook = await callProviderHook(ctx, PROVIDER_PHASES.configToml, providerHelpers());
623
+ return (hook.ran && hook.value) || {};
624
+ }
625
+
626
+ async function summary(ctx) {
627
+ step("Summary");
628
+ info(`Memory provider: ${ctx.memoryProvider.label} (${ctx.memoryProvider.id})`);
629
+ const hook = await callProviderHook(ctx, PROVIDER_PHASES.summary, providerHelpers());
630
+ const sum = (hook.ran && hook.value) || {};
631
+ info(`Running (compose project ${ctx.project}):`);
632
+ for (const line of sum.running || []) info(line);
633
+ info("Config files:");
634
+ for (const line of sum.configFiles || []) info(line);
635
+ if (fs.existsSync(ctx.indexFile)) info(` ${ctx.indexFile} (sources)`);
636
+ else info(" config/index.yml (shipped sources)");
637
+ if (fs.existsSync(ctx.notebookFile)) info(` ${ctx.notebookFile} (NotebookLM)`);
638
+ info(` ${ctx.wizardConf} (this wizard's backend choice)`);
639
+ for (const line of sum.notes || []) info(line);
640
+ const more = sum.logServices?.length ? ` (or: ${sum.logServices.join(", ")})` : "";
641
+ info(`Logs: ${composeCommandText(ctx, "logs", "-f", "indexer")}${more}`);
642
+ info(`Stop: ${composeCommandText(ctx, "down")} (keeps the memory database)`);
643
+ info(`Reset: ${composeCommandText(ctx, "down", "-v")} (deletes the memory database)`);
644
+ }
645
+
646
+ async function checkPreflight(ctx) {
647
+ step("1. Preflight");
648
+ for (const tool of ["git", "curl", "docker"]) {
649
+ if (!commandExists(tool)) die(`${tool} not found on PATH`);
650
+ }
651
+ const infoR = spawnSync("docker", ["info"], { stdio: "ignore" });
652
+ if (infoR.status !== 0) {
653
+ die(
654
+ `docker is not usable by ${os.userInfo().username}. Fix: sudo usermod -aG docker ${os.userInfo().username}, then log out and back in`,
655
+ );
656
+ }
657
+ const verR = spawnSync("docker", ["compose", "version", "--short"], { encoding: "utf8" });
658
+ const version = parseComposeVersion(verR.stdout);
659
+ if (!version) die("Docker Compose v2 is required (docker compose version). Install the compose plugin");
660
+ else if (!composeVersionOk(version)) {
661
+ // A dry run only plans, so it warns; a real run would fail at `docker compose up`.
662
+ problem(ctx, `${COMPOSE_TOO_OLD_HINT} (found ${version.major}.${version.minor})`);
663
+ }
664
+ ok(`docker and compose v${version.major} work without sudo; git and curl present`);
665
+ ok(`host UID:GID is ${os.userInfo().uid}:${os.userInfo().gid}`);
666
+ await runProviderPhase(ctx, PROVIDER_PHASES.resolvePorts, providerHelpers());
667
+ }
668
+
669
+ function configTomlTip(ctx) {
670
+ const user = path.join(ctx.home, ".config/personal-memory/config.toml");
671
+ const example = path.join(ctx.repoRoot, "config/openmem.toml.example");
672
+ if (!fs.existsSync(user) && fs.existsSync(example)) {
673
+ info(`tip: cp config/openmem.toml.example ${user} for declarative defaults`);
674
+ }
675
+ }
676
+
677
+ /** Count files and containers this run changed. */
678
+ async function changedCount(ctx, before) {
679
+ const after = await snapshot(ctx);
680
+ const beforeSet = new Set(before);
681
+ let changed = 0;
682
+ for (const line of after) if (!beforeSet.has(line)) changed++;
683
+ return changed;
684
+ }
685
+
686
+ /**
687
+ * @param {string} repoRoot the mutable stack root (compose files, .env, generated files)
688
+ * @param {string[]} argv
689
+ */
690
+ export async function runWizard(repoRoot, argv) {
691
+ const ctx = parseWizardFlags(repoRoot, argv);
692
+ // Provider manifests and hook modules load from the read-only code root; child scripts
693
+ // (scripts/setup.mjs) inherit it.
694
+ process.env.PM_PACKAGE_ROOT = ctx.packageRoot;
695
+ const pm = loadPmConfig(ctx.home, repoRoot);
696
+ ctx.pmMerged = pm.merged;
697
+ if (pm.wroteFrom.length) ok(`toml config ${pm.wroteFrom.at(-1)}`);
698
+ applyPmConfigEnv(pm.merged);
699
+ ctx.action = parseActionFromArgv(argv);
700
+ if (process.env.PM_PROJECT) ctx.project = process.env.PM_PROJECT;
701
+ ctx.providerId = process.env.MEMORY_PROVIDER || providerDefaultId(ctx);
702
+ ctx.memoryProvider = resolveProvider(ctx.providerId);
703
+ await initProvider(ctx);
704
+ configTomlTip(ctx);
705
+ const before = await snapshot(ctx);
706
+ if (ctx.dry) info("dry run: no files are written and no containers start");
707
+ info(`wizard action: ${actionLabel(ctx.action)}`);
708
+ const preflightProvider = ctx.memoryProvider.id;
709
+ await checkPreflight(ctx);
710
+
711
+ if (ctx.action === "down") {
712
+ await stackDown(ctx);
713
+ await summary(ctx);
714
+ console.log("wizard: changed=0");
715
+ return;
716
+ }
717
+
718
+ if (ctx.action === "up") {
719
+ await chooseProvider(ctx);
720
+ if (!(await hydrateFromWizardConf(ctx))) {
721
+ die("no wizard.conf — run ./setup (configure) once before --up");
722
+ }
723
+ if (ctx.memoryProvider.id !== preflightProvider) {
724
+ await initProvider(ctx);
725
+ await runProviderPhase(ctx, PROVIDER_PHASES.resolvePorts, providerHelpers());
726
+ }
727
+ await runProviderPhase(ctx, PROVIDER_PHASES.beforeStack, providerHelpers());
728
+ await startStack(ctx);
729
+ await assertMemorySetup(ctx);
730
+ await summary(ctx);
731
+ console.log(`wizard: changed=${await changedCount(ctx, before)}`);
732
+ return;
733
+ }
734
+
735
+ await chooseProvider(ctx);
736
+ if (ctx.memoryProvider.id !== preflightProvider) {
737
+ // The interactive pick differs from the id preflight used: set the new provider up.
738
+ await initProvider(ctx);
739
+ await runProviderPhase(ctx, PROVIDER_PHASES.resolvePorts, providerHelpers());
740
+ }
741
+ await runProviderPhase(ctx, PROVIDER_PHASES.afterProvider, providerHelpers());
742
+ await runProviderPhase(ctx, PROVIDER_PHASES.configureModelRouting, providerHelpers());
743
+ // A provider that saved wizard.conf itself (helpers.saveWizardConf) is left alone.
744
+ if (!ctx.wizardConfWritten) writeWizardConf(ctx, {}, { pmSections: await providerConfigSections(ctx) });
745
+ await setupEnv(ctx);
746
+ await configureIndex(ctx);
747
+ await setupNotebooklm(ctx);
748
+ await runProviderPhase(ctx, PROVIDER_PHASES.beforeStack, providerHelpers());
749
+ await startStack(ctx);
750
+ await setupCodingAgents(ctx);
751
+ await assertMemorySetup(ctx);
752
+ syncPmConfig(ctx, await providerConfigSections(ctx));
753
+ await summary(ctx);
754
+ console.log(`wizard: changed=${await changedCount(ctx, before)}`);
755
+ }