@reticular/speakable 1.4.0 → 1.5.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/index.d.ts +116 -1
- package/dist/index.js +310 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +340 -0
- package/dist/mcp.js.map +1 -1
- package/package.json +11 -2
package/dist/mcp.js
CHANGED
|
@@ -2412,6 +2412,314 @@ function formatValue(value) {
|
|
|
2412
2412
|
return String(value);
|
|
2413
2413
|
}
|
|
2414
2414
|
|
|
2415
|
+
// src/runtime/verbosity-analyzer.ts
|
|
2416
|
+
var DEFAULT_CONFIG = {
|
|
2417
|
+
interactionFrameWindow: 150,
|
|
2418
|
+
textSimilarityThreshold: 0.6
|
|
2419
|
+
};
|
|
2420
|
+
function textSimilarity(a, b) {
|
|
2421
|
+
if (!a || !b) return 0;
|
|
2422
|
+
const wordsA = new Set(a.toLowerCase().split(/\s+/).filter(Boolean));
|
|
2423
|
+
const wordsB = new Set(b.toLowerCase().split(/\s+/).filter(Boolean));
|
|
2424
|
+
if (wordsA.size === 0 || wordsB.size === 0) return 0;
|
|
2425
|
+
let overlap = 0;
|
|
2426
|
+
for (const word of wordsA) {
|
|
2427
|
+
if (wordsB.has(word)) overlap++;
|
|
2428
|
+
}
|
|
2429
|
+
const maxSize = Math.max(wordsA.size, wordsB.size);
|
|
2430
|
+
return overlap / maxSize;
|
|
2431
|
+
}
|
|
2432
|
+
function groupIntoFrames(events, windowMs) {
|
|
2433
|
+
if (events.length === 0) return [];
|
|
2434
|
+
const frames = [];
|
|
2435
|
+
let currentFrame = {
|
|
2436
|
+
start: events[0].timestamp,
|
|
2437
|
+
end: events[0].timestamp,
|
|
2438
|
+
events: [events[0]]
|
|
2439
|
+
};
|
|
2440
|
+
for (let i = 1; i < events.length; i++) {
|
|
2441
|
+
const event = events[i];
|
|
2442
|
+
if (event.timestamp - currentFrame.end <= windowMs) {
|
|
2443
|
+
currentFrame.events.push(event);
|
|
2444
|
+
currentFrame.end = event.timestamp;
|
|
2445
|
+
} else {
|
|
2446
|
+
frames.push(currentFrame);
|
|
2447
|
+
currentFrame = {
|
|
2448
|
+
start: event.timestamp,
|
|
2449
|
+
end: event.timestamp,
|
|
2450
|
+
events: [event]
|
|
2451
|
+
};
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
frames.push(currentFrame);
|
|
2455
|
+
return frames;
|
|
2456
|
+
}
|
|
2457
|
+
function detectRedundantStateAndLiveRegion(frame, _config) {
|
|
2458
|
+
const findings = [];
|
|
2459
|
+
const stateChanges = frame.events.filter((e) => e.type === "STATE_CHANGED");
|
|
2460
|
+
const announcements = frame.events.filter((e) => e.type === "ANNOUNCEMENT");
|
|
2461
|
+
for (const stateEvent of stateChanges) {
|
|
2462
|
+
const payload = stateEvent.payload;
|
|
2463
|
+
const announcedAttributes = [
|
|
2464
|
+
"aria-selected",
|
|
2465
|
+
"aria-checked",
|
|
2466
|
+
"aria-pressed",
|
|
2467
|
+
"aria-expanded",
|
|
2468
|
+
"aria-disabled",
|
|
2469
|
+
"aria-invalid",
|
|
2470
|
+
"aria-current"
|
|
2471
|
+
];
|
|
2472
|
+
if (!announcedAttributes.includes(payload.attribute)) continue;
|
|
2473
|
+
for (const announcement of announcements) {
|
|
2474
|
+
const annoPayload = announcement.payload;
|
|
2475
|
+
const annoText = annoPayload.text.toLowerCase();
|
|
2476
|
+
const elementName = stateEvent.target.accessibleName.toLowerCase();
|
|
2477
|
+
const nameInAnnouncement = elementName.length > 0 && annoText.includes(elementName);
|
|
2478
|
+
const stateEchoWords = ["selected", "deselected", "checked", "unchecked", "expanded", "collapsed", "pressed"];
|
|
2479
|
+
const hasStateEcho = stateEchoWords.some((word) => annoText.includes(word));
|
|
2480
|
+
if (nameInAnnouncement && hasStateEcho) {
|
|
2481
|
+
findings.push({
|
|
2482
|
+
pattern: "redundant-state-and-live-region",
|
|
2483
|
+
severity: "high",
|
|
2484
|
+
description: `The ${payload.attribute} state change on "${stateEvent.target.accessibleName}" (${stateEvent.target.role}) is already announced by the screen reader, but a live region also announces similar content ("${annoPayload.text}"). This causes VoiceOver to read the information 2-3 times.`,
|
|
2485
|
+
remediation: [
|
|
2486
|
+
`Remove the live region announcement when ${payload.attribute} changes on the active element, since the screen reader already announces state transitions on the focused/active item.`,
|
|
2487
|
+
`If you need the live region for summary info (e.g., "3 of 5 selected"), debounce it with a 150ms+ delay and ensure the text does NOT repeat the option name.`,
|
|
2488
|
+
`Use a summary-only pattern in the live region: "3 of 5 selected" instead of "JavaScript selected. 3 of 5 selected." The element name is already announced via the state change.`,
|
|
2489
|
+
`Consider removing the live region entirely and relying on aria-selected/aria-checked state changes for per-item feedback.`
|
|
2490
|
+
],
|
|
2491
|
+
affectedReaders: ["VoiceOver (macOS)", "VoiceOver (iOS)"],
|
|
2492
|
+
relatedEvents: [stateEvent, announcement],
|
|
2493
|
+
timeWindow: { start: frame.start, end: frame.end },
|
|
2494
|
+
elementSelector: stateEvent.target.selector
|
|
2495
|
+
});
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
return findings;
|
|
2500
|
+
}
|
|
2501
|
+
function detectActiveDescendantPlusStateChange(frame, _config) {
|
|
2502
|
+
const findings = [];
|
|
2503
|
+
const activedescendantChanges = frame.events.filter(
|
|
2504
|
+
(e) => e.type === "STATE_CHANGED" && e.payload.attribute === "aria-activedescendant"
|
|
2505
|
+
);
|
|
2506
|
+
const otherStateChanges = frame.events.filter(
|
|
2507
|
+
(e) => e.type === "STATE_CHANGED" && e.payload.attribute !== "aria-activedescendant"
|
|
2508
|
+
);
|
|
2509
|
+
for (const adChange of activedescendantChanges) {
|
|
2510
|
+
const adPayload = adChange.payload;
|
|
2511
|
+
const newValue = String(adPayload.newValue);
|
|
2512
|
+
const previousValue = String(adPayload.previousValue ?? "");
|
|
2513
|
+
const isSameElement = newValue === previousValue;
|
|
2514
|
+
for (const stateChange of otherStateChanges) {
|
|
2515
|
+
const statePayload = stateChange.payload;
|
|
2516
|
+
const targetMatchesAD = stateChange.target.selector.includes(newValue) || stateChange.target.selector === adChange.target.selector;
|
|
2517
|
+
if (!targetMatchesAD) continue;
|
|
2518
|
+
if (isSameElement) {
|
|
2519
|
+
findings.push({
|
|
2520
|
+
pattern: "activedescendant-plus-state-change",
|
|
2521
|
+
severity: "high",
|
|
2522
|
+
description: `aria-activedescendant was re-set to the same value ("${newValue}") AND ${statePayload.attribute} changed on that element within ${frame.end - frame.start}ms. VoiceOver reads the element once for the activedescendant update, then again for the state change.`,
|
|
2523
|
+
remediation: [
|
|
2524
|
+
`When toggling selection on the already-active option, do NOT re-set aria-activedescendant to the same value. Only update it when the user navigates to a different option.`,
|
|
2525
|
+
`Batch DOM attribute changes: set aria-selected BEFORE updating aria-activedescendant, or skip the activedescendant update entirely when the active item hasn't changed.`,
|
|
2526
|
+
`For click interactions on options, avoid calling both setActiveDescendant() and toggleSelection() if the clicked item is already the active descendant.`
|
|
2527
|
+
],
|
|
2528
|
+
affectedReaders: ["VoiceOver (macOS)", "VoiceOver (iOS)", "Narrator"],
|
|
2529
|
+
relatedEvents: [adChange, stateChange],
|
|
2530
|
+
timeWindow: { start: frame.start, end: frame.end },
|
|
2531
|
+
elementSelector: stateChange.target.selector
|
|
2532
|
+
});
|
|
2533
|
+
} else {
|
|
2534
|
+
findings.push({
|
|
2535
|
+
pattern: "activedescendant-plus-state-change",
|
|
2536
|
+
severity: "low",
|
|
2537
|
+
description: `aria-activedescendant moved to "${stateChange.target.accessibleName}" AND ${statePayload.attribute} changed on that element within ${frame.end - frame.start}ms. VoiceOver may announce the option name twice (once for navigation, once for state).`,
|
|
2538
|
+
remediation: [
|
|
2539
|
+
`If this is a click-to-select interaction, consider separating the activedescendant update and the state change by a microtask (Promise.resolve()) so VoiceOver processes them as separate assertions.`,
|
|
2540
|
+
`Alternatively, accept this as expected behavior for click interactions where navigation and selection happen simultaneously.`
|
|
2541
|
+
],
|
|
2542
|
+
affectedReaders: ["VoiceOver (macOS)"],
|
|
2543
|
+
relatedEvents: [adChange, stateChange],
|
|
2544
|
+
timeWindow: { start: frame.start, end: frame.end },
|
|
2545
|
+
elementSelector: stateChange.target.selector
|
|
2546
|
+
});
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
return findings;
|
|
2551
|
+
}
|
|
2552
|
+
function detectNameChangePlusLiveRegion(frame, config) {
|
|
2553
|
+
const findings = [];
|
|
2554
|
+
const nameChanges = frame.events.filter((e) => e.type === "ACCESSIBLE_NAME_CHANGED");
|
|
2555
|
+
const announcements = frame.events.filter((e) => e.type === "ANNOUNCEMENT");
|
|
2556
|
+
for (const nameEvent of nameChanges) {
|
|
2557
|
+
for (const announcement of announcements) {
|
|
2558
|
+
const annoPayload = announcement.payload;
|
|
2559
|
+
const similarity = textSimilarity(nameEvent.target.accessibleName, annoPayload.text);
|
|
2560
|
+
if (similarity >= config.textSimilarityThreshold) {
|
|
2561
|
+
findings.push({
|
|
2562
|
+
pattern: "name-change-plus-live-region",
|
|
2563
|
+
severity: "medium",
|
|
2564
|
+
description: `The accessible name of "${nameEvent.target.accessibleName}" (${nameEvent.target.role}) changed AND a live region announced similar content ("${annoPayload.text}"). Screen readers may announce both the name change and the live region.`,
|
|
2565
|
+
remediation: [
|
|
2566
|
+
`Choose one announcement channel: either update the element's accessible name (which the screen reader reads if the element is focused) OR use a live region, not both.`,
|
|
2567
|
+
`If the element is focused, prefer the name change. If it's not focused, prefer the live region.`,
|
|
2568
|
+
`Add a 200ms+ debounce to the live region update to let the name change announce first.`
|
|
2569
|
+
],
|
|
2570
|
+
affectedReaders: ["VoiceOver (macOS)", "NVDA", "JAWS"],
|
|
2571
|
+
relatedEvents: [nameEvent, announcement],
|
|
2572
|
+
timeWindow: { start: frame.start, end: frame.end },
|
|
2573
|
+
elementSelector: nameEvent.target.selector
|
|
2574
|
+
});
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
return findings;
|
|
2579
|
+
}
|
|
2580
|
+
function detectFocusPlusActiveDescendant(frame, _config) {
|
|
2581
|
+
const findings = [];
|
|
2582
|
+
const focusEvents = frame.events.filter((e) => e.type === "FOCUS_CHANGED");
|
|
2583
|
+
const adChanges = frame.events.filter(
|
|
2584
|
+
(e) => e.type === "STATE_CHANGED" && e.payload.attribute === "aria-activedescendant"
|
|
2585
|
+
);
|
|
2586
|
+
if (focusEvents.length > 0 && adChanges.length > 0) {
|
|
2587
|
+
findings.push({
|
|
2588
|
+
pattern: "focus-plus-activedescendant",
|
|
2589
|
+
severity: "medium",
|
|
2590
|
+
description: `Both real DOM focus moved AND aria-activedescendant changed within ${frame.end - frame.start}ms. Screen readers may announce the focused element AND then re-announce via activedescendant, causing a double-read.`,
|
|
2591
|
+
remediation: [
|
|
2592
|
+
`Choose one focus management strategy: either move real DOM focus to options (roving tabindex) OR use aria-activedescendant on the container. Do not mix both.`,
|
|
2593
|
+
`If using aria-activedescendant, keep DOM focus on the container (listbox/combobox) and only change the activedescendant attribute.`,
|
|
2594
|
+
`If using roving tabindex, set tabindex="0" on the active item and tabindex="-1" on others, and move focus directly via element.focus().`
|
|
2595
|
+
],
|
|
2596
|
+
affectedReaders: ["VoiceOver (macOS)", "NVDA", "JAWS", "Narrator"],
|
|
2597
|
+
relatedEvents: [...focusEvents, ...adChanges],
|
|
2598
|
+
timeWindow: { start: frame.start, end: frame.end },
|
|
2599
|
+
elementSelector: focusEvents[0].target.selector
|
|
2600
|
+
});
|
|
2601
|
+
}
|
|
2602
|
+
return findings;
|
|
2603
|
+
}
|
|
2604
|
+
function detectMultipleStateChangesSameElement(frame, _config) {
|
|
2605
|
+
const findings = [];
|
|
2606
|
+
const stateChanges = frame.events.filter((e) => e.type === "STATE_CHANGED");
|
|
2607
|
+
const bySelector = /* @__PURE__ */ new Map();
|
|
2608
|
+
for (const event of stateChanges) {
|
|
2609
|
+
const existing = bySelector.get(event.target.selector) || [];
|
|
2610
|
+
existing.push(event);
|
|
2611
|
+
bySelector.set(event.target.selector, existing);
|
|
2612
|
+
}
|
|
2613
|
+
for (const [selector, events] of bySelector) {
|
|
2614
|
+
if (events.length >= 3) {
|
|
2615
|
+
const attributes = events.map((e) => e.payload.attribute);
|
|
2616
|
+
findings.push({
|
|
2617
|
+
pattern: "multiple-state-changes-same-element",
|
|
2618
|
+
severity: "medium",
|
|
2619
|
+
description: `${events.length} ARIA attributes changed simultaneously on "${events[0].target.accessibleName}" (${events[0].target.role}): ${attributes.join(", ")}. Some screen readers announce each attribute change individually, causing a verbose multi-part announcement.`,
|
|
2620
|
+
remediation: [
|
|
2621
|
+
`Batch attribute changes into a single DOM mutation frame by setting all attributes before the next microtask. Use requestAnimationFrame() or Promise.resolve() to group them.`,
|
|
2622
|
+
`Evaluate which attributes actually need to be communicated. For example, if aria-selected changes, the screen reader already announces it; additional aria-checked or aria-pressed changes may be redundant.`,
|
|
2623
|
+
`Consider using a single composite state (e.g., just aria-selected for listbox options) rather than multiple state attributes.`
|
|
2624
|
+
],
|
|
2625
|
+
affectedReaders: ["VoiceOver (macOS)", "Narrator"],
|
|
2626
|
+
relatedEvents: events,
|
|
2627
|
+
timeWindow: { start: frame.start, end: frame.end },
|
|
2628
|
+
elementSelector: selector
|
|
2629
|
+
});
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
return findings;
|
|
2633
|
+
}
|
|
2634
|
+
function detectRapidIdenticalAnnouncements(frame, _config) {
|
|
2635
|
+
const findings = [];
|
|
2636
|
+
const announcements = frame.events.filter((e) => e.type === "ANNOUNCEMENT");
|
|
2637
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2638
|
+
for (const event of announcements) {
|
|
2639
|
+
const text = event.payload.text.toLowerCase().trim();
|
|
2640
|
+
const existing = seen.get(text) || [];
|
|
2641
|
+
existing.push(event);
|
|
2642
|
+
seen.set(text, existing);
|
|
2643
|
+
}
|
|
2644
|
+
for (const [text, events] of seen) {
|
|
2645
|
+
if (events.length >= 2) {
|
|
2646
|
+
findings.push({
|
|
2647
|
+
pattern: "rapid-identical-announcements",
|
|
2648
|
+
severity: "high",
|
|
2649
|
+
description: `The same live region content ("${text}") was announced ${events.length} times within ${frame.end - frame.start}ms. Users will hear the same information repeated.`,
|
|
2650
|
+
remediation: [
|
|
2651
|
+
`Debounce live region updates. Clear the live region text content first, then set the new value after a 100-200ms delay.`,
|
|
2652
|
+
`If updating a live region's textContent to the same value, the screen reader may ignore it OR re-read it depending on implementation. Clear to empty string first.`,
|
|
2653
|
+
`Avoid updating the live region inside a loop or rapid event handler. Compute the final state and announce once.`
|
|
2654
|
+
],
|
|
2655
|
+
affectedReaders: ["VoiceOver (macOS)", "VoiceOver (iOS)", "NVDA", "Narrator"],
|
|
2656
|
+
relatedEvents: events,
|
|
2657
|
+
timeWindow: { start: frame.start, end: frame.end },
|
|
2658
|
+
elementSelector: events[0].target.selector
|
|
2659
|
+
});
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
return findings;
|
|
2663
|
+
}
|
|
2664
|
+
function analyzeVerbosity(events, config) {
|
|
2665
|
+
const cfg = { ...DEFAULT_CONFIG, ...config };
|
|
2666
|
+
const frames = groupIntoFrames(events, cfg.interactionFrameWindow);
|
|
2667
|
+
const findings = [];
|
|
2668
|
+
for (const frame of frames) {
|
|
2669
|
+
findings.push(...detectRedundantStateAndLiveRegion(frame, cfg));
|
|
2670
|
+
findings.push(...detectActiveDescendantPlusStateChange(frame, cfg));
|
|
2671
|
+
findings.push(...detectNameChangePlusLiveRegion(frame, cfg));
|
|
2672
|
+
findings.push(...detectFocusPlusActiveDescendant(frame, cfg));
|
|
2673
|
+
findings.push(...detectMultipleStateChangesSameElement(frame, cfg));
|
|
2674
|
+
findings.push(...detectRapidIdenticalAnnouncements(frame, cfg));
|
|
2675
|
+
}
|
|
2676
|
+
const severityOrder = { high: 0, medium: 1, low: 2 };
|
|
2677
|
+
findings.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
|
|
2678
|
+
const summary = {
|
|
2679
|
+
high: findings.filter((f) => f.severity === "high").length,
|
|
2680
|
+
medium: findings.filter((f) => f.severity === "medium").length,
|
|
2681
|
+
low: findings.filter((f) => f.severity === "low").length,
|
|
2682
|
+
total: findings.length
|
|
2683
|
+
};
|
|
2684
|
+
const deductions = summary.high * 25 + summary.medium * 10 + summary.low * 3;
|
|
2685
|
+
const score = Math.max(0, 100 - deductions);
|
|
2686
|
+
return { findings, summary, score };
|
|
2687
|
+
}
|
|
2688
|
+
function formatVerbosityReport(report) {
|
|
2689
|
+
const lines = [];
|
|
2690
|
+
lines.push("");
|
|
2691
|
+
lines.push("=== VERBOSITY ANALYSIS REPORT ===");
|
|
2692
|
+
lines.push("");
|
|
2693
|
+
lines.push(`Score: ${report.score}/100 ${report.score >= 80 ? "(Good)" : report.score >= 50 ? "(Needs attention)" : "(Critical verbosity issues)"}`);
|
|
2694
|
+
lines.push(`Findings: ${report.summary.total} (${report.summary.high} high, ${report.summary.medium} medium, ${report.summary.low} low)`);
|
|
2695
|
+
lines.push("");
|
|
2696
|
+
if (report.findings.length === 0) {
|
|
2697
|
+
lines.push("No verbosity issues detected. Announcements appear clean.");
|
|
2698
|
+
return lines.join("\n");
|
|
2699
|
+
}
|
|
2700
|
+
for (let i = 0; i < report.findings.length; i++) {
|
|
2701
|
+
const finding = report.findings[i];
|
|
2702
|
+
const severityIcon = finding.severity === "high" ? "\u2718" : finding.severity === "medium" ? "\u26A0" : "\u2139";
|
|
2703
|
+
lines.push(`${severityIcon} [${finding.severity.toUpperCase()}] ${finding.pattern}`);
|
|
2704
|
+
lines.push(` Element: ${finding.elementSelector}`);
|
|
2705
|
+
lines.push(` Window: ${finding.timeWindow.start}ms - ${finding.timeWindow.end}ms`);
|
|
2706
|
+
lines.push(` ${finding.description}`);
|
|
2707
|
+
lines.push("");
|
|
2708
|
+
lines.push(" Affected: " + finding.affectedReaders.join(", "));
|
|
2709
|
+
lines.push("");
|
|
2710
|
+
lines.push(" Remediation:");
|
|
2711
|
+
for (const step of finding.remediation) {
|
|
2712
|
+
lines.push(` - ${step}`);
|
|
2713
|
+
}
|
|
2714
|
+
lines.push("");
|
|
2715
|
+
if (i < report.findings.length - 1) {
|
|
2716
|
+
lines.push(" " + "\u2500".repeat(60));
|
|
2717
|
+
lines.push("");
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
return lines.join("\n");
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2415
2723
|
// src/mcp.ts
|
|
2416
2724
|
function renderOutput(model, screenReader) {
|
|
2417
2725
|
if (screenReader === "all") {
|
|
@@ -2599,6 +2907,38 @@ server.tool(
|
|
|
2599
2907
|
}
|
|
2600
2908
|
}
|
|
2601
2909
|
);
|
|
2910
|
+
server.tool(
|
|
2911
|
+
"analyze_verbosity",
|
|
2912
|
+
"Detect redundant and duplicate screen reader announcements in a sequence of accessibility events. Identifies patterns like simultaneous aria-selected + live region updates that cause VoiceOver to read the same content 2-3 times. Provides specific remediation steps and affected screen readers.",
|
|
2913
|
+
{
|
|
2914
|
+
events: z.array(z.object({
|
|
2915
|
+
type: z.string().describe("Event type: STATE_CHANGED, ANNOUNCEMENT, FOCUS_CHANGED, ACCESSIBLE_NAME_CHANGED, etc."),
|
|
2916
|
+
timestamp: z.number().describe("Milliseconds since session start"),
|
|
2917
|
+
target: z.object({
|
|
2918
|
+
role: z.string().describe("ARIA role of the target element"),
|
|
2919
|
+
accessibleName: z.string().describe("Computed accessible name"),
|
|
2920
|
+
selector: z.string().describe("CSS selector for the element")
|
|
2921
|
+
}),
|
|
2922
|
+
payload: z.record(z.unknown()).describe("Event payload (kind field discriminates type)")
|
|
2923
|
+
})).describe("Chronologically ordered accessibility events (from runtime engine or manual capture)"),
|
|
2924
|
+
interaction_frame_window: z.number().optional().describe("Time window (ms) to group related events into a single interaction (default: 150)")
|
|
2925
|
+
},
|
|
2926
|
+
async ({ events, interaction_frame_window }) => {
|
|
2927
|
+
try {
|
|
2928
|
+
const config = interaction_frame_window ? { interactionFrameWindow: interaction_frame_window } : void 0;
|
|
2929
|
+
const report = analyzeVerbosity(events, config);
|
|
2930
|
+
const output = formatVerbosityReport(report);
|
|
2931
|
+
return {
|
|
2932
|
+
content: [{ type: "text", text: output }]
|
|
2933
|
+
};
|
|
2934
|
+
} catch (error) {
|
|
2935
|
+
return {
|
|
2936
|
+
content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : "Unknown error"}` }],
|
|
2937
|
+
isError: true
|
|
2938
|
+
};
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
);
|
|
2602
2942
|
async function main() {
|
|
2603
2943
|
const transport = new StdioServerTransport();
|
|
2604
2944
|
await server.connect(transport);
|