capman 0.5.2 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/CODEBASE.md +15 -9
- package/bin/lib/cmd-explain.js +2 -2
- package/bin/lib/cmd-run.js +2 -2
- package/bin/lib/shared.js +8 -2
- package/dist/cjs/cache.d.ts +2 -1
- package/dist/cjs/cache.d.ts.map +1 -1
- package/dist/cjs/cache.js +11 -6
- package/dist/cjs/cache.js.map +1 -1
- package/dist/cjs/engine.d.ts +30 -0
- package/dist/cjs/engine.d.ts.map +1 -1
- package/dist/cjs/engine.js +69 -25
- package/dist/cjs/engine.js.map +1 -1
- package/dist/cjs/generator.d.ts.map +1 -1
- package/dist/cjs/generator.js +16 -1
- package/dist/cjs/generator.js.map +1 -1
- package/dist/cjs/learning.d.ts +20 -10
- package/dist/cjs/learning.d.ts.map +1 -1
- package/dist/cjs/learning.js +146 -129
- package/dist/cjs/learning.js.map +1 -1
- package/dist/cjs/matcher.d.ts +5 -2
- package/dist/cjs/matcher.d.ts.map +1 -1
- package/dist/cjs/matcher.js +73 -10
- package/dist/cjs/matcher.js.map +1 -1
- package/dist/cjs/parser.js +8 -2
- package/dist/cjs/parser.js.map +1 -1
- package/dist/cjs/resolver.d.ts +7 -0
- package/dist/cjs/resolver.d.ts.map +1 -1
- package/dist/cjs/resolver.js +47 -23
- package/dist/cjs/resolver.js.map +1 -1
- package/dist/cjs/schema.d.ts +93 -1
- package/dist/cjs/schema.d.ts.map +1 -1
- package/dist/cjs/schema.js +5 -2
- package/dist/cjs/schema.js.map +1 -1
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/cache.d.ts +2 -1
- package/dist/esm/cache.js +11 -6
- package/dist/esm/engine.d.ts +30 -0
- package/dist/esm/engine.js +69 -25
- package/dist/esm/generator.js +16 -1
- package/dist/esm/learning.d.ts +20 -10
- package/dist/esm/learning.js +146 -129
- package/dist/esm/matcher.d.ts +5 -2
- package/dist/esm/matcher.js +70 -10
- package/dist/esm/parser.js +8 -2
- package/dist/esm/resolver.d.ts +7 -0
- package/dist/esm/resolver.js +47 -23
- package/dist/esm/schema.d.ts +93 -1
- package/dist/esm/schema.js +5 -2
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +11 -10
package/dist/esm/resolver.js
CHANGED
|
@@ -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'
|
|
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 {
|
|
@@ -109,15 +110,26 @@ export async function resolve(matchResult, params = {}, options = {}) {
|
|
|
109
110
|
* For capabilities where ordering or rollback matters, define separate capabilities
|
|
110
111
|
* with single endpoints and orchestrate them at the application layer.
|
|
111
112
|
*/
|
|
112
|
-
async function resolveApi(resolver, params, options) {
|
|
113
|
+
async function resolveApi(resolver, params, options, sessionParamNames = new Set()) {
|
|
113
114
|
const startTime = Date.now();
|
|
114
115
|
const retries = options.retries ?? 0;
|
|
115
116
|
const timeoutMs = options.timeoutMs ?? 5000;
|
|
116
|
-
const apiCalls = resolver.endpoints.map(endpoint =>
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
+
});
|
|
121
133
|
if (options.dryRun) {
|
|
122
134
|
return { success: true, resolverType: 'api', apiCalls, durationMs: Date.now() - startTime };
|
|
123
135
|
}
|
|
@@ -130,9 +142,14 @@ async function resolveApi(resolver, params, options) {
|
|
|
130
142
|
};
|
|
131
143
|
}
|
|
132
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).
|
|
133
147
|
async function fetchWithRetry(call) {
|
|
148
|
+
const effectiveRetries = (options.retryAllMethods || SAFE_METHODS.has(call.method))
|
|
149
|
+
? retries
|
|
150
|
+
: 0;
|
|
134
151
|
let lastErr;
|
|
135
|
-
for (let attempt = 0; attempt <=
|
|
152
|
+
for (let attempt = 0; attempt <= effectiveRetries; attempt++) {
|
|
136
153
|
const controller = new AbortController();
|
|
137
154
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
138
155
|
try {
|
|
@@ -151,8 +168,8 @@ async function resolveApi(resolver, params, options) {
|
|
|
151
168
|
clearTimeout(timer);
|
|
152
169
|
lastErr = err;
|
|
153
170
|
const isTimeout = err instanceof Error && err.name === 'AbortError';
|
|
154
|
-
if (attempt <
|
|
155
|
-
logger.warn(`Request failed (attempt ${attempt + 1}/${
|
|
171
|
+
if (attempt < effectiveRetries) {
|
|
172
|
+
logger.warn(`Request failed (attempt ${attempt + 1}/${effectiveRetries + 1}) — retrying: ${isTimeout ? 'timeout' : err}`);
|
|
156
173
|
}
|
|
157
174
|
else {
|
|
158
175
|
throw isTimeout ? new Error(`Request timed out after ${timeoutMs}ms`) : err;
|
|
@@ -209,12 +226,17 @@ function resolveNav(resolver, params) {
|
|
|
209
226
|
}
|
|
210
227
|
return { success: true, resolverType: 'nav', navTarget: destination };
|
|
211
228
|
}
|
|
212
|
-
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
|
|
217
|
-
|
|
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.
|
|
218
240
|
function buildUrl(baseUrl, urlPath, params) {
|
|
219
241
|
let resolved = urlPath;
|
|
220
242
|
const unused = {};
|
|
@@ -222,7 +244,9 @@ function buildUrl(baseUrl, urlPath, params) {
|
|
|
222
244
|
if (value === null || value === undefined)
|
|
223
245
|
continue; // never write null into URLs
|
|
224
246
|
if (resolved.includes(`{${key}}`)) {
|
|
225
|
-
|
|
247
|
+
const str = String(value);
|
|
248
|
+
validateApiPathParam(key, str);
|
|
249
|
+
resolved = resolved.replaceAll(`{${key}}`, encodeURIComponent(str));
|
|
226
250
|
}
|
|
227
251
|
else {
|
|
228
252
|
unused[key] = value;
|
package/dist/esm/schema.d.ts
CHANGED
|
@@ -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<{
|
|
@@ -405,6 +405,98 @@ export declare const CapmanConfigSchema: z.ZodObject<{
|
|
|
405
405
|
examples?: string[] | undefined;
|
|
406
406
|
}[];
|
|
407
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
|
+
} | {
|
|
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
|
+
};
|
|
492
|
+
};
|
|
493
|
+
privacy: {
|
|
494
|
+
level: "public" | "user_owned" | "admin";
|
|
495
|
+
note?: string | undefined;
|
|
496
|
+
};
|
|
497
|
+
examples?: string[] | undefined;
|
|
498
|
+
}[];
|
|
499
|
+
baseUrl?: string | undefined;
|
|
408
500
|
}>;
|
|
409
501
|
export declare const ManifestSchema: z.ZodObject<{
|
|
410
502
|
version: z.ZodString;
|
package/dist/esm/schema.js
CHANGED
|
@@ -62,11 +62,14 @@ const CapabilitySchema = z.object({
|
|
|
62
62
|
// ─── Config Schema ────────────────────────────────────────────────────────────
|
|
63
63
|
export const CapmanConfigSchema = z.object({
|
|
64
64
|
app: z.string().min(1, 'app name is required'),
|
|
65
|
-
baseUrl: z.string().url(
|
|
65
|
+
baseUrl: z.string().url().optional(),
|
|
66
66
|
capabilities: z.array(CapabilitySchema)
|
|
67
67
|
.min(1, 'at least one capability is required')
|
|
68
68
|
.refine(caps => new Set(caps.map(c => c.id)).size === caps.length, 'capability ids must be unique'),
|
|
69
|
-
})
|
|
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' });
|
|
70
73
|
// ─── Manifest Schema ──────────────────────────────────────────────────────────
|
|
71
74
|
export const ManifestSchema = z.object({
|
|
72
75
|
version: z.string(),
|
package/dist/esm/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.5.
|
|
1
|
+
export declare const VERSION = "0.5.3";
|
package/dist/esm/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated by scripts/version.js — do not edit manually
|
|
2
|
-
export const VERSION = '0.5.
|
|
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.
|
|
3
|
+
"version": "0.5.4",
|
|
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",
|
|
@@ -29,15 +29,15 @@
|
|
|
29
29
|
"CODEBASE.md"
|
|
30
30
|
],
|
|
31
31
|
"scripts": {
|
|
32
|
-
"prebuild":
|
|
33
|
-
"build:cjs":
|
|
34
|
-
"build:esm":
|
|
35
|
-
"build":
|
|
36
|
-
"dev":
|
|
37
|
-
"example":
|
|
38
|
-
"validate":
|
|
39
|
-
"inspect":
|
|
40
|
-
"test":
|
|
32
|
+
"prebuild": "node scripts/version.js",
|
|
33
|
+
"build:cjs": "tsc --project tsconfig.json",
|
|
34
|
+
"build:esm": "tsc --project tsconfig.esm.json",
|
|
35
|
+
"build": "pnpm run build:cjs && pnpm run build:esm",
|
|
36
|
+
"dev": "tsc --watch",
|
|
37
|
+
"example": "tsx examples/basic.ts",
|
|
38
|
+
"validate": "node bin/capman.js validate",
|
|
39
|
+
"inspect": "node bin/capman.js inspect",
|
|
40
|
+
"test": "vitest run"
|
|
41
41
|
},
|
|
42
42
|
"keywords": [
|
|
43
43
|
"ai",
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"dotenv": "^17.3.1",
|
|
59
|
+
"fuse.js": "^7.3.0",
|
|
59
60
|
"zod": "^3.23.0"
|
|
60
61
|
}
|
|
61
62
|
}
|