@telepat/ideon 0.1.15 → 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 +14 -14
- package/README.zh-CN.md +14 -14
- package/dist/ideon.js +871 -599
- 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({})),
|
|
@@ -521,7 +521,7 @@ import { mkdir as mkdir3, readFile as readFile2, writeFile as writeFile3 } from
|
|
|
521
521
|
import path4 from "path";
|
|
522
522
|
import envPaths2 from "env-paths";
|
|
523
523
|
import { z as z2 } from "zod";
|
|
524
|
-
var supportedAgentRuntimeValues = ["claude", "chatgpt", "gemini", "generic-mcp"];
|
|
524
|
+
var supportedAgentRuntimeValues = ["claude", "claude-desktop", "chatgpt", "gemini", "codex", "cursor", "vscode", "opencode", "generic-mcp"];
|
|
525
525
|
var integrationEntrySchema = z2.object({
|
|
526
526
|
runtime: z2.enum(supportedAgentRuntimeValues),
|
|
527
527
|
installedAt: z2.string(),
|
|
@@ -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"],
|
|
@@ -1208,7 +1222,6 @@ function compareStringArrays(drifts, id, actual, expected) {
|
|
|
1208
1222
|
}
|
|
1209
1223
|
|
|
1210
1224
|
// src/cli/commands/agent.ts
|
|
1211
|
-
var unsupportedIdeRuntimeAliases = ["cursor", "vscode"];
|
|
1212
1225
|
var defaultDependencies = {
|
|
1213
1226
|
install: installAgentIntegration,
|
|
1214
1227
|
uninstall: uninstallAgentIntegration,
|
|
@@ -1278,11 +1291,6 @@ async function collectAgentStatus(deps) {
|
|
|
1278
1291
|
}
|
|
1279
1292
|
function parseRuntime(rawRuntime) {
|
|
1280
1293
|
const runtime = rawRuntime.trim().toLowerCase();
|
|
1281
|
-
if (unsupportedIdeRuntimeAliases.includes(runtime)) {
|
|
1282
|
-
throw new ReportedError(
|
|
1283
|
-
`Unsupported runtime "${rawRuntime}". Ideon agent integration does not support Cursor or VS Code runtimes.`
|
|
1284
|
-
);
|
|
1285
|
-
}
|
|
1286
1294
|
if (!supportedAgentRuntimeValues.includes(runtime)) {
|
|
1287
1295
|
throw new ReportedError(
|
|
1288
1296
|
`Unsupported runtime "${rawRuntime}". Supported runtimes: ${supportedAgentRuntimeValues.join(", ")}.`
|
|
@@ -1354,7 +1362,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
1354
1362
|
// package.json
|
|
1355
1363
|
var package_default = {
|
|
1356
1364
|
name: "@telepat/ideon",
|
|
1357
|
-
version: "0.1.
|
|
1365
|
+
version: "0.1.18",
|
|
1358
1366
|
description: "CLI for generating rich articles and images from ideas.",
|
|
1359
1367
|
type: "module",
|
|
1360
1368
|
repository: {
|
|
@@ -2275,9 +2283,8 @@ function buildArticlePlanJsonSchema(targetLengthWords) {
|
|
|
2275
2283
|
items: {
|
|
2276
2284
|
type: "object",
|
|
2277
2285
|
additionalProperties: false,
|
|
2278
|
-
required: ["
|
|
2286
|
+
required: ["description"],
|
|
2279
2287
|
properties: {
|
|
2280
|
-
anchorAfterSection: { type: "integer", minimum: 1, maximum: 10 },
|
|
2281
2288
|
description: { type: "string" }
|
|
2282
2289
|
}
|
|
2283
2290
|
}
|
|
@@ -2316,7 +2323,7 @@ function buildArticlePlanMessages(idea, options) {
|
|
|
2316
2323
|
"- Sections are article-only structure and must not be treated as requirements for non-article channels.",
|
|
2317
2324
|
"- Include a cover image description and 2 to 3 inline image descriptions.",
|
|
2318
2325
|
"- Image descriptions must be concrete and contextual, not generic stock-photo phrasing.",
|
|
2319
|
-
"-
|
|
2326
|
+
"- Do not include section placement for inline images \u2014 placement is determined automatically after writing.",
|
|
2320
2327
|
"",
|
|
2321
2328
|
"Shared content brief context:",
|
|
2322
2329
|
`- description: ${options.contentBrief.description}`,
|
|
@@ -2335,7 +2342,7 @@ function buildArticlePlanMessages(idea, options) {
|
|
|
2335
2342
|
"- outroBrief: string",
|
|
2336
2343
|
`- sections: array of ${sectionCounts.label} objects, each with title and description strings`,
|
|
2337
2344
|
"- coverImageDescription: string",
|
|
2338
|
-
"- inlineImages: array of 2 to 3 objects, each with
|
|
2345
|
+
"- inlineImages: array of 2 to 3 objects, each with a description string only",
|
|
2339
2346
|
"",
|
|
2340
2347
|
"Do not omit any required fields. Return strict JSON only."
|
|
2341
2348
|
].join("\n")
|
|
@@ -2350,7 +2357,6 @@ var articleSectionPlanSchema = z5.object({
|
|
|
2350
2357
|
description: z5.string().min(1)
|
|
2351
2358
|
});
|
|
2352
2359
|
var inlineImagePlanSchema = z5.object({
|
|
2353
|
-
anchorAfterSection: z5.number().int().min(1).max(10),
|
|
2354
2360
|
description: z5.string().min(1)
|
|
2355
2361
|
});
|
|
2356
2362
|
var articlePlanSchema = z5.object({
|
|
@@ -2406,7 +2412,7 @@ async function planArticle({
|
|
|
2406
2412
|
...basePlan,
|
|
2407
2413
|
slug: uniqueSlug,
|
|
2408
2414
|
keywords: basePlan.keywords.slice(0, 8),
|
|
2409
|
-
inlineImages: basePlan.inlineImages.
|
|
2415
|
+
inlineImages: basePlan.inlineImages.slice(0, 3)
|
|
2410
2416
|
};
|
|
2411
2417
|
}
|
|
2412
2418
|
function buildDryRunPlan(idea, contentBrief) {
|
|
@@ -2444,11 +2450,9 @@ function buildDryRunPlan(idea, contentBrief) {
|
|
|
2444
2450
|
coverImageDescription: "A refined editorial workspace with notebooks, sketches, and glowing structured outlines, cinematic but minimal.",
|
|
2445
2451
|
inlineImages: [
|
|
2446
2452
|
{
|
|
2447
|
-
anchorAfterSection: 1,
|
|
2448
2453
|
description: "A rough idea evolving into a structured article outline on a desk full of notes."
|
|
2449
2454
|
},
|
|
2450
2455
|
{
|
|
2451
|
-
anchorAfterSection: 3,
|
|
2452
2456
|
description: "A collaborative editorial scene where human judgment and AI suggestions coexist."
|
|
2453
2457
|
}
|
|
2454
2458
|
]
|
|
@@ -2943,7 +2947,21 @@ var imagePromptSchema = {
|
|
|
2943
2947
|
prompt: { type: "string" }
|
|
2944
2948
|
}
|
|
2945
2949
|
};
|
|
2946
|
-
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.");
|
|
2947
2965
|
return [
|
|
2948
2966
|
{
|
|
2949
2967
|
role: "system",
|
|
@@ -2951,14 +2969,7 @@ function buildImagePromptMessages(plan, image) {
|
|
|
2951
2969
|
},
|
|
2952
2970
|
{
|
|
2953
2971
|
role: "user",
|
|
2954
|
-
content:
|
|
2955
|
-
`Article title: ${plan.title}`,
|
|
2956
|
-
`Article subtitle: ${plan.subtitle}`,
|
|
2957
|
-
`Article description: ${plan.description}`,
|
|
2958
|
-
`Image kind: ${image.kind}`,
|
|
2959
|
-
`Image description: ${image.description}`,
|
|
2960
|
-
"Write one strong prompt for a clean editorial illustration. Avoid text overlays or watermarks."
|
|
2961
|
-
].join("\n")
|
|
2972
|
+
content: userLines.join("\n")
|
|
2962
2973
|
}
|
|
2963
2974
|
];
|
|
2964
2975
|
}
|
|
@@ -3428,11 +3439,12 @@ function clampNumber(value2, minimum, maximum, integer, nullable) {
|
|
|
3428
3439
|
// src/pipeline/analytics.ts
|
|
3429
3440
|
var LLM_USD_PER_1K_TOKENS = {
|
|
3430
3441
|
// AUTO-GENERATED:OPENROUTER_PRICING_START
|
|
3431
|
-
// Last refreshed: 2026-
|
|
3442
|
+
// Last refreshed: 2026-05-05
|
|
3432
3443
|
// Source: https://openrouter.ai/api/v1/models (per-token USD converted to per-1k-token USD)
|
|
3433
3444
|
"anthropic/claude-3.5-sonnet": { input: 6e-3, output: 0.03 },
|
|
3434
3445
|
"deepseek/deepseek-chat": { input: 32e-5, output: 89e-5 },
|
|
3435
|
-
"
|
|
3446
|
+
"deepseek/deepseek-v4-pro": { input: 435e-6, output: 87e-5 },
|
|
3447
|
+
"moonshotai/kimi-k2.5": { input: 44e-5, output: 2e-3 },
|
|
3436
3448
|
"openai/gpt-4o-mini": { input: 15e-5, output: 6e-4 }
|
|
3437
3449
|
// AUTO-GENERATED:OPENROUTER_PRICING_END
|
|
3438
3450
|
};
|
|
@@ -3520,8 +3532,52 @@ function sumKnownCosts(values) {
|
|
|
3520
3532
|
|
|
3521
3533
|
// src/images/renderImages.ts
|
|
3522
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
|
+
}
|
|
3523
3577
|
async function expandImagePrompts({
|
|
3524
|
-
|
|
3578
|
+
slots,
|
|
3579
|
+
planContext,
|
|
3580
|
+
sections,
|
|
3525
3581
|
settings,
|
|
3526
3582
|
openRouter,
|
|
3527
3583
|
dryRun,
|
|
@@ -3529,11 +3585,11 @@ async function expandImagePrompts({
|
|
|
3529
3585
|
onPromptComplete,
|
|
3530
3586
|
onInteraction
|
|
3531
3587
|
}) {
|
|
3532
|
-
const imageSlots = buildImageSlots(plan);
|
|
3533
3588
|
const prompts = [];
|
|
3534
|
-
for (let index = 0; index <
|
|
3535
|
-
const image =
|
|
3536
|
-
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;
|
|
3537
3593
|
if (dryRun || !openRouter) {
|
|
3538
3594
|
const dryRunStartMs = Date.now();
|
|
3539
3595
|
prompts.push({
|
|
@@ -3560,7 +3616,7 @@ async function expandImagePrompts({
|
|
|
3560
3616
|
const response = await openRouter.requestStructured({
|
|
3561
3617
|
schemaName: "image_prompt",
|
|
3562
3618
|
schema: imagePromptSchema,
|
|
3563
|
-
messages: buildImagePromptMessages(
|
|
3619
|
+
messages: buildImagePromptMessages(planContext, image, sectionForImage),
|
|
3564
3620
|
settings,
|
|
3565
3621
|
interactionContext: {
|
|
3566
3622
|
stageId: "image-prompts",
|
|
@@ -3776,24 +3832,6 @@ function mergeLlmMetrics(left, right) {
|
|
|
3776
3832
|
}
|
|
3777
3833
|
};
|
|
3778
3834
|
}
|
|
3779
|
-
function buildImageSlots(plan) {
|
|
3780
|
-
return [
|
|
3781
|
-
{
|
|
3782
|
-
id: "cover",
|
|
3783
|
-
kind: "cover",
|
|
3784
|
-
prompt: "",
|
|
3785
|
-
description: plan.coverImageDescription,
|
|
3786
|
-
anchorAfterSection: null
|
|
3787
|
-
},
|
|
3788
|
-
...plan.inlineImages.map((image, index) => ({
|
|
3789
|
-
id: `inline-${index + 1}`,
|
|
3790
|
-
kind: "inline",
|
|
3791
|
-
prompt: "",
|
|
3792
|
-
description: image.description,
|
|
3793
|
-
anchorAfterSection: image.anchorAfterSection
|
|
3794
|
-
}))
|
|
3795
|
-
];
|
|
3796
|
-
}
|
|
3797
3835
|
function createReplicateInput(settings, prompt, kind) {
|
|
3798
3836
|
const model = getT2IModel(settings.t2i.modelId);
|
|
3799
3837
|
const overrides = sanitizeT2IOverrides(settings.t2i.modelId, settings.t2i.inputOverrides);
|
|
@@ -4951,7 +4989,9 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4951
4989
|
options.onUpdate?.(cloneStages(stages));
|
|
4952
4990
|
} else {
|
|
4953
4991
|
imagePrompts = await expandImagePrompts({
|
|
4954
|
-
plan,
|
|
4992
|
+
slots: selectImageSlots(plan, text.sections, { maxImages: options.maxImages }),
|
|
4993
|
+
planContext: plan,
|
|
4994
|
+
sections: text.sections,
|
|
4955
4995
|
settings: input.config.settings,
|
|
4956
4996
|
openRouter,
|
|
4957
4997
|
dryRun,
|
|
@@ -6316,98 +6356,590 @@ function resolveCustomLinks(existing, addRaw, removeExpressions) {
|
|
|
6316
6356
|
return Array.from(result.values());
|
|
6317
6357
|
}
|
|
6318
6358
|
|
|
6319
|
-
// src/cli/commands/
|
|
6320
|
-
|
|
6321
|
-
|
|
6322
|
-
|
|
6323
|
-
|
|
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;
|
|
6324
6367
|
}
|
|
6325
|
-
const [
|
|
6326
|
-
|
|
6327
|
-
|
|
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
|
+
}
|
|
6328
6385
|
}
|
|
6329
|
-
|
|
6330
|
-
|
|
6331
|
-
|
|
6332
|
-
|
|
6333
|
-
|
|
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
|
+
}
|
|
6334
6401
|
}
|
|
6335
|
-
const
|
|
6336
|
-
|
|
6337
|
-
|
|
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
|
+
}
|
|
6338
6409
|
}
|
|
6339
|
-
return
|
|
6340
|
-
contentType,
|
|
6341
|
-
count
|
|
6342
|
-
};
|
|
6410
|
+
return false;
|
|
6343
6411
|
}
|
|
6344
|
-
function
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
|
|
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;
|
|
6348
6443
|
}
|
|
6349
|
-
|
|
6350
|
-
|
|
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.`);
|
|
6351
6447
|
}
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
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;
|
|
6355
6458
|
}
|
|
6356
|
-
const
|
|
6357
|
-
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
|
|
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}`);
|
|
6363
6479
|
}
|
|
6364
|
-
|
|
6365
|
-
|
|
6366
|
-
|
|
6367
|
-
|
|
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;
|
|
6368
6499
|
}
|
|
6369
|
-
secondaryDedupedByType.set(parsed.contentType, {
|
|
6370
|
-
...parsed,
|
|
6371
|
-
role: "secondary"
|
|
6372
|
-
});
|
|
6373
6500
|
}
|
|
6374
|
-
return
|
|
6375
|
-
{
|
|
6376
|
-
...primary,
|
|
6377
|
-
role: "primary"
|
|
6378
|
-
},
|
|
6379
|
-
...secondaryDedupedByType.values()
|
|
6380
|
-
];
|
|
6501
|
+
return latestPath;
|
|
6381
6502
|
}
|
|
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
|
-
|
|
6408
|
-
|
|
6409
|
-
|
|
6410
|
-
|
|
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
|
+
});
|
|
6411
6943
|
const run = await runPipelineShell(resolved, {
|
|
6412
6944
|
workingDir: cwd(),
|
|
6413
6945
|
runMode: "fresh",
|
|
@@ -6415,7 +6947,8 @@ async function startIdeonMcpServer() {
|
|
|
6415
6947
|
enrichLinks: input.enrichLinks ?? false,
|
|
6416
6948
|
customLinks: input.link,
|
|
6417
6949
|
unlinks: input.unlink,
|
|
6418
|
-
maxLinks: input.maxLinks
|
|
6950
|
+
maxLinks: input.maxLinks,
|
|
6951
|
+
maxImages: input.maxImages
|
|
6419
6952
|
});
|
|
6420
6953
|
return {
|
|
6421
6954
|
content: [
|
|
@@ -6474,7 +7007,8 @@ async function startIdeonMcpServer() {
|
|
|
6474
7007
|
enrichLinks: input.enrichLinks ?? false,
|
|
6475
7008
|
customLinks: input.link,
|
|
6476
7009
|
unlinks: input.unlink,
|
|
6477
|
-
maxLinks: input.maxLinks
|
|
7010
|
+
maxLinks: input.maxLinks,
|
|
7011
|
+
maxImages: input.maxImages
|
|
6478
7012
|
});
|
|
6479
7013
|
return {
|
|
6480
7014
|
content: [
|
|
@@ -6575,6 +7109,50 @@ async function startIdeonMcpServer() {
|
|
|
6575
7109
|
}
|
|
6576
7110
|
}
|
|
6577
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
|
+
);
|
|
6578
7156
|
server.registerTool(
|
|
6579
7157
|
"ideon_config_get",
|
|
6580
7158
|
{
|
|
@@ -6940,476 +7518,156 @@ function applyEdit(action, value2, settings, secrets, setSettings, setSecrets) {
|
|
|
6940
7518
|
setSettings({
|
|
6941
7519
|
...settings,
|
|
6942
7520
|
notifications: {
|
|
6943
|
-
...settings.notifications,
|
|
6944
|
-
enabled: parsed
|
|
6945
|
-
}
|
|
6946
|
-
});
|
|
6947
|
-
return;
|
|
6948
|
-
}
|
|
6949
|
-
if (action === "temperature") {
|
|
6950
|
-
const nextTemperature = clampNumber2(parseNumberOrFallback(value2, settings.modelSettings.temperature), 0, 2);
|
|
6951
|
-
setSettings({
|
|
6952
|
-
...settings,
|
|
6953
|
-
modelSettings: {
|
|
6954
|
-
...settings.modelSettings,
|
|
6955
|
-
temperature: nextTemperature
|
|
6956
|
-
}
|
|
6957
|
-
});
|
|
6958
|
-
return;
|
|
6959
|
-
}
|
|
6960
|
-
if (action === "maxTokens") {
|
|
6961
|
-
const nextMaxTokens = Math.max(1, Math.round(parseNumberOrFallback(value2, settings.modelSettings.maxTokens)));
|
|
6962
|
-
setSettings({
|
|
6963
|
-
...settings,
|
|
6964
|
-
modelSettings: {
|
|
6965
|
-
...settings.modelSettings,
|
|
6966
|
-
maxTokens: nextMaxTokens
|
|
7521
|
+
...settings.notifications,
|
|
7522
|
+
enabled: parsed
|
|
6967
7523
|
}
|
|
6968
7524
|
});
|
|
6969
7525
|
return;
|
|
6970
7526
|
}
|
|
6971
|
-
if (action === "
|
|
6972
|
-
const
|
|
7527
|
+
if (action === "temperature") {
|
|
7528
|
+
const nextTemperature = clampNumber2(parseNumberOrFallback(value2, settings.modelSettings.temperature), 0, 2);
|
|
6973
7529
|
setSettings({
|
|
6974
7530
|
...settings,
|
|
6975
7531
|
modelSettings: {
|
|
6976
7532
|
...settings.modelSettings,
|
|
6977
|
-
|
|
7533
|
+
temperature: nextTemperature
|
|
6978
7534
|
}
|
|
6979
7535
|
});
|
|
6980
7536
|
return;
|
|
6981
7537
|
}
|
|
6982
|
-
if (action === "
|
|
6983
|
-
|
|
6984
|
-
return;
|
|
6985
|
-
}
|
|
6986
|
-
if (action === "assetOutputDir") {
|
|
6987
|
-
setSettings({ ...settings, assetOutputDir: value2.trim() || settings.assetOutputDir });
|
|
6988
|
-
return;
|
|
6989
|
-
}
|
|
6990
|
-
if (action.startsWith("t2i:")) {
|
|
6991
|
-
const fieldName = action.slice(4);
|
|
6992
|
-
const parsedValue = coerceT2IFieldValue(settings.t2i.modelId, fieldName, value2);
|
|
6993
|
-
const nextOverrides = { ...settings.t2i.inputOverrides };
|
|
6994
|
-
if (parsedValue === void 0) {
|
|
6995
|
-
delete nextOverrides[fieldName];
|
|
6996
|
-
} else {
|
|
6997
|
-
nextOverrides[fieldName] = parsedValue;
|
|
6998
|
-
}
|
|
7538
|
+
if (action === "maxTokens") {
|
|
7539
|
+
const nextMaxTokens = Math.max(1, Math.round(parseNumberOrFallback(value2, settings.modelSettings.maxTokens)));
|
|
6999
7540
|
setSettings({
|
|
7000
7541
|
...settings,
|
|
7001
|
-
|
|
7002
|
-
...settings.
|
|
7003
|
-
|
|
7004
|
-
}
|
|
7005
|
-
});
|
|
7006
|
-
}
|
|
7007
|
-
}
|
|
7008
|
-
function formatEditorValue(value2) {
|
|
7009
|
-
if (value2 === null || value2 === void 0) {
|
|
7010
|
-
return "";
|
|
7011
|
-
}
|
|
7012
|
-
return String(value2);
|
|
7013
|
-
}
|
|
7014
|
-
function formatValue(value2) {
|
|
7015
|
-
if (value2 === null || value2 === void 0 || value2 === "") {
|
|
7016
|
-
return "(default)";
|
|
7017
|
-
}
|
|
7018
|
-
return String(value2);
|
|
7019
|
-
}
|
|
7020
|
-
function parseNumberOrFallback(value2, fallback) {
|
|
7021
|
-
const parsed = Number(value2.trim());
|
|
7022
|
-
return Number.isFinite(parsed) ? parsed : fallback;
|
|
7023
|
-
}
|
|
7024
|
-
function clampNumber2(value2, minimum, maximum) {
|
|
7025
|
-
return Math.min(maximum, Math.max(minimum, value2));
|
|
7026
|
-
}
|
|
7027
|
-
function parseBooleanOrFallback(value2, fallback) {
|
|
7028
|
-
const normalized = value2.trim().toLowerCase();
|
|
7029
|
-
if (normalized === "true") {
|
|
7030
|
-
return true;
|
|
7031
|
-
}
|
|
7032
|
-
if (normalized === "false") {
|
|
7033
|
-
return false;
|
|
7034
|
-
}
|
|
7035
|
-
return fallback;
|
|
7036
|
-
}
|
|
7037
|
-
|
|
7038
|
-
// src/cli/commands/settings.tsx
|
|
7039
|
-
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
7040
|
-
async function openSettings() {
|
|
7041
|
-
const envSettings = readEnvSettings();
|
|
7042
|
-
const [settings, secrets] = await Promise.all([
|
|
7043
|
-
loadSavedSettings(),
|
|
7044
|
-
loadSecrets({ disableKeytar: envSettings.disableKeytar })
|
|
7045
|
-
]);
|
|
7046
|
-
let result = null;
|
|
7047
|
-
const app = render(
|
|
7048
|
-
/* @__PURE__ */ jsx2(
|
|
7049
|
-
SettingsFlow,
|
|
7050
|
-
{
|
|
7051
|
-
initialSettings: settings,
|
|
7052
|
-
initialSecrets: secrets,
|
|
7053
|
-
onDone: (value2) => {
|
|
7054
|
-
result = value2;
|
|
7055
|
-
}
|
|
7056
|
-
}
|
|
7057
|
-
)
|
|
7058
|
-
);
|
|
7059
|
-
await app.waitUntilExit();
|
|
7060
|
-
const finalResult = result;
|
|
7061
|
-
if (!finalResult) {
|
|
7062
|
-
console.log("Settings unchanged.");
|
|
7063
|
-
return;
|
|
7064
|
-
}
|
|
7065
|
-
const savedResult = finalResult;
|
|
7066
|
-
await saveSettings(savedResult.settings);
|
|
7067
|
-
try {
|
|
7068
|
-
await saveSecrets(savedResult.secrets, { disableKeytar: envSettings.disableKeytar });
|
|
7069
|
-
} catch (error) {
|
|
7070
|
-
if (error instanceof KeytarUnavailableError) {
|
|
7071
|
-
console.log("Settings saved, but secrets were not stored in the system keychain.");
|
|
7072
|
-
console.log("Use IDEON_OPENROUTER_API_KEY and IDEON_REPLICATE_API_TOKEN in this environment.");
|
|
7073
|
-
return;
|
|
7074
|
-
}
|
|
7075
|
-
throw error;
|
|
7076
|
-
}
|
|
7077
|
-
console.log(`Settings saved to ${getSettingsFilePath()}.`);
|
|
7078
|
-
}
|
|
7079
|
-
|
|
7080
|
-
// src/cli/commands/serve.ts
|
|
7081
|
-
import path12 from "path";
|
|
7082
|
-
import { spawn } from "child_process";
|
|
7083
|
-
|
|
7084
|
-
// src/server/previewHelpers.ts
|
|
7085
|
-
import { readdir, stat as stat4, readFile as readFile7 } from "fs/promises";
|
|
7086
|
-
import path10 from "path";
|
|
7087
|
-
var DEFAULT_PORT = 4173;
|
|
7088
|
-
var CONTENT_TYPE_ORDER = ["article", "blog-post", "x-thread", "x-post", "linkedin-post", "reddit-post", "newsletter"];
|
|
7089
|
-
var FILE_PREFIX_TO_CONTENT_TYPE = {
|
|
7090
|
-
article: "article",
|
|
7091
|
-
blog: "blog-post",
|
|
7092
|
-
"x-thread": "x-thread",
|
|
7093
|
-
"x-post": "x-post",
|
|
7094
|
-
x: "x-post",
|
|
7095
|
-
reddit: "reddit-post",
|
|
7096
|
-
linkedin: "linkedin-post",
|
|
7097
|
-
newsletter: "newsletter"
|
|
7098
|
-
};
|
|
7099
|
-
var CONTENT_TYPE_LABELS = {
|
|
7100
|
-
article: "Article",
|
|
7101
|
-
"blog-post": "Blog Post",
|
|
7102
|
-
"x-thread": "X Thread",
|
|
7103
|
-
"x-post": "X Post",
|
|
7104
|
-
"reddit-post": "Reddit Post",
|
|
7105
|
-
"linkedin-post": "LinkedIn Post",
|
|
7106
|
-
newsletter: "Newsletter"
|
|
7107
|
-
};
|
|
7108
|
-
function parsePort(portOption) {
|
|
7109
|
-
if (!portOption) {
|
|
7110
|
-
return DEFAULT_PORT;
|
|
7111
|
-
}
|
|
7112
|
-
const port = Number.parseInt(portOption, 10);
|
|
7113
|
-
if (Number.isNaN(port) || port < 1 || port > 65535) {
|
|
7114
|
-
throw new Error(`Invalid port "${portOption}". Choose a value between 1 and 65535.`);
|
|
7115
|
-
}
|
|
7116
|
-
return port;
|
|
7117
|
-
}
|
|
7118
|
-
function stripFrontmatter2(markdown) {
|
|
7119
|
-
return markdown.replace(/^---\s*\n[\s\S]*?\n---\s*\n?/, "");
|
|
7120
|
-
}
|
|
7121
|
-
function extractFrontmatterSlug(markdown) {
|
|
7122
|
-
const frontmatterMatch = markdown.match(/^---\s*\n([\s\S]*?)\n---\s*\n?/);
|
|
7123
|
-
const frontmatter = frontmatterMatch?.[1];
|
|
7124
|
-
if (!frontmatter) {
|
|
7125
|
-
return null;
|
|
7126
|
-
}
|
|
7127
|
-
const slugMatch = frontmatter.match(/^slug:\s*(.+)$/m);
|
|
7128
|
-
const rawSlug = slugMatch?.[1]?.trim();
|
|
7129
|
-
if (!rawSlug) {
|
|
7130
|
-
return null;
|
|
7131
|
-
}
|
|
7132
|
-
const unquoted = rawSlug.replace(/^['\"]|['\"]$/g, "").trim();
|
|
7133
|
-
return unquoted.length > 0 ? unquoted : null;
|
|
7134
|
-
}
|
|
7135
|
-
function extractHeadingTitle(markdown) {
|
|
7136
|
-
const headingMatch = markdown.match(/^#\s+(.+)$/m);
|
|
7137
|
-
if (!headingMatch || !headingMatch[1]) {
|
|
7138
|
-
return null;
|
|
7139
|
-
}
|
|
7140
|
-
return headingMatch[1].trim();
|
|
7141
|
-
}
|
|
7142
|
-
async function resolveMarkdownPath(markdownPathArg, markdownOutputDir, cwd2) {
|
|
7143
|
-
if (markdownPathArg) {
|
|
7144
|
-
const resolved = path10.isAbsolute(markdownPathArg) ? markdownPathArg : path10.resolve(cwd2, markdownPathArg);
|
|
7145
|
-
if (path10.extname(resolved).toLowerCase() !== ".md") {
|
|
7146
|
-
throw new Error(`Expected a markdown file (.md), received: ${resolved}`);
|
|
7147
|
-
}
|
|
7148
|
-
await assertFileExists(resolved, "Could not find markdown file");
|
|
7149
|
-
return resolved;
|
|
7150
|
-
}
|
|
7151
|
-
return await resolveLatestMarkdown(markdownOutputDir);
|
|
7152
|
-
}
|
|
7153
|
-
async function resolveLatestMarkdown(markdownOutputDir) {
|
|
7154
|
-
const markdownCandidates = await findMarkdownFiles(markdownOutputDir);
|
|
7155
|
-
if (markdownCandidates.length === 0) {
|
|
7156
|
-
throw new Error(
|
|
7157
|
-
`No generated articles found in ${markdownOutputDir}. Run ideon write "your idea" first or pass a markdown path.`
|
|
7158
|
-
);
|
|
7159
|
-
}
|
|
7160
|
-
let latestPath = markdownCandidates[0];
|
|
7161
|
-
let latestMtime = 0;
|
|
7162
|
-
for (const candidate of markdownCandidates) {
|
|
7163
|
-
const fileStat = await stat4(candidate);
|
|
7164
|
-
if (fileStat.mtimeMs >= latestMtime) {
|
|
7165
|
-
latestMtime = fileStat.mtimeMs;
|
|
7166
|
-
latestPath = candidate;
|
|
7167
|
-
}
|
|
7168
|
-
}
|
|
7169
|
-
return latestPath;
|
|
7170
|
-
}
|
|
7171
|
-
async function assertFileExists(filePath, errorPrefix) {
|
|
7172
|
-
try {
|
|
7173
|
-
const fileStat = await stat4(filePath);
|
|
7174
|
-
if (!fileStat.isFile()) {
|
|
7175
|
-
throw new Error(`${errorPrefix}: ${filePath}`);
|
|
7176
|
-
}
|
|
7177
|
-
} catch {
|
|
7178
|
-
throw new Error(`${errorPrefix}: ${filePath}`);
|
|
7179
|
-
}
|
|
7180
|
-
}
|
|
7181
|
-
function extractCoverImageUrl(markdown) {
|
|
7182
|
-
const body = stripFrontmatter2(markdown);
|
|
7183
|
-
const match = body.match(/!\[[^\]]*\]\(([^)]+)\)/);
|
|
7184
|
-
return match?.[1] ?? null;
|
|
7185
|
-
}
|
|
7186
|
-
async function extractArticleMetadata(markdownPath) {
|
|
7187
|
-
const markdown = await readFile7(markdownPath, "utf8");
|
|
7188
|
-
const fileStat = await stat4(markdownPath);
|
|
7189
|
-
const slug = extractFrontmatterSlug(markdown) ?? path10.basename(markdownPath, ".md");
|
|
7190
|
-
const title = extractHeadingTitle(stripFrontmatter2(markdown)) ?? slug;
|
|
7191
|
-
const body = stripFrontmatter2(markdown);
|
|
7192
|
-
const previewSnippet = body.replace(/[#\[\]()!\-*_`]/g, "").trim().substring(0, 150);
|
|
7193
|
-
const coverImageUrl = extractCoverImageUrl(markdown);
|
|
7194
|
-
return {
|
|
7195
|
-
slug,
|
|
7196
|
-
title,
|
|
7197
|
-
mtime: fileStat.mtimeMs,
|
|
7198
|
-
previewSnippet,
|
|
7199
|
-
coverImageUrl
|
|
7200
|
-
};
|
|
7201
|
-
}
|
|
7202
|
-
async function listAllGenerations(markdownOutputDir) {
|
|
7203
|
-
const markdownFiles = await findMarkdownFiles(markdownOutputDir);
|
|
7204
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
7205
|
-
for (const filePath of markdownFiles) {
|
|
7206
|
-
try {
|
|
7207
|
-
const metadata = await extractArticleMetadata(filePath);
|
|
7208
|
-
const identity = deriveOutputIdentity(filePath, markdownOutputDir);
|
|
7209
|
-
const output = {
|
|
7210
|
-
id: `${identity.generationId}:${identity.contentType}:${identity.index}`,
|
|
7211
|
-
generationId: identity.generationId,
|
|
7212
|
-
sourcePath: filePath,
|
|
7213
|
-
slug: metadata.slug,
|
|
7214
|
-
title: metadata.title,
|
|
7215
|
-
previewSnippet: metadata.previewSnippet,
|
|
7216
|
-
coverImageUrl: metadata.coverImageUrl,
|
|
7217
|
-
mtime: metadata.mtime,
|
|
7218
|
-
contentType: identity.contentType,
|
|
7219
|
-
contentTypeLabel: toContentTypeLabel(identity.contentType),
|
|
7220
|
-
index: identity.index
|
|
7221
|
-
};
|
|
7222
|
-
const existing = grouped.get(identity.generationId);
|
|
7223
|
-
if (existing) {
|
|
7224
|
-
existing.push(output);
|
|
7225
|
-
} else {
|
|
7226
|
-
grouped.set(identity.generationId, [output]);
|
|
7542
|
+
modelSettings: {
|
|
7543
|
+
...settings.modelSettings,
|
|
7544
|
+
maxTokens: nextMaxTokens
|
|
7227
7545
|
}
|
|
7228
|
-
}
|
|
7229
|
-
|
|
7546
|
+
});
|
|
7547
|
+
return;
|
|
7230
7548
|
}
|
|
7231
|
-
|
|
7232
|
-
|
|
7233
|
-
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
const newestMtime = outputs.reduce((latest, output) => Math.max(latest, output.mtime), 0);
|
|
7240
|
-
generations.push({
|
|
7241
|
-
id,
|
|
7242
|
-
title: primary.title,
|
|
7243
|
-
mtime: newestMtime,
|
|
7244
|
-
previewSnippet: primary.previewSnippet,
|
|
7245
|
-
coverImageUrl: primary.coverImageUrl ?? outputs.find((output) => Boolean(output.coverImageUrl))?.coverImageUrl ?? null,
|
|
7246
|
-
primaryContentType,
|
|
7247
|
-
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
|
+
}
|
|
7248
7557
|
});
|
|
7558
|
+
return;
|
|
7249
7559
|
}
|
|
7250
|
-
|
|
7251
|
-
|
|
7252
|
-
|
|
7253
|
-
function deriveGenerationId(markdownPath, markdownOutputDir) {
|
|
7254
|
-
const relative = path10.relative(markdownOutputDir, markdownPath);
|
|
7255
|
-
const normalized = relative.split(path10.sep).join("/");
|
|
7256
|
-
if (!normalized || normalized.startsWith("../")) {
|
|
7257
|
-
return path10.basename(markdownPath, ".md");
|
|
7560
|
+
if (action === "markdownOutputDir") {
|
|
7561
|
+
setSettings({ ...settings, markdownOutputDir: value2.trim() || settings.markdownOutputDir });
|
|
7562
|
+
return;
|
|
7258
7563
|
}
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
return
|
|
7564
|
+
if (action === "assetOutputDir") {
|
|
7565
|
+
setSettings({ ...settings, assetOutputDir: value2.trim() || settings.assetOutputDir });
|
|
7566
|
+
return;
|
|
7262
7567
|
}
|
|
7263
|
-
|
|
7264
|
-
|
|
7265
|
-
|
|
7266
|
-
|
|
7267
|
-
|
|
7268
|
-
|
|
7269
|
-
|
|
7270
|
-
|
|
7271
|
-
continue;
|
|
7272
|
-
}
|
|
7273
|
-
let entries;
|
|
7274
|
-
try {
|
|
7275
|
-
entries = await readdir(current, { withFileTypes: true });
|
|
7276
|
-
} catch {
|
|
7277
|
-
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;
|
|
7278
7576
|
}
|
|
7279
|
-
|
|
7280
|
-
|
|
7281
|
-
|
|
7282
|
-
|
|
7283
|
-
|
|
7284
|
-
}
|
|
7285
|
-
if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
|
|
7286
|
-
files.push(fullPath);
|
|
7577
|
+
setSettings({
|
|
7578
|
+
...settings,
|
|
7579
|
+
t2i: {
|
|
7580
|
+
...settings.t2i,
|
|
7581
|
+
inputOverrides: nextOverrides
|
|
7287
7582
|
}
|
|
7288
|
-
}
|
|
7583
|
+
});
|
|
7289
7584
|
}
|
|
7290
|
-
return files;
|
|
7291
7585
|
}
|
|
7292
|
-
function
|
|
7293
|
-
|
|
7294
|
-
|
|
7295
|
-
const parsed = fileBase.match(/^([a-z0-9-]+)-(\d+)$/i);
|
|
7296
|
-
if (!parsed || !parsed[1] || !parsed[2]) {
|
|
7297
|
-
return {
|
|
7298
|
-
generationId,
|
|
7299
|
-
contentType: "article",
|
|
7300
|
-
index: 1
|
|
7301
|
-
};
|
|
7586
|
+
function formatEditorValue(value2) {
|
|
7587
|
+
if (value2 === null || value2 === void 0) {
|
|
7588
|
+
return "";
|
|
7302
7589
|
}
|
|
7303
|
-
|
|
7304
|
-
const index = Number.parseInt(parsed[2], 10);
|
|
7305
|
-
return {
|
|
7306
|
-
generationId,
|
|
7307
|
-
contentType: FILE_PREFIX_TO_CONTENT_TYPE[prefix] ?? prefix,
|
|
7308
|
-
index: Number.isFinite(index) && index > 0 ? index : 1
|
|
7309
|
-
};
|
|
7590
|
+
return String(value2);
|
|
7310
7591
|
}
|
|
7311
|
-
function
|
|
7312
|
-
|
|
7313
|
-
|
|
7314
|
-
const normalizedLeft = leftIndex === -1 ? Number.MAX_SAFE_INTEGER : leftIndex;
|
|
7315
|
-
const normalizedRight = rightIndex === -1 ? Number.MAX_SAFE_INTEGER : rightIndex;
|
|
7316
|
-
if (normalizedLeft !== normalizedRight) {
|
|
7317
|
-
return normalizedLeft - normalizedRight;
|
|
7592
|
+
function formatValue(value2) {
|
|
7593
|
+
if (value2 === null || value2 === void 0 || value2 === "") {
|
|
7594
|
+
return "(default)";
|
|
7318
7595
|
}
|
|
7319
|
-
return
|
|
7596
|
+
return String(value2);
|
|
7320
7597
|
}
|
|
7321
|
-
function
|
|
7322
|
-
const
|
|
7323
|
-
|
|
7324
|
-
|
|
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;
|
|
7325
7609
|
}
|
|
7326
|
-
|
|
7610
|
+
if (normalized === "false") {
|
|
7611
|
+
return false;
|
|
7612
|
+
}
|
|
7613
|
+
return fallback;
|
|
7327
7614
|
}
|
|
7328
|
-
|
|
7329
|
-
|
|
7330
|
-
|
|
7331
|
-
|
|
7332
|
-
|
|
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;
|
|
7333
7642
|
}
|
|
7334
|
-
const
|
|
7643
|
+
const savedResult = finalResult;
|
|
7644
|
+
await saveSettings(savedResult.settings);
|
|
7335
7645
|
try {
|
|
7336
|
-
|
|
7337
|
-
|
|
7338
|
-
|
|
7339
|
-
|
|
7340
|
-
|
|
7341
|
-
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;
|
|
7342
7652
|
}
|
|
7343
|
-
|
|
7344
|
-
return fallback;
|
|
7653
|
+
throw error;
|
|
7345
7654
|
}
|
|
7346
|
-
|
|
7655
|
+
console.log(`Settings saved to ${getSettingsFilePath()}.`);
|
|
7347
7656
|
}
|
|
7348
7657
|
|
|
7658
|
+
// src/cli/commands/serve.ts
|
|
7659
|
+
import path13 from "path";
|
|
7660
|
+
import { spawn } from "child_process";
|
|
7661
|
+
|
|
7349
7662
|
// src/server/previewServer.ts
|
|
7350
7663
|
import { execFile } from "child_process";
|
|
7351
7664
|
import { promisify } from "util";
|
|
7352
|
-
import { readFile as
|
|
7665
|
+
import { readFile as readFile9, stat as stat6 } from "fs/promises";
|
|
7353
7666
|
import { watch as fsWatch } from "fs";
|
|
7354
|
-
import
|
|
7667
|
+
import path12 from "path";
|
|
7355
7668
|
import { fileURLToPath } from "url";
|
|
7356
7669
|
import express from "express";
|
|
7357
7670
|
import { marked } from "marked";
|
|
7358
|
-
|
|
7359
|
-
// src/output/enrichMarkdownWithLinks.ts
|
|
7360
|
-
function enrichMarkdownWithLinks(markdown, links) {
|
|
7361
|
-
if (links.length === 0) {
|
|
7362
|
-
return markdown;
|
|
7363
|
-
}
|
|
7364
|
-
const sorted = [...links].sort((left, right) => right.expression.length - left.expression.length);
|
|
7365
|
-
let updated = markdown;
|
|
7366
|
-
for (const link of sorted) {
|
|
7367
|
-
const escapedExpression = escapeRegExp(link.expression);
|
|
7368
|
-
const leadBoundary = /^\w/.test(link.expression) ? "\\b" : "";
|
|
7369
|
-
const trailBoundary = /\w$/.test(link.expression) ? "\\b" : "";
|
|
7370
|
-
const expressionRegex = new RegExp(`${leadBoundary}${escapedExpression}${trailBoundary}`, "g");
|
|
7371
|
-
let match;
|
|
7372
|
-
while ((match = expressionRegex.exec(updated)) !== null) {
|
|
7373
|
-
const start = match.index;
|
|
7374
|
-
const end = start + match[0].length;
|
|
7375
|
-
if (isInProtectedSpan(updated, start, end)) {
|
|
7376
|
-
continue;
|
|
7377
|
-
}
|
|
7378
|
-
updated = `${updated.slice(0, start)}[${match[0]}](${link.url})${updated.slice(end)}`;
|
|
7379
|
-
break;
|
|
7380
|
-
}
|
|
7381
|
-
}
|
|
7382
|
-
return updated;
|
|
7383
|
-
}
|
|
7384
|
-
function isInProtectedSpan(content, start, end) {
|
|
7385
|
-
const lineStart = content.lastIndexOf("\n", start) + 1;
|
|
7386
|
-
const lineEndIdx = content.indexOf("\n", end);
|
|
7387
|
-
const lineEnd = lineEndIdx === -1 ? content.length : lineEndIdx;
|
|
7388
|
-
const line = content.slice(lineStart, lineEnd);
|
|
7389
|
-
const linkPattern = /\[[^\]]*\]\([^)]*\)/g;
|
|
7390
|
-
let m;
|
|
7391
|
-
while ((m = linkPattern.exec(line)) !== null) {
|
|
7392
|
-
const absStart = lineStart + m.index;
|
|
7393
|
-
const absEnd = absStart + m[0].length;
|
|
7394
|
-
if (start >= absStart && end <= absEnd) {
|
|
7395
|
-
return true;
|
|
7396
|
-
}
|
|
7397
|
-
}
|
|
7398
|
-
const codePattern = /`[^`]+`/g;
|
|
7399
|
-
while ((m = codePattern.exec(line)) !== null) {
|
|
7400
|
-
const absStart = lineStart + m.index;
|
|
7401
|
-
const absEnd = absStart + m[0].length;
|
|
7402
|
-
if (start >= absStart && end <= absEnd) {
|
|
7403
|
-
return true;
|
|
7404
|
-
}
|
|
7405
|
-
}
|
|
7406
|
-
return false;
|
|
7407
|
-
}
|
|
7408
|
-
function escapeRegExp(value2) {
|
|
7409
|
-
return value2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7410
|
-
}
|
|
7411
|
-
|
|
7412
|
-
// src/server/previewServer.ts
|
|
7413
7671
|
var execFileAsync = promisify(execFile);
|
|
7414
7672
|
var MissingArticleError = class extends Error {
|
|
7415
7673
|
constructor(message) {
|
|
@@ -7504,7 +7762,7 @@ async function startPreviewServer(options) {
|
|
|
7504
7762
|
if (options.watch) {
|
|
7505
7763
|
let html2;
|
|
7506
7764
|
try {
|
|
7507
|
-
html2 = await
|
|
7765
|
+
html2 = await readFile9(path12.join(previewClientDir, "index.html"), "utf8");
|
|
7508
7766
|
} catch {
|
|
7509
7767
|
res.status(200).type("html").send(
|
|
7510
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>`
|
|
@@ -7515,7 +7773,7 @@ async function startPreviewServer(options) {
|
|
|
7515
7773
|
const injected = html2.replace("</body>", `${reloadScript}</body>`);
|
|
7516
7774
|
res.status(200).type("html").send(injected);
|
|
7517
7775
|
} else {
|
|
7518
|
-
res.status(200).sendFile(
|
|
7776
|
+
res.status(200).sendFile(path12.join(previewClientDir, "index.html"));
|
|
7519
7777
|
}
|
|
7520
7778
|
return;
|
|
7521
7779
|
}
|
|
@@ -7568,7 +7826,7 @@ async function getArticleContent(generationId, markdownOutputDir) {
|
|
|
7568
7826
|
generation.outputs.map(async (output) => {
|
|
7569
7827
|
let markdown = "";
|
|
7570
7828
|
try {
|
|
7571
|
-
markdown = await
|
|
7829
|
+
markdown = await readFile9(output.sourcePath, "utf8");
|
|
7572
7830
|
} catch (error) {
|
|
7573
7831
|
if (isMissingFileError(error)) {
|
|
7574
7832
|
throw new MissingArticleError(`Generation "${generationId}" no longer exists.`);
|
|
@@ -7586,7 +7844,7 @@ async function getArticleContent(generationId, markdownOutputDir) {
|
|
|
7586
7844
|
};
|
|
7587
7845
|
})
|
|
7588
7846
|
);
|
|
7589
|
-
const generationDir =
|
|
7847
|
+
const generationDir = path12.dirname(generation.outputs[0]?.sourcePath ?? "");
|
|
7590
7848
|
const interactions = generationDir ? await loadSavedInteractions(generationDir) : { llmCalls: [], t2iCalls: [] };
|
|
7591
7849
|
const analyticsSummary = generationDir ? await loadSavedAnalyticsSummary(generationDir) : null;
|
|
7592
7850
|
return {
|
|
@@ -7615,7 +7873,7 @@ async function resolveActivePreviewArticle(preferredMarkdownPath, markdownOutput
|
|
|
7615
7873
|
};
|
|
7616
7874
|
}
|
|
7617
7875
|
function resolveGenerationSourcePath(generation, markdownOutputDir) {
|
|
7618
|
-
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);
|
|
7619
7877
|
}
|
|
7620
7878
|
function isMissingFileError(error) {
|
|
7621
7879
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
@@ -7630,7 +7888,7 @@ async function renderArticleHtml(markdown, generationId, sourcePath) {
|
|
|
7630
7888
|
async function loadSavedLinks(markdownPath) {
|
|
7631
7889
|
const linksPath = resolveLinksPath(markdownPath);
|
|
7632
7890
|
try {
|
|
7633
|
-
const raw = await
|
|
7891
|
+
const raw = await readFile9(linksPath, "utf8");
|
|
7634
7892
|
const parsed = JSON.parse(raw);
|
|
7635
7893
|
if (!Array.isArray(parsed.links)) {
|
|
7636
7894
|
return [];
|
|
@@ -7654,9 +7912,9 @@ async function loadSavedLinks(markdownPath) {
|
|
|
7654
7912
|
}
|
|
7655
7913
|
}
|
|
7656
7914
|
async function loadSavedInteractions(generationDir) {
|
|
7657
|
-
const interactionsPath =
|
|
7915
|
+
const interactionsPath = path12.join(generationDir, "model.interactions.json");
|
|
7658
7916
|
try {
|
|
7659
|
-
const raw = await
|
|
7917
|
+
const raw = await readFile9(interactionsPath, "utf8");
|
|
7660
7918
|
const parsed = JSON.parse(raw);
|
|
7661
7919
|
const llmCalls = Array.isArray(parsed.llmCalls) ? parsed.llmCalls.filter(isPreviewLlmInteraction) : [];
|
|
7662
7920
|
const t2iCalls = Array.isArray(parsed.t2iCalls) ? parsed.t2iCalls.filter(isPreviewT2IInteraction) : [];
|
|
@@ -7672,9 +7930,9 @@ async function loadSavedInteractions(generationDir) {
|
|
|
7672
7930
|
}
|
|
7673
7931
|
}
|
|
7674
7932
|
async function loadSavedAnalyticsSummary(generationDir) {
|
|
7675
|
-
const analyticsPath =
|
|
7933
|
+
const analyticsPath = path12.join(generationDir, "generation.analytics.json");
|
|
7676
7934
|
try {
|
|
7677
|
-
const raw = await
|
|
7935
|
+
const raw = await readFile9(analyticsPath, "utf8");
|
|
7678
7936
|
const parsed = JSON.parse(raw);
|
|
7679
7937
|
const summary = parsed.summary;
|
|
7680
7938
|
if (!summary || typeof summary !== "object") {
|
|
@@ -7706,14 +7964,14 @@ async function getPreviewBootstrapData(preferredMarkdownPath, markdownOutputDir)
|
|
|
7706
7964
|
};
|
|
7707
7965
|
}
|
|
7708
7966
|
async function resolvePreviewClientBuildDir() {
|
|
7709
|
-
const currentDir =
|
|
7967
|
+
const currentDir = path12.dirname(fileURLToPath(import.meta.url));
|
|
7710
7968
|
const candidates = [
|
|
7711
|
-
|
|
7712
|
-
|
|
7969
|
+
path12.resolve(currentDir, "preview"),
|
|
7970
|
+
path12.resolve(currentDir, "../../dist/preview")
|
|
7713
7971
|
];
|
|
7714
7972
|
for (const candidate of candidates) {
|
|
7715
7973
|
try {
|
|
7716
|
-
const indexStat = await
|
|
7974
|
+
const indexStat = await stat6(path12.join(candidate, "index.html"));
|
|
7717
7975
|
if (indexStat.isFile()) {
|
|
7718
7976
|
return candidate;
|
|
7719
7977
|
}
|
|
@@ -7775,21 +8033,21 @@ async function resolveGenerationAssetPath(generationId, rawAssetPath, markdownOu
|
|
|
7775
8033
|
throw new MissingArticleError(`Generation "${generationId}" no longer exists.`);
|
|
7776
8034
|
}
|
|
7777
8035
|
const decodedAssetPath = decodeURIComponent(rawAssetPath);
|
|
7778
|
-
const normalizedRelative =
|
|
7779
|
-
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)) {
|
|
7780
8038
|
throw new Error("Invalid generation asset path.");
|
|
7781
8039
|
}
|
|
7782
|
-
const generationDir =
|
|
8040
|
+
const generationDir = path12.dirname(generation.outputs[0]?.sourcePath ?? "");
|
|
7783
8041
|
if (!generationDir) {
|
|
7784
8042
|
throw new MissingArticleError(`Generation "${generationId}" has no source directory.`);
|
|
7785
8043
|
}
|
|
7786
|
-
const resolvedPath =
|
|
7787
|
-
const relativeToGeneration =
|
|
7788
|
-
if (relativeToGeneration.startsWith("..") ||
|
|
8044
|
+
const resolvedPath = path12.resolve(generationDir, normalizedRelative);
|
|
8045
|
+
const relativeToGeneration = path12.relative(generationDir, resolvedPath);
|
|
8046
|
+
if (relativeToGeneration.startsWith("..") || path12.isAbsolute(relativeToGeneration)) {
|
|
7789
8047
|
throw new Error("Invalid generation asset path.");
|
|
7790
8048
|
}
|
|
7791
8049
|
try {
|
|
7792
|
-
const fileStat = await
|
|
8050
|
+
const fileStat = await stat6(resolvedPath);
|
|
7793
8051
|
if (!fileStat.isFile()) {
|
|
7794
8052
|
throw new Error("Invalid generation asset path.");
|
|
7795
8053
|
}
|
|
@@ -9269,7 +9527,7 @@ async function runServeCommand(options) {
|
|
|
9269
9527
|
const markdownPath = await resolveMarkdownPath(options.markdownPath, outputPaths.markdownOutputDir, process.cwd());
|
|
9270
9528
|
const port = parsePort(options.port);
|
|
9271
9529
|
if (options.watch) {
|
|
9272
|
-
const viteBin =
|
|
9530
|
+
const viteBin = path13.resolve(process.cwd(), "node_modules", ".bin", "vite");
|
|
9273
9531
|
const viteProcess = spawn(viteBin, ["build", "--watch"], {
|
|
9274
9532
|
stdio: "inherit",
|
|
9275
9533
|
shell: process.platform === "win32"
|
|
@@ -9295,8 +9553,8 @@ async function runServeCommand(options) {
|
|
|
9295
9553
|
openBrowser: options.openBrowser,
|
|
9296
9554
|
watch: options.watch
|
|
9297
9555
|
});
|
|
9298
|
-
const relativeArticle =
|
|
9299
|
-
const relativeAssets =
|
|
9556
|
+
const relativeArticle = path13.relative(process.cwd(), markdownPath);
|
|
9557
|
+
const relativeAssets = path13.relative(process.cwd(), outputPaths.assetOutputDir);
|
|
9300
9558
|
console.log(`Previewing ${relativeArticle || markdownPath}`);
|
|
9301
9559
|
console.log(`Serving assets from ${relativeAssets || outputPaths.assetOutputDir}`);
|
|
9302
9560
|
console.log(`Open ${server.url}`);
|
|
@@ -9782,7 +10040,7 @@ function formatPipelineStageCost(stage) {
|
|
|
9782
10040
|
}
|
|
9783
10041
|
return stage.costSource === "estimated" ? `~${formatted}` : formatted;
|
|
9784
10042
|
}
|
|
9785
|
-
async function renderPlainPipeline(input, dryRun, enrichLinks2, runMode, links, unlinks, maxLinks) {
|
|
10043
|
+
async function renderPlainPipeline(input, dryRun, enrichLinks2, runMode, links, unlinks, maxLinks, maxImages) {
|
|
9786
10044
|
let previousStages = /* @__PURE__ */ new Map();
|
|
9787
10045
|
let previousItemStatuses = /* @__PURE__ */ new Map();
|
|
9788
10046
|
const notificationsEnabled = input.config.settings.notifications.enabled;
|
|
@@ -9799,6 +10057,7 @@ async function renderPlainPipeline(input, dryRun, enrichLinks2, runMode, links,
|
|
|
9799
10057
|
customLinks: links,
|
|
9800
10058
|
unlinks,
|
|
9801
10059
|
maxLinks,
|
|
10060
|
+
maxImages,
|
|
9802
10061
|
onUpdate(stages) {
|
|
9803
10062
|
for (const stage of stages) {
|
|
9804
10063
|
const previous = previousStages.get(stage.id);
|
|
@@ -10160,6 +10419,7 @@ function WriteApp({
|
|
|
10160
10419
|
links,
|
|
10161
10420
|
unlinks,
|
|
10162
10421
|
maxLinks,
|
|
10422
|
+
maxImages,
|
|
10163
10423
|
onError
|
|
10164
10424
|
}) {
|
|
10165
10425
|
const { exit } = useApp3();
|
|
@@ -10186,6 +10446,7 @@ function WriteApp({
|
|
|
10186
10446
|
customLinks: links,
|
|
10187
10447
|
unlinks,
|
|
10188
10448
|
maxLinks,
|
|
10449
|
+
maxImages,
|
|
10189
10450
|
onUpdate(nextStages) {
|
|
10190
10451
|
if (mounted) {
|
|
10191
10452
|
setStages(nextStages);
|
|
@@ -10218,7 +10479,7 @@ function WriteApp({
|
|
|
10218
10479
|
return () => {
|
|
10219
10480
|
mounted = false;
|
|
10220
10481
|
};
|
|
10221
|
-
}, [dryRun, enrichLinks2, input, links, unlinks, maxLinks, onError, runMode]);
|
|
10482
|
+
}, [dryRun, enrichLinks2, input, links, unlinks, maxLinks, maxImages, onError, runMode]);
|
|
10222
10483
|
useEffect2(() => {
|
|
10223
10484
|
if (!result && !errorMessage) {
|
|
10224
10485
|
return;
|
|
@@ -10234,7 +10495,7 @@ function WriteApp({
|
|
|
10234
10495
|
}
|
|
10235
10496
|
async function runWriteCommand(options) {
|
|
10236
10497
|
const input = await resolveInputWithInteractiveIdeaFallback(options);
|
|
10237
|
-
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);
|
|
10238
10499
|
}
|
|
10239
10500
|
async function runWriteResumeCommand(options = {}) {
|
|
10240
10501
|
const session = await loadWriteSession();
|
|
@@ -10256,9 +10517,9 @@ async function runWriteResumeCommand(options = {}) {
|
|
|
10256
10517
|
secrets: resolved.config.secrets
|
|
10257
10518
|
}
|
|
10258
10519
|
};
|
|
10259
|
-
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);
|
|
10260
10521
|
}
|
|
10261
|
-
async function runWritePipeline(input, dryRun, enrichLinks2, runMode, noInteractive, links, unlinks, maxLinks) {
|
|
10522
|
+
async function runWritePipeline(input, dryRun, enrichLinks2, runMode, noInteractive, links, unlinks, maxLinks, maxImages) {
|
|
10262
10523
|
let interruptHandled = false;
|
|
10263
10524
|
const handleSignal = (signal) => {
|
|
10264
10525
|
if (interruptHandled) {
|
|
@@ -10292,7 +10553,7 @@ async function runWritePipeline(input, dryRun, enrichLinks2, runMode, noInteract
|
|
|
10292
10553
|
process.on("SIGTERM", onSigterm);
|
|
10293
10554
|
try {
|
|
10294
10555
|
if (noInteractive || !process.stdout.isTTY) {
|
|
10295
|
-
await renderPlainPipeline(input, dryRun, enrichLinks2, runMode, links, unlinks, maxLinks);
|
|
10556
|
+
await renderPlainPipeline(input, dryRun, enrichLinks2, runMode, links, unlinks, maxLinks, maxImages);
|
|
10296
10557
|
return;
|
|
10297
10558
|
}
|
|
10298
10559
|
let commandError = null;
|
|
@@ -10307,6 +10568,7 @@ async function runWritePipeline(input, dryRun, enrichLinks2, runMode, noInteract
|
|
|
10307
10568
|
links,
|
|
10308
10569
|
unlinks,
|
|
10309
10570
|
maxLinks,
|
|
10571
|
+
maxImages,
|
|
10310
10572
|
onError: (error) => {
|
|
10311
10573
|
commandError = error;
|
|
10312
10574
|
}
|
|
@@ -10474,7 +10736,7 @@ function collectOptionValue(value2, previous = []) {
|
|
|
10474
10736
|
}
|
|
10475
10737
|
async function runCli(argv) {
|
|
10476
10738
|
const program = new Command();
|
|
10477
|
-
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);
|
|
10478
10740
|
program.command("settings").description("Show the current Ideon settings and storage state.").action(async () => {
|
|
10479
10741
|
await openSettings();
|
|
10480
10742
|
});
|
|
@@ -10519,6 +10781,14 @@ async function runCli(argv) {
|
|
|
10519
10781
|
maxLinks: options.maxLinks
|
|
10520
10782
|
});
|
|
10521
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
|
+
});
|
|
10522
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) => {
|
|
10523
10793
|
await runServeCommand({
|
|
10524
10794
|
markdownPath,
|
|
@@ -10527,7 +10797,7 @@ async function runCli(argv) {
|
|
|
10527
10797
|
watch: options.watch
|
|
10528
10798
|
});
|
|
10529
10799
|
});
|
|
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) => {
|
|
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) => {
|
|
10531
10801
|
await runWriteCommand({
|
|
10532
10802
|
idea: options.idea ?? ideaArg,
|
|
10533
10803
|
audience: options.audience,
|
|
@@ -10542,16 +10812,18 @@ async function runCli(argv) {
|
|
|
10542
10812
|
enrichLinks: options.enrichLinks,
|
|
10543
10813
|
links: options.link,
|
|
10544
10814
|
unlinks: options.unlink,
|
|
10545
|
-
maxLinks: options.maxLinks
|
|
10815
|
+
maxLinks: options.maxLinks,
|
|
10816
|
+
maxImages: options.maxImages
|
|
10546
10817
|
});
|
|
10547
10818
|
});
|
|
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) => {
|
|
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) => {
|
|
10549
10820
|
await runWriteResumeCommand({
|
|
10550
10821
|
noInteractive: options.noInteractive,
|
|
10551
10822
|
enrichLinks: options.enrichLinks,
|
|
10552
10823
|
links: options.link,
|
|
10553
10824
|
unlinks: options.unlink,
|
|
10554
|
-
maxLinks: options.maxLinks
|
|
10825
|
+
maxLinks: options.maxLinks,
|
|
10826
|
+
maxImages: options.maxImages
|
|
10555
10827
|
});
|
|
10556
10828
|
});
|
|
10557
10829
|
await program.parseAsync(argv);
|