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.
@@ -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
+ }