pi-cc-extensions 0.9.4 → 0.9.6

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.
@@ -31,18 +31,16 @@ function renderDiffSpacerLine(width: number): string {
31
31
  return safeWidth > 0 ? " ".repeat(safeWidth) : "";
32
32
  }
33
33
 
34
- export function resolveDiffDisplayLimit(
35
- expanded: boolean,
36
- maxCollapsedLines: number,
37
- maxExpandedLines: number,
38
- ): number {
39
- const expandedLimit = Number.isFinite(maxExpandedLines)
40
- ? maxExpandedLines
41
- : DEFAULT_TOOL_DISPLAY_CONFIG.expandedPreviewMaxLines;
34
+ /**
35
+ * Body lines to render before the remainder hint. Expanded diffs are never capped:
36
+ * clicking `click to show more` expands the card to the whole diff.
37
+ */
38
+ export function resolveDiffDisplayLimit(expanded: boolean, maxCollapsedLines: number): number {
39
+ if (expanded) return Number.POSITIVE_INFINITY;
42
40
  const collapsedLimit = Number.isFinite(maxCollapsedLines)
43
41
  ? maxCollapsedLines
44
42
  : DEFAULT_TOOL_DISPLAY_CONFIG.editDiffCollapsedLines;
45
- return expanded ? Math.max(0, expandedLimit) : Math.max(1, collapsedLimit);
43
+ return Math.max(1, collapsedLimit);
46
44
  }
47
45
 
48
46
  export function resolveWriteCollapsedLimit(
@@ -99,7 +97,6 @@ export function applyLineLimit(
99
97
  width: number,
100
98
  expanded: boolean,
101
99
  maxCollapsedLines: number,
102
- maxExpandedLines: number,
103
100
  totalHunks: number,
104
101
  theme: DiffTheme,
105
102
  /**
@@ -108,17 +105,17 @@ export function applyLineLimit(
108
105
  */
109
106
  unprocessedLogicalRows = 0,
110
107
  hovered = false,
111
- ): string[] {
112
- const limit = resolveDiffDisplayLimit(expanded, maxCollapsedLines, maxExpandedLines);
108
+ ): { lines: string[]; hintLine?: string } {
109
+ const limit = resolveDiffDisplayLimit(expanded, maxCollapsedLines);
113
110
  const safeUnprocessed = Math.max(0, unprocessedLogicalRows);
114
111
  if (limit === 0 || (rows.length <= limit && safeUnprocessed === 0)) {
115
- return rows.map((row) => clampDiffLineToWidth(row.text, width));
112
+ return { lines: rows.map((row) => clampDiffLineToWidth(row.text, width)) };
116
113
  }
117
114
 
118
115
  const shown = rows.length <= limit ? rows : rows.slice(0, limit);
119
116
  const remaining = Math.max(0, rows.length - shown.length) + safeUnprocessed;
120
117
  if (remaining === 0) {
121
- return shown.map((row) => clampDiffLineToWidth(row.text, width));
118
+ return { lines: shown.map((row) => clampDiffLineToWidth(row.text, width)) };
122
119
  }
123
120
  const visibleHunks = new Set(
124
121
  shown
@@ -137,15 +134,19 @@ export function applyLineLimit(
137
134
 
138
135
  const clickLabel = showMoreHintText();
139
136
  const clickIndex = hintText.lastIndexOf(clickLabel);
137
+ // 展开态不会再截断,剩下的分支必是折叠态提示。
140
138
  const styledHint =
141
- hovered && !expanded && clickIndex >= 0
139
+ hovered && clickIndex >= 0
142
140
  ? theme.fg("muted", hintText.slice(0, clickIndex)) +
143
141
  theme.fg("text", clickLabel) +
144
142
  theme.fg("muted", hintText.slice(clickIndex + clickLabel.length))
145
- : theme.fg(expanded ? "warning" : "muted", hintText);
146
- return [
147
- ...shown.map((row) => clampDiffLineToWidth(row.text, width)),
148
- renderDiffSpacerLine(width),
149
- clampDiffLineToWidth(styledHint, width),
150
- ];
143
+ : theme.fg("muted", hintText);
144
+ return {
145
+ lines: [
146
+ ...shown.map((row) => clampDiffLineToWidth(row.text, width)),
147
+ renderDiffSpacerLine(width),
148
+ clampDiffLineToWidth(styledHint, width),
149
+ ],
150
+ hintLine: hintText,
151
+ };
151
152
  }
@@ -46,6 +46,7 @@ import {
46
46
  displayConfigCacheKey,
47
47
  resolveDiffIndicatorMode,
48
48
  resolveLiveDisplayConfig,
49
+ normalizeCollapsedHintLine,
49
50
  type DiffRenderOptions,
50
51
  type DisplayConfigInput,
51
52
  } from "./diff-component.ts";
@@ -407,6 +408,11 @@ export function renderWriteDiffResult(
407
408
 
408
409
  return {
409
410
  [RICH_DIFF_COMPONENT]: true,
411
+ /** 鼠标层只在真正的 remainder 行上提供展开入口。 */
412
+ isCollapsedHintLine(plainLine: string): boolean {
413
+ const hint = cache.getHintLine();
414
+ return hint !== undefined && normalizeCollapsedHintLine(plainLine) === hint;
415
+ },
410
416
  render(width: number): string[] {
411
417
  // Live config: panel can change indicator/wrap/limits after this component is created.
412
418
  const live = resolveLiveDisplayConfig(config);
@@ -433,22 +439,21 @@ export function renderWriteDiffResult(
433
439
  const writeCollapsedLimit = resolveWriteCollapsedLimit(live);
434
440
  const statsOnlyCollapsed = !options.expanded && writeCollapsedLimit === 0;
435
441
  if (statsOnlyCollapsed) {
442
+ const hintLines = renderWriteCollapsedHintLine(
443
+ options.fileExistedBeforeWrite === true,
444
+ safeWidth,
445
+ theme,
446
+ hovered,
447
+ options.headerLabel,
448
+ );
436
449
  return cache.set(
437
450
  safeWidth,
438
451
  options.expanded,
439
452
  mode,
440
453
  configKey,
441
454
  hovered,
442
- clampDiffLinesToWidth(
443
- renderWriteCollapsedHintLine(
444
- options.fileExistedBeforeWrite === true,
445
- safeWidth,
446
- theme,
447
- hovered,
448
- options.headerLabel,
449
- ),
450
- safeWidth,
451
- ),
455
+ clampDiffLinesToWidth(hintLines, safeWidth),
456
+ normalizeCollapsedHintLine(hintLines[0]),
452
457
  );
453
458
  }
454
459
 
@@ -500,11 +505,7 @@ export function renderWriteDiffResult(
500
505
  }
501
506
 
502
507
  const data = getDetailedData();
503
- const displayLimit = resolveDiffDisplayLimit(
504
- options.expanded,
505
- writeCollapsedLimit,
506
- live.expandedPreviewMaxLines,
507
- );
508
+ const displayLimit = resolveDiffDisplayLimit(options.expanded, writeCollapsedLimit);
508
509
  const processBudget = resolveDiffProcessBudget(displayLimit, wordWrap);
509
510
  const entryBudget = takeEntriesForLineBudget(data.entries, processBudget);
510
511
  const splitBudget = takeSplitRowsForBudget(data.splitRows, processBudget);
@@ -541,7 +542,6 @@ export function renderWriteDiffResult(
541
542
  safeWidth,
542
543
  options.expanded,
543
544
  writeCollapsedLimit,
544
- live.expandedPreviewMaxLines,
545
545
  data.hunkCount,
546
546
  theme,
547
547
  unprocessedLogicalRows,
@@ -549,9 +549,19 @@ export function renderWriteDiffResult(
549
549
  );
550
550
  const frame = renderDiffFrameLine(safeWidth, theme);
551
551
  const renderedLines =
552
- mode === "unified" ? [header, frame, ...bodyWithLimit, frame] : [header, ...bodyWithLimit];
552
+ mode === "unified"
553
+ ? [header, frame, ...bodyWithLimit.lines, frame]
554
+ : [header, ...bodyWithLimit.lines];
553
555
  const finalLines = clampDiffLinesToWidth(renderedLines, safeWidth);
554
- return cache.set(safeWidth, options.expanded, mode, configKey, hovered, finalLines);
556
+ return cache.set(
557
+ safeWidth,
558
+ options.expanded,
559
+ mode,
560
+ configKey,
561
+ hovered,
562
+ finalLines,
563
+ normalizeCollapsedHintLine(bodyWithLimit.hintLine),
564
+ );
555
565
  },
556
566
  invalidate: cache.invalidate,
557
567
  } as Component;
@@ -8,12 +8,14 @@ import {
8
8
  } from "@earendil-works/pi-tui";
9
9
  import { TOOL_LOADING_INTERVAL_MS, toolLoadingIcon } from "../../utils/tool-loading-icon.ts";
10
10
  import { getToolMouseTui } from "../mouse/scroll.ts";
11
- import { isToolTuiFullscreen, showMoreHintText } from "./show-more-hint.ts";
11
+ import { mcpToolTitle } from "./mcp-title.ts";
12
+ import { collapseHintText, isToolTuiFullscreen, showMoreHintText } from "./show-more-hint.ts";
12
13
  import { stripAnsi, stripBackgroundAnsi, stripLeadingStatusIcon } from "../../utils/ansi-text.ts";
13
14
  import { walkComponentTree } from "../../utils/component-tree.ts";
14
15
  import {
15
16
  fitToolCallSummary,
16
17
  humanizeToolLabel,
18
+ renderToolSummary,
17
19
  toolCallSummary,
18
20
  type ToolCallSummary,
19
21
  } from "./names.ts";
@@ -44,6 +46,19 @@ function toolName(tool: any): string {
44
46
  return String(tool?.toolName ?? tool?.toolDefinition?.name ?? "tool");
45
47
  }
46
48
 
49
+ /**
50
+ * 标题:MCP 工具用 adapter 暴露的真实工具名,其余回退工具名人性化。
51
+ */
52
+ function toolTitle(tool: any): string {
53
+ const name = toolName(tool);
54
+ return (
55
+ mcpToolTitle({
56
+ toolName: name,
57
+ definition: tool?.toolDefinition ?? tool?.builtInToolDefinition,
58
+ }) ?? humanizeToolLabel(name)
59
+ );
60
+ }
61
+
47
62
  function isGroupable(value: unknown): boolean {
48
63
  return value instanceof ToolExecutionComponent && !NON_GROUPABLE.has(toolName(value));
49
64
  }
@@ -170,6 +185,7 @@ export function paddedBackgroundRow(
170
185
 
171
186
  function toolSummary(tool: any): ToolCallSummary {
172
187
  return toolCallSummary(toolName(tool), tool?.args ?? {}, {
188
+ title: toolTitle(tool),
173
189
  variant: "grouping",
174
190
  cwd: tool?.cwd,
175
191
  });
@@ -347,8 +363,7 @@ export class ToolGroupComponent extends Container {
347
363
  })
348
364
  .join(` ${fg("dim", "•")} `);
349
365
  const names = new Set(this.children.map(toolName));
350
- const label =
351
- names.size === 1 ? humanizeToolLabel(toolName(this.children[0])) : "Multiple Tools";
366
+ const label = names.size === 1 ? toolTitle(this.children[0]) : "Multiple Tools";
352
367
  const overall: ToolStatus = counts.error ? "error" : counts.pending ? "pending" : "success";
353
368
  if (
354
369
  (this.children as any[]).some((tool) => tool?.executionStarted && status(tool) === "pending")
@@ -357,7 +372,8 @@ export class ToolGroupComponent extends Container {
357
372
  const overallColor = overall === "pending" ? "accent" : overall;
358
373
  const nameList = names.size > 1 ? ` ${fg("dim", `• ${toolNameList(this.children)}`)}` : "";
359
374
  // 圆点保持 dim;hover 只高亮可点击文字。
360
- const hint = `${fg("dim", "•")} ${fg(this.hintHovered ? "text" : "dim", showMoreHintText())}`;
375
+ const hintText = this._expanded ? collapseHintText() : showMoreHintText();
376
+ const hint = `${fg("dim", "•")} ${fg(this.hintHovered ? "text" : "dim", hintText)}`;
361
377
  const lines = [
362
378
  "",
363
379
  truncateToWidth(
@@ -398,7 +414,7 @@ export class ToolGroupComponent extends Container {
398
414
  const mainWidth = Math.max(0, width - visibleWidth(prefix) - visibleWidth(detail));
399
415
  lines.push(
400
416
  truncateToWidth(
401
- `${prefix}${fg("toolTitle", fitToolCallSummary(summary, mainWidth))}${detail}`,
417
+ `${prefix}${renderToolSummary(summary, mainWidth, fg)}${detail}`,
402
418
  width,
403
419
  "",
404
420
  ),
@@ -0,0 +1,39 @@
1
+ import { humanizeToolLabel } from "./names.ts";
2
+
3
+ /**
4
+ * MCP 工具的识别与标题,单卡(default-mode)与分组行/分组头(grouping)共用。
5
+ *
6
+ * - 判定:工具名带 mcp 片段(`mcp` 网关、`mcpScript`、`mcp__<server>`),或 adapter 注册时
7
+ * 给的 label 以 `MCP:` 开头。默认/none/short 前缀模式下直连工具的名字里没有 mcp 片段
8
+ * (`github_search_code`、`search_code`),只能靠 label 判定。
9
+ * - 标题:具体工具用 adapter 暴露的真实工具名,不做二次格式化;只有 `mcp` / `mcpScript`
10
+ * 这两个入口没有具体工具名,跟普通工具一样人性化(MCP / MCP Script)。
11
+ * - 参数摘要与其它工具共用 toolCallSummary 的字段链,网关额外把 `args.tool` 提到标题后。
12
+ */
13
+
14
+ /** 单次 MCP 调用的识别信息。 */
15
+ export type McpToolCall = {
16
+ toolName: string;
17
+ /** 工具定义;默认/none/short 前缀模式要靠 label 判定,会话重放时可能缺失。 */
18
+ definition?: any;
19
+ };
20
+
21
+ const GATEWAY_TOOL_NAME = "mcp";
22
+ const SCRIPT_TOOL_NAME = "mcpScript";
23
+
24
+ export function isMcpToolDefinition(definition: any, toolName: string): boolean {
25
+ const label = typeof definition?.label === "string" ? definition.label.trim() : "";
26
+ if (/^MCP(?::|$)/i.test(label)) return true;
27
+ if (toolName === GATEWAY_TOOL_NAME || toolName === SCRIPT_TOOL_NAME) return true;
28
+ return /^mcp[_:-]|[_:-]mcp[_:-]/i.test(toolName);
29
+ }
30
+
31
+ /** 标题:MCP 工具返回标题(入口走人性化,其余用真实工具名);非 MCP 返回 undefined。 */
32
+ export function mcpToolTitle(call: McpToolCall): string | undefined {
33
+ const { toolName } = call;
34
+ if (!isMcpToolDefinition(call.definition, toolName)) return undefined;
35
+ if (toolName === GATEWAY_TOOL_NAME || toolName === SCRIPT_TOOL_NAME) {
36
+ return humanizeToolLabel(toolName);
37
+ }
38
+ return toolName;
39
+ }
@@ -1,15 +1,23 @@
1
1
  import { posix, win32 } from "node:path";
2
+ import type { ThemeColor } from "@earendil-works/pi-coding-agent";
2
3
  import { visibleWidth } from "@earendil-works/pi-tui";
3
4
  import { config } from "../../config/config.ts";
4
5
  import { oneLine } from "../../utils/format.ts";
6
+ import { headTruncateToWidth } from "./result.ts";
5
7
 
6
8
  function clip(value: unknown): string {
7
9
  return oneLine(value, config.inputClip);
8
10
  }
9
11
 
12
+ /** 载荷(入参 JSON / 脚本代码)至少留出这么多宽度才显示,否则整段省略(避免只剩一个孤零零的 " {…")。 */
13
+ const MIN_PAYLOAD_WIDTH = 10;
14
+
10
15
  /** 品牌大小写固定写法;humanize 推导不出来的工具名写在这里。 */
11
16
  const TOOL_LABEL_OVERRIDES: Readonly<Record<string, string>> = {
12
17
  powershell: "PowerShell",
18
+ // MCP 入口两个没有具体工具名,跟普通工具一样人性化;缩写固定大写
19
+ mcp: "MCP",
20
+ mcpscript: "MCP Script",
13
21
  };
14
22
 
15
23
  /**
@@ -49,6 +57,8 @@ export type ToolCallSummary = {
49
57
  detail: string;
50
58
  /** 路径摘要保留结构,供最终渲染按实际宽度优先保留文件名。 */
51
59
  path?: { prefix: string; value: string };
60
+ /** 标题之外的原始载荷(完整入参 JSON、脚本代码…),已压成单行,渲染时用 dim 接在标题后。 */
61
+ payload?: string;
52
62
  };
53
63
 
54
64
  function pathApi(value: string) {
@@ -229,6 +239,18 @@ export function toolCallSummary(
229
239
  const variant = opts.variant ?? "default";
230
240
  if (!args || typeof args !== "object") return { main: title, detail: "" };
231
241
  const name = toolName.toLowerCase();
242
+
243
+ // MCP 入口沿用 mcp-adapter 自己的标题风格(adapter 的 formatMcpProxyToolCallLines):
244
+ // `MCP <动作> <目标>` / `MCP Script <代码>`,比把整包参数摊成 JSON 好读
245
+ if (name === "mcp") {
246
+ const gateway = mcpGatewaySummary(title, args);
247
+ if (gateway) return gateway;
248
+ }
249
+ if (name === "mcpscript") {
250
+ const code = typeof args.code === "string" && args.code ? clip(args.code) : "";
251
+ return code ? { main: title, detail: "", payload: code } : { main: title, detail: "" };
252
+ }
253
+
232
254
  const value = (fallback: string, ...keys: string[]) => {
233
255
  const found = keys.map((key) => args[key]).find((item) => typeof item === "string" && item);
234
256
  return `${title} ${clip(found || fallback)}`;
@@ -315,5 +337,85 @@ export function toolCallSummary(
315
337
  if (typeof preferredPath === "string" && preferredPath) {
316
338
  return pathSummary(title, preferredPath, opts.cwd);
317
339
  }
318
- return { main: title, detail: "" };
340
+
341
+ // 字段链认不出的参数(命名空间代理的 tool+args、第三方工具的自定义键…)展开完整入参 JSON,
342
+ // 而不是只剩一个标题;网关与脚本的专属形状在上面处理
343
+ const payload = jsonArgs(args);
344
+ return payload ? { main: title, detail: "", payload } : { main: title, detail: "" };
345
+ }
346
+
347
+ /**
348
+ * MCP 网关注解 `mcp <动作> <目标>`:动作与目标照搬 mcp-adapter 的 formatMcpProxyToolCallLines,
349
+ * 人眼读起来比参数 JSON 快;`instructions` 是 adapter 自己漏掉的一档(它那里会落到 mcp status)。
350
+ * 认不出的入参返回 undefined,交给通用 JSON 回退,不猜成 status。
351
+ */
352
+ function mcpGatewaySummary(title: string, args: any): ToolCallSummary | undefined {
353
+ const text = (value: unknown) => (typeof value === "string" && value ? value : "");
354
+ const tool = text(args.tool);
355
+ const connect = text(args.connect);
356
+ const describe = text(args.describe);
357
+ const instructions = text(args.instructions);
358
+ const search = text(args.search);
359
+ const server = text(args.server);
360
+ const action = text(args.action);
361
+ const scoped = (target: string) => (server ? `${target} @ ${server}` : target);
362
+
363
+ if (action === "ui-messages") return { main: `${title} ${action}`, detail: "" };
364
+ if (tool) {
365
+ const main = `${title} call ${scoped(tool)}`;
366
+ const payload = innerArgsPayload(args.args);
367
+ return payload ? { main, detail: "", payload } : { main, detail: "" };
368
+ }
369
+ if (connect) return { main: `${title} connect ${connect}`, detail: "" };
370
+ if (describe) return { main: `${title} describe ${scoped(describe)}`, detail: "" };
371
+ if (instructions) return { main: `${title} instructions ${instructions}`, detail: "" };
372
+ if (search) {
373
+ const flags = [
374
+ args.regex === true ? "regex" : "",
375
+ args.includeSchemas === false ? "schemas hidden" : "",
376
+ ].filter(Boolean);
377
+ return {
378
+ main: `${title} search ${scoped(search)}`,
379
+ detail: flags.length ? ` (${flags.join(", ")})` : "",
380
+ };
381
+ }
382
+ if (server) return { main: `${title} list ${server}`, detail: "" };
383
+ if (action) return { main: `${title} ${action}`, detail: "" };
384
+ if (Object.keys(args).length === 0) return { main: `${title} status`, detail: "" };
385
+ return undefined;
386
+ }
387
+
388
+ /** 网关 call 的内层工具入参:对象转单行 JSON;网关允许传 JSON 字符串,就原样取用不再转义。 */
389
+ function innerArgsPayload(value: unknown): string {
390
+ if (typeof value === "string") return value ? clip(value) : "";
391
+ if (!value || typeof value !== "object") return "";
392
+ return jsonArgs(value);
393
+ }
394
+
395
+ /** 入参序列化成单行 JSON;空对象或不可序列化时返回 ""。 */
396
+ function jsonArgs(args: any): string {
397
+ try {
398
+ const json = JSON.stringify(args);
399
+ return json && json !== "{}" ? clip(json) : "";
400
+ } catch {
401
+ return "";
402
+ }
403
+ }
404
+
405
+ /**
406
+ * 标题行内容:标题用调用方的 toolTitle 色,原始载荷(入参 JSON / 脚本代码)单独用 dim。
407
+ * 标题优先占宽(它是身份),剩下的宽度给载荷;载荷超宽时尾部省略,连
408
+ * MIN_PAYLOAD_WIDTH 都放不下时整段省略。
409
+ */
410
+ export function renderToolSummary(
411
+ summary: ToolCallSummary,
412
+ width: number,
413
+ fg: (color: ThemeColor, text: string) => string,
414
+ ): string {
415
+ const title = fitToolCallSummary(summary, width);
416
+ if (!summary.payload) return fg("toolTitle", title);
417
+ const room = width - visibleWidth(title);
418
+ return room >= MIN_PAYLOAD_WIDTH
419
+ ? `${fg("toolTitle", title)}${fg("dim", headTruncateToWidth(` ${summary.payload}`, room))}`
420
+ : fg("toolTitle", title);
319
421
  }
@@ -14,7 +14,7 @@ export function toolViewportWidth(width: number): number {
14
14
 
15
15
  /** 与默认工具结果相同的一级缩进包装。子组件只扣始终加上的 1 列;↳ 行多出的 2 列由 truncateToWidth 吃掉。 */
16
16
  export function insetComponent(component: any): any {
17
- return {
17
+ const wrapped: Record<string, unknown> = {
18
18
  render: (width: number) =>
19
19
  component.render(Math.max(1, width - 1)).map((line: string) => {
20
20
  const nestedMarker = line.replace(/^((?:\x1b\[[0-?]*[ -/]*[@-~])*)↳/, "$1 ↳");
@@ -22,6 +22,11 @@ export function insetComponent(component: any): any {
22
22
  }),
23
23
  invalidate: () => component.invalidate?.(),
24
24
  };
25
+ // 内层 diff 声明的 remainder 行:鼠标层只能看到这个包装组件。
26
+ if (typeof component.isCollapsedHintLine === "function") {
27
+ wrapped.isCollapsedHintLine = (line: string) => component.isCollapsedHintLine(line);
28
+ }
29
+ return wrapped;
25
30
  }
26
31
 
27
32
  function rawTextFromResult(result: any): string {
@@ -36,3 +36,13 @@ export function expandShortcutText(): string {
36
36
  export function showMoreHintText(): string {
37
37
  return isToolTuiFullscreen() ? "click to show more" : `${expandShortcutText()} to show more`;
38
38
  }
39
+
40
+ /** 展开卡的收起提示,与折叠态 showMoreHintText 同一位置对称。 */
41
+ export function collapseHintText(): string {
42
+ return isToolTuiFullscreen() ? "↑ Collapse" : `${expandShortcutText()} to collapse`;
43
+ }
44
+
45
+ /** 该行是否带展开态收起提示(用于展开卡的 hint hover 命中)。 */
46
+ export function isCollapseHintLine(plainLine: string): boolean {
47
+ return plainLine.includes(collapseHintText());
48
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cc-extensions",
3
- "version": "0.9.4",
3
+ "version": "0.9.6",
4
4
  "license": "MIT",
5
5
  "description": "Claude Code-style TUI output with some personal touches and handy utilities.",
6
6
  "type": "module",
@@ -38,6 +38,7 @@
38
38
  },
39
39
  "scripts": {
40
40
  "test": "node --test tests/**/*.test.ts",
41
+ "coverage": "node --test --experimental-test-coverage --test-coverage-include=extensions/**/*.ts tests/**/*.test.ts",
41
42
  "typecheck": "tsc --noEmit",
42
43
  "format": "biome format --write .",
43
44
  "format:check": "biome format .",