@telepat/ideon 0.1.21 → 0.1.25

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.
Files changed (2) hide show
  1. package/dist/ideon.js +1010 -643
  2. package/package.json +1 -1
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: 0, max: 1 };
488
+ if (alias === "medium") return { min: 1, max: 2 };
489
+ return { min: 2, 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.21",
1426
+ version: "0.1.25",
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,9 +1623,9 @@ 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
- import path8 from "path";
1628
+ import path9 from "path";
1570
1629
 
1571
1630
  // src/generation/enrichLinks.ts
1572
1631
  import { readFile as readFile4 } from "fs/promises";
@@ -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([
@@ -2220,8 +2304,8 @@ function deriveTitleFromIdea(idea) {
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,28 +2372,57 @@ 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,
2287
2380
  required: ["description", "anchorAfterSection"],
2288
2381
  properties: {
2289
2382
  description: { type: "string" },
2290
- anchorAfterSection: { type: "number", minimum: 1 }
2383
+ anchorAfterSection: { type: "number", minimum: 2 }
2291
2384
  }
2292
2385
  }
2293
2386
  }
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,20 +2433,20 @@ 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
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.",
2327
- "- Each inline image must specify which section it follows (anchorAfterSection, 1-based index). Choose sections where visual reinforcement adds the most value.",
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.`,
2449
+ "- Each inline image must specify which section it follows (anchorAfterSection, starting at 2). Choose sections where visual reinforcement adds the most value. Do not anchor inline images after the first section because the cover image already appears near the title.",
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
  "",
@@ -2336,16 +2458,63 @@ function buildArticlePlanMessages(idea, options) {
2336
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",
2342
2465
  "- slug: string in lowercase kebab-case",
2343
- "- description: string",
2344
- "- introBrief: string",
2345
- "- outroBrief: string",
2346
- `- sections: array of ${sectionCounts.label} objects, each with title and description strings`,
2466
+ "- description: string",
2467
+ "- introBrief: string",
2468
+ "- outroBrief: string",
2469
+ `- sections: array of ${sectionCounts.label} objects, each with title and description strings`,
2470
+ "- coverImageDescription: string",
2471
+ `- inlineImages: array of ${imageCounts.min} to ${imageCounts.max} objects, each with a description string and an anchorAfterSection number (starting at 2, since the cover image already appears near the title)`,
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)",
2347
2516
  "- coverImageDescription: string",
2348
- "- inlineImages: array of 2 to 3 objects, each with a description string and an anchorAfterSection number (1-based section index)",
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")
@@ -2361,9 +2530,24 @@ var articleSectionPlanSchema = z5.object({
2361
2530
  });
2362
2531
  var inlineImagePlanSchema = z5.object({
2363
2532
  description: z5.string().min(1),
2364
- anchorAfterSection: z5.number().int().min(1)
2533
+ anchorAfterSection: z5.number().int().min(2)
2534
+ });
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(0).max(4).optional(),
2547
+ angle: z5.string().min(1).optional()
2365
2548
  });
2366
- var articlePlanSchema = z5.object({
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),
@@ -2373,15 +2557,24 @@ var articlePlanSchema = z5.object({
2373
2557
  outroBrief: z5.string().min(1),
2374
2558
  sections: z5.array(articleSectionPlanSchema).min(2).max(10),
2375
2559
  coverImageDescription: z5.string().min(1),
2376
- inlineImages: z5.array(inlineImagePlanSchema).min(2).max(3)
2560
+ inlineImages: z5.array(inlineImagePlanSchema).min(0).max(4)
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()
2377
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,
2577
+ contentType,
2385
2578
  contentPlan,
2386
2579
  settings,
2387
2580
  markdownOutputDir,
@@ -2390,10 +2583,12 @@ async function planArticle({
2390
2583
  onLlmMetrics,
2391
2584
  onInteraction
2392
2585
  }) {
2393
- const basePlan = dryRun || !openRouter ? buildDryRunPlan(idea, contentPlan) : 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
2594
  contentPlan,
@@ -2402,32 +2597,53 @@ async function planArticle({
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(2, 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, contentPlan) {
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
2649
  description: contentPlan.description,
@@ -2436,10 +2652,10 @@ function buildDryRunPlan(idea, contentPlan) {
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, contentPlan) {
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, contentPlan) {
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",
@@ -2525,6 +2749,7 @@ function buildSingleShotContentMessages(options) {
2525
2749
  `- primaryContentType: ${options.contentPlan.primaryContentType}`,
2526
2750
  `- secondaryContentTypes: ${options.contentPlan.secondaryContentTypes.join(" | ") || "none"}`,
2527
2751
  `- secondaryContentStrategy: ${options.contentPlan.secondaryContentStrategy}`,
2752
+ planContext,
2528
2753
  "",
2529
2754
  articleContext,
2530
2755
  "",
@@ -2549,6 +2774,7 @@ async function writeSingleShotContent({
2549
2774
  outputCountForType,
2550
2775
  articleReferenceMarkdown,
2551
2776
  contentPlan,
2777
+ plan,
2552
2778
  settings,
2553
2779
  openRouter,
2554
2780
  dryRun,
@@ -2564,6 +2790,7 @@ async function writeSingleShotContent({
2564
2790
  outputIndex,
2565
2791
  outputCountForType,
2566
2792
  contentPlan,
2793
+ plan,
2567
2794
  articleReferenceMarkdown
2568
2795
  });
2569
2796
  }
@@ -2578,6 +2805,7 @@ async function writeSingleShotContent({
2578
2805
  outputIndex,
2579
2806
  outputCountForType,
2580
2807
  contentPlan,
2808
+ plan,
2581
2809
  articleReferenceMarkdown,
2582
2810
  targetLength: settings.targetLength
2583
2811
  }),
@@ -2592,6 +2820,9 @@ async function writeSingleShotContent({
2592
2820
  }
2593
2821
  function buildDryRunContent(options) {
2594
2822
  const anchorNote = options.articleReferenceMarkdown ? "Anchored to generated primary context from this run." : "No primary anchor available; generated directly from idea.";
2823
+ const planNote = options.plan ? `Plan title: ${options.plan.title}
2824
+ Plan description: ${options.plan.description}${options.plan.angle ? `
2825
+ Angle: ${options.plan.angle}` : ""}` : "No primary plan available.";
2595
2826
  return [
2596
2827
  `# ${options.contentType} draft ${options.outputIndex}`,
2597
2828
  "",
@@ -2600,6 +2831,7 @@ function buildDryRunContent(options) {
2600
2831
  `Role: ${options.role}`,
2601
2832
  `Primary content type: ${options.primaryContentType}`,
2602
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 = [
@@ -2991,7 +3276,7 @@ function buildImageSlots(plan, sections, options) {
2991
3276
  kind: "inline",
2992
3277
  prompt: "",
2993
3278
  description: img.description,
2994
- anchorAfterSection: Math.max(1, Math.min(sectionCount, img.anchorAfterSection))
3279
+ anchorAfterSection: Math.max(2, Math.min(sectionCount, img.anchorAfterSection))
2995
3280
  });
2996
3281
  }
2997
3282
  return 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,
@@ -3715,10 +4003,69 @@ ${body.join("\n").trim()}
3715
4003
  `;
3716
4004
  }
3717
4005
 
3718
- // src/pipeline/sessionStore.ts
3719
- import { createHash } from "crypto";
3720
- import { mkdir as mkdir4, readFile as readFile5, rm as rm2, writeFile as writeFile5 } from "fs/promises";
4006
+ // src/output/meta.ts
3721
4007
  import path7 from "path";
4008
+ function buildMetaJson(input) {
4009
+ const plan = input.plan;
4010
+ const contentPlan = input.contentPlan;
4011
+ const generationDir = input.generationDir;
4012
+ const title = plan?.title ?? contentPlan?.title ?? input.idea;
4013
+ const slug = plan?.slug ?? "";
4014
+ const description = plan?.description ?? contentPlan?.description ?? "";
4015
+ const subtitle = (plan && "subtitle" in plan ? plan.subtitle : null) ?? null;
4016
+ const keywords = (plan && "keywords" in plan ? plan.keywords : null) ?? [];
4017
+ const contentType = plan?.contentType ?? contentPlan?.primaryContentType ?? "article";
4018
+ const angle = plan?.angle ?? null;
4019
+ const coverImage = input.renderedImages.find((image) => image.kind === "cover") ?? null;
4020
+ const cover = coverImage ? {
4021
+ path: coverImage.outputPath,
4022
+ relativePath: coverImage.relativePath,
4023
+ description: coverImage.description
4024
+ } : null;
4025
+ const sections = plan?.sections?.map((section) => ({
4026
+ title: section.title,
4027
+ description: section.description
4028
+ })) ?? [];
4029
+ const images = input.renderedImages.map((image) => ({
4030
+ id: image.id,
4031
+ kind: image.kind,
4032
+ path: image.outputPath,
4033
+ relativePath: image.relativePath,
4034
+ description: image.description,
4035
+ anchorAfterSection: image.anchorAfterSection
4036
+ }));
4037
+ const outputs = input.outputs.map((output) => ({
4038
+ fileId: output.fileId,
4039
+ contentType: output.contentType,
4040
+ path: output.markdownPath,
4041
+ relativePath: path7.relative(generationDir, output.markdownPath)
4042
+ }));
4043
+ return {
4044
+ version: 1,
4045
+ title,
4046
+ slug,
4047
+ idea: input.idea,
4048
+ description,
4049
+ subtitle,
4050
+ keywords,
4051
+ contentType,
4052
+ style: input.style,
4053
+ intent: input.intent,
4054
+ targetLength: input.targetLength,
4055
+ angle,
4056
+ cover,
4057
+ sections,
4058
+ images,
4059
+ outputs,
4060
+ generatedAt: input.generatedAt,
4061
+ generationDir
4062
+ };
4063
+ }
4064
+
4065
+ // src/pipeline/sessionStore.ts
4066
+ import { createHash as createHash2 } from "crypto";
4067
+ import { mkdir as mkdir5, readFile as readFile5, rm as rm2, writeFile as writeFile5 } from "fs/promises";
4068
+ import path8 from "path";
3722
4069
  import envPaths3 from "env-paths";
3723
4070
  import { z as z6 } from "zod";
3724
4071
  var STAGE_IDS = ["shared-plan", "planning", "sections", "image-prompts", "images", "output", "links"];
@@ -3761,7 +4108,8 @@ var pipelineArtifactSummarySchema = z6.object({
3761
4108
  assetDir: z6.string().min(1),
3762
4109
  analyticsPath: z6.string().min(1).default("unknown.analytics.json"),
3763
4110
  interactionsPath: z6.string().min(1).default("unknown.interactions.json"),
3764
- planPath: z6.string().min(1).nullable().default(null)
4111
+ planPath: z6.string().min(1).nullable().default(null),
4112
+ metaJsonPath: z6.string().min(1).default("meta.json")
3765
4113
  });
3766
4114
  var resolvedPathsSchema = z6.object({
3767
4115
  markdownOutputDir: z6.string().min(1),
@@ -3782,7 +4130,7 @@ var writeSessionStateSchema = z6.object({
3782
4130
  failedStage: z6.enum(STAGE_IDS).nullable(),
3783
4131
  errorMessage: z6.string().nullable(),
3784
4132
  contentPlan: contentPlanSchema2.nullable().default(null),
3785
- plan: articlePlanSchema.nullable(),
4133
+ plan: primaryPlanSchema.nullable(),
3786
4134
  text: z6.object({
3787
4135
  intro: z6.string().min(1),
3788
4136
  sections: z6.array(generatedArticleSectionSchema),
@@ -3797,23 +4145,23 @@ var writeSessionStateSchema = z6.object({
3797
4145
  artifact: pipelineArtifactSummarySchema.nullable()
3798
4146
  });
3799
4147
  var ideonPaths3 = envPaths3("ideon", { suffix: "" });
3800
- var sessionsDir = path7.join(ideonPaths3.config, "sessions");
4148
+ var sessionsDir = path8.join(ideonPaths3.config, "sessions");
3801
4149
  function hashProjectPath(workingDir) {
3802
- return createHash("sha256").update(path7.resolve(workingDir)).digest("hex").slice(0, 16);
4150
+ return createHash2("sha256").update(path8.resolve(workingDir)).digest("hex").slice(0, 16);
3803
4151
  }
3804
4152
  function resolveWriteRoot(workingDir) {
3805
- return path7.join(sessionsDir, hashProjectPath(workingDir));
4153
+ return path8.join(sessionsDir, hashProjectPath(workingDir));
3806
4154
  }
3807
4155
  function resolveStateFilePath(workingDir) {
3808
- return path7.join(resolveWriteRoot(workingDir), "state.json");
4156
+ return path8.join(resolveWriteRoot(workingDir), "state.json");
3809
4157
  }
3810
4158
  function resolveLegacyStatePath(workingDir) {
3811
- return path7.join(workingDir, ".ideon", "write", "state.json");
4159
+ return path8.join(workingDir, ".ideon", "write", "state.json");
3812
4160
  }
3813
4161
  async function startFreshWriteSession(seed, workingDir = process.cwd()) {
3814
4162
  const writeRoot = resolveWriteRoot(workingDir);
3815
4163
  await rm2(writeRoot, { recursive: true, force: true });
3816
- await mkdir4(writeRoot, { recursive: true });
4164
+ await mkdir5(writeRoot, { recursive: true });
3817
4165
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
3818
4166
  const state = {
3819
4167
  version: 1,
@@ -3846,7 +4194,7 @@ async function loadWriteSession(workingDir = process.cwd()) {
3846
4194
  const raw = await readFile5(statePath, "utf8");
3847
4195
  return writeSessionStateSchema.parse(JSON.parse(raw));
3848
4196
  } catch (error) {
3849
- if (!isNotFoundError(error)) {
4197
+ if (!isNotFoundError2(error)) {
3850
4198
  throw error;
3851
4199
  }
3852
4200
  }
@@ -3857,7 +4205,7 @@ async function loadWriteSession(workingDir = process.cwd()) {
3857
4205
  await saveWriteSession(state, workingDir);
3858
4206
  return state;
3859
4207
  } catch (error) {
3860
- if (isNotFoundError(error)) {
4208
+ if (isNotFoundError2(error)) {
3861
4209
  return null;
3862
4210
  }
3863
4211
  throw error;
@@ -3869,7 +4217,7 @@ async function saveWriteSession(state, workingDir = process.cwd()) {
3869
4217
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3870
4218
  });
3871
4219
  const statePath = resolveStateFilePath(workingDir);
3872
- await mkdir4(path7.dirname(statePath), { recursive: true });
4220
+ await mkdir5(path8.dirname(statePath), { recursive: true });
3873
4221
  await writeFile5(statePath, `${JSON.stringify(next, null, 2)}
3874
4222
  `, "utf8");
3875
4223
  return next;
@@ -3897,16 +4245,12 @@ async function patchWriteSession(patch, workingDir = process.cwd()) {
3897
4245
  };
3898
4246
  return saveWriteSession(merged, workingDir);
3899
4247
  }
3900
- function isNotFoundError(error) {
4248
+ function isNotFoundError2(error) {
3901
4249
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
3902
4250
  }
3903
4251
 
3904
4252
  // src/pipeline/runner.ts
3905
- function createInitialStages(options = { isArticlePrimary: true }) {
3906
- const planningTitle = options.isArticlePrimary ? "Planning Primary Article" : "Planning Primary Content";
3907
- const planningDetail = options.isArticlePrimary ? "Generating title, slug, section plan, and image slots." : "Defining the primary angle and output intent.";
3908
- const sectionsTitle = options.isArticlePrimary ? "Writing Sections" : "Generating Primary Content";
3909
- const sectionsDetail = options.isArticlePrimary ? "Waiting for the approved article plan." : "Waiting for primary content generation to begin.";
4253
+ function createInitialStages() {
3910
4254
  return [
3911
4255
  {
3912
4256
  id: "shared-plan",
@@ -3916,15 +4260,15 @@ function createInitialStages(options = { isArticlePrimary: true }) {
3916
4260
  },
3917
4261
  {
3918
4262
  id: "planning",
3919
- title: planningTitle,
4263
+ title: "Planning Primary Content",
3920
4264
  status: "pending",
3921
- detail: planningDetail
4265
+ detail: "Generating title, slug, and content plan for the primary output."
3922
4266
  },
3923
4267
  {
3924
4268
  id: "sections",
3925
- title: sectionsTitle,
4269
+ title: "Writing Primary Content",
3926
4270
  status: "pending",
3927
- detail: sectionsDetail
4271
+ detail: "Waiting for the approved primary plan."
3928
4272
  },
3929
4273
  {
3930
4274
  id: "image-prompts",
@@ -3958,8 +4302,7 @@ async function runPipelineShell(input, options = {}) {
3958
4302
  const runId = randomUUID();
3959
4303
  const primaryTarget = getPrimaryTarget(input.config.settings.contentTargets);
3960
4304
  const secondaryTargets = getSecondaryTargets(input.config.settings.contentTargets);
3961
- const isArticlePrimary = primaryTarget.contentType === "article";
3962
- const stages = createInitialStages({ isArticlePrimary });
4305
+ const stages = createInitialStages();
3963
4306
  options.onUpdate?.(cloneStages(stages));
3964
4307
  const dryRun = options.dryRun ?? false;
3965
4308
  const shouldEnrichLinks = options.enrichLinks ?? false;
@@ -3968,8 +4311,7 @@ async function runPipelineShell(input, options = {}) {
3968
4311
  const pipelineCustomLinkRaws = options.customLinks ?? [];
3969
4312
  const pipelineUnlinks = options.unlinks ?? [];
3970
4313
  const pipelineMaxLinks = options.maxLinks;
3971
- const outputPaths = resolveOutputPaths(input.config.settings, workingDir);
3972
- const hasArticlePrimary = isArticlePrimary;
4314
+ const outputPaths = resolveOutputPaths();
3973
4315
  const stageTracking = /* @__PURE__ */ new Map();
3974
4316
  const stageRetryState = /* @__PURE__ */ new Map();
3975
4317
  const llmOperationRetryState = /* @__PURE__ */ new Map();
@@ -4102,66 +4444,69 @@ async function runPipelineShell(input, options = {}) {
4102
4444
  workingDir
4103
4445
  );
4104
4446
  }
4105
- if (hasArticlePrimary) {
4447
+ stages[1] = {
4448
+ ...stages[1],
4449
+ status: "running",
4450
+ detail: `Planning primary ${primaryTarget.contentType} content.`
4451
+ };
4452
+ markStageStarted(stageTracking, "planning");
4453
+ options.onUpdate?.(cloneStages(stages));
4454
+ if (plan) {
4455
+ markStageCompleted(stageTracking, "planning");
4106
4456
  stages[1] = {
4107
4457
  ...stages[1],
4108
- status: "running",
4109
- detail: "Generating title, slug, section plan, and image slots."
4458
+ status: "succeeded",
4459
+ detail: "Reused saved plan from cached session.",
4460
+ summary: buildPlanSummary(plan),
4461
+ stageAnalytics: snapshotStageAnalytics(stageTracking, "planning")
4110
4462
  };
4111
- markStageStarted(stageTracking, "planning");
4112
- options.onUpdate?.(cloneStages(stages));
4113
- if (plan) {
4114
- markStageCompleted(stageTracking, "planning");
4115
- stages[1] = {
4116
- ...stages[1],
4117
- status: "succeeded",
4118
- detail: "Reused saved plan from cached session.",
4119
- summary: `${plan.title} \u2022 ${plan.slug} \u2022 ${plan.sections.length} sections \u2022 ${plan.inlineImages.length + 1} images`,
4120
- stageAnalytics: snapshotStageAnalytics(stageTracking, "planning")
4121
- };
4122
- } else {
4123
- if (!contentPlan) {
4124
- throw new Error("Shared content plan is missing for article planning stage.");
4463
+ } else {
4464
+ if (!contentPlan) {
4465
+ throw new Error("Shared content plan is missing for primary planning stage.");
4466
+ }
4467
+ plan = await planPrimaryContent({
4468
+ idea: input.idea,
4469
+ contentType: primaryTarget.contentType,
4470
+ contentPlan,
4471
+ settings: input.config.settings,
4472
+ markdownOutputDir: writeSession.outputPaths.markdownOutputDir,
4473
+ openRouter,
4474
+ dryRun,
4475
+ onInteraction(interaction) {
4476
+ onLlmInteraction(interaction);
4477
+ },
4478
+ onLlmMetrics(metrics) {
4479
+ recordLlmMetrics(stageTracking, "planning", metrics);
4125
4480
  }
4126
- plan = await planArticle({
4127
- idea: input.idea,
4481
+ });
4482
+ markStageCompleted(stageTracking, "planning");
4483
+ stages[1] = {
4484
+ ...stages[1],
4485
+ status: "succeeded",
4486
+ detail: "Plan generated successfully.",
4487
+ summary: buildPlanSummary(plan),
4488
+ stageAnalytics: snapshotStageAnalytics(stageTracking, "planning")
4489
+ };
4490
+ writeSession = await patchWriteSession(
4491
+ {
4492
+ status: "running",
4493
+ lastCompletedStage: "planning",
4494
+ failedStage: null,
4495
+ errorMessage: null,
4128
4496
  contentPlan,
4129
- settings: input.config.settings,
4130
- markdownOutputDir: writeSession.outputPaths.markdownOutputDir,
4131
- openRouter,
4132
- dryRun,
4133
- onInteraction(interaction) {
4134
- onLlmInteraction(interaction);
4135
- },
4136
- onLlmMetrics(metrics) {
4137
- recordLlmMetrics(stageTracking, "planning", metrics);
4138
- }
4139
- });
4140
- markStageCompleted(stageTracking, "planning");
4141
- stages[1] = {
4142
- ...stages[1],
4143
- status: "succeeded",
4144
- detail: "Plan generated successfully.",
4145
- summary: `${plan.title} \u2022 ${plan.slug} \u2022 ${plan.sections.length} sections \u2022 ${plan.inlineImages.length + 1} images`,
4146
- stageAnalytics: snapshotStageAnalytics(stageTracking, "planning")
4147
- };
4148
- writeSession = await patchWriteSession(
4149
- {
4150
- status: "running",
4151
- lastCompletedStage: "planning",
4152
- failedStage: null,
4153
- errorMessage: null,
4154
- contentPlan,
4155
- plan
4156
- },
4157
- workingDir
4158
- );
4159
- }
4497
+ plan
4498
+ },
4499
+ workingDir
4500
+ );
4501
+ }
4502
+ const isLongForm = isLongFormPlan(plan);
4503
+ if (isLongForm) {
4504
+ const longPlan = plan;
4160
4505
  stages[2] = {
4161
4506
  ...stages[2],
4162
4507
  status: "running",
4163
4508
  detail: "Writing introduction.",
4164
- items: buildSectionItems(plan.sections.map((section) => section.title))
4509
+ items: buildSectionItems(longPlan.sections.map((section) => section.title))
4165
4510
  };
4166
4511
  markStageStarted(stageTracking, "sections");
4167
4512
  options.onUpdate?.(cloneStages(stages));
@@ -4189,7 +4534,7 @@ async function runPipelineShell(input, options = {}) {
4189
4534
  } else {
4190
4535
  const sectionItemTracking = /* @__PURE__ */ new Map();
4191
4536
  text = await writeArticleSections({
4192
- plan,
4537
+ plan: longPlan,
4193
4538
  settings: input.config.settings,
4194
4539
  openRouter,
4195
4540
  dryRun,
@@ -4302,8 +4647,8 @@ async function runPipelineShell(input, options = {}) {
4302
4647
  options.onUpdate?.(cloneStages(stages));
4303
4648
  } else {
4304
4649
  imagePrompts = await expandImagePrompts({
4305
- slots: buildImageSlots(plan, text.sections, { maxImages: options.maxImages }),
4306
- planContext: plan,
4650
+ slots: buildImageSlots(longPlan, text.sections, { maxImages: options.maxImages }),
4651
+ planContext: longPlan,
4307
4652
  sections: text.sections,
4308
4653
  settings: input.config.settings,
4309
4654
  openRouter,
@@ -4364,24 +4709,6 @@ async function runPipelineShell(input, options = {}) {
4364
4709
  );
4365
4710
  }
4366
4711
  } else {
4367
- if (!contentPlan) {
4368
- throw new Error("Shared content plan is missing for primary content planning stage.");
4369
- }
4370
- stages[1] = {
4371
- ...stages[1],
4372
- status: "running",
4373
- detail: `Defining primary direction for ${primaryTarget.contentType}.`
4374
- };
4375
- markStageStarted(stageTracking, "planning");
4376
- options.onUpdate?.(cloneStages(stages));
4377
- markStageCompleted(stageTracking, "planning");
4378
- stages[1] = {
4379
- ...stages[1],
4380
- status: "succeeded",
4381
- detail: `Primary direction locked for ${primaryTarget.contentType}.`,
4382
- summary: `Primary: ${primaryTarget.contentType}`,
4383
- stageAnalytics: snapshotStageAnalytics(stageTracking, "planning")
4384
- };
4385
4712
  stages[2] = {
4386
4713
  ...stages[2],
4387
4714
  status: "running",
@@ -4400,6 +4727,7 @@ async function runPipelineShell(input, options = {}) {
4400
4727
  outputCountForType: 1,
4401
4728
  articleReferenceMarkdown: void 0,
4402
4729
  contentPlan,
4730
+ plan,
4403
4731
  settings: input.config.settings,
4404
4732
  openRouter,
4405
4733
  dryRun,
@@ -4425,7 +4753,7 @@ async function runPipelineShell(input, options = {}) {
4425
4753
  };
4426
4754
  markStageStarted(stageTracking, "image-prompts");
4427
4755
  options.onUpdate?.(cloneStages(stages));
4428
- imagePrompts = [buildPrimaryCoverPrompt(contentPlan, primaryTarget.contentType, primaryMarkdownTemplate)];
4756
+ imagePrompts = [buildPrimaryCoverPrompt(plan, contentPlan, primaryTarget.contentType)];
4429
4757
  markStageCompleted(stageTracking, "image-prompts");
4430
4758
  stages[3] = {
4431
4759
  ...stages[3],
@@ -4448,12 +4776,12 @@ async function runPipelineShell(input, options = {}) {
4448
4776
  options.onUpdate?.(cloneStages(stages));
4449
4777
  }
4450
4778
  const baseSlug = plan?.slug ?? resolveGenerationSlug(input.idea, contentPlan?.title);
4451
- const generationDir = path8.join(
4779
+ const generationDir = path9.join(
4452
4780
  writeSession.outputPaths.markdownOutputDir,
4453
4781
  buildGenerationDirectoryName(baseSlug)
4454
4782
  );
4455
- await mkdir5(generationDir, { recursive: true });
4456
- const jobDefinitionPath = path8.join(generationDir, "job.json");
4783
+ await mkdir6(generationDir, { recursive: true });
4784
+ const jobDefinitionPath = path9.join(generationDir, "job.json");
4457
4785
  await writeJsonFile(
4458
4786
  jobDefinitionPath,
4459
4787
  buildRunJobDefinition({
@@ -4465,14 +4793,15 @@ async function runPipelineShell(input, options = {}) {
4465
4793
  sourceJob: input.job
4466
4794
  })
4467
4795
  );
4468
- const planPath = plan ? path8.join(generationDir, "plan.md") : null;
4796
+ const planPath = plan ? path9.join(generationDir, "plan.md") : null;
4469
4797
  if (plan && planPath) {
4470
4798
  await writeUtf8File(planPath, renderPlanMarkdown(plan));
4471
4799
  }
4472
4800
  const primaryFilePrefix = toFilePrefix(primaryTarget.contentType);
4473
- const primaryMarkdownPath = path8.join(generationDir, `${primaryFilePrefix}-1.md`);
4801
+ const primaryMarkdownPath = path9.join(generationDir, `${primaryFilePrefix}-1.md`);
4474
4802
  const sharedAssetDir = generationDir;
4475
- if (hasArticlePrimary) {
4803
+ if (isLongForm) {
4804
+ const longPlan = plan;
4476
4805
  if (imageArtifacts) {
4477
4806
  markStageCompleted(stageTracking, "images");
4478
4807
  stages[4] = {
@@ -4548,7 +4877,7 @@ async function runPipelineShell(input, options = {}) {
4548
4877
  throw new Error("Article generation requested but required article artifacts are missing.");
4549
4878
  }
4550
4879
  const article = {
4551
- plan,
4880
+ plan: longPlan,
4552
4881
  intro: text.intro,
4553
4882
  sections: text.sections,
4554
4883
  outro: text.outro,
@@ -4633,11 +4962,11 @@ async function runPipelineShell(input, options = {}) {
4633
4962
  }
4634
4963
  const coverImage = imageArtifacts?.renderedImages.find((image) => image.kind === "cover") ?? null;
4635
4964
  if (coverImage) {
4636
- primaryMarkdownTemplate = withCoverImage(primaryMarkdownTemplate, coverImage.relativePath, deriveTitleFromIdea2(input.idea));
4965
+ primaryMarkdownTemplate = withCoverImage(primaryMarkdownTemplate, coverImage.relativePath, plan.title || deriveTitleFromIdea2(input.idea));
4637
4966
  }
4638
4967
  primaryMarkdownTemplate = applyPrimaryTitleHeading(
4639
4968
  primaryMarkdownTemplate,
4640
- contentPlan.title || deriveTitleFromIdea2(input.idea)
4969
+ plan.title || contentPlan.title || deriveTitleFromIdea2(input.idea)
4641
4970
  );
4642
4971
  }
4643
4972
  const markdownPaths = [];
@@ -4693,7 +5022,7 @@ async function runPipelineShell(input, options = {}) {
4693
5022
  })
4694
5023
  };
4695
5024
  options.onUpdate?.(cloneStages(stages));
4696
- const markdownPath = path8.join(generationDir, `${output.filePrefix}-${output.index}.md`);
5025
+ const markdownPath = path9.join(generationDir, `${output.filePrefix}-${output.index}.md`);
4697
5026
  try {
4698
5027
  const content = await writeSingleShotContent({
4699
5028
  idea: input.idea,
@@ -4704,6 +5033,7 @@ async function runPipelineShell(input, options = {}) {
4704
5033
  outputCountForType: output.outputCountForType,
4705
5034
  articleReferenceMarkdown: primaryMarkdownTemplate ?? void 0,
4706
5035
  contentPlan,
5036
+ plan,
4707
5037
  settings: input.config.settings,
4708
5038
  openRouter,
4709
5039
  dryRun,
@@ -4755,7 +5085,7 @@ async function runPipelineShell(input, options = {}) {
4755
5085
  ...item,
4756
5086
  status: "succeeded",
4757
5087
  detail: "Saved markdown output.",
4758
- summary: path8.basename(markdownPath),
5088
+ summary: path9.basename(markdownPath),
4759
5089
  analytics: {
4760
5090
  durationMs: itemDurationMs,
4761
5091
  costUsd: knownItemCost.usd,
@@ -4970,10 +5300,24 @@ async function runPipelineShell(input, options = {}) {
4970
5300
  llmCalls: llmInteractions,
4971
5301
  t2iCalls: t2iInteractions
4972
5302
  };
4973
- const analyticsPath = path8.join(generationDir, "generation.analytics.json");
4974
- const interactionsPath = path8.join(generationDir, "model.interactions.json");
5303
+ const analyticsPath = path9.join(generationDir, "generation.analytics.json");
5304
+ const interactionsPath = path9.join(generationDir, "model.interactions.json");
4975
5305
  await writeJsonFile(analyticsPath, analytics);
4976
5306
  await writeJsonFile(interactionsPath, interactions);
5307
+ const metaJson = buildMetaJson({
5308
+ idea: input.idea,
5309
+ generationDir,
5310
+ contentPlan,
5311
+ plan,
5312
+ renderedImages: imageArtifacts?.renderedImages ?? [],
5313
+ outputs: generatedOutputs,
5314
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
5315
+ style: input.config.settings.style,
5316
+ intent: input.config.settings.intent,
5317
+ targetLength: input.config.settings.targetLength ? resolveTargetLengthAlias(input.config.settings.targetLength) : null
5318
+ });
5319
+ const metaJsonPath = path9.join(generationDir, "meta.json");
5320
+ await writeJsonFile(metaJsonPath, metaJson);
4977
5321
  const primaryMarkdownPathForArtifact = markdownPaths[0] ?? primaryMarkdownPath;
4978
5322
  const artifact = {
4979
5323
  title: plan?.title ?? contentPlan.title ?? deriveTitleFromIdea2(input.idea),
@@ -4987,7 +5331,8 @@ async function runPipelineShell(input, options = {}) {
4987
5331
  assetDir: sharedAssetDir,
4988
5332
  analyticsPath,
4989
5333
  interactionsPath,
4990
- planPath
5334
+ planPath,
5335
+ metaJsonPath
4991
5336
  };
4992
5337
  writeSession = await patchWriteSession(
4993
5338
  {
@@ -5303,20 +5648,19 @@ function getSecondaryTargets(contentTargets) {
5303
5648
  count: target.count
5304
5649
  }));
5305
5650
  }
5306
- function buildPrimaryCoverPrompt(contentPlan, primaryContentType, primaryMarkdown) {
5307
- const markdownExcerpt = primaryMarkdown.replace(/\s+/g, " ").trim().slice(0, 240);
5651
+ function buildPrimaryCoverPrompt(plan, contentPlan, primaryContentType) {
5308
5652
  return {
5309
5653
  id: "cover",
5310
5654
  kind: "cover",
5311
- description: `Cover image for ${primaryContentType}`,
5655
+ description: plan.coverImageDescription,
5312
5656
  anchorAfterSection: null,
5313
5657
  prompt: [
5314
- `Cover image for ${primaryContentType}.`,
5658
+ plan.coverImageDescription,
5659
+ `Content type: ${primaryContentType}`,
5315
5660
  `Core angle: ${contentPlan.description}`,
5316
5661
  `Audience: ${contentPlan.targetAudience}`,
5317
5662
  `Promise: ${contentPlan.corePromise}`,
5318
5663
  `Voice: ${contentPlan.voiceNotes}`,
5319
- `Primary excerpt: ${markdownExcerpt}`,
5320
5664
  "Do not include any words, letters, numbers, logos, watermarks, or signage in the image."
5321
5665
  ].join(" ")
5322
5666
  };
@@ -5380,43 +5724,60 @@ function buildRunJobDefinition(input) {
5380
5724
  };
5381
5725
  }
5382
5726
  function renderPlanMarkdown(plan) {
5383
- const yamlKeywords = plan.keywords.map((kw) => ` - "${kw}"`).join("\n");
5384
- const sectionsRows = plan.sections.map((section, index) => `| ${index + 1} | ${section.title} | ${section.description} |`).join("\n");
5385
- const inlineImageRows = plan.inlineImages.map((img, index) => `| Inline ${index + 1} | ${img.description} |`).join("\n");
5386
- const imageRows = `| Cover | ${plan.coverImageDescription} |
5387
- ${inlineImageRows}`;
5388
- return `---
5389
- title: "${plan.title.replace(/"/g, '\\"')}"
5390
- subtitle: "${plan.subtitle.replace(/"/g, '\\"')}"
5391
- slug: "${plan.slug}"
5392
- keywords:
5393
- ${yamlKeywords}
5394
- ---
5395
-
5396
- ## Description
5397
-
5398
- ${plan.description}
5399
-
5400
- ## Introduction Brief
5401
-
5402
- ${plan.introBrief}
5403
-
5404
- ## Sections
5405
-
5406
- | # | Title | Description |
5407
- |---|-------|-------------|
5408
- ${sectionsRows}
5409
-
5410
- ## Outro Brief
5411
-
5412
- ${plan.outroBrief}
5413
-
5414
- ## Image Plan
5415
-
5416
- | Type | Description |
5417
- |------|-------------|
5418
- ${imageRows}
5419
- `;
5727
+ const lines = [
5728
+ `# ${plan.title}`,
5729
+ "",
5730
+ `**Content type:** ${plan.contentType}`,
5731
+ `**Slug:** ${plan.slug}`,
5732
+ "",
5733
+ "## Description",
5734
+ "",
5735
+ plan.description,
5736
+ ""
5737
+ ];
5738
+ if (plan.subtitle) {
5739
+ lines.push("## Subtitle", "", plan.subtitle, "");
5740
+ }
5741
+ if (plan.keywords && plan.keywords.length > 0) {
5742
+ lines.push("## Keywords", "", ...plan.keywords.map((kw) => `- ${kw}`), "");
5743
+ }
5744
+ if (plan.introBrief) {
5745
+ lines.push("## Introduction Brief", "", plan.introBrief, "");
5746
+ }
5747
+ if (plan.sections && plan.sections.length > 0) {
5748
+ lines.push("## Sections", "", "| # | Title | Description |", "|---|-------|-------------|");
5749
+ plan.sections.forEach((section, index) => {
5750
+ lines.push(`| ${index + 1} | ${section.title} | ${section.description} |`);
5751
+ });
5752
+ lines.push("");
5753
+ }
5754
+ if (plan.outroBrief) {
5755
+ lines.push("## Outro Brief", "", plan.outroBrief, "");
5756
+ }
5757
+ if (plan.angle) {
5758
+ lines.push("## Angle", "", plan.angle, "");
5759
+ }
5760
+ lines.push("## Image Plan", "");
5761
+ lines.push(`- **Cover:** ${plan.coverImageDescription}`);
5762
+ if (plan.inlineImages && plan.inlineImages.length > 0) {
5763
+ plan.inlineImages.forEach((img, index) => {
5764
+ lines.push(`- **Inline ${index + 1}:** ${img.description} (after section ${img.anchorAfterSection})`);
5765
+ });
5766
+ }
5767
+ lines.push("");
5768
+ return lines.join("\n");
5769
+ }
5770
+ function buildPlanSummary(plan) {
5771
+ const parts = [plan.title, plan.slug];
5772
+ if (plan.sections && plan.sections.length > 0) {
5773
+ parts.push(`${plan.sections.length} sections`);
5774
+ }
5775
+ if (plan.inlineImages && plan.inlineImages.length > 0) {
5776
+ parts.push(`${plan.inlineImages.length + 1} images`);
5777
+ } else {
5778
+ parts.push("1 image");
5779
+ }
5780
+ return parts.join(" \u2022 ");
5420
5781
  }
5421
5782
  function asWriteStageId(stageId) {
5422
5783
  if (stageId === "shared-plan" || stageId === "planning" || stageId === "sections" || stageId === "image-prompts" || stageId === "images" || stageId === "output" || stageId === "links") {
@@ -5445,7 +5806,7 @@ function parsePipelineCustomLinks(rawLinks, unlinks) {
5445
5806
 
5446
5807
  // src/cli/commands/links.ts
5447
5808
  import { readFile as readFile6, stat as stat3 } from "fs/promises";
5448
- import path9 from "path";
5809
+ import path10 from "path";
5449
5810
  async function runLinksCommand(options, dependencies = {}) {
5450
5811
  const slug = normalizeSlug2(options.slug);
5451
5812
  const mode = normalizeMode(options.mode);
@@ -5454,9 +5815,9 @@ async function runLinksCommand(options, dependencies = {}) {
5454
5815
  const resolved = await resolveRunInput({
5455
5816
  idea: `Enrich links for ${slug}`
5456
5817
  });
5457
- const markdownPath = await resolveMarkdownPathForSlug2(resolved.config.settings, slug, cwd2);
5818
+ const markdownPath = await resolveMarkdownPathForSlug2(slug, cwd2);
5458
5819
  const frontmatter = await readFrontmatter(markdownPath);
5459
- const fileId = path9.parse(markdownPath).name;
5820
+ const fileId = path10.parse(markdownPath).name;
5460
5821
  const articleTitle = frontmatter.title ?? toTitleFromSlug(slug);
5461
5822
  const articleDescription = frontmatter.description ?? "";
5462
5823
  const openRouterApiKey = resolved.config.secrets.openRouterApiKey;
@@ -5524,16 +5885,16 @@ function normalizeSlug2(rawSlug) {
5524
5885
  }
5525
5886
  return slug;
5526
5887
  }
5527
- async function resolveMarkdownPathForSlug2(settings, slug, cwd2) {
5528
- const outputPaths = resolveOutputPaths(settings, cwd2);
5529
- const directPath = path9.join(outputPaths.markdownOutputDir, `${slug}.md`);
5888
+ async function resolveMarkdownPathForSlug2(slug, cwd2) {
5889
+ const outputPaths = resolveOutputPaths();
5890
+ const directPath = path10.join(outputPaths.markdownOutputDir, `${slug}.md`);
5530
5891
  if (await isReadableFile(directPath)) {
5531
5892
  return directPath;
5532
5893
  }
5533
5894
  const markdownFiles = await listMarkdownFilesRecursively(outputPaths.markdownOutputDir);
5534
5895
  const matches = [];
5535
5896
  for (const candidate of markdownFiles) {
5536
- if (path9.basename(candidate) === `${slug}.md`) {
5897
+ if (path10.basename(candidate) === `${slug}.md`) {
5537
5898
  matches.push(candidate);
5538
5899
  continue;
5539
5900
  }
@@ -5667,7 +6028,7 @@ function readErrorCode2(error) {
5667
6028
  return typeof code === "string" ? code : null;
5668
6029
  }
5669
6030
  function formatRelativePath2(cwd2, targetPath) {
5670
- const relativePath = path9.relative(cwd2, targetPath);
6031
+ const relativePath = path10.relative(cwd2, targetPath);
5671
6032
  return relativePath.length > 0 ? relativePath : targetPath;
5672
6033
  }
5673
6034
  function logProgress(event, log) {
@@ -5706,8 +6067,8 @@ function resolveCustomLinks(existing, addRaw, removeExpressions) {
5706
6067
  }
5707
6068
 
5708
6069
  // src/cli/commands/export.ts
5709
- import { copyFile, mkdir as mkdir6, readFile as readFile8, stat as stat5, writeFile as writeFile6 } from "fs/promises";
5710
- import path11 from "path";
6070
+ import { copyFile as copyFile2, mkdir as mkdir7, readFile as readFile8, stat as stat5, writeFile as writeFile6 } from "fs/promises";
6071
+ import path12 from "path";
5711
6072
 
5712
6073
  // src/output/enrichMarkdownWithLinks.ts
5713
6074
  function enrichMarkdownWithLinks(markdown, links) {
@@ -5764,7 +6125,7 @@ function escapeRegExp(value2) {
5764
6125
 
5765
6126
  // src/server/previewHelpers.ts
5766
6127
  import { readdir, stat as stat4, readFile as readFile7 } from "fs/promises";
5767
- import path10 from "path";
6128
+ import path11 from "path";
5768
6129
  var DEFAULT_PORT = 4173;
5769
6130
  var CONTENT_TYPE_ORDER = ["article", "blog-post", "x-thread", "x-post", "linkedin-post", "reddit-post", "newsletter"];
5770
6131
  var FILE_PREFIX_TO_CONTENT_TYPE = {
@@ -5822,8 +6183,8 @@ function extractHeadingTitle(markdown) {
5822
6183
  }
5823
6184
  async function resolveMarkdownPath(markdownPathArg, markdownOutputDir, cwd2) {
5824
6185
  if (markdownPathArg) {
5825
- const resolved = path10.isAbsolute(markdownPathArg) ? markdownPathArg : path10.resolve(cwd2, markdownPathArg);
5826
- if (path10.extname(resolved).toLowerCase() !== ".md") {
6186
+ const resolved = path11.isAbsolute(markdownPathArg) ? markdownPathArg : path11.resolve(cwd2, markdownPathArg);
6187
+ if (path11.extname(resolved).toLowerCase() !== ".md") {
5827
6188
  throw new Error(`Expected a markdown file (.md), received: ${resolved}`);
5828
6189
  }
5829
6190
  await assertFileExists(resolved, "Could not find markdown file");
@@ -5867,7 +6228,7 @@ function extractCoverImageUrl(markdown) {
5867
6228
  async function extractArticleMetadata(markdownPath) {
5868
6229
  const markdown = await readFile7(markdownPath, "utf8");
5869
6230
  const fileStat = await stat4(markdownPath);
5870
- const slug = extractFrontmatterSlug(markdown) ?? path10.basename(markdownPath, ".md");
6231
+ const slug = extractFrontmatterSlug(markdown) ?? path11.basename(markdownPath, ".md");
5871
6232
  const title = extractHeadingTitle(stripFrontmatter2(markdown)) ?? slug;
5872
6233
  const body = stripFrontmatter2(markdown);
5873
6234
  const previewSnippet = body.replace(/[#\[\]()!\-*_`]/g, "").trim().substring(0, 150);
@@ -5932,16 +6293,16 @@ async function listAllGenerations(markdownOutputDir) {
5932
6293
  return generations;
5933
6294
  }
5934
6295
  function deriveGenerationId(markdownPath, markdownOutputDir) {
5935
- const relative = path10.relative(markdownOutputDir, markdownPath);
5936
- const normalized = relative.split(path10.sep).join("/");
6296
+ const relative = path11.relative(markdownOutputDir, markdownPath);
6297
+ const normalized = relative.split(path11.sep).join("/");
5937
6298
  if (!normalized || normalized.startsWith("../")) {
5938
- return path10.basename(markdownPath, ".md");
6299
+ return path11.basename(markdownPath, ".md");
5939
6300
  }
5940
6301
  const segments = normalized.split("/").filter(Boolean);
5941
6302
  if (segments.length <= 1) {
5942
- return path10.basename(markdownPath, ".md");
6303
+ return path11.basename(markdownPath, ".md");
5943
6304
  }
5944
- return segments[0] ?? path10.basename(markdownPath, ".md");
6305
+ return segments[0] ?? path11.basename(markdownPath, ".md");
5945
6306
  }
5946
6307
  async function findMarkdownFiles(markdownOutputDir) {
5947
6308
  const files = [];
@@ -5958,7 +6319,7 @@ async function findMarkdownFiles(markdownOutputDir) {
5958
6319
  continue;
5959
6320
  }
5960
6321
  for (const entry of entries) {
5961
- const fullPath = path10.join(current, entry.name);
6322
+ const fullPath = path11.join(current, entry.name);
5962
6323
  if (entry.isDirectory()) {
5963
6324
  stack.push(fullPath);
5964
6325
  continue;
@@ -5972,7 +6333,7 @@ async function findMarkdownFiles(markdownOutputDir) {
5972
6333
  }
5973
6334
  function deriveOutputIdentity(markdownPath, markdownOutputDir) {
5974
6335
  const generationId = deriveGenerationId(markdownPath, markdownOutputDir);
5975
- const fileBase = path10.basename(markdownPath, ".md");
6336
+ const fileBase = path11.basename(markdownPath, ".md");
5976
6337
  const parsed = fileBase.match(/^([a-z0-9-]+)-(\d+)$/i);
5977
6338
  if (!parsed || !parsed[1] || !parsed[2]) {
5978
6339
  return {
@@ -6008,11 +6369,11 @@ function toContentTypeLabel(contentType) {
6008
6369
  }
6009
6370
  async function resolvePrimaryContentType(outputs) {
6010
6371
  const fallback = outputs.find((output) => output.contentType === "article")?.contentType ?? outputs[0]?.contentType ?? "article";
6011
- const generationDir = path10.dirname(outputs[0]?.sourcePath ?? "");
6372
+ const generationDir = path11.dirname(outputs[0]?.sourcePath ?? "");
6012
6373
  if (!generationDir) {
6013
6374
  return fallback;
6014
6375
  }
6015
- const jobPath = path10.join(generationDir, "job.json");
6376
+ const jobPath = path11.join(generationDir, "job.json");
6016
6377
  try {
6017
6378
  const raw = await readFile7(jobPath, "utf8");
6018
6379
  const parsed = JSON.parse(raw);
@@ -6028,12 +6389,17 @@ async function resolvePrimaryContentType(outputs) {
6028
6389
  }
6029
6390
 
6030
6391
  // src/cli/commands/export.ts
6392
+ var INTERNAL_FILE_NAMES = /* @__PURE__ */ new Set([
6393
+ "job.json",
6394
+ "model.interactions.json",
6395
+ "generation.analytics.json"
6396
+ ]);
6031
6397
  async function runOutputCommand(options, dependencies = {}) {
6032
6398
  const cwd2 = dependencies.cwd ?? process.cwd();
6033
6399
  const log = dependencies.log ?? ((message) => console.log(message));
6034
6400
  const targetIndex = options.index ?? 1;
6035
6401
  const resolved = await resolveRunInput({ idea: `Export generation ${options.generationId}` });
6036
- const outputPaths = resolveOutputPaths(resolved.config.settings, cwd2);
6402
+ const outputPaths = resolveOutputPaths();
6037
6403
  const generations = await listAllGenerations(outputPaths.markdownOutputDir);
6038
6404
  const generation = resolveGeneration(generations, options.generationId);
6039
6405
  const articleOutputs = generation.outputs.filter((output) => output.contentType === generation.primaryContentType);
@@ -6051,44 +6417,36 @@ async function runOutputCommand(options, dependencies = {}) {
6051
6417
  }
6052
6418
  const sourceMarkdownPath = articleOutput.sourcePath;
6053
6419
  const sourceMarkdown = await readFile8(sourceMarkdownPath, "utf8");
6054
- const slug = extractFrontmatterSlug2(sourceMarkdown) ?? path11.basename(sourceMarkdownPath, ".md");
6420
+ const slug = extractFrontmatterSlug2(sourceMarkdown) ?? path12.basename(sourceMarkdownPath, ".md");
6055
6421
  const exportFilename = `${slug}.md`;
6056
6422
  const destinationDir = await resolveDestinationDir(options.destinationPath, cwd2);
6057
- const destinationFilePath = path11.join(destinationDir, exportFilename);
6423
+ const destinationFilePath = path12.join(destinationDir, exportFilename);
6058
6424
  if (!options.overwrite && await fileExists2(destinationFilePath)) {
6059
6425
  throw new ReportedError(
6060
6426
  `Export file already exists: ${destinationFilePath}. Pass --overwrite to replace it.`
6061
6427
  );
6062
6428
  }
6063
- await mkdir6(destinationDir, { recursive: true });
6429
+ await mkdir7(destinationDir, { recursive: true });
6064
6430
  const links = await loadLinks(sourceMarkdownPath);
6065
6431
  const enrichedMarkdown = enrichWithFrontmatterGuard(sourceMarkdown, links);
6066
- const sourceDir = path11.dirname(sourceMarkdownPath);
6067
- const imagePaths = extractLocalImagePaths(sourceMarkdown);
6068
- const copiedImages = [];
6069
- for (const relImagePath of imagePaths) {
6070
- const absoluteImageSrc = path11.resolve(sourceDir, relImagePath);
6071
- let imageStat = null;
6072
- try {
6073
- imageStat = await stat5(absoluteImageSrc);
6074
- } catch {
6075
- throw new ReportedError(
6076
- `Referenced image not found: ${relImagePath} (resolved to ${absoluteImageSrc}).`
6077
- );
6078
- }
6079
- if (!imageStat.isFile()) {
6080
- throw new ReportedError(`Referenced image path is not a file: ${absoluteImageSrc}.`);
6081
- }
6082
- const destImagePath = path11.join(destinationDir, relImagePath);
6083
- await mkdir6(path11.dirname(destImagePath), { recursive: true });
6084
- await copyFile(absoluteImageSrc, destImagePath);
6085
- copiedImages.push(relImagePath);
6432
+ const sourceDir = path12.dirname(sourceMarkdownPath);
6433
+ const allFiles = await listFilesRecursively(sourceDir, () => true);
6434
+ const copiedFiles = [];
6435
+ for (const absoluteSrc of allFiles) {
6436
+ const basename = path12.basename(absoluteSrc);
6437
+ if (INTERNAL_FILE_NAMES.has(basename)) continue;
6438
+ if (path12.resolve(absoluteSrc) === path12.resolve(sourceMarkdownPath)) continue;
6439
+ const relativePath = path12.relative(sourceDir, absoluteSrc);
6440
+ const destPath = path12.join(destinationDir, relativePath);
6441
+ await mkdir7(path12.dirname(destPath), { recursive: true });
6442
+ await copyFile2(absoluteSrc, destPath);
6443
+ copiedFiles.push(relativePath);
6086
6444
  }
6087
6445
  await writeFile6(destinationFilePath, enrichedMarkdown, "utf8");
6088
- const relDest = path11.relative(cwd2, destinationFilePath);
6446
+ const relDest = path12.relative(cwd2, destinationFilePath);
6089
6447
  log(`Exported "${generation.id}" (${generation.primaryContentType} #${targetIndex}) \u2192 ${relDest}`);
6090
- if (copiedImages.length > 0) {
6091
- log(`Copied ${copiedImages.length} image${copiedImages.length === 1 ? "" : "s"}: ${copiedImages.join(", ")}`);
6448
+ if (copiedFiles.length > 0) {
6449
+ log(`Copied ${copiedFiles.length} file${copiedFiles.length === 1 ? "" : "s"}: ${copiedFiles.join(", ")}`);
6092
6450
  }
6093
6451
  if (links.length > 0) {
6094
6452
  log(`Injected ${links.length} inline link${links.length === 1 ? "" : "s"}.`);
@@ -6110,7 +6468,7 @@ function resolveGeneration(generations, generationId) {
6110
6468
  );
6111
6469
  }
6112
6470
  async function resolveDestinationDir(destinationPath, cwd2) {
6113
- const resolved = path11.isAbsolute(destinationPath) ? destinationPath : path11.resolve(cwd2, destinationPath);
6471
+ const resolved = path12.isAbsolute(destinationPath) ? destinationPath : path12.resolve(cwd2, destinationPath);
6114
6472
  return resolved;
6115
6473
  }
6116
6474
  async function fileExists2(filePath) {
@@ -6180,22 +6538,6 @@ function extractFrontmatterSlug2(markdown) {
6180
6538
  const unquoted = rawSlug.replace(/^['""]|['""]$/g, "").trim();
6181
6539
  return unquoted.length > 0 ? unquoted : null;
6182
6540
  }
6183
- function extractLocalImagePaths(markdown) {
6184
- const imagePattern = /!\[[^\]]*\]\(([^)]+)\)/g;
6185
- const paths = [];
6186
- let match;
6187
- while ((match = imagePattern.exec(markdown)) !== null) {
6188
- const rawPath = match[1]?.trim();
6189
- if (!rawPath) {
6190
- continue;
6191
- }
6192
- if (rawPath.startsWith("http://") || rawPath.startsWith("https://") || rawPath.startsWith("data:") || rawPath.startsWith("/") || rawPath.startsWith("#")) {
6193
- continue;
6194
- }
6195
- paths.push(rawPath);
6196
- }
6197
- return paths;
6198
- }
6199
6541
 
6200
6542
  // src/cli/commands/writeTargetSpecs.ts
6201
6543
  function parseTargetSpec(spec) {
@@ -6642,12 +6984,6 @@ import { Box, Text, useApp, useInput } from "ink";
6642
6984
  import SelectInput from "ink-select-input";
6643
6985
  import TextInput from "ink-text-input";
6644
6986
 
6645
- // src/images/limnModelCatalog.ts
6646
- import { getSupportedModelCatalog } from "@telepat/limn";
6647
- function getLimnGenerationModels() {
6648
- return getSupportedModelCatalog().filter((entry) => entry.generationEnabled);
6649
- }
6650
-
6651
6987
  // src/cli/flows/settingsFlowLogic.ts
6652
6988
  function handleMenuSelect(action, settings, secrets, setEditing, setShowModelSelect, setMenuMode, onDone, exit) {
6653
6989
  switch (action) {
@@ -6672,12 +7008,6 @@ function handleMenuSelect(action, settings, secrets, setEditing, setShowModelSel
6672
7008
  case "topP":
6673
7009
  setEditing({ key: action, label: "Top p", value: String(settings.modelSettings.topP) });
6674
7010
  return;
6675
- case "markdownOutputDir":
6676
- setEditing({ key: action, label: "Markdown output directory", value: settings.markdownOutputDir });
6677
- return;
6678
- case "assetOutputDir":
6679
- setEditing({ key: action, label: "Asset output directory", value: settings.assetOutputDir });
6680
- return;
6681
7011
  case "t2i-settings":
6682
7012
  setMenuMode("t2i");
6683
7013
  return;
@@ -6691,6 +7021,13 @@ function handleMenuSelect(action, settings, secrets, setEditing, setShowModelSel
6691
7021
  value: JSON.stringify(settings.t2i.inputOverrides, null, 2)
6692
7022
  });
6693
7023
  return;
7024
+ case "t2i-replicate-model-id":
7025
+ setEditing({
7026
+ key: action,
7027
+ label: "T2I Replicate model ID override (blank to clear)",
7028
+ value: settings.t2i.replicateModelId ?? ""
7029
+ });
7030
+ return;
6694
7031
  case "t2i-back":
6695
7032
  setMenuMode("main");
6696
7033
  return;
@@ -6761,14 +7098,6 @@ function applyEdit(action, value2, settings, secrets, setSettings, setSecrets) {
6761
7098
  });
6762
7099
  return true;
6763
7100
  }
6764
- if (action === "markdownOutputDir") {
6765
- setSettings({ ...settings, markdownOutputDir: value2.trim() || settings.markdownOutputDir });
6766
- return true;
6767
- }
6768
- if (action === "assetOutputDir") {
6769
- setSettings({ ...settings, assetOutputDir: value2.trim() || settings.assetOutputDir });
6770
- return true;
6771
- }
6772
7101
  if (action === "t2i-input-overrides") {
6773
7102
  const trimmed = value2.trim();
6774
7103
  if (trimmed.length === 0) {
@@ -6798,6 +7127,17 @@ function applyEdit(action, value2, settings, secrets, setSettings, setSecrets) {
6798
7127
  return false;
6799
7128
  }
6800
7129
  }
7130
+ if (action === "t2i-replicate-model-id") {
7131
+ const trimmed = value2.trim();
7132
+ setSettings({
7133
+ ...settings,
7134
+ t2i: {
7135
+ ...settings.t2i,
7136
+ replicateModelId: trimmed.length > 0 ? trimmed : void 0
7137
+ }
7138
+ });
7139
+ return true;
7140
+ }
6801
7141
  return false;
6802
7142
  }
6803
7143
  function parseNumberOrFallback(value2, fallback) {
@@ -6852,12 +7192,19 @@ function SettingsFlow({ initialSettings, initialSecrets, onDone }) {
6852
7192
  const count = Object.keys(overrides).length;
6853
7193
  return count === 0 ? "none" : `${count} override${count === 1 ? "" : "s"}`;
6854
7194
  };
7195
+ const formatReplicateOverrideSummary = (replicateModelId) => {
7196
+ return replicateModelId && replicateModelId.length > 0 ? replicateModelId : "auto";
7197
+ };
6855
7198
  const menuItems = useMemo(() => {
6856
7199
  const t2iSubmenu = [
6857
7200
  {
6858
7201
  label: `T2I model: ${currentModelEntry?.displayName ?? settings.t2i.modelId}`,
6859
7202
  value: "t2i-model"
6860
7203
  },
7204
+ {
7205
+ label: `T2I Replicate model override: ${formatReplicateOverrideSummary(settings.t2i.replicateModelId)}`,
7206
+ value: "t2i-replicate-model-id"
7207
+ },
6861
7208
  {
6862
7209
  label: `T2I input overrides: ${formatT2iOverridesSummary(settings.t2i.inputOverrides)}`,
6863
7210
  value: "t2i-input-overrides"
@@ -6899,14 +7246,6 @@ function SettingsFlow({ initialSettings, initialSecrets, onDone }) {
6899
7246
  label: `Top p: ${settings.modelSettings.topP}`,
6900
7247
  value: "topP"
6901
7248
  },
6902
- {
6903
- label: `Markdown output directory: ${settings.markdownOutputDir}`,
6904
- value: "markdownOutputDir"
6905
- },
6906
- {
6907
- label: `Asset output directory: ${settings.assetOutputDir}`,
6908
- value: "assetOutputDir"
6909
- },
6910
7249
  {
6911
7250
  label: `T2I settings: ${currentModelEntry?.displayName ?? settings.t2i.modelId}`,
6912
7251
  value: "t2i-settings"
@@ -6938,6 +7277,7 @@ function SettingsFlow({ initialSettings, initialSecrets, onDone }) {
6938
7277
  ...current,
6939
7278
  t2i: {
6940
7279
  modelId: item.value,
7280
+ replicateModelId: current.t2i.replicateModelId && isReplicateModelIdForFamily(item.value, current.t2i.replicateModelId) ? current.t2i.replicateModelId : void 0,
6941
7281
  inputOverrides: {}
6942
7282
  }
6943
7283
  }));
@@ -7059,7 +7399,7 @@ async function openSettings() {
7059
7399
  }
7060
7400
 
7061
7401
  // src/cli/commands/serve.ts
7062
- import path13 from "path";
7402
+ import path14 from "path";
7063
7403
  import { spawn } from "child_process";
7064
7404
 
7065
7405
  // src/server/previewServer.ts
@@ -7067,7 +7407,7 @@ import { execFile } from "child_process";
7067
7407
  import { promisify } from "util";
7068
7408
  import { readFile as readFile9, stat as stat6 } from "fs/promises";
7069
7409
  import { watch as fsWatch } from "fs";
7070
- import path12 from "path";
7410
+ import path13 from "path";
7071
7411
  import { fileURLToPath } from "url";
7072
7412
  import express from "express";
7073
7413
  import { marked } from "marked";
@@ -7165,7 +7505,7 @@ async function startPreviewServer(options) {
7165
7505
  if (options.watch) {
7166
7506
  let html2;
7167
7507
  try {
7168
- html2 = await readFile9(path12.join(previewClientDir, "index.html"), "utf8");
7508
+ html2 = await readFile9(path13.join(previewClientDir, "index.html"), "utf8");
7169
7509
  } catch {
7170
7510
  res.status(200).type("html").send(
7171
7511
  `<!doctype html><html><head><meta charset="utf-8"><title>Rebuilding\u2026</title><style>body{margin:0;display:flex;align-items:center;justify-content:center;height:100vh;font-family:sans-serif;background:#101820;color:#e0eaf0}p{font-size:15px;opacity:.7}</style></head><body><p>Rebuilding\u2026</p><script>const s=new EventSource('/api/__reload');s.onmessage=function(){location.reload()};</script></body></html>`
@@ -7176,7 +7516,7 @@ async function startPreviewServer(options) {
7176
7516
  const injected = html2.replace("</body>", `${reloadScript}</body>`);
7177
7517
  res.status(200).type("html").send(injected);
7178
7518
  } else {
7179
- res.status(200).sendFile(path12.join(previewClientDir, "index.html"));
7519
+ res.status(200).sendFile(path13.join(previewClientDir, "index.html"));
7180
7520
  }
7181
7521
  return;
7182
7522
  }
@@ -7247,15 +7587,17 @@ async function getArticleContent(generationId, markdownOutputDir) {
7247
7587
  };
7248
7588
  })
7249
7589
  );
7250
- const generationDir = path12.dirname(generation.outputs[0]?.sourcePath ?? "");
7590
+ const generationDir = path13.dirname(generation.outputs[0]?.sourcePath ?? "");
7251
7591
  const interactions = generationDir ? await loadSavedInteractions(generationDir) : { llmCalls: [], t2iCalls: [] };
7252
7592
  const analyticsSummary = generationDir ? await loadSavedAnalyticsSummary(generationDir) : null;
7593
+ const metaJson = generationDir ? await loadSavedMetaJson(generationDir) : null;
7253
7594
  return {
7254
7595
  title: generation.title,
7255
7596
  generationId: generation.id,
7256
7597
  sourcePath,
7257
7598
  interactions,
7258
7599
  analyticsSummary,
7600
+ metaJson,
7259
7601
  outputs
7260
7602
  };
7261
7603
  }
@@ -7276,7 +7618,7 @@ async function resolveActivePreviewArticle(preferredMarkdownPath, markdownOutput
7276
7618
  };
7277
7619
  }
7278
7620
  function resolveGenerationSourcePath(generation, markdownOutputDir) {
7279
- return generation.outputs.find((output) => output.contentType === generation.primaryContentType)?.sourcePath ?? generation.outputs[0]?.sourcePath ?? path12.join(markdownOutputDir, generation.id);
7621
+ return generation.outputs.find((output) => output.contentType === generation.primaryContentType)?.sourcePath ?? generation.outputs[0]?.sourcePath ?? path13.join(markdownOutputDir, generation.id);
7280
7622
  }
7281
7623
  function isMissingFileError(error) {
7282
7624
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
@@ -7315,7 +7657,7 @@ async function loadSavedLinks(markdownPath) {
7315
7657
  }
7316
7658
  }
7317
7659
  async function loadSavedInteractions(generationDir) {
7318
- const interactionsPath = path12.join(generationDir, "model.interactions.json");
7660
+ const interactionsPath = path13.join(generationDir, "model.interactions.json");
7319
7661
  try {
7320
7662
  const raw = await readFile9(interactionsPath, "utf8");
7321
7663
  const parsed = JSON.parse(raw);
@@ -7333,7 +7675,7 @@ async function loadSavedInteractions(generationDir) {
7333
7675
  }
7334
7676
  }
7335
7677
  async function loadSavedAnalyticsSummary(generationDir) {
7336
- const analyticsPath = path12.join(generationDir, "generation.analytics.json");
7678
+ const analyticsPath = path13.join(generationDir, "generation.analytics.json");
7337
7679
  try {
7338
7680
  const raw = await readFile9(analyticsPath, "utf8");
7339
7681
  const parsed = JSON.parse(raw);
@@ -7356,6 +7698,15 @@ async function loadSavedAnalyticsSummary(generationDir) {
7356
7698
  return null;
7357
7699
  }
7358
7700
  }
7701
+ async function loadSavedMetaJson(generationDir) {
7702
+ const metaJsonPath = path13.join(generationDir, "meta.json");
7703
+ try {
7704
+ const raw = await readFile9(metaJsonPath, "utf8");
7705
+ return JSON.parse(raw);
7706
+ } catch {
7707
+ return null;
7708
+ }
7709
+ }
7359
7710
  async function getPreviewBootstrapData(preferredMarkdownPath, markdownOutputDir) {
7360
7711
  const activeArticle = await resolveActivePreviewArticle(preferredMarkdownPath, markdownOutputDir);
7361
7712
  const emptyStateMessage = activeArticle ? null : `No generated content found in ${markdownOutputDir}. Run ideon write "your idea" first.`;
@@ -7367,14 +7718,14 @@ async function getPreviewBootstrapData(preferredMarkdownPath, markdownOutputDir)
7367
7718
  };
7368
7719
  }
7369
7720
  async function resolvePreviewClientBuildDir() {
7370
- const currentDir = path12.dirname(fileURLToPath(import.meta.url));
7721
+ const currentDir = path13.dirname(fileURLToPath(import.meta.url));
7371
7722
  const candidates = [
7372
- path12.resolve(currentDir, "preview"),
7373
- path12.resolve(currentDir, "../../dist/preview")
7723
+ path13.resolve(currentDir, "preview"),
7724
+ path13.resolve(currentDir, "../../dist/preview")
7374
7725
  ];
7375
7726
  for (const candidate of candidates) {
7376
7727
  try {
7377
- const indexStat = await stat6(path12.join(candidate, "index.html"));
7728
+ const indexStat = await stat6(path13.join(candidate, "index.html"));
7378
7729
  if (indexStat.isFile()) {
7379
7730
  return candidate;
7380
7731
  }
@@ -7436,17 +7787,17 @@ async function resolveGenerationAssetPath(generationId, rawAssetPath, markdownOu
7436
7787
  throw new MissingArticleError(`Generation "${generationId}" no longer exists.`);
7437
7788
  }
7438
7789
  const decodedAssetPath = decodeURIComponent(rawAssetPath);
7439
- const normalizedRelative = path12.posix.normalize(decodedAssetPath.replace(/\\/g, "/"));
7440
- if (normalizedRelative.length === 0 || normalizedRelative === "." || normalizedRelative.startsWith("../") || normalizedRelative.includes("/../") || path12.posix.isAbsolute(normalizedRelative)) {
7790
+ const normalizedRelative = path13.posix.normalize(decodedAssetPath.replace(/\\/g, "/"));
7791
+ if (normalizedRelative.length === 0 || normalizedRelative === "." || normalizedRelative.startsWith("../") || normalizedRelative.includes("/../") || path13.posix.isAbsolute(normalizedRelative)) {
7441
7792
  throw new Error("Invalid generation asset path.");
7442
7793
  }
7443
- const generationDir = path12.dirname(generation.outputs[0]?.sourcePath ?? "");
7794
+ const generationDir = path13.dirname(generation.outputs[0]?.sourcePath ?? "");
7444
7795
  if (!generationDir) {
7445
7796
  throw new MissingArticleError(`Generation "${generationId}" has no source directory.`);
7446
7797
  }
7447
- const resolvedPath = path12.resolve(generationDir, normalizedRelative);
7448
- const relativeToGeneration = path12.relative(generationDir, resolvedPath);
7449
- if (relativeToGeneration.startsWith("..") || path12.isAbsolute(relativeToGeneration)) {
7798
+ const resolvedPath = path13.resolve(generationDir, normalizedRelative);
7799
+ const relativeToGeneration = path13.relative(generationDir, resolvedPath);
7800
+ if (relativeToGeneration.startsWith("..") || path13.isAbsolute(relativeToGeneration)) {
7450
7801
  throw new Error("Invalid generation asset path.");
7451
7802
  }
7452
7803
  try {
@@ -8920,17 +9271,11 @@ function escapeHtml(value2) {
8920
9271
 
8921
9272
  // src/cli/commands/serve.ts
8922
9273
  async function runServeCommand(options) {
8923
- const [savedSettings, envSettings] = await Promise.all([loadSavedSettings(), Promise.resolve(readEnvSettings())]);
8924
- const mergedSettings = appSettingsSchema.parse({
8925
- ...savedSettings,
8926
- ...envSettings.markdownOutputDir ? { markdownOutputDir: envSettings.markdownOutputDir } : {},
8927
- ...envSettings.assetOutputDir ? { assetOutputDir: envSettings.assetOutputDir } : {}
8928
- });
8929
- const outputPaths = resolveOutputPaths(mergedSettings);
9274
+ const outputPaths = resolveOutputPaths();
8930
9275
  const markdownPath = await resolveMarkdownPath(options.markdownPath, outputPaths.markdownOutputDir, process.cwd());
8931
9276
  const port = parsePort(options.port);
8932
9277
  if (options.watch) {
8933
- const viteBin = path13.resolve(process.cwd(), "node_modules", ".bin", "vite");
9278
+ const viteBin = path14.resolve(process.cwd(), "node_modules", ".bin", "vite");
8934
9279
  const viteProcess = spawn(viteBin, ["build", "--watch"], {
8935
9280
  stdio: "inherit",
8936
9281
  shell: process.platform === "win32"
@@ -8956,8 +9301,8 @@ async function runServeCommand(options) {
8956
9301
  openBrowser: options.openBrowser,
8957
9302
  watch: options.watch
8958
9303
  });
8959
- const relativeArticle = path13.relative(process.cwd(), markdownPath);
8960
- const relativeAssets = path13.relative(process.cwd(), outputPaths.assetOutputDir);
9304
+ const relativeArticle = path14.relative(process.cwd(), markdownPath);
9305
+ const relativeAssets = path14.relative(process.cwd(), outputPaths.assetOutputDir);
8961
9306
  console.log(`Previewing ${relativeArticle || markdownPath}`);
8962
9307
  console.log(`Serving assets from ${relativeAssets || outputPaths.assetOutputDir}`);
8963
9308
  console.log(`Open ${server.url}`);
@@ -9507,6 +9852,7 @@ async function renderPlainPipeline(input, dryRun, enrichLinks2, runMode, links,
9507
9852
  title: result.artifact.title,
9508
9853
  slug: result.artifact.slug
9509
9854
  });
9855
+ return result;
9510
9856
  } catch (error) {
9511
9857
  const message = error instanceof Error ? withWriteResumeHint(error.message) : withWriteResumeHint("Pipeline failed.");
9512
9858
  await notifyWriteFailed({
@@ -9823,13 +10169,12 @@ function WriteApp({
9823
10169
  unlinks,
9824
10170
  maxLinks,
9825
10171
  maxImages,
10172
+ onSuccess,
9826
10173
  onError
9827
10174
  }) {
9828
10175
  const { exit } = useApp3();
9829
10176
  const [stages, setStages] = useState4(
9830
- () => createInitialStages({
9831
- isArticlePrimary: input.config.settings.contentTargets.some((target) => target.role === "primary" && target.contentType === "article")
9832
- })
10177
+ () => createInitialStages()
9833
10178
  );
9834
10179
  const [result, setResult] = useState4(null);
9835
10180
  const [errorMessage, setErrorMessage] = useState4(null);
@@ -9860,6 +10205,7 @@ function WriteApp({
9860
10205
  return;
9861
10206
  }
9862
10207
  setResult(runResult);
10208
+ onSuccess?.(runResult);
9863
10209
  await notifyWriteSucceeded({
9864
10210
  enabled: input.config.settings.notifications.enabled,
9865
10211
  title: runResult.artifact.title,
@@ -9898,7 +10244,7 @@ function WriteApp({
9898
10244
  }
9899
10245
  async function runWriteCommand(options) {
9900
10246
  const input = await resolveInputWithInteractiveIdeaFallback(options);
9901
- await runWritePipeline(input, options.dryRun, options.enrichLinks, "fresh", options.noInteractive, options.links, options.unlinks, options.maxLinks, options.maxImages);
10247
+ await runWritePipeline(input, options.dryRun, options.enrichLinks, "fresh", options.noInteractive, options.links, options.unlinks, options.maxLinks, options.maxImages, options.exportPath);
9902
10248
  }
9903
10249
  async function runWriteResumeCommand(options = {}) {
9904
10250
  const session = await loadWriteSession();
@@ -9920,9 +10266,9 @@ async function runWriteResumeCommand(options = {}) {
9920
10266
  secrets: resolved.config.secrets
9921
10267
  }
9922
10268
  };
9923
- await runWritePipeline(input, session.dryRun, options.enrichLinks ?? false, "resume", options.noInteractive ?? false, options.links, options.unlinks, options.maxLinks, options.maxImages);
10269
+ await runWritePipeline(input, session.dryRun, options.enrichLinks ?? false, "resume", options.noInteractive ?? false, options.links, options.unlinks, options.maxLinks, options.maxImages, options.exportPath);
9924
10270
  }
9925
- async function runWritePipeline(input, dryRun, enrichLinks2, runMode, noInteractive, links, unlinks, maxLinks, maxImages) {
10271
+ async function runWritePipeline(input, dryRun, enrichLinks2, runMode, noInteractive, links, unlinks, maxLinks, maxImages, exportPath) {
9926
10272
  let interruptHandled = false;
9927
10273
  const handleSignal = (signal) => {
9928
10274
  if (interruptHandled) {
@@ -9956,10 +10302,17 @@ async function runWritePipeline(input, dryRun, enrichLinks2, runMode, noInteract
9956
10302
  process.on("SIGTERM", onSigterm);
9957
10303
  try {
9958
10304
  if (noInteractive || !process.stdout.isTTY) {
9959
- await renderPlainPipeline(input, dryRun, enrichLinks2, runMode, links, unlinks, maxLinks, maxImages);
10305
+ const result = await renderPlainPipeline(input, dryRun, enrichLinks2, runMode, links, unlinks, maxLinks, maxImages);
10306
+ if (exportPath) {
10307
+ await runOutputCommand({
10308
+ generationId: result.artifact.slug,
10309
+ destinationPath: exportPath
10310
+ });
10311
+ }
9960
10312
  return;
9961
10313
  }
9962
10314
  let commandError = null;
10315
+ let pipelineResult = null;
9963
10316
  const app = render2(
9964
10317
  /* @__PURE__ */ jsx7(
9965
10318
  WriteApp,
@@ -9972,6 +10325,9 @@ async function runWritePipeline(input, dryRun, enrichLinks2, runMode, noInteract
9972
10325
  unlinks,
9973
10326
  maxLinks,
9974
10327
  maxImages,
10328
+ onSuccess: (result) => {
10329
+ pipelineResult = result;
10330
+ },
9975
10331
  onError: (error) => {
9976
10332
  commandError = error;
9977
10333
  }
@@ -9984,6 +10340,9 @@ async function runWritePipeline(input, dryRun, enrichLinks2, runMode, noInteract
9984
10340
  if (finalError) {
9985
10341
  throw new ReportedError(withWriteResumeHint(finalError.message));
9986
10342
  }
10343
+ if (exportPath && pipelineResult) {
10344
+ await autoExport(exportPath, pipelineResult);
10345
+ }
9987
10346
  } finally {
9988
10347
  cleanupSignalHandlers();
9989
10348
  }
@@ -10131,6 +10490,12 @@ async function promptForIdea() {
10131
10490
  readline.close();
10132
10491
  }
10133
10492
  }
10493
+ async function autoExport(exportPath, result) {
10494
+ await runOutputCommand({
10495
+ generationId: result.artifact.slug,
10496
+ destinationPath: exportPath
10497
+ });
10498
+ }
10134
10499
 
10135
10500
  // src/cli/app.ts
10136
10501
  var { version } = package_default;
@@ -10200,7 +10565,7 @@ async function runCli(argv) {
10200
10565
  watch: options.watch
10201
10566
  });
10202
10567
  });
10203
- 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) => {
10568
+ 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)).option("--export <path>", "Export the generated article to the given directory after writing").action(async (ideaArg, options) => {
10204
10569
  await runWriteCommand({
10205
10570
  idea: options.idea ?? ideaArg,
10206
10571
  audience: options.audience,
@@ -10216,17 +10581,19 @@ async function runCli(argv) {
10216
10581
  links: options.link,
10217
10582
  unlinks: options.unlink,
10218
10583
  maxLinks: options.maxLinks,
10219
- maxImages: options.maxImages
10584
+ maxImages: options.maxImages,
10585
+ exportPath: options.export
10220
10586
  });
10221
10587
  });
10222
- 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) => {
10588
+ 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)).option("--export <path>", "Export the generated article to the given directory after writing").action(async (options) => {
10223
10589
  await runWriteResumeCommand({
10224
10590
  noInteractive: options.noInteractive,
10225
10591
  enrichLinks: options.enrichLinks,
10226
10592
  links: options.link,
10227
10593
  unlinks: options.unlink,
10228
10594
  maxLinks: options.maxLinks,
10229
- maxImages: options.maxImages
10595
+ maxImages: options.maxImages,
10596
+ exportPath: options.export
10230
10597
  });
10231
10598
  });
10232
10599
  await program.parseAsync(argv);