@aisystemresources/emdee 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.
Files changed (82) hide show
  1. package/README.md +85 -0
  2. package/bin/emdee.js +196 -0
  3. package/package.json +78 -0
  4. package/src/cli/read-commands.ts +64 -0
  5. package/src/core/indexer.ts +346 -0
  6. package/src/core/parseEdges.ts +106 -0
  7. package/src/core/resolveLink.ts +136 -0
  8. package/src/core/siblings.ts +65 -0
  9. package/src/core/syncDocEdges.ts +427 -0
  10. package/src/lib/cache/bust.ts +44 -0
  11. package/src/lib/cache/invalidation.ts +28 -0
  12. package/src/lib/mcp/activity.ts +254 -0
  13. package/src/lib/mcp/tools/add_association.ts +311 -0
  14. package/src/lib/mcp/tools/append_doc.ts +70 -0
  15. package/src/lib/mcp/tools/append_section.ts +121 -0
  16. package/src/lib/mcp/tools/create_child.ts +319 -0
  17. package/src/lib/mcp/tools/delete_doc.ts +68 -0
  18. package/src/lib/mcp/tools/distill_doc.ts +262 -0
  19. package/src/lib/mcp/tools/filename.ts +63 -0
  20. package/src/lib/mcp/tools/get_context.ts +227 -0
  21. package/src/lib/mcp/tools/get_doc.ts +91 -0
  22. package/src/lib/mcp/tools/get_image.ts +62 -0
  23. package/src/lib/mcp/tools/get_neighbors.ts +96 -0
  24. package/src/lib/mcp/tools/get_summary.ts +18 -0
  25. package/src/lib/mcp/tools/index.ts +26 -0
  26. package/src/lib/mcp/tools/lint.ts +552 -0
  27. package/src/lib/mcp/tools/lint_doc.ts +76 -0
  28. package/src/lib/mcp/tools/lint_gate.ts +49 -0
  29. package/src/lib/mcp/tools/list_docs.ts +18 -0
  30. package/src/lib/mcp/tools/list_summary_drift.ts +95 -0
  31. package/src/lib/mcp/tools/materialize_subgroup.ts +274 -0
  32. package/src/lib/mcp/tools/move_doc.ts +439 -0
  33. package/src/lib/mcp/tools/patch_preamble.ts +145 -0
  34. package/src/lib/mcp/tools/patch_section.ts +113 -0
  35. package/src/lib/mcp/tools/read_doc_section.ts +70 -0
  36. package/src/lib/mcp/tools/rename_doc.ts +167 -0
  37. package/src/lib/mcp/tools/restore_doc.ts +41 -0
  38. package/src/lib/mcp/tools/search.ts +36 -0
  39. package/src/lib/mcp/tools/sections.ts +130 -0
  40. package/src/lib/mcp/tools/split_doc.ts +129 -0
  41. package/src/lib/mcp/tools/trash_doc.ts +116 -0
  42. package/src/lib/mcp/tools/types.ts +7 -0
  43. package/src/lib/mcp/tools/upload_image.ts +77 -0
  44. package/src/lib/mcp/tools/validate_args.ts +36 -0
  45. package/src/lib/mcp/tools/vault.ts +430 -0
  46. package/src/lib/mcp/tools/write_doc.ts +81 -0
  47. package/src/lib/mcp/tools/write_doc_preview.ts +59 -0
  48. package/src/lib/owner/identity.ts +65 -0
  49. package/src/lib/storage/FilesystemStorage.ts +88 -0
  50. package/src/lib/storage/SupabaseStorage.ts +358 -0
  51. package/src/lib/storage/VaultStorage.ts +35 -0
  52. package/src/lib/storage/index.ts +41 -0
  53. package/src/lib/supabase/admin.ts +16 -0
  54. package/src/lib/supabase/client.ts +8 -0
  55. package/src/lib/supabase/oauth.ts +296 -0
  56. package/src/lib/supabase/server.ts +22 -0
  57. package/src/lib/system-nodes.ts +68 -0
  58. package/src/lib/trash/state.ts +88 -0
  59. package/src/mcp/server.ts +380 -0
  60. package/templates/types/CONCEPT.md +21 -0
  61. package/templates/types/HACKATHON.md +28 -0
  62. package/templates/types/NOVEL/CHARACTERS.md +29 -0
  63. package/templates/types/NOVEL/DRAFT.md +15 -0
  64. package/templates/types/NOVEL/EDITS.md +19 -0
  65. package/templates/types/NOVEL/INBOX.md +15 -0
  66. package/templates/types/NOVEL/INSTRUCTIONS.md +32 -0
  67. package/templates/types/NOVEL/LEARNINGS.md +9 -0
  68. package/templates/types/NOVEL/OUTBOX.md +15 -0
  69. package/templates/types/NOVEL/PLOT.md +27 -0
  70. package/templates/types/NOVEL/WORLDBUILDING.md +23 -0
  71. package/templates/types/NOVEL.md +32 -0
  72. package/templates/types/PERSON.md +27 -0
  73. package/templates/types/PROJECT/BRAND.md +23 -0
  74. package/templates/types/PROJECT/BUILD.md +9 -0
  75. package/templates/types/PROJECT/IDEAS.md +11 -0
  76. package/templates/types/PROJECT/INBOX.md +15 -0
  77. package/templates/types/PROJECT/INSTRUCTIONS.md +23 -0
  78. package/templates/types/PROJECT/LEARNINGS.md +9 -0
  79. package/templates/types/PROJECT/LOGS.md +9 -0
  80. package/templates/types/PROJECT/OUTBOX.md +15 -0
  81. package/templates/types/PROJECT/SPRINT.md +56 -0
  82. package/templates/types/PROJECT.md +26 -0
package/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # Silent Mane
2
+
3
+ Local-first knowledge graph backed by plain markdown. Humans browse it through a React renderer; agents (Claude, Cursor, Codex) read and write the same files through an MCP server. The vault is the source of truth — anything an LLM says traces back to a file you wrote.
4
+
5
+ ## Why
6
+
7
+ LLM agents need a stable, human-readable substrate to read and write their own context over time. Most knowledge-graph tools are either built for humans (Obsidian) or built for agents (vector stores). Silent Mane is a single substrate for both: the markdown a human edits is the exact bytes an agent reads, with no hidden index, no parallel summaries, no schema gymnastics. Build up a working journal that survives across sessions.
8
+
9
+ ## Status
10
+
11
+ Published on npm as `@aisystemresources/emdee`. The CLI (`emdee init`, `emdee mcp`, `emdee list`, `emdee drift-batch`) is globally installable; `emdee start` / `emdee serve-next` still require a repo checkout because they run the full Vite / Next viewer.
12
+
13
+ ## Install (consumer)
14
+
15
+ ```bash
16
+ npm install -g @aisystemresources/emdee
17
+ cd ~/my-vault
18
+ emdee init --nickname "Your Name" # writes ./docs/YOUR-NAME.md as the owner node
19
+ emdee list # your owner + 5 virtual system nodes
20
+ emdee mcp # stdio MCP server — point Claude Code / claude.ai at it
21
+ ```
22
+
23
+ ## Quick start (developer)
24
+
25
+ ```bash
26
+ git clone https://github.com/elz-ming/silent-mane.git
27
+ cd silent-mane
28
+ npm install
29
+ ./bin/mane.js init # seeds docs/ with the entry doc, conventions, and sample branch
30
+ npm run dev # Vite dev server with hot reload at http://localhost:5173
31
+ npm run mcp # MCP server over stdio (point Claude.ai / Cursor / Codex at it)
32
+ ```
33
+
34
+ `mane init` lays down:
35
+
36
+ - `docs/MANE.md` — vault entry point
37
+ - `docs/VAULT.md` — meta-pillar grouping the system docs below
38
+ - `docs/INFO.md` — conventions (filenames, relationships, writing format)
39
+ - `docs/INSTRUCTIONS.md` — CEO operating protocol for cross-project agents
40
+ - `docs/BRAIN.md` — cross-project distilled wisdom (always-loaded prior)
41
+ - `docs/WORKFLOWS.md` — concrete procedures the vault runs
42
+ - `docs/SAMPLE.md` + `docs/sample/` — pedagogical examples; delete with `rm -rf docs/sample/` once you've read them
43
+
44
+ Set `SILENT_MANE_ENTRY=your-file.md` to override the default entry name.
45
+
46
+
47
+ ## MCP tools
48
+
49
+ The MCP server (`mane mcp`) exposes:
50
+
51
+ - `list_docs` — every doc as `{path, title, summary}`. Cold-start enumeration.
52
+ - `get_summary(path)` — one doc's `{path, title, summary}`. Cheap.
53
+ - `get_neighbors(path)` — focal doc + 1-hop neighbors, categorized as `parents / children / associated`. Each neighbor carries the prose note attached to its wiki-link.
54
+ - `get_doc(path)` — full markdown plus per-section `content_hash` for safe patches.
55
+ - `search(query, limit?)` — substring match over titles, summaries, content.
56
+ - `append_section(path, heading, body, create_if_missing?)` — section-scoped append. Safer than `write_doc` for incremental edits.
57
+ - `patch_section(path, heading, body, expected_content_hash)` — version-guarded section replacement. Mismatched hash returns a structured `version_conflict`.
58
+ - `write_doc_preview(path, content)` — diff and list of removed sections before any full-file write.
59
+ - `write_doc(path, content)` — full-file replace (destructive; prefer the section-scoped tools).
60
+
61
+ ## Design principles
62
+
63
+ 1. **Markdown is the only source of truth.** No persisted index, no derived database, no parallel summaries.
64
+ 2. **Same substrate, different lenses.** Renderer and MCP read the same files via the same indexer. Nothing the LLM sees is invisible to the human.
65
+ 3. **Convention over schema.** Light structure — H1 + `> blockquote` summary + three relationship sections (`## Parent of`, `## Child of`, `## Associated with`). The LLM parses English natively; rigid schemas only add authoring friction.
66
+ 4. **Single summary per doc.** The blockquote under the H1 is the routing decision for both humans and LLMs.
67
+
68
+ ## What's in here
69
+
70
+ - `bin/mane.js` — the `mane` CLI (`init`, `start`, `mcp`)
71
+ - `src/core/indexer.ts` — walks `docs/`, parses wiki-links and relationship sections, derives summaries, skips fenced code blocks
72
+ - `src/mcp/server.ts` — MCP server with the tool surface above
73
+ - `src/web/` — React + TypeScript renderer (Toast UI Editor + Cytoscape egocentric graph, category-colored nodes)
74
+ - `src/server/dev-plugin.ts` — Vite middleware that serves the index in dev
75
+ - `api/index.ts` — Vercel serverless function that serves the index in prod
76
+ - `templates/` — vault seeds plus typed templates for `PROJECT`, `NOVEL`, `PERSON`, `HACKATHON`, `CONCEPT`. The engineering layer is type-agnostic — types are conventions plus templates, not schema. Adding a new type is one new file.
77
+
78
+ ## Conventions for the vault
79
+
80
+ The seeded `docs/INFO.md` is the full conventions reference: filename rules, the relationship grammar (first wiki-link on each bullet is the declared edge, inline links are context-only), the LEARNINGS authoring format, attribution lines for provenance. Read it once when you start a vault, refer back when you forget how something works.
81
+
82
+ ## Deploying to Vercel
83
+
84
+ Vercel auto-detects Vite. Set `SILENT_MANE_DOCS` (or commit a `docs/` for a public vault) and it will serve the SPA plus the `/api/index` endpoint. The default `.gitignore` excludes `docs/` so your vault stays private; remove that line if you want the vault public.
85
+
package/bin/emdee.js ADDED
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { spawn } from "node:child_process";
4
+ import { mkdir, writeFile, access } from "node:fs/promises";
5
+ import readline from "node:readline/promises";
6
+ import path from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+ const pkgRoot = path.resolve(__dirname, "..");
11
+
12
+ // SPRINT-090: `start` and `serve-next` shell out to Vite / Next.js against
13
+ // the full repo. Those files (app/, next.config.*, etc.) aren't in the
14
+ // published tarball, so when the CLI is installed via `npm install -g`
15
+ // those commands need to bail loudly rather than crash with a confusing
16
+ // Vite / Next stack.
17
+ async function ensureRepoCheckout(commandName) {
18
+ try {
19
+ await access(path.join(pkgRoot, "app"));
20
+ } catch {
21
+ console.error(
22
+ `emdee ${commandName} requires a repo checkout — clone https://github.com/AISystemResources/emdee\n` +
23
+ `The published npm package ships the CLI + MCP server only, not the web viewer.`
24
+ );
25
+ process.exit(1);
26
+ }
27
+ }
28
+
29
+ // SPRINT-093: keep owner-title derivation logic in lockstep with
30
+ // src/lib/owner/identity.ts. The published tarball is plain JS (no tsx),
31
+ // so this duplicates the 15-line function rather than pull in a runtime
32
+ // compilation step. `e2e/cli/init.spec.ts` pins the two copies together —
33
+ // if they ever diverge, the consistency test fails.
34
+ function normalizeOwnerTitle(input) {
35
+ const normalized = input
36
+ .trim()
37
+ .toUpperCase()
38
+ .replace(/[._\s]/g, "-")
39
+ .replace(/[^A-Z0-9-]/g, "")
40
+ .replace(/-+/g, "-")
41
+ .replace(/^-|-$/g, "");
42
+ return normalized || "OWNER";
43
+ }
44
+
45
+ function ownerNodeScaffold(title) {
46
+ return `# ${title}
47
+
48
+ > Your personal subtree. Top-level content (projects, people, notes, etc.) lives here. Renameable any time via \`rename_doc\` — inbound wiki-link references update atomically across the vault.
49
+
50
+ ## Child of
51
+
52
+ * [[EMDEE]]
53
+
54
+ ## Parent of
55
+
56
+ ## Associated with
57
+
58
+ ## Notes
59
+ `;
60
+ }
61
+
62
+ const program = new Command();
63
+ program.name("emdee").description("Emdee — local docs + knowledge graph + MCP").version("0.1.0");
64
+
65
+ program
66
+ .command("init")
67
+ .description("Create a docs/ folder with your owner node. The 5 system nodes (EMDEE, VAULT, SHARED, GRAVEYARD, IMAGES) are virtual — never written to disk.")
68
+ .option("--nickname <name>", "Display name for the owner node (required non-interactively)")
69
+ .action(async (opts) => {
70
+ const cwd = process.cwd();
71
+ const docsDir = path.join(cwd, "docs");
72
+ await mkdir(docsDir, { recursive: true });
73
+
74
+ let nickname = (opts.nickname ?? "").trim();
75
+ if (!nickname) {
76
+ if (!process.stdin.isTTY) {
77
+ console.error("emdee init needs --nickname when running non-interactively.");
78
+ process.exit(1);
79
+ }
80
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
81
+ nickname = (await rl.question("Your name (owner node title): ")).trim();
82
+ rl.close();
83
+ if (!nickname) {
84
+ console.error("emdee init: nickname cannot be empty.");
85
+ process.exit(1);
86
+ }
87
+ }
88
+
89
+ const title = normalizeOwnerTitle(nickname);
90
+ if (title === "OWNER") {
91
+ console.error(`emdee init: "${nickname}" normalised to the fallback OWNER — pick a name with ASCII letters.`);
92
+ process.exit(1);
93
+ }
94
+
95
+ const target = path.join(docsDir, `${title}.md`);
96
+ try {
97
+ await access(target);
98
+ console.log(`Already initialised at docs/${title}.md — leaving it alone.`);
99
+ } catch {
100
+ await writeFile(target, ownerNodeScaffold(title), "utf8");
101
+ console.log(`Created docs/${title}.md — your owner node.`);
102
+ }
103
+ console.log(
104
+ `\nThe 5 system nodes (EMDEE, VAULT, SHARED, GRAVEYARD, IMAGES) are virtual — they appear in \`emdee list\` and \`get_doc\` without being written to disk.`
105
+ );
106
+ });
107
+
108
+ program
109
+ .command("start")
110
+ .description("Start the Emdee viewer against ./docs — requires a repo checkout")
111
+ .option("-p, --port <port>", "port", "5173")
112
+ .option("-d, --docs <dir>", "docs directory", "docs")
113
+ .action(async (opts) => {
114
+ await ensureRepoCheckout("start");
115
+ const docs = path.resolve(process.cwd(), opts.docs);
116
+ const child = spawn("npx", ["vite", "--port", opts.port], {
117
+ cwd: pkgRoot,
118
+ stdio: "inherit",
119
+ env: { ...process.env, EMDEE_DOCS: docs },
120
+ });
121
+ child.on("exit", (code) => process.exit(code ?? 0));
122
+ });
123
+
124
+ program
125
+ .command("serve-next")
126
+ .description("Start the Emdee viewer using Next.js (App Router) — requires a repo checkout")
127
+ .option("-p, --port <port>", "port", "3000")
128
+ .option("-d, --docs <dir>", "docs directory", "docs")
129
+ .action(async (opts) => {
130
+ await ensureRepoCheckout("serve-next");
131
+ const docs = path.resolve(process.cwd(), opts.docs);
132
+ const child = spawn("npx", ["next", "dev", "--port", opts.port], {
133
+ cwd: pkgRoot,
134
+ stdio: "inherit",
135
+ env: { ...process.env, EMDEE_DOCS: docs },
136
+ });
137
+ child.on("exit", (code) => process.exit(code ?? 0));
138
+ });
139
+
140
+ program
141
+ .command("mcp")
142
+ .description("Run the Emdee MCP server over stdio")
143
+ .option("-d, --docs <dir>", "docs directory", "docs")
144
+ .action((opts) => {
145
+ const docs = path.resolve(process.cwd(), opts.docs);
146
+ const child = spawn("npx", ["tsx", path.join(pkgRoot, "src/mcp/server.ts")], {
147
+ cwd: pkgRoot,
148
+ stdio: "inherit",
149
+ env: { ...process.env, EMDEE_DOCS: docs },
150
+ });
151
+ child.on("exit", (code) => process.exit(code ?? 0));
152
+ });
153
+
154
+ program
155
+ .command("list")
156
+ .description("Print one doc path per line (token-cheap; local docs/ only)")
157
+ .option("-d, --docs <dir>", "docs directory", "docs")
158
+ .option("--prefix <prefix>", "filter to paths starting with this prefix")
159
+ .action((opts) => {
160
+ const docs = path.resolve(process.cwd(), opts.docs);
161
+ const args = ["tsx", path.join(pkgRoot, "src/cli/read-commands.ts"), "list"];
162
+ if (opts.prefix) args.push("--prefix", opts.prefix);
163
+ const child = spawn("npx", args, {
164
+ cwd: pkgRoot,
165
+ stdio: "inherit",
166
+ env: { ...process.env, EMDEE_DOCS: docs },
167
+ });
168
+ child.on("exit", (code) => process.exit(code ?? 0));
169
+ });
170
+
171
+ program
172
+ .command("drift-batch")
173
+ .description("Print a batch of docs (path + summary + body) for offline summariser workflows")
174
+ .option("-d, --docs <dir>", "docs directory", "docs")
175
+ .option("--limit <n>", "docs per batch", "10")
176
+ .option("--offset <k>", "skip the first K docs", "0")
177
+ .option("--prefix <prefix>", "filter to paths starting with this prefix")
178
+ .action((opts) => {
179
+ const docs = path.resolve(process.cwd(), opts.docs);
180
+ const args = [
181
+ "tsx",
182
+ path.join(pkgRoot, "src/cli/read-commands.ts"),
183
+ "drift-batch",
184
+ "--limit", opts.limit,
185
+ "--offset", opts.offset,
186
+ ];
187
+ if (opts.prefix) args.push("--prefix", opts.prefix);
188
+ const child = spawn("npx", args, {
189
+ cwd: pkgRoot,
190
+ stdio: "inherit",
191
+ env: { ...process.env, EMDEE_DOCS: docs },
192
+ });
193
+ child.on("exit", (code) => process.exit(code ?? 0));
194
+ });
195
+
196
+ program.parseAsync();
package/package.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "@aisystemresources/emdee",
3
+ "version": "0.1.0",
4
+ "description": "Local-first document management with markdown rendering, knowledge graph, and MCP server.",
5
+ "type": "module",
6
+ "bin": {
7
+ "emdee": "./bin/emdee.js"
8
+ },
9
+ "publishConfig": {
10
+ "access": "public"
11
+ },
12
+ "scripts": {
13
+ "dev": "next dev --webpack",
14
+ "build": "next build --webpack",
15
+ "start": "next start",
16
+ "lint": "eslint",
17
+ "mcp": "tsx src/mcp/server.ts",
18
+ "typecheck": "tsc --noEmit",
19
+ "e2e": "playwright test",
20
+ "e2e:ui": "playwright test --ui",
21
+ "e2e:report": "playwright show-report",
22
+ "check:migrations": "node scripts/check-migration-drift.mjs",
23
+ "ci-package": "node scripts/verify-package.mjs"
24
+ },
25
+ "files": [
26
+ "bin/",
27
+ "src/cli/",
28
+ "src/core/",
29
+ "src/lib/cache/",
30
+ "src/lib/mcp/",
31
+ "src/lib/storage/",
32
+ "src/lib/supabase/",
33
+ "src/lib/trash/",
34
+ "src/lib/owner/",
35
+ "src/lib/system-nodes.ts",
36
+ "src/mcp/",
37
+ "templates/",
38
+ "README.md"
39
+ ],
40
+ "dependencies": {
41
+ "@clerk/nextjs": "^7.3.3",
42
+ "@modelcontextprotocol/sdk": "^1.0.4",
43
+ "@sparticuz/chromium": "^149.0.0",
44
+ "@supabase/ssr": "^0.10.3",
45
+ "@supabase/supabase-js": "^2.105.4",
46
+ "@toast-ui/editor": "^3.2.2",
47
+ "commander": "^12.1.0",
48
+ "cytoscape": "^3.30.2",
49
+ "jszip": "^3.10.1",
50
+ "marked": "^18.0.4",
51
+ "next": "16.2.6",
52
+ "puppeteer-core": "^25.1.0",
53
+ "react": "19.2.4",
54
+ "react-dom": "19.2.4",
55
+ "tsx": "^4.22.4"
56
+ },
57
+ "devDependencies": {
58
+ "@clerk/testing": "^2.1.5",
59
+ "@playwright/test": "^1.61.0",
60
+ "@tailwindcss/postcss": "^4",
61
+ "@types/cytoscape": "^3.21.4",
62
+ "@types/node": "^20",
63
+ "@types/react": "^19",
64
+ "@types/react-dom": "^19",
65
+ "eslint": "^9",
66
+ "eslint-config-next": "16.2.6",
67
+ "tailwindcss": "^4",
68
+ "typescript": "^5"
69
+ },
70
+ "engines": {
71
+ "node": ">=20"
72
+ },
73
+ "overrides": {
74
+ "postcss": "^8.5.10",
75
+ "dompurify": "^3.4.11",
76
+ "esbuild": "^0.28.1"
77
+ }
78
+ }
@@ -0,0 +1,64 @@
1
+ import path from "node:path";
2
+ import { parseArgs } from "node:util";
3
+ import { buildIndex } from "../core/indexer";
4
+
5
+ const docsDir = path.resolve(process.env.EMDEE_DOCS ?? path.join(process.cwd(), "docs"));
6
+
7
+ async function cmdList(argv: string[]): Promise<void> {
8
+ const { values } = parseArgs({
9
+ args: argv,
10
+ options: { prefix: { type: "string" } },
11
+ strict: true,
12
+ });
13
+ const idx = await buildIndex(docsDir);
14
+ const prefix = values.prefix ?? "";
15
+ for (const d of idx.docs) {
16
+ if (!prefix || d.path.startsWith(prefix)) process.stdout.write(d.path + "\n");
17
+ }
18
+ }
19
+
20
+ async function cmdDriftBatch(argv: string[]): Promise<void> {
21
+ const { values } = parseArgs({
22
+ args: argv,
23
+ options: {
24
+ limit: { type: "string", default: "10" },
25
+ offset: { type: "string", default: "0" },
26
+ prefix: { type: "string" },
27
+ },
28
+ strict: true,
29
+ });
30
+ const limit = Math.max(1, Number(values.limit) | 0);
31
+ const offset = Math.max(0, Number(values.offset) | 0);
32
+ const idx = await buildIndex(docsDir);
33
+ const filtered = idx.docs
34
+ .filter((d) => !values.prefix || d.path.startsWith(values.prefix))
35
+ .sort((a, b) => a.path.localeCompare(b.path))
36
+ .slice(offset, offset + limit);
37
+ for (const d of filtered) {
38
+ process.stdout.write(`--- ${d.path}\n`);
39
+ process.stdout.write(`${d.summary ?? ""}\n\n`);
40
+ process.stdout.write(`${d.content}\n\n`);
41
+ }
42
+ }
43
+
44
+ const [, , sub, ...rest] = process.argv;
45
+
46
+ async function main(): Promise<void> {
47
+ switch (sub) {
48
+ case "list":
49
+ await cmdList(rest);
50
+ return;
51
+ case "drift-batch":
52
+ await cmdDriftBatch(rest);
53
+ return;
54
+ default:
55
+ process.stderr.write(`unknown subcommand: ${sub ?? "(none)"}\n`);
56
+ process.stderr.write(`usage: emdee <list|drift-batch> [--prefix P] [--limit N] [--offset K]\n`);
57
+ process.exit(1);
58
+ }
59
+ }
60
+
61
+ main().catch((err) => {
62
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
63
+ process.exit(1);
64
+ });