capman 0.4.5 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/CODEBASE.md +94 -156
  3. package/README.md +23 -0
  4. package/bin/lib/cmd-generate.js +20 -3
  5. package/dist/cjs/cache.d.ts +6 -4
  6. package/dist/cjs/cache.d.ts.map +1 -1
  7. package/dist/cjs/cache.js +38 -11
  8. package/dist/cjs/cache.js.map +1 -1
  9. package/dist/cjs/engine.d.ts +46 -4
  10. package/dist/cjs/engine.d.ts.map +1 -1
  11. package/dist/cjs/engine.js +157 -211
  12. package/dist/cjs/engine.js.map +1 -1
  13. package/dist/cjs/index.d.ts +1 -1
  14. package/dist/cjs/index.d.ts.map +1 -1
  15. package/dist/cjs/index.js +2 -1
  16. package/dist/cjs/index.js.map +1 -1
  17. package/dist/cjs/learning.d.ts +16 -1
  18. package/dist/cjs/learning.d.ts.map +1 -1
  19. package/dist/cjs/learning.js +161 -10
  20. package/dist/cjs/learning.js.map +1 -1
  21. package/dist/cjs/matcher.d.ts +23 -0
  22. package/dist/cjs/matcher.d.ts.map +1 -1
  23. package/dist/cjs/matcher.js +53 -18
  24. package/dist/cjs/matcher.js.map +1 -1
  25. package/dist/cjs/parser.js +15 -1
  26. package/dist/cjs/parser.js.map +1 -1
  27. package/dist/cjs/resolver.d.ts.map +1 -1
  28. package/dist/cjs/resolver.js +22 -5
  29. package/dist/cjs/resolver.js.map +1 -1
  30. package/dist/cjs/version.d.ts +1 -1
  31. package/dist/cjs/version.js +1 -1
  32. package/dist/esm/cache.d.ts +6 -4
  33. package/dist/esm/cache.js +38 -11
  34. package/dist/esm/engine.d.ts +46 -4
  35. package/dist/esm/engine.js +158 -212
  36. package/dist/esm/index.d.ts +1 -1
  37. package/dist/esm/index.js +1 -1
  38. package/dist/esm/learning.d.ts +16 -1
  39. package/dist/esm/learning.js +161 -10
  40. package/dist/esm/matcher.d.ts +23 -0
  41. package/dist/esm/matcher.js +49 -16
  42. package/dist/esm/parser.js +15 -1
  43. package/dist/esm/resolver.js +22 -5
  44. package/dist/esm/version.d.ts +1 -1
  45. package/dist/esm/version.js +1 -1
  46. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,95 @@ All notable changes to capman are documented here.
4
4
 
5
5
  ---
6
6
 
7
+ ## [0.5.1] — 2026-04-18
8
+ ### Fixed
9
+
10
+ **Critical:**
11
+ - `getStats()` and `getIndex()` now return deep clones of the internal index — callers can no longer corrupt the learning store by mutating the returned object
12
+ - `FileCache` and `FileLearningStore` saves are now serialized through a promise queue — concurrent writes no longer silently drop entries via last-write-wins
13
+ - `clear()` now resets the incremental index and stats counters in both stores — previously left stale boost data after clearing
14
+
15
+ **High:**
16
+ - Session params (`source: 'session'`) no longer leak into API query strings — only injected when the param name appears as a `{template}` in the path
17
+ - `MemoryCache` and `FileCache` now use LRU eviction — previously used FIFO, evicting frequently-accessed entries ahead of cold ones
18
+ - Tiebreak logic in boost corrected — `b.matched` now correctly preserves original winner on tied scores (was `a.matched`, only worked when winner was first in array)
19
+ - LLM error logging now uses `err.message` instead of `${err}` — prevents potential API key exposure from SDK errors that embed auth headers in error objects
20
+
21
+ **Medium:**
22
+ - `rebuildIndex()` after pruning replaced with `subtractFromIndex()` — O(pruned × w) instead of O(n × w), avoids full index rebuild at the 10k entry cap
23
+ - `ask()` and `explain()` matching dispatch unified into `_runMatch()` — eliminates 60 lines of duplicated mode-switching logic
24
+ - Fallback param denylist extended — category nouns like `"orders"`, `"data"`, `"results"`, `"items"` no longer produce junk URLs
25
+ - File write errors now include the actual error message in the log — was silently swallowing `ENOSPC`, `EACCES` etc.
26
+ - `loadSpec()` fetch in parser now has a 10s timeout with `AbortController` — previously hung indefinitely on unresponsive URLs
27
+ - Cache entries now support optional TTL via `cacheTtlMs` in `EngineOptions` — stale entries from removed capabilities are expired on read
28
+ - `matchWithLLM` JSDoc documents the manifest injection surface — capability descriptions and examples are verbatim in the LLM prompt
29
+
30
+ ### Tests
31
+ - 82 tests passing
32
+
33
+ ## [0.5.0] — 2026-04-15
34
+ ### Added
35
+ - Learning index is now incremental — `record()` updates the index in O(w) per entry instead of rebuilding from scratch on every `getStats()` call. Eliminates O(n) CPU spike on every query under load.
36
+ - `getIndex()` method on both `FileLearningStore` and `MemoryLearningStore` — returns live keyword index directly in O(1)
37
+ - `extractParams` exported from public API — enables direct param extraction without going through `match()`
38
+ - `resolverToIntent()` exported from public API — converts a capability's resolver type to its intent string
39
+ - `STOPWORDS` exported from public API — same set used by matcher and learning index
40
+
41
+ ### Fixed
42
+
43
+ **Security:**
44
+ - Auth bypass via cache key — non-public capabilities are no longer cached. Previously User A's cached match for a `user_owned` capability could be served to User B before privacy checks ran
45
+ - Arbitrary file write via CLI — `--out` and `--config-out` flags in `capman generate` now validated against working directory. Path traversal attempts exit with a clear error
46
+ - Prompt injection hardening — system instructions now come before user data in `matchWithLLM` prompt, with explicit `USER_QUERY_START/END` delimiters
47
+
48
+ **Learning system:**
49
+ - Boost feedback loop — learning now records the pre-boost match result, not the post-boost winner
50
+ - Boosted winner no longer gets empty `extractedParams` — params extracted directly via `extractParams()`
51
+ - Cache now stores post-boost result — previously cache/live path could return different capabilities
52
+ - OOS results can no longer be promoted via boost alone — boost skipped when all candidates score 0
53
+ - Tied boost scores now preserve original winner
54
+ - Learning index no longer includes stopwords — words like `"show"`, `"get"`, `"for"` were inflating unrelated scores
55
+ - Boost logic deduplicated — `ask()` and `explain()` share `applyBoostToMatchResult()`
56
+
57
+ **Security — logs:**
58
+ - PII no longer logged at debug level — param values and `auth.userId` redacted as `[REDACTED]`
59
+
60
+ **Cache:**
61
+ - `FileCache` now has a 2048-entry cap with oldest-first eviction — previously grew without bound
62
+
63
+ **Resolver:**
64
+ - Nav params validated against allowlist before substitution — prevents open redirect via encoded path separators
65
+
66
+ **Matcher:**
67
+ - `JSON.parse` failures no longer trigger the circuit breaker — prefixed `LLM_PARSE_ERROR` and treated separately from network failures
68
+ - Required param fallback rejects generic nouns — only accepts identifier-shaped last words
69
+
70
+ **Engine:**
71
+ - Version compatibility warning now uses `console.warn` — was using `logger.warn` suppressed by default `'silent'` log level
72
+ - Version warning wording softened — advisory not mandatory
73
+ - Concurrency limitation documented in `EngineOptions` JSDoc and README
74
+
75
+ ### Tests
76
+ - 80 tests passing (up from 73)
77
+ - Boost tests now use `mode: 'balanced'` — previously used `mode: 'cheap'` making them vacuous
78
+ - Nav open redirect test added
79
+
80
+ ---
81
+
82
+ ## [0.4.5] — 2026-04-08
83
+ ### Added
84
+ - Learning index wired into keyword matcher — boost up to +15 points for historically matched capabilities
85
+ - Manifest version compatibility check — warns when manifest `major.minor` differs from engine version
86
+
87
+ ### Fixed
88
+ - Dead logger warning condition in `matchWithLLM` corrected
89
+ - Empty string `userId` no longer injected into session params
90
+ - `resolverToIntent()` exported and reused in engine
91
+ - Learning boost skipped in `cheap` mode
92
+ - Boost logic applied consistently in both `ask()` and `explain()`
93
+
94
+ ---
95
+
7
96
  ## [0.4.4] — 2026-04-05
8
97
  ### Fixed
9
98
  - Rate limit double-counting on LLM failure — `recordLLMFailure()` no longer increments `llmCallsThisMinute` (slot already reserved by `checkLLMAllowed()`)
package/CODEBASE.md CHANGED
@@ -47,7 +47,7 @@ Key exports:
47
47
  - `validateManifest(manifest)` — validates a `Manifest` at read time
48
48
 
49
49
  Notable rules:
50
- - `id` must match `/^[a-z][a-z0-9_]*$/` — snake_case only
50
+ - `id` must match `/^[a-z0-9_]+$/` — snake_case only
51
51
  - `description` minimum 10 characters
52
52
  - Capability IDs must be unique within a manifest
53
53
 
@@ -61,35 +61,38 @@ Key exports:
61
61
  - `loadConfig(path?)` → `CapmanConfig` — loads `capman.config.js` via `require()`
62
62
  - `writeManifest(manifest, path?)` — writes `manifest.json`
63
63
  - `readManifest(path?)` → `Manifest` — reads and Zod-validates `manifest.json`
64
- - `validate(manifest)` → `ValidationResult` — runs Zod + warns on missing examples
65
- - `generateStarterConfig()` → `string` — returns starter `capman.config.js` content
64
+ - `validate(manifest)` → `ValidationResult`
65
+ - `generateStarterConfig()` → `string`
66
66
  - `VERSION` — current version string, auto-generated by `scripts/version.js`
67
67
 
68
+ Note: `loadConfig()` uses `require()` internally — CJS config files only. ESM config files (`.mjs` or `"type": "module"`) are not supported. Full ESM config support is planned for v0.6.
69
+
68
70
  ---
69
71
 
70
72
  ### `src/matcher.ts`
71
73
  Intent matching — keyword scoring and LLM-based matching.
72
74
 
73
75
  Key exports:
74
- - `match(query, manifest)` → `MatchResult`
75
- - Scores every capability against the query
76
- - Returns winner + all candidates with scores
77
- - Extracts params from the query using keyword heuristics
78
- - Returns `out_of_scope` if best score < 50%
79
- - `matchWithLLM(query, manifest, { llm })` → `MatchResult`
80
- - Sends structured prompt to LLM with query as JSON field (prompt injection safe)
81
- - Returns matched capability, confidence, intent, extracted params
76
+ - `match(query, manifest)` → `MatchResult` — scores all capabilities, returns winner + all candidates
77
+ - `matchWithLLM(query, manifest, { llm })` → `MatchResult` — LLM-based matching
78
+ - Query passed as `JSON.stringify({ user_query })` with system instructions before user data
79
+ - `USER_QUERY_START/END` delimiters separate instructions from user-controlled content
80
+ - Parse failures throw `LLM_PARSE_ERROR:` prefixed errors not counted as network failures
82
81
  - Errors propagate to caller — no internal try/catch
82
+ - `extractParams(query, capability)` → `Record<string, string | null>` — direct param extraction
83
+ - `resolverToIntent(capability)` → intent string — converts resolver type to intent
84
+ - `STOPWORDS` — set of words filtered from scoring and learning index
83
85
 
84
86
  Scoring algorithm (weights):
85
- - Examples match: up to 60 points
87
+ - Examples: best single-example overlap score — up to 60 points (not accumulated across examples)
86
88
  - Description match: up to 30 points
87
89
  - Name match: up to 10 points
88
90
 
89
91
  Param extraction:
90
92
  - `isIdParam` — single token (e.g. `order_id=1234`)
91
93
  - `isNavParam` — single token after nav keywords (`to`, `open`, `show`)
92
- - Everything else multi-word joined with `-` (e.g. `product=blue-jacket`)
94
+ - Multi-word — joined with `-` (e.g. `product=blue-jacket`)
95
+ - Required param fallback — only accepts identifier-shaped last word (rejects generic nouns)
93
96
  - Optional params stay `null` if no keyword match found
94
97
 
95
98
  ---
@@ -100,26 +103,23 @@ Capability execution — API calls, navigation, hybrid.
100
103
  Key exports:
101
104
  - `resolve(matchResult, params, options)` → `ResolveResult`
102
105
  - Enforces privacy before executing
103
- - Injects `auth.userId` into session params automatically
106
+ - Injects `auth.userId` into session params (skipped if empty string or undefined)
104
107
  - Supports `dryRun: true` — returns call plan without executing
105
108
  - Retries with `AbortController` timeout on failure
106
- - - Returns `status` and parsed `data` from API response
107
- - `null` and `undefined` params are never written into URLs — skipped silently
108
- - Nav param values are URL-encoded via `encodeURIComponent` — matches API resolver behavior
109
+ - Returns `status` and parsed `data` from API response
110
+ - `null` and `undefined` params never written into URLs
111
+ - Nav param values URL-encoded via `encodeURIComponent`
112
+ - Nav params validated against `[a-zA-Z0-9_-]` allowlist — rejects path separators
109
113
 
110
114
  `ResolveOptions`:
111
- - `baseUrl` prepended to all API paths
112
- - `auth` — `{ isAuthenticated, role, userId }`
113
- - `dryRun` — skip actual fetch
114
- - `retries` — retry count on failure (default: 0)
115
- - `timeoutMs` — abort timeout (default: 5000)
116
- - `headers` — custom request headers
117
- - `fetch` — injectable fetch function (used in tests)
115
+ - `baseUrl`, `auth`, `dryRun`, `retries`, `timeoutMs`, `headers`, `fetch`
118
116
 
119
117
  Privacy enforcement:
120
118
  - `public` — always allowed
121
119
  - `user_owned` — requires `auth.isAuthenticated === true`
122
- - `admin` — requires `auth.role === 'admin'`
120
+ - `admin` — requires `auth.isAuthenticated === true` AND `auth.role === 'admin'`
121
+
122
+ Debug logging: param values and `auth.userId` are redacted as `[REDACTED]` — never logged in plaintext.
123
123
 
124
124
  ---
125
125
 
@@ -129,22 +129,24 @@ Pluggable cache backends.
129
129
  Key exports:
130
130
  - `CacheStore` interface — `get(key)`, `set(key, result)`, `clear()`, `size()`
131
131
  - `MemoryCache` — in-memory Map, 512-entry cap with oldest-first eviction
132
- - `FileCache` — async `fs.promises` read/write, lazy-loaded on first access
133
- - `ComboCache` — memory-first with file fallback, promotes file hits to memory
132
+ - `FileCache` — async `fs.promises` read/write, 2048-entry cap with oldest-first eviction
133
+ - `ComboCache` — memory-first with file fallback
134
134
  - `normalizeQuery(query)` — lowercase + trim + collapse whitespace → cache key
135
- - `buildCacheKey(query, capabilityId, params)` — smarter key using capability + params (exported for future post-match cache layer not currently used by engine)
135
+ - `buildCacheKey(query, capabilityId, params)` — exported for future post-match cache layer (not currently used by engine)
136
+
137
+ Security: Only `public` capabilities are cached. Non-public (`user_owned`, `admin`) are never cached — prevents auth bypass where one user's cached match is served to another.
136
138
 
137
139
  Notes:
138
140
  - `FileCache` and `ComboCache` are single-instance only — concurrent writers will corrupt
139
- - For multi-instance deployments, use a Redis adapter (planned v0.5)
141
+ - For multi-instance deployments, use a Redis adapter (planned v0.6)
140
142
 
141
143
  ---
142
144
 
143
145
  ### `src/learning.ts`
144
- Usage analytics and keyword index.
146
+ Usage analytics and keyword index — now incremental.
145
147
 
146
148
  Key exports:
147
- - `LearningStore` interface — `record(entry)`, `getStats()`, `getTopCapabilities(limit)`
149
+ - `LearningStore` interface — `record(entry)`, `getStats()`, `getTopCapabilities(limit)`, `getIndex()`
148
150
  - `FileLearningStore` — persists to `.capman/learning.json`, caps at 10,000 entries
149
151
  - `MemoryLearningStore` — in-memory only, used in tests
150
152
 
@@ -154,9 +156,16 @@ Key exports:
154
156
  - `timestamp`
155
157
 
156
158
  `KeywordStats` (from `getStats()`):
157
- - `index` — `{ word → { capabilityId → hitCount } }` — foundation for adaptive matching
159
+ - `index` — `{ word → { capabilityId → hitCount } }` — used by engine for learning boost
158
160
  - `totalQueries`, `llmQueries`, `cacheHits`, `outOfScope`
159
161
 
162
+ Performance:
163
+ - Index is maintained incrementally in `record()` — O(w) per entry where w = meaningful words
164
+ - `getStats()` returns cached counters — O(1), no rebuild
165
+ - `getIndex()` returns live index — O(1)
166
+ - Full rebuild only on pruning (when entries exceed 10,000 cap)
167
+ - Stopwords filtered from index — same `STOPWORDS` set as `matcher.ts`
168
+
160
169
  ---
161
170
 
162
171
  ### `src/engine.ts`
@@ -167,27 +176,31 @@ Key exports:
167
176
  - `EngineOptions` — all constructor options
168
177
  - `EngineResult` — `{ match, resolution, resolvedVia, durationMs, trace }`
169
178
 
179
+ ⚠️ **Concurrency:** `CapmanEngine` is not safe for sharing across concurrent async request handlers. The LLM rate limiter, circuit breaker, and learning index cache are instance-level mutable state. Create one engine per request in server deployments, or use `cheap` mode for shared instances.
180
+
170
181
  `CapmanEngine` methods:
171
- - `ask(query, overrides?)` → `EngineResult` — full pipeline: cache → match → resolve → learn
172
- - `explain(query)` → `ExplainResult` — match only, no execution, with candidate explanations
182
+ - `ask(query, overrides?)` → `EngineResult` — full pipeline: cache → match → boost → resolve → learn
183
+ - `explain(query)` → `ExplainResult` — match + boost only, no execution, no cache/learning write
173
184
  - `getStats()` → `KeywordStats | null`
174
185
  - `getTopCapabilities(limit?)` → `Array<{ id, hits }>`
175
186
  - `clearCache()`
176
187
 
177
188
  Matching pipeline in `ask()`:
178
- 1. Cache check — return immediately on hit
189
+ 1. Cache check — return immediately on hit (public capabilities only)
179
190
  2. Match — `cheap` / `balanced` / `accurate` mode
180
191
  3. Privacy check — recorded in trace
181
- 4. Cache setstores under normalized query key
182
- 5. Resolveactual API call or nav
183
- 6. Reasoning build human-readable array
184
- 7. Learning record
192
+ 4. Learning boostup to +15 points for historically matched capabilities (skipped in `cheap` mode, skipped if all candidates score 0)
193
+ 5. Cache set stores post-boost result under normalized query key (public only)
194
+ 6. Resolveactual API call or nav
195
+ 7. Reasoning build — human-readable array
196
+ 8. Learning record — pre-boost result recorded to prevent feedback loop
185
197
 
186
198
  LLM rate limiting (all modes respect these):
187
199
  - `maxLLMCallsPerMinute` — sliding window (default: 60)
188
200
  - `llmCooldownMs` — minimum gap between calls (default: 0)
189
201
  - `llmCircuitBreakerThreshold` — failures before circuit opens (default: 3)
190
202
  - `llmCircuitBreakerResetMs` — circuit reset time (default: 60,000ms)
203
+ - Parse failures (`LLM_PARSE_ERROR`) do NOT count toward circuit breaker — only network failures do
191
204
 
192
205
  ---
193
206
 
@@ -203,9 +216,7 @@ Key exports:
203
216
  - Extracts path/query/body params
204
217
  - Generates examples from operation summaries
205
218
 
206
- `ParseResult`:
207
- - `config` — ready-to-use `CapmanConfig`
208
- - `stats` — `{ total, skipped, warnings }`
219
+ `ParseResult`: `{ config, stats: { total, skipped, warnings } }`
209
220
 
210
221
  Supported: OpenAPI 3.x, Swagger 2.x (JSON or YAML)
211
222
 
@@ -217,7 +228,8 @@ Minimal logger. Silent by default.
217
228
  Key exports:
218
229
  - `logger` — singleton with `debug()`, `info()`, `warn()`, `error()`
219
230
  - `setLogLevel(level)` — `'silent' | 'error' | 'warn' | 'info' | 'debug'`
220
- - `LogLevel` type
231
+
232
+ Note: The manifest version compatibility warning uses `console.warn` directly (not `logger.warn`) so it is always visible regardless of log level.
221
233
 
222
234
  ---
223
235
 
@@ -225,134 +237,58 @@ Key exports:
225
237
  Public API surface. Re-exports everything the library exposes.
226
238
 
227
239
  Notable:
228
- - `ask(query, manifest, options?)` — convenience function, delegates to `CapmanEngine` internally
240
+ - `ask(query, manifest, options?)` — convenience function, delegates to `CapmanEngine`
229
241
  - Marked `@deprecated` — use `CapmanEngine` directly for full features
230
- - `MatchMode` type — `'cheap' | 'balanced' | 'accurate'`
231
- - All types, classes, and functions from all modules above
242
+ - `MatchMode` — `'cheap' | 'balanced' | 'accurate'`
243
+ - `extractParams`, `resolverToIntent`, `STOPWORDS` exported for advanced use cases
232
244
 
233
245
  ---
234
246
 
235
247
  ## bin/
236
248
 
237
249
  ### `bin/capman.js`
238
- Entry point only. Reads `command` from `process.argv` and routes to the correct module.
239
- ~20 lines. No logic here.
250
+ Entry point only (~20 lines). Routes `command` to the correct module.
240
251
 
241
252
  ### `bin/lib/shared.js`
242
- Shared utilities used by all command modules.
253
+ Exports: `args`, `command`, `flags`, `getFlag`, `c`, `log`, `header`, `requireSrc`
243
254
 
244
- Exports:
245
- - `args`, `command`, `flags` — parsed from `process.argv`
246
- - `getFlag(name)` — returns value of `--name value` flag
247
- - `c` — ANSI color codes (`reset`, `bold`, `teal`, `yellow`, `red`, `green`, `gray`)
248
- - `log` — `{ info, success, warn, error, blank }`
249
- - `header()` — prints capman version header
250
- - `requireSrc()` — loads `dist/cjs/index.js`, auto-builds if missing
251
-
252
- ### `bin/lib/cmd-init.js`
253
- Creates `capman.config.js` starter file in current directory.
255
+ `getFlag(name)` — exits with error if flag is present but has no value (e.g. `--from` with no path).
254
256
 
255
257
  ### `bin/lib/cmd-generate.js`
256
- Three generation paths:
257
- - `--from <path|url>` OpenAPI parser `capman.config.js` + `manifest.json`
258
- - `--ai` LLM-assisted generation from plain English description
259
- - _(no flags)_ → load `capman.config.js` manually → `manifest.json`
260
-
261
- Contains `buildAIPrompt(description)` and `callLLM(provider, apiKey, prompt)`.
262
- Supports `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `OPENROUTER_API_KEY`.
263
-
264
- ### `bin/lib/cmd-validate.js`
265
- Reads `manifest.json`, runs Zod validation, prints errors and warnings.
266
- Exits with code 1 if invalid.
267
-
268
- ### `bin/lib/cmd-inspect.js`
269
- Prints all capabilities in `manifest.json` — name, ID, resolver type, privacy, description, first example.
270
-
271
- ### `bin/lib/cmd-demo.js`
272
- Live demo using a hardcoded e-commerce manifest.
273
- Shows the full **QUERY → MATCH → EXECUTION → RESULT → EXPLANATION** blueprint for 4 sample queries.
274
- No config or API key required.
275
-
276
- ### `bin/lib/cmd-run.js`
277
- Runs a single query against the current `manifest.json`.
278
- `--debug` flag shows all candidate scores.
279
-
280
- ### `bin/lib/cmd-explain.js`
281
- Runs `engine.explain(query)` and prints the full explanation:
282
- - What matched and why
283
- - All candidates with per-candidate explanations
284
- - What would execute (without executing)
285
- - Whether privacy would block it
258
+ Three generation paths: `--from` (OpenAPI), `--ai` (LLM-assisted), manual.
259
+ Output paths validated via `safeOutputPath()` rejects traversal outside working directory.
260
+ Contains `buildAIPrompt()` and `callLLM()`.
261
+
262
+ ### `bin/lib/cmd-init.js` — creates `capman.config.js`
263
+ ### `bin/lib/cmd-validate.js` validates `manifest.json`
264
+ ### `bin/lib/cmd-inspect.js` — prints all capabilities
265
+ ### `bin/lib/cmd-demo.js` — live demo with hardcoded e-commerce manifest
266
+ ### `bin/lib/cmd-run.js` — runs a query, `--debug` shows all candidates
267
+ ### `bin/lib/cmd-explain.js` runs `engine.explain()` and prints full breakdown
268
+ ### `bin/lib/cmd-help.js` usage and command list
286
269
 
287
270
  ---
288
271
 
289
272
  ## tests/
290
273
 
291
- ### `tests/matcher.test.ts`
292
- 14 tests covering:
293
- - Keyword scoring accuracy
294
- - Out-of-scope detection
295
- - Param extraction (single token, multi-word, ID params, nav params)
296
- - `ask()` matching modes (cheap, balanced, accurate, default)
297
-
298
- ### `tests/resolver.test.ts`
299
- 18 tests covering:
300
- - API resolver — dry run, path substitution, query string params
301
- - Nav resolver — destination building
302
- - Hybrid resolver — both API and nav
303
- - No match — graceful failure
304
- - Privacy enforcement — public, user_owned, admin, session injection
305
- - Fetch error handling — network error, non-ok status, ok + data
306
- - Retry behaviour — succeeds after N failures, exhausts retries
307
-
308
- ### `tests/engine.test.ts`
309
- 26 tests covering:
310
- - Basic `ask()` — match and resolve
311
- - Caching — cache hit, cache cleared, ComboCache promotion
312
- - Learning — records queries, top capabilities, resolvedVia tracking
313
- - Matching modes — cheap never calls LLM, accurate calls LLM
314
- - Execution trace — candidates, reasoning, steps, cache hit trace
315
- - `explain()` — matched, out of scope, candidates with explanations, wouldExecute, blocked, no side effects
316
- - LLM rate limiting — rate limit, cooldown, circuit breaker, fallback on failure
317
-
318
- ### `tests/parser.test.ts`
319
- 9 tests covering:
320
- - Capability extraction from spec
321
- - Correct IDs from `operationId`
322
- - Privacy inference from tags and security
323
- - Path and query param extraction
324
- - Request body field extraction
325
- - Base URL extraction
326
- - Skipping operations with insufficient info
327
- - Error on missing file
328
-
329
- ---
274
+ ### `tests/matcher.test.ts` — 16 tests
275
+ Keyword scoring, OOS detection, param extraction, LLM edge cases (hallucinated ID, undefined reasoning)
330
276
 
331
- ## scripts/
277
+ ### `tests/resolver.test.ts` — 22 tests
278
+ API/nav/hybrid resolvers, privacy enforcement, session injection, null params, nav open redirect
332
279
 
333
- ### `scripts/version.js`
334
- Prebuild script. Reads `version` from `package.json` and writes:
335
- ```typescript
336
- // Auto-generated by scripts/version.js — do not edit manually
337
- export const VERSION = '0.4.2'
338
- ```
339
- to `src/version.ts`. Runs automatically before every `pnpm run build`.
280
+ ### `tests/engine.test.ts` — 33 tests
281
+ `ask()`, `explain()`, caching, learning, matching modes, trace, rate limiting, manifest version check, learning boost
340
282
 
341
- `src/version.ts` is gitignored.
283
+ ### `tests/parser.test.ts` 9 tests
284
+ OpenAPI capability extraction, privacy inference, param extraction, base URL, error handling
342
285
 
343
286
  ---
344
287
 
345
- ## test/ (integration, not in CI)
346
-
347
- ### `test/conduit.config.js`
348
- capman config for the RealWorld Conduit app (`conduit.productionready.io`).
349
- 8 capabilities covering all resolver types.
350
-
351
- ### `test/test-conduit.ts`
352
- Tests keyword matcher against live Conduit API.
288
+ ## scripts/
353
289
 
354
- ### `test/test-llm-live.ts`
355
- Tests `matchWithLLM` and `CapmanEngine` accurate mode against live API using OpenRouter free models.
290
+ ### `scripts/version.js`
291
+ Prebuild script. Reads `version` from `package.json` and writes `src/version.ts`. Runs automatically before every build.
356
292
 
357
293
  ---
358
294
 
@@ -360,13 +296,13 @@ Tests `matchWithLLM` and `CapmanEngine` accurate mode against live API using Ope
360
296
 
361
297
  | File | Purpose |
362
298
  |---|---|
363
- | `tsconfig.json` | CJS build → `dist/cjs/` |
364
- | `tsconfig.esm.json` | ESM build → `dist/esm/` |
299
+ | `tsconfig.json` | CJS build → `dist/cjs/` with `.d.ts` |
300
+ | `tsconfig.esm.json` | ESM build → `dist/esm/` with `.d.ts` |
365
301
  | `package.json` | Version, exports map, scripts, dependencies |
366
- | `.github/workflows/ci.yml` | Build + test + verify dist on every push |
367
- | `.gitignore` | Ignores `dist/`, `node_modules/`, `src/version.ts`, `.capman/` |
302
+ | `.github/workflows/ci.yml` | Build + test + verify both dist outputs on every push |
368
303
  | `CHANGELOG.md` | All notable changes per version |
369
304
  | `CODEBASE.md` | This file |
305
+ | `ROADMAP_v0.5.0.md` | Prioritized fix and feature roadmap |
370
306
 
371
307
  ---
372
308
 
@@ -381,13 +317,15 @@ Developer writes capman.config.js
381
317
 
382
318
  CapmanEngine.ask("user query")
383
319
 
384
- cache.ts → cache hit? return immediately
320
+ cache.ts → cache hit? return immediately (public only)
321
+
322
+ matcher.ts → score all capabilities → pick winner
385
323
 
386
- matcher.ts score all capabilities pick winner
324
+ learning.ts apply boost (+0 to +15) based on keyword history
387
325
 
388
- resolver.ts → enforce privacy → call API or navigate
326
+ resolver.ts → enforce privacy → call API or navigate
389
327
 
390
- learning.ts → record query + result
328
+ learning.ts → record pre-boost match result
391
329
 
392
- EngineResult → { match, resolution, trace, resolvedVia }
330
+ EngineResult → { match, resolution, trace, resolvedVia }
393
331
  ```
package/README.md CHANGED
@@ -66,6 +66,29 @@ console.log(result.resolvedVia) // 'keyword' | 'llm' | 'cache'
66
66
  console.log(result.trace.reasoning) // ['Matched "check_product_availability" with 100% confidence', ...]
67
67
  ```
68
68
 
69
+
70
+ ### ⚠️ Concurrency Warning
71
+
72
+ `CapmanEngine` is **not safe** for sharing across concurrent async request handlers. The LLM rate limiter, circuit breaker, and learning index cache are instance-level mutable state.
73
+
74
+ In a Node.js server, create one engine per request:
75
+
76
+ ```typescript
77
+ // ✅ Safe
78
+ app.post('/ask', async (req, res) => {
79
+ const engine = new CapmanEngine({ manifest, llm })
80
+ const result = await engine.ask(req.body.query)
81
+ res.json(result)
82
+ })
83
+
84
+ // ❌ Unsafe under concurrent load
85
+ const engine = new CapmanEngine({ manifest, llm })
86
+ app.post('/ask', async (req, res) => {
87
+ const result = await engine.ask(req.body.query) // race condition
88
+ res.json(result)
89
+ })
90
+ ```
91
+
69
92
  **3. See it live**
70
93
 
71
94
  ```bash
@@ -1,7 +1,22 @@
1
1
  'use strict'
2
2
 
3
+ const path = require('path')
3
4
  const fs = require('fs')
4
5
  const { header, log, c, flags, getFlag, requireSrc } = require('./shared')
6
+ // ─── Path safety guard ────────────────────────────────────────────────────────
7
+
8
+ function safeOutputPath(rawPath, cwd) {
9
+ const resolved = path.resolve(cwd, rawPath)
10
+ if (!resolved.startsWith(cwd + path.sep) && resolved !== cwd) {
11
+ log.error(`Output path "${rawPath}" resolves outside the working directory.`)
12
+ console.log(` Resolved: ${resolved}`)
13
+ console.log(` Allowed: ${cwd}`)
14
+ process.exit(1)
15
+ }
16
+ return resolved
17
+ }
18
+
19
+
5
20
 
6
21
  // ─── AI prompt builder ────────────────────────────────────────────────────────
7
22
 
@@ -120,11 +135,13 @@ module.exports = async function cmdGenerate() {
120
135
  header()
121
136
  const { generate, loadConfig, writeManifest, validate, parseOpenAPI } = requireSrc()
122
137
 
123
- const fromFlag = getFlag('--from')
138
+ const fromFlag = getFlag('--from')
124
139
  const aiFlag = flags.includes('--ai')
125
- const outPath = getFlag('--out') ?? 'manifest.json'
126
- const configOut = getFlag('--config-out') ?? 'capman.config.js'
140
+ const cwd = process.cwd()
141
+ const outPath = safeOutputPath(getFlag('--out') ?? 'manifest.json', cwd)
142
+ const configOut = safeOutputPath(getFlag('--config-out') ?? 'capman.config.js', cwd)
127
143
 
144
+
128
145
  // ── Path 1: OpenAPI parser ───────────────────────────────────────────────
129
146
  if (fromFlag) {
130
147
  log.info(`Parsing OpenAPI spec: ${fromFlag}`)
@@ -6,7 +6,7 @@ export interface CacheEntry {
6
6
  hits: number;
7
7
  }
8
8
  export interface CacheStore {
9
- get(key: string): Promise<CacheEntry | null>;
9
+ get(key: string, ttlMs?: number): Promise<CacheEntry | null>;
10
10
  set(key: string, result: MatchResult): Promise<void>;
11
11
  clear(): Promise<void>;
12
12
  size(): Promise<number>;
@@ -21,7 +21,7 @@ export declare function normalizeQuery(query: string): string;
21
21
  export declare function buildCacheKey(query: string, capabilityId: string | null, extractedParams: Record<string, string | null>): string;
22
22
  export declare class MemoryCache implements CacheStore {
23
23
  private store;
24
- get(key: string): Promise<CacheEntry | null>;
24
+ get(key: string, ttlMs?: number): Promise<CacheEntry | null>;
25
25
  set(key: string, result: MatchResult): Promise<void>;
26
26
  clear(): Promise<void>;
27
27
  size(): Promise<number>;
@@ -30,10 +30,12 @@ export declare class FileCache implements CacheStore {
30
30
  private filePath;
31
31
  private store;
32
32
  private loaded;
33
+ private saveQueue;
33
34
  constructor(filePath?: string);
34
35
  private load;
35
36
  private save;
36
- get(key: string): Promise<CacheEntry | null>;
37
+ private _doSave;
38
+ get(key: string, ttlMs?: number): Promise<CacheEntry | null>;
37
39
  set(key: string, result: MatchResult): Promise<void>;
38
40
  clear(): Promise<void>;
39
41
  size(): Promise<number>;
@@ -42,7 +44,7 @@ export declare class ComboCache implements CacheStore {
42
44
  private memory;
43
45
  private file;
44
46
  constructor(filePath?: string);
45
- get(key: string): Promise<CacheEntry | null>;
47
+ get(key: string, ttlMs?: number): Promise<CacheEntry | null>;
46
48
  set(key: string, result: MatchResult): Promise<void>;
47
49
  clear(): Promise<void>;
48
50
  size(): Promise<number>;
@@ -1 +1 @@
1
- {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/cache.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAK1C,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,WAAW,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;CACb;AAID,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAA;IAC5C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACpD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACtB,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,CAAA;CACxB;AAID,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED;;;;;GAKG;AAEH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,MAAM,GAAG,IAAI,EAC3B,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,GAC7C,MAAM,CAQR;AAMD,qBAAa,WAAY,YAAW,UAAU;IAC5C,OAAO,CAAC,KAAK,CAAgC;IAEvC,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAU5C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAepD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IACtB,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC;CAC9B;AAID,qBAAa,SAAU,YAAW,UAAU;IAC1C,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,KAAK,CAAqC;IAClD,OAAO,CAAC,MAAM,CAAQ;gBAEV,QAAQ,SAAuB;YAK7B,IAAI;YAiBJ,IAAI;IAaZ,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAW5C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAYpD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAKtB,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC;CAI9B;AAID,qBAAa,UAAW,YAAW,UAAU;IAC3C,OAAO,CAAC,MAAM,CAAa;IAC3B,OAAO,CAAC,IAAI,CAAW;gBAEX,QAAQ,SAAuB;IAKrC,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAY5C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAOpD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAOtB,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC;CAG9B"}
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/cache.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAK1C,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,WAAW,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;CACb;AAID,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAA;IAC5D,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACpD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACtB,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,CAAA;CACxB;AAID,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED;;;;;GAKG;AAEH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,MAAM,GAAG,IAAI,EAC3B,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,GAC7C,MAAM,CAQR;AAOD,qBAAa,WAAY,YAAW,UAAU;IAC5C,OAAO,CAAC,KAAK,CAAgC;IAEvC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAiB5D,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAepD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IACtB,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC;CAC9B;AAMD,qBAAa,SAAU,YAAW,UAAU;IAC1C,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,KAAK,CAAyC;IACtD,OAAO,CAAC,MAAM,CAAoC;IAClD,OAAO,CAAC,SAAS,CAA6C;gBAElD,QAAQ,SAAuB;YAK7B,IAAI;IAiBlB,OAAO,CAAC,IAAI;YAKE,OAAO;IAaf,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAmB5D,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAmBpD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAKtB,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC;CAI9B;AAID,qBAAa,UAAW,YAAW,UAAU;IAC3C,OAAO,CAAC,MAAM,CAAa;IAC3B,OAAO,CAAC,IAAI,CAAW;gBAEX,QAAQ,SAAuB;IAKrC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAY5D,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAOpD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAOtB,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC;CAG9B"}