@krishivpb60/aether-ai-cli 1.1.0 → 1.1.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/package.json +1 -1
- package/src/ai/router.js +6 -6
- package/src/ai/universal.js +49 -7
- package/src/chat.js +3 -3
- package/src/cli.js +2 -2
- package/test/router.test.js +50 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@krishivpb60/aether-ai-cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "Aether Core AI — A cyberpunk command-line AI assistant with multi-mode reasoning, 12-node failover mesh, file context injection, and offline fallbacks.",
|
|
5
5
|
"main": "src/cli.js",
|
|
6
6
|
"bin": {
|
package/src/ai/router.js
CHANGED
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
* @param {object} config - Flat config object with all API keys
|
|
27
27
|
* @returns {Promise<{ text: string, provider: string, model?: string, node: number, type?: string }>}
|
|
28
28
|
*/
|
|
29
|
-
export async function routePrompt(prompt, systemPrompt, config, onToken) {
|
|
29
|
+
export async function routePrompt(prompt, systemPrompt, config, onToken, history = []) {
|
|
30
30
|
// ── Node 0: Local Math Solver ───────────────────────────
|
|
31
31
|
const mathExpr = detectMathExpression(prompt);
|
|
32
32
|
if (mathExpr) {
|
|
@@ -71,20 +71,20 @@ export async function routePrompt(prompt, systemPrompt, config, onToken) {
|
|
|
71
71
|
result = await callOpenAICompatible(
|
|
72
72
|
prompt, systemPrompt, apiKey,
|
|
73
73
|
provider.baseUrl, model, provider.name,
|
|
74
|
-
onToken
|
|
74
|
+
onToken, history
|
|
75
75
|
);
|
|
76
76
|
break;
|
|
77
77
|
|
|
78
78
|
case "custom-google":
|
|
79
|
-
result = await callGoogleGemini(prompt, systemPrompt, apiKey, model, onToken);
|
|
79
|
+
result = await callGoogleGemini(prompt, systemPrompt, apiKey, model, onToken, history);
|
|
80
80
|
break;
|
|
81
81
|
|
|
82
82
|
case "custom-anthropic":
|
|
83
|
-
result = await callAnthropic(prompt, systemPrompt, apiKey, model, onToken);
|
|
83
|
+
result = await callAnthropic(prompt, systemPrompt, apiKey, model, onToken, history);
|
|
84
84
|
break;
|
|
85
85
|
|
|
86
86
|
case "custom-cohere":
|
|
87
|
-
result = await callCohere(prompt, systemPrompt, apiKey, model, onToken);
|
|
87
|
+
result = await callCohere(prompt, systemPrompt, apiKey, model, onToken, history);
|
|
88
88
|
break;
|
|
89
89
|
|
|
90
90
|
default:
|
|
@@ -92,7 +92,7 @@ export async function routePrompt(prompt, systemPrompt, config, onToken) {
|
|
|
92
92
|
result = await callOpenAICompatible(
|
|
93
93
|
prompt, systemPrompt, apiKey,
|
|
94
94
|
provider.baseUrl, model, provider.name,
|
|
95
|
-
onToken
|
|
95
|
+
onToken, history
|
|
96
96
|
);
|
|
97
97
|
}
|
|
98
98
|
|
package/src/ai/universal.js
CHANGED
|
@@ -13,12 +13,17 @@
|
|
|
13
13
|
* @param {string} providerName - For error messages
|
|
14
14
|
* @returns {Promise<{ text: string, provider: string, model: string }>}
|
|
15
15
|
*/
|
|
16
|
-
export async function callOpenAICompatible(prompt, systemPrompt, apiKey, baseUrl, model, providerName, onToken) {
|
|
16
|
+
export async function callOpenAICompatible(prompt, systemPrompt, apiKey, baseUrl, model, providerName, onToken, history = []) {
|
|
17
17
|
const isStreaming = typeof onToken === "function";
|
|
18
|
+
const formattedHistory = history.map(h => ({
|
|
19
|
+
role: h.role === "assistant" ? "assistant" : "user",
|
|
20
|
+
content: h.content
|
|
21
|
+
}));
|
|
18
22
|
const body = {
|
|
19
23
|
model,
|
|
20
24
|
messages: [
|
|
21
25
|
{ role: "system", content: systemPrompt },
|
|
26
|
+
...formattedHistory,
|
|
22
27
|
{ role: "user", content: prompt },
|
|
23
28
|
],
|
|
24
29
|
temperature: 0.7,
|
|
@@ -116,9 +121,10 @@ export async function callOpenAICompatible(prompt, systemPrompt, apiKey, baseUrl
|
|
|
116
121
|
* @param {string} model - Model name
|
|
117
122
|
* @returns {Promise<{ text: string, provider: string, model: string }>}
|
|
118
123
|
*/
|
|
119
|
-
export async function callGoogleGemini(prompt, systemPrompt, apiKey, model = "gemini-2.5-flash", onToken) {
|
|
124
|
+
export async function callGoogleGemini(prompt, systemPrompt, apiKey, model = "gemini-2.5-flash", onToken, history = []) {
|
|
120
125
|
const BASE = "https://generativelanguage.googleapis.com/v1beta/models";
|
|
121
126
|
const isStreaming = typeof onToken === "function";
|
|
127
|
+
let currentHistory = [...history];
|
|
122
128
|
|
|
123
129
|
if (isStreaming) {
|
|
124
130
|
let fullText = "";
|
|
@@ -128,9 +134,16 @@ export async function callGoogleGemini(prompt, systemPrompt, apiKey, model = "ge
|
|
|
128
134
|
|
|
129
135
|
while (continuations <= MAX) {
|
|
130
136
|
const url = `${BASE}/${model}:streamGenerateContent?key=${apiKey}`;
|
|
137
|
+
const formattedHistory = currentHistory.map(h => ({
|
|
138
|
+
role: h.role === "assistant" ? "model" : "user",
|
|
139
|
+
parts: [{ text: h.content }]
|
|
140
|
+
}));
|
|
131
141
|
const body = {
|
|
132
142
|
systemInstruction: { parts: [{ text: systemPrompt }] },
|
|
133
|
-
contents: [
|
|
143
|
+
contents: [
|
|
144
|
+
...formattedHistory,
|
|
145
|
+
{ role: "user", parts: [{ text: currentPrompt }] }
|
|
146
|
+
],
|
|
134
147
|
generationConfig: { temperature: 0.7, maxOutputTokens: 8192 },
|
|
135
148
|
};
|
|
136
149
|
|
|
@@ -145,6 +158,8 @@ export async function callGoogleGemini(prompt, systemPrompt, apiKey, model = "ge
|
|
|
145
158
|
throw new Error(`Gemini API error (${response.status}): ${response.statusText}. ${errorBody}`);
|
|
146
159
|
}
|
|
147
160
|
|
|
161
|
+
let streamedTextInThisTurn = "";
|
|
162
|
+
|
|
148
163
|
if (!response.body || typeof response.body.getReader !== "function") {
|
|
149
164
|
// Fallback to non-streaming if response body is not streamable (e.g. in unit tests)
|
|
150
165
|
const data = await response.json();
|
|
@@ -164,9 +179,12 @@ export async function callGoogleGemini(prompt, systemPrompt, apiKey, model = "ge
|
|
|
164
179
|
if (chunkText) {
|
|
165
180
|
onToken(chunkText);
|
|
166
181
|
fullText += chunkText;
|
|
182
|
+
streamedTextInThisTurn += chunkText;
|
|
167
183
|
}
|
|
168
184
|
|
|
169
185
|
if (finishReason === "MAX_TOKENS" && continuations < MAX) {
|
|
186
|
+
currentHistory.push({ role: "user", content: currentPrompt });
|
|
187
|
+
currentHistory.push({ role: "assistant", content: streamedTextInThisTurn });
|
|
170
188
|
continuations++;
|
|
171
189
|
currentPrompt = "Continue your previous response from exactly where you left off.";
|
|
172
190
|
} else {
|
|
@@ -217,6 +235,7 @@ export async function callGoogleGemini(prompt, systemPrompt, apiKey, model = "ge
|
|
|
217
235
|
if (text) {
|
|
218
236
|
onToken(text);
|
|
219
237
|
fullText += text;
|
|
238
|
+
streamedTextInThisTurn += text;
|
|
220
239
|
}
|
|
221
240
|
} catch (e) {
|
|
222
241
|
// Ignore parse errors
|
|
@@ -231,6 +250,8 @@ export async function callGoogleGemini(prompt, systemPrompt, apiKey, model = "ge
|
|
|
231
250
|
}
|
|
232
251
|
|
|
233
252
|
if (finishReason === "MAX_TOKENS" && continuations < MAX) {
|
|
253
|
+
currentHistory.push({ role: "user", content: currentPrompt });
|
|
254
|
+
currentHistory.push({ role: "assistant", content: streamedTextInThisTurn });
|
|
234
255
|
continuations++;
|
|
235
256
|
currentPrompt = "Continue your previous response from exactly where you left off.";
|
|
236
257
|
} else {
|
|
@@ -249,9 +270,16 @@ export async function callGoogleGemini(prompt, systemPrompt, apiKey, model = "ge
|
|
|
249
270
|
|
|
250
271
|
while (continuations <= MAX) {
|
|
251
272
|
const url = `${BASE}/${model}:generateContent?key=${apiKey}`;
|
|
273
|
+
const formattedHistory = currentHistory.map(h => ({
|
|
274
|
+
role: h.role === "assistant" ? "model" : "user",
|
|
275
|
+
parts: [{ text: h.content }]
|
|
276
|
+
}));
|
|
252
277
|
const body = {
|
|
253
278
|
systemInstruction: { parts: [{ text: systemPrompt }] },
|
|
254
|
-
contents: [
|
|
279
|
+
contents: [
|
|
280
|
+
...formattedHistory,
|
|
281
|
+
{ role: "user", parts: [{ text: currentPrompt }] }
|
|
282
|
+
],
|
|
255
283
|
generationConfig: { temperature: 0.7, maxOutputTokens: 8192 },
|
|
256
284
|
};
|
|
257
285
|
|
|
@@ -278,6 +306,8 @@ export async function callGoogleGemini(prompt, systemPrompt, apiKey, model = "ge
|
|
|
278
306
|
fullText += chunkText;
|
|
279
307
|
|
|
280
308
|
if (candidate.finishReason === "MAX_TOKENS" && continuations < MAX) {
|
|
309
|
+
currentHistory.push({ role: "user", content: currentPrompt });
|
|
310
|
+
currentHistory.push({ role: "assistant", content: chunkText });
|
|
281
311
|
continuations++;
|
|
282
312
|
currentPrompt = "Continue your previous response from exactly where you left off.";
|
|
283
313
|
} else {
|
|
@@ -298,14 +328,21 @@ export async function callGoogleGemini(prompt, systemPrompt, apiKey, model = "ge
|
|
|
298
328
|
* @param {string} model - Model name
|
|
299
329
|
* @returns {Promise<{ text: string, provider: string, model: string }>}
|
|
300
330
|
*/
|
|
301
|
-
export async function callAnthropic(prompt, systemPrompt, apiKey, model = "claude-sonnet-4-20250514", onToken) {
|
|
331
|
+
export async function callAnthropic(prompt, systemPrompt, apiKey, model = "claude-sonnet-4-20250514", onToken, history = []) {
|
|
302
332
|
const url = "https://api.anthropic.com/v1/messages";
|
|
303
333
|
const isStreaming = typeof onToken === "function";
|
|
334
|
+
const formattedHistory = history.map(h => ({
|
|
335
|
+
role: h.role === "assistant" ? "assistant" : "user",
|
|
336
|
+
content: h.content
|
|
337
|
+
}));
|
|
304
338
|
const body = {
|
|
305
339
|
model,
|
|
306
340
|
max_tokens: 4096,
|
|
307
341
|
system: systemPrompt,
|
|
308
|
-
messages: [
|
|
342
|
+
messages: [
|
|
343
|
+
...formattedHistory,
|
|
344
|
+
{ role: "user", content: prompt }
|
|
345
|
+
],
|
|
309
346
|
...(isStreaming ? { stream: true } : {}),
|
|
310
347
|
};
|
|
311
348
|
|
|
@@ -384,13 +421,18 @@ export async function callAnthropic(prompt, systemPrompt, apiKey, model = "claud
|
|
|
384
421
|
* @param {string} model - Model name
|
|
385
422
|
* @returns {Promise<{ text: string, provider: string, model: string }>}
|
|
386
423
|
*/
|
|
387
|
-
export async function callCohere(prompt, systemPrompt, apiKey, model = "command-r-plus", onToken) {
|
|
424
|
+
export async function callCohere(prompt, systemPrompt, apiKey, model = "command-r-plus", onToken, history = []) {
|
|
388
425
|
const url = "https://api.cohere.com/v2/chat";
|
|
389
426
|
const isStreaming = typeof onToken === "function";
|
|
427
|
+
const formattedHistory = history.map(h => ({
|
|
428
|
+
role: h.role === "assistant" ? "assistant" : "user",
|
|
429
|
+
content: h.content
|
|
430
|
+
}));
|
|
390
431
|
const body = {
|
|
391
432
|
model,
|
|
392
433
|
messages: [
|
|
393
434
|
{ role: "system", content: systemPrompt },
|
|
435
|
+
...formattedHistory,
|
|
394
436
|
{ role: "user", content: prompt },
|
|
395
437
|
],
|
|
396
438
|
...(isStreaming ? { stream: true } : {}),
|
package/src/chat.js
CHANGED
|
@@ -46,8 +46,8 @@ const getMarked = () => new Marked(markedTerminal({
|
|
|
46
46
|
showSectionPrefix: false,
|
|
47
47
|
code: (c) => colors.orange(c),
|
|
48
48
|
codespan: (c) => colors.accent3(c),
|
|
49
|
-
heading: (h) => colors.accent(h)
|
|
50
|
-
strong: (s) => colors.magenta(s)
|
|
49
|
+
heading: (h) => colors.accent.bold(h),
|
|
50
|
+
strong: (s) => colors.magenta.bold(s),
|
|
51
51
|
em: chalk.italic,
|
|
52
52
|
hr: (h) => colors.dim(h),
|
|
53
53
|
}));
|
|
@@ -239,7 +239,7 @@ export async function startChat(options = {}) {
|
|
|
239
239
|
};
|
|
240
240
|
|
|
241
241
|
try {
|
|
242
|
-
const result = await routePrompt(fullPrompt, currentMode.systemPrompt, aiConfig, onToken);
|
|
242
|
+
const result = await routePrompt(fullPrompt, currentMode.systemPrompt, aiConfig, onToken, history);
|
|
243
243
|
spinner.stop();
|
|
244
244
|
|
|
245
245
|
// Store in history
|
package/src/cli.js
CHANGED
|
@@ -44,8 +44,8 @@ const getMarked = () => new Marked(markedTerminal({
|
|
|
44
44
|
showSectionPrefix: false,
|
|
45
45
|
code: (c) => colors.orange(c),
|
|
46
46
|
codespan: (c) => colors.accent3(c),
|
|
47
|
-
heading: (h) => colors.accent(h)
|
|
48
|
-
strong: (s) => colors.magenta(s)
|
|
47
|
+
heading: (h) => colors.accent.bold(h),
|
|
48
|
+
strong: (s) => colors.magenta.bold(s),
|
|
49
49
|
em: chalk.italic,
|
|
50
50
|
hr: (h) => colors.dim(h),
|
|
51
51
|
}));
|
package/test/router.test.js
CHANGED
|
@@ -171,4 +171,54 @@ test("Universal AI Router Suite", async (t) => {
|
|
|
171
171
|
assert.ok(result.errors[0].includes("Node 1 Groq"));
|
|
172
172
|
assert.ok(result.errors[1].includes("Node 2 OpenAI"));
|
|
173
173
|
});
|
|
174
|
+
|
|
175
|
+
await t.test("routePrompt forwards chat history to OpenAI and Google payloads correctly", async () => {
|
|
176
|
+
const history = [
|
|
177
|
+
{ role: "user", content: "What is your name?" },
|
|
178
|
+
{ role: "assistant", content: "I am Aether." }
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
// 1. Test Groq (OpenAI-compatible) receives history
|
|
182
|
+
globalThis.fetch = async (url, options) => {
|
|
183
|
+
fetchCalls.push({ url, options });
|
|
184
|
+
return {
|
|
185
|
+
ok: true,
|
|
186
|
+
json: async () => ({
|
|
187
|
+
choices: [{ message: { content: "Groq reply" } }],
|
|
188
|
+
}),
|
|
189
|
+
};
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
await routePrompt("How are you?", "Sys prompt", { GROQ_API_KEY: "groq-key" }, null, history);
|
|
193
|
+
assert.strictEqual(fetchCalls.length, 1);
|
|
194
|
+
const groqBody = JSON.parse(fetchCalls[0].options.body);
|
|
195
|
+
assert.deepStrictEqual(groqBody.messages, [
|
|
196
|
+
{ role: "system", content: "Sys prompt" },
|
|
197
|
+
{ role: "user", content: "What is your name?" },
|
|
198
|
+
{ role: "assistant", content: "I am Aether." },
|
|
199
|
+
{ role: "user", content: "How are you?" }
|
|
200
|
+
]);
|
|
201
|
+
|
|
202
|
+
fetchCalls = [];
|
|
203
|
+
|
|
204
|
+
// 2. Test Google Gemini receives history
|
|
205
|
+
globalThis.fetch = async (url, options) => {
|
|
206
|
+
fetchCalls.push({ url, options });
|
|
207
|
+
return {
|
|
208
|
+
ok: true,
|
|
209
|
+
json: async () => ({
|
|
210
|
+
candidates: [{ content: { parts: [{ text: "Gemini reply" }] } }],
|
|
211
|
+
}),
|
|
212
|
+
};
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
await routePrompt("How are you?", "Sys prompt", { GOOGLE_API_KEYS: "google-key" }, null, history);
|
|
216
|
+
assert.strictEqual(fetchCalls.length, 1);
|
|
217
|
+
const googleBody = JSON.parse(fetchCalls[0].options.body);
|
|
218
|
+
assert.deepStrictEqual(googleBody.contents, [
|
|
219
|
+
{ role: "user", parts: [{ text: "What is your name?" }] },
|
|
220
|
+
{ role: "model", parts: [{ text: "I am Aether." }] },
|
|
221
|
+
{ role: "user", parts: [{ text: "How are you?" }] }
|
|
222
|
+
]);
|
|
223
|
+
});
|
|
174
224
|
});
|