@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
@@ -5,7 +5,7 @@ checkout. The package name lives in one place, the `name` field of `package.json
5
5
  command stays `openmem` if you switch to a scoped name such as
6
6
  `openmem`; older installs may still see `openmem`).
7
7
 
8
- Requirements: Node ≥ 20.12 (the floor of `@clack/prompts`), Docker with the compose v2
8
+ Requirements: Node ≥ 24 (`node:sqlite` with FTS5, for the session context store), Docker with the compose v2
9
9
  plugin (`docker compose version`), `curl`. Linux and macOS are supported; there is no
10
10
  bash dependency in the CLI (macOS ships bash 3.2).
11
11
 
@@ -23,11 +23,12 @@ npx @bcmyguest/openmem switch mem0 --yes
23
23
  npm i -g openmem # then: openmem setup | up | down | ...
24
24
  # From a git checkout (package not required on the registry):
25
25
  git clone https://github.com/bcmyguest/openmem && cd openmem
26
- npm install && npx . setup # same wizard; also: npm run setup / ./setup
26
+ npm install && npm run setup # same wizard; also: ./setup
27
+ node cli/openmem.mjs doctor # any other command
27
28
  ```
28
29
 
29
30
  No global `pm` command is installed: it collides with Android's `pm`. `./pm` and `npm run pm`
30
- exist only inside a checkout; prefer `npx @bcmyguest/openmem …` (or `npx . …` in a checkout).
31
+ exist only inside a checkout; prefer `npx @bcmyguest/openmem …` (or `node cli/openmem.mjs …` in a checkout).
31
32
 
32
33
  ### Commands
33
34
 
@@ -38,7 +39,8 @@ exist only inside a checkout; prefer `npx @bcmyguest/openmem …` (or `npx . …
38
39
  | `up` / `down` | start / stop the stack with the saved configuration |
39
40
  | `doctor` | validate the configuration without the smoke test |
40
41
  | `install-mcp` | merge **openmem** unified MCP (`memory_*`) by default; NotebookLM when sync is on or `--notebooklm`; native mem0/Cognee with `--provider-only`; `--no-unified-mcp` to skip |
41
- | `install-coding-agent` | `install-mcp` + session hooks for all providers; optional `--hindsight-plugin` runs `@vectorize-io/hindsight-coding-agents` on Hindsight stacks |
42
+ | `install-coding-agent` | `install-mcp` + session hooks for all providers; `--provider-plugin` runs `@vectorize-io/hindsight-coding-agents` instead of OpenMem hooks on Hindsight stacks (saved as `[agents] integration`; `--no-provider-plugin` reverts) |
43
+ | `context <on\|off\|status>` | context management (smart compaction) per agent: saves `[context_proxy.agents]` (`--agents claude-code,opencode1,codex,opencode2`, default all), re-runs `install-coding-agent`, which starts or stops the `context-proxy` service (`--dry-run`); `status` lists each agent, proxy health and pointed agents ([Context management](context-management.md)) |
42
44
  | `wiring` | report `MEMORY_PROVIDER`, API URLs, and `mcp.json` alignment (`--json`) |
43
45
  | `switch <provider>` | change `MEMORY_PROVIDER` (optional stack restart + MCP; no data migration) |
44
46
  | `providers [--json]` | list the provider manifests shipped in the package with their status |
@@ -162,7 +162,7 @@ Interactive `./setup` uses this file for **defaults** (Clack pre-selects); your
162
162
  Flags: `--yes`, `--dry-run`, `--no-stack`, `--no-mcp`. Does not migrate data between
163
163
  backends. See [README — Switch memory provider](../README.md#switch-memory-provider).
164
164
  - `feed-sync` — runs `cli/feed-sync.mjs` (RSS + OpenAlex → `feeds.out_dir` in `index.yml`).
165
- - `install-coding-agent` — unified MCP + session hooks (all providers); optional `--hindsight-plugin`; see
165
+ - `install-coding-agent` — unified MCP + session hooks (all providers); `--provider-plugin` / `--no-provider-plugin` set `[agents] integration`; see
166
166
  [coding-agent hooks](coding-agent-hooks.md).
167
167
 
168
168
  ## MCP (unified `memory_*` tools)
@@ -179,7 +179,7 @@ After changing MCP code, re-run `npx @bcmyguest/openmem install-coding-agent --y
179
179
  cp config/openmem.toml.example ~/.config/personal-memory/config.toml
180
180
  # edit provider, ports, model backend
181
181
  npx @bcmyguest/openmem configure --yes
182
- # checkout before publish: npx . configure --yes
182
+ # from a checkout: node cli/openmem.mjs configure --yes
183
183
  ```
184
184
 
185
185
  After a successful configure, the wizard **writes back** `~/.config/personal-memory/config.toml` so
@@ -7,8 +7,8 @@ Get from zero to a running stack in one wizard run. **Choose your memory provide
7
7
  | Requirement | Why |
8
8
  | --- | --- |
9
9
  | Docker + Compose v2 | Runs the stack; your user must run `docker` without sudo |
10
- | Node.js ≥ 20.12 | CLI and setup wizard |
11
- | LLM for Hindsight (default provider) | lemond, Ollama, or OpenAI-compatible — wizard wires LiteLLM |
10
+ | Node.js ≥ 24 | CLI, setup wizard, and the session context store (`node:sqlite` with FTS5) |
11
+ | LLM for Hindsight (default provider) | lemond, Ollama, OpenAI-compatible, or a Claude subscription through [Meridian](meridian-litellm.md) — wizard wires LiteLLM |
12
12
  | git, curl | Wizard preflight |
13
13
 
14
14
  Default stack is [Hindsight](hindsight.md). Other providers: wizard or `npx @bcmyguest/openmem providers` ([provider status](provider-status.md)).
@@ -27,9 +27,11 @@ npx @bcmyguest/openmem
27
27
  git clone https://github.com/bcmyguest/openmem.git
28
28
  cd openmem
29
29
  npm install
30
- npx . setup
30
+ npm run setup
31
31
  ```
32
32
 
33
+ Run other commands from the checkout with `node cli/openmem.mjs <command>`.
34
+
33
35
  The wizard asks for a memory provider, provider settings, what to index, then starts Docker and runs a smoke test.
34
36
 
35
37
  Useful variants:
@@ -41,7 +43,9 @@ npx @bcmyguest/openmem configure --yes # defaults + start, no prompts
41
43
 
42
44
  Wizard dry-run (what first setup plans — no writes):
43
45
 
44
- ![Setup dry-run: npx @bcmyguest/openmem setup --dry-run](https://bcmyguest.github.io/openmem/setup-demo.gif)
46
+ <p align="center">
47
+ <img src="https://bcmyguest.github.io/openmem/setup-demo.gif" alt="Terminal recording of the setup wizard dry run: preflight checks, memory provider, model backend, secrets, sources and stack steps, each planned write printed as a [dry-run] line" width="720" />
48
+ </p>
45
49
 
46
50
  ## 2. Confirm it is up
47
51
 
@@ -52,7 +56,7 @@ curl -fsS http://127.0.0.1:8888/health # Hindsight default; see wizard output
52
56
 
53
57
  ## 3. Wire your coding agent (optional)
54
58
 
55
- **Session memory:** `npx @bcmyguest/openmem install-coding-agent --yes` — unified MCP **plus** session hooks for Claude Code, Codex, OpenCode, Cursor CLI (any active provider). Optional `--hindsight-plugin` adds the upstream Hindsight plugin. Not the same as `install-mcp` alone.
59
+ **Session memory** (after step 1 — MCP and hooks need the running stack): `npx @bcmyguest/openmem install-coding-agent --yes` — unified MCP **plus** session hooks for Claude Code, Codex, OpenCode, Cursor CLI (any active provider). On Hindsight, `--provider-plugin` uses the upstream Hindsight plugin hooks instead (one hook set at a time). Not the same as `install-mcp` alone.
56
60
 
57
61
  **`install-mcp`** merges the **`openmem` unified MCP** (`memory_add`, `memory_search`, `memory_recall`, `memory_reflect`, `memory_consolidate`, `memory_forget` on whichever provider you picked) and, when NotebookLM sync is on, the **NotebookLM MCP**:
58
62
 
@@ -61,7 +65,7 @@ npx @bcmyguest/openmem install-mcp --yes
61
65
  npx @bcmyguest/openmem install-mcp --yes --notebooklm # force NotebookLM entry
62
66
  ```
63
67
 
64
- Session hooks use the in-repo shim by default; add `--hindsight-plugin` if you also want the upstream Hindsight plugin. Optional native mem0/Cognee MCP: `install-mcp --provider-only`. Full guide: [MCP setup](mcp-setup.md).
68
+ Session hooks use the in-repo shim by default (`[agents] integration = "openmem"`); `--provider-plugin` replaces them with the upstream Hindsight plugin and `--no-provider-plugin` switches back. Optional native mem0/Cognee MCP: `install-mcp --provider-only`. Full guide: [MCP setup](mcp-setup.md).
65
69
 
66
70
  Restart Cursor Agent after MCP changes. Move memories between providers with [Portable memory (PMM)](memory-portability.md) — `switch` alone does not migrate data.
67
71
 
@@ -79,6 +83,7 @@ npx @bcmyguest/openmem doctor
79
83
  | `up` / `down` | Start or stop the stack (down keeps data volumes) |
80
84
  | `switch <id> --yes` | Change provider (no memory migration) |
81
85
  | `doctor` | Validate config without the smoke test |
86
+ | `schedule install` | Refresh configured RSS/OpenAlex feeds hourly with the OS scheduler |
82
87
 
83
88
  In a checkout, use `npx .` instead of `npx @bcmyguest/openmem`.
84
89
 
@@ -97,5 +102,6 @@ Secrets and index config: `~/.config/hindsight/` (and optional `~/.config/person
97
102
  - [Portable memory (PMM)](memory-portability.md) — export/import
98
103
  - [Architecture](architecture.md) — data flow, indexing, MCP by provider
99
104
  - [Index sources](index-sources.md) — git, Obsidian, notes/papers/bookmarks, feed cache
105
+ - [Feed sync](sources-feeds.md) — configure RSS/OpenAlex and schedule refreshes
100
106
  - [Install with npx](npx-install.md) — upgrades, troubleshooting
101
107
  - [Configuration file](openmem-config.md) — declarative `config.toml`
@@ -5,7 +5,7 @@ The wizard is a Node program (`wizard/bin/setup.mjs`). It is exposed as:
5
5
  | How you run it | When |
6
6
  | --- | --- |
7
7
  | `npx @bcmyguest/openmem` / `npx @bcmyguest/openmem setup` | Normal install ([Install with npx](npx-install.md)) |
8
- | `npx . setup` / `npm run setup` / `./setup` | Git checkout (stack root stays in the repo) |
8
+ | `npm run setup` / `./setup` | Git checkout (stack root stays in the repo); other commands: `node cli/openmem.mjs <command>` |
9
9
 
10
10
  Interactive runs use [@clack/prompts](https://github.com/bombshell-dev/clack). `--yes` and `--dry-run` skip Clack.
11
11
 
@@ -40,8 +40,9 @@ Configure runs these steps in order (provider hooks replace or extend some steps
40
40
  | 5 | Sources to index | Writes `~/.config/hindsight/index.yml`; lists sources with a dry indexer run |
41
41
  | 6 | NotebookLM sync | Opt-in; Google login when enabled |
42
42
  | 7 | Start the stack | Build indexer image if inputs changed; health wait; smoke retain/recall (unless `--no-smoke`) |
43
- | 8 | Coding-agent plugin | Optional `npx` install for detected CLIs; provider-specific MCP hints |
44
- | 9 | Validate memory setup | Provider checks + shared validation |
43
+ | 8 | Context management (smart compaction) | Multi-select of the installed agents (Claude Code, OpenCode 1, OpenCode 2). Defaults: saved `[context_proxy.agents]` keys, else `[context_proxy] enabled`, else OpenCode 2 on and the proxy-routed agents off. Saves all four keys to `config.toml`. Claude Code or OpenCode 1 on: writes the proxy key file and store directories, then starts the `context-proxy` compose service. Neither on: stops a running `context-proxy` after step 9, unless an agent still points at it ([Context management](context-management.md)) |
44
+ | 9 | Coding agents (optional) | For detected CLIs (claude, codex, opencode, cursor-agent), offers `install-coding-agent --yes`: unified MCP, OpenMem skill, session hooks; honours `[agents] integration` (OpenMem hooks or the provider plugin) and `[context_proxy.agents]`. Default answer is no |
45
+ | 10 | Validate memory setup | Provider checks + shared validation |
45
46
 
46
47
  The last line is `wizard: changed=N` (files and containers this run touched). An unchanged re-run prints `changed=0`.
47
48
 
@@ -49,7 +50,7 @@ The last line is `wizard: changed=N` (files and containers this run touched). An
49
50
 
50
51
  | Provider | Step 3 asks for |
51
52
  | --- | --- |
52
- | Hindsight | Model backend: lemond, Ollama (host or compose profile), or custom OpenAI-compatible URL; LiteLLM routes for `memory-retain`, `memory-reflect`, `memory-consolidate` |
53
+ | Hindsight | Model backend: lemond, Ollama (host or compose profile), custom OpenAI-compatible URL, or a Claude subscription through a running Meridian ([guide](meridian-litellm.md#with-the-wizard)); LiteLLM routes for `memory-retain`, `memory-reflect`, `memory-consolidate` |
53
54
  | mem0 | Self-hosted REST base URL and API key file path |
54
55
  | Cognee | Loopback port and `LLM_API_KEY` for graph build |
55
56
 
@@ -22,31 +22,18 @@ Then ensure the stack indexer is running. Each run writes markdown files under `
22
22
 
23
23
  Network use: feed-sync calls public RSS/OpenAlex URLs from the host (not from CI by default).
24
24
 
25
- ## Cron / timer (optional)
25
+ `feed-sync` needs Python 3 with PyYAML on the host. On Windows it uses `py -3`, then `python`; on macOS and Linux it uses `python3`, then `python`. Set `PYTHON` to choose a specific executable.
26
26
 
27
- Run sync on a schedule so feeds stay fresh without manual commands:
27
+ ## Schedule feed sync (optional)
28
28
 
29
- ```ini
30
- # ~/.config/systemd/user/openmem-feed-sync.timer
31
- [Unit]
32
- Description=Refresh openmem feed cache
29
+ Install an hourly job with the current user's native scheduler. The GitHub npx source works before the next npm release includes `schedule`:
33
30
 
34
- [Timer]
35
- OnCalendar=hourly
36
- Persistent=true
37
-
38
- [Install]
39
- WantedBy=timers.target
31
+ ```sh
32
+ npx --yes github:bcmyguest/openmem schedule install --dry-run
33
+ npx --yes github:bcmyguest/openmem schedule install
34
+ npx --yes github:bcmyguest/openmem schedule remove
40
35
  ```
41
36
 
42
- ```ini
43
- # ~/.config/systemd/user/openmem-feed-sync.service
44
- [Unit]
45
- Description=openmem feed-sync
46
-
47
- [Service]
48
- Type=oneshot
49
- ExecStart=/usr/bin/env npx @bcmyguest/openmem feed-sync
50
- ```
37
+ After that release, `npx @bcmyguest/openmem schedule ...` runs the same command from npm.
51
38
 
52
- Enable with `systemctl --user enable --now openmem-feed-sync.timer`.
39
+ OpenMem installs a systemd user timer on Linux, a launchd agent on macOS, or a Task Scheduler task on Windows. The job calls the published npm package through a stable runner in your user config directory. Installation records the Python interpreter with PyYAML, so the job does not depend on your interactive shell's PATH. It runs when your user scheduler is active; the Linux timer catches up after downtime. Re-run `schedule install` if you move or upgrade Node.js or Python.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bcmyguest/openmem",
3
- "version": "0.6.3",
4
- "description": "OpenMem \u2014 memory interface for coding agents: pluggable providers (Hindsight, mem0, Cognee, \u2026), shared wizard/indexer, multi-format import/export (PMM and more).",
3
+ "version": "0.9.0",
4
+ "description": "OpenMem — memory interface for coding agents: pluggable providers (Hindsight, mem0, Cognee, …), shared wizard/indexer, multi-format import/export (PMM and more).",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
7
7
  "author": "B Guest",
@@ -40,7 +40,7 @@
40
40
  },
41
41
  "files": [
42
42
  "cli/",
43
- "skills/",
43
+ ".agents/skills/openmem/",
44
44
  "wizard/bin/",
45
45
  "wizard/lib/",
46
46
  "providers/",
@@ -49,6 +49,7 @@
49
49
  "indexer/",
50
50
  "bin/",
51
51
  "sync/",
52
+ "proxy/",
52
53
  "config/*.example",
53
54
  "config/index.yml",
54
55
  "config/notebooklm.yml",
@@ -71,7 +72,7 @@
71
72
  "!**/.pytest_cache"
72
73
  ],
73
74
  "engines": {
74
- "node": ">=20.12.0"
75
+ "node": ">=24.0.0"
75
76
  },
76
77
  "publishConfig": {
77
78
  "access": "public"
@@ -43,7 +43,7 @@ When `MEMORY_PROVIDER=cognee`:
43
43
 
44
44
  ```sh
45
45
  npx openmem install-mcp --yes --provider-only
46
- # checkout: npx . install-mcp --yes --provider-only
46
+ # checkout: node cli/openmem.mjs install-mcp --yes --provider-only
47
47
  # or during setup: coding-agent step registers the same entry
48
48
  ```
49
49
 
@@ -74,7 +74,7 @@ v1 MCP is read-only (no add/cognify). Ingest stays with the indexer or curl.
74
74
  ```sh
75
75
  npx openmem switch cognee --yes
76
76
  # later: npx openmem switch hindsight --yes
77
- # checkout: npx . switch cognee --yes
77
+ # checkout: node cli/openmem.mjs switch cognee --yes
78
78
  ```
79
79
 
80
80
  Updates `wizard.conf` (and `config.toml` `[memory].provider` when present), restarts the stack
@@ -1,10 +1,24 @@
1
1
  /**
2
- * Hindsight's questions for the Clack setup UI: model backend, models, reflect model.
2
+ * Hindsight's questions for the Clack setup UI: model backend, models, reflect model
3
+ * (or, for Meridian, its URL, key and the two Claude models).
3
4
  * The meta UI asks the provider-neutral questions (action, provider) and then calls
4
5
  * clackSetup; the returned environment is handed to the wizard so it skips its own
5
6
  * model prompts (WIZARD_SKIP_BACKEND_STEP).
6
7
  */
7
8
  import { applyConfigEnv } from "./config.mjs";
9
+ import {
10
+ bridgeReport,
11
+ containerApiBase,
12
+ dockerBridgeIp,
13
+ existingMeridianKey,
14
+ MERIDIAN_DEFAULT_BASE,
15
+ MERIDIAN_GUIDE,
16
+ MERIDIAN_REFLECT_DEFAULT,
17
+ MERIDIAN_RETAIN_DEFAULT,
18
+ normalizeMeridianBase,
19
+ pickModel,
20
+ probeMeridian,
21
+ } from "./meridian.mjs";
8
22
  import { fetchJson, normalizeOpenAiBase, ollamaModelNames, openAiModelIds } from "./probe.mjs";
9
23
  import {
10
24
  LEMOND_DEFAULT_MODEL,
@@ -51,10 +65,15 @@ export async function clackSetup(ctx, ui, h) {
51
65
  {
52
66
  value: "custom",
53
67
  label: "OpenAI-compatible URL",
54
- hint: "Meridian, hosted API, other",
68
+ hint: "hosted API, other",
69
+ },
70
+ {
71
+ value: "meridian",
72
+ label: "Claude subscription (Meridian)",
73
+ hint: "a Meridian proxy you already run on this host",
55
74
  },
56
75
  ],
57
- initialValue: ["lemond", "ollama", "custom"].includes(backendDefault)
76
+ initialValue: ["lemond", "ollama", "custom", "meridian"].includes(backendDefault)
58
77
  ? backendDefault
59
78
  : "lemond",
60
79
  });
@@ -67,7 +86,6 @@ export async function clackSetup(ctx, ui, h) {
67
86
  let apiBase = "";
68
87
  let reflectModel =
69
88
  process.env.PM_REFLECT_MODEL ||
70
- machine.PM_REFLECT_MODEL ||
71
89
  h.envValue(wizardConf, "MEMORY_REFLECT_MODEL") ||
72
90
  "";
73
91
 
@@ -85,8 +103,6 @@ export async function clackSetup(ctx, ui, h) {
85
103
  );
86
104
  }
87
105
  const defaultModel =
88
- machine.PM_DEFAULT_MODEL ||
89
- machine.MEMORY_MODEL ||
90
106
  h.envValue(wizardConf, "MEMORY_MODEL") ||
91
107
  LEMOND_DEFAULT_MODEL;
92
108
  if (ids.length > 0) {
@@ -108,7 +124,7 @@ export async function clackSetup(ctx, ui, h) {
108
124
  }
109
125
  apiBase = "http://host.docker.internal:13305/v1";
110
126
  } else if (backend === "ollama") {
111
- const ollamaBase = machine.OLLAMA_BASE || "http://127.0.0.1:11434";
127
+ const ollamaBase = "http://127.0.0.1:11434";
112
128
  const spin = ui.spinner();
113
129
  spin.start(`Checking Ollama at ${ollamaBase}`);
114
130
  const body = await fetchJson(`${ollamaBase}/api/tags`);
@@ -137,8 +153,6 @@ export async function clackSetup(ctx, ui, h) {
137
153
  }
138
154
  }
139
155
  const defaultModel =
140
- machine.PM_DEFAULT_MODEL ||
141
- machine.MEMORY_MODEL ||
142
156
  h.envValue(wizardConf, "MEMORY_MODEL") ||
143
157
  OLLAMA_DEFAULT_MODEL;
144
158
  if (names.length > 0) {
@@ -160,12 +174,15 @@ export async function clackSetup(ctx, ui, h) {
160
174
  }
161
175
  apiBase = ollamaInCompose ? OLLAMA_COMPOSE_API_BASE : OLLAMA_HOST_API_BASE;
162
176
  if (ollamaInCompose) handoff.MEMORY_OLLAMA_IN_COMPOSE = "1";
177
+ } else if (backend === "meridian") {
178
+ const m = await askMeridian(ctx, ui, h);
179
+ model = m.model;
180
+ reflectModel = m.reflectModel;
181
+ apiBase = m.apiBase;
182
+ Object.assign(handoff, m.env);
163
183
  } else {
164
- const defaultBase =
165
- machine.CUSTOM_DEFAULT_BASE || "http://host.docker.internal:3456/v1";
184
+ const defaultBase = "http://host.docker.internal:3456/v1";
166
185
  const defaultModel =
167
- machine.CUSTOM_DEFAULT_MODEL ||
168
- machine.PM_DEFAULT_MODEL ||
169
186
  h.envValue(wizardConf, "MEMORY_MODEL") ||
170
187
  LEMOND_DEFAULT_MODEL;
171
188
  const baseInput = await ui.text({
@@ -205,6 +222,24 @@ export async function clackSetup(ctx, ui, h) {
205
222
  }
206
223
  }
207
224
 
225
+ if (backend !== "meridian") reflectModel = await askReflect(ui, model, reflectModel);
226
+
227
+ ui.note("Next: Docker, secrets, indexing, and starting the stack.", "Backend configured");
228
+
229
+ handoff.WIZARD_SKIP_BACKEND_STEP = "1";
230
+ handoff.PM_BACKEND = /** @type {string} */ (backend);
231
+ handoff.PM_DEFAULT_MODEL = model;
232
+ handoff.MEMORY_API_BASE = apiBase;
233
+ handoff.PM_REFLECT_MODEL = reflectModel;
234
+ return handoff;
235
+ }
236
+
237
+ /**
238
+ * @param {typeof import("@clack/prompts")} ui
239
+ * @param {string} model retain model
240
+ * @param {string} reflectModel stored or env reflect model
241
+ */
242
+ async function askReflect(ui, model, reflectModel) {
208
243
  const reflectChoice = await ui.select({
209
244
  message: "Reflect model (needs tool calls; used for memory-reflect)",
210
245
  options: [
@@ -233,13 +268,80 @@ export async function clackSetup(ctx, ui, h) {
233
268
  } else {
234
269
  reflectModel = "";
235
270
  }
271
+ return reflectModel;
272
+ }
236
273
 
237
- ui.note("Next: Docker, secrets, indexing, and starting the stack.", "Backend configured");
274
+ /**
275
+ * Meridian URL, key and models. Detect only: Meridian must already run.
276
+ * @param {{ home: string, wizardConf: string }} ctx
277
+ * @param {typeof import("@clack/prompts")} ui
278
+ * @param {Helpers} h
279
+ */
280
+ async function askMeridian(ctx, ui, h) {
281
+ ui.log.info(`The wizard does not install or start Meridian; see ${MERIDIAN_GUIDE}.`);
282
+ const stored =
283
+ h.envValue(ctx.wizardConf, "MEMORY_BACKEND") === "meridian"
284
+ ? h.envValue(ctx.wizardConf, "MEMORY_MERIDIAN_URL")
285
+ : "";
286
+ const defaultBase = process.env.MERIDIAN_BASE || stored || MERIDIAN_DEFAULT_BASE;
287
+ const baseInput = await ui.text({
288
+ message: "Meridian URL from this host",
289
+ placeholder: defaultBase,
290
+ defaultValue: defaultBase,
291
+ });
292
+ if (ui.isCancel(baseInput)) process.exit(0);
293
+ const meridianBase = normalizeMeridianBase(/** @type {string} */ (baseInput));
294
+ const paths = {
295
+ meridianEnvFile: `${ctx.home}/.config/meridian/meridian.env`,
296
+ litellmEnv: `${ctx.home}/.config/hindsight/litellm.env`,
297
+ };
298
+ const existing = existingMeridianKey(paths, h);
299
+ const keyInput = await ui.password({
300
+ message: `MERIDIAN_API_KEY (blank uses ${existing.source || "no key"})`,
301
+ });
302
+ if (ui.isCancel(keyInput)) process.exit(0);
303
+ const typedKey = String(keyInput || "").trim();
304
+ const key = typedKey || existing.key;
238
305
 
239
- handoff.WIZARD_SKIP_BACKEND_STEP = "1";
240
- handoff.PM_BACKEND = /** @type {string} */ (backend);
241
- handoff.PM_DEFAULT_MODEL = model;
242
- handoff.MEMORY_API_BASE = apiBase;
243
- handoff.PM_REFLECT_MODEL = reflectModel;
244
- return handoff;
306
+ const bridgeIp = dockerBridgeIp();
307
+ const spin = ui.spinner();
308
+ spin.start(`Checking Meridian at ${meridianBase}`);
309
+ const probe = await probeMeridian(meridianBase, key, bridgeIp);
310
+ spin.stop(
311
+ probe.answered ? `Meridian answered (${probe.ids.length} models)` : "Meridian not reachable",
312
+ );
313
+ if (!probe.answered) {
314
+ ui.log.warn(`Start Meridian first (${MERIDIAN_GUIDE}); the route uses the guide's default models.`);
315
+ } else if (probe.modelsStatus === 401 || probe.modelsStatus === 403) {
316
+ ui.log.warn(`Meridian rejected the key on /v1/models (HTTP ${probe.modelsStatus}).`);
317
+ }
318
+ const apiBase = containerApiBase(meridianBase);
319
+ const report = bridgeReport({ ...paths, meridianBase, apiBase, dockerBridgeIp: bridgeIp }, h.envValue, probe);
320
+ if (report) (report.level === "warn" ? ui.log.warn : ui.log.info)(report.text);
321
+
322
+ const choose = async (message, preferred, family, candidates) => {
323
+ const def = pickModel(probe.ids, preferred, family, candidates);
324
+ const picked = probe.ids.length
325
+ ? await ui.select({
326
+ message,
327
+ options: probe.ids.map((id) => ({ value: id, label: id })),
328
+ initialValue: def,
329
+ })
330
+ : await ui.text({ message, placeholder: def, defaultValue: def });
331
+ if (ui.isCancel(picked)) process.exit(0);
332
+ return /** @type {string} */ (picked);
333
+ };
334
+ const sameBackend = h.envValue(ctx.wizardConf, "MEMORY_BACKEND") === "meridian";
335
+ const model = await choose("Retain / consolidate model", MERIDIAN_RETAIN_DEFAULT, /haiku/, [
336
+ process.env.PM_DEFAULT_MODEL,
337
+ sameBackend ? h.envValue(ctx.wizardConf, "MEMORY_MODEL") : "",
338
+ ]);
339
+ const reflectModel = await choose("Reflect model (needs tool calls)", MERIDIAN_REFLECT_DEFAULT, /sonnet/, [
340
+ process.env.PM_REFLECT_MODEL,
341
+ sameBackend ? h.envValue(ctx.wizardConf, "MEMORY_REFLECT_MODEL") : "",
342
+ ]);
343
+ /** @type {Record<string, string>} */
344
+ const env = { MERIDIAN_BASE: meridianBase };
345
+ if (typedKey) env.MERIDIAN_API_KEY = typedKey;
346
+ return { model, reflectModel, apiBase, env };
245
347
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Step "8. Coding-agent plugin", Hindsight half: ensure ~/.hindsight/coding-agent.json
2
+ * Step "9. Coding-agent plugin", Hindsight half: ensure ~/.hindsight/coding-agent.json
3
3
  * exists (seeded from the tracked template when missing) and check that its optInPaths
4
4
  * cover the indexed roots. Machine-local keys such as notebooklmAccess must live in a
5
5
  * real file — never in the repo template — so we do not replace a materialised config
@@ -12,6 +12,7 @@ export function defaultsFromConfig(cfg) {
12
12
  const model = /** @type {Record<string, unknown>} */ (cfg.model || {});
13
13
  const lemond = /** @type {Record<string, unknown>} */ (model.lemond || {});
14
14
  const ollama = /** @type {Record<string, unknown>} */ (model.ollama || {});
15
+ const meridian = /** @type {Record<string, unknown>} */ (model.meridian || {});
15
16
  return {
16
17
  apiPort: stack.api_port != null ? String(stack.api_port) : "",
17
18
  cpPort: stack.control_plane_port != null ? String(stack.control_plane_port) : "",
@@ -21,6 +22,7 @@ export function defaultsFromConfig(cfg) {
21
22
  modelApiBase: String(model.api_base || ""),
22
23
  lemondHost: String(lemond.host || ""),
23
24
  ollamaHost: String(ollama.host || ""),
25
+ meridianHost: String(meridian.host || ""),
24
26
  };
25
27
  }
26
28
 
@@ -43,6 +45,7 @@ export function applyConfigEnv(cfg) {
43
45
  process.env.MEMORY_API_BASE = d.modelApiBase;
44
46
  }
45
47
  if (d.lemondHost && !process.env.LEMOND_BASE) process.env.LEMOND_BASE = d.lemondHost;
48
+ if (d.meridianHost && !process.env.MERIDIAN_BASE) process.env.MERIDIAN_BASE = d.meridianHost;
46
49
  }
47
50
 
48
51
  /**
@@ -63,6 +66,9 @@ export function configToml(ctx) {
63
66
  if (ctx.backend === "ollama" || ctx.ollamaBase) {
64
67
  model.ollama = { host: ctx.ollamaBase };
65
68
  }
69
+ if (ctx.backend === "meridian" && ctx.meridianBase) {
70
+ model.meridian = { host: ctx.meridianBase };
71
+ }
66
72
  return {
67
73
  stack: {
68
74
  api_port: Number(ctx.portApi || 8888),
@@ -30,7 +30,8 @@ export function llmWired(ctx, h) {
30
30
  export function stackEnv(ctx) {
31
31
  ensureState(ctx);
32
32
  let backend = ctx.backend;
33
- if (backend === "custom") backend = "lemond";
33
+ // Own-route backends run from litellm.yaml; setup.mjs only knows lemond and ollama.
34
+ if (backend === "custom" || backend === "meridian") backend = "lemond";
34
35
  /** @type {Record<string, string>} */
35
36
  const env = { MEMORY_BACKEND: backend };
36
37
  if (ctx.ollamaInCompose) env.MEMORY_OLLAMA_IN_COMPOSE = "1";
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { configToml } from "./config.mjs";
4
+ import { renderMeridianRoutes } from "./meridian.mjs";
4
5
  import { OLLAMA_HOST_API_BASE } from "./state.mjs";
5
6
 
6
7
  /** @typedef {import("../../../wizard/lib/provider-wizard/helpers.mjs").ProviderWizardHelpers} Helpers */
@@ -80,6 +81,7 @@ function renderShipped(ctx, litellmModel, reflectLitellmModel) {
80
81
  * @returns {string}
81
82
  */
82
83
  export function litellmConfig(ctx) {
84
+ if (ctx.backend === "meridian") return renderMeridianRoutes(ctx);
83
85
  let prefix = "openai/";
84
86
  if (ctx.backend === "ollama") prefix = "ollama_chat/";
85
87
  if (ctx.backend === "custom") return renderOverride(ctx, `${prefix}${ctx.model}`);
@@ -112,6 +114,13 @@ export async function removeOverrideIfAsked(ctx, h) {
112
114
  * @param {Helpers} h
113
115
  */
114
116
  export async function writeOverride(ctx, h) {
117
+ if (ctx.dry && ctx.backend === "meridian") {
118
+ const backup = fs.existsSync(ctx.litellmOverride) ? " (backs up the existing file)" : "";
119
+ h.info(
120
+ `[dry-run] write ${ctx.litellmOverride}${backup}: Meridian routes, retain and consolidate -> anthropic/${ctx.model}, reflect -> anthropic/${ctx.reflectModel || ctx.model}, at ${ctx.apiBase}`,
121
+ );
122
+ return;
123
+ }
115
124
  if (ctx.dry) {
116
125
  h.info(
117
126
  `[dry-run] write ${ctx.litellmOverride} from config/litellm.${ctx.backend}.yaml: retain and consolidate -> ${ctx.model}, reflect -> ${ctx.reflectModel || "shipped"}, at ${ctx.apiBase}`,
@@ -150,7 +159,8 @@ export async function applyRoute(ctx, h) {
150
159
  if (ctx.backend === "ollama") shipped = ctx.ollamaDefaultModel;
151
160
  const defaultBase = shippedApiBase(ctx.backend);
152
161
  const apiMatchesShipped = !ctx.apiBase || ctx.apiBase === defaultBase;
153
- if (ctx.backend !== "custom" && ctx.model === shipped && !ctx.reflectModel && apiMatchesShipped) {
162
+ const ownRoute = ctx.backend === "custom" || ctx.backend === "meridian";
163
+ if (!ownRoute && ctx.model === shipped && !ctx.reflectModel && apiMatchesShipped) {
154
164
  await removeOverrideIfAsked(ctx, h);
155
165
  } else {
156
166
  await writeOverride(ctx, h);
@@ -173,15 +183,14 @@ export function saveConf(ctx, h) {
173
183
  h.info(`[dry-run] record backend ${ctx.backend} in ${ctx.wizardConf}`);
174
184
  return;
175
185
  }
176
- h.saveWizardConf(
177
- ctx,
178
- {
179
- MEMORY_BACKEND: ctx.backend,
180
- MEMORY_MODEL: ctx.model,
181
- MEMORY_API_BASE: ctx.apiBase,
182
- MEMORY_REFLECT_MODEL: ctx.reflectModel,
183
- MEMORY_OLLAMA_IN_COMPOSE: ctx.ollamaInCompose ? "1" : "0",
184
- },
185
- { pmSections: configToml(ctx) },
186
- );
186
+ /** @type {Record<string, string>} */
187
+ const keys = {
188
+ MEMORY_BACKEND: ctx.backend,
189
+ MEMORY_MODEL: ctx.model,
190
+ MEMORY_API_BASE: ctx.apiBase,
191
+ MEMORY_REFLECT_MODEL: ctx.reflectModel,
192
+ MEMORY_OLLAMA_IN_COMPOSE: ctx.ollamaInCompose ? "1" : "0",
193
+ };
194
+ if (ctx.backend === "meridian") keys.MEMORY_MERIDIAN_URL = ctx.meridianBase;
195
+ h.saveWizardConf(ctx, keys, { pmSections: configToml(ctx) });
187
196
  }