@codebakers/cli 3.3.0 → 3.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -23,6 +23,7 @@ import { go } from './commands/go.js';
23
23
  import { extend } from './commands/extend.js';
24
24
  import { billing } from './commands/billing.js';
25
25
  import { getCachedUpdateInfo, setCachedUpdateInfo, getCliVersion, getCachedPatternInfo, setCachedPatternInfo, getApiKey, getApiUrl, getTrialState, hasValidAccess } from './config.js';
26
+ import { checkForUpdates } from './lib/api.js';
26
27
  import { existsSync, writeFileSync, readFileSync, mkdirSync } from 'fs';
27
28
  import { join } from 'path';
28
29
 
@@ -30,37 +31,37 @@ import { join } from 'path';
30
31
  // Automatic Update Notification
31
32
  // ============================================
32
33
 
33
- const CURRENT_VERSION = '3.3.0';
34
+ const CURRENT_VERSION = '3.3.1';
34
35
 
35
36
  async function checkForUpdatesInBackground(): Promise<void> {
36
37
  // Check if we have a valid cached result first (fast path)
37
38
  const cached = getCachedUpdateInfo();
38
39
  if (cached) {
39
40
  if (cached.latestVersion !== CURRENT_VERSION) {
40
- showUpdateBanner(CURRENT_VERSION, cached.latestVersion);
41
+ showUpdateBanner(CURRENT_VERSION, cached.latestVersion, false);
41
42
  }
42
43
  return;
43
44
  }
44
45
 
45
- // Fetch from npm registry (with timeout to not block CLI)
46
+ // Use the API-based version check (with controlled rollout support)
46
47
  try {
47
- const controller = new AbortController();
48
- const timeout = setTimeout(() => controller.abort(), 3000);
49
-
50
- const response = await fetch('https://registry.npmjs.org/@codebakers/cli/latest', {
51
- headers: { 'Accept': 'application/json' },
52
- signal: controller.signal,
53
- });
48
+ const updateInfo = await checkForUpdates();
54
49
 
55
- clearTimeout(timeout);
50
+ if (updateInfo) {
51
+ setCachedUpdateInfo(updateInfo.latestVersion);
56
52
 
57
- if (response.ok) {
58
- const data = await response.json();
59
- const latestVersion = data.version;
60
- setCachedUpdateInfo(latestVersion);
53
+ // Show blocked version warning first (critical)
54
+ if (updateInfo.isBlocked) {
55
+ showBlockedVersionWarning(updateInfo.currentVersion, updateInfo.latestVersion);
56
+ return;
57
+ }
61
58
 
62
- if (latestVersion !== CURRENT_VERSION) {
63
- showUpdateBanner(CURRENT_VERSION, latestVersion);
59
+ // Only show update banner if auto-update is enabled for this version
60
+ if (updateInfo.autoUpdateEnabled && updateInfo.autoUpdateVersion) {
61
+ showUpdateBanner(updateInfo.currentVersion, updateInfo.autoUpdateVersion, true);
62
+ } else if (updateInfo.updateAvailable) {
63
+ // Update available but not auto-update enabled - show regular banner
64
+ showUpdateBanner(updateInfo.currentVersion, updateInfo.latestVersion, false);
64
65
  }
65
66
  }
66
67
  } catch {
@@ -68,11 +69,27 @@ async function checkForUpdatesInBackground(): Promise<void> {
68
69
  }
69
70
  }
70
71
 
71
- function showUpdateBanner(currentVersion: string, latestVersion: string): void {
72
+ function showBlockedVersionWarning(currentVersion: string, recommendedVersion: string): void {
73
+ console.log(chalk.red(`
74
+ ╭─────────────────────────────────────────────────────────╮
75
+ │ │
76
+ │ ${chalk.bold('⚠️ VERSION BLOCKED')} │
77
+ │ │
78
+ │ Your CLI version ${chalk.gray(currentVersion)} has critical issues. │
79
+ │ Please update immediately to ${chalk.green(recommendedVersion)} │
80
+ │ │
81
+ │ Run ${chalk.cyan('npm i -g @codebakers/cli@latest')} to update │
82
+ │ │
83
+ ╰─────────────────────────────────────────────────────────╯
84
+ `));
85
+ }
86
+
87
+ function showUpdateBanner(currentVersion: string, latestVersion: string, isRecommended: boolean): void {
88
+ const updateType = isRecommended ? chalk.green('Recommended update') : chalk.bold('Update available!');
72
89
  console.log(chalk.yellow(`
73
90
  ╭─────────────────────────────────────────────────────────╮
74
91
  │ │
75
- │ ${chalk.bold('Update available!')} ${chalk.gray(currentVersion)} → ${chalk.green(latestVersion)} │
92
+ │ ${updateType} ${chalk.gray(currentVersion)} → ${chalk.green(latestVersion)} │
76
93
  │ │
77
94
  │ Run ${chalk.cyan('npm i -g @codebakers/cli@latest')} to update │
78
95
  │ │
package/src/lib/api.ts CHANGED
@@ -155,29 +155,108 @@ export function getCliVersion(): string {
155
155
 
156
156
  /**
157
157
  * Check if there's a newer version of the CLI available
158
+ * Uses the CodeBakers API for controlled rollouts (only recommends stable, tested versions)
159
+ * Falls back to npm registry if API is unavailable
158
160
  */
159
161
  export async function checkForUpdates(): Promise<{
160
162
  currentVersion: string;
161
163
  latestVersion: string;
162
164
  updateAvailable: boolean;
165
+ autoUpdateEnabled: boolean;
166
+ autoUpdateVersion: string | null;
167
+ isBlocked: boolean;
163
168
  } | null> {
164
169
  try {
165
170
  const currentVersion = getCliVersion();
166
- const response = await fetch('https://registry.npmjs.org/@codebakers/cli/latest', {
171
+ const apiUrl = getApiUrl();
172
+
173
+ // First, try the CodeBakers API for controlled rollout info
174
+ try {
175
+ const response = await fetch(`${apiUrl}/api/cli/version`, {
176
+ headers: {
177
+ 'Accept': 'application/json',
178
+ 'X-CLI-Version': currentVersion,
179
+ },
180
+ });
181
+
182
+ if (response.ok) {
183
+ const data = await response.json();
184
+ const latestVersion = data.stableVersion || data.latestVersion;
185
+ const autoUpdateVersion = data.autoUpdateVersion;
186
+ const isBlocked = data.isBlocked === true;
187
+
188
+ return {
189
+ currentVersion,
190
+ latestVersion,
191
+ updateAvailable: latestVersion !== currentVersion,
192
+ autoUpdateEnabled: data.autoUpdateEnabled === true,
193
+ autoUpdateVersion: autoUpdateVersion || null,
194
+ isBlocked,
195
+ };
196
+ }
197
+ } catch {
198
+ // API unavailable, fall through to npm
199
+ }
200
+
201
+ // Fallback: check npm registry directly
202
+ const npmResponse = await fetch('https://registry.npmjs.org/@codebakers/cli/latest', {
167
203
  headers: { 'Accept': 'application/json' },
168
204
  });
169
205
 
170
- if (!response.ok) return null;
206
+ if (!npmResponse.ok) return null;
171
207
 
172
- const data = await response.json();
173
- const latestVersion = data.version;
208
+ const npmData = await npmResponse.json();
209
+ const latestVersion = npmData.version;
174
210
 
175
211
  return {
176
212
  currentVersion,
177
213
  latestVersion,
178
214
  updateAvailable: currentVersion !== latestVersion,
215
+ autoUpdateEnabled: false, // npm fallback doesn't have controlled rollout
216
+ autoUpdateVersion: null,
217
+ isBlocked: false,
179
218
  };
180
219
  } catch {
181
220
  return null;
182
221
  }
183
222
  }
223
+
224
+ /**
225
+ * Report a CLI error to the server for tracking
226
+ * This helps identify problematic versions for blocking
227
+ */
228
+ export async function reportCliError(
229
+ errorType: string,
230
+ errorMessage: string,
231
+ context?: Record<string, unknown>
232
+ ): Promise<void> {
233
+ try {
234
+ const apiUrl = getApiUrl();
235
+ const cliVersion = getCliVersion();
236
+
237
+ // Fire and forget - don't block on error reporting
238
+ fetch(`${apiUrl}/api/cli/error-report`, {
239
+ method: 'POST',
240
+ headers: {
241
+ 'Content-Type': 'application/json',
242
+ 'X-CLI-Version': cliVersion,
243
+ },
244
+ body: JSON.stringify({
245
+ cliVersion,
246
+ errorType,
247
+ errorMessage,
248
+ stackTrace: context?.stack,
249
+ context: JSON.stringify({
250
+ ...context,
251
+ nodeVersion: process.version,
252
+ platform: process.platform,
253
+ arch: process.arch,
254
+ }),
255
+ }),
256
+ }).catch(() => {
257
+ // Ignore reporting failures
258
+ });
259
+ } catch {
260
+ // Never fail on error reporting
261
+ }
262
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Central configuration for CodeBakers CLI stats.
3
+ * Keep in sync with server-side src/lib/stats.ts
4
+ */
5
+
6
+ export const CODEBAKERS_STATS = {
7
+ moduleCount: 59,
8
+ totalLinesDisplay: '50K+',
9
+ patternCount: 150,
10
+ } as const;
package/src/mcp/server.ts CHANGED
@@ -108,6 +108,31 @@ class CodeBakersServer {
108
108
  return {};
109
109
  }
110
110
 
111
+ /**
112
+ * Confirm download to server (non-blocking analytics)
113
+ */
114
+ private async confirmDownload(version: string, moduleCount: number): Promise<void> {
115
+ try {
116
+ const headers: Record<string, string> = {
117
+ 'Content-Type': 'application/json',
118
+ ...this.getAuthHeaders(),
119
+ };
120
+
121
+ await fetch(`${this.apiUrl}/api/content/confirm`, {
122
+ method: 'POST',
123
+ headers,
124
+ body: JSON.stringify({
125
+ version,
126
+ moduleCount,
127
+ cliVersion: getCliVersion(),
128
+ command: 'auto-update',
129
+ }),
130
+ });
131
+ } catch {
132
+ // Silently ignore - this is just for analytics
133
+ }
134
+ }
135
+
111
136
  /**
112
137
  * Automatically check for and apply pattern updates
113
138
  * Runs silently in background - no user intervention needed
@@ -213,6 +238,21 @@ class CodeBakersServer {
213
238
  };
214
239
  fs.writeFileSync(versionPath, JSON.stringify(versionInfo, null, 2));
215
240
 
241
+ // Write notification file for AI to read and show to user
242
+ const notificationPath = path.join(claudeDir, '.update-notification.json');
243
+ const notification = {
244
+ type: 'patterns_updated',
245
+ previousVersion: installed?.version || 'unknown',
246
+ newVersion: content.version,
247
+ moduleCount,
248
+ updatedAt: new Date().toISOString(),
249
+ message: `CodeBakers patterns have been automatically updated from v${installed?.version || 'unknown'} to v${content.version} (${moduleCount} modules). Your AI tools now have the latest production patterns.`,
250
+ };
251
+ fs.writeFileSync(notificationPath, JSON.stringify(notification, null, 2));
252
+
253
+ // Confirm to server (non-blocking, fire-and-forget)
254
+ this.confirmDownload(content.version, moduleCount).catch(() => {});
255
+
216
256
  this.autoUpdateChecked = true;
217
257
  this.autoUpdateInProgress = false;
218
258
 
@@ -984,6 +1024,29 @@ class CodeBakersServer {
984
1024
  required: ['request'],
985
1025
  },
986
1026
  },
1027
+ {
1028
+ name: 'check_update_notification',
1029
+ description:
1030
+ 'ALWAYS CALL THIS AT THE START OF EACH SESSION. Checks if CodeBakers patterns were recently auto-updated and returns a notification message to show the user. If an update occurred, tell the user about it with the returned message. After showing, the notification is cleared.',
1031
+ inputSchema: {
1032
+ type: 'object' as const,
1033
+ properties: {},
1034
+ },
1035
+ },
1036
+ {
1037
+ name: 'update_patterns',
1038
+ description:
1039
+ 'Download and update CodeBakers pattern files from the server. Use when user says "upgrade codebakers", "update patterns", "download latest patterns", "sync codebakers", or when patterns are missing or outdated. This tool fetches the latest CLAUDE.md router and all .claude/ module files from the server and writes them to disk.',
1040
+ inputSchema: {
1041
+ type: 'object' as const,
1042
+ properties: {
1043
+ force: {
1044
+ type: 'boolean',
1045
+ description: 'Force update even if already at latest version (default: false)',
1046
+ },
1047
+ },
1048
+ },
1049
+ },
987
1050
  ],
988
1051
  }));
989
1052
 
@@ -1111,6 +1174,12 @@ class CodeBakersServer {
1111
1174
  case 'add_api_route':
1112
1175
  return this.handleAddApiRoute(args as { request: string });
1113
1176
 
1177
+ case 'check_update_notification':
1178
+ return this.handleCheckUpdateNotification();
1179
+
1180
+ case 'update_patterns':
1181
+ return this.handleUpdatePatterns(args as { force?: boolean });
1182
+
1114
1183
  default:
1115
1184
  throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
1116
1185
  }
@@ -4839,6 +4908,44 @@ export default ${asyncKeyword}function ${pageName}Page() {${authCheck}
4839
4908
  };
4840
4909
  }
4841
4910
 
4911
+ /**
4912
+ * Check for update notifications and return message to show user
4913
+ */
4914
+ private async handleCheckUpdateNotification() {
4915
+ const cwd = process.cwd();
4916
+ const notificationPath = path.join(cwd, '.claude', '.update-notification.json');
4917
+
4918
+ try {
4919
+ if (!fs.existsSync(notificationPath)) {
4920
+ return {
4921
+ content: [{
4922
+ type: 'text' as const,
4923
+ text: 'No update notification.',
4924
+ }],
4925
+ };
4926
+ }
4927
+
4928
+ const notification = JSON.parse(fs.readFileSync(notificationPath, 'utf-8'));
4929
+
4930
+ // Delete the notification file after reading (so it only shows once)
4931
+ fs.unlinkSync(notificationPath);
4932
+
4933
+ return {
4934
+ content: [{
4935
+ type: 'text' as const,
4936
+ text: `🍪 **CodeBakers Update**\n\n${notification.message}\n\n**Previous version:** ${notification.previousVersion}\n**New version:** ${notification.newVersion}\n**Modules:** ${notification.moduleCount}\n**Updated:** ${new Date(notification.updatedAt).toLocaleString()}`,
4937
+ }],
4938
+ };
4939
+ } catch {
4940
+ return {
4941
+ content: [{
4942
+ type: 'text' as const,
4943
+ text: 'No update notification.',
4944
+ }],
4945
+ };
4946
+ }
4947
+ }
4948
+
4842
4949
  private parseApiRouteRequest(request: string): {
4843
4950
  success: boolean;
4844
4951
  routeName?: string;
@@ -5041,6 +5148,161 @@ ${handlers.join('\n')}
5041
5148
  `;
5042
5149
  }
5043
5150
 
5151
+ /**
5152
+ * Download and update CodeBakers patterns from server
5153
+ * This is the MCP equivalent of the `codebakers upgrade` CLI command
5154
+ */
5155
+ private async handleUpdatePatterns(args: { force?: boolean }) {
5156
+ const { force = false } = args;
5157
+ const cwd = process.cwd();
5158
+ const claudeMdPath = path.join(cwd, 'CLAUDE.md');
5159
+ const claudeDir = path.join(cwd, '.claude');
5160
+ const versionPath = path.join(claudeDir, '.version.json');
5161
+
5162
+ let response = `# 🔄 CodeBakers Pattern Update\n\n`;
5163
+
5164
+ try {
5165
+ // Check current version
5166
+ let currentVersion: string | null = null;
5167
+ let currentModuleCount = 0;
5168
+
5169
+ if (fs.existsSync(versionPath)) {
5170
+ try {
5171
+ const versionInfo = JSON.parse(fs.readFileSync(versionPath, 'utf-8'));
5172
+ currentVersion = versionInfo.version;
5173
+ currentModuleCount = versionInfo.moduleCount || 0;
5174
+ } catch {
5175
+ // Ignore parse errors
5176
+ }
5177
+ }
5178
+
5179
+ // Count current modules
5180
+ if (fs.existsSync(claudeDir)) {
5181
+ try {
5182
+ const files = fs.readdirSync(claudeDir).filter(f => f.endsWith('.md'));
5183
+ currentModuleCount = files.length;
5184
+ } catch {
5185
+ // Ignore read errors
5186
+ }
5187
+ }
5188
+
5189
+ response += `## Current Status\n`;
5190
+ response += `- Version: ${currentVersion || 'Unknown'}\n`;
5191
+ response += `- Modules: ${currentModuleCount}\n\n`;
5192
+
5193
+ // Fetch latest version info first
5194
+ const versionResponse = await fetch(`${this.apiUrl}/api/content/version`, {
5195
+ headers: this.getAuthHeaders(),
5196
+ });
5197
+
5198
+ if (!versionResponse.ok) {
5199
+ throw new Error('Failed to check version from server');
5200
+ }
5201
+
5202
+ const latestInfo = await versionResponse.json();
5203
+ const latestVersion = latestInfo.version;
5204
+ const latestModuleCount = latestInfo.moduleCount || 0;
5205
+
5206
+ response += `## Server Status\n`;
5207
+ response += `- Latest Version: ${latestVersion}\n`;
5208
+ response += `- Available Modules: ${latestModuleCount}\n\n`;
5209
+
5210
+ // Check if update needed
5211
+ const needsUpdate = force || !currentVersion || currentVersion !== latestVersion || currentModuleCount < latestModuleCount;
5212
+
5213
+ if (!needsUpdate) {
5214
+ response += `✅ **Already up to date!**\n\n`;
5215
+ response += `Your patterns are current (v${latestVersion} with ${latestModuleCount} modules).\n`;
5216
+ response += `Use \`force: true\` to re-download anyway.\n`;
5217
+
5218
+ return {
5219
+ content: [{
5220
+ type: 'text' as const,
5221
+ text: response,
5222
+ }],
5223
+ };
5224
+ }
5225
+
5226
+ response += `## Downloading Updates...\n\n`;
5227
+
5228
+ // Fetch full content
5229
+ const contentResponse = await fetch(`${this.apiUrl}/api/content`, {
5230
+ headers: this.getAuthHeaders(),
5231
+ });
5232
+
5233
+ if (!contentResponse.ok) {
5234
+ const error = await contentResponse.json().catch(() => ({}));
5235
+ throw new Error(error.error || error.message || 'Failed to fetch patterns');
5236
+ }
5237
+
5238
+ const content = await contentResponse.json();
5239
+ const moduleCount = content.modules ? Object.keys(content.modules).length : 0;
5240
+
5241
+ // Create .claude directory if needed
5242
+ if (!fs.existsSync(claudeDir)) {
5243
+ fs.mkdirSync(claudeDir, { recursive: true });
5244
+ response += `✓ Created .claude/ directory\n`;
5245
+ }
5246
+
5247
+ // Update CLAUDE.md router
5248
+ if (content.router) {
5249
+ fs.writeFileSync(claudeMdPath, content.router);
5250
+ response += `✓ Updated CLAUDE.md (router)\n`;
5251
+ }
5252
+
5253
+ // Update all modules
5254
+ if (content.modules && moduleCount > 0) {
5255
+ for (const [name, data] of Object.entries(content.modules)) {
5256
+ fs.writeFileSync(path.join(claudeDir, name), data as string);
5257
+ }
5258
+ response += `✓ Updated ${moduleCount} modules in .claude/\n`;
5259
+ }
5260
+
5261
+ // Save version info
5262
+ const newVersionInfo = {
5263
+ version: content.version || latestVersion,
5264
+ moduleCount,
5265
+ installedAt: currentVersion ? undefined : new Date().toISOString(),
5266
+ updatedAt: new Date().toISOString(),
5267
+ cliVersion: getCliVersion(),
5268
+ };
5269
+ fs.writeFileSync(versionPath, JSON.stringify(newVersionInfo, null, 2));
5270
+ response += `✓ Saved version info\n`;
5271
+
5272
+ // Confirm download to server (non-blocking analytics)
5273
+ this.confirmDownload(content.version || latestVersion, moduleCount).catch(() => {});
5274
+
5275
+ response += `\n## ✅ Update Complete!\n\n`;
5276
+ response += `- **From:** v${currentVersion || 'none'} (${currentModuleCount} modules)\n`;
5277
+ response += `- **To:** v${content.version || latestVersion} (${moduleCount} modules)\n\n`;
5278
+
5279
+ if (moduleCount > currentModuleCount) {
5280
+ response += `🆕 **${moduleCount - currentModuleCount} new modules added!**\n\n`;
5281
+ }
5282
+
5283
+ response += `Your patterns are now up to date. The new patterns will be used in your next response.\n`;
5284
+
5285
+ } catch (error) {
5286
+ const message = error instanceof Error ? error.message : 'Unknown error';
5287
+ response += `\n## ❌ Update Failed\n\n`;
5288
+ response += `Error: ${message}\n\n`;
5289
+
5290
+ if (message.includes('401') || message.includes('Invalid') || message.includes('expired')) {
5291
+ response += `Your API key may be invalid or expired.\n`;
5292
+ response += `Run \`codebakers setup\` in terminal to reconfigure.\n`;
5293
+ } else {
5294
+ response += `Please try again or run \`codebakers upgrade\` in terminal.\n`;
5295
+ }
5296
+ }
5297
+
5298
+ return {
5299
+ content: [{
5300
+ type: 'text' as const,
5301
+ text: response,
5302
+ }],
5303
+ };
5304
+ }
5305
+
5044
5306
  async run(): Promise<void> {
5045
5307
  const transport = new StdioServerTransport();
5046
5308
  await this.server.connect(transport);