@telepat/ideon 0.1.24 → 0.1.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ideon.js CHANGED
@@ -12,7 +12,8 @@ import { access, mkdir, writeFile } from "fs/promises";
12
12
  import os from "os";
13
13
  import path from "path";
14
14
  function resolveOutputPaths() {
15
- const base = path.join(os.homedir(), ".ideon", "output");
15
+ const ideonHome = process.env.IDEON_HOME || os.homedir();
16
+ const base = path.join(ideonHome, ".ideon", "output");
16
17
  return {
17
18
  markdownOutputDir: base,
18
19
  assetOutputDir: path.join(base, "assets")
@@ -484,9 +485,9 @@ function resolveDefaultMaxLinks(targetLengthWords) {
484
485
  }
485
486
  function resolveDefaultInlineImageCount(targetLengthWords) {
486
487
  const alias = resolveTargetLengthAlias(targetLengthWords);
487
- if (alias === "small") return { min: 1, max: 2 };
488
- if (alias === "medium") return { min: 2, max: 3 };
489
- return { min: 3, max: 4 };
488
+ if (alias === "small") return { min: 0, max: 1 };
489
+ if (alias === "medium") return { min: 1, max: 2 };
490
+ return { min: 2, max: 4 };
490
491
  }
491
492
  var contentTargetRoleValues = ["primary", "secondary"];
492
493
  var contentTargetSchema = z2.object({
@@ -1423,7 +1424,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
1423
1424
  // package.json
1424
1425
  var package_default = {
1425
1426
  name: "@telepat/ideon",
1426
- version: "0.1.24",
1427
+ version: "0.1.27",
1427
1428
  description: "CLI for generating rich articles and images from ideas.",
1428
1429
  type: "module",
1429
1430
  repository: {
@@ -1623,9 +1624,9 @@ function assertNoLegacyXMode(contentTargets, sourceLabel) {
1623
1624
  }
1624
1625
 
1625
1626
  // src/pipeline/runner.ts
1626
- import { mkdir as mkdir6, stat as stat2 } from "fs/promises";
1627
+ import { mkdir as mkdir6, readFile as readFile6, stat as stat2 } from "fs/promises";
1627
1628
  import { randomUUID } from "crypto";
1628
- import path8 from "path";
1629
+ import path9 from "path";
1629
1630
 
1630
1631
  // src/generation/enrichLinks.ts
1631
1632
  import { readFile as readFile4 } from "fs/promises";
@@ -2446,7 +2447,7 @@ function buildLongFormPlanMessages(idea, options) {
2446
2447
  "- Each section description should name the mechanism, evidence type, or practical action that makes the section useful.",
2447
2448
  "- Sections are primary-only structure and must not be treated as requirements for non-primary channels.",
2448
2449
  `- 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, 1-based index). Choose sections where visual reinforcement adds the most value.",
2450
+ "- Each inline image must specify which section it follows (anchorAfterSection, starting at 1). Choose sections where visual reinforcement adds the most value.",
2450
2451
  "- 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.",
2451
2452
  "- Image descriptions must be concrete and contextual, not generic stock-photo phrasing.",
2452
2453
  "",
@@ -2468,7 +2469,7 @@ function buildLongFormPlanMessages(idea, options) {
2468
2469
  "- outroBrief: string",
2469
2470
  `- sections: array of ${sectionCounts.label} objects, each with title and description strings`,
2470
2471
  "- coverImageDescription: string",
2471
- `- inlineImages: array of ${imageCounts.min} to ${imageCounts.max} objects, each with a description string and an anchorAfterSection number (1-based section index)`,
2472
+ `- inlineImages: array of ${imageCounts.min} to ${imageCounts.max} objects, each with a description string and an anchorAfterSection number (starting at 1).`,
2472
2473
  "",
2473
2474
  "Do not omit any required fields. Return strict JSON only."
2474
2475
  ].join("\n")
@@ -2543,7 +2544,7 @@ var primaryPlanSchema = z5.object({
2543
2544
  introBrief: z5.string().min(1).optional(),
2544
2545
  outroBrief: z5.string().min(1).optional(),
2545
2546
  sections: z5.array(articleSectionPlanSchema).min(2).max(10).optional(),
2546
- inlineImages: z5.array(inlineImagePlanSchema).min(2).max(3).optional(),
2547
+ inlineImages: z5.array(inlineImagePlanSchema).min(0).max(4).optional(),
2547
2548
  angle: z5.string().min(1).optional()
2548
2549
  });
2549
2550
  var longFormPlanSchema = z5.object({
@@ -2557,7 +2558,7 @@ var longFormPlanSchema = z5.object({
2557
2558
  outroBrief: z5.string().min(1),
2558
2559
  sections: z5.array(articleSectionPlanSchema).min(2).max(10),
2559
2560
  coverImageDescription: z5.string().min(1),
2560
- inlineImages: z5.array(inlineImagePlanSchema).min(2).max(3)
2561
+ inlineImages: z5.array(inlineImagePlanSchema).min(0).max(4)
2561
2562
  });
2562
2563
  var shortFormPlanSchema = z5.object({
2563
2564
  contentType: z5.string().min(1),
@@ -4003,10 +4004,69 @@ ${body.join("\n").trim()}
4003
4004
  `;
4004
4005
  }
4005
4006
 
4007
+ // src/output/meta.ts
4008
+ import path7 from "path";
4009
+ function buildMetaJson(input) {
4010
+ const plan = input.plan;
4011
+ const contentPlan = input.contentPlan;
4012
+ const generationDir = input.generationDir;
4013
+ const title = plan?.title ?? contentPlan?.title ?? input.idea;
4014
+ const slug = plan?.slug ?? "";
4015
+ const description = plan?.description ?? contentPlan?.description ?? "";
4016
+ const subtitle = (plan && "subtitle" in plan ? plan.subtitle : null) ?? null;
4017
+ const keywords = (plan && "keywords" in plan ? plan.keywords : null) ?? [];
4018
+ const contentType = plan?.contentType ?? contentPlan?.primaryContentType ?? "article";
4019
+ const angle = plan?.angle ?? null;
4020
+ const coverImage = input.renderedImages.find((image) => image.kind === "cover") ?? null;
4021
+ const cover = coverImage ? {
4022
+ path: coverImage.outputPath,
4023
+ relativePath: coverImage.relativePath,
4024
+ description: coverImage.description
4025
+ } : null;
4026
+ const sections = plan?.sections?.map((section) => ({
4027
+ title: section.title,
4028
+ description: section.description
4029
+ })) ?? [];
4030
+ const images = input.renderedImages.map((image) => ({
4031
+ id: image.id,
4032
+ kind: image.kind,
4033
+ path: image.outputPath,
4034
+ relativePath: image.relativePath,
4035
+ description: image.description,
4036
+ anchorAfterSection: image.anchorAfterSection
4037
+ }));
4038
+ const outputs = input.outputs.map((output) => ({
4039
+ fileId: output.fileId,
4040
+ contentType: output.contentType,
4041
+ path: output.markdownPath,
4042
+ relativePath: path7.relative(generationDir, output.markdownPath)
4043
+ }));
4044
+ return {
4045
+ version: 1,
4046
+ title,
4047
+ slug,
4048
+ idea: input.idea,
4049
+ description,
4050
+ subtitle,
4051
+ keywords,
4052
+ contentType,
4053
+ style: input.style,
4054
+ intent: input.intent,
4055
+ targetLength: input.targetLength,
4056
+ angle,
4057
+ cover,
4058
+ sections,
4059
+ images,
4060
+ outputs,
4061
+ generatedAt: input.generatedAt,
4062
+ generationDir
4063
+ };
4064
+ }
4065
+
4006
4066
  // src/pipeline/sessionStore.ts
4007
4067
  import { createHash as createHash2 } from "crypto";
4008
4068
  import { mkdir as mkdir5, readFile as readFile5, rm as rm2, writeFile as writeFile5 } from "fs/promises";
4009
- import path7 from "path";
4069
+ import path8 from "path";
4010
4070
  import envPaths3 from "env-paths";
4011
4071
  import { z as z6 } from "zod";
4012
4072
  var STAGE_IDS = ["shared-plan", "planning", "sections", "image-prompts", "images", "output", "links"];
@@ -4049,7 +4109,8 @@ var pipelineArtifactSummarySchema = z6.object({
4049
4109
  assetDir: z6.string().min(1),
4050
4110
  analyticsPath: z6.string().min(1).default("unknown.analytics.json"),
4051
4111
  interactionsPath: z6.string().min(1).default("unknown.interactions.json"),
4052
- planPath: z6.string().min(1).nullable().default(null)
4112
+ planPath: z6.string().min(1).nullable().default(null),
4113
+ metaJsonPath: z6.string().min(1).default("meta.json")
4053
4114
  });
4054
4115
  var resolvedPathsSchema = z6.object({
4055
4116
  markdownOutputDir: z6.string().min(1),
@@ -4085,18 +4146,18 @@ var writeSessionStateSchema = z6.object({
4085
4146
  artifact: pipelineArtifactSummarySchema.nullable()
4086
4147
  });
4087
4148
  var ideonPaths3 = envPaths3("ideon", { suffix: "" });
4088
- var sessionsDir = path7.join(ideonPaths3.config, "sessions");
4149
+ var sessionsDir = path8.join(ideonPaths3.config, "sessions");
4089
4150
  function hashProjectPath(workingDir) {
4090
- return createHash2("sha256").update(path7.resolve(workingDir)).digest("hex").slice(0, 16);
4151
+ return createHash2("sha256").update(path8.resolve(workingDir)).digest("hex").slice(0, 16);
4091
4152
  }
4092
4153
  function resolveWriteRoot(workingDir) {
4093
- return path7.join(sessionsDir, hashProjectPath(workingDir));
4154
+ return path8.join(sessionsDir, hashProjectPath(workingDir));
4094
4155
  }
4095
4156
  function resolveStateFilePath(workingDir) {
4096
- return path7.join(resolveWriteRoot(workingDir), "state.json");
4157
+ return path8.join(resolveWriteRoot(workingDir), "state.json");
4097
4158
  }
4098
4159
  function resolveLegacyStatePath(workingDir) {
4099
- return path7.join(workingDir, ".ideon", "write", "state.json");
4160
+ return path8.join(workingDir, ".ideon", "write", "state.json");
4100
4161
  }
4101
4162
  async function startFreshWriteSession(seed, workingDir = process.cwd()) {
4102
4163
  const writeRoot = resolveWriteRoot(workingDir);
@@ -4157,7 +4218,7 @@ async function saveWriteSession(state, workingDir = process.cwd()) {
4157
4218
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
4158
4219
  });
4159
4220
  const statePath = resolveStateFilePath(workingDir);
4160
- await mkdir5(path7.dirname(statePath), { recursive: true });
4221
+ await mkdir5(path8.dirname(statePath), { recursive: true });
4161
4222
  await writeFile5(statePath, `${JSON.stringify(next, null, 2)}
4162
4223
  `, "utf8");
4163
4224
  return next;
@@ -4716,12 +4777,12 @@ async function runPipelineShell(input, options = {}) {
4716
4777
  options.onUpdate?.(cloneStages(stages));
4717
4778
  }
4718
4779
  const baseSlug = plan?.slug ?? resolveGenerationSlug(input.idea, contentPlan?.title);
4719
- const generationDir = path8.join(
4780
+ const generationDir = path9.join(
4720
4781
  writeSession.outputPaths.markdownOutputDir,
4721
4782
  buildGenerationDirectoryName(baseSlug)
4722
4783
  );
4723
4784
  await mkdir6(generationDir, { recursive: true });
4724
- const jobDefinitionPath = path8.join(generationDir, "job.json");
4785
+ const jobDefinitionPath = path9.join(generationDir, "job.json");
4725
4786
  await writeJsonFile(
4726
4787
  jobDefinitionPath,
4727
4788
  buildRunJobDefinition({
@@ -4733,12 +4794,12 @@ async function runPipelineShell(input, options = {}) {
4733
4794
  sourceJob: input.job
4734
4795
  })
4735
4796
  );
4736
- const planPath = plan ? path8.join(generationDir, "plan.md") : null;
4797
+ const planPath = plan ? path9.join(generationDir, "plan.md") : null;
4737
4798
  if (plan && planPath) {
4738
4799
  await writeUtf8File(planPath, renderPlanMarkdown(plan));
4739
4800
  }
4740
4801
  const primaryFilePrefix = toFilePrefix(primaryTarget.contentType);
4741
- const primaryMarkdownPath = path8.join(generationDir, `${primaryFilePrefix}-1.md`);
4802
+ const primaryMarkdownPath = path9.join(generationDir, `${primaryFilePrefix}-1.md`);
4742
4803
  const sharedAssetDir = generationDir;
4743
4804
  if (isLongForm) {
4744
4805
  const longPlan = plan;
@@ -4962,7 +5023,7 @@ async function runPipelineShell(input, options = {}) {
4962
5023
  })
4963
5024
  };
4964
5025
  options.onUpdate?.(cloneStages(stages));
4965
- const markdownPath = path8.join(generationDir, `${output.filePrefix}-${output.index}.md`);
5026
+ const markdownPath = path9.join(generationDir, `${output.filePrefix}-${output.index}.md`);
4966
5027
  try {
4967
5028
  const content = await writeSingleShotContent({
4968
5029
  idea: input.idea,
@@ -5025,7 +5086,7 @@ async function runPipelineShell(input, options = {}) {
5025
5086
  ...item,
5026
5087
  status: "succeeded",
5027
5088
  detail: "Saved markdown output.",
5028
- summary: path8.basename(markdownPath),
5089
+ summary: path9.basename(markdownPath),
5029
5090
  analytics: {
5030
5091
  durationMs: itemDurationMs,
5031
5092
  costUsd: knownItemCost.usd,
@@ -5077,15 +5138,47 @@ async function runPipelineShell(input, options = {}) {
5077
5138
  markStageStarted(stageTracking, "links");
5078
5139
  options.onUpdate?.(cloneStages(stages));
5079
5140
  if (!shouldEnrichLinks) {
5080
- markStageCompleted(stageTracking, "links");
5081
- stages[6] = {
5082
- ...stages[6],
5083
- status: "succeeded",
5084
- detail: "Skipped link enrichment (enable with --enrich-links).",
5085
- summary: "Link enrichment disabled for this run",
5086
- stageAnalytics: snapshotStageAnalytics(stageTracking, "links")
5087
- };
5088
- options.onUpdate?.(cloneStages(stages));
5141
+ const customLinkActions = pipelineCustomLinkRaws.length > 0 || pipelineUnlinks.length > 0;
5142
+ if (customLinkActions && eligibleOutputsForLinks.length > 0) {
5143
+ for (const output of eligibleOutputsForLinks) {
5144
+ const existingLinks = await readExistingLinks(resolveLinksPath(output.markdownPath));
5145
+ const mergedCustomLinks = resolvePipelineCustomLinks(
5146
+ existingLinks?.customLinks ?? [],
5147
+ pipelineCustomLinkRaws,
5148
+ pipelineUnlinks
5149
+ );
5150
+ const generatedLinks = existingLinks?.links ?? [];
5151
+ await writeLinksFile(output.markdownPath, {
5152
+ version: 2,
5153
+ customLinks: mergedCustomLinks,
5154
+ links: generatedLinks
5155
+ });
5156
+ }
5157
+ markStageCompleted(stageTracking, "links");
5158
+ stages[6] = {
5159
+ ...stages[6],
5160
+ status: "succeeded",
5161
+ detail: "Updated custom links without generating new links.",
5162
+ summary: `${eligibleOutputsForLinks.length} files updated`,
5163
+ items: (stages[6].items ?? []).map((stageItem) => ({
5164
+ ...stageItem,
5165
+ status: "succeeded",
5166
+ detail: "Saved custom links sidecar."
5167
+ })),
5168
+ stageAnalytics: snapshotStageAnalytics(stageTracking, "links")
5169
+ };
5170
+ options.onUpdate?.(cloneStages(stages));
5171
+ } else {
5172
+ markStageCompleted(stageTracking, "links");
5173
+ stages[6] = {
5174
+ ...stages[6],
5175
+ status: "succeeded",
5176
+ detail: "Skipped link enrichment (enable with --enrich-links).",
5177
+ summary: "Link enrichment disabled for this run",
5178
+ stageAnalytics: snapshotStageAnalytics(stageTracking, "links")
5179
+ };
5180
+ options.onUpdate?.(cloneStages(stages));
5181
+ }
5089
5182
  } else if (eligibleOutputsForLinks.length === 0) {
5090
5183
  markStageCompleted(stageTracking, "links");
5091
5184
  stages[6] = {
@@ -5240,10 +5333,24 @@ async function runPipelineShell(input, options = {}) {
5240
5333
  llmCalls: llmInteractions,
5241
5334
  t2iCalls: t2iInteractions
5242
5335
  };
5243
- const analyticsPath = path8.join(generationDir, "generation.analytics.json");
5244
- const interactionsPath = path8.join(generationDir, "model.interactions.json");
5336
+ const analyticsPath = path9.join(generationDir, "generation.analytics.json");
5337
+ const interactionsPath = path9.join(generationDir, "model.interactions.json");
5245
5338
  await writeJsonFile(analyticsPath, analytics);
5246
5339
  await writeJsonFile(interactionsPath, interactions);
5340
+ const metaJson = buildMetaJson({
5341
+ idea: input.idea,
5342
+ generationDir,
5343
+ contentPlan,
5344
+ plan,
5345
+ renderedImages: imageArtifacts?.renderedImages ?? [],
5346
+ outputs: generatedOutputs,
5347
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
5348
+ style: input.config.settings.style,
5349
+ intent: input.config.settings.intent,
5350
+ targetLength: input.config.settings.targetLength ? resolveTargetLengthAlias(input.config.settings.targetLength) : null
5351
+ });
5352
+ const metaJsonPath = path9.join(generationDir, "meta.json");
5353
+ await writeJsonFile(metaJsonPath, metaJson);
5247
5354
  const primaryMarkdownPathForArtifact = markdownPaths[0] ?? primaryMarkdownPath;
5248
5355
  const artifact = {
5249
5356
  title: plan?.title ?? contentPlan.title ?? deriveTitleFromIdea2(input.idea),
@@ -5257,7 +5364,8 @@ async function runPipelineShell(input, options = {}) {
5257
5364
  assetDir: sharedAssetDir,
5258
5365
  analyticsPath,
5259
5366
  interactionsPath,
5260
- planPath
5367
+ planPath,
5368
+ metaJsonPath
5261
5369
  };
5262
5370
  writeSession = await patchWriteSession(
5263
5371
  {
@@ -5728,10 +5836,62 @@ function parsePipelineCustomLinks(rawLinks, unlinks) {
5728
5836
  }
5729
5837
  return Array.from(result.values());
5730
5838
  }
5839
+ function resolvePipelineCustomLinks(existing, rawLinks, unlinks) {
5840
+ const result = new Map(
5841
+ existing.map((entry) => [entry.expression.trim().toLowerCase(), entry])
5842
+ );
5843
+ for (const raw of rawLinks) {
5844
+ const separatorIndex = raw.indexOf("->");
5845
+ if (separatorIndex < 0) {
5846
+ continue;
5847
+ }
5848
+ const expression = raw.slice(0, separatorIndex).trim();
5849
+ const url = raw.slice(separatorIndex + 2).trim();
5850
+ if (expression && url) {
5851
+ result.set(expression.toLowerCase(), { expression, url, title: null });
5852
+ }
5853
+ }
5854
+ for (const expr of unlinks) {
5855
+ result.delete(expr.trim().toLowerCase());
5856
+ }
5857
+ return Array.from(result.values());
5858
+ }
5859
+ async function readExistingLinks(linksPath) {
5860
+ try {
5861
+ const raw = await readFile6(linksPath, "utf8");
5862
+ const parsed = JSON.parse(raw);
5863
+ const links = Array.isArray(parsed.links) ? parsed.links.filter((entry) => isValidLinkEntry(entry)).map((entry) => ({
5864
+ expression: entry.expression.trim(),
5865
+ url: entry.url.trim(),
5866
+ title: typeof entry.title === "string" ? entry.title : null
5867
+ })) : null;
5868
+ if (!links) {
5869
+ throw new Error(`Invalid links sidecar format at ${linksPath}. Expected { version, links[] }.`);
5870
+ }
5871
+ const customLinks = Array.isArray(parsed.customLinks) ? parsed.customLinks.filter((entry) => isValidLinkEntry(entry)).map((entry) => ({
5872
+ expression: entry.expression.trim(),
5873
+ url: entry.url.trim(),
5874
+ title: typeof entry.title === "string" ? entry.title : null
5875
+ })) : [];
5876
+ return {
5877
+ version: typeof parsed.version === "number" ? parsed.version : 2,
5878
+ customLinks,
5879
+ links
5880
+ };
5881
+ } catch (error) {
5882
+ if (error.code === "ENOENT") {
5883
+ return null;
5884
+ }
5885
+ throw error;
5886
+ }
5887
+ }
5888
+ function isValidLinkEntry(entry) {
5889
+ return typeof entry === "object" && entry !== null && typeof entry.expression === "string" && typeof entry.url === "string";
5890
+ }
5731
5891
 
5732
5892
  // src/cli/commands/links.ts
5733
- import { readFile as readFile6, stat as stat3 } from "fs/promises";
5734
- import path9 from "path";
5893
+ import { readFile as readFile7, stat as stat3 } from "fs/promises";
5894
+ import path10 from "path";
5735
5895
  async function runLinksCommand(options, dependencies = {}) {
5736
5896
  const slug = normalizeSlug2(options.slug);
5737
5897
  const mode = normalizeMode(options.mode);
@@ -5742,7 +5902,7 @@ async function runLinksCommand(options, dependencies = {}) {
5742
5902
  });
5743
5903
  const markdownPath = await resolveMarkdownPathForSlug2(slug, cwd2);
5744
5904
  const frontmatter = await readFrontmatter(markdownPath);
5745
- const fileId = path9.parse(markdownPath).name;
5905
+ const fileId = path10.parse(markdownPath).name;
5746
5906
  const articleTitle = frontmatter.title ?? toTitleFromSlug(slug);
5747
5907
  const articleDescription = frontmatter.description ?? "";
5748
5908
  const openRouterApiKey = resolved.config.secrets.openRouterApiKey;
@@ -5753,7 +5913,7 @@ async function runLinksCommand(options, dependencies = {}) {
5753
5913
  }
5754
5914
  const openRouter = new OpenRouterClient(openRouterApiKey);
5755
5915
  const linksPath = resolveLinksPath(markdownPath);
5756
- const existing = await readExistingLinks(linksPath);
5916
+ const existing = await readExistingLinks2(linksPath);
5757
5917
  const updatedCustomLinks = resolveCustomLinks(existing?.customLinks ?? [], options.links ?? [], options.unlinks ?? []);
5758
5918
  const effectiveMaxLinks = options.maxLinks;
5759
5919
  const linksResult = await enrichLinks({
@@ -5812,14 +5972,14 @@ function normalizeSlug2(rawSlug) {
5812
5972
  }
5813
5973
  async function resolveMarkdownPathForSlug2(slug, cwd2) {
5814
5974
  const outputPaths = resolveOutputPaths();
5815
- const directPath = path9.join(outputPaths.markdownOutputDir, `${slug}.md`);
5975
+ const directPath = path10.join(outputPaths.markdownOutputDir, `${slug}.md`);
5816
5976
  if (await isReadableFile(directPath)) {
5817
5977
  return directPath;
5818
5978
  }
5819
5979
  const markdownFiles = await listMarkdownFilesRecursively(outputPaths.markdownOutputDir);
5820
5980
  const matches = [];
5821
5981
  for (const candidate of markdownFiles) {
5822
- if (path9.basename(candidate) === `${slug}.md`) {
5982
+ if (path10.basename(candidate) === `${slug}.md`) {
5823
5983
  matches.push(candidate);
5824
5984
  continue;
5825
5985
  }
@@ -5848,7 +6008,7 @@ async function newestPath(paths) {
5848
6008
  return latestPath;
5849
6009
  }
5850
6010
  async function readFrontmatter(markdownPath) {
5851
- const markdown = await readFile6(markdownPath, "utf8");
6011
+ const markdown = await readFile7(markdownPath, "utf8");
5852
6012
  return parseFrontmatter(markdown);
5853
6013
  }
5854
6014
  function parseFrontmatter(markdown) {
@@ -5892,11 +6052,11 @@ async function isReadableFile(filePath) {
5892
6052
  return false;
5893
6053
  }
5894
6054
  }
5895
- async function readExistingLinks(linksPath) {
6055
+ async function readExistingLinks2(linksPath) {
5896
6056
  try {
5897
- const raw = await readFile6(linksPath, "utf8");
6057
+ const raw = await readFile7(linksPath, "utf8");
5898
6058
  const parsed = JSON.parse(raw);
5899
- const links = Array.isArray(parsed.links) ? parsed.links.filter((entry) => isValidLinkEntry(entry)).map((entry) => ({
6059
+ const links = Array.isArray(parsed.links) ? parsed.links.filter((entry) => isValidLinkEntry2(entry)).map((entry) => ({
5900
6060
  expression: entry.expression.trim(),
5901
6061
  url: entry.url.trim(),
5902
6062
  title: typeof entry.title === "string" ? entry.title : null
@@ -5904,7 +6064,7 @@ async function readExistingLinks(linksPath) {
5904
6064
  if (!links) {
5905
6065
  throw new ReportedError(`Invalid links sidecar format at ${linksPath}. Expected { version, links[] }.`);
5906
6066
  }
5907
- const customLinks = Array.isArray(parsed.customLinks) ? parsed.customLinks.filter((entry) => isValidLinkEntry(entry)).map((entry) => ({
6067
+ const customLinks = Array.isArray(parsed.customLinks) ? parsed.customLinks.filter((entry) => isValidLinkEntry2(entry)).map((entry) => ({
5908
6068
  expression: entry.expression.trim(),
5909
6069
  url: entry.url.trim(),
5910
6070
  title: typeof entry.title === "string" ? entry.title : null
@@ -5938,7 +6098,7 @@ function mergeLinks(existingLinks, generatedLinks) {
5938
6098
  }
5939
6099
  return merged;
5940
6100
  }
5941
- function isValidLinkEntry(value2) {
6101
+ function isValidLinkEntry2(value2) {
5942
6102
  if (typeof value2 !== "object" || value2 === null) {
5943
6103
  return false;
5944
6104
  }
@@ -5953,7 +6113,7 @@ function readErrorCode2(error) {
5953
6113
  return typeof code === "string" ? code : null;
5954
6114
  }
5955
6115
  function formatRelativePath2(cwd2, targetPath) {
5956
- const relativePath = path9.relative(cwd2, targetPath);
6116
+ const relativePath = path10.relative(cwd2, targetPath);
5957
6117
  return relativePath.length > 0 ? relativePath : targetPath;
5958
6118
  }
5959
6119
  function logProgress(event, log) {
@@ -5992,8 +6152,8 @@ function resolveCustomLinks(existing, addRaw, removeExpressions) {
5992
6152
  }
5993
6153
 
5994
6154
  // src/cli/commands/export.ts
5995
- import { copyFile as copyFile2, mkdir as mkdir7, readFile as readFile8, stat as stat5, writeFile as writeFile6 } from "fs/promises";
5996
- import path11 from "path";
6155
+ import { copyFile as copyFile2, mkdir as mkdir7, readFile as readFile9, stat as stat5, writeFile as writeFile6 } from "fs/promises";
6156
+ import path12 from "path";
5997
6157
 
5998
6158
  // src/output/enrichMarkdownWithLinks.ts
5999
6159
  function enrichMarkdownWithLinks(markdown, links) {
@@ -6049,8 +6209,8 @@ function escapeRegExp(value2) {
6049
6209
  }
6050
6210
 
6051
6211
  // src/server/previewHelpers.ts
6052
- import { readdir, stat as stat4, readFile as readFile7 } from "fs/promises";
6053
- import path10 from "path";
6212
+ import { readdir, stat as stat4, readFile as readFile8 } from "fs/promises";
6213
+ import path11 from "path";
6054
6214
  var DEFAULT_PORT = 4173;
6055
6215
  var CONTENT_TYPE_ORDER = ["article", "blog-post", "x-thread", "x-post", "linkedin-post", "reddit-post", "newsletter"];
6056
6216
  var FILE_PREFIX_TO_CONTENT_TYPE = {
@@ -6108,8 +6268,8 @@ function extractHeadingTitle(markdown) {
6108
6268
  }
6109
6269
  async function resolveMarkdownPath(markdownPathArg, markdownOutputDir, cwd2) {
6110
6270
  if (markdownPathArg) {
6111
- const resolved = path10.isAbsolute(markdownPathArg) ? markdownPathArg : path10.resolve(cwd2, markdownPathArg);
6112
- if (path10.extname(resolved).toLowerCase() !== ".md") {
6271
+ const resolved = path11.isAbsolute(markdownPathArg) ? markdownPathArg : path11.resolve(cwd2, markdownPathArg);
6272
+ if (path11.extname(resolved).toLowerCase() !== ".md") {
6113
6273
  throw new Error(`Expected a markdown file (.md), received: ${resolved}`);
6114
6274
  }
6115
6275
  await assertFileExists(resolved, "Could not find markdown file");
@@ -6151,9 +6311,9 @@ function extractCoverImageUrl(markdown) {
6151
6311
  return match?.[1] ?? null;
6152
6312
  }
6153
6313
  async function extractArticleMetadata(markdownPath) {
6154
- const markdown = await readFile7(markdownPath, "utf8");
6314
+ const markdown = await readFile8(markdownPath, "utf8");
6155
6315
  const fileStat = await stat4(markdownPath);
6156
- const slug = extractFrontmatterSlug(markdown) ?? path10.basename(markdownPath, ".md");
6316
+ const slug = extractFrontmatterSlug(markdown) ?? path11.basename(markdownPath, ".md");
6157
6317
  const title = extractHeadingTitle(stripFrontmatter2(markdown)) ?? slug;
6158
6318
  const body = stripFrontmatter2(markdown);
6159
6319
  const previewSnippet = body.replace(/[#\[\]()!\-*_`]/g, "").trim().substring(0, 150);
@@ -6168,7 +6328,7 @@ async function extractArticleMetadata(markdownPath) {
6168
6328
  }
6169
6329
  async function listAllGenerations(markdownOutputDir) {
6170
6330
  const markdownFiles = await findMarkdownFiles(markdownOutputDir);
6171
- const grouped = /* @__PURE__ */ new Map();
6331
+ const outputMap = /* @__PURE__ */ new Map();
6172
6332
  for (const filePath of markdownFiles) {
6173
6333
  try {
6174
6334
  const metadata = await extractArticleMetadata(filePath);
@@ -6186,15 +6346,23 @@ async function listAllGenerations(markdownOutputDir) {
6186
6346
  contentTypeLabel: toContentTypeLabel(identity.contentType),
6187
6347
  index: identity.index
6188
6348
  };
6189
- const existing = grouped.get(identity.generationId);
6190
- if (existing) {
6191
- existing.push(output);
6192
- } else {
6193
- grouped.set(identity.generationId, [output]);
6349
+ const outputKey = `${output.generationId}:${output.contentType}:${output.index}`;
6350
+ const existing = outputMap.get(outputKey);
6351
+ if (!existing || output.mtime > existing.mtime) {
6352
+ outputMap.set(outputKey, output);
6194
6353
  }
6195
6354
  } catch {
6196
6355
  }
6197
6356
  }
6357
+ const grouped = /* @__PURE__ */ new Map();
6358
+ for (const output of outputMap.values()) {
6359
+ const existing = grouped.get(output.generationId);
6360
+ if (existing) {
6361
+ existing.push(output);
6362
+ } else {
6363
+ grouped.set(output.generationId, [output]);
6364
+ }
6365
+ }
6198
6366
  const generations = [];
6199
6367
  for (const [id, outputs] of grouped.entries()) {
6200
6368
  outputs.sort((a, b) => compareContentTypes(a.contentType, b.contentType) || a.index - b.index || b.mtime - a.mtime);
@@ -6218,16 +6386,16 @@ async function listAllGenerations(markdownOutputDir) {
6218
6386
  return generations;
6219
6387
  }
6220
6388
  function deriveGenerationId(markdownPath, markdownOutputDir) {
6221
- const relative = path10.relative(markdownOutputDir, markdownPath);
6222
- const normalized = relative.split(path10.sep).join("/");
6389
+ const relative = path11.relative(markdownOutputDir, markdownPath);
6390
+ const normalized = relative.split(path11.sep).join("/");
6223
6391
  if (!normalized || normalized.startsWith("../")) {
6224
- return path10.basename(markdownPath, ".md");
6392
+ return path11.basename(markdownPath, ".md");
6225
6393
  }
6226
6394
  const segments = normalized.split("/").filter(Boolean);
6227
6395
  if (segments.length <= 1) {
6228
- return path10.basename(markdownPath, ".md");
6396
+ return path11.basename(markdownPath, ".md");
6229
6397
  }
6230
- return segments[0] ?? path10.basename(markdownPath, ".md");
6398
+ return segments[0] ?? path11.basename(markdownPath, ".md");
6231
6399
  }
6232
6400
  async function findMarkdownFiles(markdownOutputDir) {
6233
6401
  const files = [];
@@ -6244,7 +6412,7 @@ async function findMarkdownFiles(markdownOutputDir) {
6244
6412
  continue;
6245
6413
  }
6246
6414
  for (const entry of entries) {
6247
- const fullPath = path10.join(current, entry.name);
6415
+ const fullPath = path11.join(current, entry.name);
6248
6416
  if (entry.isDirectory()) {
6249
6417
  stack.push(fullPath);
6250
6418
  continue;
@@ -6258,7 +6426,7 @@ async function findMarkdownFiles(markdownOutputDir) {
6258
6426
  }
6259
6427
  function deriveOutputIdentity(markdownPath, markdownOutputDir) {
6260
6428
  const generationId = deriveGenerationId(markdownPath, markdownOutputDir);
6261
- const fileBase = path10.basename(markdownPath, ".md");
6429
+ const fileBase = path11.basename(markdownPath, ".md");
6262
6430
  const parsed = fileBase.match(/^([a-z0-9-]+)-(\d+)$/i);
6263
6431
  if (!parsed || !parsed[1] || !parsed[2]) {
6264
6432
  return {
@@ -6294,13 +6462,13 @@ function toContentTypeLabel(contentType) {
6294
6462
  }
6295
6463
  async function resolvePrimaryContentType(outputs) {
6296
6464
  const fallback = outputs.find((output) => output.contentType === "article")?.contentType ?? outputs[0]?.contentType ?? "article";
6297
- const generationDir = path10.dirname(outputs[0]?.sourcePath ?? "");
6465
+ const generationDir = path11.dirname(outputs[0]?.sourcePath ?? "");
6298
6466
  if (!generationDir) {
6299
6467
  return fallback;
6300
6468
  }
6301
- const jobPath = path10.join(generationDir, "job.json");
6469
+ const jobPath = path11.join(generationDir, "job.json");
6302
6470
  try {
6303
- const raw = await readFile7(jobPath, "utf8");
6471
+ const raw = await readFile8(jobPath, "utf8");
6304
6472
  const parsed = JSON.parse(raw);
6305
6473
  const targets = Array.isArray(parsed.contentTargets) ? parsed.contentTargets : Array.isArray(parsed.settings?.contentTargets) ? parsed.settings.contentTargets : [];
6306
6474
  const primary = targets.find((target) => target?.role === "primary" && typeof target.contentType === "string");
@@ -6314,6 +6482,11 @@ async function resolvePrimaryContentType(outputs) {
6314
6482
  }
6315
6483
 
6316
6484
  // src/cli/commands/export.ts
6485
+ var INTERNAL_FILE_NAMES = /* @__PURE__ */ new Set([
6486
+ "job.json",
6487
+ "model.interactions.json",
6488
+ "generation.analytics.json"
6489
+ ]);
6317
6490
  async function runOutputCommand(options, dependencies = {}) {
6318
6491
  const cwd2 = dependencies.cwd ?? process.cwd();
6319
6492
  const log = dependencies.log ?? ((message) => console.log(message));
@@ -6336,11 +6509,11 @@ async function runOutputCommand(options, dependencies = {}) {
6336
6509
  );
6337
6510
  }
6338
6511
  const sourceMarkdownPath = articleOutput.sourcePath;
6339
- const sourceMarkdown = await readFile8(sourceMarkdownPath, "utf8");
6340
- const slug = extractFrontmatterSlug2(sourceMarkdown) ?? path11.basename(sourceMarkdownPath, ".md");
6512
+ const sourceMarkdown = await readFile9(sourceMarkdownPath, "utf8");
6513
+ const slug = extractFrontmatterSlug2(sourceMarkdown) ?? path12.basename(sourceMarkdownPath, ".md");
6341
6514
  const exportFilename = `${slug}.md`;
6342
6515
  const destinationDir = await resolveDestinationDir(options.destinationPath, cwd2);
6343
- const destinationFilePath = path11.join(destinationDir, exportFilename);
6516
+ const destinationFilePath = path12.join(destinationDir, exportFilename);
6344
6517
  if (!options.overwrite && await fileExists2(destinationFilePath)) {
6345
6518
  throw new ReportedError(
6346
6519
  `Export file already exists: ${destinationFilePath}. Pass --overwrite to replace it.`
@@ -6349,32 +6522,24 @@ async function runOutputCommand(options, dependencies = {}) {
6349
6522
  await mkdir7(destinationDir, { recursive: true });
6350
6523
  const links = await loadLinks(sourceMarkdownPath);
6351
6524
  const enrichedMarkdown = enrichWithFrontmatterGuard(sourceMarkdown, links);
6352
- const sourceDir = path11.dirname(sourceMarkdownPath);
6353
- const imagePaths = extractLocalImagePaths(sourceMarkdown);
6354
- const copiedImages = [];
6355
- for (const relImagePath of imagePaths) {
6356
- const absoluteImageSrc = path11.resolve(sourceDir, relImagePath);
6357
- let imageStat = null;
6358
- try {
6359
- imageStat = await stat5(absoluteImageSrc);
6360
- } catch {
6361
- throw new ReportedError(
6362
- `Referenced image not found: ${relImagePath} (resolved to ${absoluteImageSrc}).`
6363
- );
6364
- }
6365
- if (!imageStat.isFile()) {
6366
- throw new ReportedError(`Referenced image path is not a file: ${absoluteImageSrc}.`);
6367
- }
6368
- const destImagePath = path11.join(destinationDir, relImagePath);
6369
- await mkdir7(path11.dirname(destImagePath), { recursive: true });
6370
- await copyFile2(absoluteImageSrc, destImagePath);
6371
- copiedImages.push(relImagePath);
6525
+ const sourceDir = path12.dirname(sourceMarkdownPath);
6526
+ const allFiles = await listFilesRecursively(sourceDir, () => true);
6527
+ const copiedFiles = [];
6528
+ for (const absoluteSrc of allFiles) {
6529
+ const basename = path12.basename(absoluteSrc);
6530
+ if (INTERNAL_FILE_NAMES.has(basename)) continue;
6531
+ if (path12.resolve(absoluteSrc) === path12.resolve(sourceMarkdownPath)) continue;
6532
+ const relativePath = path12.relative(sourceDir, absoluteSrc);
6533
+ const destPath = path12.join(destinationDir, relativePath);
6534
+ await mkdir7(path12.dirname(destPath), { recursive: true });
6535
+ await copyFile2(absoluteSrc, destPath);
6536
+ copiedFiles.push(relativePath);
6372
6537
  }
6373
6538
  await writeFile6(destinationFilePath, enrichedMarkdown, "utf8");
6374
- const relDest = path11.relative(cwd2, destinationFilePath);
6539
+ const relDest = path12.relative(cwd2, destinationFilePath);
6375
6540
  log(`Exported "${generation.id}" (${generation.primaryContentType} #${targetIndex}) \u2192 ${relDest}`);
6376
- if (copiedImages.length > 0) {
6377
- log(`Copied ${copiedImages.length} image${copiedImages.length === 1 ? "" : "s"}: ${copiedImages.join(", ")}`);
6541
+ if (copiedFiles.length > 0) {
6542
+ log(`Copied ${copiedFiles.length} file${copiedFiles.length === 1 ? "" : "s"}: ${copiedFiles.join(", ")}`);
6378
6543
  }
6379
6544
  if (links.length > 0) {
6380
6545
  log(`Injected ${links.length} inline link${links.length === 1 ? "" : "s"}.`);
@@ -6396,7 +6561,7 @@ function resolveGeneration(generations, generationId) {
6396
6561
  );
6397
6562
  }
6398
6563
  async function resolveDestinationDir(destinationPath, cwd2) {
6399
- const resolved = path11.isAbsolute(destinationPath) ? destinationPath : path11.resolve(cwd2, destinationPath);
6564
+ const resolved = path12.isAbsolute(destinationPath) ? destinationPath : path12.resolve(cwd2, destinationPath);
6400
6565
  return resolved;
6401
6566
  }
6402
6567
  async function fileExists2(filePath) {
@@ -6411,7 +6576,7 @@ async function loadLinks(markdownPath) {
6411
6576
  const linksPath = resolveLinksPath(markdownPath);
6412
6577
  let raw;
6413
6578
  try {
6414
- raw = await readFile8(linksPath, "utf8");
6579
+ raw = await readFile9(linksPath, "utf8");
6415
6580
  } catch {
6416
6581
  return [];
6417
6582
  }
@@ -6466,22 +6631,6 @@ function extractFrontmatterSlug2(markdown) {
6466
6631
  const unquoted = rawSlug.replace(/^['""]|['""]$/g, "").trim();
6467
6632
  return unquoted.length > 0 ? unquoted : null;
6468
6633
  }
6469
- function extractLocalImagePaths(markdown) {
6470
- const imagePattern = /!\[[^\]]*\]\(([^)]+)\)/g;
6471
- const paths = [];
6472
- let match;
6473
- while ((match = imagePattern.exec(markdown)) !== null) {
6474
- const rawPath = match[1]?.trim();
6475
- if (!rawPath) {
6476
- continue;
6477
- }
6478
- if (rawPath.startsWith("http://") || rawPath.startsWith("https://") || rawPath.startsWith("data:") || rawPath.startsWith("/") || rawPath.startsWith("#")) {
6479
- continue;
6480
- }
6481
- paths.push(rawPath);
6482
- }
6483
- return paths;
6484
- }
6485
6634
 
6486
6635
  // src/cli/commands/writeTargetSpecs.ts
6487
6636
  function parseTargetSpec(spec) {
@@ -7343,15 +7492,15 @@ async function openSettings() {
7343
7492
  }
7344
7493
 
7345
7494
  // src/cli/commands/serve.ts
7346
- import path13 from "path";
7495
+ import path14 from "path";
7347
7496
  import { spawn } from "child_process";
7348
7497
 
7349
7498
  // src/server/previewServer.ts
7350
7499
  import { execFile } from "child_process";
7351
7500
  import { promisify } from "util";
7352
- import { readFile as readFile9, stat as stat6 } from "fs/promises";
7501
+ import { readFile as readFile10, stat as stat6 } from "fs/promises";
7353
7502
  import { watch as fsWatch } from "fs";
7354
- import path12 from "path";
7503
+ import path13 from "path";
7355
7504
  import { fileURLToPath } from "url";
7356
7505
  import express from "express";
7357
7506
  import { marked } from "marked";
@@ -7366,7 +7515,7 @@ async function startPreviewServer(options) {
7366
7515
  const app = express();
7367
7516
  const previewClientDir = await resolvePreviewClientBuildDir();
7368
7517
  app.disable("x-powered-by");
7369
- app.use("/assets", express.static(options.assetDir));
7518
+ app.use("/assets", express.static(options.assetDir, { dotfiles: "allow" }));
7370
7519
  if (previewClientDir) {
7371
7520
  app.use(express.static(previewClientDir, { index: false }));
7372
7521
  }
@@ -7400,7 +7549,7 @@ async function startPreviewServer(options) {
7400
7549
  const assetPathParam = req.params.assetPath;
7401
7550
  const rawAssetPath = Array.isArray(assetPathParam) ? assetPathParam.join("/") : assetPathParam ?? "";
7402
7551
  const resolvedAssetPath = await resolveGenerationAssetPath(generationId, rawAssetPath, options.markdownOutputDir);
7403
- res.sendFile(resolvedAssetPath);
7552
+ res.sendFile(resolvedAssetPath, { dotfiles: "allow" });
7404
7553
  } catch (error) {
7405
7554
  const message = error instanceof Error ? error.message : "Unknown error loading generation asset";
7406
7555
  const status = error instanceof MissingArticleError ? 404 : 400;
@@ -7449,7 +7598,7 @@ async function startPreviewServer(options) {
7449
7598
  if (options.watch) {
7450
7599
  let html2;
7451
7600
  try {
7452
- html2 = await readFile9(path12.join(previewClientDir, "index.html"), "utf8");
7601
+ html2 = await readFile10(path13.join(previewClientDir, "index.html"), "utf8");
7453
7602
  } catch {
7454
7603
  res.status(200).type("html").send(
7455
7604
  `<!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>`
@@ -7460,7 +7609,7 @@ async function startPreviewServer(options) {
7460
7609
  const injected = html2.replace("</body>", `${reloadScript}</body>`);
7461
7610
  res.status(200).type("html").send(injected);
7462
7611
  } else {
7463
- res.status(200).sendFile(path12.join(previewClientDir, "index.html"));
7612
+ res.status(200).sendFile(path13.join(previewClientDir, "index.html"));
7464
7613
  }
7465
7614
  return;
7466
7615
  }
@@ -7513,7 +7662,7 @@ async function getArticleContent(generationId, markdownOutputDir) {
7513
7662
  generation.outputs.map(async (output) => {
7514
7663
  let markdown = "";
7515
7664
  try {
7516
- markdown = await readFile9(output.sourcePath, "utf8");
7665
+ markdown = await readFile10(output.sourcePath, "utf8");
7517
7666
  } catch (error) {
7518
7667
  if (isMissingFileError(error)) {
7519
7668
  throw new MissingArticleError(`Generation "${generationId}" no longer exists.`);
@@ -7531,15 +7680,17 @@ async function getArticleContent(generationId, markdownOutputDir) {
7531
7680
  };
7532
7681
  })
7533
7682
  );
7534
- const generationDir = path12.dirname(generation.outputs[0]?.sourcePath ?? "");
7683
+ const generationDir = path13.dirname(generation.outputs[0]?.sourcePath ?? "");
7535
7684
  const interactions = generationDir ? await loadSavedInteractions(generationDir) : { llmCalls: [], t2iCalls: [] };
7536
7685
  const analyticsSummary = generationDir ? await loadSavedAnalyticsSummary(generationDir) : null;
7686
+ const metaJson = generationDir ? await loadSavedMetaJson(generationDir) : null;
7537
7687
  return {
7538
7688
  title: generation.title,
7539
7689
  generationId: generation.id,
7540
7690
  sourcePath,
7541
7691
  interactions,
7542
7692
  analyticsSummary,
7693
+ metaJson,
7543
7694
  outputs
7544
7695
  };
7545
7696
  }
@@ -7560,7 +7711,7 @@ async function resolveActivePreviewArticle(preferredMarkdownPath, markdownOutput
7560
7711
  };
7561
7712
  }
7562
7713
  function resolveGenerationSourcePath(generation, markdownOutputDir) {
7563
- return generation.outputs.find((output) => output.contentType === generation.primaryContentType)?.sourcePath ?? generation.outputs[0]?.sourcePath ?? path12.join(markdownOutputDir, generation.id);
7714
+ return generation.outputs.find((output) => output.contentType === generation.primaryContentType)?.sourcePath ?? generation.outputs[0]?.sourcePath ?? path13.join(markdownOutputDir, generation.id);
7564
7715
  }
7565
7716
  function isMissingFileError(error) {
7566
7717
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
@@ -7575,7 +7726,7 @@ async function renderArticleHtml(markdown, generationId, sourcePath) {
7575
7726
  async function loadSavedLinks(markdownPath) {
7576
7727
  const linksPath = resolveLinksPath(markdownPath);
7577
7728
  try {
7578
- const raw = await readFile9(linksPath, "utf8");
7729
+ const raw = await readFile10(linksPath, "utf8");
7579
7730
  const parsed = JSON.parse(raw);
7580
7731
  if (!Array.isArray(parsed.links)) {
7581
7732
  return [];
@@ -7599,9 +7750,9 @@ async function loadSavedLinks(markdownPath) {
7599
7750
  }
7600
7751
  }
7601
7752
  async function loadSavedInteractions(generationDir) {
7602
- const interactionsPath = path12.join(generationDir, "model.interactions.json");
7753
+ const interactionsPath = path13.join(generationDir, "model.interactions.json");
7603
7754
  try {
7604
- const raw = await readFile9(interactionsPath, "utf8");
7755
+ const raw = await readFile10(interactionsPath, "utf8");
7605
7756
  const parsed = JSON.parse(raw);
7606
7757
  const llmCalls = Array.isArray(parsed.llmCalls) ? parsed.llmCalls.filter(isPreviewLlmInteraction) : [];
7607
7758
  const t2iCalls = Array.isArray(parsed.t2iCalls) ? parsed.t2iCalls.filter(isPreviewT2IInteraction) : [];
@@ -7617,9 +7768,9 @@ async function loadSavedInteractions(generationDir) {
7617
7768
  }
7618
7769
  }
7619
7770
  async function loadSavedAnalyticsSummary(generationDir) {
7620
- const analyticsPath = path12.join(generationDir, "generation.analytics.json");
7771
+ const analyticsPath = path13.join(generationDir, "generation.analytics.json");
7621
7772
  try {
7622
- const raw = await readFile9(analyticsPath, "utf8");
7773
+ const raw = await readFile10(analyticsPath, "utf8");
7623
7774
  const parsed = JSON.parse(raw);
7624
7775
  const summary = parsed.summary;
7625
7776
  if (!summary || typeof summary !== "object") {
@@ -7640,6 +7791,15 @@ async function loadSavedAnalyticsSummary(generationDir) {
7640
7791
  return null;
7641
7792
  }
7642
7793
  }
7794
+ async function loadSavedMetaJson(generationDir) {
7795
+ const metaJsonPath = path13.join(generationDir, "meta.json");
7796
+ try {
7797
+ const raw = await readFile10(metaJsonPath, "utf8");
7798
+ return JSON.parse(raw);
7799
+ } catch {
7800
+ return null;
7801
+ }
7802
+ }
7643
7803
  async function getPreviewBootstrapData(preferredMarkdownPath, markdownOutputDir) {
7644
7804
  const activeArticle = await resolveActivePreviewArticle(preferredMarkdownPath, markdownOutputDir);
7645
7805
  const emptyStateMessage = activeArticle ? null : `No generated content found in ${markdownOutputDir}. Run ideon write "your idea" first.`;
@@ -7651,14 +7811,14 @@ async function getPreviewBootstrapData(preferredMarkdownPath, markdownOutputDir)
7651
7811
  };
7652
7812
  }
7653
7813
  async function resolvePreviewClientBuildDir() {
7654
- const currentDir = path12.dirname(fileURLToPath(import.meta.url));
7814
+ const currentDir = path13.dirname(fileURLToPath(import.meta.url));
7655
7815
  const candidates = [
7656
- path12.resolve(currentDir, "preview"),
7657
- path12.resolve(currentDir, "../../dist/preview")
7816
+ path13.resolve(currentDir, "preview"),
7817
+ path13.resolve(currentDir, "../../dist/preview")
7658
7818
  ];
7659
7819
  for (const candidate of candidates) {
7660
7820
  try {
7661
- const indexStat = await stat6(path12.join(candidate, "index.html"));
7821
+ const indexStat = await stat6(path13.join(candidate, "index.html"));
7662
7822
  if (indexStat.isFile()) {
7663
7823
  return candidate;
7664
7824
  }
@@ -7720,17 +7880,17 @@ async function resolveGenerationAssetPath(generationId, rawAssetPath, markdownOu
7720
7880
  throw new MissingArticleError(`Generation "${generationId}" no longer exists.`);
7721
7881
  }
7722
7882
  const decodedAssetPath = decodeURIComponent(rawAssetPath);
7723
- const normalizedRelative = path12.posix.normalize(decodedAssetPath.replace(/\\/g, "/"));
7724
- if (normalizedRelative.length === 0 || normalizedRelative === "." || normalizedRelative.startsWith("../") || normalizedRelative.includes("/../") || path12.posix.isAbsolute(normalizedRelative)) {
7883
+ const normalizedRelative = path13.posix.normalize(decodedAssetPath.replace(/\\/g, "/"));
7884
+ if (normalizedRelative.length === 0 || normalizedRelative === "." || normalizedRelative.startsWith("../") || normalizedRelative.includes("/../") || path13.posix.isAbsolute(normalizedRelative)) {
7725
7885
  throw new Error("Invalid generation asset path.");
7726
7886
  }
7727
- const generationDir = path12.dirname(generation.outputs[0]?.sourcePath ?? "");
7887
+ const generationDir = path13.dirname(generation.outputs[0]?.sourcePath ?? "");
7728
7888
  if (!generationDir) {
7729
7889
  throw new MissingArticleError(`Generation "${generationId}" has no source directory.`);
7730
7890
  }
7731
- const resolvedPath = path12.resolve(generationDir, normalizedRelative);
7732
- const relativeToGeneration = path12.relative(generationDir, resolvedPath);
7733
- if (relativeToGeneration.startsWith("..") || path12.isAbsolute(relativeToGeneration)) {
7891
+ const resolvedPath = path13.resolve(generationDir, normalizedRelative);
7892
+ const relativeToGeneration = path13.relative(generationDir, resolvedPath);
7893
+ if (relativeToGeneration.startsWith("..") || path13.isAbsolute(relativeToGeneration)) {
7734
7894
  throw new Error("Invalid generation asset path.");
7735
7895
  }
7736
7896
  try {
@@ -9208,7 +9368,7 @@ async function runServeCommand(options) {
9208
9368
  const markdownPath = await resolveMarkdownPath(options.markdownPath, outputPaths.markdownOutputDir, process.cwd());
9209
9369
  const port = parsePort(options.port);
9210
9370
  if (options.watch) {
9211
- const viteBin = path13.resolve(process.cwd(), "node_modules", ".bin", "vite");
9371
+ const viteBin = path14.resolve(process.cwd(), "node_modules", ".bin", "vite");
9212
9372
  const viteProcess = spawn(viteBin, ["build", "--watch"], {
9213
9373
  stdio: "inherit",
9214
9374
  shell: process.platform === "win32"
@@ -9234,8 +9394,8 @@ async function runServeCommand(options) {
9234
9394
  openBrowser: options.openBrowser,
9235
9395
  watch: options.watch
9236
9396
  });
9237
- const relativeArticle = path13.relative(process.cwd(), markdownPath);
9238
- const relativeAssets = path13.relative(process.cwd(), outputPaths.assetOutputDir);
9397
+ const relativeArticle = path14.relative(process.cwd(), markdownPath);
9398
+ const relativeAssets = path14.relative(process.cwd(), outputPaths.assetOutputDir);
9239
9399
  console.log(`Previewing ${relativeArticle || markdownPath}`);
9240
9400
  console.log(`Serving assets from ${relativeAssets || outputPaths.assetOutputDir}`);
9241
9401
  console.log(`Open ${server.url}`);
@@ -9785,6 +9945,7 @@ async function renderPlainPipeline(input, dryRun, enrichLinks2, runMode, links,
9785
9945
  title: result.artifact.title,
9786
9946
  slug: result.artifact.slug
9787
9947
  });
9948
+ return result;
9788
9949
  } catch (error) {
9789
9950
  const message = error instanceof Error ? withWriteResumeHint(error.message) : withWriteResumeHint("Pipeline failed.");
9790
9951
  await notifyWriteFailed({
@@ -10101,6 +10262,7 @@ function WriteApp({
10101
10262
  unlinks,
10102
10263
  maxLinks,
10103
10264
  maxImages,
10265
+ onSuccess,
10104
10266
  onError
10105
10267
  }) {
10106
10268
  const { exit } = useApp3();
@@ -10136,6 +10298,7 @@ function WriteApp({
10136
10298
  return;
10137
10299
  }
10138
10300
  setResult(runResult);
10301
+ onSuccess?.(runResult);
10139
10302
  await notifyWriteSucceeded({
10140
10303
  enabled: input.config.settings.notifications.enabled,
10141
10304
  title: runResult.artifact.title,
@@ -10174,7 +10337,7 @@ function WriteApp({
10174
10337
  }
10175
10338
  async function runWriteCommand(options) {
10176
10339
  const input = await resolveInputWithInteractiveIdeaFallback(options);
10177
- await runWritePipeline(input, options.dryRun, options.enrichLinks, "fresh", options.noInteractive, options.links, options.unlinks, options.maxLinks, options.maxImages);
10340
+ await runWritePipeline(input, options.dryRun, options.enrichLinks, "fresh", options.noInteractive, options.links, options.unlinks, options.maxLinks, options.maxImages, options.exportPath);
10178
10341
  }
10179
10342
  async function runWriteResumeCommand(options = {}) {
10180
10343
  const session = await loadWriteSession();
@@ -10196,9 +10359,9 @@ async function runWriteResumeCommand(options = {}) {
10196
10359
  secrets: resolved.config.secrets
10197
10360
  }
10198
10361
  };
10199
- await runWritePipeline(input, session.dryRun, options.enrichLinks ?? false, "resume", options.noInteractive ?? false, options.links, options.unlinks, options.maxLinks, options.maxImages);
10362
+ await runWritePipeline(input, session.dryRun, options.enrichLinks ?? false, "resume", options.noInteractive ?? false, options.links, options.unlinks, options.maxLinks, options.maxImages, options.exportPath);
10200
10363
  }
10201
- async function runWritePipeline(input, dryRun, enrichLinks2, runMode, noInteractive, links, unlinks, maxLinks, maxImages) {
10364
+ async function runWritePipeline(input, dryRun, enrichLinks2, runMode, noInteractive, links, unlinks, maxLinks, maxImages, exportPath) {
10202
10365
  let interruptHandled = false;
10203
10366
  const handleSignal = (signal) => {
10204
10367
  if (interruptHandled) {
@@ -10232,10 +10395,17 @@ async function runWritePipeline(input, dryRun, enrichLinks2, runMode, noInteract
10232
10395
  process.on("SIGTERM", onSigterm);
10233
10396
  try {
10234
10397
  if (noInteractive || !process.stdout.isTTY) {
10235
- await renderPlainPipeline(input, dryRun, enrichLinks2, runMode, links, unlinks, maxLinks, maxImages);
10398
+ const result = await renderPlainPipeline(input, dryRun, enrichLinks2, runMode, links, unlinks, maxLinks, maxImages);
10399
+ if (exportPath) {
10400
+ await runOutputCommand({
10401
+ generationId: result.artifact.slug,
10402
+ destinationPath: exportPath
10403
+ });
10404
+ }
10236
10405
  return;
10237
10406
  }
10238
10407
  let commandError = null;
10408
+ let pipelineResult = null;
10239
10409
  const app = render2(
10240
10410
  /* @__PURE__ */ jsx7(
10241
10411
  WriteApp,
@@ -10248,6 +10418,9 @@ async function runWritePipeline(input, dryRun, enrichLinks2, runMode, noInteract
10248
10418
  unlinks,
10249
10419
  maxLinks,
10250
10420
  maxImages,
10421
+ onSuccess: (result) => {
10422
+ pipelineResult = result;
10423
+ },
10251
10424
  onError: (error) => {
10252
10425
  commandError = error;
10253
10426
  }
@@ -10260,6 +10433,9 @@ async function runWritePipeline(input, dryRun, enrichLinks2, runMode, noInteract
10260
10433
  if (finalError) {
10261
10434
  throw new ReportedError(withWriteResumeHint(finalError.message));
10262
10435
  }
10436
+ if (exportPath && pipelineResult) {
10437
+ await autoExport(exportPath, pipelineResult);
10438
+ }
10263
10439
  } finally {
10264
10440
  cleanupSignalHandlers();
10265
10441
  }
@@ -10407,6 +10583,12 @@ async function promptForIdea() {
10407
10583
  readline.close();
10408
10584
  }
10409
10585
  }
10586
+ async function autoExport(exportPath, result) {
10587
+ await runOutputCommand({
10588
+ generationId: result.artifact.slug,
10589
+ destinationPath: exportPath
10590
+ });
10591
+ }
10410
10592
 
10411
10593
  // src/cli/app.ts
10412
10594
  var { version } = package_default;
@@ -10476,7 +10658,7 @@ async function runCli(argv) {
10476
10658
  watch: options.watch
10477
10659
  });
10478
10660
  });
10479
- const writeCommand = program.command("write").description("Generate one primary content output plus optional secondary outputs from a prompt or job file.").argument("[idea]", "Natural-language idea for the generation run").option("-i, --idea <idea>", "Natural-language idea for the generation run").option("--audience <description>", "Optional natural-language audience description for shared-plan targeting").option("-j, --job <path>", "Path to a JSON job definition").option("--primary <type=count>", "Required primary output target (for example: article=1 or x-post=1)").option("--secondary <type=count>", "Secondary output target, repeatable (for example: x-thread=3, linkedin-post=2)", collectOptionValue).option("--style <style>", "Writing style (academic, analytical, authoritative, conversational, empathetic, friendly, journalistic, minimalist, persuasive, playful, professional, storytelling, technical)").option("--intent <intent>", "Content intent (announcement, case-study, cornerstone, counterargument, critique-review, deep-dive-analysis, how-to-guide, interview-q-and-a, listicle, opinion-piece, personal-essay, roundup-curation, tutorial)").option("--length <size>", "Target length: small, medium, large, or a positive integer word count").option("--no-interactive", "Fail instead of prompting for missing input in TTY mode").option("--dry-run", "Run the pipeline shell without external API calls", false).option("--enrich-links", "Run link enrichment after markdown generation", false).option("--link <pair>", 'Custom link "expression->url", repeatable', collectOptionValue).option("--unlink <expression>", "Remove a custom link by expression, repeatable", collectOptionValue).option("--max-links <n>", "Max number of generated links", (v) => Number.parseInt(v, 10)).option("--max-images <n>", "Max total images including cover (1=cover only, 2=cover+1 inline, 3=cover+2 inline)", (v) => Number.parseInt(v, 10)).action(async (ideaArg, options) => {
10661
+ 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) => {
10480
10662
  await runWriteCommand({
10481
10663
  idea: options.idea ?? ideaArg,
10482
10664
  audience: options.audience,
@@ -10492,17 +10674,19 @@ async function runCli(argv) {
10492
10674
  links: options.link,
10493
10675
  unlinks: options.unlink,
10494
10676
  maxLinks: options.maxLinks,
10495
- maxImages: options.maxImages
10677
+ maxImages: options.maxImages,
10678
+ exportPath: options.export
10496
10679
  });
10497
10680
  });
10498
- writeCommand.command("resume").description("Resume the last failed or interrupted write session.").option("--no-interactive", "Force plain non-interactive output even in TTY mode", false).option("--enrich-links", "Run link enrichment after markdown generation", false).option("--link <pair>", 'Custom link "expression->url", repeatable', collectOptionValue).option("--unlink <expression>", "Remove a custom link by expression, repeatable", collectOptionValue).option("--max-links <n>", "Max number of generated links", (v) => Number.parseInt(v, 10)).option("--max-images <n>", "Max total images including cover (1=cover only, 2=cover+1 inline, 3=cover+2 inline)", (v) => Number.parseInt(v, 10)).action(async (options) => {
10681
+ 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) => {
10499
10682
  await runWriteResumeCommand({
10500
10683
  noInteractive: options.noInteractive,
10501
10684
  enrichLinks: options.enrichLinks,
10502
10685
  links: options.link,
10503
10686
  unlinks: options.unlink,
10504
10687
  maxLinks: options.maxLinks,
10505
- maxImages: options.maxImages
10688
+ maxImages: options.maxImages,
10689
+ exportPath: options.export
10506
10690
  });
10507
10691
  });
10508
10692
  await program.parseAsync(argv);