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 ADDED
@@ -0,0 +1,188 @@
1
+ # Next.js Fullstack Agent CLI
2
+
3
+ Gemini 3 Hackathon Entry 2026
4
+
5
+ **Gemini Next.js Agent CLI** is an autonomous, tool-driven developer agent that transforms natural language into verified, production-ready Next.js fullstack applications.
6
+
7
+ The agent plans tasks, executes real commands, verifies results, attempts repairs, and asks for manual input only when automation is unsafe.
8
+
9
+ ---
10
+
11
+ ## Overview
12
+
13
+ This CLI is designed to behave like a real developer inside a real project environment.
14
+
15
+ Instead of only generating code, the agent:
16
+ - Creates and modifies files directly
17
+ - Runs installs and builds
18
+ - Verifies results through execution
19
+ - Attempts automated fixes when failures occur
20
+ - Pauses for human input when decisions require clarity
21
+
22
+ The focus is reliability, correctness, and real execution.
23
+
24
+ ---
25
+
26
+ ## Features
27
+
28
+ - Runtime: Node.js
29
+ - Language: TypeScript
30
+ - CLI Framework: Commander, Inquirer
31
+ - Agent: Gemini-powered agentic workflow
32
+ - Planning before execution
33
+ - Real filesystem operations
34
+ - Build and runtime verification
35
+ - Targeted self-repair for failures
36
+
37
+ ---
38
+
39
+ ## Requirements
40
+
41
+ Before installing, ensure you have:
42
+
43
+ - Node.js 18 or later
44
+ - npm or pnpm
45
+ - PostgreSQL (optional, only required if your project uses a database)
46
+ - A Gemini API key
47
+
48
+ ---
49
+
50
+ ## Installation (Step by Step)
51
+
52
+ 1. Clone the repository
53
+ ```bash
54
+ git clone https://github.com/kywthiha/nextjs-agent-cli.git
55
+ ```
56
+
57
+ 2. Move into the project directory
58
+ ```bash
59
+ cd nextjs-agent-cli
60
+ ```
61
+
62
+ 3. Install dependencies
63
+ ```bash
64
+ npm install
65
+ ```
66
+
67
+ 4. Configure environment variables
68
+ ```bash
69
+ cp .env.example .env
70
+ ```
71
+
72
+ Add your Gemini API key to the `.env` file:
73
+ ```
74
+ GEMINI_API_KEY=your_api_key_here
75
+ ```
76
+
77
+ ---
78
+
79
+ ## Running the CLI
80
+
81
+ Start the CLI in development mode:
82
+
83
+ ```bash
84
+ npm run dev
85
+ ```
86
+
87
+ The agent will launch an interactive session in your terminal.
88
+
89
+ ---
90
+
91
+ ## Interactive Workflow
92
+
93
+ ### 1. Project Creation
94
+
95
+ Choose where the Next.js project should be created.
96
+ ```
97
+ ? Where should the project be created? (./my-app)
98
+ ```
99
+
100
+ ---
101
+
102
+ ### 2. Model Selection
103
+
104
+ Select the Gemini model for the agent.
105
+ - gemini-3-flash-preview
106
+ - gemini-3-pro-preview
107
+
108
+ ---
109
+
110
+ ### 3. Database Configuration (Optional)
111
+
112
+ If your project requires a database, configure PostgreSQL:
113
+ ```
114
+ --- PostgreSQL Configuration ---
115
+ ? Host: localhost
116
+ ? Port: 5432
117
+ ? Username: postgres
118
+ ? Password: [HIDDEN]
119
+ ```
120
+
121
+ The agent will:
122
+ - Verify the connection
123
+ - Create the database if it does not exist
124
+ - Handle schema setup when required
125
+
126
+ ---
127
+
128
+ ### 4. Define Your Goal
129
+
130
+ Describe what you want to build using natural language.
131
+ ```
132
+ ? What do you want to build or modify?
133
+ > Build inventory management for a mobile shop with POS and user management
134
+ ```
135
+
136
+ The agent will then:
137
+ - Generate an implementation plan (plan.md)
138
+ - Generate a task checklist (task.md)
139
+ - Execute tasks step by step
140
+ - Verify builds and runtime behavior
141
+ - Attempt fixes if errors occur
142
+
143
+ ---
144
+
145
+ ## Testing and Verification
146
+
147
+ Verification is a core part of the agent’s workflow.
148
+
149
+ During execution, the agent may:
150
+ - Run dependency installs
151
+ - Run builds
152
+ - Detect runtime or build failures
153
+ - Attempt automated fixes
154
+ - Re-run verification steps
155
+
156
+ If a failure requires a business or architectural decision, the agent will pause and request manual input instead of guessing.
157
+
158
+ ---
159
+
160
+ ## Manual Testing (Optional)
161
+
162
+ After the agent finishes, you can manually verify the generated project:
163
+
164
+ ```bash
165
+ cd my-app
166
+ npm run dev
167
+ ```
168
+
169
+ Open the browser at:
170
+ ```
171
+ http://localhost:3000
172
+ ```
173
+
174
+ ---
175
+
176
+ ## Limitations
177
+
178
+ - Some runtime issues require human decisions
179
+ - Complex architecture changes may need guidance
180
+ - The agent prioritizes safety and correctness over aggressive automation
181
+
182
+ These constraints are intentional.
183
+
184
+ ---
185
+
186
+ ## License
187
+
188
+ MIT License
@@ -0,0 +1,340 @@
1
+ /**
2
+ * Core AI Agent - Agentic loop pattern with Gemini 3 SDK
3
+ */
4
+ import { GoogleGenAI, ThinkingLevel } from '@google/genai';
5
+ import { logger } from '../utils/logger.js';
6
+ import { AGENT_SYSTEM_PROMPT, createTaskPrompt } from './prompts/agent-prompt.js';
7
+ import { getAllTools } from './tools/index.js';
8
+ import { ConversationSummarizer } from './summarizer.js';
9
+ const MAX_API_RETRIES = 5;
10
+ const INITIAL_RETRY_DELAY_MS = 2000;
11
+ // Token management constants
12
+ const MAX_CONTEXT_TOKENS = 1000000; // 1M input context (Gemini 3 Pro/Flash), 64k output
13
+ const COMPRESSION_THRESHOLD = 0.7; // Compress at 70% capacity
14
+ const KEEP_RECENT_MESSAGES = 10; // Always keep last N messages
15
+ function sleep(ms) {
16
+ return new Promise(resolve => setTimeout(resolve, ms));
17
+ }
18
+ export class Agent {
19
+ client;
20
+ config;
21
+ tools;
22
+ conversation = [];
23
+ activeProjectPath = null;
24
+ isInitialized = false;
25
+ originalTaskPrompt = '';
26
+ summarizer;
27
+ lastTokenCount = 0; // Actual token count from last API response
28
+ constructor(config) {
29
+ this.config = config;
30
+ this.client = new GoogleGenAI({ apiKey: config.geminiApiKey });
31
+ this.tools = getAllTools();
32
+ this.summarizer = new ConversationSummarizer(config.geminiApiKey, config.modelName);
33
+ }
34
+ getThinkingLevel(modelName) {
35
+ return modelName.includes('pro') ? ThinkingLevel.HIGH : ThinkingLevel.MEDIUM;
36
+ }
37
+ async init() {
38
+ if (this.isInitialized)
39
+ return;
40
+ this.isInitialized = true;
41
+ }
42
+ async start(task) {
43
+ if (!this.isInitialized)
44
+ await this.init();
45
+ const { prompt, projectPath, databaseUrl } = task;
46
+ this.activeProjectPath = projectPath;
47
+ logger.step('Starting Full Stack Agent');
48
+ logger.info(`Goal: ${prompt}`);
49
+ logger.info(`Output: ${projectPath}`);
50
+ if (databaseUrl)
51
+ logger.info(`DB URL provided`);
52
+ const setupPrompt = createTaskPrompt(prompt, projectPath, databaseUrl);
53
+ this.originalTaskPrompt = setupPrompt;
54
+ // Check for previous summary to continue from
55
+ const previousSummary = await this.summarizer.loadSummary(projectPath);
56
+ if (previousSummary) {
57
+ logger.info('Found previous progress summary, continuing...');
58
+ this.conversation = [{
59
+ role: 'user',
60
+ parts: [{ text: `${setupPrompt}\n\n## Previous Progress:\n${previousSummary}\n\nContinue from where you left off.` }]
61
+ }];
62
+ }
63
+ else {
64
+ this.conversation = [{
65
+ role: 'user',
66
+ parts: [{ text: setupPrompt }]
67
+ }];
68
+ }
69
+ await this.executeTaskLoop('Planning-Phase');
70
+ logger.step('Agent session ended - Implementation Complete');
71
+ }
72
+ async chat(message) {
73
+ if (!this.isInitialized)
74
+ await this.init();
75
+ if (!this.activeProjectPath) {
76
+ throw new Error('Agent has not been started. Call start() first.');
77
+ }
78
+ logger.step('Continuing Agent Session');
79
+ logger.info(`Request: ${message}`);
80
+ this.conversation.push({
81
+ role: 'user',
82
+ parts: [{ text: message }]
83
+ });
84
+ await this.executeTaskLoop('Follow-up-Phase');
85
+ logger.step('Request Completed');
86
+ }
87
+ async executeTaskLoop(contextId) {
88
+ let iteration = 0;
89
+ let isComplete = false;
90
+ const maxIterations = this.config.maxIterations;
91
+ while (!isComplete && iteration < maxIterations) {
92
+ iteration++;
93
+ logger.step(`[${contextId}] Iteration ${iteration}/${maxIterations}`);
94
+ // Check and compress conversation if needed
95
+ await this.checkAndCompressConversation();
96
+ try {
97
+ const response = await this.callAIWithRetry();
98
+ // Track actual token usage from SDK response
99
+ this.updateTokenCount(response);
100
+ const { text, toolCalls, functionResponseParts } = this.parseResponse(response);
101
+ if (text) {
102
+ logger.dim('AI: ' + text.substring(0, 200) + (text.length > 200 ? '...' : ''));
103
+ // Check for completion signal
104
+ if (text.includes('TASK COMPLETE')) {
105
+ logger.success(`[${contextId}] Task Complete!`);
106
+ isComplete = true;
107
+ continue;
108
+ }
109
+ }
110
+ if (toolCalls.length === 0) {
111
+ if (!text) {
112
+ logger.warn('No response from AI, retrying...');
113
+ continue;
114
+ }
115
+ // AI just responded with text, ask it to continue
116
+ this.conversation.push({
117
+ role: 'user',
118
+ parts: [{ text: 'Continue with the task. Use tools as needed.' }]
119
+ });
120
+ continue;
121
+ }
122
+ const toolResults = await this.executeTools(toolCalls);
123
+ this.conversation.push({
124
+ role: 'model',
125
+ parts: functionResponseParts.length > 0 ? functionResponseParts : [{ text: text || '' }]
126
+ });
127
+ this.conversation.push({
128
+ role: 'user',
129
+ parts: toolResults.map(r => ({
130
+ functionResponse: {
131
+ name: r.name,
132
+ response: { result: r.result }
133
+ }
134
+ }))
135
+ });
136
+ }
137
+ catch (error) {
138
+ logger.error(`[${contextId}] Iteration ${iteration} error: ${error.message}`);
139
+ // Check if token limit exceeded
140
+ if (this.isTokenLimitError(error)) {
141
+ logger.warn('Token limit exceeded, generating summary and restarting...');
142
+ await this.handleTokenLimitExceeded();
143
+ continue;
144
+ }
145
+ this.conversation.push({
146
+ role: 'user',
147
+ parts: [{ text: `Error: ${error.message}. Try a different approach.` }]
148
+ });
149
+ }
150
+ }
151
+ if (!isComplete) {
152
+ logger.warn(`[${contextId}] Reached max iterations (${maxIterations}) without completion signal`);
153
+ }
154
+ }
155
+ async callAIWithRetry() {
156
+ let lastError = null;
157
+ for (let attempt = 1; attempt <= MAX_API_RETRIES; attempt++) {
158
+ try {
159
+ return await this.callAI();
160
+ }
161
+ catch (error) {
162
+ lastError = error;
163
+ const errorMessage = error.message || '';
164
+ const errorString = JSON.stringify(error);
165
+ // Check if it's a retryable error (503, 429, overloaded, etc.)
166
+ const isRetryable = errorMessage.includes('503') ||
167
+ errorMessage.includes('429') ||
168
+ errorMessage.includes('overloaded') ||
169
+ errorMessage.includes('UNAVAILABLE') ||
170
+ errorMessage.includes('rate limit') ||
171
+ errorMessage.includes('quota') ||
172
+ errorString.includes('503') ||
173
+ errorString.includes('overloaded');
174
+ if (isRetryable && attempt < MAX_API_RETRIES) {
175
+ // Exponential backoff: 2s, 4s, 8s, 16s, 32s
176
+ const delayMs = INITIAL_RETRY_DELAY_MS * Math.pow(2, attempt - 1);
177
+ logger.warn(`API error (${attempt}/${MAX_API_RETRIES}), retrying in ${delayMs / 1000}s...`);
178
+ await sleep(delayMs);
179
+ }
180
+ else if (!isRetryable) {
181
+ // Non-retryable error, throw immediately
182
+ throw error;
183
+ }
184
+ }
185
+ }
186
+ // All retries exhausted
187
+ throw lastError || new Error('API call failed after max retries');
188
+ }
189
+ async callAI() {
190
+ const functionDeclarations = this.tools.map(t => ({
191
+ name: t.name,
192
+ description: t.description,
193
+ parametersJsonSchema: {
194
+ type: 'object',
195
+ properties: Object.fromEntries(Object.entries(t.parameters.properties || {}).map(([key, value]) => [
196
+ key,
197
+ {
198
+ type: value.type,
199
+ description: value.description,
200
+ ...(value.enum && { enum: value.enum })
201
+ }
202
+ ])),
203
+ required: t.parameters.required || []
204
+ }
205
+ }));
206
+ const tools = [{ functionDeclarations }];
207
+ const modelName = this.config.modelName || 'gemini-3-flash-preview';
208
+ const thinkingLevel = this.getThinkingLevel(modelName);
209
+ const response = await this.client.models.generateContent({
210
+ model: modelName,
211
+ contents: this.conversation,
212
+ config: {
213
+ systemInstruction: AGENT_SYSTEM_PROMPT,
214
+ tools,
215
+ thinkingConfig: {
216
+ thinkingLevel,
217
+ },
218
+ }
219
+ });
220
+ return response;
221
+ }
222
+ parseResponse(response) {
223
+ const parts = response.candidates?.[0]?.content?.parts || [];
224
+ let text = '';
225
+ const toolCalls = [];
226
+ const functionResponseParts = [];
227
+ for (const part of parts) {
228
+ if (part.text) {
229
+ text += part.text;
230
+ }
231
+ if (part.functionCall) {
232
+ toolCalls.push({
233
+ id: `call_${Date.now()}_${toolCalls.length}`,
234
+ name: part.functionCall.name || '',
235
+ arguments: part.functionCall.args || {}
236
+ });
237
+ functionResponseParts.push(part);
238
+ }
239
+ }
240
+ return { text, toolCalls, functionResponseParts };
241
+ }
242
+ async executeTools(toolCalls) {
243
+ const preparedCalls = toolCalls.map(call => {
244
+ const tool = this.tools.find(t => t.name === call.name);
245
+ // Enforce project path
246
+ if (this.activeProjectPath) {
247
+ if (call.arguments.cwd !== undefined) {
248
+ call.arguments.cwd = this.activeProjectPath;
249
+ }
250
+ if (call.arguments.projectPath !== undefined) {
251
+ call.arguments.projectPath = this.activeProjectPath;
252
+ }
253
+ }
254
+ return { call, tool };
255
+ });
256
+ const promises = preparedCalls.map(async ({ call, tool }) => {
257
+ if (!tool) {
258
+ logger.warn(`Tool not found: ${call.name}`);
259
+ return { name: call.name, result: `Error: Tool "${call.name}" not found` };
260
+ }
261
+ if (this.config.verbose) {
262
+ logger.dim(`Tool: ${call.name}(${JSON.stringify(call.arguments)})`);
263
+ }
264
+ else {
265
+ logger.info(` → ${call.name}`);
266
+ }
267
+ try {
268
+ const result = await tool.execute(call.arguments);
269
+ if (this.config.verbose) {
270
+ const preview = result.length > 100 ? result.substring(0, 100) + '...' : result;
271
+ logger.dim(` Result: ${preview}`);
272
+ }
273
+ return { name: call.name, result };
274
+ }
275
+ catch (error) {
276
+ logger.error(`Tool ${call.name} failed: ${error.message}`);
277
+ return { name: call.name, result: `Error: ${error.message}` };
278
+ }
279
+ });
280
+ return await Promise.all(promises);
281
+ }
282
+ // ==================== Token Management ====================
283
+ isTokenLimitError(error) {
284
+ const msg = error.message?.toLowerCase() || '';
285
+ return msg.includes('token') ||
286
+ msg.includes('context length') ||
287
+ msg.includes('too long') ||
288
+ msg.includes('maximum context');
289
+ }
290
+ updateTokenCount(response) {
291
+ const usage = response.usageMetadata;
292
+ if (usage?.promptTokenCount) {
293
+ this.lastTokenCount = usage.promptTokenCount;
294
+ if (this.config.verbose) {
295
+ logger.dim(`Input tokens: ${this.lastTokenCount.toLocaleString()} / ${MAX_CONTEXT_TOKENS.toLocaleString()}`);
296
+ }
297
+ }
298
+ }
299
+ async checkAndCompressConversation() {
300
+ const threshold = MAX_CONTEXT_TOKENS * COMPRESSION_THRESHOLD;
301
+ if (this.lastTokenCount > threshold) {
302
+ logger.info(`Token usage ${this.lastTokenCount.toLocaleString()}/${MAX_CONTEXT_TOKENS.toLocaleString()} (${Math.round(this.lastTokenCount / MAX_CONTEXT_TOKENS * 100)}%), compressing...`);
303
+ this.compressConversation();
304
+ }
305
+ }
306
+ compressConversation() {
307
+ if (this.conversation.length <= KEEP_RECENT_MESSAGES + 1) {
308
+ return; // Nothing to compress
309
+ }
310
+ // Keep first message (original task) and last N messages
311
+ const firstMessage = this.conversation[0];
312
+ const recentMessages = this.conversation.slice(-KEEP_RECENT_MESSAGES);
313
+ this.conversation = [firstMessage, ...recentMessages];
314
+ logger.dim(`Compressed conversation to ${this.conversation.length} messages`);
315
+ }
316
+ async handleTokenLimitExceeded() {
317
+ if (!this.activeProjectPath) {
318
+ this.compressConversation();
319
+ return;
320
+ }
321
+ try {
322
+ // Generate summary using Gemini
323
+ logger.dim('Generating progress summary with Gemini...');
324
+ const summary = await this.summarizer.generateSummary(this.conversation);
325
+ // Save to file
326
+ await this.summarizer.saveSummary(this.activeProjectPath, summary);
327
+ // Restart with fresh conversation including summary
328
+ this.conversation = [{
329
+ role: 'user',
330
+ parts: [{ text: `${this.originalTaskPrompt}\n\n## Previous Progress:\n${summary}\n\nContinue from where you left off.` }]
331
+ }];
332
+ logger.success('Conversation reset with AI-generated progress summary');
333
+ }
334
+ catch (error) {
335
+ logger.error(`Failed to handle token limit: ${error.message}`);
336
+ // Fallback: just compress aggressively
337
+ this.compressConversation();
338
+ }
339
+ }
340
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Agent module exports
3
+ */
4
+ export { Agent } from './agent.js';
5
+ export { AGENT_SYSTEM_PROMPT, createTaskPrompt } from './prompts/agent-prompt.js';
6
+ export { getAllTools } from './tools/index.js';