@dubeyvishal/orbital-cli 1.6.13 → 1.6.14
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 +4 -2
- package/server/src/cli/ai/googleService.js +80 -30
- package/server/src/cli/chat/chat-with-ai-agent.js +40 -20
- package/server/src/cli/chat/chat-with-ai-tools.js +108 -46
- package/server/src/cli/chat/chat-with-ai.js +232 -197
- package/server/src/cli/commands/ai/wakeUp.js +139 -78
- package/server/src/cli/commands/auth/login.js +1 -7
- package/server/src/cli/commands/config/setkey.js +57 -6
- package/server/src/cli/main.js +4 -1
- package/server/src/config/aiConfig.js +159 -0
- package/server/src/config/googleConfig.js +1 -1
- package/server/src/config/toolConfig.js +25 -44
- package/server/src/controllers/aiController.js +10 -6
- package/server/src/lib/credentialStore.js +47 -14
- package/server/src/lib/orbitalConfig.js +144 -38
- package/server/src/service/aiService.js +5 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dubeyvishal/orbital-cli",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.14",
|
|
4
4
|
"description": "A fullstack CLI-based AI platform with chat mode, multi-tool agents, and agentic AI workflows. Includes GitHub login with device authorization, secure authentication, and modular client–server architecture for building intelligent automation tools.",
|
|
5
5
|
"author": "Vishal Dubey",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@ai-sdk/google": "^3.0.6",
|
|
24
|
+
"@ai-sdk/openai": "^4.0.57",
|
|
25
|
+
"@ai-sdk/xai": "^4.0.54",
|
|
24
26
|
"@clack/prompts": "^0.11.0",
|
|
25
27
|
"ai": "^6.0.29",
|
|
26
28
|
"boxen": "^8.0.1",
|
|
@@ -49,4 +51,4 @@
|
|
|
49
51
|
"client-server",
|
|
50
52
|
"javascript"
|
|
51
53
|
]
|
|
52
|
-
}
|
|
54
|
+
}
|
|
@@ -1,11 +1,19 @@
|
|
|
1
|
-
import { google } from "@ai-sdk/google";
|
|
2
1
|
import { streamText, generateObject } from "ai";
|
|
3
2
|
import { config } from "../../config/googleConfig.js";
|
|
4
3
|
import chalk from "chalk";
|
|
5
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
normalizeProviderName,
|
|
6
|
+
getSelectedModelSync,
|
|
7
|
+
} from "../../lib/orbitalConfig.js";
|
|
8
|
+
import {
|
|
9
|
+
AI_PROVIDERS,
|
|
10
|
+
createModelInstance,
|
|
11
|
+
getModelDisplayName,
|
|
12
|
+
parseModelChoice,
|
|
13
|
+
} from "../../config/aiConfig.js";
|
|
6
14
|
|
|
7
15
|
const MAX_RETRIES = 3;
|
|
8
|
-
const BASE_DELAY_MS =
|
|
16
|
+
const BASE_DELAY_MS = 3000; // 3 seconds
|
|
9
17
|
|
|
10
18
|
const isRateLimitError = (error) => {
|
|
11
19
|
if (!error) return false;
|
|
@@ -23,12 +31,30 @@ const isRateLimitError = (error) => {
|
|
|
23
31
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
24
32
|
|
|
25
33
|
export class AIService {
|
|
26
|
-
constructor() {
|
|
27
|
-
|
|
34
|
+
constructor(modelConfig = null) {
|
|
35
|
+
let resolved;
|
|
36
|
+
|
|
37
|
+
if (modelConfig) {
|
|
38
|
+
resolved = parseModelChoice(modelConfig);
|
|
39
|
+
} else {
|
|
40
|
+
const saved = getSelectedModelSync();
|
|
41
|
+
const provider = process.env.ORBITAL_PROVIDER || saved?.provider || "gemini";
|
|
42
|
+
const model =
|
|
43
|
+
process.env.ORBITAL_MODEL ||
|
|
44
|
+
saved?.model ||
|
|
45
|
+
AI_PROVIDERS[normalizeProviderName(provider)]?.defaultModel ||
|
|
46
|
+
"gemini-2.5-flash";
|
|
47
|
+
resolved = { provider: normalizeProviderName(provider), model };
|
|
28
48
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.provider = resolved.provider;
|
|
52
|
+
this.modelName = resolved.model;
|
|
53
|
+
this.model = createModelInstance(this.provider, this.modelName);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
getDisplayName() {
|
|
57
|
+
return getModelDisplayName(this.provider, this.modelName);
|
|
32
58
|
}
|
|
33
59
|
|
|
34
60
|
async sendMessage(messages, onChunk, tools = undefined, onToolCall = null) {
|
|
@@ -45,22 +71,28 @@ export class AIService {
|
|
|
45
71
|
if (tools && Object.keys(tools).length > 0) {
|
|
46
72
|
streamConfig.tools = tools;
|
|
47
73
|
streamConfig.maxSteps = 5;
|
|
48
|
-
if (attempt === 1) {
|
|
49
|
-
console.log(
|
|
50
|
-
chalk.gray(
|
|
51
|
-
`[DEBUG] Tools enabled: ${Object.keys(tools).join(", ")}`
|
|
52
|
-
)
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
74
|
}
|
|
56
75
|
|
|
57
76
|
const result = await streamText(streamConfig);
|
|
58
77
|
|
|
59
78
|
let fullResponse = "";
|
|
60
79
|
|
|
61
|
-
for await (const
|
|
62
|
-
|
|
63
|
-
|
|
80
|
+
for await (const part of result.fullStream) {
|
|
81
|
+
if (part.type === "text-delta") {
|
|
82
|
+
const chunk = part.text ?? part.textDelta ?? "";
|
|
83
|
+
fullResponse += chunk;
|
|
84
|
+
if (onChunk) onChunk(chunk);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
if (!fullResponse) {
|
|
91
|
+
try {
|
|
92
|
+
fullResponse = (await result.text) || "";
|
|
93
|
+
} catch {
|
|
94
|
+
// ignore if result.text is not available
|
|
95
|
+
}
|
|
64
96
|
}
|
|
65
97
|
|
|
66
98
|
const toolCalls = [];
|
|
@@ -91,6 +123,20 @@ export class AIService {
|
|
|
91
123
|
}
|
|
92
124
|
}
|
|
93
125
|
|
|
126
|
+
if (!fullResponse && toolResults.length > 0) {
|
|
127
|
+
fullResponse = toolResults
|
|
128
|
+
.map((tr) => {
|
|
129
|
+
const output = tr.output ?? tr.result;
|
|
130
|
+
const resStr =
|
|
131
|
+
typeof output === "object"
|
|
132
|
+
? JSON.stringify(output)
|
|
133
|
+
: String(output);
|
|
134
|
+
return `Tool ${tr.toolName} output: ${resStr}`;
|
|
135
|
+
})
|
|
136
|
+
.join("\n");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
94
140
|
return {
|
|
95
141
|
content: fullResponse,
|
|
96
142
|
finishReason: result.finishReason,
|
|
@@ -107,36 +153,41 @@ export class AIService {
|
|
|
107
153
|
const delaySec = Math.round(delayMs / 1000);
|
|
108
154
|
console.log(
|
|
109
155
|
chalk.yellow(
|
|
110
|
-
`\n⚠ Rate limit hit (429). Retrying in ${delaySec}s... (attempt ${attempt}/${MAX_RETRIES})`
|
|
156
|
+
`\n⚠ Rate limit hit (429) on ${this.getDisplayName()}. Retrying in ${delaySec}s... (attempt ${attempt}/${MAX_RETRIES})`
|
|
111
157
|
)
|
|
112
158
|
);
|
|
113
159
|
await sleep(delayMs);
|
|
114
160
|
continue;
|
|
115
161
|
}
|
|
116
162
|
|
|
117
|
-
// Provide
|
|
163
|
+
// Provide actionable provider-specific error messages
|
|
118
164
|
if (isRateLimitError(error)) {
|
|
165
|
+
const provInfo = AI_PROVIDERS[this.provider];
|
|
119
166
|
console.error(
|
|
120
167
|
chalk.red(
|
|
121
|
-
|
|
168
|
+
`\n✖ ${this.getDisplayName()} rate limit / quota exhausted. All retry attempts failed.`
|
|
122
169
|
)
|
|
123
170
|
);
|
|
124
171
|
console.error(
|
|
125
172
|
chalk.yellow(
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
" 4. Use a different API key with available quota"
|
|
173
|
+
` Possible fixes:\n` +
|
|
174
|
+
` 1. Wait a moment and try again\n` +
|
|
175
|
+
` 2. Check your quota & billing at: ${provInfo?.docsUrl || "provider portal"}\n` +
|
|
176
|
+
` 3. Switch to another model or update your API key: orbital set-key --provider ${this.provider} <KEY>`
|
|
131
177
|
)
|
|
132
178
|
);
|
|
133
179
|
} else {
|
|
180
|
+
const detailedMsg =
|
|
181
|
+
error?.data?.error?.message ||
|
|
182
|
+
error?.message ||
|
|
183
|
+
error;
|
|
134
184
|
console.error(
|
|
135
|
-
chalk.red(
|
|
136
|
-
|
|
185
|
+
chalk.red(`AI Service Error (${this.getDisplayName()}):`),
|
|
186
|
+
detailedMsg
|
|
137
187
|
);
|
|
138
188
|
}
|
|
139
189
|
|
|
190
|
+
|
|
140
191
|
throw error;
|
|
141
192
|
}
|
|
142
193
|
}
|
|
@@ -158,11 +209,10 @@ export class AIService {
|
|
|
158
209
|
return result.object;
|
|
159
210
|
} catch (error) {
|
|
160
211
|
console.log(
|
|
161
|
-
chalk.red(
|
|
212
|
+
chalk.red(`AI Structured Generation Error (${this.getDisplayName()}):`),
|
|
162
213
|
error?.message || error
|
|
163
214
|
);
|
|
164
215
|
throw error;
|
|
165
216
|
}
|
|
166
217
|
}
|
|
167
218
|
}
|
|
168
|
-
|
|
@@ -12,13 +12,11 @@ import {
|
|
|
12
12
|
} from "../../config/agentConfig.js";
|
|
13
13
|
import { apiRequestSafe } from "../utils/apiClient.js";
|
|
14
14
|
import { AIService } from "../ai/googleService.js";
|
|
15
|
-
import {
|
|
15
|
+
import { requireApiKey, hydrateApiKeyEnv } from "../../lib/orbitalConfig.js";
|
|
16
|
+
import { parseModelChoice } from "../../config/aiConfig.js";
|
|
16
17
|
|
|
17
|
-
marked.use(markedTerminal());
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
return [];
|
|
21
|
-
};
|
|
19
|
+
marked.use(markedTerminal());
|
|
22
20
|
|
|
23
21
|
const getUserFromToken = async () => {
|
|
24
22
|
const token = await getStoredToken();
|
|
@@ -44,7 +42,12 @@ const getUserFromToken = async () => {
|
|
|
44
42
|
}
|
|
45
43
|
};
|
|
46
44
|
|
|
47
|
-
const initConversation = async (
|
|
45
|
+
const initConversation = async (
|
|
46
|
+
userId,
|
|
47
|
+
conversationId = null,
|
|
48
|
+
mode = "agent",
|
|
49
|
+
modelDisplayName = null
|
|
50
|
+
) => {
|
|
48
51
|
const spinner = yoctoSpinner({ text: "Loading conversation..." }).start();
|
|
49
52
|
|
|
50
53
|
const result = await apiRequestSafe("/api/cli/conversations/init", {
|
|
@@ -55,16 +58,14 @@ const initConversation = async (userId, conversationId = null, mode = "tool") =>
|
|
|
55
58
|
|
|
56
59
|
spinner.success("Conversation Loaded");
|
|
57
60
|
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
? `\n${chalk.gray("Active Tools:")} ${enabledToolNames.join(", ")}`
|
|
62
|
-
: `\n${chalk.gray("No tools enabled")}`;
|
|
61
|
+
const modelLine = modelDisplayName
|
|
62
|
+
? `\n${chalk.cyan("Model: " + modelDisplayName)}`
|
|
63
|
+
: "";
|
|
63
64
|
|
|
64
65
|
const conversationInfo = boxen(
|
|
65
66
|
`${chalk.bold("Conversation")}: ${conversation.title}\n${chalk.gray(
|
|
66
67
|
"ID: " + conversation.id
|
|
67
|
-
)}\n${chalk.gray("Mode: " + conversation.mode)}${
|
|
68
|
+
)}\n${chalk.gray("Mode: " + conversation.mode)}${modelLine}\n${chalk.cyan(
|
|
68
69
|
"Working Directory: "
|
|
69
70
|
)}${process.cwd()}`,
|
|
70
71
|
{
|
|
@@ -88,7 +89,7 @@ const saveMessage = async (conversationId, role, content) => {
|
|
|
88
89
|
});
|
|
89
90
|
};
|
|
90
91
|
|
|
91
|
-
const agentLoop = async (conversation) => {
|
|
92
|
+
const agentLoop = async (conversation, modelConfig = null) => {
|
|
92
93
|
const helpBox = boxen(
|
|
93
94
|
`${chalk.cyan.bold("What can the agent do?")}\n\n` +
|
|
94
95
|
`${chalk.gray("• Generate complete applications from descriptions")}\n` +
|
|
@@ -113,8 +114,8 @@ const agentLoop = async (conversation) => {
|
|
|
113
114
|
|
|
114
115
|
while (true) {
|
|
115
116
|
const userInput = await text({
|
|
116
|
-
message: chalk.magenta("
|
|
117
|
-
placeholder: "
|
|
117
|
+
message: chalk.magenta("Describe the application you want to build:"),
|
|
118
|
+
placeholder: "e.g., A CLI todo app with SQLite, or a React counter app",
|
|
118
119
|
validate(value) {
|
|
119
120
|
if (!value || value.trim().length === 0) {
|
|
120
121
|
return "Description cannot be empty";
|
|
@@ -149,8 +150,8 @@ const agentLoop = async (conversation) => {
|
|
|
149
150
|
await saveMessage(conversation.id, "user", userInput);
|
|
150
151
|
|
|
151
152
|
try {
|
|
152
|
-
|
|
153
|
-
|
|
153
|
+
const aiService = new AIService(modelConfig);
|
|
154
|
+
await requireApiKey(aiService.provider);
|
|
154
155
|
const application = await generateApplicationPlan(userInput, aiService);
|
|
155
156
|
|
|
156
157
|
if (!application || !Array.isArray(application.files) || application.files.length === 0) {
|
|
@@ -209,7 +210,17 @@ const agentLoop = async (conversation) => {
|
|
|
209
210
|
}
|
|
210
211
|
};
|
|
211
212
|
|
|
212
|
-
export const startAgentChat = async (
|
|
213
|
+
export const startAgentChat = async (modelConfigOrConvId = null, convId = null) => {
|
|
214
|
+
let modelConfig = null;
|
|
215
|
+
let conversationId = null;
|
|
216
|
+
|
|
217
|
+
if (typeof modelConfigOrConvId === "string") {
|
|
218
|
+
conversationId = modelConfigOrConvId;
|
|
219
|
+
} else if (modelConfigOrConvId && typeof modelConfigOrConvId === "object") {
|
|
220
|
+
modelConfig = modelConfigOrConvId;
|
|
221
|
+
conversationId = convId;
|
|
222
|
+
}
|
|
223
|
+
|
|
213
224
|
try {
|
|
214
225
|
intro(
|
|
215
226
|
boxen(
|
|
@@ -223,6 +234,10 @@ export const startAgentChat = async (conversationId = null) => {
|
|
|
223
234
|
)
|
|
224
235
|
);
|
|
225
236
|
|
|
237
|
+
const { provider } = parseModelChoice(modelConfig);
|
|
238
|
+
await hydrateApiKeyEnv(provider);
|
|
239
|
+
const aiService = new AIService(modelConfig);
|
|
240
|
+
|
|
226
241
|
const user = await getUserFromToken();
|
|
227
242
|
|
|
228
243
|
const shouldContinue = await confirm({
|
|
@@ -237,8 +252,13 @@ export const startAgentChat = async (conversationId = null) => {
|
|
|
237
252
|
process.exit(0);
|
|
238
253
|
}
|
|
239
254
|
|
|
240
|
-
const conversation = await initConversation(
|
|
241
|
-
|
|
255
|
+
const conversation = await initConversation(
|
|
256
|
+
user.id,
|
|
257
|
+
conversationId,
|
|
258
|
+
"agent",
|
|
259
|
+
aiService.getDisplayName()
|
|
260
|
+
);
|
|
261
|
+
await agentLoop(conversation, modelConfig);
|
|
242
262
|
|
|
243
263
|
outro(chalk.green.bold("\nThanks for using Agent Mode!"));
|
|
244
264
|
} catch (error) {
|
|
@@ -17,11 +17,13 @@ import {
|
|
|
17
17
|
enableTools,
|
|
18
18
|
getEnabledTools,
|
|
19
19
|
getEnabledToolNames,
|
|
20
|
+
getToolsForProvider,
|
|
20
21
|
resetTools,
|
|
21
22
|
} from "../../config/toolConfig.js";
|
|
22
23
|
import { apiRequestSafe } from "../utils/apiClient.js";
|
|
23
24
|
import { AIService } from "../ai/googleService.js";
|
|
24
|
-
import {
|
|
25
|
+
import { requireApiKey, hydrateApiKeyEnv } from "../../lib/orbitalConfig.js";
|
|
26
|
+
import { parseModelChoice } from "../../config/aiConfig.js";
|
|
25
27
|
|
|
26
28
|
marked.use(
|
|
27
29
|
markedTerminal({
|
|
@@ -67,7 +69,7 @@ const getUserFromToken = async () => {
|
|
|
67
69
|
}
|
|
68
70
|
};
|
|
69
71
|
|
|
70
|
-
const selectTools = async () => {
|
|
72
|
+
const selectTools = async (provider = "gemini") => {
|
|
71
73
|
const truncateHint = (text, maxLen = 70) => {
|
|
72
74
|
if (!text) return "";
|
|
73
75
|
const singleLine = String(text).replace(/\s+/g, " ").trim();
|
|
@@ -75,10 +77,19 @@ const selectTools = async () => {
|
|
|
75
77
|
return singleLine.slice(0, Math.max(0, maxLen - 3)) + "...";
|
|
76
78
|
};
|
|
77
79
|
|
|
78
|
-
const
|
|
80
|
+
const providerTools = getToolsForProvider(provider);
|
|
81
|
+
|
|
82
|
+
if (providerTools.length === 0) {
|
|
83
|
+
console.log(
|
|
84
|
+
chalk.yellow(`\nNo tools available for ${provider}. AI will work without tools.\n`)
|
|
85
|
+
);
|
|
86
|
+
enableTools([]);
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const toolOptions = providerTools.map((tool) => ({
|
|
79
91
|
value: tool.id,
|
|
80
92
|
label: tool.name,
|
|
81
|
-
|
|
82
93
|
hint: truncateHint(tool.description),
|
|
83
94
|
}));
|
|
84
95
|
|
|
@@ -104,7 +115,7 @@ const selectTools = async () => {
|
|
|
104
115
|
chalk.green(
|
|
105
116
|
`Enabled tools:\n${selectedTools
|
|
106
117
|
.map((id) => {
|
|
107
|
-
const tool =
|
|
118
|
+
const tool = providerTools.find((t) => t.id === id);
|
|
108
119
|
return tool ? ` • ${tool.name}` : ` • ${id}`;
|
|
109
120
|
})
|
|
110
121
|
.join("\n")}`
|
|
@@ -124,7 +135,12 @@ const selectTools = async () => {
|
|
|
124
135
|
return selectedTools;
|
|
125
136
|
};
|
|
126
137
|
|
|
127
|
-
const initConversation = async (
|
|
138
|
+
const initConversation = async (
|
|
139
|
+
userId,
|
|
140
|
+
conversationId = null,
|
|
141
|
+
mode = "tool",
|
|
142
|
+
modelDisplayName = null
|
|
143
|
+
) => {
|
|
128
144
|
const spinner = yoctoSpinner({ text: "Loading conversation..." }).start();
|
|
129
145
|
const result = await apiRequestSafe("/api/cli/conversations/init", {
|
|
130
146
|
method: "POST",
|
|
@@ -142,10 +158,14 @@ const initConversation = async (userId, conversationId = null, mode = "tool") =>
|
|
|
142
158
|
? `\n${chalk.gray("Active Tools:")} ${enabledToolNames.join(", ")}`
|
|
143
159
|
: `\n${chalk.gray("No tools enabled")}`;
|
|
144
160
|
|
|
161
|
+
const modelLine = modelDisplayName
|
|
162
|
+
? `\n${chalk.cyan("Model: " + modelDisplayName)}`
|
|
163
|
+
: "";
|
|
164
|
+
|
|
145
165
|
const conversationInfo = boxen(
|
|
146
166
|
`${chalk.bold("Conversation")}: ${conversation.title}\n${chalk.gray(
|
|
147
167
|
"ID: " + conversation.id
|
|
148
|
-
)}\n${chalk.gray("Mode: " + conversation.mode)}${toolsDisplay}`,
|
|
168
|
+
)}\n${chalk.gray("Mode: " + conversation.mode)}${modelLine}${toolsDisplay}`,
|
|
149
169
|
{
|
|
150
170
|
padding: 1,
|
|
151
171
|
margin: { top: 1, bottom: 1 },
|
|
@@ -210,8 +230,22 @@ const saveMessage = async (conversationId, role, content) => {
|
|
|
210
230
|
});
|
|
211
231
|
};
|
|
212
232
|
|
|
213
|
-
const getAIResponse = async (conversationId, toolIds = []) => {
|
|
214
|
-
const
|
|
233
|
+
const getAIResponse = async (conversationId, toolIds = [], modelConfig = null) => {
|
|
234
|
+
const aiService = new AIService(modelConfig);
|
|
235
|
+
await requireApiKey(aiService.provider);
|
|
236
|
+
|
|
237
|
+
// Ensure tool state matches what the user selected
|
|
238
|
+
resetTools();
|
|
239
|
+
if (Array.isArray(toolIds)) enableTools(toolIds);
|
|
240
|
+
|
|
241
|
+
const tools = getEnabledTools(aiService.provider);
|
|
242
|
+
const enabledNames = getEnabledToolNames();
|
|
243
|
+
const toolBadge =
|
|
244
|
+
enabledNames.length > 0 ? chalk.dim(` [Tools: ${enabledNames.join(", ")}]`) : "";
|
|
245
|
+
|
|
246
|
+
const spinner = yoctoSpinner({
|
|
247
|
+
text: `${aiService.getDisplayName()} is thinking...${toolBadge}`,
|
|
248
|
+
}).start();
|
|
215
249
|
let fullResponse = "";
|
|
216
250
|
const toolCallsDetected = [];
|
|
217
251
|
|
|
@@ -224,20 +258,16 @@ const getAIResponse = async (conversationId, toolIds = []) => {
|
|
|
224
258
|
const messages = Array.isArray(messageResult?.messages) ? messageResult.messages : [];
|
|
225
259
|
const aiMessages = messages.map((m) => ({ role: m.role, content: m.content }));
|
|
226
260
|
|
|
227
|
-
// Ensure the API key is present in env before any tools are initialized.
|
|
228
|
-
await requireGeminiApiKey();
|
|
229
|
-
|
|
230
|
-
// Ensure tool state matches what the user selected.
|
|
231
|
-
resetTools();
|
|
232
|
-
if (Array.isArray(toolIds)) enableTools(toolIds);
|
|
233
|
-
|
|
234
|
-
const tools = getEnabledTools();
|
|
235
|
-
const aiService = new AIService();
|
|
236
261
|
const result = await aiService.sendMessage(aiMessages, null, tools);
|
|
237
262
|
|
|
238
263
|
spinner.stop();
|
|
239
264
|
console.log("\n");
|
|
240
|
-
|
|
265
|
+
const toolsHeader =
|
|
266
|
+
enabledNames.length > 0 ? chalk.dim(` • Tools: ${enabledNames.join(", ")}`) : "";
|
|
267
|
+
console.log(
|
|
268
|
+
chalk.green.bold(`Assistant (${aiService.getDisplayName()}):`) +
|
|
269
|
+
(toolsHeader ? ` ${toolsHeader}` : "")
|
|
270
|
+
);
|
|
241
271
|
console.log(chalk.gray("-".repeat(60)));
|
|
242
272
|
|
|
243
273
|
fullResponse = result?.content || "";
|
|
@@ -270,12 +300,20 @@ const getAIResponse = async (conversationId, toolIds = []) => {
|
|
|
270
300
|
if (result?.toolResults && result.toolResults.length > 0) {
|
|
271
301
|
const toolResultBox = boxen(
|
|
272
302
|
result.toolResults
|
|
273
|
-
.map(
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
303
|
+
.map((tr) => {
|
|
304
|
+
const output = tr.output ?? tr.result;
|
|
305
|
+
const resStr =
|
|
306
|
+
output === undefined
|
|
307
|
+
? "Completed"
|
|
308
|
+
: typeof output === "object"
|
|
309
|
+
? JSON.stringify(output, null, 2)
|
|
310
|
+
: String(output);
|
|
311
|
+
const truncated =
|
|
312
|
+
resStr.length > 200 ? `${resStr.slice(0, 200)}...` : resStr;
|
|
313
|
+
return `${chalk.green("✓ Tool:")} ${tr.toolName}\n${chalk.gray(
|
|
314
|
+
"Result:"
|
|
315
|
+
)} ${truncated}`;
|
|
316
|
+
})
|
|
279
317
|
.join("\n\n"),
|
|
280
318
|
{
|
|
281
319
|
padding: 1,
|
|
@@ -300,7 +338,7 @@ const getAIResponse = async (conversationId, toolIds = []) => {
|
|
|
300
338
|
}
|
|
301
339
|
};
|
|
302
340
|
|
|
303
|
-
const chatLoop = async (conversation, selectedToolIds = []) => {
|
|
341
|
+
const chatLoop = async (conversation, selectedToolIds = [], modelConfig = null) => {
|
|
304
342
|
const enabledToolNames = getEnabledToolNames();
|
|
305
343
|
|
|
306
344
|
const helpText = [
|
|
@@ -308,28 +346,29 @@ const chatLoop = async (conversation, selectedToolIds = []) => {
|
|
|
308
346
|
`• AI has access to: ${
|
|
309
347
|
enabledToolNames.length > 0 ? enabledToolNames.join(", ") : "No tools"
|
|
310
348
|
}`,
|
|
349
|
+
"• Tools are invoked automatically by AI when needed",
|
|
350
|
+
"• Markdown formatting is supported",
|
|
311
351
|
'• Type "exit" to end conversation',
|
|
312
352
|
"• Press Ctrl+C to quit anytime",
|
|
313
|
-
]
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
dimBorder: true,
|
|
324
|
-
})
|
|
325
|
-
);
|
|
353
|
+
].join("\n");
|
|
354
|
+
|
|
355
|
+
const helpBox = boxen(chalk.gray(helpText), {
|
|
356
|
+
padding: 1,
|
|
357
|
+
margin: { bottom: 1 },
|
|
358
|
+
borderStyle: "round",
|
|
359
|
+
borderColor: "gray",
|
|
360
|
+
dimBorder: true,
|
|
361
|
+
});
|
|
362
|
+
console.log(helpBox);
|
|
326
363
|
|
|
327
364
|
while (true) {
|
|
328
365
|
const userInput = await text({
|
|
329
366
|
message: chalk.blue("Your message"),
|
|
330
|
-
placeholder: "
|
|
367
|
+
placeholder: "Ask something that requires tools...",
|
|
331
368
|
validate(value) {
|
|
332
|
-
if (!value || value.trim().length === 0)
|
|
369
|
+
if (!value || value.trim().length === 0) {
|
|
370
|
+
return "Message cannot be empty";
|
|
371
|
+
}
|
|
333
372
|
},
|
|
334
373
|
});
|
|
335
374
|
|
|
@@ -374,7 +413,11 @@ const chatLoop = async (conversation, selectedToolIds = []) => {
|
|
|
374
413
|
{ method: "GET" }
|
|
375
414
|
);
|
|
376
415
|
|
|
377
|
-
const aiResponse = await getAIResponse(
|
|
416
|
+
const aiResponse = await getAIResponse(
|
|
417
|
+
conversation.id,
|
|
418
|
+
selectedToolIds,
|
|
419
|
+
modelConfig
|
|
420
|
+
);
|
|
378
421
|
await saveMessage(conversation.id, "assistant", aiResponse);
|
|
379
422
|
|
|
380
423
|
await updateConversationTitle(
|
|
@@ -385,7 +428,17 @@ const chatLoop = async (conversation, selectedToolIds = []) => {
|
|
|
385
428
|
}
|
|
386
429
|
};
|
|
387
430
|
|
|
388
|
-
export const startToolChat = async (
|
|
431
|
+
export const startToolChat = async (modelConfigOrConvId = null, convId = null) => {
|
|
432
|
+
let modelConfig = null;
|
|
433
|
+
let conversationId = null;
|
|
434
|
+
|
|
435
|
+
if (typeof modelConfigOrConvId === "string") {
|
|
436
|
+
conversationId = modelConfigOrConvId;
|
|
437
|
+
} else if (modelConfigOrConvId && typeof modelConfigOrConvId === "object") {
|
|
438
|
+
modelConfig = modelConfigOrConvId;
|
|
439
|
+
conversationId = convId;
|
|
440
|
+
}
|
|
441
|
+
|
|
389
442
|
try {
|
|
390
443
|
intro(
|
|
391
444
|
boxen(chalk.bold.cyan("Orbital AI - Tool Calling Mode"), {
|
|
@@ -395,13 +448,22 @@ export const startToolChat = async (conversationId) => {
|
|
|
395
448
|
})
|
|
396
449
|
);
|
|
397
450
|
|
|
451
|
+
const { provider } = parseModelChoice(modelConfig);
|
|
452
|
+
await hydrateApiKeyEnv(provider);
|
|
453
|
+
const aiService = new AIService(modelConfig);
|
|
454
|
+
|
|
398
455
|
const user = await getUserFromToken();
|
|
399
456
|
|
|
400
|
-
const selectedToolIds = await selectTools();
|
|
457
|
+
const selectedToolIds = await selectTools(aiService.provider);
|
|
401
458
|
|
|
402
|
-
const conversation = await initConversation(
|
|
459
|
+
const conversation = await initConversation(
|
|
460
|
+
user.id,
|
|
461
|
+
conversationId,
|
|
462
|
+
"tool",
|
|
463
|
+
aiService.getDisplayName()
|
|
464
|
+
);
|
|
403
465
|
|
|
404
|
-
await chatLoop(conversation, selectedToolIds);
|
|
466
|
+
await chatLoop(conversation, selectedToolIds, modelConfig);
|
|
405
467
|
|
|
406
468
|
resetTools();
|
|
407
469
|
outro(chalk.green("Thanks for using tools"));
|