maverick-ai-cli 3.0.2 → 3.0.3

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/README.md CHANGED
@@ -1,64 +1,73 @@
1
1
  # Maverick AI CLI
2
2
 
3
- Maverick is an intelligent command-line interface powered by AI, designed to be your coding companion directly in your terminal. It features a robust backend integration, secure authentication, and a powerful chat interface.
3
+ An intelligent command-line interface powered by AI your coding companion directly in your terminal. Authenticate via GitHub, chat with Gemini AI, and get intelligent responses with markdown rendering.
4
4
 
5
5
  ## Installation
6
6
 
7
- You can install Maverick globally using npm:
7
+ Install globally:
8
8
 
9
9
  ```bash
10
10
  npm install -g maverick-ai-cli
11
11
  ```
12
12
 
13
- ## Usage
13
+ Or run directly with `npx`:
14
14
 
15
- Once installed, you can use the `maverick` command to interact with the CLI.
15
+ ```bash
16
+ npx maverick-ai-cli <command>
17
+ ```
18
+
19
+ > **Prerequisites:** Node.js 18+ is required.
16
20
 
17
- ### Authentication
21
+ ## Commands
18
22
 
19
- Before using the AI features, you need to authenticate.
23
+ ### `login` Authenticate via GitHub
20
24
 
21
25
  ```bash
22
26
  maverick login
23
27
  ```
24
- This will open a browser window for you to sign in with your GitHub account.
25
28
 
26
- To check your current login status:
29
+ Opens a browser for GitHub OAuth device flow authentication. Your credentials are stored locally at `~/.better-auth/token.json`.
30
+
31
+ ### `whoami` — Check current user
32
+
27
33
  ```bash
28
34
  maverick whoami
29
35
  ```
30
36
 
31
- To logout:
37
+ ### `logout` — Sign out
38
+
32
39
  ```bash
33
40
  maverick logout
34
41
  ```
35
42
 
36
- ### AI Companion
37
-
38
- To start a chat session with Maverick:
43
+ ### `wakeup` — Start AI session
39
44
 
40
45
  ```bash
41
46
  maverick wakeup
42
47
  ```
43
48
 
44
- This will enter an interactive mode where you can:
45
- - **Chat**: Have a conversation with the AI.
46
- - **Tools**: (Coming soon) Execute tools and perform actions.
47
- - **Agent**: (Coming soon) Autonomous agent capabilities.
49
+ Select a mode:
50
+ - **Chat** Conversational AI with markdown rendering
51
+ - **Tool Calling** AI with Google Search & code execution *(coming soon)*
52
+ - **Agent** Autonomous AI agent *(coming soon)*
48
53
 
49
- Supported features in chat:
50
- - Markdown rendering in terminal.
51
- - Context-aware conversations.
52
- - Persistent session history.
54
+ ### Chat Features
55
+
56
+ - Markdown rendering in terminal
57
+ - Context-aware conversations
58
+ - Persistent session history
59
+ - Streaming AI responses
53
60
 
54
61
  ## Development
55
62
 
56
- If you want to contribute or run the server locally:
63
+ ### Server Setup
64
+
65
+ The backend server powers authentication and data storage. It's deployed at `https://maverick-cli.onrender.com`.
57
66
 
58
67
  1. **Clone the repository:**
59
68
  ```bash
60
69
  git clone https://github.com/CodeMaverick-143/maverick-cli.git
61
- cd CLI/server
70
+ cd maverick-cli/server
62
71
  ```
63
72
 
64
73
  2. **Install dependencies:**
@@ -66,18 +75,20 @@ If you want to contribute or run the server locally:
66
75
  npm install
67
76
  ```
68
77
 
69
- 3. **Set up Environment Variables:**
70
- Create a `.env` file in the `server` directory with the following:
78
+ 3. **Environment Variables:**
79
+ Create a `.env` file in the `server` directory:
71
80
  ```env
72
81
  DATABASE_URL="postgresql://..."
73
- GITHUB_CLIENT_ID="your_client_id"
74
- GITHUB_CLIENT_SECRET="your_client_secret"
75
- BETTER_AUTH_SECRET="your_auth_secret"
76
- BETTER_AUTH_URL="https://maverick-cli.onrender.com"
77
- GOOGLE_API_KEY="your_gemini_api_key"
82
+ GITHUB_CLIENT_ID="your_github_oauth_app_client_id"
83
+ GITHUB_CLIENT_SECRET="your_github_oauth_app_client_secret"
84
+ BETTER_AUTH_SECRET="your_random_auth_secret"
85
+ BETTER_AUTH_URL="http://localhost:3005"
86
+ GOOGLE_GENERATIVE_AI_API_KEY="your_gemini_api_key"
87
+ ORBITAL_MODEL="gemini-2.5-flash"
88
+ PORT=3005
78
89
  ```
79
90
 
80
- 4. **Run Database Migrations:**
91
+ 4. **Database Setup:**
81
92
  ```bash
82
93
  npx prisma generate
83
94
  npx prisma migrate dev
@@ -88,5 +99,57 @@ If you want to contribute or run the server locally:
88
99
  npm run dev
89
100
  ```
90
101
 
91
- ## Licenses
102
+ ### Architecture
103
+
104
+ ```
105
+ server/
106
+ ├── src/
107
+ │ ├── index.js # Express server + API endpoints
108
+ │ ├── cli/
109
+ │ │ ├── main.js # CLI entry point (Commander.js)
110
+ │ │ ├── commands/
111
+ │ │ │ ├── auth/login.js # login, logout, whoami
112
+ │ │ │ └── ai/wakeUp.js # wakeup command
113
+ │ │ ├── chat/
114
+ │ │ │ └── chat-with-ai.js # AI chat loop
115
+ │ │ └── ai/
116
+ │ │ └── google-service.js # Gemini AI SDK
117
+ │ ├── lib/
118
+ │ │ ├── api-client.js # CLI → Server HTTP client
119
+ │ │ ├── auth.js # Better Auth config (server)
120
+ │ │ ├── db.js # Prisma client (server)
121
+ │ │ └── token.js # Token storage (~/.better-auth/)
122
+ │ ├── config/
123
+ │ │ ├── google.config.js # AI model config
124
+ │ │ └── tool.config.js # Tool definitions
125
+ │ └── service/
126
+ │ └── chat.service.js # Chat DB service (server)
127
+ └── prisma/
128
+ └── schema.prisma # Database schema
129
+ ```
130
+
131
+ ### API Endpoints
132
+
133
+ | Endpoint | Method | Description |
134
+ |---|---|---|
135
+ | `/api/auth/*` | ALL | Better Auth routes |
136
+ | `/api/me` | GET | Get current session |
137
+ | `/api/cli/user` | GET | Get user from Bearer token |
138
+ | `/api/cli/conversations` | POST | Create conversation |
139
+ | `/api/cli/conversations/:id` | GET | Get conversation + messages |
140
+ | `/api/cli/conversations/:id/title` | PUT | Update title |
141
+ | `/api/cli/conversations/:id/messages` | GET | List messages |
142
+ | `/api/cli/conversations/:id/messages` | POST | Create message |
143
+
144
+ ## Tech Stack
145
+
146
+ - **Runtime:** Node.js
147
+ - **AI:** Google Gemini via Vercel AI SDK
148
+ - **Auth:** Better Auth (GitHub OAuth + Device Flow)
149
+ - **Database:** PostgreSQL + Prisma ORM
150
+ - **Server:** Express 5
151
+ - **CLI:** Commander.js, Clack prompts, Chalk
152
+
153
+ ## License
154
+
92
155
  ISC
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "maverick-ai-cli",
3
- "version": "3.0.2",
3
+ "version": "3.0.3",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -9,8 +9,7 @@
9
9
  "scripts": {
10
10
  "test": "echo \"Error: no test specified\" && exit 1",
11
11
  "start": "node src/index.js",
12
- "dev": "nodemon src/index.js",
13
- "postinstall": "prisma generate"
12
+ "dev": "nodemon src/index.js"
14
13
  },
15
14
  "keywords": [],
16
15
  "author": "CodeMaverick-143 AKA Arpit Sarang",
@@ -5,10 +5,7 @@ import yoctoSpinner from "yocto-spinner";
5
5
  import { marked } from "marked";
6
6
  import { markedTerminal } from "marked-terminal"
7
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";
8
+ import { apiClient } from "../../lib/api-client.js";
12
9
 
13
10
  marked.use(
14
11
  markedTerminal({
@@ -31,41 +28,36 @@ marked.use(
31
28
 
32
29
 
33
30
  const aiService = new AIService();
34
- const chatService = new ChatService();
35
31
 
36
32
 
37
33
  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
34
  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")
35
+ try {
36
+ const user = await apiClient.getUser();
37
+ spinner.success(`Welcome back, ${user.name}!`)
38
+ return user;
39
+ } catch (err) {
40
+ spinner.error("Authentication failed");
41
+ throw err;
54
42
  }
55
-
56
- spinner.success(`Welcome back, ${user.name}!`)
57
- return user;
58
43
  }
59
44
 
60
45
 
61
46
  async function initConversation(userId, conversationId = null, mode = "chat") {
62
47
  const spinner = yoctoSpinner({ text: "Loading conversation...." }).start()
63
48
 
64
- const conversation = await chatService.getOrCreateConversation(
65
- userId,
66
- conversationId,
67
- mode
68
- )
49
+ let conversation;
50
+ if (conversationId) {
51
+ try {
52
+ conversation = await apiClient.getConversation(conversationId);
53
+ } catch {
54
+ // conversation not found, create new one
55
+ }
56
+ }
57
+
58
+ if (!conversation) {
59
+ conversation = await apiClient.createConversation(mode);
60
+ }
69
61
 
70
62
  spinner.success("Conversation Loaded")
71
63
 
@@ -83,7 +75,7 @@ async function initConversation(userId, conversationId = null, mode = "chat") {
83
75
 
84
76
  console.log(conversationInfo)
85
77
 
86
- if (conversation.message?.length > 0) {
78
+ if (conversation.messages?.length > 0) {
87
79
  console.log(chalk.yellow(" Previous messages: \n"));
88
80
  displayMessages(conversation.messages);
89
81
  }
@@ -121,16 +113,18 @@ function displayMessages(messages) {
121
113
 
122
114
 
123
115
  async function saveMessage(conversationId, role, content) {
124
- const message = await chatService.createMessage(conversationId, role, content)
125
- return message
116
+ return await apiClient.createMessage(conversationId, role, content);
126
117
  }
127
118
 
128
119
 
129
120
 
130
121
  async function getAIResponse(conversationId) {
131
122
  const spinner = yoctoSpinner({ text: "Thinking...", color: "yellow" }).start();
132
- const dbMessages = await chatService.getMessages(conversationId)
133
- const aiMessages = chatService.formatMessagesForAI(dbMessages)
123
+ const dbMessages = await apiClient.getMessages(conversationId);
124
+ const aiMessages = dbMessages.map(msg => ({
125
+ role: msg.role,
126
+ content: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content)
127
+ }));
134
128
 
135
129
  let fullResponse = ""
136
130
 
@@ -175,7 +169,7 @@ async function updateConversationTitle(conversationId, userInput, messageCount)
175
169
  if (messageCount === 1) {
176
170
  const spinner = yoctoSpinner({ text: "Updating conversation title...." }).start();
177
171
  const title = userInput.slice(0, 50) + (userInput.length > 50 ? "..." : "");
178
- await chatService.updateConversationTitle(conversationId, title)
172
+ await apiClient.updateTitle(conversationId, title);
179
173
  spinner.success("Conversation title updated")
180
174
  }
181
175
  }
@@ -236,13 +230,13 @@ const chatLoop = async (conversation) => {
236
230
 
237
231
  await saveMessage(conversation.id, "user", userInput)
238
232
 
239
- const message = await chatService.getMessages(conversation.id)
233
+ const messages = await apiClient.getMessages(conversation.id)
240
234
 
241
235
  const aiResponse = await getAIResponse(conversation.id)
242
236
 
243
237
  await saveMessage(conversation.id, "assistant", aiResponse)
244
238
 
245
- await updateConversationTitle(conversation.id, userInput, message.length)
239
+ await updateConversationTitle(conversation.id, userInput, messages.length)
246
240
 
247
241
  }
248
242
 
@@ -289,5 +283,3 @@ export async function startChat(mode = "chat", conversationId = null) {
289
283
 
290
284
  }
291
285
  }
292
-
293
-
@@ -2,7 +2,7 @@ import chalk from "chalk";
2
2
  import { Command } from "commander";
3
3
  import yoctoSpinner from "yocto-spinner"
4
4
  import { requireAuth } from "../../../lib/token.js";
5
- import prisma from "../../../lib/db.js";
5
+ import { apiClient } from "../../../lib/api-client.js";
6
6
  import { select } from "@clack/prompts";
7
7
  import { startChat } from "../../chat/chat-with-ai.js";
8
8
 
@@ -13,22 +13,7 @@ const wakeUpAction = async () => {
13
13
  spinner.start()
14
14
 
15
15
 
16
- const user = await prisma.user.findFirst({
17
- where: {
18
- sessions: {
19
- some: {
20
- token: token.access_token
21
- }
22
- }
23
- },
24
- select: {
25
- id: true,
26
- name: true,
27
- email: true,
28
- image: true
29
- }
30
-
31
- });
16
+ const user = await apiClient.getUser();
32
17
 
33
18
  spinner.stop()
34
19
 
@@ -14,7 +14,7 @@ import yoctoSpinner from "yocto-spinner";
14
14
  import * as z from "zod";
15
15
  import dotenv from "dotenv";
16
16
  import { clearStoredToken, getStoredToken, isTokenExpired, requireAuth, storeToken } from "../../../lib/token.js";
17
- import prisma from "../../../lib/db.js";
17
+ import { apiClient } from "../../../lib/api-client.js";
18
18
 
19
19
  dotenv.config();
20
20
 
@@ -250,21 +250,7 @@ export async function whoamiAction(opts) {
250
250
  process.exit(1)
251
251
  }
252
252
 
253
- const user = await prisma.user.findFirst({
254
- where: {
255
- sessions: {
256
- some: {
257
- token: token.access_token,
258
- }
259
- }
260
- },
261
- select: {
262
- id: true,
263
- name: true,
264
- email: true,
265
- image: true
266
- },
267
- })
253
+ const user = await apiClient.getUser();
268
254
 
269
255
  console.log(
270
256
  chalk.bold.greenBright(`\n USER: ${user.name}
package/src/index.js CHANGED
@@ -44,6 +44,135 @@ app.get('/health', (req, res) => {
44
44
  res.send("Backend is running")
45
45
  })
46
46
 
47
+
48
+ async function authenticateToken(req, res, next) {
49
+ const authHeader = req.headers.authorization;
50
+ const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
51
+
52
+ if (!token) {
53
+ return res.status(401).json({ error: "Missing authorization token" });
54
+ }
55
+
56
+ try {
57
+ const { default: prisma } = await import("./lib/db.js");
58
+ const user = await prisma.user.findFirst({
59
+ where: {
60
+ sessions: {
61
+ some: { token }
62
+ }
63
+ },
64
+ select: { id: true, name: true, email: true, image: true }
65
+ });
66
+
67
+ if (!user) {
68
+ return res.status(401).json({ error: "Invalid or expired token" });
69
+ }
70
+
71
+ req.user = user;
72
+ next();
73
+ } catch (err) {
74
+ console.error("Auth middleware error:", err.message);
75
+ return res.status(500).json({ error: "Authentication failed" });
76
+ }
77
+ }
78
+
79
+ // GET /api/cli/user — get current user from token
80
+ app.get("/api/cli/user", authenticateToken, (req, res) => {
81
+ res.json(req.user);
82
+ });
83
+
84
+ // POST /api/cli/conversations — create a conversation
85
+ app.post("/api/cli/conversations", authenticateToken, async (req, res) => {
86
+ try {
87
+ const { default: prisma } = await import("./lib/db.js");
88
+ const { mode = "chat", title } = req.body || {};
89
+ const conversation = await prisma.conversation.create({
90
+ data: {
91
+ userId: req.user.id,
92
+ mode,
93
+ title: title || `New ${mode} conversation`
94
+ }
95
+ });
96
+ res.json(conversation);
97
+ } catch (err) {
98
+ console.error("Create conversation error:", err.message);
99
+ res.status(500).json({ error: "Failed to create conversation" });
100
+ }
101
+ });
102
+
103
+ // GET /api/cli/conversations/:id — get a conversation with messages
104
+ app.get("/api/cli/conversations/:id", authenticateToken, async (req, res) => {
105
+ try {
106
+ const { default: prisma } = await import("./lib/db.js");
107
+ const conversation = await prisma.conversation.findFirst({
108
+ where: { id: req.params.id, userId: req.user.id },
109
+ include: {
110
+ messages: { orderBy: { createdAt: "asc" } }
111
+ }
112
+ });
113
+
114
+ if (!conversation) {
115
+ return res.status(404).json({ error: "Conversation not found" });
116
+ }
117
+
118
+ res.json(conversation);
119
+ } catch (err) {
120
+ console.error("Get conversation error:", err.message);
121
+ res.status(500).json({ error: "Failed to get conversation" });
122
+ }
123
+ });
124
+
125
+ // PUT /api/cli/conversations/:id/title — update conversation title
126
+ app.put("/api/cli/conversations/:id/title", authenticateToken, async (req, res) => {
127
+ try {
128
+ const { default: prisma } = await import("./lib/db.js");
129
+ const { title } = req.body;
130
+ const conversation = await prisma.conversation.update({
131
+ where: { id: req.params.id },
132
+ data: { title }
133
+ });
134
+ res.json(conversation);
135
+ } catch (err) {
136
+ console.error("Update title error:", err.message);
137
+ res.status(500).json({ error: "Failed to update title" });
138
+ }
139
+ });
140
+
141
+ // GET /api/cli/conversations/:id/messages — get messages
142
+ app.get("/api/cli/conversations/:id/messages", authenticateToken, async (req, res) => {
143
+ try {
144
+ const { default: prisma } = await import("./lib/db.js");
145
+ const messages = await prisma.message.findMany({
146
+ where: { conversationId: req.params.id },
147
+ orderBy: { createdAt: "asc" }
148
+ });
149
+ res.json(messages);
150
+ } catch (err) {
151
+ console.error("Get messages error:", err.message);
152
+ res.status(500).json({ error: "Failed to get messages" });
153
+ }
154
+ });
155
+
156
+ // POST /api/cli/conversations/:id/messages — create a message
157
+ app.post("/api/cli/conversations/:id/messages", authenticateToken, async (req, res) => {
158
+ try {
159
+ const { default: prisma } = await import("./lib/db.js");
160
+ const { role, content } = req.body;
161
+ const contentStr = typeof content === "string" ? content : JSON.stringify(content);
162
+ const message = await prisma.message.create({
163
+ data: {
164
+ conversationId: req.params.id,
165
+ role,
166
+ content: contentStr
167
+ }
168
+ });
169
+ res.json(message);
170
+ } catch (err) {
171
+ console.error("Create message error:", err.message);
172
+ res.status(500).json({ error: "Failed to create message" });
173
+ }
174
+ });
175
+
47
176
  app.listen(process.env.PORT, () => {
48
177
  console.log(`running on PORT : http://localhost:${process.env.PORT}`)
49
178
  })
@@ -0,0 +1,63 @@
1
+ import { getStoredToken } from "./token.js";
2
+
3
+ const BASE_URL = "https://maverick-cli.onrender.com";
4
+
5
+ async function getAuthHeaders() {
6
+ const token = await getStoredToken();
7
+ if (!token?.access_token) {
8
+ throw new Error("Not authenticated. Please run 'maverick login' first.");
9
+ }
10
+ return {
11
+ "Authorization": `Bearer ${token.access_token}`,
12
+ "Content-Type": "application/json"
13
+ };
14
+ }
15
+
16
+ async function request(method, path, body = null) {
17
+ const headers = await getAuthHeaders();
18
+ const options = { method, headers };
19
+ if (body) {
20
+ options.body = JSON.stringify(body);
21
+ }
22
+
23
+ const res = await fetch(`${BASE_URL}${path}`, options);
24
+
25
+ if (!res.ok) {
26
+ const err = await res.json().catch(() => ({ error: res.statusText }));
27
+ throw new Error(err.error || `API error: ${res.status}`);
28
+ }
29
+
30
+ return res.json();
31
+ }
32
+
33
+ export const apiClient = {
34
+ /** Get current user from token */
35
+ async getUser() {
36
+ return request("GET", "/api/cli/user");
37
+ },
38
+
39
+ /** Create a new conversation */
40
+ async createConversation(mode = "chat", title = null) {
41
+ return request("POST", "/api/cli/conversations", { mode, title });
42
+ },
43
+
44
+ /** Get a conversation by ID (with messages) */
45
+ async getConversation(conversationId) {
46
+ return request("GET", `/api/cli/conversations/${conversationId}`);
47
+ },
48
+
49
+ /** Update conversation title */
50
+ async updateTitle(conversationId, title) {
51
+ return request("PUT", `/api/cli/conversations/${conversationId}/title`, { title });
52
+ },
53
+
54
+ /** Get messages for a conversation */
55
+ async getMessages(conversationId) {
56
+ return request("GET", `/api/cli/conversations/${conversationId}/messages`);
57
+ },
58
+
59
+ /** Create a message in a conversation */
60
+ async createMessage(conversationId, role, content) {
61
+ return request("POST", `/api/cli/conversations/${conversationId}/messages`, { role, content });
62
+ }
63
+ };