@telepat/ideon 0.1.16 → 0.1.18
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 +13 -13
- package/README.zh-CN.md +13 -13
- package/dist/ideon.js +873 -595
- package/package.json +1 -1
package/dist/ideon.js
CHANGED
|
@@ -118,7 +118,7 @@ var notificationsSettingsSchema = z.object({
|
|
|
118
118
|
enabled: z.boolean().default(false)
|
|
119
119
|
});
|
|
120
120
|
var appSettingsSchema = z.object({
|
|
121
|
-
model: z.string().default("
|
|
121
|
+
model: z.string().default("deepseek/deepseek-v4-pro"),
|
|
122
122
|
modelSettings: modelSettingsSchema.default(modelSettingsSchema.parse({})),
|
|
123
123
|
modelRequestTimeoutMs: z.number().int().positive().default(9e4),
|
|
124
124
|
t2i: baseT2ISettingsSchema.default(baseT2ISettingsSchema.parse({})),
|
|
@@ -975,7 +975,8 @@ var writeToolInputSchema = {
|
|
|
975
975
|
enrichLinks: z3.boolean().optional(),
|
|
976
976
|
link: z3.array(z3.string()).optional(),
|
|
977
977
|
unlink: z3.array(z3.string()).optional(),
|
|
978
|
-
maxLinks: z3.coerce.number().int().positive().optional()
|
|
978
|
+
maxLinks: z3.coerce.number().int().positive().optional(),
|
|
979
|
+
maxImages: z3.coerce.number().int().min(1).optional()
|
|
979
980
|
};
|
|
980
981
|
var writeToolInputZodSchema = z3.object(writeToolInputSchema);
|
|
981
982
|
var writeResumeToolInputSchema = {
|
|
@@ -983,7 +984,8 @@ var writeResumeToolInputSchema = {
|
|
|
983
984
|
enrichLinks: z3.boolean().optional(),
|
|
984
985
|
link: z3.array(z3.string()).optional(),
|
|
985
986
|
unlink: z3.array(z3.string()).optional(),
|
|
986
|
-
maxLinks: z3.coerce.number().int().positive().optional()
|
|
987
|
+
maxLinks: z3.coerce.number().int().positive().optional(),
|
|
988
|
+
maxImages: z3.coerce.number().int().min(1).optional()
|
|
987
989
|
};
|
|
988
990
|
var writeResumeToolInputZodSchema = z3.object(writeResumeToolInputSchema);
|
|
989
991
|
var deleteToolInputSchema = {
|
|
@@ -1007,6 +1009,13 @@ var linksToolInputSchema = {
|
|
|
1007
1009
|
maxLinks: z3.coerce.number().int().positive().optional()
|
|
1008
1010
|
};
|
|
1009
1011
|
var linksToolInputZodSchema = z3.object(linksToolInputSchema);
|
|
1012
|
+
var exportToolInputSchema = {
|
|
1013
|
+
generationId: z3.string().min(1),
|
|
1014
|
+
destinationPath: z3.string().min(1),
|
|
1015
|
+
index: z3.coerce.number().int().positive().optional(),
|
|
1016
|
+
overwrite: z3.boolean().optional()
|
|
1017
|
+
};
|
|
1018
|
+
var exportToolInputZodSchema = z3.object(exportToolInputSchema);
|
|
1010
1019
|
var configListToolInputSchema = {};
|
|
1011
1020
|
var configListToolInputZodSchema = z3.object(configListToolInputSchema);
|
|
1012
1021
|
var configUnsetToolInputSchema = {
|
|
@@ -1040,6 +1049,11 @@ var ideonToolContracts = [
|
|
|
1040
1049
|
mode: ["fresh", "append"]
|
|
1041
1050
|
}
|
|
1042
1051
|
},
|
|
1052
|
+
{
|
|
1053
|
+
name: "ideon_export",
|
|
1054
|
+
required: ["generationId", "destinationPath"],
|
|
1055
|
+
enums: {}
|
|
1056
|
+
},
|
|
1043
1057
|
{
|
|
1044
1058
|
name: "ideon_config_set",
|
|
1045
1059
|
required: ["key", "value"],
|
|
@@ -1348,7 +1362,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
1348
1362
|
// package.json
|
|
1349
1363
|
var package_default = {
|
|
1350
1364
|
name: "@telepat/ideon",
|
|
1351
|
-
version: "0.1.
|
|
1365
|
+
version: "0.1.18",
|
|
1352
1366
|
description: "CLI for generating rich articles and images from ideas.",
|
|
1353
1367
|
type: "module",
|
|
1354
1368
|
repository: {
|
|
@@ -2269,9 +2283,8 @@ function buildArticlePlanJsonSchema(targetLengthWords) {
|
|
|
2269
2283
|
items: {
|
|
2270
2284
|
type: "object",
|
|
2271
2285
|
additionalProperties: false,
|
|
2272
|
-
required: ["
|
|
2286
|
+
required: ["description"],
|
|
2273
2287
|
properties: {
|
|
2274
|
-
anchorAfterSection: { type: "integer", minimum: 1, maximum: 10 },
|
|
2275
2288
|
description: { type: "string" }
|
|
2276
2289
|
}
|
|
2277
2290
|
}
|
|
@@ -2310,7 +2323,7 @@ function buildArticlePlanMessages(idea, options) {
|
|
|
2310
2323
|
"- Sections are article-only structure and must not be treated as requirements for non-article channels.",
|
|
2311
2324
|
"- Include a cover image description and 2 to 3 inline image descriptions.",
|
|
2312
2325
|
"- Image descriptions must be concrete and contextual, not generic stock-photo phrasing.",
|
|
2313
|
-
"-
|
|
2326
|
+
"- Do not include section placement for inline images \u2014 placement is determined automatically after writing.",
|
|
2314
2327
|
"",
|
|
2315
2328
|
"Shared content brief context:",
|
|
2316
2329
|
`- description: ${options.contentBrief.description}`,
|
|
@@ -2329,7 +2342,7 @@ function buildArticlePlanMessages(idea, options) {
|
|
|
2329
2342
|
"- outroBrief: string",
|
|
2330
2343
|
`- sections: array of ${sectionCounts.label} objects, each with title and description strings`,
|
|
2331
2344
|
"- coverImageDescription: string",
|
|
2332
|
-
"- inlineImages: array of 2 to 3 objects, each with
|
|
2345
|
+
"- inlineImages: array of 2 to 3 objects, each with a description string only",
|
|
2333
2346
|
"",
|
|
2334
2347
|
"Do not omit any required fields. Return strict JSON only."
|
|
2335
2348
|
].join("\n")
|
|
@@ -2344,7 +2357,6 @@ var articleSectionPlanSchema = z5.object({
|
|
|
2344
2357
|
description: z5.string().min(1)
|
|
2345
2358
|
});
|
|
2346
2359
|
var inlineImagePlanSchema = z5.object({
|
|
2347
|
-
anchorAfterSection: z5.number().int().min(1).max(10),
|
|
2348
2360
|
description: z5.string().min(1)
|
|
2349
2361
|
});
|
|
2350
2362
|
var articlePlanSchema = z5.object({
|
|
@@ -2400,7 +2412,7 @@ async function planArticle({
|
|
|
2400
2412
|
...basePlan,
|
|
2401
2413
|
slug: uniqueSlug,
|
|
2402
2414
|
keywords: basePlan.keywords.slice(0, 8),
|
|
2403
|
-
inlineImages: basePlan.inlineImages.
|
|
2415
|
+
inlineImages: basePlan.inlineImages.slice(0, 3)
|
|
2404
2416
|
};
|
|
2405
2417
|
}
|
|
2406
2418
|
function buildDryRunPlan(idea, contentBrief) {
|
|
@@ -2438,11 +2450,9 @@ function buildDryRunPlan(idea, contentBrief) {
|
|
|
2438
2450
|
coverImageDescription: "A refined editorial workspace with notebooks, sketches, and glowing structured outlines, cinematic but minimal.",
|
|
2439
2451
|
inlineImages: [
|
|
2440
2452
|
{
|
|
2441
|
-
anchorAfterSection: 1,
|
|
2442
2453
|
description: "A rough idea evolving into a structured article outline on a desk full of notes."
|
|
2443
2454
|
},
|
|
2444
2455
|
{
|
|
2445
|
-
anchorAfterSection: 3,
|
|
2446
2456
|
description: "A collaborative editorial scene where human judgment and AI suggestions coexist."
|
|
2447
2457
|
}
|
|
2448
2458
|
]
|
|
@@ -2937,7 +2947,21 @@ var imagePromptSchema = {
|
|
|
2937
2947
|
prompt: { type: "string" }
|
|
2938
2948
|
}
|
|
2939
2949
|
};
|
|
2940
|
-
function buildImagePromptMessages(plan, image) {
|
|
2950
|
+
function buildImagePromptMessages(plan, image, section) {
|
|
2951
|
+
const userLines = [
|
|
2952
|
+
`Article title: ${plan.title}`,
|
|
2953
|
+
`Article subtitle: ${plan.subtitle}`,
|
|
2954
|
+
`Article description: ${plan.description}`,
|
|
2955
|
+
`Image kind: ${image.kind}`,
|
|
2956
|
+
`Image description: ${image.description}`
|
|
2957
|
+
];
|
|
2958
|
+
if (section) {
|
|
2959
|
+
userLines.push(
|
|
2960
|
+
`Section title: ${section.title}`,
|
|
2961
|
+
`Section excerpt: ${section.body.slice(0, 500)}`
|
|
2962
|
+
);
|
|
2963
|
+
}
|
|
2964
|
+
userLines.push("Write one strong prompt for a clean editorial illustration. Avoid text overlays or watermarks.");
|
|
2941
2965
|
return [
|
|
2942
2966
|
{
|
|
2943
2967
|
role: "system",
|
|
@@ -2945,14 +2969,7 @@ function buildImagePromptMessages(plan, image) {
|
|
|
2945
2969
|
},
|
|
2946
2970
|
{
|
|
2947
2971
|
role: "user",
|
|
2948
|
-
content:
|
|
2949
|
-
`Article title: ${plan.title}`,
|
|
2950
|
-
`Article subtitle: ${plan.subtitle}`,
|
|
2951
|
-
`Article description: ${plan.description}`,
|
|
2952
|
-
`Image kind: ${image.kind}`,
|
|
2953
|
-
`Image description: ${image.description}`,
|
|
2954
|
-
"Write one strong prompt for a clean editorial illustration. Avoid text overlays or watermarks."
|
|
2955
|
-
].join("\n")
|
|
2972
|
+
content: userLines.join("\n")
|
|
2956
2973
|
}
|
|
2957
2974
|
];
|
|
2958
2975
|
}
|
|
@@ -3422,11 +3439,12 @@ function clampNumber(value2, minimum, maximum, integer, nullable) {
|
|
|
3422
3439
|
// src/pipeline/analytics.ts
|
|
3423
3440
|
var LLM_USD_PER_1K_TOKENS = {
|
|
3424
3441
|
// AUTO-GENERATED:OPENROUTER_PRICING_START
|
|
3425
|
-
// Last refreshed: 2026-
|
|
3442
|
+
// Last refreshed: 2026-05-05
|
|
3426
3443
|
// Source: https://openrouter.ai/api/v1/models (per-token USD converted to per-1k-token USD)
|
|
3427
3444
|
"anthropic/claude-3.5-sonnet": { input: 6e-3, output: 0.03 },
|
|
3428
3445
|
"deepseek/deepseek-chat": { input: 32e-5, output: 89e-5 },
|
|
3429
|
-
"
|
|
3446
|
+
"deepseek/deepseek-v4-pro": { input: 435e-6, output: 87e-5 },
|
|
3447
|
+
"moonshotai/kimi-k2.5": { input: 44e-5, output: 2e-3 },
|
|
3430
3448
|
"openai/gpt-4o-mini": { input: 15e-5, output: 6e-4 }
|
|
3431
3449
|
// AUTO-GENERATED:OPENROUTER_PRICING_END
|
|
3432
3450
|
};
|
|
@@ -3514,8 +3532,52 @@ function sumKnownCosts(values) {
|
|
|
3514
3532
|
|
|
3515
3533
|
// src/images/renderImages.ts
|
|
3516
3534
|
var MIN_IMAGE_BYTES = 1024;
|
|
3535
|
+
function selectImageSlots(plan, sections, options) {
|
|
3536
|
+
const sectionCount = sections.length;
|
|
3537
|
+
let defaultInlineCount;
|
|
3538
|
+
if (sectionCount <= 3) {
|
|
3539
|
+
defaultInlineCount = 0;
|
|
3540
|
+
} else if (sectionCount <= 6) {
|
|
3541
|
+
defaultInlineCount = 1;
|
|
3542
|
+
} else {
|
|
3543
|
+
defaultInlineCount = 2;
|
|
3544
|
+
}
|
|
3545
|
+
const availableInlineCount = Math.min(defaultInlineCount, plan.inlineImages.length, sectionCount);
|
|
3546
|
+
let inlineCount;
|
|
3547
|
+
const maxImages = options?.maxImages;
|
|
3548
|
+
if (maxImages !== void 0 && maxImages >= 1) {
|
|
3549
|
+
inlineCount = Math.min(availableInlineCount, Math.max(0, maxImages - 1));
|
|
3550
|
+
} else {
|
|
3551
|
+
inlineCount = availableInlineCount;
|
|
3552
|
+
}
|
|
3553
|
+
const slots = [
|
|
3554
|
+
{
|
|
3555
|
+
id: "cover",
|
|
3556
|
+
kind: "cover",
|
|
3557
|
+
prompt: "",
|
|
3558
|
+
description: plan.coverImageDescription,
|
|
3559
|
+
anchorAfterSection: null
|
|
3560
|
+
}
|
|
3561
|
+
];
|
|
3562
|
+
for (let i = 0; i < inlineCount; i++) {
|
|
3563
|
+
const anchorAfterSection = Math.max(
|
|
3564
|
+
1,
|
|
3565
|
+
Math.min(sectionCount, Math.round((i + 1) / (inlineCount + 1) * sectionCount))
|
|
3566
|
+
);
|
|
3567
|
+
slots.push({
|
|
3568
|
+
id: `inline-${i + 1}`,
|
|
3569
|
+
kind: "inline",
|
|
3570
|
+
prompt: "",
|
|
3571
|
+
description: plan.inlineImages[i]?.description ?? "",
|
|
3572
|
+
anchorAfterSection
|
|
3573
|
+
});
|
|
3574
|
+
}
|
|
3575
|
+
return slots;
|
|
3576
|
+
}
|
|
3517
3577
|
async function expandImagePrompts({
|
|
3518
|
-
|
|
3578
|
+
slots,
|
|
3579
|
+
planContext,
|
|
3580
|
+
sections,
|
|
3519
3581
|
settings,
|
|
3520
3582
|
openRouter,
|
|
3521
3583
|
dryRun,
|
|
@@ -3523,11 +3585,11 @@ async function expandImagePrompts({
|
|
|
3523
3585
|
onPromptComplete,
|
|
3524
3586
|
onInteraction
|
|
3525
3587
|
}) {
|
|
3526
|
-
const imageSlots = buildImageSlots(plan);
|
|
3527
3588
|
const prompts = [];
|
|
3528
|
-
for (let index = 0; index <
|
|
3529
|
-
const image =
|
|
3530
|
-
onProgress?.(`Expanding prompt ${index + 1}/${
|
|
3589
|
+
for (let index = 0; index < slots.length; index += 1) {
|
|
3590
|
+
const image = slots[index];
|
|
3591
|
+
onProgress?.(`Expanding prompt ${index + 1}/${slots.length}: ${image.kind === "cover" ? "cover image" : image.description}`);
|
|
3592
|
+
const sectionForImage = image.kind === "inline" && image.anchorAfterSection != null && sections ? sections[image.anchorAfterSection - 1] : void 0;
|
|
3531
3593
|
if (dryRun || !openRouter) {
|
|
3532
3594
|
const dryRunStartMs = Date.now();
|
|
3533
3595
|
prompts.push({
|
|
@@ -3554,7 +3616,7 @@ async function expandImagePrompts({
|
|
|
3554
3616
|
const response = await openRouter.requestStructured({
|
|
3555
3617
|
schemaName: "image_prompt",
|
|
3556
3618
|
schema: imagePromptSchema,
|
|
3557
|
-
messages: buildImagePromptMessages(
|
|
3619
|
+
messages: buildImagePromptMessages(planContext, image, sectionForImage),
|
|
3558
3620
|
settings,
|
|
3559
3621
|
interactionContext: {
|
|
3560
3622
|
stageId: "image-prompts",
|
|
@@ -3770,24 +3832,6 @@ function mergeLlmMetrics(left, right) {
|
|
|
3770
3832
|
}
|
|
3771
3833
|
};
|
|
3772
3834
|
}
|
|
3773
|
-
function buildImageSlots(plan) {
|
|
3774
|
-
return [
|
|
3775
|
-
{
|
|
3776
|
-
id: "cover",
|
|
3777
|
-
kind: "cover",
|
|
3778
|
-
prompt: "",
|
|
3779
|
-
description: plan.coverImageDescription,
|
|
3780
|
-
anchorAfterSection: null
|
|
3781
|
-
},
|
|
3782
|
-
...plan.inlineImages.map((image, index) => ({
|
|
3783
|
-
id: `inline-${index + 1}`,
|
|
3784
|
-
kind: "inline",
|
|
3785
|
-
prompt: "",
|
|
3786
|
-
description: image.description,
|
|
3787
|
-
anchorAfterSection: image.anchorAfterSection
|
|
3788
|
-
}))
|
|
3789
|
-
];
|
|
3790
|
-
}
|
|
3791
3835
|
function createReplicateInput(settings, prompt, kind) {
|
|
3792
3836
|
const model = getT2IModel(settings.t2i.modelId);
|
|
3793
3837
|
const overrides = sanitizeT2IOverrides(settings.t2i.modelId, settings.t2i.inputOverrides);
|
|
@@ -4945,7 +4989,9 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4945
4989
|
options.onUpdate?.(cloneStages(stages));
|
|
4946
4990
|
} else {
|
|
4947
4991
|
imagePrompts = await expandImagePrompts({
|
|
4948
|
-
plan,
|
|
4992
|
+
slots: selectImageSlots(plan, text.sections, { maxImages: options.maxImages }),
|
|
4993
|
+
planContext: plan,
|
|
4994
|
+
sections: text.sections,
|
|
4949
4995
|
settings: input.config.settings,
|
|
4950
4996
|
openRouter,
|
|
4951
4997
|
dryRun,
|
|
@@ -6310,106 +6356,599 @@ function resolveCustomLinks(existing, addRaw, removeExpressions) {
|
|
|
6310
6356
|
return Array.from(result.values());
|
|
6311
6357
|
}
|
|
6312
6358
|
|
|
6313
|
-
// src/cli/commands/
|
|
6314
|
-
|
|
6315
|
-
|
|
6316
|
-
|
|
6317
|
-
|
|
6359
|
+
// src/cli/commands/export.ts
|
|
6360
|
+
import { copyFile, mkdir as mkdir6, readFile as readFile8, stat as stat5, writeFile as writeFile6 } from "fs/promises";
|
|
6361
|
+
import path11 from "path";
|
|
6362
|
+
|
|
6363
|
+
// src/output/enrichMarkdownWithLinks.ts
|
|
6364
|
+
function enrichMarkdownWithLinks(markdown, links) {
|
|
6365
|
+
if (links.length === 0) {
|
|
6366
|
+
return markdown;
|
|
6318
6367
|
}
|
|
6319
|
-
const [
|
|
6320
|
-
|
|
6321
|
-
|
|
6368
|
+
const sorted = [...links].sort((left, right) => right.expression.length - left.expression.length);
|
|
6369
|
+
let updated = markdown;
|
|
6370
|
+
for (const link of sorted) {
|
|
6371
|
+
const escapedExpression = escapeRegExp(link.expression);
|
|
6372
|
+
const leadBoundary = /^\w/.test(link.expression) ? "\\b" : "";
|
|
6373
|
+
const trailBoundary = /\w$/.test(link.expression) ? "\\b" : "";
|
|
6374
|
+
const expressionRegex = new RegExp(`${leadBoundary}${escapedExpression}${trailBoundary}`, "g");
|
|
6375
|
+
let match;
|
|
6376
|
+
while ((match = expressionRegex.exec(updated)) !== null) {
|
|
6377
|
+
const start = match.index;
|
|
6378
|
+
const end = start + match[0].length;
|
|
6379
|
+
if (isInProtectedSpan(updated, start, end)) {
|
|
6380
|
+
continue;
|
|
6381
|
+
}
|
|
6382
|
+
updated = `${updated.slice(0, start)}[${match[0]}](${link.url})${updated.slice(end)}`;
|
|
6383
|
+
break;
|
|
6384
|
+
}
|
|
6322
6385
|
}
|
|
6323
|
-
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
|
|
6386
|
+
return updated;
|
|
6387
|
+
}
|
|
6388
|
+
function isInProtectedSpan(content, start, end) {
|
|
6389
|
+
const lineStart = content.lastIndexOf("\n", start) + 1;
|
|
6390
|
+
const lineEndIdx = content.indexOf("\n", end);
|
|
6391
|
+
const lineEnd = lineEndIdx === -1 ? content.length : lineEndIdx;
|
|
6392
|
+
const line = content.slice(lineStart, lineEnd);
|
|
6393
|
+
const linkPattern = /\[[^\]]*\]\([^)]*\)/g;
|
|
6394
|
+
let m;
|
|
6395
|
+
while ((m = linkPattern.exec(line)) !== null) {
|
|
6396
|
+
const absStart = lineStart + m.index;
|
|
6397
|
+
const absEnd = absStart + m[0].length;
|
|
6398
|
+
if (start >= absStart && end <= absEnd) {
|
|
6399
|
+
return true;
|
|
6400
|
+
}
|
|
6328
6401
|
}
|
|
6329
|
-
const
|
|
6330
|
-
|
|
6331
|
-
|
|
6402
|
+
const codePattern = /`[^`]+`/g;
|
|
6403
|
+
while ((m = codePattern.exec(line)) !== null) {
|
|
6404
|
+
const absStart = lineStart + m.index;
|
|
6405
|
+
const absEnd = absStart + m[0].length;
|
|
6406
|
+
if (start >= absStart && end <= absEnd) {
|
|
6407
|
+
return true;
|
|
6408
|
+
}
|
|
6332
6409
|
}
|
|
6333
|
-
return
|
|
6334
|
-
contentType,
|
|
6335
|
-
count
|
|
6336
|
-
};
|
|
6410
|
+
return false;
|
|
6337
6411
|
}
|
|
6338
|
-
function
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
|
|
6412
|
+
function escapeRegExp(value2) {
|
|
6413
|
+
return value2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6414
|
+
}
|
|
6415
|
+
|
|
6416
|
+
// src/server/previewHelpers.ts
|
|
6417
|
+
import { readdir, stat as stat4, readFile as readFile7 } from "fs/promises";
|
|
6418
|
+
import path10 from "path";
|
|
6419
|
+
var DEFAULT_PORT = 4173;
|
|
6420
|
+
var CONTENT_TYPE_ORDER = ["article", "blog-post", "x-thread", "x-post", "linkedin-post", "reddit-post", "newsletter"];
|
|
6421
|
+
var FILE_PREFIX_TO_CONTENT_TYPE = {
|
|
6422
|
+
article: "article",
|
|
6423
|
+
blog: "blog-post",
|
|
6424
|
+
"x-thread": "x-thread",
|
|
6425
|
+
"x-post": "x-post",
|
|
6426
|
+
x: "x-post",
|
|
6427
|
+
reddit: "reddit-post",
|
|
6428
|
+
linkedin: "linkedin-post",
|
|
6429
|
+
newsletter: "newsletter"
|
|
6430
|
+
};
|
|
6431
|
+
var CONTENT_TYPE_LABELS = {
|
|
6432
|
+
article: "Article",
|
|
6433
|
+
"blog-post": "Blog Post",
|
|
6434
|
+
"x-thread": "X Thread",
|
|
6435
|
+
"x-post": "X Post",
|
|
6436
|
+
"reddit-post": "Reddit Post",
|
|
6437
|
+
"linkedin-post": "LinkedIn Post",
|
|
6438
|
+
newsletter: "Newsletter"
|
|
6439
|
+
};
|
|
6440
|
+
function parsePort(portOption) {
|
|
6441
|
+
if (!portOption) {
|
|
6442
|
+
return DEFAULT_PORT;
|
|
6342
6443
|
}
|
|
6343
|
-
|
|
6344
|
-
|
|
6444
|
+
const port = Number.parseInt(portOption, 10);
|
|
6445
|
+
if (Number.isNaN(port) || port < 1 || port > 65535) {
|
|
6446
|
+
throw new Error(`Invalid port "${portOption}". Choose a value between 1 and 65535.`);
|
|
6345
6447
|
}
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6448
|
+
return port;
|
|
6449
|
+
}
|
|
6450
|
+
function stripFrontmatter2(markdown) {
|
|
6451
|
+
return markdown.replace(/^---\s*\n[\s\S]*?\n---\s*\n?/, "");
|
|
6452
|
+
}
|
|
6453
|
+
function extractFrontmatterSlug(markdown) {
|
|
6454
|
+
const frontmatterMatch = markdown.match(/^---\s*\n([\s\S]*?)\n---\s*\n?/);
|
|
6455
|
+
const frontmatter = frontmatterMatch?.[1];
|
|
6456
|
+
if (!frontmatter) {
|
|
6457
|
+
return null;
|
|
6349
6458
|
}
|
|
6350
|
-
const
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
|
|
6459
|
+
const slugMatch = frontmatter.match(/^slug:\s*(.+)$/m);
|
|
6460
|
+
const rawSlug = slugMatch?.[1]?.trim();
|
|
6461
|
+
if (!rawSlug) {
|
|
6462
|
+
return null;
|
|
6463
|
+
}
|
|
6464
|
+
const unquoted = rawSlug.replace(/^['\"]|['\"]$/g, "").trim();
|
|
6465
|
+
return unquoted.length > 0 ? unquoted : null;
|
|
6466
|
+
}
|
|
6467
|
+
function extractHeadingTitle(markdown) {
|
|
6468
|
+
const headingMatch = markdown.match(/^#\s+(.+)$/m);
|
|
6469
|
+
if (!headingMatch || !headingMatch[1]) {
|
|
6470
|
+
return null;
|
|
6471
|
+
}
|
|
6472
|
+
return headingMatch[1].trim();
|
|
6473
|
+
}
|
|
6474
|
+
async function resolveMarkdownPath(markdownPathArg, markdownOutputDir, cwd2) {
|
|
6475
|
+
if (markdownPathArg) {
|
|
6476
|
+
const resolved = path10.isAbsolute(markdownPathArg) ? markdownPathArg : path10.resolve(cwd2, markdownPathArg);
|
|
6477
|
+
if (path10.extname(resolved).toLowerCase() !== ".md") {
|
|
6478
|
+
throw new Error(`Expected a markdown file (.md), received: ${resolved}`);
|
|
6357
6479
|
}
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
6480
|
+
await assertFileExists(resolved, "Could not find markdown file");
|
|
6481
|
+
return resolved;
|
|
6482
|
+
}
|
|
6483
|
+
return await resolveLatestMarkdown(markdownOutputDir);
|
|
6484
|
+
}
|
|
6485
|
+
async function resolveLatestMarkdown(markdownOutputDir) {
|
|
6486
|
+
const markdownCandidates = await findMarkdownFiles(markdownOutputDir);
|
|
6487
|
+
if (markdownCandidates.length === 0) {
|
|
6488
|
+
throw new Error(
|
|
6489
|
+
`No generated articles found in ${markdownOutputDir}. Run ideon write "your idea" first or pass a markdown path.`
|
|
6490
|
+
);
|
|
6491
|
+
}
|
|
6492
|
+
let latestPath = markdownCandidates[0];
|
|
6493
|
+
let latestMtime = 0;
|
|
6494
|
+
for (const candidate of markdownCandidates) {
|
|
6495
|
+
const fileStat = await stat4(candidate);
|
|
6496
|
+
if (fileStat.mtimeMs >= latestMtime) {
|
|
6497
|
+
latestMtime = fileStat.mtimeMs;
|
|
6498
|
+
latestPath = candidate;
|
|
6362
6499
|
}
|
|
6363
|
-
secondaryDedupedByType.set(parsed.contentType, {
|
|
6364
|
-
...parsed,
|
|
6365
|
-
role: "secondary"
|
|
6366
|
-
});
|
|
6367
6500
|
}
|
|
6368
|
-
return
|
|
6369
|
-
{
|
|
6370
|
-
...primary,
|
|
6371
|
-
role: "primary"
|
|
6372
|
-
},
|
|
6373
|
-
...secondaryDedupedByType.values()
|
|
6374
|
-
];
|
|
6501
|
+
return latestPath;
|
|
6375
6502
|
}
|
|
6376
|
-
|
|
6377
|
-
|
|
6378
|
-
|
|
6379
|
-
|
|
6380
|
-
|
|
6381
|
-
|
|
6382
|
-
}
|
|
6383
|
-
|
|
6384
|
-
|
|
6385
|
-
|
|
6386
|
-
|
|
6387
|
-
|
|
6388
|
-
|
|
6389
|
-
|
|
6390
|
-
|
|
6391
|
-
|
|
6392
|
-
|
|
6393
|
-
|
|
6394
|
-
|
|
6395
|
-
|
|
6396
|
-
|
|
6397
|
-
|
|
6398
|
-
|
|
6399
|
-
|
|
6400
|
-
|
|
6401
|
-
|
|
6402
|
-
|
|
6403
|
-
|
|
6404
|
-
|
|
6405
|
-
|
|
6406
|
-
|
|
6407
|
-
|
|
6503
|
+
async function assertFileExists(filePath, errorPrefix) {
|
|
6504
|
+
try {
|
|
6505
|
+
const fileStat = await stat4(filePath);
|
|
6506
|
+
if (!fileStat.isFile()) {
|
|
6507
|
+
throw new Error(`${errorPrefix}: ${filePath}`);
|
|
6508
|
+
}
|
|
6509
|
+
} catch {
|
|
6510
|
+
throw new Error(`${errorPrefix}: ${filePath}`);
|
|
6511
|
+
}
|
|
6512
|
+
}
|
|
6513
|
+
function extractCoverImageUrl(markdown) {
|
|
6514
|
+
const body = stripFrontmatter2(markdown);
|
|
6515
|
+
const match = body.match(/!\[[^\]]*\]\(([^)]+)\)/);
|
|
6516
|
+
return match?.[1] ?? null;
|
|
6517
|
+
}
|
|
6518
|
+
async function extractArticleMetadata(markdownPath) {
|
|
6519
|
+
const markdown = await readFile7(markdownPath, "utf8");
|
|
6520
|
+
const fileStat = await stat4(markdownPath);
|
|
6521
|
+
const slug = extractFrontmatterSlug(markdown) ?? path10.basename(markdownPath, ".md");
|
|
6522
|
+
const title = extractHeadingTitle(stripFrontmatter2(markdown)) ?? slug;
|
|
6523
|
+
const body = stripFrontmatter2(markdown);
|
|
6524
|
+
const previewSnippet = body.replace(/[#\[\]()!\-*_`]/g, "").trim().substring(0, 150);
|
|
6525
|
+
const coverImageUrl = extractCoverImageUrl(markdown);
|
|
6526
|
+
return {
|
|
6527
|
+
slug,
|
|
6528
|
+
title,
|
|
6529
|
+
mtime: fileStat.mtimeMs,
|
|
6530
|
+
previewSnippet,
|
|
6531
|
+
coverImageUrl
|
|
6532
|
+
};
|
|
6533
|
+
}
|
|
6534
|
+
async function listAllGenerations(markdownOutputDir) {
|
|
6535
|
+
const markdownFiles = await findMarkdownFiles(markdownOutputDir);
|
|
6536
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
6537
|
+
for (const filePath of markdownFiles) {
|
|
6538
|
+
try {
|
|
6539
|
+
const metadata = await extractArticleMetadata(filePath);
|
|
6540
|
+
const identity = deriveOutputIdentity(filePath, markdownOutputDir);
|
|
6541
|
+
const output = {
|
|
6542
|
+
id: `${identity.generationId}:${identity.contentType}:${identity.index}`,
|
|
6543
|
+
generationId: identity.generationId,
|
|
6544
|
+
sourcePath: filePath,
|
|
6545
|
+
slug: metadata.slug,
|
|
6546
|
+
title: metadata.title,
|
|
6547
|
+
previewSnippet: metadata.previewSnippet,
|
|
6548
|
+
coverImageUrl: metadata.coverImageUrl,
|
|
6549
|
+
mtime: metadata.mtime,
|
|
6550
|
+
contentType: identity.contentType,
|
|
6551
|
+
contentTypeLabel: toContentTypeLabel(identity.contentType),
|
|
6552
|
+
index: identity.index
|
|
6553
|
+
};
|
|
6554
|
+
const existing = grouped.get(identity.generationId);
|
|
6555
|
+
if (existing) {
|
|
6556
|
+
existing.push(output);
|
|
6557
|
+
} else {
|
|
6558
|
+
grouped.set(identity.generationId, [output]);
|
|
6559
|
+
}
|
|
6560
|
+
} catch {
|
|
6561
|
+
}
|
|
6562
|
+
}
|
|
6563
|
+
const generations = [];
|
|
6564
|
+
for (const [id, outputs] of grouped.entries()) {
|
|
6565
|
+
outputs.sort((a, b) => compareContentTypes(a.contentType, b.contentType) || a.index - b.index || b.mtime - a.mtime);
|
|
6566
|
+
const primaryContentType = await resolvePrimaryContentType(outputs);
|
|
6567
|
+
const primary = outputs.find((output) => output.contentType === primaryContentType) ?? outputs[0];
|
|
6568
|
+
if (!primary) {
|
|
6569
|
+
continue;
|
|
6570
|
+
}
|
|
6571
|
+
const newestMtime = outputs.reduce((latest, output) => Math.max(latest, output.mtime), 0);
|
|
6572
|
+
generations.push({
|
|
6573
|
+
id,
|
|
6574
|
+
title: primary.title,
|
|
6575
|
+
mtime: newestMtime,
|
|
6576
|
+
previewSnippet: primary.previewSnippet,
|
|
6577
|
+
coverImageUrl: primary.coverImageUrl ?? outputs.find((output) => Boolean(output.coverImageUrl))?.coverImageUrl ?? null,
|
|
6578
|
+
primaryContentType,
|
|
6579
|
+
outputs
|
|
6580
|
+
});
|
|
6581
|
+
}
|
|
6582
|
+
generations.sort((a, b) => b.mtime - a.mtime);
|
|
6583
|
+
return generations;
|
|
6584
|
+
}
|
|
6585
|
+
function deriveGenerationId(markdownPath, markdownOutputDir) {
|
|
6586
|
+
const relative = path10.relative(markdownOutputDir, markdownPath);
|
|
6587
|
+
const normalized = relative.split(path10.sep).join("/");
|
|
6588
|
+
if (!normalized || normalized.startsWith("../")) {
|
|
6589
|
+
return path10.basename(markdownPath, ".md");
|
|
6590
|
+
}
|
|
6591
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
6592
|
+
if (segments.length <= 1) {
|
|
6593
|
+
return path10.basename(markdownPath, ".md");
|
|
6594
|
+
}
|
|
6595
|
+
return segments[0] ?? path10.basename(markdownPath, ".md");
|
|
6596
|
+
}
|
|
6597
|
+
async function findMarkdownFiles(markdownOutputDir) {
|
|
6598
|
+
const files = [];
|
|
6599
|
+
const stack = [markdownOutputDir];
|
|
6600
|
+
while (stack.length > 0) {
|
|
6601
|
+
const current = stack.pop();
|
|
6602
|
+
if (!current) {
|
|
6603
|
+
continue;
|
|
6604
|
+
}
|
|
6605
|
+
let entries;
|
|
6606
|
+
try {
|
|
6607
|
+
entries = await readdir(current, { withFileTypes: true });
|
|
6608
|
+
} catch {
|
|
6609
|
+
continue;
|
|
6610
|
+
}
|
|
6611
|
+
for (const entry of entries) {
|
|
6612
|
+
const fullPath = path10.join(current, entry.name);
|
|
6613
|
+
if (entry.isDirectory()) {
|
|
6614
|
+
stack.push(fullPath);
|
|
6615
|
+
continue;
|
|
6616
|
+
}
|
|
6617
|
+
if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
|
|
6618
|
+
files.push(fullPath);
|
|
6619
|
+
}
|
|
6620
|
+
}
|
|
6621
|
+
}
|
|
6622
|
+
return files;
|
|
6623
|
+
}
|
|
6624
|
+
function deriveOutputIdentity(markdownPath, markdownOutputDir) {
|
|
6625
|
+
const generationId = deriveGenerationId(markdownPath, markdownOutputDir);
|
|
6626
|
+
const fileBase = path10.basename(markdownPath, ".md");
|
|
6627
|
+
const parsed = fileBase.match(/^([a-z0-9-]+)-(\d+)$/i);
|
|
6628
|
+
if (!parsed || !parsed[1] || !parsed[2]) {
|
|
6629
|
+
return {
|
|
6630
|
+
generationId,
|
|
6631
|
+
contentType: "article",
|
|
6632
|
+
index: 1
|
|
6633
|
+
};
|
|
6634
|
+
}
|
|
6635
|
+
const prefix = parsed[1].toLowerCase();
|
|
6636
|
+
const index = Number.parseInt(parsed[2], 10);
|
|
6637
|
+
return {
|
|
6638
|
+
generationId,
|
|
6639
|
+
contentType: FILE_PREFIX_TO_CONTENT_TYPE[prefix] ?? prefix,
|
|
6640
|
+
index: Number.isFinite(index) && index > 0 ? index : 1
|
|
6641
|
+
};
|
|
6642
|
+
}
|
|
6643
|
+
function compareContentTypes(left, right) {
|
|
6644
|
+
const leftIndex = CONTENT_TYPE_ORDER.indexOf(left);
|
|
6645
|
+
const rightIndex = CONTENT_TYPE_ORDER.indexOf(right);
|
|
6646
|
+
const normalizedLeft = leftIndex === -1 ? Number.MAX_SAFE_INTEGER : leftIndex;
|
|
6647
|
+
const normalizedRight = rightIndex === -1 ? Number.MAX_SAFE_INTEGER : rightIndex;
|
|
6648
|
+
if (normalizedLeft !== normalizedRight) {
|
|
6649
|
+
return normalizedLeft - normalizedRight;
|
|
6650
|
+
}
|
|
6651
|
+
return left.localeCompare(right);
|
|
6652
|
+
}
|
|
6653
|
+
function toContentTypeLabel(contentType) {
|
|
6654
|
+
const knownLabel = CONTENT_TYPE_LABELS[contentType];
|
|
6655
|
+
if (knownLabel) {
|
|
6656
|
+
return knownLabel;
|
|
6657
|
+
}
|
|
6658
|
+
return contentType.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
6659
|
+
}
|
|
6660
|
+
async function resolvePrimaryContentType(outputs) {
|
|
6661
|
+
const fallback = outputs.find((output) => output.contentType === "article")?.contentType ?? outputs[0]?.contentType ?? "article";
|
|
6662
|
+
const generationDir = path10.dirname(outputs[0]?.sourcePath ?? "");
|
|
6663
|
+
if (!generationDir) {
|
|
6664
|
+
return fallback;
|
|
6665
|
+
}
|
|
6666
|
+
const jobPath = path10.join(generationDir, "job.json");
|
|
6667
|
+
try {
|
|
6668
|
+
const raw = await readFile7(jobPath, "utf8");
|
|
6669
|
+
const parsed = JSON.parse(raw);
|
|
6670
|
+
const targets = Array.isArray(parsed.contentTargets) ? parsed.contentTargets : Array.isArray(parsed.settings?.contentTargets) ? parsed.settings.contentTargets : [];
|
|
6671
|
+
const primary = targets.find((target) => target?.role === "primary" && typeof target.contentType === "string");
|
|
6672
|
+
if (primary && typeof primary.contentType === "string") {
|
|
6673
|
+
return primary.contentType;
|
|
6674
|
+
}
|
|
6675
|
+
} catch {
|
|
6676
|
+
return fallback;
|
|
6677
|
+
}
|
|
6678
|
+
return fallback;
|
|
6679
|
+
}
|
|
6680
|
+
|
|
6681
|
+
// src/cli/commands/export.ts
|
|
6682
|
+
async function runOutputCommand(options, dependencies = {}) {
|
|
6683
|
+
const cwd2 = dependencies.cwd ?? process.cwd();
|
|
6684
|
+
const log = dependencies.log ?? ((message) => console.log(message));
|
|
6685
|
+
const targetIndex = options.index ?? 1;
|
|
6686
|
+
const resolved = await resolveRunInput({ idea: `Export generation ${options.generationId}` });
|
|
6687
|
+
const outputPaths = resolveOutputPaths(resolved.config.settings, cwd2);
|
|
6688
|
+
const generations = await listAllGenerations(outputPaths.markdownOutputDir);
|
|
6689
|
+
const generation = resolveGeneration(generations, options.generationId);
|
|
6690
|
+
const articleOutputs = generation.outputs.filter((output) => output.contentType === generation.primaryContentType);
|
|
6691
|
+
if (articleOutputs.length === 0) {
|
|
6692
|
+
throw new ReportedError(
|
|
6693
|
+
`Generation "${generation.id}" has no primary content outputs (type: ${generation.primaryContentType}).`
|
|
6694
|
+
);
|
|
6695
|
+
}
|
|
6696
|
+
const articleOutput = articleOutputs.find((output) => output.index === targetIndex);
|
|
6697
|
+
if (!articleOutput) {
|
|
6698
|
+
const available = articleOutputs.map((output) => output.index).join(", ");
|
|
6699
|
+
throw new ReportedError(
|
|
6700
|
+
`Generation "${generation.id}" has no primary output at index ${targetIndex}. Available: ${available}.`
|
|
6701
|
+
);
|
|
6702
|
+
}
|
|
6703
|
+
const sourceMarkdownPath = articleOutput.sourcePath;
|
|
6704
|
+
const sourceMarkdown = await readFile8(sourceMarkdownPath, "utf8");
|
|
6705
|
+
const slug = extractFrontmatterSlug2(sourceMarkdown) ?? path11.basename(sourceMarkdownPath, ".md");
|
|
6706
|
+
const exportFilename = `${slug}.md`;
|
|
6707
|
+
const destinationDir = await resolveDestinationDir(options.destinationPath, cwd2);
|
|
6708
|
+
const destinationFilePath = path11.join(destinationDir, exportFilename);
|
|
6709
|
+
if (!options.overwrite && await fileExists2(destinationFilePath)) {
|
|
6710
|
+
throw new ReportedError(
|
|
6711
|
+
`Export file already exists: ${destinationFilePath}. Pass --overwrite to replace it.`
|
|
6712
|
+
);
|
|
6713
|
+
}
|
|
6714
|
+
await mkdir6(destinationDir, { recursive: true });
|
|
6715
|
+
const links = await loadLinks(sourceMarkdownPath);
|
|
6716
|
+
const enrichedMarkdown = enrichWithFrontmatterGuard(sourceMarkdown, links);
|
|
6717
|
+
const sourceDir = path11.dirname(sourceMarkdownPath);
|
|
6718
|
+
const imagePaths = extractLocalImagePaths(sourceMarkdown);
|
|
6719
|
+
const copiedImages = [];
|
|
6720
|
+
for (const relImagePath of imagePaths) {
|
|
6721
|
+
const absoluteImageSrc = path11.resolve(sourceDir, relImagePath);
|
|
6722
|
+
let imageStat = null;
|
|
6723
|
+
try {
|
|
6724
|
+
imageStat = await stat5(absoluteImageSrc);
|
|
6725
|
+
} catch {
|
|
6726
|
+
throw new ReportedError(
|
|
6727
|
+
`Referenced image not found: ${relImagePath} (resolved to ${absoluteImageSrc}).`
|
|
6728
|
+
);
|
|
6729
|
+
}
|
|
6730
|
+
if (!imageStat.isFile()) {
|
|
6731
|
+
throw new ReportedError(`Referenced image path is not a file: ${absoluteImageSrc}.`);
|
|
6732
|
+
}
|
|
6733
|
+
const destImagePath = path11.join(destinationDir, relImagePath);
|
|
6734
|
+
await mkdir6(path11.dirname(destImagePath), { recursive: true });
|
|
6735
|
+
await copyFile(absoluteImageSrc, destImagePath);
|
|
6736
|
+
copiedImages.push(relImagePath);
|
|
6737
|
+
}
|
|
6738
|
+
await writeFile6(destinationFilePath, enrichedMarkdown, "utf8");
|
|
6739
|
+
const relDest = path11.relative(cwd2, destinationFilePath);
|
|
6740
|
+
log(`Exported "${generation.id}" (${generation.primaryContentType} #${targetIndex}) \u2192 ${relDest}`);
|
|
6741
|
+
if (copiedImages.length > 0) {
|
|
6742
|
+
log(`Copied ${copiedImages.length} image${copiedImages.length === 1 ? "" : "s"}: ${copiedImages.join(", ")}`);
|
|
6743
|
+
}
|
|
6744
|
+
if (links.length > 0) {
|
|
6745
|
+
log(`Injected ${links.length} inline link${links.length === 1 ? "" : "s"}.`);
|
|
6746
|
+
}
|
|
6747
|
+
}
|
|
6748
|
+
function resolveGeneration(generations, generationId) {
|
|
6749
|
+
const exact = generations.find((g) => g.id === generationId);
|
|
6750
|
+
if (exact) {
|
|
6751
|
+
return exact;
|
|
6752
|
+
}
|
|
6753
|
+
const bySlug = generations.find(
|
|
6754
|
+
(g) => g.outputs.some((output) => output.slug === generationId)
|
|
6755
|
+
);
|
|
6756
|
+
if (bySlug) {
|
|
6757
|
+
return bySlug;
|
|
6758
|
+
}
|
|
6759
|
+
throw new ReportedError(
|
|
6760
|
+
`Generation "${generationId}" not found. Run \`ideon preview\` to list available generations.`
|
|
6761
|
+
);
|
|
6762
|
+
}
|
|
6763
|
+
async function resolveDestinationDir(destinationPath, cwd2) {
|
|
6764
|
+
const resolved = path11.isAbsolute(destinationPath) ? destinationPath : path11.resolve(cwd2, destinationPath);
|
|
6765
|
+
return resolved;
|
|
6766
|
+
}
|
|
6767
|
+
async function fileExists2(filePath) {
|
|
6768
|
+
try {
|
|
6769
|
+
const fileStat = await stat5(filePath);
|
|
6770
|
+
return fileStat.isFile();
|
|
6771
|
+
} catch {
|
|
6772
|
+
return false;
|
|
6773
|
+
}
|
|
6774
|
+
}
|
|
6775
|
+
async function loadLinks(markdownPath) {
|
|
6776
|
+
const linksPath = resolveLinksPath(markdownPath);
|
|
6777
|
+
let raw;
|
|
6778
|
+
try {
|
|
6779
|
+
raw = await readFile8(linksPath, "utf8");
|
|
6780
|
+
} catch {
|
|
6781
|
+
return [];
|
|
6782
|
+
}
|
|
6783
|
+
let parsed;
|
|
6784
|
+
try {
|
|
6785
|
+
parsed = JSON.parse(raw);
|
|
6786
|
+
} catch {
|
|
6787
|
+
return [];
|
|
6788
|
+
}
|
|
6789
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
6790
|
+
return [];
|
|
6791
|
+
}
|
|
6792
|
+
const record = parsed;
|
|
6793
|
+
const links = Array.isArray(record.links) ? record.links : [];
|
|
6794
|
+
const customLinks = Array.isArray(record.customLinks) ? record.customLinks : [];
|
|
6795
|
+
const combined = [...customLinks, ...links];
|
|
6796
|
+
return combined.filter((entry) => {
|
|
6797
|
+
if (typeof entry !== "object" || entry === null) {
|
|
6798
|
+
return false;
|
|
6799
|
+
}
|
|
6800
|
+
const e = entry;
|
|
6801
|
+
return typeof e.expression === "string" && typeof e.url === "string" && (e.title === null || typeof e.title === "string");
|
|
6802
|
+
}).map((entry) => ({
|
|
6803
|
+
expression: entry.expression.trim(),
|
|
6804
|
+
url: entry.url.trim(),
|
|
6805
|
+
title: entry.title
|
|
6806
|
+
})).filter((entry) => entry.expression.length > 0 && entry.url.length > 0);
|
|
6807
|
+
}
|
|
6808
|
+
function enrichWithFrontmatterGuard(markdown, links) {
|
|
6809
|
+
if (links.length === 0) {
|
|
6810
|
+
return markdown;
|
|
6811
|
+
}
|
|
6812
|
+
const frontmatterMatch = markdown.match(/^---\s*\n[\s\S]*?\n---\s*\n?/);
|
|
6813
|
+
if (!frontmatterMatch) {
|
|
6814
|
+
return enrichMarkdownWithLinks(markdown, links);
|
|
6815
|
+
}
|
|
6816
|
+
const frontmatter = frontmatterMatch[0];
|
|
6817
|
+
const body = markdown.slice(frontmatter.length);
|
|
6818
|
+
return `${frontmatter}${enrichMarkdownWithLinks(body, links)}`;
|
|
6819
|
+
}
|
|
6820
|
+
function extractFrontmatterSlug2(markdown) {
|
|
6821
|
+
const frontmatterMatch = markdown.match(/^---\s*\n([\s\S]*?)\n---\s*\n?/);
|
|
6822
|
+
const block = frontmatterMatch?.[1];
|
|
6823
|
+
if (!block) {
|
|
6824
|
+
return null;
|
|
6825
|
+
}
|
|
6826
|
+
const slugMatch = block.match(/^slug:\s*(.+)$/m);
|
|
6827
|
+
const rawSlug = slugMatch?.[1]?.trim();
|
|
6828
|
+
if (!rawSlug) {
|
|
6829
|
+
return null;
|
|
6830
|
+
}
|
|
6831
|
+
const unquoted = rawSlug.replace(/^['""]|['""]$/g, "").trim();
|
|
6832
|
+
return unquoted.length > 0 ? unquoted : null;
|
|
6833
|
+
}
|
|
6834
|
+
function extractLocalImagePaths(markdown) {
|
|
6835
|
+
const imagePattern = /!\[[^\]]*\]\(([^)]+)\)/g;
|
|
6836
|
+
const paths = [];
|
|
6837
|
+
let match;
|
|
6838
|
+
while ((match = imagePattern.exec(markdown)) !== null) {
|
|
6839
|
+
const rawPath = match[1]?.trim();
|
|
6840
|
+
if (!rawPath) {
|
|
6841
|
+
continue;
|
|
6842
|
+
}
|
|
6843
|
+
if (rawPath.startsWith("http://") || rawPath.startsWith("https://") || rawPath.startsWith("data:") || rawPath.startsWith("/") || rawPath.startsWith("#")) {
|
|
6844
|
+
continue;
|
|
6845
|
+
}
|
|
6846
|
+
paths.push(rawPath);
|
|
6847
|
+
}
|
|
6848
|
+
return paths;
|
|
6849
|
+
}
|
|
6850
|
+
|
|
6851
|
+
// src/cli/commands/writeTargetSpecs.ts
|
|
6852
|
+
function parseTargetSpec(spec) {
|
|
6853
|
+
const trimmed = spec.trim();
|
|
6854
|
+
if (!trimmed) {
|
|
6855
|
+
throw new ReportedError("Target spec cannot be empty. Use <content-type=count>.");
|
|
6856
|
+
}
|
|
6857
|
+
const [rawType, rawCount] = trimmed.split("=");
|
|
6858
|
+
if (!rawType || !rawCount) {
|
|
6859
|
+
throw new ReportedError(`Invalid target "${spec}". Use format content-type=count, e.g. article=1 or x-thread=3.`);
|
|
6860
|
+
}
|
|
6861
|
+
const contentType = rawType.trim();
|
|
6862
|
+
if (!contentTypeValues.includes(contentType)) {
|
|
6863
|
+
throw new ReportedError(
|
|
6864
|
+
`Unsupported content type "${contentType}". Supported values: ${contentTypeValues.join(", ")}.`
|
|
6865
|
+
);
|
|
6866
|
+
}
|
|
6867
|
+
const count = Number.parseInt(rawCount.trim(), 10);
|
|
6868
|
+
if (!Number.isFinite(count) || count <= 0) {
|
|
6869
|
+
throw new ReportedError(`Invalid count in target "${spec}". Count must be a positive integer.`);
|
|
6870
|
+
}
|
|
6871
|
+
return {
|
|
6872
|
+
contentType,
|
|
6873
|
+
count
|
|
6874
|
+
};
|
|
6875
|
+
}
|
|
6876
|
+
function parsePrimaryAndSecondarySpecs(options) {
|
|
6877
|
+
const { primarySpec, secondarySpecs } = options;
|
|
6878
|
+
if (!primarySpec && (!secondarySpecs || secondarySpecs.length === 0)) {
|
|
6879
|
+
return void 0;
|
|
6880
|
+
}
|
|
6881
|
+
if (!primarySpec) {
|
|
6882
|
+
throw new ReportedError("Missing required --primary <content-type=count>.");
|
|
6883
|
+
}
|
|
6884
|
+
const primary = parseTargetSpec(primarySpec);
|
|
6885
|
+
if (primary.count !== 1) {
|
|
6886
|
+
throw new ReportedError("Primary target count must be exactly 1. Use --primary <content-type=1>.");
|
|
6887
|
+
}
|
|
6888
|
+
const secondaryDedupedByType = /* @__PURE__ */ new Map();
|
|
6889
|
+
for (const spec of secondarySpecs ?? []) {
|
|
6890
|
+
const parsed = parseTargetSpec(spec);
|
|
6891
|
+
if (parsed.contentType === primary.contentType) {
|
|
6892
|
+
throw new ReportedError(
|
|
6893
|
+
`Content type "${parsed.contentType}" cannot be both primary and secondary in the same run.`
|
|
6894
|
+
);
|
|
6895
|
+
}
|
|
6896
|
+
const previous = secondaryDedupedByType.get(parsed.contentType);
|
|
6897
|
+
if (previous) {
|
|
6898
|
+
previous.count += parsed.count;
|
|
6899
|
+
continue;
|
|
6900
|
+
}
|
|
6901
|
+
secondaryDedupedByType.set(parsed.contentType, {
|
|
6902
|
+
...parsed,
|
|
6903
|
+
role: "secondary"
|
|
6904
|
+
});
|
|
6905
|
+
}
|
|
6906
|
+
return [
|
|
6907
|
+
{
|
|
6908
|
+
...primary,
|
|
6909
|
+
role: "primary"
|
|
6910
|
+
},
|
|
6911
|
+
...secondaryDedupedByType.values()
|
|
6912
|
+
];
|
|
6913
|
+
}
|
|
6914
|
+
|
|
6915
|
+
// src/integrations/mcp/server.ts
|
|
6916
|
+
async function startIdeonMcpServer() {
|
|
6917
|
+
const server = new McpServer({
|
|
6918
|
+
name: "ideon",
|
|
6919
|
+
version: package_default.version
|
|
6920
|
+
});
|
|
6921
|
+
server.registerTool(
|
|
6922
|
+
"ideon_write",
|
|
6923
|
+
{
|
|
6924
|
+
title: "Ideon Write",
|
|
6925
|
+
description: "Generate content from an idea using the Ideon pipeline.",
|
|
6926
|
+
inputSchema: writeToolInputSchema
|
|
6927
|
+
},
|
|
6928
|
+
async (input) => {
|
|
6929
|
+
try {
|
|
6930
|
+
const parsedTargets = parsePrimaryAndSecondarySpecs({
|
|
6931
|
+
primarySpec: input.primary,
|
|
6932
|
+
secondarySpecs: input.secondary
|
|
6933
|
+
});
|
|
6934
|
+
const resolved = await resolveRunInput({
|
|
6935
|
+
idea: input.idea,
|
|
6936
|
+
audience: input.audience,
|
|
6937
|
+
jobPath: input.jobPath,
|
|
6938
|
+
style: input.style,
|
|
6939
|
+
intent: input.intent,
|
|
6940
|
+
targetLength: input.length,
|
|
6941
|
+
contentTargets: parsedTargets
|
|
6942
|
+
});
|
|
6943
|
+
const run = await runPipelineShell(resolved, {
|
|
6944
|
+
workingDir: cwd(),
|
|
6945
|
+
runMode: "fresh",
|
|
6408
6946
|
dryRun: input.dryRun ?? false,
|
|
6409
6947
|
enrichLinks: input.enrichLinks ?? false,
|
|
6410
6948
|
customLinks: input.link,
|
|
6411
6949
|
unlinks: input.unlink,
|
|
6412
|
-
maxLinks: input.maxLinks
|
|
6950
|
+
maxLinks: input.maxLinks,
|
|
6951
|
+
maxImages: input.maxImages
|
|
6413
6952
|
});
|
|
6414
6953
|
return {
|
|
6415
6954
|
content: [
|
|
@@ -6468,7 +7007,8 @@ async function startIdeonMcpServer() {
|
|
|
6468
7007
|
enrichLinks: input.enrichLinks ?? false,
|
|
6469
7008
|
customLinks: input.link,
|
|
6470
7009
|
unlinks: input.unlink,
|
|
6471
|
-
maxLinks: input.maxLinks
|
|
7010
|
+
maxLinks: input.maxLinks,
|
|
7011
|
+
maxImages: input.maxImages
|
|
6472
7012
|
});
|
|
6473
7013
|
return {
|
|
6474
7014
|
content: [
|
|
@@ -6569,6 +7109,50 @@ async function startIdeonMcpServer() {
|
|
|
6569
7109
|
}
|
|
6570
7110
|
}
|
|
6571
7111
|
);
|
|
7112
|
+
server.registerTool(
|
|
7113
|
+
"ideon_export",
|
|
7114
|
+
{
|
|
7115
|
+
title: "Ideon Export",
|
|
7116
|
+
description: "Export a generated article as a standalone markdown file with inline links and copied images.",
|
|
7117
|
+
inputSchema: exportToolInputZodSchema
|
|
7118
|
+
},
|
|
7119
|
+
async (input) => {
|
|
7120
|
+
try {
|
|
7121
|
+
const messages = [];
|
|
7122
|
+
await runOutputCommand(
|
|
7123
|
+
{
|
|
7124
|
+
generationId: input.generationId,
|
|
7125
|
+
destinationPath: input.destinationPath,
|
|
7126
|
+
index: input.index,
|
|
7127
|
+
overwrite: input.overwrite
|
|
7128
|
+
},
|
|
7129
|
+
{
|
|
7130
|
+
cwd: cwd(),
|
|
7131
|
+
log: (message) => {
|
|
7132
|
+
messages.push(message);
|
|
7133
|
+
}
|
|
7134
|
+
}
|
|
7135
|
+
);
|
|
7136
|
+
return {
|
|
7137
|
+
content: [
|
|
7138
|
+
{
|
|
7139
|
+
type: "text",
|
|
7140
|
+
text: messages.length > 0 ? messages.join("\n") : `Exported ${input.generationId}.`
|
|
7141
|
+
}
|
|
7142
|
+
],
|
|
7143
|
+
structuredContent: {
|
|
7144
|
+
generationId: input.generationId,
|
|
7145
|
+
destinationPath: input.destinationPath,
|
|
7146
|
+
index: input.index ?? 1,
|
|
7147
|
+
overwrite: input.overwrite ?? false,
|
|
7148
|
+
messages
|
|
7149
|
+
}
|
|
7150
|
+
};
|
|
7151
|
+
} catch (error) {
|
|
7152
|
+
return formatToolError(error);
|
|
7153
|
+
}
|
|
7154
|
+
}
|
|
7155
|
+
);
|
|
6572
7156
|
server.registerTool(
|
|
6573
7157
|
"ideon_config_get",
|
|
6574
7158
|
{
|
|
@@ -6934,476 +7518,156 @@ function applyEdit(action, value2, settings, secrets, setSettings, setSecrets) {
|
|
|
6934
7518
|
setSettings({
|
|
6935
7519
|
...settings,
|
|
6936
7520
|
notifications: {
|
|
6937
|
-
...settings.notifications,
|
|
6938
|
-
enabled: parsed
|
|
6939
|
-
}
|
|
6940
|
-
});
|
|
6941
|
-
return;
|
|
6942
|
-
}
|
|
6943
|
-
if (action === "temperature") {
|
|
6944
|
-
const nextTemperature = clampNumber2(parseNumberOrFallback(value2, settings.modelSettings.temperature), 0, 2);
|
|
6945
|
-
setSettings({
|
|
6946
|
-
...settings,
|
|
6947
|
-
modelSettings: {
|
|
6948
|
-
...settings.modelSettings,
|
|
6949
|
-
temperature: nextTemperature
|
|
6950
|
-
}
|
|
6951
|
-
});
|
|
6952
|
-
return;
|
|
6953
|
-
}
|
|
6954
|
-
if (action === "maxTokens") {
|
|
6955
|
-
const nextMaxTokens = Math.max(1, Math.round(parseNumberOrFallback(value2, settings.modelSettings.maxTokens)));
|
|
6956
|
-
setSettings({
|
|
6957
|
-
...settings,
|
|
6958
|
-
modelSettings: {
|
|
6959
|
-
...settings.modelSettings,
|
|
6960
|
-
maxTokens: nextMaxTokens
|
|
7521
|
+
...settings.notifications,
|
|
7522
|
+
enabled: parsed
|
|
6961
7523
|
}
|
|
6962
7524
|
});
|
|
6963
7525
|
return;
|
|
6964
7526
|
}
|
|
6965
|
-
if (action === "
|
|
6966
|
-
const
|
|
7527
|
+
if (action === "temperature") {
|
|
7528
|
+
const nextTemperature = clampNumber2(parseNumberOrFallback(value2, settings.modelSettings.temperature), 0, 2);
|
|
6967
7529
|
setSettings({
|
|
6968
7530
|
...settings,
|
|
6969
7531
|
modelSettings: {
|
|
6970
7532
|
...settings.modelSettings,
|
|
6971
|
-
|
|
7533
|
+
temperature: nextTemperature
|
|
6972
7534
|
}
|
|
6973
7535
|
});
|
|
6974
7536
|
return;
|
|
6975
7537
|
}
|
|
6976
|
-
if (action === "
|
|
6977
|
-
|
|
6978
|
-
return;
|
|
6979
|
-
}
|
|
6980
|
-
if (action === "assetOutputDir") {
|
|
6981
|
-
setSettings({ ...settings, assetOutputDir: value2.trim() || settings.assetOutputDir });
|
|
6982
|
-
return;
|
|
6983
|
-
}
|
|
6984
|
-
if (action.startsWith("t2i:")) {
|
|
6985
|
-
const fieldName = action.slice(4);
|
|
6986
|
-
const parsedValue = coerceT2IFieldValue(settings.t2i.modelId, fieldName, value2);
|
|
6987
|
-
const nextOverrides = { ...settings.t2i.inputOverrides };
|
|
6988
|
-
if (parsedValue === void 0) {
|
|
6989
|
-
delete nextOverrides[fieldName];
|
|
6990
|
-
} else {
|
|
6991
|
-
nextOverrides[fieldName] = parsedValue;
|
|
6992
|
-
}
|
|
7538
|
+
if (action === "maxTokens") {
|
|
7539
|
+
const nextMaxTokens = Math.max(1, Math.round(parseNumberOrFallback(value2, settings.modelSettings.maxTokens)));
|
|
6993
7540
|
setSettings({
|
|
6994
7541
|
...settings,
|
|
6995
|
-
|
|
6996
|
-
...settings.
|
|
6997
|
-
|
|
6998
|
-
}
|
|
6999
|
-
});
|
|
7000
|
-
}
|
|
7001
|
-
}
|
|
7002
|
-
function formatEditorValue(value2) {
|
|
7003
|
-
if (value2 === null || value2 === void 0) {
|
|
7004
|
-
return "";
|
|
7005
|
-
}
|
|
7006
|
-
return String(value2);
|
|
7007
|
-
}
|
|
7008
|
-
function formatValue(value2) {
|
|
7009
|
-
if (value2 === null || value2 === void 0 || value2 === "") {
|
|
7010
|
-
return "(default)";
|
|
7011
|
-
}
|
|
7012
|
-
return String(value2);
|
|
7013
|
-
}
|
|
7014
|
-
function parseNumberOrFallback(value2, fallback) {
|
|
7015
|
-
const parsed = Number(value2.trim());
|
|
7016
|
-
return Number.isFinite(parsed) ? parsed : fallback;
|
|
7017
|
-
}
|
|
7018
|
-
function clampNumber2(value2, minimum, maximum) {
|
|
7019
|
-
return Math.min(maximum, Math.max(minimum, value2));
|
|
7020
|
-
}
|
|
7021
|
-
function parseBooleanOrFallback(value2, fallback) {
|
|
7022
|
-
const normalized = value2.trim().toLowerCase();
|
|
7023
|
-
if (normalized === "true") {
|
|
7024
|
-
return true;
|
|
7025
|
-
}
|
|
7026
|
-
if (normalized === "false") {
|
|
7027
|
-
return false;
|
|
7028
|
-
}
|
|
7029
|
-
return fallback;
|
|
7030
|
-
}
|
|
7031
|
-
|
|
7032
|
-
// src/cli/commands/settings.tsx
|
|
7033
|
-
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
7034
|
-
async function openSettings() {
|
|
7035
|
-
const envSettings = readEnvSettings();
|
|
7036
|
-
const [settings, secrets] = await Promise.all([
|
|
7037
|
-
loadSavedSettings(),
|
|
7038
|
-
loadSecrets({ disableKeytar: envSettings.disableKeytar })
|
|
7039
|
-
]);
|
|
7040
|
-
let result = null;
|
|
7041
|
-
const app = render(
|
|
7042
|
-
/* @__PURE__ */ jsx2(
|
|
7043
|
-
SettingsFlow,
|
|
7044
|
-
{
|
|
7045
|
-
initialSettings: settings,
|
|
7046
|
-
initialSecrets: secrets,
|
|
7047
|
-
onDone: (value2) => {
|
|
7048
|
-
result = value2;
|
|
7049
|
-
}
|
|
7050
|
-
}
|
|
7051
|
-
)
|
|
7052
|
-
);
|
|
7053
|
-
await app.waitUntilExit();
|
|
7054
|
-
const finalResult = result;
|
|
7055
|
-
if (!finalResult) {
|
|
7056
|
-
console.log("Settings unchanged.");
|
|
7057
|
-
return;
|
|
7058
|
-
}
|
|
7059
|
-
const savedResult = finalResult;
|
|
7060
|
-
await saveSettings(savedResult.settings);
|
|
7061
|
-
try {
|
|
7062
|
-
await saveSecrets(savedResult.secrets, { disableKeytar: envSettings.disableKeytar });
|
|
7063
|
-
} catch (error) {
|
|
7064
|
-
if (error instanceof KeytarUnavailableError) {
|
|
7065
|
-
console.log("Settings saved, but secrets were not stored in the system keychain.");
|
|
7066
|
-
console.log("Use IDEON_OPENROUTER_API_KEY and IDEON_REPLICATE_API_TOKEN in this environment.");
|
|
7067
|
-
return;
|
|
7068
|
-
}
|
|
7069
|
-
throw error;
|
|
7070
|
-
}
|
|
7071
|
-
console.log(`Settings saved to ${getSettingsFilePath()}.`);
|
|
7072
|
-
}
|
|
7073
|
-
|
|
7074
|
-
// src/cli/commands/serve.ts
|
|
7075
|
-
import path12 from "path";
|
|
7076
|
-
import { spawn } from "child_process";
|
|
7077
|
-
|
|
7078
|
-
// src/server/previewHelpers.ts
|
|
7079
|
-
import { readdir, stat as stat4, readFile as readFile7 } from "fs/promises";
|
|
7080
|
-
import path10 from "path";
|
|
7081
|
-
var DEFAULT_PORT = 4173;
|
|
7082
|
-
var CONTENT_TYPE_ORDER = ["article", "blog-post", "x-thread", "x-post", "linkedin-post", "reddit-post", "newsletter"];
|
|
7083
|
-
var FILE_PREFIX_TO_CONTENT_TYPE = {
|
|
7084
|
-
article: "article",
|
|
7085
|
-
blog: "blog-post",
|
|
7086
|
-
"x-thread": "x-thread",
|
|
7087
|
-
"x-post": "x-post",
|
|
7088
|
-
x: "x-post",
|
|
7089
|
-
reddit: "reddit-post",
|
|
7090
|
-
linkedin: "linkedin-post",
|
|
7091
|
-
newsletter: "newsletter"
|
|
7092
|
-
};
|
|
7093
|
-
var CONTENT_TYPE_LABELS = {
|
|
7094
|
-
article: "Article",
|
|
7095
|
-
"blog-post": "Blog Post",
|
|
7096
|
-
"x-thread": "X Thread",
|
|
7097
|
-
"x-post": "X Post",
|
|
7098
|
-
"reddit-post": "Reddit Post",
|
|
7099
|
-
"linkedin-post": "LinkedIn Post",
|
|
7100
|
-
newsletter: "Newsletter"
|
|
7101
|
-
};
|
|
7102
|
-
function parsePort(portOption) {
|
|
7103
|
-
if (!portOption) {
|
|
7104
|
-
return DEFAULT_PORT;
|
|
7105
|
-
}
|
|
7106
|
-
const port = Number.parseInt(portOption, 10);
|
|
7107
|
-
if (Number.isNaN(port) || port < 1 || port > 65535) {
|
|
7108
|
-
throw new Error(`Invalid port "${portOption}". Choose a value between 1 and 65535.`);
|
|
7109
|
-
}
|
|
7110
|
-
return port;
|
|
7111
|
-
}
|
|
7112
|
-
function stripFrontmatter2(markdown) {
|
|
7113
|
-
return markdown.replace(/^---\s*\n[\s\S]*?\n---\s*\n?/, "");
|
|
7114
|
-
}
|
|
7115
|
-
function extractFrontmatterSlug(markdown) {
|
|
7116
|
-
const frontmatterMatch = markdown.match(/^---\s*\n([\s\S]*?)\n---\s*\n?/);
|
|
7117
|
-
const frontmatter = frontmatterMatch?.[1];
|
|
7118
|
-
if (!frontmatter) {
|
|
7119
|
-
return null;
|
|
7120
|
-
}
|
|
7121
|
-
const slugMatch = frontmatter.match(/^slug:\s*(.+)$/m);
|
|
7122
|
-
const rawSlug = slugMatch?.[1]?.trim();
|
|
7123
|
-
if (!rawSlug) {
|
|
7124
|
-
return null;
|
|
7125
|
-
}
|
|
7126
|
-
const unquoted = rawSlug.replace(/^['\"]|['\"]$/g, "").trim();
|
|
7127
|
-
return unquoted.length > 0 ? unquoted : null;
|
|
7128
|
-
}
|
|
7129
|
-
function extractHeadingTitle(markdown) {
|
|
7130
|
-
const headingMatch = markdown.match(/^#\s+(.+)$/m);
|
|
7131
|
-
if (!headingMatch || !headingMatch[1]) {
|
|
7132
|
-
return null;
|
|
7133
|
-
}
|
|
7134
|
-
return headingMatch[1].trim();
|
|
7135
|
-
}
|
|
7136
|
-
async function resolveMarkdownPath(markdownPathArg, markdownOutputDir, cwd2) {
|
|
7137
|
-
if (markdownPathArg) {
|
|
7138
|
-
const resolved = path10.isAbsolute(markdownPathArg) ? markdownPathArg : path10.resolve(cwd2, markdownPathArg);
|
|
7139
|
-
if (path10.extname(resolved).toLowerCase() !== ".md") {
|
|
7140
|
-
throw new Error(`Expected a markdown file (.md), received: ${resolved}`);
|
|
7141
|
-
}
|
|
7142
|
-
await assertFileExists(resolved, "Could not find markdown file");
|
|
7143
|
-
return resolved;
|
|
7144
|
-
}
|
|
7145
|
-
return await resolveLatestMarkdown(markdownOutputDir);
|
|
7146
|
-
}
|
|
7147
|
-
async function resolveLatestMarkdown(markdownOutputDir) {
|
|
7148
|
-
const markdownCandidates = await findMarkdownFiles(markdownOutputDir);
|
|
7149
|
-
if (markdownCandidates.length === 0) {
|
|
7150
|
-
throw new Error(
|
|
7151
|
-
`No generated articles found in ${markdownOutputDir}. Run ideon write "your idea" first or pass a markdown path.`
|
|
7152
|
-
);
|
|
7153
|
-
}
|
|
7154
|
-
let latestPath = markdownCandidates[0];
|
|
7155
|
-
let latestMtime = 0;
|
|
7156
|
-
for (const candidate of markdownCandidates) {
|
|
7157
|
-
const fileStat = await stat4(candidate);
|
|
7158
|
-
if (fileStat.mtimeMs >= latestMtime) {
|
|
7159
|
-
latestMtime = fileStat.mtimeMs;
|
|
7160
|
-
latestPath = candidate;
|
|
7161
|
-
}
|
|
7162
|
-
}
|
|
7163
|
-
return latestPath;
|
|
7164
|
-
}
|
|
7165
|
-
async function assertFileExists(filePath, errorPrefix) {
|
|
7166
|
-
try {
|
|
7167
|
-
const fileStat = await stat4(filePath);
|
|
7168
|
-
if (!fileStat.isFile()) {
|
|
7169
|
-
throw new Error(`${errorPrefix}: ${filePath}`);
|
|
7170
|
-
}
|
|
7171
|
-
} catch {
|
|
7172
|
-
throw new Error(`${errorPrefix}: ${filePath}`);
|
|
7173
|
-
}
|
|
7174
|
-
}
|
|
7175
|
-
function extractCoverImageUrl(markdown) {
|
|
7176
|
-
const body = stripFrontmatter2(markdown);
|
|
7177
|
-
const match = body.match(/!\[[^\]]*\]\(([^)]+)\)/);
|
|
7178
|
-
return match?.[1] ?? null;
|
|
7179
|
-
}
|
|
7180
|
-
async function extractArticleMetadata(markdownPath) {
|
|
7181
|
-
const markdown = await readFile7(markdownPath, "utf8");
|
|
7182
|
-
const fileStat = await stat4(markdownPath);
|
|
7183
|
-
const slug = extractFrontmatterSlug(markdown) ?? path10.basename(markdownPath, ".md");
|
|
7184
|
-
const title = extractHeadingTitle(stripFrontmatter2(markdown)) ?? slug;
|
|
7185
|
-
const body = stripFrontmatter2(markdown);
|
|
7186
|
-
const previewSnippet = body.replace(/[#\[\]()!\-*_`]/g, "").trim().substring(0, 150);
|
|
7187
|
-
const coverImageUrl = extractCoverImageUrl(markdown);
|
|
7188
|
-
return {
|
|
7189
|
-
slug,
|
|
7190
|
-
title,
|
|
7191
|
-
mtime: fileStat.mtimeMs,
|
|
7192
|
-
previewSnippet,
|
|
7193
|
-
coverImageUrl
|
|
7194
|
-
};
|
|
7195
|
-
}
|
|
7196
|
-
async function listAllGenerations(markdownOutputDir) {
|
|
7197
|
-
const markdownFiles = await findMarkdownFiles(markdownOutputDir);
|
|
7198
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
7199
|
-
for (const filePath of markdownFiles) {
|
|
7200
|
-
try {
|
|
7201
|
-
const metadata = await extractArticleMetadata(filePath);
|
|
7202
|
-
const identity = deriveOutputIdentity(filePath, markdownOutputDir);
|
|
7203
|
-
const output = {
|
|
7204
|
-
id: `${identity.generationId}:${identity.contentType}:${identity.index}`,
|
|
7205
|
-
generationId: identity.generationId,
|
|
7206
|
-
sourcePath: filePath,
|
|
7207
|
-
slug: metadata.slug,
|
|
7208
|
-
title: metadata.title,
|
|
7209
|
-
previewSnippet: metadata.previewSnippet,
|
|
7210
|
-
coverImageUrl: metadata.coverImageUrl,
|
|
7211
|
-
mtime: metadata.mtime,
|
|
7212
|
-
contentType: identity.contentType,
|
|
7213
|
-
contentTypeLabel: toContentTypeLabel(identity.contentType),
|
|
7214
|
-
index: identity.index
|
|
7215
|
-
};
|
|
7216
|
-
const existing = grouped.get(identity.generationId);
|
|
7217
|
-
if (existing) {
|
|
7218
|
-
existing.push(output);
|
|
7219
|
-
} else {
|
|
7220
|
-
grouped.set(identity.generationId, [output]);
|
|
7542
|
+
modelSettings: {
|
|
7543
|
+
...settings.modelSettings,
|
|
7544
|
+
maxTokens: nextMaxTokens
|
|
7221
7545
|
}
|
|
7222
|
-
}
|
|
7223
|
-
|
|
7546
|
+
});
|
|
7547
|
+
return;
|
|
7224
7548
|
}
|
|
7225
|
-
|
|
7226
|
-
|
|
7227
|
-
|
|
7228
|
-
|
|
7229
|
-
|
|
7230
|
-
|
|
7231
|
-
|
|
7232
|
-
|
|
7233
|
-
const newestMtime = outputs.reduce((latest, output) => Math.max(latest, output.mtime), 0);
|
|
7234
|
-
generations.push({
|
|
7235
|
-
id,
|
|
7236
|
-
title: primary.title,
|
|
7237
|
-
mtime: newestMtime,
|
|
7238
|
-
previewSnippet: primary.previewSnippet,
|
|
7239
|
-
coverImageUrl: primary.coverImageUrl ?? outputs.find((output) => Boolean(output.coverImageUrl))?.coverImageUrl ?? null,
|
|
7240
|
-
primaryContentType,
|
|
7241
|
-
outputs
|
|
7549
|
+
if (action === "topP") {
|
|
7550
|
+
const nextTopP = clampNumber2(parseNumberOrFallback(value2, settings.modelSettings.topP), 0, 1);
|
|
7551
|
+
setSettings({
|
|
7552
|
+
...settings,
|
|
7553
|
+
modelSettings: {
|
|
7554
|
+
...settings.modelSettings,
|
|
7555
|
+
topP: nextTopP
|
|
7556
|
+
}
|
|
7242
7557
|
});
|
|
7558
|
+
return;
|
|
7243
7559
|
}
|
|
7244
|
-
|
|
7245
|
-
|
|
7246
|
-
|
|
7247
|
-
function deriveGenerationId(markdownPath, markdownOutputDir) {
|
|
7248
|
-
const relative = path10.relative(markdownOutputDir, markdownPath);
|
|
7249
|
-
const normalized = relative.split(path10.sep).join("/");
|
|
7250
|
-
if (!normalized || normalized.startsWith("../")) {
|
|
7251
|
-
return path10.basename(markdownPath, ".md");
|
|
7560
|
+
if (action === "markdownOutputDir") {
|
|
7561
|
+
setSettings({ ...settings, markdownOutputDir: value2.trim() || settings.markdownOutputDir });
|
|
7562
|
+
return;
|
|
7252
7563
|
}
|
|
7253
|
-
|
|
7254
|
-
|
|
7255
|
-
return
|
|
7564
|
+
if (action === "assetOutputDir") {
|
|
7565
|
+
setSettings({ ...settings, assetOutputDir: value2.trim() || settings.assetOutputDir });
|
|
7566
|
+
return;
|
|
7256
7567
|
}
|
|
7257
|
-
|
|
7258
|
-
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
|
|
7262
|
-
|
|
7263
|
-
|
|
7264
|
-
|
|
7265
|
-
continue;
|
|
7266
|
-
}
|
|
7267
|
-
let entries;
|
|
7268
|
-
try {
|
|
7269
|
-
entries = await readdir(current, { withFileTypes: true });
|
|
7270
|
-
} catch {
|
|
7271
|
-
continue;
|
|
7568
|
+
if (action.startsWith("t2i:")) {
|
|
7569
|
+
const fieldName = action.slice(4);
|
|
7570
|
+
const parsedValue = coerceT2IFieldValue(settings.t2i.modelId, fieldName, value2);
|
|
7571
|
+
const nextOverrides = { ...settings.t2i.inputOverrides };
|
|
7572
|
+
if (parsedValue === void 0) {
|
|
7573
|
+
delete nextOverrides[fieldName];
|
|
7574
|
+
} else {
|
|
7575
|
+
nextOverrides[fieldName] = parsedValue;
|
|
7272
7576
|
}
|
|
7273
|
-
|
|
7274
|
-
|
|
7275
|
-
|
|
7276
|
-
|
|
7277
|
-
|
|
7278
|
-
}
|
|
7279
|
-
if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
|
|
7280
|
-
files.push(fullPath);
|
|
7577
|
+
setSettings({
|
|
7578
|
+
...settings,
|
|
7579
|
+
t2i: {
|
|
7580
|
+
...settings.t2i,
|
|
7581
|
+
inputOverrides: nextOverrides
|
|
7281
7582
|
}
|
|
7282
|
-
}
|
|
7583
|
+
});
|
|
7283
7584
|
}
|
|
7284
|
-
return files;
|
|
7285
7585
|
}
|
|
7286
|
-
function
|
|
7287
|
-
|
|
7288
|
-
|
|
7289
|
-
const parsed = fileBase.match(/^([a-z0-9-]+)-(\d+)$/i);
|
|
7290
|
-
if (!parsed || !parsed[1] || !parsed[2]) {
|
|
7291
|
-
return {
|
|
7292
|
-
generationId,
|
|
7293
|
-
contentType: "article",
|
|
7294
|
-
index: 1
|
|
7295
|
-
};
|
|
7586
|
+
function formatEditorValue(value2) {
|
|
7587
|
+
if (value2 === null || value2 === void 0) {
|
|
7588
|
+
return "";
|
|
7296
7589
|
}
|
|
7297
|
-
|
|
7298
|
-
const index = Number.parseInt(parsed[2], 10);
|
|
7299
|
-
return {
|
|
7300
|
-
generationId,
|
|
7301
|
-
contentType: FILE_PREFIX_TO_CONTENT_TYPE[prefix] ?? prefix,
|
|
7302
|
-
index: Number.isFinite(index) && index > 0 ? index : 1
|
|
7303
|
-
};
|
|
7590
|
+
return String(value2);
|
|
7304
7591
|
}
|
|
7305
|
-
function
|
|
7306
|
-
|
|
7307
|
-
|
|
7308
|
-
const normalizedLeft = leftIndex === -1 ? Number.MAX_SAFE_INTEGER : leftIndex;
|
|
7309
|
-
const normalizedRight = rightIndex === -1 ? Number.MAX_SAFE_INTEGER : rightIndex;
|
|
7310
|
-
if (normalizedLeft !== normalizedRight) {
|
|
7311
|
-
return normalizedLeft - normalizedRight;
|
|
7592
|
+
function formatValue(value2) {
|
|
7593
|
+
if (value2 === null || value2 === void 0 || value2 === "") {
|
|
7594
|
+
return "(default)";
|
|
7312
7595
|
}
|
|
7313
|
-
return
|
|
7596
|
+
return String(value2);
|
|
7314
7597
|
}
|
|
7315
|
-
function
|
|
7316
|
-
const
|
|
7317
|
-
|
|
7318
|
-
|
|
7598
|
+
function parseNumberOrFallback(value2, fallback) {
|
|
7599
|
+
const parsed = Number(value2.trim());
|
|
7600
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
7601
|
+
}
|
|
7602
|
+
function clampNumber2(value2, minimum, maximum) {
|
|
7603
|
+
return Math.min(maximum, Math.max(minimum, value2));
|
|
7604
|
+
}
|
|
7605
|
+
function parseBooleanOrFallback(value2, fallback) {
|
|
7606
|
+
const normalized = value2.trim().toLowerCase();
|
|
7607
|
+
if (normalized === "true") {
|
|
7608
|
+
return true;
|
|
7319
7609
|
}
|
|
7320
|
-
|
|
7610
|
+
if (normalized === "false") {
|
|
7611
|
+
return false;
|
|
7612
|
+
}
|
|
7613
|
+
return fallback;
|
|
7321
7614
|
}
|
|
7322
|
-
|
|
7323
|
-
|
|
7324
|
-
|
|
7325
|
-
|
|
7326
|
-
|
|
7615
|
+
|
|
7616
|
+
// src/cli/commands/settings.tsx
|
|
7617
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
7618
|
+
async function openSettings() {
|
|
7619
|
+
const envSettings = readEnvSettings();
|
|
7620
|
+
const [settings, secrets] = await Promise.all([
|
|
7621
|
+
loadSavedSettings(),
|
|
7622
|
+
loadSecrets({ disableKeytar: envSettings.disableKeytar })
|
|
7623
|
+
]);
|
|
7624
|
+
let result = null;
|
|
7625
|
+
const app = render(
|
|
7626
|
+
/* @__PURE__ */ jsx2(
|
|
7627
|
+
SettingsFlow,
|
|
7628
|
+
{
|
|
7629
|
+
initialSettings: settings,
|
|
7630
|
+
initialSecrets: secrets,
|
|
7631
|
+
onDone: (value2) => {
|
|
7632
|
+
result = value2;
|
|
7633
|
+
}
|
|
7634
|
+
}
|
|
7635
|
+
)
|
|
7636
|
+
);
|
|
7637
|
+
await app.waitUntilExit();
|
|
7638
|
+
const finalResult = result;
|
|
7639
|
+
if (!finalResult) {
|
|
7640
|
+
console.log("Settings unchanged.");
|
|
7641
|
+
return;
|
|
7327
7642
|
}
|
|
7328
|
-
const
|
|
7643
|
+
const savedResult = finalResult;
|
|
7644
|
+
await saveSettings(savedResult.settings);
|
|
7329
7645
|
try {
|
|
7330
|
-
|
|
7331
|
-
|
|
7332
|
-
|
|
7333
|
-
|
|
7334
|
-
|
|
7335
|
-
return
|
|
7646
|
+
await saveSecrets(savedResult.secrets, { disableKeytar: envSettings.disableKeytar });
|
|
7647
|
+
} catch (error) {
|
|
7648
|
+
if (error instanceof KeytarUnavailableError) {
|
|
7649
|
+
console.log("Settings saved, but secrets were not stored in the system keychain.");
|
|
7650
|
+
console.log("Use IDEON_OPENROUTER_API_KEY and IDEON_REPLICATE_API_TOKEN in this environment.");
|
|
7651
|
+
return;
|
|
7336
7652
|
}
|
|
7337
|
-
|
|
7338
|
-
return fallback;
|
|
7653
|
+
throw error;
|
|
7339
7654
|
}
|
|
7340
|
-
|
|
7655
|
+
console.log(`Settings saved to ${getSettingsFilePath()}.`);
|
|
7341
7656
|
}
|
|
7342
7657
|
|
|
7658
|
+
// src/cli/commands/serve.ts
|
|
7659
|
+
import path13 from "path";
|
|
7660
|
+
import { spawn } from "child_process";
|
|
7661
|
+
|
|
7343
7662
|
// src/server/previewServer.ts
|
|
7344
7663
|
import { execFile } from "child_process";
|
|
7345
7664
|
import { promisify } from "util";
|
|
7346
|
-
import { readFile as
|
|
7665
|
+
import { readFile as readFile9, stat as stat6 } from "fs/promises";
|
|
7347
7666
|
import { watch as fsWatch } from "fs";
|
|
7348
|
-
import
|
|
7667
|
+
import path12 from "path";
|
|
7349
7668
|
import { fileURLToPath } from "url";
|
|
7350
7669
|
import express from "express";
|
|
7351
7670
|
import { marked } from "marked";
|
|
7352
|
-
|
|
7353
|
-
// src/output/enrichMarkdownWithLinks.ts
|
|
7354
|
-
function enrichMarkdownWithLinks(markdown, links) {
|
|
7355
|
-
if (links.length === 0) {
|
|
7356
|
-
return markdown;
|
|
7357
|
-
}
|
|
7358
|
-
const sorted = [...links].sort((left, right) => right.expression.length - left.expression.length);
|
|
7359
|
-
let updated = markdown;
|
|
7360
|
-
for (const link of sorted) {
|
|
7361
|
-
const escapedExpression = escapeRegExp(link.expression);
|
|
7362
|
-
const leadBoundary = /^\w/.test(link.expression) ? "\\b" : "";
|
|
7363
|
-
const trailBoundary = /\w$/.test(link.expression) ? "\\b" : "";
|
|
7364
|
-
const expressionRegex = new RegExp(`${leadBoundary}${escapedExpression}${trailBoundary}`, "g");
|
|
7365
|
-
let match;
|
|
7366
|
-
while ((match = expressionRegex.exec(updated)) !== null) {
|
|
7367
|
-
const start = match.index;
|
|
7368
|
-
const end = start + match[0].length;
|
|
7369
|
-
if (isInProtectedSpan(updated, start, end)) {
|
|
7370
|
-
continue;
|
|
7371
|
-
}
|
|
7372
|
-
updated = `${updated.slice(0, start)}[${match[0]}](${link.url})${updated.slice(end)}`;
|
|
7373
|
-
break;
|
|
7374
|
-
}
|
|
7375
|
-
}
|
|
7376
|
-
return updated;
|
|
7377
|
-
}
|
|
7378
|
-
function isInProtectedSpan(content, start, end) {
|
|
7379
|
-
const lineStart = content.lastIndexOf("\n", start) + 1;
|
|
7380
|
-
const lineEndIdx = content.indexOf("\n", end);
|
|
7381
|
-
const lineEnd = lineEndIdx === -1 ? content.length : lineEndIdx;
|
|
7382
|
-
const line = content.slice(lineStart, lineEnd);
|
|
7383
|
-
const linkPattern = /\[[^\]]*\]\([^)]*\)/g;
|
|
7384
|
-
let m;
|
|
7385
|
-
while ((m = linkPattern.exec(line)) !== null) {
|
|
7386
|
-
const absStart = lineStart + m.index;
|
|
7387
|
-
const absEnd = absStart + m[0].length;
|
|
7388
|
-
if (start >= absStart && end <= absEnd) {
|
|
7389
|
-
return true;
|
|
7390
|
-
}
|
|
7391
|
-
}
|
|
7392
|
-
const codePattern = /`[^`]+`/g;
|
|
7393
|
-
while ((m = codePattern.exec(line)) !== null) {
|
|
7394
|
-
const absStart = lineStart + m.index;
|
|
7395
|
-
const absEnd = absStart + m[0].length;
|
|
7396
|
-
if (start >= absStart && end <= absEnd) {
|
|
7397
|
-
return true;
|
|
7398
|
-
}
|
|
7399
|
-
}
|
|
7400
|
-
return false;
|
|
7401
|
-
}
|
|
7402
|
-
function escapeRegExp(value2) {
|
|
7403
|
-
return value2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7404
|
-
}
|
|
7405
|
-
|
|
7406
|
-
// src/server/previewServer.ts
|
|
7407
7671
|
var execFileAsync = promisify(execFile);
|
|
7408
7672
|
var MissingArticleError = class extends Error {
|
|
7409
7673
|
constructor(message) {
|
|
@@ -7498,7 +7762,7 @@ async function startPreviewServer(options) {
|
|
|
7498
7762
|
if (options.watch) {
|
|
7499
7763
|
let html2;
|
|
7500
7764
|
try {
|
|
7501
|
-
html2 = await
|
|
7765
|
+
html2 = await readFile9(path12.join(previewClientDir, "index.html"), "utf8");
|
|
7502
7766
|
} catch {
|
|
7503
7767
|
res.status(200).type("html").send(
|
|
7504
7768
|
`<!doctype html><html><head><meta charset="utf-8"><title>Rebuilding\u2026</title><style>body{margin:0;display:flex;align-items:center;justify-content:center;height:100vh;font-family:sans-serif;background:#101820;color:#e0eaf0}p{font-size:15px;opacity:.7}</style></head><body><p>Rebuilding\u2026</p><script>const s=new EventSource('/api/__reload');s.onmessage=function(){location.reload()};</script></body></html>`
|
|
@@ -7509,7 +7773,7 @@ async function startPreviewServer(options) {
|
|
|
7509
7773
|
const injected = html2.replace("</body>", `${reloadScript}</body>`);
|
|
7510
7774
|
res.status(200).type("html").send(injected);
|
|
7511
7775
|
} else {
|
|
7512
|
-
res.status(200).sendFile(
|
|
7776
|
+
res.status(200).sendFile(path12.join(previewClientDir, "index.html"));
|
|
7513
7777
|
}
|
|
7514
7778
|
return;
|
|
7515
7779
|
}
|
|
@@ -7562,7 +7826,7 @@ async function getArticleContent(generationId, markdownOutputDir) {
|
|
|
7562
7826
|
generation.outputs.map(async (output) => {
|
|
7563
7827
|
let markdown = "";
|
|
7564
7828
|
try {
|
|
7565
|
-
markdown = await
|
|
7829
|
+
markdown = await readFile9(output.sourcePath, "utf8");
|
|
7566
7830
|
} catch (error) {
|
|
7567
7831
|
if (isMissingFileError(error)) {
|
|
7568
7832
|
throw new MissingArticleError(`Generation "${generationId}" no longer exists.`);
|
|
@@ -7580,7 +7844,7 @@ async function getArticleContent(generationId, markdownOutputDir) {
|
|
|
7580
7844
|
};
|
|
7581
7845
|
})
|
|
7582
7846
|
);
|
|
7583
|
-
const generationDir =
|
|
7847
|
+
const generationDir = path12.dirname(generation.outputs[0]?.sourcePath ?? "");
|
|
7584
7848
|
const interactions = generationDir ? await loadSavedInteractions(generationDir) : { llmCalls: [], t2iCalls: [] };
|
|
7585
7849
|
const analyticsSummary = generationDir ? await loadSavedAnalyticsSummary(generationDir) : null;
|
|
7586
7850
|
return {
|
|
@@ -7609,7 +7873,7 @@ async function resolveActivePreviewArticle(preferredMarkdownPath, markdownOutput
|
|
|
7609
7873
|
};
|
|
7610
7874
|
}
|
|
7611
7875
|
function resolveGenerationSourcePath(generation, markdownOutputDir) {
|
|
7612
|
-
return generation.outputs.find((output) => output.contentType === generation.primaryContentType)?.sourcePath ?? generation.outputs[0]?.sourcePath ??
|
|
7876
|
+
return generation.outputs.find((output) => output.contentType === generation.primaryContentType)?.sourcePath ?? generation.outputs[0]?.sourcePath ?? path12.join(markdownOutputDir, generation.id);
|
|
7613
7877
|
}
|
|
7614
7878
|
function isMissingFileError(error) {
|
|
7615
7879
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
@@ -7624,7 +7888,7 @@ async function renderArticleHtml(markdown, generationId, sourcePath) {
|
|
|
7624
7888
|
async function loadSavedLinks(markdownPath) {
|
|
7625
7889
|
const linksPath = resolveLinksPath(markdownPath);
|
|
7626
7890
|
try {
|
|
7627
|
-
const raw = await
|
|
7891
|
+
const raw = await readFile9(linksPath, "utf8");
|
|
7628
7892
|
const parsed = JSON.parse(raw);
|
|
7629
7893
|
if (!Array.isArray(parsed.links)) {
|
|
7630
7894
|
return [];
|
|
@@ -7648,9 +7912,9 @@ async function loadSavedLinks(markdownPath) {
|
|
|
7648
7912
|
}
|
|
7649
7913
|
}
|
|
7650
7914
|
async function loadSavedInteractions(generationDir) {
|
|
7651
|
-
const interactionsPath =
|
|
7915
|
+
const interactionsPath = path12.join(generationDir, "model.interactions.json");
|
|
7652
7916
|
try {
|
|
7653
|
-
const raw = await
|
|
7917
|
+
const raw = await readFile9(interactionsPath, "utf8");
|
|
7654
7918
|
const parsed = JSON.parse(raw);
|
|
7655
7919
|
const llmCalls = Array.isArray(parsed.llmCalls) ? parsed.llmCalls.filter(isPreviewLlmInteraction) : [];
|
|
7656
7920
|
const t2iCalls = Array.isArray(parsed.t2iCalls) ? parsed.t2iCalls.filter(isPreviewT2IInteraction) : [];
|
|
@@ -7666,9 +7930,9 @@ async function loadSavedInteractions(generationDir) {
|
|
|
7666
7930
|
}
|
|
7667
7931
|
}
|
|
7668
7932
|
async function loadSavedAnalyticsSummary(generationDir) {
|
|
7669
|
-
const analyticsPath =
|
|
7933
|
+
const analyticsPath = path12.join(generationDir, "generation.analytics.json");
|
|
7670
7934
|
try {
|
|
7671
|
-
const raw = await
|
|
7935
|
+
const raw = await readFile9(analyticsPath, "utf8");
|
|
7672
7936
|
const parsed = JSON.parse(raw);
|
|
7673
7937
|
const summary = parsed.summary;
|
|
7674
7938
|
if (!summary || typeof summary !== "object") {
|
|
@@ -7700,14 +7964,14 @@ async function getPreviewBootstrapData(preferredMarkdownPath, markdownOutputDir)
|
|
|
7700
7964
|
};
|
|
7701
7965
|
}
|
|
7702
7966
|
async function resolvePreviewClientBuildDir() {
|
|
7703
|
-
const currentDir =
|
|
7967
|
+
const currentDir = path12.dirname(fileURLToPath(import.meta.url));
|
|
7704
7968
|
const candidates = [
|
|
7705
|
-
|
|
7706
|
-
|
|
7969
|
+
path12.resolve(currentDir, "preview"),
|
|
7970
|
+
path12.resolve(currentDir, "../../dist/preview")
|
|
7707
7971
|
];
|
|
7708
7972
|
for (const candidate of candidates) {
|
|
7709
7973
|
try {
|
|
7710
|
-
const indexStat = await
|
|
7974
|
+
const indexStat = await stat6(path12.join(candidate, "index.html"));
|
|
7711
7975
|
if (indexStat.isFile()) {
|
|
7712
7976
|
return candidate;
|
|
7713
7977
|
}
|
|
@@ -7769,21 +8033,21 @@ async function resolveGenerationAssetPath(generationId, rawAssetPath, markdownOu
|
|
|
7769
8033
|
throw new MissingArticleError(`Generation "${generationId}" no longer exists.`);
|
|
7770
8034
|
}
|
|
7771
8035
|
const decodedAssetPath = decodeURIComponent(rawAssetPath);
|
|
7772
|
-
const normalizedRelative =
|
|
7773
|
-
if (normalizedRelative.length === 0 || normalizedRelative === "." || normalizedRelative.startsWith("../") || normalizedRelative.includes("/../") ||
|
|
8036
|
+
const normalizedRelative = path12.posix.normalize(decodedAssetPath.replace(/\\/g, "/"));
|
|
8037
|
+
if (normalizedRelative.length === 0 || normalizedRelative === "." || normalizedRelative.startsWith("../") || normalizedRelative.includes("/../") || path12.posix.isAbsolute(normalizedRelative)) {
|
|
7774
8038
|
throw new Error("Invalid generation asset path.");
|
|
7775
8039
|
}
|
|
7776
|
-
const generationDir =
|
|
8040
|
+
const generationDir = path12.dirname(generation.outputs[0]?.sourcePath ?? "");
|
|
7777
8041
|
if (!generationDir) {
|
|
7778
8042
|
throw new MissingArticleError(`Generation "${generationId}" has no source directory.`);
|
|
7779
8043
|
}
|
|
7780
|
-
const resolvedPath =
|
|
7781
|
-
const relativeToGeneration =
|
|
7782
|
-
if (relativeToGeneration.startsWith("..") ||
|
|
8044
|
+
const resolvedPath = path12.resolve(generationDir, normalizedRelative);
|
|
8045
|
+
const relativeToGeneration = path12.relative(generationDir, resolvedPath);
|
|
8046
|
+
if (relativeToGeneration.startsWith("..") || path12.isAbsolute(relativeToGeneration)) {
|
|
7783
8047
|
throw new Error("Invalid generation asset path.");
|
|
7784
8048
|
}
|
|
7785
8049
|
try {
|
|
7786
|
-
const fileStat = await
|
|
8050
|
+
const fileStat = await stat6(resolvedPath);
|
|
7787
8051
|
if (!fileStat.isFile()) {
|
|
7788
8052
|
throw new Error("Invalid generation asset path.");
|
|
7789
8053
|
}
|
|
@@ -9263,7 +9527,7 @@ async function runServeCommand(options) {
|
|
|
9263
9527
|
const markdownPath = await resolveMarkdownPath(options.markdownPath, outputPaths.markdownOutputDir, process.cwd());
|
|
9264
9528
|
const port = parsePort(options.port);
|
|
9265
9529
|
if (options.watch) {
|
|
9266
|
-
const viteBin =
|
|
9530
|
+
const viteBin = path13.resolve(process.cwd(), "node_modules", ".bin", "vite");
|
|
9267
9531
|
const viteProcess = spawn(viteBin, ["build", "--watch"], {
|
|
9268
9532
|
stdio: "inherit",
|
|
9269
9533
|
shell: process.platform === "win32"
|
|
@@ -9289,8 +9553,8 @@ async function runServeCommand(options) {
|
|
|
9289
9553
|
openBrowser: options.openBrowser,
|
|
9290
9554
|
watch: options.watch
|
|
9291
9555
|
});
|
|
9292
|
-
const relativeArticle =
|
|
9293
|
-
const relativeAssets =
|
|
9556
|
+
const relativeArticle = path13.relative(process.cwd(), markdownPath);
|
|
9557
|
+
const relativeAssets = path13.relative(process.cwd(), outputPaths.assetOutputDir);
|
|
9294
9558
|
console.log(`Previewing ${relativeArticle || markdownPath}`);
|
|
9295
9559
|
console.log(`Serving assets from ${relativeAssets || outputPaths.assetOutputDir}`);
|
|
9296
9560
|
console.log(`Open ${server.url}`);
|
|
@@ -9776,7 +10040,7 @@ function formatPipelineStageCost(stage) {
|
|
|
9776
10040
|
}
|
|
9777
10041
|
return stage.costSource === "estimated" ? `~${formatted}` : formatted;
|
|
9778
10042
|
}
|
|
9779
|
-
async function renderPlainPipeline(input, dryRun, enrichLinks2, runMode, links, unlinks, maxLinks) {
|
|
10043
|
+
async function renderPlainPipeline(input, dryRun, enrichLinks2, runMode, links, unlinks, maxLinks, maxImages) {
|
|
9780
10044
|
let previousStages = /* @__PURE__ */ new Map();
|
|
9781
10045
|
let previousItemStatuses = /* @__PURE__ */ new Map();
|
|
9782
10046
|
const notificationsEnabled = input.config.settings.notifications.enabled;
|
|
@@ -9793,6 +10057,7 @@ async function renderPlainPipeline(input, dryRun, enrichLinks2, runMode, links,
|
|
|
9793
10057
|
customLinks: links,
|
|
9794
10058
|
unlinks,
|
|
9795
10059
|
maxLinks,
|
|
10060
|
+
maxImages,
|
|
9796
10061
|
onUpdate(stages) {
|
|
9797
10062
|
for (const stage of stages) {
|
|
9798
10063
|
const previous = previousStages.get(stage.id);
|
|
@@ -10154,6 +10419,7 @@ function WriteApp({
|
|
|
10154
10419
|
links,
|
|
10155
10420
|
unlinks,
|
|
10156
10421
|
maxLinks,
|
|
10422
|
+
maxImages,
|
|
10157
10423
|
onError
|
|
10158
10424
|
}) {
|
|
10159
10425
|
const { exit } = useApp3();
|
|
@@ -10180,6 +10446,7 @@ function WriteApp({
|
|
|
10180
10446
|
customLinks: links,
|
|
10181
10447
|
unlinks,
|
|
10182
10448
|
maxLinks,
|
|
10449
|
+
maxImages,
|
|
10183
10450
|
onUpdate(nextStages) {
|
|
10184
10451
|
if (mounted) {
|
|
10185
10452
|
setStages(nextStages);
|
|
@@ -10212,7 +10479,7 @@ function WriteApp({
|
|
|
10212
10479
|
return () => {
|
|
10213
10480
|
mounted = false;
|
|
10214
10481
|
};
|
|
10215
|
-
}, [dryRun, enrichLinks2, input, links, unlinks, maxLinks, onError, runMode]);
|
|
10482
|
+
}, [dryRun, enrichLinks2, input, links, unlinks, maxLinks, maxImages, onError, runMode]);
|
|
10216
10483
|
useEffect2(() => {
|
|
10217
10484
|
if (!result && !errorMessage) {
|
|
10218
10485
|
return;
|
|
@@ -10228,7 +10495,7 @@ function WriteApp({
|
|
|
10228
10495
|
}
|
|
10229
10496
|
async function runWriteCommand(options) {
|
|
10230
10497
|
const input = await resolveInputWithInteractiveIdeaFallback(options);
|
|
10231
|
-
await runWritePipeline(input, options.dryRun, options.enrichLinks, "fresh", options.noInteractive, options.links, options.unlinks, options.maxLinks);
|
|
10498
|
+
await runWritePipeline(input, options.dryRun, options.enrichLinks, "fresh", options.noInteractive, options.links, options.unlinks, options.maxLinks, options.maxImages);
|
|
10232
10499
|
}
|
|
10233
10500
|
async function runWriteResumeCommand(options = {}) {
|
|
10234
10501
|
const session = await loadWriteSession();
|
|
@@ -10250,9 +10517,9 @@ async function runWriteResumeCommand(options = {}) {
|
|
|
10250
10517
|
secrets: resolved.config.secrets
|
|
10251
10518
|
}
|
|
10252
10519
|
};
|
|
10253
|
-
await runWritePipeline(input, session.dryRun, options.enrichLinks ?? false, "resume", options.noInteractive ?? false, options.links, options.unlinks, options.maxLinks);
|
|
10520
|
+
await runWritePipeline(input, session.dryRun, options.enrichLinks ?? false, "resume", options.noInteractive ?? false, options.links, options.unlinks, options.maxLinks, options.maxImages);
|
|
10254
10521
|
}
|
|
10255
|
-
async function runWritePipeline(input, dryRun, enrichLinks2, runMode, noInteractive, links, unlinks, maxLinks) {
|
|
10522
|
+
async function runWritePipeline(input, dryRun, enrichLinks2, runMode, noInteractive, links, unlinks, maxLinks, maxImages) {
|
|
10256
10523
|
let interruptHandled = false;
|
|
10257
10524
|
const handleSignal = (signal) => {
|
|
10258
10525
|
if (interruptHandled) {
|
|
@@ -10286,7 +10553,7 @@ async function runWritePipeline(input, dryRun, enrichLinks2, runMode, noInteract
|
|
|
10286
10553
|
process.on("SIGTERM", onSigterm);
|
|
10287
10554
|
try {
|
|
10288
10555
|
if (noInteractive || !process.stdout.isTTY) {
|
|
10289
|
-
await renderPlainPipeline(input, dryRun, enrichLinks2, runMode, links, unlinks, maxLinks);
|
|
10556
|
+
await renderPlainPipeline(input, dryRun, enrichLinks2, runMode, links, unlinks, maxLinks, maxImages);
|
|
10290
10557
|
return;
|
|
10291
10558
|
}
|
|
10292
10559
|
let commandError = null;
|
|
@@ -10301,6 +10568,7 @@ async function runWritePipeline(input, dryRun, enrichLinks2, runMode, noInteract
|
|
|
10301
10568
|
links,
|
|
10302
10569
|
unlinks,
|
|
10303
10570
|
maxLinks,
|
|
10571
|
+
maxImages,
|
|
10304
10572
|
onError: (error) => {
|
|
10305
10573
|
commandError = error;
|
|
10306
10574
|
}
|
|
@@ -10468,7 +10736,7 @@ function collectOptionValue(value2, previous = []) {
|
|
|
10468
10736
|
}
|
|
10469
10737
|
async function runCli(argv) {
|
|
10470
10738
|
const program = new Command();
|
|
10471
|
-
program.name("ideon").description("Turn
|
|
10739
|
+
program.name("ideon").description("Turn one idea into articles, threads, and social posts \u2014 quality content without the token tax.").version(version);
|
|
10472
10740
|
program.command("settings").description("Show the current Ideon settings and storage state.").action(async () => {
|
|
10473
10741
|
await openSettings();
|
|
10474
10742
|
});
|
|
@@ -10513,6 +10781,14 @@ async function runCli(argv) {
|
|
|
10513
10781
|
maxLinks: options.maxLinks
|
|
10514
10782
|
});
|
|
10515
10783
|
});
|
|
10784
|
+
program.command("export").description("Export a generated article as a standalone markdown file with inline links and copied images.").argument("<generationId>", "Generation id or article slug to export").argument("<path>", "Destination directory for the exported file and images").option("--index <n>", "Which primary article variant to export when multiple exist (default: 1)", (v) => Number.parseInt(v, 10)).option("--overwrite", "Overwrite the destination file if it already exists", false).action(async (generationId, destinationPath, options) => {
|
|
10785
|
+
await runOutputCommand({
|
|
10786
|
+
generationId,
|
|
10787
|
+
destinationPath,
|
|
10788
|
+
index: options.index,
|
|
10789
|
+
overwrite: options.overwrite
|
|
10790
|
+
});
|
|
10791
|
+
});
|
|
10516
10792
|
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) => {
|
|
10517
10793
|
await runServeCommand({
|
|
10518
10794
|
markdownPath,
|
|
@@ -10521,7 +10797,7 @@ async function runCli(argv) {
|
|
|
10521
10797
|
watch: options.watch
|
|
10522
10798
|
});
|
|
10523
10799
|
});
|
|
10524
|
-
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) => {
|
|
10800
|
+
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)).option("--max-images <n>", "Max total images including cover (1=cover only, 2=cover+1 inline, 3=cover+2 inline)", (v) => Number.parseInt(v, 10)).action(async (ideaArg, options) => {
|
|
10525
10801
|
await runWriteCommand({
|
|
10526
10802
|
idea: options.idea ?? ideaArg,
|
|
10527
10803
|
audience: options.audience,
|
|
@@ -10536,16 +10812,18 @@ async function runCli(argv) {
|
|
|
10536
10812
|
enrichLinks: options.enrichLinks,
|
|
10537
10813
|
links: options.link,
|
|
10538
10814
|
unlinks: options.unlink,
|
|
10539
|
-
maxLinks: options.maxLinks
|
|
10815
|
+
maxLinks: options.maxLinks,
|
|
10816
|
+
maxImages: options.maxImages
|
|
10540
10817
|
});
|
|
10541
10818
|
});
|
|
10542
|
-
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) => {
|
|
10819
|
+
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)).option("--max-images <n>", "Max total images including cover (1=cover only, 2=cover+1 inline, 3=cover+2 inline)", (v) => Number.parseInt(v, 10)).action(async (options) => {
|
|
10543
10820
|
await runWriteResumeCommand({
|
|
10544
10821
|
noInteractive: options.noInteractive,
|
|
10545
10822
|
enrichLinks: options.enrichLinks,
|
|
10546
10823
|
links: options.link,
|
|
10547
10824
|
unlinks: options.unlink,
|
|
10548
|
-
maxLinks: options.maxLinks
|
|
10825
|
+
maxLinks: options.maxLinks,
|
|
10826
|
+
maxImages: options.maxImages
|
|
10549
10827
|
});
|
|
10550
10828
|
});
|
|
10551
10829
|
await program.parseAsync(argv);
|