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.
- package/README.md +188 -0
- package/dist/agent/agent.js +340 -0
- package/dist/agent/index.js +6 -0
- package/dist/agent/prompts/agent-prompt.js +527 -0
- package/dist/agent/summarizer.js +97 -0
- package/dist/agent/tools/ast-tools.js +601 -0
- package/dist/agent/tools/code-tools.js +1059 -0
- package/dist/agent/tools/file-tools.js +199 -0
- package/dist/agent/tools/index.js +25 -0
- package/dist/agent/tools/search-tools.js +404 -0
- package/dist/agent/tools/shell-tools.js +334 -0
- package/dist/agent/types.js +4 -0
- package/dist/cli/commands/config.js +61 -0
- package/dist/cli/commands/start.js +236 -0
- package/dist/cli/index.js +12 -0
- package/dist/utils/cred-store.js +70 -0
- package/dist/utils/logger.js +9 -0
- package/package.json +52 -0
|
@@ -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,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();
|