@kentwynn/kgraph 0.1.25 → 0.1.27

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
@@ -107,6 +107,8 @@ kgraph "auth token refresh"
107
107
  kgraph doctor
108
108
  ```
109
109
 
110
+ `kgraph init` now scans once, then prints relevant next steps. When KGraph can detect likely AI tools on the machine, it recommends matching integrations.
111
+
110
112
  After useful AI work, assistants save durable runtime-capture notes into `.kgraph/inbox/`. These notes are not project documentation; they are KGraph input files that the next `kgraph` run processes automatically. You can also process them directly with `kgraph update`.
111
113
 
112
114
  Normal agent flow is intentionally small:
@@ -138,7 +140,7 @@ This is optional. Claude Code can use generated hook scripts for automatic captu
138
140
  kgraph init
139
141
  ```
140
142
 
141
- Required once per repo. Creates `.kgraph/` and the local config.
143
+ Required once per repo. Creates `.kgraph/`, writes the local config, runs the first scan, and prints suggested next actions based on the detected repo languages and likely local AI tools.
142
144
 
143
145
  ```bash
144
146
  kgraph init --integrations codex,copilot,cursor,claude-code,gemini,windsurf,cline
@@ -5,6 +5,9 @@ import { scanRepository } from '../../scanner/repo-scanner.js';
5
5
  import { ensureWorkspace } from '../../storage/kgraph-paths.js';
6
6
  import { readMaps, writeMaps } from '../../storage/map-store.js';
7
7
  import { KGraphError, runCommand } from '../errors.js';
8
+ import { promptForInitIntegrations, shouldPromptForInitIntegrations, } from '../init-prompt.js';
9
+ import { detectMachineIntegrationRecommendations, recommendedIntegrationsForInit, } from '../init-recommendations.js';
10
+ import { renderInitSummary } from '../init-summary.js';
8
11
  export function registerInitCommand(program) {
9
12
  program
10
13
  .command('init')
@@ -27,7 +30,7 @@ export function registerInitCommand(program) {
27
30
  const changed = await addIntegrations(workspace, names, mode);
28
31
  console.log(`Configured integrations: ${changed.map((item) => `${item.name}:${item.mode}`).join(', ')}`);
29
32
  }
30
- const config = await loadConfig(workspace);
33
+ let config = await loadConfig(workspace);
31
34
  const previousMaps = await readMaps(workspace);
32
35
  const result = await scanRepository(workspace.rootPath, config, {
33
36
  files: previousMaps.fileMap.files,
@@ -38,6 +41,32 @@ export function registerInitCommand(program) {
38
41
  });
39
42
  await writeMaps(workspace, result);
40
43
  console.log(`Scanned ${result.files.length} files and ${result.symbols.length} symbols.`);
44
+ const detectedMachineIntegrations = await detectMachineIntegrationRecommendations();
45
+ let recommendedIntegrations = recommendedIntegrationsForInit({
46
+ configuredIntegrations: config.integrations,
47
+ detectedIntegrations: detectedMachineIntegrations,
48
+ });
49
+ if (shouldPromptForInitIntegrations({
50
+ explicitIntegrationsRequested: names.length > 0,
51
+ configuredIntegrations: config.integrations,
52
+ })) {
53
+ const selected = await promptForInitIntegrations(recommendedIntegrations);
54
+ if (selected.length > 0) {
55
+ const changed = await addIntegrations(workspace, selected, 'always');
56
+ console.log(`Configured integrations: ${changed.map((item) => `${item.name}:${item.mode}`).join(', ')}`);
57
+ config = await loadConfig(workspace);
58
+ recommendedIntegrations = recommendedIntegrationsForInit({
59
+ configuredIntegrations: config.integrations,
60
+ detectedIntegrations: detectedMachineIntegrations,
61
+ });
62
+ }
63
+ }
64
+ console.log('');
65
+ console.log(renderInitSummary({
66
+ files: result.files,
67
+ integrations: config.integrations,
68
+ recommendedIntegrations,
69
+ }));
41
70
  }));
42
71
  }
43
72
  function collectOption(value, previous) {
@@ -0,0 +1,8 @@
1
+ import type { IntegrationConfig, IntegrationName } from '../types/config.js';
2
+ import type { InitIntegrationRecommendation } from './init-recommendations.js';
3
+ export declare function shouldPromptForInitIntegrations(options: {
4
+ explicitIntegrationsRequested: boolean;
5
+ configuredIntegrations: Pick<IntegrationConfig, 'name'>[];
6
+ interactive?: boolean;
7
+ }): boolean;
8
+ export declare function promptForInitIntegrations(recommendations: InitIntegrationRecommendation[]): Promise<IntegrationName[]>;
@@ -0,0 +1,61 @@
1
+ import * as clack from '@clack/prompts';
2
+ import { listIntegrationAdapters } from '../integrations/integration-registry.js';
3
+ // --- Integration prompt ---
4
+ export function shouldPromptForInitIntegrations(options) {
5
+ const interactive = options.interactive ?? isInteractiveTerminal();
6
+ return (interactive &&
7
+ !options.explicitIntegrationsRequested &&
8
+ options.configuredIntegrations.length === 0);
9
+ }
10
+ export async function promptForInitIntegrations(recommendations) {
11
+ const recNames = recommendations.map((item) => item.name);
12
+ const action = await clack.select({
13
+ message: 'AI tool integrations',
14
+ options: [
15
+ ...(recommendations.length > 0
16
+ ? [
17
+ {
18
+ value: 'recommended',
19
+ label: `Use recommended (${recNames.join(', ')})`,
20
+ hint: recommendations
21
+ .map((item) => `${item.name} — ${item.reason}`)
22
+ .join('; '),
23
+ },
24
+ ]
25
+ : []),
26
+ { value: 'custom', label: 'Custom selection' },
27
+ { value: 'skip', label: 'Skip' },
28
+ ],
29
+ });
30
+ if (clack.isCancel(action) || action === 'skip') {
31
+ return [];
32
+ }
33
+ if (action === 'recommended') {
34
+ return recNames;
35
+ }
36
+ const recommendedNames = new Set(recNames);
37
+ const otherAdapters = listIntegrationAdapters().filter((adapter) => !recommendedNames.has(adapter.name));
38
+ const allOptions = [
39
+ ...recommendations.map((rec) => ({
40
+ value: rec.name,
41
+ label: rec.name,
42
+ hint: rec.reason,
43
+ })),
44
+ ...otherAdapters.map((adapter) => ({
45
+ value: adapter.name,
46
+ label: adapter.name,
47
+ })),
48
+ ];
49
+ const selected = await clack.multiselect({
50
+ message: 'Select integrations (space to toggle, enter to confirm)',
51
+ options: allOptions,
52
+ required: false,
53
+ });
54
+ if (clack.isCancel(selected)) {
55
+ return [];
56
+ }
57
+ return selected;
58
+ }
59
+ function isInteractiveTerminal() {
60
+ return Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
61
+ }
@@ -0,0 +1,20 @@
1
+ import type { IntegrationConfig, IntegrationName } from '../types/config.js';
2
+ export interface InitIntegrationRecommendation {
3
+ name: IntegrationName;
4
+ reason: string;
5
+ }
6
+ interface MachineDetectionContext {
7
+ env?: NodeJS.ProcessEnv;
8
+ homeDir?: string;
9
+ platform?: NodeJS.Platform;
10
+ exists?: (targetPath: string) => Promise<boolean>;
11
+ readDir?: (targetPath: string) => Promise<string[]>;
12
+ localAppData?: string;
13
+ }
14
+ export declare function detectMachineIntegrationRecommendations(context?: MachineDetectionContext): Promise<InitIntegrationRecommendation[]>;
15
+ export declare function recommendedIntegrationsForInit(options: {
16
+ configuredIntegrations: Pick<IntegrationConfig, 'name'>[];
17
+ detectedIntegrations: InitIntegrationRecommendation[];
18
+ }): InitIntegrationRecommendation[];
19
+ export declare function integrationSetupCommand(recommendations: InitIntegrationRecommendation[]): string | undefined;
20
+ export {};
@@ -0,0 +1,140 @@
1
+ import { readdir } from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { pathExists } from '../storage/kgraph-paths.js';
5
+ export async function detectMachineIntegrationRecommendations(context = {}) {
6
+ const env = context.env ?? process.env;
7
+ if (env.KGRAPH_DISABLE_MACHINE_DETECTION === '1') {
8
+ return [];
9
+ }
10
+ const recommendations = [];
11
+ if ((await hasVsCodeExtension(['github.copilot-', 'github.copilot-chat-'], context)) ||
12
+ (await hasVsCodeBundledCopilot(context))) {
13
+ recommendations.push({
14
+ name: 'copilot',
15
+ reason: 'VS Code Copilot detected',
16
+ });
17
+ }
18
+ if (await hasExecutable('codex', context)) {
19
+ recommendations.push({
20
+ name: 'codex',
21
+ reason: 'codex executable detected on PATH',
22
+ });
23
+ }
24
+ if (await hasExecutable('claude', context)) {
25
+ recommendations.push({
26
+ name: 'claude-code',
27
+ reason: 'claude executable detected on PATH',
28
+ });
29
+ }
30
+ if (await hasExecutable('gemini', context)) {
31
+ recommendations.push({
32
+ name: 'gemini',
33
+ reason: 'gemini executable detected on PATH',
34
+ });
35
+ }
36
+ return recommendations.sort((left, right) => left.name.localeCompare(right.name));
37
+ }
38
+ export function recommendedIntegrationsForInit(options) {
39
+ const configured = new Set(options.configuredIntegrations.map((item) => item.name));
40
+ return options.detectedIntegrations.filter((item) => !configured.has(item.name));
41
+ }
42
+ export function integrationSetupCommand(recommendations) {
43
+ if (recommendations.length === 0) {
44
+ return undefined;
45
+ }
46
+ return `kgraph integrate add ${recommendations.map((item) => item.name).join(' ')}`;
47
+ }
48
+ async function hasVsCodeExtension(prefixes, context) {
49
+ const exists = context.exists ?? pathExists;
50
+ const readDir = context.readDir ?? defaultReadDir;
51
+ const homeDir = context.homeDir ?? os.homedir();
52
+ const candidates = [
53
+ path.join(homeDir, '.vscode', 'extensions'),
54
+ path.join(homeDir, '.vscode-insiders', 'extensions'),
55
+ ];
56
+ for (const candidate of candidates) {
57
+ if (!(await exists(candidate))) {
58
+ continue;
59
+ }
60
+ const entries = await readDir(candidate);
61
+ if (entries.some((entry) => prefixes.some((prefix) => entry.toLowerCase().startsWith(prefix)))) {
62
+ return true;
63
+ }
64
+ }
65
+ return false;
66
+ }
67
+ async function hasVsCodeBundledCopilot(context) {
68
+ const exists = context.exists ?? pathExists;
69
+ const readDir = context.readDir ?? defaultReadDir;
70
+ const platform = context.platform ?? process.platform;
71
+ const env = context.env ?? process.env;
72
+ const dirs = await resolveVsCodeBundledExtensionDirs(platform, env, context.localAppData, exists, readDir);
73
+ for (const dir of dirs) {
74
+ if (await exists(path.join(dir, 'copilot'))) {
75
+ return true;
76
+ }
77
+ }
78
+ return false;
79
+ }
80
+ async function resolveVsCodeBundledExtensionDirs(platform, env, overrideLocalAppData, exists, readDir) {
81
+ const dirs = [];
82
+ if (platform === 'win32') {
83
+ const localAppData = overrideLocalAppData ?? env.LOCALAPPDATA ?? '';
84
+ if (localAppData) {
85
+ const vsCodeRoot = path.join(localAppData, 'Programs', 'Microsoft VS Code');
86
+ if (await exists(vsCodeRoot)) {
87
+ const entries = await readDir(vsCodeRoot);
88
+ for (const entry of entries) {
89
+ dirs.push(path.join(vsCodeRoot, entry, 'resources', 'app', 'extensions'));
90
+ }
91
+ }
92
+ }
93
+ }
94
+ else if (platform === 'darwin') {
95
+ dirs.push('/Applications/Visual Studio Code.app/Contents/Resources/app/extensions');
96
+ dirs.push('/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/extensions');
97
+ }
98
+ else {
99
+ dirs.push('/usr/share/code/resources/app/extensions');
100
+ dirs.push('/usr/lib/code/resources/app/extensions');
101
+ }
102
+ return dirs;
103
+ }
104
+ async function hasExecutable(commandName, context) {
105
+ const env = context.env ?? process.env;
106
+ const exists = context.exists ?? pathExists;
107
+ const platform = context.platform ?? process.platform;
108
+ const pathValue = env.PATH ?? '';
109
+ const directories = pathValue
110
+ .split(path.delimiter)
111
+ .map((item) => item.trim())
112
+ .filter(Boolean);
113
+ if (directories.length === 0) {
114
+ return false;
115
+ }
116
+ const candidates = platform === 'win32'
117
+ ? buildWindowsExecutableCandidates(commandName, env.PATHEXT)
118
+ : [commandName];
119
+ for (const directory of directories) {
120
+ for (const candidate of candidates) {
121
+ if (await exists(path.join(directory, candidate))) {
122
+ return true;
123
+ }
124
+ }
125
+ }
126
+ return false;
127
+ }
128
+ function buildWindowsExecutableCandidates(commandName, pathExt) {
129
+ const extensions = (pathExt ?? '.EXE;.CMD;.BAT;.COM')
130
+ .split(';')
131
+ .map((item) => item.trim())
132
+ .filter(Boolean);
133
+ return [
134
+ commandName,
135
+ ...extensions.map((extension) => `${commandName}${extension}`),
136
+ ];
137
+ }
138
+ async function defaultReadDir(targetPath) {
139
+ return readdir(targetPath);
140
+ }
@@ -0,0 +1,17 @@
1
+ import type { IntegrationConfig } from '../types/config.js';
2
+ import type { RepositoryFile } from '../types/maps.js';
3
+ import { type InitIntegrationRecommendation } from './init-recommendations.js';
4
+ type CoverageLevel = 'deep' | 'basic' | 'generic';
5
+ export interface InitLanguageSummary {
6
+ language: string;
7
+ label: string;
8
+ fileCount: number;
9
+ coverage: CoverageLevel;
10
+ }
11
+ export declare function summarizeInitLanguages(files: RepositoryFile[]): InitLanguageSummary[];
12
+ export declare function renderInitSummary(options: {
13
+ files: RepositoryFile[];
14
+ integrations: Pick<IntegrationConfig, 'name' | 'enabled' | 'mode'>[];
15
+ recommendedIntegrations: InitIntegrationRecommendation[];
16
+ }): string;
17
+ export {};
@@ -0,0 +1,122 @@
1
+ import { integrationSetupCommand, } from './init-recommendations.js';
2
+ const LANGUAGE_PRESENTATION = {
3
+ javascript: { label: 'JavaScript', coverage: 'deep' },
4
+ javascriptreact: { label: 'JavaScript', coverage: 'deep' },
5
+ typescript: { label: 'TypeScript', coverage: 'deep' },
6
+ typescriptreact: { label: 'TypeScript', coverage: 'deep' },
7
+ python: { label: 'Python', coverage: 'deep' },
8
+ go: { label: 'Go', coverage: 'deep' },
9
+ rust: { label: 'Rust', coverage: 'deep' },
10
+ java: { label: 'Java', coverage: 'deep' },
11
+ kotlin: { label: 'Kotlin', coverage: 'deep' },
12
+ c: { label: 'C', coverage: 'deep' },
13
+ cpp: { label: 'C++', coverage: 'deep' },
14
+ csharp: { label: 'C#', coverage: 'deep' },
15
+ yaml: { label: 'YAML', coverage: 'generic' },
16
+ json: { label: 'JSON', coverage: 'generic' },
17
+ toml: { label: 'TOML', coverage: 'generic' },
18
+ xml: { label: 'XML', coverage: 'generic' },
19
+ graphql: { label: 'GraphQL', coverage: 'generic' },
20
+ sql: { label: 'SQL', coverage: 'generic' },
21
+ shell: { label: 'Shell', coverage: 'generic' },
22
+ };
23
+ const EXCLUDED_LANGUAGES = new Set(['unknown', 'markdown', 'restructuredtext']);
24
+ export function summarizeInitLanguages(files) {
25
+ const byLabel = new Map();
26
+ for (const file of files) {
27
+ if (EXCLUDED_LANGUAGES.has(file.language)) {
28
+ continue;
29
+ }
30
+ const descriptor = describeLanguage(file.language);
31
+ const existing = byLabel.get(descriptor.label);
32
+ if (existing) {
33
+ existing.fileCount += 1;
34
+ existing.coverage = moreDetailedCoverage(existing.coverage, descriptor.coverage);
35
+ continue;
36
+ }
37
+ byLabel.set(descriptor.label, {
38
+ language: file.language,
39
+ label: descriptor.label,
40
+ fileCount: 1,
41
+ coverage: descriptor.coverage,
42
+ });
43
+ }
44
+ return [...byLabel.values()].sort((left, right) => {
45
+ if (right.fileCount !== left.fileCount) {
46
+ return right.fileCount - left.fileCount;
47
+ }
48
+ return left.label.localeCompare(right.label);
49
+ });
50
+ }
51
+ export function renderInitSummary(options) {
52
+ const languages = summarizeInitLanguages(options.files);
53
+ const lines = ['KGraph Init Summary', ''];
54
+ lines.push('AI integrations');
55
+ if (options.recommendedIntegrations.length > 0) {
56
+ lines.push(` recommended: ${options.recommendedIntegrations.map((item) => `${item.name} (${item.reason})`).join('; ')}`);
57
+ }
58
+ if (options.integrations.length === 0) {
59
+ lines.push(' configured: none');
60
+ }
61
+ else {
62
+ for (const integration of options.integrations) {
63
+ lines.push(` configured: ${integration.name}: ${integration.enabled ? integration.mode : 'off'}`);
64
+ }
65
+ }
66
+ lines.push('');
67
+ lines.push('Repo languages');
68
+ if (languages.length === 0) {
69
+ lines.push(' none detected yet');
70
+ }
71
+ else {
72
+ for (const language of languages) {
73
+ lines.push(` ${language.label}: ${formatFileCount(language.fileCount)}, ${coverageDescription(language.coverage)}`);
74
+ }
75
+ }
76
+ lines.push('');
77
+ lines.push('Next');
78
+ lines.push(' kgraph "topic" Run the normal refresh and context workflow');
79
+ const integrationCommand = integrationSetupCommand(options.recommendedIntegrations);
80
+ if (integrationCommand) {
81
+ lines.push(` ${integrationCommand} Optional: connect detected AI tools`);
82
+ }
83
+ else if (options.integrations.length === 0) {
84
+ lines.push(' kgraph integrate add <agent> Optional: connect an AI tool');
85
+ }
86
+ lines.push(' kgraph doctor Check workspace health');
87
+ return lines.join('\n');
88
+ }
89
+ function describeLanguage(language) {
90
+ return (LANGUAGE_PRESENTATION[language] ?? {
91
+ label: humanizeLanguage(language),
92
+ coverage: 'generic',
93
+ });
94
+ }
95
+ function humanizeLanguage(language) {
96
+ return language
97
+ .split(/[-_\s]+/)
98
+ .filter(Boolean)
99
+ .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
100
+ .join(' ');
101
+ }
102
+ function moreDetailedCoverage(left, right) {
103
+ const rank = {
104
+ deep: 3,
105
+ basic: 2,
106
+ generic: 1,
107
+ };
108
+ return rank[left] >= rank[right] ? left : right;
109
+ }
110
+ function coverageDescription(coverage) {
111
+ switch (coverage) {
112
+ case 'deep':
113
+ return 'deep built-in extraction';
114
+ case 'basic':
115
+ return 'basic built-in extraction';
116
+ default:
117
+ return 'generic file coverage';
118
+ }
119
+ }
120
+ function formatFileCount(fileCount) {
121
+ return fileCount === 1 ? '1 file' : `${fileCount} files`;
122
+ }
@@ -1,2 +1,2 @@
1
1
  import type { SymbolExtractionResult } from './ts-symbol-extractor.js';
2
- export declare function extractCSymbols(sourceText: string, filePath: string): SymbolExtractionResult;
2
+ export declare function extractCSymbols(sourceText: string, filePath: string): Promise<SymbolExtractionResult>;
@@ -1,14 +1,19 @@
1
+ import { parseSource } from './tree-sitter-parser.js';
2
+ const CPP_EXTS = new Set(['.cpp', '.cc', '.cxx', '.hpp', '.hxx']);
1
3
  // Handles C (.c, .h) and C++ (.cpp, .cc, .cxx, .hpp, .hxx)
2
- export function extractCSymbols(sourceText, filePath) {
3
- const lines = sourceText.split('\n');
4
+ export async function extractCSymbols(sourceText, filePath) {
4
5
  const symbols = [];
5
6
  const dependencies = [];
6
7
  const relationships = [];
7
8
  const warnings = [];
8
- const typeStack = [];
9
- let braceDepth = 0;
10
- const addSymbol = (name, kind, lineNum, parentName) => {
11
- const id = [filePath, kind, parentName, name, lineNum]
9
+ if (!sourceText.trim()) {
10
+ return { symbols, dependencies, relationships, warnings };
11
+ }
12
+ const ext = filePath.substring(filePath.lastIndexOf('.'));
13
+ const grammar = CPP_EXTS.has(ext) ? 'cpp' : 'c';
14
+ const tree = await parseSource(sourceText, grammar);
15
+ const addSymbol = (name, kind, startLine, endLine, parentName) => {
16
+ const id = [filePath, kind, parentName, name, startLine]
12
17
  .filter(Boolean)
13
18
  .join('#');
14
19
  symbols.push({
@@ -16,8 +21,8 @@ export function extractCSymbols(sourceText, filePath) {
16
21
  name,
17
22
  kind,
18
23
  filePath,
19
- startLine: lineNum,
20
- endLine: lineNum,
24
+ startLine,
25
+ endLine,
21
26
  exported: false,
22
27
  parentName,
23
28
  });
@@ -30,67 +35,105 @@ export function extractCSymbols(sourceText, filePath) {
30
35
  confidence: 'high',
31
36
  });
32
37
  };
33
- for (let i = 0; i < lines.length; i++) {
34
- const line = lines[i];
35
- const lineNum = i + 1;
36
- const trimmed = line.trim();
37
- if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('*'))
38
- continue;
39
- braceDepth +=
40
- (line.match(/\{/g) ?? []).length - (line.match(/\}/g) ?? []).length;
41
- while (typeStack.length > 0 &&
42
- braceDepth < typeStack[typeStack.length - 1].braceDepth) {
43
- typeStack.pop();
44
- }
45
- // #include <...> or #include "..."
46
- const includeMatch = trimmed.match(/^#include\s+[<"]([^>"]+)[>"]/);
47
- if (includeMatch) {
48
- const specifier = includeMatch[1];
49
- const kind = trimmed.includes('"') ? 'local' : 'package';
50
- dependencies.push({ fromFile: filePath, specifier, kind });
51
- relationships.push({
52
- sourceType: 'file',
53
- sourceId: filePath,
54
- targetType: kind === 'local' ? 'file' : 'package',
55
- targetId: specifier,
56
- relationshipType: 'import',
57
- confidence: 'high',
58
- });
59
- continue;
38
+ function getFuncName(node) {
39
+ // function_definition has a function_declarator child which contains the identifier
40
+ const declarator = node.childForFieldName('declarator');
41
+ if (!declarator)
42
+ return null;
43
+ if (declarator.type === 'function_declarator') {
44
+ const nameNode = declarator.childForFieldName('declarator');
45
+ return nameNode?.text ?? null;
60
46
  }
61
- // class / struct (C++ with body — has name before {)
62
- const classMatch = trimmed.match(/\b(?:class|struct)\s+(\w+)\s*(?::[^{]*)?\s*\{/);
63
- if (classMatch) {
64
- addSymbol(classMatch[1], 'class', lineNum, typeStack[typeStack.length - 1]?.name);
65
- typeStack.push({ name: classMatch[1], braceDepth });
66
- continue;
47
+ // For pointer_declarator wrapping function_declarator
48
+ const funcDecl = declarator.descendantsOfType('function_declarator')[0];
49
+ if (funcDecl) {
50
+ const nameNode = funcDecl.childForFieldName('declarator');
51
+ return nameNode?.text ?? null;
67
52
  }
68
- // Function definition: returnType funcName( — must have ( and no ; on same line
69
- // Exclude preprocessor, declarations without body
70
- if (!trimmed.endsWith(';') && !trimmed.startsWith('#')) {
71
- const funcMatch = trimmed.match(/\b(\w+)\s*\((?:[^)]*)?\)\s*(?:const\s*)?(?:noexcept\s*)?(?:override\s*)?(?:final\s*)?\{?$/);
72
- // Filter out common false positives: if/for/while/switch/catch/else
73
- const CONTROL_FLOW = new Set([
74
- 'if',
75
- 'for',
76
- 'while',
77
- 'switch',
78
- 'catch',
79
- 'else',
80
- 'return',
81
- 'sizeof',
82
- 'typeof',
83
- ]);
84
- if (funcMatch &&
85
- !CONTROL_FLOW.has(funcMatch[1]) &&
86
- funcMatch[1] !== 'class' &&
87
- funcMatch[1] !== 'struct') {
88
- const parent = typeStack[typeStack.length - 1];
89
- const kind = parent ? 'method' : 'function';
90
- addSymbol(funcMatch[1], kind, lineNum, parent?.name);
91
- continue;
53
+ return null;
54
+ }
55
+ function walk(node, parentClassName) {
56
+ switch (node.type) {
57
+ case 'preproc_include': {
58
+ // #include <...> or #include "..."
59
+ const pathNode = node.namedChildren.find((c) => c.type === 'system_lib_string') ??
60
+ node.namedChildren.find((c) => c.type === 'string_literal');
61
+ if (pathNode) {
62
+ let specifier;
63
+ let kind;
64
+ if (pathNode.type === 'system_lib_string') {
65
+ // <iostream> — strip angle brackets
66
+ specifier = pathNode.text.replace(/^<|>$/g, '');
67
+ kind = 'package';
68
+ }
69
+ else {
70
+ // "myheader.h" — extract string content
71
+ const content = pathNode.namedChildren.find((c) => c.type === 'string_content');
72
+ specifier = content?.text ?? pathNode.text.replace(/^"|"$/g, '');
73
+ kind = 'local';
74
+ }
75
+ dependencies.push({ fromFile: filePath, specifier, kind });
76
+ relationships.push({
77
+ sourceType: 'file',
78
+ sourceId: filePath,
79
+ targetType: kind === 'local' ? 'file' : 'package',
80
+ targetId: specifier,
81
+ relationshipType: 'import',
82
+ confidence: 'high',
83
+ });
84
+ }
85
+ return;
86
+ }
87
+ case 'class_specifier':
88
+ case 'struct_specifier': {
89
+ const nameNode = node.childForFieldName('name');
90
+ if (nameNode) {
91
+ addSymbol(nameNode.text, 'class', node.startPosition.row + 1, node.endPosition.row + 1, parentClassName);
92
+ // Walk body for methods
93
+ const body = node.childForFieldName('body');
94
+ if (body) {
95
+ for (const child of body.namedChildren) {
96
+ walk(child, nameNode.text);
97
+ }
98
+ }
99
+ }
100
+ return;
92
101
  }
102
+ case 'function_definition': {
103
+ const name = getFuncName(node);
104
+ if (name) {
105
+ const kind = parentClassName
106
+ ? 'method'
107
+ : 'function';
108
+ addSymbol(name, kind, node.startPosition.row + 1, node.endPosition.row + 1, parentClassName);
109
+ }
110
+ return;
111
+ }
112
+ case 'declaration': {
113
+ // Could be a function declaration (prototype) inside a class
114
+ if (parentClassName) {
115
+ const declarator = node.childForFieldName('declarator');
116
+ if (declarator) {
117
+ const funcDecl = declarator.type === 'function_declarator'
118
+ ? declarator
119
+ : declarator.descendantsOfType('function_declarator')[0];
120
+ if (funcDecl) {
121
+ const nameNode = funcDecl.childForFieldName('declarator');
122
+ if (nameNode) {
123
+ addSymbol(nameNode.text, 'method', node.startPosition.row + 1, node.endPosition.row + 1, parentClassName);
124
+ }
125
+ return;
126
+ }
127
+ }
128
+ }
129
+ break;
130
+ }
131
+ }
132
+ for (const child of node.namedChildren) {
133
+ walk(child, parentClassName);
93
134
  }
94
135
  }
136
+ walk(tree.rootNode);
137
+ tree.delete();
95
138
  return { symbols, dependencies, relationships, warnings };
96
139
  }
@@ -1,2 +1,2 @@
1
1
  import type { SymbolExtractionResult } from './ts-symbol-extractor.js';
2
- export declare function extractCSharpSymbols(sourceText: string, filePath: string): SymbolExtractionResult;
2
+ export declare function extractCSharpSymbols(sourceText: string, filePath: string): Promise<SymbolExtractionResult>;