@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,97 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Mise-like task runner for openmem (reads ~/.config/personal-memory/config.toml).
4
+ * ./pm configure | up | down | doctor
5
+ * Packaged installs reach it through `openmem <task>`.
6
+ */
7
+ import { spawnSync } from "node:child_process";
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+ import { ensureWizardDeps, resolveWizardRoots } from "../../cli/roots.mjs";
11
+
12
+ const { pkgRoot, repo: REPO, packaged } = resolveWizardRoots(import.meta.url);
13
+ // setup.mjs is code, so it always comes from the package root, never the stack root.
14
+ const SETUP = path.join(pkgRoot, "wizard/bin/setup.mjs");
15
+
16
+ const TASKS = {
17
+ configure: { args: ["--yes"], help: "Full setup and start" },
18
+ up: { args: ["--up", "--yes"], help: "Start stack" },
19
+ down: { args: ["--down", "--yes"], help: "Stop stack" },
20
+ doctor: { args: ["--yes", "--no-smoke"], help: "Validate config (no smoke)" },
21
+ "install-mcp": {
22
+ script: "cli/install-memory-mcp.mjs",
23
+ help: "Install memory MCP servers into ~/.cursor/mcp.json",
24
+ },
25
+ "install-coding-agent": {
26
+ script: "cli/install-coding-agent.mjs",
27
+ help: "Wire session memory for Claude Code, Codex, OpenCode, Cursor CLI",
28
+ },
29
+ "feed-sync": {
30
+ script: "cli/feed-sync.mjs",
31
+ help: "Fetch RSS/OpenAlex feeds into index.yml feeds.out_dir",
32
+ },
33
+ switch: {
34
+ script: "cli/switch-memory-provider.mjs",
35
+ help: "Switch MEMORY_PROVIDER (wizard.conf + optional stack/MCP)",
36
+ },
37
+ };
38
+
39
+ const task = process.argv[2];
40
+ if (!task || task === "-h" || task === "--help") {
41
+ ensureWizardDeps(pkgRoot, packaged);
42
+ const { loadPmConfig } = await import("../lib/pm-config.mjs");
43
+ const home = process.env.HOME || "";
44
+ const { merged } = loadPmConfig(home, REPO);
45
+ const tasks = /** @type {Record<string, {description?: string}>} */ (
46
+ merged.tasks || {}
47
+ );
48
+ console.log("Usage: pm <task>\n");
49
+ console.log("Tasks:");
50
+ for (const name of Object.keys(TASKS)) {
51
+ const desc = tasks[name]?.description || TASKS[name].help;
52
+ console.log(` ${name.padEnd(12)} ${desc}`);
53
+ }
54
+ process.exit(task ? 0 : 1);
55
+ }
56
+
57
+ const spec = TASKS[task];
58
+ if (!spec) {
59
+ console.error(`unknown task: ${task} (try: ${Object.keys(TASKS).join(", ")})`);
60
+ process.exit(1);
61
+ }
62
+
63
+ const extra = process.argv.slice(3);
64
+
65
+ if (spec.script) {
66
+ const script = path.join(pkgRoot, spec.script);
67
+ if (!fs.existsSync(script)) {
68
+ console.error(`${spec.script} not found; run from a openmem checkout or install`);
69
+ process.exit(1);
70
+ }
71
+ const r = spawnSync(process.execPath, [script, ...extra], {
72
+ cwd: REPO,
73
+ stdio: "inherit",
74
+ env: process.env,
75
+ });
76
+ process.exit(r.status ?? 1);
77
+ }
78
+
79
+ if (!fs.existsSync(SETUP)) {
80
+ console.error("setup.mjs not found; run from a openmem checkout or install");
81
+ process.exit(1);
82
+ }
83
+
84
+ const r = spawnSync(process.execPath, [SETUP, ...spec.args, ...extra], {
85
+ cwd: REPO,
86
+ stdio: "inherit",
87
+ env: process.env,
88
+ });
89
+ if (task === "doctor" && (r.status ?? 1) === 0) {
90
+ const wiring = spawnSync(process.execPath, [path.join(pkgRoot, "cli/wiring-report.mjs")], {
91
+ cwd: REPO,
92
+ stdio: "inherit",
93
+ env: process.env,
94
+ });
95
+ process.exit(wiring.status ?? 0);
96
+ }
97
+ process.exit(r.status ?? 1);
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Setup wizard entry (./setup in a checkout; `openmem setup` when packaged).
4
+ *
5
+ * REPO is the mutable stack root (PM_STACK_ROOT, default: the checkout): compose project
6
+ * dir, .env, generated files and docker build context. PM_PACKAGE_ROOT is the read-only
7
+ * code root (providers/, scripts/setup.mjs, wizard/lib).
8
+ */
9
+ import { ensureWizardDeps, resolveWizardRoots } from "../../cli/roots.mjs";
10
+
11
+ const { pkgRoot, repo: REPO, packaged } = resolveWizardRoots(import.meta.url);
12
+ ensureWizardDeps(pkgRoot, packaged);
13
+
14
+ const { runWizard } = await import("../lib/run-wizard.mjs");
15
+
16
+ const argv = process.argv.slice(2);
17
+ const wantsClack =
18
+ !argv.includes("--yes") &&
19
+ !argv.includes("-y") &&
20
+ !argv.includes("--dry-run") &&
21
+ process.stdin.isTTY;
22
+
23
+ /** @type {Record<string, string>} */
24
+ let handoff = {};
25
+ if (wantsClack) {
26
+ const { runClackSetup } = await import("../lib/run-clack.mjs");
27
+ handoff = await runClackSetup(REPO);
28
+ for (const [k, v] of Object.entries(handoff)) {
29
+ process.env[k] = v;
30
+ }
31
+ }
32
+
33
+ await runWizard(REPO, argv);
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Docker Compose version gate for the wizard preflight.
3
+ *
4
+ * Provider compose fragments use `env_file: [{path, required: false}]`, which needs Compose 2.24;
5
+ * the Hindsight base file already needs 2.20 for `depends_on.required`. The wizard requires the
6
+ * higher bar everywhere so a stack never fails halfway through `docker compose up`.
7
+ */
8
+
9
+ export const MIN_COMPOSE = { major: 2, minor: 24 };
10
+
11
+ /**
12
+ * Parse `docker compose version --short` ("2.29.0", "v2.29.0") or the long form
13
+ * ("Docker Compose version v2.29.0"). Returns null when no version is found.
14
+ * @param {string} text
15
+ * @returns {{major:number, minor:number}|null}
16
+ */
17
+ export function parseComposeVersion(text) {
18
+ const m = String(text || "").match(/v?(\d+)\.(\d+)/);
19
+ return m ? { major: Number(m[1]), minor: Number(m[2]) } : null;
20
+ }
21
+
22
+ /**
23
+ * @param {{major:number, minor:number}|null} version
24
+ * @returns {boolean} true when the version meets MIN_COMPOSE
25
+ */
26
+ export function composeVersionOk(version) {
27
+ if (!version) return false;
28
+ if (version.major !== MIN_COMPOSE.major) return version.major > MIN_COMPOSE.major;
29
+ return version.minor >= MIN_COMPOSE.minor;
30
+ }
31
+
32
+ export const COMPOSE_TOO_OLD_HINT = `Docker Compose ${MIN_COMPOSE.major}.${MIN_COMPOSE.minor} or newer is required (docker compose version --short); provider compose files use env_file required: false. Update the compose plugin`;
@@ -0,0 +1,70 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ /**
5
+ * Compose profiles for the active provider + optional extras (Ollama, NotebookLM).
6
+ *
7
+ * @param {{ memoryProvider?: { compose?: { profile?: string } }, extraComposeProfiles?: string[] }} ctx
8
+ * @returns {string[]}
9
+ */
10
+ export function composeProfiles(ctx) {
11
+ const compose = ctx.memoryProvider?.compose || {};
12
+ const profiles = [];
13
+ if (compose.profile) profiles.push(compose.profile);
14
+ for (const p of ctx.extraComposeProfiles || []) {
15
+ if (typeof p === "string" && p && !profiles.includes(p)) profiles.push(p);
16
+ }
17
+ return profiles;
18
+ }
19
+
20
+ /**
21
+ * Arguments for `docker` that run a compose subcommand for the active provider.
22
+ *
23
+ * Every compose call in the wizard goes through here so the manifest's
24
+ * `compose.profile` and `compose.files` apply uniformly (up, build, run, down, ps).
25
+ * With no `compose.files`, no -f flag is added and compose auto-loads compose.yaml
26
+ * and compose.override.yaml from the stack root. With `compose.files`, the list is
27
+ * compose.yaml, compose.override.yaml (when present), then the extra files.
28
+ *
29
+ * Extra profiles (compose Ollama, NotebookLM) come from `ctx.extraComposeProfiles`.
30
+ * Docker Compose unions CLI `--profile` with `COMPOSE_PROFILES` from `.env`, so
31
+ * `composeSpawnOptions` overrides that env var to the same list — otherwise `.env`
32
+ * can start NotebookLM while the wizard intentionally omitted it (e.g. during smoke).
33
+ *
34
+ * @param {{ project: string, repoRoot: string, memoryProvider?: { compose?: { profile?: string, files?: string[] } }, extraComposeProfiles?: string[] }} ctx
35
+ * @param {...string} rest the compose subcommand and its arguments
36
+ * @returns {string[]}
37
+ */
38
+ export function composeArgs(ctx, ...rest) {
39
+ const compose = ctx.memoryProvider?.compose || {};
40
+ const args = ["compose", "-p", ctx.project];
41
+ const extra = Array.isArray(compose.files) ? compose.files : [];
42
+ if (extra.length) {
43
+ args.push("-f", "compose.yaml");
44
+ if (fs.existsSync(path.join(ctx.repoRoot, "compose.override.yaml"))) {
45
+ args.push("-f", "compose.override.yaml");
46
+ }
47
+ for (const f of extra) args.push("-f", f);
48
+ }
49
+ for (const p of composeProfiles(ctx)) args.push("--profile", p);
50
+ return [...args, ...rest];
51
+ }
52
+
53
+ /** `docker ...` as the wizard prints it in descriptions. */
54
+ export function composeCommandText(ctx, ...rest) {
55
+ return `docker ${composeArgs(ctx, ...rest).join(" ")}`;
56
+ }
57
+
58
+ /**
59
+ * Spawn options for compose calls: cwd is the stack root; COMPOSE_PROFILES matches
60
+ * the `--profile` flags so `.env` cannot enable extra profiles behind the wizard's back.
61
+ */
62
+ export function composeSpawnOptions(ctx) {
63
+ return {
64
+ cwd: ctx.repoRoot,
65
+ env: {
66
+ ...process.env,
67
+ COMPOSE_PROFILES: composeProfiles(ctx).join(","),
68
+ },
69
+ };
70
+ }
@@ -0,0 +1,103 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+
3
+ /**
4
+ * @param {string} url
5
+ * @param {number} [timeoutSec]
6
+ * @returns {string}
7
+ */
8
+ export function curlGet(url, timeoutSec = 5) {
9
+ const r = spawnSync("curl", ["-fsS", "-m", String(timeoutSec), url], {
10
+ encoding: "utf8",
11
+ });
12
+ if (r.status !== 0) return "";
13
+ return r.stdout || "";
14
+ }
15
+
16
+ /**
17
+ * @param {string} url
18
+ * @returns {object|null}
19
+ */
20
+ export function curlJson(url) {
21
+ const body = curlGet(url);
22
+ if (!body) return null;
23
+ try {
24
+ return JSON.parse(body);
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * @param {string} method
32
+ * @param {string} url
33
+ * @param {string} [jsonBody]
34
+ * @param {number} [timeoutSec]
35
+ * @returns {string}
36
+ */
37
+ export function curlRequest(method, url, jsonBody, timeoutSec = 60) {
38
+ const args = curlArgs(method, url, jsonBody, timeoutSec);
39
+ const r = spawnSync("curl", args, { encoding: "utf8" });
40
+ if (r.status !== 0) return "";
41
+ return r.stdout || "";
42
+ }
43
+
44
+ /**
45
+ * Build curl argv for a JSON (or empty-body) request.
46
+ * @param {string} method
47
+ * @param {string} url
48
+ * @param {string} [jsonBody]
49
+ * @param {number} timeoutSec
50
+ * @returns {string[]}
51
+ */
52
+ export function curlArgs(method, url, jsonBody, timeoutSec) {
53
+ const args = ["-fsS", "-m", String(timeoutSec), "-X", method, url];
54
+ if (jsonBody !== undefined) {
55
+ args.push("-H", "Content-Type: application/json", "-d", jsonBody);
56
+ }
57
+ return args;
58
+ }
59
+
60
+ /**
61
+ * Like curlRequest, but async with an optional heartbeat so long retains do not look hung.
62
+ *
63
+ * @param {string} method
64
+ * @param {string} url
65
+ * @param {string} [jsonBody]
66
+ * @param {number} [timeoutSec]
67
+ * @param {{ tickSec?: number, onTick?: (info: { elapsed: number, timeoutSec: number }) => void }} [opts]
68
+ * @returns {Promise<string>} response body, or "" on failure / timeout
69
+ */
70
+ export function curlRequestProgress(method, url, jsonBody, timeoutSec = 60, opts = {}) {
71
+ const tickSec = opts.tickSec ?? 15;
72
+ const onTick = opts.onTick;
73
+ const args = curlArgs(method, url, jsonBody, timeoutSec);
74
+ return new Promise((resolve) => {
75
+ const child = spawn("curl", args, { stdio: ["ignore", "pipe", "pipe"] });
76
+ let stdout = "";
77
+ let settled = false;
78
+ const started = Date.now();
79
+ const timer =
80
+ typeof onTick === "function"
81
+ ? setInterval(() => {
82
+ onTick({
83
+ elapsed: Math.round((Date.now() - started) / 1000),
84
+ timeoutSec,
85
+ });
86
+ }, tickSec * 1000)
87
+ : null;
88
+
89
+ const finish = (body) => {
90
+ if (settled) return;
91
+ settled = true;
92
+ if (timer) clearInterval(timer);
93
+ resolve(body);
94
+ };
95
+
96
+ child.stdout.setEncoding("utf8");
97
+ child.stdout.on("data", (chunk) => {
98
+ stdout += chunk;
99
+ });
100
+ child.on("error", () => finish(""));
101
+ child.on("close", (code) => finish(code === 0 ? stdout : ""));
102
+ });
103
+ }
@@ -0,0 +1,148 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { spawnSync } from "node:child_process";
5
+ import { ok, die } from "./log.mjs";
6
+ import { act } from "./exec.mjs";
7
+ import { composeArgs, composeSpawnOptions } from "./compose.mjs";
8
+
9
+ function buildDockerfile(svc) {
10
+ if (svc === "indexer") return "indexer/Dockerfile";
11
+ if (svc === "notebooklm") return "sync/Dockerfile";
12
+ die(`the wizard does not build service ${svc}`);
13
+ }
14
+
15
+ function copySourcesFromDockerfile(repoRoot, dfRel) {
16
+ const dfPath = path.join(repoRoot, dfRel);
17
+ const text = fs.readFileSync(dfPath, "utf8");
18
+ const srcs = [];
19
+ for (const line of text.split("\n")) {
20
+ if (!line.startsWith("COPY")) continue;
21
+ const parts = line.trim().split(/\s+/).slice(1);
22
+ const filtered = parts.filter((p) => !p.startsWith("--"));
23
+ for (let i = 0; i < filtered.length - 1; i++) srcs.push(filtered[i]);
24
+ }
25
+ return srcs;
26
+ }
27
+
28
+ function expandInputs(repoRoot, rel) {
29
+ const abs = path.join(repoRoot, rel);
30
+ const out = [];
31
+ if (!fs.existsSync(abs)) {
32
+ out.push(rel);
33
+ return out;
34
+ }
35
+ if (fs.statSync(abs).isDirectory()) {
36
+ const walk = (dir, prefix) => {
37
+ for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
38
+ if (ent.name === "__pycache__" || ent.name.endsWith(".pyc")) continue;
39
+ const p = path.join(dir, ent.name);
40
+ const relP = prefix ? `${prefix}/${ent.name}` : ent.name;
41
+ if (ent.isDirectory()) walk(p, relP);
42
+ else out.push(relP);
43
+ }
44
+ };
45
+ walk(abs, rel);
46
+ } else out.push(rel);
47
+ return out;
48
+ }
49
+
50
+ /**
51
+ * @param {import("./run-wizard.mjs").WizardContext} ctx
52
+ * @param {string} svc
53
+ */
54
+ export function buildHash(ctx, svc) {
55
+ const df = buildDockerfile(svc);
56
+ const files = new Set([df, "compose.yaml"]);
57
+ for (const f of ctx.memoryProvider?.compose?.files || []) files.add(f);
58
+ for (const src of copySourcesFromDockerfile(ctx.repoRoot, df)) {
59
+ for (const f of expandInputs(ctx.repoRoot, src)) files.add(f);
60
+ }
61
+ const sorted = [...files].sort();
62
+ const hash = crypto.createHash("sha256");
63
+ for (const f of sorted) {
64
+ const p = path.join(ctx.repoRoot, f);
65
+ if (!fs.existsSync(p)) die(`missing build input ${f}`);
66
+ hash.update(fs.readFileSync(p));
67
+ }
68
+ return hash.digest("hex");
69
+ }
70
+
71
+ /**
72
+ * @param {import("./run-wizard.mjs").WizardContext} ctx
73
+ * @param {string} svc
74
+ * @param {string} hash
75
+ */
76
+ function recordBuildHash(ctx, svc, hash) {
77
+ let lines = [];
78
+ if (fs.existsSync(ctx.buildHashFile)) {
79
+ lines = fs
80
+ .readFileSync(ctx.buildHashFile, "utf8")
81
+ .split("\n")
82
+ .filter((l) => l && !l.startsWith(`${svc} `));
83
+ }
84
+ lines.push(`${svc} ${hash}`);
85
+ fs.writeFileSync(ctx.buildHashFile, `${lines.join("\n")}\n`);
86
+ }
87
+
88
+ /**
89
+ * @param {import("./run-wizard.mjs").WizardContext} ctx
90
+ * @param {string} svc
91
+ * @param {string} image
92
+ */
93
+ export async function buildService(ctx, svc, image) {
94
+ const hash = buildHash(ctx, svc);
95
+ let stored = "";
96
+ if (fs.existsSync(ctx.buildHashFile)) {
97
+ for (const line of fs.readFileSync(ctx.buildHashFile, "utf8").split("\n")) {
98
+ if (line.startsWith(`${svc} `)) stored = line.split(/\s+/)[1] || "";
99
+ }
100
+ }
101
+ const inspect = spawnSync("docker", ["image", "inspect", image], { stdio: "ignore" });
102
+ const imageOk = inspect.status === 0;
103
+ if (!ctx.rebuild && stored === hash && imageOk) {
104
+ ok(`${svc} image unchanged; build skipped`);
105
+ return;
106
+ }
107
+ await act(
108
+ ctx,
109
+ `build the ${svc} image (compose project ${ctx.project})`,
110
+ "docker",
111
+ composeArgs(ctx, "build", svc),
112
+ composeSpawnOptions(ctx),
113
+ );
114
+ if (ctx.dry) return;
115
+ recordBuildHash(ctx, svc, hash);
116
+ }
117
+
118
+ /**
119
+ * @param {import("./run-wizard.mjs").WizardContext} ctx
120
+ * @returns {Promise<string[]>}
121
+ */
122
+ export async function snapshot(ctx) {
123
+ const lines = [];
124
+ const globHs = fs.existsSync(ctx.hsDir) ? fs.readdirSync(ctx.hsDir) : [];
125
+ for (const name of globHs) {
126
+ const f = path.join(ctx.hsDir, name);
127
+ if (fs.statSync(f).isFile()) {
128
+ const h = crypto.createHash("sha256").update(fs.readFileSync(f)).digest("hex");
129
+ lines.push(`${f} ${h}`);
130
+ }
131
+ }
132
+ for (const f of [path.join(ctx.repoRoot, ".env"), path.join(ctx.repoRoot, "compose.override.yaml")]) {
133
+ if (fs.existsSync(f)) {
134
+ const h = crypto.createHash("sha256").update(fs.readFileSync(f)).digest("hex");
135
+ lines.push(`${f} ${h}`);
136
+ }
137
+ }
138
+ const ps = spawnSync("docker", composeArgs(ctx, "ps", "-q"), {
139
+ encoding: "utf8",
140
+ ...composeSpawnOptions(ctx),
141
+ });
142
+ if (ps.status === 0 && ps.stdout) {
143
+ for (const id of ps.stdout.split("\n").filter(Boolean)) {
144
+ lines.push(`container ${id.trim()}`);
145
+ }
146
+ }
147
+ return lines.sort();
148
+ }
@@ -0,0 +1,41 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { info } from "./log.mjs";
3
+
4
+ /**
5
+ * Run a command, or print it when the context is a dry run. Exits the process on failure.
6
+ * @param {import("./run-wizard.mjs").WizardContext} ctx
7
+ * @param {string} description
8
+ * @param {string} cmd
9
+ * @param {string[]} args
10
+ * @param {{ env?: Record<string, string>, cwd?: string }} [opts] extra environment, working directory
11
+ */
12
+ export async function act(ctx, description, cmd, args, opts = {}) {
13
+ if (ctx.dry) {
14
+ info(`[dry-run] ${description}`);
15
+ return;
16
+ }
17
+ info(description);
18
+ const r = spawnSync(cmd, args, {
19
+ stdio: "inherit",
20
+ env: { ...process.env, ...(opts.env || {}) },
21
+ ...(opts.cwd ? { cwd: opts.cwd } : {}),
22
+ });
23
+ if (r.status !== 0) process.exit(r.status ?? 1);
24
+ }
25
+
26
+ /**
27
+ * Like act(), with extra environment variables for the child.
28
+ * @param {import("./run-wizard.mjs").WizardContext} ctx
29
+ * @param {string} description
30
+ * @param {string} cmd
31
+ * @param {string[]} args
32
+ * @param {Record<string, string>} extraEnv
33
+ */
34
+ export async function actWithEnv(ctx, description, cmd, args, extraEnv) {
35
+ await act(ctx, description, cmd, args, { env: extraEnv });
36
+ }
37
+
38
+ export function commandExists(name) {
39
+ const r = spawnSync("command", ["-v", name], { shell: true, stdio: "ignore" });
40
+ return r.status === 0;
41
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The shared indexer's container entry point. The wizard runs it with --list to preview
3
+ * which sources an index.yml selects. PM_INDEXER_BIN overrides the name if the entry
4
+ * point is renamed.
5
+ */
6
+ export const INDEXER_BIN = process.env.PM_INDEXER_BIN || "hindsight-project-index";
@@ -0,0 +1,4 @@
1
+ /** @deprecated Use ~/.config/personal-memory/config.toml. Kept for provider hook compatibility. */
2
+ export function loadMachineEnv(_repoRoot) {
3
+ return {};
4
+ }
@@ -0,0 +1,32 @@
1
+ const tty = Boolean(process.stdout.isTTY);
2
+ const BOLD = tty ? "\x1b[1m" : "";
3
+ const GREEN = tty ? "\x1b[32m" : "";
4
+ const YELLOW = tty ? "\x1b[33m" : "";
5
+ const RED = tty ? "\x1b[31m" : "";
6
+ const RESET = tty ? "\x1b[0m" : "";
7
+
8
+ export function step(title) {
9
+ console.log(`\n${BOLD}== ${title} ==${RESET}`);
10
+ }
11
+
12
+ export function info(msg) {
13
+ console.log(` ${msg}`);
14
+ }
15
+
16
+ export function ok(msg) {
17
+ console.log(` ${GREEN}ok${RESET} ${msg}`);
18
+ }
19
+
20
+ export function warn(msg) {
21
+ console.error(` ${YELLOW}warn${RESET} ${msg}`);
22
+ }
23
+
24
+ export function die(msg) {
25
+ console.error(` ${RED}error${RESET} ${msg}`);
26
+ process.exit(1);
27
+ }
28
+
29
+ export function problem(ctx, msg) {
30
+ if (ctx.dry) warn(`${msg} (dry run: continuing)`);
31
+ else die(msg);
32
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Opt-in coding-agent NotebookLM MCP access: materialize ~/.hindsight/coding-agent.json
3
+ * as a real file (never write machine settings into the repo template) and toggle
4
+ * notebooklmAccess.enabled.
5
+ */
6
+ import fs from "node:fs";
7
+ import path from "node:path";
8
+
9
+ /**
10
+ * @param {string} agentPath ~/.hindsight/coding-agent.json
11
+ * @param {string} templatePath repo config/coding-agent.json
12
+ * @returns {{ path: string, action: "created" | "materialized" | "exists", cfg: Record<string, any> }}
13
+ */
14
+ export function ensureCodingAgentJsonRealFile(agentPath, templatePath) {
15
+ fs.mkdirSync(path.dirname(agentPath), { recursive: true });
16
+
17
+ let stat = null;
18
+ try {
19
+ stat = fs.lstatSync(agentPath);
20
+ } catch {
21
+ stat = null;
22
+ }
23
+
24
+ if (stat?.isSymbolicLink()) {
25
+ const raw = fs.readFileSync(agentPath, "utf8");
26
+ const cfg = JSON.parse(raw);
27
+ fs.unlinkSync(agentPath);
28
+ fs.writeFileSync(agentPath, `${JSON.stringify(cfg, null, 2)}\n`);
29
+ return { path: agentPath, action: "materialized", cfg };
30
+ }
31
+
32
+ if (stat?.isFile()) {
33
+ const cfg = JSON.parse(fs.readFileSync(agentPath, "utf8"));
34
+ return { path: agentPath, action: "exists", cfg };
35
+ }
36
+
37
+ let cfg;
38
+ if (fs.existsSync(templatePath) && fs.statSync(templatePath).isFile()) {
39
+ cfg = JSON.parse(fs.readFileSync(templatePath, "utf8"));
40
+ } else {
41
+ cfg = {
42
+ serverMode: "self-hosted",
43
+ apiUrl: "http://127.0.0.1:8888",
44
+ optInOnly: true,
45
+ optInPaths: [],
46
+ mapPathToBank: {},
47
+ notebooklmAccess: { enabled: false, banks: ["*"] },
48
+ };
49
+ }
50
+ fs.writeFileSync(agentPath, `${JSON.stringify(cfg, null, 2)}\n`);
51
+ return { path: agentPath, action: "created", cfg };
52
+ }
53
+
54
+ /**
55
+ * Set notebooklmAccess.enabled on a real coding-agent.json. Never writes templatePath.
56
+ * Materializes a symlink into a regular file first when needed.
57
+ *
58
+ * @param {string} agentPath
59
+ * @param {string} templatePath
60
+ * @param {boolean} enabled
61
+ * @param {{ dry?: boolean }} [opts]
62
+ * @returns {{ dry?: boolean, action?: string, agentPath: string, enabled: boolean, wrote: boolean }}
63
+ */
64
+ export function setNotebooklmAccessEnabled(agentPath, templatePath, enabled, opts = {}) {
65
+ const absTemplate = path.resolve(templatePath);
66
+ if (opts.dry) {
67
+ return {
68
+ dry: true,
69
+ agentPath,
70
+ enabled,
71
+ wrote: false,
72
+ action: fs.existsSync(agentPath)
73
+ ? fs.lstatSync(agentPath).isSymbolicLink()
74
+ ? "would-materialize"
75
+ : "would-update"
76
+ : "would-create",
77
+ };
78
+ }
79
+
80
+ const { cfg, action } = ensureCodingAgentJsonRealFile(agentPath, templatePath);
81
+ // Guard: never write into the shipped template even if agentPath somehow resolves there.
82
+ if (path.resolve(agentPath) === absTemplate) {
83
+ throw new Error(`refusing to write notebooklmAccess into shipped template ${templatePath}`);
84
+ }
85
+ const access =
86
+ cfg.notebooklmAccess && typeof cfg.notebooklmAccess === "object" ? { ...cfg.notebooklmAccess } : {};
87
+ access.enabled = enabled === true;
88
+ if (!Array.isArray(access.banks) || !access.banks.length) access.banks = ["*"];
89
+ cfg.notebooklmAccess = access;
90
+ fs.writeFileSync(agentPath, `${JSON.stringify(cfg, null, 2)}\n`);
91
+ return { agentPath, enabled: access.enabled, wrote: true, action };
92
+ }