markindex 0.0.0-stage → 0.1.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/CHANGELOG.md +54 -0
- package/LICENSE +21 -0
- package/README.md +281 -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 +87 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
|
|
6
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-10-26
|
|
11
|
+
|
|
12
|
+
First release.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- Markdown section parser built on mdast: setext and ATX headings, preface before the first heading, documents
|
|
17
|
+
without headings, front matter, `#` inside code blocks, skipped heading levels, BOM and CR / CRLF / LF line
|
|
18
|
+
endings; every source line belongs to exactly one node. Heading titles are capped at 300 code points and text is
|
|
19
|
+
NFKC-normalized before segmentation.
|
|
20
|
+
- Section tree with a deterministic optimization pass (a port of PageIndex's merge / expand cost model, with pages
|
|
21
|
+
replaced by tokens) and stable node ids; oversized sections are split at block boundaries into `chunk` nodes.
|
|
22
|
+
- SQLite store (one file per library, WAL) holding the trees, the original lines and a mixed Chinese / English
|
|
23
|
+
BM25 index (`Intl.Segmenter` words plus a trigram substring fallback). Incremental indexing by content hash, one
|
|
24
|
+
transaction per document, a single-writer lock, and an index format fingerprint that triggers a full rebuild
|
|
25
|
+
(reusing cached summaries) when the format or tree parameters change.
|
|
26
|
+
- Five read-only agent tools (`browse_documents`, `get_document`, `get_document_structure`, `get_node_content`,
|
|
27
|
+
`search_documents`) with JSON envelopes, `next_steps` guidance, a 95 000-character response budget and a
|
|
28
|
+
1 024-character cap on string arguments; adapters for the Vercel AI SDK and for MCP (stdio server).
|
|
29
|
+
- Optional LLM node summaries and document descriptions through any AI SDK provider (`--summary-model
|
|
30
|
+
<provider>:<model>`), cached by node content hash, with a cost estimate (`--estimate`) that makes no LLM calls.
|
|
31
|
+
- Built-in `retrieve()` evidence loop: returns original text re-read from the index by line range, usage and a stop
|
|
32
|
+
reason (`done`, `budget`, `max_steps`), never a model-written answer.
|
|
33
|
+
- Client-side LLM rate limiting: a dependency-free sliding 60 s window for requests and estimated input tokens
|
|
34
|
+
(FIFO, no deadlock for oversized requests). `rateLimit` option for summaries and `retrieve()`; CLI flags
|
|
35
|
+
`index --summary-rpm / --summary-tpm` and `eval --rpm / --tpm`.
|
|
36
|
+
- Setext heading switch: `--setext` / `--no-setext` (library: `setext`) for converter output that uses `---` as a
|
|
37
|
+
page break, with warnings when suspect setext headings are found; the setting is stored in the index.
|
|
38
|
+
- Ignore support: `.gitignore` files at any level, an optional root `.markindexignore`, and repeatable
|
|
39
|
+
`--ignore <pattern>` options (library: `ignore`).
|
|
40
|
+
- CLI: `index`, `tree`, `search`, `mcp` (with `--reindex-on-start`) and `eval`.
|
|
41
|
+
- Evaluation harness: a 151-question zh / en / mixed question set over a public 329-document corpus, a BM25 baseline,
|
|
42
|
+
agent mode through `retrieve()`, evidence size and precision metrics (`--max-evidence-lines`), scale benchmarks,
|
|
43
|
+
and a Claude Code agent-mode runner restricted to the MarkIndex tools (30/30 on the n = 30 run).
|
|
44
|
+
- English and Chinese READMEs, third-party notices, and GitHub Actions CI on Ubuntu and macOS with Node 22 and 24.
|
|
45
|
+
|
|
46
|
+
### Notes
|
|
47
|
+
|
|
48
|
+
- Requires Node.js >= 22.12. LLM features need an [AI SDK provider](https://ai-sdk.dev/providers) package such as
|
|
49
|
+
`@ai-sdk/anthropic` installed alongside.
|
|
50
|
+
- MarkIndex is an independent, unofficial implementation inspired by
|
|
51
|
+
[VectifyAI/PageIndex](https://github.com/VectifyAI/PageIndex); see `THIRD_PARTY_NOTICES.md`.
|
|
52
|
+
|
|
53
|
+
[Unreleased]: https://github.com/jackjin1997/markindex/compare/v0.1.0...HEAD
|
|
54
|
+
[0.1.0]: https://github.com/jackjin1997/markindex/releases/tag/v0.1.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Zexu Jin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,282 @@
|
|
|
1
|
-
#
|
|
1
|
+
# MarkIndex
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/jackjin1997/markindex/actions/workflows/ci.yml) [](https://www.npmjs.com/package/markindex)
|
|
4
|
+
|
|
5
|
+
**Vectorless, structure-first retrieval over large libraries of long Markdown documents — for agents.**
|
|
6
|
+
|
|
7
|
+
MarkIndex parses every Markdown file into a tree of its own sections, stores the trees, the original lines and a
|
|
8
|
+
mixed Chinese/English BM25 index in one SQLite file, and gives an agent five read-only tools to walk
|
|
9
|
+
"library → folder → document → section" and read the original text with line numbers. No embeddings, no vector
|
|
10
|
+
database, no chunking guesswork: the document's own structure is the index, and every piece of evidence can be
|
|
11
|
+
cited back as `[doc_path, L<start>-<end>]`.
|
|
12
|
+
|
|
13
|
+
> Inspired by [VectifyAI/PageIndex](https://github.com/VectifyAI/PageIndex), MarkIndex is an independent,
|
|
14
|
+
> unofficial TypeScript implementation focused on Markdown. It is not affiliated with or endorsed by VectifyAI.
|
|
15
|
+
|
|
16
|
+
中文说明:[README.zh-CN.md](README.zh-CN.md)
|
|
17
|
+
|
|
18
|
+
## Why
|
|
19
|
+
|
|
20
|
+
- **Structure-first, not chunk-first.** Long professional documents (filings, guidelines, laws, manuals) are
|
|
21
|
+
already organized by their authors. Navigating that outline keeps context intact and makes answers explainable
|
|
22
|
+
and citable; arbitrary chunks + vector similarity cut sections apart.
|
|
23
|
+
- **Deterministic and LLM-free indexing.** Parsing (mdast, not regex), tree building and the token-cost tree
|
|
24
|
+
optimization never call a model; the same input always produces the same tree. LLMs are optional and only used
|
|
25
|
+
for node summaries, document descriptions and the built-in `retrieve()` loop.
|
|
26
|
+
- **Markdown done properly.** Setext headings, preface text before the first heading, documents without any
|
|
27
|
+
heading, front matter, `#` inside code fences, skipped heading levels, BOM / CRLF — every source line belongs to
|
|
28
|
+
exactly one node, and oversized sections are split at block boundaries into `chunk` nodes.
|
|
29
|
+
- **Keyword fallback that works for Chinese.** BM25 over `Intl.Segmenter` word segmentation (Chinese, English and
|
|
30
|
+
mixed text, identifiers like `C++`, `ISO-27001`, `v2.1`), plus a trigram substring index as a second pass.
|
|
31
|
+
- **Budgeted tool responses.** Tools never throw; they return JSON envelopes with `next_steps` guidance and stay
|
|
32
|
+
under a 95 000-character budget, paginating large outlines and sections.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install markindex # library + CLI (Node.js >= 22.12)
|
|
38
|
+
npx markindex --help
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
From source:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
git clone https://github.com/jackjin1997/markindex.git && cd markindex
|
|
45
|
+
npm install && npm run build
|
|
46
|
+
node dist/cli.js --help # or `npm link` to get a `markindex` command
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Optional, only for LLM features (summaries, `retrieve()`, agent-mode evaluation): install the
|
|
50
|
+
[AI SDK provider](https://ai-sdk.dev/providers) you want, e.g. `npm install @ai-sdk/anthropic`, and set its API key
|
|
51
|
+
(`ANTHROPIC_API_KEY`, …). With `npx` and no local install, put the provider in the same temporary environment:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npx -p markindex -p @ai-sdk/anthropic markindex index ./docs --summary-model anthropic:claude-sonnet-5-5
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## CLI
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Build or incrementally update the index (stored in <root>/.markindex/index.db)
|
|
61
|
+
markindex index ./docs
|
|
62
|
+
markindex index ./docs --summary-model anthropic:claude-sonnet-5-5 --estimate # cost estimate, no LLM calls
|
|
63
|
+
markindex index ./docs --summary-model anthropic:claude-sonnet-5-5 # with node summaries
|
|
64
|
+
markindex index ./docs --summary-model anthropic:claude-sonnet-5-5 --summary-rpm 50 --summary-tpm 40000 # rate-limited
|
|
65
|
+
markindex index ./docs --json # machine-readable report
|
|
66
|
+
markindex index ./docs --no-setext # converter output with `---` page breaks
|
|
67
|
+
markindex index ./docs --ignore 'drafts/' --ignore '*.tmp.md' # extra ignore patterns (repeatable)
|
|
68
|
+
|
|
69
|
+
# Inspect one document's section tree
|
|
70
|
+
markindex tree ./docs guides/setup.md --depth 3
|
|
71
|
+
|
|
72
|
+
# Keyword search (BM25 + trigram fallback)
|
|
73
|
+
markindex search ./docs "离职 补偿"
|
|
74
|
+
markindex search ./docs "ISO-27001 audit" --folder policies --limit 5 --json
|
|
75
|
+
|
|
76
|
+
# Serve the library to agents over MCP (stdio, read-only)
|
|
77
|
+
markindex mcp ./docs
|
|
78
|
+
markindex mcp ./docs --reindex-on-start
|
|
79
|
+
|
|
80
|
+
# Measure retrieval quality on your own documents (see "Evaluation")
|
|
81
|
+
markindex eval ./docs --questions questions.jsonl --mode bm25 --out report.json
|
|
82
|
+
markindex eval ./docs --questions questions.jsonl --mode agent --model anthropic:claude-sonnet-5-5 --out report.json
|
|
83
|
+
markindex eval ./docs --questions questions.jsonl --mode agent --model anthropic:claude-sonnet-5-5 --rpm 50 --tpm 40000
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Rate limits.** Calls are always bounded by concurrency (`--concurrency` for eval; 8 for summaries) and the AI SDK's
|
|
87
|
+
retries, which already honor `retry-after` / `retry-after-ms` on 429 / 5xx responses (up to 60 s). To stay under a
|
|
88
|
+
provider quota, add a client-side limit: `--summary-rpm` / `--summary-tpm` on `index`, `--rpm` / `--tpm` on `eval`
|
|
89
|
+
(agent mode). `rpm` caps requests and `tpm` caps *estimated input* tokens in any sliding 60 s window; both are
|
|
90
|
+
shared by all documents (index) or all questions (eval) of the run, and waiting callers are served in order. Input
|
|
91
|
+
tokens are estimated from the prompt text (about 1 token per CJK character, otherwise 4 characters), so leave
|
|
92
|
+
headroom below the real quota; output tokens and tool definitions are not counted. A single request larger than the
|
|
93
|
+
`tpm` still goes through, alone. In the library, pass `rateLimit: { requestsPerMinute, inputTokensPerMinute }` to
|
|
94
|
+
`summarizeDocument` / `indexLibrary({ summary })` or `retrieve()`; reuse the same object to share one budget.
|
|
95
|
+
|
|
96
|
+
`--summary-model` / `--model` take `<provider>:<model>`; MarkIndex imports `@ai-sdk/<provider>` dynamically, so
|
|
97
|
+
only the provider you use needs to be installed. Every command accepts `--db <file>` to use a different index file.
|
|
98
|
+
Put the index on a local disk: the writer lock uses hard links and SQLite runs in WAL mode, neither of which works
|
|
99
|
+
reliably on exFAT, SMB / NFS shares or other network volumes. If the documents live there, keep them in place and
|
|
100
|
+
point `--db` at a local path (e.g. `--db ~/.cache/markindex/docs.db`).
|
|
101
|
+
|
|
102
|
+
Indexing is incremental: files are compared by content hash, only new or changed documents are rebuilt (one
|
|
103
|
+
transaction per document), deleted files are removed, and summaries are cached by node content hash. Scanned files
|
|
104
|
+
are `.md` and `.markdown` (not `.mdx`). Ignored are: paths matched by the library's own `.gitignore` files (at any
|
|
105
|
+
level under the root; global git excludes and `.git/info/exclude` are not read), by an optional `.markindexignore`
|
|
106
|
+
file at the root (same syntax, for rules you don't want in `.gitignore`) and by `--ignore <pattern>` options
|
|
107
|
+
(gitignore syntax, relative to the root, repeatable; `ignore: string[]` in the library API; also accepted by
|
|
108
|
+
`mcp --reindex-on-start`; a leading `./` is dropped, a leading `/` anchors at the root as in gitignore, and
|
|
109
|
+
absolute filesystem paths or `../` are rejected). The sources only add exclusions: a `!` rule in `.markindexignore`
|
|
110
|
+
or `--ignore` cannot re-include a path that a `.gitignore` excludes. A document that becomes ignored is removed
|
|
111
|
+
from the index on the next run.
|
|
112
|
+
Dot-directories such as `.git` and `.markindex` are skipped and symlinks are not followed. One writer at a time (a
|
|
113
|
+
second `index` fails fast); readers such as the MCP server are never blocked. When the index was built by an older MarkIndex version or with different
|
|
114
|
+
tree parameters, the next `index` run rebuilds every document (`full rebuild: …` in the output, `rebuilt` in the
|
|
115
|
+
report); cached summaries are reused.
|
|
116
|
+
|
|
117
|
+
**Page breaks from PDF / HTML converters.** Converters often emit a bare `---` line right after a page's last
|
|
118
|
+
paragraph. CommonMark reads that as a setext heading, so the whole paragraph becomes a level-2 heading. `index`
|
|
119
|
+
warns when it sees setext h2 headings whose text spans several lines or exceeds 120 characters (`warning <path>: …`
|
|
120
|
+
on stdout, `warnings` in the JSON report). For such libraries pass `--no-setext` (also on
|
|
121
|
+
`mcp --reindex-on-start`; `setext: false` in the library API): `---` then is a thematic break and `===` plain text.
|
|
122
|
+
The setting is stored in the index: later runs without the flag (`index`, `mcp --reindex-on-start`,
|
|
123
|
+
`Library.index()`) keep it; pass `--setext` (`setext: true`) to turn it back on. Changing it rebuilds the index.
|
|
124
|
+
|
|
125
|
+
## Library usage
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
import { openLibrary } from 'markindex'
|
|
129
|
+
|
|
130
|
+
// Index (or update) a folder, then open it read-only for agents.
|
|
131
|
+
const writer = openLibrary({ root: './docs', readonly: false })
|
|
132
|
+
await writer.index() // { added, updated, removed, unchanged, skipped, summaryErrors, warnings }
|
|
133
|
+
writer.close()
|
|
134
|
+
|
|
135
|
+
const lib = openLibrary({ root: './docs' })
|
|
136
|
+
const envelope = await lib.callTool('search_documents', { query: 'termination notice period' })
|
|
137
|
+
console.log(JSON.parse(envelope)) // { success: true, data: { results: [...] }, next_steps: {...} }
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### With the Vercel AI SDK (your agent drives the tools)
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
import { generateText, stepCountIs } from 'ai'
|
|
144
|
+
import { anthropic } from '@ai-sdk/anthropic'
|
|
145
|
+
import { AGENT_INSTRUCTIONS, createAiSdkTools, openLibrary } from 'markindex'
|
|
146
|
+
|
|
147
|
+
const lib = openLibrary({ root: './docs' })
|
|
148
|
+
const { text } = await generateText({
|
|
149
|
+
model: anthropic('claude-sonnet-5-5'),
|
|
150
|
+
system: AGENT_INSTRUCTIONS, // browse first, read structure, cite [doc_path, L<start>-<end>]
|
|
151
|
+
tools: createAiSdkTools(lib),
|
|
152
|
+
stopWhen: stepCountIs(15),
|
|
153
|
+
prompt: 'What notice period applies when an employee resigns?',
|
|
154
|
+
})
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Built-in evidence retrieval
|
|
158
|
+
|
|
159
|
+
`retrieve()` runs the same tools in a model loop and returns **evidence only** (original text re-read from the
|
|
160
|
+
index by line range — never model-written text), plus usage and why it stopped:
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
import { anthropic } from '@ai-sdk/anthropic'
|
|
164
|
+
import { openLibrary, retrieve } from 'markindex'
|
|
165
|
+
|
|
166
|
+
const lib = openLibrary({ root: './docs' })
|
|
167
|
+
const { evidence, usage, stopReason } = await retrieve(lib, 'What notice period applies when an employee resigns?', {
|
|
168
|
+
model: anthropic('claude-sonnet-5-5'),
|
|
169
|
+
maxSteps: 12,
|
|
170
|
+
maxInputTokens: 200_000,
|
|
171
|
+
rateLimit: { requestsPerMinute: 50, inputTokensPerMinute: 40_000 }, // optional, client-side
|
|
172
|
+
})
|
|
173
|
+
for (const e of evidence) console.log(`${e.docPath} L${e.startLine}-${e.endLine}`, e.titlePath.join(' > '))
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Lower-level building blocks are exported too: `parseMarkdown`, `buildTree`, `optimizeTree`, `openStore`,
|
|
177
|
+
`indexLibrary`, `searchNodes`, `summarizeDocument`, `estimateSummaryCost`, `TOOL_CONTRACT`, `createMcpServer`.
|
|
178
|
+
|
|
179
|
+
## MCP setup
|
|
180
|
+
|
|
181
|
+
Build the index first (`npx markindex index /path/to/docs`), then register the server.
|
|
182
|
+
|
|
183
|
+
**Claude Code**
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
claude mcp add markindex -- npx markindex mcp /path/to/docs
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**Cursor** (`~/.cursor/mcp.json` or `.cursor/mcp.json` in a project)
|
|
190
|
+
|
|
191
|
+
```json
|
|
192
|
+
{
|
|
193
|
+
"mcpServers": {
|
|
194
|
+
"markindex": {
|
|
195
|
+
"command": "npx",
|
|
196
|
+
"args": ["markindex", "mcp", "/path/to/docs"]
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
The server sends `AGENT_INSTRUCTIONS` as its MCP instructions and marks all tools read-only.
|
|
203
|
+
|
|
204
|
+
## Tools
|
|
205
|
+
|
|
206
|
+
| Tool | What it does |
|
|
207
|
+
|---|---|
|
|
208
|
+
| `browse_documents` | List a folder: sub-folders and documents (path, title, description, tokens), paginated. The default discovery tool. |
|
|
209
|
+
| `get_document` | One document's metadata, front matter, size and `read_structure_first` (true above 15 000 tokens). |
|
|
210
|
+
| `get_document_structure` | The section outline (node ids, titles, line ranges, tokens, summaries — no body), `depth` levels (default 2), optionally only one node's subtree; paginated with `part`. |
|
|
211
|
+
| `get_node_content` | Original Markdown of 1-20 nodes with line numbers (`123│ …`), paginated for long nodes. |
|
|
212
|
+
| `search_documents` | BM25 keyword search over titles, merged sub-headings, summaries and text, with trigram fallback; scope by `folder` or `doc_path`. The escalation path when browsing stalls. |
|
|
213
|
+
|
|
214
|
+
All tools return `{ success: true, data, next_steps }` or `{ success: false, error, code, details, next_steps }` as a JSON string
|
|
215
|
+
and never throw — unknown document paths come back with similar-name suggestions, stale node ids with a
|
|
216
|
+
"re-fetch the structure" hint. Arguments that do not match the schema (wrong type, unknown field, a string over
|
|
217
|
+
1 024 characters) get an `INVALID_INPUT` envelope from `lib.callTool`; over MCP and the AI SDK, however, the
|
|
218
|
+
transport validates arguments against the same schema before the tool runs, so there such errors come back as the
|
|
219
|
+
transport's own error (an MCP tool error / an AI SDK invalid-tool-input error), not as the JSON envelope.
|
|
220
|
+
|
|
221
|
+
## Security and trust
|
|
222
|
+
|
|
223
|
+
- **Read-only, library-scoped.** The tools only read the index of the library you pointed them at; they never read
|
|
224
|
+
files outside it, never write, and never fetch URLs. Paths and node ids are looked up in the index, not on disk.
|
|
225
|
+
- **Documents are untrusted input.** Tool output is raw document text, and LLM summaries / descriptions are
|
|
226
|
+
generated from it. A document can contain text written to steer an agent (prompt injection). Treat everything
|
|
227
|
+
the tools return as data, not instructions — especially when the library holds third-party or user-supplied
|
|
228
|
+
files, and when the agent also has tools that act (send, write, execute).
|
|
229
|
+
- Summaries and `retrieve()` send document text to the model provider you configure; indexing alone makes no
|
|
230
|
+
network calls.
|
|
231
|
+
|
|
232
|
+
## Design notes
|
|
233
|
+
|
|
234
|
+
- **Tree.** Top-level mdast headings define sections; text before the first heading is a `preface` node; a
|
|
235
|
+
document without headings is one root node. Node ids are short hashes of the ancestor title chain plus the
|
|
236
|
+
sibling ordinal, so they stay stable while the outline does.
|
|
237
|
+
- **Tree optimization.** The merge / expand pass is a port of PageIndex's `tree_optimize.py` cost model with pages
|
|
238
|
+
replaced by tokens: a subtree is merged into its parent when reading it whole is no more expensive than routing
|
|
239
|
+
through it (`S(v) <= tree_cost(v)`, routing cost `R = 150` tokens), merged-away headings are kept as `keyItems`,
|
|
240
|
+
and a node whose own text exceeds `MAX_NODE_TOKENS` (8 000) is split deterministically at Markdown block
|
|
241
|
+
boundaries into `chunk` children. Credit for the cost model goes to PageIndex.
|
|
242
|
+
- **Search.** Only a node's own text is indexed (not its subtree), so parents and children don't double-count.
|
|
243
|
+
Titles weigh 10×, merged headings 5×, summaries 2×, body 1×.
|
|
244
|
+
- **Tool contract.** One zod contract (names, descriptions, schemas) feeds the AI SDK adapter, the MCP server and
|
|
245
|
+
`retrieve()`; descriptions and agent instructions are adapted from PageIndex's `agent_tools.py` (pages → line
|
|
246
|
+
ranges) and pinned by snapshot tests.
|
|
247
|
+
- **Tokens.** Counts are approximate (CJK character = 1 token, other characters / 4); a real tokenizer can be
|
|
248
|
+
injected. Defaults live in `src/config.ts`.
|
|
249
|
+
|
|
250
|
+
## Evaluation
|
|
251
|
+
|
|
252
|
+
`markindex eval` measures whether retrieval lands on the right section: give it a library folder and a JSONL
|
|
253
|
+
question file with gold locations (document path + line range, or a title path), and it reports hit rate, token
|
|
254
|
+
cost, steps and evidence size / precision overall and per language, for the BM25 baseline and for the LLM agent
|
|
255
|
+
(`--max-evidence-lines <n>` additionally requires hits to be at most `n` lines long). The question-file format,
|
|
256
|
+
the hit rule, the development corpus (329 public documents, ~38 M tokens, English filings and guidelines plus
|
|
257
|
+
Chinese laws and encyclopedia articles), baseline results and scale benchmarks are in
|
|
258
|
+
[eval/README.md](eval/README.md).
|
|
259
|
+
|
|
260
|
+
**Headline (n = 30, Claude Code sonnet restricted to the five MarkIndex MCP tools):** the gold passage was found for
|
|
261
|
+
**30/30** questions (93.3% at rank 1), citing 15 lines on average with 88.6% of cited lines inside the gold range — vs
|
|
262
|
+
73.3% hit and 209 lines (12.8% precision) for BM25 top-5 on the same questions, at ~3 agent turns per question
|
|
263
|
+
([details and caveats](eval/README.md#7-results-agent-mode-via-claude-code)).
|
|
264
|
+
|
|
265
|
+
## Development
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
npm install
|
|
269
|
+
npm test # vitest
|
|
270
|
+
npm run typecheck
|
|
271
|
+
npm run build
|
|
272
|
+
npx tsx eval/fetch-corpus.ts # download the evaluation corpus (eval/corpus, not committed)
|
|
273
|
+
npx tsx eval/bench.ts # scale benchmark
|
|
274
|
+
npx tsx eval/claude-code-eval.ts --root eval/corpus --questions eval/questions.jsonl --per-lang 10 --out cc.json
|
|
275
|
+
# agent eval with Claude Code over MCP (spends Claude usage; eval/README.md §7)
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## License
|
|
279
|
+
|
|
280
|
+
[MIT](LICENSE). PageIndex is also MIT-licensed; MarkIndex contains no PageIndex code, but ports its tree cost
|
|
281
|
+
model and adapts its agent-tool descriptions, instructions and prompts, with thanks — see
|
|
282
|
+
[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) for its copyright and license notice.
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# MarkIndex
|
|
2
|
+
|
|
3
|
+
[](https://github.com/jackjin1997/markindex/actions/workflows/ci.yml) [](https://www.npmjs.com/package/markindex)
|
|
4
|
+
|
|
5
|
+
**面向 Agent 的无向量、结构优先的长篇 Markdown 文档库检索。**
|
|
6
|
+
|
|
7
|
+
MarkIndex 把每篇 Markdown 按其自身章节解析成树,把树、原文行和中英混排 BM25 索引存进一个 SQLite 文件,
|
|
8
|
+
再给 Agent 提供 5 个只读工具,沿"库 → 文件夹 → 文档 → 章节"逐层导航,并带行号读取原文。
|
|
9
|
+
不用 embedding、不用向量库、不靠猜测切块;文档自身的结构就是索引,每条证据都能以 `[doc_path, L起-止]` 的格式引用回原文。
|
|
10
|
+
|
|
11
|
+
> 受 [VectifyAI/PageIndex](https://github.com/VectifyAI/PageIndex) 启发,MarkIndex 是一个独立、非官方的
|
|
12
|
+
> TypeScript 实现,专注 Markdown;与 VectifyAI 无隶属或背书关系。
|
|
13
|
+
|
|
14
|
+
English: [README.md](README.md)
|
|
15
|
+
|
|
16
|
+
## 为什么
|
|
17
|
+
|
|
18
|
+
- **结构优先**:长篇专业文档(年报、临床指南、法规、手册)本来就有作者组织好的目录;沿目录导航保留上下文、可解释、可引用。
|
|
19
|
+
- **建索引确定、不调 LLM**:mdast 解析、建树、token 成本模型优化都不调模型,同一输入结果逐字节一致。LLM 只用于可选的节点摘要、文档描述和内置 `retrieve()`。
|
|
20
|
+
- **认真处理 Markdown**:Setext 标题、首个标题前的前言、无标题文档、frontmatter、代码块里的 `#`、标题跳级、BOM/CRLF;每一行原文都恰好属于一个节点,过大的章节按块边界确定性拆成 `chunk` 节点。
|
|
21
|
+
- **中文可用的关键词兜底**:基于 `Intl.Segmenter` 分词的 BM25(中文、英文、中英混排,`C++`、`ISO-27001`、`v2.1` 这类标识符),另有 trigram 子串索引补位。
|
|
22
|
+
- **响应有预算**:工具从不抛异常,返回带 `next_steps` 指引的 JSON 信封,单次响应不超过 95 000 字符,大目录与长章节自动分段。
|
|
23
|
+
|
|
24
|
+
## 安装
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install markindex # 库 + CLI(Node.js >= 22.12)
|
|
28
|
+
npx markindex --help
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
从源码安装:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
git clone https://github.com/jackjin1997/markindex.git && cd markindex
|
|
35
|
+
npm install && npm run build
|
|
36
|
+
node dist/cli.js --help # 或 `npm link` 得到 `markindex` 命令
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
只有用到 LLM 功能(摘要、`retrieve()`、agent 模式评测)时才需要安装对应的 AI SDK provider,例如 `npm install @ai-sdk/anthropic`,并设置 `ANTHROPIC_API_KEY`。
|
|
40
|
+
不在本地安装、直接用 `npx` 时,把 provider 放进同一个临时环境:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npx -p markindex -p @ai-sdk/anthropic markindex index ./docs --summary-model anthropic:claude-sonnet-5-5
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## CLI
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
markindex index ./docs # 建立 / 增量更新索引(<root>/.markindex/index.db)
|
|
50
|
+
markindex index ./docs --summary-model anthropic:claude-sonnet-5-5 --estimate # 只估算摘要成本,不调 LLM
|
|
51
|
+
markindex index ./docs --summary-model anthropic:claude-sonnet-5-5 --summary-rpm 50 --summary-tpm 40000 # 摘要限速
|
|
52
|
+
markindex index ./docs --no-setext # 转换器产物里 `---` 是分页符时使用
|
|
53
|
+
markindex index ./docs --ignore 'drafts/' --ignore '*.tmp.md' # 额外的忽略规则(可重复)
|
|
54
|
+
markindex tree ./docs 指南/安装.md --depth 3 # 查看一篇文档的章节树
|
|
55
|
+
markindex search ./docs "离职 补偿" --limit 5 --json # 关键词检索
|
|
56
|
+
markindex mcp ./docs # 以 MCP server(stdio,只读)提供给 Agent
|
|
57
|
+
markindex eval ./docs --questions questions.jsonl --mode bm25 --out report.json
|
|
58
|
+
markindex eval ./docs --questions questions.jsonl --mode agent --model anthropic:claude-sonnet-5-5 --out report.json
|
|
59
|
+
markindex eval ./docs --questions questions.jsonl --mode agent --model anthropic:claude-sonnet-5-5 --rpm 50 --tpm 40000
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**限速**:调用默认受并发数(eval 的 `--concurrency`;摘要为 8)和 AI SDK 重试约束,后者已经会遵循 429 / 5xx 响应里的
|
|
63
|
+
`retry-after` / `retry-after-ms`(上限 60 秒)。要压在服务商配额以内,可再加客户端限速:`index` 用 `--summary-rpm` / `--summary-tpm`,
|
|
64
|
+
`eval`(agent 模式)用 `--rpm` / `--tpm`。`rpm` 限制任意滑动 60 秒窗口内的请求数,`tpm` 限制窗口内**估算的输入** token 数;
|
|
65
|
+
同一次运行的所有文档(index)或所有问题(eval)共用一份额度,排队的调用按先后顺序放行。输入 token 按提示词文本估算
|
|
66
|
+
(中日韩字符约 1 个 token 一个字,其余约 4 个字符 1 个 token),请在真实配额下留出余量;输出 token 与工具定义不计入。
|
|
67
|
+
单个请求超过 `tpm` 时仍会放行(独占整个窗口)。库 API 中给 `summarizeDocument` / `indexLibrary({ summary })` 或 `retrieve()`
|
|
68
|
+
传 `rateLimit: { requestsPerMinute, inputTokensPerMinute }`;复用同一个对象即共用额度。
|
|
69
|
+
|
|
70
|
+
所有命令都接受 `--db <file>` 指定索引文件。索引请放在本地磁盘:写锁依赖硬链接,SQLite 使用 WAL 模式,二者在 exFAT、
|
|
71
|
+
SMB / NFS 共享或其他网络卷上都不可靠;文档放在这类卷上时,文档原地不动,用 `--db` 指向本地路径
|
|
72
|
+
(如 `--db ~/.cache/markindex/docs.db`)。
|
|
73
|
+
|
|
74
|
+
索引是增量的:按内容哈希比较,只重建新增或改动的文档,删除的文件会被清理,摘要按节点内容哈希缓存。扫描 `.md` 与
|
|
75
|
+
`.markdown`(不含 `.mdx`);以下路径会被忽略:文档库自身各级目录下 `.gitignore` 文件匹配的路径(不读取全局 git excludes 与
|
|
76
|
+
`.git/info/exclude`)、库根目录下可选的 `.markindexignore` 文件(语法相同,用于不想写进 `.gitignore` 的规则)匹配的路径,
|
|
77
|
+
以及 `--ignore <pattern>` 选项(gitignore 语法,相对库根,可重复;库 API 为 `ignore: string[]`;`mcp --reindex-on-start` 同样支持)
|
|
78
|
+
匹配的路径(开头的 `./` 会被去掉,开头的 `/` 与 gitignore 一样表示锚定到库根,绝对文件系统路径或 `../` 会报错)。
|
|
79
|
+
这几类来源只会增加排除:`.markindexignore` 或 `--ignore` 中的 `!` 规则不能重新包含被 `.gitignore` 排除的路径。
|
|
80
|
+
变为被忽略的文档会在下一次运行时从索引中删除。跳过 `.git`、`.markindex` 等点目录,不跟随符号链接。同一时间只允许一个写进程(第二个 `index`
|
|
81
|
+
立即失败),MCP server 等读取方不受阻塞。索引由旧版本 MarkIndex 或不同的建树参数生成时,下一次 `index` 会重建全部文档
|
|
82
|
+
(输出 `full rebuild: …`,报告中带 `rebuilt`),已缓存的摘要会被复用。
|
|
83
|
+
|
|
84
|
+
**PDF / HTML 转换产物的分页符**:转换器常在一页最后一段之后紧跟一行 `---` 作为分页,CommonMark 会把它读成 setext 标题,
|
|
85
|
+
整段文字变成二级标题。`index` 发现文字跨多行或超过 120 个字符的 setext 二级标题时会给出警告(标准输出 `warning <path>: …`,
|
|
86
|
+
JSON 报告中的 `warnings`)。这类文档库请加 `--no-setext`(`mcp --reindex-on-start` 同样支持;库 API 为 `setext: false`):
|
|
87
|
+
`---` 按分隔线、`===` 按普通文字处理。该设置保存在索引中:之后不带该参数的运行(`index`、`mcp --reindex-on-start`、`Library.index()`)会沿用它;
|
|
88
|
+
要重新开启请传 `--setext`(库 API 为 `setext: true`)。改变该设置会重建整个索引。
|
|
89
|
+
|
|
90
|
+
## 库用法
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import { generateText, stepCountIs } from 'ai'
|
|
94
|
+
import { anthropic } from '@ai-sdk/anthropic'
|
|
95
|
+
import { AGENT_INSTRUCTIONS, createAiSdkTools, openLibrary, retrieve } from 'markindex'
|
|
96
|
+
|
|
97
|
+
const writer = openLibrary({ root: './docs', readonly: false })
|
|
98
|
+
await writer.index()
|
|
99
|
+
writer.close()
|
|
100
|
+
|
|
101
|
+
const lib = openLibrary({ root: './docs' })
|
|
102
|
+
await lib.callTool('search_documents', { query: '劳动合同 解除' }) // JSON 信封字符串
|
|
103
|
+
|
|
104
|
+
// 方式一:你的 Agent 自己调用工具
|
|
105
|
+
await generateText({ model: anthropic('claude-sonnet-5-5'), system: AGENT_INSTRUCTIONS, tools: createAiSdkTools(lib), stopWhen: stepCountIs(15), prompt: '员工辞职需要提前多久通知?' })
|
|
106
|
+
|
|
107
|
+
// 方式二:内置检索循环,只返回原文证据(按行号从索引回读),不生成答案
|
|
108
|
+
const { evidence } = await retrieve(lib, '员工辞职需要提前多久通知?', { model: anthropic('claude-sonnet-5-5') })
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## 接入 MCP
|
|
112
|
+
|
|
113
|
+
先 `npx markindex index /path/to/docs`,然后:
|
|
114
|
+
|
|
115
|
+
- Claude Code:`claude mcp add markindex -- npx markindex mcp /path/to/docs`
|
|
116
|
+
- Cursor(`~/.cursor/mcp.json`):
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{ "mcpServers": { "markindex": { "command": "npx", "args": ["markindex", "mcp", "/path/to/docs"] } } }
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## 工具
|
|
123
|
+
|
|
124
|
+
`browse_documents`(按文件夹浏览,默认发现入口)、`get_document`(元数据与是否需要先看结构)、
|
|
125
|
+
`get_document_structure`(不含正文的章节树,默认 2 层,可只展开某节点)、`get_node_content`(带行号原文,长节点分段)、
|
|
126
|
+
`search_documents`(BM25 + trigram 兜底,可限定文件夹或文档)。
|
|
127
|
+
|
|
128
|
+
工具从不抛异常,返回 `{ success, data | error, code, details, next_steps }` JSON 信封。参数不符合 schema(类型错误、未知字段、
|
|
129
|
+
字符串超过 1 024 字符)时,`lib.callTool` 返回 `INVALID_INPUT` 信封;但经 MCP 与 AI SDK 调用时,传输层会先按同一 schema
|
|
130
|
+
校验参数,此类错误以传输层自身的错误返回(MCP 工具错误 / AI SDK 的 invalid tool input 错误),而不是 JSON 信封。
|
|
131
|
+
|
|
132
|
+
## 安全与信任
|
|
133
|
+
|
|
134
|
+
- **只读、限定在文档库内**:工具只读取你指定的那个文档库的索引,从不读取库外文件、不写入、不访问网络;路径与节点 id 都在索引中查找,而不是直接访问磁盘。
|
|
135
|
+
- **文档是不可信输入**:工具输出是文档原文,LLM 摘要 / 描述也由原文生成。文档中可能含有试图操纵 Agent 的文字(提示词注入)。请把工具返回的一切当作数据而非指令——尤其当文档库包含第三方或用户提交的文件,且 Agent 同时拥有发送、写入、执行等有副作用的工具时。
|
|
136
|
+
- 摘要与 `retrieve()` 会把文档内容发送给你配置的模型服务商;单纯建索引不产生任何网络请求。
|
|
137
|
+
|
|
138
|
+
## 设计要点
|
|
139
|
+
|
|
140
|
+
- 树优化移植 PageIndex `tree_optimize.py` 的成本模型(页 → token):读整棵子树不比路由更贵就合并(`S(v) <= tree_cost(v)`,路由成本 150 token),被合并的标题保留为 `keyItems`;自身正文超过 8 000 token 的节点按块边界拆成 `chunk`。成本模型归功于 PageIndex。
|
|
141
|
+
- 只索引节点自身正文,父子节点不会因同一段文字重复命中;标题权重 10、合并标题 5、摘要 2、正文 1。
|
|
142
|
+
- 工具契约只有一份(zod),AI SDK 适配、MCP server 与 `retrieve()` 都是它的消费者。
|
|
143
|
+
|
|
144
|
+
## 评测(供内部验收)
|
|
145
|
+
|
|
146
|
+
问题文件为 JSONL,每行一个问题:
|
|
147
|
+
|
|
148
|
+
```json
|
|
149
|
+
{"id": "q1", "question": "员工辞职需要提前多久通知?", "lang": "zh", "gold": [{"doc_path": "人事/员工手册.md", "start_line": 120, "end_line": 134}]}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
`gold` 中任一位置与返回证据的行区间有交集即算命中;没有行号时按 `title_path`(从顶层章节到答案节点的标题链)前缀匹配;
|
|
153
|
+
只有 `doc_path` 时命中该文档任意位置即可。报告还给出每题的证据规模(行数、近似 token)与精度(证据行落在 gold 行区间内的比例),
|
|
154
|
+
并按语言汇总;`--max-evidence-lines <n>` 要求命中的证据条目不超过 `n` 行。完整格式、命中规则、开发期语料、BM25 基线结果与规模基准见
|
|
155
|
+
[eval/README.md](eval/README.md)。
|
|
156
|
+
|
|
157
|
+
**核心结果(n = 30,Claude Code sonnet,只开放 MarkIndex 的 5 个 MCP 工具)**:30/30 题都找到了标准答案段落(首位命中 93.3%),
|
|
158
|
+
平均引用 15 行,其中 88.6% 落在标准答案区间内;同一批问题上 BM25 top-5 命中 73.3%、平均 209 行(精度 12.8%)。Agent 平均每题约 3 轮
|
|
159
|
+
([详情与局限](eval/README.md#7-results-agent-mode-via-claude-code))。
|
|
160
|
+
|
|
161
|
+
## 许可
|
|
162
|
+
|
|
163
|
+
[MIT](LICENSE)。PageIndex 同为 MIT;MarkIndex 不含其代码,但移植了其树成本模型、改写了其 Agent 工具描述、指令与提示词,特此致谢;
|
|
164
|
+
其版权与许可声明见 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)。
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
MarkIndex is an independent TypeScript implementation. It contains no PageIndex source code, but parts of it are
|
|
4
|
+
adapted from [VectifyAI/PageIndex](https://github.com/VectifyAI/PageIndex):
|
|
5
|
+
|
|
6
|
+
- the agent instructions (`AGENT_INSTRUCTIONS`) and the tool descriptions in `src/tools/contract.ts`, and the
|
|
7
|
+
summarization prompts in `src/llm/summarize.ts`, are adapted from PageIndex's agent tools and prompts
|
|
8
|
+
(pages replaced by line ranges);
|
|
9
|
+
- the tree-optimization cost model in `src/tree/optimize.ts` (merge / expand by token cost) is a port of
|
|
10
|
+
PageIndex's `tree_optimize.py`, with pages replaced by tokens.
|
|
11
|
+
|
|
12
|
+
PageIndex is distributed under the following license:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
MIT License
|
|
16
|
+
|
|
17
|
+
Copyright (c) 2025 Vectify AI
|
|
18
|
+
|
|
19
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
20
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
21
|
+
in the Software without restriction, including without limitation the rights
|
|
22
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
23
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
24
|
+
furnished to do so, subject to the following conditions:
|
|
25
|
+
|
|
26
|
+
The above copyright notice and this permission notice shall be included in all
|
|
27
|
+
copies or substantial portions of the Software.
|
|
28
|
+
|
|
29
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
30
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
31
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
32
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
33
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
34
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
35
|
+
SOFTWARE.
|
|
36
|
+
```
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Tool } from 'ai';
|
|
2
|
+
import type { Library } from '../library.js';
|
|
3
|
+
import { type ToolName } from '../tools/contract.js';
|
|
4
|
+
/**
|
|
5
|
+
* The five agent tools as Vercel AI SDK tools, backed by `lib.callTool`. Each `execute` resolves to the JSON
|
|
6
|
+
* envelope string (never rejects), so the model always sees a structured result.
|
|
7
|
+
*/
|
|
8
|
+
export declare function createAiSdkTools(lib: Library): Record<ToolName, Tool>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import { TOOL_CONTRACT } from '../tools/contract.js';
|
|
3
|
+
/**
|
|
4
|
+
* The five agent tools as Vercel AI SDK tools, backed by `lib.callTool`. Each `execute` resolves to the JSON
|
|
5
|
+
* envelope string (never rejects), so the model always sees a structured result.
|
|
6
|
+
*/
|
|
7
|
+
export function createAiSdkTools(lib) {
|
|
8
|
+
const entries = Object.keys(TOOL_CONTRACT).map((name) => {
|
|
9
|
+
const contract = TOOL_CONTRACT[name];
|
|
10
|
+
return [
|
|
11
|
+
name,
|
|
12
|
+
tool({
|
|
13
|
+
description: contract.description,
|
|
14
|
+
inputSchema: contract.input,
|
|
15
|
+
execute: (args) => lib.callTool(name, args),
|
|
16
|
+
}),
|
|
17
|
+
];
|
|
18
|
+
});
|
|
19
|
+
return Object.fromEntries(entries);
|
|
20
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { Library } from '../library.js';
|
|
3
|
+
/** Package version, read from package.json (same relative location from src/ and dist/). */
|
|
4
|
+
export declare function packageVersion(): string;
|
|
5
|
+
/** MCP server exposing the five agent tools over `lib`. Connect it to a transport (stdio for `markindex mcp`). */
|
|
6
|
+
export declare function createMcpServer(lib: Library): McpServer;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { AGENT_INSTRUCTIONS, TOOL_CONTRACT } from '../tools/contract.js';
|
|
4
|
+
/** Package version, read from package.json (same relative location from src/ and dist/). */
|
|
5
|
+
export function packageVersion() {
|
|
6
|
+
const url = new URL('../../package.json', import.meta.url);
|
|
7
|
+
return JSON.parse(readFileSync(url, 'utf8')).version;
|
|
8
|
+
}
|
|
9
|
+
/** MCP server exposing the five agent tools over `lib`. Connect it to a transport (stdio for `markindex mcp`). */
|
|
10
|
+
export function createMcpServer(lib) {
|
|
11
|
+
const server = new McpServer({ name: 'markindex', version: packageVersion() }, { instructions: AGENT_INSTRUCTIONS });
|
|
12
|
+
for (const name of Object.keys(TOOL_CONTRACT)) {
|
|
13
|
+
const contract = TOOL_CONTRACT[name];
|
|
14
|
+
server.registerTool(name, { description: contract.description, inputSchema: contract.input.shape, annotations: { readOnlyHint: true } }, async (args) => {
|
|
15
|
+
const text = await lib.callTool(name, args);
|
|
16
|
+
return { content: [{ type: 'text', text }], isError: !isSuccess(text) };
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
return server;
|
|
20
|
+
}
|
|
21
|
+
function isSuccess(envelope) {
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(envelope).success === true;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
package/dist/cli.d.ts
ADDED