@codebakers/cli 3.3.0 → 3.3.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.
@@ -24,6 +24,43 @@ function prompt(question) {
24
24
  });
25
25
  });
26
26
  }
27
+ /**
28
+ * Get CLI version from package.json
29
+ */
30
+ function getCliVersion() {
31
+ try {
32
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
33
+ const pkg = require('../../package.json');
34
+ return pkg.version || '0.0.0';
35
+ }
36
+ catch {
37
+ return '0.0.0';
38
+ }
39
+ }
40
+ /**
41
+ * Confirm download to server (non-blocking, fire-and-forget)
42
+ */
43
+ async function confirmDownload(apiUrl, auth, data) {
44
+ try {
45
+ const headers = {
46
+ 'Content-Type': 'application/json',
47
+ };
48
+ if (auth.apiKey) {
49
+ headers['Authorization'] = `Bearer ${auth.apiKey}`;
50
+ }
51
+ if (auth.trialId) {
52
+ headers['X-Trial-ID'] = auth.trialId;
53
+ }
54
+ await fetch(`${apiUrl}/api/content/confirm`, {
55
+ method: 'POST',
56
+ headers,
57
+ body: JSON.stringify(data),
58
+ });
59
+ }
60
+ catch {
61
+ // Silently ignore - this is just for analytics
62
+ }
63
+ }
27
64
  function log(message, options) {
28
65
  if (options?.verbose) {
29
66
  console.log(chalk_1.default.gray(` [verbose] ${message}`));
@@ -264,7 +301,7 @@ async function installPatternsWithApiKey(apiKey, options = {}) {
264
301
  log('Response OK, parsing JSON...', options);
265
302
  const content = await response.json();
266
303
  log(`Received version: ${content.version}, modules: ${Object.keys(content.modules || {}).length}`, options);
267
- await writePatternFiles(cwd, content, spinner, options);
304
+ await writePatternFiles(cwd, content, spinner, options, { apiKey });
268
305
  }
269
306
  catch (error) {
270
307
  log(`Error: ${error instanceof Error ? error.message : String(error)}`, options);
@@ -305,12 +342,12 @@ async function installPatterns(trialId, options = {}) {
305
342
  }
306
343
  const content = await publicResponse.json();
307
344
  log(`Received version: ${content.version}, modules: ${Object.keys(content.modules || {}).length}`, options);
308
- await writePatternFiles(cwd, content, spinner, options);
345
+ await writePatternFiles(cwd, content, spinner, options, { trialId });
309
346
  return;
310
347
  }
311
348
  const content = await response.json();
312
349
  log(`Received version: ${content.version}, modules: ${Object.keys(content.modules || {}).length}`, options);
313
- await writePatternFiles(cwd, content, spinner, options);
350
+ await writePatternFiles(cwd, content, spinner, options, { trialId });
314
351
  }
315
352
  catch (error) {
316
353
  log(`Error: ${error instanceof Error ? error.message : String(error)}`, options);
@@ -318,7 +355,7 @@ async function installPatterns(trialId, options = {}) {
318
355
  console.log(chalk_1.default.gray(' Patterns will be available via MCP tools.\n'));
319
356
  }
320
357
  }
321
- async function writePatternFiles(cwd, content, spinner, options = {}) {
358
+ async function writePatternFiles(cwd, content, spinner, options = {}, auth) {
322
359
  log(`Writing pattern files to ${cwd}...`, options);
323
360
  // Check if patterns already exist
324
361
  const claudeMdPath = (0, path_1.join)(cwd, 'CLAUDE.md');
@@ -331,7 +368,8 @@ async function writePatternFiles(cwd, content, spinner, options = {}) {
331
368
  (0, fs_1.writeFileSync)(claudeMdPath, content.router);
332
369
  }
333
370
  // Write pattern modules to .claude/
334
- if (content.modules && Object.keys(content.modules).length > 0) {
371
+ const moduleCount = Object.keys(content.modules || {}).length;
372
+ if (content.modules && moduleCount > 0) {
335
373
  const modulesDir = (0, path_1.join)(cwd, '.claude');
336
374
  if (!(0, fs_1.existsSync)(modulesDir)) {
337
375
  (0, fs_1.mkdirSync)(modulesDir, { recursive: true });
@@ -350,5 +388,15 @@ async function writePatternFiles(cwd, content, spinner, options = {}) {
350
388
  }
351
389
  }
352
390
  spinner.succeed(`CodeBakers patterns installed (v${content.version})`);
353
- console.log(chalk_1.default.gray(` ${Object.keys(content.modules || {}).length} pattern modules ready\n`));
391
+ console.log(chalk_1.default.gray(` ${moduleCount} pattern modules ready\n`));
392
+ // Confirm download to server (non-blocking)
393
+ if (auth) {
394
+ const apiUrl = (0, config_js_1.getApiUrl)();
395
+ confirmDownload(apiUrl, auth, {
396
+ version: content.version,
397
+ moduleCount,
398
+ cliVersion: getCliVersion(),
399
+ command: 'go',
400
+ }).catch(() => { }); // Silently ignore
401
+ }
354
402
  }
@@ -10,6 +10,24 @@ const fs_1 = require("fs");
10
10
  const path_1 = require("path");
11
11
  const config_js_1 = require("../config.js");
12
12
  const api_js_1 = require("../lib/api.js");
13
+ /**
14
+ * Confirm download to server (non-blocking, fire-and-forget)
15
+ */
16
+ async function confirmDownload(apiUrl, apiKey, data) {
17
+ try {
18
+ await fetch(`${apiUrl}/api/content/confirm`, {
19
+ method: 'POST',
20
+ headers: {
21
+ 'Content-Type': 'application/json',
22
+ 'Authorization': `Bearer ${apiKey}`,
23
+ },
24
+ body: JSON.stringify(data),
25
+ });
26
+ }
27
+ catch {
28
+ // Silently ignore - this is just for analytics
29
+ }
30
+ }
13
31
  /**
14
32
  * Upgrade CodeBakers patterns to the latest version
15
33
  */
@@ -83,6 +101,13 @@ async function upgrade() {
83
101
  };
84
102
  (0, fs_1.writeFileSync)((0, path_1.join)(claudeDir, '.version.json'), JSON.stringify(versionInfo, null, 2));
85
103
  console.log(chalk_1.default.green(' ✓ Version info saved'));
104
+ // Confirm download to server (non-blocking)
105
+ confirmDownload(apiUrl, apiKey, {
106
+ version: content.version,
107
+ moduleCount,
108
+ cliVersion: (0, api_js_1.getCliVersion)(),
109
+ command: 'upgrade',
110
+ }).catch(() => { }); // Silently ignore confirmation failures
86
111
  console.log(chalk_1.default.green(`\n ✅ Upgraded to patterns v${content.version}!\n`));
87
112
  // Show what's new if available
88
113
  console.log(chalk_1.default.gray(' Changes take effect in your next AI session.\n'));
package/dist/index.js CHANGED
@@ -27,36 +27,39 @@ const go_js_1 = require("./commands/go.js");
27
27
  const extend_js_1 = require("./commands/extend.js");
28
28
  const billing_js_1 = require("./commands/billing.js");
29
29
  const config_js_2 = require("./config.js");
30
+ const api_js_1 = require("./lib/api.js");
30
31
  const fs_1 = require("fs");
31
32
  const path_1 = require("path");
32
33
  // ============================================
33
34
  // Automatic Update Notification
34
35
  // ============================================
35
- const CURRENT_VERSION = '3.3.0';
36
+ const CURRENT_VERSION = '3.3.1';
36
37
  async function checkForUpdatesInBackground() {
37
38
  // Check if we have a valid cached result first (fast path)
38
39
  const cached = (0, config_js_2.getCachedUpdateInfo)();
39
40
  if (cached) {
40
41
  if (cached.latestVersion !== CURRENT_VERSION) {
41
- showUpdateBanner(CURRENT_VERSION, cached.latestVersion);
42
+ showUpdateBanner(CURRENT_VERSION, cached.latestVersion, false);
42
43
  }
43
44
  return;
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
- const response = await fetch('https://registry.npmjs.org/@codebakers/cli/latest', {
50
- headers: { 'Accept': 'application/json' },
51
- signal: controller.signal,
52
- });
53
- clearTimeout(timeout);
54
- if (response.ok) {
55
- const data = await response.json();
56
- const latestVersion = data.version;
57
- (0, config_js_2.setCachedUpdateInfo)(latestVersion);
58
- if (latestVersion !== CURRENT_VERSION) {
59
- showUpdateBanner(CURRENT_VERSION, latestVersion);
48
+ const updateInfo = await (0, api_js_1.checkForUpdates)();
49
+ if (updateInfo) {
50
+ (0, config_js_2.setCachedUpdateInfo)(updateInfo.latestVersion);
51
+ // Show blocked version warning first (critical)
52
+ if (updateInfo.isBlocked) {
53
+ showBlockedVersionWarning(updateInfo.currentVersion, updateInfo.latestVersion);
54
+ return;
55
+ }
56
+ // Only show update banner if auto-update is enabled for this version
57
+ if (updateInfo.autoUpdateEnabled && updateInfo.autoUpdateVersion) {
58
+ showUpdateBanner(updateInfo.currentVersion, updateInfo.autoUpdateVersion, true);
59
+ }
60
+ else if (updateInfo.updateAvailable) {
61
+ // Update available but not auto-update enabled - show regular banner
62
+ showUpdateBanner(updateInfo.currentVersion, updateInfo.latestVersion, false);
60
63
  }
61
64
  }
62
65
  }
@@ -64,11 +67,26 @@ async function checkForUpdatesInBackground() {
64
67
  // Silently fail - don't block CLI for update check
65
68
  }
66
69
  }
67
- function showUpdateBanner(currentVersion, latestVersion) {
70
+ function showBlockedVersionWarning(currentVersion, recommendedVersion) {
71
+ console.log(chalk_1.default.red(`
72
+ ╭─────────────────────────────────────────────────────────╮
73
+ │ │
74
+ │ ${chalk_1.default.bold('⚠️ VERSION BLOCKED')} │
75
+ │ │
76
+ │ Your CLI version ${chalk_1.default.gray(currentVersion)} has critical issues. │
77
+ │ Please update immediately to ${chalk_1.default.green(recommendedVersion)} │
78
+ │ │
79
+ │ Run ${chalk_1.default.cyan('npm i -g @codebakers/cli@latest')} to update │
80
+ │ │
81
+ ╰─────────────────────────────────────────────────────────╯
82
+ `));
83
+ }
84
+ function showUpdateBanner(currentVersion, latestVersion, isRecommended) {
85
+ const updateType = isRecommended ? chalk_1.default.green('Recommended update') : chalk_1.default.bold('Update available!');
68
86
  console.log(chalk_1.default.yellow(`
69
87
  ╭─────────────────────────────────────────────────────────╮
70
88
  │ │
71
- │ ${chalk_1.default.bold('Update available!')} ${chalk_1.default.gray(currentVersion)} → ${chalk_1.default.green(latestVersion)} │
89
+ │ ${updateType} ${chalk_1.default.gray(currentVersion)} → ${chalk_1.default.green(latestVersion)} │
72
90
  │ │
73
91
  │ Run ${chalk_1.default.cyan('npm i -g @codebakers/cli@latest')} to update │
74
92
  │ │
package/dist/lib/api.d.ts CHANGED
@@ -37,9 +37,19 @@ export declare function checkApiKeyValidity(): Promise<{
37
37
  export declare function getCliVersion(): string;
38
38
  /**
39
39
  * Check if there's a newer version of the CLI available
40
+ * Uses the CodeBakers API for controlled rollouts (only recommends stable, tested versions)
41
+ * Falls back to npm registry if API is unavailable
40
42
  */
41
43
  export declare function checkForUpdates(): Promise<{
42
44
  currentVersion: string;
43
45
  latestVersion: string;
44
46
  updateAvailable: boolean;
47
+ autoUpdateEnabled: boolean;
48
+ autoUpdateVersion: string | null;
49
+ isBlocked: boolean;
45
50
  } | null>;
51
+ /**
52
+ * Report a CLI error to the server for tracking
53
+ * This helps identify problematic versions for blocking
54
+ */
55
+ export declare function reportCliError(errorType: string, errorMessage: string, context?: Record<string, unknown>): Promise<void>;
package/dist/lib/api.js CHANGED
@@ -7,6 +7,7 @@ exports.formatApiError = formatApiError;
7
7
  exports.checkApiKeyValidity = checkApiKeyValidity;
8
8
  exports.getCliVersion = getCliVersion;
9
9
  exports.checkForUpdates = checkForUpdates;
10
+ exports.reportCliError = reportCliError;
10
11
  const config_js_1 = require("../config.js");
11
12
  /**
12
13
  * Validate an API key format
@@ -136,24 +137,92 @@ function getCliVersion() {
136
137
  }
137
138
  /**
138
139
  * Check if there's a newer version of the CLI available
140
+ * Uses the CodeBakers API for controlled rollouts (only recommends stable, tested versions)
141
+ * Falls back to npm registry if API is unavailable
139
142
  */
140
143
  async function checkForUpdates() {
141
144
  try {
142
145
  const currentVersion = getCliVersion();
143
- const response = await fetch('https://registry.npmjs.org/@codebakers/cli/latest', {
146
+ const apiUrl = (0, config_js_1.getApiUrl)();
147
+ // First, try the CodeBakers API for controlled rollout info
148
+ try {
149
+ const response = await fetch(`${apiUrl}/api/cli/version`, {
150
+ headers: {
151
+ 'Accept': 'application/json',
152
+ 'X-CLI-Version': currentVersion,
153
+ },
154
+ });
155
+ if (response.ok) {
156
+ const data = await response.json();
157
+ const latestVersion = data.stableVersion || data.latestVersion;
158
+ const autoUpdateVersion = data.autoUpdateVersion;
159
+ const isBlocked = data.isBlocked === true;
160
+ return {
161
+ currentVersion,
162
+ latestVersion,
163
+ updateAvailable: latestVersion !== currentVersion,
164
+ autoUpdateEnabled: data.autoUpdateEnabled === true,
165
+ autoUpdateVersion: autoUpdateVersion || null,
166
+ isBlocked,
167
+ };
168
+ }
169
+ }
170
+ catch {
171
+ // API unavailable, fall through to npm
172
+ }
173
+ // Fallback: check npm registry directly
174
+ const npmResponse = await fetch('https://registry.npmjs.org/@codebakers/cli/latest', {
144
175
  headers: { 'Accept': 'application/json' },
145
176
  });
146
- if (!response.ok)
177
+ if (!npmResponse.ok)
147
178
  return null;
148
- const data = await response.json();
149
- const latestVersion = data.version;
179
+ const npmData = await npmResponse.json();
180
+ const latestVersion = npmData.version;
150
181
  return {
151
182
  currentVersion,
152
183
  latestVersion,
153
184
  updateAvailable: currentVersion !== latestVersion,
185
+ autoUpdateEnabled: false, // npm fallback doesn't have controlled rollout
186
+ autoUpdateVersion: null,
187
+ isBlocked: false,
154
188
  };
155
189
  }
156
190
  catch {
157
191
  return null;
158
192
  }
159
193
  }
194
+ /**
195
+ * Report a CLI error to the server for tracking
196
+ * This helps identify problematic versions for blocking
197
+ */
198
+ async function reportCliError(errorType, errorMessage, context) {
199
+ try {
200
+ const apiUrl = (0, config_js_1.getApiUrl)();
201
+ const cliVersion = getCliVersion();
202
+ // Fire and forget - don't block on error reporting
203
+ fetch(`${apiUrl}/api/cli/error-report`, {
204
+ method: 'POST',
205
+ headers: {
206
+ 'Content-Type': 'application/json',
207
+ 'X-CLI-Version': cliVersion,
208
+ },
209
+ body: JSON.stringify({
210
+ cliVersion,
211
+ errorType,
212
+ errorMessage,
213
+ stackTrace: context?.stack,
214
+ context: JSON.stringify({
215
+ ...context,
216
+ nodeVersion: process.version,
217
+ platform: process.platform,
218
+ arch: process.arch,
219
+ }),
220
+ }),
221
+ }).catch(() => {
222
+ // Ignore reporting failures
223
+ });
224
+ }
225
+ catch {
226
+ // Never fail on error reporting
227
+ }
228
+ }
@@ -89,6 +89,30 @@ class CodeBakersServer {
89
89
  }
90
90
  return {};
91
91
  }
92
+ /**
93
+ * Confirm download to server (non-blocking analytics)
94
+ */
95
+ async confirmDownload(version, moduleCount) {
96
+ try {
97
+ const headers = {
98
+ 'Content-Type': 'application/json',
99
+ ...this.getAuthHeaders(),
100
+ };
101
+ await fetch(`${this.apiUrl}/api/content/confirm`, {
102
+ method: 'POST',
103
+ headers,
104
+ body: JSON.stringify({
105
+ version,
106
+ moduleCount,
107
+ cliVersion: (0, api_js_1.getCliVersion)(),
108
+ command: 'auto-update',
109
+ }),
110
+ });
111
+ }
112
+ catch {
113
+ // Silently ignore - this is just for analytics
114
+ }
115
+ }
92
116
  /**
93
117
  * Automatically check for and apply pattern updates
94
118
  * Runs silently in background - no user intervention needed
@@ -178,6 +202,19 @@ class CodeBakersServer {
178
202
  cliVersion: (0, api_js_1.getCliVersion)(),
179
203
  };
180
204
  fs.writeFileSync(versionPath, JSON.stringify(versionInfo, null, 2));
205
+ // Write notification file for AI to read and show to user
206
+ const notificationPath = path.join(claudeDir, '.update-notification.json');
207
+ const notification = {
208
+ type: 'patterns_updated',
209
+ previousVersion: installed?.version || 'unknown',
210
+ newVersion: content.version,
211
+ moduleCount,
212
+ updatedAt: new Date().toISOString(),
213
+ 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.`,
214
+ };
215
+ fs.writeFileSync(notificationPath, JSON.stringify(notification, null, 2));
216
+ // Confirm to server (non-blocking, fire-and-forget)
217
+ this.confirmDownload(content.version, moduleCount).catch(() => { });
181
218
  this.autoUpdateChecked = true;
182
219
  this.autoUpdateInProgress = false;
183
220
  // Log success (visible in MCP logs)
@@ -902,6 +939,14 @@ class CodeBakersServer {
902
939
  required: ['request'],
903
940
  },
904
941
  },
942
+ {
943
+ name: 'check_update_notification',
944
+ description: '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.',
945
+ inputSchema: {
946
+ type: 'object',
947
+ properties: {},
948
+ },
949
+ },
905
950
  ],
906
951
  }));
907
952
  // Handle tool calls
@@ -987,6 +1032,8 @@ class CodeBakersServer {
987
1032
  return this.handleAddPage(args);
988
1033
  case 'add_api_route':
989
1034
  return this.handleAddApiRoute(args);
1035
+ case 'check_update_notification':
1036
+ return this.handleCheckUpdateNotification();
990
1037
  default:
991
1038
  throw new types_js_1.McpError(types_js_1.ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
992
1039
  }
@@ -4266,6 +4313,40 @@ export default ${asyncKeyword}function ${pageName}Page() {${authCheck}
4266
4313
  }],
4267
4314
  };
4268
4315
  }
4316
+ /**
4317
+ * Check for update notifications and return message to show user
4318
+ */
4319
+ async handleCheckUpdateNotification() {
4320
+ const cwd = process.cwd();
4321
+ const notificationPath = path.join(cwd, '.claude', '.update-notification.json');
4322
+ try {
4323
+ if (!fs.existsSync(notificationPath)) {
4324
+ return {
4325
+ content: [{
4326
+ type: 'text',
4327
+ text: 'No update notification.',
4328
+ }],
4329
+ };
4330
+ }
4331
+ const notification = JSON.parse(fs.readFileSync(notificationPath, 'utf-8'));
4332
+ // Delete the notification file after reading (so it only shows once)
4333
+ fs.unlinkSync(notificationPath);
4334
+ return {
4335
+ content: [{
4336
+ type: 'text',
4337
+ 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()}`,
4338
+ }],
4339
+ };
4340
+ }
4341
+ catch {
4342
+ return {
4343
+ content: [{
4344
+ type: 'text',
4345
+ text: 'No update notification.',
4346
+ }],
4347
+ };
4348
+ }
4349
+ }
4269
4350
  parseApiRouteRequest(request) {
4270
4351
  const lower = request.toLowerCase();
4271
4352
  // Detect webhook
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codebakers/cli",
3
- "version": "3.3.0",
3
+ "version": "3.3.1",
4
4
  "description": "CodeBakers CLI - Production patterns for AI-assisted development",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -15,7 +15,8 @@
15
15
  "mcp": "tsx src/mcp/server.ts",
16
16
  "test": "vitest run",
17
17
  "test:watch": "vitest",
18
- "test:coverage": "vitest run --coverage"
18
+ "test:coverage": "vitest run --coverage",
19
+ "postpublish": "node scripts/register-version.js"
19
20
  },
20
21
  "keywords": [
21
22
  "codebakers",
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Automatically registers a new CLI version with the CodeBakers server
5
+ * Called automatically after npm publish via postpublish hook
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ const API_URL = process.env.CODEBAKERS_API_URL || 'https://codebakers.ai';
12
+ const ADMIN_API_KEY = process.env.CODEBAKERS_ADMIN_KEY;
13
+
14
+ async function registerVersion() {
15
+ // Read version from package.json
16
+ const packagePath = path.join(__dirname, '..', 'package.json');
17
+ const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf-8'));
18
+ const version = pkg.version;
19
+
20
+ console.log(`\n📦 Registering CLI version ${version} with CodeBakers server...\n`);
21
+
22
+ if (!ADMIN_API_KEY) {
23
+ console.log('⚠️ CODEBAKERS_ADMIN_KEY not set - skipping auto-registration');
24
+ console.log(' You can manually add this version in Admin → CLI Versions\n');
25
+ return;
26
+ }
27
+
28
+ try {
29
+ // Read changelog if it exists
30
+ let changelog = '';
31
+ const changelogPath = path.join(__dirname, '..', 'CHANGELOG.md');
32
+ if (fs.existsSync(changelogPath)) {
33
+ const content = fs.readFileSync(changelogPath, 'utf-8');
34
+ // Extract the latest version's changes
35
+ const match = content.match(/## \[?\d+\.\d+\.\d+\]?[^\n]*\n([\s\S]*?)(?=## \[?\d+\.\d+\.\d+|$)/);
36
+ if (match) {
37
+ changelog = match[1].trim().slice(0, 2000); // Limit length
38
+ }
39
+ }
40
+
41
+ const response = await fetch(`${API_URL}/api/cli/register-version`, {
42
+ method: 'POST',
43
+ headers: {
44
+ 'Content-Type': 'application/json',
45
+ 'Authorization': `Bearer ${ADMIN_API_KEY}`,
46
+ },
47
+ body: JSON.stringify({
48
+ version,
49
+ npmTag: 'latest',
50
+ changelog,
51
+ minNodeVersion: '18',
52
+ }),
53
+ });
54
+
55
+ if (response.ok) {
56
+ const data = await response.json();
57
+ console.log(`✅ Version ${version} registered successfully!`);
58
+ console.log(` Status: ${data.data?.version?.status || 'draft'}`);
59
+ console.log(`\n Next steps:`);
60
+ console.log(` 1. Test the version`);
61
+ console.log(` 2. Go to Admin → CLI Versions`);
62
+ console.log(` 3. Promote to "testing" then "stable"`);
63
+ console.log(` 4. Enable auto-update when ready\n`);
64
+ } else {
65
+ const error = await response.json().catch(() => ({}));
66
+ if (error.error?.includes('already exists')) {
67
+ console.log(`ℹ️ Version ${version} already registered\n`);
68
+ } else {
69
+ console.log(`⚠️ Failed to register: ${error.error || response.statusText}`);
70
+ console.log(` You can manually add this version in Admin → CLI Versions\n`);
71
+ }
72
+ }
73
+ } catch (error) {
74
+ console.log(`⚠️ Could not reach server: ${error.message}`);
75
+ console.log(` You can manually add this version in Admin → CLI Versions\n`);
76
+ }
77
+ }
78
+
79
+ registerVersion();
@@ -39,6 +39,57 @@ interface GoOptions {
39
39
  verbose?: boolean;
40
40
  }
41
41
 
42
+ interface ConfirmData {
43
+ version: string;
44
+ moduleCount: number;
45
+ cliVersion?: string;
46
+ command: string;
47
+ projectName?: string;
48
+ }
49
+
50
+ interface AuthInfo {
51
+ apiKey?: string;
52
+ trialId?: string;
53
+ }
54
+
55
+ /**
56
+ * Get CLI version from package.json
57
+ */
58
+ function getCliVersion(): string {
59
+ try {
60
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
61
+ const pkg = require('../../package.json');
62
+ return pkg.version || '0.0.0';
63
+ } catch {
64
+ return '0.0.0';
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Confirm download to server (non-blocking, fire-and-forget)
70
+ */
71
+ async function confirmDownload(apiUrl: string, auth: AuthInfo, data: ConfirmData): Promise<void> {
72
+ try {
73
+ const headers: Record<string, string> = {
74
+ 'Content-Type': 'application/json',
75
+ };
76
+ if (auth.apiKey) {
77
+ headers['Authorization'] = `Bearer ${auth.apiKey}`;
78
+ }
79
+ if (auth.trialId) {
80
+ headers['X-Trial-ID'] = auth.trialId;
81
+ }
82
+
83
+ await fetch(`${apiUrl}/api/content/confirm`, {
84
+ method: 'POST',
85
+ headers,
86
+ body: JSON.stringify(data),
87
+ });
88
+ } catch {
89
+ // Silently ignore - this is just for analytics
90
+ }
91
+ }
92
+
42
93
  function log(message: string, options?: GoOptions): void {
43
94
  if (options?.verbose) {
44
95
  console.log(chalk.gray(` [verbose] ${message}`));
@@ -317,7 +368,7 @@ async function installPatternsWithApiKey(apiKey: string, options: GoOptions = {}
317
368
  log('Response OK, parsing JSON...', options);
318
369
  const content: ContentResponse = await response.json();
319
370
  log(`Received version: ${content.version}, modules: ${Object.keys(content.modules || {}).length}`, options);
320
- await writePatternFiles(cwd, content, spinner, options);
371
+ await writePatternFiles(cwd, content, spinner, options, { apiKey });
321
372
 
322
373
  } catch (error) {
323
374
  log(`Error: ${error instanceof Error ? error.message : String(error)}`, options);
@@ -363,13 +414,13 @@ async function installPatterns(trialId: string, options: GoOptions = {}): Promis
363
414
 
364
415
  const content: ContentResponse = await publicResponse.json();
365
416
  log(`Received version: ${content.version}, modules: ${Object.keys(content.modules || {}).length}`, options);
366
- await writePatternFiles(cwd, content, spinner, options);
417
+ await writePatternFiles(cwd, content, spinner, options, { trialId });
367
418
  return;
368
419
  }
369
420
 
370
421
  const content: ContentResponse = await response.json();
371
422
  log(`Received version: ${content.version}, modules: ${Object.keys(content.modules || {}).length}`, options);
372
- await writePatternFiles(cwd, content, spinner, options);
423
+ await writePatternFiles(cwd, content, spinner, options, { trialId });
373
424
 
374
425
  } catch (error) {
375
426
  log(`Error: ${error instanceof Error ? error.message : String(error)}`, options);
@@ -378,7 +429,13 @@ async function installPatterns(trialId: string, options: GoOptions = {}): Promis
378
429
  }
379
430
  }
380
431
 
381
- async function writePatternFiles(cwd: string, content: ContentResponse, spinner: ReturnType<typeof ora>, options: GoOptions = {}): Promise<void> {
432
+ async function writePatternFiles(
433
+ cwd: string,
434
+ content: ContentResponse,
435
+ spinner: ReturnType<typeof ora>,
436
+ options: GoOptions = {},
437
+ auth?: AuthInfo
438
+ ): Promise<void> {
382
439
  log(`Writing pattern files to ${cwd}...`, options);
383
440
  // Check if patterns already exist
384
441
  const claudeMdPath = join(cwd, 'CLAUDE.md');
@@ -393,7 +450,8 @@ async function writePatternFiles(cwd: string, content: ContentResponse, spinner:
393
450
  }
394
451
 
395
452
  // Write pattern modules to .claude/
396
- if (content.modules && Object.keys(content.modules).length > 0) {
453
+ const moduleCount = Object.keys(content.modules || {}).length;
454
+ if (content.modules && moduleCount > 0) {
397
455
  const modulesDir = join(cwd, '.claude');
398
456
  if (!existsSync(modulesDir)) {
399
457
  mkdirSync(modulesDir, { recursive: true });
@@ -415,5 +473,16 @@ async function writePatternFiles(cwd: string, content: ContentResponse, spinner:
415
473
  }
416
474
 
417
475
  spinner.succeed(`CodeBakers patterns installed (v${content.version})`);
418
- console.log(chalk.gray(` ${Object.keys(content.modules || {}).length} pattern modules ready\n`));
476
+ console.log(chalk.gray(` ${moduleCount} pattern modules ready\n`));
477
+
478
+ // Confirm download to server (non-blocking)
479
+ if (auth) {
480
+ const apiUrl = getApiUrl();
481
+ confirmDownload(apiUrl, auth, {
482
+ version: content.version,
483
+ moduleCount,
484
+ cliVersion: getCliVersion(),
485
+ command: 'go',
486
+ }).catch(() => {}); // Silently ignore
487
+ }
419
488
  }
@@ -11,6 +11,32 @@ interface ContentResponse {
11
11
  modules: Record<string, string>;
12
12
  }
13
13
 
14
+ interface ConfirmData {
15
+ version: string;
16
+ moduleCount: number;
17
+ cliVersion: string;
18
+ command: string;
19
+ projectName?: string;
20
+ }
21
+
22
+ /**
23
+ * Confirm download to server (non-blocking, fire-and-forget)
24
+ */
25
+ async function confirmDownload(apiUrl: string, apiKey: string, data: ConfirmData): Promise<void> {
26
+ try {
27
+ await fetch(`${apiUrl}/api/content/confirm`, {
28
+ method: 'POST',
29
+ headers: {
30
+ 'Content-Type': 'application/json',
31
+ 'Authorization': `Bearer ${apiKey}`,
32
+ },
33
+ body: JSON.stringify(data),
34
+ });
35
+ } catch {
36
+ // Silently ignore - this is just for analytics
37
+ }
38
+ }
39
+
14
40
  /**
15
41
  * Upgrade CodeBakers patterns to the latest version
16
42
  */
@@ -101,6 +127,14 @@ export async function upgrade(): Promise<void> {
101
127
  writeFileSync(join(claudeDir, '.version.json'), JSON.stringify(versionInfo, null, 2));
102
128
  console.log(chalk.green(' ✓ Version info saved'));
103
129
 
130
+ // Confirm download to server (non-blocking)
131
+ confirmDownload(apiUrl, apiKey, {
132
+ version: content.version,
133
+ moduleCount,
134
+ cliVersion: getCliVersion(),
135
+ command: 'upgrade',
136
+ }).catch(() => {}); // Silently ignore confirmation failures
137
+
104
138
  console.log(chalk.green(`\n ✅ Upgraded to patterns v${content.version}!\n`));
105
139
 
106
140
  // Show what's new if available
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
+ }
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,15 @@ 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
+ },
987
1036
  ],
988
1037
  }));
989
1038
 
@@ -1111,6 +1160,9 @@ class CodeBakersServer {
1111
1160
  case 'add_api_route':
1112
1161
  return this.handleAddApiRoute(args as { request: string });
1113
1162
 
1163
+ case 'check_update_notification':
1164
+ return this.handleCheckUpdateNotification();
1165
+
1114
1166
  default:
1115
1167
  throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
1116
1168
  }
@@ -4839,6 +4891,44 @@ export default ${asyncKeyword}function ${pageName}Page() {${authCheck}
4839
4891
  };
4840
4892
  }
4841
4893
 
4894
+ /**
4895
+ * Check for update notifications and return message to show user
4896
+ */
4897
+ private async handleCheckUpdateNotification() {
4898
+ const cwd = process.cwd();
4899
+ const notificationPath = path.join(cwd, '.claude', '.update-notification.json');
4900
+
4901
+ try {
4902
+ if (!fs.existsSync(notificationPath)) {
4903
+ return {
4904
+ content: [{
4905
+ type: 'text' as const,
4906
+ text: 'No update notification.',
4907
+ }],
4908
+ };
4909
+ }
4910
+
4911
+ const notification = JSON.parse(fs.readFileSync(notificationPath, 'utf-8'));
4912
+
4913
+ // Delete the notification file after reading (so it only shows once)
4914
+ fs.unlinkSync(notificationPath);
4915
+
4916
+ return {
4917
+ content: [{
4918
+ type: 'text' as const,
4919
+ 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()}`,
4920
+ }],
4921
+ };
4922
+ } catch {
4923
+ return {
4924
+ content: [{
4925
+ type: 'text' as const,
4926
+ text: 'No update notification.',
4927
+ }],
4928
+ };
4929
+ }
4930
+ }
4931
+
4842
4932
  private parseApiRouteRequest(request: string): {
4843
4933
  success: boolean;
4844
4934
  routeName?: string;