dotmd-cli 0.28.1 → 0.29.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 +10 -5
- package/package.json +1 -1
- package/src/hud.mjs +17 -3
- package/src/prompts.mjs +41 -6
- package/src/validate.mjs +10 -4
package/bin/dotmd.mjs
CHANGED
|
@@ -571,12 +571,16 @@ Subcommands:
|
|
|
571
571
|
list List pending prompts (default)
|
|
572
572
|
next Consume the oldest pending prompt:
|
|
573
573
|
print body to stdout, flip status to archived
|
|
574
|
-
use <file>
|
|
575
|
-
targets
|
|
576
|
-
archive <file>
|
|
574
|
+
use <file-or-slug> Consume a specific prompt (same as next, but
|
|
575
|
+
targets the named prompt instead of picking oldest)
|
|
576
|
+
archive <file-or-slug> Archive a prompt without printing its body
|
|
577
577
|
new <slug> [body] Create a new prompt (alias for
|
|
578
578
|
\`dotmd new prompt <slug> [body]\`)
|
|
579
579
|
|
|
580
|
+
\`<file-or-slug>\` accepts: an exact path (with or without .md), a bare
|
|
581
|
+
slug matching a prompt basename, or a unique substring of a prompt
|
|
582
|
+
path. Ambiguous substrings error with the candidate list.
|
|
583
|
+
|
|
580
584
|
Default prompt statuses: pending, claimed, archived.
|
|
581
585
|
|
|
582
586
|
Examples:
|
|
@@ -586,10 +590,11 @@ Examples:
|
|
|
586
590
|
dotmd prompts --json # JSON output
|
|
587
591
|
|
|
588
592
|
claude "$(dotmd prompts next)" # consume oldest pending + run claude
|
|
589
|
-
claude "$(dotmd prompts use
|
|
593
|
+
claude "$(dotmd prompts use resume-foo)" # by slug
|
|
594
|
+
claude "$(dotmd prompts use docs/prompts/foo.md)" # by path
|
|
590
595
|
|
|
591
596
|
dotmd prompts next --dry-run # preview without consuming
|
|
592
|
-
dotmd prompts archive
|
|
597
|
+
dotmd prompts archive old-thing
|
|
593
598
|
dotmd prompts new my-prompt "Body text here"`,
|
|
594
599
|
|
|
595
600
|
stale: `dotmd stale — list stale documents
|
package/package.json
CHANGED
package/src/hud.mjs
CHANGED
|
@@ -16,9 +16,23 @@ function previewList(items, max = MAX_PREVIEW) {
|
|
|
16
16
|
return slugs.join(', ') + more;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
// Statuses that count as "actionable" for a prompt are derived from config:
|
|
20
|
+
// types.prompt.context.expanded (the statuses the user wants prominently shown).
|
|
21
|
+
// Falls back to ['pending'] when no prompt type is configured (defensive default
|
|
22
|
+
// for stripped-down configs). This means a user who customizes
|
|
23
|
+
// types.prompt.statuses to add e.g. `urgent: { context: 'expanded' }` gets that
|
|
24
|
+
// status surfaced too, without needing a code change.
|
|
25
|
+
export function actionablePromptStatuses(config) {
|
|
26
|
+
const promptCtx = config.typeContextConfig?.get('prompt');
|
|
27
|
+
const expanded = promptCtx?.expanded;
|
|
28
|
+
if (Array.isArray(expanded) && expanded.length > 0) return new Set(expanded);
|
|
29
|
+
return new Set(['pending']);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function findActionablePrompts(config) {
|
|
20
33
|
const roots = config.docsRoots || (config.docsRoot ? [config.docsRoot] : []);
|
|
21
34
|
const archiveDir = config.archiveDir || 'archived';
|
|
35
|
+
const actionable = actionablePromptStatuses(config);
|
|
22
36
|
const found = [];
|
|
23
37
|
const seen = new Set();
|
|
24
38
|
|
|
@@ -43,7 +57,7 @@ function findPendingPrompts(config) {
|
|
|
43
57
|
if (!frontmatter) continue;
|
|
44
58
|
const fm = parseSimpleFrontmatter(frontmatter);
|
|
45
59
|
if (asString(fm.type) !== 'prompt') continue;
|
|
46
|
-
if (asString(fm.status)
|
|
60
|
+
if (!actionable.has(asString(fm.status))) continue;
|
|
47
61
|
found.push(toRepoPath(filePath, config.repoRoot));
|
|
48
62
|
}
|
|
49
63
|
}
|
|
@@ -57,7 +71,7 @@ export function buildHud(config) {
|
|
|
57
71
|
const owned = Object.values(leases).filter(l => l.session === session).map(l => l.path);
|
|
58
72
|
const queued = listQueuedHandoffs(config).map(h => h.repoPath);
|
|
59
73
|
const stale = findStaleLeases(config).map(l => l.path);
|
|
60
|
-
const prompts =
|
|
74
|
+
const prompts = findActionablePrompts(config);
|
|
61
75
|
|
|
62
76
|
return { owned, queued, stale, prompts };
|
|
63
77
|
}
|
package/src/prompts.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import { extractFrontmatter, parseSimpleFrontmatter } from './frontmatter.mjs';
|
|
3
4
|
import { asString, toRepoPath, die, resolveDocPath } from './util.mjs';
|
|
4
5
|
import { buildIndex } from './index.mjs';
|
|
@@ -74,11 +75,46 @@ function runPromptsNext(argv, config, opts = {}) {
|
|
|
74
75
|
consumePrompt(head.abs, config, opts);
|
|
75
76
|
}
|
|
76
77
|
|
|
78
|
+
// Resolve user input to a prompt path. Tries (in order): exact path,
|
|
79
|
+
// path + '.md', exact basename match across type: prompt docs, substring
|
|
80
|
+
// match across type: prompt docs. Returns the absolute path or dies with a
|
|
81
|
+
// helpful message (no match / ambiguous match).
|
|
82
|
+
function resolvePromptInput(input, config) {
|
|
83
|
+
const direct = resolveDocPath(input, config);
|
|
84
|
+
if (direct) return direct;
|
|
85
|
+
|
|
86
|
+
if (!input.endsWith('.md')) {
|
|
87
|
+
const withExt = resolveDocPath(input + '.md', config);
|
|
88
|
+
if (withExt) return withExt;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const index = buildIndex(config);
|
|
92
|
+
const prompts = index.docs.filter(d => d.type === 'prompt');
|
|
93
|
+
if (prompts.length === 0) die(`No prompts in the index.`);
|
|
94
|
+
|
|
95
|
+
const slug = input.replace(/\.md$/, '');
|
|
96
|
+
|
|
97
|
+
const byBasename = prompts.filter(d => path.basename(d.path, '.md') === slug);
|
|
98
|
+
if (byBasename.length === 1) return path.resolve(config.repoRoot, byBasename[0].path);
|
|
99
|
+
if (byBasename.length > 1) {
|
|
100
|
+
die(`Multiple prompts match "${input}" by basename:\n${byBasename.map(d => ' ' + d.path).join('\n')}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const bySubstring = prompts.filter(d =>
|
|
104
|
+
d.path.includes(slug) || path.basename(d.path).includes(slug),
|
|
105
|
+
);
|
|
106
|
+
if (bySubstring.length === 1) return path.resolve(config.repoRoot, bySubstring[0].path);
|
|
107
|
+
if (bySubstring.length > 1) {
|
|
108
|
+
die(`Multiple prompts match "${input}":\n${bySubstring.map(d => ' ' + d.path).join('\n')}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
die(`No prompt found matching: ${input}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
77
114
|
function runPromptsUse(argv, config, opts = {}) {
|
|
78
115
|
const input = argv.find(a => !a.startsWith('-'));
|
|
79
|
-
if (!input) die('Usage: dotmd prompts use <file>');
|
|
80
|
-
const filePath =
|
|
81
|
-
if (!filePath) die(`File not found: ${input}`);
|
|
116
|
+
if (!input) die('Usage: dotmd prompts use <file-or-slug>');
|
|
117
|
+
const filePath = resolvePromptInput(input, config);
|
|
82
118
|
consumePrompt(filePath, config, opts);
|
|
83
119
|
}
|
|
84
120
|
|
|
@@ -113,9 +149,8 @@ function consumePrompt(filePath, config, opts) {
|
|
|
113
149
|
|
|
114
150
|
function runPromptsArchive(argv, config, opts = {}) {
|
|
115
151
|
const input = argv.find(a => !a.startsWith('-'));
|
|
116
|
-
if (!input) die('Usage: dotmd prompts archive <file>');
|
|
117
|
-
const filePath =
|
|
118
|
-
if (!filePath) die(`File not found: ${input}`);
|
|
152
|
+
if (!input) die('Usage: dotmd prompts archive <file-or-slug>');
|
|
153
|
+
const filePath = resolvePromptInput(input, config);
|
|
119
154
|
|
|
120
155
|
const raw = readFileSync(filePath, 'utf8');
|
|
121
156
|
const { frontmatter } = extractFrontmatter(raw);
|
package/src/validate.mjs
CHANGED
|
@@ -6,10 +6,13 @@ import { toRepoPath } from './util.mjs';
|
|
|
6
6
|
const NOW = new Date();
|
|
7
7
|
|
|
8
8
|
function isValidStatus(status, root, config, type) {
|
|
9
|
-
//
|
|
9
|
+
// When a doc declares a known type, that type's status set is authoritative.
|
|
10
|
+
// Falling through to the global union (across all types) would allow a
|
|
11
|
+
// `type: prompt` doc to carry `status: active` just because `active` is valid
|
|
12
|
+
// for plans — defeating the purpose of type-scoped vocabularies.
|
|
10
13
|
if (type) {
|
|
11
14
|
const typeSet = config.typeStatuses?.get(type);
|
|
12
|
-
if (typeSet
|
|
15
|
+
if (typeSet) return typeSet.has(status);
|
|
13
16
|
}
|
|
14
17
|
const rootSet = config.rootValidStatuses?.get(root);
|
|
15
18
|
if (rootSet) return rootSet.has(status);
|
|
@@ -27,8 +30,11 @@ export function validateDoc(doc, frontmatter, headingTitle, config) {
|
|
|
27
30
|
} else if (!isValidStatus(doc.status, doc.root, config, doc.type)) {
|
|
28
31
|
const typeSet = doc.type && config.typeStatuses?.get(doc.type);
|
|
29
32
|
const rootSet = config.rootValidStatuses?.get(doc.root);
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
// When the doc has a known type, scope the error hint to that type's vocab.
|
|
34
|
+
// Otherwise fall back to root-specific or global validStatuses.
|
|
35
|
+
const hint = typeSet
|
|
36
|
+
? `valid for type \`${doc.type}\`: ${[...typeSet].join(', ')}`
|
|
37
|
+
: `valid: ${[...(rootSet ?? config.validStatuses)].join(', ')}`;
|
|
32
38
|
doc.errors.push({ path: doc.path, level: 'error', message: `Unknown status \`${doc.status}\`; ${hint}.` });
|
|
33
39
|
}
|
|
34
40
|
|