@bcmyguest/openmem 0.0.0-stage → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/.agents/skills/openmem/SKILL.md +34 -0
  2. package/LICENSE +190 -0
  3. package/README.md +140 -2
  4. package/bin/hindsight-project-index +530 -0
  5. package/cli/coding-agent/access-policy.mjs +38 -0
  6. package/cli/coding-agent/agents.mjs +34 -0
  7. package/cli/coding-agent/bank-from-cwd.mjs +140 -0
  8. package/cli/coding-agent/capabilities.mjs +34 -0
  9. package/cli/coding-agent/context-store.mjs +81 -0
  10. package/cli/coding-agent/context-trim.mjs +897 -0
  11. package/cli/coding-agent/file-io.mjs +72 -0
  12. package/cli/coding-agent/hook-harnesses.mjs +88 -0
  13. package/cli/coding-agent/hook-runner.mjs +284 -0
  14. package/cli/coding-agent/install-access.mjs +119 -0
  15. package/cli/coding-agent/install-context-proxy.mjs +333 -0
  16. package/cli/coding-agent/install-rtk.mjs +60 -0
  17. package/cli/coding-agent/install-shim.mjs +201 -0
  18. package/cli/coding-agent/opencode-config.mjs +107 -0
  19. package/cli/coding-agent/opencode-plugin.mjs +132 -0
  20. package/cli/coding-agent/opencode2-plugin/index.js +299 -0
  21. package/cli/coding-agent/recall-frame.mjs +29 -0
  22. package/cli/coding-agent/remove-upstream-access.mjs +122 -0
  23. package/cli/coding-agent/runtime.mjs +40 -0
  24. package/cli/coding-agent/session-db.mjs +626 -0
  25. package/cli/coding-agent/session-facade.mjs +86 -0
  26. package/cli/coding-agent/toml-edit.mjs +161 -0
  27. package/cli/cognee-mcp.mjs +260 -0
  28. package/cli/context-management.mjs +254 -0
  29. package/cli/feed-sync.mjs +27 -0
  30. package/cli/install-coding-agent.mjs +298 -0
  31. package/cli/install-memory-mcp.mjs +426 -0
  32. package/cli/is-main.mjs +18 -0
  33. package/cli/mcp/context-sql-runner.mjs +58 -0
  34. package/cli/mcp/context-store.mjs +670 -0
  35. package/cli/mcp/context.mjs +61 -0
  36. package/cli/mcp/export/cognee-dataset-name.mjs +12 -0
  37. package/cli/mcp/export/cognee.mjs +119 -0
  38. package/cli/mcp/export/hindsight.mjs +48 -0
  39. package/cli/mcp/export/index.mjs +26 -0
  40. package/cli/mcp/export/mem0.mjs +31 -0
  41. package/cli/mcp/knowledge.mjs +333 -0
  42. package/cli/mcp/llm-synthesize.mjs +50 -0
  43. package/cli/mcp/mem0-scope-id.mjs +22 -0
  44. package/cli/mcp/memory-facade.mjs +521 -0
  45. package/cli/mcp/memory-reflect-consolidate.mjs +120 -0
  46. package/cli/mcp/notebooklm-banks.mjs +21 -0
  47. package/cli/mcp/notebooklm-hindsight-rest.mjs +45 -0
  48. package/cli/mcp/notebooklm-provider.mjs +225 -0
  49. package/cli/mcp/pmm-records.mjs +23 -0
  50. package/cli/mcp/portability.mjs +158 -0
  51. package/cli/mcp/stdio.mjs +65 -0
  52. package/cli/mcp/unified-mcp-install.mjs +65 -0
  53. package/cli/mem0-mcp.mjs +108 -0
  54. package/cli/notebooklm-mcp.mjs +236 -0
  55. package/cli/openmem-mcp.mjs +38 -0
  56. package/cli/openmem.mjs +222 -0
  57. package/cli/roots.mjs +85 -0
  58. package/cli/schedule.mjs +176 -0
  59. package/cli/stack.mjs +438 -0
  60. package/cli/switch-memory-provider.mjs +358 -0
  61. package/cli/wiring-report.mjs +125 -0
  62. package/compose.bench.yaml +12 -0
  63. package/compose.yaml +227 -0
  64. package/config/coding-agent.json +13 -0
  65. package/config/hindsight.env.example +26 -0
  66. package/config/index.yml +54 -0
  67. package/config/litellm.lemond.yaml +40 -0
  68. package/config/litellm.ollama.yaml +33 -0
  69. package/config/notebooklm.yml +33 -0
  70. package/config/openmem.toml.example +124 -0
  71. package/docs/OPENMEM.md +23 -0
  72. package/docs/architecture.md +245 -0
  73. package/docs/assets/README.md +10 -0
  74. package/docs/assets/setup-demo.gif +0 -0
  75. package/docs/coding-agent-hooks.md +75 -0
  76. package/docs/coding-agent-scopes.md +22 -0
  77. package/docs/coding-agents.md +71 -0
  78. package/docs/cognee.md +37 -0
  79. package/docs/compatibility.md +80 -0
  80. package/docs/concepts.md +135 -0
  81. package/docs/context-management.md +255 -0
  82. package/docs/context-proxy.md +115 -0
  83. package/docs/context-research.md +215 -0
  84. package/docs/context-store.md +277 -0
  85. package/docs/hindsight.md +77 -0
  86. package/docs/index-sources-roadmap.md +38 -0
  87. package/docs/index-sources.md +39 -0
  88. package/docs/indexer-sinks.md +122 -0
  89. package/docs/indexing.md +57 -0
  90. package/docs/manual-stack-setup.md +74 -0
  91. package/docs/mcp-install-guide.md +4 -0
  92. package/docs/mcp-setup.md +104 -0
  93. package/docs/mem0.md +141 -0
  94. package/docs/memory-portability.md +73 -0
  95. package/docs/memory-providers.md +246 -0
  96. package/docs/meridian-litellm.md +198 -0
  97. package/docs/model-configuration.md +106 -0
  98. package/docs/notebooklm.md +83 -0
  99. package/docs/npx-install.md +172 -0
  100. package/docs/openmem-config.md +194 -0
  101. package/docs/provider-wizard-hooks.md +222 -0
  102. package/docs/quickstart.md +107 -0
  103. package/docs/setup-wizard.md +117 -0
  104. package/docs/sources-feeds.md +39 -0
  105. package/docs/sources-readwise.md +23 -0
  106. package/docs/sources-zotero.md +22 -0
  107. package/glama.json +4 -0
  108. package/indexer/Dockerfile +38 -0
  109. package/indexer/code_outline.py +146 -0
  110. package/indexer/feed_sync.py +107 -0
  111. package/indexer/index_config.py +290 -0
  112. package/indexer/pytest.ini +5 -0
  113. package/indexer/sinks/__init__.py +148 -0
  114. package/indexer/sinks/_http.py +44 -0
  115. package/indexer/sinks/cognee.py +329 -0
  116. package/indexer/sinks/hindsight.py +107 -0
  117. package/indexer/sinks/mem0.py +247 -0
  118. package/indexer/watch.py +240 -0
  119. package/package.json +96 -4
  120. package/providers/_template/compose.yaml +52 -0
  121. package/providers/cognee/OPERATOR.md +82 -0
  122. package/providers/cognee/compose.yaml +89 -0
  123. package/providers/cognee/lib/clack.mjs +157 -0
  124. package/providers/cognee/lib/coding-agent.mjs +26 -0
  125. package/providers/cognee/lib/cognee-mcp-install.mjs +153 -0
  126. package/providers/cognee/lib/config.mjs +98 -0
  127. package/providers/cognee/lib/env-file.mjs +194 -0
  128. package/providers/cognee/lib/env-setup.mjs +83 -0
  129. package/providers/cognee/lib/llm-env.mjs +382 -0
  130. package/providers/cognee/lib/ports.mjs +25 -0
  131. package/providers/cognee/lib/smoke.mjs +246 -0
  132. package/providers/cognee/lib/stack-env.mjs +134 -0
  133. package/providers/cognee/lib/state.mjs +101 -0
  134. package/providers/cognee/lib/summary.mjs +56 -0
  135. package/providers/cognee/lib/validate.mjs +97 -0
  136. package/providers/cognee/manifest.json +56 -0
  137. package/providers/cognee/wizard.mjs +134 -0
  138. package/providers/graphiti/manifest.json +15 -0
  139. package/providers/hindsight/lib/clack.mjs +347 -0
  140. package/providers/hindsight/lib/coding-agent.mjs +204 -0
  141. package/providers/hindsight/lib/config.mjs +79 -0
  142. package/providers/hindsight/lib/env-setup.mjs +72 -0
  143. package/providers/hindsight/lib/litellm-routes.mjs +196 -0
  144. package/providers/hindsight/lib/meridian.mjs +307 -0
  145. package/providers/hindsight/lib/model-backend.mjs +243 -0
  146. package/providers/hindsight/lib/notebooklm-mcp-install.mjs +127 -0
  147. package/providers/hindsight/lib/ollama-compose.mjs +51 -0
  148. package/providers/hindsight/lib/ports.mjs +30 -0
  149. package/providers/hindsight/lib/probe.mjs +54 -0
  150. package/providers/hindsight/lib/stack-env.mjs +125 -0
  151. package/providers/hindsight/lib/state.mjs +80 -0
  152. package/providers/hindsight/lib/summary.mjs +57 -0
  153. package/providers/hindsight/lib/validate.mjs +81 -0
  154. package/providers/hindsight/manifest.json +58 -0
  155. package/providers/hindsight/wizard.mjs +150 -0
  156. package/providers/letta/manifest.json +29 -0
  157. package/providers/mem0/OPERATOR.md +143 -0
  158. package/providers/mem0/compose.yaml +79 -0
  159. package/providers/mem0/lib/coding-agent.mjs +58 -0
  160. package/providers/mem0/lib/config.mjs +45 -0
  161. package/providers/mem0/lib/configure.mjs +201 -0
  162. package/providers/mem0/lib/dotenv.mjs +71 -0
  163. package/providers/mem0/lib/env-file.mjs +110 -0
  164. package/providers/mem0/lib/http.mjs +69 -0
  165. package/providers/mem0/lib/litellm.mjs +457 -0
  166. package/providers/mem0/lib/mcp-install.mjs +165 -0
  167. package/providers/mem0/lib/secret.mjs +30 -0
  168. package/providers/mem0/lib/smoke.mjs +179 -0
  169. package/providers/mem0/lib/stack-env.mjs +255 -0
  170. package/providers/mem0/lib/state.mjs +67 -0
  171. package/providers/mem0/lib/summary.mjs +88 -0
  172. package/providers/mem0/lib/url.mjs +106 -0
  173. package/providers/mem0/lib/validate.mjs +158 -0
  174. package/providers/mem0/manifest.json +47 -0
  175. package/providers/mem0/wizard.mjs +146 -0
  176. package/providers/zep/manifest.json +29 -0
  177. package/proxy/config.yaml +33 -0
  178. package/proxy/openmem_context/__init__.py +6 -0
  179. package/proxy/openmem_context/adapters.py +504 -0
  180. package/proxy/openmem_context/compress.py +53 -0
  181. package/proxy/openmem_context/core.py +606 -0
  182. package/proxy/openmem_context/embed.py +212 -0
  183. package/proxy/openmem_context/recall.py +49 -0
  184. package/proxy/openmem_context/session.py +70 -0
  185. package/proxy/openmem_context/sessiondb.py +276 -0
  186. package/proxy/openmem_context/settings.py +134 -0
  187. package/proxy/openmem_context/store.py +129 -0
  188. package/proxy/openmem_context/tokens.py +123 -0
  189. package/proxy/openmem_context/ttl.py +78 -0
  190. package/proxy/openmem_context_hook.py +34 -0
  191. package/scripts/check-pack.mjs +219 -0
  192. package/scripts/gen-masks.mjs +83 -0
  193. package/scripts/gen-masks.sh +4 -0
  194. package/scripts/promotion-provider-smoke.mjs +34 -0
  195. package/scripts/run-setup.sh +30 -0
  196. package/scripts/setup.mjs +107 -0
  197. package/scripts/setup.sh +5 -0
  198. package/scripts/spin-up.sh +45 -0
  199. package/scripts/wizard.sh +5 -0
  200. package/server.json +34 -0
  201. package/sync/Dockerfile +27 -0
  202. package/sync/memory_backend.py +87 -0
  203. package/sync/notebooklm_sync.py +664 -0
  204. package/wizard/bin/pm.mjs +97 -0
  205. package/wizard/bin/setup.mjs +33 -0
  206. package/wizard/lib/compose-version.mjs +32 -0
  207. package/wizard/lib/compose.mjs +70 -0
  208. package/wizard/lib/context-step.mjs +140 -0
  209. package/wizard/lib/curl.mjs +103 -0
  210. package/wizard/lib/docker-build.mjs +148 -0
  211. package/wizard/lib/exec.mjs +41 -0
  212. package/wizard/lib/indexer.mjs +6 -0
  213. package/wizard/lib/litellm-env.mjs +55 -0
  214. package/wizard/lib/load-machine-env.mjs +4 -0
  215. package/wizard/lib/log.mjs +32 -0
  216. package/wizard/lib/notebooklm-agent-access.mjs +92 -0
  217. package/wizard/lib/notebooklm.mjs +214 -0
  218. package/wizard/lib/parse-args.mjs +87 -0
  219. package/wizard/lib/paths.mjs +39 -0
  220. package/wizard/lib/pm-config.mjs +371 -0
  221. package/wizard/lib/ports.mjs +94 -0
  222. package/wizard/lib/prompts.mjs +85 -0
  223. package/wizard/lib/provider-wizard/helpers.mjs +98 -0
  224. package/wizard/lib/provider-wizard/run-steps.mjs +129 -0
  225. package/wizard/lib/providers/registry.mjs +262 -0
  226. package/wizard/lib/run-clack.mjs +108 -0
  227. package/wizard/lib/run-wizard.mjs +750 -0
  228. package/wizard/lib/validate-setup.mjs +62 -0
  229. package/wizard/lib/wizard-action.mjs +34 -0
  230. package/wizard/lib/wizard-conf.mjs +71 -0
@@ -0,0 +1,129 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ import { spawnSync } from "node:child_process";
5
+ import { info, warn } from "../log.mjs";
6
+ import { providersRoot } from "../providers/registry.mjs";
7
+
8
+ /** @type {Map<string, Record<string, unknown>>} */
9
+ const moduleCache = new Map();
10
+
11
+ /**
12
+ * Absolute path of the provider's hook module, or "" when the manifest points outside
13
+ * the provider directory.
14
+ * @param {import('../providers/registry.mjs').ProviderManifest} manifest
15
+ */
16
+ export function wizardModulePath(manifest) {
17
+ const dir = path.resolve(providersRoot(), manifest.id);
18
+ const file = path.resolve(dir, manifest.wizardHooks?.module || "wizard.mjs");
19
+ return file.startsWith(`${dir}${path.sep}`) ? file : "";
20
+ }
21
+
22
+ /**
23
+ * Import `providers/<id>/wizard.mjs` (cached per file). A provider without the module
24
+ * gets an empty export set, so every phase is a no-op for it.
25
+ * @param {import('../providers/registry.mjs').ProviderManifest} manifest
26
+ * @returns {Promise<Record<string, unknown>>}
27
+ */
28
+ export async function loadProviderWizard(manifest) {
29
+ const file = wizardModulePath(manifest);
30
+ if (!file || !fs.existsSync(file)) return {};
31
+ if (moduleCache.has(file)) return moduleCache.get(file);
32
+ const mod = await import(pathToFileURL(file).href);
33
+ moduleCache.set(file, mod);
34
+ return mod;
35
+ }
36
+
37
+ /** @typedef {keyof typeof PROVIDER_PHASES} ProviderPhase */
38
+
39
+ /**
40
+ * Hook names a provider module may export. Phases in ANNOUNCED print
41
+ * "provider <id>: <phase>" when they run; the rest are quiet queries.
42
+ */
43
+ export const PROVIDER_PHASES = {
44
+ // v1 phases (unchanged)
45
+ afterProvider: "afterProvider",
46
+ configureModelRouting: "configureModelRouting",
47
+ beforeStack: "beforeStack",
48
+ afterStackHealthy: "afterStackHealthy",
49
+ extendValidation: "extendValidation",
50
+ // v2 phases
51
+ init: "init",
52
+ resolvePorts: "resolvePorts",
53
+ setupEnv: "setupEnv",
54
+ stackEnv: "stackEnv",
55
+ hydrate: "hydrate",
56
+ configToml: "configToml",
57
+ summary: "summary",
58
+ apiBase: "apiBase",
59
+ smoke: "smoke",
60
+ setupCodingAgent: "setupCodingAgent",
61
+ };
62
+
63
+ const ANNOUNCED = new Set([
64
+ PROVIDER_PHASES.afterProvider,
65
+ PROVIDER_PHASES.configureModelRouting,
66
+ PROVIDER_PHASES.beforeStack,
67
+ PROVIDER_PHASES.afterStackHealthy,
68
+ PROVIDER_PHASES.extendValidation,
69
+ PROVIDER_PHASES.resolvePorts,
70
+ PROVIDER_PHASES.setupEnv,
71
+ PROVIDER_PHASES.smoke,
72
+ PROVIDER_PHASES.setupCodingAgent,
73
+ ]);
74
+
75
+ /**
76
+ * Call a provider hook and return its result. A provider that does not export the phase
77
+ * is a clean no-op: `{ ran: false }`.
78
+ * @param {import('../run-wizard.mjs').WizardContext} ctx
79
+ * @param {string} phase
80
+ * @param {import('./helpers.mjs').ProviderWizardHelpers} helpers
81
+ * @returns {Promise<{ ran: boolean, value?: any }>}
82
+ */
83
+ export async function callProviderHook(ctx, phase, helpers) {
84
+ const manifest = ctx.memoryProvider;
85
+ if (!manifest) return { ran: false };
86
+ const mod = await loadProviderWizard(manifest);
87
+ const fn = mod[phase];
88
+ if (typeof fn !== "function") return { ran: false };
89
+ if (ANNOUNCED.has(phase)) info(`provider ${manifest.id}: ${phase}`);
90
+ return { ran: true, value: await fn(ctx, helpers) };
91
+ }
92
+
93
+ /**
94
+ * @param {import('../run-wizard.mjs').WizardContext} ctx
95
+ * @param {string} phase
96
+ * @param {import('./helpers.mjs').ProviderWizardHelpers} helpers
97
+ * @returns {Promise<boolean>} whether the provider implements the phase
98
+ */
99
+ export async function runProviderPhase(ctx, phase, helpers) {
100
+ return (await callProviderHook(ctx, phase, helpers)).ran;
101
+ }
102
+
103
+ /**
104
+ * @param {import('../run-wizard.mjs').WizardContext} ctx
105
+ * @param {string} commandId
106
+ */
107
+ export async function runDelegatedCommand(ctx, commandId) {
108
+ const manifest = ctx.memoryProvider;
109
+ const delegates = manifest?.wizardHooks?.delegates || {};
110
+ const spec = delegates[commandId];
111
+ if (!spec?.run?.length) {
112
+ warn(`provider ${manifest?.id}: no delegate '${commandId}'`);
113
+ return false;
114
+ }
115
+ const [cmd, ...args] = spec.run;
116
+ info(`delegate ${manifest.id}/${commandId}: ${cmd} ${args.join(" ")}`);
117
+ if (ctx.dry) {
118
+ info(`[dry-run] ${cmd} ${args.join(" ")}`);
119
+ return true;
120
+ }
121
+ const r = spawnSync(cmd, args, { stdio: "inherit", env: process.env });
122
+ if (r.status !== 0) process.exit(r.status ?? 1);
123
+ return true;
124
+ }
125
+
126
+ /** @param {import('../providers/registry.mjs').ProviderManifest} manifest @returns {string[]} */
127
+ export function listDelegateCommands(manifest) {
128
+ return Object.keys(manifest.wizardHooks?.delegates || {});
129
+ }
@@ -0,0 +1,262 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { die } from "../log.mjs";
5
+
6
+ const DEFAULT_PACKAGE_ROOT = path.resolve(
7
+ path.dirname(fileURLToPath(import.meta.url)),
8
+ "../../..",
9
+ );
10
+
11
+ /** Provider used when nothing selects one. */
12
+ export const DEFAULT_PROVIDER_ID = "hindsight";
13
+
14
+ const PROVIDER_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
15
+ const PROFILE_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
16
+ /** Same rule as indexer/sinks/__init__.py NAME: the sink module name and the MEMORY_SINK value. */
17
+ const SINK_NAME_RE = /^[a-z][a-z0-9_]*$/;
18
+
19
+ /**
20
+ * Directory that holds `<id>/manifest.json` and `<id>/wizard.mjs`. Read-only code root.
21
+ * PM_PROVIDERS_DIR (tests, fixtures) beats PM_PACKAGE_ROOT/providers, which beats the
22
+ * checkout this module lives in.
23
+ */
24
+ export function providersRoot() {
25
+ if (process.env.PM_PROVIDERS_DIR) return path.resolve(process.env.PM_PROVIDERS_DIR);
26
+ return path.join(process.env.PM_PACKAGE_ROOT || DEFAULT_PACKAGE_ROOT, "providers");
27
+ }
28
+
29
+ /** @typedef {object} ProviderManifest
30
+ * @property {string} id
31
+ * @property {string} label
32
+ * @property {"supported"|"experimental"|"planned"} status
33
+ * @property {{ services?: string[], healthService?: string|null, healthPath?: string, hostPortEnv?: string, defaultHostPort?: number, profile?: string, files?: string[], logServices?: string[] }} compose
34
+ * `profile` adds `--profile <profile>` to every compose call; `files` are extra `-f`
35
+ * compose files relative to the stack root, added after compose.yaml.
36
+ * @property {object} indexer
37
+ * @property {object} codingAgent
38
+ * @property {object} wizard
39
+ * @property {{ module?: string, delegates?: Record<string, object> }} [wizardHooks]
40
+ */
41
+
42
+ /** @type {Map<string, ProviderManifest|null>} */
43
+ const cache = new Map();
44
+
45
+ /**
46
+ * @param {string} id
47
+ * @returns {ProviderManifest|null}
48
+ */
49
+ export function loadManifest(id) {
50
+ if (!PROVIDER_ID_RE.test(String(id))) return null;
51
+ const file = path.join(providersRoot(), id, "manifest.json");
52
+ if (cache.has(file)) return cache.get(file);
53
+ if (!fs.existsSync(file)) return null;
54
+ const manifest = JSON.parse(fs.readFileSync(file, "utf8"));
55
+ cache.set(file, manifest);
56
+ return manifest;
57
+ }
58
+
59
+ /** @returns {string[]} */
60
+ export function listProviderIds() {
61
+ const root = providersRoot();
62
+ if (!fs.existsSync(root)) return [];
63
+ return fs
64
+ .readdirSync(root, { withFileTypes: true })
65
+ .filter(
66
+ (d) =>
67
+ d.isDirectory() &&
68
+ !d.name.startsWith("_") &&
69
+ fs.existsSync(path.join(root, d.name, "manifest.json")),
70
+ )
71
+ .map((d) => d.name)
72
+ .sort();
73
+ }
74
+
75
+ /** @returns {ProviderManifest[]} */
76
+ export function listAllProviders() {
77
+ return listProviderIds()
78
+ .map((id) => loadManifest(id))
79
+ .filter(Boolean);
80
+ }
81
+
82
+ /** @returns {ProviderManifest[]} */
83
+ export function listSelectableProviders() {
84
+ return listAllProviders().filter(
85
+ (m) => m.status === "supported" || m.status === "experimental",
86
+ );
87
+ }
88
+
89
+ /** @returns {ProviderManifest[]} */
90
+ export function listPlannedProviders() {
91
+ return listAllProviders().filter((m) => m.status === "planned");
92
+ }
93
+
94
+ /**
95
+ * @param {import("../run-wizard.mjs").WizardContext} ctx
96
+ */
97
+ export function providerDefaultId(ctx) {
98
+ const fromConf = ctx.wizardConf && fs.existsSync(ctx.wizardConf)
99
+ ? readConfKey(ctx.wizardConf, "MEMORY_PROVIDER")
100
+ : "";
101
+ const fromToml = ctx.pmMerged
102
+ ? String(/** @type {Record<string, unknown>} */ (ctx.pmMerged.memory || {}).provider || "")
103
+ : "";
104
+ return (
105
+ process.env.MEMORY_PROVIDER ||
106
+ process.env.PM_MEMORY_PROVIDER ||
107
+ fromToml ||
108
+ fromConf ||
109
+ DEFAULT_PROVIDER_ID
110
+ );
111
+ }
112
+
113
+ function readConfKey(file, key) {
114
+ const prefix = `${key}=`;
115
+ for (const line of fs.readFileSync(file, "utf8").split("\n")) {
116
+ if (line.startsWith(prefix)) return line.slice(prefix.length).trim();
117
+ }
118
+ return "";
119
+ }
120
+
121
+ /**
122
+ * @param {string} [explicitId]
123
+ * @param {{ allowPlanned?: boolean }} [opts]
124
+ * @returns {ProviderManifest}
125
+ */
126
+ export function resolveProvider(explicitId, opts = {}) {
127
+ const fromEnv =
128
+ explicitId ||
129
+ process.env.MEMORY_PROVIDER ||
130
+ process.env.PM_MEMORY_PROVIDER ||
131
+ DEFAULT_PROVIDER_ID;
132
+ const manifest = loadManifest(fromEnv);
133
+ if (!manifest) die(`unknown memory provider '${fromEnv}' (see providers/*/manifest.json)`);
134
+ if (manifest.status === "planned" && !opts.allowPlanned) {
135
+ die(
136
+ `memory provider '${fromEnv}' is not implemented yet (${manifest.label}); pick a supported provider in ./setup or see docs/memory-providers.md`,
137
+ );
138
+ }
139
+ return manifest;
140
+ }
141
+
142
+ /** @param {unknown} v */
143
+ function isStringArray(v) {
144
+ return Array.isArray(v) && v.every((s) => typeof s === "string" && s.length > 0);
145
+ }
146
+
147
+ /**
148
+ * A compose file path from the manifest must stay inside the stack root.
149
+ * @param {string} f
150
+ */
151
+ function isStackRelativePath(f) {
152
+ if (path.isAbsolute(f) || f.includes("\0")) return false;
153
+ const norm = path.posix.normalize(f.replace(/\\/g, "/"));
154
+ return norm !== ".." && !norm.startsWith("../") && norm !== ".";
155
+ }
156
+
157
+ /**
158
+ * @param {ProviderManifest} manifest
159
+ * @returns {string[]}
160
+ */
161
+ export function validateManifest(manifest) {
162
+ const errors = [];
163
+ if (!manifest.id) errors.push("missing id");
164
+ else if (!PROVIDER_ID_RE.test(manifest.id)) errors.push(`invalid id: ${manifest.id}`);
165
+ if (!manifest.label) errors.push("missing label");
166
+ if (!["supported", "experimental", "planned"].includes(manifest.status)) {
167
+ errors.push(`invalid status: ${manifest.status}`);
168
+ }
169
+ if (manifest.status === "supported" || manifest.status === "experimental") {
170
+ const w = manifest.wizard || {};
171
+ for (const key of [
172
+ "smokeBank",
173
+ "retainPath",
174
+ "recallPath",
175
+ "deletePath",
176
+ "retainBody",
177
+ "recallBody",
178
+ ]) {
179
+ if (!w[key]) errors.push(`wizard.${key} required for ${manifest.status}`);
180
+ }
181
+ if (!manifest.compose?.healthPath) errors.push("compose.healthPath required");
182
+ if (!manifest.indexer?.sinkDriver) errors.push("indexer.sinkDriver required");
183
+ if (!manifest.indexer?.sink) errors.push("indexer.sink required");
184
+ }
185
+ const sink = manifest.indexer?.sink;
186
+ if (sink !== undefined && (typeof sink !== "string" || !SINK_NAME_RE.test(sink))) {
187
+ errors.push(`indexer.sink must match ${SINK_NAME_RE} (got ${JSON.stringify(sink)})`);
188
+ }
189
+
190
+ const compose = manifest.compose;
191
+ if (compose !== undefined) {
192
+ if (compose === null || typeof compose !== "object" || Array.isArray(compose)) {
193
+ errors.push("compose must be an object");
194
+ } else {
195
+ if (compose.profile !== undefined) {
196
+ if (typeof compose.profile !== "string" || !PROFILE_RE.test(compose.profile)) {
197
+ errors.push(`compose.profile must match ${PROFILE_RE} (got ${JSON.stringify(compose.profile)})`);
198
+ }
199
+ }
200
+ if (compose.files !== undefined) {
201
+ if (!isStringArray(compose.files)) {
202
+ errors.push("compose.files must be an array of non-empty strings");
203
+ } else {
204
+ for (const f of compose.files) {
205
+ if (!isStackRelativePath(f)) {
206
+ errors.push(`compose.files entry must be a path inside the stack root: ${f}`);
207
+ }
208
+ }
209
+ }
210
+ }
211
+ for (const key of ["services", "logServices"]) {
212
+ if (compose[key] !== undefined && !isStringArray(compose[key])) {
213
+ errors.push(`compose.${key} must be an array of non-empty strings`);
214
+ }
215
+ }
216
+ }
217
+ }
218
+ if (manifest.wizardHooks?.module !== undefined) {
219
+ const mod = manifest.wizardHooks.module;
220
+ if (typeof mod !== "string" || !mod || path.isAbsolute(mod) || mod.split(/[\\/]/).includes("..")) {
221
+ errors.push("wizardHooks.module must be a relative path inside the provider directory");
222
+ }
223
+ }
224
+ if (manifest.wizard?.recallHint !== undefined && typeof manifest.wizard.recallHint !== "string") {
225
+ errors.push("wizard.recallHint must be a string");
226
+ }
227
+ if (manifest.wizard?.retainHint !== undefined && typeof manifest.wizard.retainHint !== "string") {
228
+ errors.push("wizard.retainHint must be a string");
229
+ }
230
+ for (const key of ["smokeRetainTimeoutSec", "smokeRecallTimeoutSec"]) {
231
+ const v = manifest.wizard?.[key];
232
+ if (v !== undefined && (typeof v !== "number" || !Number.isFinite(v) || v <= 0)) {
233
+ errors.push(`wizard.${key} must be a positive number`);
234
+ }
235
+ }
236
+ return errors;
237
+ }
238
+
239
+ /**
240
+ * @param {ProviderManifest} provider
241
+ * @param {string} bank
242
+ * @param {string} template
243
+ */
244
+ export function expandPath(provider, bank, template) {
245
+ if (!template) return "";
246
+ return template.replace(/\{bank\}/g, encodeURIComponent(bank));
247
+ }
248
+
249
+ /**
250
+ * Base URL of the provider's HTTP API on this host, from the manifest's
251
+ * `compose.hostPortEnv` and `compose.defaultHostPort`. A provider that needs more
252
+ * (a remote URL, a path prefix) implements the `apiBase` hook instead.
253
+ * @param {import("../run-wizard.mjs").WizardContext} ctx
254
+ * @param {ProviderManifest} provider
255
+ */
256
+ export function providerApiBase(ctx, provider) {
257
+ if (ctx.apiUrl) return ctx.apiUrl;
258
+ const portEnv = provider.compose.hostPortEnv;
259
+ const port =
260
+ (portEnv && process.env[portEnv]) || String(provider.compose.defaultHostPort ?? 8888);
261
+ return `http://127.0.0.1:${port}`;
262
+ }
@@ -0,0 +1,108 @@
1
+ import * as p from "@clack/prompts";
2
+ import {
3
+ listAllProviders,
4
+ listSelectableProviders,
5
+ providerDefaultId,
6
+ resolveProvider,
7
+ } from "./providers/registry.mjs";
8
+ import { actionDefault } from "./wizard-action.mjs";
9
+ import { applyPmConfigEnv, defaultsFromPmConfig, loadPmConfig } from "./pm-config.mjs";
10
+ import { createHelpers } from "./provider-wizard/helpers.mjs";
11
+ import { loadProviderWizard } from "./provider-wizard/run-steps.mjs";
12
+ import path from "node:path";
13
+
14
+ /**
15
+ * Provider-neutral Clack setup UI: what to do (configure/up/down) and which provider.
16
+ * For a configure run the chosen provider's `clackSetup(ctx, ui, helpers)` hook asks its
17
+ * own questions and returns environment variables for the wizard process. A provider
18
+ * without the hook adds nothing.
19
+ *
20
+ * @param {string} repoRoot
21
+ * @returns {Promise<Record<string, string>>}
22
+ */
23
+ export async function runClackSetup(repoRoot) {
24
+ const home = process.env.HOME || "";
25
+ const wizardConf = path.join(home, ".config/hindsight/wizard.conf");
26
+ const ctxStub = { repoRoot, home, wizardConf };
27
+ const pm = loadPmConfig(home, repoRoot);
28
+ applyPmConfigEnv(pm.merged);
29
+ const pmDefaults = defaultsFromPmConfig(pm.merged);
30
+
31
+ p.intro("Personal memory setup");
32
+
33
+ const actionDefaultVal =
34
+ pmDefaults.wizardAction && ["configure", "up", "down"].includes(pmDefaults.wizardAction)
35
+ ? pmDefaults.wizardAction
36
+ : actionDefault();
37
+ const action = await p.select({
38
+ message: "What should setup do?",
39
+ options: [
40
+ {
41
+ value: "configure",
42
+ label: "Configure and start",
43
+ hint: "full wizard (default)",
44
+ },
45
+ { value: "up", label: "Start stack", hint: "docker compose up, existing config" },
46
+ { value: "down", label: "Stop stack", hint: "docker compose down" },
47
+ ],
48
+ initialValue: actionDefaultVal,
49
+ });
50
+ if (p.isCancel(action)) {
51
+ p.cancel("Setup cancelled.");
52
+ process.exit(0);
53
+ }
54
+
55
+ const selectable = listSelectableProviders();
56
+ const all = listAllProviders();
57
+ const planned = all.filter((m) => m.status === "planned");
58
+ if (planned.length) {
59
+ p.note(
60
+ planned.map((m) => `${m.label} (${m.id}): ${m.status}`).join("\n"),
61
+ "Planned providers (not selectable yet)",
62
+ );
63
+ }
64
+
65
+ ctxStub.pmMerged = pm.merged;
66
+ const providerDefault = providerDefaultId(ctxStub);
67
+ const providerPick = await p.select({
68
+ message: "Memory provider",
69
+ options: selectable.map((m) => ({
70
+ value: m.id,
71
+ label: m.label,
72
+ hint: m.status === "experimental" ? "experimental" : "supported",
73
+ })),
74
+ initialValue: selectable.some((m) => m.id === providerDefault)
75
+ ? providerDefault
76
+ : selectable[0]?.id,
77
+ });
78
+ if (p.isCancel(providerPick)) {
79
+ p.cancel("Setup cancelled.");
80
+ process.exit(0);
81
+ }
82
+
83
+ /** @type {Record<string, string>} */
84
+ const handoff = {
85
+ PM_WIZARD_ACTION: /** @type {string} */ (action),
86
+ MEMORY_PROVIDER: /** @type {string} */ (providerPick),
87
+ };
88
+
89
+ if (action === "down") {
90
+ p.outro("Stopping stack…");
91
+ return handoff;
92
+ }
93
+
94
+ if (action === "up") {
95
+ p.outro("Starting stack…");
96
+ return handoff;
97
+ }
98
+
99
+ const manifest = resolveProvider(/** @type {string} */ (providerPick));
100
+ const mod = await loadProviderWizard(manifest);
101
+ if (typeof mod.clackSetup === "function") {
102
+ const extra = await mod.clackSetup(ctxStub, p, createHelpers());
103
+ Object.assign(handoff, extra);
104
+ }
105
+
106
+ p.outro("Continuing setup…");
107
+ return handoff;
108
+ }