dave-code 1.0.4 → 1.2.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/README.md +39 -5
- package/bin/aiClient.js +878 -159
- package/bin/check.js +11 -0
- package/bin/cliMenu.js +251 -172
- package/bin/commandRouter.js +70 -0
- package/bin/configManager.js +153 -64
- package/bin/contextManager.js +167 -0
- package/bin/index.js +2103 -573
- package/bin/markdownRenderer.js +264 -0
- package/bin/memoryManager.js +182 -0
- package/bin/planManager.js +291 -0
- package/bin/projectNotebookManager.js +839 -0
- package/bin/runtimeEvents.js +104 -0
- package/bin/scanManager.js +561 -0
- package/bin/sessionManager.js +182 -0
- package/bin/terminalRenderer.js +701 -0
- package/bin/textWidth.js +194 -0
- package/bin/thunderManager.js +302 -0
- package/bin/thunderOrchestrator.js +263 -0
- package/bin/thunderPrompts.js +53 -0
- package/bin/thunderRenderer.js +200 -0
- package/bin/toolRuntime.js +1607 -0
- package/package.json +6 -5
|
@@ -0,0 +1,182 @@
|
|
|
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 os from 'os';
|
|
13
|
+
import crypto from 'crypto';
|
|
14
|
+
|
|
15
|
+
export let SESSIONS_DIR = path.join(os.homedir(), '.dave-code-sessions');
|
|
16
|
+
|
|
17
|
+
export function setSessionsDirForTesting(directory) {
|
|
18
|
+
SESSIONS_DIR = directory;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeMessage(message) {
|
|
22
|
+
if (!message || typeof message !== 'object') return null;
|
|
23
|
+
if (!['user', 'assistant', 'tool'].includes(message.role)) return null;
|
|
24
|
+
if (typeof message.content !== 'string') return null;
|
|
25
|
+
return { ...message, role: message.role, content: message.content };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeSession(session) {
|
|
29
|
+
if (!session || typeof session !== 'object' || !session.id || !Array.isArray(session.messages)) return null;
|
|
30
|
+
if (!/^[A-Za-z0-9_-]+$/.test(String(session.id))) return null;
|
|
31
|
+
const messages = session.messages.map(normalizeMessage).filter(Boolean);
|
|
32
|
+
const rawPlan = session.contextPlan && typeof session.contextPlan === 'object' ? session.contextPlan : null;
|
|
33
|
+
const contextPlan = rawPlan && Number.isFinite(rawPlan.budgetTokens) ? {
|
|
34
|
+
version: 1,
|
|
35
|
+
requestFingerprint: String(rawPlan.requestFingerprint || '').slice(0, 100),
|
|
36
|
+
snapshotId: String(rawPlan.snapshotId || '').slice(0, 100),
|
|
37
|
+
contextWindowTokens: Math.max(8192, Number(rawPlan.contextWindowTokens) || 32768),
|
|
38
|
+
usableTokens: Math.max(4096, Number(rawPlan.usableTokens) || Number(rawPlan.budgetTokens)),
|
|
39
|
+
budgetTokens: Math.max(4096, Number(rawPlan.budgetTokens)),
|
|
40
|
+
reserveTokens: Math.max(0, Number(rawPlan.reserveTokens) || 0),
|
|
41
|
+
rationale: String(rawPlan.rationale || '').slice(0, 2000),
|
|
42
|
+
recommendedFiles: Array.isArray(rawPlan.recommendedFiles) ? rawPlan.recommendedFiles.slice(0, 100) : [],
|
|
43
|
+
degraded: rawPlan.degraded === true,
|
|
44
|
+
createdAt: Number(rawPlan.createdAt) || Date.now()
|
|
45
|
+
} : null;
|
|
46
|
+
return {
|
|
47
|
+
...session,
|
|
48
|
+
id: String(session.id),
|
|
49
|
+
title: String(session.title || 'New Chat'),
|
|
50
|
+
timestamp: Number(session.timestamp) || Date.now(),
|
|
51
|
+
messages,
|
|
52
|
+
workspaceRoot: typeof session.workspaceRoot === 'string' ? session.workspaceRoot : null,
|
|
53
|
+
activeOpenFile: typeof session.activeOpenFile === 'string' ? session.activeOpenFile : null,
|
|
54
|
+
workMode: session.workMode === 'thunder' ? 'thunder' : 'highway',
|
|
55
|
+
contextPlan
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function atomicWriteSession(filePath, sessionData) {
|
|
60
|
+
const tempPath = `${filePath}.${process.pid}.${crypto.randomBytes(5).toString('hex')}.tmp`;
|
|
61
|
+
fs.writeFileSync(tempPath, JSON.stringify(sessionData, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
62
|
+
try {
|
|
63
|
+
fs.chmodSync(tempPath, 0o600);
|
|
64
|
+
} catch {
|
|
65
|
+
// Best effort on Windows.
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
fs.renameSync(tempPath, filePath);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (process.platform !== 'win32' || !fs.existsSync(filePath)) throw error;
|
|
71
|
+
fs.unlinkSync(filePath);
|
|
72
|
+
fs.renameSync(tempPath, filePath);
|
|
73
|
+
} finally {
|
|
74
|
+
if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function ensureSessionsDir() {
|
|
79
|
+
try {
|
|
80
|
+
if (!fs.existsSync(SESSIONS_DIR)) {
|
|
81
|
+
fs.mkdirSync(SESSIONS_DIR, { recursive: true, mode: 0o700 });
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
fs.chmodSync(SESSIONS_DIR, 0o700);
|
|
85
|
+
} catch {
|
|
86
|
+
// Best effort on Windows.
|
|
87
|
+
}
|
|
88
|
+
} catch (e) {
|
|
89
|
+
// Ignore folder creation errors (handled gracefully during saves)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* List all saved chat sessions sorted by timestamp descending (newest first).
|
|
95
|
+
* @returns {Array} Array of session objects
|
|
96
|
+
*/
|
|
97
|
+
export function listSessions() {
|
|
98
|
+
ensureSessionsDir();
|
|
99
|
+
try {
|
|
100
|
+
if (!fs.existsSync(SESSIONS_DIR)) return [];
|
|
101
|
+
|
|
102
|
+
const files = fs.readdirSync(SESSIONS_DIR);
|
|
103
|
+
const sessions = [];
|
|
104
|
+
for (const file of files) {
|
|
105
|
+
if (file.endsWith('.json')) {
|
|
106
|
+
try {
|
|
107
|
+
const filePath = path.join(SESSIONS_DIR, file);
|
|
108
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
109
|
+
const session = normalizeSession(JSON.parse(content));
|
|
110
|
+
if (session && session.id) {
|
|
111
|
+
sessions.push(session);
|
|
112
|
+
}
|
|
113
|
+
} catch (e) {
|
|
114
|
+
// Skip invalid JSON files
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return sessions.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
|
|
119
|
+
} catch (e) {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Save or update a session file.
|
|
126
|
+
* @param {string} sessionId Unique session identifier
|
|
127
|
+
* @param {object} sessionData Session object containing messages, title, timestamp, and workspace metadata
|
|
128
|
+
*/
|
|
129
|
+
export function saveSession(sessionId, sessionData) {
|
|
130
|
+
ensureSessionsDir();
|
|
131
|
+
try {
|
|
132
|
+
const safeId = String(sessionId || '');
|
|
133
|
+
if (!/^[A-Za-z0-9_-]+$/.test(safeId)) return false;
|
|
134
|
+
const normalized = normalizeSession({ ...sessionData, id: safeId });
|
|
135
|
+
if (!normalized) return false;
|
|
136
|
+
const filePath = path.join(SESSIONS_DIR, `${safeId}.json`);
|
|
137
|
+
atomicWriteSession(filePath, normalized);
|
|
138
|
+
return true;
|
|
139
|
+
} catch (e) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Load a session from its identifier.
|
|
146
|
+
* @param {string} sessionId Unique session identifier
|
|
147
|
+
* @returns {object|null} Loaded session or null if not found
|
|
148
|
+
*/
|
|
149
|
+
export function loadSession(sessionId) {
|
|
150
|
+
ensureSessionsDir();
|
|
151
|
+
try {
|
|
152
|
+
if (!/^[A-Za-z0-9_-]+$/.test(String(sessionId || ''))) return null;
|
|
153
|
+
const filePath = path.join(SESSIONS_DIR, `${sessionId}.json`);
|
|
154
|
+
if (fs.existsSync(filePath)) {
|
|
155
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
156
|
+
return normalizeSession(JSON.parse(content));
|
|
157
|
+
}
|
|
158
|
+
} catch (e) {
|
|
159
|
+
// Return null on parsing or read failures
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Delete a session from storage.
|
|
166
|
+
* @param {string} sessionId Unique session identifier
|
|
167
|
+
* @returns {boolean} True if successfully deleted
|
|
168
|
+
*/
|
|
169
|
+
export function deleteSession(sessionId) {
|
|
170
|
+
ensureSessionsDir();
|
|
171
|
+
try {
|
|
172
|
+
if (!/^[A-Za-z0-9_-]+$/.test(String(sessionId || ''))) return false;
|
|
173
|
+
const filePath = path.join(SESSIONS_DIR, `${sessionId}.json`);
|
|
174
|
+
if (fs.existsSync(filePath)) {
|
|
175
|
+
fs.unlinkSync(filePath);
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
} catch (e) {
|
|
179
|
+
// Ignore unlink failures
|
|
180
|
+
}
|
|
181
|
+
return false;
|
|
182
|
+
}
|