dotmd-cli 0.86.0 → 0.87.1

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/src/diff.mjs CHANGED
@@ -3,7 +3,7 @@ import { extractFrontmatter, parseSimpleFrontmatter } from './frontmatter.mjs';
3
3
  import { asString, toRepoPath, die, warn } from './util.mjs';
4
4
  import { gitDiffSince } from './git.mjs';
5
5
  import { buildIndex, resolveDocArg } from './index.mjs';
6
- import { summarizeDiffText, DEFAULT_MODEL } from './ai.mjs';
6
+ import { summarizeDiffText } from './ai.mjs';
7
7
  import { bold, dim } from './color.mjs';
8
8
 
9
9
  export function runDiff(argv, config) {
@@ -12,7 +12,7 @@ export function runDiff(argv, config) {
12
12
  let stat = false;
13
13
  let sinceOverride = null;
14
14
  let summarize = false;
15
- let model = DEFAULT_MODEL;
15
+ let model;
16
16
 
17
17
  for (let i = 0; i < argv.length; i++) {
18
18
  if (argv[i] === '--stat') { stat = true; continue; }
@@ -0,0 +1,30 @@
1
+ import { readGlobalErrors, globalErrorLogPath } from './journal.mjs';
2
+ import { die } from './util.mjs';
3
+ import { dim, red } from './color.mjs';
4
+
5
+ // `runlist errors` — the newest failed runlist commands, from the cross-repo
6
+ // error log every failing invocation appends to.
7
+ function parseArgs(argv) {
8
+ const opts = { limit: 20, repo: null, asJson: false };
9
+ for (let i = 0; i < argv.length; i++) {
10
+ const a = argv[i];
11
+ if (a === '--limit' || a === '--tail') {
12
+ const n = Number(argv[++i]);
13
+ if (!Number.isInteger(n) || n < 1) die(`${a} takes a whole number of entries, 1 or more.`);
14
+ opts.limit = n;
15
+ } else if (a === '--repo' && argv[i + 1]) opts.repo = argv[++i];
16
+ else if (a === '--json') opts.asJson = true;
17
+ }
18
+ return opts;
19
+ }
20
+
21
+ export function runErrors(argv) {
22
+ const opts = parseArgs(argv);
23
+ const entries = readGlobalErrors({ limit: opts.limit, repo: opts.repo });
24
+ if (opts.asJson) { process.stdout.write(`${JSON.stringify(entries, null, 2)}\n`); return; }
25
+ if (!entries.length) { process.stdout.write(`No failed runlist commands recorded in ${globalErrorLogPath()}.\n`); return; }
26
+ for (const e of entries) {
27
+ const repo = e.repo ? dim(` ${e.repo.split('/').pop()}`) : '';
28
+ process.stdout.write(`[${e.at}]${repo} ${e.command}\n ${red(e.message ?? '(no message)')}\n`);
29
+ }
30
+ }
package/src/fix-refs.mjs CHANGED
@@ -43,9 +43,7 @@ export function fixBrokenRefs(config, opts = {}) {
43
43
 
44
44
  // ── Fix broken frontmatter references ────────────────────────────────
45
45
 
46
- const brokenRefErrors = index.errors.filter(e =>
47
- e.message.includes('does not resolve to an existing file')
48
- );
46
+ const brokenRefErrors = index.errors.filter(error => error.meta?.kind === 'ref-resolution');
49
47
 
50
48
  if (brokenRefErrors.length > 0) {
51
49
  const fixesByDoc = new Map();
@@ -101,12 +99,12 @@ export function fixBrokenRefs(config, opts = {}) {
101
99
  // ── Fix broken body links ────────────────────────────────────────────
102
100
 
103
101
  for (const doc of index.docs) {
104
- const brokenBodyWarnings = doc.warnings.filter(w =>
105
- w.meta?.kind === 'body-link-resolution'
106
- && w.meta?.targetKind === 'document'
107
- && w.meta?.reason === 'missing'
102
+ const brokenBodyErrors = doc.errors.filter(error =>
103
+ error.meta?.kind === 'body-link-resolution'
104
+ && error.meta?.targetKind === 'document'
105
+ && error.meta?.reason === 'missing'
108
106
  );
109
- if (!brokenBodyWarnings.length) continue;
107
+ if (!brokenBodyErrors.length) continue;
110
108
 
111
109
  const absPath = path.join(config.repoRoot, doc.path);
112
110
  let raw = readFileSync(absPath, 'utf8');
@@ -116,10 +114,10 @@ export function fixBrokenRefs(config, opts = {}) {
116
114
  const bodyFixes = [];
117
115
  const seenBodyTargets = new Set();
118
116
 
119
- for (const warn of brokenBodyWarnings) {
120
- const brokenHref = warn.meta.relPath;
121
- const rawHref = warn.meta.rawHref ?? brokenHref;
122
- const targetKey = `${warn.meta.angle ? 'angle' : 'plain'}:${rawHref}`;
117
+ for (const error of brokenBodyErrors) {
118
+ const brokenHref = error.meta.relPath;
119
+ const rawHref = error.meta.rawHref ?? brokenHref;
120
+ const targetKey = `${error.meta.angle ? 'angle' : 'plain'}:${rawHref}`;
123
121
  if (seenBodyTargets.has(targetKey)) continue;
124
122
  seenBodyTargets.add(targetKey);
125
123
  const brokenBasename = path.basename(brokenHref);
@@ -133,11 +131,11 @@ export function fixBrokenRefs(config, opts = {}) {
133
131
 
134
132
  const suffixAt = rawHref.search(/[?#]/);
135
133
  const suffix = suffixAt === -1 ? '' : rawHref.slice(suffixAt);
136
- const renderedHref = warn.meta.angle
134
+ const renderedHref = error.meta.angle
137
135
  ? `<${correctHref}${suffix}>`
138
136
  : `${correctHref.replace(/([\s()[\]<>])/g, '\\$1')}${suffix}`;
139
137
  const linkRegex = new RegExp(
140
- '(\\]\\(\\s*)' + (warn.meta.angle ? '<' : '') + escapeRegex(rawHref) + (warn.meta.angle ? '>' : '') + '(?=\\s|\\))',
138
+ '(\\]\\(\\s*)' + (error.meta.angle ? '<' : '') + escapeRegex(rawHref) + (error.meta.angle ? '>' : '') + '(?=\\s|\\))',
141
139
  'g'
142
140
  );
143
141
  let replacements = 0;
package/src/journal.mjs CHANGED
@@ -322,6 +322,32 @@ export function recordGlobalError({ config, startMs, args, err, version }) {
322
322
  }
323
323
  }
324
324
 
325
+ // The newest failures first, as `{ at, command, message, repo, exit }`: the
326
+ // current file, then its one rollover. Reads what `recordGlobalError` wrote
327
+ // (argv and message already sanitized there); nothing else is kept per line.
328
+ export function readGlobalErrors({ limit = 20, repo = null } = {}) {
329
+ const file = globalErrorLogPath();
330
+ const backup = globalErrorLogBackupPath();
331
+ purgeLegacyTelemetry(file, backup);
332
+ pruneStaleBackup(backup);
333
+ const out = [];
334
+ for (const source of [file, backup]) {
335
+ const lines = readLogLines(source).reverse();
336
+ for (const e of lines) {
337
+ if (repo && !String(e.repo ?? '').includes(repo)) continue;
338
+ out.push({
339
+ at: e.ts ?? null,
340
+ command: ['runlist', ...(Array.isArray(e.argv) ? e.argv : [])].join(' '),
341
+ message: e.err ?? null,
342
+ repo: e.repo ?? null,
343
+ exit: e.exit ?? null,
344
+ });
345
+ if (limit && out.length >= limit) return out;
346
+ }
347
+ }
348
+ return out;
349
+ }
350
+
325
351
  // Misuse log: always-on, cross-repo, append-only record of every wrong-move the
326
352
  // PreToolUse guard intercepts (committing a gitignored prompt, `cat`-ing a
327
353
  // prompt instead of `dotmd use`, hand-editing a `status:` field, …). This is
package/src/lint.mjs CHANGED
@@ -4,7 +4,7 @@ import { extractFrontmatter, parseSimpleFrontmatter, replaceFrontmatter } from '
4
4
  import { asString, toRepoPath, escapeRegex, warn } from './util.mjs';
5
5
  import { buildIndex, collectDocFiles } from './index.mjs';
6
6
  import { updateFrontmatter } from './lifecycle.mjs';
7
- import { runMLX, checkUvAvailable } from './ai.mjs';
7
+ import { runModel } from './ai.mjs';
8
8
  import { bold, green, yellow, dim } from './color.mjs';
9
9
  import { authorizeManagedSweep, findLexicalDocsRoot } from './managed-path.mjs';
10
10
 
@@ -274,7 +274,7 @@ export function runLint(argv, config, opts = {}) {
274
274
  const statusList = inferTypeSet ? [...inferTypeSet].join(', ') : config.statusOrder.join(', ');
275
275
  const { body } = extractFrontmatter(raw);
276
276
  const prompt = `Given this markdown document, classify it into exactly one of these statuses: ${statusList}.\nReply with ONLY the status word, nothing else.\n\nFile: ${repoPath}\n\n${(body ?? '').slice(0, 4000)}`;
277
- const result = runMLX(prompt, { maxTokens: 10 });
277
+ const result = runModel(prompt, { maxTokens: 10 });
278
278
  const suggested = result?.trim().toLowerCase().split(/\s+/)[0];
279
279
  const inferValid = inferTypeSet ?? config.validStatuses;
280
280
  if (suggested && inferValid.has(suggested)) {
@@ -0,0 +1,33 @@
1
+ // HTTP requests to the local model server, run as a child process so the
2
+ // synchronous callers (summaries, lint's status inference) can wait on them
3
+ // without every render path turning async. Reads a list of
4
+ // {url, method, body, timeoutMs} as JSON on stdin, sends them in order, and
5
+ // writes the list of {ok, status, json, error} as JSON on stdout.
6
+
7
+ const input = JSON.parse(await new Promise(resolve => {
8
+ let data = '';
9
+ process.stdin.setEncoding('utf8');
10
+ process.stdin.on('data', chunk => { data += chunk; });
11
+ process.stdin.on('end', () => resolve(data));
12
+ }));
13
+
14
+ async function send(input) {
15
+ try {
16
+ const res = await fetch(input.url, {
17
+ method: input.method ?? 'GET',
18
+ headers: input.body ? { 'content-type': 'application/json' } : undefined,
19
+ body: input.body ? JSON.stringify(input.body) : undefined,
20
+ signal: AbortSignal.timeout(input.timeoutMs ?? 10000),
21
+ });
22
+ const text = await res.text();
23
+ let json = null;
24
+ try { json = JSON.parse(text); } catch { /* non-JSON body; status says enough */ }
25
+ return { ok: res.ok, status: res.status, json, error: res.ok ? null : (json?.error ?? text.slice(0, 200)) };
26
+ } catch (err) {
27
+ return { ok: false, status: 0, json: null, error: err.name === 'TimeoutError' ? 'timed out' : (err.cause?.code ?? err.message) };
28
+ }
29
+ }
30
+
31
+ const out = [];
32
+ for (const req of [input].flat()) out.push(await send(req));
33
+ process.stdout.write(JSON.stringify(Array.isArray(input) ? out : out[0]));