capman 0.5.0 → 0.5.1

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.
@@ -20,6 +20,7 @@ export class CapmanEngine {
20
20
  this.auth = options.auth;
21
21
  this.headers = options.headers;
22
22
  this.threshold = options.threshold ?? 50;
23
+ this.cacheTtlMs = options.cacheTtlMs ?? null;
23
24
  this.maxLLMCallsPerMinute = options.maxLLMCallsPerMinute ?? 60;
24
25
  this.llmCooldownMs = options.llmCooldownMs ?? 0;
25
26
  this.llmCircuitBreakerThreshold = options.llmCircuitBreakerThreshold ?? 3;
@@ -60,12 +61,11 @@ export class CapmanEngine {
60
61
  async ask(query, overrides = {}) {
61
62
  const start = Date.now();
62
63
  const steps = [];
63
- let resolvedVia = 'keyword';
64
64
  // ── Step 1: Check cache ──────────────────────────────────────────────────
65
65
  const cacheStart = Date.now();
66
66
  if (this.cache) {
67
67
  const queryKey = normalizeQuery(query);
68
- const cached = await this.cache.get(queryKey);
68
+ const cached = await this.cache.get(queryKey, this.cacheTtlMs ?? undefined);
69
69
  if (cached) {
70
70
  steps.push({ type: 'cache_check', status: 'hit', durationMs: Date.now() - cacheStart, detail: 'Served from cache' });
71
71
  logger.info(`Cache hit for: "${query}"`);
@@ -94,88 +94,7 @@ export class CapmanEngine {
94
94
  steps.push({ type: 'cache_check', status: 'skip', durationMs: 0, detail: 'Cache disabled' });
95
95
  }
96
96
  // ── Step 2: Match ────────────────────────────────────────────────────────
97
- let matchResult;
98
- switch (this.mode) {
99
- case 'cheap': {
100
- const t = Date.now();
101
- matchResult = _match(query, this.manifest);
102
- steps.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t, detail: `confidence: ${matchResult.confidence}%` });
103
- break;
104
- }
105
- case 'accurate': {
106
- if (this.llm) {
107
- const skipReason = this.checkLLMAllowed();
108
- if (skipReason) {
109
- logger.warn(`LLM skipped — ${skipReason} — falling back to keyword`);
110
- const t = Date.now();
111
- matchResult = _match(query, this.manifest);
112
- steps.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t, detail: `llm skipped: ${skipReason}` });
113
- }
114
- else {
115
- const t = Date.now();
116
- try {
117
- matchResult = await _matchWithLLM(query, this.manifest, { llm: this.llm });
118
- this.recordLLMSuccess();
119
- resolvedVia = 'llm';
120
- steps.push({ type: 'llm_match', status: 'pass', durationMs: Date.now() - t, detail: `confidence: ${matchResult.confidence}%` });
121
- }
122
- catch (err) {
123
- const isParseError = String(err).startsWith('LLM_PARSE_ERROR');
124
- if (!isParseError)
125
- this.recordLLMFailure();
126
- logger.warn(`LLM call failed — falling back to keyword: ${err}`);
127
- const t2 = Date.now();
128
- matchResult = _match(query, this.manifest);
129
- steps.push({ type: 'llm_match', status: 'fail', durationMs: Date.now() - t, detail: String(err) });
130
- steps.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t2, detail: 'fallback after llm failure' });
131
- }
132
- }
133
- }
134
- else {
135
- logger.warn('accurate mode requires llm — falling back to keyword');
136
- const t = Date.now();
137
- matchResult = _match(query, this.manifest);
138
- steps.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t, detail: 'llm not provided, used keyword' });
139
- }
140
- break;
141
- }
142
- case 'balanced':
143
- default: {
144
- const t1 = Date.now();
145
- const keywordResult = _match(query, this.manifest);
146
- steps.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t1, detail: `confidence: ${keywordResult.confidence}%` });
147
- if (keywordResult.confidence >= this.threshold || !this.llm) {
148
- matchResult = keywordResult;
149
- }
150
- else {
151
- const skipReason = this.checkLLMAllowed();
152
- if (skipReason) {
153
- logger.warn(`LLM skipped — ${skipReason}`);
154
- steps.push({ type: 'llm_match', status: 'skip', durationMs: 0, detail: skipReason });
155
- matchResult = keywordResult;
156
- }
157
- else {
158
- logger.info(`Low confidence (${keywordResult.confidence}%) — escalating to LLM`);
159
- const t2 = Date.now();
160
- try {
161
- matchResult = await _matchWithLLM(query, this.manifest, { llm: this.llm });
162
- this.recordLLMSuccess();
163
- resolvedVia = 'llm';
164
- steps.push({ type: 'llm_match', status: 'pass', durationMs: Date.now() - t2, detail: `confidence: ${matchResult.confidence}%` });
165
- }
166
- catch (err) {
167
- const isParseError = String(err).startsWith('LLM_PARSE_ERROR');
168
- if (!isParseError)
169
- this.recordLLMFailure();
170
- logger.warn(`LLM call failed — falling back to keyword: ${err}`);
171
- steps.push({ type: 'llm_match', status: 'fail', durationMs: Date.now() - t2, detail: String(err) });
172
- matchResult = keywordResult;
173
- }
174
- }
175
- }
176
- break;
177
- }
178
- }
97
+ let { matchResult, resolvedVia } = await this._runMatch(query, steps);
179
98
  const preBoostMatchResult = matchResult; // kept for learning recording only — prevents feedback loop
180
99
  // ── Step 2.5: Apply learning boost ───────────────────────────────────────
181
100
  matchResult = await this.applyBoostToMatchResult(query, matchResult);
@@ -295,67 +214,9 @@ export class CapmanEngine {
295
214
  */
296
215
  async explain(query) {
297
216
  const start = Date.now();
298
- // ── Match — mirrors ask() logic including rate limiting ───────────────────
299
- let matchResult;
300
- let resolvedVia = 'keyword';
301
- if (this.mode === 'accurate') {
302
- if (this.llm) {
303
- const skipReason = this.checkLLMAllowed();
304
- if (skipReason) {
305
- logger.warn(`explain(): LLM skipped — ${skipReason} — falling back to keyword`);
306
- matchResult = _match(query, this.manifest);
307
- }
308
- else {
309
- try {
310
- matchResult = await _matchWithLLM(query, this.manifest, { llm: this.llm });
311
- this.recordLLMSuccess();
312
- resolvedVia = 'llm';
313
- }
314
- catch (err) {
315
- const isParseError = String(err).startsWith('LLM_PARSE_ERROR');
316
- if (!isParseError)
317
- this.recordLLMFailure();
318
- logger.warn(`LLM call failed — falling back to keyword: ${err}`);
319
- matchResult = _match(query, this.manifest);
320
- }
321
- }
322
- }
323
- else {
324
- matchResult = _match(query, this.manifest);
325
- }
326
- }
327
- else if (this.mode === 'balanced' && this.llm) {
328
- // Keyword first — escalate to LLM if low confidence (same as ask())
329
- const keywordResult = _match(query, this.manifest);
330
- if (keywordResult.confidence >= this.threshold) {
331
- matchResult = keywordResult;
332
- }
333
- else {
334
- const skipReason = this.checkLLMAllowed();
335
- if (skipReason) {
336
- logger.warn(`explain(): LLM skipped — ${skipReason}`);
337
- matchResult = keywordResult;
338
- }
339
- else {
340
- try {
341
- matchResult = await _matchWithLLM(query, this.manifest, { llm: this.llm });
342
- this.recordLLMSuccess();
343
- resolvedVia = 'llm';
344
- }
345
- catch (err) {
346
- const isParseError = String(err).startsWith('LLM_PARSE_ERROR');
347
- if (!isParseError)
348
- this.recordLLMFailure();
349
- logger.warn(`LLM call failed — falling back to keyword: ${err}`);
350
- matchResult = keywordResult;
351
- }
352
- }
353
- }
354
- }
355
- else {
356
- // cheap mode or no llm — keyword only
357
- matchResult = _match(query, this.manifest);
358
- }
217
+ // ── Match — shared with ask() via _runMatch() ─────────────────────────────
218
+ let { matchResult, resolvedVia: _resolvedVia } = await this._runMatch(query);
219
+ let resolvedVia = _resolvedVia;
359
220
  // ── Apply learning boost (same as ask()) ─────────────────────────────────
360
221
  matchResult = await this.applyBoostToMatchResult(query, matchResult);
361
222
  // ── Build candidate explanations ─────────────────────────────────────────
@@ -541,6 +402,97 @@ export class CapmanEngine {
541
402
  logger.warn(`LLM circuit breaker opened after ${this.llmConsecutiveFails} consecutive failures — pausing for ${this.llmCircuitBreakerResetMs / 1000}s`);
542
403
  }
543
404
  }
405
+ /**
406
+ * Runs the matching pipeline for a query — shared by ask() and explain().
407
+ * Handles cheap / balanced / accurate mode dispatch and LLM rate limiting.
408
+ * Returns the match result and which resolver was used.
409
+ */
410
+ async _runMatch(query, steps) {
411
+ let matchResult;
412
+ let resolvedVia = 'keyword';
413
+ switch (this.mode) {
414
+ case 'cheap': {
415
+ const t = Date.now();
416
+ matchResult = _match(query, this.manifest);
417
+ steps?.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t, detail: `confidence: ${matchResult.confidence}%` });
418
+ break;
419
+ }
420
+ case 'accurate': {
421
+ if (this.llm) {
422
+ const skipReason = this.checkLLMAllowed();
423
+ if (skipReason) {
424
+ logger.warn(`LLM skipped — ${skipReason} — falling back to keyword`);
425
+ const t = Date.now();
426
+ matchResult = _match(query, this.manifest);
427
+ steps?.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t, detail: `llm skipped: ${skipReason}` });
428
+ }
429
+ else {
430
+ const t = Date.now();
431
+ try {
432
+ matchResult = await _matchWithLLM(query, this.manifest, { llm: this.llm });
433
+ this.recordLLMSuccess();
434
+ resolvedVia = 'llm';
435
+ steps?.push({ type: 'llm_match', status: 'pass', durationMs: Date.now() - t, detail: `confidence: ${matchResult.confidence}%` });
436
+ }
437
+ catch (err) {
438
+ const isParseError = String(err).startsWith('LLM_PARSE_ERROR');
439
+ if (!isParseError)
440
+ this.recordLLMFailure();
441
+ logger.warn(`LLM call failed — falling back to keyword: ${err instanceof Error ? err.message : String(err)}`);
442
+ const t2 = Date.now();
443
+ matchResult = _match(query, this.manifest);
444
+ steps?.push({ type: 'llm_match', status: 'fail', durationMs: Date.now() - t, detail: String(err) });
445
+ steps?.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t2, detail: 'fallback after llm failure' });
446
+ }
447
+ }
448
+ }
449
+ else {
450
+ logger.warn('accurate mode requires llm — falling back to keyword');
451
+ const t = Date.now();
452
+ matchResult = _match(query, this.manifest);
453
+ steps?.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t, detail: 'llm not provided, used keyword' });
454
+ }
455
+ break;
456
+ }
457
+ case 'balanced':
458
+ default: {
459
+ const t1 = Date.now();
460
+ const keywordResult = _match(query, this.manifest);
461
+ steps?.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t1, detail: `confidence: ${keywordResult.confidence}%` });
462
+ if (keywordResult.confidence >= this.threshold || !this.llm) {
463
+ matchResult = keywordResult;
464
+ }
465
+ else {
466
+ const skipReason = this.checkLLMAllowed();
467
+ if (skipReason) {
468
+ logger.warn(`LLM skipped — ${skipReason}`);
469
+ steps?.push({ type: 'llm_match', status: 'skip', durationMs: 0, detail: skipReason });
470
+ matchResult = keywordResult;
471
+ }
472
+ else {
473
+ logger.info(`Low confidence (${keywordResult.confidence}%) — escalating to LLM`);
474
+ const t2 = Date.now();
475
+ try {
476
+ matchResult = await _matchWithLLM(query, this.manifest, { llm: this.llm });
477
+ this.recordLLMSuccess();
478
+ resolvedVia = 'llm';
479
+ steps?.push({ type: 'llm_match', status: 'pass', durationMs: Date.now() - t2, detail: `confidence: ${matchResult.confidence}%` });
480
+ }
481
+ catch (err) {
482
+ const isParseError = String(err).startsWith('LLM_PARSE_ERROR');
483
+ if (!isParseError)
484
+ this.recordLLMFailure();
485
+ logger.warn(`LLM call failed — falling back to keyword: ${err instanceof Error ? err.message : String(err)}`);
486
+ steps?.push({ type: 'llm_match', status: 'fail', durationMs: Date.now() - t2, detail: String(err) });
487
+ matchResult = keywordResult;
488
+ }
489
+ }
490
+ }
491
+ break;
492
+ }
493
+ }
494
+ return { matchResult: matchResult, resolvedVia };
495
+ }
544
496
  /**
545
497
  * Applies learning boost to a MatchResult and returns the updated result.
546
498
  * Shared by ask() and explain() to avoid logic divergence.
@@ -38,6 +38,7 @@ export declare class FileLearningStore implements LearningStore {
38
38
  private statsCounter;
39
39
  constructor(filePath?: string);
40
40
  private updateIndex;
41
+ private subtractFromIndex;
41
42
  private rebuildIndex;
42
43
  private load;
43
44
  private save;
@@ -59,6 +60,7 @@ export declare class MemoryLearningStore implements LearningStore {
59
60
  getStats(): Promise<KeywordStats>;
60
61
  getIndex(): Promise<Record<string, Record<string, number>>>;
61
62
  private updateIndex;
63
+ private subtractFromIndex;
62
64
  private rebuildIndex;
63
65
  getTopCapabilities(limit?: number): Promise<Array<{
64
66
  id: string;
@@ -78,6 +78,37 @@ export class FileLearningStore {
78
78
  }
79
79
  }
80
80
  }
81
+ subtractFromIndex(entry) {
82
+ if (!entry.capabilityId) {
83
+ this.statsCounter.outOfScope = Math.max(0, this.statsCounter.outOfScope - 1);
84
+ this.statsCounter.totalQueries = Math.max(0, this.statsCounter.totalQueries - 1);
85
+ if (entry.resolvedVia === 'llm')
86
+ this.statsCounter.llmQueries = Math.max(0, this.statsCounter.llmQueries - 1);
87
+ if (entry.resolvedVia === 'cache')
88
+ this.statsCounter.cacheHits = Math.max(0, this.statsCounter.cacheHits - 1);
89
+ return;
90
+ }
91
+ this.statsCounter.totalQueries = Math.max(0, this.statsCounter.totalQueries - 1);
92
+ if (entry.resolvedVia === 'llm')
93
+ this.statsCounter.llmQueries = Math.max(0, this.statsCounter.llmQueries - 1);
94
+ if (entry.resolvedVia === 'cache')
95
+ this.statsCounter.cacheHits = Math.max(0, this.statsCounter.cacheHits - 1);
96
+ const words = entry.query.toLowerCase()
97
+ .split(/\W+/)
98
+ .filter(w => w.length > 2 && !STOPWORDS.has(w));
99
+ for (const word of words) {
100
+ if (!this.index[word])
101
+ continue;
102
+ this.index[word][entry.capabilityId] =
103
+ (this.index[word][entry.capabilityId] ?? 1) - 1;
104
+ if (this.index[word][entry.capabilityId] <= 0) {
105
+ delete this.index[word][entry.capabilityId];
106
+ }
107
+ if (Object.keys(this.index[word]).length === 0) {
108
+ delete this.index[word];
109
+ }
110
+ }
111
+ }
81
112
  rebuildIndex() {
82
113
  this.index = {};
83
114
  this.statsCounter = { totalQueries: 0, llmQueries: 0, cacheHits: 0, outOfScope: 0 };
@@ -128,9 +159,11 @@ export class FileLearningStore {
128
159
  this.updateIndex(entry);
129
160
  if (this.entries.length > MAX_LEARNING_ENTRIES) {
130
161
  const excess = this.entries.length - MAX_LEARNING_ENTRIES;
131
- this.entries.splice(0, excess);
132
- // Rebuild index after pruning — pruned entries may have affected counts
133
- this.rebuildIndex();
162
+ const pruned = this.entries.splice(0, excess);
163
+ // Subtract pruned entries from index O(pruned × w) instead of O(n × w) full rebuild
164
+ for (const entry of pruned) {
165
+ this.subtractFromIndex(entry);
166
+ }
134
167
  logger.debug(`Learning store pruned ${excess} oldest entries (cap: ${MAX_LEARNING_ENTRIES})`);
135
168
  }
136
169
  await this.save();
@@ -167,8 +200,11 @@ export class MemoryLearningStore {
167
200
  this.entries.push(entry);
168
201
  this.updateIndex(entry);
169
202
  if (this.entries.length > MAX_LEARNING_ENTRIES) {
170
- this.entries.splice(0, this.entries.length - MAX_LEARNING_ENTRIES);
171
- this.rebuildIndex();
203
+ const excess = this.entries.length - MAX_LEARNING_ENTRIES;
204
+ const pruned = this.entries.splice(0, excess);
205
+ for (const entry of pruned) {
206
+ this.subtractFromIndex(entry);
207
+ }
172
208
  }
173
209
  }
174
210
  async getStats() {
@@ -197,6 +233,37 @@ export class MemoryLearningStore {
197
233
  }
198
234
  }
199
235
  }
236
+ subtractFromIndex(entry) {
237
+ if (!entry.capabilityId) {
238
+ this.statsCounter.outOfScope = Math.max(0, this.statsCounter.outOfScope - 1);
239
+ this.statsCounter.totalQueries = Math.max(0, this.statsCounter.totalQueries - 1);
240
+ if (entry.resolvedVia === 'llm')
241
+ this.statsCounter.llmQueries = Math.max(0, this.statsCounter.llmQueries - 1);
242
+ if (entry.resolvedVia === 'cache')
243
+ this.statsCounter.cacheHits = Math.max(0, this.statsCounter.cacheHits - 1);
244
+ return;
245
+ }
246
+ this.statsCounter.totalQueries = Math.max(0, this.statsCounter.totalQueries - 1);
247
+ if (entry.resolvedVia === 'llm')
248
+ this.statsCounter.llmQueries = Math.max(0, this.statsCounter.llmQueries - 1);
249
+ if (entry.resolvedVia === 'cache')
250
+ this.statsCounter.cacheHits = Math.max(0, this.statsCounter.cacheHits - 1);
251
+ const words = entry.query.toLowerCase()
252
+ .split(/\W+/)
253
+ .filter(w => w.length > 2 && !STOPWORDS.has(w));
254
+ for (const word of words) {
255
+ if (!this.index[word])
256
+ continue;
257
+ this.index[word][entry.capabilityId] =
258
+ (this.index[word][entry.capabilityId] ?? 1) - 1;
259
+ if (this.index[word][entry.capabilityId] <= 0) {
260
+ delete this.index[word][entry.capabilityId];
261
+ }
262
+ if (Object.keys(this.index[word]).length === 0) {
263
+ delete this.index[word];
264
+ }
265
+ }
266
+ }
200
267
  rebuildIndex() {
201
268
  this.index = {};
202
269
  this.statsCounter = { totalQueries: 0, llmQueries: 0, cacheHits: 0, outOfScope: 0 };
@@ -16,4 +16,15 @@ export declare function match(query: string, manifest: Manifest): MatchResult;
16
16
  export interface LLMMatcherOptions {
17
17
  llm: (prompt: string) => Promise<string>;
18
18
  }
19
+ /**
20
+ * Matches a query to a capability using an LLM.
21
+ *
22
+ * ⚠️ SECURITY NOTE: Capability `description` and `examples` fields from the
23
+ * manifest are injected verbatim into the LLM prompt (system portion).
24
+ * In a solo deployment with a developer-controlled manifest this is safe.
25
+ * If your manifest is generated from third-party OpenAPI specs, user-controlled
26
+ * sources, or any external input, sanitize `description` and `examples` fields
27
+ * before passing the manifest to this function — adversarial content in those
28
+ * fields can influence LLM routing decisions.
29
+ */
19
30
  export declare function matchWithLLM(query: string, manifest: Manifest, options: LLMMatcherOptions): Promise<MatchResult>;
@@ -117,10 +117,11 @@ export function extractParams(query, cap) {
117
117
  const words = query.trim().split(/\s+/);
118
118
  const meaningful = words.filter(w => !STOPWORDS.has(w.toLowerCase()));
119
119
  const candidate = meaningful[meaningful.length - 1] ?? null;
120
- // Only use fallback if candidate looks like an identifier — not a generic noun or verb
120
+ // Only use fallback if candidate looks like an identifier — not a generic noun, verb,
121
+ // or category word that would produce junk URLs like /orders/orders or /users/data
121
122
  if (candidate &&
122
123
  /^[a-zA-Z0-9_-]{2,}$/.test(candidate) &&
123
- !/^(all|new|latest|recent|current|list|get|show|find|fetch|give|open|my|their|your)$/i.test(candidate)) {
124
+ !/^(all|new|latest|recent|current|list|get|show|find|fetch|give|open|my|their|your|orders|order|items|item|data|results|result|records|record|entries|entry|users|user|products|product|details|info|summary|history|status|feed|content|files|file|documents|document)$/i.test(candidate)) {
124
125
  extracted = candidate;
125
126
  }
126
127
  }
@@ -184,6 +185,17 @@ export function match(query, manifest) {
184
185
  candidates,
185
186
  };
186
187
  }
188
+ /**
189
+ * Matches a query to a capability using an LLM.
190
+ *
191
+ * ⚠️ SECURITY NOTE: Capability `description` and `examples` fields from the
192
+ * manifest are injected verbatim into the LLM prompt (system portion).
193
+ * In a solo deployment with a developer-controlled manifest this is safe.
194
+ * If your manifest is generated from third-party OpenAPI specs, user-controlled
195
+ * sources, or any external input, sanitize `description` and `examples` fields
196
+ * before passing the manifest to this function — adversarial content in those
197
+ * fields can influence LLM routing decisions.
198
+ */
187
199
  export async function matchWithLLM(query, manifest, options) {
188
200
  const manifestSummary = manifest.capabilities.map(c => `- ${c.id} (${c.resolver.type}): ${c.description}${c.examples?.length ? `\n examples: ${c.examples.slice(0, 2).join(', ')}` : ''}`).join('\n');
189
201
  const prompt = `You are an intent matcher for an AI agent system.
@@ -10,7 +10,21 @@ async function loadSpec(source) {
10
10
  // URL
11
11
  if (source.startsWith('http://') || source.startsWith('https://')) {
12
12
  logger.info(`Fetching OpenAPI spec from: ${source}`);
13
- const res = await fetch(source);
13
+ const controller = new AbortController();
14
+ const timer = setTimeout(() => controller.abort(), 10000);
15
+ // eslint-disable-next-line prefer-const
16
+ let res;
17
+ try {
18
+ res = await fetch(source, { signal: controller.signal });
19
+ clearTimeout(timer);
20
+ }
21
+ catch (err) {
22
+ clearTimeout(timer);
23
+ if (err instanceof Error && err.name === 'AbortError') {
24
+ throw new Error(`Timed out fetching spec from ${source} (10s limit)`);
25
+ }
26
+ throw err;
27
+ }
14
28
  if (!res.ok)
15
29
  throw new Error(`Failed to fetch spec: ${res.status} ${res.statusText}`);
16
30
  const text = await res.text();
@@ -44,11 +44,17 @@ export async function resolve(matchResult, params = {}, options = {}) {
44
44
  };
45
45
  }
46
46
  // ── Session param injection ───────────────────────────────────────────────
47
- // Inject auth.userId into any params marked as source: 'session'
47
+ // Inject auth.userId into params marked as source: 'session'
48
+ // Session params are only injected if they appear as {template} in the path —
49
+ // they must never leak into the query string as ?user_id=xyz
48
50
  const enrichedParams = { ...params };
49
51
  if (options.auth?.userId !== undefined && options.auth.userId !== '') {
52
+ const resolver = capability.resolver;
53
+ const pathTemplate = resolver.type === 'api' ? resolver.endpoints.map(e => e.path).join('') :
54
+ resolver.type === 'hybrid' ? resolver.api.endpoints.map(e => e.path).join('') :
55
+ resolver.type === 'nav' ? resolver.destination : '';
50
56
  for (const param of capability.params) {
51
- if (param.source === 'session') {
57
+ if (param.source === 'session' && pathTemplate.includes(`{${param.name}}`)) {
52
58
  enrichedParams[param.name] = options.auth.userId;
53
59
  logger.debug(`Injected session param "${param.name}" (value redacted)`);
54
60
  }
@@ -1 +1 @@
1
- export declare const VERSION = "0.5.0";
1
+ export declare const VERSION = "0.5.1";
@@ -1,2 +1,2 @@
1
1
  // Auto-generated by scripts/version.js — do not edit manually
2
- export const VERSION = '0.5.0';
2
+ export const VERSION = '0.5.1';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capman",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Capability Manifest Engine — let AI agents interact with your app without navigating the UI",
5
5
  "main": "./dist/cjs/index.js",
6
6
  "module": "./dist/esm/index.js",