dotmd-cli 0.48.2 → 0.48.4
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/bin/dotmd.mjs +19 -2
- package/package.json +2 -1
- package/src/claude-commands.mjs +11 -18
- package/src/commands.mjs +1 -1
- package/src/hud.mjs +5 -1
- package/src/index-file.mjs +23 -2
- package/src/index.mjs +10 -2
- package/src/journal.mjs +57 -4
- package/src/prompts.mjs +8 -1
package/bin/dotmd.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { resolveConfig } from '../src/config.mjs';
|
|
7
7
|
import { die, warn, levenshtein } from '../src/util.mjs';
|
|
8
|
-
import { recordCliInvocation } from '../src/journal.mjs';
|
|
8
|
+
import { recordCliInvocation, recordGlobalError } from '../src/journal.mjs';
|
|
9
9
|
import { findRepeatFailureHint } from '../src/hints.mjs';
|
|
10
10
|
|
|
11
11
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -1260,7 +1260,13 @@ async function main() {
|
|
|
1260
1260
|
const { scrubStaleSilently } = await import('../src/lease-scrub.mjs');
|
|
1261
1261
|
scrubStaleSilently(config);
|
|
1262
1262
|
}
|
|
1263
|
-
|
|
1263
|
+
// `dotmd check` is the one shared-buildIndex command that should auto-heal a
|
|
1264
|
+
// drifted index block (frontmatter edits by direct Edit/Write, `lint --fix`,
|
|
1265
|
+
// etc. leave the README out of sync; demanding the user run `dotmd index`
|
|
1266
|
+
// each time was pure noise). Print/dry-run/read-only callers (`json`, `list`,
|
|
1267
|
+
// `query`, `index --print`, ...) stay opt-out so they never mutate disk.
|
|
1268
|
+
const AUTO_HEAL_INDEX_COMMANDS = new Set(['check']);
|
|
1269
|
+
const index = buildIndex(config, { autoHealIndex: AUTO_HEAL_INDEX_COMMANDS.has(command) });
|
|
1264
1270
|
|
|
1265
1271
|
// Apply --root and --type filters
|
|
1266
1272
|
const rootFilter = rootArg;
|
|
@@ -1552,6 +1558,17 @@ function _journalExit(err) {
|
|
|
1552
1558
|
version: pkg.version,
|
|
1553
1559
|
});
|
|
1554
1560
|
} catch { /* never break exit on journal failure */ }
|
|
1561
|
+
if (err) {
|
|
1562
|
+
try {
|
|
1563
|
+
recordGlobalError({
|
|
1564
|
+
config: _resolvedConfig,
|
|
1565
|
+
startMs: _startMs,
|
|
1566
|
+
args: _invocationArgs,
|
|
1567
|
+
err,
|
|
1568
|
+
version: pkg.version,
|
|
1569
|
+
});
|
|
1570
|
+
} catch { /* never break exit on error-log failure */ }
|
|
1571
|
+
}
|
|
1555
1572
|
}
|
|
1556
1573
|
|
|
1557
1574
|
main()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dotmd-cli",
|
|
3
|
-
"version": "0.48.
|
|
3
|
+
"version": "0.48.4",
|
|
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",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"scripts": {
|
|
41
41
|
"test": "node --test test/*.test.mjs",
|
|
42
42
|
"preversion": "npm test",
|
|
43
|
+
"version": "node bin/dotmd.mjs hud >/dev/null 2>&1; git add .claude/commands docs/docs.md 2>/dev/null; true",
|
|
43
44
|
"postversion": "git push origin main --tags && gh release create v$npm_package_version --generate-notes --title v$npm_package_version && sleep 5 && gh run watch $(gh run list --workflow=publish.yml --limit 1 --json databaseId --jq '.[0].databaseId') --exit-status && sleep 10 && npm cache clean --force && npm install -g dotmd-cli@$npm_package_version"
|
|
44
45
|
},
|
|
45
46
|
"engines": {
|
package/src/claude-commands.mjs
CHANGED
|
@@ -76,6 +76,8 @@ function generatePlansCommand(config, version) {
|
|
|
76
76
|
lines.push('- `dotmd unblocks <file>` — what depends on / is blocked by a plan');
|
|
77
77
|
lines.push('- `dotmd actionable` — ready plans with next steps (what to promote)');
|
|
78
78
|
lines.push('- `dotmd query --keyword <term>` — find plans by keyword');
|
|
79
|
+
lines.push('- `dotmd runlist <hub>` — show ordered children of a runlist hub (→ marks next pickup)');
|
|
80
|
+
lines.push('- `dotmd runlist next <hub>` — pick up the next non-archived child of a runlist hub');
|
|
79
81
|
|
|
80
82
|
if (config.raw?.glossary) {
|
|
81
83
|
lines.push('- `dotmd glossary <term>` — domain term lookup with related plans');
|
|
@@ -86,6 +88,7 @@ function generatePlansCommand(config, version) {
|
|
|
86
88
|
lines.push('');
|
|
87
89
|
lines.push('If the user asks to change a plan\'s status, use `dotmd set <status> <file>`.');
|
|
88
90
|
lines.push('If the user asks to archive a plan, use `dotmd set archived <file>` (or `dotmd archive <file>`).');
|
|
91
|
+
lines.push('If the user references a runlist by name — e.g. "what\'s next on <X> runlist", "<X> runlist status", "pick up the next in <X>" — use `dotmd runlist next <X>` (or `dotmd runlist <X>` first to inspect the ordering). Do NOT fall back to `dotmd context` for runlist-scoped questions.');
|
|
89
92
|
lines.push('');
|
|
90
93
|
lines.push('**Saved prompts (`docs/prompts/*.md`):** if the user references a file under `docs/prompts/` — e.g. "resume via docs/prompts/foo.md", "use this prompt", "load that one" — consume it with `dotmd use <file>` (atomically prints the body and archives the prompt so it cannot be double-consumed). Do NOT `cat` it, read it with the file-reading tool, or copy its body into chat. To pick the oldest pending prompt without naming a file, run `dotmd use` with no arg.');
|
|
91
94
|
lines.push('');
|
|
@@ -229,22 +232,12 @@ export function refreshStaleSlashCommands(config) {
|
|
|
229
232
|
return results.filter(r => r.action === 'updated');
|
|
230
233
|
}
|
|
231
234
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const installedVersion = getInstalledVersion(filePath);
|
|
241
|
-
if (installedVersion && installedVersion !== version) {
|
|
242
|
-
warnings.push({
|
|
243
|
-
path: `.claude/commands/${name}`,
|
|
244
|
-
level: 'warning',
|
|
245
|
-
message: `Claude command outdated (v${installedVersion} → v${version}). Run \`dotmd doctor\` to update.`,
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
return warnings;
|
|
235
|
+
// Intentionally returns []. Slash-command stamp drift is auto-healed every
|
|
236
|
+
// time `dotmd hud` runs (SessionStart hook), and `dotmd doctor` regens them
|
|
237
|
+
// on demand. Surfacing a warning at `dotmd check` time was pure noise — it
|
|
238
|
+
// fired on every release until the next session, despite the user having no
|
|
239
|
+
// action to take (the heal is automatic). Kept the function for API stability
|
|
240
|
+
// in case downstream callers import it.
|
|
241
|
+
export function checkClaudeCommands(_cwd, _opts = {}) {
|
|
242
|
+
return [];
|
|
250
243
|
}
|
package/src/commands.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// templates points at a real command.
|
|
5
5
|
export const KNOWN_COMMANDS = [
|
|
6
6
|
'list', 'json', 'check', 'coverage', 'stats', 'graph', 'deps', 'briefing', 'context', 'hud',
|
|
7
|
-
'focus', 'query', 'plans', 'prompts', 'stale', 'actionable', 'index', 'pickup', 'release', 'status', 'set', 'use', 'next', 'archive', 'bulk', 'bulk-tag', 'touch', 'doctor',
|
|
7
|
+
'focus', 'query', 'plans', 'prompts', 'stale', 'actionable', 'index', 'pickup', 'release', 'status', 'set', 'use', 'next', 'archive', 'bulk', 'bulk-tag', 'touch', 'doctor', 'runlist',
|
|
8
8
|
'unblocks', 'health', 'glossary', 'modules', 'module',
|
|
9
9
|
'fix-refs', 'lint', 'rename', 'migrate', 'notion', 'export', 'summary',
|
|
10
10
|
'watch', 'diff', 'new', 'init', 'completions', 'statuses', 'journal',
|
package/src/hud.mjs
CHANGED
|
@@ -205,7 +205,11 @@ export function buildHud(config) {
|
|
|
205
205
|
// still run, so the error count matches `dotmd check`'s.
|
|
206
206
|
let errors = 0;
|
|
207
207
|
try {
|
|
208
|
-
|
|
208
|
+
// `autoHealIndex: true` mirrors `dotmd check` — drift from non-regen
|
|
209
|
+
// mutation paths (`lint --fix`, direct file edits, etc.) heals silently
|
|
210
|
+
// at SessionStart so the agent doesn't open every session with a
|
|
211
|
+
// spurious "Run `dotmd index`" error in the hud error count.
|
|
212
|
+
const index = buildIndex(config, { errorsOnly: true, autoHealIndex: true });
|
|
209
213
|
errors = index.errors.length;
|
|
210
214
|
} catch { /* swallow — bad config shouldn't break the SessionStart hook */ }
|
|
211
215
|
|
package/src/index-file.mjs
CHANGED
|
@@ -80,7 +80,19 @@ export function writeIndex(content, config) {
|
|
|
80
80
|
writeFileSync(config.indexPath, content, 'utf8');
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
// `autoHeal: true` rewrites the index in place when drift is detected and
|
|
84
|
+
// downgrades the result to a warning ("Auto-regenerated stale index block").
|
|
85
|
+
// Drift happens when frontmatter changes (status/title/current_state/module)
|
|
86
|
+
// arrive via paths that don't call `regenIndex` — direct file edits, `lint
|
|
87
|
+
// --fix`, `frontmatter-fix`, `bulk-tag`, etc. The README block is fully
|
|
88
|
+
// generated content; treating drift as an error forced the user to run
|
|
89
|
+
// `dotmd index` themselves every session. Callers inside `buildIndex` pass
|
|
90
|
+
// `autoHeal: true` because the docs there are always the canonical full set
|
|
91
|
+
// (filtering happens later in the CLI dispatcher). Direct callers with a
|
|
92
|
+
// filtered/synthetic docs list omit it to keep the old error semantics —
|
|
93
|
+
// auto-overwriting from a partial doc list would clobber valid content.
|
|
94
|
+
export function checkIndex(docs, config, opts = {}) {
|
|
95
|
+
const { autoHeal = false } = opts;
|
|
84
96
|
const warnings = [];
|
|
85
97
|
const errors = [];
|
|
86
98
|
|
|
@@ -98,7 +110,16 @@ export function checkIndex(docs, config) {
|
|
|
98
110
|
const index = { docs };
|
|
99
111
|
const expected = renderIndexFile(index, config);
|
|
100
112
|
if (expected !== current) {
|
|
101
|
-
|
|
113
|
+
if (autoHeal) {
|
|
114
|
+
try {
|
|
115
|
+
writeFileSync(config.indexPath, expected, 'utf8');
|
|
116
|
+
warnings.push({ path: config.indexPath, level: 'warning', message: 'Auto-regenerated stale index block.' });
|
|
117
|
+
} catch (err) {
|
|
118
|
+
errors.push({ path: config.indexPath, level: 'error', message: `Could not auto-regenerate stale index block: ${err.message}` });
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
errors.push({ path: config.indexPath, level: 'error', message: 'Generated index block is stale. Run `dotmd index`.' });
|
|
122
|
+
}
|
|
102
123
|
}
|
|
103
124
|
|
|
104
125
|
return { warnings, errors };
|
package/src/index.mjs
CHANGED
|
@@ -21,7 +21,7 @@ import { checkClaudeCommands } from './claude-commands.mjs';
|
|
|
21
21
|
// error COUNT, so the warning-only passes are pure overhead there. Preserves
|
|
22
22
|
// the invariant that hud's "✗ N validation errors" line matches `dotmd check`.
|
|
23
23
|
export function buildIndex(config, opts = {}) {
|
|
24
|
-
const { fast = false, errorsOnly = false } = opts;
|
|
24
|
+
const { fast = false, errorsOnly = false, autoHealIndex = false } = opts;
|
|
25
25
|
const skipWarningOnlyChecks = fast || errorsOnly;
|
|
26
26
|
const docs = collectDocFiles(config).map(f => parseDocFile(f, config, { fast }));
|
|
27
27
|
if (!fast) {
|
|
@@ -95,7 +95,15 @@ export function buildIndex(config, opts = {}) {
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
if (!fast && config.indexPath) {
|
|
98
|
-
|
|
98
|
+
// `autoHealIndex` is opt-in from the caller (currently `dotmd check` and
|
|
99
|
+
// `dotmd hud`). When true, drift triggers an in-place rewrite and a
|
|
100
|
+
// warning instead of the old "Run `dotmd index`" error — closing the
|
|
101
|
+
// class of nags produced by mutation paths that skip `regenIndex`
|
|
102
|
+
// (`lint --fix`, direct file edits, etc). `transformedDocs` here is
|
|
103
|
+
// always the canonical full set; CLI-level `--root`/`--type` filtering
|
|
104
|
+
// runs after `buildIndex` returns, so a rewrite is safe. Off by default
|
|
105
|
+
// so dry-run / print modes never mutate disk as a side effect.
|
|
106
|
+
const indexCheck = checkIndex(transformedDocs, config, { autoHeal: autoHealIndex });
|
|
99
107
|
warnings.push(...indexCheck.warnings);
|
|
100
108
|
errors.push(...indexCheck.errors);
|
|
101
109
|
}
|
package/src/journal.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, appendFileSync, statSync, renameSync, readFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
3
4
|
import { currentSessionId } from './lease.mjs';
|
|
4
5
|
|
|
5
6
|
const JOURNAL_DIR = '.dotmd';
|
|
@@ -8,6 +9,9 @@ const JOURNAL_BACKUP = 'journal.jsonl.1';
|
|
|
8
9
|
const ROTATE_SIZE_BYTES = 5 * 1024 * 1024;
|
|
9
10
|
const ROTATE_AGE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
10
11
|
|
|
12
|
+
const ERROR_LOG_FILE = 'dotmd-errors.log';
|
|
13
|
+
const ERROR_LOG_BACKUP = 'dotmd-errors.log.1';
|
|
14
|
+
|
|
11
15
|
export function isJournalEnabled(config) {
|
|
12
16
|
if (process.env.DOTMD_JOURNAL === '1') return true;
|
|
13
17
|
if (process.env.DOTMD_JOURNAL === '0') return false;
|
|
@@ -22,12 +26,12 @@ export function journalBackupPath(config) {
|
|
|
22
26
|
return path.join(config.repoRoot, JOURNAL_DIR, JOURNAL_BACKUP);
|
|
23
27
|
}
|
|
24
28
|
|
|
25
|
-
function maybeRotate(file,
|
|
29
|
+
function maybeRotate(file, backup) {
|
|
26
30
|
if (!existsSync(file)) return;
|
|
27
31
|
let st;
|
|
28
32
|
try { st = statSync(file); } catch { return; }
|
|
29
33
|
if (st.size > ROTATE_SIZE_BYTES) {
|
|
30
|
-
try { renameSync(file,
|
|
34
|
+
try { renameSync(file, backup); } catch {}
|
|
31
35
|
return;
|
|
32
36
|
}
|
|
33
37
|
if (st.size === 0) return;
|
|
@@ -41,7 +45,7 @@ function maybeRotate(file, config) {
|
|
|
41
45
|
const obj = JSON.parse(first);
|
|
42
46
|
const t = new Date(obj.ts).getTime();
|
|
43
47
|
if (!Number.isNaN(t) && (Date.now() - t) > ROTATE_AGE_MS) {
|
|
44
|
-
try { renameSync(file,
|
|
48
|
+
try { renameSync(file, backup); } catch {}
|
|
45
49
|
}
|
|
46
50
|
} catch {}
|
|
47
51
|
}
|
|
@@ -53,7 +57,7 @@ export function appendJournalEntry(config, entry) {
|
|
|
53
57
|
const dir = path.join(config.repoRoot, JOURNAL_DIR);
|
|
54
58
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
55
59
|
const file = journalFilePath(config);
|
|
56
|
-
maybeRotate(file, config);
|
|
60
|
+
maybeRotate(file, journalBackupPath(config));
|
|
57
61
|
// O_APPEND is atomic for writes under PIPE_BUF (4KB on Linux, 512B on
|
|
58
62
|
// macOS). Entries are well under either threshold, so concurrent CLI
|
|
59
63
|
// invocations interleave cleanly without locking.
|
|
@@ -96,3 +100,52 @@ export function recordCliInvocation({ config, startMs, args, err, version }) {
|
|
|
96
100
|
}
|
|
97
101
|
appendJournalEntry(config, entry);
|
|
98
102
|
}
|
|
103
|
+
|
|
104
|
+
// Global error log: always-on, cross-repo, captured per failed invocation.
|
|
105
|
+
// Independent of `isJournalEnabled` so silent failures stop disappearing.
|
|
106
|
+
// DOTMD_ERROR_LOG_DIR overrides the default location (for tests, or for
|
|
107
|
+
// users who want the log somewhere other than ~/.claude/logs).
|
|
108
|
+
|
|
109
|
+
export function globalErrorLogDir() {
|
|
110
|
+
return process.env.DOTMD_ERROR_LOG_DIR || path.join(os.homedir(), '.claude', 'logs');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function globalErrorLogPath() {
|
|
114
|
+
return path.join(globalErrorLogDir(), ERROR_LOG_FILE);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function globalErrorLogBackupPath() {
|
|
118
|
+
return path.join(globalErrorLogDir(), ERROR_LOG_BACKUP);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function recordGlobalError({ config, startMs, args, err, version }) {
|
|
122
|
+
if (!err) return;
|
|
123
|
+
const flatMsg = String(err.message ?? err).replace(/\s+/g, ' ').trim();
|
|
124
|
+
const entry = {
|
|
125
|
+
ts: new Date().toISOString(),
|
|
126
|
+
repo: config?.repoRoot || process.cwd(),
|
|
127
|
+
sid: currentSessionId(),
|
|
128
|
+
pid: process.pid,
|
|
129
|
+
argv: args,
|
|
130
|
+
exit: process.exitCode ?? 1,
|
|
131
|
+
ms: typeof startMs === 'number' ? Date.now() - startMs : null,
|
|
132
|
+
v: version,
|
|
133
|
+
err: flatMsg.length > 500 ? flatMsg.slice(0, 497) + '...' : flatMsg,
|
|
134
|
+
};
|
|
135
|
+
if (err && err.name) entry.errName = err.name;
|
|
136
|
+
if (err && err.stack) {
|
|
137
|
+
// Keep the first few frames; stacks for DotmdError are short anyway and
|
|
138
|
+
// for unexpected exceptions five frames is usually enough to localize.
|
|
139
|
+
const stack = String(err.stack).split('\n').slice(0, 6).join('\n');
|
|
140
|
+
entry.stack = stack.length > 1000 ? stack.slice(0, 997) + '...' : stack;
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
const dir = globalErrorLogDir();
|
|
144
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
145
|
+
const file = globalErrorLogPath();
|
|
146
|
+
maybeRotate(file, globalErrorLogBackupPath());
|
|
147
|
+
appendFileSync(file, JSON.stringify(entry) + '\n', { flag: 'a' });
|
|
148
|
+
} catch {
|
|
149
|
+
// Logging must never break exit.
|
|
150
|
+
}
|
|
151
|
+
}
|
package/src/prompts.mjs
CHANGED
|
@@ -226,10 +226,17 @@ export function consumePrompt(filePath, config, opts) {
|
|
|
226
226
|
return;
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
// Archive BEFORE emitting the body. If runArchive throws (git mv failure,
|
|
230
|
+
// hook crash, anything), the body must not have already gone to stdout —
|
|
231
|
+
// otherwise `claude "$(dotmd prompts next)"` consumes the prompt without it
|
|
232
|
+
// ever being archived, and the next session sees the same prompt as pending.
|
|
233
|
+
// Body is already in memory from extractFrontmatter, so the source file
|
|
234
|
+
// can move out from under us safely.
|
|
235
|
+
runArchive([filePath], config, { noIndex, showFiles, out: process.stderr });
|
|
236
|
+
|
|
229
237
|
process.stdout.write(body);
|
|
230
238
|
if (!body.endsWith('\n')) process.stdout.write('\n');
|
|
231
239
|
|
|
232
|
-
runArchive([filePath], config, { noIndex, showFiles, out: process.stderr });
|
|
233
240
|
process.stderr.write(`${green('✓ Consumed')}: ${repoPath}\n`);
|
|
234
241
|
}
|
|
235
242
|
|