capman 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/esm/cache.js CHANGED
@@ -43,36 +43,33 @@ export class FileCache {
43
43
  this.store = new Map();
44
44
  this.loaded = false;
45
45
  this.filePath = path.resolve(process.cwd(), filePath);
46
+ logger.info(`FileCache initialized — writing to: ${this.filePath}`);
46
47
  }
47
- load() {
48
+ async load() {
48
49
  if (this.loaded)
49
50
  return;
50
51
  try {
51
- if (fs.existsSync(this.filePath)) {
52
- const raw = JSON.parse(fs.readFileSync(this.filePath, 'utf-8'));
53
- this.store = new Map(Object.entries(raw));
54
- logger.debug(`File cache loaded: ${this.store.size} entries`);
55
- }
52
+ const raw = await fs.promises.readFile(this.filePath, 'utf-8');
53
+ this.store = new Map(Object.entries(JSON.parse(raw)));
54
+ logger.debug(`File cache loaded: ${this.store.size} entries`);
56
55
  }
57
56
  catch {
58
- logger.warn(`Failed to load file cache at ${this.filePath}`);
57
+ // File doesn't exist yet start fresh
59
58
  }
60
59
  this.loaded = true;
61
60
  }
62
- save() {
61
+ async save() {
63
62
  try {
64
63
  const dir = path.dirname(this.filePath);
65
- if (!fs.existsSync(dir))
66
- fs.mkdirSync(dir, { recursive: true });
67
- const obj = Object.fromEntries(this.store);
68
- fs.writeFileSync(this.filePath, JSON.stringify(obj, null, 2));
64
+ await fs.promises.mkdir(dir, { recursive: true });
65
+ await fs.promises.writeFile(this.filePath, JSON.stringify(Object.fromEntries(this.store), null, 2));
69
66
  }
70
67
  catch {
71
68
  logger.warn(`Failed to save file cache to ${this.filePath}`);
72
69
  }
73
70
  }
74
71
  async get(query) {
75
- this.load();
72
+ await this.load();
76
73
  const key = normalizeQuery(query);
77
74
  const entry = this.store.get(key);
78
75
  if (entry) {
@@ -83,7 +80,7 @@ export class FileCache {
83
80
  return null;
84
81
  }
85
82
  async set(query, result) {
86
- this.load();
83
+ await this.load();
87
84
  const key = normalizeQuery(query);
88
85
  this.store.set(key, {
89
86
  query,
@@ -91,15 +88,15 @@ export class FileCache {
91
88
  cachedAt: new Date().toISOString(),
92
89
  hits: 0,
93
90
  });
94
- this.save();
91
+ await this.save();
95
92
  logger.debug(`Cache set (file): "${query}"`);
96
93
  }
97
94
  async clear() {
98
95
  this.store.clear();
99
- this.save();
96
+ await this.save();
100
97
  }
101
98
  async size() {
102
- this.load();
99
+ await this.load();
103
100
  return this.store.size;
104
101
  }
105
102
  }
@@ -119,6 +116,7 @@ export class ComboCache {
119
116
  if (fileHit) {
120
117
  // Promote to memory for next time
121
118
  await this.memory.set(query, fileHit.result);
119
+ logger.debug(`Cache promoted to memory: "${query}"`);
122
120
  return fileHit;
123
121
  }
124
122
  return null;
@@ -1,7 +1,7 @@
1
1
  import { match as _match, matchWithLLM as _matchWithLLM } from './matcher';
2
2
  import { resolve as _resolve } from './resolver';
3
- import { ComboCache } from './cache';
4
- import { FileLearningStore } from './learning';
3
+ import { MemoryCache } from './cache';
4
+ import { MemoryLearningStore } from './learning';
5
5
  import { logger } from './logger';
6
6
  // ─── CapmanEngine ─────────────────────────────────────────────────────────────
7
7
  export class CapmanEngine {
@@ -13,14 +13,16 @@ export class CapmanEngine {
13
13
  this.auth = options.auth;
14
14
  this.headers = options.headers;
15
15
  this.threshold = options.threshold ?? 50;
16
- // Cache — default ComboCache, or disabled with false
16
+ // Cache — default MemoryCache (no filesystem writes), or disabled with false
17
+ // Use FileCache or ComboCache explicitly for persistence across restarts
17
18
  this.cache = options.cache === false
18
19
  ? null
19
- : (options.cache ?? new ComboCache());
20
- // Learning — default FileLearningStore, or disabled with false
20
+ : (options.cache ?? new MemoryCache());
21
+ // Learning — default MemoryLearningStore (no filesystem writes), or disabled with false
22
+ // Use FileLearningStore explicitly for persistence across restarts
21
23
  this.learning = options.learning === false
22
24
  ? null
23
- : (options.learning ?? new FileLearningStore());
25
+ : (options.learning ?? new MemoryLearningStore());
24
26
  logger.info(`CapmanEngine initialized — mode: ${this.mode}, cache: ${this.cache ? 'enabled' : 'disabled'}, learning: ${this.learning ? 'enabled' : 'disabled'}`);
25
27
  }
26
28
  /**
@@ -36,68 +38,145 @@ export class CapmanEngine {
36
38
  */
37
39
  async ask(query, overrides = {}) {
38
40
  const start = Date.now();
41
+ const steps = [];
42
+ let resolvedVia = 'keyword';
39
43
  // ── Step 1: Check cache ──────────────────────────────────────────────────
44
+ const cacheStart = Date.now();
40
45
  if (this.cache) {
41
46
  const cached = await this.cache.get(query);
42
47
  if (cached) {
48
+ steps.push({ type: 'cache_check', status: 'hit', durationMs: Date.now() - cacheStart, detail: 'Served from cache' });
43
49
  logger.info(`Cache hit for: "${query}"`);
44
50
  const resolution = await _resolve(cached.result, cached.result.extractedParams, this.resolveOptions(overrides));
51
+ const trace = {
52
+ query,
53
+ candidates: cached.result.candidates ?? [],
54
+ reasoning: [`Served from cache (original: ${cached.result.reasoning})`],
55
+ steps,
56
+ resolvedVia: 'cache',
57
+ totalMs: Date.now() - start,
58
+ };
45
59
  const result = {
46
60
  match: cached.result,
47
61
  resolution,
48
62
  resolvedVia: 'cache',
49
63
  durationMs: Date.now() - start,
64
+ trace,
50
65
  };
51
66
  await this.recordLearning(query, cached.result, 'cache');
52
67
  return result;
53
68
  }
69
+ steps.push({ type: 'cache_check', status: 'miss', durationMs: Date.now() - cacheStart });
70
+ }
71
+ else {
72
+ steps.push({ type: 'cache_check', status: 'skip', durationMs: 0, detail: 'Cache disabled' });
54
73
  }
55
74
  // ── Step 2: Match ────────────────────────────────────────────────────────
56
75
  let matchResult;
57
- let resolvedVia = 'keyword';
58
76
  switch (this.mode) {
59
77
  case 'cheap': {
78
+ const t = Date.now();
60
79
  matchResult = _match(query, this.manifest);
80
+ steps.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t, detail: `confidence: ${matchResult.confidence}%` });
61
81
  break;
62
82
  }
63
83
  case 'accurate': {
64
84
  if (this.llm) {
85
+ const t = Date.now();
65
86
  matchResult = await _matchWithLLM(query, this.manifest, { llm: this.llm });
66
87
  resolvedVia = 'llm';
88
+ steps.push({ type: 'llm_match', status: 'pass', durationMs: Date.now() - t, detail: `confidence: ${matchResult.confidence}%` });
67
89
  }
68
90
  else {
69
91
  logger.warn('accurate mode requires llm — falling back to keyword');
92
+ const t = Date.now();
70
93
  matchResult = _match(query, this.manifest);
94
+ steps.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t, detail: 'llm not provided, used keyword' });
71
95
  }
72
96
  break;
73
97
  }
74
98
  case 'balanced':
75
99
  default: {
100
+ const t1 = Date.now();
76
101
  const keywordResult = _match(query, this.manifest);
102
+ steps.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t1, detail: `confidence: ${keywordResult.confidence}%` });
77
103
  if (keywordResult.confidence >= this.threshold || !this.llm) {
78
104
  matchResult = keywordResult;
79
105
  }
80
106
  else {
81
107
  logger.info(`Low confidence (${keywordResult.confidence}%) — escalating to LLM`);
108
+ const t2 = Date.now();
82
109
  matchResult = await _matchWithLLM(query, this.manifest, { llm: this.llm });
83
110
  resolvedVia = 'llm';
111
+ steps.push({ type: 'llm_match', status: 'pass', durationMs: Date.now() - t2, detail: `confidence: ${matchResult.confidence}%` });
84
112
  }
85
113
  break;
86
114
  }
87
115
  }
88
- // ── Step 3: Cache the match result ───────────────────────────────────────
116
+ // ── Step 3: Privacy check ────────────────────────────────────────────────
117
+ if (matchResult.capability) {
118
+ const privacyLevel = matchResult.capability.privacy.level;
119
+ steps.push({
120
+ type: 'privacy_check',
121
+ status: 'pass',
122
+ durationMs: 0,
123
+ detail: `level: ${privacyLevel}`,
124
+ });
125
+ }
126
+ // ── Step 4: Cache the match result ───────────────────────────────────────
89
127
  if (this.cache && matchResult.capability) {
90
128
  await this.cache.set(query, matchResult);
91
129
  }
92
- // ── Step 4: Resolve ──────────────────────────────────────────────────────
130
+ // ── Step 5: Resolve ──────────────────────────────────────────────────────
131
+ const resolveStart = Date.now();
93
132
  const resolution = await _resolve(matchResult, matchResult.extractedParams, this.resolveOptions(overrides));
94
- // ── Step 5: Record learning ──────────────────────────────────────────────
133
+ steps.push({
134
+ type: 'resolve',
135
+ status: resolution.success ? 'pass' : 'fail',
136
+ durationMs: Date.now() - resolveStart,
137
+ detail: resolution.error ?? `via ${resolution.resolverType}`,
138
+ });
139
+ // ── Step 6: Build reasoning array ────────────────────────────────────────
140
+ const reasoning = [];
141
+ if (matchResult.candidates?.length) {
142
+ const winner = matchResult.candidates.find(c => c.matched);
143
+ const rejected = matchResult.candidates
144
+ .filter(c => !c.matched && c.score > 0)
145
+ .sort((a, b) => b.score - a.score)
146
+ .slice(0, 3);
147
+ if (winner) {
148
+ reasoning.push(`Matched "${winner.capabilityId}" with ${winner.score}% confidence`);
149
+ }
150
+ if (rejected.length) {
151
+ reasoning.push(`Rejected: ${rejected.map(r => `${r.capabilityId} (${r.score}%)`).join(', ')}`);
152
+ }
153
+ reasoning.push(`Resolved via: ${resolvedVia}`);
154
+ if (matchResult.extractedParams && Object.keys(matchResult.extractedParams).length) {
155
+ const params = Object.entries(matchResult.extractedParams)
156
+ .map(([k, v]) => `${k}=${v}`)
157
+ .join(', ');
158
+ reasoning.push(`Extracted params: ${params}`);
159
+ }
160
+ }
161
+ else {
162
+ reasoning.push(matchResult.reasoning);
163
+ }
164
+ // ── Step 7: Record learning ──────────────────────────────────────────────
95
165
  await this.recordLearning(query, matchResult, resolvedVia);
166
+ const trace = {
167
+ query,
168
+ candidates: matchResult.candidates ?? [],
169
+ reasoning,
170
+ steps,
171
+ resolvedVia,
172
+ totalMs: Date.now() - start,
173
+ };
96
174
  return {
97
175
  match: matchResult,
98
176
  resolution,
99
177
  resolvedVia,
100
178
  durationMs: Date.now() - start,
179
+ trace,
101
180
  };
102
181
  }
103
182
  /**
@@ -1,34 +1,74 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import { logger } from './logger';
4
+ // ─── Shared computation helpers ───────────────────────────────────────────────
5
+ function computeStats(entries) {
6
+ const index = {};
7
+ let totalQueries = 0;
8
+ let llmQueries = 0;
9
+ let cacheHits = 0;
10
+ let outOfScope = 0;
11
+ for (const entry of entries) {
12
+ totalQueries++;
13
+ if (entry.resolvedVia === 'llm')
14
+ llmQueries++;
15
+ if (entry.resolvedVia === 'cache')
16
+ cacheHits++;
17
+ if (!entry.capabilityId)
18
+ outOfScope++;
19
+ if (entry.capabilityId) {
20
+ const words = entry.query.toLowerCase()
21
+ .split(/\W+/)
22
+ .filter(w => w.length > 2);
23
+ for (const word of words) {
24
+ if (!index[word])
25
+ index[word] = {};
26
+ index[word][entry.capabilityId] =
27
+ (index[word][entry.capabilityId] ?? 0) + 1;
28
+ }
29
+ }
30
+ }
31
+ return { index, totalQueries, llmQueries, cacheHits, outOfScope };
32
+ }
33
+ function computeTopCapabilities(entries, limit) {
34
+ const counts = {};
35
+ for (const entry of entries) {
36
+ if (entry.capabilityId) {
37
+ counts[entry.capabilityId] = (counts[entry.capabilityId] ?? 0) + 1;
38
+ }
39
+ }
40
+ return Object.entries(counts)
41
+ .sort(([, a], [, b]) => b - a)
42
+ .slice(0, limit)
43
+ .map(([id, hits]) => ({ id, hits }));
44
+ }
4
45
  // ─── File Learning Store ──────────────────────────────────────────────────────
5
46
  export class FileLearningStore {
6
47
  constructor(filePath = '.capman/learning.json') {
7
48
  this.entries = [];
8
49
  this.loaded = false;
9
50
  this.filePath = path.resolve(process.cwd(), filePath);
51
+ logger.info(`FileLearningStore initialized — writing to: ${this.filePath}`);
10
52
  }
11
- load() {
53
+ async load() {
12
54
  if (this.loaded)
13
55
  return;
14
56
  try {
15
- if (fs.existsSync(this.filePath)) {
16
- const raw = JSON.parse(fs.readFileSync(this.filePath, 'utf-8'));
17
- this.entries = raw.entries ?? [];
18
- logger.debug(`Learning store loaded: ${this.entries.length} entries`);
19
- }
57
+ const raw = await fs.promises.readFile(this.filePath, 'utf-8');
58
+ const parsed = JSON.parse(raw);
59
+ this.entries = parsed.entries ?? [];
60
+ logger.debug(`Learning store loaded: ${this.entries.length} entries`);
20
61
  }
21
62
  catch {
22
- logger.warn(`Failed to load learning store at ${this.filePath}`);
63
+ // File doesn't exist yet start fresh
23
64
  }
24
65
  this.loaded = true;
25
66
  }
26
- save() {
67
+ async save() {
27
68
  try {
28
69
  const dir = path.dirname(this.filePath);
29
- if (!fs.existsSync(dir))
30
- fs.mkdirSync(dir, { recursive: true });
31
- fs.writeFileSync(this.filePath, JSON.stringify({
70
+ await fs.promises.mkdir(dir, { recursive: true });
71
+ await fs.promises.writeFile(this.filePath, JSON.stringify({
32
72
  entries: this.entries,
33
73
  updatedAt: new Date().toISOString(),
34
74
  }, null, 2));
@@ -38,57 +78,22 @@ export class FileLearningStore {
38
78
  }
39
79
  }
40
80
  async record(entry) {
41
- this.load();
81
+ await this.load();
42
82
  this.entries.push(entry);
43
- this.save();
83
+ await this.save();
44
84
  logger.debug(`Learning recorded: "${entry.query}" → ${entry.capabilityId ?? 'OUT_OF_SCOPE'} via ${entry.resolvedVia}`);
45
85
  }
46
86
  async getStats() {
47
- this.load();
48
- const index = {};
49
- let totalQueries = 0;
50
- let llmQueries = 0;
51
- let cacheHits = 0;
52
- let outOfScope = 0;
53
- for (const entry of this.entries) {
54
- totalQueries++;
55
- if (entry.resolvedVia === 'llm')
56
- llmQueries++;
57
- if (entry.resolvedVia === 'cache')
58
- cacheHits++;
59
- if (!entry.capabilityId)
60
- outOfScope++;
61
- if (entry.capabilityId) {
62
- // Index each word of the query against the matched capability
63
- const words = entry.query.toLowerCase()
64
- .split(/\W+/)
65
- .filter(w => w.length > 2);
66
- for (const word of words) {
67
- if (!index[word])
68
- index[word] = {};
69
- index[word][entry.capabilityId] =
70
- (index[word][entry.capabilityId] ?? 0) + 1;
71
- }
72
- }
73
- }
74
- return { index, totalQueries, llmQueries, cacheHits, outOfScope };
87
+ await this.load();
88
+ return computeStats(this.entries);
75
89
  }
76
90
  async getTopCapabilities(limit = 5) {
77
- this.load();
78
- const counts = {};
79
- for (const entry of this.entries) {
80
- if (entry.capabilityId) {
81
- counts[entry.capabilityId] = (counts[entry.capabilityId] ?? 0) + 1;
82
- }
83
- }
84
- return Object.entries(counts)
85
- .sort(([, a], [, b]) => b - a)
86
- .slice(0, limit)
87
- .map(([id, hits]) => ({ id, hits }));
91
+ await this.load();
92
+ return computeTopCapabilities(this.entries, limit);
88
93
  }
89
94
  async clear() {
90
95
  this.entries = [];
91
- this.save();
96
+ await this.save();
92
97
  }
93
98
  }
94
99
  // ─── Memory Learning Store (for testing) ─────────────────────────────────────
@@ -100,44 +105,10 @@ export class MemoryLearningStore {
100
105
  this.entries.push(entry);
101
106
  }
102
107
  async getStats() {
103
- const index = {};
104
- let totalQueries = 0;
105
- let llmQueries = 0;
106
- let cacheHits = 0;
107
- let outOfScope = 0;
108
- for (const entry of this.entries) {
109
- totalQueries++;
110
- if (entry.resolvedVia === 'llm')
111
- llmQueries++;
112
- if (entry.resolvedVia === 'cache')
113
- cacheHits++;
114
- if (!entry.capabilityId)
115
- outOfScope++;
116
- if (entry.capabilityId) {
117
- const words = entry.query.toLowerCase()
118
- .split(/\W+/)
119
- .filter(w => w.length > 2);
120
- for (const word of words) {
121
- if (!index[word])
122
- index[word] = {};
123
- index[word][entry.capabilityId] =
124
- (index[word][entry.capabilityId] ?? 0) + 1;
125
- }
126
- }
127
- }
128
- return { index, totalQueries, llmQueries, cacheHits, outOfScope };
108
+ return computeStats(this.entries);
129
109
  }
130
110
  async getTopCapabilities(limit = 5) {
131
- const counts = {};
132
- for (const entry of this.entries) {
133
- if (entry.capabilityId) {
134
- counts[entry.capabilityId] = (counts[entry.capabilityId] ?? 0) + 1;
135
- }
136
- }
137
- return Object.entries(counts)
138
- .sort(([, a], [, b]) => b - a)
139
- .slice(0, limit)
140
- .map(([id, hits]) => ({ id, hits }));
111
+ return computeTopCapabilities(this.entries, limit);
141
112
  }
142
113
  async clear() {
143
114
  this.entries = [];
@@ -140,33 +140,44 @@ export function match(query, manifest) {
140
140
  logger.debug(`Manifest has ${manifest.capabilities.length} capabilities`);
141
141
  let best = null;
142
142
  let bestScore = 0;
143
+ const allScores = [];
143
144
  for (const cap of manifest.capabilities) {
144
145
  const score = scoreCapability(query, cap);
145
146
  logger.debug(` scored "${cap.id}": ${score}%`);
147
+ allScores.push({ cap, score });
146
148
  if (score > bestScore) {
147
149
  bestScore = score;
148
150
  best = cap;
149
151
  }
150
152
  }
153
+ const candidates = allScores.map(({ cap, score }) => ({
154
+ capabilityId: cap.id,
155
+ score,
156
+ matched: cap.id === best?.id,
157
+ }));
151
158
  if (!best || bestScore < 50) {
152
159
  logger.info(`No match above threshold (best: ${bestScore}% for "${best?.id ?? 'none'}")`);
160
+ // Out of scope return:
153
161
  return {
154
162
  capability: null,
155
163
  confidence: bestScore,
156
164
  intent: 'out_of_scope',
157
165
  extractedParams: {},
158
166
  reasoning: `No capability matched with sufficient confidence (best score: ${bestScore})`,
167
+ candidates,
159
168
  };
160
169
  }
161
170
  const params = extractParams(query, best);
162
171
  logger.info(`Matched "${best.id}" at ${bestScore}% confidence`);
163
172
  logger.debug(`Extracted params: ${JSON.stringify(params)}`);
173
+ // Matched return:
164
174
  return {
165
175
  capability: best,
166
176
  confidence: bestScore,
167
177
  intent: resolverToIntent(best),
168
178
  extractedParams: params,
169
179
  reasoning: `Matched "${best.id}" via keyword scoring (score: ${bestScore})`,
180
+ candidates,
170
181
  };
171
182
  }
172
183
  export async function matchWithLLM(query, manifest, options) {
@@ -88,78 +88,76 @@ export async function resolve(matchResult, params = {}, options = {}) {
88
88
  }
89
89
  async function resolveApi(resolver, params, options) {
90
90
  const startTime = Date.now();
91
+ const retries = options.retries ?? 0;
92
+ const timeoutMs = options.timeoutMs ?? 5000;
91
93
  const apiCalls = resolver.endpoints.map(endpoint => ({
92
94
  method: endpoint.method,
93
95
  url: buildUrl(options.baseUrl ?? '', endpoint.path, params),
94
96
  params,
95
97
  }));
96
98
  if (options.dryRun) {
97
- return {
98
- success: true,
99
- resolverType: 'api',
100
- apiCalls,
101
- durationMs: Date.now() - startTime,
102
- };
99
+ return { success: true, resolverType: 'api', apiCalls, durationMs: Date.now() - startTime };
103
100
  }
104
101
  const fetchFn = options.fetch ?? globalThis.fetch;
105
102
  if (!fetchFn) {
106
103
  return {
107
- success: true,
108
- resolverType: 'api',
109
- apiCalls,
104
+ success: true, resolverType: 'api', apiCalls,
110
105
  durationMs: Date.now() - startTime,
111
106
  error: 'No fetch available — returning call plan only',
112
107
  };
113
108
  }
109
+ // ── Fetch with retry + timeout ────────────────────────────────────────────
110
+ async function fetchWithRetry(call, attempt) {
111
+ const controller = new AbortController();
112
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
113
+ try {
114
+ const res = await fetchFn(call.url, {
115
+ method: call.method,
116
+ headers: options.headers ?? {},
117
+ signal: controller.signal,
118
+ body: ['POST', 'PUT', 'PATCH'].includes(call.method)
119
+ ? JSON.stringify(call.params)
120
+ : undefined,
121
+ });
122
+ clearTimeout(timer);
123
+ return res;
124
+ }
125
+ catch (err) {
126
+ clearTimeout(timer);
127
+ const isTimeout = err instanceof Error && err.name === 'AbortError';
128
+ if (attempt < retries) {
129
+ logger.warn(`Request failed (attempt ${attempt + 1}/${retries + 1}) — retrying: ${isTimeout ? 'timeout' : err}`);
130
+ return fetchWithRetry(call, attempt + 1);
131
+ }
132
+ throw isTimeout ? new Error(`Request timed out after ${timeoutMs}ms`) : err;
133
+ }
134
+ }
114
135
  try {
115
- const responses = await Promise.all(apiCalls.map(c => fetchFn(c.url, {
116
- method: c.method,
117
- headers: options.headers ?? {},
118
- body: ['POST', 'PUT', 'PATCH'].includes(c.method)
119
- ? JSON.stringify(c.params)
120
- : undefined,
121
- })));
122
- // Check for HTTP errors
136
+ const responses = await Promise.all(apiCalls.map(c => fetchWithRetry(c, 0)));
123
137
  const failedIdx = responses.findIndex(r => !r.ok);
124
138
  if (failedIdx !== -1) {
125
139
  const failed = responses[failedIdx];
126
140
  return {
127
- success: false,
128
- resolverType: 'api',
129
- apiCalls,
141
+ success: false, resolverType: 'api', apiCalls,
130
142
  durationMs: Date.now() - startTime,
131
143
  error: `API request failed: ${failed.status} ${failed.statusText}`,
132
144
  };
133
145
  }
134
- // Parse response bodies
135
146
  const enrichedCalls = await Promise.all(responses.map(async (res, i) => {
136
147
  let data = undefined;
137
148
  try {
138
149
  const text = await res.text();
139
150
  data = text ? JSON.parse(text) : undefined;
140
151
  }
141
- catch {
142
- // Non-JSON response leave data undefined
143
- }
144
- return {
145
- ...apiCalls[i],
146
- status: res.status,
147
- data,
148
- };
152
+ catch { /* non-JSON response */ }
153
+ return { ...apiCalls[i], status: res.status, data };
149
154
  }));
150
155
  logger.debug(`API calls completed in ${Date.now() - startTime}ms`);
151
- return {
152
- success: true,
153
- resolverType: 'api',
154
- apiCalls: enrichedCalls,
155
- durationMs: Date.now() - startTime,
156
- };
156
+ return { success: true, resolverType: 'api', apiCalls: enrichedCalls, durationMs: Date.now() - startTime };
157
157
  }
158
158
  catch (err) {
159
159
  return {
160
- success: false,
161
- resolverType: 'api',
162
- apiCalls,
160
+ success: false, resolverType: 'api', apiCalls,
163
161
  durationMs: Date.now() - startTime,
164
162
  error: err instanceof Error ? err.message : String(err),
165
163
  };
@@ -1,2 +1,2 @@
1
1
  // Auto-generated by scripts/version.js — do not edit manually
2
- export const VERSION = '0.3.0';
2
+ export const VERSION = '0.4.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capman",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Capability Manifest Engine — let AI agents interact with your app without navigating the UI",
5
5
  "main": "./dist/cjs/index.js",
6
6
  "module": "./dist/esm/index.js",