@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 +38 -9
- package/dist/cli/commands/context.js +56 -16
- package/dist/cli/commands/doctor.js +43 -1
- package/dist/cli/commands/integrate.js +2 -1
- package/dist/cli/commands/session.js +20 -0
- package/dist/cli/commands/uninstall.d.ts +2 -0
- package/dist/cli/commands/uninstall.js +69 -0
- package/dist/cli/help.js +2 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +29 -2
- package/dist/context/context-query.js +77 -0
- package/dist/storage/kgraph-paths.js +5 -1
- package/dist/types/cognition.d.ts +8 -0
- package/package.json +2 -2
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.
|
|
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`
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
385
|
-
- Team workflows
|
|
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}
|
|
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
|
|
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,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
|
'',
|
package/dist/cli/index.d.ts
CHANGED
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
|
-
|
|
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(
|
|
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.
|
|
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.
|
|
62
|
+
"vitest": "^3.2.4"
|
|
63
63
|
},
|
|
64
64
|
"engines": {
|
|
65
65
|
"node": ">=20"
|