dotmd-cli 0.42.1 → 0.44.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/bin/dotmd.mjs +34 -15
- package/package.json +1 -1
- package/src/claude-commands.mjs +18 -20
- package/src/hud.mjs +12 -31
- package/src/lifecycle.mjs +10 -4
- package/src/new.mjs +32 -8
- package/src/pickup-card.mjs +2 -2
- package/src/prompts.mjs +1 -1
- package/src/validate.mjs +2 -2
package/bin/dotmd.mjs
CHANGED
|
@@ -14,6 +14,26 @@ const pkg = JSON.parse(readFileSync(path.join(__dirname, '..', 'package.json'),
|
|
|
14
14
|
const HELP = {
|
|
15
15
|
_main: `dotmd v${pkg.version} — frontmatter markdown document manager
|
|
16
16
|
|
|
17
|
+
Common commands:
|
|
18
|
+
plans Live plans (excludes archived)
|
|
19
|
+
briefing Full briefing with plan counts + next steps
|
|
20
|
+
set <status> [file] Transition status (start work, finish, archive — all via target status)
|
|
21
|
+
new <type> <name> Create plan/doc/prompt (pipe stdin or @path for body)
|
|
22
|
+
archive <file> Close out a plan (status → archived, move, update refs)
|
|
23
|
+
prompts [next|use|new] Manage saved prompts
|
|
24
|
+
|
|
25
|
+
More help:
|
|
26
|
+
dotmd help all Full command list
|
|
27
|
+
dotmd help statuses Status vocabulary + transitions
|
|
28
|
+
dotmd <cmd> --help Per-command details
|
|
29
|
+
|
|
30
|
+
Global flags: --config <path> --root <name> --type <t,…> --dry-run/-n --verbose --version`,
|
|
31
|
+
|
|
32
|
+
// Full command list — opt-in via \`dotmd help all\`. Kept exhaustive so the
|
|
33
|
+
// top-level \`--help\` can stay terse without losing discoverability. When you
|
|
34
|
+
// add a new command, add it here too.
|
|
35
|
+
'help:all': `dotmd v${pkg.version} — full command list
|
|
36
|
+
|
|
17
37
|
View & Query:
|
|
18
38
|
hud [--json] Two-line actionable triage (held / prompts / stuck) — silent when clean
|
|
19
39
|
list [--verbose] [--json] List docs grouped by status (default command)
|
|
@@ -42,17 +62,14 @@ Analyze:
|
|
|
42
62
|
glossary <term> [--list] [--json] Look up domain terms + related docs
|
|
43
63
|
|
|
44
64
|
Validate & Fix:
|
|
45
|
-
check [--fix] [--errors-only] [--json] Validate frontmatter and references
|
|
46
65
|
doctor [--apply] Auto-fix everything: refs, lint, dates, index (preview by default)
|
|
47
66
|
lint [--fix] Check and auto-fix frontmatter issues
|
|
48
67
|
fix-refs [--dry-run] Auto-fix broken reference paths + body links
|
|
49
68
|
|
|
50
69
|
Lifecycle:
|
|
51
|
-
|
|
52
|
-
release [<file>] [--to <s>] Release in-session lease (alias: unpickup)
|
|
70
|
+
set <status> [<file>] Unified transition: archive/release/start/transition in one verb
|
|
53
71
|
runlist <hub> [next] Show or walk an ordered group of plans (see \`dotmd help runlist\`)
|
|
54
72
|
status <file> <status> Transition document status (deprecated; prefer \`set\`)
|
|
55
|
-
set <status> [<file>] Unified transition: archive/release/transition in one verb
|
|
56
73
|
archive <file> Archive (status + move + update refs)
|
|
57
74
|
bulk archive <f1> <f2> ... Archive multiple files at once
|
|
58
75
|
ship [patch|minor|major] Regen + commit + bump in one step (default: patch)
|
|
@@ -636,16 +653,17 @@ Types and their default destinations:
|
|
|
636
653
|
\`<name>\` is slugified for the filename.
|
|
637
654
|
|
|
638
655
|
Body input (all built-in types — required for prompt, optional for plan/doc):
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
656
|
+
piped stdin Auto-consumed when stdin is piped/redirected (no flag needed)
|
|
657
|
+
@path Read body from a file
|
|
658
|
+
- Explicit stdin marker (equivalent to piped stdin)
|
|
659
|
+
--body "<text>" Explicit inline body (alias: --message)
|
|
642
660
|
<text> Inline body as 3rd positional
|
|
643
661
|
|
|
644
|
-
Tip for agents: prefer \`@path\`
|
|
645
|
-
put the entire content on the bash command line, which (a) breaks
|
|
646
|
-
quoting for backticks/dollar-signs and (b) trips PreToolUse hooks
|
|
647
|
-
command strings for forbidden literals (destructive-git patterns,
|
|
648
|
-
\`@/tmp/foo.md\`
|
|
662
|
+
Tip for agents: prefer piped stdin or \`@path\` for multi-line bodies. Inline
|
|
663
|
+
bodies put the entire content on the bash command line, which (a) breaks
|
|
664
|
+
under shell quoting for backticks/dollar-signs and (b) trips PreToolUse hooks
|
|
665
|
+
that scan command strings for forbidden literals (destructive-git patterns,
|
|
666
|
+
etc.). \`cat /tmp/foo.md | dotmd new …\` and \`@/tmp/foo.md\` both sidestep both.
|
|
649
667
|
|
|
650
668
|
For plan/doc, a single-section body lands under the type's first scaffolded
|
|
651
669
|
section (e.g. \`## Problem\` for plans). If the body already authors
|
|
@@ -656,12 +674,13 @@ the title + your body is emitted — no duplicated empty outline below
|
|
|
656
674
|
Examples:
|
|
657
675
|
dotmd new plan auth-revamp
|
|
658
676
|
dotmd new prompt resume-foo @/tmp/draft.md
|
|
659
|
-
dotmd new prompt resume-foo
|
|
677
|
+
cat /tmp/draft.md | dotmd new prompt resume-foo
|
|
678
|
+
dotmd new prompt resume-foo <<'EOF'
|
|
660
679
|
multi-line
|
|
661
680
|
prompt body
|
|
662
681
|
EOF
|
|
663
682
|
dotmd new prompt cleanup-tomorrow "look at remaining lint warnings"
|
|
664
|
-
dotmd new plan full-spec
|
|
683
|
+
dotmd new plan full-spec <<'EOF'
|
|
665
684
|
## Problem
|
|
666
685
|
…
|
|
667
686
|
## Phases
|
|
@@ -1040,7 +1059,7 @@ async function main() {
|
|
|
1040
1059
|
const key = `help:${topic}`;
|
|
1041
1060
|
if (HELP[key]) { process.stdout.write(`${HELP[key]}\n`); return; }
|
|
1042
1061
|
if (HELP[topic]) { process.stdout.write(`${HELP[topic]}\n`); return; }
|
|
1043
|
-
process.stderr.write(`Unknown help topic: ${topic}\n\nAvailable topics: statuses\nPer-command help: dotmd <cmd> --help\n`);
|
|
1062
|
+
process.stderr.write(`Unknown help topic: ${topic}\n\nAvailable topics: all, statuses\nPer-command help: dotmd <cmd> --help\n`);
|
|
1044
1063
|
process.exit(1);
|
|
1045
1064
|
}
|
|
1046
1065
|
process.stdout.write(`${HELP._main}\n`);
|
package/package.json
CHANGED
package/src/claude-commands.mjs
CHANGED
|
@@ -63,19 +63,18 @@ function generatePlansCommand(config, version) {
|
|
|
63
63
|
lines.push('');
|
|
64
64
|
lines.push('Plan-specific commands:');
|
|
65
65
|
lines.push('- `dotmd context` — briefing with active/paused/ready plans, age tags, next steps');
|
|
66
|
-
lines.push('- `dotmd
|
|
67
|
-
lines.push('- `dotmd
|
|
68
|
-
lines.push('- `dotmd
|
|
69
|
-
lines.push('- `dotmd
|
|
70
|
-
lines.push('- `dotmd
|
|
66
|
+
lines.push('- `dotmd set <status> [<file>]` — single status verb. Use this to start, transition, or close any plan:');
|
|
67
|
+
lines.push(' - `dotmd set in-session <file>` — start work on a plan (acquires the lease + prints body)');
|
|
68
|
+
lines.push(' - `dotmd set <status> [<file>]` — transition to any other status; if you held the lease it releases automatically');
|
|
69
|
+
lines.push(' - `dotmd set archived <file>` — close out (same as `dotmd archive`)');
|
|
70
|
+
lines.push('- `dotmd archive <file>` — explicit archive with ref-fixing (equivalent to `set archived`)');
|
|
71
|
+
lines.push('- `dotmd bulk archive <files>` — archive multiple at once');
|
|
71
72
|
lines.push('- `dotmd new plan <name>` — scaffold with full phase structure');
|
|
72
|
-
lines.push('- `dotmd prompts new <name
|
|
73
|
+
lines.push('- `dotmd prompts new <name>` — save a resume-prompt to docs/prompts/ (pipe stdin or @path for body)');
|
|
73
74
|
lines.push('- `dotmd prompts next` — consume oldest pending prompt (prints body, auto-archives)');
|
|
74
75
|
lines.push('- `dotmd prompts use <file>` — consume a specific prompt (prints body, auto-archives)');
|
|
75
|
-
lines.push('- `dotmd
|
|
76
|
-
lines.push('- `dotmd
|
|
77
|
-
lines.push('- `dotmd set <status> [<file>]` — unified transition (archive / release / status bump in one verb; infers path from held lease)');
|
|
78
|
-
lines.push('- `dotmd status <file> <status>` — transition status (legacy; `set` is preferred)');
|
|
76
|
+
lines.push('- `dotmd unblocks <file>` — what depends on / is blocked by a plan');
|
|
77
|
+
lines.push('- `dotmd actionable` — ready plans with next steps (what to promote)');
|
|
79
78
|
lines.push('- `dotmd query --keyword <term>` — find plans by keyword');
|
|
80
79
|
|
|
81
80
|
if (config.raw?.glossary) {
|
|
@@ -85,8 +84,8 @@ function generatePlansCommand(config, version) {
|
|
|
85
84
|
lines.push('');
|
|
86
85
|
lines.push('If the user asks about a specific plan, read its file directly (path is in the briefing or findable via `dotmd query --keyword <term>`).');
|
|
87
86
|
lines.push('');
|
|
88
|
-
lines.push('If the user asks to change a plan\'s status, use `dotmd
|
|
89
|
-
lines.push('If the user asks to archive a plan, use `dotmd archive <file
|
|
87
|
+
lines.push('If the user asks to change a plan\'s status, use `dotmd set <status> <file>`.');
|
|
88
|
+
lines.push('If the user asks to archive a plan, use `dotmd set archived <file>` (or `dotmd archive <file>`).');
|
|
90
89
|
lines.push('');
|
|
91
90
|
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 prompts 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, use `dotmd prompts next`.');
|
|
92
91
|
lines.push('');
|
|
@@ -96,15 +95,15 @@ function generatePlansCommand(config, version) {
|
|
|
96
95
|
|
|
97
96
|
function generateBatonCommand(config, version) {
|
|
98
97
|
const lines = [...frontmatterFor('baton', config), markerFor(version), ''];
|
|
99
|
-
lines.push('Wrap this session.
|
|
100
|
-
lines.push('');
|
|
101
|
-
lines.push('1. **Save the resume prompt.** `dotmd new prompt resume-<plan-slug>` with a 10-20 line body via heredoc: the next concrete decision plus any gotchas. NOT a recap of the plan body. The saved prompt IS the handoff — never print it into chat for copy-paste.');
|
|
98
|
+
lines.push('Wrap this session. Two commands:');
|
|
102
99
|
lines.push('');
|
|
103
|
-
lines.push('
|
|
100
|
+
lines.push('1. **Save the resume prompt.** `dotmd new prompt resume-<plan-slug>` — pipe stdin or pass `@path`. 10-20 line body: the next concrete decision plus any gotchas. NOT a recap of the plan body. The saved prompt IS the handoff — never print it into chat for copy-paste.');
|
|
104
101
|
lines.push('');
|
|
105
|
-
lines.push('
|
|
106
|
-
lines.push('-
|
|
107
|
-
lines.push('-
|
|
102
|
+
lines.push('2. **Close out via `dotmd set <status>`.** Pick the status that matches reality:');
|
|
103
|
+
lines.push(' - `dotmd set active <file>` — work continues, release the lease back to the queue');
|
|
104
|
+
lines.push(' - `dotmd set archived <file>` — fully shipped (also: `dotmd archive <file>`)');
|
|
105
|
+
lines.push(' - `dotmd set paused <file>` / `awaiting <file>` / `partial <file>` / `blocked <file>` — when the status really changed');
|
|
106
|
+
lines.push(' `set` releases the held lease automatically when transitioning out of `in-session`.');
|
|
108
107
|
lines.push('');
|
|
109
108
|
lines.push('If you don\'t already know which plan you hold: `dotmd hud --json` and read `.owned`. Do NOT use `dotmd plans --status in-session` — that lists every session\'s holdings, not just yours.');
|
|
110
109
|
lines.push('');
|
|
@@ -131,7 +130,6 @@ function generateDocsCommand(config, version) {
|
|
|
131
130
|
|
|
132
131
|
lines.push('Commands for working with docs:');
|
|
133
132
|
lines.push('- `dotmd context` — LLM-oriented briefing across all types');
|
|
134
|
-
lines.push('- `dotmd check` — validate frontmatter, refs, body links (target: 0 errors)');
|
|
135
133
|
lines.push('- `dotmd doctor` — auto-fix everything in one pass (refs, lint, dates, index)');
|
|
136
134
|
lines.push('- `dotmd query [filters]` — search by status, keyword, module, surface, type, staleness');
|
|
137
135
|
lines.push('- `dotmd health` — plan pipeline, velocity, aging');
|
package/src/hud.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { existsSync,
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { readLeases, findStaleLeases, currentSessionId
|
|
3
|
+
import { readLeases, findStaleLeases, currentSessionId } from './lease.mjs';
|
|
4
4
|
import { scrubStaleSilently } from './lease-scrub.mjs';
|
|
5
5
|
import { extractFrontmatter, parseSimpleFrontmatter } from './frontmatter.mjs';
|
|
6
6
|
import { asString, toRepoPath } from './util.mjs';
|
|
7
|
-
import {
|
|
7
|
+
import { dim } from './color.mjs';
|
|
8
8
|
import { buildIndex } from './index.mjs';
|
|
9
9
|
import { refreshStaleSlashCommands } from './claude-commands.mjs';
|
|
10
10
|
|
|
@@ -115,18 +115,15 @@ export function runHud(argv, config) {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
const lines = [];
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (hud.errors > 0) {
|
|
128
|
-
lines.push(red(`✗ ${hud.errors} validation error${hud.errors === 1 ? '' : 's'} ${dim('(run: dotmd check)')}`));
|
|
129
|
-
}
|
|
118
|
+
|
|
119
|
+
// Always-on command primer. Replaces the prior plan-state / prompts /
|
|
120
|
+
// stuck-leases / validation-errors lines — those signals belong inside
|
|
121
|
+
// their own commands (`plans`, `prompts`), not in the SessionStart hook.
|
|
122
|
+
// hud's job is purely to remind the agent which verbs exist, since that's
|
|
123
|
+
// the one thing the agent reaches for `--help` to recover. Keep it tight:
|
|
124
|
+
// one line, the minimum verb set.
|
|
125
|
+
lines.push(dim('dotmd: plans|briefing set <status> [<file>] new <type> <slug> prompts next|use|new archive <file>'));
|
|
126
|
+
|
|
130
127
|
if (refreshed.length > 0) {
|
|
131
128
|
const from = refreshed[0].from;
|
|
132
129
|
const to = refreshed[0].to;
|
|
@@ -134,21 +131,5 @@ export function runHud(argv, config) {
|
|
|
134
131
|
lines.push(dim(`↻ slash commands refreshed (v${from} → v${to}): ${names}`));
|
|
135
132
|
}
|
|
136
133
|
|
|
137
|
-
// Teach-once primer for fresh installs. The first session that runs hud in
|
|
138
|
-
// a dotmd-managed repo gets one line explaining what dotmd is and how to
|
|
139
|
-
// hand off — without this, a clean repo emits nothing and Claude has no
|
|
140
|
-
// signal that dotmd is the prompt/handoff machinery here. Gated on a
|
|
141
|
-
// marker under .dotmd/ (already the per-repo state dir, already gitignored
|
|
142
|
-
// by `dotmd init`) so subsequent sessions stay silent.
|
|
143
|
-
const primerMarker = path.join(config.repoRoot, '.dotmd', 'primer-shown');
|
|
144
|
-
if (!existsSync(primerMarker)) {
|
|
145
|
-
lines.push(dim('dotmd: managing this repo\'s docs. Use `dotmd new prompt` for handoffs (never hand-write docs/prompts/*.md). `dotmd plans` shows the queue. `dotmd --help` for more.'));
|
|
146
|
-
try {
|
|
147
|
-
mkdirSync(path.dirname(primerMarker), { recursive: true });
|
|
148
|
-
writeFileSync(primerMarker, '');
|
|
149
|
-
} catch { /* best-effort — failed marker write just means the primer reprints next session */ }
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (lines.length === 0) return; // silent when clean
|
|
153
134
|
process.stdout.write(lines.join('\n') + '\n');
|
|
154
135
|
}
|
package/src/lifecycle.mjs
CHANGED
|
@@ -303,11 +303,11 @@ export async function runPickup(argv, config, opts = {}) {
|
|
|
303
303
|
const pickupable = new Set(['active', 'planned', 'in-session']);
|
|
304
304
|
if (oldStatus && !pickupable.has(oldStatus)) {
|
|
305
305
|
die(
|
|
306
|
-
`Cannot
|
|
306
|
+
`Cannot start work on a plan with status '${oldStatus}'. Must be active or planned.\n` +
|
|
307
307
|
` ${repoPath}\n` +
|
|
308
308
|
`\n` +
|
|
309
309
|
`Recover with:\n` +
|
|
310
|
-
` dotmd
|
|
310
|
+
` dotmd set active ${repoPath} && dotmd set in-session ${repoPath}`,
|
|
311
311
|
);
|
|
312
312
|
}
|
|
313
313
|
|
|
@@ -370,7 +370,7 @@ export async function runPickup(argv, config, opts = {}) {
|
|
|
370
370
|
process.stderr.write(`${green('▶ Picked up')}: ${repoPath} (${oldStatus ?? 'unset'} → in-session)\n\n`);
|
|
371
371
|
}
|
|
372
372
|
if (fullBody) {
|
|
373
|
-
const header = `[dotmd] holding ${repoPath} —
|
|
373
|
+
const header = `[dotmd] holding ${repoPath} — close with: dotmd set <status> ${repoPath}\n---\n`;
|
|
374
374
|
process.stdout.write(header);
|
|
375
375
|
const content = (body ?? '').trim();
|
|
376
376
|
if (content) process.stdout.write(content + '\n');
|
|
@@ -665,8 +665,14 @@ export async function runSet(argv, config, opts = {}) {
|
|
|
665
665
|
|
|
666
666
|
if (!newStatus) die('Usage: dotmd set <status> [<path>]');
|
|
667
667
|
|
|
668
|
+
// `set in-session` acquires a lease + prints the plan body (same as the
|
|
669
|
+
// legacy `pickup` verb). Delegated so `set` is the single status verb.
|
|
668
670
|
if (newStatus === 'in-session') {
|
|
669
|
-
|
|
671
|
+
const pickArgs = input ? [input] : [];
|
|
672
|
+
if (argv.includes('--takeover')) pickArgs.push('--takeover');
|
|
673
|
+
if (noIndex) pickArgs.push('--no-index');
|
|
674
|
+
if (showFiles) pickArgs.push('--show-files');
|
|
675
|
+
return runPickup(pickArgs, config, { dryRun });
|
|
670
676
|
}
|
|
671
677
|
|
|
672
678
|
if (!input) {
|
package/src/new.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, fstatSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { toRepoPath, die, warn, nowIso, emitFilesFooter } from './util.mjs';
|
|
@@ -26,7 +26,7 @@ const BUILTIN_TEMPLATES = {
|
|
|
26
26
|
doc: {
|
|
27
27
|
description: 'Reference doc, design note, module overview — build-up shape lite',
|
|
28
28
|
defaultStatus: 'active',
|
|
29
|
-
// Body input optional. When passed (inline / --
|
|
29
|
+
// Body input optional. When passed (inline / --body / @file / stdin),
|
|
30
30
|
// it lands in the Overview section. Without it, Overview is left blank
|
|
31
31
|
// and the user fills it in.
|
|
32
32
|
acceptsBody: true,
|
|
@@ -263,12 +263,18 @@ export async function runNew(argv, config, opts = {}) {
|
|
|
263
263
|
let status = null;
|
|
264
264
|
let title = null;
|
|
265
265
|
let rootName = opts.root ?? null;
|
|
266
|
-
let
|
|
266
|
+
let bodyFlag = null;
|
|
267
|
+
let bodyFlagName = null; // tracks which spelling the caller used, for error attribution
|
|
267
268
|
let showFiles = opts.showFiles ?? false;
|
|
268
269
|
for (let i = 0; i < argv.length; i++) {
|
|
269
270
|
if (argv[i] === '--status' && argv[i + 1]) { status = argv[++i]; continue; }
|
|
270
271
|
if (argv[i] === '--title' && argv[i + 1]) { title = argv[++i]; continue; }
|
|
271
|
-
|
|
272
|
+
// --body is the canonical flag; --message is a back-compat alias.
|
|
273
|
+
if ((argv[i] === '--body' || argv[i] === '--message') && argv[i + 1]) {
|
|
274
|
+
bodyFlagName = argv[i];
|
|
275
|
+
bodyFlag = argv[++i];
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
272
278
|
if (argv[i] === '--root' && argv[i + 1]) { rootName = argv[++i]; continue; }
|
|
273
279
|
if (argv[i] === '--config') { i++; continue; }
|
|
274
280
|
if (argv[i] === '--show-files') { showFiles = true; continue; }
|
|
@@ -304,7 +310,7 @@ export async function runNew(argv, config, opts = {}) {
|
|
|
304
310
|
name = await promptText(`${typeName} name: `);
|
|
305
311
|
if (!name) die('No name provided.');
|
|
306
312
|
} else {
|
|
307
|
-
die(`Usage: dotmd new <type> <name> [body]\n types: ${[...knownTypes].join(', ')}\n body: inline text |
|
|
313
|
+
die(`Usage: dotmd new <type> <name> [body]\n types: ${[...knownTypes].join(', ')}\n body: inline text | piped stdin (auto) | "@path" (file) | --body "..."`);
|
|
308
314
|
}
|
|
309
315
|
}
|
|
310
316
|
|
|
@@ -331,13 +337,31 @@ export async function runNew(argv, config, opts = {}) {
|
|
|
331
337
|
die(`Invalid status \`${status}\` for type \`${typeName}\`\nValid: ${[...effective].join(', ')}`);
|
|
332
338
|
}
|
|
333
339
|
|
|
334
|
-
// Body input resolution:
|
|
340
|
+
// Body input resolution: --body flag > positional bodyArg > auto-piped-stdin > nothing
|
|
335
341
|
let bodyInput = null;
|
|
336
342
|
let bodyInputSource = null;
|
|
337
|
-
if (
|
|
343
|
+
if (bodyFlag !== null) { bodyInput = readBodyInput(bodyFlag); bodyInputSource = bodyFlagName; }
|
|
338
344
|
else if (bodyArg !== null) {
|
|
339
345
|
bodyInput = readBodyInput(bodyArg);
|
|
340
346
|
bodyInputSource = bodyArg === '-' ? 'stdin (`-`)' : (bodyArg.startsWith('@') ? `file (\`${bodyArg}\`)` : 'inline body argument');
|
|
347
|
+
} else if (template.acceptsBody || template.requiresBody) {
|
|
348
|
+
// Auto-consume piped or redirected stdin so agents don't need the `-`
|
|
349
|
+
// placeholder for the most common pattern (`cat draft.md | dotmd new …`,
|
|
350
|
+
// `dotmd new … < draft.md`, or a `<<'EOF'` heredoc). We probe stdin via
|
|
351
|
+
// fstatSync rather than `!isTTY` so a closed/inherited fd doesn't trigger
|
|
352
|
+
// a blocking read of an empty stream. We accept FIFO (shell pipes), regular
|
|
353
|
+
// file (shell redirection / heredoc), and socket (Node spawnSync `input:`
|
|
354
|
+
// delivers stdin as an AF_UNIX socket).
|
|
355
|
+
try {
|
|
356
|
+
const stat = fstatSync(0);
|
|
357
|
+
if (stat.isFIFO() || stat.isFile() || stat.isSocket()) {
|
|
358
|
+
const piped = readFileSync(0, 'utf8');
|
|
359
|
+
if (piped.length > 0) {
|
|
360
|
+
bodyInput = piped;
|
|
361
|
+
bodyInputSource = 'piped stdin';
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
} catch { /* stdin not introspectable — skip auto-consume */ }
|
|
341
365
|
}
|
|
342
366
|
|
|
343
367
|
// If the body input has a leading `---…---` frontmatter block, lift its keys
|
|
@@ -355,7 +379,7 @@ export async function runNew(argv, config, opts = {}) {
|
|
|
355
379
|
}
|
|
356
380
|
|
|
357
381
|
if (template.requiresBody && (!bodyInput || !bodyInput.trim())) {
|
|
358
|
-
die(`\`${typeName}\` template requires a body.
|
|
382
|
+
die(`\`${typeName}\` template requires a body. Pipe stdin (\`cat draft.md | dotmd new ${typeName} <slug>\`), pass @path, --body "...", or inline text.`);
|
|
359
383
|
}
|
|
360
384
|
|
|
361
385
|
// Fail-fast when the user passes body input to a template that doesn't
|
package/src/pickup-card.mjs
CHANGED
|
@@ -194,7 +194,7 @@ export function buildCard(filePath, raw, config) {
|
|
|
194
194
|
// Render the card to human-format string.
|
|
195
195
|
export function renderCard(card) {
|
|
196
196
|
const lines = [];
|
|
197
|
-
lines.push(`[dotmd] holding ${card.path} —
|
|
197
|
+
lines.push(`[dotmd] holding ${card.path} — close with: dotmd set <status> ${card.path}`);
|
|
198
198
|
lines.push('---');
|
|
199
199
|
lines.push(`# ${card.title}`);
|
|
200
200
|
const meta = [card.status, card.updated && `updated ${card.updated}`].filter(Boolean).join(' · ');
|
|
@@ -252,7 +252,7 @@ export function renderCard(card) {
|
|
|
252
252
|
}
|
|
253
253
|
|
|
254
254
|
lines.push('');
|
|
255
|
-
lines.push(dim(`Body: ${formatBytes(card.bodyBytes)}. \`dotmd
|
|
255
|
+
lines.push(dim(`Body: ${formatBytes(card.bodyBytes)}. \`dotmd set in-session ${card.path} --full\` for everything, or Read the file with offset/limit to target a section by line number.`));
|
|
256
256
|
return lines.join('\n') + '\n';
|
|
257
257
|
}
|
|
258
258
|
|
package/src/prompts.mjs
CHANGED
|
@@ -248,7 +248,7 @@ function runPromptsArchive(argv, config, opts = {}) {
|
|
|
248
248
|
|
|
249
249
|
async function runPromptsNew(argv, config, opts = {}) {
|
|
250
250
|
if (!argv[0] || argv[0].startsWith('-')) {
|
|
251
|
-
die('Usage: dotmd prompts new <slug> [body]\n body: inline text |
|
|
251
|
+
die('Usage: dotmd prompts new <slug> [body]\n body: inline text | piped stdin (auto) | "@path" (file) | --body "..."');
|
|
252
252
|
}
|
|
253
253
|
return runNew(['prompt', ...argv], config, opts);
|
|
254
254
|
}
|
package/src/validate.mjs
CHANGED
|
@@ -186,14 +186,14 @@ export function validateDoc(doc, frontmatter, headingTitle, config) {
|
|
|
186
186
|
doc.warnings.push({
|
|
187
187
|
path: doc.path,
|
|
188
188
|
level: 'warning',
|
|
189
|
-
message: `\`status: in-session\` but no active lease found (last session may have crashed without releasing). Run \`dotmd
|
|
189
|
+
message: `\`status: in-session\` but no active lease found (last session may have crashed without releasing). Run \`dotmd set active ${doc.path}\` to clear and re-queue.`,
|
|
190
190
|
});
|
|
191
191
|
} else if (isLeaseStale(lease)) {
|
|
192
192
|
const ageHours = Math.floor((Date.now() - new Date(lease.pickedUpAt).getTime()) / (1000 * 60 * 60));
|
|
193
193
|
doc.warnings.push({
|
|
194
194
|
path: doc.path,
|
|
195
195
|
level: 'warning',
|
|
196
|
-
message: `\`status: in-session\` but lease is stale (last touched ${ageHours}h ago, >${STALE_LEASE_AGE_HOURS}h threshold). Run \`dotmd
|
|
196
|
+
message: `\`status: in-session\` but lease is stale (last touched ${ageHours}h ago, >${STALE_LEASE_AGE_HOURS}h threshold). Run \`dotmd set active ${doc.path}\` to clear and re-queue.`,
|
|
197
197
|
});
|
|
198
198
|
}
|
|
199
199
|
}
|