@bcmyguest/openmem 0.6.3 → 0.9.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 (104) hide show
  1. package/{skills → .agents/skills}/openmem/SKILL.md +5 -0
  2. package/README.md +81 -76
  3. package/cli/coding-agent/capabilities.mjs +14 -3
  4. package/cli/coding-agent/context-store.mjs +81 -0
  5. package/cli/coding-agent/context-trim.mjs +899 -0
  6. package/cli/coding-agent/file-io.mjs +72 -0
  7. package/cli/coding-agent/hook-harnesses.mjs +2 -1
  8. package/cli/coding-agent/hook-runner.mjs +28 -4
  9. package/cli/coding-agent/install-access.mjs +43 -17
  10. package/cli/coding-agent/install-context-proxy.mjs +333 -0
  11. package/cli/coding-agent/install-rtk.mjs +60 -0
  12. package/cli/coding-agent/install-shim.mjs +119 -15
  13. package/cli/coding-agent/opencode-config.mjs +21 -5
  14. package/cli/coding-agent/opencode-plugin.mjs +39 -3
  15. package/cli/coding-agent/opencode2-plugin/index.js +299 -0
  16. package/cli/coding-agent/recall-frame.mjs +29 -0
  17. package/cli/coding-agent/remove-upstream-access.mjs +122 -0
  18. package/cli/coding-agent/runtime.mjs +2 -1
  19. package/cli/coding-agent/session-db.mjs +626 -0
  20. package/cli/coding-agent/session-facade.mjs +30 -6
  21. package/cli/coding-agent/toml-edit.mjs +161 -0
  22. package/cli/context-management.mjs +254 -0
  23. package/cli/feed-sync.mjs +18 -2
  24. package/cli/install-coding-agent.mjs +173 -40
  25. package/cli/mcp/context-sql-runner.mjs +58 -0
  26. package/cli/mcp/context-store.mjs +670 -0
  27. package/cli/mcp/knowledge.mjs +333 -0
  28. package/cli/mcp/memory-facade.mjs +99 -33
  29. package/cli/openmem-mcp.mjs +3 -0
  30. package/cli/openmem.mjs +18 -1
  31. package/cli/schedule.mjs +176 -0
  32. package/cli/stack.mjs +2 -0
  33. package/cli/switch-memory-provider.mjs +1 -1
  34. package/compose.yaml +67 -4
  35. package/config/hindsight.env.example +2 -0
  36. package/config/openmem.toml.example +64 -2
  37. package/docs/architecture.md +35 -9
  38. package/docs/coding-agent-hooks.md +26 -2
  39. package/docs/coding-agents.md +27 -7
  40. package/docs/compatibility.md +80 -0
  41. package/docs/concepts.md +30 -7
  42. package/docs/context-management.md +230 -0
  43. package/docs/context-proxy.md +115 -0
  44. package/docs/context-research.md +226 -0
  45. package/docs/context-store.md +277 -0
  46. package/docs/evidence-plan.md +216 -0
  47. package/docs/hindsight.md +5 -2
  48. package/docs/mcp-setup.md +5 -2
  49. package/docs/mem0.md +39 -1
  50. package/docs/memory-providers.md +2 -2
  51. package/docs/meridian-litellm.md +198 -0
  52. package/docs/model-configuration.md +22 -0
  53. package/docs/npx-install.md +6 -4
  54. package/docs/openmem-config.md +2 -2
  55. package/docs/quickstart.md +12 -6
  56. package/docs/setup-wizard.md +5 -4
  57. package/docs/sources-feeds.md +9 -22
  58. package/package.json +5 -4
  59. package/providers/cognee/OPERATOR.md +2 -2
  60. package/providers/hindsight/lib/clack.mjs +122 -20
  61. package/providers/hindsight/lib/coding-agent.mjs +1 -1
  62. package/providers/hindsight/lib/config.mjs +6 -0
  63. package/providers/hindsight/lib/env-setup.mjs +2 -1
  64. package/providers/hindsight/lib/litellm-routes.mjs +21 -12
  65. package/providers/hindsight/lib/meridian.mjs +307 -0
  66. package/providers/hindsight/lib/model-backend.mjs +38 -9
  67. package/providers/hindsight/lib/probe.mjs +21 -0
  68. package/providers/hindsight/lib/stack-env.mjs +5 -12
  69. package/providers/hindsight/lib/state.mjs +6 -1
  70. package/providers/hindsight/lib/summary.mjs +16 -2
  71. package/providers/hindsight/lib/validate.mjs +6 -0
  72. package/providers/hindsight/wizard.mjs +3 -2
  73. package/providers/mem0/OPERATOR.md +21 -2
  74. package/providers/mem0/compose.yaml +24 -2
  75. package/providers/mem0/lib/coding-agent.mjs +1 -1
  76. package/providers/mem0/lib/configure.mjs +66 -0
  77. package/providers/mem0/lib/litellm.mjs +457 -0
  78. package/providers/mem0/lib/stack-env.mjs +40 -1
  79. package/providers/mem0/lib/summary.mjs +30 -2
  80. package/providers/mem0/manifest.json +1 -1
  81. package/providers/mem0/wizard.mjs +15 -2
  82. package/proxy/config.yaml +33 -0
  83. package/proxy/openmem_context/__init__.py +6 -0
  84. package/proxy/openmem_context/adapters.py +504 -0
  85. package/proxy/openmem_context/compress.py +53 -0
  86. package/proxy/openmem_context/core.py +608 -0
  87. package/proxy/openmem_context/embed.py +212 -0
  88. package/proxy/openmem_context/recall.py +49 -0
  89. package/proxy/openmem_context/session.py +70 -0
  90. package/proxy/openmem_context/sessiondb.py +276 -0
  91. package/proxy/openmem_context/settings.py +134 -0
  92. package/proxy/openmem_context/store.py +129 -0
  93. package/proxy/openmem_context/tokens.py +123 -0
  94. package/proxy/openmem_context/ttl.py +78 -0
  95. package/proxy/openmem_context_hook.py +34 -0
  96. package/scripts/check-pack.mjs +2 -1
  97. package/scripts/run-setup.sh +14 -5
  98. package/scripts/setup.mjs +14 -1
  99. package/server.json +2 -2
  100. package/wizard/lib/context-step.mjs +140 -0
  101. package/wizard/lib/litellm-env.mjs +55 -0
  102. package/wizard/lib/pm-config.mjs +212 -4
  103. package/wizard/lib/prompts.mjs +28 -0
  104. package/wizard/lib/run-wizard.mjs +26 -31
@@ -0,0 +1,140 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import {
3
+ CONTEXT_PROXY_SERVICE,
4
+ contextProxyRunning,
5
+ prepareContextProxyFiles,
6
+ proxyServiceNeeded,
7
+ } from "../../cli/context-management.mjs";
8
+ import { agentsPointedAtContextProxy } from "../../cli/coding-agent/install-context-proxy.mjs";
9
+ import { act } from "./exec.mjs";
10
+ import { composeArgs, composeSpawnOptions } from "./compose.mjs";
11
+ import { info, ok, step, warn } from "./log.mjs";
12
+ import { multiselect } from "./prompts.mjs";
13
+ import { CONTEXT_AGENT_KEYS, contextAgentsFromPmConfig, updatePmConfigContextAgents } from "./pm-config.mjs";
14
+
15
+ /**
16
+ * Wizard step "Context management (smart compaction)": pick the installed agents to trim, save
17
+ * `[context_proxy.agents]` and `[context_proxy] enabled` (syncPmConfig keeps the table when it rewrites
18
+ * config.toml), and start or stop the compose `context-proxy` service. install-coding-agent reads the
19
+ * saved values when the next step wires agents.
20
+ */
21
+
22
+ export const CONTEXT_STEP_TITLE = "Context management (smart compaction)";
23
+ /** Agents the step offers. */
24
+ const SELECTABLE = ["claude-code", "opencode1", "codex", "opencode2"];
25
+ const LABELS = {
26
+ "claude-code": "Claude Code (context-proxy service)",
27
+ opencode1: "OpenCode 1 (context-proxy service)",
28
+ codex: "Codex (context-proxy service, ChatGPT login)",
29
+ opencode2: "OpenCode 2 (native plugin, no service)",
30
+ };
31
+
32
+ /**
33
+ * Config keys for detected harnesses (`claude-code`, `codex`, `opencode`, `cursor-cli`).
34
+ * @param {string[]} harnesses
35
+ * @param {number} opencodeMajor
36
+ */
37
+ export function contextAgentKeys(harnesses, opencodeMajor) {
38
+ const keys = [];
39
+ if (harnesses.includes("claude-code")) keys.push("claude-code");
40
+ if (harnesses.includes("codex")) keys.push("codex");
41
+ if (harnesses.includes("opencode")) keys.push(opencodeMajor >= 2 ? "opencode2" : "opencode1");
42
+ return keys;
43
+ }
44
+
45
+ /**
46
+ * Defaults: a saved per-agent key, else a saved `enabled`, else OpenCode 2 on and the proxy-routed agents off.
47
+ * @param {Record<string, unknown>} cfg
48
+ * @returns {Record<string, boolean>}
49
+ */
50
+ export function contextAgentDefaults(cfg) {
51
+ return contextAgentsFromPmConfig(cfg).agents;
52
+ }
53
+
54
+ /** Whether the proxy service is needed: the step's choice, else the saved config. */
55
+ export function contextServiceWanted(ctx) {
56
+ return proxyServiceNeeded(ctx.contextAgents || contextAgentsFromPmConfig(ctx.pmMerged || {}).agents);
57
+ }
58
+
59
+ /** Context with the context-proxy profile added, for compose calls on that service. */
60
+ function withProxyProfile(ctx) {
61
+ return { ...ctx, extraComposeProfiles: [...(ctx.extraComposeProfiles || []), CONTEXT_PROXY_SERVICE] };
62
+ }
63
+
64
+ /**
65
+ * Multi-select the installed agents (defaults: contextAgentDefaults) and save the values when they differ
66
+ * from config. Agents that are not installed keep their saved value, else their default.
67
+ * @param {import("./run-wizard.mjs").WizardContext} ctx
68
+ * @param {string} number step number shown in the title
69
+ * @param {string[]} installed config keys of the installed agents (contextAgentKeys)
70
+ * @returns {Promise<Record<string, boolean>>}
71
+ */
72
+ export async function chooseContextManagement(ctx, number, installed) {
73
+ step(`${number}. ${CONTEXT_STEP_TITLE}`);
74
+ info("Stores every session message in a local SQLite store and trims long sessions; agents query the trimmed turns with smart_compact_search and smart_compact_sql.");
75
+ info("Claude Code, OpenCode 1 and Codex go through the local context-proxy compose service; OpenCode 2 uses its native plugin.");
76
+ const cfg = ctx.pmMerged || {};
77
+ const { agents: current, saved } = contextAgentsFromPmConfig(cfg);
78
+ const defaults = contextAgentDefaults(cfg);
79
+ const offered = SELECTABLE.filter((k) => installed.includes(k));
80
+ let values;
81
+ if (!offered.length) {
82
+ info("no agent that supports context management is installed; saved values kept");
83
+ values = current;
84
+ } else {
85
+ const picked = await multiselect(
86
+ ctx,
87
+ "Trim long sessions for which agents?",
88
+ offered.map((k) => ({ value: k, label: LABELS[k] })),
89
+ offered.filter((k) => defaults[k]),
90
+ );
91
+ values = Object.fromEntries(CONTEXT_AGENT_KEYS.map((k) => [k, offered.includes(k) ? picked.includes(k) : (saved.includes(k) ? current[k] : defaults[k])]));
92
+ const changed = CONTEXT_AGENT_KEYS.some((k) => !saved.includes(k) || current[k] !== values[k]);
93
+ if (changed) {
94
+ const r = updatePmConfigContextAgents(ctx.home, values, { dry: ctx.dry });
95
+ const summary = CONTEXT_AGENT_KEYS.map((k) => `${k} = ${values[k]}`).join(", ");
96
+ info(`${r.result === "dry-run" ? "[dry-run] would save" : "saved"} [context_proxy.agents] ${summary}`);
97
+ if (r.result !== "dry-run") {
98
+ const table = { ...(cfg.context_proxy || {}), enabled: r.enabled, agents: r.agents };
99
+ ctx.pmMerged = { ...cfg, context_proxy: table };
100
+ }
101
+ }
102
+ }
103
+ ctx.contextAgents = values;
104
+ ok(`context management: ${CONTEXT_AGENT_KEYS.map((k) => `${k} ${values[k] ? "on" : "off"}`).join(", ")}`);
105
+ return values;
106
+ }
107
+
108
+ /**
109
+ * Start the service when a proxy-routed agent is on (before agents are pointed at it). Writes the key file
110
+ * and store directories compose needs first.
111
+ * @param {import("./run-wizard.mjs").WizardContext} ctx
112
+ */
113
+ export async function startContextProxy(ctx) {
114
+ if (!contextServiceWanted(ctx)) return;
115
+ if (!ctx.dry) prepareContextProxyFiles(ctx.home);
116
+ const c = withProxyProfile(ctx);
117
+ const up = composeArgs(c, "up", "-d", "--no-build", CONTEXT_PROXY_SERVICE);
118
+ await act(ctx, `docker ${up.join(" ")}`, "docker", up, composeSpawnOptions(c));
119
+ if (!ctx.dry) ok("context-proxy started");
120
+ }
121
+
122
+ /**
123
+ * Stop the service when no proxy-routed agent is on and it runs, unless an agent still points at it
124
+ * (stopping would break that agent's every request).
125
+ * @param {import("./run-wizard.mjs").WizardContext} ctx
126
+ * @param {typeof spawnSync} [spawn]
127
+ */
128
+ export async function stopContextProxy(ctx, spawn = spawnSync) {
129
+ if (contextServiceWanted(ctx)) return;
130
+ const c = withProxyProfile(ctx);
131
+ if (!contextProxyRunning(composeArgs(c), composeSpawnOptions(c), spawn)) return;
132
+ const pointed = ctx.dry ? [] : agentsPointedAtContextProxy(ctx.home);
133
+ if (pointed.length) {
134
+ warn(`context-proxy left running: ${pointed.join(", ")} still point at it. Run: npx @bcmyguest/openmem install-coding-agent --yes`);
135
+ return;
136
+ }
137
+ const stop = composeArgs(c, "stop", CONTEXT_PROXY_SERVICE);
138
+ await act(ctx, `docker ${stop.join(" ")}`, "docker", stop, composeSpawnOptions(c));
139
+ if (!ctx.dry) ok("context-proxy stopped");
140
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The stack LiteLLM's secrets file (litellm.env, mode 600): LITELLM_MASTER_KEY and the
3
+ * provider keys its routes reference as os.environ/<NAME>. Shared by every provider that
4
+ * enables the `litellm` compose service (Hindsight, mem0).
5
+ */
6
+ import crypto from "node:crypto";
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+
10
+ /**
11
+ * Create litellm.env (mode 600) with a fresh LITELLM_MASTER_KEY when it does not exist.
12
+ * @param {string} litellmEnv
13
+ * @returns {boolean} true when the file was created
14
+ */
15
+ export function ensureLitellmEnv(litellmEnv) {
16
+ if (fs.existsSync(litellmEnv)) return false;
17
+ const key = crypto.randomBytes(24).toString("hex");
18
+ const body =
19
+ "# LiteLLM proxy secrets. Read by the litellm compose service. Do not commit.\n" +
20
+ `LITELLM_MASTER_KEY=sk-${key}\n` +
21
+ "# lemond ignores the key; any non-empty value works.\n" +
22
+ "LEMOND_API_KEY=placeholder\n" +
23
+ "# Hosted models: add the provider key here, e.g. ANTHROPIC_API_KEY=..., and reference it as os.environ/ANTHROPIC_API_KEY.\n";
24
+ fs.mkdirSync(path.dirname(litellmEnv), { recursive: true, mode: 0o700 });
25
+ fs.writeFileSync(litellmEnv, body, { mode: 0o600 });
26
+ return true;
27
+ }
28
+
29
+ /**
30
+ * Set NAME=value in litellm.env (mode 600), creating the file with a master key first when
31
+ * it does not exist. Never prints the value.
32
+ * @param {string} litellmEnv
33
+ * @param {string} name
34
+ * @param {string} value
35
+ * @returns {"unchanged"|"updated"|"added"}
36
+ */
37
+ export function setLitellmEnvValue(litellmEnv, name, value) {
38
+ if (!/^[A-Z][A-Z0-9_]*$/.test(name)) throw new Error(`invalid variable name: ${name}`);
39
+ if (/[\r\n]/.test(value)) throw new Error(`${name} must be one line`);
40
+ ensureLitellmEnv(litellmEnv);
41
+ const lines = fs.readFileSync(litellmEnv, "utf8").replace(/\n$/, "").split("\n");
42
+ const line = `${name}=${value}`;
43
+ const at = lines.findIndex((l) => l.startsWith(`${name}=`));
44
+ if (at >= 0 && lines[at] === line) {
45
+ fs.chmodSync(litellmEnv, 0o600);
46
+ return "unchanged";
47
+ }
48
+ if (at >= 0) lines[at] = line;
49
+ else lines.push(line);
50
+ const tmp = path.join(path.dirname(litellmEnv), `.litellm.env.${process.pid}`);
51
+ fs.writeFileSync(tmp, `${lines.join("\n")}\n`, { mode: 0o600 });
52
+ fs.chmodSync(tmp, 0o600);
53
+ fs.renameSync(tmp, litellmEnv);
54
+ return at >= 0 ? "updated" : "added";
55
+ }
@@ -1,8 +1,36 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import TOML from "@iarna/toml";
4
+ import { writeFileAtomic } from "../../cli/coding-agent/file-io.mjs";
4
5
  import { DEFAULT_PROVIDER_ID } from "./providers/registry.mjs";
5
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 = 125000;
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
+
6
34
  /** @param {string} home */
7
35
  export function userConfigPath(home) {
8
36
  return path.join(home, ".config/personal-memory/config.toml");
@@ -32,6 +60,11 @@ function readTomlFile(file) {
32
60
  }
33
61
  }
34
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
+
35
68
  /** @param {Record<string, unknown>} base @param {Record<string, unknown>} over */
36
69
  function deepMerge(base, over) {
37
70
  const out = { ...base };
@@ -116,7 +149,16 @@ export function syncPmConfig(ctx, sections = {}) {
116
149
  const dir = path.dirname(userConfigPath(ctx.home));
117
150
  fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
118
151
  const file = userConfigPath(ctx.home);
119
- fs.writeFileSync(file, buildPmConfigToml(ctx, sections), { mode: 0o600 });
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 }));
120
162
  }
121
163
 
122
164
  /**
@@ -140,12 +182,178 @@ export function updatePmConfigProvider(home, providerId, opts = {}) {
140
182
  provider: providerId,
141
183
  };
142
184
  doc.memory = memory;
143
- const header =
144
- "# Written by openmem — mise-like config.\n# Docs: docs/openmem-config.md\n";
145
- fs.writeFileSync(file, `${header}${TOML.stringify(doc)}`, { mode: 0o600 });
185
+ writeConfigFile(file, `${CONFIG_HEADER}${TOML.stringify(doc)}`);
146
186
  return "updated";
147
187
  }
148
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` (125000; 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
+
149
357
  /**
150
358
  * Apply config defaults into process.env when env/CLI did not set them. Only the
151
359
  * provider-neutral keys live here; a provider maps its own tables in its `init` hook.
@@ -28,6 +28,34 @@ export async function ask(ctx, prompt, defaultValue) {
28
28
  }
29
29
  }
30
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
+
31
59
  /**
32
60
  * @param {import("./run-wizard.mjs").WizardContext} ctx
33
61
  * @param {string} prompt
@@ -25,6 +25,8 @@ import { validateMemorySetup } from "./validate-setup.mjs";
25
25
  import { actionLabel, parseActionFromArgv } from "./wizard-action.mjs";
26
26
  import { applyPmConfigEnv, loadPmConfig, syncPmConfig } from "./pm-config.mjs";
27
27
  import { writeWizardConf } from "./wizard-conf.mjs";
28
+ import { chooseContextManagement, contextAgentKeys, contextServiceWanted, startContextProxy, stopContextProxy } from "./context-step.mjs";
29
+ import { opencodeMajorVersion } from "../../cli/coding-agent/opencode-config.mjs";
28
30
  import { setupNotebooklm as runNotebooklmSetup } from "./notebooklm.mjs";
29
31
  import { createHelpers } from "./provider-wizard/helpers.mjs";
30
32
  import {
@@ -492,8 +494,10 @@ async function smokeTest(ctx) {
492
494
  async function stackDown(ctx) {
493
495
  step("Stop the stack");
494
496
  info(`action: ${actionLabel("down")}`);
495
- const args = composeArgs(ctx, "down");
496
- await act(ctx, `docker ${args.join(" ")}`, "docker", args, composeSpawnOptions(ctx));
497
+ // A profiled service is part of `down` only when its profile is active.
498
+ const c = contextServiceWanted(ctx) ? { ...ctx, extraComposeProfiles: [...(ctx.extraComposeProfiles || []), "context-proxy"] } : ctx;
499
+ const args = composeArgs(c, "down");
500
+ await act(ctx, `docker ${args.join(" ")}`, "docker", args, composeSpawnOptions(c));
497
501
  ok(`compose project ${ctx.project} stopped`);
498
502
  }
499
503
 
@@ -555,46 +559,29 @@ function detectedCodingHarnesses() {
555
559
  }
556
560
 
557
561
  async function setupCodingAgents(ctx) {
558
- step("8. Coding-agent plugin (optional)");
562
+ step("9. Coding agents (optional)");
559
563
  const pkg = ctx.memoryProvider.codingAgent?.pluginPackage;
560
564
  const harnesses = detectedCodingHarnesses();
561
- // Link config/coding-agent.json first so the installer can adopt self-hosted settings
562
- // (and mem0/Cognee hooks can print their non-plugin guidance).
565
+ // Link config/coding-agent.json first (mem0/Cognee hooks print their own guidance here).
563
566
  await runProviderPhase(ctx, PROVIDER_PHASES.setupCodingAgent, providerHelpers());
564
- if (!pkg) {
565
- info(`no coding-agent plugin defined for ${ctx.memoryProvider.label}; step skipped`);
566
- return;
567
- }
567
+ if (!pkg) info(`no coding-agent plugin defined for ${ctx.memoryProvider.label}; OpenMem hooks cover it`);
568
568
  if (!harnesses.length) {
569
- info("no supported coding-agent CLIs on PATH (claude, codex, opencode, agent/cursor-agent): plugin skipped");
569
+ info("no supported coding-agent CLIs on PATH (claude, codex, opencode, agent/cursor-agent): step skipped");
570
570
  return;
571
571
  }
572
- const apiUrl = await resolveApiBase(ctx);
573
- // Scripted install: --server/--api-url skip the "Where should memory live?" prompt
574
- // (--server/--api-url skip the installer's memory-location prompt.)
575
- const installArgs = [
576
- ...harnesses,
577
- "--server",
578
- "self-hosted",
579
- "--api-url",
580
- apiUrl,
581
- ];
582
- info(`detected harnesses: ${harnesses.join(" ")} → self-hosted ${apiUrl}`);
583
- if (await confirm(ctx, `Install the ${ctx.memoryProvider.label} plugin for them now (npx)?`, "n")) {
584
- await act(ctx, `npx --yes ${pkg} install ${installArgs.join(" ")}`, "npx", [
585
- "--yes",
586
- pkg,
587
- "install",
588
- ...installArgs,
589
- ]);
590
- ok("restart Cursor Agent (or your coding agent) so it picks up the new plugin wiring");
572
+ // install-coding-agent honours [agents] integration (openmem hooks, or the provider's own plugin).
573
+ const later = "npx @bcmyguest/openmem install-coding-agent --yes";
574
+ info(`detected coding agents: ${harnesses.join(" ")}`);
575
+ if (await confirm(ctx, "Wire them now (unified memory MCP, openmem skill, session hooks)?", "n")) {
576
+ await act(ctx, later, process.execPath, [path.join(ctx.packageRoot, "cli", "install-coding-agent.mjs"), "--yes"]);
577
+ ok("restart your coding agents so they pick up the new wiring");
591
578
  } else {
592
- info(`skipped; later: npx --yes ${pkg} install ${installArgs.join(" ")}`);
579
+ info(`skipped; later: ${later}`);
593
580
  }
594
581
  }
595
582
 
596
583
  async function assertMemorySetup(ctx) {
597
- step("9. Validate memory setup");
584
+ step("10. Validate memory setup");
598
585
  ctx.validationErrors = [];
599
586
  ctx.validationChecks = [];
600
587
  await runProviderPhase(ctx, PROVIDER_PHASES.extendValidation, providerHelpers());
@@ -726,6 +713,8 @@ export async function runWizard(repoRoot, argv) {
726
713
  }
727
714
  await runProviderPhase(ctx, PROVIDER_PHASES.beforeStack, providerHelpers());
728
715
  await startStack(ctx);
716
+ await startContextProxy(ctx);
717
+ await stopContextProxy(ctx);
729
718
  await assertMemorySetup(ctx);
730
719
  await summary(ctx);
731
720
  console.log(`wizard: changed=${await changedCount(ctx, before)}`);
@@ -747,7 +736,13 @@ export async function runWizard(repoRoot, argv) {
747
736
  await setupNotebooklm(ctx);
748
737
  await runProviderPhase(ctx, PROVIDER_PHASES.beforeStack, providerHelpers());
749
738
  await startStack(ctx);
739
+ const harnesses = detectedCodingHarnesses();
740
+ const opencodeMajor = harnesses.includes("opencode") ? opencodeMajorVersion() : 1;
741
+ await chooseContextManagement(ctx, "8", contextAgentKeys(harnesses, opencodeMajor));
742
+ // On: start the proxy before agents point at it. Off: stop it after they are unwired.
743
+ await startContextProxy(ctx);
750
744
  await setupCodingAgents(ctx);
745
+ await stopContextProxy(ctx);
751
746
  await assertMemorySetup(ctx);
752
747
  syncPmConfig(ctx, await providerConfigSections(ctx));
753
748
  await summary(ctx);