@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,371 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import TOML from "@iarna/toml";
4
+ import { writeFileAtomic } from "../../cli/coding-agent/file-io.mjs";
5
+ import { DEFAULT_PROVIDER_ID } from "./providers/registry.mjs";
6
+
7
+ /** Tables owned by install-coding-agent; the wizard carries them over when it rewrites the file. */
8
+ const INSTALLER_TABLES = ["agents", "context_proxy", "compaction", "context_store"];
9
+ const AGENT_INTEGRATIONS = ["openmem", "provider"];
10
+ const DEFAULT_CONTEXT_PROXY_PORT = 4100;
11
+ /** `[context_proxy.agents]` keys. opencode2 trims in its plugin; the others route through the proxy service. */
12
+ export const CONTEXT_AGENT_KEYS = ["claude-code", "opencode1", "codex", "opencode2"];
13
+ export const CONTEXT_PROXY_AGENT_KEYS = ["claude-code", "opencode1", "codex"];
14
+ // Same defaults as proxy/openmem_context/settings.py and cli/coding-agent/context-trim.mjs.
15
+ const DEFAULT_TRIM_AT_TOKENS = 120000;
16
+ const DEFAULT_COMPRESS_OVER_TOKENS = 4000;
17
+ const DEFAULT_COMPACTION_MAX_CHARS = 12000;
18
+ /** Config keys that reach the proxy container (stack .env) and the OpenCode 2 plugin as env. */
19
+ const CONTEXT_TRIM_ENV_KEYS = [
20
+ ["context_proxy", "trim_at_tokens", "CONTEXT_TRIM_AT_TOKENS"],
21
+ ["context_proxy", "compress_over_tokens", "CONTEXT_COMPRESS_OVER_TOKENS"],
22
+ ["compaction", "max_chars", "CONTEXT_COMPACTION_MAX_CHARS"],
23
+ ["context_store", "ttl_days", "CONTEXT_STORE_TTL_DAYS"],
24
+ ["context_store", "embedding_max_chars", "CONTEXT_EMBEDDING_MAX_CHARS"],
25
+ ];
26
+ /** String keys of `[context_store]` and their env names (session database vectors, docs/context-store.md). */
27
+ const CONTEXT_STORE_STRING_KEYS = [
28
+ ["embedding_model", "CONTEXT_EMBEDDING_MODEL"],
29
+ ["embedding_url", "CONTEXT_EMBEDDING_URL"],
30
+ ];
31
+ const CONFIG_MODE = 0o600;
32
+ const CONFIG_HEADER = "# Written by openmem — mise-like config.\n# Docs: docs/openmem-config.md\n";
33
+
34
+ /** @param {string} home */
35
+ export function userConfigPath(home) {
36
+ return path.join(home, ".config/personal-memory/config.toml");
37
+ }
38
+
39
+ /** @param {string} repoRoot */
40
+ export function projectConfigPaths(repoRoot) {
41
+ return [
42
+ path.join(repoRoot, "openmem.toml"),
43
+ path.join(repoRoot, "config/openmem.toml"),
44
+ // Pre-rebrand filenames still load if present.
45
+ path.join(repoRoot, "personal-memory.toml"),
46
+ path.join(repoRoot, "config/personal-memory.toml"),
47
+ ];
48
+ }
49
+
50
+ /**
51
+ * @param {string} file
52
+ * @returns {Record<string, unknown>}
53
+ */
54
+ function readTomlFile(file) {
55
+ if (!fs.existsSync(file)) return {};
56
+ try {
57
+ return TOML.parse(fs.readFileSync(file, "utf8"));
58
+ } catch (e) {
59
+ throw new Error(`${file}: ${/** @type {Error} */ (e).message}`);
60
+ }
61
+ }
62
+
63
+ /** Atomic rewrite; a new file is created mode 600, an existing one keeps its mode. */
64
+ function writeConfigFile(file, text) {
65
+ writeFileAtomic(file, text, fs.existsSync(file) ? {} : { mode: CONFIG_MODE });
66
+ }
67
+
68
+ /** @param {Record<string, unknown>} base @param {Record<string, unknown>} over */
69
+ function deepMerge(base, over) {
70
+ const out = { ...base };
71
+ for (const [k, v] of Object.entries(over)) {
72
+ if (v && typeof v === "object" && !Array.isArray(v) && typeof out[k] === "object") {
73
+ out[k] = deepMerge(/** @type {Record<string, unknown>} */ (out[k]), /** @type {Record<string, unknown>} */ (v));
74
+ } else out[k] = v;
75
+ }
76
+ return out;
77
+ }
78
+
79
+ /**
80
+ * @param {string} home
81
+ * @param {string} repoRoot
82
+ * @returns {{ merged: Record<string, unknown>, userPath: string, wroteFrom: string[] }}
83
+ */
84
+ export function loadPmConfig(home, repoRoot) {
85
+ let merged = {};
86
+ const wroteFrom = [];
87
+ for (const p of projectConfigPaths(repoRoot)) {
88
+ if (fs.existsSync(p)) {
89
+ merged = deepMerge(merged, readTomlFile(p));
90
+ wroteFrom.push(p);
91
+ }
92
+ }
93
+ const userPath = userConfigPath(home);
94
+ if (fs.existsSync(userPath)) {
95
+ merged = deepMerge(merged, readTomlFile(userPath));
96
+ wroteFrom.push(userPath);
97
+ }
98
+ return { merged, userPath, wroteFrom };
99
+ }
100
+
101
+ /**
102
+ * Flat defaults for the wizard (--yes / Clack initialValue). Provider-specific tables
103
+ * ([model], provider ports) are read by the provider's own hooks from `cfg`.
104
+ * @param {Record<string, unknown>} cfg
105
+ */
106
+ export function defaultsFromPmConfig(cfg) {
107
+ const memory = /** @type {Record<string, unknown>} */ (cfg.memory || {});
108
+ const stack = /** @type {Record<string, unknown>} */ (cfg.stack || {});
109
+ const wizard = /** @type {Record<string, unknown>} */ (cfg.wizard || {});
110
+
111
+ return {
112
+ memoryProvider: String(memory.provider || ""),
113
+ stackProject: String(stack.project || ""),
114
+ wizardAction: String(wizard.action || ""),
115
+ };
116
+ }
117
+
118
+ /**
119
+ * @param {import("./run-wizard.mjs").WizardContext} ctx
120
+ * @param {Record<string, Record<string, unknown>>} [sections] provider-owned tables:
121
+ * `stack` keys are merged into [stack]; `memory` sub-tables (for example `memory.mem0`) are
122
+ * merged into [memory], and `provider` there is always the active provider id; every other
123
+ * key becomes its own top-level table
124
+ * @returns {string}
125
+ */
126
+ export function buildPmConfigToml(ctx, sections = {}) {
127
+ const { stack: stackExtra = {}, memory: memoryExtra = {}, ...rest } = sections;
128
+ const doc = {
129
+ memory: { ...memoryExtra, provider: ctx.memoryProvider?.id || DEFAULT_PROVIDER_ID },
130
+ stack: { project: ctx.project, ...stackExtra },
131
+ wizard: { action: ctx.action || "configure" },
132
+ ...rest,
133
+ tasks: {
134
+ configure: { description: "Secrets, index sources, build images, compose up" },
135
+ up: { description: "Start stack using existing config" },
136
+ down: { description: "docker compose down for this project" },
137
+ doctor: { description: "Validate memory setup on this machine" },
138
+ },
139
+ };
140
+ return `# Written by ./setup — mise-like config for openmem.\n# Docs: docs/openmem-config.md\n${TOML.stringify(doc)}`;
141
+ }
142
+
143
+ /**
144
+ * @param {import("./run-wizard.mjs").WizardContext} ctx
145
+ * @param {Record<string, Record<string, unknown>>} [sections] see buildPmConfigToml
146
+ */
147
+ export function syncPmConfig(ctx, sections = {}) {
148
+ if (ctx.dry) return;
149
+ const dir = path.dirname(userConfigPath(ctx.home));
150
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
151
+ const file = userConfigPath(ctx.home);
152
+ let previous = {};
153
+ try {
154
+ previous = readTomlFile(file);
155
+ } catch {
156
+ // A malformed file is replaced, as before; nothing to carry over.
157
+ }
158
+ const kept = Object.fromEntries(
159
+ INSTALLER_TABLES.filter((t) => previous[t] && !(t in sections)).map((t) => [t, previous[t]]),
160
+ );
161
+ writeConfigFile(file, buildPmConfigToml(ctx, { ...sections, ...kept }));
162
+ }
163
+
164
+ /**
165
+ * Minimal in-place update of `memory.provider` in the user config.toml when the file
166
+ * already exists. Does not create the file (unlike syncPmConfig).
167
+ *
168
+ * @param {string} home
169
+ * @param {string} providerId
170
+ * @param {{ dry?: boolean }} [opts]
171
+ * @returns {"updated"|"skipped"|"dry-run"}
172
+ */
173
+ export function updatePmConfigProvider(home, providerId, opts = {}) {
174
+ const file = userConfigPath(home);
175
+ if (!fs.existsSync(file)) return "skipped";
176
+ if (opts.dry) return "dry-run";
177
+ const doc = readTomlFile(file);
178
+ const memory = {
179
+ ...(doc.memory && typeof doc.memory === "object" && !Array.isArray(doc.memory)
180
+ ? /** @type {Record<string, unknown>} */ (doc.memory)
181
+ : {}),
182
+ provider: providerId,
183
+ };
184
+ doc.memory = memory;
185
+ writeConfigFile(file, `${CONFIG_HEADER}${TOML.stringify(doc)}`);
186
+ return "updated";
187
+ }
188
+
189
+ /**
190
+ * Merge `values` into one top-level table of the user config.toml, creating the file
191
+ * (mode 600, directory 700) when it is missing. Other tables are kept.
192
+ *
193
+ * @param {string} home
194
+ * @param {string} table
195
+ * @param {Record<string, unknown>} values
196
+ * @param {{ dry?: boolean }} [opts]
197
+ * @returns {"updated"|"dry-run"}
198
+ */
199
+ export function updatePmConfigTable(home, table, values, opts = {}) {
200
+ if (opts.dry) return "dry-run";
201
+ const file = userConfigPath(home);
202
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
203
+ const doc = readTomlFile(file);
204
+ const current = doc[table] && typeof doc[table] === "object" && !Array.isArray(doc[table])
205
+ ? /** @type {Record<string, unknown>} */ (doc[table])
206
+ : {};
207
+ doc[table] = { ...current, ...values };
208
+ writeConfigFile(file, `${CONFIG_HEADER}${TOML.stringify(doc)}`);
209
+ return "updated";
210
+ }
211
+
212
+ /**
213
+ * Write per-agent context management values into `[context_proxy.agents]` (merged with saved keys) and set
214
+ * `[context_proxy] enabled` to whether any agent resolves on. Other `[context_proxy]` keys are kept.
215
+ * @param {string} home
216
+ * @param {Record<string, boolean>} values keys from CONTEXT_AGENT_KEYS
217
+ * @param {{ dry?: boolean }} [opts]
218
+ * @returns {{ result: "updated"|"dry-run", agents: Record<string, boolean>, enabled: boolean }}
219
+ */
220
+ export function updatePmConfigContextAgents(home, values, opts = {}) {
221
+ const file = userConfigPath(home);
222
+ const doc = fs.existsSync(file) ? readTomlFile(file) : {};
223
+ const current = contextAgentsFromPmConfig(doc).agents;
224
+ const agents = { ...current, ...values };
225
+ const enabled = CONTEXT_AGENT_KEYS.some((k) => agents[k]);
226
+ const result = updatePmConfigTable(home, "context_proxy", { enabled, agents }, opts);
227
+ return { result, agents, enabled };
228
+ }
229
+
230
+ /**
231
+ * `[agents] integration`: "openmem" (OpenMem hooks) or "provider" (the provider's own plugin).
232
+ * @param {Record<string, unknown>} cfg
233
+ */
234
+ export function agentIntegrationFromPmConfig(cfg) {
235
+ const value = String(/** @type {Record<string, unknown>} */ (cfg.agents || {}).integration || "");
236
+ return AGENT_INTEGRATIONS.includes(value) ? value : "openmem";
237
+ }
238
+
239
+ /** @param {Record<string, unknown>} cfg @param {string} name */
240
+ function tableOf(cfg, name) {
241
+ const t = cfg?.[name];
242
+ return t && typeof t === "object" && !Array.isArray(t) ? /** @type {Record<string, unknown>} */ (t) : {};
243
+ }
244
+
245
+ /** A non-negative integer config value, or undefined when unset or invalid. */
246
+ function countValue(value) {
247
+ if (value === null || value === undefined || typeof value === "boolean" || typeof value === "string") return undefined;
248
+ const n = Number(value);
249
+ return Number.isSafeInteger(n) && n >= 0 ? n : undefined;
250
+ }
251
+
252
+ /**
253
+ * `[context_proxy]` wiring with defaults: disabled, port 4100.
254
+ * @param {Record<string, unknown>} cfg
255
+ */
256
+ export function contextProxyFromPmConfig(cfg) {
257
+ const table = tableOf(cfg, "context_proxy");
258
+ const port = Number(table.port);
259
+ return {
260
+ enabled: table.enabled === true,
261
+ port: Number.isInteger(port) && port > 0 && port < 65536 ? port : DEFAULT_CONTEXT_PROXY_PORT,
262
+ };
263
+ }
264
+
265
+ /**
266
+ * Per-agent context management: `[context_proxy.agents] <key> = true|false`, a missing key following
267
+ * `[context_proxy] enabled`; with neither set, opencode2 is on (it needs no service) and the proxy-routed
268
+ * agents are off. Keys: CONTEXT_AGENT_KEYS. `saved` lists the keys that config.toml sets.
269
+ * @param {Record<string, unknown>} cfg
270
+ * @returns {{ agents: Record<string, boolean>, saved: string[], enabledSaved: boolean }}
271
+ */
272
+ export function contextAgentsFromPmConfig(cfg) {
273
+ const table = tableOf(cfg, "context_proxy");
274
+ const overrides = tableOf(table, "agents");
275
+ const enabledSaved = typeof table.enabled === "boolean";
276
+ const agents = {};
277
+ const saved = [];
278
+ for (const key of CONTEXT_AGENT_KEYS) {
279
+ if (typeof overrides[key] === "boolean") {
280
+ agents[key] = overrides[key];
281
+ saved.push(key);
282
+ } else agents[key] = enabledSaved ? table.enabled : key === "opencode2";
283
+ }
284
+ return { agents, saved, enabledSaved };
285
+ }
286
+
287
+ /**
288
+ * Trim settings with defaults: `[context_proxy] trim_at_tokens` (120000; unset, the proxy and plugin trim Haiku at 95000), `compress_over_tokens`
289
+ * (4000), `standing_instruction` (true), `[compaction] max_chars` (12000), `native` (true). Env overrides are applied by
290
+ * `contextTrimEnvFromPmConfig` + `trimSettings()`, not here.
291
+ * @param {Record<string, unknown>} cfg
292
+ */
293
+ export function contextTrimFromPmConfig(cfg) {
294
+ const proxy = tableOf(cfg, "context_proxy");
295
+ const compaction = tableOf(cfg, "compaction");
296
+ return {
297
+ trimAtTokens: countValue(proxy.trim_at_tokens) ?? DEFAULT_TRIM_AT_TOKENS,
298
+ compressOverTokens: countValue(proxy.compress_over_tokens) ?? DEFAULT_COMPRESS_OVER_TOKENS,
299
+ compactionMaxChars: countValue(compaction.max_chars) ?? DEFAULT_COMPACTION_MAX_CHARS,
300
+ compactionNative: compaction.native !== false,
301
+ standingInstruction: proxy.standing_instruction !== false,
302
+ };
303
+ }
304
+
305
+ /**
306
+ * Context-trim settings that config.toml sets, as the environment variables the proxy and
307
+ * `trimSettings()` read: `[context_proxy] trim_at_tokens` → CONTEXT_TRIM_AT_TOKENS,
308
+ * `compress_over_tokens` → CONTEXT_COMPRESS_OVER_TOKENS, `[compaction] max_chars` →
309
+ * CONTEXT_COMPACTION_MAX_CHARS, `[context_proxy] standing_instruction` (boolean) →
310
+ * CONTEXT_STANDING_INSTRUCTION ("1" / "0"), `[compaction] recall` (boolean) → CONTEXT_COMPACTION_RECALL
311
+ * ("1" / "0"), `[context_proxy] chars_per_token` (positive number) →
312
+ * CONTEXT_CHARS_PER_TOKEN, `trim_to_fraction` (0 < x < 1) → CONTEXT_TRIM_TO_FRACTION, `[context_store] ttl_days` → CONTEXT_STORE_TTL_DAYS, `embedding_model` →
313
+ * CONTEXT_EMBEDDING_MODEL, `embedding_url` → CONTEXT_EMBEDDING_URL, `embedding_timeout_s` (positive number) →
314
+ * CONTEXT_EMBEDDING_TIMEOUT_S, `embedding_max_chars` → CONTEXT_EMBEDDING_MAX_CHARS. Unset or invalid keys are left out, so the reader's defaults
315
+ * apply. Real env vars override: `trimSettings({ ...contextTrimEnvFromPmConfig(cfg), ...process.env })`.
316
+ * @param {Record<string, unknown>} cfg
317
+ * @returns {Record<string, string>}
318
+ */
319
+ export function contextTrimEnvFromPmConfig(cfg) {
320
+ const out = {};
321
+ for (const [table, key, envName] of CONTEXT_TRIM_ENV_KEYS) {
322
+ const value = countValue(tableOf(cfg, table)[key]);
323
+ if (value !== undefined) out[envName] = String(value);
324
+ }
325
+ const standing = tableOf(cfg, "context_proxy").standing_instruction;
326
+ if (typeof standing === "boolean") out.CONTEXT_STANDING_INSTRUCTION = standing ? "1" : "0";
327
+ const recall = tableOf(cfg, "compaction").recall;
328
+ if (typeof recall === "boolean") out.CONTEXT_COMPACTION_RECALL = recall ? "1" : "0";
329
+ const fraction = tableOf(cfg, "context_proxy").trim_to_fraction;
330
+ if (typeof fraction === "number" && Number.isFinite(fraction) && fraction > 0 && fraction < 1) {
331
+ out.CONTEXT_TRIM_TO_FRACTION = String(fraction);
332
+ }
333
+ const ratio = tableOf(cfg, "context_proxy").chars_per_token;
334
+ if (typeof ratio === "number" && Number.isFinite(ratio) && ratio > 0) out.CONTEXT_CHARS_PER_TOKEN = String(ratio);
335
+ const store = tableOf(cfg, "context_store");
336
+ const timeout = store.embedding_timeout_s;
337
+ if (typeof timeout === "number" && Number.isFinite(timeout) && timeout > 0) out.CONTEXT_EMBEDDING_TIMEOUT_S = String(timeout);
338
+ for (const [key, envName] of CONTEXT_STORE_STRING_KEYS) {
339
+ if (typeof store[key] === "string") out[envName] = store[key].trim();
340
+ }
341
+ return out;
342
+ }
343
+
344
+ /**
345
+ * Stack .env entries for the context-proxy compose service: the trim env above plus
346
+ * CONTEXT_PROXY_PORT when `[context_proxy] port` is set, so the container binds the port
347
+ * Claude Code is pointed at.
348
+ * @param {Record<string, unknown>} cfg
349
+ * @returns {Record<string, string>}
350
+ */
351
+ export function contextProxyStackEnvFromPmConfig(cfg) {
352
+ const out = contextTrimEnvFromPmConfig(cfg);
353
+ if (tableOf(cfg, "context_proxy").port !== undefined) out.CONTEXT_PROXY_PORT = String(contextProxyFromPmConfig(cfg).port);
354
+ return out;
355
+ }
356
+
357
+ /**
358
+ * Apply config defaults into process.env when env/CLI did not set them. Only the
359
+ * provider-neutral keys live here; a provider maps its own tables in its `init` hook.
360
+ * @param {Record<string, unknown>} cfg
361
+ */
362
+ export function applyPmConfigEnv(cfg) {
363
+ const d = defaultsFromPmConfig(cfg);
364
+ if (d.memoryProvider && !process.env.MEMORY_PROVIDER) {
365
+ process.env.MEMORY_PROVIDER = d.memoryProvider;
366
+ }
367
+ if (d.stackProject && !process.env.PM_PROJECT) process.env.PM_PROJECT = d.stackProject;
368
+ if (d.wizardAction && !process.env.PM_WIZARD_ACTION) {
369
+ process.env.PM_WIZARD_ACTION = d.wizardAction;
370
+ }
371
+ }
@@ -0,0 +1,94 @@
1
+ import net from "node:net";
2
+ import path from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+ import { envValue } from "./paths.mjs";
5
+ import { ok, warn, die } from "./log.mjs";
6
+ import { confirm } from "./prompts.mjs";
7
+
8
+ /**
9
+ * @param {number} port
10
+ * @returns {Promise<boolean>}
11
+ */
12
+ export function portFree(port) {
13
+ return new Promise((resolve) => {
14
+ const server = net.createServer();
15
+ server.unref();
16
+ server.on("error", () => resolve(false));
17
+ server.listen({ port, host: "127.0.0.1", exclusive: true }, () => {
18
+ server.close(() => resolve(true));
19
+ });
20
+ });
21
+ }
22
+
23
+ /**
24
+ * Keep a host port when it is free (or held by this compose project); otherwise offer
25
+ * the next free one.
26
+ * @param {import("./run-wizard.mjs").WizardContext} ctx
27
+ * @param {string} label
28
+ * @param {number} port
29
+ * @param {{ envVars?: string[] }} [opts] env var names that override the ports, for messages
30
+ * @returns {Promise<number>}
31
+ */
32
+ export async function choosePort(ctx, label, port, opts = {}) {
33
+ const envVars = opts.envVars || [];
34
+ if (await portFree(port)) {
35
+ ok(`${label} port ${port} is free`);
36
+ return port;
37
+ }
38
+ let owner = "";
39
+ const ps = spawnSync(
40
+ "docker",
41
+ ["ps", "--filter", `publish=${port}`, "--format", "{{.Label \"com.docker.compose.project\"}}"],
42
+ { encoding: "utf8" },
43
+ );
44
+ if (ps.status === 0 && ps.stdout) {
45
+ owner = ps.stdout.split("\n").find((l) => l.trim())?.trim() || "";
46
+ }
47
+ if (owner === ctx.project) {
48
+ ok(`${label} port ${port} is held by this project (${ctx.project})`);
49
+ return port;
50
+ }
51
+ if (owner) warn(`${label} port ${port} is held by compose project '${owner}'`);
52
+ else warn(`${label} port ${port} is held by another process`);
53
+
54
+ let alt = 0;
55
+ for (let i = 1; i <= 50; i++) {
56
+ if (await portFree(port + i)) {
57
+ alt = port + i;
58
+ break;
59
+ }
60
+ }
61
+ if (!alt) {
62
+ die(
63
+ `no free port near ${port}${envVars.length ? `; set ${envVars.join(" or ")} and` : ";"} rerun`,
64
+ );
65
+ }
66
+ if (await confirm(ctx, `Use port ${alt} for the ${label} instead (saved to ${path.join(ctx.repoRoot, ".env")})?`, "y")) {
67
+ return alt;
68
+ }
69
+ die(`free port ${port}${envVars.length ? ` (or set ${envVars.join(" / ")})` : ""} and rerun ./setup`);
70
+ }
71
+
72
+ /**
73
+ * Resolve one host port from the active provider's manifest (`compose.hostPortEnv`,
74
+ * `compose.defaultHostPort`): process env, then the stack .env, then the default.
75
+ * Exports the chosen port in process.env under the manifest's variable name.
76
+ * @param {import("./run-wizard.mjs").WizardContext} ctx
77
+ * @param {string} [label]
78
+ * @returns {Promise<number>}
79
+ */
80
+ export async function resolveHostPort(ctx, label) {
81
+ const compose = ctx.memoryProvider?.compose || {};
82
+ const envName = compose.hostPortEnv;
83
+ if (!envName) die(`provider ${ctx.memoryProvider?.id} declares no compose.hostPortEnv`);
84
+ const wanted =
85
+ process.env[envName] ||
86
+ envValue(path.join(ctx.repoRoot, ".env"), envName) ||
87
+ String(compose.defaultHostPort ?? 8888);
88
+ if (!/^\d+$/.test(wanted)) die(`${envName} must be a port number`);
89
+ const port = await choosePort(ctx, label || `${ctx.memoryProvider.label} API`, Number(wanted), {
90
+ envVars: [envName],
91
+ });
92
+ process.env[envName] = String(port);
93
+ return port;
94
+ }
@@ -0,0 +1,85 @@
1
+ import readline from "node:readline/promises";
2
+ import { stdin as input, stdout as output } from "node:process";
3
+
4
+ /**
5
+ * @param {import("./run-wizard.mjs").WizardContext} ctx
6
+ * @param {string} prompt
7
+ * @param {string} defaultValue
8
+ */
9
+ export async function ask(ctx, prompt, defaultValue) {
10
+ if (ctx.yes) return defaultValue;
11
+ if (!process.stdin.isTTY) return defaultValue;
12
+ try {
13
+ const clack = await import("@clack/prompts");
14
+ const v = await clack.text({
15
+ message: prompt,
16
+ placeholder: defaultValue,
17
+ defaultValue,
18
+ });
19
+ if (clack.isCancel(v)) process.exit(0);
20
+ const s = String(v ?? "").trim();
21
+ return s || defaultValue;
22
+ } catch {
23
+ const rl = readline.createInterface({ input, output });
24
+ const line = await rl.question(` ${prompt} [${defaultValue}]: `);
25
+ rl.close();
26
+ const s = line.trim();
27
+ return s || defaultValue;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Pick any of `options`; `--yes` and non-TTY runs return `initial`.
33
+ * @param {import("./run-wizard.mjs").WizardContext} ctx
34
+ * @param {string} prompt
35
+ * @param {{ value: string, label: string }[]} options
36
+ * @param {string[]} initial
37
+ * @returns {Promise<string[]>}
38
+ */
39
+ export async function multiselect(ctx, prompt, options, initial) {
40
+ if (ctx.yes) return initial;
41
+ if (!process.stdin.isTTY) return initial;
42
+ try {
43
+ const clack = await import("@clack/prompts");
44
+ const v = await clack.multiselect({ message: prompt, options, initialValues: initial, required: false });
45
+ if (clack.isCancel(v)) process.exit(0);
46
+ return /** @type {string[]} */ (v);
47
+ } catch {
48
+ const rl = readline.createInterface({ input, output });
49
+ const names = options.map((o) => o.value).join(", ");
50
+ const line = await rl.question(` ${prompt} (comma-separated: ${names}; "none" for none) [${initial.join(",") || "none"}]: `);
51
+ rl.close();
52
+ const s = line.trim();
53
+ if (!s) return initial;
54
+ if (s === "none") return [];
55
+ return s.split(",").map((x) => x.trim()).filter((x) => options.some((o) => o.value === x));
56
+ }
57
+ }
58
+
59
+ /**
60
+ * @param {import("./run-wizard.mjs").WizardContext} ctx
61
+ * @param {string} prompt
62
+ * @param {"y"|"n"} defaultYN
63
+ */
64
+ export async function confirm(ctx, prompt, defaultYN) {
65
+ if (ctx.yes) return defaultYN === "y";
66
+ if (!process.stdin.isTTY) return defaultYN === "y";
67
+ try {
68
+ const clack = await import("@clack/prompts");
69
+ const v = await clack.confirm({
70
+ message: prompt,
71
+ active: "yes",
72
+ inactive: "no",
73
+ initialValue: defaultYN === "y",
74
+ });
75
+ if (clack.isCancel(v)) process.exit(0);
76
+ return Boolean(v);
77
+ } catch {
78
+ const hint = defaultYN === "y" ? "Y/n" : "y/N";
79
+ const rl = readline.createInterface({ input, output });
80
+ const line = await rl.question(` ${prompt} [${hint}]: `);
81
+ rl.close();
82
+ const reply = (line.trim() || defaultYN).toLowerCase();
83
+ return reply === "y" || reply === "yes";
84
+ }
85
+ }
@@ -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
+ }