dotmd-cli 0.12.0 → 0.13.1

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 CHANGED
@@ -113,7 +113,8 @@ dotmd coverage [--json] Metadata coverage report
113
113
  dotmd stats [--json] Doc health dashboard
114
114
  dotmd graph [--dot|--json] Visualize document relationships
115
115
  dotmd deps [file] Dependency tree or overview
116
- dotmd context [--summarize] Compact briefing (LLM-oriented)
116
+ dotmd briefing Compact summary for session start
117
+ dotmd context [--summarize] Full briefing (LLM-oriented)
117
118
  dotmd focus [status] Detailed view for one status group
118
119
  dotmd query [filters] Filtered search
119
120
  dotmd plans List all plans
package/bin/dotmd.mjs CHANGED
@@ -4,29 +4,6 @@ import { readFileSync } from 'node:fs';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import path from 'node:path';
6
6
  import { resolveConfig } from '../src/config.mjs';
7
- import { buildIndex } from '../src/index.mjs';
8
- import { renderCompactList, renderVerboseList, renderContext, renderCheck, renderCoverage, buildCoverage } from '../src/render.mjs';
9
- import { renderIndexFile, writeIndex } from '../src/index-file.mjs';
10
- import { runFocus, runQuery } from '../src/query.mjs';
11
- import { runStatus, runArchive, runTouch, runBulkArchive, runPickup, runFinish } from '../src/lifecycle.mjs';
12
- import { runInit } from '../src/init.mjs';
13
- import { runNew } from '../src/new.mjs';
14
- import { runCompletions } from '../src/completions.mjs';
15
- import { runWatch } from '../src/watch.mjs';
16
- import { runDiff } from '../src/diff.mjs';
17
- import { runLint } from '../src/lint.mjs';
18
- import { runRename } from '../src/rename.mjs';
19
- import { runMigrate } from '../src/migrate.mjs';
20
- import { runFixRefs, fixBrokenRefs } from '../src/fix-refs.mjs';
21
- import { buildGraph, renderGraphText, renderGraphDot, renderGraphJson } from '../src/graph.mjs';
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, runUnblocks } from '../src/deps.mjs';
26
- import { runHealth } from '../src/health.mjs';
27
- import { runGlossary } from '../src/glossary.mjs';
28
- import { runExport } from '../src/export.mjs';
29
- import { runNotion } from '../src/notion.mjs';
30
7
  import { die, warn, levenshtein } from '../src/util.mjs';
31
8
 
32
9
  const __filename = fileURLToPath(import.meta.url);
@@ -39,7 +16,8 @@ const HELP = {
39
16
  View & Query:
40
17
  list [--verbose] [--json] List docs grouped by status (default command)
41
18
  json Full index as JSON
42
- context [--summarize] [--json] Compact briefing (LLM-oriented)
19
+ briefing [--json] Compact summary for session start (5-10 lines)
20
+ context [--summarize] [--json] Full briefing (LLM-oriented)
43
21
  focus [status] [--json] Detailed view for one status group
44
22
  query [filters] [--json] Filtered search (--status, --keyword, --stale, etc.)
45
23
  coverage [--json] Metadata coverage report
@@ -191,7 +169,15 @@ Shows detailed info for all docs matching the given status (default: active).
191
169
  Options:
192
170
  --json Output as JSON`,
193
171
 
194
- context: `dotmd context — compact briefing (LLM-oriented)
172
+ briefing: `dotmd briefing — compact summary for session start
173
+
174
+ Shows plan statuses with next steps, doc/research counts, and health
175
+ in 5-10 lines. Designed for LLM context injection.
176
+
177
+ Options:
178
+ --json Output as JSON`,
179
+
180
+ context: `dotmd context — full briefing (LLM-oriented)
195
181
 
196
182
  Generates a compact status briefing designed for AI/LLM consumption.
197
183
 
@@ -418,6 +404,7 @@ async function main() {
418
404
  }
419
405
 
420
406
  if (command === 'completions') {
407
+ const { runCompletions } = await import('../src/completions.mjs');
421
408
  runCompletions(args.slice(1));
422
409
  return;
423
410
  }
@@ -438,12 +425,13 @@ async function main() {
438
425
 
439
426
  // Init — now has access to config for Claude command generation
440
427
  if (command === 'init') {
428
+ const { runInit } = await import('../src/init.mjs');
441
429
  runInit(process.cwd(), config.configFound ? config : null);
442
430
  return;
443
431
  }
444
432
 
445
433
  // Watch is a pure proxy — pass raw args so the child process gets all flags
446
- if (command === 'watch') { runWatch(args.slice(1), config); return; }
434
+ if (command === 'watch') { const { runWatch } = await import('../src/watch.mjs'); runWatch(args.slice(1), config); return; }
447
435
 
448
436
  // Strip global flags from restArgs so commands don't have to filter them
449
437
  const restArgs = [];
@@ -476,35 +464,41 @@ async function main() {
476
464
 
477
465
  // Preset aliases
478
466
  if (config.presets[command]) {
467
+ const { buildIndex } = await import('../src/index.mjs');
468
+ const { runQuery } = await import('../src/query.mjs');
479
469
  const index = buildIndex(config);
480
470
  runQuery(index, [...config.presets[command], ...restArgs], config);
481
471
  return;
482
472
  }
483
473
 
484
474
  // Commands that handle their own index building
485
- if (command === 'diff') { runDiff(restArgs, config); return; }
486
- if (command === 'summary') { runSummary(restArgs, config); return; }
487
- if (command === 'deps') { runDeps(restArgs, config); return; }
488
- if (command === 'unblocks') { runUnblocks(restArgs, config); return; }
489
- if (command === 'health') { runHealth(restArgs, config); return; }
490
- if (command === 'glossary') { runGlossary(restArgs, config); return; }
491
- if (command === 'export') { runExport(restArgs, config, { dryRun, root: rootArg, type: typeArg }); return; }
492
- if (command === 'notion') { await runNotion(restArgs, config, { dryRun }); return; }
475
+ if (command === 'diff') { const { runDiff } = await import('../src/diff.mjs'); runDiff(restArgs, config); return; }
476
+ if (command === 'summary') { const { runSummary } = await import('../src/summary.mjs'); runSummary(restArgs, config); return; }
477
+ if (command === 'deps') { const { runDeps } = await import('../src/deps.mjs'); runDeps(restArgs, config); return; }
478
+ if (command === 'unblocks') { const { runUnblocks } = await import('../src/deps.mjs'); runUnblocks(restArgs, config); return; }
479
+ if (command === 'health') { const { runHealth } = await import('../src/health.mjs'); runHealth(restArgs, config); return; }
480
+ if (command === 'glossary') { const { runGlossary } = await import('../src/glossary.mjs'); runGlossary(restArgs, config); return; }
481
+ if (command === 'export') { const { runExport } = await import('../src/export.mjs'); runExport(restArgs, config, { dryRun, root: rootArg, type: typeArg }); return; }
482
+ if (command === 'notion') { const { runNotion } = await import('../src/notion.mjs'); await runNotion(restArgs, config, { dryRun }); return; }
493
483
 
494
484
  // Lifecycle commands
495
- if (command === 'pickup') { await runPickup(restArgs, config, { dryRun }); return; }
496
- if (command === 'finish') { await runFinish(restArgs, config, { dryRun }); return; }
497
- if (command === 'status') { await runStatus(restArgs, config, { dryRun }); return; }
498
- if (command === 'archive') { runArchive(restArgs, config, { dryRun }); return; }
499
- if (command === 'bulk' && restArgs[0] === 'archive') { runBulkArchive(restArgs.slice(1), config, { dryRun }); return; }
500
- if (command === 'touch') { runTouch(restArgs, config, { dryRun }); return; }
501
- if (command === 'new') { await runNew(restArgs, config, { dryRun, root: rootArg }); return; }
502
- if (command === 'lint') { runLint(restArgs, config, { dryRun }); return; }
503
- if (command === 'rename') { await runRename(restArgs, config, { dryRun }); return; }
504
- if (command === 'migrate') { runMigrate(restArgs, config, { dryRun }); return; }
505
- if (command === 'fix-refs') { runFixRefs(restArgs, config, { dryRun }); return; }
506
- if (command === 'doctor') { runDoctor(restArgs, config, { dryRun }); return; }
507
-
485
+ if (command === 'pickup') { const { runPickup } = await import('../src/lifecycle.mjs'); await runPickup(restArgs, config, { dryRun }); return; }
486
+ if (command === 'finish') { const { runFinish } = await import('../src/lifecycle.mjs'); await runFinish(restArgs, config, { dryRun }); return; }
487
+ if (command === 'status') { const { runStatus } = await import('../src/lifecycle.mjs'); await runStatus(restArgs, config, { dryRun }); return; }
488
+ if (command === 'archive') { const { runArchive } = await import('../src/lifecycle.mjs'); runArchive(restArgs, config, { dryRun }); return; }
489
+ if (command === 'bulk' && restArgs[0] === 'archive') { const { runBulkArchive } = await import('../src/lifecycle.mjs'); runBulkArchive(restArgs.slice(1), config, { dryRun }); return; }
490
+ if (command === 'touch') { const { runTouch } = await import('../src/lifecycle.mjs'); runTouch(restArgs, config, { dryRun }); return; }
491
+ if (command === 'new') { const { runNew } = await import('../src/new.mjs'); await runNew(restArgs, config, { dryRun, root: rootArg }); return; }
492
+ if (command === 'lint') { const { runLint } = await import('../src/lint.mjs'); runLint(restArgs, config, { dryRun }); return; }
493
+ if (command === 'rename') { const { runRename } = await import('../src/rename.mjs'); await runRename(restArgs, config, { dryRun }); return; }
494
+ if (command === 'migrate') { const { runMigrate } = await import('../src/migrate.mjs'); runMigrate(restArgs, config, { dryRun }); return; }
495
+ if (command === 'fix-refs') { const { runFixRefs } = await import('../src/fix-refs.mjs'); runFixRefs(restArgs, config, { dryRun }); return; }
496
+ if (command === 'doctor') { const { runDoctor } = await import('../src/doctor.mjs'); runDoctor(restArgs, config, { dryRun }); return; }
497
+
498
+ // All remaining commands need the index + render modules
499
+ const { buildIndex } = await import('../src/index.mjs');
500
+ const { renderCompactList, renderVerboseList, renderContext, renderBriefing, renderCheck, renderCoverage, buildCoverage } = await import('../src/render.mjs');
501
+ const { runFocus, runQuery } = await import('../src/query.mjs');
508
502
  const index = buildIndex(config);
509
503
 
510
504
  // Apply --root and --type filters
@@ -561,6 +555,8 @@ async function main() {
561
555
 
562
556
  if (fix) {
563
557
  // Auto-fix: broken refs, then lint, then rebuild index
558
+ const { fixBrokenRefs } = await import('../src/fix-refs.mjs');
559
+ const { runLint } = await import('../src/lint.mjs');
564
560
  fixBrokenRefs(config, { dryRun, quiet: false });
565
561
  runLint(['--fix'], config, { dryRun });
566
562
  if (config.indexPath) {
@@ -620,6 +616,7 @@ async function main() {
620
616
  }
621
617
 
622
618
  if (command === 'stats') {
619
+ const { buildStats, renderStats, renderStatsJson } = await import('../src/stats.mjs');
623
620
  const stats = buildStats(index, config);
624
621
  if (args.includes('--json')) {
625
622
  process.stdout.write(renderStatsJson(stats));
@@ -634,6 +631,7 @@ async function main() {
634
631
  die('Index generation is not configured. Add an `index` section to your dotmd.config.mjs.');
635
632
  }
636
633
  const write = args.includes('--write');
634
+ const { renderIndexFile, writeIndex } = await import('../src/index-file.mjs');
637
635
  const rendered = renderIndexFile(index, config);
638
636
  if (write && !dryRun) {
639
637
  writeIndex(rendered, config);
@@ -648,6 +646,24 @@ async function main() {
648
646
 
649
647
  if (command === 'focus') { runFocus(index, restArgs, config); return; }
650
648
  if (command === 'query') { runQuery(index, restArgs, config); return; }
649
+ if (command === 'briefing') {
650
+ if (args.includes('--json')) {
651
+ const plans = index.docs.filter(d => d.type === 'plan');
652
+ const docs = index.docs.filter(d => d.type === 'doc');
653
+ const research = index.docs.filter(d => d.type === 'research');
654
+ const stale = index.docs.filter(d => d.isStale && !config.lifecycle.skipStaleFor.has(d.status)).length;
655
+ process.stdout.write(JSON.stringify({
656
+ plans: { total: plans.length, inSession: plans.filter(d => d.status === 'in-session').map(d => ({ path: d.path, title: d.title, nextStep: d.nextStep })), active: plans.filter(d => d.status === 'active').map(d => ({ path: d.path, title: d.title, nextStep: d.nextStep })) },
657
+ docs: { total: docs.length, active: docs.filter(d => !config.lifecycle.terminalStatuses.has(d.status)).length },
658
+ research: { total: research.length, active: research.filter(d => d.status === 'active').length },
659
+ stale, errorCount: index.errors.length, warningCount: index.warnings.length,
660
+ }, null, 2) + '\n');
661
+ } else {
662
+ process.stdout.write(renderBriefing(index, config));
663
+ }
664
+ return;
665
+ }
666
+
651
667
  if (command === 'context') {
652
668
  const summarize = args.includes('--summarize');
653
669
  const modelIdx = args.indexOf('--model');
@@ -703,6 +719,7 @@ async function main() {
703
719
  }
704
720
 
705
721
  if (command === 'graph') {
722
+ const { buildGraph, renderGraphText, renderGraphDot, renderGraphJson } = await import('../src/graph.mjs');
706
723
  const statusFilter = (() => { const i = args.indexOf('--status'); return i !== -1 && args[i + 1] ? args[i + 1] : null; })();
707
724
  const moduleFilter = (() => { const i = args.indexOf('--module'); return i !== -1 && args[i + 1] ? args[i + 1] : null; })();
708
725
  const surfaceFilter = (() => { const i = args.indexOf('--surface'); return i !== -1 && args[i + 1] ? args[i + 1] : null; })();
@@ -723,7 +740,7 @@ async function main() {
723
740
 
724
741
  // Unknown command — suggest closest match
725
742
  const allCommands = [
726
- 'list', 'json', 'check', 'coverage', 'stats', 'graph', 'deps', 'context',
743
+ 'list', 'json', 'check', 'coverage', 'stats', 'graph', 'deps', 'briefing', 'context',
727
744
  'focus', 'query', 'plans', 'stale', 'actionable', 'index', 'pickup', 'finish', 'status', 'archive', 'touch', 'doctor',
728
745
  'fix-refs', 'lint', 'rename', 'migrate', 'notion', 'export', 'summary',
729
746
  'watch', 'diff', 'new', 'init', 'completions',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotmd-cli",
3
- "version": "0.12.0",
3
+ "version": "0.13.1",
4
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",
@@ -1,7 +1,7 @@
1
1
  import { die } from './util.mjs';
2
2
 
3
3
  const COMMANDS = [
4
- 'list', 'json', 'check', 'coverage', 'stats', 'graph', 'deps', 'unblocks', 'health', 'glossary', 'context', 'focus', 'query',
4
+ 'list', 'json', 'check', 'coverage', 'stats', 'graph', 'deps', 'unblocks', 'health', 'glossary', 'briefing', 'context', 'focus', 'query',
5
5
  'plans', 'stale', 'actionable', 'index', 'pickup', 'finish', 'status', 'archive', 'bulk', 'touch', 'doctor', 'lint', 'rename', 'migrate',
6
6
  'fix-refs', 'notion', 'export', 'summary', 'watch', 'diff', 'init', 'new', 'completions',
7
7
  ];
@@ -32,6 +32,7 @@ const COMMAND_FLAGS = {
32
32
  plans: ['--status', '--json', '--sort', '--limit', '--all', '--stale', '--has-next-step'],
33
33
  stale: ['--json', '--sort', '--limit', '--all'],
34
34
  actionable: ['--json', '--sort', '--limit', '--all'],
35
+ briefing: ['--json'],
35
36
  pickup: ['--json'],
36
37
  finish: ['--json'],
37
38
  status: [],
package/src/git.mjs CHANGED
@@ -19,6 +19,24 @@ export function getGitLastModified(relPath, repoRoot) {
19
19
  return result.stdout.trim();
20
20
  }
21
21
 
22
+ export function getGitLastModifiedBatch(repoRoot) {
23
+ const result = spawnSync('git', [
24
+ 'log', '--format=commit %aI', '--name-only', '--diff-filter=ACDMR', 'HEAD',
25
+ ], { cwd: repoRoot, encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 });
26
+ if (result.error || result.status !== 0) return new Map();
27
+
28
+ const map = new Map();
29
+ let currentDate = null;
30
+ for (const line of result.stdout.split('\n')) {
31
+ if (line.startsWith('commit ')) {
32
+ currentDate = line.slice(7).trim();
33
+ } else if (line && currentDate && !map.has(line)) {
34
+ map.set(line, currentDate);
35
+ }
36
+ }
37
+ return map;
38
+ }
39
+
22
40
  export function gitMv(source, target, repoRoot) {
23
41
  ensureGit();
24
42
  const result = spawnSync('git', ['mv', source, target], {
package/src/lifecycle.mjs CHANGED
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { extractFrontmatter, parseSimpleFrontmatter, replaceFrontmatter } from './frontmatter.mjs';
4
4
  import { asString, toRepoPath, die, warn, resolveDocPath, escapeRegex } from './util.mjs';
5
- import { gitMv, getGitLastModified } from './git.mjs';
5
+ import { gitMv, getGitLastModified, getGitLastModifiedBatch } from './git.mjs';
6
6
  import { buildIndex, collectDocFiles } from './index.mjs';
7
7
  import { renderIndexFile, writeIndex } from './index-file.mjs';
8
8
  import { green, dim, yellow } from './color.mjs';
@@ -356,6 +356,7 @@ export function runTouch(argv, config, opts = {}) {
356
356
 
357
357
  const prefix = dryRun ? dim('[dry-run] ') : '';
358
358
  let synced = 0;
359
+ const gitDates = getGitLastModifiedBatch(config.repoRoot);
359
360
 
360
361
  for (const filePath of allFiles) {
361
362
  const repoPath = toRepoPath(filePath, config.repoRoot);
@@ -368,7 +369,7 @@ export function runTouch(argv, config, opts = {}) {
368
369
  if (config.lifecycle.skipStaleFor.has(status)) continue;
369
370
 
370
371
  const fmUpdated = asString(parsed.updated);
371
- const gitDate = getGitLastModified(repoPath, config.repoRoot);
372
+ const gitDate = gitDates.get(repoPath) ?? null;
372
373
  if (!gitDate) continue;
373
374
 
374
375
  const gitDay = gitDate.slice(0, 10);
package/src/query.mjs CHANGED
@@ -3,7 +3,7 @@ import path from 'node:path';
3
3
  import { capitalize, toSlug, warn } from './util.mjs';
4
4
  import { renderProgressBar } from './render.mjs';
5
5
  import { computeDaysSinceUpdate, computeIsStale } from './validate.mjs';
6
- import { getGitLastModified } from './git.mjs';
6
+ import { getGitLastModifiedBatch } from './git.mjs';
7
7
  import { extractFrontmatter } from './frontmatter.mjs';
8
8
  import { summarizeDocBody } from './ai.mjs';
9
9
  import { dim } from './color.mjs';
@@ -128,8 +128,9 @@ export function filterDocs(docs, filters, config) {
128
128
  if (filters.updatedSince) result = result.filter(d => d.updated && d.updated >= filters.updatedSince);
129
129
 
130
130
  if (filters.git) {
131
+ const gitDates = getGitLastModifiedBatch(config.repoRoot);
131
132
  for (const doc of result) {
132
- const gitDate = getGitLastModified(doc.path, config.repoRoot);
133
+ const gitDate = gitDates.get(doc.path) ?? null;
133
134
  if (gitDate) {
134
135
  doc.daysSinceUpdate = computeDaysSinceUpdate(gitDate);
135
136
  doc.isStale = computeIsStale(doc.status, gitDate, config);
package/src/render.mjs CHANGED
@@ -258,6 +258,44 @@ function _renderContext(index, config, opts = {}) {
258
258
  return `${lines.join('\n').trimEnd()}\n`;
259
259
  }
260
260
 
261
+ export function renderBriefing(index, config) {
262
+ const lines = [];
263
+ const plans = index.docs.filter(d => d.type === 'plan');
264
+ const docs = index.docs.filter(d => d.type === 'doc');
265
+ const research = index.docs.filter(d => d.type === 'research');
266
+ const untyped = index.docs.filter(d => !d.type);
267
+
268
+ if (plans.length) {
269
+ const bySt = {};
270
+ for (const p of plans) { bySt[p.status] = (bySt[p.status] ?? 0) + 1; }
271
+ const counts = Object.entries(bySt).map(([s, n]) => `${n} ${s}`).join(', ');
272
+ lines.push(`${plans.length} plans: ${counts}`);
273
+ const show = plans.filter(p => p.status === 'in-session' || p.status === 'active');
274
+ for (const p of show) {
275
+ const next = p.nextStep ? `next: ${p.nextStep}` : '(no next step)';
276
+ lines.push(` > ${path.basename(p.path, '.md')} (${p.status}) ${next}`);
277
+ }
278
+ }
279
+
280
+ const parts = [];
281
+ if (docs.length) {
282
+ const active = docs.filter(d => !config.lifecycle.terminalStatuses.has(d.status)).length;
283
+ const rest = docs.length - active;
284
+ parts.push(`${active} docs active` + (rest ? `, ${rest} other` : ''));
285
+ }
286
+ if (research.length) {
287
+ const active = research.filter(d => d.status === 'active').length;
288
+ parts.push(`${active} research active`);
289
+ }
290
+ if (untyped.length) parts.push(`${untyped.length} untyped`);
291
+ if (parts.length) lines.push(parts.join(' | '));
292
+
293
+ const stale = index.docs.filter(d => d.isStale && !config.lifecycle.skipStaleFor.has(d.status)).length;
294
+ lines.push(`Stale: ${stale} | Errors: ${index.errors.length} | Warnings: ${index.warnings.length}`);
295
+
296
+ return lines.join('\n') + '\n';
297
+ }
298
+
261
299
  export function renderCheck(index, config, opts = {}) {
262
300
  const defaultRenderer = (idx) => _renderCheck(idx, opts);
263
301
  if (config.hooks.renderCheck) {
package/src/validate.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { asString } from './util.mjs';
4
- import { getGitLastModified } from './git.mjs';
4
+ import { getGitLastModified, getGitLastModifiedBatch } from './git.mjs';
5
5
  import { toRepoPath } from './util.mjs';
6
6
 
7
7
  const NOW = new Date();
@@ -150,11 +150,12 @@ export function checkBidirectionalReferences(docs, config) {
150
150
 
151
151
  export function checkGitStaleness(docs, config) {
152
152
  const warnings = [];
153
+ const gitDates = getGitLastModifiedBatch(config.repoRoot);
153
154
  for (const doc of docs) {
154
155
  if (config.lifecycle.skipStaleFor.has(doc.status)) continue;
155
156
  if (!doc.updated) continue;
156
157
 
157
- const gitDate = getGitLastModified(doc.path, config.repoRoot);
158
+ const gitDate = gitDates.get(doc.path) ?? null;
158
159
  if (!gitDate) continue;
159
160
 
160
161
  const gitDay = gitDate.slice(0, 10);