markindex 0.0.0-stage → 0.1.1
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 +65 -0
- package/LICENSE +21 -0
- package/README.md +288 -2
- package/README.zh-CN.md +164 -0
- package/THIRD_PARTY_NOTICES.md +36 -0
- package/dist/adapters/ai-sdk.d.ts +8 -0
- package/dist/adapters/ai-sdk.js +20 -0
- package/dist/adapters/mcp.d.ts +6 -0
- package/dist/adapters/mcp.js +28 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +297 -0
- package/dist/config.d.ts +18 -0
- package/dist/config.js +18 -0
- package/dist/eval/questions.d.ts +34 -0
- package/dist/eval/questions.js +80 -0
- package/dist/eval/run.d.ts +62 -0
- package/dist/eval/run.js +163 -0
- package/dist/eval/score.d.ts +74 -0
- package/dist/eval/score.js +104 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +23 -0
- package/dist/library/indexer.d.ts +89 -0
- package/dist/library/indexer.js +199 -0
- package/dist/library/scan.d.ts +29 -0
- package/dist/library/scan.js +106 -0
- package/dist/library.d.ts +30 -0
- package/dist/library.js +42 -0
- package/dist/llm/provider.d.ts +10 -0
- package/dist/llm/provider.js +32 -0
- package/dist/llm/rate-limit.d.ts +33 -0
- package/dist/llm/rate-limit.js +95 -0
- package/dist/llm/summarize.d.ts +67 -0
- package/dist/llm/summarize.js +193 -0
- package/dist/parse/markdown.d.ts +38 -0
- package/dist/parse/markdown.js +58 -0
- package/dist/retrieve.d.ts +65 -0
- package/dist/retrieve.js +174 -0
- package/dist/store/db.d.ts +50 -0
- package/dist/store/db.js +399 -0
- package/dist/store/search.d.ts +27 -0
- package/dist/store/search.js +116 -0
- package/dist/text/segment.d.ts +4 -0
- package/dist/text/segment.js +37 -0
- package/dist/text/tokens.d.ts +3 -0
- package/dist/text/tokens.js +13 -0
- package/dist/tools/contract.d.ts +51 -0
- package/dist/tools/contract.js +159 -0
- package/dist/tools/envelope.d.ts +31 -0
- package/dist/tools/envelope.js +62 -0
- package/dist/tools/impl.d.ts +7 -0
- package/dist/tools/impl.js +441 -0
- package/dist/tools/similar.d.ts +8 -0
- package/dist/tools/similar.js +40 -0
- package/dist/tree/build.d.ts +27 -0
- package/dist/tree/build.js +139 -0
- package/dist/tree/optimize.d.ts +39 -0
- package/dist/tree/optimize.js +185 -0
- package/dist/types.d.ts +27 -0
- package/dist/types.js +1 -0
- package/package.json +88 -4
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ParsedDoc } from '../parse/markdown.js';
|
|
2
|
+
import { type TokenCounter } from '../text/tokens.js';
|
|
3
|
+
import type { TreeNode } from '../types.js';
|
|
4
|
+
export interface OptimizeTreeOptions {
|
|
5
|
+
/** Expand threshold, and upper bound for a node created by merge. */
|
|
6
|
+
maxNodeTokens?: number;
|
|
7
|
+
/** R(v): constant cost of routing through a node (title, summary, child descriptions). */
|
|
8
|
+
routingCost?: number;
|
|
9
|
+
/** Must be the counter the tree was built with. */
|
|
10
|
+
countTokens?: TokenCounter;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Token-cost tree optimization, ported from PageIndex `tree_optimize.py` with pages -> tokens (spec §5.3).
|
|
14
|
+
*
|
|
15
|
+
* S(v) = v.tokens (whole subtree)
|
|
16
|
+
* R(v) = routingCost
|
|
17
|
+
* S_residual(v) = v.ownTokens
|
|
18
|
+
* tree_cost(v) = S(v) v is a leaf
|
|
19
|
+
* = R(v) + max(S_residual(v), max_c tree_cost(c)) otherwise
|
|
20
|
+
*
|
|
21
|
+
* Pure: returns a new tree, the input is not mutated. Retained nodes keep their nodeId.
|
|
22
|
+
*
|
|
23
|
+
* 1. expand (no LLM): every node whose own text exceeds `maxNodeTokens` is split deterministically.
|
|
24
|
+
* The own range is packed greedily into segments of <= maxNodeTokens; the first segment stays the
|
|
25
|
+
* node's own text, the others become `chunk` children inserted before any existing children
|
|
26
|
+
* (so a non-leaf with a huge intro is handled too). nodeId = parent id + `-c` + 1-based ordinal.
|
|
27
|
+
* Split points, coarsest first: starts of top-level mdast blocks (a block plus the blank lines after
|
|
28
|
+
* it is atomic); only a block that alone exceeds the limit is split by lines, and then never inside a
|
|
29
|
+
* fenced code block nested in it (e.g. in a list item); only a fenced region that alone exceeds the
|
|
30
|
+
* limit (including a top-level code block) is split at arbitrary lines. A single line is never split,
|
|
31
|
+
* so one line above the limit yields an oversized segment.
|
|
32
|
+
* 2. merge, bottom-up: merge(v) iff S(v) <= tree_cost(v) and S(v) <= maxNodeTokens. The second clause
|
|
33
|
+
* is an addition to PageIndex: merging must not recreate a node that expand would split again.
|
|
34
|
+
* Removed descendant titles (and their own keyItems) go to v.keyItems in pre-order.
|
|
35
|
+
*
|
|
36
|
+
* Expand runs first so that a large intro next to small children is chunked instead of swallowing the
|
|
37
|
+
* children's headings; chunks are never merged back because their parent always exceeds the limit.
|
|
38
|
+
*/
|
|
39
|
+
export declare function optimizeTree(roots: TreeNode[], doc: ParsedDoc, opts?: OptimizeTreeOptions): TreeNode[];
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { MAX_NODE_TOKENS, ROUTING_COST_TOKENS } from '../config.js';
|
|
2
|
+
import { approxTokens } from '../text/tokens.js';
|
|
3
|
+
const CHUNK_TITLE_CHARS = 60;
|
|
4
|
+
/**
|
|
5
|
+
* Token-cost tree optimization, ported from PageIndex `tree_optimize.py` with pages -> tokens (spec §5.3).
|
|
6
|
+
*
|
|
7
|
+
* S(v) = v.tokens (whole subtree)
|
|
8
|
+
* R(v) = routingCost
|
|
9
|
+
* S_residual(v) = v.ownTokens
|
|
10
|
+
* tree_cost(v) = S(v) v is a leaf
|
|
11
|
+
* = R(v) + max(S_residual(v), max_c tree_cost(c)) otherwise
|
|
12
|
+
*
|
|
13
|
+
* Pure: returns a new tree, the input is not mutated. Retained nodes keep their nodeId.
|
|
14
|
+
*
|
|
15
|
+
* 1. expand (no LLM): every node whose own text exceeds `maxNodeTokens` is split deterministically.
|
|
16
|
+
* The own range is packed greedily into segments of <= maxNodeTokens; the first segment stays the
|
|
17
|
+
* node's own text, the others become `chunk` children inserted before any existing children
|
|
18
|
+
* (so a non-leaf with a huge intro is handled too). nodeId = parent id + `-c` + 1-based ordinal.
|
|
19
|
+
* Split points, coarsest first: starts of top-level mdast blocks (a block plus the blank lines after
|
|
20
|
+
* it is atomic); only a block that alone exceeds the limit is split by lines, and then never inside a
|
|
21
|
+
* fenced code block nested in it (e.g. in a list item); only a fenced region that alone exceeds the
|
|
22
|
+
* limit (including a top-level code block) is split at arbitrary lines. A single line is never split,
|
|
23
|
+
* so one line above the limit yields an oversized segment.
|
|
24
|
+
* 2. merge, bottom-up: merge(v) iff S(v) <= tree_cost(v) and S(v) <= maxNodeTokens. The second clause
|
|
25
|
+
* is an addition to PageIndex: merging must not recreate a node that expand would split again.
|
|
26
|
+
* Removed descendant titles (and their own keyItems) go to v.keyItems in pre-order.
|
|
27
|
+
*
|
|
28
|
+
* Expand runs first so that a large intro next to small children is chunked instead of swallowing the
|
|
29
|
+
* children's headings; chunks are never merged back because their parent always exceeds the limit.
|
|
30
|
+
*/
|
|
31
|
+
export function optimizeTree(roots, doc, opts = {}) {
|
|
32
|
+
const ctx = {
|
|
33
|
+
doc,
|
|
34
|
+
max: opts.maxNodeTokens ?? MAX_NODE_TOKENS,
|
|
35
|
+
routing: opts.routingCost ?? ROUTING_COST_TOKENS,
|
|
36
|
+
count: opts.countTokens ?? approxTokens,
|
|
37
|
+
};
|
|
38
|
+
return roots.map((root) => {
|
|
39
|
+
const node = expand(structuredClone(root), ctx);
|
|
40
|
+
merge(node, ctx);
|
|
41
|
+
return node;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
const slice = (doc, from, to) => doc.lines.slice(from - 1, to).join('\n');
|
|
45
|
+
// ---------------------------------------------------------------- expand
|
|
46
|
+
function expand(node, ctx) {
|
|
47
|
+
node.children = node.children.map((c) => expand(c, ctx));
|
|
48
|
+
if (node.ownTokens <= ctx.max)
|
|
49
|
+
return node;
|
|
50
|
+
const segments = pack({ start: node.startLine, end: node.ownEndLine }, ctx);
|
|
51
|
+
if (segments.length < 2)
|
|
52
|
+
return node;
|
|
53
|
+
const [own, ...rest] = segments;
|
|
54
|
+
node.ownEndLine = own.end;
|
|
55
|
+
node.ownTokens = ctx.count(slice(ctx.doc, own.start, own.end));
|
|
56
|
+
const chunks = rest.map((r, i) => {
|
|
57
|
+
const tokens = ctx.count(slice(ctx.doc, r.start, r.end));
|
|
58
|
+
return {
|
|
59
|
+
nodeId: `${node.nodeId}-c${i + 1}`,
|
|
60
|
+
title: chunkTitle(ctx.doc.lines.slice(r.start - 1, r.end)) || `Part ${i + 1}`,
|
|
61
|
+
kind: 'chunk',
|
|
62
|
+
level: node.level + 1,
|
|
63
|
+
startLine: r.start,
|
|
64
|
+
endLine: r.end,
|
|
65
|
+
ownEndLine: r.end,
|
|
66
|
+
tokens,
|
|
67
|
+
ownTokens: tokens,
|
|
68
|
+
children: [],
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
node.children = [...chunks, ...node.children];
|
|
72
|
+
return node;
|
|
73
|
+
}
|
|
74
|
+
/** Splits `range` into consecutive segments of <= ctx.max tokens where possible. */
|
|
75
|
+
function pack(range, ctx) {
|
|
76
|
+
const blockStarts = ctx.doc.blocks.map((b) => b.startLine).filter((l) => l > range.start && l <= range.end);
|
|
77
|
+
const atoms = cut(range, blockStarts).flatMap((r) => refine(r, ctx));
|
|
78
|
+
const segments = [];
|
|
79
|
+
let cur = [];
|
|
80
|
+
let estimate = 0;
|
|
81
|
+
const flush = () => {
|
|
82
|
+
// The additive estimate is an upper bound for the default counter; verify against the real counter.
|
|
83
|
+
while (cur.length > 0) {
|
|
84
|
+
let n = cur.length;
|
|
85
|
+
while (n > 1 && ctx.count(slice(ctx.doc, cur[0].start, cur[n - 1].end)) > ctx.max)
|
|
86
|
+
n--;
|
|
87
|
+
segments.push({ start: cur[0].start, end: cur[n - 1].end });
|
|
88
|
+
cur = cur.slice(n);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
for (const atom of atoms) {
|
|
92
|
+
const t = ctx.count(slice(ctx.doc, atom.start, atom.end));
|
|
93
|
+
if (cur.length > 0 && estimate + 1 + t > ctx.max) {
|
|
94
|
+
flush();
|
|
95
|
+
estimate = 0;
|
|
96
|
+
}
|
|
97
|
+
estimate = cur.length === 0 ? t : estimate + 1 + t;
|
|
98
|
+
cur.push(atom);
|
|
99
|
+
}
|
|
100
|
+
flush();
|
|
101
|
+
return segments;
|
|
102
|
+
}
|
|
103
|
+
/** Ranges starting at range.start and at each of `points` (sorted, inside (start, end]). */
|
|
104
|
+
function cut(range, points) {
|
|
105
|
+
const starts = [range.start, ...points];
|
|
106
|
+
return starts.map((s, i) => ({ start: s, end: i + 1 < starts.length ? starts[i + 1] - 1 : range.end }));
|
|
107
|
+
}
|
|
108
|
+
/** An atom above the limit is cut at lines outside fenced code, then (fence too big) at every line. */
|
|
109
|
+
function refine(atom, ctx) {
|
|
110
|
+
if (atom.end === atom.start || ctx.count(slice(ctx.doc, atom.start, atom.end)) <= ctx.max)
|
|
111
|
+
return [atom];
|
|
112
|
+
const outside = cut(atom, linesOutsideFences(ctx.doc, atom));
|
|
113
|
+
return outside.flatMap((r) => r.end > r.start && ctx.count(slice(ctx.doc, r.start, r.end)) > ctx.max
|
|
114
|
+
? cut(r, range(r.start + 1, r.end))
|
|
115
|
+
: [r]);
|
|
116
|
+
}
|
|
117
|
+
const FENCE = /^\s*(`{3,}|~{3,})/;
|
|
118
|
+
/** Lines in (atom.start, atom.end] that do not lie inside a fenced code block (closing fence counts as inside). */
|
|
119
|
+
function linesOutsideFences(doc, atom) {
|
|
120
|
+
const legal = [];
|
|
121
|
+
let open;
|
|
122
|
+
for (let l = atom.start; l <= atom.end; l++) {
|
|
123
|
+
if (l > atom.start && open === undefined)
|
|
124
|
+
legal.push(l);
|
|
125
|
+
const m = FENCE.exec(doc.lines[l - 1]);
|
|
126
|
+
if (!m)
|
|
127
|
+
continue;
|
|
128
|
+
const fence = m[1];
|
|
129
|
+
if (open === undefined)
|
|
130
|
+
open = fence;
|
|
131
|
+
else if (fence[0] === open[0] && fence.length >= open.length && doc.lines[l - 1].trim() === fence)
|
|
132
|
+
open = undefined;
|
|
133
|
+
}
|
|
134
|
+
return legal;
|
|
135
|
+
}
|
|
136
|
+
const range = (from, to) => Array.from({ length: to - from + 1 }, (_, i) => from + i);
|
|
137
|
+
/** First non-empty line with markdown syntax removed, truncated to 60 characters. */
|
|
138
|
+
function chunkTitle(lines) {
|
|
139
|
+
for (const raw of lines) {
|
|
140
|
+
if (FENCE.test(raw))
|
|
141
|
+
continue;
|
|
142
|
+
const t = raw
|
|
143
|
+
.replace(/^\s*(?:>\s*)+/, '')
|
|
144
|
+
.replace(/^\s*#{1,6}\s+/, '')
|
|
145
|
+
.replace(/^\s*(?:[-*+]|\d+[.)])\s+/, '')
|
|
146
|
+
.replace(/^\[[ xX]\]\s+/, '')
|
|
147
|
+
.replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1')
|
|
148
|
+
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
|
|
149
|
+
.replace(/\[([^\]]*)\]\[[^\]]*\]/g, '$1')
|
|
150
|
+
.replace(/<[^>]+>/g, '')
|
|
151
|
+
.replace(/\*\*|__|~~|[*`]/g, '')
|
|
152
|
+
.replace(/\|/g, ' ')
|
|
153
|
+
.replace(/\s+/g, ' ')
|
|
154
|
+
.trim();
|
|
155
|
+
if (t !== '' && !/^[-:\s]+$/.test(t))
|
|
156
|
+
return Array.from(t).slice(0, CHUNK_TITLE_CHARS).join('').trimEnd();
|
|
157
|
+
}
|
|
158
|
+
return '';
|
|
159
|
+
}
|
|
160
|
+
// ---------------------------------------------------------------- merge
|
|
161
|
+
/** Bottom-up merge; returns tree_cost(node) after the decision. */
|
|
162
|
+
function merge(node, ctx) {
|
|
163
|
+
if (node.children.length === 0)
|
|
164
|
+
return node.tokens;
|
|
165
|
+
const childCost = Math.max(...node.children.map((c) => merge(c, ctx)));
|
|
166
|
+
const treeCost = ctx.routing + Math.max(node.ownTokens, childCost);
|
|
167
|
+
if (node.tokens > treeCost || node.tokens > ctx.max)
|
|
168
|
+
return treeCost;
|
|
169
|
+
const keyItems = [...(node.keyItems ?? [])];
|
|
170
|
+
const collect = (nodes) => {
|
|
171
|
+
for (const c of nodes) {
|
|
172
|
+
if (c.kind !== 'chunk')
|
|
173
|
+
keyItems.push(c.title);
|
|
174
|
+
keyItems.push(...(c.keyItems ?? []));
|
|
175
|
+
collect(c.children);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
collect(node.children);
|
|
179
|
+
node.children = [];
|
|
180
|
+
node.ownEndLine = node.endLine;
|
|
181
|
+
node.ownTokens = node.tokens;
|
|
182
|
+
if (keyItems.length > 0)
|
|
183
|
+
node.keyItems = keyItems;
|
|
184
|
+
return node.tokens;
|
|
185
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type NodeKind = 'preface' | 'section' | 'chunk';
|
|
2
|
+
export interface TreeNode {
|
|
3
|
+
nodeId: string;
|
|
4
|
+
title: string;
|
|
5
|
+
kind: NodeKind;
|
|
6
|
+
level: number;
|
|
7
|
+
startLine: number;
|
|
8
|
+
endLine: number;
|
|
9
|
+
ownEndLine: number;
|
|
10
|
+
tokens: number;
|
|
11
|
+
ownTokens: number;
|
|
12
|
+
summary?: string;
|
|
13
|
+
prefixSummary?: string;
|
|
14
|
+
keyItems?: string[];
|
|
15
|
+
children: TreeNode[];
|
|
16
|
+
}
|
|
17
|
+
export interface DocumentRecord {
|
|
18
|
+
docId: string;
|
|
19
|
+
path: string;
|
|
20
|
+
title: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
metadata: Record<string, unknown>;
|
|
23
|
+
contentHash: string;
|
|
24
|
+
lineCount: number;
|
|
25
|
+
tokens: number;
|
|
26
|
+
indexedAt: string;
|
|
27
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,90 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "markindex",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"mcpName": "io.github.jackjin1997/markindex",
|
|
5
|
+
"description": "Vectorless, structure-first retrieval over large Markdown libraries for agents: section trees, mixed Chinese/English BM25 in SQLite, five read-only tools (AI SDK, MCP) and a built-in evidence loop",
|
|
6
|
+
"author": "Zexu Jin",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/jackjin1997/markindex.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/jackjin1997/markindex#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/jackjin1997/markindex/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"markdown",
|
|
17
|
+
"rag",
|
|
18
|
+
"vectorless",
|
|
19
|
+
"retrieval",
|
|
20
|
+
"mcp",
|
|
21
|
+
"mcp-server",
|
|
22
|
+
"ai-agents",
|
|
23
|
+
"pageindex",
|
|
24
|
+
"bm25",
|
|
25
|
+
"sqlite",
|
|
26
|
+
"fts5",
|
|
27
|
+
"typescript",
|
|
28
|
+
"chinese"
|
|
29
|
+
],
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"type": "module",
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=22.12"
|
|
34
|
+
},
|
|
35
|
+
"bin": {
|
|
36
|
+
"markindex": "dist/cli.js"
|
|
37
|
+
},
|
|
38
|
+
"main": "dist/index.js",
|
|
39
|
+
"types": "dist/index.d.ts",
|
|
40
|
+
"exports": {
|
|
41
|
+
".": {
|
|
42
|
+
"types": "./dist/index.d.ts",
|
|
43
|
+
"import": "./dist/index.js"
|
|
44
|
+
},
|
|
45
|
+
"./package.json": "./package.json"
|
|
46
|
+
},
|
|
47
|
+
"files": [
|
|
48
|
+
"dist",
|
|
49
|
+
"README.md",
|
|
50
|
+
"README.zh-CN.md",
|
|
51
|
+
"LICENSE",
|
|
52
|
+
"THIRD_PARTY_NOTICES.md",
|
|
53
|
+
"CHANGELOG.md"
|
|
54
|
+
],
|
|
55
|
+
"publishConfig": {
|
|
56
|
+
"registry": "https://registry.npmjs.org/",
|
|
57
|
+
"access": "public"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"build": "tsc -p tsconfig.build.json",
|
|
61
|
+
"test": "vitest run",
|
|
62
|
+
"typecheck": "tsc --noEmit",
|
|
63
|
+
"prepublishOnly": "npm test",
|
|
64
|
+
"prepack": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && npm run build"
|
|
65
|
+
},
|
|
66
|
+
"dependencies": {
|
|
67
|
+
"@modelcontextprotocol/sdk": "^1.32.0",
|
|
68
|
+
"@types/better-sqlite3": "^9.6.0",
|
|
69
|
+
"ai": "^7.0.150",
|
|
70
|
+
"better-sqlite3": "^13.0.3",
|
|
71
|
+
"commander": "^15.0.0",
|
|
72
|
+
"ignore": "^7.0.13",
|
|
73
|
+
"mdast-util-from-markdown": "^2.1.0",
|
|
74
|
+
"mdast-util-frontmatter": "^2.0.1",
|
|
75
|
+
"mdast-util-gfm": "^3.1.0",
|
|
76
|
+
"mdast-util-to-string": "^4.0.0",
|
|
77
|
+
"micromark-extension-frontmatter": "^2.0.0",
|
|
78
|
+
"micromark-extension-gfm": "^3.0.0",
|
|
79
|
+
"yaml": "^2.9.1",
|
|
80
|
+
"zod": "^4.6.7"
|
|
81
|
+
},
|
|
82
|
+
"devDependencies": {
|
|
83
|
+
"@types/node": "^26.6.5",
|
|
84
|
+
"@types/turndown": "^5.0.6",
|
|
85
|
+
"tsx": "^4.23.15",
|
|
86
|
+
"turndown": "^7.2.4",
|
|
87
|
+
"typescript": "^7.0.2",
|
|
88
|
+
"vitest": "^5.0.5"
|
|
89
|
+
}
|
|
90
|
+
}
|