@kentwynn/kgraph 0.2.0 → 0.2.2

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
 
@@ -73,6 +74,8 @@ That shows matched files/symbols, files importing the target, known callers/call
73
74
 
74
75
  ## Install
75
76
 
77
+ The official npm package is `@kentwynn/kgraph`; the official repository is `github.com/kentwynn/KGraph`.
78
+
76
79
  Use the published CLI:
77
80
 
78
81
  ```bash
@@ -89,6 +92,8 @@ npx @kentwynn/kgraph@latest "auth token refresh"
89
92
 
90
93
  KGraph requires Node.js 20 or newer.
91
94
 
95
+ KGraph's core functionality is free and local-first. It does not require accounts, telemetry, cloud services, API keys, or source-code upload.
96
+
92
97
  ## Quick Start
93
98
 
94
99
  From the root of a repository:
@@ -103,7 +108,7 @@ kgraph integrate add codex copilot cursor claude-code gemini windsurf cline
103
108
  # 3. Run the normal workflow for a topic
104
109
  kgraph "auth token refresh"
105
110
 
106
- # 4. Check health if something feels off
111
+ # 4. Verify the setup and use doctor as the quality gate
107
112
  kgraph doctor
108
113
  ```
109
114
 
@@ -120,7 +125,7 @@ kgraph "topic"
120
125
  kgraph
121
126
  ```
122
127
 
123
- Use `kgraph doctor --quality` and `kgraph repair --dry-run` only when stale or noisy cognition references start making context harder to trust.
128
+ 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
129
 
125
130
  Agents can also report session activity so KGraph can estimate token waste:
126
131
 
@@ -167,6 +172,8 @@ kgraph doctor --quality
167
172
 
168
173
  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
174
 
175
+ 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.
176
+
170
177
  ```bash
171
178
  kgraph repair --dry-run
172
179
  kgraph repair
@@ -174,6 +181,14 @@ kgraph repair
174
181
 
175
182
  `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
183
 
184
+ ```bash
185
+ kgraph uninstall
186
+ kgraph uninstall --yes
187
+ kgraph uninstall --keep-integrations --yes
188
+ ```
189
+
190
+ `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.
191
+
177
192
  ```bash
178
193
  kgraph impact "Button"
179
194
  kgraph impact "createSession" --json
@@ -192,6 +207,7 @@ kgraph session end --agent codex
192
207
  ```
193
208
 
194
209
  Track agent-reported read/write activity, repeated reads, and estimated token cost. Supported agents are `codex`, `claude-code`, `copilot`, `cursor`, `gemini`, `windsurf`, and `cline`.
210
+ The text report now includes next actions, such as using `kgraph context "<topic>"` before repeated broad file inspection.
195
211
 
196
212
  ## Optional Step Commands
197
213
 
@@ -209,6 +225,7 @@ kgraph context "auth token refresh" --json
209
225
  ```
210
226
 
211
227
  Return context from existing maps and cognition without scanning or updating first.
228
+ 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
229
 
213
230
  ```bash
214
231
  kgraph update
@@ -289,7 +306,7 @@ All runtime data lives under `.kgraph/`:
289
306
  └── context/
290
307
  ```
291
308
 
292
- The files are local, inspectable, and human-readable. There is no database, telemetry, cloud service, account, API key, embedding service, or model provider.
309
+ The files are local, inspectable, and human-readable. Core KGraph functionality is free. There is no database, telemetry, cloud service, account, API key, embedding service, model provider, or source-code upload.
293
310
 
294
311
  ## Language Support
295
312
 
@@ -358,14 +375,25 @@ npm run release:pack
358
375
 
359
376
  ## Release
360
377
 
361
- Releases are tag-driven:
378
+ Releases are PR-first because `main` is protected. Use the Makefile helper to bump the version on a release branch, push it, and open a pull request when the GitHub CLI is available:
379
+
380
+ ```bash
381
+ make release
382
+ ```
383
+
384
+ Use `RELEASE=minor` or `RELEASE=major` when needed:
385
+
386
+ ```bash
387
+ make release RELEASE=minor
388
+ ```
389
+
390
+ After the PR is merged, tag the merged commit from an up-to-date `main`:
362
391
 
363
392
  ```bash
364
- npm version patch
365
- git push origin main --follow-tags
393
+ make release-tag VERSION=v0.2.2
366
394
  ```
367
395
 
368
- The release workflow builds, tests, packs, publishes the npm package on version tags, creates a GitHub Release, and uploads the tarball artifact.
396
+ The release workflow builds, tests, packs, publishes the npm package on version tags, creates a GitHub Release, and uploads the tarball artifact. Do not push directly to `main` for releases.
369
397
 
370
398
  ## Design Principles
371
399
 
@@ -378,8 +406,9 @@ The release workflow builds, tests, packs, publishes the npm package on version
378
406
 
379
407
  ## Roadmap
380
408
 
409
+ - Better Git-aware token saving and diff context.
381
410
  - Smarter cross-file symbol and call relationship inference.
382
411
  - Stronger TypeScript path alias and package export resolution.
383
412
  - Richer graph filtering for large repositories.
384
- - Optional MCP server for editor tool-call access.
385
- - Team workflows for shared committed cognition.
413
+ - Optional MCP and editor integration.
414
+ - Team-friendly shared cognition workflows that stay local-first.
@@ -27,7 +27,7 @@ export function registerContextCommand(program) {
27
27
  export function renderContextMarkdown(response) {
28
28
  const lines = [`# KGraph Context`, ``, `Query: ${response.query}`, ``];
29
29
  lines.push('## Matched Domains', '');
30
- lines.push(...formatList(response.matchedDomains.map((item) => `- ${item.item.name} (${item.reasons.join(', ')})`)));
30
+ lines.push(...formatList(response.matchedDomains.map((item) => `- ${item.item.name} because ${formatReasons(item.reasons)}`)));
31
31
  lines.push('', '## Relevant Files', '');
32
32
  lines.push(...formatList(response.relevantFiles.map((item) => {
33
33
  const f = item.item;
@@ -37,7 +37,7 @@ export function renderContextMarkdown(response) {
37
37
  ]
38
38
  .filter(Boolean)
39
39
  .join(', ');
40
- return `- ${f.path}${meta ? ` [${meta}]` : ''}`;
40
+ return `- ${f.path}${meta ? ` [${meta}]` : ''} because ${formatReasons(item.reasons)}`;
41
41
  })));
42
42
  lines.push('', '## Relevant Symbols', '');
43
43
  lines.push(...formatList(response.relevantSymbols.map((item) => {
@@ -46,25 +46,29 @@ export function renderContextMarkdown(response) {
46
46
  const lineRange = s.startLine != null && s.endLine != null
47
47
  ? `:${s.startLine}-${s.endLine}`
48
48
  : '';
49
- return `- ${s.name} (${kindInfo}) in ${s.filePath}${lineRange}`;
49
+ return `- ${s.name} (${kindInfo}) in ${s.filePath}${lineRange} because ${formatReasons(item.reasons)}`;
50
50
  })));
51
51
  lines.push('', '## Relevant Cognition', '');
52
- lines.push(...formatList(response.relevantCognition.map((item) => `- ${item.item.title} [${item.item.referencesStatus}]`)));
52
+ lines.push(...formatList(response.relevantCognition.map((item) => `- ${item.item.title} [${item.item.referencesStatus}] because ${formatReasons(item.reasons)}`)));
53
53
  lines.push('', '## Relationships', '');
54
- lines.push(...formatGroupedRelationships(response.relationships));
54
+ lines.push(...formatGroupedRelationships(response.relationships, response.relationshipExplanations));
55
55
  lines.push('', '## Nearby Symbols (1-hop imports)', '');
56
- lines.push(...formatList((response.nearbySymbols ?? []).map((s) => {
56
+ lines.push(...formatList(nearbySymbolItems(response).map(({ symbol: s, reasons }) => {
57
57
  const kindInfo = [s.kind, s.parentName].filter(Boolean).join(', ');
58
58
  const lineRange = s.startLine != null && s.endLine != null
59
59
  ? `:${s.startLine}-${s.endLine}`
60
60
  : '';
61
- return `- ${s.name} (${kindInfo}) in ${s.filePath}${lineRange}`;
61
+ return `- ${s.name} (${kindInfo}) in ${s.filePath}${lineRange} because ${formatReasons(reasons)}`;
62
62
  })));
63
63
  lines.push('', '## Stale References', '');
64
64
  lines.push(...formatList(response.staleReferences.map((ref) => `- ${ref}`)));
65
65
  return lines.join('\n');
66
66
  }
67
- function formatGroupedRelationships(relationships) {
67
+ function formatGroupedRelationships(relationships, explanations) {
68
+ const reasonsByRelationship = new Map((explanations ?? []).map((item) => [
69
+ relationshipKey(item.relationship),
70
+ item.reasons,
71
+ ]));
68
72
  const imports = relationships.filter((r) => r.relationshipType === 'import');
69
73
  const calls = relationships.filter((r) => r.relationshipType === 'calls');
70
74
  const contains = relationships.filter((r) => r.relationshipType === 'symbol-contains');
@@ -77,26 +81,62 @@ function formatGroupedRelationships(relationships) {
77
81
  const lines = [];
78
82
  if (imports.length > 0) {
79
83
  lines.push('Imports:');
80
- for (const r of imports)
81
- lines.push(` ${r.sourceId} → ${r.targetId}`);
84
+ for (const r of imports) {
85
+ lines.push(` ${r.sourceId} → ${r.targetId}${formatRelationshipReason(r, reasonsByRelationship)}`);
86
+ }
82
87
  }
83
88
  if (calls.length > 0) {
84
89
  lines.push('Calls:');
85
- for (const r of calls)
86
- lines.push(` ${r.sourceId} → ${r.targetId}`);
90
+ for (const r of calls) {
91
+ lines.push(` ${r.sourceId} → ${r.targetId}${formatRelationshipReason(r, reasonsByRelationship)}`);
92
+ }
87
93
  }
88
94
  if (contains.length > 0) {
89
95
  lines.push('Contains:');
90
- for (const r of contains)
91
- lines.push(` ${r.sourceId} contains ${r.targetId}`);
96
+ for (const r of contains) {
97
+ lines.push(` ${r.sourceId} contains ${r.targetId}${formatRelationshipReason(r, reasonsByRelationship)}`);
98
+ }
92
99
  }
93
100
  if (other.length > 0) {
94
101
  lines.push('Other:');
95
- for (const r of other)
96
- lines.push(` ${r.sourceId} ${r.relationshipType} ${r.targetId}`);
102
+ for (const r of other) {
103
+ lines.push(` ${r.sourceId} ${r.relationshipType} ${r.targetId}${formatRelationshipReason(r, reasonsByRelationship)}`);
104
+ }
97
105
  }
98
106
  return lines.length > 0 ? lines : ['- None'];
99
107
  }
100
108
  function formatList(items) {
101
109
  return items.length > 0 ? items : ['- None'];
102
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');
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
+ }
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
  '',
@@ -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
@@ -95,6 +102,13 @@ export async function queryContext(workspace, config, maps, query) {
95
102
  importedFilePaths.has(s.filePath) &&
96
103
  !matchedSymbolIds.has(s.id))
97
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
+ }));
98
112
  return {
99
113
  query,
100
114
  matchedDomains,
@@ -102,8 +116,71 @@ export async function queryContext(workspace, config, maps, query) {
102
116
  relevantSymbols,
103
117
  relevantCognition,
104
118
  relationships: relationships.slice(0, max),
119
+ relationshipExplanations: relationshipExplanations.slice(0, max),
105
120
  nearbySymbols,
121
+ nearbySymbolExplanations,
106
122
  staleReferences,
107
123
  warnings: [],
108
124
  };
109
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
+ }
@@ -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()) {
@@ -38,7 +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
+ }>;
41
45
  nearbySymbols?: CodeSymbol[];
46
+ nearbySymbolExplanations?: Array<{
47
+ symbol: CodeSymbol;
48
+ reasons: string[];
49
+ }>;
42
50
  staleReferences: string[];
43
51
  warnings: string[];
44
52
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kentwynn/kgraph",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
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"