@dotsetlabs/bellwether 1.0.3 → 2.0.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/CHANGELOG.md +74 -0
- package/README.md +8 -2
- package/dist/baseline/accessors.d.ts +1 -1
- package/dist/baseline/accessors.js +1 -3
- package/dist/baseline/baseline-format.d.ts +287 -0
- package/dist/baseline/baseline-format.js +12 -0
- package/dist/baseline/comparator.js +249 -11
- package/dist/baseline/converter.d.ts +15 -15
- package/dist/baseline/converter.js +46 -34
- package/dist/baseline/diff.d.ts +1 -1
- package/dist/baseline/diff.js +45 -28
- package/dist/baseline/error-analyzer.d.ts +1 -1
- package/dist/baseline/error-analyzer.js +90 -17
- package/dist/baseline/incremental-checker.js +8 -5
- package/dist/baseline/index.d.ts +2 -12
- package/dist/baseline/index.js +3 -23
- package/dist/baseline/performance-tracker.d.ts +0 -1
- package/dist/baseline/performance-tracker.js +13 -20
- package/dist/baseline/response-fingerprint.js +39 -2
- package/dist/baseline/saver.js +41 -10
- package/dist/baseline/schema-compare.d.ts +22 -0
- package/dist/baseline/schema-compare.js +259 -16
- package/dist/baseline/types.d.ts +10 -7
- package/dist/cache/response-cache.d.ts +8 -0
- package/dist/cache/response-cache.js +110 -0
- package/dist/cli/commands/check.js +23 -6
- package/dist/cli/commands/explore.js +34 -14
- package/dist/cli/index.js +8 -0
- package/dist/config/template.js +8 -7
- package/dist/config/validator.d.ts +59 -59
- package/dist/config/validator.js +245 -90
- package/dist/constants/core.d.ts +4 -0
- package/dist/constants/core.js +8 -19
- package/dist/constants/registry.d.ts +17 -0
- package/dist/constants/registry.js +18 -0
- package/dist/constants/testing.d.ts +0 -369
- package/dist/constants/testing.js +18 -456
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/docs/contract.js +131 -83
- package/dist/docs/report.js +8 -5
- package/dist/interview/insights.d.ts +17 -0
- package/dist/interview/insights.js +52 -0
- package/dist/interview/interviewer.js +52 -10
- package/dist/interview/prompt-test-generator.d.ts +12 -0
- package/dist/interview/prompt-test-generator.js +77 -0
- package/dist/interview/resource-test-generator.d.ts +12 -0
- package/dist/interview/resource-test-generator.js +20 -0
- package/dist/interview/schema-inferrer.js +26 -4
- package/dist/interview/schema-test-generator.js +278 -31
- package/dist/interview/stateful-test-runner.d.ts +3 -0
- package/dist/interview/stateful-test-runner.js +80 -0
- package/dist/interview/types.d.ts +12 -0
- package/dist/transport/mcp-client.js +1 -1
- package/dist/transport/sse-transport.d.ts +7 -3
- package/dist/transport/sse-transport.js +157 -67
- package/dist/version.js +1 -1
- package/man/bellwether.1 +1 -1
- package/man/bellwether.1.md +2 -2
- package/package.json +1 -1
- package/schemas/bellwether-check.schema.json +185 -0
- package/schemas/bellwether-explore.schema.json +837 -0
- package/scripts/completions/bellwether.bash +10 -4
- package/scripts/completions/bellwether.zsh +55 -2
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* Enables reuse of tool call results and LLM analysis across personas.
|
|
4
4
|
*/
|
|
5
5
|
import { createHash } from 'crypto';
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
6
8
|
import { getLogger } from '../logging/logger.js';
|
|
7
9
|
import { TIME_CONSTANTS, CACHE } from '../constants.js';
|
|
8
10
|
const logger = getLogger('response-cache');
|
|
@@ -18,13 +20,19 @@ export class ResponseCache {
|
|
|
18
20
|
evictions: 0,
|
|
19
21
|
};
|
|
20
22
|
totalSizeBytes = 0;
|
|
23
|
+
cacheDir;
|
|
21
24
|
constructor(config = {}) {
|
|
22
25
|
this.config = {
|
|
23
26
|
defaultTTLMs: config.defaultTTLMs ?? TIME_CONSTANTS.DEFAULT_CACHE_TTL,
|
|
24
27
|
maxEntries: config.maxEntries ?? CACHE.MAX_ENTRIES,
|
|
25
28
|
maxSizeBytes: config.maxSizeBytes ?? 50 * 1024 * 1024, // 50MB
|
|
26
29
|
enabled: config.enabled ?? true,
|
|
30
|
+
dir: config.dir ?? '',
|
|
27
31
|
};
|
|
32
|
+
this.cacheDir = this.config.enabled ? this.config.dir || undefined : undefined;
|
|
33
|
+
if (this.cacheDir) {
|
|
34
|
+
this.ensureCacheDir(this.cacheDir);
|
|
35
|
+
}
|
|
28
36
|
}
|
|
29
37
|
/**
|
|
30
38
|
* Generate a cache key from input data.
|
|
@@ -43,6 +51,13 @@ export class ResponseCache {
|
|
|
43
51
|
}
|
|
44
52
|
const entry = this.cache.get(key);
|
|
45
53
|
if (!entry) {
|
|
54
|
+
const diskEntry = this.loadFromDisk(key);
|
|
55
|
+
if (diskEntry) {
|
|
56
|
+
this.cache.set(key, diskEntry);
|
|
57
|
+
this.totalSizeBytes += this.estimateSize(diskEntry.value);
|
|
58
|
+
this.stats.hits++;
|
|
59
|
+
return diskEntry.value;
|
|
60
|
+
}
|
|
46
61
|
this.stats.misses++;
|
|
47
62
|
return undefined;
|
|
48
63
|
}
|
|
@@ -89,6 +104,7 @@ export class ResponseCache {
|
|
|
89
104
|
this.totalSizeBytes += entrySize;
|
|
90
105
|
this.cache.set(key, entry);
|
|
91
106
|
logger.debug({ key, ttlMs: ttl, description: options?.description }, 'Cache entry set');
|
|
107
|
+
this.saveToDisk(entry);
|
|
92
108
|
}
|
|
93
109
|
/**
|
|
94
110
|
* Check if key exists and is not expired.
|
|
@@ -115,8 +131,10 @@ export class ResponseCache {
|
|
|
115
131
|
if (entry) {
|
|
116
132
|
this.totalSizeBytes -= this.estimateSize(entry.value);
|
|
117
133
|
this.cache.delete(key);
|
|
134
|
+
this.deleteFromDisk(key);
|
|
118
135
|
return true;
|
|
119
136
|
}
|
|
137
|
+
this.deleteFromDisk(key);
|
|
120
138
|
return false;
|
|
121
139
|
}
|
|
122
140
|
/**
|
|
@@ -125,6 +143,16 @@ export class ResponseCache {
|
|
|
125
143
|
clear() {
|
|
126
144
|
this.cache.clear();
|
|
127
145
|
this.totalSizeBytes = 0;
|
|
146
|
+
if (this.cacheDir && existsSync(this.cacheDir)) {
|
|
147
|
+
try {
|
|
148
|
+
for (const file of listCacheFiles(this.cacheDir)) {
|
|
149
|
+
unlinkSync(file);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// Ignore disk cleanup errors
|
|
154
|
+
}
|
|
155
|
+
}
|
|
128
156
|
logger.debug('Cache cleared');
|
|
129
157
|
}
|
|
130
158
|
/**
|
|
@@ -195,6 +223,88 @@ export class ResponseCache {
|
|
|
195
223
|
return 1000; // Default estimate for non-serializable values
|
|
196
224
|
}
|
|
197
225
|
}
|
|
226
|
+
ensureCacheDir(dir) {
|
|
227
|
+
try {
|
|
228
|
+
if (!existsSync(dir)) {
|
|
229
|
+
mkdirSync(dir, { recursive: true });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
logger.warn({ dir, error: String(error) }, 'Failed to create cache directory');
|
|
234
|
+
this.cacheDir = undefined;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
getCachePath(key) {
|
|
238
|
+
if (!this.cacheDir)
|
|
239
|
+
return null;
|
|
240
|
+
return join(this.cacheDir, `${key}.json`);
|
|
241
|
+
}
|
|
242
|
+
saveToDisk(entry) {
|
|
243
|
+
const path = this.getCachePath(entry.key);
|
|
244
|
+
if (!path)
|
|
245
|
+
return;
|
|
246
|
+
try {
|
|
247
|
+
const serialized = JSON.stringify({
|
|
248
|
+
...entry,
|
|
249
|
+
createdAt: entry.createdAt.toISOString(),
|
|
250
|
+
lastAccessedAt: entry.lastAccessedAt.toISOString(),
|
|
251
|
+
expiresAt: entry.expiresAt.toISOString(),
|
|
252
|
+
});
|
|
253
|
+
writeFileSync(path, serialized, 'utf-8');
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
logger.debug({ key: entry.key, error: String(error) }, 'Failed to persist cache entry');
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
loadFromDisk(key) {
|
|
260
|
+
const path = this.getCachePath(key);
|
|
261
|
+
if (!path || !existsSync(path))
|
|
262
|
+
return null;
|
|
263
|
+
try {
|
|
264
|
+
const raw = readFileSync(path, 'utf-8');
|
|
265
|
+
const parsed = JSON.parse(raw);
|
|
266
|
+
const entry = {
|
|
267
|
+
...parsed,
|
|
268
|
+
createdAt: new Date(parsed.createdAt),
|
|
269
|
+
lastAccessedAt: new Date(parsed.lastAccessedAt),
|
|
270
|
+
expiresAt: new Date(parsed.expiresAt),
|
|
271
|
+
};
|
|
272
|
+
if (new Date() > entry.expiresAt) {
|
|
273
|
+
this.deleteFromDisk(key);
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
entry.hitCount = (entry.hitCount ?? 0) + 1;
|
|
277
|
+
entry.lastAccessedAt = new Date();
|
|
278
|
+
this.saveToDisk(entry);
|
|
279
|
+
return entry;
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
logger.debug({ key, error: String(error) }, 'Failed to load cache entry');
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
deleteFromDisk(key) {
|
|
287
|
+
const path = this.getCachePath(key);
|
|
288
|
+
if (!path || !existsSync(path))
|
|
289
|
+
return;
|
|
290
|
+
try {
|
|
291
|
+
unlinkSync(path);
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
// Ignore delete errors
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
function listCacheFiles(dir) {
|
|
299
|
+
try {
|
|
300
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
301
|
+
return entries
|
|
302
|
+
.filter((entry) => entry.isFile())
|
|
303
|
+
.map((entry) => join(dir, entry.name));
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
return [];
|
|
307
|
+
}
|
|
198
308
|
}
|
|
199
309
|
/**
|
|
200
310
|
* Stable, deterministic JSON stringify with deep key sorting.
|
|
@@ -26,6 +26,7 @@ import { loadWorkflowsFromFile, tryLoadDefaultWorkflows, DEFAULT_WORKFLOWS_FILE,
|
|
|
26
26
|
import * as output from '../output.js';
|
|
27
27
|
import { extractServerContextFromArgs } from '../utils/server-context.js';
|
|
28
28
|
import { configureLogger } from '../../logging/logger.js';
|
|
29
|
+
import { buildInterviewInsights } from '../../interview/insights.js';
|
|
29
30
|
import { EXIT_CODES, SEVERITY_TO_EXIT_CODE, PATHS, SECURITY_TESTING, CHECK_SAMPLING, WORKFLOW, REPORT_SCHEMAS, PERCENTAGE_CONVERSION, } from '../../constants.js';
|
|
30
31
|
export const checkCommand = new Command('check')
|
|
31
32
|
.description('Check MCP server schema and detect drift (free, fast, deterministic)')
|
|
@@ -166,7 +167,7 @@ export const checkCommand = new Command('check')
|
|
|
166
167
|
metricsCollector.startInterview();
|
|
167
168
|
// Initialize cache
|
|
168
169
|
resetGlobalCache();
|
|
169
|
-
const cache = getGlobalCache({ enabled: cacheEnabled });
|
|
170
|
+
const cache = getGlobalCache({ enabled: cacheEnabled, dir: config.cache.dir });
|
|
170
171
|
if (cacheEnabled && verbose) {
|
|
171
172
|
output.info('Response caching enabled');
|
|
172
173
|
}
|
|
@@ -382,6 +383,8 @@ export const checkCommand = new Command('check')
|
|
|
382
383
|
};
|
|
383
384
|
output.info('Checking schemas...\n');
|
|
384
385
|
const result = await interviewer.interview(mcpClient, discovery, progressCallback);
|
|
386
|
+
const insights = buildInterviewInsights(result);
|
|
387
|
+
const enrichedResult = { ...result, ...insights };
|
|
385
388
|
progressBar.stop();
|
|
386
389
|
if (!verbose) {
|
|
387
390
|
output.newline();
|
|
@@ -448,7 +451,7 @@ export const checkCommand = new Command('check')
|
|
|
448
451
|
output.info(`Rate-limited tools: ${rateLimit.tools.slice(0, 5).join(', ')}${rateLimit.tools.length > 5 ? ' ...' : ''}`);
|
|
449
452
|
}
|
|
450
453
|
}
|
|
451
|
-
const checkSummary = buildCheckSummary(
|
|
454
|
+
const checkSummary = buildCheckSummary(enrichedResult);
|
|
452
455
|
output.newline();
|
|
453
456
|
output.lines(...checkSummary.lines);
|
|
454
457
|
if (checkSummary.nextSteps.length > 0) {
|
|
@@ -629,12 +632,25 @@ export const checkCommand = new Command('check')
|
|
|
629
632
|
}
|
|
630
633
|
// Generate documentation (after security testing so findings can be included)
|
|
631
634
|
output.info('Generating documentation...');
|
|
632
|
-
const writeDocs = outputFormat === 'both' || outputFormat === '
|
|
635
|
+
const writeDocs = outputFormat === 'both' || outputFormat === 'docs';
|
|
633
636
|
const writeJson = outputFormat === 'both' || outputFormat === 'json';
|
|
634
637
|
if (writeDocs) {
|
|
635
|
-
const
|
|
638
|
+
const semanticMap = insights.semanticInferences
|
|
639
|
+
? new Map(Object.entries(insights.semanticInferences))
|
|
640
|
+
: undefined;
|
|
641
|
+
const schemaEvolutionMap = insights.schemaEvolution
|
|
642
|
+
? new Map(Object.entries(insights.schemaEvolution))
|
|
643
|
+
: undefined;
|
|
644
|
+
const errorAnalysisMap = insights.errorAnalysisSummaries
|
|
645
|
+
? new Map(Object.entries(insights.errorAnalysisSummaries))
|
|
646
|
+
: undefined;
|
|
647
|
+
const contractMd = generateContractMd(enrichedResult, {
|
|
636
648
|
securityFingerprints: securityEnabled ? securityFingerprints : undefined,
|
|
637
649
|
workflowResults: workflowResults.length > 0 ? workflowResults : undefined,
|
|
650
|
+
semanticInferences: semanticMap,
|
|
651
|
+
schemaEvolution: schemaEvolutionMap,
|
|
652
|
+
errorAnalysisSummaries: errorAnalysisMap,
|
|
653
|
+
documentationScore: insights.documentationScore,
|
|
638
654
|
exampleLength,
|
|
639
655
|
fullExamples,
|
|
640
656
|
maxExamplesPerTool,
|
|
@@ -648,11 +664,12 @@ export const checkCommand = new Command('check')
|
|
|
648
664
|
}
|
|
649
665
|
if (writeJson) {
|
|
650
666
|
// Add workflow results to the result object for the JSON report
|
|
651
|
-
const resultWithWorkflows = workflowResults.length > 0 ? { ...
|
|
667
|
+
const resultWithWorkflows = workflowResults.length > 0 ? { ...enrichedResult, workflowResults } : enrichedResult;
|
|
652
668
|
let jsonReport;
|
|
653
669
|
try {
|
|
654
670
|
jsonReport = generateJsonReport(resultWithWorkflows, {
|
|
655
671
|
schemaUrl: REPORT_SCHEMAS.CHECK_REPORT_SCHEMA_URL,
|
|
672
|
+
schemaPath: REPORT_SCHEMAS.CHECK_REPORT_SCHEMA_FILE,
|
|
656
673
|
validate: true,
|
|
657
674
|
});
|
|
658
675
|
}
|
|
@@ -665,7 +682,7 @@ export const checkCommand = new Command('check')
|
|
|
665
682
|
output.info(`Written: ${jsonPath}`);
|
|
666
683
|
}
|
|
667
684
|
// Create baseline from results
|
|
668
|
-
let currentBaseline = createBaseline(
|
|
685
|
+
let currentBaseline = createBaseline(enrichedResult, fullServerCommand);
|
|
669
686
|
// Attach security fingerprints to tool fingerprints if security testing was run
|
|
670
687
|
if (securityEnabled && securityFingerprints.size > 0) {
|
|
671
688
|
currentBaseline = {
|
|
@@ -13,22 +13,23 @@ import { MCPClient } from '../../transport/mcp-client.js';
|
|
|
13
13
|
import { discover } from '../../discovery/discovery.js';
|
|
14
14
|
import { Interviewer } from '../../interview/interviewer.js';
|
|
15
15
|
import { generateAgentsMd, generateJsonReport } from '../../docs/generator.js';
|
|
16
|
-
import { loadConfig, ConfigNotFoundError, parseCommandString } from '../../config/loader.js';
|
|
16
|
+
import { loadConfig, ConfigNotFoundError, parseCommandString, } from '../../config/loader.js';
|
|
17
17
|
import { validateConfigForExplore } from '../../config/validator.js';
|
|
18
18
|
import { CostTracker, estimateInterviewCost, estimateInterviewTime, formatCostAndTimeEstimate, suggestOptimizations, formatOptimizationSuggestions, } from '../../cost/index.js';
|
|
19
19
|
import { getMetricsCollector, resetMetricsCollector } from '../../metrics/collector.js';
|
|
20
|
-
import { EXIT_CODES, WORKFLOW, PATHS } from '../../constants.js';
|
|
20
|
+
import { EXIT_CODES, WORKFLOW, PATHS, REPORT_SCHEMAS } from '../../constants.js';
|
|
21
21
|
import { FallbackLLMClient } from '../../llm/fallback.js';
|
|
22
22
|
import { getGlobalCache, resetGlobalCache } from '../../cache/response-cache.js';
|
|
23
23
|
import { InterviewProgressBar, formatExploreBanner } from '../utils/progress.js';
|
|
24
24
|
import { parsePersonas } from '../../persona/builtins.js';
|
|
25
|
-
import { loadScenariosFromFile, tryLoadDefaultScenarios, DEFAULT_SCENARIOS_FILE } from '../../scenarios/index.js';
|
|
26
|
-
import { loadWorkflowsFromFile, tryLoadDefaultWorkflows, DEFAULT_WORKFLOWS_FILE } from '../../workflow/loader.js';
|
|
25
|
+
import { loadScenariosFromFile, tryLoadDefaultScenarios, DEFAULT_SCENARIOS_FILE, } from '../../scenarios/index.js';
|
|
26
|
+
import { loadWorkflowsFromFile, tryLoadDefaultWorkflows, DEFAULT_WORKFLOWS_FILE, } from '../../workflow/loader.js';
|
|
27
27
|
import * as output from '../output.js';
|
|
28
28
|
import { StreamingDisplay } from '../output.js';
|
|
29
|
-
import { suppressLogs, restoreLogLevel, configureLogger } from '../../logging/logger.js';
|
|
29
|
+
import { suppressLogs, restoreLogLevel, configureLogger, } from '../../logging/logger.js';
|
|
30
30
|
import { extractServerContextFromArgs } from '../utils/server-context.js';
|
|
31
31
|
import { isCI } from '../utils/env.js';
|
|
32
|
+
import { buildInterviewInsights } from '../../interview/insights.js';
|
|
32
33
|
/**
|
|
33
34
|
* Wrapper to parse personas with warning output.
|
|
34
35
|
*/
|
|
@@ -120,7 +121,7 @@ export const exploreCommand = new Command('explore')
|
|
|
120
121
|
metricsCollector.startInterview();
|
|
121
122
|
// Initialize cache
|
|
122
123
|
resetGlobalCache();
|
|
123
|
-
const cache = getGlobalCache({ enabled: cacheEnabled });
|
|
124
|
+
const cache = getGlobalCache({ enabled: cacheEnabled, dir: config.cache.dir });
|
|
124
125
|
if (cacheEnabled && verbose) {
|
|
125
126
|
output.info('Response caching enabled');
|
|
126
127
|
}
|
|
@@ -138,7 +139,13 @@ export const exploreCommand = new Command('explore')
|
|
|
138
139
|
};
|
|
139
140
|
try {
|
|
140
141
|
llmClient = new FallbackLLMClient({
|
|
141
|
-
providers: [
|
|
142
|
+
providers: [
|
|
143
|
+
{
|
|
144
|
+
provider,
|
|
145
|
+
model,
|
|
146
|
+
baseUrl: provider === 'ollama' ? config.llm.ollama.baseUrl : undefined,
|
|
147
|
+
},
|
|
148
|
+
],
|
|
142
149
|
useOllamaFallback: true,
|
|
143
150
|
onUsage: onUsageCallback,
|
|
144
151
|
});
|
|
@@ -166,9 +173,12 @@ export const exploreCommand = new Command('explore')
|
|
|
166
173
|
}
|
|
167
174
|
// Discovery phase
|
|
168
175
|
output.info('Discovering capabilities...');
|
|
169
|
-
const discovery = await discover(mcpClient, transport === 'stdio' ? serverCommand : remoteUrl ?? serverCommand, transport === 'stdio' ? args : []);
|
|
176
|
+
const discovery = await discover(mcpClient, transport === 'stdio' ? serverCommand : (remoteUrl ?? serverCommand), transport === 'stdio' ? args : []);
|
|
170
177
|
const resourceCount = discovery.resources?.length ?? 0;
|
|
171
|
-
const discoveryParts = [
|
|
178
|
+
const discoveryParts = [
|
|
179
|
+
`${discovery.tools.length} tools`,
|
|
180
|
+
`${discovery.prompts.length} prompts`,
|
|
181
|
+
];
|
|
172
182
|
if (resourceCount > 0) {
|
|
173
183
|
discoveryParts.push(`${resourceCount} resources`);
|
|
174
184
|
}
|
|
@@ -277,13 +287,17 @@ export const exploreCommand = new Command('explore')
|
|
|
277
287
|
let prefix = '';
|
|
278
288
|
switch (opType) {
|
|
279
289
|
case 'generate-questions':
|
|
280
|
-
prefix = context
|
|
290
|
+
prefix = context
|
|
291
|
+
? `\n Generating questions for ${context}... `
|
|
292
|
+
: '\n Generating questions... ';
|
|
281
293
|
break;
|
|
282
294
|
case 'analyze':
|
|
283
295
|
prefix = context ? `\n Analyzing ${context}... ` : '\n Analyzing... ';
|
|
284
296
|
break;
|
|
285
297
|
case 'synthesize-tool':
|
|
286
|
-
prefix = context
|
|
298
|
+
prefix = context
|
|
299
|
+
? `\n Synthesizing profile for ${context}... `
|
|
300
|
+
: '\n Synthesizing profile... ';
|
|
287
301
|
break;
|
|
288
302
|
case 'synthesize-overall':
|
|
289
303
|
prefix = '\n Synthesizing overall findings... ';
|
|
@@ -365,6 +379,8 @@ export const exploreCommand = new Command('explore')
|
|
|
365
379
|
};
|
|
366
380
|
output.info('Starting exploration...\n');
|
|
367
381
|
const result = await interviewer.interview(mcpClient, discovery, progressCallback);
|
|
382
|
+
const insights = buildInterviewInsights(result);
|
|
383
|
+
const enrichedResult = { ...result, ...insights };
|
|
368
384
|
progressBar.stop();
|
|
369
385
|
if (!verbose) {
|
|
370
386
|
output.newline();
|
|
@@ -375,16 +391,20 @@ export const exploreCommand = new Command('explore')
|
|
|
375
391
|
if (docsDir !== outputDir) {
|
|
376
392
|
mkdirSync(docsDir, { recursive: true });
|
|
377
393
|
}
|
|
378
|
-
const writeDocs = outputFormat === 'both' || outputFormat === '
|
|
394
|
+
const writeDocs = outputFormat === 'both' || outputFormat === 'docs';
|
|
379
395
|
const writeJson = outputFormat === 'both' || outputFormat === 'json';
|
|
380
396
|
if (writeDocs) {
|
|
381
|
-
const agentsMd = generateAgentsMd(
|
|
397
|
+
const agentsMd = generateAgentsMd(enrichedResult);
|
|
382
398
|
const agentsMdPath = join(docsDir, config.output.files.agentsDoc);
|
|
383
399
|
writeFileSync(agentsMdPath, agentsMd);
|
|
384
400
|
output.info(`Written: ${agentsMdPath}`);
|
|
385
401
|
}
|
|
386
402
|
if (writeJson) {
|
|
387
|
-
const jsonReport = generateJsonReport(
|
|
403
|
+
const jsonReport = generateJsonReport(enrichedResult, {
|
|
404
|
+
schemaUrl: REPORT_SCHEMAS.EXPLORE_REPORT_SCHEMA_URL,
|
|
405
|
+
schemaPath: REPORT_SCHEMAS.EXPLORE_REPORT_SCHEMA_FILE,
|
|
406
|
+
validate: true,
|
|
407
|
+
});
|
|
388
408
|
const jsonPath = join(outputDir, config.output.files.exploreReport);
|
|
389
409
|
writeFileSync(jsonPath, jsonReport);
|
|
390
410
|
output.info(`Written: ${jsonPath}`);
|
package/dist/cli/index.js
CHANGED
|
@@ -115,6 +115,14 @@ Check MCP servers for drift. Explore behavior. Generate documentation.
|
|
|
115
115
|
Commands:
|
|
116
116
|
check - Schema validation and drift detection (free, fast, deterministic)
|
|
117
117
|
explore - LLM-powered behavioral exploration and documentation
|
|
118
|
+
discover - Quick capability discovery (no tests)
|
|
119
|
+
registry - Search the MCP Registry
|
|
120
|
+
baseline - Manage baselines (save/compare/accept/diff/show)
|
|
121
|
+
golden - Golden output regression testing
|
|
122
|
+
contract - Contract validation (generate/validate/show)
|
|
123
|
+
watch - Continuous checking on file changes
|
|
124
|
+
auth - Manage LLM provider API keys
|
|
125
|
+
validate-config - Validate bellwether.yaml without running tests
|
|
118
126
|
|
|
119
127
|
For more information on a specific command, use:
|
|
120
128
|
bellwether <command> --help`)
|
package/dist/config/template.js
CHANGED
|
@@ -23,13 +23,11 @@ export function generateConfigTemplate(options = {}) {
|
|
|
23
23
|
// Override security.enabled if preset specifies it
|
|
24
24
|
const securityEnabledValue = securityEnabled ? 'true' : String(defaults.check.security.enabled);
|
|
25
25
|
const serverArgsYaml = serverArgs.length > 0
|
|
26
|
-
? `\n args:\n${serverArgs.map(arg => ` - "${arg}"`).join('\n')}`
|
|
26
|
+
? `\n args:\n${serverArgs.map((arg) => ` - "${arg}"`).join('\n')}`
|
|
27
27
|
: '\n args: []';
|
|
28
28
|
const presetComment = preset ? `# Generated with: bellwether init --preset ${preset}\n` : '';
|
|
29
29
|
// Generate env section if env vars were detected
|
|
30
|
-
const envVarsYaml = envVars.length > 0
|
|
31
|
-
? `\n env:\n${envVars.map(v => ` ${v}: "\${${v}}"`).join('\n')}`
|
|
32
|
-
: '';
|
|
30
|
+
const envVarsYaml = envVars.length > 0 ? `\n env:\n${envVars.map((v) => ` ${v}: "\${${v}}"`).join('\n')}` : '';
|
|
33
31
|
return `# Bellwether Configuration
|
|
34
32
|
# Generated by: bellwether init
|
|
35
33
|
# Docs: https://docs.bellwether.sh/guides/configuration
|
|
@@ -61,10 +59,12 @@ server:
|
|
|
61
59
|
timeout: ${defaults.server.timeout}
|
|
62
60
|
|
|
63
61
|
# Additional environment variables for the server process
|
|
64
|
-
# Use \${VAR} syntax to reference environment variables${envVarsYaml}${envVars.length === 0
|
|
62
|
+
# Use \${VAR} syntax to reference environment variables${envVarsYaml}${envVars.length === 0
|
|
63
|
+
? `
|
|
65
64
|
# env:
|
|
66
65
|
# NODE_ENV: production
|
|
67
|
-
# API_KEY: "\${API_KEY}"`
|
|
66
|
+
# API_KEY: "\${API_KEY}"`
|
|
67
|
+
: ''}
|
|
68
68
|
|
|
69
69
|
# =============================================================================
|
|
70
70
|
# SCENARIOS (used by both commands)
|
|
@@ -91,7 +91,8 @@ output:
|
|
|
91
91
|
# These are kept in root by default for visibility
|
|
92
92
|
docsDir: "${defaults.output.docsDir}"
|
|
93
93
|
|
|
94
|
-
# Output format:
|
|
94
|
+
# Output format: docs (markdown only), json (JSON only), or both
|
|
95
|
+
# Legacy alias: agents.md (treated as docs)
|
|
95
96
|
format: ${defaults.output.format}
|
|
96
97
|
|
|
97
98
|
# Example output settings (for CONTRACT.md and AGENTS.md)
|