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.
Files changed (131) hide show
  1. package/.acmecode/config.json +6 -0
  2. package/README.md +124 -0
  3. package/dist/agent/index.js +161 -0
  4. package/dist/cli/bin/acmecode.js +3 -0
  5. package/dist/cli/package.json +25 -0
  6. package/dist/cli/src/index.d.ts +1 -0
  7. package/dist/cli/src/index.js +53 -0
  8. package/dist/config/index.js +92 -0
  9. package/dist/context/index.js +30 -0
  10. package/dist/core/src/agent/index.d.ts +52 -0
  11. package/dist/core/src/agent/index.js +476 -0
  12. package/dist/core/src/config/index.d.ts +83 -0
  13. package/dist/core/src/config/index.js +318 -0
  14. package/dist/core/src/context/index.d.ts +1 -0
  15. package/dist/core/src/context/index.js +30 -0
  16. package/dist/core/src/llm/provider.d.ts +27 -0
  17. package/dist/core/src/llm/provider.js +202 -0
  18. package/dist/core/src/llm/vision.d.ts +7 -0
  19. package/dist/core/src/llm/vision.js +37 -0
  20. package/dist/core/src/mcp/index.d.ts +10 -0
  21. package/dist/core/src/mcp/index.js +84 -0
  22. package/dist/core/src/prompt/anthropic.d.ts +1 -0
  23. package/dist/core/src/prompt/anthropic.js +32 -0
  24. package/dist/core/src/prompt/architect.d.ts +1 -0
  25. package/dist/core/src/prompt/architect.js +17 -0
  26. package/dist/core/src/prompt/autopilot.d.ts +1 -0
  27. package/dist/core/src/prompt/autopilot.js +18 -0
  28. package/dist/core/src/prompt/beast.d.ts +1 -0
  29. package/dist/core/src/prompt/beast.js +83 -0
  30. package/dist/core/src/prompt/gemini.d.ts +1 -0
  31. package/dist/core/src/prompt/gemini.js +45 -0
  32. package/dist/core/src/prompt/index.d.ts +18 -0
  33. package/dist/core/src/prompt/index.js +239 -0
  34. package/dist/core/src/prompt/zen.d.ts +1 -0
  35. package/dist/core/src/prompt/zen.js +13 -0
  36. package/dist/core/src/session/index.d.ts +18 -0
  37. package/dist/core/src/session/index.js +97 -0
  38. package/dist/core/src/skills/index.d.ts +6 -0
  39. package/dist/core/src/skills/index.js +72 -0
  40. package/dist/core/src/tools/batch.d.ts +2 -0
  41. package/dist/core/src/tools/batch.js +65 -0
  42. package/dist/core/src/tools/browser.d.ts +7 -0
  43. package/dist/core/src/tools/browser.js +86 -0
  44. package/dist/core/src/tools/edit.d.ts +11 -0
  45. package/dist/core/src/tools/edit.js +312 -0
  46. package/dist/core/src/tools/index.d.ts +13 -0
  47. package/dist/core/src/tools/index.js +980 -0
  48. package/dist/core/src/tools/lsp-client.d.ts +11 -0
  49. package/dist/core/src/tools/lsp-client.js +224 -0
  50. package/dist/index.js +41 -0
  51. package/dist/llm/provider.js +34 -0
  52. package/dist/mcp/index.js +84 -0
  53. package/dist/session/index.js +74 -0
  54. package/dist/skills/index.js +32 -0
  55. package/dist/tools/index.js +96 -0
  56. package/dist/tui/App.js +297 -0
  57. package/dist/tui/Spinner.js +16 -0
  58. package/dist/tui/TextInput.js +98 -0
  59. package/dist/tui/src/App.d.ts +11 -0
  60. package/dist/tui/src/App.js +1211 -0
  61. package/dist/tui/src/CatLogo.d.ts +10 -0
  62. package/dist/tui/src/CatLogo.js +99 -0
  63. package/dist/tui/src/OptionList.d.ts +15 -0
  64. package/dist/tui/src/OptionList.js +60 -0
  65. package/dist/tui/src/Spinner.d.ts +7 -0
  66. package/dist/tui/src/Spinner.js +18 -0
  67. package/dist/tui/src/TextInput.d.ts +28 -0
  68. package/dist/tui/src/TextInput.js +139 -0
  69. package/dist/tui/src/Tips.d.ts +2 -0
  70. package/dist/tui/src/Tips.js +62 -0
  71. package/dist/tui/src/Toast.d.ts +19 -0
  72. package/dist/tui/src/Toast.js +39 -0
  73. package/dist/tui/src/TodoItem.d.ts +7 -0
  74. package/dist/tui/src/TodoItem.js +21 -0
  75. package/dist/tui/src/i18n.d.ts +172 -0
  76. package/dist/tui/src/i18n.js +189 -0
  77. package/dist/tui/src/markdown.d.ts +6 -0
  78. package/dist/tui/src/markdown.js +356 -0
  79. package/dist/tui/src/theme.d.ts +31 -0
  80. package/dist/tui/src/theme.js +239 -0
  81. package/output.txt +0 -0
  82. package/package.json +44 -0
  83. package/packages/cli/package.json +25 -0
  84. package/packages/cli/src/index.ts +59 -0
  85. package/packages/cli/tsconfig.json +26 -0
  86. package/packages/core/package.json +39 -0
  87. package/packages/core/src/agent/index.ts +588 -0
  88. package/packages/core/src/config/index.ts +383 -0
  89. package/packages/core/src/context/index.ts +34 -0
  90. package/packages/core/src/llm/provider.ts +237 -0
  91. package/packages/core/src/llm/vision.ts +43 -0
  92. package/packages/core/src/mcp/index.ts +110 -0
  93. package/packages/core/src/prompt/anthropic.ts +32 -0
  94. package/packages/core/src/prompt/architect.ts +17 -0
  95. package/packages/core/src/prompt/autopilot.ts +18 -0
  96. package/packages/core/src/prompt/beast.ts +83 -0
  97. package/packages/core/src/prompt/gemini.ts +45 -0
  98. package/packages/core/src/prompt/index.ts +267 -0
  99. package/packages/core/src/prompt/zen.ts +13 -0
  100. package/packages/core/src/session/index.ts +129 -0
  101. package/packages/core/src/skills/index.ts +86 -0
  102. package/packages/core/src/tools/batch.ts +73 -0
  103. package/packages/core/src/tools/browser.ts +95 -0
  104. package/packages/core/src/tools/edit.ts +317 -0
  105. package/packages/core/src/tools/index.ts +1112 -0
  106. package/packages/core/src/tools/lsp-client.ts +303 -0
  107. package/packages/core/tsconfig.json +19 -0
  108. package/packages/tui/package.json +24 -0
  109. package/packages/tui/src/App.tsx +1702 -0
  110. package/packages/tui/src/CatLogo.tsx +134 -0
  111. package/packages/tui/src/OptionList.tsx +95 -0
  112. package/packages/tui/src/Spinner.tsx +28 -0
  113. package/packages/tui/src/TextInput.tsx +202 -0
  114. package/packages/tui/src/Tips.tsx +64 -0
  115. package/packages/tui/src/Toast.tsx +60 -0
  116. package/packages/tui/src/TodoItem.tsx +29 -0
  117. package/packages/tui/src/i18n.ts +203 -0
  118. package/packages/tui/src/markdown.ts +403 -0
  119. package/packages/tui/src/theme.ts +287 -0
  120. package/packages/tui/tsconfig.json +24 -0
  121. package/tsconfig.json +18 -0
  122. package/vscode-acmecode/.vscodeignore +11 -0
  123. package/vscode-acmecode/README.md +57 -0
  124. package/vscode-acmecode/esbuild.js +46 -0
  125. package/vscode-acmecode/images/button-dark.svg +5 -0
  126. package/vscode-acmecode/images/button-light.svg +5 -0
  127. package/vscode-acmecode/images/icon.png +1 -0
  128. package/vscode-acmecode/package-lock.json +490 -0
  129. package/vscode-acmecode/package.json +87 -0
  130. package/vscode-acmecode/src/extension.ts +128 -0
  131. 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
+ }