snow-ai 0.3.0 → 0.3.2
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 +13 -9
- package/dist/api/anthropic.js +77 -34
- package/dist/api/chat.d.ts +14 -29
- package/dist/api/chat.js +62 -19
- package/dist/api/gemini.d.ts +1 -10
- package/dist/api/gemini.js +104 -82
- package/dist/api/models.js +6 -7
- package/dist/api/responses.d.ts +2 -17
- package/dist/api/responses.js +59 -17
- package/dist/api/types.d.ts +39 -0
- package/dist/api/types.js +4 -0
- package/dist/ui/pages/ConfigScreen.js +67 -49
- package/dist/ui/pages/WelcomeScreen.js +1 -1
- package/dist/utils/contextCompressor.d.ts +1 -1
- package/dist/utils/contextCompressor.js +193 -81
- package/package.json +1 -4
|
@@ -1,20 +1,45 @@
|
|
|
1
|
-
import OpenAI from 'openai';
|
|
2
|
-
import { GoogleGenAI } from '@google/genai';
|
|
3
|
-
import Anthropic from '@anthropic-ai/sdk';
|
|
4
1
|
import { getOpenAiConfig, getCustomHeaders, getCustomSystemPrompt } from './apiConfig.js';
|
|
5
2
|
import { SYSTEM_PROMPT } from '../api/systemPrompt.js';
|
|
6
3
|
/**
|
|
7
4
|
* Compression request prompt - asks AI to summarize conversation with focus on task continuity
|
|
8
5
|
*/
|
|
9
6
|
const COMPRESSION_PROMPT = 'Please provide a concise summary of our conversation so far. Focus on: 1) The current task or goal we are working on, 2) Key decisions and approaches we have agreed upon, 3) Important context needed to continue, 4) Any pending or unfinished work. Keep it brief but ensure I can seamlessly continue assisting with the task.';
|
|
7
|
+
/**
|
|
8
|
+
* Parse Server-Sent Events (SSE) stream
|
|
9
|
+
*/
|
|
10
|
+
async function* parseSSEStream(reader) {
|
|
11
|
+
const decoder = new TextDecoder();
|
|
12
|
+
let buffer = '';
|
|
13
|
+
while (true) {
|
|
14
|
+
const { done, value } = await reader.read();
|
|
15
|
+
if (done)
|
|
16
|
+
break;
|
|
17
|
+
buffer += decoder.decode(value, { stream: true });
|
|
18
|
+
const lines = buffer.split('\n');
|
|
19
|
+
buffer = lines.pop() || '';
|
|
20
|
+
for (const line of lines) {
|
|
21
|
+
const trimmed = line.trim();
|
|
22
|
+
if (!trimmed || trimmed.startsWith(':'))
|
|
23
|
+
continue;
|
|
24
|
+
if (trimmed === 'data: [DONE]') {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (trimmed.startsWith('data: ')) {
|
|
28
|
+
const data = trimmed.slice(6);
|
|
29
|
+
try {
|
|
30
|
+
yield JSON.parse(data);
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
console.error('Failed to parse SSE data:', data);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
10
39
|
/**
|
|
11
40
|
* Compress context using OpenAI Chat Completions API
|
|
12
41
|
*/
|
|
13
42
|
async function compressWithChatCompletions(baseUrl, apiKey, modelName, conversationMessages, systemPrompt) {
|
|
14
|
-
const client = new OpenAI({
|
|
15
|
-
apiKey,
|
|
16
|
-
baseURL: baseUrl,
|
|
17
|
-
});
|
|
18
43
|
const customHeaders = getCustomHeaders();
|
|
19
44
|
// Build messages with system prompt support
|
|
20
45
|
const messages = [];
|
|
@@ -48,17 +73,29 @@ async function compressWithChatCompletions(baseUrl, apiKey, modelName, conversat
|
|
|
48
73
|
stream: true,
|
|
49
74
|
stream_options: { include_usage: true },
|
|
50
75
|
};
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
headers:
|
|
54
|
-
|
|
76
|
+
const response = await fetch(`${baseUrl}/chat/completions`, {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
headers: {
|
|
79
|
+
'Content-Type': 'application/json',
|
|
80
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
81
|
+
...customHeaders
|
|
82
|
+
},
|
|
83
|
+
body: JSON.stringify(requestPayload)
|
|
84
|
+
});
|
|
85
|
+
if (!response.ok) {
|
|
86
|
+
const errorText = await response.text();
|
|
87
|
+
throw new Error(`OpenAI API error: ${response.status} ${response.statusText} - ${errorText}`);
|
|
88
|
+
}
|
|
89
|
+
if (!response.body) {
|
|
90
|
+
throw new Error('No response body from OpenAI API');
|
|
91
|
+
}
|
|
55
92
|
let summary = '';
|
|
56
93
|
let usage = {
|
|
57
94
|
prompt_tokens: 0,
|
|
58
95
|
completion_tokens: 0,
|
|
59
96
|
total_tokens: 0,
|
|
60
97
|
};
|
|
61
|
-
for await (const chunk of
|
|
98
|
+
for await (const chunk of parseSSEStream(response.body.getReader())) {
|
|
62
99
|
const delta = chunk.choices[0]?.delta;
|
|
63
100
|
if (delta?.content) {
|
|
64
101
|
summary += delta.content;
|
|
@@ -84,10 +121,6 @@ async function compressWithChatCompletions(baseUrl, apiKey, modelName, conversat
|
|
|
84
121
|
* Compress context using OpenAI Responses API
|
|
85
122
|
*/
|
|
86
123
|
async function compressWithResponses(baseUrl, apiKey, modelName, conversationMessages, systemPrompt) {
|
|
87
|
-
const client = new OpenAI({
|
|
88
|
-
apiKey,
|
|
89
|
-
baseURL: baseUrl,
|
|
90
|
-
});
|
|
91
124
|
const customHeaders = getCustomHeaders();
|
|
92
125
|
// Build instructions
|
|
93
126
|
const instructions = systemPrompt || SYSTEM_PROMPT;
|
|
@@ -130,17 +163,29 @@ async function compressWithResponses(baseUrl, apiKey, modelName, conversationMes
|
|
|
130
163
|
input,
|
|
131
164
|
stream: true,
|
|
132
165
|
};
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
headers:
|
|
166
|
+
const response = await fetch(`${baseUrl}/responses`, {
|
|
167
|
+
method: 'POST',
|
|
168
|
+
headers: {
|
|
169
|
+
'Content-Type': 'application/json',
|
|
170
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
171
|
+
...customHeaders
|
|
172
|
+
},
|
|
173
|
+
body: JSON.stringify(requestPayload)
|
|
136
174
|
});
|
|
175
|
+
if (!response.ok) {
|
|
176
|
+
const errorText = await response.text();
|
|
177
|
+
throw new Error(`OpenAI Responses API error: ${response.status} ${response.statusText} - ${errorText}`);
|
|
178
|
+
}
|
|
179
|
+
if (!response.body) {
|
|
180
|
+
throw new Error('No response body from OpenAI Responses API');
|
|
181
|
+
}
|
|
137
182
|
let summary = '';
|
|
138
183
|
let usage = {
|
|
139
184
|
prompt_tokens: 0,
|
|
140
185
|
completion_tokens: 0,
|
|
141
186
|
total_tokens: 0,
|
|
142
187
|
};
|
|
143
|
-
for await (const chunk of
|
|
188
|
+
for await (const chunk of parseSSEStream(response.body.getReader())) {
|
|
144
189
|
const eventType = chunk.type;
|
|
145
190
|
// Handle text content delta
|
|
146
191
|
if (eventType === 'response.output_text.delta') {
|
|
@@ -150,7 +195,7 @@ async function compressWithResponses(baseUrl, apiKey, modelName, conversationMes
|
|
|
150
195
|
}
|
|
151
196
|
}
|
|
152
197
|
// Handle usage info
|
|
153
|
-
if (eventType === 'response.
|
|
198
|
+
if (eventType === 'response.completed') {
|
|
154
199
|
const response = chunk.response;
|
|
155
200
|
if (response?.usage) {
|
|
156
201
|
usage = {
|
|
@@ -173,26 +218,7 @@ async function compressWithResponses(baseUrl, apiKey, modelName, conversationMes
|
|
|
173
218
|
* Compress context using Gemini API
|
|
174
219
|
*/
|
|
175
220
|
async function compressWithGemini(baseUrl, apiKey, modelName, conversationMessages, systemPrompt) {
|
|
176
|
-
const clientConfig = {
|
|
177
|
-
apiKey,
|
|
178
|
-
};
|
|
179
221
|
const customHeaders = getCustomHeaders();
|
|
180
|
-
// Support custom baseUrl and headers for proxy servers
|
|
181
|
-
if (baseUrl && baseUrl !== 'https://api.openai.com/v1') {
|
|
182
|
-
clientConfig.httpOptions = {
|
|
183
|
-
baseUrl,
|
|
184
|
-
headers: {
|
|
185
|
-
'x-goog-api-key': apiKey,
|
|
186
|
-
...customHeaders,
|
|
187
|
-
},
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
else if (Object.keys(customHeaders).length > 0) {
|
|
191
|
-
clientConfig.httpOptions = {
|
|
192
|
-
headers: customHeaders,
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
const client = new GoogleGenAI(clientConfig);
|
|
196
222
|
// Build system instruction
|
|
197
223
|
const systemInstruction = systemPrompt || SYSTEM_PROMPT;
|
|
198
224
|
// Build contents array with conversation history
|
|
@@ -220,30 +246,81 @@ async function compressWithGemini(baseUrl, apiKey, modelName, conversationMessag
|
|
|
220
246
|
text: COMPRESSION_PROMPT,
|
|
221
247
|
}],
|
|
222
248
|
});
|
|
223
|
-
const
|
|
224
|
-
model: modelName,
|
|
225
|
-
systemInstruction,
|
|
249
|
+
const requestBody = {
|
|
226
250
|
contents,
|
|
251
|
+
systemInstruction: systemInstruction ? { parts: [{ text: systemInstruction }] } : undefined,
|
|
227
252
|
};
|
|
228
|
-
//
|
|
229
|
-
const
|
|
253
|
+
// Extract model name
|
|
254
|
+
const effectiveBaseUrl = baseUrl && baseUrl !== 'https://api.openai.com/v1'
|
|
255
|
+
? baseUrl
|
|
256
|
+
: 'https://generativelanguage.googleapis.com/v1beta';
|
|
257
|
+
const model = modelName.startsWith('models/') ? modelName : `models/${modelName}`;
|
|
258
|
+
const url = `${effectiveBaseUrl}/${model}:streamGenerateContent?key=${apiKey}&alt=sse`;
|
|
259
|
+
const response = await fetch(url, {
|
|
260
|
+
method: 'POST',
|
|
261
|
+
headers: {
|
|
262
|
+
'Content-Type': 'application/json',
|
|
263
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
264
|
+
...customHeaders
|
|
265
|
+
},
|
|
266
|
+
body: JSON.stringify(requestBody)
|
|
267
|
+
});
|
|
268
|
+
if (!response.ok) {
|
|
269
|
+
const errorText = await response.text();
|
|
270
|
+
throw new Error(`Gemini API error: ${response.status} ${response.statusText} - ${errorText}`);
|
|
271
|
+
}
|
|
272
|
+
if (!response.body) {
|
|
273
|
+
throw new Error('No response body from Gemini API');
|
|
274
|
+
}
|
|
230
275
|
let summary = '';
|
|
231
276
|
let usage = {
|
|
232
277
|
prompt_tokens: 0,
|
|
233
278
|
completion_tokens: 0,
|
|
234
279
|
total_tokens: 0,
|
|
235
280
|
};
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
281
|
+
// Parse SSE stream
|
|
282
|
+
const reader = response.body.getReader();
|
|
283
|
+
const decoder = new TextDecoder();
|
|
284
|
+
let buffer = '';
|
|
285
|
+
while (true) {
|
|
286
|
+
const { done, value } = await reader.read();
|
|
287
|
+
if (done)
|
|
288
|
+
break;
|
|
289
|
+
buffer += decoder.decode(value, { stream: true });
|
|
290
|
+
const lines = buffer.split('\n');
|
|
291
|
+
buffer = lines.pop() || '';
|
|
292
|
+
for (const line of lines) {
|
|
293
|
+
const trimmed = line.trim();
|
|
294
|
+
if (!trimmed || trimmed.startsWith(':'))
|
|
295
|
+
continue;
|
|
296
|
+
if (trimmed.startsWith('data: ')) {
|
|
297
|
+
const data = trimmed.slice(6);
|
|
298
|
+
try {
|
|
299
|
+
const chunk = JSON.parse(data);
|
|
300
|
+
// Process candidates
|
|
301
|
+
if (chunk.candidates && chunk.candidates.length > 0) {
|
|
302
|
+
const candidate = chunk.candidates[0];
|
|
303
|
+
if (candidate.content && candidate.content.parts) {
|
|
304
|
+
for (const part of candidate.content.parts) {
|
|
305
|
+
if (part.text) {
|
|
306
|
+
summary += part.text;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
// Collect usage info
|
|
312
|
+
if (chunk.usageMetadata) {
|
|
313
|
+
usage = {
|
|
314
|
+
prompt_tokens: chunk.usageMetadata.promptTokenCount || 0,
|
|
315
|
+
completion_tokens: chunk.usageMetadata.candidatesTokenCount || 0,
|
|
316
|
+
total_tokens: chunk.usageMetadata.totalTokenCount || 0,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
catch (e) {
|
|
321
|
+
console.error('Failed to parse Gemini SSE data:', data);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
247
324
|
}
|
|
248
325
|
}
|
|
249
326
|
if (!summary) {
|
|
@@ -258,18 +335,7 @@ async function compressWithGemini(baseUrl, apiKey, modelName, conversationMessag
|
|
|
258
335
|
* Compress context using Anthropic API
|
|
259
336
|
*/
|
|
260
337
|
async function compressWithAnthropic(baseUrl, apiKey, modelName, conversationMessages, systemPrompt) {
|
|
261
|
-
const clientConfig = {
|
|
262
|
-
apiKey,
|
|
263
|
-
};
|
|
264
|
-
if (baseUrl && baseUrl !== 'https://api.openai.com/v1') {
|
|
265
|
-
clientConfig.baseURL = baseUrl;
|
|
266
|
-
}
|
|
267
338
|
const customHeaders = getCustomHeaders();
|
|
268
|
-
clientConfig.defaultHeaders = {
|
|
269
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
270
|
-
...customHeaders,
|
|
271
|
-
};
|
|
272
|
-
const client = new Anthropic(clientConfig);
|
|
273
339
|
// Build messages array with conversation history
|
|
274
340
|
const messages = [];
|
|
275
341
|
// If custom system prompt exists, add default as first user message
|
|
@@ -298,27 +364,73 @@ async function compressWithAnthropic(baseUrl, apiKey, modelName, conversationMes
|
|
|
298
364
|
max_tokens: 4096,
|
|
299
365
|
system: systemParam,
|
|
300
366
|
messages,
|
|
367
|
+
stream: true
|
|
301
368
|
};
|
|
302
|
-
|
|
303
|
-
|
|
369
|
+
const effectiveBaseUrl = baseUrl && baseUrl !== 'https://api.openai.com/v1'
|
|
370
|
+
? baseUrl
|
|
371
|
+
: 'https://api.anthropic.com/v1';
|
|
372
|
+
const response = await fetch(`${effectiveBaseUrl}/messages`, {
|
|
373
|
+
method: 'POST',
|
|
374
|
+
headers: {
|
|
375
|
+
'Content-Type': 'application/json',
|
|
376
|
+
'x-api-key': apiKey,
|
|
377
|
+
'authorization': `Bearer ${apiKey}`,
|
|
378
|
+
...customHeaders
|
|
379
|
+
},
|
|
380
|
+
body: JSON.stringify(requestPayload)
|
|
381
|
+
});
|
|
382
|
+
if (!response.ok) {
|
|
383
|
+
const errorText = await response.text();
|
|
384
|
+
throw new Error(`Anthropic API error: ${response.status} ${response.statusText} - ${errorText}`);
|
|
385
|
+
}
|
|
386
|
+
if (!response.body) {
|
|
387
|
+
throw new Error('No response body from Anthropic API');
|
|
388
|
+
}
|
|
304
389
|
let summary = '';
|
|
305
390
|
let usage = {
|
|
306
391
|
prompt_tokens: 0,
|
|
307
392
|
completion_tokens: 0,
|
|
308
393
|
total_tokens: 0,
|
|
309
394
|
};
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
395
|
+
// Parse Anthropic SSE stream
|
|
396
|
+
const reader = response.body.getReader();
|
|
397
|
+
const decoder = new TextDecoder();
|
|
398
|
+
let buffer = '';
|
|
399
|
+
while (true) {
|
|
400
|
+
const { done, value } = await reader.read();
|
|
401
|
+
if (done)
|
|
402
|
+
break;
|
|
403
|
+
buffer += decoder.decode(value, { stream: true });
|
|
404
|
+
const lines = buffer.split('\n');
|
|
405
|
+
buffer = lines.pop() || '';
|
|
406
|
+
for (const line of lines) {
|
|
407
|
+
const trimmed = line.trim();
|
|
408
|
+
if (!trimmed || trimmed.startsWith(':'))
|
|
409
|
+
continue;
|
|
410
|
+
if (trimmed.startsWith('event: ')) {
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
if (trimmed.startsWith('data: ')) {
|
|
414
|
+
const data = trimmed.slice(6);
|
|
415
|
+
try {
|
|
416
|
+
const event = JSON.parse(data);
|
|
417
|
+
if (event.type === 'content_block_delta' && event.delta?.type === 'text_delta') {
|
|
418
|
+
summary += event.delta.text;
|
|
419
|
+
}
|
|
420
|
+
// Collect usage info from message_start event
|
|
421
|
+
if (event.type === 'message_start' && event.message?.usage) {
|
|
422
|
+
usage.prompt_tokens = event.message.usage.input_tokens || 0;
|
|
423
|
+
}
|
|
424
|
+
// Collect usage info from message_delta event
|
|
425
|
+
if (event.type === 'message_delta' && event.usage) {
|
|
426
|
+
usage.completion_tokens = event.usage.output_tokens || 0;
|
|
427
|
+
usage.total_tokens = usage.prompt_tokens + usage.completion_tokens;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
catch (e) {
|
|
431
|
+
console.error('Failed to parse Anthropic SSE data:', data);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
322
434
|
}
|
|
323
435
|
}
|
|
324
436
|
if (!summary) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "snow-ai",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Intelligent Command Line Assistant powered by AI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
@@ -39,8 +39,6 @@
|
|
|
39
39
|
"dist"
|
|
40
40
|
],
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@anthropic-ai/sdk": "^0.65.0",
|
|
43
|
-
"@google/genai": "^1.23.0",
|
|
44
42
|
"@inkjs/ui": "^2.0.0",
|
|
45
43
|
"@modelcontextprotocol/sdk": "^1.17.3",
|
|
46
44
|
"chalk-template": "^1.1.2",
|
|
@@ -57,7 +55,6 @@
|
|
|
57
55
|
"ink-text-input": "^6.0.0",
|
|
58
56
|
"ink-tree-select": "^2.3.1",
|
|
59
57
|
"meow": "^11.0.0",
|
|
60
|
-
"openai": "^6.1.0",
|
|
61
58
|
"puppeteer-core": "^24.25.0",
|
|
62
59
|
"react": "^18.2.0",
|
|
63
60
|
"string-width": "^7.2.0",
|