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.
@@ -0,0 +1,166 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const crypto = require('crypto');
7
+ const { API_BASE, DEFAULT_CONTEXT_CHARS, DEFAULT_SEARCH_RESULTS, RUNTIME_SCHEMA_VERSION } = require('./constants.cjs');
8
+
9
+ function codexHome(env = process.env) {
10
+ return path.resolve(env.CODEX_HOME || path.join(os.homedir(), '.codex'));
11
+ }
12
+
13
+ function dataHome(env = process.env) {
14
+ return path.resolve(env.MEMORYROUTER_CODEX_HOME || path.join(os.homedir(), '.memoryrouter', 'codex'));
15
+ }
16
+
17
+ function configPath(env = process.env) {
18
+ return path.join(dataHome(env), 'config.json');
19
+ }
20
+
21
+ function ensurePrivateDir(dir) {
22
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
23
+ try { fs.chmodSync(dir, 0o700); } catch { /* Windows/best effort */ }
24
+ }
25
+
26
+ function atomicWriteJson(file, value, mode = 0o600) {
27
+ ensurePrivateDir(path.dirname(file));
28
+ const tmp = `${file}.${process.pid}.${crypto.randomBytes(4).toString('hex')}.tmp`;
29
+ fs.writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, { mode });
30
+ try { fs.chmodSync(tmp, mode); } catch { /* best effort */ }
31
+ fs.renameSync(tmp, file);
32
+ try { fs.chmodSync(file, mode); } catch { /* best effort */ }
33
+ }
34
+
35
+ function defaultConfig() {
36
+ return {
37
+ version: RUNTIME_SCHEMA_VERSION,
38
+ apiBase: API_BASE,
39
+ contextChars: DEFAULT_CONTEXT_CHARS,
40
+ maxSearchResults: DEFAULT_SEARCH_RESULTS,
41
+ retrievalScope: 'project',
42
+ forgottenHandles: [],
43
+ };
44
+ }
45
+
46
+ function loadConfig(env = process.env) {
47
+ const file = configPath(env);
48
+ let parsed = {};
49
+ try { parsed = JSON.parse(fs.readFileSync(file, 'utf8')); } catch { /* defaults */ }
50
+ return { ...defaultConfig(), ...(parsed && typeof parsed === 'object' ? parsed : {}) };
51
+ }
52
+
53
+ function saveConfig(config, env = process.env) {
54
+ const safe = { ...defaultConfig(), ...config, version: RUNTIME_SCHEMA_VERSION };
55
+ atomicWriteJson(configPath(env), safe, 0o600);
56
+ return safe;
57
+ }
58
+
59
+ function getApiKey(env = process.env, config = loadConfig(env)) {
60
+ return env.MEMORYROUTER_API_KEY || config.apiKey || null;
61
+ }
62
+
63
+ function safeId(value) {
64
+ return crypto.createHash('sha256').update(String(value || 'unknown')).digest('hex').slice(0, 32);
65
+ }
66
+
67
+ function stateDir(env = process.env) {
68
+ const dir = path.join(dataHome(env), 'state');
69
+ ensurePrivateDir(dir);
70
+ return dir;
71
+ }
72
+
73
+ function sessionStatePath(sessionId, env = process.env) {
74
+ return path.join(stateDir(env), `${safeId(sessionId)}.json`);
75
+ }
76
+
77
+ function tracePath(sessionId, turnId, env = process.env) {
78
+ return path.join(stateDir(env), `${safeId(sessionId)}-${safeId(turnId)}.trace.jsonl`);
79
+ }
80
+
81
+ function loadSessionState(sessionId, env = process.env) {
82
+ try {
83
+ const value = JSON.parse(fs.readFileSync(sessionStatePath(sessionId, env), 'utf8'));
84
+ return value && typeof value === 'object' ? value : {};
85
+ } catch { return {}; }
86
+ }
87
+
88
+ function saveSessionState(sessionId, state, env = process.env) {
89
+ atomicWriteJson(sessionStatePath(sessionId, env), state, 0o600);
90
+ }
91
+
92
+ function pruneState(env = process.env, maxAgeMs = 30 * 24 * 60 * 60 * 1000) {
93
+ const dir = stateDir(env);
94
+ const cutoff = Date.now() - maxAgeMs;
95
+ let removed = 0;
96
+ for (const name of fs.readdirSync(dir)) {
97
+ if (!name.endsWith('.json') && !name.endsWith('.jsonl')) continue;
98
+ const file = path.join(dir, name);
99
+ try {
100
+ if (fs.statSync(file).mtimeMs < cutoff) { fs.rmSync(file, { force: true }); removed += 1; }
101
+ } catch { /* raced with another hook */ }
102
+ }
103
+ return removed;
104
+ }
105
+
106
+ function auditPath(env = process.env) {
107
+ return path.join(dataHome(env), 'audit.jsonl');
108
+ }
109
+
110
+ function recordAudit(event, details = {}, env = process.env) {
111
+ try {
112
+ const file = auditPath(env);
113
+ ensurePrivateDir(path.dirname(file));
114
+ try {
115
+ if (fs.statSync(file).size > 5 * 1024 * 1024) fs.renameSync(file, `${file}.1`);
116
+ } catch { /* new file */ }
117
+ const safe = { at: new Date().toISOString(), event, ...details };
118
+ delete safe.apiKey;
119
+ delete safe.key;
120
+ delete safe.content;
121
+ fs.appendFileSync(file, `${JSON.stringify(safe)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'a' });
122
+ } catch { /* audit logging cannot block memory or Codex */ }
123
+ }
124
+
125
+ function healthPath(env = process.env) {
126
+ return path.join(dataHome(env), 'health.json');
127
+ }
128
+
129
+ function recordHealth(event, details = {}, env = process.env) {
130
+ let health = {};
131
+ try { health = JSON.parse(fs.readFileSync(healthPath(env), 'utf8')); } catch { /* empty */ }
132
+ health[event] = { at: new Date().toISOString(), ...details };
133
+ try { atomicWriteJson(healthPath(env), health, 0o600); } catch { /* observability never blocks */ }
134
+ }
135
+
136
+ function forgetHandle(handle, env = process.env) {
137
+ const cfg = loadConfig(env);
138
+ const set = new Set(Array.isArray(cfg.forgottenHandles) ? cfg.forgottenHandles : []);
139
+ set.add(handle);
140
+ cfg.forgottenHandles = [...set].slice(-1000);
141
+ saveConfig(cfg, env);
142
+ }
143
+
144
+ module.exports = {
145
+ codexHome,
146
+ dataHome,
147
+ configPath,
148
+ ensurePrivateDir,
149
+ atomicWriteJson,
150
+ defaultConfig,
151
+ loadConfig,
152
+ saveConfig,
153
+ getApiKey,
154
+ safeId,
155
+ stateDir,
156
+ sessionStatePath,
157
+ tracePath,
158
+ loadSessionState,
159
+ saveSessionState,
160
+ pruneState,
161
+ auditPath,
162
+ recordAudit,
163
+ healthPath,
164
+ recordHealth,
165
+ forgetHandle,
166
+ };
@@ -0,0 +1,23 @@
1
+ 'use strict';
2
+
3
+ const INTEGRATION_VERSION = '1.0.0';
4
+ const RUNTIME_SCHEMA_VERSION = 1;
5
+ const API_BASE = 'https://api.memoryrouter.ai';
6
+ const MCP_ENDPOINT = 'https://mcp.memoryrouter.ai/mcp';
7
+ const DEFAULT_CONTEXT_CHARS = 12000;
8
+ const DEFAULT_SEARCH_RESULTS = 12;
9
+ const MAX_TRACE_RECORD_CHARS = 8000;
10
+ const MAX_TRACE_TOTAL_CHARS = 60000;
11
+ const ENVELOPE_HEADER = '[MemoryRouter Codex Memory v1]';
12
+
13
+ module.exports = {
14
+ INTEGRATION_VERSION,
15
+ RUNTIME_SCHEMA_VERSION,
16
+ API_BASE,
17
+ MCP_ENDPOINT,
18
+ DEFAULT_CONTEXT_CHARS,
19
+ DEFAULT_SEARCH_RESULTS,
20
+ MAX_TRACE_RECORD_CHARS,
21
+ MAX_TRACE_TOTAL_CHARS,
22
+ ENVELOPE_HEADER,
23
+ };
@@ -0,0 +1,260 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const { loadConfig, getApiKey, loadSessionState, saveSessionState, tracePath, pruneState, recordHealth } = require('./config.cjs');
6
+ const { resolveProject } = require('./project.cjs');
7
+ const { searchScoped, storeScoped, memoryHandle } = require('./memory.cjs');
8
+ const { MAX_TRACE_RECORD_CHARS, MAX_TRACE_TOTAL_CHARS } = require('./constants.cjs');
9
+
10
+ function debug(...values) {
11
+ if (process.env.MEMORYROUTER_DEBUG) console.error('[memoryrouter-codex]', ...values);
12
+ }
13
+
14
+ function readStdin(timeoutMs = 1500) {
15
+ return new Promise(resolve => {
16
+ let value = '';
17
+ const timer = setTimeout(() => resolve(value), timeoutMs);
18
+ process.stdin.setEncoding('utf8');
19
+ process.stdin.on('data', chunk => { value += chunk; });
20
+ process.stdin.on('end', () => { clearTimeout(timer); resolve(value); });
21
+ process.stdin.on('error', () => { clearTimeout(timer); resolve(value); });
22
+ });
23
+ }
24
+
25
+ function truncate(value, limit = MAX_TRACE_RECORD_CHARS) {
26
+ let text;
27
+ try { text = typeof value === 'string' ? value : JSON.stringify(value); } catch { text = String(value); }
28
+ if (text.length <= limit) return text;
29
+ return `${text.slice(0, limit)}\n[truncated by memoryrouter-codex]`;
30
+ }
31
+
32
+ function emitContext(eventName, context) {
33
+ if (!context) return false;
34
+ process.stdout.write(JSON.stringify({
35
+ continue: true,
36
+ hookSpecificOutput: { hookEventName: eventName, additionalContext: context },
37
+ }));
38
+ return true;
39
+ }
40
+
41
+ function appendTrace(input, env = process.env) {
42
+ if (!input.session_id || !input.turn_id) return;
43
+ const record = {
44
+ at: new Date().toISOString(),
45
+ turn_id: input.turn_id,
46
+ tool_name: input.tool_name,
47
+ tool_use_id: input.tool_use_id,
48
+ input: truncate(input.tool_input),
49
+ response: truncate(input.tool_response),
50
+ };
51
+ const file = tracePath(input.session_id, input.turn_id, env);
52
+ fs.appendFileSync(file, `${JSON.stringify(record)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'a' });
53
+ }
54
+
55
+ function readTrace(sessionId, turnId, env = process.env) {
56
+ const file = tracePath(sessionId, turnId, env);
57
+ let text;
58
+ try { text = fs.readFileSync(file, 'utf8'); } catch { return []; }
59
+ const records = [];
60
+ let total = 0;
61
+ for (const line of text.split('\n')) {
62
+ if (!line.trim()) continue;
63
+ try {
64
+ const record = JSON.parse(line);
65
+ const size = JSON.stringify(record).length;
66
+ if (total + size > MAX_TRACE_TOTAL_CHARS) break;
67
+ records.push(record);
68
+ total += size;
69
+ } catch { /* tolerate partial concurrent append */ }
70
+ }
71
+ return records;
72
+ }
73
+
74
+ function removeTrace(sessionId, turnId, env = process.env) {
75
+ try { fs.unlinkSync(tracePath(sessionId, turnId, env)); } catch { /* absent */ }
76
+ }
77
+
78
+ function turnContent(input, state, records, partial = false) {
79
+ const prompt = state.prompts?.[input.turn_id] || state.lastPrompt || '';
80
+ const parts = [
81
+ partial ? 'Codex work checkpoint before compaction/session end.' : 'Completed Codex turn.',
82
+ prompt ? `User request:\n${truncate(prompt, 40000)}` : null,
83
+ ];
84
+ if (records.length) {
85
+ parts.push(`Tool activity (${records.length} calls):`);
86
+ for (const record of records) {
87
+ parts.push([
88
+ `- ${record.tool_name || 'tool'} (${record.tool_use_id || 'unknown'})`,
89
+ ` input: ${record.input}`,
90
+ ` result: ${record.response}`,
91
+ ].join('\n'));
92
+ }
93
+ }
94
+ if (input.last_assistant_message) parts.push(`Final assistant response:\n${truncate(input.last_assistant_message, 60000)}`);
95
+ return parts.filter(Boolean).join('\n\n');
96
+ }
97
+
98
+ async function handleInject(input, context) {
99
+ const prompt = typeof input.prompt === 'string' ? input.prompt.trim() : '';
100
+ if (!prompt) return;
101
+ const state = loadSessionState(input.session_id, context.env);
102
+ state.prompts = { ...(state.prompts || {}), [input.turn_id]: prompt.slice(0, 100000) };
103
+ state.lastPrompt = prompt.slice(0, 100000);
104
+ state.lastTurnId = input.turn_id;
105
+ saveSessionState(input.session_id, state, context.env);
106
+
107
+ const result = await searchScoped(prompt, {
108
+ ...context,
109
+ sessionId: `mrcodex:${context.project.handle}:${input.session_id}`,
110
+ timeoutMs: 5500,
111
+ });
112
+ recordHealth('inject', {
113
+ ok: result.ok,
114
+ project: context.project.handle,
115
+ memories: result.memories?.length || 0,
116
+ status: result.status,
117
+ }, context.env);
118
+ emitContext('UserPromptSubmit', result.context);
119
+ }
120
+
121
+ async function handleSessionStart(input, context) {
122
+ try { pruneState(context.env); } catch { /* cleanup is best effort */ }
123
+ const result = await searchScoped(
124
+ 'Current project decisions, conventions, unresolved work, preferences, and recent implementation context',
125
+ { ...context, sessionId: `mrcodex:${context.project.handle}:${input.session_id}`, timeoutMs: 6500 },
126
+ );
127
+ recordHealth('session-start', {
128
+ ok: result.ok,
129
+ source: input.source,
130
+ project: context.project.handle,
131
+ memories: result.memories?.length || 0,
132
+ }, context.env);
133
+ emitContext('SessionStart', result.context);
134
+ }
135
+
136
+ async function captureTurn(input, context, partial = false) {
137
+ const sessionId = input.session_id;
138
+ const turnId = input.turn_id || loadSessionState(sessionId, context.env).lastTurnId;
139
+ if (!sessionId || !turnId) return { ok: false, reason: 'missing-turn' };
140
+ if (!partial && input.stop_hook_active) return { ok: true, skipped: 'stop-hook-active' };
141
+ const state = loadSessionState(sessionId, context.env);
142
+ if (!partial && state.capturedTurns?.[turnId]) {
143
+ return { ok: true, skipped: 'deduplicated', handle: state.capturedTurns[turnId] };
144
+ }
145
+ const records = readTrace(sessionId, turnId, context.env);
146
+ if (!input.last_assistant_message && records.length === 0) return { ok: true, skipped: 'empty' };
147
+ const content = turnContent({ ...input, turn_id: turnId }, state, records, partial);
148
+ const kind = partial ? 'checkpoint' : 'turn';
149
+ const handle = memoryHandle(context.project.handle, kind, content);
150
+ if ((state.capturedHandles || []).includes(handle)) return { ok: true, skipped: 'deduplicated', handle };
151
+ const result = await storeScoped(content, {
152
+ ...context,
153
+ kind,
154
+ handle,
155
+ sessionId: `mrcodex:${context.project.handle}:${sessionId}`,
156
+ skipRemoteDedupe: true,
157
+ timeoutMs: partial ? 10000 : 15000,
158
+ tags: ['codex', kind],
159
+ });
160
+ if (result.ok && result.stored) {
161
+ state.capturedHandles = [...(state.capturedHandles || []), handle].slice(-500);
162
+ if (!partial) state.capturedTurns = { ...(state.capturedTurns || {}), [turnId]: handle };
163
+ if (Object.keys(state.capturedTurns || {}).length > 500) {
164
+ state.capturedTurns = Object.fromEntries(Object.entries(state.capturedTurns).slice(-500));
165
+ }
166
+ if (!partial && state.prompts) delete state.prompts[turnId];
167
+ state.lastCaptureAt = new Date().toISOString();
168
+ saveSessionState(sessionId, state, context.env);
169
+ if (!partial) removeTrace(sessionId, turnId, context.env);
170
+ }
171
+ recordHealth(partial ? 'flush' : 'capture', {
172
+ ok: result.ok,
173
+ status: result.status,
174
+ project: context.project.handle,
175
+ handle,
176
+ toolCalls: records.length,
177
+ }, context.env);
178
+ return result;
179
+ }
180
+
181
+ async function captureSubagent(input, context) {
182
+ if (!input.last_assistant_message || !input.session_id) return { ok: true, skipped: 'empty' };
183
+ const state = loadSessionState(input.session_id, context.env);
184
+ const captureKey = `subagent:${input.agent_id || 'unknown'}:${input.turn_id || 'unknown'}`;
185
+ if (state.capturedTurns?.[captureKey]) return { ok: true, skipped: 'deduplicated', handle: state.capturedTurns[captureKey] };
186
+ const content = [
187
+ 'Completed Codex subagent outcome.',
188
+ `Subagent type: ${input.agent_type || 'unknown'}`,
189
+ `Subagent id: ${input.agent_id || 'unknown'}`,
190
+ `Outcome:\n${truncate(input.last_assistant_message, 60000)}`,
191
+ ].join('\n\n');
192
+ const handle = memoryHandle(context.project.handle, 'subagent', content);
193
+ const result = await storeScoped(content, {
194
+ ...context,
195
+ kind: 'subagent',
196
+ handle,
197
+ sessionId: `mrcodex:${context.project.handle}:${input.session_id}`,
198
+ skipRemoteDedupe: true,
199
+ timeoutMs: 12000,
200
+ tags: ['codex', 'subagent', input.agent_type || 'unknown'],
201
+ });
202
+ if (result.ok && result.stored) {
203
+ state.capturedHandles = [...(state.capturedHandles || []), handle].slice(-500);
204
+ state.capturedTurns = { ...(state.capturedTurns || {}), [captureKey]: handle };
205
+ if (Object.keys(state.capturedTurns).length > 500) state.capturedTurns = Object.fromEntries(Object.entries(state.capturedTurns).slice(-500));
206
+ saveSessionState(input.session_id, state, context.env);
207
+ }
208
+ recordHealth('capture-subagent', { ok: result.ok, status: result.status, project: context.project.handle, handle }, context.env);
209
+ return result;
210
+ }
211
+
212
+ async function run(kind, input, options = {}) {
213
+ const env = options.env || process.env;
214
+ const config = options.config || loadConfig(env);
215
+ const key = options.key || getApiKey(env, config);
216
+ const project = options.project || resolveProject(input.cwd || process.cwd());
217
+ const context = { env, config, key, project, cwd: input.cwd || process.cwd() };
218
+ if (!key) {
219
+ recordHealth(kind, { ok: false, reason: 'no-api-key', project: project.handle }, env);
220
+ return;
221
+ }
222
+ switch (kind) {
223
+ case 'inject': return handleInject(input, context);
224
+ case 'session-start': return handleSessionStart(input, context);
225
+ case 'trace': appendTrace(input, env); return;
226
+ case 'capture': return captureTurn(input, context, false);
227
+ case 'capture-subagent': return captureSubagent(input, context);
228
+ case 'flush': return captureTurn(input, context, true);
229
+ default: throw new Error(`Unknown hook kind: ${kind}`);
230
+ }
231
+ }
232
+
233
+ async function main() {
234
+ const kind = process.argv[2];
235
+ const wallClock = setTimeout(() => process.exit(0), kind === 'trace' ? 2500 : kind === 'inject' ? 7500 : 18000);
236
+ wallClock.unref();
237
+ let input = {};
238
+ try { input = JSON.parse((await readStdin()) || '{}'); } catch { debug('invalid hook JSON'); }
239
+ try { await run(kind, input); } catch (error) { debug(error?.stack || error); }
240
+ if (kind === 'capture' || kind === 'capture-subagent') process.stdout.write(JSON.stringify({ continue: true }));
241
+ process.exitCode = 0;
242
+ }
243
+
244
+ if (require.main === module) main();
245
+
246
+ module.exports = {
247
+ readStdin,
248
+ truncate,
249
+ emitContext,
250
+ appendTrace,
251
+ readTrace,
252
+ removeTrace,
253
+ turnContent,
254
+ handleInject,
255
+ handleSessionStart,
256
+ captureTurn,
257
+ captureSubagent,
258
+ run,
259
+ main,
260
+ };
@@ -0,0 +1,160 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const readline = require('readline');
5
+ const fs = require('fs');
6
+ const { loadConfig, getApiKey, configPath, auditPath, recordAudit, healthPath } = require('./config.cjs');
7
+ const { resolveProject } = require('./project.cjs');
8
+ const { searchScoped, storeScoped, forgetLocally } = require('./memory.cjs');
9
+ const api = require('./api.cjs');
10
+ const { INTEGRATION_VERSION } = require('./constants.cjs');
11
+
12
+ const SERVER_INSTRUCTIONS = 'MemoryRouter provides durable cross-machine, cross-tool, team-shareable memory. Search project memory before answering questions about prior decisions, preferences, incidents, or unfinished work. Store concise durable decisions and preferences after they are established. Automatic Codex hooks may already capture completed turns, so do not duplicate them. Use returned memory handles when referring to a record. Codex native Memories remain the local personal recall layer; MemoryRouter is the shared and auditable layer.';
13
+
14
+ const TOOLS = [
15
+ {
16
+ name: 'memory_search',
17
+ description: 'Search durable MemoryRouter memories. Defaults to the current git project and filters other projects out.',
18
+ inputSchema: {
19
+ type: 'object',
20
+ properties: {
21
+ query: { type: 'string', description: 'Semantic search query.' },
22
+ limit: { type: 'integer', minimum: 1, maximum: 50, description: 'Candidate results before project filtering.' },
23
+ },
24
+ required: ['query'],
25
+ },
26
+ annotations: { title: 'Search MemoryRouter', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
27
+ },
28
+ {
29
+ name: 'memory_store',
30
+ description: 'Store a concise durable project memory. Returns a stable content-derived handle and deduplicates exact repeats.',
31
+ inputSchema: {
32
+ type: 'object',
33
+ properties: {
34
+ content: { type: 'string' },
35
+ kind: { type: 'string', enum: ['decision', 'preference', 'fact', 'incident', 'note'] },
36
+ tags: { type: 'array', items: { type: 'string' }, maxItems: 20 },
37
+ },
38
+ required: ['content'],
39
+ },
40
+ annotations: { title: 'Store MemoryRouter memory', readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
41
+ },
42
+ {
43
+ name: 'memory_status',
44
+ description: 'Show MemoryRouter authentication, project identity, vault statistics, and recent hook health without exposing secrets.',
45
+ inputSchema: { type: 'object', properties: {} },
46
+ annotations: { title: 'MemoryRouter status', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
47
+ },
48
+ {
49
+ name: 'memory_forget',
50
+ description: 'Suppress one memory handle from this machine, or clear the entire server vault only with an exact confirmation phrase. Handle suppression is not server deletion.',
51
+ inputSchema: {
52
+ type: 'object',
53
+ properties: {
54
+ handle: { type: 'string', description: 'mrh_... handle to suppress locally.' },
55
+ clear_vault_confirmation: { type: 'string', description: 'Exact phrase CLEAR MY ENTIRE MEMORYROUTER VAULT to irreversibly clear every memory in the vault.' },
56
+ },
57
+ },
58
+ annotations: { title: 'Forget MemoryRouter memory', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
59
+ },
60
+ ];
61
+
62
+ function textResult(text, isError = false) {
63
+ return { content: [{ type: 'text', text: String(text) }], ...(isError ? { isError: true } : {}) };
64
+ }
65
+
66
+ async function callTool(name, args = {}, options = {}) {
67
+ const env = options.env || process.env;
68
+ const config = options.config || loadConfig(env);
69
+ const key = options.key || getApiKey(env, config);
70
+ const cwd = options.cwd || process.cwd();
71
+ const project = options.project || resolveProject(cwd);
72
+ if (!key) return textResult('MemoryRouter API key is not configured. Run `memoryrouter-codex init` or set MEMORYROUTER_API_KEY.', true);
73
+
74
+ if (name === 'memory_search') {
75
+ if (!args.query) return textResult('query is required', true);
76
+ const result = await searchScoped(args.query, { env, config, key, cwd, project, limit: args.limit });
77
+ if (!result.ok) return textResult(`Memory search failed${result.status ? ` (HTTP ${result.status})` : ''}.`, true);
78
+ if (!result.context) return textResult(`No memories matched current project ${project.handle}.`);
79
+ return textResult(result.context);
80
+ }
81
+ if (name === 'memory_store') {
82
+ if (!args.content) return textResult('content is required', true);
83
+ const result = await storeScoped(args.content, {
84
+ env, config, key, cwd, project, kind: args.kind || 'note', tags: args.tags || [],
85
+ });
86
+ if (!result.ok) return textResult(`Memory store failed${result.status ? ` (HTTP ${result.status})` : ''}.`, true);
87
+ return textResult(result.deduplicated
88
+ ? `Already stored. Memory handle: ${result.handle}`
89
+ : `Stored for project ${project.handle}. Memory handle: ${result.handle}`);
90
+ }
91
+ if (name === 'memory_status') {
92
+ const response = await api.stats(key, { apiBase: config.apiBase, timeoutMs: 8000, retries: 1 });
93
+ let health = null;
94
+ try { health = JSON.parse(fs.readFileSync(healthPath(env), 'utf8')); } catch { /* optional */ }
95
+ return textResult(JSON.stringify({
96
+ connected: response.ok,
97
+ status: response.status,
98
+ project: { handle: project.handle, identitySource: project.source, crossMachineStable: project.portable },
99
+ auth: process.env.MEMORYROUTER_API_KEY ? 'environment' : 'private-config-file',
100
+ configFile: configPath(env),
101
+ localAuditFile: auditPath(env),
102
+ retrievalScope: config.retrievalScope,
103
+ vault: response.ok ? response.data : null,
104
+ recentHooks: health,
105
+ }, null, 2));
106
+ }
107
+ if (name === 'memory_forget') {
108
+ if (args.clear_vault_confirmation === 'CLEAR MY ENTIRE MEMORYROUTER VAULT') {
109
+ const response = await api.clearVault(key, { apiBase: config.apiBase, timeoutMs: 15000 });
110
+ recordAudit('clear-vault', { ok: response.ok, status: response.status, project: project.handle }, env);
111
+ return response.ok
112
+ ? textResult('The entire MemoryRouter vault was cleared. This cannot be undone.')
113
+ : textResult(`Vault clear failed (HTTP ${response.status}).`, true);
114
+ }
115
+ if (!args.handle) return textResult('Provide a memory handle, or the exact full-vault confirmation phrase.', true);
116
+ const result = forgetLocally(args.handle, { env });
117
+ return result.ok ? textResult(result.message) : textResult('Invalid memory handle.', true);
118
+ }
119
+ return textResult(`Unknown tool: ${name}`, true);
120
+ }
121
+
122
+ async function dispatch(message) {
123
+ const { id, method, params = {} } = message;
124
+ if (method === 'initialize') return {
125
+ jsonrpc: '2.0', id,
126
+ result: {
127
+ protocolVersion: params.protocolVersion || '2025-11-25',
128
+ capabilities: { tools: {} },
129
+ serverInfo: { name: 'MemoryRouter Codex local fallback', version: INTEGRATION_VERSION },
130
+ instructions: SERVER_INSTRUCTIONS,
131
+ },
132
+ };
133
+ if (method === 'tools/list') return { jsonrpc: '2.0', id, result: { tools: TOOLS } };
134
+ if (method === 'tools/call') return { jsonrpc: '2.0', id, result: await callTool(params.name, params.arguments || {}) };
135
+ if (method === 'ping') return { jsonrpc: '2.0', id, result: {} };
136
+ if (id === undefined || id === null) return null;
137
+ return { jsonrpc: '2.0', id, error: { code: -32601, message: `Method not found: ${method}` } };
138
+ }
139
+
140
+ async function main() {
141
+ const input = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
142
+ for await (const line of input) {
143
+ if (!line.trim()) continue;
144
+ let message;
145
+ try { message = JSON.parse(line); } catch {
146
+ process.stdout.write(`${JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } })}\n`);
147
+ continue;
148
+ }
149
+ try {
150
+ const response = await dispatch(message);
151
+ if (response) process.stdout.write(`${JSON.stringify(response)}\n`);
152
+ } catch (error) {
153
+ if (message.id !== undefined) process.stdout.write(`${JSON.stringify({ jsonrpc: '2.0', id: message.id, error: { code: -32603, message: error?.message || 'Internal error' } })}\n`);
154
+ }
155
+ }
156
+ }
157
+
158
+ if (require.main === module) main();
159
+
160
+ module.exports = { SERVER_INSTRUCTIONS, TOOLS, textResult, callTool, dispatch, main };