acmecode 1.0.0
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/.acmecode/config.json +6 -0
- package/README.md +124 -0
- package/dist/agent/index.js +161 -0
- package/dist/cli/bin/acmecode.js +3 -0
- package/dist/cli/package.json +25 -0
- package/dist/cli/src/index.d.ts +1 -0
- package/dist/cli/src/index.js +53 -0
- package/dist/config/index.js +92 -0
- package/dist/context/index.js +30 -0
- package/dist/core/src/agent/index.d.ts +52 -0
- package/dist/core/src/agent/index.js +476 -0
- package/dist/core/src/config/index.d.ts +83 -0
- package/dist/core/src/config/index.js +318 -0
- package/dist/core/src/context/index.d.ts +1 -0
- package/dist/core/src/context/index.js +30 -0
- package/dist/core/src/llm/provider.d.ts +27 -0
- package/dist/core/src/llm/provider.js +202 -0
- package/dist/core/src/llm/vision.d.ts +7 -0
- package/dist/core/src/llm/vision.js +37 -0
- package/dist/core/src/mcp/index.d.ts +10 -0
- package/dist/core/src/mcp/index.js +84 -0
- package/dist/core/src/prompt/anthropic.d.ts +1 -0
- package/dist/core/src/prompt/anthropic.js +32 -0
- package/dist/core/src/prompt/architect.d.ts +1 -0
- package/dist/core/src/prompt/architect.js +17 -0
- package/dist/core/src/prompt/autopilot.d.ts +1 -0
- package/dist/core/src/prompt/autopilot.js +18 -0
- package/dist/core/src/prompt/beast.d.ts +1 -0
- package/dist/core/src/prompt/beast.js +83 -0
- package/dist/core/src/prompt/gemini.d.ts +1 -0
- package/dist/core/src/prompt/gemini.js +45 -0
- package/dist/core/src/prompt/index.d.ts +18 -0
- package/dist/core/src/prompt/index.js +239 -0
- package/dist/core/src/prompt/zen.d.ts +1 -0
- package/dist/core/src/prompt/zen.js +13 -0
- package/dist/core/src/session/index.d.ts +18 -0
- package/dist/core/src/session/index.js +97 -0
- package/dist/core/src/skills/index.d.ts +6 -0
- package/dist/core/src/skills/index.js +72 -0
- package/dist/core/src/tools/batch.d.ts +2 -0
- package/dist/core/src/tools/batch.js +65 -0
- package/dist/core/src/tools/browser.d.ts +7 -0
- package/dist/core/src/tools/browser.js +86 -0
- package/dist/core/src/tools/edit.d.ts +11 -0
- package/dist/core/src/tools/edit.js +312 -0
- package/dist/core/src/tools/index.d.ts +13 -0
- package/dist/core/src/tools/index.js +980 -0
- package/dist/core/src/tools/lsp-client.d.ts +11 -0
- package/dist/core/src/tools/lsp-client.js +224 -0
- package/dist/index.js +41 -0
- package/dist/llm/provider.js +34 -0
- package/dist/mcp/index.js +84 -0
- package/dist/session/index.js +74 -0
- package/dist/skills/index.js +32 -0
- package/dist/tools/index.js +96 -0
- package/dist/tui/App.js +297 -0
- package/dist/tui/Spinner.js +16 -0
- package/dist/tui/TextInput.js +98 -0
- package/dist/tui/src/App.d.ts +11 -0
- package/dist/tui/src/App.js +1211 -0
- package/dist/tui/src/CatLogo.d.ts +10 -0
- package/dist/tui/src/CatLogo.js +99 -0
- package/dist/tui/src/OptionList.d.ts +15 -0
- package/dist/tui/src/OptionList.js +60 -0
- package/dist/tui/src/Spinner.d.ts +7 -0
- package/dist/tui/src/Spinner.js +18 -0
- package/dist/tui/src/TextInput.d.ts +28 -0
- package/dist/tui/src/TextInput.js +139 -0
- package/dist/tui/src/Tips.d.ts +2 -0
- package/dist/tui/src/Tips.js +62 -0
- package/dist/tui/src/Toast.d.ts +19 -0
- package/dist/tui/src/Toast.js +39 -0
- package/dist/tui/src/TodoItem.d.ts +7 -0
- package/dist/tui/src/TodoItem.js +21 -0
- package/dist/tui/src/i18n.d.ts +172 -0
- package/dist/tui/src/i18n.js +189 -0
- package/dist/tui/src/markdown.d.ts +6 -0
- package/dist/tui/src/markdown.js +356 -0
- package/dist/tui/src/theme.d.ts +31 -0
- package/dist/tui/src/theme.js +239 -0
- package/output.txt +0 -0
- package/package.json +44 -0
- package/packages/cli/package.json +25 -0
- package/packages/cli/src/index.ts +59 -0
- package/packages/cli/tsconfig.json +26 -0
- package/packages/core/package.json +39 -0
- package/packages/core/src/agent/index.ts +588 -0
- package/packages/core/src/config/index.ts +383 -0
- package/packages/core/src/context/index.ts +34 -0
- package/packages/core/src/llm/provider.ts +237 -0
- package/packages/core/src/llm/vision.ts +43 -0
- package/packages/core/src/mcp/index.ts +110 -0
- package/packages/core/src/prompt/anthropic.ts +32 -0
- package/packages/core/src/prompt/architect.ts +17 -0
- package/packages/core/src/prompt/autopilot.ts +18 -0
- package/packages/core/src/prompt/beast.ts +83 -0
- package/packages/core/src/prompt/gemini.ts +45 -0
- package/packages/core/src/prompt/index.ts +267 -0
- package/packages/core/src/prompt/zen.ts +13 -0
- package/packages/core/src/session/index.ts +129 -0
- package/packages/core/src/skills/index.ts +86 -0
- package/packages/core/src/tools/batch.ts +73 -0
- package/packages/core/src/tools/browser.ts +95 -0
- package/packages/core/src/tools/edit.ts +317 -0
- package/packages/core/src/tools/index.ts +1112 -0
- package/packages/core/src/tools/lsp-client.ts +303 -0
- package/packages/core/tsconfig.json +19 -0
- package/packages/tui/package.json +24 -0
- package/packages/tui/src/App.tsx +1702 -0
- package/packages/tui/src/CatLogo.tsx +134 -0
- package/packages/tui/src/OptionList.tsx +95 -0
- package/packages/tui/src/Spinner.tsx +28 -0
- package/packages/tui/src/TextInput.tsx +202 -0
- package/packages/tui/src/Tips.tsx +64 -0
- package/packages/tui/src/Toast.tsx +60 -0
- package/packages/tui/src/TodoItem.tsx +29 -0
- package/packages/tui/src/i18n.ts +203 -0
- package/packages/tui/src/markdown.ts +403 -0
- package/packages/tui/src/theme.ts +287 -0
- package/packages/tui/tsconfig.json +24 -0
- package/tsconfig.json +18 -0
- package/vscode-acmecode/.vscodeignore +11 -0
- package/vscode-acmecode/README.md +57 -0
- package/vscode-acmecode/esbuild.js +46 -0
- package/vscode-acmecode/images/button-dark.svg +5 -0
- package/vscode-acmecode/images/button-light.svg +5 -0
- package/vscode-acmecode/images/icon.png +1 -0
- package/vscode-acmecode/package-lock.json +490 -0
- package/vscode-acmecode/package.json +87 -0
- package/vscode-acmecode/src/extension.ts +128 -0
- package/vscode-acmecode/tsconfig.json +16 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ChildProcessWithoutNullStreams } from "child_process";
|
|
2
|
+
import { MessageConnection } from "vscode-jsonrpc/lib/node/main.js";
|
|
3
|
+
import type { Diagnostic } from "vscode-languageserver-types";
|
|
4
|
+
export interface LspClient {
|
|
5
|
+
connection: MessageConnection;
|
|
6
|
+
process: ChildProcessWithoutNullStreams;
|
|
7
|
+
diagnostics: Map<string, Diagnostic[]>;
|
|
8
|
+
openFile: (filePath: string, timeoutMs?: number) => Promise<void>;
|
|
9
|
+
shutdown: () => Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
export declare function getLspClientForFile(workspaceRoot: string, filePath: string): Promise<LspClient>;
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { pathToFileURL } from "url";
|
|
4
|
+
import { createMessageConnection, StreamMessageReader, StreamMessageWriter, } from "vscode-jsonrpc/lib/node/main.js";
|
|
5
|
+
import * as fs from "fs/promises";
|
|
6
|
+
const SERVER_CONFIGS = {
|
|
7
|
+
typescript: {
|
|
8
|
+
command: process.platform === "win32" ? "npx.cmd" : "npx",
|
|
9
|
+
args: ["--no-install", "typescript-language-server", "--stdio"],
|
|
10
|
+
languageIdResolver: (filePath) => filePath.endsWith(".tsx")
|
|
11
|
+
? "typescriptreact"
|
|
12
|
+
: filePath.endsWith(".jsx")
|
|
13
|
+
? "javascriptreact"
|
|
14
|
+
: filePath.endsWith(".js")
|
|
15
|
+
? "javascript"
|
|
16
|
+
: "typescript",
|
|
17
|
+
},
|
|
18
|
+
python: {
|
|
19
|
+
command: process.platform === "win32" ? "npx.cmd" : "npx",
|
|
20
|
+
// npx pyright-langserver will use standard pyright if available
|
|
21
|
+
args: ["--no-install", "pyright-langserver", "--stdio"],
|
|
22
|
+
languageIdResolver: () => "python",
|
|
23
|
+
},
|
|
24
|
+
go: {
|
|
25
|
+
command: "gopls",
|
|
26
|
+
args: [], // gopls runs in stdio mode by default
|
|
27
|
+
languageIdResolver: () => "go",
|
|
28
|
+
},
|
|
29
|
+
rust: {
|
|
30
|
+
command: "rust-analyzer",
|
|
31
|
+
args: [],
|
|
32
|
+
languageIdResolver: () => "rust",
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
const clients = new Map();
|
|
36
|
+
const clientInitializationPromises = new Map();
|
|
37
|
+
// Track file versions globally across all servers
|
|
38
|
+
const fileVersions = new Map();
|
|
39
|
+
function getLanguageIdForExtension(filePath) {
|
|
40
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
41
|
+
if ([".ts", ".tsx", ".js", ".jsx", ".cjs", ".mjs"].includes(ext))
|
|
42
|
+
return "typescript";
|
|
43
|
+
if ([".py", ".pyi"].includes(ext))
|
|
44
|
+
return "python";
|
|
45
|
+
if ([".go"].includes(ext))
|
|
46
|
+
return "go";
|
|
47
|
+
if ([".rs"].includes(ext))
|
|
48
|
+
return "rust";
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
async function getTsServerPath(workspaceRoot) {
|
|
52
|
+
const localTsServer = path.join(workspaceRoot, "node_modules", "typescript", "lib", "tsserver.js");
|
|
53
|
+
try {
|
|
54
|
+
await fs.stat(localTsServer);
|
|
55
|
+
return localTsServer;
|
|
56
|
+
}
|
|
57
|
+
catch { }
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
async function resolveTsLsCommand(workspaceRoot) {
|
|
61
|
+
// Walk up from workspaceRoot looking for node_modules/.bin/typescript-language-server
|
|
62
|
+
const bin = process.platform === "win32"
|
|
63
|
+
? "typescript-language-server.cmd"
|
|
64
|
+
: "typescript-language-server";
|
|
65
|
+
let dir = workspaceRoot;
|
|
66
|
+
for (let i = 0; i < 5; i++) {
|
|
67
|
+
const candidate = path.join(dir, "node_modules", ".bin", bin);
|
|
68
|
+
try {
|
|
69
|
+
await fs.access(candidate);
|
|
70
|
+
return { command: candidate, args: ["--stdio"] };
|
|
71
|
+
}
|
|
72
|
+
catch { }
|
|
73
|
+
const parent = path.dirname(dir);
|
|
74
|
+
if (parent === dir)
|
|
75
|
+
break;
|
|
76
|
+
dir = parent;
|
|
77
|
+
}
|
|
78
|
+
// Fallback: try global install via npx (with --no-install to avoid slow download)
|
|
79
|
+
return {
|
|
80
|
+
command: process.platform === "win32" ? "npx.cmd" : "npx",
|
|
81
|
+
args: ["--no-install", "typescript-language-server", "--stdio"],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
export async function getLspClientForFile(workspaceRoot, filePath) {
|
|
85
|
+
const langId = getLanguageIdForExtension(filePath);
|
|
86
|
+
if (!langId) {
|
|
87
|
+
throw new Error(`Unsupported file extension for LSP: ${filePath}`);
|
|
88
|
+
}
|
|
89
|
+
if (clients.has(langId)) {
|
|
90
|
+
return clients.get(langId);
|
|
91
|
+
}
|
|
92
|
+
if (!clientInitializationPromises.has(langId)) {
|
|
93
|
+
const promise = initializeLspClient(workspaceRoot, langId).catch((err) => {
|
|
94
|
+
clientInitializationPromises.delete(langId);
|
|
95
|
+
throw err;
|
|
96
|
+
});
|
|
97
|
+
clientInitializationPromises.set(langId, promise);
|
|
98
|
+
}
|
|
99
|
+
return clientInitializationPromises.get(langId);
|
|
100
|
+
}
|
|
101
|
+
async function initializeLspClient(workspaceRoot, langId) {
|
|
102
|
+
let config = SERVER_CONFIGS[langId];
|
|
103
|
+
// For TypeScript, prefer the local node_modules binary over npx
|
|
104
|
+
if (langId === "typescript") {
|
|
105
|
+
const resolved = await resolveTsLsCommand(workspaceRoot);
|
|
106
|
+
config = { ...config, ...resolved };
|
|
107
|
+
}
|
|
108
|
+
console.log(`[LSP] Starting ${langId} language server (${config.command} ${config.args.join(" ")})...`);
|
|
109
|
+
const proc = spawn(config.command, config.args, {
|
|
110
|
+
cwd: workspaceRoot,
|
|
111
|
+
env: { ...process.env },
|
|
112
|
+
shell: false, // direct binary, no shell needed
|
|
113
|
+
});
|
|
114
|
+
// Fast-fail: if the process exits immediately, it's not installed
|
|
115
|
+
await new Promise((resolve, reject) => {
|
|
116
|
+
const onExit = (code) => {
|
|
117
|
+
reject(new Error(`[LSP] ${langId} server exited immediately (code ${code}). Is it installed?`));
|
|
118
|
+
};
|
|
119
|
+
proc.once("exit", onExit);
|
|
120
|
+
// Give it 500ms to either crash or start successfully
|
|
121
|
+
setTimeout(() => {
|
|
122
|
+
proc.removeListener("exit", onExit);
|
|
123
|
+
resolve();
|
|
124
|
+
}, 500);
|
|
125
|
+
});
|
|
126
|
+
// Capture stderr for debug logging
|
|
127
|
+
proc.stderr.on("data", (data) => {
|
|
128
|
+
// console.error(`[LSP ${langId}] ${data.toString()}`);
|
|
129
|
+
});
|
|
130
|
+
const connection = createMessageConnection(new StreamMessageReader(proc.stdout), new StreamMessageWriter(proc.stdin));
|
|
131
|
+
const diagnosticsMap = new Map();
|
|
132
|
+
// Listeners waiting for diagnostics on a specific URI
|
|
133
|
+
const diagnosticsListeners = new Map();
|
|
134
|
+
connection.onNotification("textDocument/publishDiagnostics", (params) => {
|
|
135
|
+
diagnosticsMap.set(params.uri, params.diagnostics);
|
|
136
|
+
// Notify any waiters for this URI
|
|
137
|
+
const listeners = diagnosticsListeners.get(params.uri);
|
|
138
|
+
if (listeners) {
|
|
139
|
+
diagnosticsListeners.delete(params.uri);
|
|
140
|
+
for (const cb of listeners)
|
|
141
|
+
cb();
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
connection.onRequest("window/workDoneProgress/create", () => null);
|
|
145
|
+
connection.onRequest("workspace/configuration", () => [{}]);
|
|
146
|
+
connection.listen();
|
|
147
|
+
let initializationOptions = {};
|
|
148
|
+
if (langId === "typescript") {
|
|
149
|
+
const tsserverPath = await getTsServerPath(workspaceRoot);
|
|
150
|
+
if (tsserverPath) {
|
|
151
|
+
initializationOptions = { tsserver: { path: tsserverPath } };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// 1. Initialize (with timeout to avoid hanging on slow server startup)
|
|
155
|
+
const INIT_TIMEOUT_MS = 15000;
|
|
156
|
+
await Promise.race([
|
|
157
|
+
connection.sendRequest("initialize", {
|
|
158
|
+
processId: process.pid,
|
|
159
|
+
rootUri: pathToFileURL(workspaceRoot).href,
|
|
160
|
+
capabilities: {
|
|
161
|
+
textDocument: {
|
|
162
|
+
synchronization: { didOpen: true, didChange: true },
|
|
163
|
+
publishDiagnostics: { relatedInformation: true },
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
initializationOptions,
|
|
167
|
+
}),
|
|
168
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`[LSP] initialize timed out after ${INIT_TIMEOUT_MS}ms`)), INIT_TIMEOUT_MS)),
|
|
169
|
+
]);
|
|
170
|
+
// 2. Initialized
|
|
171
|
+
await connection.sendNotification("initialized", {});
|
|
172
|
+
const client = {
|
|
173
|
+
connection,
|
|
174
|
+
process: proc,
|
|
175
|
+
diagnostics: diagnosticsMap,
|
|
176
|
+
openFile: async (filePath, timeoutMs = 3000) => {
|
|
177
|
+
const uri = pathToFileURL(filePath).href;
|
|
178
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
179
|
+
const version = fileVersions.get(uri);
|
|
180
|
+
const resolvedLangId = config.languageIdResolver(filePath);
|
|
181
|
+
// Set up a promise that resolves when publishDiagnostics fires for this URI
|
|
182
|
+
const diagReady = new Promise((resolve) => {
|
|
183
|
+
const existing = diagnosticsListeners.get(uri) ?? [];
|
|
184
|
+
existing.push(resolve);
|
|
185
|
+
diagnosticsListeners.set(uri, existing);
|
|
186
|
+
});
|
|
187
|
+
if (version !== undefined) {
|
|
188
|
+
const nextVersion = version + 1;
|
|
189
|
+
fileVersions.set(uri, nextVersion);
|
|
190
|
+
await connection.sendNotification("textDocument/didChange", {
|
|
191
|
+
textDocument: { uri, version: nextVersion },
|
|
192
|
+
contentChanges: [{ text: content }],
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
fileVersions.set(uri, 0);
|
|
197
|
+
await connection.sendNotification("textDocument/didOpen", {
|
|
198
|
+
textDocument: {
|
|
199
|
+
uri,
|
|
200
|
+
languageId: resolvedLangId,
|
|
201
|
+
version: 0,
|
|
202
|
+
text: content,
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
// Wait for diagnostics or timeout — whichever comes first
|
|
207
|
+
await Promise.race([
|
|
208
|
+
diagReady,
|
|
209
|
+
new Promise((resolve) => setTimeout(resolve, timeoutMs)),
|
|
210
|
+
]);
|
|
211
|
+
},
|
|
212
|
+
shutdown: async () => {
|
|
213
|
+
await connection.sendRequest("shutdown");
|
|
214
|
+
await connection.sendNotification("exit");
|
|
215
|
+
connection.dispose();
|
|
216
|
+
proc.kill();
|
|
217
|
+
clients.delete(langId);
|
|
218
|
+
clientInitializationPromises.delete(langId);
|
|
219
|
+
// Note: fileVersions is global, we don't clear it here so documents stay "open" if another server needs them
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
clients.set(langId, client);
|
|
223
|
+
return client;
|
|
224
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import "dotenv/config";
|
|
3
|
+
import pkg from "../package.json" with { type: "json" };
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { render } from 'ink';
|
|
6
|
+
import { App } from './tui/App.js';
|
|
7
|
+
import { createSession, listSessions } from './session/index.js';
|
|
8
|
+
import crypto from 'crypto';
|
|
9
|
+
function main() {
|
|
10
|
+
const program = new Command();
|
|
11
|
+
program
|
|
12
|
+
.name("acmecode")
|
|
13
|
+
.description("AI Coding Assistant CLI")
|
|
14
|
+
.version(pkg.version)
|
|
15
|
+
.option('-n, --new', 'Create a new session')
|
|
16
|
+
.option('-s, --session <id>', 'Resume a specific session ID')
|
|
17
|
+
.option('--list', 'List all sessions')
|
|
18
|
+
.action((options) => {
|
|
19
|
+
if (options.list) {
|
|
20
|
+
const sessions = listSessions();
|
|
21
|
+
console.log('Available Sessions:');
|
|
22
|
+
sessions.forEach(s => console.log(`- ${s.id} (${s.title}) updated at ${s.updated_at}`));
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
let sessionId = options.session;
|
|
26
|
+
// Always create a fresh session unless explicitly resuming one
|
|
27
|
+
if (!sessionId) {
|
|
28
|
+
sessionId = crypto.randomUUID().slice(0, 8);
|
|
29
|
+
createSession(sessionId, `Session ${new Date().toLocaleString()}`);
|
|
30
|
+
}
|
|
31
|
+
const { unmount } = render(React.createElement(App, {
|
|
32
|
+
sessionId: sessionId,
|
|
33
|
+
onExit: () => {
|
|
34
|
+
unmount();
|
|
35
|
+
process.exit(0);
|
|
36
|
+
}
|
|
37
|
+
}));
|
|
38
|
+
});
|
|
39
|
+
program.parse();
|
|
40
|
+
}
|
|
41
|
+
main();
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createOpenAI } from '@ai-sdk/openai';
|
|
2
|
+
import { createAnthropic } from '@ai-sdk/anthropic';
|
|
3
|
+
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
|
4
|
+
import { getProviderKey, getProviderBaseUrl } from '../config/index.js';
|
|
5
|
+
export function getModel(provider, modelName) {
|
|
6
|
+
switch (provider) {
|
|
7
|
+
case 'openai': {
|
|
8
|
+
const key = getProviderKey('openai');
|
|
9
|
+
const baseURL = getProviderBaseUrl('openai');
|
|
10
|
+
if (!key)
|
|
11
|
+
throw new Error("OPENAI_API_KEY is not set.");
|
|
12
|
+
const openai = createOpenAI({ apiKey: key, baseURL });
|
|
13
|
+
return openai.chat(modelName);
|
|
14
|
+
}
|
|
15
|
+
case 'anthropic': {
|
|
16
|
+
const key = getProviderKey('anthropic');
|
|
17
|
+
const baseURL = getProviderBaseUrl('anthropic');
|
|
18
|
+
if (!key)
|
|
19
|
+
throw new Error("ANTHROPIC_API_KEY is not set.");
|
|
20
|
+
const anthropic = createAnthropic({ apiKey: key, baseURL });
|
|
21
|
+
return anthropic(modelName);
|
|
22
|
+
}
|
|
23
|
+
case 'google': {
|
|
24
|
+
const key = getProviderKey('google');
|
|
25
|
+
const baseURL = getProviderBaseUrl('google');
|
|
26
|
+
if (!key)
|
|
27
|
+
throw new Error("GOOGLE_GENERATIVE_AI_API_KEY is not set.");
|
|
28
|
+
const google = createGoogleGenerativeAI({ apiKey: key, baseURL });
|
|
29
|
+
return google(modelName);
|
|
30
|
+
}
|
|
31
|
+
default:
|
|
32
|
+
throw new Error(`Unsupported provider: ${provider}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
2
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
3
|
+
import { tool as createTool } from 'ai';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import * as fs from 'fs/promises';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
// bypass ai tools strict generic typings
|
|
9
|
+
const tool = (options) => createTool(options);
|
|
10
|
+
const clients = {};
|
|
11
|
+
export async function loadMcpConfig() {
|
|
12
|
+
// Check project-level first, then global
|
|
13
|
+
const projectConfigPath = path.join(process.cwd(), '.acmecode-mcp.json');
|
|
14
|
+
const globalConfigPath = path.join(os.homedir(), '.acmecode', 'mcp.json');
|
|
15
|
+
for (const configPath of [projectConfigPath, globalConfigPath]) {
|
|
16
|
+
try {
|
|
17
|
+
const content = await fs.readFile(configPath, 'utf8');
|
|
18
|
+
return JSON.parse(content);
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
if (err.code !== 'ENOENT') {
|
|
22
|
+
console.error(`Error reading MCP config at ${configPath}:`, err.message);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
// Convert MCP JSON Schema to Zod (simplified for now)
|
|
29
|
+
function jsonSchemaToZod(schema) {
|
|
30
|
+
// For basic string/object support in AI SDK without complex parsing
|
|
31
|
+
// In a real implementation, you'd want a robust JSONSchema to Zod converter.
|
|
32
|
+
return z.any();
|
|
33
|
+
}
|
|
34
|
+
export async function getMcpTools() {
|
|
35
|
+
const config = await loadMcpConfig();
|
|
36
|
+
if (!config || !config.mcpServers) {
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
const aiTools = {};
|
|
40
|
+
for (const [serverName, serverConfig] of Object.entries(config.mcpServers)) {
|
|
41
|
+
try {
|
|
42
|
+
if (!clients[serverName]) {
|
|
43
|
+
const transport = new StdioClientTransport({
|
|
44
|
+
command: serverConfig.command,
|
|
45
|
+
args: serverConfig.args || [],
|
|
46
|
+
env: { ...process.env, ...(serverConfig.env || {}) },
|
|
47
|
+
});
|
|
48
|
+
const client = new Client({ name: `acmecode-client-${serverName}`, version: '1.0.0' }, { capabilities: {} });
|
|
49
|
+
await client.connect(transport);
|
|
50
|
+
clients[serverName] = client;
|
|
51
|
+
}
|
|
52
|
+
const client = clients[serverName];
|
|
53
|
+
const { tools } = await client.listTools();
|
|
54
|
+
for (const mcpTool of tools) {
|
|
55
|
+
// Prefix tool names with server name to avoid collisions
|
|
56
|
+
const toolName = `${serverName}__${mcpTool.name}`;
|
|
57
|
+
aiTools[toolName] = tool({
|
|
58
|
+
description: `[MCP: ${serverName}] ${mcpTool.description || mcpTool.name}`,
|
|
59
|
+
parameters: z.any(), // Since we bypass types, any works for JSON schema args
|
|
60
|
+
execute: async (args) => {
|
|
61
|
+
try {
|
|
62
|
+
const result = await client.callTool({
|
|
63
|
+
name: mcpTool.name,
|
|
64
|
+
arguments: args
|
|
65
|
+
});
|
|
66
|
+
if (result.isError) {
|
|
67
|
+
return `Error from MCP tool: ${JSON.stringify(result.content)}`;
|
|
68
|
+
}
|
|
69
|
+
// Map result content to text
|
|
70
|
+
return result.content.map((c) => c.type === 'text' ? c.text : JSON.stringify(c)).join('\n');
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
return `Exception calling MCP tool: ${err.message}`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
console.error(`Failed to initialize MCP server ${serverName}:`, err.message);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return aiTools;
|
|
84
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
let db = null;
|
|
6
|
+
const DB_DIR = path.join(os.homedir(), '.acmecode');
|
|
7
|
+
const DB_PATH = path.join(DB_DIR, 'sessions.db');
|
|
8
|
+
export function initDb() {
|
|
9
|
+
if (db)
|
|
10
|
+
return;
|
|
11
|
+
if (!fs.existsSync(DB_DIR)) {
|
|
12
|
+
fs.mkdirSync(DB_DIR, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
db = new Database(DB_PATH);
|
|
15
|
+
db.exec(`
|
|
16
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
17
|
+
id TEXT PRIMARY KEY,
|
|
18
|
+
title TEXT NOT NULL,
|
|
19
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
23
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
24
|
+
session_id TEXT NOT NULL,
|
|
25
|
+
role TEXT NOT NULL,
|
|
26
|
+
content_json TEXT NOT NULL,
|
|
27
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
28
|
+
FOREIGN KEY(session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
|
29
|
+
);
|
|
30
|
+
`);
|
|
31
|
+
}
|
|
32
|
+
export function createSession(id, title) {
|
|
33
|
+
initDb();
|
|
34
|
+
const titleToUse = title || `Session ${new Date().toLocaleString()}`;
|
|
35
|
+
const stmt = db.prepare('INSERT INTO sessions (id, title) VALUES (?, ?)');
|
|
36
|
+
stmt.run(id, titleToUse);
|
|
37
|
+
return { id, title: titleToUse, updated_at: new Date().toISOString() };
|
|
38
|
+
}
|
|
39
|
+
export function saveMessages(sessionId, messages) {
|
|
40
|
+
initDb();
|
|
41
|
+
// Clear existing messages for this session to overwrite with the updated array
|
|
42
|
+
// This is a simple strategy since the messages array grows append-only
|
|
43
|
+
const deleteStmt = db.prepare('DELETE FROM messages WHERE session_id = ?');
|
|
44
|
+
deleteStmt.run(sessionId);
|
|
45
|
+
const insertStmt = db.prepare('INSERT INTO messages (session_id, role, content_json) VALUES (?, ?, ?)');
|
|
46
|
+
const insertMany = db.transaction((msgs) => {
|
|
47
|
+
for (const msg of msgs) {
|
|
48
|
+
insertStmt.run(sessionId, msg.role, JSON.stringify(msg.content));
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
insertMany(messages);
|
|
52
|
+
// Update session timestamp
|
|
53
|
+
const updateStmt = db.prepare('UPDATE sessions SET updated_at = CURRENT_TIMESTAMP WHERE id = ?');
|
|
54
|
+
updateStmt.run(sessionId);
|
|
55
|
+
}
|
|
56
|
+
export function loadSession(sessionId) {
|
|
57
|
+
initDb();
|
|
58
|
+
const stmt = db.prepare('SELECT role, content_json FROM messages WHERE session_id = ? ORDER BY id ASC');
|
|
59
|
+
const rows = stmt.all(sessionId);
|
|
60
|
+
return rows.map(row => ({
|
|
61
|
+
role: row.role,
|
|
62
|
+
content: JSON.parse(row.content_json)
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
export function listSessions() {
|
|
66
|
+
initDb();
|
|
67
|
+
const stmt = db.prepare('SELECT id, title, updated_at FROM sessions ORDER BY updated_at DESC');
|
|
68
|
+
return stmt.all();
|
|
69
|
+
}
|
|
70
|
+
export function deleteSession(sessionId) {
|
|
71
|
+
initDb();
|
|
72
|
+
const stmt = db.prepare('DELETE FROM sessions WHERE id = ?');
|
|
73
|
+
stmt.run(sessionId);
|
|
74
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
export async function loadSkills() {
|
|
5
|
+
const globalSkillsPath = path.join(os.homedir(), '.acmecode', 'skills');
|
|
6
|
+
const localSkillsPath = path.join(process.cwd(), '.acmecode', 'skills');
|
|
7
|
+
const skills = [];
|
|
8
|
+
for (const dir of [globalSkillsPath, localSkillsPath]) {
|
|
9
|
+
try {
|
|
10
|
+
const files = await fs.readdir(dir);
|
|
11
|
+
for (const file of files) {
|
|
12
|
+
if (file.endsWith('.md')) {
|
|
13
|
+
const content = await fs.readFile(path.join(dir, file), 'utf8');
|
|
14
|
+
const name = file.replace('.md', '');
|
|
15
|
+
// simple frontmatter parsing
|
|
16
|
+
let description = `Skill ${name}`;
|
|
17
|
+
const descriptionMatch = content.match(/description:\s*(.*)/i);
|
|
18
|
+
if (descriptionMatch) {
|
|
19
|
+
description = descriptionMatch[1].trim();
|
|
20
|
+
}
|
|
21
|
+
skills.push({ name, description, content });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
if (err.code !== 'ENOENT') {
|
|
27
|
+
console.warn(`Could not read skills directory ${dir}: ${err.message}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return skills;
|
|
32
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { tool as createTool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import * as fs from 'fs/promises';
|
|
4
|
+
import { exec } from 'child_process';
|
|
5
|
+
import { promisify } from 'util';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
const execAsync = promisify(exec);
|
|
8
|
+
export const toolDefinitions = {
|
|
9
|
+
read_file: {
|
|
10
|
+
description: 'Read the contents of a file. Relative paths are resolved against the current working directory.',
|
|
11
|
+
parameters: z.object({
|
|
12
|
+
path: z.string().min(1).describe('The path to the file to read'),
|
|
13
|
+
}),
|
|
14
|
+
},
|
|
15
|
+
write_file: {
|
|
16
|
+
description: 'Write content to a file. Creates parent directories if needed. Relative paths are resolved against the current working directory.',
|
|
17
|
+
parameters: z.object({
|
|
18
|
+
path: z.string().min(1).describe('The path to the file to write'),
|
|
19
|
+
content: z.string().describe('The content to write into the file'),
|
|
20
|
+
}),
|
|
21
|
+
},
|
|
22
|
+
run_command: {
|
|
23
|
+
description: 'Run a shell command on the host machine',
|
|
24
|
+
parameters: z.object({
|
|
25
|
+
command: z.string().min(1).describe('The shell command to execute'),
|
|
26
|
+
cwd: z.string().optional().describe('The working directory to run the command in'),
|
|
27
|
+
}),
|
|
28
|
+
},
|
|
29
|
+
list_dir: {
|
|
30
|
+
description: 'List the contents of a directory. Relative paths are resolved against the current working directory.',
|
|
31
|
+
parameters: z.object({
|
|
32
|
+
path: z.string().min(1).describe('The path to the directory to list'),
|
|
33
|
+
}),
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
export const toolExecutors = {
|
|
37
|
+
read_file: async (args) => {
|
|
38
|
+
try {
|
|
39
|
+
if (!args.path)
|
|
40
|
+
return `Error: The "path" argument is required`;
|
|
41
|
+
const filePath = path.resolve(process.cwd(), args.path);
|
|
42
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
43
|
+
return content;
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
return `Error reading file: ${err.message}`;
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
write_file: async (args) => {
|
|
50
|
+
try {
|
|
51
|
+
if (!args.path)
|
|
52
|
+
return `Error: The "path" argument is required`;
|
|
53
|
+
if (args.content === undefined)
|
|
54
|
+
return `Error: The "content" argument is required`;
|
|
55
|
+
const filePath = path.resolve(process.cwd(), args.path);
|
|
56
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
57
|
+
await fs.writeFile(filePath, args.content, 'utf8');
|
|
58
|
+
const lineCount = (args.content.match(/\n/g) || []).length + 1;
|
|
59
|
+
return `Successfully wrote to ${filePath} (+${lineCount}行)`;
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
return `Error writing file: ${err.message}`;
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
run_command: async (args) => {
|
|
66
|
+
try {
|
|
67
|
+
const { stdout, stderr } = await execAsync(args.command, { cwd: args.cwd || process.cwd() });
|
|
68
|
+
return `Stdout:\n${stdout}\nStderr:\n${stderr}`;
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
return `Command failed: ${err.message}\nStderr: ${err.stderr}`;
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
list_dir: async (args) => {
|
|
75
|
+
try {
|
|
76
|
+
// Default to current directory if not specified
|
|
77
|
+
const targetPath = args.path || '.';
|
|
78
|
+
const dirPath = path.resolve(process.cwd(), targetPath);
|
|
79
|
+
const files = await fs.readdir(dirPath);
|
|
80
|
+
return files.join('\n');
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
return `Error listing directory: ${err.message}`;
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
// Create AI SDK tool objects (with execute) for backward compatibility
|
|
88
|
+
const tool = (options) => createTool(options);
|
|
89
|
+
export const builtInTools = {};
|
|
90
|
+
for (const [name, def] of Object.entries(toolDefinitions)) {
|
|
91
|
+
builtInTools[name] = tool({
|
|
92
|
+
description: def.description,
|
|
93
|
+
parameters: def.parameters,
|
|
94
|
+
execute: toolExecutors[name],
|
|
95
|
+
});
|
|
96
|
+
}
|