@thispointon/kondi-chat 0.1.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/LICENSE +21 -0
- package/README.md +556 -0
- package/bin/kondi-chat +56 -0
- package/bin/kondi-chat.js +72 -0
- package/package.json +55 -0
- package/scripts/demo.tape +49 -0
- package/scripts/postinstall.cjs +103 -0
- package/src/audit/analytics.ts +261 -0
- package/src/audit/ledger.ts +253 -0
- package/src/audit/telemetry.ts +165 -0
- package/src/cli/backend.ts +675 -0
- package/src/cli/commands.ts +419 -0
- package/src/cli/help.ts +182 -0
- package/src/cli/submit-helpers.ts +159 -0
- package/src/cli/submit.ts +539 -0
- package/src/cli/wizard.ts +121 -0
- package/src/context/bootstrap.ts +138 -0
- package/src/context/budget.ts +100 -0
- package/src/context/manager.ts +666 -0
- package/src/context/memory.ts +160 -0
- package/src/context/preflight.ts +176 -0
- package/src/context/project-brain.ts +101 -0
- package/src/context/receipts.ts +108 -0
- package/src/context/skills.ts +154 -0
- package/src/context/symbol-index.ts +240 -0
- package/src/council/profiles.ts +137 -0
- package/src/council/tool.ts +138 -0
- package/src/council-engine/cli/council-artifacts.ts +230 -0
- package/src/council-engine/cli/council-config.ts +178 -0
- package/src/council-engine/cli/council-session-export.ts +116 -0
- package/src/council-engine/cli/kondi.ts +98 -0
- package/src/council-engine/cli/llm-caller.ts +229 -0
- package/src/council-engine/cli/localStorage-shim.ts +119 -0
- package/src/council-engine/cli/node-platform.ts +68 -0
- package/src/council-engine/cli/run-council.ts +481 -0
- package/src/council-engine/cli/run-pipeline.ts +772 -0
- package/src/council-engine/cli/session-export.ts +153 -0
- package/src/council-engine/configs/councils/analysis.json +101 -0
- package/src/council-engine/configs/councils/code-planning.json +86 -0
- package/src/council-engine/configs/councils/coding.json +89 -0
- package/src/council-engine/configs/councils/debate.json +97 -0
- package/src/council-engine/configs/councils/solo-claude.json +34 -0
- package/src/council-engine/configs/councils/solo-gpt.json +34 -0
- package/src/council-engine/council/coding-orchestrator.ts +1205 -0
- package/src/council-engine/council/context-bootstrap.ts +147 -0
- package/src/council-engine/council/context-inspection.ts +42 -0
- package/src/council-engine/council/context-store.ts +763 -0
- package/src/council-engine/council/deliberation-orchestrator.ts +2762 -0
- package/src/council-engine/council/factory.ts +164 -0
- package/src/council-engine/council/index.ts +201 -0
- package/src/council-engine/council/ledger-store.ts +438 -0
- package/src/council-engine/council/prompts.ts +1689 -0
- package/src/council-engine/council/storage-cleanup.ts +164 -0
- package/src/council-engine/council/store.ts +1110 -0
- package/src/council-engine/council/synthesis.ts +291 -0
- package/src/council-engine/council/types.ts +845 -0
- package/src/council-engine/council/validation.ts +613 -0
- package/src/council-engine/pipeline/build-detect.ts +73 -0
- package/src/council-engine/pipeline/executor.ts +1048 -0
- package/src/council-engine/pipeline/index.ts +9 -0
- package/src/council-engine/pipeline/install-detect.ts +84 -0
- package/src/council-engine/pipeline/memory-store.ts +182 -0
- package/src/council-engine/pipeline/output-parsers.ts +146 -0
- package/src/council-engine/pipeline/run-output.ts +149 -0
- package/src/council-engine/pipeline/session-import.ts +177 -0
- package/src/council-engine/pipeline/store.ts +753 -0
- package/src/council-engine/pipeline/test-detect.ts +82 -0
- package/src/council-engine/pipeline/types.ts +401 -0
- package/src/council-engine/services/deliberationSummary.ts +114 -0
- package/src/council-engine/tsconfig.json +16 -0
- package/src/council-engine/types/mcp.ts +122 -0
- package/src/council-engine/utils/filterTools.ts +73 -0
- package/src/engine/apply.ts +238 -0
- package/src/engine/checkpoints.ts +237 -0
- package/src/engine/consultants.ts +347 -0
- package/src/engine/diff.ts +171 -0
- package/src/engine/errors.ts +102 -0
- package/src/engine/git-tools.ts +246 -0
- package/src/engine/hooks.ts +181 -0
- package/src/engine/loop-guard.ts +155 -0
- package/src/engine/permissions.ts +293 -0
- package/src/engine/pipeline.ts +376 -0
- package/src/engine/sub-agents.ts +133 -0
- package/src/engine/task-card.ts +185 -0
- package/src/engine/task-router.ts +256 -0
- package/src/engine/task-store.ts +86 -0
- package/src/engine/tools.ts +783 -0
- package/src/engine/verify.ts +111 -0
- package/src/mcp/client.ts +225 -0
- package/src/mcp/config.ts +120 -0
- package/src/mcp/tool-manager.ts +192 -0
- package/src/mcp/types.ts +61 -0
- package/src/providers/llm-caller.ts +943 -0
- package/src/providers/rate-limiter.ts +238 -0
- package/src/router/NOTES.md +28 -0
- package/src/router/collector.ts +474 -0
- package/src/router/embeddings.ts +286 -0
- package/src/router/index.ts +299 -0
- package/src/router/intent-router.ts +225 -0
- package/src/router/nn-router.ts +205 -0
- package/src/router/profiles.ts +309 -0
- package/src/router/registry.ts +565 -0
- package/src/router/rules.ts +274 -0
- package/src/router/train.py +408 -0
- package/src/session/store.ts +211 -0
- package/src/test-utils/mock-llm.ts +39 -0
- package/src/types.ts +322 -0
- package/src/web/manager.ts +311 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verification — run local tests, lint, and typecheck after task execution.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { execSync } from 'node:child_process';
|
|
6
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import type { VerificationResult, RepoMap } from '../types.ts';
|
|
9
|
+
|
|
10
|
+
const TIMEOUT_MS = 60_000; // 1 minute per command
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Run all available verification commands and return results.
|
|
14
|
+
*/
|
|
15
|
+
export function verify(workingDir: string, repoMap?: RepoMap): VerificationResult {
|
|
16
|
+
const commands = repoMap?.commands || detectCommands(workingDir);
|
|
17
|
+
const result: VerificationResult = { passed: true };
|
|
18
|
+
|
|
19
|
+
// Test
|
|
20
|
+
if (commands.test) {
|
|
21
|
+
const testResult = runCommand(commands.test, workingDir);
|
|
22
|
+
result.testOutput = testResult.output;
|
|
23
|
+
if (!testResult.ok) result.passed = false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Typecheck
|
|
27
|
+
if (commands.typecheck) {
|
|
28
|
+
const typecheckResult = runCommand(commands.typecheck, workingDir);
|
|
29
|
+
result.typecheckOutput = typecheckResult.output;
|
|
30
|
+
if (!typecheckResult.ok) result.passed = false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Lint
|
|
34
|
+
if (commands.lint) {
|
|
35
|
+
const lintResult = runCommand(commands.lint, workingDir);
|
|
36
|
+
result.lintOutput = lintResult.output;
|
|
37
|
+
if (!lintResult.ok) result.passed = false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Auto-detect build/test/lint commands from project files. Exported so the
|
|
45
|
+
* agent loop's per-edit auto-verify can reuse the same detection instead of
|
|
46
|
+
* hardcoding `tsc --noEmit` for every project.
|
|
47
|
+
*/
|
|
48
|
+
export function detectCommands(workingDir: string): RepoMap['commands'] {
|
|
49
|
+
// fs and path imported at top level
|
|
50
|
+
const commands: RepoMap['commands'] = {};
|
|
51
|
+
|
|
52
|
+
// Node.js / package.json
|
|
53
|
+
const pkgPath = join(workingDir, 'package.json');
|
|
54
|
+
if (existsSync(pkgPath)) {
|
|
55
|
+
try {
|
|
56
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
57
|
+
const scripts = pkg.scripts || {};
|
|
58
|
+
if (scripts.test) commands.test = 'npm test';
|
|
59
|
+
if (scripts.lint) commands.lint = 'npm run lint';
|
|
60
|
+
if (scripts.typecheck) commands.typecheck = 'npm run typecheck';
|
|
61
|
+
else if (pkg.devDependencies?.typescript || pkg.dependencies?.typescript) {
|
|
62
|
+
commands.typecheck = 'npx tsc --noEmit';
|
|
63
|
+
}
|
|
64
|
+
if (scripts.build) commands.build = 'npm run build';
|
|
65
|
+
} catch { /* ignore */ }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Python
|
|
69
|
+
if (existsSync(join(workingDir, 'pyproject.toml')) || existsSync(join(workingDir, 'setup.py'))) {
|
|
70
|
+
if (!commands.test) commands.test = 'pytest';
|
|
71
|
+
if (!commands.lint) commands.lint = 'ruff check .';
|
|
72
|
+
if (!commands.typecheck) commands.typecheck = 'mypy .';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Rust
|
|
76
|
+
if (existsSync(join(workingDir, 'Cargo.toml'))) {
|
|
77
|
+
commands.test = 'cargo test';
|
|
78
|
+
commands.build = 'cargo build';
|
|
79
|
+
commands.lint = 'cargo clippy';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Go
|
|
83
|
+
if (existsSync(join(workingDir, 'go.mod'))) {
|
|
84
|
+
commands.test = 'go test ./...';
|
|
85
|
+
commands.build = 'go build ./...';
|
|
86
|
+
commands.lint = 'golangci-lint run';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return commands;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// Helpers
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
function runCommand(cmd: string, cwd: string): { ok: boolean; output: string } {
|
|
97
|
+
try {
|
|
98
|
+
const output = execSync(cmd, {
|
|
99
|
+
cwd,
|
|
100
|
+
encoding: 'utf-8',
|
|
101
|
+
timeout: TIMEOUT_MS,
|
|
102
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
103
|
+
});
|
|
104
|
+
return { ok: true, output: output.slice(-2000) }; // Keep last 2K chars
|
|
105
|
+
} catch (error: any) {
|
|
106
|
+
const stdout = error.stdout?.toString() || '';
|
|
107
|
+
const stderr = error.stderr?.toString() || '';
|
|
108
|
+
const combined = `${stdout}\n${stderr}`.trim().slice(-2000);
|
|
109
|
+
return { ok: false, output: combined || error.message };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Client Manager — connects to MCP servers and exposes their tools.
|
|
3
|
+
*
|
|
4
|
+
* Supports:
|
|
5
|
+
* - stdio: local process (command + args)
|
|
6
|
+
* - http/sse: remote server (URL + optional headers)
|
|
7
|
+
*
|
|
8
|
+
* Each server's tools are namespaced as "servername__toolname" to avoid
|
|
9
|
+
* collisions across servers.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
13
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
14
|
+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
15
|
+
import type {
|
|
16
|
+
McpScopedConfig, McpServerState, McpToolInfo, McpConnectionStatus,
|
|
17
|
+
} from './types.ts';
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// MCP Client Manager
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
export class McpClientManager {
|
|
24
|
+
private servers: Map<string, McpServerState> = new Map();
|
|
25
|
+
private clients: Map<string, { client: Client; cleanup: () => Promise<void> }> = new Map();
|
|
26
|
+
|
|
27
|
+
/** Connect to all configured servers */
|
|
28
|
+
async connectAll(configs: Map<string, McpScopedConfig>): Promise<void> {
|
|
29
|
+
const promises = [...configs.entries()].map(([name, config]) =>
|
|
30
|
+
this.connect(name, config)
|
|
31
|
+
);
|
|
32
|
+
await Promise.allSettled(promises);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Connect to a single server */
|
|
36
|
+
async connect(name: string, config: McpScopedConfig): Promise<McpServerState> {
|
|
37
|
+
// Clean up existing connection if any
|
|
38
|
+
await this.disconnect(name);
|
|
39
|
+
|
|
40
|
+
const state: McpServerState = {
|
|
41
|
+
name,
|
|
42
|
+
status: 'pending',
|
|
43
|
+
config,
|
|
44
|
+
tools: [],
|
|
45
|
+
};
|
|
46
|
+
this.servers.set(name, state);
|
|
47
|
+
|
|
48
|
+
const configType = 'type' in config ? config.type : 'stdio';
|
|
49
|
+
process.stderr.write(`[mcp] Connecting to ${name} (${configType || 'stdio'})...\n`);
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const { client, cleanup } = await this.createClient(name, config);
|
|
53
|
+
this.clients.set(name, { client, cleanup });
|
|
54
|
+
|
|
55
|
+
// Discover tools
|
|
56
|
+
const toolsResult = await client.listTools();
|
|
57
|
+
const tools: McpToolInfo[] = (toolsResult.tools || []).map(tool => ({
|
|
58
|
+
qualifiedName: `${name}__${tool.name}`,
|
|
59
|
+
originalName: tool.name,
|
|
60
|
+
serverName: name,
|
|
61
|
+
description: tool.description || '',
|
|
62
|
+
parameters: (tool.inputSchema as Record<string, unknown>) || { type: 'object', properties: {} },
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
state.status = 'connected';
|
|
66
|
+
state.tools = tools;
|
|
67
|
+
|
|
68
|
+
process.stderr.write(`[mcp] ${name}: connected (${tools.length} tools)\n`);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
state.status = 'failed';
|
|
71
|
+
state.error = (error as Error).message;
|
|
72
|
+
process.stderr.write(`[mcp] ${name}: failed — ${state.error}\n`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return state;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Disconnect a server */
|
|
79
|
+
async disconnect(name: string): Promise<void> {
|
|
80
|
+
const entry = this.clients.get(name);
|
|
81
|
+
if (entry) {
|
|
82
|
+
try {
|
|
83
|
+
await entry.cleanup();
|
|
84
|
+
} catch { /* ignore cleanup errors */ }
|
|
85
|
+
this.clients.delete(name);
|
|
86
|
+
}
|
|
87
|
+
this.servers.delete(name);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Disconnect all servers */
|
|
91
|
+
async disconnectAll(): Promise<void> {
|
|
92
|
+
const names = [...this.servers.keys()];
|
|
93
|
+
await Promise.allSettled(names.map(n => this.disconnect(n)));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// -------------------------------------------------------------------------
|
|
97
|
+
// Tool access
|
|
98
|
+
// -------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
/** Get all tools from all connected servers */
|
|
101
|
+
getAllTools(): McpToolInfo[] {
|
|
102
|
+
const tools: McpToolInfo[] = [];
|
|
103
|
+
for (const state of this.servers.values()) {
|
|
104
|
+
if (state.status === 'connected') {
|
|
105
|
+
tools.push(...state.tools);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return tools;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Call a tool on an MCP server */
|
|
112
|
+
async callTool(
|
|
113
|
+
qualifiedName: string,
|
|
114
|
+
args: Record<string, unknown>,
|
|
115
|
+
): Promise<{ content: string; isError?: boolean }> {
|
|
116
|
+
// Parse server__tool
|
|
117
|
+
const sep = qualifiedName.indexOf('__');
|
|
118
|
+
if (sep < 0) {
|
|
119
|
+
return { content: `Invalid MCP tool name: ${qualifiedName}`, isError: true };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const serverName = qualifiedName.slice(0, sep);
|
|
123
|
+
const toolName = qualifiedName.slice(sep + 2);
|
|
124
|
+
|
|
125
|
+
const entry = this.clients.get(serverName);
|
|
126
|
+
if (!entry) {
|
|
127
|
+
return { content: `MCP server not connected: ${serverName}`, isError: true };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const result = await entry.client.callTool({ name: toolName, arguments: args });
|
|
132
|
+
|
|
133
|
+
// Extract text content from MCP response
|
|
134
|
+
const textParts = (result.content as any[])
|
|
135
|
+
?.filter((c: any) => c.type === 'text')
|
|
136
|
+
.map((c: any) => c.text) || [];
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
content: textParts.join('\n') || '(no output)',
|
|
140
|
+
isError: result.isError === true,
|
|
141
|
+
};
|
|
142
|
+
} catch (error) {
|
|
143
|
+
return { content: `MCP tool error: ${(error as Error).message}`, isError: true };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// -------------------------------------------------------------------------
|
|
148
|
+
// Server state
|
|
149
|
+
// -------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
getServers(): McpServerState[] {
|
|
152
|
+
return [...this.servers.values()];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
getServer(name: string): McpServerState | undefined {
|
|
156
|
+
return this.servers.get(name);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Format for display */
|
|
160
|
+
format(): string {
|
|
161
|
+
const servers = this.getServers();
|
|
162
|
+
if (servers.length === 0) return 'No MCP servers configured. See /mcp add.';
|
|
163
|
+
|
|
164
|
+
const lines: string[] = ['MCP Servers:'];
|
|
165
|
+
for (const s of servers) {
|
|
166
|
+
const icon = s.status === 'connected' ? 'OK' : s.status === 'failed' ? 'FAIL' : '...';
|
|
167
|
+
const scope = s.config.scope;
|
|
168
|
+
const type = s.config.type || 'stdio';
|
|
169
|
+
lines.push(` [${icon.padEnd(4)}] ${s.name} (${type}, ${scope})`);
|
|
170
|
+
if (s.status === 'connected') {
|
|
171
|
+
lines.push(` ${s.tools.length} tools: ${s.tools.map(t => t.originalName).join(', ')}`);
|
|
172
|
+
}
|
|
173
|
+
if (s.error) {
|
|
174
|
+
lines.push(` Error: ${s.error}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return lines.join('\n');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// -------------------------------------------------------------------------
|
|
181
|
+
// Transport creation
|
|
182
|
+
// -------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
private async createClient(
|
|
185
|
+
name: string,
|
|
186
|
+
config: McpScopedConfig,
|
|
187
|
+
): Promise<{ client: Client; cleanup: () => Promise<void> }> {
|
|
188
|
+
const client = new Client({ name: `kondi-chat-${name}`, version: '0.1.0' });
|
|
189
|
+
const type = 'type' in config ? config.type || 'stdio' : 'stdio';
|
|
190
|
+
|
|
191
|
+
if (type === 'stdio') {
|
|
192
|
+
const stdioConfig = config as { command: string; args?: string[]; env?: Record<string, string>; scope: string };
|
|
193
|
+
const transport = new StdioClientTransport({
|
|
194
|
+
command: stdioConfig.command,
|
|
195
|
+
args: stdioConfig.args || [],
|
|
196
|
+
env: { ...process.env, ...(stdioConfig.env || {}) } as Record<string, string>,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
await client.connect(transport);
|
|
200
|
+
return {
|
|
201
|
+
client,
|
|
202
|
+
cleanup: async () => {
|
|
203
|
+
await client.close();
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (type === 'http' || type === 'sse') {
|
|
209
|
+
const httpConfig = config as { url: string; headers?: Record<string, string>; scope: string };
|
|
210
|
+
const transport = new StreamableHTTPClientTransport(
|
|
211
|
+
new URL(httpConfig.url),
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
await client.connect(transport);
|
|
215
|
+
return {
|
|
216
|
+
client,
|
|
217
|
+
cleanup: async () => {
|
|
218
|
+
await client.close();
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
throw new Error(`Unsupported MCP transport: ${type}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Config — loads server configuration from multiple scopes.
|
|
3
|
+
*
|
|
4
|
+
* Config files (checked in order, merged):
|
|
5
|
+
* 1. Project: .kondi-chat/mcp.json (project-specific)
|
|
6
|
+
* 2. User: ~/.kondi-chat/mcp.json (user-wide)
|
|
7
|
+
*
|
|
8
|
+
* Format (same as Claude Code):
|
|
9
|
+
* {
|
|
10
|
+
* "mcpServers": {
|
|
11
|
+
* "filesystem": {
|
|
12
|
+
* "command": "npx",
|
|
13
|
+
* "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path"]
|
|
14
|
+
* },
|
|
15
|
+
* "remote-api": {
|
|
16
|
+
* "type": "http",
|
|
17
|
+
* "url": "https://api.example.com/mcp"
|
|
18
|
+
* }
|
|
19
|
+
* }
|
|
20
|
+
* }
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
24
|
+
import { join } from 'node:path';
|
|
25
|
+
import { homedir } from 'node:os';
|
|
26
|
+
import type { McpConfigFile, McpServerConfig, McpScopedConfig, McpScope } from './types.ts';
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Config loader
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
export function loadMcpConfig(projectDir: string): Map<string, McpScopedConfig> {
|
|
33
|
+
const servers = new Map<string, McpScopedConfig>();
|
|
34
|
+
|
|
35
|
+
// Load user-level config first (lower priority)
|
|
36
|
+
const userDir = join(homedir(), '.kondi-chat');
|
|
37
|
+
const userConfig = loadConfigFile(join(userDir, 'mcp.json'), 'user');
|
|
38
|
+
for (const [name, config] of userConfig) {
|
|
39
|
+
servers.set(name, config);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Load project-level config (higher priority, overrides user)
|
|
43
|
+
const projectConfig = loadConfigFile(join(projectDir, '.kondi-chat', 'mcp.json'), 'project');
|
|
44
|
+
for (const [name, config] of projectConfig) {
|
|
45
|
+
servers.set(name, config);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return servers;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function loadConfigFile(path: string, scope: McpScope): Map<string, McpScopedConfig> {
|
|
52
|
+
const servers = new Map<string, McpScopedConfig>();
|
|
53
|
+
if (!existsSync(path)) return servers;
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const raw = readFileSync(path, 'utf-8');
|
|
57
|
+
const config: McpConfigFile = JSON.parse(raw);
|
|
58
|
+
|
|
59
|
+
if (config.mcpServers) {
|
|
60
|
+
for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
|
|
61
|
+
servers.set(name, { ...serverConfig, scope });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} catch (error) {
|
|
65
|
+
process.stderr.write(`[mcp] Failed to load ${path}: ${(error as Error).message}\n`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return servers;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Config writer
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
export function saveMcpServer(
|
|
76
|
+
projectDir: string,
|
|
77
|
+
name: string,
|
|
78
|
+
config: McpServerConfig,
|
|
79
|
+
scope: McpScope = 'project',
|
|
80
|
+
): void {
|
|
81
|
+
const dir = scope === 'user'
|
|
82
|
+
? join(homedir(), '.kondi-chat')
|
|
83
|
+
: join(projectDir, '.kondi-chat');
|
|
84
|
+
const path = join(dir, 'mcp.json');
|
|
85
|
+
|
|
86
|
+
mkdirSync(dir, { recursive: true });
|
|
87
|
+
|
|
88
|
+
let existing: McpConfigFile = { mcpServers: {} };
|
|
89
|
+
if (existsSync(path)) {
|
|
90
|
+
try {
|
|
91
|
+
existing = JSON.parse(readFileSync(path, 'utf-8'));
|
|
92
|
+
} catch { /* start fresh */ }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
existing.mcpServers[name] = config;
|
|
96
|
+
writeFileSync(path, JSON.stringify(existing, null, 2));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function removeMcpServer(
|
|
100
|
+
projectDir: string,
|
|
101
|
+
name: string,
|
|
102
|
+
scope: McpScope = 'project',
|
|
103
|
+
): boolean {
|
|
104
|
+
const dir = scope === 'user'
|
|
105
|
+
? join(homedir(), '.kondi-chat')
|
|
106
|
+
: join(projectDir, '.kondi-chat');
|
|
107
|
+
const path = join(dir, 'mcp.json');
|
|
108
|
+
|
|
109
|
+
if (!existsSync(path)) return false;
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const existing: McpConfigFile = JSON.parse(readFileSync(path, 'utf-8'));
|
|
113
|
+
if (!existing.mcpServers[name]) return false;
|
|
114
|
+
delete existing.mcpServers[name];
|
|
115
|
+
writeFileSync(path, JSON.stringify(existing, null, 2));
|
|
116
|
+
return true;
|
|
117
|
+
} catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Manager — merges built-in tools and MCP tools into a unified set.
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* - Combining built-in tools (read_file, etc.) with MCP tools
|
|
6
|
+
* - Dispatching tool calls to the right handler (built-in or MCP)
|
|
7
|
+
* - Filtering tools per request to save tokens
|
|
8
|
+
* - Converting MCP tools to our ToolDefinition format
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ToolDefinition } from '../types.ts';
|
|
12
|
+
import { AGENT_TOOLS, executeTool, type ToolContext, type ToolExecutionResult } from '../engine/tools.ts';
|
|
13
|
+
import { McpClientManager } from './client.ts';
|
|
14
|
+
import type { McpToolInfo } from './types.ts';
|
|
15
|
+
import type { HookRunner } from '../engine/hooks.ts';
|
|
16
|
+
|
|
17
|
+
/** Extra tools registered at runtime (e.g., council) */
|
|
18
|
+
let extraTools: ToolDefinition[] = [];
|
|
19
|
+
let extraExecutors: Map<string, (args: Record<string, unknown>, ctx: ToolContext) => Promise<ToolExecutionResult>> = new Map();
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Tool categories for filtering
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
/** Built-in tool categories */
|
|
26
|
+
const BUILTIN_CATEGORIES: Record<string, string[]> = {
|
|
27
|
+
read_file: ['filesystem', 'coding'],
|
|
28
|
+
write_file: ['filesystem', 'coding'],
|
|
29
|
+
edit_file: ['filesystem', 'coding'],
|
|
30
|
+
list_files: ['filesystem', 'coding'],
|
|
31
|
+
search_code: ['coding', 'analysis'],
|
|
32
|
+
run_command: ['system', 'coding'],
|
|
33
|
+
create_task: ['coding', 'execution'],
|
|
34
|
+
update_plan: ['planning'],
|
|
35
|
+
update_memory: ['planning'],
|
|
36
|
+
spawn_agent: ['planning', 'coding'],
|
|
37
|
+
git_status: ['git', 'analysis'],
|
|
38
|
+
git_diff: ['git', 'analysis'],
|
|
39
|
+
git_log: ['git', 'analysis'],
|
|
40
|
+
git_branch: ['git', 'coding'],
|
|
41
|
+
git_commit: ['git', 'coding'],
|
|
42
|
+
git_create_pr: ['git', 'coding'],
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** Phase → which tool categories are relevant */
|
|
46
|
+
const PHASE_TOOLS: Record<string, string[]> = {
|
|
47
|
+
discuss: ['filesystem', 'coding', 'analysis', 'planning', 'system', 'git'],
|
|
48
|
+
dispatch: ['planning'],
|
|
49
|
+
execute: ['filesystem', 'coding', 'system', 'git'],
|
|
50
|
+
reflect: [],
|
|
51
|
+
compress: [],
|
|
52
|
+
state_update: ['planning'],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Tool Manager
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
export class ToolManager {
|
|
60
|
+
private mcpClient: McpClientManager;
|
|
61
|
+
private hookRunner?: HookRunner;
|
|
62
|
+
|
|
63
|
+
constructor(mcpClient: McpClientManager) {
|
|
64
|
+
this.mcpClient = mcpClient;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
setHookRunner(runner: HookRunner): void {
|
|
68
|
+
this.hookRunner = runner;
|
|
69
|
+
runner.setToolExecutor((name, args, ctx) => this.executeWithoutHooks(name, args, ctx));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Register an extra tool (e.g., council) */
|
|
73
|
+
registerTool(
|
|
74
|
+
tool: ToolDefinition,
|
|
75
|
+
executor: (args: Record<string, unknown>, ctx: ToolContext) => Promise<ToolExecutionResult>,
|
|
76
|
+
): void {
|
|
77
|
+
extraTools.push(tool);
|
|
78
|
+
extraExecutors.set(tool.name, executor);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get all available tool definitions (built-in + MCP).
|
|
83
|
+
* Optionally filter by phase for token efficiency.
|
|
84
|
+
*/
|
|
85
|
+
getTools(phase?: string): ToolDefinition[] {
|
|
86
|
+
const builtIn = AGENT_TOOLS;
|
|
87
|
+
const mcpTools = this.mcpClient.getAllTools().map(mcpToToolDef);
|
|
88
|
+
const all = [...builtIn, ...extraTools, ...mcpTools];
|
|
89
|
+
|
|
90
|
+
if (!phase) return all;
|
|
91
|
+
|
|
92
|
+
// Filter by phase relevance
|
|
93
|
+
const relevantCategories = PHASE_TOOLS[phase];
|
|
94
|
+
if (!relevantCategories) return all;
|
|
95
|
+
|
|
96
|
+
return all.filter(tool => {
|
|
97
|
+
// Built-in tools: check category
|
|
98
|
+
const categories = BUILTIN_CATEGORIES[tool.name];
|
|
99
|
+
if (categories) {
|
|
100
|
+
return categories.some(c => relevantCategories.includes(c));
|
|
101
|
+
}
|
|
102
|
+
// MCP tools: always include for now (can't categorize without metadata)
|
|
103
|
+
// TODO: use tool description to auto-categorize
|
|
104
|
+
return true;
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Execute a tool call — routes to built-in handler or MCP server.
|
|
110
|
+
* Spec 01: every path goes through the permission wedge first.
|
|
111
|
+
*/
|
|
112
|
+
async execute(
|
|
113
|
+
name: string,
|
|
114
|
+
args: Record<string, unknown>,
|
|
115
|
+
toolCtx: ToolContext,
|
|
116
|
+
): Promise<ToolExecutionResult> {
|
|
117
|
+
// Spec 12 — before hooks run first (can block before the permission dialog).
|
|
118
|
+
if (this.hookRunner) {
|
|
119
|
+
const before = await this.hookRunner.runBefore(name, args, toolCtx, toolCtx.emit);
|
|
120
|
+
if (before.blocked) {
|
|
121
|
+
return { content: `Tool blocked by hook: ${before.messages.join('; ')}`, isError: true };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const result = await this.executeWithoutHooks(name, args, toolCtx);
|
|
126
|
+
|
|
127
|
+
if (this.hookRunner) {
|
|
128
|
+
return this.hookRunner.runAfter(name, args, result, toolCtx, toolCtx.emit);
|
|
129
|
+
}
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Run a tool without the before/after hook pass — used by `tool:` hooks to
|
|
135
|
+
* avoid recursion. Permissions still apply (Spec 01 hasn't moved).
|
|
136
|
+
*/
|
|
137
|
+
async executeWithoutHooks(
|
|
138
|
+
name: string,
|
|
139
|
+
args: Record<string, unknown>,
|
|
140
|
+
toolCtx: ToolContext,
|
|
141
|
+
): Promise<ToolExecutionResult> {
|
|
142
|
+
// Spec 01 — permission gate (applies to built-in, extra, and MCP tools)
|
|
143
|
+
const pm = toolCtx.permissionManager;
|
|
144
|
+
if (pm) {
|
|
145
|
+
const tier = pm.check(name, args);
|
|
146
|
+
if (tier !== 'auto-approve') {
|
|
147
|
+
if (!toolCtx.emit) {
|
|
148
|
+
return { content: `Permission check for ${name} requires an emit channel`, isError: true };
|
|
149
|
+
}
|
|
150
|
+
const decision = await pm.requestPermission(name, args, toolCtx.emit);
|
|
151
|
+
if (decision === 'denied') {
|
|
152
|
+
return { content: `Permission denied for ${name}.`, isError: true };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Check extra tools first (e.g., council)
|
|
158
|
+
const extraExec = extraExecutors.get(name);
|
|
159
|
+
if (extraExec) {
|
|
160
|
+
return extraExec(args, toolCtx);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Check if it's an MCP tool (contains __ separator)
|
|
164
|
+
if (name.includes('__')) {
|
|
165
|
+
return this.mcpClient.callTool(name, args);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Built-in tool
|
|
169
|
+
return executeTool(name, args, toolCtx);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Get summary for display */
|
|
173
|
+
getSummary(): { builtIn: number; mcp: number; servers: number } {
|
|
174
|
+
return {
|
|
175
|
+
builtIn: AGENT_TOOLS.length,
|
|
176
|
+
mcp: this.mcpClient.getAllTools().length,
|
|
177
|
+
servers: this.mcpClient.getServers().filter(s => s.status === 'connected').length,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
// Helpers
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
function mcpToToolDef(tool: McpToolInfo): ToolDefinition {
|
|
187
|
+
return {
|
|
188
|
+
name: tool.qualifiedName,
|
|
189
|
+
description: `[${tool.serverName}] ${tool.description}`,
|
|
190
|
+
parameters: tool.parameters,
|
|
191
|
+
};
|
|
192
|
+
}
|