@dubeyvishal/orbital-cli 1.0.3 → 1.0.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.
Files changed (32) hide show
  1. package/README.md +13 -0
  2. package/package.json +2 -1
  3. package/server/src/cli/ai/googleService.js +3 -4
  4. package/server/src/cli/chat/chat-with-ai-agent.js +45 -53
  5. package/server/src/cli/chat/chat-with-ai-tools.js +72 -84
  6. package/server/src/cli/chat/chat-with-ai.js +58 -67
  7. package/server/src/cli/commands/General/openApp.js +3 -20
  8. package/server/src/cli/commands/ai/wakeUp.js +11 -22
  9. package/server/src/cli/commands/auth/aboutMe.js +2 -1
  10. package/server/src/cli/commands/auth/login.js +34 -11
  11. package/server/src/cli/commands/config/setkey.js +12 -5
  12. package/server/src/cli/main.js +1 -1
  13. package/server/src/cli/utils/apiClient.js +66 -0
  14. package/server/src/config/agentConfig.js +22 -17
  15. package/server/src/config/api.js +22 -0
  16. package/server/src/config/env.js +2 -16
  17. package/server/src/config/toolConfig.js +7 -0
  18. package/server/src/controllers/aiController.js +58 -0
  19. package/server/src/controllers/cliController.js +77 -0
  20. package/server/src/db/prisma.js +3 -0
  21. package/server/src/index.js +12 -6
  22. package/server/src/lib/auth.js +4 -4
  23. package/server/src/lib/credentialStore.js +47 -0
  24. package/server/src/lib/orbitalConfig.js +165 -17
  25. package/server/src/lib/token.js +8 -14
  26. package/server/src/middleware/auth.js +39 -0
  27. package/server/src/middleware/errorHandler.js +11 -0
  28. package/server/src/routes/authRoutes.js +14 -0
  29. package/server/src/routes/cliRoutes.js +18 -0
  30. package/server/src/services/aiService.js +10 -0
  31. package/server/src/services/chatService.js +1 -0
  32. package/server/src/types/express.d.ts +19 -0
package/README.md CHANGED
@@ -6,6 +6,19 @@ Built with **Next.js**, **Node.js**, **Express**, and **Commander.js**, with sec
6
6
 
7
7
  ---
8
8
 
9
+ ## 📸 Project Screenshots
10
+
11
+ ### 🔐 Orbital Login Screen
12
+ ![Orbital Login Screen](./images/pic1.png)
13
+
14
+ ### 🛠️ Orbital Wakeup – Tools Loaded
15
+ ![Orbital Wakeup](./images/pic2.png)
16
+
17
+ ### 🤖 Agentic Mode
18
+ ![Agentic Mode](./images/pic3.png)
19
+
20
+ ---
21
+
9
22
  ## ✨ Features
10
23
 
11
24
  ### AI Chat Mode
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dubeyvishal/orbital-cli",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
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",
@@ -31,6 +31,7 @@
31
31
  "commander": "^14.0.2",
32
32
  "dotenv": "^17.2.3",
33
33
  "figlet": "^1.9.4",
34
+ "keytar": "^7.9.0",
34
35
  "marked": "^15.0.12",
35
36
  "marked-terminal": "^7.3.0",
36
37
  "open": "^11.0.0",
@@ -2,15 +2,14 @@ import { google } from "@ai-sdk/google";
2
2
  import { streamText, generateObject } from "ai";
3
3
  import { config } from "../../config/googleConfig.js";
4
4
  import chalk from "chalk";
5
+ import { requireGeminiApiKeySync } from "../../lib/orbitalConfig.js";
5
6
 
6
7
  export class AIService {
7
8
  constructor() {
8
- if (!config.googleApiKey) {
9
- throw new Error("GOOGLE_GENERATIVE_AI_API_KEY is not set in env");
10
- }
9
+ const apiKey = requireGeminiApiKeySync();
11
10
 
12
11
  this.model = google(config.model, {
13
- apiKey: config.googleApiKey,
12
+ apiKey,
14
13
  });
15
14
  }
16
15
 
@@ -4,18 +4,18 @@ import { text, isCancel, cancel, intro, outro, confirm } from "@clack/prompts";
4
4
  import yoctoSpinner from "yocto-spinner";
5
5
  import { marked } from "marked";
6
6
  import { markedTerminal } from "marked-terminal";
7
- import { AIService } from "../ai/googleService.js";
8
- import { ChatService } from "../../service/chatService.js";
9
7
  import { getStoredToken } from "../../lib/token.js";
10
- import prisma from "../../lib/db.js";
11
- import { ensureDbConnection } from "../../lib/dbHealth.js";
12
- import { generateApplication } from "../../config/agentConfig.js";
8
+ import {
9
+ createApplicationFiles,
10
+ displayFileTree,
11
+ generateApplicationPlan,
12
+ } from "../../config/agentConfig.js";
13
+ import { apiRequestSafe } from "../utils/apiClient.js";
14
+ import { AIService } from "../ai/googleService.js";
15
+ import { requireGeminiApiKey } from "../../lib/orbitalConfig.js";
13
16
 
14
17
  marked.use(markedTerminal());
15
18
 
16
- let aiService;
17
- const chatService = new ChatService();
18
-
19
19
  const getEnabledToolNames = () => {
20
20
  return [];
21
21
  };
@@ -27,40 +27,31 @@ const getUserFromToken = async () => {
27
27
  throw new Error("Not authenticated. Please run 'orbital login' first.");
28
28
  }
29
29
 
30
- const dbOk = await ensureDbConnection();
31
- if (!dbOk) {
32
- throw new Error(
33
- "Database unavailable. Fix DATABASE_URL/connectivity and try again."
34
- );
35
- }
36
-
37
30
  const spinner = yoctoSpinner({ text: "Authenticating..." }).start();
31
+ try {
32
+ const result = await apiRequestSafe("/api/cli/me");
33
+ const user = result?.user;
38
34
 
39
- const user = await prisma.user.findFirst({
40
- where: {
41
- sessions: {
42
- some: { token: token.access_token },
43
- },
44
- },
45
- });
35
+ if (!user) {
36
+ spinner.error("User not found");
37
+ throw new Error("User not found. Please login again");
38
+ }
46
39
 
47
- if (!user) {
48
- spinner.error("User not found");
49
- throw new Error("User not found. Please login again");
40
+ spinner.success(`Welcome back, ${user.name}!`);
41
+ return user;
42
+ } finally {
43
+ spinner.stop();
50
44
  }
51
-
52
- spinner.success(`Welcome back, ${user.name}!`);
53
- return user;
54
45
  };
55
46
 
56
47
  const initConversation = async (userId, conversationId = null, mode = "tool") => {
57
48
  const spinner = yoctoSpinner({ text: "Loading conversation..." }).start();
58
49
 
59
- const conversation = await chatService.getOrCreateConversation(
60
- userId,
61
- conversationId,
62
- mode
63
- );
50
+ const result = await apiRequestSafe("/api/cli/conversations/init", {
51
+ method: "POST",
52
+ body: { conversationId, mode },
53
+ });
54
+ const conversation = result?.conversation;
64
55
 
65
56
  spinner.success("Conversation Loaded");
66
57
 
@@ -91,7 +82,10 @@ const initConversation = async (userId, conversationId = null, mode = "tool") =>
91
82
  };
92
83
 
93
84
  const saveMessage = async (conversationId, role, content) => {
94
- return await chatService.addMessage(conversationId, role, content);
85
+ return await apiRequestSafe("/api/cli/messages", {
86
+ method: "POST",
87
+ body: { conversationId, role, content },
88
+ });
95
89
  };
96
90
 
97
91
  const agentLoop = async (conversation) => {
@@ -155,21 +149,27 @@ const agentLoop = async (conversation) => {
155
149
  await saveMessage(conversation.id, "user", userInput);
156
150
 
157
151
  try {
158
- const result = await generateApplication(
159
- userInput,
160
- aiService,
161
- process.cwd()
162
- );
152
+ await requireGeminiApiKey();
153
+ const aiService = new AIService();
154
+ const application = await generateApplicationPlan(userInput, aiService);
163
155
 
164
- if (!result || !result.success) {
165
- throw new Error("Generation returned no result.");
156
+ if (!application || !Array.isArray(application.files) || application.files.length === 0) {
157
+ throw new Error("Generation returned no files.");
166
158
  }
167
159
 
160
+ displayFileTree(application.files, application.folderName);
161
+
162
+ const appDir = await createApplicationFiles(
163
+ process.cwd(),
164
+ application.folderName,
165
+ application.files
166
+ );
167
+
168
168
  const responseMessage =
169
- `Generated application: ${result.folderName}\n` +
170
- `Files created: ${result.files.length}\n` +
171
- `Location: ${result.appDir}\n\n` +
172
- `Setup commands:\n${result.commands.join("\n")}`;
169
+ `Generated application: ${application.folderName}\n` +
170
+ `Files created: ${application.files.length}\n` +
171
+ `Location: ${appDir}\n\n` +
172
+ `Setup commands:\n${(application.setupCommands || []).join("\n")}`;
173
173
 
174
174
  console.log(
175
175
  boxen(chalk.green(responseMessage), {
@@ -211,14 +211,6 @@ const agentLoop = async (conversation) => {
211
211
 
212
212
  export const startAgentChat = async (conversationId = null) => {
213
213
  try {
214
- if (!process.env.GOOGLE_GENERATIVE_AI_API_KEY) {
215
- throw new Error(
216
- "Gemini API key is not set. Run: orbital setkey <your-gemini-api-key>"
217
- );
218
- }
219
-
220
- aiService = new AIService();
221
-
222
214
  intro(
223
215
  boxen(
224
216
  chalk.bold.magenta("Orbital AI - Agent Mode\n\n") +
@@ -11,11 +11,7 @@ import {
11
11
  import yoctoSpinner from "yocto-spinner";
12
12
  import { marked } from "marked";
13
13
  import { markedTerminal } from "marked-terminal";
14
- import { AIService } from "../ai/googleService.js";
15
- import { ChatService } from "../../service/chatService.js";
16
14
  import { getStoredToken } from "../../lib/token.js";
17
- import prisma from "../../lib/db.js";
18
- import { ensureDbConnection } from "../../lib/dbHealth.js";
19
15
  import {
20
16
  availableTools,
21
17
  enableTools,
@@ -23,6 +19,9 @@ import {
23
19
  getEnabledToolNames,
24
20
  resetTools,
25
21
  } from "../../config/toolConfig.js";
22
+ import { apiRequestSafe } from "../utils/apiClient.js";
23
+ import { AIService } from "../ai/googleService.js";
24
+ import { requireGeminiApiKey } from "../../lib/orbitalConfig.js";
26
25
 
27
26
  marked.use(
28
27
  markedTerminal({
@@ -43,9 +42,6 @@ marked.use(
43
42
  })
44
43
  );
45
44
 
46
- let aiService;
47
- const chatService = new ChatService();
48
-
49
45
  const getUserFromToken = async () => {
50
46
  const token = await getStoredToken();
51
47
 
@@ -53,28 +49,22 @@ const getUserFromToken = async () => {
53
49
  throw new Error("Not authenticated. Please run 'orbital login' first.");
54
50
  }
55
51
 
56
- const dbOk = await ensureDbConnection();
57
- if (!dbOk) {
58
- throw new Error("Database unavailable. Fix DATABASE_URL/connectivity and try again.");
59
- }
60
-
61
52
  const spinner = yoctoSpinner({ text: "Authenticating..." }).start();
62
53
 
63
- const user = await prisma.user.findFirst({
64
- where: {
65
- sessions: {
66
- some: { token: token.access_token },
67
- },
68
- },
69
- });
54
+ try {
55
+ const result = await apiRequestSafe("/api/cli/me");
56
+ const user = result?.user;
70
57
 
71
- if (!user) {
72
- spinner.error("User not found");
73
- throw new Error("User not found. Please login again");
74
- }
58
+ if (!user) {
59
+ spinner.error("User not found");
60
+ throw new Error("User not found. Please login again");
61
+ }
75
62
 
76
- spinner.success(`Welcome back, ${user.name}!`);
77
- return user;
63
+ spinner.success(`Welcome back, ${user.name}!`);
64
+ return user;
65
+ } finally {
66
+ spinner.stop();
67
+ }
78
68
  };
79
69
 
80
70
  const selectTools = async () => {
@@ -88,7 +78,7 @@ const selectTools = async () => {
88
78
  const toolOptions = availableTools.map((tool) => ({
89
79
  value: tool.id,
90
80
  label: tool.name,
91
- // Long hints wrap in the terminal and look like duplicated options.
81
+
92
82
  hint: truncateHint(tool.description),
93
83
  }));
94
84
 
@@ -131,17 +121,18 @@ const selectTools = async () => {
131
121
  console.log(toolBox);
132
122
  }
133
123
 
134
- return selectedTools.length > 0;
124
+ return selectedTools;
135
125
  };
136
126
 
137
127
  const initConversation = async (userId, conversationId = null, mode = "tool") => {
138
128
  const spinner = yoctoSpinner({ text: "Loading conversation..." }).start();
129
+ const result = await apiRequestSafe("/api/cli/conversations/init", {
130
+ method: "POST",
131
+ body: { conversationId, mode },
132
+ });
139
133
 
140
- const conversation = await chatService.getOrCreateConversation(
141
- userId,
142
- conversationId,
143
- mode
144
- );
134
+ const conversation = result?.conversation;
135
+ const messages = result?.messages || [];
145
136
 
146
137
  spinner.success("Conversation Loaded");
147
138
 
@@ -167,9 +158,9 @@ const initConversation = async (userId, conversationId = null, mode = "tool") =>
167
158
 
168
159
  console.log(conversationInfo);
169
160
 
170
- if (conversation.messages?.length > 0) {
161
+ if (messages.length > 0) {
171
162
  console.log(chalk.yellow("Previous Messages:\n"));
172
- displayMessages(conversation.messages);
163
+ displayMessages(messages);
173
164
  }
174
165
 
175
166
  return conversation;
@@ -205,54 +196,52 @@ const displayMessages = (messages) => {
205
196
  const updateConversationTitle = async (conversationId, userInput, messageCount) => {
206
197
  if (messageCount === 1) {
207
198
  const title = userInput.slice(0, 50) + (userInput.length > 50 ? "..." : "");
208
- await chatService.updateTitle(conversationId, title);
199
+ await apiRequestSafe(`/api/cli/conversations/${conversationId}/title`, {
200
+ method: "PATCH",
201
+ body: { title },
202
+ });
209
203
  }
210
204
  };
211
205
 
212
206
  const saveMessage = async (conversationId, role, content) => {
213
- return await chatService.addMessage(conversationId, role, content);
207
+ return await apiRequestSafe("/api/cli/messages", {
208
+ method: "POST",
209
+ body: { conversationId, role, content },
210
+ });
214
211
  };
215
212
 
216
- const getAIResponse = async (conversationId) => {
213
+ const getAIResponse = async (conversationId, toolIds = []) => {
217
214
  const spinner = yoctoSpinner({ text: "AI is thinking..." }).start();
218
-
219
- const dbMessages = await chatService.getMessages(conversationId);
220
- const aiMessages = chatService.formatMessageForAI(dbMessages);
221
-
222
- const tools = getEnabledTools();
223
-
224
215
  let fullResponse = "";
225
- let isFirstChunk = true;
226
216
  const toolCallsDetected = [];
227
217
 
228
218
  try {
229
- const result = await aiService.sendMessage(
230
- aiMessages,
231
- (chunk) => {
232
- if (isFirstChunk) {
233
- spinner.stop();
234
- console.log("\n");
235
- console.log(chalk.green.bold("Assistant: "));
236
- console.log(chalk.gray("-".repeat(60)));
237
- isFirstChunk = false;
238
- }
239
- fullResponse += chunk;
240
- },
241
- tools,
242
- (toolCall) => {
243
- toolCallsDetected.push(toolCall);
244
- }
219
+ const messageResult = await apiRequestSafe(
220
+ `/api/cli/messages?conversationId=${encodeURIComponent(conversationId)}`,
221
+ { method: "GET" }
245
222
  );
246
223
 
247
- // If the model returned without streaming any chunks, stop the spinner here
248
- // so it doesn't keep animating into the next prompt.
249
- if (isFirstChunk) {
250
- spinner.stop();
251
- console.log("\n");
252
- console.log(chalk.green.bold("Assistant: "));
253
- console.log(chalk.gray("-".repeat(60)));
254
- isFirstChunk = false;
255
- }
224
+ const messages = Array.isArray(messageResult?.messages) ? messageResult.messages : [];
225
+ const aiMessages = messages.map((m) => ({ role: m.role, content: m.content }));
226
+
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
+ const result = await aiService.sendMessage(aiMessages, null, tools);
237
+
238
+ spinner.stop();
239
+ console.log("\n");
240
+ console.log(chalk.green.bold("Assistant: "));
241
+ console.log(chalk.gray("-".repeat(60)));
242
+
243
+ fullResponse = result?.content || "";
244
+ if (Array.isArray(result?.toolCalls)) toolCallsDetected.push(...result.toolCalls);
256
245
 
257
246
  if (toolCallsDetected.length > 0) {
258
247
  console.log("\n");
@@ -304,14 +293,14 @@ const getAIResponse = async (conversationId) => {
304
293
  console.log(chalk.gray("-".repeat(60)));
305
294
  console.log("\n");
306
295
 
307
- return result?.content ?? fullResponse.trim();
296
+ return fullResponse.trim();
308
297
  } catch (error) {
309
298
  spinner.error("Failed to get AI response");
310
299
  throw error;
311
300
  }
312
301
  };
313
302
 
314
- const chatLoop = async (conversation) => {
303
+ const chatLoop = async (conversation, selectedToolIds = []) => {
315
304
  const enabledToolNames = getEnabledToolNames();
316
305
 
317
306
  const helpText = [
@@ -380,25 +369,24 @@ const chatLoop = async (conversation) => {
380
369
  );
381
370
 
382
371
  await saveMessage(conversation.id, "user", userInput);
383
- const messages = await chatService.getMessages(conversation.id);
372
+ const messageResult = await apiRequestSafe(
373
+ `/api/cli/messages?conversationId=${encodeURIComponent(conversation.id)}`,
374
+ { method: "GET" }
375
+ );
384
376
 
385
- const aiResponse = await getAIResponse(conversation.id);
377
+ const aiResponse = await getAIResponse(conversation.id, selectedToolIds);
386
378
  await saveMessage(conversation.id, "assistant", aiResponse);
387
379
 
388
- await updateConversationTitle(conversation.id, userInput, messages.length);
380
+ await updateConversationTitle(
381
+ conversation.id,
382
+ userInput,
383
+ messageResult?.messages?.length || 0
384
+ );
389
385
  }
390
386
  };
391
387
 
392
388
  export const startToolChat = async (conversationId) => {
393
389
  try {
394
- if (!process.env.GOOGLE_GENERATIVE_AI_API_KEY) {
395
- throw new Error(
396
- "Gemini API key is not set. Run: orbital setkey <your-gemini-api-key>"
397
- );
398
- }
399
-
400
- aiService = new AIService();
401
-
402
390
  intro(
403
391
  boxen(chalk.bold.cyan("Orbital AI - Tool Calling Mode"), {
404
392
  padding: 1,
@@ -409,11 +397,11 @@ export const startToolChat = async (conversationId) => {
409
397
 
410
398
  const user = await getUserFromToken();
411
399
 
412
- await selectTools();
400
+ const selectedToolIds = await selectTools();
413
401
 
414
402
  const conversation = await initConversation(user.id, conversationId, "tool");
415
403
 
416
- await chatLoop(conversation);
404
+ await chatLoop(conversation, selectedToolIds);
417
405
 
418
406
  resetTools();
419
407
  outro(chalk.green("Thanks for using tools"));
@@ -4,11 +4,10 @@ import {text , isCancel , cancel , intro , outro }from "@clack/prompts";
4
4
  import yoctoSpinner from "yocto-spinner";
5
5
  import {marked} from "marked";
6
6
  import {markedTerminal} from "marked-terminal";
7
- import {AIService} from "../ai/googleService.js";
8
- import {ChatService} from "../../service/chatService.js"
9
7
  import {getStoredToken} from "../../lib/token.js";
10
- import prisma from "../../lib/db.js"
11
- import { ensureDbConnection } from "../../lib/dbHealth.js";
8
+ import { apiRequestSafe } from "../utils/apiClient.js";
9
+ import { AIService } from "../ai/googleService.js";
10
+ import { requireGeminiApiKey } from "../../lib/orbitalConfig.js";
12
11
 
13
12
  marked.use(
14
13
  markedTerminal({
@@ -29,43 +28,36 @@ marked.use(
29
28
  })
30
29
  )
31
30
 
32
- let aiService;
33
- const chatService = new ChatService();
34
-
35
31
  const getUserFromToken = async()=>{
36
32
  const token = await getStoredToken()
37
33
  if(!token?.access_token){
38
34
  throw new Error("Not authenticated. Please run 'orbital login' first.");
39
35
  }
40
36
 
41
- const dbOk = await ensureDbConnection();
42
- if(!dbOk){
43
- throw new Error("Database unavailable. Fix DATABASE_URL/connectivity and try again.");
44
- }
45
-
46
37
  const spinner = yoctoSpinner({text: "Authenticating..."}).start();
47
- const user = await prisma.user.findFirst({
48
- where : {
49
- sessions : {
50
- some : {token : token.access_token} ,
51
- }}
52
- });
53
- if(!user){
54
- spinner.error("User not found");
55
- throw new Error("User not found. Please login again");
38
+ try {
39
+ const result = await apiRequestSafe("/api/cli/me");
40
+ const user = result?.user;
41
+ if(!user){
42
+ spinner.error("User not found");
43
+ throw new Error("User not found. Please login again");
44
+ }
45
+ spinner.success(`Welcome back , ${user.name}!`);
46
+ return user;
47
+ } finally {
48
+ spinner.stop();
56
49
  }
57
- spinner.success(`Welcome back , ${user.name}!`);
58
- return user;
59
50
  }
60
51
 
61
52
  const initConversation = async(userId , conversationId = null , mode = "chat")=>{
62
53
  const spinner = yoctoSpinner({text : "Loading conservation ..."}).start();
63
54
 
64
- const conversation = await chatService.getOrCreateConversation(
65
- userId ,
66
- conversationId ,
67
- mode
68
- )
55
+ const result = await apiRequestSafe("/api/cli/conversations/init", {
56
+ method: "POST",
57
+ body: { conversationId, mode },
58
+ });
59
+ const conversation = result?.conversation;
60
+ const messages = result?.messages || [];
69
61
  spinner.success("Conversation Loaded");
70
62
 
71
63
  const conversationInfo = boxen(
@@ -81,9 +73,9 @@ const initConversation = async(userId , conversationId = null , mode = "chat")=>
81
73
  );
82
74
  console.log(conversationInfo);
83
75
 
84
- if(conversation.messages?.length > 0){
76
+ if(messages.length > 0){
85
77
  console.log(chalk.yellow("Previous Messsages: \n"));
86
- displayMessages(conversation.messages);
78
+ displayMessages(messages);
87
79
  }
88
80
 
89
81
  return conversation ;
@@ -117,7 +109,10 @@ const displayMessages = (messages) => {
117
109
  };
118
110
 
119
111
  const saveMessage = async(conversationId , role , content) =>{
120
- return await chatService.addMessage(conversationId , role , content)
112
+ return await apiRequestSafe("/api/cli/messages", {
113
+ method: "POST",
114
+ body: { conversationId, role, content },
115
+ });
121
116
  }
122
117
 
123
118
  const getAIResponse = async(conversationId)=>{
@@ -126,40 +121,34 @@ const getAIResponse = async(conversationId)=>{
126
121
  color: "cyan"
127
122
  }).start();
128
123
 
129
- const dbMessages = await chatService.getMessages(conversationId);
130
- const aiMessages = chatService.formatMessageForAI(dbMessages);
131
-
132
124
  let fullResponse = "";
133
- let isFirstChunk = true ;
134
125
  try{
135
- const result = await aiService.sendMessage(aiMessages , (chunk)=>{
136
- if(isFirstChunk){
137
- spinner.stop();
138
- console.log("\n");
139
- const header = chalk.green.bold("Assistent: ");
140
- console.log(header);
141
- console.log(chalk.gray("-".repeat(60)));
142
- isFirstChunk = false ;
143
- }
144
- fullResponse += chunk ;
145
- });
126
+ const messageResult = await apiRequestSafe(
127
+ `/api/cli/messages?conversationId=${encodeURIComponent(conversationId)}`,
128
+ { method: "GET" }
129
+ );
146
130
 
147
- if(isFirstChunk){
148
- spinner.stop();
149
- console.log("\n");
150
- const header = chalk.green.bold("Assistent: ");
151
- console.log(header);
152
- console.log(chalk.gray("-".repeat(60)));
153
- isFirstChunk = false;
154
- }
131
+ const messages = Array.isArray(messageResult?.messages) ? messageResult.messages : [];
132
+ const aiMessages = messages.map((m) => ({ role: m.role, content: m.content }));
155
133
 
134
+ await requireGeminiApiKey();
135
+ const aiService = new AIService();
136
+ const result = await aiService.sendMessage(aiMessages);
137
+
138
+ spinner.stop();
139
+ console.log("\n");
140
+ const header = chalk.green.bold("Assistent: ");
141
+ console.log(header);
142
+ console.log(chalk.gray("-".repeat(60)));
143
+
144
+ fullResponse = result?.content || "";
156
145
  console.log("\n");
157
146
  const renderMarkdown = marked.parse(fullResponse);
158
147
  console.log(renderMarkdown);
159
148
  console.log(chalk.gray("-".repeat(60)));
160
149
  console.log("\n");
161
150
 
162
- return result.content ;
151
+ return fullResponse ;
163
152
  }
164
153
  catch(error){
165
154
  spinner.error("Failed to get AI response");
@@ -170,7 +159,10 @@ const getAIResponse = async(conversationId)=>{
170
159
  const updateConversationTitle = async(conversationId , userInput , messageCount)=>{
171
160
  if(messageCount ===1){
172
161
  const title = userInput.slice(0,50) + (userInput.length > 50 ? "..." : "");
173
- await chatService.updateTitle(conversationId , title);
162
+ await apiRequestSafe(`/api/cli/conversations/${conversationId}/title`, {
163
+ method: "PATCH",
164
+ body: { title },
165
+ });
174
166
  }
175
167
  }
176
168
 
@@ -220,12 +212,19 @@ const chatLoop = async(conversation)=>{
220
212
  break ;
221
213
  }
222
214
  await saveMessage(conversation.id , "user" , userInput);
223
- const messages = await chatService.getMessages(conversation.id);
215
+ const messageResult = await apiRequestSafe(
216
+ `/api/cli/messages?conversationId=${encodeURIComponent(conversation.id)}`,
217
+ { method: "GET" }
218
+ );
224
219
 
225
220
  const aiResponse = await getAIResponse(conversation.id);
226
- await saveMessage(conversation.id , "assistant" , aiResponse);
221
+ await saveMessage(conversation.id , "assistant" , aiResponse);
227
222
 
228
- await updateConversationTitle(conversation.id , userInput , messages.length)
223
+ await updateConversationTitle(
224
+ conversation.id ,
225
+ userInput ,
226
+ messageResult?.messages?.length || 0
227
+ )
229
228
  }
230
229
  }
231
230
 
@@ -233,14 +232,6 @@ const chatLoop = async(conversation)=>{
233
232
 
234
233
  export const startChat = async(mode="chat" , conversationId = null)=>{
235
234
  try{
236
- if (!process.env.GOOGLE_GENERATIVE_AI_API_KEY) {
237
- throw new Error(
238
- "Gemini API key is not set. Run: orbital setkey <your-gemini-api-key>"
239
- );
240
- }
241
-
242
- aiService = new AIService();
243
-
244
235
  intro(
245
236
  boxen(chalk.bold.cyan("Orbital AI Chat") , {
246
237
  padding: 1 ,