@telepat/ideon 0.1.13 → 0.1.15
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/README.md +35 -33
- package/README.zh-CN.md +116 -0
- package/dist/ideon.js +563 -342
- package/dist/preview/app-assets/{index-FIB5ZtVD.js → index-D3k0cA_1.js} +33 -33
- package/dist/preview/app-assets/{index-NfIf2cKW.css → index-IigmpN5C.css} +1 -1
- package/dist/preview/index.html +2 -2
- package/package.json +1 -1
- package/README.zh-Hans.md +0 -114
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),
|
|
@@ -966,12 +972,18 @@ var writeToolInputSchema = {
|
|
|
966
972
|
intent: z3.enum(contentIntentValues).optional(),
|
|
967
973
|
length: z3.union([z3.enum(targetLengthValues), z3.coerce.number().int().positive()]).optional(),
|
|
968
974
|
dryRun: z3.boolean().optional(),
|
|
969
|
-
enrichLinks: z3.boolean().optional()
|
|
975
|
+
enrichLinks: z3.boolean().optional(),
|
|
976
|
+
link: z3.array(z3.string()).optional(),
|
|
977
|
+
unlink: z3.array(z3.string()).optional(),
|
|
978
|
+
maxLinks: z3.coerce.number().int().positive().optional()
|
|
970
979
|
};
|
|
971
980
|
var writeToolInputZodSchema = z3.object(writeToolInputSchema);
|
|
972
981
|
var writeResumeToolInputSchema = {
|
|
973
982
|
dryRun: z3.boolean().optional(),
|
|
974
|
-
enrichLinks: z3.boolean().optional()
|
|
983
|
+
enrichLinks: z3.boolean().optional(),
|
|
984
|
+
link: z3.array(z3.string()).optional(),
|
|
985
|
+
unlink: z3.array(z3.string()).optional(),
|
|
986
|
+
maxLinks: z3.coerce.number().int().positive().optional()
|
|
975
987
|
};
|
|
976
988
|
var writeResumeToolInputZodSchema = z3.object(writeResumeToolInputSchema);
|
|
977
989
|
var deleteToolInputSchema = {
|
|
@@ -987,6 +999,20 @@ var configSetToolInputSchema = {
|
|
|
987
999
|
value: z3.string()
|
|
988
1000
|
};
|
|
989
1001
|
var configSetToolInputZodSchema = z3.object(configSetToolInputSchema);
|
|
1002
|
+
var linksToolInputSchema = {
|
|
1003
|
+
slug: z3.string().min(1),
|
|
1004
|
+
mode: z3.enum(["fresh", "append"]).optional(),
|
|
1005
|
+
link: z3.array(z3.string()).optional(),
|
|
1006
|
+
unlink: z3.array(z3.string()).optional(),
|
|
1007
|
+
maxLinks: z3.coerce.number().int().positive().optional()
|
|
1008
|
+
};
|
|
1009
|
+
var linksToolInputZodSchema = z3.object(linksToolInputSchema);
|
|
1010
|
+
var configListToolInputSchema = {};
|
|
1011
|
+
var configListToolInputZodSchema = z3.object(configListToolInputSchema);
|
|
1012
|
+
var configUnsetToolInputSchema = {
|
|
1013
|
+
key: z3.enum(configKeys)
|
|
1014
|
+
};
|
|
1015
|
+
var configUnsetToolInputZodSchema = z3.object(configUnsetToolInputSchema);
|
|
990
1016
|
var ideonToolContracts = [
|
|
991
1017
|
{
|
|
992
1018
|
name: "ideon_write",
|
|
@@ -997,6 +1023,23 @@ var ideonToolContracts = [
|
|
|
997
1023
|
length: [...targetLengthValues]
|
|
998
1024
|
}
|
|
999
1025
|
},
|
|
1026
|
+
{
|
|
1027
|
+
name: "ideon_write_resume",
|
|
1028
|
+
required: [],
|
|
1029
|
+
enums: {}
|
|
1030
|
+
},
|
|
1031
|
+
{
|
|
1032
|
+
name: "ideon_delete",
|
|
1033
|
+
required: ["slug"],
|
|
1034
|
+
enums: {}
|
|
1035
|
+
},
|
|
1036
|
+
{
|
|
1037
|
+
name: "ideon_links",
|
|
1038
|
+
required: ["slug"],
|
|
1039
|
+
enums: {
|
|
1040
|
+
mode: ["fresh", "append"]
|
|
1041
|
+
}
|
|
1042
|
+
},
|
|
1000
1043
|
{
|
|
1001
1044
|
name: "ideon_config_set",
|
|
1002
1045
|
required: ["key", "value"],
|
|
@@ -1010,6 +1053,18 @@ var ideonToolContracts = [
|
|
|
1010
1053
|
enums: {
|
|
1011
1054
|
key: [...configKeys]
|
|
1012
1055
|
}
|
|
1056
|
+
},
|
|
1057
|
+
{
|
|
1058
|
+
name: "ideon_config_list",
|
|
1059
|
+
required: [],
|
|
1060
|
+
enums: {}
|
|
1061
|
+
},
|
|
1062
|
+
{
|
|
1063
|
+
name: "ideon_config_unset",
|
|
1064
|
+
required: ["key"],
|
|
1065
|
+
enums: {
|
|
1066
|
+
key: [...configKeys]
|
|
1067
|
+
}
|
|
1013
1068
|
}
|
|
1014
1069
|
];
|
|
1015
1070
|
|
|
@@ -1299,7 +1354,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
1299
1354
|
// package.json
|
|
1300
1355
|
var package_default = {
|
|
1301
1356
|
name: "@telepat/ideon",
|
|
1302
|
-
version: "0.1.
|
|
1357
|
+
version: "0.1.15",
|
|
1303
1358
|
description: "CLI for generating rich articles and images from ideas.",
|
|
1304
1359
|
type: "module",
|
|
1305
1360
|
repository: {
|
|
@@ -1508,7 +1563,7 @@ import path8 from "path";
|
|
|
1508
1563
|
import { readFile as readFile4 } from "fs/promises";
|
|
1509
1564
|
|
|
1510
1565
|
// src/llm/prompts/linkEnrichment.ts
|
|
1511
|
-
function buildLinkCandidatesJsonSchema() {
|
|
1566
|
+
function buildLinkCandidatesJsonSchema(maxLinks = 10) {
|
|
1512
1567
|
return {
|
|
1513
1568
|
type: "object",
|
|
1514
1569
|
additionalProperties: false,
|
|
@@ -1517,13 +1572,13 @@ function buildLinkCandidatesJsonSchema() {
|
|
|
1517
1572
|
expressions: {
|
|
1518
1573
|
type: "array",
|
|
1519
1574
|
minItems: 0,
|
|
1520
|
-
maxItems:
|
|
1575
|
+
maxItems: maxLinks,
|
|
1521
1576
|
items: { type: "string", minLength: 2 }
|
|
1522
1577
|
}
|
|
1523
1578
|
}
|
|
1524
1579
|
};
|
|
1525
1580
|
}
|
|
1526
|
-
function buildLinkCandidatesMessages(content, contentType) {
|
|
1581
|
+
function buildLinkCandidatesMessages(content, contentType, maxLinks = 10) {
|
|
1527
1582
|
return [
|
|
1528
1583
|
{
|
|
1529
1584
|
role: "system",
|
|
@@ -1539,7 +1594,7 @@ function buildLinkCandidatesMessages(content, contentType) {
|
|
|
1539
1594
|
role: "user",
|
|
1540
1595
|
content: [
|
|
1541
1596
|
`Content type: ${contentType}`,
|
|
1542
|
-
|
|
1597
|
+
`Select up to ${maxLinks} expressions that should become links in this content.`,
|
|
1543
1598
|
"Each expression must be copied exactly from the text and be useful to link.",
|
|
1544
1599
|
"",
|
|
1545
1600
|
"Content:",
|
|
@@ -1594,6 +1649,8 @@ async function enrichLinks({
|
|
|
1594
1649
|
openRouter,
|
|
1595
1650
|
settings,
|
|
1596
1651
|
dryRun,
|
|
1652
|
+
customLinks = [],
|
|
1653
|
+
maxLinks = 10,
|
|
1597
1654
|
onLlmMetrics,
|
|
1598
1655
|
onItemProgress,
|
|
1599
1656
|
onInteraction
|
|
@@ -1615,7 +1672,8 @@ async function enrichLinks({
|
|
|
1615
1672
|
fileId: item.fileId,
|
|
1616
1673
|
contentType: item.contentType,
|
|
1617
1674
|
markdownPath: item.markdownPath,
|
|
1618
|
-
links: []
|
|
1675
|
+
links: [],
|
|
1676
|
+
customLinks
|
|
1619
1677
|
});
|
|
1620
1678
|
continue;
|
|
1621
1679
|
}
|
|
@@ -1634,7 +1692,8 @@ async function enrichLinks({
|
|
|
1634
1692
|
fileId: item.fileId,
|
|
1635
1693
|
contentType: item.contentType,
|
|
1636
1694
|
markdownPath: item.markdownPath,
|
|
1637
|
-
links: []
|
|
1695
|
+
links: [],
|
|
1696
|
+
customLinks
|
|
1638
1697
|
});
|
|
1639
1698
|
continue;
|
|
1640
1699
|
}
|
|
@@ -1645,8 +1704,8 @@ async function enrichLinks({
|
|
|
1645
1704
|
});
|
|
1646
1705
|
const candidateResult = await openRouter.requestStructured({
|
|
1647
1706
|
schemaName: "link_candidates",
|
|
1648
|
-
schema: buildLinkCandidatesJsonSchema(),
|
|
1649
|
-
messages: buildLinkCandidatesMessages(content, item.contentType),
|
|
1707
|
+
schema: buildLinkCandidatesJsonSchema(maxLinks),
|
|
1708
|
+
messages: buildLinkCandidatesMessages(content, item.contentType, maxLinks),
|
|
1650
1709
|
settings,
|
|
1651
1710
|
reasoning: LINKS_REASONING_SETTINGS,
|
|
1652
1711
|
interactionContext: {
|
|
@@ -1657,7 +1716,10 @@ async function enrichLinks({
|
|
|
1657
1716
|
parse(data) {
|
|
1658
1717
|
const record = data;
|
|
1659
1718
|
const expressions = Array.isArray(record.expressions) ? record.expressions.filter((value2) => typeof value2 === "string") : [];
|
|
1660
|
-
|
|
1719
|
+
const customExpressions = new Set(customLinks.map((e) => e.expression.trim().toLowerCase()));
|
|
1720
|
+
return {
|
|
1721
|
+
expressions: dedupeExpressions(expressions).filter((expr) => !customExpressions.has(expr.trim().toLowerCase())).slice(0, maxLinks)
|
|
1722
|
+
};
|
|
1661
1723
|
},
|
|
1662
1724
|
onMetrics(metrics) {
|
|
1663
1725
|
onLlmMetrics?.(item.fileId, metrics);
|
|
@@ -1734,7 +1796,8 @@ async function enrichLinks({
|
|
|
1734
1796
|
fileId: item.fileId,
|
|
1735
1797
|
contentType: item.contentType,
|
|
1736
1798
|
markdownPath: item.markdownPath,
|
|
1737
|
-
links
|
|
1799
|
+
links,
|
|
1800
|
+
customLinks
|
|
1738
1801
|
});
|
|
1739
1802
|
}
|
|
1740
1803
|
return results;
|
|
@@ -4355,7 +4418,8 @@ var linksResultSchema = z6.object({
|
|
|
4355
4418
|
fileId: z6.string().min(1),
|
|
4356
4419
|
contentType: z6.string().min(1),
|
|
4357
4420
|
markdownPath: z6.string().min(1),
|
|
4358
|
-
links: z6.array(linkEntrySchema)
|
|
4421
|
+
links: z6.array(linkEntrySchema),
|
|
4422
|
+
customLinks: z6.array(linkEntrySchema).default([])
|
|
4359
4423
|
});
|
|
4360
4424
|
var pipelineArtifactSummarySchema = z6.object({
|
|
4361
4425
|
title: z6.string().min(1),
|
|
@@ -4553,6 +4617,9 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4553
4617
|
const shouldEnrichLinks = options.enrichLinks ?? false;
|
|
4554
4618
|
const runMode = options.runMode ?? "fresh";
|
|
4555
4619
|
const workingDir = options.workingDir ?? process.cwd();
|
|
4620
|
+
const pipelineCustomLinkRaws = options.customLinks ?? [];
|
|
4621
|
+
const pipelineUnlinks = options.unlinks ?? [];
|
|
4622
|
+
const pipelineMaxLinks = options.maxLinks;
|
|
4556
4623
|
const outputPaths = resolveOutputPaths(input.config.settings, workingDir);
|
|
4557
4624
|
const hasArticlePrimary = isArticlePrimary;
|
|
4558
4625
|
const stageTracking = /* @__PURE__ */ new Map();
|
|
@@ -5422,15 +5489,19 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5422
5489
|
options.onUpdate?.(cloneStages(stages));
|
|
5423
5490
|
} else if (linksResult) {
|
|
5424
5491
|
const linksByFileId = new Map(linksResult.map((item) => [item.fileId, item.links]));
|
|
5425
|
-
|
|
5492
|
+
const customLinksByFileId = new Map(linksResult.map((item) => [item.fileId, item.customLinks]));
|
|
5493
|
+
const resumedLinks = eligibleOutputsForLinks.map((output) => ({
|
|
5426
5494
|
fileId: output.fileId,
|
|
5427
5495
|
contentType: output.contentType,
|
|
5428
5496
|
markdownPath: output.markdownPath,
|
|
5429
|
-
links: linksByFileId.get(output.fileId) ?? []
|
|
5497
|
+
links: linksByFileId.get(output.fileId) ?? [],
|
|
5498
|
+
customLinks: customLinksByFileId.get(output.fileId) ?? []
|
|
5430
5499
|
}));
|
|
5431
|
-
|
|
5500
|
+
linksResult = resumedLinks;
|
|
5501
|
+
for (const item of resumedLinks) {
|
|
5432
5502
|
await writeLinksFile(item.markdownPath, {
|
|
5433
|
-
version:
|
|
5503
|
+
version: 2,
|
|
5504
|
+
customLinks: item.customLinks,
|
|
5434
5505
|
links: item.links
|
|
5435
5506
|
});
|
|
5436
5507
|
}
|
|
@@ -5439,7 +5510,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5439
5510
|
...stages[6],
|
|
5440
5511
|
status: "succeeded",
|
|
5441
5512
|
detail: "Reused saved link metadata from .ideon/write.",
|
|
5442
|
-
summary: `${
|
|
5513
|
+
summary: `${resumedLinks.reduce((sum, item) => sum + item.links.length, 0)} links`,
|
|
5443
5514
|
items: (stages[6].items ?? []).map((item) => ({
|
|
5444
5515
|
...item,
|
|
5445
5516
|
status: "succeeded",
|
|
@@ -5457,6 +5528,8 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5457
5528
|
openRouter,
|
|
5458
5529
|
settings: input.config.settings,
|
|
5459
5530
|
dryRun,
|
|
5531
|
+
customLinks: parsePipelineCustomLinks(pipelineCustomLinkRaws, pipelineUnlinks),
|
|
5532
|
+
maxLinks: pipelineMaxLinks ?? resolveDefaultMaxLinks(input.config.settings.targetLength),
|
|
5460
5533
|
onInteraction(interaction) {
|
|
5461
5534
|
onLlmInteraction(interaction);
|
|
5462
5535
|
},
|
|
@@ -5501,7 +5574,8 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5501
5574
|
costSource
|
|
5502
5575
|
});
|
|
5503
5576
|
await writeLinksFile(item.markdownPath, {
|
|
5504
|
-
version:
|
|
5577
|
+
version: 2,
|
|
5578
|
+
customLinks: item.customLinks,
|
|
5505
5579
|
links: item.links
|
|
5506
5580
|
});
|
|
5507
5581
|
}
|
|
@@ -5866,7 +5940,6 @@ function toFilePrefix(contentType) {
|
|
|
5866
5940
|
if (contentType === "reddit-post") return "reddit";
|
|
5867
5941
|
if (contentType === "linkedin-post") return "linkedin";
|
|
5868
5942
|
if (contentType === "newsletter") return "newsletter";
|
|
5869
|
-
if (contentType === "landing-page-copy") return "landing";
|
|
5870
5943
|
return contentType.replace(/[^a-z0-9]+/gi, "-").replace(/^-+|-+$/g, "").toLowerCase() || "content";
|
|
5871
5944
|
}
|
|
5872
5945
|
function getPrimaryTarget(contentTargets) {
|
|
@@ -5962,71 +6035,351 @@ function asWriteStageId(stageId) {
|
|
|
5962
6035
|
}
|
|
5963
6036
|
return null;
|
|
5964
6037
|
}
|
|
6038
|
+
function parsePipelineCustomLinks(rawLinks, unlinks) {
|
|
6039
|
+
const result = /* @__PURE__ */ new Map();
|
|
6040
|
+
for (const raw of rawLinks) {
|
|
6041
|
+
const separatorIndex = raw.indexOf("->");
|
|
6042
|
+
if (separatorIndex < 0) {
|
|
6043
|
+
continue;
|
|
6044
|
+
}
|
|
6045
|
+
const expression = raw.slice(0, separatorIndex).trim();
|
|
6046
|
+
const url = raw.slice(separatorIndex + 2).trim();
|
|
6047
|
+
if (expression && url) {
|
|
6048
|
+
result.set(expression.toLowerCase(), { expression, url, title: null });
|
|
6049
|
+
}
|
|
6050
|
+
}
|
|
6051
|
+
for (const expr of unlinks) {
|
|
6052
|
+
result.delete(expr.trim().toLowerCase());
|
|
6053
|
+
}
|
|
6054
|
+
return Array.from(result.values());
|
|
6055
|
+
}
|
|
5965
6056
|
|
|
5966
|
-
// src/cli/commands/
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
6057
|
+
// src/cli/commands/links.ts
|
|
6058
|
+
import { readFile as readFile6, stat as stat3 } from "fs/promises";
|
|
6059
|
+
import path9 from "path";
|
|
6060
|
+
async function runLinksCommand(options, dependencies = {}) {
|
|
6061
|
+
const slug = normalizeSlug2(options.slug);
|
|
6062
|
+
const mode = normalizeMode(options.mode);
|
|
6063
|
+
const cwd2 = dependencies.cwd ?? process.cwd();
|
|
6064
|
+
const log = dependencies.log ?? ((message) => console.log(message));
|
|
6065
|
+
const resolved = await resolveRunInput({
|
|
6066
|
+
idea: `Enrich links for ${slug}`
|
|
6067
|
+
});
|
|
6068
|
+
const markdownPath = await resolveMarkdownPathForSlug2(resolved.config.settings, slug, cwd2);
|
|
6069
|
+
const frontmatter = await readFrontmatter(markdownPath);
|
|
6070
|
+
const fileId = path9.parse(markdownPath).name;
|
|
6071
|
+
const articleTitle = frontmatter.title ?? toTitleFromSlug(slug);
|
|
6072
|
+
const articleDescription = frontmatter.description ?? "";
|
|
6073
|
+
const openRouterApiKey = resolved.config.secrets.openRouterApiKey;
|
|
6074
|
+
if (!openRouterApiKey) {
|
|
6075
|
+
throw new ReportedError(
|
|
6076
|
+
"Missing OpenRouter API key. Run `ideon settings` to configure credentials or set IDEON_OPENROUTER_API_KEY."
|
|
6077
|
+
);
|
|
5971
6078
|
}
|
|
5972
|
-
const
|
|
5973
|
-
|
|
5974
|
-
|
|
6079
|
+
const openRouter = new OpenRouterClient(openRouterApiKey);
|
|
6080
|
+
const linksPath = resolveLinksPath(markdownPath);
|
|
6081
|
+
const existing = await readExistingLinks(linksPath);
|
|
6082
|
+
const updatedCustomLinks = resolveCustomLinks(existing?.customLinks ?? [], options.links ?? [], options.unlinks ?? []);
|
|
6083
|
+
const effectiveMaxLinks = options.maxLinks;
|
|
6084
|
+
const linksResult = await enrichLinks({
|
|
6085
|
+
markdownFiles: [{ markdownPath, fileId, contentType: "article" }],
|
|
6086
|
+
articleTitle,
|
|
6087
|
+
articleDescription,
|
|
6088
|
+
openRouter,
|
|
6089
|
+
settings: resolved.config.settings,
|
|
6090
|
+
dryRun: false,
|
|
6091
|
+
customLinks: updatedCustomLinks,
|
|
6092
|
+
maxLinks: effectiveMaxLinks,
|
|
6093
|
+
onItemProgress(event) {
|
|
6094
|
+
logProgress(event, log);
|
|
6095
|
+
}
|
|
6096
|
+
});
|
|
6097
|
+
const generatedLinks = linksResult[0]?.links ?? [];
|
|
6098
|
+
const mergedGeneratedLinks = mode === "append" ? mergeLinks(existing?.links ?? [], generatedLinks) : generatedLinks;
|
|
6099
|
+
const appendedCount = Math.max(0, mergedGeneratedLinks.length - (existing?.links.length ?? 0));
|
|
6100
|
+
await writeLinksFile(markdownPath, {
|
|
6101
|
+
version: 2,
|
|
6102
|
+
customLinks: updatedCustomLinks,
|
|
6103
|
+
links: mergedGeneratedLinks
|
|
6104
|
+
});
|
|
6105
|
+
const relativeMarkdownPath = formatRelativePath2(cwd2, markdownPath);
|
|
6106
|
+
const relativeLinksPath = formatRelativePath2(cwd2, linksPath);
|
|
6107
|
+
if (mode === "fresh") {
|
|
6108
|
+
const replaced = existing ? "Replaced existing links." : "Created links sidecar.";
|
|
6109
|
+
log(`Enriched links for "${slug}".`);
|
|
6110
|
+
log(`${replaced} Saved ${generatedLinks.length} generated + ${updatedCustomLinks.length} custom links to ${relativeLinksPath} (${relativeMarkdownPath}).`);
|
|
6111
|
+
return;
|
|
5975
6112
|
}
|
|
5976
|
-
const
|
|
5977
|
-
|
|
6113
|
+
const baseCount = existing?.links.length ?? 0;
|
|
6114
|
+
const verb = existing ? "Appended and deduplicated links." : "Created links sidecar.";
|
|
6115
|
+
log(`Enriched links for "${slug}".`);
|
|
6116
|
+
log(`${verb} Base ${baseCount}, added ${appendedCount}, total ${mergedGeneratedLinks.length} generated + ${updatedCustomLinks.length} custom in ${relativeLinksPath} (${relativeMarkdownPath}).`);
|
|
6117
|
+
}
|
|
6118
|
+
function normalizeMode(rawMode) {
|
|
6119
|
+
const normalized = rawMode.trim().toLowerCase();
|
|
6120
|
+
if (normalized === "fresh" || normalized === "append") {
|
|
6121
|
+
return normalized;
|
|
6122
|
+
}
|
|
6123
|
+
throw new ReportedError(`Unsupported --mode value "${rawMode}". Use "fresh" or "append".`);
|
|
6124
|
+
}
|
|
6125
|
+
function normalizeSlug2(rawSlug) {
|
|
6126
|
+
const slug = rawSlug.trim();
|
|
6127
|
+
if (!slug) {
|
|
6128
|
+
throw new ReportedError("Slug cannot be empty. Pass the generated article slug, for example `ideon links my-article-slug`.");
|
|
6129
|
+
}
|
|
6130
|
+
if (slug.toLowerCase().endsWith(".md")) {
|
|
6131
|
+
throw new ReportedError(`Expected a slug, not a markdown filename: ${slug}. Pass the slug without .md.`);
|
|
6132
|
+
}
|
|
6133
|
+
if (slug === "." || slug === ".." || /[/\\]/.test(slug)) {
|
|
6134
|
+
throw new ReportedError(`Invalid slug "${slug}". Pass the article slug only, without any path separators.`);
|
|
6135
|
+
}
|
|
6136
|
+
return slug;
|
|
6137
|
+
}
|
|
6138
|
+
async function resolveMarkdownPathForSlug2(settings, slug, cwd2) {
|
|
6139
|
+
const outputPaths = resolveOutputPaths(settings, cwd2);
|
|
6140
|
+
const directPath = path9.join(outputPaths.markdownOutputDir, `${slug}.md`);
|
|
6141
|
+
if (await isReadableFile(directPath)) {
|
|
6142
|
+
return directPath;
|
|
6143
|
+
}
|
|
6144
|
+
const markdownFiles = await listMarkdownFilesRecursively(outputPaths.markdownOutputDir);
|
|
6145
|
+
const matches = [];
|
|
6146
|
+
for (const candidate of markdownFiles) {
|
|
6147
|
+
if (path9.basename(candidate) === `${slug}.md`) {
|
|
6148
|
+
matches.push(candidate);
|
|
6149
|
+
continue;
|
|
6150
|
+
}
|
|
6151
|
+
const frontmatter = await readFrontmatter(candidate);
|
|
6152
|
+
if (frontmatter.slug === slug) {
|
|
6153
|
+
matches.push(candidate);
|
|
6154
|
+
}
|
|
6155
|
+
}
|
|
6156
|
+
if (matches.length === 0) {
|
|
5978
6157
|
throw new ReportedError(
|
|
5979
|
-
`
|
|
6158
|
+
`Could not find article "${slug}". Expected a markdown file in ${outputPaths.markdownOutputDir} with frontmatter slug "${slug}".`
|
|
5980
6159
|
);
|
|
5981
6160
|
}
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
6161
|
+
return newestPath(matches);
|
|
6162
|
+
}
|
|
6163
|
+
async function newestPath(paths) {
|
|
6164
|
+
let latestPath = paths[0];
|
|
6165
|
+
let latestMtime = 0;
|
|
6166
|
+
for (const candidate of paths) {
|
|
6167
|
+
const candidateStat = await stat3(candidate);
|
|
6168
|
+
if (candidateStat.mtimeMs >= latestMtime) {
|
|
6169
|
+
latestMtime = candidateStat.mtimeMs;
|
|
6170
|
+
latestPath = candidate;
|
|
6171
|
+
}
|
|
5985
6172
|
}
|
|
6173
|
+
return latestPath;
|
|
6174
|
+
}
|
|
6175
|
+
async function readFrontmatter(markdownPath) {
|
|
6176
|
+
const markdown = await readFile6(markdownPath, "utf8");
|
|
6177
|
+
return parseFrontmatter(markdown);
|
|
6178
|
+
}
|
|
6179
|
+
function parseFrontmatter(markdown) {
|
|
6180
|
+
if (!markdown.startsWith("---\n")) {
|
|
6181
|
+
return { slug: null, title: null, description: null };
|
|
6182
|
+
}
|
|
6183
|
+
const frontmatterEnd = markdown.indexOf("\n---\n", 4);
|
|
6184
|
+
if (frontmatterEnd < 0) {
|
|
6185
|
+
return { slug: null, title: null, description: null };
|
|
6186
|
+
}
|
|
6187
|
+
const block = markdown.slice(4, frontmatterEnd);
|
|
5986
6188
|
return {
|
|
5987
|
-
|
|
5988
|
-
|
|
6189
|
+
slug: parseFrontmatterValue(block, "slug"),
|
|
6190
|
+
title: parseFrontmatterValue(block, "title"),
|
|
6191
|
+
description: parseFrontmatterValue(block, "description")
|
|
5989
6192
|
};
|
|
5990
6193
|
}
|
|
5991
|
-
function
|
|
5992
|
-
const
|
|
5993
|
-
|
|
5994
|
-
|
|
6194
|
+
function parseFrontmatterValue(block, key) {
|
|
6195
|
+
const pattern = new RegExp(`^${key}:\\s*(.+)$`, "m");
|
|
6196
|
+
const match = block.match(pattern);
|
|
6197
|
+
if (!match || !match[1]) {
|
|
6198
|
+
return null;
|
|
5995
6199
|
}
|
|
5996
|
-
|
|
5997
|
-
|
|
6200
|
+
const rawValue = match[1].trim();
|
|
6201
|
+
if (!rawValue) {
|
|
6202
|
+
return null;
|
|
5998
6203
|
}
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
throw new ReportedError("Primary target count must be exactly 1. Use --primary <content-type=1>.");
|
|
6204
|
+
if (rawValue.startsWith('"') && rawValue.endsWith('"') || rawValue.startsWith("'") && rawValue.endsWith("'")) {
|
|
6205
|
+
return rawValue.slice(1, -1);
|
|
6002
6206
|
}
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6207
|
+
return rawValue;
|
|
6208
|
+
}
|
|
6209
|
+
function toTitleFromSlug(slug) {
|
|
6210
|
+
return slug.split("-").filter((part) => part.length > 0).map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ");
|
|
6211
|
+
}
|
|
6212
|
+
async function isReadableFile(filePath) {
|
|
6213
|
+
try {
|
|
6214
|
+
const fileStat = await stat3(filePath);
|
|
6215
|
+
return fileStat.isFile();
|
|
6216
|
+
} catch {
|
|
6217
|
+
return false;
|
|
6218
|
+
}
|
|
6219
|
+
}
|
|
6220
|
+
async function readExistingLinks(linksPath) {
|
|
6221
|
+
try {
|
|
6222
|
+
const raw = await readFile6(linksPath, "utf8");
|
|
6223
|
+
const parsed = JSON.parse(raw);
|
|
6224
|
+
const links = Array.isArray(parsed.links) ? parsed.links.filter((entry) => isValidLinkEntry(entry)).map((entry) => ({
|
|
6225
|
+
expression: entry.expression.trim(),
|
|
6226
|
+
url: entry.url.trim(),
|
|
6227
|
+
title: typeof entry.title === "string" ? entry.title : null
|
|
6228
|
+
})) : null;
|
|
6229
|
+
if (!links) {
|
|
6230
|
+
throw new ReportedError(`Invalid links sidecar format at ${linksPath}. Expected { version, links[] }.`);
|
|
6010
6231
|
}
|
|
6011
|
-
const
|
|
6012
|
-
|
|
6013
|
-
|
|
6232
|
+
const customLinks = Array.isArray(parsed.customLinks) ? parsed.customLinks.filter((entry) => isValidLinkEntry(entry)).map((entry) => ({
|
|
6233
|
+
expression: entry.expression.trim(),
|
|
6234
|
+
url: entry.url.trim(),
|
|
6235
|
+
title: typeof entry.title === "string" ? entry.title : null
|
|
6236
|
+
})) : [];
|
|
6237
|
+
return {
|
|
6238
|
+
version: typeof parsed.version === "number" ? parsed.version : 1,
|
|
6239
|
+
customLinks,
|
|
6240
|
+
links
|
|
6241
|
+
};
|
|
6242
|
+
} catch (error) {
|
|
6243
|
+
if (readErrorCode2(error) === "ENOENT") {
|
|
6244
|
+
return null;
|
|
6245
|
+
}
|
|
6246
|
+
if (error instanceof ReportedError) {
|
|
6247
|
+
throw error;
|
|
6248
|
+
}
|
|
6249
|
+
const message = error instanceof Error ? error.message : "Unknown links sidecar read error.";
|
|
6250
|
+
throw new ReportedError(`Failed to read existing links from ${linksPath}: ${message}`);
|
|
6251
|
+
}
|
|
6252
|
+
}
|
|
6253
|
+
function mergeLinks(existingLinks, generatedLinks) {
|
|
6254
|
+
const merged = [];
|
|
6255
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6256
|
+
for (const entry of [...existingLinks, ...generatedLinks]) {
|
|
6257
|
+
const key = `${entry.expression.trim().toLowerCase()}::${entry.url.trim().toLowerCase()}`;
|
|
6258
|
+
if (seen.has(key)) {
|
|
6014
6259
|
continue;
|
|
6015
6260
|
}
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
role: "secondary"
|
|
6019
|
-
});
|
|
6261
|
+
seen.add(key);
|
|
6262
|
+
merged.push(entry);
|
|
6020
6263
|
}
|
|
6021
|
-
return
|
|
6022
|
-
{
|
|
6023
|
-
...primary,
|
|
6024
|
-
role: "primary"
|
|
6025
|
-
},
|
|
6026
|
-
...secondaryDedupedByType.values()
|
|
6027
|
-
];
|
|
6264
|
+
return merged;
|
|
6028
6265
|
}
|
|
6029
|
-
|
|
6266
|
+
function isValidLinkEntry(value2) {
|
|
6267
|
+
if (typeof value2 !== "object" || value2 === null) {
|
|
6268
|
+
return false;
|
|
6269
|
+
}
|
|
6270
|
+
const record = value2;
|
|
6271
|
+
return typeof record.expression === "string" && typeof record.url === "string" && (typeof record.title === "string" || record.title === null || record.title === void 0);
|
|
6272
|
+
}
|
|
6273
|
+
function readErrorCode2(error) {
|
|
6274
|
+
if (typeof error !== "object" || error === null || !("code" in error)) {
|
|
6275
|
+
return null;
|
|
6276
|
+
}
|
|
6277
|
+
const code = error.code;
|
|
6278
|
+
return typeof code === "string" ? code : null;
|
|
6279
|
+
}
|
|
6280
|
+
function formatRelativePath2(cwd2, targetPath) {
|
|
6281
|
+
const relativePath = path9.relative(cwd2, targetPath);
|
|
6282
|
+
return relativePath.length > 0 ? relativePath : targetPath;
|
|
6283
|
+
}
|
|
6284
|
+
function logProgress(event, log) {
|
|
6285
|
+
if (event.phase === "resolving-expression" || event.phase === "selecting-expressions") {
|
|
6286
|
+
return;
|
|
6287
|
+
}
|
|
6288
|
+
log(event.detail);
|
|
6289
|
+
}
|
|
6290
|
+
function parseCustomLinkFlag(raw) {
|
|
6291
|
+
const separatorIndex = raw.indexOf("->");
|
|
6292
|
+
if (separatorIndex < 0) {
|
|
6293
|
+
throw new ReportedError(`Invalid --link value "${raw}". Expected format: "expression->url".`);
|
|
6294
|
+
}
|
|
6295
|
+
const expression = raw.slice(0, separatorIndex).trim();
|
|
6296
|
+
const url = raw.slice(separatorIndex + 2).trim();
|
|
6297
|
+
if (!expression) {
|
|
6298
|
+
throw new ReportedError(`Invalid --link value "${raw}": expression (left side of ->) cannot be empty.`);
|
|
6299
|
+
}
|
|
6300
|
+
if (!url) {
|
|
6301
|
+
throw new ReportedError(`Invalid --link value "${raw}": url (right side of ->) cannot be empty.`);
|
|
6302
|
+
}
|
|
6303
|
+
return { expression, url };
|
|
6304
|
+
}
|
|
6305
|
+
function resolveCustomLinks(existing, addRaw, removeExpressions) {
|
|
6306
|
+
const result = new Map(
|
|
6307
|
+
existing.map((entry) => [entry.expression.trim().toLowerCase(), entry])
|
|
6308
|
+
);
|
|
6309
|
+
for (const raw of addRaw) {
|
|
6310
|
+
const { expression, url } = parseCustomLinkFlag(raw);
|
|
6311
|
+
result.set(expression.toLowerCase(), { expression, url, title: null });
|
|
6312
|
+
}
|
|
6313
|
+
for (const expr of removeExpressions) {
|
|
6314
|
+
result.delete(expr.trim().toLowerCase());
|
|
6315
|
+
}
|
|
6316
|
+
return Array.from(result.values());
|
|
6317
|
+
}
|
|
6318
|
+
|
|
6319
|
+
// src/cli/commands/writeTargetSpecs.ts
|
|
6320
|
+
function parseTargetSpec(spec) {
|
|
6321
|
+
const trimmed = spec.trim();
|
|
6322
|
+
if (!trimmed) {
|
|
6323
|
+
throw new ReportedError("Target spec cannot be empty. Use <content-type=count>.");
|
|
6324
|
+
}
|
|
6325
|
+
const [rawType, rawCount] = trimmed.split("=");
|
|
6326
|
+
if (!rawType || !rawCount) {
|
|
6327
|
+
throw new ReportedError(`Invalid target "${spec}". Use format content-type=count, e.g. article=1 or x-thread=3.`);
|
|
6328
|
+
}
|
|
6329
|
+
const contentType = rawType.trim();
|
|
6330
|
+
if (!contentTypeValues.includes(contentType)) {
|
|
6331
|
+
throw new ReportedError(
|
|
6332
|
+
`Unsupported content type "${contentType}". Supported values: ${contentTypeValues.join(", ")}.`
|
|
6333
|
+
);
|
|
6334
|
+
}
|
|
6335
|
+
const count = Number.parseInt(rawCount.trim(), 10);
|
|
6336
|
+
if (!Number.isFinite(count) || count <= 0) {
|
|
6337
|
+
throw new ReportedError(`Invalid count in target "${spec}". Count must be a positive integer.`);
|
|
6338
|
+
}
|
|
6339
|
+
return {
|
|
6340
|
+
contentType,
|
|
6341
|
+
count
|
|
6342
|
+
};
|
|
6343
|
+
}
|
|
6344
|
+
function parsePrimaryAndSecondarySpecs(options) {
|
|
6345
|
+
const { primarySpec, secondarySpecs } = options;
|
|
6346
|
+
if (!primarySpec && (!secondarySpecs || secondarySpecs.length === 0)) {
|
|
6347
|
+
return void 0;
|
|
6348
|
+
}
|
|
6349
|
+
if (!primarySpec) {
|
|
6350
|
+
throw new ReportedError("Missing required --primary <content-type=count>.");
|
|
6351
|
+
}
|
|
6352
|
+
const primary = parseTargetSpec(primarySpec);
|
|
6353
|
+
if (primary.count !== 1) {
|
|
6354
|
+
throw new ReportedError("Primary target count must be exactly 1. Use --primary <content-type=1>.");
|
|
6355
|
+
}
|
|
6356
|
+
const secondaryDedupedByType = /* @__PURE__ */ new Map();
|
|
6357
|
+
for (const spec of secondarySpecs ?? []) {
|
|
6358
|
+
const parsed = parseTargetSpec(spec);
|
|
6359
|
+
if (parsed.contentType === primary.contentType) {
|
|
6360
|
+
throw new ReportedError(
|
|
6361
|
+
`Content type "${parsed.contentType}" cannot be both primary and secondary in the same run.`
|
|
6362
|
+
);
|
|
6363
|
+
}
|
|
6364
|
+
const previous = secondaryDedupedByType.get(parsed.contentType);
|
|
6365
|
+
if (previous) {
|
|
6366
|
+
previous.count += parsed.count;
|
|
6367
|
+
continue;
|
|
6368
|
+
}
|
|
6369
|
+
secondaryDedupedByType.set(parsed.contentType, {
|
|
6370
|
+
...parsed,
|
|
6371
|
+
role: "secondary"
|
|
6372
|
+
});
|
|
6373
|
+
}
|
|
6374
|
+
return [
|
|
6375
|
+
{
|
|
6376
|
+
...primary,
|
|
6377
|
+
role: "primary"
|
|
6378
|
+
},
|
|
6379
|
+
...secondaryDedupedByType.values()
|
|
6380
|
+
];
|
|
6381
|
+
}
|
|
6382
|
+
|
|
6030
6383
|
// src/integrations/mcp/server.ts
|
|
6031
6384
|
async function startIdeonMcpServer() {
|
|
6032
6385
|
const server = new McpServer({
|
|
@@ -6051,6 +6404,7 @@ async function startIdeonMcpServer() {
|
|
|
6051
6404
|
audience: input.audience,
|
|
6052
6405
|
jobPath: input.jobPath,
|
|
6053
6406
|
style: input.style,
|
|
6407
|
+
intent: input.intent,
|
|
6054
6408
|
targetLength: input.length,
|
|
6055
6409
|
contentTargets: parsedTargets
|
|
6056
6410
|
});
|
|
@@ -6058,7 +6412,10 @@ async function startIdeonMcpServer() {
|
|
|
6058
6412
|
workingDir: cwd(),
|
|
6059
6413
|
runMode: "fresh",
|
|
6060
6414
|
dryRun: input.dryRun ?? false,
|
|
6061
|
-
enrichLinks: input.enrichLinks ??
|
|
6415
|
+
enrichLinks: input.enrichLinks ?? false,
|
|
6416
|
+
customLinks: input.link,
|
|
6417
|
+
unlinks: input.unlink,
|
|
6418
|
+
maxLinks: input.maxLinks
|
|
6062
6419
|
});
|
|
6063
6420
|
return {
|
|
6064
6421
|
content: [
|
|
@@ -6114,7 +6471,10 @@ async function startIdeonMcpServer() {
|
|
|
6114
6471
|
workingDir: cwd(),
|
|
6115
6472
|
runMode: "resume",
|
|
6116
6473
|
dryRun: input.dryRun ?? false,
|
|
6117
|
-
enrichLinks: input.enrichLinks ??
|
|
6474
|
+
enrichLinks: input.enrichLinks ?? false,
|
|
6475
|
+
customLinks: input.link,
|
|
6476
|
+
unlinks: input.unlink,
|
|
6477
|
+
maxLinks: input.maxLinks
|
|
6118
6478
|
});
|
|
6119
6479
|
return {
|
|
6120
6480
|
content: [
|
|
@@ -6173,6 +6533,48 @@ async function startIdeonMcpServer() {
|
|
|
6173
6533
|
}
|
|
6174
6534
|
}
|
|
6175
6535
|
);
|
|
6536
|
+
server.registerTool(
|
|
6537
|
+
"ideon_links",
|
|
6538
|
+
{
|
|
6539
|
+
title: "Ideon Links",
|
|
6540
|
+
description: "Run link enrichment for a previously generated article by slug.",
|
|
6541
|
+
inputSchema: linksToolInputSchema
|
|
6542
|
+
},
|
|
6543
|
+
async (input) => {
|
|
6544
|
+
try {
|
|
6545
|
+
const messages = [];
|
|
6546
|
+
await runLinksCommand(
|
|
6547
|
+
{
|
|
6548
|
+
slug: input.slug,
|
|
6549
|
+
mode: input.mode ?? "fresh",
|
|
6550
|
+
links: input.link,
|
|
6551
|
+
unlinks: input.unlink,
|
|
6552
|
+
maxLinks: input.maxLinks
|
|
6553
|
+
},
|
|
6554
|
+
{
|
|
6555
|
+
cwd: cwd(),
|
|
6556
|
+
log: (message) => {
|
|
6557
|
+
messages.push(message);
|
|
6558
|
+
}
|
|
6559
|
+
}
|
|
6560
|
+
);
|
|
6561
|
+
return {
|
|
6562
|
+
content: [
|
|
6563
|
+
{
|
|
6564
|
+
type: "text",
|
|
6565
|
+
text: messages.length > 0 ? messages.join("\n") : `Enriched links for ${input.slug}.`
|
|
6566
|
+
}
|
|
6567
|
+
],
|
|
6568
|
+
structuredContent: {
|
|
6569
|
+
slug: input.slug,
|
|
6570
|
+
mode: input.mode ?? "fresh"
|
|
6571
|
+
}
|
|
6572
|
+
};
|
|
6573
|
+
} catch (error) {
|
|
6574
|
+
return formatToolError(error);
|
|
6575
|
+
}
|
|
6576
|
+
}
|
|
6577
|
+
);
|
|
6176
6578
|
server.registerTool(
|
|
6177
6579
|
"ideon_config_get",
|
|
6178
6580
|
{
|
|
@@ -6234,6 +6636,60 @@ async function startIdeonMcpServer() {
|
|
|
6234
6636
|
}
|
|
6235
6637
|
}
|
|
6236
6638
|
);
|
|
6639
|
+
server.registerTool(
|
|
6640
|
+
"ideon_config_list",
|
|
6641
|
+
{
|
|
6642
|
+
title: "Ideon Config List",
|
|
6643
|
+
description: "List current settings and secret availability flags.",
|
|
6644
|
+
inputSchema: configListToolInputSchema
|
|
6645
|
+
},
|
|
6646
|
+
async (_input) => {
|
|
6647
|
+
try {
|
|
6648
|
+
const result = await configList();
|
|
6649
|
+
return {
|
|
6650
|
+
content: [
|
|
6651
|
+
{
|
|
6652
|
+
type: "text",
|
|
6653
|
+
text: JSON.stringify(result, null, 2)
|
|
6654
|
+
}
|
|
6655
|
+
],
|
|
6656
|
+
structuredContent: result
|
|
6657
|
+
};
|
|
6658
|
+
} catch (error) {
|
|
6659
|
+
return formatToolError(error);
|
|
6660
|
+
}
|
|
6661
|
+
}
|
|
6662
|
+
);
|
|
6663
|
+
server.registerTool(
|
|
6664
|
+
"ideon_config_unset",
|
|
6665
|
+
{
|
|
6666
|
+
title: "Ideon Config Unset",
|
|
6667
|
+
description: "Reset a setting to its default or delete a stored secret.",
|
|
6668
|
+
inputSchema: configUnsetToolInputSchema
|
|
6669
|
+
},
|
|
6670
|
+
async (input) => {
|
|
6671
|
+
try {
|
|
6672
|
+
if (!isConfigKey(input.key)) {
|
|
6673
|
+
throw new ReportedError(`Unsupported config key: ${input.key}`);
|
|
6674
|
+
}
|
|
6675
|
+
await configUnset(input.key);
|
|
6676
|
+
return {
|
|
6677
|
+
content: [
|
|
6678
|
+
{
|
|
6679
|
+
type: "text",
|
|
6680
|
+
text: `Unset ${input.key}.`
|
|
6681
|
+
}
|
|
6682
|
+
],
|
|
6683
|
+
structuredContent: {
|
|
6684
|
+
key: input.key,
|
|
6685
|
+
updated: true
|
|
6686
|
+
}
|
|
6687
|
+
};
|
|
6688
|
+
} catch (error) {
|
|
6689
|
+
return formatToolError(error);
|
|
6690
|
+
}
|
|
6691
|
+
}
|
|
6692
|
+
);
|
|
6237
6693
|
const transport = new StdioServerTransport();
|
|
6238
6694
|
await server.connect(transport);
|
|
6239
6695
|
}
|
|
@@ -6250,229 +6706,6 @@ async function runMcpServeCommand() {
|
|
|
6250
6706
|
await startIdeonMcpServer();
|
|
6251
6707
|
}
|
|
6252
6708
|
|
|
6253
|
-
// src/cli/commands/links.ts
|
|
6254
|
-
import { readFile as readFile6, stat as stat3 } from "fs/promises";
|
|
6255
|
-
import path9 from "path";
|
|
6256
|
-
async function runLinksCommand(options, dependencies = {}) {
|
|
6257
|
-
const slug = normalizeSlug2(options.slug);
|
|
6258
|
-
const mode = normalizeMode(options.mode);
|
|
6259
|
-
const cwd2 = dependencies.cwd ?? process.cwd();
|
|
6260
|
-
const log = dependencies.log ?? ((message) => console.log(message));
|
|
6261
|
-
const resolved = await resolveRunInput({
|
|
6262
|
-
idea: `Enrich links for ${slug}`
|
|
6263
|
-
});
|
|
6264
|
-
const markdownPath = await resolveMarkdownPathForSlug2(resolved.config.settings, slug, cwd2);
|
|
6265
|
-
const frontmatter = await readFrontmatter(markdownPath);
|
|
6266
|
-
const fileId = path9.parse(markdownPath).name;
|
|
6267
|
-
const articleTitle = frontmatter.title ?? toTitleFromSlug(slug);
|
|
6268
|
-
const articleDescription = frontmatter.description ?? "";
|
|
6269
|
-
const openRouterApiKey = resolved.config.secrets.openRouterApiKey;
|
|
6270
|
-
if (!openRouterApiKey) {
|
|
6271
|
-
throw new ReportedError(
|
|
6272
|
-
"Missing OpenRouter API key. Run `ideon settings` to configure credentials or set IDEON_OPENROUTER_API_KEY."
|
|
6273
|
-
);
|
|
6274
|
-
}
|
|
6275
|
-
const openRouter = new OpenRouterClient(openRouterApiKey);
|
|
6276
|
-
const linksResult = await enrichLinks({
|
|
6277
|
-
markdownFiles: [{ markdownPath, fileId, contentType: "article" }],
|
|
6278
|
-
articleTitle,
|
|
6279
|
-
articleDescription,
|
|
6280
|
-
openRouter,
|
|
6281
|
-
settings: resolved.config.settings,
|
|
6282
|
-
dryRun: false,
|
|
6283
|
-
onItemProgress(event) {
|
|
6284
|
-
logProgress(event, log);
|
|
6285
|
-
}
|
|
6286
|
-
});
|
|
6287
|
-
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));
|
|
6292
|
-
await writeLinksFile(markdownPath, {
|
|
6293
|
-
version: 1,
|
|
6294
|
-
links: mergedLinks
|
|
6295
|
-
});
|
|
6296
|
-
const relativeMarkdownPath = formatRelativePath2(cwd2, markdownPath);
|
|
6297
|
-
const relativeLinksPath = formatRelativePath2(cwd2, linksPath);
|
|
6298
|
-
if (mode === "fresh") {
|
|
6299
|
-
const replaced = existing ? "Replaced existing links." : "Created links sidecar.";
|
|
6300
|
-
log(`Enriched links for "${slug}".`);
|
|
6301
|
-
log(`${replaced} Saved ${generatedLinks.length} links to ${relativeLinksPath} (${relativeMarkdownPath}).`);
|
|
6302
|
-
return;
|
|
6303
|
-
}
|
|
6304
|
-
const baseCount = existing?.links.length ?? 0;
|
|
6305
|
-
const verb = existing ? "Appended and deduplicated links." : "Created links sidecar.";
|
|
6306
|
-
log(`Enriched links for "${slug}".`);
|
|
6307
|
-
log(`${verb} Base ${baseCount}, added ${appendedCount}, total ${mergedLinks.length} in ${relativeLinksPath} (${relativeMarkdownPath}).`);
|
|
6308
|
-
}
|
|
6309
|
-
function normalizeMode(rawMode) {
|
|
6310
|
-
const normalized = rawMode.trim().toLowerCase();
|
|
6311
|
-
if (normalized === "fresh" || normalized === "append") {
|
|
6312
|
-
return normalized;
|
|
6313
|
-
}
|
|
6314
|
-
throw new ReportedError(`Unsupported --mode value "${rawMode}". Use "fresh" or "append".`);
|
|
6315
|
-
}
|
|
6316
|
-
function normalizeSlug2(rawSlug) {
|
|
6317
|
-
const slug = rawSlug.trim();
|
|
6318
|
-
if (!slug) {
|
|
6319
|
-
throw new ReportedError("Slug cannot be empty. Pass the generated article slug, for example `ideon links my-article-slug`.");
|
|
6320
|
-
}
|
|
6321
|
-
if (slug.toLowerCase().endsWith(".md")) {
|
|
6322
|
-
throw new ReportedError(`Expected a slug, not a markdown filename: ${slug}. Pass the slug without .md.`);
|
|
6323
|
-
}
|
|
6324
|
-
if (slug === "." || slug === ".." || /[/\\]/.test(slug)) {
|
|
6325
|
-
throw new ReportedError(`Invalid slug "${slug}". Pass the article slug only, without any path separators.`);
|
|
6326
|
-
}
|
|
6327
|
-
return slug;
|
|
6328
|
-
}
|
|
6329
|
-
async function resolveMarkdownPathForSlug2(settings, slug, cwd2) {
|
|
6330
|
-
const outputPaths = resolveOutputPaths(settings, cwd2);
|
|
6331
|
-
const directPath = path9.join(outputPaths.markdownOutputDir, `${slug}.md`);
|
|
6332
|
-
if (await isReadableFile(directPath)) {
|
|
6333
|
-
return directPath;
|
|
6334
|
-
}
|
|
6335
|
-
const markdownFiles = await listMarkdownFilesRecursively(outputPaths.markdownOutputDir);
|
|
6336
|
-
const matches = [];
|
|
6337
|
-
for (const candidate of markdownFiles) {
|
|
6338
|
-
if (path9.basename(candidate) === `${slug}.md`) {
|
|
6339
|
-
matches.push(candidate);
|
|
6340
|
-
continue;
|
|
6341
|
-
}
|
|
6342
|
-
const frontmatter = await readFrontmatter(candidate);
|
|
6343
|
-
if (frontmatter.slug === slug) {
|
|
6344
|
-
matches.push(candidate);
|
|
6345
|
-
}
|
|
6346
|
-
}
|
|
6347
|
-
if (matches.length === 0) {
|
|
6348
|
-
throw new ReportedError(
|
|
6349
|
-
`Could not find article "${slug}". Expected a markdown file in ${outputPaths.markdownOutputDir} with frontmatter slug "${slug}".`
|
|
6350
|
-
);
|
|
6351
|
-
}
|
|
6352
|
-
return newestPath(matches);
|
|
6353
|
-
}
|
|
6354
|
-
async function newestPath(paths) {
|
|
6355
|
-
let latestPath = paths[0];
|
|
6356
|
-
let latestMtime = 0;
|
|
6357
|
-
for (const candidate of paths) {
|
|
6358
|
-
const candidateStat = await stat3(candidate);
|
|
6359
|
-
if (candidateStat.mtimeMs >= latestMtime) {
|
|
6360
|
-
latestMtime = candidateStat.mtimeMs;
|
|
6361
|
-
latestPath = candidate;
|
|
6362
|
-
}
|
|
6363
|
-
}
|
|
6364
|
-
return latestPath;
|
|
6365
|
-
}
|
|
6366
|
-
async function readFrontmatter(markdownPath) {
|
|
6367
|
-
const markdown = await readFile6(markdownPath, "utf8");
|
|
6368
|
-
return parseFrontmatter(markdown);
|
|
6369
|
-
}
|
|
6370
|
-
function parseFrontmatter(markdown) {
|
|
6371
|
-
if (!markdown.startsWith("---\n")) {
|
|
6372
|
-
return { slug: null, title: null, description: null };
|
|
6373
|
-
}
|
|
6374
|
-
const frontmatterEnd = markdown.indexOf("\n---\n", 4);
|
|
6375
|
-
if (frontmatterEnd < 0) {
|
|
6376
|
-
return { slug: null, title: null, description: null };
|
|
6377
|
-
}
|
|
6378
|
-
const block = markdown.slice(4, frontmatterEnd);
|
|
6379
|
-
return {
|
|
6380
|
-
slug: parseFrontmatterValue(block, "slug"),
|
|
6381
|
-
title: parseFrontmatterValue(block, "title"),
|
|
6382
|
-
description: parseFrontmatterValue(block, "description")
|
|
6383
|
-
};
|
|
6384
|
-
}
|
|
6385
|
-
function parseFrontmatterValue(block, key) {
|
|
6386
|
-
const pattern = new RegExp(`^${key}:\\s*(.+)$`, "m");
|
|
6387
|
-
const match = block.match(pattern);
|
|
6388
|
-
if (!match || !match[1]) {
|
|
6389
|
-
return null;
|
|
6390
|
-
}
|
|
6391
|
-
const rawValue = match[1].trim();
|
|
6392
|
-
if (!rawValue) {
|
|
6393
|
-
return null;
|
|
6394
|
-
}
|
|
6395
|
-
if (rawValue.startsWith('"') && rawValue.endsWith('"') || rawValue.startsWith("'") && rawValue.endsWith("'")) {
|
|
6396
|
-
return rawValue.slice(1, -1);
|
|
6397
|
-
}
|
|
6398
|
-
return rawValue;
|
|
6399
|
-
}
|
|
6400
|
-
function toTitleFromSlug(slug) {
|
|
6401
|
-
return slug.split("-").filter((part) => part.length > 0).map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ");
|
|
6402
|
-
}
|
|
6403
|
-
async function isReadableFile(filePath) {
|
|
6404
|
-
try {
|
|
6405
|
-
const fileStat = await stat3(filePath);
|
|
6406
|
-
return fileStat.isFile();
|
|
6407
|
-
} catch {
|
|
6408
|
-
return false;
|
|
6409
|
-
}
|
|
6410
|
-
}
|
|
6411
|
-
async function readExistingLinks(linksPath) {
|
|
6412
|
-
try {
|
|
6413
|
-
const raw = await readFile6(linksPath, "utf8");
|
|
6414
|
-
const parsed = JSON.parse(raw);
|
|
6415
|
-
const links = Array.isArray(parsed.links) ? parsed.links.filter((entry) => isValidLinkEntry(entry)).map((entry) => ({
|
|
6416
|
-
expression: entry.expression.trim(),
|
|
6417
|
-
url: entry.url.trim(),
|
|
6418
|
-
title: typeof entry.title === "string" ? entry.title : null
|
|
6419
|
-
})) : null;
|
|
6420
|
-
if (!links) {
|
|
6421
|
-
throw new ReportedError(`Invalid links sidecar format at ${linksPath}. Expected { version, links[] }.`);
|
|
6422
|
-
}
|
|
6423
|
-
return {
|
|
6424
|
-
version: typeof parsed.version === "number" ? parsed.version : 1,
|
|
6425
|
-
links
|
|
6426
|
-
};
|
|
6427
|
-
} catch (error) {
|
|
6428
|
-
if (readErrorCode2(error) === "ENOENT") {
|
|
6429
|
-
return null;
|
|
6430
|
-
}
|
|
6431
|
-
if (error instanceof ReportedError) {
|
|
6432
|
-
throw error;
|
|
6433
|
-
}
|
|
6434
|
-
const message = error instanceof Error ? error.message : "Unknown links sidecar read error.";
|
|
6435
|
-
throw new ReportedError(`Failed to read existing links from ${linksPath}: ${message}`);
|
|
6436
|
-
}
|
|
6437
|
-
}
|
|
6438
|
-
function mergeLinks(existingLinks, generatedLinks) {
|
|
6439
|
-
const merged = [];
|
|
6440
|
-
const seen = /* @__PURE__ */ new Set();
|
|
6441
|
-
for (const entry of [...existingLinks, ...generatedLinks]) {
|
|
6442
|
-
const key = `${entry.expression.trim().toLowerCase()}::${entry.url.trim().toLowerCase()}`;
|
|
6443
|
-
if (seen.has(key)) {
|
|
6444
|
-
continue;
|
|
6445
|
-
}
|
|
6446
|
-
seen.add(key);
|
|
6447
|
-
merged.push(entry);
|
|
6448
|
-
}
|
|
6449
|
-
return merged;
|
|
6450
|
-
}
|
|
6451
|
-
function isValidLinkEntry(value2) {
|
|
6452
|
-
if (typeof value2 !== "object" || value2 === null) {
|
|
6453
|
-
return false;
|
|
6454
|
-
}
|
|
6455
|
-
const record = value2;
|
|
6456
|
-
return typeof record.expression === "string" && typeof record.url === "string" && (typeof record.title === "string" || record.title === null || record.title === void 0);
|
|
6457
|
-
}
|
|
6458
|
-
function readErrorCode2(error) {
|
|
6459
|
-
if (typeof error !== "object" || error === null || !("code" in error)) {
|
|
6460
|
-
return null;
|
|
6461
|
-
}
|
|
6462
|
-
const code = error.code;
|
|
6463
|
-
return typeof code === "string" ? code : null;
|
|
6464
|
-
}
|
|
6465
|
-
function formatRelativePath2(cwd2, targetPath) {
|
|
6466
|
-
const relativePath = path9.relative(cwd2, targetPath);
|
|
6467
|
-
return relativePath.length > 0 ? relativePath : targetPath;
|
|
6468
|
-
}
|
|
6469
|
-
function logProgress(event, log) {
|
|
6470
|
-
if (event.phase === "resolving-expression" || event.phase === "selecting-expressions") {
|
|
6471
|
-
return;
|
|
6472
|
-
}
|
|
6473
|
-
log(event.detail);
|
|
6474
|
-
}
|
|
6475
|
-
|
|
6476
6709
|
// src/cli/commands/settings.tsx
|
|
6477
6710
|
import { render } from "ink";
|
|
6478
6711
|
|
|
@@ -6852,7 +7085,7 @@ import { spawn } from "child_process";
|
|
|
6852
7085
|
import { readdir, stat as stat4, readFile as readFile7 } from "fs/promises";
|
|
6853
7086
|
import path10 from "path";
|
|
6854
7087
|
var DEFAULT_PORT = 4173;
|
|
6855
|
-
var CONTENT_TYPE_ORDER = ["article", "blog-post", "x-thread", "x-post", "linkedin-post", "reddit-post", "newsletter"
|
|
7088
|
+
var CONTENT_TYPE_ORDER = ["article", "blog-post", "x-thread", "x-post", "linkedin-post", "reddit-post", "newsletter"];
|
|
6856
7089
|
var FILE_PREFIX_TO_CONTENT_TYPE = {
|
|
6857
7090
|
article: "article",
|
|
6858
7091
|
blog: "blog-post",
|
|
@@ -6861,8 +7094,7 @@ var FILE_PREFIX_TO_CONTENT_TYPE = {
|
|
|
6861
7094
|
x: "x-post",
|
|
6862
7095
|
reddit: "reddit-post",
|
|
6863
7096
|
linkedin: "linkedin-post",
|
|
6864
|
-
newsletter: "newsletter"
|
|
6865
|
-
landing: "landing-page-copy"
|
|
7097
|
+
newsletter: "newsletter"
|
|
6866
7098
|
};
|
|
6867
7099
|
var CONTENT_TYPE_LABELS = {
|
|
6868
7100
|
article: "Article",
|
|
@@ -6871,8 +7103,7 @@ var CONTENT_TYPE_LABELS = {
|
|
|
6871
7103
|
"x-post": "X Post",
|
|
6872
7104
|
"reddit-post": "Reddit Post",
|
|
6873
7105
|
"linkedin-post": "LinkedIn Post",
|
|
6874
|
-
newsletter: "Newsletter"
|
|
6875
|
-
"landing-page-copy": "Landing Page Copy"
|
|
7106
|
+
newsletter: "Newsletter"
|
|
6876
7107
|
};
|
|
6877
7108
|
function parsePort(portOption) {
|
|
6878
7109
|
if (!portOption) {
|
|
@@ -7630,10 +7861,6 @@ function renderShell({
|
|
|
7630
7861
|
--newsletter-bg: #fffdf4;
|
|
7631
7862
|
--newsletter-header-bg: #fff5cc;
|
|
7632
7863
|
--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
7864
|
color-scheme: light;
|
|
7638
7865
|
}
|
|
7639
7866
|
|
|
@@ -7673,10 +7900,6 @@ function renderShell({
|
|
|
7673
7900
|
--newsletter-bg: #2e291b;
|
|
7674
7901
|
--newsletter-header-bg: #3b331e;
|
|
7675
7902
|
--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
7903
|
color-scheme: dark;
|
|
7681
7904
|
}
|
|
7682
7905
|
}
|
|
@@ -7716,10 +7939,6 @@ function renderShell({
|
|
|
7716
7939
|
--newsletter-bg: #fffdf4;
|
|
7717
7940
|
--newsletter-header-bg: #fff5cc;
|
|
7718
7941
|
--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
7942
|
color-scheme: light;
|
|
7724
7943
|
}
|
|
7725
7944
|
|
|
@@ -7758,10 +7977,6 @@ function renderShell({
|
|
|
7758
7977
|
--newsletter-bg: #2e291b;
|
|
7759
7978
|
--newsletter-header-bg: #3b331e;
|
|
7760
7979
|
--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
7980
|
color-scheme: dark;
|
|
7766
7981
|
}
|
|
7767
7982
|
|
|
@@ -8281,21 +8496,6 @@ function renderShell({
|
|
|
8281
8496
|
background: var(--newsletter-header-bg);
|
|
8282
8497
|
}
|
|
8283
8498
|
|
|
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
8499
|
.channel-article,
|
|
8300
8500
|
.channel-blog-post {
|
|
8301
8501
|
background: var(--paper);
|
|
@@ -8481,7 +8681,7 @@ function renderShell({
|
|
|
8481
8681
|
const articleElement = document.getElementById('article');
|
|
8482
8682
|
const articleListElement = document.getElementById('articleList');
|
|
8483
8683
|
const themeToggleButton = document.getElementById('themeToggle');
|
|
8484
|
-
const typeOrder = ['article', 'blog-post', 'x-thread', 'x-post', 'linkedin-post', 'reddit-post', 'newsletter'
|
|
8684
|
+
const typeOrder = ['article', 'blog-post', 'x-thread', 'x-post', 'linkedin-post', 'reddit-post', 'newsletter'];
|
|
8485
8685
|
const stageOrder = ['shared-brief', 'planning', 'sections', 'image-prompts', 'images', 'output', 'links'];
|
|
8486
8686
|
|
|
8487
8687
|
let currentGeneration = null;
|
|
@@ -9582,7 +9782,7 @@ function formatPipelineStageCost(stage) {
|
|
|
9582
9782
|
}
|
|
9583
9783
|
return stage.costSource === "estimated" ? `~${formatted}` : formatted;
|
|
9584
9784
|
}
|
|
9585
|
-
async function renderPlainPipeline(input, dryRun, enrichLinks2, runMode) {
|
|
9785
|
+
async function renderPlainPipeline(input, dryRun, enrichLinks2, runMode, links, unlinks, maxLinks) {
|
|
9586
9786
|
let previousStages = /* @__PURE__ */ new Map();
|
|
9587
9787
|
let previousItemStatuses = /* @__PURE__ */ new Map();
|
|
9588
9788
|
const notificationsEnabled = input.config.settings.notifications.enabled;
|
|
@@ -9596,6 +9796,9 @@ async function renderPlainPipeline(input, dryRun, enrichLinks2, runMode) {
|
|
|
9596
9796
|
dryRun,
|
|
9597
9797
|
enrichLinks: enrichLinks2,
|
|
9598
9798
|
runMode,
|
|
9799
|
+
customLinks: links,
|
|
9800
|
+
unlinks,
|
|
9801
|
+
maxLinks,
|
|
9599
9802
|
onUpdate(stages) {
|
|
9600
9803
|
for (const stage of stages) {
|
|
9601
9804
|
const previous = previousStages.get(stage.id);
|
|
@@ -9954,6 +10157,9 @@ function WriteApp({
|
|
|
9954
10157
|
dryRun,
|
|
9955
10158
|
enrichLinks: enrichLinks2,
|
|
9956
10159
|
runMode,
|
|
10160
|
+
links,
|
|
10161
|
+
unlinks,
|
|
10162
|
+
maxLinks,
|
|
9957
10163
|
onError
|
|
9958
10164
|
}) {
|
|
9959
10165
|
const { exit } = useApp3();
|
|
@@ -9977,6 +10183,9 @@ function WriteApp({
|
|
|
9977
10183
|
dryRun,
|
|
9978
10184
|
enrichLinks: enrichLinks2,
|
|
9979
10185
|
runMode,
|
|
10186
|
+
customLinks: links,
|
|
10187
|
+
unlinks,
|
|
10188
|
+
maxLinks,
|
|
9980
10189
|
onUpdate(nextStages) {
|
|
9981
10190
|
if (mounted) {
|
|
9982
10191
|
setStages(nextStages);
|
|
@@ -10009,7 +10218,7 @@ function WriteApp({
|
|
|
10009
10218
|
return () => {
|
|
10010
10219
|
mounted = false;
|
|
10011
10220
|
};
|
|
10012
|
-
}, [dryRun, enrichLinks2, input, onError, runMode]);
|
|
10221
|
+
}, [dryRun, enrichLinks2, input, links, unlinks, maxLinks, onError, runMode]);
|
|
10013
10222
|
useEffect2(() => {
|
|
10014
10223
|
if (!result && !errorMessage) {
|
|
10015
10224
|
return;
|
|
@@ -10025,7 +10234,7 @@ function WriteApp({
|
|
|
10025
10234
|
}
|
|
10026
10235
|
async function runWriteCommand(options) {
|
|
10027
10236
|
const input = await resolveInputWithInteractiveIdeaFallback(options);
|
|
10028
|
-
await runWritePipeline(input, options.dryRun, options.enrichLinks, "fresh", options.noInteractive);
|
|
10237
|
+
await runWritePipeline(input, options.dryRun, options.enrichLinks, "fresh", options.noInteractive, options.links, options.unlinks, options.maxLinks);
|
|
10029
10238
|
}
|
|
10030
10239
|
async function runWriteResumeCommand(options = {}) {
|
|
10031
10240
|
const session = await loadWriteSession();
|
|
@@ -10047,9 +10256,9 @@ async function runWriteResumeCommand(options = {}) {
|
|
|
10047
10256
|
secrets: resolved.config.secrets
|
|
10048
10257
|
}
|
|
10049
10258
|
};
|
|
10050
|
-
await runWritePipeline(input, session.dryRun, options.enrichLinks ?? false, "resume", options.noInteractive ?? false);
|
|
10259
|
+
await runWritePipeline(input, session.dryRun, options.enrichLinks ?? false, "resume", options.noInteractive ?? false, options.links, options.unlinks, options.maxLinks);
|
|
10051
10260
|
}
|
|
10052
|
-
async function runWritePipeline(input, dryRun, enrichLinks2, runMode, noInteractive) {
|
|
10261
|
+
async function runWritePipeline(input, dryRun, enrichLinks2, runMode, noInteractive, links, unlinks, maxLinks) {
|
|
10053
10262
|
let interruptHandled = false;
|
|
10054
10263
|
const handleSignal = (signal) => {
|
|
10055
10264
|
if (interruptHandled) {
|
|
@@ -10083,7 +10292,7 @@ async function runWritePipeline(input, dryRun, enrichLinks2, runMode, noInteract
|
|
|
10083
10292
|
process.on("SIGTERM", onSigterm);
|
|
10084
10293
|
try {
|
|
10085
10294
|
if (noInteractive || !process.stdout.isTTY) {
|
|
10086
|
-
await renderPlainPipeline(input, dryRun, enrichLinks2, runMode);
|
|
10295
|
+
await renderPlainPipeline(input, dryRun, enrichLinks2, runMode, links, unlinks, maxLinks);
|
|
10087
10296
|
return;
|
|
10088
10297
|
}
|
|
10089
10298
|
let commandError = null;
|
|
@@ -10095,6 +10304,9 @@ async function runWritePipeline(input, dryRun, enrichLinks2, runMode, noInteract
|
|
|
10095
10304
|
dryRun,
|
|
10096
10305
|
enrichLinks: enrichLinks2,
|
|
10097
10306
|
runMode,
|
|
10307
|
+
links,
|
|
10308
|
+
unlinks,
|
|
10309
|
+
maxLinks,
|
|
10098
10310
|
onError: (error) => {
|
|
10099
10311
|
commandError = error;
|
|
10100
10312
|
}
|
|
@@ -10298,10 +10510,13 @@ async function runCli(argv) {
|
|
|
10298
10510
|
force: options.force
|
|
10299
10511
|
});
|
|
10300
10512
|
});
|
|
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) => {
|
|
10513
|
+
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
10514
|
await runLinksCommand({
|
|
10303
10515
|
slug,
|
|
10304
|
-
mode: options.mode
|
|
10516
|
+
mode: options.mode,
|
|
10517
|
+
links: options.link,
|
|
10518
|
+
unlinks: options.unlink,
|
|
10519
|
+
maxLinks: options.maxLinks
|
|
10305
10520
|
});
|
|
10306
10521
|
});
|
|
10307
10522
|
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 +10527,7 @@ async function runCli(argv) {
|
|
|
10312
10527
|
watch: options.watch
|
|
10313
10528
|
});
|
|
10314
10529
|
});
|
|
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) => {
|
|
10530
|
+
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
10531
|
await runWriteCommand({
|
|
10317
10532
|
idea: options.idea ?? ideaArg,
|
|
10318
10533
|
audience: options.audience,
|
|
@@ -10324,13 +10539,19 @@ async function runCli(argv) {
|
|
|
10324
10539
|
length: options.length,
|
|
10325
10540
|
noInteractive: !options.interactive,
|
|
10326
10541
|
dryRun: options.dryRun,
|
|
10327
|
-
enrichLinks: options.enrichLinks
|
|
10542
|
+
enrichLinks: options.enrichLinks,
|
|
10543
|
+
links: options.link,
|
|
10544
|
+
unlinks: options.unlink,
|
|
10545
|
+
maxLinks: options.maxLinks
|
|
10328
10546
|
});
|
|
10329
10547
|
});
|
|
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) => {
|
|
10548
|
+
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
10549
|
await runWriteResumeCommand({
|
|
10332
10550
|
noInteractive: options.noInteractive,
|
|
10333
|
-
enrichLinks: options.enrichLinks
|
|
10551
|
+
enrichLinks: options.enrichLinks,
|
|
10552
|
+
links: options.link,
|
|
10553
|
+
unlinks: options.unlink,
|
|
10554
|
+
maxLinks: options.maxLinks
|
|
10334
10555
|
});
|
|
10335
10556
|
});
|
|
10336
10557
|
await program.parseAsync(argv);
|