@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.
- package/CHANGELOG.md +313 -0
- package/LICENSE +83 -0
- package/README.md +470 -0
- package/defaults/learnings.json +146 -0
- package/dist/activation.d.ts +48 -0
- package/dist/activation.js +377 -0
- package/dist/adapters.d.ts +101 -0
- package/dist/adapters.js +171 -0
- package/dist/agents.d.ts +137 -0
- package/dist/agents.js +1638 -0
- package/dist/audit.d.ts +23 -0
- package/dist/audit.js +163 -0
- package/dist/cache.d.ts +15 -0
- package/dist/cache.js +117 -0
- package/dist/claude-integration.d.ts +95 -0
- package/dist/claude-integration.js +247 -0
- package/dist/cli.d.ts +18 -0
- package/dist/cli.js +1823 -0
- package/dist/code-chunker.d.ts +12 -0
- package/dist/code-chunker.js +270 -0
- package/dist/collectors.d.ts +63 -0
- package/dist/collectors.js +617 -0
- package/dist/config.d.ts +73 -0
- package/dist/config.js +239 -0
- package/dist/embeddings.d.ts +36 -0
- package/dist/embeddings.js +124 -0
- package/dist/firewall.d.ts +133 -0
- package/dist/firewall.js +631 -0
- package/dist/hooks.d.ts +76 -0
- package/dist/hooks.js +313 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1081 -0
- package/dist/ingest.d.ts +32 -0
- package/dist/ingest.js +162 -0
- package/dist/learnings.d.ts +108 -0
- package/dist/learnings.js +714 -0
- package/dist/license-sig.d.ts +47 -0
- package/dist/license-sig.js +104 -0
- package/dist/policy.d.ts +131 -0
- package/dist/policy.js +182 -0
- package/dist/search.d.ts +11 -0
- package/dist/search.js +99 -0
- package/dist/sessions.d.ts +46 -0
- package/dist/sessions.js +153 -0
- package/examples/adapters/notion-adapter.js +108 -0
- package/examples/adapters/rss-adapter.js +76 -0
- package/package.json +87 -0
- 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
|
package/dist/adapters.js
ADDED
|
@@ -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
|