@remnic/plugin-codex 1.0.0 → 9.3.515
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 +105 -0
- package/bin/materialize.cjs +153 -50
- package/hooks/bin/session-start.sh +119 -13
- package/package.json +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# @remnic/plugin-codex
|
|
2
|
+
|
|
3
|
+
Native [OpenAI Codex CLI](https://github.com/openai/codex) plugin for [Remnic](https://github.com/joshuaswarren/remnic) memory. Wires Codex's session hooks, MCP server, skills, and memory-extension into a running Remnic daemon so every Codex session gets persistent long-term memory automatically.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Three discrete steps. None is automated end-to-end today; each writes to a different place.
|
|
8
|
+
|
|
9
|
+
1. **Mint a Remnic-side bearer token, record the connector, and install the phase-2 consolidation guide.**
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
remnic connectors install codex-cli
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
This writes `~/.remnic/connectors/codex-cli.json` (Remnic's connector-state file), stores a bearer token, and calls `@remnic/core`'s `installCodexMemoryExtension` which materializes `~/.codex/memories_extensions/remnic/instructions.md` (the local-only phase-2 consolidation guide; see the file-table row below). It does NOT write `~/.codex/config.toml` and it does NOT deploy `.codex-plugin/`, `hooks/`, or `skills/`.
|
|
16
|
+
|
|
17
|
+
2. **Add Remnic as an MCP server in `~/.codex/config.toml`.** Paste the TOML block from the "MCP setup" section below unchanged, then set `REMNIC_AUTH_TOKEN` in Codex's environment to the bearer token generated in step 1. Without this step Codex has no way to talk to the Remnic daemon.
|
|
18
|
+
|
|
19
|
+
3. **Install this package and load it through Codex's plugin system** so the hooks, skills, and `.codex-plugin` manifest are actually active:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install -g @remnic/plugin-codex
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Consult Codex's plugin docs for the exact load mechanism your install supports (symlink into `~/.codex/plugins/`, marketplace install, etc.). Until this step runs, the session hooks and skills aren't active and you won't get auto-recall / auto-observe.
|
|
26
|
+
|
|
27
|
+
## What ships
|
|
28
|
+
|
|
29
|
+
The package is **data + one small runtime materializer** (no runtime JS beyond the memory-materializer helper; the actual plugin install is driven by `@remnic/core`):
|
|
30
|
+
|
|
31
|
+
| File / dir | Purpose |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `.codex-plugin/plugin.json` | Plugin manifest |
|
|
34
|
+
| `hooks/hooks.json` + `hooks/bin/*.sh` | Codex session-lifecycle hooks (recall, observe, session-end) |
|
|
35
|
+
| `skills/` | `remnic-recall`, `remnic-remember`, `remnic-search`, `remnic-status`, `remnic-entities`, `remnic-memory-workflow` — invocable from Codex chats |
|
|
36
|
+
| `memories_extensions/remnic/` | Codex phase-2 consolidation instructions — tells the Codex compactor sub-agent to treat Remnic's on-disk Markdown as an authoritative local memory source when it builds `MEMORY.md`. Local-only (no MCP, no network); runtime recall/observe still flow through the hooks above. |
|
|
37
|
+
| `.mcp.json` | MCP server config pointing Codex at `http://localhost:4318/mcp` |
|
|
38
|
+
| `bin/materialize.cjs` | Runtime entrypoint invoked exclusively by the Codex `Stop` hook (`hooks/bin/session-end.sh`) to refresh `~/.codex/memories` from the Remnic store at the end of a session. Not an installer, and not wired into any `remnic` CLI command. |
|
|
39
|
+
|
|
40
|
+
## What you get at runtime
|
|
41
|
+
|
|
42
|
+
Once installed and a Remnic daemon is running (`remnic daemon start`):
|
|
43
|
+
|
|
44
|
+
- **Auto-recall** on `SessionStart` and on every `UserPromptSubmit` — relevant memories are injected before Codex's first turn and before each subsequent user turn.
|
|
45
|
+
- **Auto-observe** on `PostToolUse` for the `Bash` tool and on `Stop` (session end) — new facts, decisions, and entities touched by shell work (or accumulated through the session) are buffered for extraction automatically.
|
|
46
|
+
- **Memory skills** — invoke `/remnic-recall`, `/remnic-search`, `/remnic-remember`, `/remnic-entities`, `/remnic-status` directly in Codex chats.
|
|
47
|
+
- **Cross-agent sharing** — the same memory store is shared with every other Remnic-connected agent (Claude Code, OpenClaw, Replit, Hermes, etc.), so what one agent learns is available to all.
|
|
48
|
+
|
|
49
|
+
## MCP setup
|
|
50
|
+
|
|
51
|
+
The plugin expects a Remnic daemon reachable at `http://localhost:4318/mcp` with a bearer token. Codex reads MCP servers from `~/.codex/config.toml`; add the following block (this is step 2 of the Install flow above — `remnic connectors install codex-cli` does NOT write it for you):
|
|
52
|
+
|
|
53
|
+
```toml
|
|
54
|
+
[mcp_servers.remnic]
|
|
55
|
+
url = "http://127.0.0.1:4318/mcp"
|
|
56
|
+
bearer_token_env_var = "REMNIC_AUTH_TOKEN"
|
|
57
|
+
http_headers = { "X-Engram-Client-Id" = "codex" }
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Then export the token Codex looks up. Use the bearer token printed by `remnic connectors install codex-cli`. If you need to mint a replacement, `remnic token generate` prints a multi-line status block (not just the raw token), so either:
|
|
61
|
+
|
|
62
|
+
Copy the `Token:` value from:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
remnic token generate codex-cli
|
|
66
|
+
# → Generated token for codex-cli:
|
|
67
|
+
# Token: <paste this into REMNIC_AUTH_TOKEN>
|
|
68
|
+
# Created: ...
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
and set it in your shell profile:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
export REMNIC_AUTH_TOKEN="<paste the token value here>"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Or extract it in one line with `awk`:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
export REMNIC_AUTH_TOKEN=$(remnic token generate codex-cli | awk '/^ *Token:/ {print $2}')
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
See `docs/integration/connector-setup.md` in the Remnic repo for the canonical snippet.
|
|
84
|
+
|
|
85
|
+
## Agent note
|
|
86
|
+
|
|
87
|
+
If you're an AI agent scaffolding a Codex integration: **do not** hand-edit `~/.codex/` directly. The full setup has three components:
|
|
88
|
+
|
|
89
|
+
1. `remnic connectors install codex-cli` mints/stores the Remnic bearer token, records connector state, and drives `@remnic/core`'s `installCodexMemoryExtension` to write `memories_extensions/remnic/instructions.md`. It does NOT write `~/.codex/config.toml` and does NOT deploy `.codex-plugin/`, `hooks/`, or `skills/`.
|
|
90
|
+
2. Add the MCP TOML block above unchanged and expose the generated token as `REMNIC_AUTH_TOKEN`.
|
|
91
|
+
3. Load this package into Codex via Codex's own plugin loader to activate the hooks and skills.
|
|
92
|
+
|
|
93
|
+
`bin/materialize.cjs` is a runtime helper called only by the Codex `Stop` hook to refresh `~/.codex/memories` from the live Remnic store at session end; it's not an installer and not wired into any `remnic` CLI command, so re-running it manually won't recover a broken plugin install.
|
|
94
|
+
|
|
95
|
+
## Related
|
|
96
|
+
|
|
97
|
+
- [`@remnic/cli`](https://www.npmjs.com/package/@remnic/cli) — daemon lifecycle + installer
|
|
98
|
+
- [`@remnic/plugin-claude-code`](https://www.npmjs.com/package/@remnic/plugin-claude-code) — same idea, for Anthropic Claude Code
|
|
99
|
+
- [`@remnic/plugin-openclaw`](https://www.npmjs.com/package/@remnic/plugin-openclaw) — OpenClaw memory-slot plugin
|
|
100
|
+
- Connector guide: [docs/integration/connector-setup.md](https://github.com/joshuaswarren/remnic/blob/main/docs/integration/connector-setup.md) in the repo
|
|
101
|
+
- Source + issues: <https://github.com/joshuaswarren/remnic>
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
MIT. See the root [LICENSE](https://github.com/joshuaswarren/remnic/blob/main/LICENSE) file.
|
package/bin/materialize.cjs
CHANGED
|
@@ -68,58 +68,141 @@ function parseArgs(argv) {
|
|
|
68
68
|
return args;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
function isPlainRecord(value) {
|
|
72
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const OPENCLAW_REMNIC_PLUGIN_IDS = ["openclaw-remnic", "openclaw-engram"];
|
|
76
|
+
|
|
77
|
+
function getOpenClawPluginEntries(raw) {
|
|
78
|
+
const plugins = isPlainRecord(raw.plugins) ? raw.plugins : undefined;
|
|
79
|
+
return plugins && isPlainRecord(plugins.entries) ? plugins.entries : undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function getOpenClawMemorySlotId(raw) {
|
|
83
|
+
const plugins = isPlainRecord(raw.plugins) ? raw.plugins : undefined;
|
|
84
|
+
const slots = plugins && isPlainRecord(plugins.slots) ? plugins.slots : undefined;
|
|
85
|
+
return typeof slots?.memory === "string" ? slots.memory : undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function resolveOpenClawRemnicPluginEntry(raw, resolveEntry) {
|
|
89
|
+
return resolveEntry(raw, {
|
|
90
|
+
candidateIds: OPENCLAW_REMNIC_PLUGIN_IDS,
|
|
91
|
+
getEntries: getOpenClawPluginEntries,
|
|
92
|
+
getSlotId: getOpenClawMemorySlotId,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
class MaterializeConfigError extends Error {
|
|
97
|
+
constructor(message) {
|
|
98
|
+
super(message);
|
|
99
|
+
this.name = "MaterializeConfigError";
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function envValue(env, key) {
|
|
104
|
+
const value = env[key];
|
|
105
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function expandConfigPathForEnv(filePath, home) {
|
|
109
|
+
if (filePath === undefined) return undefined;
|
|
110
|
+
if (filePath === "~") return home.length > 0 ? home : filePath;
|
|
111
|
+
if (filePath.startsWith("~/") || filePath.startsWith("~\\")) {
|
|
112
|
+
return home.length > 0 ? path.join(home, filePath.slice(2)) : filePath;
|
|
113
|
+
}
|
|
114
|
+
return filePath;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function safeErrorDetail(error) {
|
|
118
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
119
|
+
const cleaned = raw
|
|
120
|
+
.replace(/[\r\n\t]+/g, " ")
|
|
121
|
+
.replace(/[^\w .,:;()[\]{}'"!?/@+-]/g, "?")
|
|
122
|
+
.trim();
|
|
123
|
+
return cleaned.length > 240 ? `${cleaned.slice(0, 237)}...` : cleaned;
|
|
124
|
+
}
|
|
125
|
+
|
|
71
126
|
/**
|
|
72
127
|
* Return candidate config file paths to search, in priority order.
|
|
73
128
|
* The caller is responsible for parsing and entry-resolution.
|
|
74
129
|
*/
|
|
75
|
-
function configCandidates() {
|
|
76
|
-
const home =
|
|
130
|
+
function configCandidates(env = process.env) {
|
|
131
|
+
const home = envValue(env, "HOME") || "";
|
|
77
132
|
const openclawConfigPath =
|
|
78
|
-
|
|
79
|
-
|
|
133
|
+
envValue(env, "OPENCLAW_CONFIG_PATH") ||
|
|
134
|
+
envValue(env, "OPENCLAW_ENGRAM_CONFIG_PATH") ||
|
|
80
135
|
path.join(home, ".openclaw", "openclaw.json");
|
|
81
136
|
return [
|
|
82
|
-
|
|
83
|
-
|
|
137
|
+
{
|
|
138
|
+
path: expandConfigPathForEnv(envValue(env, "REMNIC_CONFIG"), home),
|
|
139
|
+
label: "REMNIC_CONFIG",
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
path: expandConfigPathForEnv(openclawConfigPath, home),
|
|
143
|
+
label:
|
|
144
|
+
envValue(env, "OPENCLAW_CONFIG_PATH") !== undefined
|
|
145
|
+
? "OPENCLAW_CONFIG_PATH"
|
|
146
|
+
: envValue(env, "OPENCLAW_ENGRAM_CONFIG_PATH") !== undefined
|
|
147
|
+
? "OPENCLAW_ENGRAM_CONFIG_PATH"
|
|
148
|
+
: "default OpenClaw config",
|
|
149
|
+
},
|
|
84
150
|
path.join(home, ".config", "remnic", "config.json"),
|
|
85
151
|
path.join(home, ".config", "engram", "config.json"),
|
|
86
152
|
path.join(home, ".remnic", "config.json"),
|
|
87
|
-
]
|
|
153
|
+
]
|
|
154
|
+
.map((candidate) => {
|
|
155
|
+
return typeof candidate === "string"
|
|
156
|
+
? { path: candidate, label: candidate }
|
|
157
|
+
: candidate;
|
|
158
|
+
})
|
|
159
|
+
.filter(
|
|
160
|
+
(candidate) =>
|
|
161
|
+
typeof candidate.path === "string" && candidate.path.length > 0,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function extractRemnicConfigFromRaw(raw, resolveEntry) {
|
|
166
|
+
const entry = resolveOpenClawRemnicPluginEntry(raw, resolveEntry);
|
|
167
|
+
if (isPlainRecord(entry)) {
|
|
168
|
+
return isPlainRecord(entry.config) ? entry.config : entry;
|
|
169
|
+
}
|
|
170
|
+
// Legacy / developer config layout: the top-level object IS the config.
|
|
171
|
+
// Honour it only when the file is not an OpenClaw-shaped config.
|
|
172
|
+
if (!Object.prototype.hasOwnProperty.call(raw, "plugins")) {
|
|
173
|
+
return raw;
|
|
174
|
+
}
|
|
175
|
+
return undefined;
|
|
88
176
|
}
|
|
89
177
|
|
|
90
178
|
/**
|
|
91
179
|
* Load the Remnic plugin config block from the first matching config file.
|
|
92
180
|
*
|
|
93
|
-
* Entry resolution is delegated to
|
|
94
|
-
* `@remnic/core`
|
|
95
|
-
* in exactly one place across all five config-loader sites (#403).
|
|
181
|
+
* Entry resolution is delegated to the generic plugin-entry resolver from
|
|
182
|
+
* `@remnic/core` with OpenClaw ids supplied by this adapter wrapper.
|
|
96
183
|
*
|
|
97
|
-
* @param {Function} resolveEntry -
|
|
184
|
+
* @param {Function} resolveEntry - resolvePluginEntry from @remnic/core
|
|
185
|
+
* @param {NodeJS.ProcessEnv} env - environment override for tests
|
|
98
186
|
*/
|
|
99
|
-
function loadRawConfig(resolveEntry) {
|
|
100
|
-
for (const candidate of configCandidates()) {
|
|
101
|
-
if (!fs.existsSync(candidate)) continue;
|
|
187
|
+
function loadRawConfig(resolveEntry, env = process.env) {
|
|
188
|
+
for (const candidate of configCandidates(env)) {
|
|
189
|
+
if (!fs.existsSync(candidate.path)) continue;
|
|
190
|
+
let raw;
|
|
102
191
|
try {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (!raw.plugins) {
|
|
118
|
-
return raw;
|
|
119
|
-
}
|
|
120
|
-
// OpenClaw config but no Remnic entry found — skip to next candidate.
|
|
121
|
-
} catch (_err) {
|
|
122
|
-
// fall through to next candidate
|
|
192
|
+
raw = JSON.parse(fs.readFileSync(candidate.path, "utf-8"));
|
|
193
|
+
} catch (err) {
|
|
194
|
+
throw new MaterializeConfigError(
|
|
195
|
+
`codex-materialize config error: invalid JSON in ${candidate.label} (${candidate.path}): ${safeErrorDetail(err)}`,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
if (!isPlainRecord(raw)) {
|
|
199
|
+
throw new MaterializeConfigError(
|
|
200
|
+
`codex-materialize config error: invalid config in ${candidate.label} (${candidate.path}): expected a JSON object`,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
const resolved = extractRemnicConfigFromRaw(raw, resolveEntry);
|
|
204
|
+
if (resolved) {
|
|
205
|
+
return resolved;
|
|
123
206
|
}
|
|
124
207
|
}
|
|
125
208
|
return {};
|
|
@@ -152,21 +235,28 @@ async function main() {
|
|
|
152
235
|
|
|
153
236
|
// Dynamic import because @remnic/core is ESM-only.
|
|
154
237
|
const core = await import("@remnic/core");
|
|
155
|
-
const { parseConfig, runCodexMaterialize,
|
|
238
|
+
const { parseConfig, runCodexMaterialize, resolvePluginEntry } = core;
|
|
156
239
|
if (
|
|
157
240
|
typeof parseConfig !== "function" ||
|
|
158
241
|
typeof runCodexMaterialize !== "function" ||
|
|
159
|
-
typeof
|
|
242
|
+
typeof resolvePluginEntry !== "function"
|
|
160
243
|
) {
|
|
161
244
|
throw new Error(
|
|
162
|
-
"codex-materialize: @remnic/core is missing expected exports (parseConfig, runCodexMaterialize,
|
|
245
|
+
"codex-materialize: @remnic/core is missing expected exports (parseConfig, runCodexMaterialize, resolvePluginEntry)",
|
|
163
246
|
);
|
|
164
247
|
}
|
|
165
248
|
|
|
166
|
-
// Pass the
|
|
167
|
-
//
|
|
168
|
-
const rawConfig = loadRawConfig(
|
|
169
|
-
|
|
249
|
+
// Pass the generic resolver so loadRawConfig uses the same slot → id lookup
|
|
250
|
+
// semantics as the other OpenClaw config-loader sites (#403).
|
|
251
|
+
const rawConfig = loadRawConfig(resolvePluginEntry);
|
|
252
|
+
let config;
|
|
253
|
+
try {
|
|
254
|
+
config = parseConfig(rawConfig);
|
|
255
|
+
} catch (err) {
|
|
256
|
+
throw new MaterializeConfigError(
|
|
257
|
+
`codex-materialize config error: parseConfig rejected the resolved config: ${safeErrorDetail(err)}`,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
170
260
|
if (args.memoryDir) {
|
|
171
261
|
// parseConfig already locked in a memoryDir, but the CLI override wins.
|
|
172
262
|
config.memoryDir = args.memoryDir;
|
|
@@ -201,12 +291,25 @@ async function main() {
|
|
|
201
291
|
return 0;
|
|
202
292
|
}
|
|
203
293
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
294
|
+
module.exports = {
|
|
295
|
+
configCandidates,
|
|
296
|
+
extractRemnicConfigFromRaw,
|
|
297
|
+
loadRawConfig,
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
function formatFatalError(error) {
|
|
301
|
+
if (error instanceof MaterializeConfigError) {
|
|
302
|
+
return error.message;
|
|
303
|
+
}
|
|
304
|
+
return "codex-materialize failed; see logs for details";
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (require.main === module) {
|
|
308
|
+
main().then(
|
|
309
|
+
(code) => process.exit(code),
|
|
310
|
+
(error) => {
|
|
311
|
+
console.error(formatFatalError(error));
|
|
312
|
+
process.exit(1);
|
|
313
|
+
},
|
|
314
|
+
);
|
|
315
|
+
}
|
|
@@ -57,7 +57,85 @@ SESSION_ID="$(node -e "const d=JSON.parse(process.argv[1]); process.stdout.write
|
|
|
57
57
|
CWD="$(node -e "const d=JSON.parse(process.argv[1]); process.stdout.write(d.cwd||'')" "$INPUT" 2>/dev/null || echo "")"
|
|
58
58
|
PROJECT_NAME="$(basename "$CWD" 2>/dev/null || echo "unknown")"
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
# Resolve git context for the session's cwd (issue #569 PR 6).
|
|
61
|
+
# Produces either a JSON `codingContext` object to embed in the recall
|
|
62
|
+
# request, or an empty string when the cwd is not inside a git repo.
|
|
63
|
+
# Any failure silently drops back to no-context (CLAUDE.md #30 escape hatch).
|
|
64
|
+
CODING_CONTEXT_JSON=""
|
|
65
|
+
if [ -n "$CWD" ] && [ -d "$CWD" ] && command -v git >/dev/null 2>&1; then
|
|
66
|
+
REMNIC_GIT_TOP="$(git -C "$CWD" rev-parse --show-toplevel 2>/dev/null || echo "")"
|
|
67
|
+
if [ -n "$REMNIC_GIT_TOP" ]; then
|
|
68
|
+
REMNIC_GIT_BRANCH="$(git -C "$REMNIC_GIT_TOP" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "HEAD")"
|
|
69
|
+
[ "$REMNIC_GIT_BRANCH" = "HEAD" ] && REMNIC_GIT_BRANCH=""
|
|
70
|
+
REMNIC_GIT_ORIGIN="$(git -C "$REMNIC_GIT_TOP" remote get-url origin 2>/dev/null || echo "")"
|
|
71
|
+
REMNIC_GIT_DEFAULT_BRANCH="$(git -C "$REMNIC_GIT_TOP" symbolic-ref --quiet refs/remotes/origin/HEAD 2>/dev/null | sed 's|^refs/remotes/origin/||' || echo "")"
|
|
72
|
+
CODING_CONTEXT_JSON="$(REMNIC_GIT_TOP="$REMNIC_GIT_TOP" REMNIC_GIT_BRANCH="$REMNIC_GIT_BRANCH" REMNIC_GIT_ORIGIN="$REMNIC_GIT_ORIGIN" REMNIC_GIT_DEFAULT_BRANCH="$REMNIC_GIT_DEFAULT_BRANCH" node -e "
|
|
73
|
+
// Mirror @remnic/core's resolveGitContext for projectId derivation.
|
|
74
|
+
// FNV-1a 32-bit stable hash.
|
|
75
|
+
const rootPath = process.env.REMNIC_GIT_TOP || '';
|
|
76
|
+
const branch = process.env.REMNIC_GIT_BRANCH || null;
|
|
77
|
+
const origin = process.env.REMNIC_GIT_ORIGIN || '';
|
|
78
|
+
const defaultBranch = process.env.REMNIC_GIT_DEFAULT_BRANCH || null;
|
|
79
|
+
function stableHash(input) {
|
|
80
|
+
let hash = 0x811c9dc5;
|
|
81
|
+
for (let i = 0; i < input.length; i++) {
|
|
82
|
+
hash ^= input.charCodeAt(i);
|
|
83
|
+
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
84
|
+
}
|
|
85
|
+
return hash.toString(16).padStart(8, '0');
|
|
86
|
+
}
|
|
87
|
+
// Mirrors packages/remnic-core/src/coding/git-context.ts
|
|
88
|
+
// normalizeOriginUrl. Keep the two in sync so the hook-computed
|
|
89
|
+
// projectId matches what the daemon computes on the same origin.
|
|
90
|
+
function normalizeOriginUrl(raw) {
|
|
91
|
+
let u = (raw || '').trim();
|
|
92
|
+
if (!u) return '';
|
|
93
|
+
// Case-insensitive .git strip — matches the TS canonical form.
|
|
94
|
+
if (/\\.git\$/i.test(u)) u = u.slice(0, -4);
|
|
95
|
+
// Windows drive-letter: short-circuit scp parsing.
|
|
96
|
+
if (/^[A-Za-z]:[\\\\/]/.test(u)) return u.toLowerCase();
|
|
97
|
+
// Protocol form: handles ssh://, https://, file:///, bracketed
|
|
98
|
+
// IPv6 hosts, optional user, optional port, and empty host
|
|
99
|
+
// (file:///path).
|
|
100
|
+
const proto = /^[a-z][a-z0-9+.-]*:\\/\\/(?:[^@/]+@)?(\\[[^\\]]+\\]|[^/:]*)(?::(\\d+))?(\\/.*)?\$/i.exec(u);
|
|
101
|
+
if (proto) {
|
|
102
|
+
let host = proto[1] || '';
|
|
103
|
+
const wasBracketed = host.startsWith('[') && host.endsWith(']');
|
|
104
|
+
if (wasBracketed) host = host.slice(1, -1);
|
|
105
|
+
const port = proto[2];
|
|
106
|
+
const p = (proto[3] || '').replace(/^\\/+/, '');
|
|
107
|
+
const hostPort = port
|
|
108
|
+
? (wasBracketed ? '[' + host + ']:' + port : host + ':' + port)
|
|
109
|
+
: host;
|
|
110
|
+
const prefix = hostPort.length > 0 ? hostPort : 'localhost';
|
|
111
|
+
return (prefix + '/' + p).toLowerCase();
|
|
112
|
+
}
|
|
113
|
+
// scp form: [user@]host:path — user@ optional, bracketed IPv6 host
|
|
114
|
+
// supported. A matched path starting with // is a protocol-URL
|
|
115
|
+
// leftover and is rejected.
|
|
116
|
+
const scp = /^(?:([^@\\s\\/]+)@)?(\\[[^\\]]+\\]|[^:@\\s\\/]+):(.+)\$/.exec(u);
|
|
117
|
+
if (scp) {
|
|
118
|
+
let host = scp[2] || '';
|
|
119
|
+
if (host.startsWith('[') && host.endsWith(']')) host = host.slice(1, -1);
|
|
120
|
+
const p = scp[3] || '';
|
|
121
|
+
if (p.startsWith('//')) return u.toLowerCase();
|
|
122
|
+
return (host + '/' + p.replace(/^\\/+/, '')).toLowerCase();
|
|
123
|
+
}
|
|
124
|
+
return u.toLowerCase();
|
|
125
|
+
}
|
|
126
|
+
const normalized = normalizeOriginUrl(origin);
|
|
127
|
+
const projectId = normalized ? 'origin:' + stableHash(normalized) : 'root:' + stableHash(rootPath);
|
|
128
|
+
process.stdout.write(JSON.stringify({
|
|
129
|
+
projectId,
|
|
130
|
+
branch: branch || null,
|
|
131
|
+
rootPath,
|
|
132
|
+
defaultBranch: defaultBranch || null,
|
|
133
|
+
}));
|
|
134
|
+
" 2>/dev/null || echo "")"
|
|
135
|
+
fi
|
|
136
|
+
fi
|
|
137
|
+
|
|
138
|
+
log "session=$SESSION_ID project=$PROJECT_NAME coding-context=${CODING_CONTEXT_JSON:+yes}"
|
|
61
139
|
|
|
62
140
|
# Health check — start daemon if not running
|
|
63
141
|
if ! curl -sf --max-time 2 "$REMNIC_HEALTH_URL" >/dev/null 2>&1; then
|
|
@@ -83,12 +161,29 @@ fi
|
|
|
83
161
|
|
|
84
162
|
QUERY="Starting a new coding session in project: ${PROJECT_NAME}. Recall relevant memories, preferences, decisions, patterns, and context about this project and the user."
|
|
85
163
|
|
|
86
|
-
REQUEST_BODY="$(node -e "
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
164
|
+
REQUEST_BODY="$(REMNIC_CODING_CONTEXT_JSON="$CODING_CONTEXT_JSON" node -e "
|
|
165
|
+
const body = {
|
|
166
|
+
query: process.argv[1],
|
|
167
|
+
sessionKey: process.argv[2],
|
|
168
|
+
topK: 12,
|
|
169
|
+
mode: 'auto',
|
|
170
|
+
};
|
|
171
|
+
const raw = process.env.REMNIC_CODING_CONTEXT_JSON || '';
|
|
172
|
+
if (raw) {
|
|
173
|
+
try { body.codingContext = JSON.parse(raw); } catch (_) {
|
|
174
|
+
// Context envelope was provided but failed to parse. Explicitly
|
|
175
|
+
// clear any previously-attached context for this session so a
|
|
176
|
+
// malformed envelope does not silently keep stale state.
|
|
177
|
+
body.codingContext = null;
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
// No git context resolvable for this cwd. Explicitly clear any
|
|
181
|
+
// previously-attached context so a session that moves out of a repo
|
|
182
|
+
// does not keep routing to the old project namespace.
|
|
183
|
+
body.codingContext = null;
|
|
184
|
+
}
|
|
185
|
+
process.stdout.write(JSON.stringify(body));
|
|
186
|
+
" "$QUERY" "$SESSION_ID" 2>/dev/null)"
|
|
92
187
|
|
|
93
188
|
[ -z "$REQUEST_BODY" ] && echo '{"continue":true}' && exit 0
|
|
94
189
|
|
|
@@ -105,12 +200,23 @@ RESPONSE="$(echo "$RAW" | sed '$d')"
|
|
|
105
200
|
|
|
106
201
|
if [ $CURL_EXIT -ne 0 ] || ! [[ "$HTTP_STATUS" =~ ^2 ]] || [ -z "$RESPONSE" ]; then
|
|
107
202
|
log "full recall failed (curl=$CURL_EXIT http=$HTTP_STATUS) — falling back to minimal"
|
|
108
|
-
MINIMAL_BODY="$(node -e "
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
203
|
+
MINIMAL_BODY="$(REMNIC_CODING_CONTEXT_JSON="$CODING_CONTEXT_JSON" node -e "
|
|
204
|
+
const body = {
|
|
205
|
+
query: process.argv[1],
|
|
206
|
+
sessionKey: process.argv[2],
|
|
207
|
+
topK: 8,
|
|
208
|
+
mode: 'minimal',
|
|
209
|
+
};
|
|
210
|
+
const raw = process.env.REMNIC_CODING_CONTEXT_JSON || '';
|
|
211
|
+
if (raw) {
|
|
212
|
+
try { body.codingContext = JSON.parse(raw); } catch (_) {
|
|
213
|
+
body.codingContext = null;
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
body.codingContext = null;
|
|
217
|
+
}
|
|
218
|
+
process.stdout.write(JSON.stringify(body));
|
|
219
|
+
" "$QUERY" "$SESSION_ID" 2>/dev/null)"
|
|
114
220
|
RAW="$(curl -s -w "\n%{http_code}" --max-time 20 \
|
|
115
221
|
-X POST "$REMNIC_URL" \
|
|
116
222
|
-H "Authorization: Bearer ${REMNIC_TOKEN}" \
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remnic/plugin-codex",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "9.3.515",
|
|
4
4
|
"description": "Remnic memory plugin for Codex CLI — hooks, skills, MCP integration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,6 +30,6 @@
|
|
|
30
30
|
".mcp.json"
|
|
31
31
|
],
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@remnic/core": "^
|
|
33
|
+
"@remnic/core": "^9.3.515"
|
|
34
34
|
}
|
|
35
35
|
}
|