@telepat/ideon 0.1.24 → 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 +211 -120
  2. package/package.json +1 -1
package/dist/ideon.js CHANGED
@@ -484,9 +484,9 @@ function resolveDefaultMaxLinks(targetLengthWords) {
484
484
  }
485
485
  function resolveDefaultInlineImageCount(targetLengthWords) {
486
486
  const alias = resolveTargetLengthAlias(targetLengthWords);
487
- if (alias === "small") return { min: 1, max: 2 };
488
- if (alias === "medium") return { min: 2, max: 3 };
489
- return { min: 3, max: 4 };
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
490
  }
491
491
  var contentTargetRoleValues = ["primary", "secondary"];
492
492
  var contentTargetSchema = z2.object({
@@ -1423,7 +1423,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
1423
1423
  // package.json
1424
1424
  var package_default = {
1425
1425
  name: "@telepat/ideon",
1426
- version: "0.1.24",
1426
+ version: "0.1.25",
1427
1427
  description: "CLI for generating rich articles and images from ideas.",
1428
1428
  type: "module",
1429
1429
  repository: {
@@ -1625,7 +1625,7 @@ function assertNoLegacyXMode(contentTargets, sourceLabel) {
1625
1625
  // src/pipeline/runner.ts
1626
1626
  import { mkdir as mkdir6, stat as stat2 } from "fs/promises";
1627
1627
  import { randomUUID } from "crypto";
1628
- import path8 from "path";
1628
+ import path9 from "path";
1629
1629
 
1630
1630
  // src/generation/enrichLinks.ts
1631
1631
  import { readFile as readFile4 } from "fs/promises";
@@ -2380,7 +2380,7 @@ function buildLongFormPlanJsonSchema(targetLengthWords) {
2380
2380
  required: ["description", "anchorAfterSection"],
2381
2381
  properties: {
2382
2382
  description: { type: "string" },
2383
- anchorAfterSection: { type: "number", minimum: 1 }
2383
+ anchorAfterSection: { type: "number", minimum: 2 }
2384
2384
  }
2385
2385
  }
2386
2386
  }
@@ -2446,7 +2446,7 @@ function buildLongFormPlanMessages(idea, options) {
2446
2446
  "- Each section description should name the mechanism, evidence type, or practical action that makes the section useful.",
2447
2447
  "- Sections are primary-only structure and must not be treated as requirements for non-primary channels.",
2448
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, 1-based index). Choose sections where visual reinforcement adds the most value.",
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.",
2450
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.",
2451
2451
  "- Image descriptions must be concrete and contextual, not generic stock-photo phrasing.",
2452
2452
  "",
@@ -2468,7 +2468,7 @@ function buildLongFormPlanMessages(idea, options) {
2468
2468
  "- outroBrief: string",
2469
2469
  `- sections: array of ${sectionCounts.label} objects, each with title and description strings`,
2470
2470
  "- 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)`,
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
2472
  "",
2473
2473
  "Do not omit any required fields. Return strict JSON only."
2474
2474
  ].join("\n")
@@ -2530,7 +2530,7 @@ var articleSectionPlanSchema = z5.object({
2530
2530
  });
2531
2531
  var inlineImagePlanSchema = z5.object({
2532
2532
  description: z5.string().min(1),
2533
- anchorAfterSection: z5.number().int().min(1)
2533
+ anchorAfterSection: z5.number().int().min(2)
2534
2534
  });
2535
2535
  var primaryPlanSchema = z5.object({
2536
2536
  contentType: z5.string().min(1).default("article"),
@@ -2543,7 +2543,7 @@ var primaryPlanSchema = z5.object({
2543
2543
  introBrief: z5.string().min(1).optional(),
2544
2544
  outroBrief: z5.string().min(1).optional(),
2545
2545
  sections: z5.array(articleSectionPlanSchema).min(2).max(10).optional(),
2546
- inlineImages: z5.array(inlineImagePlanSchema).min(2).max(3).optional(),
2546
+ inlineImages: z5.array(inlineImagePlanSchema).min(0).max(4).optional(),
2547
2547
  angle: z5.string().min(1).optional()
2548
2548
  });
2549
2549
  var longFormPlanSchema = z5.object({
@@ -2557,7 +2557,7 @@ var longFormPlanSchema = z5.object({
2557
2557
  outroBrief: z5.string().min(1),
2558
2558
  sections: z5.array(articleSectionPlanSchema).min(2).max(10),
2559
2559
  coverImageDescription: z5.string().min(1),
2560
- inlineImages: z5.array(inlineImagePlanSchema).min(2).max(3)
2560
+ inlineImages: z5.array(inlineImagePlanSchema).min(0).max(4)
2561
2561
  });
2562
2562
  var shortFormPlanSchema = z5.object({
2563
2563
  contentType: z5.string().min(1),
@@ -2619,7 +2619,7 @@ async function planPrimaryContent({
2619
2619
  keywords: longPlan.keywords.slice(0, 8),
2620
2620
  inlineImages: longPlan.inlineImages.slice(0, 3).map((img) => ({
2621
2621
  ...img,
2622
- anchorAfterSection: Math.max(1, Math.min(sectionCount, img.anchorAfterSection))
2622
+ anchorAfterSection: Math.max(2, Math.min(sectionCount, img.anchorAfterSection))
2623
2623
  }))
2624
2624
  };
2625
2625
  }
@@ -3276,7 +3276,7 @@ function buildImageSlots(plan, sections, options) {
3276
3276
  kind: "inline",
3277
3277
  prompt: "",
3278
3278
  description: img.description,
3279
- anchorAfterSection: Math.max(1, Math.min(sectionCount, img.anchorAfterSection))
3279
+ anchorAfterSection: Math.max(2, Math.min(sectionCount, img.anchorAfterSection))
3280
3280
  });
3281
3281
  }
3282
3282
  return slots;
@@ -4003,10 +4003,69 @@ ${body.join("\n").trim()}
4003
4003
  `;
4004
4004
  }
4005
4005
 
4006
+ // src/output/meta.ts
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
+
4006
4065
  // src/pipeline/sessionStore.ts
4007
4066
  import { createHash as createHash2 } from "crypto";
4008
4067
  import { mkdir as mkdir5, readFile as readFile5, rm as rm2, writeFile as writeFile5 } from "fs/promises";
4009
- import path7 from "path";
4068
+ import path8 from "path";
4010
4069
  import envPaths3 from "env-paths";
4011
4070
  import { z as z6 } from "zod";
4012
4071
  var STAGE_IDS = ["shared-plan", "planning", "sections", "image-prompts", "images", "output", "links"];
@@ -4049,7 +4108,8 @@ var pipelineArtifactSummarySchema = z6.object({
4049
4108
  assetDir: z6.string().min(1),
4050
4109
  analyticsPath: z6.string().min(1).default("unknown.analytics.json"),
4051
4110
  interactionsPath: z6.string().min(1).default("unknown.interactions.json"),
4052
- 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")
4053
4113
  });
4054
4114
  var resolvedPathsSchema = z6.object({
4055
4115
  markdownOutputDir: z6.string().min(1),
@@ -4085,18 +4145,18 @@ var writeSessionStateSchema = z6.object({
4085
4145
  artifact: pipelineArtifactSummarySchema.nullable()
4086
4146
  });
4087
4147
  var ideonPaths3 = envPaths3("ideon", { suffix: "" });
4088
- var sessionsDir = path7.join(ideonPaths3.config, "sessions");
4148
+ var sessionsDir = path8.join(ideonPaths3.config, "sessions");
4089
4149
  function hashProjectPath(workingDir) {
4090
- return createHash2("sha256").update(path7.resolve(workingDir)).digest("hex").slice(0, 16);
4150
+ return createHash2("sha256").update(path8.resolve(workingDir)).digest("hex").slice(0, 16);
4091
4151
  }
4092
4152
  function resolveWriteRoot(workingDir) {
4093
- return path7.join(sessionsDir, hashProjectPath(workingDir));
4153
+ return path8.join(sessionsDir, hashProjectPath(workingDir));
4094
4154
  }
4095
4155
  function resolveStateFilePath(workingDir) {
4096
- return path7.join(resolveWriteRoot(workingDir), "state.json");
4156
+ return path8.join(resolveWriteRoot(workingDir), "state.json");
4097
4157
  }
4098
4158
  function resolveLegacyStatePath(workingDir) {
4099
- return path7.join(workingDir, ".ideon", "write", "state.json");
4159
+ return path8.join(workingDir, ".ideon", "write", "state.json");
4100
4160
  }
4101
4161
  async function startFreshWriteSession(seed, workingDir = process.cwd()) {
4102
4162
  const writeRoot = resolveWriteRoot(workingDir);
@@ -4157,7 +4217,7 @@ async function saveWriteSession(state, workingDir = process.cwd()) {
4157
4217
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
4158
4218
  });
4159
4219
  const statePath = resolveStateFilePath(workingDir);
4160
- await mkdir5(path7.dirname(statePath), { recursive: true });
4220
+ await mkdir5(path8.dirname(statePath), { recursive: true });
4161
4221
  await writeFile5(statePath, `${JSON.stringify(next, null, 2)}
4162
4222
  `, "utf8");
4163
4223
  return next;
@@ -4716,12 +4776,12 @@ async function runPipelineShell(input, options = {}) {
4716
4776
  options.onUpdate?.(cloneStages(stages));
4717
4777
  }
4718
4778
  const baseSlug = plan?.slug ?? resolveGenerationSlug(input.idea, contentPlan?.title);
4719
- const generationDir = path8.join(
4779
+ const generationDir = path9.join(
4720
4780
  writeSession.outputPaths.markdownOutputDir,
4721
4781
  buildGenerationDirectoryName(baseSlug)
4722
4782
  );
4723
4783
  await mkdir6(generationDir, { recursive: true });
4724
- const jobDefinitionPath = path8.join(generationDir, "job.json");
4784
+ const jobDefinitionPath = path9.join(generationDir, "job.json");
4725
4785
  await writeJsonFile(
4726
4786
  jobDefinitionPath,
4727
4787
  buildRunJobDefinition({
@@ -4733,12 +4793,12 @@ async function runPipelineShell(input, options = {}) {
4733
4793
  sourceJob: input.job
4734
4794
  })
4735
4795
  );
4736
- const planPath = plan ? path8.join(generationDir, "plan.md") : null;
4796
+ const planPath = plan ? path9.join(generationDir, "plan.md") : null;
4737
4797
  if (plan && planPath) {
4738
4798
  await writeUtf8File(planPath, renderPlanMarkdown(plan));
4739
4799
  }
4740
4800
  const primaryFilePrefix = toFilePrefix(primaryTarget.contentType);
4741
- const primaryMarkdownPath = path8.join(generationDir, `${primaryFilePrefix}-1.md`);
4801
+ const primaryMarkdownPath = path9.join(generationDir, `${primaryFilePrefix}-1.md`);
4742
4802
  const sharedAssetDir = generationDir;
4743
4803
  if (isLongForm) {
4744
4804
  const longPlan = plan;
@@ -4962,7 +5022,7 @@ async function runPipelineShell(input, options = {}) {
4962
5022
  })
4963
5023
  };
4964
5024
  options.onUpdate?.(cloneStages(stages));
4965
- const markdownPath = path8.join(generationDir, `${output.filePrefix}-${output.index}.md`);
5025
+ const markdownPath = path9.join(generationDir, `${output.filePrefix}-${output.index}.md`);
4966
5026
  try {
4967
5027
  const content = await writeSingleShotContent({
4968
5028
  idea: input.idea,
@@ -5025,7 +5085,7 @@ async function runPipelineShell(input, options = {}) {
5025
5085
  ...item,
5026
5086
  status: "succeeded",
5027
5087
  detail: "Saved markdown output.",
5028
- summary: path8.basename(markdownPath),
5088
+ summary: path9.basename(markdownPath),
5029
5089
  analytics: {
5030
5090
  durationMs: itemDurationMs,
5031
5091
  costUsd: knownItemCost.usd,
@@ -5240,10 +5300,24 @@ async function runPipelineShell(input, options = {}) {
5240
5300
  llmCalls: llmInteractions,
5241
5301
  t2iCalls: t2iInteractions
5242
5302
  };
5243
- const analyticsPath = path8.join(generationDir, "generation.analytics.json");
5244
- 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");
5245
5305
  await writeJsonFile(analyticsPath, analytics);
5246
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);
5247
5321
  const primaryMarkdownPathForArtifact = markdownPaths[0] ?? primaryMarkdownPath;
5248
5322
  const artifact = {
5249
5323
  title: plan?.title ?? contentPlan.title ?? deriveTitleFromIdea2(input.idea),
@@ -5257,7 +5331,8 @@ async function runPipelineShell(input, options = {}) {
5257
5331
  assetDir: sharedAssetDir,
5258
5332
  analyticsPath,
5259
5333
  interactionsPath,
5260
- planPath
5334
+ planPath,
5335
+ metaJsonPath
5261
5336
  };
5262
5337
  writeSession = await patchWriteSession(
5263
5338
  {
@@ -5731,7 +5806,7 @@ function parsePipelineCustomLinks(rawLinks, unlinks) {
5731
5806
 
5732
5807
  // src/cli/commands/links.ts
5733
5808
  import { readFile as readFile6, stat as stat3 } from "fs/promises";
5734
- import path9 from "path";
5809
+ import path10 from "path";
5735
5810
  async function runLinksCommand(options, dependencies = {}) {
5736
5811
  const slug = normalizeSlug2(options.slug);
5737
5812
  const mode = normalizeMode(options.mode);
@@ -5742,7 +5817,7 @@ async function runLinksCommand(options, dependencies = {}) {
5742
5817
  });
5743
5818
  const markdownPath = await resolveMarkdownPathForSlug2(slug, cwd2);
5744
5819
  const frontmatter = await readFrontmatter(markdownPath);
5745
- const fileId = path9.parse(markdownPath).name;
5820
+ const fileId = path10.parse(markdownPath).name;
5746
5821
  const articleTitle = frontmatter.title ?? toTitleFromSlug(slug);
5747
5822
  const articleDescription = frontmatter.description ?? "";
5748
5823
  const openRouterApiKey = resolved.config.secrets.openRouterApiKey;
@@ -5812,14 +5887,14 @@ function normalizeSlug2(rawSlug) {
5812
5887
  }
5813
5888
  async function resolveMarkdownPathForSlug2(slug, cwd2) {
5814
5889
  const outputPaths = resolveOutputPaths();
5815
- const directPath = path9.join(outputPaths.markdownOutputDir, `${slug}.md`);
5890
+ const directPath = path10.join(outputPaths.markdownOutputDir, `${slug}.md`);
5816
5891
  if (await isReadableFile(directPath)) {
5817
5892
  return directPath;
5818
5893
  }
5819
5894
  const markdownFiles = await listMarkdownFilesRecursively(outputPaths.markdownOutputDir);
5820
5895
  const matches = [];
5821
5896
  for (const candidate of markdownFiles) {
5822
- if (path9.basename(candidate) === `${slug}.md`) {
5897
+ if (path10.basename(candidate) === `${slug}.md`) {
5823
5898
  matches.push(candidate);
5824
5899
  continue;
5825
5900
  }
@@ -5953,7 +6028,7 @@ function readErrorCode2(error) {
5953
6028
  return typeof code === "string" ? code : null;
5954
6029
  }
5955
6030
  function formatRelativePath2(cwd2, targetPath) {
5956
- const relativePath = path9.relative(cwd2, targetPath);
6031
+ const relativePath = path10.relative(cwd2, targetPath);
5957
6032
  return relativePath.length > 0 ? relativePath : targetPath;
5958
6033
  }
5959
6034
  function logProgress(event, log) {
@@ -5993,7 +6068,7 @@ function resolveCustomLinks(existing, addRaw, removeExpressions) {
5993
6068
 
5994
6069
  // src/cli/commands/export.ts
5995
6070
  import { copyFile as copyFile2, mkdir as mkdir7, readFile as readFile8, stat as stat5, writeFile as writeFile6 } from "fs/promises";
5996
- import path11 from "path";
6071
+ import path12 from "path";
5997
6072
 
5998
6073
  // src/output/enrichMarkdownWithLinks.ts
5999
6074
  function enrichMarkdownWithLinks(markdown, links) {
@@ -6050,7 +6125,7 @@ function escapeRegExp(value2) {
6050
6125
 
6051
6126
  // src/server/previewHelpers.ts
6052
6127
  import { readdir, stat as stat4, readFile as readFile7 } from "fs/promises";
6053
- import path10 from "path";
6128
+ import path11 from "path";
6054
6129
  var DEFAULT_PORT = 4173;
6055
6130
  var CONTENT_TYPE_ORDER = ["article", "blog-post", "x-thread", "x-post", "linkedin-post", "reddit-post", "newsletter"];
6056
6131
  var FILE_PREFIX_TO_CONTENT_TYPE = {
@@ -6108,8 +6183,8 @@ function extractHeadingTitle(markdown) {
6108
6183
  }
6109
6184
  async function resolveMarkdownPath(markdownPathArg, markdownOutputDir, cwd2) {
6110
6185
  if (markdownPathArg) {
6111
- const resolved = path10.isAbsolute(markdownPathArg) ? markdownPathArg : path10.resolve(cwd2, markdownPathArg);
6112
- if (path10.extname(resolved).toLowerCase() !== ".md") {
6186
+ const resolved = path11.isAbsolute(markdownPathArg) ? markdownPathArg : path11.resolve(cwd2, markdownPathArg);
6187
+ if (path11.extname(resolved).toLowerCase() !== ".md") {
6113
6188
  throw new Error(`Expected a markdown file (.md), received: ${resolved}`);
6114
6189
  }
6115
6190
  await assertFileExists(resolved, "Could not find markdown file");
@@ -6153,7 +6228,7 @@ function extractCoverImageUrl(markdown) {
6153
6228
  async function extractArticleMetadata(markdownPath) {
6154
6229
  const markdown = await readFile7(markdownPath, "utf8");
6155
6230
  const fileStat = await stat4(markdownPath);
6156
- const slug = extractFrontmatterSlug(markdown) ?? path10.basename(markdownPath, ".md");
6231
+ const slug = extractFrontmatterSlug(markdown) ?? path11.basename(markdownPath, ".md");
6157
6232
  const title = extractHeadingTitle(stripFrontmatter2(markdown)) ?? slug;
6158
6233
  const body = stripFrontmatter2(markdown);
6159
6234
  const previewSnippet = body.replace(/[#\[\]()!\-*_`]/g, "").trim().substring(0, 150);
@@ -6218,16 +6293,16 @@ async function listAllGenerations(markdownOutputDir) {
6218
6293
  return generations;
6219
6294
  }
6220
6295
  function deriveGenerationId(markdownPath, markdownOutputDir) {
6221
- const relative = path10.relative(markdownOutputDir, markdownPath);
6222
- const normalized = relative.split(path10.sep).join("/");
6296
+ const relative = path11.relative(markdownOutputDir, markdownPath);
6297
+ const normalized = relative.split(path11.sep).join("/");
6223
6298
  if (!normalized || normalized.startsWith("../")) {
6224
- return path10.basename(markdownPath, ".md");
6299
+ return path11.basename(markdownPath, ".md");
6225
6300
  }
6226
6301
  const segments = normalized.split("/").filter(Boolean);
6227
6302
  if (segments.length <= 1) {
6228
- return path10.basename(markdownPath, ".md");
6303
+ return path11.basename(markdownPath, ".md");
6229
6304
  }
6230
- return segments[0] ?? path10.basename(markdownPath, ".md");
6305
+ return segments[0] ?? path11.basename(markdownPath, ".md");
6231
6306
  }
6232
6307
  async function findMarkdownFiles(markdownOutputDir) {
6233
6308
  const files = [];
@@ -6244,7 +6319,7 @@ async function findMarkdownFiles(markdownOutputDir) {
6244
6319
  continue;
6245
6320
  }
6246
6321
  for (const entry of entries) {
6247
- const fullPath = path10.join(current, entry.name);
6322
+ const fullPath = path11.join(current, entry.name);
6248
6323
  if (entry.isDirectory()) {
6249
6324
  stack.push(fullPath);
6250
6325
  continue;
@@ -6258,7 +6333,7 @@ async function findMarkdownFiles(markdownOutputDir) {
6258
6333
  }
6259
6334
  function deriveOutputIdentity(markdownPath, markdownOutputDir) {
6260
6335
  const generationId = deriveGenerationId(markdownPath, markdownOutputDir);
6261
- const fileBase = path10.basename(markdownPath, ".md");
6336
+ const fileBase = path11.basename(markdownPath, ".md");
6262
6337
  const parsed = fileBase.match(/^([a-z0-9-]+)-(\d+)$/i);
6263
6338
  if (!parsed || !parsed[1] || !parsed[2]) {
6264
6339
  return {
@@ -6294,11 +6369,11 @@ function toContentTypeLabel(contentType) {
6294
6369
  }
6295
6370
  async function resolvePrimaryContentType(outputs) {
6296
6371
  const fallback = outputs.find((output) => output.contentType === "article")?.contentType ?? outputs[0]?.contentType ?? "article";
6297
- const generationDir = path10.dirname(outputs[0]?.sourcePath ?? "");
6372
+ const generationDir = path11.dirname(outputs[0]?.sourcePath ?? "");
6298
6373
  if (!generationDir) {
6299
6374
  return fallback;
6300
6375
  }
6301
- const jobPath = path10.join(generationDir, "job.json");
6376
+ const jobPath = path11.join(generationDir, "job.json");
6302
6377
  try {
6303
6378
  const raw = await readFile7(jobPath, "utf8");
6304
6379
  const parsed = JSON.parse(raw);
@@ -6314,6 +6389,11 @@ async function resolvePrimaryContentType(outputs) {
6314
6389
  }
6315
6390
 
6316
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
+ ]);
6317
6397
  async function runOutputCommand(options, dependencies = {}) {
6318
6398
  const cwd2 = dependencies.cwd ?? process.cwd();
6319
6399
  const log = dependencies.log ?? ((message) => console.log(message));
@@ -6337,10 +6417,10 @@ async function runOutputCommand(options, dependencies = {}) {
6337
6417
  }
6338
6418
  const sourceMarkdownPath = articleOutput.sourcePath;
6339
6419
  const sourceMarkdown = await readFile8(sourceMarkdownPath, "utf8");
6340
- const slug = extractFrontmatterSlug2(sourceMarkdown) ?? path11.basename(sourceMarkdownPath, ".md");
6420
+ const slug = extractFrontmatterSlug2(sourceMarkdown) ?? path12.basename(sourceMarkdownPath, ".md");
6341
6421
  const exportFilename = `${slug}.md`;
6342
6422
  const destinationDir = await resolveDestinationDir(options.destinationPath, cwd2);
6343
- const destinationFilePath = path11.join(destinationDir, exportFilename);
6423
+ const destinationFilePath = path12.join(destinationDir, exportFilename);
6344
6424
  if (!options.overwrite && await fileExists2(destinationFilePath)) {
6345
6425
  throw new ReportedError(
6346
6426
  `Export file already exists: ${destinationFilePath}. Pass --overwrite to replace it.`
@@ -6349,32 +6429,24 @@ async function runOutputCommand(options, dependencies = {}) {
6349
6429
  await mkdir7(destinationDir, { recursive: true });
6350
6430
  const links = await loadLinks(sourceMarkdownPath);
6351
6431
  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);
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);
6372
6444
  }
6373
6445
  await writeFile6(destinationFilePath, enrichedMarkdown, "utf8");
6374
- const relDest = path11.relative(cwd2, destinationFilePath);
6446
+ const relDest = path12.relative(cwd2, destinationFilePath);
6375
6447
  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(", ")}`);
6448
+ if (copiedFiles.length > 0) {
6449
+ log(`Copied ${copiedFiles.length} file${copiedFiles.length === 1 ? "" : "s"}: ${copiedFiles.join(", ")}`);
6378
6450
  }
6379
6451
  if (links.length > 0) {
6380
6452
  log(`Injected ${links.length} inline link${links.length === 1 ? "" : "s"}.`);
@@ -6396,7 +6468,7 @@ function resolveGeneration(generations, generationId) {
6396
6468
  );
6397
6469
  }
6398
6470
  async function resolveDestinationDir(destinationPath, cwd2) {
6399
- const resolved = path11.isAbsolute(destinationPath) ? destinationPath : path11.resolve(cwd2, destinationPath);
6471
+ const resolved = path12.isAbsolute(destinationPath) ? destinationPath : path12.resolve(cwd2, destinationPath);
6400
6472
  return resolved;
6401
6473
  }
6402
6474
  async function fileExists2(filePath) {
@@ -6466,22 +6538,6 @@ function extractFrontmatterSlug2(markdown) {
6466
6538
  const unquoted = rawSlug.replace(/^['""]|['""]$/g, "").trim();
6467
6539
  return unquoted.length > 0 ? unquoted : null;
6468
6540
  }
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
6541
 
6486
6542
  // src/cli/commands/writeTargetSpecs.ts
6487
6543
  function parseTargetSpec(spec) {
@@ -7343,7 +7399,7 @@ async function openSettings() {
7343
7399
  }
7344
7400
 
7345
7401
  // src/cli/commands/serve.ts
7346
- import path13 from "path";
7402
+ import path14 from "path";
7347
7403
  import { spawn } from "child_process";
7348
7404
 
7349
7405
  // src/server/previewServer.ts
@@ -7351,7 +7407,7 @@ import { execFile } from "child_process";
7351
7407
  import { promisify } from "util";
7352
7408
  import { readFile as readFile9, stat as stat6 } from "fs/promises";
7353
7409
  import { watch as fsWatch } from "fs";
7354
- import path12 from "path";
7410
+ import path13 from "path";
7355
7411
  import { fileURLToPath } from "url";
7356
7412
  import express from "express";
7357
7413
  import { marked } from "marked";
@@ -7449,7 +7505,7 @@ async function startPreviewServer(options) {
7449
7505
  if (options.watch) {
7450
7506
  let html2;
7451
7507
  try {
7452
- html2 = await readFile9(path12.join(previewClientDir, "index.html"), "utf8");
7508
+ html2 = await readFile9(path13.join(previewClientDir, "index.html"), "utf8");
7453
7509
  } catch {
7454
7510
  res.status(200).type("html").send(
7455
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>`
@@ -7460,7 +7516,7 @@ async function startPreviewServer(options) {
7460
7516
  const injected = html2.replace("</body>", `${reloadScript}</body>`);
7461
7517
  res.status(200).type("html").send(injected);
7462
7518
  } else {
7463
- res.status(200).sendFile(path12.join(previewClientDir, "index.html"));
7519
+ res.status(200).sendFile(path13.join(previewClientDir, "index.html"));
7464
7520
  }
7465
7521
  return;
7466
7522
  }
@@ -7531,15 +7587,17 @@ async function getArticleContent(generationId, markdownOutputDir) {
7531
7587
  };
7532
7588
  })
7533
7589
  );
7534
- const generationDir = path12.dirname(generation.outputs[0]?.sourcePath ?? "");
7590
+ const generationDir = path13.dirname(generation.outputs[0]?.sourcePath ?? "");
7535
7591
  const interactions = generationDir ? await loadSavedInteractions(generationDir) : { llmCalls: [], t2iCalls: [] };
7536
7592
  const analyticsSummary = generationDir ? await loadSavedAnalyticsSummary(generationDir) : null;
7593
+ const metaJson = generationDir ? await loadSavedMetaJson(generationDir) : null;
7537
7594
  return {
7538
7595
  title: generation.title,
7539
7596
  generationId: generation.id,
7540
7597
  sourcePath,
7541
7598
  interactions,
7542
7599
  analyticsSummary,
7600
+ metaJson,
7543
7601
  outputs
7544
7602
  };
7545
7603
  }
@@ -7560,7 +7618,7 @@ async function resolveActivePreviewArticle(preferredMarkdownPath, markdownOutput
7560
7618
  };
7561
7619
  }
7562
7620
  function resolveGenerationSourcePath(generation, markdownOutputDir) {
7563
- 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);
7564
7622
  }
7565
7623
  function isMissingFileError(error) {
7566
7624
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
@@ -7599,7 +7657,7 @@ async function loadSavedLinks(markdownPath) {
7599
7657
  }
7600
7658
  }
7601
7659
  async function loadSavedInteractions(generationDir) {
7602
- const interactionsPath = path12.join(generationDir, "model.interactions.json");
7660
+ const interactionsPath = path13.join(generationDir, "model.interactions.json");
7603
7661
  try {
7604
7662
  const raw = await readFile9(interactionsPath, "utf8");
7605
7663
  const parsed = JSON.parse(raw);
@@ -7617,7 +7675,7 @@ async function loadSavedInteractions(generationDir) {
7617
7675
  }
7618
7676
  }
7619
7677
  async function loadSavedAnalyticsSummary(generationDir) {
7620
- const analyticsPath = path12.join(generationDir, "generation.analytics.json");
7678
+ const analyticsPath = path13.join(generationDir, "generation.analytics.json");
7621
7679
  try {
7622
7680
  const raw = await readFile9(analyticsPath, "utf8");
7623
7681
  const parsed = JSON.parse(raw);
@@ -7640,6 +7698,15 @@ async function loadSavedAnalyticsSummary(generationDir) {
7640
7698
  return null;
7641
7699
  }
7642
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
+ }
7643
7710
  async function getPreviewBootstrapData(preferredMarkdownPath, markdownOutputDir) {
7644
7711
  const activeArticle = await resolveActivePreviewArticle(preferredMarkdownPath, markdownOutputDir);
7645
7712
  const emptyStateMessage = activeArticle ? null : `No generated content found in ${markdownOutputDir}. Run ideon write "your idea" first.`;
@@ -7651,14 +7718,14 @@ async function getPreviewBootstrapData(preferredMarkdownPath, markdownOutputDir)
7651
7718
  };
7652
7719
  }
7653
7720
  async function resolvePreviewClientBuildDir() {
7654
- const currentDir = path12.dirname(fileURLToPath(import.meta.url));
7721
+ const currentDir = path13.dirname(fileURLToPath(import.meta.url));
7655
7722
  const candidates = [
7656
- path12.resolve(currentDir, "preview"),
7657
- path12.resolve(currentDir, "../../dist/preview")
7723
+ path13.resolve(currentDir, "preview"),
7724
+ path13.resolve(currentDir, "../../dist/preview")
7658
7725
  ];
7659
7726
  for (const candidate of candidates) {
7660
7727
  try {
7661
- const indexStat = await stat6(path12.join(candidate, "index.html"));
7728
+ const indexStat = await stat6(path13.join(candidate, "index.html"));
7662
7729
  if (indexStat.isFile()) {
7663
7730
  return candidate;
7664
7731
  }
@@ -7720,17 +7787,17 @@ async function resolveGenerationAssetPath(generationId, rawAssetPath, markdownOu
7720
7787
  throw new MissingArticleError(`Generation "${generationId}" no longer exists.`);
7721
7788
  }
7722
7789
  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)) {
7790
+ const normalizedRelative = path13.posix.normalize(decodedAssetPath.replace(/\\/g, "/"));
7791
+ if (normalizedRelative.length === 0 || normalizedRelative === "." || normalizedRelative.startsWith("../") || normalizedRelative.includes("/../") || path13.posix.isAbsolute(normalizedRelative)) {
7725
7792
  throw new Error("Invalid generation asset path.");
7726
7793
  }
7727
- const generationDir = path12.dirname(generation.outputs[0]?.sourcePath ?? "");
7794
+ const generationDir = path13.dirname(generation.outputs[0]?.sourcePath ?? "");
7728
7795
  if (!generationDir) {
7729
7796
  throw new MissingArticleError(`Generation "${generationId}" has no source directory.`);
7730
7797
  }
7731
- const resolvedPath = path12.resolve(generationDir, normalizedRelative);
7732
- const relativeToGeneration = path12.relative(generationDir, resolvedPath);
7733
- 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)) {
7734
7801
  throw new Error("Invalid generation asset path.");
7735
7802
  }
7736
7803
  try {
@@ -9208,7 +9275,7 @@ async function runServeCommand(options) {
9208
9275
  const markdownPath = await resolveMarkdownPath(options.markdownPath, outputPaths.markdownOutputDir, process.cwd());
9209
9276
  const port = parsePort(options.port);
9210
9277
  if (options.watch) {
9211
- const viteBin = path13.resolve(process.cwd(), "node_modules", ".bin", "vite");
9278
+ const viteBin = path14.resolve(process.cwd(), "node_modules", ".bin", "vite");
9212
9279
  const viteProcess = spawn(viteBin, ["build", "--watch"], {
9213
9280
  stdio: "inherit",
9214
9281
  shell: process.platform === "win32"
@@ -9234,8 +9301,8 @@ async function runServeCommand(options) {
9234
9301
  openBrowser: options.openBrowser,
9235
9302
  watch: options.watch
9236
9303
  });
9237
- const relativeArticle = path13.relative(process.cwd(), markdownPath);
9238
- 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);
9239
9306
  console.log(`Previewing ${relativeArticle || markdownPath}`);
9240
9307
  console.log(`Serving assets from ${relativeAssets || outputPaths.assetOutputDir}`);
9241
9308
  console.log(`Open ${server.url}`);
@@ -9785,6 +9852,7 @@ async function renderPlainPipeline(input, dryRun, enrichLinks2, runMode, links,
9785
9852
  title: result.artifact.title,
9786
9853
  slug: result.artifact.slug
9787
9854
  });
9855
+ return result;
9788
9856
  } catch (error) {
9789
9857
  const message = error instanceof Error ? withWriteResumeHint(error.message) : withWriteResumeHint("Pipeline failed.");
9790
9858
  await notifyWriteFailed({
@@ -10101,6 +10169,7 @@ function WriteApp({
10101
10169
  unlinks,
10102
10170
  maxLinks,
10103
10171
  maxImages,
10172
+ onSuccess,
10104
10173
  onError
10105
10174
  }) {
10106
10175
  const { exit } = useApp3();
@@ -10136,6 +10205,7 @@ function WriteApp({
10136
10205
  return;
10137
10206
  }
10138
10207
  setResult(runResult);
10208
+ onSuccess?.(runResult);
10139
10209
  await notifyWriteSucceeded({
10140
10210
  enabled: input.config.settings.notifications.enabled,
10141
10211
  title: runResult.artifact.title,
@@ -10174,7 +10244,7 @@ function WriteApp({
10174
10244
  }
10175
10245
  async function runWriteCommand(options) {
10176
10246
  const input = await resolveInputWithInteractiveIdeaFallback(options);
10177
- 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);
10178
10248
  }
10179
10249
  async function runWriteResumeCommand(options = {}) {
10180
10250
  const session = await loadWriteSession();
@@ -10196,9 +10266,9 @@ async function runWriteResumeCommand(options = {}) {
10196
10266
  secrets: resolved.config.secrets
10197
10267
  }
10198
10268
  };
10199
- 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);
10200
10270
  }
10201
- 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) {
10202
10272
  let interruptHandled = false;
10203
10273
  const handleSignal = (signal) => {
10204
10274
  if (interruptHandled) {
@@ -10232,10 +10302,17 @@ async function runWritePipeline(input, dryRun, enrichLinks2, runMode, noInteract
10232
10302
  process.on("SIGTERM", onSigterm);
10233
10303
  try {
10234
10304
  if (noInteractive || !process.stdout.isTTY) {
10235
- 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
+ }
10236
10312
  return;
10237
10313
  }
10238
10314
  let commandError = null;
10315
+ let pipelineResult = null;
10239
10316
  const app = render2(
10240
10317
  /* @__PURE__ */ jsx7(
10241
10318
  WriteApp,
@@ -10248,6 +10325,9 @@ async function runWritePipeline(input, dryRun, enrichLinks2, runMode, noInteract
10248
10325
  unlinks,
10249
10326
  maxLinks,
10250
10327
  maxImages,
10328
+ onSuccess: (result) => {
10329
+ pipelineResult = result;
10330
+ },
10251
10331
  onError: (error) => {
10252
10332
  commandError = error;
10253
10333
  }
@@ -10260,6 +10340,9 @@ async function runWritePipeline(input, dryRun, enrichLinks2, runMode, noInteract
10260
10340
  if (finalError) {
10261
10341
  throw new ReportedError(withWriteResumeHint(finalError.message));
10262
10342
  }
10343
+ if (exportPath && pipelineResult) {
10344
+ await autoExport(exportPath, pipelineResult);
10345
+ }
10263
10346
  } finally {
10264
10347
  cleanupSignalHandlers();
10265
10348
  }
@@ -10407,6 +10490,12 @@ async function promptForIdea() {
10407
10490
  readline.close();
10408
10491
  }
10409
10492
  }
10493
+ async function autoExport(exportPath, result) {
10494
+ await runOutputCommand({
10495
+ generationId: result.artifact.slug,
10496
+ destinationPath: exportPath
10497
+ });
10498
+ }
10410
10499
 
10411
10500
  // src/cli/app.ts
10412
10501
  var { version } = package_default;
@@ -10476,7 +10565,7 @@ async function runCli(argv) {
10476
10565
  watch: options.watch
10477
10566
  });
10478
10567
  });
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) => {
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) => {
10480
10569
  await runWriteCommand({
10481
10570
  idea: options.idea ?? ideaArg,
10482
10571
  audience: options.audience,
@@ -10492,17 +10581,19 @@ async function runCli(argv) {
10492
10581
  links: options.link,
10493
10582
  unlinks: options.unlink,
10494
10583
  maxLinks: options.maxLinks,
10495
- maxImages: options.maxImages
10584
+ maxImages: options.maxImages,
10585
+ exportPath: options.export
10496
10586
  });
10497
10587
  });
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) => {
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) => {
10499
10589
  await runWriteResumeCommand({
10500
10590
  noInteractive: options.noInteractive,
10501
10591
  enrichLinks: options.enrichLinks,
10502
10592
  links: options.link,
10503
10593
  unlinks: options.unlink,
10504
10594
  maxLinks: options.maxLinks,
10505
- maxImages: options.maxImages
10595
+ maxImages: options.maxImages,
10596
+ exportPath: options.export
10506
10597
  });
10507
10598
  });
10508
10599
  await program.parseAsync(argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telepat/ideon",
3
- "version": "0.1.24",
3
+ "version": "0.1.25",
4
4
  "description": "CLI for generating rich articles and images from ideas.",
5
5
  "type": "module",
6
6
  "repository": {