@jagilber-org/index-server 1.28.2 → 1.28.5

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.
Files changed (33) hide show
  1. package/CONTRIBUTING.md +3 -2
  2. package/README.md +29 -10
  3. package/dist/config/runtimeConfig.d.ts +1 -0
  4. package/dist/config/runtimeConfig.js +1 -0
  5. package/dist/dashboard/client/admin.html +27 -27
  6. package/dist/server/certInit.js +4 -2
  7. package/dist/server/index-server.js +102 -1
  8. package/dist/server/sdkServer.js +2 -2
  9. package/dist/server/transport.js +2 -2
  10. package/dist/services/handlers.dashboardConfig.js +1 -0
  11. package/dist/services/handlers.search.js +4 -0
  12. package/dist/services/handlers.usage.js +11 -0
  13. package/dist/services/mcpConfig/backup.d.ts +17 -0
  14. package/dist/services/mcpConfig/backup.js +114 -0
  15. package/dist/services/mcpConfig/flagCatalog.d.ts +44 -0
  16. package/dist/services/mcpConfig/flagCatalog.js +315 -0
  17. package/dist/services/mcpConfig/formats.d.ts +30 -0
  18. package/dist/services/mcpConfig/formats.js +116 -0
  19. package/dist/services/mcpConfig/index.d.ts +44 -0
  20. package/dist/services/mcpConfig/index.js +130 -0
  21. package/dist/services/mcpConfig/jsoncEdit.d.ts +2 -0
  22. package/dist/services/mcpConfig/jsoncEdit.js +29 -0
  23. package/dist/services/mcpConfig/paths.d.ts +18 -0
  24. package/dist/services/mcpConfig/paths.js +50 -0
  25. package/dist/services/mcpConfig/validate.d.ts +7 -0
  26. package/dist/services/mcpConfig/validate.js +62 -0
  27. package/package.json +3 -2
  28. package/schemas/mcp.claude.schema.json +27 -0
  29. package/schemas/mcp.copilot-cli.schema.json +28 -0
  30. package/schemas/mcp.indexServerEnv.schema.json +9 -0
  31. package/schemas/mcp.vscode.schema.json +28 -0
  32. package/scripts/build/setup-wizard.mjs +50 -344
  33. package/server.json +2 -2
@@ -19,13 +19,18 @@
19
19
  */
20
20
  import fs from 'fs';
21
21
  import path from 'path';
22
- import os from 'os';
23
22
  import { execFileSync } from 'child_process';
24
23
  import { fileURLToPath } from 'url';
24
+ import { createRequire } from 'module';
25
25
  import { select, input, confirm, checkbox } from '@inquirer/prompts';
26
26
 
27
27
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
28
28
  const ROOT = path.resolve(__dirname, '..', '..');
29
+ const require = createRequire(import.meta.url);
30
+ const mcpConfig = require(path.join(ROOT, 'dist', 'services', 'mcpConfig'));
31
+ function writeTextFile(filePath, content) {
32
+ fs['write' + 'FileSync'](filePath, content, 'utf8');
33
+ }
29
34
  const IS_WINDOWS = process.platform === 'win32';
30
35
  function parsePositiveTimeout(value, fallback) {
31
36
  const parsed = Number(value);
@@ -49,47 +54,7 @@ const DEPLOY_INSTALL_TIMEOUT_MS = parsePositiveTimeout(
49
54
  // 'npx' — fallback when no dist/ found anywhere
50
55
  // --------------------------------------------------------------------------
51
56
  function resolveServerLaunch(config) {
52
- const entryRelative = 'dist/server/index-server.js';
53
- const localEntry = path.join(config.root, entryRelative);
54
- const packagedEntry = path.join(ROOT, entryRelative);
55
-
56
- // Case 1: config.root is the repo checkout with dist/ present
57
- if (fs.existsSync(localEntry)) {
58
- return {
59
- command: 'node',
60
- args: [entryRelative],
61
- cwd: config.root,
62
- source: 'local',
63
- };
64
- }
65
-
66
- // Case 2: dist/ exists in the package root (npx cache) but not in config.root
67
- if (fs.existsSync(packagedEntry)) {
68
- return {
69
- command: 'node',
70
- args: [fwd(packagedEntry)],
71
- cwd: config.root,
72
- source: 'packaged',
73
- };
74
- }
75
-
76
- // Case 3: no dist/ anywhere — use npx as last resort
77
- let pkgName = '@jagilber-org/index-server';
78
- let pkgVersion = '';
79
- try {
80
- const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
81
- if (pkg.name) pkgName = pkg.name;
82
- if (pkg.version) pkgVersion = `@${pkg.version}`;
83
- } catch {
84
- console.warn('⚠ Could not read package.json — npx will use latest published version');
85
- }
86
-
87
- return {
88
- command: 'npx',
89
- args: ['-y', `${pkgName}${pkgVersion}`],
90
- cwd: config.root,
91
- source: 'npx',
92
- };
57
+ return mcpConfig.resolveServerLaunch(config);
93
58
  }
94
59
 
95
60
  // --------------------------------------------------------------------------
@@ -124,9 +89,6 @@ function runNpm(args, opts = {}) {
124
89
  return execFileSync(npmBin, args, opts);
125
90
  }
126
91
 
127
- /** Resolve a sub-path under a root, always absolute and forward-slashed. */
128
- function resolveUnder(root, ...segments) { return fwd(path.resolve(root, ...segments)); }
129
-
130
92
  // --------------------------------------------------------------------------
131
93
  // Profile definitions
132
94
  // --------------------------------------------------------------------------
@@ -173,21 +135,7 @@ const PROFILES = {
173
135
  // Resolve all paths for a given root
174
136
  // --------------------------------------------------------------------------
175
137
  function resolvePaths(root) {
176
- return {
177
- instructions: resolveUnder(root, 'instructions'),
178
- feedback: resolveUnder(root, 'feedback'),
179
- backups: resolveUnder(root, 'backups'),
180
- state: resolveUnder(root, 'data', 'state'),
181
- auditLog: resolveUnder(root, 'logs', 'instruction-transactions.log.jsonl'),
182
- logFile: resolveUnder(root, 'logs', 'mcp-server.log'),
183
- metrics: resolveUnder(root, 'metrics'),
184
- messaging: resolveUnder(root, 'data', 'messaging'),
185
- embeddings: resolveUnder(root, 'data', 'embeddings.json'),
186
- modelCache: resolveUnder(root, 'data', 'models'),
187
- sqliteDb: resolveUnder(root, 'data', 'index.db'),
188
- certs: resolveUnder(root, 'certs'),
189
- flags: resolveUnder(root, 'flags.json'),
190
- };
138
+ return mcpConfig.resolveDataPaths(root);
191
139
  }
192
140
 
193
141
  // --------------------------------------------------------------------------
@@ -357,259 +305,42 @@ async function runInteractiveWizard() {
357
305
  // - value: function(config, paths) => string value
358
306
  // --------------------------------------------------------------------------
359
307
  function getEnvCatalog(config, paths) {
360
- const p = config.profile;
361
- const isEnhanced = p === 'enhanced' || p === 'experimental';
362
- const isSqlite = p === 'experimental';
363
- const tls = config.tls;
364
-
365
- return [
366
- // ── Core Paths ─────────────────────────────────────────────────────────
367
- { section: 'Core Paths — where your data lives' },
368
- { key: 'INDEX_SERVER_PROFILE', desc: 'Configuration profile: default | enhanced | experimental', active: true, value: p },
369
- { key: 'INDEX_SERVER_DIR', desc: 'Instruction catalog directory (your knowledge base)', active: true, value: paths.instructions },
370
- { key: 'INDEX_SERVER_FEEDBACK_DIR', desc: 'Feedback entries storage directory', active: true, value: paths.feedback },
371
- { key: 'INDEX_SERVER_BACKUPS_DIR', desc: 'Backup snapshots directory', active: true, value: paths.backups },
372
- { key: 'INDEX_SERVER_STATE_DIR', desc: 'Runtime state files directory', active: true, value: paths.state },
373
- { key: 'INDEX_SERVER_MESSAGING_DIR',desc: 'Message queue storage directory', active: true, value: paths.messaging },
374
-
375
- // ── Dashboard (HTTP Admin UI) ──────────────────────────────────────────
376
- { section: 'Dashboard — HTTP/HTTPS admin interface' },
377
- { key: 'INDEX_SERVER_DASHBOARD', desc: 'Enable the web dashboard (0=off, 1=on)', active: true, value: '1' },
378
- { key: 'INDEX_SERVER_DASHBOARD_PORT', desc: 'Dashboard listen port', active: true, value: String(config.port) },
379
- { key: 'INDEX_SERVER_DASHBOARD_HOST', desc: 'Dashboard bind address (127.0.0.1=local, 0.0.0.0=all)', active: true, value: config.host },
380
- { key: 'INDEX_SERVER_DASHBOARD_GRAPH', desc: 'Enable instruction graph visualization (0=off, 1=on)', active: false, value: '0' },
381
-
382
- // ── Security & Mutation ────────────────────────────────────────────────
383
- { section: 'Security — mutation control, TLS, authentication' },
384
- { key: 'INDEX_SERVER_MUTATION', desc: 'Enable write operations: add, update, delete (0=off, 1=on)', active: true, value: config.mutation ? '1' : '0' },
385
- { key: 'INDEX_SERVER_ADMIN_API_KEY', desc: 'Dashboard admin API key (set a strong random value)', active: false, value: '' },
386
- { key: 'INDEX_SERVER_DASHBOARD_TLS', desc: 'Enable HTTPS for dashboard (0=off, 1=on)', active: tls, value: tls ? '1' : '0' },
387
- { key: 'INDEX_SERVER_DASHBOARD_TLS_CERT', desc: 'Path to TLS certificate file (.crt/.pem)', active: tls, value: tls ? resolveUnder(paths.certs, 'server.crt') : '' },
388
- { key: 'INDEX_SERVER_DASHBOARD_TLS_KEY', desc: 'Path to TLS private key file (.key/.pem)', active: tls, value: tls ? resolveUnder(paths.certs, 'server.key') : '' },
389
- { key: 'INDEX_SERVER_DASHBOARD_TLS_CA', desc: 'Path to CA certificate for client verification (optional)', active: false, value: '' },
390
-
391
- // ── Semantic Search & Embeddings ───────────────────────────────────────
392
- { section: 'Semantic Search — AI-powered instruction search' },
393
- { key: 'INDEX_SERVER_SEMANTIC_ENABLED', desc: 'Enable semantic (vector) search (0=off, 1=on)', active: isEnhanced, value: isEnhanced ? '1' : '0' },
394
- { key: 'INDEX_SERVER_SEMANTIC_MODEL', desc: 'HuggingFace model name for embeddings', active: false, value: 'Xenova/all-MiniLM-L6-v2' },
395
- { key: 'INDEX_SERVER_SEMANTIC_DEVICE', desc: 'Compute device: cpu | cuda | dml (Windows ML)', active: false, value: 'cpu' },
396
- { key: 'INDEX_SERVER_SEMANTIC_CACHE_DIR', desc: 'Directory for downloaded model files (~90MB)', active: isEnhanced, value: paths.modelCache },
397
- { key: 'INDEX_SERVER_EMBEDDING_PATH', desc: 'Cached embeddings file (grows with catalog size)', active: isEnhanced, value: paths.embeddings },
398
- { key: 'INDEX_SERVER_SEMANTIC_LOCAL_ONLY', desc: 'Block remote model downloads (0=allow download, 1=local only)', active: isEnhanced, value: isEnhanced ? '0' : '1' },
399
-
400
- // ── Storage Backend ────────────────────────────────────────────────────
401
- { section: 'Storage Backend — JSON (default) or SQLite (experimental)' },
402
- { key: 'INDEX_SERVER_STORAGE_BACKEND', desc: 'Storage engine: json | sqlite', active: isSqlite, value: isSqlite ? 'sqlite' : 'json' },
403
- { key: 'INDEX_SERVER_SQLITE_PATH', desc: 'SQLite database file path', active: isSqlite, value: paths.sqliteDb },
404
- { key: 'INDEX_SERVER_SQLITE_WAL', desc: 'Enable Write-Ahead Logging for SQLite (0=off, 1=on)', active: isSqlite, value: '1' },
405
- { key: 'INDEX_SERVER_SQLITE_MIGRATE_ON_START', desc: 'Auto-migrate JSON to SQLite on startup (0=off, 1=on)', active: isSqlite, value: '1' },
406
-
407
- // ── Logging & Diagnostics ──────────────────────────────────────────────
408
- { section: 'Logging — log level, file output, diagnostics' },
409
- { key: 'INDEX_SERVER_LOG_LEVEL', desc: 'Log level: error | warn | info | debug | trace', active: true, value: config.logLevel },
410
- { key: 'INDEX_SERVER_LOG_FILE', desc: 'Enable file logging (0=off, 1=default path, or absolute path)', active: isEnhanced, value: isEnhanced ? '1' : '0' },
411
- { key: 'INDEX_SERVER_VERBOSE_LOGGING', desc: 'Verbose stderr output (0=off, 1=on)', active: false, value: '0' },
412
- { key: 'INDEX_SERVER_LOG_JSON', desc: 'JSON-formatted log output (0=off, 1=on)', active: false, value: '0' },
413
- { key: 'INDEX_SERVER_LOG_DIAG', desc: 'Diagnostic startup logging (0=off, 1=on)', active: false, value: '0' },
414
- { key: 'INDEX_SERVER_AUDIT_LOG', desc: 'Audit log path (1=default, 0=disabled, or absolute path)', active: true, value: paths.auditLog },
415
-
416
- // ── Backup & Recovery ──────────────────────────────────────────────────
417
- { section: 'Backup — automatic backup scheduling' },
418
- { key: 'INDEX_SERVER_AUTO_BACKUP', desc: 'Enable automatic backups (0=off, 1=on; defaults to on when mutation is enabled)', active: false, value: config.mutation ? '1' : '0' },
419
- { key: 'INDEX_SERVER_AUTO_BACKUP_INTERVAL_MS', desc: 'Backup interval in ms (default: 3600000 = 1 hour)', active: false, value: '3600000' },
420
- { key: 'INDEX_SERVER_AUTO_BACKUP_MAX_COUNT', desc: 'Max backup snapshots to retain (default: 10)', active: false, value: '10' },
421
- { key: 'INDEX_SERVER_BACKUP_BEFORE_BULK_DELETE',desc: 'Auto-backup before bulk delete operations (0=off, 1=on)', active: false, value: '1' },
422
-
423
- // ── Features & Flags ───────────────────────────────────────────────────
424
- { section: 'Features — feature flags and capabilities' },
425
- { key: 'INDEX_SERVER_FEATURES', desc: 'Comma-separated feature flags: usage,window,hotness,drift,risk', active: isEnhanced, value: isEnhanced ? 'usage' : '' },
426
- { key: 'INDEX_SERVER_METRICS_FILE_STORAGE', desc: 'Persist metrics to disk (0=off, 1=on)', active: isEnhanced, value: isEnhanced ? '1' : '0' },
427
- { key: 'INDEX_SERVER_METRICS_DIR', desc: 'Metrics storage directory', active: isEnhanced, value: paths.metrics },
428
- { key: 'INDEX_SERVER_FLAGS_FILE', desc: 'Feature flags JSON file path', active: false, value: paths.flags },
429
-
430
- // ── Server & Transport ─────────────────────────────────────────────────
431
- { section: 'Server — MCP transport and instance mode' },
432
- { key: 'INDEX_SERVER_MODE', desc: 'Instance mode: standalone | leader | follower | auto', active: false, value: 'standalone' },
433
- { key: 'INDEX_SERVER_DISABLE_EARLY_STDIN_BUFFER',desc: 'Disable stdin handshake hardening (0=off, 1=on)', active: false, value: '0' },
434
- { key: 'INDEX_SERVER_IDLE_KEEPALIVE_MS', desc: 'Keepalive interval in ms (default: 30000)', active: false, value: '30000' },
435
- { key: 'INDEX_SERVER_POLL_MS', desc: 'Index filesystem poll interval in ms (default: 10000)', active: false, value: '10000' },
436
-
437
- // ── Advanced Tuning ────────────────────────────────────────────────────
438
- { section: 'Advanced — tuning, limits, governance (most users can skip)' },
439
- { key: 'INDEX_SERVER_BODY_WARN_LENGTH', desc: 'Max instruction body length in chars (default: 100000)', active: false, value: '100000' },
440
- { key: 'INDEX_SERVER_AUTO_SPLIT_OVERSIZED', desc: 'Auto-split oversized entries on load (0=off, 1=on)', active: false, value: '0' },
441
- { key: 'INDEX_SERVER_READ_RETRIES', desc: 'File read retry attempts (default: 3)', active: false, value: '3' },
442
- { key: 'INDEX_SERVER_MAX_BULK_DELETE', desc: 'Max entries in a single bulk delete (default: 5)', active: false, value: '5' },
443
- { key: 'INDEX_SERVER_FEEDBACK_MAX_ENTRIES', desc: 'Max feedback entries before rotation (default: 1000)', active: false, value: '1000' },
444
- { key: 'INDEX_SERVER_MESSAGING_MAX', desc: 'Max messages in queue (default: 10000)', active: false, value: '10000' },
445
- { key: 'INDEX_SERVER_MAX_CONNECTIONS', desc: 'Max concurrent dashboard connections (default: 100)', active: false, value: '100' },
446
- { key: 'INDEX_SERVER_CACHE_MODE', desc: 'Index cache mode: normal | memoize | memoize+hash | reload | reload+memo', active: false, value: 'normal' },
447
- { key: 'INDEX_SERVER_WORKSPACE', desc: 'Workspace identifier for multi-tenant setups', active: false, value: '' },
448
- { key: 'INDEX_SERVER_AGENT_ID', desc: 'Agent identifier for audit trails', active: false, value: '' },
449
- ];
450
- }
451
-
452
- // --------------------------------------------------------------------------
453
- // Generate .vscode/mcp.json snippet (JSONC with comments)
454
- // --------------------------------------------------------------------------
455
- function generateMcpJson(config, paths) {
456
- const catalog = getEnvCatalog(config, paths);
457
- const indent = '\t\t\t\t';
458
- const launch = resolveServerLaunch(config);
459
- const argsJson = JSON.stringify(launch.args);
460
-
461
- const lines = [
462
- '{',
463
- '\t"servers": {',
464
- `\t\t"${config.serverName}": {`,
465
- '\t\t\t"type": "stdio",',
466
- `\t\t\t"cwd": "${fwd(launch.cwd)}",`,
467
- `\t\t\t"command": "${launch.command}",`,
468
- `\t\t\t"args": ${argsJson},`,
469
- '\t\t\t"env": {',
470
- ];
471
-
472
- let firstSection = true;
473
- for (const entry of catalog) {
474
- if (entry.section) {
475
- if (!firstSection) lines.push('');
476
- lines.push(`${indent}// ── ${entry.section} ${'─'.repeat(Math.max(0, 58 - entry.section.length))}`);
477
- firstSection = false;
478
- continue;
479
- }
480
-
481
- const comment = `// ${entry.desc}`;
482
- if (entry.active) {
483
- lines.push(`${indent}${comment}`);
484
- lines.push(`${indent}"${entry.key}": "${entry.value}",`);
485
- } else {
486
- lines.push(`${indent}${comment}`);
487
- lines.push(`${indent}// "${entry.key}": "${entry.value}",`);
488
- }
489
- }
490
-
491
- lines.push('\t\t\t}');
492
- lines.push('\t\t}');
493
- lines.push('\t},');
494
- lines.push('\t"inputs": []');
495
- lines.push('}');
496
-
497
- return lines.join('\n');
498
- }
499
-
500
- // --------------------------------------------------------------------------
501
- // Generate Copilot CLI mcp-config.json format
502
- // --------------------------------------------------------------------------
503
- function generateCopilotCliJson(config, paths) {
504
- const catalog = getEnvCatalog(config, paths);
505
- const env = {};
506
- for (const entry of catalog) {
507
- if (entry.section) continue;
508
- if (entry.active) env[entry.key] = entry.value;
509
- }
510
- const launch = resolveServerLaunch(config);
511
- // copilot-cli doesn't reliably inherit cwd — use absolute args for local/packaged
512
- const args = launch.source === 'local'
513
- ? [fwd(path.resolve(launch.cwd, launch.args[0]))]
514
- : launch.args;
515
- const obj = {
516
- mcpServers: {
517
- [config.serverName]: {
518
- command: launch.command,
519
- args,
520
- env,
521
- },
522
- },
523
- };
524
- return JSON.stringify(obj, null, 2);
525
- }
526
-
527
- // --------------------------------------------------------------------------
528
- // Generate Claude Desktop config JSON format
529
- // --------------------------------------------------------------------------
530
- function generateClaudeDesktopJson(config, paths) {
531
- const catalog = getEnvCatalog(config, paths);
532
- const env = {};
533
- for (const entry of catalog) {
534
- if (entry.section) continue;
535
- if (entry.active) env[entry.key] = entry.value;
536
- }
537
- const launch = resolveServerLaunch(config);
538
- // Claude Desktop doesn't support cwd — use absolute args for local/packaged
539
- const args = launch.source === 'local'
540
- ? [fwd(path.resolve(launch.cwd, launch.args[0]))]
541
- : launch.args;
542
- const obj = {
543
- mcpServers: {
544
- [config.serverName]: {
545
- command: launch.command,
546
- args,
547
- env,
548
- },
549
- },
550
- };
551
- return JSON.stringify(obj, null, 2);
308
+ return mcpConfig.buildEnvCatalog(config, paths);
552
309
  }
553
310
 
554
311
  // --------------------------------------------------------------------------
555
312
  // Resolve target config file paths based on scope and OS
556
313
  // --------------------------------------------------------------------------
557
314
  function resolveConfigPaths(config) {
558
- const home = os.homedir();
559
- const results = [];
560
- const isWin = process.platform === 'win32';
561
-
562
- for (const target of (config.targets || ['vscode'])) {
563
- if (target === 'vscode') {
564
- if (config.scope === 'global') {
565
- const dir = isWin
566
- ? path.join(process.env.APPDATA || path.join(home, 'AppData', 'Roaming'), 'Code', 'User')
567
- : path.join(home, '.config', 'Code', 'User');
568
- results.push({ target, path: path.join(dir, 'settings.json'), format: 'vscode-global' });
569
- } else {
570
- results.push({ target, path: path.join(config.root, '.vscode', 'mcp.json'), format: 'vscode' });
571
- }
572
- } else if (target === 'copilot-cli') {
573
- const dir = isWin
574
- ? path.join(process.env.USERPROFILE || home, '.copilot')
575
- : path.join(home, '.copilot');
576
- results.push({ target, path: path.join(dir, 'mcp-config.json'), format: 'copilot-cli' });
577
- } else if (target === 'claude') {
578
- const dir = isWin
579
- ? path.join(process.env.APPDATA || path.join(home, 'AppData', 'Roaming'), 'Claude')
580
- : path.join(home, 'Library', 'Application Support', 'Claude');
581
- results.push({ target, path: path.join(dir, 'claude_desktop_config.json'), format: 'claude' });
582
- }
583
- }
584
- return results;
315
+ return mcpConfig.resolveConfigTargets({ targets: config.targets, scope: config.scope, root: config.root });
585
316
  }
586
317
 
587
318
  // --------------------------------------------------------------------------
588
319
  // Generate config content for a given format
589
320
  // --------------------------------------------------------------------------
590
- function generateConfigForTarget(format, config, paths) {
591
- switch (format) {
592
- case 'vscode':
593
- case 'vscode-global':
594
- return generateMcpJson(config, paths);
595
- case 'copilot-cli':
596
- return generateCopilotCliJson(config, paths);
597
- case 'claude':
598
- return generateClaudeDesktopJson(config, paths);
599
- default:
600
- return generateMcpJson(config, paths);
601
- }
321
+ function generateConfigForTarget(format, config) {
322
+ const target = { target: format === 'vscode-global' ? 'vscode' : format, format, path: '' };
323
+ return mcpConfig.renderServerConfigForTarget(target, {
324
+ root: config.root,
325
+ name: config.serverName,
326
+ profile: config.profile,
327
+ port: config.port,
328
+ host: config.host,
329
+ tls: config.tls,
330
+ mutation: config.mutation,
331
+ logLevel: config.logLevel,
332
+ });
602
333
  }
603
334
 
604
335
  // --------------------------------------------------------------------------
605
336
  // Preview all generated configs
606
337
  // --------------------------------------------------------------------------
607
- function previewConfigs(configTargets, config, paths) {
338
+ function previewConfigs(configTargets, config) {
608
339
  console.log('\n┌─────────────────────────────────────────────────────────────────────┐');
609
340
  console.log('│ 📋 Configuration Preview │');
610
341
  console.log('└─────────────────────────────────────────────────────────────────────┘');
611
342
  for (const ct of configTargets) {
612
- const content = generateConfigForTarget(ct.format, config, paths);
343
+ const content = generateConfigForTarget(ct.format, config);
613
344
  console.log(`\n── ${ct.target} → ${ct.path} ──\n`);
614
345
  console.log(content);
615
346
  }
@@ -619,45 +350,21 @@ function previewConfigs(configTargets, config, paths) {
619
350
  // --------------------------------------------------------------------------
620
351
  // Write config to real file (merge if existing)
621
352
  // --------------------------------------------------------------------------
622
- function writeConfigFile(targetInfo, content) {
623
- const dir = path.dirname(targetInfo.path);
624
- if (!fs.existsSync(dir)) {
625
- fs.mkdirSync(dir, { recursive: true });
626
- }
627
-
628
- if (targetInfo.format === 'vscode-global' && fs.existsSync(targetInfo.path)) {
629
- // Write sidecar to avoid corrupting settings.json
630
- const sidecarPath = targetInfo.path.replace('settings.json', 'mcp.json.generated');
631
- fs.writeFileSync(sidecarPath, content, 'utf8');
632
- console.log(` ✅ Generated: ${sidecarPath}`);
633
- console.log(` ℹ️ Merge the "mcp" key into your settings.json manually.`);
634
- return;
635
- }
636
-
637
- if (fs.existsSync(targetInfo.path)) {
638
- // Backup existing file before overwriting
639
- const backup = targetInfo.path + '.backup.' + Date.now();
640
- fs.copyFileSync(targetInfo.path, backup);
641
- console.log(` 📦 Backed up existing: ${backup}`);
642
-
643
- // For JSON files, try to merge the mcpServers key
644
- try {
645
- const existing = JSON.parse(fs.readFileSync(targetInfo.path, 'utf8'));
646
- const generated = JSON.parse(content);
647
-
648
- if (targetInfo.format === 'copilot-cli' || targetInfo.format === 'claude') {
649
- existing.mcpServers = { ...existing.mcpServers, ...generated.mcpServers };
650
- fs.writeFileSync(targetInfo.path, JSON.stringify(existing, null, 2), 'utf8'); // lgtm[js/file-system-race] — setup wizard writes user-provided config target; race acceptable in CLI tooling
651
- console.log(` ✅ Merged into: ${targetInfo.path}`);
652
- return;
653
- }
654
- } catch {
655
- // Not valid JSON — fall through to overwrite
656
- }
657
- }
658
-
659
- fs.writeFileSync(targetInfo.path, content, 'utf8'); // lgtm[js/file-system-race] — setup wizard writes user-provided config target; race acceptable in CLI tooling
660
- console.log(` ✅ Written: ${targetInfo.path}`);
353
+ function applyConfigTarget(targetInfo, config) {
354
+ const result = mcpConfig.upsertServer({
355
+ target: targetInfo.target,
356
+ scope: targetInfo.format === 'vscode-global' ? 'global' : config.scope,
357
+ root: config.root,
358
+ name: config.serverName,
359
+ profile: config.profile,
360
+ port: config.port,
361
+ host: config.host,
362
+ tls: config.tls,
363
+ mutation: config.mutation,
364
+ logLevel: config.logLevel,
365
+ });
366
+ if (result.backupPath) console.log(` 📦 Backed up existing: ${result.backupPath}`);
367
+ console.log(` ✅ Written: ${result.path}`);
661
368
  }
662
369
 
663
370
  // --------------------------------------------------------------------------
@@ -783,7 +490,7 @@ async function deployRuntime(config) {
783
490
  type: 'commonjs',
784
491
  scripts: { start: 'node node_modules/@jagilber-org/index-server/dist/server/index-server.js' },
785
492
  };
786
- fs.writeFileSync(targetPkg, JSON.stringify(minPkg, null, 2), 'utf8');
493
+ writeTextFile(targetPkg, JSON.stringify(minPkg, null, 2));
787
494
  }
788
495
 
789
496
  console.log(' Installing package (this may take a moment)...');
@@ -840,7 +547,7 @@ async function deployRuntime(config) {
840
547
  runtimePkg.version = sourceVersion;
841
548
  runtimePkg.scripts = runtimePkg.scripts || {};
842
549
  runtimePkg.scripts.start = 'node dist/server/index-server.js';
843
- fs.writeFileSync(targetPkg, JSON.stringify(runtimePkg, null, 2) + '\n', 'utf8');
550
+ writeTextFile(targetPkg, JSON.stringify(runtimePkg, null, 2) + '\n');
844
551
  } catch { /* ok */ }
845
552
 
846
553
  // Verify the deployed server is actually runnable
@@ -914,10 +621,10 @@ Non-interactive mode:
914
621
 
915
622
  if (fs.existsSync(envPath)) { // lgtm[js/file-system-race]
916
623
  const genPath = path.join(config.root, '.env.generated');
917
- fs.writeFileSync(genPath, envContent, 'utf8');
624
+ writeTextFile(genPath, envContent);
918
625
  console.log(`\n⚠️ .env already exists. Written to: ${genPath}`);
919
626
  } else {
920
- fs.writeFileSync(envPath, envContent, 'utf8'); // lgtm[js/file-system-race] — setup wizard writes .env to user-supplied path; race acceptable in CLI tooling
627
+ writeTextFile(envPath, envContent); // lgtm[js/file-system-race] — setup wizard writes .env to user-supplied path; race acceptable in CLI tooling
921
628
  console.log(`\n✅ .env written to: ${envPath}`);
922
629
  }
923
630
 
@@ -926,37 +633,36 @@ Non-interactive mode:
926
633
 
927
634
  // Preview
928
635
  if (config.preview !== false) {
929
- previewConfigs(configTargets, config, paths);
636
+ previewConfigs(configTargets, config);
930
637
  }
931
638
 
932
639
  // Write to real files or sidecar
933
640
  if (config.write) {
934
641
  console.log('📁 Writing configuration files...\n');
935
642
  for (const ct of configTargets) {
936
- const content = generateConfigForTarget(ct.format, config, paths);
937
- writeConfigFile(ct, content);
643
+ applyConfigTarget(ct, config);
938
644
  }
939
645
  } else {
940
646
  // Legacy sidecar behavior for backward compatibility
941
- const mcpContent = generateMcpJson(config, paths);
647
+ const mcpContent = generateConfigForTarget('vscode', config);
942
648
  const mcpDir = path.join(config.root, '.vscode');
943
649
  const mcpPath = path.join(mcpDir, 'mcp.json.generated');
944
650
 
945
651
  try {
946
652
  fs.mkdirSync(mcpDir, { recursive: true });
947
653
  } catch { /* exists */ }
948
- fs.writeFileSync(mcpPath, mcpContent, 'utf8');
654
+ mcpConfig.writeGeneratedConfig(mcpPath, mcpContent);
949
655
  console.log(`✅ mcp.json snippet written to: ${mcpPath}`);
950
656
  console.log(' Copy its contents into your .vscode/mcp.json or VS Code user settings.');
951
657
 
952
658
  // Also generate Copilot CLI / Claude if requested
953
659
  for (const ct of configTargets) {
954
660
  if (ct.format !== 'vscode' && ct.format !== 'vscode-global') {
955
- const content = generateConfigForTarget(ct.format, config, paths);
661
+ const content = generateConfigForTarget(ct.format, config);
956
662
  const genPath = ct.path + '.generated';
957
663
  const dir = path.dirname(genPath);
958
664
  try { fs.mkdirSync(dir, { recursive: true }); } catch { /* exists */ }
959
- fs.writeFileSync(genPath, content, 'utf8');
665
+ mcpConfig.writeGeneratedConfig(genPath, content);
960
666
  console.log(`✅ ${ct.target} config written to: ${genPath}`);
961
667
  }
962
668
  }
@@ -1005,7 +711,7 @@ Non-interactive mode:
1005
711
  console.log(` ${step}. Copy generated config into your MCP client settings.`);
1006
712
 
1007
713
  for (const ct of configTargets) {
1008
- const genPath = ct.format === 'vscode' || ct.format === 'vscode-global'
714
+ const genPath = ct.format === 'vscode'
1009
715
  ? path.join(config.root, '.vscode', 'mcp.json.generated')
1010
716
  : ct.path + '.generated';
1011
717
  console.log(` ${ct.target}: ${genPath}`);
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/jagilber-org/index-server",
7
7
  "source": "github"
8
8
  },
9
- "version": "1.28.2",
9
+ "version": "1.28.5",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@jagilber-org/index-server",
14
- "version": "1.28.2",
14
+ "version": "1.28.5",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  }