@dotsetlabs/bellwether 2.0.0 → 2.1.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.
Files changed (75) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +9 -0
  3. package/dist/auth/credentials.js +2 -0
  4. package/dist/baseline/accessors.js +12 -0
  5. package/dist/baseline/baseline-format.d.ts +48 -0
  6. package/dist/baseline/comparator.js +263 -20
  7. package/dist/baseline/converter.js +52 -4
  8. package/dist/baseline/dependency-analyzer.js +46 -25
  9. package/dist/baseline/diff.js +51 -39
  10. package/dist/baseline/documentation-scorer.d.ts +1 -1
  11. package/dist/baseline/documentation-scorer.js +4 -4
  12. package/dist/baseline/error-analyzer.js +1 -1
  13. package/dist/baseline/external-dependency-detector.js +16 -7
  14. package/dist/baseline/performance-tracker.js +2 -2
  15. package/dist/baseline/response-fingerprint.js +1 -1
  16. package/dist/baseline/response-schema-tracker.js +17 -22
  17. package/dist/baseline/saver.js +34 -0
  18. package/dist/baseline/types.d.ts +21 -1
  19. package/dist/cache/response-cache.js +9 -2
  20. package/dist/cli/commands/auth.js +15 -18
  21. package/dist/cli/commands/baseline-accept.js +1 -1
  22. package/dist/cli/commands/baseline.js +71 -36
  23. package/dist/cli/commands/check.js +54 -14
  24. package/dist/cli/commands/discover.js +2 -2
  25. package/dist/cli/commands/explore.js +38 -5
  26. package/dist/cli/commands/golden.js +20 -23
  27. package/dist/cli/commands/init.js +10 -7
  28. package/dist/cli/commands/registry.js +37 -35
  29. package/dist/cli/commands/watch.js +5 -5
  30. package/dist/cli/output/terminal-reporter.js +9 -9
  31. package/dist/cli/output.d.ts +1 -1
  32. package/dist/cli/output.js +9 -11
  33. package/dist/config/loader.js +2 -2
  34. package/dist/config/validator.d.ts +33 -33
  35. package/dist/constants/core.d.ts +4 -8
  36. package/dist/constants/core.js +4 -8
  37. package/dist/constants/testing.d.ts +11 -11
  38. package/dist/constants/testing.js +11 -11
  39. package/dist/contract/validator.js +7 -7
  40. package/dist/discovery/discovery.js +88 -14
  41. package/dist/discovery/types.d.ts +5 -1
  42. package/dist/docs/agents.js +145 -57
  43. package/dist/docs/contract.js +136 -40
  44. package/dist/errors/retry.js +11 -5
  45. package/dist/interview/dependency-resolver.d.ts +3 -2
  46. package/dist/interview/dependency-resolver.js +31 -2
  47. package/dist/interview/interviewer.js +10 -2
  48. package/dist/interview/rate-limiter.js +7 -3
  49. package/dist/interview/stateful-test-runner.d.ts +1 -0
  50. package/dist/interview/stateful-test-runner.js +4 -0
  51. package/dist/interview/types.d.ts +3 -0
  52. package/dist/llm/anthropic.js +14 -4
  53. package/dist/llm/fallback.d.ts +1 -0
  54. package/dist/llm/fallback.js +7 -1
  55. package/dist/llm/openai.js +15 -4
  56. package/dist/prompts/templates.js +30 -15
  57. package/dist/protocol/index.d.ts +2 -0
  58. package/dist/protocol/index.js +2 -0
  59. package/dist/protocol/version-registry.d.ts +66 -0
  60. package/dist/protocol/version-registry.js +159 -0
  61. package/dist/scenarios/evaluator.js +9 -10
  62. package/dist/transport/http-transport.d.ts +11 -1
  63. package/dist/transport/http-transport.js +21 -2
  64. package/dist/transport/mcp-client.d.ts +29 -1
  65. package/dist/transport/mcp-client.js +92 -7
  66. package/dist/transport/sse-transport.js +5 -4
  67. package/dist/transport/types.d.ts +134 -1
  68. package/dist/utils/concurrency.d.ts +2 -0
  69. package/dist/utils/concurrency.js +9 -2
  70. package/dist/utils/markdown.js +13 -18
  71. package/dist/utils/timeout.js +2 -1
  72. package/dist/version.js +1 -1
  73. package/man/bellwether.1 +1 -1
  74. package/man/bellwether.1.md +2 -2
  75. package/package.json +2 -1
@@ -5,6 +5,7 @@ import { HTTPTransport } from './http-transport.js';
5
5
  import { getLogger, startTiming } from '../logging/logger.js';
6
6
  import { TIMEOUTS, MCP, TRANSPORT_ERRORS } from '../constants.js';
7
7
  import { VERSION } from '../version.js';
8
+ import { getFeatureFlags, isKnownProtocolVersion, } from '../protocol/index.js';
8
9
  /**
9
10
  * Environment variables to filter out when spawning MCP server processes.
10
11
  * These may contain sensitive credentials that should not be exposed.
@@ -75,6 +76,7 @@ export class MCPClient {
75
76
  requestId = 0;
76
77
  pendingRequests = new Map();
77
78
  serverCapabilities = null;
79
+ serverInstructions;
78
80
  timeout;
79
81
  startupDelay;
80
82
  serverReady = false;
@@ -92,6 +94,8 @@ export class MCPClient {
92
94
  transportErrors = [];
93
95
  /** Connection state for diagnostic error messages */
94
96
  connectionState = { attempted: false };
97
+ /** Protocol version negotiated with the server during initialization */
98
+ negotiatedProtocolVersion = null;
95
99
  constructor(options) {
96
100
  this.timeout = options?.timeout ?? DEFAULT_TIMEOUT;
97
101
  this.startupDelay = options?.startupDelay ?? DEFAULT_STARTUP_DELAY;
@@ -444,9 +448,19 @@ export class MCPClient {
444
448
  },
445
449
  });
446
450
  this.serverCapabilities = result.capabilities;
451
+ this.serverInstructions = result.instructions;
452
+ this.negotiatedProtocolVersion = result.protocolVersion;
453
+ // Warn if server returned an unknown protocol version
454
+ if (result.protocolVersion && !isKnownProtocolVersion(result.protocolVersion)) {
455
+ this.logger.warn({ protocolVersion: result.protocolVersion }, 'Server returned unknown protocol version');
456
+ }
457
+ // Update HTTP transport with negotiated version for subsequent requests
458
+ if (this.transport instanceof HTTPTransport && result.protocolVersion) {
459
+ this.transport.setNegotiatedVersion(result.protocolVersion);
460
+ }
447
461
  // Send initialized notification
448
462
  this.sendNotification('notifications/initialized', {});
449
- this.logger.info({ capabilities: result.capabilities }, 'MCP server initialized successfully');
463
+ this.logger.info({ capabilities: result.capabilities, protocolVersion: result.protocolVersion }, 'MCP server initialized successfully');
450
464
  return result;
451
465
  }
452
466
  catch (error) {
@@ -477,24 +491,71 @@ export class MCPClient {
477
491
  }
478
492
  /**
479
493
  * List all tools available on the server.
494
+ * Handles pagination via nextCursor per MCP 2025-11-25 spec.
480
495
  */
481
496
  async listTools(options) {
482
- const result = await this.sendRequest('tools/list', {}, options);
483
- return result.tools;
497
+ const allTools = [];
498
+ let cursor;
499
+ do {
500
+ const params = {};
501
+ if (cursor)
502
+ params.cursor = cursor;
503
+ const result = await this.sendRequest('tools/list', params, options);
504
+ allTools.push(...result.tools);
505
+ cursor = result.nextCursor;
506
+ } while (cursor);
507
+ return allTools;
484
508
  }
485
509
  /**
486
510
  * List all prompts available on the server.
511
+ * Handles pagination via nextCursor per MCP 2025-11-25 spec.
487
512
  */
488
513
  async listPrompts(options) {
489
- const result = await this.sendRequest('prompts/list', {}, options);
490
- return result.prompts;
514
+ const allPrompts = [];
515
+ let cursor;
516
+ do {
517
+ const params = {};
518
+ if (cursor)
519
+ params.cursor = cursor;
520
+ const result = await this.sendRequest('prompts/list', params, options);
521
+ allPrompts.push(...result.prompts);
522
+ cursor = result.nextCursor;
523
+ } while (cursor);
524
+ return allPrompts;
491
525
  }
492
526
  /**
493
527
  * List all resources available on the server.
528
+ * Handles pagination via nextCursor per MCP 2025-11-25 spec.
494
529
  */
495
530
  async listResources(options) {
496
- const result = await this.sendRequest('resources/list', {}, options);
497
- return result.resources;
531
+ const allResources = [];
532
+ let cursor;
533
+ do {
534
+ const params = {};
535
+ if (cursor)
536
+ params.cursor = cursor;
537
+ const result = await this.sendRequest('resources/list', params, options);
538
+ allResources.push(...result.resources);
539
+ cursor = result.nextCursor;
540
+ } while (cursor);
541
+ return allResources;
542
+ }
543
+ /**
544
+ * List all resource templates available on the server.
545
+ * Handles pagination via nextCursor per MCP 2025-11-25 spec.
546
+ */
547
+ async listResourceTemplates(options) {
548
+ const allTemplates = [];
549
+ let cursor;
550
+ do {
551
+ const params = {};
552
+ if (cursor)
553
+ params.cursor = cursor;
554
+ const result = await this.sendRequest('resources/templates/list', params, options);
555
+ allTemplates.push(...result.resourceTemplates);
556
+ cursor = result.nextCursor;
557
+ } while (cursor);
558
+ return allTemplates;
498
559
  }
499
560
  /**
500
561
  * Read a resource from the server by URI.
@@ -555,6 +616,28 @@ export class MCPClient {
555
616
  getCapabilities() {
556
617
  return this.serverCapabilities;
557
618
  }
619
+ /**
620
+ * Get server instructions from initialization.
621
+ */
622
+ getInstructions() {
623
+ return this.serverInstructions;
624
+ }
625
+ /**
626
+ * Get the protocol version negotiated with the server during initialization.
627
+ * Returns null if initialization hasn't completed yet.
628
+ */
629
+ getNegotiatedProtocolVersion() {
630
+ return this.negotiatedProtocolVersion;
631
+ }
632
+ /**
633
+ * Get feature flags based on the negotiated protocol version.
634
+ * Returns null if initialization hasn't completed yet.
635
+ */
636
+ getFeatureFlags() {
637
+ if (!this.negotiatedProtocolVersion)
638
+ return null;
639
+ return getFeatureFlags(this.negotiatedProtocolVersion);
640
+ }
558
641
  /**
559
642
  * Check if the server is ready (startup delay complete and initialized).
560
643
  * Note: This only indicates if startup delay has passed - true readiness
@@ -764,6 +847,8 @@ export class MCPClient {
764
847
  this.transport = null;
765
848
  this.process = null;
766
849
  this.serverCapabilities = null;
850
+ this.serverInstructions = undefined;
851
+ this.negotiatedProtocolVersion = null;
767
852
  this.serverReady = false;
768
853
  }
769
854
  }
@@ -316,10 +316,11 @@ export class SSETransport extends BaseTransport {
316
316
  // Use the endpoint provided by the server, or default to /message
317
317
  const endpoint = this.messageEndpoint || `${this.baseUrl}/message`;
318
318
  this.log('Sending message', { endpoint, message });
319
- // Create a new abort controller for this request
320
- this.abortController = new AbortController();
319
+ // Create a local abort controller for this request to avoid overwriting
320
+ // the instance controller and leaking previous controllers
321
+ const requestController = new AbortController();
321
322
  const timeoutId = setTimeout(() => {
322
- this.abortController?.abort();
323
+ requestController.abort();
323
324
  }, this.timeout);
324
325
  fetch(endpoint, {
325
326
  method: 'POST',
@@ -328,7 +329,7 @@ export class SSETransport extends BaseTransport {
328
329
  ...this.headers,
329
330
  },
330
331
  body: JSON.stringify(message),
331
- signal: this.abortController.signal,
332
+ signal: requestController.signal,
332
333
  })
333
334
  .then(async (response) => {
334
335
  clearTimeout(timeoutId);
@@ -25,41 +25,122 @@ export interface JSONRPCNotification {
25
25
  }
26
26
  export type JSONRPCMessage = JSONRPCRequest | JSONRPCResponse | JSONRPCNotification;
27
27
  /**
28
- * MCP Protocol types
28
+ * MCP Protocol types (aligned with MCP specification 2025-11-25)
29
29
  */
30
+ /**
31
+ * Icon for MCP entities (tools, resources, prompts, server).
32
+ */
33
+ export interface MCPIcon {
34
+ /** URI or data URI for the icon */
35
+ src: string;
36
+ /** MIME type of the icon */
37
+ mimeType?: string;
38
+ /** Supported sizes (e.g., ['16x16', '32x32']) */
39
+ sizes?: string[];
40
+ /** Theme hint for the icon */
41
+ theme?: 'light' | 'dark';
42
+ }
43
+ /**
44
+ * Annotations for content blocks and resources.
45
+ * Provides metadata about intended audience, priority, and freshness.
46
+ */
47
+ export interface MCPAnnotations {
48
+ /** Intended audience for the content */
49
+ audience?: ('user' | 'assistant')[];
50
+ /** Priority hint (0.0 = lowest, 1.0 = highest) */
51
+ priority?: number;
52
+ /** ISO 8601 timestamp of when the content was last modified */
53
+ lastModified?: string;
54
+ }
55
+ /**
56
+ * Behavioral annotations for tools.
57
+ * Provides hints about tool behavior to help clients make decisions.
58
+ */
59
+ export interface MCPToolAnnotations {
60
+ /** Human-readable title for the annotation group */
61
+ title?: string;
62
+ /** Whether the tool only reads data and does not modify state */
63
+ readOnlyHint?: boolean;
64
+ /** Whether the tool may perform destructive operations */
65
+ destructiveHint?: boolean;
66
+ /** Whether calling the tool multiple times with the same args has the same effect */
67
+ idempotentHint?: boolean;
68
+ /** Whether the tool interacts with entities outside the server's controlled environment */
69
+ openWorldHint?: boolean;
70
+ }
30
71
  export interface MCPTool {
31
72
  name: string;
32
73
  description?: string;
33
74
  inputSchema?: Record<string, unknown>;
75
+ /** Human-readable title for the tool */
76
+ title?: string;
77
+ /** Icons for the tool */
78
+ icons?: MCPIcon[];
79
+ /** JSON Schema for the tool's output (structured content) */
80
+ outputSchema?: Record<string, unknown>;
81
+ /** Behavioral annotations/hints */
82
+ annotations?: MCPToolAnnotations;
83
+ /** Task execution configuration */
84
+ execution?: {
85
+ taskSupport?: 'forbidden' | 'optional' | 'required';
86
+ };
87
+ /** Extension metadata */
88
+ _meta?: Record<string, unknown>;
34
89
  }
35
90
  export interface MCPPrompt {
36
91
  name: string;
37
92
  description?: string;
38
93
  arguments?: MCPPromptArgument[];
94
+ /** Human-readable title for the prompt */
95
+ title?: string;
96
+ /** Icons for the prompt */
97
+ icons?: MCPIcon[];
98
+ /** Extension metadata */
99
+ _meta?: Record<string, unknown>;
39
100
  }
40
101
  export interface MCPPromptArgument {
41
102
  name: string;
42
103
  description?: string;
43
104
  required?: boolean;
105
+ /** Human-readable title for the argument */
106
+ title?: string;
44
107
  }
45
108
  export interface MCPServerCapabilities {
46
109
  tools?: Record<string, unknown>;
47
110
  prompts?: Record<string, unknown>;
48
111
  resources?: Record<string, unknown>;
49
112
  logging?: Record<string, unknown>;
113
+ /** Server supports completions (autocomplete) */
114
+ completions?: Record<string, unknown>;
115
+ /** Server supports task management */
116
+ tasks?: Record<string, unknown>;
117
+ /** Experimental/vendor-specific capabilities */
118
+ experimental?: Record<string, unknown>;
50
119
  }
51
120
  export interface MCPServerInfo {
52
121
  name: string;
53
122
  version: string;
123
+ /** Human-readable title for the server */
124
+ title?: string;
125
+ /** Description of the server */
126
+ description?: string;
127
+ /** Icons for the server */
128
+ icons?: MCPIcon[];
129
+ /** Website URL for the server */
130
+ websiteUrl?: string;
54
131
  }
55
132
  export interface MCPInitializeResult {
56
133
  protocolVersion: string;
57
134
  capabilities: MCPServerCapabilities;
58
135
  serverInfo: MCPServerInfo;
136
+ /** Server-provided instructions for the client */
137
+ instructions?: string;
59
138
  }
60
139
  export interface MCPToolCallResult {
61
140
  content: MCPContentBlock[];
62
141
  isError?: boolean;
142
+ /** Structured output content (validated against tool's outputSchema) */
143
+ structuredContent?: Record<string, unknown>;
63
144
  }
64
145
  /**
65
146
  * MCP content block types per MCP specification (2025-11-25).
@@ -75,12 +156,26 @@ export interface MCPContentBlock {
75
156
  mimeType?: string;
76
157
  /** URI reference (for type: 'resource_link') */
77
158
  uri?: string;
159
+ /** Content/resource annotations */
160
+ annotations?: MCPAnnotations;
161
+ /** Extension metadata */
162
+ _meta?: Record<string, unknown>;
163
+ /** Embedded resource content (for type: 'resource') */
164
+ resource?: MCPResourceContent;
165
+ /** Resource name (for type: 'resource_link') */
166
+ name?: string;
167
+ /** Resource description (for type: 'resource_link') */
168
+ description?: string;
78
169
  }
79
170
  export interface MCPToolsListResult {
80
171
  tools: MCPTool[];
172
+ /** Cursor for pagination */
173
+ nextCursor?: string;
81
174
  }
82
175
  export interface MCPPromptsListResult {
83
176
  prompts: MCPPrompt[];
177
+ /** Cursor for pagination */
178
+ nextCursor?: string;
84
179
  }
85
180
  /**
86
181
  * MCP Resource types
@@ -94,9 +189,21 @@ export interface MCPResource {
94
189
  description?: string;
95
190
  /** MIME type of the resource content */
96
191
  mimeType?: string;
192
+ /** Human-readable title for the resource */
193
+ title?: string;
194
+ /** Icons for the resource */
195
+ icons?: MCPIcon[];
196
+ /** Resource annotations */
197
+ annotations?: MCPAnnotations;
198
+ /** Size of the resource in bytes */
199
+ size?: number;
200
+ /** Extension metadata */
201
+ _meta?: Record<string, unknown>;
97
202
  }
98
203
  export interface MCPResourcesListResult {
99
204
  resources: MCPResource[];
205
+ /** Cursor for pagination */
206
+ nextCursor?: string;
100
207
  }
101
208
  export interface MCPResourceReadResult {
102
209
  contents: MCPResourceContent[];
@@ -111,6 +218,32 @@ export interface MCPResourceContent {
111
218
  /** Binary content as base64 (for binary resources) */
112
219
  blob?: string;
113
220
  }
221
+ /**
222
+ * MCP Resource Template for URI-templated resources.
223
+ */
224
+ export interface MCPResourceTemplate {
225
+ /** URI template (RFC 6570) */
226
+ uriTemplate: string;
227
+ /** Human-readable name */
228
+ name: string;
229
+ /** Human-readable title */
230
+ title?: string;
231
+ /** Description of the resource template */
232
+ description?: string;
233
+ /** Expected MIME type of resources matching this template */
234
+ mimeType?: string;
235
+ /** Icons for the resource template */
236
+ icons?: MCPIcon[];
237
+ /** Resource annotations */
238
+ annotations?: MCPAnnotations;
239
+ /** Extension metadata */
240
+ _meta?: Record<string, unknown>;
241
+ }
242
+ export interface MCPResourceTemplatesListResult {
243
+ resourceTemplates: MCPResourceTemplate[];
244
+ /** Cursor for pagination */
245
+ nextCursor?: string;
246
+ }
114
247
  export interface MCPPromptMessage {
115
248
  role: 'user' | 'assistant';
116
249
  content: MCPPromptContent;
@@ -7,6 +7,8 @@
7
7
  export interface ParallelOptions<T> {
8
8
  /** Maximum concurrent tasks (default: 3) */
9
9
  concurrency?: number;
10
+ /** Timeout per task in milliseconds (default: no timeout) */
11
+ taskTimeoutMs?: number;
10
12
  /** Callback when a task completes */
11
13
  onTaskComplete?: (result: T, index: number) => void;
12
14
  /** Callback when a task fails */
@@ -10,7 +10,7 @@
10
10
  * @returns Results of all tasks
11
11
  */
12
12
  export async function parallelLimit(tasks, options = {}) {
13
- const { concurrency = 3, onTaskComplete, onTaskError } = options;
13
+ const { concurrency = 3, taskTimeoutMs, onTaskComplete, onTaskError } = options;
14
14
  const results = new Array(tasks.length);
15
15
  const errors = new Map();
16
16
  let running = 0;
@@ -30,7 +30,14 @@ export async function parallelLimit(tasks, options = {}) {
30
30
  while (running < concurrency && index < tasks.length) {
31
31
  const currentIndex = index++;
32
32
  running++;
33
- tasks[currentIndex]()
33
+ const taskPromise = tasks[currentIndex]();
34
+ const wrappedPromise = taskTimeoutMs
35
+ ? Promise.race([
36
+ taskPromise,
37
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`Task ${currentIndex} timed out after ${taskTimeoutMs}ms`)), taskTimeoutMs)),
38
+ ])
39
+ : taskPromise;
40
+ wrappedPromise
34
41
  .then((result) => {
35
42
  results[currentIndex] = result;
36
43
  onTaskComplete?.(result, currentIndex);
@@ -15,13 +15,13 @@ import { DISPLAY_LIMITS } from '../constants.js';
15
15
  export function escapeTableCell(text) {
16
16
  if (!text)
17
17
  return '';
18
- return text
18
+ return (text
19
19
  // Escape pipe characters (break table columns)
20
20
  .replace(/\|/g, '\\|')
21
21
  // Escape newlines (break table rows)
22
22
  .replace(/\r?\n/g, '<br>')
23
23
  // Escape leading/trailing whitespace that might affect rendering
24
- .trim();
24
+ .trim());
25
25
  }
26
26
  /**
27
27
  * Escape a string for use inside a code block.
@@ -47,7 +47,7 @@ export function escapeCodeBlock(text) {
47
47
  export function escapeMermaid(text) {
48
48
  if (!text)
49
49
  return '';
50
- return text
50
+ return (text
51
51
  // Escape double quotes (break Mermaid string literals)
52
52
  .replace(/"/g, '#quot;')
53
53
  // Escape square brackets (node syntax)
@@ -64,7 +64,7 @@ export function escapeMermaid(text) {
64
64
  .replace(/->/g, '#rarr;')
65
65
  .replace(/\|/g, '#pipe;')
66
66
  // Escape newlines
67
- .replace(/\r?\n/g, ' ');
67
+ .replace(/\r?\n/g, ' '));
68
68
  }
69
69
  /**
70
70
  * Escape a string for use as a Mermaid node label.
@@ -81,10 +81,7 @@ export function mermaidLabel(text) {
81
81
  return text;
82
82
  }
83
83
  // Escape and wrap in quotes for complex text
84
- const escaped = text
85
- .replace(/"/g, "'")
86
- .replace(/\r?\n/g, ' ')
87
- .trim();
84
+ const escaped = text.replace(/"/g, "'").replace(/\r?\n/g, ' ').trim();
88
85
  return `"${escaped}"`;
89
86
  }
90
87
  /**
@@ -104,9 +101,7 @@ export function validateJsonForCodeBlock(json, options = {}) {
104
101
  if (typeof json === 'string') {
105
102
  try {
106
103
  const parsed = JSON.parse(json);
107
- content = prettyPrint
108
- ? JSON.stringify(parsed, null, indent)
109
- : JSON.stringify(parsed);
104
+ content = prettyPrint ? JSON.stringify(parsed, null, indent) : JSON.stringify(parsed);
110
105
  }
111
106
  catch (e) {
112
107
  valid = false;
@@ -116,9 +111,7 @@ export function validateJsonForCodeBlock(json, options = {}) {
116
111
  }
117
112
  else {
118
113
  try {
119
- content = prettyPrint
120
- ? JSON.stringify(json, null, indent)
121
- : JSON.stringify(json);
114
+ content = prettyPrint ? JSON.stringify(json, null, indent) : JSON.stringify(json);
122
115
  }
123
116
  catch (e) {
124
117
  valid = false;
@@ -164,12 +157,12 @@ export function escapeInlineCode(text) {
164
157
  export function escapeLinkTitle(text) {
165
158
  if (!text)
166
159
  return '';
167
- return text
160
+ return (text
168
161
  // Escape quotes
169
162
  .replace(/"/g, '\\"')
170
163
  // Escape parentheses
171
164
  .replace(/\)/g, '\\)')
172
- .replace(/\(/g, '\\(');
165
+ .replace(/\(/g, '\\('));
173
166
  }
174
167
  /**
175
168
  * Escape text for use in a Markdown bullet list item.
@@ -236,7 +229,7 @@ export function wrapTableCell(text, maxWidth = DISPLAY_LIMITS.TABLE_CELL_MAX_WID
236
229
  export function buildTable(headers, rows, alignments) {
237
230
  const lines = [];
238
231
  // Header row
239
- const escapedHeaders = headers.map(h => escapeTableCell(h));
232
+ const escapedHeaders = headers.map((h) => escapeTableCell(h));
240
233
  lines.push(`| ${escapedHeaders.join(' | ')} |`);
241
234
  // Separator row with alignment
242
235
  const separators = headers.map((_, i) => {
@@ -253,11 +246,13 @@ export function buildTable(headers, rows, alignments) {
253
246
  lines.push(`| ${separators.join(' | ')} |`);
254
247
  // Data rows
255
248
  for (const row of rows) {
256
- const escapedCells = row.map(cell => escapeTableCell(cell));
249
+ const escapedCells = row.map((cell) => escapeTableCell(cell));
257
250
  // Pad row if needed
258
251
  while (escapedCells.length < headers.length) {
259
252
  escapedCells.push('');
260
253
  }
254
+ // Truncate excess columns
255
+ escapedCells.length = headers.length;
261
256
  lines.push(`| ${escapedCells.join(' | ')} |`);
262
257
  }
263
258
  return lines.join('\n');
@@ -95,7 +95,8 @@ export async function withTimeout(promise, timeoutMs, operationName, options) {
95
95
  return await Promise.race([promise, timeoutPromise]);
96
96
  }
97
97
  finally {
98
- clearTimeout(timeoutId);
98
+ if (timeoutId)
99
+ clearTimeout(timeoutId);
99
100
  }
100
101
  }
101
102
  /**
package/dist/version.js CHANGED
@@ -30,7 +30,7 @@ function getPackageVersion() {
30
30
  }
31
31
  catch {
32
32
  // Fallback version - should match package.json
33
- return '2.0.0';
33
+ return '2.1.0';
34
34
  }
35
35
  }
36
36
  /**
package/man/bellwether.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH "BELLWETHER" "1" "2026\-02\-04" "Bellwether 2.0.0" "User Commands"
1
+ .TH "BELLWETHER" "1" "2026\-02\-11" "Bellwether 2.1.0" "User Commands"
2
2
  .SH NAME
3
3
  .PP
4
4
  bellwether \[em] MCP server testing and validation tool
@@ -2,8 +2,8 @@
2
2
  title: BELLWETHER
3
3
  section: 1
4
4
  header: User Commands
5
- footer: Bellwether 2.0.0
6
- date: 2026-02-04
5
+ footer: Bellwether 2.1.0
6
+ date: 2026-02-11
7
7
  ---
8
8
 
9
9
  # NAME
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotsetlabs/bellwether",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "The open-source MCP testing tool. Structural drift detection and behavioral documentation for Model Context Protocol servers.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -95,6 +95,7 @@
95
95
  "@types/node": "^25.0.9",
96
96
  "@typescript-eslint/eslint-plugin": "^6.21.0",
97
97
  "@typescript-eslint/parser": "^6.21.0",
98
+ "@vitest/coverage-v8": "^4.0.18",
98
99
  "eslint": "^8.57.1",
99
100
  "husky": "^9.1.0",
100
101
  "lint-staged": "^16.2.7",