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
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,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>;
|