@10kdevs/matha 0.1.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,305 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import * as fs from 'fs/promises';
5
+ import * as path from 'path';
6
+ import { mathaGetRules, mathaGetDangerZones, mathaGetDecisions, mathaGetStability, mathaBrief, mathaRecordDecision, mathaRecordDanger, mathaRecordContract, } from './tools.js';
7
+ import { checkSchemaVersion, getSchemaMessage } from '../utils/schema-version.js';
8
+ /**
9
+ * MATHA MCP Server
10
+ *
11
+ * Exposes the MATHA brain via MCP protocol for IDE integration.
12
+ * Loads config from .matha/config.json or uses project root if not initialized.
13
+ *
14
+ * Tools:
15
+ * - READ: matha_get_rules, matha_get_danger_zones, matha_get_decisions, matha_get_stability, matha_brief
16
+ * - WRITE: matha_record_decision, matha_record_danger, matha_record_contract
17
+ */
18
+ const server = new Server({
19
+ name: 'MATHA',
20
+ version: '1.0.0',
21
+ });
22
+ let mathaDir;
23
+ // ──────────────────────────────────────────────────────────────────────
24
+ // TOOL DEFINITIONS
25
+ // ──────────────────────────────────────────────────────────────────────
26
+ const tools = [
27
+ {
28
+ name: 'matha_get_rules',
29
+ description: 'Returns all non-negotiable business rules for the project. Used to understand project constraints.',
30
+ inputSchema: {
31
+ type: 'object',
32
+ properties: {},
33
+ required: [],
34
+ },
35
+ },
36
+ {
37
+ name: 'matha_get_danger_zones',
38
+ description: 'Returns identified danger zones (patterns to avoid). Optionally filter by context.',
39
+ inputSchema: {
40
+ type: 'object',
41
+ properties: {
42
+ context: {
43
+ type: 'string',
44
+ description: 'Optional context to filter danger zones (e.g., component name)',
45
+ },
46
+ },
47
+ required: [],
48
+ },
49
+ },
50
+ {
51
+ name: 'matha_get_decisions',
52
+ description: 'Returns past decisions made on this project. Optionally filter by component.',
53
+ inputSchema: {
54
+ type: 'object',
55
+ properties: {
56
+ component: {
57
+ type: 'string',
58
+ description: 'Optional component name to filter decisions',
59
+ },
60
+ limit: {
61
+ type: 'number',
62
+ description: 'Optional limit on number of results',
63
+ },
64
+ },
65
+ required: [],
66
+ },
67
+ },
68
+ {
69
+ name: 'matha_get_stability',
70
+ description: 'Returns stability classification for specified files. Stability indicates how mature/frozen a file is.',
71
+ inputSchema: {
72
+ type: 'object',
73
+ properties: {
74
+ files: {
75
+ type: 'array',
76
+ items: { type: 'string' },
77
+ description: 'Array of file paths to check stability for',
78
+ },
79
+ },
80
+ required: ['files'],
81
+ },
82
+ },
83
+ {
84
+ name: 'matha_brief',
85
+ description: 'Returns the most recent session brief, or intent + rules if no session exists. Used to understand current project state.',
86
+ inputSchema: {
87
+ type: 'object',
88
+ properties: {
89
+ scope: {
90
+ type: 'string',
91
+ description: 'Optional scope to filter session brief',
92
+ },
93
+ },
94
+ required: [],
95
+ },
96
+ },
97
+ {
98
+ name: 'matha_record_decision',
99
+ description: 'Records a decision (learning) about what was assumed vs. what was discovered. Confidence defaults to "probable" (not "confirmed" which requires human verification).',
100
+ inputSchema: {
101
+ type: 'object',
102
+ properties: {
103
+ component: {
104
+ type: 'string',
105
+ description: 'Component or file this decision relates to',
106
+ },
107
+ previous_assumption: {
108
+ type: 'string',
109
+ description: 'What was previously thought to be true',
110
+ },
111
+ correction: {
112
+ type: 'string',
113
+ description: 'What was discovered to actually be true',
114
+ },
115
+ confidence: {
116
+ type: 'string',
117
+ enum: ['confirmed', 'probable', 'uncertain'],
118
+ description: 'Confidence level. Default: probable (agent-level). Use "confirmed" only for human-verified facts.',
119
+ },
120
+ },
121
+ required: ['component', 'previous_assumption', 'correction'],
122
+ },
123
+ },
124
+ {
125
+ name: 'matha_record_danger',
126
+ description: 'Records a danger zone (pattern to avoid) discovered during development.',
127
+ inputSchema: {
128
+ type: 'object',
129
+ properties: {
130
+ component: {
131
+ type: 'string',
132
+ description: 'Component or file where danger zone was found',
133
+ },
134
+ description: {
135
+ type: 'string',
136
+ description: 'Description of the danger pattern',
137
+ },
138
+ },
139
+ required: ['component', 'description'],
140
+ },
141
+ },
142
+ {
143
+ name: 'matha_record_contract',
144
+ description: 'Records a behaviour contract (set of invariant assertions) for a component. Overwrites existing contract for the same component.',
145
+ inputSchema: {
146
+ type: 'object',
147
+ properties: {
148
+ component: {
149
+ type: 'string',
150
+ description: 'Component or file this contract applies to',
151
+ },
152
+ assertions: {
153
+ type: 'array',
154
+ items: { type: 'string' },
155
+ description: 'List of invariant assertions (must remain true)',
156
+ },
157
+ },
158
+ required: ['component', 'assertions'],
159
+ },
160
+ },
161
+ ];
162
+ // ──────────────────────────────────────────────────────────────────────
163
+ // REQUEST HANDLERS
164
+ // ──────────────────────────────────────────────────────────────────────
165
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
166
+ tools,
167
+ }));
168
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
169
+ const name = request.params?.name;
170
+ const args = request.params?.arguments;
171
+ try {
172
+ let result;
173
+ switch (name) {
174
+ case 'matha_get_rules':
175
+ result = await mathaGetRules(mathaDir);
176
+ break;
177
+ case 'matha_get_danger_zones':
178
+ result = await mathaGetDangerZones(mathaDir, args?.context);
179
+ break;
180
+ case 'matha_get_decisions':
181
+ result = await mathaGetDecisions(mathaDir, args?.component, args?.limit);
182
+ break;
183
+ case 'matha_get_stability':
184
+ result = await mathaGetStability(mathaDir, args?.files || []);
185
+ break;
186
+ case 'matha_brief':
187
+ result = await mathaBrief(mathaDir, args?.scope);
188
+ break;
189
+ case 'matha_record_decision':
190
+ result = await mathaRecordDecision(mathaDir, args?.component, args?.previous_assumption, args?.correction, args?.confidence || 'probable');
191
+ break;
192
+ case 'matha_record_danger':
193
+ result = await mathaRecordDanger(mathaDir, args?.component, args?.description);
194
+ break;
195
+ case 'matha_record_contract':
196
+ result = await mathaRecordContract(mathaDir, args?.component, args?.assertions);
197
+ break;
198
+ default:
199
+ return {
200
+ content: [
201
+ {
202
+ type: 'text',
203
+ text: JSON.stringify({ error: `Unknown tool: ${name}` }),
204
+ },
205
+ ],
206
+ };
207
+ }
208
+ // All tool results are JSON strings
209
+ return {
210
+ content: [
211
+ {
212
+ type: 'text',
213
+ text: result,
214
+ },
215
+ ],
216
+ };
217
+ }
218
+ catch (err) {
219
+ return {
220
+ content: [
221
+ {
222
+ type: 'text',
223
+ text: JSON.stringify({ error: `Tool execution failed: ${err.message}` }),
224
+ },
225
+ ],
226
+ isError: true,
227
+ };
228
+ }
229
+ });
230
+ // ──────────────────────────────────────────────────────────────────────
231
+ // INITIALIZATION & STARTUP
232
+ // ──────────────────────────────────────────────────────────────────────
233
+ /**
234
+ * Initialize the server by locating and validating the MATHA directory.
235
+ *
236
+ * Strategy:
237
+ * 1. Look for .matha/ directory in current working directory or parent directories
238
+ * 2. If found, use that
239
+ * 3. If not found, use CWD/.matha (will use defaults gracefully)
240
+ * 4. Write mcp-config.json with absolute paths for this session
241
+ */
242
+ async function initialize() {
243
+ const cwd = process.cwd();
244
+ // Try to find existing .matha directory
245
+ let found = false;
246
+ let searchDir = cwd;
247
+ for (let i = 0; i < 10; i++) {
248
+ const candidate = path.join(searchDir, '.matha');
249
+ try {
250
+ await fs.access(candidate);
251
+ mathaDir = candidate;
252
+ found = true;
253
+ break;
254
+ }
255
+ catch {
256
+ // Not found, try parent
257
+ }
258
+ const parent = path.dirname(searchDir);
259
+ if (parent === searchDir)
260
+ break; // Reached root
261
+ searchDir = parent;
262
+ }
263
+ // If not found, use default location
264
+ if (!found) {
265
+ mathaDir = path.join(cwd, '.matha');
266
+ }
267
+ // Write mcp-config.json with absolute paths
268
+ try {
269
+ const configPath = path.join(mathaDir, 'mcp-config.json');
270
+ const config = {
271
+ matha_dir: mathaDir,
272
+ cwd: cwd,
273
+ initialized: found,
274
+ timestamp: new Date().toISOString(),
275
+ };
276
+ await fs.mkdir(mathaDir, { recursive: true });
277
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2));
278
+ }
279
+ catch {
280
+ // Ignore write errors - server can still run in degraded mode
281
+ }
282
+ // SCHEMA VERSION CHECK
283
+ const schemaResult = await checkSchemaVersion(mathaDir);
284
+ const schemaMsg = getSchemaMessage(schemaResult);
285
+ if (schemaMsg)
286
+ console.error(schemaMsg);
287
+ if (schemaResult.status === 'newer') {
288
+ process.exit(1);
289
+ }
290
+ }
291
+ /**
292
+ * Start the MCP server on stdio
293
+ */
294
+ async function main() {
295
+ await initialize();
296
+ const transport = new StdioServerTransport();
297
+ await server.connect(transport);
298
+ // Log startup info to stderr so it doesn't interfere with stdio protocol
299
+ const msg = `MATHA MCP server running on stdio, mathaDir: ${mathaDir}`;
300
+ console.error(msg);
301
+ }
302
+ main().catch((err) => {
303
+ console.error('Server initialization failed:', err);
304
+ process.exit(1);
305
+ });
@@ -0,0 +1,379 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ import { readJsonOrNull } from '../storage/reader.js';
4
+ import { writeAtomic } from '../storage/writer.js';
5
+ import { getRules, getDangerZones, getDecisions, recordDecision, recordDangerZone, } from '../brain/hippocampus.js';
6
+ import { refreshFromGit, getStability, getSnapshot, } from '../brain/cortex.js';
7
+ import { matchAll } from '../analysis/contract-matcher.js';
8
+ import { analyseDeltas, persistAnalysis, getRecommendation } from '../brain/dopamine.js';
9
+ // Simple UUID-like ID generator
10
+ function generateId() {
11
+ return Math.random().toString(36).substring(2, 15) +
12
+ Math.random().toString(36).substring(2, 15);
13
+ }
14
+ // ──────────────────────────────────────────────────────────────────────
15
+ // READ TOOLS
16
+ // ──────────────────────────────────────────────────────────────────────
17
+ /**
18
+ * matha_get_rules: Returns all business rules.
19
+ */
20
+ export async function mathaGetRules(mathaDir) {
21
+ try {
22
+ const rules = await getRules(mathaDir);
23
+ return JSON.stringify({ rules });
24
+ }
25
+ catch (err) {
26
+ return JSON.stringify({ error: `Failed to get rules: ${err.message}` });
27
+ }
28
+ }
29
+ /**
30
+ * matha_get_danger_zones: Returns danger zones, optionally filtered by context.
31
+ */
32
+ export async function mathaGetDangerZones(mathaDir, context) {
33
+ try {
34
+ const zones = await getDangerZones(mathaDir, context);
35
+ return JSON.stringify({ zones });
36
+ }
37
+ catch (err) {
38
+ return JSON.stringify({ error: `Failed to get danger zones: ${err.message}` });
39
+ }
40
+ }
41
+ /**
42
+ * matha_get_decisions: Returns decisions, optionally filtered by component and limit.
43
+ */
44
+ export async function mathaGetDecisions(mathaDir, component, limit) {
45
+ try {
46
+ const decisions = await getDecisions(mathaDir, component, limit);
47
+ return JSON.stringify({ decisions });
48
+ }
49
+ catch (err) {
50
+ return JSON.stringify({ error: `Failed to get decisions: ${err.message}` });
51
+ }
52
+ }
53
+ /**
54
+ * matha_get_stability: Returns stability classification for requested files.
55
+ * Uses cortex.getStability — returns StabilityRecord | null per file.
56
+ * repoPath assumption: process.cwd() is the project root.
57
+ */
58
+ export async function mathaGetStability(mathaDir, files) {
59
+ try {
60
+ const stability = await getStability(mathaDir, files);
61
+ return JSON.stringify({ stability });
62
+ }
63
+ catch (err) {
64
+ // On any error, return null for all
65
+ const stability = {};
66
+ for (const file of files) {
67
+ stability[file] = null;
68
+ }
69
+ return JSON.stringify({ stability });
70
+ }
71
+ }
72
+ /**
73
+ * matha_brief: Returns the most recent session brief, or intent + rules.
74
+ * If directory provided, filters decisions/danger zones/stability to that directory.
75
+ */
76
+ export async function mathaBrief(mathaDir, scope, directory) {
77
+ try {
78
+ // DIRECTORY FILTER MODE
79
+ if (directory) {
80
+ const dirLower = directory.toLowerCase();
81
+ // Filter decisions by component matching directory
82
+ let decisions = [];
83
+ try {
84
+ const allDecisions = await getDecisions(mathaDir);
85
+ decisions = allDecisions.filter((d) => (d.component || '').toLowerCase().includes(dirLower));
86
+ }
87
+ catch {
88
+ decisions = [];
89
+ }
90
+ // Filter danger zones by component matching directory
91
+ let zones = [];
92
+ try {
93
+ zones = await getDangerZones(mathaDir, directory);
94
+ }
95
+ catch {
96
+ zones = [];
97
+ }
98
+ // Filter stability records where filepath starts with directory
99
+ let stabilityRecords = [];
100
+ try {
101
+ const snapshot = await getSnapshot(mathaDir);
102
+ if (snapshot && snapshot.stability) {
103
+ stabilityRecords = snapshot.stability.filter((s) => s.filepath.toLowerCase().startsWith(dirLower));
104
+ }
105
+ }
106
+ catch {
107
+ stabilityRecords = [];
108
+ }
109
+ const hasData = decisions.length > 0 || zones.length > 0 || stabilityRecords.length > 0;
110
+ const matchContext = {
111
+ scope: directory,
112
+ intent: '',
113
+ operationType: 'unknown',
114
+ filepaths: [directory],
115
+ };
116
+ let matchResults = [];
117
+ try {
118
+ matchResults = await matchAll(matchContext, mathaDir);
119
+ }
120
+ catch {
121
+ matchResults = [];
122
+ }
123
+ const hasCritical = matchResults.some((r) => r.severity === 'critical');
124
+ return JSON.stringify({
125
+ directory,
126
+ filtered: true,
127
+ hasData,
128
+ message: hasData ? null : `No MATHA data found for directory: ${directory}`,
129
+ decisions,
130
+ dangerZones: zones,
131
+ stability: stabilityRecords,
132
+ matchResults,
133
+ hasCritical,
134
+ });
135
+ }
136
+ // STANDARD MODE — most recent session brief
137
+ const sessionsDir = path.join(mathaDir, 'sessions');
138
+ // Try to find the most recent .brief file
139
+ let briefData = null;
140
+ try {
141
+ const files = await fs.readdir(sessionsDir);
142
+ const briefFiles = files
143
+ .filter((f) => f.endsWith('.brief'))
144
+ .sort()
145
+ .reverse();
146
+ if (briefFiles.length > 0) {
147
+ const briefPath = path.join(sessionsDir, briefFiles[0]);
148
+ briefData = await readJsonOrNull(briefPath);
149
+ }
150
+ }
151
+ catch {
152
+ // Sessions directory might not exist
153
+ briefData = null;
154
+ }
155
+ // If we have a brief and scope provided, check if it matches
156
+ if (briefData && scope) {
157
+ const briefScope = briefData.scope || '';
158
+ if (!briefScope.toLowerCase().includes(scope.toLowerCase())) {
159
+ briefData = null;
160
+ }
161
+ }
162
+ // Determine base response
163
+ let baseResponse;
164
+ if (briefData) {
165
+ baseResponse = briefData;
166
+ }
167
+ else {
168
+ // Otherwise return intent + rules
169
+ const intentPath = path.join(mathaDir, 'hippocampus/intent.json');
170
+ const intent = await readJsonOrNull(intentPath);
171
+ const rules = await getRules(mathaDir).catch(() => []);
172
+ const parsedRules = typeof rules === 'string' ? JSON.parse(rules).rules : [];
173
+ baseResponse = {
174
+ why: intent?.why ?? '',
175
+ rules: parsedRules,
176
+ };
177
+ }
178
+ // Augment with matchAll
179
+ const matchContext = {
180
+ scope: baseResponse.scope || '',
181
+ intent: baseResponse.operation_description || baseResponse.why || '',
182
+ operationType: baseResponse.operationType || 'unknown',
183
+ filepaths: (baseResponse.scope || '').split(',').map((s) => s.trim()).filter(Boolean),
184
+ };
185
+ let matchResults = [];
186
+ try {
187
+ matchResults = await matchAll(matchContext, mathaDir);
188
+ }
189
+ catch {
190
+ matchResults = [];
191
+ }
192
+ const hasCritical = matchResults.some((r) => r.severity === 'critical');
193
+ return JSON.stringify({
194
+ ...baseResponse,
195
+ matchResults,
196
+ hasCritical,
197
+ });
198
+ }
199
+ catch (err) {
200
+ return JSON.stringify({ error: `Failed to get brief: ${err.message}` });
201
+ }
202
+ }
203
+ // ──────────────────────────────────────────────────────────────────────
204
+ // WRITE TOOLS
205
+ // ──────────────────────────────────────────────────────────────────────
206
+ /**
207
+ * matha_record_decision: Records a decision from an AI agent.
208
+ * Uses 'probable' confidence (not 'confirmed' — that is human-verified).
209
+ */
210
+ export async function mathaRecordDecision(mathaDir, component, previousAssumption, correction, confidence = 'probable') {
211
+ try {
212
+ const id = `${Date.now()}-${generateId()}`;
213
+ const timestamp = new Date().toISOString();
214
+ const decision = {
215
+ id,
216
+ timestamp,
217
+ component,
218
+ previous_assumption: previousAssumption,
219
+ correction,
220
+ trigger: 'mcp-call',
221
+ confidence,
222
+ status: 'active',
223
+ supersedes: null,
224
+ session_id: id,
225
+ };
226
+ await recordDecision(mathaDir, decision);
227
+ return JSON.stringify({ success: true, id });
228
+ }
229
+ catch (err) {
230
+ return JSON.stringify({
231
+ success: false,
232
+ error: `Failed to record decision: ${err.message}`,
233
+ });
234
+ }
235
+ }
236
+ /**
237
+ * matha_record_danger: Records a danger zone discovered by an agent.
238
+ */
239
+ export async function mathaRecordDanger(mathaDir, component, description) {
240
+ try {
241
+ const id = `danger-${Date.now()}-${generateId()}`;
242
+ const zone = {
243
+ id,
244
+ component,
245
+ pattern: description,
246
+ description: description,
247
+ };
248
+ await recordDangerZone(mathaDir, zone);
249
+ return JSON.stringify({ success: true, id });
250
+ }
251
+ catch (err) {
252
+ return JSON.stringify({
253
+ success: false,
254
+ error: `Failed to record danger zone: ${err.message}`,
255
+ });
256
+ }
257
+ }
258
+ /**
259
+ * matha_record_contract: Records a behaviour contract for a component.
260
+ * Overwrites existing contract for same component (versioned).
261
+ */
262
+ export async function mathaRecordContract(mathaDir, component, assertions) {
263
+ try {
264
+ // Sanitize component name for filename
265
+ const filename = component
266
+ .replace(/[^a-zA-Z0-9._-]/g, '-')
267
+ .toLowerCase();
268
+ const contractPath = path.join(mathaDir, `cerebellum/contracts/${filename}.json`);
269
+ const contract = {
270
+ component,
271
+ version: 1,
272
+ last_updated: new Date().toISOString(),
273
+ assertions: assertions.map((description, idx) => ({
274
+ id: `${component}-assertion-${idx}`,
275
+ description,
276
+ type: 'invariant',
277
+ status: 'active',
278
+ violation_count: 0,
279
+ last_violated: null,
280
+ })),
281
+ };
282
+ await writeAtomic(contractPath, contract, { overwrite: true });
283
+ return JSON.stringify({ success: true, component });
284
+ }
285
+ catch (err) {
286
+ return JSON.stringify({
287
+ success: false,
288
+ error: `Failed to record contract: ${err.message}`,
289
+ });
290
+ }
291
+ }
292
+ /**
293
+ * matha_match: Runs the contract matcher independently to get danger/history warnings.
294
+ */
295
+ export async function mathaMatch(mathaDir, scope, intent, operationType = 'unknown', filepaths = []) {
296
+ try {
297
+ const context = {
298
+ scope,
299
+ intent,
300
+ operationType,
301
+ filepaths: filepaths.length > 0 ? filepaths : scope.split(',').map((s) => s.trim()).filter(Boolean),
302
+ };
303
+ const results = await matchAll(context, mathaDir);
304
+ const hasCritical = results.some((r) => r.severity === 'critical');
305
+ const summary = {
306
+ critical: results.filter((r) => r.severity === 'critical').length,
307
+ warning: results.filter((r) => r.severity === 'warning').length,
308
+ info: results.filter((r) => r.severity === 'info').length,
309
+ total: results.length,
310
+ };
311
+ return JSON.stringify({
312
+ results,
313
+ hasCritical,
314
+ summary,
315
+ });
316
+ }
317
+ catch (err) {
318
+ return JSON.stringify({ error: `Failed to run contract matcher: ${err.message}`, results: [], hasCritical: false });
319
+ }
320
+ }
321
+ /**
322
+ * matha_get_routing: Close the Dopamine Loop.
323
+ * If operationType provided: returns specific recommendation.
324
+ * If not provided: runs full delta analysis and returns DopamineAnalysis report.
325
+ */
326
+ export async function mathaGetRouting(mathaDir, operationType) {
327
+ try {
328
+ if (operationType) {
329
+ const rec = await getRecommendation(mathaDir, operationType);
330
+ return JSON.stringify({
331
+ operation_type: operationType,
332
+ recommended_tier: rec.tier,
333
+ recommended_budget: rec.budget,
334
+ source: rec.source,
335
+ confidence: rec.confidence,
336
+ sample_size: rec.sample_size
337
+ });
338
+ }
339
+ else {
340
+ // Full analysis trigger
341
+ const analysis = await analyseDeltas(mathaDir);
342
+ await persistAnalysis(mathaDir, analysis);
343
+ return JSON.stringify(analysis);
344
+ }
345
+ }
346
+ catch (err) {
347
+ return JSON.stringify({ error: err.message });
348
+ }
349
+ }
350
+ // ──────────────────────────────────────────────────────────────────────
351
+ // CORTEX TOOLS
352
+ // ──────────────────────────────────────────────────────────────────────
353
+ /**
354
+ * matha_refresh_cortex: Triggers a git analysis refresh of the cortex.
355
+ * repoPath assumption: process.cwd() is the project root.
356
+ * Never throws to MCP caller.
357
+ */
358
+ export async function mathaRefreshCortex(mathaDir) {
359
+ try {
360
+ // Use mathaDir to derive repoPath (go up from .matha)
361
+ const repoPath = path.dirname(mathaDir);
362
+ const snapshot = await refreshFromGit(repoPath, mathaDir);
363
+ return JSON.stringify({
364
+ success: true,
365
+ commitCount: snapshot.commitCount,
366
+ fileCount: snapshot.fileCount,
367
+ summary: snapshot.summary,
368
+ });
369
+ }
370
+ catch (err) {
371
+ return JSON.stringify({
372
+ success: false,
373
+ error: `Failed to refresh cortex: ${err.message}`,
374
+ commitCount: 0,
375
+ fileCount: 0,
376
+ summary: null,
377
+ });
378
+ }
379
+ }