snow-ai 0.4.3 → 0.4.5
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/dist/api/anthropic.d.ts +0 -3
- package/dist/api/anthropic.js +54 -30
- package/dist/api/chat.js +46 -30
- package/dist/api/gemini.js +97 -81
- package/dist/api/responses.js +46 -30
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +14 -3
- package/dist/hooks/useConversation.js +4 -1
- package/dist/ui/components/MarkdownRenderer.js +63 -16
- package/dist/utils/devMode.d.ts +13 -0
- package/dist/utils/devMode.js +54 -0
- package/package.json +4 -2
package/dist/api/anthropic.d.ts
CHANGED
|
@@ -41,7 +41,4 @@ export interface AnthropicMessageParam {
|
|
|
41
41
|
content: string | Array<any>;
|
|
42
42
|
}
|
|
43
43
|
export declare function resetAnthropicClient(): void;
|
|
44
|
-
/**
|
|
45
|
-
* Create streaming chat completion using Anthropic API
|
|
46
|
-
*/
|
|
47
44
|
export declare function createStreamingAnthropicCompletion(options: AnthropicOptions, abortSignal?: AbortSignal, onRetry?: (error: Error, attempt: number, nextDelay: number) => void): AsyncGenerator<AnthropicStreamChunk, void, unknown>;
|
package/dist/api/anthropic.js
CHANGED
|
@@ -5,6 +5,7 @@ import { withRetryGenerator, parseJsonWithFix } from '../utils/retryUtils.js';
|
|
|
5
5
|
import { logger } from '../utils/logger.js';
|
|
6
6
|
import { addProxyToFetchOptions } from '../utils/proxyUtils.js';
|
|
7
7
|
import { saveUsageToFile } from '../utils/usageLogger.js';
|
|
8
|
+
import { isDevMode, getDevUserId } from '../utils/devMode.js';
|
|
8
9
|
let anthropicConfig = null;
|
|
9
10
|
// Persistent userId that remains the same until application restart
|
|
10
11
|
let persistentUserId = null;
|
|
@@ -35,8 +36,16 @@ export function resetAnthropicClient() {
|
|
|
35
36
|
* Generate a persistent user_id that remains the same until application restart
|
|
36
37
|
* Format: user_<hash>_account__session_<uuid>
|
|
37
38
|
* This matches Anthropic's expected format for tracking and caching
|
|
39
|
+
*
|
|
40
|
+
* In dev mode (--dev flag), uses a persistent userId from ~/.snow/dev-user-id
|
|
41
|
+
* instead of generating a new one each session
|
|
38
42
|
*/
|
|
39
43
|
function getPersistentUserId() {
|
|
44
|
+
// Check if dev mode is enabled
|
|
45
|
+
if (isDevMode()) {
|
|
46
|
+
return getDevUserId();
|
|
47
|
+
}
|
|
48
|
+
// Normal mode: generate userId per session
|
|
40
49
|
if (!persistentUserId) {
|
|
41
50
|
const sessionId = randomUUID();
|
|
42
51
|
const hash = createHash('sha256')
|
|
@@ -251,43 +260,58 @@ function convertToAnthropicMessages(messages, includeBuiltinSystemPrompt = true)
|
|
|
251
260
|
async function* parseSSEStream(reader) {
|
|
252
261
|
const decoder = new TextDecoder();
|
|
253
262
|
let buffer = '';
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
continue;
|
|
265
|
-
if (trimmed === 'data: [DONE]' || trimmed === 'data:[DONE]') {
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
// Handle both "event: " and "event:" formats
|
|
269
|
-
if (trimmed.startsWith('event:')) {
|
|
270
|
-
// Event type, will be followed by data
|
|
271
|
-
continue;
|
|
263
|
+
try {
|
|
264
|
+
while (true) {
|
|
265
|
+
const { done, value } = await reader.read();
|
|
266
|
+
if (done) {
|
|
267
|
+
// ✅ 关键修复:检查buffer是否有残留数据
|
|
268
|
+
if (buffer.trim()) {
|
|
269
|
+
// 连接异常中断,抛出明确错误
|
|
270
|
+
throw new Error(`Stream terminated unexpectedly with incomplete data: ${buffer.substring(0, 100)}...`);
|
|
271
|
+
}
|
|
272
|
+
break; // 正常结束
|
|
272
273
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
274
|
+
buffer += decoder.decode(value, { stream: true });
|
|
275
|
+
const lines = buffer.split('\n');
|
|
276
|
+
buffer = lines.pop() || '';
|
|
277
|
+
for (const line of lines) {
|
|
278
|
+
const trimmed = line.trim();
|
|
279
|
+
if (!trimmed || trimmed.startsWith(':'))
|
|
280
|
+
continue;
|
|
281
|
+
if (trimmed === 'data: [DONE]' || trimmed === 'data:[DONE]') {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
// Handle both "event: " and "event:" formats
|
|
285
|
+
if (trimmed.startsWith('event:')) {
|
|
286
|
+
// Event type, will be followed by data
|
|
287
|
+
continue;
|
|
280
288
|
}
|
|
281
|
-
|
|
282
|
-
|
|
289
|
+
// Handle both "data: " and "data:" formats
|
|
290
|
+
if (trimmed.startsWith('data:')) {
|
|
291
|
+
const data = trimmed.startsWith('data: ')
|
|
292
|
+
? trimmed.slice(6)
|
|
293
|
+
: trimmed.slice(5);
|
|
294
|
+
const parseResult = parseJsonWithFix(data, {
|
|
295
|
+
toolName: 'SSE stream',
|
|
296
|
+
logWarning: false,
|
|
297
|
+
logError: true,
|
|
298
|
+
});
|
|
299
|
+
if (parseResult.success) {
|
|
300
|
+
yield parseResult.data;
|
|
301
|
+
}
|
|
283
302
|
}
|
|
284
303
|
}
|
|
285
304
|
}
|
|
286
305
|
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
const { logger } = await import('../utils/logger.js');
|
|
308
|
+
logger.error('SSE stream parsing error:', {
|
|
309
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
310
|
+
remainingBuffer: buffer.substring(0, 200),
|
|
311
|
+
});
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
287
314
|
}
|
|
288
|
-
/**
|
|
289
|
-
* Create streaming chat completion using Anthropic API
|
|
290
|
-
*/
|
|
291
315
|
export async function* createStreamingAnthropicCompletion(options, abortSignal, onRetry) {
|
|
292
316
|
yield* withRetryGenerator(async function* () {
|
|
293
317
|
const config = getAnthropicConfig();
|
package/dist/api/chat.js
CHANGED
|
@@ -126,41 +126,57 @@ export function resetOpenAIClient() {
|
|
|
126
126
|
async function* parseSSEStream(reader) {
|
|
127
127
|
const decoder = new TextDecoder();
|
|
128
128
|
let buffer = '';
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
continue;
|
|
140
|
-
if (trimmed === 'data: [DONE]' || trimmed === 'data:[DONE]') {
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
// Handle both "event: " and "event:" formats
|
|
144
|
-
if (trimmed.startsWith('event:')) {
|
|
145
|
-
// Event type, will be followed by data
|
|
146
|
-
continue;
|
|
129
|
+
try {
|
|
130
|
+
while (true) {
|
|
131
|
+
const { done, value } = await reader.read();
|
|
132
|
+
if (done) {
|
|
133
|
+
// ✅ 关键修复:检查buffer是否有残留数据
|
|
134
|
+
if (buffer.trim()) {
|
|
135
|
+
// 连接异常中断,抛出明确错误
|
|
136
|
+
throw new Error(`Stream terminated unexpectedly with incomplete data: ${buffer.substring(0, 100)}...`);
|
|
137
|
+
}
|
|
138
|
+
break; // 正常结束
|
|
147
139
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
|
|
140
|
+
buffer += decoder.decode(value, { stream: true });
|
|
141
|
+
const lines = buffer.split('\n');
|
|
142
|
+
buffer = lines.pop() || '';
|
|
143
|
+
for (const line of lines) {
|
|
144
|
+
const trimmed = line.trim();
|
|
145
|
+
if (!trimmed || trimmed.startsWith(':'))
|
|
146
|
+
continue;
|
|
147
|
+
if (trimmed === 'data: [DONE]' || trimmed === 'data:[DONE]') {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
// Handle both "event: " and "event:" formats
|
|
151
|
+
if (trimmed.startsWith('event:')) {
|
|
152
|
+
// Event type, will be followed by data
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
// Handle both "data: " and "data:" formats
|
|
156
|
+
if (trimmed.startsWith('data:')) {
|
|
157
|
+
const data = trimmed.startsWith('data: ')
|
|
158
|
+
? trimmed.slice(6)
|
|
159
|
+
: trimmed.slice(5);
|
|
160
|
+
const parseResult = parseJsonWithFix(data, {
|
|
161
|
+
toolName: 'SSE stream',
|
|
162
|
+
logWarning: false,
|
|
163
|
+
logError: true,
|
|
164
|
+
});
|
|
165
|
+
if (parseResult.success) {
|
|
166
|
+
yield parseResult.data;
|
|
167
|
+
}
|
|
160
168
|
}
|
|
161
169
|
}
|
|
162
170
|
}
|
|
163
171
|
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
const { logger } = await import('../utils/logger.js');
|
|
174
|
+
logger.error('SSE stream parsing error:', {
|
|
175
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
176
|
+
remainingBuffer: buffer.substring(0, 200),
|
|
177
|
+
});
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
164
180
|
}
|
|
165
181
|
/**
|
|
166
182
|
* Simple streaming chat completion - only handles OpenAI interaction
|
package/dist/api/gemini.js
CHANGED
|
@@ -280,97 +280,113 @@ export async function* createStreamingGeminiCompletion(options, abortSignal, onR
|
|
|
280
280
|
const reader = response.body.getReader();
|
|
281
281
|
const decoder = new TextDecoder();
|
|
282
282
|
let buffer = '';
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
for (const line of lines) {
|
|
294
|
-
const trimmed = line.trim();
|
|
295
|
-
if (!trimmed || trimmed.startsWith(':'))
|
|
296
|
-
continue;
|
|
297
|
-
if (trimmed === 'data: [DONE]' || trimmed === 'data:[DONE]') {
|
|
298
|
-
break;
|
|
283
|
+
try {
|
|
284
|
+
while (true) {
|
|
285
|
+
const { done, value } = await reader.read();
|
|
286
|
+
if (done) {
|
|
287
|
+
// ✅ 关键修复:检查buffer是否有残留数据
|
|
288
|
+
if (buffer.trim()) {
|
|
289
|
+
// 连接异常中断,抛出明确错误
|
|
290
|
+
throw new Error(`Stream terminated unexpectedly with incomplete data: ${buffer.substring(0, 100)}...`);
|
|
291
|
+
}
|
|
292
|
+
break; // 正常结束
|
|
299
293
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
// Event type, will be followed by data
|
|
303
|
-
continue;
|
|
294
|
+
if (abortSignal?.aborted) {
|
|
295
|
+
return;
|
|
304
296
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
//
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
297
|
+
buffer += decoder.decode(value, { stream: true });
|
|
298
|
+
const lines = buffer.split('\n');
|
|
299
|
+
buffer = lines.pop() || '';
|
|
300
|
+
for (const line of lines) {
|
|
301
|
+
const trimmed = line.trim();
|
|
302
|
+
if (!trimmed || trimmed.startsWith(':'))
|
|
303
|
+
continue;
|
|
304
|
+
if (trimmed === 'data: [DONE]' || trimmed === 'data:[DONE]') {
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
// Handle both "event: " and "event:" formats
|
|
308
|
+
if (trimmed.startsWith('event:')) {
|
|
309
|
+
// Event type, will be followed by data
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
// Handle both "data: " and "data:" formats
|
|
313
|
+
if (trimmed.startsWith('data:')) {
|
|
314
|
+
const data = trimmed.startsWith('data: ')
|
|
315
|
+
? trimmed.slice(6)
|
|
316
|
+
: trimmed.slice(5);
|
|
317
|
+
const parseResult = parseJsonWithFix(data, {
|
|
318
|
+
toolName: 'Gemini SSE stream',
|
|
319
|
+
logWarning: false,
|
|
320
|
+
logError: true,
|
|
321
|
+
});
|
|
322
|
+
if (parseResult.success) {
|
|
323
|
+
const chunk = parseResult.data;
|
|
324
|
+
// Process candidates
|
|
325
|
+
if (chunk.candidates && chunk.candidates.length > 0) {
|
|
326
|
+
const candidate = chunk.candidates[0];
|
|
327
|
+
if (candidate.content && candidate.content.parts) {
|
|
328
|
+
for (const part of candidate.content.parts) {
|
|
329
|
+
// Process thought content (Gemini thinking)
|
|
330
|
+
// When part.thought === true, the text field contains thinking content
|
|
331
|
+
if (part.thought === true && part.text) {
|
|
332
|
+
thinkingTextBuffer += part.text;
|
|
333
|
+
yield {
|
|
334
|
+
type: 'reasoning_delta',
|
|
335
|
+
delta: part.text,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
// Process regular text content (when thought is not true)
|
|
339
|
+
else if (part.text) {
|
|
340
|
+
contentBuffer += part.text;
|
|
341
|
+
yield {
|
|
342
|
+
type: 'content',
|
|
343
|
+
content: part.text,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
// Process function calls
|
|
347
|
+
if (part.functionCall) {
|
|
348
|
+
hasToolCalls = true;
|
|
349
|
+
const fc = part.functionCall;
|
|
350
|
+
const toolCall = {
|
|
351
|
+
id: `call_${toolCallIndex++}`,
|
|
352
|
+
type: 'function',
|
|
353
|
+
function: {
|
|
354
|
+
name: fc.name,
|
|
355
|
+
arguments: JSON.stringify(fc.args || {}),
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
toolCallsBuffer.push(toolCall);
|
|
359
|
+
// Yield delta for token counting
|
|
360
|
+
const deltaText = fc.name + JSON.stringify(fc.args || {});
|
|
361
|
+
yield {
|
|
362
|
+
type: 'tool_call_delta',
|
|
363
|
+
delta: deltaText,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
358
366
|
}
|
|
359
367
|
}
|
|
360
368
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
}
|
|
369
|
+
// Track usage info
|
|
370
|
+
if (chunk.usageMetadata) {
|
|
371
|
+
totalTokens = {
|
|
372
|
+
prompt: chunk.usageMetadata.promptTokenCount || 0,
|
|
373
|
+
completion: chunk.usageMetadata.candidatesTokenCount || 0,
|
|
374
|
+
total: chunk.usageMetadata.totalTokenCount || 0,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
369
377
|
}
|
|
370
378
|
}
|
|
371
379
|
}
|
|
372
380
|
}
|
|
373
381
|
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
const { logger } = await import('../utils/logger.js');
|
|
384
|
+
logger.error('Gemini SSE stream parsing error:', {
|
|
385
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
386
|
+
remainingBuffer: buffer.substring(0, 200),
|
|
387
|
+
});
|
|
388
|
+
throw error;
|
|
389
|
+
}
|
|
374
390
|
// Yield tool calls if any
|
|
375
391
|
if (hasToolCalls && toolCallsBuffer.length > 0) {
|
|
376
392
|
yield {
|
package/dist/api/responses.js
CHANGED
|
@@ -207,41 +207,57 @@ function convertToResponseInput(messages, includeBuiltinSystemPrompt = true) {
|
|
|
207
207
|
async function* parseSSEStream(reader) {
|
|
208
208
|
const decoder = new TextDecoder();
|
|
209
209
|
let buffer = '';
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
continue;
|
|
221
|
-
if (trimmed === 'data: [DONE]' || trimmed === 'data:[DONE]') {
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
// Handle both "event: " and "event:" formats
|
|
225
|
-
if (trimmed.startsWith('event:')) {
|
|
226
|
-
// Event type, will be followed by data
|
|
227
|
-
continue;
|
|
210
|
+
try {
|
|
211
|
+
while (true) {
|
|
212
|
+
const { done, value } = await reader.read();
|
|
213
|
+
if (done) {
|
|
214
|
+
// ✅ 关键修复:检查buffer是否有残留数据
|
|
215
|
+
if (buffer.trim()) {
|
|
216
|
+
// 连接异常中断,抛出明确错误
|
|
217
|
+
throw new Error(`Stream terminated unexpectedly with incomplete data: ${buffer.substring(0, 100)}...`);
|
|
218
|
+
}
|
|
219
|
+
break; // 正常结束
|
|
228
220
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
|
|
221
|
+
buffer += decoder.decode(value, { stream: true });
|
|
222
|
+
const lines = buffer.split('\n');
|
|
223
|
+
buffer = lines.pop() || '';
|
|
224
|
+
for (const line of lines) {
|
|
225
|
+
const trimmed = line.trim();
|
|
226
|
+
if (!trimmed || trimmed.startsWith(':'))
|
|
227
|
+
continue;
|
|
228
|
+
if (trimmed === 'data: [DONE]' || trimmed === 'data:[DONE]') {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
// Handle both "event: " and "event:" formats
|
|
232
|
+
if (trimmed.startsWith('event:')) {
|
|
233
|
+
// Event type, will be followed by data
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
// Handle both "data: " and "data:" formats
|
|
237
|
+
if (trimmed.startsWith('data:')) {
|
|
238
|
+
const data = trimmed.startsWith('data: ')
|
|
239
|
+
? trimmed.slice(6)
|
|
240
|
+
: trimmed.slice(5);
|
|
241
|
+
const parseResult = parseJsonWithFix(data, {
|
|
242
|
+
toolName: 'Responses API SSE stream',
|
|
243
|
+
logWarning: false,
|
|
244
|
+
logError: true,
|
|
245
|
+
});
|
|
246
|
+
if (parseResult.success) {
|
|
247
|
+
yield parseResult.data;
|
|
248
|
+
}
|
|
241
249
|
}
|
|
242
250
|
}
|
|
243
251
|
}
|
|
244
252
|
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
const { logger } = await import('../utils/logger.js');
|
|
255
|
+
logger.error('Responses API SSE stream parsing error:', {
|
|
256
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
257
|
+
remainingBuffer: buffer.substring(0, 200),
|
|
258
|
+
});
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
245
261
|
}
|
|
246
262
|
/**
|
|
247
263
|
* 使用 Responses API 创建流式响应(带自动工具调用)
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
export {};
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// CRITICAL: Patch cli-highlight BEFORE any other imports
|
|
3
|
-
// This must be the first import to ensure the patch is applied before cli-markdown loads
|
|
4
|
-
import './utils/patch-highlight.js';
|
|
5
2
|
import React from 'react';
|
|
6
3
|
import { render, Text, Box } from 'ink';
|
|
7
4
|
import Spinner from 'ink-spinner';
|
|
@@ -13,6 +10,7 @@ import { vscodeConnection } from './utils/vscodeConnection.js';
|
|
|
13
10
|
import { resourceMonitor } from './utils/resourceMonitor.js';
|
|
14
11
|
import { initializeProfiles } from './utils/configManager.js';
|
|
15
12
|
import { processManager } from './utils/processManager.js';
|
|
13
|
+
import { enableDevMode, getDevUserId } from './utils/devMode.js';
|
|
16
14
|
const execAsync = promisify(exec);
|
|
17
15
|
// Check for updates asynchronously
|
|
18
16
|
async function checkForUpdates(currentVersion) {
|
|
@@ -43,6 +41,7 @@ Options
|
|
|
43
41
|
--update Update to latest version
|
|
44
42
|
-c Skip welcome screen and resume last conversation
|
|
45
43
|
--ask Quick question mode (headless mode with single prompt)
|
|
44
|
+
--dev Enable developer mode with persistent userId for testing
|
|
46
45
|
`, {
|
|
47
46
|
importMeta: import.meta,
|
|
48
47
|
flags: {
|
|
@@ -57,6 +56,10 @@ Options
|
|
|
57
56
|
ask: {
|
|
58
57
|
type: 'string',
|
|
59
58
|
},
|
|
59
|
+
dev: {
|
|
60
|
+
type: 'boolean',
|
|
61
|
+
default: false,
|
|
62
|
+
},
|
|
60
63
|
},
|
|
61
64
|
});
|
|
62
65
|
// Handle update flag
|
|
@@ -72,6 +75,14 @@ if (cli.flags.update) {
|
|
|
72
75
|
process.exit(1);
|
|
73
76
|
}
|
|
74
77
|
}
|
|
78
|
+
// Handle dev mode flag
|
|
79
|
+
if (cli.flags.dev) {
|
|
80
|
+
enableDevMode();
|
|
81
|
+
const userId = getDevUserId();
|
|
82
|
+
console.log('🔧 Developer mode enabled');
|
|
83
|
+
console.log(`📝 Using persistent userId: ${userId}`);
|
|
84
|
+
console.log(`📂 Stored in: ~/.snow/dev-user-id\n`);
|
|
85
|
+
}
|
|
75
86
|
// Start resource monitoring in development/debug mode
|
|
76
87
|
if (process.env['NODE_ENV'] === 'development' || process.env['DEBUG']) {
|
|
77
88
|
resourceMonitor.startMonitoring(30000); // Monitor every 30 seconds
|
|
@@ -1041,9 +1041,12 @@ export async function handleConversationWithTools(options) {
|
|
|
1041
1041
|
freeEncoder();
|
|
1042
1042
|
}
|
|
1043
1043
|
catch (error) {
|
|
1044
|
-
freeEncoder();
|
|
1045
1044
|
throw error;
|
|
1046
1045
|
}
|
|
1046
|
+
finally {
|
|
1047
|
+
// ✅ 确保总是释放encoder资源,避免资源泄漏
|
|
1048
|
+
freeEncoder();
|
|
1049
|
+
}
|
|
1047
1050
|
// Return the accumulated usage data
|
|
1048
1051
|
return { usage: accumulatedUsage };
|
|
1049
1052
|
}
|
|
@@ -1,23 +1,70 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Text, Box } from 'ink';
|
|
3
|
-
|
|
4
|
-
import
|
|
3
|
+
import { marked } from 'marked';
|
|
4
|
+
import TerminalRenderer from 'marked-terminal';
|
|
5
5
|
import logger from '../../utils/logger.js';
|
|
6
|
+
// Configure marked to use terminal renderer
|
|
7
|
+
marked.setOptions({
|
|
8
|
+
// @ts-expect-error - marked-terminal types mismatch with marked v15
|
|
9
|
+
renderer: new TerminalRenderer({
|
|
10
|
+
reflowText: true,
|
|
11
|
+
width: 80,
|
|
12
|
+
emoji: false,
|
|
13
|
+
tab: 2,
|
|
14
|
+
}),
|
|
15
|
+
});
|
|
16
|
+
/**
|
|
17
|
+
* Sanitize markdown content to prevent number-to-alphabet errors
|
|
18
|
+
* Fixes invalid ordered list start attributes (0 or negative values)
|
|
19
|
+
*/
|
|
20
|
+
function sanitizeMarkdownContent(content) {
|
|
21
|
+
// Replace <ol start="0">, <ol start="-1">, etc. with <ol start="1">
|
|
22
|
+
return content.replace(/<ol\s+start=["']?(0|-\d+)["']?>/gi, '<ol start="1">');
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Fallback renderer for when cli-markdown fails
|
|
26
|
+
* Renders content as plain text to ensure visibility
|
|
27
|
+
*/
|
|
28
|
+
function renderFallback(content) {
|
|
29
|
+
const lines = content.split('\n');
|
|
30
|
+
return (React.createElement(Box, { flexDirection: "column" }, lines.map((line, index) => (React.createElement(Text, { key: index }, line)))));
|
|
31
|
+
}
|
|
6
32
|
export default function MarkdownRenderer({ content }) {
|
|
7
|
-
// Use
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
33
|
+
// Use marked + marked-terminal for elegant markdown rendering with syntax highlighting
|
|
34
|
+
// marked provides better stability and cross-platform support
|
|
35
|
+
try {
|
|
36
|
+
// Stage 1: Sanitize content to prevent invalid list numbering
|
|
37
|
+
const sanitizedContent = sanitizeMarkdownContent(content);
|
|
38
|
+
// Stage 2: Render with marked
|
|
39
|
+
const rendered = marked.parse(sanitizedContent);
|
|
40
|
+
// Split into lines and render each separately
|
|
41
|
+
// This prevents Ink's Text component from creating mysterious whitespace
|
|
42
|
+
// when handling multi-line content with \n characters
|
|
43
|
+
const lines = rendered.split('\n');
|
|
44
|
+
// Safety check: prevent rendering issues with excessively long output
|
|
45
|
+
if (lines.length > 500) {
|
|
46
|
+
logger.warn('[MarkdownRenderer] Rendered output has too many lines', {
|
|
47
|
+
totalLines: lines.length,
|
|
48
|
+
truncatedTo: 500,
|
|
49
|
+
});
|
|
50
|
+
return (React.createElement(Box, { flexDirection: "column" }, lines.slice(0, 500).map((line, index) => (React.createElement(Text, { key: index }, line)))));
|
|
51
|
+
}
|
|
52
|
+
return (React.createElement(Box, { flexDirection: "column" }, lines.map((line, index) => (React.createElement(Text, { key: index }, line)))));
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
// Stage 3: Error handling - catch number-to-alphabet errors
|
|
56
|
+
if (error?.message?.includes('Number must be >')) {
|
|
57
|
+
logger.warn('[MarkdownRenderer] Invalid list numbering detected, falling back to plain text', {
|
|
58
|
+
error: error.message,
|
|
59
|
+
});
|
|
60
|
+
return renderFallback(content);
|
|
61
|
+
}
|
|
62
|
+
// Re-throw other errors for debugging
|
|
63
|
+
logger.error('[MarkdownRenderer] Unexpected error during markdown rendering', {
|
|
64
|
+
error: error.message,
|
|
65
|
+
stack: error.stack,
|
|
19
66
|
});
|
|
20
|
-
|
|
67
|
+
// Still provide fallback to prevent crash
|
|
68
|
+
return renderFallback(content);
|
|
21
69
|
}
|
|
22
|
-
return (React.createElement(Box, { flexDirection: "column" }, lines.map((line, index) => (React.createElement(Text, { key: index }, line)))));
|
|
23
70
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get or create persistent dev userId
|
|
3
|
+
* The userId is stored in ~/.snow/dev-user-id and persists across sessions
|
|
4
|
+
*/
|
|
5
|
+
export declare function getDevUserId(): string;
|
|
6
|
+
/**
|
|
7
|
+
* Check if dev mode is enabled
|
|
8
|
+
*/
|
|
9
|
+
export declare function isDevMode(): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Enable dev mode by setting environment variable
|
|
12
|
+
*/
|
|
13
|
+
export declare function enableDevMode(): void;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'crypto';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
const SNOW_DIR = join(homedir(), '.snow');
|
|
6
|
+
const DEV_USER_ID_FILE = join(SNOW_DIR, 'dev-user-id');
|
|
7
|
+
/**
|
|
8
|
+
* Ensure .snow directory exists
|
|
9
|
+
*/
|
|
10
|
+
function ensureSnowDir() {
|
|
11
|
+
if (!existsSync(SNOW_DIR)) {
|
|
12
|
+
mkdirSync(SNOW_DIR, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Generate a persistent dev userId following Anthropic's format
|
|
17
|
+
* Format: user_<hash>_account__session_<uuid>
|
|
18
|
+
*/
|
|
19
|
+
function generateDevUserId() {
|
|
20
|
+
const sessionId = randomUUID();
|
|
21
|
+
const hash = createHash('sha256')
|
|
22
|
+
.update(`anthropic_dev_user_${sessionId}`)
|
|
23
|
+
.digest('hex');
|
|
24
|
+
return `user_${hash}_account__session_${sessionId}`;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Get or create persistent dev userId
|
|
28
|
+
* The userId is stored in ~/.snow/dev-user-id and persists across sessions
|
|
29
|
+
*/
|
|
30
|
+
export function getDevUserId() {
|
|
31
|
+
ensureSnowDir();
|
|
32
|
+
if (existsSync(DEV_USER_ID_FILE)) {
|
|
33
|
+
const userId = readFileSync(DEV_USER_ID_FILE, 'utf-8').trim();
|
|
34
|
+
if (userId) {
|
|
35
|
+
return userId;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Generate new userId if file doesn't exist or is empty
|
|
39
|
+
const userId = generateDevUserId();
|
|
40
|
+
writeFileSync(DEV_USER_ID_FILE, userId, 'utf-8');
|
|
41
|
+
return userId;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Check if dev mode is enabled
|
|
45
|
+
*/
|
|
46
|
+
export function isDevMode() {
|
|
47
|
+
return process.env['SNOW_DEV_MODE'] === 'true';
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Enable dev mode by setting environment variable
|
|
51
|
+
*/
|
|
52
|
+
export function enableDevMode() {
|
|
53
|
+
process.env['SNOW_DEV_MODE'] = 'true';
|
|
54
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "snow-ai",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
4
4
|
"description": "Intelligent Command Line Assistant powered by AI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
@@ -44,8 +44,9 @@
|
|
|
44
44
|
"@types/better-sqlite3": "^7.6.13",
|
|
45
45
|
"better-sqlite3": "^12.4.1",
|
|
46
46
|
"cli-highlight": "^2.1.11",
|
|
47
|
-
"cli-markdown": "^3.5.1",
|
|
48
47
|
"diff": "^8.0.2",
|
|
48
|
+
"marked": "^15.0.6",
|
|
49
|
+
"marked-terminal": "^7.3.0",
|
|
49
50
|
"fzf": "^0.5.2",
|
|
50
51
|
"http-proxy-agent": "^7.0.2",
|
|
51
52
|
"https-proxy-agent": "^7.0.6",
|
|
@@ -66,6 +67,7 @@
|
|
|
66
67
|
"devDependencies": {
|
|
67
68
|
"@sindresorhus/tsconfig": "^3.0.1",
|
|
68
69
|
"@types/diff": "^7.0.2",
|
|
70
|
+
"@types/marked-terminal": "^6.1.1",
|
|
69
71
|
"@types/prettier": "^2.7.3",
|
|
70
72
|
"@types/react": "^18.0.32",
|
|
71
73
|
"@types/ws": "^8.5.8",
|