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/README.md +59 -0
- package/bin/dotmd.mjs +103 -5
- package/package.json +1 -1
- package/runlist.config.example.mjs +1 -1
- package/src/ai.mjs +16 -42
- package/src/body-link.mjs +11 -3
- package/src/commands.mjs +12 -0
- package/src/config.mjs +2 -0
- package/src/decisions.mjs +752 -0
- package/src/diff.mjs +2 -2
- package/src/errors-read.mjs +30 -0
- package/src/fix-refs.mjs +12 -14
- package/src/journal.mjs +26 -0
- package/src/lint.mjs +2 -2
- package/src/model-request.mjs +33 -0
- package/src/model.mjs +523 -0
- package/src/render.mjs +1 -9
- package/src/show.mjs +101 -0
- package/src/summary.mjs +1 -1
- package/src/validate.mjs +32 -39
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
|
|
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
|
|
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(
|
|
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
|
|
105
|
-
|
|
106
|
-
&&
|
|
107
|
-
&&
|
|
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 (!
|
|
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
|
|
120
|
-
const brokenHref =
|
|
121
|
-
const rawHref =
|
|
122
|
-
const targetKey = `${
|
|
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 =
|
|
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*)' + (
|
|
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 {
|
|
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 =
|
|
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]));
|