@telepat/ideon 0.1.7 → 0.1.13
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 +896 -227
- package/package.json +1 -1
package/dist/ideon.js
CHANGED
|
@@ -12,15 +12,87 @@ 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
|
+
}
|
|
24
96
|
var contentTargetRoleValues = ["primary", "secondary"];
|
|
25
97
|
var contentTargetSchema = z.object({
|
|
26
98
|
contentType: z.enum(contentTypeValues),
|
|
@@ -51,7 +123,8 @@ var appSettingsSchema = z.object({
|
|
|
51
123
|
message: "contentTargets must include exactly one primary target."
|
|
52
124
|
}).default([{ contentType: "article", role: "primary", count: 1 }]),
|
|
53
125
|
style: z.enum(writingStyleValues).default("professional"),
|
|
54
|
-
|
|
126
|
+
intent: z.enum(contentIntentValues).default("tutorial"),
|
|
127
|
+
targetLength: targetLengthWordsSchema.default(defaultTargetLengthWords)
|
|
55
128
|
});
|
|
56
129
|
var envSettingsSchema = z.object({
|
|
57
130
|
openRouterApiKey: z.string().optional(),
|
|
@@ -66,7 +139,8 @@ var envSettingsSchema = z.object({
|
|
|
66
139
|
markdownOutputDir: z.string().optional(),
|
|
67
140
|
assetOutputDir: z.string().optional(),
|
|
68
141
|
style: z.enum(writingStyleValues).optional(),
|
|
69
|
-
|
|
142
|
+
intent: z.enum(contentIntentValues).optional(),
|
|
143
|
+
targetLength: targetLengthWordsSchema.optional()
|
|
70
144
|
});
|
|
71
145
|
var jobInputSchema = z.object({
|
|
72
146
|
idea: z.string().min(1).optional(),
|
|
@@ -111,6 +185,7 @@ function readEnvSettings(env = process.env) {
|
|
|
111
185
|
markdownOutputDir: env.IDEON_MARKDOWN_OUTPUT_DIR,
|
|
112
186
|
assetOutputDir: env.IDEON_ASSET_OUTPUT_DIR,
|
|
113
187
|
style: env.IDEON_STYLE,
|
|
188
|
+
intent: env.IDEON_INTENT,
|
|
114
189
|
targetLength: env.IDEON_TARGET_LENGTH
|
|
115
190
|
});
|
|
116
191
|
}
|
|
@@ -179,6 +254,37 @@ function buildGenerationDirectoryName(baseSlug, now = /* @__PURE__ */ new Date()
|
|
|
179
254
|
].join("");
|
|
180
255
|
return `${stamp}-${baseSlug}`;
|
|
181
256
|
}
|
|
257
|
+
async function listMarkdownFilesRecursively(rootDir) {
|
|
258
|
+
return listFilesRecursively(rootDir, (fileName) => fileName.toLowerCase().endsWith(".md"));
|
|
259
|
+
}
|
|
260
|
+
async function listFilesRecursively(rootDir, predicate) {
|
|
261
|
+
const fs = await import("fs/promises");
|
|
262
|
+
const results = [];
|
|
263
|
+
const stack = [rootDir];
|
|
264
|
+
while (stack.length > 0) {
|
|
265
|
+
const current = stack.pop();
|
|
266
|
+
if (!current) {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
let entries;
|
|
270
|
+
try {
|
|
271
|
+
entries = await fs.readdir(current, { withFileTypes: true });
|
|
272
|
+
} catch {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
for (const entry of entries) {
|
|
276
|
+
const fullPath = path2.join(current, entry.name);
|
|
277
|
+
if (entry.isDirectory()) {
|
|
278
|
+
stack.push(fullPath);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (entry.isFile() && predicate(entry.name)) {
|
|
282
|
+
results.push(fullPath);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return results;
|
|
287
|
+
}
|
|
182
288
|
async function writeUtf8File(filePath, content) {
|
|
183
289
|
await mkdir2(path2.dirname(filePath), { recursive: true });
|
|
184
290
|
await writeFile2(filePath, content, "utf8");
|
|
@@ -637,6 +743,7 @@ var configSettingKeys = [
|
|
|
637
743
|
"markdownOutputDir",
|
|
638
744
|
"assetOutputDir",
|
|
639
745
|
"style",
|
|
746
|
+
"intent",
|
|
640
747
|
"targetLength"
|
|
641
748
|
];
|
|
642
749
|
var configSecretKeys = ["openRouterApiKey", "replicateApiToken"];
|
|
@@ -665,6 +772,7 @@ async function configList() {
|
|
|
665
772
|
markdownOutputDir: settings.markdownOutputDir,
|
|
666
773
|
assetOutputDir: settings.assetOutputDir,
|
|
667
774
|
style: settings.style,
|
|
775
|
+
intent: settings.intent,
|
|
668
776
|
targetLength: settings.targetLength
|
|
669
777
|
},
|
|
670
778
|
secrets: {
|
|
@@ -767,12 +875,23 @@ function coerceSettingValue(key, rawValue) {
|
|
|
767
875
|
}
|
|
768
876
|
return trimmed;
|
|
769
877
|
}
|
|
770
|
-
case "
|
|
771
|
-
if (!
|
|
772
|
-
throw new Error(`
|
|
878
|
+
case "intent": {
|
|
879
|
+
if (!contentIntentValues.includes(trimmed)) {
|
|
880
|
+
throw new Error(`intent must be one of: ${contentIntentValues.join(", ")}.`);
|
|
773
881
|
}
|
|
774
882
|
return trimmed;
|
|
775
883
|
}
|
|
884
|
+
case "targetLength": {
|
|
885
|
+
const normalized = trimmed.toLowerCase();
|
|
886
|
+
if (targetLengthValues.includes(normalized)) {
|
|
887
|
+
return normalized;
|
|
888
|
+
}
|
|
889
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
890
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
891
|
+
throw new Error(`targetLength must be one of: ${targetLengthValues.join(", ")}, or a positive integer word count.`);
|
|
892
|
+
}
|
|
893
|
+
return parsed;
|
|
894
|
+
}
|
|
776
895
|
default:
|
|
777
896
|
throw new Error(`Unsupported config key: ${key}`);
|
|
778
897
|
}
|
|
@@ -797,6 +916,8 @@ function getSettingValue(settings, key) {
|
|
|
797
916
|
return settings.assetOutputDir;
|
|
798
917
|
case "style":
|
|
799
918
|
return settings.style;
|
|
919
|
+
case "intent":
|
|
920
|
+
return settings.intent;
|
|
800
921
|
case "targetLength":
|
|
801
922
|
return settings.targetLength;
|
|
802
923
|
default:
|
|
@@ -823,6 +944,8 @@ function setSettingValue(settings, key, value2) {
|
|
|
823
944
|
return { ...settings, assetOutputDir: value2 };
|
|
824
945
|
case "style":
|
|
825
946
|
return { ...settings, style: value2 };
|
|
947
|
+
case "intent":
|
|
948
|
+
return { ...settings, intent: value2 };
|
|
826
949
|
case "targetLength":
|
|
827
950
|
return { ...settings, targetLength: value2 };
|
|
828
951
|
default:
|
|
@@ -840,7 +963,8 @@ var writeToolInputSchema = {
|
|
|
840
963
|
primary: z3.string().optional(),
|
|
841
964
|
secondary: z3.array(z3.string()).optional(),
|
|
842
965
|
style: z3.enum(writingStyleValues).optional(),
|
|
843
|
-
|
|
966
|
+
intent: z3.enum(contentIntentValues).optional(),
|
|
967
|
+
length: z3.union([z3.enum(targetLengthValues), z3.coerce.number().int().positive()]).optional(),
|
|
844
968
|
dryRun: z3.boolean().optional(),
|
|
845
969
|
enrichLinks: z3.boolean().optional()
|
|
846
970
|
};
|
|
@@ -869,6 +993,7 @@ var ideonToolContracts = [
|
|
|
869
993
|
required: ["idea"],
|
|
870
994
|
enums: {
|
|
871
995
|
style: [...writingStyleValues],
|
|
996
|
+
intent: [...contentIntentValues],
|
|
872
997
|
length: [...targetLengthValues]
|
|
873
998
|
}
|
|
874
999
|
},
|
|
@@ -899,6 +1024,7 @@ var ideonSkillRegistry = [
|
|
|
899
1024
|
required: ["idea"],
|
|
900
1025
|
enums: {
|
|
901
1026
|
style: [...writingStyleValues],
|
|
1027
|
+
intent: [...contentIntentValues],
|
|
902
1028
|
length: [...targetLengthValues]
|
|
903
1029
|
}
|
|
904
1030
|
}
|
|
@@ -959,6 +1085,18 @@ function validateIntegrationContracts(sources = {
|
|
|
959
1085
|
[...writeSkill.inputContract.enums.style ?? []].sort(),
|
|
960
1086
|
[...writingStyleValues].sort()
|
|
961
1087
|
);
|
|
1088
|
+
compareStringArrays(
|
|
1089
|
+
drifts,
|
|
1090
|
+
"write.enum.intent.tool-vs-schema",
|
|
1091
|
+
[...writeTool.enums.intent ?? []].sort(),
|
|
1092
|
+
[...contentIntentValues].sort()
|
|
1093
|
+
);
|
|
1094
|
+
compareStringArrays(
|
|
1095
|
+
drifts,
|
|
1096
|
+
"write.enum.intent.skill-vs-schema",
|
|
1097
|
+
[...writeSkill.inputContract.enums.intent ?? []].sort(),
|
|
1098
|
+
[...contentIntentValues].sort()
|
|
1099
|
+
);
|
|
962
1100
|
compareStringArrays(
|
|
963
1101
|
drifts,
|
|
964
1102
|
"write.enum.length.tool-vs-schema",
|
|
@@ -1161,7 +1299,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
1161
1299
|
// package.json
|
|
1162
1300
|
var package_default = {
|
|
1163
1301
|
name: "@telepat/ideon",
|
|
1164
|
-
version: "0.1.
|
|
1302
|
+
version: "0.1.13",
|
|
1165
1303
|
description: "CLI for generating rich articles and images from ideas.",
|
|
1166
1304
|
type: "module",
|
|
1167
1305
|
repository: {
|
|
@@ -1301,8 +1439,10 @@ async function resolveRunInput(input) {
|
|
|
1301
1439
|
...envSettings.markdownOutputDir ? { markdownOutputDir: envSettings.markdownOutputDir } : {},
|
|
1302
1440
|
...envSettings.assetOutputDir ? { assetOutputDir: envSettings.assetOutputDir } : {},
|
|
1303
1441
|
...envSettings.style ? { style: envSettings.style } : {},
|
|
1442
|
+
...envSettings.intent ? { intent: envSettings.intent } : {},
|
|
1304
1443
|
...envSettings.targetLength ? { targetLength: envSettings.targetLength } : {},
|
|
1305
1444
|
...input.style ? { style: input.style } : {},
|
|
1445
|
+
...input.intent ? { intent: input.intent } : {},
|
|
1306
1446
|
...input.targetLength ? { targetLength: input.targetLength } : {},
|
|
1307
1447
|
...input.contentTargets ? { contentTargets: input.contentTargets } : {}
|
|
1308
1448
|
});
|
|
@@ -1362,7 +1502,7 @@ function assertNoLegacyXMode(contentTargets, sourceLabel) {
|
|
|
1362
1502
|
// src/pipeline/runner.ts
|
|
1363
1503
|
import { mkdir as mkdir5, stat as stat2 } from "fs/promises";
|
|
1364
1504
|
import { randomUUID } from "crypto";
|
|
1365
|
-
import
|
|
1505
|
+
import path8 from "path";
|
|
1366
1506
|
|
|
1367
1507
|
// src/generation/enrichLinks.ts
|
|
1368
1508
|
import { readFile as readFile4 } from "fs/promises";
|
|
@@ -1669,44 +1809,6 @@ function toExpressionPreview(expression, maxLength = 60) {
|
|
|
1669
1809
|
}
|
|
1670
1810
|
|
|
1671
1811
|
// 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
1812
|
function buildRunContextDirective(contentTypes) {
|
|
1711
1813
|
const normalizedTypes = contentTypes.length > 0 ? contentTypes.join(", ") : "article";
|
|
1712
1814
|
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 +1818,148 @@ var TARGET_LENGTH_TIERS = {
|
|
|
1716
1818
|
label: "small",
|
|
1717
1819
|
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
1820
|
"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
1821
|
"linkedin-post": "Target length (small linkedin post): 50\u2013150 words, 3\u20136 lines, single insight.",
|
|
1723
1822
|
newsletter: "Target length (small newsletter): 300\u2013800 words, 1\u20132 sections, one core idea.",
|
|
1724
|
-
"
|
|
1823
|
+
"press-release": "Target length (small press release): 300\u2013700 words with headline, lead, core announcement details, and concise quote block.",
|
|
1824
|
+
"reddit-post": "Target length (small reddit post): 150\u2013400 words. Quick question or observation, minimal formatting.",
|
|
1825
|
+
"science-paper": "Target length (small science paper): 800\u20131,400 words condensed structure with abstract-style opener, methods summary, and key findings.",
|
|
1826
|
+
"x-post": "Target length (small x-post): 70\u2013150 characters, one idea, pure hook or insight.",
|
|
1827
|
+
"x-thread": "Target length (small x-thread): 3\u20135 posts, each post one clear idea with momentum from one step to the next.",
|
|
1725
1828
|
fallback: "Target length (small): 50\u2013300 words. Compressed insight density. Prioritise hooks and key points over elaboration."
|
|
1726
1829
|
},
|
|
1727
1830
|
medium: {
|
|
1728
1831
|
label: "medium",
|
|
1729
1832
|
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
1833
|
"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
1834
|
"linkedin-post": "Target length (medium linkedin post): 150\u2013400 words, 8\u201315 short lines, story plus takeaway. Best performing range.",
|
|
1735
1835
|
newsletter: "Target length (medium newsletter): 800\u20131,800 words, 2\u20134 sections, curated insights. Ideal default.",
|
|
1736
|
-
"
|
|
1836
|
+
"press-release": "Target length (medium press release): 700\u20131,200 words with complete release anatomy, context, quote, and next-step details.",
|
|
1837
|
+
"reddit-post": "Target length (medium reddit post): 400\u20131,200 words. Context, experience, and question. Conversational tone.",
|
|
1838
|
+
"science-paper": "Target length (medium science paper): 1,400\u20132,600 words with clearer methodological depth, results framing, and discussion of limitations.",
|
|
1839
|
+
"x-post": "Target length (medium x-post): 150\u2013280 characters or 2\u20134 short lines, 1\u20132 ideas with slight expansion.",
|
|
1840
|
+
"x-thread": "Target length (medium x-thread): 5\u20138 posts, each post concise and additive, with clear narrative progression.",
|
|
1737
1841
|
fallback: "Target length (medium): 300\u20131,200 words. Balanced depth and breadth with examples and actionable takeaways."
|
|
1738
1842
|
},
|
|
1739
1843
|
large: {
|
|
1740
1844
|
label: "large",
|
|
1741
1845
|
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
1846
|
"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
1847
|
"linkedin-post": "Target length (large linkedin post): 400\u2013900 words. Structured storytelling, multiple insights. Use sparingly for deep authority posts.",
|
|
1747
1848
|
newsletter: "Target length (large newsletter): 1,800\u20133,000 words. Multi-topic edition with deep commentary. Use for weekly deep dives.",
|
|
1748
|
-
"
|
|
1849
|
+
"press-release": "Target length (large press release): 1,200\u20132,000+ words with full context, expanded quote material, and detailed release implications.",
|
|
1850
|
+
"reddit-post": "Target length (large reddit post): 1,200\u20132,500+ words. Detailed breakdown with story, lessons, numbers, and mistakes.",
|
|
1851
|
+
"science-paper": "Target length (large science paper): 2,600\u20134,500+ words with full narrative arc from research question through methods, results, and implications.",
|
|
1852
|
+
"x-post": "Target length (large x-post): 200\u2013300 characters, one strong stance plus one supporting detail or proof.",
|
|
1853
|
+
"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
1854
|
fallback: "Target length (large): 1,200\u20133,500+ words. Deep exploration with frameworks, multiple examples, and expanded narrative."
|
|
1750
1855
|
}
|
|
1751
1856
|
};
|
|
1752
|
-
function buildTargetLengthDirective(contentType,
|
|
1753
|
-
const
|
|
1754
|
-
|
|
1857
|
+
function buildTargetLengthDirective(contentType, targetLengthWords) {
|
|
1858
|
+
const normalizedTargetLengthWords = Number.isFinite(targetLengthWords) && targetLengthWords > 0 ? Math.round(targetLengthWords) : 900;
|
|
1859
|
+
const alias = resolveTargetLengthAlias(normalizedTargetLengthWords);
|
|
1860
|
+
const tier = TARGET_LENGTH_TIERS[alias] ?? TARGET_LENGTH_TIERS["medium"];
|
|
1861
|
+
if (contentType === "article") {
|
|
1862
|
+
return `Target length (article): aim for about ${normalizedTargetLengthWords} words total while keeping section depth and structure consistent.`;
|
|
1863
|
+
}
|
|
1864
|
+
const baseDirective = tier[contentType] ?? tier.fallback;
|
|
1865
|
+
return `${baseDirective} Overall run target is about ${normalizedTargetLengthWords} words.`;
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
// src/llm/prompts/guideBundles.ts
|
|
1869
|
+
import { existsSync, readFileSync } from "fs";
|
|
1870
|
+
import path5 from "path";
|
|
1871
|
+
var guideCache = /* @__PURE__ */ new Map();
|
|
1872
|
+
function normalizeGuideContent(content) {
|
|
1873
|
+
return content.replace(/\r\n/g, "\n").trim();
|
|
1874
|
+
}
|
|
1875
|
+
function readGuideFile(relativePath) {
|
|
1876
|
+
const cached = guideCache.get(relativePath);
|
|
1877
|
+
if (cached) {
|
|
1878
|
+
return cached;
|
|
1879
|
+
}
|
|
1880
|
+
const absolutePath = path5.resolve(process.cwd(), relativePath);
|
|
1881
|
+
if (!existsSync(absolutePath)) {
|
|
1882
|
+
const fallback = `Guide unavailable: ${relativePath}. Continue with the remaining guidance.`;
|
|
1883
|
+
guideCache.set(relativePath, fallback);
|
|
1884
|
+
return fallback;
|
|
1885
|
+
}
|
|
1886
|
+
try {
|
|
1887
|
+
const content = normalizeGuideContent(readFileSync(absolutePath, "utf8"));
|
|
1888
|
+
guideCache.set(relativePath, content);
|
|
1889
|
+
return content;
|
|
1890
|
+
} catch {
|
|
1891
|
+
const fallback = `Guide failed to load: ${relativePath}. Continue with the remaining guidance.`;
|
|
1892
|
+
guideCache.set(relativePath, fallback);
|
|
1893
|
+
return fallback;
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
function buildGuideSection(relativePath) {
|
|
1897
|
+
const content = readGuideFile(relativePath);
|
|
1898
|
+
return [
|
|
1899
|
+
`Guide source: ${relativePath}`,
|
|
1900
|
+
content
|
|
1901
|
+
].join("\n");
|
|
1902
|
+
}
|
|
1903
|
+
function formatToGuidePath(contentType) {
|
|
1904
|
+
return `writing-guide/formats/${contentType}.md`;
|
|
1905
|
+
}
|
|
1906
|
+
function intentToGuidePath(intent) {
|
|
1907
|
+
return `writing-guide/content-intent/${intent}.md`;
|
|
1908
|
+
}
|
|
1909
|
+
function styleToGuidePath(style) {
|
|
1910
|
+
return `writing-guide/styles/${style}.md`;
|
|
1911
|
+
}
|
|
1912
|
+
function dedupe(items) {
|
|
1913
|
+
return Array.from(new Set(items));
|
|
1914
|
+
}
|
|
1915
|
+
function buildGuideBundle(relativePaths) {
|
|
1916
|
+
const blocks = dedupe(relativePaths).map((relativePath) => buildGuideSection(relativePath));
|
|
1917
|
+
return [
|
|
1918
|
+
"External writing guides (apply these rules directly):",
|
|
1919
|
+
...blocks
|
|
1920
|
+
].join("\n\n");
|
|
1921
|
+
}
|
|
1922
|
+
function buildArticlePlanGuideInstruction(intent, contentType) {
|
|
1923
|
+
return buildGuideBundle([
|
|
1924
|
+
"writing-guide/references/headline-writing-systems.md",
|
|
1925
|
+
"writing-guide/references/ideation-and-credibility-systems.md",
|
|
1926
|
+
"writing-guide/references/content-frameworks.md",
|
|
1927
|
+
intentToGuidePath(intent),
|
|
1928
|
+
formatToGuidePath(contentType)
|
|
1929
|
+
]);
|
|
1930
|
+
}
|
|
1931
|
+
function buildArticleSectionGuideInstruction(style, intent, contentType) {
|
|
1932
|
+
return buildGuideBundle([
|
|
1933
|
+
"writing-guide/general/core-web-writing-rules.md",
|
|
1934
|
+
"writing-guide/references/emotional-resonance.md",
|
|
1935
|
+
"writing-guide/references/prose-quality-checks.md",
|
|
1936
|
+
"writing-guide/references/readability-and-pace.md",
|
|
1937
|
+
"writing-guide/references/skimmability-patterns.md",
|
|
1938
|
+
styleToGuidePath(style),
|
|
1939
|
+
intentToGuidePath(intent),
|
|
1940
|
+
formatToGuidePath(contentType)
|
|
1941
|
+
]);
|
|
1942
|
+
}
|
|
1943
|
+
function buildContentBriefGuideInstruction(intent, primaryContentType, secondaryContentTypes) {
|
|
1944
|
+
return buildGuideBundle([
|
|
1945
|
+
"writing-guide/references/multi-channel-brief-strategy.md",
|
|
1946
|
+
"writing-guide/references/content-frameworks.md",
|
|
1947
|
+
"writing-guide/references/target-length-guidance.md",
|
|
1948
|
+
intentToGuidePath(intent),
|
|
1949
|
+
formatToGuidePath(primaryContentType),
|
|
1950
|
+
...secondaryContentTypes.map((contentType) => formatToGuidePath(contentType))
|
|
1951
|
+
]);
|
|
1952
|
+
}
|
|
1953
|
+
function buildChannelContentGuideInstruction(style, intent, contentType) {
|
|
1954
|
+
const conditionalGuides = contentType === "x-thread" ? ["writing-guide/references/x-thread-hooks.md"] : [];
|
|
1955
|
+
return buildGuideBundle([
|
|
1956
|
+
"writing-guide/references/truthful-value-framing.md",
|
|
1957
|
+
"writing-guide/references/target-length-guidance.md",
|
|
1958
|
+
...conditionalGuides,
|
|
1959
|
+
styleToGuidePath(style),
|
|
1960
|
+
intentToGuidePath(intent),
|
|
1961
|
+
formatToGuidePath(contentType)
|
|
1962
|
+
]);
|
|
1755
1963
|
}
|
|
1756
1964
|
|
|
1757
1965
|
// src/llm/prompts/contentBrief.ts
|
|
@@ -1791,11 +1999,11 @@ var contentBriefSchema = {
|
|
|
1791
1999
|
};
|
|
1792
2000
|
function buildContentBriefMessages(idea, options) {
|
|
1793
2001
|
const audienceSeed = options.targetAudienceHint?.trim() || "A general, non-specific audience.";
|
|
2002
|
+
const hasSecondaryContentTypes = options.secondaryContentTypes.length > 0;
|
|
1794
2003
|
const systemInstruction = [
|
|
1795
2004
|
"You are a senior editorial strategist.",
|
|
1796
2005
|
"Produce a shared content brief that can guide all requested content types in this run.",
|
|
1797
|
-
|
|
1798
|
-
buildStyleDirective(options.style),
|
|
2006
|
+
buildContentBriefGuideInstruction(options.intent, options.primaryContentType, options.secondaryContentTypes),
|
|
1799
2007
|
buildRunContextDirective([options.primaryContentType, ...options.secondaryContentTypes]),
|
|
1800
2008
|
"The brief must be specific, concrete, and directly usable by writers without extra clarification.",
|
|
1801
2009
|
"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 +2032,7 @@ function buildContentBriefMessages(idea, options) {
|
|
|
1824
2032
|
"- voiceNotes: practical tone/voice constraints to keep outputs consistent.",
|
|
1825
2033
|
`- primaryContentType: set to "${options.primaryContentType}" exactly.`,
|
|
1826
2034
|
`- 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.",
|
|
2035
|
+
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
2036
|
"",
|
|
1829
2037
|
"Return JSON only with all required fields."
|
|
1830
2038
|
].join("\n")
|
|
@@ -1834,6 +2042,15 @@ function buildContentBriefMessages(idea, options) {
|
|
|
1834
2042
|
|
|
1835
2043
|
// src/types/contentBriefSchema.ts
|
|
1836
2044
|
import { z as z4 } from "zod";
|
|
2045
|
+
var secondaryTypeSentinelValues = /* @__PURE__ */ new Set([
|
|
2046
|
+
"none",
|
|
2047
|
+
"n/a",
|
|
2048
|
+
"na",
|
|
2049
|
+
"null",
|
|
2050
|
+
"not applicable",
|
|
2051
|
+
"no secondary content",
|
|
2052
|
+
"no secondary outputs"
|
|
2053
|
+
]);
|
|
1837
2054
|
var contentBriefSchema2 = z4.object({
|
|
1838
2055
|
title: z4.string().min(8),
|
|
1839
2056
|
description: z4.string().min(40),
|
|
@@ -1842,8 +2059,24 @@ var contentBriefSchema2 = z4.object({
|
|
|
1842
2059
|
keyPoints: z4.array(z4.string().min(8)).min(3).max(6),
|
|
1843
2060
|
voiceNotes: z4.string().min(20),
|
|
1844
2061
|
primaryContentType: z4.string().min(2),
|
|
1845
|
-
secondaryContentTypes: z4.array(z4.string().min(2)).max(10),
|
|
1846
|
-
secondaryContentStrategy: z4.string()
|
|
2062
|
+
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()))),
|
|
2063
|
+
secondaryContentStrategy: z4.string()
|
|
2064
|
+
}).superRefine((brief, ctx) => {
|
|
2065
|
+
const hasSecondaryTargets = brief.secondaryContentTypes.length > 0;
|
|
2066
|
+
if (!hasSecondaryTargets) {
|
|
2067
|
+
return;
|
|
2068
|
+
}
|
|
2069
|
+
if (brief.secondaryContentStrategy.trim().length < 20) {
|
|
2070
|
+
ctx.addIssue({
|
|
2071
|
+
code: z4.ZodIssueCode.too_small,
|
|
2072
|
+
minimum: 20,
|
|
2073
|
+
inclusive: true,
|
|
2074
|
+
origin: "string",
|
|
2075
|
+
path: ["secondaryContentStrategy"],
|
|
2076
|
+
type: "string",
|
|
2077
|
+
message: "Too small: expected string to have >=20 characters"
|
|
2078
|
+
});
|
|
2079
|
+
}
|
|
1847
2080
|
});
|
|
1848
2081
|
|
|
1849
2082
|
// src/generation/planContentBrief.ts
|
|
@@ -1871,7 +2104,7 @@ async function planContentBrief({
|
|
|
1871
2104
|
schemaName: "content_brief",
|
|
1872
2105
|
schema: contentBriefSchema,
|
|
1873
2106
|
messages: buildContentBriefMessages(idea, {
|
|
1874
|
-
|
|
2107
|
+
intent: settings.intent,
|
|
1875
2108
|
targetAudienceHint,
|
|
1876
2109
|
primaryContentType: settings.contentTargets.find((target) => target.role === "primary")?.contentType ?? "article",
|
|
1877
2110
|
secondaryContentTypes: settings.contentTargets.filter((target) => target.role === "secondary").map((target) => target.contentType)
|
|
@@ -1916,13 +2149,19 @@ function deriveTitleFromIdea(idea) {
|
|
|
1916
2149
|
}
|
|
1917
2150
|
|
|
1918
2151
|
// src/llm/prompts/articlePlan.ts
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
2152
|
+
function deriveArticleSectionCounts(targetLengthWords) {
|
|
2153
|
+
const normalizedWords = Number.isFinite(targetLengthWords) && targetLengthWords > 0 ? targetLengthWords : 900;
|
|
2154
|
+
const center = Math.max(2, Math.min(10, Math.round(normalizedWords / 220)));
|
|
2155
|
+
const min = Math.max(2, center - 1);
|
|
2156
|
+
const max = Math.min(10, center + 1);
|
|
2157
|
+
return {
|
|
2158
|
+
min,
|
|
2159
|
+
max,
|
|
2160
|
+
label: `${min} to ${max}`
|
|
2161
|
+
};
|
|
2162
|
+
}
|
|
2163
|
+
function buildArticlePlanJsonSchema(targetLengthWords) {
|
|
2164
|
+
const sectionCounts = deriveArticleSectionCounts(targetLengthWords);
|
|
1926
2165
|
return {
|
|
1927
2166
|
type: "object",
|
|
1928
2167
|
additionalProperties: false,
|
|
@@ -1984,16 +2223,12 @@ function buildArticlePlanJsonSchema(targetLength) {
|
|
|
1984
2223
|
};
|
|
1985
2224
|
}
|
|
1986
2225
|
function buildArticlePlanMessages(idea, options) {
|
|
1987
|
-
const sectionCounts =
|
|
2226
|
+
const sectionCounts = deriveArticleSectionCounts(options.targetLength);
|
|
1988
2227
|
const systemInstruction = [
|
|
1989
2228
|
"You are a senior editorial strategist. Produce a rigorous article plan for a polished long-form Markdown article.",
|
|
1990
|
-
|
|
1991
|
-
buildStyleDirective(options.style),
|
|
2229
|
+
buildArticlePlanGuideInstruction(options.intent, "article"),
|
|
1992
2230
|
buildRunContextDirective(options.contentTypes),
|
|
1993
2231
|
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
2232
|
"Return only the requested JSON."
|
|
1998
2233
|
].join(" ");
|
|
1999
2234
|
return [
|
|
@@ -2011,7 +2246,7 @@ function buildArticlePlanMessages(idea, options) {
|
|
|
2011
2246
|
"- The article should feel authoritative, practical, and clearly structured for scanning and deep reading.",
|
|
2012
2247
|
"- Generate a memorable title and a sharp subtitle that promise a concrete benefit, mechanism, or outcome.",
|
|
2013
2248
|
"- The slug must be lowercase kebab-case and publication-ready.",
|
|
2014
|
-
"- The description should work as a concise meta description
|
|
2249
|
+
"- The description should work as a concise meta description and align with the shared content brief.",
|
|
2015
2250
|
`- Plan ${sectionCounts.label} strong sections with distinct focus areas and logical progression (no repetitive section intent).`,
|
|
2016
2251
|
"- Frame section titles to reflect likely search intent or practical reader questions when appropriate.",
|
|
2017
2252
|
"- Each section description should name the mechanism, evidence type, or practical action that makes the section useful.",
|
|
@@ -2019,7 +2254,6 @@ function buildArticlePlanMessages(idea, options) {
|
|
|
2019
2254
|
"- Include a cover image description and 2 to 3 inline image descriptions.",
|
|
2020
2255
|
"- Image descriptions must be concrete and contextual, not generic stock-photo phrasing.",
|
|
2021
2256
|
"- 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
2257
|
"",
|
|
2024
2258
|
"Shared content brief context:",
|
|
2025
2259
|
`- description: ${options.contentBrief.description}`,
|
|
@@ -2087,7 +2321,7 @@ async function planArticle({
|
|
|
2087
2321
|
schemaName: "article_plan",
|
|
2088
2322
|
schema: buildArticlePlanJsonSchema(settings.targetLength),
|
|
2089
2323
|
messages: buildArticlePlanMessages(idea, {
|
|
2090
|
-
|
|
2324
|
+
intent: settings.intent,
|
|
2091
2325
|
contentTypes: settings.contentTargets.map((target) => target.contentType),
|
|
2092
2326
|
contentBrief,
|
|
2093
2327
|
targetLength: settings.targetLength
|
|
@@ -2162,40 +2396,17 @@ function slugify(value2) {
|
|
|
2162
2396
|
}
|
|
2163
2397
|
|
|
2164
2398
|
// 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
|
-
};
|
|
2399
|
+
function buildOutputShapeConstraint(contentType) {
|
|
2400
|
+
if (contentType === "x-thread") {
|
|
2401
|
+
return 'Return a numbered thread with one post per line prefixed like "1/7".';
|
|
2402
|
+
}
|
|
2403
|
+
if (contentType === "x-post") {
|
|
2404
|
+
return "Return one concise post only. Do not return numbered thread lines.";
|
|
2405
|
+
}
|
|
2406
|
+
return "";
|
|
2407
|
+
}
|
|
2197
2408
|
function buildSingleShotContentMessages(options) {
|
|
2198
|
-
const
|
|
2409
|
+
const outputShapeConstraint = buildOutputShapeConstraint(options.contentType);
|
|
2199
2410
|
const articleContext = options.articleReferenceMarkdown ? [
|
|
2200
2411
|
"Reference primary context (use as anchor source, but adapt natively for the requested channel):",
|
|
2201
2412
|
options.articleReferenceMarkdown
|
|
@@ -2213,10 +2424,9 @@ function buildSingleShotContentMessages(options) {
|
|
|
2213
2424
|
content: [
|
|
2214
2425
|
"You are a senior content strategist and copywriter.",
|
|
2215
2426
|
`Write exactly one ${options.contentType} output.`,
|
|
2216
|
-
|
|
2217
|
-
buildStyleDirective(options.style),
|
|
2427
|
+
buildChannelContentGuideInstruction(options.style, options.intent, options.contentType),
|
|
2218
2428
|
roleDirective,
|
|
2219
|
-
|
|
2429
|
+
outputShapeConstraint
|
|
2220
2430
|
].join(" ")
|
|
2221
2431
|
},
|
|
2222
2432
|
{
|
|
@@ -2257,6 +2467,7 @@ async function writeSingleShotContent({
|
|
|
2257
2467
|
role = "secondary",
|
|
2258
2468
|
primaryContentType,
|
|
2259
2469
|
style,
|
|
2470
|
+
intent,
|
|
2260
2471
|
outputIndex,
|
|
2261
2472
|
outputCountForType,
|
|
2262
2473
|
articleReferenceMarkdown,
|
|
@@ -2286,6 +2497,7 @@ async function writeSingleShotContent({
|
|
|
2286
2497
|
role,
|
|
2287
2498
|
primaryContentType,
|
|
2288
2499
|
style,
|
|
2500
|
+
intent,
|
|
2289
2501
|
outputIndex,
|
|
2290
2502
|
outputCountForType,
|
|
2291
2503
|
contentBrief,
|
|
@@ -2333,13 +2545,12 @@ var OUTRO_PARAGRAPH_COUNTS = {
|
|
|
2333
2545
|
medium: "2 to 3",
|
|
2334
2546
|
large: "3 to 5"
|
|
2335
2547
|
};
|
|
2336
|
-
function buildSystemInstruction(base, style, contentTypes,
|
|
2548
|
+
function buildSystemInstruction(base, style, intent, contentTypes, targetLengthWords) {
|
|
2337
2549
|
return [
|
|
2338
2550
|
base,
|
|
2339
|
-
|
|
2340
|
-
buildStyleDirective(style),
|
|
2551
|
+
buildArticleSectionGuideInstruction(style, intent, "article"),
|
|
2341
2552
|
buildRunContextDirective(contentTypes),
|
|
2342
|
-
buildTargetLengthDirective("article",
|
|
2553
|
+
buildTargetLengthDirective("article", targetLengthWords)
|
|
2343
2554
|
].join(" ");
|
|
2344
2555
|
}
|
|
2345
2556
|
function sharedPlanContext(plan) {
|
|
@@ -2363,14 +2574,16 @@ function sharedDraftContext(articleSoFar) {
|
|
|
2363
2574
|
normalized
|
|
2364
2575
|
].join("\n");
|
|
2365
2576
|
}
|
|
2366
|
-
function buildIntroMessages(plan, style, contentTypes,
|
|
2577
|
+
function buildIntroMessages(plan, style, intent, contentTypes, targetLengthWords, introTargetWords) {
|
|
2367
2578
|
const baseSystemInstruction = buildSystemInstruction(
|
|
2368
2579
|
"You write polished editorial prose for Markdown articles. Return only the prose body with no heading and no code fences.",
|
|
2369
2580
|
style,
|
|
2581
|
+
intent,
|
|
2370
2582
|
contentTypes,
|
|
2371
|
-
|
|
2583
|
+
targetLengthWords
|
|
2372
2584
|
);
|
|
2373
|
-
const
|
|
2585
|
+
const targetLengthAlias = resolveTargetLengthAlias(targetLengthWords);
|
|
2586
|
+
const paragraphCount = INTRO_PARAGRAPH_COUNTS[targetLengthAlias] ?? INTRO_PARAGRAPH_COUNTS["medium"];
|
|
2374
2587
|
return [
|
|
2375
2588
|
{
|
|
2376
2589
|
role: "system",
|
|
@@ -2384,20 +2597,23 @@ function buildIntroMessages(plan, style, contentTypes, targetLength) {
|
|
|
2384
2597
|
`Write the article introduction using this brief: ${plan.introBrief}`,
|
|
2385
2598
|
"Requirements:",
|
|
2386
2599
|
`- ${paragraphCount} paragraphs.`,
|
|
2600
|
+
`- Target length: about ${introTargetWords} words.`,
|
|
2387
2601
|
"- Hook the reader quickly.",
|
|
2388
2602
|
"- Set up the argument and tone for the rest of the article."
|
|
2389
2603
|
].join("\n")
|
|
2390
2604
|
}
|
|
2391
2605
|
];
|
|
2392
2606
|
}
|
|
2393
|
-
function buildSectionMessages(plan, section, articleSoFar, style, contentTypes,
|
|
2607
|
+
function buildSectionMessages(plan, section, articleSoFar, style, intent, contentTypes, targetLengthWords, sectionTargetWords) {
|
|
2394
2608
|
const baseSystemInstruction = buildSystemInstruction(
|
|
2395
2609
|
"You write in-depth Markdown article sections. Return only the prose body for the section, with no heading and no code fences.",
|
|
2396
2610
|
style,
|
|
2611
|
+
intent,
|
|
2397
2612
|
contentTypes,
|
|
2398
|
-
|
|
2613
|
+
targetLengthWords
|
|
2399
2614
|
);
|
|
2400
|
-
const
|
|
2615
|
+
const targetLengthAlias = resolveTargetLengthAlias(targetLengthWords);
|
|
2616
|
+
const paragraphCount = SECTION_PARAGRAPH_COUNTS[targetLengthAlias] ?? SECTION_PARAGRAPH_COUNTS["medium"];
|
|
2401
2617
|
return [
|
|
2402
2618
|
{
|
|
2403
2619
|
role: "system",
|
|
@@ -2414,6 +2630,7 @@ function buildSectionMessages(plan, section, articleSoFar, style, contentTypes,
|
|
|
2414
2630
|
`Section focus: ${section.description}`,
|
|
2415
2631
|
"Requirements:",
|
|
2416
2632
|
`- ${paragraphCount} paragraphs.`,
|
|
2633
|
+
`- Target length: about ${sectionTargetWords} words.`,
|
|
2417
2634
|
"- Be concrete and specific.",
|
|
2418
2635
|
"- Continue naturally from the article draft so far without rehashing prior sections.",
|
|
2419
2636
|
"- Use short Markdown lists only if they materially improve clarity."
|
|
@@ -2421,14 +2638,16 @@ function buildSectionMessages(plan, section, articleSoFar, style, contentTypes,
|
|
|
2421
2638
|
}
|
|
2422
2639
|
];
|
|
2423
2640
|
}
|
|
2424
|
-
function buildOutroMessages(plan, style, contentTypes,
|
|
2641
|
+
function buildOutroMessages(plan, style, intent, contentTypes, targetLengthWords, outroTargetWords) {
|
|
2425
2642
|
const baseSystemInstruction = buildSystemInstruction(
|
|
2426
2643
|
"You write polished editorial conclusions for Markdown articles. Return only the prose body with no heading and no code fences.",
|
|
2427
2644
|
style,
|
|
2645
|
+
intent,
|
|
2428
2646
|
contentTypes,
|
|
2429
|
-
|
|
2647
|
+
targetLengthWords
|
|
2430
2648
|
);
|
|
2431
|
-
const
|
|
2649
|
+
const targetLengthAlias = resolveTargetLengthAlias(targetLengthWords);
|
|
2650
|
+
const paragraphCount = OUTRO_PARAGRAPH_COUNTS[targetLengthAlias] ?? OUTRO_PARAGRAPH_COUNTS["medium"];
|
|
2432
2651
|
return [
|
|
2433
2652
|
{
|
|
2434
2653
|
role: "system",
|
|
@@ -2442,6 +2661,7 @@ function buildOutroMessages(plan, style, contentTypes, targetLength) {
|
|
|
2442
2661
|
`Write the article conclusion using this brief: ${plan.outroBrief}`,
|
|
2443
2662
|
"Requirements:",
|
|
2444
2663
|
`- ${paragraphCount} paragraphs.`,
|
|
2664
|
+
`- Target length: about ${outroTargetWords} words.`,
|
|
2445
2665
|
"- Synthesize the main argument.",
|
|
2446
2666
|
"- End with a strong, thoughtful closing line."
|
|
2447
2667
|
].join("\n")
|
|
@@ -2459,13 +2679,16 @@ async function writeArticleSections({
|
|
|
2459
2679
|
onLlmMetrics,
|
|
2460
2680
|
onInteraction
|
|
2461
2681
|
}) {
|
|
2682
|
+
const wordBudgets = allocateWordBudgets(settings.targetLength, plan.sections.length);
|
|
2462
2683
|
onSectionStart?.("Writing introduction");
|
|
2463
2684
|
const intro = dryRun || !openRouter ? dryRunIntro(plan) : await openRouter.requestText({
|
|
2464
2685
|
messages: buildIntroMessages(
|
|
2465
2686
|
plan,
|
|
2466
2687
|
settings.style,
|
|
2688
|
+
settings.intent,
|
|
2467
2689
|
settings.contentTargets.map((target) => target.contentType),
|
|
2468
|
-
settings.targetLength
|
|
2690
|
+
settings.targetLength,
|
|
2691
|
+
wordBudgets.intro
|
|
2469
2692
|
),
|
|
2470
2693
|
settings,
|
|
2471
2694
|
interactionContext: {
|
|
@@ -2487,8 +2710,10 @@ async function writeArticleSections({
|
|
|
2487
2710
|
section,
|
|
2488
2711
|
buildArticleSoFarContext(intro, sections),
|
|
2489
2712
|
settings.style,
|
|
2713
|
+
settings.intent,
|
|
2490
2714
|
settings.contentTargets.map((target) => target.contentType),
|
|
2491
|
-
settings.targetLength
|
|
2715
|
+
settings.targetLength,
|
|
2716
|
+
wordBudgets.sections[index] ?? wordBudgets.sections[wordBudgets.sections.length - 1] ?? 150
|
|
2492
2717
|
),
|
|
2493
2718
|
settings,
|
|
2494
2719
|
interactionContext: {
|
|
@@ -2510,8 +2735,10 @@ async function writeArticleSections({
|
|
|
2510
2735
|
messages: buildOutroMessages(
|
|
2511
2736
|
plan,
|
|
2512
2737
|
settings.style,
|
|
2738
|
+
settings.intent,
|
|
2513
2739
|
settings.contentTargets.map((target) => target.contentType),
|
|
2514
|
-
settings.targetLength
|
|
2740
|
+
settings.targetLength,
|
|
2741
|
+
wordBudgets.outro
|
|
2515
2742
|
),
|
|
2516
2743
|
settings,
|
|
2517
2744
|
interactionContext: {
|
|
@@ -2547,6 +2774,23 @@ function dryRunOutro(plan) {
|
|
|
2547
2774
|
"What matters is a workflow that can repeatedly transform a promising idea into a piece that is clear, useful, and worth reading."
|
|
2548
2775
|
].join("\n\n");
|
|
2549
2776
|
}
|
|
2777
|
+
function allocateWordBudgets(totalTargetWords, sectionCount) {
|
|
2778
|
+
const normalizedTotal = Number.isFinite(totalTargetWords) && totalTargetWords > 0 ? Math.round(totalTargetWords) : 900;
|
|
2779
|
+
const normalizedSectionCount = Math.max(1, sectionCount);
|
|
2780
|
+
const intro = Math.max(80, Math.round(normalizedTotal * 0.15));
|
|
2781
|
+
const outro = Math.max(80, Math.round(normalizedTotal * 0.1));
|
|
2782
|
+
const remainingForSections = Math.max(normalizedSectionCount * 120, normalizedTotal - intro - outro);
|
|
2783
|
+
const baseSectionWords = Math.floor(remainingForSections / normalizedSectionCount);
|
|
2784
|
+
let remainder = remainingForSections - baseSectionWords * normalizedSectionCount;
|
|
2785
|
+
const sections = Array.from({ length: normalizedSectionCount }, () => {
|
|
2786
|
+
const next = baseSectionWords + (remainder > 0 ? 1 : 0);
|
|
2787
|
+
if (remainder > 0) {
|
|
2788
|
+
remainder -= 1;
|
|
2789
|
+
}
|
|
2790
|
+
return Math.max(120, next);
|
|
2791
|
+
});
|
|
2792
|
+
return { intro, sections, outro };
|
|
2793
|
+
}
|
|
2550
2794
|
function buildArticleSoFarContext(intro, sections) {
|
|
2551
2795
|
const parts = ["## Introduction", intro.trim()];
|
|
2552
2796
|
for (const section of sections) {
|
|
@@ -2594,6 +2838,14 @@ var ReplicateClient = class {
|
|
|
2594
2838
|
const backoff = backoffMs(attempt);
|
|
2595
2839
|
retries += 1;
|
|
2596
2840
|
retryBackoffMs += backoff;
|
|
2841
|
+
options.onRetry?.({
|
|
2842
|
+
attempts,
|
|
2843
|
+
retries,
|
|
2844
|
+
retryBackoffMs,
|
|
2845
|
+
backoffMs: backoff,
|
|
2846
|
+
errorMessage: lastError.message,
|
|
2847
|
+
modelId: model
|
|
2848
|
+
});
|
|
2597
2849
|
await wait(backoff);
|
|
2598
2850
|
continue;
|
|
2599
2851
|
}
|
|
@@ -2617,7 +2869,7 @@ function wait(ms) {
|
|
|
2617
2869
|
|
|
2618
2870
|
// src/images/renderImages.ts
|
|
2619
2871
|
import { writeFile as writeFile4 } from "fs/promises";
|
|
2620
|
-
import
|
|
2872
|
+
import path6 from "path";
|
|
2621
2873
|
|
|
2622
2874
|
// src/llm/prompts/imagePrompt.ts
|
|
2623
2875
|
var imagePromptSchema = {
|
|
@@ -3296,14 +3548,15 @@ async function renderExpandedImages({
|
|
|
3296
3548
|
dryRun,
|
|
3297
3549
|
onProgress,
|
|
3298
3550
|
onRenderComplete,
|
|
3299
|
-
onInteraction
|
|
3551
|
+
onInteraction,
|
|
3552
|
+
onRetry
|
|
3300
3553
|
}) {
|
|
3301
3554
|
const renderedImages = [];
|
|
3302
3555
|
for (let index = 0; index < prompts.length; index += 1) {
|
|
3303
3556
|
const prompt = prompts[index];
|
|
3304
3557
|
onProgress?.(`Rendering image ${index + 1}/${prompts.length} with ${settings.t2i.modelId}`);
|
|
3305
3558
|
const fileName = `${prompt.kind === "cover" ? "cover" : `inline-${prompt.anchorAfterSection}`}-${index + 1}.${resolveOutputFormat(settings)}`;
|
|
3306
|
-
const outputPath =
|
|
3559
|
+
const outputPath = path6.join(assetDir, fileName);
|
|
3307
3560
|
if (dryRun || !replicate) {
|
|
3308
3561
|
const dryRunStartMs = Date.now();
|
|
3309
3562
|
await writeFile4(outputPath, `Placeholder image for: ${prompt.prompt}
|
|
@@ -3361,6 +3614,14 @@ async function renderExpandedImages({
|
|
|
3361
3614
|
runAttempts = metrics.attempts;
|
|
3362
3615
|
runRetries = metrics.retries;
|
|
3363
3616
|
runRetryBackoffMs = metrics.retryBackoffMs;
|
|
3617
|
+
},
|
|
3618
|
+
onRetry(event) {
|
|
3619
|
+
onRetry?.({
|
|
3620
|
+
imageId: prompt.id,
|
|
3621
|
+
kind: prompt.kind,
|
|
3622
|
+
retries: event.retries,
|
|
3623
|
+
errorMessage: event.errorMessage
|
|
3624
|
+
});
|
|
3364
3625
|
}
|
|
3365
3626
|
});
|
|
3366
3627
|
const bytes = await normalizeReplicateOutput(output);
|
|
@@ -4067,7 +4328,7 @@ ${body.join("\n").trim()}
|
|
|
4067
4328
|
|
|
4068
4329
|
// src/pipeline/sessionStore.ts
|
|
4069
4330
|
import { mkdir as mkdir4, readFile as readFile5, rm as rm2, writeFile as writeFile5 } from "fs/promises";
|
|
4070
|
-
import
|
|
4331
|
+
import path7 from "path";
|
|
4071
4332
|
import { z as z6 } from "zod";
|
|
4072
4333
|
var STAGE_IDS = ["shared-brief", "planning", "sections", "image-prompts", "images", "output", "links"];
|
|
4073
4334
|
var generatedArticleSectionSchema = z6.object({
|
|
@@ -4143,10 +4404,10 @@ var writeSessionStateSchema = z6.object({
|
|
|
4143
4404
|
artifact: pipelineArtifactSummarySchema.nullable()
|
|
4144
4405
|
});
|
|
4145
4406
|
function resolveWriteRoot(workingDir) {
|
|
4146
|
-
return
|
|
4407
|
+
return path7.join(workingDir, ".ideon", "write");
|
|
4147
4408
|
}
|
|
4148
4409
|
function resolveStateFilePath(workingDir) {
|
|
4149
|
-
return
|
|
4410
|
+
return path7.join(resolveWriteRoot(workingDir), "state.json");
|
|
4150
4411
|
}
|
|
4151
4412
|
async function startFreshWriteSession(seed, workingDir = process.cwd()) {
|
|
4152
4413
|
const writeRoot = resolveWriteRoot(workingDir);
|
|
@@ -4196,7 +4457,7 @@ async function saveWriteSession(state, workingDir = process.cwd()) {
|
|
|
4196
4457
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4197
4458
|
});
|
|
4198
4459
|
const statePath = resolveStateFilePath(workingDir);
|
|
4199
|
-
await mkdir4(
|
|
4460
|
+
await mkdir4(path7.dirname(statePath), { recursive: true });
|
|
4200
4461
|
await writeFile5(statePath, `${JSON.stringify(next, null, 2)}
|
|
4201
4462
|
`, "utf8");
|
|
4202
4463
|
return next;
|
|
@@ -4289,12 +4550,15 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4289
4550
|
const stages = createInitialStages({ isArticlePrimary });
|
|
4290
4551
|
options.onUpdate?.(cloneStages(stages));
|
|
4291
4552
|
const dryRun = options.dryRun ?? false;
|
|
4292
|
-
const shouldEnrichLinks = options.enrichLinks ??
|
|
4553
|
+
const shouldEnrichLinks = options.enrichLinks ?? false;
|
|
4293
4554
|
const runMode = options.runMode ?? "fresh";
|
|
4294
4555
|
const workingDir = options.workingDir ?? process.cwd();
|
|
4295
4556
|
const outputPaths = resolveOutputPaths(input.config.settings, workingDir);
|
|
4296
4557
|
const hasArticlePrimary = isArticlePrimary;
|
|
4297
4558
|
const stageTracking = /* @__PURE__ */ new Map();
|
|
4559
|
+
const stageRetryState = /* @__PURE__ */ new Map();
|
|
4560
|
+
const llmOperationRetryState = /* @__PURE__ */ new Map();
|
|
4561
|
+
const imageOperationRetryState = /* @__PURE__ */ new Map();
|
|
4298
4562
|
stageTracking.set("shared-brief", {
|
|
4299
4563
|
startedAtMs: runStartedAtMs,
|
|
4300
4564
|
endedAtMs: null,
|
|
@@ -4309,6 +4573,41 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4309
4573
|
const llmInteractions = [];
|
|
4310
4574
|
const t2iInteractions = [];
|
|
4311
4575
|
let writeSession;
|
|
4576
|
+
const applyRetryUpdate = (stageId, retryIncrement, errorMessage) => {
|
|
4577
|
+
if (retryIncrement <= 0) {
|
|
4578
|
+
return;
|
|
4579
|
+
}
|
|
4580
|
+
const stageIndex = stages.findIndex((stage) => stage.id === stageId);
|
|
4581
|
+
if (stageIndex < 0) {
|
|
4582
|
+
return;
|
|
4583
|
+
}
|
|
4584
|
+
const existing = stageRetryState.get(stageId) ?? { retries: 0, lastError: null };
|
|
4585
|
+
const next = {
|
|
4586
|
+
retries: existing.retries + retryIncrement,
|
|
4587
|
+
lastError: errorMessage && errorMessage.trim().length > 0 ? errorMessage : existing.lastError
|
|
4588
|
+
};
|
|
4589
|
+
stageRetryState.set(stageId, next);
|
|
4590
|
+
stages[stageIndex] = {
|
|
4591
|
+
...stages[stageIndex],
|
|
4592
|
+
retryCount: next.retries,
|
|
4593
|
+
lastRetryError: next.lastError ?? void 0
|
|
4594
|
+
};
|
|
4595
|
+
options.onUpdate?.(cloneStages(stages));
|
|
4596
|
+
};
|
|
4597
|
+
const onLlmInteraction = (interaction) => {
|
|
4598
|
+
llmInteractions.push(interaction);
|
|
4599
|
+
const stageId = asWriteStageId(interaction.stageId);
|
|
4600
|
+
if (!stageId) {
|
|
4601
|
+
return;
|
|
4602
|
+
}
|
|
4603
|
+
const previousRetries = llmOperationRetryState.get(interaction.operationId) ?? 0;
|
|
4604
|
+
if (interaction.retries <= previousRetries) {
|
|
4605
|
+
return;
|
|
4606
|
+
}
|
|
4607
|
+
const retryIncrement = interaction.retries - previousRetries;
|
|
4608
|
+
llmOperationRetryState.set(interaction.operationId, interaction.retries);
|
|
4609
|
+
applyRetryUpdate(stageId, retryIncrement, interaction.errorMessage);
|
|
4610
|
+
};
|
|
4312
4611
|
if (runMode === "fresh") {
|
|
4313
4612
|
writeSession = await startFreshWriteSession(
|
|
4314
4613
|
{
|
|
@@ -4360,7 +4659,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4360
4659
|
openRouter,
|
|
4361
4660
|
dryRun,
|
|
4362
4661
|
onInteraction(interaction) {
|
|
4363
|
-
|
|
4662
|
+
onLlmInteraction(interaction);
|
|
4364
4663
|
},
|
|
4365
4664
|
onLlmMetrics(metrics) {
|
|
4366
4665
|
recordLlmMetrics(stageTracking, "shared-brief", metrics);
|
|
@@ -4414,7 +4713,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4414
4713
|
openRouter,
|
|
4415
4714
|
dryRun,
|
|
4416
4715
|
onInteraction(interaction) {
|
|
4417
|
-
|
|
4716
|
+
onLlmInteraction(interaction);
|
|
4418
4717
|
},
|
|
4419
4718
|
onLlmMetrics(metrics) {
|
|
4420
4719
|
recordLlmMetrics(stageTracking, "planning", metrics);
|
|
@@ -4477,7 +4776,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4477
4776
|
openRouter,
|
|
4478
4777
|
dryRun,
|
|
4479
4778
|
onInteraction(interaction) {
|
|
4480
|
-
|
|
4779
|
+
onLlmInteraction(interaction);
|
|
4481
4780
|
},
|
|
4482
4781
|
onLlmMetrics(phase, metrics, sectionIndex) {
|
|
4483
4782
|
recordLlmMetrics(stageTracking, "sections", metrics);
|
|
@@ -4590,7 +4889,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4590
4889
|
openRouter,
|
|
4591
4890
|
dryRun,
|
|
4592
4891
|
onInteraction(interaction) {
|
|
4593
|
-
|
|
4892
|
+
onLlmInteraction(interaction);
|
|
4594
4893
|
},
|
|
4595
4894
|
onPromptComplete(metrics) {
|
|
4596
4895
|
imagePromptCalls.push({
|
|
@@ -4676,6 +4975,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4676
4975
|
role: "primary",
|
|
4677
4976
|
primaryContentType: primaryTarget.contentType,
|
|
4678
4977
|
style: input.config.settings.style,
|
|
4978
|
+
intent: input.config.settings.intent,
|
|
4679
4979
|
outputIndex: 1,
|
|
4680
4980
|
outputCountForType: 1,
|
|
4681
4981
|
articleReferenceMarkdown: void 0,
|
|
@@ -4684,7 +4984,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4684
4984
|
openRouter,
|
|
4685
4985
|
dryRun,
|
|
4686
4986
|
onInteraction(interaction) {
|
|
4687
|
-
|
|
4987
|
+
onLlmInteraction(interaction);
|
|
4688
4988
|
},
|
|
4689
4989
|
onLlmMetrics(metrics) {
|
|
4690
4990
|
recordLlmMetrics(stageTracking, "sections", metrics);
|
|
@@ -4728,12 +5028,12 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4728
5028
|
options.onUpdate?.(cloneStages(stages));
|
|
4729
5029
|
}
|
|
4730
5030
|
const baseSlug = plan?.slug ?? slugifyIdea(input.idea);
|
|
4731
|
-
const generationDir =
|
|
5031
|
+
const generationDir = path8.join(
|
|
4732
5032
|
writeSession.outputPaths.markdownOutputDir,
|
|
4733
5033
|
buildGenerationDirectoryName(baseSlug)
|
|
4734
5034
|
);
|
|
4735
5035
|
await mkdir5(generationDir, { recursive: true });
|
|
4736
|
-
const jobDefinitionPath =
|
|
5036
|
+
const jobDefinitionPath = path8.join(generationDir, "job.json");
|
|
4737
5037
|
await writeJsonFile(
|
|
4738
5038
|
jobDefinitionPath,
|
|
4739
5039
|
buildRunJobDefinition({
|
|
@@ -4746,7 +5046,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4746
5046
|
})
|
|
4747
5047
|
);
|
|
4748
5048
|
const primaryFilePrefix = toFilePrefix(primaryTarget.contentType);
|
|
4749
|
-
const primaryMarkdownPath =
|
|
5049
|
+
const primaryMarkdownPath = path8.join(generationDir, `${primaryFilePrefix}-1.md`);
|
|
4750
5050
|
const sharedAssetDir = generationDir;
|
|
4751
5051
|
if (hasArticlePrimary) {
|
|
4752
5052
|
if (imageArtifacts) {
|
|
@@ -4788,6 +5088,15 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4788
5088
|
recordStageCost(stageTracking, "images", metrics.costUsd, metrics.costSource);
|
|
4789
5089
|
addStageRetries(stageTracking, "images", metrics.retries);
|
|
4790
5090
|
},
|
|
5091
|
+
onRetry(event) {
|
|
5092
|
+
const operationKey = `images:${event.imageId}`;
|
|
5093
|
+
const previousRetries = imageOperationRetryState.get(operationKey) ?? 0;
|
|
5094
|
+
if (event.retries <= previousRetries) {
|
|
5095
|
+
return;
|
|
5096
|
+
}
|
|
5097
|
+
imageOperationRetryState.set(operationKey, event.retries);
|
|
5098
|
+
applyRetryUpdate("images", event.retries - previousRetries, event.errorMessage);
|
|
5099
|
+
},
|
|
4791
5100
|
onProgress(detail) {
|
|
4792
5101
|
stages[4] = {
|
|
4793
5102
|
...stages[4],
|
|
@@ -4872,6 +5181,15 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4872
5181
|
recordStageCost(stageTracking, "images", metrics.costUsd, metrics.costSource);
|
|
4873
5182
|
addStageRetries(stageTracking, "images", metrics.retries);
|
|
4874
5183
|
},
|
|
5184
|
+
onRetry(event) {
|
|
5185
|
+
const operationKey = `images:${event.imageId}`;
|
|
5186
|
+
const previousRetries = imageOperationRetryState.get(operationKey) ?? 0;
|
|
5187
|
+
if (event.retries <= previousRetries) {
|
|
5188
|
+
return;
|
|
5189
|
+
}
|
|
5190
|
+
imageOperationRetryState.set(operationKey, event.retries);
|
|
5191
|
+
applyRetryUpdate("images", event.retries - previousRetries, event.errorMessage);
|
|
5192
|
+
},
|
|
4875
5193
|
onProgress(detail) {
|
|
4876
5194
|
stages[4] = {
|
|
4877
5195
|
...stages[4],
|
|
@@ -4969,12 +5287,13 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4969
5287
|
})
|
|
4970
5288
|
};
|
|
4971
5289
|
options.onUpdate?.(cloneStages(stages));
|
|
4972
|
-
const markdownPath =
|
|
5290
|
+
const markdownPath = path8.join(generationDir, `${output.filePrefix}-${output.index}.md`);
|
|
4973
5291
|
try {
|
|
4974
5292
|
const content = await writeSingleShotContent({
|
|
4975
5293
|
idea: input.idea,
|
|
4976
5294
|
contentType: output.contentType,
|
|
4977
5295
|
style: input.config.settings.style,
|
|
5296
|
+
intent: input.config.settings.intent,
|
|
4978
5297
|
outputIndex: output.index,
|
|
4979
5298
|
outputCountForType: output.outputCountForType,
|
|
4980
5299
|
articleReferenceMarkdown: primaryMarkdownTemplate ?? void 0,
|
|
@@ -4985,7 +5304,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4985
5304
|
role: "secondary",
|
|
4986
5305
|
primaryContentType: primaryTarget.contentType,
|
|
4987
5306
|
onInteraction(interaction) {
|
|
4988
|
-
|
|
5307
|
+
onLlmInteraction(interaction);
|
|
4989
5308
|
},
|
|
4990
5309
|
onLlmMetrics(metrics) {
|
|
4991
5310
|
recordLlmMetrics(stageTracking, "output", metrics);
|
|
@@ -5030,7 +5349,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5030
5349
|
...item,
|
|
5031
5350
|
status: "succeeded",
|
|
5032
5351
|
detail: "Saved markdown output.",
|
|
5033
|
-
summary:
|
|
5352
|
+
summary: path8.basename(markdownPath),
|
|
5034
5353
|
analytics: {
|
|
5035
5354
|
durationMs: itemDurationMs,
|
|
5036
5355
|
costUsd: knownItemCost.usd,
|
|
@@ -5086,7 +5405,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5086
5405
|
stages[6] = {
|
|
5087
5406
|
...stages[6],
|
|
5088
5407
|
status: "succeeded",
|
|
5089
|
-
detail: "Skipped link enrichment (--
|
|
5408
|
+
detail: "Skipped link enrichment (enable with --enrich-links).",
|
|
5090
5409
|
summary: "Link enrichment disabled for this run",
|
|
5091
5410
|
stageAnalytics: snapshotStageAnalytics(stageTracking, "links")
|
|
5092
5411
|
};
|
|
@@ -5139,7 +5458,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5139
5458
|
settings: input.config.settings,
|
|
5140
5459
|
dryRun,
|
|
5141
5460
|
onInteraction(interaction) {
|
|
5142
|
-
|
|
5461
|
+
onLlmInteraction(interaction);
|
|
5143
5462
|
},
|
|
5144
5463
|
onLlmMetrics(fileId, metrics) {
|
|
5145
5464
|
recordLlmMetrics(stageTracking, "links", metrics);
|
|
@@ -5238,8 +5557,8 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5238
5557
|
llmCalls: llmInteractions,
|
|
5239
5558
|
t2iCalls: t2iInteractions
|
|
5240
5559
|
};
|
|
5241
|
-
const analyticsPath =
|
|
5242
|
-
const interactionsPath =
|
|
5560
|
+
const analyticsPath = path8.join(generationDir, "generation.analytics.json");
|
|
5561
|
+
const interactionsPath = path8.join(generationDir, "model.interactions.json");
|
|
5243
5562
|
await writeJsonFile(analyticsPath, analytics);
|
|
5244
5563
|
await writeJsonFile(interactionsPath, interactions);
|
|
5245
5564
|
const primaryMarkdownPathForArtifact = markdownPaths[0] ?? primaryMarkdownPath;
|
|
@@ -5931,6 +6250,229 @@ async function runMcpServeCommand() {
|
|
|
5931
6250
|
await startIdeonMcpServer();
|
|
5932
6251
|
}
|
|
5933
6252
|
|
|
6253
|
+
// src/cli/commands/links.ts
|
|
6254
|
+
import { readFile as readFile6, stat as stat3 } from "fs/promises";
|
|
6255
|
+
import path9 from "path";
|
|
6256
|
+
async function runLinksCommand(options, dependencies = {}) {
|
|
6257
|
+
const slug = normalizeSlug2(options.slug);
|
|
6258
|
+
const mode = normalizeMode(options.mode);
|
|
6259
|
+
const cwd2 = dependencies.cwd ?? process.cwd();
|
|
6260
|
+
const log = dependencies.log ?? ((message) => console.log(message));
|
|
6261
|
+
const resolved = await resolveRunInput({
|
|
6262
|
+
idea: `Enrich links for ${slug}`
|
|
6263
|
+
});
|
|
6264
|
+
const markdownPath = await resolveMarkdownPathForSlug2(resolved.config.settings, slug, cwd2);
|
|
6265
|
+
const frontmatter = await readFrontmatter(markdownPath);
|
|
6266
|
+
const fileId = path9.parse(markdownPath).name;
|
|
6267
|
+
const articleTitle = frontmatter.title ?? toTitleFromSlug(slug);
|
|
6268
|
+
const articleDescription = frontmatter.description ?? "";
|
|
6269
|
+
const openRouterApiKey = resolved.config.secrets.openRouterApiKey;
|
|
6270
|
+
if (!openRouterApiKey) {
|
|
6271
|
+
throw new ReportedError(
|
|
6272
|
+
"Missing OpenRouter API key. Run `ideon settings` to configure credentials or set IDEON_OPENROUTER_API_KEY."
|
|
6273
|
+
);
|
|
6274
|
+
}
|
|
6275
|
+
const openRouter = new OpenRouterClient(openRouterApiKey);
|
|
6276
|
+
const linksResult = await enrichLinks({
|
|
6277
|
+
markdownFiles: [{ markdownPath, fileId, contentType: "article" }],
|
|
6278
|
+
articleTitle,
|
|
6279
|
+
articleDescription,
|
|
6280
|
+
openRouter,
|
|
6281
|
+
settings: resolved.config.settings,
|
|
6282
|
+
dryRun: false,
|
|
6283
|
+
onItemProgress(event) {
|
|
6284
|
+
logProgress(event, log);
|
|
6285
|
+
}
|
|
6286
|
+
});
|
|
6287
|
+
const generatedLinks = linksResult[0]?.links ?? [];
|
|
6288
|
+
const linksPath = resolveLinksPath(markdownPath);
|
|
6289
|
+
const existing = await readExistingLinks(linksPath);
|
|
6290
|
+
const mergedLinks = mode === "append" ? mergeLinks(existing?.links ?? [], generatedLinks) : generatedLinks;
|
|
6291
|
+
const appendedCount = Math.max(0, mergedLinks.length - (existing?.links.length ?? 0));
|
|
6292
|
+
await writeLinksFile(markdownPath, {
|
|
6293
|
+
version: 1,
|
|
6294
|
+
links: mergedLinks
|
|
6295
|
+
});
|
|
6296
|
+
const relativeMarkdownPath = formatRelativePath2(cwd2, markdownPath);
|
|
6297
|
+
const relativeLinksPath = formatRelativePath2(cwd2, linksPath);
|
|
6298
|
+
if (mode === "fresh") {
|
|
6299
|
+
const replaced = existing ? "Replaced existing links." : "Created links sidecar.";
|
|
6300
|
+
log(`Enriched links for "${slug}".`);
|
|
6301
|
+
log(`${replaced} Saved ${generatedLinks.length} links to ${relativeLinksPath} (${relativeMarkdownPath}).`);
|
|
6302
|
+
return;
|
|
6303
|
+
}
|
|
6304
|
+
const baseCount = existing?.links.length ?? 0;
|
|
6305
|
+
const verb = existing ? "Appended and deduplicated links." : "Created links sidecar.";
|
|
6306
|
+
log(`Enriched links for "${slug}".`);
|
|
6307
|
+
log(`${verb} Base ${baseCount}, added ${appendedCount}, total ${mergedLinks.length} in ${relativeLinksPath} (${relativeMarkdownPath}).`);
|
|
6308
|
+
}
|
|
6309
|
+
function normalizeMode(rawMode) {
|
|
6310
|
+
const normalized = rawMode.trim().toLowerCase();
|
|
6311
|
+
if (normalized === "fresh" || normalized === "append") {
|
|
6312
|
+
return normalized;
|
|
6313
|
+
}
|
|
6314
|
+
throw new ReportedError(`Unsupported --mode value "${rawMode}". Use "fresh" or "append".`);
|
|
6315
|
+
}
|
|
6316
|
+
function normalizeSlug2(rawSlug) {
|
|
6317
|
+
const slug = rawSlug.trim();
|
|
6318
|
+
if (!slug) {
|
|
6319
|
+
throw new ReportedError("Slug cannot be empty. Pass the generated article slug, for example `ideon links my-article-slug`.");
|
|
6320
|
+
}
|
|
6321
|
+
if (slug.toLowerCase().endsWith(".md")) {
|
|
6322
|
+
throw new ReportedError(`Expected a slug, not a markdown filename: ${slug}. Pass the slug without .md.`);
|
|
6323
|
+
}
|
|
6324
|
+
if (slug === "." || slug === ".." || /[/\\]/.test(slug)) {
|
|
6325
|
+
throw new ReportedError(`Invalid slug "${slug}". Pass the article slug only, without any path separators.`);
|
|
6326
|
+
}
|
|
6327
|
+
return slug;
|
|
6328
|
+
}
|
|
6329
|
+
async function resolveMarkdownPathForSlug2(settings, slug, cwd2) {
|
|
6330
|
+
const outputPaths = resolveOutputPaths(settings, cwd2);
|
|
6331
|
+
const directPath = path9.join(outputPaths.markdownOutputDir, `${slug}.md`);
|
|
6332
|
+
if (await isReadableFile(directPath)) {
|
|
6333
|
+
return directPath;
|
|
6334
|
+
}
|
|
6335
|
+
const markdownFiles = await listMarkdownFilesRecursively(outputPaths.markdownOutputDir);
|
|
6336
|
+
const matches = [];
|
|
6337
|
+
for (const candidate of markdownFiles) {
|
|
6338
|
+
if (path9.basename(candidate) === `${slug}.md`) {
|
|
6339
|
+
matches.push(candidate);
|
|
6340
|
+
continue;
|
|
6341
|
+
}
|
|
6342
|
+
const frontmatter = await readFrontmatter(candidate);
|
|
6343
|
+
if (frontmatter.slug === slug) {
|
|
6344
|
+
matches.push(candidate);
|
|
6345
|
+
}
|
|
6346
|
+
}
|
|
6347
|
+
if (matches.length === 0) {
|
|
6348
|
+
throw new ReportedError(
|
|
6349
|
+
`Could not find article "${slug}". Expected a markdown file in ${outputPaths.markdownOutputDir} with frontmatter slug "${slug}".`
|
|
6350
|
+
);
|
|
6351
|
+
}
|
|
6352
|
+
return newestPath(matches);
|
|
6353
|
+
}
|
|
6354
|
+
async function newestPath(paths) {
|
|
6355
|
+
let latestPath = paths[0];
|
|
6356
|
+
let latestMtime = 0;
|
|
6357
|
+
for (const candidate of paths) {
|
|
6358
|
+
const candidateStat = await stat3(candidate);
|
|
6359
|
+
if (candidateStat.mtimeMs >= latestMtime) {
|
|
6360
|
+
latestMtime = candidateStat.mtimeMs;
|
|
6361
|
+
latestPath = candidate;
|
|
6362
|
+
}
|
|
6363
|
+
}
|
|
6364
|
+
return latestPath;
|
|
6365
|
+
}
|
|
6366
|
+
async function readFrontmatter(markdownPath) {
|
|
6367
|
+
const markdown = await readFile6(markdownPath, "utf8");
|
|
6368
|
+
return parseFrontmatter(markdown);
|
|
6369
|
+
}
|
|
6370
|
+
function parseFrontmatter(markdown) {
|
|
6371
|
+
if (!markdown.startsWith("---\n")) {
|
|
6372
|
+
return { slug: null, title: null, description: null };
|
|
6373
|
+
}
|
|
6374
|
+
const frontmatterEnd = markdown.indexOf("\n---\n", 4);
|
|
6375
|
+
if (frontmatterEnd < 0) {
|
|
6376
|
+
return { slug: null, title: null, description: null };
|
|
6377
|
+
}
|
|
6378
|
+
const block = markdown.slice(4, frontmatterEnd);
|
|
6379
|
+
return {
|
|
6380
|
+
slug: parseFrontmatterValue(block, "slug"),
|
|
6381
|
+
title: parseFrontmatterValue(block, "title"),
|
|
6382
|
+
description: parseFrontmatterValue(block, "description")
|
|
6383
|
+
};
|
|
6384
|
+
}
|
|
6385
|
+
function parseFrontmatterValue(block, key) {
|
|
6386
|
+
const pattern = new RegExp(`^${key}:\\s*(.+)$`, "m");
|
|
6387
|
+
const match = block.match(pattern);
|
|
6388
|
+
if (!match || !match[1]) {
|
|
6389
|
+
return null;
|
|
6390
|
+
}
|
|
6391
|
+
const rawValue = match[1].trim();
|
|
6392
|
+
if (!rawValue) {
|
|
6393
|
+
return null;
|
|
6394
|
+
}
|
|
6395
|
+
if (rawValue.startsWith('"') && rawValue.endsWith('"') || rawValue.startsWith("'") && rawValue.endsWith("'")) {
|
|
6396
|
+
return rawValue.slice(1, -1);
|
|
6397
|
+
}
|
|
6398
|
+
return rawValue;
|
|
6399
|
+
}
|
|
6400
|
+
function toTitleFromSlug(slug) {
|
|
6401
|
+
return slug.split("-").filter((part) => part.length > 0).map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ");
|
|
6402
|
+
}
|
|
6403
|
+
async function isReadableFile(filePath) {
|
|
6404
|
+
try {
|
|
6405
|
+
const fileStat = await stat3(filePath);
|
|
6406
|
+
return fileStat.isFile();
|
|
6407
|
+
} catch {
|
|
6408
|
+
return false;
|
|
6409
|
+
}
|
|
6410
|
+
}
|
|
6411
|
+
async function readExistingLinks(linksPath) {
|
|
6412
|
+
try {
|
|
6413
|
+
const raw = await readFile6(linksPath, "utf8");
|
|
6414
|
+
const parsed = JSON.parse(raw);
|
|
6415
|
+
const links = Array.isArray(parsed.links) ? parsed.links.filter((entry) => isValidLinkEntry(entry)).map((entry) => ({
|
|
6416
|
+
expression: entry.expression.trim(),
|
|
6417
|
+
url: entry.url.trim(),
|
|
6418
|
+
title: typeof entry.title === "string" ? entry.title : null
|
|
6419
|
+
})) : null;
|
|
6420
|
+
if (!links) {
|
|
6421
|
+
throw new ReportedError(`Invalid links sidecar format at ${linksPath}. Expected { version, links[] }.`);
|
|
6422
|
+
}
|
|
6423
|
+
return {
|
|
6424
|
+
version: typeof parsed.version === "number" ? parsed.version : 1,
|
|
6425
|
+
links
|
|
6426
|
+
};
|
|
6427
|
+
} catch (error) {
|
|
6428
|
+
if (readErrorCode2(error) === "ENOENT") {
|
|
6429
|
+
return null;
|
|
6430
|
+
}
|
|
6431
|
+
if (error instanceof ReportedError) {
|
|
6432
|
+
throw error;
|
|
6433
|
+
}
|
|
6434
|
+
const message = error instanceof Error ? error.message : "Unknown links sidecar read error.";
|
|
6435
|
+
throw new ReportedError(`Failed to read existing links from ${linksPath}: ${message}`);
|
|
6436
|
+
}
|
|
6437
|
+
}
|
|
6438
|
+
function mergeLinks(existingLinks, generatedLinks) {
|
|
6439
|
+
const merged = [];
|
|
6440
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6441
|
+
for (const entry of [...existingLinks, ...generatedLinks]) {
|
|
6442
|
+
const key = `${entry.expression.trim().toLowerCase()}::${entry.url.trim().toLowerCase()}`;
|
|
6443
|
+
if (seen.has(key)) {
|
|
6444
|
+
continue;
|
|
6445
|
+
}
|
|
6446
|
+
seen.add(key);
|
|
6447
|
+
merged.push(entry);
|
|
6448
|
+
}
|
|
6449
|
+
return merged;
|
|
6450
|
+
}
|
|
6451
|
+
function isValidLinkEntry(value2) {
|
|
6452
|
+
if (typeof value2 !== "object" || value2 === null) {
|
|
6453
|
+
return false;
|
|
6454
|
+
}
|
|
6455
|
+
const record = value2;
|
|
6456
|
+
return typeof record.expression === "string" && typeof record.url === "string" && (typeof record.title === "string" || record.title === null || record.title === void 0);
|
|
6457
|
+
}
|
|
6458
|
+
function readErrorCode2(error) {
|
|
6459
|
+
if (typeof error !== "object" || error === null || !("code" in error)) {
|
|
6460
|
+
return null;
|
|
6461
|
+
}
|
|
6462
|
+
const code = error.code;
|
|
6463
|
+
return typeof code === "string" ? code : null;
|
|
6464
|
+
}
|
|
6465
|
+
function formatRelativePath2(cwd2, targetPath) {
|
|
6466
|
+
const relativePath = path9.relative(cwd2, targetPath);
|
|
6467
|
+
return relativePath.length > 0 ? relativePath : targetPath;
|
|
6468
|
+
}
|
|
6469
|
+
function logProgress(event, log) {
|
|
6470
|
+
if (event.phase === "resolving-expression" || event.phase === "selecting-expressions") {
|
|
6471
|
+
return;
|
|
6472
|
+
}
|
|
6473
|
+
log(event.detail);
|
|
6474
|
+
}
|
|
6475
|
+
|
|
5934
6476
|
// src/cli/commands/settings.tsx
|
|
5935
6477
|
import { render } from "ink";
|
|
5936
6478
|
|
|
@@ -6303,12 +6845,12 @@ async function openSettings() {
|
|
|
6303
6845
|
}
|
|
6304
6846
|
|
|
6305
6847
|
// src/cli/commands/serve.ts
|
|
6306
|
-
import
|
|
6848
|
+
import path12 from "path";
|
|
6307
6849
|
import { spawn } from "child_process";
|
|
6308
6850
|
|
|
6309
6851
|
// src/server/previewHelpers.ts
|
|
6310
|
-
import { readdir, stat as
|
|
6311
|
-
import
|
|
6852
|
+
import { readdir, stat as stat4, readFile as readFile7 } from "fs/promises";
|
|
6853
|
+
import path10 from "path";
|
|
6312
6854
|
var DEFAULT_PORT = 4173;
|
|
6313
6855
|
var CONTENT_TYPE_ORDER = ["article", "blog-post", "x-thread", "x-post", "linkedin-post", "reddit-post", "newsletter", "landing-page-copy"];
|
|
6314
6856
|
var FILE_PREFIX_TO_CONTENT_TYPE = {
|
|
@@ -6368,8 +6910,8 @@ function extractHeadingTitle(markdown) {
|
|
|
6368
6910
|
}
|
|
6369
6911
|
async function resolveMarkdownPath(markdownPathArg, markdownOutputDir, cwd2) {
|
|
6370
6912
|
if (markdownPathArg) {
|
|
6371
|
-
const resolved =
|
|
6372
|
-
if (
|
|
6913
|
+
const resolved = path10.isAbsolute(markdownPathArg) ? markdownPathArg : path10.resolve(cwd2, markdownPathArg);
|
|
6914
|
+
if (path10.extname(resolved).toLowerCase() !== ".md") {
|
|
6373
6915
|
throw new Error(`Expected a markdown file (.md), received: ${resolved}`);
|
|
6374
6916
|
}
|
|
6375
6917
|
await assertFileExists(resolved, "Could not find markdown file");
|
|
@@ -6387,7 +6929,7 @@ async function resolveLatestMarkdown(markdownOutputDir) {
|
|
|
6387
6929
|
let latestPath = markdownCandidates[0];
|
|
6388
6930
|
let latestMtime = 0;
|
|
6389
6931
|
for (const candidate of markdownCandidates) {
|
|
6390
|
-
const fileStat = await
|
|
6932
|
+
const fileStat = await stat4(candidate);
|
|
6391
6933
|
if (fileStat.mtimeMs >= latestMtime) {
|
|
6392
6934
|
latestMtime = fileStat.mtimeMs;
|
|
6393
6935
|
latestPath = candidate;
|
|
@@ -6397,7 +6939,7 @@ async function resolveLatestMarkdown(markdownOutputDir) {
|
|
|
6397
6939
|
}
|
|
6398
6940
|
async function assertFileExists(filePath, errorPrefix) {
|
|
6399
6941
|
try {
|
|
6400
|
-
const fileStat = await
|
|
6942
|
+
const fileStat = await stat4(filePath);
|
|
6401
6943
|
if (!fileStat.isFile()) {
|
|
6402
6944
|
throw new Error(`${errorPrefix}: ${filePath}`);
|
|
6403
6945
|
}
|
|
@@ -6411,9 +6953,9 @@ function extractCoverImageUrl(markdown) {
|
|
|
6411
6953
|
return match?.[1] ?? null;
|
|
6412
6954
|
}
|
|
6413
6955
|
async function extractArticleMetadata(markdownPath) {
|
|
6414
|
-
const markdown = await
|
|
6415
|
-
const fileStat = await
|
|
6416
|
-
const slug = extractFrontmatterSlug(markdown) ??
|
|
6956
|
+
const markdown = await readFile7(markdownPath, "utf8");
|
|
6957
|
+
const fileStat = await stat4(markdownPath);
|
|
6958
|
+
const slug = extractFrontmatterSlug(markdown) ?? path10.basename(markdownPath, ".md");
|
|
6417
6959
|
const title = extractHeadingTitle(stripFrontmatter2(markdown)) ?? slug;
|
|
6418
6960
|
const body = stripFrontmatter2(markdown);
|
|
6419
6961
|
const previewSnippet = body.replace(/[#\[\]()!\-*_`]/g, "").trim().substring(0, 150);
|
|
@@ -6478,16 +7020,16 @@ async function listAllGenerations(markdownOutputDir) {
|
|
|
6478
7020
|
return generations;
|
|
6479
7021
|
}
|
|
6480
7022
|
function deriveGenerationId(markdownPath, markdownOutputDir) {
|
|
6481
|
-
const relative =
|
|
6482
|
-
const normalized = relative.split(
|
|
7023
|
+
const relative = path10.relative(markdownOutputDir, markdownPath);
|
|
7024
|
+
const normalized = relative.split(path10.sep).join("/");
|
|
6483
7025
|
if (!normalized || normalized.startsWith("../")) {
|
|
6484
|
-
return
|
|
7026
|
+
return path10.basename(markdownPath, ".md");
|
|
6485
7027
|
}
|
|
6486
7028
|
const segments = normalized.split("/").filter(Boolean);
|
|
6487
7029
|
if (segments.length <= 1) {
|
|
6488
|
-
return
|
|
7030
|
+
return path10.basename(markdownPath, ".md");
|
|
6489
7031
|
}
|
|
6490
|
-
return segments[0] ??
|
|
7032
|
+
return segments[0] ?? path10.basename(markdownPath, ".md");
|
|
6491
7033
|
}
|
|
6492
7034
|
async function findMarkdownFiles(markdownOutputDir) {
|
|
6493
7035
|
const files = [];
|
|
@@ -6504,7 +7046,7 @@ async function findMarkdownFiles(markdownOutputDir) {
|
|
|
6504
7046
|
continue;
|
|
6505
7047
|
}
|
|
6506
7048
|
for (const entry of entries) {
|
|
6507
|
-
const fullPath =
|
|
7049
|
+
const fullPath = path10.join(current, entry.name);
|
|
6508
7050
|
if (entry.isDirectory()) {
|
|
6509
7051
|
stack.push(fullPath);
|
|
6510
7052
|
continue;
|
|
@@ -6518,7 +7060,7 @@ async function findMarkdownFiles(markdownOutputDir) {
|
|
|
6518
7060
|
}
|
|
6519
7061
|
function deriveOutputIdentity(markdownPath, markdownOutputDir) {
|
|
6520
7062
|
const generationId = deriveGenerationId(markdownPath, markdownOutputDir);
|
|
6521
|
-
const fileBase =
|
|
7063
|
+
const fileBase = path10.basename(markdownPath, ".md");
|
|
6522
7064
|
const parsed = fileBase.match(/^([a-z0-9-]+)-(\d+)$/i);
|
|
6523
7065
|
if (!parsed || !parsed[1] || !parsed[2]) {
|
|
6524
7066
|
return {
|
|
@@ -6554,13 +7096,13 @@ function toContentTypeLabel(contentType) {
|
|
|
6554
7096
|
}
|
|
6555
7097
|
async function resolvePrimaryContentType(outputs) {
|
|
6556
7098
|
const fallback = outputs.find((output) => output.contentType === "article")?.contentType ?? outputs[0]?.contentType ?? "article";
|
|
6557
|
-
const generationDir =
|
|
7099
|
+
const generationDir = path10.dirname(outputs[0]?.sourcePath ?? "");
|
|
6558
7100
|
if (!generationDir) {
|
|
6559
7101
|
return fallback;
|
|
6560
7102
|
}
|
|
6561
|
-
const jobPath =
|
|
7103
|
+
const jobPath = path10.join(generationDir, "job.json");
|
|
6562
7104
|
try {
|
|
6563
|
-
const raw = await
|
|
7105
|
+
const raw = await readFile7(jobPath, "utf8");
|
|
6564
7106
|
const parsed = JSON.parse(raw);
|
|
6565
7107
|
const targets = Array.isArray(parsed.contentTargets) ? parsed.contentTargets : Array.isArray(parsed.settings?.contentTargets) ? parsed.settings.contentTargets : [];
|
|
6566
7108
|
const primary = targets.find((target) => target?.role === "primary" && typeof target.contentType === "string");
|
|
@@ -6576,9 +7118,9 @@ async function resolvePrimaryContentType(outputs) {
|
|
|
6576
7118
|
// src/server/previewServer.ts
|
|
6577
7119
|
import { execFile } from "child_process";
|
|
6578
7120
|
import { promisify } from "util";
|
|
6579
|
-
import { readFile as
|
|
7121
|
+
import { readFile as readFile8, stat as stat5 } from "fs/promises";
|
|
6580
7122
|
import { watch as fsWatch } from "fs";
|
|
6581
|
-
import
|
|
7123
|
+
import path11 from "path";
|
|
6582
7124
|
import { fileURLToPath } from "url";
|
|
6583
7125
|
import express from "express";
|
|
6584
7126
|
import { marked } from "marked";
|
|
@@ -6731,7 +7273,7 @@ async function startPreviewServer(options) {
|
|
|
6731
7273
|
if (options.watch) {
|
|
6732
7274
|
let html2;
|
|
6733
7275
|
try {
|
|
6734
|
-
html2 = await
|
|
7276
|
+
html2 = await readFile8(path11.join(previewClientDir, "index.html"), "utf8");
|
|
6735
7277
|
} catch {
|
|
6736
7278
|
res.status(200).type("html").send(
|
|
6737
7279
|
`<!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 +7284,7 @@ async function startPreviewServer(options) {
|
|
|
6742
7284
|
const injected = html2.replace("</body>", `${reloadScript}</body>`);
|
|
6743
7285
|
res.status(200).type("html").send(injected);
|
|
6744
7286
|
} else {
|
|
6745
|
-
res.status(200).sendFile(
|
|
7287
|
+
res.status(200).sendFile(path11.join(previewClientDir, "index.html"));
|
|
6746
7288
|
}
|
|
6747
7289
|
return;
|
|
6748
7290
|
}
|
|
@@ -6795,7 +7337,7 @@ async function getArticleContent(generationId, markdownOutputDir) {
|
|
|
6795
7337
|
generation.outputs.map(async (output) => {
|
|
6796
7338
|
let markdown = "";
|
|
6797
7339
|
try {
|
|
6798
|
-
markdown = await
|
|
7340
|
+
markdown = await readFile8(output.sourcePath, "utf8");
|
|
6799
7341
|
} catch (error) {
|
|
6800
7342
|
if (isMissingFileError(error)) {
|
|
6801
7343
|
throw new MissingArticleError(`Generation "${generationId}" no longer exists.`);
|
|
@@ -6813,7 +7355,7 @@ async function getArticleContent(generationId, markdownOutputDir) {
|
|
|
6813
7355
|
};
|
|
6814
7356
|
})
|
|
6815
7357
|
);
|
|
6816
|
-
const generationDir =
|
|
7358
|
+
const generationDir = path11.dirname(generation.outputs[0]?.sourcePath ?? "");
|
|
6817
7359
|
const interactions = generationDir ? await loadSavedInteractions(generationDir) : { llmCalls: [], t2iCalls: [] };
|
|
6818
7360
|
const analyticsSummary = generationDir ? await loadSavedAnalyticsSummary(generationDir) : null;
|
|
6819
7361
|
return {
|
|
@@ -6842,7 +7384,7 @@ async function resolveActivePreviewArticle(preferredMarkdownPath, markdownOutput
|
|
|
6842
7384
|
};
|
|
6843
7385
|
}
|
|
6844
7386
|
function resolveGenerationSourcePath(generation, markdownOutputDir) {
|
|
6845
|
-
return generation.outputs.find((output) => output.contentType === generation.primaryContentType)?.sourcePath ?? generation.outputs[0]?.sourcePath ??
|
|
7387
|
+
return generation.outputs.find((output) => output.contentType === generation.primaryContentType)?.sourcePath ?? generation.outputs[0]?.sourcePath ?? path11.join(markdownOutputDir, generation.id);
|
|
6846
7388
|
}
|
|
6847
7389
|
function isMissingFileError(error) {
|
|
6848
7390
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
@@ -6857,7 +7399,7 @@ async function renderArticleHtml(markdown, generationId, sourcePath) {
|
|
|
6857
7399
|
async function loadSavedLinks(markdownPath) {
|
|
6858
7400
|
const linksPath = resolveLinksPath(markdownPath);
|
|
6859
7401
|
try {
|
|
6860
|
-
const raw = await
|
|
7402
|
+
const raw = await readFile8(linksPath, "utf8");
|
|
6861
7403
|
const parsed = JSON.parse(raw);
|
|
6862
7404
|
if (!Array.isArray(parsed.links)) {
|
|
6863
7405
|
return [];
|
|
@@ -6881,9 +7423,9 @@ async function loadSavedLinks(markdownPath) {
|
|
|
6881
7423
|
}
|
|
6882
7424
|
}
|
|
6883
7425
|
async function loadSavedInteractions(generationDir) {
|
|
6884
|
-
const interactionsPath =
|
|
7426
|
+
const interactionsPath = path11.join(generationDir, "model.interactions.json");
|
|
6885
7427
|
try {
|
|
6886
|
-
const raw = await
|
|
7428
|
+
const raw = await readFile8(interactionsPath, "utf8");
|
|
6887
7429
|
const parsed = JSON.parse(raw);
|
|
6888
7430
|
const llmCalls = Array.isArray(parsed.llmCalls) ? parsed.llmCalls.filter(isPreviewLlmInteraction) : [];
|
|
6889
7431
|
const t2iCalls = Array.isArray(parsed.t2iCalls) ? parsed.t2iCalls.filter(isPreviewT2IInteraction) : [];
|
|
@@ -6899,9 +7441,9 @@ async function loadSavedInteractions(generationDir) {
|
|
|
6899
7441
|
}
|
|
6900
7442
|
}
|
|
6901
7443
|
async function loadSavedAnalyticsSummary(generationDir) {
|
|
6902
|
-
const analyticsPath =
|
|
7444
|
+
const analyticsPath = path11.join(generationDir, "generation.analytics.json");
|
|
6903
7445
|
try {
|
|
6904
|
-
const raw = await
|
|
7446
|
+
const raw = await readFile8(analyticsPath, "utf8");
|
|
6905
7447
|
const parsed = JSON.parse(raw);
|
|
6906
7448
|
const summary = parsed.summary;
|
|
6907
7449
|
if (!summary || typeof summary !== "object") {
|
|
@@ -6933,14 +7475,14 @@ async function getPreviewBootstrapData(preferredMarkdownPath, markdownOutputDir)
|
|
|
6933
7475
|
};
|
|
6934
7476
|
}
|
|
6935
7477
|
async function resolvePreviewClientBuildDir() {
|
|
6936
|
-
const currentDir =
|
|
7478
|
+
const currentDir = path11.dirname(fileURLToPath(import.meta.url));
|
|
6937
7479
|
const candidates = [
|
|
6938
|
-
|
|
6939
|
-
|
|
7480
|
+
path11.resolve(currentDir, "preview"),
|
|
7481
|
+
path11.resolve(currentDir, "../../dist/preview")
|
|
6940
7482
|
];
|
|
6941
7483
|
for (const candidate of candidates) {
|
|
6942
7484
|
try {
|
|
6943
|
-
const indexStat = await
|
|
7485
|
+
const indexStat = await stat5(path11.join(candidate, "index.html"));
|
|
6944
7486
|
if (indexStat.isFile()) {
|
|
6945
7487
|
return candidate;
|
|
6946
7488
|
}
|
|
@@ -7002,21 +7544,21 @@ async function resolveGenerationAssetPath(generationId, rawAssetPath, markdownOu
|
|
|
7002
7544
|
throw new MissingArticleError(`Generation "${generationId}" no longer exists.`);
|
|
7003
7545
|
}
|
|
7004
7546
|
const decodedAssetPath = decodeURIComponent(rawAssetPath);
|
|
7005
|
-
const normalizedRelative =
|
|
7006
|
-
if (normalizedRelative.length === 0 || normalizedRelative === "." || normalizedRelative.startsWith("../") || normalizedRelative.includes("/../") ||
|
|
7547
|
+
const normalizedRelative = path11.posix.normalize(decodedAssetPath.replace(/\\/g, "/"));
|
|
7548
|
+
if (normalizedRelative.length === 0 || normalizedRelative === "." || normalizedRelative.startsWith("../") || normalizedRelative.includes("/../") || path11.posix.isAbsolute(normalizedRelative)) {
|
|
7007
7549
|
throw new Error("Invalid generation asset path.");
|
|
7008
7550
|
}
|
|
7009
|
-
const generationDir =
|
|
7551
|
+
const generationDir = path11.dirname(generation.outputs[0]?.sourcePath ?? "");
|
|
7010
7552
|
if (!generationDir) {
|
|
7011
7553
|
throw new MissingArticleError(`Generation "${generationId}" has no source directory.`);
|
|
7012
7554
|
}
|
|
7013
|
-
const resolvedPath =
|
|
7014
|
-
const relativeToGeneration =
|
|
7015
|
-
if (relativeToGeneration.startsWith("..") ||
|
|
7555
|
+
const resolvedPath = path11.resolve(generationDir, normalizedRelative);
|
|
7556
|
+
const relativeToGeneration = path11.relative(generationDir, resolvedPath);
|
|
7557
|
+
if (relativeToGeneration.startsWith("..") || path11.isAbsolute(relativeToGeneration)) {
|
|
7016
7558
|
throw new Error("Invalid generation asset path.");
|
|
7017
7559
|
}
|
|
7018
7560
|
try {
|
|
7019
|
-
const fileStat = await
|
|
7561
|
+
const fileStat = await stat5(resolvedPath);
|
|
7020
7562
|
if (!fileStat.isFile()) {
|
|
7021
7563
|
throw new Error("Invalid generation asset path.");
|
|
7022
7564
|
}
|
|
@@ -8527,7 +9069,7 @@ async function runServeCommand(options) {
|
|
|
8527
9069
|
const markdownPath = await resolveMarkdownPath(options.markdownPath, outputPaths.markdownOutputDir, process.cwd());
|
|
8528
9070
|
const port = parsePort(options.port);
|
|
8529
9071
|
if (options.watch) {
|
|
8530
|
-
const viteBin =
|
|
9072
|
+
const viteBin = path12.resolve(process.cwd(), "node_modules", ".bin", "vite");
|
|
8531
9073
|
const viteProcess = spawn(viteBin, ["build", "--watch"], {
|
|
8532
9074
|
stdio: "inherit",
|
|
8533
9075
|
shell: process.platform === "win32"
|
|
@@ -8553,8 +9095,8 @@ async function runServeCommand(options) {
|
|
|
8553
9095
|
openBrowser: options.openBrowser,
|
|
8554
9096
|
watch: options.watch
|
|
8555
9097
|
});
|
|
8556
|
-
const relativeArticle =
|
|
8557
|
-
const relativeAssets =
|
|
9098
|
+
const relativeArticle = path12.relative(process.cwd(), markdownPath);
|
|
9099
|
+
const relativeAssets = path12.relative(process.cwd(), outputPaths.assetOutputDir);
|
|
8558
9100
|
console.log(`Previewing ${relativeArticle || markdownPath}`);
|
|
8559
9101
|
console.log(`Serving assets from ${relativeAssets || outputPaths.assetOutputDir}`);
|
|
8560
9102
|
console.log(`Open ${server.url}`);
|
|
@@ -8594,11 +9136,28 @@ function colon(id) {
|
|
|
8594
9136
|
function value(id, text) {
|
|
8595
9137
|
return { id, text, color: "white" };
|
|
8596
9138
|
}
|
|
9139
|
+
function formatStageCost(costUsd, costSource) {
|
|
9140
|
+
const formatted = formatCost(costUsd);
|
|
9141
|
+
if (costUsd === null) {
|
|
9142
|
+
return formatted;
|
|
9143
|
+
}
|
|
9144
|
+
return costSource === "estimated" ? `~${formatted}` : formatted;
|
|
9145
|
+
}
|
|
9146
|
+
function formatStageId(stageId) {
|
|
9147
|
+
if (stageId === "shared-brief") return "shared-brief";
|
|
9148
|
+
if (stageId === "planning") return "planning";
|
|
9149
|
+
if (stageId === "sections") return "sections";
|
|
9150
|
+
if (stageId === "image-prompts") return "image-prompts";
|
|
9151
|
+
if (stageId === "images") return "images";
|
|
9152
|
+
if (stageId === "output") return "output";
|
|
9153
|
+
if (stageId === "links") return "links";
|
|
9154
|
+
return stageId;
|
|
9155
|
+
}
|
|
8597
9156
|
function buildFinalSummaryRows({
|
|
8598
9157
|
artifact,
|
|
8599
9158
|
analytics
|
|
8600
9159
|
}) {
|
|
8601
|
-
|
|
9160
|
+
const rows = [
|
|
8602
9161
|
{
|
|
8603
9162
|
id: "slug",
|
|
8604
9163
|
segments: [
|
|
@@ -8648,6 +9207,16 @@ function buildFinalSummaryRows({
|
|
|
8648
9207
|
]
|
|
8649
9208
|
}
|
|
8650
9209
|
];
|
|
9210
|
+
const stageCostRows = analytics.stages.map((stage) => ({
|
|
9211
|
+
id: `stage-cost:${stage.stageId}`,
|
|
9212
|
+
segments: [
|
|
9213
|
+
label(`stage-cost-label:${stage.stageId}`, `cost/${formatStageId(stage.stageId)}`, "greenBright"),
|
|
9214
|
+
colon(`stage-cost-colon:${stage.stageId}`),
|
|
9215
|
+
value(`stage-cost-value:${stage.stageId}`, formatStageCost(stage.costUsd, stage.costSource))
|
|
9216
|
+
]
|
|
9217
|
+
}));
|
|
9218
|
+
rows.push(...stageCostRows);
|
|
9219
|
+
return rows;
|
|
8651
9220
|
}
|
|
8652
9221
|
|
|
8653
9222
|
// src/cli/ui/finalSummary.tsx
|
|
@@ -8715,7 +9284,7 @@ function formatDuration(durationMs) {
|
|
|
8715
9284
|
}
|
|
8716
9285
|
return `${durationMs}ms`;
|
|
8717
9286
|
}
|
|
8718
|
-
function
|
|
9287
|
+
function formatStageCost2(stage) {
|
|
8719
9288
|
const analytics = stage.stageAnalytics;
|
|
8720
9289
|
if (!analytics || analytics.costUsd === null) {
|
|
8721
9290
|
return "no cost data";
|
|
@@ -8723,6 +9292,15 @@ function formatStageCost(stage) {
|
|
|
8723
9292
|
const formatted = `$${analytics.costUsd.toFixed(4)}`;
|
|
8724
9293
|
return analytics.costSource === "estimated" ? `~${formatted}` : formatted;
|
|
8725
9294
|
}
|
|
9295
|
+
function formatRetryContext(stage) {
|
|
9296
|
+
if (!stage.retryCount || stage.retryCount <= 0) {
|
|
9297
|
+
return "";
|
|
9298
|
+
}
|
|
9299
|
+
if (stage.lastRetryError) {
|
|
9300
|
+
return ` \u2022 retried ${stage.retryCount}x \u2022 last error: ${stage.lastRetryError}`;
|
|
9301
|
+
}
|
|
9302
|
+
return ` \u2022 retried ${stage.retryCount}x`;
|
|
9303
|
+
}
|
|
8726
9304
|
function StageRow({
|
|
8727
9305
|
stage,
|
|
8728
9306
|
isActive,
|
|
@@ -8748,7 +9326,10 @@ function StageRow({
|
|
|
8748
9326
|
/* @__PURE__ */ jsx4(Text3, { children: " " }),
|
|
8749
9327
|
/* @__PURE__ */ jsx4(Text3, { bold: stage.status === "running", children: stage.title })
|
|
8750
9328
|
] }),
|
|
8751
|
-
/* @__PURE__ */ jsx4(Box3, { marginLeft: 2, children: /* @__PURE__ */
|
|
9329
|
+
/* @__PURE__ */ jsx4(Box3, { marginLeft: 2, children: /* @__PURE__ */ jsxs3(Text3, { color: "gray", children: [
|
|
9330
|
+
stage.detail,
|
|
9331
|
+
formatRetryContext(stage)
|
|
9332
|
+
] }) }),
|
|
8752
9333
|
/* @__PURE__ */ jsx4(
|
|
8753
9334
|
ItemRows,
|
|
8754
9335
|
{
|
|
@@ -8763,7 +9344,7 @@ function StageRow({
|
|
|
8763
9344
|
"analytics: ",
|
|
8764
9345
|
formatDuration(stage.stageAnalytics.durationMs),
|
|
8765
9346
|
" \u2022 cost: ",
|
|
8766
|
-
|
|
9347
|
+
formatStageCost2(stage)
|
|
8767
9348
|
] }) }) : null
|
|
8768
9349
|
] });
|
|
8769
9350
|
}
|
|
@@ -8947,7 +9528,7 @@ function formatDuration2(durationMs) {
|
|
|
8947
9528
|
}
|
|
8948
9529
|
return `${durationMs}ms`;
|
|
8949
9530
|
}
|
|
8950
|
-
function
|
|
9531
|
+
function formatStageCost3(stage) {
|
|
8951
9532
|
const analytics = stage.stageAnalytics;
|
|
8952
9533
|
if (!analytics) {
|
|
8953
9534
|
return "unavailable";
|
|
@@ -8959,9 +9540,19 @@ function formatStage(stage) {
|
|
|
8959
9540
|
const summary = stage.summary ? `
|
|
8960
9541
|
${stage.summary}` : "";
|
|
8961
9542
|
const analytics = stage.status === "succeeded" && stage.stageAnalytics ? `
|
|
8962
|
-
analytics: ${formatDuration2(stage.stageAnalytics.durationMs)} \u2022 cost: ${
|
|
9543
|
+
analytics: ${formatDuration2(stage.stageAnalytics.durationMs)} \u2022 cost: ${formatStageCost3(stage)}` : "";
|
|
9544
|
+
const retryContext = formatRetryContext2(stage);
|
|
8963
9545
|
return `[${stage.status}] ${stage.title}
|
|
8964
|
-
${stage.detail}${summary}${analytics}`;
|
|
9546
|
+
${stage.detail}${retryContext}${summary}${analytics}`;
|
|
9547
|
+
}
|
|
9548
|
+
function formatRetryContext2(stage) {
|
|
9549
|
+
if (!stage.retryCount || stage.retryCount <= 0) {
|
|
9550
|
+
return "";
|
|
9551
|
+
}
|
|
9552
|
+
if (stage.lastRetryError) {
|
|
9553
|
+
return ` \u2022 retried ${stage.retryCount}x \u2022 last error: ${stage.lastRetryError}`;
|
|
9554
|
+
}
|
|
9555
|
+
return ` \u2022 retried ${stage.retryCount}x`;
|
|
8965
9556
|
}
|
|
8966
9557
|
function formatItem(stage, item) {
|
|
8967
9558
|
const summary = item.summary ? `
|
|
@@ -8984,8 +9575,15 @@ function formatCost2(costUsd) {
|
|
|
8984
9575
|
}
|
|
8985
9576
|
return `$${costUsd.toFixed(4)}`;
|
|
8986
9577
|
}
|
|
9578
|
+
function formatPipelineStageCost(stage) {
|
|
9579
|
+
const formatted = formatCost2(stage.costUsd);
|
|
9580
|
+
if (stage.costUsd === null) {
|
|
9581
|
+
return formatted;
|
|
9582
|
+
}
|
|
9583
|
+
return stage.costSource === "estimated" ? `~${formatted}` : formatted;
|
|
9584
|
+
}
|
|
8987
9585
|
async function renderPlainPipeline(input, dryRun, enrichLinks2, runMode) {
|
|
8988
|
-
let
|
|
9586
|
+
let previousStages = /* @__PURE__ */ new Map();
|
|
8989
9587
|
let previousItemStatuses = /* @__PURE__ */ new Map();
|
|
8990
9588
|
const notificationsEnabled = input.config.settings.notifications.enabled;
|
|
8991
9589
|
try {
|
|
@@ -9000,10 +9598,16 @@ async function renderPlainPipeline(input, dryRun, enrichLinks2, runMode) {
|
|
|
9000
9598
|
runMode,
|
|
9001
9599
|
onUpdate(stages) {
|
|
9002
9600
|
for (const stage of stages) {
|
|
9003
|
-
const previous =
|
|
9004
|
-
|
|
9601
|
+
const previous = previousStages.get(stage.id);
|
|
9602
|
+
const shouldLogStage = !previous || previous.status !== stage.status || stage.status === "running" && (previous.detail !== stage.detail || previous.retryCount !== stage.retryCount || previous.lastRetryError !== stage.lastRetryError);
|
|
9603
|
+
if (shouldLogStage) {
|
|
9005
9604
|
console.log(formatStage(stage));
|
|
9006
|
-
|
|
9605
|
+
previousStages.set(stage.id, {
|
|
9606
|
+
status: stage.status,
|
|
9607
|
+
detail: stage.detail,
|
|
9608
|
+
retryCount: stage.retryCount,
|
|
9609
|
+
lastRetryError: stage.lastRetryError
|
|
9610
|
+
});
|
|
9007
9611
|
}
|
|
9008
9612
|
for (const item of stage.items ?? []) {
|
|
9009
9613
|
const itemKey = `${stage.id}:${item.id}`;
|
|
@@ -9029,6 +9633,10 @@ async function renderPlainPipeline(input, dryRun, enrichLinks2, runMode) {
|
|
|
9029
9633
|
console.log(` duration_ms: ${result.analytics.summary.totalDurationMs}`);
|
|
9030
9634
|
console.log(` retries: ${result.analytics.summary.totalRetries}`);
|
|
9031
9635
|
console.log(` cost: ${formatCost2(result.analytics.summary.totalCostUsd)}`);
|
|
9636
|
+
console.log(" cost_by_stage:");
|
|
9637
|
+
for (const stage of result.analytics.stages) {
|
|
9638
|
+
console.log(` ${stage.stageId}: ${formatPipelineStageCost(stage)}`);
|
|
9639
|
+
}
|
|
9032
9640
|
await notifyWriteSucceeded({
|
|
9033
9641
|
enabled: notificationsEnabled,
|
|
9034
9642
|
title: result.artifact.title,
|
|
@@ -9053,9 +9661,11 @@ import TextInput2 from "ink-text-input";
|
|
|
9053
9661
|
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
9054
9662
|
function WriteOptionsFlow({
|
|
9055
9663
|
askStyle,
|
|
9664
|
+
askIntent,
|
|
9056
9665
|
askTargets,
|
|
9057
9666
|
askLength,
|
|
9058
9667
|
initialStyle,
|
|
9668
|
+
initialIntent,
|
|
9059
9669
|
initialTargetLength,
|
|
9060
9670
|
initialTargets,
|
|
9061
9671
|
onDone
|
|
@@ -9064,6 +9674,7 @@ function WriteOptionsFlow({
|
|
|
9064
9674
|
const [step, setStep] = useState3(() => {
|
|
9065
9675
|
if (askTargets) return "primary";
|
|
9066
9676
|
if (askStyle) return "style";
|
|
9677
|
+
if (askIntent) return "intent";
|
|
9067
9678
|
if (askLength) return "length";
|
|
9068
9679
|
return "primary";
|
|
9069
9680
|
});
|
|
@@ -9089,6 +9700,7 @@ function WriteOptionsFlow({
|
|
|
9089
9700
|
const [countInput, setCountInput] = useState3("1");
|
|
9090
9701
|
const [countIndex, setCountIndex] = useState3(0);
|
|
9091
9702
|
const [style, setStyle] = useState3(initialStyle);
|
|
9703
|
+
const [intent, setIntent] = useState3(initialIntent);
|
|
9092
9704
|
const [targetLength, setTargetLength] = useState3(initialTargetLength);
|
|
9093
9705
|
const selectedSecondaryTypes = useMemo2(
|
|
9094
9706
|
() => secondarySelections.filter((item) => item.checked).map((item) => item.contentType),
|
|
@@ -9215,6 +9827,8 @@ function WriteOptionsFlow({
|
|
|
9215
9827
|
if (nextIndex >= countTypes.length) {
|
|
9216
9828
|
if (askStyle) {
|
|
9217
9829
|
setStep("style");
|
|
9830
|
+
} else if (askIntent) {
|
|
9831
|
+
setStep("intent");
|
|
9218
9832
|
} else if (askLength) {
|
|
9219
9833
|
setStep("length");
|
|
9220
9834
|
} else {
|
|
@@ -9252,6 +9866,10 @@ function WriteOptionsFlow({
|
|
|
9252
9866
|
onSelect: (item) => {
|
|
9253
9867
|
const contentTargets = askTargets ? buildContentTargets(primaryType, selectedSecondaryTypes) : void 0;
|
|
9254
9868
|
setStyle(item.value);
|
|
9869
|
+
if (askIntent) {
|
|
9870
|
+
setStep("intent");
|
|
9871
|
+
return;
|
|
9872
|
+
}
|
|
9255
9873
|
if (askLength) {
|
|
9256
9874
|
setStep("length");
|
|
9257
9875
|
return;
|
|
@@ -9266,6 +9884,37 @@ function WriteOptionsFlow({
|
|
|
9266
9884
|
) })
|
|
9267
9885
|
] });
|
|
9268
9886
|
}
|
|
9887
|
+
const intentItems = contentIntentValues.map((value2) => ({
|
|
9888
|
+
label: value2,
|
|
9889
|
+
value: value2
|
|
9890
|
+
}));
|
|
9891
|
+
if (step === "intent") {
|
|
9892
|
+
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
|
|
9893
|
+
/* @__PURE__ */ jsx6(Text5, { bold: true, color: "cyanBright", children: "Select Intent" }),
|
|
9894
|
+
/* @__PURE__ */ jsx6(Text5, { color: "gray", children: "Choose the primary content intent for this generation run." }),
|
|
9895
|
+
/* @__PURE__ */ jsx6(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx6(
|
|
9896
|
+
SelectInput2,
|
|
9897
|
+
{
|
|
9898
|
+
items: intentItems,
|
|
9899
|
+
initialIndex: Math.max(0, intentItems.findIndex((item) => item.value === intent)),
|
|
9900
|
+
onSelect: (item) => {
|
|
9901
|
+
const contentTargets = askTargets ? buildContentTargets(primaryType, selectedSecondaryTypes) : void 0;
|
|
9902
|
+
setIntent(item.value);
|
|
9903
|
+
if (askLength) {
|
|
9904
|
+
setStep("length");
|
|
9905
|
+
return;
|
|
9906
|
+
}
|
|
9907
|
+
onDone({
|
|
9908
|
+
...askStyle ? { style } : {},
|
|
9909
|
+
intent: item.value,
|
|
9910
|
+
...contentTargets ? { contentTargets } : {}
|
|
9911
|
+
});
|
|
9912
|
+
exit();
|
|
9913
|
+
}
|
|
9914
|
+
}
|
|
9915
|
+
) })
|
|
9916
|
+
] });
|
|
9917
|
+
}
|
|
9269
9918
|
const lengthItems = targetLengthValues.map((value2) => ({
|
|
9270
9919
|
label: value2,
|
|
9271
9920
|
value: value2
|
|
@@ -9284,6 +9933,7 @@ function WriteOptionsFlow({
|
|
|
9284
9933
|
setTargetLength(item.value);
|
|
9285
9934
|
onDone({
|
|
9286
9935
|
...askStyle ? { style } : {},
|
|
9936
|
+
...askIntent ? { intent } : {},
|
|
9287
9937
|
targetLength: item.value,
|
|
9288
9938
|
...contentTargets ? { contentTargets } : {}
|
|
9289
9939
|
});
|
|
@@ -9397,7 +10047,7 @@ async function runWriteResumeCommand(options = {}) {
|
|
|
9397
10047
|
secrets: resolved.config.secrets
|
|
9398
10048
|
}
|
|
9399
10049
|
};
|
|
9400
|
-
await runWritePipeline(input, session.dryRun,
|
|
10050
|
+
await runWritePipeline(input, session.dryRun, options.enrichLinks ?? false, "resume", options.noInteractive ?? false);
|
|
9401
10051
|
}
|
|
9402
10052
|
async function runWritePipeline(input, dryRun, enrichLinks2, runMode, noInteractive) {
|
|
9403
10053
|
let interruptHandled = false;
|
|
@@ -9482,6 +10132,7 @@ async function resolveInputWithInteractiveIdeaFallback(options) {
|
|
|
9482
10132
|
audience: options.audience,
|
|
9483
10133
|
jobPath: options.jobPath,
|
|
9484
10134
|
style: options.style,
|
|
10135
|
+
intent: options.intent,
|
|
9485
10136
|
targetLength: options.length,
|
|
9486
10137
|
contentTargets: parsedTargets
|
|
9487
10138
|
});
|
|
@@ -9499,6 +10150,7 @@ async function resolveInputWithInteractiveIdeaFallback(options) {
|
|
|
9499
10150
|
audience: options.audience,
|
|
9500
10151
|
jobPath: options.jobPath,
|
|
9501
10152
|
style: options.style,
|
|
10153
|
+
intent: options.intent,
|
|
9502
10154
|
targetLength: options.length,
|
|
9503
10155
|
contentTargets: parsedTargets
|
|
9504
10156
|
});
|
|
@@ -9507,12 +10159,14 @@ async function resolveInputWithInteractiveIdeaFallback(options) {
|
|
|
9507
10159
|
}
|
|
9508
10160
|
async function applyInteractiveWriteOptionsIfNeeded(resolved, options, parsedTargets) {
|
|
9509
10161
|
const styleProvided = Boolean(options.style ?? resolved.job?.settings?.style);
|
|
10162
|
+
const intentProvided = Boolean(options.intent);
|
|
9510
10163
|
const lengthProvided = Boolean(options.length ?? resolved.job?.settings?.targetLength);
|
|
9511
10164
|
const providedTargets = parsedTargets && parsedTargets.length > 0 ? parsedTargets : resolved.job?.settings?.contentTargets ?? resolved.config.settings.contentTargets;
|
|
9512
10165
|
const targetsProvided = Boolean(parsedTargets && parsedTargets.length > 0 || resolved.job?.settings?.contentTargets?.length);
|
|
9513
|
-
if (options.noInteractive && (!styleProvided || !targetsProvided || !lengthProvided)) {
|
|
10166
|
+
if (options.noInteractive && (!styleProvided || !intentProvided || !targetsProvided || !lengthProvided)) {
|
|
9514
10167
|
const missingFlags = [
|
|
9515
10168
|
!styleProvided ? "--style <style>" : null,
|
|
10169
|
+
!intentProvided ? "--intent <intent>" : null,
|
|
9516
10170
|
!targetsProvided ? "--primary <content-type=1>" : null,
|
|
9517
10171
|
!lengthProvided ? "--length <size>" : null
|
|
9518
10172
|
].filter((value2) => Boolean(value2));
|
|
@@ -9523,15 +10177,17 @@ async function applyInteractiveWriteOptionsIfNeeded(resolved, options, parsedTar
|
|
|
9523
10177
|
if (!process.stdout.isTTY || !process.stdin.isTTY || options.noInteractive) {
|
|
9524
10178
|
return resolved;
|
|
9525
10179
|
}
|
|
9526
|
-
if (styleProvided && targetsProvided && lengthProvided) {
|
|
10180
|
+
if (styleProvided && intentProvided && targetsProvided && lengthProvided) {
|
|
9527
10181
|
return resolved;
|
|
9528
10182
|
}
|
|
9529
10183
|
const prompted = await promptForMissingWriteOptions({
|
|
9530
10184
|
askStyle: !styleProvided,
|
|
10185
|
+
askIntent: !intentProvided,
|
|
9531
10186
|
askTargets: !targetsProvided,
|
|
9532
10187
|
askLength: !lengthProvided,
|
|
9533
10188
|
style: resolved.config.settings.style,
|
|
9534
10189
|
targetLength: resolved.config.settings.targetLength,
|
|
10190
|
+
intent: resolved.config.settings.intent,
|
|
9535
10191
|
targets: providedTargets
|
|
9536
10192
|
});
|
|
9537
10193
|
return {
|
|
@@ -9541,6 +10197,7 @@ async function applyInteractiveWriteOptionsIfNeeded(resolved, options, parsedTar
|
|
|
9541
10197
|
settings: appSettingsSchema.parse({
|
|
9542
10198
|
...resolved.config.settings,
|
|
9543
10199
|
...prompted.style ? { style: prompted.style } : {},
|
|
10200
|
+
...prompted.intent ? { intent: prompted.intent } : {},
|
|
9544
10201
|
...prompted.targetLength ? { targetLength: prompted.targetLength } : {},
|
|
9545
10202
|
...prompted.contentTargets ? { contentTargets: prompted.contentTargets } : {}
|
|
9546
10203
|
})
|
|
@@ -9552,10 +10209,12 @@ async function promptForMissingWriteOptions(params) {
|
|
|
9552
10209
|
const app = render2(
|
|
9553
10210
|
React4.createElement(WriteOptionsFlow, {
|
|
9554
10211
|
askStyle: params.askStyle,
|
|
10212
|
+
askIntent: params.askIntent,
|
|
9555
10213
|
askTargets: params.askTargets,
|
|
9556
10214
|
askLength: params.askLength,
|
|
9557
10215
|
initialStyle: writingStyleValues.includes(params.style) ? params.style : "professional",
|
|
9558
|
-
|
|
10216
|
+
initialIntent: contentIntentValues.includes(params.intent) ? params.intent : "tutorial",
|
|
10217
|
+
initialTargetLength: resolveTargetLengthAlias(params.targetLength),
|
|
9559
10218
|
initialTargets: params.targets,
|
|
9560
10219
|
onDone: (result) => {
|
|
9561
10220
|
flowResult = result;
|
|
@@ -9639,6 +10298,12 @@ async function runCli(argv) {
|
|
|
9639
10298
|
force: options.force
|
|
9640
10299
|
});
|
|
9641
10300
|
});
|
|
10301
|
+
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").action(async (slug, options) => {
|
|
10302
|
+
await runLinksCommand({
|
|
10303
|
+
slug,
|
|
10304
|
+
mode: options.mode
|
|
10305
|
+
});
|
|
10306
|
+
});
|
|
9642
10307
|
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
10308
|
await runServeCommand({
|
|
9644
10309
|
markdownPath,
|
|
@@ -9647,7 +10312,7 @@ async function runCli(argv) {
|
|
|
9647
10312
|
watch: options.watch
|
|
9648
10313
|
});
|
|
9649
10314
|
});
|
|
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 (
|
|
10315
|
+
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).action(async (ideaArg, options) => {
|
|
9651
10316
|
await runWriteCommand({
|
|
9652
10317
|
idea: options.idea ?? ideaArg,
|
|
9653
10318
|
audience: options.audience,
|
|
@@ -9655,14 +10320,18 @@ async function runCli(argv) {
|
|
|
9655
10320
|
primarySpec: options.primary,
|
|
9656
10321
|
secondarySpecs: options.secondary,
|
|
9657
10322
|
style: options.style,
|
|
10323
|
+
intent: options.intent,
|
|
9658
10324
|
length: options.length,
|
|
9659
10325
|
noInteractive: !options.interactive,
|
|
9660
10326
|
dryRun: options.dryRun,
|
|
9661
10327
|
enrichLinks: options.enrichLinks
|
|
9662
10328
|
});
|
|
9663
10329
|
});
|
|
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({
|
|
10330
|
+
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).action(async (options) => {
|
|
10331
|
+
await runWriteResumeCommand({
|
|
10332
|
+
noInteractive: options.noInteractive,
|
|
10333
|
+
enrichLinks: options.enrichLinks
|
|
10334
|
+
});
|
|
9666
10335
|
});
|
|
9667
10336
|
await program.parseAsync(argv);
|
|
9668
10337
|
}
|