@jagilber-org/index-server 1.28.1 → 1.28.4

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 (34) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +15 -3
  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 +301 -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/generate-certs.mjs +1 -1
  33. package/scripts/build/setup-wizard.mjs +71 -352
  34. package/server.json +2 -2
@@ -14,19 +14,36 @@
14
14
  * Usage:
15
15
  * npx @jagilber-org/index-server --setup
16
16
  * npm run setup
17
- * node scripts/setup-wizard.mjs
18
- * node scripts/setup-wizard.mjs --non-interactive --profile enhanced --root C:/mcp/index-server
17
+ * node scripts/build/setup-wizard.mjs
18
+ * node scripts/build/setup-wizard.mjs --non-interactive --profile enhanced --root C:/mcp/index-server
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';
35
+ function parsePositiveTimeout(value, fallback) {
36
+ const parsed = Number(value);
37
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
38
+ }
39
+ const DEPLOY_PACK_TIMEOUT_MS = parsePositiveTimeout(
40
+ process.env.INDEX_SERVER_SETUP_PACK_TIMEOUT_MS,
41
+ 30_000
42
+ );
43
+ const DEPLOY_INSTALL_TIMEOUT_MS = parsePositiveTimeout(
44
+ process.env.INDEX_SERVER_SETUP_INSTALL_TIMEOUT_MS,
45
+ IS_WINDOWS ? 420_000 : 120_000
46
+ );
30
47
 
31
48
  // --------------------------------------------------------------------------
32
49
  // Launch spec resolver — determines how to invoke index-server at runtime.
@@ -37,47 +54,7 @@ const IS_WINDOWS = process.platform === 'win32';
37
54
  // 'npx' — fallback when no dist/ found anywhere
38
55
  // --------------------------------------------------------------------------
39
56
  function resolveServerLaunch(config) {
40
- const entryRelative = 'dist/server/index-server.js';
41
- const localEntry = path.join(config.root, entryRelative);
42
- const packagedEntry = path.join(ROOT, entryRelative);
43
-
44
- // Case 1: config.root is the repo checkout with dist/ present
45
- if (fs.existsSync(localEntry)) {
46
- return {
47
- command: 'node',
48
- args: [entryRelative],
49
- cwd: config.root,
50
- source: 'local',
51
- };
52
- }
53
-
54
- // Case 2: dist/ exists in the package root (npx cache) but not in config.root
55
- if (fs.existsSync(packagedEntry)) {
56
- return {
57
- command: 'node',
58
- args: [fwd(packagedEntry)],
59
- cwd: config.root,
60
- source: 'packaged',
61
- };
62
- }
63
-
64
- // Case 3: no dist/ anywhere — use npx as last resort
65
- let pkgName = '@jagilber-org/index-server';
66
- let pkgVersion = '';
67
- try {
68
- const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
69
- if (pkg.name) pkgName = pkg.name;
70
- if (pkg.version) pkgVersion = `@${pkg.version}`;
71
- } catch {
72
- console.warn('⚠ Could not read package.json — npx will use latest published version');
73
- }
74
-
75
- return {
76
- command: 'npx',
77
- args: ['-y', `${pkgName}${pkgVersion}`],
78
- cwd: config.root,
79
- source: 'npx',
80
- };
57
+ return mcpConfig.resolveServerLaunch(config);
81
58
  }
82
59
 
83
60
  // --------------------------------------------------------------------------
@@ -112,9 +89,6 @@ function runNpm(args, opts = {}) {
112
89
  return execFileSync(npmBin, args, opts);
113
90
  }
114
91
 
115
- /** Resolve a sub-path under a root, always absolute and forward-slashed. */
116
- function resolveUnder(root, ...segments) { return fwd(path.resolve(root, ...segments)); }
117
-
118
92
  // --------------------------------------------------------------------------
119
93
  // Profile definitions
120
94
  // --------------------------------------------------------------------------
@@ -161,21 +135,7 @@ const PROFILES = {
161
135
  // Resolve all paths for a given root
162
136
  // --------------------------------------------------------------------------
163
137
  function resolvePaths(root) {
164
- return {
165
- instructions: resolveUnder(root, 'instructions'),
166
- feedback: resolveUnder(root, 'feedback'),
167
- backups: resolveUnder(root, 'backups'),
168
- state: resolveUnder(root, 'data', 'state'),
169
- auditLog: resolveUnder(root, 'logs', 'instruction-transactions.log.jsonl'),
170
- logFile: resolveUnder(root, 'logs', 'mcp-server.log'),
171
- metrics: resolveUnder(root, 'metrics'),
172
- messaging: resolveUnder(root, 'data', 'messaging'),
173
- embeddings: resolveUnder(root, 'data', 'embeddings.json'),
174
- modelCache: resolveUnder(root, 'data', 'models'),
175
- sqliteDb: resolveUnder(root, 'data', 'index.db'),
176
- certs: resolveUnder(root, 'certs'),
177
- flags: resolveUnder(root, 'flags.json'),
178
- };
138
+ return mcpConfig.resolveDataPaths(root);
179
139
  }
180
140
 
181
141
  // --------------------------------------------------------------------------
@@ -345,259 +305,42 @@ async function runInteractiveWizard() {
345
305
  // - value: function(config, paths) => string value
346
306
  // --------------------------------------------------------------------------
347
307
  function getEnvCatalog(config, paths) {
348
- const p = config.profile;
349
- const isEnhanced = p === 'enhanced' || p === 'experimental';
350
- const isSqlite = p === 'experimental';
351
- const tls = config.tls;
352
-
353
- return [
354
- // ── Core Paths ─────────────────────────────────────────────────────────
355
- { section: 'Core Paths — where your data lives' },
356
- { key: 'INDEX_SERVER_PROFILE', desc: 'Configuration profile: default | enhanced | experimental', active: true, value: p },
357
- { key: 'INDEX_SERVER_DIR', desc: 'Instruction catalog directory (your knowledge base)', active: true, value: paths.instructions },
358
- { key: 'INDEX_SERVER_FEEDBACK_DIR', desc: 'Feedback entries storage directory', active: true, value: paths.feedback },
359
- { key: 'INDEX_SERVER_BACKUPS_DIR', desc: 'Backup snapshots directory', active: true, value: paths.backups },
360
- { key: 'INDEX_SERVER_STATE_DIR', desc: 'Runtime state files directory', active: true, value: paths.state },
361
- { key: 'INDEX_SERVER_MESSAGING_DIR',desc: 'Message queue storage directory', active: true, value: paths.messaging },
362
-
363
- // ── Dashboard (HTTP Admin UI) ──────────────────────────────────────────
364
- { section: 'Dashboard — HTTP/HTTPS admin interface' },
365
- { key: 'INDEX_SERVER_DASHBOARD', desc: 'Enable the web dashboard (0=off, 1=on)', active: true, value: '1' },
366
- { key: 'INDEX_SERVER_DASHBOARD_PORT', desc: 'Dashboard listen port', active: true, value: String(config.port) },
367
- { key: 'INDEX_SERVER_DASHBOARD_HOST', desc: 'Dashboard bind address (127.0.0.1=local, 0.0.0.0=all)', active: true, value: config.host },
368
- { key: 'INDEX_SERVER_DASHBOARD_GRAPH', desc: 'Enable instruction graph visualization (0=off, 1=on)', active: false, value: '0' },
369
-
370
- // ── Security & Mutation ────────────────────────────────────────────────
371
- { section: 'Security — mutation control, TLS, authentication' },
372
- { key: 'INDEX_SERVER_MUTATION', desc: 'Enable write operations: add, update, delete (0=off, 1=on)', active: true, value: config.mutation ? '1' : '0' },
373
- { key: 'INDEX_SERVER_ADMIN_API_KEY', desc: 'Dashboard admin API key (set a strong random value)', active: false, value: '' },
374
- { key: 'INDEX_SERVER_DASHBOARD_TLS', desc: 'Enable HTTPS for dashboard (0=off, 1=on)', active: tls, value: tls ? '1' : '0' },
375
- { key: 'INDEX_SERVER_DASHBOARD_TLS_CERT', desc: 'Path to TLS certificate file (.crt/.pem)', active: tls, value: tls ? resolveUnder(paths.certs, 'server.crt') : '' },
376
- { key: 'INDEX_SERVER_DASHBOARD_TLS_KEY', desc: 'Path to TLS private key file (.key/.pem)', active: tls, value: tls ? resolveUnder(paths.certs, 'server.key') : '' },
377
- { key: 'INDEX_SERVER_DASHBOARD_TLS_CA', desc: 'Path to CA certificate for client verification (optional)', active: false, value: '' },
378
-
379
- // ── Semantic Search & Embeddings ───────────────────────────────────────
380
- { section: 'Semantic Search — AI-powered instruction search' },
381
- { key: 'INDEX_SERVER_SEMANTIC_ENABLED', desc: 'Enable semantic (vector) search (0=off, 1=on)', active: isEnhanced, value: isEnhanced ? '1' : '0' },
382
- { key: 'INDEX_SERVER_SEMANTIC_MODEL', desc: 'HuggingFace model name for embeddings', active: false, value: 'Xenova/all-MiniLM-L6-v2' },
383
- { key: 'INDEX_SERVER_SEMANTIC_DEVICE', desc: 'Compute device: cpu | cuda | dml (Windows ML)', active: false, value: 'cpu' },
384
- { key: 'INDEX_SERVER_SEMANTIC_CACHE_DIR', desc: 'Directory for downloaded model files (~90MB)', active: isEnhanced, value: paths.modelCache },
385
- { key: 'INDEX_SERVER_EMBEDDING_PATH', desc: 'Cached embeddings file (grows with catalog size)', active: isEnhanced, value: paths.embeddings },
386
- { key: 'INDEX_SERVER_SEMANTIC_LOCAL_ONLY', desc: 'Block remote model downloads (0=allow download, 1=local only)', active: isEnhanced, value: isEnhanced ? '0' : '1' },
387
-
388
- // ── Storage Backend ────────────────────────────────────────────────────
389
- { section: 'Storage Backend — JSON (default) or SQLite (experimental)' },
390
- { key: 'INDEX_SERVER_STORAGE_BACKEND', desc: 'Storage engine: json | sqlite', active: isSqlite, value: isSqlite ? 'sqlite' : 'json' },
391
- { key: 'INDEX_SERVER_SQLITE_PATH', desc: 'SQLite database file path', active: isSqlite, value: paths.sqliteDb },
392
- { key: 'INDEX_SERVER_SQLITE_WAL', desc: 'Enable Write-Ahead Logging for SQLite (0=off, 1=on)', active: isSqlite, value: '1' },
393
- { key: 'INDEX_SERVER_SQLITE_MIGRATE_ON_START', desc: 'Auto-migrate JSON to SQLite on startup (0=off, 1=on)', active: isSqlite, value: '1' },
394
-
395
- // ── Logging & Diagnostics ──────────────────────────────────────────────
396
- { section: 'Logging — log level, file output, diagnostics' },
397
- { key: 'INDEX_SERVER_LOG_LEVEL', desc: 'Log level: error | warn | info | debug | trace', active: true, value: config.logLevel },
398
- { key: 'INDEX_SERVER_LOG_FILE', desc: 'Enable file logging (0=off, 1=default path, or absolute path)', active: isEnhanced, value: isEnhanced ? '1' : '0' },
399
- { key: 'INDEX_SERVER_VERBOSE_LOGGING', desc: 'Verbose stderr output (0=off, 1=on)', active: false, value: '0' },
400
- { key: 'INDEX_SERVER_LOG_JSON', desc: 'JSON-formatted log output (0=off, 1=on)', active: false, value: '0' },
401
- { key: 'INDEX_SERVER_LOG_DIAG', desc: 'Diagnostic startup logging (0=off, 1=on)', active: false, value: '0' },
402
- { key: 'INDEX_SERVER_AUDIT_LOG', desc: 'Audit log path (1=default, 0=disabled, or absolute path)', active: true, value: paths.auditLog },
403
-
404
- // ── Backup & Recovery ──────────────────────────────────────────────────
405
- { section: 'Backup — automatic backup scheduling' },
406
- { 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' },
407
- { key: 'INDEX_SERVER_AUTO_BACKUP_INTERVAL_MS', desc: 'Backup interval in ms (default: 3600000 = 1 hour)', active: false, value: '3600000' },
408
- { key: 'INDEX_SERVER_AUTO_BACKUP_MAX_COUNT', desc: 'Max backup snapshots to retain (default: 10)', active: false, value: '10' },
409
- { key: 'INDEX_SERVER_BACKUP_BEFORE_BULK_DELETE',desc: 'Auto-backup before bulk delete operations (0=off, 1=on)', active: false, value: '1' },
410
-
411
- // ── Features & Flags ───────────────────────────────────────────────────
412
- { section: 'Features — feature flags and capabilities' },
413
- { key: 'INDEX_SERVER_FEATURES', desc: 'Comma-separated feature flags: usage,window,hotness,drift,risk', active: isEnhanced, value: isEnhanced ? 'usage' : '' },
414
- { key: 'INDEX_SERVER_METRICS_FILE_STORAGE', desc: 'Persist metrics to disk (0=off, 1=on)', active: isEnhanced, value: isEnhanced ? '1' : '0' },
415
- { key: 'INDEX_SERVER_METRICS_DIR', desc: 'Metrics storage directory', active: isEnhanced, value: paths.metrics },
416
- { key: 'INDEX_SERVER_FLAGS_FILE', desc: 'Feature flags JSON file path', active: false, value: paths.flags },
417
-
418
- // ── Server & Transport ─────────────────────────────────────────────────
419
- { section: 'Server — MCP transport and instance mode' },
420
- { key: 'INDEX_SERVER_MODE', desc: 'Instance mode: standalone | leader | follower | auto', active: false, value: 'standalone' },
421
- { key: 'INDEX_SERVER_DISABLE_EARLY_STDIN_BUFFER',desc: 'Disable stdin handshake hardening (0=off, 1=on)', active: false, value: '0' },
422
- { key: 'INDEX_SERVER_IDLE_KEEPALIVE_MS', desc: 'Keepalive interval in ms (default: 30000)', active: false, value: '30000' },
423
- { key: 'INDEX_SERVER_POLL_MS', desc: 'Index filesystem poll interval in ms (default: 10000)', active: false, value: '10000' },
424
-
425
- // ── Advanced Tuning ────────────────────────────────────────────────────
426
- { section: 'Advanced — tuning, limits, governance (most users can skip)' },
427
- { key: 'INDEX_SERVER_BODY_WARN_LENGTH', desc: 'Max instruction body length in chars (default: 100000)', active: false, value: '100000' },
428
- { key: 'INDEX_SERVER_AUTO_SPLIT_OVERSIZED', desc: 'Auto-split oversized entries on load (0=off, 1=on)', active: false, value: '0' },
429
- { key: 'INDEX_SERVER_READ_RETRIES', desc: 'File read retry attempts (default: 3)', active: false, value: '3' },
430
- { key: 'INDEX_SERVER_MAX_BULK_DELETE', desc: 'Max entries in a single bulk delete (default: 5)', active: false, value: '5' },
431
- { key: 'INDEX_SERVER_FEEDBACK_MAX_ENTRIES', desc: 'Max feedback entries before rotation (default: 1000)', active: false, value: '1000' },
432
- { key: 'INDEX_SERVER_MESSAGING_MAX', desc: 'Max messages in queue (default: 10000)', active: false, value: '10000' },
433
- { key: 'INDEX_SERVER_MAX_CONNECTIONS', desc: 'Max concurrent dashboard connections (default: 100)', active: false, value: '100' },
434
- { key: 'INDEX_SERVER_CACHE_MODE', desc: 'Index cache mode: normal | memoize | memoize+hash | reload | reload+memo', active: false, value: 'normal' },
435
- { key: 'INDEX_SERVER_WORKSPACE', desc: 'Workspace identifier for multi-tenant setups', active: false, value: '' },
436
- { key: 'INDEX_SERVER_AGENT_ID', desc: 'Agent identifier for audit trails', active: false, value: '' },
437
- ];
438
- }
439
-
440
- // --------------------------------------------------------------------------
441
- // Generate .vscode/mcp.json snippet (JSONC with comments)
442
- // --------------------------------------------------------------------------
443
- function generateMcpJson(config, paths) {
444
- const catalog = getEnvCatalog(config, paths);
445
- const indent = '\t\t\t\t';
446
- const launch = resolveServerLaunch(config);
447
- const argsJson = JSON.stringify(launch.args);
448
-
449
- const lines = [
450
- '{',
451
- '\t"servers": {',
452
- `\t\t"${config.serverName}": {`,
453
- '\t\t\t"type": "stdio",',
454
- `\t\t\t"cwd": "${fwd(launch.cwd)}",`,
455
- `\t\t\t"command": "${launch.command}",`,
456
- `\t\t\t"args": ${argsJson},`,
457
- '\t\t\t"env": {',
458
- ];
459
-
460
- let firstSection = true;
461
- for (const entry of catalog) {
462
- if (entry.section) {
463
- if (!firstSection) lines.push('');
464
- lines.push(`${indent}// ── ${entry.section} ${'─'.repeat(Math.max(0, 58 - entry.section.length))}`);
465
- firstSection = false;
466
- continue;
467
- }
468
-
469
- const comment = `// ${entry.desc}`;
470
- if (entry.active) {
471
- lines.push(`${indent}${comment}`);
472
- lines.push(`${indent}"${entry.key}": "${entry.value}",`);
473
- } else {
474
- lines.push(`${indent}${comment}`);
475
- lines.push(`${indent}// "${entry.key}": "${entry.value}",`);
476
- }
477
- }
478
-
479
- lines.push('\t\t\t}');
480
- lines.push('\t\t}');
481
- lines.push('\t},');
482
- lines.push('\t"inputs": []');
483
- lines.push('}');
484
-
485
- return lines.join('\n');
486
- }
487
-
488
- // --------------------------------------------------------------------------
489
- // Generate Copilot CLI mcp-config.json format
490
- // --------------------------------------------------------------------------
491
- function generateCopilotCliJson(config, paths) {
492
- const catalog = getEnvCatalog(config, paths);
493
- const env = {};
494
- for (const entry of catalog) {
495
- if (entry.section) continue;
496
- if (entry.active) env[entry.key] = entry.value;
497
- }
498
- const launch = resolveServerLaunch(config);
499
- // copilot-cli doesn't reliably inherit cwd — use absolute args for local/packaged
500
- const args = launch.source === 'local'
501
- ? [fwd(path.resolve(launch.cwd, launch.args[0]))]
502
- : launch.args;
503
- const obj = {
504
- mcpServers: {
505
- [config.serverName]: {
506
- command: launch.command,
507
- args,
508
- env,
509
- },
510
- },
511
- };
512
- return JSON.stringify(obj, null, 2);
513
- }
514
-
515
- // --------------------------------------------------------------------------
516
- // Generate Claude Desktop config JSON format
517
- // --------------------------------------------------------------------------
518
- function generateClaudeDesktopJson(config, paths) {
519
- const catalog = getEnvCatalog(config, paths);
520
- const env = {};
521
- for (const entry of catalog) {
522
- if (entry.section) continue;
523
- if (entry.active) env[entry.key] = entry.value;
524
- }
525
- const launch = resolveServerLaunch(config);
526
- // Claude Desktop doesn't support cwd — use absolute args for local/packaged
527
- const args = launch.source === 'local'
528
- ? [fwd(path.resolve(launch.cwd, launch.args[0]))]
529
- : launch.args;
530
- const obj = {
531
- mcpServers: {
532
- [config.serverName]: {
533
- command: launch.command,
534
- args,
535
- env,
536
- },
537
- },
538
- };
539
- return JSON.stringify(obj, null, 2);
308
+ return mcpConfig.buildEnvCatalog(config, paths);
540
309
  }
541
310
 
542
311
  // --------------------------------------------------------------------------
543
312
  // Resolve target config file paths based on scope and OS
544
313
  // --------------------------------------------------------------------------
545
314
  function resolveConfigPaths(config) {
546
- const home = os.homedir();
547
- const results = [];
548
- const isWin = process.platform === 'win32';
549
-
550
- for (const target of (config.targets || ['vscode'])) {
551
- if (target === 'vscode') {
552
- if (config.scope === 'global') {
553
- const dir = isWin
554
- ? path.join(process.env.APPDATA || path.join(home, 'AppData', 'Roaming'), 'Code', 'User')
555
- : path.join(home, '.config', 'Code', 'User');
556
- results.push({ target, path: path.join(dir, 'settings.json'), format: 'vscode-global' });
557
- } else {
558
- results.push({ target, path: path.join(config.root, '.vscode', 'mcp.json'), format: 'vscode' });
559
- }
560
- } else if (target === 'copilot-cli') {
561
- const dir = isWin
562
- ? path.join(process.env.USERPROFILE || home, '.copilot')
563
- : path.join(home, '.copilot');
564
- results.push({ target, path: path.join(dir, 'mcp-config.json'), format: 'copilot-cli' });
565
- } else if (target === 'claude') {
566
- const dir = isWin
567
- ? path.join(process.env.APPDATA || path.join(home, 'AppData', 'Roaming'), 'Claude')
568
- : path.join(home, 'Library', 'Application Support', 'Claude');
569
- results.push({ target, path: path.join(dir, 'claude_desktop_config.json'), format: 'claude' });
570
- }
571
- }
572
- return results;
315
+ return mcpConfig.resolveConfigTargets({ targets: config.targets, scope: config.scope, root: config.root });
573
316
  }
574
317
 
575
318
  // --------------------------------------------------------------------------
576
319
  // Generate config content for a given format
577
320
  // --------------------------------------------------------------------------
578
- function generateConfigForTarget(format, config, paths) {
579
- switch (format) {
580
- case 'vscode':
581
- case 'vscode-global':
582
- return generateMcpJson(config, paths);
583
- case 'copilot-cli':
584
- return generateCopilotCliJson(config, paths);
585
- case 'claude':
586
- return generateClaudeDesktopJson(config, paths);
587
- default:
588
- return generateMcpJson(config, paths);
589
- }
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
+ });
590
333
  }
591
334
 
592
335
  // --------------------------------------------------------------------------
593
336
  // Preview all generated configs
594
337
  // --------------------------------------------------------------------------
595
- function previewConfigs(configTargets, config, paths) {
338
+ function previewConfigs(configTargets, config) {
596
339
  console.log('\n┌─────────────────────────────────────────────────────────────────────┐');
597
340
  console.log('│ 📋 Configuration Preview │');
598
341
  console.log('└─────────────────────────────────────────────────────────────────────┘');
599
342
  for (const ct of configTargets) {
600
- const content = generateConfigForTarget(ct.format, config, paths);
343
+ const content = generateConfigForTarget(ct.format, config);
601
344
  console.log(`\n── ${ct.target} → ${ct.path} ──\n`);
602
345
  console.log(content);
603
346
  }
@@ -607,45 +350,21 @@ function previewConfigs(configTargets, config, paths) {
607
350
  // --------------------------------------------------------------------------
608
351
  // Write config to real file (merge if existing)
609
352
  // --------------------------------------------------------------------------
610
- function writeConfigFile(targetInfo, content) {
611
- const dir = path.dirname(targetInfo.path);
612
- if (!fs.existsSync(dir)) {
613
- fs.mkdirSync(dir, { recursive: true });
614
- }
615
-
616
- if (targetInfo.format === 'vscode-global' && fs.existsSync(targetInfo.path)) {
617
- // Write sidecar to avoid corrupting settings.json
618
- const sidecarPath = targetInfo.path.replace('settings.json', 'mcp.json.generated');
619
- fs.writeFileSync(sidecarPath, content, 'utf8');
620
- console.log(` ✅ Generated: ${sidecarPath}`);
621
- console.log(` ℹ️ Merge the "mcp" key into your settings.json manually.`);
622
- return;
623
- }
624
-
625
- if (fs.existsSync(targetInfo.path)) {
626
- // Backup existing file before overwriting
627
- const backup = targetInfo.path + '.backup.' + Date.now();
628
- fs.copyFileSync(targetInfo.path, backup);
629
- console.log(` 📦 Backed up existing: ${backup}`);
630
-
631
- // For JSON files, try to merge the mcpServers key
632
- try {
633
- const existing = JSON.parse(fs.readFileSync(targetInfo.path, 'utf8'));
634
- const generated = JSON.parse(content);
635
-
636
- if (targetInfo.format === 'copilot-cli' || targetInfo.format === 'claude') {
637
- existing.mcpServers = { ...existing.mcpServers, ...generated.mcpServers };
638
- 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
639
- console.log(` ✅ Merged into: ${targetInfo.path}`);
640
- return;
641
- }
642
- } catch {
643
- // Not valid JSON — fall through to overwrite
644
- }
645
- }
646
-
647
- fs.writeFileSync(targetInfo.path, content, 'utf8'); // lgtm[js/file-system-race] — setup wizard writes user-provided config target; race acceptable in CLI tooling
648
- 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}`);
649
368
  }
650
369
 
651
370
  // --------------------------------------------------------------------------
@@ -771,7 +490,7 @@ async function deployRuntime(config) {
771
490
  type: 'commonjs',
772
491
  scripts: { start: 'node node_modules/@jagilber-org/index-server/dist/server/index-server.js' },
773
492
  };
774
- fs.writeFileSync(targetPkg, JSON.stringify(minPkg, null, 2), 'utf8');
493
+ writeTextFile(targetPkg, JSON.stringify(minPkg, null, 2));
775
494
  }
776
495
 
777
496
  console.log(' Installing package (this may take a moment)...');
@@ -781,7 +500,7 @@ async function deployRuntime(config) {
781
500
  // and produces a proper self-contained node_modules tree.
782
501
  const packOutput = runNpm(
783
502
  ['pack', '--pack-destination', targetRoot],
784
- { cwd: ROOT, stdio: ['pipe', 'pipe', 'inherit'], timeout: 30_000 }
503
+ { cwd: ROOT, stdio: ['pipe', 'pipe', 'inherit'], timeout: DEPLOY_PACK_TIMEOUT_MS }
785
504
  ).toString().trim();
786
505
  const tarballName = packOutput.split('\n').pop();
787
506
 
@@ -789,9 +508,10 @@ async function deployRuntime(config) {
789
508
 
790
509
  try {
791
510
  // Install from the local tarball
511
+ console.log(` npm install timeout: ${DEPLOY_INSTALL_TIMEOUT_MS}ms`);
792
512
  runNpm(
793
513
  ['install', tarballPath, '--omit=dev', '--no-fund', '--no-audit'],
794
- { cwd: targetRoot, stdio: 'inherit', timeout: 120_000 }
514
+ { cwd: targetRoot, stdio: 'inherit', timeout: DEPLOY_INSTALL_TIMEOUT_MS }
795
515
  );
796
516
  } finally {
797
517
  // Clean up tarball
@@ -827,7 +547,7 @@ async function deployRuntime(config) {
827
547
  runtimePkg.version = sourceVersion;
828
548
  runtimePkg.scripts = runtimePkg.scripts || {};
829
549
  runtimePkg.scripts.start = 'node dist/server/index-server.js';
830
- fs.writeFileSync(targetPkg, JSON.stringify(runtimePkg, null, 2) + '\n', 'utf8');
550
+ writeTextFile(targetPkg, JSON.stringify(runtimePkg, null, 2) + '\n');
831
551
  } catch { /* ok */ }
832
552
 
833
553
  // Verify the deployed server is actually runnable
@@ -860,10 +580,10 @@ async function main() {
860
580
  Interactive mode:
861
581
  npx @jagilber-org/index-server --setup
862
582
  npm run setup
863
- node scripts/setup-wizard.mjs
583
+ node scripts/build/setup-wizard.mjs
864
584
 
865
585
  Non-interactive mode:
866
- node scripts/setup-wizard.mjs --non-interactive [options]
586
+ node scripts/build/setup-wizard.mjs --non-interactive [options]
867
587
  --profile <name> default | enhanced | experimental
868
588
  --root <dir> Base directory for all data paths
869
589
  --port <n> Dashboard port (default: 8787)
@@ -901,10 +621,10 @@ Non-interactive mode:
901
621
 
902
622
  if (fs.existsSync(envPath)) { // lgtm[js/file-system-race]
903
623
  const genPath = path.join(config.root, '.env.generated');
904
- fs.writeFileSync(genPath, envContent, 'utf8');
624
+ writeTextFile(genPath, envContent);
905
625
  console.log(`\n⚠️ .env already exists. Written to: ${genPath}`);
906
626
  } else {
907
- 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
908
628
  console.log(`\n✅ .env written to: ${envPath}`);
909
629
  }
910
630
 
@@ -913,37 +633,36 @@ Non-interactive mode:
913
633
 
914
634
  // Preview
915
635
  if (config.preview !== false) {
916
- previewConfigs(configTargets, config, paths);
636
+ previewConfigs(configTargets, config);
917
637
  }
918
638
 
919
639
  // Write to real files or sidecar
920
640
  if (config.write) {
921
641
  console.log('📁 Writing configuration files...\n');
922
642
  for (const ct of configTargets) {
923
- const content = generateConfigForTarget(ct.format, config, paths);
924
- writeConfigFile(ct, content);
643
+ applyConfigTarget(ct, config);
925
644
  }
926
645
  } else {
927
646
  // Legacy sidecar behavior for backward compatibility
928
- const mcpContent = generateMcpJson(config, paths);
647
+ const mcpContent = generateConfigForTarget('vscode', config);
929
648
  const mcpDir = path.join(config.root, '.vscode');
930
649
  const mcpPath = path.join(mcpDir, 'mcp.json.generated');
931
650
 
932
651
  try {
933
652
  fs.mkdirSync(mcpDir, { recursive: true });
934
653
  } catch { /* exists */ }
935
- fs.writeFileSync(mcpPath, mcpContent, 'utf8');
654
+ mcpConfig.writeGeneratedConfig(mcpPath, mcpContent);
936
655
  console.log(`✅ mcp.json snippet written to: ${mcpPath}`);
937
656
  console.log(' Copy its contents into your .vscode/mcp.json or VS Code user settings.');
938
657
 
939
658
  // Also generate Copilot CLI / Claude if requested
940
659
  for (const ct of configTargets) {
941
660
  if (ct.format !== 'vscode' && ct.format !== 'vscode-global') {
942
- const content = generateConfigForTarget(ct.format, config, paths);
661
+ const content = generateConfigForTarget(ct.format, config);
943
662
  const genPath = ct.path + '.generated';
944
663
  const dir = path.dirname(genPath);
945
664
  try { fs.mkdirSync(dir, { recursive: true }); } catch { /* exists */ }
946
- fs.writeFileSync(genPath, content, 'utf8');
665
+ mcpConfig.writeGeneratedConfig(genPath, content);
947
666
  console.log(`✅ ${ct.target} config written to: ${genPath}`);
948
667
  }
949
668
  }
@@ -959,12 +678,12 @@ Non-interactive mode:
959
678
  const certDir = path.join(config.root, 'certs');
960
679
  execFileSync(
961
680
  process.execPath,
962
- [path.join(ROOT, 'scripts', 'generate-certs.mjs'), '--hostname', 'localhost', '--output', certDir],
681
+ [path.join(ROOT, 'scripts', 'build', 'generate-certs.mjs'), '--hostname', 'localhost', '--output', certDir],
963
682
  { stdio: 'inherit' }
964
683
  );
965
684
  } catch {
966
685
  console.error('❌ Certificate generation failed. Run manually:');
967
- console.error(` node scripts/generate-certs.mjs --output "${path.join(config.root, 'certs')}"`);
686
+ console.error(` node scripts/build/generate-certs.mjs --output "${path.join(config.root, 'certs')}"`);
968
687
  }
969
688
  }
970
689
 
@@ -992,7 +711,7 @@ Non-interactive mode:
992
711
  console.log(` ${step}. Copy generated config into your MCP client settings.`);
993
712
 
994
713
  for (const ct of configTargets) {
995
- const genPath = ct.format === 'vscode' || ct.format === 'vscode-global'
714
+ const genPath = ct.format === 'vscode'
996
715
  ? path.join(config.root, '.vscode', 'mcp.json.generated')
997
716
  : ct.path + '.generated';
998
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.1",
9
+ "version": "1.28.4",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@jagilber-org/index-server",
14
- "version": "1.28.1",
14
+ "version": "1.28.4",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  }