@telepat/ideon 0.1.13 → 0.1.14

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
@@ -93,6 +93,12 @@ function resolveTargetLengthAlias(targetLengthWords) {
93
93
  }
94
94
  return "large";
95
95
  }
96
+ function resolveDefaultMaxLinks(targetLengthWords) {
97
+ const alias = resolveTargetLengthAlias(targetLengthWords);
98
+ if (alias === "small") return 5;
99
+ if (alias === "medium") return 8;
100
+ return 12;
101
+ }
96
102
  var contentTargetRoleValues = ["primary", "secondary"];
97
103
  var contentTargetSchema = z.object({
98
104
  contentType: z.enum(contentTypeValues),
@@ -1299,7 +1305,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
1299
1305
  // package.json
1300
1306
  var package_default = {
1301
1307
  name: "@telepat/ideon",
1302
- version: "0.1.13",
1308
+ version: "0.1.14",
1303
1309
  description: "CLI for generating rich articles and images from ideas.",
1304
1310
  type: "module",
1305
1311
  repository: {
@@ -1508,7 +1514,7 @@ import path8 from "path";
1508
1514
  import { readFile as readFile4 } from "fs/promises";
1509
1515
 
1510
1516
  // src/llm/prompts/linkEnrichment.ts
1511
- function buildLinkCandidatesJsonSchema() {
1517
+ function buildLinkCandidatesJsonSchema(maxLinks = 10) {
1512
1518
  return {
1513
1519
  type: "object",
1514
1520
  additionalProperties: false,
@@ -1517,13 +1523,13 @@ function buildLinkCandidatesJsonSchema() {
1517
1523
  expressions: {
1518
1524
  type: "array",
1519
1525
  minItems: 0,
1520
- maxItems: 10,
1526
+ maxItems: maxLinks,
1521
1527
  items: { type: "string", minLength: 2 }
1522
1528
  }
1523
1529
  }
1524
1530
  };
1525
1531
  }
1526
- function buildLinkCandidatesMessages(content, contentType) {
1532
+ function buildLinkCandidatesMessages(content, contentType, maxLinks = 10) {
1527
1533
  return [
1528
1534
  {
1529
1535
  role: "system",
@@ -1539,7 +1545,7 @@ function buildLinkCandidatesMessages(content, contentType) {
1539
1545
  role: "user",
1540
1546
  content: [
1541
1547
  `Content type: ${contentType}`,
1542
- "Select up to 10 expressions that should become links in this content.",
1548
+ `Select up to ${maxLinks} expressions that should become links in this content.`,
1543
1549
  "Each expression must be copied exactly from the text and be useful to link.",
1544
1550
  "",
1545
1551
  "Content:",
@@ -1594,6 +1600,8 @@ async function enrichLinks({
1594
1600
  openRouter,
1595
1601
  settings,
1596
1602
  dryRun,
1603
+ customLinks = [],
1604
+ maxLinks = 10,
1597
1605
  onLlmMetrics,
1598
1606
  onItemProgress,
1599
1607
  onInteraction
@@ -1615,7 +1623,8 @@ async function enrichLinks({
1615
1623
  fileId: item.fileId,
1616
1624
  contentType: item.contentType,
1617
1625
  markdownPath: item.markdownPath,
1618
- links: []
1626
+ links: [],
1627
+ customLinks
1619
1628
  });
1620
1629
  continue;
1621
1630
  }
@@ -1634,7 +1643,8 @@ async function enrichLinks({
1634
1643
  fileId: item.fileId,
1635
1644
  contentType: item.contentType,
1636
1645
  markdownPath: item.markdownPath,
1637
- links: []
1646
+ links: [],
1647
+ customLinks
1638
1648
  });
1639
1649
  continue;
1640
1650
  }
@@ -1645,8 +1655,8 @@ async function enrichLinks({
1645
1655
  });
1646
1656
  const candidateResult = await openRouter.requestStructured({
1647
1657
  schemaName: "link_candidates",
1648
- schema: buildLinkCandidatesJsonSchema(),
1649
- messages: buildLinkCandidatesMessages(content, item.contentType),
1658
+ schema: buildLinkCandidatesJsonSchema(maxLinks),
1659
+ messages: buildLinkCandidatesMessages(content, item.contentType, maxLinks),
1650
1660
  settings,
1651
1661
  reasoning: LINKS_REASONING_SETTINGS,
1652
1662
  interactionContext: {
@@ -1657,7 +1667,10 @@ async function enrichLinks({
1657
1667
  parse(data) {
1658
1668
  const record = data;
1659
1669
  const expressions = Array.isArray(record.expressions) ? record.expressions.filter((value2) => typeof value2 === "string") : [];
1660
- return { expressions: dedupeExpressions(expressions).slice(0, 10) };
1670
+ const customExpressions = new Set(customLinks.map((e) => e.expression.trim().toLowerCase()));
1671
+ return {
1672
+ expressions: dedupeExpressions(expressions).filter((expr) => !customExpressions.has(expr.trim().toLowerCase())).slice(0, maxLinks)
1673
+ };
1661
1674
  },
1662
1675
  onMetrics(metrics) {
1663
1676
  onLlmMetrics?.(item.fileId, metrics);
@@ -1734,7 +1747,8 @@ async function enrichLinks({
1734
1747
  fileId: item.fileId,
1735
1748
  contentType: item.contentType,
1736
1749
  markdownPath: item.markdownPath,
1737
- links
1750
+ links,
1751
+ customLinks
1738
1752
  });
1739
1753
  }
1740
1754
  return results;
@@ -4355,7 +4369,8 @@ var linksResultSchema = z6.object({
4355
4369
  fileId: z6.string().min(1),
4356
4370
  contentType: z6.string().min(1),
4357
4371
  markdownPath: z6.string().min(1),
4358
- links: z6.array(linkEntrySchema)
4372
+ links: z6.array(linkEntrySchema),
4373
+ customLinks: z6.array(linkEntrySchema).default([])
4359
4374
  });
4360
4375
  var pipelineArtifactSummarySchema = z6.object({
4361
4376
  title: z6.string().min(1),
@@ -4553,6 +4568,9 @@ async function runPipelineShell(input, options = {}) {
4553
4568
  const shouldEnrichLinks = options.enrichLinks ?? false;
4554
4569
  const runMode = options.runMode ?? "fresh";
4555
4570
  const workingDir = options.workingDir ?? process.cwd();
4571
+ const pipelineCustomLinkRaws = options.customLinks ?? [];
4572
+ const pipelineUnlinks = options.unlinks ?? [];
4573
+ const pipelineMaxLinks = options.maxLinks;
4556
4574
  const outputPaths = resolveOutputPaths(input.config.settings, workingDir);
4557
4575
  const hasArticlePrimary = isArticlePrimary;
4558
4576
  const stageTracking = /* @__PURE__ */ new Map();
@@ -5422,15 +5440,19 @@ async function runPipelineShell(input, options = {}) {
5422
5440
  options.onUpdate?.(cloneStages(stages));
5423
5441
  } else if (linksResult) {
5424
5442
  const linksByFileId = new Map(linksResult.map((item) => [item.fileId, item.links]));
5425
- linksResult = eligibleOutputsForLinks.map((output) => ({
5443
+ const customLinksByFileId = new Map(linksResult.map((item) => [item.fileId, item.customLinks]));
5444
+ const resumedLinks = eligibleOutputsForLinks.map((output) => ({
5426
5445
  fileId: output.fileId,
5427
5446
  contentType: output.contentType,
5428
5447
  markdownPath: output.markdownPath,
5429
- links: linksByFileId.get(output.fileId) ?? []
5448
+ links: linksByFileId.get(output.fileId) ?? [],
5449
+ customLinks: customLinksByFileId.get(output.fileId) ?? []
5430
5450
  }));
5431
- for (const item of linksResult) {
5451
+ linksResult = resumedLinks;
5452
+ for (const item of resumedLinks) {
5432
5453
  await writeLinksFile(item.markdownPath, {
5433
- version: 1,
5454
+ version: 2,
5455
+ customLinks: item.customLinks,
5434
5456
  links: item.links
5435
5457
  });
5436
5458
  }
@@ -5439,7 +5461,7 @@ async function runPipelineShell(input, options = {}) {
5439
5461
  ...stages[6],
5440
5462
  status: "succeeded",
5441
5463
  detail: "Reused saved link metadata from .ideon/write.",
5442
- summary: `${linksResult.reduce((sum, item) => sum + item.links.length, 0)} links`,
5464
+ summary: `${resumedLinks.reduce((sum, item) => sum + item.links.length, 0)} links`,
5443
5465
  items: (stages[6].items ?? []).map((item) => ({
5444
5466
  ...item,
5445
5467
  status: "succeeded",
@@ -5457,6 +5479,8 @@ async function runPipelineShell(input, options = {}) {
5457
5479
  openRouter,
5458
5480
  settings: input.config.settings,
5459
5481
  dryRun,
5482
+ customLinks: parsePipelineCustomLinks(pipelineCustomLinkRaws, pipelineUnlinks),
5483
+ maxLinks: pipelineMaxLinks ?? resolveDefaultMaxLinks(input.config.settings.targetLength),
5460
5484
  onInteraction(interaction) {
5461
5485
  onLlmInteraction(interaction);
5462
5486
  },
@@ -5501,7 +5525,8 @@ async function runPipelineShell(input, options = {}) {
5501
5525
  costSource
5502
5526
  });
5503
5527
  await writeLinksFile(item.markdownPath, {
5504
- version: 1,
5528
+ version: 2,
5529
+ customLinks: item.customLinks,
5505
5530
  links: item.links
5506
5531
  });
5507
5532
  }
@@ -5866,7 +5891,6 @@ function toFilePrefix(contentType) {
5866
5891
  if (contentType === "reddit-post") return "reddit";
5867
5892
  if (contentType === "linkedin-post") return "linkedin";
5868
5893
  if (contentType === "newsletter") return "newsletter";
5869
- if (contentType === "landing-page-copy") return "landing";
5870
5894
  return contentType.replace(/[^a-z0-9]+/gi, "-").replace(/^-+|-+$/g, "").toLowerCase() || "content";
5871
5895
  }
5872
5896
  function getPrimaryTarget(contentTargets) {
@@ -5962,6 +5986,24 @@ function asWriteStageId(stageId) {
5962
5986
  }
5963
5987
  return null;
5964
5988
  }
5989
+ function parsePipelineCustomLinks(rawLinks, unlinks) {
5990
+ const result = /* @__PURE__ */ new Map();
5991
+ for (const raw of rawLinks) {
5992
+ const separatorIndex = raw.indexOf("->");
5993
+ if (separatorIndex < 0) {
5994
+ continue;
5995
+ }
5996
+ const expression = raw.slice(0, separatorIndex).trim();
5997
+ const url = raw.slice(separatorIndex + 2).trim();
5998
+ if (expression && url) {
5999
+ result.set(expression.toLowerCase(), { expression, url, title: null });
6000
+ }
6001
+ }
6002
+ for (const expr of unlinks) {
6003
+ result.delete(expr.trim().toLowerCase());
6004
+ }
6005
+ return Array.from(result.values());
6006
+ }
5965
6007
 
5966
6008
  // src/cli/commands/writeTargetSpecs.ts
5967
6009
  function parseTargetSpec(spec) {
@@ -6273,6 +6315,10 @@ async function runLinksCommand(options, dependencies = {}) {
6273
6315
  );
6274
6316
  }
6275
6317
  const openRouter = new OpenRouterClient(openRouterApiKey);
6318
+ const linksPath = resolveLinksPath(markdownPath);
6319
+ const existing = await readExistingLinks(linksPath);
6320
+ const updatedCustomLinks = resolveCustomLinks(existing?.customLinks ?? [], options.links ?? [], options.unlinks ?? []);
6321
+ const effectiveMaxLinks = options.maxLinks;
6276
6322
  const linksResult = await enrichLinks({
6277
6323
  markdownFiles: [{ markdownPath, fileId, contentType: "article" }],
6278
6324
  articleTitle,
@@ -6280,31 +6326,32 @@ async function runLinksCommand(options, dependencies = {}) {
6280
6326
  openRouter,
6281
6327
  settings: resolved.config.settings,
6282
6328
  dryRun: false,
6329
+ customLinks: updatedCustomLinks,
6330
+ maxLinks: effectiveMaxLinks,
6283
6331
  onItemProgress(event) {
6284
6332
  logProgress(event, log);
6285
6333
  }
6286
6334
  });
6287
6335
  const generatedLinks = linksResult[0]?.links ?? [];
6288
- const linksPath = resolveLinksPath(markdownPath);
6289
- const existing = await readExistingLinks(linksPath);
6290
- const mergedLinks = mode === "append" ? mergeLinks(existing?.links ?? [], generatedLinks) : generatedLinks;
6291
- const appendedCount = Math.max(0, mergedLinks.length - (existing?.links.length ?? 0));
6336
+ const mergedGeneratedLinks = mode === "append" ? mergeLinks(existing?.links ?? [], generatedLinks) : generatedLinks;
6337
+ const appendedCount = Math.max(0, mergedGeneratedLinks.length - (existing?.links.length ?? 0));
6292
6338
  await writeLinksFile(markdownPath, {
6293
- version: 1,
6294
- links: mergedLinks
6339
+ version: 2,
6340
+ customLinks: updatedCustomLinks,
6341
+ links: mergedGeneratedLinks
6295
6342
  });
6296
6343
  const relativeMarkdownPath = formatRelativePath2(cwd2, markdownPath);
6297
6344
  const relativeLinksPath = formatRelativePath2(cwd2, linksPath);
6298
6345
  if (mode === "fresh") {
6299
6346
  const replaced = existing ? "Replaced existing links." : "Created links sidecar.";
6300
6347
  log(`Enriched links for "${slug}".`);
6301
- log(`${replaced} Saved ${generatedLinks.length} links to ${relativeLinksPath} (${relativeMarkdownPath}).`);
6348
+ log(`${replaced} Saved ${generatedLinks.length} generated + ${updatedCustomLinks.length} custom links to ${relativeLinksPath} (${relativeMarkdownPath}).`);
6302
6349
  return;
6303
6350
  }
6304
6351
  const baseCount = existing?.links.length ?? 0;
6305
6352
  const verb = existing ? "Appended and deduplicated links." : "Created links sidecar.";
6306
6353
  log(`Enriched links for "${slug}".`);
6307
- log(`${verb} Base ${baseCount}, added ${appendedCount}, total ${mergedLinks.length} in ${relativeLinksPath} (${relativeMarkdownPath}).`);
6354
+ log(`${verb} Base ${baseCount}, added ${appendedCount}, total ${mergedGeneratedLinks.length} generated + ${updatedCustomLinks.length} custom in ${relativeLinksPath} (${relativeMarkdownPath}).`);
6308
6355
  }
6309
6356
  function normalizeMode(rawMode) {
6310
6357
  const normalized = rawMode.trim().toLowerCase();
@@ -6420,8 +6467,14 @@ async function readExistingLinks(linksPath) {
6420
6467
  if (!links) {
6421
6468
  throw new ReportedError(`Invalid links sidecar format at ${linksPath}. Expected { version, links[] }.`);
6422
6469
  }
6470
+ const customLinks = Array.isArray(parsed.customLinks) ? parsed.customLinks.filter((entry) => isValidLinkEntry(entry)).map((entry) => ({
6471
+ expression: entry.expression.trim(),
6472
+ url: entry.url.trim(),
6473
+ title: typeof entry.title === "string" ? entry.title : null
6474
+ })) : [];
6423
6475
  return {
6424
6476
  version: typeof parsed.version === "number" ? parsed.version : 1,
6477
+ customLinks,
6425
6478
  links
6426
6479
  };
6427
6480
  } catch (error) {
@@ -6472,6 +6525,34 @@ function logProgress(event, log) {
6472
6525
  }
6473
6526
  log(event.detail);
6474
6527
  }
6528
+ function parseCustomLinkFlag(raw) {
6529
+ const separatorIndex = raw.indexOf("->");
6530
+ if (separatorIndex < 0) {
6531
+ throw new ReportedError(`Invalid --link value "${raw}". Expected format: "expression->url".`);
6532
+ }
6533
+ const expression = raw.slice(0, separatorIndex).trim();
6534
+ const url = raw.slice(separatorIndex + 2).trim();
6535
+ if (!expression) {
6536
+ throw new ReportedError(`Invalid --link value "${raw}": expression (left side of ->) cannot be empty.`);
6537
+ }
6538
+ if (!url) {
6539
+ throw new ReportedError(`Invalid --link value "${raw}": url (right side of ->) cannot be empty.`);
6540
+ }
6541
+ return { expression, url };
6542
+ }
6543
+ function resolveCustomLinks(existing, addRaw, removeExpressions) {
6544
+ const result = new Map(
6545
+ existing.map((entry) => [entry.expression.trim().toLowerCase(), entry])
6546
+ );
6547
+ for (const raw of addRaw) {
6548
+ const { expression, url } = parseCustomLinkFlag(raw);
6549
+ result.set(expression.toLowerCase(), { expression, url, title: null });
6550
+ }
6551
+ for (const expr of removeExpressions) {
6552
+ result.delete(expr.trim().toLowerCase());
6553
+ }
6554
+ return Array.from(result.values());
6555
+ }
6475
6556
 
6476
6557
  // src/cli/commands/settings.tsx
6477
6558
  import { render } from "ink";
@@ -6852,7 +6933,7 @@ import { spawn } from "child_process";
6852
6933
  import { readdir, stat as stat4, readFile as readFile7 } from "fs/promises";
6853
6934
  import path10 from "path";
6854
6935
  var DEFAULT_PORT = 4173;
6855
- var CONTENT_TYPE_ORDER = ["article", "blog-post", "x-thread", "x-post", "linkedin-post", "reddit-post", "newsletter", "landing-page-copy"];
6936
+ var CONTENT_TYPE_ORDER = ["article", "blog-post", "x-thread", "x-post", "linkedin-post", "reddit-post", "newsletter"];
6856
6937
  var FILE_PREFIX_TO_CONTENT_TYPE = {
6857
6938
  article: "article",
6858
6939
  blog: "blog-post",
@@ -6861,8 +6942,7 @@ var FILE_PREFIX_TO_CONTENT_TYPE = {
6861
6942
  x: "x-post",
6862
6943
  reddit: "reddit-post",
6863
6944
  linkedin: "linkedin-post",
6864
- newsletter: "newsletter",
6865
- landing: "landing-page-copy"
6945
+ newsletter: "newsletter"
6866
6946
  };
6867
6947
  var CONTENT_TYPE_LABELS = {
6868
6948
  article: "Article",
@@ -6871,8 +6951,7 @@ var CONTENT_TYPE_LABELS = {
6871
6951
  "x-post": "X Post",
6872
6952
  "reddit-post": "Reddit Post",
6873
6953
  "linkedin-post": "LinkedIn Post",
6874
- newsletter: "Newsletter",
6875
- "landing-page-copy": "Landing Page Copy"
6954
+ newsletter: "Newsletter"
6876
6955
  };
6877
6956
  function parsePort(portOption) {
6878
6957
  if (!portOption) {
@@ -7630,10 +7709,6 @@ function renderShell({
7630
7709
  --newsletter-bg: #fffdf4;
7631
7710
  --newsletter-header-bg: #fff5cc;
7632
7711
  --newsletter-border: #cfb95a;
7633
- --landing-bg: linear-gradient(155deg, #10395c 0%, #3d7fa0 100%);
7634
- --landing-text: #f8fdff;
7635
- --landing-link: #d7f0ff;
7636
- --landing-border: rgba(255, 255, 255, 0.3);
7637
7712
  color-scheme: light;
7638
7713
  }
7639
7714
 
@@ -7673,10 +7748,6 @@ function renderShell({
7673
7748
  --newsletter-bg: #2e291b;
7674
7749
  --newsletter-header-bg: #3b331e;
7675
7750
  --newsletter-border: #d6b25f;
7676
- --landing-bg: linear-gradient(155deg, #0f2236 0%, #245d7e 100%);
7677
- --landing-text: #e7f4ff;
7678
- --landing-link: #b8e4ff;
7679
- --landing-border: rgba(220, 239, 255, 0.35);
7680
7751
  color-scheme: dark;
7681
7752
  }
7682
7753
  }
@@ -7716,10 +7787,6 @@ function renderShell({
7716
7787
  --newsletter-bg: #fffdf4;
7717
7788
  --newsletter-header-bg: #fff5cc;
7718
7789
  --newsletter-border: #cfb95a;
7719
- --landing-bg: linear-gradient(155deg, #10395c 0%, #3d7fa0 100%);
7720
- --landing-text: #f8fdff;
7721
- --landing-link: #d7f0ff;
7722
- --landing-border: rgba(255, 255, 255, 0.3);
7723
7790
  color-scheme: light;
7724
7791
  }
7725
7792
 
@@ -7758,10 +7825,6 @@ function renderShell({
7758
7825
  --newsletter-bg: #2e291b;
7759
7826
  --newsletter-header-bg: #3b331e;
7760
7827
  --newsletter-border: #d6b25f;
7761
- --landing-bg: linear-gradient(155deg, #0f2236 0%, #245d7e 100%);
7762
- --landing-text: #e7f4ff;
7763
- --landing-link: #b8e4ff;
7764
- --landing-border: rgba(220, 239, 255, 0.35);
7765
7828
  color-scheme: dark;
7766
7829
  }
7767
7830
 
@@ -8281,21 +8344,6 @@ function renderShell({
8281
8344
  background: var(--newsletter-header-bg);
8282
8345
  }
8283
8346
 
8284
- .channel-landing-page-copy {
8285
- background: var(--landing-bg);
8286
- color: var(--landing-text);
8287
- border: none;
8288
- }
8289
-
8290
- .channel-landing-page-copy .channel-header {
8291
- border-bottom: 1px solid var(--landing-border);
8292
- }
8293
-
8294
- .channel-landing-page-copy .channel-meta,
8295
- .channel-landing-page-copy a {
8296
- color: var(--landing-link);
8297
- }
8298
-
8299
8347
  .channel-article,
8300
8348
  .channel-blog-post {
8301
8349
  background: var(--paper);
@@ -8481,7 +8529,7 @@ function renderShell({
8481
8529
  const articleElement = document.getElementById('article');
8482
8530
  const articleListElement = document.getElementById('articleList');
8483
8531
  const themeToggleButton = document.getElementById('themeToggle');
8484
- const typeOrder = ['article', 'blog-post', 'x-thread', 'x-post', 'linkedin-post', 'reddit-post', 'newsletter', 'landing-page-copy'];
8532
+ const typeOrder = ['article', 'blog-post', 'x-thread', 'x-post', 'linkedin-post', 'reddit-post', 'newsletter'];
8485
8533
  const stageOrder = ['shared-brief', 'planning', 'sections', 'image-prompts', 'images', 'output', 'links'];
8486
8534
 
8487
8535
  let currentGeneration = null;
@@ -9582,7 +9630,7 @@ function formatPipelineStageCost(stage) {
9582
9630
  }
9583
9631
  return stage.costSource === "estimated" ? `~${formatted}` : formatted;
9584
9632
  }
9585
- async function renderPlainPipeline(input, dryRun, enrichLinks2, runMode) {
9633
+ async function renderPlainPipeline(input, dryRun, enrichLinks2, runMode, links, unlinks, maxLinks) {
9586
9634
  let previousStages = /* @__PURE__ */ new Map();
9587
9635
  let previousItemStatuses = /* @__PURE__ */ new Map();
9588
9636
  const notificationsEnabled = input.config.settings.notifications.enabled;
@@ -9596,6 +9644,9 @@ async function renderPlainPipeline(input, dryRun, enrichLinks2, runMode) {
9596
9644
  dryRun,
9597
9645
  enrichLinks: enrichLinks2,
9598
9646
  runMode,
9647
+ customLinks: links,
9648
+ unlinks,
9649
+ maxLinks,
9599
9650
  onUpdate(stages) {
9600
9651
  for (const stage of stages) {
9601
9652
  const previous = previousStages.get(stage.id);
@@ -9954,6 +10005,9 @@ function WriteApp({
9954
10005
  dryRun,
9955
10006
  enrichLinks: enrichLinks2,
9956
10007
  runMode,
10008
+ links,
10009
+ unlinks,
10010
+ maxLinks,
9957
10011
  onError
9958
10012
  }) {
9959
10013
  const { exit } = useApp3();
@@ -9977,6 +10031,9 @@ function WriteApp({
9977
10031
  dryRun,
9978
10032
  enrichLinks: enrichLinks2,
9979
10033
  runMode,
10034
+ customLinks: links,
10035
+ unlinks,
10036
+ maxLinks,
9980
10037
  onUpdate(nextStages) {
9981
10038
  if (mounted) {
9982
10039
  setStages(nextStages);
@@ -10009,7 +10066,7 @@ function WriteApp({
10009
10066
  return () => {
10010
10067
  mounted = false;
10011
10068
  };
10012
- }, [dryRun, enrichLinks2, input, onError, runMode]);
10069
+ }, [dryRun, enrichLinks2, input, links, unlinks, maxLinks, onError, runMode]);
10013
10070
  useEffect2(() => {
10014
10071
  if (!result && !errorMessage) {
10015
10072
  return;
@@ -10025,7 +10082,7 @@ function WriteApp({
10025
10082
  }
10026
10083
  async function runWriteCommand(options) {
10027
10084
  const input = await resolveInputWithInteractiveIdeaFallback(options);
10028
- await runWritePipeline(input, options.dryRun, options.enrichLinks, "fresh", options.noInteractive);
10085
+ await runWritePipeline(input, options.dryRun, options.enrichLinks, "fresh", options.noInteractive, options.links, options.unlinks, options.maxLinks);
10029
10086
  }
10030
10087
  async function runWriteResumeCommand(options = {}) {
10031
10088
  const session = await loadWriteSession();
@@ -10047,9 +10104,9 @@ async function runWriteResumeCommand(options = {}) {
10047
10104
  secrets: resolved.config.secrets
10048
10105
  }
10049
10106
  };
10050
- await runWritePipeline(input, session.dryRun, options.enrichLinks ?? false, "resume", options.noInteractive ?? false);
10107
+ await runWritePipeline(input, session.dryRun, options.enrichLinks ?? false, "resume", options.noInteractive ?? false, options.links, options.unlinks, options.maxLinks);
10051
10108
  }
10052
- async function runWritePipeline(input, dryRun, enrichLinks2, runMode, noInteractive) {
10109
+ async function runWritePipeline(input, dryRun, enrichLinks2, runMode, noInteractive, links, unlinks, maxLinks) {
10053
10110
  let interruptHandled = false;
10054
10111
  const handleSignal = (signal) => {
10055
10112
  if (interruptHandled) {
@@ -10083,7 +10140,7 @@ async function runWritePipeline(input, dryRun, enrichLinks2, runMode, noInteract
10083
10140
  process.on("SIGTERM", onSigterm);
10084
10141
  try {
10085
10142
  if (noInteractive || !process.stdout.isTTY) {
10086
- await renderPlainPipeline(input, dryRun, enrichLinks2, runMode);
10143
+ await renderPlainPipeline(input, dryRun, enrichLinks2, runMode, links, unlinks, maxLinks);
10087
10144
  return;
10088
10145
  }
10089
10146
  let commandError = null;
@@ -10095,6 +10152,9 @@ async function runWritePipeline(input, dryRun, enrichLinks2, runMode, noInteract
10095
10152
  dryRun,
10096
10153
  enrichLinks: enrichLinks2,
10097
10154
  runMode,
10155
+ links,
10156
+ unlinks,
10157
+ maxLinks,
10098
10158
  onError: (error) => {
10099
10159
  commandError = error;
10100
10160
  }
@@ -10298,10 +10358,13 @@ async function runCli(argv) {
10298
10358
  force: options.force
10299
10359
  });
10300
10360
  });
10301
- program.command("links").description("Run link enrichment for a previously generated article by slug.").argument("<slug>", "Slug of the generated article to enrich").option("--mode <mode>", "Link merge mode: fresh or append", "fresh").action(async (slug, options) => {
10361
+ program.command("links").description("Run link enrichment for a previously generated article by slug.").argument("<slug>", "Slug of the generated article to enrich").option("--mode <mode>", "Link merge mode: fresh or append", "fresh").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)).action(async (slug, options) => {
10302
10362
  await runLinksCommand({
10303
10363
  slug,
10304
- mode: options.mode
10364
+ mode: options.mode,
10365
+ links: options.link,
10366
+ unlinks: options.unlink,
10367
+ maxLinks: options.maxLinks
10305
10368
  });
10306
10369
  });
10307
10370
  program.command("preview").description("Preview a generated article in a local browser with linked assets.").argument("[markdownPath]", "Path to the markdown file to preview").option("-p, --port <port>", "Port for the local preview server (default: 4173)").option("--no-open", "Do not auto-open browser after server startup").option("--watch", "Rebuild the preview UI on source changes and auto-reload the browser", false).action(async (markdownPath, options) => {
@@ -10312,7 +10375,7 @@ async function runCli(argv) {
10312
10375
  watch: options.watch
10313
10376
  });
10314
10377
  });
10315
- const writeCommand = program.command("write").description("Generate one primary content output plus optional secondary outputs from a prompt or job file.").argument("[idea]", "Natural-language idea for the generation run").option("-i, --idea <idea>", "Natural-language idea for the generation run").option("--audience <description>", "Optional natural-language audience description for shared-brief targeting").option("-j, --job <path>", "Path to a JSON job definition").option("--primary <type=count>", "Required primary output target (for example: article=1 or x-post=1)").option("--secondary <type=count>", "Secondary output target, repeatable (for example: x-thread=3, linkedin-post=2)", collectOptionValue).option("--style <style>", "Writing style (academic, analytical, authoritative, conversational, empathetic, friendly, journalistic, minimalist, persuasive, playful, professional, storytelling, technical)").option("--intent <intent>", "Content intent (announcement, case-study, cornerstone, counterargument, critique-review, deep-dive-analysis, how-to-guide, interview-q-and-a, listicle, opinion-piece, personal-essay, roundup-curation, tutorial)").option("--length <size>", "Target length: small, medium, large, or a positive integer word count").option("--no-interactive", "Fail instead of prompting for missing input in TTY mode").option("--dry-run", "Run the pipeline shell without external API calls", false).option("--enrich-links", "Run link enrichment after markdown generation", false).action(async (ideaArg, options) => {
10378
+ const writeCommand = program.command("write").description("Generate one primary content output plus optional secondary outputs from a prompt or job file.").argument("[idea]", "Natural-language idea for the generation run").option("-i, --idea <idea>", "Natural-language idea for the generation run").option("--audience <description>", "Optional natural-language audience description for shared-brief targeting").option("-j, --job <path>", "Path to a JSON job definition").option("--primary <type=count>", "Required primary output target (for example: article=1 or x-post=1)").option("--secondary <type=count>", "Secondary output target, repeatable (for example: x-thread=3, linkedin-post=2)", collectOptionValue).option("--style <style>", "Writing style (academic, analytical, authoritative, conversational, empathetic, friendly, journalistic, minimalist, persuasive, playful, professional, storytelling, technical)").option("--intent <intent>", "Content intent (announcement, case-study, cornerstone, counterargument, critique-review, deep-dive-analysis, how-to-guide, interview-q-and-a, listicle, opinion-piece, personal-essay, roundup-curation, tutorial)").option("--length <size>", "Target length: small, medium, large, or a positive integer word count").option("--no-interactive", "Fail instead of prompting for missing input in TTY mode").option("--dry-run", "Run the pipeline shell without external API calls", false).option("--enrich-links", "Run link enrichment after markdown generation", false).option("--link <pair>", 'Custom link "expression->url", repeatable', collectOptionValue).option("--unlink <expression>", "Remove a custom link by expression, repeatable", collectOptionValue).option("--max-links <n>", "Max number of generated links", (v) => Number.parseInt(v, 10)).action(async (ideaArg, options) => {
10316
10379
  await runWriteCommand({
10317
10380
  idea: options.idea ?? ideaArg,
10318
10381
  audience: options.audience,
@@ -10324,13 +10387,19 @@ async function runCli(argv) {
10324
10387
  length: options.length,
10325
10388
  noInteractive: !options.interactive,
10326
10389
  dryRun: options.dryRun,
10327
- enrichLinks: options.enrichLinks
10390
+ enrichLinks: options.enrichLinks,
10391
+ links: options.link,
10392
+ unlinks: options.unlink,
10393
+ maxLinks: options.maxLinks
10328
10394
  });
10329
10395
  });
10330
- writeCommand.command("resume").description("Resume the last failed or interrupted write session from .ideon/write.").option("--no-interactive", "Force plain non-interactive output even in TTY mode", false).option("--enrich-links", "Run link enrichment after markdown generation", false).action(async (options) => {
10396
+ writeCommand.command("resume").description("Resume the last failed or interrupted write session from .ideon/write.").option("--no-interactive", "Force plain non-interactive output even in TTY mode", false).option("--enrich-links", "Run link enrichment after markdown generation", false).option("--link <pair>", 'Custom link "expression->url", repeatable', collectOptionValue).option("--unlink <expression>", "Remove a custom link by expression, repeatable", collectOptionValue).option("--max-links <n>", "Max number of generated links", (v) => Number.parseInt(v, 10)).action(async (options) => {
10331
10397
  await runWriteResumeCommand({
10332
10398
  noInteractive: options.noInteractive,
10333
- enrichLinks: options.enrichLinks
10399
+ enrichLinks: options.enrichLinks,
10400
+ links: options.link,
10401
+ unlinks: options.unlink,
10402
+ maxLinks: options.maxLinks
10334
10403
  });
10335
10404
  });
10336
10405
  await program.parseAsync(argv);