kyawthiha-nextjs-agent-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.
@@ -0,0 +1,334 @@
1
+ /**
2
+ * Shell tools for the AI Agent
3
+ * Safe shell execution with command validation
4
+ */
5
+ import { exec, spawn } from 'child_process';
6
+ import { promisify } from 'util';
7
+ import fs from 'fs/promises';
8
+ const execAsync = promisify(exec);
9
+ /**
10
+ * Dangerous commands and patterns that should be blocked
11
+ */
12
+ const DANGEROUS_PATTERNS = [
13
+ /rm\s+(-rf?|--recursive)\s+[\/\\]/, // rm -rf /
14
+ /rm\s+(-rf?|--recursive)\s+~/, // rm -rf ~
15
+ /rm\s+(-rf?|--recursive)\s+\*/, // rm -rf *
16
+ /del\s+\/[sf]\s+[a-z]:\\/i, // Windows: del /s C:\
17
+ /format\s+[a-z]:/i, // Windows: format C:
18
+ /mkfs\./i, // Linux filesystem format
19
+ /dd\s+if=.*of=\/dev/i, // dd to device
20
+ />\s*\/dev\/sd[a-z]/i, // Write to disk device
21
+ /chmod\s+777\s+\//i, // chmod 777 /
22
+ /chown\s+-R.*\//i, // chown -R /
23
+ /:\s*\(\)\s*\{\s*:\|:&\s*\}/, // Fork bomb
24
+ /wget.*\|\s*(ba)?sh/i, // Download and execute
25
+ /curl.*\|\s*(ba)?sh/i, // Download and execute
26
+ /shutdown/i,
27
+ /reboot/i,
28
+ /init\s+0/,
29
+ /halt/i,
30
+ /poweroff/i,
31
+ ];
32
+ /**
33
+ * Commands that require extra caution but aren't blocked
34
+ */
35
+ const CAUTION_PATTERNS = [
36
+ /npm\s+publish/i,
37
+ /git\s+push\s+--force/i,
38
+ /git\s+push\s+-f/i,
39
+ /DROP\s+TABLE/i,
40
+ /DROP\s+DATABASE/i,
41
+ /TRUNCATE/i,
42
+ /DELETE\s+FROM.*WHERE\s+1\s*=\s*1/i,
43
+ ];
44
+ /**
45
+ * Check if a command is safe to execute
46
+ */
47
+ function validateCommand(command) {
48
+ // Check for dangerous patterns
49
+ for (const pattern of DANGEROUS_PATTERNS) {
50
+ if (pattern.test(command)) {
51
+ return {
52
+ safe: false,
53
+ reason: `Blocked: Command matches dangerous pattern. Pattern: ${pattern.toString()}`
54
+ };
55
+ }
56
+ }
57
+ // Check for caution patterns
58
+ for (const pattern of CAUTION_PATTERNS) {
59
+ if (pattern.test(command)) {
60
+ return {
61
+ safe: true,
62
+ caution: `Warning: This command may have significant effects: ${pattern.toString()}`
63
+ };
64
+ }
65
+ }
66
+ return { safe: true };
67
+ }
68
+ /**
69
+ * Tool: Execute Command
70
+ * Safe shell execution with validation
71
+ */
72
+ export const execCommandTool = {
73
+ name: 'exec_command',
74
+ description: `Execute a shell command safely.
75
+ Blocks dangerous commands (rm -rf /, format, etc.)
76
+ Captures stdout, stderr, and exit code.
77
+
78
+ Automatically uses the correct shell for the OS:
79
+ - Windows: PowerShell
80
+ - Unix: bash
81
+
82
+ Best for:
83
+ - Running build commands (npm, pnpm, yarn)
84
+ - Git operations
85
+ - File operations
86
+ - Development servers`,
87
+ parameters: {
88
+ type: 'object',
89
+ properties: {
90
+ command: {
91
+ type: 'string',
92
+ description: 'The shell command to execute'
93
+ },
94
+ cwd: {
95
+ type: 'string',
96
+ description: 'Working directory for the command'
97
+ },
98
+ timeout: {
99
+ type: 'string',
100
+ description: 'Timeout in seconds (default: 60, max: 300)'
101
+ },
102
+ background: {
103
+ type: 'string',
104
+ description: 'Run command in background (for servers)',
105
+ enum: ['true', 'false']
106
+ }
107
+ },
108
+ required: ['command']
109
+ },
110
+ execute: async (input) => {
111
+ try {
112
+ const command = input.command;
113
+ const cwd = input.cwd || process.cwd();
114
+ const timeoutSec = Math.min(parseInt(input.timeout || '60', 10), 300);
115
+ const background = input.background === 'true';
116
+ // Validate command
117
+ const validation = validateCommand(command);
118
+ if (!validation.safe) {
119
+ return `Error: ${validation.reason}`;
120
+ }
121
+ // Check cwd exists
122
+ try {
123
+ await fs.access(cwd);
124
+ }
125
+ catch {
126
+ return `Error: Working directory does not exist: ${cwd}`;
127
+ }
128
+ const shell = process.platform === 'win32' ? 'powershell.exe' : '/bin/bash';
129
+ // Output any caution warnings
130
+ let output = '';
131
+ if (validation.caution) {
132
+ output += `⚠️ ${validation.caution}\n\n`;
133
+ }
134
+ if (background) {
135
+ // For background processes, use spawn and detach
136
+ const args = process.platform === 'win32'
137
+ ? ['-Command', command]
138
+ : ['-c', command];
139
+ const child = spawn(shell, args, {
140
+ cwd,
141
+ detached: true,
142
+ stdio: 'ignore'
143
+ });
144
+ child.unref();
145
+ return output + `Started background process with PID: ${child.pid}\nCommand: ${command}\nWorking directory: ${cwd}`;
146
+ }
147
+ // Execute command
148
+ try {
149
+ const { stdout, stderr } = await execAsync(command, {
150
+ cwd,
151
+ shell,
152
+ timeout: timeoutSec * 1000,
153
+ maxBuffer: 10 * 1024 * 1024, // 10MB
154
+ encoding: 'utf-8'
155
+ });
156
+ output += `Command: ${command}\n`;
157
+ output += `Working directory: ${cwd}\n`;
158
+ output += `Exit code: 0\n\n`;
159
+ if (stdout) {
160
+ const lines = stdout.split('\n');
161
+ if (lines.length > 100) {
162
+ output += `--- STDOUT (${lines.length} lines, showing last 100) ---\n`;
163
+ output += lines.slice(-100).join('\n');
164
+ }
165
+ else {
166
+ output += `--- STDOUT ---\n${stdout}`;
167
+ }
168
+ }
169
+ if (stderr) {
170
+ output += `\n--- STDERR ---\n${stderr}`;
171
+ }
172
+ if (!stdout && !stderr) {
173
+ output += '(no output)';
174
+ }
175
+ return output;
176
+ }
177
+ catch (error) {
178
+ output += `Command: ${command}\n`;
179
+ output += `Working directory: ${cwd}\n`;
180
+ output += `Exit code: ${error.code || 'unknown'}\n\n`;
181
+ if (error.stdout) {
182
+ output += `--- STDOUT ---\n${error.stdout}\n`;
183
+ }
184
+ if (error.stderr) {
185
+ output += `--- STDERR ---\n${error.stderr}\n`;
186
+ }
187
+ if (!error.stdout && !error.stderr) {
188
+ output += `Error: ${error.message}`;
189
+ }
190
+ return output;
191
+ }
192
+ }
193
+ catch (error) {
194
+ return `Error: ${error.message}`;
195
+ }
196
+ }
197
+ };
198
+ /**
199
+ * Tool: Get Process Info
200
+ * Check if a process is running
201
+ */
202
+ export const processInfoTool = {
203
+ name: 'process_info',
204
+ description: `Get information about running processes.
205
+ Search by name or PID.
206
+
207
+ Useful for checking if servers are running.`,
208
+ parameters: {
209
+ type: 'object',
210
+ properties: {
211
+ name: {
212
+ type: 'string',
213
+ description: 'Process name to search for'
214
+ },
215
+ pid: {
216
+ type: 'string',
217
+ description: 'Process ID to check'
218
+ }
219
+ },
220
+ required: []
221
+ },
222
+ execute: async (input) => {
223
+ try {
224
+ const name = input.name;
225
+ const pid = input.pid;
226
+ const shell = process.platform === 'win32' ? 'powershell.exe' : '/bin/bash';
227
+ let command;
228
+ if (process.platform === 'win32') {
229
+ if (pid) {
230
+ command = `Get-Process -Id ${pid} | Format-List Name,Id,CPU,WorkingSet`;
231
+ }
232
+ else if (name) {
233
+ command = `Get-Process -Name *${name}* | Format-Table Name,Id,CPU,WorkingSet -AutoSize`;
234
+ }
235
+ else {
236
+ command = `Get-Process | Sort-Object CPU -Descending | Select-Object -First 10 | Format-Table Name,Id,CPU,WorkingSet -AutoSize`;
237
+ }
238
+ }
239
+ else {
240
+ if (pid) {
241
+ command = `ps -p ${pid} -o pid,ppid,user,%cpu,%mem,command`;
242
+ }
243
+ else if (name) {
244
+ command = `ps aux | grep -i "${name}" | grep -v grep | head -20`;
245
+ }
246
+ else {
247
+ command = `ps aux --sort=-%cpu | head -11`;
248
+ }
249
+ }
250
+ try {
251
+ const { stdout } = await execAsync(command, { shell, timeout: 10000 });
252
+ if (!stdout.trim()) {
253
+ return 'No matching processes found';
254
+ }
255
+ return `Process Information:\n${stdout}`;
256
+ }
257
+ catch {
258
+ return 'No matching processes found';
259
+ }
260
+ }
261
+ catch (error) {
262
+ return `Error: ${error.message}`;
263
+ }
264
+ }
265
+ };
266
+ /**
267
+ * Tool: Kill Process
268
+ * Terminate a process by PID
269
+ */
270
+ export const killProcessTool = {
271
+ name: 'kill_process',
272
+ description: `Terminate a process by PID.
273
+ Use with caution - only kill processes you started.`,
274
+ parameters: {
275
+ type: 'object',
276
+ properties: {
277
+ pid: {
278
+ type: 'string',
279
+ description: 'Process ID to terminate'
280
+ },
281
+ force: {
282
+ type: 'string',
283
+ description: 'Force kill (SIGKILL)',
284
+ enum: ['true', 'false']
285
+ }
286
+ },
287
+ required: ['pid']
288
+ },
289
+ execute: async (input) => {
290
+ try {
291
+ const pid = parseInt(input.pid, 10);
292
+ const force = input.force === 'true';
293
+ if (isNaN(pid)) {
294
+ return 'Error: Invalid PID';
295
+ }
296
+ // Safety check - don't kill system processes
297
+ if (pid <= 1) {
298
+ return 'Error: Cannot kill system processes';
299
+ }
300
+ const shell = process.platform === 'win32' ? 'powershell.exe' : '/bin/bash';
301
+ let command;
302
+ if (process.platform === 'win32') {
303
+ command = force
304
+ ? `Stop-Process -Id ${pid} -Force`
305
+ : `Stop-Process -Id ${pid}`;
306
+ }
307
+ else {
308
+ command = force
309
+ ? `kill -9 ${pid}`
310
+ : `kill ${pid}`;
311
+ }
312
+ try {
313
+ await execAsync(command, { shell, timeout: 5000 });
314
+ return `Process ${pid} terminated successfully`;
315
+ }
316
+ catch (error) {
317
+ return `Error terminating process: ${error.message}`;
318
+ }
319
+ }
320
+ catch (error) {
321
+ return `Error: ${error.message}`;
322
+ }
323
+ }
324
+ };
325
+ /**
326
+ * Get all shell tools
327
+ */
328
+ export function getShellTools() {
329
+ return [
330
+ execCommandTool,
331
+ processInfoTool,
332
+ killProcessTool,
333
+ ];
334
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Core types for the AI Agent system
3
+ */
4
+ export {};
@@ -0,0 +1,61 @@
1
+ import { Command } from 'commander';
2
+ import inquirer from 'inquirer';
3
+ import { logger } from '../../utils/logger.js';
4
+ import { getCredPath, getGeminiApiKey, setGeminiApiKey } from '../../utils/cred-store.js';
5
+ export const configCommand = new Command('config')
6
+ .description('Manage CLI configuration');
7
+ /**
8
+ * Subcommand: set-api-key
9
+ * Prompts for and saves the Gemini API key to credential store
10
+ */
11
+ configCommand
12
+ .command('set-api-key')
13
+ .description('Set or update the Gemini API key')
14
+ .action(async () => {
15
+ try {
16
+ const currentKey = await getGeminiApiKey();
17
+ if (currentKey) {
18
+ const redactedKey = currentKey.slice(0, 6) + '...' + currentKey.slice(-4);
19
+ logger.info(`Current API key: ${redactedKey}`);
20
+ }
21
+ const answer = await inquirer.prompt([{
22
+ type: 'password',
23
+ name: 'apiKey',
24
+ message: 'Enter your Gemini API Key:',
25
+ validate: (input) => input.length > 0 ? true : 'API Key is required'
26
+ }]);
27
+ await setGeminiApiKey(answer.apiKey);
28
+ logger.success(`API key saved to ${getCredPath()}`);
29
+ }
30
+ catch (error) {
31
+ logger.error(`Failed to set API key: ${error.message}`);
32
+ process.exit(1);
33
+ }
34
+ });
35
+ /**
36
+ * Subcommand: show
37
+ * Displays current configuration info
38
+ */
39
+ configCommand
40
+ .command('show')
41
+ .description('Show current configuration')
42
+ .action(async () => {
43
+ try {
44
+ const credPath = getCredPath();
45
+ const apiKey = await getGeminiApiKey();
46
+ console.log('\n--- Configuration ---');
47
+ console.log(`Config file: ${credPath}`);
48
+ if (apiKey) {
49
+ const redactedKey = apiKey.slice(0, 6) + '...' + apiKey.slice(-4);
50
+ console.log(`Gemini API Key: ${redactedKey}`);
51
+ }
52
+ else {
53
+ console.log('Gemini API Key: (not set)');
54
+ }
55
+ console.log('');
56
+ }
57
+ catch (error) {
58
+ logger.error(`Failed to show config: ${error.message}`);
59
+ process.exit(1);
60
+ }
61
+ });
@@ -0,0 +1,236 @@
1
+ import 'dotenv/config'; // Load env vars
2
+ import { Command } from 'commander';
3
+ import inquirer from 'inquirer';
4
+ import ora from 'ora';
5
+ import chalk from 'chalk';
6
+ import { logger } from '../../utils/logger.js';
7
+ import { Agent } from '../../agent/index.js';
8
+ import path from 'path';
9
+ import { getGeminiApiKey, setGeminiApiKey, getCredPath } from '../../utils/cred-store.js';
10
+ /**
11
+ * Display welcome banner
12
+ */
13
+ function showWelcomeBanner() {
14
+ console.log('');
15
+ console.log(chalk.cyan.bold('╔═══════════════════════════════════════════════════════════╗'));
16
+ console.log(chalk.cyan.bold('║') + chalk.white.bold(' 🚀 Next.js Fullstack Agent CLI ') + chalk.cyan.bold('║'));
17
+ console.log(chalk.cyan.bold('║') + chalk.gray(' Build full-stack apps with AI assistance ') + chalk.cyan.bold('║'));
18
+ console.log(chalk.cyan.bold('╚═══════════════════════════════════════════════════════════╝'));
19
+ console.log('');
20
+ }
21
+ export const startCommand = new Command('start')
22
+ .description('Start the AI Agent')
23
+ .option('-n, --project-name <name>', 'Project name (will be created in current directory)')
24
+ .option('-m, --max-iterations <number>', 'Maximum agent iterations', '500')
25
+ .option('--skip-db', 'Skip PostgreSQL configuration (for static sites)')
26
+ .option('--no-verbose', 'Disable verbose logging')
27
+ .action(async (options) => {
28
+ try {
29
+ // Show welcome banner
30
+ showWelcomeBanner();
31
+ const spinner = ora({
32
+ text: chalk.blue('Initializing Next.js Agent...'),
33
+ spinner: 'dots'
34
+ }).start();
35
+ // Small delay for visual effect
36
+ await new Promise(resolve => setTimeout(resolve, 500));
37
+ spinner.succeed(chalk.green('Ready to build!'));
38
+ console.log('');
39
+ // 1. Initial Input Resolution
40
+ let currentPrompt;
41
+ let projectName = options.projectName;
42
+ if (!projectName) {
43
+ const answer = await inquirer.prompt([{
44
+ type: 'input',
45
+ name: 'projectName',
46
+ message: 'What is your project name?',
47
+ default: 'my-app',
48
+ validate: (input) => {
49
+ // Validate project name (no spaces, special chars)
50
+ if (/^[a-z0-9-]+$/.test(input))
51
+ return true;
52
+ return 'Project name should only contain lowercase letters, numbers, and hyphens';
53
+ }
54
+ }]);
55
+ projectName = answer.projectName;
56
+ }
57
+ // Derive projectPath from projectName (absolute path for consistency)
58
+ const projectPath = path.resolve(process.cwd(), projectName);
59
+ // API Key resolution: env -> credential store -> prompt
60
+ let geminiKey = process.env.GEMINI_API_KEY;
61
+ if (!geminiKey) {
62
+ // Try credential store
63
+ geminiKey = await getGeminiApiKey();
64
+ if (geminiKey) {
65
+ logger.info(`Using API key from ${getCredPath()}`);
66
+ }
67
+ }
68
+ if (!geminiKey) {
69
+ // Prompt for key
70
+ const answers = await inquirer.prompt([{
71
+ type: 'password',
72
+ name: 'apiKey',
73
+ message: 'Please enter your Gemini API Key:',
74
+ validate: (input) => input.length > 0 ? true : 'API Key is required'
75
+ }]);
76
+ const key = answers.apiKey;
77
+ // Ask if user wants to save to credential store
78
+ const saveAnswer = await inquirer.prompt([{
79
+ type: 'confirm',
80
+ name: 'save',
81
+ message: 'Save API key for future sessions?',
82
+ default: true
83
+ }]);
84
+ if (saveAnswer.save) {
85
+ await setGeminiApiKey(key);
86
+ logger.success(`API key saved to ${getCredPath()}`);
87
+ }
88
+ geminiKey = key;
89
+ }
90
+ if (!geminiKey) {
91
+ logger.error('Failed to resolve Gemini API Key');
92
+ process.exit(1);
93
+ }
94
+ const maxIterations = parseInt(options.maxIterations, 10);
95
+ const verbose = options.verbose ?? true; // Commander with --no-verbose sets this to true by default, false if flag used.
96
+ // 2. Interactive Loop
97
+ let iteration = 1;
98
+ let active = true;
99
+ // Ask for model selection
100
+ const modelAnswer = await inquirer.prompt([{
101
+ type: 'select',
102
+ name: 'model',
103
+ message: 'Select Gemini Model:',
104
+ choices: [
105
+ 'gemini-3-flash-preview',
106
+ 'gemini-3-pro-preview'
107
+ ],
108
+ default: 'gemini-3-flash-preview'
109
+ }]);
110
+ // Database Creds (Step-by-Step) - Skip if --skip-db is used
111
+ let databaseUrl;
112
+ if (!options.skipDb) {
113
+ const defaultDbName = projectName;
114
+ console.log('\n--- PostgreSQL Configuration ---');
115
+ const dbCreds = await inquirer.prompt([
116
+ {
117
+ type: 'input',
118
+ name: 'host',
119
+ message: 'Host:',
120
+ default: 'localhost'
121
+ },
122
+ {
123
+ type: 'input',
124
+ name: 'port',
125
+ message: 'Port:',
126
+ default: '5432'
127
+ },
128
+ {
129
+ type: 'input',
130
+ name: 'username',
131
+ message: 'Username:',
132
+ default: 'postgres'
133
+ },
134
+ {
135
+ type: 'input',
136
+ name: 'password',
137
+ message: 'Password:',
138
+ default: 'postgres'
139
+ },
140
+ {
141
+ type: 'input',
142
+ name: 'dbName',
143
+ message: 'Database Name:',
144
+ default: defaultDbName
145
+ }
146
+ ]);
147
+ databaseUrl = `postgresql://${dbCreds.username}:${dbCreds.password}@${dbCreds.host}:${dbCreds.port}/${dbCreds.dbName}`;
148
+ }
149
+ else {
150
+ logger.info('Skipping PostgreSQL configuration (--skip-db)');
151
+ }
152
+ const config = {
153
+ geminiApiKey: geminiKey,
154
+ maxIterations: maxIterations,
155
+ verbose: verbose,
156
+ modelName: modelAnswer.model,
157
+ };
158
+ // Initialize agent once
159
+ const agent = new Agent(config);
160
+ await agent.init();
161
+ while (active) {
162
+ if (!currentPrompt) {
163
+ const answer = await inquirer.prompt([{
164
+ type: 'input',
165
+ name: 'prompt',
166
+ message: 'What do you want to build or modify?',
167
+ validate: (input) => input.trim() !== '' ? true : 'Prompt is required'
168
+ }]);
169
+ currentPrompt = answer.prompt;
170
+ }
171
+ if (currentPrompt && currentPrompt.toLowerCase() === 'exit') {
172
+ console.log('Exiting...');
173
+ break;
174
+ }
175
+ logger.info(`\n--- Iteration ${iteration} ---`);
176
+ logger.info(`Goal: ${currentPrompt}`);
177
+ logger.info(`Project: ${projectPath}`);
178
+ // Enhance prompt with serial artifact instructions
179
+ const enhancedPrompt = `${currentPrompt}\n\nIMPORTANT RULES:
180
+ 1. You MUST create the following plan files inside "${projectPath}/.agent":
181
+ - "plan${iteration}.md" (Implementation Plan)
182
+ - "task${iteration}.md" (Task Checklist)
183
+ - Update "task${iteration}.md" as you progress.
184
+ 2. If creating a new project, use "create_nextjs_project" with "projectPath" set EXACTLY to "${projectPath}".
185
+ 3. CRITICAL: Treat "${projectPath}" as your ROOT working directory. All file writes and commands must be executed relative to this path. Do NOT create arbitrary subdirectories for the project itself.`;
186
+ // Execute agent
187
+ try {
188
+ if (iteration === 1) {
189
+ const task = {
190
+ prompt: enhancedPrompt,
191
+ projectPath: projectPath,
192
+ databaseUrl: databaseUrl
193
+ };
194
+ await agent.start(task);
195
+ }
196
+ else {
197
+ await agent.chat(enhancedPrompt);
198
+ }
199
+ logger.success(`Iteration ${iteration} completed!`);
200
+ // Post-generation Guide
201
+ console.log(`\n--------------------------------------------`);
202
+ console.log(`To run your project:`);
203
+ console.log(` cd ${projectPath}`);
204
+ console.log(` pnpm dev`);
205
+ console.log(`--------------------------------------------`);
206
+ console.log(`Artifacts stored in: ${projectPath}/.agent/plan${iteration}.md`);
207
+ console.log(`--------------------------------------------\n`);
208
+ }
209
+ catch (error) {
210
+ logger.error(`Iteration failed: ${error.message}`);
211
+ // Don't exit process, allow user to try again or exit
212
+ }
213
+ // Prepare for next loop
214
+ iteration++;
215
+ currentPrompt = ''; // Reset prompt to force simple ask
216
+ // Simple "Next" Prompt
217
+ const nextStep = await inquirer.prompt([{
218
+ type: 'input',
219
+ name: 'next',
220
+ message: 'Enter your next request, feedback, or type "exit" to quit:'
221
+ }]);
222
+ const userNext = nextStep.next.trim();
223
+ if (userNext.toLowerCase() === 'exit') {
224
+ active = false;
225
+ }
226
+ else {
227
+ currentPrompt = userNext;
228
+ }
229
+ }
230
+ logger.success('Session ended. Happy coding!');
231
+ }
232
+ catch (error) {
233
+ logger.error(`Error: ${error.message || 'Unknown error'}`);
234
+ process.exit(1);
235
+ }
236
+ });
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { startCommand } from './commands/start.js';
4
+ import { configCommand } from './commands/config.js';
5
+ const program = new Command();
6
+ program
7
+ .name('next-agent')
8
+ .description('Next.js Fullstack Agent CLI')
9
+ .version('1.0.0');
10
+ program.addCommand(startCommand);
11
+ program.addCommand(configCommand);
12
+ program.parse();