@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,98 @@
1
+ import { step, info, ok, warn, die, problem } from "../log.mjs";
2
+ import { ask, confirm } from "../prompts.mjs";
3
+ import { act, actWithEnv, commandExists } from "../exec.mjs";
4
+ import { curlGet, curlJson, curlRequest } from "../curl.mjs";
5
+ import { loadMachineEnv } from "../load-machine-env.mjs";
6
+ import { envValue, expandHome, ensureHsDir } from "../paths.mjs";
7
+ import { portFree, choosePort, resolveHostPort } from "../ports.mjs";
8
+ import { composeArgs } from "../compose.mjs";
9
+ import { writeWizardConf } from "../wizard-conf.mjs";
10
+ import { providerApiBase } from "../providers/registry.mjs";
11
+
12
+ /** Bump when a helper is removed or changes meaning; adding helpers does not bump it. */
13
+ export const HELPERS_API_VERSION = 2;
14
+
15
+ /**
16
+ * API passed as the second argument of every hook in providers/<id>/wizard.mjs.
17
+ * Providers must not import run-wizard.mjs (it imports them); everything the meta wizard
18
+ * owns reaches them through this object. Check `apiVersion` before relying on a helper.
19
+ *
20
+ * Every function that takes `ctx` receives the WizardContext the hook was called with
21
+ * (`ctx.repoRoot` = mutable stack root, `ctx.packageRoot` = read-only code root).
22
+ *
23
+ * @typedef {object} ProviderWizardHelpers
24
+ * @property {2} apiVersion
25
+ * @property {(title: string) => void} step print a numbered step heading
26
+ * @property {(msg: string) => void} info
27
+ * @property {(msg: string) => void} ok
28
+ * @property {(msg: string) => void} warn
29
+ * @property {(msg: string) => never} die print an error and exit 1
30
+ * @property {(ctx: object, msg: string) => void} problem die, or warn in a dry run
31
+ * @property {(ctx: object, prompt: string, defaultValue: string) => Promise<string>} ask
32
+ * @property {(ctx: object, prompt: string, defaultYN: "y"|"n") => Promise<boolean>} confirm
33
+ * @property {(ctx: object, description: string, cmd: string, args: string[], opts?: { env?: Record<string, string>, cwd?: string }) => Promise<void>} act
34
+ * run a command (printed, not run, in a dry run); exits the wizard on failure
35
+ * @property {(ctx: object, description: string, cmd: string, args: string[], extraEnv: Record<string, string>) => Promise<void>} actWithEnv
36
+ * @property {(name: string) => boolean} commandExists
37
+ * @property {(repoRoot: string) => Record<string, string>} loadMachineEnv deprecated; always {}
38
+ * @property {(file: string, key: string) => string} envValue read one KEY=value from an env file
39
+ * @property {(p: string, home?: string) => string} expandHome
40
+ * @property {(ctx: object) => void} ensureConfigDir create the per-machine config dir (no-op in a dry run)
41
+ * @property {(url: string, timeoutSec?: number) => string} curlGet
42
+ * @property {(url: string) => object|null} curlJson
43
+ * @property {(method: string, url: string, jsonBody?: string, timeoutSec?: number) => string} curlRequest
44
+ * @property {(port: number) => Promise<boolean>} portFree
45
+ * @property {(ctx: object, label: string, port: number, opts?: { envVars?: string[] }) => Promise<number>} choosePort
46
+ * @property {(ctx: object, label?: string) => Promise<number>} resolveHostPort
47
+ * pick the manifest's host port (compose.hostPortEnv / defaultHostPort) and export it in process.env
48
+ * @property {(ctx: object, ...rest: string[]) => string[]} composeArgs
49
+ * `docker` args for a compose subcommand, with the manifest's profile and files applied
50
+ * @property {(ctx: object, extra?: Record<string, string>, opts?: { pmSections?: Record<string, Record<string, unknown>> }) => void} saveWizardConf
51
+ * write wizard.conf (MEMORY_PROVIDER + extra keys) and config.toml; no-op in a dry run
52
+ * @property {(ctx: object, opts?: { description?: string, args?: string[], env?: Record<string, string> }) => Promise<void>} runStackSetup
53
+ * run scripts/setup.mjs (writes the stack .env and compose.override.yaml, then the provider's
54
+ * `prepareStackEnv`); `description` and `env` default to what the `stackEnv` hook returns
55
+ * @property {(ctx: object) => Promise<string>|string} apiBase the provider's API base URL (apiBase hook, else manifest)
56
+ */
57
+
58
+ /**
59
+ * Build the helper object. With no argument it is fully functional from leaf modules;
60
+ * the meta wizard overrides the few helpers that need its own state.
61
+ * @param {Partial<ProviderWizardHelpers>} [impl]
62
+ * @returns {ProviderWizardHelpers}
63
+ */
64
+ export function createHelpers(impl = {}) {
65
+ return Object.freeze(
66
+ /** @type {ProviderWizardHelpers} */ ({
67
+ apiVersion: HELPERS_API_VERSION,
68
+ step,
69
+ info,
70
+ ok,
71
+ warn,
72
+ die,
73
+ problem,
74
+ ask,
75
+ confirm,
76
+ act,
77
+ actWithEnv,
78
+ commandExists,
79
+ loadMachineEnv,
80
+ envValue,
81
+ expandHome,
82
+ ensureConfigDir: ensureHsDir,
83
+ curlGet,
84
+ curlJson,
85
+ curlRequest,
86
+ portFree,
87
+ choosePort,
88
+ resolveHostPort,
89
+ composeArgs,
90
+ saveWizardConf: writeWizardConf,
91
+ runStackSetup: async () => {
92
+ throw new Error("runStackSetup is only available inside the meta wizard");
93
+ },
94
+ apiBase: (ctx) => providerApiBase(ctx, ctx.memoryProvider),
95
+ ...impl,
96
+ }),
97
+ );
98
+ }
@@ -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
+ }