@dotsetlabs/bellwether 2.1.2 → 2.1.3
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 +18 -0
- package/README.md +2 -2
- package/dist/baseline/golden-output.d.ts +0 -4
- package/dist/baseline/golden-output.js +2 -47
- package/dist/cli/commands/baseline-accept.js +14 -45
- package/dist/cli/commands/baseline.js +23 -78
- package/dist/cli/commands/check-formatters.d.ts +10 -0
- package/dist/cli/commands/check-formatters.js +160 -0
- package/dist/cli/commands/check.js +33 -241
- package/dist/cli/commands/contract.js +1 -13
- package/dist/cli/commands/explore.js +19 -66
- package/dist/cli/commands/watch.js +2 -3
- package/dist/cli/output.d.ts +0 -42
- package/dist/cli/output.js +73 -110
- package/dist/cli/utils/config-loader.d.ts +6 -0
- package/dist/cli/utils/config-loader.js +19 -0
- package/dist/cli/utils/error-hints.d.ts +9 -0
- package/dist/cli/utils/error-hints.js +128 -0
- package/dist/cli/utils/headers.js +2 -25
- package/dist/cli/utils/path-resolution.d.ts +10 -0
- package/dist/cli/utils/path-resolution.js +27 -0
- package/dist/cli/utils/report-loader.d.ts +9 -0
- package/dist/cli/utils/report-loader.js +31 -0
- package/dist/cli/utils/server-runtime.d.ts +16 -0
- package/dist/cli/utils/server-runtime.js +31 -0
- package/dist/config/defaults.d.ts +0 -1
- package/dist/config/defaults.js +0 -1
- package/dist/constants/core.d.ts +0 -42
- package/dist/constants/core.js +0 -50
- package/dist/contract/validator.js +2 -47
- package/dist/interview/question-category.d.ts +5 -0
- package/dist/interview/question-category.js +2 -0
- package/dist/interview/question-types.d.ts +80 -0
- package/dist/interview/question-types.js +2 -0
- package/dist/interview/schema-test-generator.d.ts +3 -29
- package/dist/interview/schema-test-generator.js +11 -286
- package/dist/interview/test-fixtures.d.ts +19 -0
- package/dist/interview/test-fixtures.js +2 -0
- package/dist/interview/types.d.ts +5 -80
- package/dist/persona/types.d.ts +3 -5
- package/dist/scenarios/types.d.ts +1 -1
- package/dist/transport/auth-errors.d.ts +15 -0
- package/dist/transport/auth-errors.js +22 -0
- package/dist/transport/http-transport.js +7 -9
- package/dist/transport/mcp-client.d.ts +0 -4
- package/dist/transport/mcp-client.js +13 -37
- package/dist/transport/sse-transport.d.ts +0 -1
- package/dist/transport/sse-transport.js +13 -28
- package/dist/utils/content-type.d.ts +14 -0
- package/dist/utils/content-type.js +37 -0
- package/dist/utils/http-headers.d.ts +9 -0
- package/dist/utils/http-headers.js +34 -0
- package/dist/utils/smart-truncate.js +2 -23
- package/package.json +2 -2
|
@@ -5,11 +5,14 @@ import type { ResponseSchemaEvolution } from '../baseline/response-schema-tracke
|
|
|
5
5
|
import type { ErrorAnalysisSummary } from '../baseline/error-analyzer.js';
|
|
6
6
|
import type { DocumentationScore } from '../baseline/documentation-scorer.js';
|
|
7
7
|
import type { SemanticInference } from '../validation/semantic-types.js';
|
|
8
|
-
import type { Persona
|
|
8
|
+
import type { Persona } from '../persona/types.js';
|
|
9
|
+
import type { QuestionCategory } from './question-category.js';
|
|
9
10
|
import type { Workflow, WorkflowResult, WorkflowTimeoutConfig } from '../workflow/types.js';
|
|
10
11
|
import type { LoadedScenarios, ScenarioResult } from '../scenarios/types.js';
|
|
11
12
|
import type { ToolResponseCache } from '../cache/response-cache.js';
|
|
12
|
-
import type { TestFixturesConfig } from './
|
|
13
|
+
import type { TestFixturesConfig } from './test-fixtures.js';
|
|
14
|
+
import type { InterviewQuestion, OutcomeAssessment } from './question-types.js';
|
|
15
|
+
export type { ExpectedOutcome, InterviewQuestion, OutcomeAssessment } from './question-types.js';
|
|
13
16
|
/**
|
|
14
17
|
* Server context extracted during discovery/initial probing.
|
|
15
18
|
* Used to generate contextually appropriate test cases.
|
|
@@ -112,84 +115,6 @@ export interface InterviewConfig {
|
|
|
112
115
|
/** Test fixtures for overriding default parameter values */
|
|
113
116
|
testFixtures?: TestFixturesConfig;
|
|
114
117
|
}
|
|
115
|
-
/**
|
|
116
|
-
* Expected outcome for a test question.
|
|
117
|
-
* - 'success': Test expects the tool to execute successfully
|
|
118
|
-
* - 'error': Test expects the tool to reject/fail (validation test)
|
|
119
|
-
* - 'either': Test outcome is acceptable either way
|
|
120
|
-
*/
|
|
121
|
-
export type ExpectedOutcome = 'success' | 'error' | 'either';
|
|
122
|
-
/**
|
|
123
|
-
* A question to ask about a tool's behavior.
|
|
124
|
-
*/
|
|
125
|
-
export interface InterviewQuestion {
|
|
126
|
-
/** Description of what this question tests */
|
|
127
|
-
description: string;
|
|
128
|
-
/** Category of question */
|
|
129
|
-
category: QuestionCategory;
|
|
130
|
-
/** Arguments to pass to the tool */
|
|
131
|
-
args: Record<string, unknown>;
|
|
132
|
-
/**
|
|
133
|
-
* Expected outcome of this test.
|
|
134
|
-
* Used to determine if the tool behaved correctly.
|
|
135
|
-
* - 'success': Expects successful execution (happy path)
|
|
136
|
-
* - 'error': Expects rejection/error (validation test)
|
|
137
|
-
* - 'either': Either outcome is acceptable
|
|
138
|
-
*/
|
|
139
|
-
expectedOutcome?: ExpectedOutcome;
|
|
140
|
-
/** Semantic validation metadata (for tests generated from semantic type inference) */
|
|
141
|
-
metadata?: {
|
|
142
|
-
/** The inferred semantic type being tested */
|
|
143
|
-
semanticType?: string;
|
|
144
|
-
/** Expected behavior: 'reject' for invalid values, 'accept' for valid */
|
|
145
|
-
expectedBehavior?: 'reject' | 'accept';
|
|
146
|
-
/** Confidence level of the semantic type inference (0-1) */
|
|
147
|
-
confidence?: number;
|
|
148
|
-
/** Stateful testing metadata */
|
|
149
|
-
stateful?: {
|
|
150
|
-
/** Keys injected from prior tool outputs */
|
|
151
|
-
usedKeys?: string[];
|
|
152
|
-
/** Keys captured from this response */
|
|
153
|
-
providedKeys?: string[];
|
|
154
|
-
};
|
|
155
|
-
/**
|
|
156
|
-
* Whether this tool uses operation-based dispatch pattern.
|
|
157
|
-
* Tools with this pattern have different required args per operation.
|
|
158
|
-
*/
|
|
159
|
-
operationBased?: boolean;
|
|
160
|
-
/** The parameter name that selects the operation (e.g., "operation", "action") */
|
|
161
|
-
operationParam?: string;
|
|
162
|
-
/** The parameter name that holds operation-specific args (e.g., "args", "params") */
|
|
163
|
-
argsParam?: string;
|
|
164
|
-
/**
|
|
165
|
-
* Whether this tool requires prior state (session, chain, etc.).
|
|
166
|
-
* These tools need an active session before they can work.
|
|
167
|
-
*/
|
|
168
|
-
selfStateful?: boolean;
|
|
169
|
-
/** Reason for self-stateful detection */
|
|
170
|
-
selfStatefulReason?: string;
|
|
171
|
-
/**
|
|
172
|
-
* Whether this tool has complex array schemas requiring structured data.
|
|
173
|
-
* Simple test data generation often fails for these tools.
|
|
174
|
-
*/
|
|
175
|
-
hasComplexArrays?: boolean;
|
|
176
|
-
/** Array parameters with complex item schemas */
|
|
177
|
-
complexArrayParams?: string[];
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
/**
|
|
181
|
-
* Assessment of whether a tool interaction outcome matched expectations.
|
|
182
|
-
*/
|
|
183
|
-
export interface OutcomeAssessment {
|
|
184
|
-
/** What outcome was expected */
|
|
185
|
-
expected: ExpectedOutcome;
|
|
186
|
-
/** What actually happened */
|
|
187
|
-
actual: 'success' | 'error';
|
|
188
|
-
/** Whether the tool behaved correctly (matches expectation) */
|
|
189
|
-
correct: boolean;
|
|
190
|
-
/** True if this was a validation test that correctly rejected invalid input */
|
|
191
|
-
isValidationSuccess?: boolean;
|
|
192
|
-
}
|
|
193
118
|
/**
|
|
194
119
|
* Result of asking a single question.
|
|
195
120
|
*/
|
package/dist/persona/types.d.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Persona types for configurable interviewer personalities.
|
|
3
3
|
*/
|
|
4
|
-
import type { InterviewQuestion } from '../interview/types.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
*/
|
|
8
|
-
export type QuestionCategory = 'happy_path' | 'edge_case' | 'error_handling' | 'boundary' | 'security';
|
|
4
|
+
import type { InterviewQuestion } from '../interview/question-types.js';
|
|
5
|
+
import type { QuestionCategory } from '../interview/question-category.js';
|
|
6
|
+
export type { QuestionCategory } from '../interview/question-category.js';
|
|
9
7
|
/**
|
|
10
8
|
* Weight distribution for question categories.
|
|
11
9
|
* Values should be 0-1 and represent relative likelihood.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* These types define the schema for user-defined test cases
|
|
5
5
|
* that can be provided via bellwether-tests.yaml files.
|
|
6
6
|
*/
|
|
7
|
-
import type { QuestionCategory } from '../
|
|
7
|
+
import type { QuestionCategory } from '../interview/question-category.js';
|
|
8
8
|
/**
|
|
9
9
|
* Valid assertion conditions for scenario expectations.
|
|
10
10
|
*/
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ServerAuthError } from '../errors/types.js';
|
|
2
|
+
interface AuthErrorConfig {
|
|
3
|
+
unauthorizedMessage: string;
|
|
4
|
+
forbiddenMessage: string;
|
|
5
|
+
proxyMessage?: string;
|
|
6
|
+
unauthorizedHint?: string;
|
|
7
|
+
forbiddenHint?: string;
|
|
8
|
+
proxyHint?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Map HTTP auth-related status codes to typed transport auth errors.
|
|
12
|
+
*/
|
|
13
|
+
export declare function createServerAuthError(status: number, config: AuthErrorConfig): ServerAuthError | null;
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=auth-errors.d.ts.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ServerAuthError } from '../errors/types.js';
|
|
2
|
+
const DEFAULT_HINTS = {
|
|
3
|
+
unauthorized: 'Add server.headers.Authorization (for example: Bearer token) in bellwether.yaml or pass --header.',
|
|
4
|
+
forbidden: 'Credentials are recognized but lack required permissions. Verify token scopes/roles.',
|
|
5
|
+
proxy: 'Configure proxy credentials and retry.',
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Map HTTP auth-related status codes to typed transport auth errors.
|
|
9
|
+
*/
|
|
10
|
+
export function createServerAuthError(status, config) {
|
|
11
|
+
if (status === 401) {
|
|
12
|
+
return new ServerAuthError(config.unauthorizedMessage, 401, config.unauthorizedHint ?? DEFAULT_HINTS.unauthorized);
|
|
13
|
+
}
|
|
14
|
+
if (status === 403) {
|
|
15
|
+
return new ServerAuthError(config.forbiddenMessage, 403, config.forbiddenHint ?? DEFAULT_HINTS.forbidden);
|
|
16
|
+
}
|
|
17
|
+
if (status === 407) {
|
|
18
|
+
return new ServerAuthError(config.proxyMessage ?? 'Proxy authentication required (407)', 407, config.proxyHint ?? DEFAULT_HINTS.proxy);
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=auth-errors.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BaseTransport } from './base-transport.js';
|
|
2
2
|
import { TIMEOUTS, DISPLAY_LIMITS, MCP } from '../constants.js';
|
|
3
|
-
import {
|
|
3
|
+
import { createServerAuthError } from './auth-errors.js';
|
|
4
4
|
/**
|
|
5
5
|
* HTTPTransport connects to MCP servers over HTTP using POST requests.
|
|
6
6
|
*
|
|
@@ -112,14 +112,12 @@ export class HTTPTransport extends BaseTransport {
|
|
|
112
112
|
});
|
|
113
113
|
clearTimeout(timeoutId);
|
|
114
114
|
if (!response.ok) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (response.status === 407) {
|
|
122
|
-
throw new ServerAuthError('Proxy authentication required (407)', 407, 'Configure proxy credentials and retry.');
|
|
115
|
+
const authError = createServerAuthError(response.status, {
|
|
116
|
+
unauthorizedMessage: 'Remote MCP server authentication failed (401 Unauthorized)',
|
|
117
|
+
forbiddenMessage: 'Remote MCP server authorization failed (403 Forbidden)',
|
|
118
|
+
});
|
|
119
|
+
if (authError) {
|
|
120
|
+
throw authError;
|
|
123
121
|
}
|
|
124
122
|
// MCP 2025-11-25: 404 means session expired, clear session ID
|
|
125
123
|
if (response.status === 404 && this.sessionId) {
|
|
@@ -60,10 +60,6 @@ export declare class MCPClient {
|
|
|
60
60
|
/** Whether to run an explicit preflight before remote connection */
|
|
61
61
|
private remotePreflight;
|
|
62
62
|
constructor(options?: MCPClientOptions);
|
|
63
|
-
/**
|
|
64
|
-
* Merge two header maps, giving precedence to override values.
|
|
65
|
-
*/
|
|
66
|
-
private mergeHeaders;
|
|
67
63
|
/**
|
|
68
64
|
* Optional remote connectivity/auth preflight (enabled by default).
|
|
69
65
|
* Uses GET (not HEAD/OPTIONS) for broader compatibility.
|
|
@@ -8,6 +8,8 @@ import { VERSION } from '../version.js';
|
|
|
8
8
|
import { getFeatureFlags, isKnownProtocolVersion, } from '../protocol/index.js';
|
|
9
9
|
import { filterSpawnEnv } from './env-filter.js';
|
|
10
10
|
import { ConnectionError, ServerAuthError } from '../errors/types.js';
|
|
11
|
+
import { mergeHeaderMaps } from '../utils/http-headers.js';
|
|
12
|
+
import { createServerAuthError } from './auth-errors.js';
|
|
11
13
|
const DEFAULT_TIMEOUT = TIMEOUTS.DEFAULT;
|
|
12
14
|
const DEFAULT_STARTUP_DELAY = TIMEOUTS.SERVER_STARTUP;
|
|
13
15
|
/**
|
|
@@ -56,31 +58,6 @@ export class MCPClient {
|
|
|
56
58
|
this.httpConfig = options?.httpConfig;
|
|
57
59
|
this.remotePreflight = options?.remotePreflight ?? true;
|
|
58
60
|
}
|
|
59
|
-
/**
|
|
60
|
-
* Merge two header maps, giving precedence to override values.
|
|
61
|
-
*/
|
|
62
|
-
mergeHeaders(base, override) {
|
|
63
|
-
if (!base && !override)
|
|
64
|
-
return undefined;
|
|
65
|
-
const merged = {};
|
|
66
|
-
const apply = (headers) => {
|
|
67
|
-
if (!headers)
|
|
68
|
-
return;
|
|
69
|
-
for (const [name, value] of Object.entries(headers)) {
|
|
70
|
-
const normalized = name.toLowerCase();
|
|
71
|
-
for (const existing of Object.keys(merged)) {
|
|
72
|
-
if (existing.toLowerCase() === normalized) {
|
|
73
|
-
delete merged[existing];
|
|
74
|
-
break;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
merged[name] = value;
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
apply(base);
|
|
81
|
-
apply(override);
|
|
82
|
-
return Object.keys(merged).length > 0 ? merged : undefined;
|
|
83
|
-
}
|
|
84
61
|
/**
|
|
85
62
|
* Optional remote connectivity/auth preflight (enabled by default).
|
|
86
63
|
* Uses GET (not HEAD/OPTIONS) for broader compatibility.
|
|
@@ -112,17 +89,16 @@ export class MCPClient {
|
|
|
112
89
|
// Ignore body cancellation failures; preflight status handling is primary.
|
|
113
90
|
}
|
|
114
91
|
};
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (response.status === 407) {
|
|
92
|
+
const authError = createServerAuthError(response.status, {
|
|
93
|
+
unauthorizedMessage: `Remote MCP preflight failed: unauthorized (${response.status})`,
|
|
94
|
+
forbiddenMessage: `Remote MCP preflight failed: forbidden (${response.status})`,
|
|
95
|
+
proxyMessage: `Remote MCP preflight failed: proxy authentication required (${response.status})`,
|
|
96
|
+
unauthorizedHint: 'Add authentication headers (for example Authorization) and retry.',
|
|
97
|
+
forbiddenHint: 'Credentials are valid but lack required permissions/scopes.',
|
|
98
|
+
});
|
|
99
|
+
if (authError) {
|
|
124
100
|
await closePreflightBody();
|
|
125
|
-
throw
|
|
101
|
+
throw authError;
|
|
126
102
|
}
|
|
127
103
|
if (!response.ok) {
|
|
128
104
|
// Non-auth HTTP statuses are compatibility-safe to continue:
|
|
@@ -388,8 +364,8 @@ export class MCPClient {
|
|
|
388
364
|
};
|
|
389
365
|
this.logger.info({ url, transport }, 'Connecting to remote MCP server');
|
|
390
366
|
const mergedHeaders = transport === 'sse'
|
|
391
|
-
?
|
|
392
|
-
:
|
|
367
|
+
? mergeHeaderMaps(this.sseConfig?.headers, options?.headers)
|
|
368
|
+
: mergeHeaderMaps(this.httpConfig?.headers, options?.headers);
|
|
393
369
|
await this.preflightRemote(url, transport, mergedHeaders);
|
|
394
370
|
if (transport === 'sse') {
|
|
395
371
|
const sseTransport = new SSETransport({
|
|
@@ -30,7 +30,6 @@ export interface SSETransportConfig extends BaseTransportConfig {
|
|
|
30
30
|
*/
|
|
31
31
|
export declare class SSETransport extends BaseTransport {
|
|
32
32
|
private streamAbortController;
|
|
33
|
-
private abortController;
|
|
34
33
|
private connected;
|
|
35
34
|
private reconnectAttempts;
|
|
36
35
|
private readonly baseUrl;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BaseTransport } from './base-transport.js';
|
|
2
2
|
import { TIME_CONSTANTS, TIMEOUTS } from '../constants.js';
|
|
3
3
|
import { isLocalhost } from '../utils/index.js';
|
|
4
|
-
import {
|
|
4
|
+
import { createServerAuthError } from './auth-errors.js';
|
|
5
5
|
/**
|
|
6
6
|
* Validate that a URL uses HTTPS in production contexts.
|
|
7
7
|
* Allows HTTP only for localhost/127.0.0.1 for local development.
|
|
@@ -103,7 +103,6 @@ class SSEParser {
|
|
|
103
103
|
*/
|
|
104
104
|
export class SSETransport extends BaseTransport {
|
|
105
105
|
streamAbortController = null;
|
|
106
|
-
abortController = null;
|
|
107
106
|
connected = false;
|
|
108
107
|
reconnectAttempts = 0;
|
|
109
108
|
baseUrl;
|
|
@@ -162,14 +161,12 @@ export class SSETransport extends BaseTransport {
|
|
|
162
161
|
signal: this.streamAbortController.signal,
|
|
163
162
|
});
|
|
164
163
|
if (!response.ok) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if (response.status === 407) {
|
|
172
|
-
throw new ServerAuthError('Proxy authentication required (407)', 407, 'Configure proxy credentials and retry.');
|
|
164
|
+
const authError = createServerAuthError(response.status, {
|
|
165
|
+
unauthorizedMessage: 'Remote MCP SSE authentication failed (401 Unauthorized)',
|
|
166
|
+
forbiddenMessage: 'Remote MCP SSE authorization failed (403 Forbidden)',
|
|
167
|
+
});
|
|
168
|
+
if (authError) {
|
|
169
|
+
throw authError;
|
|
173
170
|
}
|
|
174
171
|
throw new Error(`Failed to connect to SSE endpoint: HTTP ${response.status}`);
|
|
175
172
|
}
|
|
@@ -344,14 +341,12 @@ export class SSETransport extends BaseTransport {
|
|
|
344
341
|
.then(async (response) => {
|
|
345
342
|
clearTimeout(timeoutId);
|
|
346
343
|
if (!response.ok) {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
if (response.status === 407) {
|
|
354
|
-
throw new ServerAuthError('Proxy authentication required (407)', 407, 'Configure proxy credentials and retry.');
|
|
344
|
+
const authError = createServerAuthError(response.status, {
|
|
345
|
+
unauthorizedMessage: 'Remote MCP message authentication failed (401 Unauthorized)',
|
|
346
|
+
forbiddenMessage: 'Remote MCP message authorization failed (403 Forbidden)',
|
|
347
|
+
});
|
|
348
|
+
if (authError) {
|
|
349
|
+
throw authError;
|
|
355
350
|
}
|
|
356
351
|
const errorText = await response.text().catch(() => 'Unknown error');
|
|
357
352
|
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
|
@@ -404,16 +399,6 @@ export class SSETransport extends BaseTransport {
|
|
|
404
399
|
}
|
|
405
400
|
this.streamAbortController = null;
|
|
406
401
|
}
|
|
407
|
-
// Abort any in-flight HTTP requests
|
|
408
|
-
if (this.abortController) {
|
|
409
|
-
try {
|
|
410
|
-
this.abortController.abort();
|
|
411
|
-
}
|
|
412
|
-
catch {
|
|
413
|
-
// Ignore abort errors
|
|
414
|
-
}
|
|
415
|
-
this.abortController = null;
|
|
416
|
-
}
|
|
417
402
|
this.messageEndpoint = null;
|
|
418
403
|
this.reconnectAttempts = 0;
|
|
419
404
|
this.emit('close');
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type DetectedContentType = 'json' | 'markdown' | 'text';
|
|
2
|
+
export interface DetectContentTypeOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Markdown detection mode:
|
|
5
|
+
* - `strict`: conservative heading/link/fence patterns (legacy contract/golden behavior)
|
|
6
|
+
* - `lenient`: broader markdown indicators (legacy smart-truncate behavior)
|
|
7
|
+
*/
|
|
8
|
+
markdownHeuristics?: 'strict' | 'lenient';
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Detect whether content is JSON, Markdown, or plain text.
|
|
12
|
+
*/
|
|
13
|
+
export declare function detectContentType(content: string, options?: DetectContentTypeOptions): DetectedContentType;
|
|
14
|
+
//# sourceMappingURL=content-type.d.ts.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
function looksLikeMarkdownStrict(trimmed) {
|
|
2
|
+
return /^#|^\*{1,3}[^*]|\[.*\]\(.*\)|^```/.test(trimmed);
|
|
3
|
+
}
|
|
4
|
+
function looksLikeMarkdownLenient(trimmed) {
|
|
5
|
+
if (looksLikeMarkdownStrict(trimmed)) {
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
return (trimmed.includes('\n#') ||
|
|
9
|
+
/^[-*]\s/.test(trimmed) ||
|
|
10
|
+
/^\d+\.\s/.test(trimmed) ||
|
|
11
|
+
trimmed.includes('```') ||
|
|
12
|
+
trimmed.includes('**') ||
|
|
13
|
+
trimmed.includes('__'));
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Detect whether content is JSON, Markdown, or plain text.
|
|
17
|
+
*/
|
|
18
|
+
export function detectContentType(content, options = {}) {
|
|
19
|
+
const mode = options.markdownHeuristics ?? 'strict';
|
|
20
|
+
const trimmed = content.trim();
|
|
21
|
+
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) ||
|
|
22
|
+
(trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
|
23
|
+
try {
|
|
24
|
+
JSON.parse(trimmed);
|
|
25
|
+
return 'json';
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// Not valid JSON
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const isMarkdown = mode === 'lenient' ? looksLikeMarkdownLenient(trimmed) : looksLikeMarkdownStrict(trimmed);
|
|
32
|
+
if (isMarkdown) {
|
|
33
|
+
return 'markdown';
|
|
34
|
+
}
|
|
35
|
+
return 'text';
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=content-type.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge HTTP header maps case-insensitively, preserving latest key casing.
|
|
3
|
+
*/
|
|
4
|
+
export declare function mergeHeaderMaps(base?: Record<string, string>, override?: Record<string, string>): Record<string, string> | undefined;
|
|
5
|
+
/**
|
|
6
|
+
* Set a header while treating header names as case-insensitive.
|
|
7
|
+
*/
|
|
8
|
+
export declare function setHeaderCaseInsensitive(headers: Record<string, string>, name: string, value: string): void;
|
|
9
|
+
//# sourceMappingURL=http-headers.d.ts.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge HTTP header maps case-insensitively, preserving latest key casing.
|
|
3
|
+
*/
|
|
4
|
+
export function mergeHeaderMaps(base, override) {
|
|
5
|
+
if (!base && !override) {
|
|
6
|
+
return undefined;
|
|
7
|
+
}
|
|
8
|
+
const merged = {};
|
|
9
|
+
if (base) {
|
|
10
|
+
for (const [name, value] of Object.entries(base)) {
|
|
11
|
+
setHeaderCaseInsensitive(merged, name, value);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
if (override) {
|
|
15
|
+
for (const [name, value] of Object.entries(override)) {
|
|
16
|
+
setHeaderCaseInsensitive(merged, name, value);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return Object.keys(merged).length > 0 ? merged : undefined;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Set a header while treating header names as case-insensitive.
|
|
23
|
+
*/
|
|
24
|
+
export function setHeaderCaseInsensitive(headers, name, value) {
|
|
25
|
+
const normalized = name.toLowerCase();
|
|
26
|
+
for (const existing of Object.keys(headers)) {
|
|
27
|
+
if (existing.toLowerCase() === normalized) {
|
|
28
|
+
delete headers[existing];
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
headers[name] = value;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=http-headers.js.map
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* - Provide helpful truncation indicators
|
|
12
12
|
*/
|
|
13
13
|
import { EXAMPLE_OUTPUT } from '../constants.js';
|
|
14
|
+
import { detectContentType as detectGeneralContentType } from './content-type.js';
|
|
14
15
|
// ==================== Main Functions ====================
|
|
15
16
|
/**
|
|
16
17
|
* Smart truncate content based on type.
|
|
@@ -321,29 +322,7 @@ function truncateAtSentence(text, maxLength) {
|
|
|
321
322
|
* @returns Detected content type
|
|
322
323
|
*/
|
|
323
324
|
export function detectContentType(content) {
|
|
324
|
-
|
|
325
|
-
// Check for JSON
|
|
326
|
-
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) ||
|
|
327
|
-
(trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
|
328
|
-
try {
|
|
329
|
-
JSON.parse(trimmed);
|
|
330
|
-
return 'json';
|
|
331
|
-
}
|
|
332
|
-
catch {
|
|
333
|
-
// Not valid JSON
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
// Check for Markdown indicators
|
|
337
|
-
if (trimmed.includes('\n#') ||
|
|
338
|
-
trimmed.startsWith('#') ||
|
|
339
|
-
/^[-*]\s/.test(trimmed) ||
|
|
340
|
-
/^\d+\.\s/.test(trimmed) ||
|
|
341
|
-
trimmed.includes('```') ||
|
|
342
|
-
trimmed.includes('**') ||
|
|
343
|
-
trimmed.includes('__')) {
|
|
344
|
-
return 'markdown';
|
|
345
|
-
}
|
|
346
|
-
return 'text';
|
|
325
|
+
return detectGeneralContentType(content, { markdownHeuristics: 'lenient' });
|
|
347
326
|
}
|
|
348
327
|
/**
|
|
349
328
|
* Get the appropriate example length based on options.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotsetlabs/bellwether",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.3",
|
|
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",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"format:check": "prettier --check \"src/**/*.ts\"",
|
|
35
35
|
"check:consistency": "node ./scripts/validate-consistency.mjs",
|
|
36
36
|
"clean": "rm -rf dist",
|
|
37
|
-
"docs:generate": "
|
|
37
|
+
"docs:generate": "node ./scripts/build-docs.mjs",
|
|
38
38
|
"docs:dev": "npm --prefix website run start",
|
|
39
39
|
"prepublishOnly": "npm run build"
|
|
40
40
|
},
|