@remnic/plugin-codex 1.0.1 → 9.3.517

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 CHANGED
@@ -14,7 +14,7 @@ Three discrete steps. None is automated end-to-end today; each writes to a diffe
14
14
 
15
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
16
 
17
- 2. **Add Remnic as an MCP server in `~/.codex/config.toml`.** Paste the TOML block from the "MCP setup" section below, replacing `{{REMNIC_TOKEN}}` with the token from step 1. Without this step Codex has no way to talk to the Remnic daemon.
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
18
 
19
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
20
 
@@ -57,7 +57,7 @@ bearer_token_env_var = "REMNIC_AUTH_TOKEN"
57
57
  http_headers = { "X-Engram-Client-Id" = "codex" }
58
58
  ```
59
59
 
60
- Then export the token Codex looks up. `remnic token generate` prints a multi-line status block (not just the raw token), so either:
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
61
 
62
62
  Copy the `Token:` value from:
63
63
 
@@ -84,10 +84,11 @@ See `docs/integration/connector-setup.md` in the Remnic repo for the canonical s
84
84
 
85
85
  ## Agent note
86
86
 
87
- If you're an AI agent scaffolding a Codex integration: **do not** hand-edit `~/.codex/` directly. The full setup has two components:
87
+ If you're an AI agent scaffolding a Codex integration: **do not** hand-edit `~/.codex/` directly. The full setup has three components:
88
88
 
89
- 1. `remnic connectors install codex-cli` (drives `@remnic/core`'s `installCodexMemoryExtension`) handles the MCP config, token rotation, and writes `memories_extensions/remnic/instructions.md`. It does NOT deploy `.codex-plugin/`, `hooks/`, or `skills/`.
90
- 2. Load this package into Codex via Codex's own plugin loader to activate the hooks and skills.
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.
91
92
 
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.
93
94
 
@@ -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.1",
3
+ "version": "9.3.517",
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.517"
34
34
  }
35
35
  }