Compare commits

...
2 Commits
9 changed files with 68 additions and 1 deletions

No files matched your search

+46
View File
@@ -285,6 +285,52 @@ test("parseClaudeResult: success maps text/cost/tokens/model", () => {
expect(r.structuredOutputFailure).toBe(false);
});
test("parseClaudeResult: records thinking tokens as reasoning, without inflating output", () => {
const stdout = JSON.stringify({
type: "result",
subtype: "success",
is_error: false,
result: "the findings",
total_cost_usd: 0.5,
usage: {
input_tokens: 10,
output_tokens: 200,
output_tokens_details: { thinking_tokens: 170 },
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0,
},
});
const r = parseClaudeResult(stdout);
expect(r.tokens.reasoning).toBe(170);
expect(r.tokens.output).toBe(200);
});
test("parseClaudeResult: omits reasoning when the CLI reports no breakdown", () => {
const stdout = JSON.stringify({
type: "result",
subtype: "success",
is_error: false,
result: "the findings",
total_cost_usd: 0.5,
usage: { input_tokens: 10, output_tokens: 200 },
});
expect(parseClaudeResult(stdout).tokens.reasoning).toBeUndefined();
});
test("parseClaudeResult: survives a null/!object output_tokens_details", () => {
for (const details of [null, "nope", 7]) {
const stdout = JSON.stringify({
type: "result",
subtype: "success",
is_error: false,
result: "x",
total_cost_usd: 0,
usage: { input_tokens: 1, output_tokens: 2, output_tokens_details: details },
});
expect(parseClaudeResult(stdout).tokens.reasoning).toBeUndefined();
}
});
test("parseClaudeResult: reads the final result from stream-json JSONL", () => {
const stdout = [
JSON.stringify({ type: "system", subtype: "init", model: "claude-opus-5" }),
+1
View File
@@ -39,6 +39,7 @@ test("stdio MCP advertises the read-only documentation tools and their enforced
);
expect(searchTool?.description ?? "").toContain("Native source retains platform context");
expect(searchTool?.description ?? "").toContain("react-native-worklets=Worklets");
expect(searchTool?.description ?? "").toContain("metro=Metro bundler");
expect(fetchTool?.annotations?.readOnlyHint).toBe(true);
expect(fetchTool?.description ?? "").toMatch(/every redirect/);
const inputSchema = searchTool?.inputSchema as {
@@ -7,6 +7,7 @@ import {
expoProvider,
glideProvider,
jetbrainsIssuesProvider,
metroProvider,
okHttpProvider,
reactNativeGestureHandlerProvider,
reactNativeProvider,
@@ -180,6 +181,7 @@ test("React Native ecosystem providers stay on their exact documentation hosts a
reactNativeWorkletsProvider,
"https://docs.swmansion.com/react-native-worklets/docs/fundamentals/getting-started/",
],
[metroProvider, "https://metrobundler.dev/docs/configuration"],
] as const) {
expect(resolveAllowedUrl(provider, url).hostname).toBe(new URL(url).hostname);
}
@@ -193,4 +195,7 @@ test("React Native ecosystem providers stay on their exact documentation hosts a
expect(() => resolveAllowedUrl(expoProvider, "https://expo.dev/accounts")).toThrow(
/outside the expo documentation allowlist/,
);
expect(() =>
resolveAllowedUrl(metroProvider, "https://metrobundler.dev.evil.example/docs/configuration"),
).toThrow(/outside the metro documentation allowlist/);
});
+3
View File
@@ -582,6 +582,9 @@ export function parseClaudeResult(stdout: string): ClaudeResult {
const tokens: TokenUsage = {
input: num(usage.input_tokens),
output: num(usage.output_tokens),
reasoning: num(
(usage.output_tokens_details as Record<string, unknown> | null | undefined)?.thinking_tokens,
),
cache: {
write: num(usage.cache_creation_input_tokens),
read: num(usage.cache_read_input_tokens),
+2
View File
@@ -33,6 +33,7 @@ const DIRECT_PROVIDER_ORDER: readonly ProviderId[] = [
"react-native-screens",
"react-native-worklets",
"react-native",
"metro",
"expo",
];
@@ -56,6 +57,7 @@ const DIRECT_SOURCE_KIND: Record<ProviderId, SourceKind> = {
"react-native-gesture-handler": "official-guide",
"react-native-screens": "official-guide",
"react-native-worklets": "official-guide",
metro: "official-guide",
};
export interface DirectDocumentationTarget {
+5
View File
@@ -431,6 +431,10 @@ export const reactNativeWorkletsProvider = htmlProvider(
true,
);
export const metroProvider = htmlProvider("metro", "react-native", "Metro bundler documentation", [
{ hostname: "metrobundler.dev", prefixes: [""] },
]);
const providers: Record<ProviderId, DocumentationProvider> = {
apple: appleProvider,
"apple-releases": appleReleasesProvider,
@@ -451,6 +455,7 @@ const providers: Record<ProviderId, DocumentationProvider> = {
"react-native-gesture-handler": reactNativeGestureHandlerProvider,
"react-native-screens": reactNativeScreensProvider,
"react-native-worklets": reactNativeWorkletsProvider,
metro: metroProvider,
};
export function getProvider(provider: ProviderId): DocumentationProvider {
+4
View File
@@ -91,6 +91,10 @@ const providerSearchDefinitions: Record<Exclude<ProviderId, "expo">, ProviderSea
scopes: ["docs.swmansion.com/react-native-worklets/docs"],
sourceKind: "official-api",
},
metro: {
scopes: ["metrobundler.dev/docs"],
sourceKind: "official-guide",
},
};
export interface RemoteSearchOptions {
+1 -1
View File
@@ -25,7 +25,7 @@ const queryGuidance =
"Formulate short documentation queries from exact API symbols plus one behavior or constraint term. Good: `CameraView barcodeScannerSettings`, `NWPathMonitor pathUpdateHandler`, `GestureDetector simultaneous gestures`. Avoid questions, prose, package/import names, code snippets, literals, paths, credentials, and other sensitive context. If the first result is broad, retry with a narrower symbol or member name.";
const providerGuidance =
"Provider map: apple=Apple SDK APIs and Human Interface Guidelines; apple-releases=Xcode and Apple platform release notes; swift-evolution=Swift Evolution proposals; sdwebimage=SDWebImage APIs and caching/loading behavior; android=Android, Jetpack, Compose, and Google Play services APIs; android-releases=Android platform releases and behavior changes; media3=Jetpack Media3; glide=Glide; okhttp=OkHttp; kotlin-coroutines=Kotlin coroutines; gradle=Gradle; agp=Android Gradle Plugin; jetbrains-issues=JetBrains YouTrack context; expo=Expo documentation; react-native=React Native core; react-native-reanimated=Reanimated; react-native-gesture-handler=Gesture Handler; react-native-screens=Screens; react-native-worklets=Worklets. Native source retains platform context: use apple/android for OS contracts and add the dependency provider for dependency-owned behavior; an Expo package path does not make a native API an Expo-docs query. Issue-tracker results are context, not API contracts.";
"Provider map: apple=Apple SDK APIs and Human Interface Guidelines; apple-releases=Xcode and Apple platform release notes; swift-evolution=Swift Evolution proposals; sdwebimage=SDWebImage APIs and caching/loading behavior; android=Android, Jetpack, Compose, and Google Play services APIs; android-releases=Android platform releases and behavior changes; media3=Jetpack Media3; glide=Glide; okhttp=OkHttp; kotlin-coroutines=Kotlin coroutines; gradle=Gradle; agp=Android Gradle Plugin; jetbrains-issues=JetBrains YouTrack context; expo=Expo documentation; react-native=React Native core; react-native-reanimated=Reanimated; react-native-gesture-handler=Gesture Handler; react-native-screens=Screens; react-native-worklets=Worklets; metro=Metro bundler. Native source retains platform context: use apple/android for OS contracts and add the dependency provider for dependency-owned behavior; an Expo package path does not make a native API an Expo-docs query. Issue-tracker results are context, not API contracts.";
export interface DocumentationServerOptions {
braveApiKey?: string;
+1
View File
@@ -23,6 +23,7 @@ export const PROVIDERS = [
"react-native-gesture-handler",
"react-native-screens",
"react-native-worklets",
"metro",
] as const;
export type ProviderId = (typeof PROVIDERS)[number];