@kentwynn/kgraph 0.2.38 → 0.2.40
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 +33 -10
- package/dist/cli/commands/init.js +7 -0
- package/dist/cli/commands/integrate.js +7 -0
- package/dist/cli/commands/mcp.d.ts +2 -0
- package/dist/cli/commands/mcp.js +10 -0
- package/dist/cli/help.js +3 -0
- package/dist/cli/index.js +2 -0
- package/dist/integrations/vscode-mcp.d.ts +10 -0
- package/dist/integrations/vscode-mcp.js +76 -0
- package/dist/integrations/workflow-steps.js +19 -16
- package/dist/mcp/server.d.ts +7 -0
- package/dist/mcp/server.js +665 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
<img src="media/logo.svg" alt="KGraph Atom Core Logo" width="140" height="140">
|
|
4
4
|
|
|
5
|
-
# KGraph:
|
|
5
|
+
# KGraph: Local repo memory for AI coding tools.
|
|
6
6
|
|
|
7
7
|
<strong>atoms · evidence · context packs</strong>
|
|
8
8
|
|
|
@@ -35,10 +35,20 @@
|
|
|
35
35
|
|
|
36
36
|
</div>
|
|
37
37
|
|
|
38
|
-
KGraph gives
|
|
38
|
+
KGraph gives AI coding tools a local repo memory: file maps, symbols, imports, relationships, and durable knowledge atoms stored under `.kgraph/`. The goal is simple: stop re-learning the same codebase every session.
|
|
39
39
|
|
|
40
40
|
The CLI presents this as **Atom Core**: lightweight local atoms plus deterministic repo maps, context packs, and session history that remain inspectable under `.kgraph/`.
|
|
41
41
|
|
|
42
|
+
## Try It in 30 Seconds
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm install -g @kentwynn/kgraph@latest
|
|
46
|
+
kgraph init
|
|
47
|
+
kgraph "auth token refresh"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
That gets you a local scan, a focused context response, and durable memory under `.kgraph/` for the next session.
|
|
51
|
+
|
|
42
52
|
## The Workflow
|
|
43
53
|
|
|
44
54
|
Use KGraph with one setup command and one normal daily command:
|
|
@@ -148,19 +158,21 @@ From the root of a repository:
|
|
|
148
158
|
# 1. Create the local KGraph workspace and run the first scan
|
|
149
159
|
kgraph init
|
|
150
160
|
|
|
151
|
-
# 2.
|
|
152
|
-
# or add integrations later when you want KGraph-managed instructions
|
|
153
|
-
kgraph integrate add codex copilot cursor claude-code gemini windsurf cline
|
|
154
|
-
|
|
155
|
-
# 3. Run the normal workflow for a topic
|
|
161
|
+
# 2. Ask for focused context on a real repo topic
|
|
156
162
|
kgraph "auth token refresh"
|
|
157
163
|
|
|
158
|
-
#
|
|
164
|
+
# 3. Optional: verify the workspace and saved intelligence
|
|
159
165
|
kgraph doctor
|
|
160
166
|
```
|
|
161
167
|
|
|
162
168
|
`kgraph init` scans once, prints repo language coverage, detects likely local AI tools, and recommends matching integrations. Integrations are still optional: they only write local instruction files so tools know when to run KGraph. They do not start background agents or call AI providers.
|
|
163
169
|
|
|
170
|
+
If you already know exactly which integrations you want, you can add them later:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
kgraph integrate add codex copilot cursor claude-code gemini windsurf cline
|
|
174
|
+
```
|
|
175
|
+
|
|
164
176
|
After useful AI work, assistants save durable runtime-capture notes into `.kgraph/inbox/`. These notes are not project documentation; they are KGraph input files that the next `kgraph` run processes automatically. You can also process them directly with `kgraph update`.
|
|
165
177
|
|
|
166
178
|
Normal agent flow is intentionally small. For coding context, agents use the
|
|
@@ -372,6 +384,13 @@ kgraph history --json
|
|
|
372
384
|
|
|
373
385
|
Show processed capture history. Add a query to find historical work by title, summary, file, symbol, or note body.
|
|
374
386
|
|
|
387
|
+
```bash
|
|
388
|
+
kgraph mcp
|
|
389
|
+
kgraph mcp --root /path/to/repo
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
Start the local KGraph MCP server over stdio. You normally do not run this command by hand; MCP clients such as VS Code start it automatically after KGraph is registered in that client's MCP config. MCP clients can call typed `kgraph_*` tools for orchestration, context packs, impact analysis, capture, health checks, and command-compatible access to the full CLI surface. Prefer MCP tools when the client exposes them; otherwise use the normal CLI commands.
|
|
393
|
+
|
|
375
394
|
## AI Tool Integrations
|
|
376
395
|
|
|
377
396
|
KGraph integrations are local files. They do not start background agents, call AI providers, or send data anywhere.
|
|
@@ -381,14 +400,18 @@ You do not need integrations to use KGraph manually. They are useful when you wa
|
|
|
381
400
|
`kgraph init` detects likely local tools and recommends integrations when possible. You can also manage them explicitly:
|
|
382
401
|
|
|
383
402
|
```bash
|
|
403
|
+
kgraph init --integrations copilot --mcp
|
|
384
404
|
kgraph integrate add codex copilot cursor claude-code gemini windsurf cline
|
|
405
|
+
kgraph integrate add copilot --mcp
|
|
385
406
|
kgraph integrate add copilot --mode smart
|
|
386
407
|
kgraph integrate set copilot --mode manual
|
|
387
408
|
kgraph integrate list
|
|
388
409
|
kgraph integrate remove cursor
|
|
389
410
|
```
|
|
390
411
|
|
|
391
|
-
New integrations default to `always` mode, so every chat in the repository starts with the matching KGraph command. Use `--mode smart` to run KGraph only for repo-specific work, or `--mode manual` to run only when explicitly asked.
|
|
412
|
+
New integrations default to `always` mode, so every chat in the repository starts with the matching KGraph command. Use `--mode smart` to run KGraph only for repo-specific work, or `--mode manual` to run only when explicitly asked. Plain `kgraph init` stays repo-local; add `--mcp` only when you want KGraph to write editor/client MCP config.
|
|
413
|
+
|
|
414
|
+
Add `--mcp` when configuring Copilot from VS Code. KGraph writes a `KGraph` stdio server entry to the VS Code MCP config for this repository and prints the config path; reload VS Code afterward so Copilot can start the server. If an AI client exposes MCP tools named `kgraph_*`, use those tools for KGraph orchestration and context. If MCP is unavailable, use the CLI commands generated in the integration instructions. MCP changes the transport, not the `always`, `smart`, `manual`, or `off` policy. See the [MCP setup wiki page](docs/wiki/MCP-Setup.md) for details.
|
|
392
415
|
|
|
393
416
|
| Mode | Behavior |
|
|
394
417
|
| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -545,5 +568,5 @@ The release workflow builds, tests, packs, publishes the npm package on version
|
|
|
545
568
|
- Smarter cross-file symbol and call relationship inference.
|
|
546
569
|
- Stronger TypeScript path alias and package export resolution.
|
|
547
570
|
- Richer graph filtering for large repositories.
|
|
548
|
-
-
|
|
571
|
+
- More MCP client setup targets beyond VS Code/Copilot.
|
|
549
572
|
- Team-friendly shared knowledge workflows that stay local-first.
|
|
@@ -2,6 +2,7 @@ import { loadConfig, saveConfig, writeDefaultConfig, } from '../../config/config
|
|
|
2
2
|
import { installCopilotMemory } from '../../integrations/copilot-memory.js';
|
|
3
3
|
import { normalizeIntegrationNames } from '../../integrations/integration-registry.js';
|
|
4
4
|
import { addIntegrations } from '../../integrations/integration-store.js';
|
|
5
|
+
import { installVSCodeMcpServer } from '../../integrations/vscode-mcp.js';
|
|
5
6
|
import { ensureKnowledgeStore } from '../../knowledge/atom-store.js';
|
|
6
7
|
import { scanRepository } from '../../scanner/repo-scanner.js';
|
|
7
8
|
import { ensureWorkspace } from '../../storage/kgraph-paths.js';
|
|
@@ -18,6 +19,7 @@ export function registerInitCommand(program) {
|
|
|
18
19
|
.option('--integration <name>', 'Configure an AI tool integration', collectOption, [])
|
|
19
20
|
.option('--integrations <names>', 'Configure comma-separated AI tool integrations')
|
|
20
21
|
.option('--mode <mode>', 'Integration mode: always, smart, manual, or off', 'always')
|
|
22
|
+
.option('--mcp', 'Configure the VS Code/Copilot MCP server for this repository')
|
|
21
23
|
.action((options) => runCommand(async () => {
|
|
22
24
|
const workspace = await ensureWorkspace(process.cwd());
|
|
23
25
|
await ensureKnowledgeStore(workspace);
|
|
@@ -34,6 +36,11 @@ export function registerInitCommand(program) {
|
|
|
34
36
|
const changed = await addIntegrations(workspace, names, mode);
|
|
35
37
|
console.log(`Configured integrations: ${changed.map((item) => `${item.name}:${item.mode}`).join(', ')}`);
|
|
36
38
|
}
|
|
39
|
+
if (options.mcp) {
|
|
40
|
+
const result = await installVSCodeMcpServer(workspace);
|
|
41
|
+
console.log(`${result.changed ? 'Configured' : 'Verified'} VS Code MCP server: ${result.serverName} (${result.configPath})`);
|
|
42
|
+
console.log('Reload VS Code so it starts the KGraph MCP server.');
|
|
43
|
+
}
|
|
37
44
|
let config = await loadConfig(workspace);
|
|
38
45
|
// Workspace detection — only for fresh init, non-destructive
|
|
39
46
|
const workspaceInfo = await detectWorkspaces(workspace.rootPath);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { normalizeIntegrationNames } from '../../integrations/integration-registry.js';
|
|
2
2
|
import { addIntegrations, listIntegrations, removeIntegrations, setIntegrationMode, } from '../../integrations/integration-store.js';
|
|
3
|
+
import { installVSCodeMcpServer } from '../../integrations/vscode-mcp.js';
|
|
3
4
|
import { assertWorkspace } from '../../storage/kgraph-paths.js';
|
|
4
5
|
import { KGraphError, runCommand } from '../errors.js';
|
|
5
6
|
export function registerIntegrateCommand(program) {
|
|
@@ -26,6 +27,7 @@ export function registerIntegrateCommand(program) {
|
|
|
26
27
|
.description('Add AI tool integrations')
|
|
27
28
|
.argument('<names...>')
|
|
28
29
|
.option('--mode <mode>', 'always, smart, manual, or off', 'always')
|
|
30
|
+
.option('--mcp', 'Configure the VS Code/Copilot MCP server for this repository')
|
|
29
31
|
.action((names, options) => runCommand(async () => {
|
|
30
32
|
const workspace = await assertWorkspace(process.cwd());
|
|
31
33
|
const normalized = normalizeIntegrationNames(names);
|
|
@@ -35,6 +37,11 @@ export function registerIntegrateCommand(program) {
|
|
|
35
37
|
const mode = normalizeIntegrationMode(options.mode);
|
|
36
38
|
const changed = await addIntegrations(workspace, normalized, mode);
|
|
37
39
|
console.log(`Configured integrations: ${changed.map((item) => `${item.name}:${item.mode}`).join(', ')}`);
|
|
40
|
+
if (options.mcp) {
|
|
41
|
+
const result = await installVSCodeMcpServer(workspace);
|
|
42
|
+
console.log(`${result.changed ? 'Configured' : 'Verified'} VS Code MCP server: ${result.serverName} (${result.configPath})`);
|
|
43
|
+
console.log('Reload VS Code so it starts the KGraph MCP server.');
|
|
44
|
+
}
|
|
38
45
|
}));
|
|
39
46
|
integrate
|
|
40
47
|
.command('set')
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { runMcpServer } from '../../mcp/server.js';
|
|
2
|
+
export function registerMcpCommand(program) {
|
|
3
|
+
program
|
|
4
|
+
.command('mcp')
|
|
5
|
+
.description('Start the local KGraph MCP server over stdio')
|
|
6
|
+
.option('--root <path>', 'Repository root path', process.cwd())
|
|
7
|
+
.action(async (options) => {
|
|
8
|
+
await runMcpServer({ rootPath: options.root ?? process.cwd() });
|
|
9
|
+
});
|
|
10
|
+
}
|
package/dist/cli/help.js
CHANGED
|
@@ -24,6 +24,7 @@ export function renderRootHelp(useColor = supportsColor()) {
|
|
|
24
24
|
sectionTitle(theme, `${accent} Start`),
|
|
25
25
|
command('init', 'Required once: create .kgraph/ workspace'),
|
|
26
26
|
command('init --integrations codex,gemini', 'Optional: initialize and connect named AI tools'),
|
|
27
|
+
command('init --integrations copilot --mcp', 'Optional: also register KGraph as a VS Code MCP server'),
|
|
27
28
|
'',
|
|
28
29
|
sectionTitle(theme, `${accent} Daily workflow`),
|
|
29
30
|
command('kgraph', 'Refresh scan maps and process pending capture notes'),
|
|
@@ -55,11 +56,13 @@ export function renderRootHelp(useColor = supportsColor()) {
|
|
|
55
56
|
command('uninstall --yes --memory', 'Also remove Copilot memory rule'),
|
|
56
57
|
command('visualize', 'Interactive dependency graph at http://localhost:4242'),
|
|
57
58
|
command('history "blog button"', 'Search processed cognition sessions'),
|
|
59
|
+
command('mcp', 'Start the local MCP server over stdio'),
|
|
58
60
|
'',
|
|
59
61
|
sectionTitle(theme, `${accent} Integrations`),
|
|
60
62
|
command('integrate list', 'Show configured AI tool integrations'),
|
|
61
63
|
command('integrate add gemini windsurf cline', 'Write KGraph instructions using always mode by default'),
|
|
62
64
|
command('integrate add copilot --mode smart', 'Run KGraph for repo-specific Copilot work only'),
|
|
65
|
+
command('integrate add copilot --mcp', 'Also register the KGraph MCP server in VS Code'),
|
|
63
66
|
command('integrate set copilot --mode manual', 'Only run KGraph when explicitly requested'),
|
|
64
67
|
command('integrate remove cursor', 'Remove KGraph-managed instruction blocks'),
|
|
65
68
|
command('--mode smart|always|manual|off', 'Control automatic KGraph involvement per integration'),
|
package/dist/cli/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import { registerImpactCommand } from './commands/impact.js';
|
|
|
13
13
|
import { registerInitCommand } from './commands/init.js';
|
|
14
14
|
import { registerIntegrateCommand } from './commands/integrate.js';
|
|
15
15
|
import { registerKnowledgeCommand } from './commands/knowledge.js';
|
|
16
|
+
import { registerMcpCommand } from './commands/mcp.js';
|
|
16
17
|
import { registerPackCommand } from './commands/pack.js';
|
|
17
18
|
import { registerRepairCommand } from './commands/repair.js';
|
|
18
19
|
import { registerScanCommand } from './commands/scan.js';
|
|
@@ -71,6 +72,7 @@ export function createProgram() {
|
|
|
71
72
|
registerContextCommand(program);
|
|
72
73
|
registerPackCommand(program);
|
|
73
74
|
registerKnowledgeCommand(program);
|
|
75
|
+
registerMcpCommand(program);
|
|
74
76
|
registerStaleCommand(program);
|
|
75
77
|
registerBlameCommand(program);
|
|
76
78
|
registerImpactCommand(program);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { KGraphWorkspace } from '../types/config.js';
|
|
2
|
+
export interface VSCodeMcpInstallResult {
|
|
3
|
+
configPath: string;
|
|
4
|
+
serverName: string;
|
|
5
|
+
command: string;
|
|
6
|
+
args: string[];
|
|
7
|
+
changed: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function installVSCodeMcpServer(workspace: KGraphWorkspace): Promise<VSCodeMcpInstallResult>;
|
|
10
|
+
export declare function resolveVSCodeMcpConfigPath(): string;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { KGraphError } from '../cli/errors.js';
|
|
5
|
+
const SERVER_NAME = 'KGraph';
|
|
6
|
+
export async function installVSCodeMcpServer(workspace) {
|
|
7
|
+
const configPath = resolveVSCodeMcpConfigPath();
|
|
8
|
+
const command = resolveKGraphMcpCommand();
|
|
9
|
+
const args = ['mcp', '--root', workspace.rootPath];
|
|
10
|
+
const existing = await readVSCodeMcpConfig(configPath);
|
|
11
|
+
const servers = normalizeServers(existing.servers);
|
|
12
|
+
const nextServer = {
|
|
13
|
+
command,
|
|
14
|
+
args,
|
|
15
|
+
type: 'stdio',
|
|
16
|
+
};
|
|
17
|
+
const previous = servers[SERVER_NAME];
|
|
18
|
+
servers[SERVER_NAME] = nextServer;
|
|
19
|
+
const next = {
|
|
20
|
+
...existing,
|
|
21
|
+
servers,
|
|
22
|
+
inputs: Array.isArray(existing.inputs) ? existing.inputs : [],
|
|
23
|
+
};
|
|
24
|
+
const changed = JSON.stringify(previous ?? null) !== JSON.stringify(nextServer);
|
|
25
|
+
await mkdir(path.dirname(configPath), { recursive: true });
|
|
26
|
+
await writeFile(configPath, `${JSON.stringify(next, null, 2)}\n`, 'utf8');
|
|
27
|
+
return { configPath, serverName: SERVER_NAME, command, args, changed };
|
|
28
|
+
}
|
|
29
|
+
export function resolveVSCodeMcpConfigPath() {
|
|
30
|
+
if (process.env.KGRAPH_VSCODE_MCP_CONFIG) {
|
|
31
|
+
return process.env.KGRAPH_VSCODE_MCP_CONFIG;
|
|
32
|
+
}
|
|
33
|
+
if (process.platform === 'darwin') {
|
|
34
|
+
return path.join(os.homedir(), 'Library', 'Application Support', 'Code', 'User', 'mcp.json');
|
|
35
|
+
}
|
|
36
|
+
if (process.platform === 'win32') {
|
|
37
|
+
const appData = process.env.APPDATA;
|
|
38
|
+
if (!appData) {
|
|
39
|
+
throw new KGraphError('APPDATA is not set; cannot locate VS Code MCP config.');
|
|
40
|
+
}
|
|
41
|
+
return path.join(appData, 'Code', 'User', 'mcp.json');
|
|
42
|
+
}
|
|
43
|
+
return path.join(os.homedir(), '.config', 'Code', 'User', 'mcp.json');
|
|
44
|
+
}
|
|
45
|
+
function resolveKGraphMcpCommand() {
|
|
46
|
+
if (process.env.KGRAPH_MCP_COMMAND) {
|
|
47
|
+
return process.env.KGRAPH_MCP_COMMAND;
|
|
48
|
+
}
|
|
49
|
+
const entrypoint = process.argv[1];
|
|
50
|
+
if (entrypoint && path.basename(entrypoint).startsWith('kgraph')) {
|
|
51
|
+
return entrypoint;
|
|
52
|
+
}
|
|
53
|
+
return 'kgraph';
|
|
54
|
+
}
|
|
55
|
+
async function readVSCodeMcpConfig(configPath) {
|
|
56
|
+
try {
|
|
57
|
+
return JSON.parse(await readFile(configPath, 'utf8'));
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (isMissingFile(error)) {
|
|
61
|
+
return { servers: {}, inputs: [] };
|
|
62
|
+
}
|
|
63
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
64
|
+
throw new KGraphError(`Invalid VS Code MCP config at ${configPath}: ${message}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function normalizeServers(value) {
|
|
68
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
69
|
+
? { ...value }
|
|
70
|
+
: {};
|
|
71
|
+
}
|
|
72
|
+
function isMissingFile(error) {
|
|
73
|
+
return (error instanceof Error &&
|
|
74
|
+
'code' in error &&
|
|
75
|
+
error.code === 'ENOENT');
|
|
76
|
+
}
|
|
@@ -32,6 +32,7 @@ const COMPACT_STEP = `Run \`kgraph compact --dry-run\` when cognition looks dupl
|
|
|
32
32
|
const HISTORY_STEP = `Run \`kgraph history "<topic>"\` when the user asks what was done, decided, or changed — it covers the full timeline including notes captured via \`conclude\`, \`--capture\`, and inbox. Prefer history over \`knowledge list\` when the question is about sequence, timing, or authorship rather than atom details.`;
|
|
33
33
|
const KNOWLEDGE_STEP = `Run \`kgraph knowledge list --topic "<topic>"\` or \`kgraph knowledge get <atom-id>\` when the user asks what KGraph remembers or atom provenance/lifecycle matters.`;
|
|
34
34
|
const STALE_STEP = `Run \`kgraph stale\` when changed or deleted code may have invalidated durable knowledge. Run \`kgraph blame <atom-id>\` when provenance or evidence for a memory matters.`;
|
|
35
|
+
const MCP_ROUTING_STEP = `If MCP tools named \`kgraph_*\` are available in this client, prefer those tools for KGraph orchestration and context. If MCP is not available, use the normal CLI commands shown here. MCP changes the transport, not the memory policy.`;
|
|
35
36
|
const EXPLORATION_BOUNDARY_STEP = `Keep exploration bounded by the task. For simple edits, use KGraph to identify the likely file, then read only that file or a narrow range and make the edit. Do not keep searching after the target file is found, do not retry malformed shell commands with broader variants, and do not run broad \`find\`, recursive \`grep\`, or repeated full-file dumps after KGraph already returned candidate files.`;
|
|
36
37
|
const VERIFY_EDIT_STEP = `After editing, verify the change actually landed before claiming completion. Prefer a narrow read of the changed range or \`git diff -- <path>\`; if there is no diff or the expected text is missing, say the edit did not apply and fix it before summarizing.`;
|
|
37
38
|
const SCAN_STEP = `After bulk file creation, deletion, or rename (3+ files), run \`kgraph scan\` before the next \`kgraph pack\` so maps stay accurate.`;
|
|
@@ -48,24 +49,25 @@ function sessionStep(agentName, qualifier) {
|
|
|
48
49
|
*/
|
|
49
50
|
export function numberedWorkflow(agentName, options = {}) {
|
|
50
51
|
return `1. Infer the topic from the user's request.
|
|
51
|
-
2. {
|
|
52
|
-
3.
|
|
53
|
-
4.
|
|
54
|
-
5. ${
|
|
55
|
-
6. ${
|
|
56
|
-
7. ${
|
|
57
|
-
8. ${
|
|
58
|
-
9. ${
|
|
59
|
-
10. ${
|
|
60
|
-
11. ${
|
|
61
|
-
12. ${
|
|
52
|
+
2. ${MCP_ROUTING_STEP}
|
|
53
|
+
3. {{KGRAPH_CONTEXT_POLICY}}
|
|
54
|
+
4. When a pack item includes an \`excerpt\` field (symbol) or a \`content\` field (file), you already have the source code inline — do not read that file separately. Items in the \`omitted\` array were evaluated and excluded — do not manually search for them unless the user explicitly asks.
|
|
55
|
+
5. ${EXPLORATION_BOUNDARY_STEP}
|
|
56
|
+
6. ${VERIFY_EDIT_STEP}
|
|
57
|
+
7. ${smartRootStep(agentName)}
|
|
58
|
+
8. ${KNOWLEDGE_STEP}
|
|
59
|
+
9. ${DOCTOR_STEP}
|
|
60
|
+
10. ${STALE_STEP}
|
|
61
|
+
11. ${SCAN_STEP}
|
|
62
|
+
12. ${sessionStep(agentName, options.sessionQualifier)}
|
|
63
|
+
13. ${IMPACT_STEP}
|
|
62
64
|
|
|
63
65
|
{{KGRAPH_CAPTURE_POLICY}}
|
|
64
66
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
14. ${REPAIR_STEP}
|
|
68
|
+
15. ${COMPACT_STEP}
|
|
69
|
+
16. Run \`kgraph visualize\` when the user wants to inspect the dependency graph — opens an interactive graph locally with PNG export.
|
|
70
|
+
17. ${HISTORY_STEP}
|
|
69
71
|
|
|
70
72
|
${DECISION_CONTEXT}`;
|
|
71
73
|
}
|
|
@@ -74,7 +76,8 @@ ${DECISION_CONTEXT}`;
|
|
|
74
76
|
* Used by: cursor, cline, windsurf, gemini.
|
|
75
77
|
*/
|
|
76
78
|
export function bulletWorkflow(agentName, options = {}) {
|
|
77
|
-
return `- {
|
|
79
|
+
return `- ${MCP_ROUTING_STEP}
|
|
80
|
+
- {{KGRAPH_CONTEXT_POLICY}}
|
|
78
81
|
- When a pack item includes an \`excerpt\` field (symbol) or a \`content\` field (file), you already have the source code inline — do not read that file separately. Items in the \`omitted\` array were evaluated and excluded — do not manually search for them unless the user explicitly asks.
|
|
79
82
|
- ${EXPLORATION_BOUNDARY_STEP}
|
|
80
83
|
- ${VERIFY_EDIT_STEP}
|
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
import { createProgram } from '../cli/index.js';
|
|
2
|
+
import { KGraphError } from '../cli/errors.js';
|
|
3
|
+
import { concludeTopic } from '../cognition/conclusion.js';
|
|
4
|
+
import { normalizeConfidence, normalizeKind } from '../cli/commands/conclude.js';
|
|
5
|
+
import { loadConfig } from '../config/config.js';
|
|
6
|
+
import { buildContextPack } from '../context/context-pack.js';
|
|
7
|
+
import { queryContext } from '../context/context-query.js';
|
|
8
|
+
import { analyzeImpact } from '../context/impact.js';
|
|
9
|
+
import { atomToCognitionNote, refreshKnowledgeAtomStatuses, } from '../knowledge/atom-store.js';
|
|
10
|
+
import { assertSessionAgent, recordSessionEvent, } from '../session/session-store.js';
|
|
11
|
+
import { listInboxNotes } from '../storage/cognition-store.js';
|
|
12
|
+
import { assertWorkspace } from '../storage/kgraph-paths.js';
|
|
13
|
+
import { mapsExist, readMaps } from '../storage/map-store.js';
|
|
14
|
+
export async function runMcpServer(options = {}) {
|
|
15
|
+
const server = new KGraphMcpServer(options.rootPath ?? process.cwd());
|
|
16
|
+
await server.run(options.stdin ?? process.stdin, options.stdout ?? process.stdout);
|
|
17
|
+
}
|
|
18
|
+
class KGraphMcpServer {
|
|
19
|
+
defaultRootPath;
|
|
20
|
+
tools;
|
|
21
|
+
constructor(defaultRootPath) {
|
|
22
|
+
this.defaultRootPath = defaultRootPath;
|
|
23
|
+
this.tools = createTools(defaultRootPath);
|
|
24
|
+
}
|
|
25
|
+
async run(stdin, stdout) {
|
|
26
|
+
let buffer = Buffer.alloc(0);
|
|
27
|
+
const pending = [];
|
|
28
|
+
let queue = Promise.resolve();
|
|
29
|
+
stdin.on('data', (chunk) => {
|
|
30
|
+
buffer = Buffer.concat([buffer, Buffer.from(chunk)]);
|
|
31
|
+
const parsed = parseMessages(buffer);
|
|
32
|
+
buffer = parsed.remaining;
|
|
33
|
+
for (const raw of parsed.messages) {
|
|
34
|
+
queue = queue.then(() => this.handleRawMessage(raw, stdout));
|
|
35
|
+
pending.push(queue);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
await new Promise((resolve) => stdin.on('end', () => resolve()));
|
|
39
|
+
await Promise.all(pending);
|
|
40
|
+
}
|
|
41
|
+
async handleRawMessage(raw, stdout) {
|
|
42
|
+
let request;
|
|
43
|
+
try {
|
|
44
|
+
request = JSON.parse(raw);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
writeMessage(stdout, {
|
|
48
|
+
jsonrpc: '2.0',
|
|
49
|
+
id: null,
|
|
50
|
+
error: { code: -32700, message: 'Parse error' },
|
|
51
|
+
});
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (request.id === undefined) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const result = await this.dispatch(request);
|
|
59
|
+
writeMessage(stdout, { jsonrpc: '2.0', id: request.id, result });
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
63
|
+
writeMessage(stdout, {
|
|
64
|
+
jsonrpc: '2.0',
|
|
65
|
+
id: request.id,
|
|
66
|
+
error: { code: -32000, message },
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async dispatch(request) {
|
|
71
|
+
if (request.method === 'initialize') {
|
|
72
|
+
return {
|
|
73
|
+
protocolVersion: '2025-06-18',
|
|
74
|
+
capabilities: { tools: {} },
|
|
75
|
+
serverInfo: { name: 'kgraph', version: '0.1.0' },
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
if (request.method === 'ping') {
|
|
79
|
+
return {};
|
|
80
|
+
}
|
|
81
|
+
if (request.method === 'tools/list') {
|
|
82
|
+
return {
|
|
83
|
+
tools: this.tools.map((tool) => ({
|
|
84
|
+
name: tool.name,
|
|
85
|
+
description: `${tool.description} Mutability: ${tool.mutability}.`,
|
|
86
|
+
inputSchema: tool.inputSchema,
|
|
87
|
+
})),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (request.method === 'tools/call') {
|
|
91
|
+
const name = stringParam(request.params, 'name');
|
|
92
|
+
const args = objectParam(request.params, 'arguments', {});
|
|
93
|
+
const tool = this.tools.find((candidate) => candidate.name === name);
|
|
94
|
+
if (!tool) {
|
|
95
|
+
throw new KGraphError(`Unknown MCP tool "${name}".`);
|
|
96
|
+
}
|
|
97
|
+
const result = await tool.handler(args);
|
|
98
|
+
return {
|
|
99
|
+
content: [{ type: 'text', text: result.text }],
|
|
100
|
+
structuredContent: result.structuredContent ?? {},
|
|
101
|
+
isError: result.isError ?? false,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
throw new KGraphError(`Unsupported MCP method "${request.method}".`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function createTools(defaultRootPath) {
|
|
108
|
+
const command = commandTool(defaultRootPath);
|
|
109
|
+
const commandBacked = (name, description, mutability, buildArgs, inputSchema = schema({})) => ({
|
|
110
|
+
name,
|
|
111
|
+
description,
|
|
112
|
+
mutability,
|
|
113
|
+
inputSchema,
|
|
114
|
+
handler: async (args) => command.handler({ ...args, args: buildArgs(args) }),
|
|
115
|
+
});
|
|
116
|
+
return [
|
|
117
|
+
orchestrateTool(defaultRootPath),
|
|
118
|
+
contextPackTool(defaultRootPath),
|
|
119
|
+
impactTool(defaultRootPath),
|
|
120
|
+
captureTool(defaultRootPath),
|
|
121
|
+
healthTool(defaultRootPath),
|
|
122
|
+
command,
|
|
123
|
+
commandBacked('kgraph_scan', 'Refresh deterministic file, symbol, import, and relationship maps.', 'repo-write', (args) => [
|
|
124
|
+
'scan',
|
|
125
|
+
...(booleanParam(args, 'verbose') ? ['--verbose'] : []),
|
|
126
|
+
]),
|
|
127
|
+
commandBacked('kgraph_update', 'Process Markdown cognition notes from .kgraph/inbox.', 'repo-write', (args) => [
|
|
128
|
+
'update',
|
|
129
|
+
...(booleanParam(args, 'dryRun') ? ['--dry-run'] : []),
|
|
130
|
+
]),
|
|
131
|
+
commandBacked('kgraph_context', 'Return compact repo context for a query.', 'read-only', (args) => [
|
|
132
|
+
'context',
|
|
133
|
+
requiredString(args, 'query'),
|
|
134
|
+
'--json',
|
|
135
|
+
], schema({ query: { type: 'string' } }, ['query'])),
|
|
136
|
+
commandBacked('kgraph_pack', 'Build a budget-aware context pack.', 'read-only', (args) => [
|
|
137
|
+
'pack',
|
|
138
|
+
requiredString(args, 'task'),
|
|
139
|
+
'--budget',
|
|
140
|
+
String(numberParam(args, 'budget', 8000)),
|
|
141
|
+
'--json',
|
|
142
|
+
...agentArgs(args),
|
|
143
|
+
], schema({ task: { type: 'string' }, budget: { type: 'number' }, agent: { type: 'string' } }, ['task'])),
|
|
144
|
+
commandBacked('kgraph_history', 'Show processed cognition session history.', 'read-only', (args) => [
|
|
145
|
+
'history',
|
|
146
|
+
...optionalWords(args, 'query'),
|
|
147
|
+
...numberOption(args, 'last', '--last'),
|
|
148
|
+
'--json',
|
|
149
|
+
]),
|
|
150
|
+
commandBacked('kgraph_stale', 'Show stale or needs-review knowledge atoms.', 'read-only', () => [
|
|
151
|
+
'stale',
|
|
152
|
+
'--json',
|
|
153
|
+
]),
|
|
154
|
+
commandBacked('kgraph_blame', 'Show provenance and evidence for a knowledge atom.', 'read-only', (args) => [
|
|
155
|
+
'blame',
|
|
156
|
+
requiredString(args, 'atomId'),
|
|
157
|
+
'--json',
|
|
158
|
+
], schema({ atomId: { type: 'string' } }, ['atomId'])),
|
|
159
|
+
commandBacked('kgraph_doctor', 'Check KGraph workspace health.', 'read-only', (args) => [
|
|
160
|
+
'doctor',
|
|
161
|
+
...(booleanParam(args, 'quality') ? ['--quality'] : []),
|
|
162
|
+
]),
|
|
163
|
+
commandBacked('kgraph_repair', 'Clean noisy stale references from knowledge atoms.', 'repo-write', (args) => [
|
|
164
|
+
'repair',
|
|
165
|
+
...(booleanParam(args, 'dryRun', true) ? ['--dry-run'] : []),
|
|
166
|
+
]),
|
|
167
|
+
commandBacked('kgraph_compact', 'Merge duplicate cognition and archive low-value stale entries.', 'repo-write', (args) => [
|
|
168
|
+
'compact',
|
|
169
|
+
...(booleanParam(args, 'dryRun', true) ? ['--dry-run'] : []),
|
|
170
|
+
'--json',
|
|
171
|
+
]),
|
|
172
|
+
commandBacked('kgraph_knowledge_list', 'List canonical knowledge atoms.', 'read-only', (args) => [
|
|
173
|
+
'knowledge',
|
|
174
|
+
'list',
|
|
175
|
+
...stringOption(args, 'type', '--type'),
|
|
176
|
+
...stringOption(args, 'topic', '--topic'),
|
|
177
|
+
...(booleanParam(args, 'includeArchived') ? ['--include-archived'] : []),
|
|
178
|
+
'--json',
|
|
179
|
+
]),
|
|
180
|
+
commandBacked('kgraph_knowledge_get', 'Get one canonical knowledge atom.', 'read-only', (args) => [
|
|
181
|
+
'knowledge',
|
|
182
|
+
'get',
|
|
183
|
+
requiredString(args, 'atomId'),
|
|
184
|
+
'--json',
|
|
185
|
+
], schema({ atomId: { type: 'string' } }, ['atomId'])),
|
|
186
|
+
commandBacked('kgraph_knowledge_archive', 'Archive one knowledge atom.', 'repo-write', (args) => [
|
|
187
|
+
'knowledge',
|
|
188
|
+
'archive',
|
|
189
|
+
requiredString(args, 'atomId'),
|
|
190
|
+
'--json',
|
|
191
|
+
], schema({ atomId: { type: 'string' } }, ['atomId'])),
|
|
192
|
+
commandBacked('kgraph_knowledge_supersede', 'Mark one atom as superseded by another.', 'repo-write', (args) => [
|
|
193
|
+
'knowledge',
|
|
194
|
+
'supersede',
|
|
195
|
+
requiredString(args, 'oldId'),
|
|
196
|
+
requiredString(args, 'newId'),
|
|
197
|
+
'--json',
|
|
198
|
+
], schema({ oldId: { type: 'string' }, newId: { type: 'string' } }, ['oldId', 'newId'])),
|
|
199
|
+
commandBacked('kgraph_session_report', 'Report agent read/write activity and token estimates.', 'read-only', () => [
|
|
200
|
+
'session',
|
|
201
|
+
'--json',
|
|
202
|
+
]),
|
|
203
|
+
commandBacked('kgraph_session_start', 'Start lightweight session tracking for an agent.', 'repo-write', (args) => [
|
|
204
|
+
'session',
|
|
205
|
+
'start',
|
|
206
|
+
...requiredAgentArgs(args),
|
|
207
|
+
...sourceArgs(args),
|
|
208
|
+
]),
|
|
209
|
+
commandBacked('kgraph_session_read', 'Record an agent file read.', 'repo-write', (args) => [
|
|
210
|
+
'session',
|
|
211
|
+
'read',
|
|
212
|
+
requiredString(args, 'path'),
|
|
213
|
+
...requiredAgentArgs(args),
|
|
214
|
+
...sourceArgs(args),
|
|
215
|
+
]),
|
|
216
|
+
commandBacked('kgraph_session_write', 'Record an agent file write.', 'repo-write', (args) => [
|
|
217
|
+
'session',
|
|
218
|
+
'write',
|
|
219
|
+
requiredString(args, 'path'),
|
|
220
|
+
...requiredAgentArgs(args),
|
|
221
|
+
...sourceArgs(args),
|
|
222
|
+
]),
|
|
223
|
+
commandBacked('kgraph_session_end', 'End session tracking for an agent.', 'repo-write', (args) => [
|
|
224
|
+
'session',
|
|
225
|
+
'end',
|
|
226
|
+
...requiredAgentArgs(args),
|
|
227
|
+
...(booleanParam(args, 'conclude') ? ['--conclude'] : []),
|
|
228
|
+
...stringOption(args, 'topic', '--topic'),
|
|
229
|
+
...stringOption(args, 'note', '--note'),
|
|
230
|
+
...stringOption(args, 'confidence', '--confidence'),
|
|
231
|
+
...sourceArgs(args),
|
|
232
|
+
]),
|
|
233
|
+
commandBacked('kgraph_session_reset', 'Clear current session tracking.', 'destructive', () => [
|
|
234
|
+
'session',
|
|
235
|
+
'reset',
|
|
236
|
+
]),
|
|
237
|
+
commandBacked('kgraph_integrate_list', 'List configured AI tool integrations.', 'read-only', () => [
|
|
238
|
+
'integrate',
|
|
239
|
+
'list',
|
|
240
|
+
]),
|
|
241
|
+
commandBacked('kgraph_integrate_add', 'Add AI tool integrations.', 'repo-write', (args) => [
|
|
242
|
+
'integrate',
|
|
243
|
+
'add',
|
|
244
|
+
...stringArray(args, 'names'),
|
|
245
|
+
...stringOption(args, 'mode', '--mode'),
|
|
246
|
+
]),
|
|
247
|
+
commandBacked('kgraph_integrate_set', 'Set AI tool integration modes.', 'repo-write', (args) => [
|
|
248
|
+
'integrate',
|
|
249
|
+
'set',
|
|
250
|
+
...stringArray(args, 'names'),
|
|
251
|
+
...stringOption(args, 'mode', '--mode'),
|
|
252
|
+
]),
|
|
253
|
+
commandBacked('kgraph_integrate_remove', 'Remove AI tool integrations.', 'destructive', (args) => [
|
|
254
|
+
'integrate',
|
|
255
|
+
'remove',
|
|
256
|
+
...stringArray(args, 'names'),
|
|
257
|
+
]),
|
|
258
|
+
commandBacked('kgraph_init', 'Initialize a .kgraph workspace.', 'repo-write', (args) => [
|
|
259
|
+
'init',
|
|
260
|
+
...stringOption(args, 'integration', '--integration'),
|
|
261
|
+
...stringOption(args, 'integrations', '--integrations'),
|
|
262
|
+
...(booleanParam(args, 'yes') ? ['--yes'] : []),
|
|
263
|
+
]),
|
|
264
|
+
commandBacked('kgraph_uninstall', 'Preview or remove KGraph from this repository.', 'destructive', (args) => [
|
|
265
|
+
'uninstall',
|
|
266
|
+
...(booleanParam(args, 'yes') ? ['--yes'] : []),
|
|
267
|
+
...(booleanParam(args, 'keepIntegrations') ? ['--keep-integrations'] : []),
|
|
268
|
+
...(booleanParam(args, 'memory') ? ['--memory'] : []),
|
|
269
|
+
]),
|
|
270
|
+
commandBacked('kgraph_visualize', 'Return visualization command output or local URL guidance.', 'read-only', (args) => [
|
|
271
|
+
'visualize',
|
|
272
|
+
'--no-open',
|
|
273
|
+
...numberOption(args, 'port', '--port'),
|
|
274
|
+
]),
|
|
275
|
+
];
|
|
276
|
+
}
|
|
277
|
+
function commandTool(defaultRootPath) {
|
|
278
|
+
return {
|
|
279
|
+
name: 'kgraph_command',
|
|
280
|
+
description: 'Compatibility wrapper for the exact KGraph CLI surface. Prefer typed kgraph_* MCP tools when possible.',
|
|
281
|
+
mutability: 'repo-write',
|
|
282
|
+
inputSchema: schema({
|
|
283
|
+
args: { type: 'array', items: { type: 'string' } },
|
|
284
|
+
rootPath: { type: 'string' },
|
|
285
|
+
}, ['args']),
|
|
286
|
+
handler: async (args) => {
|
|
287
|
+
const commandArgs = stringArray(args, 'args');
|
|
288
|
+
const result = await runCliInProcess(stringParam(args, 'rootPath', defaultRootPath), commandArgs);
|
|
289
|
+
const parsed = parseJsonOrUndefined(result.stdout);
|
|
290
|
+
const structuredContent = {
|
|
291
|
+
args: commandArgs,
|
|
292
|
+
exitCode: result.code,
|
|
293
|
+
stdout: result.stdout,
|
|
294
|
+
stderr: result.stderr,
|
|
295
|
+
};
|
|
296
|
+
if (parsed !== undefined) {
|
|
297
|
+
structuredContent.parsed = parsed;
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
text: summarizeCommand(commandArgs, result),
|
|
301
|
+
structuredContent,
|
|
302
|
+
isError: result.code !== 0,
|
|
303
|
+
};
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
function orchestrateTool(defaultRootPath) {
|
|
308
|
+
return {
|
|
309
|
+
name: 'kgraph_orchestrate',
|
|
310
|
+
description: 'Smart root workflow: refresh maps, process inbox notes, optionally return topic context, run final checks, or capture durable knowledge.',
|
|
311
|
+
mutability: 'repo-write',
|
|
312
|
+
inputSchema: schema({
|
|
313
|
+
topic: { type: 'string' },
|
|
314
|
+
final: { type: 'boolean' },
|
|
315
|
+
capture: { type: 'string' },
|
|
316
|
+
captureType: { type: 'string' },
|
|
317
|
+
confidence: { type: 'string' },
|
|
318
|
+
domain: { type: 'string' },
|
|
319
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
320
|
+
files: { type: 'array', items: { type: 'string' } },
|
|
321
|
+
symbols: { type: 'array', items: { type: 'string' } },
|
|
322
|
+
agent: { type: 'string' },
|
|
323
|
+
rootPath: { type: 'string' },
|
|
324
|
+
}),
|
|
325
|
+
handler: async (args) => {
|
|
326
|
+
const cliArgs = [
|
|
327
|
+
...optionalWords(args, 'topic'),
|
|
328
|
+
...(booleanParam(args, 'final') ? ['--final'] : []),
|
|
329
|
+
...stringOption(args, 'capture', '--capture'),
|
|
330
|
+
...stringOption(args, 'captureType', '--capture-type'),
|
|
331
|
+
...stringOption(args, 'confidence', '--capture-confidence'),
|
|
332
|
+
...stringOption(args, 'domain', '--capture-domain'),
|
|
333
|
+
...repeatOption(args, 'tags', '--capture-tag'),
|
|
334
|
+
...repeatOption(args, 'files', '--capture-file'),
|
|
335
|
+
...repeatOption(args, 'symbols', '--capture-symbol'),
|
|
336
|
+
...agentArgs(args),
|
|
337
|
+
];
|
|
338
|
+
const result = await runCliInProcess(stringParam(args, 'rootPath', defaultRootPath), cliArgs);
|
|
339
|
+
return {
|
|
340
|
+
text: summarizeCommand(cliArgs, result),
|
|
341
|
+
structuredContent: {
|
|
342
|
+
args: cliArgs,
|
|
343
|
+
exitCode: result.code,
|
|
344
|
+
stdout: result.stdout,
|
|
345
|
+
stderr: result.stderr,
|
|
346
|
+
},
|
|
347
|
+
isError: result.code !== 0,
|
|
348
|
+
};
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
function contextPackTool(defaultRootPath) {
|
|
353
|
+
return {
|
|
354
|
+
name: 'kgraph_context_pack',
|
|
355
|
+
description: 'Build a structured budget-aware ContextPack directly from KGraph internals.',
|
|
356
|
+
mutability: 'read-only',
|
|
357
|
+
inputSchema: schema({
|
|
358
|
+
task: { type: 'string' },
|
|
359
|
+
budget: { type: 'number' },
|
|
360
|
+
agent: { type: 'string' },
|
|
361
|
+
rootPath: { type: 'string' },
|
|
362
|
+
}, ['task']),
|
|
363
|
+
handler: async (args) => {
|
|
364
|
+
const task = requiredString(args, 'task');
|
|
365
|
+
const budget = numberParam(args, 'budget', 8000);
|
|
366
|
+
const workspace = await assertWorkspace(stringParam(args, 'rootPath', defaultRootPath));
|
|
367
|
+
if (!(await mapsExist(workspace))) {
|
|
368
|
+
throw new KGraphError('KGraph maps are missing. Run `kgraph scan` first.');
|
|
369
|
+
}
|
|
370
|
+
const [config, maps] = await Promise.all([
|
|
371
|
+
loadConfig(workspace),
|
|
372
|
+
readMaps(workspace),
|
|
373
|
+
]);
|
|
374
|
+
const response = await queryContext(workspace, config, maps, task);
|
|
375
|
+
const pack = buildContextPack(response, budget, workspace.rootPath);
|
|
376
|
+
const pendingInboxFiles = await listInboxNotes(workspace);
|
|
377
|
+
if (pendingInboxFiles.length > 0) {
|
|
378
|
+
pack.pendingInbox = {
|
|
379
|
+
count: pendingInboxFiles.length,
|
|
380
|
+
files: pendingInboxFiles,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
const agent = stringParam(args, 'agent', '');
|
|
384
|
+
if (agent) {
|
|
385
|
+
await recordSessionEvent(workspace, {
|
|
386
|
+
agent: assertSessionAgent(agent),
|
|
387
|
+
type: 'context',
|
|
388
|
+
captureSource: 'automatic',
|
|
389
|
+
packUsedTokens: pack.usedTokens,
|
|
390
|
+
packOmittedTokens: pack.omitted.reduce((sum, item) => sum + item.tokenEstimate, 0),
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
return {
|
|
394
|
+
text: `KGraph context pack for "${task}": ${pack.items.length} item(s), ${pack.usedTokens}/${pack.budget} tokens.`,
|
|
395
|
+
structuredContent: pack,
|
|
396
|
+
};
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function impactTool(defaultRootPath) {
|
|
401
|
+
return {
|
|
402
|
+
name: 'kgraph_impact',
|
|
403
|
+
description: 'Analyze practical impact for a file, symbol, or topic.',
|
|
404
|
+
mutability: 'read-only',
|
|
405
|
+
inputSchema: schema({ query: { type: 'string' }, rootPath: { type: 'string' } }, ['query']),
|
|
406
|
+
handler: async (args) => {
|
|
407
|
+
const query = requiredString(args, 'query');
|
|
408
|
+
const workspace = await assertWorkspace(stringParam(args, 'rootPath', defaultRootPath));
|
|
409
|
+
if (!(await mapsExist(workspace))) {
|
|
410
|
+
throw new KGraphError('KGraph maps are missing. Run `kgraph scan` first.');
|
|
411
|
+
}
|
|
412
|
+
const [config, maps] = await Promise.all([
|
|
413
|
+
loadConfig(workspace),
|
|
414
|
+
readMaps(workspace),
|
|
415
|
+
]);
|
|
416
|
+
const { atoms } = await refreshKnowledgeAtomStatuses(workspace, {
|
|
417
|
+
fileMap: maps.fileMap,
|
|
418
|
+
symbolMap: maps.symbolMap,
|
|
419
|
+
});
|
|
420
|
+
const response = analyzeImpact(query, maps, atoms.filter((atom) => atom.status !== 'archived').map(atomToCognitionNote), config.maxContextItems);
|
|
421
|
+
return {
|
|
422
|
+
text: `KGraph impact for "${query}": ${response.files.length} file(s), ${response.symbols.length} symbol(s), ${response.risk.length} risk signal(s).`,
|
|
423
|
+
structuredContent: response,
|
|
424
|
+
};
|
|
425
|
+
},
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
function captureTool(defaultRootPath) {
|
|
429
|
+
return {
|
|
430
|
+
name: 'kgraph_capture',
|
|
431
|
+
description: 'Store a durable typed engineering conclusion.',
|
|
432
|
+
mutability: 'repo-write',
|
|
433
|
+
inputSchema: schema({
|
|
434
|
+
topic: { type: 'string' },
|
|
435
|
+
note: { type: 'string' },
|
|
436
|
+
type: { type: 'string' },
|
|
437
|
+
confidence: { type: 'string' },
|
|
438
|
+
domain: { type: 'string' },
|
|
439
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
440
|
+
files: { type: 'array', items: { type: 'string' } },
|
|
441
|
+
symbols: { type: 'array', items: { type: 'string' } },
|
|
442
|
+
rootPath: { type: 'string' },
|
|
443
|
+
}, ['topic']),
|
|
444
|
+
handler: async (args) => {
|
|
445
|
+
const workspace = await assertWorkspace(stringParam(args, 'rootPath', defaultRootPath));
|
|
446
|
+
const note = await concludeTopic(workspace, {
|
|
447
|
+
topic: requiredString(args, 'topic'),
|
|
448
|
+
body: stringParam(args, 'note', undefined),
|
|
449
|
+
kind: normalizeKind(stringParam(args, 'type', 'summary')),
|
|
450
|
+
confidence: normalizeConfidence(stringParam(args, 'confidence', 'medium')),
|
|
451
|
+
domain: stringParam(args, 'domain', undefined),
|
|
452
|
+
tags: stringArray(args, 'tags', []),
|
|
453
|
+
relatedFiles: stringArray(args, 'files', []),
|
|
454
|
+
relatedSymbols: stringArray(args, 'symbols', []),
|
|
455
|
+
source: 'conclude',
|
|
456
|
+
});
|
|
457
|
+
return {
|
|
458
|
+
text: `Stored ${note.kind} cognition: ${note.title}`,
|
|
459
|
+
structuredContent: note,
|
|
460
|
+
};
|
|
461
|
+
},
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
function healthTool(defaultRootPath) {
|
|
465
|
+
return {
|
|
466
|
+
name: 'kgraph_health',
|
|
467
|
+
description: 'Check KGraph workspace health and optional cognition quality.',
|
|
468
|
+
mutability: 'read-only',
|
|
469
|
+
inputSchema: schema({ quality: { type: 'boolean' }, rootPath: { type: 'string' } }),
|
|
470
|
+
handler: async (args) => {
|
|
471
|
+
const result = await runCliInProcess(stringParam(args, 'rootPath', defaultRootPath), ['doctor', ...(booleanParam(args, 'quality') ? ['--quality'] : [])]);
|
|
472
|
+
return {
|
|
473
|
+
text: summarizeCommand(['doctor'], result),
|
|
474
|
+
structuredContent: {
|
|
475
|
+
exitCode: result.code,
|
|
476
|
+
stdout: result.stdout,
|
|
477
|
+
stderr: result.stderr,
|
|
478
|
+
checks: parseDoctorChecks(result.stdout),
|
|
479
|
+
},
|
|
480
|
+
isError: result.code !== 0,
|
|
481
|
+
};
|
|
482
|
+
},
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
async function runCliInProcess(rootPath, args) {
|
|
486
|
+
const originalCwd = process.cwd();
|
|
487
|
+
const originalStdout = process.stdout.write;
|
|
488
|
+
const originalStderr = process.stderr.write;
|
|
489
|
+
const originalConsoleLog = console.log;
|
|
490
|
+
const originalConsoleError = console.error;
|
|
491
|
+
const originalExitCode = process.exitCode;
|
|
492
|
+
let stdout = '';
|
|
493
|
+
let stderr = '';
|
|
494
|
+
process.chdir(rootPath);
|
|
495
|
+
process.exitCode = undefined;
|
|
496
|
+
process.stdout.write = ((chunk) => {
|
|
497
|
+
stdout += chunk.toString();
|
|
498
|
+
return true;
|
|
499
|
+
});
|
|
500
|
+
process.stderr.write = ((chunk) => {
|
|
501
|
+
stderr += chunk.toString();
|
|
502
|
+
return true;
|
|
503
|
+
});
|
|
504
|
+
console.log = (...items) => {
|
|
505
|
+
stdout += `${items.map(String).join(' ')}\n`;
|
|
506
|
+
};
|
|
507
|
+
console.error = (...items) => {
|
|
508
|
+
stderr += `${items.map(String).join(' ')}\n`;
|
|
509
|
+
};
|
|
510
|
+
try {
|
|
511
|
+
await createProgram().parseAsync(['node', 'kgraph', ...args], {
|
|
512
|
+
from: 'node',
|
|
513
|
+
});
|
|
514
|
+
return {
|
|
515
|
+
stdout,
|
|
516
|
+
stderr,
|
|
517
|
+
code: typeof process.exitCode === 'number' ? process.exitCode : 0,
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
catch (error) {
|
|
521
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
522
|
+
return { stdout, stderr: `${stderr}${message}\n`, code: 1 };
|
|
523
|
+
}
|
|
524
|
+
finally {
|
|
525
|
+
process.stdout.write = originalStdout;
|
|
526
|
+
process.stderr.write = originalStderr;
|
|
527
|
+
console.log = originalConsoleLog;
|
|
528
|
+
console.error = originalConsoleError;
|
|
529
|
+
process.exitCode = originalExitCode;
|
|
530
|
+
process.chdir(originalCwd);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
function parseMessages(buffer) {
|
|
534
|
+
const messages = [];
|
|
535
|
+
let remaining = buffer;
|
|
536
|
+
while (remaining.length > 0) {
|
|
537
|
+
const headerEnd = remaining.indexOf('\r\n\r\n');
|
|
538
|
+
if (headerEnd === -1) {
|
|
539
|
+
const newline = remaining.indexOf('\n');
|
|
540
|
+
if (newline === -1)
|
|
541
|
+
break;
|
|
542
|
+
const line = remaining.subarray(0, newline).toString('utf8').trim();
|
|
543
|
+
if (!line) {
|
|
544
|
+
remaining = remaining.subarray(newline + 1);
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
messages.push(line);
|
|
548
|
+
remaining = remaining.subarray(newline + 1);
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
const header = remaining.subarray(0, headerEnd).toString('utf8');
|
|
552
|
+
const match = /content-length:\s*(\d+)/i.exec(header);
|
|
553
|
+
if (!match) {
|
|
554
|
+
remaining = remaining.subarray(headerEnd + 4);
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
const length = Number.parseInt(match[1], 10);
|
|
558
|
+
const bodyStart = headerEnd + 4;
|
|
559
|
+
const bodyEnd = bodyStart + length;
|
|
560
|
+
if (remaining.length < bodyEnd)
|
|
561
|
+
break;
|
|
562
|
+
messages.push(remaining.subarray(bodyStart, bodyEnd).toString('utf8'));
|
|
563
|
+
remaining = remaining.subarray(bodyEnd);
|
|
564
|
+
}
|
|
565
|
+
return { messages, remaining };
|
|
566
|
+
}
|
|
567
|
+
function writeMessage(stdout, message) {
|
|
568
|
+
stdout.write(`${JSON.stringify(message)}\n`);
|
|
569
|
+
}
|
|
570
|
+
function schema(properties, required = []) {
|
|
571
|
+
return {
|
|
572
|
+
type: 'object',
|
|
573
|
+
properties: {
|
|
574
|
+
rootPath: {
|
|
575
|
+
type: 'string',
|
|
576
|
+
description: 'Repository root path. Defaults to the MCP server cwd.',
|
|
577
|
+
},
|
|
578
|
+
...properties,
|
|
579
|
+
},
|
|
580
|
+
required,
|
|
581
|
+
additionalProperties: false,
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
function objectParam(params, key, fallback) {
|
|
585
|
+
const value = params?.[key];
|
|
586
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
587
|
+
? value
|
|
588
|
+
: fallback;
|
|
589
|
+
}
|
|
590
|
+
function stringParam(args, key, fallback = '') {
|
|
591
|
+
const value = args?.[key];
|
|
592
|
+
return typeof value === 'string' ? value : fallback;
|
|
593
|
+
}
|
|
594
|
+
function requiredString(args, key) {
|
|
595
|
+
const value = stringParam(args, key).trim();
|
|
596
|
+
if (!value)
|
|
597
|
+
throw new KGraphError(`${key} is required.`);
|
|
598
|
+
return value;
|
|
599
|
+
}
|
|
600
|
+
function booleanParam(args, key, fallback = false) {
|
|
601
|
+
const value = args[key];
|
|
602
|
+
return typeof value === 'boolean' ? value : fallback;
|
|
603
|
+
}
|
|
604
|
+
function numberParam(args, key, fallback) {
|
|
605
|
+
const value = args[key];
|
|
606
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
607
|
+
}
|
|
608
|
+
function stringArray(args, key, fallback = []) {
|
|
609
|
+
const value = args[key];
|
|
610
|
+
if (!Array.isArray(value))
|
|
611
|
+
return fallback;
|
|
612
|
+
return value.filter((item) => typeof item === 'string');
|
|
613
|
+
}
|
|
614
|
+
function stringOption(args, key, flag) {
|
|
615
|
+
const value = stringParam(args, key);
|
|
616
|
+
return value ? [flag, value] : [];
|
|
617
|
+
}
|
|
618
|
+
function numberOption(args, key, flag) {
|
|
619
|
+
const value = args[key];
|
|
620
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
621
|
+
? [flag, String(value)]
|
|
622
|
+
: [];
|
|
623
|
+
}
|
|
624
|
+
function repeatOption(args, key, flag) {
|
|
625
|
+
return stringArray(args, key).flatMap((value) => [flag, value]);
|
|
626
|
+
}
|
|
627
|
+
function optionalWords(args, key) {
|
|
628
|
+
const value = stringParam(args, key);
|
|
629
|
+
return value ? [value] : [];
|
|
630
|
+
}
|
|
631
|
+
function agentArgs(args) {
|
|
632
|
+
return stringOption(args, 'agent', '--agent');
|
|
633
|
+
}
|
|
634
|
+
function requiredAgentArgs(args) {
|
|
635
|
+
return ['--agent', requiredString(args, 'agent')];
|
|
636
|
+
}
|
|
637
|
+
function sourceArgs(args) {
|
|
638
|
+
return stringOption(args, 'source', '--source');
|
|
639
|
+
}
|
|
640
|
+
function parseJsonOrUndefined(value) {
|
|
641
|
+
try {
|
|
642
|
+
return JSON.parse(value);
|
|
643
|
+
}
|
|
644
|
+
catch {
|
|
645
|
+
return undefined;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
function summarizeCommand(args, result) {
|
|
649
|
+
const command = args.length > 0 ? `kgraph ${args.join(' ')}` : 'kgraph';
|
|
650
|
+
const firstLine = result.stdout.split('\n').find((line) => line.trim()) ??
|
|
651
|
+
result.stderr.split('\n').find((line) => line.trim()) ??
|
|
652
|
+
'no output';
|
|
653
|
+
return `${command} exited ${result.code}: ${firstLine}`;
|
|
654
|
+
}
|
|
655
|
+
function parseDoctorChecks(stdout) {
|
|
656
|
+
return stdout
|
|
657
|
+
.split('\n')
|
|
658
|
+
.map((line) => /^(OK|FAIL)\s+([^:]+):\s*(.*)$/.exec(line))
|
|
659
|
+
.filter((match) => Boolean(match))
|
|
660
|
+
.map((match) => ({
|
|
661
|
+
ok: match[1] === 'OK',
|
|
662
|
+
label: match[2].trim(),
|
|
663
|
+
detail: match[3].trim(),
|
|
664
|
+
}));
|
|
665
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kentwynn/kgraph",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.40",
|
|
4
4
|
"description": "Persistent repo intelligence for AI coding assistants.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"tree-sitter-rust": "^0.24.0",
|
|
76
76
|
"tree-sitter-scala": "^0.24.0",
|
|
77
77
|
"tree-sitter-wasms": "^0.1.13",
|
|
78
|
-
"tsx": "^4.
|
|
78
|
+
"tsx": "^4.22.4",
|
|
79
79
|
"vitest": "^3.2.4"
|
|
80
80
|
},
|
|
81
81
|
"engines": {
|