@dubeyvishal/orbital-cli 1.0.1

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.
@@ -0,0 +1,432 @@
1
+ import chalk from "chalk";
2
+ import boxen from "boxen";
3
+ import {
4
+ text,
5
+ isCancel,
6
+ cancel,
7
+ intro,
8
+ outro,
9
+ multiselect,
10
+ } from "@clack/prompts";
11
+ import yoctoSpinner from "yocto-spinner";
12
+ import { marked } from "marked";
13
+ import { markedTerminal } from "marked-terminal";
14
+ import { AIService } from "../ai/googleService.js";
15
+ import { ChatService } from "../../service/chatService.js";
16
+ import { getStoredToken } from "../../lib/token.js";
17
+ import prisma from "../../lib/db.js";
18
+ import { ensureDbConnection } from "../../lib/dbHealth.js";
19
+ import {
20
+ availableTools,
21
+ enableTools,
22
+ getEnabledTools,
23
+ getEnabledToolNames,
24
+ resetTools,
25
+ } from "../../config/toolConfig.js";
26
+
27
+ marked.use(
28
+ markedTerminal({
29
+ code: chalk.cyan,
30
+ blockquote: chalk.gray.italic,
31
+ heading: chalk.green.bold,
32
+ firstHeading: chalk.magenta.underline.bold,
33
+ hr: chalk.reset,
34
+ listitem: chalk.reset,
35
+ list: chalk.reset,
36
+ paragraph: chalk.reset,
37
+ strong: chalk.bold,
38
+ em: chalk.italic,
39
+ codespan: chalk.yellow.bgBlack,
40
+ del: chalk.dim.gray.strikethrough,
41
+ link: chalk.blue.underline,
42
+ href: chalk.blue.underline,
43
+ })
44
+ );
45
+
46
+ let aiService;
47
+ const chatService = new ChatService();
48
+
49
+ const getUserFromToken = async () => {
50
+ const token = await getStoredToken();
51
+
52
+ if (!token?.access_token) {
53
+ throw new Error("Not authenticated. Please run 'orbital login' first.");
54
+ }
55
+
56
+ const dbOk = await ensureDbConnection();
57
+ if (!dbOk) {
58
+ throw new Error("Database unavailable. Fix DATABASE_URL/connectivity and try again.");
59
+ }
60
+
61
+ const spinner = yoctoSpinner({ text: "Authenticating..." }).start();
62
+
63
+ const user = await prisma.user.findFirst({
64
+ where: {
65
+ sessions: {
66
+ some: { token: token.access_token },
67
+ },
68
+ },
69
+ });
70
+
71
+ if (!user) {
72
+ spinner.error("User not found");
73
+ throw new Error("User not found. Please login again");
74
+ }
75
+
76
+ spinner.success(`Welcome back, ${user.name}!`);
77
+ return user;
78
+ };
79
+
80
+ const selectTools = async () => {
81
+ const truncateHint = (text, maxLen = 70) => {
82
+ if (!text) return "";
83
+ const singleLine = String(text).replace(/\s+/g, " ").trim();
84
+ if (singleLine.length <= maxLen) return singleLine;
85
+ return singleLine.slice(0, Math.max(0, maxLen - 3)) + "...";
86
+ };
87
+
88
+ const toolOptions = availableTools.map((tool) => ({
89
+ value: tool.id,
90
+ label: tool.name,
91
+ // Long hints wrap in the terminal and look like duplicated options.
92
+ hint: truncateHint(tool.description),
93
+ }));
94
+
95
+ const selectedTools = await multiselect({
96
+ message: chalk.cyan(
97
+ "Select tools to enable (Space to select, Enter to confirm):"
98
+ ),
99
+ options: toolOptions,
100
+ required: false,
101
+ });
102
+
103
+ if (isCancel(selectedTools)) {
104
+ cancel(chalk.yellow("Tool selection cancelled"));
105
+ process.exit(0);
106
+ }
107
+
108
+ enableTools(selectedTools);
109
+
110
+ if (selectedTools.length === 0) {
111
+ console.log(chalk.yellow("\nNo tools selected. AI will work without tools.\n"));
112
+ } else {
113
+ const toolBox = boxen(
114
+ chalk.green(
115
+ `Enabled tools:\n${selectedTools
116
+ .map((id) => {
117
+ const tool = availableTools.find((t) => t.id === id);
118
+ return tool ? ` • ${tool.name}` : ` • ${id}`;
119
+ })
120
+ .join("\n")}`
121
+ ),
122
+ {
123
+ padding: 1,
124
+ margin: { top: 1, bottom: 1 },
125
+ borderStyle: "round",
126
+ borderColor: "green",
127
+ title: "Active Tools",
128
+ titleAlignment: "center",
129
+ }
130
+ );
131
+ console.log(toolBox);
132
+ }
133
+
134
+ return selectedTools.length > 0;
135
+ };
136
+
137
+ const initConversation = async (userId, conversationId = null, mode = "tool") => {
138
+ const spinner = yoctoSpinner({ text: "Loading conversation..." }).start();
139
+
140
+ const conversation = await chatService.getOrCreateConversation(
141
+ userId,
142
+ conversationId,
143
+ mode
144
+ );
145
+
146
+ spinner.success("Conversation Loaded");
147
+
148
+ const enabledToolNames = getEnabledToolNames();
149
+ const toolsDisplay =
150
+ enabledToolNames.length > 0
151
+ ? `\n${chalk.gray("Active Tools:")} ${enabledToolNames.join(", ")}`
152
+ : `\n${chalk.gray("No tools enabled")}`;
153
+
154
+ const conversationInfo = boxen(
155
+ `${chalk.bold("Conversation")}: ${conversation.title}\n${chalk.gray(
156
+ "ID: " + conversation.id
157
+ )}\n${chalk.gray("Mode: " + conversation.mode)}${toolsDisplay}`,
158
+ {
159
+ padding: 1,
160
+ margin: { top: 1, bottom: 1 },
161
+ borderStyle: "round",
162
+ borderColor: "cyan",
163
+ title: "Chat Session",
164
+ titleAlignment: "center",
165
+ }
166
+ );
167
+
168
+ console.log(conversationInfo);
169
+
170
+ if (conversation.messages?.length > 0) {
171
+ console.log(chalk.yellow("Previous Messages:\n"));
172
+ displayMessages(conversation.messages);
173
+ }
174
+
175
+ return conversation;
176
+ };
177
+
178
+ const displayMessages = (messages) => {
179
+ messages.forEach((msg) => {
180
+ if (msg.role === "user") {
181
+ const userBox = boxen(chalk.white(msg.content), {
182
+ padding: 1,
183
+ margin: { left: 2, bottom: 1 },
184
+ borderStyle: "round",
185
+ borderColor: "blue",
186
+ title: "You",
187
+ titleAlignment: "left",
188
+ });
189
+ console.log(userBox);
190
+ } else {
191
+ const renderedContent = marked.parse(msg.content);
192
+ const assistantBox = boxen(renderedContent.trim(), {
193
+ padding: 1,
194
+ margin: { left: 2, bottom: 1 },
195
+ borderStyle: "round",
196
+ borderColor: "green",
197
+ title: "Assistant",
198
+ titleAlignment: "left",
199
+ });
200
+ console.log(assistantBox);
201
+ }
202
+ });
203
+ };
204
+
205
+ const updateConversationTitle = async (conversationId, userInput, messageCount) => {
206
+ if (messageCount === 1) {
207
+ const title = userInput.slice(0, 50) + (userInput.length > 50 ? "..." : "");
208
+ await chatService.updateTitle(conversationId, title);
209
+ }
210
+ };
211
+
212
+ const saveMessage = async (conversationId, role, content) => {
213
+ return await chatService.addMessage(conversationId, role, content);
214
+ };
215
+
216
+ const getAIResponse = async (conversationId) => {
217
+ 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
+ let fullResponse = "";
225
+ let isFirstChunk = true;
226
+ const toolCallsDetected = [];
227
+
228
+ 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
+ }
245
+ );
246
+
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
+ }
256
+
257
+ if (toolCallsDetected.length > 0) {
258
+ console.log("\n");
259
+ const toolCallBox = boxen(
260
+ toolCallsDetected
261
+ .map(
262
+ (tc) =>
263
+ `${chalk.cyan("Tool:")} ${tc.toolName}\n${chalk.gray("Args:")} ${JSON.stringify(
264
+ tc.args,
265
+ null,
266
+ 2
267
+ )}`
268
+ )
269
+ .join("\n\n"),
270
+ {
271
+ padding: 1,
272
+ margin: 1,
273
+ borderStyle: "round",
274
+ borderColor: "cyan",
275
+ title: "Tool Calls",
276
+ }
277
+ );
278
+ console.log(toolCallBox);
279
+ }
280
+
281
+ if (result?.toolResults && result.toolResults.length > 0) {
282
+ const toolResultBox = boxen(
283
+ result.toolResults
284
+ .map(
285
+ (tr) =>
286
+ `${chalk.green("✓ Tool:")} ${tr.toolName}\n${chalk.gray(
287
+ "Result:"
288
+ )} ${String(JSON.stringify(tr.result, null, 2)).slice(0, 200)}...`
289
+ )
290
+ .join("\n\n"),
291
+ {
292
+ padding: 1,
293
+ margin: 1,
294
+ borderStyle: "round",
295
+ borderColor: "green",
296
+ title: "🧰 Tool Results",
297
+ }
298
+ );
299
+ console.log(toolResultBox);
300
+ }
301
+
302
+ console.log("\n");
303
+ console.log(marked.parse(fullResponse));
304
+ console.log(chalk.gray("-".repeat(60)));
305
+ console.log("\n");
306
+
307
+ return result?.content ?? fullResponse.trim();
308
+ } catch (error) {
309
+ spinner.error("Failed to get AI response");
310
+ throw error;
311
+ }
312
+ };
313
+
314
+ const chatLoop = async (conversation) => {
315
+ const enabledToolNames = getEnabledToolNames();
316
+
317
+ const helpText = [
318
+ "• Type your message and press Enter",
319
+ `• AI has access to: ${
320
+ enabledToolNames.length > 0 ? enabledToolNames.join(", ") : "No tools"
321
+ }`,
322
+ '• Type "exit" to end conversation',
323
+ "• Press Ctrl+C to quit anytime",
324
+ ]
325
+ .map((line) => chalk.gray(line))
326
+ .join("\n");
327
+
328
+ console.log(
329
+ boxen(helpText, {
330
+ padding: 1,
331
+ margin: { bottom: 1 },
332
+ borderStyle: "round",
333
+ borderColor: "gray",
334
+ dimBorder: true,
335
+ })
336
+ );
337
+
338
+ while (true) {
339
+ const userInput = await text({
340
+ message: chalk.blue("Your message"),
341
+ placeholder: "Type your message...",
342
+ validate(value) {
343
+ if (!value || value.trim().length === 0) return "Message cannot be empty";
344
+ },
345
+ });
346
+
347
+ if (isCancel(userInput)) {
348
+ console.log(
349
+ boxen(chalk.yellow("Chat session ended. Goodbye!"), {
350
+ padding: 1,
351
+ margin: 1,
352
+ borderStyle: "round",
353
+ borderColor: "yellow",
354
+ })
355
+ );
356
+ process.exit(0);
357
+ }
358
+
359
+ if (userInput.trim().toLowerCase() === "exit") {
360
+ console.log(
361
+ boxen(chalk.yellow("Chat session ended. Goodbye!"), {
362
+ padding: 1,
363
+ margin: 1,
364
+ borderStyle: "round",
365
+ borderColor: "yellow",
366
+ })
367
+ );
368
+ break;
369
+ }
370
+
371
+ console.log(
372
+ boxen(chalk.white(userInput), {
373
+ padding: 1,
374
+ margin: { left: 2, top: 1, bottom: 1 },
375
+ borderStyle: "round",
376
+ borderColor: "blue",
377
+ title: "You",
378
+ titleAlignment: "left",
379
+ })
380
+ );
381
+
382
+ await saveMessage(conversation.id, "user", userInput);
383
+ const messages = await chatService.getMessages(conversation.id);
384
+
385
+ const aiResponse = await getAIResponse(conversation.id);
386
+ await saveMessage(conversation.id, "assistant", aiResponse);
387
+
388
+ await updateConversationTitle(conversation.id, userInput, messages.length);
389
+ }
390
+ };
391
+
392
+ export const startToolChat = async (conversationId) => {
393
+ 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
+ intro(
403
+ boxen(chalk.bold.cyan("Orbital AI - Tool Calling Mode"), {
404
+ padding: 1,
405
+ borderStyle: "double",
406
+ borderColor: "cyan",
407
+ })
408
+ );
409
+
410
+ const user = await getUserFromToken();
411
+
412
+ await selectTools();
413
+
414
+ const conversation = await initConversation(user.id, conversationId, "tool");
415
+
416
+ await chatLoop(conversation);
417
+
418
+ resetTools();
419
+ outro(chalk.green("Thanks for using tools"));
420
+ } catch (error) {
421
+ console.log(
422
+ boxen(chalk.red(`Error: ${error.message}`), {
423
+ padding: 1,
424
+ margin: 1,
425
+ borderStyle: "round",
426
+ borderColor: "red",
427
+ })
428
+ );
429
+ resetTools();
430
+ process.exit(1);
431
+ }
432
+ };
@@ -0,0 +1,269 @@
1
+ import chalk from "chalk";
2
+ import boxen from "boxen";
3
+ import {text , isCancel , cancel , intro , outro }from "@clack/prompts";
4
+ import yoctoSpinner from "yocto-spinner";
5
+ import {marked} from "marked";
6
+ import {markedTerminal} from "marked-terminal";
7
+ import {AIService} from "../ai/googleService.js";
8
+ import {ChatService} from "../../service/chatService.js"
9
+ import {getStoredToken} from "../../lib/token.js";
10
+ import prisma from "../../lib/db.js"
11
+ import { ensureDbConnection } from "../../lib/dbHealth.js";
12
+
13
+ marked.use(
14
+ markedTerminal({
15
+ code : chalk.cyan ,
16
+ blockquote : chalk.gray.italic ,
17
+ heading : chalk.green.bold ,
18
+ firstHeading: chalk.magenta.underline.bold ,
19
+ hr : chalk.reset ,
20
+ listitem : chalk.reset ,
21
+ list : chalk.reset ,
22
+ paragraph : chalk.reset ,
23
+ strong : chalk.bold ,
24
+ em : chalk.italic ,
25
+ codespan : chalk.yellow.bgBlack ,
26
+ del : chalk.dim.gray.strikethrough ,
27
+ link : chalk.blue.underline ,
28
+ href : chalk.blue.underline
29
+ })
30
+ )
31
+
32
+ let aiService;
33
+ const chatService = new ChatService();
34
+
35
+ const getUserFromToken = async()=>{
36
+ const token = await getStoredToken()
37
+ if(!token?.access_token){
38
+ throw new Error("Not authenticated. Please run 'orbital login' first.");
39
+ }
40
+
41
+ const dbOk = await ensureDbConnection();
42
+ if(!dbOk){
43
+ throw new Error("Database unavailable. Fix DATABASE_URL/connectivity and try again.");
44
+ }
45
+
46
+ 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");
56
+ }
57
+ spinner.success(`Welcome back , ${user.name}!`);
58
+ return user;
59
+ }
60
+
61
+ const initConversation = async(userId , conversationId = null , mode = "chat")=>{
62
+ const spinner = yoctoSpinner({text : "Loading conservation ..."}).start();
63
+
64
+ const conversation = await chatService.getOrCreateConversation(
65
+ userId ,
66
+ conversationId ,
67
+ mode
68
+ )
69
+ spinner.success("Conversation Loaded");
70
+
71
+ const conversationInfo = boxen(
72
+ `${chalk.bold("Conversation")} : ${conversation.title}\n ${chalk.gray("ID: " + conversation.id)} \n ${chalk.gray("Mode: " + conversation.mode)}`,
73
+ {
74
+ padding : 1 ,
75
+ margin : {top: 1 , bottom : 1} ,
76
+ borderStyle : "round" ,
77
+ borderColor : "cyan" ,
78
+ title : "Chat Settion",
79
+ titleAlignment : "center"
80
+ }
81
+ );
82
+ console.log(conversationInfo);
83
+
84
+ if(conversation.messages?.length > 0){
85
+ console.log(chalk.yellow("Previous Messsages: \n"));
86
+ displayMessages(conversation.messages);
87
+ }
88
+
89
+ return conversation ;
90
+ }
91
+
92
+ const displayMessages = (messages) => {
93
+ messages.forEach((msg) => {
94
+ if (msg.role === "user") {
95
+ const userBox = boxen(chalk.white(msg.content), {
96
+ padding: 1,
97
+ margin: { left: 2, bottom: 1 },
98
+ borderStyle: "round",
99
+ borderColor: "blue",
100
+ title: "You",
101
+ titleAlignment: "left",
102
+ });
103
+ console.log(userBox);
104
+ } else {
105
+ const renderedContent = marked.parse(msg.content);
106
+ const assistantBox = boxen(renderedContent.trim(), {
107
+ padding: 1,
108
+ margin: { left: 2, bottom: 1 },
109
+ borderStyle: "round",
110
+ borderColor: "green",
111
+ title: "Assistant",
112
+ titleAlignment: "left",
113
+ });
114
+ console.log(assistantBox);
115
+ }
116
+ });
117
+ };
118
+
119
+ const saveMessage = async(conversationId , role , content) =>{
120
+ return await chatService.addMessage(conversationId , role , content)
121
+ }
122
+
123
+ const getAIResponse = async(conversationId)=>{
124
+ const spinner = yoctoSpinner({
125
+ text : "AI is thinking...",
126
+ color: "cyan"
127
+ }).start();
128
+
129
+ const dbMessages = await chatService.getMessages(conversationId);
130
+ const aiMessages = chatService.formatMessageForAI(dbMessages);
131
+
132
+ let fullResponse = "";
133
+ let isFirstChunk = true ;
134
+ 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
+ });
146
+
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
+ }
155
+
156
+ console.log("\n");
157
+ const renderMarkdown = marked.parse(fullResponse);
158
+ console.log(renderMarkdown);
159
+ console.log(chalk.gray("-".repeat(60)));
160
+ console.log("\n");
161
+
162
+ return result.content ;
163
+ }
164
+ catch(error){
165
+ spinner.error("Failed to get AI response");
166
+ throw error ;
167
+ }
168
+ }
169
+
170
+ const updateConversationTitle = async(conversationId , userInput , messageCount)=>{
171
+ if(messageCount ===1){
172
+ const title = userInput.slice(0,50) + (userInput.length > 50 ? "..." : "");
173
+ await chatService.updateTitle(conversationId , title);
174
+ }
175
+ }
176
+
177
+ const chatLoop = async(conversation)=>{
178
+ const helpBox = boxen(
179
+ `${chalk.gray(`* Type your message and press Enter`)}\n ${chalk.gray(`* Markdown formatting is supported in response`)}
180
+ \n${chalk.gray(`*Type "exit" to end conversation`)}\n ${chalk.gray(`*Please ctrl + C to quit anytime`)}`,
181
+ {
182
+ padding : 1 ,
183
+ margin : {bottom : 1},
184
+ borderStyle : "round" ,
185
+ borderColor : "gray" ,
186
+ dimBorder : true
187
+ }
188
+ );
189
+ console.log(helpBox);
190
+
191
+ while(true){
192
+ const userInput = await text({
193
+ message: chalk.blue("Your message"),
194
+ placeholder : "Type your message...",
195
+ validate(value){
196
+ if(!value || value.trim().length ===0){
197
+ return "Message cannot be empty";
198
+ }
199
+ },
200
+ });
201
+
202
+ if(isCancel(userInput)){
203
+ const exitBox = boxen(chalk.yellow("Chat session ended. GoodBye! "),{
204
+ padding : 1 ,
205
+ margin : 1,
206
+ borderStyle : "round" ,
207
+ borderColor : "yellow" ,
208
+ });
209
+ console.log(exitBox);
210
+ process.exit(0);
211
+ }
212
+ if(userInput.trim().toLowerCase() === "exit"){
213
+ const exitBox = boxen(chalk.yellow("Chat session ended. GoodBye! "),{
214
+ padding : 1 ,
215
+ margin : 1,
216
+ borderStyle : "round" ,
217
+ borderColor : "yellow" ,
218
+ });
219
+ console.log(exitBox);
220
+ break ;
221
+ }
222
+ await saveMessage(conversation.id , "user" , userInput);
223
+ const messages = await chatService.getMessages(conversation.id);
224
+
225
+ const aiResponse = await getAIResponse(conversation.id);
226
+ await saveMessage(conversation.id , "assistant" , aiResponse);
227
+
228
+ await updateConversationTitle(conversation.id , userInput , messages.length)
229
+ }
230
+ }
231
+
232
+
233
+
234
+ export const startChat = async(mode="chat" , conversationId = null)=>{
235
+ 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
+ intro(
245
+ boxen(chalk.bold.cyan("Orbital AI Chat") , {
246
+ padding: 1 ,
247
+ borderStyle : "double",
248
+ borderColor: "cyan"
249
+ })
250
+ )
251
+
252
+ const user = await getUserFromToken()
253
+ const conversation = await initConversation(user.id , conversationId , mode);
254
+ await chatLoop(conversation)
255
+
256
+ outro(chalk.green("Thanks for chatting"))
257
+ }
258
+ catch(error){
259
+ const errorBox = boxen(chalk.red(`Error: ${error.message}`),{
260
+ padding : 1 ,
261
+ margin : 1 ,
262
+ borderStyle : "round",
263
+ borderColor : "red",
264
+
265
+ });
266
+ console.log(errorBox);
267
+ process.exit(1);
268
+ }
269
+ }