@compr/opscontext-mcp 2.0.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 (48) hide show
  1. package/CHANGELOG.md +313 -0
  2. package/LICENSE +83 -0
  3. package/README.md +470 -0
  4. package/defaults/learnings.json +146 -0
  5. package/dist/activation.d.ts +48 -0
  6. package/dist/activation.js +377 -0
  7. package/dist/adapters.d.ts +101 -0
  8. package/dist/adapters.js +171 -0
  9. package/dist/agents.d.ts +137 -0
  10. package/dist/agents.js +1638 -0
  11. package/dist/audit.d.ts +23 -0
  12. package/dist/audit.js +163 -0
  13. package/dist/cache.d.ts +15 -0
  14. package/dist/cache.js +117 -0
  15. package/dist/claude-integration.d.ts +95 -0
  16. package/dist/claude-integration.js +247 -0
  17. package/dist/cli.d.ts +18 -0
  18. package/dist/cli.js +1823 -0
  19. package/dist/code-chunker.d.ts +12 -0
  20. package/dist/code-chunker.js +270 -0
  21. package/dist/collectors.d.ts +63 -0
  22. package/dist/collectors.js +617 -0
  23. package/dist/config.d.ts +73 -0
  24. package/dist/config.js +239 -0
  25. package/dist/embeddings.d.ts +36 -0
  26. package/dist/embeddings.js +124 -0
  27. package/dist/firewall.d.ts +133 -0
  28. package/dist/firewall.js +631 -0
  29. package/dist/hooks.d.ts +76 -0
  30. package/dist/hooks.js +313 -0
  31. package/dist/index.d.ts +3 -0
  32. package/dist/index.js +1081 -0
  33. package/dist/ingest.d.ts +32 -0
  34. package/dist/ingest.js +162 -0
  35. package/dist/learnings.d.ts +108 -0
  36. package/dist/learnings.js +714 -0
  37. package/dist/license-sig.d.ts +47 -0
  38. package/dist/license-sig.js +104 -0
  39. package/dist/policy.d.ts +131 -0
  40. package/dist/policy.js +182 -0
  41. package/dist/search.d.ts +11 -0
  42. package/dist/search.js +99 -0
  43. package/dist/sessions.d.ts +46 -0
  44. package/dist/sessions.js +153 -0
  45. package/examples/adapters/notion-adapter.js +108 -0
  46. package/examples/adapters/rss-adapter.js +76 -0
  47. package/package.json +87 -0
  48. package/skills/opscontext/SKILL.md +260 -0
@@ -0,0 +1,377 @@
1
+ // LOCKED — verified March 3 2026 — activation + delta decryption + machine fingerprint + heartbeat
2
+ // DO NOT RE-AUDIT — E2E tested Feb 23 2026, all 4 Pro tools verified
3
+ /**
4
+ * Activation & Delta Module System
5
+ *
6
+ * The npm package ships with core functionality (search, sessions, learnings,
7
+ * operational collectors). PRO unlocks the four high-value tools that consume
8
+ * collector + multi-project data.
9
+ *
10
+ * Free (no activation required):
11
+ * - search_context, list_sources, read_source, reindex
12
+ * - save/load/list/delete/end_session
13
+ * - save/list/delete/import_learning
14
+ * - Operational collectors run during reindex (PM2, nginx, Docker, git,
15
+ * cron, .env redacted, composer, systemd) — collected data is searchable
16
+ * via search_context
17
+ *
18
+ * Activation unlocks the four PRO tools that consume collected/multi-project
19
+ * data and the HTML report generators:
20
+ * - list_projects (cross-project tech-stack analysis)
21
+ * - check_ports (cross-project port conflict scan)
22
+ * - run_audit (compliance audit with HTML report)
23
+ * - score_project (AI-readiness score + SCORE.md + score-report.html)
24
+ *
25
+ * On activation:
26
+ * 1. License key is validated against the ContextEngine API
27
+ * 2. Server returns a signed delta bundle (encrypted JS modules)
28
+ * 3. Delta is cached locally at ~/.contextengine/delta/
29
+ * 4. Premium tools become available
30
+ */
31
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, unlinkSync } from "fs";
32
+ import { join } from "path";
33
+ import { homedir } from "os";
34
+ import { createHash, createDecipheriv } from "crypto";
35
+ import { safeAppend } from "./audit.js";
36
+ import { verifyLicenseSignature } from "./license-sig.js";
37
+ // ---------------------------------------------------------------------------
38
+ // Constants
39
+ // ---------------------------------------------------------------------------
40
+ const DELTA_DIR = join(homedir(), ".contextengine", "delta");
41
+ const LICENSE_FILE = join(homedir(), ".contextengine", "license.json");
42
+ const ACTIVATION_API_BASE = process.env.CONTEXTENGINE_API || "https://api.compr.ch/contextengine";
43
+ const ACTIVATION_API = `${ACTIVATION_API_BASE}/activate`;
44
+ const HEARTBEAT_INTERVAL_MS = 24 * 60 * 60 * 1000; // daily check
45
+ // Premium modules that require activation.
46
+ // NOTE: collectors.ts runs unconditionally during reindex for all users
47
+ // (operational data feeds search_context for everyone). The PRO tools below
48
+ // in PREMIUM_TOOLS are what consume that data for scoring/audit/cross-project
49
+ // reports. Keep the gate at the tool layer, not the data-collection layer.
50
+ export const PREMIUM_MODULES = [
51
+ "agents", // scorer, auditor, port checker, HTML report formatters
52
+ "search-adv", // advanced BM25 with tuned parameters
53
+ ];
54
+ // Tools that require activation
55
+ export const PREMIUM_TOOLS = [
56
+ "score_project",
57
+ "run_audit",
58
+ "check_ports",
59
+ "list_projects",
60
+ ];
61
+ // ---------------------------------------------------------------------------
62
+ // Machine fingerprint (non-PII)
63
+ // ---------------------------------------------------------------------------
64
+ function getMachineId() {
65
+ const components = [
66
+ process.platform,
67
+ process.arch,
68
+ homedir().split("/").slice(0, 3).join("/"), // just /Users/xxx level
69
+ process.env.USER || process.env.USERNAME || "unknown",
70
+ ];
71
+ return createHash("sha256")
72
+ .update(components.join("|"))
73
+ .digest("hex")
74
+ .slice(0, 16);
75
+ }
76
+ // ---------------------------------------------------------------------------
77
+ // License management
78
+ // ---------------------------------------------------------------------------
79
+ export function loadLicense() {
80
+ try {
81
+ if (!existsSync(LICENSE_FILE))
82
+ return null;
83
+ const data = JSON.parse(readFileSync(LICENSE_FILE, "utf-8"));
84
+ // Check expiry
85
+ if (new Date(data.expiresAt) < new Date()) {
86
+ console.error("[ContextEngine] ⚠ License expired — premium features disabled");
87
+ return null;
88
+ }
89
+ // Verify machine binding
90
+ if (data.machineId !== getMachineId()) {
91
+ console.error("[ContextEngine] ⚠ License bound to different machine");
92
+ return null;
93
+ }
94
+ // Verify Ed25519 signature. Three outcomes:
95
+ // ed25519 → cryptographically verified, full trust
96
+ // legacy-grandfathered → pre-Ed25519 SHA-256 hash, allowed until flag day
97
+ // reject → tampered / wrong keypair / missing signature
98
+ const verify = verifyLicenseSignature(data);
99
+ if (!verify.ok) {
100
+ console.error(`[ContextEngine] ⛔ License signature rejected — ${verify.reason}. Premium features disabled. ` +
101
+ `Reactivate at https://compr.ch/contextengine/pricing if this surprises you.`);
102
+ safeAppend("activation.signature_reject", {
103
+ plan: data.plan,
104
+ machine_id: data.machineId,
105
+ reason: verify.reason,
106
+ });
107
+ return null;
108
+ }
109
+ if (verify.mode === "legacy-grandfathered") {
110
+ console.error(`[ContextEngine] ⚠ ${verify.warning}`);
111
+ safeAppend("activation.legacy_signature", {
112
+ plan: data.plan,
113
+ machine_id: data.machineId,
114
+ });
115
+ }
116
+ return data;
117
+ }
118
+ catch {
119
+ return null;
120
+ }
121
+ }
122
+ function saveLicense(license) {
123
+ const dir = join(homedir(), ".contextengine");
124
+ if (!existsSync(dir))
125
+ mkdirSync(dir, { recursive: true });
126
+ writeFileSync(LICENSE_FILE, JSON.stringify(license, null, 2));
127
+ }
128
+ // ---------------------------------------------------------------------------
129
+ // Activation flow
130
+ // ---------------------------------------------------------------------------
131
+ export async function activate(licenseKey, email) {
132
+ try {
133
+ const machineId = getMachineId();
134
+ const response = await fetch(ACTIVATION_API, {
135
+ method: "POST",
136
+ headers: { "Content-Type": "application/json" },
137
+ body: JSON.stringify({
138
+ key: licenseKey,
139
+ email,
140
+ machineId,
141
+ version: getPackageVersion(),
142
+ platform: process.platform,
143
+ arch: process.arch,
144
+ }),
145
+ });
146
+ if (!response.ok) {
147
+ const text = await response.text();
148
+ return { success: false, message: `Activation failed: ${response.status} ${text}` };
149
+ }
150
+ const data = (await response.json());
151
+ if (!data.success) {
152
+ return { success: false, message: data.error || "Activation rejected" };
153
+ }
154
+ // Save license
155
+ saveLicense(data.license);
156
+ // Decrypt and store delta modules
157
+ await installDelta(data.delta, data.license.key);
158
+ safeAppend("activation.activate", {
159
+ plan: data.license.plan,
160
+ email: data.license.email,
161
+ machine_id: data.license.machineId,
162
+ expires_at: data.license.expiresAt,
163
+ delta_version: data.license.deltaVersion,
164
+ });
165
+ return {
166
+ success: true,
167
+ message: `✅ Activated! Plan: ${data.license.plan}, expires: ${data.license.expiresAt}`,
168
+ plan: data.license.plan,
169
+ };
170
+ }
171
+ catch (err) {
172
+ return { success: false, message: `Activation error: ${err.message}` };
173
+ }
174
+ }
175
+ // ---------------------------------------------------------------------------
176
+ // Delta module management
177
+ // ---------------------------------------------------------------------------
178
+ async function installDelta(delta, licenseKey) {
179
+ if (!existsSync(DELTA_DIR))
180
+ mkdirSync(DELTA_DIR, { recursive: true });
181
+ // Derive decryption key from license key
182
+ const derivedKey = createHash("sha256")
183
+ .update(licenseKey + getMachineId())
184
+ .digest();
185
+ const iv = Buffer.from(delta.iv, "hex");
186
+ for (const mod of delta.modules) {
187
+ const encrypted = Buffer.from(mod.payload, "base64");
188
+ // AES-256-CBC decrypt
189
+ const decipher = createDecipheriv("aes-256-cbc", derivedKey, iv);
190
+ let decrypted = decipher.update(encrypted);
191
+ decrypted = Buffer.concat([decrypted, decipher.final()]);
192
+ const content = decrypted.toString("utf-8");
193
+ // Verify checksum
194
+ const checksum = createHash("sha256").update(content).digest("hex");
195
+ if (checksum !== mod.checksum) {
196
+ throw new Error(`Delta module ${mod.name} checksum mismatch — possible tampering`);
197
+ }
198
+ // Write to delta directory
199
+ writeFileSync(join(DELTA_DIR, `${mod.name}.mjs`), content);
200
+ }
201
+ // Write version marker
202
+ writeFileSync(join(DELTA_DIR, "manifest.json"), JSON.stringify({
203
+ version: delta.version,
204
+ installedAt: new Date().toISOString(),
205
+ modules: delta.modules.map((m) => m.name),
206
+ }));
207
+ console.error(`[ContextEngine] 📦 Delta v${delta.version} installed (${delta.modules.length} modules)`);
208
+ }
209
+ /**
210
+ * Check if delta modules are installed and valid.
211
+ */
212
+ export function isDeltaInstalled() {
213
+ const manifestPath = join(DELTA_DIR, "manifest.json");
214
+ if (!existsSync(manifestPath))
215
+ return false;
216
+ try {
217
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
218
+ // Verify all expected module files exist
219
+ for (const modName of manifest.modules) {
220
+ if (!existsSync(join(DELTA_DIR, `${modName}.mjs`)))
221
+ return false;
222
+ }
223
+ return true;
224
+ }
225
+ catch {
226
+ return false;
227
+ }
228
+ }
229
+ /**
230
+ * Dynamically import a delta module.
231
+ * Returns null if not activated or module not found.
232
+ */
233
+ export async function loadDeltaModule(name) {
234
+ if (!isDeltaInstalled())
235
+ return null;
236
+ const modulePath = join(DELTA_DIR, `${name}.mjs`);
237
+ if (!existsSync(modulePath))
238
+ return null;
239
+ try {
240
+ // Dynamic import of the decrypted module
241
+ const moduleUrl = `file://${modulePath}`;
242
+ return await import(moduleUrl);
243
+ }
244
+ catch (err) {
245
+ console.error(`[ContextEngine] ⚠ Failed to load delta module ${name}:`, err.message);
246
+ return null;
247
+ }
248
+ }
249
+ // ---------------------------------------------------------------------------
250
+ // Heartbeat — periodic license validation
251
+ // ---------------------------------------------------------------------------
252
+ export async function heartbeat() {
253
+ const license = loadLicense();
254
+ if (!license)
255
+ return false;
256
+ const lastBeat = new Date(license.lastHeartbeat).getTime();
257
+ const now = Date.now();
258
+ // Only check once per day
259
+ if (now - lastBeat < HEARTBEAT_INTERVAL_MS)
260
+ return true;
261
+ try {
262
+ const response = await fetch(`${ACTIVATION_API_BASE}/heartbeat`, {
263
+ method: "POST",
264
+ headers: { "Content-Type": "application/json" },
265
+ body: JSON.stringify({
266
+ key: license.key,
267
+ machineId: getMachineId(),
268
+ deltaVersion: license.deltaVersion,
269
+ }),
270
+ });
271
+ if (response.ok) {
272
+ license.lastHeartbeat = new Date().toISOString();
273
+ saveLicense(license);
274
+ return true;
275
+ }
276
+ // License revoked or expired server-side
277
+ console.error("[ContextEngine] ⚠ License validation failed — premium features disabled");
278
+ return false;
279
+ }
280
+ catch {
281
+ // Network error — allow offline grace period (7 days)
282
+ const daysSinceLastBeat = (now - lastBeat) / (1000 * 60 * 60 * 24);
283
+ if (daysSinceLastBeat > 7) {
284
+ console.error("[ContextEngine] ⚠ Offline too long — premium features disabled");
285
+ return false;
286
+ }
287
+ return true; // grace period
288
+ }
289
+ }
290
+ // ---------------------------------------------------------------------------
291
+ // Deactivation
292
+ // ---------------------------------------------------------------------------
293
+ export function deactivate() {
294
+ const prior = loadLicense();
295
+ // Remove license
296
+ if (existsSync(LICENSE_FILE))
297
+ unlinkSync(LICENSE_FILE);
298
+ // Remove delta modules
299
+ if (existsSync(DELTA_DIR)) {
300
+ for (const file of readdirSync(DELTA_DIR)) {
301
+ unlinkSync(join(DELTA_DIR, file));
302
+ }
303
+ }
304
+ safeAppend("activation.deactivate", {
305
+ prior_plan: prior?.plan ?? null,
306
+ prior_email: prior?.email ?? null,
307
+ machine_id: prior?.machineId ?? null,
308
+ });
309
+ console.error("[ContextEngine] 🔒 Deactivated — premium features removed");
310
+ }
311
+ // ---------------------------------------------------------------------------
312
+ // Status
313
+ // ---------------------------------------------------------------------------
314
+ export function getActivationStatus() {
315
+ const license = loadLicense();
316
+ const deltaInstalled = isDeltaInstalled();
317
+ if (!license || !deltaInstalled) {
318
+ return {
319
+ activated: false,
320
+ plan: "community",
321
+ expiresAt: "n/a",
322
+ deltaVersion: "n/a",
323
+ premiumTools: [],
324
+ machineId: getMachineId(),
325
+ };
326
+ }
327
+ return {
328
+ activated: true,
329
+ plan: license.plan,
330
+ expiresAt: license.expiresAt,
331
+ deltaVersion: license.deltaVersion,
332
+ premiumTools: [...PREMIUM_TOOLS],
333
+ machineId: getMachineId(),
334
+ };
335
+ }
336
+ /**
337
+ * Check if a specific tool requires activation.
338
+ */
339
+ export function requiresActivation(toolName) {
340
+ return PREMIUM_TOOLS.includes(toolName);
341
+ }
342
+ /**
343
+ * Gate check — returns error message if tool requires activation but isn't activated.
344
+ * Returns null if tool is available.
345
+ */
346
+ export function gateCheck(toolName) {
347
+ if (!requiresActivation(toolName))
348
+ return null;
349
+ const license = loadLicense();
350
+ if (!license) {
351
+ return `🔒 "${toolName}" requires a ContextEngine Pro license.\n\n` +
352
+ `Activate with: npx contextengine activate <license-key> <email>\n` +
353
+ `Get a license: https://compr.ch/contextengine/pricing\n\n` +
354
+ `Free tools available: search_context, list_sources, read_source, reindex, ` +
355
+ `save_session, load_session, list_sessions, end_session, save_learning, ` +
356
+ `list_learnings, import_learnings`;
357
+ }
358
+ if (!isDeltaInstalled()) {
359
+ return `🔒 Premium modules not installed. Re-activate:\n` +
360
+ `npx contextengine activate ${license.key} ${license.email}`;
361
+ }
362
+ return null;
363
+ }
364
+ // ---------------------------------------------------------------------------
365
+ // Helpers
366
+ // ---------------------------------------------------------------------------
367
+ function getPackageVersion() {
368
+ try {
369
+ const pkgPath = join(import.meta.url.replace("file://", ""), "..", "..", "package.json");
370
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
371
+ return pkg.version || "unknown";
372
+ }
373
+ catch {
374
+ return "unknown";
375
+ }
376
+ }
377
+ //# sourceMappingURL=activation.js.map
@@ -0,0 +1,101 @@
1
+ import type { Chunk } from "./ingest.js";
2
+ /**
3
+ * ContextEngine Plugin/Adapter System
4
+ *
5
+ * Adapters are pluggable data source connectors that extend ContextEngine
6
+ * with custom data collection. Each adapter implements the Adapter interface
7
+ * and returns Chunk[] compatible with the existing search pipeline.
8
+ *
9
+ * Built-in collectors (git, package.json, docker, pm2, etc.) remain as-is.
10
+ * Adapters add NEW sources without modifying core code.
11
+ *
12
+ * Usage in contextengine.json:
13
+ * {
14
+ * "adapters": [
15
+ * { "name": "notion", "module": "./adapters/notion-adapter.js", "config": { "token": "$NOTION_TOKEN" } },
16
+ * { "name": "jira", "module": "@compr/contextengine-jira", "config": { "baseUrl": "https://myorg.atlassian.net" } }
17
+ * ]
18
+ * }
19
+ */
20
+ /**
21
+ * Configuration for a single adapter entry in contextengine.json
22
+ */
23
+ export interface AdapterEntry {
24
+ /** Unique identifier for this adapter instance */
25
+ name: string;
26
+ /** Module path — local file (./adapters/foo.js) or npm package (@compr/contextengine-jira) */
27
+ module: string;
28
+ /** Adapter-specific configuration (passed to init/collect) */
29
+ config?: Record<string, unknown>;
30
+ /** Whether this adapter is enabled (default: true) */
31
+ enabled?: boolean;
32
+ }
33
+ /**
34
+ * The interface every adapter must implement.
35
+ *
36
+ * Adapters are ES modules that export a default object implementing this interface,
37
+ * OR export a `createAdapter(config)` factory function.
38
+ */
39
+ export interface Adapter {
40
+ /** Human-readable adapter name */
41
+ name: string;
42
+ /** Short description of what this adapter collects */
43
+ description: string;
44
+ /**
45
+ * Collect data and return searchable chunks.
46
+ * Called during reindex. Must be safe (read-only, no side effects).
47
+ * Should never throw — return empty array on failure.
48
+ *
49
+ * @param config — Adapter-specific config from contextengine.json
50
+ * @returns Array of chunks to merge into the search index
51
+ */
52
+ collect(config?: Record<string, unknown>): Promise<Chunk[]> | Chunk[];
53
+ /**
54
+ * Optional: validate configuration before collect().
55
+ * Return an error message string if config is invalid, or null if OK.
56
+ */
57
+ validate?(config?: Record<string, unknown>): string | null;
58
+ /**
59
+ * Optional: one-time initialization (e.g. auth handshake).
60
+ * Called once when the adapter is first loaded.
61
+ */
62
+ init?(config?: Record<string, unknown>): Promise<void> | void;
63
+ /**
64
+ * Optional: cleanup resources on shutdown.
65
+ */
66
+ destroy?(): Promise<void> | void;
67
+ }
68
+ /**
69
+ * Factory function signature — adapters can export this instead of a static object.
70
+ * Allows per-instance configuration.
71
+ */
72
+ export type AdapterFactory = (config?: Record<string, unknown>) => Adapter | Promise<Adapter>;
73
+ /**
74
+ * Load and register all adapters from config.
75
+ * Safe — logs errors but never crashes.
76
+ *
77
+ * @param entries — Adapter entries from contextengine.json
78
+ * @returns Number of successfully loaded adapters
79
+ */
80
+ export declare function loadAdapters(entries: AdapterEntry[]): Promise<number>;
81
+ /**
82
+ * Collect data from all registered adapters.
83
+ * Returns combined chunks from all adapters.
84
+ * Safe — individual adapter failures don't affect others.
85
+ *
86
+ * @param entries — Adapter entries (for config lookup)
87
+ * @returns Combined chunks from all adapters
88
+ */
89
+ export declare function collectFromAdapters(entries: AdapterEntry[]): Promise<Chunk[]>;
90
+ /**
91
+ * Destroy all registered adapters (cleanup).
92
+ */
93
+ export declare function destroyAdapters(): Promise<void>;
94
+ /**
95
+ * Get list of registered adapter names and their descriptions.
96
+ */
97
+ export declare function listRegisteredAdapters(): Array<{
98
+ name: string;
99
+ description: string;
100
+ }>;
101
+ //# sourceMappingURL=adapters.d.ts.map
@@ -0,0 +1,171 @@
1
+ import { resolve } from "path";
2
+ // ---------------------------------------------------------------------------
3
+ // Adapter Registry
4
+ // ---------------------------------------------------------------------------
5
+ /** Active adapter instances keyed by name */
6
+ const adapterRegistry = new Map();
7
+ /**
8
+ * Resolve environment variable references in config values.
9
+ * Supports "$ENV_VAR" syntax — replaces with process.env value.
10
+ */
11
+ function resolveEnvVars(config) {
12
+ const resolved = {};
13
+ for (const [key, value] of Object.entries(config)) {
14
+ if (typeof value === "string" && value.startsWith("$")) {
15
+ const envVar = value.slice(1);
16
+ resolved[key] = process.env[envVar] || value;
17
+ }
18
+ else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
19
+ resolved[key] = resolveEnvVars(value);
20
+ }
21
+ else {
22
+ resolved[key] = value;
23
+ }
24
+ }
25
+ return resolved;
26
+ }
27
+ /**
28
+ * Load a single adapter from its module path.
29
+ * Supports:
30
+ * - Local files: "./adapters/foo.js" (relative to config file)
31
+ * - npm packages: "@compr/contextengine-jira"
32
+ * - Named exports: "my-package#myAdapter"
33
+ */
34
+ async function loadAdapterModule(modulePath, config) {
35
+ let moduleSpecifier = modulePath;
36
+ let exportName = null;
37
+ // Support "module#export" syntax
38
+ if (modulePath.includes("#")) {
39
+ const [mod, exp] = modulePath.split("#", 2);
40
+ moduleSpecifier = mod;
41
+ exportName = exp;
42
+ }
43
+ // Resolve relative paths from CWD
44
+ if (moduleSpecifier.startsWith(".") || moduleSpecifier.startsWith("/")) {
45
+ moduleSpecifier = resolve(process.cwd(), moduleSpecifier);
46
+ }
47
+ const mod = await import(moduleSpecifier);
48
+ // Check for factory function
49
+ if (exportName && typeof mod[exportName] === "function") {
50
+ return await mod[exportName](config);
51
+ }
52
+ if (typeof mod.createAdapter === "function") {
53
+ return await mod.createAdapter(config);
54
+ }
55
+ // Check for default export
56
+ if (mod.default) {
57
+ if (typeof mod.default === "function") {
58
+ return await mod.default(config);
59
+ }
60
+ if (typeof mod.default.collect === "function") {
61
+ return mod.default;
62
+ }
63
+ }
64
+ // Check for named adapter export
65
+ if (typeof mod.adapter === "object" && typeof mod.adapter.collect === "function") {
66
+ return mod.adapter;
67
+ }
68
+ throw new Error(`Module "${modulePath}" does not export a valid adapter. ` +
69
+ `Expected: default export with collect(), createAdapter() factory, or named 'adapter' export.`);
70
+ }
71
+ /**
72
+ * Load and register all adapters from config.
73
+ * Safe — logs errors but never crashes.
74
+ *
75
+ * @param entries — Adapter entries from contextengine.json
76
+ * @returns Number of successfully loaded adapters
77
+ */
78
+ export async function loadAdapters(entries) {
79
+ let loaded = 0;
80
+ for (const entry of entries) {
81
+ if (entry.enabled === false) {
82
+ console.error(`[ContextEngine] 🔌 Adapter "${entry.name}" — disabled, skipping`);
83
+ continue;
84
+ }
85
+ try {
86
+ const resolvedConfig = entry.config ? resolveEnvVars(entry.config) : undefined;
87
+ const adapter = await loadAdapterModule(entry.module, resolvedConfig);
88
+ // Validate config if adapter supports it
89
+ if (adapter.validate) {
90
+ const error = adapter.validate(resolvedConfig);
91
+ if (error) {
92
+ console.error(`[ContextEngine] ⚠ Adapter "${entry.name}" config invalid: ${error}`);
93
+ continue;
94
+ }
95
+ }
96
+ // Initialize
97
+ if (adapter.init) {
98
+ await adapter.init(resolvedConfig);
99
+ }
100
+ adapterRegistry.set(entry.name, adapter);
101
+ loaded++;
102
+ console.error(`[ContextEngine] 🔌 Adapter "${entry.name}" loaded — ${adapter.description}`);
103
+ }
104
+ catch (err) {
105
+ console.error(`[ContextEngine] ⚠ Failed to load adapter "${entry.name}" from "${entry.module}": ${err instanceof Error ? err.message : String(err)}`);
106
+ }
107
+ }
108
+ return loaded;
109
+ }
110
+ /**
111
+ * Collect data from all registered adapters.
112
+ * Returns combined chunks from all adapters.
113
+ * Safe — individual adapter failures don't affect others.
114
+ *
115
+ * @param entries — Adapter entries (for config lookup)
116
+ * @returns Combined chunks from all adapters
117
+ */
118
+ export async function collectFromAdapters(entries) {
119
+ const allChunks = [];
120
+ for (const entry of entries) {
121
+ const adapter = adapterRegistry.get(entry.name);
122
+ if (!adapter)
123
+ continue;
124
+ try {
125
+ const resolvedConfig = entry.config ? resolveEnvVars(entry.config) : undefined;
126
+ const startMs = Date.now();
127
+ const chunks = await adapter.collect(resolvedConfig);
128
+ const elapsedMs = Date.now() - startMs;
129
+ // Tag chunks with adapter source
130
+ for (const chunk of chunks) {
131
+ if (!chunk.source.includes(entry.name)) {
132
+ chunk.source = `${entry.name} — ${chunk.source}`;
133
+ }
134
+ }
135
+ allChunks.push(...chunks);
136
+ if (chunks.length > 0) {
137
+ console.error(`[ContextEngine] 🔌 Adapter "${entry.name}" collected ${chunks.length} chunks (${elapsedMs}ms)`);
138
+ }
139
+ }
140
+ catch (err) {
141
+ console.error(`[ContextEngine] ⚠ Adapter "${entry.name}" collect() failed: ${err instanceof Error ? err.message : String(err)}`);
142
+ }
143
+ }
144
+ return allChunks;
145
+ }
146
+ /**
147
+ * Destroy all registered adapters (cleanup).
148
+ */
149
+ export async function destroyAdapters() {
150
+ for (const [name, adapter] of adapterRegistry) {
151
+ try {
152
+ if (adapter.destroy) {
153
+ await adapter.destroy();
154
+ }
155
+ }
156
+ catch (err) {
157
+ console.error(`[ContextEngine] ⚠ Adapter "${name}" destroy() failed: ${err instanceof Error ? err.message : String(err)}`);
158
+ }
159
+ }
160
+ adapterRegistry.clear();
161
+ }
162
+ /**
163
+ * Get list of registered adapter names and their descriptions.
164
+ */
165
+ export function listRegisteredAdapters() {
166
+ return Array.from(adapterRegistry.entries()).map(([name, adapter]) => ({
167
+ name,
168
+ description: adapter.description,
169
+ }));
170
+ }
171
+ //# sourceMappingURL=adapters.js.map