dotmd-cli 0.7.6 → 0.8.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 +120 -41
- package/bin/dotmd.mjs +99 -3
- package/package.json +14 -3
- package/src/ai.mjs +50 -0
- package/src/completions.mjs +12 -5
- package/src/config.mjs +10 -3
- package/src/deps.mjs +249 -0
- package/src/diff.mjs +2 -28
- package/src/export.mjs +344 -0
- package/src/fix-refs.mjs +102 -52
- package/src/index.mjs +15 -4
- package/src/init.mjs +88 -4
- package/src/lifecycle.mjs +11 -4
- package/src/new.mjs +15 -1
- package/src/notion.mjs +528 -0
- package/src/query.mjs +36 -4
- package/src/render.mjs +22 -4
- package/src/stats.mjs +161 -0
- package/src/summary.mjs +63 -0
- package/src/util.mjs +5 -2
- package/src/watch.mjs +12 -9
package/README.md
CHANGED
|
@@ -34,16 +34,19 @@ eval "$(dotmd completions zsh)" # add to ~/.zshrc
|
|
|
34
34
|
|
|
35
35
|
## What It Does
|
|
36
36
|
|
|
37
|
-
dotmd scans a directory of markdown files, parses their YAML frontmatter, and gives you tools to work with them:
|
|
38
|
-
|
|
39
37
|
- **Index** — group docs by status, show progress bars, next steps
|
|
40
38
|
- **Query** — filter by status, keyword, module, surface, owner, staleness
|
|
41
|
-
- **Validate** — check for missing fields, broken references,
|
|
39
|
+
- **Validate** — check for missing fields, broken references, broken body links, stale dates
|
|
40
|
+
- **Stats** — health dashboard with staleness, completeness, audit coverage
|
|
42
41
|
- **Graph** — visualize document relationships as text, Graphviz DOT, or JSON
|
|
42
|
+
- **Deps** — dependency tree or overview of what blocks what
|
|
43
43
|
- **Lifecycle** — transition statuses, auto-archive with `git mv` and reference updates
|
|
44
44
|
- **Doctor** — auto-fix broken refs, lint issues, date drift, and stale indexes in one pass
|
|
45
45
|
- **Scaffold** — create new docs from templates (plan, ADR, RFC, audit, design)
|
|
46
|
-
- **
|
|
46
|
+
- **AI summaries** — summarize docs via local MLX model or custom hook
|
|
47
|
+
- **Export** — generate concatenated markdown, static HTML site, or JSON bundle
|
|
48
|
+
- **Notion** — import from, export to, and bidirectionally sync with Notion databases
|
|
49
|
+
- **Multi-root** — manage docs across multiple directories with a single config
|
|
47
50
|
- **Context briefing** — compact summary designed for AI/LLM consumption
|
|
48
51
|
- **Dry-run** — preview any mutation with `--dry-run` before committing
|
|
49
52
|
|
|
@@ -72,17 +75,19 @@ Design doc content here...
|
|
|
72
75
|
- [ ] Add tests
|
|
73
76
|
```
|
|
74
77
|
|
|
75
|
-
The only required field is `status`. Everything else is optional but unlocks more features
|
|
78
|
+
The only required field is `status`. Everything else is optional but unlocks more features.
|
|
76
79
|
|
|
77
80
|
## Commands
|
|
78
81
|
|
|
79
82
|
```
|
|
80
83
|
dotmd list [--verbose] List docs grouped by status (default)
|
|
81
84
|
dotmd json Full index as JSON
|
|
82
|
-
dotmd check [
|
|
85
|
+
dotmd check [flags] Validate frontmatter and references
|
|
83
86
|
dotmd coverage [--json] Metadata coverage report
|
|
87
|
+
dotmd stats [--json] Doc health dashboard
|
|
84
88
|
dotmd graph [--dot|--json] Visualize document relationships
|
|
85
|
-
dotmd
|
|
89
|
+
dotmd deps [file] Dependency tree or overview
|
|
90
|
+
dotmd context [--summarize] Compact briefing (LLM-oriented)
|
|
86
91
|
dotmd focus [status] Detailed view for one status group
|
|
87
92
|
dotmd query [filters] Filtered search
|
|
88
93
|
dotmd index [--write] Generate/update docs.md index block
|
|
@@ -95,6 +100,9 @@ dotmd fix-refs Auto-fix broken reference paths
|
|
|
95
100
|
dotmd lint [--fix] Check and auto-fix frontmatter issues
|
|
96
101
|
dotmd rename <old> <new> Rename doc and update references
|
|
97
102
|
dotmd migrate <f> <old> <new> Batch update a frontmatter field
|
|
103
|
+
dotmd notion <sub> [db-id] Notion import/export/sync
|
|
104
|
+
dotmd export [file] Export docs as md, html, or json
|
|
105
|
+
dotmd summary <file> AI summary of a document
|
|
98
106
|
dotmd watch [command] Re-run a command on file changes
|
|
99
107
|
dotmd diff [file] Show changes since last updated date
|
|
100
108
|
dotmd new <name> Create a new document from template
|
|
@@ -107,6 +115,7 @@ dotmd completions <shell> Output shell completion script (bash, zsh)
|
|
|
107
115
|
```
|
|
108
116
|
--config <path> Explicit config file path
|
|
109
117
|
--dry-run, -n Preview changes without writing anything
|
|
118
|
+
--root <name> Filter to a specific docs root
|
|
110
119
|
--verbose Show resolved config details
|
|
111
120
|
--help, -h Show help (per-command with: dotmd <cmd> --help)
|
|
112
121
|
--version, -v Show version
|
|
@@ -119,9 +128,11 @@ dotmd query --status active,ready --module auth
|
|
|
119
128
|
dotmd query --keyword "token" --has-next-step
|
|
120
129
|
dotmd query --stale --sort updated --all
|
|
121
130
|
dotmd query --surface backend --checklist-open
|
|
131
|
+
dotmd query --status active --summarize # AI summaries
|
|
132
|
+
dotmd query --status active --summarize --summarize-limit 3
|
|
122
133
|
```
|
|
123
134
|
|
|
124
|
-
Flags: `--status`, `--keyword`, `--module`, `--surface`, `--domain`, `--owner`, `--updated-since`, `--stale`, `--has-next-step`, `--has-blockers`, `--checklist-open`, `--sort`, `--limit`, `--all`, `--git`, `--json`.
|
|
135
|
+
Flags: `--status`, `--keyword`, `--module`, `--surface`, `--domain`, `--owner`, `--updated-since`, `--stale`, `--has-next-step`, `--has-blockers`, `--checklist-open`, `--sort`, `--limit`, `--all`, `--git`, `--json`, `--summarize`, `--summarize-limit`, `--model`.
|
|
125
136
|
|
|
126
137
|
### Scaffold with Templates
|
|
127
138
|
|
|
@@ -133,6 +144,7 @@ dotmd new my-proposal --template rfc # RFC: Summary, Motivation
|
|
|
133
144
|
dotmd new my-audit --template audit # Audit: Scope, Findings, Recommendations
|
|
134
145
|
dotmd new my-design --template design # Design: Goals, Non-Goals, Design
|
|
135
146
|
dotmd new my-feature --status planned --title "Title" # custom status and title
|
|
147
|
+
dotmd new my-doc --root modules # create in a specific root
|
|
136
148
|
dotmd new --list-templates # show all available templates
|
|
137
149
|
```
|
|
138
150
|
|
|
@@ -158,21 +170,24 @@ dotmd check --fix # auto-fix broken refs + lint + regen index
|
|
|
158
170
|
|
|
159
171
|
Validates: required fields, status values, broken reference paths, broken body links (`[text](path.md)`), bidirectional reference symmetry, git date drift, taxonomy mismatches.
|
|
160
172
|
|
|
161
|
-
###
|
|
173
|
+
### Stats
|
|
162
174
|
|
|
163
|
-
|
|
175
|
+
```bash
|
|
176
|
+
dotmd stats # health dashboard
|
|
177
|
+
dotmd stats --json # machine-readable
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Shows: status counts, staleness, errors/warnings, freshness (today/week/month), completeness (owner/surface/module/next_step), checklist progress, audit coverage.
|
|
181
|
+
|
|
182
|
+
### Doctor
|
|
164
183
|
|
|
165
184
|
```bash
|
|
166
185
|
dotmd doctor # fix refs → lint → sync git dates → regen index
|
|
167
186
|
dotmd doctor --dry-run # preview all changes
|
|
168
187
|
```
|
|
169
188
|
|
|
170
|
-
Runs in sequence: `fix-refs` → `lint --fix` → `touch --git` → `index --write` → shows remaining issues.
|
|
171
|
-
|
|
172
189
|
### Graph
|
|
173
190
|
|
|
174
|
-
Visualize how documents reference each other:
|
|
175
|
-
|
|
176
191
|
```bash
|
|
177
192
|
dotmd graph # text adjacency list
|
|
178
193
|
dotmd graph --dot | dot -Tpng -o g.png # Graphviz PNG
|
|
@@ -181,6 +196,78 @@ dotmd graph --status active,ready # filter by status
|
|
|
181
196
|
dotmd graph --module auth # filter by module
|
|
182
197
|
```
|
|
183
198
|
|
|
199
|
+
### Deps
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
dotmd deps # overview: most blocking, most blocked
|
|
203
|
+
dotmd deps docs/plan-a.md # tree: depends-on + depended-on-by
|
|
204
|
+
dotmd deps docs/plan-a.md --depth 2 # limit tree depth
|
|
205
|
+
dotmd deps --json # machine-readable
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### AI Summaries
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
dotmd summary docs/plan-a.md # AI summary of a single doc
|
|
212
|
+
dotmd summary docs/plan-a.md --json # JSON output
|
|
213
|
+
dotmd query --status active --summarize # AI summaries in query results
|
|
214
|
+
dotmd context --summarize # AI-enhanced briefing
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Uses local MLX model via `uv`. Override with `--model <name>` or the `summarizeDoc` hook.
|
|
218
|
+
|
|
219
|
+
### Export
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
dotmd export # all docs as concatenated markdown
|
|
223
|
+
dotmd export --format html --output site # static HTML site
|
|
224
|
+
dotmd export --format json > bundle.json # JSON bundle with bodies
|
|
225
|
+
dotmd export docs/plan-a.md # single doc + dependencies
|
|
226
|
+
dotmd export --status active # filtered export
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Notion Integration
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
dotmd notion import <database-id> # pull Notion database → local .md files
|
|
233
|
+
dotmd notion export <database-id> # push local docs → Notion database
|
|
234
|
+
dotmd notion sync <database-id> # bidirectional sync (newer wins)
|
|
235
|
+
dotmd notion import <db-id> --force # overwrite existing files
|
|
236
|
+
dotmd notion sync <db-id> --dry-run # preview sync actions
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Requires `NOTION_TOKEN` env var or `notion.token` in config. Maps Notion properties (select, multi_select, date, status, people, etc.) to YAML frontmatter fields. Configure property mapping in config:
|
|
240
|
+
|
|
241
|
+
```js
|
|
242
|
+
export const notion = {
|
|
243
|
+
token: process.env.NOTION_TOKEN,
|
|
244
|
+
database: 'your-database-id',
|
|
245
|
+
propertyMap: {
|
|
246
|
+
'Status': 'status',
|
|
247
|
+
'Last Updated': 'updated',
|
|
248
|
+
'Tags': 'surfaces',
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Multi-Root
|
|
254
|
+
|
|
255
|
+
Manage docs across multiple directories:
|
|
256
|
+
|
|
257
|
+
```js
|
|
258
|
+
export const root = ['docs/plans', 'docs/modules', 'docs/app'];
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
All commands work across all roots. Filter with `--root`:
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
dotmd list --root plans # only docs from docs/plans
|
|
265
|
+
dotmd stats --root modules # stats for modules only
|
|
266
|
+
dotmd new my-doc --root modules # create in docs/modules
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Archive stays within the source file's root. Cross-root references validate correctly.
|
|
270
|
+
|
|
184
271
|
### Archive
|
|
185
272
|
|
|
186
273
|
```bash
|
|
@@ -188,44 +275,33 @@ dotmd archive docs/old-plan.md # move + update refs + regen index
|
|
|
188
275
|
dotmd archive docs/old-plan.md -n # preview
|
|
189
276
|
```
|
|
190
277
|
|
|
191
|
-
Archives a document: sets status to `archived`, moves to archive directory via `git mv`, auto-updates references in other docs, and regenerates the index.
|
|
192
|
-
|
|
193
278
|
### Touch
|
|
194
279
|
|
|
195
280
|
```bash
|
|
196
281
|
dotmd touch docs/my-doc.md # set updated to today
|
|
197
282
|
dotmd touch --git # bulk-sync all docs from git history
|
|
198
|
-
dotmd touch --git docs/my-doc.md # sync one file from git
|
|
199
283
|
```
|
|
200
284
|
|
|
201
285
|
### Fix References
|
|
202
286
|
|
|
203
287
|
```bash
|
|
204
|
-
dotmd fix-refs #
|
|
288
|
+
dotmd fix-refs # fix broken frontmatter refs + body links
|
|
205
289
|
dotmd fix-refs --dry-run # preview fixes
|
|
206
290
|
```
|
|
207
291
|
|
|
208
|
-
Scans all reference fields for broken paths, resolves by basename matching, and rewrites frontmatter.
|
|
209
|
-
|
|
210
292
|
### Lint
|
|
211
293
|
|
|
212
294
|
```bash
|
|
213
295
|
dotmd lint # report issues
|
|
214
296
|
dotmd lint --fix # fix all issues
|
|
215
|
-
dotmd lint --fix --dry-run # preview fixes without writing
|
|
216
297
|
```
|
|
217
298
|
|
|
218
|
-
Detected issues: missing `updated`, status casing, camelCase keys, trailing whitespace, missing EOF newline.
|
|
219
|
-
|
|
220
299
|
### Rename
|
|
221
300
|
|
|
222
301
|
```bash
|
|
223
302
|
dotmd rename old-name.md new-name # renames + updates refs
|
|
224
|
-
dotmd rename old-name.md new-name -n # preview without writing
|
|
225
303
|
```
|
|
226
304
|
|
|
227
|
-
Uses `git mv` and updates all frontmatter references. Body markdown links are warned about but not auto-fixed.
|
|
228
|
-
|
|
229
305
|
### Migrate
|
|
230
306
|
|
|
231
307
|
```bash
|
|
@@ -261,12 +337,16 @@ dotmd diff --stat # summary stats only
|
|
|
261
337
|
dotmd diff --summarize # AI summary via local MLX model
|
|
262
338
|
```
|
|
263
339
|
|
|
340
|
+
### Init Auto-Detect
|
|
341
|
+
|
|
342
|
+
When `dotmd init` runs in a directory with existing `.md` files, it scans them and pre-populates the config with discovered statuses, surfaces, modules, and reference fields.
|
|
343
|
+
|
|
264
344
|
## Configuration
|
|
265
345
|
|
|
266
346
|
Create `dotmd.config.mjs` at your project root (or run `dotmd init`):
|
|
267
347
|
|
|
268
348
|
```js
|
|
269
|
-
export const root = 'docs/plans'; //
|
|
349
|
+
export const root = 'docs/plans'; // string or array of paths
|
|
270
350
|
export const archiveDir = 'archived'; // subdirectory for archived docs
|
|
271
351
|
|
|
272
352
|
export const statuses = {
|
|
@@ -312,8 +392,7 @@ export function validate(doc, ctx) {
|
|
|
312
392
|
const warnings = [];
|
|
313
393
|
if (doc.status === 'active' && !doc.owner) {
|
|
314
394
|
warnings.push({
|
|
315
|
-
path: doc.path,
|
|
316
|
-
level: 'warning',
|
|
395
|
+
path: doc.path, level: 'warning',
|
|
317
396
|
message: 'Active docs should have an owner.',
|
|
318
397
|
});
|
|
319
398
|
}
|
|
@@ -332,7 +411,7 @@ export function renderContext(index, defaultRenderer) {
|
|
|
332
411
|
}
|
|
333
412
|
```
|
|
334
413
|
|
|
335
|
-
Available: `renderContext`, `renderCompactList`, `renderCheck`, `renderGraph`, `formatSnapshot`.
|
|
414
|
+
Available: `renderContext`, `renderCompactList`, `renderCheck`, `renderGraph`, `renderStats`, `formatSnapshot`.
|
|
336
415
|
|
|
337
416
|
### Lifecycle Hooks
|
|
338
417
|
|
|
@@ -340,33 +419,33 @@ Available: `renderContext`, `renderCompactList`, `renderCheck`, `renderGraph`, `
|
|
|
340
419
|
export function onArchive(doc, { oldPath, newPath }) {
|
|
341
420
|
console.log(`Archived: ${oldPath} → ${newPath}`);
|
|
342
421
|
}
|
|
343
|
-
|
|
344
|
-
export function onStatusChange(doc, { oldStatus, newStatus }) {
|
|
345
|
-
// notify, log, trigger CI, etc.
|
|
346
|
-
}
|
|
347
422
|
```
|
|
348
423
|
|
|
349
424
|
Available: `onArchive`, `onStatusChange`, `onTouch`, `onNew`, `onRename`, `onLint`.
|
|
350
425
|
|
|
351
|
-
###
|
|
352
|
-
|
|
353
|
-
Override the diff summarizer (replaces the default MLX model call):
|
|
426
|
+
### AI Hooks
|
|
354
427
|
|
|
355
428
|
```js
|
|
429
|
+
// Override doc summarization (replaces local MLX model)
|
|
430
|
+
export function summarizeDoc(body, meta) {
|
|
431
|
+
return 'Custom summary for ' + meta.title;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Override diff summarization
|
|
356
435
|
export function summarizeDiff(diffOutput, filePath) {
|
|
357
|
-
// call your preferred LLM, return a string summary
|
|
358
436
|
return `Changes in ${filePath}: ...`;
|
|
359
437
|
}
|
|
360
438
|
```
|
|
361
439
|
|
|
362
440
|
## Features
|
|
363
441
|
|
|
364
|
-
- **Zero dependencies** — pure Node.js builtins (`fs`, `path`, `child_process`)
|
|
365
|
-
- **No build step** — ships as plain ESM, runs directly
|
|
366
442
|
- **Git-aware** — detects frontmatter date drift vs git history, uses `git mv` for archives
|
|
367
443
|
- **Dry-run everything** — preview any mutation with `--dry-run` / `-n`
|
|
368
|
-
- **
|
|
444
|
+
- **Multi-root** — manage docs across multiple directories with `--root` filtering
|
|
445
|
+
- **Configurable** — statuses, taxonomy, lifecycle, validation rules, display, templates
|
|
369
446
|
- **Hook system** — extend with JS functions, no plugin framework to learn
|
|
447
|
+
- **AI-powered** — local MLX summaries for docs, queries, diffs, and context briefings
|
|
448
|
+
- **Notion sync** — import, export, and bidirectional sync with Notion databases
|
|
370
449
|
- **LLM-friendly** — `dotmd context` generates compact briefings for AI assistants
|
|
371
450
|
- **Shell completion** — bash and zsh via `dotmd completions`
|
|
372
451
|
|
package/bin/dotmd.mjs
CHANGED
|
@@ -20,6 +20,11 @@ import { runMigrate } from '../src/migrate.mjs';
|
|
|
20
20
|
import { runFixRefs, fixBrokenRefs } from '../src/fix-refs.mjs';
|
|
21
21
|
import { buildGraph, renderGraphText, renderGraphDot, renderGraphJson } from '../src/graph.mjs';
|
|
22
22
|
import { runDoctor } from '../src/doctor.mjs';
|
|
23
|
+
import { buildStats, renderStats, renderStatsJson } from '../src/stats.mjs';
|
|
24
|
+
import { runSummary } from '../src/summary.mjs';
|
|
25
|
+
import { runDeps } from '../src/deps.mjs';
|
|
26
|
+
import { runExport } from '../src/export.mjs';
|
|
27
|
+
import { runNotion } from '../src/notion.mjs';
|
|
23
28
|
import { die, warn } from '../src/util.mjs';
|
|
24
29
|
|
|
25
30
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -34,7 +39,9 @@ Commands:
|
|
|
34
39
|
json Full index as JSON
|
|
35
40
|
check [flags] Validate frontmatter and references
|
|
36
41
|
coverage [--json] Metadata coverage report
|
|
42
|
+
stats [--json] Doc health dashboard
|
|
37
43
|
graph [--dot|--json] Visualize document relationships
|
|
44
|
+
deps [file] Dependency tree or overview
|
|
38
45
|
context Compact briefing (LLM-oriented)
|
|
39
46
|
focus [status] Detailed view for one status group
|
|
40
47
|
query [filters] Filtered search
|
|
@@ -48,8 +55,11 @@ Commands:
|
|
|
48
55
|
rename <old> <new> Rename doc and update references
|
|
49
56
|
migrate <f> <old> <new> Batch update a frontmatter field
|
|
50
57
|
watch [command] Re-run a command on file changes
|
|
58
|
+
notion <sub> [db-id] Notion import/export/sync
|
|
59
|
+
export [file] Export docs as md, html, or json
|
|
60
|
+
summary <file> AI summary of a document
|
|
51
61
|
diff [file] Show changes since last updated date
|
|
52
|
-
new <name> Create a new document
|
|
62
|
+
new <name> Create a new document from template
|
|
53
63
|
init Create starter config + docs directory
|
|
54
64
|
completions <shell> Output shell completion script (bash, zsh)
|
|
55
65
|
|
|
@@ -101,6 +111,14 @@ references in other docs, and regenerates the index.
|
|
|
101
111
|
|
|
102
112
|
Use --dry-run (-n) to preview changes without writing anything.`,
|
|
103
113
|
|
|
114
|
+
stats: `dotmd stats — doc health dashboard
|
|
115
|
+
|
|
116
|
+
Shows aggregated metrics: status counts, staleness, errors/warnings,
|
|
117
|
+
freshness, completeness, checklist progress, and audit coverage.
|
|
118
|
+
|
|
119
|
+
Options:
|
|
120
|
+
--json Machine-readable JSON output`,
|
|
121
|
+
|
|
104
122
|
graph: `dotmd graph — visualize document relationships
|
|
105
123
|
|
|
106
124
|
Output formats:
|
|
@@ -113,6 +131,18 @@ Filters:
|
|
|
113
131
|
--module <name> Show only docs with this module
|
|
114
132
|
--surface <name> Show only docs with this surface`,
|
|
115
133
|
|
|
134
|
+
deps: `dotmd deps [file] — dependency tree or overview
|
|
135
|
+
|
|
136
|
+
Without a file, shows a flat overview: most blocking docs, most blocked
|
|
137
|
+
docs, docs with blockers, and orphans.
|
|
138
|
+
|
|
139
|
+
With a file, shows a tree: what the doc depends on (recursive) and what
|
|
140
|
+
depends on it.
|
|
141
|
+
|
|
142
|
+
Options:
|
|
143
|
+
--depth <n> Max tree depth (default: 5)
|
|
144
|
+
--json Machine-readable JSON output`,
|
|
145
|
+
|
|
116
146
|
doctor: `dotmd doctor — auto-fix everything in one pass
|
|
117
147
|
|
|
118
148
|
Runs in sequence: fix broken references, lint --fix, sync dates from
|
|
@@ -167,6 +197,40 @@ Examples:
|
|
|
167
197
|
dotmd watch check # re-run check on changes
|
|
168
198
|
dotmd watch context # live briefing`,
|
|
169
199
|
|
|
200
|
+
notion: `dotmd notion — Notion database integration
|
|
201
|
+
|
|
202
|
+
Subcommands:
|
|
203
|
+
import <database-id> Pull Notion database → local .md files
|
|
204
|
+
export <database-id> Push local docs → Notion database rows
|
|
205
|
+
sync <database-id> Bidirectional sync (merge by slug)
|
|
206
|
+
|
|
207
|
+
Options:
|
|
208
|
+
--force Overwrite existing files on import
|
|
209
|
+
--dry-run, -n Preview without changes
|
|
210
|
+
|
|
211
|
+
Requires NOTION_TOKEN env var or notion.token in config.`,
|
|
212
|
+
|
|
213
|
+
export: `dotmd export — export docs as markdown, HTML, or JSON
|
|
214
|
+
|
|
215
|
+
Without a file, exports all docs (with optional filters).
|
|
216
|
+
With a file, exports that doc plus all its dependencies.
|
|
217
|
+
|
|
218
|
+
Options:
|
|
219
|
+
--format <md|html|json> Output format (default: md)
|
|
220
|
+
--output <path> Write to file/directory (default: stdout for md/json)
|
|
221
|
+
--status <s1,s2> Filter by status
|
|
222
|
+
--module <name> Filter by module
|
|
223
|
+
--root <name> Filter by root`,
|
|
224
|
+
|
|
225
|
+
summary: `dotmd summary <file> — AI summary of a document
|
|
226
|
+
|
|
227
|
+
Generates an AI-powered summary using a local MLX model via uv.
|
|
228
|
+
|
|
229
|
+
Options:
|
|
230
|
+
--model <name> MLX model (default: mlx-community/Llama-3.2-3B-Instruct-4bit)
|
|
231
|
+
--max-tokens <n> Max tokens for generation (default: 200)
|
|
232
|
+
--json Output as JSON`,
|
|
233
|
+
|
|
170
234
|
diff: `dotmd diff [file] — show changes since last updated date
|
|
171
235
|
|
|
172
236
|
Shows git diffs for docs that changed after their frontmatter updated date.
|
|
@@ -270,7 +334,8 @@ async function main() {
|
|
|
270
334
|
|
|
271
335
|
if (verbose) {
|
|
272
336
|
process.stderr.write(`Config: ${config.configPath ?? 'none'}\n`);
|
|
273
|
-
|
|
337
|
+
const roots = config.docsRoots || [config.docsRoot];
|
|
338
|
+
process.stderr.write(`Docs root${roots.length > 1 ? 's' : ''}: ${roots.join(', ')}\n`);
|
|
274
339
|
process.stderr.write(`Repo root: ${config.repoRoot}\n`);
|
|
275
340
|
}
|
|
276
341
|
|
|
@@ -284,6 +349,10 @@ async function main() {
|
|
|
284
349
|
// Watch and diff (handle their own index building)
|
|
285
350
|
if (command === 'watch') { runWatch(restArgs, config); return; }
|
|
286
351
|
if (command === 'diff') { runDiff(restArgs, config); return; }
|
|
352
|
+
if (command === 'summary') { runSummary(restArgs, config); return; }
|
|
353
|
+
if (command === 'deps') { runDeps(restArgs, config); return; }
|
|
354
|
+
if (command === 'export') { runExport(restArgs, config); return; }
|
|
355
|
+
if (command === 'notion') { await runNotion(restArgs, config, { dryRun }); return; }
|
|
287
356
|
|
|
288
357
|
// Lifecycle commands
|
|
289
358
|
if (command === 'status') { runStatus(restArgs, config, { dryRun }); return; }
|
|
@@ -298,6 +367,17 @@ async function main() {
|
|
|
298
367
|
|
|
299
368
|
const index = buildIndex(config);
|
|
300
369
|
|
|
370
|
+
// Apply --root filter
|
|
371
|
+
const rootFilter = (() => { const i = args.indexOf('--root'); return i !== -1 && args[i + 1] ? args[i + 1] : null; })();
|
|
372
|
+
if (rootFilter) {
|
|
373
|
+
index.docs = index.docs.filter(d => d.root === rootFilter || d.root.endsWith('/' + rootFilter) || d.root.split('/').pop() === rootFilter);
|
|
374
|
+
index.errors = index.errors.filter(e => index.docs.some(d => d.path === e.path));
|
|
375
|
+
index.warnings = index.warnings.filter(w => index.docs.some(d => d.path === w.path));
|
|
376
|
+
for (const status of config.statusOrder) {
|
|
377
|
+
index.countsByStatus[status] = index.docs.filter(d => d.status === status).length;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
301
381
|
if (verbose) {
|
|
302
382
|
process.stderr.write(`Docs found: ${index.docs.length}\n`);
|
|
303
383
|
}
|
|
@@ -353,6 +433,16 @@ async function main() {
|
|
|
353
433
|
return;
|
|
354
434
|
}
|
|
355
435
|
|
|
436
|
+
if (command === 'stats') {
|
|
437
|
+
const stats = buildStats(index, config);
|
|
438
|
+
if (args.includes('--json')) {
|
|
439
|
+
process.stdout.write(renderStatsJson(stats));
|
|
440
|
+
} else {
|
|
441
|
+
process.stdout.write(renderStats(stats, config));
|
|
442
|
+
}
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
356
446
|
if (command === 'index') {
|
|
357
447
|
if (!config.indexPath) {
|
|
358
448
|
die('Index generation is not configured. Add an `index` section to your dotmd.config.mjs.');
|
|
@@ -372,7 +462,13 @@ async function main() {
|
|
|
372
462
|
|
|
373
463
|
if (command === 'focus') { runFocus(index, restArgs, config); return; }
|
|
374
464
|
if (command === 'query') { runQuery(index, restArgs, config); return; }
|
|
375
|
-
if (command === 'context') {
|
|
465
|
+
if (command === 'context') {
|
|
466
|
+
const summarize = args.includes('--summarize');
|
|
467
|
+
const modelIdx = args.indexOf('--model');
|
|
468
|
+
const model = modelIdx !== -1 && args[modelIdx + 1] ? args[modelIdx + 1] : undefined;
|
|
469
|
+
process.stdout.write(renderContext(index, config, { summarize, model }));
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
376
472
|
|
|
377
473
|
if (command === 'graph') {
|
|
378
474
|
const statusFilter = (() => { const i = args.indexOf('--status'); return i !== -1 && args[i + 1] ? args[i + 1] : null; })();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dotmd-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "CLI for managing markdown documents with YAML frontmatter — index, query, validate,
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "CLI for managing markdown documents with YAML frontmatter — index, query, validate, graph, export, Notion sync, AI summaries.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"bin": {
|
|
@@ -24,16 +24,27 @@
|
|
|
24
24
|
"adr",
|
|
25
25
|
"rfc",
|
|
26
26
|
"document-management",
|
|
27
|
-
"yaml"
|
|
27
|
+
"yaml",
|
|
28
|
+
"notion",
|
|
29
|
+
"graph",
|
|
30
|
+
"export",
|
|
31
|
+
"ai-summary",
|
|
32
|
+
"validation",
|
|
33
|
+
"lifecycle"
|
|
28
34
|
],
|
|
29
35
|
"repository": {
|
|
30
36
|
"type": "git",
|
|
31
37
|
"url": "git+https://github.com/reowens/dotmd.git"
|
|
32
38
|
},
|
|
39
|
+
"homepage": "https://github.com/reowens/dotmd#readme",
|
|
33
40
|
"scripts": {
|
|
34
41
|
"test": "node --test test/*.test.mjs"
|
|
35
42
|
},
|
|
36
43
|
"engines": {
|
|
37
44
|
"node": ">=18"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@notionhq/client": "^5.13.0",
|
|
48
|
+
"notion-to-md": "^3.1.9"
|
|
38
49
|
}
|
|
39
50
|
}
|
package/src/ai.mjs
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { warn } from './util.mjs';
|
|
3
|
+
|
|
4
|
+
let uvChecked = null;
|
|
5
|
+
|
|
6
|
+
export function checkUvAvailable() {
|
|
7
|
+
if (uvChecked !== null) return uvChecked;
|
|
8
|
+
const result = spawnSync('uv', ['--version'], { encoding: 'utf8' });
|
|
9
|
+
uvChecked = !result.error && result.status === 0;
|
|
10
|
+
if (!uvChecked) warn('uv is not installed. Install it to enable AI summaries: https://docs.astral.sh/uv/');
|
|
11
|
+
return uvChecked;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function runMLX(prompt, opts = {}) {
|
|
15
|
+
const { model = 'mlx-community/Llama-3.2-3B-Instruct-4bit', maxTokens = 200, timeout = 120000 } = opts;
|
|
16
|
+
if (!checkUvAvailable()) return null;
|
|
17
|
+
|
|
18
|
+
const result = spawnSync('uv', [
|
|
19
|
+
'run', '--with', 'mlx-lm',
|
|
20
|
+
'python3', '-m', 'mlx_lm', 'generate',
|
|
21
|
+
'--model', model,
|
|
22
|
+
'--prompt', prompt,
|
|
23
|
+
'--max-tokens', String(maxTokens),
|
|
24
|
+
'--verbose', 'false',
|
|
25
|
+
], { encoding: 'utf8', timeout });
|
|
26
|
+
|
|
27
|
+
if (result.status !== 0) return null;
|
|
28
|
+
|
|
29
|
+
const output = result.stdout.trim();
|
|
30
|
+
const lines = output.split('\n')
|
|
31
|
+
.filter(l => !l.includes('Fetching') && !l.includes('Warning:') && !l.includes('=========='));
|
|
32
|
+
return lines.join(' ').trim() || null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function summarizeDocBody(bodyText, meta, opts = {}) {
|
|
36
|
+
if (!bodyText?.trim()) return null;
|
|
37
|
+
const prompt = `Summarize this markdown document in 2-3 sentences. Focus on what the document is about, its current state, and any key next actions or open questions.
|
|
38
|
+
|
|
39
|
+
Title: ${meta.title}
|
|
40
|
+
Status: ${meta.status}
|
|
41
|
+
File: ${meta.path}
|
|
42
|
+
|
|
43
|
+
${bodyText.slice(0, 6000)}`;
|
|
44
|
+
return runMLX(prompt, { maxTokens: 200, ...opts });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function summarizeDiffText(diffText, filePath, model) {
|
|
48
|
+
const prompt = `Summarize this git diff in 1-2 sentences. Focus on what changed semantically, not line counts.\n\nFile: ${filePath}\n\n${diffText.slice(0, 4000)}`;
|
|
49
|
+
return runMLX(prompt, { model, maxTokens: 150 });
|
|
50
|
+
}
|
package/src/completions.mjs
CHANGED
|
@@ -1,28 +1,35 @@
|
|
|
1
1
|
import { die } from './util.mjs';
|
|
2
2
|
|
|
3
3
|
const COMMANDS = [
|
|
4
|
-
'list', 'json', 'check', 'coverage', 'graph', 'context', 'focus', 'query',
|
|
4
|
+
'list', 'json', 'check', 'coverage', 'stats', 'graph', 'deps', 'context', 'focus', 'query',
|
|
5
5
|
'index', 'status', 'archive', 'touch', 'doctor', 'lint', 'rename', 'migrate',
|
|
6
|
-
'fix-refs', 'watch', 'diff', 'init', 'new', 'completions',
|
|
6
|
+
'fix-refs', 'notion', 'export', 'summary', 'watch', 'diff', 'init', 'new', 'completions',
|
|
7
7
|
];
|
|
8
8
|
|
|
9
|
-
const GLOBAL_FLAGS = ['--config', '--dry-run', '--verbose', '--help', '--version'];
|
|
9
|
+
const GLOBAL_FLAGS = ['--config', '--dry-run', '--verbose', '--root', '--help', '--version'];
|
|
10
10
|
|
|
11
11
|
const COMMAND_FLAGS = {
|
|
12
12
|
query: ['--status', '--keyword', '--module', '--surface', '--domain', '--owner',
|
|
13
13
|
'--updated-since', '--stale', '--has-next-step', '--has-blockers',
|
|
14
|
-
'--checklist-open', '--sort', '--limit', '--all', '--git', '--json'
|
|
14
|
+
'--checklist-open', '--sort', '--limit', '--all', '--git', '--json',
|
|
15
|
+
'--summarize', '--summarize-limit', '--model'],
|
|
15
16
|
index: ['--write'],
|
|
16
17
|
list: ['--verbose'],
|
|
17
18
|
coverage: ['--json'],
|
|
18
|
-
new: ['--status', '--title', '--template', '--list-templates'],
|
|
19
|
+
new: ['--status', '--title', '--template', '--list-templates', '--root'],
|
|
19
20
|
diff: ['--stat', '--since', '--summarize', '--model'],
|
|
20
21
|
check: ['--errors-only', '--fix'],
|
|
22
|
+
stats: ['--json'],
|
|
21
23
|
graph: ['--dot', '--json', '--status', '--module', '--surface'],
|
|
24
|
+
deps: ['--json', '--depth'],
|
|
25
|
+
notion: ['import', 'export', 'sync', '--force', '--dry-run'],
|
|
26
|
+
export: ['--format', '--output', '--status', '--module', '--root'],
|
|
22
27
|
lint: ['--fix'],
|
|
23
28
|
rename: [],
|
|
24
29
|
migrate: [],
|
|
25
30
|
'fix-refs': [],
|
|
31
|
+
summary: ['--model', '--max-tokens', '--json'],
|
|
32
|
+
context: ['--summarize', '--model'],
|
|
26
33
|
touch: ['--git'],
|
|
27
34
|
};
|
|
28
35
|
|
package/src/config.mjs
CHANGED
|
@@ -57,6 +57,8 @@ const DEFAULTS = {
|
|
|
57
57
|
|
|
58
58
|
templates: {},
|
|
59
59
|
|
|
60
|
+
notion: null,
|
|
61
|
+
|
|
60
62
|
presets: {
|
|
61
63
|
stale: ['--status', 'active,ready,planned,blocked,research', '--stale', '--sort', 'updated', '--all'],
|
|
62
64
|
actionable: ['--status', 'active,ready', '--has-next-step', '--sort', 'updated', '--all'],
|
|
@@ -182,11 +184,15 @@ export async function resolveConfig(cwd, explicitConfigPath) {
|
|
|
182
184
|
|
|
183
185
|
const config = deepMerge(DEFAULTS, userConfig);
|
|
184
186
|
|
|
185
|
-
const
|
|
187
|
+
const rootPaths = Array.isArray(config.root) ? config.root : [config.root];
|
|
188
|
+
const docsRoots = rootPaths.map(r => path.resolve(configDir, r));
|
|
189
|
+
const docsRoot = docsRoots[0]; // primary root for backwards compat
|
|
186
190
|
|
|
187
191
|
const earlyWarnings = [];
|
|
188
|
-
|
|
189
|
-
|
|
192
|
+
for (const dr of docsRoots) {
|
|
193
|
+
if (!existsSync(dr)) {
|
|
194
|
+
earlyWarnings.push('Config: docs root does not exist: ' + dr);
|
|
195
|
+
}
|
|
190
196
|
}
|
|
191
197
|
|
|
192
198
|
// Find repo root by walking up looking for .git
|
|
@@ -235,6 +241,7 @@ export async function resolveConfig(cwd, explicitConfigPath) {
|
|
|
235
241
|
raw: config,
|
|
236
242
|
|
|
237
243
|
docsRoot,
|
|
244
|
+
docsRoots,
|
|
238
245
|
repoRoot,
|
|
239
246
|
configDir,
|
|
240
247
|
configPath: configPath ?? null,
|