@bike4mind/cli 0.2.13 → 0.2.14-feat-cli-mcp-management.17477

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.
@@ -0,0 +1,611 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/storage/ConfigStore.ts
4
+ import { promises as fs, existsSync } from "fs";
5
+ import path from "path";
6
+ import { homedir } from "os";
7
+ import { v4 as uuidv4 } from "uuid";
8
+ import { z } from "zod";
9
+ var AuthTokensSchema = z.object({
10
+ accessToken: z.string(),
11
+ refreshToken: z.string(),
12
+ expiresAt: z.string(),
13
+ userId: z.string()
14
+ });
15
+ var ApiConfigSchema = z.object({
16
+ customUrl: z.string().url().optional()
17
+ });
18
+ var CliConfigSchema = z.object({
19
+ version: z.string(),
20
+ userId: z.string(),
21
+ auth: AuthTokensSchema.optional(),
22
+ defaultModel: z.string(),
23
+ apiConfig: ApiConfigSchema.optional(),
24
+ toolApiKeys: z.object({
25
+ openweather: z.string().optional(),
26
+ serper: z.string().optional()
27
+ }).optional(),
28
+ mcpServers: z.array(
29
+ z.object({
30
+ name: z.string(),
31
+ command: z.string().optional(),
32
+ args: z.array(z.string()).optional(),
33
+ env: z.record(z.string()),
34
+ enabled: z.boolean()
35
+ })
36
+ ),
37
+ preferences: z.object({
38
+ maxTokens: z.number(),
39
+ temperature: z.number(),
40
+ autoSave: z.boolean(),
41
+ theme: z.enum(["light", "dark"]),
42
+ exportFormat: z.enum(["markdown", "json"]),
43
+ maxIterations: z.number().nullable().default(10)
44
+ }),
45
+ tools: z.object({
46
+ enabled: z.array(z.string()),
47
+ disabled: z.array(z.string()),
48
+ config: z.record(z.any())
49
+ }),
50
+ trustedTools: z.array(z.string()).optional().default([])
51
+ });
52
+ var ProjectConfigSchema = z.object({
53
+ tools: z.object({
54
+ enabled: z.array(z.string()).optional(),
55
+ denied: z.array(z.string()).optional(),
56
+ config: z.record(z.any()).optional()
57
+ }).optional(),
58
+ defaultModel: z.string().optional(),
59
+ mcpServers: z.array(
60
+ z.object({
61
+ name: z.string(),
62
+ command: z.string().optional(),
63
+ args: z.array(z.string()).optional(),
64
+ env: z.record(z.string()),
65
+ enabled: z.boolean()
66
+ })
67
+ ).optional(),
68
+ preferences: z.object({
69
+ maxTokens: z.number().optional(),
70
+ temperature: z.number().optional(),
71
+ autoSave: z.boolean().optional(),
72
+ theme: z.enum(["light", "dark"]).optional(),
73
+ exportFormat: z.enum(["markdown", "json"]).optional()
74
+ }).optional()
75
+ });
76
+ var ProjectLocalConfigSchema = z.object({
77
+ trustedTools: z.array(z.string()).optional(),
78
+ toolApiKeys: z.object({
79
+ openweather: z.string().optional(),
80
+ serper: z.string().optional()
81
+ }).optional(),
82
+ preferences: z.object({
83
+ maxTokens: z.number().optional(),
84
+ temperature: z.number().optional(),
85
+ autoSave: z.boolean().optional(),
86
+ theme: z.enum(["light", "dark"]).optional(),
87
+ exportFormat: z.enum(["markdown", "json"]).optional()
88
+ }).optional(),
89
+ mcpServers: z.array(
90
+ z.object({
91
+ name: z.string(),
92
+ command: z.string().optional(),
93
+ args: z.array(z.string()).optional(),
94
+ env: z.record(z.string()),
95
+ enabled: z.boolean()
96
+ })
97
+ ).optional()
98
+ });
99
+ var DEFAULT_CONFIG = {
100
+ version: "0.1.0",
101
+ userId: uuidv4(),
102
+ defaultModel: "claude-sonnet-4-5-20250929",
103
+ toolApiKeys: {
104
+ openweather: void 0,
105
+ serper: void 0
106
+ },
107
+ mcpServers: [],
108
+ preferences: {
109
+ maxTokens: 4096,
110
+ temperature: 0.7,
111
+ autoSave: true,
112
+ theme: "dark",
113
+ exportFormat: "markdown",
114
+ maxIterations: 10
115
+ },
116
+ tools: {
117
+ enabled: [],
118
+ disabled: ["blog_publish", "blog_edit", "blog_draft"],
119
+ // Web-only tools
120
+ config: {}
121
+ },
122
+ trustedTools: []
123
+ // No tools trusted by default
124
+ };
125
+ function findProjectConfigDir(startDir = process.cwd()) {
126
+ let currentDir = startDir;
127
+ const { root } = path.parse(currentDir);
128
+ while (currentDir !== root) {
129
+ const gitPath = path.join(currentDir, ".git");
130
+ try {
131
+ if (existsSync(gitPath)) {
132
+ return currentDir;
133
+ }
134
+ } catch {
135
+ }
136
+ currentDir = path.dirname(currentDir);
137
+ }
138
+ return process.cwd();
139
+ }
140
+ async function loadProjectConfig(projectDir) {
141
+ const configPath = path.join(projectDir, ".bike4mind", "config.json");
142
+ try {
143
+ const data = await fs.readFile(configPath, "utf-8");
144
+ const rawConfig = JSON.parse(data);
145
+ const validated = ProjectConfigSchema.parse(rawConfig);
146
+ return validated;
147
+ } catch (error) {
148
+ if (error.code === "ENOENT") {
149
+ return null;
150
+ }
151
+ if (error instanceof z.ZodError) {
152
+ console.error("Project config validation error:", error.errors);
153
+ return null;
154
+ }
155
+ console.error("Failed to load project config:", error);
156
+ return null;
157
+ }
158
+ }
159
+ async function loadProjectLocalConfig(projectDir) {
160
+ const configPath = path.join(projectDir, ".bike4mind", "local.json");
161
+ try {
162
+ const data = await fs.readFile(configPath, "utf-8");
163
+ const rawConfig = JSON.parse(data);
164
+ const validated = ProjectLocalConfigSchema.parse(rawConfig);
165
+ return validated;
166
+ } catch (error) {
167
+ if (error.code === "ENOENT") {
168
+ return null;
169
+ }
170
+ if (error instanceof z.ZodError) {
171
+ console.error("Project local config validation error:", error.errors);
172
+ return null;
173
+ }
174
+ console.error("Failed to load project local config:", error);
175
+ return null;
176
+ }
177
+ }
178
+ function mergeMcpServers(...serverArrays) {
179
+ const serverMap = /* @__PURE__ */ new Map();
180
+ for (const servers of serverArrays) {
181
+ if (servers) {
182
+ for (const server of servers) {
183
+ serverMap.set(server.name, server);
184
+ }
185
+ }
186
+ }
187
+ return Array.from(serverMap.values());
188
+ }
189
+ function mergeConfigs(global, project, local) {
190
+ const merged = { ...global };
191
+ if (project) {
192
+ if (project.defaultModel) {
193
+ merged.defaultModel = project.defaultModel;
194
+ }
195
+ if (project.preferences) {
196
+ merged.preferences = {
197
+ ...merged.preferences,
198
+ ...project.preferences
199
+ };
200
+ }
201
+ if (project.tools) {
202
+ merged.tools = {
203
+ ...merged.tools,
204
+ enabled: [...merged.tools.enabled || [], ...project.tools.enabled || []],
205
+ disabled: [...merged.tools.disabled, ...project.tools.denied || []],
206
+ config: {
207
+ ...merged.tools.config,
208
+ ...project.tools.config
209
+ }
210
+ };
211
+ }
212
+ if (project.mcpServers) {
213
+ merged.mcpServers = mergeMcpServers(merged.mcpServers, project.mcpServers);
214
+ }
215
+ }
216
+ if (local) {
217
+ if (local.trustedTools) {
218
+ const trustedSet = /* @__PURE__ */ new Set([...merged.trustedTools || [], ...local.trustedTools]);
219
+ merged.trustedTools = Array.from(trustedSet);
220
+ }
221
+ if (local.toolApiKeys) {
222
+ merged.toolApiKeys = {
223
+ ...merged.toolApiKeys,
224
+ ...local.toolApiKeys
225
+ };
226
+ }
227
+ if (local.preferences) {
228
+ merged.preferences = {
229
+ ...merged.preferences,
230
+ ...local.preferences
231
+ };
232
+ }
233
+ if (local.mcpServers) {
234
+ merged.mcpServers = mergeMcpServers(merged.mcpServers, local.mcpServers);
235
+ }
236
+ }
237
+ return merged;
238
+ }
239
+ var ConfigStore = class {
240
+ constructor(configPath) {
241
+ this.config = null;
242
+ this.projectConfigDir = null;
243
+ this.configPath = configPath || path.join(homedir(), ".bike4mind", "config.json");
244
+ }
245
+ /**
246
+ * Initialize config directory
247
+ */
248
+ async init() {
249
+ const dir = path.dirname(this.configPath);
250
+ try {
251
+ await fs.mkdir(dir, { recursive: true });
252
+ } catch (error) {
253
+ console.error("Failed to initialize config directory:", error);
254
+ throw error;
255
+ }
256
+ }
257
+ /**
258
+ * Load configuration from disk with Zod validation
259
+ * Merges global → project → local configs
260
+ */
261
+ async load() {
262
+ if (this.config) {
263
+ return this.config;
264
+ }
265
+ try {
266
+ let globalConfig;
267
+ try {
268
+ try {
269
+ const stats = await fs.stat(this.configPath);
270
+ const mode = stats.mode & 511;
271
+ if (mode !== 384) {
272
+ console.warn(`\u26A0\uFE0F Config file has insecure permissions (${mode.toString(8)}). Setting to 0600...`);
273
+ await fs.chmod(this.configPath, 384);
274
+ }
275
+ } catch (statError) {
276
+ }
277
+ const data = await fs.readFile(this.configPath, "utf-8");
278
+ const rawConfig = JSON.parse(data);
279
+ if (rawConfig.apiConfig && "environment" in rawConfig.apiConfig) {
280
+ const oldApiConfig = rawConfig.apiConfig;
281
+ if (oldApiConfig.environment === "custom" && oldApiConfig.customUrl) {
282
+ rawConfig.apiConfig = { customUrl: oldApiConfig.customUrl };
283
+ } else {
284
+ rawConfig.apiConfig = {};
285
+ }
286
+ }
287
+ const validated = CliConfigSchema.parse(rawConfig);
288
+ globalConfig = {
289
+ ...DEFAULT_CONFIG,
290
+ ...validated,
291
+ auth: validated.auth,
292
+ // Explicitly preserve auth field
293
+ preferences: {
294
+ ...DEFAULT_CONFIG.preferences,
295
+ ...validated.preferences
296
+ },
297
+ tools: {
298
+ ...DEFAULT_CONFIG.tools,
299
+ ...validated.tools
300
+ },
301
+ toolApiKeys: {
302
+ ...DEFAULT_CONFIG.toolApiKeys,
303
+ ...validated.toolApiKeys || {}
304
+ },
305
+ trustedTools: validated.trustedTools || []
306
+ };
307
+ } catch (error) {
308
+ if (error.code === "ENOENT") {
309
+ globalConfig = { ...DEFAULT_CONFIG };
310
+ } else if (error instanceof z.ZodError) {
311
+ console.error("Global config validation error:", error.errors);
312
+ console.error("Using default configuration");
313
+ globalConfig = { ...DEFAULT_CONFIG };
314
+ } else {
315
+ throw error;
316
+ }
317
+ }
318
+ let projectConfig = null;
319
+ let projectLocalConfig = null;
320
+ if (process.env.B4M_NO_PROJECT_CONFIG !== "1") {
321
+ this.projectConfigDir = findProjectConfigDir();
322
+ if (this.projectConfigDir) {
323
+ projectConfig = await loadProjectConfig(this.projectConfigDir);
324
+ projectLocalConfig = await loadProjectLocalConfig(this.projectConfigDir);
325
+ if (projectConfig) {
326
+ console.log(`\u{1F4C1} Project config loaded from: ${this.projectConfigDir}/.bike4mind/`);
327
+ }
328
+ }
329
+ } else {
330
+ this.projectConfigDir = null;
331
+ }
332
+ this.config = mergeConfigs(globalConfig, projectConfig, projectLocalConfig);
333
+ return this.config;
334
+ } catch (error) {
335
+ if (error.code === "ENOENT") {
336
+ return this.reset();
337
+ }
338
+ if (error instanceof z.ZodError) {
339
+ console.error("Config validation error:", error.errors);
340
+ console.error("Resetting to default configuration");
341
+ return this.reset();
342
+ }
343
+ console.error("Failed to load config:", error);
344
+ throw error;
345
+ }
346
+ }
347
+ /**
348
+ * Save configuration to disk
349
+ */
350
+ async save(config) {
351
+ await this.init();
352
+ if (config) {
353
+ const existingConfig = await this.load();
354
+ this.config = {
355
+ ...existingConfig,
356
+ ...config,
357
+ auth: config.auth !== void 0 ? config.auth : existingConfig.auth,
358
+ preferences: {
359
+ ...existingConfig.preferences,
360
+ ...config.preferences || {}
361
+ },
362
+ tools: {
363
+ ...existingConfig.tools,
364
+ ...config.tools || {}
365
+ },
366
+ toolApiKeys: {
367
+ ...existingConfig.toolApiKeys,
368
+ ...config.toolApiKeys || {}
369
+ }
370
+ };
371
+ }
372
+ if (!this.config) {
373
+ throw new Error("No configuration to save");
374
+ }
375
+ try {
376
+ await fs.writeFile(this.configPath, JSON.stringify(this.config, null, 2), "utf-8");
377
+ await fs.chmod(this.configPath, 384);
378
+ } catch (error) {
379
+ console.error("Failed to save config:", error);
380
+ throw error;
381
+ }
382
+ }
383
+ /**
384
+ * Reset configuration to defaults
385
+ */
386
+ async reset() {
387
+ this.config = { ...DEFAULT_CONFIG, userId: uuidv4() };
388
+ await this.save();
389
+ return this.config;
390
+ }
391
+ /**
392
+ * Get current configuration
393
+ */
394
+ async get() {
395
+ return this.load();
396
+ }
397
+ /**
398
+ * Update a specific configuration value
399
+ */
400
+ async update(updates) {
401
+ await this.save(updates);
402
+ }
403
+ /**
404
+ * Add MCP server configuration
405
+ */
406
+ async addMcpServer(server) {
407
+ const config = await this.load();
408
+ config.mcpServers = config.mcpServers.filter((s) => s.name !== server.name);
409
+ config.mcpServers.push(server);
410
+ await this.save(config);
411
+ }
412
+ /**
413
+ * Remove MCP server configuration
414
+ */
415
+ async removeMcpServer(name) {
416
+ const config = await this.load();
417
+ config.mcpServers = config.mcpServers.filter((s) => s.name !== name);
418
+ await this.save(config);
419
+ }
420
+ /**
421
+ * Enable/disable MCP server
422
+ */
423
+ async toggleMcpServer(name, enabled) {
424
+ const config = await this.load();
425
+ const server = config.mcpServers.find((s) => s.name === name);
426
+ if (server) {
427
+ server.enabled = enabled;
428
+ await this.save(config);
429
+ }
430
+ }
431
+ /**
432
+ * Add a tool to trusted tools list
433
+ */
434
+ async trustTool(toolName) {
435
+ const config = await this.load();
436
+ if (!config.trustedTools) {
437
+ config.trustedTools = [];
438
+ }
439
+ if (!config.trustedTools.includes(toolName)) {
440
+ config.trustedTools.push(toolName);
441
+ await this.save(config);
442
+ }
443
+ }
444
+ /**
445
+ * Remove a tool from trusted tools list
446
+ */
447
+ async untrustTool(toolName) {
448
+ const config = await this.load();
449
+ if (config.trustedTools) {
450
+ config.trustedTools = config.trustedTools.filter((t) => t !== toolName);
451
+ await this.save(config);
452
+ }
453
+ }
454
+ /**
455
+ * Get list of trusted tools
456
+ */
457
+ async getTrustedTools() {
458
+ const config = await this.load();
459
+ return config.trustedTools || [];
460
+ }
461
+ /**
462
+ * Clear all trusted tools
463
+ */
464
+ async clearTrustedTools() {
465
+ const config = await this.load();
466
+ config.trustedTools = [];
467
+ await this.save(config);
468
+ }
469
+ /**
470
+ * Get authentication tokens
471
+ */
472
+ async getAuthTokens() {
473
+ const config = await this.load();
474
+ return config.auth || null;
475
+ }
476
+ /**
477
+ * Set authentication tokens
478
+ */
479
+ async setAuthTokens(tokens) {
480
+ const config = await this.load();
481
+ config.auth = tokens;
482
+ await this.save(config);
483
+ }
484
+ /**
485
+ * Clear authentication tokens (logout)
486
+ */
487
+ async clearAuthTokens() {
488
+ const config = await this.load();
489
+ config.auth = void 0;
490
+ await this.save(config);
491
+ }
492
+ /**
493
+ * Check if user is authenticated
494
+ */
495
+ async isAuthenticated() {
496
+ const tokens = await this.getAuthTokens();
497
+ if (!tokens) return false;
498
+ const expiresAt = new Date(tokens.expiresAt);
499
+ return expiresAt > /* @__PURE__ */ new Date();
500
+ }
501
+ /**
502
+ * Get API configuration
503
+ */
504
+ async getApiConfig() {
505
+ const config = await this.load();
506
+ return config.apiConfig;
507
+ }
508
+ /**
509
+ * Set custom API URL for self-hosted Bike4Mind instance
510
+ * Pass null to reset to Bike4Mind main service
511
+ */
512
+ async setCustomApiUrl(url) {
513
+ const config = await this.load();
514
+ if (url === null) {
515
+ config.apiConfig = void 0;
516
+ } else {
517
+ config.apiConfig = { customUrl: url };
518
+ }
519
+ await this.save(config);
520
+ }
521
+ /**
522
+ * Get project config directory (if any)
523
+ */
524
+ getProjectConfigDir() {
525
+ return this.projectConfigDir;
526
+ }
527
+ /**
528
+ * Initialize project config directory
529
+ * Creates .bike4mind/ directory and ensures local.json is gitignored
530
+ * Does NOT auto-create config.json (user creates that manually)
531
+ */
532
+ async initProjectConfig() {
533
+ const projectDir = this.projectConfigDir || findProjectConfigDir();
534
+ if (!projectDir) {
535
+ return;
536
+ }
537
+ const configDir = path.join(projectDir, ".bike4mind");
538
+ await fs.mkdir(configDir, { recursive: true });
539
+ await this.ensureGitignore(projectDir);
540
+ }
541
+ /**
542
+ * Ensure .gitignore includes .bike4mind/local.json
543
+ */
544
+ async ensureGitignore(projectDir) {
545
+ const gitignorePath = path.join(projectDir, ".gitignore");
546
+ const entryToAdd = ".bike4mind/local.json";
547
+ try {
548
+ let gitignoreContent = "";
549
+ try {
550
+ gitignoreContent = await fs.readFile(gitignorePath, "utf-8");
551
+ } catch {
552
+ }
553
+ if (gitignoreContent.includes(entryToAdd)) {
554
+ return;
555
+ }
556
+ const newContent = gitignoreContent.trim() + (gitignoreContent ? "\n" : "") + `
557
+ # Bike4Mind local config (developer-specific)
558
+ ${entryToAdd}
559
+ `;
560
+ await fs.writeFile(gitignorePath, newContent, "utf-8");
561
+ console.log(`\u2705 Added ${entryToAdd} to .gitignore`);
562
+ } catch (error) {
563
+ console.warn(`\u26A0\uFE0F Failed to update .gitignore:`, error);
564
+ }
565
+ }
566
+ /**
567
+ * Save project config to .bike4mind/config.json
568
+ */
569
+ async saveProjectConfig(config, projectDir) {
570
+ const targetDir = projectDir || this.projectConfigDir || process.cwd();
571
+ const configPath = path.join(targetDir, ".bike4mind", "config.json");
572
+ await fs.mkdir(path.dirname(configPath), { recursive: true });
573
+ const validated = ProjectConfigSchema.parse(config);
574
+ await fs.writeFile(configPath, JSON.stringify(validated, null, 2), "utf-8");
575
+ console.log(`\u2705 Saved project config to: ${configPath}`);
576
+ }
577
+ /**
578
+ * Save project-local config to .bike4mind/local.json
579
+ */
580
+ async saveProjectLocalConfig(config, projectDir) {
581
+ const targetDir = projectDir || this.projectConfigDir || process.cwd();
582
+ const configPath = path.join(targetDir, ".bike4mind", "local.json");
583
+ await fs.mkdir(path.dirname(configPath), { recursive: true });
584
+ const validated = ProjectLocalConfigSchema.parse(config);
585
+ await fs.writeFile(configPath, JSON.stringify(validated, null, 2), "utf-8");
586
+ await fs.chmod(configPath, 384);
587
+ console.log(`\u2705 Saved project-local config to: ${configPath}`);
588
+ }
589
+ /**
590
+ * Load raw project config (without merging)
591
+ */
592
+ async loadRawProjectConfig() {
593
+ if (!this.projectConfigDir) {
594
+ return null;
595
+ }
596
+ return loadProjectConfig(this.projectConfigDir);
597
+ }
598
+ /**
599
+ * Load raw project-local config (without merging)
600
+ */
601
+ async loadRawProjectLocalConfig() {
602
+ if (!this.projectConfigDir) {
603
+ return null;
604
+ }
605
+ return loadProjectLocalConfig(this.projectConfigDir);
606
+ }
607
+ };
608
+
609
+ export {
610
+ ConfigStore
611
+ };