@optave/codegraph 2.1.0 → 2.1.1-dev.00f091c

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.
@@ -313,6 +313,18 @@ function extractImplementsFromNode(node) {
313
313
  return result;
314
314
  }
315
315
 
316
+ function extractReceiverName(objNode) {
317
+ if (!objNode) return undefined;
318
+ if (objNode.type === 'identifier') return objNode.text;
319
+ if (objNode.type === 'this') return 'this';
320
+ if (objNode.type === 'super') return 'super';
321
+ if (objNode.type === 'member_expression') {
322
+ const prop = objNode.childForFieldName('property');
323
+ if (prop) return objNode.text;
324
+ }
325
+ return objNode.text;
326
+ }
327
+
316
328
  function extractCallInfo(fn, callNode) {
317
329
  if (fn.type === 'identifier') {
318
330
  return { name: fn.text, line: callNode.startPosition.row + 1 };
@@ -335,19 +347,25 @@ function extractCallInfo(fn, callNode) {
335
347
 
336
348
  if (prop.type === 'string' || prop.type === 'string_fragment') {
337
349
  const methodName = prop.text.replace(/['"]/g, '');
338
- if (methodName)
339
- return { name: methodName, line: callNode.startPosition.row + 1, dynamic: true };
350
+ if (methodName) {
351
+ const receiver = extractReceiverName(obj);
352
+ return { name: methodName, line: callNode.startPosition.row + 1, dynamic: true, receiver };
353
+ }
340
354
  }
341
355
 
342
- return { name: prop.text, line: callNode.startPosition.row + 1 };
356
+ const receiver = extractReceiverName(obj);
357
+ return { name: prop.text, line: callNode.startPosition.row + 1, receiver };
343
358
  }
344
359
 
345
360
  if (fn.type === 'subscript_expression') {
361
+ const obj = fn.childForFieldName('object');
346
362
  const index = fn.childForFieldName('index');
347
363
  if (index && (index.type === 'string' || index.type === 'template_string')) {
348
364
  const methodName = index.text.replace(/['"`]/g, '');
349
- if (methodName && !methodName.includes('$'))
350
- return { name: methodName, line: callNode.startPosition.row + 1, dynamic: true };
365
+ if (methodName && !methodName.includes('$')) {
366
+ const receiver = extractReceiverName(obj);
367
+ return { name: methodName, line: callNode.startPosition.row + 1, dynamic: true, receiver };
368
+ }
351
369
  }
352
370
  }
353
371
 
@@ -206,7 +206,10 @@ export function extractPHPSymbols(tree, _filePath) {
206
206
  case 'member_call_expression': {
207
207
  const name = node.childForFieldName('name');
208
208
  if (name) {
209
- calls.push({ name: name.text, line: node.startPosition.row + 1 });
209
+ const obj = node.childForFieldName('object');
210
+ const call = { name: name.text, line: node.startPosition.row + 1 };
211
+ if (obj) call.receiver = obj.text;
212
+ calls.push(call);
210
213
  }
211
214
  break;
212
215
  }
@@ -214,7 +217,10 @@ export function extractPHPSymbols(tree, _filePath) {
214
217
  case 'scoped_call_expression': {
215
218
  const name = node.childForFieldName('name');
216
219
  if (name) {
217
- calls.push({ name: name.text, line: node.startPosition.row + 1 });
220
+ const scope = node.childForFieldName('scope');
221
+ const call = { name: name.text, line: node.startPosition.row + 1 };
222
+ if (scope) call.receiver = scope.text;
223
+ calls.push(call);
218
224
  }
219
225
  break;
220
226
  }
@@ -69,12 +69,19 @@ export function extractPythonSymbols(tree, _filePath) {
69
69
  const fn = node.childForFieldName('function');
70
70
  if (fn) {
71
71
  let callName = null;
72
+ let receiver;
72
73
  if (fn.type === 'identifier') callName = fn.text;
73
74
  else if (fn.type === 'attribute') {
74
75
  const attr = fn.childForFieldName('attribute');
75
76
  if (attr) callName = attr.text;
77
+ const obj = fn.childForFieldName('object');
78
+ if (obj) receiver = obj.text;
79
+ }
80
+ if (callName) {
81
+ const call = { name: callName, line: node.startPosition.row + 1 };
82
+ if (receiver) call.receiver = receiver;
83
+ calls.push(call);
76
84
  }
77
- if (callName) calls.push({ name: callName, line: node.startPosition.row + 1 });
78
85
  }
79
86
  break;
80
87
  }
@@ -170,7 +170,10 @@ export function extractRubySymbols(tree, _filePath) {
170
170
  }
171
171
  }
172
172
  } else {
173
- calls.push({ name: methodNode.text, line: node.startPosition.row + 1 });
173
+ const recv = node.childForFieldName('receiver');
174
+ const call = { name: methodNode.text, line: node.startPosition.row + 1 };
175
+ if (recv) call.receiver = recv.text;
176
+ calls.push(call);
174
177
  }
175
178
  }
176
179
  break;
@@ -135,10 +135,20 @@ export function extractRustSymbols(tree, _filePath) {
135
135
  calls.push({ name: fn.text, line: node.startPosition.row + 1 });
136
136
  } else if (fn.type === 'field_expression') {
137
137
  const field = fn.childForFieldName('field');
138
- if (field) calls.push({ name: field.text, line: node.startPosition.row + 1 });
138
+ if (field) {
139
+ const value = fn.childForFieldName('value');
140
+ const call = { name: field.text, line: node.startPosition.row + 1 };
141
+ if (value) call.receiver = value.text;
142
+ calls.push(call);
143
+ }
139
144
  } else if (fn.type === 'scoped_identifier') {
140
145
  const name = fn.childForFieldName('name');
141
- if (name) calls.push({ name: name.text, line: node.startPosition.row + 1 });
146
+ if (name) {
147
+ const path = fn.childForFieldName('path');
148
+ const call = { name: name.text, line: node.startPosition.row + 1 };
149
+ if (path) call.receiver = path.text;
150
+ calls.push(call);
151
+ }
142
152
  }
143
153
  }
144
154
  break;
package/src/index.js CHANGED
@@ -38,6 +38,7 @@ export { isNativeAvailable } from './native.js';
38
38
  export { getActiveEngine, parseFileAuto, parseFilesAuto } from './parser.js';
39
39
  // Query functions (data-returning)
40
40
  export {
41
+ contextData,
41
42
  diffImpactData,
42
43
  fileDepsData,
43
44
  fnDepsData,
package/src/journal.js ADDED
@@ -0,0 +1,109 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { debug, warn } from './logger.js';
4
+
5
+ export const JOURNAL_FILENAME = 'changes.journal';
6
+ const HEADER_PREFIX = '# codegraph-journal v1 ';
7
+
8
+ /**
9
+ * Read and validate the change journal.
10
+ * Returns { valid, timestamp, changed[], removed[] } or { valid: false }.
11
+ */
12
+ export function readJournal(rootDir) {
13
+ const journalPath = path.join(rootDir, '.codegraph', JOURNAL_FILENAME);
14
+ let content;
15
+ try {
16
+ content = fs.readFileSync(journalPath, 'utf-8');
17
+ } catch {
18
+ return { valid: false };
19
+ }
20
+
21
+ const lines = content.split('\n');
22
+ if (lines.length === 0 || !lines[0].startsWith(HEADER_PREFIX)) {
23
+ debug('Journal has malformed or missing header');
24
+ return { valid: false };
25
+ }
26
+
27
+ const timestamp = Number(lines[0].slice(HEADER_PREFIX.length).trim());
28
+ if (!Number.isFinite(timestamp) || timestamp <= 0) {
29
+ debug('Journal has invalid timestamp');
30
+ return { valid: false };
31
+ }
32
+
33
+ const changed = [];
34
+ const removed = [];
35
+ const seenChanged = new Set();
36
+ const seenRemoved = new Set();
37
+
38
+ for (let i = 1; i < lines.length; i++) {
39
+ const line = lines[i].trim();
40
+ if (!line || line.startsWith('#')) continue;
41
+
42
+ if (line.startsWith('DELETED ')) {
43
+ const filePath = line.slice(8);
44
+ if (filePath && !seenRemoved.has(filePath)) {
45
+ seenRemoved.add(filePath);
46
+ removed.push(filePath);
47
+ }
48
+ } else {
49
+ if (!seenChanged.has(line)) {
50
+ seenChanged.add(line);
51
+ changed.push(line);
52
+ }
53
+ }
54
+ }
55
+
56
+ return { valid: true, timestamp, changed, removed };
57
+ }
58
+
59
+ /**
60
+ * Append changed/deleted paths to the journal.
61
+ * Creates the journal with a header if it doesn't exist.
62
+ */
63
+ export function appendJournalEntries(rootDir, entries) {
64
+ const dir = path.join(rootDir, '.codegraph');
65
+ const journalPath = path.join(dir, JOURNAL_FILENAME);
66
+
67
+ if (!fs.existsSync(dir)) {
68
+ fs.mkdirSync(dir, { recursive: true });
69
+ }
70
+
71
+ // If journal doesn't exist, create with a placeholder header
72
+ if (!fs.existsSync(journalPath)) {
73
+ fs.writeFileSync(journalPath, `${HEADER_PREFIX}0\n`);
74
+ }
75
+
76
+ const lines = entries.map((e) => {
77
+ if (e.deleted) return `DELETED ${e.file}`;
78
+ return e.file;
79
+ });
80
+
81
+ fs.appendFileSync(journalPath, `${lines.join('\n')}\n`);
82
+ }
83
+
84
+ /**
85
+ * Write a fresh journal header after a successful build.
86
+ * Atomic: write to temp file then rename.
87
+ */
88
+ export function writeJournalHeader(rootDir, timestamp) {
89
+ const dir = path.join(rootDir, '.codegraph');
90
+ const journalPath = path.join(dir, JOURNAL_FILENAME);
91
+ const tmpPath = `${journalPath}.tmp`;
92
+
93
+ if (!fs.existsSync(dir)) {
94
+ fs.mkdirSync(dir, { recursive: true });
95
+ }
96
+
97
+ try {
98
+ fs.writeFileSync(tmpPath, `${HEADER_PREFIX}${timestamp}\n`);
99
+ fs.renameSync(tmpPath, journalPath);
100
+ } catch (err) {
101
+ warn(`Failed to write journal header: ${err.message}`);
102
+ // Clean up temp file if rename failed
103
+ try {
104
+ fs.unlinkSync(tmpPath);
105
+ } catch {
106
+ /* ignore */
107
+ }
108
+ }
109
+ }
package/src/mcp.js CHANGED
@@ -100,6 +100,34 @@ const BASE_TOOLS = [
100
100
  required: ['name'],
101
101
  },
102
102
  },
103
+ {
104
+ name: 'context',
105
+ description:
106
+ 'Full context for a function: source code, dependencies with summaries, callers, signature, and related tests — everything needed to understand or modify a function in one call',
107
+ inputSchema: {
108
+ type: 'object',
109
+ properties: {
110
+ name: { type: 'string', description: 'Function/method/class name (partial match)' },
111
+ depth: {
112
+ type: 'number',
113
+ description: 'Include callee source up to N levels deep (0=no source, 1=direct)',
114
+ default: 0,
115
+ },
116
+ no_source: {
117
+ type: 'boolean',
118
+ description: 'Skip source extraction (metadata only)',
119
+ default: false,
120
+ },
121
+ no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
122
+ include_tests: {
123
+ type: 'boolean',
124
+ description: 'Include test file source code',
125
+ default: false,
126
+ },
127
+ },
128
+ required: ['name'],
129
+ },
130
+ },
103
131
  {
104
132
  name: 'diff_impact',
105
133
  description: 'Analyze git diff to find which functions changed and their transitive callers',
@@ -245,12 +273,15 @@ export { TOOLS, buildToolList };
245
273
  export async function startMCPServer(customDbPath, options = {}) {
246
274
  const { allowedRepos } = options;
247
275
  const multiRepo = options.multiRepo || !!allowedRepos;
248
- let Server, StdioServerTransport;
276
+ let Server, StdioServerTransport, ListToolsRequestSchema, CallToolRequestSchema;
249
277
  try {
250
278
  const sdk = await import('@modelcontextprotocol/sdk/server/index.js');
251
279
  Server = sdk.Server;
252
280
  const transport = await import('@modelcontextprotocol/sdk/server/stdio.js');
253
281
  StdioServerTransport = transport.StdioServerTransport;
282
+ const types = await import('@modelcontextprotocol/sdk/types.js');
283
+ ListToolsRequestSchema = types.ListToolsRequestSchema;
284
+ CallToolRequestSchema = types.CallToolRequestSchema;
254
285
  } catch {
255
286
  console.error(
256
287
  'MCP server requires @modelcontextprotocol/sdk.\n' +
@@ -267,6 +298,7 @@ export async function startMCPServer(customDbPath, options = {}) {
267
298
  fileDepsData,
268
299
  fnDepsData,
269
300
  fnImpactData,
301
+ contextData,
270
302
  diffImpactData,
271
303
  listFunctionsData,
272
304
  } = await import('./queries.js');
@@ -279,9 +311,11 @@ export async function startMCPServer(customDbPath, options = {}) {
279
311
  { capabilities: { tools: {} } },
280
312
  );
281
313
 
282
- server.setRequestHandler('tools/list', async () => ({ tools: buildToolList(multiRepo) }));
314
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
315
+ tools: buildToolList(multiRepo),
316
+ }));
283
317
 
284
- server.setRequestHandler('tools/call', async (request) => {
318
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
285
319
  const { name, arguments: args } = request.params;
286
320
 
287
321
  try {
@@ -343,6 +377,14 @@ export async function startMCPServer(customDbPath, options = {}) {
343
377
  noTests: args.no_tests,
344
378
  });
345
379
  break;
380
+ case 'context':
381
+ result = contextData(args.name, dbPath, {
382
+ depth: args.depth,
383
+ noSource: args.no_source,
384
+ noTests: args.no_tests,
385
+ includeTests: args.include_tests,
386
+ });
387
+ break;
346
388
  case 'diff_impact':
347
389
  result = diffImpactData(dbPath, {
348
390
  staged: args.staged,
package/src/parser.js CHANGED
@@ -101,6 +101,7 @@ function normalizeNativeSymbols(result) {
101
101
  name: c.name,
102
102
  line: c.line,
103
103
  dynamic: c.dynamic,
104
+ receiver: c.receiver,
104
105
  })),
105
106
  imports: (result.imports || []).map((i) => ({
106
107
  source: i.source,