dotmd-cli 0.77.1 → 0.77.3
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 +3 -1
- package/bin/dotmd.mjs +29 -23
- package/package.json +1 -1
- package/src/config.mjs +15 -3
- package/src/host-integration.mjs +21 -0
- package/src/install.mjs +22 -5
- package/src/prompts.mjs +14 -19
- package/src/update.mjs +81 -7
- package/src/util.mjs +9 -0
package/README.md
CHANGED
|
@@ -40,6 +40,8 @@ dotmd doctor --session # what identity dotmd sees here, and from where
|
|
|
40
40
|
```
|
|
41
41
|
|
|
42
42
|
Both are one-time and global; `dotmd update` keeps them in step with the CLI.
|
|
43
|
+
Codex needs no install for identity: it exports `CODEX_THREAD_ID` to every tool
|
|
44
|
+
shell, and dotmd reads it as a per-session identity automatically.
|
|
43
45
|
|
|
44
46
|
### Claude Code Plugin
|
|
45
47
|
|
|
@@ -163,7 +165,7 @@ readable for compatibility and can be migrated with `dotmd lint --fix`.
|
|
|
163
165
|
|---|---|---|
|
|
164
166
|
| `plan` | Executable work | `in-session`, `active`, `planned`, `blocked`, `partial`, `paused`, `awaiting`, `queued-after`, `archived` |
|
|
165
167
|
| `doc` | Specs, ADRs, audits, and reference material | `draft`, `active`, `review`, `reference`, `deprecated`, `archived` |
|
|
166
|
-
| `prompt` | Saved future-session instructions | `pending`, `
|
|
168
|
+
| `prompt` | Saved future-session instructions | `pending`, `archived` |
|
|
167
169
|
|
|
168
170
|
Status definitions can be customized per type. Rich status objects co-locate
|
|
169
171
|
display, staleness, validation, terminal, and archive behavior in one place.
|
package/bin/dotmd.mjs
CHANGED
|
@@ -103,7 +103,7 @@ const HELP = {
|
|
|
103
103
|
|
|
104
104
|
Common commands:
|
|
105
105
|
plans Live plans (excludes archived)
|
|
106
|
-
prompts Prompt queue/admin (list, next, archive, new
|
|
106
|
+
prompts Prompt queue/admin (list, next, archive, new)
|
|
107
107
|
briefing Full briefing with plan counts + next steps
|
|
108
108
|
agent-context Compact bounded JSON context for agents
|
|
109
109
|
set <status> [file] Transition status (start work, finish, archive — all via target status)
|
|
@@ -160,6 +160,12 @@ host gets that a different way:
|
|
|
160
160
|
with "plugin not installed"), and the README's slash commands only
|
|
161
161
|
work from inside a session. Without the \`claude\` CLI on PATH the
|
|
162
162
|
two in-session commands are printed instead.
|
|
163
|
+
It also repairs a plugin Claude lists as "failed to load: Marketplace
|
|
164
|
+
dotmd not found" — an install record whose marketplace registration
|
|
165
|
+
is gone — by re-adding the marketplace and updating the plugin. If
|
|
166
|
+
Claude refuses the marketplace, ~/.claude/settings.json declares it
|
|
167
|
+
under extraKnownMarketplaces with a source that no longer matches;
|
|
168
|
+
dotmd names the field but never edits that file.
|
|
163
169
|
|
|
164
170
|
opencode Writes one auto-discovered plugin file. OpenCode has no plugin
|
|
165
171
|
registry but globs \`{plugin,plugins}/*.{ts,js}\` under its global
|
|
@@ -190,7 +196,11 @@ reports a missing integration but never installs one.`,
|
|
|
190
196
|
The plugin and CLI ship in lockstep; a release bumps both. Updating the plugin
|
|
191
197
|
requires a session restart (or /reload-plugins) to apply. The plugin step needs
|
|
192
198
|
the \`claude\` CLI on PATH — otherwise it prints the \`/plugin update\` command to
|
|
193
|
-
run from a session instead
|
|
199
|
+
run from a session instead. The OpenCode file is refreshed in the same run when
|
|
200
|
+
it is present and behind. The hosts are independent, so a failing step does not
|
|
201
|
+
stop the others: every step runs, the failures are listed together, and the
|
|
202
|
+
exit code is 1. A plugin whose marketplace registration is gone gets the
|
|
203
|
+
marketplace re-added before the update (see \`dotmd install claude\`).`,
|
|
194
204
|
|
|
195
205
|
misuse: `dotmd misuse — read the cross-repo guard log (~/.claude/logs/dotmd-misuse.log)
|
|
196
206
|
|
|
@@ -219,7 +229,7 @@ View & Query:
|
|
|
219
229
|
plans Live plans (excludes archived; --include-archived for all)
|
|
220
230
|
use [<file-or-slug>] Open a doc by type: prompt → consume, plan → start, doc → read
|
|
221
231
|
baton [<plan>|<slug>] <@<file>|-> Save a resume prompt; releases the plan + prints the commit when one is in-session
|
|
222
|
-
prompts [list|show|archive|new
|
|
232
|
+
prompts [list|show|archive|new] Prompt admin (list / peek / archive / save). Use \`dotmd use\` to consume.
|
|
223
233
|
stale Stale docs (preset)
|
|
224
234
|
actionable Docs with next steps (preset)
|
|
225
235
|
|
|
@@ -359,21 +369,21 @@ doc statuses
|
|
|
359
369
|
────────────────────────────────────────────────────────────────────
|
|
360
370
|
prompt statuses
|
|
361
371
|
|
|
372
|
+
A prompt has two states and no third.
|
|
373
|
+
|
|
362
374
|
pending Ready for the next session to consume.
|
|
363
375
|
\`dotmd prompts use <file>\` prints body + archives atomically.
|
|
364
376
|
\`dotmd prompts next\` does the same for the oldest pending.
|
|
365
377
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
Still listed by \`dotmd prompts list\`.
|
|
369
|
-
\`dotmd prompts unhold <file>\` → pending.
|
|
370
|
-
|
|
371
|
-
shelved Legacy spelling for held prompts. \`dotmd prompts shelve\`
|
|
372
|
-
now writes \`status: held\`.
|
|
373
|
-
|
|
374
|
-
claimed Legacy intermediate state (atomic use → archived now).
|
|
378
|
+
archived Consumed prompt; body preserved in the archive directory,
|
|
379
|
+
which is the only directory a prompt ever moves into.
|
|
375
380
|
|
|
376
|
-
|
|
381
|
+
\`held\`, \`shelved\` and \`claimed\` were removed 2026-08-30, along with
|
|
382
|
+
\`prompts hold\` / \`unhold\` / \`shelve\` / \`unshelve\` and the
|
|
383
|
+
prompts/held/ bucket. A prompt directs work, so parking one instead of
|
|
384
|
+
archiving it left work state outside the plan that owns it, fighting with
|
|
385
|
+
that plan's own status. If a prompt should not be consumed, lift its content
|
|
386
|
+
into the plan and archive the prompt.
|
|
377
387
|
|
|
378
388
|
────────────────────────────────────────────────────────────────────
|
|
379
389
|
Related commands:
|
|
@@ -504,9 +514,10 @@ another session's work.
|
|
|
504
514
|
a unique bare slug / basename across the doc roots (\`set paused auth-revamp\`).
|
|
505
515
|
Ambiguous slugs error with the candidate list instead of guessing.
|
|
506
516
|
When the path is omitted, exactly one plan must be owned by this session.
|
|
507
|
-
Claude Code session IDs are recognized automatically
|
|
508
|
-
|
|
509
|
-
|
|
517
|
+
Claude Code and Codex session IDs are recognized automatically (Codex exports
|
|
518
|
+
CODEX_THREAD_ID to every tool shell), as is OpenCode (via OPENCODE_PID — per
|
|
519
|
+
OpenCode process, not per session). Other hosts must set RUNLIST_SESSION_ID;
|
|
520
|
+
anonymous ownership mutations fail closed.
|
|
510
521
|
Pickup hooks use at-least-once delivery with a stable operationId; hook side
|
|
511
522
|
effects must deduplicate that ID.
|
|
512
523
|
|
|
@@ -995,7 +1006,7 @@ Other options:
|
|
|
995
1006
|
|
|
996
1007
|
For plans, the default status vocabulary is: in-session, active, planned,
|
|
997
1008
|
blocked, partial, paused, awaiting, queued-after, archived.
|
|
998
|
-
For prompts: pending (default),
|
|
1009
|
+
For prompts: pending (default), archived.
|
|
999
1010
|
|
|
1000
1011
|
Use --dry-run (-n) to preview without creating the file.`,
|
|
1001
1012
|
|
|
@@ -1132,11 +1143,6 @@ Subcommands:
|
|
|
1132
1143
|
show <file-or-slug> Read-only peek: print the body WITHOUT consuming
|
|
1133
1144
|
(triage). \`peek\` is an alias.
|
|
1134
1145
|
archive <file-or-slug> Archive a prompt without printing its body
|
|
1135
|
-
hold <file-or-slug> Park a prompt (status → held) under prompts/held/:
|
|
1136
|
-
kept in list, hidden from hud/briefing pending
|
|
1137
|
-
surfaces, skipped by \`prompts next\`.
|
|
1138
|
-
unhold <file-or-slug> Move a held prompt back to pending.
|
|
1139
|
-
shelve / unshelve Legacy aliases for hold / unhold.
|
|
1140
1146
|
new <slug> [body] Create a new prompt (alias for
|
|
1141
1147
|
\`dotmd new prompt <slug> [body]\`)
|
|
1142
1148
|
|
|
@@ -1144,7 +1150,7 @@ Subcommands:
|
|
|
1144
1150
|
slug matching a prompt basename, or a unique substring of a prompt
|
|
1145
1151
|
path. Ambiguous substrings error with the candidate list.
|
|
1146
1152
|
|
|
1147
|
-
Default prompt statuses: pending,
|
|
1153
|
+
Default prompt statuses: pending, archived.
|
|
1148
1154
|
|
|
1149
1155
|
Examples:
|
|
1150
1156
|
dotmd prompts # pending prompts (default)
|
package/package.json
CHANGED
package/src/config.mjs
CHANGED
|
@@ -37,9 +37,17 @@ const DEFAULTS = {
|
|
|
37
37
|
context: { expanded: ['active'], listed: ['draft', 'review'], counted: ['reference', 'deprecated', 'archived'] },
|
|
38
38
|
staleDays: { draft: 30, active: 14, review: 14 },
|
|
39
39
|
},
|
|
40
|
+
// A prompt has two states and no third. It is pending — the next session
|
|
41
|
+
// should consume it — or it is archived, consumed, body kept under the
|
|
42
|
+
// archive directory. `held`, `shelved` and `claimed` were removed 2026-08-30:
|
|
43
|
+
// a prompt directs work, so parking one instead of archiving it leaves work
|
|
44
|
+
// state sitting outside the plan that owns it, where it fights with the
|
|
45
|
+
// plan's own status and neither one is the truth. If a prompt should not be
|
|
46
|
+
// consumed, its content belongs in the plan and the prompt should be
|
|
47
|
+
// archived. See lifecycle.filedStatuses — prompts file nowhere.
|
|
40
48
|
prompt: {
|
|
41
|
-
statuses: ['pending', '
|
|
42
|
-
context: { expanded: ['pending'],
|
|
49
|
+
statuses: ['pending', 'archived'],
|
|
50
|
+
context: { expanded: ['pending'], counted: ['archived'] },
|
|
43
51
|
staleDays: { pending: 30 },
|
|
44
52
|
},
|
|
45
53
|
},
|
|
@@ -64,7 +72,11 @@ const DEFAULTS = {
|
|
|
64
72
|
// F15: per-status filing buckets (status → dirName). Built-in held/paused
|
|
65
73
|
// statuses file under the owning type folder; archive remains a separate
|
|
66
74
|
// primitive untouched.
|
|
67
|
-
|
|
75
|
+
// Plans only. A paused plan files under <plansDir>/held/ so the live plan
|
|
76
|
+
// list stays readable. Prompts were removed from this map 2026-08-30 along
|
|
77
|
+
// with their held/shelved statuses — `archived` is the only directory a
|
|
78
|
+
// prompt ever moves into.
|
|
79
|
+
filedStatuses: { paused: 'held' },
|
|
68
80
|
// Types whose archive nests under their own type dir (<typeDir>/<archiveDir>,
|
|
69
81
|
// e.g. docs/prompts/archived/) instead of the shared <root>/<archiveDir>.
|
|
70
82
|
// Prompts are session-local churn — keeping their archive out of the shared
|
package/src/host-integration.mjs
CHANGED
|
@@ -23,6 +23,7 @@ import path from 'node:path';
|
|
|
23
23
|
import { fileURLToPath } from 'node:url';
|
|
24
24
|
import { hostSessionSource } from './util.mjs';
|
|
25
25
|
import { readEnv, stateDir } from './naming.mjs';
|
|
26
|
+
import { planMarketplaceRepair } from './update.mjs';
|
|
26
27
|
|
|
27
28
|
export const GENERATED_MARKER = 'dotmd-generated:';
|
|
28
29
|
const PLUGIN_FILENAME = 'dotmd.js';
|
|
@@ -233,6 +234,19 @@ export function degradedIdentityNotice(repoRoot, opts = {}) {
|
|
|
233
234
|
export const CLAUDE_MARKETPLACE = 'reowens/dotmd';
|
|
234
235
|
export const CLAUDE_PLUGIN_ID = 'dotmd@dotmd';
|
|
235
236
|
|
|
237
|
+
// Why `claude plugin marketplace add` can refuse a marketplace that is not
|
|
238
|
+
// even registered: settings.json may still DECLARE it (extraKnownMarketplaces),
|
|
239
|
+
// and Claude rejects an add whose source differs from that declaration in any
|
|
240
|
+
// fetch-shaping field. dotmd never writes that declaration and will not edit
|
|
241
|
+
// it, so the most it can do is name the field.
|
|
242
|
+
export function claudeMarketplaceRefusalHint(marketplace = 'dotmd') {
|
|
243
|
+
return [
|
|
244
|
+
`Claude refused to register marketplace "${marketplace}". If ~/.claude/settings.json declares it under`,
|
|
245
|
+
`extraKnownMarketplaces, its source must be exactly {"source":"github","repo":"${CLAUDE_MARKETPLACE}"} —`,
|
|
246
|
+
'a stray "path", "ref" or header there makes the add fail and the installed plugin fail to load.',
|
|
247
|
+
];
|
|
248
|
+
}
|
|
249
|
+
|
|
236
250
|
// Pure planner, mirroring planUpdate: the orchestration is unit-testable and
|
|
237
251
|
// the side effects stay in the caller.
|
|
238
252
|
export function planClaudeInstall({ installed, hasClaude, remove = false } = {}) {
|
|
@@ -242,6 +256,13 @@ export function planClaudeInstall({ installed, hasClaude, remove = false } = {})
|
|
|
242
256
|
? [{ kind: 'run', cmd: ['claude', 'plugin', 'uninstall', installed.id] }]
|
|
243
257
|
: [{ kind: 'manual', lines: [`/plugin uninstall ${installed.id}`] }];
|
|
244
258
|
}
|
|
259
|
+
// An install record whose marketplace registration is gone is not an
|
|
260
|
+
// installed plugin — Claude lists it as "failed to load". Skipping here with
|
|
261
|
+
// "already installed" left the user with no dotmd verb that could repair it.
|
|
262
|
+
if (installed?.marketplaceRegistered === false) {
|
|
263
|
+
return planMarketplaceRepair(installed, { hasClaude, verb: 'update' })
|
|
264
|
+
.map(step => (step.kind === 'marketplace' || step.kind === 'plugin' ? { ...step, kind: 'run', step: step.kind } : step));
|
|
265
|
+
}
|
|
245
266
|
if (installed) return [{ kind: 'skip', reason: `dotmd plugin already installed (${installed.version ?? 'unknown version'})` }];
|
|
246
267
|
// The marketplace has to be registered before the plugin resolves; adding one
|
|
247
268
|
// already present is a no-op, so this stays safe to re-run.
|
package/src/install.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { bold, dim, green, yellow } from './color.mjs';
|
|
6
6
|
import {
|
|
7
|
-
CLAUDE_MARKETPLACE, CLAUDE_PLUGIN_ID, installOpencodePlugin, opencodeDetected,
|
|
7
|
+
CLAUDE_MARKETPLACE, CLAUDE_PLUGIN_ID, claudeMarketplaceRefusalHint, installOpencodePlugin, opencodeDetected,
|
|
8
8
|
opencodeStatus, planClaudeInstall, removeOpencodePlugin,
|
|
9
9
|
} from './host-integration.mjs';
|
|
10
10
|
import { readInstalledPlugin } from './update.mjs';
|
|
@@ -24,7 +24,14 @@ function flagValue(argv, name) {
|
|
|
24
24
|
function hostStates() {
|
|
25
25
|
const plugin = readInstalledPlugin();
|
|
26
26
|
return {
|
|
27
|
-
claude: {
|
|
27
|
+
claude: {
|
|
28
|
+
installed: Boolean(plugin),
|
|
29
|
+
version: plugin?.version ?? null,
|
|
30
|
+
id: plugin?.id ?? CLAUDE_PLUGIN_ID,
|
|
31
|
+
marketplace: plugin?.marketplace ?? null,
|
|
32
|
+
// false = record present, marketplace gone: Claude shows "failed to load".
|
|
33
|
+
marketplaceRegistered: plugin ? plugin.marketplaceRegistered : null,
|
|
34
|
+
},
|
|
28
35
|
opencode: { ...opencodeStatus({ version: pkg.version }), detected: opencodeDetected() },
|
|
29
36
|
};
|
|
30
37
|
}
|
|
@@ -38,7 +45,11 @@ function reportStatus(json) {
|
|
|
38
45
|
const { claude, opencode } = states;
|
|
39
46
|
process.stdout.write(`${bold('dotmd host integrations')} ${dim(`CLI ${pkg.version}`)}\n\n`);
|
|
40
47
|
|
|
41
|
-
|
|
48
|
+
const claudeBroken = claude.installed && claude.marketplaceRegistered === false;
|
|
49
|
+
const claudeState = !claude.installed ? yellow('not installed')
|
|
50
|
+
: claudeBroken ? yellow(`${claude.version ?? 'installed'} — marketplace "${claude.marketplace}" not registered, plugin fails to load`)
|
|
51
|
+
: green(claude.version ?? 'installed');
|
|
52
|
+
process.stdout.write(` claude ${claudeState}\n`);
|
|
42
53
|
process.stdout.write(` ${dim(claude.installed ? claude.id : 'plugin: SessionStart primer, PreToolUse guard, workflow skill')}\n`);
|
|
43
54
|
|
|
44
55
|
const ocState = opencode.foreign ? yellow('unmanaged file present')
|
|
@@ -49,7 +60,7 @@ function reportStatus(json) {
|
|
|
49
60
|
process.stdout.write(` ${dim(opencode.path)}\n`);
|
|
50
61
|
|
|
51
62
|
const todo = [];
|
|
52
|
-
if (!claude.installed) todo.push('dotmd install claude');
|
|
63
|
+
if (!claude.installed || claudeBroken) todo.push('dotmd install claude');
|
|
53
64
|
if (!opencode.exists || opencode.stale) todo.push('dotmd install opencode');
|
|
54
65
|
if (todo.length) {
|
|
55
66
|
process.stdout.write('\n');
|
|
@@ -73,10 +84,11 @@ function installClaude(argv, dryRun, json) {
|
|
|
73
84
|
for (const step of steps) {
|
|
74
85
|
if (step.kind === 'skip') { process.stdout.write(`${dim('skip:')} ${step.reason}\n`); continue; }
|
|
75
86
|
if (step.kind === 'manual') {
|
|
76
|
-
process.stdout.write(`${yellow('claude CLI not on PATH')} — run these from a Claude Code session:\n`);
|
|
87
|
+
process.stdout.write(`${yellow(step.reason ?? 'claude CLI not on PATH')} — run these from a Claude Code session:\n`);
|
|
77
88
|
for (const line of step.lines) process.stdout.write(` ${bold(line)}\n`);
|
|
78
89
|
continue;
|
|
79
90
|
}
|
|
91
|
+
if (step.reason) process.stdout.write(`${step.reason}\n`);
|
|
80
92
|
if (dryRun) { process.stdout.write(dim(`[dry-run] Would run: ${step.cmd.join(' ')}\n`)); continue; }
|
|
81
93
|
process.stdout.write(dim(`$ ${step.cmd.join(' ')}\n`));
|
|
82
94
|
const result = spawnSync(executableName(step.cmd[0]), step.cmd.slice(1), {
|
|
@@ -85,6 +97,11 @@ function installClaude(argv, dryRun, json) {
|
|
|
85
97
|
ran = true;
|
|
86
98
|
if (result.status !== 0) {
|
|
87
99
|
process.stdout.write(yellow(`(claude exited ${result.status ?? '?'})\n`));
|
|
100
|
+
// Each later step here depends on this one (the plugin resolves through
|
|
101
|
+
// the marketplace), so unlike `update` there is nothing independent left.
|
|
102
|
+
if (step.step === 'marketplace' || step.cmd.includes('marketplace')) {
|
|
103
|
+
for (const line of claudeMarketplaceRefusalHint(readInstalledPlugin()?.marketplace)) process.stdout.write(yellow(`${line}\n`));
|
|
104
|
+
}
|
|
88
105
|
process.exitCode = 1;
|
|
89
106
|
return;
|
|
90
107
|
}
|
package/src/prompts.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { extractFrontmatter, parseSimpleFrontmatter } from './frontmatter.mjs';
|
|
|
4
4
|
import { asString, toRepoPath, die, resolveDocPath, resolveRefPath, isArchivedPath } from './util.mjs';
|
|
5
5
|
import { buildIndex, resolveDocArg } from './index.mjs';
|
|
6
6
|
import { runQuery } from './query.mjs';
|
|
7
|
-
import { completePlanClaim, regenIndex, renderLifecycleMutation, runArchive
|
|
7
|
+
import { completePlanClaim, regenIndex, renderLifecycleMutation, runArchive } from './lifecycle.mjs';
|
|
8
8
|
import { runNew } from './new.mjs';
|
|
9
9
|
import { green, dim, yellow } from './color.mjs';
|
|
10
10
|
import { authorizeManagedSource } from './managed-path.mjs';
|
|
@@ -21,11 +21,23 @@ import { LEGACY_STATE_DIR, STATE_DIR } from './naming.mjs';
|
|
|
21
21
|
// `resume` is an alias for `use` — agents reach for "resume" when continuing a
|
|
22
22
|
// session; `use` reads as internal mechanics. Both names stay valid; the
|
|
23
23
|
// canonical output ("Consumed: …") is unchanged.
|
|
24
|
-
const SUBCOMMANDS = new Set(['list', 'next', 'use', 'resume', 'show', 'peek', 'archive', 'new'
|
|
24
|
+
const SUBCOMMANDS = new Set(['list', 'next', 'use', 'resume', 'show', 'peek', 'archive', 'new']);
|
|
25
|
+
|
|
26
|
+
// Removed 2026-08-30 with the `held`/`shelved` statuses. Named rather than left
|
|
27
|
+
// to fall through to the list filter below, which would silently answer a
|
|
28
|
+
// removed command with an empty list — the caller needs to be told the state is
|
|
29
|
+
// gone and what replaced it.
|
|
30
|
+
const REMOVED_SUBCOMMANDS = new Set(['hold', 'unhold', 'shelve', 'unshelve']);
|
|
25
31
|
|
|
26
32
|
export async function runPrompts(argv, config, opts = {}) {
|
|
27
33
|
const sub = argv[0];
|
|
28
34
|
|
|
35
|
+
if (sub && REMOVED_SUBCOMMANDS.has(sub)) {
|
|
36
|
+
die(`\`dotmd prompts ${sub}\` was removed — a prompt is pending or archived, and nothing else.\n`
|
|
37
|
+
+ 'A prompt directs work, so parking one instead of archiving it leaves work state outside the\n'
|
|
38
|
+
+ 'plan that owns it. Lift its content into that plan, then `dotmd prompts archive <file>`.');
|
|
39
|
+
}
|
|
40
|
+
|
|
29
41
|
if (!sub || !SUBCOMMANDS.has(sub)) {
|
|
30
42
|
return runPromptsList(argv, config, opts);
|
|
31
43
|
}
|
|
@@ -40,10 +52,6 @@ export async function runPrompts(argv, config, opts = {}) {
|
|
|
40
52
|
case 'peek': return runPromptsShow(rest, config);
|
|
41
53
|
case 'archive': return runPromptsArchive(rest, config, opts);
|
|
42
54
|
case 'new': return runPromptsNew(rest, config, opts);
|
|
43
|
-
case 'hold': return runPromptsHold(rest, config, opts);
|
|
44
|
-
case 'unhold': return runPromptsUnhold(rest, config, opts);
|
|
45
|
-
case 'shelve': return runPromptsHold(rest, config, opts);
|
|
46
|
-
case 'unshelve': return runPromptsUnhold(rest, config, opts);
|
|
47
55
|
}
|
|
48
56
|
}
|
|
49
57
|
|
|
@@ -562,16 +570,3 @@ async function runPromptsNew(argv, config, opts = {}) {
|
|
|
562
570
|
return runNew(['prompt', ...argv], config, opts);
|
|
563
571
|
}
|
|
564
572
|
|
|
565
|
-
async function runPromptsHold(argv, config, opts = {}) {
|
|
566
|
-
const input = argv.find(a => !a.startsWith('-'));
|
|
567
|
-
if (!input) die('Usage: dotmd prompts hold <file-or-slug>');
|
|
568
|
-
const filePath = resolvePromptInput(input, config);
|
|
569
|
-
return runStatus([filePath, 'held'], config, opts);
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
async function runPromptsUnhold(argv, config, opts = {}) {
|
|
573
|
-
const input = argv.find(a => !a.startsWith('-'));
|
|
574
|
-
if (!input) die('Usage: dotmd prompts unhold <file-or-slug>');
|
|
575
|
-
const filePath = resolvePromptInput(input, config);
|
|
576
|
-
return runStatus([filePath, 'pending'], config, opts);
|
|
577
|
-
}
|
package/src/update.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import os from 'node:os';
|
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { green, dim, yellow } from './color.mjs';
|
|
7
7
|
import { executableName, which } from './util.mjs';
|
|
8
|
-
import { installOpencodePlugin, opencodeStatus } from './host-integration.mjs';
|
|
8
|
+
import { CLAUDE_MARKETPLACE, claudeMarketplaceRefusalHint, installOpencodePlugin, opencodeStatus } from './host-integration.mjs';
|
|
9
9
|
|
|
10
10
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
11
|
const pkg = JSON.parse(readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
|
|
@@ -50,10 +50,54 @@ export function readInstalledPluginRecords(opts = {}) {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
// Claude Code's marketplace registry, keyed by marketplace name. A plugin id
|
|
54
|
+
// is `<plugin>@<marketplace>`, and the plugin only loads while its marketplace
|
|
55
|
+
// is registered here — an install record alone is not an installed plugin.
|
|
56
|
+
export function readKnownMarketplaces(opts = {}) {
|
|
57
|
+
const home = opts.home || os.homedir();
|
|
58
|
+
const file = path.join(home, '.claude', 'plugins', 'known_marketplaces.json');
|
|
59
|
+
try {
|
|
60
|
+
const parsed = JSON.parse(readFileSync(file, 'utf8'));
|
|
61
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
62
|
+
} catch {
|
|
63
|
+
return {};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// `marketplaceRegistered: false` is the state `claude plugin list` shows as
|
|
68
|
+
// "failed to load: Marketplace dotmd not found": the install record survived,
|
|
69
|
+
// the marketplace registration did not (a settings.json declaration that no
|
|
70
|
+
// longer matches, a wiped registry). Reading only the install record called
|
|
71
|
+
// that "installed", so `dotmd install claude` skipped the one repair it owns
|
|
72
|
+
// and `dotmd update` ran a `plugin update` that could only fail.
|
|
53
73
|
export function readInstalledPlugin(opts = {}) {
|
|
54
74
|
const records = readInstalledPluginRecords(opts);
|
|
55
75
|
if (!records) return null;
|
|
56
|
-
|
|
76
|
+
const marketplace = records.id.split('@')[1] || null;
|
|
77
|
+
const known = readKnownMarketplaces(opts);
|
|
78
|
+
return {
|
|
79
|
+
id: records.id,
|
|
80
|
+
version: records.entries[0]?.version ?? null,
|
|
81
|
+
marketplace,
|
|
82
|
+
marketplaceRegistered: Boolean(marketplace && known[marketplace]),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// The steps that put a plugin whose marketplace registration is gone back on
|
|
87
|
+
// its feet. Only the marketplace dotmd publishes has a source dotmd knows; a
|
|
88
|
+
// plugin installed from some other marketplace names a source we cannot guess.
|
|
89
|
+
export function planMarketplaceRepair(plugin, { hasClaude, verb }) {
|
|
90
|
+
const reason = `marketplace "${plugin.marketplace}" is not registered, so the installed plugin cannot load`;
|
|
91
|
+
if (plugin.marketplace !== 'dotmd') {
|
|
92
|
+
return [{ kind: 'skip', reason: `${reason} — re-add that marketplace (dotmd does not know its source), then rerun` }];
|
|
93
|
+
}
|
|
94
|
+
if (!hasClaude) {
|
|
95
|
+
return [{ kind: 'manual', reason, lines: [`/plugin marketplace add ${CLAUDE_MARKETPLACE}`, `/plugin ${verb} ${plugin.id}`] }];
|
|
96
|
+
}
|
|
97
|
+
return [
|
|
98
|
+
{ kind: 'marketplace', reason: `${reason} — re-adding it first`, cmd: ['claude', 'plugin', 'marketplace', 'add', CLAUDE_MARKETPLACE] },
|
|
99
|
+
{ kind: 'plugin', needs: 'marketplace', cmd: ['claude', 'plugin', verb, plugin.id] },
|
|
100
|
+
];
|
|
57
101
|
}
|
|
58
102
|
|
|
59
103
|
// Decide which steps `dotmd update` should run. Pure — no side effects — so the
|
|
@@ -69,6 +113,8 @@ export function planUpdate(opts, ctx) {
|
|
|
69
113
|
if (!opts.cliOnly) {
|
|
70
114
|
if (!ctx.plugin) {
|
|
71
115
|
steps.push({ kind: 'skip', reason: 'dotmd plugin not installed — skipping plugin update' });
|
|
116
|
+
} else if (ctx.plugin.marketplaceRegistered === false) {
|
|
117
|
+
steps.push(...planMarketplaceRepair(ctx.plugin, { hasClaude: ctx.hasClaude, verb: 'update' }));
|
|
72
118
|
} else if (!ctx.hasClaude) {
|
|
73
119
|
steps.push({ kind: 'skip', reason: `claude CLI not found — run \`/plugin update ${ctx.plugin.id}\` from a session instead` });
|
|
74
120
|
} else {
|
|
@@ -102,6 +148,9 @@ export function runUpdate(argv, _config, opts = {}) {
|
|
|
102
148
|
: cmp < 0 ? yellow('behind — run `dotmd update`')
|
|
103
149
|
: yellow('ahead — CLI is behind');
|
|
104
150
|
process.stdout.write(`dotmd plugin: ${plugin.version ?? '?'} (${plugin.id}) ${tag}\n`);
|
|
151
|
+
if (plugin.marketplaceRegistered === false) {
|
|
152
|
+
process.stdout.write(yellow(` marketplace "${plugin.marketplace}" is not registered — the plugin fails to load; run \`dotmd install claude\`\n`));
|
|
153
|
+
}
|
|
105
154
|
} else {
|
|
106
155
|
process.stdout.write(dim('dotmd plugin: not installed — `dotmd install claude`\n'));
|
|
107
156
|
}
|
|
@@ -117,24 +166,41 @@ export function runUpdate(argv, _config, opts = {}) {
|
|
|
117
166
|
if (opts.dryRun) {
|
|
118
167
|
for (const step of steps) {
|
|
119
168
|
if (step.kind === 'skip') process.stdout.write(dim(`[dry-run] skip: ${step.reason}\n`));
|
|
169
|
+
else if (step.kind === 'manual') for (const line of step.lines) process.stdout.write(dim(`[dry-run] Run from a session: ${line}\n`));
|
|
120
170
|
else if (step.kind === 'opencode') process.stdout.write(dim(`[dry-run] Would refresh: ${step.path}\n`));
|
|
121
171
|
else process.stdout.write(dim(`[dry-run] Would run: ${step.cmd.join(' ')}\n`));
|
|
122
172
|
}
|
|
123
173
|
return;
|
|
124
174
|
}
|
|
175
|
+
// The hosts are independent: a failed `claude plugin update` says nothing
|
|
176
|
+
// about the OpenCode file, so every step runs and the failures are reported
|
|
177
|
+
// together at the end. Stopping at the first one left OpenCode stale behind a
|
|
178
|
+
// Claude registry problem — silently, since the abort said nothing about the
|
|
179
|
+
// steps it never reached.
|
|
125
180
|
let ran = false;
|
|
126
|
-
|
|
181
|
+
const failures = [];
|
|
182
|
+
const failedKinds = new Set();
|
|
127
183
|
for (const s of steps) {
|
|
128
184
|
if (s.kind === 'skip') {
|
|
129
185
|
process.stdout.write(dim(`skip: ${s.reason}\n`));
|
|
130
186
|
continue;
|
|
131
187
|
}
|
|
188
|
+
if (s.kind === 'manual') {
|
|
189
|
+
process.stdout.write(`${yellow(s.reason)} — run these from a Claude Code session:\n`);
|
|
190
|
+
for (const line of s.lines) process.stdout.write(` ${line}\n`);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (s.needs && failedKinds.has(s.needs)) {
|
|
194
|
+
process.stdout.write(dim(`skip: ${s.cmd.join(' ')} — the ${s.needs} step it depends on failed\n`));
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
132
197
|
if (s.kind === 'opencode') {
|
|
133
198
|
const result = installOpencodePlugin({ version: pkg.version });
|
|
134
199
|
process.stdout.write(dim(`refreshed opencode integration → ${pkg.version} ${result.path}\n`));
|
|
135
200
|
ran = true;
|
|
136
201
|
continue;
|
|
137
202
|
}
|
|
203
|
+
if (s.reason) process.stdout.write(dim(`${s.reason}\n`));
|
|
138
204
|
process.stdout.write(dim(`$ ${s.cmd.join(' ')}\n`));
|
|
139
205
|
const r = spawnSync(executableName(s.cmd[0]), s.cmd.slice(1), {
|
|
140
206
|
stdio: 'inherit',
|
|
@@ -142,13 +208,21 @@ export function runUpdate(argv, _config, opts = {}) {
|
|
|
142
208
|
});
|
|
143
209
|
ran = true;
|
|
144
210
|
if (r.status !== 0) {
|
|
145
|
-
|
|
211
|
+
failedKinds.add(s.kind);
|
|
212
|
+
failures.push(s);
|
|
146
213
|
process.stdout.write(yellow(`(${s.cmd[0]} exited ${r.status ?? '?'})\n`));
|
|
147
|
-
|
|
214
|
+
if (s.kind === 'marketplace') for (const line of claudeMarketplaceRefusalHint(plugin?.marketplace)) process.stdout.write(yellow(`${line}\n`));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (failures.length) {
|
|
218
|
+
process.stdout.write(yellow(`\n${failures.length === 1 ? '1 step' : `${failures.length} steps`} failed: ${failures.map(s => s.cmd.join(' ')).join('; ')}\n`));
|
|
219
|
+
if (ran && failures.length < steps.filter(s => s.kind !== 'skip' && s.kind !== 'manual').length) {
|
|
220
|
+
process.stdout.write(dim('the other steps completed; restart your Claude Code session (or /reload-plugins) to apply them.\n'));
|
|
148
221
|
}
|
|
222
|
+
process.exitCode = 1;
|
|
223
|
+
return;
|
|
149
224
|
}
|
|
150
|
-
if (ran
|
|
225
|
+
if (ran) {
|
|
151
226
|
process.stdout.write(green('\n✓ restart your Claude Code session (or /reload-plugins) to apply.\n'));
|
|
152
227
|
}
|
|
153
|
-
if (failed) process.exitCode = 1;
|
|
154
228
|
}
|
package/src/util.mjs
CHANGED
|
@@ -26,6 +26,15 @@ const SESSION_ID_SOURCES = [
|
|
|
26
26
|
{ variable: 'DOTMD_SESSION_ID', prefix: null, scope: 'session', host: 'explicit override' },
|
|
27
27
|
{ variable: 'CLAUDE_CODE_SESSION_ID', prefix: null, scope: 'session', host: 'Claude Code' },
|
|
28
28
|
{ variable: 'CLAUDE_SESSION_ID', prefix: null, scope: 'session', host: 'Claude Code' },
|
|
29
|
+
// Verified against codex-cli 0.153.4 (2026-09-07): `codex exec` ran
|
|
30
|
+
// `env | grep ^CODEX` in a tool shell and printed both variables, each equal
|
|
31
|
+
// to the "session id:" line Codex prints in its own header, so they name one
|
|
32
|
+
// Codex thread (session), not the Codex process. THREAD_ID is what the exec
|
|
33
|
+
// path (core/src/exec.rs) sets first; SESSION_ID is its older twin. No
|
|
34
|
+
// CODEX_PID exists, so a Codex claim records no owning process and reads as
|
|
35
|
+
// 'unverifiable' — a takeover stays an explicit --force.
|
|
36
|
+
{ variable: 'CODEX_THREAD_ID', prefix: null, scope: 'session', host: 'Codex' },
|
|
37
|
+
{ variable: 'CODEX_SESSION_ID', prefix: null, scope: 'session', host: 'Codex' },
|
|
29
38
|
{ variable: 'OPENCODE_SESSION_ID', prefix: null, scope: 'session', host: 'OpenCode' },
|
|
30
39
|
{ variable: 'OPENCODE_SESSION', prefix: null, scope: 'session', host: 'OpenCode' },
|
|
31
40
|
{ variable: 'OPENCODE_PID', prefix: 'opencode', scope: 'process', host: 'OpenCode' },
|