@ajdev0/token-shrink 2.0.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/README.md ADDED
@@ -0,0 +1,345 @@
1
+ # token-shrink
2
+
3
+ A **local-first, framework-aware token reduction engine** — a polyglot AST semantic proxy and MCP server. It prunes implementation bodies out of **dependency files** while preserving every type signature, interface, and module export, so LLM agents see the full shape of the code at a fraction of the tokens.
4
+
5
+ > **The 80–90% reduction target**: full type information, no implementation noise. Ring 0 (your active file) stays complete; Ring 1 (its direct imports) is delivered as pruned skeletons.
6
+
7
+ ---
8
+
9
+ ## How it works
10
+
11
+ ```
12
+ active file imports (Ring 1)
13
+ ┌──────────────────┐ ┌──────────────────────┐
14
+ │ src/page.ts │ ──► │ src/util.ts │
15
+ └──────────────────┘ └──────────────────────┘
16
+ ▾ ▾
17
+ tree-sitter (WASM) ─────────────► prune impl blocks
18
+ parse & query keep interfaces · types ·
19
+ signatures · exports
20
+
21
+ pruned skeleton (Ring 0 full source)
22
+
23
+ Compressed Code Context (Markdown)
24
+ │ │
25
+ via MCP (stdio) via HTTP (Fastify)
26
+ get_compressed_code_context POST /v1/context
27
+ ```
28
+
29
+ Pipeline stages:
30
+
31
+ 1. **Parse** — `web-tree-sitter` loads a `.wasm` grammar per language (auto-downloaded on first run).
32
+ 2. **Prune** — an S-expression query matches implementation blocks (`statement_block`, `block`, `compound_statement`…), which are replaced with a short token (`/* ... */`, or `pass` for Python) using **descending-order splicing** so offsets stay valid.
33
+ 3. **Watch** — `chokidar` watches the repo, `sha1`-hashes file contents, and refreshes the cache only on change.
34
+ 4. **Assemble** — the active file's imports are resolved and merged into a Markdown context payload (Ring 0 + Ring 1).
35
+
36
+ ---
37
+
38
+
39
+
40
+ ## Install
41
+
42
+ Requires **Node.js 18+**.
43
+
44
+ ```bash
45
+ # run anywhere without installing
46
+ # --root project root --port http port --host bind address
47
+ npx @ajdev0/token-shrink --root /path/to/project
48
+
49
+ # or install locally
50
+ npm install @ajdev0/token-shrink
51
+ ```
52
+
53
+
54
+
55
+ ### Build from source
56
+
57
+ ```bash
58
+ # install deps
59
+ npm install
60
+
61
+ # compile (tsup -> dist/), typecheck, and run tests
62
+ npm run build
63
+ npm run typecheck
64
+ npm test
65
+ ```
66
+
67
+ The build produces three binaries:
68
+
69
+
70
+ | Binary | Entry | Purpose |
71
+ | -------------------- | --------------- | -------------------------------------------- |
72
+ | `@ajdev0/token-shrink` | `dist/cli.cjs` | Fastify HTTP server (`POST /v1/context`) |
73
+ | `@ajdev0/token-shrink-mcp` | `dist/mcp.cjs` | MCP stdio server for AI agents |
74
+ | library | `dist/index.js` | `prune()`, `assemble()`, `createWatcher()` … |
75
+
76
+ ### Publish to npm
77
+
78
+ ```bash
79
+ npm login
80
+ npm run build && npm test
81
+ npm pack --dry-run # preview tarball contents
82
+ npm publish
83
+ ```
84
+
85
+ The package name on npm is **`@ajdev0/token-shrink`**. After publishing, users can run:
86
+
87
+ ```bash
88
+ npx @ajdev0/token-shrink-mcp --root /path/to/project
89
+ ```
90
+
91
+
92
+ ---
93
+
94
+
95
+
96
+ ## WASM grammars (auto-download)
97
+
98
+ Grammars are fetched from the official tree-sitter GitHub releases on **first use** and cached in `wasm/`:
99
+
100
+ ```text
101
+ wasm/
102
+ ├── tree-sitter-typescript.wasm
103
+ ├── tree-sitter-javascript.wasm
104
+ ├── tree-sitter-tsx.wasm
105
+ ├── tree-sitter-python.wasm
106
+ ├── tree-sitter-go.wasm
107
+ ├── ...
108
+ ```
109
+
110
+ - First run requires network access; afterwards everything is offline and fast.
111
+ - Files are written atomically (`*.tmp` → rename) with an in-flight lock, so concurrent first-run parses never corrupt the cache.
112
+
113
+ ---
114
+
115
+
116
+
117
+ ## Usage
118
+
119
+
120
+
121
+ ### 1. MCP server (AI agents — Cursor, Claude, etc.)
122
+
123
+ Run the stdio MCP server and expose the `get_compressed_code_context` tool:
124
+
125
+ ```bash
126
+ # point it at your project
127
+ token-shrink-mcp --root /path/to/project
128
+
129
+ # root also works via env or cwd
130
+ ROOT=/path/to/project token-shrink-mcp
131
+ cd /path/to/project && token-shrink-mcp
132
+ ```
133
+
134
+ **Cursor MCP config** (`.cursor/mcp.json`):
135
+
136
+ ```json
137
+ {
138
+ "mcpServers": {
139
+ "token-shrink": {
140
+ "command": "token-shrink-mcp",
141
+ "args": ["--root", "/absolute/path/to/your/project"]
142
+ }
143
+ }
144
+ }
145
+ ```
146
+
147
+ **Claude Code MCP config** — add it to the project's `.mcp.json`, or register with the Claude CLI:
148
+
149
+ ```bash
150
+ # register the server for this project
151
+ claude mcp add token-shrink -- token-shrink-mcp --root /path/to/project
152
+ # persistent flag: -- transport stdio
153
+ claude mcp add token-shrink --transport stdio -- token-shrink-mcp --root /path/to/project
154
+ ```
155
+
156
+ or place in `.claude/settings.json` / project `.mcp.json`:
157
+
158
+ ```json
159
+ {
160
+ "mcpServers": {
161
+ "token-shrink": {
162
+ "command": "token-shrink-mcp",
163
+ "args": ["--root", "/path/to/project"]
164
+ }
165
+ }
166
+ }
167
+ ```
168
+
169
+ **Auto rule**: by default the server writes agent integration rules so the tool is used automatically on every prompt:
170
+
171
+ - **Cursor**: `.cursor/rules/token-shrink.mdc`
172
+ - **Claude Code**: `.claude/rules/token-shrink.md`
173
+
174
+ Both are sentinel-tagged and never rewrite a user-authored file at the same path. Repeated starts are no-ops. Choose the target(s) with `--rule-target=cursor|claude|all` (default `all`, comma-separated values allowed):
175
+
176
+ ```bash
177
+ # only Claude Code
178
+ token-shrink-mcp --root /path/to/project --rule-target=claude
179
+
180
+ # completely disable auto-rules
181
+ token-shrink-mcp --root /path/to/project --no-create-rule
182
+ ```
183
+
184
+ Opt out also via `--create-rule=false` or `TOKEN_SHRINK_CREATE_RULE=0`.
185
+
186
+ **Tool:** `get_compressed_code_context`
187
+
188
+
189
+ | Argument | Type | Required | Description |
190
+ | ---------------- | --------- | -------- | --------------------------------------------- |
191
+ | `activeFilePath` | `string` | yes | The file the agent is working on |
192
+ | `maxSkeletons` | `number` | no | Cap on Ring-1 files (default `50`, max `200`) |
193
+ | `includeStats` | `boolean` | no | Append approximate token counts |
194
+
195
+
196
+ Returns a Markdown payload with the active file fully inlined (Ring 0) and the pruned skeletons of its direct imports (Ring 1).
197
+
198
+ ### 2. HTTP server (Fastify)
199
+
200
+ ```bash
201
+ token-shrink --root /path/to/project --port 3000
202
+ # env equivalents: ROOT=… PORT=… HOST=…
203
+ ```
204
+
205
+
206
+ | Route | Method | Body | Returns |
207
+ | ------------- | ------ | -------------------------------------------------- | -------------------------------- |
208
+ | `/health` | `GET` | — | status, root, indexed file count |
209
+ | `/v1/context` | `POST` | `{ activeFilePath, maxSkeletons?, includeStats? }` | assembled Markdown + deps |
210
+
211
+
212
+ ```bash
213
+ curl -s http://localhost:3000/health
214
+ # {"status":"ok","service":"token-shrink","version":"2.0.0","root":".","indexed":182}
215
+
216
+ curl -s -X POST http://localhost:3000/v1/context \
217
+ -H 'Content-Type: application/json' \
218
+ -d '{"activeFilePath":"./src/page.ts","includeStats":true}'
219
+ ```
220
+
221
+
222
+
223
+ ### 3. Library API
224
+
225
+ ```ts
226
+ import { prune, assemble, createWatcher } from 'token-shrink';
227
+
228
+ // prune a single file -> skeleton (keeps signatures, strips bodies)
229
+ const { code, removed } = await prune('src/util.ts', sourceText);
230
+
231
+ // assemble context for an active file from a warm cache
232
+ const { markdown } = assemble('src/page.ts', watcher.cache.entries, {
233
+ includeStats: true,
234
+ });
235
+
236
+ // incremental watcher
237
+ const watcher = createWatcher({ root: process.cwd(), ignored: ['node_modules'] });
238
+ await watcher.indexAll();
239
+ ```
240
+
241
+ ---
242
+
243
+
244
+
245
+ ## Supported languages
246
+
247
+ S-expression queries match implementation blocks; interfaces, signatures, and exports are never touched. The `Block node` column shows the AST node that gets collapsed during pruning.
248
+
249
+
250
+ | Language | Extensions | Grammar wasm | Block node |
251
+ | --------------- | --------------------------------------- | ----------------------------- | -------------------- |
252
+ | TypeScript | `.ts` `.cts` `.mts` | `tree-sitter-typescript.wasm` | `statement_block` |
253
+ | JavaScript | `.js` `.cjs` `.mjs` | `tree-sitter-javascript.wasm` | `statement_block` |
254
+ | React / Next.js | `.tsx` | `tree-sitter-tsx.wasm` | `statement_block`¹ |
255
+ | React (JSX) | `.jsx` | `tree-sitter-javascript.wasm` | `statement_block`¹ |
256
+ | Python | `.py` `.pyi` | `tree-sitter-python.wasm` | `block` → `pass` |
257
+ | Dart / Flutter | `.dart` | `tree-sitter-dart.wasm` | `block` |
258
+ | Swift / SwiftUI | `.swift` | `tree-sitter-swift.wasm` | `statements` |
259
+ | Go | `.go` | `tree-sitter-go.wasm` | `block` |
260
+ | Rust | `.rs` | `tree-sitter-rust.wasm` | `block` |
261
+ | Java | `.java` | `tree-sitter-java.wasm` | `block` |
262
+ | Kotlin | `.kt` `.kts` | `tree-sitter-kotlin.wasm` | `block` |
263
+ | C | `.c` `.h` | `tree-sitter-c.wasm` | `compound_statement` |
264
+ | C++ | `.cc` `.cpp` `.cxx` `.hpp` `.hh` `.hxx` | `tree-sitter-cpp.wasm` | `compound_statement` |
265
+ | PHP | `.php` | `tree-sitter-php.wasm` | `compound_statement` |
266
+
267
+
268
+ > ¹ TSX/JSX also preserve `'use client'` / `'use server'` directive lines inside otherwise-pruned bodies (framework-aware).
269
+
270
+ Language IDs: `typescript · javascript · tsx · jsx · python · dart · swift · go · rust · java · kotlin · c · cpp · php`.
271
+
272
+ ---
273
+
274
+
275
+
276
+ ## Example
277
+
278
+ **Input** `src/util.ts`
279
+
280
+ ```ts
281
+ export interface User {
282
+ id: number;
283
+ name: string;
284
+ }
285
+ export function buildGreeting(u: User) {
286
+ const parts = [u.name, u.email];
287
+ return parts.join(' | ');
288
+ }
289
+ export const formatEmail = (u: User) => {
290
+ return u.email.toLowerCase().trim();
291
+ };
292
+ ```
293
+
294
+ **Pruned skeleton (Ring 1)** — signatures and the interface intact, bodies collapsed:
295
+
296
+ ```ts
297
+ export interface User {
298
+ id: number;
299
+ name: string;
300
+ }
301
+ export function buildGreeting(u: User) /* ... */
302
+ export const formatEmail = (u: User) => /* ... */;
303
+ ```
304
+
305
+ ---
306
+
307
+
308
+
309
+ ## Design notes
310
+
311
+ - **Bottom-up splicing** — ranges are sorted by start index descending and replaced in place, so earlier offsets never shift and the output stays a valid, parseable file.
312
+ - **Regex-based import extraction** — resilient across languages; resolves relative imports (`./x`, `../y`), aliases (`@/`, `~`), and skips bare package specifiers.
313
+ - **Incremental hashing** — files are re-pruned only when their `sha1` hash changes; the watcher is debounced (100 ms) and zero-CPU while idle.
314
+ - **Ram-safe watchers** — sockets / non-regular files are never opened with `fs.watch`, so stray unix sockets in the tree can't crash the server.
315
+
316
+ ---
317
+
318
+
319
+
320
+ ## Project layout
321
+
322
+ ```
323
+ token-shrink/
324
+ ├── package.json / tsconfig.json / tsup.config.ts / vitest.config.ts
325
+ ├── src/
326
+ │ ├── index.ts # library entry (exports)
327
+ │ ├── cli.ts # Fastify HTTP server
328
+ │ ├── mcp.ts # MCP stdio server
329
+ │ ├── parser/
330
+ │ │ ├── registry.ts # extension → language spec + S-queries
331
+ │ │ ├── wasm.ts # auto-download + cache of .wasm files
332
+ │ │ └── pruner.ts # prune(filePath, source) → skeleton
333
+ │ ├── watcher/
334
+ │ │ └── sync.ts # chokidar watch + hash cache + import graph
335
+ │ └── server/
336
+ │ └── assembler.ts # Ring 0 + Ring 1 Markdown payload
337
+ ├── tests/ # pruning integrity + token-reduction tests
338
+ └── wasm/ # auto-downloaded grammars (gitignored)
339
+ ```
340
+
341
+
342
+
343
+ ## License
344
+
345
+ MIT
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ assemble,
4
+ createWatcher,
5
+ warmGrammars
6
+ } from "./chunk-HRF3BIOV.js";
7
+
8
+ // src/cli.ts
9
+ import Fastify from "fastify";
10
+ import picocolors from "picocolors";
11
+ import path from "path";
12
+ function parseArgv(argv = process.argv) {
13
+ const out = {};
14
+ for (let i = 0; i < argv.length; i++) {
15
+ const cur = argv[i];
16
+ if (cur?.startsWith("--")) {
17
+ const key = cur.slice(2);
18
+ const next = argv[i + 1];
19
+ if (next && !next.startsWith("--")) {
20
+ out[key] = next;
21
+ i++;
22
+ } else {
23
+ out[key] = "true";
24
+ }
25
+ }
26
+ }
27
+ return out;
28
+ }
29
+ async function startServer(opts = {}) {
30
+ const args = parseArgv();
31
+ const port = opts.port ?? Number(args.port ?? process.env.PORT ?? 3e3);
32
+ const host = opts.host ?? args.host ?? "0.0.0.0";
33
+ const root = path.resolve(opts.root ?? args.root ?? process.cwd());
34
+ const silent = opts.silent ?? args.silent === "true";
35
+ const log = (msg) => {
36
+ if (!silent) console.log(picocolors.dim(msg));
37
+ };
38
+ void warmGrammars().catch(() => {
39
+ });
40
+ const watcher = createWatcher({ root, ignored: opts.ignored });
41
+ log(`Indexing ${root} in the background\u2026`);
42
+ void watcher.indexAll().then((n) => log(`Indexed ${n} files.`));
43
+ const app = Fastify({ logger: !silent });
44
+ app.get("/health", async () => ({
45
+ status: "ok",
46
+ service: "token-shrink",
47
+ version: "2.0.0",
48
+ root,
49
+ indexed: watcher.cache.entries.size
50
+ }));
51
+ app.post("/v1/context", async (req, reply) => {
52
+ const body = req.body ?? {};
53
+ if (!body.activeFilePath) {
54
+ return reply.status(400).send({ error: "`activeFilePath` is required" });
55
+ }
56
+ const result = assemble(body.activeFilePath, watcher.cache.entries, {
57
+ maxSkeletons: body.maxSkeletons,
58
+ includeStats: body.includeStats
59
+ });
60
+ return {
61
+ markdown: result.markdown,
62
+ activeFilePath: result.activeFilePath,
63
+ dependencies: result.included.map((i) => i.filePath),
64
+ unresolved: result.unresolved
65
+ };
66
+ });
67
+ app.setNotFoundHandler(async (req, reply) => {
68
+ void req;
69
+ return reply.status(404).send({ error: "Not found" });
70
+ });
71
+ await app.listen({ port, host });
72
+ log(`Listening on http://${host}:${port}`);
73
+ return { app, watcher };
74
+ }
75
+ var argv1 = process.argv[1] ? path.basename(process.argv[1]) : "";
76
+ if (argv1 === "cli.js" || argv1 === "cli.mjs" || argv1 === "cli.cjs" || argv1 === "cli.ts") {
77
+ startServer().catch((err) => {
78
+ console.error(picocolors.red(`[token-shrink] ${err.message}`));
79
+ process.exitCode = 1;
80
+ });
81
+ }
82
+
83
+ export {
84
+ startServer
85
+ };
86
+ //# sourceMappingURL=chunk-7H6PGILN.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli.ts"],"sourcesContent":["/**\n * HTTP/CLI server. Exposes the context assembler as a small Fastify service:\n *\n * GET /health -> liveness\n * POST /v1/context -> { activeFilePath, maxSkeletons?, includeStats? } => markdown\n *\n * Run as `token-shrink` or `node dist/cli.js`. The server watches `--root`\n * (defaults to cwd) and keeps the import graph + skeletons warm.\n */\n\nimport Fastify from 'fastify';\nimport picocolors from 'picocolors';\nimport path from 'node:path';\n\nimport { assemble } from './server/assembler.js';\nimport { createWatcher, type Matcher } from './watcher/sync.js';\nimport { warmGrammars } from './parser/wasm.js';\n\nexport interface CliOptions {\n port?: number;\n host?: string;\n root?: string;\n ignored?: Matcher[];\n silent?: boolean;\n}\n\nfunction parseArgv(argv = process.argv): Record<string, string> {\n const out: Record<string, string> = {};\n for (let i = 0; i < argv.length; i++) {\n const cur = argv[i];\n if (cur?.startsWith('--')) {\n const key = cur.slice(2);\n const next = argv[i + 1];\n if (next && !next.startsWith('--')) {\n out[key] = next;\n i++;\n } else {\n out[key] = 'true';\n }\n }\n }\n return out;\n}\n\n/** Start the Fastify server; returns the running instance + watcher handle. */\nexport async function startServer(opts: CliOptions = {}) {\n const args = parseArgv();\n const port = opts.port ?? Number(args.port ?? process.env.PORT ?? 3000);\n const host = opts.host ?? args.host ?? '0.0.0.0';\n const root = path.resolve(opts.root ?? args.root ?? process.cwd());\n const silent = opts.silent ?? args.silent === 'true';\n\n const log = (msg: string) => {\n if (!silent) console.log(picocolors.dim(msg));\n };\n\n // Warm grammars in the background so the first prune isn't slow.\n void warmGrammars().catch(() => {});\n\n const watcher = createWatcher({ root, ignored: opts.ignored });\n log(`Indexing ${root} in the background…`);\n void watcher.indexAll().then((n) => log(`Indexed ${n} files.`));\n\n const app = Fastify({ logger: !silent });\n\n app.get('/health', async () => ({\n status: 'ok',\n service: 'token-shrink',\n version: '2.0.0',\n root,\n indexed: watcher.cache.entries.size,\n }));\n\n app.post('/v1/context', async (req, reply) => {\n const body = (req.body ?? {}) as {\n activeFilePath?: string;\n maxSkeletons?: number;\n includeStats?: boolean;\n };\n if (!body.activeFilePath) {\n return reply.status(400).send({ error: '`activeFilePath` is required' });\n }\n const result = assemble(body.activeFilePath, watcher.cache.entries, {\n maxSkeletons: body.maxSkeletons,\n includeStats: body.includeStats,\n });\n return {\n markdown: result.markdown,\n activeFilePath: result.activeFilePath,\n dependencies: result.included.map((i) => i.filePath),\n unresolved: result.unresolved,\n };\n });\n\n app.setNotFoundHandler(async (req, reply) => {\n void req;\n return reply.status(404).send({ error: 'Not found' });\n });\n\n await app.listen({ port, host });\n log(`Listening on http://${host}:${port}`);\n\n return { app, watcher };\n}\n\n// Start when invoked directly (`node dist/cli.cjs` / `token-shrink`).\nconst argv1 = process.argv[1] ? path.basename(process.argv[1]) : '';\nif (\n argv1 === 'cli.js' || argv1 === 'cli.mjs' ||\n argv1 === 'cli.cjs' || argv1 === 'cli.ts'\n) {\n startServer().catch((err) => {\n console.error(picocolors.red(`[token-shrink] ${err.message}`));\n process.exitCode = 1;\n });\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/parser/registry.ts
4
+ import path from "path";
5
+ var BLOCK = { query: "(statement_block) @block", replacement: { token: "/* ... */" } };
6
+ var SWIFT_BLOCK = { query: "(statements) @block", replacement: { token: "/* ... */" } };
7
+ var GO_BLOCK = { query: "(block) @block", replacement: { token: "/* ... */" } };
8
+ var RUST_BLOCK = { query: "(block) @block", replacement: { token: "/* ... */" } };
9
+ var PY_BLOCK = { query: "(block) @block", replacement: { token: "pass" } };
10
+ var C_BLOCK = { query: "(compound_statement) @block", replacement: { token: "/* ... */" } };
11
+ var JAVA_BLOCK = {
12
+ query: "(block) @block",
13
+ replacement: { token: "/* ... */" }
14
+ };
15
+ var PHP_BLOCK = {
16
+ query: "(compound_statement) @block",
17
+ replacement: { token: "/* ... */" }
18
+ };
19
+ var KOTLIN_BLOCK = { query: "(block) @block", replacement: { token: "/* ... */" } };
20
+ var DART_BLOCK = { query: "(block) @block", replacement: { token: "/* ... */" } };
21
+ var DIRECTIVE_KEEP = {
22
+ token: "/* ... */",
23
+ keepIf: /^\s*'use (client|server)'/
24
+ };
25
+ var registry = {
26
+ typescript: {
27
+ name: "TypeScript",
28
+ wasm: "tree-sitter-typescript.wasm",
29
+ url: "https://github.com/tree-sitter/tree-sitter-typescript/releases/latest/download/tree-sitter-typescript.wasm",
30
+ extensions: [".ts", ".cts", ".mts"],
31
+ rules: [BLOCK]
32
+ },
33
+ javascript: {
34
+ name: "JavaScript",
35
+ wasm: "tree-sitter-javascript.wasm",
36
+ url: "https://github.com/tree-sitter/tree-sitter-javascript/releases/latest/download/tree-sitter-javascript.wasm",
37
+ extensions: [".js", ".cjs", ".mjs"],
38
+ rules: [BLOCK]
39
+ },
40
+ tsx: {
41
+ name: "React / Next.js (TSX)",
42
+ wasm: "tree-sitter-tsx.wasm",
43
+ url: "https://github.com/tree-sitter/tree-sitter-typescript/releases/latest/download/tree-sitter-tsx.wasm",
44
+ extensions: [".tsx"],
45
+ rules: [{ query: "(statement_block) @block", replacement: DIRECTIVE_KEEP }]
46
+ },
47
+ jsx: {
48
+ name: "React (JSX)",
49
+ wasm: "tree-sitter-javascript.wasm",
50
+ url: "https://github.com/tree-sitter/tree-sitter-javascript/releases/latest/download/tree-sitter-javascript.wasm",
51
+ extensions: [".jsx"],
52
+ rules: [{ query: "(statement_block) @block", replacement: DIRECTIVE_KEEP }]
53
+ },
54
+ python: {
55
+ name: "Python",
56
+ wasm: "tree-sitter-python.wasm",
57
+ url: "https://github.com/tree-sitter/tree-sitter-python/releases/latest/download/tree-sitter-python.wasm",
58
+ extensions: [".py", ".pyi"],
59
+ rules: [PY_BLOCK]
60
+ },
61
+ dart: {
62
+ name: "Dart / Flutter",
63
+ wasm: "tree-sitter-dart.wasm",
64
+ url: "https://github.com/UserNobody14/tree-sitter-dart.wasm/releases/latest/download/tree-sitter-dart.wasm",
65
+ extensions: [".dart"],
66
+ rules: [DART_BLOCK]
67
+ },
68
+ swift: {
69
+ name: "Swift / SwiftUI",
70
+ wasm: "tree-sitter-swift.wasm",
71
+ url: "https://github.com/alex-pinkus/tree-sitter-swift/releases/latest/download/tree-sitter-swift.wasm",
72
+ extensions: [".swift"],
73
+ rules: [SWIFT_BLOCK]
74
+ },
75
+ go: {
76
+ name: "Go",
77
+ wasm: "tree-sitter-go.wasm",
78
+ url: "https://github.com/tree-sitter/tree-sitter-go/releases/latest/download/tree-sitter-go.wasm",
79
+ extensions: [".go"],
80
+ rules: [GO_BLOCK]
81
+ },
82
+ rust: {
83
+ name: "Rust",
84
+ wasm: "tree-sitter-rust.wasm",
85
+ url: "https://github.com/tree-sitter/tree-sitter-rust/releases/latest/download/tree-sitter-rust.wasm",
86
+ extensions: [".rs"],
87
+ rules: [RUST_BLOCK]
88
+ },
89
+ java: {
90
+ name: "Java",
91
+ wasm: "tree-sitter-java.wasm",
92
+ url: "https://github.com/tree-sitter/tree-sitter-java/releases/latest/download/tree-sitter-java.wasm",
93
+ extensions: [".java"],
94
+ rules: [JAVA_BLOCK]
95
+ },
96
+ kotlin: {
97
+ name: "Kotlin",
98
+ wasm: "tree-sitter-kotlin.wasm",
99
+ url: "https://github.com/fwcd/tree-sitter-kotlin/releases/latest/download/tree-sitter-kotlin.wasm",
100
+ extensions: [".kt", ".kts"],
101
+ rules: [KOTLIN_BLOCK]
102
+ },
103
+ c: {
104
+ name: "C",
105
+ wasm: "tree-sitter-c.wasm",
106
+ url: "https://github.com/tree-sitter/tree-sitter-c/releases/latest/download/tree-sitter-c.wasm",
107
+ extensions: [".c", ".h"],
108
+ rules: [C_BLOCK]
109
+ },
110
+ cpp: {
111
+ name: "C++",
112
+ wasm: "tree-sitter-cpp.wasm",
113
+ url: "https://github.com/tree-sitter/tree-sitter-cpp/releases/latest/download/tree-sitter-cpp.wasm",
114
+ extensions: [".cc", ".cpp", ".cxx", ".hpp", ".hh", ".hxx"],
115
+ rules: [C_BLOCK]
116
+ },
117
+ php: {
118
+ name: "PHP",
119
+ wasm: "tree-sitter-php.wasm",
120
+ url: "https://github.com/tree-sitter/tree-sitter-php/releases/latest/download/tree-sitter-php.wasm",
121
+ extensions: [".php"],
122
+ rules: [PHP_BLOCK]
123
+ }
124
+ };
125
+ var extensionIndex = /* @__PURE__ */ new Map();
126
+ for (const [id, spec] of Object.entries(registry)) {
127
+ for (const ext of spec.extensions) {
128
+ extensionIndex.set(ext, id);
129
+ }
130
+ }
131
+ function languageForFile(filePath) {
132
+ const langId = extensionIndex.get(path.extname(filePath).toLowerCase());
133
+ if (!langId) return null;
134
+ return registry[langId];
135
+ }
136
+ var supportedLanguageIds = Object.keys(registry);
137
+ var allExtensions = [...extensionIndex.keys()];
138
+
139
+ export {
140
+ registry,
141
+ languageForFile,
142
+ supportedLanguageIds,
143
+ allExtensions
144
+ };
145
+ //# sourceMappingURL=chunk-7SQ6HMWM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/parser/registry.ts"],"sourcesContent":["/**\n * Language registry mapping file extensions to tree-sitter grammars and\n * pruning strategies. Each spec defines:\n * - the web-tree-sitter language name\n * - the `.wasm` file and the authoritative download URL\n * - one or more S-expression queries (the \"prune query\") that match the\n * implementation blocks to remove, plus the replacement token.\n *\n * The empty-query default of '' matches nothing, so the pruner leaves the\n * file untouched when a language has no registered prune rules yet.\n */\n\nimport path from 'node:path';\n\n/** Source of truth mirror in the PRD language matrix. */\nexport type Replacement = {\n /** Token used to replace a pruned block. */\n token: string;\n /**\n * Optional regex, run against the raw block text. If it matches, the block\n * is kept instead of pruned. Used to preserve directive/header lines\n * ('use client/server') even when they live inside an otherwise-pruned body.\n */\n keepIf?: RegExp;\n};\n\nexport interface LanguageSpec {\n /** Friendly name, used in logs and the report header. */\n name: string;\n /** web-tree-sitter `Language.load` file name. */\n wasm: string;\n /** Where to download the `.wasm` binary if not cached locally. */\n url: string;\n /** Extensions that map to this language: `.ts`, `.tsx`, etc. */\n extensions: string[];\n /**\n * One or more (query, replacement) pairs. Blocks matched by each query are\n * pruned bottom-up. Queries are combined; a block is pruned if it matches\n * any. This keeps small, frequently repeated constructs cheap to configure.\n */\n rules: { query: string; replacement: Replacement }[];\n}\n\n// ---------------------------------------------------------------------------\n// Pruning workhorse: prune every `statement_block` / `block` node.\n// ---------------------------------------------------------------------------\n\nconst BLOCK = { query: '(statement_block) @block', replacement: { token: '/* ... */' } };\nconst SWIFT_BLOCK = { query: '(statements) @block', replacement: { token: '/* ... */' } };\nconst GO_BLOCK = { query: '(block) @block', replacement: { token: '/* ... */' } };\nconst RUST_BLOCK = { query: '(block) @block', replacement: { token: '/* ... */' } };\nconst PY_BLOCK = { query: '(block) @block', replacement: { token: 'pass' } };\nconst C_BLOCK = { query: '(compound_statement) @block', replacement: { token: '/* ... */' } };\nconst JAVA_BLOCK = {\n query: '(block) @block',\n replacement: { token: '/* ... */' },\n};\nconst PHP_BLOCK = {\n query: '(compound_statement) @block',\n replacement: { token: '/* ... */' },\n};\nconst KOTLIN_BLOCK = { query: '(block) @block', replacement: { token: '/* ... */' } };\nconst DART_BLOCK = { query: '(block) @block', replacement: { token: '/* ... */' } };\n\n// Keeps `'use client'`/`'use server'` and top-level directive strings.\nconst DIRECTIVE_KEEP: Replacement = {\n token: '/* ... */',\n keepIf: /^\\s*'use (client|server)'/,\n};\n\nexport const registry: Record<string, LanguageSpec> = {\n typescript: {\n name: 'TypeScript',\n wasm: 'tree-sitter-typescript.wasm',\n url: 'https://github.com/tree-sitter/tree-sitter-typescript/releases/latest/download/tree-sitter-typescript.wasm',\n extensions: ['.ts', '.cts', '.mts'],\n rules: [BLOCK],\n },\n javascript: {\n name: 'JavaScript',\n wasm: 'tree-sitter-javascript.wasm',\n url: 'https://github.com/tree-sitter/tree-sitter-javascript/releases/latest/download/tree-sitter-javascript.wasm',\n extensions: ['.js', '.cjs', '.mjs'],\n rules: [BLOCK],\n },\n tsx: {\n name: 'React / Next.js (TSX)',\n wasm: 'tree-sitter-tsx.wasm',\n url: 'https://github.com/tree-sitter/tree-sitter-typescript/releases/latest/download/tree-sitter-tsx.wasm',\n extensions: ['.tsx'],\n rules: [{ query: '(statement_block) @block', replacement: DIRECTIVE_KEEP }],\n },\n jsx: {\n name: 'React (JSX)',\n wasm: 'tree-sitter-javascript.wasm',\n url: 'https://github.com/tree-sitter/tree-sitter-javascript/releases/latest/download/tree-sitter-javascript.wasm',\n extensions: ['.jsx'],\n rules: [{ query: '(statement_block) @block', replacement: DIRECTIVE_KEEP }],\n },\n python: {\n name: 'Python',\n wasm: 'tree-sitter-python.wasm',\n url: 'https://github.com/tree-sitter/tree-sitter-python/releases/latest/download/tree-sitter-python.wasm',\n extensions: ['.py', '.pyi'],\n rules: [PY_BLOCK],\n },\n dart: {\n name: 'Dart / Flutter',\n wasm: 'tree-sitter-dart.wasm',\n url: 'https://github.com/UserNobody14/tree-sitter-dart.wasm/releases/latest/download/tree-sitter-dart.wasm',\n extensions: ['.dart'],\n rules: [DART_BLOCK],\n },\n swift: {\n name: 'Swift / SwiftUI',\n wasm: 'tree-sitter-swift.wasm',\n url: 'https://github.com/alex-pinkus/tree-sitter-swift/releases/latest/download/tree-sitter-swift.wasm',\n extensions: ['.swift'],\n rules: [SWIFT_BLOCK],\n },\n go: {\n name: 'Go',\n wasm: 'tree-sitter-go.wasm',\n url: 'https://github.com/tree-sitter/tree-sitter-go/releases/latest/download/tree-sitter-go.wasm',\n extensions: ['.go'],\n rules: [GO_BLOCK],\n },\n rust: {\n name: 'Rust',\n wasm: 'tree-sitter-rust.wasm',\n url: 'https://github.com/tree-sitter/tree-sitter-rust/releases/latest/download/tree-sitter-rust.wasm',\n extensions: ['.rs'],\n rules: [RUST_BLOCK],\n },\n java: {\n name: 'Java',\n wasm: 'tree-sitter-java.wasm',\n url: 'https://github.com/tree-sitter/tree-sitter-java/releases/latest/download/tree-sitter-java.wasm',\n extensions: ['.java'],\n rules: [JAVA_BLOCK],\n },\n kotlin: {\n name: 'Kotlin',\n wasm: 'tree-sitter-kotlin.wasm',\n url: 'https://github.com/fwcd/tree-sitter-kotlin/releases/latest/download/tree-sitter-kotlin.wasm',\n extensions: ['.kt', '.kts'],\n rules: [KOTLIN_BLOCK],\n },\n c: {\n name: 'C',\n wasm: 'tree-sitter-c.wasm',\n url: 'https://github.com/tree-sitter/tree-sitter-c/releases/latest/download/tree-sitter-c.wasm',\n extensions: ['.c', '.h'],\n rules: [C_BLOCK],\n },\n cpp: {\n name: 'C++',\n wasm: 'tree-sitter-cpp.wasm',\n url: 'https://github.com/tree-sitter/tree-sitter-cpp/releases/latest/download/tree-sitter-cpp.wasm',\n extensions: ['.cc', '.cpp', '.cxx', '.hpp', '.hh', '.hxx'],\n rules: [C_BLOCK],\n },\n php: {\n name: 'PHP',\n wasm: 'tree-sitter-php.wasm',\n url: 'https://github.com/tree-sitter/tree-sitter-php/releases/latest/download/tree-sitter-php.wasm',\n extensions: ['.php'],\n rules: [PHP_BLOCK],\n },\n} satisfies Record<string, LanguageSpec>;\n\n/** Map of extension -> language spec id, built once. */\nconst extensionIndex = new Map<string, string>();\nfor (const [id, spec] of Object.entries(registry)) {\n for (const ext of spec.extensions) {\n extensionIndex.set(ext, id);\n }\n}\n\nexport type LanguageId = keyof typeof registry;\n\n/** Return the language spec for a file path, or null if unsupported. */\nexport function languageForFile(filePath: string): LanguageSpec | null {\n const langId = extensionIndex.get(path.extname(filePath).toLowerCase());\n if (!langId) return null;\n return registry[langId];\n}\n\n/** All languages that have at least one prune rule. */\nexport const supportedLanguageIds = Object.keys(registry) as LanguageId[];\n\n/** Convenience: list every known extension. */\nexport const allExtensions = [...extensionIndex.keys()];\n"],"mappings":"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","names":[]}