ai-pdf-builder 0.5.2 → 0.6.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/dist/chunk-BDGLLRIX.mjs +1269 -0
- package/dist/chunk-PIUXNG6H.mjs +1109 -0
- package/dist/cli.js +528 -15
- package/dist/cli.mjs +166 -7
- package/dist/index.d.mts +75 -1
- package/dist/index.d.ts +75 -1
- package/dist/index.js +372 -0
- package/dist/index.mjs +13 -1
- package/package.json +1 -1
|
@@ -0,0 +1,1269 @@
|
|
|
1
|
+
// src/generator.ts
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
import * as fs3 from "fs";
|
|
4
|
+
import * as path3 from "path";
|
|
5
|
+
|
|
6
|
+
// src/templates.ts
|
|
7
|
+
import * as fs from "fs";
|
|
8
|
+
import * as path from "path";
|
|
9
|
+
var TEMPLATES_DIR = path.join(__dirname, "..", "templates");
|
|
10
|
+
var BUILT_IN_TEMPLATES = {
|
|
11
|
+
default: {
|
|
12
|
+
name: "default",
|
|
13
|
+
path: path.join(TEMPLATES_DIR, "default.latex"),
|
|
14
|
+
description: "Clean, professional default template with modern styling",
|
|
15
|
+
supportedDocTypes: ["memo", "whitepaper", "report", "proposal", "generic"]
|
|
16
|
+
},
|
|
17
|
+
memo: {
|
|
18
|
+
name: "memo",
|
|
19
|
+
path: path.join(TEMPLATES_DIR, "memo.latex"),
|
|
20
|
+
description: "Business memo template with executive summary styling",
|
|
21
|
+
supportedDocTypes: ["memo", "report", "proposal"]
|
|
22
|
+
},
|
|
23
|
+
agreement: {
|
|
24
|
+
name: "agreement",
|
|
25
|
+
path: path.join(TEMPLATES_DIR, "agreement.latex"),
|
|
26
|
+
description: "Legal agreement template with formal structure",
|
|
27
|
+
supportedDocTypes: ["agreement", "generic"]
|
|
28
|
+
},
|
|
29
|
+
termsheet: {
|
|
30
|
+
name: "termsheet",
|
|
31
|
+
path: path.join(TEMPLATES_DIR, "termsheet.latex"),
|
|
32
|
+
description: "Term sheet template for investment documents",
|
|
33
|
+
supportedDocTypes: ["termsheet", "agreement"]
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
var customTemplates = /* @__PURE__ */ new Map();
|
|
37
|
+
var templateCache = /* @__PURE__ */ new Map();
|
|
38
|
+
function getTemplate(name) {
|
|
39
|
+
if (templateCache.has(name)) {
|
|
40
|
+
return templateCache.get(name) || null;
|
|
41
|
+
}
|
|
42
|
+
if (customTemplates.has(name)) {
|
|
43
|
+
const config = customTemplates.get(name);
|
|
44
|
+
return loadTemplateFile(config.path, name);
|
|
45
|
+
}
|
|
46
|
+
if (BUILT_IN_TEMPLATES[name]) {
|
|
47
|
+
return loadTemplateFile(BUILT_IN_TEMPLATES[name].path, name);
|
|
48
|
+
}
|
|
49
|
+
if (fs.existsSync(name)) {
|
|
50
|
+
return loadTemplateFile(name, name);
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
function getTemplatePath(name) {
|
|
55
|
+
if (customTemplates.has(name)) {
|
|
56
|
+
return customTemplates.get(name).path;
|
|
57
|
+
}
|
|
58
|
+
if (BUILT_IN_TEMPLATES[name]) {
|
|
59
|
+
return BUILT_IN_TEMPLATES[name].path;
|
|
60
|
+
}
|
|
61
|
+
if (fs.existsSync(name)) {
|
|
62
|
+
return name;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
function loadTemplateFile(filePath, cacheKey) {
|
|
67
|
+
try {
|
|
68
|
+
if (!fs.existsSync(filePath)) {
|
|
69
|
+
console.warn(`Template file not found: ${filePath}`);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
73
|
+
templateCache.set(cacheKey, content);
|
|
74
|
+
return content;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error(`Error loading template ${filePath}:`, error);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function listTemplates() {
|
|
81
|
+
const builtIn = Object.values(BUILT_IN_TEMPLATES);
|
|
82
|
+
const custom = Array.from(customTemplates.values());
|
|
83
|
+
return [...builtIn, ...custom];
|
|
84
|
+
}
|
|
85
|
+
function getTemplateConfig(name) {
|
|
86
|
+
if (customTemplates.has(name)) {
|
|
87
|
+
return customTemplates.get(name) || null;
|
|
88
|
+
}
|
|
89
|
+
if (BUILT_IN_TEMPLATES[name]) {
|
|
90
|
+
return BUILT_IN_TEMPLATES[name];
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
function registerTemplate(config) {
|
|
95
|
+
if (!fs.existsSync(config.path)) {
|
|
96
|
+
throw new Error(`Template file not found: ${config.path}`);
|
|
97
|
+
}
|
|
98
|
+
const absolutePath = path.resolve(config.path);
|
|
99
|
+
customTemplates.set(config.name, {
|
|
100
|
+
...config,
|
|
101
|
+
path: absolutePath
|
|
102
|
+
});
|
|
103
|
+
templateCache.delete(config.name);
|
|
104
|
+
}
|
|
105
|
+
function unregisterTemplate(name) {
|
|
106
|
+
if (customTemplates.has(name)) {
|
|
107
|
+
customTemplates.delete(name);
|
|
108
|
+
templateCache.delete(name);
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
function hasTemplate(name) {
|
|
114
|
+
return customTemplates.has(name) || BUILT_IN_TEMPLATES.hasOwnProperty(name) || fs.existsSync(name);
|
|
115
|
+
}
|
|
116
|
+
function getTemplatesForDocType(docType) {
|
|
117
|
+
const all = listTemplates();
|
|
118
|
+
return all.filter((t) => t.supportedDocTypes.includes(docType));
|
|
119
|
+
}
|
|
120
|
+
function clearTemplateCache() {
|
|
121
|
+
templateCache.clear();
|
|
122
|
+
}
|
|
123
|
+
function getTemplatesDirectory() {
|
|
124
|
+
return TEMPLATES_DIR;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/utils.ts
|
|
128
|
+
import { execSync } from "child_process";
|
|
129
|
+
import * as fs2 from "fs";
|
|
130
|
+
import * as path2 from "path";
|
|
131
|
+
import * as os from "os";
|
|
132
|
+
function checkPandoc() {
|
|
133
|
+
try {
|
|
134
|
+
const version = execSync("pandoc --version", { encoding: "utf-8" });
|
|
135
|
+
const versionMatch = version.match(/pandoc\s+([\d.]+)/);
|
|
136
|
+
const pathResult = execSync("which pandoc", { encoding: "utf-8" }).trim();
|
|
137
|
+
return {
|
|
138
|
+
available: true,
|
|
139
|
+
version: versionMatch ? versionMatch[1] : "unknown",
|
|
140
|
+
path: pathResult
|
|
141
|
+
};
|
|
142
|
+
} catch (error) {
|
|
143
|
+
return {
|
|
144
|
+
available: false,
|
|
145
|
+
error: "Pandoc not found. Install with: brew install pandoc (macOS) or apt-get install pandoc (Linux)"
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function checkLaTeX() {
|
|
150
|
+
try {
|
|
151
|
+
const version = execSync("pdflatex --version", { encoding: "utf-8" });
|
|
152
|
+
const versionMatch = version.match(/pdfTeX\s+([\d.-]+)/);
|
|
153
|
+
const pathResult = execSync("which pdflatex", { encoding: "utf-8" }).trim();
|
|
154
|
+
return {
|
|
155
|
+
available: true,
|
|
156
|
+
engine: "pdflatex",
|
|
157
|
+
version: versionMatch ? versionMatch[1] : "unknown",
|
|
158
|
+
path: pathResult
|
|
159
|
+
};
|
|
160
|
+
} catch (error) {
|
|
161
|
+
return {
|
|
162
|
+
available: false,
|
|
163
|
+
error: "pdflatex not found. Install BasicTeX (brew install --cask basictex) or TeX Live"
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function checkSystem() {
|
|
168
|
+
const pandoc = checkPandoc();
|
|
169
|
+
const latex = checkLaTeX();
|
|
170
|
+
const ready = pandoc.available && latex.available;
|
|
171
|
+
let message = "";
|
|
172
|
+
if (ready) {
|
|
173
|
+
message = `Ready: Pandoc ${pandoc.version}, pdfTeX ${latex.version}`;
|
|
174
|
+
} else {
|
|
175
|
+
const missing = [];
|
|
176
|
+
if (!pandoc.available) missing.push("Pandoc");
|
|
177
|
+
if (!latex.available) missing.push("LaTeX/pdflatex");
|
|
178
|
+
message = `Missing dependencies: ${missing.join(", ")}`;
|
|
179
|
+
}
|
|
180
|
+
return { pandoc, latex, ready, message };
|
|
181
|
+
}
|
|
182
|
+
function createTempDir(prefix = "pdf-builder") {
|
|
183
|
+
const tempDir = path2.join(os.tmpdir(), `${prefix}-${Date.now()}`);
|
|
184
|
+
fs2.mkdirSync(tempDir, { recursive: true });
|
|
185
|
+
return tempDir;
|
|
186
|
+
}
|
|
187
|
+
function cleanupTempDir(dirPath) {
|
|
188
|
+
try {
|
|
189
|
+
if (fs2.existsSync(dirPath)) {
|
|
190
|
+
fs2.rmSync(dirPath, { recursive: true, force: true });
|
|
191
|
+
}
|
|
192
|
+
} catch (error) {
|
|
193
|
+
console.warn(`Warning: Could not clean up temp directory: ${dirPath}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function generateYAMLFrontMatter(metadata) {
|
|
197
|
+
const lines = ["---"];
|
|
198
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
199
|
+
if (value !== void 0 && value !== "") {
|
|
200
|
+
const escapedValue = String(value).replace(/"/g, '\\"');
|
|
201
|
+
lines.push(`${key}: "${escapedValue}"`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
lines.push("---");
|
|
205
|
+
return lines.join("\n");
|
|
206
|
+
}
|
|
207
|
+
function sanitizeContent(content) {
|
|
208
|
+
const dangerousCommands = [
|
|
209
|
+
"\\input",
|
|
210
|
+
"\\include",
|
|
211
|
+
"\\write18",
|
|
212
|
+
"\\immediate",
|
|
213
|
+
"\\openin",
|
|
214
|
+
"\\openout",
|
|
215
|
+
"\\read",
|
|
216
|
+
"\\write",
|
|
217
|
+
"\\newwrite",
|
|
218
|
+
"\\closeout",
|
|
219
|
+
"\\closein"
|
|
220
|
+
];
|
|
221
|
+
let sanitized = content;
|
|
222
|
+
for (const cmd of dangerousCommands) {
|
|
223
|
+
const regex = new RegExp(cmd.replace("\\", "\\\\") + "[^\\s{]*({[^}]*})?", "gi");
|
|
224
|
+
sanitized = sanitized.replace(regex, "");
|
|
225
|
+
}
|
|
226
|
+
return sanitized;
|
|
227
|
+
}
|
|
228
|
+
function parseColor(color) {
|
|
229
|
+
if (/^\d+,\s*\d+,\s*\d+$/.test(color)) {
|
|
230
|
+
return color.replace(/\s/g, "");
|
|
231
|
+
}
|
|
232
|
+
const rgbMatch = color.match(/RGB\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/i);
|
|
233
|
+
if (rgbMatch) {
|
|
234
|
+
return `${rgbMatch[1]},${rgbMatch[2]},${rgbMatch[3]}`;
|
|
235
|
+
}
|
|
236
|
+
const hexMatch = color.match(/^#?([A-Fa-f0-9]{2})([A-Fa-f0-9]{2})([A-Fa-f0-9]{2})$/);
|
|
237
|
+
if (hexMatch) {
|
|
238
|
+
const r = parseInt(hexMatch[1], 16);
|
|
239
|
+
const g = parseInt(hexMatch[2], 16);
|
|
240
|
+
const b = parseInt(hexMatch[3], 16);
|
|
241
|
+
return `${r},${g},${b}`;
|
|
242
|
+
}
|
|
243
|
+
return "59,130,246";
|
|
244
|
+
}
|
|
245
|
+
function formatFileSize(bytes) {
|
|
246
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
247
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
248
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// src/generator.ts
|
|
252
|
+
var DEFAULT_OPTIONS = {
|
|
253
|
+
toc: true,
|
|
254
|
+
tocDepth: 2,
|
|
255
|
+
numberSections: true,
|
|
256
|
+
fontSize: 11,
|
|
257
|
+
margin: "1in",
|
|
258
|
+
paperSize: "letter",
|
|
259
|
+
timeout: 6e4
|
|
260
|
+
};
|
|
261
|
+
async function generatePDF(options) {
|
|
262
|
+
const startTime = Date.now();
|
|
263
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
264
|
+
const sysCheck = checkSystem();
|
|
265
|
+
if (!sysCheck.ready) {
|
|
266
|
+
return {
|
|
267
|
+
success: false,
|
|
268
|
+
error: `System requirements not met: ${sysCheck.message}`
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
if (!opts.content || opts.content.trim().length === 0) {
|
|
272
|
+
return {
|
|
273
|
+
success: false,
|
|
274
|
+
error: "Content is required"
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
let tempDir = null;
|
|
278
|
+
try {
|
|
279
|
+
tempDir = opts.workDir || createTempDir("pdf-builder");
|
|
280
|
+
const sanitizedContent = sanitizeContent(opts.content);
|
|
281
|
+
const fullMarkdown = buildMarkdownDocument(sanitizedContent, opts);
|
|
282
|
+
const inputPath = path3.join(tempDir, "input.md");
|
|
283
|
+
fs3.writeFileSync(inputPath, fullMarkdown, "utf-8");
|
|
284
|
+
const templatePath = await prepareTemplate(tempDir, opts);
|
|
285
|
+
const outputPath = opts.outputPath || path3.join(tempDir, "output.pdf");
|
|
286
|
+
const pandocOpts = {
|
|
287
|
+
inputPath,
|
|
288
|
+
outputPath,
|
|
289
|
+
templatePath,
|
|
290
|
+
toc: opts.toc ?? true,
|
|
291
|
+
tocDepth: opts.tocDepth ?? 2,
|
|
292
|
+
numberSections: opts.numberSections ?? true,
|
|
293
|
+
fontSize: opts.fontSize ?? 11,
|
|
294
|
+
margin: opts.margin ?? "1in",
|
|
295
|
+
paperSize: opts.paperSize ?? "letter",
|
|
296
|
+
timeout: opts.timeout ?? 6e4
|
|
297
|
+
};
|
|
298
|
+
await executePandoc(pandocOpts);
|
|
299
|
+
if (!fs3.existsSync(outputPath)) {
|
|
300
|
+
return {
|
|
301
|
+
success: false,
|
|
302
|
+
error: "PDF generation failed - output file not created"
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
const buffer = fs3.readFileSync(outputPath);
|
|
306
|
+
const stats = fs3.statSync(outputPath);
|
|
307
|
+
const generationTime = Date.now() - startTime;
|
|
308
|
+
const result = {
|
|
309
|
+
success: true,
|
|
310
|
+
buffer: opts.outputPath ? void 0 : buffer,
|
|
311
|
+
path: opts.outputPath || void 0,
|
|
312
|
+
fileSize: stats.size,
|
|
313
|
+
generationTime
|
|
314
|
+
};
|
|
315
|
+
result.pageCount = estimatePageCount(buffer);
|
|
316
|
+
return result;
|
|
317
|
+
} catch (error) {
|
|
318
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
319
|
+
return {
|
|
320
|
+
success: false,
|
|
321
|
+
error: `PDF generation failed: ${errorMessage}`,
|
|
322
|
+
generationTime: Date.now() - startTime
|
|
323
|
+
};
|
|
324
|
+
} finally {
|
|
325
|
+
if (tempDir && !opts.workDir && !opts.outputPath) {
|
|
326
|
+
} else if (tempDir && !opts.workDir) {
|
|
327
|
+
cleanupTempDir(tempDir);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
function buildMarkdownDocument(content, opts) {
|
|
332
|
+
const parts = [];
|
|
333
|
+
if (opts.metadata) {
|
|
334
|
+
const frontMatter = generateYAMLFrontMatter({
|
|
335
|
+
...opts.metadata,
|
|
336
|
+
geometry: `margin=${opts.margin || "1in"}`,
|
|
337
|
+
fontsize: `${opts.fontSize || 11}pt`,
|
|
338
|
+
papersize: opts.paperSize || "letter"
|
|
339
|
+
});
|
|
340
|
+
parts.push(frontMatter);
|
|
341
|
+
parts.push("");
|
|
342
|
+
}
|
|
343
|
+
parts.push(content);
|
|
344
|
+
return parts.join("\n");
|
|
345
|
+
}
|
|
346
|
+
async function prepareTemplate(tempDir, opts) {
|
|
347
|
+
const templateName = opts.template || "default";
|
|
348
|
+
let templateContent = getTemplate(templateName);
|
|
349
|
+
if (!templateContent) {
|
|
350
|
+
const templatePath = getTemplatePath(templateName);
|
|
351
|
+
if (templatePath && fs3.existsSync(templatePath)) {
|
|
352
|
+
templateContent = fs3.readFileSync(templatePath, "utf-8");
|
|
353
|
+
} else {
|
|
354
|
+
templateContent = getTemplate("default");
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (!templateContent) {
|
|
358
|
+
throw new Error(`Template not found: ${templateName}`);
|
|
359
|
+
}
|
|
360
|
+
if (opts.customColors) {
|
|
361
|
+
templateContent = applyColorCustomizations(templateContent, opts.customColors);
|
|
362
|
+
}
|
|
363
|
+
const customTemplatePath = path3.join(tempDir, "template.latex");
|
|
364
|
+
fs3.writeFileSync(customTemplatePath, templateContent, "utf-8");
|
|
365
|
+
return customTemplatePath;
|
|
366
|
+
}
|
|
367
|
+
function applyColorCustomizations(template, colors) {
|
|
368
|
+
let result = template;
|
|
369
|
+
if (colors.primary) {
|
|
370
|
+
const rgb = parseColor(colors.primary);
|
|
371
|
+
result = result.replace(
|
|
372
|
+
/\\definecolor{(?:primaryBlue|primaryColor)}{RGB}{[^}]+}/g,
|
|
373
|
+
`\\definecolor{primaryColor}{RGB}{${rgb}}`
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
if (colors.secondary) {
|
|
377
|
+
const rgb = parseColor(colors.secondary);
|
|
378
|
+
result = result.replace(
|
|
379
|
+
/\\definecolor{(?:primaryGray|secondaryColor)}{RGB}{[^}]+}/g,
|
|
380
|
+
`\\definecolor{secondaryColor}{RGB}{${rgb}}`
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
if (colors.accent) {
|
|
384
|
+
const rgb = parseColor(colors.accent);
|
|
385
|
+
result = result.replace(
|
|
386
|
+
/\\definecolor{(?:primaryDark|accentColor)}{RGB}{[^}]+}/g,
|
|
387
|
+
`\\definecolor{accentColor}{RGB}{${rgb}}`
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
return result;
|
|
391
|
+
}
|
|
392
|
+
function executePandoc(opts) {
|
|
393
|
+
return new Promise((resolve2, reject) => {
|
|
394
|
+
const args = [
|
|
395
|
+
opts.inputPath,
|
|
396
|
+
"-o",
|
|
397
|
+
opts.outputPath,
|
|
398
|
+
"--pdf-engine=pdflatex",
|
|
399
|
+
`-V`,
|
|
400
|
+
`geometry:margin=${opts.margin}`,
|
|
401
|
+
`-V`,
|
|
402
|
+
`fontsize=${opts.fontSize}pt`,
|
|
403
|
+
`-V`,
|
|
404
|
+
`papersize=${opts.paperSize}`,
|
|
405
|
+
`-V`,
|
|
406
|
+
`documentclass=article`,
|
|
407
|
+
`-V`,
|
|
408
|
+
`colorlinks=true`,
|
|
409
|
+
`-V`,
|
|
410
|
+
`linkcolor=blue`,
|
|
411
|
+
`-V`,
|
|
412
|
+
`urlcolor=blue`
|
|
413
|
+
];
|
|
414
|
+
if (opts.templatePath) {
|
|
415
|
+
args.push(`--template=${opts.templatePath}`);
|
|
416
|
+
}
|
|
417
|
+
if (opts.toc) {
|
|
418
|
+
args.push("--toc");
|
|
419
|
+
args.push(`--toc-depth=${opts.tocDepth}`);
|
|
420
|
+
}
|
|
421
|
+
if (opts.numberSections) {
|
|
422
|
+
args.push("--number-sections");
|
|
423
|
+
}
|
|
424
|
+
const pandoc = spawn("pandoc", args, {
|
|
425
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
426
|
+
});
|
|
427
|
+
let stderr = "";
|
|
428
|
+
pandoc.stderr.on("data", (data) => {
|
|
429
|
+
stderr += data.toString();
|
|
430
|
+
});
|
|
431
|
+
const timeout = setTimeout(() => {
|
|
432
|
+
pandoc.kill("SIGKILL");
|
|
433
|
+
reject(new Error(`Pandoc timed out after ${opts.timeout}ms`));
|
|
434
|
+
}, opts.timeout);
|
|
435
|
+
pandoc.on("close", (code) => {
|
|
436
|
+
clearTimeout(timeout);
|
|
437
|
+
if (code === 0) {
|
|
438
|
+
resolve2();
|
|
439
|
+
} else {
|
|
440
|
+
reject(new Error(`Pandoc failed with code ${code}: ${stderr}`));
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
pandoc.on("error", (error) => {
|
|
444
|
+
clearTimeout(timeout);
|
|
445
|
+
reject(new Error(`Failed to execute Pandoc: ${error.message}`));
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
function estimatePageCount(buffer) {
|
|
450
|
+
const content = buffer.toString("binary");
|
|
451
|
+
const matches = content.match(/\/Type\s*\/Page[^s]/g);
|
|
452
|
+
return matches ? matches.length : 1;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// src/presets.ts
|
|
456
|
+
async function generateMemo(content, metadata, options) {
|
|
457
|
+
return generatePDF({
|
|
458
|
+
content,
|
|
459
|
+
metadata,
|
|
460
|
+
template: "memo",
|
|
461
|
+
toc: false,
|
|
462
|
+
numberSections: true,
|
|
463
|
+
fontSize: 11,
|
|
464
|
+
margin: "1in",
|
|
465
|
+
...options
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
async function generateAgreement(content, metadata, options) {
|
|
469
|
+
return generatePDF({
|
|
470
|
+
content,
|
|
471
|
+
metadata,
|
|
472
|
+
template: "agreement",
|
|
473
|
+
toc: false,
|
|
474
|
+
numberSections: true,
|
|
475
|
+
fontSize: 10,
|
|
476
|
+
margin: "1in",
|
|
477
|
+
...options
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
async function generateTermsheet(content, metadata, options) {
|
|
481
|
+
return generatePDF({
|
|
482
|
+
content,
|
|
483
|
+
metadata,
|
|
484
|
+
template: "termsheet",
|
|
485
|
+
toc: false,
|
|
486
|
+
numberSections: true,
|
|
487
|
+
fontSize: 10,
|
|
488
|
+
margin: "1in",
|
|
489
|
+
...options
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
async function generateWhitepaper(content, metadata, options) {
|
|
493
|
+
return generatePDF({
|
|
494
|
+
content,
|
|
495
|
+
metadata,
|
|
496
|
+
template: "whitepaper",
|
|
497
|
+
toc: true,
|
|
498
|
+
tocDepth: 2,
|
|
499
|
+
numberSections: true,
|
|
500
|
+
fontSize: 11,
|
|
501
|
+
margin: "1.2in",
|
|
502
|
+
...options
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
async function generateReport(content, metadata, options) {
|
|
506
|
+
return generatePDF({
|
|
507
|
+
content,
|
|
508
|
+
metadata,
|
|
509
|
+
template: "default",
|
|
510
|
+
toc: true,
|
|
511
|
+
tocDepth: 2,
|
|
512
|
+
numberSections: true,
|
|
513
|
+
fontSize: 11,
|
|
514
|
+
margin: "1in",
|
|
515
|
+
...options
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
async function generateProposal(content, metadata, options) {
|
|
519
|
+
return generatePDF({
|
|
520
|
+
content,
|
|
521
|
+
metadata,
|
|
522
|
+
template: "default",
|
|
523
|
+
toc: false,
|
|
524
|
+
numberSections: true,
|
|
525
|
+
fontSize: 11,
|
|
526
|
+
margin: "1in",
|
|
527
|
+
...options
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
async function generateCapitalIntroAgreement(content, metadata, options) {
|
|
531
|
+
return generatePDF({
|
|
532
|
+
content,
|
|
533
|
+
metadata,
|
|
534
|
+
template: "agreement",
|
|
535
|
+
toc: false,
|
|
536
|
+
numberSections: true,
|
|
537
|
+
fontSize: 10,
|
|
538
|
+
margin: "1in",
|
|
539
|
+
...options
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
async function generateSAFE(content, metadata, options) {
|
|
543
|
+
return generatePDF({
|
|
544
|
+
content,
|
|
545
|
+
metadata,
|
|
546
|
+
template: "agreement",
|
|
547
|
+
toc: false,
|
|
548
|
+
numberSections: true,
|
|
549
|
+
fontSize: 10,
|
|
550
|
+
margin: "1in",
|
|
551
|
+
...options
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
async function generateNDA(content, metadata, options) {
|
|
555
|
+
return generatePDF({
|
|
556
|
+
content,
|
|
557
|
+
metadata,
|
|
558
|
+
template: "agreement",
|
|
559
|
+
toc: false,
|
|
560
|
+
numberSections: true,
|
|
561
|
+
fontSize: 10,
|
|
562
|
+
margin: "1in",
|
|
563
|
+
...options
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// src/ai.ts
|
|
568
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
569
|
+
function sanitizeForLatex(content) {
|
|
570
|
+
return content.replace(/[┌┐└┘├┤┬┴┼│─═║╔╗╚╝╠╣╦╩╬]/g, "").replace(/→/g, "->").replace(/←/g, "<-").replace(/↔/g, "<->").replace(/•/g, "-").replace(/·/g, "-").replace(/…/g, "...").replace(/–/g, "-").replace(/—/g, "---").replace(/"/g, '"').replace(/"/g, '"').replace(/'/g, "'").replace(/'/g, "'").replace(/©/g, "(c)").replace(/®/g, "(R)").replace(/™/g, "(TM)").replace(/°/g, " degrees").replace(/±/g, "+/-").replace(/×/g, "x").replace(/÷/g, "/").replace(/≤/g, "<=").replace(/≥/g, ">=").replace(/≠/g, "!=").replace(/∞/g, "infinity").replace(/[^\x00-\x7F\xC0-\xFF]/g, (char) => {
|
|
571
|
+
if (char.charCodeAt(0) < 32) return char;
|
|
572
|
+
return "";
|
|
573
|
+
}).replace(/ +/g, " ").replace(/\n\n\n+/g, "\n\n");
|
|
574
|
+
}
|
|
575
|
+
var SYSTEM_PROMPTS = {
|
|
576
|
+
whitepaper: `You are a technical writer creating professional whitepapers.
|
|
577
|
+
|
|
578
|
+
IMPORTANT RULES:
|
|
579
|
+
- Do NOT use placeholder text like "[Company Name]" or "[Your Name]"
|
|
580
|
+
- Do NOT use ASCII art, box diagrams, or Unicode box-drawing characters
|
|
581
|
+
- Do NOT invent fake statistics or metrics
|
|
582
|
+
- Use real, general industry insights when specific data isn't provided
|
|
583
|
+
- Keep diagrams as simple bullet lists or tables, not ASCII art
|
|
584
|
+
|
|
585
|
+
Write clear, authoritative content with:
|
|
586
|
+
- Executive summary (2-3 paragraphs)
|
|
587
|
+
- Problem statement
|
|
588
|
+
- Solution overview
|
|
589
|
+
- Key benefits
|
|
590
|
+
- Implementation approach
|
|
591
|
+
- Conclusion
|
|
592
|
+
|
|
593
|
+
Use markdown formatting with ## for sections. Be specific and substantive.`,
|
|
594
|
+
memo: `You are a business writer creating executive memos.
|
|
595
|
+
|
|
596
|
+
IMPORTANT RULES:
|
|
597
|
+
- Do NOT use placeholder text like "[Recipient]" or "[Date]"
|
|
598
|
+
- Do NOT invent fake names, emails, or contact info
|
|
599
|
+
- If company/author info is provided, use it. Otherwise, omit those fields.
|
|
600
|
+
- Keep it professional and actionable
|
|
601
|
+
|
|
602
|
+
Write concise content with:
|
|
603
|
+
- Clear purpose statement
|
|
604
|
+
- Key points (3-5 bullets)
|
|
605
|
+
- Recommendations
|
|
606
|
+
- Next steps
|
|
607
|
+
|
|
608
|
+
Use markdown formatting. Be direct and brief.`,
|
|
609
|
+
termsheet: `You are a financial/legal writer creating investment term sheets.
|
|
610
|
+
|
|
611
|
+
IMPORTANT RULES:
|
|
612
|
+
- Do NOT use placeholder brackets like "[Amount]" - use the actual values provided
|
|
613
|
+
- If specific numbers aren't given, use realistic example figures
|
|
614
|
+
- Do NOT invent company names - use what's provided or say "the Company"
|
|
615
|
+
- Keep language precise and professional
|
|
616
|
+
|
|
617
|
+
Include standard sections:
|
|
618
|
+
- Investment overview (amount, type, valuation)
|
|
619
|
+
- Key terms table
|
|
620
|
+
- Investor rights
|
|
621
|
+
- Governance provisions
|
|
622
|
+
- Conditions and timeline
|
|
623
|
+
|
|
624
|
+
Use markdown with tables where appropriate.`,
|
|
625
|
+
safe: `You are a legal writer creating SAFE agreements.
|
|
626
|
+
|
|
627
|
+
IMPORTANT RULES:
|
|
628
|
+
- Use actual investment amounts and terms provided
|
|
629
|
+
- Do NOT use placeholder brackets
|
|
630
|
+
- Follow standard Y Combinator SAFE conventions
|
|
631
|
+
- If company name not provided, use "the Company"
|
|
632
|
+
|
|
633
|
+
Include:
|
|
634
|
+
- Investment amount and type
|
|
635
|
+
- Valuation cap (if applicable)
|
|
636
|
+
- Discount rate (if applicable)
|
|
637
|
+
- Pro-rata rights
|
|
638
|
+
- Standard SAFE provisions
|
|
639
|
+
|
|
640
|
+
Use markdown formatting with clear section headers.`,
|
|
641
|
+
nda: `You are a legal writer creating Non-Disclosure Agreements.
|
|
642
|
+
|
|
643
|
+
IMPORTANT RULES:
|
|
644
|
+
- Do NOT use placeholder brackets for party names
|
|
645
|
+
- Use "Disclosing Party" and "Receiving Party" if names not provided
|
|
646
|
+
- Keep language clear and enforceable
|
|
647
|
+
- Include standard NDA provisions
|
|
648
|
+
|
|
649
|
+
Include:
|
|
650
|
+
- Definition of confidential information
|
|
651
|
+
- Obligations of receiving party
|
|
652
|
+
- Exclusions from confidentiality
|
|
653
|
+
- Term (suggest 2-3 years if not specified)
|
|
654
|
+
- Return/destruction of materials
|
|
655
|
+
|
|
656
|
+
Use markdown formatting.`,
|
|
657
|
+
agreement: `You are a legal writer creating professional agreements.
|
|
658
|
+
|
|
659
|
+
IMPORTANT RULES:
|
|
660
|
+
- Do NOT use placeholder brackets
|
|
661
|
+
- Use clear party designations if names not provided
|
|
662
|
+
- Keep language professional and enforceable
|
|
663
|
+
|
|
664
|
+
Include appropriate sections:
|
|
665
|
+
- Parties and purpose
|
|
666
|
+
- Terms and conditions
|
|
667
|
+
- Rights and obligations
|
|
668
|
+
- Term and termination
|
|
669
|
+
- General provisions
|
|
670
|
+
|
|
671
|
+
Use markdown formatting.`,
|
|
672
|
+
report: `You are a business analyst creating professional reports.
|
|
673
|
+
|
|
674
|
+
IMPORTANT RULES:
|
|
675
|
+
- Do NOT invent fake statistics or metrics
|
|
676
|
+
- Use qualitative insights when specific data isn't provided
|
|
677
|
+
- Do NOT use ASCII art or box diagrams
|
|
678
|
+
- Keep analysis substantive and actionable
|
|
679
|
+
|
|
680
|
+
Include:
|
|
681
|
+
- Executive summary
|
|
682
|
+
- Key findings (use bullet points)
|
|
683
|
+
- Analysis and insights
|
|
684
|
+
- Recommendations
|
|
685
|
+
- Next steps
|
|
686
|
+
|
|
687
|
+
Use markdown with tables for data where appropriate.`,
|
|
688
|
+
proposal: `You are a business development writer creating compelling proposals.
|
|
689
|
+
|
|
690
|
+
IMPORTANT RULES:
|
|
691
|
+
- Do NOT use placeholder brackets
|
|
692
|
+
- If company/pricing not provided, focus on value and approach
|
|
693
|
+
- Be persuasive but realistic
|
|
694
|
+
- No fake testimonials or statistics
|
|
695
|
+
|
|
696
|
+
Include:
|
|
697
|
+
- Executive summary
|
|
698
|
+
- Understanding of the opportunity
|
|
699
|
+
- Proposed approach
|
|
700
|
+
- Key deliverables
|
|
701
|
+
- Timeline
|
|
702
|
+
- Investment/next steps
|
|
703
|
+
|
|
704
|
+
Use markdown formatting. Be confident and professional.`
|
|
705
|
+
};
|
|
706
|
+
var LENGTH_TOKENS = {
|
|
707
|
+
short: 1500,
|
|
708
|
+
medium: 3e3,
|
|
709
|
+
long: 6e3
|
|
710
|
+
};
|
|
711
|
+
async function generateContent(options, config = {}) {
|
|
712
|
+
const apiKey = config.apiKey || process.env.ANTHROPIC_API_KEY;
|
|
713
|
+
if (!apiKey) {
|
|
714
|
+
throw new Error(
|
|
715
|
+
"ANTHROPIC_API_KEY not found.\n\nTo use AI features:\n 1. Get an API key at https://console.anthropic.com\n 2. Set it: export ANTHROPIC_API_KEY=sk-ant-...\n\nOr use the non-AI generate command with your own markdown file."
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
const client = new Anthropic({ apiKey });
|
|
719
|
+
const model = config.model || "claude-sonnet-4-20250514";
|
|
720
|
+
const maxTokens = config.maxTokens || LENGTH_TOKENS[options.length || "medium"];
|
|
721
|
+
const systemPrompt = SYSTEM_PROMPTS[options.type] || SYSTEM_PROMPTS.whitepaper;
|
|
722
|
+
let userPrompt = `Create a ${options.type} about: ${options.topic}`;
|
|
723
|
+
if (options.company) {
|
|
724
|
+
userPrompt += `
|
|
725
|
+
|
|
726
|
+
Company: ${options.company}`;
|
|
727
|
+
}
|
|
728
|
+
if (options.author) {
|
|
729
|
+
userPrompt += `
|
|
730
|
+
Author: ${options.author}`;
|
|
731
|
+
}
|
|
732
|
+
if (options.context) {
|
|
733
|
+
userPrompt += `
|
|
734
|
+
|
|
735
|
+
Additional context: ${options.context}`;
|
|
736
|
+
}
|
|
737
|
+
if (options.style) {
|
|
738
|
+
userPrompt += `
|
|
739
|
+
|
|
740
|
+
Writing style: ${options.style}`;
|
|
741
|
+
}
|
|
742
|
+
userPrompt += `
|
|
743
|
+
|
|
744
|
+
Generate the complete document in markdown format. Start with a title using # heading. Do not use placeholder text or brackets.`;
|
|
745
|
+
try {
|
|
746
|
+
const response = await client.messages.create({
|
|
747
|
+
model,
|
|
748
|
+
max_tokens: maxTokens,
|
|
749
|
+
system: systemPrompt,
|
|
750
|
+
messages: [
|
|
751
|
+
{ role: "user", content: userPrompt }
|
|
752
|
+
]
|
|
753
|
+
});
|
|
754
|
+
const content = response.content[0];
|
|
755
|
+
if (content.type !== "text") {
|
|
756
|
+
throw new Error("Unexpected response format from AI");
|
|
757
|
+
}
|
|
758
|
+
return sanitizeForLatex(content.text);
|
|
759
|
+
} catch (error) {
|
|
760
|
+
if (error.status === 401) {
|
|
761
|
+
throw new Error(
|
|
762
|
+
"Invalid API key. Please check your ANTHROPIC_API_KEY.\nGet a valid key at https://console.anthropic.com"
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
if (error.status === 429) {
|
|
766
|
+
throw new Error(
|
|
767
|
+
"Rate limited. Please wait a moment and try again.\nOr check your API usage at https://console.anthropic.com"
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
if (error.status === 500 || error.status === 503) {
|
|
771
|
+
throw new Error(
|
|
772
|
+
"AI service temporarily unavailable. Please try again in a few moments."
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
throw error;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
async function enhanceContent(content, type, config = {}) {
|
|
779
|
+
const apiKey = config.apiKey || process.env.ANTHROPIC_API_KEY;
|
|
780
|
+
if (!apiKey) {
|
|
781
|
+
throw new Error(
|
|
782
|
+
"ANTHROPIC_API_KEY not found. Set it to use AI enhancement features."
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
const client = new Anthropic({ apiKey });
|
|
786
|
+
const model = config.model || "claude-sonnet-4-20250514";
|
|
787
|
+
try {
|
|
788
|
+
const response = await client.messages.create({
|
|
789
|
+
model,
|
|
790
|
+
max_tokens: 4e3,
|
|
791
|
+
system: `You are a professional editor improving ${type} documents.
|
|
792
|
+
|
|
793
|
+
IMPORTANT:
|
|
794
|
+
- Do NOT add placeholder text or brackets
|
|
795
|
+
- Do NOT use ASCII art or box-drawing characters
|
|
796
|
+
- Preserve all real names, numbers, and specific details
|
|
797
|
+
- Only improve writing quality, clarity, and flow
|
|
798
|
+
|
|
799
|
+
Enhance the document by:
|
|
800
|
+
- Improving clarity and readability
|
|
801
|
+
- Fixing grammar and style issues
|
|
802
|
+
- Strengthening arguments and flow
|
|
803
|
+
- Adding professional polish
|
|
804
|
+
- Maintaining markdown formatting`,
|
|
805
|
+
messages: [
|
|
806
|
+
{
|
|
807
|
+
role: "user",
|
|
808
|
+
content: `Enhance this ${type} document:
|
|
809
|
+
|
|
810
|
+
${content}
|
|
811
|
+
|
|
812
|
+
Return the improved version in markdown format.`
|
|
813
|
+
}
|
|
814
|
+
]
|
|
815
|
+
});
|
|
816
|
+
const responseContent = response.content[0];
|
|
817
|
+
if (responseContent.type !== "text") {
|
|
818
|
+
throw new Error("Unexpected response format from AI");
|
|
819
|
+
}
|
|
820
|
+
return sanitizeForLatex(responseContent.text);
|
|
821
|
+
} catch (error) {
|
|
822
|
+
if (error.status === 401) {
|
|
823
|
+
throw new Error("Invalid API key. Please check your ANTHROPIC_API_KEY.");
|
|
824
|
+
}
|
|
825
|
+
throw error;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
async function generateSummary(content, config = {}) {
|
|
829
|
+
const apiKey = config.apiKey || process.env.ANTHROPIC_API_KEY;
|
|
830
|
+
if (!apiKey) {
|
|
831
|
+
throw new Error(
|
|
832
|
+
"ANTHROPIC_API_KEY not found. Set it to use AI summary features."
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
const client = new Anthropic({ apiKey });
|
|
836
|
+
const model = config.model || "claude-sonnet-4-20250514";
|
|
837
|
+
try {
|
|
838
|
+
const response = await client.messages.create({
|
|
839
|
+
model,
|
|
840
|
+
max_tokens: 1e3,
|
|
841
|
+
system: `You are an executive assistant creating concise summaries.
|
|
842
|
+
|
|
843
|
+
IMPORTANT:
|
|
844
|
+
- Extract only facts present in the document
|
|
845
|
+
- Do NOT invent statistics or claims
|
|
846
|
+
- Keep it to 3-5 focused paragraphs
|
|
847
|
+
- Use clear, executive-friendly language`,
|
|
848
|
+
messages: [
|
|
849
|
+
{
|
|
850
|
+
role: "user",
|
|
851
|
+
content: `Create an executive summary for this document:
|
|
852
|
+
|
|
853
|
+
${content}
|
|
854
|
+
|
|
855
|
+
Return just the summary in markdown, starting with "## Executive Summary".`
|
|
856
|
+
}
|
|
857
|
+
]
|
|
858
|
+
});
|
|
859
|
+
const responseContent = response.content[0];
|
|
860
|
+
if (responseContent.type !== "text") {
|
|
861
|
+
throw new Error("Unexpected response format from AI");
|
|
862
|
+
}
|
|
863
|
+
return sanitizeForLatex(responseContent.text);
|
|
864
|
+
} catch (error) {
|
|
865
|
+
if (error.status === 401) {
|
|
866
|
+
throw new Error("Invalid API key. Please check your ANTHROPIC_API_KEY.");
|
|
867
|
+
}
|
|
868
|
+
throw error;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// src/deai.ts
|
|
873
|
+
var EM_DASH_PATTERNS = [
|
|
874
|
+
// "Word — Word" at start of definition -> "Word:"
|
|
875
|
+
{ pattern: /\*\*([^*]+)\*\* — /g, replacement: "**$1:** " },
|
|
876
|
+
// "something — something else" mid-sentence -> ". " or ", "
|
|
877
|
+
{ pattern: / — /g, replacement: ". " },
|
|
878
|
+
// Standalone em dash
|
|
879
|
+
{ pattern: /—/g, replacement: ":" }
|
|
880
|
+
];
|
|
881
|
+
var WORD_REPLACEMENTS = {
|
|
882
|
+
"utilize": "use",
|
|
883
|
+
"utilizes": "uses",
|
|
884
|
+
"utilized": "used",
|
|
885
|
+
"utilizing": "using",
|
|
886
|
+
"utilization": "use",
|
|
887
|
+
"leverage": "use",
|
|
888
|
+
"leverages": "uses",
|
|
889
|
+
"leveraged": "used",
|
|
890
|
+
"leveraging": "using",
|
|
891
|
+
"facilitate": "help",
|
|
892
|
+
"facilitates": "helps",
|
|
893
|
+
"facilitated": "helped",
|
|
894
|
+
"facilitating": "helping",
|
|
895
|
+
"facilitation": "help",
|
|
896
|
+
"implement": "build",
|
|
897
|
+
"implements": "builds",
|
|
898
|
+
"implemented": "built",
|
|
899
|
+
"implementing": "building",
|
|
900
|
+
"implementation": "build",
|
|
901
|
+
"comprehensive": "full",
|
|
902
|
+
"robust": "strong",
|
|
903
|
+
"seamless": "smooth",
|
|
904
|
+
"seamlessly": "smoothly",
|
|
905
|
+
"cutting-edge": "modern",
|
|
906
|
+
"state-of-the-art": "modern",
|
|
907
|
+
"best-in-class": "top",
|
|
908
|
+
"world-class": "top",
|
|
909
|
+
"innovative": "new",
|
|
910
|
+
"revolutionize": "change",
|
|
911
|
+
"revolutionizes": "changes",
|
|
912
|
+
"revolutionizing": "changing",
|
|
913
|
+
"transformative": "major",
|
|
914
|
+
"synergy": "benefit",
|
|
915
|
+
"synergies": "benefits",
|
|
916
|
+
"paradigm": "model",
|
|
917
|
+
"paradigms": "models",
|
|
918
|
+
"ecosystem": "system",
|
|
919
|
+
"ecosystems": "systems",
|
|
920
|
+
"holistic": "complete",
|
|
921
|
+
"scalable": "grows with you",
|
|
922
|
+
"endeavor": "effort",
|
|
923
|
+
"endeavors": "efforts",
|
|
924
|
+
"whilst": "while",
|
|
925
|
+
"amongst": "among",
|
|
926
|
+
"towards": "toward",
|
|
927
|
+
"furthermore": "also",
|
|
928
|
+
"moreover": "also",
|
|
929
|
+
"nevertheless": "still",
|
|
930
|
+
"nonetheless": "still",
|
|
931
|
+
"henceforth": "from now on",
|
|
932
|
+
"thereby": "so",
|
|
933
|
+
"thereof": "of it",
|
|
934
|
+
"wherein": "where",
|
|
935
|
+
"whereby": "by which",
|
|
936
|
+
"insofar": "as far"
|
|
937
|
+
};
|
|
938
|
+
var PHRASE_REPLACEMENTS = [
|
|
939
|
+
// Filler phrases to cut entirely
|
|
940
|
+
{ pattern: /It is important to note that /gi, replacement: "" },
|
|
941
|
+
{ pattern: /It is worth noting that /gi, replacement: "" },
|
|
942
|
+
{ pattern: /It should be noted that /gi, replacement: "" },
|
|
943
|
+
{ pattern: /It goes without saying that /gi, replacement: "" },
|
|
944
|
+
{ pattern: /Needless to say, /gi, replacement: "" },
|
|
945
|
+
{ pattern: /As mentioned earlier, /gi, replacement: "" },
|
|
946
|
+
{ pattern: /As previously mentioned, /gi, replacement: "" },
|
|
947
|
+
{ pattern: /In today's world, /gi, replacement: "" },
|
|
948
|
+
{ pattern: /In this day and age, /gi, replacement: "" },
|
|
949
|
+
{ pattern: /At the end of the day, /gi, replacement: "" },
|
|
950
|
+
{ pattern: /When all is said and done, /gi, replacement: "" },
|
|
951
|
+
{ pattern: /In the realm of /gi, replacement: "In " },
|
|
952
|
+
{ pattern: /In the world of /gi, replacement: "In " },
|
|
953
|
+
{ pattern: /In the space of /gi, replacement: "In " },
|
|
954
|
+
// Verbose phrases to tighten
|
|
955
|
+
{ pattern: /in order to /gi, replacement: "to " },
|
|
956
|
+
{ pattern: /In order to /gi, replacement: "To " },
|
|
957
|
+
{ pattern: /due to the fact that /gi, replacement: "because " },
|
|
958
|
+
{ pattern: /Due to the fact that /gi, replacement: "Because " },
|
|
959
|
+
{ pattern: /for the purpose of /gi, replacement: "for " },
|
|
960
|
+
{ pattern: /For the purpose of /gi, replacement: "For " },
|
|
961
|
+
{ pattern: /with the aim of /gi, replacement: "to " },
|
|
962
|
+
{ pattern: /With the aim of /gi, replacement: "To " },
|
|
963
|
+
{ pattern: /with regard to /gi, replacement: "about " },
|
|
964
|
+
{ pattern: /With regard to /gi, replacement: "About " },
|
|
965
|
+
{ pattern: /with respect to /gi, replacement: "about " },
|
|
966
|
+
{ pattern: /With respect to /gi, replacement: "About " },
|
|
967
|
+
{ pattern: /in the event that /gi, replacement: "if " },
|
|
968
|
+
{ pattern: /In the event that /gi, replacement: "If " },
|
|
969
|
+
{ pattern: /on a daily basis/gi, replacement: "daily" },
|
|
970
|
+
{ pattern: /on a regular basis/gi, replacement: "regularly" },
|
|
971
|
+
{ pattern: /at this point in time/gi, replacement: "now" },
|
|
972
|
+
{ pattern: /at the present time/gi, replacement: "now" },
|
|
973
|
+
{ pattern: /in the near future/gi, replacement: "soon" },
|
|
974
|
+
{ pattern: /a large number of /gi, replacement: "many " },
|
|
975
|
+
{ pattern: /A large number of /gi, replacement: "Many " },
|
|
976
|
+
{ pattern: /a wide range of /gi, replacement: "many " },
|
|
977
|
+
{ pattern: /A wide range of /gi, replacement: "Many " },
|
|
978
|
+
{ pattern: /a variety of /gi, replacement: "various " },
|
|
979
|
+
{ pattern: /A variety of /gi, replacement: "Various " },
|
|
980
|
+
{ pattern: /the ability to /gi, replacement: "can " },
|
|
981
|
+
{ pattern: /The ability to /gi, replacement: "Can " },
|
|
982
|
+
{ pattern: /has the ability to /gi, replacement: "can " },
|
|
983
|
+
{ pattern: /have the ability to /gi, replacement: "can " },
|
|
984
|
+
{ pattern: /is able to /gi, replacement: "can " },
|
|
985
|
+
{ pattern: /are able to /gi, replacement: "can " },
|
|
986
|
+
{ pattern: /was able to /gi, replacement: "could " },
|
|
987
|
+
{ pattern: /were able to /gi, replacement: "could " },
|
|
988
|
+
{ pattern: /the fact that /gi, replacement: "that " },
|
|
989
|
+
{ pattern: /despite the fact that /gi, replacement: "although " },
|
|
990
|
+
{ pattern: /Despite the fact that /gi, replacement: "Although " },
|
|
991
|
+
{ pattern: /in spite of the fact that /gi, replacement: "although " },
|
|
992
|
+
{ pattern: /In spite of the fact that /gi, replacement: "Although " },
|
|
993
|
+
// AI opener patterns
|
|
994
|
+
{ pattern: /^This document provides /gim, replacement: "This is " },
|
|
995
|
+
{ pattern: /This enables /gi, replacement: "This lets " },
|
|
996
|
+
{ pattern: /This allows /gi, replacement: "This lets " },
|
|
997
|
+
{ pattern: /This ensures /gi, replacement: "This keeps " },
|
|
998
|
+
{ pattern: /This provides /gi, replacement: "This gives " },
|
|
999
|
+
{ pattern: /This creates /gi, replacement: "This makes " },
|
|
1000
|
+
{ pattern: /enables users to /gi, replacement: "lets users " },
|
|
1001
|
+
{ pattern: /allows users to /gi, replacement: "lets users " },
|
|
1002
|
+
{ pattern: /designed to /gi, replacement: "built to " },
|
|
1003
|
+
// Redundant words
|
|
1004
|
+
{ pattern: /completely unique/gi, replacement: "unique" },
|
|
1005
|
+
{ pattern: /very unique/gi, replacement: "unique" },
|
|
1006
|
+
{ pattern: /absolutely essential/gi, replacement: "essential" },
|
|
1007
|
+
{ pattern: /basic fundamentals/gi, replacement: "fundamentals" },
|
|
1008
|
+
{ pattern: /future plans/gi, replacement: "plans" },
|
|
1009
|
+
{ pattern: /past history/gi, replacement: "history" },
|
|
1010
|
+
{ pattern: /end result/gi, replacement: "result" },
|
|
1011
|
+
{ pattern: /final outcome/gi, replacement: "outcome" },
|
|
1012
|
+
{ pattern: /free gift/gi, replacement: "gift" },
|
|
1013
|
+
{ pattern: /new innovation/gi, replacement: "innovation" },
|
|
1014
|
+
{ pattern: /unexpected surprise/gi, replacement: "surprise" },
|
|
1015
|
+
{ pattern: /close proximity/gi, replacement: "near" },
|
|
1016
|
+
{ pattern: /each and every/gi, replacement: "every" },
|
|
1017
|
+
{ pattern: /first and foremost/gi, replacement: "first" },
|
|
1018
|
+
{ pattern: /any and all/gi, replacement: "all" },
|
|
1019
|
+
{ pattern: /one and only/gi, replacement: "only" },
|
|
1020
|
+
{ pattern: /null and void/gi, replacement: "void" },
|
|
1021
|
+
{ pattern: /peace and quiet/gi, replacement: "quiet" }
|
|
1022
|
+
];
|
|
1023
|
+
function removeEmDashes(text) {
|
|
1024
|
+
let result = text;
|
|
1025
|
+
for (const { pattern, replacement } of EM_DASH_PATTERNS) {
|
|
1026
|
+
result = result.replace(pattern, replacement);
|
|
1027
|
+
}
|
|
1028
|
+
return result;
|
|
1029
|
+
}
|
|
1030
|
+
function replaceAIWords(text) {
|
|
1031
|
+
let result = text;
|
|
1032
|
+
for (const [aiWord, simpleWord] of Object.entries(WORD_REPLACEMENTS)) {
|
|
1033
|
+
const regex = new RegExp(`\\b${aiWord}\\b`, "gi");
|
|
1034
|
+
result = result.replace(regex, (match) => {
|
|
1035
|
+
if (match[0] === match[0].toUpperCase()) {
|
|
1036
|
+
return simpleWord.charAt(0).toUpperCase() + simpleWord.slice(1);
|
|
1037
|
+
}
|
|
1038
|
+
return simpleWord;
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
return result;
|
|
1042
|
+
}
|
|
1043
|
+
function replacePhrases(text) {
|
|
1044
|
+
let result = text;
|
|
1045
|
+
for (const { pattern, replacement } of PHRASE_REPLACEMENTS) {
|
|
1046
|
+
result = result.replace(pattern, replacement);
|
|
1047
|
+
}
|
|
1048
|
+
return result;
|
|
1049
|
+
}
|
|
1050
|
+
function cleanupWhitespace(text) {
|
|
1051
|
+
return text.replace(/ +/g, " ").replace(/ \./g, ".").replace(/ ,/g, ",").replace(/\. \./g, ".").replace(/\n +/g, "\n").replace(/ +\n/g, "\n").replace(/\n{3,}/g, "\n\n");
|
|
1052
|
+
}
|
|
1053
|
+
function deAIify(text) {
|
|
1054
|
+
let result = text;
|
|
1055
|
+
result = replacePhrases(result);
|
|
1056
|
+
result = replaceAIWords(result);
|
|
1057
|
+
result = removeEmDashes(result);
|
|
1058
|
+
result = cleanupWhitespace(result);
|
|
1059
|
+
return result;
|
|
1060
|
+
}
|
|
1061
|
+
function deAIifyStats(text) {
|
|
1062
|
+
let emDashes = 0;
|
|
1063
|
+
let aiWords = 0;
|
|
1064
|
+
let phrases = 0;
|
|
1065
|
+
emDashes = (text.match(/—/g) || []).length;
|
|
1066
|
+
for (const aiWord of Object.keys(WORD_REPLACEMENTS)) {
|
|
1067
|
+
const regex = new RegExp(`\\b${aiWord}\\b`, "gi");
|
|
1068
|
+
aiWords += (text.match(regex) || []).length;
|
|
1069
|
+
}
|
|
1070
|
+
for (const { pattern } of PHRASE_REPLACEMENTS) {
|
|
1071
|
+
phrases += (text.match(pattern) || []).length;
|
|
1072
|
+
}
|
|
1073
|
+
return { emDashes, aiWords, phrases };
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// src/pdfutils.ts
|
|
1077
|
+
import { execSync as execSync2 } from "child_process";
|
|
1078
|
+
import * as fs4 from "fs";
|
|
1079
|
+
function formatSize(bytes) {
|
|
1080
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
1081
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
1082
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
1083
|
+
}
|
|
1084
|
+
function commandExists(cmd) {
|
|
1085
|
+
try {
|
|
1086
|
+
execSync2(`which ${cmd}`, { stdio: "ignore" });
|
|
1087
|
+
return true;
|
|
1088
|
+
} catch {
|
|
1089
|
+
return false;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
function getPdfTool() {
|
|
1093
|
+
if (commandExists("pdftk")) return "pdftk";
|
|
1094
|
+
if (commandExists("qpdf")) return "qpdf";
|
|
1095
|
+
if (commandExists("gs")) return "gs";
|
|
1096
|
+
return null;
|
|
1097
|
+
}
|
|
1098
|
+
function getPdfInfo(pdfPath) {
|
|
1099
|
+
if (!fs4.existsSync(pdfPath)) {
|
|
1100
|
+
throw new Error(`File not found: ${pdfPath}`);
|
|
1101
|
+
}
|
|
1102
|
+
const stats = fs4.statSync(pdfPath);
|
|
1103
|
+
if (commandExists("pdfinfo")) {
|
|
1104
|
+
try {
|
|
1105
|
+
const output = execSync2(`pdfinfo "${pdfPath}"`, { encoding: "utf-8" });
|
|
1106
|
+
const lines = output.split("\n");
|
|
1107
|
+
const info = {
|
|
1108
|
+
pages: 0,
|
|
1109
|
+
fileSize: stats.size,
|
|
1110
|
+
fileSizeHuman: formatSize(stats.size),
|
|
1111
|
+
encrypted: false
|
|
1112
|
+
};
|
|
1113
|
+
for (const line of lines) {
|
|
1114
|
+
const [key, ...valueParts] = line.split(":");
|
|
1115
|
+
const value = valueParts.join(":").trim();
|
|
1116
|
+
switch (key?.trim()) {
|
|
1117
|
+
case "Title":
|
|
1118
|
+
info.title = value;
|
|
1119
|
+
break;
|
|
1120
|
+
case "Author":
|
|
1121
|
+
info.author = value;
|
|
1122
|
+
break;
|
|
1123
|
+
case "Subject":
|
|
1124
|
+
info.subject = value;
|
|
1125
|
+
break;
|
|
1126
|
+
case "Creator":
|
|
1127
|
+
info.creator = value;
|
|
1128
|
+
break;
|
|
1129
|
+
case "Producer":
|
|
1130
|
+
info.producer = value;
|
|
1131
|
+
break;
|
|
1132
|
+
case "CreationDate":
|
|
1133
|
+
info.creationDate = value;
|
|
1134
|
+
break;
|
|
1135
|
+
case "ModDate":
|
|
1136
|
+
info.modDate = value;
|
|
1137
|
+
break;
|
|
1138
|
+
case "Pages":
|
|
1139
|
+
info.pages = parseInt(value) || 0;
|
|
1140
|
+
break;
|
|
1141
|
+
case "Encrypted":
|
|
1142
|
+
info.encrypted = value.toLowerCase() !== "no";
|
|
1143
|
+
break;
|
|
1144
|
+
case "PDF version":
|
|
1145
|
+
info.pdfVersion = value;
|
|
1146
|
+
break;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
return info;
|
|
1150
|
+
} catch (e) {
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
return {
|
|
1154
|
+
pages: 0,
|
|
1155
|
+
// Can't determine without tools
|
|
1156
|
+
fileSize: stats.size,
|
|
1157
|
+
fileSizeHuman: formatSize(stats.size),
|
|
1158
|
+
encrypted: false
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
function mergePdfs(inputPaths, outputPath) {
|
|
1162
|
+
for (const p of inputPaths) {
|
|
1163
|
+
if (!fs4.existsSync(p)) {
|
|
1164
|
+
throw new Error(`File not found: ${p}`);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
const tool = getPdfTool();
|
|
1168
|
+
if (tool === "pdftk") {
|
|
1169
|
+
const inputs = inputPaths.map((p) => `"${p}"`).join(" ");
|
|
1170
|
+
execSync2(`pdftk ${inputs} cat output "${outputPath}"`, { stdio: "ignore" });
|
|
1171
|
+
} else if (tool === "qpdf") {
|
|
1172
|
+
const inputs = inputPaths.map((p) => `"${p}"`).join(" ");
|
|
1173
|
+
execSync2(`qpdf --empty --pages ${inputs} -- "${outputPath}"`, { stdio: "ignore" });
|
|
1174
|
+
} else if (tool === "gs") {
|
|
1175
|
+
const inputs = inputPaths.map((p) => `"${p}"`).join(" ");
|
|
1176
|
+
execSync2(`gs -dBATCH -dNOPAUSE -q -sDEVICE=pdfwrite -sOutputFile="${outputPath}" ${inputs}`, { stdio: "ignore" });
|
|
1177
|
+
} else {
|
|
1178
|
+
throw new Error("No PDF tool available. Install pdftk, qpdf, or ghostscript.");
|
|
1179
|
+
}
|
|
1180
|
+
const info = getPdfInfo(outputPath);
|
|
1181
|
+
return {
|
|
1182
|
+
outputPath,
|
|
1183
|
+
totalPages: info.pages,
|
|
1184
|
+
fileSize: info.fileSize,
|
|
1185
|
+
fileSizeHuman: info.fileSizeHuman
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
function extractPages(inputPath, outputPath, pages) {
|
|
1189
|
+
if (!fs4.existsSync(inputPath)) {
|
|
1190
|
+
throw new Error(`File not found: ${inputPath}`);
|
|
1191
|
+
}
|
|
1192
|
+
const tool = getPdfTool();
|
|
1193
|
+
if (tool === "pdftk") {
|
|
1194
|
+
const pageSpec = pages.replace(/,/g, " ");
|
|
1195
|
+
execSync2(`pdftk "${inputPath}" cat ${pageSpec} output "${outputPath}"`, { stdio: "ignore" });
|
|
1196
|
+
} else if (tool === "qpdf") {
|
|
1197
|
+
execSync2(`qpdf "${inputPath}" --pages . ${pages} -- "${outputPath}"`, { stdio: "ignore" });
|
|
1198
|
+
} else if (tool === "gs") {
|
|
1199
|
+
const match = pages.match(/^(\d+)-(\d+)$/);
|
|
1200
|
+
if (match) {
|
|
1201
|
+
const first = match[1];
|
|
1202
|
+
const last = match[2];
|
|
1203
|
+
execSync2(`gs -dBATCH -dNOPAUSE -q -sDEVICE=pdfwrite -dFirstPage=${first} -dLastPage=${last} -sOutputFile="${outputPath}" "${inputPath}"`, { stdio: "ignore" });
|
|
1204
|
+
} else {
|
|
1205
|
+
throw new Error('Ghostscript only supports simple page ranges (e.g., "1-5"). Install pdftk or qpdf for complex selections.');
|
|
1206
|
+
}
|
|
1207
|
+
} else {
|
|
1208
|
+
throw new Error("No PDF tool available. Install pdftk, qpdf, or ghostscript.");
|
|
1209
|
+
}
|
|
1210
|
+
const info = getPdfInfo(outputPath);
|
|
1211
|
+
return {
|
|
1212
|
+
outputPath,
|
|
1213
|
+
pagesExtracted: info.pages,
|
|
1214
|
+
fileSize: info.fileSize,
|
|
1215
|
+
fileSizeHuman: info.fileSizeHuman
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
function checkPdfTools() {
|
|
1219
|
+
const pdfinfo = commandExists("pdfinfo");
|
|
1220
|
+
const pdftk = commandExists("pdftk");
|
|
1221
|
+
const qpdf = commandExists("qpdf");
|
|
1222
|
+
const gs = commandExists("gs");
|
|
1223
|
+
let recommended = null;
|
|
1224
|
+
if (!pdftk && !qpdf && !gs) {
|
|
1225
|
+
recommended = "brew install pdftk-java qpdf poppler";
|
|
1226
|
+
} else if (!pdfinfo) {
|
|
1227
|
+
recommended = "brew install poppler";
|
|
1228
|
+
}
|
|
1229
|
+
return { pdfinfo, pdftk, qpdf, gs, recommended };
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
export {
|
|
1233
|
+
getTemplate,
|
|
1234
|
+
getTemplatePath,
|
|
1235
|
+
listTemplates,
|
|
1236
|
+
getTemplateConfig,
|
|
1237
|
+
registerTemplate,
|
|
1238
|
+
unregisterTemplate,
|
|
1239
|
+
hasTemplate,
|
|
1240
|
+
getTemplatesForDocType,
|
|
1241
|
+
clearTemplateCache,
|
|
1242
|
+
getTemplatesDirectory,
|
|
1243
|
+
checkPandoc,
|
|
1244
|
+
checkLaTeX,
|
|
1245
|
+
checkSystem,
|
|
1246
|
+
sanitizeContent,
|
|
1247
|
+
parseColor,
|
|
1248
|
+
formatFileSize,
|
|
1249
|
+
generatePDF,
|
|
1250
|
+
generateMemo,
|
|
1251
|
+
generateAgreement,
|
|
1252
|
+
generateTermsheet,
|
|
1253
|
+
generateWhitepaper,
|
|
1254
|
+
generateReport,
|
|
1255
|
+
generateProposal,
|
|
1256
|
+
generateCapitalIntroAgreement,
|
|
1257
|
+
generateSAFE,
|
|
1258
|
+
generateNDA,
|
|
1259
|
+
sanitizeForLatex,
|
|
1260
|
+
generateContent,
|
|
1261
|
+
enhanceContent,
|
|
1262
|
+
generateSummary,
|
|
1263
|
+
deAIify,
|
|
1264
|
+
deAIifyStats,
|
|
1265
|
+
getPdfInfo,
|
|
1266
|
+
mergePdfs,
|
|
1267
|
+
extractPages,
|
|
1268
|
+
checkPdfTools
|
|
1269
|
+
};
|