enigma-memory 0.1.0

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 (105) hide show
  1. package/LICENSE +55 -0
  2. package/README.md +379 -0
  3. package/apps/cli/bin/enigma.mjs +1117 -0
  4. package/apps/desktop/src/app.js +1327 -0
  5. package/apps/desktop/src/index.html +1373 -0
  6. package/apps/desktop/src/styles.css +386 -0
  7. package/apps/gateway/bin/enigma-gateway.mjs +52 -0
  8. package/apps/gateway/src/server.mjs +1403 -0
  9. package/apps/native-host/README.md +265 -0
  10. package/apps/native-host/bin/enigma-native-host.mjs +499 -0
  11. package/apps/native-host/manifests/com.enigma.native_host.chrome.json +9 -0
  12. package/apps/native-host/manifests/com.enigma.native_host.edge.json +9 -0
  13. package/apps/native-host/manifests/com.enigma.native_host.firefox.json +9 -0
  14. package/apps/relay/bin/enigma-relay.mjs +52 -0
  15. package/apps/relay/src/server.mjs +1571 -0
  16. package/apps/verifier/bin/enigma-verify.mjs +240 -0
  17. package/deploy/docker-compose.production.example.yml +150 -0
  18. package/deploy/kubernetes/enigma-backend.example.yaml +580 -0
  19. package/package.json +196 -0
  20. package/packages/adapters/PACKAGE_CONTRACT.md +47 -0
  21. package/packages/adapters/src/index.js +183 -0
  22. package/packages/boundary/src/index.js +468 -0
  23. package/packages/connectors/src/index.js +533 -0
  24. package/packages/core/src/index.js +766 -0
  25. package/packages/enterprise/src/index.js +505 -0
  26. package/packages/importers/src/index.js +1083 -0
  27. package/packages/mcp-server/bin/enigma-mcp.mjs +19 -0
  28. package/packages/mcp-server/src/index.js +1185 -0
  29. package/packages/mesh/src/index.js +531 -0
  30. package/packages/metering/PACKAGE_CONTRACT.md +20 -0
  31. package/packages/metering/src/index.js +246 -0
  32. package/packages/optimizer/src/index.js +707 -0
  33. package/packages/passport/src/index.js +651 -0
  34. package/packages/settlement/PACKAGE_CONTRACT.md +28 -0
  35. package/packages/settlement/src/index.js +369 -0
  36. package/packages/storage/PACKAGE_CONTRACT.md +23 -0
  37. package/packages/storage/src/index.js +392 -0
  38. package/packages/vault/src/index.js +904 -0
  39. package/scripts/build-ai-orchestration-plan.mjs +248 -0
  40. package/scripts/build-cloudflare-pages-release-packet.mjs +287 -0
  41. package/scripts/build-cloudflare-token-policy.mjs +239 -0
  42. package/scripts/build-cloudflare-token-request.mjs +314 -0
  43. package/scripts/build-demo-assets.mjs +263 -0
  44. package/scripts/build-edge-backend-workers.mjs +353 -0
  45. package/scripts/build-goal-completion-audit.mjs +398 -0
  46. package/scripts/build-hosted-probe-worker.mjs +191 -0
  47. package/scripts/build-operator-acceptance-packet.mjs +254 -0
  48. package/scripts/build-operator-evidence-starter.mjs +525 -0
  49. package/scripts/build-production-backend-env-kit.mjs +384 -0
  50. package/scripts/build-production-dependency-report.mjs +369 -0
  51. package/scripts/build-production-handoff-packet.mjs +475 -0
  52. package/scripts/build-production-readiness-manifest.mjs +209 -0
  53. package/scripts/build-production-status-board.mjs +297 -0
  54. package/scripts/build-production-storage-migration.mjs +86 -0
  55. package/scripts/build-production-workplan.mjs +371 -0
  56. package/scripts/build-review-packet.mjs +594 -0
  57. package/scripts/check.mjs +419 -0
  58. package/scripts/cloudflare-ops.mjs +1672 -0
  59. package/scripts/cloudflare-secret-env.mjs +128 -0
  60. package/scripts/collect-hosted-backend-live-evidence.mjs +263 -0
  61. package/scripts/infrastructure-readiness.mjs +944 -0
  62. package/scripts/install-enigma-local.mjs +270 -0
  63. package/scripts/memory-optimization-benchmark.mjs +242 -0
  64. package/scripts/monitor-live-endpoints.mjs +358 -0
  65. package/scripts/release-audit.mjs +5588 -0
  66. package/scripts/release-provenance.mjs +250 -0
  67. package/scripts/run-backend-readiness-smoke.mjs +220 -0
  68. package/scripts/validate-backup-restore-drill.mjs +166 -0
  69. package/scripts/validate-cloudflare-credentials.mjs +112 -0
  70. package/scripts/validate-cloudflare-worker-inspect.mjs +179 -0
  71. package/scripts/validate-domain-tls.mjs +248 -0
  72. package/scripts/validate-hosted-backend-live.mjs +319 -0
  73. package/scripts/validate-incident-drill.mjs +221 -0
  74. package/scripts/validate-kms-custody.mjs +235 -0
  75. package/scripts/validate-legal-compliance-approval.mjs +274 -0
  76. package/scripts/validate-monitoring-alerting.mjs +263 -0
  77. package/scripts/validate-network-access-policy.mjs +285 -0
  78. package/scripts/validate-operator-acceptance.mjs +298 -0
  79. package/scripts/validate-production-manifests.mjs +325 -0
  80. package/scripts/validate-public-site-security.mjs +252 -0
  81. package/scripts/validate-security-threat-model.mjs +285 -0
  82. package/scripts/validate-service-settlement.mjs +256 -0
  83. package/scripts/validate-support-sla.mjs +190 -0
  84. package/scripts/validate-tenant-policy-approval.mjs +228 -0
  85. package/scripts/validate-usage-metering.mjs +224 -0
  86. package/scripts/validate-whitepaper-claims.mjs +166 -0
  87. package/specs/boundary-manifest-v1.schema.json +35 -0
  88. package/specs/capsule-v1.schema.json +55 -0
  89. package/specs/claim-boundary-manifest-v1.schema.json +22 -0
  90. package/specs/connector-profile-v1.schema.json +62 -0
  91. package/specs/deletion-tombstone-v1.schema.json +26 -0
  92. package/specs/enterprise-policy-v1.schema.json +59 -0
  93. package/specs/gateway-request-v1.schema.json +32 -0
  94. package/specs/goal-completion-audit-v1.schema.json +70 -0
  95. package/specs/hosted-backend-live-evidence-v1.schema.json +147 -0
  96. package/specs/import-report-v1.schema.json +214 -0
  97. package/specs/infrastructure-readiness-manifest-v1.schema.json +130 -0
  98. package/specs/memory-event-v1.schema.json +42 -0
  99. package/specs/mesh-node-v1.schema.json +126 -0
  100. package/specs/passport-v1.schema.json +50 -0
  101. package/specs/production-handoff-packet-v1.schema.json +149 -0
  102. package/specs/receipt-v1.schema.json +61 -0
  103. package/specs/relay-record-v1.schema.json +43 -0
  104. package/specs/state-checkpoint-v1.schema.json +37 -0
  105. package/specs/trust-bundle-v1.schema.json +56 -0
@@ -0,0 +1,533 @@
1
+ import { access, copyFile, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
2
+ import { dirname, posix, win32 } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+
5
+ const PROFILE_SCHEMA = 'enigma.connector_profile.v1';
6
+ const DEFAULT_SERVER_NAME = 'enigma';
7
+ const MCP_COMMAND = 'enigma-mcp';
8
+ const MCP_CONTAINER_PATH = Object.freeze(['mcpServers']);
9
+ const SUPPORTED_PLATFORMS = Object.freeze(['win32', 'darwin', 'linux']);
10
+
11
+ const CLIENT_DEFINITIONS = Object.freeze({
12
+ 'claude-desktop': Object.freeze({
13
+ display_name: 'Claude Desktop',
14
+ description: 'Anthropic Claude Desktop MCP server configuration.',
15
+ default_config_paths: Object.freeze({
16
+ win32: '%APPDATA%\\Claude\\claude_desktop_config.json',
17
+ darwin: '$HOME/Library/Application Support/Claude/claude_desktop_config.json',
18
+ linux: '$HOME/.config/Claude/claude_desktop_config.json',
19
+ }),
20
+ }),
21
+ cursor: Object.freeze({
22
+ display_name: 'Cursor',
23
+ description: 'Cursor global MCP configuration.',
24
+ default_config_paths: Object.freeze({
25
+ win32: '%USERPROFILE%\\.cursor\\mcp.json',
26
+ darwin: '$HOME/.cursor/mcp.json',
27
+ linux: '$HOME/.cursor/mcp.json',
28
+ }),
29
+ }),
30
+ 'kimi-code': Object.freeze({
31
+ display_name: 'Kimi Code',
32
+ description: 'Kimi Code MCP configuration.',
33
+ default_config_paths: Object.freeze({
34
+ win32: '%APPDATA%\\Kimi Code\\mcp.json',
35
+ darwin: '$HOME/Library/Application Support/Kimi Code/mcp.json',
36
+ linux: '$HOME/.config/kimi-code/mcp.json',
37
+ }),
38
+ }),
39
+ 'vscode-cline': Object.freeze({
40
+ display_name: 'VS Code Cline',
41
+ description: 'Cline extension MCP settings for VS Code.',
42
+ default_config_paths: Object.freeze({
43
+ win32: '%APPDATA%\\Code\\User\\globalStorage\\saoudrizwan.claude-dev\\settings\\cline_mcp_settings.json',
44
+ darwin: '$HOME/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json',
45
+ linux: '$HOME/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json',
46
+ }),
47
+ }),
48
+ roo: Object.freeze({
49
+ display_name: 'Roo Code',
50
+ description: 'Roo Code extension MCP settings for VS Code.',
51
+ default_config_paths: Object.freeze({
52
+ win32: '%APPDATA%\\Code\\User\\globalStorage\\rooveterinaryinc.roo-cline\\settings\\mcp_settings.json',
53
+ darwin: '$HOME/Library/Application Support/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/mcp_settings.json',
54
+ linux: '$HOME/.config/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/mcp_settings.json',
55
+ }),
56
+ }),
57
+ opencode: Object.freeze({
58
+ display_name: 'OpenCode',
59
+ description: 'OpenCode local MCP configuration.',
60
+ default_config_paths: Object.freeze({
61
+ win32: '%APPDATA%\\opencode\\opencode.json',
62
+ darwin: '$HOME/Library/Application Support/opencode/opencode.json',
63
+ linux: '$HOME/.config/opencode/opencode.json',
64
+ }),
65
+ }),
66
+ 'generic-mcp': Object.freeze({
67
+ display_name: 'Generic MCP Client',
68
+ description: 'Portable MCP config using the standard mcpServers object.',
69
+ default_config_paths: Object.freeze({
70
+ win32: '%APPDATA%\\Enigma\\mcp.json',
71
+ darwin: '$HOME/Library/Application Support/Enigma/mcp.json',
72
+ linux: '$HOME/.config/enigma/mcp.json',
73
+ }),
74
+ }),
75
+ });
76
+
77
+ export const supportedClients = Object.freeze(Object.keys(CLIENT_DEFINITIONS));
78
+
79
+ function isPlainObject(value) {
80
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
81
+ }
82
+
83
+ function cloneJson(value) {
84
+ return JSON.parse(JSON.stringify(value));
85
+ }
86
+
87
+ function jsonEqual(left, right) {
88
+ if (Object.is(left, right)) return true;
89
+ if (Array.isArray(left) || Array.isArray(right)) {
90
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
91
+ for (let index = 0; index < left.length; index += 1) {
92
+ if (!jsonEqual(left[index], right[index])) return false;
93
+ }
94
+ return true;
95
+ }
96
+ if (isPlainObject(left) || isPlainObject(right)) {
97
+ if (!isPlainObject(left) || !isPlainObject(right)) return false;
98
+ const leftKeys = Object.keys(left);
99
+ const rightKeys = Object.keys(right);
100
+ if (leftKeys.length !== rightKeys.length) return false;
101
+ for (const key of leftKeys) {
102
+ if (!Object.hasOwn(right, key) || !jsonEqual(left[key], right[key])) return false;
103
+ }
104
+ return true;
105
+ }
106
+ return false;
107
+ }
108
+
109
+ function normalizeOptions(clientIdOrOptions, maybeOptions = {}) {
110
+ if (typeof clientIdOrOptions === 'string') return { ...maybeOptions, clientId: clientIdOrOptions };
111
+ return { ...(clientIdOrOptions ?? {}) };
112
+ }
113
+
114
+ function normalizeClientId(value) {
115
+ const clientId = String(value ?? 'generic-mcp');
116
+ if (!Object.hasOwn(CLIENT_DEFINITIONS, clientId)) {
117
+ throw new Error(`Unsupported Enigma connector client: ${clientId}`);
118
+ }
119
+ return clientId;
120
+ }
121
+
122
+ function normalizePlatform(value = process.platform) {
123
+ const platform = String(value);
124
+ if (!SUPPORTED_PLATFORMS.includes(platform)) {
125
+ throw new Error(`Unsupported connector platform: ${platform}`);
126
+ }
127
+ return platform;
128
+ }
129
+
130
+ function envValue(env, name) {
131
+ return typeof env?.[name] === 'string' && env[name].length > 0 ? env[name] : undefined;
132
+ }
133
+
134
+ function mcpCommandFromOptions(options = {}) {
135
+ return String(options.mcpCommand ?? options.mcp_command ?? options.command ?? MCP_COMMAND);
136
+ }
137
+
138
+ function homeForPlatform(platform, options = {}) {
139
+ if (typeof options.homeDir === 'string' && options.homeDir.length > 0) return options.homeDir;
140
+ const env = options.env ?? process.env;
141
+ if (platform === 'win32') {
142
+ const userProfile = envValue(env, 'USERPROFILE');
143
+ if (userProfile) return userProfile;
144
+ const homeDrive = envValue(env, 'HOMEDRIVE');
145
+ const homePath = envValue(env, 'HOMEPATH');
146
+ if (homeDrive && homePath) return `${homeDrive}${homePath}`;
147
+ } else {
148
+ const home = envValue(env, 'HOME');
149
+ if (home) return home;
150
+ }
151
+ const fallback = homedir();
152
+ if (fallback) return fallback;
153
+ throw new Error(`Cannot resolve home directory for ${platform}. Pass homeDir or configPath explicitly.`);
154
+ }
155
+
156
+ function appDataForPlatform(platform, options = {}) {
157
+ const env = options.env ?? process.env;
158
+ const home = homeForPlatform(platform, options);
159
+ if (platform === 'win32') return envValue(env, 'APPDATA') ?? win32.join(home, 'AppData', 'Roaming');
160
+ if (platform === 'darwin') return posix.join(home, 'Library', 'Application Support');
161
+ return posix.join(home, '.config');
162
+ }
163
+
164
+ function joinForPlatform(platform, ...segments) {
165
+ return platform === 'win32' ? win32.join(...segments) : posix.join(...segments);
166
+ }
167
+
168
+ function defaultBundlePath(options = {}) {
169
+ const platform = normalizePlatform(options.platform ?? process.platform);
170
+ return joinForPlatform(platform, homeForPlatform(platform, options), '.enigma', 'bundle.json');
171
+ }
172
+
173
+ function pathDirectory(path) {
174
+ if (/^[A-Za-z]:[\\/]/.test(path) || path.includes('\\')) return win32.dirname(path);
175
+ return dirname(path);
176
+ }
177
+
178
+ function defaultConfigPathFor(clientId, platform, options = {}) {
179
+ const home = homeForPlatform(platform, options);
180
+ const appData = appDataForPlatform(platform, options);
181
+ const codeStorage = platform === 'win32'
182
+ ? win32.join(appData, 'Code', 'User', 'globalStorage')
183
+ : posix.join(appData, 'Code', 'User', 'globalStorage');
184
+
185
+ switch (clientId) {
186
+ case 'claude-desktop':
187
+ return joinForPlatform(platform, appData, 'Claude', 'claude_desktop_config.json');
188
+ case 'cursor':
189
+ return joinForPlatform(platform, home, '.cursor', 'mcp.json');
190
+ case 'kimi-code':
191
+ return platform === 'linux'
192
+ ? joinForPlatform(platform, appData, 'kimi-code', 'mcp.json')
193
+ : joinForPlatform(platform, appData, 'Kimi Code', 'mcp.json');
194
+ case 'vscode-cline':
195
+ return joinForPlatform(platform, codeStorage, 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json');
196
+ case 'roo':
197
+ return joinForPlatform(platform, codeStorage, 'rooveterinaryinc.roo-cline', 'settings', 'mcp_settings.json');
198
+ case 'opencode':
199
+ return joinForPlatform(platform, appData, 'opencode', 'opencode.json');
200
+ case 'generic-mcp':
201
+ return platform === 'linux'
202
+ ? joinForPlatform(platform, appData, 'enigma', 'mcp.json')
203
+ : joinForPlatform(platform, appData, 'Enigma', 'mcp.json');
204
+ default:
205
+ throw new Error(`Unsupported Enigma connector client: ${clientId}`);
206
+ }
207
+ }
208
+
209
+ export function platformDefaultConfigPath(clientIdOrOptions = 'generic-mcp', maybeOptions = {}) {
210
+ const options = normalizeOptions(clientIdOrOptions, maybeOptions);
211
+ const clientId = normalizeClientId(options.clientId ?? options.client_id);
212
+ const platform = normalizePlatform(options.platform ?? process.platform);
213
+ return defaultConfigPathFor(clientId, platform, options);
214
+ }
215
+
216
+ export function getClientProfile(clientIdOrOptions = 'generic-mcp', maybeOptions = {}) {
217
+ const options = normalizeOptions(clientIdOrOptions, maybeOptions);
218
+ const clientId = normalizeClientId(options.clientId ?? options.client_id);
219
+ const platform = normalizePlatform(options.platform ?? process.platform);
220
+ const definition = CLIENT_DEFINITIONS[clientId];
221
+ return Object.freeze({
222
+ schema: PROFILE_SCHEMA,
223
+ client_id: clientId,
224
+ display_name: definition.display_name,
225
+ description: definition.description,
226
+ platforms: SUPPORTED_PLATFORMS,
227
+ default_config_path: platformDefaultConfigPath(clientId, options),
228
+ default_config_paths: { ...definition.default_config_paths },
229
+ config_format: 'json',
230
+ server_container_path: [...MCP_CONTAINER_PATH],
231
+ server_name: DEFAULT_SERVER_NAME,
232
+ command: MCP_COMMAND,
233
+ required_env: Object.freeze(['ENIGMA_BUNDLE']),
234
+ platform,
235
+ });
236
+ }
237
+
238
+ function serverEntryFromOptions(options = {}) {
239
+ const env = {};
240
+ if (isPlainObject(options.env)) {
241
+ for (const [key, value] of Object.entries(options.env)) {
242
+ if (value !== undefined && value !== null) env[key] = String(value);
243
+ }
244
+ }
245
+ const bundlePath = String(options.bundlePath ?? options.bundle_path ?? env.ENIGMA_BUNDLE ?? defaultBundlePath(options));
246
+ env.ENIGMA_BUNDLE = bundlePath;
247
+ return {
248
+ command: mcpCommandFromOptions(options),
249
+ args: Array.isArray(options.args) ? [...options.args].map(String) : [],
250
+ env,
251
+ };
252
+ }
253
+
254
+ function ensureContainer(config, path, create) {
255
+ let cursor = config;
256
+ for (const segment of path) {
257
+ if (!isPlainObject(cursor)) throw new Error(`Invalid connector config: ${segment} parent is not an object.`);
258
+ if (cursor[segment] === undefined) {
259
+ if (!create) return undefined;
260
+ cursor[segment] = {};
261
+ }
262
+ if (!isPlainObject(cursor[segment])) throw new Error(`Invalid connector config: ${segment} is not an object.`);
263
+ cursor = cursor[segment];
264
+ }
265
+ return cursor;
266
+ }
267
+
268
+ function applyServer(config, profile, serverName, serverEntry) {
269
+ const next = cloneJson(config);
270
+ const container = ensureContainer(next, profile.server_container_path, true);
271
+ container[serverName] = cloneJson(serverEntry);
272
+ return next;
273
+ }
274
+
275
+ function removeServer(config, profile, serverName) {
276
+ const next = cloneJson(config);
277
+ const container = ensureContainer(next, profile.server_container_path, false);
278
+ if (!container || !Object.hasOwn(container, serverName)) return next;
279
+ delete container[serverName];
280
+ return next;
281
+ }
282
+
283
+ function stringifyConfig(config) {
284
+ return `${JSON.stringify(config, null, 2)}\n`;
285
+ }
286
+
287
+ function timestampSuffix(now = new Date()) {
288
+ const date = now instanceof Date ? now : new Date(now);
289
+ const value = date.getTime();
290
+ if (!Number.isFinite(value)) throw new Error('Invalid connector backup timestamp.');
291
+ return date.toISOString().replace(/[:.]/g, '');
292
+ }
293
+
294
+ function backupPathFor(configPath, now) {
295
+ return `${configPath}.bak.${timestampSuffix(now)}`;
296
+ }
297
+
298
+ async function unusedBackupPath(configPath, now) {
299
+ const base = backupPathFor(configPath, now);
300
+ for (let index = 0; index < 1000; index += 1) {
301
+ const candidate = index === 0 ? base : `${base}.${index}`;
302
+ try {
303
+ await access(candidate);
304
+ } catch (error) {
305
+ if (error?.code === 'ENOENT') return candidate;
306
+ throw error;
307
+ }
308
+ }
309
+ throw new Error(`Cannot allocate backup path for ${configPath}.`);
310
+ }
311
+
312
+ async function readJsonConfig(configPath) {
313
+ let text;
314
+ try {
315
+ text = await readFile(configPath, 'utf8');
316
+ } catch (error) {
317
+ if (error?.code === 'ENOENT') return { exists: false, config: {} };
318
+ throw error;
319
+ }
320
+
321
+ let config;
322
+ try {
323
+ config = JSON.parse(text);
324
+ } catch (error) {
325
+ throw new Error(`Cannot parse JSON connector config at ${configPath}: ${error.message}`);
326
+ }
327
+ if (!isPlainObject(config)) throw new Error(`Connector config at ${configPath} must be a JSON object.`);
328
+ return { exists: true, config };
329
+ }
330
+
331
+ function plannedWritesFor({ dryRun, exists, changed, backupPath, configPath, nextConfig }) {
332
+ if (!dryRun || !changed) return [];
333
+ const writes = [];
334
+ if (exists && backupPath) writes.push({ type: 'backup', from: configPath, path: backupPath });
335
+ writes.push({ type: 'write', path: configPath, content: stringifyConfig(nextConfig) });
336
+ return writes;
337
+ }
338
+
339
+ function connectPlan({ clientId, options, exists, existingConfig, backupPath }) {
340
+ const profile = getClientProfile(clientId, options);
341
+ const serverName = String(options.serverName ?? options.server_name ?? profile.server_name);
342
+ const serverEntry = serverEntryFromOptions(options);
343
+ const nextConfig = applyServer(existingConfig, profile, serverName, serverEntry);
344
+ const changed = !jsonEqual(existingConfig, nextConfig);
345
+ const dryRun = options.dryRun === true || options.dry_run === true;
346
+ const configPath = String(options.configPath ?? options.config_path ?? profile.default_config_path);
347
+ const effectiveBackupPath = exists && changed ? backupPath : null;
348
+ return {
349
+ ok: true,
350
+ action: 'connect',
351
+ client_id: profile.client_id,
352
+ configPath,
353
+ serverName,
354
+ changed,
355
+ dryRun,
356
+ backupPath: effectiveBackupPath,
357
+ plannedWrites: plannedWritesFor({ dryRun, exists, changed, backupPath: effectiveBackupPath, configPath, nextConfig }),
358
+ config: nextConfig,
359
+ generatedJson: stringifyConfig(nextConfig),
360
+ };
361
+ }
362
+
363
+ function disconnectPlan({ clientId, options, exists, existingConfig, backupPath }) {
364
+ const profile = getClientProfile(clientId, options);
365
+ const serverName = String(options.serverName ?? options.server_name ?? profile.server_name);
366
+ const nextConfig = removeServer(existingConfig, profile, serverName);
367
+ const changed = !jsonEqual(existingConfig, nextConfig);
368
+ const dryRun = options.dryRun === true || options.dry_run === true;
369
+ const configPath = String(options.configPath ?? options.config_path ?? profile.default_config_path);
370
+ const effectiveBackupPath = exists && changed ? backupPath : null;
371
+ return {
372
+ ok: true,
373
+ action: 'disconnect',
374
+ client_id: profile.client_id,
375
+ configPath,
376
+ serverName,
377
+ changed,
378
+ dryRun,
379
+ backupPath: effectiveBackupPath,
380
+ plannedWrites: plannedWritesFor({ dryRun, exists, changed, backupPath: effectiveBackupPath, configPath, nextConfig }),
381
+ config: nextConfig,
382
+ generatedJson: stringifyConfig(nextConfig),
383
+ };
384
+ }
385
+
386
+ async function writePlan(plan, originalExists) {
387
+ if (!plan.changed || plan.dryRun) return plan;
388
+ await mkdir(pathDirectory(plan.configPath), { recursive: true });
389
+ const tempPath = `${plan.configPath}.tmp.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`;
390
+ try {
391
+ await writeFile(tempPath, plan.generatedJson, 'utf8');
392
+ if (originalExists && plan.backupPath) await copyFile(plan.configPath, plan.backupPath);
393
+ await rename(tempPath, plan.configPath);
394
+ } catch (error) {
395
+ await rm(tempPath, { force: true });
396
+ throw error;
397
+ }
398
+ return plan;
399
+ }
400
+
401
+ export function renderMcpConfig(clientIdOrOptions = 'generic-mcp', maybeOptions = {}) {
402
+ const options = normalizeOptions(clientIdOrOptions, maybeOptions);
403
+ const profile = getClientProfile(options.clientId ?? options.client_id ?? 'generic-mcp', options);
404
+ const serverName = String(options.serverName ?? options.server_name ?? profile.server_name);
405
+ const config = {};
406
+ const container = ensureContainer(config, profile.server_container_path, true);
407
+ container[serverName] = serverEntryFromOptions(options);
408
+ return config;
409
+ }
410
+
411
+ export async function connectClient(clientIdOrOptions = 'generic-mcp', maybeOptions = {}) {
412
+ const options = normalizeOptions(clientIdOrOptions, maybeOptions);
413
+ const clientId = normalizeClientId(options.clientId ?? options.client_id);
414
+ const configPath = String(options.configPath ?? options.config_path ?? platformDefaultConfigPath(clientId, options));
415
+ const { exists, config } = await readJsonConfig(configPath);
416
+ const plan = connectPlan({ clientId, options: { ...options, configPath }, exists, existingConfig: config, backupPath: null });
417
+ if (exists && plan.changed) {
418
+ plan.backupPath = await unusedBackupPath(configPath, options.now);
419
+ plan.plannedWrites = plannedWritesFor({ dryRun: plan.dryRun, exists, changed: plan.changed, backupPath: plan.backupPath, configPath, nextConfig: plan.config });
420
+ }
421
+ return writePlan(plan, exists);
422
+ }
423
+
424
+ export async function disconnectClient(clientIdOrOptions = 'generic-mcp', maybeOptions = {}) {
425
+ const options = normalizeOptions(clientIdOrOptions, maybeOptions);
426
+ const clientId = normalizeClientId(options.clientId ?? options.client_id);
427
+ const configPath = String(options.configPath ?? options.config_path ?? platformDefaultConfigPath(clientId, options));
428
+ const { exists, config } = await readJsonConfig(configPath);
429
+ const plan = disconnectPlan({ clientId, options: { ...options, configPath }, exists, existingConfig: config, backupPath: null });
430
+ if (exists && plan.changed) {
431
+ plan.backupPath = await unusedBackupPath(configPath, options.now);
432
+ plan.plannedWrites = plannedWritesFor({ dryRun: plan.dryRun, exists, changed: plan.changed, backupPath: plan.backupPath, configPath, nextConfig: plan.config });
433
+ }
434
+ return writePlan(plan, exists);
435
+ }
436
+
437
+ function installedState(config, profile, serverName, options = {}) {
438
+ const container = ensureContainer(config, profile.server_container_path, false);
439
+ if (!container || !isPlainObject(container[serverName])) return { installed: false, commandOk: false, bundleEnvOk: false };
440
+ const entry = container[serverName];
441
+ return {
442
+ installed: true,
443
+ commandOk: entry.command === mcpCommandFromOptions(options),
444
+ bundleEnvOk: typeof entry.env?.ENIGMA_BUNDLE === 'string' && entry.env.ENIGMA_BUNDLE.length > 0,
445
+ };
446
+ }
447
+
448
+ export async function doctorConnectors(clientIdOrOptions = {}, maybeOptions = {}) {
449
+ const options = normalizeOptions(clientIdOrOptions, maybeOptions);
450
+ const selected = options.clientId ?? options.client_id;
451
+ const clientIds = selected ? [normalizeClientId(selected)] : supportedClients;
452
+ const clients = [];
453
+
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
+ }
479
+ }
480
+
481
+ return { ok: clients.every((client) => client.ok), clients };
482
+ }
483
+
484
+ export function runConnectorDemo(input = {}) {
485
+ const options = { ...input, clientId: input.clientId ?? input.client_id ?? 'generic-mcp' };
486
+ const clientId = normalizeClientId(options.clientId);
487
+ const profile = getClientProfile(clientId, options);
488
+ const bundlePath = String(options.bundlePath ?? options.bundle_path ?? joinForPlatform(profile.platform, homeForPlatform(profile.platform, options), '.enigma-demo', 'bundle.json'));
489
+ const demoOptions = {
490
+ ...options,
491
+ bundlePath,
492
+ configPath: options.configPath ?? options.config_path ?? joinForPlatform(profile.platform, homeForPlatform(profile.platform, options), '.enigma-demo', `${clientId}.json`),
493
+ now: options.now ?? '2026-01-01T00:00:00.000Z',
494
+ };
495
+ const sampleConfig = renderMcpConfig(clientId, demoOptions);
496
+ const firstConnect = connectPlan({
497
+ clientId,
498
+ options: demoOptions,
499
+ exists: false,
500
+ existingConfig: {},
501
+ backupPath: null,
502
+ });
503
+ const idempotentReconnect = connectPlan({
504
+ clientId,
505
+ options: demoOptions,
506
+ exists: true,
507
+ existingConfig: firstConnect.config,
508
+ backupPath: backupPathFor(demoOptions.configPath, demoOptions.now),
509
+ });
510
+ const disconnectResult = disconnectPlan({
511
+ clientId,
512
+ options: demoOptions,
513
+ exists: true,
514
+ existingConfig: firstConnect.config,
515
+ backupPath: backupPathFor(demoOptions.configPath, demoOptions.now),
516
+ });
517
+ const generatedJson = stringifyConfig(sampleConfig);
518
+
519
+ return {
520
+ ok: true,
521
+ supportedClients: [...supportedClients],
522
+ supported_clients: [...supportedClients],
523
+ sampleConfig,
524
+ sample_config: sampleConfig,
525
+ firstConnect,
526
+ idempotentReconnect,
527
+ idempotentReconnectResult: idempotentReconnect,
528
+ disconnectResult,
529
+ disconnect: disconnectResult,
530
+ generatedJson,
531
+ generatedJSON: generatedJson,
532
+ };
533
+ }