@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 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.
@@ -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 = process.env.HOME || "";
130
+ function configCandidates(env = process.env) {
131
+ const home = envValue(env, "HOME") || "";
77
132
  const openclawConfigPath =
78
- process.env.OPENCLAW_ENGRAM_CONFIG_PATH ||
79
- process.env.OPENCLAW_CONFIG_PATH ||
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
- process.env.REMNIC_CONFIG,
83
- openclawConfigPath,
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
- ].filter((p) => typeof p === "string" && p.length > 0);
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 `resolveRemnicPluginEntry` from
94
- * `@remnic/core` so the slot PLUGIN_ID LEGACY_PLUGIN_ID logic lives
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 - resolveRemnicPluginEntry from @remnic/core
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
- const raw = JSON.parse(fs.readFileSync(candidate, "utf-8"));
104
- if (!raw || typeof raw !== "object") continue;
105
- // Try the structured OpenClaw config layout first (plugins.entries).
106
- // resolveEntry returns the full plugin entry (including .config).
107
- const entry = resolveEntry(raw);
108
- if (entry && typeof entry === "object") {
109
- return entry.config && typeof entry.config === "object"
110
- ? entry.config
111
- : entry;
112
- }
113
- // Legacy / developer config layout: the top-level object IS the config.
114
- // Honour it as long as it has no `plugins` subtree (so we don't
115
- // accidentally treat a complete OpenClaw config with an unknown plugin
116
- // slot as a flat Remnic config).
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, resolveRemnicPluginEntry } = core;
238
+ const { parseConfig, runCodexMaterialize, resolvePluginEntry } = core;
156
239
  if (
157
240
  typeof parseConfig !== "function" ||
158
241
  typeof runCodexMaterialize !== "function" ||
159
- typeof resolveRemnicPluginEntry !== "function"
242
+ typeof resolvePluginEntry !== "function"
160
243
  ) {
161
244
  throw new Error(
162
- "codex-materialize: @remnic/core is missing expected exports (parseConfig, runCodexMaterialize, resolveRemnicPluginEntry)",
245
+ "codex-materialize: @remnic/core is missing expected exports (parseConfig, runCodexMaterialize, resolvePluginEntry)",
163
246
  );
164
247
  }
165
248
 
166
- // Pass the shared resolver so loadRawConfig uses the same slot → id lookup
167
- // logic as all other config-loader sites (#403).
168
- const rawConfig = loadRawConfig(resolveRemnicPluginEntry);
169
- const config = parseConfig(rawConfig);
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
- main().then(
205
- (code) => process.exit(code),
206
- (error) => {
207
- console.error(
208
- `codex-materialize failed: ${error instanceof Error ? error.message : String(error)}`,
209
- );
210
- process.exit(1);
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
- log "session=$SESSION_ID project=$PROJECT_NAME"
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 "process.stdout.write(JSON.stringify({
87
- query: process.argv[1],
88
- sessionKey: process.argv[2],
89
- topK: 12,
90
- mode: 'auto'
91
- }))" "$QUERY" "$SESSION_ID" 2>/dev/null)"
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 "process.stdout.write(JSON.stringify({
109
- query: process.argv[1],
110
- sessionKey: process.argv[2],
111
- topK: 8,
112
- mode: 'minimal'
113
- }))" "$QUERY" "$SESSION_ID" 2>/dev/null)"
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": "1.0.0",
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": "^1.0.3"
33
+ "@remnic/core": "^9.3.515"
34
34
  }
35
35
  }