mikasa-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,219 @@
1
+ import chalk from "chalk";
2
+ import boxen from "boxen";
3
+ import { text, isCancel, cancel, intro, outro, confirm } from "@clack/prompts";
4
+ import { AIService } from "../ai/google-service.js";
5
+ import { ChatService } from "../../services/chat.services.js";
6
+ // import ChatService from "../ai/google-service.js";
7
+ import { getStoredToken } from "../commands/auth/login.js";
8
+ import prisma from "../../lib/db.js";
9
+ import { generateApplication } from "../../config/agent.config.js";
10
+
11
+ const aiService = new AIService();
12
+ const chatService = new ChatService();
13
+
14
+ async function getUserFromToken() {
15
+ const token = await getStoredToken();
16
+
17
+ if (!token?.access_token) {
18
+ throw new Error("Not authenticated. Please run 'mikasa login' first.");
19
+ }
20
+
21
+ const user = await prisma.user.findFirst({
22
+ where: {
23
+ sessions: {
24
+ some: { token: token.access_token },
25
+ },
26
+ },
27
+ });
28
+
29
+ if (!user) {
30
+ throw new Error("User not found. Please login again.");
31
+ }
32
+
33
+ console.log(chalk.green(`\nāœ“ Welcome back, ${user.name}!\n`));
34
+ return user;
35
+ }
36
+
37
+ async function initConversation(userId, conversationId = null) {
38
+ const conversation = await chatService.getOrCreateConversation(
39
+ userId,
40
+ conversationId,
41
+ "agent"
42
+ );
43
+
44
+ const conversationInfo = boxen(
45
+ `${chalk.bold("Conversation")}: ${conversation.title}\n` +
46
+ `${chalk.gray("ID:")} ${conversation.id}\n` +
47
+ `${chalk.gray("Mode:")} ${chalk.magenta("Agent (Code Generator)")}\n` +
48
+ `${chalk.cyan("Working Directory:")} ${process.cwd()}`,
49
+ {
50
+ padding: 1,
51
+ margin: { top: 1, bottom: 1 },
52
+ borderStyle: "round",
53
+ borderColor: "magenta",
54
+ title: "šŸ¤– Agent Mode",
55
+ titleAlignment: "center",
56
+ }
57
+ );
58
+
59
+ console.log(conversationInfo);
60
+
61
+ return conversation;
62
+ }
63
+
64
+ async function saveMessage(conversationId, role, content) {
65
+ return await chatService.addMessage(conversationId, role, content);
66
+ }
67
+
68
+ async function agentLoop(conversation) {
69
+ const helpBox = boxen(
70
+ `${chalk.cyan.bold("What can the agent do?")}\n\n` +
71
+ `${chalk.gray('• Generate complete applications from descriptions')}\n` +
72
+ `${chalk.gray('• Create all necessary files and folders')}\n` +
73
+ `${chalk.gray('• Include setup instructions and commands')}\n` +
74
+ `${chalk.gray('• Generate production-ready code')}\n\n` +
75
+ `${chalk.yellow.bold("Examples:")}\n` +
76
+ `${chalk.white('• "Build a todo app with React and Tailwind"')}\n` +
77
+ `${chalk.white('• "Create a REST API with Express and MongoDB"')}\n` +
78
+ `${chalk.white('• "Make a weather app using OpenWeatherMap API"')}\n\n` +
79
+ `${chalk.gray('Type "exit" to end the session')}`,
80
+ {
81
+ padding: 1,
82
+ margin: { bottom: 1 },
83
+ borderStyle: "round",
84
+ borderColor: "cyan",
85
+ title: "šŸ’” Agent Instructions",
86
+ }
87
+ );
88
+
89
+ console.log(helpBox);
90
+
91
+ while (true) {
92
+ const userInput = await text({
93
+ message: chalk.magenta("šŸ¤– What would you like to build?"),
94
+ placeholder: "Describe your application...",
95
+ validate(value) {
96
+ if (!value || value.trim().length === 0) {
97
+ return "Description cannot be empty";
98
+ }
99
+ if (value.trim().length < 10) {
100
+ return "Please provide more details (at least 10 characters)";
101
+ }
102
+ },
103
+ });
104
+
105
+ if (isCancel(userInput)) {
106
+ console.log(chalk.yellow("\nšŸ‘‹ Agent session cancelled\n"));
107
+ process.exit(0);
108
+ }
109
+
110
+ if (userInput.toLowerCase() === "exit") {
111
+ console.log(chalk.yellow("\nšŸ‘‹ Agent session ended\n"));
112
+ break;
113
+ }
114
+
115
+ const userBox = boxen(chalk.white(userInput), {
116
+ padding: 1,
117
+ margin: { top: 1, bottom: 1 },
118
+ borderStyle: "round",
119
+ borderColor: "blue",
120
+ title: "šŸ‘¤ Your Request",
121
+ titleAlignment: "left",
122
+ });
123
+ console.log(userBox);
124
+
125
+ // Save user message
126
+ await saveMessage(conversation.id, "user", userInput);
127
+
128
+ try {
129
+ // Generate application using structured output
130
+ const result = await generateApplication(
131
+ userInput,
132
+ aiService,
133
+ process.cwd()
134
+ );
135
+
136
+ if (result && result.success) {
137
+ // Save successful generation details
138
+ const responseMessage = `Generated application: ${result.folderName}\n` +
139
+ `Files created: ${result.files.length}\n` +
140
+ `Location: ${result.appDir}\n\n` +
141
+ `Setup commands:\n${result.commands.join('\n')}`;
142
+
143
+ await saveMessage(conversation.id, "assistant", responseMessage);
144
+
145
+ // Ask if user wants to generate another app
146
+ const continuePrompt = await confirm({
147
+ message: chalk.cyan("Would you like to generate another application?"),
148
+ initialValue: false,
149
+ });
150
+
151
+ if (isCancel(continuePrompt) || !continuePrompt) {
152
+ console.log(chalk.yellow("\nšŸ‘‹ Great! Check your new application.\n"));
153
+ break;
154
+ }
155
+
156
+ } else {
157
+ throw new Error("Generation returned no result");
158
+ }
159
+
160
+ } catch (error) {
161
+ console.log(chalk.red(`\nāŒ Error: ${error.message}\n`));
162
+
163
+ await saveMessage(conversation.id, "assistant", `Error: ${error.message}`);
164
+
165
+ const retry = await confirm({
166
+ message: chalk.cyan("Would you like to try again?"),
167
+ initialValue: true,
168
+ });
169
+
170
+ if (isCancel(retry) || !retry) {
171
+ break;
172
+ }
173
+ }
174
+ }
175
+ }
176
+
177
+ export async function startAgentChat(conversationId = null) {
178
+ try {
179
+ intro(
180
+ boxen(
181
+ chalk.bold.magenta("šŸ¤– Mikasa AI - Agent Mode\n\n") +
182
+ chalk.gray("Autonomous Application Generator"),
183
+ {
184
+ padding: 1,
185
+ borderStyle: "double",
186
+ borderColor: "magenta",
187
+ }
188
+ )
189
+ );
190
+
191
+ const user = await getUserFromToken();
192
+
193
+ // Warning about file system access
194
+ const shouldContinue = await confirm({
195
+ message: chalk.yellow("āš ļø The agent will create files and folders in the current directory. Continue?"),
196
+ initialValue: true,
197
+ });
198
+
199
+ if (isCancel(shouldContinue) || !shouldContinue) {
200
+ cancel(chalk.yellow("Agent mode cancelled"));
201
+ process.exit(0);
202
+ }
203
+
204
+ const conversation = await initConversation(user.id, conversationId);
205
+ await agentLoop(conversation);
206
+
207
+ outro(chalk.green.bold("\n✨ Thanks for using Agent Mode!"));
208
+
209
+ } catch (error) {
210
+ const errorBox = boxen(chalk.red(`āŒ Error: ${error.message}`), {
211
+ padding: 1,
212
+ margin: 1,
213
+ borderStyle: "round",
214
+ borderColor: "red",
215
+ });
216
+ console.log(errorBox);
217
+ process.exit(1);
218
+ }
219
+ }
@@ -0,0 +1,377 @@
1
+ import chalk from "chalk";
2
+ import boxen from "boxen";
3
+ import { text, isCancel, cancel, intro, outro, multiselect } 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 "../../services/chat.services.js";
9
+ // import ChatService from "../ai/google-service.js";
10
+ import { getStoredToken } from "../commands/auth/login.js";
11
+ import prisma from "../../lib/db.js";
12
+ import {
13
+ availableTools,
14
+ getEnabledTools,
15
+ enableTools,
16
+ getEnabledToolNames,
17
+ resetTools
18
+ } from "../../config/tool.config.js";
19
+
20
+ // Configure marked for terminal
21
+ marked.use(
22
+ markedTerminal({
23
+ code: chalk.cyan,
24
+ blockquote: chalk.gray.italic,
25
+ heading: chalk.green.bold,
26
+ firstHeading: chalk.magenta.underline.bold,
27
+ hr: chalk.reset,
28
+ listitem: chalk.reset,
29
+ list: chalk.reset,
30
+ paragraph: chalk.reset,
31
+ strong: chalk.bold,
32
+ em: chalk.italic,
33
+ codespan: chalk.yellow.bgBlack,
34
+ del: chalk.dim.gray.strikethrough,
35
+ link: chalk.blue.underline,
36
+ href: chalk.blue.underline,
37
+ })
38
+ );
39
+
40
+ const aiService = new AIService();
41
+ const chatService = new ChatService();
42
+
43
+ async function getUserFromToken() {
44
+ const token = await getStoredToken();
45
+
46
+ if (!token?.access_token) {
47
+ throw new Error("Not authenticated. Please run 'Mikasa login' first.");
48
+ }
49
+
50
+ const spinner = yoctoSpinner({ text: "Authenticating..." }).start();
51
+
52
+ const user = await prisma.user.findFirst({
53
+ where: {
54
+ sessions: {
55
+ some: { token: token.access_token },
56
+ },
57
+ },
58
+ });
59
+
60
+ if (!user) {
61
+ spinner.error("User not found");
62
+ throw new Error("User not found. Please login again.");
63
+ }
64
+
65
+ spinner.success(`Welcome back, ${user.name}!`);
66
+ return user;
67
+ }
68
+
69
+ async function selectTools() {
70
+ const toolOptions = availableTools.map(tool => ({
71
+ value: tool.id,
72
+ label: tool.name,
73
+ hint: tool.description,
74
+ }));
75
+
76
+ const selectedTools = await multiselect({
77
+ message: chalk.cyan("Select tools to enable (Space to select, Enter to confirm):"),
78
+ options: toolOptions,
79
+ required: false,
80
+ });
81
+
82
+ if (isCancel(selectedTools)) {
83
+ cancel(chalk.yellow("Tool selection cancelled"));
84
+ process.exit(0);
85
+ }
86
+
87
+ // Enable selected tools
88
+ enableTools(selectedTools);
89
+
90
+ if (selectedTools.length === 0) {
91
+ console.log(chalk.yellow("\nāš ļø No tools selected. Mikasa will work without tools.\n"));
92
+ } else {
93
+ const toolsBox = boxen(
94
+ chalk.green(`āœ… Enabled tools:\n${selectedTools.map(id => {
95
+ const tool = availableTools.find(t => t.id === id);
96
+ return ` • ${tool.name}`;
97
+ }).join('\n')}`),
98
+ {
99
+ padding: 1,
100
+ margin: { top: 1, bottom: 1 },
101
+ borderStyle: "round",
102
+ borderColor: "green",
103
+ title: "šŸ› ļø Active Tools",
104
+ titleAlignment: "center",
105
+ }
106
+ );
107
+ console.log(toolsBox);
108
+ }
109
+
110
+ return selectedTools.length > 0;
111
+ }
112
+
113
+ async function initConversation(userId, conversationId = null, mode = "tool") {
114
+ const spinner = yoctoSpinner({ text: "Loading conversation..." }).start();
115
+
116
+ const conversation = await chatService.getOrCreateConversation(
117
+ userId,
118
+ conversationId,
119
+ mode
120
+ );
121
+
122
+ spinner.success("Conversation loaded");
123
+
124
+ // Get enabled tool names for display
125
+ const enabledToolNames = getEnabledToolNames();
126
+ const toolsDisplay = enabledToolNames.length > 0
127
+ ? `\n${chalk.gray("Active Tools:")} ${enabledToolNames.join(", ")}`
128
+ : `\n${chalk.gray("No tools enabled")}`;
129
+
130
+ // Display conversation info in a box
131
+ const conversationInfo = boxen(
132
+ `${chalk.bold("Conversation")}: ${conversation.title}\n${chalk.gray("ID: " + conversation.id)}\n${chalk.gray("Mode: " + conversation.mode)}${toolsDisplay}`,
133
+ {
134
+ padding: 1,
135
+ margin: { top: 1, bottom: 1 },
136
+ borderStyle: "round",
137
+ borderColor: "cyan",
138
+ title: "šŸ’¬ Tool Calling Session",
139
+ titleAlignment: "center",
140
+ }
141
+ );
142
+
143
+ console.log(conversationInfo);
144
+
145
+ // Display existing messages if any
146
+ if (conversation.messages?.length > 0) {
147
+ console.log(chalk.yellow("šŸ“œ Previous messages:\n"));
148
+ displayMessages(conversation.messages);
149
+ }
150
+
151
+ return conversation;
152
+ }
153
+
154
+ function displayMessages(messages) {
155
+ messages.forEach((msg) => {
156
+ if (msg.role === "user") {
157
+ const userBox = boxen(chalk.white(msg.content), {
158
+ padding: 1,
159
+ margin: { left: 2, bottom: 1 },
160
+ borderStyle: "round",
161
+ borderColor: "blue",
162
+ title: "šŸ‘¤ You",
163
+ titleAlignment: "left",
164
+ });
165
+ console.log(userBox);
166
+ } else if (msg.role === "assistant") {
167
+ const renderedContent = marked.parse(msg.content);
168
+ const assistantBox = boxen(renderedContent.trim(), {
169
+ padding: 1,
170
+ margin: { left: 2, bottom: 1 },
171
+ borderStyle: "round",
172
+ borderColor: "green",
173
+ title: "šŸ¤– Assistant (with tools)",
174
+ titleAlignment: "left",
175
+ });
176
+ console.log(assistantBox);
177
+ }
178
+ });
179
+ }
180
+
181
+ async function saveMessage(conversationId, role, content) {
182
+ return await chatService.addMessage(conversationId, role, content);
183
+ }
184
+
185
+ async function getAIResponse(conversationId) {
186
+ const spinner = yoctoSpinner({
187
+ text: "Mikasa was thinking...",
188
+ color: "cyan"
189
+ }).start();
190
+
191
+ const dbMessages = await chatService.getMessages(conversationId);
192
+ const aiMessages = chatService.formatMessagesForAI(dbMessages);
193
+
194
+ const tools = getEnabledTools();
195
+
196
+ let fullResponse = "";
197
+ let isFirstChunk = true;
198
+ const toolCallsDetected = [];
199
+
200
+ try {
201
+ // IMPORTANT: Pass tools in the streamText config
202
+ const result = await aiService.sendMessage(
203
+ aiMessages,
204
+ (chunk) => {
205
+ if (isFirstChunk) {
206
+ spinner.stop();
207
+ console.log("\n");
208
+ const header = chalk.green.bold("šŸ¤– Assistant:");
209
+ console.log(header);
210
+ console.log(chalk.gray("─".repeat(60)));
211
+ isFirstChunk = false;
212
+ }
213
+ fullResponse += chunk;
214
+ },
215
+ tools,
216
+ (toolCall) => {
217
+ toolCallsDetected.push(toolCall);
218
+ }
219
+ );
220
+
221
+ // Display tool calls if any
222
+ if (toolCallsDetected.length > 0) {
223
+ console.log("\n");
224
+ const toolCallBox = boxen(
225
+ toolCallsDetected.map(tc =>
226
+ `${chalk.cyan("šŸ”§ Tool:")} ${tc.toolName}\n${chalk.gray("Args:")} ${JSON.stringify(tc.args, null, 2)}`
227
+ ).join("\n\n"),
228
+ {
229
+ padding: 1,
230
+ margin: 1,
231
+ borderStyle: "round",
232
+ borderColor: "cyan",
233
+ title: "šŸ› ļø Tool Calls",
234
+ }
235
+ );
236
+ console.log(toolCallBox);
237
+ }
238
+
239
+ // Display tool results if any
240
+ if (result.toolResults && result.toolResults.length > 0) {
241
+ const toolResultBox = boxen(
242
+ result.toolResults.map(tr =>
243
+ `${chalk.green("āœ… Tool:")} ${tr.toolName}\n${chalk.gray("Result:")} ${JSON.stringify(tr.result, null, 2).slice(0, 200)}...`
244
+ ).join("\n\n"),
245
+ {
246
+ padding: 1,
247
+ margin: 1,
248
+ borderStyle: "round",
249
+ borderColor: "green",
250
+ title: "šŸ“Š Tool Results",
251
+ }
252
+ );
253
+ console.log(toolResultBox);
254
+ }
255
+
256
+ // Render markdown response
257
+ console.log("\n");
258
+ const renderedMarkdown = marked.parse(fullResponse);
259
+ console.log(renderedMarkdown);
260
+ console.log(chalk.gray("─".repeat(60)));
261
+ console.log("\n");
262
+
263
+ return result.content;
264
+ } catch (error) {
265
+ spinner.error("Failed to get Mikasa response");
266
+ throw error;
267
+ }
268
+ }
269
+
270
+
271
+ async function updateConversationTitle(conversationId, userInput, messageCount) {
272
+ if (messageCount === 1) {
273
+ const title = userInput.slice(0, 50) + (userInput.length > 50 ? "..." : "");
274
+ await chatService.updateTitle(conversationId, title);
275
+ }
276
+ }
277
+
278
+ async function chatLoop(conversation) {
279
+ const enabledToolNames = getEnabledToolNames();
280
+ const helpBox = boxen(
281
+ `${chalk.gray('• Type your message and press Enter')}\n${chalk.gray('• Mikasa has access to:')} ${enabledToolNames.length > 0 ? enabledToolNames.join(", ") : "No tools"}\n${chalk.gray('• Type "exit" to end conversation')}\n${chalk.gray('• Press Ctrl+C to quit anytime')}`,
282
+ {
283
+ padding: 1,
284
+ margin: { bottom: 1 },
285
+ borderStyle: "round",
286
+ borderColor: "gray",
287
+ dimBorder: true,
288
+ }
289
+ );
290
+
291
+ console.log(helpBox);
292
+
293
+ while (true) {
294
+ const userInput = await text({
295
+ message: chalk.blue("šŸ’¬ Your message"),
296
+ placeholder: "Type your message...",
297
+ validate(value) {
298
+ if (!value || value.trim().length === 0) {
299
+ return "Message cannot be empty";
300
+ }
301
+ },
302
+ });
303
+
304
+ if (isCancel(userInput)) {
305
+ const exitBox = boxen(chalk.yellow("The chat session has ended, and Eren was calling me šŸ‘©šŸ»ā€šŸ¦°, so goodbye! šŸ‘‹"), {
306
+ padding: 1,
307
+ margin: 1,
308
+ borderStyle: "round",
309
+ borderColor: "yellow",
310
+ });
311
+ console.log(exitBox);
312
+ process.exit(0);
313
+ }
314
+
315
+ if (userInput.toLowerCase() === "exit") {
316
+ const exitBox = boxen(chalk.yellow("The chat session has ended, and Eren was calling me šŸ‘©šŸ»ā€šŸ¦°, so goodbye! šŸ‘‹"), {
317
+ padding: 1,
318
+ margin: 1,
319
+ borderStyle: "round",
320
+ borderColor: "yellow",
321
+ });
322
+ console.log(exitBox);
323
+ break;
324
+ }
325
+
326
+ const userBox = boxen(chalk.white(userInput), {
327
+ padding: 1,
328
+ margin: { left: 2, top: 1, bottom: 1 },
329
+ borderStyle: "round",
330
+ borderColor: "blue",
331
+ title: "šŸ‘¤ You",
332
+ titleAlignment: "left",
333
+ });
334
+ console.log(userBox);
335
+
336
+ await saveMessage(conversation.id, "user", userInput);
337
+ const messages = await chatService.getMessages(conversation.id);
338
+ const aiResponse = await getAIResponse(conversation.id);
339
+ await saveMessage(conversation.id, "assistant", aiResponse);
340
+ await updateConversationTitle(conversation.id, userInput, messages.length);
341
+ }
342
+ }
343
+
344
+ export async function startToolChat(conversationId = null) {
345
+ try {
346
+ intro(
347
+ boxen(chalk.bold.cyan("šŸ› ļø Mikasa AI - Tool Calling Mode"), {
348
+ padding: 1,
349
+ borderStyle: "double",
350
+ borderColor: "cyan",
351
+ })
352
+ );
353
+
354
+ const user = await getUserFromToken();
355
+
356
+ // Select tools
357
+ await selectTools();
358
+
359
+ const conversation = await initConversation(user.id, conversationId, "tool");
360
+ await chatLoop(conversation);
361
+
362
+ // Reset tools on exit
363
+ resetTools();
364
+
365
+ outro(chalk.green("✨ Thanks for using tools!"));
366
+ } catch (error) {
367
+ const errorBox = boxen(chalk.red(`āŒ Error: ${error.message}`), {
368
+ padding: 1,
369
+ margin: 1,
370
+ borderStyle: "round",
371
+ borderColor: "red",
372
+ });
373
+ console.log(errorBox);
374
+ resetTools();
375
+ process.exit(1);
376
+ }
377
+ }