dave-code 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/LICENSE +15 -0
- package/README.md +53 -0
- package/assets/dave-octopus.ico +0 -0
- package/assets/dave-octopus.png +0 -0
- package/bin/aiClient.js +243 -0
- package/bin/cliMenu.js +496 -0
- package/bin/configManager.js +220 -0
- package/bin/index.js +994 -0
- package/bin/logoRenderer.js +71 -0
- package/package.json +29 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARDARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Dave Code
|
|
2
|
+
|
|
3
|
+
A terminal-based AI coding assistant CLI similar to Claude Code. Dave Code integrates directly with your local workspace, helping you inspect, write, search, and edit files, and answer software engineering questions using state-of-the-art LLMs.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ⚡ **Interactive CLI REPL**: Ask questions and run commands in your workspace with animated terminal visual elements.
|
|
8
|
+
- ⚙️ **Effort Presets**: Adjust AI's effort level from `/effort` (choose between `low`, `medium`, `high`, `xhigh`, `max`, and `ultracode`) to control token consumption and task complexity.
|
|
9
|
+
- 📖 **Smart Incremental Reading (精读模式)**: For non-ultra modes, the assistant silently and delicately reads large files in precise range chunks (e.g. lines 40-100) instead of loading the entire file, saving context tokens and prevent terminal warning clutter.
|
|
10
|
+
- 🔄 **Ultracode Digestion Workflow**: In `ultracode` mode, read and digest large files (>1000 lines) concurrently in chunks with live, animated progress indicators.
|
|
11
|
+
- 🧠 **DeepSeek Reasoning Support**: Detects and cleanly renders internal reasoning (`<think>` blocks) in grey, preventing raw HTML tags from polluting your terminal.
|
|
12
|
+
- 📊 **Token Usage Statistics**: Real-time prompt, completion, and total token usage tracked and printed after each request. Run `/stats` or `/tokens` to see session totals.
|
|
13
|
+
- 🌐 **Proxy & Key Configuration**: Interactively configure API keys, models, base URLs, and proxies via `/config`, `/model`, or `/api`.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
Install globally via npm:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install -g dave-code
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Getting Started
|
|
24
|
+
|
|
25
|
+
1. Set up your LLM provider by running the configuration command:
|
|
26
|
+
```bash
|
|
27
|
+
calldave /config
|
|
28
|
+
```
|
|
29
|
+
This will open the configuration file in your default editor. Fill in your model name, API key, and base URL.
|
|
30
|
+
|
|
31
|
+
2. Start the assistant in your project working directory:
|
|
32
|
+
```bash
|
|
33
|
+
calldave
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
3. Type `/help` to see list of available commands.
|
|
37
|
+
|
|
38
|
+
## Available Commands
|
|
39
|
+
|
|
40
|
+
- `/help` - Show help instructions.
|
|
41
|
+
- `/stats` / `/tokens` - Display session and last request token usage statistics.
|
|
42
|
+
- `/effort` - Interactively adjust AI thinking/reading effort level.
|
|
43
|
+
- `/model` - Switch active LLM profile.
|
|
44
|
+
- `/api` - Set or change the API key for the active profile.
|
|
45
|
+
- `/open <path>` - Open a file or folder in your workspace.
|
|
46
|
+
- `/clear` - Clear screen and reset REPL layout.
|
|
47
|
+
- `/reset` - Clear conversation history and reset token counters.
|
|
48
|
+
- `/lang` - Switch terminal interface between Chinese and English.
|
|
49
|
+
- `/exit` - Exit the assistant.
|
|
50
|
+
|
|
51
|
+
## License
|
|
52
|
+
|
|
53
|
+
ISC License.
|
|
Binary file
|
|
Binary file
|
package/bin/aiClient.js
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ── DESIGN PHILOSOPHY / 视觉设计与交互规范 ──
|
|
3
|
+
* 为了向用户提供极致、顺滑的交互体验,Dave Code 规定:
|
|
4
|
+
* 1. 所有耗时的 API 请求、大文件处理和本地工具调用,都必须具有动画化(Animated)或动态(Dynamic)的终端 UI。
|
|
5
|
+
* 2. 严禁使用静态、刷屏式的连续 Log 输出。对于多步骤操作(如分片消化),应使用单行原地更新(In-place update)的进度条和加载动画。
|
|
6
|
+
* 3. 使用富文本终端色彩(如精心调配的 HSL/ANSI 颜色)和微型字符动画(如 Spinner)来提供实时的视觉反馈。
|
|
7
|
+
* 后续维护与功能续写时,必须严格遵守此设计理念。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { getActiveProfile } from './configManager.js';
|
|
13
|
+
import { ProxyAgent } from 'undici';
|
|
14
|
+
|
|
15
|
+
const systemPrompt = `You are Dave, a terminal-based AI assistant.
|
|
16
|
+
You help the user with coding, CLI tasks, and software engineering in their workspace.
|
|
17
|
+
|
|
18
|
+
You have access to local workspace tools to inspect and modify files/directories. If you need to use them, output the tag EXACTLY as specified below. Before outputting any tool call tag, write a brief, one-sentence thought explaining why you need to execute this action (e.g., what you are looking for or why you choose to read these specific lines). Keep it concise, then output the tag.
|
|
19
|
+
|
|
20
|
+
Tools:
|
|
21
|
+
1. List directory contents:
|
|
22
|
+
<<LIST_DIR: path/to/dir>>
|
|
23
|
+
|
|
24
|
+
2. Read file content (optionally specify line range as file:startLine-endLine):
|
|
25
|
+
<<READ_FILE: path/to/file>> or <<READ_FILE: path/to/file:startLine-endLine>>
|
|
26
|
+
|
|
27
|
+
3. Write/overwrite a file:
|
|
28
|
+
<<WRITE_FILE: path/to/file
|
|
29
|
+
[file contents]
|
|
30
|
+
>>
|
|
31
|
+
|
|
32
|
+
4. Search text inside workspace files (grep):
|
|
33
|
+
<<SEARCH_GREP: query>>
|
|
34
|
+
|
|
35
|
+
Rules for tools:
|
|
36
|
+
- The paths must be relative to the current working directory.
|
|
37
|
+
- To execute multiple tool calls, output them one by one. You must output a single tool call tag (with your preceding thought), wait for the system response, and then output the next if needed.
|
|
38
|
+
- Once you have all info or have completed the task, answer the user normally without any tool tags.`;
|
|
39
|
+
|
|
40
|
+
export const sessionTokenUsage = {
|
|
41
|
+
inputTokens: 0,
|
|
42
|
+
outputTokens: 0,
|
|
43
|
+
totalTokens: 0,
|
|
44
|
+
lastInputTokens: 0,
|
|
45
|
+
lastOutputTokens: 0,
|
|
46
|
+
lastTotalTokens: 0
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export function resetTokenUsage() {
|
|
50
|
+
sessionTokenUsage.inputTokens = 0;
|
|
51
|
+
sessionTokenUsage.outputTokens = 0;
|
|
52
|
+
sessionTokenUsage.totalTokens = 0;
|
|
53
|
+
sessionTokenUsage.lastInputTokens = 0;
|
|
54
|
+
sessionTokenUsage.lastOutputTokens = 0;
|
|
55
|
+
sessionTokenUsage.lastTotalTokens = 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function updateTokenUsage(input, output) {
|
|
59
|
+
sessionTokenUsage.lastInputTokens = input;
|
|
60
|
+
sessionTokenUsage.lastOutputTokens = output;
|
|
61
|
+
sessionTokenUsage.lastTotalTokens = input + output;
|
|
62
|
+
|
|
63
|
+
sessionTokenUsage.inputTokens += input;
|
|
64
|
+
sessionTokenUsage.outputTokens += output;
|
|
65
|
+
sessionTokenUsage.totalTokens += (input + output);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function hasApiKey() {
|
|
69
|
+
const profile = getActiveProfile();
|
|
70
|
+
return !!(profile && profile.apiKey && profile.apiKey !== 'YOUR_API_KEY');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function getAIResponse(messages, activeOpenFile = null, maxReadLines = 600) {
|
|
74
|
+
const profile = getActiveProfile();
|
|
75
|
+
if (!profile) {
|
|
76
|
+
throw new Error('No active configuration profile found. Run /config to add one.');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const { model, apiKey, apiBase, proxyUrl } = profile;
|
|
80
|
+
|
|
81
|
+
if (!apiKey) {
|
|
82
|
+
throw new Error(`No API key configured for model "${model}".`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Setup proxy agent if proxyUrl is configured
|
|
86
|
+
let dispatcher;
|
|
87
|
+
if (proxyUrl) {
|
|
88
|
+
try {
|
|
89
|
+
dispatcher = new ProxyAgent(proxyUrl);
|
|
90
|
+
} catch (e) {
|
|
91
|
+
throw new Error(`Invalid Proxy URL: ${e.message}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Load project instructions from CLAUDE.md if it exists in current working directory
|
|
96
|
+
let activeSystemPrompt = systemPrompt;
|
|
97
|
+
const claudeMdPath = path.join(process.cwd(), 'CLAUDE.md');
|
|
98
|
+
if (fs.existsSync(claudeMdPath)) {
|
|
99
|
+
try {
|
|
100
|
+
const claudeMdContent = fs.readFileSync(claudeMdPath, 'utf8');
|
|
101
|
+
activeSystemPrompt += `\n\n[Project Instructions from CLAUDE.md]:\n${claudeMdContent}`;
|
|
102
|
+
} catch (e) {
|
|
103
|
+
// Ignore
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (activeOpenFile) {
|
|
108
|
+
const relPath = path.relative(process.cwd(), activeOpenFile);
|
|
109
|
+
activeSystemPrompt += `\n\n[Currently Active File]: You are currently editing and working on the file "${relPath}" (absolute path: "${activeOpenFile}"). All reading, writing, and modifications should default to this file unless the user explicitly asks you to work on another file.`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
activeSystemPrompt += `\n\n[Incremental Reading Guidelines]:
|
|
113
|
+
- If you are analyzing a large file, DO NOT try to read the entire file if you only need specific sections.
|
|
114
|
+
- Use SEARCH_GREP to locate keywords/classes/functions first, find their line numbers, and then use <<READ_FILE: path:startLine-endLine>> to read only the relevant range.
|
|
115
|
+
- Try to understand the file using the MINIMUM amount of reading possible. Keep your read ranges targeted and precise.
|
|
116
|
+
- You can read up to ${maxReadLines} lines at a time if you need to, but optimize your range to read only what is necessary to answer the user's request.`;
|
|
117
|
+
|
|
118
|
+
// Smart Type Auto-Detection
|
|
119
|
+
const isAnthropic = (apiBase && apiBase.includes('anthropic.com')) || (!apiBase && model.startsWith('claude-'));
|
|
120
|
+
const isGemini = (apiBase && apiBase.includes('generativelanguage.googleapis.com')) || (!apiBase && model.startsWith('gemini-'));
|
|
121
|
+
|
|
122
|
+
// 1. Anthropic Claude API Payload
|
|
123
|
+
if (isAnthropic) {
|
|
124
|
+
const base = apiBase || 'https://api.anthropic.com';
|
|
125
|
+
const url = `${base}/v1/messages`;
|
|
126
|
+
|
|
127
|
+
const response = await fetch(url, {
|
|
128
|
+
method: 'POST',
|
|
129
|
+
headers: {
|
|
130
|
+
'Content-Type': 'application/json',
|
|
131
|
+
'x-api-key': apiKey,
|
|
132
|
+
'anthropic-version': '2023-06-01'
|
|
133
|
+
},
|
|
134
|
+
body: JSON.stringify({
|
|
135
|
+
model,
|
|
136
|
+
max_tokens: 2048,
|
|
137
|
+
system: activeSystemPrompt,
|
|
138
|
+
messages: messages
|
|
139
|
+
}),
|
|
140
|
+
dispatcher
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
if (!response.ok) {
|
|
144
|
+
const errText = await response.text();
|
|
145
|
+
throw new Error(`Anthropic API error (${response.status}): ${errText}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const data = await response.json();
|
|
149
|
+
if (!data.content || data.content.length === 0) {
|
|
150
|
+
throw new Error('Anthropic returned an empty response.');
|
|
151
|
+
}
|
|
152
|
+
if (data.usage) {
|
|
153
|
+
const input = data.usage.input_tokens || 0;
|
|
154
|
+
const output = data.usage.output_tokens || 0;
|
|
155
|
+
updateTokenUsage(input, output);
|
|
156
|
+
}
|
|
157
|
+
return data.content[0].text;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 2. Google Gemini API Payload
|
|
161
|
+
if (isGemini) {
|
|
162
|
+
const base = apiBase || 'https://generativelanguage.googleapis.com';
|
|
163
|
+
const url = `${base}/v1beta/models/${model}:generateContent?key=${apiKey}`;
|
|
164
|
+
|
|
165
|
+
const contents = messages.map(msg => ({
|
|
166
|
+
role: msg.role === 'assistant' ? 'model' : 'user',
|
|
167
|
+
parts: [{ text: msg.content }]
|
|
168
|
+
}));
|
|
169
|
+
|
|
170
|
+
const response = await fetch(url, {
|
|
171
|
+
method: 'POST',
|
|
172
|
+
headers: {
|
|
173
|
+
'Content-Type': 'application/json'
|
|
174
|
+
},
|
|
175
|
+
body: JSON.stringify({
|
|
176
|
+
contents,
|
|
177
|
+
systemInstruction: {
|
|
178
|
+
parts: [{ text: activeSystemPrompt }]
|
|
179
|
+
}
|
|
180
|
+
}),
|
|
181
|
+
dispatcher
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
if (!response.ok) {
|
|
185
|
+
const errText = await response.text();
|
|
186
|
+
throw new Error(`Gemini API error (${response.status}): ${errText}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const data = await response.json();
|
|
190
|
+
if (!data.candidates || data.candidates.length === 0 || !data.candidates[0].content) {
|
|
191
|
+
throw new Error('Gemini returned an empty response candidate.');
|
|
192
|
+
}
|
|
193
|
+
if (data.usageMetadata) {
|
|
194
|
+
const input = data.usageMetadata.promptTokenCount || 0;
|
|
195
|
+
const output = data.usageMetadata.candidatesTokenCount || 0;
|
|
196
|
+
updateTokenUsage(input, output);
|
|
197
|
+
}
|
|
198
|
+
return data.candidates[0].content.parts[0].text;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// 3. OpenAI-Compatible API Payload (Default fallback)
|
|
202
|
+
const base = apiBase || 'https://api.openai.com/v1';
|
|
203
|
+
const url = `${base}/chat/completions`;
|
|
204
|
+
|
|
205
|
+
const formattedMessages = [
|
|
206
|
+
{ role: 'system', content: activeSystemPrompt },
|
|
207
|
+
...messages
|
|
208
|
+
];
|
|
209
|
+
|
|
210
|
+
const response = await fetch(url, {
|
|
211
|
+
method: 'POST',
|
|
212
|
+
headers: {
|
|
213
|
+
'Content-Type': 'application/json',
|
|
214
|
+
'Authorization': `Bearer ${apiKey}`
|
|
215
|
+
},
|
|
216
|
+
body: JSON.stringify({
|
|
217
|
+
model,
|
|
218
|
+
messages: formattedMessages
|
|
219
|
+
}),
|
|
220
|
+
dispatcher
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
if (!response.ok) {
|
|
224
|
+
const errText = await response.text();
|
|
225
|
+
throw new Error(`API error (${response.status}): ${errText}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const data = await response.json();
|
|
229
|
+
if (!data.choices || data.choices.length === 0) {
|
|
230
|
+
throw new Error('API returned an empty completion response.');
|
|
231
|
+
}
|
|
232
|
+
if (data.usage) {
|
|
233
|
+
const input = data.usage.prompt_tokens || 0;
|
|
234
|
+
const output = data.usage.completion_tokens || 0;
|
|
235
|
+
updateTokenUsage(input, output);
|
|
236
|
+
}
|
|
237
|
+
const message = data.choices[0].message;
|
|
238
|
+
let text = message.content || '';
|
|
239
|
+
if (message.reasoning_content) {
|
|
240
|
+
text = `<think>\n${message.reasoning_content}\n</think>\n` + text;
|
|
241
|
+
}
|
|
242
|
+
return text;
|
|
243
|
+
}
|