@telepat/ideon 0.1.7 → 0.1.14
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/ideon.js
CHANGED
|
@@ -12,15 +12,93 @@ import { z } from "zod";
|
|
|
12
12
|
var contentTypeValues = [
|
|
13
13
|
"article",
|
|
14
14
|
"blog-post",
|
|
15
|
-
"x-thread",
|
|
16
|
-
"x-post",
|
|
17
|
-
"reddit-post",
|
|
18
15
|
"linkedin-post",
|
|
19
16
|
"newsletter",
|
|
20
|
-
"
|
|
17
|
+
"press-release",
|
|
18
|
+
"reddit-post",
|
|
19
|
+
"science-paper",
|
|
20
|
+
"x-post",
|
|
21
|
+
"x-thread"
|
|
22
|
+
];
|
|
23
|
+
var writingStyleValues = [
|
|
24
|
+
"academic",
|
|
25
|
+
"analytical",
|
|
26
|
+
"authoritative",
|
|
27
|
+
"conversational",
|
|
28
|
+
"empathetic",
|
|
29
|
+
"friendly",
|
|
30
|
+
"journalistic",
|
|
31
|
+
"minimalist",
|
|
32
|
+
"persuasive",
|
|
33
|
+
"playful",
|
|
34
|
+
"professional",
|
|
35
|
+
"storytelling",
|
|
36
|
+
"technical"
|
|
37
|
+
];
|
|
38
|
+
var contentIntentValues = [
|
|
39
|
+
"announcement",
|
|
40
|
+
"case-study",
|
|
41
|
+
"cornerstone",
|
|
42
|
+
"counterargument",
|
|
43
|
+
"critique-review",
|
|
44
|
+
"deep-dive-analysis",
|
|
45
|
+
"how-to-guide",
|
|
46
|
+
"interview-q-and-a",
|
|
47
|
+
"listicle",
|
|
48
|
+
"opinion-piece",
|
|
49
|
+
"personal-essay",
|
|
50
|
+
"roundup-curation",
|
|
51
|
+
"tutorial"
|
|
21
52
|
];
|
|
22
|
-
var writingStyleValues = ["professional", "friendly", "technical", "academic", "opinionated", "storytelling"];
|
|
23
53
|
var targetLengthValues = ["small", "medium", "large"];
|
|
54
|
+
var targetLengthAliasWordCounts = {
|
|
55
|
+
small: 500,
|
|
56
|
+
medium: 900,
|
|
57
|
+
large: 1400
|
|
58
|
+
};
|
|
59
|
+
var defaultTargetLengthWords = targetLengthAliasWordCounts.medium;
|
|
60
|
+
function parseTargetLengthWords(value2) {
|
|
61
|
+
if (typeof value2 === "number") {
|
|
62
|
+
return Number.isInteger(value2) && value2 > 0 ? value2 : void 0;
|
|
63
|
+
}
|
|
64
|
+
if (typeof value2 !== "string") {
|
|
65
|
+
return void 0;
|
|
66
|
+
}
|
|
67
|
+
const normalized = value2.trim().toLowerCase();
|
|
68
|
+
if (normalized.length === 0) {
|
|
69
|
+
return void 0;
|
|
70
|
+
}
|
|
71
|
+
if (targetLengthValues.includes(normalized)) {
|
|
72
|
+
return targetLengthAliasWordCounts[normalized];
|
|
73
|
+
}
|
|
74
|
+
if (!/^\d+$/.test(normalized)) {
|
|
75
|
+
return void 0;
|
|
76
|
+
}
|
|
77
|
+
const parsed = Number.parseInt(normalized, 10);
|
|
78
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : void 0;
|
|
79
|
+
}
|
|
80
|
+
var targetLengthWordsSchema = z.preprocess(
|
|
81
|
+
(value2) => parseTargetLengthWords(value2),
|
|
82
|
+
z.number().int().positive()
|
|
83
|
+
);
|
|
84
|
+
function resolveTargetLengthAlias(targetLengthWords) {
|
|
85
|
+
if (!Number.isFinite(targetLengthWords) || targetLengthWords <= 0) {
|
|
86
|
+
return "medium";
|
|
87
|
+
}
|
|
88
|
+
if (targetLengthWords <= 700) {
|
|
89
|
+
return "small";
|
|
90
|
+
}
|
|
91
|
+
if (targetLengthWords <= 1150) {
|
|
92
|
+
return "medium";
|
|
93
|
+
}
|
|
94
|
+
return "large";
|
|
95
|
+
}
|
|
96
|
+
function resolveDefaultMaxLinks(targetLengthWords) {
|
|
97
|
+
const alias = resolveTargetLengthAlias(targetLengthWords);
|
|
98
|
+
if (alias === "small") return 5;
|
|
99
|
+
if (alias === "medium") return 8;
|
|
100
|
+
return 12;
|
|
101
|
+
}
|
|
24
102
|
var contentTargetRoleValues = ["primary", "secondary"];
|
|
25
103
|
var contentTargetSchema = z.object({
|
|
26
104
|
contentType: z.enum(contentTypeValues),
|
|
@@ -51,7 +129,8 @@ var appSettingsSchema = z.object({
|
|
|
51
129
|
message: "contentTargets must include exactly one primary target."
|
|
52
130
|
}).default([{ contentType: "article", role: "primary", count: 1 }]),
|
|
53
131
|
style: z.enum(writingStyleValues).default("professional"),
|
|
54
|
-
|
|
132
|
+
intent: z.enum(contentIntentValues).default("tutorial"),
|
|
133
|
+
targetLength: targetLengthWordsSchema.default(defaultTargetLengthWords)
|
|
55
134
|
});
|
|
56
135
|
var envSettingsSchema = z.object({
|
|
57
136
|
openRouterApiKey: z.string().optional(),
|
|
@@ -66,7 +145,8 @@ var envSettingsSchema = z.object({
|
|
|
66
145
|
markdownOutputDir: z.string().optional(),
|
|
67
146
|
assetOutputDir: z.string().optional(),
|
|
68
147
|
style: z.enum(writingStyleValues).optional(),
|
|
69
|
-
|
|
148
|
+
intent: z.enum(contentIntentValues).optional(),
|
|
149
|
+
targetLength: targetLengthWordsSchema.optional()
|
|
70
150
|
});
|
|
71
151
|
var jobInputSchema = z.object({
|
|
72
152
|
idea: z.string().min(1).optional(),
|
|
@@ -111,6 +191,7 @@ function readEnvSettings(env = process.env) {
|
|
|
111
191
|
markdownOutputDir: env.IDEON_MARKDOWN_OUTPUT_DIR,
|
|
112
192
|
assetOutputDir: env.IDEON_ASSET_OUTPUT_DIR,
|
|
113
193
|
style: env.IDEON_STYLE,
|
|
194
|
+
intent: env.IDEON_INTENT,
|
|
114
195
|
targetLength: env.IDEON_TARGET_LENGTH
|
|
115
196
|
});
|
|
116
197
|
}
|
|
@@ -179,6 +260,37 @@ function buildGenerationDirectoryName(baseSlug, now = /* @__PURE__ */ new Date()
|
|
|
179
260
|
].join("");
|
|
180
261
|
return `${stamp}-${baseSlug}`;
|
|
181
262
|
}
|
|
263
|
+
async function listMarkdownFilesRecursively(rootDir) {
|
|
264
|
+
return listFilesRecursively(rootDir, (fileName) => fileName.toLowerCase().endsWith(".md"));
|
|
265
|
+
}
|
|
266
|
+
async function listFilesRecursively(rootDir, predicate) {
|
|
267
|
+
const fs = await import("fs/promises");
|
|
268
|
+
const results = [];
|
|
269
|
+
const stack = [rootDir];
|
|
270
|
+
while (stack.length > 0) {
|
|
271
|
+
const current = stack.pop();
|
|
272
|
+
if (!current) {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
let entries;
|
|
276
|
+
try {
|
|
277
|
+
entries = await fs.readdir(current, { withFileTypes: true });
|
|
278
|
+
} catch {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
for (const entry of entries) {
|
|
282
|
+
const fullPath = path2.join(current, entry.name);
|
|
283
|
+
if (entry.isDirectory()) {
|
|
284
|
+
stack.push(fullPath);
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (entry.isFile() && predicate(entry.name)) {
|
|
288
|
+
results.push(fullPath);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return results;
|
|
293
|
+
}
|
|
182
294
|
async function writeUtf8File(filePath, content) {
|
|
183
295
|
await mkdir2(path2.dirname(filePath), { recursive: true });
|
|
184
296
|
await writeFile2(filePath, content, "utf8");
|
|
@@ -637,6 +749,7 @@ var configSettingKeys = [
|
|
|
637
749
|
"markdownOutputDir",
|
|
638
750
|
"assetOutputDir",
|
|
639
751
|
"style",
|
|
752
|
+
"intent",
|
|
640
753
|
"targetLength"
|
|
641
754
|
];
|
|
642
755
|
var configSecretKeys = ["openRouterApiKey", "replicateApiToken"];
|
|
@@ -665,6 +778,7 @@ async function configList() {
|
|
|
665
778
|
markdownOutputDir: settings.markdownOutputDir,
|
|
666
779
|
assetOutputDir: settings.assetOutputDir,
|
|
667
780
|
style: settings.style,
|
|
781
|
+
intent: settings.intent,
|
|
668
782
|
targetLength: settings.targetLength
|
|
669
783
|
},
|
|
670
784
|
secrets: {
|
|
@@ -767,12 +881,23 @@ function coerceSettingValue(key, rawValue) {
|
|
|
767
881
|
}
|
|
768
882
|
return trimmed;
|
|
769
883
|
}
|
|
770
|
-
case "
|
|
771
|
-
if (!
|
|
772
|
-
throw new Error(`
|
|
884
|
+
case "intent": {
|
|
885
|
+
if (!contentIntentValues.includes(trimmed)) {
|
|
886
|
+
throw new Error(`intent must be one of: ${contentIntentValues.join(", ")}.`);
|
|
773
887
|
}
|
|
774
888
|
return trimmed;
|
|
775
889
|
}
|
|
890
|
+
case "targetLength": {
|
|
891
|
+
const normalized = trimmed.toLowerCase();
|
|
892
|
+
if (targetLengthValues.includes(normalized)) {
|
|
893
|
+
return normalized;
|
|
894
|
+
}
|
|
895
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
896
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
897
|
+
throw new Error(`targetLength must be one of: ${targetLengthValues.join(", ")}, or a positive integer word count.`);
|
|
898
|
+
}
|
|
899
|
+
return parsed;
|
|
900
|
+
}
|
|
776
901
|
default:
|
|
777
902
|
throw new Error(`Unsupported config key: ${key}`);
|
|
778
903
|
}
|
|
@@ -797,6 +922,8 @@ function getSettingValue(settings, key) {
|
|
|
797
922
|
return settings.assetOutputDir;
|
|
798
923
|
case "style":
|
|
799
924
|
return settings.style;
|
|
925
|
+
case "intent":
|
|
926
|
+
return settings.intent;
|
|
800
927
|
case "targetLength":
|
|
801
928
|
return settings.targetLength;
|
|
802
929
|
default:
|
|
@@ -823,6 +950,8 @@ function setSettingValue(settings, key, value2) {
|
|
|
823
950
|
return { ...settings, assetOutputDir: value2 };
|
|
824
951
|
case "style":
|
|
825
952
|
return { ...settings, style: value2 };
|
|
953
|
+
case "intent":
|
|
954
|
+
return { ...settings, intent: value2 };
|
|
826
955
|
case "targetLength":
|
|
827
956
|
return { ...settings, targetLength: value2 };
|
|
828
957
|
default:
|
|
@@ -840,7 +969,8 @@ var writeToolInputSchema = {
|
|
|
840
969
|
primary: z3.string().optional(),
|
|
841
970
|
secondary: z3.array(z3.string()).optional(),
|
|
842
971
|
style: z3.enum(writingStyleValues).optional(),
|
|
843
|
-
|
|
972
|
+
intent: z3.enum(contentIntentValues).optional(),
|
|
973
|
+
length: z3.union([z3.enum(targetLengthValues), z3.coerce.number().int().positive()]).optional(),
|
|
844
974
|
dryRun: z3.boolean().optional(),
|
|
845
975
|
enrichLinks: z3.boolean().optional()
|
|
846
976
|
};
|
|
@@ -869,6 +999,7 @@ var ideonToolContracts = [
|
|
|
869
999
|
required: ["idea"],
|
|
870
1000
|
enums: {
|
|
871
1001
|
style: [...writingStyleValues],
|
|
1002
|
+
intent: [...contentIntentValues],
|
|
872
1003
|
length: [...targetLengthValues]
|
|
873
1004
|
}
|
|
874
1005
|
},
|
|
@@ -899,6 +1030,7 @@ var ideonSkillRegistry = [
|
|
|
899
1030
|
required: ["idea"],
|
|
900
1031
|
enums: {
|
|
901
1032
|
style: [...writingStyleValues],
|
|
1033
|
+
intent: [...contentIntentValues],
|
|
902
1034
|
length: [...targetLengthValues]
|
|
903
1035
|
}
|
|
904
1036
|
}
|
|
@@ -959,6 +1091,18 @@ function validateIntegrationContracts(sources = {
|
|
|
959
1091
|
[...writeSkill.inputContract.enums.style ?? []].sort(),
|
|
960
1092
|
[...writingStyleValues].sort()
|
|
961
1093
|
);
|
|
1094
|
+
compareStringArrays(
|
|
1095
|
+
drifts,
|
|
1096
|
+
"write.enum.intent.tool-vs-schema",
|
|
1097
|
+
[...writeTool.enums.intent ?? []].sort(),
|
|
1098
|
+
[...contentIntentValues].sort()
|
|
1099
|
+
);
|
|
1100
|
+
compareStringArrays(
|
|
1101
|
+
drifts,
|
|
1102
|
+
"write.enum.intent.skill-vs-schema",
|
|
1103
|
+
[...writeSkill.inputContract.enums.intent ?? []].sort(),
|
|
1104
|
+
[...contentIntentValues].sort()
|
|
1105
|
+
);
|
|
962
1106
|
compareStringArrays(
|
|
963
1107
|
drifts,
|
|
964
1108
|
"write.enum.length.tool-vs-schema",
|
|
@@ -1161,7 +1305,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
1161
1305
|
// package.json
|
|
1162
1306
|
var package_default = {
|
|
1163
1307
|
name: "@telepat/ideon",
|
|
1164
|
-
version: "0.1.
|
|
1308
|
+
version: "0.1.14",
|
|
1165
1309
|
description: "CLI for generating rich articles and images from ideas.",
|
|
1166
1310
|
type: "module",
|
|
1167
1311
|
repository: {
|
|
@@ -1301,8 +1445,10 @@ async function resolveRunInput(input) {
|
|
|
1301
1445
|
...envSettings.markdownOutputDir ? { markdownOutputDir: envSettings.markdownOutputDir } : {},
|
|
1302
1446
|
...envSettings.assetOutputDir ? { assetOutputDir: envSettings.assetOutputDir } : {},
|
|
1303
1447
|
...envSettings.style ? { style: envSettings.style } : {},
|
|
1448
|
+
...envSettings.intent ? { intent: envSettings.intent } : {},
|
|
1304
1449
|
...envSettings.targetLength ? { targetLength: envSettings.targetLength } : {},
|
|
1305
1450
|
...input.style ? { style: input.style } : {},
|
|
1451
|
+
...input.intent ? { intent: input.intent } : {},
|
|
1306
1452
|
...input.targetLength ? { targetLength: input.targetLength } : {},
|
|
1307
1453
|
...input.contentTargets ? { contentTargets: input.contentTargets } : {}
|
|
1308
1454
|
});
|
|
@@ -1362,13 +1508,13 @@ function assertNoLegacyXMode(contentTargets, sourceLabel) {
|
|
|
1362
1508
|
// src/pipeline/runner.ts
|
|
1363
1509
|
import { mkdir as mkdir5, stat as stat2 } from "fs/promises";
|
|
1364
1510
|
import { randomUUID } from "crypto";
|
|
1365
|
-
import
|
|
1511
|
+
import path8 from "path";
|
|
1366
1512
|
|
|
1367
1513
|
// src/generation/enrichLinks.ts
|
|
1368
1514
|
import { readFile as readFile4 } from "fs/promises";
|
|
1369
1515
|
|
|
1370
1516
|
// src/llm/prompts/linkEnrichment.ts
|
|
1371
|
-
function buildLinkCandidatesJsonSchema() {
|
|
1517
|
+
function buildLinkCandidatesJsonSchema(maxLinks = 10) {
|
|
1372
1518
|
return {
|
|
1373
1519
|
type: "object",
|
|
1374
1520
|
additionalProperties: false,
|
|
@@ -1377,13 +1523,13 @@ function buildLinkCandidatesJsonSchema() {
|
|
|
1377
1523
|
expressions: {
|
|
1378
1524
|
type: "array",
|
|
1379
1525
|
minItems: 0,
|
|
1380
|
-
maxItems:
|
|
1526
|
+
maxItems: maxLinks,
|
|
1381
1527
|
items: { type: "string", minLength: 2 }
|
|
1382
1528
|
}
|
|
1383
1529
|
}
|
|
1384
1530
|
};
|
|
1385
1531
|
}
|
|
1386
|
-
function buildLinkCandidatesMessages(content, contentType) {
|
|
1532
|
+
function buildLinkCandidatesMessages(content, contentType, maxLinks = 10) {
|
|
1387
1533
|
return [
|
|
1388
1534
|
{
|
|
1389
1535
|
role: "system",
|
|
@@ -1399,7 +1545,7 @@ function buildLinkCandidatesMessages(content, contentType) {
|
|
|
1399
1545
|
role: "user",
|
|
1400
1546
|
content: [
|
|
1401
1547
|
`Content type: ${contentType}`,
|
|
1402
|
-
|
|
1548
|
+
`Select up to ${maxLinks} expressions that should become links in this content.`,
|
|
1403
1549
|
"Each expression must be copied exactly from the text and be useful to link.",
|
|
1404
1550
|
"",
|
|
1405
1551
|
"Content:",
|
|
@@ -1454,6 +1600,8 @@ async function enrichLinks({
|
|
|
1454
1600
|
openRouter,
|
|
1455
1601
|
settings,
|
|
1456
1602
|
dryRun,
|
|
1603
|
+
customLinks = [],
|
|
1604
|
+
maxLinks = 10,
|
|
1457
1605
|
onLlmMetrics,
|
|
1458
1606
|
onItemProgress,
|
|
1459
1607
|
onInteraction
|
|
@@ -1475,7 +1623,8 @@ async function enrichLinks({
|
|
|
1475
1623
|
fileId: item.fileId,
|
|
1476
1624
|
contentType: item.contentType,
|
|
1477
1625
|
markdownPath: item.markdownPath,
|
|
1478
|
-
links: []
|
|
1626
|
+
links: [],
|
|
1627
|
+
customLinks
|
|
1479
1628
|
});
|
|
1480
1629
|
continue;
|
|
1481
1630
|
}
|
|
@@ -1494,7 +1643,8 @@ async function enrichLinks({
|
|
|
1494
1643
|
fileId: item.fileId,
|
|
1495
1644
|
contentType: item.contentType,
|
|
1496
1645
|
markdownPath: item.markdownPath,
|
|
1497
|
-
links: []
|
|
1646
|
+
links: [],
|
|
1647
|
+
customLinks
|
|
1498
1648
|
});
|
|
1499
1649
|
continue;
|
|
1500
1650
|
}
|
|
@@ -1505,8 +1655,8 @@ async function enrichLinks({
|
|
|
1505
1655
|
});
|
|
1506
1656
|
const candidateResult = await openRouter.requestStructured({
|
|
1507
1657
|
schemaName: "link_candidates",
|
|
1508
|
-
schema: buildLinkCandidatesJsonSchema(),
|
|
1509
|
-
messages: buildLinkCandidatesMessages(content, item.contentType),
|
|
1658
|
+
schema: buildLinkCandidatesJsonSchema(maxLinks),
|
|
1659
|
+
messages: buildLinkCandidatesMessages(content, item.contentType, maxLinks),
|
|
1510
1660
|
settings,
|
|
1511
1661
|
reasoning: LINKS_REASONING_SETTINGS,
|
|
1512
1662
|
interactionContext: {
|
|
@@ -1517,7 +1667,10 @@ async function enrichLinks({
|
|
|
1517
1667
|
parse(data) {
|
|
1518
1668
|
const record = data;
|
|
1519
1669
|
const expressions = Array.isArray(record.expressions) ? record.expressions.filter((value2) => typeof value2 === "string") : [];
|
|
1520
|
-
|
|
1670
|
+
const customExpressions = new Set(customLinks.map((e) => e.expression.trim().toLowerCase()));
|
|
1671
|
+
return {
|
|
1672
|
+
expressions: dedupeExpressions(expressions).filter((expr) => !customExpressions.has(expr.trim().toLowerCase())).slice(0, maxLinks)
|
|
1673
|
+
};
|
|
1521
1674
|
},
|
|
1522
1675
|
onMetrics(metrics) {
|
|
1523
1676
|
onLlmMetrics?.(item.fileId, metrics);
|
|
@@ -1594,7 +1747,8 @@ async function enrichLinks({
|
|
|
1594
1747
|
fileId: item.fileId,
|
|
1595
1748
|
contentType: item.contentType,
|
|
1596
1749
|
markdownPath: item.markdownPath,
|
|
1597
|
-
links
|
|
1750
|
+
links,
|
|
1751
|
+
customLinks
|
|
1598
1752
|
});
|
|
1599
1753
|
}
|
|
1600
1754
|
return results;
|
|
@@ -1669,44 +1823,6 @@ function toExpressionPreview(expression, maxLength = 60) {
|
|
|
1669
1823
|
}
|
|
1670
1824
|
|
|
1671
1825
|
// src/llm/prompts/writingFramework.ts
|
|
1672
|
-
var BASE_WRITING_FRAMEWORK = [
|
|
1673
|
-
"Writing framework:",
|
|
1674
|
-
"Structure with intent: open with a clear hook, build ideas in a logical progression, and close with a concrete takeaway.",
|
|
1675
|
-
"Information density mandate: each sentence must add new value, mechanism, evidence, or action; avoid empty recap lines.",
|
|
1676
|
-
"Specificity over vagueness: use concrete details, named mechanisms, and practical examples instead of abstract filler.",
|
|
1677
|
-
"Rhythm and readability: vary sentence length with short, medium, and occasional longer lines to avoid monotony and keep pace.",
|
|
1678
|
-
"Scannability and signposting: make section flow obvious with strong headings, parallel list structure, and clear paragraph openings.",
|
|
1679
|
-
"Active voice and concrete subjects: make the actor explicit and keep claims verifiable.",
|
|
1680
|
-
"Story discipline: use narrative only when it clarifies the idea, and tie every story beat to reader outcome.",
|
|
1681
|
-
"Channel fit: match native conventions of the target format while preserving clarity and substance.",
|
|
1682
|
-
"Authenticity filter: prefer plain professional language over polished AI-sounding phrasing or generic corporate jargon."
|
|
1683
|
-
].join(" ");
|
|
1684
|
-
var DO_AVOID_EXAMPLES = [
|
|
1685
|
-
"Do examples:",
|
|
1686
|
-
'Do write concrete guidance such as "Use a 3-step rollout checklist with owner, deadline, and acceptance signal".',
|
|
1687
|
-
'Do write a precise hook such as "Most teams lose two weeks per launch because approvals have no clear owner".',
|
|
1688
|
-
"Do make outcomes measurable with numbers, constraints, or operational tradeoffs when possible.",
|
|
1689
|
-
"Avoid examples:",
|
|
1690
|
-
'Avoid generic lines such as "In todays world, innovation is important".',
|
|
1691
|
-
'Avoid empty claims such as "This strategy changes everything" without evidence or mechanism.',
|
|
1692
|
-
"Avoid over-polished transitions and dramatic cliches when a simple connector is clearer.",
|
|
1693
|
-
"Avoid ending paragraphs with summary-only filler that adds no new information."
|
|
1694
|
-
].join(" ");
|
|
1695
|
-
var STYLE_DIRECTIVES = {
|
|
1696
|
-
professional: "Style directive (professional): use crisp, confident language, balanced tone, and decision-ready framing. Favor precise terms, low hype, and explicit constraints.",
|
|
1697
|
-
friendly: "Style directive (friendly): use warm, conversational language, simple transitions, and approachable phrasing. Use natural contractions and short punchy lines without losing specificity.",
|
|
1698
|
-
technical: "Style directive (technical): prioritize precision, explicit terminology, and implementation-level clarity. Preserve canonical technical terms, avoid unnecessary synonyms, and state assumptions directly.",
|
|
1699
|
-
academic: "Style directive (academic): use formal tone, careful qualification, and analytical structure. Distinguish evidence from inference and avoid rhetorical overstatement.",
|
|
1700
|
-
opinionated: "Style directive (opinionated): take a clear stance, defend it with reasoning, and avoid hedging. Make tradeoffs explicit and support claims with concrete examples.",
|
|
1701
|
-
storytelling: "Style directive (storytelling): foreground scene and momentum, then extract practical insight at each turn. Use sensory or situational detail sparingly and always tie it to utility."
|
|
1702
|
-
};
|
|
1703
|
-
var FALLBACK_STYLE_DIRECTIVE = "Style directive: keep tone consistent, intentional, and aligned with requested audience and channel. Prefer specific, active, and concrete language over generic polish.";
|
|
1704
|
-
function buildWritingFrameworkInstruction() {
|
|
1705
|
-
return [BASE_WRITING_FRAMEWORK, DO_AVOID_EXAMPLES].join(" ");
|
|
1706
|
-
}
|
|
1707
|
-
function buildStyleDirective(style) {
|
|
1708
|
-
return STYLE_DIRECTIVES[style] ?? FALLBACK_STYLE_DIRECTIVE;
|
|
1709
|
-
}
|
|
1710
1826
|
function buildRunContextDirective(contentTypes) {
|
|
1711
1827
|
const normalizedTypes = contentTypes.length > 0 ? contentTypes.join(", ") : "article";
|
|
1712
1828
|
return `Run context: requested content types are ${normalizedTypes}. Keep output aligned with this distribution plan, maintain one shared content brief, and adapt structure per channel without duplicating article-only scaffolding.`;
|
|
@@ -1716,42 +1832,148 @@ var TARGET_LENGTH_TIERS = {
|
|
|
1716
1832
|
label: "small",
|
|
1717
1833
|
article: "Target length (small article): 300\u2013800 words total, 2\u20134 sections, ~2\u20133 paragraphs per section. One core idea, lightly explored. Minimal storytelling. Use for quick explainers.",
|
|
1718
1834
|
"blog-post": "Target length (small blog post): 500\u2013900 words. Answer-focused, minimal fluff, straight to value. Good for long-tail SEO and short how-to queries.",
|
|
1719
|
-
"x-thread": "Target length (small x-thread): 3\u20135 posts, each post one clear idea with momentum from one step to the next.",
|
|
1720
|
-
"x-post": "Target length (small x-post): 70\u2013150 characters, one idea, pure hook or insight.",
|
|
1721
|
-
"reddit-post": "Target length (small reddit post): 150\u2013400 words. Quick question or observation, minimal formatting.",
|
|
1722
1835
|
"linkedin-post": "Target length (small linkedin post): 50\u2013150 words, 3\u20136 lines, single insight.",
|
|
1723
1836
|
newsletter: "Target length (small newsletter): 300\u2013800 words, 1\u20132 sections, one core idea.",
|
|
1724
|
-
"
|
|
1837
|
+
"press-release": "Target length (small press release): 300\u2013700 words with headline, lead, core announcement details, and concise quote block.",
|
|
1838
|
+
"reddit-post": "Target length (small reddit post): 150\u2013400 words. Quick question or observation, minimal formatting.",
|
|
1839
|
+
"science-paper": "Target length (small science paper): 800\u20131,400 words condensed structure with abstract-style opener, methods summary, and key findings.",
|
|
1840
|
+
"x-post": "Target length (small x-post): 70\u2013150 characters, one idea, pure hook or insight.",
|
|
1841
|
+
"x-thread": "Target length (small x-thread): 3\u20135 posts, each post one clear idea with momentum from one step to the next.",
|
|
1725
1842
|
fallback: "Target length (small): 50\u2013300 words. Compressed insight density. Prioritise hooks and key points over elaboration."
|
|
1726
1843
|
},
|
|
1727
1844
|
medium: {
|
|
1728
1845
|
label: "medium",
|
|
1729
1846
|
article: "Target length (medium article): 800\u20131,800 words total, 4\u20136 sections, ~3\u20135 paragraphs per section. 2\u20133 core ideas with examples and light narrative. Default best-performing size.",
|
|
1730
1847
|
"blog-post": "Target length (medium blog post): 900\u20131,800 words. Structured with clear H2s, includes examples and takeaways. Sweet spot for SEO and readability.",
|
|
1731
|
-
"x-thread": "Target length (medium x-thread): 5\u20138 posts, each post concise and additive, with clear narrative progression.",
|
|
1732
|
-
"x-post": "Target length (medium x-post): 150\u2013280 characters or 2\u20134 short lines, 1\u20132 ideas with slight expansion.",
|
|
1733
|
-
"reddit-post": "Target length (medium reddit post): 400\u20131,200 words. Context, experience, and question. Conversational tone.",
|
|
1734
1848
|
"linkedin-post": "Target length (medium linkedin post): 150\u2013400 words, 8\u201315 short lines, story plus takeaway. Best performing range.",
|
|
1735
1849
|
newsletter: "Target length (medium newsletter): 800\u20131,800 words, 2\u20134 sections, curated insights. Ideal default.",
|
|
1736
|
-
"
|
|
1850
|
+
"press-release": "Target length (medium press release): 700\u20131,200 words with complete release anatomy, context, quote, and next-step details.",
|
|
1851
|
+
"reddit-post": "Target length (medium reddit post): 400\u20131,200 words. Context, experience, and question. Conversational tone.",
|
|
1852
|
+
"science-paper": "Target length (medium science paper): 1,400\u20132,600 words with clearer methodological depth, results framing, and discussion of limitations.",
|
|
1853
|
+
"x-post": "Target length (medium x-post): 150\u2013280 characters or 2\u20134 short lines, 1\u20132 ideas with slight expansion.",
|
|
1854
|
+
"x-thread": "Target length (medium x-thread): 5\u20138 posts, each post concise and additive, with clear narrative progression.",
|
|
1737
1855
|
fallback: "Target length (medium): 300\u20131,200 words. Balanced depth and breadth with examples and actionable takeaways."
|
|
1738
1856
|
},
|
|
1739
1857
|
large: {
|
|
1740
1858
|
label: "large",
|
|
1741
1859
|
article: "Target length (large article): 1,800\u20133,500+ words total, 6\u201310 sections, ~5\u20138 paragraphs per section. Deep exploration with frameworks, strong internal linking potential. Use for SEO authority and pillar content.",
|
|
1742
1860
|
"blog-post": "Target length (large blog post): 1,800\u20133,000 words. Comprehensive coverage with FAQs, examples, and edge cases. Use when competing for high-value keywords.",
|
|
1743
|
-
"x-thread": "Target length (large x-thread): 8\u201312 posts, each post one punchy idea with strong narrative progression. Every post must independently hook.",
|
|
1744
|
-
"x-post": "Target length (large x-post): 200\u2013300 characters, one strong stance plus one supporting detail or proof.",
|
|
1745
|
-
"reddit-post": "Target length (large reddit post): 1,200\u20132,500+ words. Detailed breakdown with story, lessons, numbers, and mistakes.",
|
|
1746
1861
|
"linkedin-post": "Target length (large linkedin post): 400\u2013900 words. Structured storytelling, multiple insights. Use sparingly for deep authority posts.",
|
|
1747
1862
|
newsletter: "Target length (large newsletter): 1,800\u20133,000 words. Multi-topic edition with deep commentary. Use for weekly deep dives.",
|
|
1748
|
-
"
|
|
1863
|
+
"press-release": "Target length (large press release): 1,200\u20132,000+ words with full context, expanded quote material, and detailed release implications.",
|
|
1864
|
+
"reddit-post": "Target length (large reddit post): 1,200\u20132,500+ words. Detailed breakdown with story, lessons, numbers, and mistakes.",
|
|
1865
|
+
"science-paper": "Target length (large science paper): 2,600\u20134,500+ words with full narrative arc from research question through methods, results, and implications.",
|
|
1866
|
+
"x-post": "Target length (large x-post): 200\u2013300 characters, one strong stance plus one supporting detail or proof.",
|
|
1867
|
+
"x-thread": "Target length (large x-thread): 8\u201312 posts, each post one punchy idea with strong narrative progression. Every post must independently hook.",
|
|
1749
1868
|
fallback: "Target length (large): 1,200\u20133,500+ words. Deep exploration with frameworks, multiple examples, and expanded narrative."
|
|
1750
1869
|
}
|
|
1751
1870
|
};
|
|
1752
|
-
function buildTargetLengthDirective(contentType,
|
|
1753
|
-
const
|
|
1754
|
-
|
|
1871
|
+
function buildTargetLengthDirective(contentType, targetLengthWords) {
|
|
1872
|
+
const normalizedTargetLengthWords = Number.isFinite(targetLengthWords) && targetLengthWords > 0 ? Math.round(targetLengthWords) : 900;
|
|
1873
|
+
const alias = resolveTargetLengthAlias(normalizedTargetLengthWords);
|
|
1874
|
+
const tier = TARGET_LENGTH_TIERS[alias] ?? TARGET_LENGTH_TIERS["medium"];
|
|
1875
|
+
if (contentType === "article") {
|
|
1876
|
+
return `Target length (article): aim for about ${normalizedTargetLengthWords} words total while keeping section depth and structure consistent.`;
|
|
1877
|
+
}
|
|
1878
|
+
const baseDirective = tier[contentType] ?? tier.fallback;
|
|
1879
|
+
return `${baseDirective} Overall run target is about ${normalizedTargetLengthWords} words.`;
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
// src/llm/prompts/guideBundles.ts
|
|
1883
|
+
import { existsSync, readFileSync } from "fs";
|
|
1884
|
+
import path5 from "path";
|
|
1885
|
+
var guideCache = /* @__PURE__ */ new Map();
|
|
1886
|
+
function normalizeGuideContent(content) {
|
|
1887
|
+
return content.replace(/\r\n/g, "\n").trim();
|
|
1888
|
+
}
|
|
1889
|
+
function readGuideFile(relativePath) {
|
|
1890
|
+
const cached = guideCache.get(relativePath);
|
|
1891
|
+
if (cached) {
|
|
1892
|
+
return cached;
|
|
1893
|
+
}
|
|
1894
|
+
const absolutePath = path5.resolve(process.cwd(), relativePath);
|
|
1895
|
+
if (!existsSync(absolutePath)) {
|
|
1896
|
+
const fallback = `Guide unavailable: ${relativePath}. Continue with the remaining guidance.`;
|
|
1897
|
+
guideCache.set(relativePath, fallback);
|
|
1898
|
+
return fallback;
|
|
1899
|
+
}
|
|
1900
|
+
try {
|
|
1901
|
+
const content = normalizeGuideContent(readFileSync(absolutePath, "utf8"));
|
|
1902
|
+
guideCache.set(relativePath, content);
|
|
1903
|
+
return content;
|
|
1904
|
+
} catch {
|
|
1905
|
+
const fallback = `Guide failed to load: ${relativePath}. Continue with the remaining guidance.`;
|
|
1906
|
+
guideCache.set(relativePath, fallback);
|
|
1907
|
+
return fallback;
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
function buildGuideSection(relativePath) {
|
|
1911
|
+
const content = readGuideFile(relativePath);
|
|
1912
|
+
return [
|
|
1913
|
+
`Guide source: ${relativePath}`,
|
|
1914
|
+
content
|
|
1915
|
+
].join("\n");
|
|
1916
|
+
}
|
|
1917
|
+
function formatToGuidePath(contentType) {
|
|
1918
|
+
return `writing-guide/formats/${contentType}.md`;
|
|
1919
|
+
}
|
|
1920
|
+
function intentToGuidePath(intent) {
|
|
1921
|
+
return `writing-guide/content-intent/${intent}.md`;
|
|
1922
|
+
}
|
|
1923
|
+
function styleToGuidePath(style) {
|
|
1924
|
+
return `writing-guide/styles/${style}.md`;
|
|
1925
|
+
}
|
|
1926
|
+
function dedupe(items) {
|
|
1927
|
+
return Array.from(new Set(items));
|
|
1928
|
+
}
|
|
1929
|
+
function buildGuideBundle(relativePaths) {
|
|
1930
|
+
const blocks = dedupe(relativePaths).map((relativePath) => buildGuideSection(relativePath));
|
|
1931
|
+
return [
|
|
1932
|
+
"External writing guides (apply these rules directly):",
|
|
1933
|
+
...blocks
|
|
1934
|
+
].join("\n\n");
|
|
1935
|
+
}
|
|
1936
|
+
function buildArticlePlanGuideInstruction(intent, contentType) {
|
|
1937
|
+
return buildGuideBundle([
|
|
1938
|
+
"writing-guide/references/headline-writing-systems.md",
|
|
1939
|
+
"writing-guide/references/ideation-and-credibility-systems.md",
|
|
1940
|
+
"writing-guide/references/content-frameworks.md",
|
|
1941
|
+
intentToGuidePath(intent),
|
|
1942
|
+
formatToGuidePath(contentType)
|
|
1943
|
+
]);
|
|
1944
|
+
}
|
|
1945
|
+
function buildArticleSectionGuideInstruction(style, intent, contentType) {
|
|
1946
|
+
return buildGuideBundle([
|
|
1947
|
+
"writing-guide/general/core-web-writing-rules.md",
|
|
1948
|
+
"writing-guide/references/emotional-resonance.md",
|
|
1949
|
+
"writing-guide/references/prose-quality-checks.md",
|
|
1950
|
+
"writing-guide/references/readability-and-pace.md",
|
|
1951
|
+
"writing-guide/references/skimmability-patterns.md",
|
|
1952
|
+
styleToGuidePath(style),
|
|
1953
|
+
intentToGuidePath(intent),
|
|
1954
|
+
formatToGuidePath(contentType)
|
|
1955
|
+
]);
|
|
1956
|
+
}
|
|
1957
|
+
function buildContentBriefGuideInstruction(intent, primaryContentType, secondaryContentTypes) {
|
|
1958
|
+
return buildGuideBundle([
|
|
1959
|
+
"writing-guide/references/multi-channel-brief-strategy.md",
|
|
1960
|
+
"writing-guide/references/content-frameworks.md",
|
|
1961
|
+
"writing-guide/references/target-length-guidance.md",
|
|
1962
|
+
intentToGuidePath(intent),
|
|
1963
|
+
formatToGuidePath(primaryContentType),
|
|
1964
|
+
...secondaryContentTypes.map((contentType) => formatToGuidePath(contentType))
|
|
1965
|
+
]);
|
|
1966
|
+
}
|
|
1967
|
+
function buildChannelContentGuideInstruction(style, intent, contentType) {
|
|
1968
|
+
const conditionalGuides = contentType === "x-thread" ? ["writing-guide/references/x-thread-hooks.md"] : [];
|
|
1969
|
+
return buildGuideBundle([
|
|
1970
|
+
"writing-guide/references/truthful-value-framing.md",
|
|
1971
|
+
"writing-guide/references/target-length-guidance.md",
|
|
1972
|
+
...conditionalGuides,
|
|
1973
|
+
styleToGuidePath(style),
|
|
1974
|
+
intentToGuidePath(intent),
|
|
1975
|
+
formatToGuidePath(contentType)
|
|
1976
|
+
]);
|
|
1755
1977
|
}
|
|
1756
1978
|
|
|
1757
1979
|
// src/llm/prompts/contentBrief.ts
|
|
@@ -1791,11 +2013,11 @@ var contentBriefSchema = {
|
|
|
1791
2013
|
};
|
|
1792
2014
|
function buildContentBriefMessages(idea, options) {
|
|
1793
2015
|
const audienceSeed = options.targetAudienceHint?.trim() || "A general, non-specific audience.";
|
|
2016
|
+
const hasSecondaryContentTypes = options.secondaryContentTypes.length > 0;
|
|
1794
2017
|
const systemInstruction = [
|
|
1795
2018
|
"You are a senior editorial strategist.",
|
|
1796
2019
|
"Produce a shared content brief that can guide all requested content types in this run.",
|
|
1797
|
-
|
|
1798
|
-
buildStyleDirective(options.style),
|
|
2020
|
+
buildContentBriefGuideInstruction(options.intent, options.primaryContentType, options.secondaryContentTypes),
|
|
1799
2021
|
buildRunContextDirective([options.primaryContentType, ...options.secondaryContentTypes]),
|
|
1800
2022
|
"The brief must be specific, concrete, and directly usable by writers without extra clarification.",
|
|
1801
2023
|
"This run has one explicit primary output and optional secondary outputs that should promote or incite interest in the primary while remaining independently valuable.",
|
|
@@ -1824,7 +2046,7 @@ function buildContentBriefMessages(idea, options) {
|
|
|
1824
2046
|
"- voiceNotes: practical tone/voice constraints to keep outputs consistent.",
|
|
1825
2047
|
`- primaryContentType: set to "${options.primaryContentType}" exactly.`,
|
|
1826
2048
|
`- secondaryContentTypes: include these types exactly: ${options.secondaryContentTypes.join(", ") || "none"}.`,
|
|
1827
|
-
"- secondaryContentStrategy: explicit guidance for making secondary outputs channel-native, self-contained, and enticing gateways into the primary content.",
|
|
2049
|
+
hasSecondaryContentTypes ? "- secondaryContentStrategy: explicit guidance for making secondary outputs channel-native, self-contained, and enticing gateways into the primary content." : "- secondaryContentStrategy: set to an empty string because this run has no secondary outputs.",
|
|
1828
2050
|
"",
|
|
1829
2051
|
"Return JSON only with all required fields."
|
|
1830
2052
|
].join("\n")
|
|
@@ -1834,6 +2056,15 @@ function buildContentBriefMessages(idea, options) {
|
|
|
1834
2056
|
|
|
1835
2057
|
// src/types/contentBriefSchema.ts
|
|
1836
2058
|
import { z as z4 } from "zod";
|
|
2059
|
+
var secondaryTypeSentinelValues = /* @__PURE__ */ new Set([
|
|
2060
|
+
"none",
|
|
2061
|
+
"n/a",
|
|
2062
|
+
"na",
|
|
2063
|
+
"null",
|
|
2064
|
+
"not applicable",
|
|
2065
|
+
"no secondary content",
|
|
2066
|
+
"no secondary outputs"
|
|
2067
|
+
]);
|
|
1837
2068
|
var contentBriefSchema2 = z4.object({
|
|
1838
2069
|
title: z4.string().min(8),
|
|
1839
2070
|
description: z4.string().min(40),
|
|
@@ -1842,8 +2073,24 @@ var contentBriefSchema2 = z4.object({
|
|
|
1842
2073
|
keyPoints: z4.array(z4.string().min(8)).min(3).max(6),
|
|
1843
2074
|
voiceNotes: z4.string().min(20),
|
|
1844
2075
|
primaryContentType: z4.string().min(2),
|
|
1845
|
-
secondaryContentTypes: z4.array(z4.string().min(2)).max(10),
|
|
1846
|
-
secondaryContentStrategy: z4.string()
|
|
2076
|
+
secondaryContentTypes: z4.array(z4.string().min(2)).max(10).transform((values) => values.map((value2) => value2.trim()).filter((value2) => value2.length > 0).filter((value2) => !secondaryTypeSentinelValues.has(value2.toLowerCase()))),
|
|
2077
|
+
secondaryContentStrategy: z4.string()
|
|
2078
|
+
}).superRefine((brief, ctx) => {
|
|
2079
|
+
const hasSecondaryTargets = brief.secondaryContentTypes.length > 0;
|
|
2080
|
+
if (!hasSecondaryTargets) {
|
|
2081
|
+
return;
|
|
2082
|
+
}
|
|
2083
|
+
if (brief.secondaryContentStrategy.trim().length < 20) {
|
|
2084
|
+
ctx.addIssue({
|
|
2085
|
+
code: z4.ZodIssueCode.too_small,
|
|
2086
|
+
minimum: 20,
|
|
2087
|
+
inclusive: true,
|
|
2088
|
+
origin: "string",
|
|
2089
|
+
path: ["secondaryContentStrategy"],
|
|
2090
|
+
type: "string",
|
|
2091
|
+
message: "Too small: expected string to have >=20 characters"
|
|
2092
|
+
});
|
|
2093
|
+
}
|
|
1847
2094
|
});
|
|
1848
2095
|
|
|
1849
2096
|
// src/generation/planContentBrief.ts
|
|
@@ -1871,7 +2118,7 @@ async function planContentBrief({
|
|
|
1871
2118
|
schemaName: "content_brief",
|
|
1872
2119
|
schema: contentBriefSchema,
|
|
1873
2120
|
messages: buildContentBriefMessages(idea, {
|
|
1874
|
-
|
|
2121
|
+
intent: settings.intent,
|
|
1875
2122
|
targetAudienceHint,
|
|
1876
2123
|
primaryContentType: settings.contentTargets.find((target) => target.role === "primary")?.contentType ?? "article",
|
|
1877
2124
|
secondaryContentTypes: settings.contentTargets.filter((target) => target.role === "secondary").map((target) => target.contentType)
|
|
@@ -1916,13 +2163,19 @@ function deriveTitleFromIdea(idea) {
|
|
|
1916
2163
|
}
|
|
1917
2164
|
|
|
1918
2165
|
// src/llm/prompts/articlePlan.ts
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
2166
|
+
function deriveArticleSectionCounts(targetLengthWords) {
|
|
2167
|
+
const normalizedWords = Number.isFinite(targetLengthWords) && targetLengthWords > 0 ? targetLengthWords : 900;
|
|
2168
|
+
const center = Math.max(2, Math.min(10, Math.round(normalizedWords / 220)));
|
|
2169
|
+
const min = Math.max(2, center - 1);
|
|
2170
|
+
const max = Math.min(10, center + 1);
|
|
2171
|
+
return {
|
|
2172
|
+
min,
|
|
2173
|
+
max,
|
|
2174
|
+
label: `${min} to ${max}`
|
|
2175
|
+
};
|
|
2176
|
+
}
|
|
2177
|
+
function buildArticlePlanJsonSchema(targetLengthWords) {
|
|
2178
|
+
const sectionCounts = deriveArticleSectionCounts(targetLengthWords);
|
|
1926
2179
|
return {
|
|
1927
2180
|
type: "object",
|
|
1928
2181
|
additionalProperties: false,
|
|
@@ -1984,16 +2237,12 @@ function buildArticlePlanJsonSchema(targetLength) {
|
|
|
1984
2237
|
};
|
|
1985
2238
|
}
|
|
1986
2239
|
function buildArticlePlanMessages(idea, options) {
|
|
1987
|
-
const sectionCounts =
|
|
2240
|
+
const sectionCounts = deriveArticleSectionCounts(options.targetLength);
|
|
1988
2241
|
const systemInstruction = [
|
|
1989
2242
|
"You are a senior editorial strategist. Produce a rigorous article plan for a polished long-form Markdown article.",
|
|
1990
|
-
|
|
1991
|
-
buildStyleDirective(options.style),
|
|
2243
|
+
buildArticlePlanGuideInstruction(options.intent, "article"),
|
|
1992
2244
|
buildRunContextDirective(options.contentTypes),
|
|
1993
2245
|
buildTargetLengthDirective("article", options.targetLength),
|
|
1994
|
-
"Quality bar: produce expert-level structure with high information density, concrete mechanisms, and practical reader outcomes.",
|
|
1995
|
-
"Choose an adaptive persuasion structure (AIDA, PAS, or BAB) based on audience need, search intent, and the job-to-be-done of the idea.",
|
|
1996
|
-
"Avoid generic filler, empty wrap-up sentences, and vague claims that do not specify how or why.",
|
|
1997
2246
|
"Return only the requested JSON."
|
|
1998
2247
|
].join(" ");
|
|
1999
2248
|
return [
|
|
@@ -2011,7 +2260,7 @@ function buildArticlePlanMessages(idea, options) {
|
|
|
2011
2260
|
"- The article should feel authoritative, practical, and clearly structured for scanning and deep reading.",
|
|
2012
2261
|
"- Generate a memorable title and a sharp subtitle that promise a concrete benefit, mechanism, or outcome.",
|
|
2013
2262
|
"- The slug must be lowercase kebab-case and publication-ready.",
|
|
2014
|
-
"- The description should work as a concise meta description
|
|
2263
|
+
"- The description should work as a concise meta description and align with the shared content brief.",
|
|
2015
2264
|
`- Plan ${sectionCounts.label} strong sections with distinct focus areas and logical progression (no repetitive section intent).`,
|
|
2016
2265
|
"- Frame section titles to reflect likely search intent or practical reader questions when appropriate.",
|
|
2017
2266
|
"- Each section description should name the mechanism, evidence type, or practical action that makes the section useful.",
|
|
@@ -2019,7 +2268,6 @@ function buildArticlePlanMessages(idea, options) {
|
|
|
2019
2268
|
"- Include a cover image description and 2 to 3 inline image descriptions.",
|
|
2020
2269
|
"- Image descriptions must be concrete and contextual, not generic stock-photo phrasing.",
|
|
2021
2270
|
"- Inline images should be anchored after specific sections using 1-based indexes.",
|
|
2022
|
-
"- Avoid AI giveaway phrasing, dramatic cliches, and generic conclusions that add no new information.",
|
|
2023
2271
|
"",
|
|
2024
2272
|
"Shared content brief context:",
|
|
2025
2273
|
`- description: ${options.contentBrief.description}`,
|
|
@@ -2087,7 +2335,7 @@ async function planArticle({
|
|
|
2087
2335
|
schemaName: "article_plan",
|
|
2088
2336
|
schema: buildArticlePlanJsonSchema(settings.targetLength),
|
|
2089
2337
|
messages: buildArticlePlanMessages(idea, {
|
|
2090
|
-
|
|
2338
|
+
intent: settings.intent,
|
|
2091
2339
|
contentTypes: settings.contentTargets.map((target) => target.contentType),
|
|
2092
2340
|
contentBrief,
|
|
2093
2341
|
targetLength: settings.targetLength
|
|
@@ -2162,40 +2410,17 @@ function slugify(value2) {
|
|
|
2162
2410
|
}
|
|
2163
2411
|
|
|
2164
2412
|
// src/llm/prompts/channelContent.ts
|
|
2165
|
-
|
|
2166
|
-
"
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
].join(" "),
|
|
2175
|
-
"x-post": [
|
|
2176
|
-
"Write native X content with short lines, high signal, and a strong hook in the first line.",
|
|
2177
|
-
"Return one concise post only. Do not return numbered thread lines."
|
|
2178
|
-
].join(" "),
|
|
2179
|
-
"reddit-post": [
|
|
2180
|
-
"Write a Reddit-native post in plain, authentic voice with practical detail and no marketing gloss.",
|
|
2181
|
-
"Use first-hand framing, candid constraints, and only minimal formatting that improves readability."
|
|
2182
|
-
].join(" "),
|
|
2183
|
-
"linkedin-post": [
|
|
2184
|
-
"Write a LinkedIn-native post for professional clarity and engagement.",
|
|
2185
|
-
"Open with a strong two-line hook, use spaced short paragraphs, and end with one focused reflection or CTA."
|
|
2186
|
-
].join(" "),
|
|
2187
|
-
newsletter: [
|
|
2188
|
-
"Write a concise newsletter piece with a subject-line-quality opening and clear section flow.",
|
|
2189
|
-
"Prioritize practical value density, strong transitions, and sustained reader momentum."
|
|
2190
|
-
].join(" "),
|
|
2191
|
-
"landing-page-copy": [
|
|
2192
|
-
"Write landing-page copy in Markdown with headline, value proposition, proof-oriented body blocks, objection handling, and clear CTA text.",
|
|
2193
|
-
"Keep claims specific, credible, and measurable. Avoid hype language."
|
|
2194
|
-
].join(" "),
|
|
2195
|
-
article: "Write a polished Markdown article."
|
|
2196
|
-
};
|
|
2413
|
+
function buildOutputShapeConstraint(contentType) {
|
|
2414
|
+
if (contentType === "x-thread") {
|
|
2415
|
+
return 'Return a numbered thread with one post per line prefixed like "1/7".';
|
|
2416
|
+
}
|
|
2417
|
+
if (contentType === "x-post") {
|
|
2418
|
+
return "Return one concise post only. Do not return numbered thread lines.";
|
|
2419
|
+
}
|
|
2420
|
+
return "";
|
|
2421
|
+
}
|
|
2197
2422
|
function buildSingleShotContentMessages(options) {
|
|
2198
|
-
const
|
|
2423
|
+
const outputShapeConstraint = buildOutputShapeConstraint(options.contentType);
|
|
2199
2424
|
const articleContext = options.articleReferenceMarkdown ? [
|
|
2200
2425
|
"Reference primary context (use as anchor source, but adapt natively for the requested channel):",
|
|
2201
2426
|
options.articleReferenceMarkdown
|
|
@@ -2213,10 +2438,9 @@ function buildSingleShotContentMessages(options) {
|
|
|
2213
2438
|
content: [
|
|
2214
2439
|
"You are a senior content strategist and copywriter.",
|
|
2215
2440
|
`Write exactly one ${options.contentType} output.`,
|
|
2216
|
-
|
|
2217
|
-
buildStyleDirective(options.style),
|
|
2441
|
+
buildChannelContentGuideInstruction(options.style, options.intent, options.contentType),
|
|
2218
2442
|
roleDirective,
|
|
2219
|
-
|
|
2443
|
+
outputShapeConstraint
|
|
2220
2444
|
].join(" ")
|
|
2221
2445
|
},
|
|
2222
2446
|
{
|
|
@@ -2257,6 +2481,7 @@ async function writeSingleShotContent({
|
|
|
2257
2481
|
role = "secondary",
|
|
2258
2482
|
primaryContentType,
|
|
2259
2483
|
style,
|
|
2484
|
+
intent,
|
|
2260
2485
|
outputIndex,
|
|
2261
2486
|
outputCountForType,
|
|
2262
2487
|
articleReferenceMarkdown,
|
|
@@ -2286,6 +2511,7 @@ async function writeSingleShotContent({
|
|
|
2286
2511
|
role,
|
|
2287
2512
|
primaryContentType,
|
|
2288
2513
|
style,
|
|
2514
|
+
intent,
|
|
2289
2515
|
outputIndex,
|
|
2290
2516
|
outputCountForType,
|
|
2291
2517
|
contentBrief,
|
|
@@ -2333,13 +2559,12 @@ var OUTRO_PARAGRAPH_COUNTS = {
|
|
|
2333
2559
|
medium: "2 to 3",
|
|
2334
2560
|
large: "3 to 5"
|
|
2335
2561
|
};
|
|
2336
|
-
function buildSystemInstruction(base, style, contentTypes,
|
|
2562
|
+
function buildSystemInstruction(base, style, intent, contentTypes, targetLengthWords) {
|
|
2337
2563
|
return [
|
|
2338
2564
|
base,
|
|
2339
|
-
|
|
2340
|
-
buildStyleDirective(style),
|
|
2565
|
+
buildArticleSectionGuideInstruction(style, intent, "article"),
|
|
2341
2566
|
buildRunContextDirective(contentTypes),
|
|
2342
|
-
buildTargetLengthDirective("article",
|
|
2567
|
+
buildTargetLengthDirective("article", targetLengthWords)
|
|
2343
2568
|
].join(" ");
|
|
2344
2569
|
}
|
|
2345
2570
|
function sharedPlanContext(plan) {
|
|
@@ -2363,14 +2588,16 @@ function sharedDraftContext(articleSoFar) {
|
|
|
2363
2588
|
normalized
|
|
2364
2589
|
].join("\n");
|
|
2365
2590
|
}
|
|
2366
|
-
function buildIntroMessages(plan, style, contentTypes,
|
|
2591
|
+
function buildIntroMessages(plan, style, intent, contentTypes, targetLengthWords, introTargetWords) {
|
|
2367
2592
|
const baseSystemInstruction = buildSystemInstruction(
|
|
2368
2593
|
"You write polished editorial prose for Markdown articles. Return only the prose body with no heading and no code fences.",
|
|
2369
2594
|
style,
|
|
2595
|
+
intent,
|
|
2370
2596
|
contentTypes,
|
|
2371
|
-
|
|
2597
|
+
targetLengthWords
|
|
2372
2598
|
);
|
|
2373
|
-
const
|
|
2599
|
+
const targetLengthAlias = resolveTargetLengthAlias(targetLengthWords);
|
|
2600
|
+
const paragraphCount = INTRO_PARAGRAPH_COUNTS[targetLengthAlias] ?? INTRO_PARAGRAPH_COUNTS["medium"];
|
|
2374
2601
|
return [
|
|
2375
2602
|
{
|
|
2376
2603
|
role: "system",
|
|
@@ -2384,20 +2611,23 @@ function buildIntroMessages(plan, style, contentTypes, targetLength) {
|
|
|
2384
2611
|
`Write the article introduction using this brief: ${plan.introBrief}`,
|
|
2385
2612
|
"Requirements:",
|
|
2386
2613
|
`- ${paragraphCount} paragraphs.`,
|
|
2614
|
+
`- Target length: about ${introTargetWords} words.`,
|
|
2387
2615
|
"- Hook the reader quickly.",
|
|
2388
2616
|
"- Set up the argument and tone for the rest of the article."
|
|
2389
2617
|
].join("\n")
|
|
2390
2618
|
}
|
|
2391
2619
|
];
|
|
2392
2620
|
}
|
|
2393
|
-
function buildSectionMessages(plan, section, articleSoFar, style, contentTypes,
|
|
2621
|
+
function buildSectionMessages(plan, section, articleSoFar, style, intent, contentTypes, targetLengthWords, sectionTargetWords) {
|
|
2394
2622
|
const baseSystemInstruction = buildSystemInstruction(
|
|
2395
2623
|
"You write in-depth Markdown article sections. Return only the prose body for the section, with no heading and no code fences.",
|
|
2396
2624
|
style,
|
|
2625
|
+
intent,
|
|
2397
2626
|
contentTypes,
|
|
2398
|
-
|
|
2627
|
+
targetLengthWords
|
|
2399
2628
|
);
|
|
2400
|
-
const
|
|
2629
|
+
const targetLengthAlias = resolveTargetLengthAlias(targetLengthWords);
|
|
2630
|
+
const paragraphCount = SECTION_PARAGRAPH_COUNTS[targetLengthAlias] ?? SECTION_PARAGRAPH_COUNTS["medium"];
|
|
2401
2631
|
return [
|
|
2402
2632
|
{
|
|
2403
2633
|
role: "system",
|
|
@@ -2414,6 +2644,7 @@ function buildSectionMessages(plan, section, articleSoFar, style, contentTypes,
|
|
|
2414
2644
|
`Section focus: ${section.description}`,
|
|
2415
2645
|
"Requirements:",
|
|
2416
2646
|
`- ${paragraphCount} paragraphs.`,
|
|
2647
|
+
`- Target length: about ${sectionTargetWords} words.`,
|
|
2417
2648
|
"- Be concrete and specific.",
|
|
2418
2649
|
"- Continue naturally from the article draft so far without rehashing prior sections.",
|
|
2419
2650
|
"- Use short Markdown lists only if they materially improve clarity."
|
|
@@ -2421,14 +2652,16 @@ function buildSectionMessages(plan, section, articleSoFar, style, contentTypes,
|
|
|
2421
2652
|
}
|
|
2422
2653
|
];
|
|
2423
2654
|
}
|
|
2424
|
-
function buildOutroMessages(plan, style, contentTypes,
|
|
2655
|
+
function buildOutroMessages(plan, style, intent, contentTypes, targetLengthWords, outroTargetWords) {
|
|
2425
2656
|
const baseSystemInstruction = buildSystemInstruction(
|
|
2426
2657
|
"You write polished editorial conclusions for Markdown articles. Return only the prose body with no heading and no code fences.",
|
|
2427
2658
|
style,
|
|
2659
|
+
intent,
|
|
2428
2660
|
contentTypes,
|
|
2429
|
-
|
|
2661
|
+
targetLengthWords
|
|
2430
2662
|
);
|
|
2431
|
-
const
|
|
2663
|
+
const targetLengthAlias = resolveTargetLengthAlias(targetLengthWords);
|
|
2664
|
+
const paragraphCount = OUTRO_PARAGRAPH_COUNTS[targetLengthAlias] ?? OUTRO_PARAGRAPH_COUNTS["medium"];
|
|
2432
2665
|
return [
|
|
2433
2666
|
{
|
|
2434
2667
|
role: "system",
|
|
@@ -2442,6 +2675,7 @@ function buildOutroMessages(plan, style, contentTypes, targetLength) {
|
|
|
2442
2675
|
`Write the article conclusion using this brief: ${plan.outroBrief}`,
|
|
2443
2676
|
"Requirements:",
|
|
2444
2677
|
`- ${paragraphCount} paragraphs.`,
|
|
2678
|
+
`- Target length: about ${outroTargetWords} words.`,
|
|
2445
2679
|
"- Synthesize the main argument.",
|
|
2446
2680
|
"- End with a strong, thoughtful closing line."
|
|
2447
2681
|
].join("\n")
|
|
@@ -2459,13 +2693,16 @@ async function writeArticleSections({
|
|
|
2459
2693
|
onLlmMetrics,
|
|
2460
2694
|
onInteraction
|
|
2461
2695
|
}) {
|
|
2696
|
+
const wordBudgets = allocateWordBudgets(settings.targetLength, plan.sections.length);
|
|
2462
2697
|
onSectionStart?.("Writing introduction");
|
|
2463
2698
|
const intro = dryRun || !openRouter ? dryRunIntro(plan) : await openRouter.requestText({
|
|
2464
2699
|
messages: buildIntroMessages(
|
|
2465
2700
|
plan,
|
|
2466
2701
|
settings.style,
|
|
2702
|
+
settings.intent,
|
|
2467
2703
|
settings.contentTargets.map((target) => target.contentType),
|
|
2468
|
-
settings.targetLength
|
|
2704
|
+
settings.targetLength,
|
|
2705
|
+
wordBudgets.intro
|
|
2469
2706
|
),
|
|
2470
2707
|
settings,
|
|
2471
2708
|
interactionContext: {
|
|
@@ -2487,8 +2724,10 @@ async function writeArticleSections({
|
|
|
2487
2724
|
section,
|
|
2488
2725
|
buildArticleSoFarContext(intro, sections),
|
|
2489
2726
|
settings.style,
|
|
2727
|
+
settings.intent,
|
|
2490
2728
|
settings.contentTargets.map((target) => target.contentType),
|
|
2491
|
-
settings.targetLength
|
|
2729
|
+
settings.targetLength,
|
|
2730
|
+
wordBudgets.sections[index] ?? wordBudgets.sections[wordBudgets.sections.length - 1] ?? 150
|
|
2492
2731
|
),
|
|
2493
2732
|
settings,
|
|
2494
2733
|
interactionContext: {
|
|
@@ -2510,8 +2749,10 @@ async function writeArticleSections({
|
|
|
2510
2749
|
messages: buildOutroMessages(
|
|
2511
2750
|
plan,
|
|
2512
2751
|
settings.style,
|
|
2752
|
+
settings.intent,
|
|
2513
2753
|
settings.contentTargets.map((target) => target.contentType),
|
|
2514
|
-
settings.targetLength
|
|
2754
|
+
settings.targetLength,
|
|
2755
|
+
wordBudgets.outro
|
|
2515
2756
|
),
|
|
2516
2757
|
settings,
|
|
2517
2758
|
interactionContext: {
|
|
@@ -2547,6 +2788,23 @@ function dryRunOutro(plan) {
|
|
|
2547
2788
|
"What matters is a workflow that can repeatedly transform a promising idea into a piece that is clear, useful, and worth reading."
|
|
2548
2789
|
].join("\n\n");
|
|
2549
2790
|
}
|
|
2791
|
+
function allocateWordBudgets(totalTargetWords, sectionCount) {
|
|
2792
|
+
const normalizedTotal = Number.isFinite(totalTargetWords) && totalTargetWords > 0 ? Math.round(totalTargetWords) : 900;
|
|
2793
|
+
const normalizedSectionCount = Math.max(1, sectionCount);
|
|
2794
|
+
const intro = Math.max(80, Math.round(normalizedTotal * 0.15));
|
|
2795
|
+
const outro = Math.max(80, Math.round(normalizedTotal * 0.1));
|
|
2796
|
+
const remainingForSections = Math.max(normalizedSectionCount * 120, normalizedTotal - intro - outro);
|
|
2797
|
+
const baseSectionWords = Math.floor(remainingForSections / normalizedSectionCount);
|
|
2798
|
+
let remainder = remainingForSections - baseSectionWords * normalizedSectionCount;
|
|
2799
|
+
const sections = Array.from({ length: normalizedSectionCount }, () => {
|
|
2800
|
+
const next = baseSectionWords + (remainder > 0 ? 1 : 0);
|
|
2801
|
+
if (remainder > 0) {
|
|
2802
|
+
remainder -= 1;
|
|
2803
|
+
}
|
|
2804
|
+
return Math.max(120, next);
|
|
2805
|
+
});
|
|
2806
|
+
return { intro, sections, outro };
|
|
2807
|
+
}
|
|
2550
2808
|
function buildArticleSoFarContext(intro, sections) {
|
|
2551
2809
|
const parts = ["## Introduction", intro.trim()];
|
|
2552
2810
|
for (const section of sections) {
|
|
@@ -2594,6 +2852,14 @@ var ReplicateClient = class {
|
|
|
2594
2852
|
const backoff = backoffMs(attempt);
|
|
2595
2853
|
retries += 1;
|
|
2596
2854
|
retryBackoffMs += backoff;
|
|
2855
|
+
options.onRetry?.({
|
|
2856
|
+
attempts,
|
|
2857
|
+
retries,
|
|
2858
|
+
retryBackoffMs,
|
|
2859
|
+
backoffMs: backoff,
|
|
2860
|
+
errorMessage: lastError.message,
|
|
2861
|
+
modelId: model
|
|
2862
|
+
});
|
|
2597
2863
|
await wait(backoff);
|
|
2598
2864
|
continue;
|
|
2599
2865
|
}
|
|
@@ -2617,7 +2883,7 @@ function wait(ms) {
|
|
|
2617
2883
|
|
|
2618
2884
|
// src/images/renderImages.ts
|
|
2619
2885
|
import { writeFile as writeFile4 } from "fs/promises";
|
|
2620
|
-
import
|
|
2886
|
+
import path6 from "path";
|
|
2621
2887
|
|
|
2622
2888
|
// src/llm/prompts/imagePrompt.ts
|
|
2623
2889
|
var imagePromptSchema = {
|
|
@@ -3296,14 +3562,15 @@ async function renderExpandedImages({
|
|
|
3296
3562
|
dryRun,
|
|
3297
3563
|
onProgress,
|
|
3298
3564
|
onRenderComplete,
|
|
3299
|
-
onInteraction
|
|
3565
|
+
onInteraction,
|
|
3566
|
+
onRetry
|
|
3300
3567
|
}) {
|
|
3301
3568
|
const renderedImages = [];
|
|
3302
3569
|
for (let index = 0; index < prompts.length; index += 1) {
|
|
3303
3570
|
const prompt = prompts[index];
|
|
3304
3571
|
onProgress?.(`Rendering image ${index + 1}/${prompts.length} with ${settings.t2i.modelId}`);
|
|
3305
3572
|
const fileName = `${prompt.kind === "cover" ? "cover" : `inline-${prompt.anchorAfterSection}`}-${index + 1}.${resolveOutputFormat(settings)}`;
|
|
3306
|
-
const outputPath =
|
|
3573
|
+
const outputPath = path6.join(assetDir, fileName);
|
|
3307
3574
|
if (dryRun || !replicate) {
|
|
3308
3575
|
const dryRunStartMs = Date.now();
|
|
3309
3576
|
await writeFile4(outputPath, `Placeholder image for: ${prompt.prompt}
|
|
@@ -3361,6 +3628,14 @@ async function renderExpandedImages({
|
|
|
3361
3628
|
runAttempts = metrics.attempts;
|
|
3362
3629
|
runRetries = metrics.retries;
|
|
3363
3630
|
runRetryBackoffMs = metrics.retryBackoffMs;
|
|
3631
|
+
},
|
|
3632
|
+
onRetry(event) {
|
|
3633
|
+
onRetry?.({
|
|
3634
|
+
imageId: prompt.id,
|
|
3635
|
+
kind: prompt.kind,
|
|
3636
|
+
retries: event.retries,
|
|
3637
|
+
errorMessage: event.errorMessage
|
|
3638
|
+
});
|
|
3364
3639
|
}
|
|
3365
3640
|
});
|
|
3366
3641
|
const bytes = await normalizeReplicateOutput(output);
|
|
@@ -4067,7 +4342,7 @@ ${body.join("\n").trim()}
|
|
|
4067
4342
|
|
|
4068
4343
|
// src/pipeline/sessionStore.ts
|
|
4069
4344
|
import { mkdir as mkdir4, readFile as readFile5, rm as rm2, writeFile as writeFile5 } from "fs/promises";
|
|
4070
|
-
import
|
|
4345
|
+
import path7 from "path";
|
|
4071
4346
|
import { z as z6 } from "zod";
|
|
4072
4347
|
var STAGE_IDS = ["shared-brief", "planning", "sections", "image-prompts", "images", "output", "links"];
|
|
4073
4348
|
var generatedArticleSectionSchema = z6.object({
|
|
@@ -4094,7 +4369,8 @@ var linksResultSchema = z6.object({
|
|
|
4094
4369
|
fileId: z6.string().min(1),
|
|
4095
4370
|
contentType: z6.string().min(1),
|
|
4096
4371
|
markdownPath: z6.string().min(1),
|
|
4097
|
-
links: z6.array(linkEntrySchema)
|
|
4372
|
+
links: z6.array(linkEntrySchema),
|
|
4373
|
+
customLinks: z6.array(linkEntrySchema).default([])
|
|
4098
4374
|
});
|
|
4099
4375
|
var pipelineArtifactSummarySchema = z6.object({
|
|
4100
4376
|
title: z6.string().min(1),
|
|
@@ -4143,10 +4419,10 @@ var writeSessionStateSchema = z6.object({
|
|
|
4143
4419
|
artifact: pipelineArtifactSummarySchema.nullable()
|
|
4144
4420
|
});
|
|
4145
4421
|
function resolveWriteRoot(workingDir) {
|
|
4146
|
-
return
|
|
4422
|
+
return path7.join(workingDir, ".ideon", "write");
|
|
4147
4423
|
}
|
|
4148
4424
|
function resolveStateFilePath(workingDir) {
|
|
4149
|
-
return
|
|
4425
|
+
return path7.join(resolveWriteRoot(workingDir), "state.json");
|
|
4150
4426
|
}
|
|
4151
4427
|
async function startFreshWriteSession(seed, workingDir = process.cwd()) {
|
|
4152
4428
|
const writeRoot = resolveWriteRoot(workingDir);
|
|
@@ -4196,7 +4472,7 @@ async function saveWriteSession(state, workingDir = process.cwd()) {
|
|
|
4196
4472
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4197
4473
|
});
|
|
4198
4474
|
const statePath = resolveStateFilePath(workingDir);
|
|
4199
|
-
await mkdir4(
|
|
4475
|
+
await mkdir4(path7.dirname(statePath), { recursive: true });
|
|
4200
4476
|
await writeFile5(statePath, `${JSON.stringify(next, null, 2)}
|
|
4201
4477
|
`, "utf8");
|
|
4202
4478
|
return next;
|
|
@@ -4289,12 +4565,18 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4289
4565
|
const stages = createInitialStages({ isArticlePrimary });
|
|
4290
4566
|
options.onUpdate?.(cloneStages(stages));
|
|
4291
4567
|
const dryRun = options.dryRun ?? false;
|
|
4292
|
-
const shouldEnrichLinks = options.enrichLinks ??
|
|
4568
|
+
const shouldEnrichLinks = options.enrichLinks ?? false;
|
|
4293
4569
|
const runMode = options.runMode ?? "fresh";
|
|
4294
4570
|
const workingDir = options.workingDir ?? process.cwd();
|
|
4571
|
+
const pipelineCustomLinkRaws = options.customLinks ?? [];
|
|
4572
|
+
const pipelineUnlinks = options.unlinks ?? [];
|
|
4573
|
+
const pipelineMaxLinks = options.maxLinks;
|
|
4295
4574
|
const outputPaths = resolveOutputPaths(input.config.settings, workingDir);
|
|
4296
4575
|
const hasArticlePrimary = isArticlePrimary;
|
|
4297
4576
|
const stageTracking = /* @__PURE__ */ new Map();
|
|
4577
|
+
const stageRetryState = /* @__PURE__ */ new Map();
|
|
4578
|
+
const llmOperationRetryState = /* @__PURE__ */ new Map();
|
|
4579
|
+
const imageOperationRetryState = /* @__PURE__ */ new Map();
|
|
4298
4580
|
stageTracking.set("shared-brief", {
|
|
4299
4581
|
startedAtMs: runStartedAtMs,
|
|
4300
4582
|
endedAtMs: null,
|
|
@@ -4309,6 +4591,41 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4309
4591
|
const llmInteractions = [];
|
|
4310
4592
|
const t2iInteractions = [];
|
|
4311
4593
|
let writeSession;
|
|
4594
|
+
const applyRetryUpdate = (stageId, retryIncrement, errorMessage) => {
|
|
4595
|
+
if (retryIncrement <= 0) {
|
|
4596
|
+
return;
|
|
4597
|
+
}
|
|
4598
|
+
const stageIndex = stages.findIndex((stage) => stage.id === stageId);
|
|
4599
|
+
if (stageIndex < 0) {
|
|
4600
|
+
return;
|
|
4601
|
+
}
|
|
4602
|
+
const existing = stageRetryState.get(stageId) ?? { retries: 0, lastError: null };
|
|
4603
|
+
const next = {
|
|
4604
|
+
retries: existing.retries + retryIncrement,
|
|
4605
|
+
lastError: errorMessage && errorMessage.trim().length > 0 ? errorMessage : existing.lastError
|
|
4606
|
+
};
|
|
4607
|
+
stageRetryState.set(stageId, next);
|
|
4608
|
+
stages[stageIndex] = {
|
|
4609
|
+
...stages[stageIndex],
|
|
4610
|
+
retryCount: next.retries,
|
|
4611
|
+
lastRetryError: next.lastError ?? void 0
|
|
4612
|
+
};
|
|
4613
|
+
options.onUpdate?.(cloneStages(stages));
|
|
4614
|
+
};
|
|
4615
|
+
const onLlmInteraction = (interaction) => {
|
|
4616
|
+
llmInteractions.push(interaction);
|
|
4617
|
+
const stageId = asWriteStageId(interaction.stageId);
|
|
4618
|
+
if (!stageId) {
|
|
4619
|
+
return;
|
|
4620
|
+
}
|
|
4621
|
+
const previousRetries = llmOperationRetryState.get(interaction.operationId) ?? 0;
|
|
4622
|
+
if (interaction.retries <= previousRetries) {
|
|
4623
|
+
return;
|
|
4624
|
+
}
|
|
4625
|
+
const retryIncrement = interaction.retries - previousRetries;
|
|
4626
|
+
llmOperationRetryState.set(interaction.operationId, interaction.retries);
|
|
4627
|
+
applyRetryUpdate(stageId, retryIncrement, interaction.errorMessage);
|
|
4628
|
+
};
|
|
4312
4629
|
if (runMode === "fresh") {
|
|
4313
4630
|
writeSession = await startFreshWriteSession(
|
|
4314
4631
|
{
|
|
@@ -4360,7 +4677,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4360
4677
|
openRouter,
|
|
4361
4678
|
dryRun,
|
|
4362
4679
|
onInteraction(interaction) {
|
|
4363
|
-
|
|
4680
|
+
onLlmInteraction(interaction);
|
|
4364
4681
|
},
|
|
4365
4682
|
onLlmMetrics(metrics) {
|
|
4366
4683
|
recordLlmMetrics(stageTracking, "shared-brief", metrics);
|
|
@@ -4414,7 +4731,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4414
4731
|
openRouter,
|
|
4415
4732
|
dryRun,
|
|
4416
4733
|
onInteraction(interaction) {
|
|
4417
|
-
|
|
4734
|
+
onLlmInteraction(interaction);
|
|
4418
4735
|
},
|
|
4419
4736
|
onLlmMetrics(metrics) {
|
|
4420
4737
|
recordLlmMetrics(stageTracking, "planning", metrics);
|
|
@@ -4477,7 +4794,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4477
4794
|
openRouter,
|
|
4478
4795
|
dryRun,
|
|
4479
4796
|
onInteraction(interaction) {
|
|
4480
|
-
|
|
4797
|
+
onLlmInteraction(interaction);
|
|
4481
4798
|
},
|
|
4482
4799
|
onLlmMetrics(phase, metrics, sectionIndex) {
|
|
4483
4800
|
recordLlmMetrics(stageTracking, "sections", metrics);
|
|
@@ -4590,7 +4907,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4590
4907
|
openRouter,
|
|
4591
4908
|
dryRun,
|
|
4592
4909
|
onInteraction(interaction) {
|
|
4593
|
-
|
|
4910
|
+
onLlmInteraction(interaction);
|
|
4594
4911
|
},
|
|
4595
4912
|
onPromptComplete(metrics) {
|
|
4596
4913
|
imagePromptCalls.push({
|
|
@@ -4676,6 +4993,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4676
4993
|
role: "primary",
|
|
4677
4994
|
primaryContentType: primaryTarget.contentType,
|
|
4678
4995
|
style: input.config.settings.style,
|
|
4996
|
+
intent: input.config.settings.intent,
|
|
4679
4997
|
outputIndex: 1,
|
|
4680
4998
|
outputCountForType: 1,
|
|
4681
4999
|
articleReferenceMarkdown: void 0,
|
|
@@ -4684,7 +5002,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4684
5002
|
openRouter,
|
|
4685
5003
|
dryRun,
|
|
4686
5004
|
onInteraction(interaction) {
|
|
4687
|
-
|
|
5005
|
+
onLlmInteraction(interaction);
|
|
4688
5006
|
},
|
|
4689
5007
|
onLlmMetrics(metrics) {
|
|
4690
5008
|
recordLlmMetrics(stageTracking, "sections", metrics);
|
|
@@ -4728,12 +5046,12 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4728
5046
|
options.onUpdate?.(cloneStages(stages));
|
|
4729
5047
|
}
|
|
4730
5048
|
const baseSlug = plan?.slug ?? slugifyIdea(input.idea);
|
|
4731
|
-
const generationDir =
|
|
5049
|
+
const generationDir = path8.join(
|
|
4732
5050
|
writeSession.outputPaths.markdownOutputDir,
|
|
4733
5051
|
buildGenerationDirectoryName(baseSlug)
|
|
4734
5052
|
);
|
|
4735
5053
|
await mkdir5(generationDir, { recursive: true });
|
|
4736
|
-
const jobDefinitionPath =
|
|
5054
|
+
const jobDefinitionPath = path8.join(generationDir, "job.json");
|
|
4737
5055
|
await writeJsonFile(
|
|
4738
5056
|
jobDefinitionPath,
|
|
4739
5057
|
buildRunJobDefinition({
|
|
@@ -4746,7 +5064,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4746
5064
|
})
|
|
4747
5065
|
);
|
|
4748
5066
|
const primaryFilePrefix = toFilePrefix(primaryTarget.contentType);
|
|
4749
|
-
const primaryMarkdownPath =
|
|
5067
|
+
const primaryMarkdownPath = path8.join(generationDir, `${primaryFilePrefix}-1.md`);
|
|
4750
5068
|
const sharedAssetDir = generationDir;
|
|
4751
5069
|
if (hasArticlePrimary) {
|
|
4752
5070
|
if (imageArtifacts) {
|
|
@@ -4788,6 +5106,15 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4788
5106
|
recordStageCost(stageTracking, "images", metrics.costUsd, metrics.costSource);
|
|
4789
5107
|
addStageRetries(stageTracking, "images", metrics.retries);
|
|
4790
5108
|
},
|
|
5109
|
+
onRetry(event) {
|
|
5110
|
+
const operationKey = `images:${event.imageId}`;
|
|
5111
|
+
const previousRetries = imageOperationRetryState.get(operationKey) ?? 0;
|
|
5112
|
+
if (event.retries <= previousRetries) {
|
|
5113
|
+
return;
|
|
5114
|
+
}
|
|
5115
|
+
imageOperationRetryState.set(operationKey, event.retries);
|
|
5116
|
+
applyRetryUpdate("images", event.retries - previousRetries, event.errorMessage);
|
|
5117
|
+
},
|
|
4791
5118
|
onProgress(detail) {
|
|
4792
5119
|
stages[4] = {
|
|
4793
5120
|
...stages[4],
|
|
@@ -4872,6 +5199,15 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4872
5199
|
recordStageCost(stageTracking, "images", metrics.costUsd, metrics.costSource);
|
|
4873
5200
|
addStageRetries(stageTracking, "images", metrics.retries);
|
|
4874
5201
|
},
|
|
5202
|
+
onRetry(event) {
|
|
5203
|
+
const operationKey = `images:${event.imageId}`;
|
|
5204
|
+
const previousRetries = imageOperationRetryState.get(operationKey) ?? 0;
|
|
5205
|
+
if (event.retries <= previousRetries) {
|
|
5206
|
+
return;
|
|
5207
|
+
}
|
|
5208
|
+
imageOperationRetryState.set(operationKey, event.retries);
|
|
5209
|
+
applyRetryUpdate("images", event.retries - previousRetries, event.errorMessage);
|
|
5210
|
+
},
|
|
4875
5211
|
onProgress(detail) {
|
|
4876
5212
|
stages[4] = {
|
|
4877
5213
|
...stages[4],
|
|
@@ -4969,12 +5305,13 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4969
5305
|
})
|
|
4970
5306
|
};
|
|
4971
5307
|
options.onUpdate?.(cloneStages(stages));
|
|
4972
|
-
const markdownPath =
|
|
5308
|
+
const markdownPath = path8.join(generationDir, `${output.filePrefix}-${output.index}.md`);
|
|
4973
5309
|
try {
|
|
4974
5310
|
const content = await writeSingleShotContent({
|
|
4975
5311
|
idea: input.idea,
|
|
4976
5312
|
contentType: output.contentType,
|
|
4977
5313
|
style: input.config.settings.style,
|
|
5314
|
+
intent: input.config.settings.intent,
|
|
4978
5315
|
outputIndex: output.index,
|
|
4979
5316
|
outputCountForType: output.outputCountForType,
|
|
4980
5317
|
articleReferenceMarkdown: primaryMarkdownTemplate ?? void 0,
|
|
@@ -4985,7 +5322,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4985
5322
|
role: "secondary",
|
|
4986
5323
|
primaryContentType: primaryTarget.contentType,
|
|
4987
5324
|
onInteraction(interaction) {
|
|
4988
|
-
|
|
5325
|
+
onLlmInteraction(interaction);
|
|
4989
5326
|
},
|
|
4990
5327
|
onLlmMetrics(metrics) {
|
|
4991
5328
|
recordLlmMetrics(stageTracking, "output", metrics);
|
|
@@ -5030,7 +5367,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5030
5367
|
...item,
|
|
5031
5368
|
status: "succeeded",
|
|
5032
5369
|
detail: "Saved markdown output.",
|
|
5033
|
-
summary:
|
|
5370
|
+
summary: path8.basename(markdownPath),
|
|
5034
5371
|
analytics: {
|
|
5035
5372
|
durationMs: itemDurationMs,
|
|
5036
5373
|
costUsd: knownItemCost.usd,
|
|
@@ -5086,7 +5423,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5086
5423
|
stages[6] = {
|
|
5087
5424
|
...stages[6],
|
|
5088
5425
|
status: "succeeded",
|
|
5089
|
-
detail: "Skipped link enrichment (--
|
|
5426
|
+
detail: "Skipped link enrichment (enable with --enrich-links).",
|
|
5090
5427
|
summary: "Link enrichment disabled for this run",
|
|
5091
5428
|
stageAnalytics: snapshotStageAnalytics(stageTracking, "links")
|
|
5092
5429
|
};
|
|
@@ -5103,15 +5440,19 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5103
5440
|
options.onUpdate?.(cloneStages(stages));
|
|
5104
5441
|
} else if (linksResult) {
|
|
5105
5442
|
const linksByFileId = new Map(linksResult.map((item) => [item.fileId, item.links]));
|
|
5106
|
-
|
|
5443
|
+
const customLinksByFileId = new Map(linksResult.map((item) => [item.fileId, item.customLinks]));
|
|
5444
|
+
const resumedLinks = eligibleOutputsForLinks.map((output) => ({
|
|
5107
5445
|
fileId: output.fileId,
|
|
5108
5446
|
contentType: output.contentType,
|
|
5109
5447
|
markdownPath: output.markdownPath,
|
|
5110
|
-
links: linksByFileId.get(output.fileId) ?? []
|
|
5448
|
+
links: linksByFileId.get(output.fileId) ?? [],
|
|
5449
|
+
customLinks: customLinksByFileId.get(output.fileId) ?? []
|
|
5111
5450
|
}));
|
|
5112
|
-
|
|
5451
|
+
linksResult = resumedLinks;
|
|
5452
|
+
for (const item of resumedLinks) {
|
|
5113
5453
|
await writeLinksFile(item.markdownPath, {
|
|
5114
|
-
version:
|
|
5454
|
+
version: 2,
|
|
5455
|
+
customLinks: item.customLinks,
|
|
5115
5456
|
links: item.links
|
|
5116
5457
|
});
|
|
5117
5458
|
}
|
|
@@ -5120,7 +5461,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5120
5461
|
...stages[6],
|
|
5121
5462
|
status: "succeeded",
|
|
5122
5463
|
detail: "Reused saved link metadata from .ideon/write.",
|
|
5123
|
-
summary: `${
|
|
5464
|
+
summary: `${resumedLinks.reduce((sum, item) => sum + item.links.length, 0)} links`,
|
|
5124
5465
|
items: (stages[6].items ?? []).map((item) => ({
|
|
5125
5466
|
...item,
|
|
5126
5467
|
status: "succeeded",
|
|
@@ -5138,8 +5479,10 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5138
5479
|
openRouter,
|
|
5139
5480
|
settings: input.config.settings,
|
|
5140
5481
|
dryRun,
|
|
5482
|
+
customLinks: parsePipelineCustomLinks(pipelineCustomLinkRaws, pipelineUnlinks),
|
|
5483
|
+
maxLinks: pipelineMaxLinks ?? resolveDefaultMaxLinks(input.config.settings.targetLength),
|
|
5141
5484
|
onInteraction(interaction) {
|
|
5142
|
-
|
|
5485
|
+
onLlmInteraction(interaction);
|
|
5143
5486
|
},
|
|
5144
5487
|
onLlmMetrics(fileId, metrics) {
|
|
5145
5488
|
recordLlmMetrics(stageTracking, "links", metrics);
|
|
@@ -5182,7 +5525,8 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5182
5525
|
costSource
|
|
5183
5526
|
});
|
|
5184
5527
|
await writeLinksFile(item.markdownPath, {
|
|
5185
|
-
version:
|
|
5528
|
+
version: 2,
|
|
5529
|
+
customLinks: item.customLinks,
|
|
5186
5530
|
links: item.links
|
|
5187
5531
|
});
|
|
5188
5532
|
}
|
|
@@ -5238,8 +5582,8 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5238
5582
|
llmCalls: llmInteractions,
|
|
5239
5583
|
t2iCalls: t2iInteractions
|
|
5240
5584
|
};
|
|
5241
|
-
const analyticsPath =
|
|
5242
|
-
const interactionsPath =
|
|
5585
|
+
const analyticsPath = path8.join(generationDir, "generation.analytics.json");
|
|
5586
|
+
const interactionsPath = path8.join(generationDir, "model.interactions.json");
|
|
5243
5587
|
await writeJsonFile(analyticsPath, analytics);
|
|
5244
5588
|
await writeJsonFile(interactionsPath, interactions);
|
|
5245
5589
|
const primaryMarkdownPathForArtifact = markdownPaths[0] ?? primaryMarkdownPath;
|
|
@@ -5547,7 +5891,6 @@ function toFilePrefix(contentType) {
|
|
|
5547
5891
|
if (contentType === "reddit-post") return "reddit";
|
|
5548
5892
|
if (contentType === "linkedin-post") return "linkedin";
|
|
5549
5893
|
if (contentType === "newsletter") return "newsletter";
|
|
5550
|
-
if (contentType === "landing-page-copy") return "landing";
|
|
5551
5894
|
return contentType.replace(/[^a-z0-9]+/gi, "-").replace(/^-+|-+$/g, "").toLowerCase() || "content";
|
|
5552
5895
|
}
|
|
5553
5896
|
function getPrimaryTarget(contentTargets) {
|
|
@@ -5643,6 +5986,24 @@ function asWriteStageId(stageId) {
|
|
|
5643
5986
|
}
|
|
5644
5987
|
return null;
|
|
5645
5988
|
}
|
|
5989
|
+
function parsePipelineCustomLinks(rawLinks, unlinks) {
|
|
5990
|
+
const result = /* @__PURE__ */ new Map();
|
|
5991
|
+
for (const raw of rawLinks) {
|
|
5992
|
+
const separatorIndex = raw.indexOf("->");
|
|
5993
|
+
if (separatorIndex < 0) {
|
|
5994
|
+
continue;
|
|
5995
|
+
}
|
|
5996
|
+
const expression = raw.slice(0, separatorIndex).trim();
|
|
5997
|
+
const url = raw.slice(separatorIndex + 2).trim();
|
|
5998
|
+
if (expression && url) {
|
|
5999
|
+
result.set(expression.toLowerCase(), { expression, url, title: null });
|
|
6000
|
+
}
|
|
6001
|
+
}
|
|
6002
|
+
for (const expr of unlinks) {
|
|
6003
|
+
result.delete(expr.trim().toLowerCase());
|
|
6004
|
+
}
|
|
6005
|
+
return Array.from(result.values());
|
|
6006
|
+
}
|
|
5646
6007
|
|
|
5647
6008
|
// src/cli/commands/writeTargetSpecs.ts
|
|
5648
6009
|
function parseTargetSpec(spec) {
|
|
@@ -5931,6 +6292,268 @@ async function runMcpServeCommand() {
|
|
|
5931
6292
|
await startIdeonMcpServer();
|
|
5932
6293
|
}
|
|
5933
6294
|
|
|
6295
|
+
// src/cli/commands/links.ts
|
|
6296
|
+
import { readFile as readFile6, stat as stat3 } from "fs/promises";
|
|
6297
|
+
import path9 from "path";
|
|
6298
|
+
async function runLinksCommand(options, dependencies = {}) {
|
|
6299
|
+
const slug = normalizeSlug2(options.slug);
|
|
6300
|
+
const mode = normalizeMode(options.mode);
|
|
6301
|
+
const cwd2 = dependencies.cwd ?? process.cwd();
|
|
6302
|
+
const log = dependencies.log ?? ((message) => console.log(message));
|
|
6303
|
+
const resolved = await resolveRunInput({
|
|
6304
|
+
idea: `Enrich links for ${slug}`
|
|
6305
|
+
});
|
|
6306
|
+
const markdownPath = await resolveMarkdownPathForSlug2(resolved.config.settings, slug, cwd2);
|
|
6307
|
+
const frontmatter = await readFrontmatter(markdownPath);
|
|
6308
|
+
const fileId = path9.parse(markdownPath).name;
|
|
6309
|
+
const articleTitle = frontmatter.title ?? toTitleFromSlug(slug);
|
|
6310
|
+
const articleDescription = frontmatter.description ?? "";
|
|
6311
|
+
const openRouterApiKey = resolved.config.secrets.openRouterApiKey;
|
|
6312
|
+
if (!openRouterApiKey) {
|
|
6313
|
+
throw new ReportedError(
|
|
6314
|
+
"Missing OpenRouter API key. Run `ideon settings` to configure credentials or set IDEON_OPENROUTER_API_KEY."
|
|
6315
|
+
);
|
|
6316
|
+
}
|
|
6317
|
+
const openRouter = new OpenRouterClient(openRouterApiKey);
|
|
6318
|
+
const linksPath = resolveLinksPath(markdownPath);
|
|
6319
|
+
const existing = await readExistingLinks(linksPath);
|
|
6320
|
+
const updatedCustomLinks = resolveCustomLinks(existing?.customLinks ?? [], options.links ?? [], options.unlinks ?? []);
|
|
6321
|
+
const effectiveMaxLinks = options.maxLinks;
|
|
6322
|
+
const linksResult = await enrichLinks({
|
|
6323
|
+
markdownFiles: [{ markdownPath, fileId, contentType: "article" }],
|
|
6324
|
+
articleTitle,
|
|
6325
|
+
articleDescription,
|
|
6326
|
+
openRouter,
|
|
6327
|
+
settings: resolved.config.settings,
|
|
6328
|
+
dryRun: false,
|
|
6329
|
+
customLinks: updatedCustomLinks,
|
|
6330
|
+
maxLinks: effectiveMaxLinks,
|
|
6331
|
+
onItemProgress(event) {
|
|
6332
|
+
logProgress(event, log);
|
|
6333
|
+
}
|
|
6334
|
+
});
|
|
6335
|
+
const generatedLinks = linksResult[0]?.links ?? [];
|
|
6336
|
+
const mergedGeneratedLinks = mode === "append" ? mergeLinks(existing?.links ?? [], generatedLinks) : generatedLinks;
|
|
6337
|
+
const appendedCount = Math.max(0, mergedGeneratedLinks.length - (existing?.links.length ?? 0));
|
|
6338
|
+
await writeLinksFile(markdownPath, {
|
|
6339
|
+
version: 2,
|
|
6340
|
+
customLinks: updatedCustomLinks,
|
|
6341
|
+
links: mergedGeneratedLinks
|
|
6342
|
+
});
|
|
6343
|
+
const relativeMarkdownPath = formatRelativePath2(cwd2, markdownPath);
|
|
6344
|
+
const relativeLinksPath = formatRelativePath2(cwd2, linksPath);
|
|
6345
|
+
if (mode === "fresh") {
|
|
6346
|
+
const replaced = existing ? "Replaced existing links." : "Created links sidecar.";
|
|
6347
|
+
log(`Enriched links for "${slug}".`);
|
|
6348
|
+
log(`${replaced} Saved ${generatedLinks.length} generated + ${updatedCustomLinks.length} custom links to ${relativeLinksPath} (${relativeMarkdownPath}).`);
|
|
6349
|
+
return;
|
|
6350
|
+
}
|
|
6351
|
+
const baseCount = existing?.links.length ?? 0;
|
|
6352
|
+
const verb = existing ? "Appended and deduplicated links." : "Created links sidecar.";
|
|
6353
|
+
log(`Enriched links for "${slug}".`);
|
|
6354
|
+
log(`${verb} Base ${baseCount}, added ${appendedCount}, total ${mergedGeneratedLinks.length} generated + ${updatedCustomLinks.length} custom in ${relativeLinksPath} (${relativeMarkdownPath}).`);
|
|
6355
|
+
}
|
|
6356
|
+
function normalizeMode(rawMode) {
|
|
6357
|
+
const normalized = rawMode.trim().toLowerCase();
|
|
6358
|
+
if (normalized === "fresh" || normalized === "append") {
|
|
6359
|
+
return normalized;
|
|
6360
|
+
}
|
|
6361
|
+
throw new ReportedError(`Unsupported --mode value "${rawMode}". Use "fresh" or "append".`);
|
|
6362
|
+
}
|
|
6363
|
+
function normalizeSlug2(rawSlug) {
|
|
6364
|
+
const slug = rawSlug.trim();
|
|
6365
|
+
if (!slug) {
|
|
6366
|
+
throw new ReportedError("Slug cannot be empty. Pass the generated article slug, for example `ideon links my-article-slug`.");
|
|
6367
|
+
}
|
|
6368
|
+
if (slug.toLowerCase().endsWith(".md")) {
|
|
6369
|
+
throw new ReportedError(`Expected a slug, not a markdown filename: ${slug}. Pass the slug without .md.`);
|
|
6370
|
+
}
|
|
6371
|
+
if (slug === "." || slug === ".." || /[/\\]/.test(slug)) {
|
|
6372
|
+
throw new ReportedError(`Invalid slug "${slug}". Pass the article slug only, without any path separators.`);
|
|
6373
|
+
}
|
|
6374
|
+
return slug;
|
|
6375
|
+
}
|
|
6376
|
+
async function resolveMarkdownPathForSlug2(settings, slug, cwd2) {
|
|
6377
|
+
const outputPaths = resolveOutputPaths(settings, cwd2);
|
|
6378
|
+
const directPath = path9.join(outputPaths.markdownOutputDir, `${slug}.md`);
|
|
6379
|
+
if (await isReadableFile(directPath)) {
|
|
6380
|
+
return directPath;
|
|
6381
|
+
}
|
|
6382
|
+
const markdownFiles = await listMarkdownFilesRecursively(outputPaths.markdownOutputDir);
|
|
6383
|
+
const matches = [];
|
|
6384
|
+
for (const candidate of markdownFiles) {
|
|
6385
|
+
if (path9.basename(candidate) === `${slug}.md`) {
|
|
6386
|
+
matches.push(candidate);
|
|
6387
|
+
continue;
|
|
6388
|
+
}
|
|
6389
|
+
const frontmatter = await readFrontmatter(candidate);
|
|
6390
|
+
if (frontmatter.slug === slug) {
|
|
6391
|
+
matches.push(candidate);
|
|
6392
|
+
}
|
|
6393
|
+
}
|
|
6394
|
+
if (matches.length === 0) {
|
|
6395
|
+
throw new ReportedError(
|
|
6396
|
+
`Could not find article "${slug}". Expected a markdown file in ${outputPaths.markdownOutputDir} with frontmatter slug "${slug}".`
|
|
6397
|
+
);
|
|
6398
|
+
}
|
|
6399
|
+
return newestPath(matches);
|
|
6400
|
+
}
|
|
6401
|
+
async function newestPath(paths) {
|
|
6402
|
+
let latestPath = paths[0];
|
|
6403
|
+
let latestMtime = 0;
|
|
6404
|
+
for (const candidate of paths) {
|
|
6405
|
+
const candidateStat = await stat3(candidate);
|
|
6406
|
+
if (candidateStat.mtimeMs >= latestMtime) {
|
|
6407
|
+
latestMtime = candidateStat.mtimeMs;
|
|
6408
|
+
latestPath = candidate;
|
|
6409
|
+
}
|
|
6410
|
+
}
|
|
6411
|
+
return latestPath;
|
|
6412
|
+
}
|
|
6413
|
+
async function readFrontmatter(markdownPath) {
|
|
6414
|
+
const markdown = await readFile6(markdownPath, "utf8");
|
|
6415
|
+
return parseFrontmatter(markdown);
|
|
6416
|
+
}
|
|
6417
|
+
function parseFrontmatter(markdown) {
|
|
6418
|
+
if (!markdown.startsWith("---\n")) {
|
|
6419
|
+
return { slug: null, title: null, description: null };
|
|
6420
|
+
}
|
|
6421
|
+
const frontmatterEnd = markdown.indexOf("\n---\n", 4);
|
|
6422
|
+
if (frontmatterEnd < 0) {
|
|
6423
|
+
return { slug: null, title: null, description: null };
|
|
6424
|
+
}
|
|
6425
|
+
const block = markdown.slice(4, frontmatterEnd);
|
|
6426
|
+
return {
|
|
6427
|
+
slug: parseFrontmatterValue(block, "slug"),
|
|
6428
|
+
title: parseFrontmatterValue(block, "title"),
|
|
6429
|
+
description: parseFrontmatterValue(block, "description")
|
|
6430
|
+
};
|
|
6431
|
+
}
|
|
6432
|
+
function parseFrontmatterValue(block, key) {
|
|
6433
|
+
const pattern = new RegExp(`^${key}:\\s*(.+)$`, "m");
|
|
6434
|
+
const match = block.match(pattern);
|
|
6435
|
+
if (!match || !match[1]) {
|
|
6436
|
+
return null;
|
|
6437
|
+
}
|
|
6438
|
+
const rawValue = match[1].trim();
|
|
6439
|
+
if (!rawValue) {
|
|
6440
|
+
return null;
|
|
6441
|
+
}
|
|
6442
|
+
if (rawValue.startsWith('"') && rawValue.endsWith('"') || rawValue.startsWith("'") && rawValue.endsWith("'")) {
|
|
6443
|
+
return rawValue.slice(1, -1);
|
|
6444
|
+
}
|
|
6445
|
+
return rawValue;
|
|
6446
|
+
}
|
|
6447
|
+
function toTitleFromSlug(slug) {
|
|
6448
|
+
return slug.split("-").filter((part) => part.length > 0).map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ");
|
|
6449
|
+
}
|
|
6450
|
+
async function isReadableFile(filePath) {
|
|
6451
|
+
try {
|
|
6452
|
+
const fileStat = await stat3(filePath);
|
|
6453
|
+
return fileStat.isFile();
|
|
6454
|
+
} catch {
|
|
6455
|
+
return false;
|
|
6456
|
+
}
|
|
6457
|
+
}
|
|
6458
|
+
async function readExistingLinks(linksPath) {
|
|
6459
|
+
try {
|
|
6460
|
+
const raw = await readFile6(linksPath, "utf8");
|
|
6461
|
+
const parsed = JSON.parse(raw);
|
|
6462
|
+
const links = Array.isArray(parsed.links) ? parsed.links.filter((entry) => isValidLinkEntry(entry)).map((entry) => ({
|
|
6463
|
+
expression: entry.expression.trim(),
|
|
6464
|
+
url: entry.url.trim(),
|
|
6465
|
+
title: typeof entry.title === "string" ? entry.title : null
|
|
6466
|
+
})) : null;
|
|
6467
|
+
if (!links) {
|
|
6468
|
+
throw new ReportedError(`Invalid links sidecar format at ${linksPath}. Expected { version, links[] }.`);
|
|
6469
|
+
}
|
|
6470
|
+
const customLinks = Array.isArray(parsed.customLinks) ? parsed.customLinks.filter((entry) => isValidLinkEntry(entry)).map((entry) => ({
|
|
6471
|
+
expression: entry.expression.trim(),
|
|
6472
|
+
url: entry.url.trim(),
|
|
6473
|
+
title: typeof entry.title === "string" ? entry.title : null
|
|
6474
|
+
})) : [];
|
|
6475
|
+
return {
|
|
6476
|
+
version: typeof parsed.version === "number" ? parsed.version : 1,
|
|
6477
|
+
customLinks,
|
|
6478
|
+
links
|
|
6479
|
+
};
|
|
6480
|
+
} catch (error) {
|
|
6481
|
+
if (readErrorCode2(error) === "ENOENT") {
|
|
6482
|
+
return null;
|
|
6483
|
+
}
|
|
6484
|
+
if (error instanceof ReportedError) {
|
|
6485
|
+
throw error;
|
|
6486
|
+
}
|
|
6487
|
+
const message = error instanceof Error ? error.message : "Unknown links sidecar read error.";
|
|
6488
|
+
throw new ReportedError(`Failed to read existing links from ${linksPath}: ${message}`);
|
|
6489
|
+
}
|
|
6490
|
+
}
|
|
6491
|
+
function mergeLinks(existingLinks, generatedLinks) {
|
|
6492
|
+
const merged = [];
|
|
6493
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6494
|
+
for (const entry of [...existingLinks, ...generatedLinks]) {
|
|
6495
|
+
const key = `${entry.expression.trim().toLowerCase()}::${entry.url.trim().toLowerCase()}`;
|
|
6496
|
+
if (seen.has(key)) {
|
|
6497
|
+
continue;
|
|
6498
|
+
}
|
|
6499
|
+
seen.add(key);
|
|
6500
|
+
merged.push(entry);
|
|
6501
|
+
}
|
|
6502
|
+
return merged;
|
|
6503
|
+
}
|
|
6504
|
+
function isValidLinkEntry(value2) {
|
|
6505
|
+
if (typeof value2 !== "object" || value2 === null) {
|
|
6506
|
+
return false;
|
|
6507
|
+
}
|
|
6508
|
+
const record = value2;
|
|
6509
|
+
return typeof record.expression === "string" && typeof record.url === "string" && (typeof record.title === "string" || record.title === null || record.title === void 0);
|
|
6510
|
+
}
|
|
6511
|
+
function readErrorCode2(error) {
|
|
6512
|
+
if (typeof error !== "object" || error === null || !("code" in error)) {
|
|
6513
|
+
return null;
|
|
6514
|
+
}
|
|
6515
|
+
const code = error.code;
|
|
6516
|
+
return typeof code === "string" ? code : null;
|
|
6517
|
+
}
|
|
6518
|
+
function formatRelativePath2(cwd2, targetPath) {
|
|
6519
|
+
const relativePath = path9.relative(cwd2, targetPath);
|
|
6520
|
+
return relativePath.length > 0 ? relativePath : targetPath;
|
|
6521
|
+
}
|
|
6522
|
+
function logProgress(event, log) {
|
|
6523
|
+
if (event.phase === "resolving-expression" || event.phase === "selecting-expressions") {
|
|
6524
|
+
return;
|
|
6525
|
+
}
|
|
6526
|
+
log(event.detail);
|
|
6527
|
+
}
|
|
6528
|
+
function parseCustomLinkFlag(raw) {
|
|
6529
|
+
const separatorIndex = raw.indexOf("->");
|
|
6530
|
+
if (separatorIndex < 0) {
|
|
6531
|
+
throw new ReportedError(`Invalid --link value "${raw}". Expected format: "expression->url".`);
|
|
6532
|
+
}
|
|
6533
|
+
const expression = raw.slice(0, separatorIndex).trim();
|
|
6534
|
+
const url = raw.slice(separatorIndex + 2).trim();
|
|
6535
|
+
if (!expression) {
|
|
6536
|
+
throw new ReportedError(`Invalid --link value "${raw}": expression (left side of ->) cannot be empty.`);
|
|
6537
|
+
}
|
|
6538
|
+
if (!url) {
|
|
6539
|
+
throw new ReportedError(`Invalid --link value "${raw}": url (right side of ->) cannot be empty.`);
|
|
6540
|
+
}
|
|
6541
|
+
return { expression, url };
|
|
6542
|
+
}
|
|
6543
|
+
function resolveCustomLinks(existing, addRaw, removeExpressions) {
|
|
6544
|
+
const result = new Map(
|
|
6545
|
+
existing.map((entry) => [entry.expression.trim().toLowerCase(), entry])
|
|
6546
|
+
);
|
|
6547
|
+
for (const raw of addRaw) {
|
|
6548
|
+
const { expression, url } = parseCustomLinkFlag(raw);
|
|
6549
|
+
result.set(expression.toLowerCase(), { expression, url, title: null });
|
|
6550
|
+
}
|
|
6551
|
+
for (const expr of removeExpressions) {
|
|
6552
|
+
result.delete(expr.trim().toLowerCase());
|
|
6553
|
+
}
|
|
6554
|
+
return Array.from(result.values());
|
|
6555
|
+
}
|
|
6556
|
+
|
|
5934
6557
|
// src/cli/commands/settings.tsx
|
|
5935
6558
|
import { render } from "ink";
|
|
5936
6559
|
|
|
@@ -6303,14 +6926,14 @@ async function openSettings() {
|
|
|
6303
6926
|
}
|
|
6304
6927
|
|
|
6305
6928
|
// src/cli/commands/serve.ts
|
|
6306
|
-
import
|
|
6929
|
+
import path12 from "path";
|
|
6307
6930
|
import { spawn } from "child_process";
|
|
6308
6931
|
|
|
6309
6932
|
// src/server/previewHelpers.ts
|
|
6310
|
-
import { readdir, stat as
|
|
6311
|
-
import
|
|
6933
|
+
import { readdir, stat as stat4, readFile as readFile7 } from "fs/promises";
|
|
6934
|
+
import path10 from "path";
|
|
6312
6935
|
var DEFAULT_PORT = 4173;
|
|
6313
|
-
var CONTENT_TYPE_ORDER = ["article", "blog-post", "x-thread", "x-post", "linkedin-post", "reddit-post", "newsletter"
|
|
6936
|
+
var CONTENT_TYPE_ORDER = ["article", "blog-post", "x-thread", "x-post", "linkedin-post", "reddit-post", "newsletter"];
|
|
6314
6937
|
var FILE_PREFIX_TO_CONTENT_TYPE = {
|
|
6315
6938
|
article: "article",
|
|
6316
6939
|
blog: "blog-post",
|
|
@@ -6319,8 +6942,7 @@ var FILE_PREFIX_TO_CONTENT_TYPE = {
|
|
|
6319
6942
|
x: "x-post",
|
|
6320
6943
|
reddit: "reddit-post",
|
|
6321
6944
|
linkedin: "linkedin-post",
|
|
6322
|
-
newsletter: "newsletter"
|
|
6323
|
-
landing: "landing-page-copy"
|
|
6945
|
+
newsletter: "newsletter"
|
|
6324
6946
|
};
|
|
6325
6947
|
var CONTENT_TYPE_LABELS = {
|
|
6326
6948
|
article: "Article",
|
|
@@ -6329,8 +6951,7 @@ var CONTENT_TYPE_LABELS = {
|
|
|
6329
6951
|
"x-post": "X Post",
|
|
6330
6952
|
"reddit-post": "Reddit Post",
|
|
6331
6953
|
"linkedin-post": "LinkedIn Post",
|
|
6332
|
-
newsletter: "Newsletter"
|
|
6333
|
-
"landing-page-copy": "Landing Page Copy"
|
|
6954
|
+
newsletter: "Newsletter"
|
|
6334
6955
|
};
|
|
6335
6956
|
function parsePort(portOption) {
|
|
6336
6957
|
if (!portOption) {
|
|
@@ -6368,8 +6989,8 @@ function extractHeadingTitle(markdown) {
|
|
|
6368
6989
|
}
|
|
6369
6990
|
async function resolveMarkdownPath(markdownPathArg, markdownOutputDir, cwd2) {
|
|
6370
6991
|
if (markdownPathArg) {
|
|
6371
|
-
const resolved =
|
|
6372
|
-
if (
|
|
6992
|
+
const resolved = path10.isAbsolute(markdownPathArg) ? markdownPathArg : path10.resolve(cwd2, markdownPathArg);
|
|
6993
|
+
if (path10.extname(resolved).toLowerCase() !== ".md") {
|
|
6373
6994
|
throw new Error(`Expected a markdown file (.md), received: ${resolved}`);
|
|
6374
6995
|
}
|
|
6375
6996
|
await assertFileExists(resolved, "Could not find markdown file");
|
|
@@ -6387,7 +7008,7 @@ async function resolveLatestMarkdown(markdownOutputDir) {
|
|
|
6387
7008
|
let latestPath = markdownCandidates[0];
|
|
6388
7009
|
let latestMtime = 0;
|
|
6389
7010
|
for (const candidate of markdownCandidates) {
|
|
6390
|
-
const fileStat = await
|
|
7011
|
+
const fileStat = await stat4(candidate);
|
|
6391
7012
|
if (fileStat.mtimeMs >= latestMtime) {
|
|
6392
7013
|
latestMtime = fileStat.mtimeMs;
|
|
6393
7014
|
latestPath = candidate;
|
|
@@ -6397,7 +7018,7 @@ async function resolveLatestMarkdown(markdownOutputDir) {
|
|
|
6397
7018
|
}
|
|
6398
7019
|
async function assertFileExists(filePath, errorPrefix) {
|
|
6399
7020
|
try {
|
|
6400
|
-
const fileStat = await
|
|
7021
|
+
const fileStat = await stat4(filePath);
|
|
6401
7022
|
if (!fileStat.isFile()) {
|
|
6402
7023
|
throw new Error(`${errorPrefix}: ${filePath}`);
|
|
6403
7024
|
}
|
|
@@ -6411,9 +7032,9 @@ function extractCoverImageUrl(markdown) {
|
|
|
6411
7032
|
return match?.[1] ?? null;
|
|
6412
7033
|
}
|
|
6413
7034
|
async function extractArticleMetadata(markdownPath) {
|
|
6414
|
-
const markdown = await
|
|
6415
|
-
const fileStat = await
|
|
6416
|
-
const slug = extractFrontmatterSlug(markdown) ??
|
|
7035
|
+
const markdown = await readFile7(markdownPath, "utf8");
|
|
7036
|
+
const fileStat = await stat4(markdownPath);
|
|
7037
|
+
const slug = extractFrontmatterSlug(markdown) ?? path10.basename(markdownPath, ".md");
|
|
6417
7038
|
const title = extractHeadingTitle(stripFrontmatter2(markdown)) ?? slug;
|
|
6418
7039
|
const body = stripFrontmatter2(markdown);
|
|
6419
7040
|
const previewSnippet = body.replace(/[#\[\]()!\-*_`]/g, "").trim().substring(0, 150);
|
|
@@ -6478,16 +7099,16 @@ async function listAllGenerations(markdownOutputDir) {
|
|
|
6478
7099
|
return generations;
|
|
6479
7100
|
}
|
|
6480
7101
|
function deriveGenerationId(markdownPath, markdownOutputDir) {
|
|
6481
|
-
const relative =
|
|
6482
|
-
const normalized = relative.split(
|
|
7102
|
+
const relative = path10.relative(markdownOutputDir, markdownPath);
|
|
7103
|
+
const normalized = relative.split(path10.sep).join("/");
|
|
6483
7104
|
if (!normalized || normalized.startsWith("../")) {
|
|
6484
|
-
return
|
|
7105
|
+
return path10.basename(markdownPath, ".md");
|
|
6485
7106
|
}
|
|
6486
7107
|
const segments = normalized.split("/").filter(Boolean);
|
|
6487
7108
|
if (segments.length <= 1) {
|
|
6488
|
-
return
|
|
7109
|
+
return path10.basename(markdownPath, ".md");
|
|
6489
7110
|
}
|
|
6490
|
-
return segments[0] ??
|
|
7111
|
+
return segments[0] ?? path10.basename(markdownPath, ".md");
|
|
6491
7112
|
}
|
|
6492
7113
|
async function findMarkdownFiles(markdownOutputDir) {
|
|
6493
7114
|
const files = [];
|
|
@@ -6504,7 +7125,7 @@ async function findMarkdownFiles(markdownOutputDir) {
|
|
|
6504
7125
|
continue;
|
|
6505
7126
|
}
|
|
6506
7127
|
for (const entry of entries) {
|
|
6507
|
-
const fullPath =
|
|
7128
|
+
const fullPath = path10.join(current, entry.name);
|
|
6508
7129
|
if (entry.isDirectory()) {
|
|
6509
7130
|
stack.push(fullPath);
|
|
6510
7131
|
continue;
|
|
@@ -6518,7 +7139,7 @@ async function findMarkdownFiles(markdownOutputDir) {
|
|
|
6518
7139
|
}
|
|
6519
7140
|
function deriveOutputIdentity(markdownPath, markdownOutputDir) {
|
|
6520
7141
|
const generationId = deriveGenerationId(markdownPath, markdownOutputDir);
|
|
6521
|
-
const fileBase =
|
|
7142
|
+
const fileBase = path10.basename(markdownPath, ".md");
|
|
6522
7143
|
const parsed = fileBase.match(/^([a-z0-9-]+)-(\d+)$/i);
|
|
6523
7144
|
if (!parsed || !parsed[1] || !parsed[2]) {
|
|
6524
7145
|
return {
|
|
@@ -6554,13 +7175,13 @@ function toContentTypeLabel(contentType) {
|
|
|
6554
7175
|
}
|
|
6555
7176
|
async function resolvePrimaryContentType(outputs) {
|
|
6556
7177
|
const fallback = outputs.find((output) => output.contentType === "article")?.contentType ?? outputs[0]?.contentType ?? "article";
|
|
6557
|
-
const generationDir =
|
|
7178
|
+
const generationDir = path10.dirname(outputs[0]?.sourcePath ?? "");
|
|
6558
7179
|
if (!generationDir) {
|
|
6559
7180
|
return fallback;
|
|
6560
7181
|
}
|
|
6561
|
-
const jobPath =
|
|
7182
|
+
const jobPath = path10.join(generationDir, "job.json");
|
|
6562
7183
|
try {
|
|
6563
|
-
const raw = await
|
|
7184
|
+
const raw = await readFile7(jobPath, "utf8");
|
|
6564
7185
|
const parsed = JSON.parse(raw);
|
|
6565
7186
|
const targets = Array.isArray(parsed.contentTargets) ? parsed.contentTargets : Array.isArray(parsed.settings?.contentTargets) ? parsed.settings.contentTargets : [];
|
|
6566
7187
|
const primary = targets.find((target) => target?.role === "primary" && typeof target.contentType === "string");
|
|
@@ -6576,9 +7197,9 @@ async function resolvePrimaryContentType(outputs) {
|
|
|
6576
7197
|
// src/server/previewServer.ts
|
|
6577
7198
|
import { execFile } from "child_process";
|
|
6578
7199
|
import { promisify } from "util";
|
|
6579
|
-
import { readFile as
|
|
7200
|
+
import { readFile as readFile8, stat as stat5 } from "fs/promises";
|
|
6580
7201
|
import { watch as fsWatch } from "fs";
|
|
6581
|
-
import
|
|
7202
|
+
import path11 from "path";
|
|
6582
7203
|
import { fileURLToPath } from "url";
|
|
6583
7204
|
import express from "express";
|
|
6584
7205
|
import { marked } from "marked";
|
|
@@ -6731,7 +7352,7 @@ async function startPreviewServer(options) {
|
|
|
6731
7352
|
if (options.watch) {
|
|
6732
7353
|
let html2;
|
|
6733
7354
|
try {
|
|
6734
|
-
html2 = await
|
|
7355
|
+
html2 = await readFile8(path11.join(previewClientDir, "index.html"), "utf8");
|
|
6735
7356
|
} catch {
|
|
6736
7357
|
res.status(200).type("html").send(
|
|
6737
7358
|
`<!doctype html><html><head><meta charset="utf-8"><title>Rebuilding\u2026</title><style>body{margin:0;display:flex;align-items:center;justify-content:center;height:100vh;font-family:sans-serif;background:#101820;color:#e0eaf0}p{font-size:15px;opacity:.7}</style></head><body><p>Rebuilding\u2026</p><script>const s=new EventSource('/api/__reload');s.onmessage=function(){location.reload()};</script></body></html>`
|
|
@@ -6742,7 +7363,7 @@ async function startPreviewServer(options) {
|
|
|
6742
7363
|
const injected = html2.replace("</body>", `${reloadScript}</body>`);
|
|
6743
7364
|
res.status(200).type("html").send(injected);
|
|
6744
7365
|
} else {
|
|
6745
|
-
res.status(200).sendFile(
|
|
7366
|
+
res.status(200).sendFile(path11.join(previewClientDir, "index.html"));
|
|
6746
7367
|
}
|
|
6747
7368
|
return;
|
|
6748
7369
|
}
|
|
@@ -6795,7 +7416,7 @@ async function getArticleContent(generationId, markdownOutputDir) {
|
|
|
6795
7416
|
generation.outputs.map(async (output) => {
|
|
6796
7417
|
let markdown = "";
|
|
6797
7418
|
try {
|
|
6798
|
-
markdown = await
|
|
7419
|
+
markdown = await readFile8(output.sourcePath, "utf8");
|
|
6799
7420
|
} catch (error) {
|
|
6800
7421
|
if (isMissingFileError(error)) {
|
|
6801
7422
|
throw new MissingArticleError(`Generation "${generationId}" no longer exists.`);
|
|
@@ -6813,7 +7434,7 @@ async function getArticleContent(generationId, markdownOutputDir) {
|
|
|
6813
7434
|
};
|
|
6814
7435
|
})
|
|
6815
7436
|
);
|
|
6816
|
-
const generationDir =
|
|
7437
|
+
const generationDir = path11.dirname(generation.outputs[0]?.sourcePath ?? "");
|
|
6817
7438
|
const interactions = generationDir ? await loadSavedInteractions(generationDir) : { llmCalls: [], t2iCalls: [] };
|
|
6818
7439
|
const analyticsSummary = generationDir ? await loadSavedAnalyticsSummary(generationDir) : null;
|
|
6819
7440
|
return {
|
|
@@ -6842,7 +7463,7 @@ async function resolveActivePreviewArticle(preferredMarkdownPath, markdownOutput
|
|
|
6842
7463
|
};
|
|
6843
7464
|
}
|
|
6844
7465
|
function resolveGenerationSourcePath(generation, markdownOutputDir) {
|
|
6845
|
-
return generation.outputs.find((output) => output.contentType === generation.primaryContentType)?.sourcePath ?? generation.outputs[0]?.sourcePath ??
|
|
7466
|
+
return generation.outputs.find((output) => output.contentType === generation.primaryContentType)?.sourcePath ?? generation.outputs[0]?.sourcePath ?? path11.join(markdownOutputDir, generation.id);
|
|
6846
7467
|
}
|
|
6847
7468
|
function isMissingFileError(error) {
|
|
6848
7469
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
@@ -6857,7 +7478,7 @@ async function renderArticleHtml(markdown, generationId, sourcePath) {
|
|
|
6857
7478
|
async function loadSavedLinks(markdownPath) {
|
|
6858
7479
|
const linksPath = resolveLinksPath(markdownPath);
|
|
6859
7480
|
try {
|
|
6860
|
-
const raw = await
|
|
7481
|
+
const raw = await readFile8(linksPath, "utf8");
|
|
6861
7482
|
const parsed = JSON.parse(raw);
|
|
6862
7483
|
if (!Array.isArray(parsed.links)) {
|
|
6863
7484
|
return [];
|
|
@@ -6881,9 +7502,9 @@ async function loadSavedLinks(markdownPath) {
|
|
|
6881
7502
|
}
|
|
6882
7503
|
}
|
|
6883
7504
|
async function loadSavedInteractions(generationDir) {
|
|
6884
|
-
const interactionsPath =
|
|
7505
|
+
const interactionsPath = path11.join(generationDir, "model.interactions.json");
|
|
6885
7506
|
try {
|
|
6886
|
-
const raw = await
|
|
7507
|
+
const raw = await readFile8(interactionsPath, "utf8");
|
|
6887
7508
|
const parsed = JSON.parse(raw);
|
|
6888
7509
|
const llmCalls = Array.isArray(parsed.llmCalls) ? parsed.llmCalls.filter(isPreviewLlmInteraction) : [];
|
|
6889
7510
|
const t2iCalls = Array.isArray(parsed.t2iCalls) ? parsed.t2iCalls.filter(isPreviewT2IInteraction) : [];
|
|
@@ -6899,9 +7520,9 @@ async function loadSavedInteractions(generationDir) {
|
|
|
6899
7520
|
}
|
|
6900
7521
|
}
|
|
6901
7522
|
async function loadSavedAnalyticsSummary(generationDir) {
|
|
6902
|
-
const analyticsPath =
|
|
7523
|
+
const analyticsPath = path11.join(generationDir, "generation.analytics.json");
|
|
6903
7524
|
try {
|
|
6904
|
-
const raw = await
|
|
7525
|
+
const raw = await readFile8(analyticsPath, "utf8");
|
|
6905
7526
|
const parsed = JSON.parse(raw);
|
|
6906
7527
|
const summary = parsed.summary;
|
|
6907
7528
|
if (!summary || typeof summary !== "object") {
|
|
@@ -6933,14 +7554,14 @@ async function getPreviewBootstrapData(preferredMarkdownPath, markdownOutputDir)
|
|
|
6933
7554
|
};
|
|
6934
7555
|
}
|
|
6935
7556
|
async function resolvePreviewClientBuildDir() {
|
|
6936
|
-
const currentDir =
|
|
7557
|
+
const currentDir = path11.dirname(fileURLToPath(import.meta.url));
|
|
6937
7558
|
const candidates = [
|
|
6938
|
-
|
|
6939
|
-
|
|
7559
|
+
path11.resolve(currentDir, "preview"),
|
|
7560
|
+
path11.resolve(currentDir, "../../dist/preview")
|
|
6940
7561
|
];
|
|
6941
7562
|
for (const candidate of candidates) {
|
|
6942
7563
|
try {
|
|
6943
|
-
const indexStat = await
|
|
7564
|
+
const indexStat = await stat5(path11.join(candidate, "index.html"));
|
|
6944
7565
|
if (indexStat.isFile()) {
|
|
6945
7566
|
return candidate;
|
|
6946
7567
|
}
|
|
@@ -7002,21 +7623,21 @@ async function resolveGenerationAssetPath(generationId, rawAssetPath, markdownOu
|
|
|
7002
7623
|
throw new MissingArticleError(`Generation "${generationId}" no longer exists.`);
|
|
7003
7624
|
}
|
|
7004
7625
|
const decodedAssetPath = decodeURIComponent(rawAssetPath);
|
|
7005
|
-
const normalizedRelative =
|
|
7006
|
-
if (normalizedRelative.length === 0 || normalizedRelative === "." || normalizedRelative.startsWith("../") || normalizedRelative.includes("/../") ||
|
|
7626
|
+
const normalizedRelative = path11.posix.normalize(decodedAssetPath.replace(/\\/g, "/"));
|
|
7627
|
+
if (normalizedRelative.length === 0 || normalizedRelative === "." || normalizedRelative.startsWith("../") || normalizedRelative.includes("/../") || path11.posix.isAbsolute(normalizedRelative)) {
|
|
7007
7628
|
throw new Error("Invalid generation asset path.");
|
|
7008
7629
|
}
|
|
7009
|
-
const generationDir =
|
|
7630
|
+
const generationDir = path11.dirname(generation.outputs[0]?.sourcePath ?? "");
|
|
7010
7631
|
if (!generationDir) {
|
|
7011
7632
|
throw new MissingArticleError(`Generation "${generationId}" has no source directory.`);
|
|
7012
7633
|
}
|
|
7013
|
-
const resolvedPath =
|
|
7014
|
-
const relativeToGeneration =
|
|
7015
|
-
if (relativeToGeneration.startsWith("..") ||
|
|
7634
|
+
const resolvedPath = path11.resolve(generationDir, normalizedRelative);
|
|
7635
|
+
const relativeToGeneration = path11.relative(generationDir, resolvedPath);
|
|
7636
|
+
if (relativeToGeneration.startsWith("..") || path11.isAbsolute(relativeToGeneration)) {
|
|
7016
7637
|
throw new Error("Invalid generation asset path.");
|
|
7017
7638
|
}
|
|
7018
7639
|
try {
|
|
7019
|
-
const fileStat = await
|
|
7640
|
+
const fileStat = await stat5(resolvedPath);
|
|
7020
7641
|
if (!fileStat.isFile()) {
|
|
7021
7642
|
throw new Error("Invalid generation asset path.");
|
|
7022
7643
|
}
|
|
@@ -7088,10 +7709,6 @@ function renderShell({
|
|
|
7088
7709
|
--newsletter-bg: #fffdf4;
|
|
7089
7710
|
--newsletter-header-bg: #fff5cc;
|
|
7090
7711
|
--newsletter-border: #cfb95a;
|
|
7091
|
-
--landing-bg: linear-gradient(155deg, #10395c 0%, #3d7fa0 100%);
|
|
7092
|
-
--landing-text: #f8fdff;
|
|
7093
|
-
--landing-link: #d7f0ff;
|
|
7094
|
-
--landing-border: rgba(255, 255, 255, 0.3);
|
|
7095
7712
|
color-scheme: light;
|
|
7096
7713
|
}
|
|
7097
7714
|
|
|
@@ -7131,10 +7748,6 @@ function renderShell({
|
|
|
7131
7748
|
--newsletter-bg: #2e291b;
|
|
7132
7749
|
--newsletter-header-bg: #3b331e;
|
|
7133
7750
|
--newsletter-border: #d6b25f;
|
|
7134
|
-
--landing-bg: linear-gradient(155deg, #0f2236 0%, #245d7e 100%);
|
|
7135
|
-
--landing-text: #e7f4ff;
|
|
7136
|
-
--landing-link: #b8e4ff;
|
|
7137
|
-
--landing-border: rgba(220, 239, 255, 0.35);
|
|
7138
7751
|
color-scheme: dark;
|
|
7139
7752
|
}
|
|
7140
7753
|
}
|
|
@@ -7174,10 +7787,6 @@ function renderShell({
|
|
|
7174
7787
|
--newsletter-bg: #fffdf4;
|
|
7175
7788
|
--newsletter-header-bg: #fff5cc;
|
|
7176
7789
|
--newsletter-border: #cfb95a;
|
|
7177
|
-
--landing-bg: linear-gradient(155deg, #10395c 0%, #3d7fa0 100%);
|
|
7178
|
-
--landing-text: #f8fdff;
|
|
7179
|
-
--landing-link: #d7f0ff;
|
|
7180
|
-
--landing-border: rgba(255, 255, 255, 0.3);
|
|
7181
7790
|
color-scheme: light;
|
|
7182
7791
|
}
|
|
7183
7792
|
|
|
@@ -7216,10 +7825,6 @@ function renderShell({
|
|
|
7216
7825
|
--newsletter-bg: #2e291b;
|
|
7217
7826
|
--newsletter-header-bg: #3b331e;
|
|
7218
7827
|
--newsletter-border: #d6b25f;
|
|
7219
|
-
--landing-bg: linear-gradient(155deg, #0f2236 0%, #245d7e 100%);
|
|
7220
|
-
--landing-text: #e7f4ff;
|
|
7221
|
-
--landing-link: #b8e4ff;
|
|
7222
|
-
--landing-border: rgba(220, 239, 255, 0.35);
|
|
7223
7828
|
color-scheme: dark;
|
|
7224
7829
|
}
|
|
7225
7830
|
|
|
@@ -7739,21 +8344,6 @@ function renderShell({
|
|
|
7739
8344
|
background: var(--newsletter-header-bg);
|
|
7740
8345
|
}
|
|
7741
8346
|
|
|
7742
|
-
.channel-landing-page-copy {
|
|
7743
|
-
background: var(--landing-bg);
|
|
7744
|
-
color: var(--landing-text);
|
|
7745
|
-
border: none;
|
|
7746
|
-
}
|
|
7747
|
-
|
|
7748
|
-
.channel-landing-page-copy .channel-header {
|
|
7749
|
-
border-bottom: 1px solid var(--landing-border);
|
|
7750
|
-
}
|
|
7751
|
-
|
|
7752
|
-
.channel-landing-page-copy .channel-meta,
|
|
7753
|
-
.channel-landing-page-copy a {
|
|
7754
|
-
color: var(--landing-link);
|
|
7755
|
-
}
|
|
7756
|
-
|
|
7757
8347
|
.channel-article,
|
|
7758
8348
|
.channel-blog-post {
|
|
7759
8349
|
background: var(--paper);
|
|
@@ -7939,7 +8529,7 @@ function renderShell({
|
|
|
7939
8529
|
const articleElement = document.getElementById('article');
|
|
7940
8530
|
const articleListElement = document.getElementById('articleList');
|
|
7941
8531
|
const themeToggleButton = document.getElementById('themeToggle');
|
|
7942
|
-
const typeOrder = ['article', 'blog-post', 'x-thread', 'x-post', 'linkedin-post', 'reddit-post', 'newsletter'
|
|
8532
|
+
const typeOrder = ['article', 'blog-post', 'x-thread', 'x-post', 'linkedin-post', 'reddit-post', 'newsletter'];
|
|
7943
8533
|
const stageOrder = ['shared-brief', 'planning', 'sections', 'image-prompts', 'images', 'output', 'links'];
|
|
7944
8534
|
|
|
7945
8535
|
let currentGeneration = null;
|
|
@@ -8527,7 +9117,7 @@ async function runServeCommand(options) {
|
|
|
8527
9117
|
const markdownPath = await resolveMarkdownPath(options.markdownPath, outputPaths.markdownOutputDir, process.cwd());
|
|
8528
9118
|
const port = parsePort(options.port);
|
|
8529
9119
|
if (options.watch) {
|
|
8530
|
-
const viteBin =
|
|
9120
|
+
const viteBin = path12.resolve(process.cwd(), "node_modules", ".bin", "vite");
|
|
8531
9121
|
const viteProcess = spawn(viteBin, ["build", "--watch"], {
|
|
8532
9122
|
stdio: "inherit",
|
|
8533
9123
|
shell: process.platform === "win32"
|
|
@@ -8553,8 +9143,8 @@ async function runServeCommand(options) {
|
|
|
8553
9143
|
openBrowser: options.openBrowser,
|
|
8554
9144
|
watch: options.watch
|
|
8555
9145
|
});
|
|
8556
|
-
const relativeArticle =
|
|
8557
|
-
const relativeAssets =
|
|
9146
|
+
const relativeArticle = path12.relative(process.cwd(), markdownPath);
|
|
9147
|
+
const relativeAssets = path12.relative(process.cwd(), outputPaths.assetOutputDir);
|
|
8558
9148
|
console.log(`Previewing ${relativeArticle || markdownPath}`);
|
|
8559
9149
|
console.log(`Serving assets from ${relativeAssets || outputPaths.assetOutputDir}`);
|
|
8560
9150
|
console.log(`Open ${server.url}`);
|
|
@@ -8594,11 +9184,28 @@ function colon(id) {
|
|
|
8594
9184
|
function value(id, text) {
|
|
8595
9185
|
return { id, text, color: "white" };
|
|
8596
9186
|
}
|
|
9187
|
+
function formatStageCost(costUsd, costSource) {
|
|
9188
|
+
const formatted = formatCost(costUsd);
|
|
9189
|
+
if (costUsd === null) {
|
|
9190
|
+
return formatted;
|
|
9191
|
+
}
|
|
9192
|
+
return costSource === "estimated" ? `~${formatted}` : formatted;
|
|
9193
|
+
}
|
|
9194
|
+
function formatStageId(stageId) {
|
|
9195
|
+
if (stageId === "shared-brief") return "shared-brief";
|
|
9196
|
+
if (stageId === "planning") return "planning";
|
|
9197
|
+
if (stageId === "sections") return "sections";
|
|
9198
|
+
if (stageId === "image-prompts") return "image-prompts";
|
|
9199
|
+
if (stageId === "images") return "images";
|
|
9200
|
+
if (stageId === "output") return "output";
|
|
9201
|
+
if (stageId === "links") return "links";
|
|
9202
|
+
return stageId;
|
|
9203
|
+
}
|
|
8597
9204
|
function buildFinalSummaryRows({
|
|
8598
9205
|
artifact,
|
|
8599
9206
|
analytics
|
|
8600
9207
|
}) {
|
|
8601
|
-
|
|
9208
|
+
const rows = [
|
|
8602
9209
|
{
|
|
8603
9210
|
id: "slug",
|
|
8604
9211
|
segments: [
|
|
@@ -8648,6 +9255,16 @@ function buildFinalSummaryRows({
|
|
|
8648
9255
|
]
|
|
8649
9256
|
}
|
|
8650
9257
|
];
|
|
9258
|
+
const stageCostRows = analytics.stages.map((stage) => ({
|
|
9259
|
+
id: `stage-cost:${stage.stageId}`,
|
|
9260
|
+
segments: [
|
|
9261
|
+
label(`stage-cost-label:${stage.stageId}`, `cost/${formatStageId(stage.stageId)}`, "greenBright"),
|
|
9262
|
+
colon(`stage-cost-colon:${stage.stageId}`),
|
|
9263
|
+
value(`stage-cost-value:${stage.stageId}`, formatStageCost(stage.costUsd, stage.costSource))
|
|
9264
|
+
]
|
|
9265
|
+
}));
|
|
9266
|
+
rows.push(...stageCostRows);
|
|
9267
|
+
return rows;
|
|
8651
9268
|
}
|
|
8652
9269
|
|
|
8653
9270
|
// src/cli/ui/finalSummary.tsx
|
|
@@ -8715,7 +9332,7 @@ function formatDuration(durationMs) {
|
|
|
8715
9332
|
}
|
|
8716
9333
|
return `${durationMs}ms`;
|
|
8717
9334
|
}
|
|
8718
|
-
function
|
|
9335
|
+
function formatStageCost2(stage) {
|
|
8719
9336
|
const analytics = stage.stageAnalytics;
|
|
8720
9337
|
if (!analytics || analytics.costUsd === null) {
|
|
8721
9338
|
return "no cost data";
|
|
@@ -8723,6 +9340,15 @@ function formatStageCost(stage) {
|
|
|
8723
9340
|
const formatted = `$${analytics.costUsd.toFixed(4)}`;
|
|
8724
9341
|
return analytics.costSource === "estimated" ? `~${formatted}` : formatted;
|
|
8725
9342
|
}
|
|
9343
|
+
function formatRetryContext(stage) {
|
|
9344
|
+
if (!stage.retryCount || stage.retryCount <= 0) {
|
|
9345
|
+
return "";
|
|
9346
|
+
}
|
|
9347
|
+
if (stage.lastRetryError) {
|
|
9348
|
+
return ` \u2022 retried ${stage.retryCount}x \u2022 last error: ${stage.lastRetryError}`;
|
|
9349
|
+
}
|
|
9350
|
+
return ` \u2022 retried ${stage.retryCount}x`;
|
|
9351
|
+
}
|
|
8726
9352
|
function StageRow({
|
|
8727
9353
|
stage,
|
|
8728
9354
|
isActive,
|
|
@@ -8748,7 +9374,10 @@ function StageRow({
|
|
|
8748
9374
|
/* @__PURE__ */ jsx4(Text3, { children: " " }),
|
|
8749
9375
|
/* @__PURE__ */ jsx4(Text3, { bold: stage.status === "running", children: stage.title })
|
|
8750
9376
|
] }),
|
|
8751
|
-
/* @__PURE__ */ jsx4(Box3, { marginLeft: 2, children: /* @__PURE__ */
|
|
9377
|
+
/* @__PURE__ */ jsx4(Box3, { marginLeft: 2, children: /* @__PURE__ */ jsxs3(Text3, { color: "gray", children: [
|
|
9378
|
+
stage.detail,
|
|
9379
|
+
formatRetryContext(stage)
|
|
9380
|
+
] }) }),
|
|
8752
9381
|
/* @__PURE__ */ jsx4(
|
|
8753
9382
|
ItemRows,
|
|
8754
9383
|
{
|
|
@@ -8763,7 +9392,7 @@ function StageRow({
|
|
|
8763
9392
|
"analytics: ",
|
|
8764
9393
|
formatDuration(stage.stageAnalytics.durationMs),
|
|
8765
9394
|
" \u2022 cost: ",
|
|
8766
|
-
|
|
9395
|
+
formatStageCost2(stage)
|
|
8767
9396
|
] }) }) : null
|
|
8768
9397
|
] });
|
|
8769
9398
|
}
|
|
@@ -8947,7 +9576,7 @@ function formatDuration2(durationMs) {
|
|
|
8947
9576
|
}
|
|
8948
9577
|
return `${durationMs}ms`;
|
|
8949
9578
|
}
|
|
8950
|
-
function
|
|
9579
|
+
function formatStageCost3(stage) {
|
|
8951
9580
|
const analytics = stage.stageAnalytics;
|
|
8952
9581
|
if (!analytics) {
|
|
8953
9582
|
return "unavailable";
|
|
@@ -8959,9 +9588,19 @@ function formatStage(stage) {
|
|
|
8959
9588
|
const summary = stage.summary ? `
|
|
8960
9589
|
${stage.summary}` : "";
|
|
8961
9590
|
const analytics = stage.status === "succeeded" && stage.stageAnalytics ? `
|
|
8962
|
-
analytics: ${formatDuration2(stage.stageAnalytics.durationMs)} \u2022 cost: ${
|
|
9591
|
+
analytics: ${formatDuration2(stage.stageAnalytics.durationMs)} \u2022 cost: ${formatStageCost3(stage)}` : "";
|
|
9592
|
+
const retryContext = formatRetryContext2(stage);
|
|
8963
9593
|
return `[${stage.status}] ${stage.title}
|
|
8964
|
-
${stage.detail}${summary}${analytics}`;
|
|
9594
|
+
${stage.detail}${retryContext}${summary}${analytics}`;
|
|
9595
|
+
}
|
|
9596
|
+
function formatRetryContext2(stage) {
|
|
9597
|
+
if (!stage.retryCount || stage.retryCount <= 0) {
|
|
9598
|
+
return "";
|
|
9599
|
+
}
|
|
9600
|
+
if (stage.lastRetryError) {
|
|
9601
|
+
return ` \u2022 retried ${stage.retryCount}x \u2022 last error: ${stage.lastRetryError}`;
|
|
9602
|
+
}
|
|
9603
|
+
return ` \u2022 retried ${stage.retryCount}x`;
|
|
8965
9604
|
}
|
|
8966
9605
|
function formatItem(stage, item) {
|
|
8967
9606
|
const summary = item.summary ? `
|
|
@@ -8984,8 +9623,15 @@ function formatCost2(costUsd) {
|
|
|
8984
9623
|
}
|
|
8985
9624
|
return `$${costUsd.toFixed(4)}`;
|
|
8986
9625
|
}
|
|
8987
|
-
|
|
8988
|
-
|
|
9626
|
+
function formatPipelineStageCost(stage) {
|
|
9627
|
+
const formatted = formatCost2(stage.costUsd);
|
|
9628
|
+
if (stage.costUsd === null) {
|
|
9629
|
+
return formatted;
|
|
9630
|
+
}
|
|
9631
|
+
return stage.costSource === "estimated" ? `~${formatted}` : formatted;
|
|
9632
|
+
}
|
|
9633
|
+
async function renderPlainPipeline(input, dryRun, enrichLinks2, runMode, links, unlinks, maxLinks) {
|
|
9634
|
+
let previousStages = /* @__PURE__ */ new Map();
|
|
8989
9635
|
let previousItemStatuses = /* @__PURE__ */ new Map();
|
|
8990
9636
|
const notificationsEnabled = input.config.settings.notifications.enabled;
|
|
8991
9637
|
try {
|
|
@@ -8998,12 +9644,21 @@ async function renderPlainPipeline(input, dryRun, enrichLinks2, runMode) {
|
|
|
8998
9644
|
dryRun,
|
|
8999
9645
|
enrichLinks: enrichLinks2,
|
|
9000
9646
|
runMode,
|
|
9647
|
+
customLinks: links,
|
|
9648
|
+
unlinks,
|
|
9649
|
+
maxLinks,
|
|
9001
9650
|
onUpdate(stages) {
|
|
9002
9651
|
for (const stage of stages) {
|
|
9003
|
-
const previous =
|
|
9004
|
-
|
|
9652
|
+
const previous = previousStages.get(stage.id);
|
|
9653
|
+
const shouldLogStage = !previous || previous.status !== stage.status || stage.status === "running" && (previous.detail !== stage.detail || previous.retryCount !== stage.retryCount || previous.lastRetryError !== stage.lastRetryError);
|
|
9654
|
+
if (shouldLogStage) {
|
|
9005
9655
|
console.log(formatStage(stage));
|
|
9006
|
-
|
|
9656
|
+
previousStages.set(stage.id, {
|
|
9657
|
+
status: stage.status,
|
|
9658
|
+
detail: stage.detail,
|
|
9659
|
+
retryCount: stage.retryCount,
|
|
9660
|
+
lastRetryError: stage.lastRetryError
|
|
9661
|
+
});
|
|
9007
9662
|
}
|
|
9008
9663
|
for (const item of stage.items ?? []) {
|
|
9009
9664
|
const itemKey = `${stage.id}:${item.id}`;
|
|
@@ -9029,6 +9684,10 @@ async function renderPlainPipeline(input, dryRun, enrichLinks2, runMode) {
|
|
|
9029
9684
|
console.log(` duration_ms: ${result.analytics.summary.totalDurationMs}`);
|
|
9030
9685
|
console.log(` retries: ${result.analytics.summary.totalRetries}`);
|
|
9031
9686
|
console.log(` cost: ${formatCost2(result.analytics.summary.totalCostUsd)}`);
|
|
9687
|
+
console.log(" cost_by_stage:");
|
|
9688
|
+
for (const stage of result.analytics.stages) {
|
|
9689
|
+
console.log(` ${stage.stageId}: ${formatPipelineStageCost(stage)}`);
|
|
9690
|
+
}
|
|
9032
9691
|
await notifyWriteSucceeded({
|
|
9033
9692
|
enabled: notificationsEnabled,
|
|
9034
9693
|
title: result.artifact.title,
|
|
@@ -9053,9 +9712,11 @@ import TextInput2 from "ink-text-input";
|
|
|
9053
9712
|
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
9054
9713
|
function WriteOptionsFlow({
|
|
9055
9714
|
askStyle,
|
|
9715
|
+
askIntent,
|
|
9056
9716
|
askTargets,
|
|
9057
9717
|
askLength,
|
|
9058
9718
|
initialStyle,
|
|
9719
|
+
initialIntent,
|
|
9059
9720
|
initialTargetLength,
|
|
9060
9721
|
initialTargets,
|
|
9061
9722
|
onDone
|
|
@@ -9064,6 +9725,7 @@ function WriteOptionsFlow({
|
|
|
9064
9725
|
const [step, setStep] = useState3(() => {
|
|
9065
9726
|
if (askTargets) return "primary";
|
|
9066
9727
|
if (askStyle) return "style";
|
|
9728
|
+
if (askIntent) return "intent";
|
|
9067
9729
|
if (askLength) return "length";
|
|
9068
9730
|
return "primary";
|
|
9069
9731
|
});
|
|
@@ -9089,6 +9751,7 @@ function WriteOptionsFlow({
|
|
|
9089
9751
|
const [countInput, setCountInput] = useState3("1");
|
|
9090
9752
|
const [countIndex, setCountIndex] = useState3(0);
|
|
9091
9753
|
const [style, setStyle] = useState3(initialStyle);
|
|
9754
|
+
const [intent, setIntent] = useState3(initialIntent);
|
|
9092
9755
|
const [targetLength, setTargetLength] = useState3(initialTargetLength);
|
|
9093
9756
|
const selectedSecondaryTypes = useMemo2(
|
|
9094
9757
|
() => secondarySelections.filter((item) => item.checked).map((item) => item.contentType),
|
|
@@ -9215,6 +9878,8 @@ function WriteOptionsFlow({
|
|
|
9215
9878
|
if (nextIndex >= countTypes.length) {
|
|
9216
9879
|
if (askStyle) {
|
|
9217
9880
|
setStep("style");
|
|
9881
|
+
} else if (askIntent) {
|
|
9882
|
+
setStep("intent");
|
|
9218
9883
|
} else if (askLength) {
|
|
9219
9884
|
setStep("length");
|
|
9220
9885
|
} else {
|
|
@@ -9252,6 +9917,10 @@ function WriteOptionsFlow({
|
|
|
9252
9917
|
onSelect: (item) => {
|
|
9253
9918
|
const contentTargets = askTargets ? buildContentTargets(primaryType, selectedSecondaryTypes) : void 0;
|
|
9254
9919
|
setStyle(item.value);
|
|
9920
|
+
if (askIntent) {
|
|
9921
|
+
setStep("intent");
|
|
9922
|
+
return;
|
|
9923
|
+
}
|
|
9255
9924
|
if (askLength) {
|
|
9256
9925
|
setStep("length");
|
|
9257
9926
|
return;
|
|
@@ -9266,6 +9935,37 @@ function WriteOptionsFlow({
|
|
|
9266
9935
|
) })
|
|
9267
9936
|
] });
|
|
9268
9937
|
}
|
|
9938
|
+
const intentItems = contentIntentValues.map((value2) => ({
|
|
9939
|
+
label: value2,
|
|
9940
|
+
value: value2
|
|
9941
|
+
}));
|
|
9942
|
+
if (step === "intent") {
|
|
9943
|
+
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
|
|
9944
|
+
/* @__PURE__ */ jsx6(Text5, { bold: true, color: "cyanBright", children: "Select Intent" }),
|
|
9945
|
+
/* @__PURE__ */ jsx6(Text5, { color: "gray", children: "Choose the primary content intent for this generation run." }),
|
|
9946
|
+
/* @__PURE__ */ jsx6(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx6(
|
|
9947
|
+
SelectInput2,
|
|
9948
|
+
{
|
|
9949
|
+
items: intentItems,
|
|
9950
|
+
initialIndex: Math.max(0, intentItems.findIndex((item) => item.value === intent)),
|
|
9951
|
+
onSelect: (item) => {
|
|
9952
|
+
const contentTargets = askTargets ? buildContentTargets(primaryType, selectedSecondaryTypes) : void 0;
|
|
9953
|
+
setIntent(item.value);
|
|
9954
|
+
if (askLength) {
|
|
9955
|
+
setStep("length");
|
|
9956
|
+
return;
|
|
9957
|
+
}
|
|
9958
|
+
onDone({
|
|
9959
|
+
...askStyle ? { style } : {},
|
|
9960
|
+
intent: item.value,
|
|
9961
|
+
...contentTargets ? { contentTargets } : {}
|
|
9962
|
+
});
|
|
9963
|
+
exit();
|
|
9964
|
+
}
|
|
9965
|
+
}
|
|
9966
|
+
) })
|
|
9967
|
+
] });
|
|
9968
|
+
}
|
|
9269
9969
|
const lengthItems = targetLengthValues.map((value2) => ({
|
|
9270
9970
|
label: value2,
|
|
9271
9971
|
value: value2
|
|
@@ -9284,6 +9984,7 @@ function WriteOptionsFlow({
|
|
|
9284
9984
|
setTargetLength(item.value);
|
|
9285
9985
|
onDone({
|
|
9286
9986
|
...askStyle ? { style } : {},
|
|
9987
|
+
...askIntent ? { intent } : {},
|
|
9287
9988
|
targetLength: item.value,
|
|
9288
9989
|
...contentTargets ? { contentTargets } : {}
|
|
9289
9990
|
});
|
|
@@ -9304,6 +10005,9 @@ function WriteApp({
|
|
|
9304
10005
|
dryRun,
|
|
9305
10006
|
enrichLinks: enrichLinks2,
|
|
9306
10007
|
runMode,
|
|
10008
|
+
links,
|
|
10009
|
+
unlinks,
|
|
10010
|
+
maxLinks,
|
|
9307
10011
|
onError
|
|
9308
10012
|
}) {
|
|
9309
10013
|
const { exit } = useApp3();
|
|
@@ -9327,6 +10031,9 @@ function WriteApp({
|
|
|
9327
10031
|
dryRun,
|
|
9328
10032
|
enrichLinks: enrichLinks2,
|
|
9329
10033
|
runMode,
|
|
10034
|
+
customLinks: links,
|
|
10035
|
+
unlinks,
|
|
10036
|
+
maxLinks,
|
|
9330
10037
|
onUpdate(nextStages) {
|
|
9331
10038
|
if (mounted) {
|
|
9332
10039
|
setStages(nextStages);
|
|
@@ -9359,7 +10066,7 @@ function WriteApp({
|
|
|
9359
10066
|
return () => {
|
|
9360
10067
|
mounted = false;
|
|
9361
10068
|
};
|
|
9362
|
-
}, [dryRun, enrichLinks2, input, onError, runMode]);
|
|
10069
|
+
}, [dryRun, enrichLinks2, input, links, unlinks, maxLinks, onError, runMode]);
|
|
9363
10070
|
useEffect2(() => {
|
|
9364
10071
|
if (!result && !errorMessage) {
|
|
9365
10072
|
return;
|
|
@@ -9375,7 +10082,7 @@ function WriteApp({
|
|
|
9375
10082
|
}
|
|
9376
10083
|
async function runWriteCommand(options) {
|
|
9377
10084
|
const input = await resolveInputWithInteractiveIdeaFallback(options);
|
|
9378
|
-
await runWritePipeline(input, options.dryRun, options.enrichLinks, "fresh", options.noInteractive);
|
|
10085
|
+
await runWritePipeline(input, options.dryRun, options.enrichLinks, "fresh", options.noInteractive, options.links, options.unlinks, options.maxLinks);
|
|
9379
10086
|
}
|
|
9380
10087
|
async function runWriteResumeCommand(options = {}) {
|
|
9381
10088
|
const session = await loadWriteSession();
|
|
@@ -9397,9 +10104,9 @@ async function runWriteResumeCommand(options = {}) {
|
|
|
9397
10104
|
secrets: resolved.config.secrets
|
|
9398
10105
|
}
|
|
9399
10106
|
};
|
|
9400
|
-
await runWritePipeline(input, session.dryRun,
|
|
10107
|
+
await runWritePipeline(input, session.dryRun, options.enrichLinks ?? false, "resume", options.noInteractive ?? false, options.links, options.unlinks, options.maxLinks);
|
|
9401
10108
|
}
|
|
9402
|
-
async function runWritePipeline(input, dryRun, enrichLinks2, runMode, noInteractive) {
|
|
10109
|
+
async function runWritePipeline(input, dryRun, enrichLinks2, runMode, noInteractive, links, unlinks, maxLinks) {
|
|
9403
10110
|
let interruptHandled = false;
|
|
9404
10111
|
const handleSignal = (signal) => {
|
|
9405
10112
|
if (interruptHandled) {
|
|
@@ -9433,7 +10140,7 @@ async function runWritePipeline(input, dryRun, enrichLinks2, runMode, noInteract
|
|
|
9433
10140
|
process.on("SIGTERM", onSigterm);
|
|
9434
10141
|
try {
|
|
9435
10142
|
if (noInteractive || !process.stdout.isTTY) {
|
|
9436
|
-
await renderPlainPipeline(input, dryRun, enrichLinks2, runMode);
|
|
10143
|
+
await renderPlainPipeline(input, dryRun, enrichLinks2, runMode, links, unlinks, maxLinks);
|
|
9437
10144
|
return;
|
|
9438
10145
|
}
|
|
9439
10146
|
let commandError = null;
|
|
@@ -9445,6 +10152,9 @@ async function runWritePipeline(input, dryRun, enrichLinks2, runMode, noInteract
|
|
|
9445
10152
|
dryRun,
|
|
9446
10153
|
enrichLinks: enrichLinks2,
|
|
9447
10154
|
runMode,
|
|
10155
|
+
links,
|
|
10156
|
+
unlinks,
|
|
10157
|
+
maxLinks,
|
|
9448
10158
|
onError: (error) => {
|
|
9449
10159
|
commandError = error;
|
|
9450
10160
|
}
|
|
@@ -9482,6 +10192,7 @@ async function resolveInputWithInteractiveIdeaFallback(options) {
|
|
|
9482
10192
|
audience: options.audience,
|
|
9483
10193
|
jobPath: options.jobPath,
|
|
9484
10194
|
style: options.style,
|
|
10195
|
+
intent: options.intent,
|
|
9485
10196
|
targetLength: options.length,
|
|
9486
10197
|
contentTargets: parsedTargets
|
|
9487
10198
|
});
|
|
@@ -9499,6 +10210,7 @@ async function resolveInputWithInteractiveIdeaFallback(options) {
|
|
|
9499
10210
|
audience: options.audience,
|
|
9500
10211
|
jobPath: options.jobPath,
|
|
9501
10212
|
style: options.style,
|
|
10213
|
+
intent: options.intent,
|
|
9502
10214
|
targetLength: options.length,
|
|
9503
10215
|
contentTargets: parsedTargets
|
|
9504
10216
|
});
|
|
@@ -9507,12 +10219,14 @@ async function resolveInputWithInteractiveIdeaFallback(options) {
|
|
|
9507
10219
|
}
|
|
9508
10220
|
async function applyInteractiveWriteOptionsIfNeeded(resolved, options, parsedTargets) {
|
|
9509
10221
|
const styleProvided = Boolean(options.style ?? resolved.job?.settings?.style);
|
|
10222
|
+
const intentProvided = Boolean(options.intent);
|
|
9510
10223
|
const lengthProvided = Boolean(options.length ?? resolved.job?.settings?.targetLength);
|
|
9511
10224
|
const providedTargets = parsedTargets && parsedTargets.length > 0 ? parsedTargets : resolved.job?.settings?.contentTargets ?? resolved.config.settings.contentTargets;
|
|
9512
10225
|
const targetsProvided = Boolean(parsedTargets && parsedTargets.length > 0 || resolved.job?.settings?.contentTargets?.length);
|
|
9513
|
-
if (options.noInteractive && (!styleProvided || !targetsProvided || !lengthProvided)) {
|
|
10226
|
+
if (options.noInteractive && (!styleProvided || !intentProvided || !targetsProvided || !lengthProvided)) {
|
|
9514
10227
|
const missingFlags = [
|
|
9515
10228
|
!styleProvided ? "--style <style>" : null,
|
|
10229
|
+
!intentProvided ? "--intent <intent>" : null,
|
|
9516
10230
|
!targetsProvided ? "--primary <content-type=1>" : null,
|
|
9517
10231
|
!lengthProvided ? "--length <size>" : null
|
|
9518
10232
|
].filter((value2) => Boolean(value2));
|
|
@@ -9523,15 +10237,17 @@ async function applyInteractiveWriteOptionsIfNeeded(resolved, options, parsedTar
|
|
|
9523
10237
|
if (!process.stdout.isTTY || !process.stdin.isTTY || options.noInteractive) {
|
|
9524
10238
|
return resolved;
|
|
9525
10239
|
}
|
|
9526
|
-
if (styleProvided && targetsProvided && lengthProvided) {
|
|
10240
|
+
if (styleProvided && intentProvided && targetsProvided && lengthProvided) {
|
|
9527
10241
|
return resolved;
|
|
9528
10242
|
}
|
|
9529
10243
|
const prompted = await promptForMissingWriteOptions({
|
|
9530
10244
|
askStyle: !styleProvided,
|
|
10245
|
+
askIntent: !intentProvided,
|
|
9531
10246
|
askTargets: !targetsProvided,
|
|
9532
10247
|
askLength: !lengthProvided,
|
|
9533
10248
|
style: resolved.config.settings.style,
|
|
9534
10249
|
targetLength: resolved.config.settings.targetLength,
|
|
10250
|
+
intent: resolved.config.settings.intent,
|
|
9535
10251
|
targets: providedTargets
|
|
9536
10252
|
});
|
|
9537
10253
|
return {
|
|
@@ -9541,6 +10257,7 @@ async function applyInteractiveWriteOptionsIfNeeded(resolved, options, parsedTar
|
|
|
9541
10257
|
settings: appSettingsSchema.parse({
|
|
9542
10258
|
...resolved.config.settings,
|
|
9543
10259
|
...prompted.style ? { style: prompted.style } : {},
|
|
10260
|
+
...prompted.intent ? { intent: prompted.intent } : {},
|
|
9544
10261
|
...prompted.targetLength ? { targetLength: prompted.targetLength } : {},
|
|
9545
10262
|
...prompted.contentTargets ? { contentTargets: prompted.contentTargets } : {}
|
|
9546
10263
|
})
|
|
@@ -9552,10 +10269,12 @@ async function promptForMissingWriteOptions(params) {
|
|
|
9552
10269
|
const app = render2(
|
|
9553
10270
|
React4.createElement(WriteOptionsFlow, {
|
|
9554
10271
|
askStyle: params.askStyle,
|
|
10272
|
+
askIntent: params.askIntent,
|
|
9555
10273
|
askTargets: params.askTargets,
|
|
9556
10274
|
askLength: params.askLength,
|
|
9557
10275
|
initialStyle: writingStyleValues.includes(params.style) ? params.style : "professional",
|
|
9558
|
-
|
|
10276
|
+
initialIntent: contentIntentValues.includes(params.intent) ? params.intent : "tutorial",
|
|
10277
|
+
initialTargetLength: resolveTargetLengthAlias(params.targetLength),
|
|
9559
10278
|
initialTargets: params.targets,
|
|
9560
10279
|
onDone: (result) => {
|
|
9561
10280
|
flowResult = result;
|
|
@@ -9639,6 +10358,15 @@ async function runCli(argv) {
|
|
|
9639
10358
|
force: options.force
|
|
9640
10359
|
});
|
|
9641
10360
|
});
|
|
10361
|
+
program.command("links").description("Run link enrichment for a previously generated article by slug.").argument("<slug>", "Slug of the generated article to enrich").option("--mode <mode>", "Link merge mode: fresh or append", "fresh").option("--link <pair>", 'Custom link "expression->url", repeatable', collectOptionValue).option("--unlink <expression>", "Remove a custom link by expression, repeatable", collectOptionValue).option("--max-links <n>", "Max number of generated links", (v) => Number.parseInt(v, 10)).action(async (slug, options) => {
|
|
10362
|
+
await runLinksCommand({
|
|
10363
|
+
slug,
|
|
10364
|
+
mode: options.mode,
|
|
10365
|
+
links: options.link,
|
|
10366
|
+
unlinks: options.unlink,
|
|
10367
|
+
maxLinks: options.maxLinks
|
|
10368
|
+
});
|
|
10369
|
+
});
|
|
9642
10370
|
program.command("preview").description("Preview a generated article in a local browser with linked assets.").argument("[markdownPath]", "Path to the markdown file to preview").option("-p, --port <port>", "Port for the local preview server (default: 4173)").option("--no-open", "Do not auto-open browser after server startup").option("--watch", "Rebuild the preview UI on source changes and auto-reload the browser", false).action(async (markdownPath, options) => {
|
|
9643
10371
|
await runServeCommand({
|
|
9644
10372
|
markdownPath,
|
|
@@ -9647,7 +10375,7 @@ async function runCli(argv) {
|
|
|
9647
10375
|
watch: options.watch
|
|
9648
10376
|
});
|
|
9649
10377
|
});
|
|
9650
|
-
const writeCommand = program.command("write").description("Generate one primary content output plus optional secondary outputs from a prompt or job file.").argument("[idea]", "Natural-language idea for the generation run").option("-i, --idea <idea>", "Natural-language idea for the generation run").option("--audience <description>", "Optional natural-language audience description for shared-brief targeting").option("-j, --job <path>", "Path to a JSON job definition").option("--primary <type=count>", "Required primary output target (for example: article=1 or x-post=1)").option("--secondary <type=count>", "Secondary output target, repeatable (for example: x-thread=3, linkedin-post=2)", collectOptionValue).option("--style <style>", "Writing style (
|
|
10378
|
+
const writeCommand = program.command("write").description("Generate one primary content output plus optional secondary outputs from a prompt or job file.").argument("[idea]", "Natural-language idea for the generation run").option("-i, --idea <idea>", "Natural-language idea for the generation run").option("--audience <description>", "Optional natural-language audience description for shared-brief targeting").option("-j, --job <path>", "Path to a JSON job definition").option("--primary <type=count>", "Required primary output target (for example: article=1 or x-post=1)").option("--secondary <type=count>", "Secondary output target, repeatable (for example: x-thread=3, linkedin-post=2)", collectOptionValue).option("--style <style>", "Writing style (academic, analytical, authoritative, conversational, empathetic, friendly, journalistic, minimalist, persuasive, playful, professional, storytelling, technical)").option("--intent <intent>", "Content intent (announcement, case-study, cornerstone, counterargument, critique-review, deep-dive-analysis, how-to-guide, interview-q-and-a, listicle, opinion-piece, personal-essay, roundup-curation, tutorial)").option("--length <size>", "Target length: small, medium, large, or a positive integer word count").option("--no-interactive", "Fail instead of prompting for missing input in TTY mode").option("--dry-run", "Run the pipeline shell without external API calls", false).option("--enrich-links", "Run link enrichment after markdown generation", false).option("--link <pair>", 'Custom link "expression->url", repeatable', collectOptionValue).option("--unlink <expression>", "Remove a custom link by expression, repeatable", collectOptionValue).option("--max-links <n>", "Max number of generated links", (v) => Number.parseInt(v, 10)).action(async (ideaArg, options) => {
|
|
9651
10379
|
await runWriteCommand({
|
|
9652
10380
|
idea: options.idea ?? ideaArg,
|
|
9653
10381
|
audience: options.audience,
|
|
@@ -9655,14 +10383,24 @@ async function runCli(argv) {
|
|
|
9655
10383
|
primarySpec: options.primary,
|
|
9656
10384
|
secondarySpecs: options.secondary,
|
|
9657
10385
|
style: options.style,
|
|
10386
|
+
intent: options.intent,
|
|
9658
10387
|
length: options.length,
|
|
9659
10388
|
noInteractive: !options.interactive,
|
|
9660
10389
|
dryRun: options.dryRun,
|
|
9661
|
-
enrichLinks: options.enrichLinks
|
|
10390
|
+
enrichLinks: options.enrichLinks,
|
|
10391
|
+
links: options.link,
|
|
10392
|
+
unlinks: options.unlink,
|
|
10393
|
+
maxLinks: options.maxLinks
|
|
9662
10394
|
});
|
|
9663
10395
|
});
|
|
9664
|
-
writeCommand.command("resume").description("Resume the last failed or interrupted write session from .ideon/write.").option("--no-interactive", "Force plain non-interactive output even in TTY mode", false).action(async (options) => {
|
|
9665
|
-
await runWriteResumeCommand({
|
|
10396
|
+
writeCommand.command("resume").description("Resume the last failed or interrupted write session from .ideon/write.").option("--no-interactive", "Force plain non-interactive output even in TTY mode", false).option("--enrich-links", "Run link enrichment after markdown generation", false).option("--link <pair>", 'Custom link "expression->url", repeatable', collectOptionValue).option("--unlink <expression>", "Remove a custom link by expression, repeatable", collectOptionValue).option("--max-links <n>", "Max number of generated links", (v) => Number.parseInt(v, 10)).action(async (options) => {
|
|
10397
|
+
await runWriteResumeCommand({
|
|
10398
|
+
noInteractive: options.noInteractive,
|
|
10399
|
+
enrichLinks: options.enrichLinks,
|
|
10400
|
+
links: options.link,
|
|
10401
|
+
unlinks: options.unlink,
|
|
10402
|
+
maxLinks: options.maxLinks
|
|
10403
|
+
});
|
|
9666
10404
|
});
|
|
9667
10405
|
await program.parseAsync(argv);
|
|
9668
10406
|
}
|