@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.
- package/{skills → .agents/skills}/openmem/SKILL.md +5 -0
- package/README.md +81 -76
- package/cli/coding-agent/capabilities.mjs +14 -3
- package/cli/coding-agent/context-store.mjs +81 -0
- package/cli/coding-agent/context-trim.mjs +899 -0
- package/cli/coding-agent/file-io.mjs +72 -0
- package/cli/coding-agent/hook-harnesses.mjs +2 -1
- package/cli/coding-agent/hook-runner.mjs +28 -4
- package/cli/coding-agent/install-access.mjs +43 -17
- package/cli/coding-agent/install-context-proxy.mjs +333 -0
- package/cli/coding-agent/install-rtk.mjs +60 -0
- package/cli/coding-agent/install-shim.mjs +119 -15
- package/cli/coding-agent/opencode-config.mjs +21 -5
- package/cli/coding-agent/opencode-plugin.mjs +39 -3
- package/cli/coding-agent/opencode2-plugin/index.js +299 -0
- package/cli/coding-agent/recall-frame.mjs +29 -0
- package/cli/coding-agent/remove-upstream-access.mjs +122 -0
- package/cli/coding-agent/runtime.mjs +2 -1
- package/cli/coding-agent/session-db.mjs +626 -0
- package/cli/coding-agent/session-facade.mjs +30 -6
- package/cli/coding-agent/toml-edit.mjs +161 -0
- package/cli/context-management.mjs +254 -0
- package/cli/feed-sync.mjs +18 -2
- package/cli/install-coding-agent.mjs +173 -40
- package/cli/mcp/context-sql-runner.mjs +58 -0
- package/cli/mcp/context-store.mjs +670 -0
- package/cli/mcp/knowledge.mjs +333 -0
- package/cli/mcp/memory-facade.mjs +99 -33
- package/cli/openmem-mcp.mjs +3 -0
- package/cli/openmem.mjs +18 -1
- package/cli/schedule.mjs +176 -0
- package/cli/stack.mjs +2 -0
- package/cli/switch-memory-provider.mjs +1 -1
- package/compose.yaml +67 -4
- package/config/hindsight.env.example +2 -0
- package/config/openmem.toml.example +64 -2
- package/docs/architecture.md +35 -9
- package/docs/coding-agent-hooks.md +26 -2
- package/docs/coding-agents.md +27 -7
- package/docs/compatibility.md +80 -0
- package/docs/concepts.md +30 -7
- package/docs/context-management.md +230 -0
- package/docs/context-proxy.md +115 -0
- package/docs/context-research.md +226 -0
- package/docs/context-store.md +277 -0
- package/docs/evidence-plan.md +216 -0
- package/docs/hindsight.md +5 -2
- package/docs/mcp-setup.md +5 -2
- package/docs/mem0.md +39 -1
- package/docs/memory-providers.md +2 -2
- package/docs/meridian-litellm.md +198 -0
- package/docs/model-configuration.md +22 -0
- package/docs/npx-install.md +6 -4
- package/docs/openmem-config.md +2 -2
- package/docs/quickstart.md +12 -6
- package/docs/setup-wizard.md +5 -4
- package/docs/sources-feeds.md +9 -22
- package/package.json +5 -4
- package/providers/cognee/OPERATOR.md +2 -2
- package/providers/hindsight/lib/clack.mjs +122 -20
- package/providers/hindsight/lib/coding-agent.mjs +1 -1
- package/providers/hindsight/lib/config.mjs +6 -0
- package/providers/hindsight/lib/env-setup.mjs +2 -1
- package/providers/hindsight/lib/litellm-routes.mjs +21 -12
- package/providers/hindsight/lib/meridian.mjs +307 -0
- package/providers/hindsight/lib/model-backend.mjs +38 -9
- package/providers/hindsight/lib/probe.mjs +21 -0
- package/providers/hindsight/lib/stack-env.mjs +5 -12
- package/providers/hindsight/lib/state.mjs +6 -1
- package/providers/hindsight/lib/summary.mjs +16 -2
- package/providers/hindsight/lib/validate.mjs +6 -0
- package/providers/hindsight/wizard.mjs +3 -2
- package/providers/mem0/OPERATOR.md +21 -2
- package/providers/mem0/compose.yaml +24 -2
- package/providers/mem0/lib/coding-agent.mjs +1 -1
- package/providers/mem0/lib/configure.mjs +66 -0
- package/providers/mem0/lib/litellm.mjs +457 -0
- package/providers/mem0/lib/stack-env.mjs +40 -1
- package/providers/mem0/lib/summary.mjs +30 -2
- package/providers/mem0/manifest.json +1 -1
- package/providers/mem0/wizard.mjs +15 -2
- package/proxy/config.yaml +33 -0
- package/proxy/openmem_context/__init__.py +6 -0
- package/proxy/openmem_context/adapters.py +504 -0
- package/proxy/openmem_context/compress.py +53 -0
- package/proxy/openmem_context/core.py +608 -0
- package/proxy/openmem_context/embed.py +212 -0
- package/proxy/openmem_context/recall.py +49 -0
- package/proxy/openmem_context/session.py +70 -0
- package/proxy/openmem_context/sessiondb.py +276 -0
- package/proxy/openmem_context/settings.py +134 -0
- package/proxy/openmem_context/store.py +129 -0
- package/proxy/openmem_context/tokens.py +123 -0
- package/proxy/openmem_context/ttl.py +78 -0
- package/proxy/openmem_context_hook.py +34 -0
- package/scripts/check-pack.mjs +2 -1
- package/scripts/run-setup.sh +14 -5
- package/scripts/setup.mjs +14 -1
- package/server.json +2 -2
- package/wizard/lib/context-step.mjs +140 -0
- package/wizard/lib/litellm-env.mjs +55 -0
- package/wizard/lib/pm-config.mjs +212 -4
- package/wizard/lib/prompts.mjs +28 -0
- 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
|
+
}
|
package/wizard/lib/pm-config.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
package/wizard/lib/prompts.mjs
CHANGED
|
@@ -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
|
-
|
|
496
|
-
|
|
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("
|
|
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
|
|
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):
|
|
569
|
+
info("no supported coding-agent CLIs on PATH (claude, codex, opencode, agent/cursor-agent): step skipped");
|
|
570
570
|
return;
|
|
571
571
|
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
"
|
|
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:
|
|
579
|
+
info(`skipped; later: ${later}`);
|
|
593
580
|
}
|
|
594
581
|
}
|
|
595
582
|
|
|
596
583
|
async function assertMemorySetup(ctx) {
|
|
597
|
-
step("
|
|
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);
|