capman 0.5.5 → 0.6.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.
@@ -18,40 +18,226 @@ export const STOPWORDS = new Set([
18
18
  'it', 'its', 'how', 'when', 'where', 'who', 'which', 'all',
19
19
  'just', 'some', 'any', 'there', 'their', 'them', 'they',
20
20
  ]);
21
- function filterStopwords(words) {
22
- return words.filter(w => !STOPWORDS.has(w.toLowerCase()) && w.length > 1);
21
+ // ─── Type Patterns ────────────────────────────────────────────────────────────
22
+ /**
23
+ * Regex patterns for common param types.
24
+ * Used when a CapabilityParam has `pattern` set to a named type.
25
+ */
26
+ export const TYPE_PATTERNS = {
27
+ email: /\b[\w.+-]+@[\w-]+\.[a-zA-Z]{2,}\b/,
28
+ date: /\b\d{4}-\d{2}-\d{2}\b|\b(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)\w*\s+\d{1,2}\b/i,
29
+ orderId: /\b[A-Z]{2,}-?\d{4,}\b|\b\d{6,}\b/,
30
+ url: /https?:\/\/[^\s]+/,
31
+ };
32
+ /**
33
+ * Extracts a value from a query using an example template pattern.
34
+ * e.g. template "order {orderId}", query "track order 12345" → "12345"
35
+ * e.g. template "booking {ref}", query "cancel booking ABC-001" → "ABC-001"
36
+ */
37
+ function extractFromTemplate(query, template, paramName) {
38
+ // Split template on {paramName} to get prefix and suffix
39
+ const placeholder = `{${paramName}}`;
40
+ const idx = template.indexOf(placeholder);
41
+ if (idx === -1)
42
+ return null;
43
+ const prefix = template.slice(0, idx).trim().toLowerCase();
44
+ const suffix = template.slice(idx + placeholder.length).trim().toLowerCase();
45
+ const q = query.toLowerCase();
46
+ if (prefix) {
47
+ const prefixIdx = q.indexOf(prefix);
48
+ if (prefixIdx === -1)
49
+ return null;
50
+ const after = query.slice(prefixIdx + prefix.length).trim();
51
+ const tokens = after.split(/\s+/).filter(t => t.length > 0);
52
+ if (!tokens.length)
53
+ return null;
54
+ // If there's a suffix, find it and take what's between
55
+ if (suffix) {
56
+ const suffixIdx = after.toLowerCase().indexOf(suffix);
57
+ if (suffixIdx > 0) {
58
+ return after.slice(0, suffixIdx).trim().split(/\s+/)[0] ?? null;
59
+ }
60
+ }
61
+ return tokens[0].replace(/[^a-zA-Z0-9\-_.@]/g, '') || null;
62
+ }
63
+ // Prefix is empty — placeholder is at start of template e.g. "{email} unsubscribe"
64
+ if (!prefix) {
65
+ if (suffix) {
66
+ // Find suffix in query — take what comes before it
67
+ const suffixIdx = query.toLowerCase().indexOf(suffix);
68
+ if (suffixIdx > 0) {
69
+ return query.slice(0, suffixIdx).trim().split(/\s+/).pop()
70
+ ?.replace(/[^a-zA-Z0-9\-_.@]/g, '') || null;
71
+ }
72
+ }
73
+ // No prefix, no suffix — template is just "{paramName}"; take last meaningful word
74
+ const words = query.trim().split(/\s+/);
75
+ return words[words.length - 1]?.replace(/[^a-zA-Z0-9\-_.@]/g, '') || null;
76
+ }
77
+ return null;
78
+ }
79
+ // ─── Stem cache ───────────────────────────────────────────────────────────────
80
+ // Each word stemmed exactly once per process — O(1) on repeat lookups.
81
+ // Module-level — persists for the process lifetime. Vocabulary in production
82
+ // is finite (capability names + user query vocabulary) so growth is bounded
83
+ // in practice. In test environments with synthetic random strings, this may
84
+ // grow larger but remains functionally harmless.
85
+ const stemCache = new Map();
86
+ /**
87
+ * Simplified suffix-stripping stemmer — 10 most common English morphological
88
+ * patterns covering ~80% of benefit at ~25% the complexity of Porter stemmer.
89
+ * Applied symmetrically to both query words and capability index words.
90
+ */
91
+ export function stem(word) {
92
+ const cached = stemCache.get(word);
93
+ if (cached !== undefined)
94
+ return cached;
95
+ let s = word;
96
+ if (s.length > 7 && s.endsWith('ation'))
97
+ s = s.slice(0, -5); // cancellation → cancell
98
+ else if (s.length > 6 && s.endsWith('tion'))
99
+ s = s.slice(0, -4); // completion → comple
100
+ else if (s.length > 6 && s.endsWith('ing'))
101
+ s = s.slice(0, -3); // tracking → track
102
+ else if (s.length > 6 && s.endsWith('ity'))
103
+ s = s.slice(0, -3); // availability → availabil
104
+ else if (s.length > 5 && s.endsWith('ion'))
105
+ s = s.slice(0, -3); // version → vers
106
+ else if (s.length > 6 && s.endsWith('est'))
107
+ s = s.slice(0, -3); // fastest → fast
108
+ else if (s.length > 4 && s.endsWith('er'))
109
+ s = s.slice(0, -2); // tracker → track
110
+ else if (s.length > 4 && s.endsWith('ed'))
111
+ s = s.slice(0, -2); // ordered → order
112
+ else if (s.length > 4 && s.endsWith('ly'))
113
+ s = s.slice(0, -2); // quickly → quick
114
+ else if (s.length > 4 && s.endsWith('es'))
115
+ s = s.slice(0, -2); // fetches → fetch
116
+ else if (s.length > 3 && s.endsWith('s') &&
117
+ !s.endsWith('ss'))
118
+ s = s.slice(0, -1); // orders → order
119
+ stemCache.set(word, s);
120
+ return s;
121
+ }
122
+ /**
123
+ * Shared tokenizer — used by scorer, learning index, and boost system.
124
+ * Applies stopword filtering AND stemming symmetrically.
125
+ * Any site that tokenizes text for matching MUST use this function
126
+ * to avoid silent mismatches between query and index tokens.
127
+ */
128
+ export function tokenize(text) {
129
+ return text
130
+ .toLowerCase()
131
+ .split(/\W+/)
132
+ .filter(w => w.length > 2 && !STOPWORDS.has(w))
133
+ .map(stem);
134
+ }
135
+ /** Build a BM25 index over all capabilities. Call once at manifest load. */
136
+ export function buildBM25Index(capabilities) {
137
+ const N = capabilities.length;
138
+ if (N === 0)
139
+ return { df: {}, avgdl: { examples: 0, description: 0, name: 0 }, N: 0, bigrams: {}, };
140
+ const df = {};
141
+ let totalExLen = 0;
142
+ let totalDescLen = 0;
143
+ let totalNameLen = 0;
144
+ for (const cap of capabilities) {
145
+ const exTokens = tokenize((cap.examples ?? []).join(' '));
146
+ const descTokens = tokenize(cap.description);
147
+ const nameTokens = tokenize(cap.name);
148
+ totalExLen += exTokens.length;
149
+ totalDescLen += descTokens.length;
150
+ totalNameLen += nameTokens.length;
151
+ // Count document frequency — each term counted once per capability
152
+ const seen = new Set();
153
+ for (const t of [...exTokens, ...descTokens, ...nameTokens]) {
154
+ if (!seen.has(t)) {
155
+ df[t] = (df[t] ?? 0) + 1;
156
+ seen.add(t);
157
+ }
158
+ }
159
+ }
160
+ // Build bigram sets per capability — examples field only
161
+ // Clean bigrams only: post-stopword, post-stem tokens
162
+ const bigrams = {};
163
+ for (const cap of capabilities) {
164
+ const set = new Set();
165
+ for (const example of cap.examples ?? []) {
166
+ for (const bg of extractBigrams(tokenize(example)))
167
+ set.add(bg);
168
+ }
169
+ bigrams[cap.id] = set;
170
+ }
171
+ return {
172
+ df,
173
+ avgdl: {
174
+ examples: totalExLen / N,
175
+ description: totalDescLen / N,
176
+ name: totalNameLen / N,
177
+ },
178
+ N,
179
+ bigrams,
180
+ };
23
181
  }
24
- function scoreCapability(qWordSet, cap) {
182
+ /**
183
+ * BM25 scoring with field weights.
184
+ * k1 = 1.5 (TF saturation), b = 0.75 (length normalization)
185
+ * Field weights: examples 0.6, description 0.3, name 0.1
186
+ */
187
+ export function scoreCapability(qWordSet, cap, index, k1 = 1.5, b = 0.75) {
188
+ if (index.N === 0)
189
+ return 0;
190
+ const score = bm25Field(qWordSet, tokenize((cap.examples ?? []).join(' ')), index, 'examples', k1, b) * 0.6
191
+ + bm25Field(qWordSet, tokenize(cap.description), index, 'description', k1, b) * 0.3
192
+ + bm25Field(qWordSet, tokenize(cap.name), index, 'name', k1, b) * 0.1;
193
+ return score;
194
+ }
195
+ function bm25Field(queryTerms, fieldTokens, index, field, k1, b) {
196
+ if (fieldTokens.length === 0)
197
+ return 0;
198
+ const avgdl = index.avgdl[field] || 1;
199
+ const dl = fieldTokens.length;
200
+ const tf = new Map();
201
+ for (const t of fieldTokens) {
202
+ tf.set(t, (tf.get(t) ?? 0) + 1);
203
+ }
25
204
  let score = 0;
26
- // Check examples take the best single example match, not the sum.
27
- // Accumulating across examples rewards bloated example lists over precise ones:
28
- // 10 examples at 50% overlap = 300 points (clamped to 60) beats 1 perfect example at 60.
29
- // Taking Math.max means quality of examples matters, not quantity.
30
- let bestExampleScore = 0;
31
- for (const example of cap.examples ?? []) {
32
- const exWords = filterStopwords(example.toLowerCase().split(/\s+/));
33
- if (exWords.length === 0)
205
+ for (const term of queryTerms) {
206
+ const termTf = tf.get(term) ?? 0;
207
+ if (termTf === 0)
34
208
  continue;
35
- const overlap = exWords.filter(w => qWordSet.has(w)).length;
36
- const contribution = (overlap / exWords.length) * 60;
37
- bestExampleScore = Math.max(bestExampleScore, contribution);
209
+ const df = index.df[term] ?? 0;
210
+ const idf = Math.log((index.N - df + 0.5) / (df + 0.5) + 1);
211
+ const tfNorm = (termTf * (k1 + 1)) / (termTf + k1 * (1 - b + b * (dl / avgdl)));
212
+ score += idf * tfNorm;
38
213
  }
39
- score += bestExampleScore;
40
- // Check description words — normalize against min(length, 10) to avoid
41
- // penalizing rich documentation (many words = lower ratio) while also
42
- // preventing single-word descriptions from maxing out on any match.
43
- const descWords = filterStopwords(cap.description.toLowerCase().split(/\W+/).filter(Boolean));
44
- if (descWords.length > 0) {
45
- const descOverlap = descWords.filter(w => qWordSet.has(w)).length;
46
- score += Math.min((descOverlap / Math.min(descWords.length, 10)) * 30, 30);
214
+ return score;
215
+ }
216
+ /**
217
+ * Extracts bigrams from a token array as "token1__token2" strings.
218
+ * Input must already be post-stopword and post-stem (use tokenize() first).
219
+ */
220
+ export function extractBigrams(tokens) {
221
+ const bigrams = new Set();
222
+ for (let i = 0; i < tokens.length - 1; i++) {
223
+ bigrams.add(`${tokens[i]}__${tokens[i + 1]}`);
47
224
  }
48
- // Check name words
49
- const nameWords = filterStopwords(cap.name.toLowerCase().split(/\W+/).filter(Boolean));
50
- if (nameWords.length > 0) {
51
- const nameOverlap = nameWords.filter(w => qWordSet.has(w)).length;
52
- score += (nameOverlap / nameWords.length) * 10;
225
+ return bigrams;
226
+ }
227
+ /**
228
+ * Returns a fixed bonus in normalized points (0–15), applied after BM25 normalization.
229
+ * 5 points per matching bigram, saturates at 3 bigrams (15 points).
230
+ * Fixed point value regardless of manifest size — ceiling-independent.
231
+ */
232
+ function bigramBonus(queryBigrams, capBigrams) {
233
+ if (queryBigrams.size === 0 || capBigrams.size === 0)
234
+ return 0;
235
+ let overlap = 0;
236
+ for (const bigram of queryBigrams) {
237
+ if (capBigrams.has(bigram))
238
+ overlap++;
53
239
  }
54
- return Math.min(Math.round(score), 100);
240
+ return Math.min(overlap * 5, 15); // normalized points — 3 bigrams saturate at 15
55
241
  }
56
242
  export function resolverToIntent(cap) {
57
243
  const t = cap.resolver.type;
@@ -68,7 +254,7 @@ export function resolverToIntent(cap) {
68
254
  * capability field values before injection into the system prompt.
69
255
  * Removes control characters, newlines, and delimiter-like sequences.
70
256
  */
71
- function sanitizeForPrompt(value, maxLen) {
257
+ export function sanitizeForPrompt(value, maxLen) {
72
258
  return value
73
259
  .replace(/[\r\n\t]/g, ' ') // newlines → space
74
260
  .replace(/---+/g, '—') // horizontal rules → em dash
@@ -104,6 +290,26 @@ export function extractParams(query, cap) {
104
290
  result[param.name] = null;
105
291
  continue;
106
292
  }
293
+ // ── Pattern extraction (highest priority) ─────────────────────────────
294
+ if (param.pattern) {
295
+ const namedPattern = TYPE_PATTERNS[param.pattern];
296
+ if (namedPattern) {
297
+ // Named type pattern — match regex directly against full query
298
+ const match = query.match(namedPattern);
299
+ if (match) {
300
+ result[param.name] = match[0];
301
+ continue;
302
+ }
303
+ }
304
+ else if (param.pattern.includes(`{${param.name}}`)) {
305
+ // Example template — positional extraction
306
+ const extracted = extractFromTemplate(query, param.pattern, param.name);
307
+ if (extracted) {
308
+ result[param.name] = extracted;
309
+ continue;
310
+ }
311
+ }
312
+ }
107
313
  // Try to extract value after known keywords
108
314
  // e.g. "profile for johndoe" → johndoe
109
315
  // "articles by jane" → jane
@@ -225,10 +431,34 @@ export function match(query, manifest, options = {}) {
225
431
  }
226
432
  // ── Score all capabilities ────────────────────────────────────────────────
227
433
  // Build qWordSet once — O(1) lookups instead of O(n) Array.includes per word
228
- const qWordSet = new Set(filterStopwords(query.toLowerCase().split(/\W+/).filter(Boolean)));
434
+ const qTokens = tokenize(query);
435
+ const qWordSet = new Set(qTokens);
436
+ // Build query bigrams for phrase bonus
437
+ const qBigrams = extractBigrams(qTokens);
438
+ // Build BM25 index for this manifest — O(capabilities × tokens)
439
+ // In CapmanEngine this is pre-built; for direct match() calls it's built per-call
440
+ const bm25Index = options.bm25Index ?? buildBM25Index(manifest.capabilities);
441
+ const k1 = options.bm25K1 ?? 1.5;
442
+ const b = options.bm25B ?? 0.75;
443
+ // Calibrate ceiling — max self-score for normalization
444
+ const ceiling = options.bm25Ceiling ?? (() => {
445
+ let max = 0;
446
+ for (const cap of manifest.capabilities) {
447
+ if (!cap.examples?.length)
448
+ continue;
449
+ const selfWords = new Set(tokenize(cap.examples[0]));
450
+ const raw = scoreCapability(selfWords, cap, bm25Index, k1, b);
451
+ if (raw > max)
452
+ max = raw;
453
+ }
454
+ return max > 0 ? max : 100;
455
+ })();
229
456
  const allScores = [];
230
457
  for (const cap of manifest.capabilities) {
231
- const keywordScore = scoreCapability(qWordSet, cap);
458
+ const rawBM25 = scoreCapability(qWordSet, cap, bm25Index, k1, b);
459
+ const bm25Score = Math.min(100, Math.round((rawBM25 / ceiling) * 100));
460
+ const bonusPoints = bigramBonus(qBigrams, bm25Index.bigrams[cap.id] ?? new Set());
461
+ const keywordScore = Math.min(100, bm25Score + bonusPoints);
232
462
  const fuzzyScore = fuzzyScoreMap.get(cap.id) ?? 0;
233
463
  const via = fuzzyScore > keywordScore ? 'fuzzy' : 'keyword';
234
464
  const score = Math.min(100, Math.round(Math.max(keywordScore, fuzzyScore)));
@@ -13,18 +13,21 @@ export declare const CapmanConfigSchema: z.ZodEffects<z.ZodObject<{
13
13
  required: z.ZodBoolean;
14
14
  source: z.ZodEnum<["user_query", "session"]>;
15
15
  default: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
16
+ pattern: z.ZodOptional<z.ZodString>;
16
17
  }, "strip", z.ZodTypeAny, {
17
18
  name: string;
18
- required: boolean;
19
19
  description: string;
20
+ required: boolean;
20
21
  source: "user_query" | "session";
21
22
  default?: string | number | boolean | undefined;
23
+ pattern?: string | undefined;
22
24
  }, {
23
25
  name: string;
24
- required: boolean;
25
26
  description: string;
27
+ required: boolean;
26
28
  source: "user_query" | "session";
27
29
  default?: string | number | boolean | undefined;
30
+ pattern?: string | undefined;
28
31
  }>, "many">;
29
32
  returns: z.ZodArray<z.ZodString, "many">;
30
33
  resolver: z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
@@ -147,14 +150,15 @@ export declare const CapmanConfigSchema: z.ZodEffects<z.ZodObject<{
147
150
  }, "strip", z.ZodTypeAny, {
148
151
  name: string;
149
152
  id: string;
153
+ description: string;
150
154
  params: {
151
155
  name: string;
152
- required: boolean;
153
156
  description: string;
157
+ required: boolean;
154
158
  source: "user_query" | "session";
155
159
  default?: string | number | boolean | undefined;
160
+ pattern?: string | undefined;
156
161
  }[];
157
- description: string;
158
162
  returns: string[];
159
163
  resolver: {
160
164
  type: "api";
@@ -189,14 +193,15 @@ export declare const CapmanConfigSchema: z.ZodEffects<z.ZodObject<{
189
193
  }, {
190
194
  name: string;
191
195
  id: string;
196
+ description: string;
192
197
  params: {
193
198
  name: string;
194
- required: boolean;
195
199
  description: string;
200
+ required: boolean;
196
201
  source: "user_query" | "session";
197
202
  default?: string | number | boolean | undefined;
203
+ pattern?: string | undefined;
198
204
  }[];
199
- description: string;
200
205
  returns: string[];
201
206
  resolver: {
202
207
  type: "api";
@@ -231,14 +236,15 @@ export declare const CapmanConfigSchema: z.ZodEffects<z.ZodObject<{
231
236
  }>, "many">, {
232
237
  name: string;
233
238
  id: string;
239
+ description: string;
234
240
  params: {
235
241
  name: string;
236
- required: boolean;
237
242
  description: string;
243
+ required: boolean;
238
244
  source: "user_query" | "session";
239
245
  default?: string | number | boolean | undefined;
246
+ pattern?: string | undefined;
240
247
  }[];
241
- description: string;
242
248
  returns: string[];
243
249
  resolver: {
244
250
  type: "api";
@@ -273,14 +279,15 @@ export declare const CapmanConfigSchema: z.ZodEffects<z.ZodObject<{
273
279
  }[], {
274
280
  name: string;
275
281
  id: string;
282
+ description: string;
276
283
  params: {
277
284
  name: string;
278
- required: boolean;
279
285
  description: string;
286
+ required: boolean;
280
287
  source: "user_query" | "session";
281
288
  default?: string | number | boolean | undefined;
289
+ pattern?: string | undefined;
282
290
  }[];
283
- description: string;
284
291
  returns: string[];
285
292
  resolver: {
286
293
  type: "api";
@@ -318,14 +325,15 @@ export declare const CapmanConfigSchema: z.ZodEffects<z.ZodObject<{
318
325
  capabilities: {
319
326
  name: string;
320
327
  id: string;
328
+ description: string;
321
329
  params: {
322
330
  name: string;
323
- required: boolean;
324
331
  description: string;
332
+ required: boolean;
325
333
  source: "user_query" | "session";
326
334
  default?: string | number | boolean | undefined;
335
+ pattern?: string | undefined;
327
336
  }[];
328
- description: string;
329
337
  returns: string[];
330
338
  resolver: {
331
339
  type: "api";
@@ -364,14 +372,15 @@ export declare const CapmanConfigSchema: z.ZodEffects<z.ZodObject<{
364
372
  capabilities: {
365
373
  name: string;
366
374
  id: string;
375
+ description: string;
367
376
  params: {
368
377
  name: string;
369
- required: boolean;
370
378
  description: string;
379
+ required: boolean;
371
380
  source: "user_query" | "session";
372
381
  default?: string | number | boolean | undefined;
382
+ pattern?: string | undefined;
373
383
  }[];
374
- description: string;
375
384
  returns: string[];
376
385
  resolver: {
377
386
  type: "api";
@@ -410,14 +419,15 @@ export declare const CapmanConfigSchema: z.ZodEffects<z.ZodObject<{
410
419
  capabilities: {
411
420
  name: string;
412
421
  id: string;
422
+ description: string;
413
423
  params: {
414
424
  name: string;
415
- required: boolean;
416
425
  description: string;
426
+ required: boolean;
417
427
  source: "user_query" | "session";
418
428
  default?: string | number | boolean | undefined;
429
+ pattern?: string | undefined;
419
430
  }[];
420
- description: string;
421
431
  returns: string[];
422
432
  resolver: {
423
433
  type: "api";
@@ -456,14 +466,15 @@ export declare const CapmanConfigSchema: z.ZodEffects<z.ZodObject<{
456
466
  capabilities: {
457
467
  name: string;
458
468
  id: string;
469
+ description: string;
459
470
  params: {
460
471
  name: string;
461
- required: boolean;
462
472
  description: string;
473
+ required: boolean;
463
474
  source: "user_query" | "session";
464
475
  default?: string | number | boolean | undefined;
476
+ pattern?: string | undefined;
465
477
  }[];
466
- description: string;
467
478
  returns: string[];
468
479
  resolver: {
469
480
  type: "api";
@@ -513,18 +524,21 @@ export declare const ManifestSchema: z.ZodObject<{
513
524
  required: z.ZodBoolean;
514
525
  source: z.ZodEnum<["user_query", "session"]>;
515
526
  default: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
527
+ pattern: z.ZodOptional<z.ZodString>;
516
528
  }, "strip", z.ZodTypeAny, {
517
529
  name: string;
518
- required: boolean;
519
530
  description: string;
531
+ required: boolean;
520
532
  source: "user_query" | "session";
521
533
  default?: string | number | boolean | undefined;
534
+ pattern?: string | undefined;
522
535
  }, {
523
536
  name: string;
524
- required: boolean;
525
537
  description: string;
538
+ required: boolean;
526
539
  source: "user_query" | "session";
527
540
  default?: string | number | boolean | undefined;
541
+ pattern?: string | undefined;
528
542
  }>, "many">;
529
543
  returns: z.ZodArray<z.ZodString, "many">;
530
544
  resolver: z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
@@ -647,14 +661,15 @@ export declare const ManifestSchema: z.ZodObject<{
647
661
  }, "strip", z.ZodTypeAny, {
648
662
  name: string;
649
663
  id: string;
664
+ description: string;
650
665
  params: {
651
666
  name: string;
652
- required: boolean;
653
667
  description: string;
668
+ required: boolean;
654
669
  source: "user_query" | "session";
655
670
  default?: string | number | boolean | undefined;
671
+ pattern?: string | undefined;
656
672
  }[];
657
- description: string;
658
673
  returns: string[];
659
674
  resolver: {
660
675
  type: "api";
@@ -689,14 +704,15 @@ export declare const ManifestSchema: z.ZodObject<{
689
704
  }, {
690
705
  name: string;
691
706
  id: string;
707
+ description: string;
692
708
  params: {
693
709
  name: string;
694
- required: boolean;
695
710
  description: string;
711
+ required: boolean;
696
712
  source: "user_query" | "session";
697
713
  default?: string | number | boolean | undefined;
714
+ pattern?: string | undefined;
698
715
  }[];
699
- description: string;
700
716
  returns: string[];
701
717
  resolver: {
702
718
  type: "api";
@@ -735,14 +751,15 @@ export declare const ManifestSchema: z.ZodObject<{
735
751
  capabilities: {
736
752
  name: string;
737
753
  id: string;
754
+ description: string;
738
755
  params: {
739
756
  name: string;
740
- required: boolean;
741
757
  description: string;
758
+ required: boolean;
742
759
  source: "user_query" | "session";
743
760
  default?: string | number | boolean | undefined;
761
+ pattern?: string | undefined;
744
762
  }[];
745
- description: string;
746
763
  returns: string[];
747
764
  resolver: {
748
765
  type: "api";
@@ -782,14 +799,15 @@ export declare const ManifestSchema: z.ZodObject<{
782
799
  capabilities: {
783
800
  name: string;
784
801
  id: string;
802
+ description: string;
785
803
  params: {
786
804
  name: string;
787
- required: boolean;
788
805
  description: string;
806
+ required: boolean;
789
807
  source: "user_query" | "session";
790
808
  default?: string | number | boolean | undefined;
809
+ pattern?: string | undefined;
791
810
  }[];
792
- description: string;
793
811
  returns: string[];
794
812
  resolver: {
795
813
  type: "api";
@@ -6,6 +6,7 @@ const CapabilityParamSchema = z.object({
6
6
  required: z.boolean(),
7
7
  source: z.enum(['user_query', 'session']),
8
8
  default: z.union([z.string(), z.number(), z.boolean()]).optional(),
9
+ pattern: z.string().optional(),
9
10
  });
10
11
  // ─── Resolver Schemas ─────────────────────────────────────────────────────────
11
12
  const ApiResolverSchema = z.object({
@@ -5,7 +5,13 @@ export interface CapabilityParam {
5
5
  description: string;
6
6
  required: boolean;
7
7
  source: 'user_query' | 'session';
8
- default?: string | number | boolean;
8
+ /**
9
+ * Optional extraction hint. Either a named type or an example template.
10
+ * Named types: 'email' | 'date' | 'orderId' | 'url'
11
+ * Example template: "order {paramName}" — extracts token after "order"
12
+ * When provided, pattern matching runs before keyword heuristics.
13
+ */
14
+ pattern?: string;
9
15
  }
10
16
  export interface ApiResolver {
11
17
  type: 'api';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capman",
3
- "version": "0.5.5",
3
+ "version": "0.6.0",
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",