capman 0.5.1 → 0.5.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 (60) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/CODEBASE.md +2 -1
  3. package/bin/lib/cmd-demo.js +2 -2
  4. package/dist/cjs/cache.d.ts +4 -1
  5. package/dist/cjs/cache.d.ts.map +1 -1
  6. package/dist/cjs/cache.js +42 -20
  7. package/dist/cjs/cache.js.map +1 -1
  8. package/dist/cjs/engine.d.ts +3 -1
  9. package/dist/cjs/engine.d.ts.map +1 -1
  10. package/dist/cjs/engine.js +103 -34
  11. package/dist/cjs/engine.js.map +1 -1
  12. package/dist/cjs/generator.d.ts.map +1 -1
  13. package/dist/cjs/generator.js +16 -1
  14. package/dist/cjs/generator.js.map +1 -1
  15. package/dist/cjs/index.d.ts +3 -2
  16. package/dist/cjs/index.d.ts.map +1 -1
  17. package/dist/cjs/index.js +3 -1
  18. package/dist/cjs/index.js.map +1 -1
  19. package/dist/cjs/learning.d.ts +20 -11
  20. package/dist/cjs/learning.d.ts.map +1 -1
  21. package/dist/cjs/learning.js +169 -135
  22. package/dist/cjs/learning.js.map +1 -1
  23. package/dist/cjs/matcher.d.ts +4 -1
  24. package/dist/cjs/matcher.d.ts.map +1 -1
  25. package/dist/cjs/matcher.js +33 -13
  26. package/dist/cjs/matcher.js.map +1 -1
  27. package/dist/cjs/parser.js +10 -4
  28. package/dist/cjs/parser.js.map +1 -1
  29. package/dist/cjs/resolver.d.ts +7 -0
  30. package/dist/cjs/resolver.d.ts.map +1 -1
  31. package/dist/cjs/resolver.js +63 -19
  32. package/dist/cjs/resolver.js.map +1 -1
  33. package/dist/cjs/schema.d.ts +106 -14
  34. package/dist/cjs/schema.d.ts.map +1 -1
  35. package/dist/cjs/schema.js +9 -4
  36. package/dist/cjs/schema.js.map +1 -1
  37. package/dist/cjs/types.d.ts +1 -0
  38. package/dist/cjs/types.d.ts.map +1 -1
  39. package/dist/cjs/version.d.ts +1 -1
  40. package/dist/cjs/version.js +1 -1
  41. package/dist/esm/cache.d.ts +4 -1
  42. package/dist/esm/cache.js +42 -20
  43. package/dist/esm/engine.d.ts +3 -1
  44. package/dist/esm/engine.js +104 -35
  45. package/dist/esm/generator.js +16 -1
  46. package/dist/esm/index.d.ts +3 -2
  47. package/dist/esm/index.js +1 -0
  48. package/dist/esm/learning.d.ts +20 -11
  49. package/dist/esm/learning.js +169 -135
  50. package/dist/esm/matcher.d.ts +4 -1
  51. package/dist/esm/matcher.js +31 -12
  52. package/dist/esm/parser.js +10 -4
  53. package/dist/esm/resolver.d.ts +7 -0
  54. package/dist/esm/resolver.js +63 -19
  55. package/dist/esm/schema.d.ts +106 -14
  56. package/dist/esm/schema.js +9 -4
  57. package/dist/esm/types.d.ts +1 -0
  58. package/dist/esm/version.d.ts +1 -1
  59. package/dist/esm/version.js +1 -1
  60. package/package.json +1 -1
@@ -1,4 +1,6 @@
1
1
  import { logger } from './logger';
2
+ // ─── Constants ────────────────────────────────────────────────────────────────
3
+ const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']);
2
4
  function redactParams(params) {
3
5
  return Object.fromEntries(Object.entries(params).map(([k, v]) => [k, v != null ? '[REDACTED]' : 'null']));
4
6
  }
@@ -49,12 +51,8 @@ export async function resolve(matchResult, params = {}, options = {}) {
49
51
  // they must never leak into the query string as ?user_id=xyz
50
52
  const enrichedParams = { ...params };
51
53
  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 : '';
56
54
  for (const param of capability.params) {
57
- if (param.source === 'session' && pathTemplate.includes(`{${param.name}}`)) {
55
+ if (param.source === 'session') {
58
56
  enrichedParams[param.name] = options.auth.userId;
59
57
  logger.debug(`Injected session param "${param.name}" (value redacted)`);
60
58
  }
@@ -65,15 +63,18 @@ export async function resolve(matchResult, params = {}, options = {}) {
65
63
  logger.debug(`Params: ${JSON.stringify(redactParams(params))}`);
66
64
  logger.debug(`Options: baseUrl=${options.baseUrl} dryRun=${options.dryRun}`);
67
65
  try {
66
+ const sessionParamNames = new Set(capability.params
67
+ .filter(p => p.source === 'session')
68
+ .map(p => p.name));
68
69
  switch (resolver.type) {
69
70
  case 'api':
70
- return await resolveApi(resolver, enrichedParams, options);
71
+ return await resolveApi(resolver, enrichedParams, options, sessionParamNames);
71
72
  case 'nav':
72
73
  return resolveNav(resolver, enrichedParams);
73
74
  case 'hybrid': {
74
75
  logger.debug('Hybrid resolver — running API and nav in parallel');
75
76
  const [apiResult, navResult] = await Promise.all([
76
- resolveApi(resolver.api, enrichedParams, options),
77
+ resolveApi(resolver.api, enrichedParams, options, sessionParamNames),
77
78
  Promise.resolve(resolveNav(resolver.nav, enrichedParams)),
78
79
  ]);
79
80
  return {
@@ -95,15 +96,40 @@ export async function resolve(matchResult, params = {}, options = {}) {
95
96
  };
96
97
  }
97
98
  }
98
- async function resolveApi(resolver, params, options) {
99
+ /**
100
+ * Resolves an API capability by executing all configured endpoints.
101
+ *
102
+ * ⚠️ PARALLEL EXECUTION: All endpoints are fired simultaneously via Promise.all().
103
+ * If any endpoint fails, the entire result is marked as failed and partial results
104
+ * are discarded — but side effects from successful endpoints cannot be rolled back.
105
+ *
106
+ * Example: a capability with two endpoints [POST /reserve, POST /confirm] will
107
+ * fire both in parallel. If /confirm fails after /reserve succeeded, the reservation
108
+ * exists but the caller receives success: false with no indication that /reserve ran.
109
+ *
110
+ * For capabilities where ordering or rollback matters, define separate capabilities
111
+ * with single endpoints and orchestrate them at the application layer.
112
+ */
113
+ async function resolveApi(resolver, params, options, sessionParamNames = new Set()) {
99
114
  const startTime = Date.now();
100
115
  const retries = options.retries ?? 0;
101
116
  const timeoutMs = options.timeoutMs ?? 5000;
102
- const apiCalls = resolver.endpoints.map(endpoint => ({
103
- method: endpoint.method,
104
- url: buildUrl(options.baseUrl ?? '', endpoint.path, params),
105
- params,
106
- }));
117
+ const apiCalls = resolver.endpoints.map(endpoint => {
118
+ // Build per-endpoint params — only inject session params if this
119
+ // specific endpoint has the placeholder. Prevents userId leaking
120
+ // as ?user_id=xyz on endpoints that don't use it in their path.
121
+ const endpointParams = { ...params };
122
+ for (const name of sessionParamNames) {
123
+ if (!endpoint.path.includes(`{${name}}`)) {
124
+ delete endpointParams[name]; // strip session param — not in this endpoint's path
125
+ }
126
+ }
127
+ return {
128
+ method: endpoint.method,
129
+ url: buildUrl(options.baseUrl ?? '', endpoint.path, endpointParams),
130
+ params: Object.fromEntries(Object.entries(endpointParams).filter(([, v]) => v !== null && v !== undefined)),
131
+ };
132
+ });
107
133
  if (options.dryRun) {
108
134
  return { success: true, resolverType: 'api', apiCalls, durationMs: Date.now() - startTime };
109
135
  }
@@ -116,9 +142,14 @@ async function resolveApi(resolver, params, options) {
116
142
  };
117
143
  }
118
144
  // ── Fetch with retry + timeout (iterative — no recursion) ────────────────
145
+ // Only retry safe/idempotent methods — retrying POST/PUT/PATCH/DELETE
146
+ // can cause duplicate side effects (e.g. duplicate orders, double charges).
119
147
  async function fetchWithRetry(call) {
148
+ const effectiveRetries = (options.retryAllMethods || SAFE_METHODS.has(call.method))
149
+ ? retries
150
+ : 0;
120
151
  let lastErr;
121
- for (let attempt = 0; attempt <= retries; attempt++) {
152
+ for (let attempt = 0; attempt <= effectiveRetries; attempt++) {
122
153
  const controller = new AbortController();
123
154
  const timer = setTimeout(() => controller.abort(), timeoutMs);
124
155
  try {
@@ -127,7 +158,7 @@ async function resolveApi(resolver, params, options) {
127
158
  headers: options.headers ?? {},
128
159
  signal: controller.signal,
129
160
  body: ['POST', 'PUT', 'PATCH'].includes(call.method)
130
- ? JSON.stringify(call.params)
161
+ ? JSON.stringify(Object.fromEntries(Object.entries(call.params).filter(([, v]) => v !== null && v !== undefined)))
131
162
  : undefined,
132
163
  });
133
164
  clearTimeout(timer);
@@ -137,8 +168,8 @@ async function resolveApi(resolver, params, options) {
137
168
  clearTimeout(timer);
138
169
  lastErr = err;
139
170
  const isTimeout = err instanceof Error && err.name === 'AbortError';
140
- if (attempt < retries) {
141
- logger.warn(`Request failed (attempt ${attempt + 1}/${retries + 1}) — retrying: ${isTimeout ? 'timeout' : err}`);
171
+ if (attempt < effectiveRetries) {
172
+ logger.warn(`Request failed (attempt ${attempt + 1}/${effectiveRetries + 1}) — retrying: ${isTimeout ? 'timeout' : err}`);
142
173
  }
143
174
  else {
144
175
  throw isTimeout ? new Error(`Request timed out after ${timeoutMs}ms`) : err;
@@ -191,10 +222,21 @@ function resolveNav(resolver, params) {
191
222
  continue;
192
223
  const str = String(value);
193
224
  validateNavParam(key, str);
194
- destination = destination.replace(`{${key}}`, encodeURIComponent(str));
225
+ destination = destination.replaceAll(`{${key}}`, encodeURIComponent(str));
195
226
  }
196
227
  return { success: true, resolverType: 'nav', navTarget: destination };
197
228
  }
229
+ function validateApiPathParam(key, value) {
230
+ // Prevent path traversal via unencoded slashes — encodeURIComponent does not
231
+ // encode '/' so a value like '../../admin' would traverse the path hierarchy.
232
+ // This mirrors the allowlist validation already applied in resolveNav().
233
+ if (!/^[a-zA-Z0-9_\-.:@]+$/.test(value)) {
234
+ throw new Error(`API path param "${key}" contains invalid characters: "${value}". ` +
235
+ `Only alphanumeric, hyphens, underscores, dots, colons, and @ are allowed.`);
236
+ }
237
+ }
238
+ // Both buildUrl (API) and resolveNav (nav) validate path param values against
239
+ // an allowlist before substitution — prevents path traversal via unencoded slashes.
198
240
  function buildUrl(baseUrl, urlPath, params) {
199
241
  let resolved = urlPath;
200
242
  const unused = {};
@@ -202,7 +244,9 @@ function buildUrl(baseUrl, urlPath, params) {
202
244
  if (value === null || value === undefined)
203
245
  continue; // never write null into URLs
204
246
  if (resolved.includes(`{${key}}`)) {
205
- resolved = resolved.replace(`{${key}}`, encodeURIComponent(String(value)));
247
+ const str = String(value);
248
+ validateApiPathParam(key, str);
249
+ resolved = resolved.replaceAll(`{${key}}`, encodeURIComponent(str));
206
250
  }
207
251
  else {
208
252
  unused[key] = value;
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- export declare const CapmanConfigSchema: z.ZodObject<{
2
+ export declare const CapmanConfigSchema: z.ZodEffects<z.ZodObject<{
3
3
  app: z.ZodString;
4
4
  baseUrl: z.ZodOptional<z.ZodString>;
5
5
  capabilities: z.ZodEffects<z.ZodArray<z.ZodObject<{
@@ -108,6 +108,7 @@ export declare const CapmanConfigSchema: z.ZodObject<{
108
108
  hint?: string | undefined;
109
109
  }>;
110
110
  }, "strip", z.ZodTypeAny, {
111
+ type: "hybrid";
111
112
  api: {
112
113
  endpoints: {
113
114
  method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
@@ -119,8 +120,8 @@ export declare const CapmanConfigSchema: z.ZodObject<{
119
120
  destination: string;
120
121
  hint?: string | undefined;
121
122
  };
122
- type: "hybrid";
123
123
  }, {
124
+ type: "hybrid";
124
125
  api: {
125
126
  endpoints: {
126
127
  method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
@@ -132,7 +133,6 @@ export declare const CapmanConfigSchema: z.ZodObject<{
132
133
  destination: string;
133
134
  hint?: string | undefined;
134
135
  };
135
- type: "hybrid";
136
136
  }>]>;
137
137
  privacy: z.ZodObject<{
138
138
  level: z.ZodEnum<["public", "user_owned", "admin"]>;
@@ -168,6 +168,7 @@ export declare const CapmanConfigSchema: z.ZodObject<{
168
168
  destination: string;
169
169
  hint?: string | undefined;
170
170
  } | {
171
+ type: "hybrid";
171
172
  api: {
172
173
  endpoints: {
173
174
  method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
@@ -179,7 +180,6 @@ export declare const CapmanConfigSchema: z.ZodObject<{
179
180
  destination: string;
180
181
  hint?: string | undefined;
181
182
  };
182
- type: "hybrid";
183
183
  };
184
184
  privacy: {
185
185
  level: "public" | "user_owned" | "admin";
@@ -210,6 +210,7 @@ export declare const CapmanConfigSchema: z.ZodObject<{
210
210
  destination: string;
211
211
  hint?: string | undefined;
212
212
  } | {
213
+ type: "hybrid";
213
214
  api: {
214
215
  endpoints: {
215
216
  method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
@@ -221,7 +222,6 @@ export declare const CapmanConfigSchema: z.ZodObject<{
221
222
  destination: string;
222
223
  hint?: string | undefined;
223
224
  };
224
- type: "hybrid";
225
225
  };
226
226
  privacy: {
227
227
  level: "public" | "user_owned" | "admin";
@@ -252,6 +252,7 @@ export declare const CapmanConfigSchema: z.ZodObject<{
252
252
  destination: string;
253
253
  hint?: string | undefined;
254
254
  } | {
255
+ type: "hybrid";
255
256
  api: {
256
257
  endpoints: {
257
258
  method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
@@ -263,7 +264,6 @@ export declare const CapmanConfigSchema: z.ZodObject<{
263
264
  destination: string;
264
265
  hint?: string | undefined;
265
266
  };
266
- type: "hybrid";
267
267
  };
268
268
  privacy: {
269
269
  level: "public" | "user_owned" | "admin";
@@ -294,6 +294,7 @@ export declare const CapmanConfigSchema: z.ZodObject<{
294
294
  destination: string;
295
295
  hint?: string | undefined;
296
296
  } | {
297
+ type: "hybrid";
297
298
  api: {
298
299
  endpoints: {
299
300
  method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
@@ -305,7 +306,6 @@ export declare const CapmanConfigSchema: z.ZodObject<{
305
306
  destination: string;
306
307
  hint?: string | undefined;
307
308
  };
308
- type: "hybrid";
309
309
  };
310
310
  privacy: {
311
311
  level: "public" | "user_owned" | "admin";
@@ -339,6 +339,7 @@ export declare const CapmanConfigSchema: z.ZodObject<{
339
339
  destination: string;
340
340
  hint?: string | undefined;
341
341
  } | {
342
+ type: "hybrid";
342
343
  api: {
343
344
  endpoints: {
344
345
  method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
@@ -350,7 +351,6 @@ export declare const CapmanConfigSchema: z.ZodObject<{
350
351
  destination: string;
351
352
  hint?: string | undefined;
352
353
  };
353
- type: "hybrid";
354
354
  };
355
355
  privacy: {
356
356
  level: "public" | "user_owned" | "admin";
@@ -385,6 +385,7 @@ export declare const CapmanConfigSchema: z.ZodObject<{
385
385
  destination: string;
386
386
  hint?: string | undefined;
387
387
  } | {
388
+ type: "hybrid";
388
389
  api: {
389
390
  endpoints: {
390
391
  method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
@@ -396,7 +397,98 @@ export declare const CapmanConfigSchema: z.ZodObject<{
396
397
  destination: string;
397
398
  hint?: string | undefined;
398
399
  };
400
+ };
401
+ privacy: {
402
+ level: "public" | "user_owned" | "admin";
403
+ note?: string | undefined;
404
+ };
405
+ examples?: string[] | undefined;
406
+ }[];
407
+ baseUrl?: string | undefined;
408
+ }>, {
409
+ app: string;
410
+ capabilities: {
411
+ name: string;
412
+ id: string;
413
+ params: {
414
+ name: string;
415
+ required: boolean;
416
+ description: string;
417
+ source: "user_query" | "session" | "context" | "static";
418
+ default?: string | number | boolean | undefined;
419
+ }[];
420
+ description: string;
421
+ returns: string[];
422
+ resolver: {
423
+ type: "api";
424
+ endpoints: {
425
+ method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
426
+ path: string;
427
+ params?: string[] | undefined;
428
+ }[];
429
+ } | {
430
+ type: "nav";
431
+ destination: string;
432
+ hint?: string | undefined;
433
+ } | {
399
434
  type: "hybrid";
435
+ api: {
436
+ endpoints: {
437
+ method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
438
+ path: string;
439
+ params?: string[] | undefined;
440
+ }[];
441
+ };
442
+ nav: {
443
+ destination: string;
444
+ hint?: string | undefined;
445
+ };
446
+ };
447
+ privacy: {
448
+ level: "public" | "user_owned" | "admin";
449
+ note?: string | undefined;
450
+ };
451
+ examples?: string[] | undefined;
452
+ }[];
453
+ baseUrl?: string | undefined;
454
+ }, {
455
+ app: string;
456
+ capabilities: {
457
+ name: string;
458
+ id: string;
459
+ params: {
460
+ name: string;
461
+ required: boolean;
462
+ description: string;
463
+ source: "user_query" | "session" | "context" | "static";
464
+ default?: string | number | boolean | undefined;
465
+ }[];
466
+ description: string;
467
+ returns: string[];
468
+ resolver: {
469
+ type: "api";
470
+ endpoints: {
471
+ method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
472
+ path: string;
473
+ params?: string[] | undefined;
474
+ }[];
475
+ } | {
476
+ type: "nav";
477
+ destination: string;
478
+ hint?: string | undefined;
479
+ } | {
480
+ type: "hybrid";
481
+ api: {
482
+ endpoints: {
483
+ method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
484
+ path: string;
485
+ params?: string[] | undefined;
486
+ }[];
487
+ };
488
+ nav: {
489
+ destination: string;
490
+ hint?: string | undefined;
491
+ };
400
492
  };
401
493
  privacy: {
402
494
  level: "public" | "user_owned" | "admin";
@@ -516,6 +608,7 @@ export declare const ManifestSchema: z.ZodObject<{
516
608
  hint?: string | undefined;
517
609
  }>;
518
610
  }, "strip", z.ZodTypeAny, {
611
+ type: "hybrid";
519
612
  api: {
520
613
  endpoints: {
521
614
  method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
@@ -527,8 +620,8 @@ export declare const ManifestSchema: z.ZodObject<{
527
620
  destination: string;
528
621
  hint?: string | undefined;
529
622
  };
530
- type: "hybrid";
531
623
  }, {
624
+ type: "hybrid";
532
625
  api: {
533
626
  endpoints: {
534
627
  method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
@@ -540,7 +633,6 @@ export declare const ManifestSchema: z.ZodObject<{
540
633
  destination: string;
541
634
  hint?: string | undefined;
542
635
  };
543
- type: "hybrid";
544
636
  }>]>;
545
637
  privacy: z.ZodObject<{
546
638
  level: z.ZodEnum<["public", "user_owned", "admin"]>;
@@ -576,6 +668,7 @@ export declare const ManifestSchema: z.ZodObject<{
576
668
  destination: string;
577
669
  hint?: string | undefined;
578
670
  } | {
671
+ type: "hybrid";
579
672
  api: {
580
673
  endpoints: {
581
674
  method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
@@ -587,7 +680,6 @@ export declare const ManifestSchema: z.ZodObject<{
587
680
  destination: string;
588
681
  hint?: string | undefined;
589
682
  };
590
- type: "hybrid";
591
683
  };
592
684
  privacy: {
593
685
  level: "public" | "user_owned" | "admin";
@@ -618,6 +710,7 @@ export declare const ManifestSchema: z.ZodObject<{
618
710
  destination: string;
619
711
  hint?: string | undefined;
620
712
  } | {
713
+ type: "hybrid";
621
714
  api: {
622
715
  endpoints: {
623
716
  method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
@@ -629,7 +722,6 @@ export declare const ManifestSchema: z.ZodObject<{
629
722
  destination: string;
630
723
  hint?: string | undefined;
631
724
  };
632
- type: "hybrid";
633
725
  };
634
726
  privacy: {
635
727
  level: "public" | "user_owned" | "admin";
@@ -664,6 +756,7 @@ export declare const ManifestSchema: z.ZodObject<{
664
756
  destination: string;
665
757
  hint?: string | undefined;
666
758
  } | {
759
+ type: "hybrid";
667
760
  api: {
668
761
  endpoints: {
669
762
  method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
@@ -675,7 +768,6 @@ export declare const ManifestSchema: z.ZodObject<{
675
768
  destination: string;
676
769
  hint?: string | undefined;
677
770
  };
678
- type: "hybrid";
679
771
  };
680
772
  privacy: {
681
773
  level: "public" | "user_owned" | "admin";
@@ -711,6 +803,7 @@ export declare const ManifestSchema: z.ZodObject<{
711
803
  destination: string;
712
804
  hint?: string | undefined;
713
805
  } | {
806
+ type: "hybrid";
714
807
  api: {
715
808
  endpoints: {
716
809
  method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
@@ -722,7 +815,6 @@ export declare const ManifestSchema: z.ZodObject<{
722
815
  destination: string;
723
816
  hint?: string | undefined;
724
817
  };
725
- type: "hybrid";
726
818
  };
727
819
  privacy: {
728
820
  level: "public" | "user_owned" | "admin";
@@ -50,8 +50,10 @@ const CapabilitySchema = z.object({
50
50
  id: z.string().min(1, 'capability id is required')
51
51
  .regex(/^[a-z0-9_]+$/, 'id must be snake_case (lowercase, numbers, underscores only)'),
52
52
  name: z.string().min(1, 'capability name is required'),
53
- description: z.string().min(10, 'description must be at least 10 characters for accurate matching'),
54
- examples: z.array(z.string()).optional(),
53
+ description: z.string()
54
+ .min(10, 'description must be at least 10 characters for accurate matching')
55
+ .max(500, 'description must be 500 characters or fewer'),
56
+ examples: z.array(z.string().max(200, 'each example must be 200 characters or fewer')).optional(),
55
57
  params: z.array(CapabilityParamSchema),
56
58
  returns: z.array(z.string()),
57
59
  resolver: ResolverSchema,
@@ -60,11 +62,14 @@ const CapabilitySchema = z.object({
60
62
  // ─── Config Schema ────────────────────────────────────────────────────────────
61
63
  export const CapmanConfigSchema = z.object({
62
64
  app: z.string().min(1, 'app name is required'),
63
- baseUrl: z.string().url('baseUrl must be a valid URL').optional(),
65
+ baseUrl: z.string().url().optional(),
64
66
  capabilities: z.array(CapabilitySchema)
65
67
  .min(1, 'at least one capability is required')
66
68
  .refine(caps => new Set(caps.map(c => c.id)).size === caps.length, 'capability ids must be unique'),
67
- });
69
+ }).refine(cfg => {
70
+ const needsBaseUrl = cfg.capabilities.some(c => c.resolver.type === 'api' || c.resolver.type === 'hybrid');
71
+ return !needsBaseUrl || !!cfg.baseUrl;
72
+ }, { message: 'baseUrl is required when any capability uses an api or hybrid resolver' });
68
73
  // ─── Manifest Schema ──────────────────────────────────────────────────────────
69
74
  export const ManifestSchema = z.object({
70
75
  version: z.string(),
@@ -134,3 +134,4 @@ export interface ExplainResult {
134
134
  resolvedVia: 'keyword' | 'llm';
135
135
  durationMs: number;
136
136
  }
137
+ export type MatchMode = 'cheap' | 'balanced' | 'accurate';
@@ -1 +1 @@
1
- export declare const VERSION = "0.5.1";
1
+ export declare const VERSION = "0.5.3";
@@ -1,2 +1,2 @@
1
1
  // Auto-generated by scripts/version.js — do not edit manually
2
- export const VERSION = '0.5.1';
2
+ export const VERSION = '0.5.3';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capman",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
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",