maverick-ai-cli 1.0.0

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 ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "maverick-ai-cli",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "maverick": "./src/cli/main.js"
8
+ },
9
+ "scripts": {
10
+ "test": "echo \"Error: no test specified\" && exit 1",
11
+ "start": "node src/index.js",
12
+ "dev": "nodemon src/index.js"
13
+ },
14
+ "keywords": [],
15
+ "author": "CodeMaverick-143 AKA Arpit Sarang",
16
+ "license": "ISC",
17
+ "type": "module",
18
+ "dependencies": {
19
+ "@ai-sdk/google": "^2.0.33",
20
+ "@clack/prompts": "^0.11.0",
21
+ "@prisma/client": "^6.19.0",
22
+ "ai": "^5.0.93",
23
+ "better-auth": "^1.3.34",
24
+ "boxen": "^8.0.1",
25
+ "chalk": "^5.6.2",
26
+ "commander": "^14.0.2",
27
+ "cors": "^2.8.5",
28
+ "dotenv": "^17.2.3",
29
+ "express": "^5.1.0",
30
+ "figlet": "^1.9.4",
31
+ "marked": "^15.0.12",
32
+ "marked-terminal": "^7.3.0",
33
+ "nodemon": "^3.1.11",
34
+ "open": "^11.0.0",
35
+ "prisma": "^6.19.0",
36
+ "yocto-spinner": "^1.0.0",
37
+ "zod": "^4.1.12"
38
+ }
39
+ }
@@ -0,0 +1,6 @@
1
+ -- CreateTable
2
+ CREATE TABLE "Test" (
3
+ "id" TEXT NOT NULL,
4
+
5
+ CONSTRAINT "Test_pkey" PRIMARY KEY ("id")
6
+ );
@@ -0,0 +1,69 @@
1
+ -- CreateTable
2
+ CREATE TABLE "user" (
3
+ "id" TEXT NOT NULL,
4
+ "name" TEXT NOT NULL,
5
+ "email" TEXT NOT NULL,
6
+ "emailVerified" BOOLEAN NOT NULL DEFAULT false,
7
+ "image" TEXT,
8
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
9
+ "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
10
+
11
+ CONSTRAINT "user_pkey" PRIMARY KEY ("id")
12
+ );
13
+
14
+ -- CreateTable
15
+ CREATE TABLE "session" (
16
+ "id" TEXT NOT NULL,
17
+ "expiresAt" TIMESTAMP(3) NOT NULL,
18
+ "token" TEXT NOT NULL,
19
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
20
+ "updatedAt" TIMESTAMP(3) NOT NULL,
21
+ "ipAddress" TEXT,
22
+ "userAgent" TEXT,
23
+ "userId" TEXT NOT NULL,
24
+
25
+ CONSTRAINT "session_pkey" PRIMARY KEY ("id")
26
+ );
27
+
28
+ -- CreateTable
29
+ CREATE TABLE "account" (
30
+ "id" TEXT NOT NULL,
31
+ "accountId" TEXT NOT NULL,
32
+ "providerId" TEXT NOT NULL,
33
+ "userId" TEXT NOT NULL,
34
+ "accessToken" TEXT,
35
+ "refreshToken" TEXT,
36
+ "idToken" TEXT,
37
+ "accessTokenExpiresAt" TIMESTAMP(3),
38
+ "refreshTokenExpiresAt" TIMESTAMP(3),
39
+ "scope" TEXT,
40
+ "password" TEXT,
41
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
42
+ "updatedAt" TIMESTAMP(3) NOT NULL,
43
+
44
+ CONSTRAINT "account_pkey" PRIMARY KEY ("id")
45
+ );
46
+
47
+ -- CreateTable
48
+ CREATE TABLE "verification" (
49
+ "id" TEXT NOT NULL,
50
+ "identifier" TEXT NOT NULL,
51
+ "value" TEXT NOT NULL,
52
+ "expiresAt" TIMESTAMP(3) NOT NULL,
53
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
54
+ "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
55
+
56
+ CONSTRAINT "verification_pkey" PRIMARY KEY ("id")
57
+ );
58
+
59
+ -- CreateIndex
60
+ CREATE UNIQUE INDEX "user_email_key" ON "user"("email");
61
+
62
+ -- CreateIndex
63
+ CREATE UNIQUE INDEX "session_token_key" ON "session"("token");
64
+
65
+ -- AddForeignKey
66
+ ALTER TABLE "session" ADD CONSTRAINT "session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
67
+
68
+ -- AddForeignKey
69
+ ALTER TABLE "account" ADD CONSTRAINT "account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,15 @@
1
+ -- CreateTable
2
+ CREATE TABLE "deviceCode" (
3
+ "id" TEXT NOT NULL,
4
+ "deviceCode" TEXT NOT NULL,
5
+ "userCode" TEXT NOT NULL,
6
+ "userId" TEXT,
7
+ "expiresAt" TIMESTAMP(3) NOT NULL,
8
+ "status" TEXT NOT NULL,
9
+ "lastPolledAt" TIMESTAMP(3),
10
+ "pollingInterval" INTEGER,
11
+ "clientId" TEXT,
12
+ "scope" TEXT,
13
+
14
+ CONSTRAINT "deviceCode_pkey" PRIMARY KEY ("id")
15
+ );
@@ -0,0 +1,31 @@
1
+ -- CreateTable
2
+ CREATE TABLE "Conversation" (
3
+ "id" TEXT NOT NULL,
4
+ "userId" TEXT NOT NULL,
5
+ "title" TEXT,
6
+ "mode" TEXT NOT NULL DEFAULT 'chat',
7
+ "creadetAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
8
+ "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
9
+
10
+ CONSTRAINT "Conversation_pkey" PRIMARY KEY ("id")
11
+ );
12
+
13
+ -- CreateTable
14
+ CREATE TABLE "Message" (
15
+ "id" TEXT NOT NULL,
16
+ "conversationId" TEXT NOT NULL,
17
+ "role" TEXT NOT NULL,
18
+ "content" TEXT NOT NULL,
19
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
20
+
21
+ CONSTRAINT "Message_pkey" PRIMARY KEY ("id")
22
+ );
23
+
24
+ -- CreateIndex
25
+ CREATE INDEX "Message_conversationId_idx" ON "Message"("conversationId");
26
+
27
+ -- AddForeignKey
28
+ ALTER TABLE "Conversation" ADD CONSTRAINT "Conversation_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
29
+
30
+ -- AddForeignKey
31
+ ALTER TABLE "Message" ADD CONSTRAINT "Message_conversationId_fkey" FOREIGN KEY ("conversationId") REFERENCES "Conversation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,3 @@
1
+ # Please do not edit this file manually
2
+ # It should be added in your version-control system (e.g., Git)
3
+ provider = "postgresql"
@@ -0,0 +1,112 @@
1
+ generator client {
2
+ provider = "prisma-client-js"
3
+ }
4
+
5
+ datasource db {
6
+ provider = "postgresql"
7
+ url = env("DATABASE_URL")
8
+ }
9
+
10
+ model Test {
11
+ id String @id @default(cuid())
12
+ }
13
+
14
+ model User {
15
+ id String @id
16
+ name String
17
+ email String
18
+ emailVerified Boolean @default(false)
19
+ image String?
20
+ createdAt DateTime @default(now())
21
+ updatedAt DateTime @default(now()) @updatedAt
22
+ sessions Session[]
23
+ accounts Account[]
24
+ conversations Conversation[]
25
+
26
+ @@unique([email])
27
+ @@map("user")
28
+ }
29
+
30
+ model Session {
31
+ id String @id
32
+ expiresAt DateTime
33
+ token String
34
+ createdAt DateTime @default(now())
35
+ updatedAt DateTime @updatedAt
36
+ ipAddress String?
37
+ userAgent String?
38
+ userId String
39
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
40
+
41
+ @@unique([token])
42
+ @@map("session")
43
+ }
44
+
45
+ model Account {
46
+ id String @id
47
+ accountId String
48
+ providerId String
49
+ userId String
50
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
51
+ accessToken String?
52
+ refreshToken String?
53
+ idToken String?
54
+ accessTokenExpiresAt DateTime?
55
+ refreshTokenExpiresAt DateTime?
56
+ scope String?
57
+ password String?
58
+ createdAt DateTime @default(now())
59
+ updatedAt DateTime @updatedAt
60
+
61
+ @@map("account")
62
+ }
63
+
64
+ model Verification {
65
+ id String @id
66
+ identifier String
67
+ value String
68
+ expiresAt DateTime
69
+ createdAt DateTime @default(now())
70
+ updatedAt DateTime @default(now()) @updatedAt
71
+
72
+ @@map("verification")
73
+ }
74
+
75
+ model DeviceCode {
76
+ id String @id
77
+ deviceCode String
78
+ userCode String
79
+ userId String?
80
+ expiresAt DateTime
81
+ status String
82
+ lastPolledAt DateTime?
83
+ pollingInterval Int?
84
+ clientId String?
85
+ scope String?
86
+
87
+ @@map("deviceCode")
88
+ }
89
+
90
+ model Conversation {
91
+ id String @id @default(cuid())
92
+ userId String
93
+ title String?
94
+ mode String @default("chat")
95
+ creadetAt DateTime @default(now())
96
+ updatedAt DateTime @default(now()) @updatedAt
97
+
98
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
99
+ messages Message[]
100
+ }
101
+
102
+ model Message {
103
+ id String @id @default(cuid())
104
+ conversationId String
105
+ role String
106
+ content String
107
+ createdAt DateTime @default(now())
108
+
109
+ consversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
110
+
111
+ @@index([conversationId])
112
+ }
@@ -0,0 +1,99 @@
1
+ import { google } from "@ai-sdk/google";
2
+ import { convertToModelMessages, streamText } from "ai";
3
+ import { config } from "../../config/google.config.js";
4
+ import chalk from "chalk"
5
+
6
+
7
+ export class AIService {
8
+ constructor() {
9
+ if (!config.googleApiKey) {
10
+ throw new Error("GOOGLE_API_KEY is not set in env")
11
+ }
12
+
13
+ this.model = google(config.model, {
14
+ apiKey: config.googleApiKey,
15
+ })
16
+ }
17
+
18
+
19
+
20
+ async sendMessage(messages, onChunk, tools = undefined, onToolCall = null) {
21
+ try {
22
+ const streamConfig = {
23
+ model: this.model,
24
+ messages: messages,
25
+ }
26
+
27
+ if (tools && Object.keys(tools).length > 0) {
28
+ streamConfig.tools = tools;
29
+ streamConfig.maxSteps = 5 // Allow up to 5 tool calls steps
30
+
31
+ console.log(chalk.gray(`[DEBUG] Tools enabled : ${Object.keys(tools).join(", ")}`))
32
+ }
33
+
34
+
35
+
36
+ const result = streamText(streamConfig);
37
+ let fullResponse = " "
38
+
39
+ for await (const chunk of result.textStream) {
40
+ fullResponse += chunk;
41
+ if (onChunk) {
42
+ onChunk(chunk)
43
+ }
44
+ }
45
+
46
+ const fullResult = result;
47
+ const toolCalls = [];
48
+ const toolResults = []
49
+
50
+ if (fullResult.steps && Array.isArray(fullResult.steps)) {
51
+ for (const step of fullResult.steps) {
52
+ if (step.toolCall && step.toolCall.lenght > 0) {
53
+ for (const toolCall of step.toolCalls) {
54
+ toolCalls.push(toolCall)
55
+
56
+ if (onToolCall) {
57
+ onToolCall(toolCall)
58
+ }
59
+ }
60
+ }
61
+
62
+ if (step.toolResult && step.toolResult.length > 0) {
63
+ toolResults.push(toolResult)
64
+ }
65
+ }
66
+ }
67
+
68
+
69
+ return {
70
+ content: fullResponse,
71
+ finishResponse: fullResult.finishResponse,
72
+ usage: fullResult.usage,
73
+ toolCalls,
74
+ toolResults,
75
+ steps: fullResult.steps
76
+ }
77
+
78
+ } catch (error) {
79
+
80
+ console.error(chalk.red("AI Service Error:"), error.message);
81
+ throw error;
82
+ }
83
+ }
84
+
85
+ async getMessage(messages, tools = undefined) {
86
+ let fullResponse = "";
87
+
88
+ const result = await this.sendMessage(messages, (chunk) => {
89
+ fullResponse += chunk
90
+ }, tools)
91
+
92
+ return result.content
93
+
94
+
95
+ }
96
+
97
+
98
+
99
+ }
@@ -0,0 +1,293 @@
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/google-service.js";
8
+ import { ChatService } from "../../service/chat.service.js";
9
+ import { getStoredToken } from "../../lib/token.js"
10
+ import prisma from "../../lib/db.js";
11
+ import { yo } from "zod/v4/locales";
12
+
13
+ marked.use(
14
+ markedTerminal({
15
+ code: chalk.cyan,
16
+ blockquote: chalk.gray.italic,
17
+ heading: chalk.green.bold, // all headings
18
+ firstHeading: chalk.green.bold.underline, // H1 specifically
19
+ hr: chalk.gray, // horizontal rules
20
+ listitem: chalk.white, // bullet items
21
+ list: chalk.white, // list container
22
+ paragraph: chalk.white, // paragraphs
23
+ strong: chalk.bold, // **bold**
24
+ em: chalk.italic, // *italic*
25
+ codespan: chalk.bgBlack.white, // inline code
26
+ del: chalk.strikethrough, // ~~strikethrough~~
27
+ link: chalk.blue, // markdown link text
28
+ href: chalk.underline.blue, // link href (actual URL)
29
+ })
30
+ );
31
+
32
+
33
+ const aiService = new AIService();
34
+ const chatService = new ChatService();
35
+
36
+
37
+ async function getUserFromToken() {
38
+ const token = await getStoredToken()
39
+ if (!token?.access_token) {
40
+ throw new Error("Not authenticated. Please run 'maverick login' first.")
41
+ }
42
+ const spinner = yoctoSpinner({ text: "Authenticating...." }).start();
43
+ const user = await prisma.user.findFirst({
44
+ where: {
45
+ sessions: {
46
+ some: { token: token.access_token }
47
+ }
48
+ }
49
+ });
50
+
51
+ if (!user) {
52
+ spinner.error("User not found");
53
+ throw new Error("User not found. Please login again")
54
+ }
55
+
56
+ spinner.success(`Welcome back, ${user.name}!`)
57
+ return user;
58
+ }
59
+
60
+
61
+ async function initConversation(userId, conversationId = null, mode = "chat") {
62
+ const spinner = yoctoSpinner({ text: "Loading conversation...." }).start()
63
+
64
+ const conversation = await chatService.getOrCreateConversation(
65
+ userId,
66
+ conversationId,
67
+ mode
68
+ )
69
+
70
+ spinner.success("Conversation Loaded")
71
+
72
+ const conversationInfo = boxen(
73
+ `${chalk.bold("Conversation")} : ${conversation.title} \n ${chalk.gray("ID: " + conversation.id)} \n ${chalk.gray("Mode: " + conversation.mode)}`, {
74
+ padding: 1,
75
+ margin: { top: 1, bottom: 1 },
76
+ borderStyle: "round",
77
+ borderColor: "cyan",
78
+ title: "💬 Chat Session",
79
+ titleAlignment: "center"
80
+ }
81
+ );
82
+
83
+
84
+ console.log(conversationInfo)
85
+
86
+ if (conversation.message?.length > 0) {
87
+ console.log(chalk.yellow(" Previous messages: \n"));
88
+ displayMessages(conversation.messages);
89
+ }
90
+
91
+ return conversation
92
+ }
93
+
94
+
95
+ function displayMessages(messages) {
96
+ messages.forEach(message => {
97
+ if (message.role === "user") {
98
+ const userBox = boxen(chalk.white(message.content), {
99
+ padding: 1,
100
+ margin: { top: 1, bottom: 1, left: 2, right: 2 },
101
+ borderStyle: "round",
102
+ borderColor: "cyan",
103
+ title: "You",
104
+ titleAlignment: "left"
105
+ })
106
+ console.log(userBox)
107
+ } else {
108
+ const renderContent = marked.parse(message.content)
109
+ const assistantBox = boxen(chalk.white(renderContent.trim()), {
110
+ padding: 1,
111
+ margin: { top: 1, bottom: 1, left: 2, right: 2 },
112
+ borderStyle: "round",
113
+ borderColor: "cyan",
114
+ title: "Assistant",
115
+ titleAlignment: "right"
116
+ })
117
+ console.log(assistantBox)
118
+ }
119
+ });
120
+ }
121
+
122
+
123
+ async function saveMessage(conversationId, role, content) {
124
+ const message = await chatService.createMessage(conversationId, role, content)
125
+ return message
126
+ }
127
+
128
+
129
+
130
+ async function getAIResponse(conversationId) {
131
+ const spinner = yoctoSpinner({ text: "Thinking...", color: "yellow" }).start();
132
+ const dbMessages = await chatService.getMessages(conversationId)
133
+ const aiMessages = chatService.formatMessagesForAI(dbMessages)
134
+
135
+ let fullResponse = ""
136
+
137
+ let isFirstChunk = true
138
+
139
+ try {
140
+
141
+ const result = await aiService.sendMessage(aiMessages, (chunk) => {
142
+ if (isFirstChunk) {
143
+
144
+ spinner.stop();
145
+ console.log("\n")
146
+ const header = chalk.green.bold("Assistant: ")
147
+ console.log(header)
148
+ console.log(chalk.gray("-".repeat(60)))
149
+ isFirstChunk = false
150
+
151
+ }
152
+
153
+ fullResponse += chunk
154
+ });
155
+
156
+
157
+ console.log("\n");
158
+ const renderedMarkedown = marked.parse(fullResponse)
159
+ console.log(renderedMarkedown)
160
+ console.log(chalk.gray("-".repeat(60)))
161
+ console.log("\n")
162
+
163
+ return result.content;
164
+
165
+ } catch (error) {
166
+ spinner.error("Error getting AI response:")
167
+ throw error
168
+ }
169
+ }
170
+
171
+
172
+
173
+
174
+ async function updateConversationTitle(conversationId, userInput, messageCount) {
175
+ if (messageCount === 1) {
176
+ const spinner = yoctoSpinner({ text: "Updating conversation title...." }).start();
177
+ const title = userInput.slice(0, 50) + (userInput.length > 50 ? "..." : "");
178
+ await chatService.updateConversationTitle(conversationId, title)
179
+ spinner.success("Conversation title updated")
180
+ }
181
+ }
182
+
183
+
184
+ const chatLoop = async (conversation) => {
185
+ try {
186
+
187
+ const helpBox = boxen(`${chalk.gray(' • Type your messgae and press enter')}\n${chalk.gray(' • Markdown formatting is supported in responses')}\n${chalk.gray('• Type "exit" to end the conversation')}\n${chalk.gray('• press Ctrl+C to exit')}`,
188
+ {
189
+ padding: 1,
190
+ margin: 1,
191
+ borderStyle: "round",
192
+ borderColor: "cyan",
193
+ title: "Help",
194
+ titleAlignment: "center",
195
+ dimBorder: true
196
+ })
197
+ console.log(helpBox)
198
+ while (true) {
199
+ const userInput = await text({
200
+ message: chalk.blue("Your Message...."),
201
+ placeholder: "Type your message here...",
202
+ validate(value) {
203
+ if (value.trim() === "") return "Message cannot be empty"
204
+ }
205
+
206
+ })
207
+
208
+ if (isCancel(userInput)) {
209
+ const exitBox = boxen(chalk.yellow("Chat session ended. GoodBye! "), {
210
+ padding: 1,
211
+ margin: 1,
212
+ borderStyle: "round",
213
+ borderColor: "cyan",
214
+ title: "GoodBye",
215
+ titleAlignment: "center",
216
+ dimBorder: true
217
+ })
218
+ console.log(exitBox)
219
+ process.exit(0)
220
+ }
221
+
222
+
223
+ if (userInput.trim().toLowerCase() === "exit") {
224
+ const exitBox = boxen(chalk.yellow("Chat session ended. GoodBye! "), {
225
+ padding: 1,
226
+ margin: 1,
227
+ borderStyle: "round",
228
+ borderColor: "cyan",
229
+ title: "GoodBye",
230
+ titleAlignment: "center",
231
+ dimBorder: true
232
+ })
233
+ console.log(exitBox)
234
+ break
235
+ }
236
+
237
+ await saveMessage(conversation.id, "user", userInput)
238
+
239
+ const message = await chatService.getMessages(conversation.id)
240
+
241
+ const aiResponse = await getAIResponse(conversation.id)
242
+
243
+ await saveMessage(conversation.id, "assistant", aiResponse)
244
+
245
+ await updateConversationTitle(conversation.id, userInput, message.length)
246
+
247
+ }
248
+
249
+ } catch (error) {
250
+ const errorBox = boxen(chalk.red(`Error: ${error.message}`), {
251
+ padding: 1,
252
+ margin: 1,
253
+ borderStyle: "round",
254
+ borderColor: "red",
255
+ title: "Error",
256
+ titleAlignment: "center",
257
+ dimBorder: true
258
+ })
259
+ console.log(errorBox)
260
+ process.exit(1)
261
+ }
262
+ }
263
+
264
+
265
+
266
+ export async function startChat(mode = "chat", conversationId = null) {
267
+ try {
268
+ intro(
269
+ boxen(chalk.bold.cyan("Maverick AI Chat"), {
270
+ padding: 1,
271
+ borderStyle: "double",
272
+ borderColor: "cyan"
273
+ })
274
+ )
275
+
276
+ const user = await getUserFromToken()
277
+ const conversation = await initConversation(user.id, conversationId, mode);
278
+ await chatLoop(conversation)
279
+
280
+ outro(chalk.green('⚡️ Thanks For Chatting.....'))
281
+
282
+ } catch (error) {
283
+ const errorBox = boxen(chalk.red(`Error: ${error.message}`), {
284
+ padding: 1,
285
+ margin: 1,
286
+ borderStyle: "round",
287
+ borderColor: "red",
288
+ })
289
+
290
+ }
291
+ }
292
+
293
+