@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 +3 -1
- package/dist/cli/commands/init.js +30 -1
- package/dist/cli/init-prompt.d.ts +8 -0
- package/dist/cli/init-prompt.js +61 -0
- package/dist/cli/init-recommendations.d.ts +20 -0
- package/dist/cli/init-recommendations.js +140 -0
- package/dist/cli/init-summary.d.ts +17 -0
- package/dist/cli/init-summary.js +122 -0
- package/dist/scanner/c-symbol-extractor.d.ts +1 -1
- package/dist/scanner/c-symbol-extractor.js +108 -65
- package/dist/scanner/csharp-symbol-extractor.d.ts +1 -1
- package/dist/scanner/csharp-symbol-extractor.js +93 -67
- package/dist/scanner/go-symbol-extractor.d.ts +1 -1
- package/dist/scanner/go-symbol-extractor.js +75 -60
- package/dist/scanner/jvm-symbol-extractor.d.ts +1 -1
- package/dist/scanner/jvm-symbol-extractor.js +139 -71
- package/dist/scanner/python-symbol-extractor.d.ts +1 -1
- package/dist/scanner/python-symbol-extractor.js +92 -71
- package/dist/scanner/repo-scanner.js +3 -3
- package/dist/scanner/rust-symbol-extractor.d.ts +1 -1
- package/dist/scanner/rust-symbol-extractor.js +94 -89
- package/dist/scanner/tree-sitter-parser.d.ts +5 -0
- package/dist/scanner/tree-sitter-parser.js +55 -0
- package/dist/types/config.d.ts +2 -2
- package/package.json +11 -1
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
|
|
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
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
20
|
-
endLine
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
//
|
|
62
|
-
const
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
'
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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>;
|