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.
- package/README.md +367 -379
- package/apps/browser-extension/manifest.json +41 -0
- package/apps/browser-extension/src/background.js +88 -0
- package/apps/browser-extension/src/content-script.js +602 -0
- package/apps/browser-extension/src/native-bridge.js +289 -0
- package/apps/cli/bin/enigma.mjs +347 -2
- package/apps/desktop/src/tray.js +231 -0
- package/docs/browser-extension-install.md +169 -0
- package/docs/developer-ecosystem.md +74 -0
- package/docs/hosted-cloud-product.md +68 -0
- package/docs/installers-and-desktop.md +76 -0
- package/docs/memory-benchmarks.md +51 -0
- package/docs/sdk-api.md +181 -0
- package/examples/ci/github-actions.yml +63 -0
- package/examples/node-basic-memory.mjs +84 -0
- package/package.json +22 -1
- package/packages/connectors/src/index.js +274 -39
- package/packages/hosted-cloud/src/index.js +538 -0
- package/packages/mcp-server/src/index.js +1 -1
- package/scripts/build-installer-assets.mjs +273 -0
- package/scripts/package-browser-extension.mjs +473 -0
- package/scripts/run-memory-benchmarks.mjs +585 -0
- package/scripts/verify-registry-install.mjs +410 -0
- package/templates/mcp-client-config.json +10 -0
|
@@ -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
|
-
|
|
241
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
444
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
}
|