capman 0.4.2 → 0.4.3
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 +127 -0
- package/CODEBASE.md +391 -0
- package/README.md +1 -1
- package/bin/capman.js +11 -724
- package/bin/lib/cmd-demo.js +180 -0
- package/bin/lib/cmd-explain.js +72 -0
- package/bin/lib/cmd-generate.js +280 -0
- package/bin/lib/cmd-help.js +26 -0
- package/bin/lib/cmd-init.js +19 -0
- package/bin/lib/cmd-inspect.js +33 -0
- package/bin/lib/cmd-run.js +71 -0
- package/bin/lib/cmd-validate.js +32 -0
- package/bin/lib/shared.js +70 -0
- package/dist/cjs/engine.d.ts +58 -1
- package/dist/cjs/engine.d.ts.map +1 -1
- package/dist/cjs/engine.js +307 -12
- package/dist/cjs/engine.js.map +1 -1
- package/dist/cjs/generator.d.ts.map +1 -1
- package/dist/cjs/generator.js +4 -0
- package/dist/cjs/generator.js.map +1 -1
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/matcher.d.ts.map +1 -1
- package/dist/cjs/matcher.js +19 -25
- package/dist/cjs/matcher.js.map +1 -1
- package/dist/cjs/types.d.ts +27 -0
- package/dist/cjs/types.d.ts.map +1 -1
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/cache.d.ts +49 -0
- package/dist/esm/engine.d.ts +138 -0
- package/dist/esm/engine.js +307 -12
- package/dist/esm/generator.d.ts +7 -0
- package/dist/esm/generator.js +4 -0
- package/dist/esm/index.d.ts +47 -0
- package/dist/esm/learning.d.ts +55 -0
- package/dist/esm/logger.d.ts +21 -0
- package/dist/esm/matcher.d.ts +6 -0
- package/dist/esm/matcher.js +19 -25
- package/dist/esm/parser.d.ts +10 -0
- package/dist/esm/resolver.d.ts +21 -0
- package/dist/esm/schema.d.ts +740 -0
- package/dist/esm/types.d.ts +136 -0
- package/dist/esm/version.d.ts +1 -0
- package/dist/esm/version.js +1 -1
- package/package.json +5 -3
package/dist/esm/engine.js
CHANGED
|
@@ -6,6 +6,12 @@ import { MemoryCache, normalizeQuery } from './cache';
|
|
|
6
6
|
// ─── CapmanEngine ─────────────────────────────────────────────────────────────
|
|
7
7
|
export class CapmanEngine {
|
|
8
8
|
constructor(options) {
|
|
9
|
+
// ── LLM rate limiting state ────────────────────────────────────────────────
|
|
10
|
+
this.llmCallsThisMinute = 0;
|
|
11
|
+
this.llmWindowStart = Date.now();
|
|
12
|
+
this.llmLastCallAt = 0;
|
|
13
|
+
this.llmConsecutiveFails = 0;
|
|
14
|
+
this.llmCircuitOpenAt = 0;
|
|
9
15
|
this.manifest = options.manifest;
|
|
10
16
|
this.mode = options.mode ?? 'balanced';
|
|
11
17
|
this.llm = options.llm;
|
|
@@ -13,6 +19,10 @@ export class CapmanEngine {
|
|
|
13
19
|
this.auth = options.auth;
|
|
14
20
|
this.headers = options.headers;
|
|
15
21
|
this.threshold = options.threshold ?? 50;
|
|
22
|
+
this.maxLLMCallsPerMinute = options.maxLLMCallsPerMinute ?? 60;
|
|
23
|
+
this.llmCooldownMs = options.llmCooldownMs ?? 0;
|
|
24
|
+
this.llmCircuitBreakerThreshold = options.llmCircuitBreakerThreshold ?? 3;
|
|
25
|
+
this.llmCircuitBreakerResetMs = options.llmCircuitBreakerResetMs ?? 60000;
|
|
16
26
|
// Cache — default MemoryCache (no filesystem writes), or disabled with false
|
|
17
27
|
// Use FileCache or ComboCache explicitly for persistence across restarts
|
|
18
28
|
this.cache = options.cache === false
|
|
@@ -51,7 +61,7 @@ export class CapmanEngine {
|
|
|
51
61
|
const resolution = await _resolve(cached.result, cached.result.extractedParams, this.resolveOptions(overrides));
|
|
52
62
|
const trace = {
|
|
53
63
|
query,
|
|
54
|
-
candidates: cached.result.candidates
|
|
64
|
+
candidates: cached.result.candidates,
|
|
55
65
|
reasoning: [`Served from cache (original: ${cached.result.reasoning})`],
|
|
56
66
|
steps,
|
|
57
67
|
resolvedVia: 'cache',
|
|
@@ -83,10 +93,30 @@ export class CapmanEngine {
|
|
|
83
93
|
}
|
|
84
94
|
case 'accurate': {
|
|
85
95
|
if (this.llm) {
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
96
|
+
const skipReason = this.checkLLMAllowed();
|
|
97
|
+
if (skipReason) {
|
|
98
|
+
logger.warn(`LLM skipped — ${skipReason} — falling back to keyword`);
|
|
99
|
+
const t = Date.now();
|
|
100
|
+
matchResult = _match(query, this.manifest);
|
|
101
|
+
steps.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t, detail: `llm skipped: ${skipReason}` });
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
const t = Date.now();
|
|
105
|
+
try {
|
|
106
|
+
matchResult = await _matchWithLLM(query, this.manifest, { llm: this.llm });
|
|
107
|
+
this.recordLLMSuccess();
|
|
108
|
+
resolvedVia = 'llm';
|
|
109
|
+
steps.push({ type: 'llm_match', status: 'pass', durationMs: Date.now() - t, detail: `confidence: ${matchResult.confidence}%` });
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
this.recordLLMFailure();
|
|
113
|
+
logger.warn(`LLM call failed — falling back to keyword: ${err}`);
|
|
114
|
+
const t2 = Date.now();
|
|
115
|
+
matchResult = _match(query, this.manifest);
|
|
116
|
+
steps.push({ type: 'llm_match', status: 'fail', durationMs: Date.now() - t, detail: String(err) });
|
|
117
|
+
steps.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t2, detail: 'fallback after llm failure' });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
90
120
|
}
|
|
91
121
|
else {
|
|
92
122
|
logger.warn('accurate mode requires llm — falling back to keyword');
|
|
@@ -105,11 +135,28 @@ export class CapmanEngine {
|
|
|
105
135
|
matchResult = keywordResult;
|
|
106
136
|
}
|
|
107
137
|
else {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
138
|
+
const skipReason = this.checkLLMAllowed();
|
|
139
|
+
if (skipReason) {
|
|
140
|
+
logger.warn(`LLM skipped — ${skipReason}`);
|
|
141
|
+
steps.push({ type: 'llm_match', status: 'skip', durationMs: 0, detail: skipReason });
|
|
142
|
+
matchResult = keywordResult;
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
logger.info(`Low confidence (${keywordResult.confidence}%) — escalating to LLM`);
|
|
146
|
+
const t2 = Date.now();
|
|
147
|
+
try {
|
|
148
|
+
matchResult = await _matchWithLLM(query, this.manifest, { llm: this.llm });
|
|
149
|
+
this.recordLLMSuccess();
|
|
150
|
+
resolvedVia = 'llm';
|
|
151
|
+
steps.push({ type: 'llm_match', status: 'pass', durationMs: Date.now() - t2, detail: `confidence: ${matchResult.confidence}%` });
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
this.recordLLMFailure();
|
|
155
|
+
logger.warn(`LLM call failed — falling back to keyword: ${err}`);
|
|
156
|
+
steps.push({ type: 'llm_match', status: 'fail', durationMs: Date.now() - t2, detail: String(err) });
|
|
157
|
+
matchResult = keywordResult;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
113
160
|
}
|
|
114
161
|
break;
|
|
115
162
|
}
|
|
@@ -140,7 +187,7 @@ export class CapmanEngine {
|
|
|
140
187
|
});
|
|
141
188
|
// ── Step 6: Build reasoning array ────────────────────────────────────────
|
|
142
189
|
const reasoning = [];
|
|
143
|
-
if (matchResult.candidates
|
|
190
|
+
if (matchResult.candidates.length) {
|
|
144
191
|
const winner = matchResult.candidates.find(c => c.matched);
|
|
145
192
|
const rejected = matchResult.candidates
|
|
146
193
|
.filter(c => !c.matched && c.score > 0)
|
|
@@ -167,7 +214,7 @@ export class CapmanEngine {
|
|
|
167
214
|
await this.recordLearning(query, matchResult, resolvedVia);
|
|
168
215
|
const trace = {
|
|
169
216
|
query,
|
|
170
|
-
candidates: matchResult.candidates
|
|
217
|
+
candidates: matchResult.candidates,
|
|
171
218
|
reasoning,
|
|
172
219
|
steps,
|
|
173
220
|
resolvedVia,
|
|
@@ -205,6 +252,254 @@ export class CapmanEngine {
|
|
|
205
252
|
if (this.cache)
|
|
206
253
|
await this.cache.clear();
|
|
207
254
|
}
|
|
255
|
+
/**
|
|
256
|
+
* Explain what would happen for a query — without executing it.
|
|
257
|
+
* Shows matched capability, all candidate scores with reasoning,
|
|
258
|
+
* and what action would be taken.
|
|
259
|
+
*
|
|
260
|
+
* @example
|
|
261
|
+
* const explanation = await engine.explain('track order 1234')
|
|
262
|
+
* console.log(explanation.matched.reasoning)
|
|
263
|
+
* console.log(explanation.wouldExecute.action)
|
|
264
|
+
* console.log(explanation.candidates)
|
|
265
|
+
*/
|
|
266
|
+
async explain(query) {
|
|
267
|
+
const start = Date.now();
|
|
268
|
+
// ── Match — mirrors ask() logic including rate limiting ───────────────────
|
|
269
|
+
let matchResult;
|
|
270
|
+
let resolvedVia = 'keyword';
|
|
271
|
+
if (this.mode === 'accurate') {
|
|
272
|
+
if (this.llm) {
|
|
273
|
+
const skipReason = this.checkLLMAllowed();
|
|
274
|
+
if (skipReason) {
|
|
275
|
+
logger.warn(`explain(): LLM skipped — ${skipReason} — falling back to keyword`);
|
|
276
|
+
matchResult = _match(query, this.manifest);
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
try {
|
|
280
|
+
matchResult = await _matchWithLLM(query, this.manifest, { llm: this.llm });
|
|
281
|
+
this.recordLLMSuccess();
|
|
282
|
+
resolvedVia = 'llm';
|
|
283
|
+
}
|
|
284
|
+
catch (err) {
|
|
285
|
+
this.recordLLMFailure();
|
|
286
|
+
logger.warn(`explain(): LLM call failed — falling back to keyword: ${err}`);
|
|
287
|
+
matchResult = _match(query, this.manifest);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
matchResult = _match(query, this.manifest);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
else if (this.mode === 'balanced' && this.llm) {
|
|
296
|
+
// Keyword first — escalate to LLM if low confidence (same as ask())
|
|
297
|
+
const keywordResult = _match(query, this.manifest);
|
|
298
|
+
if (keywordResult.confidence >= this.threshold) {
|
|
299
|
+
matchResult = keywordResult;
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
const skipReason = this.checkLLMAllowed();
|
|
303
|
+
if (skipReason) {
|
|
304
|
+
logger.warn(`explain(): LLM skipped — ${skipReason}`);
|
|
305
|
+
matchResult = keywordResult;
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
try {
|
|
309
|
+
matchResult = await _matchWithLLM(query, this.manifest, { llm: this.llm });
|
|
310
|
+
this.recordLLMSuccess();
|
|
311
|
+
resolvedVia = 'llm';
|
|
312
|
+
}
|
|
313
|
+
catch (err) {
|
|
314
|
+
this.recordLLMFailure();
|
|
315
|
+
logger.warn(`explain(): LLM call failed — falling back to keyword: ${err}`);
|
|
316
|
+
matchResult = keywordResult;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
// cheap mode or no llm — keyword only
|
|
323
|
+
matchResult = _match(query, this.manifest);
|
|
324
|
+
}
|
|
325
|
+
// ── Build candidate explanations ─────────────────────────────────────────
|
|
326
|
+
const candidates = matchResult.candidates
|
|
327
|
+
.sort((a, b) => b.score - a.score)
|
|
328
|
+
.map(c => {
|
|
329
|
+
const cap = this.manifest.capabilities.find(mc => mc.id === c.capabilityId);
|
|
330
|
+
let explanation = '';
|
|
331
|
+
if (c.score === 0) {
|
|
332
|
+
explanation = 'No keyword overlap with examples or description';
|
|
333
|
+
}
|
|
334
|
+
else if (c.score >= 90) {
|
|
335
|
+
explanation = `Strong match (${c.score}%) — query closely matches examples`;
|
|
336
|
+
}
|
|
337
|
+
else if (c.score >= 50) {
|
|
338
|
+
const qWords = query.toLowerCase().split(/\W+/).filter(Boolean);
|
|
339
|
+
const matchedWords = (cap?.examples ?? [])
|
|
340
|
+
.flatMap(e => e.toLowerCase().split(/\s+/))
|
|
341
|
+
.filter(w => qWords.includes(w) && w.length > 2);
|
|
342
|
+
const unique = [...new Set(matchedWords)].slice(0, 3);
|
|
343
|
+
explanation = unique.length
|
|
344
|
+
? `Matched keywords: ${unique.join(', ')} (${c.score}%)`
|
|
345
|
+
: `Partial match (${c.score}%) — some keyword overlap`;
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
explanation = `Weak match (${c.score}%) — below 50% confidence threshold, rejected`;
|
|
349
|
+
}
|
|
350
|
+
return { capabilityId: c.capabilityId, score: c.score, matched: c.matched, explanation };
|
|
351
|
+
});
|
|
352
|
+
// ── Build reasoning array ────────────────────────────────────────────────
|
|
353
|
+
const reasoning = [];
|
|
354
|
+
const winner = candidates.find(c => c.matched);
|
|
355
|
+
const rejected = candidates.filter(c => !c.matched && c.score > 0).slice(0, 3);
|
|
356
|
+
if (winner) {
|
|
357
|
+
reasoning.push(`Matched "${winner.capabilityId}" with ${winner.score}% confidence`);
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
reasoning.push('No capability matched above the 50% confidence threshold');
|
|
361
|
+
}
|
|
362
|
+
if (rejected.length) {
|
|
363
|
+
reasoning.push(`Rejected: ${rejected.map(r => `${r.capabilityId} (${r.score}%)`).join(', ')}`);
|
|
364
|
+
}
|
|
365
|
+
reasoning.push(`Resolved via: ${resolvedVia}`);
|
|
366
|
+
if (matchResult.extractedParams && Object.keys(matchResult.extractedParams).length) {
|
|
367
|
+
const params = Object.entries(matchResult.extractedParams)
|
|
368
|
+
.filter(([, v]) => v !== null)
|
|
369
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
370
|
+
.join(', ');
|
|
371
|
+
if (params)
|
|
372
|
+
reasoning.push(`Would extract params: ${params}`);
|
|
373
|
+
}
|
|
374
|
+
// ── Build wouldExecute ───────────────────────────────────────────────────
|
|
375
|
+
const cap = matchResult.capability;
|
|
376
|
+
let action = null;
|
|
377
|
+
let blocked = null;
|
|
378
|
+
let privacy = null;
|
|
379
|
+
let resolverType = null;
|
|
380
|
+
if (cap) {
|
|
381
|
+
privacy = cap.privacy.level;
|
|
382
|
+
resolverType = cap.resolver.type;
|
|
383
|
+
// Check if privacy would block
|
|
384
|
+
if (cap.privacy.level === 'user_owned' && !this.auth?.isAuthenticated) {
|
|
385
|
+
blocked = `Requires authentication (privacy: user_owned)`;
|
|
386
|
+
}
|
|
387
|
+
else if (cap.privacy.level === 'admin' && this.auth?.role !== 'admin') {
|
|
388
|
+
blocked = `Requires admin role (current: ${this.auth?.role ?? 'none'})`;
|
|
389
|
+
}
|
|
390
|
+
if (!blocked) {
|
|
391
|
+
// Build action string
|
|
392
|
+
const params = matchResult.extractedParams;
|
|
393
|
+
if (cap.resolver.type === 'api') {
|
|
394
|
+
const endpoint = cap.resolver.endpoints[0];
|
|
395
|
+
let path = endpoint.path;
|
|
396
|
+
for (const [k, v] of Object.entries(params)) {
|
|
397
|
+
if (v)
|
|
398
|
+
path = path.replace(`{${k}}`, v);
|
|
399
|
+
}
|
|
400
|
+
const base = this.baseUrl ?? '';
|
|
401
|
+
action = `${endpoint.method} ${base}${path}`;
|
|
402
|
+
}
|
|
403
|
+
else if (cap.resolver.type === 'nav') {
|
|
404
|
+
let dest = cap.resolver.destination;
|
|
405
|
+
for (const [k, v] of Object.entries(params)) {
|
|
406
|
+
if (v)
|
|
407
|
+
dest = dest.replace(`{${k}}`, v);
|
|
408
|
+
}
|
|
409
|
+
action = `navigate → ${dest}`;
|
|
410
|
+
}
|
|
411
|
+
else if (cap.resolver.type === 'hybrid') {
|
|
412
|
+
const hybrid = cap.resolver;
|
|
413
|
+
const endpoint = hybrid.api.endpoints[0];
|
|
414
|
+
let path = endpoint.path;
|
|
415
|
+
for (const [k, v] of Object.entries(params)) {
|
|
416
|
+
if (v)
|
|
417
|
+
path = path.replace(`{${k}}`, v);
|
|
418
|
+
}
|
|
419
|
+
let dest = hybrid.nav.destination;
|
|
420
|
+
for (const [k, v] of Object.entries(params)) {
|
|
421
|
+
if (v)
|
|
422
|
+
dest = dest.replace(`{${k}}`, v);
|
|
423
|
+
}
|
|
424
|
+
const base = this.baseUrl ?? '';
|
|
425
|
+
action = `${endpoint.method} ${base}${path} + navigate → ${dest}`;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return {
|
|
430
|
+
query,
|
|
431
|
+
matched: {
|
|
432
|
+
capability: matchResult.capability,
|
|
433
|
+
confidence: matchResult.confidence,
|
|
434
|
+
intent: matchResult.intent,
|
|
435
|
+
reasoning,
|
|
436
|
+
},
|
|
437
|
+
candidates,
|
|
438
|
+
wouldExecute: { resolverType, action, privacy, blocked },
|
|
439
|
+
resolvedVia,
|
|
440
|
+
durationMs: Date.now() - start,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Checks all rate limiting and circuit breaker conditions.
|
|
445
|
+
* Returns null if LLM call is allowed, or a skip reason string if it should be skipped.
|
|
446
|
+
*/
|
|
447
|
+
checkLLMAllowed() {
|
|
448
|
+
const now = Date.now();
|
|
449
|
+
// ── Circuit breaker ──────────────────────────────────────────────────────
|
|
450
|
+
if (this.llmCircuitOpenAt > 0) {
|
|
451
|
+
const elapsed = now - this.llmCircuitOpenAt;
|
|
452
|
+
if (elapsed < this.llmCircuitBreakerResetMs) {
|
|
453
|
+
const remainingSec = Math.ceil((this.llmCircuitBreakerResetMs - elapsed) / 1000);
|
|
454
|
+
return `circuit breaker open — ${remainingSec}s remaining`;
|
|
455
|
+
}
|
|
456
|
+
// Reset circuit breaker — try again
|
|
457
|
+
logger.info('LLM circuit breaker reset — trying again');
|
|
458
|
+
this.llmCircuitOpenAt = 0;
|
|
459
|
+
this.llmConsecutiveFails = 0;
|
|
460
|
+
}
|
|
461
|
+
// ── Cooldown between calls ───────────────────────────────────────────────
|
|
462
|
+
if (this.llmCooldownMs > 0 && this.llmLastCallAt > 0) {
|
|
463
|
+
const elapsed = now - this.llmLastCallAt;
|
|
464
|
+
if (elapsed < this.llmCooldownMs) {
|
|
465
|
+
const remainingMs = this.llmCooldownMs - elapsed;
|
|
466
|
+
return `cooldown active — ${remainingMs}ms remaining`;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
// ── Per-minute rate limit ────────────────────────────────────────────────
|
|
470
|
+
const windowElapsed = now - this.llmWindowStart;
|
|
471
|
+
if (windowElapsed >= 60000) {
|
|
472
|
+
// Reset window
|
|
473
|
+
this.llmCallsThisMinute = 0;
|
|
474
|
+
this.llmWindowStart = now;
|
|
475
|
+
}
|
|
476
|
+
if (this.llmCallsThisMinute >= this.maxLLMCallsPerMinute) {
|
|
477
|
+
const windowResetIn = Math.ceil((60000 - windowElapsed) / 1000);
|
|
478
|
+
return `rate limit reached (${this.maxLLMCallsPerMinute}/min) — resets in ${windowResetIn}s`;
|
|
479
|
+
}
|
|
480
|
+
// Reserve the slot atomically before the call happens
|
|
481
|
+
this.llmCallsThisMinute++;
|
|
482
|
+
this.llmLastCallAt = Date.now();
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Records a successful LLM call — updates rate limit counters.
|
|
487
|
+
*/
|
|
488
|
+
recordLLMSuccess() {
|
|
489
|
+
this.llmConsecutiveFails = 0;
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Records a failed LLM call — may open the circuit breaker.
|
|
493
|
+
*/
|
|
494
|
+
recordLLMFailure() {
|
|
495
|
+
this.llmCallsThisMinute++;
|
|
496
|
+
this.llmConsecutiveFails++;
|
|
497
|
+
this.llmLastCallAt = Date.now();
|
|
498
|
+
if (this.llmConsecutiveFails >= this.llmCircuitBreakerThreshold) {
|
|
499
|
+
this.llmCircuitOpenAt = Date.now();
|
|
500
|
+
logger.warn(`LLM circuit breaker opened after ${this.llmConsecutiveFails} consecutive failures — pausing for ${this.llmCircuitBreakerResetMs / 1000}s`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
208
503
|
// ── Private helpers ────────────────────────────────────────────────────────
|
|
209
504
|
resolveOptions(overrides = {}) {
|
|
210
505
|
return {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CapmanConfig, Manifest, ValidationResult } from './types';
|
|
2
|
+
export declare function generate(config: CapmanConfig): Manifest;
|
|
3
|
+
export declare function loadConfig(configPath?: string): CapmanConfig;
|
|
4
|
+
export declare function writeManifest(manifest: Manifest, outputPath?: string): string;
|
|
5
|
+
export declare function readManifest(manifestPath?: string): Manifest;
|
|
6
|
+
export declare function validate(manifest: Manifest): ValidationResult;
|
|
7
|
+
export declare function generateStarterConfig(): string;
|
package/dist/esm/generator.js
CHANGED
|
@@ -28,6 +28,10 @@ export function loadConfig(configPath) {
|
|
|
28
28
|
if (fs.existsSync(resolved)) {
|
|
29
29
|
let raw;
|
|
30
30
|
// Catch syntax errors in config file
|
|
31
|
+
// Note: require() only works with CJS config files (.js, .json)
|
|
32
|
+
// ESM config files (.mjs or "type": "module") are not supported.
|
|
33
|
+
// Use a CJS config file or convert with: module.exports = { ... }
|
|
34
|
+
// Full ESM config support is planned for v0.5.
|
|
31
35
|
try {
|
|
32
36
|
const mod = require(resolved);
|
|
33
37
|
raw = mod.default ?? mod;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export { setLogLevel } from './logger';
|
|
2
|
+
export type { LogLevel } from './logger';
|
|
3
|
+
export type { Capability, CapabilityParam, CapmanConfig, Manifest, MatchResult, ExecutionTrace, TraceStep, MatchCandidate, ResolveResult, ApiCallResult, ValidationResult, Resolver, ApiResolver, NavResolver, HybridResolver, PrivacyScope, ResolverType, HttpMethod, ExplainResult, ExplainCandidate, } from './types';
|
|
4
|
+
export { generate, loadConfig, writeManifest, readManifest, validate, generateStarterConfig, } from './generator';
|
|
5
|
+
export { match, matchWithLLM, } from './matcher';
|
|
6
|
+
export type { LLMMatcherOptions } from './matcher';
|
|
7
|
+
export { resolve } from './resolver';
|
|
8
|
+
export type { ResolveOptions, AuthContext } from './resolver';
|
|
9
|
+
export { CapmanEngine } from './engine';
|
|
10
|
+
export type { EngineOptions, EngineResult } from './engine';
|
|
11
|
+
export { MemoryCache, FileCache, ComboCache, buildCacheKey, normalizeQuery } from './cache';
|
|
12
|
+
export type { CacheStore, CacheEntry } from './cache';
|
|
13
|
+
export { FileLearningStore, MemoryLearningStore } from './learning';
|
|
14
|
+
export type { LearningStore, LearningEntry, KeywordStats } from './learning';
|
|
15
|
+
export { parseOpenAPI } from './parser';
|
|
16
|
+
export type { ParseResult } from './parser';
|
|
17
|
+
import type { Manifest, MatchResult, ResolveResult } from './types';
|
|
18
|
+
import type { LLMMatcherOptions } from './matcher';
|
|
19
|
+
import type { ResolveOptions } from './resolver';
|
|
20
|
+
export type MatchMode = 'cheap' | 'balanced' | 'accurate';
|
|
21
|
+
export interface AskOptions extends ResolveOptions {
|
|
22
|
+
llm?: LLMMatcherOptions['llm'];
|
|
23
|
+
/**
|
|
24
|
+
* Controls how intent matching is performed.
|
|
25
|
+
* - 'cheap' — keyword only, no LLM calls
|
|
26
|
+
* - 'balanced' — keyword first, LLM fallback if confidence < 50% (default)
|
|
27
|
+
* - 'accurate' — LLM first, keyword fallback
|
|
28
|
+
* @default 'balanced'
|
|
29
|
+
*/
|
|
30
|
+
mode?: MatchMode;
|
|
31
|
+
}
|
|
32
|
+
export interface AskResult {
|
|
33
|
+
match: MatchResult;
|
|
34
|
+
resolution: ResolveResult;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* One-shot convenience: match + resolve in a single call.
|
|
38
|
+
* Delegates to CapmanEngine internally.
|
|
39
|
+
*
|
|
40
|
+
* @deprecated For full features including trace and caching, use CapmanEngine directly.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* const result = await ask("show me the dashboard", manifest, {
|
|
44
|
+
* baseUrl: 'https://api.your-app.com',
|
|
45
|
+
* })
|
|
46
|
+
*/
|
|
47
|
+
export declare function ask(query: string, manifest: Manifest, options?: AskOptions): Promise<AskResult>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export interface LearningEntry {
|
|
2
|
+
query: string;
|
|
3
|
+
capabilityId: string | null;
|
|
4
|
+
confidence: number;
|
|
5
|
+
intent: string;
|
|
6
|
+
extractedParams: Record<string, string | null>;
|
|
7
|
+
resolvedVia: 'keyword' | 'llm' | 'cache';
|
|
8
|
+
timestamp: string;
|
|
9
|
+
}
|
|
10
|
+
export interface KeywordStats {
|
|
11
|
+
/** keyword → Map of capabilityId → hit count */
|
|
12
|
+
index: Record<string, Record<string, number>>;
|
|
13
|
+
/** Total queries processed */
|
|
14
|
+
totalQueries: number;
|
|
15
|
+
/** Queries that went to LLM */
|
|
16
|
+
llmQueries: number;
|
|
17
|
+
/** Queries served from cache */
|
|
18
|
+
cacheHits: number;
|
|
19
|
+
/** Out of scope queries */
|
|
20
|
+
outOfScope: number;
|
|
21
|
+
}
|
|
22
|
+
export interface LearningStore {
|
|
23
|
+
record(entry: LearningEntry): Promise<void>;
|
|
24
|
+
getStats(): Promise<KeywordStats>;
|
|
25
|
+
getTopCapabilities(limit?: number): Promise<Array<{
|
|
26
|
+
id: string;
|
|
27
|
+
hits: number;
|
|
28
|
+
}>>;
|
|
29
|
+
clear(): Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
export declare class FileLearningStore implements LearningStore {
|
|
32
|
+
private filePath;
|
|
33
|
+
private entries;
|
|
34
|
+
private loaded;
|
|
35
|
+
constructor(filePath?: string);
|
|
36
|
+
private load;
|
|
37
|
+
private save;
|
|
38
|
+
record(entry: LearningEntry): Promise<void>;
|
|
39
|
+
getStats(): Promise<KeywordStats>;
|
|
40
|
+
getTopCapabilities(limit?: number): Promise<Array<{
|
|
41
|
+
id: string;
|
|
42
|
+
hits: number;
|
|
43
|
+
}>>;
|
|
44
|
+
clear(): Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
export declare class MemoryLearningStore implements LearningStore {
|
|
47
|
+
private entries;
|
|
48
|
+
record(entry: LearningEntry): Promise<void>;
|
|
49
|
+
getStats(): Promise<KeywordStats>;
|
|
50
|
+
getTopCapabilities(limit?: number): Promise<Array<{
|
|
51
|
+
id: string;
|
|
52
|
+
hits: number;
|
|
53
|
+
}>>;
|
|
54
|
+
clear(): Promise<void>;
|
|
55
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type LogLevel = 'silent' | 'error' | 'warn' | 'info' | 'debug';
|
|
2
|
+
export declare class Logger {
|
|
3
|
+
private level;
|
|
4
|
+
constructor(level?: LogLevel);
|
|
5
|
+
setLevel(level: LogLevel): void;
|
|
6
|
+
error(msg: string, ...args: unknown[]): void;
|
|
7
|
+
warn(msg: string, ...args: unknown[]): void;
|
|
8
|
+
info(msg: string, ...args: unknown[]): void;
|
|
9
|
+
debug(msg: string, ...args: unknown[]): void;
|
|
10
|
+
}
|
|
11
|
+
export declare const logger: Logger;
|
|
12
|
+
/**
|
|
13
|
+
* Set the global log level for capman.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* import { setLogLevel } from 'capman'
|
|
17
|
+
* setLogLevel('debug') // see everything
|
|
18
|
+
* setLogLevel('info') // see key steps
|
|
19
|
+
* setLogLevel('silent') // no output (default)
|
|
20
|
+
*/
|
|
21
|
+
export declare function setLogLevel(level: LogLevel): void;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Manifest, MatchResult } from './types';
|
|
2
|
+
export declare function match(query: string, manifest: Manifest): MatchResult;
|
|
3
|
+
export interface LLMMatcherOptions {
|
|
4
|
+
llm: (prompt: string) => Promise<string>;
|
|
5
|
+
}
|
|
6
|
+
export declare function matchWithLLM(query: string, manifest: Manifest, options: LLMMatcherOptions): Promise<MatchResult>;
|
package/dist/esm/matcher.js
CHANGED
|
@@ -203,29 +203,23 @@ export async function matchWithLLM(query, manifest, options) {
|
|
|
203
203
|
"reasoning": "<one sentence>",
|
|
204
204
|
"extracted_params": { "<param_name>": "<value or null>" }
|
|
205
205
|
}`;
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
catch (err) {
|
|
228
|
-
logger.warn(`LLM match failed, falling back to keyword matcher: ${err}`);
|
|
229
|
-
return match(query, manifest);
|
|
230
|
-
}
|
|
206
|
+
const raw = await options.llm(prompt);
|
|
207
|
+
const clean = raw.replace(/```json|```/g, '').trim();
|
|
208
|
+
const parsed = JSON.parse(clean);
|
|
209
|
+
const isOOS = parsed.matched_capability === 'OUT_OF_SCOPE';
|
|
210
|
+
const capability = isOOS
|
|
211
|
+
? null
|
|
212
|
+
: manifest.capabilities.find(c => c.id === parsed.matched_capability) ?? null;
|
|
213
|
+
return {
|
|
214
|
+
capability,
|
|
215
|
+
confidence: parsed.confidence,
|
|
216
|
+
intent: isOOS ? 'out_of_scope' : parsed.intent,
|
|
217
|
+
extractedParams: parsed.extracted_params ?? {},
|
|
218
|
+
reasoning: parsed.reasoning,
|
|
219
|
+
candidates: capability ? [{
|
|
220
|
+
capabilityId: capability.id,
|
|
221
|
+
score: parsed.confidence,
|
|
222
|
+
matched: true,
|
|
223
|
+
}] : [],
|
|
224
|
+
};
|
|
231
225
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CapmanConfig } from './types';
|
|
2
|
+
export interface ParseResult {
|
|
3
|
+
config: CapmanConfig;
|
|
4
|
+
stats: {
|
|
5
|
+
total: number;
|
|
6
|
+
skipped: number;
|
|
7
|
+
warnings: string[];
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export declare function parseOpenAPI(specPathOrUrl: string): Promise<ParseResult>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { MatchResult, ResolveResult } from './types';
|
|
2
|
+
export interface AuthContext {
|
|
3
|
+
/** Whether the current request is authenticated */
|
|
4
|
+
isAuthenticated: boolean;
|
|
5
|
+
/** Current user's role */
|
|
6
|
+
role?: 'user' | 'admin';
|
|
7
|
+
/** Current user's ID — injected into session params */
|
|
8
|
+
userId?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ResolveOptions {
|
|
11
|
+
baseUrl?: string;
|
|
12
|
+
fetch?: typeof globalThis.fetch;
|
|
13
|
+
dryRun?: boolean;
|
|
14
|
+
headers?: Record<string, string>;
|
|
15
|
+
auth?: AuthContext;
|
|
16
|
+
/** Number of retries on failure (default: 0) */
|
|
17
|
+
retries?: number;
|
|
18
|
+
/** Timeout in milliseconds (default: 5000) */
|
|
19
|
+
timeoutMs?: number;
|
|
20
|
+
}
|
|
21
|
+
export declare function resolve(matchResult: MatchResult, params?: Record<string, unknown>, options?: ResolveOptions): Promise<ResolveResult>;
|