@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 path3 from "path";
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 as mkdir2, writeFile as writeFile2 } from "fs/promises";
228
- import path2 from "path";
229
- function resolveOutputPaths(settings, cwd2 = process.cwd()) {
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: resolveConfiguredDir(settings.markdownOutputDir, cwd2),
232
- assetOutputDir: resolveConfiguredDir(settings.assetOutputDir, cwd2)
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
- mkdir2(paths.markdownOutputDir, { recursive: true }),
238
- mkdir2(paths.assetOutputDir, { recursive: true })
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(path2.join(markdownOutputDir, `${candidate}.md`))) {
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 = path2.join(current, entry.name);
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 mkdir2(path2.dirname(filePath), { recursive: true });
296
- await writeFile2(filePath, content, "utf8");
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 = path2.parse(markdownPath);
304
- return path2.join(parsed.dir, `${parsed.name}.links.json`);
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 = path2.parse(markdownPath);
311
- return path2.join(parsed.dir, `${parsed.name}.analytics.json`);
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 path2.relative(path2.dirname(markdownPath), assetPath).split(path2.sep).join("/");
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 [savedSettings, envSettings] = await Promise.all([loadSavedSettings(), Promise.resolve(readEnvSettings())]);
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: path3.dirname(markdownPath)
159
+ assetDir: path2.dirname(markdownPath)
389
160
  };
390
161
  }
391
162
  async function resolveMarkdownPathForSlug(markdownOutputDir, slug) {
392
- const directPath = path3.join(markdownOutputDir, `${slug}.md`);
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 = path3.join(current, entry.name);
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 = path3.relative(cwd2, targetPath);
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 mkdir3, readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
521
- import path4 from "path";
522
- import envPaths2 from "env-paths";
523
- import { z as z2 } from "zod";
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 = z2.object({
526
- runtime: z2.enum(supportedAgentRuntimeValues),
527
- installedAt: z2.string(),
528
- updatedAt: z2.string()
296
+ var integrationEntrySchema = z.object({
297
+ runtime: z.enum(supportedAgentRuntimeValues),
298
+ installedAt: z.string(),
299
+ updatedAt: z.string()
529
300
  });
530
- var integrationStoreSchema = z2.object({
531
- version: z2.literal(1),
532
- integrations: z2.record(z2.string(), integrationEntrySchema).default({})
301
+ var integrationStoreSchema = z.object({
302
+ version: z.literal(1),
303
+ integrations: z.record(z.string(), integrationEntrySchema).default({})
533
304
  });
534
- var ideonPaths2 = envPaths2("ideon", { suffix: "" });
535
- var storeDir = ideonPaths2.config;
536
- var storePath = path4.join(storeDir, "agent-integrations.json");
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 readFile2(targetStorePath, "utf8");
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 mkdir3(path4.dirname(targetStorePath), { recursive: true });
592
- await writeFile3(targetStorePath, `${JSON.stringify(store, null, 2)}
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
- "markdownOutputDir",
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
- markdownOutputDir: settings.markdownOutputDir,
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 "markdownOutputDir":
920
- return settings.markdownOutputDir;
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 "markdownOutputDir":
948
- return { ...settings, markdownOutputDir: value2 };
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.20",
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 mkdir5, stat as stat2 } from "fs/promises";
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 brief, and adapt structure per channel without duplicating article-only scaffolding.`;
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 buildArticlePlanGuideInstruction(intent, contentType) {
1995
- return buildGuideBundle([
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 buildContentBriefGuideInstruction(intent, primaryContentType, secondaryContentTypes) {
2099
+ function buildContentPlanGuideInstruction(intent, primaryContentType, secondaryContentTypes) {
2016
2100
  return buildGuideBundle([
2017
- "writing-guide/references/multi-channel-brief-strategy.md",
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/contentBrief.ts
2038
- var contentBriefSchema = {
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 buildContentBriefMessages(idea, options) {
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 brief that can guide all requested content types in this run.",
2078
- buildContentBriefGuideInstruction(options.intent, options.primaryContentType, options.secondaryContentTypes),
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 brief must be specific, concrete, and directly usable by writers without extra clarification.",
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 brief from this idea:",
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/contentBriefSchema.ts
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 contentBriefSchema2 = z4.object({
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((brief, ctx) => {
2137
- const hasSecondaryTargets = brief.secondaryContentTypes.length > 0;
2220
+ }).superRefine((plan, ctx) => {
2221
+ const hasSecondaryTargets = plan.secondaryContentTypes.length > 0;
2138
2222
  if (!hasSecondaryTargets) {
2139
2223
  return;
2140
2224
  }
2141
- if (brief.secondaryContentStrategy.trim().length < 20) {
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/planContentBrief.ts
2155
- var SHARED_BRIEF_MAX_TOKENS = 8e3;
2156
- async function planContentBrief({
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 buildDryRunContentBrief(idea, targetAudienceHint);
2250
+ return buildDryRunContentPlan(idea, targetAudienceHint);
2167
2251
  }
2168
- const sharedBriefSettings = {
2252
+ const sharedPlanSettings = {
2169
2253
  ...settings,
2170
2254
  modelSettings: {
2171
2255
  ...settings.modelSettings,
2172
- maxTokens: Math.max(settings.modelSettings.maxTokens, SHARED_BRIEF_MAX_TOKENS)
2256
+ maxTokens: Math.max(settings.modelSettings.maxTokens, SHARED_PLAN_MAX_TOKENS)
2173
2257
  }
2174
2258
  };
2175
2259
  return await openRouter.requestStructured({
2176
- schemaName: "content_brief",
2177
- schema: contentBriefSchema,
2178
- messages: buildContentBriefMessages(idea, {
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: sharedBriefSettings,
2268
+ settings: sharedPlanSettings,
2185
2269
  interactionContext: {
2186
- stageId: "shared-brief",
2187
- operationId: "shared-brief:content-brief"
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 contentBriefSchema2.parse(data);
2276
+ return contentPlanSchema2.parse(data);
2193
2277
  }
2194
2278
  });
2195
2279
  }
2196
- function buildDryRunContentBrief(idea, targetAudienceHint) {
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 Brief";
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/articlePlan.ts
2224
- function deriveArticleSectionCounts(targetLengthWords) {
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 buildArticlePlanJsonSchema(targetLengthWords) {
2236
- const sectionCounts = deriveArticleSectionCounts(targetLengthWords);
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: 2,
2283
- maxItems: 3,
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 buildArticlePlanMessages(idea, options) {
2298
- const sectionCounts = deriveArticleSectionCounts(options.targetLength);
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 article plan for a polished long-form Markdown article.",
2301
- buildArticlePlanGuideInstruction(options.intent, "article"),
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("article", options.targetLength),
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
- "Create an article plan from this idea:",
2436
+ `Create a ${options.contentType} plan from this idea:`,
2315
2437
  idea,
2316
2438
  "",
2317
2439
  "Requirements:",
2318
- "- The article should feel authoritative, practical, and clearly structured for scanning and deep reading.",
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 brief.",
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 article-only structure and must not be treated as requirements for non-article channels.",
2326
- "- Include a cover image description and 2 to 3 inline image descriptions.",
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 brief context:",
2332
- `- description: ${options.contentBrief.description}`,
2333
- `- targetAudience: ${options.contentBrief.targetAudience}`,
2334
- `- corePromise: ${options.contentBrief.corePromise}`,
2335
- `- keyPoints: ${options.contentBrief.keyPoints.join(" | ")}`,
2336
- `- voiceNotes: ${options.contentBrief.voiceNotes}`,
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
- "- inlineImages: array of 2 to 3 objects, each with a description string and an anchorAfterSection number (1-based section index)",
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 articlePlanSchema = z5.object({
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/planArticle.ts
2383
- async function planArticle({
2574
+ // src/generation/planPrimaryContent.ts
2575
+ async function planPrimaryContent({
2384
2576
  idea,
2385
- contentBrief,
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 basePlan = dryRun || !openRouter ? buildDryRunPlan(idea, contentBrief) : await openRouter.requestStructured({
2394
- schemaName: "article_plan",
2395
- schema: buildArticlePlanJsonSchema(settings.targetLength),
2396
- messages: buildArticlePlanMessages(idea, {
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
- contentBrief,
2594
+ contentPlan,
2400
2595
  targetLength: settings.targetLength
2401
2596
  }),
2402
2597
  settings,
2403
2598
  interactionContext: {
2404
2599
  stageId: "planning",
2405
- operationId: "planning:article-plan"
2600
+ operationId: `planning:${contentType}-plan`
2406
2601
  },
2407
2602
  onInteraction,
2408
2603
  onMetrics: onLlmMetrics,
2409
2604
  parse(data) {
2410
- return articlePlanSchema.parse(data);
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
- const sectionCount = basePlan.sections.length;
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, contentBrief) {
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 a strong article",
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: contentBrief.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 articles need structure, intent, and editorial judgment."
2655
+ description: "Explain why strong content needs structure, intent, and editorial judgment."
2440
2656
  },
2441
2657
  {
2442
- title: "Designing the article before drafting",
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 article outline on a desk full of notes.",
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-article";
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 brief (must guide this output):",
2519
- `- title: ${options.contentBrief.title}`,
2520
- `- description: ${options.contentBrief.description}`,
2521
- `- targetAudience: ${options.contentBrief.targetAudience}`,
2522
- `- corePromise: ${options.contentBrief.corePromise}`,
2523
- `- keyPoints: ${options.contentBrief.keyPoints.join(" | ")}`,
2524
- `- voiceNotes: ${options.contentBrief.voiceNotes}`,
2525
- `- primaryContentType: ${options.contentBrief.primaryContentType}`,
2526
- `- secondaryContentTypes: ${options.contentBrief.secondaryContentTypes.join(" | ") || "none"}`,
2527
- `- secondaryContentStrategy: ${options.contentBrief.secondaryContentStrategy}`,
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
- contentBrief,
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
- contentBrief,
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
- contentBrief,
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 brief: ${options.contentBrief.description}`,
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, "article"),
2860
+ buildArticleSectionGuideInstruction(style, intent, contentType),
2629
2861
  buildRunContextDirective(contentTypes),
2630
- buildTargetLengthDirective("article", targetLengthWords)
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
- return normalized.replace(/^```(?:markdown)?\s*/i, "").replace(/```\s*$/i, "").trim();
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 { mkdir as mkdir4, readFile as readFile5, rm as rm2, writeFile as writeFile5 } from "fs/promises";
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-brief", "planning", "sections", "image-prompts", "images", "output", "links"];
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
- contentBrief: contentBriefSchema2.nullable().default(null),
3783
- plan: articlePlanSchema.nullable(),
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(workingDir, ".ideon", "write");
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 mkdir4(writeRoot, { recursive: true });
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
- contentBrief: null,
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 (isNotFoundError(error)) {
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 mkdir4(path7.dirname(statePath), { recursive: true });
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 in .ideon/write/state.json. Start a fresh write first.");
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
- contentBrief: has("contentBrief") ? patch.contentBrief ?? null : existing.contentBrief,
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 isNotFoundError(error) {
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(options = { isArticlePrimary: true }) {
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-brief",
3892
- title: "Planning Shared Brief",
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: planningTitle,
4203
+ title: "Planning Primary Content",
3899
4204
  status: "pending",
3900
- detail: planningDetail
4205
+ detail: "Generating title, slug, and content plan for the primary output."
3901
4206
  },
3902
4207
  {
3903
4208
  id: "sections",
3904
- title: sectionsTitle,
4209
+ title: "Writing Primary Content",
3905
4210
  status: "pending",
3906
- detail: sectionsDetail
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 isArticlePrimary = primaryTarget.contentType === "article";
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(input.config.settings, workingDir);
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-brief", {
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 in .ideon/write/state.json. Start a fresh write first.");
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 contentBrief = writeSession.contentBrief;
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 (contentBrief) {
4043
- markStageCompleted(stageTracking, "shared-brief");
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 brief from .ideon/write.",
4048
- summary: contentBrief.title,
4049
- stageAnalytics: snapshotStageAnalytics(stageTracking, "shared-brief")
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
- contentBrief = await planContentBrief({
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-brief", metrics);
4365
+ recordLlmMetrics(stageTracking, "shared-plan", metrics);
4063
4366
  }
4064
4367
  });
4065
- markStageCompleted(stageTracking, "shared-brief");
4368
+ markStageCompleted(stageTracking, "shared-plan");
4066
4369
  stages[0] = {
4067
4370
  ...stages[0],
4068
4371
  status: "succeeded",
4069
- detail: "Shared brief generated successfully.",
4070
- summary: contentBrief.title,
4071
- stageAnalytics: snapshotStageAnalytics(stageTracking, "shared-brief")
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-brief",
4379
+ lastCompletedStage: "shared-plan",
4077
4380
  failedStage: null,
4078
4381
  errorMessage: null,
4079
- contentBrief
4382
+ contentPlan
4080
4383
  },
4081
4384
  workingDir
4082
4385
  );
4083
4386
  }
4084
- if (hasArticlePrimary) {
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: "running",
4088
- detail: "Generating title, slug, section plan, and image slots."
4398
+ status: "succeeded",
4399
+ detail: "Reused saved plan from cached session.",
4400
+ summary: buildPlanSummary(plan),
4401
+ stageAnalytics: snapshotStageAnalytics(stageTracking, "planning")
4089
4402
  };
4090
- markStageStarted(stageTracking, "planning");
4091
- options.onUpdate?.(cloneStages(stages));
4092
- if (plan) {
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(plan.sections.map((section) => section.title))
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 .ideon/write.",
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 .ideon/write."
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 .ideon/write.",
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(plan, text.sections, { maxImages: options.maxImages }),
4285
- planContext: plan,
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
- contentBrief,
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(contentBrief, primaryTarget.contentType, primaryMarkdownTemplate)];
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 ?? slugifyIdea(input.idea);
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 mkdir5(generationDir, { recursive: true });
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 (hasArticlePrimary) {
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 .ideon/write.",
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 .ideon/write.",
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
- contentBrief.title || deriveTitleFromIdea2(input.idea)
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 (!contentBrief) {
4650
- throw new Error("Shared content brief is missing for output generation stage.");
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
- contentBrief,
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 .ideon/write.",
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 ?? contentBrief.title ?? deriveTitleFromIdea2(input.idea),
4845
- articleDescription: plan?.description ?? contentBrief.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 ?? contentBrief.title ?? deriveTitleFromIdea2(input.idea),
4959
- slug: plan?.slug ?? slugifyIdea(input.idea),
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-brief", "planning", "sections", "image-prompts", "images", "output", "links"];
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(contentBrief, primaryContentType, primaryMarkdown) {
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: `Cover image for ${primaryContentType}`,
5580
+ description: plan.coverImageDescription,
5291
5581
  anchorAfterSection: null,
5292
5582
  prompt: [
5293
- `Cover image for ${primaryContentType}.`,
5294
- `Core angle: ${contentBrief.description}`,
5295
- `Audience: ${contentBrief.targetAudience}`,
5296
- `Promise: ${contentBrief.corePromise}`,
5297
- `Voice: ${contentBrief.voiceNotes}`,
5298
- `Primary excerpt: ${markdownExcerpt}`,
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
- return idea.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "generated-content";
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 yamlKeywords = plan.keywords.map((kw) => ` - "${kw}"`).join("\n");
5353
- const sectionsRows = plan.sections.map((section, index) => `| ${index + 1} | ${section.title} | ${section.description} |`).join("\n");
5354
- const inlineImageRows = plan.inlineImages.map((img, index) => `| Inline ${index + 1} | ${img.description} |`).join("\n");
5355
- const imageRows = `| Cover | ${plan.coverImageDescription} |
5356
- ${inlineImageRows}`;
5357
- return `---
5358
- title: "${plan.title.replace(/"/g, '\\"')}"
5359
- subtitle: "${plan.subtitle.replace(/"/g, '\\"')}"
5360
- slug: "${plan.slug}"
5361
- keywords:
5362
- ${yamlKeywords}
5363
- ---
5364
-
5365
- ## Description
5366
-
5367
- ${plan.description}
5368
-
5369
- ## Introduction Brief
5370
-
5371
- ${plan.introBrief}
5372
-
5373
- ## Sections
5374
-
5375
- | # | Title | Description |
5376
- |---|-------|-------------|
5377
- ${sectionsRows}
5378
-
5379
- ## Outro Brief
5380
-
5381
- ${plan.outroBrief}
5382
-
5383
- ## Image Plan
5384
-
5385
- | Type | Description |
5386
- |------|-------------|
5387
- ${imageRows}
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-brief" || stageId === "planning" || stageId === "sections" || stageId === "image-prompts" || stageId === "images" || stageId === "output" || stageId === "links") {
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(resolved.config.settings, slug, cwd2);
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(settings, slug, cwd2) {
5497
- const outputPaths = resolveOutputPaths(settings, cwd2);
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 mkdir6, readFile as readFile8, stat as stat5, writeFile as writeFile6 } from "fs/promises";
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(resolved.config.settings, cwd2);
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 mkdir6(destinationDir, { recursive: true });
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 mkdir6(path11.dirname(destImagePath), { recursive: true });
6053
- await copyFile(absoluteImageSrc, destImagePath);
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 in .ideon/write/state.json.");
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-brief', 'planning', 'sections', 'image-prompts', 'images', 'output', 'links'];
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 [savedSettings, envSettings] = await Promise.all([loadSavedSettings(), Promise.resolve(readEnvSettings())]);
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-brief") return "shared-brief";
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 in .ideon/write/state.json. Run ideon write <idea> first.");
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-brief targeting").option("-j, --job <path>", "Path to a JSON job definition").option("--primary <type=count>", "Required primary output target (for example: article=1 or x-post=1)").option("--secondary <type=count>", "Secondary output target, repeatable (for example: x-thread=3, linkedin-post=2)", collectOptionValue).option("--style <style>", "Writing style (academic, analytical, authoritative, conversational, empathetic, friendly, journalistic, minimalist, persuasive, playful, professional, storytelling, technical)").option("--intent <intent>", "Content intent (announcement, case-study, cornerstone, counterargument, critique-review, deep-dive-analysis, how-to-guide, interview-q-and-a, listicle, opinion-piece, personal-essay, roundup-curation, tutorial)").option("--length <size>", "Target length: small, medium, large, or a positive integer word count").option("--no-interactive", "Fail instead of prompting for missing input in TTY mode").option("--dry-run", "Run the pipeline shell without external API calls", false).option("--enrich-links", "Run link enrichment after markdown generation", false).option("--link <pair>", 'Custom link "expression->url", repeatable', collectOptionValue).option("--unlink <expression>", "Remove a custom link by expression, repeatable", collectOptionValue).option("--max-links <n>", "Max number of generated links", (v) => Number.parseInt(v, 10)).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) => {
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 from .ideon/write.").option("--no-interactive", "Force plain non-interactive output even in TTY mode", false).option("--enrich-links", "Run link enrichment after markdown generation", false).option("--link <pair>", 'Custom link "expression->url", repeatable', collectOptionValue).option("--unlink <expression>", "Remove a custom link by expression, repeatable", collectOptionValue).option("--max-links <n>", "Max number of generated links", (v) => Number.parseInt(v, 10)).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) => {
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,