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.
Files changed (47) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/CODEBASE.md +391 -0
  3. package/README.md +1 -1
  4. package/bin/capman.js +11 -724
  5. package/bin/lib/cmd-demo.js +180 -0
  6. package/bin/lib/cmd-explain.js +72 -0
  7. package/bin/lib/cmd-generate.js +280 -0
  8. package/bin/lib/cmd-help.js +26 -0
  9. package/bin/lib/cmd-init.js +19 -0
  10. package/bin/lib/cmd-inspect.js +33 -0
  11. package/bin/lib/cmd-run.js +71 -0
  12. package/bin/lib/cmd-validate.js +32 -0
  13. package/bin/lib/shared.js +70 -0
  14. package/dist/cjs/engine.d.ts +58 -1
  15. package/dist/cjs/engine.d.ts.map +1 -1
  16. package/dist/cjs/engine.js +307 -12
  17. package/dist/cjs/engine.js.map +1 -1
  18. package/dist/cjs/generator.d.ts.map +1 -1
  19. package/dist/cjs/generator.js +4 -0
  20. package/dist/cjs/generator.js.map +1 -1
  21. package/dist/cjs/index.d.ts +1 -1
  22. package/dist/cjs/index.d.ts.map +1 -1
  23. package/dist/cjs/index.js.map +1 -1
  24. package/dist/cjs/matcher.d.ts.map +1 -1
  25. package/dist/cjs/matcher.js +19 -25
  26. package/dist/cjs/matcher.js.map +1 -1
  27. package/dist/cjs/types.d.ts +27 -0
  28. package/dist/cjs/types.d.ts.map +1 -1
  29. package/dist/cjs/version.d.ts +1 -1
  30. package/dist/cjs/version.js +1 -1
  31. package/dist/esm/cache.d.ts +49 -0
  32. package/dist/esm/engine.d.ts +138 -0
  33. package/dist/esm/engine.js +307 -12
  34. package/dist/esm/generator.d.ts +7 -0
  35. package/dist/esm/generator.js +4 -0
  36. package/dist/esm/index.d.ts +47 -0
  37. package/dist/esm/learning.d.ts +55 -0
  38. package/dist/esm/logger.d.ts +21 -0
  39. package/dist/esm/matcher.d.ts +6 -0
  40. package/dist/esm/matcher.js +19 -25
  41. package/dist/esm/parser.d.ts +10 -0
  42. package/dist/esm/resolver.d.ts +21 -0
  43. package/dist/esm/schema.d.ts +740 -0
  44. package/dist/esm/types.d.ts +136 -0
  45. package/dist/esm/version.d.ts +1 -0
  46. package/dist/esm/version.js +1 -1
  47. package/package.json +5 -3
@@ -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 t = Date.now();
87
- matchResult = await _matchWithLLM(query, this.manifest, { llm: this.llm });
88
- resolvedVia = 'llm';
89
- steps.push({ type: 'llm_match', status: 'pass', durationMs: Date.now() - t, detail: `confidence: ${matchResult.confidence}%` });
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
- logger.info(`Low confidence (${keywordResult.confidence}%) — escalating to LLM`);
109
- const t2 = Date.now();
110
- matchResult = await _matchWithLLM(query, this.manifest, { llm: this.llm });
111
- resolvedVia = 'llm';
112
- steps.push({ type: 'llm_match', status: 'pass', durationMs: Date.now() - t2, detail: `confidence: ${matchResult.confidence}%` });
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?.length) {
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;
@@ -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>;
@@ -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
- try {
207
- const raw = await options.llm(prompt);
208
- const clean = raw.replace(/```json|```/g, '').trim();
209
- const parsed = JSON.parse(clean);
210
- const isOOS = parsed.matched_capability === 'OUT_OF_SCOPE';
211
- const capability = isOOS
212
- ? null
213
- : manifest.capabilities.find(c => c.id === parsed.matched_capability) ?? null;
214
- return {
215
- capability,
216
- confidence: parsed.confidence,
217
- intent: isOOS ? 'out_of_scope' : parsed.intent,
218
- extractedParams: parsed.extracted_params ?? {},
219
- reasoning: parsed.reasoning,
220
- candidates: capability ? [{
221
- capabilityId: capability.id,
222
- score: parsed.confidence,
223
- matched: true,
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>;