capman 0.5.0 → 0.5.2
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 +55 -0
- package/bin/lib/cmd-demo.js +2 -2
- package/dist/cjs/cache.d.ts +6 -4
- package/dist/cjs/cache.d.ts.map +1 -1
- package/dist/cjs/cache.js +44 -13
- package/dist/cjs/cache.js.map +1 -1
- package/dist/cjs/engine.d.ts +23 -1
- package/dist/cjs/engine.d.ts.map +1 -1
- package/dist/cjs/engine.js +168 -165
- package/dist/cjs/engine.js.map +1 -1
- package/dist/cjs/index.d.ts +3 -2
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +3 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/learning.d.ts +2 -1
- package/dist/cjs/learning.d.ts.map +1 -1
- package/dist/cjs/learning.js +104 -20
- package/dist/cjs/learning.js.map +1 -1
- package/dist/cjs/matcher.d.ts +15 -1
- package/dist/cjs/matcher.d.ts.map +1 -1
- package/dist/cjs/matcher.js +45 -14
- package/dist/cjs/matcher.js.map +1 -1
- package/dist/cjs/parser.js +16 -2
- package/dist/cjs/parser.js.map +1 -1
- package/dist/cjs/resolver.d.ts.map +1 -1
- package/dist/cjs/resolver.js +32 -6
- package/dist/cjs/resolver.js.map +1 -1
- package/dist/cjs/schema.d.ts +14 -14
- package/dist/cjs/schema.d.ts.map +1 -1
- package/dist/cjs/schema.js +4 -2
- package/dist/cjs/schema.js.map +1 -1
- package/dist/cjs/types.d.ts +1 -0
- package/dist/cjs/types.d.ts.map +1 -1
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/cache.d.ts +6 -4
- package/dist/esm/cache.js +44 -13
- package/dist/esm/engine.d.ts +23 -1
- package/dist/esm/engine.js +169 -166
- package/dist/esm/index.d.ts +3 -2
- package/dist/esm/index.js +1 -0
- package/dist/esm/learning.d.ts +2 -1
- package/dist/esm/learning.js +104 -20
- package/dist/esm/matcher.d.ts +15 -1
- package/dist/esm/matcher.js +43 -13
- package/dist/esm/parser.js +16 -2
- package/dist/esm/resolver.js +32 -6
- package/dist/esm/schema.d.ts +14 -14
- package/dist/esm/schema.js +4 -2
- package/dist/esm/types.d.ts +1 -0
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +1 -1
package/dist/esm/learning.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { logger } from './logger';
|
|
4
|
-
const MAX_LEARNING_ENTRIES =
|
|
4
|
+
const MAX_LEARNING_ENTRIES = 10_000;
|
|
5
5
|
import { STOPWORDS } from './matcher';
|
|
6
6
|
// ─── Shared computation helpers ───────────────────────────────────────────────
|
|
7
7
|
function computeStats(entries) {
|
|
@@ -55,11 +55,17 @@ export class FileLearningStore {
|
|
|
55
55
|
this.statsCounter = {
|
|
56
56
|
totalQueries: 0, llmQueries: 0, cacheHits: 0, outOfScope: 0,
|
|
57
57
|
};
|
|
58
|
-
|
|
58
|
+
const cwd = process.cwd();
|
|
59
|
+
const resolved = path.resolve(cwd, filePath);
|
|
60
|
+
const allowedPrefix = cwd === '/' ? '/' : cwd + path.sep;
|
|
61
|
+
if (!resolved.startsWith(allowedPrefix)) {
|
|
62
|
+
throw new Error(`FileCache path "${filePath}" resolves outside the working directory.\n` +
|
|
63
|
+
`Resolved: ${resolved}\nAllowed: ${cwd}`);
|
|
64
|
+
}
|
|
65
|
+
this.filePath = resolved;
|
|
59
66
|
logger.info(`FileLearningStore initialized — writing to: ${this.filePath}`);
|
|
60
67
|
}
|
|
61
68
|
updateIndex(entry) {
|
|
62
|
-
var _a;
|
|
63
69
|
this.statsCounter.totalQueries++;
|
|
64
70
|
if (entry.resolvedVia === 'llm')
|
|
65
71
|
this.statsCounter.llmQueries++;
|
|
@@ -72,12 +78,43 @@ export class FileLearningStore {
|
|
|
72
78
|
.split(/\W+/)
|
|
73
79
|
.filter(w => w.length > 2 && !STOPWORDS.has(w));
|
|
74
80
|
for (const word of words) {
|
|
75
|
-
|
|
81
|
+
this.index[word] ??= {};
|
|
76
82
|
this.index[word][entry.capabilityId] =
|
|
77
83
|
(this.index[word][entry.capabilityId] ?? 0) + 1;
|
|
78
84
|
}
|
|
79
85
|
}
|
|
80
86
|
}
|
|
87
|
+
subtractFromIndex(entry) {
|
|
88
|
+
if (!entry.capabilityId) {
|
|
89
|
+
this.statsCounter.outOfScope = Math.max(0, this.statsCounter.outOfScope - 1);
|
|
90
|
+
this.statsCounter.totalQueries = Math.max(0, this.statsCounter.totalQueries - 1);
|
|
91
|
+
if (entry.resolvedVia === 'llm')
|
|
92
|
+
this.statsCounter.llmQueries = Math.max(0, this.statsCounter.llmQueries - 1);
|
|
93
|
+
if (entry.resolvedVia === 'cache')
|
|
94
|
+
this.statsCounter.cacheHits = Math.max(0, this.statsCounter.cacheHits - 1);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
this.statsCounter.totalQueries = Math.max(0, this.statsCounter.totalQueries - 1);
|
|
98
|
+
if (entry.resolvedVia === 'llm')
|
|
99
|
+
this.statsCounter.llmQueries = Math.max(0, this.statsCounter.llmQueries - 1);
|
|
100
|
+
if (entry.resolvedVia === 'cache')
|
|
101
|
+
this.statsCounter.cacheHits = Math.max(0, this.statsCounter.cacheHits - 1);
|
|
102
|
+
const words = entry.query.toLowerCase()
|
|
103
|
+
.split(/\W+/)
|
|
104
|
+
.filter(w => w.length > 2 && !STOPWORDS.has(w));
|
|
105
|
+
for (const word of words) {
|
|
106
|
+
if (!this.index[word])
|
|
107
|
+
continue;
|
|
108
|
+
this.index[word][entry.capabilityId] =
|
|
109
|
+
(this.index[word][entry.capabilityId] ?? 1) - 1;
|
|
110
|
+
if (this.index[word][entry.capabilityId] <= 0) {
|
|
111
|
+
delete this.index[word][entry.capabilityId];
|
|
112
|
+
}
|
|
113
|
+
if (Object.keys(this.index[word]).length === 0) {
|
|
114
|
+
delete this.index[word];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
81
118
|
rebuildIndex() {
|
|
82
119
|
this.index = {};
|
|
83
120
|
this.statsCounter = { totalQueries: 0, llmQueries: 0, cacheHits: 0, outOfScope: 0 };
|
|
@@ -124,13 +161,26 @@ export class FileLearningStore {
|
|
|
124
161
|
}
|
|
125
162
|
async record(entry) {
|
|
126
163
|
await this.load();
|
|
127
|
-
|
|
128
|
-
|
|
164
|
+
// Store only tokenized keywords — never raw query text.
|
|
165
|
+
// Raw queries may contain PII (emails, names, order IDs) that should
|
|
166
|
+
// not be persisted to disk under GDPR/CCPA data retention requirements.
|
|
167
|
+
const sanitized = {
|
|
168
|
+
...entry,
|
|
169
|
+
query: entry.query
|
|
170
|
+
.toLowerCase()
|
|
171
|
+
.split(/\W+/)
|
|
172
|
+
.filter(w => w.length > 2 && !STOPWORDS.has(w))
|
|
173
|
+
.join(' '),
|
|
174
|
+
};
|
|
175
|
+
this.entries.push(sanitized);
|
|
176
|
+
this.updateIndex(sanitized);
|
|
129
177
|
if (this.entries.length > MAX_LEARNING_ENTRIES) {
|
|
130
178
|
const excess = this.entries.length - MAX_LEARNING_ENTRIES;
|
|
131
|
-
this.entries.splice(0, excess);
|
|
132
|
-
//
|
|
133
|
-
|
|
179
|
+
const pruned = this.entries.splice(0, excess);
|
|
180
|
+
// Subtract pruned entries from index — O(pruned × w) instead of O(n × w) full rebuild
|
|
181
|
+
for (const entry of pruned) {
|
|
182
|
+
this.subtractFromIndex(entry);
|
|
183
|
+
}
|
|
134
184
|
logger.debug(`Learning store pruned ${excess} oldest entries (cap: ${MAX_LEARNING_ENTRIES})`);
|
|
135
185
|
}
|
|
136
186
|
await this.save();
|
|
@@ -164,11 +214,22 @@ export class MemoryLearningStore {
|
|
|
164
214
|
};
|
|
165
215
|
}
|
|
166
216
|
async record(entry) {
|
|
167
|
-
|
|
168
|
-
|
|
217
|
+
const sanitized = {
|
|
218
|
+
...entry,
|
|
219
|
+
query: entry.query
|
|
220
|
+
.toLowerCase()
|
|
221
|
+
.split(/\W+/)
|
|
222
|
+
.filter(w => w.length > 2 && !STOPWORDS.has(w))
|
|
223
|
+
.join(' '),
|
|
224
|
+
};
|
|
225
|
+
this.entries.push(sanitized);
|
|
226
|
+
this.updateIndex(sanitized);
|
|
169
227
|
if (this.entries.length > MAX_LEARNING_ENTRIES) {
|
|
170
|
-
|
|
171
|
-
this.
|
|
228
|
+
const excess = this.entries.length - MAX_LEARNING_ENTRIES;
|
|
229
|
+
const pruned = this.entries.splice(0, excess);
|
|
230
|
+
for (const entry of pruned) {
|
|
231
|
+
this.subtractFromIndex(entry);
|
|
232
|
+
}
|
|
172
233
|
}
|
|
173
234
|
}
|
|
174
235
|
async getStats() {
|
|
@@ -178,7 +239,6 @@ export class MemoryLearningStore {
|
|
|
178
239
|
return structuredClone(this.index);
|
|
179
240
|
}
|
|
180
241
|
updateIndex(entry) {
|
|
181
|
-
var _a;
|
|
182
242
|
this.statsCounter.totalQueries++;
|
|
183
243
|
if (entry.resolvedVia === 'llm')
|
|
184
244
|
this.statsCounter.llmQueries++;
|
|
@@ -191,17 +251,41 @@ export class MemoryLearningStore {
|
|
|
191
251
|
.split(/\W+/)
|
|
192
252
|
.filter(w => w.length > 2 && !STOPWORDS.has(w));
|
|
193
253
|
for (const word of words) {
|
|
194
|
-
|
|
254
|
+
this.index[word] ??= {};
|
|
195
255
|
this.index[word][entry.capabilityId] =
|
|
196
256
|
(this.index[word][entry.capabilityId] ?? 0) + 1;
|
|
197
257
|
}
|
|
198
258
|
}
|
|
199
259
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
260
|
+
subtractFromIndex(entry) {
|
|
261
|
+
if (!entry.capabilityId) {
|
|
262
|
+
this.statsCounter.outOfScope = Math.max(0, this.statsCounter.outOfScope - 1);
|
|
263
|
+
this.statsCounter.totalQueries = Math.max(0, this.statsCounter.totalQueries - 1);
|
|
264
|
+
if (entry.resolvedVia === 'llm')
|
|
265
|
+
this.statsCounter.llmQueries = Math.max(0, this.statsCounter.llmQueries - 1);
|
|
266
|
+
if (entry.resolvedVia === 'cache')
|
|
267
|
+
this.statsCounter.cacheHits = Math.max(0, this.statsCounter.cacheHits - 1);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
this.statsCounter.totalQueries = Math.max(0, this.statsCounter.totalQueries - 1);
|
|
271
|
+
if (entry.resolvedVia === 'llm')
|
|
272
|
+
this.statsCounter.llmQueries = Math.max(0, this.statsCounter.llmQueries - 1);
|
|
273
|
+
if (entry.resolvedVia === 'cache')
|
|
274
|
+
this.statsCounter.cacheHits = Math.max(0, this.statsCounter.cacheHits - 1);
|
|
275
|
+
const words = entry.query.toLowerCase()
|
|
276
|
+
.split(/\W+/)
|
|
277
|
+
.filter(w => w.length > 2 && !STOPWORDS.has(w));
|
|
278
|
+
for (const word of words) {
|
|
279
|
+
if (!this.index[word])
|
|
280
|
+
continue;
|
|
281
|
+
this.index[word][entry.capabilityId] =
|
|
282
|
+
(this.index[word][entry.capabilityId] ?? 1) - 1;
|
|
283
|
+
if (this.index[word][entry.capabilityId] <= 0) {
|
|
284
|
+
delete this.index[word][entry.capabilityId];
|
|
285
|
+
}
|
|
286
|
+
if (Object.keys(this.index[word]).length === 0) {
|
|
287
|
+
delete this.index[word];
|
|
288
|
+
}
|
|
205
289
|
}
|
|
206
290
|
}
|
|
207
291
|
async getTopCapabilities(limit = 5) {
|
package/dist/esm/matcher.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Manifest, Capability, MatchResult } from './types';
|
|
2
|
+
export declare class LLMParseError extends Error {
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
2
5
|
export declare const STOPWORDS: Set<string>;
|
|
3
6
|
export declare function resolverToIntent(cap: Capability): MatchResult['intent'];
|
|
4
7
|
/**
|
|
@@ -16,4 +19,15 @@ export declare function match(query: string, manifest: Manifest): MatchResult;
|
|
|
16
19
|
export interface LLMMatcherOptions {
|
|
17
20
|
llm: (prompt: string) => Promise<string>;
|
|
18
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Matches a query to a capability using an LLM.
|
|
24
|
+
*
|
|
25
|
+
* ⚠️ SECURITY NOTE: Capability `description` and `examples` fields from the
|
|
26
|
+
* manifest are injected verbatim into the LLM prompt (system portion).
|
|
27
|
+
* In a solo deployment with a developer-controlled manifest this is safe.
|
|
28
|
+
* If your manifest is generated from third-party OpenAPI specs, user-controlled
|
|
29
|
+
* sources, or any external input, sanitize `description` and `examples` fields
|
|
30
|
+
* before passing the manifest to this function — adversarial content in those
|
|
31
|
+
* fields can influence LLM routing decisions.
|
|
32
|
+
*/
|
|
19
33
|
export declare function matchWithLLM(query: string, manifest: Manifest, options: LLMMatcherOptions): Promise<MatchResult>;
|
package/dist/esm/matcher.js
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { logger } from './logger';
|
|
2
|
+
// ─── Typed error for LLM parse failures ──────────────────────────────────────
|
|
3
|
+
export class LLMParseError extends Error {
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = 'LLMParseError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
2
9
|
export const STOPWORDS = new Set([
|
|
3
10
|
'show', 'me', 'the', 'get', 'find', 'fetch', 'give', 'please',
|
|
4
11
|
'can', 'you', 'i', 'want', 'to', 'a', 'an', 'my', 'our', 'your',
|
|
@@ -65,7 +72,7 @@ export function extractParams(query, cap) {
|
|
|
65
72
|
for (const param of cap.params) {
|
|
66
73
|
// Session params come from auth context, not query
|
|
67
74
|
if (param.source === 'session') {
|
|
68
|
-
result[param.name] =
|
|
75
|
+
result[param.name] = null; // injected by resolver from auth context — not extracted from query
|
|
69
76
|
continue;
|
|
70
77
|
}
|
|
71
78
|
if (param.source !== 'user_query') {
|
|
@@ -117,10 +124,11 @@ export function extractParams(query, cap) {
|
|
|
117
124
|
const words = query.trim().split(/\s+/);
|
|
118
125
|
const meaningful = words.filter(w => !STOPWORDS.has(w.toLowerCase()));
|
|
119
126
|
const candidate = meaningful[meaningful.length - 1] ?? null;
|
|
120
|
-
// Only use fallback if candidate looks like an identifier — not a generic noun
|
|
127
|
+
// Only use fallback if candidate looks like an identifier — not a generic noun, verb,
|
|
128
|
+
// or category word that would produce junk URLs like /orders/orders or /users/data
|
|
121
129
|
if (candidate &&
|
|
122
130
|
/^[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)) {
|
|
131
|
+
!/^(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
132
|
extracted = candidate;
|
|
125
133
|
}
|
|
126
134
|
}
|
|
@@ -184,8 +192,25 @@ export function match(query, manifest) {
|
|
|
184
192
|
candidates,
|
|
185
193
|
};
|
|
186
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* Matches a query to a capability using an LLM.
|
|
197
|
+
*
|
|
198
|
+
* ⚠️ SECURITY NOTE: Capability `description` and `examples` fields from the
|
|
199
|
+
* manifest are injected verbatim into the LLM prompt (system portion).
|
|
200
|
+
* In a solo deployment with a developer-controlled manifest this is safe.
|
|
201
|
+
* If your manifest is generated from third-party OpenAPI specs, user-controlled
|
|
202
|
+
* sources, or any external input, sanitize `description` and `examples` fields
|
|
203
|
+
* before passing the manifest to this function — adversarial content in those
|
|
204
|
+
* fields can influence LLM routing decisions.
|
|
205
|
+
*/
|
|
187
206
|
export async function matchWithLLM(query, manifest, options) {
|
|
188
|
-
|
|
207
|
+
// Truncate description and examples — prevents context window overflow and
|
|
208
|
+
// reduces prompt injection surface from third-party OpenAPI spec content.
|
|
209
|
+
const MAX_DESC_LEN = 200;
|
|
210
|
+
const MAX_EXAMPLE_LEN = 100;
|
|
211
|
+
const manifestSummary = manifest.capabilities.map(c => `- ${c.id} (${c.resolver.type}): ${c.description.slice(0, MAX_DESC_LEN)}${c.description.length > MAX_DESC_LEN ? '…' : ''}${c.examples?.length
|
|
212
|
+
? `\n examples: ${c.examples.slice(0, 2).map(e => e.slice(0, MAX_EXAMPLE_LEN)).join(', ')}`
|
|
213
|
+
: ''}`).join('\n');
|
|
189
214
|
const prompt = `You are an intent matcher for an AI agent system.
|
|
190
215
|
|
|
191
216
|
App: ${manifest.app}
|
|
@@ -216,13 +241,13 @@ ${JSON.stringify({ user_query: query })}
|
|
|
216
241
|
parsed = JSON.parse(clean);
|
|
217
242
|
}
|
|
218
243
|
catch {
|
|
219
|
-
throw new
|
|
244
|
+
throw new LLMParseError(`LLM returned invalid JSON. First 300 chars: ${clean.slice(0, 300)}`);
|
|
220
245
|
}
|
|
221
246
|
if (typeof parsed.matched_capability !== 'string') {
|
|
222
|
-
throw new
|
|
247
|
+
throw new LLMParseError(`missing "matched_capability" field in response`);
|
|
223
248
|
}
|
|
224
249
|
if (typeof parsed.confidence !== 'number') {
|
|
225
|
-
throw new
|
|
250
|
+
throw new LLMParseError(`missing numeric "confidence" field in response`);
|
|
226
251
|
}
|
|
227
252
|
const isOOS = parsed.matched_capability === 'OUT_OF_SCOPE';
|
|
228
253
|
const capability = isOOS
|
|
@@ -233,16 +258,21 @@ ${JSON.stringify({ user_query: query })}
|
|
|
233
258
|
if (!isOOS && capability === null) {
|
|
234
259
|
logger.warn(`LLM returned unknown capability ID: "${parsed.matched_capability}" — treating as out_of_scope`);
|
|
235
260
|
}
|
|
261
|
+
// Build full candidate list — all capabilities scored, LLM winner marked as matched.
|
|
262
|
+
// This aligns the shape with keyword match results and allows the learning boost
|
|
263
|
+
// to surface alternatives if the LLM made a wrong call.
|
|
264
|
+
const llmConfidence = effectivelyOOS ? 0 : parsed.confidence;
|
|
265
|
+
const allCandidates = manifest.capabilities.map(c => ({
|
|
266
|
+
capabilityId: c.id,
|
|
267
|
+
score: c.id === capability?.id ? llmConfidence : 0,
|
|
268
|
+
matched: c.id === capability?.id,
|
|
269
|
+
}));
|
|
236
270
|
return {
|
|
237
271
|
capability,
|
|
238
|
-
confidence:
|
|
272
|
+
confidence: llmConfidence,
|
|
239
273
|
intent: effectivelyOOS ? 'out_of_scope' : parsed.intent,
|
|
240
274
|
extractedParams: (parsed.extracted_params ?? {}),
|
|
241
275
|
reasoning: parsed.reasoning ?? 'No reasoning provided',
|
|
242
|
-
candidates:
|
|
243
|
-
capabilityId: capability.id,
|
|
244
|
-
score: parsed.confidence,
|
|
245
|
-
matched: true,
|
|
246
|
-
}] : [],
|
|
276
|
+
candidates: allCandidates,
|
|
247
277
|
};
|
|
248
278
|
}
|
package/dist/esm/parser.js
CHANGED
|
@@ -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
|
|
13
|
+
const controller = new AbortController();
|
|
14
|
+
const timer = setTimeout(() => controller.abort(), 10_000);
|
|
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();
|
|
@@ -22,7 +36,7 @@ async function loadSpec(source) {
|
|
|
22
36
|
throw new Error(`Spec file not found: ${resolved}`);
|
|
23
37
|
}
|
|
24
38
|
logger.info(`Reading OpenAPI spec from: ${resolved}`);
|
|
25
|
-
const text = fs.
|
|
39
|
+
const text = await fs.promises.readFile(resolved, 'utf-8');
|
|
26
40
|
return parseSpecText(text, source);
|
|
27
41
|
}
|
|
28
42
|
function parseSpecText(text, source) {
|
package/dist/esm/resolver.js
CHANGED
|
@@ -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
|
|
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
|
}
|
|
@@ -89,6 +95,20 @@ export async function resolve(matchResult, params = {}, options = {}) {
|
|
|
89
95
|
};
|
|
90
96
|
}
|
|
91
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Resolves an API capability by executing all configured endpoints.
|
|
100
|
+
*
|
|
101
|
+
* ⚠️ PARALLEL EXECUTION: All endpoints are fired simultaneously via Promise.all().
|
|
102
|
+
* If any endpoint fails, the entire result is marked as failed and partial results
|
|
103
|
+
* are discarded — but side effects from successful endpoints cannot be rolled back.
|
|
104
|
+
*
|
|
105
|
+
* Example: a capability with two endpoints [POST /reserve, POST /confirm] will
|
|
106
|
+
* fire both in parallel. If /confirm fails after /reserve succeeded, the reservation
|
|
107
|
+
* exists but the caller receives success: false with no indication that /reserve ran.
|
|
108
|
+
*
|
|
109
|
+
* For capabilities where ordering or rollback matters, define separate capabilities
|
|
110
|
+
* with single endpoints and orchestrate them at the application layer.
|
|
111
|
+
*/
|
|
92
112
|
async function resolveApi(resolver, params, options) {
|
|
93
113
|
const startTime = Date.now();
|
|
94
114
|
const retries = options.retries ?? 0;
|
|
@@ -96,7 +116,7 @@ async function resolveApi(resolver, params, options) {
|
|
|
96
116
|
const apiCalls = resolver.endpoints.map(endpoint => ({
|
|
97
117
|
method: endpoint.method,
|
|
98
118
|
url: buildUrl(options.baseUrl ?? '', endpoint.path, params),
|
|
99
|
-
params,
|
|
119
|
+
params: Object.fromEntries(Object.entries(params).filter(([, v]) => v !== null && v !== undefined)),
|
|
100
120
|
}));
|
|
101
121
|
if (options.dryRun) {
|
|
102
122
|
return { success: true, resolverType: 'api', apiCalls, durationMs: Date.now() - startTime };
|
|
@@ -121,7 +141,7 @@ async function resolveApi(resolver, params, options) {
|
|
|
121
141
|
headers: options.headers ?? {},
|
|
122
142
|
signal: controller.signal,
|
|
123
143
|
body: ['POST', 'PUT', 'PATCH'].includes(call.method)
|
|
124
|
-
? JSON.stringify(call.params)
|
|
144
|
+
? JSON.stringify(Object.fromEntries(Object.entries(call.params).filter(([, v]) => v !== null && v !== undefined)))
|
|
125
145
|
: undefined,
|
|
126
146
|
});
|
|
127
147
|
clearTimeout(timer);
|
|
@@ -185,10 +205,16 @@ function resolveNav(resolver, params) {
|
|
|
185
205
|
continue;
|
|
186
206
|
const str = String(value);
|
|
187
207
|
validateNavParam(key, str);
|
|
188
|
-
destination = destination.
|
|
208
|
+
destination = destination.replaceAll(`{${key}}`, encodeURIComponent(str));
|
|
189
209
|
}
|
|
190
210
|
return { success: true, resolverType: 'nav', navTarget: destination };
|
|
191
211
|
}
|
|
212
|
+
// Note: buildUrl does not validate param values against an allowlist.
|
|
213
|
+
// resolveNav() does validate via validateNavParam() because nav destinations
|
|
214
|
+
// are used as deep links where path traversal is a real risk.
|
|
215
|
+
// For API URLs, extractParams() strips most dangerous characters upstream,
|
|
216
|
+
// so the practical risk is low — but any future caller bypassing extractParams
|
|
217
|
+
// should add validation here too.
|
|
192
218
|
function buildUrl(baseUrl, urlPath, params) {
|
|
193
219
|
let resolved = urlPath;
|
|
194
220
|
const unused = {};
|
|
@@ -196,7 +222,7 @@ function buildUrl(baseUrl, urlPath, params) {
|
|
|
196
222
|
if (value === null || value === undefined)
|
|
197
223
|
continue; // never write null into URLs
|
|
198
224
|
if (resolved.includes(`{${key}}`)) {
|
|
199
|
-
resolved = resolved.
|
|
225
|
+
resolved = resolved.replaceAll(`{${key}}`, encodeURIComponent(String(value)));
|
|
200
226
|
}
|
|
201
227
|
else {
|
|
202
228
|
unused[key] = value;
|
package/dist/esm/schema.d.ts
CHANGED
|
@@ -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,6 @@ export declare const CapmanConfigSchema: z.ZodObject<{
|
|
|
396
397
|
destination: string;
|
|
397
398
|
hint?: string | undefined;
|
|
398
399
|
};
|
|
399
|
-
type: "hybrid";
|
|
400
400
|
};
|
|
401
401
|
privacy: {
|
|
402
402
|
level: "public" | "user_owned" | "admin";
|
|
@@ -516,6 +516,7 @@ export declare const ManifestSchema: z.ZodObject<{
|
|
|
516
516
|
hint?: string | undefined;
|
|
517
517
|
}>;
|
|
518
518
|
}, "strip", z.ZodTypeAny, {
|
|
519
|
+
type: "hybrid";
|
|
519
520
|
api: {
|
|
520
521
|
endpoints: {
|
|
521
522
|
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
@@ -527,8 +528,8 @@ export declare const ManifestSchema: z.ZodObject<{
|
|
|
527
528
|
destination: string;
|
|
528
529
|
hint?: string | undefined;
|
|
529
530
|
};
|
|
530
|
-
type: "hybrid";
|
|
531
531
|
}, {
|
|
532
|
+
type: "hybrid";
|
|
532
533
|
api: {
|
|
533
534
|
endpoints: {
|
|
534
535
|
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
@@ -540,7 +541,6 @@ export declare const ManifestSchema: z.ZodObject<{
|
|
|
540
541
|
destination: string;
|
|
541
542
|
hint?: string | undefined;
|
|
542
543
|
};
|
|
543
|
-
type: "hybrid";
|
|
544
544
|
}>]>;
|
|
545
545
|
privacy: z.ZodObject<{
|
|
546
546
|
level: z.ZodEnum<["public", "user_owned", "admin"]>;
|
|
@@ -576,6 +576,7 @@ export declare const ManifestSchema: z.ZodObject<{
|
|
|
576
576
|
destination: string;
|
|
577
577
|
hint?: string | undefined;
|
|
578
578
|
} | {
|
|
579
|
+
type: "hybrid";
|
|
579
580
|
api: {
|
|
580
581
|
endpoints: {
|
|
581
582
|
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
@@ -587,7 +588,6 @@ export declare const ManifestSchema: z.ZodObject<{
|
|
|
587
588
|
destination: string;
|
|
588
589
|
hint?: string | undefined;
|
|
589
590
|
};
|
|
590
|
-
type: "hybrid";
|
|
591
591
|
};
|
|
592
592
|
privacy: {
|
|
593
593
|
level: "public" | "user_owned" | "admin";
|
|
@@ -618,6 +618,7 @@ export declare const ManifestSchema: z.ZodObject<{
|
|
|
618
618
|
destination: string;
|
|
619
619
|
hint?: string | undefined;
|
|
620
620
|
} | {
|
|
621
|
+
type: "hybrid";
|
|
621
622
|
api: {
|
|
622
623
|
endpoints: {
|
|
623
624
|
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
@@ -629,7 +630,6 @@ export declare const ManifestSchema: z.ZodObject<{
|
|
|
629
630
|
destination: string;
|
|
630
631
|
hint?: string | undefined;
|
|
631
632
|
};
|
|
632
|
-
type: "hybrid";
|
|
633
633
|
};
|
|
634
634
|
privacy: {
|
|
635
635
|
level: "public" | "user_owned" | "admin";
|
|
@@ -664,6 +664,7 @@ export declare const ManifestSchema: z.ZodObject<{
|
|
|
664
664
|
destination: string;
|
|
665
665
|
hint?: string | undefined;
|
|
666
666
|
} | {
|
|
667
|
+
type: "hybrid";
|
|
667
668
|
api: {
|
|
668
669
|
endpoints: {
|
|
669
670
|
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
@@ -675,7 +676,6 @@ export declare const ManifestSchema: z.ZodObject<{
|
|
|
675
676
|
destination: string;
|
|
676
677
|
hint?: string | undefined;
|
|
677
678
|
};
|
|
678
|
-
type: "hybrid";
|
|
679
679
|
};
|
|
680
680
|
privacy: {
|
|
681
681
|
level: "public" | "user_owned" | "admin";
|
|
@@ -711,6 +711,7 @@ export declare const ManifestSchema: z.ZodObject<{
|
|
|
711
711
|
destination: string;
|
|
712
712
|
hint?: string | undefined;
|
|
713
713
|
} | {
|
|
714
|
+
type: "hybrid";
|
|
714
715
|
api: {
|
|
715
716
|
endpoints: {
|
|
716
717
|
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
@@ -722,7 +723,6 @@ export declare const ManifestSchema: z.ZodObject<{
|
|
|
722
723
|
destination: string;
|
|
723
724
|
hint?: string | undefined;
|
|
724
725
|
};
|
|
725
|
-
type: "hybrid";
|
|
726
726
|
};
|
|
727
727
|
privacy: {
|
|
728
728
|
level: "public" | "user_owned" | "admin";
|
package/dist/esm/schema.js
CHANGED
|
@@ -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()
|
|
54
|
-
|
|
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,
|
package/dist/esm/types.d.ts
CHANGED
package/dist/esm/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.5.
|
|
1
|
+
export declare const VERSION = "0.5.2";
|
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.2';
|