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,6 @@
1
+ {
2
+ "provider": "openai",
3
+ "model": "o1-mini",
4
+ "visionProvider": "anthropic",
5
+ "visionModel": "claude-3-5-sonnet-20241022"
6
+ }
package/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # AcmeCode
2
+
3
+ 原生终端的 AI 编程助手。自主运行,读写文件、执行命令、调用 LSP、操控浏览器,帮你完成真实的编程任务。
4
+
5
+ ## 特性
6
+
7
+ - **多提供商 LLM 支持** — OpenAI、Anthropic、Google、xAI、Mistral、Groq、DeepInfra、OpenRouter,以及任意 OpenAI 兼容接口
8
+ - **内置工具集** — 文件读写编辑、目录浏览、命令执行、ripgrep 搜索、网页抓取、LSP 代码分析、Playwright 浏览器自动化
9
+ - **4 种 Agent 模式** — `agent`(自主)、`plan`(方案设计)、`code`(代码执行)、`zen`(极简对话)
10
+ - **Plan → Code 工作流** — Architect 模式设计方案后自动切换到 Code 模式执行,任务清单持久化跟踪
11
+ - **MCP 协议** — 通过 `.acmecode-mcp.json` 连接外部 MCP 服务器,工具自动注入 Agent
12
+ - **技能系统** — Markdown 文件注入系统提示词,实现专业角色和工作流
13
+ - **会话持久化** — SQLite 本地存储完整对话历史,随时恢复
14
+ - **安全机制** — 危险命令本地检测 + 用户审批
15
+ - **精美 TUI** — 基于 React + Ink,支持 Markdown 渲染、6 种主题、中英文界面
16
+
17
+ ## 安装
18
+
19
+ ```bash
20
+ git clone https://github.com/your-username/acmecode
21
+ cd acmecode
22
+ npm install
23
+ npm run build
24
+ npm link
25
+ ```
26
+
27
+ ## 配置
28
+
29
+ AcmeCode 从 `.env` 文件读取 API 密钥,按以下顺序查找:
30
+
31
+ 1. 当前工作目录的 `.env`
32
+ 2. 全局 `~/.acmecode/.env`
33
+
34
+ ```bash
35
+ # OpenAI(或兼容接口,如 DeepSeek、Qwen)
36
+ OPENAI_API_KEY="sk-..."
37
+ OPENAI_BASE_URL="https://api.deepseek.com/v1" # 可选
38
+
39
+ # Anthropic
40
+ ANTHROPIC_API_KEY="sk-ant-..."
41
+
42
+ # Google
43
+ GOOGLE_GENERATIVE_AI_API_KEY="AIza..."
44
+
45
+ # xAI / Mistral / Groq / DeepInfra / OpenRouter
46
+ XAI_API_KEY="..."
47
+ MISTRAL_API_KEY="..."
48
+ GROQ_API_KEY="..."
49
+ DEEPINFRA_API_KEY="..."
50
+ OPENROUTER_API_KEY="..."
51
+
52
+ # 网页搜索(可选)
53
+ EXA_API_KEY="..."
54
+ ```
55
+
56
+ ## 使用
57
+
58
+ ```bash
59
+ acmecode # 恢复上次会话
60
+ acmecode -n # 新建会话
61
+ acmecode -s <id> # 恢复指定会话
62
+ acmecode --list # 列出所有历史会话
63
+ acmecode [dir] # 在指定目录启动
64
+ ```
65
+
66
+ ### 斜杠命令
67
+
68
+ | 命令 | 说明 |
69
+ | --------------- | --------------------------------------------------------------------- |
70
+ | `/model` | 切换 LLM 模型 |
71
+ | `/mode` | 切换 Agent 模式(agent / plan / code / zen) |
72
+ | `/vision` | 切换视觉模型 |
73
+ | `/reason` | 切换推理模式 |
74
+ | `/skill <name>` | 加载技能文件 |
75
+ | `/theme` | 切换主题(dark / dracula / tokyonight / nord / catppuccin / monokai) |
76
+ | `/lang` | 切换界面语言(en / zh) |
77
+ | `/config` | 查看当前配置 |
78
+ | `/clear` | 清空聊天记录 |
79
+ | `/cancel` | 取消当前 Agent 运行 |
80
+ | `/exit` | 退出 |
81
+
82
+ ## MCP 服务器
83
+
84
+ 在项目根目录创建 `.acmecode-mcp.json`,或全局创建 `~/.acmecode/mcp.json`:
85
+
86
+ ```json
87
+ {
88
+ "mcpServers": {
89
+ "my-server": {
90
+ "command": "npx",
91
+ "args": ["-y", "@modelcontextprotocol/server-example"]
92
+ }
93
+ }
94
+ }
95
+ ```
96
+
97
+ 启动时自动发现并将工具注入 Agent。
98
+
99
+ ## 技能系统
100
+
101
+ 在 `~/.acmecode/skills/` 下创建 Markdown 文件:
102
+
103
+ ```markdown
104
+ ---
105
+ description: 严格执行最佳实践的代码审查员
106
+ ---
107
+
108
+ 你是一位专业的代码审查员...
109
+ ```
110
+
111
+ 在 TUI 中运行 `/skill <filename>` 激活。
112
+
113
+ ## 项目上下文
114
+
115
+ 在项目根目录创建 `ACMECODE.md`,AcmeCode 启动时会自动读取并注入系统提示词,结合 Git 状态提供精准的项目上下文。
116
+
117
+ ## 技术栈
118
+
119
+ - TypeScript 5 + Node.js ESM
120
+ - React 19 + Ink 6(终端 UI)
121
+ - Vercel AI SDK v6(LLM 调用)
122
+ - better-sqlite3(会话存储)
123
+ - Playwright(浏览器自动化)
124
+ - npm workspaces monorepo(`core` / `tui` / `cli`)
@@ -0,0 +1,161 @@
1
+ import { streamText } from 'ai';
2
+ import { getModel } from '../llm/provider.js';
3
+ import { builtInTools, toolExecutors } from '../tools/index.js';
4
+ import { getMcpTools } from '../mcp/index.js';
5
+ const MAX_OUTPUT_LENGTH = 20000; // Safety limit to prevent infinite generation loops
6
+ export async function* runAgent(provider, modelName, messages, systemPrompt) {
7
+ const model = getModel(provider, modelName);
8
+ const mcpTools = await getMcpTools();
9
+ const tools = {
10
+ ...builtInTools,
11
+ ...mcpTools,
12
+ };
13
+ let currentMessages = [...messages];
14
+ let stepCount = 0;
15
+ const MAX_STEPS = 10;
16
+ // Dedup set persists across ALL steps to prevent re-running the same tool call
17
+ const globalCalledSet = new Set();
18
+ while (stepCount < MAX_STEPS) {
19
+ stepCount++;
20
+ let stepText = '';
21
+ const collectedToolCalls = [];
22
+ const collectedToolResults = [];
23
+ try {
24
+ const result = await streamText({
25
+ model,
26
+ messages: currentMessages,
27
+ system: systemPrompt || "You are AcmeCode, an AI coding assistant.",
28
+ tools,
29
+ maxSteps: 1,
30
+ });
31
+ for await (const chunk of result.fullStream) {
32
+ if (chunk.type === 'text-delta') {
33
+ const text = chunk.textDelta || chunk.text || "";
34
+ if (text) {
35
+ stepText += text;
36
+ if (stepText.length > MAX_OUTPUT_LENGTH) {
37
+ yield { type: 'text', text: '\n[Error: Model output exceeded maximum length safety limit]\n' };
38
+ break;
39
+ }
40
+ yield { type: 'text', text };
41
+ }
42
+ }
43
+ else if (chunk.type === 'tool-call') {
44
+ const name = chunk.toolName || '';
45
+ const args = chunk.args || {};
46
+ if (name) {
47
+ collectedToolCalls.push({ name, args });
48
+ yield { type: 'tool-call', name, args };
49
+ }
50
+ }
51
+ else if (chunk.type === 'tool-result') {
52
+ const name = chunk.toolName || '';
53
+ const rawResult = chunk.result ?? chunk.output ?? '';
54
+ const resultStr = typeof rawResult === 'string' ? rawResult : JSON.stringify(rawResult);
55
+ collectedToolResults.push({ name, result: resultStr });
56
+ yield { type: 'tool-result', name, result: resultStr };
57
+ }
58
+ }
59
+ }
60
+ catch (err) {
61
+ yield { type: 'text', text: `\n[Error: ${err.message}]\n` };
62
+ yield { type: 'messages', messages: currentMessages };
63
+ return [];
64
+ }
65
+ // ── Fallback: Handle R1-style fake tool calls in text ──
66
+ if (collectedToolCalls.length === 0) {
67
+ // Strip <think>...</think> blocks — R1 models put draft (often incomplete/empty)
68
+ // tool calls inside these, but real tool calls come AFTER </think>.
69
+ const parseText = stepText.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
70
+ for (const [toolName, toolFn] of Object.entries(toolExecutors)) {
71
+ // Pattern 1: XML-style <read_file>path.html</read_file>
72
+ // Pattern 2: bracket-style [read_file("path.html")] or [read_file('path.html')] or [read_file(path.html)]
73
+ const patterns = [
74
+ new RegExp(`<${toolName}>([\\s\\S]*?)<\/${toolName}>`, 'g'),
75
+ // Match args inside brackets, allowing quoted strings with spaces
76
+ new RegExp(`\\[${toolName}\\(([^\\[\\]]*)\\)\\]`, 'g')
77
+ ];
78
+ for (const regex of patterns) {
79
+ regex.lastIndex = 0; // always reset before scanning
80
+ let match;
81
+ while ((match = regex.exec(parseText)) !== null) {
82
+ const innerText = match[1].trim();
83
+ const args = {};
84
+ // Try XML inner args first: <key>value</key>
85
+ const argRegex = /<(\w+)>([\s\S]*?)<\/\1>/g;
86
+ let argMatch;
87
+ let foundInnerArgs = false;
88
+ while ((argMatch = argRegex.exec(innerText)) !== null) {
89
+ args[argMatch[1]] = argMatch[2].trim();
90
+ foundInnerArgs = true;
91
+ }
92
+ // Fallback: if no XML args, treat inner text as the first positional argument
93
+ if (!foundInnerArgs && innerText) {
94
+ if (toolName === 'list_dir' || toolName === 'read_file') {
95
+ // Strip surrounding quotes (single or double)
96
+ args.path = innerText.replace(/^["']|["']$/g, '').trim();
97
+ }
98
+ else if (toolName === 'run_command') {
99
+ args.command = innerText.replace(/^["']|["']$/g, '').trim();
100
+ }
101
+ }
102
+ // Validate we have a non-empty path/command
103
+ const firstArgValue = Object.values(args)[0];
104
+ if (!firstArgValue)
105
+ continue;
106
+ // De-duplicate: skip if we already ran this exact call in ANY step
107
+ const dedupKey = `${toolName}:${JSON.stringify(args)}`;
108
+ if (globalCalledSet.has(dedupKey))
109
+ continue;
110
+ globalCalledSet.add(dedupKey);
111
+ collectedToolCalls.push({ name: toolName, args });
112
+ yield { type: 'tool-call', name: toolName, args };
113
+ try {
114
+ const result = await toolFn(args);
115
+ const resultStr = typeof result === 'string' ? result : JSON.stringify(result);
116
+ collectedToolResults.push({ name: toolName, result: resultStr });
117
+ yield { type: 'tool-result', name: toolName, result: resultStr };
118
+ }
119
+ catch (err) {
120
+ const resultStr = `Error: ${err.message}`;
121
+ collectedToolResults.push({ name: toolName, result: resultStr });
122
+ yield { type: 'tool-result', name: toolName, result: resultStr };
123
+ }
124
+ }
125
+ }
126
+ }
127
+ }
128
+ // ── If no tools were called (native or XML), we're done ──
129
+ if (collectedToolCalls.length === 0) {
130
+ currentMessages.push({ role: 'assistant', content: stepText });
131
+ yield { type: 'messages', messages: currentMessages };
132
+ return [];
133
+ }
134
+ // ── Step 2: Tools were called. We loop again! ──
135
+ yield { type: 'text', text: '\n' }; // visual separator
136
+ // Build a full tool result summary for the model.
137
+ // Do NOT truncate read_file results — the model needs the full file content
138
+ // to perform its task (otherwise it keeps re-reading the same file).
139
+ const toolSummary = collectedToolResults.map((tr, i) => {
140
+ const tc = collectedToolCalls[i];
141
+ const argsStr = tc ? Object.entries(tc.args)
142
+ .filter(([k]) => k !== 'content')
143
+ .map(([k, v]) => `${k}=${JSON.stringify(v)}`)
144
+ .join(', ') : '';
145
+ // Only truncate for very large results (>10k chars) to stay within context limits
146
+ const resultBody = tr.result.length > 10000
147
+ ? tr.result.slice(0, 10000) + '\n... (content truncated at 10000 chars)'
148
+ : tr.result;
149
+ return `### Tool: ${tr.name}(${argsStr})\n\`\`\`\n${resultBody}\n\`\`\``;
150
+ }).join('\n\n');
151
+ currentMessages.push({ role: 'assistant', content: stepText });
152
+ currentMessages.push({
153
+ role: 'user',
154
+ content: `[SYSTEM] Here are the complete results of the tools you just called. Do NOT call the same tool with the same arguments again.\n\n${toolSummary}\n\nContinue with your task using the information above. If you have finished the task, output the final result now.`
155
+ });
156
+ } // end while
157
+ // Fallback if max steps reached
158
+ yield { type: 'text', text: '\n[Note: Reached maximum autonomous steps.]\n' };
159
+ yield { type: 'messages', messages: currentMessages };
160
+ return [];
161
+ }
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ import '../src/index.js';
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@acmecode/cli",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "acmecode": "bin/acmecode.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "start": "node dist/index.js",
12
+ "dev": "tsx src/index.ts"
13
+ },
14
+ "dependencies": {
15
+ "@acmecode/core": "^1.0.0",
16
+ "@acmecode/tui": "^1.0.0",
17
+ "commander": "^14.0.3",
18
+ "ink": "^6.8.0",
19
+ "react": "^19.2.4"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^25.3.0",
23
+ "@types/react": "^19.2.14"
24
+ }
25
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,53 @@
1
+ import { Command } from "commander";
2
+ import { config as dotenvConfig } from "dotenv";
3
+ dotenvConfig({ quiet: true });
4
+ import pkg from "../package.json" with { type: "json" };
5
+ import React from 'react';
6
+ import { render } from 'ink';
7
+ import App from '@acmecode/tui/App.js';
8
+ import { createSession, listSessions } from '@acmecode/core/session/index.js';
9
+ import crypto from 'crypto';
10
+ import path from 'path';
11
+ function main() {
12
+ const program = new Command();
13
+ program
14
+ .name("acmecode")
15
+ .description("AI Coding Assistant CLI")
16
+ .version(pkg.version)
17
+ .argument('[dir]', 'Directory to run in', '.')
18
+ .option('-n, --new', 'Create a new session')
19
+ .option('-s, --session <id>', 'Resume a specific session ID')
20
+ .option('--list', 'List all sessions')
21
+ .action((dir, options) => {
22
+ // Change current working directory to the specified directory
23
+ const targetDir = path.resolve(process.cwd(), dir);
24
+ try {
25
+ process.chdir(targetDir);
26
+ }
27
+ catch (err) {
28
+ console.error(`Failed to change directory to ${targetDir}: ${err.message}`);
29
+ process.exit(1);
30
+ }
31
+ if (options.list) {
32
+ const sessions = listSessions();
33
+ console.log('Available Sessions:');
34
+ sessions.forEach(s => console.log(`- ${s.id} (${s.title}) updated at ${s.updated_at}`));
35
+ process.exit(0);
36
+ }
37
+ let sessionId = options.session;
38
+ // Always create a fresh session unless explicitly resuming one
39
+ if (!sessionId) {
40
+ sessionId = crypto.randomUUID().slice(0, 8);
41
+ createSession(sessionId, `Session ${new Date().toLocaleString()}`);
42
+ }
43
+ const { unmount } = render(React.createElement(App, {
44
+ sessionId: sessionId,
45
+ onExit: () => {
46
+ unmount();
47
+ process.exit(0);
48
+ }
49
+ }));
50
+ });
51
+ program.parse();
52
+ }
53
+ main();
@@ -0,0 +1,92 @@
1
+ import { config } from "dotenv";
2
+ import { resolve, join } from "path";
3
+ import { homedir } from "os";
4
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
5
+ // Load environment variables from .env in project root if it exists
6
+ config();
7
+ // Load environment variables from ~/.acmecode/.env if it exists
8
+ config({ path: resolve(homedir(), ".acmecode", ".env") });
9
+ const GLOBAL_CONFIG_DIR = resolve(homedir(), ".acmecode");
10
+ const GLOBAL_CONFIG_FILE = join(GLOBAL_CONFIG_DIR, "config.json");
11
+ function getProjectConfigDir() {
12
+ return resolve(process.cwd(), ".acmecode");
13
+ }
14
+ function getProjectConfigFile() {
15
+ return join(getProjectConfigDir(), "config.json");
16
+ }
17
+ function readJsonFile(filePath) {
18
+ try {
19
+ if (existsSync(filePath)) {
20
+ return JSON.parse(readFileSync(filePath, "utf-8"));
21
+ }
22
+ }
23
+ catch { }
24
+ return null;
25
+ }
26
+ function writeJsonFile(filePath, data) {
27
+ const dir = resolve(filePath, "..");
28
+ if (!existsSync(dir)) {
29
+ mkdirSync(dir, { recursive: true });
30
+ }
31
+ writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
32
+ }
33
+ /**
34
+ * Load model config with priority: project .acmecode/config.json > global ~/.acmecode/config.json > defaults
35
+ */
36
+ export function loadModelConfig() {
37
+ // 1. Project-level config
38
+ const projectConfig = readJsonFile(getProjectConfigFile());
39
+ if (projectConfig?.provider && projectConfig?.model) {
40
+ return { provider: projectConfig.provider, model: projectConfig.model };
41
+ }
42
+ // 2. Global config
43
+ const globalConfig = readJsonFile(GLOBAL_CONFIG_FILE);
44
+ if (globalConfig?.provider && globalConfig?.model) {
45
+ return { provider: globalConfig.provider, model: globalConfig.model };
46
+ }
47
+ // 3. Hardcoded defaults
48
+ return { provider: "openai", model: "gpt-4o" };
49
+ }
50
+ /**
51
+ * Save model config to the project's .acmecode/config.json
52
+ */
53
+ export function saveProjectModelConfig(provider, model) {
54
+ const filePath = getProjectConfigFile();
55
+ const existing = readJsonFile(filePath) || {};
56
+ existing.provider = provider;
57
+ existing.model = model;
58
+ writeJsonFile(filePath, existing);
59
+ }
60
+ /**
61
+ * Save model config to the global ~/.acmecode/config.json
62
+ */
63
+ export function saveGlobalModelConfig(provider, model) {
64
+ const existing = readJsonFile(GLOBAL_CONFIG_FILE) || {};
65
+ existing.provider = provider;
66
+ existing.model = model;
67
+ writeJsonFile(GLOBAL_CONFIG_FILE, existing);
68
+ }
69
+ export const getProviderKey = (provider) => {
70
+ switch (provider) {
71
+ case "openai":
72
+ return process.env.OPENAI_API_KEY || "sk-Plobc3VM4qzRkIUAakQjtj7hwHPedlSoU4haaPNWWNIESiya";
73
+ case "anthropic":
74
+ return process.env.ANTHROPIC_API_KEY;
75
+ case "google":
76
+ return process.env.GOOGLE_GENERATIVE_AI_API_KEY;
77
+ default:
78
+ return undefined;
79
+ }
80
+ };
81
+ export const getProviderBaseUrl = (provider) => {
82
+ switch (provider) {
83
+ case "openai":
84
+ return process.env.OPENAI_BASE_URL || "https://apis.acmecloud.cn/v1";
85
+ case "anthropic":
86
+ return process.env.ANTHROPIC_BASE_URL;
87
+ case "google":
88
+ return process.env.GOOGLE_GENERATIVE_AI_BASE_URL;
89
+ default:
90
+ return undefined;
91
+ }
92
+ };
@@ -0,0 +1,30 @@
1
+ import * as fs from 'fs/promises';
2
+ import { exec } from 'child_process';
3
+ import { promisify } from 'util';
4
+ import path from 'path';
5
+ const execAsync = promisify(exec);
6
+ export async function getProjectContext() {
7
+ let context = '';
8
+ // 1. ACMECODE.md
9
+ try {
10
+ const acmecodePath = path.join(process.cwd(), 'ACMECODE.md');
11
+ const content = await fs.readFile(acmecodePath, 'utf8');
12
+ context += `\n\n[Project Instructions (ACMECODE.md)]\n${content}`;
13
+ }
14
+ catch (e) {
15
+ // Ignore if missing
16
+ }
17
+ // 2. Git context
18
+ try {
19
+ const { stdout: branch } = await execAsync('git rev-parse --abbrev-ref HEAD');
20
+ const { stdout: status } = await execAsync('git status -s');
21
+ context += `\n\n[Git Status (Branch: ${branch.trim()})]\n${status.trim() || 'Clean working directory'}`;
22
+ // Let's also get the last commit message for context
23
+ const { stdout: log } = await execAsync('git log -1 --oneline');
24
+ context += `\nLast Commit: ${log.trim()}`;
25
+ }
26
+ catch (e) {
27
+ // Ignore if not a git repo
28
+ }
29
+ return context;
30
+ }
@@ -0,0 +1,52 @@
1
+ import { ProviderType } from "../llm/provider.js";
2
+ import { ReasoningLevel, AgentMode } from "../config/index.js";
3
+ export type AgentEvent = {
4
+ type: "text";
5
+ text: string;
6
+ } | {
7
+ type: "tool-call";
8
+ name: string;
9
+ args: Record<string, unknown>;
10
+ toolCallId?: string;
11
+ } | {
12
+ type: "tool-call-delta";
13
+ name: string;
14
+ args: Record<string, unknown>;
15
+ partial?: boolean;
16
+ toolCallId?: string;
17
+ } | {
18
+ type: "tool-result";
19
+ name: string;
20
+ result: string;
21
+ toolCallId?: string;
22
+ } | {
23
+ type: "tool-generating";
24
+ name: string;
25
+ args: Record<string, unknown>;
26
+ } | {
27
+ type: "tool-approval-required";
28
+ name: string;
29
+ args: Record<string, unknown>;
30
+ riskLevel?: string;
31
+ isLocalGuard?: boolean;
32
+ } | {
33
+ type: "messages";
34
+ messages: any[];
35
+ promptLength?: number;
36
+ } | {
37
+ type: "step";
38
+ step: number;
39
+ maxSteps: number;
40
+ } | {
41
+ type: "finish";
42
+ usage: any;
43
+ } | {
44
+ type: "mode-changed";
45
+ mode: AgentMode;
46
+ planFile?: string;
47
+ };
48
+ /**
49
+ * Run the AI agent with streaming, multi-step tool calls, and structured events.
50
+ * Inspired by opencode's LLM.stream() pattern.
51
+ */
52
+ export declare function runAgent(provider: ProviderType, modelName: string, messages: any[], systemPrompt?: string, abortSignal?: AbortSignal, reasoningLevel?: ReasoningLevel, agentMode?: AgentMode, activePlanFile?: string, activeSkillContent?: string): AsyncGenerator<AgentEvent, any[], unknown>;