@myvillage/cli 1.7.1 → 1.8.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.
@@ -1,5 +1,6 @@
1
1
  import { mkdirSync, writeFileSync } from 'fs';
2
2
  import { join } from 'path';
3
+ import crypto from 'crypto';
3
4
 
4
5
  // MyVillage brand colors
5
6
  const BRAND = {
@@ -27,7 +28,7 @@ const TOOL_GROUPS = {
27
28
  meetings: ['recall_send_bot', 'recall_meeting_attendance'],
28
29
  };
29
30
 
30
- // ─── Agentic App Template ───────────────────────────────────────────────────
31
+ // ─── Agentic App Template (Next.js) ─────────────────────────────────────────
31
32
 
32
33
  export function createAgenticAppProject(targetDir, options) {
33
34
  const {
@@ -53,100 +54,113 @@ export function createAgenticAppProject(targetDir, options) {
53
54
  const dirs = [
54
55
  '',
55
56
  'public',
56
- 'server',
57
- 'server/agent',
58
- 'src',
59
- 'src/components',
60
- 'src/pages',
57
+ 'app',
58
+ 'lib',
59
+ 'components',
61
60
  ];
62
61
 
63
- if (includeMcp) {
64
- dirs.push('server/mcp');
65
- }
66
- if (hasOAuth || hasEmailPassword) {
67
- dirs.push('src/auth');
68
- dirs.push('server/auth');
62
+ if (hasOAuth) {
63
+ dirs.push(
64
+ 'app/login',
65
+ 'app/api/auth/login',
66
+ 'app/api/auth/callback',
67
+ 'app/api/auth/session',
68
+ 'app/api/auth/logout',
69
+ );
69
70
  }
71
+
72
+ dirs.push('app/api/agent/chat');
73
+
70
74
  if (includeRestApi) {
71
- dirs.push('server/api');
75
+ dirs.push('app/api/proxy/[...path]');
72
76
  }
73
77
 
78
+ if (hasDashboard) dirs.push('app/dashboard');
79
+ if (hasSettings) dirs.push('app/settings');
80
+ if (hasUsers) dirs.push('app/users');
81
+ if (hasNotifications) dirs.push('app/notifications');
82
+
74
83
  for (const dir of dirs) {
75
84
  mkdirSync(join(targetDir, dir), { recursive: true });
76
85
  }
77
86
 
78
87
  // Write root files
79
88
  writeFileSync(join(targetDir, 'package.json'), generatePackageJson(slug, description, includeMcp));
80
- writeFileSync(join(targetDir, 'vite.config.js'), generateViteConfig());
89
+ writeFileSync(join(targetDir, 'next.config.js'), generateNextConfig());
81
90
  writeFileSync(join(targetDir, '.gitignore'), generateGitignore());
82
- writeFileSync(join(targetDir, '.env'), generateEnv(oauthCredentials, hasOAuth, includeMcp));
91
+ writeFileSync(join(targetDir, '.env.local'), generateEnv(oauthCredentials, hasOAuth, includeMcp));
83
92
  writeFileSync(join(targetDir, '.env.example'), generateEnvExample(hasOAuth, includeMcp));
84
- writeFileSync(join(targetDir, 'index.html'), generateIndexHtml(name));
85
93
  writeFileSync(join(targetDir, 'README.md'), generateReadme(name, description, hasOAuth, includeMcp, includeRestApi, features));
94
+ writeFileSync(join(targetDir, 'jsconfig.json'), generateJsConfig());
86
95
 
87
- // Backend: Express server with agent + integrations
88
- writeFileSync(join(targetDir, 'server/index.js'), generateEntrypoint(name, hasOAuth, hasEmailPassword, includeMcp, includeRestApi));
89
- writeFileSync(join(targetDir, 'server/agent/index.js'), generateAgent(includeMcp));
96
+ // Lib: server-side modules
97
+ writeFileSync(join(targetDir, 'lib/agent.js'), generateAgent(includeMcp));
90
98
 
91
99
  if (includeMcp) {
92
- writeFileSync(join(targetDir, 'server/mcp/client.js'), generateMcpClient(mcpToolGroups));
100
+ writeFileSync(join(targetDir, 'lib/mcp-client.js'), generateMcpClient(mcpToolGroups));
93
101
  }
94
102
  if (hasOAuth) {
95
- writeFileSync(join(targetDir, 'server/auth/oauth.js'), generateOAuthModule());
103
+ writeFileSync(join(targetDir, 'lib/oauth.js'), generateOAuthLib());
104
+ writeFileSync(join(targetDir, 'lib/session.js'), generateSessionLib());
105
+ writeFileSync(join(targetDir, 'middleware.js'), generateMiddleware());
96
106
  }
97
- if (includeRestApi) {
98
- writeFileSync(join(targetDir, 'server/api/routes.js'), generateApiRoutes(hasOAuth));
107
+
108
+ // API routes
109
+ writeFileSync(join(targetDir, 'app/api/agent/chat/route.js'), generateAgentChatRoute(hasOAuth));
110
+
111
+ if (hasOAuth) {
112
+ writeFileSync(join(targetDir, 'app/api/auth/login/route.js'), generateAuthLoginRoute());
113
+ writeFileSync(join(targetDir, 'app/api/auth/callback/route.js'), generateAuthCallbackRoute());
114
+ writeFileSync(join(targetDir, 'app/api/auth/session/route.js'), generateAuthSessionRoute());
115
+ writeFileSync(join(targetDir, 'app/api/auth/logout/route.js'), generateAuthLogoutRoute());
99
116
  }
100
117
 
101
- // Frontend: React + Vite
102
- writeFileSync(join(targetDir, 'src/main.jsx'), generateMainJsx());
103
- writeFileSync(join(targetDir, 'src/App.jsx'), generateAppJsx(features, hasOAuth));
104
- writeFileSync(join(targetDir, 'src/App.css'), generateAppCss());
105
- writeFileSync(join(targetDir, 'src/index.css'), generateIndexCss());
118
+ if (includeRestApi) {
119
+ writeFileSync(join(targetDir, 'app/api/proxy/[...path]/route.js'), generateProxyCatchAll(hasOAuth));
120
+ }
106
121
 
107
- // Components
108
- writeFileSync(join(targetDir, 'src/components/Layout.jsx'), generateLayoutComponent());
109
- writeFileSync(join(targetDir, 'src/components/Sidebar.jsx'), generateSidebarComponent(features));
110
- writeFileSync(join(targetDir, 'src/components/Header.jsx'), generateHeaderComponent(hasOAuth));
111
- writeFileSync(join(targetDir, 'src/components/AgentChat.jsx'), generateAgentChatComponent());
122
+ // App: layout, pages, styles
123
+ writeFileSync(join(targetDir, 'app/layout.jsx'), generateRootLayout(name, hasOAuth, features));
124
+ writeFileSync(join(targetDir, 'app/globals.css'), generateGlobalsCss());
125
+ writeFileSync(join(targetDir, 'app/page.jsx'), generateHomePage(hasDashboard));
126
+ writeFileSync(join(targetDir, 'app/not-found.jsx'), generateNotFoundPage());
112
127
 
113
128
  if (hasOAuth) {
114
- writeFileSync(join(targetDir, 'src/components/ProtectedRoute.jsx'), generateProtectedRoute());
115
- writeFileSync(join(targetDir, 'src/auth/oauth.js'), generateBrowserOAuthModule());
116
- writeFileSync(join(targetDir, 'src/auth/AuthProvider.jsx'), generateAuthProvider());
117
- writeFileSync(join(targetDir, 'src/pages/Login.jsx'), generateLoginPage());
118
- writeFileSync(join(targetDir, 'src/pages/Callback.jsx'), generateCallbackPage());
129
+ writeFileSync(join(targetDir, 'app/login/page.jsx'), generateLoginPage());
119
130
  }
120
-
121
- // Pages
122
131
  if (hasDashboard) {
123
- writeFileSync(join(targetDir, 'src/pages/Dashboard.jsx'), generateDashboardPage(includeMcp));
132
+ writeFileSync(join(targetDir, 'app/dashboard/page.jsx'), generateDashboardPage(includeMcp));
124
133
  }
125
134
  if (hasSettings) {
126
- writeFileSync(join(targetDir, 'src/pages/Settings.jsx'), generateSettingsPage());
135
+ writeFileSync(join(targetDir, 'app/settings/page.jsx'), generateSettingsPage());
127
136
  }
128
137
  if (hasUsers) {
129
- writeFileSync(join(targetDir, 'src/pages/Users.jsx'), generateUsersPage());
138
+ writeFileSync(join(targetDir, 'app/users/page.jsx'), generateUsersPage());
130
139
  }
131
140
  if (hasNotifications) {
132
- writeFileSync(join(targetDir, 'src/pages/Notifications.jsx'), generateNotificationsPage());
141
+ writeFileSync(join(targetDir, 'app/notifications/page.jsx'), generateNotificationsPage());
133
142
  }
134
143
 
135
- writeFileSync(join(targetDir, 'src/pages/NotFound.jsx'), generateNotFoundPage());
144
+ // Components
145
+ writeFileSync(join(targetDir, 'components/Sidebar.jsx'), generateSidebarComponent(features));
146
+ writeFileSync(join(targetDir, 'components/Header.jsx'), generateHeaderComponent(hasOAuth));
147
+ writeFileSync(join(targetDir, 'components/AgentChat.jsx'), generateAgentChatComponent());
148
+
149
+ if (hasOAuth) {
150
+ writeFileSync(join(targetDir, 'components/SignOutButton.jsx'), generateSignOutButton());
151
+ }
136
152
  }
137
153
 
138
- // ─── Generator Functions ────────────────────────────────────────────────────
154
+ // ─── Root Config Generators ─────────────────────────────────────────────────
139
155
 
140
156
  function generatePackageJson(slug, description, includeMcp) {
141
157
  const deps = {
142
- 'react': '^18.3.0',
143
- 'react-dom': '^18.3.0',
144
- 'react-router-dom': '^6.22.0',
158
+ 'next': '^15.0.0',
159
+ 'react': '^19.0.0',
160
+ 'react-dom': '^19.0.0',
161
+ 'iron-session': '^8.0.0',
145
162
  'ai': '^4.0.0',
146
163
  '@ai-sdk/anthropic': '^1.0.0',
147
- 'express': '^4.21.0',
148
- 'cors': '^2.8.5',
149
- 'dotenv': '^16.4.0',
150
164
  };
151
165
 
152
166
  if (includeMcp) {
@@ -157,33 +171,46 @@ function generatePackageJson(slug, description, includeMcp) {
157
171
  name: slug,
158
172
  version: '0.1.0',
159
173
  description: description || 'MyVillageOS application',
160
- type: 'module',
161
174
  private: true,
162
175
  scripts: {
163
- dev: 'concurrently "vite" "node --watch server/index.js"',
164
- 'dev:client': 'vite',
165
- 'dev:server': 'node --watch server/index.js',
166
- build: 'vite build',
167
- start: 'node server/index.js',
168
- preview: 'vite preview',
176
+ dev: 'next dev',
177
+ build: 'next build',
178
+ start: 'next start',
169
179
  },
170
180
  dependencies: deps,
171
- devDependencies: {
172
- 'vite': '^5.0.0',
173
- '@vitejs/plugin-react': '^4.2.0',
174
- 'concurrently': '^8.2.0',
175
- },
176
181
  };
177
182
 
178
183
  return JSON.stringify(pkg, null, 2) + '\n';
179
184
  }
180
185
 
186
+ function generateNextConfig() {
187
+ return `/** @type {import('next').NextConfig} */
188
+ const nextConfig = {
189
+ reactStrictMode: true,
190
+ };
191
+
192
+ export default nextConfig;
193
+ `;
194
+ }
195
+
196
+ function generateJsConfig() {
197
+ return `{
198
+ "compilerOptions": {
199
+ "paths": {
200
+ "@/*": ["./*"]
201
+ }
202
+ }
203
+ }
204
+ `;
205
+ }
206
+
181
207
  function generateGitignore() {
182
208
  return `node_modules/
183
- dist/
209
+ .next/
210
+ out/
184
211
  .DS_Store
185
212
  *.log
186
- .env
213
+ .env.local
187
214
  `;
188
215
  }
189
216
 
@@ -193,14 +220,11 @@ function generateEnv(oauthCredentials, hasOAuth, includeMcp) {
193
220
  if (hasOAuth) {
194
221
  const clientId = oauthCredentials?.clientId || '';
195
222
  const clientSecret = oauthCredentials?.clientSecret || '';
196
- // Server-side OAuth credentials
197
223
  lines.push(`MYVILLAGEOS_CLIENT_ID=${clientId}`);
198
224
  lines.push(`MYVILLAGEOS_CLIENT_SECRET=${clientSecret}`);
199
- // Browser-side OAuth credentials (Vite exposes VITE_ prefixed vars)
200
- lines.push(`VITE_OAUTH_CLIENT_ID=${clientId}`);
201
- lines.push(`VITE_OAUTH_CLIENT_SECRET=${clientSecret}`);
202
- lines.push(`VITE_OAUTH_BASE_URL=${OAUTH_BASE_URL}`);
203
- lines.push('VITE_OAUTH_REDIRECT_URI=http://localhost:5173/callback');
225
+ lines.push(`OAUTH_REDIRECT_URI=http://localhost:3000/api/auth/callback`);
226
+ // Generate a random session secret
227
+ lines.push(`SESSION_SECRET=${crypto.randomBytes(32).toString('hex')}`);
204
228
  }
205
229
 
206
230
  lines.push('MYVILLAGEOS_API_URL=https://portal.myvillageproject.ai');
@@ -210,7 +234,6 @@ function generateEnv(oauthCredentials, hasOAuth, includeMcp) {
210
234
  }
211
235
 
212
236
  lines.push('ANTHROPIC_API_KEY=');
213
- lines.push('PORT=3000');
214
237
 
215
238
  return lines.join('\n') + '\n';
216
239
  }
@@ -221,10 +244,8 @@ function generateEnvExample(hasOAuth, includeMcp) {
221
244
  if (hasOAuth) {
222
245
  lines.push('MYVILLAGEOS_CLIENT_ID=');
223
246
  lines.push('MYVILLAGEOS_CLIENT_SECRET=');
224
- lines.push(`VITE_OAUTH_CLIENT_ID=`);
225
- lines.push(`VITE_OAUTH_CLIENT_SECRET=`);
226
- lines.push(`VITE_OAUTH_BASE_URL=${OAUTH_BASE_URL}`);
227
- lines.push('VITE_OAUTH_REDIRECT_URI=http://localhost:5173/callback');
247
+ lines.push('OAUTH_REDIRECT_URI=http://localhost:3000/api/auth/callback');
248
+ lines.push('SESSION_SECRET=');
228
249
  }
229
250
 
230
251
  lines.push('MYVILLAGEOS_API_URL=https://portal.myvillageproject.ai');
@@ -234,7 +255,6 @@ function generateEnvExample(hasOAuth, includeMcp) {
234
255
  }
235
256
 
236
257
  lines.push('ANTHROPIC_API_KEY=');
237
- lines.push('PORT=3000');
238
258
 
239
259
  return lines.join('\n') + '\n';
240
260
  }
@@ -255,15 +275,15 @@ npm install
255
275
  2. Copy the environment file and fill in your credentials:
256
276
 
257
277
  \`\`\`bash
258
- cp .env.example .env
278
+ cp .env.example .env.local
259
279
  \`\`\`
260
280
 
261
- 3. Set your \`ANTHROPIC_API_KEY\` in \`.env\`.
281
+ 3. Set your \`ANTHROPIC_API_KEY\` in \`.env.local\`.
262
282
  `;
263
283
 
264
284
  if (hasOAuth) {
265
285
  readme += `
266
- 4. Register an OAuth client at [portal.myvillageproject.ai](https://portal.myvillageproject.ai) and add your \`MYVILLAGEOS_CLIENT_ID\` and \`MYVILLAGEOS_CLIENT_SECRET\` to \`.env\`.
286
+ 4. Your OAuth credentials (\`MYVILLAGEOS_CLIENT_ID\` and \`MYVILLAGEOS_CLIENT_SECRET\`) should already be in \`.env.local\` if you created this project with the CLI. Otherwise, register an OAuth client at [portal.myvillageproject.ai](https://portal.myvillageproject.ai).
267
287
  `;
268
288
  }
269
289
 
@@ -271,50 +291,34 @@ cp .env.example .env
271
291
  ## Running
272
292
 
273
293
  \`\`\`bash
274
- # Development (React frontend + Express backend concurrently)
275
- npm run dev
276
-
277
- # Or run separately:
278
- npm run dev:client # React frontend on http://localhost:5173
279
- npm run dev:server # Express backend on http://localhost:3000
280
-
281
- # Production
282
- npm run build # Build React frontend
283
- npm start # Start Express server (serves built frontend)
294
+ npm run dev # Development server on http://localhost:3000
295
+ npm run build # Production build
296
+ npm start # Start production server
284
297
  \`\`\`
285
298
 
286
299
  ## Architecture
287
300
 
288
- ### Frontend (React + Vite)
289
- - **src/App.jsx** - Root component with routing
290
- - **src/components/** - Layout, Sidebar, Header, AgentChat
291
- - **src/pages/** - Dashboard, Settings, etc.
292
- ${hasOAuth ? '- **src/auth/** - Browser-side OAuth PKCE flow\n' : ''}
293
- ### Backend (Express)
294
- - **server/index.js** - Express server with agent endpoint
295
- - **server/agent/index.js** - AI agent powered by Vercel AI SDK + Anthropic
296
- `;
297
-
298
- if (includeMcp) {
299
- readme += `- **server/mcp/client.js** - MCP client for MyVillageOS tools
300
- `;
301
- }
302
-
303
- if (hasOAuth) {
304
- readme += `- **server/auth/oauth.js** - Server-side OAuth 2.0 + PKCE helpers
305
- `;
306
- }
307
-
308
- if (includeRestApi) {
309
- readme += `- **server/api/routes.js** - REST API proxy routes for platform integration
310
- `;
311
- }
301
+ This is a **Next.js App Router** application with server-side OAuth and an AI agent backend.
302
+
303
+ ### App Routes
304
+ - **app/layout.jsx** Root layout with sidebar and header
305
+ - **app/page.jsx** Home page
306
+ ${hasOAuth ? '- **app/login/page.jsx** — Login page\n' : ''}${features.includes('dashboard') ? '- **app/dashboard/page.jsx** — Dashboard with AI agent chat\n' : ''}${features.includes('settings') ? '- **app/settings/page.jsx** — Settings\n' : ''}${features.includes('users') ? '- **app/users/page.jsx** — Users\n' : ''}${features.includes('notifications') ? '- **app/notifications/page.jsx** — Notifications\n' : ''}
307
+ ### API Routes
308
+ - **app/api/agent/chat/** AI agent endpoint (Vercel AI SDK + Anthropic)
309
+ ${hasOAuth ? '- **app/api/auth/** — OAuth login, callback, session, logout\n' : ''}${includeRestApi ? '- **app/api/proxy/** — Authenticated proxy to MyVillageOS platform API\n' : ''}
310
+ ### Server Libraries
311
+ - **lib/agent.js** — AI agent powered by Vercel AI SDK + Anthropic
312
+ ${includeMcp ? '- **lib/mcp-client.js** MCP client for MyVillageOS tools\n' : ''}${hasOAuth ? '- **lib/oauth.js** — Server-side OAuth 2.0 + PKCE helpers\n- **lib/session.js** — Encrypted session management (iron-session)\n' : ''}
313
+ ### Components
314
+ - **components/Sidebar.jsx** — Navigation sidebar
315
+ - **components/Header.jsx** — Top header with user info
316
+ - **components/AgentChat.jsx** — Interactive AI agent chat widget
312
317
 
313
- readme += `
314
318
  ## Brand Colors
315
319
 
316
320
  | Color | Hex |
317
- |------------|-----------|
321
+ |------------|-----------||
318
322
  | Gold | ${BRAND.gold} |
319
323
  | Brown | ${BRAND.brown} |
320
324
  | Green | ${BRAND.green} |
@@ -330,133 +334,350 @@ Built with [MyVillageOS](https://myvillageproject.ai)
330
334
  return readme;
331
335
  }
332
336
 
333
- function generateEntrypoint(name, hasOAuth, hasEmailPassword, includeMcp, includeRestApi) {
334
- let imports = `import 'dotenv/config';
335
- import express from 'express';
336
- import cors from 'cors';
337
- import { runAgent } from './agent/index.js';
338
- `;
337
+ // ─── Auth & Session Generators ──────────────────────────────────────────────
339
338
 
340
- if (includeRestApi) {
341
- imports += `import { createApiRouter } from './api/routes.js';
339
+ function generateSessionLib() {
340
+ return `import { getIronSession } from 'iron-session';
341
+ import { cookies } from 'next/headers';
342
+
343
+ const sessionOptions = {
344
+ password: process.env.SESSION_SECRET,
345
+ cookieName: 'myvillage_session',
346
+ cookieOptions: {
347
+ secure: process.env.NODE_ENV === 'production',
348
+ httpOnly: true,
349
+ sameSite: 'lax',
350
+ maxAge: 60 * 60 * 24 * 7, // 7 days
351
+ },
352
+ };
353
+
354
+ /**
355
+ * Get the session from cookies (for use in Server Components and API routes).
356
+ */
357
+ export async function getSession() {
358
+ const cookieStore = await cookies();
359
+ return getIronSession(cookieStore, sessionOptions);
360
+ }
361
+
362
+ /**
363
+ * Get session options (for use in middleware or route handlers that need raw options).
364
+ */
365
+ export function getSessionOptions() {
366
+ return sessionOptions;
367
+ }
342
368
  `;
369
+ }
370
+
371
+ function generateOAuthLib() {
372
+ return `import crypto from 'crypto';
373
+
374
+ const OAUTH_BASE_URL = process.env.MYVILLAGEOS_API_URL
375
+ ? \`\${process.env.MYVILLAGEOS_API_URL}/api/oauth\`
376
+ : '${OAUTH_BASE_URL}';
377
+
378
+ const CLIENT_ID = process.env.MYVILLAGEOS_CLIENT_ID;
379
+ const CLIENT_SECRET = process.env.MYVILLAGEOS_CLIENT_SECRET;
380
+ const REDIRECT_URI = process.env.OAUTH_REDIRECT_URI || 'http://localhost:3000/api/auth/callback';
381
+ const SCOPES = '${OAUTH_SCOPES}';
382
+
383
+ /**
384
+ * Generate a cryptographically random PKCE code verifier.
385
+ */
386
+ export function generateCodeVerifier() {
387
+ return crypto.randomBytes(64).toString('base64url');
388
+ }
389
+
390
+ /**
391
+ * Generate the S256 code challenge from a verifier.
392
+ */
393
+ export function generateCodeChallenge(verifier) {
394
+ return crypto.createHash('sha256').update(verifier).digest('base64url');
395
+ }
396
+
397
+ /**
398
+ * Build the authorization URL for initiating the OAuth login flow.
399
+ * Returns the URL and the code verifier (which must be stored for the callback).
400
+ */
401
+ export function buildAuthorizationUrl() {
402
+ const codeVerifier = generateCodeVerifier();
403
+ const codeChallenge = generateCodeChallenge(codeVerifier);
404
+ const state = crypto.randomBytes(16).toString('hex');
405
+
406
+ const params = new URLSearchParams({
407
+ response_type: 'code',
408
+ client_id: CLIENT_ID,
409
+ redirect_uri: REDIRECT_URI,
410
+ scope: SCOPES,
411
+ state,
412
+ code_challenge: codeChallenge,
413
+ code_challenge_method: 'S256',
414
+ });
415
+
416
+ const url = \`\${OAUTH_BASE_URL}/authorize?\${params.toString()}\`;
417
+ return { url, codeVerifier, state };
418
+ }
419
+
420
+ /**
421
+ * Exchange an authorization code for tokens (server-side).
422
+ */
423
+ export async function exchangeCodeForTokens(code, codeVerifier) {
424
+ const body = new URLSearchParams({
425
+ grant_type: 'authorization_code',
426
+ code,
427
+ redirect_uri: REDIRECT_URI,
428
+ client_id: CLIENT_ID,
429
+ client_secret: CLIENT_SECRET,
430
+ code_verifier: codeVerifier,
431
+ });
432
+
433
+ const response = await fetch(\`\${OAUTH_BASE_URL}/token\`, {
434
+ method: 'POST',
435
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
436
+ body: body.toString(),
437
+ });
438
+
439
+ if (!response.ok) {
440
+ const error = await response.text();
441
+ throw new Error(\`Token exchange failed: \${response.status} \${error}\`);
343
442
  }
344
443
 
345
- if (hasOAuth) {
346
- imports += `import { initiateOAuthLogin, handleOAuthCallback, getStoredTokens } from './auth/oauth.js';
347
- `;
444
+ return response.json();
445
+ }
446
+
447
+ /**
448
+ * Fetch the authenticated user's profile from the userinfo endpoint.
449
+ */
450
+ export async function fetchUserInfo(accessToken) {
451
+ const response = await fetch(\`\${OAUTH_BASE_URL}/userinfo\`, {
452
+ headers: { Authorization: \`Bearer \${accessToken}\` },
453
+ });
454
+
455
+ if (!response.ok) {
456
+ throw new Error(\`Userinfo request failed: \${response.status}\`);
348
457
  }
349
458
 
350
- let body = `
351
- const app = express();
352
- const PORT = process.env.PORT || 3000;
459
+ return response.json();
460
+ }
353
461
 
354
- app.use(express.json());
355
- app.use(cors());
462
+ /**
463
+ * Refresh an access token using a refresh token.
464
+ */
465
+ export async function refreshAccessToken(refreshToken) {
466
+ const body = new URLSearchParams({
467
+ grant_type: 'refresh_token',
468
+ refresh_token: refreshToken,
469
+ client_id: CLIENT_ID,
470
+ client_secret: CLIENT_SECRET,
471
+ });
356
472
 
357
- // Health check
358
- app.get('/health', (req, res) => {
359
- res.json({ status: 'ok', name: '${name}', timestamp: new Date().toISOString() });
360
- });
361
- `;
473
+ const response = await fetch(\`\${OAUTH_BASE_URL}/token\`, {
474
+ method: 'POST',
475
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
476
+ body: body.toString(),
477
+ });
362
478
 
363
- if (hasOAuth) {
364
- body += `
365
- // OAuth login flow
366
- app.get('/auth/login', (req, res) => {
367
- const { url, codeVerifier } = initiateOAuthLogin();
368
- // Store codeVerifier in session or a temporary store for the callback
369
- app.locals.pendingAuth = { codeVerifier };
370
- res.redirect(url);
371
- });
372
-
373
- app.get('/auth/callback', async (req, res) => {
374
- try {
375
- const { code } = req.query;
376
- const { codeVerifier } = app.locals.pendingAuth || {};
377
- if (!code || !codeVerifier) {
378
- return res.status(400).json({ error: 'Missing code or code verifier' });
379
- }
380
- const tokens = await handleOAuthCallback(code, codeVerifier);
381
- app.locals.tokens = tokens;
382
- delete app.locals.pendingAuth;
383
- res.json({ message: 'Authenticated successfully' });
384
- } catch (err) {
385
- console.error('[Auth] OAuth callback error:', err.message);
386
- res.status(500).json({ error: 'Authentication failed' });
479
+ if (!response.ok) {
480
+ const error = await response.text();
481
+ throw new Error(\`Token refresh failed: \${response.status} \${error}\`);
387
482
  }
388
- });
483
+
484
+ return response.json();
485
+ }
389
486
  `;
487
+ }
488
+
489
+ function generateMiddleware() {
490
+ return `import { NextResponse } from 'next/server';
491
+
492
+ // Routes that don't require authentication
493
+ const publicPaths = ['/login', '/api/auth/login', '/api/auth/callback', '/_next', '/favicon.ico'];
494
+
495
+ export function middleware(request) {
496
+ const { pathname } = request.nextUrl;
497
+
498
+ // Allow public paths
499
+ if (publicPaths.some(path => pathname.startsWith(path))) {
500
+ return NextResponse.next();
390
501
  }
391
502
 
392
- if (includeRestApi) {
393
- if (hasOAuth) {
394
- body += `
395
- // Mount API routes with token accessor
396
- app.use('/api', createApiRouter(() => app.locals.tokens));
397
- `;
398
- } else {
399
- body += `
400
- // Mount API routes
401
- app.use('/api', createApiRouter());
402
- `;
403
- }
503
+ // Check for session cookie (actual validation happens in route handlers)
504
+ const sessionCookie = request.cookies.get('myvillage_session');
505
+ if (!sessionCookie) {
506
+ return NextResponse.redirect(new URL('/login', request.url));
404
507
  }
405
508
 
406
- body += `
407
- // Agent endpoint - send a message to the AI agent
408
- app.post('/agent/chat', async (req, res) => {
409
- try {
410
- const { message } = req.body;
411
- if (!message) {
412
- return res.status(400).json({ error: 'Message is required' });
413
- }
414
- `;
509
+ return NextResponse.next();
510
+ }
415
511
 
416
- if (hasOAuth) {
417
- body += ` const tokens = app.locals.tokens || null;
418
- const result = await runAgent(message, { tokens });
512
+ export const config = {
513
+ matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
514
+ };
419
515
  `;
420
- } else {
421
- body += ` const result = await runAgent(message);
516
+ }
517
+
518
+ // ─── Auth Route Generators ──────────────────────────────────────────────────
519
+
520
+ function generateAuthLoginRoute() {
521
+ return `import { NextResponse } from 'next/server';
522
+ import { cookies } from 'next/headers';
523
+ import { buildAuthorizationUrl } from '@/lib/oauth';
524
+
525
+ /**
526
+ * GET /api/auth/login
527
+ * Initiates the OAuth login flow. Generates PKCE verifier, stores it in a
528
+ * temporary cookie, and redirects to the MyVillageOS authorization page.
529
+ */
530
+ export async function GET() {
531
+ const { url, codeVerifier, state } = buildAuthorizationUrl();
532
+
533
+ const cookieStore = await cookies();
534
+
535
+ // Store verifier and state in short-lived httpOnly cookies for the callback
536
+ cookieStore.set('oauth_verifier', codeVerifier, {
537
+ httpOnly: true,
538
+ secure: process.env.NODE_ENV === 'production',
539
+ sameSite: 'lax',
540
+ maxAge: 600, // 10 minutes
541
+ path: '/',
542
+ });
543
+
544
+ cookieStore.set('oauth_state', state, {
545
+ httpOnly: true,
546
+ secure: process.env.NODE_ENV === 'production',
547
+ sameSite: 'lax',
548
+ maxAge: 600,
549
+ path: '/',
550
+ });
551
+
552
+ return NextResponse.redirect(url);
553
+ }
422
554
  `;
555
+ }
556
+
557
+ function generateAuthCallbackRoute() {
558
+ return `import { NextResponse } from 'next/server';
559
+ import { cookies } from 'next/headers';
560
+ import { exchangeCodeForTokens, fetchUserInfo } from '@/lib/oauth';
561
+ import { getSession } from '@/lib/session';
562
+
563
+ /**
564
+ * GET /api/auth/callback
565
+ * Handles the OAuth callback. Exchanges the authorization code for tokens
566
+ * server-side (no CORS issues), fetches user info, and stores everything
567
+ * in an encrypted session cookie.
568
+ */
569
+ export async function GET(request) {
570
+ const { searchParams } = new URL(request.url);
571
+ const code = searchParams.get('code');
572
+ const state = searchParams.get('state');
573
+ const error = searchParams.get('error');
574
+
575
+ if (error) {
576
+ const errorDesc = searchParams.get('error_description') || 'Authorization failed';
577
+ console.error('[Auth Callback] OAuth error:', error, errorDesc);
578
+ return NextResponse.redirect(new URL(\`/login?error=\${encodeURIComponent(errorDesc)}\`, request.url));
423
579
  }
424
580
 
425
- body += ` res.json({ response: result });
426
- } catch (err) {
427
- console.error('[Agent] Error:', err.message);
428
- res.status(500).json({ error: 'Agent processing failed' });
581
+ if (!code) {
582
+ return NextResponse.redirect(new URL('/login?error=Missing+authorization+code', request.url));
429
583
  }
430
- });
431
584
 
432
- app.listen(PORT, () => {
433
- console.log(\`[Server] ${name} running on http://localhost:\${PORT}\`);
434
- console.log('[Server] POST /agent/chat - Send messages to the AI agent');
435
- `;
585
+ const cookieStore = await cookies();
586
+ const savedVerifier = cookieStore.get('oauth_verifier')?.value;
587
+ const savedState = cookieStore.get('oauth_state')?.value;
436
588
 
437
- if (hasOAuth) {
438
- body += ` console.log('[Server] GET /auth/login - Start OAuth login flow');
439
- `;
589
+ if (!savedVerifier) {
590
+ return NextResponse.redirect(new URL('/login?error=Missing+code+verifier', request.url));
440
591
  }
441
592
 
442
- if (includeRestApi) {
443
- body += ` console.log('[Server] /api/* - Platform API proxy routes');
593
+ if (state && savedState && state !== savedState) {
594
+ return NextResponse.redirect(new URL('/login?error=State+mismatch', request.url));
595
+ }
596
+
597
+ try {
598
+ // Exchange code for tokens (server-to-server, no CORS)
599
+ const tokens = await exchangeCodeForTokens(code, savedVerifier);
600
+
601
+ // Fetch user profile
602
+ const user = await fetchUserInfo(tokens.access_token);
603
+
604
+ // Store in encrypted session
605
+ const session = await getSession();
606
+ session.accessToken = tokens.access_token;
607
+ session.refreshToken = tokens.refresh_token;
608
+ session.expiresAt = Date.now() + (tokens.expires_in * 1000);
609
+ session.user = {
610
+ id: user.sub || user.id,
611
+ name: user.name,
612
+ email: user.email,
613
+ villagerId: user.villager_id,
614
+ };
615
+ await session.save();
616
+
617
+ // Clean up temporary cookies
618
+ cookieStore.delete('oauth_verifier');
619
+ cookieStore.delete('oauth_state');
620
+
621
+ console.log('[Auth Callback] Login successful for', user.name || user.email);
622
+
623
+ return NextResponse.redirect(new URL('/', request.url));
624
+ } catch (err) {
625
+ console.error('[Auth Callback] Error:', err.message);
626
+ return NextResponse.redirect(new URL(\`/login?error=\${encodeURIComponent(err.message)}\`, request.url));
627
+ }
628
+ }
444
629
  `;
630
+ }
631
+
632
+ function generateAuthSessionRoute() {
633
+ return `import { NextResponse } from 'next/server';
634
+ import { getSession } from '@/lib/session';
635
+
636
+ /**
637
+ * GET /api/auth/session
638
+ * Returns the current session user info (for client components).
639
+ */
640
+ export async function GET() {
641
+ const session = await getSession();
642
+
643
+ if (!session.user) {
644
+ return NextResponse.json({ authenticated: false }, { status: 401 });
445
645
  }
446
646
 
447
- body += `});
647
+ return NextResponse.json({
648
+ authenticated: true,
649
+ user: session.user,
650
+ });
651
+ }
448
652
  `;
653
+ }
654
+
655
+ function generateAuthLogoutRoute() {
656
+ return `import { NextResponse } from 'next/server';
657
+ import { getSession } from '@/lib/session';
449
658
 
450
- return imports + body;
659
+ /**
660
+ * POST /api/auth/logout
661
+ * Destroys the session and redirects to the login page.
662
+ */
663
+ export async function POST() {
664
+ const session = await getSession();
665
+ session.destroy();
666
+
667
+ return NextResponse.redirect(new URL('/login', process.env.NEXTAUTH_URL || 'http://localhost:3000'));
668
+ }
669
+ `;
451
670
  }
452
671
 
672
+ // ─── Agent & MCP Generators ─────────────────────────────────────────────────
673
+
453
674
  function generateAgent(includeMcp) {
454
675
  let code = `import { generateText } from 'ai';
455
676
  import { anthropic } from '@ai-sdk/anthropic';
456
677
  `;
457
678
 
458
679
  if (includeMcp) {
459
- code += `import { getMcpTools } from '../mcp/client.js';
680
+ code += `import { getMcpTools } from './mcp-client.js';
460
681
  `;
461
682
  }
462
683
 
@@ -515,20 +736,11 @@ to fulfill the user's request rather than just describing what could be done.\`;
515
736
  }
516
737
 
517
738
  function generateMcpClient(selectedGroups) {
518
- // Build the list of allowed tool names from selected groups
519
- const allowedTools = [];
520
- for (const group of selectedGroups) {
521
- if (TOOL_GROUPS[group]) {
522
- allowedTools.push(...TOOL_GROUPS[group]);
523
- }
524
- }
525
-
526
739
  const code = `import { Client } from '@modelcontextprotocol/sdk/client/index.js';
527
740
  import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
528
741
  import { jsonSchema } from 'ai';
529
742
 
530
743
  // ─── MCP Tool Group Definitions ─────────────────────────────────────────────
531
- // Each group maps to a set of MyVillageOS MCP tool names.
532
744
 
533
745
  const TOOL_GROUPS = {
534
746
  social: ['post_create', 'post_view', 'post_list', 'post_edit', 'post_delete', 'comment_create', 'comment_list', 'vote_cast'],
@@ -551,7 +763,6 @@ let mcpClient = null;
551
763
 
552
764
  /**
553
765
  * Initialize the MCP client by spawning the MyVillageOS MCP server as a child process.
554
- * Uses stdio transport for local communication.
555
766
  */
556
767
  async function getClient() {
557
768
  if (mcpClient) return mcpClient;
@@ -576,8 +787,6 @@ async function getClient() {
576
787
  /**
577
788
  * Convert MCP tools to Vercel AI SDK tool format.
578
789
  * Filters tools based on the selected tool groups.
579
- *
580
- * @returns {object} Tools object compatible with Vercel AI SDK generateText/streamText
581
790
  */
582
791
  export async function getMcpTools() {
583
792
  const client = await getClient();
@@ -586,7 +795,6 @@ export async function getMcpTools() {
586
795
  const aiTools = {};
587
796
 
588
797
  for (const tool of mcpTools) {
589
- // Filter to only the tools in selected groups (if groups were specified)
590
798
  if (ALLOWED_TOOLS.size > 0 && !ALLOWED_TOOLS.has(tool.name)) {
591
799
  continue;
592
800
  }
@@ -631,890 +839,339 @@ export async function disconnectMcp() {
631
839
  return code;
632
840
  }
633
841
 
634
- function generateOAuthModule() {
635
- return `import crypto from 'crypto';
636
-
637
- // ─── OAuth 2.0 + PKCE Helpers ───────────────────────────────────────────────
638
- // Server-side OAuth flow for authenticating with MyVillageOS.
639
-
640
- const OAUTH_BASE_URL = process.env.MYVILLAGEOS_API_URL
641
- ? \`\${process.env.MYVILLAGEOS_API_URL}/api/oauth\`
642
- : '${OAUTH_BASE_URL}';
643
-
644
- const CLIENT_ID = process.env.MYVILLAGEOS_CLIENT_ID;
645
- const CLIENT_SECRET = process.env.MYVILLAGEOS_CLIENT_SECRET;
646
- const REDIRECT_URI = 'http://localhost:' + (process.env.PORT || '3000') + '/auth/callback';
647
- const SCOPES = '${OAUTH_SCOPES}';
842
+ function generateAgentChatRoute(hasOAuth) {
843
+ let code = `import { NextResponse } from 'next/server';
844
+ import { runAgent } from '@/lib/agent';
845
+ `;
648
846
 
649
- // In-memory token store (replace with a database in production)
650
- let storedTokens = null;
847
+ if (hasOAuth) {
848
+ code += `import { getSession } from '@/lib/session';
849
+ `;
850
+ }
651
851
 
852
+ code += `
652
853
  /**
653
- * Generate a cryptographically random code verifier for PKCE.
854
+ * POST /api/agent/chat
855
+ * Send a message to the AI agent and get a response.
654
856
  */
655
- function generateCodeVerifier() {
656
- return crypto.randomBytes(64).toString('base64url');
657
- }
857
+ export async function POST(request) {
858
+ try {
859
+ const { message } = await request.json();
860
+ if (!message) {
861
+ return NextResponse.json({ error: 'Message is required' }, { status: 400 });
862
+ }
658
863
 
659
- /**
660
- * Generate the code challenge from a code verifier (S256 method).
661
- */
662
- function generateCodeChallenge(verifier) {
663
- return crypto.createHash('sha256').update(verifier).digest('base64url');
664
- }
864
+ `;
665
865
 
666
- /**
667
- * Initiate the OAuth login flow. Returns the authorization URL and code verifier.
668
- * The caller should store the codeVerifier and redirect the user to the URL.
669
- */
670
- export function initiateOAuthLogin() {
671
- const codeVerifier = generateCodeVerifier();
672
- const codeChallenge = generateCodeChallenge(codeVerifier);
673
- const state = crypto.randomBytes(16).toString('hex');
674
-
675
- const params = new URLSearchParams({
676
- response_type: 'code',
677
- client_id: CLIENT_ID,
678
- redirect_uri: REDIRECT_URI,
679
- scope: SCOPES,
680
- state,
681
- code_challenge: codeChallenge,
682
- code_challenge_method: 'S256',
683
- });
684
-
685
- const url = \`\${OAUTH_BASE_URL}/authorize?\${params.toString()}\`;
686
- console.log('[OAuth] Login URL generated');
687
-
688
- return { url, codeVerifier, state };
689
- }
690
-
691
- /**
692
- * Exchange an authorization code for tokens.
693
- */
694
- export async function handleOAuthCallback(code, codeVerifier) {
695
- const body = new URLSearchParams({
696
- grant_type: 'authorization_code',
697
- code,
698
- redirect_uri: REDIRECT_URI,
699
- client_id: CLIENT_ID,
700
- client_secret: CLIENT_SECRET,
701
- code_verifier: codeVerifier,
702
- });
703
-
704
- const response = await fetch(\`\${OAUTH_BASE_URL}/token\`, {
705
- method: 'POST',
706
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
707
- body: body.toString(),
708
- });
709
-
710
- if (!response.ok) {
711
- const error = await response.text();
712
- throw new Error(\`Token exchange failed: \${response.status} \${error}\`);
713
- }
714
-
715
- const tokens = await response.json();
716
- storedTokens = {
717
- accessToken: tokens.access_token,
718
- refreshToken: tokens.refresh_token,
719
- expiresAt: Date.now() + (tokens.expires_in * 1000),
720
- idToken: tokens.id_token,
721
- };
722
-
723
- console.log('[OAuth] Tokens obtained successfully');
724
- return storedTokens;
725
- }
726
-
727
- /**
728
- * Refresh the access token using the stored refresh token.
729
- */
730
- export async function refreshAccessToken() {
731
- if (!storedTokens?.refreshToken) {
732
- throw new Error('No refresh token available');
733
- }
734
-
735
- const body = new URLSearchParams({
736
- grant_type: 'refresh_token',
737
- refresh_token: storedTokens.refreshToken,
738
- client_id: CLIENT_ID,
739
- client_secret: CLIENT_SECRET,
740
- });
741
-
742
- const response = await fetch(\`\${OAUTH_BASE_URL}/token\`, {
743
- method: 'POST',
744
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
745
- body: body.toString(),
746
- });
747
-
748
- if (!response.ok) {
749
- const error = await response.text();
750
- throw new Error(\`Token refresh failed: \${response.status} \${error}\`);
751
- }
752
-
753
- const tokens = await response.json();
754
- storedTokens = {
755
- accessToken: tokens.access_token,
756
- refreshToken: tokens.refresh_token || storedTokens.refreshToken,
757
- expiresAt: Date.now() + (tokens.expires_in * 1000),
758
- idToken: tokens.id_token,
759
- };
760
-
761
- console.log('[OAuth] Tokens refreshed successfully');
762
- return storedTokens;
763
- }
764
-
765
- /**
766
- * Get a valid access token, refreshing if needed.
767
- */
768
- export async function getValidAccessToken() {
769
- if (!storedTokens) {
770
- throw new Error('Not authenticated. Visit /auth/login to authenticate.');
866
+ if (hasOAuth) {
867
+ code += ` const session = await getSession();
868
+ const tokens = session.accessToken ? { accessToken: session.accessToken } : null;
869
+ const result = await runAgent(message, { tokens });
870
+ `;
871
+ } else {
872
+ code += ` const result = await runAgent(message);
873
+ `;
771
874
  }
772
875
 
773
- // Refresh if token expires within 60 seconds
774
- if (storedTokens.expiresAt - Date.now() < 60_000) {
775
- await refreshAccessToken();
876
+ code += ` return NextResponse.json({ response: result });
877
+ } catch (err) {
878
+ console.error('[Agent] Error:', err.message);
879
+ return NextResponse.json({ error: 'Agent processing failed' }, { status: 500 });
776
880
  }
777
-
778
- return storedTokens.accessToken;
779
- }
780
-
781
- /**
782
- * Get the currently stored tokens (may be null if not authenticated).
783
- */
784
- export function getStoredTokens() {
785
- return storedTokens;
786
881
  }
787
882
  `;
788
- }
789
883
 
790
- function generateApiRoutes(hasOAuth) {
791
- let code = `import { Router } from 'express';
792
-
793
- // ─── Platform API Routes ────────────────────────────────────────────────────
794
- // Express router with helpers for making authenticated API calls to MyVillageOS.
884
+ return code;
885
+ }
795
886
 
796
- const API_BASE_URL = process.env.MYVILLAGEOS_API_URL || 'https://portal.myvillageproject.ai';
887
+ // ─── API Proxy Generator ────────────────────────────────────────────────────
797
888
 
889
+ function generateProxyCatchAll(hasOAuth) {
890
+ let code = `import { NextResponse } from 'next/server';
798
891
  `;
799
892
 
800
893
  if (hasOAuth) {
801
- code += `/**
802
- * Create the API router.
803
- * @param {Function} getTokens - Function that returns the current OAuth tokens
804
- */
805
- export function createApiRouter(getTokens) {
806
- const router = Router();
807
-
808
- /**
809
- * Make an authenticated request to the MyVillageOS platform API.
810
- */
811
- async function apiRequest(path, options = {}) {
812
- const tokens = getTokens?.();
813
- const headers = {
814
- 'Content-Type': 'application/json',
815
- ...options.headers,
816
- };
817
-
818
- if (tokens?.accessToken) {
819
- headers['Authorization'] = \`Bearer \${tokens.accessToken}\`;
820
- }
821
-
822
- const url = \`\${API_BASE_URL}\${path}\`;
823
- console.log(\`[API] \${options.method || 'GET'} \${url}\`);
824
-
825
- const response = await fetch(url, {
826
- ...options,
827
- headers,
828
- });
894
+ code += `import { getSession } from '@/lib/session';
895
+ `;
896
+ }
829
897
 
830
- if (!response.ok) {
831
- const error = await response.text();
832
- throw new Error(\`API request failed: \${response.status} \${error}\`);
833
- }
898
+ code += `
899
+ const API_BASE_URL = process.env.MYVILLAGEOS_API_URL || 'https://portal.myvillageproject.ai';
834
900
 
835
- return response.json();
836
- }
837
- `;
838
- } else {
839
- code += `/**
840
- * Create the API router.
901
+ /**
902
+ * Catch-all proxy to the MyVillageOS platform API.
903
+ * Forwards requests with authentication headers.
904
+ *
905
+ * Usage: /api/proxy/network/posts → GET https://portal.myvillageproject.ai/api/network/posts
841
906
  */
842
- export function createApiRouter() {
843
- const router = Router();
844
-
845
- /**
846
- * Make a request to the MyVillageOS platform API.
847
- */
848
- async function apiRequest(path, options = {}) {
849
- const headers = {
850
- 'Content-Type': 'application/json',
851
- ...options.headers,
852
- };
907
+ async function proxyRequest(request, { params }) {
908
+ const { path } = await params;
909
+ const apiPath = '/api/' + path.join('/');
910
+ const url = new URL(apiPath, API_BASE_URL);
853
911
 
854
- const url = \`\${API_BASE_URL}\${path}\`;
855
- console.log(\`[API] \${options.method || 'GET'} \${url}\`);
912
+ // Forward query parameters
913
+ const { searchParams } = new URL(request.url);
914
+ searchParams.forEach((value, key) => url.searchParams.set(key, value));
856
915
 
857
- const response = await fetch(url, {
858
- ...options,
859
- headers,
860
- });
916
+ const headers = {
917
+ 'Content-Type': 'application/json',
918
+ };
861
919
 
862
- if (!response.ok) {
863
- const error = await response.text();
864
- throw new Error(\`API request failed: \${response.status} \${error}\`);
865
- }
920
+ `;
866
921
 
867
- return response.json();
922
+ if (hasOAuth) {
923
+ code += ` const session = await getSession();
924
+ if (session.accessToken) {
925
+ headers['Authorization'] = \`Bearer \${session.accessToken}\`;
868
926
  }
869
927
  `;
870
928
  }
871
929
 
872
930
  code += `
873
- // ─── Network / Social ──────────────────────────────────────────────────────
874
-
875
- // List posts from the MAN network
876
- router.get('/network/posts', async (req, res) => {
877
- try {
878
- const data = await apiRequest('/api/network/posts');
879
- res.json(data);
880
- } catch (err) {
881
- console.error('[API] Error fetching posts:', err.message);
882
- res.status(500).json({ error: err.message });
883
- }
884
- });
885
-
886
- // Create a new post
887
- router.post('/network/posts', async (req, res) => {
888
- try {
889
- const data = await apiRequest('/api/network/posts', {
890
- method: 'POST',
891
- body: JSON.stringify(req.body),
892
- });
893
- res.json(data);
894
- } catch (err) {
895
- console.error('[API] Error creating post:', err.message);
896
- res.status(500).json({ error: err.message });
897
- }
898
- });
899
-
900
- // ─── Communities ────────────────────────────────────────────────────────────
901
-
902
- // List communities
903
- router.get('/communities', async (req, res) => {
904
- try {
905
- const data = await apiRequest('/api/network/communities');
906
- res.json(data);
907
- } catch (err) {
908
- console.error('[API] Error fetching communities:', err.message);
909
- res.status(500).json({ error: err.message });
910
- }
911
- });
912
-
913
- // ─── Villages ───────────────────────────────────────────────────────────────
931
+ const fetchOptions = {
932
+ method: request.method,
933
+ headers,
934
+ };
914
935
 
915
- // List villages
916
- router.get('/villages', async (req, res) => {
936
+ if (request.method !== 'GET' && request.method !== 'HEAD') {
917
937
  try {
918
- const data = await apiRequest('/api/network/villages');
919
- res.json(data);
920
- } catch (err) {
921
- console.error('[API] Error fetching villages:', err.message);
922
- res.status(500).json({ error: err.message });
938
+ fetchOptions.body = await request.text();
939
+ } catch {
940
+ // No body
923
941
  }
924
- });
942
+ }
925
943
 
926
- // ─── Profile ────────────────────────────────────────────────────────────────
944
+ console.log(\`[Proxy] \${request.method} \${url}\`);
927
945
 
928
- // Get current user profile
929
- router.get('/profile', async (req, res) => {
930
- try {
931
- const data = await apiRequest('/api/oauth/userinfo');
932
- res.json(data);
933
- } catch (err) {
934
- console.error('[API] Error fetching profile:', err.message);
935
- res.status(500).json({ error: err.message });
936
- }
937
- });
946
+ try {
947
+ const response = await fetch(url.toString(), fetchOptions);
948
+ const data = await response.text();
938
949
 
939
- return router;
950
+ return new NextResponse(data, {
951
+ status: response.status,
952
+ headers: { 'Content-Type': response.headers.get('Content-Type') || 'application/json' },
953
+ });
954
+ } catch (err) {
955
+ console.error('[Proxy] Error:', err.message);
956
+ return NextResponse.json({ error: err.message }, { status: 502 });
957
+ }
940
958
  }
959
+
960
+ export const GET = proxyRequest;
961
+ export const POST = proxyRequest;
962
+ export const PUT = proxyRequest;
963
+ export const DELETE = proxyRequest;
964
+ export const PATCH = proxyRequest;
941
965
  `;
942
966
 
943
967
  return code;
944
968
  }
945
969
 
946
- // ─── Vite & HTML Generators ─────────────────────────────────────────────────
970
+ // ─── App Layout & Page Generators ───────────────────────────────────────────
947
971
 
948
- function generateViteConfig() {
949
- return `import { defineConfig } from 'vite';
950
- import react from '@vitejs/plugin-react';
951
-
952
- export default defineConfig({
953
- plugins: [react()],
954
- server: {
955
- port: 5173,
956
- open: true,
957
- proxy: {
958
- '/api': 'http://localhost:3000',
959
- '/agent': 'http://localhost:3000',
960
- '/auth': 'http://localhost:3000',
961
- '/health': 'http://localhost:3000',
962
- },
963
- },
964
- build: { outDir: 'dist' },
965
- });
966
- `;
967
- }
972
+ function generateRootLayout(name, hasOAuth, features) {
973
+ let code = `import './globals.css';
974
+ import Sidebar from '@/components/Sidebar';
975
+ import Header from '@/components/Header';
968
976
 
969
- function generateIndexHtml(name) {
970
- return `<!DOCTYPE html>
971
- <html lang="en">
972
- <head>
973
- <meta charset="UTF-8" />
974
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
975
- <title>${name} - MyVillageOS</title>
976
- </head>
977
- <body>
978
- <div id="root"></div>
979
- <script type="module" src="/src/main.jsx"></script>
980
- </body>
981
- </html>
982
- `;
983
- }
977
+ export const metadata = {
978
+ title: '${name} - MyVillageOS',
979
+ description: 'A MyVillageOS agentic application',
980
+ };
984
981
 
985
- // ─── Frontend Generators ────────────────────────────────────────────────────
986
- // NOTE: These functions return string literals that will be written to files.
987
- // Template expressions like ${} are JS interpolation, not output.
988
- // Backticks, $, and { that appear in output JSX are safe in regular strings.
989
-
990
- function generateMainJsx() {
991
- return `import React from 'react';
992
- import ReactDOM from 'react-dom/client';
993
- import { BrowserRouter } from 'react-router-dom';
994
- import App from './App.jsx';
995
- import './index.css';
996
-
997
- ReactDOM.createRoot(document.getElementById('root')).render(
998
- <React.StrictMode>
999
- <BrowserRouter>
1000
- <App />
1001
- </BrowserRouter>
1002
- </React.StrictMode>
1003
- );
1004
- `;
982
+ export default function RootLayout({ children }) {
983
+ return (
984
+ <html lang="en">
985
+ <body>
986
+ {children}
987
+ </body>
988
+ </html>
989
+ );
1005
990
  }
1006
991
 
1007
- function generateAppJsx(features, hasOAuth) {
1008
- const hasDashboard = features.includes('dashboard');
1009
- const hasSettings = features.includes('settings');
1010
- const hasUsers = features.includes('users');
1011
- const hasNotifications = features.includes('notifications');
1012
-
1013
- const imports = ["import { Routes, Route } from 'react-router-dom';"];
1014
- imports.push("import Layout from './components/Layout.jsx';");
1015
- imports.push("import NotFound from './pages/NotFound.jsx';");
1016
- imports.push("import './App.css';");
1017
-
1018
- if (hasOAuth) {
1019
- imports.push("import { AuthProvider } from './auth/AuthProvider.jsx';");
1020
- imports.push("import ProtectedRoute from './components/ProtectedRoute.jsx';");
1021
- imports.push("import Login from './pages/Login.jsx';");
1022
- imports.push("import Callback from './pages/Callback.jsx';");
1023
- }
1024
- if (hasDashboard) imports.push("import Dashboard from './pages/Dashboard.jsx';");
1025
- if (hasSettings) imports.push("import Settings from './pages/Settings.jsx';");
1026
- if (hasUsers) imports.push("import Users from './pages/Users.jsx';");
1027
- if (hasNotifications) imports.push("import Notifications from './pages/Notifications.jsx';");
1028
-
1029
- const routes = [];
1030
- const homeElement = hasDashboard ? '<Dashboard />' : '<div className="welcome"><h1>Welcome</h1><p>Select a page from the sidebar to get started.</p></div>';
1031
-
1032
- if (hasOAuth) {
1033
- routes.push(' <Route path="/" element={<ProtectedRoute><Layout /></ProtectedRoute>}>');
1034
- } else {
1035
- routes.push(' <Route path="/" element={<Layout />}>');
1036
- }
1037
-
1038
- routes.push(' <Route index element={' + homeElement + '} />');
1039
- if (hasDashboard) routes.push(' <Route path="dashboard" element={<Dashboard />} />');
1040
- if (hasSettings) routes.push(' <Route path="settings" element={<Settings />} />');
1041
- if (hasUsers) routes.push(' <Route path="users" element={<Users />} />');
1042
- if (hasNotifications) routes.push(' <Route path="notifications" element={<Notifications />} />');
1043
- routes.push(' <Route path="*" element={<NotFound />} />');
1044
- routes.push(' </Route>');
1045
-
1046
- if (hasOAuth) {
1047
- routes.push(' <Route path="/login" element={<Login />} />');
1048
- routes.push(' <Route path="/callback" element={<Callback />} />');
1049
- }
1050
-
1051
- let body;
1052
- if (hasOAuth) {
1053
- body = ' <AuthProvider>\n <Routes>\n' + routes.join('\n') + '\n </Routes>\n </AuthProvider>';
1054
- } else {
1055
- body = ' <Routes>\n' + routes.join('\n') + '\n </Routes>';
1056
- }
1057
-
1058
- return imports.join('\n') + '\n\nexport default function App() {\n return (\n' + body + '\n );\n}\n';
1059
- }
1060
-
1061
- function generateAppCss() {
1062
- return ':root {\n' +
1063
- ' --brand-gold: ' + BRAND.gold + ';\n' +
1064
- ' --brand-brown: ' + BRAND.brown + ';\n' +
1065
- ' --brand-green: ' + BRAND.green + ';\n' +
1066
- ' --brand-primary: ' + BRAND.primary + ';\n' +
1067
- ' --brand-secondary: ' + BRAND.secondary + ';\n' +
1068
- ' --brand-dark-brown: ' + BRAND.darkBrown + ';\n' +
1069
- ' --brand-deep-green: ' + BRAND.deepGreen + ';\n' +
1070
- ' --brand-teal: ' + BRAND.teal + ';\n' +
1071
- ' --sidebar-width: 240px;\n' +
1072
- ' --header-height: 56px;\n' +
1073
- '}\n' +
1074
- '\n' +
1075
- '.layout { display: flex; height: 100vh; }\n' +
1076
- '.layout-main { flex: 1; display: flex; flex-direction: column; overflow: hidden; }\n' +
1077
- '.layout-content { flex: 1; overflow-y: auto; padding: 24px; background: #f5f3ef; }\n' +
1078
- '\n' +
1079
- '.sidebar {\n' +
1080
- ' width: var(--sidebar-width);\n' +
1081
- ' background: var(--brand-dark-brown);\n' +
1082
- ' color: var(--brand-secondary);\n' +
1083
- ' display: flex;\n' +
1084
- ' flex-direction: column;\n' +
1085
- '}\n' +
1086
- '.sidebar-brand { padding: 16px 20px; border-bottom: 1px solid rgba(255,255,255,0.1); }\n' +
1087
- '.sidebar-brand h2 { color: var(--brand-gold); font-size: 18px; margin: 0; }\n' +
1088
- '.sidebar-nav { flex: 1; padding: 12px 0; list-style: none; }\n' +
1089
- '.sidebar-nav a {\n' +
1090
- ' display: block; padding: 10px 20px; color: var(--brand-secondary);\n' +
1091
- ' text-decoration: none; font-size: 14px; transition: background 0.2s;\n' +
1092
- '}\n' +
1093
- '.sidebar-nav a:hover, .sidebar-nav a.active {\n' +
1094
- ' background: rgba(255,215,0,0.1); color: var(--brand-gold);\n' +
1095
- '}\n' +
1096
- '\n' +
1097
- '.header {\n' +
1098
- ' height: var(--header-height); background: white; border-bottom: 1px solid #e0ddd7;\n' +
1099
- ' display: flex; align-items: center; justify-content: space-between; padding: 0 24px;\n' +
1100
- '}\n' +
1101
- '.header-title { font-size: 16px; font-weight: 600; color: var(--brand-dark-brown); }\n' +
1102
- '.header-user { display: flex; align-items: center; gap: 12px; }\n' +
1103
- '.header-user button {\n' +
1104
- ' padding: 6px 14px; background: var(--brand-primary); color: white;\n' +
1105
- ' border: none; border-radius: 6px; font-size: 13px; cursor: pointer;\n' +
1106
- '}\n' +
1107
- '\n' +
1108
- '.login-page {\n' +
1109
- ' display: flex; justify-content: center; align-items: center; height: 100vh;\n' +
1110
- ' background: var(--brand-secondary);\n' +
1111
- '}\n' +
1112
- '.login-card {\n' +
1113
- ' text-align: center; padding: 48px; background: white;\n' +
1114
- ' border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.1);\n' +
1115
- '}\n' +
1116
- '.login-btn {\n' +
1117
- ' margin-top: 20px; padding: 12px 32px; background: var(--brand-primary);\n' +
1118
- ' color: white; border: none; border-radius: 8px; font-size: 16px; cursor: pointer;\n' +
1119
- '}\n' +
1120
- '.login-btn:hover { background: var(--brand-gold); color: var(--brand-dark-brown); }\n' +
1121
- '\n' +
1122
- '.stat-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 16px; margin-bottom: 24px; }\n' +
1123
- '.stat-card {\n' +
1124
- ' background: white; border-radius: 8px; padding: 20px;\n' +
1125
- ' box-shadow: 0 1px 3px rgba(0,0,0,0.08);\n' +
1126
- '}\n' +
1127
- '.stat-card h3 { font-size: 13px; color: var(--brand-teal); margin-bottom: 8px; }\n' +
1128
- '.stat-card .value { font-size: 28px; font-weight: 700; color: var(--brand-dark-brown); }\n' +
1129
- '\n' +
1130
- '.agent-chat {\n' +
1131
- ' background: white; border-radius: 8px; padding: 20px;\n' +
1132
- ' box-shadow: 0 1px 3px rgba(0,0,0,0.08); margin-top: 24px;\n' +
1133
- '}\n' +
1134
- '.agent-chat h3 { margin-bottom: 12px; color: var(--brand-dark-brown); }\n' +
1135
- '.agent-chat-messages {\n' +
1136
- ' max-height: 300px; overflow-y: auto; margin-bottom: 12px;\n' +
1137
- ' border: 1px solid #e0ddd7; border-radius: 6px; padding: 12px;\n' +
1138
- '}\n' +
1139
- '.agent-chat-input { display: flex; gap: 8px; }\n' +
1140
- '.agent-chat-input input {\n' +
1141
- ' flex: 1; padding: 10px 14px; border: 1px solid #e0ddd7;\n' +
1142
- ' border-radius: 6px; font-size: 14px;\n' +
1143
- '}\n' +
1144
- '.agent-chat-input button {\n' +
1145
- ' padding: 10px 20px; background: var(--brand-primary); color: white;\n' +
1146
- ' border: none; border-radius: 6px; font-size: 14px; cursor: pointer;\n' +
1147
- '}\n' +
1148
- '.message { margin-bottom: 8px; }\n' +
1149
- '.message.user { color: var(--brand-primary); }\n' +
1150
- '.message.agent { color: var(--brand-deep-green); }\n' +
1151
- '\n' +
1152
- '.page-header { margin-bottom: 24px; }\n' +
1153
- '.page-header h1 { font-size: 24px; color: var(--brand-dark-brown); }\n' +
1154
- '.page-header p { color: var(--brand-teal); margin-top: 4px; }\n';
1155
- }
1156
-
1157
- function generateIndexCss() {
1158
- return '*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n' +
1159
- 'html, body {\n' +
1160
- ' height: 100%;\n' +
1161
- " font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;\n" +
1162
- ' background: ' + BRAND.secondary + ';\n' +
1163
- ' color: ' + BRAND.darkBrown + ';\n' +
1164
- ' line-height: 1.6;\n' +
1165
- '}\n' +
1166
- '#root { height: 100%; }\n' +
1167
- 'a { color: ' + BRAND.primary + '; text-decoration: none; }\n' +
1168
- 'a:hover { text-decoration: underline; }\n' +
1169
- 'button { cursor: pointer; font-family: inherit; }\n';
1170
- }
1171
-
1172
- // ─── React Components ───────────────────────────────────────────────────────
1173
-
1174
- function generateLayoutComponent() {
1175
- return `import { Outlet } from 'react-router-dom';
1176
- import Sidebar from './Sidebar.jsx';
1177
- import Header from './Header.jsx';
1178
-
1179
- export default function Layout() {
992
+ /**
993
+ * Shared layout for authenticated pages with sidebar and header.
994
+ * Import and use this in page layouts that need the app shell.
995
+ */
996
+ export function AppShell({ children, user }) {
1180
997
  return (
1181
998
  <div className="layout">
1182
999
  <Sidebar />
1183
1000
  <div className="layout-main">
1184
- <Header />
1001
+ <Header user={user} />
1185
1002
  <main className="layout-content">
1186
- <Outlet />
1003
+ {children}
1187
1004
  </main>
1188
1005
  </div>
1189
1006
  </div>
1190
1007
  );
1191
1008
  }
1192
1009
  `;
1010
+
1011
+ return code;
1193
1012
  }
1194
1013
 
1195
- function generateSidebarComponent(features) {
1196
- const links = [' <li><a href="/">Home</a></li>'];
1197
- if (features.includes('dashboard')) links.push(' <li><a href="/dashboard">Dashboard</a></li>');
1198
- if (features.includes('settings')) links.push(' <li><a href="/settings">Settings</a></li>');
1199
- if (features.includes('users')) links.push(' <li><a href="/users">Users</a></li>');
1200
- if (features.includes('notifications')) links.push(' <li><a href="/notifications">Notifications</a></li>');
1201
-
1202
- return 'export default function Sidebar() {\n' +
1203
- ' return (\n' +
1204
- ' <aside className="sidebar">\n' +
1205
- ' <div className="sidebar-brand">\n' +
1206
- ' <h2>MyVillageOS</h2>\n' +
1207
- ' </div>\n' +
1208
- ' <ul className="sidebar-nav">\n' +
1209
- links.join('\n') + '\n' +
1210
- ' </ul>\n' +
1211
- ' </aside>\n' +
1212
- ' );\n' +
1213
- '}\n';
1014
+ function generateGlobalsCss() {
1015
+ return `*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
1016
+ html, body {
1017
+ height: 100%;
1018
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
1019
+ background: ${BRAND.secondary};
1020
+ color: ${BRAND.darkBrown};
1021
+ line-height: 1.6;
1022
+ }
1023
+ #__next { height: 100%; }
1024
+ a { color: ${BRAND.primary}; text-decoration: none; }
1025
+ a:hover { text-decoration: underline; }
1026
+ button { cursor: pointer; font-family: inherit; }
1027
+
1028
+ :root {
1029
+ --brand-gold: ${BRAND.gold};
1030
+ --brand-brown: ${BRAND.brown};
1031
+ --brand-green: ${BRAND.green};
1032
+ --brand-primary: ${BRAND.primary};
1033
+ --brand-secondary: ${BRAND.secondary};
1034
+ --brand-dark-brown: ${BRAND.darkBrown};
1035
+ --brand-deep-green: ${BRAND.deepGreen};
1036
+ --brand-teal: ${BRAND.teal};
1037
+ --sidebar-width: 240px;
1038
+ --header-height: 56px;
1214
1039
  }
1215
1040
 
1216
- function generateHeaderComponent(hasOAuth) {
1217
- if (hasOAuth) {
1218
- return `import { useAuth } from '../auth/AuthProvider.jsx';
1041
+ .layout { display: flex; height: 100vh; }
1042
+ .layout-main { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
1043
+ .layout-content { flex: 1; overflow-y: auto; padding: 24px; background: #f5f3ef; }
1219
1044
 
1220
- export default function Header() {
1221
- const { user, logout } = useAuth();
1045
+ .sidebar {
1046
+ width: var(--sidebar-width);
1047
+ background: var(--brand-dark-brown);
1048
+ color: var(--brand-secondary);
1049
+ display: flex;
1050
+ flex-direction: column;
1051
+ }
1052
+ .sidebar-brand { padding: 16px 20px; border-bottom: 1px solid rgba(255,255,255,0.1); }
1053
+ .sidebar-brand h2 { color: var(--brand-gold); font-size: 18px; margin: 0; }
1054
+ .sidebar-nav { flex: 1; padding: 12px 0; list-style: none; }
1055
+ .sidebar-nav a {
1056
+ display: block; padding: 10px 20px; color: var(--brand-secondary);
1057
+ text-decoration: none; font-size: 14px; transition: background 0.2s;
1058
+ }
1059
+ .sidebar-nav a:hover, .sidebar-nav a.active {
1060
+ background: rgba(255,215,0,0.1); color: var(--brand-gold);
1061
+ }
1222
1062
 
1223
- return (
1224
- <header className="header">
1225
- <span className="header-title">MyVillageOS App</span>
1226
- <div className="header-user">
1227
- {user && <span>{user.name || user.email}</span>}
1228
- {user && <button onClick={logout}>Sign out</button>}
1229
- </div>
1230
- </header>
1231
- );
1063
+ .header {
1064
+ height: var(--header-height); background: white; border-bottom: 1px solid #e0ddd7;
1065
+ display: flex; align-items: center; justify-content: space-between; padding: 0 24px;
1066
+ }
1067
+ .header-title { font-size: 16px; font-weight: 600; color: var(--brand-dark-brown); }
1068
+ .header-user { display: flex; align-items: center; gap: 12px; }
1069
+ .header-user button {
1070
+ padding: 6px 14px; background: var(--brand-primary); color: white;
1071
+ border: none; border-radius: 6px; font-size: 13px; cursor: pointer;
1232
1072
  }
1233
- `;
1234
- }
1235
1073
 
1236
- return `export default function Header() {
1237
- return (
1238
- <header className="header">
1239
- <span className="header-title">MyVillageOS App</span>
1240
- </header>
1241
- );
1074
+ .login-page {
1075
+ display: flex; justify-content: center; align-items: center; height: 100vh;
1076
+ background: var(--brand-secondary);
1242
1077
  }
1243
- `;
1078
+ .login-card {
1079
+ text-align: center; padding: 48px; background: white;
1080
+ border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.1);
1244
1081
  }
1082
+ .login-btn {
1083
+ margin-top: 20px; padding: 12px 32px; background: var(--brand-primary);
1084
+ color: white; border: none; border-radius: 8px; font-size: 16px; cursor: pointer;
1085
+ }
1086
+ .login-btn:hover { background: var(--brand-gold); color: var(--brand-dark-brown); }
1245
1087
 
1246
- function generateAgentChatComponent() {
1247
- return "import { useState } from 'react';\n" +
1248
- '\n' +
1249
- 'export default function AgentChat() {\n' +
1250
- ' const [messages, setMessages] = useState([]);\n' +
1251
- " const [input, setInput] = useState('');\n" +
1252
- ' const [loading, setLoading] = useState(false);\n' +
1253
- '\n' +
1254
- ' const sendMessage = async () => {\n' +
1255
- ' if (!input.trim() || loading) return;\n' +
1256
- '\n' +
1257
- ' const userMsg = input.trim();\n' +
1258
- " setMessages(prev => [...prev, { role: 'user', text: userMsg }]);\n" +
1259
- " setInput('');\n" +
1260
- ' setLoading(true);\n' +
1261
- '\n' +
1262
- ' try {\n' +
1263
- " const res = await fetch('/agent/chat', {\n" +
1264
- " method: 'POST',\n" +
1265
- " headers: { 'Content-Type': 'application/json' },\n" +
1266
- ' body: JSON.stringify({ message: userMsg }),\n' +
1267
- ' });\n' +
1268
- ' const data = await res.json();\n' +
1269
- " setMessages(prev => [...prev, { role: 'agent', text: data.response || data.error }]);\n" +
1270
- ' } catch (err) {\n' +
1271
- " setMessages(prev => [...prev, { role: 'agent', text: 'Error: ' + err.message }]);\n" +
1272
- ' } finally {\n' +
1273
- ' setLoading(false);\n' +
1274
- ' }\n' +
1275
- ' };\n' +
1276
- '\n' +
1277
- ' return (\n' +
1278
- ' <div className="agent-chat">\n' +
1279
- ' <h3>AI Agent</h3>\n' +
1280
- ' <div className="agent-chat-messages">\n' +
1281
- " {messages.length === 0 && <p style={{ color: '#999' }}>Ask the agent anything about your village...</p>}\n" +
1282
- ' {messages.map((msg, i) => (\n' +
1283
- ' <div key={i} className={`message ${msg.role}`}>\n' +
1284
- " <strong>{msg.role === 'user' ? 'You' : 'Agent'}:</strong> {msg.text}\n" +
1285
- ' </div>\n' +
1286
- ' ))}\n' +
1287
- ' {loading && <div className="message agent"><em>Thinking...</em></div>}\n' +
1288
- ' </div>\n' +
1289
- ' <div className="agent-chat-input">\n' +
1290
- ' <input\n' +
1291
- ' value={input}\n' +
1292
- ' onChange={e => setInput(e.target.value)}\n' +
1293
- " onKeyDown={e => e.key === 'Enter' && sendMessage()}\n" +
1294
- ' placeholder="Ask the AI agent..."\n' +
1295
- ' disabled={loading}\n' +
1296
- ' />\n' +
1297
- ' <button onClick={sendMessage} disabled={loading}>Send</button>\n' +
1298
- ' </div>\n' +
1299
- ' </div>\n' +
1300
- ' );\n' +
1301
- '}\n';
1302
- }
1303
-
1304
- function generateProtectedRoute() {
1305
- return `import { Navigate } from 'react-router-dom';
1306
- import { useAuth } from '../auth/AuthProvider.jsx';
1307
-
1308
- export default function ProtectedRoute({ children }) {
1309
- const { isAuthenticated, isLoading } = useAuth();
1310
- if (isLoading) return <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>Loading...</div>;
1311
- if (!isAuthenticated) return <Navigate to="/login" replace />;
1312
- return children;
1088
+ .stat-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 16px; margin-bottom: 24px; }
1089
+ .stat-card {
1090
+ background: white; border-radius: 8px; padding: 20px;
1091
+ box-shadow: 0 1px 3px rgba(0,0,0,0.08);
1313
1092
  }
1093
+ .stat-card h3 { font-size: 13px; color: var(--brand-teal); margin-bottom: 8px; }
1094
+ .stat-card .value { font-size: 28px; font-weight: 700; color: var(--brand-dark-brown); }
1095
+
1096
+ .agent-chat {
1097
+ background: white; border-radius: 8px; padding: 20px;
1098
+ box-shadow: 0 1px 3px rgba(0,0,0,0.08); margin-top: 24px;
1099
+ }
1100
+ .agent-chat h3 { margin-bottom: 12px; color: var(--brand-dark-brown); }
1101
+ .agent-chat-messages {
1102
+ max-height: 300px; overflow-y: auto; margin-bottom: 12px;
1103
+ border: 1px solid #e0ddd7; border-radius: 6px; padding: 12px;
1104
+ }
1105
+ .agent-chat-input { display: flex; gap: 8px; }
1106
+ .agent-chat-input input {
1107
+ flex: 1; padding: 10px 14px; border: 1px solid #e0ddd7;
1108
+ border-radius: 6px; font-size: 14px;
1109
+ }
1110
+ .agent-chat-input button {
1111
+ padding: 10px 20px; background: var(--brand-primary); color: white;
1112
+ border: none; border-radius: 6px; font-size: 14px; cursor: pointer;
1113
+ }
1114
+ .message { margin-bottom: 8px; }
1115
+ .message.user { color: var(--brand-primary); }
1116
+ .message.agent { color: var(--brand-deep-green); }
1117
+
1118
+ .page-header { margin-bottom: 24px; }
1119
+ .page-header h1 { font-size: 24px; color: var(--brand-dark-brown); }
1120
+ .page-header p { color: var(--brand-teal); margin-top: 4px; }
1121
+
1122
+ .welcome { padding: 40px; text-align: center; }
1123
+ .welcome h1 { color: var(--brand-dark-brown); margin-bottom: 8px; }
1124
+ .welcome p { color: var(--brand-teal); }
1314
1125
  `;
1315
1126
  }
1316
1127
 
1317
- function generateBrowserOAuthModule() {
1318
- return '// Browser-side OAuth 2.0 + PKCE implementation\n' +
1319
- '\n' +
1320
- 'function base64UrlEncode(buffer) {\n' +
1321
- ' const bytes = new Uint8Array(buffer);\n' +
1322
- " let str = '';\n" +
1323
- " for (const b of bytes) str += String.fromCharCode(b);\n" +
1324
- " return btoa(str).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n" +
1325
- '}\n' +
1326
- '\n' +
1327
- 'export function generateCodeVerifier() {\n' +
1328
- ' const array = new Uint8Array(64);\n' +
1329
- ' crypto.getRandomValues(array);\n' +
1330
- ' return base64UrlEncode(array);\n' +
1331
- '}\n' +
1332
- '\n' +
1333
- 'export async function generateCodeChallenge(verifier) {\n' +
1334
- ' const encoder = new TextEncoder();\n' +
1335
- ' const data = encoder.encode(verifier);\n' +
1336
- " const digest = await crypto.subtle.digest('SHA-256', data);\n" +
1337
- ' return base64UrlEncode(digest);\n' +
1338
- '}\n' +
1339
- '\n' +
1340
- 'export async function startLogin() {\n' +
1341
- ' const verifier = generateCodeVerifier();\n' +
1342
- ' const challenge = await generateCodeChallenge(verifier);\n' +
1343
- ' const state = base64UrlEncode(crypto.getRandomValues(new Uint8Array(32)));\n' +
1344
- '\n' +
1345
- " sessionStorage.setItem('oauth_verifier', verifier);\n" +
1346
- " sessionStorage.setItem('oauth_state', state);\n" +
1347
- '\n' +
1348
- ' const baseUrl = import.meta.env.VITE_OAUTH_BASE_URL;\n' +
1349
- ' const clientId = import.meta.env.VITE_OAUTH_CLIENT_ID;\n' +
1350
- ' const redirectUri = import.meta.env.VITE_OAUTH_REDIRECT_URI;\n' +
1351
- '\n' +
1352
- ' const params = new URLSearchParams({\n' +
1353
- ' client_id: clientId,\n' +
1354
- ' redirect_uri: redirectUri,\n' +
1355
- " response_type: 'code',\n" +
1356
- " scope: 'openid profile email villager offline_access',\n" +
1357
- ' state,\n' +
1358
- ' code_challenge: challenge,\n' +
1359
- " code_challenge_method: 'S256',\n" +
1360
- ' });\n' +
1361
- '\n' +
1362
- ' window.location.href = `${baseUrl}/authorize?${params.toString()}`;\n' +
1363
- '}\n' +
1364
- '\n' +
1365
- 'export async function handleCallback(code, state) {\n' +
1366
- " const savedState = sessionStorage.getItem('oauth_state');\n" +
1367
- " const verifier = sessionStorage.getItem('oauth_verifier');\n" +
1368
- " if (state !== savedState) throw new Error('OAuth state mismatch');\n" +
1369
- '\n' +
1370
- ' const baseUrl = import.meta.env.VITE_OAUTH_BASE_URL;\n' +
1371
- ' const clientId = import.meta.env.VITE_OAUTH_CLIENT_ID;\n' +
1372
- ' const clientSecret = import.meta.env.VITE_OAUTH_CLIENT_SECRET;\n' +
1373
- ' const redirectUri = import.meta.env.VITE_OAUTH_REDIRECT_URI;\n' +
1374
- '\n' +
1375
- ' const body = new URLSearchParams({\n' +
1376
- " grant_type: 'authorization_code',\n" +
1377
- ' code,\n' +
1378
- ' redirect_uri: redirectUri,\n' +
1379
- ' client_id: clientId,\n' +
1380
- ' client_secret: clientSecret,\n' +
1381
- ' code_verifier: verifier,\n' +
1382
- ' });\n' +
1383
- '\n' +
1384
- ' const response = await fetch(`${baseUrl}/token`, {\n' +
1385
- " method: 'POST',\n" +
1386
- " headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n" +
1387
- ' body: body.toString(),\n' +
1388
- ' });\n' +
1389
- '\n' +
1390
- " if (!response.ok) throw new Error('Token exchange failed');\n" +
1391
- '\n' +
1392
- ' const tokens = await response.json();\n' +
1393
- ' storeTokens(tokens);\n' +
1394
- " sessionStorage.removeItem('oauth_verifier');\n" +
1395
- " sessionStorage.removeItem('oauth_state');\n" +
1396
- ' return tokens;\n' +
1397
- '}\n' +
1398
- '\n' +
1399
- 'export async function refreshToken(token) {\n' +
1400
- ' const baseUrl = import.meta.env.VITE_OAUTH_BASE_URL;\n' +
1401
- ' const clientId = import.meta.env.VITE_OAUTH_CLIENT_ID;\n' +
1402
- ' const clientSecret = import.meta.env.VITE_OAUTH_CLIENT_SECRET;\n' +
1403
- '\n' +
1404
- ' const body = new URLSearchParams({\n' +
1405
- " grant_type: 'refresh_token',\n" +
1406
- ' refresh_token: token,\n' +
1407
- ' client_id: clientId,\n' +
1408
- ' client_secret: clientSecret,\n' +
1409
- ' });\n' +
1410
- '\n' +
1411
- ' const response = await fetch(`${baseUrl}/token`, {\n' +
1412
- " method: 'POST',\n" +
1413
- " headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n" +
1414
- ' body: body.toString(),\n' +
1415
- ' });\n' +
1416
- '\n' +
1417
- " if (!response.ok) throw new Error('Token refresh failed');\n" +
1418
- '\n' +
1419
- ' const tokens = await response.json();\n' +
1420
- ' storeTokens(tokens);\n' +
1421
- ' return tokens;\n' +
1422
- '}\n' +
1423
- '\n' +
1424
- 'export function getStoredTokens() {\n' +
1425
- " const raw = localStorage.getItem('myvillage_tokens');\n" +
1426
- ' return raw ? JSON.parse(raw) : null;\n' +
1427
- '}\n' +
1428
- '\n' +
1429
- 'export function storeTokens(tokens) {\n' +
1430
- " localStorage.setItem('myvillage_tokens', JSON.stringify(tokens));\n" +
1431
- '}\n' +
1432
- '\n' +
1433
- 'export function clearTokens() {\n' +
1434
- " localStorage.removeItem('myvillage_tokens');\n" +
1435
- '}\n';
1436
- }
1437
-
1438
- function generateAuthProvider() {
1439
- return "import { createContext, useContext, useState, useEffect, useCallback } from 'react';\n" +
1440
- "import { useNavigate } from 'react-router-dom';\n" +
1441
- "import { startLogin, getStoredTokens, clearTokens, refreshToken } from './oauth.js';\n" +
1442
- '\n' +
1443
- 'const AuthContext = createContext(null);\n' +
1444
- '\n' +
1445
- 'export function useAuth() {\n' +
1446
- ' const ctx = useContext(AuthContext);\n' +
1447
- " if (!ctx) throw new Error('useAuth must be used within AuthProvider');\n" +
1448
- ' return ctx;\n' +
1449
- '}\n' +
1450
- '\n' +
1451
- 'export function AuthProvider({ children }) {\n' +
1452
- ' const [user, setUser] = useState(null);\n' +
1453
- ' const [isLoading, setIsLoading] = useState(true);\n' +
1454
- ' const navigate = useNavigate();\n' +
1455
- '\n' +
1456
- ' const fetchUser = useCallback(async (accessToken) => {\n' +
1457
- ' const baseUrl = import.meta.env.VITE_OAUTH_BASE_URL;\n' +
1458
- ' const res = await fetch(`${baseUrl}/userinfo`, {\n' +
1459
- ' headers: { Authorization: `Bearer ${accessToken}` },\n' +
1460
- ' });\n' +
1461
- ' if (res.ok) {\n' +
1462
- ' setUser(await res.json());\n' +
1463
- ' } else {\n' +
1464
- ' clearTokens();\n' +
1465
- ' setUser(null);\n' +
1466
- ' }\n' +
1467
- ' }, []);\n' +
1468
- '\n' +
1469
- ' useEffect(() => {\n' +
1470
- ' const tokens = getStoredTokens();\n' +
1471
- ' if (tokens?.access_token) {\n' +
1472
- ' fetchUser(tokens.access_token).finally(() => setIsLoading(false));\n' +
1473
- ' } else {\n' +
1474
- ' setIsLoading(false);\n' +
1475
- ' }\n' +
1476
- ' }, [fetchUser]);\n' +
1477
- '\n' +
1478
- ' useEffect(() => {\n' +
1479
- ' const tokens = getStoredTokens();\n' +
1480
- ' if (!tokens?.expires_in || !tokens?.refresh_token) return;\n' +
1481
- ' const refreshMs = (tokens.expires_in - 60) * 1000;\n' +
1482
- ' if (refreshMs <= 0) return;\n' +
1483
- ' const timer = setTimeout(async () => {\n' +
1484
- ' try {\n' +
1485
- ' const newTokens = await refreshToken(tokens.refresh_token);\n' +
1486
- ' await fetchUser(newTokens.access_token);\n' +
1487
- ' } catch {\n' +
1488
- ' clearTokens();\n' +
1489
- ' setUser(null);\n' +
1490
- ' }\n' +
1491
- ' }, refreshMs);\n' +
1492
- ' return () => clearTimeout(timer);\n' +
1493
- ' }, [user, fetchUser]);\n' +
1494
- '\n' +
1495
- ' const login = () => startLogin();\n' +
1496
- " const logout = () => { clearTokens(); setUser(null); navigate('/login'); };\n" +
1497
- ' const isAuthenticated = !!user;\n' +
1498
- '\n' +
1499
- ' return (\n' +
1500
- ' <AuthContext.Provider value={{ user, login, logout, isAuthenticated, isLoading }}>\n' +
1501
- ' {children}\n' +
1502
- ' </AuthContext.Provider>\n' +
1503
- ' );\n' +
1504
- '}\n';
1128
+ function generateHomePage(hasDashboard) {
1129
+ if (hasDashboard) {
1130
+ return `import { redirect } from 'next/navigation';
1131
+
1132
+ export default function Home() {
1133
+ redirect('/dashboard');
1134
+ }
1135
+ `;
1136
+ }
1137
+
1138
+ return `import { AppShell } from './layout';
1139
+ import { getSession } from '@/lib/session';
1140
+
1141
+ export default async function Home() {
1142
+ const session = await getSession();
1143
+
1144
+ return (
1145
+ <AppShell user={session.user}>
1146
+ <div className="welcome">
1147
+ <h1>Welcome</h1>
1148
+ <p>Select a page from the sidebar to get started.</p>
1149
+ </div>
1150
+ </AppShell>
1151
+ );
1152
+ }
1153
+ `;
1505
1154
  }
1506
1155
 
1507
1156
  function generateLoginPage() {
1508
- return `import { useAuth } from '../auth/AuthProvider.jsx';
1157
+ return `export default function LoginPage({ searchParams }) {
1158
+ const error = searchParams?.error;
1509
1159
 
1510
- export default function Login() {
1511
- const { login } = useAuth();
1512
1160
  return (
1513
1161
  <div className="login-page">
1514
1162
  <div className="login-card">
1515
- <h1>MyVillageOS</h1>
1516
- <p>Sign in to access your application</p>
1517
- <button className="login-btn" onClick={login}>Sign in with MyVillageOS</button>
1163
+ <h1 style={{ color: 'var(--brand-dark-brown)', marginBottom: '8px' }}>Welcome</h1>
1164
+ <p style={{ color: 'var(--brand-teal)', marginBottom: '24px' }}>
1165
+ Sign in with your MyVillageOS account to continue.
1166
+ </p>
1167
+ {error && (
1168
+ <p style={{ color: '#dc2626', marginBottom: '16px', fontSize: '14px' }}>
1169
+ {decodeURIComponent(error)}
1170
+ </p>
1171
+ )}
1172
+ <a href="/api/auth/login" className="login-btn" style={{ textDecoration: 'none' }}>
1173
+ Sign in with MyVillageOS
1174
+ </a>
1518
1175
  </div>
1519
1176
  </div>
1520
1177
  );
@@ -1522,131 +1179,241 @@ export default function Login() {
1522
1179
  `;
1523
1180
  }
1524
1181
 
1525
- function generateCallbackPage() {
1526
- return `import { useEffect, useState } from 'react';
1527
- import { useNavigate, useSearchParams } from 'react-router-dom';
1528
- import { handleCallback } from '../auth/oauth.js';
1529
-
1530
- export default function Callback() {
1531
- const [searchParams] = useSearchParams();
1532
- const navigate = useNavigate();
1533
- const [error, setError] = useState(null);
1534
-
1535
- useEffect(() => {
1536
- const code = searchParams.get('code');
1537
- const state = searchParams.get('state');
1538
- if (!code || !state) { setError('Missing authorization code or state'); return; }
1539
- handleCallback(code, state)
1540
- .then(() => navigate('/', { replace: true }))
1541
- .catch((err) => setError(err.message));
1542
- }, [searchParams, navigate]);
1182
+ function generateNotFoundPage() {
1183
+ return `import Link from 'next/link';
1543
1184
 
1544
- if (error) {
1545
- return (
1546
- <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh', flexDirection: 'column' }}>
1547
- <h2>Authentication Error</h2>
1548
- <p>{error}</p>
1549
- <a href="/login">Try again</a>
1185
+ export default function NotFound() {
1186
+ return (
1187
+ <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
1188
+ <div style={{ textAlign: 'center' }}>
1189
+ <h1 style={{ fontSize: '48px', color: 'var(--brand-dark-brown)' }}>404</h1>
1190
+ <p style={{ color: 'var(--brand-teal)', marginBottom: '16px' }}>Page not found</p>
1191
+ <Link href="/" style={{ color: 'var(--brand-primary)' }}>Go home</Link>
1550
1192
  </div>
1551
- );
1552
- }
1553
-
1554
- return <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>Completing sign in...</div>;
1193
+ </div>
1194
+ );
1555
1195
  }
1556
1196
  `;
1557
1197
  }
1558
1198
 
1559
- // ─── Page Generators ────────────────────────────────────────────────────────
1560
-
1561
1199
  function generateDashboardPage(includeMcp) {
1562
- return `import AgentChat from '../components/AgentChat.jsx';
1200
+ return `import { AppShell } from '../layout';
1201
+ import { getSession } from '@/lib/session';
1202
+ import AgentChat from '@/components/AgentChat';
1203
+
1204
+ export default async function DashboardPage() {
1205
+ const session = await getSession();
1563
1206
 
1564
- export default function Dashboard() {
1565
1207
  return (
1566
- <div>
1208
+ <AppShell user={session.user}>
1567
1209
  <div className="page-header">
1568
1210
  <h1>Dashboard</h1>
1569
- <p>Your application overview</p>
1211
+ <p>Welcome back${includeMcp ? '. Your AI agent is ready to help.' : '.'}</p>
1570
1212
  </div>
1571
1213
  <div className="stat-cards">
1572
1214
  <div className="stat-card">
1573
- <h3>Communities</h3>
1574
- <div className="value">--</div>
1575
- </div>
1576
- <div className="stat-card">
1577
- <h3>Posts</h3>
1578
- <div className="value">--</div>
1579
- </div>
1580
- <div className="stat-card">
1581
- <h3>Members</h3>
1582
- <div className="value">--</div>
1215
+ <h3>Status</h3>
1216
+ <div className="value" style={{ color: 'var(--brand-green)' }}>Active</div>
1583
1217
  </div>
1584
1218
  </div>
1585
1219
  <AgentChat />
1586
- </div>
1220
+ </AppShell>
1587
1221
  );
1588
1222
  }
1589
1223
  `;
1590
1224
  }
1591
1225
 
1592
1226
  function generateSettingsPage() {
1593
- return `export default function Settings() {
1227
+ return `import { AppShell } from '../layout';
1228
+ import { getSession } from '@/lib/session';
1229
+
1230
+ export default async function SettingsPage() {
1231
+ const session = await getSession();
1232
+
1594
1233
  return (
1595
- <div>
1234
+ <AppShell user={session.user}>
1596
1235
  <div className="page-header">
1597
1236
  <h1>Settings</h1>
1598
- <p>Configure your application</p>
1237
+ <p>Manage your application settings.</p>
1599
1238
  </div>
1600
- <div style={{ background: 'white', borderRadius: 8, padding: 24 }}>
1601
- <p>Settings page - customize as needed.</p>
1239
+ <div style={{ background: 'white', borderRadius: '8px', padding: '24px', boxShadow: '0 1px 3px rgba(0,0,0,0.08)' }}>
1240
+ <p style={{ color: 'var(--brand-teal)' }}>Settings page \u2014 customize this for your application.</p>
1602
1241
  </div>
1603
- </div>
1242
+ </AppShell>
1604
1243
  );
1605
1244
  }
1606
1245
  `;
1607
1246
  }
1608
1247
 
1609
1248
  function generateUsersPage() {
1610
- return `export default function Users() {
1249
+ return `import { AppShell } from '../layout';
1250
+ import { getSession } from '@/lib/session';
1251
+
1252
+ export default async function UsersPage() {
1253
+ const session = await getSession();
1254
+
1611
1255
  return (
1612
- <div>
1256
+ <AppShell user={session.user}>
1613
1257
  <div className="page-header">
1614
1258
  <h1>Users</h1>
1615
- <p>Manage users and permissions</p>
1259
+ <p>Manage users and permissions.</p>
1616
1260
  </div>
1617
- <div style={{ background: 'white', borderRadius: 8, padding: 24 }}>
1618
- <p>User management - customize as needed.</p>
1261
+ <div style={{ background: 'white', borderRadius: '8px', padding: '24px', boxShadow: '0 1px 3px rgba(0,0,0,0.08)' }}>
1262
+ <p style={{ color: 'var(--brand-teal)' }}>Users page \u2014 customize this for your application.</p>
1619
1263
  </div>
1620
- </div>
1264
+ </AppShell>
1621
1265
  );
1622
1266
  }
1623
1267
  `;
1624
1268
  }
1625
1269
 
1626
1270
  function generateNotificationsPage() {
1627
- return `export default function Notifications() {
1271
+ return `import { AppShell } from '../layout';
1272
+ import { getSession } from '@/lib/session';
1273
+
1274
+ export default async function NotificationsPage() {
1275
+ const session = await getSession();
1276
+
1628
1277
  return (
1629
- <div>
1278
+ <AppShell user={session.user}>
1630
1279
  <div className="page-header">
1631
1280
  <h1>Notifications</h1>
1632
- <p>Stay updated on activity</p>
1281
+ <p>View your latest notifications.</p>
1633
1282
  </div>
1634
- <div style={{ background: 'white', borderRadius: 8, padding: 24 }}>
1635
- <p>No new notifications.</p>
1283
+ <div style={{ background: 'white', borderRadius: '8px', padding: '24px', boxShadow: '0 1px 3px rgba(0,0,0,0.08)' }}>
1284
+ <p style={{ color: 'var(--brand-teal)' }}>Notifications page \u2014 customize this for your application.</p>
1636
1285
  </div>
1637
- </div>
1286
+ </AppShell>
1638
1287
  );
1639
1288
  }
1640
1289
  `;
1641
1290
  }
1642
1291
 
1643
- function generateNotFoundPage() {
1644
- return `export default function NotFound() {
1292
+ // ─── Component Generators ───────────────────────────────────────────────────
1293
+
1294
+ function generateSidebarComponent(features) {
1295
+ const links = [" <li><a href=\"/\">Home</a></li>"];
1296
+ if (features.includes('dashboard')) links.push(" <li><a href=\"/dashboard\">Dashboard</a></li>");
1297
+ if (features.includes('settings')) links.push(" <li><a href=\"/settings\">Settings</a></li>");
1298
+ if (features.includes('users')) links.push(" <li><a href=\"/users\">Users</a></li>");
1299
+ if (features.includes('notifications')) links.push(" <li><a href=\"/notifications\">Notifications</a></li>");
1300
+
1301
+ return `import Link from 'next/link';
1302
+
1303
+ export default function Sidebar() {
1304
+ return (
1305
+ <aside className="sidebar">
1306
+ <div className="sidebar-brand">
1307
+ <h2>MyVillageOS</h2>
1308
+ </div>
1309
+ <ul className="sidebar-nav">
1310
+ ${links.map(l => l.replace(/<a href="/g, '<Link href="').replace(/<\/a>/g, '</Link>')).join('\n')}
1311
+ </ul>
1312
+ </aside>
1313
+ );
1314
+ }
1315
+ `;
1316
+ }
1317
+
1318
+ function generateHeaderComponent(hasOAuth) {
1319
+ if (hasOAuth) {
1320
+ return `import SignOutButton from './SignOutButton';
1321
+
1322
+ export default function Header({ user }) {
1323
+ return (
1324
+ <header className="header">
1325
+ <span className="header-title">MyVillageOS App</span>
1326
+ <div className="header-user">
1327
+ {user && <span>{user.name || user.email}</span>}
1328
+ {user && <SignOutButton />}
1329
+ </div>
1330
+ </header>
1331
+ );
1332
+ }
1333
+ `;
1334
+ }
1335
+
1336
+ return `export default function Header() {
1337
+ return (
1338
+ <header className="header">
1339
+ <span className="header-title">MyVillageOS App</span>
1340
+ </header>
1341
+ );
1342
+ }
1343
+ `;
1344
+ }
1345
+
1346
+ function generateSignOutButton() {
1347
+ return `'use client';
1348
+
1349
+ export default function SignOutButton() {
1350
+ const handleSignOut = async () => {
1351
+ await fetch('/api/auth/logout', { method: 'POST' });
1352
+ window.location.href = '/login';
1353
+ };
1354
+
1645
1355
  return (
1646
- <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '50vh', flexDirection: 'column' }}>
1647
- <h1>404</h1>
1648
- <p>Page not found</p>
1649
- <a href="/">Go home</a>
1356
+ <button onClick={handleSignOut}>Sign out</button>
1357
+ );
1358
+ }
1359
+ `;
1360
+ }
1361
+
1362
+ function generateAgentChatComponent() {
1363
+ return `'use client';
1364
+
1365
+ import { useState } from 'react';
1366
+
1367
+ export default function AgentChat() {
1368
+ const [messages, setMessages] = useState([]);
1369
+ const [input, setInput] = useState('');
1370
+ const [loading, setLoading] = useState(false);
1371
+
1372
+ const sendMessage = async () => {
1373
+ if (!input.trim() || loading) return;
1374
+
1375
+ const userMsg = input.trim();
1376
+ setMessages(prev => [...prev, { role: 'user', text: userMsg }]);
1377
+ setInput('');
1378
+ setLoading(true);
1379
+
1380
+ try {
1381
+ const res = await fetch('/api/agent/chat', {
1382
+ method: 'POST',
1383
+ headers: { 'Content-Type': 'application/json' },
1384
+ body: JSON.stringify({ message: userMsg }),
1385
+ });
1386
+ const data = await res.json();
1387
+ setMessages(prev => [...prev, { role: 'agent', text: data.response || data.error }]);
1388
+ } catch (err) {
1389
+ setMessages(prev => [...prev, { role: 'agent', text: 'Error: ' + err.message }]);
1390
+ } finally {
1391
+ setLoading(false);
1392
+ }
1393
+ };
1394
+
1395
+ return (
1396
+ <div className="agent-chat">
1397
+ <h3>AI Agent</h3>
1398
+ <div className="agent-chat-messages">
1399
+ {messages.length === 0 && <p style={{ color: '#999' }}>Ask the agent anything about your village...</p>}
1400
+ {messages.map((msg, i) => (
1401
+ <div key={i} className={\`message \${msg.role}\`}>
1402
+ <strong>{msg.role === 'user' ? 'You' : 'Agent'}:</strong> {msg.text}
1403
+ </div>
1404
+ ))}
1405
+ {loading && <div className="message agent"><em>Thinking...</em></div>}
1406
+ </div>
1407
+ <div className="agent-chat-input">
1408
+ <input
1409
+ value={input}
1410
+ onChange={e => setInput(e.target.value)}
1411
+ onKeyDown={e => e.key === 'Enter' && sendMessage()}
1412
+ placeholder="Ask the AI agent..."
1413
+ disabled={loading}
1414
+ />
1415
+ <button onClick={sendMessage} disabled={loading}>Send</button>
1416
+ </div>
1650
1417
  </div>
1651
1418
  );
1652
1419
  }