orbital-cli-agent 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,29 @@
1
+ {
2
+ "name": "orbital-cli-agent",
3
+ "version": "1.0.0",
4
+ "description": "A CLI based AI Agent",
5
+ "main": "src/cli/main.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "orbital": "./src/cli/main.js"
9
+ },
10
+ "dependencies": {
11
+ "@ai-sdk/google": "^3.0.86",
12
+ "@clack/prompts": "^1.5.1",
13
+ "ai": "^6.0.214",
14
+ "better-auth": "^1.6.5",
15
+ "boxen": "^8.0.1",
16
+ "chalk": "^5.6.2",
17
+ "commander": "^15.0.0",
18
+ "dotenv": "^17.4.2",
19
+ "figlet": "^1.11.0",
20
+ "marked": "^15.0.12",
21
+ "marked-terminal": "^7.3.0",
22
+ "open": "^11.0.0",
23
+ "yocto-spinner": "^1.2.0",
24
+ "zod": "^4.4.3"
25
+ },
26
+ "files": [
27
+ "src"
28
+ ]
29
+ }
@@ -0,0 +1,128 @@
1
+ import { google } from "@ai-sdk/google";
2
+ import { streamText, generateObject } from "ai";
3
+ import { config } from "../../config/google.config.js";
4
+ import chalk from "chalk";
5
+
6
+ export class AIService {
7
+ constructor() {
8
+ if (!config.googleApiKey) {
9
+ throw new Error("GOOGLE_API_KEY is not set in environment variables");
10
+ }
11
+
12
+ this.model = google(config.orbitalModel, {
13
+ apiKey: config.googleApiKey,
14
+ });
15
+ }
16
+
17
+ /**
18
+ * Send a message and get streaming response
19
+ * @param {Array} messages - Array of message objects {role, content}
20
+ * @param {Function} onChunk - Callback for each text chunk
21
+ * @param {Object} tools - Optional tools object
22
+ * @param {Function} onToolCall - Callback for tool calls
23
+ * @returns {Promise<Object>} Full response with content, tool calls, and usage
24
+ */
25
+ async sendMessage(messages, onChunk, tools = undefined, onToolCall = null) {
26
+ try {
27
+ const streamConfig = {
28
+ model: this.model,
29
+ messages: messages,
30
+ temperature: config.temperature,
31
+ maxTokens: config.maxTokens,
32
+ };
33
+
34
+ // Add tools if provided with maxSteps for multi-step tool calling
35
+ if (tools && Object.keys(tools).length > 0) {
36
+ streamConfig.tools = tools;
37
+ streamConfig.maxSteps = 5; // Allow up to 5 tool call steps
38
+
39
+ console.log(chalk.gray(`[DEBUG] Tools enabled: ${Object.keys(tools).join(', ')}`));
40
+ }
41
+
42
+ const result = streamText(streamConfig);
43
+
44
+ let fullResponse = "";
45
+
46
+ // Stream text chunks
47
+ for await (const chunk of result.textStream) {
48
+ fullResponse += chunk;
49
+ if (onChunk) {
50
+ onChunk(chunk);
51
+ }
52
+ }
53
+
54
+ // IMPORTANT: Await the result to get access to steps, toolCalls, etc.
55
+ const fullResult = await result;
56
+
57
+ const toolCalls = [];
58
+ const toolResults = [];
59
+
60
+ // Collect tool calls from all steps (if they exist)
61
+ if (fullResult.steps && Array.isArray(fullResult.steps)) {
62
+ for (const step of fullResult.steps) {
63
+ if (step.toolCalls && step.toolCalls.length > 0) {
64
+ for (const toolCall of step.toolCalls) {
65
+ toolCalls.push(toolCall);
66
+ if (onToolCall) {
67
+ onToolCall(toolCall);
68
+ }
69
+ }
70
+ }
71
+
72
+ // Collect tool results
73
+ if (step.toolResults && step.toolResults.length > 0) {
74
+ toolResults.push(...step.toolResults);
75
+ }
76
+ }
77
+ }
78
+
79
+ return {
80
+ content: fullResponse,
81
+ finishReason: fullResult.finishReason,
82
+ usage: fullResult.usage,
83
+ toolCalls,
84
+ toolResults,
85
+ steps: fullResult.steps,
86
+ };
87
+ } catch (error) {
88
+ console.error(chalk.red("AI Service Error:"), error.message);
89
+ console.error(chalk.red("Full error:"), error);
90
+ throw error;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Get a non-streaming response
96
+ * @param {Array} messages - Array of message objects
97
+ * @param {Object} tools - Optional tools
98
+ * @returns {Promise<string>} Response text
99
+ */
100
+ async getMessage(messages, tools = undefined) {
101
+ let fullResponse = "";
102
+ const result = await this.sendMessage(messages, (chunk) => {
103
+ fullResponse += chunk;
104
+ }, tools);
105
+ return result.content;
106
+ }
107
+
108
+ /**
109
+ * Generate structured output using a Zod schema
110
+ * @param {Object} schema - Zod schema
111
+ * @param {string} prompt - Prompt for generation
112
+ * @returns {Promise<Object>} Parsed object matching the schema
113
+ */
114
+ async generateStructured(schema, prompt) {
115
+ try {
116
+ const result = await generateObject({
117
+ model: this.model,
118
+ schema: schema,
119
+ prompt: prompt,
120
+ });
121
+
122
+ return result.object;
123
+ } catch (error) {
124
+ console.error(chalk.red("AI Structured Generation Error:"), error.message);
125
+ throw error;
126
+ }
127
+ }
128
+ }
@@ -0,0 +1,115 @@
1
+ import chalk from "chalk";
2
+ import dotenv from "dotenv";
3
+
4
+ dotenv.config();
5
+
6
+ const URL = process.env.BETTER_AUTH_URL || "http://localhost:3005";
7
+
8
+ export async function getUserFromApi(accessToken) {
9
+ try {
10
+ const response = await fetch(`${URL}/api/me`, {
11
+ headers: {
12
+ Authorization: `Bearer ${accessToken}`,
13
+ },
14
+ });
15
+
16
+ if (!response.ok) {
17
+ return null;
18
+ }
19
+
20
+ const data = await response.json();
21
+ return data?.user || null;
22
+ } catch (error) {
23
+ console.error(chalk.red("Failed to fetch user session from server:"), error.message);
24
+ return null;
25
+ }
26
+ }
27
+
28
+ export async function getOrCreateConversation(accessToken, conversationId, mode = "chat") {
29
+ try {
30
+ const response = await fetch(`${URL}/api/chat/conversations`, {
31
+ method: "POST",
32
+ headers: {
33
+ "Content-Type": "application/json",
34
+ Authorization: `Bearer ${accessToken}`,
35
+ },
36
+ body: JSON.stringify({ conversationId, mode }),
37
+ });
38
+
39
+ if (!response.ok) {
40
+ const errData = await response.json();
41
+ throw new Error(errData.error || "Failed to get or create conversation");
42
+ }
43
+
44
+ return await response.json();
45
+ } catch (error) {
46
+ console.error(chalk.red("API Error (getOrCreateConversation):"), error.message);
47
+ throw error;
48
+ }
49
+ }
50
+
51
+ export async function addMessage(accessToken, conversationId, role, content) {
52
+ try {
53
+ const response = await fetch(`${URL}/api/chat/messages`, {
54
+ method: "POST",
55
+ headers: {
56
+ "Content-Type": "application/json",
57
+ Authorization: `Bearer ${accessToken}`,
58
+ },
59
+ body: JSON.stringify({ conversationId, role, content }),
60
+ });
61
+
62
+ if (!response.ok) {
63
+ const errData = await response.json();
64
+ throw new Error(errData.error || "Failed to add message");
65
+ }
66
+
67
+ return await response.json();
68
+ } catch (error) {
69
+ console.error(chalk.red("API Error (addMessage):"), error.message);
70
+ throw error;
71
+ }
72
+ }
73
+
74
+ export async function getMessages(accessToken, conversationId) {
75
+ try {
76
+ const response = await fetch(`${URL}/api/chat/conversations/${conversationId}/messages`, {
77
+ headers: {
78
+ Authorization: `Bearer ${accessToken}`,
79
+ },
80
+ });
81
+
82
+ if (!response.ok) {
83
+ const errData = await response.json();
84
+ throw new Error(errData.error || "Failed to get messages");
85
+ }
86
+
87
+ return await response.json();
88
+ } catch (error) {
89
+ console.error(chalk.red("API Error (getMessages):"), error.message);
90
+ throw error;
91
+ }
92
+ }
93
+
94
+ export async function updateTitle(accessToken, conversationId, title) {
95
+ try {
96
+ const response = await fetch(`${URL}/api/chat/conversations/${conversationId}`, {
97
+ method: "PUT",
98
+ headers: {
99
+ "Content-Type": "application/json",
100
+ Authorization: `Bearer ${accessToken}`,
101
+ },
102
+ body: JSON.stringify({ title }),
103
+ });
104
+
105
+ if (!response.ok) {
106
+ const errData = await response.json();
107
+ throw new Error(errData.error || "Failed to update title");
108
+ }
109
+
110
+ return await response.json();
111
+ } catch (error) {
112
+ console.error(chalk.red("API Error (updateTitle):"), error.message);
113
+ throw error;
114
+ }
115
+ }
@@ -0,0 +1,237 @@
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 { getStoredToken } from "../commands/auth/login.js";
6
+ import { generateApplication } from "../../config/agent.config.js";
7
+ import * as apiClient from "../api/api-client.js";
8
+ import path from "path";
9
+
10
+ const aiService = new AIService();
11
+
12
+ function getSmartOutputDir() {
13
+ if (process.env.ORBIT_OUTPUT_DIR) {
14
+ return path.resolve(process.env.ORBIT_OUTPUT_DIR);
15
+ }
16
+
17
+ const cwd = process.cwd();
18
+ const isInsideWorkspace = cwd.includes("CLI AI AGENT") || cwd.includes("cli-ai-agent");
19
+
20
+ if (isInsideWorkspace) {
21
+ const pathParts = cwd.split(path.sep);
22
+ const index = pathParts.findIndex(part =>
23
+ part.toLowerCase() === "cli ai agent" || part.toLowerCase() === "cli-ai-agent"
24
+ );
25
+
26
+ if (index !== -1) {
27
+ const parentDir = pathParts.slice(0, index).join(path.sep);
28
+ return path.join(parentDir, "orbit-apps");
29
+ }
30
+
31
+ return path.join(cwd, "..", "orbit-apps");
32
+ }
33
+
34
+ return cwd;
35
+ }
36
+
37
+ async function getUserFromToken() {
38
+ const token = await getStoredToken();
39
+
40
+ if (!token?.access_token) {
41
+ throw new Error("Not authenticated. Please run 'orbit login' first.");
42
+ }
43
+
44
+ const user = await apiClient.getUserFromApi(token.access_token);
45
+
46
+ if (!user) {
47
+ throw new Error("User not found. Please login again.");
48
+ }
49
+
50
+ console.log(chalk.green(`\n✓ Welcome back, ${user.name}!\n`));
51
+ return { user, token };
52
+ }
53
+
54
+ async function initConversation(token, userId, conversationId = null) {
55
+ const conversation = await apiClient.getOrCreateConversation(
56
+ token,
57
+ conversationId,
58
+ "agent"
59
+ );
60
+
61
+ const conversationInfo = boxen(
62
+ `${chalk.bold("Conversation")}: ${conversation.title}\n` +
63
+ `${chalk.gray("ID:")} ${conversation.id}\n` +
64
+ `${chalk.gray("Mode:")} ${chalk.magenta("Agent (Code Generator)")}\n` +
65
+ `${chalk.cyan("Target Directory:")} ${getSmartOutputDir()}`,
66
+ {
67
+ padding: 1,
68
+ margin: { top: 1, bottom: 1 },
69
+ borderStyle: "round",
70
+ borderColor: "magenta",
71
+ title: "🤖 Agent Mode",
72
+ titleAlignment: "center",
73
+ }
74
+ );
75
+
76
+ console.log(conversationInfo);
77
+
78
+ return conversation;
79
+ }
80
+
81
+ async function saveMessage(token, conversationId, role, content) {
82
+ return await apiClient.addMessage(token, conversationId, role, content);
83
+ }
84
+
85
+ async function agentLoop(token, conversation) {
86
+ const helpBox = boxen(
87
+ `${chalk.cyan.bold("What can the agent do?")}\n\n` +
88
+ `${chalk.gray('• Generate complete applications from descriptions')}\n` +
89
+ `${chalk.gray('• Create all necessary files and folders')}\n` +
90
+ `${chalk.gray('• Include setup instructions and commands')}\n` +
91
+ `${chalk.gray('• Generate production-ready code')}\n\n` +
92
+ `${chalk.yellow.bold("Examples:")}\n` +
93
+ `${chalk.white('• "Build a todo app with React and Tailwind"')}\n` +
94
+ `${chalk.white('• "Create a REST API with Express and MongoDB"')}\n` +
95
+ `${chalk.white('• "Make a weather app using OpenWeatherMap API"')}\n\n` +
96
+ `${chalk.gray('Type "exit" to end the session')}`,
97
+ {
98
+ padding: 1,
99
+ margin: { bottom: 1 },
100
+ borderStyle: "round",
101
+ borderColor: "cyan",
102
+ title: "💡 Agent Instructions",
103
+ }
104
+ );
105
+
106
+ console.log(helpBox);
107
+
108
+ while (true) {
109
+ const userInput = await text({
110
+ message: chalk.magenta("🤖 What would you like to build?"),
111
+ placeholder: "Describe your application...",
112
+ validate(value) {
113
+ if (!value || value.trim().length === 0) {
114
+ return "Description cannot be empty";
115
+ }
116
+ if (value.trim().length < 10) {
117
+ return "Please provide more details (at least 10 characters)";
118
+ }
119
+ },
120
+ });
121
+
122
+ if (isCancel(userInput)) {
123
+ console.log(chalk.yellow("\n👋 Agent session cancelled\n"));
124
+ process.exit(0);
125
+ }
126
+
127
+ if (userInput.toLowerCase() === "exit") {
128
+ console.log(chalk.yellow("\n👋 Agent session ended\n"));
129
+ break;
130
+ }
131
+
132
+ const userBox = boxen(chalk.white(userInput), {
133
+ padding: 1,
134
+ margin: { top: 1, bottom: 1 },
135
+ borderStyle: "round",
136
+ borderColor: "blue",
137
+ title: "👤 Your Request",
138
+ titleAlignment: "left",
139
+ });
140
+ console.log(userBox);
141
+
142
+ // Save user message
143
+ await saveMessage(token, conversation.id, "user", userInput);
144
+
145
+ try {
146
+ // Generate application using structured output
147
+ const outputDir = getSmartOutputDir();
148
+ const result = await generateApplication(
149
+ userInput,
150
+ aiService,
151
+ outputDir
152
+ );
153
+
154
+ if (result && result.success) {
155
+ // Save successful generation details
156
+ const responseMessage = `Generated application: ${result.folderName}\n` +
157
+ `Files created: ${result.files.length}\n` +
158
+ `Location: ${result.appDir}\n\n` +
159
+ `Setup commands:\n${result.commands.join('\n')}`;
160
+
161
+ await saveMessage(token, conversation.id, "assistant", responseMessage);
162
+
163
+ // Ask if user wants to generate another app
164
+ const continuePrompt = await confirm({
165
+ message: chalk.cyan("Would you like to generate another application?"),
166
+ initialValue: false,
167
+ });
168
+
169
+ if (isCancel(continuePrompt) || !continuePrompt) {
170
+ console.log(chalk.yellow("\n👋 Great! Check your new application.\n"));
171
+ break;
172
+ }
173
+
174
+ } else {
175
+ throw new Error("Generation returned no result");
176
+ }
177
+
178
+ } catch (error) {
179
+ console.log(chalk.red(`\n❌ Error: ${error.message}\n`));
180
+
181
+ await saveMessage(token, conversation.id, "assistant", `Error: ${error.message}`);
182
+
183
+ const retry = await confirm({
184
+ message: chalk.cyan("Would you like to try again?"),
185
+ initialValue: true,
186
+ });
187
+
188
+ if (isCancel(retry) || !retry) {
189
+ break;
190
+ }
191
+ }
192
+ }
193
+ }
194
+
195
+ export async function startAgentChat(conversationId = null) {
196
+ try {
197
+ intro(
198
+ boxen(
199
+ chalk.bold.magenta("🤖 Orbit AI - Agent Mode\n\n") +
200
+ chalk.gray("Autonomous Application Generator"),
201
+ {
202
+ padding: 1,
203
+ borderStyle: "double",
204
+ borderColor: "magenta",
205
+ }
206
+ )
207
+ );
208
+
209
+ const { user, token } = await getUserFromToken();
210
+
211
+ // Warning about file system access
212
+ const shouldContinue = await confirm({
213
+ message: chalk.yellow("⚠️ The agent will create files and folders in the current directory. Continue?"),
214
+ initialValue: true,
215
+ });
216
+
217
+ if (isCancel(shouldContinue) || !shouldContinue) {
218
+ cancel(chalk.yellow("Agent mode cancelled"));
219
+ process.exit(0);
220
+ }
221
+
222
+ const conversation = await initConversation(token.access_token, user.id, conversationId);
223
+ await agentLoop(token.access_token, conversation);
224
+
225
+ outro(chalk.green.bold("\n✨ Thanks for using Agent Mode!"));
226
+
227
+ } catch (error) {
228
+ const errorBox = boxen(chalk.red(`❌ Error: ${error.message}`), {
229
+ padding: 1,
230
+ margin: 1,
231
+ borderStyle: "round",
232
+ borderColor: "red",
233
+ });
234
+ console.log(errorBox);
235
+ process.exit(1);
236
+ }
237
+ }