@kentwynn/kgraph 0.1.27 → 0.2.1

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
@@ -62,6 +62,7 @@ kgraph "blog admin token usage"
62
62
  ```
63
63
 
64
64
  Instead of reading the whole repo, it gets a compact starting point: relevant files, symbols, relationships, domains, prior notes, and stale references to watch.
65
+ Each context item explains why it was returned, such as a path/name match, a matched cognition reference, a domain match, or a nearby import relationship.
65
66
 
66
67
  When you need change impact instead of broad context:
67
68
 
@@ -103,7 +104,7 @@ kgraph integrate add codex copilot cursor claude-code gemini windsurf cline
103
104
  # 3. Run the normal workflow for a topic
104
105
  kgraph "auth token refresh"
105
106
 
106
- # 4. Check health if something feels off
107
+ # 4. Verify the setup and use doctor as the quality gate
107
108
  kgraph doctor
108
109
  ```
109
110
 
@@ -120,7 +121,7 @@ kgraph "topic"
120
121
  kgraph
121
122
  ```
122
123
 
123
- Use `kgraph doctor --quality` and `kgraph repair --dry-run` only when stale or noisy cognition references start making context harder to trust.
124
+ Use `kgraph doctor` after setup and before trusting a repo's saved intelligence. It checks initialization, maps, pending inbox notes, integration targets, and actionable quality problems. Use `kgraph doctor --quality` and `kgraph repair --dry-run` when stale or noisy cognition references start making context harder to trust.
124
125
 
125
126
  Agents can also report session activity so KGraph can estimate token waste:
126
127
 
@@ -167,6 +168,8 @@ kgraph doctor --quality
167
168
 
168
169
  Checks whether the workspace is initialized, maps exist, inbox notes are pending, and configured integrations point to real files. Use `--quality` when context shows stale/noisy cognition references, unresolved local imports, unresolved call edges, duplicate cognition titles, or generated files in the scan.
169
170
 
171
+ The default doctor result is the main quality gate. It fails on actionable hygiene issues such as stale/noisy cognition, duplicate cognition titles, generated integration files leaking into scans, missing maps, or broken integration targets. Scanner coverage counts such as unresolved local imports or unresolved call edges remain visible in `--quality`, but they do not fail the gate by themselves because they often reflect current parser limits.
172
+
170
173
  ```bash
171
174
  kgraph repair --dry-run
172
175
  kgraph repair
@@ -174,6 +177,14 @@ kgraph repair
174
177
 
175
178
  `repair --dry-run` previews cleanup for noisy cognition references, such as framework names recorded as files or local variables recorded as symbols. `repair` applies only the safe noisy-reference cleanup; broader quality findings stay report-only. Run repair intentionally when stale references make context noisy; it is not part of every normal workflow.
176
179
 
180
+ ```bash
181
+ kgraph uninstall
182
+ kgraph uninstall --yes
183
+ kgraph uninstall --keep-integrations --yes
184
+ ```
185
+
186
+ `uninstall` previews repo-local removal and does not delete anything unless `--yes` is passed. `uninstall --yes` removes `.kgraph/` and KGraph-managed integration blocks/files while preserving source files and user-authored text outside managed blocks. Use `--keep-integrations --yes` to remove only `.kgraph/` while leaving AI tool instruction files in place. After uninstalling, `kgraph init` can be run again for a fresh setup.
187
+
177
188
  ```bash
178
189
  kgraph impact "Button"
179
190
  kgraph impact "createSession" --json
@@ -192,6 +203,7 @@ kgraph session end --agent codex
192
203
  ```
193
204
 
194
205
  Track agent-reported read/write activity, repeated reads, and estimated token cost. Supported agents are `codex`, `claude-code`, `copilot`, `cursor`, `gemini`, `windsurf`, and `cline`.
206
+ The text report now includes next actions, such as using `kgraph context "<topic>"` before repeated broad file inspection.
195
207
 
196
208
  ## Optional Step Commands
197
209
 
@@ -209,6 +221,7 @@ kgraph context "auth token refresh" --json
209
221
  ```
210
222
 
211
223
  Return context from existing maps and cognition without scanning or updating first.
224
+ Markdown output includes the reason each file, symbol, cognition note, nearby symbol, or relationship was selected. Use `--json` when an agent or script needs the same explanation data programmatically.
212
225
 
213
226
  ```bash
214
227
  kgraph update
@@ -1,4 +1,4 @@
1
- import type { Command } from "commander";
2
- import type { ContextResponse } from "../../types/cognition.js";
1
+ import type { Command } from 'commander';
2
+ import type { ContextResponse } from '../../types/cognition.js';
3
3
  export declare function registerContextCommand(program: Command): void;
4
4
  export declare function renderContextMarkdown(response: ContextResponse): string;
@@ -1,43 +1,142 @@
1
- import { loadConfig } from "../../config/config.js";
2
- import { queryContext } from "../../context/context-query.js";
3
- import { assertWorkspace } from "../../storage/kgraph-paths.js";
4
- import { mapsExist, readMaps } from "../../storage/map-store.js";
5
- import { KGraphError, runCommand } from "../errors.js";
1
+ import { loadConfig } from '../../config/config.js';
2
+ import { queryContext } from '../../context/context-query.js';
3
+ import { assertWorkspace } from '../../storage/kgraph-paths.js';
4
+ import { mapsExist, readMaps } from '../../storage/map-store.js';
5
+ import { KGraphError, runCommand } from '../errors.js';
6
6
  export function registerContextCommand(program) {
7
7
  program
8
- .command("context <query>")
9
- .description("Return compact repo context for a query")
10
- .option("--json", "Print JSON output")
8
+ .command('context <query>')
9
+ .description('Return compact repo context for a query')
10
+ .option('--json', 'Print JSON output')
11
11
  .action((query, options) => runCommand(async () => {
12
12
  if (!query.trim()) {
13
- throw new KGraphError("Query cannot be empty.");
13
+ throw new KGraphError('Query cannot be empty.');
14
14
  }
15
15
  const workspace = await assertWorkspace(process.cwd());
16
16
  if (!(await mapsExist(workspace))) {
17
- throw new KGraphError("KGraph maps are missing. Run `kgraph scan` first.");
17
+ throw new KGraphError('KGraph maps are missing. Run `kgraph scan` first.');
18
18
  }
19
19
  const config = await loadConfig(workspace);
20
20
  const maps = await readMaps(workspace);
21
21
  const response = await queryContext(workspace, config, maps, query);
22
- console.log(options.json ? JSON.stringify(response, null, 2) : renderContextMarkdown(response));
22
+ console.log(options.json
23
+ ? JSON.stringify(response, null, 2)
24
+ : renderContextMarkdown(response));
23
25
  }));
24
26
  }
25
27
  export function renderContextMarkdown(response) {
26
28
  const lines = [`# KGraph Context`, ``, `Query: ${response.query}`, ``];
27
- lines.push("## Matched Domains", "");
28
- lines.push(...formatList(response.matchedDomains.map((item) => `- ${item.item.name} (${item.reasons.join(", ")})`)));
29
- lines.push("", "## Relevant Files", "");
30
- lines.push(...formatList(response.relevantFiles.map((item) => `- ${item.item.path} (${item.reasons.join(", ")})`)));
31
- lines.push("", "## Relevant Symbols", "");
32
- lines.push(...formatList(response.relevantSymbols.map((item) => `- ${item.item.name} in ${item.item.filePath}`)));
33
- lines.push("", "## Relevant Cognition", "");
34
- lines.push(...formatList(response.relevantCognition.map((item) => `- ${item.item.title} [${item.item.referencesStatus}]`)));
35
- lines.push("", "## Relationships", "");
36
- lines.push(...formatList(response.relationships.map((relationship) => `- ${relationship.sourceId} ${relationship.relationshipType} ${relationship.targetId} (${relationship.confidence})`)));
37
- lines.push("", "## Stale References", "");
29
+ lines.push('## Matched Domains', '');
30
+ lines.push(...formatList(response.matchedDomains.map((item) => `- ${item.item.name} because ${formatReasons(item.reasons)}`)));
31
+ lines.push('', '## Relevant Files', '');
32
+ lines.push(...formatList(response.relevantFiles.map((item) => {
33
+ const f = item.item;
34
+ const meta = [
35
+ f.language,
36
+ f.tokenEstimate ? `~${f.tokenEstimate} tokens` : '',
37
+ ]
38
+ .filter(Boolean)
39
+ .join(', ');
40
+ return `- ${f.path}${meta ? ` [${meta}]` : ''} because ${formatReasons(item.reasons)}`;
41
+ })));
42
+ lines.push('', '## Relevant Symbols', '');
43
+ lines.push(...formatList(response.relevantSymbols.map((item) => {
44
+ const s = item.item;
45
+ const kindInfo = [s.kind, s.parentName].filter(Boolean).join(', ');
46
+ const lineRange = s.startLine != null && s.endLine != null
47
+ ? `:${s.startLine}-${s.endLine}`
48
+ : '';
49
+ return `- ${s.name} (${kindInfo}) in ${s.filePath}${lineRange} because ${formatReasons(item.reasons)}`;
50
+ })));
51
+ lines.push('', '## Relevant Cognition', '');
52
+ lines.push(...formatList(response.relevantCognition.map((item) => `- ${item.item.title} [${item.item.referencesStatus}] because ${formatReasons(item.reasons)}`)));
53
+ lines.push('', '## Relationships', '');
54
+ lines.push(...formatGroupedRelationships(response.relationships, response.relationshipExplanations));
55
+ lines.push('', '## Nearby Symbols (1-hop imports)', '');
56
+ lines.push(...formatList(nearbySymbolItems(response).map(({ symbol: s, reasons }) => {
57
+ const kindInfo = [s.kind, s.parentName].filter(Boolean).join(', ');
58
+ const lineRange = s.startLine != null && s.endLine != null
59
+ ? `:${s.startLine}-${s.endLine}`
60
+ : '';
61
+ return `- ${s.name} (${kindInfo}) in ${s.filePath}${lineRange} because ${formatReasons(reasons)}`;
62
+ })));
63
+ lines.push('', '## Stale References', '');
38
64
  lines.push(...formatList(response.staleReferences.map((ref) => `- ${ref}`)));
39
- return lines.join("\n");
65
+ return lines.join('\n');
66
+ }
67
+ function formatGroupedRelationships(relationships, explanations) {
68
+ const reasonsByRelationship = new Map((explanations ?? []).map((item) => [
69
+ relationshipKey(item.relationship),
70
+ item.reasons,
71
+ ]));
72
+ const imports = relationships.filter((r) => r.relationshipType === 'import');
73
+ const calls = relationships.filter((r) => r.relationshipType === 'calls');
74
+ const contains = relationships.filter((r) => r.relationshipType === 'symbol-contains');
75
+ const other = relationships.filter((r) => r.relationshipType !== 'import' &&
76
+ r.relationshipType !== 'calls' &&
77
+ r.relationshipType !== 'symbol-contains' &&
78
+ r.relationshipType !== 'mentions' &&
79
+ r.relationshipType !== 'belongs-to-domain' &&
80
+ r.relationshipType !== 'stale-reference');
81
+ const lines = [];
82
+ if (imports.length > 0) {
83
+ lines.push('Imports:');
84
+ for (const r of imports) {
85
+ lines.push(` ${r.sourceId} → ${r.targetId}${formatRelationshipReason(r, reasonsByRelationship)}`);
86
+ }
87
+ }
88
+ if (calls.length > 0) {
89
+ lines.push('Calls:');
90
+ for (const r of calls) {
91
+ lines.push(` ${r.sourceId} → ${r.targetId}${formatRelationshipReason(r, reasonsByRelationship)}`);
92
+ }
93
+ }
94
+ if (contains.length > 0) {
95
+ lines.push('Contains:');
96
+ for (const r of contains) {
97
+ lines.push(` ${r.sourceId} contains ${r.targetId}${formatRelationshipReason(r, reasonsByRelationship)}`);
98
+ }
99
+ }
100
+ if (other.length > 0) {
101
+ lines.push('Other:');
102
+ for (const r of other) {
103
+ lines.push(` ${r.sourceId} ${r.relationshipType} ${r.targetId}${formatRelationshipReason(r, reasonsByRelationship)}`);
104
+ }
105
+ }
106
+ return lines.length > 0 ? lines : ['- None'];
40
107
  }
41
108
  function formatList(items) {
42
- return items.length > 0 ? items : ["- None"];
109
+ return items.length > 0 ? items : ['- None'];
110
+ }
111
+ function formatReasons(reasons) {
112
+ if (reasons.length === 0) {
113
+ return 'it is near the query';
114
+ }
115
+ const visible = reasons.slice(0, 3);
116
+ const remaining = reasons.length - visible.length;
117
+ return remaining > 0
118
+ ? `${visible.join('; ')}; and ${remaining} more`
119
+ : visible.join('; ');
120
+ }
121
+ function nearbySymbolItems(response) {
122
+ if (response.nearbySymbolExplanations) {
123
+ return response.nearbySymbolExplanations;
124
+ }
125
+ return (response.nearbySymbols ?? []).map((symbol) => ({
126
+ symbol,
127
+ reasons: ['exported symbol from 1-hop import'],
128
+ }));
129
+ }
130
+ function formatRelationshipReason(relationship, reasonsByRelationship) {
131
+ const reasons = reasonsByRelationship.get(relationshipKey(relationship));
132
+ return reasons && reasons.length > 0
133
+ ? ` because ${formatReasons(reasons)}`
134
+ : '';
135
+ }
136
+ function relationshipKey(relationship) {
137
+ return [
138
+ relationship.sourceId,
139
+ relationship.targetId,
140
+ relationship.relationshipType,
141
+ ].join('\0');
43
142
  }
@@ -80,12 +80,28 @@ export function registerDoctorCommand(program) {
80
80
  : `${integration.name}: missing ${integration.targetPath}`)
81
81
  .join('; '),
82
82
  });
83
+ let qualityReport;
84
+ if (maps) {
85
+ qualityReport = await analyzeCognitionQuality(workspace, maps);
86
+ const qualityFindings = summarizeQualityFindings(qualityReport);
87
+ const coverageNotes = summarizeCoverageNotes(qualityReport);
88
+ checks.push({
89
+ label: 'quality gate',
90
+ ok: qualityFindings.length === 0,
91
+ detail: qualityFindings.length === 0
92
+ ? [
93
+ 'no stale/noisy cognition, generated scan noise, or duplicate titles',
94
+ ...coverageNotes,
95
+ ].join('; ')
96
+ : qualityFindings.join('; '),
97
+ });
98
+ }
83
99
  printChecks(checks);
84
100
  if (options.quality && maps) {
85
101
  console.log('');
86
102
  console.log('KGraph Cognition Quality');
87
103
  console.log('');
88
- printQualityReport(await analyzeCognitionQuality(workspace, maps));
104
+ printQualityReport(qualityReport ?? (await analyzeCognitionQuality(workspace, maps)));
89
105
  }
90
106
  if (checks.some((check) => !check.ok)) {
91
107
  process.exitCode = 1;
@@ -134,3 +150,29 @@ export function printQualityReport(report) {
134
150
  console.log(` next status: ${change.nextStatus}`);
135
151
  }
136
152
  }
153
+ function summarizeQualityFindings(report) {
154
+ const findings = [];
155
+ if (report.mixedOrStaleCount > 0) {
156
+ findings.push(`${report.mixedOrStaleCount} stale/mixed/unresolved note(s)`);
157
+ }
158
+ if (report.noisyFileRefCount > 0 || report.noisySymbolRefCount > 0) {
159
+ findings.push(`${report.noisyFileRefCount + report.noisySymbolRefCount} noisy cognition ref(s); run \`kgraph repair --dry-run\``);
160
+ }
161
+ if (report.duplicateTitleCount > 0) {
162
+ findings.push(`${report.duplicateTitleCount} duplicate cognition title(s)`);
163
+ }
164
+ if (report.generatedFileScanCount > 0) {
165
+ findings.push(`${report.generatedFileScanCount} generated/integration file(s) scanned; update excludes`);
166
+ }
167
+ return findings;
168
+ }
169
+ function summarizeCoverageNotes(report) {
170
+ const notes = [];
171
+ if (report.unresolvedLocalImportCount > 0) {
172
+ notes.push(`${report.unresolvedLocalImportCount} unresolved local import(s) visible in --quality`);
173
+ }
174
+ if (report.unresolvedCallCount > 0) {
175
+ notes.push(`${report.unresolvedCallCount} unresolved call edge(s) visible in --quality`);
176
+ }
177
+ return notes;
178
+ }
@@ -5,7 +5,8 @@ import { KGraphError, runCommand } from '../errors.js';
5
5
  export function registerIntegrateCommand(program) {
6
6
  const integrate = program
7
7
  .command('integrate')
8
- .description('Manage AI tool integrations');
8
+ .description('Manage AI tool integrations')
9
+ .helpOption('-h, --help', 'Show help');
9
10
  integrate
10
11
  .command('list')
11
12
  .description('List configured integrations')
@@ -6,6 +6,7 @@ export function registerSessionCommand(program) {
6
6
  const session = program
7
7
  .command('session')
8
8
  .description('Track agent read/write session activity and token estimates')
9
+ .helpOption('-h, --help', 'Show help')
9
10
  .option('--json', 'Print JSON output')
10
11
  .action((options) => runCommand(async () => {
11
12
  const workspace = await assertWorkspace(process.cwd());
@@ -93,6 +94,8 @@ export function renderSessionReport(report) {
93
94
  lines.push(...formatList(report.recentEvents.map((event) => `- ${event.agent} ${event.type}${event.path ? ` ${event.path}` : ''} [${event.captureSource}]`)));
94
95
  lines.push('', 'Recent Ledger');
95
96
  lines.push(...formatList(report.ledger.map((entry) => `- ${entry.agent} ${entry.readCount} reads, ${entry.writeCount} writes, ${entry.repeatedReadCount} repeated`)));
97
+ lines.push('', 'Next');
98
+ lines.push(...sessionNextActions(report));
96
99
  return lines.join('\n');
97
100
  }
98
101
  function requireAgent(value) {
@@ -110,3 +113,20 @@ function normalizeSource(value) {
110
113
  function formatList(items) {
111
114
  return items.length > 0 ? items : ['- None'];
112
115
  }
116
+ function sessionNextActions(report) {
117
+ if (report.readCount === 0 && report.writeCount === 0) {
118
+ return [
119
+ '- Start tracking with `kgraph session start --agent <name>`.',
120
+ '- Record meaningful reads/writes with `kgraph session read <path> --agent <name>` and `kgraph session write <path> --agent <name>`.',
121
+ ];
122
+ }
123
+ if (report.repeatedReadCount > 0) {
124
+ return [
125
+ '- Repeated reads are present; run `kgraph context "<topic>"` before broad file inspection.',
126
+ '- End the tracked work with `kgraph session end --agent <name>` when the coding session is done.',
127
+ ];
128
+ }
129
+ return [
130
+ '- End the tracked work with `kgraph session end --agent <name>` when the coding session is done.',
131
+ ];
132
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerUninstallCommand(program: Command): void;
@@ -0,0 +1,69 @@
1
+ import { rm } from 'node:fs/promises';
2
+ import { loadConfig } from '../../config/config.js';
3
+ import { removeIntegrations } from '../../integrations/integration-store.js';
4
+ import { pathExists, resolveWorkspace } from '../../storage/kgraph-paths.js';
5
+ import { runCommand } from '../errors.js';
6
+ export function registerUninstallCommand(program) {
7
+ program
8
+ .command('uninstall')
9
+ .description('Remove KGraph from this repository')
10
+ .option('--yes', 'Apply the uninstall after previewing what will be removed')
11
+ .option('--keep-integrations', 'Remove only .kgraph/ and preserve generated AI tool instruction files')
12
+ .action((options) => runCommand(async () => {
13
+ const workspace = resolveWorkspace(process.cwd());
14
+ const initialized = await pathExists(workspace.kgraphPath);
15
+ const configuredIntegrations = initialized
16
+ ? (await loadConfig(workspace)).integrations.map((integration) => integration.name)
17
+ : [];
18
+ printUninstallPreview({
19
+ initialized,
20
+ integrations: configuredIntegrations,
21
+ keepIntegrations: options.keepIntegrations === true,
22
+ applying: options.yes === true,
23
+ });
24
+ if (!options.yes) {
25
+ return;
26
+ }
27
+ if (initialized &&
28
+ !options.keepIntegrations &&
29
+ configuredIntegrations.length > 0) {
30
+ await removeIntegrations(workspace, configuredIntegrations);
31
+ }
32
+ if (initialized) {
33
+ await rm(workspace.kgraphPath, { recursive: true, force: true });
34
+ }
35
+ console.log('');
36
+ console.log('KGraph uninstall complete.');
37
+ console.log('Run `kgraph init` to set up this repository again.');
38
+ }));
39
+ }
40
+ function printUninstallPreview(input) {
41
+ console.log('KGraph Uninstall Preview');
42
+ console.log('');
43
+ console.log('Will remove:');
44
+ if (input.initialized) {
45
+ console.log('- .kgraph/ runtime workspace');
46
+ }
47
+ else {
48
+ console.log('- Nothing: .kgraph/ does not exist in this repository');
49
+ }
50
+ if (!input.keepIntegrations) {
51
+ if (input.integrations.length > 0) {
52
+ console.log(`- KGraph-managed integration blocks/files for: ${input.integrations.join(', ')}`);
53
+ }
54
+ else {
55
+ console.log('- No configured integration blocks/files found');
56
+ }
57
+ }
58
+ console.log('');
59
+ console.log('Will preserve:');
60
+ console.log('- Repository source files');
61
+ console.log('- User-authored content outside KGraph managed blocks');
62
+ if (input.keepIntegrations) {
63
+ console.log('- KGraph-managed integration instruction files');
64
+ }
65
+ if (!input.applying) {
66
+ console.log('');
67
+ console.log('No files were removed. Run `kgraph uninstall --yes` to apply.');
68
+ }
69
+ }
@@ -1,5 +1,4 @@
1
- import { updateCognition } from '../../cognition/cognition-updater.js';
2
- import { refreshCognitionReferenceStatuses } from '../../cognition/cognition-updater.js';
1
+ import { refreshCognitionReferenceStatuses, updateCognition, } from '../../cognition/cognition-updater.js';
3
2
  import { loadConfig } from '../../config/config.js';
4
3
  import { queryContext } from '../../context/context-query.js';
5
4
  import { scanRepository } from '../../scanner/repo-scanner.js';
@@ -35,6 +34,7 @@ export async function runDefaultWorkflow(query) {
35
34
  console.log(renderWorkflowBanner({
36
35
  files: scan.files.length,
37
36
  symbols: scan.symbols.length,
37
+ skippedFiles: scan.skippedFiles,
38
38
  cognitionNotes: update.processed.length,
39
39
  integrations: config.integrations.map((integration) => ({
40
40
  name: integration.name,
@@ -3,6 +3,7 @@ interface WorkflowBannerStats {
3
3
  files: number;
4
4
  symbols: number;
5
5
  cognitionNotes: number;
6
+ skippedFiles?: number;
6
7
  integrations?: WorkflowBannerIntegration[];
7
8
  }
8
9
  interface WorkflowBannerIntegration {
package/dist/cli/help.js CHANGED
@@ -36,6 +36,8 @@ export function renderRootHelp(useColor = supportsColor()) {
36
36
  command('doctor --quality', 'Report stale/noisy cognition references'),
37
37
  command('repair --dry-run', 'Preview cognition reference cleanup'),
38
38
  command('repair', 'Clean noisy stale cognition references'),
39
+ command('uninstall', 'Preview repo-local KGraph removal'),
40
+ command('uninstall --yes', 'Remove .kgraph/ and managed integrations'),
39
41
  command('visualize', 'Interactive dependency graph at http://localhost:4242'),
40
42
  command('history "blog button"', 'Search processed cognition sessions'),
41
43
  '',
@@ -77,7 +79,10 @@ export function renderWorkflowBanner(stats, useColor = supportsColor()) {
77
79
  ` ${theme.bold('KGraph')} ${theme.dim('repo intelligence refreshed')}`,
78
80
  '',
79
81
  theme.bold('Refresh Complete'),
80
- command('files', String(stats.files)),
82
+ command('files', String(stats.files) +
83
+ (stats.skippedFiles
84
+ ? ` (${stats.skippedFiles} unchanged, skipped)`
85
+ : '')),
81
86
  command('symbols', String(stats.symbols)),
82
87
  command('cognition notes processed', String(stats.cognitionNotes)),
83
88
  command('integration modes', integrationLine),
@@ -1,3 +1,4 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
3
  export declare function createProgram(): Command;
4
+ export declare function shouldRenderRootHelpBeforeParse(argv: string[]): boolean;
package/dist/cli/index.js CHANGED
@@ -12,6 +12,7 @@ import { registerIntegrateCommand } from './commands/integrate.js';
12
12
  import { registerRepairCommand } from './commands/repair.js';
13
13
  import { registerScanCommand } from './commands/scan.js';
14
14
  import { registerSessionCommand } from './commands/session.js';
15
+ import { registerUninstallCommand } from './commands/uninstall.js';
15
16
  import { registerUpdateCommand } from './commands/update.js';
16
17
  import { registerVisualizeCommand } from './commands/visualize.js';
17
18
  import { runDefaultWorkflow } from './commands/workflow.js';
@@ -25,7 +26,6 @@ export function createProgram() {
25
26
  .description('Persistent repo intelligence for AI coding assistants')
26
27
  .argument('[topic...]', 'Run the default refresh workflow and optionally return context for a topic')
27
28
  .version(version)
28
- .addHelpText('beforeAll', renderRootHelp())
29
29
  .helpOption(false)
30
30
  .action(async (topicParts = []) => {
31
31
  await runDefaultWorkflow(topicParts.join(' '));
@@ -48,17 +48,44 @@ export function createProgram() {
48
48
  registerHistoryCommand(program);
49
49
  registerDoctorCommand(program);
50
50
  registerRepairCommand(program);
51
+ registerUninstallCommand(program);
51
52
  return program;
52
53
  }
53
54
  if (isCliEntrypoint()) {
54
55
  const program = createProgram();
55
- if (process.argv.includes('-h') || process.argv.includes('--help')) {
56
+ const helpTarget = findExplicitHelpTarget(program, process.argv.slice(2));
57
+ if (helpTarget === program || shouldRenderRootHelpBeforeParse(process.argv)) {
56
58
  console.log(renderRootHelp());
57
59
  }
60
+ else if (helpTarget) {
61
+ helpTarget.outputHelp();
62
+ }
58
63
  else {
59
64
  await program.parseAsync(process.argv);
60
65
  }
61
66
  }
67
+ export function shouldRenderRootHelpBeforeParse(argv) {
68
+ const args = argv.slice(2);
69
+ return args.length === 1 && (args[0] === '-h' || args[0] === '--help');
70
+ }
71
+ function findExplicitHelpTarget(program, args) {
72
+ let command = program;
73
+ let matchedSubcommand = false;
74
+ for (const arg of args) {
75
+ if (arg === '-h' || arg === '--help') {
76
+ return matchedSubcommand ? command : program;
77
+ }
78
+ if (arg.startsWith('-')) {
79
+ continue;
80
+ }
81
+ const next = command.commands.find((candidate) => candidate.name() === arg || candidate.aliases().includes(arg));
82
+ if (next) {
83
+ command = next;
84
+ matchedSubcommand = true;
85
+ }
86
+ }
87
+ return undefined;
88
+ }
62
89
  function isCliEntrypoint() {
63
90
  if (!process.argv[1]) {
64
91
  return false;
@@ -59,6 +59,13 @@ export async function queryContext(workspace, config, maps, query) {
59
59
  ].filter((relationship, index, all) => all.findIndex((candidate) => candidate.sourceId === relationship.sourceId &&
60
60
  candidate.targetId === relationship.targetId &&
61
61
  candidate.relationshipType === relationship.relationshipType) === index);
62
+ const relationshipExplanations = explainRelationships(relationships, {
63
+ rankedRelationships,
64
+ relevantFiles,
65
+ relevantSymbols,
66
+ relevantCognition,
67
+ matchedDomains,
68
+ });
62
69
  const filePaths = new Set(maps.fileMap.files.map((f) => f.path));
63
70
  const symbolNames = new Set(maps.symbolMap.symbols.map((s) => s.name));
64
71
  const staleReferences = cognition
@@ -73,6 +80,35 @@ export async function queryContext(workspace, config, maps, query) {
73
80
  .filter((s) => !symbolNames.has(s))
74
81
  .map((ref) => `${note.title}: ${ref}`),
75
82
  ]);
83
+ // Collect nearby symbols: exported symbols from files 1-hop imported by matched files
84
+ const matchedFilePaths = new Set([
85
+ ...relevantFiles.map((f) => f.item.path),
86
+ ...relevantSymbols.map((s) => s.item.filePath),
87
+ ]);
88
+ const matchedSymbolIds = new Set(relevantSymbols.map((s) => s.item.id));
89
+ const importedFilePaths = new Set();
90
+ for (const dep of maps.dependencyMap.dependencies) {
91
+ if (dep.kind === 'local' &&
92
+ dep.resolvedFile &&
93
+ matchedFilePaths.has(dep.fromFile)) {
94
+ importedFilePaths.add(dep.resolvedFile);
95
+ }
96
+ }
97
+ // Remove files already in the matched set
98
+ for (const p of matchedFilePaths)
99
+ importedFilePaths.delete(p);
100
+ const nearbySymbols = maps.symbolMap.symbols
101
+ .filter((s) => s.exported &&
102
+ importedFilePaths.has(s.filePath) &&
103
+ !matchedSymbolIds.has(s.id))
104
+ .slice(0, max);
105
+ const nearbySymbolExplanations = nearbySymbols.map((symbol) => ({
106
+ symbol,
107
+ reasons: [
108
+ `exported symbol from 1-hop import ${symbol.filePath}`,
109
+ ...dependenciesForImportedSymbol(symbol, maps.dependencyMap.dependencies),
110
+ ],
111
+ }));
76
112
  return {
77
113
  query,
78
114
  matchedDomains,
@@ -80,7 +116,71 @@ export async function queryContext(workspace, config, maps, query) {
80
116
  relevantSymbols,
81
117
  relevantCognition,
82
118
  relationships: relationships.slice(0, max),
119
+ relationshipExplanations: relationshipExplanations.slice(0, max),
120
+ nearbySymbols,
121
+ nearbySymbolExplanations,
83
122
  staleReferences,
84
123
  warnings: [],
85
124
  };
86
125
  }
126
+ function explainRelationships(relationships, context) {
127
+ const rankedReasons = new Map(context.rankedRelationships.map((ranked) => [
128
+ relationshipKey(ranked.item),
129
+ ranked.reasons,
130
+ ]));
131
+ return relationships.map((relationship) => {
132
+ const reasons = new Set();
133
+ for (const reason of rankedReasons.get(relationshipKey(relationship)) ?? []) {
134
+ reasons.add(reason);
135
+ }
136
+ for (const file of context.relevantFiles) {
137
+ if (relationship.sourceId === file.item.path ||
138
+ relationship.targetId === file.item.path) {
139
+ reasons.add(`connected to matched file ${file.item.path}`);
140
+ }
141
+ }
142
+ for (const symbol of context.relevantSymbols) {
143
+ if (relationship.sourceId === symbol.item.id ||
144
+ relationship.targetId === symbol.item.id ||
145
+ relationship.sourceId === symbol.item.name ||
146
+ relationship.targetId === symbol.item.name ||
147
+ relationship.sourceId === symbol.item.filePath ||
148
+ relationship.targetId === symbol.item.filePath) {
149
+ reasons.add(`connected to matched symbol ${symbol.item.name}`);
150
+ }
151
+ }
152
+ for (const note of context.relevantCognition) {
153
+ if (note.item.relatedFiles.includes(relationship.sourceId) ||
154
+ note.item.relatedFiles.includes(relationship.targetId) ||
155
+ note.item.relatedSymbols.includes(relationship.sourceId) ||
156
+ note.item.relatedSymbols.includes(relationship.targetId)) {
157
+ reasons.add(`referenced by cognition "${note.item.title}"`);
158
+ }
159
+ }
160
+ for (const domain of context.matchedDomains) {
161
+ if (domain.item.files.includes(relationship.sourceId) ||
162
+ domain.item.files.includes(relationship.targetId) ||
163
+ domain.item.symbols.includes(relationship.sourceId) ||
164
+ domain.item.symbols.includes(relationship.targetId)) {
165
+ reasons.add(`inside matched domain ${domain.item.name}`);
166
+ }
167
+ }
168
+ if (reasons.size === 0) {
169
+ reasons.add(`nearby ${relationship.relationshipType} relationship`);
170
+ }
171
+ return { relationship, reasons: [...reasons] };
172
+ });
173
+ }
174
+ function dependenciesForImportedSymbol(symbol, dependencies) {
175
+ return dependencies
176
+ .filter((dependency) => dependency.kind === 'local' &&
177
+ dependency.resolvedFile === symbol.filePath)
178
+ .map((dependency) => `imported by ${dependency.fromFile} via ${dependency.specifier}`);
179
+ }
180
+ function relationshipKey(relationship) {
181
+ return [
182
+ relationship.sourceId,
183
+ relationship.targetId,
184
+ relationship.relationshipType,
185
+ ].join('\0');
186
+ }
@@ -46,21 +46,71 @@ export async function scanRepository(rootPath, config, previous) {
46
46
  unique: true,
47
47
  ignore: buildFastGlobIgnore(allExcludes),
48
48
  });
49
+ // Build lookup maps from previous scan for incremental skip
50
+ const prevFileByPath = new Map((previous?.files ?? []).map((f) => [f.path, f]));
51
+ const prevSymbolsByFile = new Map();
52
+ const prevDepsByFile = new Map();
53
+ const prevRelsBySource = new Map();
54
+ if (previous) {
55
+ for (const sym of previous.symbols) {
56
+ const arr = prevSymbolsByFile.get(sym.filePath) ?? [];
57
+ arr.push(sym);
58
+ prevSymbolsByFile.set(sym.filePath, arr);
59
+ }
60
+ for (const dep of previous.dependencies) {
61
+ const arr = prevDepsByFile.get(dep.fromFile) ?? [];
62
+ arr.push(dep);
63
+ prevDepsByFile.set(dep.fromFile, arr);
64
+ }
65
+ for (const rel of previous.relationships) {
66
+ if (rel.relationshipType !== 'import' &&
67
+ rel.relationshipType !== 'moved-from') {
68
+ const arr = prevRelsBySource.get(rel.sourceId) ?? [];
69
+ arr.push(rel);
70
+ prevRelsBySource.set(rel.sourceId, arr);
71
+ }
72
+ }
73
+ }
49
74
  const files = [];
50
75
  const symbols = [];
51
76
  const dependencies = [];
52
77
  const relationships = [];
53
78
  const warnings = [];
79
+ let skippedFiles = 0;
54
80
  for (const repoPath of entries.sort()) {
55
81
  if (shouldExclude(repoPath, mergedConfig)) {
56
82
  continue;
57
83
  }
58
84
  const absolutePath = path.join(rootPath, repoPath);
59
85
  try {
60
- const [info, content] = await Promise.all([
61
- stat(absolutePath),
62
- readFile(absolutePath),
63
- ]);
86
+ const info = await stat(absolutePath);
87
+ // Incremental skip: if mtime and size match previous, carry forward
88
+ const prevFile = prevFileByPath.get(repoPath);
89
+ if (prevFile &&
90
+ prevFile.sizeBytes === info.size &&
91
+ prevFile.modifiedAt === info.mtime.toISOString()) {
92
+ files.push({ ...prevFile, modifiedAt: info.mtime.toISOString() });
93
+ const prevSyms = prevSymbolsByFile.get(repoPath);
94
+ if (prevSyms)
95
+ symbols.push(...prevSyms);
96
+ const prevDeps = prevDepsByFile.get(repoPath);
97
+ if (prevDeps)
98
+ dependencies.push(...prevDeps);
99
+ const prevRels = prevRelsBySource.get(repoPath);
100
+ if (prevRels)
101
+ relationships.push(...prevRels);
102
+ // Also carry forward symbol-sourced relationships
103
+ if (prevSyms) {
104
+ for (const sym of prevSyms) {
105
+ const symRels = prevRelsBySource.get(sym.id);
106
+ if (symRels)
107
+ relationships.push(...symRels);
108
+ }
109
+ }
110
+ skippedFiles++;
111
+ continue;
112
+ }
113
+ const content = await readFile(absolutePath);
64
114
  const text = content.toString('utf8');
65
115
  const contentHash = crypto
66
116
  .createHash('sha256')
@@ -105,7 +155,14 @@ export async function scanRepository(rootPath, config, previous) {
105
155
  resolveLocalDependencies(dependencies, files);
106
156
  relationships.push(...buildImportRelationships(dependencies));
107
157
  relationships.push(...detectMovedFiles(previous?.files ?? [], files));
108
- return { files, symbols, dependencies, relationships, warnings };
158
+ return {
159
+ files,
160
+ symbols,
161
+ dependencies,
162
+ relationships,
163
+ warnings,
164
+ skippedFiles,
165
+ };
109
166
  }
110
167
  const SOURCE_EXTENSIONS = [
111
168
  '.ts',
@@ -37,7 +37,11 @@ export async function pathExists(targetPath) {
37
37
  export async function assertWorkspace(rootPath = process.cwd()) {
38
38
  const workspace = resolveWorkspace(rootPath);
39
39
  if (!(await pathExists(workspace.kgraphPath))) {
40
- throw new KGraphError("KGraph is not initialized. Run `kgraph init` first.");
40
+ throw new KGraphError([
41
+ "KGraph is not initialized for this repository.",
42
+ "Run `kgraph init` first, or use `kgraph init --integrations codex,copilot,cursor,claude-code` to initialize and connect common AI tools.",
43
+ "After init, run `kgraph doctor` to verify maps, integrations, and cognition quality.",
44
+ ].join("\n"));
41
45
  }
42
46
  const info = await stat(workspace.kgraphPath);
43
47
  if (!info.isDirectory()) {
@@ -1,5 +1,5 @@
1
- import type { CodeSymbol, Relationship, RepositoryFile } from "./maps.js";
2
- export type ReferenceStatus = "current" | "stale" | "unresolved" | "mixed";
1
+ import type { CodeSymbol, Relationship, RepositoryFile } from './maps.js';
2
+ export type ReferenceStatus = 'current' | 'stale' | 'unresolved' | 'mixed';
3
3
  export interface ParsedCognitionNote {
4
4
  title: string;
5
5
  domain?: string;
@@ -38,6 +38,15 @@ export interface ContextResponse {
38
38
  relevantSymbols: RankedItem<CodeSymbol>[];
39
39
  relevantCognition: RankedItem<CognitionNote>[];
40
40
  relationships: Relationship[];
41
+ relationshipExplanations?: Array<{
42
+ relationship: Relationship;
43
+ reasons: string[];
44
+ }>;
45
+ nearbySymbols?: CodeSymbol[];
46
+ nearbySymbolExplanations?: Array<{
47
+ symbol: CodeSymbol;
48
+ reasons: string[];
49
+ }>;
41
50
  staleReferences: string[];
42
51
  warnings: string[];
43
52
  }
@@ -1,7 +1,7 @@
1
- export type ScanStatus = "mapped" | "generic" | "failed";
2
- export type DependencyKind = "local" | "package" | "unknown";
3
- export type SymbolKind = "function" | "class" | "method" | "type" | "interface" | "export" | "import";
4
- export type RelationshipType = "import" | "contains" | "symbol-contains" | "calls" | "mentions" | "belongs-to-domain" | "stale-reference" | "moved-from";
1
+ export type ScanStatus = 'mapped' | 'generic' | 'failed';
2
+ export type DependencyKind = 'local' | 'package' | 'unknown';
3
+ export type SymbolKind = 'function' | 'class' | 'method' | 'type' | 'interface' | 'export' | 'import';
4
+ export type RelationshipType = 'import' | 'contains' | 'symbol-contains' | 'calls' | 'mentions' | 'belongs-to-domain' | 'stale-reference' | 'moved-from';
5
5
  export interface RepositoryFile {
6
6
  id: string;
7
7
  path: string;
@@ -36,7 +36,7 @@ export interface Relationship {
36
36
  targetType: string;
37
37
  targetId: string;
38
38
  relationshipType: RelationshipType;
39
- confidence: "high" | "medium" | "low";
39
+ confidence: 'high' | 'medium' | 'low';
40
40
  }
41
41
  export interface FileMap {
42
42
  generatedAt: string;
@@ -60,4 +60,5 @@ export interface ScanResult {
60
60
  dependencies: Dependency[];
61
61
  relationships: Relationship[];
62
62
  warnings: string[];
63
+ skippedFiles?: number;
63
64
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kentwynn/kgraph",
3
- "version": "0.1.27",
3
+ "version": "0.2.1",
4
4
  "description": "Persistent repo intelligence for AI coding assistants.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -59,7 +59,7 @@
59
59
  "devDependencies": {
60
60
  "@types/node": "^20.17.10",
61
61
  "tsx": "^4.19.2",
62
- "vitest": "^2.1.8"
62
+ "vitest": "^3.2.4"
63
63
  },
64
64
  "engines": {
65
65
  "node": ">=20"