enigma-memory 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -235,14 +235,19 @@ export function getClientProfile(clientIdOrOptions = 'generic-mcp', maybeOptions
235
235
  });
236
236
  }
237
237
 
238
+ function serverEnvFromOptions(options = {}) {
239
+ return options.serverEnv ?? options.server_env ?? options.mcpEnv ?? options.mcp_env;
240
+ }
241
+
238
242
  function serverEntryFromOptions(options = {}) {
239
243
  const env = {};
240
- if (isPlainObject(options.env)) {
241
- for (const [key, value] of Object.entries(options.env)) {
244
+ const serverEnv = serverEnvFromOptions(options);
245
+ if (isPlainObject(serverEnv)) {
246
+ for (const [key, value] of Object.entries(serverEnv)) {
242
247
  if (value !== undefined && value !== null) env[key] = String(value);
243
248
  }
244
249
  }
245
- const bundlePath = String(options.bundlePath ?? options.bundle_path ?? env.ENIGMA_BUNDLE ?? defaultBundlePath(options));
250
+ const bundlePath = String(options.bundlePath ?? options.bundle_path ?? env.ENIGMA_BUNDLE ?? options.env?.ENIGMA_BUNDLE ?? defaultBundlePath(options));
246
251
  env.ENIGMA_BUNDLE = bundlePath;
247
252
  return {
248
253
  command: mcpCommandFromOptions(options),
@@ -309,10 +314,18 @@ async function unusedBackupPath(configPath, now) {
309
314
  throw new Error(`Cannot allocate backup path for ${configPath}.`);
310
315
  }
311
316
 
312
- async function readJsonConfig(configPath) {
317
+ function optionFileReader(options = {}) {
318
+ const reader = options.readFile ?? options.read_file ?? options.fs?.readFile ?? options.fileSystem?.readFile;
319
+ if (reader === undefined) return { reader: readFile, thisArg: undefined };
320
+ if (typeof reader !== 'function') throw new Error('Connector readFile option must be a function.');
321
+ return { reader, thisArg: options.fs ?? options.fileSystem };
322
+ }
323
+
324
+ async function readJsonConfig(configPath, options = {}) {
313
325
  let text;
314
326
  try {
315
- text = await readFile(configPath, 'utf8');
327
+ const { reader, thisArg } = optionFileReader(options);
328
+ text = await reader.call(thisArg, configPath, 'utf8');
316
329
  } catch (error) {
317
330
  if (error?.code === 'ENOENT') return { exists: false, config: {} };
318
331
  throw error;
@@ -322,9 +335,15 @@ async function readJsonConfig(configPath) {
322
335
  try {
323
336
  config = JSON.parse(text);
324
337
  } catch (error) {
325
- throw new Error(`Cannot parse JSON connector config at ${configPath}: ${error.message}`);
338
+ const parseError = new Error(`Cannot parse JSON connector config at ${configPath}: ${error.message}`);
339
+ parseError.code = 'EJSONPARSE';
340
+ throw parseError;
341
+ }
342
+ if (!isPlainObject(config)) {
343
+ const typeError = new Error(`Connector config at ${configPath} must be a JSON object.`);
344
+ typeError.code = 'EJSONTYPE';
345
+ throw typeError;
326
346
  }
327
- if (!isPlainObject(config)) throw new Error(`Connector config at ${configPath} must be a JSON object.`);
328
347
  return { exists: true, config };
329
348
  }
330
349
 
@@ -412,7 +431,7 @@ export async function connectClient(clientIdOrOptions = 'generic-mcp', maybeOpti
412
431
  const options = normalizeOptions(clientIdOrOptions, maybeOptions);
413
432
  const clientId = normalizeClientId(options.clientId ?? options.client_id);
414
433
  const configPath = String(options.configPath ?? options.config_path ?? platformDefaultConfigPath(clientId, options));
415
- const { exists, config } = await readJsonConfig(configPath);
434
+ const { exists, config } = await readJsonConfig(configPath, options);
416
435
  const plan = connectPlan({ clientId, options: { ...options, configPath }, exists, existingConfig: config, backupPath: null });
417
436
  if (exists && plan.changed) {
418
437
  plan.backupPath = await unusedBackupPath(configPath, options.now);
@@ -425,7 +444,7 @@ export async function disconnectClient(clientIdOrOptions = 'generic-mcp', maybeO
425
444
  const options = normalizeOptions(clientIdOrOptions, maybeOptions);
426
445
  const clientId = normalizeClientId(options.clientId ?? options.client_id);
427
446
  const configPath = String(options.configPath ?? options.config_path ?? platformDefaultConfigPath(clientId, options));
428
- const { exists, config } = await readJsonConfig(configPath);
447
+ const { exists, config } = await readJsonConfig(configPath, options);
429
448
  const plan = disconnectPlan({ clientId, options: { ...options, configPath }, exists, existingConfig: config, backupPath: null });
430
449
  if (exists && plan.changed) {
431
450
  plan.backupPath = await unusedBackupPath(configPath, options.now);
@@ -434,53 +453,266 @@ export async function disconnectClient(clientIdOrOptions = 'generic-mcp', maybeO
434
453
  return writePlan(plan, exists);
435
454
  }
436
455
 
456
+ function emptyInstalledState() {
457
+ return {
458
+ installed: false,
459
+ serverEntryExists: false,
460
+ server_entry_exists: false,
461
+ commandOk: false,
462
+ command_ok: false,
463
+ argsOk: false,
464
+ args_ok: false,
465
+ bundleEnvPresent: false,
466
+ bundle_env_present: false,
467
+ bundleEnvOk: false,
468
+ bundle_env_ok: false,
469
+ envOk: false,
470
+ env_ok: false,
471
+ };
472
+ }
473
+
437
474
  function installedState(config, profile, serverName, options = {}) {
438
475
  const container = ensureContainer(config, profile.server_container_path, false);
439
- if (!container || !isPlainObject(container[serverName])) return { installed: false, commandOk: false, bundleEnvOk: false };
476
+ if (!container || !isPlainObject(container[serverName])) return emptyInstalledState();
440
477
  const entry = container[serverName];
478
+ const expectedEntry = serverEntryFromOptions(options);
479
+ const actualArgs = Array.isArray(entry.args) ? [...entry.args].map(String) : [];
480
+ const actualEnv = {};
481
+ if (isPlainObject(entry.env)) {
482
+ for (const [key, value] of Object.entries(entry.env)) {
483
+ if (value !== undefined && value !== null) actualEnv[key] = String(value);
484
+ }
485
+ }
486
+ const actualBundlePath = typeof actualEnv.ENIGMA_BUNDLE === 'string' ? actualEnv.ENIGMA_BUNDLE : '';
487
+ const commandOk = entry.command === expectedEntry.command;
488
+ const argsOk = jsonEqual(actualArgs, expectedEntry.args);
489
+ const bundleEnvOk = actualBundlePath === expectedEntry.env.ENIGMA_BUNDLE;
490
+ const envOk = jsonEqual(actualEnv, expectedEntry.env);
441
491
  return {
442
492
  installed: true,
443
- commandOk: entry.command === mcpCommandFromOptions(options),
444
- bundleEnvOk: typeof entry.env?.ENIGMA_BUNDLE === 'string' && entry.env.ENIGMA_BUNDLE.length > 0,
493
+ serverEntryExists: true,
494
+ server_entry_exists: true,
495
+ commandOk,
496
+ command_ok: commandOk,
497
+ argsOk,
498
+ args_ok: argsOk,
499
+ bundleEnvPresent: actualBundlePath.length > 0,
500
+ bundle_env_present: actualBundlePath.length > 0,
501
+ bundleEnvOk,
502
+ bundle_env_ok: bundleEnvOk,
503
+ envOk,
504
+ env_ok: envOk,
445
505
  };
446
506
  }
447
507
 
448
- export async function doctorConnectors(clientIdOrOptions = {}, maybeOptions = {}) {
508
+ function recommendedConnectorAction(exists, state, error) {
509
+ if (error) return 'repair';
510
+ if (!exists) return 'missing_client_config';
511
+ if (!state.installed) return 'connect';
512
+ return state.commandOk && state.argsOk && state.envOk ? 'already_configured' : 'repair';
513
+ }
514
+
515
+ function connectorRepairReasons(exists, state, error) {
516
+ if (error?.code === 'EJSONPARSE') return ['config_json_invalid'];
517
+ if (error?.code === 'EJSONTYPE') return ['config_json_not_object'];
518
+ if (error) return ['config_unreadable'];
519
+ if (!exists) return ['client_config_missing'];
520
+ if (!state.installed) return ['enigma_server_missing'];
521
+ const reasons = [];
522
+ if (!state.commandOk) reasons.push('command_mismatch');
523
+ if (!state.argsOk) reasons.push('args_mismatch');
524
+ if (!state.bundleEnvPresent) reasons.push('bundle_env_missing');
525
+ else if (!state.bundleEnvOk) reasons.push('bundle_env_mismatch');
526
+ else if (!state.envOk) reasons.push('env_mismatch');
527
+ return reasons;
528
+ }
529
+
530
+ function shouldRedactPaths(options = {}) {
531
+ return options.redactPaths === true || options.redact_paths === true || options.redact === true;
532
+ }
533
+
534
+ function redactedPath(path, label, options = {}) {
535
+ return shouldRedactPaths(options) ? `[redacted:${label}]` : path;
536
+ }
537
+
538
+ function redactErrorMessage(message, configPath, options = {}) {
539
+ if (!shouldRedactPaths(options)) return message;
540
+ const candidates = [
541
+ configPath,
542
+ options.homeDir,
543
+ options.home_dir,
544
+ options.bundlePath,
545
+ options.bundle_path,
546
+ options.env?.HOME,
547
+ options.env?.USERPROFILE,
548
+ options.env?.APPDATA,
549
+ ].filter((value) => typeof value === 'string' && value.length > 0);
550
+ let redacted = String(message);
551
+ for (const candidate of candidates) {
552
+ redacted = redacted.split(candidate).join('[redacted:path]');
553
+ }
554
+ return redacted.split('[redacted:path]').join(redactedPath(configPath, 'config_path', options));
555
+ }
556
+
557
+ function publicBundlePlaceholder(platform) {
558
+ return platform === 'win32' ? '%USERPROFILE%\\.enigma\\bundle.json' : '$HOME/.enigma/bundle.json';
559
+ }
560
+
561
+ function publicDefaultConfigPath(clientId, platform) {
562
+ return CLIENT_DEFINITIONS[clientId].default_config_paths[platform];
563
+ }
564
+
565
+ function connectCommandFor(clientId, platform) {
566
+ return `enigma connect ${clientId} --bundle "${publicBundlePlaceholder(platform)}"`;
567
+ }
568
+
569
+ function wizardStepsForClient(clientId, platform) {
570
+ const steps = [
571
+ {
572
+ order: 1,
573
+ id: 'install_package',
574
+ title: 'Install the published package first.',
575
+ command: 'npm install -g enigma-memory',
576
+ writes: 'global_npm_package',
577
+ },
578
+ {
579
+ order: 2,
580
+ id: 'create_local_bundle',
581
+ title: 'Create and verify the local Enigma bundle.',
582
+ commands: [
583
+ `enigma quickstart --bundle "${publicBundlePlaceholder(platform)}" --overwrite`,
584
+ `enigma verify --bundle "${publicBundlePlaceholder(platform)}"`,
585
+ ],
586
+ writes: 'local_enigma_bundle',
587
+ },
588
+ {
589
+ order: 3,
590
+ id: 'doctor_client',
591
+ title: 'Inspect the client config before changing it.',
592
+ command: `enigma doctor --client ${clientId}`,
593
+ writes: false,
594
+ },
595
+ {
596
+ order: 4,
597
+ id: 'connect_client',
598
+ title: 'Merge only the Enigma MCP server entry into the client config.',
599
+ command: connectCommandFor(clientId, platform),
600
+ writes: 'client_config_when_user_runs_command',
601
+ },
602
+ {
603
+ order: 5,
604
+ id: 'restart_client',
605
+ title: 'Restart or reload the client so it re-reads MCP settings.',
606
+ writes: false,
607
+ },
608
+ ];
609
+ if (clientId === 'kimi-code') {
610
+ steps.splice(4, 0, {
611
+ order: 5,
612
+ id: 'kimi_gui_path_caveat',
613
+ title: 'If Kimi Code was launched from the GUI and cannot find enigma-mcp, reconnect with an absolute command path.',
614
+ command: `${connectCommandFor(clientId, platform)} --mcp-command "/absolute/path/to/enigma-mcp"`,
615
+ writes: 'client_config_when_user_runs_command',
616
+ });
617
+ for (let index = 5; index < steps.length; index += 1) steps[index].order = index + 1;
618
+ }
619
+ return steps;
620
+ }
621
+
622
+ export function planConnectWizard(clientIdOrOptions = {}, maybeOptions = {}) {
449
623
  const options = normalizeOptions(clientIdOrOptions, maybeOptions);
450
624
  const selected = options.clientId ?? options.client_id;
451
625
  const clientIds = selected ? [normalizeClientId(selected)] : supportedClients;
452
- const clients = [];
626
+ const platform = normalizePlatform(options.platform ?? process.platform);
627
+ return {
628
+ ok: true,
629
+ schema: 'enigma.connect_wizard_plan.v1',
630
+ platform,
631
+ writes_performed: false,
632
+ writesPerformed: false,
633
+ clients: clientIds.map((clientId) => ({
634
+ client_id: clientId,
635
+ display_name: CLIENT_DEFINITIONS[clientId].display_name,
636
+ default_config_path: publicDefaultConfigPath(clientId, platform),
637
+ steps: wizardStepsForClient(clientId, platform),
638
+ })),
639
+ };
640
+ }
453
641
 
454
- for (const clientId of clientIds) {
455
- const profile = getClientProfile(clientId, options);
456
- const serverName = String(options.serverName ?? options.server_name ?? profile.server_name);
457
- const configPath = String(options.configPath ?? options.config_path ?? profile.default_config_path);
458
- try {
459
- const { exists, config } = await readJsonConfig(configPath);
460
- const state = exists ? installedState(config, profile, serverName, options) : { installed: false, commandOk: false, bundleEnvOk: false };
461
- clients.push({
462
- client_id: clientId,
463
- ok: !exists || (state.installed && state.commandOk && state.bundleEnvOk),
464
- exists,
465
- configPath,
466
- serverName,
467
- ...state,
468
- });
469
- } catch (error) {
470
- clients.push({
471
- client_id: clientId,
472
- ok: false,
473
- exists: true,
474
- configPath,
475
- serverName,
476
- error: error.message,
477
- });
478
- }
642
+ export async function detectClientConnector(clientIdOrOptions = 'generic-mcp', maybeOptions = {}) {
643
+ const options = normalizeOptions(clientIdOrOptions, maybeOptions);
644
+ const clientId = normalizeClientId(options.clientId ?? options.client_id ?? 'generic-mcp');
645
+ const profile = getClientProfile(clientId, options);
646
+ const serverName = String(options.serverName ?? options.server_name ?? profile.server_name);
647
+ const configPath = String(options.configPath ?? options.config_path ?? profile.default_config_path);
648
+ const displayedConfigPath = redactedPath(configPath, 'config_path', options);
649
+ const base = {
650
+ client_id: clientId,
651
+ display_name: profile.display_name,
652
+ platform: profile.platform,
653
+ configPath: displayedConfigPath,
654
+ config_path: displayedConfigPath,
655
+ public_default_config_path: publicDefaultConfigPath(clientId, profile.platform),
656
+ serverName,
657
+ server_name: serverName,
658
+ };
659
+
660
+ try {
661
+ const { exists, config } = await readJsonConfig(configPath, options);
662
+ const state = exists ? installedState(config, profile, serverName, options) : emptyInstalledState();
663
+ const action = recommendedConnectorAction(exists, state);
664
+ return {
665
+ ...base,
666
+ ok: action === 'already_configured' || action === 'missing_client_config',
667
+ exists,
668
+ configPathExists: exists,
669
+ config_path_exists: exists,
670
+ ...state,
671
+ action,
672
+ recommendedAction: action,
673
+ recommended_action: action,
674
+ repairReasons: connectorRepairReasons(exists, state),
675
+ repair_reasons: connectorRepairReasons(exists, state),
676
+ wizard: planConnectWizard(clientId, { platform: profile.platform }).clients[0],
677
+ };
678
+ } catch (error) {
679
+ const state = emptyInstalledState();
680
+ const action = recommendedConnectorAction(true, state, error);
681
+ return {
682
+ ...base,
683
+ ok: false,
684
+ exists: true,
685
+ configPathExists: true,
686
+ config_path_exists: true,
687
+ ...state,
688
+ action,
689
+ recommendedAction: action,
690
+ recommended_action: action,
691
+ repairReasons: connectorRepairReasons(true, state, error),
692
+ repair_reasons: connectorRepairReasons(true, state, error),
693
+ parseError: error?.code === 'EJSONPARSE' || error?.code === 'EJSONTYPE',
694
+ parse_error: error?.code === 'EJSONPARSE' || error?.code === 'EJSONTYPE',
695
+ error: redactErrorMessage(error.message, configPath, options),
696
+ wizard: planConnectWizard(clientId, { platform: profile.platform }).clients[0],
697
+ };
479
698
  }
699
+ }
480
700
 
701
+ export async function detectConnectors(clientIdOrOptions = {}, maybeOptions = {}) {
702
+ const options = normalizeOptions(clientIdOrOptions, maybeOptions);
703
+ const selected = options.clientId ?? options.client_id;
704
+ const clientIds = selected ? [normalizeClientId(selected)] : supportedClients;
705
+ const clients = [];
706
+ for (const clientId of clientIds) {
707
+ clients.push(await detectClientConnector(clientId, options));
708
+ }
481
709
  return { ok: clients.every((client) => client.ok), clients };
482
710
  }
483
711
 
712
+ export async function doctorConnectors(clientIdOrOptions = {}, maybeOptions = {}) {
713
+ return detectConnectors(clientIdOrOptions, maybeOptions);
714
+ }
715
+
484
716
  export function runConnectorDemo(input = {}) {
485
717
  const options = { ...input, clientId: input.clientId ?? input.client_id ?? 'generic-mcp' };
486
718
  const clientId = normalizeClientId(options.clientId);
@@ -515,6 +747,7 @@ export function runConnectorDemo(input = {}) {
515
747
  backupPath: backupPathFor(demoOptions.configPath, demoOptions.now),
516
748
  });
517
749
  const generatedJson = stringifyConfig(sampleConfig);
750
+ const wizardPlan = planConnectWizard(clientId, { platform: profile.platform }).clients[0];
518
751
 
519
752
  return {
520
753
  ok: true,
@@ -529,5 +762,7 @@ export function runConnectorDemo(input = {}) {
529
762
  disconnect: disconnectResult,
530
763
  generatedJson,
531
764
  generatedJSON: generatedJson,
765
+ wizardPlan,
766
+ wizard_plan: wizardPlan,
532
767
  };
533
768
  }