@codebakers/cli 2.9.0 → 3.0.1

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.
@@ -53,11 +53,15 @@ class CodeBakersServer {
53
53
  server;
54
54
  apiKey;
55
55
  apiUrl;
56
+ trialState;
57
+ authMode;
56
58
  autoUpdateChecked = false;
57
59
  autoUpdateInProgress = false;
58
60
  constructor() {
59
61
  this.apiKey = (0, config_js_1.getApiKey)();
60
62
  this.apiUrl = (0, config_js_1.getApiUrl)();
63
+ this.trialState = (0, config_js_1.getTrialState)();
64
+ this.authMode = (0, config_js_1.getAuthMode)();
61
65
  this.server = new index_js_1.Server({
62
66
  name: 'codebakers',
63
67
  version: '1.0.0',
@@ -72,12 +76,25 @@ class CodeBakersServer {
72
76
  // Silently ignore errors - don't interrupt user
73
77
  });
74
78
  }
79
+ /**
80
+ * Get authorization headers for API requests
81
+ * Supports both API key (paid users) and trial ID (free users)
82
+ */
83
+ getAuthHeaders() {
84
+ if (this.apiKey) {
85
+ return { 'Authorization': `Bearer ${this.apiKey}` };
86
+ }
87
+ if (this.trialState?.trialId) {
88
+ return { 'X-Trial-Id': this.trialState.trialId };
89
+ }
90
+ return {};
91
+ }
75
92
  /**
76
93
  * Automatically check for and apply pattern updates
77
94
  * Runs silently in background - no user intervention needed
78
95
  */
79
96
  async checkAndAutoUpdate() {
80
- if (this.autoUpdateChecked || this.autoUpdateInProgress || !this.apiKey) {
97
+ if (this.autoUpdateChecked || this.autoUpdateInProgress || this.authMode === 'none') {
81
98
  return;
82
99
  }
83
100
  this.autoUpdateInProgress = true;
@@ -110,7 +127,7 @@ class CodeBakersServer {
110
127
  }
111
128
  // Fetch latest version
112
129
  const response = await fetch(`${this.apiUrl}/api/content/version`, {
113
- headers: { 'Authorization': `Bearer ${this.apiKey}` },
130
+ headers: this.getAuthHeaders(),
114
131
  });
115
132
  if (!response.ok) {
116
133
  this.autoUpdateInProgress = false;
@@ -128,7 +145,7 @@ class CodeBakersServer {
128
145
  }
129
146
  // Fetch full content and update
130
147
  const contentResponse = await fetch(`${this.apiUrl}/api/content`, {
131
- headers: { 'Authorization': `Bearer ${this.apiKey}` },
148
+ headers: this.getAuthHeaders(),
132
149
  });
133
150
  if (!contentResponse.ok) {
134
151
  this.autoUpdateInProgress = false;
@@ -362,7 +379,7 @@ class CodeBakersServer {
362
379
  let latest = null;
363
380
  try {
364
381
  const response = await fetch(`${this.apiUrl}/api/content/version`, {
365
- headers: this.apiKey ? { 'Authorization': `Bearer ${this.apiKey}` } : {},
382
+ headers: this.getAuthHeaders(),
366
383
  });
367
384
  if (response.ok) {
368
385
  latest = await response.json();
@@ -801,12 +818,114 @@ class CodeBakersServer {
801
818
  required: ['token'],
802
819
  },
803
820
  },
821
+ {
822
+ name: 'update_constant',
823
+ description: 'Update a business constant (pricing, trial days, module count, etc.) using natural language. Use this when user says things like "change Pro price to $59", "set trial to 10 days", "update module count to 45". Automatically edits src/lib/constants.ts.',
824
+ inputSchema: {
825
+ type: 'object',
826
+ properties: {
827
+ request: {
828
+ type: 'string',
829
+ description: 'Natural language request describing what to change (e.g., "change Pro monthly price to $59", "set anonymous trial days to 10", "update Agency seats to 10")',
830
+ },
831
+ },
832
+ required: ['request'],
833
+ },
834
+ },
835
+ {
836
+ name: 'update_schema',
837
+ description: 'Add or modify database tables using natural language. Use this when user says things like "add a tags table", "add a status field to users", "create a comments table with user_id and content". Automatically edits src/db/schema.ts and creates migration.',
838
+ inputSchema: {
839
+ type: 'object',
840
+ properties: {
841
+ request: {
842
+ type: 'string',
843
+ description: 'Natural language request describing schema changes (e.g., "add a tags table with name and color fields", "add isArchived boolean to projects table", "create a comments table")',
844
+ },
845
+ },
846
+ required: ['request'],
847
+ },
848
+ },
849
+ {
850
+ name: 'update_env',
851
+ description: 'Add or update environment variables. Use this when user says things like "add OPENAI_API_KEY", "set up Stripe keys", "add database URL". Updates both .env.local and .env.example.',
852
+ inputSchema: {
853
+ type: 'object',
854
+ properties: {
855
+ request: {
856
+ type: 'string',
857
+ description: 'Natural language request describing env vars to add (e.g., "add OPENAI_API_KEY", "add Stripe test keys", "add RESEND_API_KEY for emails")',
858
+ },
859
+ },
860
+ required: ['request'],
861
+ },
862
+ },
863
+ {
864
+ name: 'billing_action',
865
+ description: 'Perform billing and subscription actions. Use this when user says things like "show my subscription", "extend my trial", "upgrade to Pro", "check my usage". Opens billing page or shows subscription info.',
866
+ inputSchema: {
867
+ type: 'object',
868
+ properties: {
869
+ action: {
870
+ type: 'string',
871
+ description: 'Natural language billing action (e.g., "show my subscription", "extend trial", "upgrade to team", "check usage")',
872
+ },
873
+ },
874
+ required: ['action'],
875
+ },
876
+ },
877
+ {
878
+ name: 'add_page',
879
+ description: 'Create a new page or route in the Next.js app. Use this when user says things like "create a settings page", "add an about page", "make a dashboard page with stats". Creates the file in src/app/ with proper structure.',
880
+ inputSchema: {
881
+ type: 'object',
882
+ properties: {
883
+ request: {
884
+ type: 'string',
885
+ description: 'Natural language request describing the page (e.g., "create a settings page with tabs", "add an about page", "make a user profile page")',
886
+ },
887
+ },
888
+ required: ['request'],
889
+ },
890
+ },
891
+ {
892
+ name: 'add_api_route',
893
+ description: 'Create a new API route endpoint. Use this when user says things like "create a feedback endpoint", "add an API for user preferences", "make a webhook endpoint for Stripe". Creates properly structured route.ts file.',
894
+ inputSchema: {
895
+ type: 'object',
896
+ properties: {
897
+ request: {
898
+ type: 'string',
899
+ description: 'Natural language request describing the API route (e.g., "create POST endpoint for feedback", "add GET/POST for user settings", "make a Stripe webhook endpoint")',
900
+ },
901
+ },
902
+ required: ['request'],
903
+ },
904
+ },
804
905
  ],
805
906
  }));
806
907
  // Handle tool calls
807
908
  this.server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
808
- if (!this.apiKey) {
809
- throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidRequest, 'Not logged in. Run `codebakers login` first.');
909
+ // Check access: API key OR valid trial
910
+ if (this.authMode === 'none') {
911
+ throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidRequest, 'Not logged in. Run `codebakers go` to start a free trial, or `codebakers setup` if you have an account.');
912
+ }
913
+ // Check if trial expired
914
+ if (this.authMode === 'trial' && (0, config_js_1.isTrialExpired)()) {
915
+ const trialState = (0, config_js_1.getTrialState)();
916
+ if (trialState?.stage === 'anonymous') {
917
+ throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidRequest, 'Trial expired. Run `codebakers extend` to add 7 more days with GitHub, or `codebakers billing` to upgrade.');
918
+ }
919
+ else {
920
+ throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidRequest, 'Trial expired. Run `codebakers billing` to upgrade to a paid plan.');
921
+ }
922
+ }
923
+ // Show warning if trial expiring soon
924
+ if (this.authMode === 'trial') {
925
+ const daysRemaining = (0, config_js_1.getTrialDaysRemaining)();
926
+ if (daysRemaining <= 2) {
927
+ console.error(`[CodeBakers] Trial expires in ${daysRemaining} day${daysRemaining !== 1 ? 's' : ''}. Run 'codebakers extend' or 'codebakers billing'.`);
928
+ }
810
929
  }
811
930
  const { name, arguments: args } = request.params;
812
931
  switch (name) {
@@ -856,6 +975,18 @@ class CodeBakersServer {
856
975
  return this.handleVercelDeployments(args);
857
976
  case 'vercel_connect':
858
977
  return this.handleVercelConnect(args);
978
+ case 'update_constant':
979
+ return this.handleUpdateConstant(args);
980
+ case 'update_schema':
981
+ return this.handleUpdateSchema(args);
982
+ case 'update_env':
983
+ return this.handleUpdateEnv(args);
984
+ case 'billing_action':
985
+ return this.handleBillingAction(args);
986
+ case 'add_page':
987
+ return this.handleAddPage(args);
988
+ case 'add_api_route':
989
+ return this.handleAddApiRoute(args);
859
990
  default:
860
991
  throw new types_js_1.McpError(types_js_1.ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
861
992
  }
@@ -872,7 +1003,7 @@ class CodeBakersServer {
872
1003
  method: 'POST',
873
1004
  headers: {
874
1005
  'Content-Type': 'application/json',
875
- Authorization: `Bearer ${this.apiKey}`,
1006
+ ...this.getAuthHeaders(),
876
1007
  },
877
1008
  body: JSON.stringify({
878
1009
  prompt: userRequest,
@@ -981,9 +1112,7 @@ Show the user what their simple request was expanded into, then proceed with the
981
1112
  async handleListPatterns() {
982
1113
  const response = await fetch(`${this.apiUrl}/api/patterns`, {
983
1114
  method: 'GET',
984
- headers: {
985
- Authorization: `Bearer ${this.apiKey}`,
986
- },
1115
+ headers: this.getAuthHeaders(),
987
1116
  });
988
1117
  if (!response.ok) {
989
1118
  const error = await response.json().catch(() => ({}));
@@ -1026,7 +1155,7 @@ Show the user what their simple request was expanded into, then proceed with the
1026
1155
  method: 'POST',
1027
1156
  headers: {
1028
1157
  'Content-Type': 'application/json',
1029
- Authorization: `Bearer ${this.apiKey}`,
1158
+ ...this.getAuthHeaders(),
1030
1159
  },
1031
1160
  body: JSON.stringify({ patterns }),
1032
1161
  });
@@ -1043,7 +1172,7 @@ Show the user what their simple request was expanded into, then proceed with the
1043
1172
  method: 'POST',
1044
1173
  headers: {
1045
1174
  'Content-Type': 'application/json',
1046
- Authorization: `Bearer ${this.apiKey}`,
1175
+ ...this.getAuthHeaders(),
1047
1176
  },
1048
1177
  body: JSON.stringify({ query }),
1049
1178
  });
@@ -1291,7 +1420,7 @@ Show the user what their simple request was expanded into, then proceed with the
1291
1420
  results.push('\n## Installing CodeBakers Patterns...\n');
1292
1421
  const response = await fetch(`${this.apiUrl}/api/content`, {
1293
1422
  method: 'GET',
1294
- headers: { Authorization: `Bearer ${this.apiKey}` },
1423
+ headers: this.getAuthHeaders(),
1295
1424
  });
1296
1425
  if (response.ok) {
1297
1426
  const content = await response.json();
@@ -1671,7 +1800,7 @@ Or if user declines, call without fullDeploy:
1671
1800
  try {
1672
1801
  const response = await fetch(`${this.apiUrl}/api/content`, {
1673
1802
  method: 'GET',
1674
- headers: { Authorization: `Bearer ${this.apiKey}` },
1803
+ headers: this.getAuthHeaders(),
1675
1804
  });
1676
1805
  if (!response.ok) {
1677
1806
  throw new Error('Failed to fetch patterns from API');
@@ -2678,7 +2807,7 @@ Just describe what you want to build! I'll automatically:
2678
2807
  method: 'POST',
2679
2808
  headers: {
2680
2809
  'Content-Type': 'application/json',
2681
- Authorization: `Bearer ${this.apiKey}`,
2810
+ ...this.getAuthHeaders(),
2682
2811
  },
2683
2812
  body: JSON.stringify({
2684
2813
  category,
@@ -2725,7 +2854,7 @@ Just describe what you want to build! I'll automatically:
2725
2854
  method: 'POST',
2726
2855
  headers: {
2727
2856
  'Content-Type': 'application/json',
2728
- Authorization: `Bearer ${this.apiKey}`,
2857
+ ...this.getAuthHeaders(),
2729
2858
  },
2730
2859
  body: JSON.stringify({
2731
2860
  eventType,
@@ -3148,6 +3277,1180 @@ Just describe what you want to build! I'll automatically:
3148
3277
  };
3149
3278
  }
3150
3279
  }
3280
+ /**
3281
+ * Handle update_constant tool - updates constants via natural language
3282
+ * Parses requests like "change Pro price to $59" and edits constants.ts
3283
+ */
3284
+ async handleUpdateConstant(args) {
3285
+ const { request } = args;
3286
+ const cwd = process.cwd();
3287
+ // Find constants.ts file (server project)
3288
+ const possiblePaths = [
3289
+ path.join(cwd, 'src', 'lib', 'constants.ts'),
3290
+ path.join(cwd, 'lib', 'constants.ts'),
3291
+ path.join(cwd, 'constants.ts'),
3292
+ ];
3293
+ let constantsPath = null;
3294
+ for (const p of possiblePaths) {
3295
+ if (fs.existsSync(p)) {
3296
+ constantsPath = p;
3297
+ break;
3298
+ }
3299
+ }
3300
+ if (!constantsPath) {
3301
+ return {
3302
+ content: [{
3303
+ type: 'text',
3304
+ text: `❌ Could not find constants.ts file.\n\nLooked in:\n${possiblePaths.map(p => `- ${p}`).join('\n')}\n\nMake sure you're in the right directory or create src/lib/constants.ts first.`,
3305
+ }],
3306
+ isError: true,
3307
+ };
3308
+ }
3309
+ // Read current constants file
3310
+ const currentContent = fs.readFileSync(constantsPath, 'utf-8');
3311
+ // Parse the natural language request to identify what to change
3312
+ const parsed = this.parseConstantRequest(request);
3313
+ if (!parsed.success) {
3314
+ return {
3315
+ content: [{
3316
+ type: 'text',
3317
+ text: `❓ I couldn't understand that request.\n\n**Request:** "${request}"\n\n**Supported changes:**\n- "change Pro monthly price to $59"\n- "set Team yearly price to $1200"\n- "update Agency seats to 10"\n- "set anonymous trial days to 10"\n- "change extended trial days to 14"\n- "update module count to 45"\n- "change support email to help@example.com"\n- "update tagline to 'Build faster'"\n\n**Try rephrasing** your request with the constant name and new value.`,
3318
+ }],
3319
+ };
3320
+ }
3321
+ // Apply the change
3322
+ const { constantPath, newValue, description } = parsed;
3323
+ const updatedContent = this.updateConstantValue(currentContent, constantPath, newValue);
3324
+ if (updatedContent === currentContent) {
3325
+ return {
3326
+ content: [{
3327
+ type: 'text',
3328
+ text: `⚠️ No changes made.\n\nCouldn't find or update: \`${constantPath}\`\n\nThe constant might not exist or the path format is different.`,
3329
+ }],
3330
+ };
3331
+ }
3332
+ // Write the updated file
3333
+ fs.writeFileSync(constantsPath, updatedContent, 'utf-8');
3334
+ return {
3335
+ content: [{
3336
+ type: 'text',
3337
+ text: `✅ **Constant Updated**\n\n**Change:** ${description}\n**File:** \`${path.relative(cwd, constantsPath)}\`\n\n---\n\n⚡ This change is now active across the entire codebase.\n\nAll components, pages, and APIs that reference this constant will automatically use the new value.`,
3338
+ }],
3339
+ };
3340
+ }
3341
+ /**
3342
+ * Parse natural language request to identify constant and new value
3343
+ */
3344
+ parseConstantRequest(request) {
3345
+ const lower = request.toLowerCase();
3346
+ // PRICING patterns
3347
+ // "change/set/update pro monthly price to $59" or "pro monthly to 59"
3348
+ const pricingMatch = lower.match(/(?:change|set|update|make)?\s*(pro|team|agency|enterprise)\s*(monthly|yearly)\s*(?:price|cost)?\s*(?:to|=|:)?\s*\$?(\d+)/i);
3349
+ if (pricingMatch) {
3350
+ const plan = pricingMatch[1].toUpperCase();
3351
+ const period = pricingMatch[2].toUpperCase();
3352
+ const value = parseInt(pricingMatch[3]);
3353
+ return {
3354
+ success: true,
3355
+ constantPath: `PRICING.${plan}.${period}`,
3356
+ newValue: value,
3357
+ description: `${plan} ${period.toLowerCase()} price → $${value}`,
3358
+ };
3359
+ }
3360
+ // SEATS patterns
3361
+ // "change/set team seats to 10" or "agency seats to unlimited"
3362
+ const seatsMatch = lower.match(/(?:change|set|update|make)?\s*(pro|team|agency|enterprise)\s*seats?\s*(?:to|=|:)?\s*(\d+|unlimited|-1)/i);
3363
+ if (seatsMatch) {
3364
+ const plan = seatsMatch[1].toUpperCase();
3365
+ let value;
3366
+ if (seatsMatch[2] === 'unlimited' || seatsMatch[2] === '-1') {
3367
+ value = -1;
3368
+ }
3369
+ else {
3370
+ value = parseInt(seatsMatch[2]);
3371
+ }
3372
+ return {
3373
+ success: true,
3374
+ constantPath: `PRICING.${plan}.SEATS`,
3375
+ newValue: value,
3376
+ description: `${plan} seats → ${value === -1 ? 'unlimited' : value}`,
3377
+ };
3378
+ }
3379
+ // TRIAL patterns
3380
+ // "set anonymous trial days to 10" or "anonymous days to 10"
3381
+ const trialMatch = lower.match(/(?:change|set|update|make)?\s*(anonymous|extended|total|expiring)\s*(?:trial)?\s*(?:days|threshold)?\s*(?:to|=|:)?\s*(\d+)/i);
3382
+ if (trialMatch) {
3383
+ const trialType = trialMatch[1].toUpperCase();
3384
+ const value = parseInt(trialMatch[2]);
3385
+ let path;
3386
+ let desc;
3387
+ switch (trialType) {
3388
+ case 'ANONYMOUS':
3389
+ path = 'TRIAL.ANONYMOUS_DAYS';
3390
+ desc = `Anonymous trial days → ${value}`;
3391
+ break;
3392
+ case 'EXTENDED':
3393
+ path = 'TRIAL.EXTENDED_DAYS';
3394
+ desc = `Extended trial days → ${value}`;
3395
+ break;
3396
+ case 'TOTAL':
3397
+ path = 'TRIAL.TOTAL_DAYS';
3398
+ desc = `Total trial days → ${value}`;
3399
+ break;
3400
+ case 'EXPIRING':
3401
+ path = 'TRIAL.EXPIRING_SOON_THRESHOLD';
3402
+ desc = `Expiring warning threshold → ${value} days`;
3403
+ break;
3404
+ default:
3405
+ return { success: false };
3406
+ }
3407
+ return { success: true, constantPath: path, newValue: value, description: desc };
3408
+ }
3409
+ // MODULE COUNT pattern
3410
+ // "set module count to 45" or "modules to 45"
3411
+ const moduleMatch = lower.match(/(?:change|set|update|make)?\s*(?:module|pattern)\s*count\s*(?:to|=|:)?\s*(\d+)/i);
3412
+ if (moduleMatch) {
3413
+ const value = parseInt(moduleMatch[1]);
3414
+ return {
3415
+ success: true,
3416
+ constantPath: 'MODULES.COUNT',
3417
+ newValue: value,
3418
+ description: `Module count → ${value}`,
3419
+ };
3420
+ }
3421
+ // PRODUCT string patterns
3422
+ // "change tagline to 'Build faster'" or "set support email to help@example.com"
3423
+ const productMatch = lower.match(/(?:change|set|update|make)?\s*(name|tagline|support\s*email|website|cli\s*command)\s*(?:to|=|:)?\s*["']?([^"']+)["']?/i);
3424
+ if (productMatch) {
3425
+ const field = productMatch[1].toLowerCase().replace(/\s+/g, '_').toUpperCase();
3426
+ const value = productMatch[2].trim();
3427
+ let path;
3428
+ if (field === 'SUPPORT_EMAIL') {
3429
+ path = 'PRODUCT.SUPPORT_EMAIL';
3430
+ }
3431
+ else if (field === 'NAME') {
3432
+ path = 'PRODUCT.NAME';
3433
+ }
3434
+ else if (field === 'TAGLINE') {
3435
+ path = 'PRODUCT.TAGLINE';
3436
+ }
3437
+ else if (field === 'WEBSITE') {
3438
+ path = 'PRODUCT.WEBSITE';
3439
+ }
3440
+ else if (field === 'CLI_COMMAND') {
3441
+ path = 'PRODUCT.CLI_COMMAND';
3442
+ }
3443
+ else {
3444
+ return { success: false };
3445
+ }
3446
+ return {
3447
+ success: true,
3448
+ constantPath: path,
3449
+ newValue: value,
3450
+ description: `${field.replace(/_/g, ' ').toLowerCase()} → "${value}"`,
3451
+ };
3452
+ }
3453
+ // API KEYS patterns
3454
+ // "set rate limit to 100 per minute"
3455
+ const rateLimitMatch = lower.match(/(?:change|set|update|make)?\s*(?:rate\s*limit|requests)\s*(?:per|\/)\s*(minute|hour)\s*(?:to|=|:)?\s*(\d+)/i);
3456
+ if (rateLimitMatch) {
3457
+ const period = rateLimitMatch[1].toUpperCase();
3458
+ const value = parseInt(rateLimitMatch[2]);
3459
+ const path = period === 'MINUTE'
3460
+ ? 'API_KEYS.RATE_LIMIT.REQUESTS_PER_MINUTE'
3461
+ : 'API_KEYS.RATE_LIMIT.REQUESTS_PER_HOUR';
3462
+ return {
3463
+ success: true,
3464
+ constantPath: path,
3465
+ newValue: value,
3466
+ description: `Rate limit → ${value} requests per ${period.toLowerCase()}`,
3467
+ };
3468
+ }
3469
+ // FEATURE FLAGS patterns
3470
+ // "enable/disable trial system" or "set trial system to true/false"
3471
+ const featureMatch = lower.match(/(?:enable|disable|turn\s*on|turn\s*off|set)?\s*(trial\s*system|github\s*extension|anonymous\s*trial)\s*(?:enabled|disabled|on|off|to\s*true|to\s*false)?/i);
3472
+ if (featureMatch) {
3473
+ const feature = featureMatch[1].toLowerCase().replace(/\s+/g, '_').toUpperCase();
3474
+ const enabled = lower.includes('enable') || lower.includes('turn on') || lower.includes('to true');
3475
+ let path;
3476
+ if (feature.includes('TRIAL_SYSTEM')) {
3477
+ path = 'FEATURES.TRIAL_SYSTEM_ENABLED';
3478
+ }
3479
+ else if (feature.includes('GITHUB_EXTENSION')) {
3480
+ path = 'FEATURES.GITHUB_EXTENSION_ENABLED';
3481
+ }
3482
+ else if (feature.includes('ANONYMOUS_TRIAL')) {
3483
+ path = 'FEATURES.ANONYMOUS_TRIAL_ENABLED';
3484
+ }
3485
+ else {
3486
+ return { success: false };
3487
+ }
3488
+ return {
3489
+ success: true,
3490
+ constantPath: path,
3491
+ newValue: enabled,
3492
+ description: `${feature.replace(/_/g, ' ').toLowerCase()} → ${enabled ? 'enabled' : 'disabled'}`,
3493
+ };
3494
+ }
3495
+ return { success: false };
3496
+ }
3497
+ /**
3498
+ * Update a specific constant value in the file content
3499
+ */
3500
+ updateConstantValue(content, constantPath, newValue) {
3501
+ const parts = constantPath.split('.');
3502
+ // Format the new value for TypeScript
3503
+ let formattedValue;
3504
+ if (typeof newValue === 'string') {
3505
+ formattedValue = `'${newValue}'`;
3506
+ }
3507
+ else if (typeof newValue === 'boolean') {
3508
+ formattedValue = newValue.toString();
3509
+ }
3510
+ else if (newValue === null) {
3511
+ formattedValue = 'null';
3512
+ }
3513
+ else {
3514
+ formattedValue = newValue.toString();
3515
+ }
3516
+ // Build regex to find and replace the value
3517
+ // e.g., for PRICING.PRO.MONTHLY, look for "MONTHLY: <number>" within PRO block
3518
+ if (parts.length === 2) {
3519
+ // Simple case: MODULES.COUNT or FEATURES.X
3520
+ const [obj, key] = parts;
3521
+ const regex = new RegExp(`(${key}:\\s*)([^,}\\n]+)`, 'g');
3522
+ // Find the right object and update within it
3523
+ const objRegex = new RegExp(`(export const ${obj}\\s*=\\s*\\{[\\s\\S]*?)(${key}:\\s*)([^,}\\n]+)([\\s\\S]*?\\}\\s*as\\s*const)`, 'g');
3524
+ return content.replace(objRegex, `$1$2${formattedValue}$4`);
3525
+ }
3526
+ else if (parts.length === 3) {
3527
+ // Nested case: PRICING.PRO.MONTHLY
3528
+ const [obj, subObj, key] = parts;
3529
+ // More complex regex to find nested value
3530
+ // Look for the pattern within the specific sub-object
3531
+ const objPattern = `export const ${obj}\\s*=\\s*\\{[\\s\\S]*?${subObj}:\\s*\\{[\\s\\S]*?${key}:\\s*`;
3532
+ const lines = content.split('\n');
3533
+ let inObject = false;
3534
+ let inSubObject = false;
3535
+ let braceDepth = 0;
3536
+ for (let i = 0; i < lines.length; i++) {
3537
+ const line = lines[i];
3538
+ // Track if we're in the right object
3539
+ if (line.includes(`export const ${obj}`)) {
3540
+ inObject = true;
3541
+ }
3542
+ if (inObject) {
3543
+ // Count braces
3544
+ braceDepth += (line.match(/\{/g) || []).length;
3545
+ braceDepth -= (line.match(/\}/g) || []).length;
3546
+ // Check if we're in the sub-object
3547
+ if (line.includes(`${subObj}:`)) {
3548
+ inSubObject = true;
3549
+ }
3550
+ if (inSubObject && line.includes(`${key}:`)) {
3551
+ // Found the line to update
3552
+ lines[i] = line.replace(new RegExp(`(${key}:\\s*)([^,}\\n]+)`), `$1${formattedValue}`);
3553
+ break;
3554
+ }
3555
+ // Exit sub-object when we see closing brace at right depth
3556
+ if (inSubObject && line.includes('}') && !line.includes('{')) {
3557
+ // Check if this closes the sub-object
3558
+ const match = line.match(/^\s*\}/);
3559
+ if (match) {
3560
+ inSubObject = false;
3561
+ }
3562
+ }
3563
+ if (braceDepth === 0) {
3564
+ inObject = false;
3565
+ }
3566
+ }
3567
+ }
3568
+ return lines.join('\n');
3569
+ }
3570
+ else if (parts.length === 4) {
3571
+ // Deeply nested: API_KEYS.RATE_LIMIT.REQUESTS_PER_MINUTE
3572
+ const [obj, subObj1, subObj2, key] = parts;
3573
+ const lines = content.split('\n');
3574
+ let inObject = false;
3575
+ let inSubObj1 = false;
3576
+ let inSubObj2 = false;
3577
+ for (let i = 0; i < lines.length; i++) {
3578
+ const line = lines[i];
3579
+ if (line.includes(`export const ${obj}`)) {
3580
+ inObject = true;
3581
+ }
3582
+ if (inObject && line.includes(`${subObj1}:`)) {
3583
+ inSubObj1 = true;
3584
+ }
3585
+ if (inSubObj1 && line.includes(`${subObj2}:`)) {
3586
+ inSubObj2 = true;
3587
+ }
3588
+ if (inSubObj2 && line.includes(`${key}:`)) {
3589
+ lines[i] = line.replace(new RegExp(`(${key}:\\s*)([^,}\\n]+)`), `$1${formattedValue}`);
3590
+ break;
3591
+ }
3592
+ // Reset on closing braces (simplified - could be more robust)
3593
+ if (inSubObj2 && line.trim() === '},') {
3594
+ inSubObj2 = false;
3595
+ }
3596
+ if (inSubObj1 && line.trim() === '},') {
3597
+ inSubObj1 = false;
3598
+ }
3599
+ }
3600
+ return lines.join('\n');
3601
+ }
3602
+ return content;
3603
+ }
3604
+ /**
3605
+ * Handle update_schema tool - add/modify database tables via natural language
3606
+ */
3607
+ async handleUpdateSchema(args) {
3608
+ const { request } = args;
3609
+ const cwd = process.cwd();
3610
+ // Find schema file
3611
+ const possiblePaths = [
3612
+ path.join(cwd, 'src', 'db', 'schema.ts'),
3613
+ path.join(cwd, 'db', 'schema.ts'),
3614
+ path.join(cwd, 'schema.ts'),
3615
+ path.join(cwd, 'drizzle', 'schema.ts'),
3616
+ ];
3617
+ let schemaPath = null;
3618
+ for (const p of possiblePaths) {
3619
+ if (fs.existsSync(p)) {
3620
+ schemaPath = p;
3621
+ break;
3622
+ }
3623
+ }
3624
+ if (!schemaPath) {
3625
+ return {
3626
+ content: [{
3627
+ type: 'text',
3628
+ text: `❌ Could not find schema.ts file.\n\nLooked in:\n${possiblePaths.map(p => `- ${p}`).join('\n')}\n\nMake sure you have a Drizzle schema file or create one at src/db/schema.ts`,
3629
+ }],
3630
+ isError: true,
3631
+ };
3632
+ }
3633
+ // Parse the request to understand what to create
3634
+ const parsed = this.parseSchemaRequest(request);
3635
+ if (!parsed.success) {
3636
+ return {
3637
+ content: [{
3638
+ type: 'text',
3639
+ text: `❓ I couldn't understand that schema request.\n\n**Request:** "${request}"\n\n**Supported changes:**\n- "add a tags table with name and color fields"\n- "create a comments table with userId, postId, and content"\n- "add isArchived boolean to projects table"\n- "add createdAt timestamp to users"\n\n**Try rephrasing** with the table name and fields.`,
3640
+ }],
3641
+ };
3642
+ }
3643
+ // Read current schema
3644
+ const currentSchema = fs.readFileSync(schemaPath, 'utf-8');
3645
+ // Generate new schema code
3646
+ const { tableName, fields, action, description } = parsed;
3647
+ let newCode;
3648
+ let updatedSchema;
3649
+ if (action === 'create_table') {
3650
+ newCode = this.generateTableSchema(tableName, fields);
3651
+ // Add to end of file before any exports
3652
+ const exportIndex = currentSchema.lastIndexOf('export {');
3653
+ if (exportIndex > 0) {
3654
+ updatedSchema = currentSchema.slice(0, exportIndex) + newCode + '\n\n' + currentSchema.slice(exportIndex);
3655
+ }
3656
+ else {
3657
+ updatedSchema = currentSchema + '\n\n' + newCode;
3658
+ }
3659
+ }
3660
+ else if (action === 'add_field') {
3661
+ // Find the table and add field
3662
+ const tableRegex = new RegExp(`export const ${tableName} = pgTable\\([^)]+\\{([\\s\\S]*?)\\}\\s*\\)`, 'g');
3663
+ const match = tableRegex.exec(currentSchema);
3664
+ if (!match) {
3665
+ return {
3666
+ content: [{
3667
+ type: 'text',
3668
+ text: `❌ Could not find table "${tableName}" in schema.\n\nMake sure the table exists before adding fields.`,
3669
+ }],
3670
+ };
3671
+ }
3672
+ const fieldCode = this.generateFieldCode(fields[0]);
3673
+ // Insert before the closing brace
3674
+ const insertPos = match.index + match[0].lastIndexOf('}');
3675
+ updatedSchema = currentSchema.slice(0, insertPos) + fieldCode + ',\n ' + currentSchema.slice(insertPos);
3676
+ }
3677
+ else {
3678
+ return {
3679
+ content: [{
3680
+ type: 'text',
3681
+ text: `❌ Unknown schema action: ${action}`,
3682
+ }],
3683
+ };
3684
+ }
3685
+ // Write updated schema
3686
+ fs.writeFileSync(schemaPath, updatedSchema, 'utf-8');
3687
+ // Generate migration reminder
3688
+ const migrationHint = `\n\n📝 **Next steps:**\n1. Run \`npx drizzle-kit generate\` to create migration\n2. Run \`npx drizzle-kit migrate\` to apply it`;
3689
+ return {
3690
+ content: [{
3691
+ type: 'text',
3692
+ text: `✅ **Schema Updated**\n\n**Change:** ${description}\n**File:** \`${path.relative(cwd, schemaPath)}\`${migrationHint}`,
3693
+ }],
3694
+ };
3695
+ }
3696
+ parseSchemaRequest(request) {
3697
+ const lower = request.toLowerCase();
3698
+ // Create table pattern: "add/create a X table with Y, Z fields"
3699
+ const createMatch = lower.match(/(?:add|create)\s+(?:a\s+)?(\w+)\s+table\s+(?:with\s+)?(.+)/i);
3700
+ if (createMatch) {
3701
+ const tableName = createMatch[1];
3702
+ const fieldsStr = createMatch[2];
3703
+ const fields = this.parseFields(fieldsStr);
3704
+ return {
3705
+ success: true,
3706
+ action: 'create_table',
3707
+ tableName,
3708
+ fields,
3709
+ description: `Created "${tableName}" table with ${fields.length} fields`,
3710
+ };
3711
+ }
3712
+ // Add field pattern: "add X to Y table"
3713
+ const addFieldMatch = lower.match(/add\s+(\w+)\s+(?:field|column)?\s*(?:to\s+)?(\w+)\s+table/i);
3714
+ if (addFieldMatch) {
3715
+ const fieldName = addFieldMatch[1];
3716
+ const tableName = addFieldMatch[2];
3717
+ // Detect type from name
3718
+ let fieldType = 'text';
3719
+ if (fieldName.includes('id') || fieldName.includes('Id'))
3720
+ fieldType = 'uuid';
3721
+ else if (fieldName.includes('at') || fieldName.includes('At') || fieldName.includes('date'))
3722
+ fieldType = 'timestamp';
3723
+ else if (fieldName.includes('is') || fieldName.includes('Is') || fieldName.includes('has') || fieldName.includes('Has'))
3724
+ fieldType = 'boolean';
3725
+ else if (fieldName.includes('count') || fieldName.includes('Count') || fieldName.includes('num') || fieldName.includes('Num'))
3726
+ fieldType = 'integer';
3727
+ return {
3728
+ success: true,
3729
+ action: 'add_field',
3730
+ tableName,
3731
+ fields: [{ name: fieldName, type: fieldType }],
3732
+ description: `Added "${fieldName}" field to "${tableName}" table`,
3733
+ };
3734
+ }
3735
+ // Add typed field: "add isArchived boolean to projects"
3736
+ const typedFieldMatch = lower.match(/add\s+(\w+)\s+(boolean|text|integer|timestamp|uuid)\s+(?:to\s+)?(\w+)/i);
3737
+ if (typedFieldMatch) {
3738
+ return {
3739
+ success: true,
3740
+ action: 'add_field',
3741
+ tableName: typedFieldMatch[3],
3742
+ fields: [{ name: typedFieldMatch[1], type: typedFieldMatch[2] }],
3743
+ description: `Added "${typedFieldMatch[1]}" (${typedFieldMatch[2]}) to "${typedFieldMatch[3]}" table`,
3744
+ };
3745
+ }
3746
+ return { success: false };
3747
+ }
3748
+ parseFields(fieldsStr) {
3749
+ const fields = [];
3750
+ // Split by "and" or ","
3751
+ const parts = fieldsStr.split(/,|\band\b/).map(s => s.trim()).filter(Boolean);
3752
+ for (const part of parts) {
3753
+ const words = part.split(/\s+/);
3754
+ let name = words[0].replace(/[^a-zA-Z0-9_]/g, '');
3755
+ // Skip common filler words
3756
+ if (['a', 'an', 'the', 'field', 'fields', 'column', 'columns'].includes(name)) {
3757
+ name = words[1]?.replace(/[^a-zA-Z0-9_]/g, '') || '';
3758
+ }
3759
+ if (!name)
3760
+ continue;
3761
+ // Detect type from name or explicit type
3762
+ let type = 'text';
3763
+ const fullPart = part.toLowerCase();
3764
+ if (fullPart.includes('boolean') || fullPart.includes('bool'))
3765
+ type = 'boolean';
3766
+ else if (fullPart.includes('integer') || fullPart.includes('int') || fullPart.includes('number'))
3767
+ type = 'integer';
3768
+ else if (fullPart.includes('timestamp') || fullPart.includes('datetime') || fullPart.includes('date'))
3769
+ type = 'timestamp';
3770
+ else if (fullPart.includes('uuid'))
3771
+ type = 'uuid';
3772
+ else if (name.endsWith('Id') || name.endsWith('_id'))
3773
+ type = 'uuid';
3774
+ else if (name.startsWith('is') || name.startsWith('has') || name.startsWith('can'))
3775
+ type = 'boolean';
3776
+ else if (name.endsWith('At') || name.endsWith('_at') || name.includes('date') || name.includes('Date'))
3777
+ type = 'timestamp';
3778
+ else if (name.includes('count') || name.includes('Count') || name.includes('num') || name.includes('Num'))
3779
+ type = 'integer';
3780
+ fields.push({ name, type });
3781
+ }
3782
+ // Add default fields if creating a new table
3783
+ const hasId = fields.some(f => f.name === 'id');
3784
+ const hasCreatedAt = fields.some(f => f.name === 'createdAt' || f.name === 'created_at');
3785
+ if (!hasId) {
3786
+ fields.unshift({ name: 'id', type: 'uuid' });
3787
+ }
3788
+ if (!hasCreatedAt) {
3789
+ fields.push({ name: 'createdAt', type: 'timestamp' });
3790
+ }
3791
+ return fields;
3792
+ }
3793
+ generateTableSchema(tableName, fields) {
3794
+ const fieldLines = fields.map(f => this.generateFieldCode(f)).join(',\n ');
3795
+ return `export const ${tableName} = pgTable('${this.toSnakeCase(tableName)}', {
3796
+ ${fieldLines},
3797
+ });`;
3798
+ }
3799
+ generateFieldCode(field) {
3800
+ const { name, type, nullable } = field;
3801
+ const snakeName = this.toSnakeCase(name);
3802
+ let code = '';
3803
+ switch (type) {
3804
+ case 'uuid':
3805
+ if (name === 'id') {
3806
+ code = `${name}: uuid('${snakeName}').defaultRandom().primaryKey()`;
3807
+ }
3808
+ else {
3809
+ code = `${name}: uuid('${snakeName}')`;
3810
+ }
3811
+ break;
3812
+ case 'text':
3813
+ code = `${name}: text('${snakeName}')`;
3814
+ break;
3815
+ case 'boolean':
3816
+ code = `${name}: boolean('${snakeName}').default(false)`;
3817
+ break;
3818
+ case 'integer':
3819
+ code = `${name}: integer('${snakeName}')`;
3820
+ break;
3821
+ case 'timestamp':
3822
+ if (name === 'createdAt' || name === 'created_at') {
3823
+ code = `${name}: timestamp('${snakeName}').defaultNow()`;
3824
+ }
3825
+ else if (name === 'updatedAt' || name === 'updated_at') {
3826
+ code = `${name}: timestamp('${snakeName}').defaultNow()`;
3827
+ }
3828
+ else {
3829
+ code = `${name}: timestamp('${snakeName}')`;
3830
+ }
3831
+ break;
3832
+ default:
3833
+ code = `${name}: text('${snakeName}')`;
3834
+ }
3835
+ if (nullable && !code.includes('.default')) {
3836
+ // Fields are nullable by default in Drizzle unless .notNull() is added
3837
+ }
3838
+ return code;
3839
+ }
3840
+ toSnakeCase(str) {
3841
+ return str.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '');
3842
+ }
3843
+ /**
3844
+ * Handle update_env tool - add/update environment variables
3845
+ */
3846
+ async handleUpdateEnv(args) {
3847
+ const { request } = args;
3848
+ const cwd = process.cwd();
3849
+ const parsed = this.parseEnvRequest(request);
3850
+ if (!parsed.success || !parsed.variables || parsed.variables.length === 0) {
3851
+ return {
3852
+ content: [{
3853
+ type: 'text',
3854
+ text: `❓ I couldn't understand that env request.\n\n**Request:** "${request}"\n\n**Supported:**\n- "add OPENAI_API_KEY"\n- "add Stripe keys" (adds STRIPE_SECRET_KEY and STRIPE_PUBLISHABLE_KEY)\n- "add RESEND_API_KEY for emails"\n- "add DATABASE_URL"\n\n**Try rephrasing** with the variable name(s).`,
3855
+ }],
3856
+ };
3857
+ }
3858
+ const results = [];
3859
+ const { variables } = parsed;
3860
+ // Update .env.local
3861
+ const envLocalPath = path.join(cwd, '.env.local');
3862
+ let envLocalContent = fs.existsSync(envLocalPath) ? fs.readFileSync(envLocalPath, 'utf-8') : '';
3863
+ for (const v of variables) {
3864
+ if (!envLocalContent.includes(`${v.name}=`)) {
3865
+ envLocalContent += `\n${v.name}=${v.placeholder || ''}`;
3866
+ results.push(`✓ Added ${v.name} to .env.local`);
3867
+ }
3868
+ else {
3869
+ results.push(`⚠️ ${v.name} already exists in .env.local`);
3870
+ }
3871
+ }
3872
+ fs.writeFileSync(envLocalPath, envLocalContent.trim() + '\n', 'utf-8');
3873
+ // Update .env.example
3874
+ const envExamplePath = path.join(cwd, '.env.example');
3875
+ let envExampleContent = fs.existsSync(envExamplePath) ? fs.readFileSync(envExamplePath, 'utf-8') : '';
3876
+ for (const v of variables) {
3877
+ if (!envExampleContent.includes(`${v.name}=`)) {
3878
+ const comment = v.comment ? `# ${v.comment}\n` : '';
3879
+ envExampleContent += `\n${comment}${v.name}=`;
3880
+ results.push(`✓ Added ${v.name} to .env.example`);
3881
+ }
3882
+ }
3883
+ fs.writeFileSync(envExamplePath, envExampleContent.trim() + '\n', 'utf-8');
3884
+ return {
3885
+ content: [{
3886
+ type: 'text',
3887
+ text: `✅ **Environment Variables Updated**\n\n${results.join('\n')}\n\n📝 **Don't forget** to add the actual values to .env.local`,
3888
+ }],
3889
+ };
3890
+ }
3891
+ parseEnvRequest(request) {
3892
+ const lower = request.toLowerCase();
3893
+ const variables = [];
3894
+ // Known service patterns
3895
+ if (lower.includes('stripe')) {
3896
+ variables.push({ name: 'STRIPE_SECRET_KEY', placeholder: 'sk_test_...', comment: 'Stripe secret key' }, { name: 'STRIPE_PUBLISHABLE_KEY', placeholder: 'pk_test_...', comment: 'Stripe publishable key' }, { name: 'STRIPE_WEBHOOK_SECRET', placeholder: 'whsec_...', comment: 'Stripe webhook secret' });
3897
+ }
3898
+ if (lower.includes('openai')) {
3899
+ variables.push({ name: 'OPENAI_API_KEY', placeholder: 'sk-...', comment: 'OpenAI API key' });
3900
+ }
3901
+ if (lower.includes('anthropic') || lower.includes('claude')) {
3902
+ variables.push({ name: 'ANTHROPIC_API_KEY', placeholder: 'sk-ant-...', comment: 'Anthropic API key' });
3903
+ }
3904
+ if (lower.includes('resend') || lower.includes('email')) {
3905
+ variables.push({ name: 'RESEND_API_KEY', placeholder: 're_...', comment: 'Resend API key for emails' });
3906
+ }
3907
+ if (lower.includes('supabase')) {
3908
+ variables.push({ name: 'NEXT_PUBLIC_SUPABASE_URL', placeholder: 'https://xxx.supabase.co', comment: 'Supabase project URL' }, { name: 'NEXT_PUBLIC_SUPABASE_ANON_KEY', placeholder: 'eyJ...', comment: 'Supabase anon key' }, { name: 'SUPABASE_SERVICE_ROLE_KEY', placeholder: 'eyJ...', comment: 'Supabase service role key (server only)' });
3909
+ }
3910
+ if (lower.includes('database') || lower.includes('postgres') || lower.includes('db')) {
3911
+ variables.push({ name: 'DATABASE_URL', placeholder: 'postgresql://...', comment: 'PostgreSQL connection string' });
3912
+ }
3913
+ if (lower.includes('github')) {
3914
+ variables.push({ name: 'GITHUB_CLIENT_ID', placeholder: '', comment: 'GitHub OAuth client ID' }, { name: 'GITHUB_CLIENT_SECRET', placeholder: '', comment: 'GitHub OAuth client secret' });
3915
+ }
3916
+ if (lower.includes('vercel')) {
3917
+ variables.push({ name: 'VERCEL_TOKEN', placeholder: '', comment: 'Vercel API token' });
3918
+ }
3919
+ // Extract explicit variable names from request
3920
+ const explicitMatch = request.match(/\b([A-Z][A-Z0-9_]{2,})\b/g);
3921
+ if (explicitMatch) {
3922
+ for (const name of explicitMatch) {
3923
+ if (!variables.some(v => v.name === name)) {
3924
+ variables.push({ name, placeholder: '', comment: `Added via CLI` });
3925
+ }
3926
+ }
3927
+ }
3928
+ return {
3929
+ success: variables.length > 0,
3930
+ variables,
3931
+ };
3932
+ }
3933
+ /**
3934
+ * Handle billing_action tool - subscription and billing management
3935
+ */
3936
+ async handleBillingAction(args) {
3937
+ const { action } = args;
3938
+ const lower = action.toLowerCase();
3939
+ // Check current auth status
3940
+ const trialState = (0, config_js_1.getTrialState)();
3941
+ const hasApiKey = !!this.apiKey;
3942
+ // Show subscription status
3943
+ if (lower.includes('show') || lower.includes('status') || lower.includes('check') || lower.includes('my')) {
3944
+ let status = '';
3945
+ if (hasApiKey) {
3946
+ // Fetch subscription info from API
3947
+ try {
3948
+ const response = await fetch(`${this.apiUrl}/api/subscription/status`, {
3949
+ headers: this.getAuthHeaders(),
3950
+ });
3951
+ if (response.ok) {
3952
+ const data = await response.json();
3953
+ status = `# 💳 Subscription Status\n\n`;
3954
+ status += `**Plan:** ${data.plan || 'Unknown'}\n`;
3955
+ status += `**Status:** ${data.status || 'Unknown'}\n`;
3956
+ if (data.seats)
3957
+ status += `**Seats:** ${data.usedSeats}/${data.seats}\n`;
3958
+ if (data.renewsAt)
3959
+ status += `**Renews:** ${new Date(data.renewsAt).toLocaleDateString()}\n`;
3960
+ }
3961
+ else {
3962
+ status = `# 💳 Subscription Status\n\n**Status:** Active (API key configured)\n\nVisit https://codebakers.ai/billing for details.`;
3963
+ }
3964
+ }
3965
+ catch {
3966
+ status = `# 💳 Subscription Status\n\n**Status:** Active (API key configured)\n\nVisit https://codebakers.ai/billing for details.`;
3967
+ }
3968
+ }
3969
+ else if (trialState) {
3970
+ const daysRemaining = (0, config_js_1.getTrialDaysRemaining)();
3971
+ const isExpired = (0, config_js_1.isTrialExpired)();
3972
+ status = `# 🎁 Trial Status\n\n`;
3973
+ status += `**Stage:** ${trialState.stage}\n`;
3974
+ status += `**Days Remaining:** ${isExpired ? '0 (expired)' : daysRemaining}\n`;
3975
+ if (trialState.stage === 'anonymous' && !isExpired) {
3976
+ status += `\n💡 **Extend your trial:** Run \`codebakers extend\` to connect GitHub and get 7 more days free.`;
3977
+ }
3978
+ else if (isExpired) {
3979
+ status += `\n⚠️ **Trial expired.** Run \`codebakers billing\` to upgrade.`;
3980
+ }
3981
+ }
3982
+ else {
3983
+ status = `# ❓ No Subscription\n\nRun \`codebakers go\` to start a free trial, or \`codebakers setup\` if you have an account.`;
3984
+ }
3985
+ return {
3986
+ content: [{
3987
+ type: 'text',
3988
+ text: status,
3989
+ }],
3990
+ };
3991
+ }
3992
+ // Extend trial
3993
+ if (lower.includes('extend')) {
3994
+ if (!trialState) {
3995
+ return {
3996
+ content: [{
3997
+ type: 'text',
3998
+ text: `❌ No active trial to extend.\n\nRun \`codebakers go\` to start a free trial first.`,
3999
+ }],
4000
+ };
4001
+ }
4002
+ if (trialState.stage === 'extended') {
4003
+ return {
4004
+ content: [{
4005
+ type: 'text',
4006
+ text: `⚠️ Trial already extended.\n\nYou've already connected GitHub. Run \`codebakers billing\` to upgrade to a paid plan.`,
4007
+ }],
4008
+ };
4009
+ }
4010
+ return {
4011
+ content: [{
4012
+ type: 'text',
4013
+ text: `# 🚀 Extend Your Trial\n\nRun this command to connect GitHub and get 7 more days free:\n\n\`\`\`\ncodebakers extend\n\`\`\`\n\nOr visit: https://codebakers.ai/api/auth/github?extend=true`,
4014
+ }],
4015
+ };
4016
+ }
4017
+ // Upgrade
4018
+ if (lower.includes('upgrade') || lower.includes('pro') || lower.includes('team') || lower.includes('agency')) {
4019
+ let plan = 'pro';
4020
+ if (lower.includes('team'))
4021
+ plan = 'team';
4022
+ if (lower.includes('agency'))
4023
+ plan = 'agency';
4024
+ return {
4025
+ content: [{
4026
+ type: 'text',
4027
+ text: `# 💎 Upgrade to ${plan.charAt(0).toUpperCase() + plan.slice(1)}\n\nRun this command to open billing:\n\n\`\`\`\ncodebakers billing\n\`\`\`\n\nOr visit: https://codebakers.ai/billing?plan=${plan}`,
4028
+ }],
4029
+ };
4030
+ }
4031
+ // Default: show help
4032
+ return {
4033
+ content: [{
4034
+ type: 'text',
4035
+ text: `# 💳 Billing Actions\n\n**Available commands:**\n- "show my subscription" - View current status\n- "extend trial" - Get 7 more days with GitHub\n- "upgrade to Pro" - Open upgrade page\n- "upgrade to Team" - Open Team plan page\n\nOr run \`codebakers billing\` to open the billing page.`,
4036
+ }],
4037
+ };
4038
+ }
4039
+ /**
4040
+ * Handle add_page tool - create new Next.js pages
4041
+ */
4042
+ async handleAddPage(args) {
4043
+ const { request } = args;
4044
+ const cwd = process.cwd();
4045
+ const parsed = this.parsePageRequest(request);
4046
+ if (!parsed.success) {
4047
+ return {
4048
+ content: [{
4049
+ type: 'text',
4050
+ text: `❓ I couldn't understand that page request.\n\n**Request:** "${request}"\n\n**Supported:**\n- "create a settings page"\n- "add an about page"\n- "make a user profile page"\n- "create a dashboard page with stats"\n\n**Try rephrasing** with the page name and optional features.`,
4051
+ }],
4052
+ };
4053
+ }
4054
+ const { pageName, route, features, isProtected } = parsed;
4055
+ // Determine the correct app directory
4056
+ const appDir = path.join(cwd, 'src', 'app');
4057
+ if (!fs.existsSync(appDir)) {
4058
+ return {
4059
+ content: [{
4060
+ type: 'text',
4061
+ text: `❌ Could not find src/app directory.\n\nMake sure you're in a Next.js project root.`,
4062
+ }],
4063
+ isError: true,
4064
+ };
4065
+ }
4066
+ // Determine route group
4067
+ let targetDir;
4068
+ if (isProtected) {
4069
+ targetDir = path.join(appDir, '(dashboard)', route);
4070
+ }
4071
+ else {
4072
+ targetDir = path.join(appDir, '(marketing)', route);
4073
+ }
4074
+ // Create directory if needed
4075
+ if (!fs.existsSync(targetDir)) {
4076
+ fs.mkdirSync(targetDir, { recursive: true });
4077
+ }
4078
+ // Generate page content
4079
+ const pageContent = this.generatePageContent(pageName, features || [], isProtected || false);
4080
+ // Write page file
4081
+ const pagePath = path.join(targetDir, 'page.tsx');
4082
+ fs.writeFileSync(pagePath, pageContent, 'utf-8');
4083
+ return {
4084
+ content: [{
4085
+ type: 'text',
4086
+ text: `✅ **Page Created**\n\n**Name:** ${pageName}\n**Route:** /${route}\n**File:** \`${path.relative(cwd, pagePath)}\`\n**Protected:** ${isProtected ? 'Yes (requires auth)' : 'No (public)'}\n\n📝 **Next steps:**\n1. Customize the page content\n2. Add to navigation if needed`,
4087
+ }],
4088
+ };
4089
+ }
4090
+ parsePageRequest(request) {
4091
+ const lower = request.toLowerCase();
4092
+ // Extract page name
4093
+ const pageMatch = lower.match(/(?:create|add|make)\s+(?:a\s+)?(\w+)\s+page/i);
4094
+ if (!pageMatch)
4095
+ return { success: false };
4096
+ const pageName = pageMatch[1];
4097
+ const route = pageName.toLowerCase();
4098
+ // Detect features
4099
+ const features = [];
4100
+ if (lower.includes('tab'))
4101
+ features.push('tabs');
4102
+ if (lower.includes('form'))
4103
+ features.push('form');
4104
+ if (lower.includes('stat'))
4105
+ features.push('stats');
4106
+ if (lower.includes('table') || lower.includes('list'))
4107
+ features.push('table');
4108
+ if (lower.includes('card'))
4109
+ features.push('cards');
4110
+ // Detect if protected
4111
+ const protectedKeywords = ['dashboard', 'settings', 'profile', 'account', 'admin', 'billing'];
4112
+ const isProtected = protectedKeywords.some(k => lower.includes(k));
4113
+ return {
4114
+ success: true,
4115
+ pageName: pageName.charAt(0).toUpperCase() + pageName.slice(1),
4116
+ route,
4117
+ features,
4118
+ isProtected,
4119
+ };
4120
+ }
4121
+ generatePageContent(pageName, features, isProtected) {
4122
+ const imports = [];
4123
+ const components = [];
4124
+ if (isProtected) {
4125
+ imports.push(`import { redirect } from 'next/navigation';`);
4126
+ imports.push(`import { getServerSession } from 'next-auth';`);
4127
+ }
4128
+ if (features.includes('tabs')) {
4129
+ imports.push(`import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';`);
4130
+ }
4131
+ if (features.includes('cards') || features.includes('stats')) {
4132
+ imports.push(`import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';`);
4133
+ }
4134
+ // Generate component based on features
4135
+ let content = '';
4136
+ if (features.includes('stats')) {
4137
+ content = `
4138
+ <div className="grid gap-4 md:grid-cols-3">
4139
+ <Card>
4140
+ <CardHeader>
4141
+ <CardTitle>Total Users</CardTitle>
4142
+ </CardHeader>
4143
+ <CardContent>
4144
+ <p className="text-2xl font-bold">1,234</p>
4145
+ </CardContent>
4146
+ </Card>
4147
+ <Card>
4148
+ <CardHeader>
4149
+ <CardTitle>Revenue</CardTitle>
4150
+ </CardHeader>
4151
+ <CardContent>
4152
+ <p className="text-2xl font-bold">$12,345</p>
4153
+ </CardContent>
4154
+ </Card>
4155
+ <Card>
4156
+ <CardHeader>
4157
+ <CardTitle>Active Projects</CardTitle>
4158
+ </CardHeader>
4159
+ <CardContent>
4160
+ <p className="text-2xl font-bold">42</p>
4161
+ </CardContent>
4162
+ </Card>
4163
+ </div>`;
4164
+ }
4165
+ else if (features.includes('tabs')) {
4166
+ content = `
4167
+ <Tabs defaultValue="general" className="w-full">
4168
+ <TabsList>
4169
+ <TabsTrigger value="general">General</TabsTrigger>
4170
+ <TabsTrigger value="notifications">Notifications</TabsTrigger>
4171
+ <TabsTrigger value="security">Security</TabsTrigger>
4172
+ </TabsList>
4173
+ <TabsContent value="general">
4174
+ <Card>
4175
+ <CardHeader>
4176
+ <CardTitle>General Settings</CardTitle>
4177
+ <CardDescription>Manage your general preferences</CardDescription>
4178
+ </CardHeader>
4179
+ <CardContent>
4180
+ {/* Add form fields here */}
4181
+ </CardContent>
4182
+ </Card>
4183
+ </TabsContent>
4184
+ <TabsContent value="notifications">
4185
+ <Card>
4186
+ <CardHeader>
4187
+ <CardTitle>Notification Settings</CardTitle>
4188
+ </CardHeader>
4189
+ <CardContent>
4190
+ {/* Add notification settings here */}
4191
+ </CardContent>
4192
+ </Card>
4193
+ </TabsContent>
4194
+ <TabsContent value="security">
4195
+ <Card>
4196
+ <CardHeader>
4197
+ <CardTitle>Security Settings</CardTitle>
4198
+ </CardHeader>
4199
+ <CardContent>
4200
+ {/* Add security settings here */}
4201
+ </CardContent>
4202
+ </Card>
4203
+ </TabsContent>
4204
+ </Tabs>`;
4205
+ }
4206
+ else {
4207
+ content = `
4208
+ <p className="text-muted-foreground">
4209
+ Welcome to the ${pageName} page. Add your content here.
4210
+ </p>`;
4211
+ }
4212
+ const asyncKeyword = isProtected ? 'async ' : '';
4213
+ const authCheck = isProtected ? `
4214
+ const session = await getServerSession();
4215
+ if (!session) {
4216
+ redirect('/login');
4217
+ }
4218
+ ` : '';
4219
+ return `${imports.join('\n')}
4220
+
4221
+ export const metadata = {
4222
+ title: '${pageName}',
4223
+ };
4224
+
4225
+ export default ${asyncKeyword}function ${pageName}Page() {${authCheck}
4226
+ return (
4227
+ <div className="container mx-auto py-8">
4228
+ <h1 className="text-3xl font-bold mb-8">${pageName}</h1>
4229
+ ${content}
4230
+ </div>
4231
+ );
4232
+ }
4233
+ `;
4234
+ }
4235
+ /**
4236
+ * Handle add_api_route tool - create new API endpoints
4237
+ */
4238
+ async handleAddApiRoute(args) {
4239
+ const { request } = args;
4240
+ const cwd = process.cwd();
4241
+ const parsed = this.parseApiRouteRequest(request);
4242
+ if (!parsed.success) {
4243
+ return {
4244
+ content: [{
4245
+ type: 'text',
4246
+ text: `❓ I couldn't understand that API route request.\n\n**Request:** "${request}"\n\n**Supported:**\n- "create a feedback endpoint"\n- "add POST endpoint for user settings"\n- "make a webhook for Stripe"\n- "create GET/POST for preferences"\n\n**Try rephrasing** with the endpoint name and HTTP methods.`,
4247
+ }],
4248
+ };
4249
+ }
4250
+ const { routeName, routePath, methods, isWebhook, requiresAuth } = parsed;
4251
+ // Determine the API directory
4252
+ const apiDir = path.join(cwd, 'src', 'app', 'api', routePath);
4253
+ // Create directory if needed
4254
+ if (!fs.existsSync(apiDir)) {
4255
+ fs.mkdirSync(apiDir, { recursive: true });
4256
+ }
4257
+ // Generate route content
4258
+ const routeContent = this.generateApiRouteContent(routeName, methods, isWebhook, requiresAuth);
4259
+ // Write route file
4260
+ const routeFilePath = path.join(apiDir, 'route.ts');
4261
+ fs.writeFileSync(routeFilePath, routeContent, 'utf-8');
4262
+ return {
4263
+ content: [{
4264
+ type: 'text',
4265
+ text: `✅ **API Route Created**\n\n**Name:** ${routeName}\n**Path:** /api/${routePath}\n**Methods:** ${methods.join(', ')}\n**File:** \`${path.relative(cwd, routeFilePath)}\`\n**Auth Required:** ${requiresAuth ? 'Yes' : 'No'}\n\n📝 **Next steps:**\n1. Implement the business logic\n2. Add validation with Zod\n3. Test the endpoint`,
4266
+ }],
4267
+ };
4268
+ }
4269
+ parseApiRouteRequest(request) {
4270
+ const lower = request.toLowerCase();
4271
+ // Detect webhook
4272
+ const isWebhook = lower.includes('webhook');
4273
+ // Extract route name
4274
+ let routeName = '';
4275
+ let routePath = '';
4276
+ // Pattern: "create a X endpoint" or "add X route"
4277
+ const nameMatch = lower.match(/(?:create|add|make)\s+(?:a\s+)?(?:post\s+|get\s+)?(?:endpoint|route|api)?\s*(?:for\s+)?(\w+)/i);
4278
+ if (nameMatch) {
4279
+ routeName = nameMatch[1];
4280
+ routePath = routeName.toLowerCase();
4281
+ }
4282
+ else {
4283
+ return { success: false };
4284
+ }
4285
+ // Detect methods
4286
+ const methods = [];
4287
+ if (lower.includes('get'))
4288
+ methods.push('GET');
4289
+ if (lower.includes('post'))
4290
+ methods.push('POST');
4291
+ if (lower.includes('put'))
4292
+ methods.push('PUT');
4293
+ if (lower.includes('patch'))
4294
+ methods.push('PATCH');
4295
+ if (lower.includes('delete'))
4296
+ methods.push('DELETE');
4297
+ // Default to POST for most endpoints, GET for queries
4298
+ if (methods.length === 0) {
4299
+ if (lower.includes('list') || lower.includes('fetch') || lower.includes('query')) {
4300
+ methods.push('GET');
4301
+ }
4302
+ else {
4303
+ methods.push('POST');
4304
+ }
4305
+ }
4306
+ // Detect if auth required
4307
+ const noAuthKeywords = ['webhook', 'public', 'open'];
4308
+ const requiresAuth = !noAuthKeywords.some(k => lower.includes(k));
4309
+ return {
4310
+ success: true,
4311
+ routeName: routeName.charAt(0).toUpperCase() + routeName.slice(1),
4312
+ routePath,
4313
+ methods,
4314
+ isWebhook,
4315
+ requiresAuth,
4316
+ };
4317
+ }
4318
+ generateApiRouteContent(routeName, methods, isWebhook, requiresAuth) {
4319
+ const imports = [
4320
+ `import { NextRequest, NextResponse } from 'next/server';`,
4321
+ `import { z } from 'zod';`,
4322
+ ];
4323
+ if (requiresAuth && !isWebhook) {
4324
+ imports.push(`import { getServerSession } from 'next-auth';`);
4325
+ }
4326
+ const handlers = [];
4327
+ for (const method of methods) {
4328
+ let handler = '';
4329
+ if (method === 'GET') {
4330
+ handler = `
4331
+ export async function GET(request: NextRequest) {
4332
+ try {${requiresAuth ? `
4333
+ const session = await getServerSession();
4334
+ if (!session) {
4335
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
4336
+ }
4337
+ ` : ''}
4338
+ // TODO: Implement ${routeName} GET logic
4339
+
4340
+ return NextResponse.json({ message: 'Success' });
4341
+ } catch (error) {
4342
+ console.error('[${routeName.toUpperCase()}] Error:', error);
4343
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
4344
+ }
4345
+ }`;
4346
+ }
4347
+ else if (method === 'POST') {
4348
+ const schemaName = `${routeName}Schema`;
4349
+ handler = `
4350
+ const ${schemaName} = z.object({
4351
+ // TODO: Define your schema
4352
+ // example: z.string().min(1),
4353
+ });
4354
+
4355
+ export async function POST(request: NextRequest) {
4356
+ try {${requiresAuth ? `
4357
+ const session = await getServerSession();
4358
+ if (!session) {
4359
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
4360
+ }
4361
+ ` : ''}
4362
+ const body = await request.json();
4363
+ const validated = ${schemaName}.safeParse(body);
4364
+
4365
+ if (!validated.success) {
4366
+ return NextResponse.json({ error: 'Invalid request', details: validated.error.flatten() }, { status: 400 });
4367
+ }
4368
+
4369
+ // TODO: Implement ${routeName} POST logic
4370
+
4371
+ return NextResponse.json({ message: 'Success' }, { status: 201 });
4372
+ } catch (error) {
4373
+ console.error('[${routeName.toUpperCase()}] Error:', error);
4374
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
4375
+ }
4376
+ }`;
4377
+ }
4378
+ else if (method === 'PUT' || method === 'PATCH') {
4379
+ handler = `
4380
+ export async function ${method}(request: NextRequest) {
4381
+ try {${requiresAuth ? `
4382
+ const session = await getServerSession();
4383
+ if (!session) {
4384
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
4385
+ }
4386
+ ` : ''}
4387
+ const body = await request.json();
4388
+
4389
+ // TODO: Implement ${routeName} ${method} logic
4390
+
4391
+ return NextResponse.json({ message: 'Updated' });
4392
+ } catch (error) {
4393
+ console.error('[${routeName.toUpperCase()}] Error:', error);
4394
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
4395
+ }
4396
+ }`;
4397
+ }
4398
+ else if (method === 'DELETE') {
4399
+ handler = `
4400
+ export async function DELETE(request: NextRequest) {
4401
+ try {${requiresAuth ? `
4402
+ const session = await getServerSession();
4403
+ if (!session) {
4404
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
4405
+ }
4406
+ ` : ''}
4407
+ // TODO: Implement ${routeName} DELETE logic
4408
+
4409
+ return NextResponse.json({ message: 'Deleted' });
4410
+ } catch (error) {
4411
+ console.error('[${routeName.toUpperCase()}] Error:', error);
4412
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
4413
+ }
4414
+ }`;
4415
+ }
4416
+ handlers.push(handler);
4417
+ }
4418
+ // Webhook-specific template
4419
+ if (isWebhook) {
4420
+ return `import { NextRequest, NextResponse } from 'next/server';
4421
+ import { headers } from 'next/headers';
4422
+
4423
+ export async function POST(request: NextRequest) {
4424
+ try {
4425
+ const body = await request.text();
4426
+ const headersList = headers();
4427
+
4428
+ // TODO: Verify webhook signature
4429
+ // const signature = headersList.get('stripe-signature');
4430
+
4431
+ // TODO: Parse and handle webhook event
4432
+ const event = JSON.parse(body);
4433
+
4434
+ switch (event.type) {
4435
+ case 'example.event':
4436
+ // Handle event
4437
+ break;
4438
+ default:
4439
+ console.log(\`Unhandled event type: \${event.type}\`);
4440
+ }
4441
+
4442
+ return NextResponse.json({ received: true });
4443
+ } catch (error) {
4444
+ console.error('[WEBHOOK] Error:', error);
4445
+ return NextResponse.json({ error: 'Webhook error' }, { status: 400 });
4446
+ }
4447
+ }
4448
+ `;
4449
+ }
4450
+ return `${imports.join('\n')}
4451
+ ${handlers.join('\n')}
4452
+ `;
4453
+ }
3151
4454
  async run() {
3152
4455
  const transport = new stdio_js_1.StdioServerTransport();
3153
4456
  await this.server.connect(transport);