@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 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. Run the normal workflow for a topic
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
- # 4. Check health if something feels off
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/` and the local config.
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,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerExtractorCommand(program: Command): void;
@@ -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
- const config = await loadConfig(workspace);
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
+ }
@@ -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
+ }
@@ -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 = "claude-code" | "cline" | "codex" | "copilot" | "cursor" | "gemini" | "windsurf";
16
- export type IntegrationMode = "smart" | "always" | "manual" | "off";
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.25",
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",