memoryrouter-codex 1.0.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/LICENSE +21 -0
- package/README.md +225 -0
- package/RESEARCH.md +57 -0
- package/bin/cli.cjs +102 -0
- package/lib/hooks.cjs +103 -0
- package/lib/installer.cjs +235 -0
- package/lib/managed.cjs +168 -0
- package/package.json +39 -0
- package/runtime/api.cjs +80 -0
- package/runtime/config.cjs +166 -0
- package/runtime/constants.cjs +23 -0
- package/runtime/hook.cjs +260 -0
- package/runtime/mcp-stdio.cjs +160 -0
- package/runtime/memory.cjs +207 -0
- package/runtime/project.cjs +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MemoryRouter
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# MemoryRouter for OpenAI Codex
|
|
2
|
+
|
|
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
|
+
|
|
5
|
+
## What this integration does
|
|
6
|
+
|
|
7
|
+
| Layer | Behavior |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| `UserPromptSubmit` hook | Searches MemoryRouter before every prompt and injects bounded, project-scoped context as developer context. |
|
|
10
|
+
| `PostToolUse` + `Stop` hooks | Captures the user request, stable hook-provided tool inputs/results, and final answer after each completed turn. |
|
|
11
|
+
| `SubagentStop` hook | Captures the stable final outcome of completed subagents without parsing their unstable transcript files. |
|
|
12
|
+
| `PreCompact` / compact `SessionStart` | Checkpoints work before compaction and recalls context before Codex continues after compaction. |
|
|
13
|
+
| `SessionEnd` | Best-effort final checkpoint within Codex's three-second SessionEnd budget. |
|
|
14
|
+
| Remote MCP | OAuth-based explicit search/store through `https://mcp.memoryrouter.ai/mcp`. |
|
|
15
|
+
| Local MCP fallback | API-key-authenticated stdio tools with strict project filtering, handles, local suppression, status, and full-vault clear confirmation. |
|
|
16
|
+
| `AGENTS.md` | Tells Codex when to recall/store and how MemoryRouter coexists with native Memories. |
|
|
17
|
+
|
|
18
|
+
MemoryRouter is the **cross-machine, cross-tool, team-shareable, and auditable** layer. Codex's native Memories remain useful as an automated local personal recall layer.
|
|
19
|
+
|
|
20
|
+
## Requirements and supported surfaces
|
|
21
|
+
|
|
22
|
+
- Node.js 18 or newer.
|
|
23
|
+
- **Codex 0.145.0 or newer** for the complete lifecycle-hook integration. Version 0.146.0 was tested directly.
|
|
24
|
+
- Local Codex CLI, ChatGPT desktop app's local Codex host, and Codex IDE extension share `config.toml` MCP configuration.
|
|
25
|
+
- User/project hooks require the Codex host to support and trust command hooks. Use `/hooks` to review them.
|
|
26
|
+
- Project `.codex` config is loaded only for trusted projects.
|
|
27
|
+
- ChatGPT web does **not** read a laptop's `~/.codex/config.toml` or local hooks. Use MemoryRouter's hosted plugin/connector surface there.
|
|
28
|
+
- 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.
|
|
29
|
+
|
|
30
|
+
There is no dependency on the obsolete `experimental_use_rmcp_client` setting in current Codex. That flag was required by old Codex releases; it is absent from the current 0.146 configuration reference and is not written by this installer.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
### Recommended: remote OAuth MCP plus automatic hooks
|
|
35
|
+
|
|
36
|
+
Obtain a MemoryRouter memory key from your account. Pass it on stdin so it does not appear in shell history:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
printf '%s' "$MEMORYROUTER_API_KEY" | \
|
|
40
|
+
npx -y memoryrouter-codex@1.0.0 init --scope user --auth oauth --api-key-stdin
|
|
41
|
+
|
|
42
|
+
codex mcp login memoryrouter
|
|
43
|
+
memoryrouter-codex doctor --scope user
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Then start Codex and inspect:
|
|
47
|
+
|
|
48
|
+
```text
|
|
49
|
+
/mcp verbose
|
|
50
|
+
/hooks
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
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.
|
|
54
|
+
|
|
55
|
+
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 either source, remote OAuth MCP tools can work after login, but automatic hooks intentionally become no-op/fail-open.
|
|
56
|
+
|
|
57
|
+
### Project scope
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
cd /path/to/repository
|
|
61
|
+
printf '%s' "$MEMORYROUTER_API_KEY" | \
|
|
62
|
+
npx -y memoryrouter-codex@1.0.0 init --scope project --auth oauth --api-key-stdin
|
|
63
|
+
codex mcp login memoryrouter
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
This writes managed blocks/files under the repository's `AGENTS.md` and `.codex/`. The runtime contains no secret. Review which files your team wants to commit. Every machine or cloud environment must supply its own authentication.
|
|
67
|
+
|
|
68
|
+
### Secure API-key fallback when OAuth is blocked
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
printf '%s' "$MEMORYROUTER_API_KEY" | \
|
|
72
|
+
npx -y memoryrouter-codex@1.0.0 init --scope user --auth api-key --api-key-stdin
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
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:
|
|
76
|
+
|
|
77
|
+
- `memory_search`
|
|
78
|
+
- `memory_store`
|
|
79
|
+
- `memory_status`
|
|
80
|
+
- `memory_forget`
|
|
81
|
+
|
|
82
|
+
For project scope, the stdio command is package-version-pinned through `npx`; secrets still come from the environment or private home config.
|
|
83
|
+
|
|
84
|
+
## Exact configuration written
|
|
85
|
+
|
|
86
|
+
The remote mode adds a versioned managed block equivalent to:
|
|
87
|
+
|
|
88
|
+
```toml
|
|
89
|
+
[mcp_servers.memoryrouter]
|
|
90
|
+
url = "https://mcp.memoryrouter.ai/mcp"
|
|
91
|
+
auth = "oauth"
|
|
92
|
+
enabled = true
|
|
93
|
+
required = false
|
|
94
|
+
startup_timeout_sec = 20
|
|
95
|
+
tool_timeout_sec = 45
|
|
96
|
+
default_tools_approval_mode = "writes"
|
|
97
|
+
scopes = ["memories:read", "memories:write"]
|
|
98
|
+
|
|
99
|
+
[mcp_servers.memoryrouter.tools.store_memory]
|
|
100
|
+
approval_mode = "prompt"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`oauth_resource` is intentionally omitted: MemoryRouter's RFC 9728 metadata already supplies the exact resource. Codex 0.146 was observed duplicating the `resource` authorization parameter when the same value was also pinned in config.
|
|
104
|
+
|
|
105
|
+
The installer preserves unrelated TOML byte-for-byte outside its managed block. If an unmanaged `[mcp_servers.memoryrouter]` table already exists, it refuses to create invalid duplicate TOML. `--force` creates a timestamped backup, removes only that table and its nested MemoryRouter tool tables, and installs the managed block.
|
|
106
|
+
|
|
107
|
+
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.
|
|
108
|
+
|
|
109
|
+
## Project identity and scoping
|
|
110
|
+
|
|
111
|
+
The runtime computes:
|
|
112
|
+
|
|
113
|
+
```text
|
|
114
|
+
proj_<first 20 hex chars of SHA-256(canonical project identity)>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Identity preference:
|
|
118
|
+
|
|
119
|
+
1. Canonical `origin` git URL, with credentials removed and `.git` normalized.
|
|
120
|
+
2. Git common directory (so local worktrees share a project).
|
|
121
|
+
3. Absolute path fallback.
|
|
122
|
+
|
|
123
|
+
Only the opaque handle is stored remotely. Local paths are not sent as identity metadata. A canonical remote makes identity stable across worktrees and machines.
|
|
124
|
+
|
|
125
|
+
Automatic memories use an envelope containing project handle, stable memory handle, kind, and capture time. Retrieval requests extra semantic candidates and then **strictly filters** to the current project handle before context injection. Legacy/unscoped vault records remain available through explicit remote MCP search but are not automatically injected in project mode.
|
|
126
|
+
|
|
127
|
+
Set `retrievalScope` to `"vault"` in the private config only if you intentionally want unscoped automatic recall.
|
|
128
|
+
|
|
129
|
+
## Handles, deduplication, retries, and forgetting
|
|
130
|
+
|
|
131
|
+
- Handles (`mrh_...`) are content-derived from project, kind, and normalized content.
|
|
132
|
+
- Explicit local `memory_store` searches for the exact handle before writing, so exact repeats deduplicate across machines when the earlier record is searchable.
|
|
133
|
+
- Automatic turn capture keeps local captured-turn/handle state and does not retry writes. This avoids most duplicates. Because the current ingest API has no server-side idempotency key, a network timeout after server acceptance can still create a duplicate on a later attempt. The integration does not claim otherwise.
|
|
134
|
+
- Read/search/status requests retry transient errors with short exponential backoff. Writes and destructive operations are never blindly retried.
|
|
135
|
+
- `memory_forget(handle=...)` suppresses that handle from this machine's Codex integration; it explicitly does **not** claim server deletion because the current API has no per-memory delete endpoint.
|
|
136
|
+
- Whole-vault deletion is available only when `memory_forget` receives the exact phrase `CLEAR MY ENTIRE MEMORYROUTER VAULT`. It is irreversible and should require Codex approval.
|
|
137
|
+
- Use the MemoryRouter dashboard for server-side audit/management where available.
|
|
138
|
+
|
|
139
|
+
## Context budget and failure behavior
|
|
140
|
+
|
|
141
|
+
- Default injected context cap: 12,000 characters, assembled at memory boundaries.
|
|
142
|
+
- Hook `additionalContextLimit`: approximately 3,500 tokens, so Codex can spill unexpectedly large context safely instead of consuming the whole model window.
|
|
143
|
+
- Candidate search default: 12 before strict project filtering.
|
|
144
|
+
- All hook entrypoints exit zero on missing credentials, malformed input, API errors, and timeouts. Memory never blocks Codex work.
|
|
145
|
+
- Hook status is recorded without secrets in `~/.memoryrouter/codex/health.json`. Search/store/forget outcomes (metadata only, never memory content or keys) append to a rotating 5 MiB local `~/.memoryrouter/codex/audit.jsonl`. `memory_status` exposes both locations. This is a host-local integration audit, not a server-wide team access log. Session/trace state older than 30 days is pruned at session start.
|
|
146
|
+
- Set `MEMORYROUTER_DEBUG=1` for hook errors on stderr. Debug is off by default.
|
|
147
|
+
|
|
148
|
+
## Codex native Memories coexistence
|
|
149
|
+
|
|
150
|
+
This installer does not overwrite native-memory settings. Native Memories are off by default and can be enabled independently:
|
|
151
|
+
|
|
152
|
+
```toml
|
|
153
|
+
[features]
|
|
154
|
+
memories = true
|
|
155
|
+
|
|
156
|
+
[memories]
|
|
157
|
+
disable_on_external_context = true
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`disable_on_external_context = true` keeps chats that used MCP/web/tool-search context out of Codex's background memory generation, avoiding a second divergent extraction of MemoryRouter-assisted work. This also means those chats will not contribute to native Memories. Leave it `false` if you intentionally want both systems to learn from the same chat.
|
|
161
|
+
|
|
162
|
+
Use `/memories` for per-chat native-memory controls. Keep required team rules in `AGENTS.md`, not in either memory system.
|
|
163
|
+
|
|
164
|
+
## OAuth findings (tested 2026-07-31)
|
|
165
|
+
|
|
166
|
+
Live verification with official `@openai/codex` **0.146.0** established:
|
|
167
|
+
|
|
168
|
+
1. `codex mcp add --url https://mcp.memoryrouter.ai/mcp memoryrouter` writes the expected streamable HTTP entry.
|
|
169
|
+
2. Codex discovers RFC 9728 protected-resource metadata.
|
|
170
|
+
3. Codex discovers `https://auth.memoryrouter.ai` authorization metadata.
|
|
171
|
+
4. Dynamic client registration succeeds and issues a `dcr_...` client ID.
|
|
172
|
+
5. Codex generates an authorization URL with PKCE S256, `memories:read memories:write`, the exact RFC 8707 resource, and a loopback callback.
|
|
173
|
+
6. The browser consent/token step was deliberately not completed by the package test because it is account-specific. Therefore `doctor` verifies discovery compatibility but never reports an OAuth session as logged in without user consent. Use `codex mcp login memoryrouter` and `/mcp verbose` to complete/verify it.
|
|
174
|
+
|
|
175
|
+
The remote server currently negotiates legacy MCP `2025-11-25`. Codex 0.146 remains dual-era and initializes it successfully today. MemoryRouter's MCP server still needs its separate `2026-07-28` protocol modernization; this client integration does not hide that server limitation.
|
|
176
|
+
|
|
177
|
+
## Doctor
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
memoryrouter-codex doctor --scope user
|
|
181
|
+
memoryrouter-codex doctor --scope project --project-dir /repo
|
|
182
|
+
memoryrouter-codex doctor --offline
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Doctor checks Codex version, managed config, runtime version, hook count, `AGENTS.md`, private-key availability/validity, OAuth discovery, native-memory coexistence, project identity portability, and local/cloud surface limitations.
|
|
186
|
+
|
|
187
|
+
## Upgrade and uninstall
|
|
188
|
+
|
|
189
|
+
Upgrade idempotently by re-running the pinned/new package:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
npx -y memoryrouter-codex@1.0.0 init --scope user
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Uninstall:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
npx -y memoryrouter-codex@1.0.0 uninstall --scope user
|
|
199
|
+
# or
|
|
200
|
+
npx -y memoryrouter-codex@1.0.0 uninstall --scope project --project-dir /repo
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
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.
|
|
204
|
+
|
|
205
|
+
## Honest limitations
|
|
206
|
+
|
|
207
|
+
- Hooks are the deterministic automatic path. Remote MCP alone remains model-invoked, not guaranteed every turn.
|
|
208
|
+
- The transcript file path supplied to hooks is documented as unstable, so this integration does **not** parse Codex transcript JSONL. It captures only stable hook fields: prompt, tool input/result, and final assistant message.
|
|
209
|
+
- Hosted tools such as web search do not currently emit `PostToolUse`; their detailed output is not automatically captured unless reflected in the final response.
|
|
210
|
+
- Some specialized tool paths can opt out of hooks. Codex documents hooks as a useful extension surface, not a complete enforcement boundary.
|
|
211
|
+
- 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.
|
|
212
|
+
- Project scoping is strict for this package's automatic retrieval/local MCP. The existing hosted remote MCP server has no first-class project argument, so use local tools/hooks when cross-project isolation is required.
|
|
213
|
+
- Per-memory server deletion is not available through the current API. Local suppression and explicit full-vault clear are accurately labeled.
|
|
214
|
+
- API-level idempotency is not yet available; timeout-after-acceptance duplicates remain possible.
|
|
215
|
+
|
|
216
|
+
## Development
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
cd integrations/codex
|
|
220
|
+
npm test
|
|
221
|
+
npm run typecheck
|
|
222
|
+
npm run pack:dry
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
No package, config, fixture, or runtime file contains a production secret.
|
package/RESEARCH.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Codex integration research and acceptance evidence
|
|
2
|
+
|
|
3
|
+
Verified 2026-07-31. This is engineering evidence, not public marketing copy.
|
|
4
|
+
|
|
5
|
+
## Official sources
|
|
6
|
+
|
|
7
|
+
- MCP configuration and supported local hosts: <https://developers.openai.com/codex/mcp>
|
|
8
|
+
- Current configuration reference: <https://developers.openai.com/codex/config-reference>
|
|
9
|
+
- Lifecycle hooks contract and schemas: <https://developers.openai.com/codex/hooks>
|
|
10
|
+
- `AGENTS.md` discovery and 32 KiB default budget: <https://developers.openai.com/codex/guides/agents-md>
|
|
11
|
+
- Native two-phase local Memories: <https://developers.openai.com/codex/memories>
|
|
12
|
+
- CLI/MCP command reference: <https://developers.openai.com/codex/cli/reference>
|
|
13
|
+
- Codex 0.146.0 release (2026-07-29): <https://github.com/openai/codex/releases/tag/rust-v0.146.0>
|
|
14
|
+
- Current RMCP client source: <https://github.com/openai/codex/blob/main/codex-rs/rmcp-client/src/rmcp_client.rs>
|
|
15
|
+
- Current hook schemas: <https://github.com/openai/codex/tree/main/codex-rs/hooks/schema/generated>
|
|
16
|
+
|
|
17
|
+
## Platform findings used in the architecture
|
|
18
|
+
|
|
19
|
+
- Current Codex supports command hooks for `SessionStart`, `SessionEnd`, `PreToolUse`, `PostToolUse`, `PreCompact`, `PostCompact`, `UserPromptSubmit`, `SubagentStart`, `SubagentStop`, and `Stop`.
|
|
20
|
+
- Hook transcript paths are explicitly documented as unstable. The integration therefore uses stable event fields and an append-only local tool trace rather than parsing transcript JSONL.
|
|
21
|
+
- Hooks are enabled by default and project hooks require trust. `features.codex_hooks` is only a deprecated alias; the current canonical key is `features.hooks`.
|
|
22
|
+
- Remote MCP OAuth no longer needs `experimental_use_rmcp_client` in current Codex. That key is absent from the current configuration reference.
|
|
23
|
+
- The local CLI, desktop Codex host, and IDE extension share Codex config. ChatGPT web uses hosted plugin tools and does not inherit local config.
|
|
24
|
+
- Project `.codex` layers load only in trusted projects.
|
|
25
|
+
- Native Memories are local, off by default, background-extracted/consolidated, and independently controlled by `generate_memories`, `use_memories`, and `disable_on_external_context`.
|
|
26
|
+
|
|
27
|
+
## Live OAuth acceptance item C1
|
|
28
|
+
|
|
29
|
+
Using the official npm package `@openai/codex@0.146.0` with a temporary empty `CODEX_HOME`:
|
|
30
|
+
|
|
31
|
+
1. `codex mcp add --url https://mcp.memoryrouter.ai/mcp memoryrouter` wrote a valid streamable HTTP server entry and immediately detected OAuth.
|
|
32
|
+
2. `codex mcp list` reported the server enabled and not logged in before consent.
|
|
33
|
+
3. `codex mcp get memoryrouter` reported the correct URL and transport.
|
|
34
|
+
4. `codex mcp login memoryrouter` discovered MemoryRouter's RFC 9728 metadata and OAuth authorization metadata.
|
|
35
|
+
5. Dynamic client registration succeeded and returned a `dcr_...` client ID.
|
|
36
|
+
6. Codex produced a loopback authorization URL containing PKCE S256, `memories:read memories:write`, and `resource=https://mcp.memoryrouter.ai/mcp`.
|
|
37
|
+
7. Browser consent was deliberately not completed because token issuance is account-specific. No code claims that user login succeeded.
|
|
38
|
+
8. When `oauth_resource` was explicitly configured to the same value already advertised by RFC 9728, Codex 0.146 emitted the `resource` query parameter twice. The installer intentionally omits that redundant field; a second probe then emitted exactly one resource parameter.
|
|
39
|
+
|
|
40
|
+
## Live protocol finding
|
|
41
|
+
|
|
42
|
+
An unauthenticated safe `initialize` probe to `https://mcp.memoryrouter.ai/mcp` returned `protocolVersion: 2025-11-25`. Codex 0.146 is dual-era and currently accepts it. The hosted server is not yet MCP `2026-07-28` compliant; this package reports rather than conceals that limitation.
|
|
43
|
+
|
|
44
|
+
## Local fallback acceptance
|
|
45
|
+
|
|
46
|
+
The generated API-key stdio configuration was parsed by Codex 0.146. `codex mcp list/get` reported the expected `node .../mcp-stdio.cjs` transport. Direct stdio initialization and `tools/list` returned all four annotated tools.
|
|
47
|
+
|
|
48
|
+
## Validation commands
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
cd integrations/codex
|
|
52
|
+
npm test
|
|
53
|
+
npm run typecheck
|
|
54
|
+
npm pack --dry-run
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The integration suite covers config migration/backup, foreign-config preservation, corrupt config fail-closed behavior, idempotent upgrades, uninstall, project/worktree identity, project-vs-vault filtering, native-memory config coexistence, OAuth/local fallback configuration, read retries, write deduplication, hook fail-open behavior, full stable-field turn capture, context budgets, local suppression, and MCP tool annotations.
|
package/bin/cli.cjs
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const { init, uninstall, doctor } = require('../lib/installer.cjs');
|
|
6
|
+
const { INTEGRATION_VERSION } = require('../runtime/constants.cjs');
|
|
7
|
+
|
|
8
|
+
function parseArgs(argv) {
|
|
9
|
+
const result = { _: [] };
|
|
10
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
11
|
+
const arg = argv[i];
|
|
12
|
+
if (!arg.startsWith('--')) { result._.push(arg); continue; }
|
|
13
|
+
const [rawKey, inline] = arg.slice(2).split('=', 2);
|
|
14
|
+
const key = rawKey.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
|
|
15
|
+
if (rawKey.startsWith('no-')) {
|
|
16
|
+
const positive = rawKey.slice(3).replace(/-([a-z])/g, (_, char) => char.toUpperCase());
|
|
17
|
+
result[positive] = false;
|
|
18
|
+
} else if (inline !== undefined) result[key] = inline;
|
|
19
|
+
else if (argv[i + 1] && !argv[i + 1].startsWith('--')) result[key] = argv[++i];
|
|
20
|
+
else result[key] = true;
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
|
|
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 mcp-server\n\nAuth modes:\n oauth Current remote MCP at ${'https://mcp.memoryrouter.ai/mcp'} (default).\n Run \`codex mcp login memoryrouter\` after init. Hooks additionally\n require MEMORYROUTER_API_KEY or a key saved with --api-key-stdin.\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
|
+
}
|
|
28
|
+
|
|
29
|
+
function optionsFromArgs(args) {
|
|
30
|
+
const options = {
|
|
31
|
+
scope: args.scope || 'user',
|
|
32
|
+
auth: args.auth || 'oauth',
|
|
33
|
+
projectDir: args.projectDir,
|
|
34
|
+
hooks: args.hooks,
|
|
35
|
+
agents: args.agents,
|
|
36
|
+
force: Boolean(args.force),
|
|
37
|
+
offline: Boolean(args.offline),
|
|
38
|
+
};
|
|
39
|
+
if (args.apiKeyStdin) {
|
|
40
|
+
options.apiKey = fs.readFileSync(0, 'utf8').trim();
|
|
41
|
+
if (!options.apiKey) throw new Error('--api-key-stdin received an empty value');
|
|
42
|
+
} else if (args.apiKey) {
|
|
43
|
+
process.stderr.write('Warning: --api-key can remain in shell history. Prefer --api-key-stdin or MEMORYROUTER_API_KEY.\n');
|
|
44
|
+
options.apiKey = String(args.apiKey);
|
|
45
|
+
}
|
|
46
|
+
return options;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function main() {
|
|
50
|
+
const args = parseArgs(process.argv.slice(2));
|
|
51
|
+
const command = args._[0];
|
|
52
|
+
if (!command || command === 'help' || args.help) {
|
|
53
|
+
process.stdout.write(usage());
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (command === 'version' || args.version) {
|
|
57
|
+
process.stdout.write(`${INTEGRATION_VERSION}\n`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (command === 'hook') {
|
|
61
|
+
process.argv = [process.argv[0], process.argv[1], args._[1]];
|
|
62
|
+
await require('../runtime/hook.cjs').main();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (command === 'mcp-server') {
|
|
66
|
+
await require('../runtime/mcp-stdio.cjs').main();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const options = optionsFromArgs(args);
|
|
70
|
+
if (command === 'init') {
|
|
71
|
+
const result = init(options);
|
|
72
|
+
process.stdout.write(`MemoryRouter Codex ${INTEGRATION_VERSION} installed (${result.layout.scope}, ${result.auth}).\n`);
|
|
73
|
+
process.stdout.write(` config: ${result.layout.configFile}\n`);
|
|
74
|
+
if (options.hooks !== false) process.stdout.write(` hooks: ${result.layout.hooksFile}\n`);
|
|
75
|
+
if (options.agents !== false) process.stdout.write(` guide: ${result.layout.agentsFile}\n`);
|
|
76
|
+
if (result.auth === 'oauth') process.stdout.write('\nNext: codex mcp login memoryrouter\nThen open Codex and run /mcp verbose and /hooks.\n');
|
|
77
|
+
if (!result.automaticMemory) 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');
|
|
78
|
+
process.stdout.write('\nRun: memoryrouter-codex doctor\n');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (command === 'doctor') {
|
|
82
|
+
const result = await doctor(options);
|
|
83
|
+
for (const check of result.checks) {
|
|
84
|
+
const icon = check.level === 'pass' ? 'PASS' : check.level === 'error' ? 'FAIL' : check.level.toUpperCase();
|
|
85
|
+
process.stdout.write(`[${icon}] ${check.name}: ${check.message}\n`);
|
|
86
|
+
}
|
|
87
|
+
process.exitCode = result.ok ? 0 : 1;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (command === 'uninstall') {
|
|
91
|
+
const result = uninstall(options);
|
|
92
|
+
process.stdout.write(`Removed managed MemoryRouter Codex files from ${result.layout.scope} scope. Foreign config/hooks/instructions were preserved.\n`);
|
|
93
|
+
process.stdout.write(`Private credentials at ~/.memoryrouter/codex were retained. Delete them manually only if no other installation uses them.\n`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
throw new Error(`Unknown command: ${command}\n\n${usage()}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
main().catch(error => {
|
|
100
|
+
process.stderr.write(`memoryrouter-codex: ${error.message}\n`);
|
|
101
|
+
process.exitCode = 1;
|
|
102
|
+
});
|
package/lib/hooks.cjs
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { atomicWrite, readText } = require('./managed.cjs');
|
|
6
|
+
const { INTEGRATION_VERSION } = require('../runtime/constants.cjs');
|
|
7
|
+
|
|
8
|
+
function isOwnedHook(handler) {
|
|
9
|
+
return handler?.type === 'command' && typeof handler.command === 'string' &&
|
|
10
|
+
(/memoryrouter[\\/]runtime[\\/]hook\.cjs/.test(handler.command) || /memoryrouter-codex(?:@[^ ]+)?\s+hook\b/.test(handler.command));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function cleanOwnedHooks(document) {
|
|
14
|
+
const clone = JSON.parse(JSON.stringify(document || {}));
|
|
15
|
+
clone.hooks = clone.hooks && typeof clone.hooks === 'object' ? clone.hooks : {};
|
|
16
|
+
for (const [event, groups] of Object.entries(clone.hooks)) {
|
|
17
|
+
if (!Array.isArray(groups)) continue;
|
|
18
|
+
clone.hooks[event] = groups.map(group => ({
|
|
19
|
+
...group,
|
|
20
|
+
hooks: Array.isArray(group.hooks) ? group.hooks.filter(handler => !isOwnedHook(handler)) : [],
|
|
21
|
+
})).filter(group => group.hooks.length > 0);
|
|
22
|
+
if (clone.hooks[event].length === 0) delete clone.hooks[event];
|
|
23
|
+
}
|
|
24
|
+
return clone;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function commandFor(runtimeDir, scope) {
|
|
28
|
+
if (scope === 'project') {
|
|
29
|
+
return 'node "$(git rev-parse --show-toplevel)/.codex/memoryrouter/runtime/hook.cjs"';
|
|
30
|
+
}
|
|
31
|
+
return `node ${JSON.stringify(path.join(runtimeDir, 'hook.cjs'))}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function commandWindowsFor(runtimeDir, scope) {
|
|
35
|
+
if (scope === 'project') {
|
|
36
|
+
return `npx -y memoryrouter-codex@${INTEGRATION_VERSION} hook`;
|
|
37
|
+
}
|
|
38
|
+
return `node ${JSON.stringify(path.join(runtimeDir, 'hook.cjs'))}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function managedGroups(runtimeDir, scope) {
|
|
42
|
+
const base = commandFor(runtimeDir, scope);
|
|
43
|
+
const windows = commandWindowsFor(runtimeDir, scope);
|
|
44
|
+
const handler = (kind, timeout, statusMessage, additionalContextLimit) => ({
|
|
45
|
+
type: 'command',
|
|
46
|
+
command: `${base} ${kind}`,
|
|
47
|
+
commandWindows: `${windows} ${kind}`,
|
|
48
|
+
timeout,
|
|
49
|
+
...(statusMessage ? { statusMessage } : {}),
|
|
50
|
+
...(additionalContextLimit ? { additionalContextLimit } : {}),
|
|
51
|
+
});
|
|
52
|
+
return {
|
|
53
|
+
SessionStart: [{ matcher: 'startup|resume|clear|compact', hooks: [handler('session-start', 8, 'Recalling MemoryRouter context', 3500)] }],
|
|
54
|
+
UserPromptSubmit: [{ hooks: [handler('inject', 8, 'Searching project memory', 3500)] }],
|
|
55
|
+
PostToolUse: [{ hooks: [handler('trace', 3)] }],
|
|
56
|
+
Stop: [{ hooks: [handler('capture', 18, 'Saving durable project memory')] }],
|
|
57
|
+
SubagentStop: [{ hooks: [handler('capture-subagent', 15, 'Saving durable subagent outcome')] }],
|
|
58
|
+
PreCompact: [{ matcher: 'manual|auto', hooks: [handler('flush', 18, 'Saving memory before compaction')] }],
|
|
59
|
+
SessionEnd: [{ hooks: [handler('flush', 3)] }],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseHooks(file) {
|
|
64
|
+
const raw = readText(file);
|
|
65
|
+
if (!raw.trim()) return { description: 'Lifecycle hooks for this Codex configuration.', hooks: {} };
|
|
66
|
+
try {
|
|
67
|
+
const parsed = JSON.parse(raw);
|
|
68
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('root must be an object');
|
|
69
|
+
return parsed;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
throw new Error(`Cannot update invalid hooks JSON at ${file}: ${error.message}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function installHooks(file, runtimeDir, scope) {
|
|
76
|
+
const current = parseHooks(file);
|
|
77
|
+
const clean = cleanOwnedHooks(current);
|
|
78
|
+
clean.description = clean.description || 'Lifecycle hooks for this Codex configuration.';
|
|
79
|
+
clean.hooks = clean.hooks || {};
|
|
80
|
+
for (const [event, groups] of Object.entries(managedGroups(runtimeDir, scope))) {
|
|
81
|
+
clean.hooks[event] = [...(clean.hooks[event] || []), ...groups];
|
|
82
|
+
}
|
|
83
|
+
const rendered = `${JSON.stringify(clean, null, 2)}\n`;
|
|
84
|
+
const previous = readText(file);
|
|
85
|
+
atomicWrite(file, rendered);
|
|
86
|
+
return { changed: rendered !== previous, file, events: Object.keys(managedGroups(runtimeDir, scope)) };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function uninstallHooks(file) {
|
|
90
|
+
if (!fs.existsSync(file)) return { changed: false, file };
|
|
91
|
+
const current = parseHooks(file);
|
|
92
|
+
const clean = cleanOwnedHooks(current);
|
|
93
|
+
const changed = JSON.stringify(clean) !== JSON.stringify(current);
|
|
94
|
+
if (!changed) return { changed: false, file };
|
|
95
|
+
if (Object.keys(clean.hooks || {}).length === 0 && (!clean.description || clean.description === 'Lifecycle hooks for this Codex configuration.')) {
|
|
96
|
+
fs.rmSync(file, { force: true });
|
|
97
|
+
} else {
|
|
98
|
+
atomicWrite(file, `${JSON.stringify(clean, null, 2)}\n`);
|
|
99
|
+
}
|
|
100
|
+
return { changed: true, file };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = { isOwnedHook, cleanOwnedHooks, commandFor, managedGroups, parseHooks, installHooks, uninstallHooks };
|