@vicentelyrio/netherite 0.0.0-stage → 1.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.
Files changed (30) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +86 -2
  3. package/dist/app/assets/index-B8ZfC4FJ.css +1 -0
  4. package/dist/app/assets/index-BHFWtT7b.js +185 -0
  5. package/dist/app/assets/material-symbols-rounded-latin-fill-normal-C51MhZg_.woff2 +0 -0
  6. package/dist/app/assets/source-code-pro-cyrillic-ext-wght-normal-BODHZjil.woff2 +0 -0
  7. package/dist/app/assets/source-code-pro-cyrillic-wght-normal-aqTvhEeI.woff2 +0 -0
  8. package/dist/app/assets/source-code-pro-greek-wght-normal-BvT6WsG1.woff2 +0 -0
  9. package/dist/app/assets/source-code-pro-latin-ext-wght-normal-0UZNSh4u.woff2 +0 -0
  10. package/dist/app/assets/source-code-pro-latin-wght-normal-BP8Zz55n.woff2 +0 -0
  11. package/dist/app/assets/source-code-pro-vietnamese-wght-normal-B6x53GZG.woff2 +0 -0
  12. package/dist/app/assets/source-sans-3-cyrillic-ext-wght-italic-F_qGh3gi.woff2 +0 -0
  13. package/dist/app/assets/source-sans-3-cyrillic-ext-wght-normal-c-5QAGAI.woff2 +0 -0
  14. package/dist/app/assets/source-sans-3-cyrillic-wght-italic-BqzjwPnH.woff2 +0 -0
  15. package/dist/app/assets/source-sans-3-cyrillic-wght-normal-BMDVbyM7.woff2 +0 -0
  16. package/dist/app/assets/source-sans-3-greek-ext-wght-italic-DBLPjGyR.woff2 +0 -0
  17. package/dist/app/assets/source-sans-3-greek-ext-wght-normal-BXZfsJY4.woff2 +0 -0
  18. package/dist/app/assets/source-sans-3-greek-wght-italic-CXgqVJLg.woff2 +0 -0
  19. package/dist/app/assets/source-sans-3-greek-wght-normal-Cc-u-6OI.woff2 +0 -0
  20. package/dist/app/assets/source-sans-3-latin-ext-wght-italic-B1mMy0tD.woff2 +0 -0
  21. package/dist/app/assets/source-sans-3-latin-ext-wght-normal-C8iNium2.woff2 +0 -0
  22. package/dist/app/assets/source-sans-3-latin-wght-italic-DLiStKT6.woff2 +0 -0
  23. package/dist/app/assets/source-sans-3-latin-wght-normal-Bo_j9asI.woff2 +0 -0
  24. package/dist/app/assets/source-sans-3-vietnamese-wght-italic-Obd_lscc.woff2 +0 -0
  25. package/dist/app/assets/source-sans-3-vietnamese-wght-normal-BDs68eHQ.woff2 +0 -0
  26. package/dist/app/index.html +13 -0
  27. package/dist/bin.mjs +11181 -0
  28. package/dist/dist-BZlxnGRh.mjs +260 -0
  29. package/dist/markdown.mjs +2 -0
  30. package/package.json +81 -3
@@ -0,0 +1,260 @@
1
+ #!/usr/bin/env node
2
+ import { gfmFootnoteFromMarkdown } from "mdast-util-gfm-footnote";
3
+ import { gfmStrikethroughFromMarkdown } from "mdast-util-gfm-strikethrough";
4
+ import { gfmTableFromMarkdown } from "mdast-util-gfm-table";
5
+ import { gfmTaskListItemFromMarkdown } from "mdast-util-gfm-task-list-item";
6
+ import { gfmFootnote } from "micromark-extension-gfm-footnote";
7
+ import { gfmStrikethrough } from "micromark-extension-gfm-strikethrough";
8
+ import { gfmTable } from "micromark-extension-gfm-table";
9
+ import { gfmTaskListItem } from "micromark-extension-gfm-task-list-item";
10
+ import remarkFrontmatter from "remark-frontmatter";
11
+ import remarkGfm from "remark-gfm";
12
+ import remarkParse from "remark-parse";
13
+ import { unified } from "unified";
14
+ import { LineCounter, isMap, isScalar, parseDocument } from "yaml";
15
+ import { SKIP, visit } from "unist-util-visit";
16
+ // ../../packages/markdown/dist/index.js
17
+ /**
18
+ * GitHub Flavored Markdown without autolink literals: tables, task lists, strikethrough and
19
+ * footnotes. Finding bare URLs and e-mail addresses in text is the slowest part of GFM and
20
+ * the index does not use it, so parsing files for the index is about a third faster. Rendering
21
+ * uses the full `remark-gfm`.
22
+ */
23
+ const remarkGfmLite = function() {
24
+ const data = this.data();
25
+ data.micromarkExtensions ??= [];
26
+ data.fromMarkdownExtensions ??= [];
27
+ const micromark = data.micromarkExtensions;
28
+ const fromMarkdown = data.fromMarkdownExtensions;
29
+ micromark.push(gfmTaskListItem(), gfmTable(), gfmStrikethrough(), gfmFootnote());
30
+ fromMarkdown.push(gfmTaskListItemFromMarkdown(), gfmTableFromMarkdown(), gfmStrikethroughFromMarkdown(), gfmFootnoteFromMarkdown());
31
+ };
32
+ /** GitHub-style heading slug: lower case, punctuation removed, spaces become hyphens. */
33
+ const slugify = (text) => text.toLowerCase().trim().replace(/[^\p{L}\p{N}\s-]/gu, "").replace(/\s/g, "-");
34
+ /** Slugs for a list of headings in order, adding `-1`, `-2` to repeats. */
35
+ const uniqueSlugs = (texts) => {
36
+ const seen = /* @__PURE__ */ new Map();
37
+ return texts.map((text) => {
38
+ const slug = slugify(text);
39
+ const count = seen.get(slug) ?? 0;
40
+ seen.set(slug, count + 1);
41
+ return count === 0 ? slug : `${slug}-${count}`;
42
+ });
43
+ };
44
+ const pattern = /(?<!\\)\[\[([^[\]|#\n]+?)(?:#([^[\]|\n]+?))?(?:\|([^[\]\n]+?))?\]\]/g;
45
+ /** Finds wikilinks in raw text. Offsets are relative to `text`. */
46
+ const findWikiLinks = (text) => {
47
+ const found = [];
48
+ for (const match of text.matchAll(pattern)) {
49
+ const target = match[1]?.trim();
50
+ if (!target) continue;
51
+ const data = { target };
52
+ const heading = match[2]?.trim();
53
+ const label = match[3]?.trim();
54
+ if (heading) data.heading = heading;
55
+ if (label) data.label = label;
56
+ found.push({
57
+ start: match.index,
58
+ end: match.index + match[0].length,
59
+ data,
60
+ raw: match[0]
61
+ });
62
+ }
63
+ return found;
64
+ };
65
+ /**
66
+ * remark plugin that turns wikilinks in text into `wikiLink` nodes. It reads the raw source of
67
+ * each text node, so positions stay exact and escaped brackets are left alone.
68
+ */
69
+ const remarkWikiLinks = () => (tree, file) => {
70
+ const source = String(file.value);
71
+ visit(tree, "text", (node, index, parent) => {
72
+ const position = node.position;
73
+ const start = position?.start.offset;
74
+ const end = position?.end.offset;
75
+ if (!position || start === void 0 || end === void 0 || !parent || index === void 0) return;
76
+ const raw = source.slice(start, end);
77
+ const links = findWikiLinks(raw);
78
+ if (links.length === 0) return;
79
+ const at = (offset) => {
80
+ const lines = raw.slice(0, offset).split("\n");
81
+ const last = lines.at(-1) ?? "";
82
+ return {
83
+ line: position.start.line + lines.length - 1,
84
+ column: lines.length === 1 ? position.start.column + last.length : last.length + 1,
85
+ offset: start + offset
86
+ };
87
+ };
88
+ const replacement = [];
89
+ let cursor = 0;
90
+ for (const link of links) {
91
+ if (link.start > cursor) replacement.push({
92
+ type: "text",
93
+ value: raw.slice(cursor, link.start),
94
+ position: {
95
+ start: at(cursor),
96
+ end: at(link.start)
97
+ }
98
+ });
99
+ replacement.push({
100
+ type: "wikiLink",
101
+ value: link.raw,
102
+ data: link.data,
103
+ position: {
104
+ start: at(link.start),
105
+ end: at(link.end)
106
+ }
107
+ });
108
+ cursor = link.end;
109
+ }
110
+ if (cursor < raw.length) replacement.push({
111
+ type: "text",
112
+ value: raw.slice(cursor),
113
+ position: {
114
+ start: at(cursor),
115
+ end: position.end
116
+ }
117
+ });
118
+ parent.children.splice(index, 1, ...replacement);
119
+ return [SKIP, index + replacement.length];
120
+ });
121
+ };
122
+ /**
123
+ * The unified processor Netherite uses. The app renders with the same plugins (ADR-00011).
124
+ * By default it supports full GFM; `autolinks: false` leaves out bare-URL detection, which is
125
+ * not needed to index a file and costs about a third of the parse time.
126
+ */
127
+ const createProcessor = ({ autolinks = true } = {}) => unified().use(remarkParse).use(remarkFrontmatter, ["yaml"]).use(autolinks ? remarkGfm : remarkGfmLite).use(remarkWikiLinks);
128
+ const processor = createProcessor({ autolinks: false });
129
+ const inlineText = (node) => {
130
+ if ("value" in node && node.type !== "yaml") return node.type === "wikiLink" ? node.data.label ?? node.data.target : String(node.value);
131
+ if ("children" in node) return node.children.map(inlineText).join("");
132
+ if (node.type === "image") return node.alt ?? "";
133
+ return "";
134
+ };
135
+ const plainText = (tree) => {
136
+ const lines = [];
137
+ const walk = (node) => {
138
+ if (node.type === "yaml") return;
139
+ if (node.type === "code") {
140
+ lines.push(node.value);
141
+ return;
142
+ }
143
+ if ((node.type === "paragraph" || node.type === "heading" || node.type === "tableRow") && "children" in node) {
144
+ lines.push(node.type === "tableRow" ? node.children.map(inlineText).join(" ") : node.children.map(inlineText).join(""));
145
+ return;
146
+ }
147
+ if ("children" in node) for (const child of node.children) walk(child);
148
+ };
149
+ walk(tree);
150
+ return lines.filter((line) => line.length > 0).join("\n");
151
+ };
152
+ const readFrontmatter = (tree) => {
153
+ const node = tree.children[0];
154
+ if (node?.type !== "yaml" || !node.position) return void 0;
155
+ const start = node.position.start.offset ?? 0;
156
+ const end = node.position.end.offset ?? start;
157
+ const raw = node.value;
158
+ const lineCounter = new LineCounter();
159
+ const doc = parseDocument(raw, { lineCounter });
160
+ const firstLine = node.position.start.line + 1;
161
+ const errors = doc.errors.map((error) => ({
162
+ message: error.message.split("\n")[0] ?? error.message,
163
+ line: (error.linePos?.[0].line ?? 1) + firstLine - 1,
164
+ column: error.linePos?.[0].col ?? 1
165
+ }));
166
+ const fields = {};
167
+ if (isMap(doc.contents)) for (const pair of doc.contents.items) {
168
+ if (!isScalar(pair.key) || !pair.key.range) continue;
169
+ const { line, col } = lineCounter.linePos(pair.key.range[0]);
170
+ fields[String(pair.key.value)] = {
171
+ line: line + firstLine - 1,
172
+ column: col
173
+ };
174
+ }
175
+ return {
176
+ raw,
177
+ data: errors.length === 0 ? doc.toJS() : void 0,
178
+ errors,
179
+ fields,
180
+ start,
181
+ end
182
+ };
183
+ };
184
+ const markerPattern = /^[ \t]*(?:[-*+]|\d+[.)])[ \t]+\[( |x|X)\]/;
185
+ /**
186
+ * Parses a Markdown file with optional YAML frontmatter and extracts what Netherite needs:
187
+ * links, headings, the checklist and plain text, all with source positions.
188
+ */
189
+ const parseMarkdown = (source) => {
190
+ const file = { value: source };
191
+ const tree = processor.runSync(processor.parse(source), file);
192
+ const frontmatter = readFrontmatter(tree);
193
+ const headingNodes = [];
194
+ const links = [];
195
+ const items = [];
196
+ const walk = (node, listDepth) => {
197
+ if (node.type === "heading") headingNodes.push(node);
198
+ if (node.type === "wikiLink" && node.position) links.push({
199
+ ...node.data,
200
+ raw: node.value,
201
+ position: node.position
202
+ });
203
+ if (node.type === "listItem") items.push({
204
+ node,
205
+ depth: listDepth - 1
206
+ });
207
+ if ("children" in node) {
208
+ const next = node.type === "list" ? listDepth + 1 : listDepth;
209
+ for (const child of node.children) walk(child, next);
210
+ }
211
+ };
212
+ walk(tree, 0);
213
+ const slugs = uniqueSlugs(headingNodes.map((h) => inlineText(h)));
214
+ const headings = headingNodes.map((node, i) => {
215
+ const next = headingNodes.slice(i + 1).find((candidate) => candidate.depth <= node.depth);
216
+ return {
217
+ depth: node.depth,
218
+ text: inlineText(node),
219
+ slug: slugs[i],
220
+ position: node.position,
221
+ sectionEnd: next?.position?.start.offset ?? source.length
222
+ };
223
+ });
224
+ const headingBefore = (offset) => headings.filter((h) => (h.position.start.offset ?? 0) < offset).at(-1)?.text;
225
+ const checklist = [];
226
+ for (const { node, depth } of items) {
227
+ if (node.checked === null || node.checked === void 0 || !node.position) continue;
228
+ const start = node.position.start.offset ?? 0;
229
+ const lineStart = source.lastIndexOf("\n", start - 1) + 1;
230
+ const match = markerPattern.exec(source.slice(lineStart, node.position.end.offset));
231
+ if (!match) continue;
232
+ const first = node.children[0];
233
+ checklist.push({
234
+ checked: node.checked,
235
+ text: first && first.type === "paragraph" ? inlineText(first).split("\n")[0] ?? "" : "",
236
+ position: node.position,
237
+ toggleOffset: lineStart + match[0].length - 2,
238
+ depth,
239
+ section: headingBefore(start)
240
+ });
241
+ }
242
+ return {
243
+ source,
244
+ frontmatter,
245
+ tree,
246
+ bodyStart: frontmatter ? frontmatter.end : 0,
247
+ headings,
248
+ links,
249
+ checklist,
250
+ text: plainText(tree)
251
+ };
252
+ };
253
+ /** Like `parseMarkdown`, but drops the tree. Use it to index many files. */
254
+ const outlineMarkdown = (source) => {
255
+ const { tree: _tree, ...outline } = parseMarkdown(source);
256
+ return outline;
257
+ };
258
+ export { outlineMarkdown as t };
259
+
260
+ //# sourceMappingURL=dist-BZlxnGRh.mjs.map
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export { t as outlineMarkdown } from "./dist-BZlxnGRh.mjs";
package/package.json CHANGED
@@ -1,6 +1,84 @@
1
1
  {
2
2
  "name": "@vicentelyrio/netherite",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.0.0",
4
+ "description": "A typed, linked knowledge base in Markdown and git: a CLI, an MCP server for agents, and a local app.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/vicentelyrio/netherite.git",
9
+ "directory": "apps/netherite"
10
+ },
11
+ "homepage": "https://vicentelyrio.github.io/netherite/",
12
+ "bugs": {
13
+ "url": "https://github.com/vicentelyrio/netherite/issues"
14
+ },
15
+ "keywords": [
16
+ "knowledge-base",
17
+ "markdown",
18
+ "git",
19
+ "mcp",
20
+ "agents",
21
+ "adr",
22
+ "tasks"
23
+ ],
24
+ "type": "module",
25
+ "bin": {
26
+ "netherite": "./dist/bin.mjs"
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "!dist/**/*.map"
31
+ ],
32
+ "engines": {
33
+ "node": ">=24"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public",
37
+ "provenance": true
38
+ },
39
+ "dependencies": {
40
+ "@effect/platform-node": "4.1.1",
41
+ "@modelcontextprotocol/sdk": "^1.32.2",
42
+ "@parcel/watcher": "^2.6.0",
43
+ "effect": "4.1.1",
44
+ "graphology": "^0.26.0",
45
+ "mdast-util-gfm-footnote": "^2.1.0",
46
+ "mdast-util-gfm-strikethrough": "^2.0.1",
47
+ "mdast-util-gfm-table": "^2.0.0",
48
+ "mdast-util-gfm-task-list-item": "^2.0.0",
49
+ "micromark-extension-gfm-footnote": "^2.1.0",
50
+ "micromark-extension-gfm-strikethrough": "^2.1.0",
51
+ "micromark-extension-gfm-table": "^2.1.2",
52
+ "micromark-extension-gfm-task-list-item": "^2.1.0",
53
+ "minisearch": "^7.2.0",
54
+ "picomatch": "^4.0.7",
55
+ "remark-frontmatter": "^5.0.0",
56
+ "remark-gfm": "^4.0.1",
57
+ "remark-parse": "^11.0.0",
58
+ "unified": "^11.0.5",
59
+ "unist-util-visit": "^5.2.0",
60
+ "yaml": "^2.9.1"
61
+ },
62
+ "devDependencies": {
63
+ "@netherite/api": "0.0.0",
64
+ "@netherite/cli": "0.0.0",
65
+ "@netherite/config": "0.0.0",
66
+ "@netherite/core": "0.0.0",
67
+ "@netherite/git": "0.0.0",
68
+ "@netherite/markdown": "0.0.0",
69
+ "@netherite/mcp": "0.0.0",
70
+ "@netherite/schema": "0.0.0",
71
+ "@netherite/server": "0.0.0",
72
+ "@netherite/web": "0.0.0",
73
+ "@types/node": "^24.0.0",
74
+ "tsdown": "^0.23.4",
75
+ "typescript": "^7.0.2",
76
+ "vitest": "^5.0.5"
77
+ },
78
+ "scripts": {
79
+ "build": "tsdown && node scripts/bundle-app.mjs",
80
+ "typecheck": "tsc",
81
+ "test": "vitest run --passWithNoTests",
82
+ "smoke": "node scripts/smoke.mjs"
83
+ }
6
84
  }