memoryrouter-codex 1.6.1 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -32
- package/bin/cli.cjs +33 -6
- package/lib/hooks.cjs +47 -8
- package/lib/installer.cjs +80 -19
- package/lib/managed.cjs +28 -4
- package/package.json +2 -2
- package/runtime/api.cjs +5 -1
- package/runtime/config.cjs +38 -3
- package/runtime/constants.cjs +1 -1
- package/runtime/hook.cjs +62 -47
- package/runtime/mcp-stdio.cjs +12 -13
- package/runtime/memory.cjs +108 -41
- package/runtime/project.cjs +171 -23
- package/runtime/vault-binding.cjs +51 -0
package/README.md
CHANGED
|
@@ -2,29 +2,39 @@
|
|
|
2
2
|
|
|
3
3
|
Native Codex integration for durable MemoryRouter memory. It uses Codex's current MCP, lifecycle hooks, and `AGENTS.md` surfaces rather than pretending that model-selected MCP calls alone are automatic memory.
|
|
4
4
|
|
|
5
|
-
> **Release status:** `memoryrouter-codex
|
|
5
|
+
> **Release status:** `memoryrouter-codex` is the published npm release. Its automatic capture is message-only: one user request plus the final assistant response, with no tool payloads.
|
|
6
6
|
|
|
7
7
|
## What this integration does
|
|
8
8
|
|
|
9
|
+
Every turn, automatically: **recall before the prompt, save after the turn.** What is saved is exactly two things per turn, the user's message and the assistant's final response. Tool calls, tool output, reasoning, plans, intermediate assistant messages and subagent output are never saved.
|
|
10
|
+
|
|
9
11
|
| Layer | Behavior |
|
|
10
12
|
| --- | --- |
|
|
11
|
-
| `UserPromptSubmit` hook | Searches MemoryRouter
|
|
12
|
-
| `Stop` hook |
|
|
13
|
-
| `
|
|
14
|
-
| `
|
|
15
|
-
|
|
|
16
|
-
|
|
|
17
|
-
| Local MCP fallback | API-key-authenticated stdio tools with strict project filtering, handles, local suppression, status, and full-vault clear confirmation. |
|
|
13
|
+
| `UserPromptSubmit` hook | Searches your whole MemoryRouter vault with the prompt and injects what it finds as developer context for that turn. The vault is shared with Claude Code and every other MemoryRouter surface. Records the prompt for this turn. |
|
|
14
|
+
| `Stop` hook | Saves the user's message and the final assistant response for the turn. Nothing else. |
|
|
15
|
+
| `SessionStart` hook | Restores context after Codex compacts a long chat (key mode also on new/resumed chats). |
|
|
16
|
+
| `SessionEnd` (key mode) | Only if the last turn never finished (interrupted), saves that turn's user message. A completed turn is never saved twice. |
|
|
17
|
+
| Remote MCP | OAuth-based explicit search/store through `https://mcp.memoryrouter.ai/mcp` (`search_memories`, `store_memory`, ...). |
|
|
18
|
+
| Local MCP fallback | API-key-authenticated stdio tools over the same whole vault. |
|
|
18
19
|
| `AGENTS.md` | Tells Codex when to recall/store and how MemoryRouter coexists with native Memories. |
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
**Two ways the hooks reach MemoryRouter (1.8.0):**
|
|
22
|
+
|
|
23
|
+
- **Over your MemoryRouter sign-in (no API key).** Default when you signed in with OAuth (Codex app Settings > Plugins > Authenticate, or `codex mcp login memoryrouter`) and no key is configured. The hooks are Codex `mcp_tool` hooks that call hidden lifecycle tools on the already-connected `memoryrouter` server (`codex_turn_start`, `codex_turn_end`, `codex_session_start`) with your OAuth token and the vault you picked when you signed in. Nothing secret is written to disk.
|
|
24
|
+
- **Local runtime with a Memory Key.** Used when a key is configured (`MEMORYROUTER_API_KEY`, `--api-key-stdin`, or `memoryrouter-claude`'s `~/.memoryrouter/config.json`). Same per-turn behavior.
|
|
25
|
+
|
|
26
|
+
Force either with `--hooks-via mcp` or `--hooks-via command`.
|
|
27
|
+
|
|
28
|
+
1.8.0 removed the `SubagentStop` and `PreCompact` capture hooks: subagent output is intermediate work, and the PreCompact checkpoint re-saved a prompt the `Stop` hook already captured.
|
|
29
|
+
|
|
30
|
+
MemoryRouter is the **cross-machine, cross-tool, team-shareable, and auditable** layer: one memory across every session, repo and tool, shared by Codex, Claude Code, ChatGPT, and Claude. Codex's native Memories remain useful as an automated local personal recall layer.
|
|
21
31
|
|
|
22
32
|
## Requirements and supported surfaces
|
|
23
33
|
|
|
24
34
|
- Node.js 18 or newer.
|
|
25
35
|
- **Codex 0.145.0 or newer** for the complete lifecycle-hook integration. Version 0.146.0 was tested directly.
|
|
26
36
|
- Local Codex CLI, ChatGPT desktop app's local Codex host, and Codex IDE extension share `config.toml` MCP configuration.
|
|
27
|
-
-
|
|
37
|
+
- Hooks must be trusted once: Codex app **Settings > Hooks**, or `/hooks` in the CLI. OAuth (`mcp_tool`) hooks need a Codex build with MCP tool hooks (the current app and CLI 0.166 have them).
|
|
28
38
|
- Project `.codex` config is loaded only for trusted projects.
|
|
29
39
|
- ChatGPT web does **not** read a laptop's `~/.codex/config.toml` or local hooks. Use MemoryRouter's hosted plugin/connector surface there.
|
|
30
40
|
- Codex cloud environments do not automatically inherit host-local user config or secrets. A project-scope install can travel with a repository, but the environment still needs Node, project trust, package/network availability for the stdio fallback, and `MEMORYROUTER_API_KEY` for hooks.
|
|
@@ -33,19 +43,40 @@ There is no dependency on the obsolete `experimental_use_rmcp_client` setting in
|
|
|
33
43
|
|
|
34
44
|
## Install
|
|
35
45
|
|
|
36
|
-
###
|
|
46
|
+
### Codex app (Mac): two steps, no key
|
|
47
|
+
|
|
48
|
+
1. Add the server in the app: **Settings > Plugins > Add > Add MCP server**, Type **Streamable HTTP**, Name `memoryrouter`, URL `https://mcp.memoryrouter.ai/mcp`, **Save**, then **Authenticate** and sign in (or create an account) in the browser tab that opens.
|
|
49
|
+
2. In a Codex chat, paste:
|
|
50
|
+
|
|
51
|
+
> Run `npx -y memoryrouter-codex@latest init` with network access, then tell me to trust the MemoryRouter hooks in Settings > Hooks.
|
|
52
|
+
|
|
53
|
+
Then trust the three MemoryRouter hooks in **Settings > Hooks** and open a **New chat**.
|
|
54
|
+
|
|
55
|
+
`init` keeps the server the app added (and its sign-in), adds the three `mcp_tool` hooks to `~/.codex/hooks.json` and the MemoryRouter guidance to `~/.codex/AGENTS.md`. The Codex CLI and IDE extension on the same Mac share all of it.
|
|
56
|
+
|
|
57
|
+
### Codex CLI, OAuth (no key)
|
|
37
58
|
|
|
38
|
-
|
|
59
|
+
```bash
|
|
60
|
+
npx -y memoryrouter-codex init --scope user
|
|
61
|
+
codex mcp login memoryrouter
|
|
62
|
+
npx -y memoryrouter-codex doctor --scope user
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Then start Codex, trust the MemoryRouter hooks in `/hooks`, and check `/mcp verbose`.
|
|
66
|
+
|
|
67
|
+
### With a Memory Key (local hook runtime)
|
|
68
|
+
|
|
69
|
+
Pass the key on stdin so it does not appear in shell history:
|
|
39
70
|
|
|
40
71
|
```bash
|
|
41
72
|
printf '%s' "$MEMORYROUTER_API_KEY" | \
|
|
42
|
-
npx -y memoryrouter-codex
|
|
73
|
+
npx -y memoryrouter-codex init --scope user --auth oauth --api-key-stdin
|
|
43
74
|
|
|
44
75
|
codex mcp login memoryrouter
|
|
45
76
|
memoryrouter-codex doctor --scope user
|
|
46
77
|
```
|
|
47
78
|
|
|
48
|
-
Then start Codex and inspect:
|
|
79
|
+
Then start Codex and inspect (trust the MemoryRouter hook definitions in `/hooks`):
|
|
49
80
|
|
|
50
81
|
```text
|
|
51
82
|
/mcp verbose
|
|
@@ -54,14 +85,16 @@ Then start Codex and inspect:
|
|
|
54
85
|
|
|
55
86
|
The API key is written to `~/.memoryrouter/codex/config.json` with mode `0600`; the directory is `0700`. It is never written to `config.toml`, `hooks.json`, `AGENTS.md`, or a repository.
|
|
56
87
|
|
|
57
|
-
|
|
88
|
+
Already using MemoryRouter in Claude Code? When no Codex key is configured, the hooks reuse the `memoryrouter-claude` key in `~/.memoryrouter/config.json`, so both agents use the same vault (one memory for every session, repo and tool) with no second paste. Key precedence: `MEMORYROUTER_API_KEY`, then the Codex private config, then the Claude Code config.
|
|
89
|
+
|
|
90
|
+
If you prefer not to persist the key, omit `--api-key-stdin` and ensure `MEMORYROUTER_API_KEY` is in the environment of the Codex host. Without any key, `init` installs the OAuth (`mcp_tool`) hooks instead, which work as soon as you are signed in.
|
|
58
91
|
|
|
59
92
|
### Project scope
|
|
60
93
|
|
|
61
94
|
```bash
|
|
62
95
|
cd /path/to/repository
|
|
63
96
|
printf '%s' "$MEMORYROUTER_API_KEY" | \
|
|
64
|
-
npx -y memoryrouter-codex
|
|
97
|
+
npx -y memoryrouter-codex init --scope project --auth oauth --api-key-stdin
|
|
65
98
|
codex mcp login memoryrouter
|
|
66
99
|
```
|
|
67
100
|
|
|
@@ -71,7 +104,7 @@ This writes managed blocks/files under the repository's `AGENTS.md` and `.codex/
|
|
|
71
104
|
|
|
72
105
|
```bash
|
|
73
106
|
printf '%s' "$MEMORYROUTER_API_KEY" | \
|
|
74
|
-
npx -y memoryrouter-codex
|
|
107
|
+
npx -y memoryrouter-codex init --scope user --auth api-key --api-key-stdin
|
|
75
108
|
```
|
|
76
109
|
|
|
77
110
|
This uses a local stdio MCP server and the MemoryRouter API. It does not put the memory key in Codex config. The fallback tools are:
|
|
@@ -110,25 +143,27 @@ The installer preserves unrelated TOML byte-for-byte outside its managed block.
|
|
|
110
143
|
|
|
111
144
|
Hooks are merged into `hooks.json`; foreign hooks remain. Re-running `init` upgrades/replaces only MemoryRouter-owned handlers. `uninstall` removes only owned handlers and managed blocks.
|
|
112
145
|
|
|
113
|
-
##
|
|
146
|
+
## One vault per user (shared with Claude Code and every MemoryRouter surface)
|
|
147
|
+
|
|
148
|
+
Since **1.7.0**, every hook call (recall before each prompt, capture after each turn, compaction checkpoints, post-compaction recall) and every local MCP tool call reads and writes your **whole vault**. No `session_id` is sent, so there is no per-session or per-repository partition. That gives you:
|
|
114
149
|
|
|
115
|
-
|
|
150
|
+
- **A new Codex session recalls earlier sessions**, in this repository and every other one.
|
|
151
|
+
- **Codex and Claude Code share one memory.** A decision captured in Claude Code in repo A is recalled by Codex in repo B, and the other way round (requires `memoryrouter-claude` 2.7.0 or newer on the same vault).
|
|
152
|
+
- **Everything else on the vault is recalled too:** saves from the hosted MCP (`store_memory` from ChatGPT, Claude, or `codex mcp login`), consolidated reflections, the server's pending buffer, and Codex envelopes from any release. Only handles you suppressed locally with `memory_forget` are hidden.
|
|
153
|
+
|
|
154
|
+
**Repository tag.** Codex still computes the same `ccp_` id as Claude Code:
|
|
116
155
|
|
|
117
156
|
```text
|
|
118
|
-
|
|
157
|
+
ccp_<first 24 hex chars of SHA-256(canonical project identity)>
|
|
119
158
|
```
|
|
120
159
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
1. Canonical `origin` git URL, with credentials removed and `.git` normalized.
|
|
124
|
-
2. Git common directory (so local worktrees share a project).
|
|
125
|
-
3. Absolute path fallback.
|
|
160
|
+
from, in order, an explicit id (`MEMORYROUTER_PROJECT_ID` or `projectId` in `.memoryrouter.json`), the normalized git remote, the git common directory, then the directory path. It is metadata only: Codex stamps it on the turns it saves and labels recalled entries from the current repository as `this repo`. It never limits what is recalled. Only the opaque hash leaves the machine.
|
|
126
161
|
|
|
127
|
-
|
|
162
|
+
**One user, one vault.** When you also connect MemoryRouter over OAuth (`codex mcp login memoryrouter`, ChatGPT, Claude), pick the same vault your hook key belongs to. `init` asks `https://auth.memoryrouter.ai/v1/vault-binding` whether the hook key is the vault your OAuth connections selected: on a mismatch it stops before writing anything and names the selected vault; with no connection yet it tells you which vault to pick at login. `doctor` reports the same `one-vault` check. The response never contains a key.
|
|
128
163
|
|
|
129
|
-
|
|
164
|
+
`.memoryrouterignore` (or `enabled: false` in `.memoryrouter.json`) still disables hooks for a repository. The pre-1.7 settings `retrievalScope`, `includeGlobal`, and `memoryScope` are ignored and removed from the private config.
|
|
130
165
|
|
|
131
|
-
|
|
166
|
+
**Upgrading from 1.6.x.** Releases before 1.7.0 used a per-Codex-session partition (`mrcodex:<project>:<session>`), so every new session started empty and Codex never saw Claude Code's memory. When 1.7.0 shipped, those partitions (and `memoryrouter-claude`'s per-repository `ccp_` partitions) were moved into each vault's main memory server-side, with stored embeddings kept, exact repeats skipped, and unflushed buffers saved, so old memories are recalled again with nothing lost or duplicated. Re-run `init` to upgrade the installed runtime.
|
|
132
167
|
|
|
133
168
|
## Historical import (agent-driven)
|
|
134
169
|
|
|
@@ -222,15 +257,15 @@ Doctor checks Codex version, managed config, runtime version, hook count, `AGENT
|
|
|
222
257
|
Upgrade idempotently by re-running the pinned/new package:
|
|
223
258
|
|
|
224
259
|
```bash
|
|
225
|
-
npx -y memoryrouter-codex
|
|
260
|
+
npx -y memoryrouter-codex init --scope user
|
|
226
261
|
```
|
|
227
262
|
|
|
228
263
|
Uninstall:
|
|
229
264
|
|
|
230
265
|
```bash
|
|
231
|
-
npx -y memoryrouter-codex
|
|
266
|
+
npx -y memoryrouter-codex uninstall --scope user
|
|
232
267
|
# or
|
|
233
|
-
npx -y memoryrouter-codex
|
|
268
|
+
npx -y memoryrouter-codex uninstall --scope project --project-dir /repo
|
|
234
269
|
```
|
|
235
270
|
|
|
236
271
|
Uninstall deliberately retains `~/.memoryrouter/codex/` because another project installation may use the credentials and local suppression state. Delete that private directory manually only when no integration uses it.
|
|
@@ -242,7 +277,7 @@ Uninstall deliberately retains `~/.memoryrouter/codex/` because another project
|
|
|
242
277
|
- Tool-call records are intentionally excluded. MemoryRouter does not store tool names, tool IDs, tool inputs, tool results, commands, patches, or other `PostToolUse` payloads from automatic Codex capture. Important outcomes must appear in the final assistant response to be retained.
|
|
243
278
|
- Some specialized tool paths can opt out of hooks. Codex documents hooks as a useful extension surface, not a complete enforcement boundary.
|
|
244
279
|
- SessionEnd can happen after a 30-minute idle period and has a three-second maximum. It is a best-effort checkpoint; normal `Stop` capture is the primary write path.
|
|
245
|
-
-
|
|
280
|
+
- Everything shares one vault per user. Strict isolation between two bodies of work requires a separate account and vault.
|
|
246
281
|
- Per-memory server deletion is not available through the current API. Local suppression and explicit full-vault clear are accurately labeled.
|
|
247
282
|
- API-level idempotency is not yet available; timeout-after-acceptance duplicates remain possible.
|
|
248
283
|
|
package/bin/cli.cjs
CHANGED
|
@@ -23,7 +23,7 @@ function parseArgs(argv) {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
function usage() {
|
|
26
|
-
return `memoryrouter-codex ${INTEGRATION_VERSION}\n\nUsage:\n memoryrouter-codex init [--scope user|project] [--auth oauth|api-key]\n [--project-dir PATH] [--api-key-stdin]\n [--no-hooks] [--no-agents]\n memoryrouter-codex doctor [--scope user|project] [--project-dir PATH] [--offline]\n memoryrouter-codex uninstall [--scope user|project] [--project-dir PATH]\n memoryrouter-codex reflect [--tier 1|2] [--batches N|all] [--max-tokens N] [--dry-run]\n memoryrouter-codex import [--instructions [--run]] [--file PATH] [--dismiss] [--yes]\n memoryrouter-codex mcp-server\n\nImport:\n Import historical Codex conversations into your vault. --instructions\n fetches the server's extraction prompt for the host agent to execute\n (--run spawns it via codex exec); --file uploads a JSONL extract with a\n preview, price quote, explicit approval (--yes for non-interactive),\n idempotent batches, and resume; --dismiss silences the import nudge\n permanently. Re-running --file is always safe: committed batches replay\n for free and duplicates are never re-billed.\n\nReflect:\n Consolidates unconsolidated memories into reflections using Codex itself as\n the model (codex exec, you pay your own inference). Server-authored\n instructions; resumable; no local state. --dry-run prints the checkout and\n lets the lease expire without committing.\n Automatic mode: when the server reports consolidation debt above its\n threshold, the hooks spawn this same loop in the background (max 3 batches\n per trigger, 30 minute cooldown, machine-wide lockfile at\n ~/.memoryrouter/consolidate.lock). Opt out with "autoConsolidate": false in\n ~/.memoryrouter/codex/config.json. Logs: ~/.memoryrouter/consolidate.log\n\nAuth modes:\n oauth Current remote MCP at ${'https://mcp.memoryrouter.ai/mcp'} (default).\n
|
|
26
|
+
return `memoryrouter-codex ${INTEGRATION_VERSION}\n\nUsage:\n memoryrouter-codex init [--scope user|project] [--auth oauth|api-key]\n [--project-dir PATH] [--api-key-stdin]\n [--hooks-via mcp|command] [--no-hooks] [--no-agents]\n memoryrouter-codex doctor [--scope user|project] [--project-dir PATH] [--offline]\n memoryrouter-codex uninstall [--scope user|project] [--project-dir PATH]\n memoryrouter-codex reflect [--tier 1|2] [--batches N|all] [--max-tokens N] [--dry-run]\n memoryrouter-codex import [--instructions [--run]] [--file PATH] [--dismiss] [--yes]\n memoryrouter-codex mcp-server\n\nImport:\n Import historical Codex conversations into your vault. --instructions\n fetches the server's extraction prompt for the host agent to execute\n (--run spawns it via codex exec); --file uploads a JSONL extract with a\n preview, price quote, explicit approval (--yes for non-interactive),\n idempotent batches, and resume; --dismiss silences the import nudge\n permanently. Re-running --file is always safe: committed batches replay\n for free and duplicates are never re-billed.\n\nReflect:\n Consolidates unconsolidated memories into reflections using Codex itself as\n the model (codex exec, you pay your own inference). Server-authored\n instructions; resumable; no local state. --dry-run prints the checkout and\n lets the lease expire without committing.\n Automatic mode: when the server reports consolidation debt above its\n threshold, the hooks spawn this same loop in the background (max 3 batches\n per trigger, 30 minute cooldown, machine-wide lockfile at\n ~/.memoryrouter/consolidate.lock). Opt out with "autoConsolidate": false in\n ~/.memoryrouter/codex/config.json. Logs: ~/.memoryrouter/consolidate.log\n\nAuth modes:\n oauth Current remote MCP at ${'https://mcp.memoryrouter.ai/mcp'} (default).\n Sign in once (Codex app: Settings > Plugins > Authenticate; CLI:\n \`codex mcp login memoryrouter\`). With no API key, the automatic\n hooks call MemoryRouter over that same sign-in (Codex mcp_tool\n hooks): recall before every prompt, save the prompt and the final\n response after every turn. No key on disk. If a key is configured\n (MEMORYROUTER_API_KEY, --api-key-stdin, or memoryrouter-claude's\n ~/.memoryrouter/config.json) the local hook runtime is used\n instead; force either with --hooks-via mcp|command.\n A server you already added in the Codex app is kept as is.\n api-key Local stdio MCP fallback plus automatic hooks. The key is read only\n from the environment/private config, never from project files.\n\nSecure key setup:\n printf '%s' "$MEMORYROUTER_API_KEY" | memoryrouter-codex init --api-key-stdin\n`;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
function optionsFromArgs(args) {
|
|
@@ -35,6 +35,7 @@ function optionsFromArgs(args) {
|
|
|
35
35
|
agents: args.agents,
|
|
36
36
|
force: Boolean(args.force),
|
|
37
37
|
offline: Boolean(args.offline),
|
|
38
|
+
...(args.hooksVia ? { hookMode: String(args.hooksVia) } : {}),
|
|
38
39
|
};
|
|
39
40
|
if (args.apiKeyStdin) {
|
|
40
41
|
options.apiKey = fs.readFileSync(0, 'utf8').trim();
|
|
@@ -111,14 +112,40 @@ async function main() {
|
|
|
111
112
|
}
|
|
112
113
|
const options = optionsFromArgs(args);
|
|
113
114
|
if (command === 'init') {
|
|
115
|
+
// One user = one vault: before writing anything, make sure the Memory Key
|
|
116
|
+
// the hooks will use is the vault selected in OAuth (codex mcp login).
|
|
117
|
+
const { loadConfig, getApiKey } = require('../runtime/config.cjs');
|
|
118
|
+
const { checkVaultBinding, vaultLabel, VaultMismatchError } = require('../runtime/vault-binding.cjs');
|
|
119
|
+
const hookKey = options.apiKey || getApiKey(process.env, loadConfig(process.env));
|
|
120
|
+
let binding = null;
|
|
121
|
+
if (hookKey && !options.offline) {
|
|
122
|
+
binding = await checkVaultBinding(hookKey);
|
|
123
|
+
if (binding.status === 'mismatch') throw new VaultMismatchError(binding);
|
|
124
|
+
}
|
|
114
125
|
const result = init(options);
|
|
115
|
-
process.stdout.write(`MemoryRouter Codex ${INTEGRATION_VERSION} installed (${result.layout.scope}, ${result.auth}).\n`);
|
|
116
|
-
process.stdout.write(` config: ${result.layout.configFile}\n`);
|
|
126
|
+
process.stdout.write(`MemoryRouter Codex ${INTEGRATION_VERSION} installed (${result.layout.scope}, ${result.auth}${result.hookMode === 'mcp' ? ', hooks over your MemoryRouter sign-in' : ''}).\n`);
|
|
127
|
+
process.stdout.write(` config: ${result.layout.configFile}${result.adoptedServer ? ` (kept your existing "${result.adoptedServer}" server)` : ''}\n`);
|
|
117
128
|
if (options.hooks !== false) process.stdout.write(` hooks: ${result.layout.hooksFile}\n`);
|
|
118
129
|
if (options.agents !== false) process.stdout.write(` guide: ${result.layout.agentsFile}\n`);
|
|
119
|
-
if (
|
|
120
|
-
if (
|
|
121
|
-
process.stdout.write(
|
|
130
|
+
if (binding && binding.status === 'match') process.stdout.write(`\nVault check: the hooks use the same vault you selected in OAuth, ${vaultLabel(binding)}.\n`);
|
|
131
|
+
else if (binding && binding.status === 'mixed') process.stdout.write(`\nVault check: ${binding.message}\n`);
|
|
132
|
+
else if (binding && binding.status === 'unknown') process.stdout.write(`\nVault check skipped (${binding.message}). Run doctor later to confirm one vault.\n`);
|
|
133
|
+
if (result.hookMode === 'mcp') {
|
|
134
|
+
process.stdout.write('\nAutomatic memory: before every prompt Codex searches your vault and adds what it finds as context; after every turn it saves your message and its final response (never tool calls, tool output or intermediate steps). No API key needed.\n');
|
|
135
|
+
process.stdout.write('\nNext:\n');
|
|
136
|
+
if (!result.adoptedServer) process.stdout.write(' 1. Sign in: Codex app Settings > Plugins > memoryrouter > Authenticate (or run: codex mcp login memoryrouter)\n');
|
|
137
|
+
process.stdout.write(` ${result.adoptedServer ? '1' : '2'}. Trust the MemoryRouter hooks once: Codex app Settings > Hooks (or /hooks in the CLI)\n`);
|
|
138
|
+
process.stdout.write(` ${result.adoptedServer ? '2' : '3'}. Start a New chat\n`);
|
|
139
|
+
} else if (result.auth === 'oauth') {
|
|
140
|
+
const pick = binding && binding.vault ? ` and select the vault ${vaultLabel(binding)} (the one your hooks use)` : '';
|
|
141
|
+
process.stdout.write(`\nNext: codex mcp login memoryrouter${pick}\nThen open Codex, run /hooks and trust the MemoryRouter hooks, and check /mcp verbose.\n`);
|
|
142
|
+
}
|
|
143
|
+
else process.stdout.write('\nNext: open Codex, run /hooks, and trust the MemoryRouter hooks.\n');
|
|
144
|
+
if (result.keySource === 'claude-code-config') process.stdout.write('\nAutomatic hooks reuse your Claude Code Memory Key (~/.memoryrouter/config.json), so both agents share the same vault.\n');
|
|
145
|
+
if (!result.automaticMemory && options.hooks !== false) process.stdout.write('\nRemote MCP tools are configured, but automatic hooks are fail-open/no-op until MEMORYROUTER_API_KEY is available or init is re-run with --api-key-stdin.\n');
|
|
146
|
+
const scopeArgs = result.layout.scope === 'project' ? `--scope project --project-dir ${JSON.stringify(result.layout.root)}` : '--scope user';
|
|
147
|
+
process.stdout.write(`\nVerify: npx -y memoryrouter-codex@${INTEGRATION_VERSION} doctor ${scopeArgs}\n`);
|
|
148
|
+
process.stdout.write('One memory across every session, repo and tool: Codex, Claude Code (memoryrouter-claude) and the MemoryRouter MCP all use the same vault.\n');
|
|
122
149
|
return;
|
|
123
150
|
}
|
|
124
151
|
if (command === 'doctor') {
|
package/lib/hooks.cjs
CHANGED
|
@@ -5,7 +5,14 @@ const path = require('path');
|
|
|
5
5
|
const { atomicWrite, readText } = require('./managed.cjs');
|
|
6
6
|
const { INTEGRATION_VERSION } = require('../runtime/constants.cjs');
|
|
7
7
|
|
|
8
|
+
// The hidden Codex lifecycle tools on the MemoryRouter MCP server
|
|
9
|
+
// (mcp-server/src/tools/codex-hooks.ts).
|
|
10
|
+
const MCP_HOOK_TOOLS = new Set(['codex_turn_start', 'codex_turn_end', 'codex_session_start']);
|
|
11
|
+
|
|
8
12
|
function isOwnedHook(handler) {
|
|
13
|
+
if (handler?.type === 'mcp_tool') {
|
|
14
|
+
return handler.server === 'memoryrouter' && MCP_HOOK_TOOLS.has(handler.tool);
|
|
15
|
+
}
|
|
9
16
|
return handler?.type === 'command' && typeof handler.command === 'string' &&
|
|
10
17
|
(/memoryrouter[\\/]runtime[\\/]hook\.cjs/.test(handler.command) || /memoryrouter-codex(?:@[^ ]+)?\s+hook\b/.test(handler.command));
|
|
11
18
|
}
|
|
@@ -49,16 +56,47 @@ function managedGroups(runtimeDir, scope) {
|
|
|
49
56
|
...(statusMessage ? { statusMessage } : {}),
|
|
50
57
|
...(additionalContextLimit ? { additionalContextLimit } : {}),
|
|
51
58
|
});
|
|
59
|
+
// Per turn: recall before the prompt, save the user's prompt and the final
|
|
60
|
+
// assistant response after the turn. Nothing in between (no tool calls, tool
|
|
61
|
+
// output, reasoning, subagent output). SessionEnd only saves the prompt of a
|
|
62
|
+
// last turn that never finished (interrupted), see runtime/hook.cjs flush.
|
|
52
63
|
return {
|
|
53
64
|
SessionStart: [{ matcher: 'startup|resume|clear|compact', hooks: [handler('session-start', 8, 'Recalling MemoryRouter context', 3500)] }],
|
|
54
|
-
UserPromptSubmit: [{ hooks: [handler('inject', 8, 'Searching
|
|
55
|
-
Stop: [{ hooks: [handler('capture', 18, 'Saving
|
|
56
|
-
SubagentStop: [{ hooks: [handler('capture-subagent', 15, 'Saving durable subagent outcome')] }],
|
|
57
|
-
PreCompact: [{ matcher: 'manual|auto', hooks: [handler('flush', 18, 'Saving memory before compaction')] }],
|
|
65
|
+
UserPromptSubmit: [{ hooks: [handler('inject', 8, 'Searching MemoryRouter', 3500)] }],
|
|
66
|
+
Stop: [{ hooks: [handler('capture', 18, 'Saving to MemoryRouter')] }],
|
|
58
67
|
SessionEnd: [{ hooks: [handler('flush', 3)] }],
|
|
59
68
|
};
|
|
60
69
|
}
|
|
61
70
|
|
|
71
|
+
/**
|
|
72
|
+
* OAuth hooks: no API key and no local runtime. Codex calls hidden tools on the
|
|
73
|
+
* already-authenticated `memoryrouter` MCP server (`type: "mcp_tool"`), with
|
|
74
|
+
* the user's OAuth token and the vault picked during sign-in.
|
|
75
|
+
* UserPromptSubmit -> codex_turn_start: recall for this prompt (returned as
|
|
76
|
+
* developer context), then save the prompt.
|
|
77
|
+
* Stop -> codex_turn_end: save the final assistant message.
|
|
78
|
+
* SessionStart (compact) -> codex_session_start: restore context right after
|
|
79
|
+
* Codex compacts a long chat.
|
|
80
|
+
* Codex does not support mcp_tool hooks on SessionEnd; none is needed.
|
|
81
|
+
*/
|
|
82
|
+
function mcpManagedGroups(serverName = 'memoryrouter') {
|
|
83
|
+
const turn = { session_id: '${session_id}', turn_id: '${turn_id}', cwd: '${cwd}' };
|
|
84
|
+
return {
|
|
85
|
+
// Only after compaction: on startup/resume/clear the first prompt's
|
|
86
|
+
// codex_turn_start recall already injects context (no double injection).
|
|
87
|
+
SessionStart: [{
|
|
88
|
+
matcher: 'compact',
|
|
89
|
+
hooks: [{ type: 'mcp_tool', server: serverName, tool: 'codex_session_start', input: { session_id: '${session_id}', cwd: '${cwd}', source: '${source}' }, timeout: 12, statusMessage: 'Recalling MemoryRouter context' }],
|
|
90
|
+
}],
|
|
91
|
+
UserPromptSubmit: [{
|
|
92
|
+
hooks: [{ type: 'mcp_tool', server: serverName, tool: 'codex_turn_start', input: { prompt: '${prompt}', ...turn }, timeout: 12, statusMessage: 'Searching MemoryRouter' }],
|
|
93
|
+
}],
|
|
94
|
+
Stop: [{
|
|
95
|
+
hooks: [{ type: 'mcp_tool', server: serverName, tool: 'codex_turn_end', input: { last_assistant_message: '${last_assistant_message}', ...turn }, timeout: 15, statusMessage: 'Saving to MemoryRouter' }],
|
|
96
|
+
}],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
62
100
|
function parseHooks(file) {
|
|
63
101
|
const raw = readText(file);
|
|
64
102
|
if (!raw.trim()) return { description: 'Lifecycle hooks for this Codex configuration.', hooks: {} };
|
|
@@ -71,18 +109,19 @@ function parseHooks(file) {
|
|
|
71
109
|
}
|
|
72
110
|
}
|
|
73
111
|
|
|
74
|
-
function installHooks(file, runtimeDir, scope) {
|
|
112
|
+
function installHooks(file, runtimeDir, scope, options = {}) {
|
|
75
113
|
const current = parseHooks(file);
|
|
76
114
|
const clean = cleanOwnedHooks(current);
|
|
77
115
|
clean.description = clean.description || 'Lifecycle hooks for this Codex configuration.';
|
|
78
116
|
clean.hooks = clean.hooks || {};
|
|
79
|
-
|
|
117
|
+
const groupsToInstall = options.mode === 'mcp' ? mcpManagedGroups(options.serverName) : managedGroups(runtimeDir, scope);
|
|
118
|
+
for (const [event, groups] of Object.entries(groupsToInstall)) {
|
|
80
119
|
clean.hooks[event] = [...(clean.hooks[event] || []), ...groups];
|
|
81
120
|
}
|
|
82
121
|
const rendered = `${JSON.stringify(clean, null, 2)}\n`;
|
|
83
122
|
const previous = readText(file);
|
|
84
123
|
atomicWrite(file, rendered);
|
|
85
|
-
return { changed: rendered !== previous, file, events: Object.keys(
|
|
124
|
+
return { changed: rendered !== previous, file, mode: options.mode === 'mcp' ? 'mcp' : 'command', events: Object.keys(groupsToInstall) };
|
|
86
125
|
}
|
|
87
126
|
|
|
88
127
|
function uninstallHooks(file) {
|
|
@@ -99,4 +138,4 @@ function uninstallHooks(file) {
|
|
|
99
138
|
return { changed: true, file };
|
|
100
139
|
}
|
|
101
140
|
|
|
102
|
-
module.exports = { isOwnedHook, cleanOwnedHooks, commandFor, managedGroups, parseHooks, installHooks, uninstallHooks };
|
|
141
|
+
module.exports = { isOwnedHook, cleanOwnedHooks, commandFor, managedGroups, mcpManagedGroups, MCP_HOOK_TOOLS, parseHooks, installHooks, uninstallHooks };
|
package/lib/installer.cjs
CHANGED
|
@@ -4,14 +4,14 @@ const fs = require('fs');
|
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { execFileSync } = require('child_process');
|
|
7
|
-
const { codexHome, dataHome, loadConfig, saveConfig, configPath } = require('../runtime/config.cjs');
|
|
7
|
+
const { codexHome, dataHome, loadConfig, saveConfig, configPath, getApiKey, apiKeySource } = require('../runtime/config.cjs');
|
|
8
8
|
const { resolveProject } = require('../runtime/project.cjs');
|
|
9
9
|
const { MCP_ENDPOINT, INTEGRATION_VERSION } = require('../runtime/constants.cjs');
|
|
10
10
|
const {
|
|
11
11
|
installToml, uninstallToml, installAgents, uninstallAgents, readText, replaceBlock,
|
|
12
|
-
hasUnmanagedMemoryRouterTable, TOML_START, TOML_END, AGENTS_START, AGENTS_END,
|
|
12
|
+
hasUnmanagedMemoryRouterTable, externalRemoteServerName, TOML_START, TOML_END, AGENTS_START, AGENTS_END,
|
|
13
13
|
} = require('./managed.cjs');
|
|
14
|
-
const { installHooks, uninstallHooks, isOwnedHook, managedGroups, parseHooks } = require('./hooks.cjs');
|
|
14
|
+
const { installHooks, uninstallHooks, isOwnedHook, managedGroups, mcpManagedGroups, parseHooks } = require('./hooks.cjs');
|
|
15
15
|
|
|
16
16
|
function resolveLayout(options = {}) {
|
|
17
17
|
const env = options.env || process.env;
|
|
@@ -54,6 +54,9 @@ function preflight(layout, options = {}) {
|
|
|
54
54
|
const configText = readText(layout.configFile);
|
|
55
55
|
// Validate marker pairs before any file is changed.
|
|
56
56
|
replaceBlock(configText, TOML_START, TOML_END, '');
|
|
57
|
+
// A remote MemoryRouter server the user added themselves (Codex app:
|
|
58
|
+
// Settings > Plugins > Add MCP server) is adopted as is, never an error.
|
|
59
|
+
if (options.auth !== 'api-key' && externalRemoteServerName(configText, MCP_ENDPOINT)) return;
|
|
57
60
|
if (hasUnmanagedMemoryRouterTable(configText) && !options.force) {
|
|
58
61
|
throw new Error(`An unmanaged [mcp_servers.memoryrouter] table already exists in ${layout.configFile}. Re-run with --force after reviewing the migration backup.`);
|
|
59
62
|
}
|
|
@@ -61,16 +64,41 @@ function preflight(layout, options = {}) {
|
|
|
61
64
|
if (options.agents !== false) replaceBlock(readText(layout.agentsFile), AGENTS_START, AGENTS_END, '');
|
|
62
65
|
}
|
|
63
66
|
|
|
67
|
+
/**
|
|
68
|
+
* How the automatic hooks reach MemoryRouter.
|
|
69
|
+
* - 'mcp': Codex `mcp_tool` hooks call MemoryRouter's hidden lifecycle tools
|
|
70
|
+
* over the signed-in OAuth MCP connection. No API key on disk.
|
|
71
|
+
* - 'command': the local Node runtime with a Memory Key (api-key mode, or
|
|
72
|
+
* OAuth users who also configured a key, e.g. shared with Claude Code).
|
|
73
|
+
*/
|
|
74
|
+
function resolveHookMode(options, auth, env) {
|
|
75
|
+
if (options.hookMode === 'mcp' || options.hookMode === 'command') return options.hookMode;
|
|
76
|
+
if (auth === 'api-key') return 'command';
|
|
77
|
+
const key = options.apiKey || getApiKey(env, loadConfig(options.env));
|
|
78
|
+
return key ? 'command' : 'mcp';
|
|
79
|
+
}
|
|
80
|
+
|
|
64
81
|
function init(options = {}) {
|
|
65
82
|
const layout = resolveLayout(options);
|
|
66
83
|
const auth = options.auth || 'oauth';
|
|
67
84
|
if (!['oauth', 'api-key'].includes(auth)) throw new Error('--auth must be oauth or api-key');
|
|
68
|
-
|
|
85
|
+
if (options.hookMode && !['mcp', 'command'].includes(options.hookMode)) throw new Error('--hooks-via must be mcp or command');
|
|
86
|
+
const env = options.env || process.env;
|
|
87
|
+
preflight(layout, { ...options, auth });
|
|
88
|
+
const hookMode = resolveHookMode(options, auth, env);
|
|
89
|
+
if (hookMode === 'mcp' && auth === 'api-key') throw new Error('--hooks-via mcp needs the OAuth MCP server (--auth oauth).');
|
|
69
90
|
copyRuntime(layout.runtimeDir);
|
|
70
91
|
const results = [];
|
|
71
|
-
|
|
72
|
-
if (
|
|
73
|
-
|
|
92
|
+
const adoptedServer = auth === 'oauth' ? externalRemoteServerName(readText(layout.configFile), MCP_ENDPOINT) : null;
|
|
93
|
+
if (adoptedServer) {
|
|
94
|
+
// Leave the user's own server table (and its sign-in) untouched.
|
|
95
|
+
results.push({ changed: false, file: layout.configFile, adopted: adoptedServer });
|
|
96
|
+
} else {
|
|
97
|
+
results.push(installToml(layout.configFile, { auth, scope: layout.scope, runtimeDir: layout.runtimeDir, force: options.force }));
|
|
98
|
+
}
|
|
99
|
+
const serverName = adoptedServer || 'memoryrouter';
|
|
100
|
+
if (options.hooks !== false) results.push(installHooks(layout.hooksFile, layout.runtimeDir, layout.scope, { mode: hookMode, serverName }));
|
|
101
|
+
if (options.agents !== false) results.push(installAgents(layout.agentsFile, { hookMode }));
|
|
74
102
|
|
|
75
103
|
if (options.apiKey) {
|
|
76
104
|
const current = loadConfig(options.env);
|
|
@@ -81,6 +109,9 @@ function init(options = {}) {
|
|
|
81
109
|
installedAt: new Date().toISOString(),
|
|
82
110
|
scope: layout.scope,
|
|
83
111
|
auth,
|
|
112
|
+
hookMode,
|
|
113
|
+
serverName,
|
|
114
|
+
adoptedServer: Boolean(adoptedServer),
|
|
84
115
|
configFile: layout.configFile,
|
|
85
116
|
hooksFile: options.hooks === false ? null : layout.hooksFile,
|
|
86
117
|
agentsFile: options.agents === false ? null : layout.agentsFile,
|
|
@@ -90,7 +121,16 @@ function init(options = {}) {
|
|
|
90
121
|
fs.mkdirSync(manifestDir, { recursive: true, mode: 0o700 });
|
|
91
122
|
const manifestName = Buffer.from(`${layout.scope}:${layout.root}`).toString('base64url');
|
|
92
123
|
fs.writeFileSync(path.join(manifestDir, `${manifestName}.json`), `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
|
|
93
|
-
return {
|
|
124
|
+
return {
|
|
125
|
+
layout,
|
|
126
|
+
auth,
|
|
127
|
+
hookMode,
|
|
128
|
+
serverName,
|
|
129
|
+
adoptedServer,
|
|
130
|
+
results,
|
|
131
|
+
automaticMemory: options.hooks !== false && (hookMode === 'mcp' || Boolean(options.apiKey || getApiKey(env, loadConfig(options.env)))),
|
|
132
|
+
keySource: options.apiKey ? 'codex-config' : apiKeySource(env, loadConfig(options.env)),
|
|
133
|
+
};
|
|
94
134
|
}
|
|
95
135
|
|
|
96
136
|
function uninstall(options = {}) {
|
|
@@ -168,7 +208,9 @@ async function doctor(options = {}) {
|
|
|
168
208
|
else if (!versionAtLeast(codex.version, [0, 145, 0])) addCheck(checks, 'codex-version', 'error', `${codex.output} is too old for the lifecycle hooks used here. Upgrade to Codex 0.145.0 or newer.`);
|
|
169
209
|
else addCheck(checks, 'codex-version', 'pass', `${codex.output} supports current hooks and remote MCP configuration.`);
|
|
170
210
|
|
|
171
|
-
|
|
211
|
+
const adopted = externalRemoteServerName(configText, MCP_ENDPOINT);
|
|
212
|
+
if (adopted) addCheck(checks, 'mcp-config', 'pass', `MemoryRouter MCP server "${adopted}" (added in the Codex app or by hand) is configured.`);
|
|
213
|
+
else addCheck(checks, 'mcp-config', configText.includes(TOML_START) && configText.includes('[mcp_servers.memoryrouter]') ? 'pass' : 'error',
|
|
172
214
|
configText.includes(TOML_START) ? 'Managed MemoryRouter MCP configuration is present.' : 'Managed MemoryRouter MCP configuration is missing.');
|
|
173
215
|
addCheck(checks, 'runtime', runtimeVersion === INTEGRATION_VERSION ? 'pass' : 'error',
|
|
174
216
|
runtimeVersion ? `Installed runtime version is ${runtimeVersion}; package version is ${INTEGRATION_VERSION}.` : 'Managed runtime is missing.');
|
|
@@ -176,22 +218,40 @@ async function doctor(options = {}) {
|
|
|
176
218
|
let hooks = null;
|
|
177
219
|
try { hooks = hooksText ? JSON.parse(hooksText) : null; } catch { /* handled */ }
|
|
178
220
|
const owned = hooks ? Object.values(hooks.hooks || {}).flat().flatMap(group => group.hooks || []).filter(isOwnedHook) : [];
|
|
179
|
-
const
|
|
180
|
-
|
|
221
|
+
const mcpHooks = owned.filter(handler => handler.type === 'mcp_tool');
|
|
222
|
+
const expectedOwned = mcpHooks.length
|
|
223
|
+
? Object.values(mcpManagedGroups()).flat().flatMap(group => group.hooks || []).length
|
|
224
|
+
: Object.values(managedGroups(layout.runtimeDir, layout.scope)).flat().flatMap(group => group.hooks || []).length;
|
|
225
|
+
addCheck(checks, 'hooks', owned.length === expectedOwned ? 'pass' : 'error', owned.length
|
|
226
|
+
? `${owned.length} of ${expectedOwned} managed lifecycle hooks found${mcpHooks.length ? ' (over the signed-in MCP connection, no API key needed)' : ''}. Trust them once in Codex: app Settings > Hooks, or /hooks in the CLI.`
|
|
227
|
+
: 'Managed lifecycle hooks are missing or hooks.json is invalid.');
|
|
181
228
|
addCheck(checks, 'agents', agentsText.includes(AGENTS_START) ? 'pass' : 'warn', agentsText.includes(AGENTS_START) ? 'Managed AGENTS.md guidance is present.' : 'Managed AGENTS.md guidance is missing.');
|
|
182
229
|
|
|
183
230
|
const cfg = loadConfig(options.env);
|
|
184
|
-
const
|
|
185
|
-
|
|
231
|
+
const keyEnv = options.env || process.env;
|
|
232
|
+
const key = getApiKey(keyEnv, cfg);
|
|
233
|
+
const keyFrom = apiKeySource(keyEnv, cfg);
|
|
234
|
+
if (!key && mcpHooks.length) addCheck(checks, 'automatic-auth', 'pass', 'Automatic recall and saving use your MemoryRouter sign-in (OAuth). No API key needed.');
|
|
235
|
+
else if (!key) addCheck(checks, 'automatic-auth', 'warn', 'No MemoryRouter API key is available. Remote OAuth tools can work after `codex mcp login memoryrouter`, but automatic hook retrieval/capture is disabled.');
|
|
186
236
|
else {
|
|
187
237
|
try {
|
|
188
238
|
const { stats } = require('../runtime/api.cjs');
|
|
189
239
|
const response = options.offline ? { ok: true, status: 0 } : await stats(key, { apiBase: cfg.apiBase, timeoutMs: 6000, retries: 0 });
|
|
190
|
-
addCheck(checks, 'automatic-auth', response.ok ? 'pass' : 'error', options.offline ? 'API key is configured (network check skipped).' : response.ok ?
|
|
240
|
+
addCheck(checks, 'automatic-auth', response.ok ? 'pass' : 'error', options.offline ? 'API key is configured (network check skipped).' : response.ok ? `API key is valid${keyFrom === 'claude-code-config' ? ' (reusing the Claude Code key in ~/.memoryrouter/config.json)' : ''}; automatic retrieval/capture is active.` : `API key validation failed (HTTP ${response.status}).`);
|
|
191
241
|
} catch (error) { addCheck(checks, 'automatic-auth', 'error', `API key validation failed: ${error.message}`); }
|
|
242
|
+
if (!options.offline) {
|
|
243
|
+
const { checkVaultBinding, vaultLabel } = require('../runtime/vault-binding.cjs');
|
|
244
|
+
const binding = await checkVaultBinding(key, { env: options.env || process.env, timeoutMs: 5000 });
|
|
245
|
+
const levels = { match: 'pass', mismatch: 'error', mixed: 'warn', 'not-connected': 'info', unknown: 'warn' };
|
|
246
|
+
addCheck(checks, 'one-vault', levels[binding.status] || 'warn',
|
|
247
|
+
binding.status === 'match' ? `Hooks and your OAuth connection use the same vault ${vaultLabel(binding)}.`
|
|
248
|
+
: binding.status === 'not-connected' ? `No MemoryRouter OAuth connection yet. When you run \`codex mcp login memoryrouter\`, select ${vaultLabel(binding)}.`
|
|
249
|
+
: binding.status === 'unknown' ? `Vault match not checked (${binding.message}).`
|
|
250
|
+
: binding.message);
|
|
251
|
+
}
|
|
192
252
|
}
|
|
193
253
|
|
|
194
|
-
if (configText.includes('auth = "oauth"')) {
|
|
254
|
+
if (configText.includes('auth = "oauth"') || adopted) {
|
|
195
255
|
if (options.offline) {
|
|
196
256
|
addCheck(checks, 'oauth-discovery', 'warn', 'OAuth discovery check skipped in offline mode.');
|
|
197
257
|
addCheck(checks, 'oauth-session', 'info', 'Browser consent/token issuance is user-specific. Run `codex mcp login memoryrouter`; then `/mcp verbose`. Doctor does not claim login completion without interactive consent.');
|
|
@@ -222,10 +282,11 @@ async function doctor(options = {}) {
|
|
|
222
282
|
? `Codex native Memories are enabled${externalDisabled ? ' and external-context generation is disabled.' : '. Consider [memories] disable_on_external_context = true to avoid re-extracting MemoryRouter-assisted chats.'}`
|
|
223
283
|
: 'Codex native Memories are off or configured in another layer. This integration does not overwrite native-memory settings.');
|
|
224
284
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
285
|
+
// One vault per user: report the scope plainly, plus the repository label
|
|
286
|
+
// that captures from this directory carry (metadata only, never a wall).
|
|
287
|
+
const identityDir = layout.scope === 'user' ? (options.projectDir || process.cwd()) : layout.root;
|
|
288
|
+
const project = resolveProject(identityDir, { env: options.env, config: loadConfig(options.env) });
|
|
289
|
+
addCheck(checks, 'memory-scope', 'pass', `One vault for everything: every Codex session, every repository, Claude Code (memoryrouter-claude), and the MemoryRouter MCP read and write the same memory. Captures from this directory are tagged ${project.handle} (${project.source}) so recall can label them; the tag never limits what is recalled.`);
|
|
229
290
|
addCheck(checks, 'cloud-surface', layout.scope === 'project' ? 'info' : 'warn', layout.scope === 'project'
|
|
230
291
|
? 'Project config/hooks can travel with the repository, but cloud environments must trust the project, install Node, and provide MEMORYROUTER_API_KEY. ChatGPT web does not consume local Codex config; use a hosted plugin/connector there.'
|
|
231
292
|
: 'User-scope config covers local CLI, desktop app, and IDE on this host. Codex cloud and ChatGPT web do not inherit this machine-local config.');
|
package/lib/managed.cjs
CHANGED
|
@@ -53,6 +53,26 @@ function removeUnmanagedMemoryRouterTables(text) {
|
|
|
53
53
|
return kept.join('\n').replace(/\n{3,}/g, '\n\n');
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* The MCP server name the user already connected to MemoryRouter's remote
|
|
58
|
+
* endpoint outside our managed block (for example the Codex app's
|
|
59
|
+
* Settings > Plugins > Add MCP server), or null. Returns the first
|
|
60
|
+
* `[mcp_servers.<name>]` whose `url` is the MemoryRouter MCP endpoint.
|
|
61
|
+
*/
|
|
62
|
+
function externalRemoteServerName(text, endpoint) {
|
|
63
|
+
const stripped = replaceBlock(text, TOML_START, TOML_END, '');
|
|
64
|
+
const lines = stripped.split('\n');
|
|
65
|
+
let current = null;
|
|
66
|
+
for (const line of lines) {
|
|
67
|
+
const table = line.match(/^\s*\[mcp_servers\.("?)([^\]".]+)\1\]\s*(?:#.*)?$/);
|
|
68
|
+
if (table) { current = table[2]; continue; }
|
|
69
|
+
if (/^\s*\[/.test(line)) { current = null; continue; }
|
|
70
|
+
const url = line.match(/^\s*url\s*=\s*"([^"]+)"/);
|
|
71
|
+
if (current && url && url[1].replace(/\/+$/, '') === endpoint.replace(/\/+$/, '')) return current;
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
56
76
|
function tomlString(value) { return JSON.stringify(String(value)); }
|
|
57
77
|
|
|
58
78
|
function mcpBlock({ auth = 'oauth', scope = 'user', runtimeDir }) {
|
|
@@ -129,13 +149,16 @@ function uninstallToml(file) {
|
|
|
129
149
|
return { changed: true, file };
|
|
130
150
|
}
|
|
131
151
|
|
|
132
|
-
function agentsBlock() {
|
|
133
|
-
|
|
152
|
+
function agentsBlock(options = {}) {
|
|
153
|
+
const automatic = options.hookMode === 'mcp'
|
|
154
|
+
? '- Automatic hooks (over the signed-in MemoryRouter connection) search the vault before each prompt and add what they find as context, then save each completed turn: the user\'s message and your final response only, never tool calls, tool output or intermediate steps.\n'
|
|
155
|
+
: '- Automatic hooks search the vault before each prompt and save each completed turn (the user\'s message and your final response only) when a MemoryRouter API key is configured.\n';
|
|
156
|
+
return `## MemoryRouter memory\n\n- MemoryRouter is the durable cross-machine, cross-tool, and team/auditable memory layer. Codex native Memories may remain enabled as the local personal recall layer.\n${automatic}- For explicit recall, search MemoryRouter before answering questions about prior decisions, preferences, incidents, or unfinished work. Depending on auth mode, the tool is named \`search_memories\` or \`memory_search\`.\n- Store only concise, durable decisions, preferences, facts, and incident outcomes. Depending on auth mode, use \`store_memory\` or \`memory_store\`. Do not duplicate a completed turn that hooks already captured.\n- Keep and cite returned \`mrh_...\` handles when a specific memory must be referenced later.\n- Project scope is derived from the canonical git remote when available, so worktrees and machines share identity without uploading local paths. It is the same project memory Claude Code uses through memoryrouter-claude, so decisions made in either agent are available in both.\n- If MemoryRouter is offline, continue the task without memory; never block work on recall or capture.`;
|
|
134
157
|
}
|
|
135
158
|
|
|
136
|
-
function installAgents(file) {
|
|
159
|
+
function installAgents(file, options = {}) {
|
|
137
160
|
const current = readText(file);
|
|
138
|
-
const updated = replaceBlock(current, AGENTS_START, AGENTS_END, agentsBlock());
|
|
161
|
+
const updated = replaceBlock(current, AGENTS_START, AGENTS_END, agentsBlock(options));
|
|
139
162
|
atomicWrite(file, updated);
|
|
140
163
|
return { changed: updated !== current, file };
|
|
141
164
|
}
|
|
@@ -158,6 +181,7 @@ module.exports = {
|
|
|
158
181
|
atomicWrite,
|
|
159
182
|
replaceBlock,
|
|
160
183
|
hasUnmanagedMemoryRouterTable,
|
|
184
|
+
externalRemoteServerName,
|
|
161
185
|
removeUnmanagedMemoryRouterTables,
|
|
162
186
|
mcpBlock,
|
|
163
187
|
installToml,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memoryrouter-codex",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Automatic
|
|
3
|
+
"version": "1.8.0",
|
|
4
|
+
"description": "Automatic MemoryRouter memory for OpenAI Codex: one vault shared by every session, repo and tool, including Claude Code",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|
|
7
7
|
"bin": {
|