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.
@@ -0,0 +1,423 @@
1
+ import { cancel, confirm, intro, isCancel, outro } from "@clack/prompts";
2
+ import { logger } from "better-auth";
3
+ import { createAuthClient } from "better-auth/client";
4
+ import { deviceAuthorizationClient } from "better-auth/client/plugins";
5
+
6
+ import chalk from "chalk";
7
+ import { Command } from "commander";
8
+ import fs from "fs/promises";
9
+ import open from "open";
10
+ import os from "os";
11
+ import path from "path";
12
+ import yoctoSpinner from "yocto-spinner";
13
+
14
+
15
+ import { z } from "zod";
16
+ import dotenv from "dotenv";
17
+ import { getUserFromApi } from "../../api/api-client.js";
18
+
19
+ dotenv.config();
20
+
21
+ const URL = "http://localhost:3005";
22
+ const CLIENT_ID = process.env.GITHUB_CLIENT_ID;
23
+ const CONFIG_DIR = path.join(os.homedir(), ".better-auth");
24
+ const TOKEN_FILE = path.join(CONFIG_DIR, "token.json");
25
+
26
+ // ============================================
27
+ // TOKEN MANAGEMENT (Export these for use in other commands)
28
+ // ============================================
29
+
30
+ export async function getStoredToken() {
31
+ try {
32
+ const data = await fs.readFile(TOKEN_FILE, "utf-8");
33
+ const token = JSON.parse(data);
34
+ return token;
35
+ } catch (error) {
36
+ // File doesn't exist or can't be read
37
+ return null;
38
+ }
39
+ }
40
+
41
+ export async function storeToken(token) {
42
+ try {
43
+ // Ensure config directory exists
44
+ await fs.mkdir(CONFIG_DIR, { recursive: true });
45
+
46
+ // Store token with metadata
47
+ const tokenData = {
48
+ access_token: token.access_token,
49
+ refresh_token: token.refresh_token, // Store if available
50
+ token_type: token.token_type || "Bearer",
51
+ scope: token.scope,
52
+ expires_at: token.expires_in
53
+ ? new Date(Date.now() + token.expires_in * 1000).toISOString()
54
+ : null,
55
+ created_at: new Date().toISOString(),
56
+ };
57
+
58
+ await fs.writeFile(TOKEN_FILE, JSON.stringify(tokenData, null, 2), "utf-8");
59
+ return true;
60
+ } catch (error) {
61
+ console.error(chalk.red("Failed to store token:"), error.message);
62
+ return false;
63
+ }
64
+ }
65
+
66
+ export async function clearStoredToken() {
67
+ try {
68
+ await fs.unlink(TOKEN_FILE);
69
+ return true;
70
+ } catch (error) {
71
+ // File doesn't exist or can't be deleted
72
+ return false;
73
+ }
74
+ }
75
+
76
+ export async function isTokenExpired() {
77
+ const token = await getStoredToken();
78
+ if (!token || !token.expires_at) {
79
+ return true;
80
+ }
81
+
82
+ const expiresAt = new Date(token.expires_at);
83
+ const now = new Date();
84
+
85
+ // Consider expired if less than 5 minutes remaining
86
+ return expiresAt.getTime() - now.getTime() < 5 * 60 * 1000;
87
+ }
88
+
89
+ export async function requireAuth() {
90
+ const token = await getStoredToken();
91
+
92
+ if (!token) {
93
+ console.log(
94
+ chalk.red("āŒ Not authenticated. Please run 'your-cli login' first.")
95
+ );
96
+ process.exit(1);
97
+ }
98
+
99
+ if (await isTokenExpired()) {
100
+ console.log(
101
+ chalk.yellow("āš ļø Your session has expired. Please login again.")
102
+ );
103
+ console.log(chalk.gray(" Run: your-cli login\n"));
104
+ process.exit(1);
105
+ }
106
+
107
+ return token;
108
+ }
109
+
110
+ // ============================================
111
+ // LOGIN COMMAND
112
+ // ============================================
113
+
114
+ export async function loginAction(opts) {
115
+ const options = z
116
+ .object({
117
+ serverUrl: z.string().optional(),
118
+ clientId: z.string().optional(),
119
+ })
120
+ .parse(opts);
121
+
122
+ const serverUrl = options.serverUrl || URL;
123
+ const clientId = options.clientId || CLIENT_ID;
124
+
125
+ intro(chalk.bold("šŸ” Auth CLI Login"));
126
+
127
+ if (!clientId) {
128
+ logger.error("CLIENT_ID is not set in .env file");
129
+ console.log(
130
+ chalk.red("\nāŒ Please set GITHUB_CLIENT_ID in your .env file")
131
+ );
132
+ process.exit(1);
133
+ }
134
+
135
+ // Check if already logged in
136
+ const existingToken = await getStoredToken();
137
+ const expired = await isTokenExpired();
138
+
139
+ if (existingToken && !expired) {
140
+ const shouldReauth = await confirm({
141
+ message: "You're already logged in. Do you want to log in again?",
142
+ initialValue: false,
143
+ });
144
+
145
+ if (isCancel(shouldReauth) || !shouldReauth) {
146
+ cancel("Login cancelled");
147
+ process.exit(0);
148
+ }
149
+ }
150
+
151
+ // Create the auth client
152
+ const authClient = createAuthClient({
153
+ baseURL: serverUrl,
154
+ plugins: [deviceAuthorizationClient()],
155
+ });
156
+
157
+ const spinner = yoctoSpinner({ text: "Requesting device authorization..." });
158
+ spinner.start();
159
+
160
+ try {
161
+ // Request device code
162
+ const { data, error } = await authClient.device.code({
163
+ client_id: clientId,
164
+ scope: "openid profile email",
165
+ });
166
+
167
+ spinner.stop();
168
+
169
+ if (error || !data) {
170
+ logger.error(
171
+ `Failed to request device authorization: ${
172
+ error?.error_description || error?.message || "Unknown error"
173
+ }`
174
+ );
175
+
176
+ if (error?.status === 404) {
177
+ console.log(chalk.red("\nāŒ Device authorization endpoint not found."));
178
+ console.log(chalk.yellow(" Make sure your auth server is running."));
179
+ } else if (error?.status === 400) {
180
+ console.log(
181
+ chalk.red("\nāŒ Bad request - check your CLIENT_ID configuration.")
182
+ );
183
+ }
184
+
185
+ process.exit(1);
186
+ }
187
+
188
+ const {
189
+ device_code,
190
+ user_code,
191
+ verification_uri,
192
+ verification_uri_complete,
193
+ interval = 5,
194
+ expires_in,
195
+ } = data;
196
+
197
+ // Display authorization instructions
198
+ console.log("");
199
+ console.log(chalk.cyan("šŸ“± Device Authorization Required"));
200
+ console.log("");
201
+ console.log(
202
+ `Please visit: ${chalk.underline.blue(
203
+ verification_uri_complete || verification_uri
204
+ )}`
205
+ );
206
+ console.log(`Enter code: ${chalk.bold.green(user_code)}`);
207
+ console.log("");
208
+
209
+ // Ask if user wants to open browser
210
+ const shouldOpen = await confirm({
211
+ message: "Open browser automatically?",
212
+ initialValue: true,
213
+ });
214
+
215
+ if (!isCancel(shouldOpen) && shouldOpen) {
216
+ const urlToOpen = verification_uri_complete || verification_uri;
217
+ await open(urlToOpen);
218
+ }
219
+
220
+ // Start polling
221
+ console.log(
222
+ chalk.gray(
223
+ `Waiting for authorization (expires in ${Math.floor(
224
+ expires_in / 60
225
+ )} minutes)...`
226
+ )
227
+ );
228
+
229
+ const token = await pollForToken(
230
+ authClient,
231
+ device_code,
232
+ clientId,
233
+ interval
234
+ );
235
+
236
+ if (token) {
237
+ // Store the token
238
+ const saved = await storeToken(token);
239
+
240
+ if (!saved) {
241
+ console.log(
242
+ chalk.yellow("\nāš ļø Warning: Could not save authentication token.")
243
+ );
244
+ console.log(
245
+ chalk.yellow(" You may need to login again on next use.")
246
+ );
247
+ }
248
+
249
+ // Get user info
250
+ const { data: session } = await authClient.getSession({
251
+ fetchOptions: {
252
+ headers: {
253
+ Authorization: `Bearer ${token.access_token}`,
254
+ },
255
+ },
256
+ });
257
+
258
+ outro(
259
+ chalk.green(
260
+ `āœ… Login successful! Welcome ${
261
+ session?.user?.name || session?.user?.email || "User"
262
+ }`
263
+ )
264
+ );
265
+
266
+ console.log(chalk.gray(`\nšŸ“ Token saved to: ${TOKEN_FILE}`));
267
+ console.log(
268
+ chalk.gray(" You can now use AI commands without logging in again.\n")
269
+ );
270
+ }
271
+ } catch (err) {
272
+ spinner.stop();
273
+ console.error(chalk.red("\nLogin failed:"), err.message);
274
+ process.exit(1);
275
+ }
276
+ }
277
+
278
+ async function pollForToken(authClient, deviceCode, clientId, initialInterval) {
279
+ let pollingInterval = initialInterval;
280
+ const spinner = yoctoSpinner({ text: "", color: "cyan" });
281
+ let dots = 0;
282
+
283
+ return new Promise((resolve, reject) => {
284
+ const poll = async () => {
285
+ dots = (dots + 1) % 4;
286
+ spinner.text = chalk.gray(
287
+ `Polling for authorization${".".repeat(dots)}${" ".repeat(3 - dots)}`
288
+ );
289
+ if (!spinner.isSpinning) spinner.start();
290
+
291
+ try {
292
+ const { data, error } = await authClient.device.token({
293
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
294
+ device_code: deviceCode,
295
+ client_id: clientId,
296
+ fetchOptions: {
297
+ headers: {
298
+ "user-agent": `Better Auth CLI`,
299
+ },
300
+ },
301
+ });
302
+
303
+ if (data?.access_token) {
304
+ console.log(
305
+ chalk.bold.yellow(`Your access token: ${data.access_token}`)
306
+ );
307
+ spinner.stop();
308
+ resolve(data);
309
+ return;
310
+ } else if (error) {
311
+ switch (error.error) {
312
+ case "authorization_pending":
313
+ // Continue polling
314
+ break;
315
+ case "slow_down":
316
+ pollingInterval += 5;
317
+ break;
318
+ case "access_denied":
319
+ spinner.stop();
320
+ logger.error("Access was denied by the user");
321
+ process.exit(1);
322
+ break;
323
+ case "expired_token":
324
+ spinner.stop();
325
+ logger.error("The device code has expired. Please try again.");
326
+ process.exit(1);
327
+ break;
328
+ default:
329
+ spinner.stop();
330
+ logger.error(`Error: ${error.error_description}`);
331
+ process.exit(1);
332
+ }
333
+ }
334
+ } catch (err) {
335
+ spinner.stop();
336
+ logger.error(`Network error: ${err.message}`);
337
+ process.exit(1);
338
+ }
339
+
340
+ setTimeout(poll, pollingInterval * 1000);
341
+ };
342
+
343
+ setTimeout(poll, pollingInterval * 1000);
344
+ });
345
+ }
346
+
347
+ // ============================================
348
+ // LOGOUT COMMAND
349
+ // ============================================
350
+
351
+ export async function logoutAction() {
352
+ intro(chalk.bold("šŸ‘‹ Logout"));
353
+
354
+ const token = await getStoredToken();
355
+
356
+ if (!token) {
357
+ console.log(chalk.yellow("You're not logged in."));
358
+ process.exit(0);
359
+ }
360
+
361
+ const shouldLogout = await confirm({
362
+ message: "Are you sure you want to logout?",
363
+ initialValue: false,
364
+ });
365
+
366
+ if (isCancel(shouldLogout) || !shouldLogout) {
367
+ cancel("Logout cancelled");
368
+ process.exit(0);
369
+ }
370
+
371
+ const cleared = await clearStoredToken();
372
+
373
+ if (cleared) {
374
+ outro(chalk.green("āœ… Successfully logged out!"));
375
+ } else {
376
+ console.log(chalk.yellow("āš ļø Could not clear token file."));
377
+ }
378
+ }
379
+
380
+ // ============================================
381
+ // WHOAMI COMMAND
382
+ // ============================================
383
+
384
+ export async function whoamiAction(opts) {
385
+ const token = await requireAuth();
386
+ if (!token?.access_token) {
387
+ console.log("No access token found. Please login.");
388
+ process.exit(1);
389
+ }
390
+
391
+ const user = await getUserFromApi(token.access_token);
392
+
393
+ if (!user) {
394
+ console.log(chalk.red("\nāŒ Session not found or expired on the server. Please login again.\n"));
395
+ process.exit(1);
396
+ }
397
+
398
+ // Output user session info
399
+ console.log(
400
+ chalk.bold.greenBright(`\nšŸ‘¤ User: ${user.name}
401
+ šŸ“§ Email: ${user.email}
402
+ šŸ‘¤ ID: ${user.id}`)
403
+ );
404
+ }
405
+
406
+ // ============================================
407
+ // COMMANDER SETUP
408
+ // ============================================
409
+
410
+ export const login = new Command("login")
411
+ .description("Login to Better Auth")
412
+ .option("--server-url <url>", "The Better Auth server URL", URL)
413
+ .option("--client-id <id>", "The OAuth client ID", CLIENT_ID)
414
+ .action(loginAction);
415
+
416
+ export const logout = new Command("logout")
417
+ .description("Logout and clear stored credentials")
418
+ .action(logoutAction);
419
+
420
+ export const whoami = new Command("whoami")
421
+ .description("Show current authenticated user")
422
+ .option("--server-url <url>", "The Better Auth server URL", URL)
423
+ .action(whoamiAction);
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+
3
+ import dotenv from "dotenv";
4
+
5
+ import chalk from "chalk";
6
+ import figlet from "figlet";
7
+
8
+ import { Command } from "commander";
9
+ import { login, logout, whoami } from "./commands/auth/login.js";
10
+ import { wakeup } from "./commands/ai/wakeup.js";
11
+
12
+
13
+ dotenv.config();
14
+
15
+ async function main() {
16
+ // banner ke liye
17
+ console.log(
18
+ chalk.cyan(
19
+ figlet.textSync("Orbit Agent", {
20
+ font: "Standard",
21
+ horizontalLayout: "default",
22
+ })
23
+ )
24
+ );
25
+ console.log(chalk.green("A Cli based AI Agent \n"));
26
+
27
+ const program = new Command("orbit");
28
+
29
+ program
30
+ .version("0.0.1")
31
+ .description("Orbit CLI - Device Flow Authentication")
32
+ .addCommand(login)
33
+ .addCommand(logout)
34
+ .addCommand(whoami)
35
+ .addCommand(wakeup)
36
+
37
+
38
+ // Default action shows help
39
+ program.action(() => {
40
+ program.help();
41
+ });
42
+
43
+
44
+
45
+ program.parse();
46
+
47
+ }
48
+
49
+ main().catch((error) => {
50
+ console.error(chalk.red("Error running Orbit CLI:"), error);
51
+ process.exit(1);
52
+ });
@@ -0,0 +1,94 @@
1
+ import { z } from "zod";
2
+ import fs from "fs/promises";
3
+ import path from "path";
4
+ import chalk from "chalk";
5
+ import yoctoSpinner from "yocto-spinner";
6
+
7
+ // Schema for the files to be created
8
+ const FileSchema = z.object({
9
+ path: z.string().describe("The relative path of the file, e.g., 'src/index.js' or 'public/index.html'"),
10
+ content: z.string().describe("The full content of the file"),
11
+ });
12
+
13
+ // Schema for the entire application generation output
14
+ const ApplicationSchema = z.object({
15
+ folderName: z.string().describe("Suggested URL-safe folder name for the app (kebab-case)"),
16
+ files: z.array(FileSchema).describe("List of files to create"),
17
+ commands: z.array(z.string()).describe("List of terminal commands to install dependencies or run the application"),
18
+ readmeInstructions: z.string().describe("Detailed markdown explanation of the app structure, features, and setup"),
19
+ });
20
+
21
+ /**
22
+ * Generate files and setup script for a requested application using AI structured outputs
23
+ * @param {string} prompt - User request description
24
+ * @param {AIService} aiService - Instantiated AIService
25
+ * @param {string} outputDir - Parent directory where the folder should be generated
26
+ * @returns {Promise<Object>} Generation status details
27
+ */
28
+ export async function generateApplication(prompt, aiService, outputDir) {
29
+ const spinner = yoctoSpinner({ text: "AI is planning and generating application files...", color: "magenta" }).start();
30
+
31
+ try {
32
+ const systemPrompt =
33
+ "You are an autonomous AI Agent developer.\n" +
34
+ "Your job is to generate all required files, directory structure, dependency installations, and setup instructions to build a complete, functional application based on the user's description.\n" +
35
+ "Generate clean, modern, and production-ready code. Do not use placeholders or omit files. If you need styling, include CSS or CDN Tailwind setups directly.";
36
+
37
+ const fullPrompt = `${systemPrompt}\n\nUser Request: ${prompt}`;
38
+
39
+ // Request structured generation
40
+ const appDetails = await aiService.generateStructured(ApplicationSchema, fullPrompt);
41
+
42
+ spinner.text = "Creating files and folders on disk...";
43
+
44
+ // Determine target directory
45
+ const targetDir = path.join(outputDir, appDetails.folderName);
46
+
47
+ // Create parent directory
48
+ await fs.mkdir(targetDir, { recursive: true });
49
+
50
+ // Write all files
51
+ for (const file of appDetails.files) {
52
+ const filePath = path.join(targetDir, file.path);
53
+ const dirPath = path.dirname(filePath);
54
+
55
+ // Ensure subdirectories exist
56
+ await fs.mkdir(dirPath, { recursive: true });
57
+
58
+ // Write file content
59
+ await fs.writeFile(filePath, file.content, "utf-8");
60
+ }
61
+
62
+ // Write a README.md detailing instructions and commands
63
+ const readmeContent = `# ${appDetails.folderName}\n\n` +
64
+ `${appDetails.readmeInstructions}\n\n` +
65
+ `## šŸš€ Setup & Execution Commands\n\n` +
66
+ `Run these commands in your terminal:\n` +
67
+ "```bash\n" +
68
+ appDetails.commands.map(cmd => cmd).join('\n') +
69
+ "\n```\n";
70
+
71
+ await fs.writeFile(path.join(targetDir, "README.md"), readmeContent, "utf-8");
72
+
73
+ spinner.success("Application generated successfully!");
74
+
75
+ console.log(chalk.bold.green(`\n✨ Successfully created: ${appDetails.folderName}`));
76
+ console.log(chalk.gray(`šŸ“ Location: ${targetDir}\n`));
77
+ console.log(chalk.bold.yellow("šŸ”§ Suggested setup commands:"));
78
+ appDetails.commands.forEach(cmd => console.log(chalk.cyan(` $ ${cmd}`)));
79
+ console.log("");
80
+
81
+ return {
82
+ success: true,
83
+ folderName: appDetails.folderName,
84
+ appDir: targetDir,
85
+ files: appDetails.files,
86
+ commands: appDetails.commands,
87
+ };
88
+
89
+ } catch (error) {
90
+ spinner.error("Failed to generate application files");
91
+ console.error(chalk.red("Agent Error:"), error);
92
+ throw error;
93
+ }
94
+ }
@@ -0,0 +1,8 @@
1
+ import dotenv from "dotenv";
2
+
3
+ dotenv.config();
4
+
5
+ export const config = {
6
+ googleApiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY,
7
+ orbitalModel: process.env.ORBITAL_MODEL
8
+ }
@@ -0,0 +1,107 @@
1
+ import { google } from "@ai-sdk/google";
2
+ import { tool } from "ai";
3
+ import { z } from "zod";
4
+ import chalk from "chalk";
5
+ import fs from "fs/promises";
6
+ import path from "path";
7
+
8
+ // Define the schema for package.json dependencies
9
+ const packageJsonSchema = z.object({
10
+ dependencies: z.record(z.string()).optional(),
11
+ devDependencies: z.record(z.string()).optional(),
12
+ });
13
+
14
+ // Define all available tools
15
+ export const availableTools = [
16
+ {
17
+ id: "read-file",
18
+ name: "Read File Content",
19
+ description: "Read the complete code content of a file",
20
+ tool: tool({
21
+ description: "Read the complete code content of a file",
22
+ parameters: z.object({
23
+ filePath: z.string().describe("The absolute path of the file to read"),
24
+ }),
25
+ execute: async ({ filePath }) => {
26
+ try {
27
+ const content = await fs.readFile(filePath, "utf-8");
28
+ return { success: true, content };
29
+ } catch (error) {
30
+ return { success: false, error: error.message };
31
+ }
32
+ },
33
+ }),
34
+ },
35
+ {
36
+ id: "list-dir",
37
+ name: "List Directory Contents",
38
+ description: "List all files and folders in a directory",
39
+ tool: tool({
40
+ description: "List all files and subfolders in a directory (recursive = false)",
41
+ parameters: z.object({
42
+ dirPath: z.string().describe("The absolute path of the directory to list"),
43
+ }),
44
+ execute: async ({ dirPath }) => {
45
+ try {
46
+ const files = await fs.readdir(dirPath, { withFileTypes: true });
47
+ const items = files.map((file) => ({
48
+ name: file.name,
49
+ isDirectory: file.isDirectory(),
50
+ isFile: file.isFile(),
51
+ }));
52
+ return { success: true, items };
53
+ } catch (error) {
54
+ return { success: false, error: error.message };
55
+ }
56
+ },
57
+ }),
58
+ },
59
+ {
60
+ id: "run-command",
61
+ name: "Execute Terminal Command",
62
+ description: "Execute a command in the terminal (powershell/sh)",
63
+ tool: tool({
64
+ description: "Run a shell/powershell command on behalf of the user",
65
+ parameters: z.object({
66
+ command: z.string().describe("The exact terminal command to run"),
67
+ cwd: z.string().optional().describe("Working directory for the command execution"),
68
+ }),
69
+ execute: async ({ command, cwd }) => {
70
+ // Mock command runner warning/execution logic or safe stub for security
71
+ console.log(chalk.yellow(`\nāš ļø CLI request to execute: "${command}"`));
72
+ return {
73
+ success: true,
74
+ message: "Note: Real terminal command execution was disabled inside the sandbox tool configuration for safety reasons."
75
+ };
76
+ },
77
+ }),
78
+ },
79
+ ];
80
+
81
+ // Tracks enabled tools in the current session
82
+ const enabledToolIds = new Set();
83
+
84
+ export function enableTools(ids) {
85
+ enabledToolIds.clear();
86
+ ids.forEach((id) => enabledToolIds.add(id));
87
+ }
88
+
89
+ export function resetTools() {
90
+ enabledToolIds.clear();
91
+ }
92
+
93
+ export function getEnabledToolNames() {
94
+ return availableTools
95
+ .filter((t) => enabledToolIds.has(t.id))
96
+ .map((t) => t.name);
97
+ }
98
+
99
+ export function getEnabledTools() {
100
+ const tools = {};
101
+ availableTools.forEach((t) => {
102
+ if (enabledToolIds.has(t.id)) {
103
+ tools[t.id] = t.tool;
104
+ }
105
+ });
106
+ return tools;
107
+ }