@telepat/ideon 0.1.20 → 0.1.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ideon.js
CHANGED
|
@@ -5,243 +5,29 @@ import { Command } from "commander";
|
|
|
5
5
|
|
|
6
6
|
// src/cli/commands/delete.ts
|
|
7
7
|
import { rm, stat } from "fs/promises";
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
// src/config/schema.ts
|
|
11
|
-
import { z } from "zod";
|
|
12
|
-
var contentTypeValues = [
|
|
13
|
-
"article",
|
|
14
|
-
"blog-post",
|
|
15
|
-
"linkedin-post",
|
|
16
|
-
"newsletter",
|
|
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"
|
|
52
|
-
];
|
|
53
|
-
var targetLengthValues = ["small", "medium", "large"];
|
|
54
|
-
var targetLengthAliasWordCounts = {
|
|
55
|
-
small: 500,
|
|
56
|
-
medium: 900,
|
|
57
|
-
large: 1400
|
|
58
|
-
};
|
|
59
|
-
var defaultTargetLengthWords = targetLengthAliasWordCounts.medium;
|
|
60
|
-
function parseTargetLengthWords(value2) {
|
|
61
|
-
if (typeof value2 === "number") {
|
|
62
|
-
return Number.isInteger(value2) && value2 > 0 ? value2 : void 0;
|
|
63
|
-
}
|
|
64
|
-
if (typeof value2 !== "string") {
|
|
65
|
-
return void 0;
|
|
66
|
-
}
|
|
67
|
-
const normalized = value2.trim().toLowerCase();
|
|
68
|
-
if (normalized.length === 0) {
|
|
69
|
-
return void 0;
|
|
70
|
-
}
|
|
71
|
-
if (targetLengthValues.includes(normalized)) {
|
|
72
|
-
return targetLengthAliasWordCounts[normalized];
|
|
73
|
-
}
|
|
74
|
-
if (!/^\d+$/.test(normalized)) {
|
|
75
|
-
return void 0;
|
|
76
|
-
}
|
|
77
|
-
const parsed = Number.parseInt(normalized, 10);
|
|
78
|
-
return Number.isInteger(parsed) && parsed > 0 ? parsed : void 0;
|
|
79
|
-
}
|
|
80
|
-
var targetLengthWordsSchema = z.preprocess(
|
|
81
|
-
(value2) => parseTargetLengthWords(value2),
|
|
82
|
-
z.number().int().positive()
|
|
83
|
-
);
|
|
84
|
-
function resolveTargetLengthAlias(targetLengthWords) {
|
|
85
|
-
if (!Number.isFinite(targetLengthWords) || targetLengthWords <= 0) {
|
|
86
|
-
return "medium";
|
|
87
|
-
}
|
|
88
|
-
if (targetLengthWords <= 700) {
|
|
89
|
-
return "small";
|
|
90
|
-
}
|
|
91
|
-
if (targetLengthWords <= 1150) {
|
|
92
|
-
return "medium";
|
|
93
|
-
}
|
|
94
|
-
return "large";
|
|
95
|
-
}
|
|
96
|
-
function resolveDefaultMaxLinks(targetLengthWords) {
|
|
97
|
-
const alias = resolveTargetLengthAlias(targetLengthWords);
|
|
98
|
-
if (alias === "small") return 5;
|
|
99
|
-
if (alias === "medium") return 8;
|
|
100
|
-
return 12;
|
|
101
|
-
}
|
|
102
|
-
var contentTargetRoleValues = ["primary", "secondary"];
|
|
103
|
-
var contentTargetSchema = z.object({
|
|
104
|
-
contentType: z.enum(contentTypeValues),
|
|
105
|
-
role: z.enum(contentTargetRoleValues),
|
|
106
|
-
count: z.number().int().positive().default(1)
|
|
107
|
-
});
|
|
108
|
-
var modelSettingsSchema = z.object({
|
|
109
|
-
temperature: z.number().min(0).max(2).default(0.7),
|
|
110
|
-
maxTokens: z.number().int().positive().default(4e3),
|
|
111
|
-
topP: z.number().min(0).max(1).default(1)
|
|
112
|
-
});
|
|
113
|
-
var baseT2ISettingsSchema = z.object({
|
|
114
|
-
modelId: z.string().default("flux"),
|
|
115
|
-
inputOverrides: z.record(z.string(), z.unknown()).default({})
|
|
116
|
-
});
|
|
117
|
-
var notificationsSettingsSchema = z.object({
|
|
118
|
-
enabled: z.boolean().default(false)
|
|
119
|
-
});
|
|
120
|
-
var appSettingsSchema = z.object({
|
|
121
|
-
model: z.string().default("deepseek/deepseek-v4-pro"),
|
|
122
|
-
modelSettings: modelSettingsSchema.default(modelSettingsSchema.parse({})),
|
|
123
|
-
modelRequestTimeoutMs: z.number().int().positive().default(9e4),
|
|
124
|
-
t2i: baseT2ISettingsSchema.default(baseT2ISettingsSchema.parse({})),
|
|
125
|
-
notifications: notificationsSettingsSchema.default(notificationsSettingsSchema.parse({})),
|
|
126
|
-
markdownOutputDir: z.string().default("/output"),
|
|
127
|
-
assetOutputDir: z.string().default("/output/assets"),
|
|
128
|
-
contentTargets: z.array(contentTargetSchema).min(1).refine((targets) => targets.filter((target) => target.role === "primary").length === 1, {
|
|
129
|
-
message: "contentTargets must include exactly one primary target."
|
|
130
|
-
}).default([{ contentType: "article", role: "primary", count: 1 }]),
|
|
131
|
-
style: z.enum(writingStyleValues).default("professional"),
|
|
132
|
-
intent: z.enum(contentIntentValues).default("tutorial"),
|
|
133
|
-
targetLength: targetLengthWordsSchema.default(defaultTargetLengthWords)
|
|
134
|
-
});
|
|
135
|
-
var envSettingsSchema = z.object({
|
|
136
|
-
openRouterApiKey: z.string().optional(),
|
|
137
|
-
replicateApiToken: z.string().optional(),
|
|
138
|
-
disableKeytar: z.boolean().optional(),
|
|
139
|
-
model: z.string().optional(),
|
|
140
|
-
temperature: z.number().min(0).max(2).optional(),
|
|
141
|
-
maxTokens: z.number().int().positive().optional(),
|
|
142
|
-
topP: z.number().min(0).max(1).optional(),
|
|
143
|
-
modelRequestTimeoutMs: z.number().int().positive().optional(),
|
|
144
|
-
notificationsEnabled: z.boolean().optional(),
|
|
145
|
-
markdownOutputDir: z.string().optional(),
|
|
146
|
-
assetOutputDir: z.string().optional(),
|
|
147
|
-
style: z.enum(writingStyleValues).optional(),
|
|
148
|
-
intent: z.enum(contentIntentValues).optional(),
|
|
149
|
-
targetLength: targetLengthWordsSchema.optional()
|
|
150
|
-
});
|
|
151
|
-
var jobInputSchema = z.object({
|
|
152
|
-
idea: z.string().min(1).optional(),
|
|
153
|
-
prompt: z.string().min(1).optional(),
|
|
154
|
-
targetAudience: z.string().min(1).optional(),
|
|
155
|
-
settings: appSettingsSchema.partial().optional()
|
|
156
|
-
});
|
|
157
|
-
var defaultAppSettings = appSettingsSchema.parse({});
|
|
158
|
-
|
|
159
|
-
// src/config/env.ts
|
|
160
|
-
function parseNumber(value2) {
|
|
161
|
-
if (!value2) {
|
|
162
|
-
return void 0;
|
|
163
|
-
}
|
|
164
|
-
const parsed = Number(value2);
|
|
165
|
-
return Number.isFinite(parsed) ? parsed : void 0;
|
|
166
|
-
}
|
|
167
|
-
function parseBoolean(value2) {
|
|
168
|
-
if (!value2) {
|
|
169
|
-
return void 0;
|
|
170
|
-
}
|
|
171
|
-
const normalized = value2.trim().toLowerCase();
|
|
172
|
-
if (normalized === "true") {
|
|
173
|
-
return true;
|
|
174
|
-
}
|
|
175
|
-
if (normalized === "false") {
|
|
176
|
-
return false;
|
|
177
|
-
}
|
|
178
|
-
return void 0;
|
|
179
|
-
}
|
|
180
|
-
function readEnvSettings(env = process.env) {
|
|
181
|
-
return envSettingsSchema.parse({
|
|
182
|
-
openRouterApiKey: env.IDEON_OPENROUTER_API_KEY,
|
|
183
|
-
replicateApiToken: env.IDEON_REPLICATE_API_TOKEN,
|
|
184
|
-
disableKeytar: parseBoolean(env.IDEON_DISABLE_KEYTAR),
|
|
185
|
-
model: env.IDEON_MODEL,
|
|
186
|
-
temperature: parseNumber(env.IDEON_TEMPERATURE),
|
|
187
|
-
maxTokens: parseNumber(env.IDEON_MAX_TOKENS),
|
|
188
|
-
topP: parseNumber(env.IDEON_TOP_P),
|
|
189
|
-
modelRequestTimeoutMs: parseNumber(env.IDEON_MODEL_REQUEST_TIMEOUT_MS),
|
|
190
|
-
notificationsEnabled: parseBoolean(env.IDEON_NOTIFICATIONS_ENABLED),
|
|
191
|
-
markdownOutputDir: env.IDEON_MARKDOWN_OUTPUT_DIR,
|
|
192
|
-
assetOutputDir: env.IDEON_ASSET_OUTPUT_DIR,
|
|
193
|
-
style: env.IDEON_STYLE,
|
|
194
|
-
intent: env.IDEON_INTENT,
|
|
195
|
-
targetLength: env.IDEON_TARGET_LENGTH
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// src/config/settingsFile.ts
|
|
200
|
-
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
201
|
-
import path from "path";
|
|
202
|
-
import envPaths from "env-paths";
|
|
203
|
-
var ideonPaths = envPaths("ideon", { suffix: "" });
|
|
204
|
-
var settingsDir = path.join(ideonPaths.config);
|
|
205
|
-
var settingsFilePath = path.join(settingsDir, "settings.json");
|
|
206
|
-
function getSettingsFilePath() {
|
|
207
|
-
return settingsFilePath;
|
|
208
|
-
}
|
|
209
|
-
async function loadSavedSettings() {
|
|
210
|
-
try {
|
|
211
|
-
const raw = await readFile(settingsFilePath, "utf8");
|
|
212
|
-
return appSettingsSchema.parse(JSON.parse(raw));
|
|
213
|
-
} catch (error) {
|
|
214
|
-
if (error.code === "ENOENT") {
|
|
215
|
-
return defaultAppSettings;
|
|
216
|
-
}
|
|
217
|
-
throw error;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
async function saveSettings(settings) {
|
|
221
|
-
await mkdir(settingsDir, { recursive: true });
|
|
222
|
-
await writeFile(settingsFilePath, `${JSON.stringify(settings, null, 2)}
|
|
223
|
-
`, "utf8");
|
|
224
|
-
}
|
|
8
|
+
import path2 from "path";
|
|
225
9
|
|
|
226
10
|
// src/output/filesystem.ts
|
|
227
|
-
import { access, mkdir
|
|
228
|
-
import
|
|
229
|
-
|
|
11
|
+
import { access, mkdir, writeFile } from "fs/promises";
|
|
12
|
+
import os from "os";
|
|
13
|
+
import path from "path";
|
|
14
|
+
function resolveOutputPaths() {
|
|
15
|
+
const base = path.join(os.homedir(), ".ideon", "output");
|
|
230
16
|
return {
|
|
231
|
-
markdownOutputDir:
|
|
232
|
-
assetOutputDir:
|
|
17
|
+
markdownOutputDir: base,
|
|
18
|
+
assetOutputDir: path.join(base, "assets")
|
|
233
19
|
};
|
|
234
20
|
}
|
|
235
21
|
async function ensureOutputDirectories(paths) {
|
|
236
22
|
await Promise.all([
|
|
237
|
-
|
|
238
|
-
|
|
23
|
+
mkdir(paths.markdownOutputDir, { recursive: true }),
|
|
24
|
+
mkdir(paths.assetOutputDir, { recursive: true })
|
|
239
25
|
]);
|
|
240
26
|
}
|
|
241
27
|
async function resolveUniqueSlug(markdownOutputDir, baseSlug) {
|
|
242
28
|
let attempt = 0;
|
|
243
29
|
let candidate = baseSlug;
|
|
244
|
-
while (await fileExists(
|
|
30
|
+
while (await fileExists(path.join(markdownOutputDir, `${candidate}.md`))) {
|
|
245
31
|
attempt += 1;
|
|
246
32
|
candidate = `${baseSlug}-${attempt}`;
|
|
247
33
|
}
|
|
@@ -279,7 +65,7 @@ async function listFilesRecursively(rootDir, predicate) {
|
|
|
279
65
|
continue;
|
|
280
66
|
}
|
|
281
67
|
for (const entry of entries) {
|
|
282
|
-
const fullPath =
|
|
68
|
+
const fullPath = path.join(current, entry.name);
|
|
283
69
|
if (entry.isDirectory()) {
|
|
284
70
|
stack.push(fullPath);
|
|
285
71
|
continue;
|
|
@@ -292,35 +78,26 @@ async function listFilesRecursively(rootDir, predicate) {
|
|
|
292
78
|
return results;
|
|
293
79
|
}
|
|
294
80
|
async function writeUtf8File(filePath, content) {
|
|
295
|
-
await
|
|
296
|
-
await
|
|
81
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
82
|
+
await writeFile(filePath, content, "utf8");
|
|
297
83
|
}
|
|
298
84
|
async function writeJsonFile(filePath, data) {
|
|
299
85
|
await writeUtf8File(filePath, `${JSON.stringify(data, null, 2)}
|
|
300
86
|
`);
|
|
301
87
|
}
|
|
302
88
|
function resolveLinksPath(markdownPath) {
|
|
303
|
-
const parsed =
|
|
304
|
-
return
|
|
89
|
+
const parsed = path.parse(markdownPath);
|
|
90
|
+
return path.join(parsed.dir, `${parsed.name}.links.json`);
|
|
305
91
|
}
|
|
306
92
|
async function writeLinksFile(markdownPath, links) {
|
|
307
93
|
await writeJsonFile(resolveLinksPath(markdownPath), links);
|
|
308
94
|
}
|
|
309
95
|
function resolveAnalyticsPath(markdownPath) {
|
|
310
|
-
const parsed =
|
|
311
|
-
return
|
|
96
|
+
const parsed = path.parse(markdownPath);
|
|
97
|
+
return path.join(parsed.dir, `${parsed.name}.analytics.json`);
|
|
312
98
|
}
|
|
313
99
|
function relativeAssetPath(markdownPath, assetPath) {
|
|
314
|
-
return
|
|
315
|
-
}
|
|
316
|
-
function resolveConfiguredDir(configuredPath, cwd2) {
|
|
317
|
-
if (configuredPath === "/output" || configuredPath.startsWith("/output/")) {
|
|
318
|
-
return path2.join(cwd2, configuredPath.slice(1));
|
|
319
|
-
}
|
|
320
|
-
if (path2.isAbsolute(configuredPath)) {
|
|
321
|
-
return configuredPath;
|
|
322
|
-
}
|
|
323
|
-
return path2.resolve(cwd2, configuredPath);
|
|
100
|
+
return path.relative(path.dirname(markdownPath), assetPath).split(path.sep).join("/");
|
|
324
101
|
}
|
|
325
102
|
async function fileExists(filePath) {
|
|
326
103
|
try {
|
|
@@ -372,24 +149,18 @@ async function runDeleteCommand(options, dependencies = {}) {
|
|
|
372
149
|
log(`Removed ${relativeMarkdown} and cleaned ${relativeAssetDir}.`);
|
|
373
150
|
}
|
|
374
151
|
async function resolveDeleteTargets(slug, cwd2) {
|
|
375
|
-
const
|
|
376
|
-
const mergedSettings = appSettingsSchema.parse({
|
|
377
|
-
...savedSettings,
|
|
378
|
-
...envSettings.markdownOutputDir ? { markdownOutputDir: envSettings.markdownOutputDir } : {},
|
|
379
|
-
...envSettings.assetOutputDir ? { assetOutputDir: envSettings.assetOutputDir } : {}
|
|
380
|
-
});
|
|
381
|
-
const outputPaths = resolveOutputPaths(mergedSettings, cwd2);
|
|
152
|
+
const outputPaths = resolveOutputPaths();
|
|
382
153
|
const markdownPath = await resolveMarkdownPathForSlug(outputPaths.markdownOutputDir, slug);
|
|
383
154
|
await assertMarkdownExists(markdownPath, slug);
|
|
384
155
|
return {
|
|
385
156
|
slug,
|
|
386
157
|
markdownPath,
|
|
387
158
|
analyticsPath: resolveAnalyticsPath(markdownPath),
|
|
388
|
-
assetDir:
|
|
159
|
+
assetDir: path2.dirname(markdownPath)
|
|
389
160
|
};
|
|
390
161
|
}
|
|
391
162
|
async function resolveMarkdownPathForSlug(markdownOutputDir, slug) {
|
|
392
|
-
const directPath =
|
|
163
|
+
const directPath = path2.join(markdownOutputDir, `${slug}.md`);
|
|
393
164
|
if (await pathExists(directPath)) {
|
|
394
165
|
return directPath;
|
|
395
166
|
}
|
|
@@ -424,7 +195,7 @@ async function findMarkdownCandidates(rootDir, fileName) {
|
|
|
424
195
|
continue;
|
|
425
196
|
}
|
|
426
197
|
for (const entry of entries) {
|
|
427
|
-
const fullPath =
|
|
198
|
+
const fullPath = path2.join(current, entry.name);
|
|
428
199
|
if (entry.isDirectory()) {
|
|
429
200
|
stack.push(fullPath);
|
|
430
201
|
continue;
|
|
@@ -503,7 +274,7 @@ function isNodeError(error) {
|
|
|
503
274
|
return error instanceof Error;
|
|
504
275
|
}
|
|
505
276
|
function formatRelativePath(cwd2, targetPath) {
|
|
506
|
-
const relativePath =
|
|
277
|
+
const relativePath = path2.relative(cwd2, targetPath);
|
|
507
278
|
return relativePath.length > 0 ? relativePath : targetPath;
|
|
508
279
|
}
|
|
509
280
|
async function directoryContainsMarkdown(dirPath) {
|
|
@@ -517,23 +288,23 @@ async function directoryContainsMarkdown(dirPath) {
|
|
|
517
288
|
}
|
|
518
289
|
|
|
519
290
|
// src/integrations/agent/store.ts
|
|
520
|
-
import { mkdir as
|
|
521
|
-
import
|
|
522
|
-
import
|
|
523
|
-
import { z
|
|
291
|
+
import { mkdir as mkdir2, readFile, writeFile as writeFile2 } from "fs/promises";
|
|
292
|
+
import path3 from "path";
|
|
293
|
+
import envPaths from "env-paths";
|
|
294
|
+
import { z } from "zod";
|
|
524
295
|
var supportedAgentRuntimeValues = ["claude", "claude-desktop", "chatgpt", "gemini", "codex", "cursor", "vscode", "opencode", "generic-mcp"];
|
|
525
|
-
var integrationEntrySchema =
|
|
526
|
-
runtime:
|
|
527
|
-
installedAt:
|
|
528
|
-
updatedAt:
|
|
296
|
+
var integrationEntrySchema = z.object({
|
|
297
|
+
runtime: z.enum(supportedAgentRuntimeValues),
|
|
298
|
+
installedAt: z.string(),
|
|
299
|
+
updatedAt: z.string()
|
|
529
300
|
});
|
|
530
|
-
var integrationStoreSchema =
|
|
531
|
-
version:
|
|
532
|
-
integrations:
|
|
301
|
+
var integrationStoreSchema = z.object({
|
|
302
|
+
version: z.literal(1),
|
|
303
|
+
integrations: z.record(z.string(), integrationEntrySchema).default({})
|
|
533
304
|
});
|
|
534
|
-
var
|
|
535
|
-
var storeDir =
|
|
536
|
-
var storePath =
|
|
305
|
+
var ideonPaths = envPaths("ideon", { suffix: "" });
|
|
306
|
+
var storeDir = ideonPaths.config;
|
|
307
|
+
var storePath = path3.join(storeDir, "agent-integrations.json");
|
|
537
308
|
function getAgentIntegrationStorePath() {
|
|
538
309
|
return storePath;
|
|
539
310
|
}
|
|
@@ -578,7 +349,7 @@ async function uninstallAgentIntegration(runtime, targetStorePath = storePath) {
|
|
|
578
349
|
}
|
|
579
350
|
async function readStore(targetStorePath) {
|
|
580
351
|
try {
|
|
581
|
-
const raw = await
|
|
352
|
+
const raw = await readFile(targetStorePath, "utf8");
|
|
582
353
|
return integrationStoreSchema.parse(JSON.parse(raw));
|
|
583
354
|
} catch (error) {
|
|
584
355
|
if (error.code === "ENOENT") {
|
|
@@ -588,8 +359,301 @@ async function readStore(targetStorePath) {
|
|
|
588
359
|
}
|
|
589
360
|
}
|
|
590
361
|
async function writeStore(store, targetStorePath) {
|
|
591
|
-
await
|
|
592
|
-
await
|
|
362
|
+
await mkdir2(path3.dirname(targetStorePath), { recursive: true });
|
|
363
|
+
await writeFile2(targetStorePath, `${JSON.stringify(store, null, 2)}
|
|
364
|
+
`, "utf8");
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// src/config/schema.ts
|
|
368
|
+
import { z as z2 } from "zod";
|
|
369
|
+
|
|
370
|
+
// src/images/limnModelCatalog.ts
|
|
371
|
+
import { getSupportedModelCatalog } from "@telepat/limn";
|
|
372
|
+
function getLimnGenerationModels() {
|
|
373
|
+
return getSupportedModelCatalog().filter((entry) => entry.generationEnabled);
|
|
374
|
+
}
|
|
375
|
+
var DEFAULT_LIMN_MODEL_ID = "flux";
|
|
376
|
+
function resolveFamilyFromReplicateModelId(replicateModelId) {
|
|
377
|
+
const match = getLimnGenerationModels().find((model) => model.replicateModelIds.includes(replicateModelId));
|
|
378
|
+
return match?.family ?? null;
|
|
379
|
+
}
|
|
380
|
+
function isKnownLimnFamily(family) {
|
|
381
|
+
return getLimnGenerationModels().some((model) => model.family === family);
|
|
382
|
+
}
|
|
383
|
+
function isKnownReplicateModelId(replicateModelId) {
|
|
384
|
+
return getLimnGenerationModels().some((model) => model.replicateModelIds.includes(replicateModelId));
|
|
385
|
+
}
|
|
386
|
+
function isReplicateModelIdForFamily(family, replicateModelId) {
|
|
387
|
+
const match = getLimnGenerationModels().find((model) => model.family === family);
|
|
388
|
+
if (!match) {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
return match.replicateModelIds.includes(replicateModelId);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// src/config/schema.ts
|
|
395
|
+
var contentTypeValues = [
|
|
396
|
+
"article",
|
|
397
|
+
"blog-post",
|
|
398
|
+
"linkedin-post",
|
|
399
|
+
"newsletter",
|
|
400
|
+
"press-release",
|
|
401
|
+
"reddit-post",
|
|
402
|
+
"science-paper",
|
|
403
|
+
"x-post",
|
|
404
|
+
"x-thread"
|
|
405
|
+
];
|
|
406
|
+
var writingStyleValues = [
|
|
407
|
+
"academic",
|
|
408
|
+
"analytical",
|
|
409
|
+
"authoritative",
|
|
410
|
+
"conversational",
|
|
411
|
+
"empathetic",
|
|
412
|
+
"friendly",
|
|
413
|
+
"journalistic",
|
|
414
|
+
"minimalist",
|
|
415
|
+
"persuasive",
|
|
416
|
+
"playful",
|
|
417
|
+
"professional",
|
|
418
|
+
"storytelling",
|
|
419
|
+
"technical"
|
|
420
|
+
];
|
|
421
|
+
var contentIntentValues = [
|
|
422
|
+
"announcement",
|
|
423
|
+
"case-study",
|
|
424
|
+
"cornerstone",
|
|
425
|
+
"counterargument",
|
|
426
|
+
"critique-review",
|
|
427
|
+
"deep-dive-analysis",
|
|
428
|
+
"how-to-guide",
|
|
429
|
+
"interview-q-and-a",
|
|
430
|
+
"listicle",
|
|
431
|
+
"opinion-piece",
|
|
432
|
+
"personal-essay",
|
|
433
|
+
"roundup-curation",
|
|
434
|
+
"tutorial"
|
|
435
|
+
];
|
|
436
|
+
var targetLengthValues = ["small", "medium", "large"];
|
|
437
|
+
var targetLengthAliasWordCounts = {
|
|
438
|
+
small: 500,
|
|
439
|
+
medium: 900,
|
|
440
|
+
large: 1400
|
|
441
|
+
};
|
|
442
|
+
var defaultTargetLengthWords = targetLengthAliasWordCounts.medium;
|
|
443
|
+
function parseTargetLengthWords(value2) {
|
|
444
|
+
if (typeof value2 === "number") {
|
|
445
|
+
return Number.isInteger(value2) && value2 > 0 ? value2 : void 0;
|
|
446
|
+
}
|
|
447
|
+
if (typeof value2 !== "string") {
|
|
448
|
+
return void 0;
|
|
449
|
+
}
|
|
450
|
+
const normalized = value2.trim().toLowerCase();
|
|
451
|
+
if (normalized.length === 0) {
|
|
452
|
+
return void 0;
|
|
453
|
+
}
|
|
454
|
+
if (targetLengthValues.includes(normalized)) {
|
|
455
|
+
return targetLengthAliasWordCounts[normalized];
|
|
456
|
+
}
|
|
457
|
+
if (!/^\d+$/.test(normalized)) {
|
|
458
|
+
return void 0;
|
|
459
|
+
}
|
|
460
|
+
const parsed = Number.parseInt(normalized, 10);
|
|
461
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : void 0;
|
|
462
|
+
}
|
|
463
|
+
var targetLengthWordsSchema = z2.preprocess(
|
|
464
|
+
(value2) => parseTargetLengthWords(value2),
|
|
465
|
+
z2.number().int().positive()
|
|
466
|
+
);
|
|
467
|
+
function resolveTargetLengthAlias(targetLengthWords) {
|
|
468
|
+
if (!Number.isFinite(targetLengthWords) || targetLengthWords <= 0) {
|
|
469
|
+
return "medium";
|
|
470
|
+
}
|
|
471
|
+
if (targetLengthWords <= 700) {
|
|
472
|
+
return "small";
|
|
473
|
+
}
|
|
474
|
+
if (targetLengthWords <= 1150) {
|
|
475
|
+
return "medium";
|
|
476
|
+
}
|
|
477
|
+
return "large";
|
|
478
|
+
}
|
|
479
|
+
function resolveDefaultMaxLinks(targetLengthWords) {
|
|
480
|
+
const alias = resolveTargetLengthAlias(targetLengthWords);
|
|
481
|
+
if (alias === "small") return 5;
|
|
482
|
+
if (alias === "medium") return 8;
|
|
483
|
+
return 12;
|
|
484
|
+
}
|
|
485
|
+
function resolveDefaultInlineImageCount(targetLengthWords) {
|
|
486
|
+
const alias = resolveTargetLengthAlias(targetLengthWords);
|
|
487
|
+
if (alias === "small") return { min: 1, max: 2 };
|
|
488
|
+
if (alias === "medium") return { min: 2, max: 3 };
|
|
489
|
+
return { min: 3, max: 4 };
|
|
490
|
+
}
|
|
491
|
+
var contentTargetRoleValues = ["primary", "secondary"];
|
|
492
|
+
var contentTargetSchema = z2.object({
|
|
493
|
+
contentType: z2.enum(contentTypeValues),
|
|
494
|
+
role: z2.enum(contentTargetRoleValues),
|
|
495
|
+
count: z2.number().int().positive().default(1)
|
|
496
|
+
});
|
|
497
|
+
var modelSettingsSchema = z2.object({
|
|
498
|
+
temperature: z2.number().min(0).max(2).default(0.7),
|
|
499
|
+
maxTokens: z2.number().int().positive().default(4e3),
|
|
500
|
+
topP: z2.number().min(0).max(1).default(1)
|
|
501
|
+
});
|
|
502
|
+
function normalizeT2ISettings(value2) {
|
|
503
|
+
if (!value2 || typeof value2 !== "object" || Array.isArray(value2)) {
|
|
504
|
+
return value2;
|
|
505
|
+
}
|
|
506
|
+
const raw = value2;
|
|
507
|
+
const rawModelId = typeof raw.modelId === "string" ? raw.modelId.trim() : "";
|
|
508
|
+
const rawReplicateModelId = typeof raw.replicateModelId === "string" ? raw.replicateModelId.trim() : "";
|
|
509
|
+
let modelId = rawModelId || DEFAULT_LIMN_MODEL_ID;
|
|
510
|
+
let replicateModelId = rawReplicateModelId || void 0;
|
|
511
|
+
if (isKnownLimnFamily(modelId)) {
|
|
512
|
+
if (replicateModelId && !isReplicateModelIdForFamily(modelId, replicateModelId)) {
|
|
513
|
+
replicateModelId = void 0;
|
|
514
|
+
}
|
|
515
|
+
return {
|
|
516
|
+
...raw,
|
|
517
|
+
modelId,
|
|
518
|
+
replicateModelId
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
if (isKnownReplicateModelId(modelId)) {
|
|
522
|
+
const derivedFamily = resolveFamilyFromReplicateModelId(modelId);
|
|
523
|
+
if (derivedFamily) {
|
|
524
|
+
modelId = derivedFamily;
|
|
525
|
+
replicateModelId = replicateModelId && isReplicateModelIdForFamily(modelId, replicateModelId) ? replicateModelId : rawModelId;
|
|
526
|
+
return {
|
|
527
|
+
...raw,
|
|
528
|
+
modelId,
|
|
529
|
+
replicateModelId
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (replicateModelId && isKnownReplicateModelId(replicateModelId)) {
|
|
534
|
+
const derivedFamily = resolveFamilyFromReplicateModelId(replicateModelId);
|
|
535
|
+
if (derivedFamily) {
|
|
536
|
+
return {
|
|
537
|
+
...raw,
|
|
538
|
+
modelId: derivedFamily,
|
|
539
|
+
replicateModelId
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return {
|
|
544
|
+
...raw,
|
|
545
|
+
modelId: DEFAULT_LIMN_MODEL_ID,
|
|
546
|
+
replicateModelId: void 0
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
var baseT2ISettingsSchema = z2.preprocess(
|
|
550
|
+
normalizeT2ISettings,
|
|
551
|
+
z2.object({
|
|
552
|
+
modelId: z2.string().default(DEFAULT_LIMN_MODEL_ID),
|
|
553
|
+
replicateModelId: z2.string().optional(),
|
|
554
|
+
inputOverrides: z2.record(z2.string(), z2.unknown()).default({})
|
|
555
|
+
})
|
|
556
|
+
);
|
|
557
|
+
var notificationsSettingsSchema = z2.object({
|
|
558
|
+
enabled: z2.boolean().default(false)
|
|
559
|
+
});
|
|
560
|
+
var appSettingsSchema = z2.object({
|
|
561
|
+
model: z2.string().default("deepseek/deepseek-v4-pro"),
|
|
562
|
+
modelSettings: modelSettingsSchema.default(modelSettingsSchema.parse({})),
|
|
563
|
+
modelRequestTimeoutMs: z2.number().int().positive().default(9e4),
|
|
564
|
+
t2i: baseT2ISettingsSchema.default(baseT2ISettingsSchema.parse({})),
|
|
565
|
+
notifications: notificationsSettingsSchema.default(notificationsSettingsSchema.parse({})),
|
|
566
|
+
contentTargets: z2.array(contentTargetSchema).min(1).refine((targets) => targets.filter((target) => target.role === "primary").length === 1, {
|
|
567
|
+
message: "contentTargets must include exactly one primary target."
|
|
568
|
+
}).default([{ contentType: "article", role: "primary", count: 1 }]),
|
|
569
|
+
style: z2.enum(writingStyleValues).default("professional"),
|
|
570
|
+
intent: z2.enum(contentIntentValues).default("tutorial"),
|
|
571
|
+
targetLength: targetLengthWordsSchema.default(defaultTargetLengthWords)
|
|
572
|
+
});
|
|
573
|
+
var envSettingsSchema = z2.object({
|
|
574
|
+
openRouterApiKey: z2.string().optional(),
|
|
575
|
+
replicateApiToken: z2.string().optional(),
|
|
576
|
+
disableKeytar: z2.boolean().optional(),
|
|
577
|
+
model: z2.string().optional(),
|
|
578
|
+
temperature: z2.number().min(0).max(2).optional(),
|
|
579
|
+
maxTokens: z2.number().int().positive().optional(),
|
|
580
|
+
topP: z2.number().min(0).max(1).optional(),
|
|
581
|
+
modelRequestTimeoutMs: z2.number().int().positive().optional(),
|
|
582
|
+
notificationsEnabled: z2.boolean().optional(),
|
|
583
|
+
style: z2.enum(writingStyleValues).optional(),
|
|
584
|
+
intent: z2.enum(contentIntentValues).optional(),
|
|
585
|
+
targetLength: targetLengthWordsSchema.optional()
|
|
586
|
+
});
|
|
587
|
+
var jobInputSchema = z2.object({
|
|
588
|
+
idea: z2.string().min(1).optional(),
|
|
589
|
+
prompt: z2.string().min(1).optional(),
|
|
590
|
+
targetAudience: z2.string().min(1).optional(),
|
|
591
|
+
settings: appSettingsSchema.partial().optional()
|
|
592
|
+
});
|
|
593
|
+
var defaultAppSettings = appSettingsSchema.parse({});
|
|
594
|
+
|
|
595
|
+
// src/config/env.ts
|
|
596
|
+
function parseNumber(value2) {
|
|
597
|
+
if (!value2) {
|
|
598
|
+
return void 0;
|
|
599
|
+
}
|
|
600
|
+
const parsed = Number(value2);
|
|
601
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
602
|
+
}
|
|
603
|
+
function parseBoolean(value2) {
|
|
604
|
+
if (!value2) {
|
|
605
|
+
return void 0;
|
|
606
|
+
}
|
|
607
|
+
const normalized = value2.trim().toLowerCase();
|
|
608
|
+
if (normalized === "true") {
|
|
609
|
+
return true;
|
|
610
|
+
}
|
|
611
|
+
if (normalized === "false") {
|
|
612
|
+
return false;
|
|
613
|
+
}
|
|
614
|
+
return void 0;
|
|
615
|
+
}
|
|
616
|
+
function readEnvSettings(env = process.env) {
|
|
617
|
+
return envSettingsSchema.parse({
|
|
618
|
+
openRouterApiKey: env.IDEON_OPENROUTER_API_KEY,
|
|
619
|
+
replicateApiToken: env.IDEON_REPLICATE_API_TOKEN,
|
|
620
|
+
disableKeytar: parseBoolean(env.IDEON_DISABLE_KEYTAR),
|
|
621
|
+
model: env.IDEON_MODEL,
|
|
622
|
+
temperature: parseNumber(env.IDEON_TEMPERATURE),
|
|
623
|
+
maxTokens: parseNumber(env.IDEON_MAX_TOKENS),
|
|
624
|
+
topP: parseNumber(env.IDEON_TOP_P),
|
|
625
|
+
modelRequestTimeoutMs: parseNumber(env.IDEON_MODEL_REQUEST_TIMEOUT_MS),
|
|
626
|
+
notificationsEnabled: parseBoolean(env.IDEON_NOTIFICATIONS_ENABLED),
|
|
627
|
+
style: env.IDEON_STYLE,
|
|
628
|
+
intent: env.IDEON_INTENT,
|
|
629
|
+
targetLength: env.IDEON_TARGET_LENGTH
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// src/config/settingsFile.ts
|
|
634
|
+
import { mkdir as mkdir3, readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
|
|
635
|
+
import path4 from "path";
|
|
636
|
+
import envPaths2 from "env-paths";
|
|
637
|
+
var ideonPaths2 = envPaths2("ideon", { suffix: "" });
|
|
638
|
+
var settingsDir = path4.join(ideonPaths2.config);
|
|
639
|
+
var settingsFilePath = path4.join(settingsDir, "settings.json");
|
|
640
|
+
function getSettingsFilePath() {
|
|
641
|
+
return settingsFilePath;
|
|
642
|
+
}
|
|
643
|
+
async function loadSavedSettings() {
|
|
644
|
+
try {
|
|
645
|
+
const raw = await readFile2(settingsFilePath, "utf8");
|
|
646
|
+
return appSettingsSchema.parse(JSON.parse(raw));
|
|
647
|
+
} catch (error) {
|
|
648
|
+
if (error.code === "ENOENT") {
|
|
649
|
+
return defaultAppSettings;
|
|
650
|
+
}
|
|
651
|
+
throw error;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
async function saveSettings(settings) {
|
|
655
|
+
await mkdir3(settingsDir, { recursive: true });
|
|
656
|
+
await writeFile3(settingsFilePath, `${JSON.stringify(settings, null, 2)}
|
|
593
657
|
`, "utf8");
|
|
594
658
|
}
|
|
595
659
|
|
|
@@ -746,8 +810,7 @@ var configSettingKeys = [
|
|
|
746
810
|
"modelSettings.topP",
|
|
747
811
|
"modelRequestTimeoutMs",
|
|
748
812
|
"notifications.enabled",
|
|
749
|
-
"
|
|
750
|
-
"assetOutputDir",
|
|
813
|
+
"t2i.replicateModelId",
|
|
751
814
|
"style",
|
|
752
815
|
"intent",
|
|
753
816
|
"targetLength"
|
|
@@ -775,8 +838,7 @@ async function configList() {
|
|
|
775
838
|
"modelSettings.topP": settings.modelSettings.topP,
|
|
776
839
|
modelRequestTimeoutMs: settings.modelRequestTimeoutMs,
|
|
777
840
|
"notifications.enabled": settings.notifications.enabled,
|
|
778
|
-
|
|
779
|
-
assetOutputDir: settings.assetOutputDir,
|
|
841
|
+
"t2i.replicateModelId": settings.t2i.replicateModelId,
|
|
780
842
|
style: settings.style,
|
|
781
843
|
intent: settings.intent,
|
|
782
844
|
targetLength: settings.targetLength
|
|
@@ -837,12 +899,15 @@ function coerceSettingValue(key, rawValue) {
|
|
|
837
899
|
const trimmed = rawValue.trim();
|
|
838
900
|
switch (key) {
|
|
839
901
|
case "model":
|
|
840
|
-
case "markdownOutputDir":
|
|
841
|
-
case "assetOutputDir": {
|
|
842
902
|
if (trimmed.length === 0) {
|
|
843
903
|
throw new Error(`${key} cannot be empty.`);
|
|
844
904
|
}
|
|
845
905
|
return trimmed;
|
|
906
|
+
case "t2i.replicateModelId": {
|
|
907
|
+
if (trimmed.length === 0) {
|
|
908
|
+
throw new Error("t2i.replicateModelId cannot be empty. Use config unset to clear it.");
|
|
909
|
+
}
|
|
910
|
+
return trimmed;
|
|
846
911
|
}
|
|
847
912
|
case "modelSettings.temperature": {
|
|
848
913
|
const parsed = Number(trimmed);
|
|
@@ -916,10 +981,8 @@ function getSettingValue(settings, key) {
|
|
|
916
981
|
return settings.modelRequestTimeoutMs;
|
|
917
982
|
case "notifications.enabled":
|
|
918
983
|
return settings.notifications.enabled;
|
|
919
|
-
case "
|
|
920
|
-
return settings.
|
|
921
|
-
case "assetOutputDir":
|
|
922
|
-
return settings.assetOutputDir;
|
|
984
|
+
case "t2i.replicateModelId":
|
|
985
|
+
return settings.t2i.replicateModelId;
|
|
923
986
|
case "style":
|
|
924
987
|
return settings.style;
|
|
925
988
|
case "intent":
|
|
@@ -944,10 +1007,8 @@ function setSettingValue(settings, key, value2) {
|
|
|
944
1007
|
return { ...settings, modelRequestTimeoutMs: value2 };
|
|
945
1008
|
case "notifications.enabled":
|
|
946
1009
|
return { ...settings, notifications: { ...settings.notifications, enabled: value2 } };
|
|
947
|
-
case "
|
|
948
|
-
return { ...settings,
|
|
949
|
-
case "assetOutputDir":
|
|
950
|
-
return { ...settings, assetOutputDir: value2 };
|
|
1010
|
+
case "t2i.replicateModelId":
|
|
1011
|
+
return { ...settings, t2i: { ...settings.t2i, replicateModelId: value2 } };
|
|
951
1012
|
case "style":
|
|
952
1013
|
return { ...settings, style: value2 };
|
|
953
1014
|
case "intent":
|
|
@@ -1362,7 +1423,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
1362
1423
|
// package.json
|
|
1363
1424
|
var package_default = {
|
|
1364
1425
|
name: "@telepat/ideon",
|
|
1365
|
-
version: "0.1.
|
|
1426
|
+
version: "0.1.24",
|
|
1366
1427
|
description: "CLI for generating rich articles and images from ideas.",
|
|
1367
1428
|
type: "module",
|
|
1368
1429
|
repository: {
|
|
@@ -1500,8 +1561,6 @@ async function resolveRunInput(input) {
|
|
|
1500
1561
|
...envSettings.topP !== void 0 ? { topP: envSettings.topP } : {}
|
|
1501
1562
|
}
|
|
1502
1563
|
} : {},
|
|
1503
|
-
...envSettings.markdownOutputDir ? { markdownOutputDir: envSettings.markdownOutputDir } : {},
|
|
1504
|
-
...envSettings.assetOutputDir ? { assetOutputDir: envSettings.assetOutputDir } : {},
|
|
1505
1564
|
...envSettings.style ? { style: envSettings.style } : {},
|
|
1506
1565
|
...envSettings.intent ? { intent: envSettings.intent } : {},
|
|
1507
1566
|
...envSettings.targetLength ? { targetLength: envSettings.targetLength } : {},
|
|
@@ -1564,7 +1623,7 @@ function assertNoLegacyXMode(contentTargets, sourceLabel) {
|
|
|
1564
1623
|
}
|
|
1565
1624
|
|
|
1566
1625
|
// src/pipeline/runner.ts
|
|
1567
|
-
import { mkdir as
|
|
1626
|
+
import { mkdir as mkdir6, stat as stat2 } from "fs/promises";
|
|
1568
1627
|
import { randomUUID } from "crypto";
|
|
1569
1628
|
import path8 from "path";
|
|
1570
1629
|
|
|
@@ -1883,7 +1942,7 @@ function toExpressionPreview(expression, maxLength = 60) {
|
|
|
1883
1942
|
// src/llm/prompts/writingFramework.ts
|
|
1884
1943
|
function buildRunContextDirective(contentTypes) {
|
|
1885
1944
|
const normalizedTypes = contentTypes.length > 0 ? contentTypes.join(", ") : "article";
|
|
1886
|
-
return `Run context: requested content types are ${normalizedTypes}. Keep output aligned with this distribution plan, maintain one shared content
|
|
1945
|
+
return `Run context: requested content types are ${normalizedTypes}. Keep output aligned with this distribution plan, maintain one shared content plan, and adapt structure per channel without duplicating article-only scaffolding.`;
|
|
1887
1946
|
}
|
|
1888
1947
|
var TARGET_LENGTH_TIERS = {
|
|
1889
1948
|
small: {
|
|
@@ -1940,6 +1999,23 @@ function buildTargetLengthDirective(contentType, targetLengthWords) {
|
|
|
1940
1999
|
// src/llm/prompts/guideBundles.ts
|
|
1941
2000
|
import { existsSync, readFileSync } from "fs";
|
|
1942
2001
|
import path5 from "path";
|
|
2002
|
+
|
|
2003
|
+
// src/types/article.ts
|
|
2004
|
+
var LONG_FORM_CONTENT_TYPES = [
|
|
2005
|
+
"article",
|
|
2006
|
+
"blog-post",
|
|
2007
|
+
"newsletter",
|
|
2008
|
+
"press-release",
|
|
2009
|
+
"science-paper"
|
|
2010
|
+
];
|
|
2011
|
+
function isLongFormContentType(contentType) {
|
|
2012
|
+
return LONG_FORM_CONTENT_TYPES.includes(contentType);
|
|
2013
|
+
}
|
|
2014
|
+
function isLongFormPlan(plan) {
|
|
2015
|
+
return isLongFormContentType(plan.contentType) && plan.sections !== void 0 && plan.sections.length > 0;
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
// src/llm/prompts/guideBundles.ts
|
|
1943
2019
|
var guideCache = /* @__PURE__ */ new Map();
|
|
1944
2020
|
function normalizeGuideContent(content) {
|
|
1945
2021
|
return content.replace(/\r\n/g, "\n").trim();
|
|
@@ -1991,14 +2067,22 @@ function buildGuideBundle(relativePaths) {
|
|
|
1991
2067
|
...blocks
|
|
1992
2068
|
].join("\n\n");
|
|
1993
2069
|
}
|
|
1994
|
-
function
|
|
1995
|
-
|
|
2070
|
+
function buildPrimaryPlanGuideInstruction(intent, contentType) {
|
|
2071
|
+
const baseGuides = [
|
|
1996
2072
|
"writing-guide/references/headline-writing-systems.md",
|
|
1997
2073
|
"writing-guide/references/ideation-and-credibility-systems.md",
|
|
1998
2074
|
"writing-guide/references/content-frameworks.md",
|
|
1999
2075
|
intentToGuidePath(intent),
|
|
2000
2076
|
formatToGuidePath(contentType)
|
|
2001
|
-
]
|
|
2077
|
+
];
|
|
2078
|
+
if (!isLongFormContentType(contentType)) {
|
|
2079
|
+
return buildGuideBundle([
|
|
2080
|
+
"writing-guide/references/headline-writing-systems.md",
|
|
2081
|
+
intentToGuidePath(intent),
|
|
2082
|
+
formatToGuidePath(contentType)
|
|
2083
|
+
]);
|
|
2084
|
+
}
|
|
2085
|
+
return buildGuideBundle(baseGuides);
|
|
2002
2086
|
}
|
|
2003
2087
|
function buildArticleSectionGuideInstruction(style, intent, contentType) {
|
|
2004
2088
|
return buildGuideBundle([
|
|
@@ -2012,9 +2096,9 @@ function buildArticleSectionGuideInstruction(style, intent, contentType) {
|
|
|
2012
2096
|
formatToGuidePath(contentType)
|
|
2013
2097
|
]);
|
|
2014
2098
|
}
|
|
2015
|
-
function
|
|
2099
|
+
function buildContentPlanGuideInstruction(intent, primaryContentType, secondaryContentTypes) {
|
|
2016
2100
|
return buildGuideBundle([
|
|
2017
|
-
"writing-guide/references/multi-channel-
|
|
2101
|
+
"writing-guide/references/multi-channel-plan-strategy.md",
|
|
2018
2102
|
"writing-guide/references/content-frameworks.md",
|
|
2019
2103
|
"writing-guide/references/target-length-guidance.md",
|
|
2020
2104
|
intentToGuidePath(intent),
|
|
@@ -2034,8 +2118,8 @@ function buildChannelContentGuideInstruction(style, intent, contentType) {
|
|
|
2034
2118
|
]);
|
|
2035
2119
|
}
|
|
2036
2120
|
|
|
2037
|
-
// src/llm/prompts/
|
|
2038
|
-
var
|
|
2121
|
+
// src/llm/prompts/contentPlan.ts
|
|
2122
|
+
var contentPlanSchema = {
|
|
2039
2123
|
type: "object",
|
|
2040
2124
|
additionalProperties: false,
|
|
2041
2125
|
required: [
|
|
@@ -2069,15 +2153,15 @@ var contentBriefSchema = {
|
|
|
2069
2153
|
secondaryContentStrategy: { type: "string" }
|
|
2070
2154
|
}
|
|
2071
2155
|
};
|
|
2072
|
-
function
|
|
2156
|
+
function buildContentPlanMessages(idea, options) {
|
|
2073
2157
|
const audienceSeed = options.targetAudienceHint?.trim() || "A general, non-specific audience.";
|
|
2074
2158
|
const hasSecondaryContentTypes = options.secondaryContentTypes.length > 0;
|
|
2075
2159
|
const systemInstruction = [
|
|
2076
2160
|
"You are a senior editorial strategist.",
|
|
2077
|
-
"Produce a shared content
|
|
2078
|
-
|
|
2161
|
+
"Produce a shared content plan that can guide all requested content types in this run.",
|
|
2162
|
+
buildContentPlanGuideInstruction(options.intent, options.primaryContentType, options.secondaryContentTypes),
|
|
2079
2163
|
buildRunContextDirective([options.primaryContentType, ...options.secondaryContentTypes]),
|
|
2080
|
-
"The
|
|
2164
|
+
"The plan must be specific, concrete, and directly usable by writers without extra clarification.",
|
|
2081
2165
|
"This run has one explicit primary output and optional secondary outputs that should promote or incite interest in the primary while remaining independently valuable.",
|
|
2082
2166
|
"Return only the requested JSON."
|
|
2083
2167
|
].join(" ");
|
|
@@ -2089,7 +2173,7 @@ function buildContentBriefMessages(idea, options) {
|
|
|
2089
2173
|
{
|
|
2090
2174
|
role: "user",
|
|
2091
2175
|
content: [
|
|
2092
|
-
"Create a shared content
|
|
2176
|
+
"Create a shared content plan from this idea:",
|
|
2093
2177
|
idea,
|
|
2094
2178
|
"",
|
|
2095
2179
|
`Audience seed (optional user guidance): ${audienceSeed}`,
|
|
@@ -2112,7 +2196,7 @@ function buildContentBriefMessages(idea, options) {
|
|
|
2112
2196
|
];
|
|
2113
2197
|
}
|
|
2114
2198
|
|
|
2115
|
-
// src/types/
|
|
2199
|
+
// src/types/contentPlanSchema.ts
|
|
2116
2200
|
import { z as z4 } from "zod";
|
|
2117
2201
|
var secondaryTypeSentinelValues = /* @__PURE__ */ new Set([
|
|
2118
2202
|
"none",
|
|
@@ -2123,7 +2207,7 @@ var secondaryTypeSentinelValues = /* @__PURE__ */ new Set([
|
|
|
2123
2207
|
"no secondary content",
|
|
2124
2208
|
"no secondary outputs"
|
|
2125
2209
|
]);
|
|
2126
|
-
var
|
|
2210
|
+
var contentPlanSchema2 = z4.object({
|
|
2127
2211
|
title: z4.string().min(8),
|
|
2128
2212
|
description: z4.string().min(40),
|
|
2129
2213
|
targetAudience: z4.string().min(10),
|
|
@@ -2133,12 +2217,12 @@ var contentBriefSchema2 = z4.object({
|
|
|
2133
2217
|
primaryContentType: z4.string().min(2),
|
|
2134
2218
|
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()))),
|
|
2135
2219
|
secondaryContentStrategy: z4.string()
|
|
2136
|
-
}).superRefine((
|
|
2137
|
-
const hasSecondaryTargets =
|
|
2220
|
+
}).superRefine((plan, ctx) => {
|
|
2221
|
+
const hasSecondaryTargets = plan.secondaryContentTypes.length > 0;
|
|
2138
2222
|
if (!hasSecondaryTargets) {
|
|
2139
2223
|
return;
|
|
2140
2224
|
}
|
|
2141
|
-
if (
|
|
2225
|
+
if (plan.secondaryContentStrategy.trim().length < 20) {
|
|
2142
2226
|
ctx.addIssue({
|
|
2143
2227
|
code: z4.ZodIssueCode.too_small,
|
|
2144
2228
|
minimum: 20,
|
|
@@ -2151,9 +2235,9 @@ var contentBriefSchema2 = z4.object({
|
|
|
2151
2235
|
}
|
|
2152
2236
|
});
|
|
2153
2237
|
|
|
2154
|
-
// src/generation/
|
|
2155
|
-
var
|
|
2156
|
-
async function
|
|
2238
|
+
// src/generation/planContentPlan.ts
|
|
2239
|
+
var SHARED_PLAN_MAX_TOKENS = 8e3;
|
|
2240
|
+
async function planContentPlan({
|
|
2157
2241
|
idea,
|
|
2158
2242
|
targetAudienceHint,
|
|
2159
2243
|
settings,
|
|
@@ -2163,37 +2247,37 @@ async function planContentBrief({
|
|
|
2163
2247
|
onInteraction
|
|
2164
2248
|
}) {
|
|
2165
2249
|
if (dryRun || !openRouter) {
|
|
2166
|
-
return
|
|
2250
|
+
return buildDryRunContentPlan(idea, targetAudienceHint);
|
|
2167
2251
|
}
|
|
2168
|
-
const
|
|
2252
|
+
const sharedPlanSettings = {
|
|
2169
2253
|
...settings,
|
|
2170
2254
|
modelSettings: {
|
|
2171
2255
|
...settings.modelSettings,
|
|
2172
|
-
maxTokens: Math.max(settings.modelSettings.maxTokens,
|
|
2256
|
+
maxTokens: Math.max(settings.modelSettings.maxTokens, SHARED_PLAN_MAX_TOKENS)
|
|
2173
2257
|
}
|
|
2174
2258
|
};
|
|
2175
2259
|
return await openRouter.requestStructured({
|
|
2176
|
-
schemaName: "
|
|
2177
|
-
schema:
|
|
2178
|
-
messages:
|
|
2260
|
+
schemaName: "content_plan",
|
|
2261
|
+
schema: contentPlanSchema,
|
|
2262
|
+
messages: buildContentPlanMessages(idea, {
|
|
2179
2263
|
intent: settings.intent,
|
|
2180
2264
|
targetAudienceHint,
|
|
2181
2265
|
primaryContentType: settings.contentTargets.find((target) => target.role === "primary")?.contentType ?? "article",
|
|
2182
2266
|
secondaryContentTypes: settings.contentTargets.filter((target) => target.role === "secondary").map((target) => target.contentType)
|
|
2183
2267
|
}),
|
|
2184
|
-
settings:
|
|
2268
|
+
settings: sharedPlanSettings,
|
|
2185
2269
|
interactionContext: {
|
|
2186
|
-
stageId: "shared-
|
|
2187
|
-
operationId: "shared-
|
|
2270
|
+
stageId: "shared-plan",
|
|
2271
|
+
operationId: "shared-plan:content-plan"
|
|
2188
2272
|
},
|
|
2189
2273
|
onInteraction,
|
|
2190
2274
|
onMetrics: onLlmMetrics,
|
|
2191
2275
|
parse(data) {
|
|
2192
|
-
return
|
|
2276
|
+
return contentPlanSchema2.parse(data);
|
|
2193
2277
|
}
|
|
2194
2278
|
});
|
|
2195
2279
|
}
|
|
2196
|
-
function
|
|
2280
|
+
function buildDryRunContentPlan(idea, targetAudienceHint) {
|
|
2197
2281
|
const normalizedIdea = idea.trim();
|
|
2198
2282
|
const normalizedAudience = targetAudienceHint?.trim();
|
|
2199
2283
|
const targetAudience = normalizedAudience && normalizedAudience.length > 0 ? `Audience seed: ${normalizedAudience}. Extend this profile with specific motivations, constraints, and context tied to ${normalizedIdea}.` : "A broad, general audience of curious professionals and creators seeking practical, applicable insight.";
|
|
@@ -2215,13 +2299,13 @@ function buildDryRunContentBrief(idea, targetAudienceHint) {
|
|
|
2215
2299
|
}
|
|
2216
2300
|
function deriveTitleFromIdea(idea) {
|
|
2217
2301
|
if (!idea) {
|
|
2218
|
-
return "Generated Content
|
|
2302
|
+
return "Generated Content Plan";
|
|
2219
2303
|
}
|
|
2220
2304
|
return idea.split(/\s+/).slice(0, 8).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
2221
2305
|
}
|
|
2222
2306
|
|
|
2223
|
-
// src/llm/prompts/
|
|
2224
|
-
function
|
|
2307
|
+
// src/llm/prompts/primaryPlan.ts
|
|
2308
|
+
function deriveSectionCounts(targetLengthWords) {
|
|
2225
2309
|
const normalizedWords = Number.isFinite(targetLengthWords) && targetLengthWords > 0 ? targetLengthWords : 900;
|
|
2226
2310
|
const center = Math.max(2, Math.min(10, Math.round(normalizedWords / 220)));
|
|
2227
2311
|
const min = Math.max(2, center - 1);
|
|
@@ -2232,12 +2316,20 @@ function deriveArticleSectionCounts(targetLengthWords) {
|
|
|
2232
2316
|
label: `${min} to ${max}`
|
|
2233
2317
|
};
|
|
2234
2318
|
}
|
|
2235
|
-
function
|
|
2236
|
-
|
|
2319
|
+
function buildPrimaryPlanJsonSchema(contentType, targetLengthWords) {
|
|
2320
|
+
if (!isLongFormContentType(contentType)) {
|
|
2321
|
+
return buildShortFormPlanJsonSchema();
|
|
2322
|
+
}
|
|
2323
|
+
return buildLongFormPlanJsonSchema(targetLengthWords);
|
|
2324
|
+
}
|
|
2325
|
+
function buildLongFormPlanJsonSchema(targetLengthWords) {
|
|
2326
|
+
const sectionCounts = deriveSectionCounts(targetLengthWords);
|
|
2327
|
+
const imageCounts = resolveDefaultInlineImageCount(targetLengthWords);
|
|
2237
2328
|
return {
|
|
2238
2329
|
type: "object",
|
|
2239
2330
|
additionalProperties: false,
|
|
2240
2331
|
required: [
|
|
2332
|
+
"contentType",
|
|
2241
2333
|
"title",
|
|
2242
2334
|
"subtitle",
|
|
2243
2335
|
"keywords",
|
|
@@ -2250,6 +2342,7 @@ function buildArticlePlanJsonSchema(targetLengthWords) {
|
|
|
2250
2342
|
"inlineImages"
|
|
2251
2343
|
],
|
|
2252
2344
|
properties: {
|
|
2345
|
+
contentType: { type: "string" },
|
|
2253
2346
|
title: { type: "string" },
|
|
2254
2347
|
subtitle: { type: "string" },
|
|
2255
2348
|
keywords: {
|
|
@@ -2279,8 +2372,8 @@ function buildArticlePlanJsonSchema(targetLengthWords) {
|
|
|
2279
2372
|
coverImageDescription: { type: "string" },
|
|
2280
2373
|
inlineImages: {
|
|
2281
2374
|
type: "array",
|
|
2282
|
-
minItems:
|
|
2283
|
-
maxItems:
|
|
2375
|
+
minItems: imageCounts.min,
|
|
2376
|
+
maxItems: imageCounts.max,
|
|
2284
2377
|
items: {
|
|
2285
2378
|
type: "object",
|
|
2286
2379
|
additionalProperties: false,
|
|
@@ -2294,13 +2387,42 @@ function buildArticlePlanJsonSchema(targetLengthWords) {
|
|
|
2294
2387
|
}
|
|
2295
2388
|
};
|
|
2296
2389
|
}
|
|
2297
|
-
function
|
|
2298
|
-
|
|
2390
|
+
function buildShortFormPlanJsonSchema() {
|
|
2391
|
+
return {
|
|
2392
|
+
type: "object",
|
|
2393
|
+
additionalProperties: false,
|
|
2394
|
+
required: [
|
|
2395
|
+
"contentType",
|
|
2396
|
+
"title",
|
|
2397
|
+
"slug",
|
|
2398
|
+
"description",
|
|
2399
|
+
"coverImageDescription",
|
|
2400
|
+
"angle"
|
|
2401
|
+
],
|
|
2402
|
+
properties: {
|
|
2403
|
+
contentType: { type: "string" },
|
|
2404
|
+
title: { type: "string" },
|
|
2405
|
+
slug: { type: "string" },
|
|
2406
|
+
description: { type: "string" },
|
|
2407
|
+
coverImageDescription: { type: "string" },
|
|
2408
|
+
angle: { type: "string" }
|
|
2409
|
+
}
|
|
2410
|
+
};
|
|
2411
|
+
}
|
|
2412
|
+
function buildPrimaryPlanMessages(idea, options) {
|
|
2413
|
+
if (!isLongFormContentType(options.contentType)) {
|
|
2414
|
+
return buildShortFormPlanMessages(idea, options);
|
|
2415
|
+
}
|
|
2416
|
+
return buildLongFormPlanMessages(idea, options);
|
|
2417
|
+
}
|
|
2418
|
+
function buildLongFormPlanMessages(idea, options) {
|
|
2419
|
+
const sectionCounts = deriveSectionCounts(options.targetLength);
|
|
2420
|
+
const imageCounts = resolveDefaultInlineImageCount(options.targetLength);
|
|
2299
2421
|
const systemInstruction = [
|
|
2300
|
-
"You are a senior editorial strategist. Produce a rigorous
|
|
2301
|
-
|
|
2422
|
+
"You are a senior editorial strategist. Produce a rigorous content plan for a polished long-form Markdown output.",
|
|
2423
|
+
buildPrimaryPlanGuideInstruction(options.intent, options.contentType),
|
|
2302
2424
|
buildRunContextDirective(options.contentTypes),
|
|
2303
|
-
buildTargetLengthDirective(
|
|
2425
|
+
buildTargetLengthDirective(options.contentType, options.targetLength),
|
|
2304
2426
|
"Return only the requested JSON."
|
|
2305
2427
|
].join(" ");
|
|
2306
2428
|
return [
|
|
@@ -2311,31 +2433,32 @@ function buildArticlePlanMessages(idea, options) {
|
|
|
2311
2433
|
{
|
|
2312
2434
|
role: "user",
|
|
2313
2435
|
content: [
|
|
2314
|
-
|
|
2436
|
+
`Create a ${options.contentType} plan from this idea:`,
|
|
2315
2437
|
idea,
|
|
2316
2438
|
"",
|
|
2317
2439
|
"Requirements:",
|
|
2318
|
-
"- The
|
|
2440
|
+
"- The content should feel authoritative, practical, and clearly structured for scanning and deep reading.",
|
|
2319
2441
|
"- Generate a memorable title and a sharp subtitle that promise a concrete benefit, mechanism, or outcome.",
|
|
2320
2442
|
"- The slug must be lowercase kebab-case and publication-ready.",
|
|
2321
|
-
"- The description should work as a concise meta description and align with the shared content
|
|
2443
|
+
"- The description should work as a concise meta description and align with the shared content plan.",
|
|
2322
2444
|
`- Plan ${sectionCounts.label} strong sections with distinct focus areas and logical progression (no repetitive section intent).`,
|
|
2323
2445
|
"- Frame section titles to reflect likely search intent or practical reader questions when appropriate.",
|
|
2324
2446
|
"- Each section description should name the mechanism, evidence type, or practical action that makes the section useful.",
|
|
2325
|
-
"- Sections are
|
|
2326
|
-
|
|
2447
|
+
"- Sections are primary-only structure and must not be treated as requirements for non-primary channels.",
|
|
2448
|
+
`- Include a cover image description and ${imageCounts.min} to ${imageCounts.max} inline image descriptions.`,
|
|
2327
2449
|
"- Each inline image must specify which section it follows (anchorAfterSection, 1-based index). Choose sections where visual reinforcement adds the most value.",
|
|
2328
2450
|
"- Image descriptions should capture the general concept and mood \u2014 the exact text-to-image prompt will be refined later using the actual section content.",
|
|
2329
2451
|
"- Image descriptions must be concrete and contextual, not generic stock-photo phrasing.",
|
|
2330
2452
|
"",
|
|
2331
|
-
"Shared content
|
|
2332
|
-
`- description: ${options.
|
|
2333
|
-
`- targetAudience: ${options.
|
|
2334
|
-
`- corePromise: ${options.
|
|
2335
|
-
`- keyPoints: ${options.
|
|
2336
|
-
`- voiceNotes: ${options.
|
|
2453
|
+
"Shared content plan context:",
|
|
2454
|
+
`- description: ${options.contentPlan.description}`,
|
|
2455
|
+
`- targetAudience: ${options.contentPlan.targetAudience}`,
|
|
2456
|
+
`- corePromise: ${options.contentPlan.corePromise}`,
|
|
2457
|
+
`- keyPoints: ${options.contentPlan.keyPoints.join(" | ")}`,
|
|
2458
|
+
`- voiceNotes: ${options.contentPlan.voiceNotes}`,
|
|
2337
2459
|
"",
|
|
2338
2460
|
"Return JSON with all required fields:",
|
|
2461
|
+
`- contentType: set to "${options.contentType}" exactly`,
|
|
2339
2462
|
"- title: string",
|
|
2340
2463
|
"- subtitle: string",
|
|
2341
2464
|
"- keywords: array of 3 to 8 strings",
|
|
@@ -2345,7 +2468,53 @@ function buildArticlePlanMessages(idea, options) {
|
|
|
2345
2468
|
"- outroBrief: string",
|
|
2346
2469
|
`- sections: array of ${sectionCounts.label} objects, each with title and description strings`,
|
|
2347
2470
|
"- coverImageDescription: string",
|
|
2348
|
-
|
|
2471
|
+
`- inlineImages: array of ${imageCounts.min} to ${imageCounts.max} objects, each with a description string and an anchorAfterSection number (1-based section index)`,
|
|
2472
|
+
"",
|
|
2473
|
+
"Do not omit any required fields. Return strict JSON only."
|
|
2474
|
+
].join("\n")
|
|
2475
|
+
}
|
|
2476
|
+
];
|
|
2477
|
+
}
|
|
2478
|
+
function buildShortFormPlanMessages(idea, options) {
|
|
2479
|
+
const systemInstruction = [
|
|
2480
|
+
"You are a senior content strategist. Produce a concise content plan for a short-form social media post.",
|
|
2481
|
+
buildPrimaryPlanGuideInstruction(options.intent, options.contentType),
|
|
2482
|
+
buildRunContextDirective(options.contentTypes),
|
|
2483
|
+
"Return only the requested JSON."
|
|
2484
|
+
].join(" ");
|
|
2485
|
+
return [
|
|
2486
|
+
{
|
|
2487
|
+
role: "system",
|
|
2488
|
+
content: systemInstruction
|
|
2489
|
+
},
|
|
2490
|
+
{
|
|
2491
|
+
role: "user",
|
|
2492
|
+
content: [
|
|
2493
|
+
`Create a ${options.contentType} plan from this idea:`,
|
|
2494
|
+
idea,
|
|
2495
|
+
"",
|
|
2496
|
+
"Requirements:",
|
|
2497
|
+
"- Generate a sharp, attention-grabbing title suitable for social media.",
|
|
2498
|
+
"- The slug must be lowercase kebab-case and publication-ready.",
|
|
2499
|
+
"- The description should capture the core message in one sentence.",
|
|
2500
|
+
"- The angle should describe the hook, framing, or unique take that makes this post compelling.",
|
|
2501
|
+
"- Include a cover image description that works as a visual anchor for the post.",
|
|
2502
|
+
"- Do NOT include sections, subtitles, keywords, intros, or outros \u2014 this is short-form content.",
|
|
2503
|
+
"",
|
|
2504
|
+
"Shared content plan context:",
|
|
2505
|
+
`- description: ${options.contentPlan.description}`,
|
|
2506
|
+
`- targetAudience: ${options.contentPlan.targetAudience}`,
|
|
2507
|
+
`- corePromise: ${options.contentPlan.corePromise}`,
|
|
2508
|
+
`- keyPoints: ${options.contentPlan.keyPoints.join(" | ")}`,
|
|
2509
|
+
`- voiceNotes: ${options.contentPlan.voiceNotes}`,
|
|
2510
|
+
"",
|
|
2511
|
+
"Return JSON with all required fields:",
|
|
2512
|
+
`- contentType: set to "${options.contentType}" exactly`,
|
|
2513
|
+
"- title: string (short, punchy, social-media-ready)",
|
|
2514
|
+
"- slug: string in lowercase kebab-case",
|
|
2515
|
+
"- description: string (one-sentence core message)",
|
|
2516
|
+
"- coverImageDescription: string",
|
|
2517
|
+
"- angle: string (the hook or framing that makes this post work)",
|
|
2349
2518
|
"",
|
|
2350
2519
|
"Do not omit any required fields. Return strict JSON only."
|
|
2351
2520
|
].join("\n")
|
|
@@ -2363,7 +2532,22 @@ var inlineImagePlanSchema = z5.object({
|
|
|
2363
2532
|
description: z5.string().min(1),
|
|
2364
2533
|
anchorAfterSection: z5.number().int().min(1)
|
|
2365
2534
|
});
|
|
2366
|
-
var
|
|
2535
|
+
var primaryPlanSchema = z5.object({
|
|
2536
|
+
contentType: z5.string().min(1).default("article"),
|
|
2537
|
+
title: z5.string().min(1),
|
|
2538
|
+
slug: z5.string().min(1),
|
|
2539
|
+
description: z5.string().min(1),
|
|
2540
|
+
coverImageDescription: z5.string().min(1),
|
|
2541
|
+
subtitle: z5.string().min(1).optional(),
|
|
2542
|
+
keywords: z5.array(z5.string().min(1)).min(3).max(8).optional(),
|
|
2543
|
+
introBrief: z5.string().min(1).optional(),
|
|
2544
|
+
outroBrief: z5.string().min(1).optional(),
|
|
2545
|
+
sections: z5.array(articleSectionPlanSchema).min(2).max(10).optional(),
|
|
2546
|
+
inlineImages: z5.array(inlineImagePlanSchema).min(2).max(3).optional(),
|
|
2547
|
+
angle: z5.string().min(1).optional()
|
|
2548
|
+
});
|
|
2549
|
+
var longFormPlanSchema = z5.object({
|
|
2550
|
+
contentType: z5.string().min(1),
|
|
2367
2551
|
title: z5.string().min(1),
|
|
2368
2552
|
subtitle: z5.string().min(1),
|
|
2369
2553
|
keywords: z5.array(z5.string().min(1)).min(3).max(8),
|
|
@@ -2375,14 +2559,23 @@ var articlePlanSchema = z5.object({
|
|
|
2375
2559
|
coverImageDescription: z5.string().min(1),
|
|
2376
2560
|
inlineImages: z5.array(inlineImagePlanSchema).min(2).max(3)
|
|
2377
2561
|
});
|
|
2562
|
+
var shortFormPlanSchema = z5.object({
|
|
2563
|
+
contentType: z5.string().min(1),
|
|
2564
|
+
title: z5.string().min(1),
|
|
2565
|
+
slug: z5.string().min(1),
|
|
2566
|
+
description: z5.string().min(1),
|
|
2567
|
+
coverImageDescription: z5.string().min(1),
|
|
2568
|
+
angle: z5.string().min(1).optional()
|
|
2569
|
+
});
|
|
2378
2570
|
var imagePromptResultSchema = z5.object({
|
|
2379
2571
|
prompt: z5.string().min(1)
|
|
2380
2572
|
});
|
|
2381
2573
|
|
|
2382
|
-
// src/generation/
|
|
2383
|
-
async function
|
|
2574
|
+
// src/generation/planPrimaryContent.ts
|
|
2575
|
+
async function planPrimaryContent({
|
|
2384
2576
|
idea,
|
|
2385
|
-
|
|
2577
|
+
contentType,
|
|
2578
|
+
contentPlan,
|
|
2386
2579
|
settings,
|
|
2387
2580
|
markdownOutputDir,
|
|
2388
2581
|
openRouter,
|
|
@@ -2390,56 +2583,79 @@ async function planArticle({
|
|
|
2390
2583
|
onLlmMetrics,
|
|
2391
2584
|
onInteraction
|
|
2392
2585
|
}) {
|
|
2393
|
-
const
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2586
|
+
const isLongForm = isLongFormContentType(contentType);
|
|
2587
|
+
const basePlan = dryRun || !openRouter ? buildDryRunPlan(idea, contentType, contentPlan) : await openRouter.requestStructured({
|
|
2588
|
+
schemaName: "primary_plan",
|
|
2589
|
+
schema: buildPrimaryPlanJsonSchema(contentType, settings.targetLength),
|
|
2590
|
+
messages: buildPrimaryPlanMessages(idea, {
|
|
2591
|
+
contentType,
|
|
2397
2592
|
intent: settings.intent,
|
|
2398
2593
|
contentTypes: settings.contentTargets.map((target) => target.contentType),
|
|
2399
|
-
|
|
2594
|
+
contentPlan,
|
|
2400
2595
|
targetLength: settings.targetLength
|
|
2401
2596
|
}),
|
|
2402
2597
|
settings,
|
|
2403
2598
|
interactionContext: {
|
|
2404
2599
|
stageId: "planning",
|
|
2405
|
-
operationId:
|
|
2600
|
+
operationId: `planning:${contentType}-plan`
|
|
2406
2601
|
},
|
|
2407
2602
|
onInteraction,
|
|
2408
2603
|
onMetrics: onLlmMetrics,
|
|
2409
2604
|
parse(data) {
|
|
2410
|
-
|
|
2605
|
+
if (isLongForm) {
|
|
2606
|
+
return longFormPlanSchema.parse(data);
|
|
2607
|
+
}
|
|
2608
|
+
return shortFormPlanSchema.parse(data);
|
|
2411
2609
|
}
|
|
2412
2610
|
});
|
|
2413
2611
|
const normalizedSlug = slugify(basePlan.slug || basePlan.title);
|
|
2414
2612
|
const uniqueSlug = await resolveUniqueSlug(markdownOutputDir, normalizedSlug);
|
|
2415
|
-
|
|
2613
|
+
if (isLongForm) {
|
|
2614
|
+
const longPlan = basePlan;
|
|
2615
|
+
const sectionCount = longPlan.sections.length;
|
|
2616
|
+
return {
|
|
2617
|
+
...longPlan,
|
|
2618
|
+
slug: uniqueSlug,
|
|
2619
|
+
keywords: longPlan.keywords.slice(0, 8),
|
|
2620
|
+
inlineImages: longPlan.inlineImages.slice(0, 3).map((img) => ({
|
|
2621
|
+
...img,
|
|
2622
|
+
anchorAfterSection: Math.max(1, Math.min(sectionCount, img.anchorAfterSection))
|
|
2623
|
+
}))
|
|
2624
|
+
};
|
|
2625
|
+
}
|
|
2416
2626
|
return {
|
|
2417
2627
|
...basePlan,
|
|
2418
|
-
slug: uniqueSlug
|
|
2419
|
-
keywords: basePlan.keywords.slice(0, 8),
|
|
2420
|
-
inlineImages: basePlan.inlineImages.slice(0, 3).map((img) => ({
|
|
2421
|
-
...img,
|
|
2422
|
-
anchorAfterSection: Math.max(1, Math.min(sectionCount, img.anchorAfterSection))
|
|
2423
|
-
}))
|
|
2628
|
+
slug: uniqueSlug
|
|
2424
2629
|
};
|
|
2425
2630
|
}
|
|
2426
|
-
function buildDryRunPlan(idea,
|
|
2427
|
-
const title = idea.trim().split(/\s+/).slice(0, 7).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
2631
|
+
function buildDryRunPlan(idea, contentType, contentPlan) {
|
|
2632
|
+
const title = idea.trim().split(/\s+/).slice(0, 7).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ") || contentPlan.title;
|
|
2633
|
+
if (!isLongFormContentType(contentType)) {
|
|
2634
|
+
return {
|
|
2635
|
+
contentType,
|
|
2636
|
+
title,
|
|
2637
|
+
slug: slugify(title),
|
|
2638
|
+
description: contentPlan.description,
|
|
2639
|
+
coverImageDescription: `A visually striking cover image for a ${contentType} about ${idea.trim().split(/\s+/).slice(0, 5).join(" ")}.`,
|
|
2640
|
+
angle: `Direct, practical framing that hooks the audience immediately with a clear value proposition tied to ${contentPlan.corePromise}`
|
|
2641
|
+
};
|
|
2642
|
+
}
|
|
2428
2643
|
return {
|
|
2644
|
+
contentType,
|
|
2429
2645
|
title,
|
|
2430
|
-
subtitle: "A practical editorial blueprint for turning a good idea into
|
|
2646
|
+
subtitle: "A practical editorial blueprint for turning a good idea into strong published content",
|
|
2431
2647
|
keywords: ["writing", "editorial workflow", "ai tools", "content strategy"],
|
|
2432
2648
|
slug: slugify(title),
|
|
2433
|
-
description:
|
|
2649
|
+
description: contentPlan.description,
|
|
2434
2650
|
introBrief: "Frame the tension between having ideas and actually shaping them into useful published work.",
|
|
2435
2651
|
outroBrief: "End by emphasizing disciplined workflows, taste, and iteration.",
|
|
2436
2652
|
sections: [
|
|
2437
2653
|
{
|
|
2438
2654
|
title: "Why raw ideas are not enough",
|
|
2439
|
-
description: "Explain why strong
|
|
2655
|
+
description: "Explain why strong content needs structure, intent, and editorial judgment."
|
|
2440
2656
|
},
|
|
2441
2657
|
{
|
|
2442
|
-
title: "Designing the
|
|
2658
|
+
title: "Designing the content before drafting",
|
|
2443
2659
|
description: "Show how planning title, sections, and narrative flow improves the final result."
|
|
2444
2660
|
},
|
|
2445
2661
|
{
|
|
@@ -2458,7 +2674,7 @@ function buildDryRunPlan(idea, contentBrief) {
|
|
|
2458
2674
|
coverImageDescription: "A refined editorial workspace with notebooks, sketches, and glowing structured outlines, cinematic but minimal.",
|
|
2459
2675
|
inlineImages: [
|
|
2460
2676
|
{
|
|
2461
|
-
description: "A rough idea evolving into a structured
|
|
2677
|
+
description: "A rough idea evolving into a structured content outline on a desk full of notes.",
|
|
2462
2678
|
anchorAfterSection: 2
|
|
2463
2679
|
},
|
|
2464
2680
|
{
|
|
@@ -2469,7 +2685,7 @@ function buildDryRunPlan(idea, contentBrief) {
|
|
|
2469
2685
|
};
|
|
2470
2686
|
}
|
|
2471
2687
|
function slugify(value2) {
|
|
2472
|
-
return value2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "untitled-
|
|
2688
|
+
return value2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "untitled-content";
|
|
2473
2689
|
}
|
|
2474
2690
|
|
|
2475
2691
|
// src/llm/prompts/channelContent.ts
|
|
@@ -2495,6 +2711,14 @@ function buildSingleShotContentMessages(options) {
|
|
|
2495
2711
|
`This output is secondary content and must promote or incite interest in the primary ${options.primaryContentType} content.`,
|
|
2496
2712
|
"Keep it independently useful, avoid sounding like an ad, and include channel-native cues that point back to the primary narrative."
|
|
2497
2713
|
].join(" ");
|
|
2714
|
+
const planContext = options.plan ? [
|
|
2715
|
+
"",
|
|
2716
|
+
"Primary content plan (use to guide tone, angle, and structure):",
|
|
2717
|
+
`- title: ${options.plan.title}`,
|
|
2718
|
+
`- description: ${options.plan.description}`,
|
|
2719
|
+
`- coverImageDescription: ${options.plan.coverImageDescription}`,
|
|
2720
|
+
...options.plan.angle ? [`- angle: ${options.plan.angle}`] : []
|
|
2721
|
+
].join("\n") : "";
|
|
2498
2722
|
return [
|
|
2499
2723
|
{
|
|
2500
2724
|
role: "system",
|
|
@@ -2515,16 +2739,17 @@ function buildSingleShotContentMessages(options) {
|
|
|
2515
2739
|
`Primary content type: ${options.primaryContentType}`,
|
|
2516
2740
|
`Output index: ${options.outputIndex} of ${options.outputCountForType}`,
|
|
2517
2741
|
"",
|
|
2518
|
-
"Shared content
|
|
2519
|
-
`- title: ${options.
|
|
2520
|
-
`- description: ${options.
|
|
2521
|
-
`- targetAudience: ${options.
|
|
2522
|
-
`- corePromise: ${options.
|
|
2523
|
-
`- keyPoints: ${options.
|
|
2524
|
-
`- voiceNotes: ${options.
|
|
2525
|
-
`- primaryContentType: ${options.
|
|
2526
|
-
`- secondaryContentTypes: ${options.
|
|
2527
|
-
`- secondaryContentStrategy: ${options.
|
|
2742
|
+
"Shared content plan (must guide this output):",
|
|
2743
|
+
`- title: ${options.contentPlan.title}`,
|
|
2744
|
+
`- description: ${options.contentPlan.description}`,
|
|
2745
|
+
`- targetAudience: ${options.contentPlan.targetAudience}`,
|
|
2746
|
+
`- corePromise: ${options.contentPlan.corePromise}`,
|
|
2747
|
+
`- keyPoints: ${options.contentPlan.keyPoints.join(" | ")}`,
|
|
2748
|
+
`- voiceNotes: ${options.contentPlan.voiceNotes}`,
|
|
2749
|
+
`- primaryContentType: ${options.contentPlan.primaryContentType}`,
|
|
2750
|
+
`- secondaryContentTypes: ${options.contentPlan.secondaryContentTypes.join(" | ") || "none"}`,
|
|
2751
|
+
`- secondaryContentStrategy: ${options.contentPlan.secondaryContentStrategy}`,
|
|
2752
|
+
planContext,
|
|
2528
2753
|
"",
|
|
2529
2754
|
articleContext,
|
|
2530
2755
|
"",
|
|
@@ -2548,7 +2773,8 @@ async function writeSingleShotContent({
|
|
|
2548
2773
|
outputIndex,
|
|
2549
2774
|
outputCountForType,
|
|
2550
2775
|
articleReferenceMarkdown,
|
|
2551
|
-
|
|
2776
|
+
contentPlan,
|
|
2777
|
+
plan,
|
|
2552
2778
|
settings,
|
|
2553
2779
|
openRouter,
|
|
2554
2780
|
dryRun,
|
|
@@ -2563,7 +2789,8 @@ async function writeSingleShotContent({
|
|
|
2563
2789
|
primaryContentType,
|
|
2564
2790
|
outputIndex,
|
|
2565
2791
|
outputCountForType,
|
|
2566
|
-
|
|
2792
|
+
contentPlan,
|
|
2793
|
+
plan,
|
|
2567
2794
|
articleReferenceMarkdown
|
|
2568
2795
|
});
|
|
2569
2796
|
}
|
|
@@ -2577,7 +2804,8 @@ async function writeSingleShotContent({
|
|
|
2577
2804
|
intent,
|
|
2578
2805
|
outputIndex,
|
|
2579
2806
|
outputCountForType,
|
|
2580
|
-
|
|
2807
|
+
contentPlan,
|
|
2808
|
+
plan,
|
|
2581
2809
|
articleReferenceMarkdown,
|
|
2582
2810
|
targetLength: settings.targetLength
|
|
2583
2811
|
}),
|
|
@@ -2592,6 +2820,9 @@ async function writeSingleShotContent({
|
|
|
2592
2820
|
}
|
|
2593
2821
|
function buildDryRunContent(options) {
|
|
2594
2822
|
const anchorNote = options.articleReferenceMarkdown ? "Anchored to generated primary context from this run." : "No primary anchor available; generated directly from idea.";
|
|
2823
|
+
const planNote = options.plan ? `Plan title: ${options.plan.title}
|
|
2824
|
+
Plan description: ${options.plan.description}${options.plan.angle ? `
|
|
2825
|
+
Angle: ${options.plan.angle}` : ""}` : "No primary plan available.";
|
|
2595
2826
|
return [
|
|
2596
2827
|
`# ${options.contentType} draft ${options.outputIndex}`,
|
|
2597
2828
|
"",
|
|
@@ -2599,7 +2830,8 @@ function buildDryRunContent(options) {
|
|
|
2599
2830
|
`Variant: ${options.outputIndex}/${options.outputCountForType}`,
|
|
2600
2831
|
`Role: ${options.role}`,
|
|
2601
2832
|
`Primary content type: ${options.primaryContentType}`,
|
|
2602
|
-
`Shared
|
|
2833
|
+
`Shared plan: ${options.contentPlan.description}`,
|
|
2834
|
+
planNote,
|
|
2603
2835
|
anchorNote,
|
|
2604
2836
|
"",
|
|
2605
2837
|
"This is a dry-run placeholder for single-prompt channel generation."
|
|
@@ -2622,12 +2854,12 @@ var OUTRO_PARAGRAPH_COUNTS = {
|
|
|
2622
2854
|
medium: "2 to 3",
|
|
2623
2855
|
large: "3 to 5"
|
|
2624
2856
|
};
|
|
2625
|
-
function buildSystemInstruction(base, style, intent, contentTypes, targetLengthWords) {
|
|
2857
|
+
function buildSystemInstruction(base, style, intent, contentTypes, targetLengthWords, contentType) {
|
|
2626
2858
|
return [
|
|
2627
2859
|
base,
|
|
2628
|
-
buildArticleSectionGuideInstruction(style, intent,
|
|
2860
|
+
buildArticleSectionGuideInstruction(style, intent, contentType),
|
|
2629
2861
|
buildRunContextDirective(contentTypes),
|
|
2630
|
-
buildTargetLengthDirective(
|
|
2862
|
+
buildTargetLengthDirective(contentType, targetLengthWords)
|
|
2631
2863
|
].join(" ");
|
|
2632
2864
|
}
|
|
2633
2865
|
function sharedPlanContext(plan) {
|
|
@@ -2657,7 +2889,8 @@ function buildIntroMessages(plan, style, intent, contentTypes, targetLengthWords
|
|
|
2657
2889
|
style,
|
|
2658
2890
|
intent,
|
|
2659
2891
|
contentTypes,
|
|
2660
|
-
targetLengthWords
|
|
2892
|
+
targetLengthWords,
|
|
2893
|
+
plan.contentType
|
|
2661
2894
|
);
|
|
2662
2895
|
const targetLengthAlias = resolveTargetLengthAlias(targetLengthWords);
|
|
2663
2896
|
const paragraphCount = INTRO_PARAGRAPH_COUNTS[targetLengthAlias] ?? INTRO_PARAGRAPH_COUNTS["medium"];
|
|
@@ -2687,7 +2920,8 @@ function buildSectionMessages(plan, section, articleSoFar, style, intent, conten
|
|
|
2687
2920
|
style,
|
|
2688
2921
|
intent,
|
|
2689
2922
|
contentTypes,
|
|
2690
|
-
targetLengthWords
|
|
2923
|
+
targetLengthWords,
|
|
2924
|
+
plan.contentType
|
|
2691
2925
|
);
|
|
2692
2926
|
const targetLengthAlias = resolveTargetLengthAlias(targetLengthWords);
|
|
2693
2927
|
const paragraphCount = SECTION_PARAGRAPH_COUNTS[targetLengthAlias] ?? SECTION_PARAGRAPH_COUNTS["medium"];
|
|
@@ -2721,7 +2955,8 @@ function buildOutroMessages(plan, style, intent, contentTypes, targetLengthWords
|
|
|
2721
2955
|
style,
|
|
2722
2956
|
intent,
|
|
2723
2957
|
contentTypes,
|
|
2724
|
-
targetLengthWords
|
|
2958
|
+
targetLengthWords,
|
|
2959
|
+
plan.contentType
|
|
2725
2960
|
);
|
|
2726
2961
|
const targetLengthAlias = resolveTargetLengthAlias(targetLengthWords);
|
|
2727
2962
|
const paragraphCount = OUTRO_PARAGRAPH_COUNTS[targetLengthAlias] ?? OUTRO_PARAGRAPH_COUNTS["medium"];
|
|
@@ -2876,19 +3111,36 @@ function buildArticleSoFarContext(intro, sections) {
|
|
|
2876
3111
|
}
|
|
2877
3112
|
return parts.join("\n\n").trim();
|
|
2878
3113
|
}
|
|
3114
|
+
function normalizeWhitespaceForHeadingMatch(text) {
|
|
3115
|
+
return text.toLowerCase().replace(/\s+/g, " ").trim();
|
|
3116
|
+
}
|
|
3117
|
+
function stripMatchingLeadingHeading(content, label2) {
|
|
3118
|
+
const headingMatch = content.match(/^#{1,6}\s+(.+?)(?:\r?\n|$)/);
|
|
3119
|
+
if (!headingMatch) {
|
|
3120
|
+
return content;
|
|
3121
|
+
}
|
|
3122
|
+
const headingText = normalizeWhitespaceForHeadingMatch(headingMatch[1] ?? "");
|
|
3123
|
+
const expectedLabel = normalizeWhitespaceForHeadingMatch(label2);
|
|
3124
|
+
if (headingText !== expectedLabel) {
|
|
3125
|
+
return content;
|
|
3126
|
+
}
|
|
3127
|
+
return content.slice(headingMatch[0].length).trimStart();
|
|
3128
|
+
}
|
|
2879
3129
|
function normalizeGeneratedSection(content, label2) {
|
|
2880
3130
|
const normalized = content.trim();
|
|
2881
3131
|
if (!normalized) {
|
|
2882
3132
|
throw new Error(`The model returned an empty ${label2} draft.`);
|
|
2883
3133
|
}
|
|
2884
|
-
|
|
3134
|
+
const withoutFences = normalized.replace(/^```(?:markdown)?\s*/i, "").replace(/```\s*$/i, "").trim();
|
|
3135
|
+
return stripMatchingLeadingHeading(withoutFences, label2).trim();
|
|
2885
3136
|
}
|
|
2886
3137
|
|
|
2887
3138
|
// src/pipeline/runner.ts
|
|
2888
3139
|
import { Limn } from "@telepat/limn";
|
|
2889
3140
|
|
|
2890
3141
|
// src/images/renderImages.ts
|
|
2891
|
-
import { writeFile as writeFile4 } from "fs/promises";
|
|
3142
|
+
import { copyFile, mkdir as mkdir4, rename, unlink, writeFile as writeFile4 } from "fs/promises";
|
|
3143
|
+
import { createHash } from "crypto";
|
|
2892
3144
|
import path6 from "path";
|
|
2893
3145
|
|
|
2894
3146
|
// src/llm/prompts/imagePrompt.ts
|
|
@@ -2965,6 +3217,39 @@ function sumKnownCosts(values) {
|
|
|
2965
3217
|
|
|
2966
3218
|
// src/images/renderImages.ts
|
|
2967
3219
|
var MIN_IMAGE_BYTES = 1024;
|
|
3220
|
+
function getLocalSessionArtifactDir(workingDir = process.cwd()) {
|
|
3221
|
+
const hash = createHash("sha256").update(path6.resolve(workingDir)).digest("hex").slice(0, 16);
|
|
3222
|
+
return path6.join(workingDir, ".ideon", "sessions", hash, "limn-artifacts");
|
|
3223
|
+
}
|
|
3224
|
+
function isNotFoundError(error) {
|
|
3225
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
3226
|
+
}
|
|
3227
|
+
function isCrossDeviceError(error) {
|
|
3228
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "EXDEV";
|
|
3229
|
+
}
|
|
3230
|
+
async function moveLimnTemporaryArtifact(savedPath) {
|
|
3231
|
+
if (!savedPath || savedPath.trim().length === 0) {
|
|
3232
|
+
return;
|
|
3233
|
+
}
|
|
3234
|
+
const sourcePath = path6.resolve(savedPath);
|
|
3235
|
+
const destinationDir = getLocalSessionArtifactDir(path6.dirname(sourcePath));
|
|
3236
|
+
const destinationPath = path6.join(destinationDir, path6.basename(sourcePath));
|
|
3237
|
+
await mkdir4(destinationDir, { recursive: true });
|
|
3238
|
+
try {
|
|
3239
|
+
await rename(sourcePath, destinationPath);
|
|
3240
|
+
return;
|
|
3241
|
+
} catch (error) {
|
|
3242
|
+
if (isCrossDeviceError(error)) {
|
|
3243
|
+
await copyFile(sourcePath, destinationPath);
|
|
3244
|
+
await unlink(sourcePath);
|
|
3245
|
+
return;
|
|
3246
|
+
}
|
|
3247
|
+
if (isNotFoundError(error)) {
|
|
3248
|
+
return;
|
|
3249
|
+
}
|
|
3250
|
+
throw error;
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
2968
3253
|
function buildImageSlots(plan, sections, options) {
|
|
2969
3254
|
const sectionCount = sections.length;
|
|
2970
3255
|
const slots = [
|
|
@@ -3140,12 +3425,14 @@ async function renderExpandedImages({
|
|
|
3140
3425
|
continue;
|
|
3141
3426
|
}
|
|
3142
3427
|
const family = settings.t2i.modelId;
|
|
3428
|
+
const replicateModelOverride = settings.t2i.replicateModelId;
|
|
3429
|
+
const limnOptions = {
|
|
3430
|
+
aspectRatio: "16:9",
|
|
3431
|
+
...replicateModelOverride && isReplicateModelIdForFamily(family, replicateModelOverride) ? { replicateModel: replicateModelOverride } : {}
|
|
3432
|
+
};
|
|
3143
3433
|
const renderStartedAtMs = Date.now();
|
|
3144
3434
|
try {
|
|
3145
|
-
const result = await limn.generate(prompt.prompt, family,
|
|
3146
|
-
replicateModel: settings.t2i.modelId,
|
|
3147
|
-
aspectRatio: "16:9"
|
|
3148
|
-
});
|
|
3435
|
+
const result = await limn.generate(prompt.prompt, family, limnOptions);
|
|
3149
3436
|
const ext = mimeTypeToExtension(result.mimeType);
|
|
3150
3437
|
const liveFileName = `${prompt.kind === "cover" ? "cover" : `inline-${prompt.anchorAfterSection}`}-${index + 1}.${ext}`;
|
|
3151
3438
|
const liveOutputPath = path6.join(assetDir, liveFileName);
|
|
@@ -3155,6 +3442,7 @@ async function renderExpandedImages({
|
|
|
3155
3442
|
);
|
|
3156
3443
|
}
|
|
3157
3444
|
await writeFile4(liveOutputPath, result.image);
|
|
3445
|
+
await moveLimnTemporaryArtifact(result.savedPath);
|
|
3158
3446
|
renderedImages.push({
|
|
3159
3447
|
...prompt,
|
|
3160
3448
|
outputPath: liveOutputPath,
|
|
@@ -3716,10 +4004,12 @@ ${body.join("\n").trim()}
|
|
|
3716
4004
|
}
|
|
3717
4005
|
|
|
3718
4006
|
// src/pipeline/sessionStore.ts
|
|
3719
|
-
import {
|
|
4007
|
+
import { createHash as createHash2 } from "crypto";
|
|
4008
|
+
import { mkdir as mkdir5, readFile as readFile5, rm as rm2, writeFile as writeFile5 } from "fs/promises";
|
|
3720
4009
|
import path7 from "path";
|
|
4010
|
+
import envPaths3 from "env-paths";
|
|
3721
4011
|
import { z as z6 } from "zod";
|
|
3722
|
-
var STAGE_IDS = ["shared-
|
|
4012
|
+
var STAGE_IDS = ["shared-plan", "planning", "sections", "image-prompts", "images", "output", "links"];
|
|
3723
4013
|
var generatedArticleSectionSchema = z6.object({
|
|
3724
4014
|
title: z6.string().min(1),
|
|
3725
4015
|
body: z6.string().min(1)
|
|
@@ -3779,8 +4069,8 @@ var writeSessionStateSchema = z6.object({
|
|
|
3779
4069
|
lastCompletedStage: z6.enum(STAGE_IDS).nullable(),
|
|
3780
4070
|
failedStage: z6.enum(STAGE_IDS).nullable(),
|
|
3781
4071
|
errorMessage: z6.string().nullable(),
|
|
3782
|
-
|
|
3783
|
-
plan:
|
|
4072
|
+
contentPlan: contentPlanSchema2.nullable().default(null),
|
|
4073
|
+
plan: primaryPlanSchema.nullable(),
|
|
3784
4074
|
text: z6.object({
|
|
3785
4075
|
intro: z6.string().min(1),
|
|
3786
4076
|
sections: z6.array(generatedArticleSectionSchema),
|
|
@@ -3794,16 +4084,24 @@ var writeSessionStateSchema = z6.object({
|
|
|
3794
4084
|
links: z6.array(linksResultSchema).nullable().default(null),
|
|
3795
4085
|
artifact: pipelineArtifactSummarySchema.nullable()
|
|
3796
4086
|
});
|
|
4087
|
+
var ideonPaths3 = envPaths3("ideon", { suffix: "" });
|
|
4088
|
+
var sessionsDir = path7.join(ideonPaths3.config, "sessions");
|
|
4089
|
+
function hashProjectPath(workingDir) {
|
|
4090
|
+
return createHash2("sha256").update(path7.resolve(workingDir)).digest("hex").slice(0, 16);
|
|
4091
|
+
}
|
|
3797
4092
|
function resolveWriteRoot(workingDir) {
|
|
3798
|
-
return path7.join(
|
|
4093
|
+
return path7.join(sessionsDir, hashProjectPath(workingDir));
|
|
3799
4094
|
}
|
|
3800
4095
|
function resolveStateFilePath(workingDir) {
|
|
3801
4096
|
return path7.join(resolveWriteRoot(workingDir), "state.json");
|
|
3802
4097
|
}
|
|
4098
|
+
function resolveLegacyStatePath(workingDir) {
|
|
4099
|
+
return path7.join(workingDir, ".ideon", "write", "state.json");
|
|
4100
|
+
}
|
|
3803
4101
|
async function startFreshWriteSession(seed, workingDir = process.cwd()) {
|
|
3804
4102
|
const writeRoot = resolveWriteRoot(workingDir);
|
|
3805
4103
|
await rm2(writeRoot, { recursive: true, force: true });
|
|
3806
|
-
await
|
|
4104
|
+
await mkdir5(writeRoot, { recursive: true });
|
|
3807
4105
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
3808
4106
|
const state = {
|
|
3809
4107
|
version: 1,
|
|
@@ -3819,7 +4117,7 @@ async function startFreshWriteSession(seed, workingDir = process.cwd()) {
|
|
|
3819
4117
|
lastCompletedStage: null,
|
|
3820
4118
|
failedStage: null,
|
|
3821
4119
|
errorMessage: null,
|
|
3822
|
-
|
|
4120
|
+
contentPlan: null,
|
|
3823
4121
|
plan: null,
|
|
3824
4122
|
text: null,
|
|
3825
4123
|
imagePrompts: null,
|
|
@@ -3836,7 +4134,18 @@ async function loadWriteSession(workingDir = process.cwd()) {
|
|
|
3836
4134
|
const raw = await readFile5(statePath, "utf8");
|
|
3837
4135
|
return writeSessionStateSchema.parse(JSON.parse(raw));
|
|
3838
4136
|
} catch (error) {
|
|
3839
|
-
if (
|
|
4137
|
+
if (!isNotFoundError2(error)) {
|
|
4138
|
+
throw error;
|
|
4139
|
+
}
|
|
4140
|
+
}
|
|
4141
|
+
const legacyPath = resolveLegacyStatePath(workingDir);
|
|
4142
|
+
try {
|
|
4143
|
+
const raw = await readFile5(legacyPath, "utf8");
|
|
4144
|
+
const state = writeSessionStateSchema.parse(JSON.parse(raw));
|
|
4145
|
+
await saveWriteSession(state, workingDir);
|
|
4146
|
+
return state;
|
|
4147
|
+
} catch (error) {
|
|
4148
|
+
if (isNotFoundError2(error)) {
|
|
3840
4149
|
return null;
|
|
3841
4150
|
}
|
|
3842
4151
|
throw error;
|
|
@@ -3848,7 +4157,7 @@ async function saveWriteSession(state, workingDir = process.cwd()) {
|
|
|
3848
4157
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3849
4158
|
});
|
|
3850
4159
|
const statePath = resolveStateFilePath(workingDir);
|
|
3851
|
-
await
|
|
4160
|
+
await mkdir5(path7.dirname(statePath), { recursive: true });
|
|
3852
4161
|
await writeFile5(statePath, `${JSON.stringify(next, null, 2)}
|
|
3853
4162
|
`, "utf8");
|
|
3854
4163
|
return next;
|
|
@@ -3856,7 +4165,7 @@ async function saveWriteSession(state, workingDir = process.cwd()) {
|
|
|
3856
4165
|
async function patchWriteSession(patch, workingDir = process.cwd()) {
|
|
3857
4166
|
const existing = await loadWriteSession(workingDir);
|
|
3858
4167
|
if (!existing) {
|
|
3859
|
-
throw new Error("No active write session found
|
|
4168
|
+
throw new Error("No active write session found. Start a fresh write first.");
|
|
3860
4169
|
}
|
|
3861
4170
|
const has = (key) => Object.hasOwn(patch, key);
|
|
3862
4171
|
const merged = {
|
|
@@ -3866,7 +4175,7 @@ async function patchWriteSession(patch, workingDir = process.cwd()) {
|
|
|
3866
4175
|
lastCompletedStage: has("lastCompletedStage") ? patch.lastCompletedStage ?? null : existing.lastCompletedStage,
|
|
3867
4176
|
failedStage: has("failedStage") ? patch.failedStage ?? null : existing.failedStage,
|
|
3868
4177
|
errorMessage: has("errorMessage") ? patch.errorMessage ?? null : existing.errorMessage,
|
|
3869
|
-
|
|
4178
|
+
contentPlan: has("contentPlan") ? patch.contentPlan ?? null : existing.contentPlan,
|
|
3870
4179
|
plan: has("plan") ? patch.plan ?? null : existing.plan,
|
|
3871
4180
|
text: has("text") ? patch.text ?? null : existing.text,
|
|
3872
4181
|
imagePrompts: has("imagePrompts") ? patch.imagePrompts ?? null : existing.imagePrompts,
|
|
@@ -3876,34 +4185,30 @@ async function patchWriteSession(patch, workingDir = process.cwd()) {
|
|
|
3876
4185
|
};
|
|
3877
4186
|
return saveWriteSession(merged, workingDir);
|
|
3878
4187
|
}
|
|
3879
|
-
function
|
|
4188
|
+
function isNotFoundError2(error) {
|
|
3880
4189
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
3881
4190
|
}
|
|
3882
4191
|
|
|
3883
4192
|
// src/pipeline/runner.ts
|
|
3884
|
-
function createInitialStages(
|
|
3885
|
-
const planningTitle = options.isArticlePrimary ? "Planning Primary Article" : "Planning Primary Content";
|
|
3886
|
-
const planningDetail = options.isArticlePrimary ? "Generating title, slug, section plan, and image slots." : "Defining the primary angle and output intent.";
|
|
3887
|
-
const sectionsTitle = options.isArticlePrimary ? "Writing Sections" : "Generating Primary Content";
|
|
3888
|
-
const sectionsDetail = options.isArticlePrimary ? "Waiting for the approved article plan." : "Waiting for primary content generation to begin.";
|
|
4193
|
+
function createInitialStages() {
|
|
3889
4194
|
return [
|
|
3890
4195
|
{
|
|
3891
|
-
id: "shared-
|
|
3892
|
-
title: "Planning Shared
|
|
4196
|
+
id: "shared-plan",
|
|
4197
|
+
title: "Planning Shared Plan",
|
|
3893
4198
|
status: "running",
|
|
3894
4199
|
detail: "Generating explicit cross-channel content guidance."
|
|
3895
4200
|
},
|
|
3896
4201
|
{
|
|
3897
4202
|
id: "planning",
|
|
3898
|
-
title:
|
|
4203
|
+
title: "Planning Primary Content",
|
|
3899
4204
|
status: "pending",
|
|
3900
|
-
detail:
|
|
4205
|
+
detail: "Generating title, slug, and content plan for the primary output."
|
|
3901
4206
|
},
|
|
3902
4207
|
{
|
|
3903
4208
|
id: "sections",
|
|
3904
|
-
title:
|
|
4209
|
+
title: "Writing Primary Content",
|
|
3905
4210
|
status: "pending",
|
|
3906
|
-
detail:
|
|
4211
|
+
detail: "Waiting for the approved primary plan."
|
|
3907
4212
|
},
|
|
3908
4213
|
{
|
|
3909
4214
|
id: "image-prompts",
|
|
@@ -3937,8 +4242,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
3937
4242
|
const runId = randomUUID();
|
|
3938
4243
|
const primaryTarget = getPrimaryTarget(input.config.settings.contentTargets);
|
|
3939
4244
|
const secondaryTargets = getSecondaryTargets(input.config.settings.contentTargets);
|
|
3940
|
-
const
|
|
3941
|
-
const stages = createInitialStages({ isArticlePrimary });
|
|
4245
|
+
const stages = createInitialStages();
|
|
3942
4246
|
options.onUpdate?.(cloneStages(stages));
|
|
3943
4247
|
const dryRun = options.dryRun ?? false;
|
|
3944
4248
|
const shouldEnrichLinks = options.enrichLinks ?? false;
|
|
@@ -3947,12 +4251,11 @@ async function runPipelineShell(input, options = {}) {
|
|
|
3947
4251
|
const pipelineCustomLinkRaws = options.customLinks ?? [];
|
|
3948
4252
|
const pipelineUnlinks = options.unlinks ?? [];
|
|
3949
4253
|
const pipelineMaxLinks = options.maxLinks;
|
|
3950
|
-
const outputPaths = resolveOutputPaths(
|
|
3951
|
-
const hasArticlePrimary = isArticlePrimary;
|
|
4254
|
+
const outputPaths = resolveOutputPaths();
|
|
3952
4255
|
const stageTracking = /* @__PURE__ */ new Map();
|
|
3953
4256
|
const stageRetryState = /* @__PURE__ */ new Map();
|
|
3954
4257
|
const llmOperationRetryState = /* @__PURE__ */ new Map();
|
|
3955
|
-
stageTracking.set("shared-
|
|
4258
|
+
stageTracking.set("shared-plan", {
|
|
3956
4259
|
startedAtMs: runStartedAtMs,
|
|
3957
4260
|
endedAtMs: null,
|
|
3958
4261
|
retries: 0,
|
|
@@ -4016,7 +4319,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4016
4319
|
} else {
|
|
4017
4320
|
const existing = await loadWriteSession(workingDir);
|
|
4018
4321
|
if (!existing) {
|
|
4019
|
-
throw new Error("No resumable write session found
|
|
4322
|
+
throw new Error("No resumable write session found. Start a fresh write first.");
|
|
4020
4323
|
}
|
|
4021
4324
|
if (existing.status === "completed") {
|
|
4022
4325
|
}
|
|
@@ -4032,24 +4335,24 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4032
4335
|
replicateApiKey: requireSecret(input.config.secrets.replicateApiToken, "Replicate API token"),
|
|
4033
4336
|
openrouterModel: input.config.settings.model
|
|
4034
4337
|
});
|
|
4035
|
-
let
|
|
4338
|
+
let contentPlan = writeSession.contentPlan;
|
|
4036
4339
|
let plan = writeSession.plan;
|
|
4037
4340
|
let text = writeSession.text;
|
|
4038
4341
|
let imagePrompts = writeSession.imagePrompts ?? writeSession.imageArtifacts?.imagePrompts ?? null;
|
|
4039
4342
|
let imageArtifacts = writeSession.imageArtifacts;
|
|
4040
4343
|
let linksResult = writeSession.links;
|
|
4041
4344
|
let primaryMarkdownTemplate = null;
|
|
4042
|
-
if (
|
|
4043
|
-
markStageCompleted(stageTracking, "shared-
|
|
4345
|
+
if (contentPlan) {
|
|
4346
|
+
markStageCompleted(stageTracking, "shared-plan");
|
|
4044
4347
|
stages[0] = {
|
|
4045
4348
|
...stages[0],
|
|
4046
4349
|
status: "succeeded",
|
|
4047
|
-
detail: "Reused saved shared
|
|
4048
|
-
summary:
|
|
4049
|
-
stageAnalytics: snapshotStageAnalytics(stageTracking, "shared-
|
|
4350
|
+
detail: "Reused saved shared plan from cached session.",
|
|
4351
|
+
summary: contentPlan.title,
|
|
4352
|
+
stageAnalytics: snapshotStageAnalytics(stageTracking, "shared-plan")
|
|
4050
4353
|
};
|
|
4051
4354
|
} else {
|
|
4052
|
-
|
|
4355
|
+
contentPlan = await planContentPlan({
|
|
4053
4356
|
idea: input.idea,
|
|
4054
4357
|
targetAudienceHint: input.targetAudienceHint,
|
|
4055
4358
|
settings: input.config.settings,
|
|
@@ -4059,88 +4362,91 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4059
4362
|
onLlmInteraction(interaction);
|
|
4060
4363
|
},
|
|
4061
4364
|
onLlmMetrics(metrics) {
|
|
4062
|
-
recordLlmMetrics(stageTracking, "shared-
|
|
4365
|
+
recordLlmMetrics(stageTracking, "shared-plan", metrics);
|
|
4063
4366
|
}
|
|
4064
4367
|
});
|
|
4065
|
-
markStageCompleted(stageTracking, "shared-
|
|
4368
|
+
markStageCompleted(stageTracking, "shared-plan");
|
|
4066
4369
|
stages[0] = {
|
|
4067
4370
|
...stages[0],
|
|
4068
4371
|
status: "succeeded",
|
|
4069
|
-
detail: "Shared
|
|
4070
|
-
summary:
|
|
4071
|
-
stageAnalytics: snapshotStageAnalytics(stageTracking, "shared-
|
|
4372
|
+
detail: "Shared plan generated successfully.",
|
|
4373
|
+
summary: contentPlan.title,
|
|
4374
|
+
stageAnalytics: snapshotStageAnalytics(stageTracking, "shared-plan")
|
|
4072
4375
|
};
|
|
4073
4376
|
writeSession = await patchWriteSession(
|
|
4074
4377
|
{
|
|
4075
4378
|
status: "running",
|
|
4076
|
-
lastCompletedStage: "shared-
|
|
4379
|
+
lastCompletedStage: "shared-plan",
|
|
4077
4380
|
failedStage: null,
|
|
4078
4381
|
errorMessage: null,
|
|
4079
|
-
|
|
4382
|
+
contentPlan
|
|
4080
4383
|
},
|
|
4081
4384
|
workingDir
|
|
4082
4385
|
);
|
|
4083
4386
|
}
|
|
4084
|
-
|
|
4387
|
+
stages[1] = {
|
|
4388
|
+
...stages[1],
|
|
4389
|
+
status: "running",
|
|
4390
|
+
detail: `Planning primary ${primaryTarget.contentType} content.`
|
|
4391
|
+
};
|
|
4392
|
+
markStageStarted(stageTracking, "planning");
|
|
4393
|
+
options.onUpdate?.(cloneStages(stages));
|
|
4394
|
+
if (plan) {
|
|
4395
|
+
markStageCompleted(stageTracking, "planning");
|
|
4085
4396
|
stages[1] = {
|
|
4086
4397
|
...stages[1],
|
|
4087
|
-
status: "
|
|
4088
|
-
detail: "
|
|
4398
|
+
status: "succeeded",
|
|
4399
|
+
detail: "Reused saved plan from cached session.",
|
|
4400
|
+
summary: buildPlanSummary(plan),
|
|
4401
|
+
stageAnalytics: snapshotStageAnalytics(stageTracking, "planning")
|
|
4089
4402
|
};
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
markStageCompleted(stageTracking, "planning");
|
|
4094
|
-
stages[1] = {
|
|
4095
|
-
...stages[1],
|
|
4096
|
-
status: "succeeded",
|
|
4097
|
-
detail: "Reused saved plan from .ideon/write.",
|
|
4098
|
-
summary: `${plan.title} \u2022 ${plan.slug} \u2022 ${plan.sections.length} sections \u2022 ${plan.inlineImages.length + 1} images`,
|
|
4099
|
-
stageAnalytics: snapshotStageAnalytics(stageTracking, "planning")
|
|
4100
|
-
};
|
|
4101
|
-
} else {
|
|
4102
|
-
if (!contentBrief) {
|
|
4103
|
-
throw new Error("Shared content brief is missing for article planning stage.");
|
|
4104
|
-
}
|
|
4105
|
-
plan = await planArticle({
|
|
4106
|
-
idea: input.idea,
|
|
4107
|
-
contentBrief,
|
|
4108
|
-
settings: input.config.settings,
|
|
4109
|
-
markdownOutputDir: writeSession.outputPaths.markdownOutputDir,
|
|
4110
|
-
openRouter,
|
|
4111
|
-
dryRun,
|
|
4112
|
-
onInteraction(interaction) {
|
|
4113
|
-
onLlmInteraction(interaction);
|
|
4114
|
-
},
|
|
4115
|
-
onLlmMetrics(metrics) {
|
|
4116
|
-
recordLlmMetrics(stageTracking, "planning", metrics);
|
|
4117
|
-
}
|
|
4118
|
-
});
|
|
4119
|
-
markStageCompleted(stageTracking, "planning");
|
|
4120
|
-
stages[1] = {
|
|
4121
|
-
...stages[1],
|
|
4122
|
-
status: "succeeded",
|
|
4123
|
-
detail: "Plan generated successfully.",
|
|
4124
|
-
summary: `${plan.title} \u2022 ${plan.slug} \u2022 ${plan.sections.length} sections \u2022 ${plan.inlineImages.length + 1} images`,
|
|
4125
|
-
stageAnalytics: snapshotStageAnalytics(stageTracking, "planning")
|
|
4126
|
-
};
|
|
4127
|
-
writeSession = await patchWriteSession(
|
|
4128
|
-
{
|
|
4129
|
-
status: "running",
|
|
4130
|
-
lastCompletedStage: "planning",
|
|
4131
|
-
failedStage: null,
|
|
4132
|
-
errorMessage: null,
|
|
4133
|
-
contentBrief,
|
|
4134
|
-
plan
|
|
4135
|
-
},
|
|
4136
|
-
workingDir
|
|
4137
|
-
);
|
|
4403
|
+
} else {
|
|
4404
|
+
if (!contentPlan) {
|
|
4405
|
+
throw new Error("Shared content plan is missing for primary planning stage.");
|
|
4138
4406
|
}
|
|
4407
|
+
plan = await planPrimaryContent({
|
|
4408
|
+
idea: input.idea,
|
|
4409
|
+
contentType: primaryTarget.contentType,
|
|
4410
|
+
contentPlan,
|
|
4411
|
+
settings: input.config.settings,
|
|
4412
|
+
markdownOutputDir: writeSession.outputPaths.markdownOutputDir,
|
|
4413
|
+
openRouter,
|
|
4414
|
+
dryRun,
|
|
4415
|
+
onInteraction(interaction) {
|
|
4416
|
+
onLlmInteraction(interaction);
|
|
4417
|
+
},
|
|
4418
|
+
onLlmMetrics(metrics) {
|
|
4419
|
+
recordLlmMetrics(stageTracking, "planning", metrics);
|
|
4420
|
+
}
|
|
4421
|
+
});
|
|
4422
|
+
markStageCompleted(stageTracking, "planning");
|
|
4423
|
+
stages[1] = {
|
|
4424
|
+
...stages[1],
|
|
4425
|
+
status: "succeeded",
|
|
4426
|
+
detail: "Plan generated successfully.",
|
|
4427
|
+
summary: buildPlanSummary(plan),
|
|
4428
|
+
stageAnalytics: snapshotStageAnalytics(stageTracking, "planning")
|
|
4429
|
+
};
|
|
4430
|
+
writeSession = await patchWriteSession(
|
|
4431
|
+
{
|
|
4432
|
+
status: "running",
|
|
4433
|
+
lastCompletedStage: "planning",
|
|
4434
|
+
failedStage: null,
|
|
4435
|
+
errorMessage: null,
|
|
4436
|
+
contentPlan,
|
|
4437
|
+
plan
|
|
4438
|
+
},
|
|
4439
|
+
workingDir
|
|
4440
|
+
);
|
|
4441
|
+
}
|
|
4442
|
+
const isLongForm = isLongFormPlan(plan);
|
|
4443
|
+
if (isLongForm) {
|
|
4444
|
+
const longPlan = plan;
|
|
4139
4445
|
stages[2] = {
|
|
4140
4446
|
...stages[2],
|
|
4141
4447
|
status: "running",
|
|
4142
4448
|
detail: "Writing introduction.",
|
|
4143
|
-
items: buildSectionItems(
|
|
4449
|
+
items: buildSectionItems(longPlan.sections.map((section) => section.title))
|
|
4144
4450
|
};
|
|
4145
4451
|
markStageStarted(stageTracking, "sections");
|
|
4146
4452
|
options.onUpdate?.(cloneStages(stages));
|
|
@@ -4149,12 +4455,12 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4149
4455
|
stages[2] = {
|
|
4150
4456
|
...stages[2],
|
|
4151
4457
|
status: "succeeded",
|
|
4152
|
-
detail: "Reused saved section drafts from .
|
|
4458
|
+
detail: "Reused saved section drafts from cached session.",
|
|
4153
4459
|
summary: `Intro + ${text.sections.length} sections + conclusion`,
|
|
4154
4460
|
items: (stages[2].items ?? []).map((item) => ({
|
|
4155
4461
|
...item,
|
|
4156
4462
|
status: "succeeded",
|
|
4157
|
-
detail: "Reused saved section draft from .
|
|
4463
|
+
detail: "Reused saved section draft from cached session."
|
|
4158
4464
|
})),
|
|
4159
4465
|
stageAnalytics: snapshotStageAnalytics(stageTracking, "sections")
|
|
4160
4466
|
};
|
|
@@ -4168,7 +4474,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4168
4474
|
} else {
|
|
4169
4475
|
const sectionItemTracking = /* @__PURE__ */ new Map();
|
|
4170
4476
|
text = await writeArticleSections({
|
|
4171
|
-
plan,
|
|
4477
|
+
plan: longPlan,
|
|
4172
4478
|
settings: input.config.settings,
|
|
4173
4479
|
openRouter,
|
|
4174
4480
|
dryRun,
|
|
@@ -4268,7 +4574,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4268
4574
|
stages[3] = {
|
|
4269
4575
|
...stages[3],
|
|
4270
4576
|
status: "succeeded",
|
|
4271
|
-
detail: "Reused saved image prompts from .
|
|
4577
|
+
detail: "Reused saved image prompts from cached session.",
|
|
4272
4578
|
summary: `${imagePrompts.length} prompts ready`,
|
|
4273
4579
|
stageAnalytics: snapshotStageAnalytics(stageTracking, "image-prompts")
|
|
4274
4580
|
};
|
|
@@ -4281,8 +4587,8 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4281
4587
|
options.onUpdate?.(cloneStages(stages));
|
|
4282
4588
|
} else {
|
|
4283
4589
|
imagePrompts = await expandImagePrompts({
|
|
4284
|
-
slots: buildImageSlots(
|
|
4285
|
-
planContext:
|
|
4590
|
+
slots: buildImageSlots(longPlan, text.sections, { maxImages: options.maxImages }),
|
|
4591
|
+
planContext: longPlan,
|
|
4286
4592
|
sections: text.sections,
|
|
4287
4593
|
settings: input.config.settings,
|
|
4288
4594
|
openRouter,
|
|
@@ -4343,24 +4649,6 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4343
4649
|
);
|
|
4344
4650
|
}
|
|
4345
4651
|
} else {
|
|
4346
|
-
if (!contentBrief) {
|
|
4347
|
-
throw new Error("Shared content brief is missing for primary content planning stage.");
|
|
4348
|
-
}
|
|
4349
|
-
stages[1] = {
|
|
4350
|
-
...stages[1],
|
|
4351
|
-
status: "running",
|
|
4352
|
-
detail: `Defining primary direction for ${primaryTarget.contentType}.`
|
|
4353
|
-
};
|
|
4354
|
-
markStageStarted(stageTracking, "planning");
|
|
4355
|
-
options.onUpdate?.(cloneStages(stages));
|
|
4356
|
-
markStageCompleted(stageTracking, "planning");
|
|
4357
|
-
stages[1] = {
|
|
4358
|
-
...stages[1],
|
|
4359
|
-
status: "succeeded",
|
|
4360
|
-
detail: `Primary direction locked for ${primaryTarget.contentType}.`,
|
|
4361
|
-
summary: `Primary: ${primaryTarget.contentType}`,
|
|
4362
|
-
stageAnalytics: snapshotStageAnalytics(stageTracking, "planning")
|
|
4363
|
-
};
|
|
4364
4652
|
stages[2] = {
|
|
4365
4653
|
...stages[2],
|
|
4366
4654
|
status: "running",
|
|
@@ -4378,7 +4666,8 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4378
4666
|
outputIndex: 1,
|
|
4379
4667
|
outputCountForType: 1,
|
|
4380
4668
|
articleReferenceMarkdown: void 0,
|
|
4381
|
-
|
|
4669
|
+
contentPlan,
|
|
4670
|
+
plan,
|
|
4382
4671
|
settings: input.config.settings,
|
|
4383
4672
|
openRouter,
|
|
4384
4673
|
dryRun,
|
|
@@ -4404,7 +4693,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4404
4693
|
};
|
|
4405
4694
|
markStageStarted(stageTracking, "image-prompts");
|
|
4406
4695
|
options.onUpdate?.(cloneStages(stages));
|
|
4407
|
-
imagePrompts = [buildPrimaryCoverPrompt(
|
|
4696
|
+
imagePrompts = [buildPrimaryCoverPrompt(plan, contentPlan, primaryTarget.contentType)];
|
|
4408
4697
|
markStageCompleted(stageTracking, "image-prompts");
|
|
4409
4698
|
stages[3] = {
|
|
4410
4699
|
...stages[3],
|
|
@@ -4426,12 +4715,12 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4426
4715
|
};
|
|
4427
4716
|
options.onUpdate?.(cloneStages(stages));
|
|
4428
4717
|
}
|
|
4429
|
-
const baseSlug = plan?.slug ??
|
|
4718
|
+
const baseSlug = plan?.slug ?? resolveGenerationSlug(input.idea, contentPlan?.title);
|
|
4430
4719
|
const generationDir = path8.join(
|
|
4431
4720
|
writeSession.outputPaths.markdownOutputDir,
|
|
4432
4721
|
buildGenerationDirectoryName(baseSlug)
|
|
4433
4722
|
);
|
|
4434
|
-
await
|
|
4723
|
+
await mkdir6(generationDir, { recursive: true });
|
|
4435
4724
|
const jobDefinitionPath = path8.join(generationDir, "job.json");
|
|
4436
4725
|
await writeJsonFile(
|
|
4437
4726
|
jobDefinitionPath,
|
|
@@ -4451,13 +4740,14 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4451
4740
|
const primaryFilePrefix = toFilePrefix(primaryTarget.contentType);
|
|
4452
4741
|
const primaryMarkdownPath = path8.join(generationDir, `${primaryFilePrefix}-1.md`);
|
|
4453
4742
|
const sharedAssetDir = generationDir;
|
|
4454
|
-
if (
|
|
4743
|
+
if (isLongForm) {
|
|
4744
|
+
const longPlan = plan;
|
|
4455
4745
|
if (imageArtifacts) {
|
|
4456
4746
|
markStageCompleted(stageTracking, "images");
|
|
4457
4747
|
stages[4] = {
|
|
4458
4748
|
...stages[4],
|
|
4459
4749
|
status: "succeeded",
|
|
4460
|
-
detail: "Reused previously rendered images from .
|
|
4750
|
+
detail: "Reused previously rendered images from cached session.",
|
|
4461
4751
|
summary: sharedAssetDir,
|
|
4462
4752
|
stageAnalytics: snapshotStageAnalytics(stageTracking, "images")
|
|
4463
4753
|
};
|
|
@@ -4527,7 +4817,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4527
4817
|
throw new Error("Article generation requested but required article artifacts are missing.");
|
|
4528
4818
|
}
|
|
4529
4819
|
const article = {
|
|
4530
|
-
plan,
|
|
4820
|
+
plan: longPlan,
|
|
4531
4821
|
intro: text.intro,
|
|
4532
4822
|
sections: text.sections,
|
|
4533
4823
|
outro: text.outro,
|
|
@@ -4544,7 +4834,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4544
4834
|
stages[4] = {
|
|
4545
4835
|
...stages[4],
|
|
4546
4836
|
status: "succeeded",
|
|
4547
|
-
detail: "Reused previously rendered primary cover image from .
|
|
4837
|
+
detail: "Reused previously rendered primary cover image from cached session.",
|
|
4548
4838
|
summary: sharedAssetDir,
|
|
4549
4839
|
stageAnalytics: snapshotStageAnalytics(stageTracking, "images")
|
|
4550
4840
|
};
|
|
@@ -4612,11 +4902,11 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4612
4902
|
}
|
|
4613
4903
|
const coverImage = imageArtifacts?.renderedImages.find((image) => image.kind === "cover") ?? null;
|
|
4614
4904
|
if (coverImage) {
|
|
4615
|
-
primaryMarkdownTemplate = withCoverImage(primaryMarkdownTemplate, coverImage.relativePath, deriveTitleFromIdea2(input.idea));
|
|
4905
|
+
primaryMarkdownTemplate = withCoverImage(primaryMarkdownTemplate, coverImage.relativePath, plan.title || deriveTitleFromIdea2(input.idea));
|
|
4616
4906
|
}
|
|
4617
4907
|
primaryMarkdownTemplate = applyPrimaryTitleHeading(
|
|
4618
4908
|
primaryMarkdownTemplate,
|
|
4619
|
-
|
|
4909
|
+
plan.title || contentPlan.title || deriveTitleFromIdea2(input.idea)
|
|
4620
4910
|
);
|
|
4621
4911
|
}
|
|
4622
4912
|
const markdownPaths = [];
|
|
@@ -4646,8 +4936,8 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4646
4936
|
};
|
|
4647
4937
|
markStageStarted(stageTracking, "output");
|
|
4648
4938
|
options.onUpdate?.(cloneStages(stages));
|
|
4649
|
-
if (!
|
|
4650
|
-
throw new Error("Shared content
|
|
4939
|
+
if (!contentPlan) {
|
|
4940
|
+
throw new Error("Shared content plan is missing for output generation stage.");
|
|
4651
4941
|
}
|
|
4652
4942
|
for (const output of requestedOutputs) {
|
|
4653
4943
|
const itemId = toOutputItemId(output.filePrefix, output.index);
|
|
@@ -4682,7 +4972,8 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4682
4972
|
outputIndex: output.index,
|
|
4683
4973
|
outputCountForType: output.outputCountForType,
|
|
4684
4974
|
articleReferenceMarkdown: primaryMarkdownTemplate ?? void 0,
|
|
4685
|
-
|
|
4975
|
+
contentPlan,
|
|
4976
|
+
plan,
|
|
4686
4977
|
settings: input.config.settings,
|
|
4687
4978
|
openRouter,
|
|
4688
4979
|
dryRun,
|
|
@@ -4827,7 +5118,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4827
5118
|
stages[6] = {
|
|
4828
5119
|
...stages[6],
|
|
4829
5120
|
status: "succeeded",
|
|
4830
|
-
detail: "Reused saved link metadata from .
|
|
5121
|
+
detail: "Reused saved link metadata from cached session.",
|
|
4831
5122
|
summary: `${resumedLinks.reduce((sum, item) => sum + item.links.length, 0)} links`,
|
|
4832
5123
|
items: (stages[6].items ?? []).map((item) => ({
|
|
4833
5124
|
...item,
|
|
@@ -4841,8 +5132,8 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4841
5132
|
const itemTracking = /* @__PURE__ */ new Map();
|
|
4842
5133
|
linksResult = await enrichLinks({
|
|
4843
5134
|
markdownFiles: eligibleOutputsForLinks,
|
|
4844
|
-
articleTitle: plan?.title ??
|
|
4845
|
-
articleDescription: plan?.description ??
|
|
5135
|
+
articleTitle: plan?.title ?? contentPlan.title ?? deriveTitleFromIdea2(input.idea),
|
|
5136
|
+
articleDescription: plan?.description ?? contentPlan.description,
|
|
4846
5137
|
openRouter,
|
|
4847
5138
|
settings: input.config.settings,
|
|
4848
5139
|
dryRun,
|
|
@@ -4955,8 +5246,8 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4955
5246
|
await writeJsonFile(interactionsPath, interactions);
|
|
4956
5247
|
const primaryMarkdownPathForArtifact = markdownPaths[0] ?? primaryMarkdownPath;
|
|
4957
5248
|
const artifact = {
|
|
4958
|
-
title: plan?.title ??
|
|
4959
|
-
slug: plan?.slug ??
|
|
5249
|
+
title: plan?.title ?? contentPlan.title ?? deriveTitleFromIdea2(input.idea),
|
|
5250
|
+
slug: plan?.slug ?? resolveGenerationSlug(input.idea, contentPlan?.title),
|
|
4960
5251
|
sectionCount: text?.sections.length ?? 0,
|
|
4961
5252
|
imageCount: imageArtifacts?.renderedImages.length ?? 0,
|
|
4962
5253
|
outputCount: markdownPaths.length,
|
|
@@ -5065,7 +5356,7 @@ function buildRunAnalytics({
|
|
|
5065
5356
|
linkEnrichmentCalls
|
|
5066
5357
|
}) {
|
|
5067
5358
|
const runEndedAtMs = Date.now();
|
|
5068
|
-
const orderedStageIds = ["shared-
|
|
5359
|
+
const orderedStageIds = ["shared-plan", "planning", "sections", "image-prompts", "images", "output", "links"];
|
|
5069
5360
|
const stages = orderedStageIds.map((stageId) => {
|
|
5070
5361
|
const tracked = stageTracking.get(stageId);
|
|
5071
5362
|
const startedAtMs = tracked?.startedAtMs ?? runEndedAtMs;
|
|
@@ -5282,20 +5573,19 @@ function getSecondaryTargets(contentTargets) {
|
|
|
5282
5573
|
count: target.count
|
|
5283
5574
|
}));
|
|
5284
5575
|
}
|
|
5285
|
-
function buildPrimaryCoverPrompt(
|
|
5286
|
-
const markdownExcerpt = primaryMarkdown.replace(/\s+/g, " ").trim().slice(0, 240);
|
|
5576
|
+
function buildPrimaryCoverPrompt(plan, contentPlan, primaryContentType) {
|
|
5287
5577
|
return {
|
|
5288
5578
|
id: "cover",
|
|
5289
5579
|
kind: "cover",
|
|
5290
|
-
description:
|
|
5580
|
+
description: plan.coverImageDescription,
|
|
5291
5581
|
anchorAfterSection: null,
|
|
5292
5582
|
prompt: [
|
|
5293
|
-
|
|
5294
|
-
`
|
|
5295
|
-
`
|
|
5296
|
-
`
|
|
5297
|
-
`
|
|
5298
|
-
`
|
|
5583
|
+
plan.coverImageDescription,
|
|
5584
|
+
`Content type: ${primaryContentType}`,
|
|
5585
|
+
`Core angle: ${contentPlan.description}`,
|
|
5586
|
+
`Audience: ${contentPlan.targetAudience}`,
|
|
5587
|
+
`Promise: ${contentPlan.corePromise}`,
|
|
5588
|
+
`Voice: ${contentPlan.voiceNotes}`,
|
|
5299
5589
|
"Do not include any words, letters, numbers, logos, watermarks, or signage in the image."
|
|
5300
5590
|
].join(" ")
|
|
5301
5591
|
};
|
|
@@ -5329,8 +5619,18 @@ function deriveTitleFromIdea2(idea) {
|
|
|
5329
5619
|
}
|
|
5330
5620
|
return normalized.split(/\s+/).slice(0, 8).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
5331
5621
|
}
|
|
5332
|
-
function slugifyIdea(idea) {
|
|
5333
|
-
|
|
5622
|
+
function slugifyIdea(idea, maxLength) {
|
|
5623
|
+
const slug = idea.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "generated-content";
|
|
5624
|
+
if (maxLength !== void 0 && slug.length > maxLength) {
|
|
5625
|
+
return slug.slice(0, maxLength).replace(/-+$/, "");
|
|
5626
|
+
}
|
|
5627
|
+
return slug;
|
|
5628
|
+
}
|
|
5629
|
+
function resolveGenerationSlug(idea, planTitle) {
|
|
5630
|
+
if (planTitle) {
|
|
5631
|
+
return slugifyIdea(planTitle);
|
|
5632
|
+
}
|
|
5633
|
+
return slugifyIdea(idea, 80);
|
|
5334
5634
|
}
|
|
5335
5635
|
function buildRunJobDefinition(input) {
|
|
5336
5636
|
return {
|
|
@@ -5349,46 +5649,63 @@ function buildRunJobDefinition(input) {
|
|
|
5349
5649
|
};
|
|
5350
5650
|
}
|
|
5351
5651
|
function renderPlanMarkdown(plan) {
|
|
5352
|
-
const
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
${
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
${
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
## Sections
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
##
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5652
|
+
const lines = [
|
|
5653
|
+
`# ${plan.title}`,
|
|
5654
|
+
"",
|
|
5655
|
+
`**Content type:** ${plan.contentType}`,
|
|
5656
|
+
`**Slug:** ${plan.slug}`,
|
|
5657
|
+
"",
|
|
5658
|
+
"## Description",
|
|
5659
|
+
"",
|
|
5660
|
+
plan.description,
|
|
5661
|
+
""
|
|
5662
|
+
];
|
|
5663
|
+
if (plan.subtitle) {
|
|
5664
|
+
lines.push("## Subtitle", "", plan.subtitle, "");
|
|
5665
|
+
}
|
|
5666
|
+
if (plan.keywords && plan.keywords.length > 0) {
|
|
5667
|
+
lines.push("## Keywords", "", ...plan.keywords.map((kw) => `- ${kw}`), "");
|
|
5668
|
+
}
|
|
5669
|
+
if (plan.introBrief) {
|
|
5670
|
+
lines.push("## Introduction Brief", "", plan.introBrief, "");
|
|
5671
|
+
}
|
|
5672
|
+
if (plan.sections && plan.sections.length > 0) {
|
|
5673
|
+
lines.push("## Sections", "", "| # | Title | Description |", "|---|-------|-------------|");
|
|
5674
|
+
plan.sections.forEach((section, index) => {
|
|
5675
|
+
lines.push(`| ${index + 1} | ${section.title} | ${section.description} |`);
|
|
5676
|
+
});
|
|
5677
|
+
lines.push("");
|
|
5678
|
+
}
|
|
5679
|
+
if (plan.outroBrief) {
|
|
5680
|
+
lines.push("## Outro Brief", "", plan.outroBrief, "");
|
|
5681
|
+
}
|
|
5682
|
+
if (plan.angle) {
|
|
5683
|
+
lines.push("## Angle", "", plan.angle, "");
|
|
5684
|
+
}
|
|
5685
|
+
lines.push("## Image Plan", "");
|
|
5686
|
+
lines.push(`- **Cover:** ${plan.coverImageDescription}`);
|
|
5687
|
+
if (plan.inlineImages && plan.inlineImages.length > 0) {
|
|
5688
|
+
plan.inlineImages.forEach((img, index) => {
|
|
5689
|
+
lines.push(`- **Inline ${index + 1}:** ${img.description} (after section ${img.anchorAfterSection})`);
|
|
5690
|
+
});
|
|
5691
|
+
}
|
|
5692
|
+
lines.push("");
|
|
5693
|
+
return lines.join("\n");
|
|
5694
|
+
}
|
|
5695
|
+
function buildPlanSummary(plan) {
|
|
5696
|
+
const parts = [plan.title, plan.slug];
|
|
5697
|
+
if (plan.sections && plan.sections.length > 0) {
|
|
5698
|
+
parts.push(`${plan.sections.length} sections`);
|
|
5699
|
+
}
|
|
5700
|
+
if (plan.inlineImages && plan.inlineImages.length > 0) {
|
|
5701
|
+
parts.push(`${plan.inlineImages.length + 1} images`);
|
|
5702
|
+
} else {
|
|
5703
|
+
parts.push("1 image");
|
|
5704
|
+
}
|
|
5705
|
+
return parts.join(" \u2022 ");
|
|
5389
5706
|
}
|
|
5390
5707
|
function asWriteStageId(stageId) {
|
|
5391
|
-
if (stageId === "shared-
|
|
5708
|
+
if (stageId === "shared-plan" || stageId === "planning" || stageId === "sections" || stageId === "image-prompts" || stageId === "images" || stageId === "output" || stageId === "links") {
|
|
5392
5709
|
return stageId;
|
|
5393
5710
|
}
|
|
5394
5711
|
return null;
|
|
@@ -5423,7 +5740,7 @@ async function runLinksCommand(options, dependencies = {}) {
|
|
|
5423
5740
|
const resolved = await resolveRunInput({
|
|
5424
5741
|
idea: `Enrich links for ${slug}`
|
|
5425
5742
|
});
|
|
5426
|
-
const markdownPath = await resolveMarkdownPathForSlug2(
|
|
5743
|
+
const markdownPath = await resolveMarkdownPathForSlug2(slug, cwd2);
|
|
5427
5744
|
const frontmatter = await readFrontmatter(markdownPath);
|
|
5428
5745
|
const fileId = path9.parse(markdownPath).name;
|
|
5429
5746
|
const articleTitle = frontmatter.title ?? toTitleFromSlug(slug);
|
|
@@ -5493,8 +5810,8 @@ function normalizeSlug2(rawSlug) {
|
|
|
5493
5810
|
}
|
|
5494
5811
|
return slug;
|
|
5495
5812
|
}
|
|
5496
|
-
async function resolveMarkdownPathForSlug2(
|
|
5497
|
-
const outputPaths = resolveOutputPaths(
|
|
5813
|
+
async function resolveMarkdownPathForSlug2(slug, cwd2) {
|
|
5814
|
+
const outputPaths = resolveOutputPaths();
|
|
5498
5815
|
const directPath = path9.join(outputPaths.markdownOutputDir, `${slug}.md`);
|
|
5499
5816
|
if (await isReadableFile(directPath)) {
|
|
5500
5817
|
return directPath;
|
|
@@ -5675,7 +5992,7 @@ function resolveCustomLinks(existing, addRaw, removeExpressions) {
|
|
|
5675
5992
|
}
|
|
5676
5993
|
|
|
5677
5994
|
// src/cli/commands/export.ts
|
|
5678
|
-
import { copyFile, mkdir as
|
|
5995
|
+
import { copyFile as copyFile2, mkdir as mkdir7, readFile as readFile8, stat as stat5, writeFile as writeFile6 } from "fs/promises";
|
|
5679
5996
|
import path11 from "path";
|
|
5680
5997
|
|
|
5681
5998
|
// src/output/enrichMarkdownWithLinks.ts
|
|
@@ -6002,7 +6319,7 @@ async function runOutputCommand(options, dependencies = {}) {
|
|
|
6002
6319
|
const log = dependencies.log ?? ((message) => console.log(message));
|
|
6003
6320
|
const targetIndex = options.index ?? 1;
|
|
6004
6321
|
const resolved = await resolveRunInput({ idea: `Export generation ${options.generationId}` });
|
|
6005
|
-
const outputPaths = resolveOutputPaths(
|
|
6322
|
+
const outputPaths = resolveOutputPaths();
|
|
6006
6323
|
const generations = await listAllGenerations(outputPaths.markdownOutputDir);
|
|
6007
6324
|
const generation = resolveGeneration(generations, options.generationId);
|
|
6008
6325
|
const articleOutputs = generation.outputs.filter((output) => output.contentType === generation.primaryContentType);
|
|
@@ -6029,7 +6346,7 @@ async function runOutputCommand(options, dependencies = {}) {
|
|
|
6029
6346
|
`Export file already exists: ${destinationFilePath}. Pass --overwrite to replace it.`
|
|
6030
6347
|
);
|
|
6031
6348
|
}
|
|
6032
|
-
await
|
|
6349
|
+
await mkdir7(destinationDir, { recursive: true });
|
|
6033
6350
|
const links = await loadLinks(sourceMarkdownPath);
|
|
6034
6351
|
const enrichedMarkdown = enrichWithFrontmatterGuard(sourceMarkdown, links);
|
|
6035
6352
|
const sourceDir = path11.dirname(sourceMarkdownPath);
|
|
@@ -6049,8 +6366,8 @@ async function runOutputCommand(options, dependencies = {}) {
|
|
|
6049
6366
|
throw new ReportedError(`Referenced image path is not a file: ${absoluteImageSrc}.`);
|
|
6050
6367
|
}
|
|
6051
6368
|
const destImagePath = path11.join(destinationDir, relImagePath);
|
|
6052
|
-
await
|
|
6053
|
-
await
|
|
6369
|
+
await mkdir7(path11.dirname(destImagePath), { recursive: true });
|
|
6370
|
+
await copyFile2(absoluteImageSrc, destImagePath);
|
|
6054
6371
|
copiedImages.push(relImagePath);
|
|
6055
6372
|
}
|
|
6056
6373
|
await writeFile6(destinationFilePath, enrichedMarkdown, "utf8");
|
|
@@ -6301,7 +6618,7 @@ async function startIdeonMcpServer() {
|
|
|
6301
6618
|
try {
|
|
6302
6619
|
const session = await loadWriteSession(cwd());
|
|
6303
6620
|
if (!session) {
|
|
6304
|
-
throw new ReportedError("No resumable write session found
|
|
6621
|
+
throw new ReportedError("No resumable write session found.");
|
|
6305
6622
|
}
|
|
6306
6623
|
if (session.status === "completed") {
|
|
6307
6624
|
throw new ReportedError("The last write session already completed.");
|
|
@@ -6611,12 +6928,6 @@ import { Box, Text, useApp, useInput } from "ink";
|
|
|
6611
6928
|
import SelectInput from "ink-select-input";
|
|
6612
6929
|
import TextInput from "ink-text-input";
|
|
6613
6930
|
|
|
6614
|
-
// src/images/limnModelCatalog.ts
|
|
6615
|
-
import { getSupportedModelCatalog } from "@telepat/limn";
|
|
6616
|
-
function getLimnGenerationModels() {
|
|
6617
|
-
return getSupportedModelCatalog().filter((entry) => entry.generationEnabled);
|
|
6618
|
-
}
|
|
6619
|
-
|
|
6620
6931
|
// src/cli/flows/settingsFlowLogic.ts
|
|
6621
6932
|
function handleMenuSelect(action, settings, secrets, setEditing, setShowModelSelect, setMenuMode, onDone, exit) {
|
|
6622
6933
|
switch (action) {
|
|
@@ -6641,12 +6952,6 @@ function handleMenuSelect(action, settings, secrets, setEditing, setShowModelSel
|
|
|
6641
6952
|
case "topP":
|
|
6642
6953
|
setEditing({ key: action, label: "Top p", value: String(settings.modelSettings.topP) });
|
|
6643
6954
|
return;
|
|
6644
|
-
case "markdownOutputDir":
|
|
6645
|
-
setEditing({ key: action, label: "Markdown output directory", value: settings.markdownOutputDir });
|
|
6646
|
-
return;
|
|
6647
|
-
case "assetOutputDir":
|
|
6648
|
-
setEditing({ key: action, label: "Asset output directory", value: settings.assetOutputDir });
|
|
6649
|
-
return;
|
|
6650
6955
|
case "t2i-settings":
|
|
6651
6956
|
setMenuMode("t2i");
|
|
6652
6957
|
return;
|
|
@@ -6660,6 +6965,13 @@ function handleMenuSelect(action, settings, secrets, setEditing, setShowModelSel
|
|
|
6660
6965
|
value: JSON.stringify(settings.t2i.inputOverrides, null, 2)
|
|
6661
6966
|
});
|
|
6662
6967
|
return;
|
|
6968
|
+
case "t2i-replicate-model-id":
|
|
6969
|
+
setEditing({
|
|
6970
|
+
key: action,
|
|
6971
|
+
label: "T2I Replicate model ID override (blank to clear)",
|
|
6972
|
+
value: settings.t2i.replicateModelId ?? ""
|
|
6973
|
+
});
|
|
6974
|
+
return;
|
|
6663
6975
|
case "t2i-back":
|
|
6664
6976
|
setMenuMode("main");
|
|
6665
6977
|
return;
|
|
@@ -6730,14 +7042,6 @@ function applyEdit(action, value2, settings, secrets, setSettings, setSecrets) {
|
|
|
6730
7042
|
});
|
|
6731
7043
|
return true;
|
|
6732
7044
|
}
|
|
6733
|
-
if (action === "markdownOutputDir") {
|
|
6734
|
-
setSettings({ ...settings, markdownOutputDir: value2.trim() || settings.markdownOutputDir });
|
|
6735
|
-
return true;
|
|
6736
|
-
}
|
|
6737
|
-
if (action === "assetOutputDir") {
|
|
6738
|
-
setSettings({ ...settings, assetOutputDir: value2.trim() || settings.assetOutputDir });
|
|
6739
|
-
return true;
|
|
6740
|
-
}
|
|
6741
7045
|
if (action === "t2i-input-overrides") {
|
|
6742
7046
|
const trimmed = value2.trim();
|
|
6743
7047
|
if (trimmed.length === 0) {
|
|
@@ -6767,6 +7071,17 @@ function applyEdit(action, value2, settings, secrets, setSettings, setSecrets) {
|
|
|
6767
7071
|
return false;
|
|
6768
7072
|
}
|
|
6769
7073
|
}
|
|
7074
|
+
if (action === "t2i-replicate-model-id") {
|
|
7075
|
+
const trimmed = value2.trim();
|
|
7076
|
+
setSettings({
|
|
7077
|
+
...settings,
|
|
7078
|
+
t2i: {
|
|
7079
|
+
...settings.t2i,
|
|
7080
|
+
replicateModelId: trimmed.length > 0 ? trimmed : void 0
|
|
7081
|
+
}
|
|
7082
|
+
});
|
|
7083
|
+
return true;
|
|
7084
|
+
}
|
|
6770
7085
|
return false;
|
|
6771
7086
|
}
|
|
6772
7087
|
function parseNumberOrFallback(value2, fallback) {
|
|
@@ -6821,12 +7136,19 @@ function SettingsFlow({ initialSettings, initialSecrets, onDone }) {
|
|
|
6821
7136
|
const count = Object.keys(overrides).length;
|
|
6822
7137
|
return count === 0 ? "none" : `${count} override${count === 1 ? "" : "s"}`;
|
|
6823
7138
|
};
|
|
7139
|
+
const formatReplicateOverrideSummary = (replicateModelId) => {
|
|
7140
|
+
return replicateModelId && replicateModelId.length > 0 ? replicateModelId : "auto";
|
|
7141
|
+
};
|
|
6824
7142
|
const menuItems = useMemo(() => {
|
|
6825
7143
|
const t2iSubmenu = [
|
|
6826
7144
|
{
|
|
6827
7145
|
label: `T2I model: ${currentModelEntry?.displayName ?? settings.t2i.modelId}`,
|
|
6828
7146
|
value: "t2i-model"
|
|
6829
7147
|
},
|
|
7148
|
+
{
|
|
7149
|
+
label: `T2I Replicate model override: ${formatReplicateOverrideSummary(settings.t2i.replicateModelId)}`,
|
|
7150
|
+
value: "t2i-replicate-model-id"
|
|
7151
|
+
},
|
|
6830
7152
|
{
|
|
6831
7153
|
label: `T2I input overrides: ${formatT2iOverridesSummary(settings.t2i.inputOverrides)}`,
|
|
6832
7154
|
value: "t2i-input-overrides"
|
|
@@ -6868,14 +7190,6 @@ function SettingsFlow({ initialSettings, initialSecrets, onDone }) {
|
|
|
6868
7190
|
label: `Top p: ${settings.modelSettings.topP}`,
|
|
6869
7191
|
value: "topP"
|
|
6870
7192
|
},
|
|
6871
|
-
{
|
|
6872
|
-
label: `Markdown output directory: ${settings.markdownOutputDir}`,
|
|
6873
|
-
value: "markdownOutputDir"
|
|
6874
|
-
},
|
|
6875
|
-
{
|
|
6876
|
-
label: `Asset output directory: ${settings.assetOutputDir}`,
|
|
6877
|
-
value: "assetOutputDir"
|
|
6878
|
-
},
|
|
6879
7193
|
{
|
|
6880
7194
|
label: `T2I settings: ${currentModelEntry?.displayName ?? settings.t2i.modelId}`,
|
|
6881
7195
|
value: "t2i-settings"
|
|
@@ -6907,6 +7221,7 @@ function SettingsFlow({ initialSettings, initialSecrets, onDone }) {
|
|
|
6907
7221
|
...current,
|
|
6908
7222
|
t2i: {
|
|
6909
7223
|
modelId: item.value,
|
|
7224
|
+
replicateModelId: current.t2i.replicateModelId && isReplicateModelIdForFamily(item.value, current.t2i.replicateModelId) ? current.t2i.replicateModelId : void 0,
|
|
6910
7225
|
inputOverrides: {}
|
|
6911
7226
|
}
|
|
6912
7227
|
}));
|
|
@@ -8312,7 +8627,7 @@ function renderShell({
|
|
|
8312
8627
|
const articleListElement = document.getElementById('articleList');
|
|
8313
8628
|
const themeToggleButton = document.getElementById('themeToggle');
|
|
8314
8629
|
const typeOrder = ['article', 'blog-post', 'x-thread', 'x-post', 'linkedin-post', 'reddit-post', 'newsletter'];
|
|
8315
|
-
const stageOrder = ['shared-
|
|
8630
|
+
const stageOrder = ['shared-plan', 'planning', 'sections', 'image-prompts', 'images', 'output', 'links'];
|
|
8316
8631
|
|
|
8317
8632
|
let currentGeneration = null;
|
|
8318
8633
|
let activeType = '';
|
|
@@ -8889,13 +9204,7 @@ function escapeHtml(value2) {
|
|
|
8889
9204
|
|
|
8890
9205
|
// src/cli/commands/serve.ts
|
|
8891
9206
|
async function runServeCommand(options) {
|
|
8892
|
-
const
|
|
8893
|
-
const mergedSettings = appSettingsSchema.parse({
|
|
8894
|
-
...savedSettings,
|
|
8895
|
-
...envSettings.markdownOutputDir ? { markdownOutputDir: envSettings.markdownOutputDir } : {},
|
|
8896
|
-
...envSettings.assetOutputDir ? { assetOutputDir: envSettings.assetOutputDir } : {}
|
|
8897
|
-
});
|
|
8898
|
-
const outputPaths = resolveOutputPaths(mergedSettings);
|
|
9207
|
+
const outputPaths = resolveOutputPaths();
|
|
8899
9208
|
const markdownPath = await resolveMarkdownPath(options.markdownPath, outputPaths.markdownOutputDir, process.cwd());
|
|
8900
9209
|
const port = parsePort(options.port);
|
|
8901
9210
|
if (options.watch) {
|
|
@@ -8974,7 +9283,7 @@ function formatStageCost(costUsd, costSource) {
|
|
|
8974
9283
|
return costSource === "estimated" ? `~${formatted}` : formatted;
|
|
8975
9284
|
}
|
|
8976
9285
|
function formatStageId(stageId) {
|
|
8977
|
-
if (stageId === "shared-
|
|
9286
|
+
if (stageId === "shared-plan") return "shared-plan";
|
|
8978
9287
|
if (stageId === "planning") return "planning";
|
|
8979
9288
|
if (stageId === "sections") return "sections";
|
|
8980
9289
|
if (stageId === "image-prompts") return "image-prompts";
|
|
@@ -9796,9 +10105,7 @@ function WriteApp({
|
|
|
9796
10105
|
}) {
|
|
9797
10106
|
const { exit } = useApp3();
|
|
9798
10107
|
const [stages, setStages] = useState4(
|
|
9799
|
-
() => createInitialStages(
|
|
9800
|
-
isArticlePrimary: input.config.settings.contentTargets.some((target) => target.role === "primary" && target.contentType === "article")
|
|
9801
|
-
})
|
|
10108
|
+
() => createInitialStages()
|
|
9802
10109
|
);
|
|
9803
10110
|
const [result, setResult] = useState4(null);
|
|
9804
10111
|
const [errorMessage, setErrorMessage] = useState4(null);
|
|
@@ -9872,7 +10179,7 @@ async function runWriteCommand(options) {
|
|
|
9872
10179
|
async function runWriteResumeCommand(options = {}) {
|
|
9873
10180
|
const session = await loadWriteSession();
|
|
9874
10181
|
if (!session) {
|
|
9875
|
-
throw new ReportedError("No resumable write session found
|
|
10182
|
+
throw new ReportedError("No resumable write session found. Run ideon write <idea> first.");
|
|
9876
10183
|
}
|
|
9877
10184
|
if (session.status === "completed") {
|
|
9878
10185
|
throw new ReportedError("The last write session already completed. Run ideon write <idea> to start fresh.");
|
|
@@ -10169,7 +10476,7 @@ async function runCli(argv) {
|
|
|
10169
10476
|
watch: options.watch
|
|
10170
10477
|
});
|
|
10171
10478
|
});
|
|
10172
|
-
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-
|
|
10479
|
+
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-plan targeting").option("-j, --job <path>", "Path to a JSON job definition").option("--primary <type=count>", "Required primary output target (for example: article=1 or x-post=1)").option("--secondary <type=count>", "Secondary output target, repeatable (for example: x-thread=3, linkedin-post=2)", collectOptionValue).option("--style <style>", "Writing style (academic, analytical, authoritative, conversational, empathetic, friendly, journalistic, minimalist, persuasive, playful, professional, storytelling, technical)").option("--intent <intent>", "Content intent (announcement, case-study, cornerstone, counterargument, critique-review, deep-dive-analysis, how-to-guide, interview-q-and-a, listicle, opinion-piece, personal-essay, roundup-curation, tutorial)").option("--length <size>", "Target length: small, medium, large, or a positive integer word count").option("--no-interactive", "Fail instead of prompting for missing input in TTY mode").option("--dry-run", "Run the pipeline shell without external API calls", false).option("--enrich-links", "Run link enrichment after markdown generation", false).option("--link <pair>", 'Custom link "expression->url", repeatable', collectOptionValue).option("--unlink <expression>", "Remove a custom link by expression, repeatable", collectOptionValue).option("--max-links <n>", "Max number of generated links", (v) => Number.parseInt(v, 10)).option("--max-images <n>", "Max total images including cover (1=cover only, 2=cover+1 inline, 3=cover+2 inline)", (v) => Number.parseInt(v, 10)).action(async (ideaArg, options) => {
|
|
10173
10480
|
await runWriteCommand({
|
|
10174
10481
|
idea: options.idea ?? ideaArg,
|
|
10175
10482
|
audience: options.audience,
|
|
@@ -10188,7 +10495,7 @@ async function runCli(argv) {
|
|
|
10188
10495
|
maxImages: options.maxImages
|
|
10189
10496
|
});
|
|
10190
10497
|
});
|
|
10191
|
-
writeCommand.command("resume").description("Resume the last failed or interrupted write session
|
|
10498
|
+
writeCommand.command("resume").description("Resume the last failed or interrupted write session.").option("--no-interactive", "Force plain non-interactive output even in TTY mode", false).option("--enrich-links", "Run link enrichment after markdown generation", false).option("--link <pair>", 'Custom link "expression->url", repeatable', collectOptionValue).option("--unlink <expression>", "Remove a custom link by expression, repeatable", collectOptionValue).option("--max-links <n>", "Max number of generated links", (v) => Number.parseInt(v, 10)).option("--max-images <n>", "Max total images including cover (1=cover only, 2=cover+1 inline, 3=cover+2 inline)", (v) => Number.parseInt(v, 10)).action(async (options) => {
|
|
10192
10499
|
await runWriteResumeCommand({
|
|
10193
10500
|
noInteractive: options.noInteractive,
|
|
10194
10501
|
enrichLinks: options.enrichLinks,
|