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 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> Consume a specific prompt (same as next, but
575
- targets <file> instead of picking oldest)
576
- archive <file> Archive a prompt without printing its body
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 docs/prompts/foo.md)"
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 docs/prompts/old.md
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotmd-cli",
3
- "version": "0.28.1",
3
+ "version": "0.29.0",
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",
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
- function findPendingPrompts(config) {
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) !== 'pending') continue;
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 = findPendingPrompts(config);
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 = resolveDocPath(input, config);
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 = resolveDocPath(input, config);
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
- // Union type-specific + root-specific statuses (a doc can satisfy either)
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 && typeSet.has(status)) return true;
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
- const combined = new Set([...(typeSet ?? []), ...(rootSet ?? config.validStatuses)]);
31
- const hint = `valid: ${[...combined].join(', ')}`;
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