@kentwynn/kgraph 0.1.25 → 0.1.26
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 +20 -3
- package/dist/cli/commands/extractor.d.ts +2 -0
- package/dist/cli/commands/extractor.js +50 -0
- package/dist/cli/commands/init.js +53 -1
- package/dist/cli/help.js +5 -0
- package/dist/cli/index.js +2 -0
- package/dist/cli/init-prompt.d.ts +13 -0
- package/dist/cli/init-prompt.js +124 -0
- package/dist/cli/init-recommendations.d.ts +31 -0
- package/dist/cli/init-recommendations.js +163 -0
- package/dist/cli/init-summary.d.ts +19 -0
- package/dist/cli/init-summary.js +147 -0
- package/dist/config/config.js +33 -0
- package/dist/extractors/extractor-registry.d.ts +11 -0
- package/dist/extractors/extractor-registry.js +70 -0
- package/dist/extractors/extractor-store.d.ts +10 -0
- package/dist/extractors/extractor-store.js +58 -0
- package/dist/types/config.d.ts +9 -2
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -100,13 +100,18 @@ kgraph init
|
|
|
100
100
|
# 2. Optional: connect AI tools so they know the KGraph workflow
|
|
101
101
|
kgraph integrate add codex copilot cursor claude-code gemini windsurf cline
|
|
102
102
|
|
|
103
|
-
# 3.
|
|
103
|
+
# 3. Optional: configure deep language extractors for non-TS repos
|
|
104
|
+
kgraph extractor add jvm python
|
|
105
|
+
|
|
106
|
+
# 4. Run the normal workflow for a topic
|
|
104
107
|
kgraph "auth token refresh"
|
|
105
108
|
|
|
106
|
-
#
|
|
109
|
+
# 5. Check health if something feels off
|
|
107
110
|
kgraph doctor
|
|
108
111
|
```
|
|
109
112
|
|
|
113
|
+
`kgraph init` now scans once, then prints relevant next steps. When KGraph can detect likely AI tools on the machine, it recommends matching integrations. When the repository contains languages that only have basic built-in extraction today, it recommends optional deep extractors and prints the exact install/configure commands.
|
|
114
|
+
|
|
110
115
|
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
116
|
|
|
112
117
|
Normal agent flow is intentionally small:
|
|
@@ -138,7 +143,7 @@ This is optional. Claude Code can use generated hook scripts for automatic captu
|
|
|
138
143
|
kgraph init
|
|
139
144
|
```
|
|
140
145
|
|
|
141
|
-
Required once per repo. Creates `.kgraph
|
|
146
|
+
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
147
|
|
|
143
148
|
```bash
|
|
144
149
|
kgraph init --integrations codex,copilot,cursor,claude-code,gemini,windsurf,cline
|
|
@@ -258,6 +263,18 @@ New integrations default to `always` mode because coding agents often under-clas
|
|
|
258
263
|
| Codex | `AGENTS.md`, `.agents/skills/kgraph/SKILL.md` |
|
|
259
264
|
| GitHub Copilot | `.github/copilot-instructions.md`, `.github/prompts/*` |
|
|
260
265
|
| Cursor | `.cursor/rules/kgraph.mdc` |
|
|
266
|
+
|
|
267
|
+
## Optional Deep Extractors
|
|
268
|
+
|
|
269
|
+
KGraph ships with built-in extractors for several languages, but TypeScript and JavaScript still have the deepest built-in analysis today. For languages such as Java, Kotlin, Python, Go, Rust, C/C++, and C#, `kgraph init` can recommend optional deep extractors when those languages are detected in the repository.
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
kgraph extractor list
|
|
273
|
+
kgraph extractor add jvm python
|
|
274
|
+
kgraph extractor remove jvm
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
`extractor add` writes extractor configuration into `.kgraph/config.yaml` and prints the exact `npm install -D ...` command for the matching optional packages. This is explicit on purpose: KGraph recommends install commands by default rather than silently changing package-manager state.
|
|
261
278
|
| Claude Code | `CLAUDE.md`, `.claude/commands/*` |
|
|
262
279
|
| Gemini CLI | `GEMINI.md` |
|
|
263
280
|
| Windsurf | `.windsurf/rules/kgraph.md` |
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { installCommandForExtractors, normalizeExtractorNames, } from '../../extractors/extractor-registry.js';
|
|
2
|
+
import { addExtractors, listExtractors, removeExtractors, } from '../../extractors/extractor-store.js';
|
|
3
|
+
import { assertWorkspace } from '../../storage/kgraph-paths.js';
|
|
4
|
+
import { KGraphError, runCommand } from '../errors.js';
|
|
5
|
+
export function registerExtractorCommand(program) {
|
|
6
|
+
const extractor = program
|
|
7
|
+
.command('extractor')
|
|
8
|
+
.description('Manage optional deep language extractors');
|
|
9
|
+
extractor
|
|
10
|
+
.command('list')
|
|
11
|
+
.description('List configured optional extractors')
|
|
12
|
+
.action(() => runCommand(async () => {
|
|
13
|
+
const workspace = await assertWorkspace(process.cwd());
|
|
14
|
+
const extractors = await listExtractors(workspace);
|
|
15
|
+
if (extractors.length === 0) {
|
|
16
|
+
console.log('No extractors configured.');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
for (const item of extractors) {
|
|
20
|
+
console.log(`${item.name} ${item.enabled ? 'enabled' : 'disabled'} ${item.packageName} ${item.packageInstalled ? 'present' : 'missing'}`);
|
|
21
|
+
}
|
|
22
|
+
}));
|
|
23
|
+
extractor
|
|
24
|
+
.command('add')
|
|
25
|
+
.description('Configure optional deep extractors')
|
|
26
|
+
.argument('<names...>')
|
|
27
|
+
.action((names) => runCommand(async () => {
|
|
28
|
+
const workspace = await assertWorkspace(process.cwd());
|
|
29
|
+
const normalized = normalizeExtractorNames(names);
|
|
30
|
+
if (normalized.length === 0) {
|
|
31
|
+
throw new KGraphError('Provide at least one extractor name.');
|
|
32
|
+
}
|
|
33
|
+
const changed = await addExtractors(workspace, normalized);
|
|
34
|
+
console.log(`Configured extractors: ${changed.map((item) => item.name).join(', ')}`);
|
|
35
|
+
console.log(`Install packages: ${installCommandForExtractors(changed.map((item) => item.packageName))}`);
|
|
36
|
+
}));
|
|
37
|
+
extractor
|
|
38
|
+
.command('remove')
|
|
39
|
+
.description('Remove optional deep extractors')
|
|
40
|
+
.argument('<names...>')
|
|
41
|
+
.action((names) => runCommand(async () => {
|
|
42
|
+
const workspace = await assertWorkspace(process.cwd());
|
|
43
|
+
const normalized = normalizeExtractorNames(names);
|
|
44
|
+
if (normalized.length === 0) {
|
|
45
|
+
throw new KGraphError('Provide at least one extractor name.');
|
|
46
|
+
}
|
|
47
|
+
const removed = await removeExtractors(workspace, normalized);
|
|
48
|
+
console.log(`Removed extractors: ${removed.join(', ')}`);
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { loadConfig, writeDefaultConfig } from '../../config/config.js';
|
|
2
|
+
import { installCommandForExtractors } from '../../extractors/extractor-registry.js';
|
|
3
|
+
import { addExtractors } from '../../extractors/extractor-store.js';
|
|
2
4
|
import { normalizeIntegrationNames } from '../../integrations/integration-registry.js';
|
|
3
5
|
import { addIntegrations } from '../../integrations/integration-store.js';
|
|
4
6
|
import { scanRepository } from '../../scanner/repo-scanner.js';
|
|
5
7
|
import { ensureWorkspace } from '../../storage/kgraph-paths.js';
|
|
6
8
|
import { readMaps, writeMaps } from '../../storage/map-store.js';
|
|
7
9
|
import { KGraphError, runCommand } from '../errors.js';
|
|
10
|
+
import { promptForInitExtractors, promptForInitIntegrations, shouldPromptForInitExtractors, shouldPromptForInitIntegrations, } from '../init-prompt.js';
|
|
11
|
+
import { detectMachineIntegrationRecommendations, recommendedExtractorsForInit, recommendedIntegrationsForInit, } from '../init-recommendations.js';
|
|
12
|
+
import { renderInitSummary } from '../init-summary.js';
|
|
8
13
|
export function registerInitCommand(program) {
|
|
9
14
|
program
|
|
10
15
|
.command('init')
|
|
@@ -27,7 +32,7 @@ export function registerInitCommand(program) {
|
|
|
27
32
|
const changed = await addIntegrations(workspace, names, mode);
|
|
28
33
|
console.log(`Configured integrations: ${changed.map((item) => `${item.name}:${item.mode}`).join(', ')}`);
|
|
29
34
|
}
|
|
30
|
-
|
|
35
|
+
let config = await loadConfig(workspace);
|
|
31
36
|
const previousMaps = await readMaps(workspace);
|
|
32
37
|
const result = await scanRepository(workspace.rootPath, config, {
|
|
33
38
|
files: previousMaps.fileMap.files,
|
|
@@ -38,6 +43,53 @@ export function registerInitCommand(program) {
|
|
|
38
43
|
});
|
|
39
44
|
await writeMaps(workspace, result);
|
|
40
45
|
console.log(`Scanned ${result.files.length} files and ${result.symbols.length} symbols.`);
|
|
46
|
+
const detectedMachineIntegrations = await detectMachineIntegrationRecommendations();
|
|
47
|
+
let recommendedIntegrations = recommendedIntegrationsForInit({
|
|
48
|
+
configuredIntegrations: config.integrations,
|
|
49
|
+
detectedIntegrations: detectedMachineIntegrations,
|
|
50
|
+
});
|
|
51
|
+
let recommendedExtractors = recommendedExtractorsForInit({
|
|
52
|
+
files: result.files,
|
|
53
|
+
configuredExtractors: config.extractors,
|
|
54
|
+
});
|
|
55
|
+
if (shouldPromptForInitIntegrations({
|
|
56
|
+
explicitIntegrationsRequested: names.length > 0,
|
|
57
|
+
configuredIntegrations: config.integrations,
|
|
58
|
+
})) {
|
|
59
|
+
const selected = await promptForInitIntegrations(recommendedIntegrations);
|
|
60
|
+
if (selected.length > 0) {
|
|
61
|
+
const changed = await addIntegrations(workspace, selected, 'always');
|
|
62
|
+
console.log(`Configured integrations: ${changed.map((item) => `${item.name}:${item.mode}`).join(', ')}`);
|
|
63
|
+
config = await loadConfig(workspace);
|
|
64
|
+
recommendedIntegrations = recommendedIntegrationsForInit({
|
|
65
|
+
configuredIntegrations: config.integrations,
|
|
66
|
+
detectedIntegrations: detectedMachineIntegrations,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (shouldPromptForInitExtractors({
|
|
71
|
+
configuredExtractors: config.extractors,
|
|
72
|
+
})) {
|
|
73
|
+
const selected = await promptForInitExtractors(recommendedExtractors);
|
|
74
|
+
if (selected.length > 0) {
|
|
75
|
+
const changed = await addExtractors(workspace, selected);
|
|
76
|
+
console.log(`Configured extractors: ${changed.map((item) => item.name).join(', ')}`);
|
|
77
|
+
console.log(`Install packages: ${installCommandForExtractors(changed.map((item) => item.packageName))}`);
|
|
78
|
+
config = await loadConfig(workspace);
|
|
79
|
+
recommendedExtractors = recommendedExtractorsForInit({
|
|
80
|
+
files: result.files,
|
|
81
|
+
configuredExtractors: config.extractors,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
console.log('');
|
|
86
|
+
console.log(renderInitSummary({
|
|
87
|
+
files: result.files,
|
|
88
|
+
integrations: config.integrations,
|
|
89
|
+
recommendedIntegrations,
|
|
90
|
+
extractors: config.extractors,
|
|
91
|
+
recommendedExtractors,
|
|
92
|
+
}));
|
|
41
93
|
}));
|
|
42
94
|
}
|
|
43
95
|
function collectOption(value, previous) {
|
package/dist/cli/help.js
CHANGED
|
@@ -47,6 +47,11 @@ export function renderRootHelp(useColor = supportsColor()) {
|
|
|
47
47
|
command('integrate remove cursor', 'Remove KGraph-managed instruction blocks'),
|
|
48
48
|
command('--mode smart|always|manual|off', 'Control automatic KGraph involvement per integration'),
|
|
49
49
|
'',
|
|
50
|
+
theme.bold('Extractors'),
|
|
51
|
+
command('extractor list', 'Show configured optional deep extractors'),
|
|
52
|
+
command('extractor add jvm python', 'Configure optional deep extractors and print install commands'),
|
|
53
|
+
command('extractor remove jvm', 'Remove extractor configuration'),
|
|
54
|
+
'',
|
|
50
55
|
theme.bold('Options'),
|
|
51
56
|
command('-V, --version', 'Show version'),
|
|
52
57
|
command('-h, --help', 'Show this help'),
|
package/dist/cli/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { createRequire } from 'node:module';
|
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { registerContextCommand } from './commands/context.js';
|
|
7
7
|
import { registerDoctorCommand } from './commands/doctor.js';
|
|
8
|
+
import { registerExtractorCommand } from './commands/extractor.js';
|
|
8
9
|
import { registerHistoryCommand } from './commands/history.js';
|
|
9
10
|
import { registerImpactCommand } from './commands/impact.js';
|
|
10
11
|
import { registerInitCommand } from './commands/init.js';
|
|
@@ -43,6 +44,7 @@ export function createProgram() {
|
|
|
43
44
|
registerUpdateCommand(program);
|
|
44
45
|
registerContextCommand(program);
|
|
45
46
|
registerImpactCommand(program);
|
|
47
|
+
registerExtractorCommand(program);
|
|
46
48
|
registerIntegrateCommand(program);
|
|
47
49
|
registerVisualizeCommand(program);
|
|
48
50
|
registerHistoryCommand(program);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ExtractorConfig, ExtractorName, IntegrationConfig, IntegrationName } from '../types/config.js';
|
|
2
|
+
import type { InitExtractorRecommendation, 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[]>;
|
|
9
|
+
export declare function shouldPromptForInitExtractors(options: {
|
|
10
|
+
configuredExtractors: Pick<ExtractorConfig, 'name'>[];
|
|
11
|
+
interactive?: boolean;
|
|
12
|
+
}): boolean;
|
|
13
|
+
export declare function promptForInitExtractors(recommendations: InitExtractorRecommendation[]): Promise<ExtractorName[]>;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import * as clack from '@clack/prompts';
|
|
2
|
+
import { listExtractorAdapters } from '../extractors/extractor-registry.js';
|
|
3
|
+
import { listIntegrationAdapters } from '../integrations/integration-registry.js';
|
|
4
|
+
// --- Integration prompt ---
|
|
5
|
+
export function shouldPromptForInitIntegrations(options) {
|
|
6
|
+
const interactive = options.interactive ?? isInteractiveTerminal();
|
|
7
|
+
return (interactive &&
|
|
8
|
+
!options.explicitIntegrationsRequested &&
|
|
9
|
+
options.configuredIntegrations.length === 0);
|
|
10
|
+
}
|
|
11
|
+
export async function promptForInitIntegrations(recommendations) {
|
|
12
|
+
const recNames = recommendations.map((item) => item.name);
|
|
13
|
+
const action = await clack.select({
|
|
14
|
+
message: 'AI tool integrations',
|
|
15
|
+
options: [
|
|
16
|
+
...(recommendations.length > 0
|
|
17
|
+
? [
|
|
18
|
+
{
|
|
19
|
+
value: 'recommended',
|
|
20
|
+
label: `Use recommended (${recNames.join(', ')})`,
|
|
21
|
+
hint: recommendations
|
|
22
|
+
.map((item) => `${item.name} — ${item.reason}`)
|
|
23
|
+
.join('; '),
|
|
24
|
+
},
|
|
25
|
+
]
|
|
26
|
+
: []),
|
|
27
|
+
{ value: 'custom', label: 'Custom selection' },
|
|
28
|
+
{ value: 'skip', label: 'Skip' },
|
|
29
|
+
],
|
|
30
|
+
});
|
|
31
|
+
if (clack.isCancel(action) || action === 'skip') {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
if (action === 'recommended') {
|
|
35
|
+
return recNames;
|
|
36
|
+
}
|
|
37
|
+
const recommendedNames = new Set(recNames);
|
|
38
|
+
const otherAdapters = listIntegrationAdapters().filter((adapter) => !recommendedNames.has(adapter.name));
|
|
39
|
+
const allOptions = [
|
|
40
|
+
...recommendations.map((rec) => ({
|
|
41
|
+
value: rec.name,
|
|
42
|
+
label: rec.name,
|
|
43
|
+
hint: rec.reason,
|
|
44
|
+
})),
|
|
45
|
+
...otherAdapters.map((adapter) => ({
|
|
46
|
+
value: adapter.name,
|
|
47
|
+
label: adapter.name,
|
|
48
|
+
})),
|
|
49
|
+
];
|
|
50
|
+
const selected = await clack.multiselect({
|
|
51
|
+
message: 'Select integrations (space to toggle, enter to confirm)',
|
|
52
|
+
options: allOptions,
|
|
53
|
+
required: false,
|
|
54
|
+
});
|
|
55
|
+
if (clack.isCancel(selected)) {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
return selected;
|
|
59
|
+
}
|
|
60
|
+
// --- Extractor prompt ---
|
|
61
|
+
export function shouldPromptForInitExtractors(options) {
|
|
62
|
+
const interactive = options.interactive ?? isInteractiveTerminal();
|
|
63
|
+
return interactive && options.configuredExtractors.length === 0;
|
|
64
|
+
}
|
|
65
|
+
export async function promptForInitExtractors(recommendations) {
|
|
66
|
+
const recNames = recommendations.map((item) => item.name);
|
|
67
|
+
const hasRecommendations = recommendations.length > 0;
|
|
68
|
+
const action = await clack.select({
|
|
69
|
+
message: 'Optional deep language extractors',
|
|
70
|
+
options: [
|
|
71
|
+
...(hasRecommendations
|
|
72
|
+
? [
|
|
73
|
+
{
|
|
74
|
+
value: 'recommended',
|
|
75
|
+
label: `Use recommended (${recNames.join(', ')})`,
|
|
76
|
+
hint: recommendations
|
|
77
|
+
.map((item) => `${item.name} for ${item.languages.join(', ')}`)
|
|
78
|
+
.join('; '),
|
|
79
|
+
},
|
|
80
|
+
]
|
|
81
|
+
: []),
|
|
82
|
+
{
|
|
83
|
+
value: 'custom',
|
|
84
|
+
label: 'Custom selection',
|
|
85
|
+
hint: hasRecommendations
|
|
86
|
+
? undefined
|
|
87
|
+
: 'no language-specific extractors detected; pick manually',
|
|
88
|
+
},
|
|
89
|
+
{ value: 'skip', label: 'Skip' },
|
|
90
|
+
],
|
|
91
|
+
});
|
|
92
|
+
if (clack.isCancel(action) || action === 'skip') {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
if (action === 'recommended') {
|
|
96
|
+
return recNames;
|
|
97
|
+
}
|
|
98
|
+
const recommendedNames = new Set(recNames);
|
|
99
|
+
const otherAdapters = listExtractorAdapters().filter((adapter) => !recommendedNames.has(adapter.name));
|
|
100
|
+
const allOptions = [
|
|
101
|
+
...recommendations.map((rec) => ({
|
|
102
|
+
value: rec.name,
|
|
103
|
+
label: rec.name,
|
|
104
|
+
hint: `${rec.languages.join(', ')} — ${rec.packageName} (recommended)`,
|
|
105
|
+
})),
|
|
106
|
+
...otherAdapters.map((adapter) => ({
|
|
107
|
+
value: adapter.name,
|
|
108
|
+
label: adapter.name,
|
|
109
|
+
hint: `${adapter.languages.join(', ')} — ${adapter.packageName}`,
|
|
110
|
+
})),
|
|
111
|
+
];
|
|
112
|
+
const selected = await clack.multiselect({
|
|
113
|
+
message: 'Select extractors (space to toggle, enter to confirm)',
|
|
114
|
+
options: allOptions,
|
|
115
|
+
required: false,
|
|
116
|
+
});
|
|
117
|
+
if (clack.isCancel(selected)) {
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
return selected;
|
|
121
|
+
}
|
|
122
|
+
function isInteractiveTerminal() {
|
|
123
|
+
return Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
|
|
124
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ExtractorConfig, ExtractorName, IntegrationConfig, IntegrationName } from '../types/config.js';
|
|
2
|
+
import type { RepositoryFile } from '../types/maps.js';
|
|
3
|
+
export interface InitIntegrationRecommendation {
|
|
4
|
+
name: IntegrationName;
|
|
5
|
+
reason: string;
|
|
6
|
+
}
|
|
7
|
+
export interface InitExtractorRecommendation {
|
|
8
|
+
name: ExtractorName;
|
|
9
|
+
packageName: string;
|
|
10
|
+
languages: string[];
|
|
11
|
+
}
|
|
12
|
+
interface MachineDetectionContext {
|
|
13
|
+
env?: NodeJS.ProcessEnv;
|
|
14
|
+
homeDir?: string;
|
|
15
|
+
platform?: NodeJS.Platform;
|
|
16
|
+
exists?: (targetPath: string) => Promise<boolean>;
|
|
17
|
+
readDir?: (targetPath: string) => Promise<string[]>;
|
|
18
|
+
localAppData?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function detectMachineIntegrationRecommendations(context?: MachineDetectionContext): Promise<InitIntegrationRecommendation[]>;
|
|
21
|
+
export declare function recommendedIntegrationsForInit(options: {
|
|
22
|
+
configuredIntegrations: Pick<IntegrationConfig, 'name'>[];
|
|
23
|
+
detectedIntegrations: InitIntegrationRecommendation[];
|
|
24
|
+
}): InitIntegrationRecommendation[];
|
|
25
|
+
export declare function recommendedExtractorsForInit(options: {
|
|
26
|
+
files: RepositoryFile[];
|
|
27
|
+
configuredExtractors: Pick<ExtractorConfig, 'name'>[];
|
|
28
|
+
}): InitExtractorRecommendation[];
|
|
29
|
+
export declare function integrationSetupCommand(recommendations: InitIntegrationRecommendation[]): string | undefined;
|
|
30
|
+
export declare function extractorSetupCommands(recommendations: InitExtractorRecommendation[]): string[];
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { readdir } from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { installCommandForExtractors, listExtractorAdapters, } from '../extractors/extractor-registry.js';
|
|
5
|
+
import { pathExists } from '../storage/kgraph-paths.js';
|
|
6
|
+
export async function detectMachineIntegrationRecommendations(context = {}) {
|
|
7
|
+
const env = context.env ?? process.env;
|
|
8
|
+
if (env.KGRAPH_DISABLE_MACHINE_DETECTION === '1') {
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
const recommendations = [];
|
|
12
|
+
if ((await hasVsCodeExtension(['github.copilot-', 'github.copilot-chat-'], context)) ||
|
|
13
|
+
(await hasVsCodeBundledCopilot(context))) {
|
|
14
|
+
recommendations.push({
|
|
15
|
+
name: 'copilot',
|
|
16
|
+
reason: 'VS Code Copilot detected',
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
if (await hasExecutable('codex', context)) {
|
|
20
|
+
recommendations.push({
|
|
21
|
+
name: 'codex',
|
|
22
|
+
reason: 'codex executable detected on PATH',
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
if (await hasExecutable('claude', context)) {
|
|
26
|
+
recommendations.push({
|
|
27
|
+
name: 'claude-code',
|
|
28
|
+
reason: 'claude executable detected on PATH',
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
if (await hasExecutable('gemini', context)) {
|
|
32
|
+
recommendations.push({
|
|
33
|
+
name: 'gemini',
|
|
34
|
+
reason: 'gemini executable detected on PATH',
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return recommendations.sort((left, right) => left.name.localeCompare(right.name));
|
|
38
|
+
}
|
|
39
|
+
export function recommendedIntegrationsForInit(options) {
|
|
40
|
+
const configured = new Set(options.configuredIntegrations.map((item) => item.name));
|
|
41
|
+
return options.detectedIntegrations.filter((item) => !configured.has(item.name));
|
|
42
|
+
}
|
|
43
|
+
export function recommendedExtractorsForInit(options) {
|
|
44
|
+
const configured = new Set(options.configuredExtractors.map((item) => item.name));
|
|
45
|
+
const detectedLanguages = new Set(options.files.map((file) => file.language));
|
|
46
|
+
return listExtractorAdapters()
|
|
47
|
+
.filter((adapter) => !configured.has(adapter.name))
|
|
48
|
+
.map((adapter) => ({
|
|
49
|
+
name: adapter.name,
|
|
50
|
+
packageName: adapter.packageName,
|
|
51
|
+
languages: adapter.languages.filter((language) => detectedLanguages.has(language)),
|
|
52
|
+
}))
|
|
53
|
+
.filter((adapter) => adapter.languages.length > 0)
|
|
54
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
55
|
+
}
|
|
56
|
+
export function integrationSetupCommand(recommendations) {
|
|
57
|
+
if (recommendations.length === 0) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
return `kgraph integrate add ${recommendations.map((item) => item.name).join(' ')}`;
|
|
61
|
+
}
|
|
62
|
+
export function extractorSetupCommands(recommendations) {
|
|
63
|
+
if (recommendations.length === 0) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
return [
|
|
67
|
+
`kgraph extractor add ${recommendations.map((item) => item.name).join(' ')}`,
|
|
68
|
+
installCommandForExtractors(recommendations.map((item) => item.packageName)),
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
async function hasVsCodeExtension(prefixes, context) {
|
|
72
|
+
const exists = context.exists ?? pathExists;
|
|
73
|
+
const readDir = context.readDir ?? defaultReadDir;
|
|
74
|
+
const homeDir = context.homeDir ?? os.homedir();
|
|
75
|
+
const candidates = [
|
|
76
|
+
path.join(homeDir, '.vscode', 'extensions'),
|
|
77
|
+
path.join(homeDir, '.vscode-insiders', 'extensions'),
|
|
78
|
+
];
|
|
79
|
+
for (const candidate of candidates) {
|
|
80
|
+
if (!(await exists(candidate))) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const entries = await readDir(candidate);
|
|
84
|
+
if (entries.some((entry) => prefixes.some((prefix) => entry.toLowerCase().startsWith(prefix)))) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
async function hasVsCodeBundledCopilot(context) {
|
|
91
|
+
const exists = context.exists ?? pathExists;
|
|
92
|
+
const readDir = context.readDir ?? defaultReadDir;
|
|
93
|
+
const platform = context.platform ?? process.platform;
|
|
94
|
+
const env = context.env ?? process.env;
|
|
95
|
+
const dirs = await resolveVsCodeBundledExtensionDirs(platform, env, context.localAppData, exists, readDir);
|
|
96
|
+
for (const dir of dirs) {
|
|
97
|
+
if (await exists(path.join(dir, 'copilot'))) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
async function resolveVsCodeBundledExtensionDirs(platform, env, overrideLocalAppData, exists, readDir) {
|
|
104
|
+
const dirs = [];
|
|
105
|
+
if (platform === 'win32') {
|
|
106
|
+
const localAppData = overrideLocalAppData ?? env.LOCALAPPDATA ?? '';
|
|
107
|
+
if (localAppData) {
|
|
108
|
+
const vsCodeRoot = path.join(localAppData, 'Programs', 'Microsoft VS Code');
|
|
109
|
+
if (await exists(vsCodeRoot)) {
|
|
110
|
+
const entries = await readDir(vsCodeRoot);
|
|
111
|
+
for (const entry of entries) {
|
|
112
|
+
dirs.push(path.join(vsCodeRoot, entry, 'resources', 'app', 'extensions'));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else if (platform === 'darwin') {
|
|
118
|
+
dirs.push('/Applications/Visual Studio Code.app/Contents/Resources/app/extensions');
|
|
119
|
+
dirs.push('/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/extensions');
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
dirs.push('/usr/share/code/resources/app/extensions');
|
|
123
|
+
dirs.push('/usr/lib/code/resources/app/extensions');
|
|
124
|
+
}
|
|
125
|
+
return dirs;
|
|
126
|
+
}
|
|
127
|
+
async function hasExecutable(commandName, context) {
|
|
128
|
+
const env = context.env ?? process.env;
|
|
129
|
+
const exists = context.exists ?? pathExists;
|
|
130
|
+
const platform = context.platform ?? process.platform;
|
|
131
|
+
const pathValue = env.PATH ?? '';
|
|
132
|
+
const directories = pathValue
|
|
133
|
+
.split(path.delimiter)
|
|
134
|
+
.map((item) => item.trim())
|
|
135
|
+
.filter(Boolean);
|
|
136
|
+
if (directories.length === 0) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
const candidates = platform === 'win32'
|
|
140
|
+
? buildWindowsExecutableCandidates(commandName, env.PATHEXT)
|
|
141
|
+
: [commandName];
|
|
142
|
+
for (const directory of directories) {
|
|
143
|
+
for (const candidate of candidates) {
|
|
144
|
+
if (await exists(path.join(directory, candidate))) {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
function buildWindowsExecutableCandidates(commandName, pathExt) {
|
|
152
|
+
const extensions = (pathExt ?? '.EXE;.CMD;.BAT;.COM')
|
|
153
|
+
.split(';')
|
|
154
|
+
.map((item) => item.trim())
|
|
155
|
+
.filter(Boolean);
|
|
156
|
+
return [
|
|
157
|
+
commandName,
|
|
158
|
+
...extensions.map((extension) => `${commandName}${extension}`),
|
|
159
|
+
];
|
|
160
|
+
}
|
|
161
|
+
async function defaultReadDir(targetPath) {
|
|
162
|
+
return readdir(targetPath);
|
|
163
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ExtractorConfig, IntegrationConfig } from '../types/config.js';
|
|
2
|
+
import type { RepositoryFile } from '../types/maps.js';
|
|
3
|
+
import { type InitExtractorRecommendation, 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
|
+
extractors: Pick<ExtractorConfig, 'name' | 'enabled' | 'packageName'>[];
|
|
17
|
+
recommendedExtractors: InitExtractorRecommendation[];
|
|
18
|
+
}): string;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { extractorSetupCommands, 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: 'basic' },
|
|
8
|
+
go: { label: 'Go', coverage: 'basic' },
|
|
9
|
+
rust: { label: 'Rust', coverage: 'basic' },
|
|
10
|
+
java: { label: 'Java', coverage: 'basic' },
|
|
11
|
+
kotlin: { label: 'Kotlin', coverage: 'basic' },
|
|
12
|
+
c: { label: 'C', coverage: 'basic' },
|
|
13
|
+
cpp: { label: 'C++', coverage: 'basic' },
|
|
14
|
+
csharp: { label: 'C#', coverage: 'basic' },
|
|
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 recommendedExtractorByLanguage = new Map();
|
|
54
|
+
for (const extractor of options.recommendedExtractors) {
|
|
55
|
+
for (const language of extractor.languages) {
|
|
56
|
+
recommendedExtractorByLanguage.set(language, extractor.name);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const lines = ['KGraph Init Summary', ''];
|
|
60
|
+
lines.push('AI integrations');
|
|
61
|
+
if (options.recommendedIntegrations.length > 0) {
|
|
62
|
+
lines.push(` recommended: ${options.recommendedIntegrations.map((item) => `${item.name} (${item.reason})`).join('; ')}`);
|
|
63
|
+
}
|
|
64
|
+
if (options.integrations.length === 0) {
|
|
65
|
+
lines.push(' configured: none');
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
for (const integration of options.integrations) {
|
|
69
|
+
lines.push(` configured: ${integration.name}: ${integration.enabled ? integration.mode : 'off'}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
lines.push('');
|
|
73
|
+
lines.push('Repo languages');
|
|
74
|
+
if (languages.length === 0) {
|
|
75
|
+
lines.push(' none detected yet');
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
for (const language of languages) {
|
|
79
|
+
const recommendedExtractor = recommendedExtractorByLanguage.get(language.language);
|
|
80
|
+
lines.push(` ${language.label}: ${formatFileCount(language.fileCount)}, ${coverageDescription(language.coverage)}${recommendedExtractor ? `; recommended extractor: ${recommendedExtractor}` : ''}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
lines.push('');
|
|
84
|
+
lines.push('Optional extractors');
|
|
85
|
+
if (options.extractors.length === 0) {
|
|
86
|
+
lines.push(' configured: none');
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
for (const extractor of options.extractors) {
|
|
90
|
+
lines.push(` configured: ${extractor.name}: ${extractor.enabled ? extractor.packageName : 'off'}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (options.recommendedExtractors.length > 0) {
|
|
94
|
+
for (const extractor of options.recommendedExtractors) {
|
|
95
|
+
lines.push(` recommended: ${extractor.name} for ${extractor.languages.map(humanizeLanguage).join(', ')} (${extractor.packageName})`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
lines.push('');
|
|
99
|
+
lines.push('Next');
|
|
100
|
+
lines.push(' kgraph "topic" Run the normal refresh and context workflow');
|
|
101
|
+
const integrationCommand = integrationSetupCommand(options.recommendedIntegrations);
|
|
102
|
+
if (integrationCommand) {
|
|
103
|
+
lines.push(` ${integrationCommand} Optional: connect detected AI tools`);
|
|
104
|
+
}
|
|
105
|
+
else if (options.integrations.length === 0) {
|
|
106
|
+
lines.push(' kgraph integrate add <agent> Optional: connect an AI tool');
|
|
107
|
+
}
|
|
108
|
+
for (const command of extractorSetupCommands(options.recommendedExtractors)) {
|
|
109
|
+
lines.push(` ${command}`);
|
|
110
|
+
}
|
|
111
|
+
lines.push(' kgraph doctor Check workspace health');
|
|
112
|
+
return lines.join('\n');
|
|
113
|
+
}
|
|
114
|
+
function describeLanguage(language) {
|
|
115
|
+
return (LANGUAGE_PRESENTATION[language] ?? {
|
|
116
|
+
label: humanizeLanguage(language),
|
|
117
|
+
coverage: 'generic',
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
function humanizeLanguage(language) {
|
|
121
|
+
return language
|
|
122
|
+
.split(/[-_\s]+/)
|
|
123
|
+
.filter(Boolean)
|
|
124
|
+
.map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
|
|
125
|
+
.join(' ');
|
|
126
|
+
}
|
|
127
|
+
function moreDetailedCoverage(left, right) {
|
|
128
|
+
const rank = {
|
|
129
|
+
deep: 3,
|
|
130
|
+
basic: 2,
|
|
131
|
+
generic: 1,
|
|
132
|
+
};
|
|
133
|
+
return rank[left] >= rank[right] ? left : right;
|
|
134
|
+
}
|
|
135
|
+
function coverageDescription(coverage) {
|
|
136
|
+
switch (coverage) {
|
|
137
|
+
case 'deep':
|
|
138
|
+
return 'deep built-in extraction';
|
|
139
|
+
case 'basic':
|
|
140
|
+
return 'basic built-in extraction';
|
|
141
|
+
default:
|
|
142
|
+
return 'generic file coverage';
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function formatFileCount(fileCount) {
|
|
146
|
+
return fileCount === 1 ? '1 file' : `${fileCount} files`;
|
|
147
|
+
}
|
package/dist/config/config.js
CHANGED
|
@@ -71,6 +71,7 @@ export const DEFAULT_CONFIG = {
|
|
|
71
71
|
maxContextItems: 8,
|
|
72
72
|
domainHints: {},
|
|
73
73
|
integrations: [],
|
|
74
|
+
extractors: [],
|
|
74
75
|
};
|
|
75
76
|
export async function writeDefaultConfig(workspace) {
|
|
76
77
|
if (await pathExists(workspace.configPath)) {
|
|
@@ -114,6 +115,7 @@ export function normalizeConfig(config) {
|
|
|
114
115
|
? config.domainHints
|
|
115
116
|
: {},
|
|
116
117
|
integrations: normalizeIntegrations(config.integrations),
|
|
118
|
+
extractors: normalizeExtractors(config.extractors),
|
|
117
119
|
};
|
|
118
120
|
}
|
|
119
121
|
function mergeUnique(base, extra) {
|
|
@@ -161,3 +163,34 @@ function normalizeIntegrationMode(value) {
|
|
|
161
163
|
? value
|
|
162
164
|
: 'smart';
|
|
163
165
|
}
|
|
166
|
+
function normalizeExtractors(value) {
|
|
167
|
+
if (!Array.isArray(value)) {
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
const seen = new Set();
|
|
171
|
+
const extractors = [];
|
|
172
|
+
for (const item of value) {
|
|
173
|
+
if (!item || typeof item !== 'object') {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const candidate = item;
|
|
177
|
+
if (typeof candidate.name !== 'string' ||
|
|
178
|
+
typeof candidate.packageName !== 'string' ||
|
|
179
|
+
seen.has(candidate.name)) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (!isExtractorName(candidate.name)) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
seen.add(candidate.name);
|
|
186
|
+
extractors.push({
|
|
187
|
+
name: candidate.name,
|
|
188
|
+
enabled: candidate.enabled !== false,
|
|
189
|
+
packageName: candidate.packageName,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
return extractors;
|
|
193
|
+
}
|
|
194
|
+
function isExtractorName(value) {
|
|
195
|
+
return ['c-family', 'csharp', 'go', 'jvm', 'python', 'rust'].includes(value);
|
|
196
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ExtractorName } from '../types/config.js';
|
|
2
|
+
export interface ExtractorAdapter {
|
|
3
|
+
name: ExtractorName;
|
|
4
|
+
label: string;
|
|
5
|
+
packageName: string;
|
|
6
|
+
languages: string[];
|
|
7
|
+
}
|
|
8
|
+
export declare function listExtractorAdapters(): ExtractorAdapter[];
|
|
9
|
+
export declare function getExtractorAdapter(name: string): ExtractorAdapter;
|
|
10
|
+
export declare function normalizeExtractorNames(values: string[] | undefined): ExtractorName[];
|
|
11
|
+
export declare function installCommandForExtractors(packageNames: string[]): string;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const ADAPTERS = [
|
|
2
|
+
{
|
|
3
|
+
name: 'python',
|
|
4
|
+
label: 'Python deep extractor',
|
|
5
|
+
packageName: '@kentwynn/kgraph-extractor-python',
|
|
6
|
+
languages: ['python'],
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: 'jvm',
|
|
10
|
+
label: 'JVM deep extractor',
|
|
11
|
+
packageName: '@kentwynn/kgraph-extractor-jvm',
|
|
12
|
+
languages: ['java', 'kotlin'],
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: 'go',
|
|
16
|
+
label: 'Go deep extractor',
|
|
17
|
+
packageName: '@kentwynn/kgraph-extractor-go',
|
|
18
|
+
languages: ['go'],
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'rust',
|
|
22
|
+
label: 'Rust deep extractor',
|
|
23
|
+
packageName: '@kentwynn/kgraph-extractor-rust',
|
|
24
|
+
languages: ['rust'],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'c-family',
|
|
28
|
+
label: 'C/C++ deep extractor',
|
|
29
|
+
packageName: '@kentwynn/kgraph-extractor-c-family',
|
|
30
|
+
languages: ['c', 'cpp'],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'csharp',
|
|
34
|
+
label: 'C# deep extractor',
|
|
35
|
+
packageName: '@kentwynn/kgraph-extractor-csharp',
|
|
36
|
+
languages: ['csharp'],
|
|
37
|
+
},
|
|
38
|
+
].sort((left, right) => left.name.localeCompare(right.name));
|
|
39
|
+
export function listExtractorAdapters() {
|
|
40
|
+
return ADAPTERS;
|
|
41
|
+
}
|
|
42
|
+
export function getExtractorAdapter(name) {
|
|
43
|
+
const adapter = ADAPTERS.find((item) => item.name === name);
|
|
44
|
+
if (!adapter) {
|
|
45
|
+
throw new Error(`Unsupported extractor "${name}". Supported extractors: ${ADAPTERS.map((item) => item.name).join(', ')}`);
|
|
46
|
+
}
|
|
47
|
+
return adapter;
|
|
48
|
+
}
|
|
49
|
+
export function normalizeExtractorNames(values) {
|
|
50
|
+
if (!values || values.length === 0) {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
const names = [];
|
|
54
|
+
const seen = new Set();
|
|
55
|
+
for (const value of values) {
|
|
56
|
+
for (const raw of value.split(/[\s,]+/)) {
|
|
57
|
+
const name = raw.trim();
|
|
58
|
+
if (!name || seen.has(name)) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const adapter = getExtractorAdapter(name);
|
|
62
|
+
seen.add(adapter.name);
|
|
63
|
+
names.push(adapter.name);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return names;
|
|
67
|
+
}
|
|
68
|
+
export function installCommandForExtractors(packageNames) {
|
|
69
|
+
return `npm install -D ${packageNames.join(' ')}`;
|
|
70
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ExtractorConfig, ExtractorName, KGraphWorkspace } from '../types/config.js';
|
|
2
|
+
export interface ExtractorStatus {
|
|
3
|
+
name: ExtractorName;
|
|
4
|
+
enabled: boolean;
|
|
5
|
+
packageName: string;
|
|
6
|
+
packageInstalled: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function listExtractors(workspace: KGraphWorkspace): Promise<ExtractorStatus[]>;
|
|
9
|
+
export declare function addExtractors(workspace: KGraphWorkspace, names: ExtractorName[]): Promise<ExtractorConfig[]>;
|
|
10
|
+
export declare function removeExtractors(workspace: KGraphWorkspace, names: ExtractorName[]): Promise<ExtractorName[]>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { loadConfig, saveConfig } from '../config/config.js';
|
|
4
|
+
import { pathExists } from '../storage/kgraph-paths.js';
|
|
5
|
+
import { getExtractorAdapter } from './extractor-registry.js';
|
|
6
|
+
export async function listExtractors(workspace) {
|
|
7
|
+
const config = await loadConfig(workspace);
|
|
8
|
+
const statuses = await Promise.all(config.extractors.map(async (extractor) => ({
|
|
9
|
+
name: extractor.name,
|
|
10
|
+
enabled: extractor.enabled,
|
|
11
|
+
packageName: extractor.packageName,
|
|
12
|
+
packageInstalled: await isExtractorInstalled(workspace.rootPath, extractor.packageName),
|
|
13
|
+
})));
|
|
14
|
+
return statuses.sort((left, right) => left.name.localeCompare(right.name));
|
|
15
|
+
}
|
|
16
|
+
export async function addExtractors(workspace, names) {
|
|
17
|
+
const config = await loadConfig(workspace);
|
|
18
|
+
const byName = new Map(config.extractors.map((extractor) => [extractor.name, extractor]));
|
|
19
|
+
const changed = [];
|
|
20
|
+
for (const name of names) {
|
|
21
|
+
const adapter = getExtractorAdapter(name);
|
|
22
|
+
const next = {
|
|
23
|
+
name: adapter.name,
|
|
24
|
+
enabled: true,
|
|
25
|
+
packageName: adapter.packageName,
|
|
26
|
+
};
|
|
27
|
+
byName.set(adapter.name, next);
|
|
28
|
+
changed.push(next);
|
|
29
|
+
}
|
|
30
|
+
config.extractors = [...byName.values()].sort((left, right) => left.name.localeCompare(right.name));
|
|
31
|
+
await saveConfig(workspace, config);
|
|
32
|
+
return changed;
|
|
33
|
+
}
|
|
34
|
+
export async function removeExtractors(workspace, names) {
|
|
35
|
+
const config = await loadConfig(workspace);
|
|
36
|
+
const removeNames = new Set(names);
|
|
37
|
+
config.extractors = config.extractors.filter((extractor) => !removeNames.has(extractor.name));
|
|
38
|
+
await saveConfig(workspace, config);
|
|
39
|
+
return [...removeNames].sort((left, right) => left.localeCompare(right));
|
|
40
|
+
}
|
|
41
|
+
async function isExtractorInstalled(rootPath, packageName) {
|
|
42
|
+
const packageJsonPath = path.join(rootPath, 'package.json');
|
|
43
|
+
if (await pathExists(packageJsonPath)) {
|
|
44
|
+
try {
|
|
45
|
+
const raw = await readFile(packageJsonPath, 'utf8');
|
|
46
|
+
const pkg = JSON.parse(raw);
|
|
47
|
+
if (pkg.dependencies?.[packageName] ||
|
|
48
|
+
pkg.devDependencies?.[packageName] ||
|
|
49
|
+
pkg.optionalDependencies?.[packageName]) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return pathExists(path.join(rootPath, 'node_modules', ...packageName.split('/'), 'package.json'));
|
|
58
|
+
}
|
package/dist/types/config.d.ts
CHANGED
|
@@ -7,19 +7,26 @@ export interface KGraphConfig {
|
|
|
7
7
|
maxContextItems: number;
|
|
8
8
|
domainHints: Record<string, DomainHint>;
|
|
9
9
|
integrations: IntegrationConfig[];
|
|
10
|
+
extractors: ExtractorConfig[];
|
|
10
11
|
}
|
|
11
12
|
export interface DomainHint {
|
|
12
13
|
paths?: string[];
|
|
13
14
|
tags?: string[];
|
|
14
15
|
}
|
|
15
|
-
export type IntegrationName =
|
|
16
|
-
export type IntegrationMode =
|
|
16
|
+
export type IntegrationName = 'claude-code' | 'cline' | 'codex' | 'copilot' | 'cursor' | 'gemini' | 'windsurf';
|
|
17
|
+
export type IntegrationMode = 'smart' | 'always' | 'manual' | 'off';
|
|
18
|
+
export type ExtractorName = 'c-family' | 'csharp' | 'go' | 'jvm' | 'python' | 'rust';
|
|
17
19
|
export interface IntegrationConfig {
|
|
18
20
|
name: IntegrationName;
|
|
19
21
|
enabled: boolean;
|
|
20
22
|
mode: IntegrationMode;
|
|
21
23
|
targetPath: string;
|
|
22
24
|
}
|
|
25
|
+
export interface ExtractorConfig {
|
|
26
|
+
name: ExtractorName;
|
|
27
|
+
enabled: boolean;
|
|
28
|
+
packageName: string;
|
|
29
|
+
}
|
|
23
30
|
export interface KGraphWorkspace {
|
|
24
31
|
rootPath: string;
|
|
25
32
|
kgraphPath: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kentwynn/kgraph",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.26",
|
|
4
4
|
"description": "Persistent repo intelligence for AI coding assistants.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
"homepage": "https://github.com/kentwynn/KGraph#readme",
|
|
41
41
|
"dependencies": {
|
|
42
|
+
"@clack/prompts": "^1.3.0",
|
|
42
43
|
"chalk": "^5.6.2",
|
|
43
44
|
"commander": "^12.1.0",
|
|
44
45
|
"fast-glob": "^3.3.2",
|