agent-reader 1.0.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/README.md +213 -0
- package/bin/agent-reader.js +83 -0
- package/package.json +52 -0
- package/src/cli/commands.js +602 -0
- package/src/core/assets.js +429 -0
- package/src/core/exporter.js +710 -0
- package/src/core/opener.js +329 -0
- package/src/core/renderer.js +235 -0
- package/src/core/sanitizer.js +79 -0
- package/src/core/slideshow.js +383 -0
- package/src/core/templates/docx-table.lua +4 -0
- package/src/core/templates/reference.docx +0 -0
- package/src/core/themes/dark.css +256 -0
- package/src/core/themes/light.css +312 -0
- package/src/core/themes/print.css +54 -0
- package/src/mcp/server.js +381 -0
- package/src/templates/document.html +145 -0
- package/src/templates/slideshow.html +42 -0
- package/src/utils/logger.js +64 -0
- package/src/utils/naturalSort.js +12 -0
- package/src/utils/output.js +85 -0
- package/src/utils/preferences.js +89 -0
- package/src/utils/server.js +295 -0
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import open from 'open';
|
|
4
|
+
import { execa } from 'execa';
|
|
5
|
+
import { renderMarkdown } from '../core/renderer.js';
|
|
6
|
+
import { createSlideshow } from '../core/slideshow.js';
|
|
7
|
+
import { checkPandoc, checkPuppeteer, exportDOCX, exportDOCXFromHTML, exportPDF } from '../core/exporter.js';
|
|
8
|
+
import { openTarget } from '../core/opener.js';
|
|
9
|
+
import { cleanOldOutputs, createOutputDir } from '../utils/output.js';
|
|
10
|
+
import { createLogger } from '../utils/logger.js';
|
|
11
|
+
import {
|
|
12
|
+
getPreferencesPath,
|
|
13
|
+
loadPreferences,
|
|
14
|
+
normalizeOpenMode,
|
|
15
|
+
savePreferences,
|
|
16
|
+
updatePreferences,
|
|
17
|
+
} from '../utils/preferences.js';
|
|
18
|
+
import { startStaticServer, waitForServerExit } from '../utils/server.js';
|
|
19
|
+
|
|
20
|
+
function normalizeMode(options) {
|
|
21
|
+
const profile = options.profile || 'human';
|
|
22
|
+
const jsonOnly = Boolean(options.json || profile === 'agent');
|
|
23
|
+
const logger = createLogger(profile, jsonOnly);
|
|
24
|
+
|
|
25
|
+
let autoOpen;
|
|
26
|
+
if (typeof options.open === 'boolean') {
|
|
27
|
+
autoOpen = options.open;
|
|
28
|
+
} else {
|
|
29
|
+
autoOpen = profile === 'human';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (jsonOnly) {
|
|
33
|
+
autoOpen = false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
profile,
|
|
38
|
+
jsonOnly,
|
|
39
|
+
logger,
|
|
40
|
+
autoOpen,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function readAllFromStdin() {
|
|
45
|
+
const chunks = [];
|
|
46
|
+
for await (const chunk of process.stdin) {
|
|
47
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
48
|
+
}
|
|
49
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function resolveMarkdownInput(file, options, logger) {
|
|
53
|
+
if (options.stdin) {
|
|
54
|
+
const content = await readAllFromStdin();
|
|
55
|
+
const baseDir = options.baseDir ? path.resolve(options.baseDir) : undefined;
|
|
56
|
+
|
|
57
|
+
if (!baseDir) {
|
|
58
|
+
logger.warn('warning: stdin mode without --base-dir, relative images will be skipped');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
markdown: content,
|
|
63
|
+
name: 'stdin',
|
|
64
|
+
baseDir,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!file) {
|
|
69
|
+
throw new Error('missing input markdown file path (or use --stdin)');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const sourcePath = path.resolve(file);
|
|
73
|
+
const markdown = await fs.readFile(sourcePath, 'utf8');
|
|
74
|
+
return {
|
|
75
|
+
markdown,
|
|
76
|
+
name: path.parse(sourcePath).name,
|
|
77
|
+
baseDir: options.baseDir ? path.resolve(options.baseDir) : path.dirname(sourcePath),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function statSize(filePath) {
|
|
82
|
+
return fs.stat(filePath).then((item) => item.size);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function dedupeWarnings(warnings) {
|
|
86
|
+
return [...new Set((warnings || []).filter(Boolean))];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function buildPrintHtml(imageDir) {
|
|
90
|
+
const absDir = path.resolve(imageDir);
|
|
91
|
+
const entries = await fs.readdir(absDir, { withFileTypes: true });
|
|
92
|
+
const images = entries
|
|
93
|
+
.filter((e) => e.isFile() && /\.(png|jpe?g|gif|svg|webp)$/i.test(e.name))
|
|
94
|
+
.map((e) => e.name)
|
|
95
|
+
.sort();
|
|
96
|
+
|
|
97
|
+
const slides = images.map((name) => {
|
|
98
|
+
const src = `file://${path.join(absDir, name)}`;
|
|
99
|
+
return `<div class="page"><img src="${src}"></div>`;
|
|
100
|
+
}).join('\n');
|
|
101
|
+
|
|
102
|
+
return `<!doctype html>
|
|
103
|
+
<html><head><meta charset="utf-8"><style>
|
|
104
|
+
@page { size: A4 landscape; margin: 0; }
|
|
105
|
+
* { margin: 0; padding: 0; }
|
|
106
|
+
body { background: #fff; }
|
|
107
|
+
.page {
|
|
108
|
+
width: 100vw; height: 100vh;
|
|
109
|
+
display: flex; align-items: center; justify-content: center;
|
|
110
|
+
page-break-after: always; break-after: page;
|
|
111
|
+
background: #fff;
|
|
112
|
+
}
|
|
113
|
+
.page:last-child { page-break-after: auto; break-after: auto; }
|
|
114
|
+
img { max-width: 95vw; max-height: 95vh; object-fit: contain; }
|
|
115
|
+
</style></head><body>${slides}</body></html>`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function normalizeFormat(format) {
|
|
119
|
+
const value = String(format || '').toLowerCase();
|
|
120
|
+
if (!['pdf', 'docx'].includes(value)) {
|
|
121
|
+
throw new Error(`unsupported export format: ${format}`);
|
|
122
|
+
}
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function maybeOpenPath(targetPath, autoOpen) {
|
|
127
|
+
if (!autoOpen || !targetPath) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
await open(targetPath);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function maybeServeOutput(options, outputDir, targetPath, logger, jsonOnly) {
|
|
134
|
+
if (!options.serve || !outputDir || !targetPath) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const serverHandle = await startStaticServer(outputDir, {
|
|
139
|
+
host: '127.0.0.1',
|
|
140
|
+
port: Number(options.port || 3000),
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const rel = path.relative(outputDir, targetPath).split(path.sep).join('/');
|
|
144
|
+
const url = `${serverHandle.url}/${rel}`;
|
|
145
|
+
|
|
146
|
+
if (!jsonOnly) {
|
|
147
|
+
logger.info(`serve mode: ${url}`);
|
|
148
|
+
logger.info('press Ctrl+C to stop server');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
handle: serverHandle,
|
|
153
|
+
url,
|
|
154
|
+
port: serverHandle.port,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function renderCommand(file, options) {
|
|
159
|
+
const mode = normalizeMode(options);
|
|
160
|
+
const restoreConsole = mode.logger.patchConsole();
|
|
161
|
+
try {
|
|
162
|
+
const input = await resolveMarkdownInput(file, options, mode.logger);
|
|
163
|
+
const outputDir = await createOutputDir(input.name, options.outDir);
|
|
164
|
+
|
|
165
|
+
const rendered = await renderMarkdown(input.markdown, {
|
|
166
|
+
theme: options.theme || 'light',
|
|
167
|
+
baseDir: input.baseDir,
|
|
168
|
+
inlineAll: Boolean(options.inlineAll),
|
|
169
|
+
fetchRemote: options.fetchRemote !== false,
|
|
170
|
+
outDir: outputDir,
|
|
171
|
+
title: input.name,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const outPath = path.join(outputDir, `${input.name}.html`);
|
|
175
|
+
await fs.writeFile(outPath, rendered.html, 'utf8');
|
|
176
|
+
|
|
177
|
+
// Human mode: auto-serve so export buttons work in browser (file:// can't call API)
|
|
178
|
+
// Agent mode: only serve if explicitly requested with --serve
|
|
179
|
+
const shouldServe = options.serve || (mode.autoOpen && !mode.jsonOnly);
|
|
180
|
+
const serveOptions = shouldServe ? { ...options, serve: true } : options;
|
|
181
|
+
const serve = await maybeServeOutput(serveOptions, outputDir, outPath, mode.logger, mode.jsonOnly);
|
|
182
|
+
|
|
183
|
+
if (!serve) {
|
|
184
|
+
await maybeOpenPath(outPath, mode.autoOpen);
|
|
185
|
+
} else if (mode.autoOpen) {
|
|
186
|
+
await open(serve.url);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const warnings = dedupeWarnings(rendered.warnings);
|
|
190
|
+
const size = await statSize(outPath);
|
|
191
|
+
|
|
192
|
+
const payload = {
|
|
193
|
+
path: outPath,
|
|
194
|
+
format: 'html',
|
|
195
|
+
size,
|
|
196
|
+
warnings,
|
|
197
|
+
...(serve ? { url: serve.url, port: serve.port } : {}),
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
if (mode.jsonOnly) {
|
|
201
|
+
mode.logger.json(payload);
|
|
202
|
+
} else {
|
|
203
|
+
mode.logger.info(`generated: ${outPath}`);
|
|
204
|
+
if (warnings.length) {
|
|
205
|
+
mode.logger.warn(`warnings: ${warnings.length}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (serve) {
|
|
210
|
+
await waitForServerExit(serve.handle, 10 * 60 * 1000);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return payload;
|
|
214
|
+
} finally {
|
|
215
|
+
restoreConsole();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export async function exportCommand(file, options) {
|
|
220
|
+
const mode = normalizeMode(options);
|
|
221
|
+
const restoreConsole = mode.logger.patchConsole();
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
const format = normalizeFormat(options.format);
|
|
225
|
+
const input = await resolveMarkdownInput(file, options, mode.logger);
|
|
226
|
+
const outputDir = await createOutputDir(input.name, options.outDir);
|
|
227
|
+
const warnings = [];
|
|
228
|
+
|
|
229
|
+
let targetPath;
|
|
230
|
+
let size;
|
|
231
|
+
|
|
232
|
+
if (format === 'pdf') {
|
|
233
|
+
const rendered = await renderMarkdown(input.markdown, {
|
|
234
|
+
theme: options.theme || 'light',
|
|
235
|
+
baseDir: input.baseDir,
|
|
236
|
+
inlineAll: true,
|
|
237
|
+
fetchRemote: options.fetchRemote !== false,
|
|
238
|
+
outDir: outputDir,
|
|
239
|
+
title: input.name,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
warnings.push(...rendered.warnings);
|
|
243
|
+
|
|
244
|
+
const htmlPath = path.join(outputDir, `${input.name}.html`);
|
|
245
|
+
await fs.writeFile(htmlPath, rendered.html, 'utf8');
|
|
246
|
+
|
|
247
|
+
const pdf = await exportPDF(rendered.html, {
|
|
248
|
+
pageSize: options.pageSize || 'A4',
|
|
249
|
+
outDir: outputDir,
|
|
250
|
+
fileName: `${input.name}.pdf`,
|
|
251
|
+
htmlPath,
|
|
252
|
+
});
|
|
253
|
+
warnings.push(...pdf.warnings);
|
|
254
|
+
targetPath = pdf.pdfPath;
|
|
255
|
+
size = pdf.size;
|
|
256
|
+
} else {
|
|
257
|
+
const docx = await exportDOCX(input.markdown, {
|
|
258
|
+
baseDir: input.baseDir,
|
|
259
|
+
outDir: outputDir,
|
|
260
|
+
fileName: `${input.name}.docx`,
|
|
261
|
+
});
|
|
262
|
+
warnings.push(...docx.warnings);
|
|
263
|
+
targetPath = docx.docxPath;
|
|
264
|
+
size = docx.size;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const serve = await maybeServeOutput(options, outputDir, targetPath, mode.logger, mode.jsonOnly);
|
|
268
|
+
|
|
269
|
+
if (!serve) {
|
|
270
|
+
await maybeOpenPath(targetPath, mode.autoOpen);
|
|
271
|
+
} else if (mode.autoOpen) {
|
|
272
|
+
await open(serve.url);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const payload = {
|
|
276
|
+
path: targetPath,
|
|
277
|
+
format,
|
|
278
|
+
size,
|
|
279
|
+
warnings: dedupeWarnings(warnings),
|
|
280
|
+
...(serve ? { url: serve.url, port: serve.port } : {}),
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
if (mode.jsonOnly) {
|
|
284
|
+
mode.logger.json(payload);
|
|
285
|
+
} else {
|
|
286
|
+
mode.logger.info(`generated: ${targetPath}`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (serve) {
|
|
290
|
+
await waitForServerExit(serve.handle, 10 * 60 * 1000);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return payload;
|
|
294
|
+
} finally {
|
|
295
|
+
restoreConsole();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export async function slidesCommand(dir, options) {
|
|
300
|
+
const mode = normalizeMode(options);
|
|
301
|
+
const restoreConsole = mode.logger.patchConsole();
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
if (!dir) {
|
|
305
|
+
throw new Error('missing image directory path');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const inputDir = path.resolve(dir);
|
|
309
|
+
const outputDir = await createOutputDir(path.basename(inputDir), options.outDir);
|
|
310
|
+
|
|
311
|
+
const result = await createSlideshow(inputDir, {
|
|
312
|
+
autoPlay: options.auto,
|
|
313
|
+
inlineAll: Boolean(options.inlineAll),
|
|
314
|
+
outDir: outputDir,
|
|
315
|
+
theme: options.theme || 'light',
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const outPath = path.join(outputDir, 'slideshow.html');
|
|
319
|
+
await fs.writeFile(outPath, result.html, 'utf8');
|
|
320
|
+
|
|
321
|
+
const printHtmlPath = path.join(outputDir, '_print.html');
|
|
322
|
+
await fs.writeFile(printHtmlPath, result.printHtml, 'utf8');
|
|
323
|
+
|
|
324
|
+
const format = String(options.format || '').toLowerCase();
|
|
325
|
+
const dirName = path.basename(inputDir);
|
|
326
|
+
|
|
327
|
+
if (format === 'pdf') {
|
|
328
|
+
const pdf = await exportPDF(result.printHtml, {
|
|
329
|
+
pageSize: 'A4',
|
|
330
|
+
landscape: true,
|
|
331
|
+
outDir: outputDir,
|
|
332
|
+
fileName: `${dirName}.pdf`,
|
|
333
|
+
htmlPath: printHtmlPath,
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
const pdfWarnings = dedupeWarnings([...result.warnings, ...(pdf.warnings || [])]);
|
|
337
|
+
|
|
338
|
+
await maybeOpenPath(pdf.pdfPath, mode.autoOpen);
|
|
339
|
+
|
|
340
|
+
const payload = {
|
|
341
|
+
path: pdf.pdfPath,
|
|
342
|
+
format: 'pdf',
|
|
343
|
+
size: pdf.size,
|
|
344
|
+
warnings: pdfWarnings,
|
|
345
|
+
image_count: result.imageCount,
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
if (mode.jsonOnly) {
|
|
349
|
+
mode.logger.json(payload);
|
|
350
|
+
} else {
|
|
351
|
+
mode.logger.info(`generated: ${pdf.pdfPath}`);
|
|
352
|
+
mode.logger.info(`images: ${result.imageCount}`);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return payload;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const serve = await maybeServeOutput(options, outputDir, outPath, mode.logger, mode.jsonOnly);
|
|
359
|
+
|
|
360
|
+
if (!serve) {
|
|
361
|
+
await maybeOpenPath(outPath, mode.autoOpen);
|
|
362
|
+
} else if (mode.autoOpen) {
|
|
363
|
+
await open(serve.url);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const payload = {
|
|
367
|
+
path: outPath,
|
|
368
|
+
format: 'html',
|
|
369
|
+
size: await statSize(outPath),
|
|
370
|
+
warnings: dedupeWarnings(result.warnings),
|
|
371
|
+
image_count: result.imageCount,
|
|
372
|
+
...(serve ? { url: serve.url, port: serve.port } : {}),
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
if (mode.jsonOnly) {
|
|
376
|
+
mode.logger.json(payload);
|
|
377
|
+
} else {
|
|
378
|
+
mode.logger.info(`generated: ${outPath}`);
|
|
379
|
+
mode.logger.info(`images: ${result.imageCount}`);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (serve) {
|
|
383
|
+
await waitForServerExit(serve.handle, 10 * 60 * 1000);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return payload;
|
|
387
|
+
} finally {
|
|
388
|
+
restoreConsole();
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export async function openCommand(target, options) {
|
|
393
|
+
const mode = normalizeMode(options);
|
|
394
|
+
const restoreConsole = mode.logger.patchConsole();
|
|
395
|
+
|
|
396
|
+
try {
|
|
397
|
+
if (!target) {
|
|
398
|
+
throw new Error('missing target path');
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const preferences = await loadPreferences();
|
|
402
|
+
const requestedMode = normalizeOpenMode(options.as || options.mode || 'auto', 'auto');
|
|
403
|
+
|
|
404
|
+
const result = await openTarget(target, {
|
|
405
|
+
mode: requestedMode,
|
|
406
|
+
defaultMode: preferences.default_open_mode,
|
|
407
|
+
theme: options.theme || preferences.default_theme,
|
|
408
|
+
outDir: options.outDir,
|
|
409
|
+
autoPlay: options.auto,
|
|
410
|
+
pageSize: options.pageSize || 'A4',
|
|
411
|
+
fetchRemote: options.fetchRemote !== false,
|
|
412
|
+
returnContent: false,
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
const canServe = result.format === 'html' && result.path;
|
|
416
|
+
const serveRoot = result.output_dir || (canServe ? path.dirname(result.path) : null);
|
|
417
|
+
const serve = canServe
|
|
418
|
+
? await maybeServeOutput(options, serveRoot, result.path, mode.logger, mode.jsonOnly)
|
|
419
|
+
: null;
|
|
420
|
+
|
|
421
|
+
if (!serve) {
|
|
422
|
+
await maybeOpenPath(result.path, mode.autoOpen);
|
|
423
|
+
} else if (mode.autoOpen) {
|
|
424
|
+
await open(serve.url);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const payload = {
|
|
428
|
+
path: result.path,
|
|
429
|
+
format: result.format,
|
|
430
|
+
size: result.size,
|
|
431
|
+
warnings: dedupeWarnings(result.warnings),
|
|
432
|
+
resolved_mode: result.resolved_mode,
|
|
433
|
+
...(serve ? { url: serve.url, port: serve.port } : {}),
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
if (mode.jsonOnly) {
|
|
437
|
+
mode.logger.json(payload);
|
|
438
|
+
} else {
|
|
439
|
+
mode.logger.info(`opened via ${payload.resolved_mode}: ${payload.path}`);
|
|
440
|
+
if (payload.warnings.length) {
|
|
441
|
+
mode.logger.warn(`warnings: ${payload.warnings.join('; ')}`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (serve) {
|
|
446
|
+
await waitForServerExit(serve.handle, 10 * 60 * 1000);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return payload;
|
|
450
|
+
} finally {
|
|
451
|
+
restoreConsole();
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
export async function setupCommand(options) {
|
|
456
|
+
const mode = normalizeMode(options);
|
|
457
|
+
const restoreConsole = mode.logger.patchConsole();
|
|
458
|
+
|
|
459
|
+
try {
|
|
460
|
+
let preferences;
|
|
461
|
+
|
|
462
|
+
if (options.reset) {
|
|
463
|
+
preferences = await savePreferences({
|
|
464
|
+
default_open_mode: 'web',
|
|
465
|
+
default_theme: 'light',
|
|
466
|
+
});
|
|
467
|
+
} else if (options.defaultOpen || options.theme) {
|
|
468
|
+
const updates = {};
|
|
469
|
+
if (options.defaultOpen) {
|
|
470
|
+
updates.default_open_mode = normalizeOpenMode(options.defaultOpen, 'web');
|
|
471
|
+
}
|
|
472
|
+
if (options.theme) {
|
|
473
|
+
updates.default_theme = options.theme;
|
|
474
|
+
}
|
|
475
|
+
preferences = await updatePreferences(updates);
|
|
476
|
+
} else {
|
|
477
|
+
preferences = await loadPreferences();
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const payload = {
|
|
481
|
+
preferences,
|
|
482
|
+
config_path: getPreferencesPath(),
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
if (mode.jsonOnly) {
|
|
486
|
+
mode.logger.json(payload);
|
|
487
|
+
} else {
|
|
488
|
+
mode.logger.info(`default open: ${preferences.default_open_mode}`);
|
|
489
|
+
mode.logger.info(`default theme: ${preferences.default_theme}`);
|
|
490
|
+
mode.logger.info(`config path: ${payload.config_path}`);
|
|
491
|
+
mode.logger.info('tip: use `agent-reader open <path>` and it will follow this preference');
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return payload;
|
|
495
|
+
} finally {
|
|
496
|
+
restoreConsole();
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
export async function doctorCommand(options) {
|
|
501
|
+
const mode = normalizeMode(options);
|
|
502
|
+
const restoreConsole = mode.logger.patchConsole();
|
|
503
|
+
|
|
504
|
+
try {
|
|
505
|
+
const checks = [];
|
|
506
|
+
|
|
507
|
+
checks.push({
|
|
508
|
+
name: 'Node.js >= 18',
|
|
509
|
+
ok: Number(process.versions.node.split('.')[0]) >= 18,
|
|
510
|
+
detail: process.version,
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
const pandoc = await checkPandoc();
|
|
514
|
+
checks.push({
|
|
515
|
+
name: 'Pandoc',
|
|
516
|
+
ok: pandoc.available,
|
|
517
|
+
detail: pandoc.version || 'not installed',
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
const puppeteer = await checkPuppeteer();
|
|
521
|
+
checks.push({
|
|
522
|
+
name: 'Puppeteer',
|
|
523
|
+
ok: puppeteer.available,
|
|
524
|
+
detail: puppeteer.version || 'not installed',
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
const payload = {
|
|
528
|
+
checks,
|
|
529
|
+
ok: checks.every((check) => check.ok),
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
if (mode.jsonOnly) {
|
|
533
|
+
mode.logger.json(payload);
|
|
534
|
+
} else {
|
|
535
|
+
for (const check of checks) {
|
|
536
|
+
mode.logger.info(`${check.ok ? '✓' : '✗'} ${check.name}: ${check.detail}`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return payload;
|
|
541
|
+
} finally {
|
|
542
|
+
restoreConsole();
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
export async function cleanCommand(options) {
|
|
547
|
+
const mode = normalizeMode(options);
|
|
548
|
+
const restoreConsole = mode.logger.patchConsole();
|
|
549
|
+
|
|
550
|
+
try {
|
|
551
|
+
const maxAge = options.days ? Number(options.days) : 7;
|
|
552
|
+
const cleaned = await cleanOldOutputs(maxAge, options.outDir);
|
|
553
|
+
|
|
554
|
+
const payload = {
|
|
555
|
+
deleted_count: cleaned.deletedCount,
|
|
556
|
+
reclaimed_bytes: cleaned.reclaimedBytes,
|
|
557
|
+
root_dir: cleaned.rootDir,
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
if (mode.jsonOnly) {
|
|
561
|
+
mode.logger.json(payload);
|
|
562
|
+
} else {
|
|
563
|
+
mode.logger.info(`deleted directories: ${cleaned.deletedCount}`);
|
|
564
|
+
mode.logger.info(`reclaimed bytes: ${cleaned.reclaimedBytes}`);
|
|
565
|
+
mode.logger.info(`root dir: ${cleaned.rootDir}`);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return payload;
|
|
569
|
+
} finally {
|
|
570
|
+
restoreConsole();
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
export async function mcpStubCommand() {
|
|
575
|
+
const result = await execa('node', ['src/mcp/server.js'], { stdio: 'inherit' });
|
|
576
|
+
return result;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
export async function runCommandSafely(handler) {
|
|
580
|
+
try {
|
|
581
|
+
await handler();
|
|
582
|
+
} catch (error) {
|
|
583
|
+
process.stderr.write(`error: ${error.message}\n`);
|
|
584
|
+
process.exitCode = 1;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
export function setupCommonCommandOptions(command) {
|
|
589
|
+
return command
|
|
590
|
+
.option('--profile <mode>', 'use profile: human|agent', 'human')
|
|
591
|
+
.option('--out-dir <dir>', 'output directory')
|
|
592
|
+
.option('--theme <name>', 'theme name or css path', 'light')
|
|
593
|
+
.option('--json', 'json-only output for agents')
|
|
594
|
+
.option('--open', 'open generated result in default app')
|
|
595
|
+
.option('--no-open', 'do not open generated result')
|
|
596
|
+
.option('--stdin', 'read markdown from stdin')
|
|
597
|
+
.option('--base-dir <dir>', 'base directory for relative assets')
|
|
598
|
+
.option('--inline-all', 'inline all assets to base64')
|
|
599
|
+
.option('--no-fetch-remote', 'disable remote image fetching')
|
|
600
|
+
.option('--serve', 'serve generated files via local HTTP server')
|
|
601
|
+
.option('--port <port>', 'port for --serve mode', '3000');
|
|
602
|
+
}
|