@telepat/ideon 0.1.16 → 0.1.19
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 +1211 -1604
- package/package.json +2 -1
package/dist/ideon.js
CHANGED
|
@@ -111,14 +111,14 @@ var modelSettingsSchema = z.object({
|
|
|
111
111
|
topP: z.number().min(0).max(1).default(1)
|
|
112
112
|
});
|
|
113
113
|
var baseT2ISettingsSchema = z.object({
|
|
114
|
-
modelId: z.string().default("
|
|
114
|
+
modelId: z.string().default("flux"),
|
|
115
115
|
inputOverrides: z.record(z.string(), z.unknown()).default({})
|
|
116
116
|
});
|
|
117
117
|
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.19",
|
|
1352
1366
|
description: "CLI for generating rich articles and images from ideas.",
|
|
1353
1367
|
type: "module",
|
|
1354
1368
|
repository: {
|
|
@@ -1406,6 +1420,7 @@ var package_default = {
|
|
|
1406
1420
|
dependencies: {
|
|
1407
1421
|
"@ant-design/icons": "^6.1.1",
|
|
1408
1422
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
1423
|
+
"@telepat/limn": "^0.1.5",
|
|
1409
1424
|
antd: "^6.3.4",
|
|
1410
1425
|
commander: "^14.0.1",
|
|
1411
1426
|
"env-paths": "^3.0.0",
|
|
@@ -2269,9 +2284,8 @@ function buildArticlePlanJsonSchema(targetLengthWords) {
|
|
|
2269
2284
|
items: {
|
|
2270
2285
|
type: "object",
|
|
2271
2286
|
additionalProperties: false,
|
|
2272
|
-
required: ["
|
|
2287
|
+
required: ["description"],
|
|
2273
2288
|
properties: {
|
|
2274
|
-
anchorAfterSection: { type: "integer", minimum: 1, maximum: 10 },
|
|
2275
2289
|
description: { type: "string" }
|
|
2276
2290
|
}
|
|
2277
2291
|
}
|
|
@@ -2310,7 +2324,7 @@ function buildArticlePlanMessages(idea, options) {
|
|
|
2310
2324
|
"- Sections are article-only structure and must not be treated as requirements for non-article channels.",
|
|
2311
2325
|
"- Include a cover image description and 2 to 3 inline image descriptions.",
|
|
2312
2326
|
"- Image descriptions must be concrete and contextual, not generic stock-photo phrasing.",
|
|
2313
|
-
"-
|
|
2327
|
+
"- Do not include section placement for inline images \u2014 placement is determined automatically after writing.",
|
|
2314
2328
|
"",
|
|
2315
2329
|
"Shared content brief context:",
|
|
2316
2330
|
`- description: ${options.contentBrief.description}`,
|
|
@@ -2329,7 +2343,7 @@ function buildArticlePlanMessages(idea, options) {
|
|
|
2329
2343
|
"- outroBrief: string",
|
|
2330
2344
|
`- sections: array of ${sectionCounts.label} objects, each with title and description strings`,
|
|
2331
2345
|
"- coverImageDescription: string",
|
|
2332
|
-
"- inlineImages: array of 2 to 3 objects, each with
|
|
2346
|
+
"- inlineImages: array of 2 to 3 objects, each with a description string only",
|
|
2333
2347
|
"",
|
|
2334
2348
|
"Do not omit any required fields. Return strict JSON only."
|
|
2335
2349
|
].join("\n")
|
|
@@ -2344,7 +2358,6 @@ var articleSectionPlanSchema = z5.object({
|
|
|
2344
2358
|
description: z5.string().min(1)
|
|
2345
2359
|
});
|
|
2346
2360
|
var inlineImagePlanSchema = z5.object({
|
|
2347
|
-
anchorAfterSection: z5.number().int().min(1).max(10),
|
|
2348
2361
|
description: z5.string().min(1)
|
|
2349
2362
|
});
|
|
2350
2363
|
var articlePlanSchema = z5.object({
|
|
@@ -2400,7 +2413,7 @@ async function planArticle({
|
|
|
2400
2413
|
...basePlan,
|
|
2401
2414
|
slug: uniqueSlug,
|
|
2402
2415
|
keywords: basePlan.keywords.slice(0, 8),
|
|
2403
|
-
inlineImages: basePlan.inlineImages.
|
|
2416
|
+
inlineImages: basePlan.inlineImages.slice(0, 3)
|
|
2404
2417
|
};
|
|
2405
2418
|
}
|
|
2406
2419
|
function buildDryRunPlan(idea, contentBrief) {
|
|
@@ -2438,11 +2451,9 @@ function buildDryRunPlan(idea, contentBrief) {
|
|
|
2438
2451
|
coverImageDescription: "A refined editorial workspace with notebooks, sketches, and glowing structured outlines, cinematic but minimal.",
|
|
2439
2452
|
inlineImages: [
|
|
2440
2453
|
{
|
|
2441
|
-
anchorAfterSection: 1,
|
|
2442
2454
|
description: "A rough idea evolving into a structured article outline on a desk full of notes."
|
|
2443
2455
|
},
|
|
2444
2456
|
{
|
|
2445
|
-
anchorAfterSection: 3,
|
|
2446
2457
|
description: "A collaborative editorial scene where human judgment and AI suggestions coexist."
|
|
2447
2458
|
}
|
|
2448
2459
|
]
|
|
@@ -2864,65 +2875,8 @@ function normalizeGeneratedSection(content, label2) {
|
|
|
2864
2875
|
return normalized.replace(/^```(?:markdown)?\s*/i, "").replace(/```\s*$/i, "").trim();
|
|
2865
2876
|
}
|
|
2866
2877
|
|
|
2867
|
-
// src/
|
|
2868
|
-
import
|
|
2869
|
-
var ReplicateClient = class {
|
|
2870
|
-
client;
|
|
2871
|
-
constructor(apiToken) {
|
|
2872
|
-
this.client = new Replicate({ auth: apiToken });
|
|
2873
|
-
}
|
|
2874
|
-
async runModel(model, input, options = {}) {
|
|
2875
|
-
let lastError = null;
|
|
2876
|
-
const startedAtMs = Date.now();
|
|
2877
|
-
let attempts = 0;
|
|
2878
|
-
let retries = 0;
|
|
2879
|
-
let retryBackoffMs = 0;
|
|
2880
|
-
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
2881
|
-
attempts = attempt + 1;
|
|
2882
|
-
try {
|
|
2883
|
-
const output = await this.client.run(model, { input });
|
|
2884
|
-
options.onMetrics?.({
|
|
2885
|
-
durationMs: Date.now() - startedAtMs,
|
|
2886
|
-
attempts,
|
|
2887
|
-
retries,
|
|
2888
|
-
retryBackoffMs,
|
|
2889
|
-
modelId: model
|
|
2890
|
-
});
|
|
2891
|
-
return output;
|
|
2892
|
-
} catch (error) {
|
|
2893
|
-
lastError = error instanceof Error ? error : new Error("Unknown Replicate client error.");
|
|
2894
|
-
if (attempt < 2 && shouldRetryError(lastError)) {
|
|
2895
|
-
const backoff = backoffMs(attempt);
|
|
2896
|
-
retries += 1;
|
|
2897
|
-
retryBackoffMs += backoff;
|
|
2898
|
-
options.onRetry?.({
|
|
2899
|
-
attempts,
|
|
2900
|
-
retries,
|
|
2901
|
-
retryBackoffMs,
|
|
2902
|
-
backoffMs: backoff,
|
|
2903
|
-
errorMessage: lastError.message,
|
|
2904
|
-
modelId: model
|
|
2905
|
-
});
|
|
2906
|
-
await wait(backoff);
|
|
2907
|
-
continue;
|
|
2908
|
-
}
|
|
2909
|
-
break;
|
|
2910
|
-
}
|
|
2911
|
-
}
|
|
2912
|
-
throw lastError ?? new Error("Replicate request failed.");
|
|
2913
|
-
}
|
|
2914
|
-
};
|
|
2915
|
-
function shouldRetryError(error) {
|
|
2916
|
-
return /timeout|network|fetch|temporarily|rate|429|500|502|503|504/i.test(error.message);
|
|
2917
|
-
}
|
|
2918
|
-
function backoffMs(attempt) {
|
|
2919
|
-
return 500 * (attempt + 1);
|
|
2920
|
-
}
|
|
2921
|
-
function wait(ms) {
|
|
2922
|
-
return new Promise((resolve) => {
|
|
2923
|
-
setTimeout(resolve, ms);
|
|
2924
|
-
});
|
|
2925
|
-
}
|
|
2878
|
+
// src/pipeline/runner.ts
|
|
2879
|
+
import { Limn } from "@telepat/limn";
|
|
2926
2880
|
|
|
2927
2881
|
// src/images/renderImages.ts
|
|
2928
2882
|
import { writeFile as writeFile4 } from "fs/promises";
|
|
@@ -2937,496 +2891,42 @@ var imagePromptSchema = {
|
|
|
2937
2891
|
prompt: { type: "string" }
|
|
2938
2892
|
}
|
|
2939
2893
|
};
|
|
2940
|
-
function buildImagePromptMessages(plan, image) {
|
|
2894
|
+
function buildImagePromptMessages(plan, image, section) {
|
|
2895
|
+
const userLines = [
|
|
2896
|
+
`Article title: ${plan.title}`,
|
|
2897
|
+
`Article subtitle: ${plan.subtitle}`,
|
|
2898
|
+
`Article description: ${plan.description}`,
|
|
2899
|
+
`Image kind: ${image.kind}`,
|
|
2900
|
+
`Image description: ${image.description}`
|
|
2901
|
+
];
|
|
2902
|
+
if (section) {
|
|
2903
|
+
userLines.push(
|
|
2904
|
+
`Section title: ${section.title}`,
|
|
2905
|
+
`Section excerpt: ${section.body.slice(0, 500)}`
|
|
2906
|
+
);
|
|
2907
|
+
}
|
|
2908
|
+
userLines.push("Write one strong visual prompt describing the image in natural language.");
|
|
2941
2909
|
return [
|
|
2942
2910
|
{
|
|
2943
2911
|
role: "system",
|
|
2944
|
-
content: "You write concise, high-signal prompts for text-to-image models. The prompt should be vivid
|
|
2912
|
+
content: "You write concise, high-signal prompts for text-to-image models. The prompt should be vivid and compositionally clear. Do not include any words, letters, numbers, logos, watermarks, or signage in the image, unless text in the image was explicitly requested. Return only the requested JSON."
|
|
2945
2913
|
},
|
|
2946
2914
|
{
|
|
2947
2915
|
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")
|
|
2916
|
+
content: userLines.join("\n")
|
|
2956
2917
|
}
|
|
2957
2918
|
];
|
|
2958
2919
|
}
|
|
2959
2920
|
|
|
2960
|
-
// src/models/t2i/definitions/bytedance__seedream-4.json
|
|
2961
|
-
var bytedance_seedream_4_default = {
|
|
2962
|
-
modelId: "bytedance/seedream-4",
|
|
2963
|
-
provider: "replicate",
|
|
2964
|
-
displayName: "Seedream 4",
|
|
2965
|
-
category: "image-generation",
|
|
2966
|
-
pricingSourceUrl: "https://replicate.com/bytedance/seedream-4",
|
|
2967
|
-
pricingNotes: "Replicate pricing: $0.03 per output image.",
|
|
2968
|
-
pricing: {
|
|
2969
|
-
usdPerSecond: null,
|
|
2970
|
-
usdPer1kInputTokens: null,
|
|
2971
|
-
usdPer1kOutputTokens: null
|
|
2972
|
-
},
|
|
2973
|
-
pricingRules: {
|
|
2974
|
-
basis: "output_image_count",
|
|
2975
|
-
usdPerImage: 0.03
|
|
2976
|
-
},
|
|
2977
|
-
inputOptions: {
|
|
2978
|
-
userConfigurable: ["size", "sequential_image_generation", "max_images", "enhance_prompt"],
|
|
2979
|
-
pipelineManaged: ["prompt", "aspect_ratio"],
|
|
2980
|
-
fields: {
|
|
2981
|
-
size: {
|
|
2982
|
-
type: "string",
|
|
2983
|
-
default: "2K",
|
|
2984
|
-
enum: ["1K", "2K", "4K"]
|
|
2985
|
-
},
|
|
2986
|
-
sequential_image_generation: {
|
|
2987
|
-
type: "string",
|
|
2988
|
-
default: "disabled",
|
|
2989
|
-
enum: ["disabled", "auto"]
|
|
2990
|
-
},
|
|
2991
|
-
max_images: {
|
|
2992
|
-
type: "integer",
|
|
2993
|
-
default: 1,
|
|
2994
|
-
minimum: 1,
|
|
2995
|
-
maximum: 15
|
|
2996
|
-
},
|
|
2997
|
-
enhance_prompt: {
|
|
2998
|
-
type: "boolean",
|
|
2999
|
-
default: true
|
|
3000
|
-
},
|
|
3001
|
-
prompt: {
|
|
3002
|
-
type: "string",
|
|
3003
|
-
required: true
|
|
3004
|
-
},
|
|
3005
|
-
aspect_ratio: {
|
|
3006
|
-
type: "string",
|
|
3007
|
-
required: true,
|
|
3008
|
-
enum: ["1:1", "16:9", "9:16"]
|
|
3009
|
-
}
|
|
3010
|
-
}
|
|
3011
|
-
}
|
|
3012
|
-
};
|
|
3013
|
-
|
|
3014
|
-
// src/models/t2i/definitions/black-forest-labs__flux-2-pro.json
|
|
3015
|
-
var black_forest_labs_flux_2_pro_default = {
|
|
3016
|
-
modelId: "black-forest-labs/flux-2-pro",
|
|
3017
|
-
provider: "replicate",
|
|
3018
|
-
displayName: "FLUX 2 Pro",
|
|
3019
|
-
category: "image-generation",
|
|
3020
|
-
pricingSourceUrl: "https://replicate.com/black-forest-labs/flux-2-pro",
|
|
3021
|
-
pricingNotes: "Replicate lists multi-property pricing for run and input/output megapixels; this model currently uses best-effort cost fallback when exact rule mapping is unavailable.",
|
|
3022
|
-
pricing: {
|
|
3023
|
-
usdPerSecond: null,
|
|
3024
|
-
usdPer1kInputTokens: null,
|
|
3025
|
-
usdPer1kOutputTokens: null
|
|
3026
|
-
},
|
|
3027
|
-
inputOptions: {
|
|
3028
|
-
userConfigurable: ["safety_tolerance", "seed", "output_format", "output_quality"],
|
|
3029
|
-
pipelineManaged: ["prompt", "aspect_ratio", "width", "height"],
|
|
3030
|
-
fields: {
|
|
3031
|
-
safety_tolerance: {
|
|
3032
|
-
type: "integer",
|
|
3033
|
-
default: 2,
|
|
3034
|
-
minimum: 1,
|
|
3035
|
-
maximum: 5
|
|
3036
|
-
},
|
|
3037
|
-
seed: {
|
|
3038
|
-
type: "integer",
|
|
3039
|
-
nullable: true
|
|
3040
|
-
},
|
|
3041
|
-
output_format: {
|
|
3042
|
-
type: "string",
|
|
3043
|
-
default: "webp",
|
|
3044
|
-
enum: ["webp", "png", "jpg", "jpeg"]
|
|
3045
|
-
},
|
|
3046
|
-
output_quality: {
|
|
3047
|
-
type: "integer",
|
|
3048
|
-
default: 80,
|
|
3049
|
-
minimum: 0,
|
|
3050
|
-
maximum: 100
|
|
3051
|
-
},
|
|
3052
|
-
prompt: {
|
|
3053
|
-
type: "string",
|
|
3054
|
-
required: true
|
|
3055
|
-
},
|
|
3056
|
-
aspect_ratio: {
|
|
3057
|
-
type: "string",
|
|
3058
|
-
required: true,
|
|
3059
|
-
enum: ["custom"]
|
|
3060
|
-
},
|
|
3061
|
-
width: {
|
|
3062
|
-
type: "integer",
|
|
3063
|
-
required: true,
|
|
3064
|
-
minimum: 256,
|
|
3065
|
-
maximum: 2048
|
|
3066
|
-
},
|
|
3067
|
-
height: {
|
|
3068
|
-
type: "integer",
|
|
3069
|
-
required: true,
|
|
3070
|
-
minimum: 256,
|
|
3071
|
-
maximum: 2048
|
|
3072
|
-
}
|
|
3073
|
-
}
|
|
3074
|
-
}
|
|
3075
|
-
};
|
|
3076
|
-
|
|
3077
|
-
// src/models/t2i/definitions/black-forest-labs__flux-schnell.json
|
|
3078
|
-
var black_forest_labs_flux_schnell_default = {
|
|
3079
|
-
modelId: "black-forest-labs/flux-schnell",
|
|
3080
|
-
provider: "replicate",
|
|
3081
|
-
displayName: "FLUX Schnell",
|
|
3082
|
-
category: "image-generation",
|
|
3083
|
-
pricingSourceUrl: "https://replicate.com/black-forest-labs/flux-schnell",
|
|
3084
|
-
pricingNotes: "Replicate pricing: $3 per 1000 output images.",
|
|
3085
|
-
pricing: {
|
|
3086
|
-
usdPerSecond: null,
|
|
3087
|
-
usdPer1kInputTokens: null,
|
|
3088
|
-
usdPer1kOutputTokens: null
|
|
3089
|
-
},
|
|
3090
|
-
pricingRules: {
|
|
3091
|
-
basis: "output_image_count",
|
|
3092
|
-
usdPerImage: 3e-3
|
|
3093
|
-
},
|
|
3094
|
-
inputOptions: {
|
|
3095
|
-
userConfigurable: ["num_outputs", "num_inference_steps", "seed", "output_format", "output_quality", "disable_safety_checker", "go_fast", "megapixels"],
|
|
3096
|
-
pipelineManaged: ["prompt", "aspect_ratio"],
|
|
3097
|
-
fields: {
|
|
3098
|
-
num_outputs: {
|
|
3099
|
-
type: "integer",
|
|
3100
|
-
default: 1,
|
|
3101
|
-
minimum: 1,
|
|
3102
|
-
maximum: 4
|
|
3103
|
-
},
|
|
3104
|
-
num_inference_steps: {
|
|
3105
|
-
type: "integer",
|
|
3106
|
-
default: 4,
|
|
3107
|
-
minimum: 1,
|
|
3108
|
-
maximum: 4
|
|
3109
|
-
},
|
|
3110
|
-
seed: {
|
|
3111
|
-
type: "integer",
|
|
3112
|
-
nullable: true
|
|
3113
|
-
},
|
|
3114
|
-
output_format: {
|
|
3115
|
-
type: "string",
|
|
3116
|
-
default: "webp",
|
|
3117
|
-
enum: ["webp", "jpg", "png"]
|
|
3118
|
-
},
|
|
3119
|
-
output_quality: {
|
|
3120
|
-
type: "integer",
|
|
3121
|
-
default: 80,
|
|
3122
|
-
minimum: 0,
|
|
3123
|
-
maximum: 100
|
|
3124
|
-
},
|
|
3125
|
-
disable_safety_checker: {
|
|
3126
|
-
type: "boolean",
|
|
3127
|
-
default: false
|
|
3128
|
-
},
|
|
3129
|
-
go_fast: {
|
|
3130
|
-
type: "boolean",
|
|
3131
|
-
default: true
|
|
3132
|
-
},
|
|
3133
|
-
megapixels: {
|
|
3134
|
-
type: "string",
|
|
3135
|
-
default: "1",
|
|
3136
|
-
allowAnyString: true,
|
|
3137
|
-
recommendedValues: ["1"]
|
|
3138
|
-
},
|
|
3139
|
-
prompt: {
|
|
3140
|
-
type: "string",
|
|
3141
|
-
required: true
|
|
3142
|
-
},
|
|
3143
|
-
aspect_ratio: {
|
|
3144
|
-
type: "string",
|
|
3145
|
-
required: true,
|
|
3146
|
-
enum: ["1:1", "16:9", "9:16"]
|
|
3147
|
-
}
|
|
3148
|
-
}
|
|
3149
|
-
}
|
|
3150
|
-
};
|
|
3151
|
-
|
|
3152
|
-
// src/models/t2i/definitions/google__nano-banana-pro.json
|
|
3153
|
-
var google_nano_banana_pro_default = {
|
|
3154
|
-
modelId: "google/nano-banana-pro",
|
|
3155
|
-
provider: "replicate",
|
|
3156
|
-
displayName: "Nano Banana Pro",
|
|
3157
|
-
category: "image-generation",
|
|
3158
|
-
pricingSourceUrl: "https://replicate.com/google/nano-banana-pro",
|
|
3159
|
-
pricingNotes: "Replicate pricing by target resolution: 1K/2K=$0.15 per output image, 4K=$0.30 per output image, fallback tier=$0.035 per output image.",
|
|
3160
|
-
pricing: {
|
|
3161
|
-
usdPerSecond: null,
|
|
3162
|
-
usdPer1kInputTokens: null,
|
|
3163
|
-
usdPer1kOutputTokens: null
|
|
3164
|
-
},
|
|
3165
|
-
pricingRules: {
|
|
3166
|
-
basis: "output_image_resolution",
|
|
3167
|
-
tiers: [
|
|
3168
|
-
{
|
|
3169
|
-
resolution: "1K",
|
|
3170
|
-
usdPerImage: 0.15
|
|
3171
|
-
},
|
|
3172
|
-
{
|
|
3173
|
-
resolution: "2K",
|
|
3174
|
-
usdPerImage: 0.15
|
|
3175
|
-
},
|
|
3176
|
-
{
|
|
3177
|
-
resolution: "4K",
|
|
3178
|
-
usdPerImage: 0.3
|
|
3179
|
-
},
|
|
3180
|
-
{
|
|
3181
|
-
resolution: "fallback",
|
|
3182
|
-
usdPerImage: 0.035
|
|
3183
|
-
}
|
|
3184
|
-
]
|
|
3185
|
-
},
|
|
3186
|
-
inputOptions: {
|
|
3187
|
-
userConfigurable: ["resolution", "output_format", "safety_filter_level", "allow_fallback_model"],
|
|
3188
|
-
pipelineManaged: ["prompt", "aspect_ratio"],
|
|
3189
|
-
fields: {
|
|
3190
|
-
resolution: {
|
|
3191
|
-
type: "string",
|
|
3192
|
-
default: "2K",
|
|
3193
|
-
enum: ["1K", "2K", "4K"]
|
|
3194
|
-
},
|
|
3195
|
-
output_format: {
|
|
3196
|
-
type: "string",
|
|
3197
|
-
default: "jpg",
|
|
3198
|
-
enum: ["jpg", "png", "webp"]
|
|
3199
|
-
},
|
|
3200
|
-
safety_filter_level: {
|
|
3201
|
-
type: "string",
|
|
3202
|
-
default: "block_only_high",
|
|
3203
|
-
enum: ["block_low_and_above", "block_medium_and_above", "block_only_high"]
|
|
3204
|
-
},
|
|
3205
|
-
allow_fallback_model: {
|
|
3206
|
-
type: "boolean",
|
|
3207
|
-
default: false
|
|
3208
|
-
},
|
|
3209
|
-
prompt: {
|
|
3210
|
-
type: "string",
|
|
3211
|
-
required: true
|
|
3212
|
-
},
|
|
3213
|
-
aspect_ratio: {
|
|
3214
|
-
type: "string",
|
|
3215
|
-
required: true,
|
|
3216
|
-
enum: ["1:1", "16:9", "9:16"]
|
|
3217
|
-
}
|
|
3218
|
-
}
|
|
3219
|
-
}
|
|
3220
|
-
};
|
|
3221
|
-
|
|
3222
|
-
// src/models/t2i/definitions/prunaai__z-image-turbo.json
|
|
3223
|
-
var prunaai_z_image_turbo_default = {
|
|
3224
|
-
modelId: "prunaai/z-image-turbo",
|
|
3225
|
-
provider: "replicate",
|
|
3226
|
-
displayName: "Z Image Turbo",
|
|
3227
|
-
category: "image-generation",
|
|
3228
|
-
pricingSourceUrl: "https://replicate.com/prunaai/z-image-turbo",
|
|
3229
|
-
pricingNotes: "Replicate pricing is tiered by output image resolution (megapixels).",
|
|
3230
|
-
pricing: {
|
|
3231
|
-
usdPerSecond: null,
|
|
3232
|
-
usdPer1kInputTokens: null,
|
|
3233
|
-
usdPer1kOutputTokens: null
|
|
3234
|
-
},
|
|
3235
|
-
pricingRules: {
|
|
3236
|
-
basis: "output_image_megapixels",
|
|
3237
|
-
tiers: [
|
|
3238
|
-
{
|
|
3239
|
-
maxMegapixels: 0.5,
|
|
3240
|
-
usdPerImage: 25e-4
|
|
3241
|
-
},
|
|
3242
|
-
{
|
|
3243
|
-
maxMegapixels: 1,
|
|
3244
|
-
usdPerImage: 5e-3
|
|
3245
|
-
},
|
|
3246
|
-
{
|
|
3247
|
-
maxMegapixels: 2,
|
|
3248
|
-
usdPerImage: 0.01
|
|
3249
|
-
},
|
|
3250
|
-
{
|
|
3251
|
-
maxMegapixels: 3,
|
|
3252
|
-
usdPerImage: 0.015
|
|
3253
|
-
},
|
|
3254
|
-
{
|
|
3255
|
-
maxMegapixels: 4,
|
|
3256
|
-
usdPerImage: 0.02
|
|
3257
|
-
}
|
|
3258
|
-
]
|
|
3259
|
-
},
|
|
3260
|
-
inputOptions: {
|
|
3261
|
-
userConfigurable: ["num_inference_steps", "guidance_scale", "seed", "go_fast", "output_format", "output_quality"],
|
|
3262
|
-
pipelineManaged: ["prompt", "width", "height"],
|
|
3263
|
-
fields: {
|
|
3264
|
-
num_inference_steps: {
|
|
3265
|
-
type: "integer",
|
|
3266
|
-
default: 8,
|
|
3267
|
-
minimum: 1,
|
|
3268
|
-
maximum: 50
|
|
3269
|
-
},
|
|
3270
|
-
guidance_scale: {
|
|
3271
|
-
type: "number",
|
|
3272
|
-
default: 0,
|
|
3273
|
-
minimum: 0,
|
|
3274
|
-
maximum: 20
|
|
3275
|
-
},
|
|
3276
|
-
seed: {
|
|
3277
|
-
type: "integer",
|
|
3278
|
-
nullable: true
|
|
3279
|
-
},
|
|
3280
|
-
go_fast: {
|
|
3281
|
-
type: "boolean",
|
|
3282
|
-
default: false
|
|
3283
|
-
},
|
|
3284
|
-
output_format: {
|
|
3285
|
-
type: "string",
|
|
3286
|
-
default: "jpg",
|
|
3287
|
-
enum: ["jpg", "jpeg", "png", "webp"]
|
|
3288
|
-
},
|
|
3289
|
-
output_quality: {
|
|
3290
|
-
type: "integer",
|
|
3291
|
-
default: 80,
|
|
3292
|
-
minimum: 0,
|
|
3293
|
-
maximum: 100
|
|
3294
|
-
},
|
|
3295
|
-
prompt: {
|
|
3296
|
-
type: "string",
|
|
3297
|
-
required: true
|
|
3298
|
-
},
|
|
3299
|
-
width: {
|
|
3300
|
-
type: "integer",
|
|
3301
|
-
required: true,
|
|
3302
|
-
minimum: 64,
|
|
3303
|
-
maximum: 2048
|
|
3304
|
-
},
|
|
3305
|
-
height: {
|
|
3306
|
-
type: "integer",
|
|
3307
|
-
required: true,
|
|
3308
|
-
minimum: 64,
|
|
3309
|
-
maximum: 2048
|
|
3310
|
-
}
|
|
3311
|
-
}
|
|
3312
|
-
}
|
|
3313
|
-
};
|
|
3314
|
-
|
|
3315
|
-
// src/models/t2i/registry.ts
|
|
3316
|
-
var modelDefinitions = [
|
|
3317
|
-
black_forest_labs_flux_schnell_default,
|
|
3318
|
-
black_forest_labs_flux_2_pro_default,
|
|
3319
|
-
bytedance_seedream_4_default,
|
|
3320
|
-
google_nano_banana_pro_default,
|
|
3321
|
-
prunaai_z_image_turbo_default
|
|
3322
|
-
];
|
|
3323
|
-
function getSupportedT2IModels() {
|
|
3324
|
-
return modelDefinitions;
|
|
3325
|
-
}
|
|
3326
|
-
function getT2IModel(modelId) {
|
|
3327
|
-
const model = modelDefinitions.find((entry) => entry.modelId === modelId);
|
|
3328
|
-
if (!model) {
|
|
3329
|
-
throw new Error(`Unsupported T2I model: ${modelId}`);
|
|
3330
|
-
}
|
|
3331
|
-
return model;
|
|
3332
|
-
}
|
|
3333
|
-
|
|
3334
|
-
// src/models/t2i/options.ts
|
|
3335
|
-
function getT2IFieldDefault(modelId, fieldName) {
|
|
3336
|
-
const field = getFieldDefinition(modelId, fieldName);
|
|
3337
|
-
return field?.default;
|
|
3338
|
-
}
|
|
3339
|
-
function sanitizeT2IOverrides(modelId, overrides) {
|
|
3340
|
-
const model = getT2IModel(modelId);
|
|
3341
|
-
return Object.fromEntries(
|
|
3342
|
-
Object.entries(overrides).flatMap(([fieldName, value2]) => {
|
|
3343
|
-
if (!model.inputOptions.userConfigurable.includes(fieldName)) {
|
|
3344
|
-
return [];
|
|
3345
|
-
}
|
|
3346
|
-
const sanitized = coerceT2IFieldValue(modelId, fieldName, value2);
|
|
3347
|
-
return sanitized === void 0 ? [] : [[fieldName, sanitized]];
|
|
3348
|
-
})
|
|
3349
|
-
);
|
|
3350
|
-
}
|
|
3351
|
-
function coerceT2IFieldValue(modelId, fieldName, value2) {
|
|
3352
|
-
const field = getFieldDefinition(modelId, fieldName);
|
|
3353
|
-
if (!field) {
|
|
3354
|
-
return void 0;
|
|
3355
|
-
}
|
|
3356
|
-
if (value2 === null) {
|
|
3357
|
-
return field.nullable ? null : void 0;
|
|
3358
|
-
}
|
|
3359
|
-
if (value2 === void 0) {
|
|
3360
|
-
return void 0;
|
|
3361
|
-
}
|
|
3362
|
-
if (field.type === "boolean") {
|
|
3363
|
-
return coerceBoolean(value2);
|
|
3364
|
-
}
|
|
3365
|
-
if (field.type === "integer") {
|
|
3366
|
-
return clampNumber(parseFiniteNumber(value2, true), field.minimum, field.maximum, true, field.nullable);
|
|
3367
|
-
}
|
|
3368
|
-
if (field.type === "number") {
|
|
3369
|
-
return clampNumber(parseFiniteNumber(value2, false), field.minimum, field.maximum, false, field.nullable);
|
|
3370
|
-
}
|
|
3371
|
-
if (field.type === "string" || typeof value2 === "string") {
|
|
3372
|
-
const normalized = String(value2).trim();
|
|
3373
|
-
if (!normalized) {
|
|
3374
|
-
return field.nullable ? null : void 0;
|
|
3375
|
-
}
|
|
3376
|
-
if (field.enum && field.enum.length > 0 && !field.allowAnyString && !field.enum.includes(normalized)) {
|
|
3377
|
-
return void 0;
|
|
3378
|
-
}
|
|
3379
|
-
return normalized;
|
|
3380
|
-
}
|
|
3381
|
-
return value2;
|
|
3382
|
-
}
|
|
3383
|
-
function getFieldDefinition(modelId, fieldName) {
|
|
3384
|
-
const model = getT2IModel(modelId);
|
|
3385
|
-
return model.inputOptions.fields[fieldName];
|
|
3386
|
-
}
|
|
3387
|
-
function coerceBoolean(value2) {
|
|
3388
|
-
if (typeof value2 === "boolean") {
|
|
3389
|
-
return value2;
|
|
3390
|
-
}
|
|
3391
|
-
if (typeof value2 === "string") {
|
|
3392
|
-
if (value2.trim() === "true") {
|
|
3393
|
-
return true;
|
|
3394
|
-
}
|
|
3395
|
-
if (value2.trim() === "false") {
|
|
3396
|
-
return false;
|
|
3397
|
-
}
|
|
3398
|
-
}
|
|
3399
|
-
return void 0;
|
|
3400
|
-
}
|
|
3401
|
-
function parseFiniteNumber(value2, integer) {
|
|
3402
|
-
const parsed = typeof value2 === "number" ? value2 : Number(String(value2).trim());
|
|
3403
|
-
if (!Number.isFinite(parsed)) {
|
|
3404
|
-
return void 0;
|
|
3405
|
-
}
|
|
3406
|
-
return integer ? Math.round(parsed) : parsed;
|
|
3407
|
-
}
|
|
3408
|
-
function clampNumber(value2, minimum, maximum, integer, nullable) {
|
|
3409
|
-
if (value2 === void 0) {
|
|
3410
|
-
return nullable ? null : void 0;
|
|
3411
|
-
}
|
|
3412
|
-
let next = value2;
|
|
3413
|
-
if (minimum !== void 0) {
|
|
3414
|
-
next = Math.max(minimum, next);
|
|
3415
|
-
}
|
|
3416
|
-
if (maximum !== void 0) {
|
|
3417
|
-
next = Math.min(maximum, next);
|
|
3418
|
-
}
|
|
3419
|
-
return integer ? Math.round(next) : next;
|
|
3420
|
-
}
|
|
3421
|
-
|
|
3422
2921
|
// src/pipeline/analytics.ts
|
|
3423
2922
|
var LLM_USD_PER_1K_TOKENS = {
|
|
3424
2923
|
// AUTO-GENERATED:OPENROUTER_PRICING_START
|
|
3425
|
-
// Last refreshed: 2026-
|
|
2924
|
+
// Last refreshed: 2026-05-05
|
|
3426
2925
|
// Source: https://openrouter.ai/api/v1/models (per-token USD converted to per-1k-token USD)
|
|
3427
2926
|
"anthropic/claude-3.5-sonnet": { input: 6e-3, output: 0.03 },
|
|
3428
2927
|
"deepseek/deepseek-chat": { input: 32e-5, output: 89e-5 },
|
|
3429
|
-
"
|
|
2928
|
+
"deepseek/deepseek-v4-pro": { input: 435e-6, output: 87e-5 },
|
|
2929
|
+
"moonshotai/kimi-k2.5": { input: 44e-5, output: 2e-3 },
|
|
3430
2930
|
"openai/gpt-4o-mini": { input: 15e-5, output: 6e-4 }
|
|
3431
2931
|
// AUTO-GENERATED:OPENROUTER_PRICING_END
|
|
3432
2932
|
};
|
|
@@ -3443,64 +2943,6 @@ function estimateLlmCostUsd(modelId, usage) {
|
|
|
3443
2943
|
const usd = promptTokens / 1e3 * pricing.input + completionTokens / 1e3 * pricing.output;
|
|
3444
2944
|
return { usd, source: "estimated" };
|
|
3445
2945
|
}
|
|
3446
|
-
function estimateImageCostUsd(modelId, input, imageCount) {
|
|
3447
|
-
const model = getT2IModel(modelId);
|
|
3448
|
-
const pricingRules = "pricingRules" in model ? model.pricingRules : void 0;
|
|
3449
|
-
if (!pricingRules) {
|
|
3450
|
-
return { usd: null, source: "unavailable" };
|
|
3451
|
-
}
|
|
3452
|
-
if (pricingRules.basis === "output_image_count" && "usdPerImage" in pricingRules && typeof pricingRules.usdPerImage === "number") {
|
|
3453
|
-
return {
|
|
3454
|
-
usd: pricingRules.usdPerImage * imageCount,
|
|
3455
|
-
source: "estimated"
|
|
3456
|
-
};
|
|
3457
|
-
}
|
|
3458
|
-
if (pricingRules.basis === "output_image_resolution" && "tiers" in pricingRules && Array.isArray(pricingRules.tiers)) {
|
|
3459
|
-
const resolution = typeof input.resolution === "string" ? input.resolution : "fallback";
|
|
3460
|
-
const tiers = pricingRules.tiers;
|
|
3461
|
-
const tier = tiers.find((entry) => entry.resolution === resolution) ?? tiers.find((entry) => entry.resolution === "fallback");
|
|
3462
|
-
if (tier?.usdPerImage !== void 0) {
|
|
3463
|
-
return {
|
|
3464
|
-
usd: tier.usdPerImage * imageCount,
|
|
3465
|
-
source: "estimated"
|
|
3466
|
-
};
|
|
3467
|
-
}
|
|
3468
|
-
}
|
|
3469
|
-
if (pricingRules.basis === "output_image_megapixels" && "tiers" in pricingRules && Array.isArray(pricingRules.tiers)) {
|
|
3470
|
-
const megapixels = resolveMegapixels(input);
|
|
3471
|
-
const tiers = pricingRules.tiers;
|
|
3472
|
-
const tier = tiers.find((entry) => megapixels <= entry.maxMegapixels);
|
|
3473
|
-
if (tier?.usdPerImage !== void 0) {
|
|
3474
|
-
return {
|
|
3475
|
-
usd: tier.usdPerImage * imageCount,
|
|
3476
|
-
source: "estimated"
|
|
3477
|
-
};
|
|
3478
|
-
}
|
|
3479
|
-
}
|
|
3480
|
-
return { usd: null, source: "unavailable" };
|
|
3481
|
-
}
|
|
3482
|
-
function resolveMegapixels(input) {
|
|
3483
|
-
const explicit = toNumber(input.megapixels);
|
|
3484
|
-
if (explicit !== null) {
|
|
3485
|
-
return explicit;
|
|
3486
|
-
}
|
|
3487
|
-
const width = toNumber(input.width);
|
|
3488
|
-
const height = toNumber(input.height);
|
|
3489
|
-
if (width !== null && height !== null && width > 0 && height > 0) {
|
|
3490
|
-
return width * height / 1e6;
|
|
3491
|
-
}
|
|
3492
|
-
return 1;
|
|
3493
|
-
}
|
|
3494
|
-
function toNumber(value2) {
|
|
3495
|
-
if (typeof value2 === "number" && Number.isFinite(value2)) {
|
|
3496
|
-
return value2;
|
|
3497
|
-
}
|
|
3498
|
-
if (typeof value2 === "string" && value2.trim() !== "") {
|
|
3499
|
-
const parsed = Number(value2);
|
|
3500
|
-
return Number.isFinite(parsed) ? parsed : null;
|
|
3501
|
-
}
|
|
3502
|
-
return null;
|
|
3503
|
-
}
|
|
3504
2946
|
function sumKnownCosts(values) {
|
|
3505
2947
|
const known = values.filter((value2) => typeof value2 === "number");
|
|
3506
2948
|
if (known.length !== values.length) {
|
|
@@ -3514,8 +2956,52 @@ function sumKnownCosts(values) {
|
|
|
3514
2956
|
|
|
3515
2957
|
// src/images/renderImages.ts
|
|
3516
2958
|
var MIN_IMAGE_BYTES = 1024;
|
|
2959
|
+
function selectImageSlots(plan, sections, options) {
|
|
2960
|
+
const sectionCount = sections.length;
|
|
2961
|
+
let defaultInlineCount;
|
|
2962
|
+
if (sectionCount <= 3) {
|
|
2963
|
+
defaultInlineCount = 0;
|
|
2964
|
+
} else if (sectionCount <= 6) {
|
|
2965
|
+
defaultInlineCount = 1;
|
|
2966
|
+
} else {
|
|
2967
|
+
defaultInlineCount = 2;
|
|
2968
|
+
}
|
|
2969
|
+
const availableInlineCount = Math.min(defaultInlineCount, plan.inlineImages.length, sectionCount);
|
|
2970
|
+
let inlineCount;
|
|
2971
|
+
const maxImages = options?.maxImages;
|
|
2972
|
+
if (maxImages !== void 0 && maxImages >= 1) {
|
|
2973
|
+
inlineCount = Math.min(availableInlineCount, Math.max(0, maxImages - 1));
|
|
2974
|
+
} else {
|
|
2975
|
+
inlineCount = availableInlineCount;
|
|
2976
|
+
}
|
|
2977
|
+
const slots = [
|
|
2978
|
+
{
|
|
2979
|
+
id: "cover",
|
|
2980
|
+
kind: "cover",
|
|
2981
|
+
prompt: "",
|
|
2982
|
+
description: plan.coverImageDescription,
|
|
2983
|
+
anchorAfterSection: null
|
|
2984
|
+
}
|
|
2985
|
+
];
|
|
2986
|
+
for (let i = 0; i < inlineCount; i++) {
|
|
2987
|
+
const anchorAfterSection = Math.max(
|
|
2988
|
+
1,
|
|
2989
|
+
Math.min(sectionCount, Math.round((i + 1) / (inlineCount + 1) * sectionCount))
|
|
2990
|
+
);
|
|
2991
|
+
slots.push({
|
|
2992
|
+
id: `inline-${i + 1}`,
|
|
2993
|
+
kind: "inline",
|
|
2994
|
+
prompt: "",
|
|
2995
|
+
description: plan.inlineImages[i]?.description ?? "",
|
|
2996
|
+
anchorAfterSection
|
|
2997
|
+
});
|
|
2998
|
+
}
|
|
2999
|
+
return slots;
|
|
3000
|
+
}
|
|
3517
3001
|
async function expandImagePrompts({
|
|
3518
|
-
|
|
3002
|
+
slots,
|
|
3003
|
+
planContext,
|
|
3004
|
+
sections,
|
|
3519
3005
|
settings,
|
|
3520
3006
|
openRouter,
|
|
3521
3007
|
dryRun,
|
|
@@ -3523,16 +3009,16 @@ async function expandImagePrompts({
|
|
|
3523
3009
|
onPromptComplete,
|
|
3524
3010
|
onInteraction
|
|
3525
3011
|
}) {
|
|
3526
|
-
const imageSlots = buildImageSlots(plan);
|
|
3527
3012
|
const prompts = [];
|
|
3528
|
-
for (let index = 0; index <
|
|
3529
|
-
const image =
|
|
3530
|
-
onProgress?.(`Expanding prompt ${index + 1}/${
|
|
3013
|
+
for (let index = 0; index < slots.length; index += 1) {
|
|
3014
|
+
const image = slots[index];
|
|
3015
|
+
onProgress?.(`Expanding prompt ${index + 1}/${slots.length}: ${image.kind === "cover" ? "cover image" : image.description}`);
|
|
3016
|
+
const sectionForImage = image.kind === "inline" && image.anchorAfterSection != null && sections ? sections[image.anchorAfterSection - 1] : void 0;
|
|
3531
3017
|
if (dryRun || !openRouter) {
|
|
3532
3018
|
const dryRunStartMs = Date.now();
|
|
3533
3019
|
prompts.push({
|
|
3534
3020
|
...image,
|
|
3535
|
-
prompt: `${image.description}
|
|
3021
|
+
prompt: `${image.description}`
|
|
3536
3022
|
});
|
|
3537
3023
|
onPromptComplete?.({
|
|
3538
3024
|
imageId: image.id,
|
|
@@ -3554,7 +3040,7 @@ async function expandImagePrompts({
|
|
|
3554
3040
|
const response = await openRouter.requestStructured({
|
|
3555
3041
|
schemaName: "image_prompt",
|
|
3556
3042
|
schema: imagePromptSchema,
|
|
3557
|
-
messages: buildImagePromptMessages(
|
|
3043
|
+
messages: buildImagePromptMessages(planContext, image, sectionForImage),
|
|
3558
3044
|
settings,
|
|
3559
3045
|
interactionContext: {
|
|
3560
3046
|
stageId: "image-prompts",
|
|
@@ -3599,29 +3085,26 @@ async function expandImagePrompts({
|
|
|
3599
3085
|
async function renderExpandedImages({
|
|
3600
3086
|
prompts,
|
|
3601
3087
|
settings,
|
|
3602
|
-
|
|
3088
|
+
limn,
|
|
3603
3089
|
markdownPath,
|
|
3604
3090
|
assetDir,
|
|
3605
3091
|
dryRun,
|
|
3606
3092
|
onProgress,
|
|
3607
3093
|
onRenderComplete,
|
|
3608
|
-
onInteraction
|
|
3609
|
-
onRetry
|
|
3094
|
+
onInteraction
|
|
3610
3095
|
}) {
|
|
3611
3096
|
const renderedImages = [];
|
|
3612
3097
|
for (let index = 0; index < prompts.length; index += 1) {
|
|
3613
3098
|
const prompt = prompts[index];
|
|
3614
3099
|
onProgress?.(`Rendering image ${index + 1}/${prompts.length} with ${settings.t2i.modelId}`);
|
|
3615
|
-
const fileName = `${prompt.kind === "cover" ? "cover" : `inline-${prompt.anchorAfterSection}`}-${index + 1}
|
|
3100
|
+
const fileName = `${prompt.kind === "cover" ? "cover" : `inline-${prompt.anchorAfterSection}`}-${index + 1}.png`;
|
|
3616
3101
|
const outputPath = path6.join(assetDir, fileName);
|
|
3617
|
-
if (dryRun || !
|
|
3102
|
+
if (dryRun || !limn) {
|
|
3618
3103
|
const dryRunStartMs = Date.now();
|
|
3619
3104
|
await writeFile4(outputPath, `Placeholder image for: ${prompt.prompt}
|
|
3620
3105
|
`, "utf8");
|
|
3621
3106
|
const outputBytes = Buffer.byteLength(`Placeholder image for: ${prompt.prompt}
|
|
3622
3107
|
`, "utf8");
|
|
3623
|
-
const dryRunInput = createReplicateInput(settings, prompt.prompt, prompt.kind);
|
|
3624
|
-
const dryRunCost = estimateImageCostUsd(settings.t2i.modelId, dryRunInput, 1);
|
|
3625
3108
|
renderedImages.push({
|
|
3626
3109
|
...prompt,
|
|
3627
3110
|
outputPath,
|
|
@@ -3636,13 +3119,13 @@ async function renderExpandedImages({
|
|
|
3636
3119
|
retries: 0,
|
|
3637
3120
|
retryBackoffMs: 0,
|
|
3638
3121
|
outputBytes,
|
|
3639
|
-
costUsd:
|
|
3640
|
-
costSource:
|
|
3122
|
+
costUsd: null,
|
|
3123
|
+
costSource: "unavailable"
|
|
3641
3124
|
});
|
|
3642
3125
|
onInteraction?.({
|
|
3643
3126
|
stageId: "images",
|
|
3644
3127
|
operationId: `images:${prompt.id}`,
|
|
3645
|
-
provider: "
|
|
3128
|
+
provider: "limn-dry-run",
|
|
3646
3129
|
modelId: settings.t2i.modelId,
|
|
3647
3130
|
kind: prompt.kind,
|
|
3648
3131
|
startedAt: new Date(dryRunStartMs).toISOString(),
|
|
@@ -3653,92 +3136,79 @@ async function renderExpandedImages({
|
|
|
3653
3136
|
retryBackoffMs: 0,
|
|
3654
3137
|
status: "succeeded",
|
|
3655
3138
|
prompt: prompt.prompt,
|
|
3656
|
-
input:
|
|
3139
|
+
input: {},
|
|
3657
3140
|
errorMessage: null
|
|
3658
3141
|
});
|
|
3659
3142
|
continue;
|
|
3660
3143
|
}
|
|
3661
|
-
const
|
|
3144
|
+
const family = settings.t2i.modelId;
|
|
3662
3145
|
const renderStartedAtMs = Date.now();
|
|
3663
|
-
let runDurationMs = 0;
|
|
3664
|
-
let runAttempts = 1;
|
|
3665
|
-
let runRetries = 0;
|
|
3666
|
-
let runRetryBackoffMs = 0;
|
|
3667
3146
|
try {
|
|
3668
|
-
const
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
runAttempts = metrics.attempts;
|
|
3672
|
-
runRetries = metrics.retries;
|
|
3673
|
-
runRetryBackoffMs = metrics.retryBackoffMs;
|
|
3674
|
-
},
|
|
3675
|
-
onRetry(event) {
|
|
3676
|
-
onRetry?.({
|
|
3677
|
-
imageId: prompt.id,
|
|
3678
|
-
kind: prompt.kind,
|
|
3679
|
-
retries: event.retries,
|
|
3680
|
-
errorMessage: event.errorMessage
|
|
3681
|
-
});
|
|
3682
|
-
}
|
|
3147
|
+
const result = await limn.generate(prompt.prompt, family, {
|
|
3148
|
+
replicateModel: settings.t2i.modelId,
|
|
3149
|
+
aspectRatio: "16:9"
|
|
3683
3150
|
});
|
|
3684
|
-
const
|
|
3685
|
-
|
|
3151
|
+
const ext = mimeTypeToExtension(result.mimeType);
|
|
3152
|
+
const liveFileName = `${prompt.kind === "cover" ? "cover" : `inline-${prompt.anchorAfterSection}`}-${index + 1}.${ext}`;
|
|
3153
|
+
const liveOutputPath = path6.join(assetDir, liveFileName);
|
|
3154
|
+
if (result.image.byteLength < MIN_IMAGE_BYTES) {
|
|
3686
3155
|
throw new Error(
|
|
3687
|
-
`Image ${index + 1} download appears corrupted: only ${
|
|
3156
|
+
`Image ${index + 1} download appears corrupted: only ${result.image.byteLength} bytes received.`
|
|
3688
3157
|
);
|
|
3689
3158
|
}
|
|
3690
|
-
await writeFile4(
|
|
3159
|
+
await writeFile4(liveOutputPath, result.image);
|
|
3691
3160
|
renderedImages.push({
|
|
3692
3161
|
...prompt,
|
|
3693
|
-
outputPath,
|
|
3694
|
-
relativePath: relativeAssetPath(markdownPath,
|
|
3162
|
+
outputPath: liveOutputPath,
|
|
3163
|
+
relativePath: relativeAssetPath(markdownPath, liveOutputPath)
|
|
3695
3164
|
});
|
|
3696
|
-
const
|
|
3165
|
+
const costSource = result.analytics.costSource === "unknown" ? "unavailable" : "estimated";
|
|
3697
3166
|
onRenderComplete?.({
|
|
3698
3167
|
imageId: prompt.id,
|
|
3699
3168
|
kind: prompt.kind,
|
|
3700
|
-
modelId:
|
|
3701
|
-
durationMs:
|
|
3702
|
-
attempts:
|
|
3703
|
-
retries:
|
|
3704
|
-
retryBackoffMs:
|
|
3705
|
-
outputBytes:
|
|
3706
|
-
costUsd:
|
|
3707
|
-
costSource
|
|
3169
|
+
modelId: result.modelSlug,
|
|
3170
|
+
durationMs: result.analytics.totalDurationMs,
|
|
3171
|
+
attempts: 1,
|
|
3172
|
+
retries: 0,
|
|
3173
|
+
retryBackoffMs: 0,
|
|
3174
|
+
outputBytes: result.image.byteLength,
|
|
3175
|
+
costUsd: result.analytics.totalEstimatedCostUsd,
|
|
3176
|
+
costSource
|
|
3708
3177
|
});
|
|
3709
3178
|
onInteraction?.({
|
|
3710
3179
|
stageId: "images",
|
|
3711
3180
|
operationId: `images:${prompt.id}`,
|
|
3712
|
-
provider: "
|
|
3713
|
-
modelId:
|
|
3181
|
+
provider: "limn",
|
|
3182
|
+
modelId: result.modelSlug,
|
|
3714
3183
|
kind: prompt.kind,
|
|
3715
3184
|
startedAt: new Date(renderStartedAtMs).toISOString(),
|
|
3716
3185
|
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3717
|
-
durationMs:
|
|
3718
|
-
attempts:
|
|
3719
|
-
retries:
|
|
3720
|
-
retryBackoffMs:
|
|
3186
|
+
durationMs: result.analytics.totalDurationMs,
|
|
3187
|
+
attempts: 1,
|
|
3188
|
+
retries: 0,
|
|
3189
|
+
retryBackoffMs: 0,
|
|
3721
3190
|
status: "succeeded",
|
|
3722
3191
|
prompt: prompt.prompt,
|
|
3723
|
-
input,
|
|
3192
|
+
input: {},
|
|
3724
3193
|
errorMessage: null
|
|
3725
3194
|
});
|
|
3726
3195
|
} catch (error) {
|
|
3196
|
+
const durationMs = Date.now() - renderStartedAtMs;
|
|
3727
3197
|
onInteraction?.({
|
|
3728
3198
|
stageId: "images",
|
|
3729
3199
|
operationId: `images:${prompt.id}`,
|
|
3730
|
-
provider: "
|
|
3200
|
+
provider: "limn",
|
|
3731
3201
|
modelId: settings.t2i.modelId,
|
|
3732
3202
|
kind: prompt.kind,
|
|
3733
3203
|
startedAt: new Date(renderStartedAtMs).toISOString(),
|
|
3734
3204
|
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3735
|
-
durationMs
|
|
3736
|
-
attempts:
|
|
3737
|
-
retries:
|
|
3738
|
-
retryBackoffMs:
|
|
3205
|
+
durationMs,
|
|
3206
|
+
attempts: 1,
|
|
3207
|
+
retries: 0,
|
|
3208
|
+
retryBackoffMs: 0,
|
|
3739
3209
|
status: "failed",
|
|
3740
3210
|
prompt: prompt.prompt,
|
|
3741
|
-
input,
|
|
3211
|
+
input: {},
|
|
3742
3212
|
errorMessage: error instanceof Error ? error.message : "Unknown image render error."
|
|
3743
3213
|
});
|
|
3744
3214
|
throw error;
|
|
@@ -3770,146 +3240,10 @@ function mergeLlmMetrics(left, right) {
|
|
|
3770
3240
|
}
|
|
3771
3241
|
};
|
|
3772
3242
|
}
|
|
3773
|
-
function
|
|
3774
|
-
return
|
|
3775
|
-
|
|
3776
|
-
|
|
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
|
-
function createReplicateInput(settings, prompt, kind) {
|
|
3792
|
-
const model = getT2IModel(settings.t2i.modelId);
|
|
3793
|
-
const overrides = sanitizeT2IOverrides(settings.t2i.modelId, settings.t2i.inputOverrides);
|
|
3794
|
-
const input = { ...overrides, prompt };
|
|
3795
|
-
if (model.inputOptions.pipelineManaged.includes("aspect_ratio")) {
|
|
3796
|
-
input.aspect_ratio = kind === "cover" ? "16:9" : "16:9";
|
|
3797
|
-
}
|
|
3798
|
-
if (model.inputOptions.pipelineManaged.includes("width")) {
|
|
3799
|
-
input.width = 1536;
|
|
3800
|
-
}
|
|
3801
|
-
if (model.inputOptions.pipelineManaged.includes("height")) {
|
|
3802
|
-
input.height = 864;
|
|
3803
|
-
}
|
|
3804
|
-
if (!("output_format" in input)) {
|
|
3805
|
-
const fallback = getT2IFieldDefault(settings.t2i.modelId, "output_format");
|
|
3806
|
-
if (typeof fallback === "string") {
|
|
3807
|
-
input.output_format = fallback;
|
|
3808
|
-
}
|
|
3809
|
-
}
|
|
3810
|
-
if (!("num_outputs" in input) && "num_outputs" in model.inputOptions.fields) {
|
|
3811
|
-
input.num_outputs = coerceT2IFieldValue(settings.t2i.modelId, "num_outputs", getT2IFieldDefault(settings.t2i.modelId, "num_outputs")) ?? 1;
|
|
3812
|
-
}
|
|
3813
|
-
if (!("max_images" in input) && "max_images" in model.inputOptions.fields) {
|
|
3814
|
-
input.max_images = coerceT2IFieldValue(settings.t2i.modelId, "max_images", getT2IFieldDefault(settings.t2i.modelId, "max_images")) ?? 1;
|
|
3815
|
-
}
|
|
3816
|
-
return input;
|
|
3817
|
-
}
|
|
3818
|
-
function resolveOutputFormat(settings) {
|
|
3819
|
-
const outputFormat = coerceT2IFieldValue(settings.t2i.modelId, "output_format", settings.t2i.inputOverrides.output_format);
|
|
3820
|
-
if (typeof outputFormat === "string") {
|
|
3821
|
-
return outputFormat === "jpeg" ? "jpg" : outputFormat;
|
|
3822
|
-
}
|
|
3823
|
-
const fallback = getT2IFieldDefault(settings.t2i.modelId, "output_format");
|
|
3824
|
-
const normalizedFallback = typeof fallback === "string" ? fallback : "png";
|
|
3825
|
-
return normalizedFallback === "jpeg" ? "jpg" : normalizedFallback;
|
|
3826
|
-
}
|
|
3827
|
-
async function normalizeReplicateOutput(output) {
|
|
3828
|
-
const first = Array.isArray(output) ? output[0] : output;
|
|
3829
|
-
if (!first) {
|
|
3830
|
-
throw new Error("Replicate returned no image output.");
|
|
3831
|
-
}
|
|
3832
|
-
if (typeof first === "string") {
|
|
3833
|
-
return fetchBytes(first);
|
|
3834
|
-
}
|
|
3835
|
-
if (first instanceof URL) {
|
|
3836
|
-
return fetchBytes(first.toString());
|
|
3837
|
-
}
|
|
3838
|
-
if (first instanceof Uint8Array) {
|
|
3839
|
-
return first;
|
|
3840
|
-
}
|
|
3841
|
-
if (first instanceof ArrayBuffer) {
|
|
3842
|
-
return new Uint8Array(first);
|
|
3843
|
-
}
|
|
3844
|
-
if (typeof Blob !== "undefined" && first instanceof Blob) {
|
|
3845
|
-
return new Uint8Array(await first.arrayBuffer());
|
|
3846
|
-
}
|
|
3847
|
-
if (typeof ReadableStream !== "undefined" && first instanceof ReadableStream) {
|
|
3848
|
-
return new Uint8Array(await new Response(first).arrayBuffer());
|
|
3849
|
-
}
|
|
3850
|
-
const fromBlobMethod = await maybeBytesFromBlobMethod(first);
|
|
3851
|
-
if (fromBlobMethod) {
|
|
3852
|
-
return fromBlobMethod;
|
|
3853
|
-
}
|
|
3854
|
-
const fromUrlMethod = await maybeBytesFromUrlMethod(first);
|
|
3855
|
-
if (fromUrlMethod) {
|
|
3856
|
-
return fromUrlMethod;
|
|
3857
|
-
}
|
|
3858
|
-
const fromArrayBufferMethod = await maybeBytesFromArrayBufferMethod(first);
|
|
3859
|
-
if (fromArrayBufferMethod) {
|
|
3860
|
-
return fromArrayBufferMethod;
|
|
3861
|
-
}
|
|
3862
|
-
throw new Error("Unsupported Replicate output format.");
|
|
3863
|
-
}
|
|
3864
|
-
async function maybeBytesFromBlobMethod(value2) {
|
|
3865
|
-
if (!isRecord(value2) || typeof value2.blob !== "function") {
|
|
3866
|
-
return null;
|
|
3867
|
-
}
|
|
3868
|
-
const blobLike = await value2.blob();
|
|
3869
|
-
if (typeof Blob === "undefined" || !(blobLike instanceof Blob)) {
|
|
3870
|
-
return null;
|
|
3871
|
-
}
|
|
3872
|
-
return new Uint8Array(await blobLike.arrayBuffer());
|
|
3873
|
-
}
|
|
3874
|
-
async function maybeBytesFromUrlMethod(value2) {
|
|
3875
|
-
if (!isRecord(value2) || typeof value2.url !== "function") {
|
|
3876
|
-
return null;
|
|
3877
|
-
}
|
|
3878
|
-
const urlLike = value2.url();
|
|
3879
|
-
if (typeof urlLike === "string") {
|
|
3880
|
-
return fetchBytes(urlLike);
|
|
3881
|
-
}
|
|
3882
|
-
if (urlLike instanceof URL) {
|
|
3883
|
-
return fetchBytes(urlLike.toString());
|
|
3884
|
-
}
|
|
3885
|
-
return null;
|
|
3886
|
-
}
|
|
3887
|
-
async function maybeBytesFromArrayBufferMethod(value2) {
|
|
3888
|
-
if (!isRecord(value2) || typeof value2.arrayBuffer !== "function") {
|
|
3889
|
-
return null;
|
|
3890
|
-
}
|
|
3891
|
-
const data = await value2.arrayBuffer();
|
|
3892
|
-
if (data instanceof ArrayBuffer) {
|
|
3893
|
-
return new Uint8Array(data);
|
|
3894
|
-
}
|
|
3895
|
-
return null;
|
|
3896
|
-
}
|
|
3897
|
-
function isRecord(value2) {
|
|
3898
|
-
return typeof value2 === "object" && value2 !== null;
|
|
3899
|
-
}
|
|
3900
|
-
async function fetchBytes(url) {
|
|
3901
|
-
const controller = new AbortController();
|
|
3902
|
-
const timer = setTimeout(() => controller.abort(), 6e4);
|
|
3903
|
-
timer.unref();
|
|
3904
|
-
try {
|
|
3905
|
-
const response = await fetch(url, { signal: controller.signal });
|
|
3906
|
-
if (!response.ok) {
|
|
3907
|
-
throw new Error(`Failed to download generated asset from ${url}`);
|
|
3908
|
-
}
|
|
3909
|
-
return new Uint8Array(await response.arrayBuffer());
|
|
3910
|
-
} finally {
|
|
3911
|
-
clearTimeout(timer);
|
|
3912
|
-
}
|
|
3243
|
+
function mimeTypeToExtension(mimeType) {
|
|
3244
|
+
if (mimeType === "image/jpeg") return "jpg";
|
|
3245
|
+
if (mimeType === "image/webp") return "webp";
|
|
3246
|
+
return "png";
|
|
3913
3247
|
}
|
|
3914
3248
|
|
|
3915
3249
|
// src/llm/openRouterClient.ts
|
|
@@ -3958,9 +3292,9 @@ var OpenRouterClient = class {
|
|
|
3958
3292
|
return structured;
|
|
3959
3293
|
} catch (parseError) {
|
|
3960
3294
|
if (attempt < 2 && shouldRetryStructuredParseError(parseError)) {
|
|
3961
|
-
const backoff =
|
|
3295
|
+
const backoff = backoffMs(attempt);
|
|
3962
3296
|
aggregatedMetrics = recordParseRetryMetrics(aggregatedMetrics, backoff);
|
|
3963
|
-
await
|
|
3297
|
+
await wait(backoff);
|
|
3964
3298
|
continue;
|
|
3965
3299
|
}
|
|
3966
3300
|
throw parseError;
|
|
@@ -4072,7 +3406,7 @@ var OpenRouterClient = class {
|
|
|
4072
3406
|
if (!response.ok) {
|
|
4073
3407
|
const message = json?.error?.message ?? `OpenRouter request failed with status ${response.status}`;
|
|
4074
3408
|
if (shouldRetryStatus(response.status) && attempt < 2) {
|
|
4075
|
-
const backoff =
|
|
3409
|
+
const backoff = backoffMs(attempt);
|
|
4076
3410
|
retries += 1;
|
|
4077
3411
|
retryBackoffMs += backoff;
|
|
4078
3412
|
onInteraction?.({
|
|
@@ -4092,14 +3426,14 @@ var OpenRouterClient = class {
|
|
|
4092
3426
|
responseBody: responseBodyRaw,
|
|
4093
3427
|
errorMessage: message
|
|
4094
3428
|
});
|
|
4095
|
-
await
|
|
3429
|
+
await wait(backoff);
|
|
4096
3430
|
continue;
|
|
4097
3431
|
}
|
|
4098
3432
|
throw new Error(message);
|
|
4099
3433
|
}
|
|
4100
3434
|
const content = json.choices?.[0]?.message?.content;
|
|
4101
3435
|
if (!content && attempt < 2) {
|
|
4102
|
-
const backoff =
|
|
3436
|
+
const backoff = backoffMs(attempt);
|
|
4103
3437
|
retries += 1;
|
|
4104
3438
|
retryBackoffMs += backoff;
|
|
4105
3439
|
onInteraction?.({
|
|
@@ -4119,7 +3453,7 @@ var OpenRouterClient = class {
|
|
|
4119
3453
|
responseBody: responseBodyRaw,
|
|
4120
3454
|
errorMessage: "OpenRouter returned an empty response."
|
|
4121
3455
|
});
|
|
4122
|
-
await
|
|
3456
|
+
await wait(backoff);
|
|
4123
3457
|
continue;
|
|
4124
3458
|
}
|
|
4125
3459
|
onInteraction?.({
|
|
@@ -4174,11 +3508,11 @@ var OpenRouterClient = class {
|
|
|
4174
3508
|
responseBody: responseBodyRaw,
|
|
4175
3509
|
errorMessage: lastError.message
|
|
4176
3510
|
});
|
|
4177
|
-
if (attempt < 2 &&
|
|
4178
|
-
const backoff =
|
|
3511
|
+
if (attempt < 2 && shouldRetryError(lastError)) {
|
|
3512
|
+
const backoff = backoffMs(attempt);
|
|
4179
3513
|
retries += 1;
|
|
4180
3514
|
retryBackoffMs += backoff;
|
|
4181
|
-
await
|
|
3515
|
+
await wait(backoff);
|
|
4182
3516
|
continue;
|
|
4183
3517
|
}
|
|
4184
3518
|
} finally {
|
|
@@ -4274,7 +3608,7 @@ function isStructuredOutputCompatibilityError(message) {
|
|
|
4274
3608
|
function shouldRetryStatus(status) {
|
|
4275
3609
|
return status === 408 || status === 409 || status === 429 || status >= 500;
|
|
4276
3610
|
}
|
|
4277
|
-
function
|
|
3611
|
+
function shouldRetryError(error) {
|
|
4278
3612
|
return /timeout|network|fetch|temporarily|aborted/i.test(error.message);
|
|
4279
3613
|
}
|
|
4280
3614
|
function shouldRetryStructuredParseError(error) {
|
|
@@ -4298,7 +3632,7 @@ function normalizeClientError(error, timeoutMs) {
|
|
|
4298
3632
|
}
|
|
4299
3633
|
return new Error("Unknown OpenRouter client error.");
|
|
4300
3634
|
}
|
|
4301
|
-
function
|
|
3635
|
+
function backoffMs(attempt) {
|
|
4302
3636
|
return 500 * (attempt + 1);
|
|
4303
3637
|
}
|
|
4304
3638
|
function aggregateLlmMetrics(total, next) {
|
|
@@ -4332,7 +3666,7 @@ function sumNullableNumber(left, right) {
|
|
|
4332
3666
|
}
|
|
4333
3667
|
return left + right;
|
|
4334
3668
|
}
|
|
4335
|
-
function
|
|
3669
|
+
function wait(ms) {
|
|
4336
3670
|
return new Promise((resolve) => {
|
|
4337
3671
|
setTimeout(resolve, ms);
|
|
4338
3672
|
});
|
|
@@ -4619,7 +3953,6 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4619
3953
|
const stageTracking = /* @__PURE__ */ new Map();
|
|
4620
3954
|
const stageRetryState = /* @__PURE__ */ new Map();
|
|
4621
3955
|
const llmOperationRetryState = /* @__PURE__ */ new Map();
|
|
4622
|
-
const imageOperationRetryState = /* @__PURE__ */ new Map();
|
|
4623
3956
|
stageTracking.set("shared-brief", {
|
|
4624
3957
|
startedAtMs: runStartedAtMs,
|
|
4625
3958
|
endedAtMs: null,
|
|
@@ -4695,7 +4028,11 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4695
4028
|
const openRouter = dryRun ? null : new OpenRouterClient(requireSecret(input.config.secrets.openRouterApiKey, "OpenRouter API key"));
|
|
4696
4029
|
const canRenderImagesLive = Boolean(input.config.secrets.replicateApiToken);
|
|
4697
4030
|
const imageDryRun = dryRun || !canRenderImagesLive;
|
|
4698
|
-
const
|
|
4031
|
+
const limn = imageDryRun ? null : new Limn({
|
|
4032
|
+
openrouterApiKey: input.config.secrets.openRouterApiKey ?? void 0,
|
|
4033
|
+
replicateApiKey: requireSecret(input.config.secrets.replicateApiToken, "Replicate API token"),
|
|
4034
|
+
openrouterModel: input.config.settings.model
|
|
4035
|
+
});
|
|
4699
4036
|
let contentBrief = writeSession.contentBrief;
|
|
4700
4037
|
let plan = writeSession.plan;
|
|
4701
4038
|
let text = writeSession.text;
|
|
@@ -4945,7 +4282,9 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4945
4282
|
options.onUpdate?.(cloneStages(stages));
|
|
4946
4283
|
} else {
|
|
4947
4284
|
imagePrompts = await expandImagePrompts({
|
|
4948
|
-
plan,
|
|
4285
|
+
slots: selectImageSlots(plan, text.sections, { maxImages: options.maxImages }),
|
|
4286
|
+
planContext: plan,
|
|
4287
|
+
sections: text.sections,
|
|
4949
4288
|
settings: input.config.settings,
|
|
4950
4289
|
openRouter,
|
|
4951
4290
|
dryRun,
|
|
@@ -5126,7 +4465,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5126
4465
|
const renderedImages = await renderExpandedImages({
|
|
5127
4466
|
prompts: imagePrompts,
|
|
5128
4467
|
settings: input.config.settings,
|
|
5129
|
-
|
|
4468
|
+
limn,
|
|
5130
4469
|
markdownPath: primaryMarkdownPath,
|
|
5131
4470
|
assetDir: sharedAssetDir,
|
|
5132
4471
|
dryRun: imageDryRun,
|
|
@@ -5149,15 +4488,6 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5149
4488
|
recordStageCost(stageTracking, "images", metrics.costUsd, metrics.costSource);
|
|
5150
4489
|
addStageRetries(stageTracking, "images", metrics.retries);
|
|
5151
4490
|
},
|
|
5152
|
-
onRetry(event) {
|
|
5153
|
-
const operationKey = `images:${event.imageId}`;
|
|
5154
|
-
const previousRetries = imageOperationRetryState.get(operationKey) ?? 0;
|
|
5155
|
-
if (event.retries <= previousRetries) {
|
|
5156
|
-
return;
|
|
5157
|
-
}
|
|
5158
|
-
imageOperationRetryState.set(operationKey, event.retries);
|
|
5159
|
-
applyRetryUpdate("images", event.retries - previousRetries, event.errorMessage);
|
|
5160
|
-
},
|
|
5161
4491
|
onProgress(detail) {
|
|
5162
4492
|
stages[4] = {
|
|
5163
4493
|
...stages[4],
|
|
@@ -5219,7 +4549,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5219
4549
|
const renderedImages = await renderExpandedImages({
|
|
5220
4550
|
prompts: imagePrompts,
|
|
5221
4551
|
settings: input.config.settings,
|
|
5222
|
-
|
|
4552
|
+
limn,
|
|
5223
4553
|
markdownPath: primaryMarkdownPath,
|
|
5224
4554
|
assetDir: sharedAssetDir,
|
|
5225
4555
|
dryRun: imageDryRun,
|
|
@@ -5242,15 +4572,6 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5242
4572
|
recordStageCost(stageTracking, "images", metrics.costUsd, metrics.costSource);
|
|
5243
4573
|
addStageRetries(stageTracking, "images", metrics.retries);
|
|
5244
4574
|
},
|
|
5245
|
-
onRetry(event) {
|
|
5246
|
-
const operationKey = `images:${event.imageId}`;
|
|
5247
|
-
const previousRetries = imageOperationRetryState.get(operationKey) ?? 0;
|
|
5248
|
-
if (event.retries <= previousRetries) {
|
|
5249
|
-
return;
|
|
5250
|
-
}
|
|
5251
|
-
imageOperationRetryState.set(operationKey, event.retries);
|
|
5252
|
-
applyRetryUpdate("images", event.retries - previousRetries, event.errorMessage);
|
|
5253
|
-
},
|
|
5254
4575
|
onProgress(detail) {
|
|
5255
4576
|
stages[4] = {
|
|
5256
4577
|
...stages[4],
|
|
@@ -5965,13 +5286,13 @@ function buildPrimaryCoverPrompt(contentBrief, primaryContentType, primaryMarkdo
|
|
|
5965
5286
|
description: `Cover image for ${primaryContentType}`,
|
|
5966
5287
|
anchorAfterSection: null,
|
|
5967
5288
|
prompt: [
|
|
5968
|
-
`
|
|
5289
|
+
`Cover image for ${primaryContentType}.`,
|
|
5969
5290
|
`Core angle: ${contentBrief.description}`,
|
|
5970
5291
|
`Audience: ${contentBrief.targetAudience}`,
|
|
5971
5292
|
`Promise: ${contentBrief.corePromise}`,
|
|
5972
5293
|
`Voice: ${contentBrief.voiceNotes}`,
|
|
5973
5294
|
`Primary excerpt: ${markdownExcerpt}`,
|
|
5974
|
-
"
|
|
5295
|
+
"Do not include any words, letters, numbers, logos, watermarks, or signage in the image."
|
|
5975
5296
|
].join(" ")
|
|
5976
5297
|
};
|
|
5977
5298
|
}
|
|
@@ -6310,84 +5631,576 @@ function resolveCustomLinks(existing, addRaw, removeExpressions) {
|
|
|
6310
5631
|
return Array.from(result.values());
|
|
6311
5632
|
}
|
|
6312
5633
|
|
|
6313
|
-
// src/cli/commands/
|
|
6314
|
-
|
|
6315
|
-
|
|
6316
|
-
|
|
6317
|
-
|
|
5634
|
+
// src/cli/commands/export.ts
|
|
5635
|
+
import { copyFile, mkdir as mkdir6, readFile as readFile8, stat as stat5, writeFile as writeFile6 } from "fs/promises";
|
|
5636
|
+
import path11 from "path";
|
|
5637
|
+
|
|
5638
|
+
// src/output/enrichMarkdownWithLinks.ts
|
|
5639
|
+
function enrichMarkdownWithLinks(markdown, links) {
|
|
5640
|
+
if (links.length === 0) {
|
|
5641
|
+
return markdown;
|
|
6318
5642
|
}
|
|
6319
|
-
const [
|
|
6320
|
-
|
|
6321
|
-
|
|
5643
|
+
const sorted = [...links].sort((left, right) => right.expression.length - left.expression.length);
|
|
5644
|
+
let updated = markdown;
|
|
5645
|
+
for (const link of sorted) {
|
|
5646
|
+
const escapedExpression = escapeRegExp(link.expression);
|
|
5647
|
+
const leadBoundary = /^\w/.test(link.expression) ? "\\b" : "";
|
|
5648
|
+
const trailBoundary = /\w$/.test(link.expression) ? "\\b" : "";
|
|
5649
|
+
const expressionRegex = new RegExp(`${leadBoundary}${escapedExpression}${trailBoundary}`, "g");
|
|
5650
|
+
let match;
|
|
5651
|
+
while ((match = expressionRegex.exec(updated)) !== null) {
|
|
5652
|
+
const start = match.index;
|
|
5653
|
+
const end = start + match[0].length;
|
|
5654
|
+
if (isInProtectedSpan(updated, start, end)) {
|
|
5655
|
+
continue;
|
|
5656
|
+
}
|
|
5657
|
+
updated = `${updated.slice(0, start)}[${match[0]}](${link.url})${updated.slice(end)}`;
|
|
5658
|
+
break;
|
|
5659
|
+
}
|
|
6322
5660
|
}
|
|
6323
|
-
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
|
|
5661
|
+
return updated;
|
|
5662
|
+
}
|
|
5663
|
+
function isInProtectedSpan(content, start, end) {
|
|
5664
|
+
const lineStart = content.lastIndexOf("\n", start) + 1;
|
|
5665
|
+
const lineEndIdx = content.indexOf("\n", end);
|
|
5666
|
+
const lineEnd = lineEndIdx === -1 ? content.length : lineEndIdx;
|
|
5667
|
+
const line = content.slice(lineStart, lineEnd);
|
|
5668
|
+
const linkPattern = /\[[^\]]*\]\([^)]*\)/g;
|
|
5669
|
+
let m;
|
|
5670
|
+
while ((m = linkPattern.exec(line)) !== null) {
|
|
5671
|
+
const absStart = lineStart + m.index;
|
|
5672
|
+
const absEnd = absStart + m[0].length;
|
|
5673
|
+
if (start >= absStart && end <= absEnd) {
|
|
5674
|
+
return true;
|
|
5675
|
+
}
|
|
6328
5676
|
}
|
|
6329
|
-
const
|
|
6330
|
-
|
|
6331
|
-
|
|
5677
|
+
const codePattern = /`[^`]+`/g;
|
|
5678
|
+
while ((m = codePattern.exec(line)) !== null) {
|
|
5679
|
+
const absStart = lineStart + m.index;
|
|
5680
|
+
const absEnd = absStart + m[0].length;
|
|
5681
|
+
if (start >= absStart && end <= absEnd) {
|
|
5682
|
+
return true;
|
|
5683
|
+
}
|
|
6332
5684
|
}
|
|
6333
|
-
return
|
|
6334
|
-
contentType,
|
|
6335
|
-
count
|
|
6336
|
-
};
|
|
5685
|
+
return false;
|
|
6337
5686
|
}
|
|
6338
|
-
function
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
|
|
5687
|
+
function escapeRegExp(value2) {
|
|
5688
|
+
return value2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5689
|
+
}
|
|
5690
|
+
|
|
5691
|
+
// src/server/previewHelpers.ts
|
|
5692
|
+
import { readdir, stat as stat4, readFile as readFile7 } from "fs/promises";
|
|
5693
|
+
import path10 from "path";
|
|
5694
|
+
var DEFAULT_PORT = 4173;
|
|
5695
|
+
var CONTENT_TYPE_ORDER = ["article", "blog-post", "x-thread", "x-post", "linkedin-post", "reddit-post", "newsletter"];
|
|
5696
|
+
var FILE_PREFIX_TO_CONTENT_TYPE = {
|
|
5697
|
+
article: "article",
|
|
5698
|
+
blog: "blog-post",
|
|
5699
|
+
"x-thread": "x-thread",
|
|
5700
|
+
"x-post": "x-post",
|
|
5701
|
+
x: "x-post",
|
|
5702
|
+
reddit: "reddit-post",
|
|
5703
|
+
linkedin: "linkedin-post",
|
|
5704
|
+
newsletter: "newsletter"
|
|
5705
|
+
};
|
|
5706
|
+
var CONTENT_TYPE_LABELS = {
|
|
5707
|
+
article: "Article",
|
|
5708
|
+
"blog-post": "Blog Post",
|
|
5709
|
+
"x-thread": "X Thread",
|
|
5710
|
+
"x-post": "X Post",
|
|
5711
|
+
"reddit-post": "Reddit Post",
|
|
5712
|
+
"linkedin-post": "LinkedIn Post",
|
|
5713
|
+
newsletter: "Newsletter"
|
|
5714
|
+
};
|
|
5715
|
+
function parsePort(portOption) {
|
|
5716
|
+
if (!portOption) {
|
|
5717
|
+
return DEFAULT_PORT;
|
|
6342
5718
|
}
|
|
6343
|
-
|
|
6344
|
-
|
|
5719
|
+
const port = Number.parseInt(portOption, 10);
|
|
5720
|
+
if (Number.isNaN(port) || port < 1 || port > 65535) {
|
|
5721
|
+
throw new Error(`Invalid port "${portOption}". Choose a value between 1 and 65535.`);
|
|
6345
5722
|
}
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
5723
|
+
return port;
|
|
5724
|
+
}
|
|
5725
|
+
function stripFrontmatter2(markdown) {
|
|
5726
|
+
return markdown.replace(/^---\s*\n[\s\S]*?\n---\s*\n?/, "");
|
|
5727
|
+
}
|
|
5728
|
+
function extractFrontmatterSlug(markdown) {
|
|
5729
|
+
const frontmatterMatch = markdown.match(/^---\s*\n([\s\S]*?)\n---\s*\n?/);
|
|
5730
|
+
const frontmatter = frontmatterMatch?.[1];
|
|
5731
|
+
if (!frontmatter) {
|
|
5732
|
+
return null;
|
|
6349
5733
|
}
|
|
6350
|
-
const
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
|
|
5734
|
+
const slugMatch = frontmatter.match(/^slug:\s*(.+)$/m);
|
|
5735
|
+
const rawSlug = slugMatch?.[1]?.trim();
|
|
5736
|
+
if (!rawSlug) {
|
|
5737
|
+
return null;
|
|
5738
|
+
}
|
|
5739
|
+
const unquoted = rawSlug.replace(/^['\"]|['\"]$/g, "").trim();
|
|
5740
|
+
return unquoted.length > 0 ? unquoted : null;
|
|
5741
|
+
}
|
|
5742
|
+
function extractHeadingTitle(markdown) {
|
|
5743
|
+
const headingMatch = markdown.match(/^#\s+(.+)$/m);
|
|
5744
|
+
if (!headingMatch || !headingMatch[1]) {
|
|
5745
|
+
return null;
|
|
5746
|
+
}
|
|
5747
|
+
return headingMatch[1].trim();
|
|
5748
|
+
}
|
|
5749
|
+
async function resolveMarkdownPath(markdownPathArg, markdownOutputDir, cwd2) {
|
|
5750
|
+
if (markdownPathArg) {
|
|
5751
|
+
const resolved = path10.isAbsolute(markdownPathArg) ? markdownPathArg : path10.resolve(cwd2, markdownPathArg);
|
|
5752
|
+
if (path10.extname(resolved).toLowerCase() !== ".md") {
|
|
5753
|
+
throw new Error(`Expected a markdown file (.md), received: ${resolved}`);
|
|
6357
5754
|
}
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
5755
|
+
await assertFileExists(resolved, "Could not find markdown file");
|
|
5756
|
+
return resolved;
|
|
5757
|
+
}
|
|
5758
|
+
return await resolveLatestMarkdown(markdownOutputDir);
|
|
5759
|
+
}
|
|
5760
|
+
async function resolveLatestMarkdown(markdownOutputDir) {
|
|
5761
|
+
const markdownCandidates = await findMarkdownFiles(markdownOutputDir);
|
|
5762
|
+
if (markdownCandidates.length === 0) {
|
|
5763
|
+
throw new Error(
|
|
5764
|
+
`No generated articles found in ${markdownOutputDir}. Run ideon write "your idea" first or pass a markdown path.`
|
|
5765
|
+
);
|
|
5766
|
+
}
|
|
5767
|
+
let latestPath = markdownCandidates[0];
|
|
5768
|
+
let latestMtime = 0;
|
|
5769
|
+
for (const candidate of markdownCandidates) {
|
|
5770
|
+
const fileStat = await stat4(candidate);
|
|
5771
|
+
if (fileStat.mtimeMs >= latestMtime) {
|
|
5772
|
+
latestMtime = fileStat.mtimeMs;
|
|
5773
|
+
latestPath = candidate;
|
|
6362
5774
|
}
|
|
6363
|
-
secondaryDedupedByType.set(parsed.contentType, {
|
|
6364
|
-
...parsed,
|
|
6365
|
-
role: "secondary"
|
|
6366
|
-
});
|
|
6367
5775
|
}
|
|
6368
|
-
return
|
|
6369
|
-
{
|
|
6370
|
-
...primary,
|
|
6371
|
-
role: "primary"
|
|
6372
|
-
},
|
|
6373
|
-
...secondaryDedupedByType.values()
|
|
6374
|
-
];
|
|
5776
|
+
return latestPath;
|
|
6375
5777
|
}
|
|
6376
|
-
|
|
6377
|
-
|
|
6378
|
-
|
|
6379
|
-
|
|
6380
|
-
|
|
6381
|
-
|
|
6382
|
-
}
|
|
6383
|
-
|
|
6384
|
-
|
|
6385
|
-
|
|
6386
|
-
|
|
6387
|
-
|
|
6388
|
-
|
|
6389
|
-
|
|
6390
|
-
|
|
5778
|
+
async function assertFileExists(filePath, errorPrefix) {
|
|
5779
|
+
try {
|
|
5780
|
+
const fileStat = await stat4(filePath);
|
|
5781
|
+
if (!fileStat.isFile()) {
|
|
5782
|
+
throw new Error(`${errorPrefix}: ${filePath}`);
|
|
5783
|
+
}
|
|
5784
|
+
} catch {
|
|
5785
|
+
throw new Error(`${errorPrefix}: ${filePath}`);
|
|
5786
|
+
}
|
|
5787
|
+
}
|
|
5788
|
+
function extractCoverImageUrl(markdown) {
|
|
5789
|
+
const body = stripFrontmatter2(markdown);
|
|
5790
|
+
const match = body.match(/!\[[^\]]*\]\(([^)]+)\)/);
|
|
5791
|
+
return match?.[1] ?? null;
|
|
5792
|
+
}
|
|
5793
|
+
async function extractArticleMetadata(markdownPath) {
|
|
5794
|
+
const markdown = await readFile7(markdownPath, "utf8");
|
|
5795
|
+
const fileStat = await stat4(markdownPath);
|
|
5796
|
+
const slug = extractFrontmatterSlug(markdown) ?? path10.basename(markdownPath, ".md");
|
|
5797
|
+
const title = extractHeadingTitle(stripFrontmatter2(markdown)) ?? slug;
|
|
5798
|
+
const body = stripFrontmatter2(markdown);
|
|
5799
|
+
const previewSnippet = body.replace(/[#\[\]()!\-*_`]/g, "").trim().substring(0, 150);
|
|
5800
|
+
const coverImageUrl = extractCoverImageUrl(markdown);
|
|
5801
|
+
return {
|
|
5802
|
+
slug,
|
|
5803
|
+
title,
|
|
5804
|
+
mtime: fileStat.mtimeMs,
|
|
5805
|
+
previewSnippet,
|
|
5806
|
+
coverImageUrl
|
|
5807
|
+
};
|
|
5808
|
+
}
|
|
5809
|
+
async function listAllGenerations(markdownOutputDir) {
|
|
5810
|
+
const markdownFiles = await findMarkdownFiles(markdownOutputDir);
|
|
5811
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
5812
|
+
for (const filePath of markdownFiles) {
|
|
5813
|
+
try {
|
|
5814
|
+
const metadata = await extractArticleMetadata(filePath);
|
|
5815
|
+
const identity = deriveOutputIdentity(filePath, markdownOutputDir);
|
|
5816
|
+
const output = {
|
|
5817
|
+
id: `${identity.generationId}:${identity.contentType}:${identity.index}`,
|
|
5818
|
+
generationId: identity.generationId,
|
|
5819
|
+
sourcePath: filePath,
|
|
5820
|
+
slug: metadata.slug,
|
|
5821
|
+
title: metadata.title,
|
|
5822
|
+
previewSnippet: metadata.previewSnippet,
|
|
5823
|
+
coverImageUrl: metadata.coverImageUrl,
|
|
5824
|
+
mtime: metadata.mtime,
|
|
5825
|
+
contentType: identity.contentType,
|
|
5826
|
+
contentTypeLabel: toContentTypeLabel(identity.contentType),
|
|
5827
|
+
index: identity.index
|
|
5828
|
+
};
|
|
5829
|
+
const existing = grouped.get(identity.generationId);
|
|
5830
|
+
if (existing) {
|
|
5831
|
+
existing.push(output);
|
|
5832
|
+
} else {
|
|
5833
|
+
grouped.set(identity.generationId, [output]);
|
|
5834
|
+
}
|
|
5835
|
+
} catch {
|
|
5836
|
+
}
|
|
5837
|
+
}
|
|
5838
|
+
const generations = [];
|
|
5839
|
+
for (const [id, outputs] of grouped.entries()) {
|
|
5840
|
+
outputs.sort((a, b) => compareContentTypes(a.contentType, b.contentType) || a.index - b.index || b.mtime - a.mtime);
|
|
5841
|
+
const primaryContentType = await resolvePrimaryContentType(outputs);
|
|
5842
|
+
const primary = outputs.find((output) => output.contentType === primaryContentType) ?? outputs[0];
|
|
5843
|
+
if (!primary) {
|
|
5844
|
+
continue;
|
|
5845
|
+
}
|
|
5846
|
+
const newestMtime = outputs.reduce((latest, output) => Math.max(latest, output.mtime), 0);
|
|
5847
|
+
generations.push({
|
|
5848
|
+
id,
|
|
5849
|
+
title: primary.title,
|
|
5850
|
+
mtime: newestMtime,
|
|
5851
|
+
previewSnippet: primary.previewSnippet,
|
|
5852
|
+
coverImageUrl: primary.coverImageUrl ?? outputs.find((output) => Boolean(output.coverImageUrl))?.coverImageUrl ?? null,
|
|
5853
|
+
primaryContentType,
|
|
5854
|
+
outputs
|
|
5855
|
+
});
|
|
5856
|
+
}
|
|
5857
|
+
generations.sort((a, b) => b.mtime - a.mtime);
|
|
5858
|
+
return generations;
|
|
5859
|
+
}
|
|
5860
|
+
function deriveGenerationId(markdownPath, markdownOutputDir) {
|
|
5861
|
+
const relative = path10.relative(markdownOutputDir, markdownPath);
|
|
5862
|
+
const normalized = relative.split(path10.sep).join("/");
|
|
5863
|
+
if (!normalized || normalized.startsWith("../")) {
|
|
5864
|
+
return path10.basename(markdownPath, ".md");
|
|
5865
|
+
}
|
|
5866
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
5867
|
+
if (segments.length <= 1) {
|
|
5868
|
+
return path10.basename(markdownPath, ".md");
|
|
5869
|
+
}
|
|
5870
|
+
return segments[0] ?? path10.basename(markdownPath, ".md");
|
|
5871
|
+
}
|
|
5872
|
+
async function findMarkdownFiles(markdownOutputDir) {
|
|
5873
|
+
const files = [];
|
|
5874
|
+
const stack = [markdownOutputDir];
|
|
5875
|
+
while (stack.length > 0) {
|
|
5876
|
+
const current = stack.pop();
|
|
5877
|
+
if (!current) {
|
|
5878
|
+
continue;
|
|
5879
|
+
}
|
|
5880
|
+
let entries;
|
|
5881
|
+
try {
|
|
5882
|
+
entries = await readdir(current, { withFileTypes: true });
|
|
5883
|
+
} catch {
|
|
5884
|
+
continue;
|
|
5885
|
+
}
|
|
5886
|
+
for (const entry of entries) {
|
|
5887
|
+
const fullPath = path10.join(current, entry.name);
|
|
5888
|
+
if (entry.isDirectory()) {
|
|
5889
|
+
stack.push(fullPath);
|
|
5890
|
+
continue;
|
|
5891
|
+
}
|
|
5892
|
+
if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
|
|
5893
|
+
files.push(fullPath);
|
|
5894
|
+
}
|
|
5895
|
+
}
|
|
5896
|
+
}
|
|
5897
|
+
return files;
|
|
5898
|
+
}
|
|
5899
|
+
function deriveOutputIdentity(markdownPath, markdownOutputDir) {
|
|
5900
|
+
const generationId = deriveGenerationId(markdownPath, markdownOutputDir);
|
|
5901
|
+
const fileBase = path10.basename(markdownPath, ".md");
|
|
5902
|
+
const parsed = fileBase.match(/^([a-z0-9-]+)-(\d+)$/i);
|
|
5903
|
+
if (!parsed || !parsed[1] || !parsed[2]) {
|
|
5904
|
+
return {
|
|
5905
|
+
generationId,
|
|
5906
|
+
contentType: "article",
|
|
5907
|
+
index: 1
|
|
5908
|
+
};
|
|
5909
|
+
}
|
|
5910
|
+
const prefix = parsed[1].toLowerCase();
|
|
5911
|
+
const index = Number.parseInt(parsed[2], 10);
|
|
5912
|
+
return {
|
|
5913
|
+
generationId,
|
|
5914
|
+
contentType: FILE_PREFIX_TO_CONTENT_TYPE[prefix] ?? prefix,
|
|
5915
|
+
index: Number.isFinite(index) && index > 0 ? index : 1
|
|
5916
|
+
};
|
|
5917
|
+
}
|
|
5918
|
+
function compareContentTypes(left, right) {
|
|
5919
|
+
const leftIndex = CONTENT_TYPE_ORDER.indexOf(left);
|
|
5920
|
+
const rightIndex = CONTENT_TYPE_ORDER.indexOf(right);
|
|
5921
|
+
const normalizedLeft = leftIndex === -1 ? Number.MAX_SAFE_INTEGER : leftIndex;
|
|
5922
|
+
const normalizedRight = rightIndex === -1 ? Number.MAX_SAFE_INTEGER : rightIndex;
|
|
5923
|
+
if (normalizedLeft !== normalizedRight) {
|
|
5924
|
+
return normalizedLeft - normalizedRight;
|
|
5925
|
+
}
|
|
5926
|
+
return left.localeCompare(right);
|
|
5927
|
+
}
|
|
5928
|
+
function toContentTypeLabel(contentType) {
|
|
5929
|
+
const knownLabel = CONTENT_TYPE_LABELS[contentType];
|
|
5930
|
+
if (knownLabel) {
|
|
5931
|
+
return knownLabel;
|
|
5932
|
+
}
|
|
5933
|
+
return contentType.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
5934
|
+
}
|
|
5935
|
+
async function resolvePrimaryContentType(outputs) {
|
|
5936
|
+
const fallback = outputs.find((output) => output.contentType === "article")?.contentType ?? outputs[0]?.contentType ?? "article";
|
|
5937
|
+
const generationDir = path10.dirname(outputs[0]?.sourcePath ?? "");
|
|
5938
|
+
if (!generationDir) {
|
|
5939
|
+
return fallback;
|
|
5940
|
+
}
|
|
5941
|
+
const jobPath = path10.join(generationDir, "job.json");
|
|
5942
|
+
try {
|
|
5943
|
+
const raw = await readFile7(jobPath, "utf8");
|
|
5944
|
+
const parsed = JSON.parse(raw);
|
|
5945
|
+
const targets = Array.isArray(parsed.contentTargets) ? parsed.contentTargets : Array.isArray(parsed.settings?.contentTargets) ? parsed.settings.contentTargets : [];
|
|
5946
|
+
const primary = targets.find((target) => target?.role === "primary" && typeof target.contentType === "string");
|
|
5947
|
+
if (primary && typeof primary.contentType === "string") {
|
|
5948
|
+
return primary.contentType;
|
|
5949
|
+
}
|
|
5950
|
+
} catch {
|
|
5951
|
+
return fallback;
|
|
5952
|
+
}
|
|
5953
|
+
return fallback;
|
|
5954
|
+
}
|
|
5955
|
+
|
|
5956
|
+
// src/cli/commands/export.ts
|
|
5957
|
+
async function runOutputCommand(options, dependencies = {}) {
|
|
5958
|
+
const cwd2 = dependencies.cwd ?? process.cwd();
|
|
5959
|
+
const log = dependencies.log ?? ((message) => console.log(message));
|
|
5960
|
+
const targetIndex = options.index ?? 1;
|
|
5961
|
+
const resolved = await resolveRunInput({ idea: `Export generation ${options.generationId}` });
|
|
5962
|
+
const outputPaths = resolveOutputPaths(resolved.config.settings, cwd2);
|
|
5963
|
+
const generations = await listAllGenerations(outputPaths.markdownOutputDir);
|
|
5964
|
+
const generation = resolveGeneration(generations, options.generationId);
|
|
5965
|
+
const articleOutputs = generation.outputs.filter((output) => output.contentType === generation.primaryContentType);
|
|
5966
|
+
if (articleOutputs.length === 0) {
|
|
5967
|
+
throw new ReportedError(
|
|
5968
|
+
`Generation "${generation.id}" has no primary content outputs (type: ${generation.primaryContentType}).`
|
|
5969
|
+
);
|
|
5970
|
+
}
|
|
5971
|
+
const articleOutput = articleOutputs.find((output) => output.index === targetIndex);
|
|
5972
|
+
if (!articleOutput) {
|
|
5973
|
+
const available = articleOutputs.map((output) => output.index).join(", ");
|
|
5974
|
+
throw new ReportedError(
|
|
5975
|
+
`Generation "${generation.id}" has no primary output at index ${targetIndex}. Available: ${available}.`
|
|
5976
|
+
);
|
|
5977
|
+
}
|
|
5978
|
+
const sourceMarkdownPath = articleOutput.sourcePath;
|
|
5979
|
+
const sourceMarkdown = await readFile8(sourceMarkdownPath, "utf8");
|
|
5980
|
+
const slug = extractFrontmatterSlug2(sourceMarkdown) ?? path11.basename(sourceMarkdownPath, ".md");
|
|
5981
|
+
const exportFilename = `${slug}.md`;
|
|
5982
|
+
const destinationDir = await resolveDestinationDir(options.destinationPath, cwd2);
|
|
5983
|
+
const destinationFilePath = path11.join(destinationDir, exportFilename);
|
|
5984
|
+
if (!options.overwrite && await fileExists2(destinationFilePath)) {
|
|
5985
|
+
throw new ReportedError(
|
|
5986
|
+
`Export file already exists: ${destinationFilePath}. Pass --overwrite to replace it.`
|
|
5987
|
+
);
|
|
5988
|
+
}
|
|
5989
|
+
await mkdir6(destinationDir, { recursive: true });
|
|
5990
|
+
const links = await loadLinks(sourceMarkdownPath);
|
|
5991
|
+
const enrichedMarkdown = enrichWithFrontmatterGuard(sourceMarkdown, links);
|
|
5992
|
+
const sourceDir = path11.dirname(sourceMarkdownPath);
|
|
5993
|
+
const imagePaths = extractLocalImagePaths(sourceMarkdown);
|
|
5994
|
+
const copiedImages = [];
|
|
5995
|
+
for (const relImagePath of imagePaths) {
|
|
5996
|
+
const absoluteImageSrc = path11.resolve(sourceDir, relImagePath);
|
|
5997
|
+
let imageStat = null;
|
|
5998
|
+
try {
|
|
5999
|
+
imageStat = await stat5(absoluteImageSrc);
|
|
6000
|
+
} catch {
|
|
6001
|
+
throw new ReportedError(
|
|
6002
|
+
`Referenced image not found: ${relImagePath} (resolved to ${absoluteImageSrc}).`
|
|
6003
|
+
);
|
|
6004
|
+
}
|
|
6005
|
+
if (!imageStat.isFile()) {
|
|
6006
|
+
throw new ReportedError(`Referenced image path is not a file: ${absoluteImageSrc}.`);
|
|
6007
|
+
}
|
|
6008
|
+
const destImagePath = path11.join(destinationDir, relImagePath);
|
|
6009
|
+
await mkdir6(path11.dirname(destImagePath), { recursive: true });
|
|
6010
|
+
await copyFile(absoluteImageSrc, destImagePath);
|
|
6011
|
+
copiedImages.push(relImagePath);
|
|
6012
|
+
}
|
|
6013
|
+
await writeFile6(destinationFilePath, enrichedMarkdown, "utf8");
|
|
6014
|
+
const relDest = path11.relative(cwd2, destinationFilePath);
|
|
6015
|
+
log(`Exported "${generation.id}" (${generation.primaryContentType} #${targetIndex}) \u2192 ${relDest}`);
|
|
6016
|
+
if (copiedImages.length > 0) {
|
|
6017
|
+
log(`Copied ${copiedImages.length} image${copiedImages.length === 1 ? "" : "s"}: ${copiedImages.join(", ")}`);
|
|
6018
|
+
}
|
|
6019
|
+
if (links.length > 0) {
|
|
6020
|
+
log(`Injected ${links.length} inline link${links.length === 1 ? "" : "s"}.`);
|
|
6021
|
+
}
|
|
6022
|
+
}
|
|
6023
|
+
function resolveGeneration(generations, generationId) {
|
|
6024
|
+
const exact = generations.find((g) => g.id === generationId);
|
|
6025
|
+
if (exact) {
|
|
6026
|
+
return exact;
|
|
6027
|
+
}
|
|
6028
|
+
const bySlug = generations.find(
|
|
6029
|
+
(g) => g.outputs.some((output) => output.slug === generationId)
|
|
6030
|
+
);
|
|
6031
|
+
if (bySlug) {
|
|
6032
|
+
return bySlug;
|
|
6033
|
+
}
|
|
6034
|
+
throw new ReportedError(
|
|
6035
|
+
`Generation "${generationId}" not found. Run \`ideon preview\` to list available generations.`
|
|
6036
|
+
);
|
|
6037
|
+
}
|
|
6038
|
+
async function resolveDestinationDir(destinationPath, cwd2) {
|
|
6039
|
+
const resolved = path11.isAbsolute(destinationPath) ? destinationPath : path11.resolve(cwd2, destinationPath);
|
|
6040
|
+
return resolved;
|
|
6041
|
+
}
|
|
6042
|
+
async function fileExists2(filePath) {
|
|
6043
|
+
try {
|
|
6044
|
+
const fileStat = await stat5(filePath);
|
|
6045
|
+
return fileStat.isFile();
|
|
6046
|
+
} catch {
|
|
6047
|
+
return false;
|
|
6048
|
+
}
|
|
6049
|
+
}
|
|
6050
|
+
async function loadLinks(markdownPath) {
|
|
6051
|
+
const linksPath = resolveLinksPath(markdownPath);
|
|
6052
|
+
let raw;
|
|
6053
|
+
try {
|
|
6054
|
+
raw = await readFile8(linksPath, "utf8");
|
|
6055
|
+
} catch {
|
|
6056
|
+
return [];
|
|
6057
|
+
}
|
|
6058
|
+
let parsed;
|
|
6059
|
+
try {
|
|
6060
|
+
parsed = JSON.parse(raw);
|
|
6061
|
+
} catch {
|
|
6062
|
+
return [];
|
|
6063
|
+
}
|
|
6064
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
6065
|
+
return [];
|
|
6066
|
+
}
|
|
6067
|
+
const record = parsed;
|
|
6068
|
+
const links = Array.isArray(record.links) ? record.links : [];
|
|
6069
|
+
const customLinks = Array.isArray(record.customLinks) ? record.customLinks : [];
|
|
6070
|
+
const combined = [...customLinks, ...links];
|
|
6071
|
+
return combined.filter((entry) => {
|
|
6072
|
+
if (typeof entry !== "object" || entry === null) {
|
|
6073
|
+
return false;
|
|
6074
|
+
}
|
|
6075
|
+
const e = entry;
|
|
6076
|
+
return typeof e.expression === "string" && typeof e.url === "string" && (e.title === null || typeof e.title === "string");
|
|
6077
|
+
}).map((entry) => ({
|
|
6078
|
+
expression: entry.expression.trim(),
|
|
6079
|
+
url: entry.url.trim(),
|
|
6080
|
+
title: entry.title
|
|
6081
|
+
})).filter((entry) => entry.expression.length > 0 && entry.url.length > 0);
|
|
6082
|
+
}
|
|
6083
|
+
function enrichWithFrontmatterGuard(markdown, links) {
|
|
6084
|
+
if (links.length === 0) {
|
|
6085
|
+
return markdown;
|
|
6086
|
+
}
|
|
6087
|
+
const frontmatterMatch = markdown.match(/^---\s*\n[\s\S]*?\n---\s*\n?/);
|
|
6088
|
+
if (!frontmatterMatch) {
|
|
6089
|
+
return enrichMarkdownWithLinks(markdown, links);
|
|
6090
|
+
}
|
|
6091
|
+
const frontmatter = frontmatterMatch[0];
|
|
6092
|
+
const body = markdown.slice(frontmatter.length);
|
|
6093
|
+
return `${frontmatter}${enrichMarkdownWithLinks(body, links)}`;
|
|
6094
|
+
}
|
|
6095
|
+
function extractFrontmatterSlug2(markdown) {
|
|
6096
|
+
const frontmatterMatch = markdown.match(/^---\s*\n([\s\S]*?)\n---\s*\n?/);
|
|
6097
|
+
const block = frontmatterMatch?.[1];
|
|
6098
|
+
if (!block) {
|
|
6099
|
+
return null;
|
|
6100
|
+
}
|
|
6101
|
+
const slugMatch = block.match(/^slug:\s*(.+)$/m);
|
|
6102
|
+
const rawSlug = slugMatch?.[1]?.trim();
|
|
6103
|
+
if (!rawSlug) {
|
|
6104
|
+
return null;
|
|
6105
|
+
}
|
|
6106
|
+
const unquoted = rawSlug.replace(/^['""]|['""]$/g, "").trim();
|
|
6107
|
+
return unquoted.length > 0 ? unquoted : null;
|
|
6108
|
+
}
|
|
6109
|
+
function extractLocalImagePaths(markdown) {
|
|
6110
|
+
const imagePattern = /!\[[^\]]*\]\(([^)]+)\)/g;
|
|
6111
|
+
const paths = [];
|
|
6112
|
+
let match;
|
|
6113
|
+
while ((match = imagePattern.exec(markdown)) !== null) {
|
|
6114
|
+
const rawPath = match[1]?.trim();
|
|
6115
|
+
if (!rawPath) {
|
|
6116
|
+
continue;
|
|
6117
|
+
}
|
|
6118
|
+
if (rawPath.startsWith("http://") || rawPath.startsWith("https://") || rawPath.startsWith("data:") || rawPath.startsWith("/") || rawPath.startsWith("#")) {
|
|
6119
|
+
continue;
|
|
6120
|
+
}
|
|
6121
|
+
paths.push(rawPath);
|
|
6122
|
+
}
|
|
6123
|
+
return paths;
|
|
6124
|
+
}
|
|
6125
|
+
|
|
6126
|
+
// src/cli/commands/writeTargetSpecs.ts
|
|
6127
|
+
function parseTargetSpec(spec) {
|
|
6128
|
+
const trimmed = spec.trim();
|
|
6129
|
+
if (!trimmed) {
|
|
6130
|
+
throw new ReportedError("Target spec cannot be empty. Use <content-type=count>.");
|
|
6131
|
+
}
|
|
6132
|
+
const [rawType, rawCount] = trimmed.split("=");
|
|
6133
|
+
if (!rawType || !rawCount) {
|
|
6134
|
+
throw new ReportedError(`Invalid target "${spec}". Use format content-type=count, e.g. article=1 or x-thread=3.`);
|
|
6135
|
+
}
|
|
6136
|
+
const contentType = rawType.trim();
|
|
6137
|
+
if (!contentTypeValues.includes(contentType)) {
|
|
6138
|
+
throw new ReportedError(
|
|
6139
|
+
`Unsupported content type "${contentType}". Supported values: ${contentTypeValues.join(", ")}.`
|
|
6140
|
+
);
|
|
6141
|
+
}
|
|
6142
|
+
const count = Number.parseInt(rawCount.trim(), 10);
|
|
6143
|
+
if (!Number.isFinite(count) || count <= 0) {
|
|
6144
|
+
throw new ReportedError(`Invalid count in target "${spec}". Count must be a positive integer.`);
|
|
6145
|
+
}
|
|
6146
|
+
return {
|
|
6147
|
+
contentType,
|
|
6148
|
+
count
|
|
6149
|
+
};
|
|
6150
|
+
}
|
|
6151
|
+
function parsePrimaryAndSecondarySpecs(options) {
|
|
6152
|
+
const { primarySpec, secondarySpecs } = options;
|
|
6153
|
+
if (!primarySpec && (!secondarySpecs || secondarySpecs.length === 0)) {
|
|
6154
|
+
return void 0;
|
|
6155
|
+
}
|
|
6156
|
+
if (!primarySpec) {
|
|
6157
|
+
throw new ReportedError("Missing required --primary <content-type=count>.");
|
|
6158
|
+
}
|
|
6159
|
+
const primary = parseTargetSpec(primarySpec);
|
|
6160
|
+
if (primary.count !== 1) {
|
|
6161
|
+
throw new ReportedError("Primary target count must be exactly 1. Use --primary <content-type=1>.");
|
|
6162
|
+
}
|
|
6163
|
+
const secondaryDedupedByType = /* @__PURE__ */ new Map();
|
|
6164
|
+
for (const spec of secondarySpecs ?? []) {
|
|
6165
|
+
const parsed = parseTargetSpec(spec);
|
|
6166
|
+
if (parsed.contentType === primary.contentType) {
|
|
6167
|
+
throw new ReportedError(
|
|
6168
|
+
`Content type "${parsed.contentType}" cannot be both primary and secondary in the same run.`
|
|
6169
|
+
);
|
|
6170
|
+
}
|
|
6171
|
+
const previous = secondaryDedupedByType.get(parsed.contentType);
|
|
6172
|
+
if (previous) {
|
|
6173
|
+
previous.count += parsed.count;
|
|
6174
|
+
continue;
|
|
6175
|
+
}
|
|
6176
|
+
secondaryDedupedByType.set(parsed.contentType, {
|
|
6177
|
+
...parsed,
|
|
6178
|
+
role: "secondary"
|
|
6179
|
+
});
|
|
6180
|
+
}
|
|
6181
|
+
return [
|
|
6182
|
+
{
|
|
6183
|
+
...primary,
|
|
6184
|
+
role: "primary"
|
|
6185
|
+
},
|
|
6186
|
+
...secondaryDedupedByType.values()
|
|
6187
|
+
];
|
|
6188
|
+
}
|
|
6189
|
+
|
|
6190
|
+
// src/integrations/mcp/server.ts
|
|
6191
|
+
async function startIdeonMcpServer() {
|
|
6192
|
+
const server = new McpServer({
|
|
6193
|
+
name: "ideon",
|
|
6194
|
+
version: package_default.version
|
|
6195
|
+
});
|
|
6196
|
+
server.registerTool(
|
|
6197
|
+
"ideon_write",
|
|
6198
|
+
{
|
|
6199
|
+
title: "Ideon Write",
|
|
6200
|
+
description: "Generate content from an idea using the Ideon pipeline.",
|
|
6201
|
+
inputSchema: writeToolInputSchema
|
|
6202
|
+
},
|
|
6203
|
+
async (input) => {
|
|
6391
6204
|
try {
|
|
6392
6205
|
const parsedTargets = parsePrimaryAndSecondarySpecs({
|
|
6393
6206
|
primarySpec: input.primary,
|
|
@@ -6409,7 +6222,8 @@ async function startIdeonMcpServer() {
|
|
|
6409
6222
|
enrichLinks: input.enrichLinks ?? false,
|
|
6410
6223
|
customLinks: input.link,
|
|
6411
6224
|
unlinks: input.unlink,
|
|
6412
|
-
maxLinks: input.maxLinks
|
|
6225
|
+
maxLinks: input.maxLinks,
|
|
6226
|
+
maxImages: input.maxImages
|
|
6413
6227
|
});
|
|
6414
6228
|
return {
|
|
6415
6229
|
content: [
|
|
@@ -6468,7 +6282,8 @@ async function startIdeonMcpServer() {
|
|
|
6468
6282
|
enrichLinks: input.enrichLinks ?? false,
|
|
6469
6283
|
customLinks: input.link,
|
|
6470
6284
|
unlinks: input.unlink,
|
|
6471
|
-
maxLinks: input.maxLinks
|
|
6285
|
+
maxLinks: input.maxLinks,
|
|
6286
|
+
maxImages: input.maxImages
|
|
6472
6287
|
});
|
|
6473
6288
|
return {
|
|
6474
6289
|
content: [
|
|
@@ -6569,6 +6384,50 @@ async function startIdeonMcpServer() {
|
|
|
6569
6384
|
}
|
|
6570
6385
|
}
|
|
6571
6386
|
);
|
|
6387
|
+
server.registerTool(
|
|
6388
|
+
"ideon_export",
|
|
6389
|
+
{
|
|
6390
|
+
title: "Ideon Export",
|
|
6391
|
+
description: "Export a generated article as a standalone markdown file with inline links and copied images.",
|
|
6392
|
+
inputSchema: exportToolInputZodSchema
|
|
6393
|
+
},
|
|
6394
|
+
async (input) => {
|
|
6395
|
+
try {
|
|
6396
|
+
const messages = [];
|
|
6397
|
+
await runOutputCommand(
|
|
6398
|
+
{
|
|
6399
|
+
generationId: input.generationId,
|
|
6400
|
+
destinationPath: input.destinationPath,
|
|
6401
|
+
index: input.index,
|
|
6402
|
+
overwrite: input.overwrite
|
|
6403
|
+
},
|
|
6404
|
+
{
|
|
6405
|
+
cwd: cwd(),
|
|
6406
|
+
log: (message) => {
|
|
6407
|
+
messages.push(message);
|
|
6408
|
+
}
|
|
6409
|
+
}
|
|
6410
|
+
);
|
|
6411
|
+
return {
|
|
6412
|
+
content: [
|
|
6413
|
+
{
|
|
6414
|
+
type: "text",
|
|
6415
|
+
text: messages.length > 0 ? messages.join("\n") : `Exported ${input.generationId}.`
|
|
6416
|
+
}
|
|
6417
|
+
],
|
|
6418
|
+
structuredContent: {
|
|
6419
|
+
generationId: input.generationId,
|
|
6420
|
+
destinationPath: input.destinationPath,
|
|
6421
|
+
index: input.index ?? 1,
|
|
6422
|
+
overwrite: input.overwrite ?? false,
|
|
6423
|
+
messages
|
|
6424
|
+
}
|
|
6425
|
+
};
|
|
6426
|
+
} catch (error) {
|
|
6427
|
+
return formatToolError(error);
|
|
6428
|
+
}
|
|
6429
|
+
}
|
|
6430
|
+
);
|
|
6572
6431
|
server.registerTool(
|
|
6573
6432
|
"ideon_config_get",
|
|
6574
6433
|
{
|
|
@@ -6658,213 +6517,65 @@ async function startIdeonMcpServer() {
|
|
|
6658
6517
|
"ideon_config_unset",
|
|
6659
6518
|
{
|
|
6660
6519
|
title: "Ideon Config Unset",
|
|
6661
|
-
description: "Reset a setting to its default or delete a stored secret.",
|
|
6662
|
-
inputSchema: configUnsetToolInputSchema
|
|
6663
|
-
},
|
|
6664
|
-
async (input) => {
|
|
6665
|
-
try {
|
|
6666
|
-
if (!isConfigKey(input.key)) {
|
|
6667
|
-
throw new ReportedError(`Unsupported config key: ${input.key}`);
|
|
6668
|
-
}
|
|
6669
|
-
await configUnset(input.key);
|
|
6670
|
-
return {
|
|
6671
|
-
content: [
|
|
6672
|
-
{
|
|
6673
|
-
type: "text",
|
|
6674
|
-
text: `Unset ${input.key}.`
|
|
6675
|
-
}
|
|
6676
|
-
],
|
|
6677
|
-
structuredContent: {
|
|
6678
|
-
key: input.key,
|
|
6679
|
-
updated: true
|
|
6680
|
-
}
|
|
6681
|
-
};
|
|
6682
|
-
} catch (error) {
|
|
6683
|
-
return formatToolError(error);
|
|
6684
|
-
}
|
|
6685
|
-
}
|
|
6686
|
-
);
|
|
6687
|
-
const transport = new StdioServerTransport();
|
|
6688
|
-
await server.connect(transport);
|
|
6689
|
-
}
|
|
6690
|
-
function formatToolError(error) {
|
|
6691
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
6692
|
-
return {
|
|
6693
|
-
content: [{ type: "text", text: message }],
|
|
6694
|
-
isError: true
|
|
6695
|
-
};
|
|
6696
|
-
}
|
|
6697
|
-
|
|
6698
|
-
// src/cli/commands/mcp.ts
|
|
6699
|
-
async function runMcpServeCommand() {
|
|
6700
|
-
await startIdeonMcpServer();
|
|
6701
|
-
}
|
|
6702
|
-
|
|
6703
|
-
// src/cli/commands/settings.tsx
|
|
6704
|
-
import { render } from "ink";
|
|
6705
|
-
|
|
6706
|
-
// src/cli/flows/settingsFlow.tsx
|
|
6707
|
-
import { useMemo, useState } from "react";
|
|
6708
|
-
import { Box, Text, useApp, useInput } from "ink";
|
|
6709
|
-
import SelectInput from "ink-select-input";
|
|
6710
|
-
import TextInput from "ink-text-input";
|
|
6711
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6712
|
-
function SettingsFlow({ initialSettings, initialSecrets, onDone }) {
|
|
6713
|
-
const { exit } = useApp();
|
|
6714
|
-
const [settings, setSettings] = useState(initialSettings);
|
|
6715
|
-
const [secrets, setSecrets] = useState(initialSecrets);
|
|
6716
|
-
const [editing, setEditing] = useState(null);
|
|
6717
|
-
const [showModelSelect, setShowModelSelect] = useState(false);
|
|
6718
|
-
const currentModel = getT2IModel(settings.t2i.modelId);
|
|
6719
|
-
useInput((input, key) => {
|
|
6720
|
-
if (key.escape) {
|
|
6721
|
-
if (editing) {
|
|
6722
|
-
setEditing(null);
|
|
6723
|
-
return;
|
|
6724
|
-
}
|
|
6725
|
-
if (showModelSelect) {
|
|
6726
|
-
setShowModelSelect(false);
|
|
6727
|
-
}
|
|
6728
|
-
}
|
|
6729
|
-
if (key.ctrl && input === "c") {
|
|
6730
|
-
onDone(null);
|
|
6731
|
-
exit();
|
|
6732
|
-
}
|
|
6733
|
-
});
|
|
6734
|
-
const menuItems = useMemo(() => {
|
|
6735
|
-
const t2iItems = currentModel.inputOptions.userConfigurable.map((fieldName) => ({
|
|
6736
|
-
label: `${fieldName}: ${formatValue(settings.t2i.inputOverrides[fieldName] ?? getT2IFieldDefault(settings.t2i.modelId, fieldName))}`,
|
|
6737
|
-
value: `t2i:${fieldName}`
|
|
6738
|
-
}));
|
|
6739
|
-
return [
|
|
6740
|
-
{
|
|
6741
|
-
label: `OpenRouter API key: ${secrets.openRouterApiKey ? "stored in keychain" : "missing"}`,
|
|
6742
|
-
value: "openrouter"
|
|
6743
|
-
},
|
|
6744
|
-
{
|
|
6745
|
-
label: `Replicate API token: ${secrets.replicateApiToken ? "stored in keychain" : "missing"}`,
|
|
6746
|
-
value: "replicate"
|
|
6747
|
-
},
|
|
6748
|
-
{
|
|
6749
|
-
label: `LLM model: ${settings.model}`,
|
|
6750
|
-
value: "llm-model"
|
|
6751
|
-
},
|
|
6752
|
-
{
|
|
6753
|
-
label: `Notifications > OS notifications enabled: ${settings.notifications.enabled ? "true" : "false"}`,
|
|
6754
|
-
value: "notifications-enabled"
|
|
6755
|
-
},
|
|
6756
|
-
{
|
|
6757
|
-
label: `Temperature: ${settings.modelSettings.temperature}`,
|
|
6758
|
-
value: "temperature"
|
|
6759
|
-
},
|
|
6760
|
-
{
|
|
6761
|
-
label: `Max tokens: ${settings.modelSettings.maxTokens}`,
|
|
6762
|
-
value: "maxTokens"
|
|
6763
|
-
},
|
|
6764
|
-
{
|
|
6765
|
-
label: `Top p: ${settings.modelSettings.topP}`,
|
|
6766
|
-
value: "topP"
|
|
6767
|
-
},
|
|
6768
|
-
{
|
|
6769
|
-
label: `Markdown output directory: ${settings.markdownOutputDir}`,
|
|
6770
|
-
value: "markdownOutputDir"
|
|
6771
|
-
},
|
|
6772
|
-
{
|
|
6773
|
-
label: `Asset output directory: ${settings.assetOutputDir}`,
|
|
6774
|
-
value: "assetOutputDir"
|
|
6775
|
-
},
|
|
6776
|
-
{
|
|
6777
|
-
label: `T2I model: ${currentModel.displayName}`,
|
|
6778
|
-
value: "t2i-model"
|
|
6779
|
-
},
|
|
6780
|
-
...t2iItems,
|
|
6781
|
-
{
|
|
6782
|
-
label: "Save and exit",
|
|
6783
|
-
value: "save"
|
|
6784
|
-
},
|
|
6785
|
-
{
|
|
6786
|
-
label: "Cancel",
|
|
6787
|
-
value: "cancel"
|
|
6788
|
-
}
|
|
6789
|
-
];
|
|
6790
|
-
}, [currentModel, secrets.openRouterApiKey, secrets.replicateApiToken, settings]);
|
|
6791
|
-
if (showModelSelect) {
|
|
6792
|
-
const items = getSupportedT2IModels().map((model) => ({
|
|
6793
|
-
label: `${model.displayName} (${model.modelId})`,
|
|
6794
|
-
value: model.modelId
|
|
6795
|
-
}));
|
|
6796
|
-
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
6797
|
-
/* @__PURE__ */ jsx(Text, { bold: true, color: "cyanBright", children: "Choose T2I Model" }),
|
|
6798
|
-
/* @__PURE__ */ jsx(Text, { color: "gray", children: "Press Esc to go back." }),
|
|
6799
|
-
/* @__PURE__ */ jsx(
|
|
6800
|
-
SelectInput,
|
|
6801
|
-
{
|
|
6802
|
-
items,
|
|
6803
|
-
onSelect: (item) => {
|
|
6804
|
-
setSettings((current) => ({
|
|
6805
|
-
...current,
|
|
6806
|
-
t2i: {
|
|
6807
|
-
modelId: item.value,
|
|
6808
|
-
inputOverrides: sanitizeT2IOverrides(item.value, current.t2i.inputOverrides)
|
|
6809
|
-
}
|
|
6810
|
-
}));
|
|
6811
|
-
setShowModelSelect(false);
|
|
6812
|
-
}
|
|
6813
|
-
}
|
|
6814
|
-
)
|
|
6815
|
-
] });
|
|
6816
|
-
}
|
|
6817
|
-
if (editing) {
|
|
6818
|
-
return /* @__PURE__ */ jsx(
|
|
6819
|
-
EditorView,
|
|
6820
|
-
{
|
|
6821
|
-
editing,
|
|
6822
|
-
onSubmit: (value2) => {
|
|
6823
|
-
applyEdit(editing.key, value2, settings, secrets, setSettings, setSecrets);
|
|
6824
|
-
setEditing(null);
|
|
6825
|
-
},
|
|
6826
|
-
onCancel: () => {
|
|
6827
|
-
setEditing(null);
|
|
6520
|
+
description: "Reset a setting to its default or delete a stored secret.",
|
|
6521
|
+
inputSchema: configUnsetToolInputSchema
|
|
6522
|
+
},
|
|
6523
|
+
async (input) => {
|
|
6524
|
+
try {
|
|
6525
|
+
if (!isConfigKey(input.key)) {
|
|
6526
|
+
throw new ReportedError(`Unsupported config key: ${input.key}`);
|
|
6828
6527
|
}
|
|
6528
|
+
await configUnset(input.key);
|
|
6529
|
+
return {
|
|
6530
|
+
content: [
|
|
6531
|
+
{
|
|
6532
|
+
type: "text",
|
|
6533
|
+
text: `Unset ${input.key}.`
|
|
6534
|
+
}
|
|
6535
|
+
],
|
|
6536
|
+
structuredContent: {
|
|
6537
|
+
key: input.key,
|
|
6538
|
+
updated: true
|
|
6539
|
+
}
|
|
6540
|
+
};
|
|
6541
|
+
} catch (error) {
|
|
6542
|
+
return formatToolError(error);
|
|
6829
6543
|
}
|
|
6830
|
-
|
|
6831
|
-
|
|
6832
|
-
|
|
6833
|
-
|
|
6834
|
-
/* @__PURE__ */ jsx(Text, { color: "gray", children: "Enter to edit. Esc backs out of nested menus. Ctrl+C cancels." }),
|
|
6835
|
-
/* @__PURE__ */ jsx(Box, { marginTop: 1, flexDirection: "column", children: /* @__PURE__ */ jsx(SelectInput, { items: menuItems, onSelect: (item) => handleMenuSelect(item.value, settings, secrets, setEditing, setShowModelSelect, onDone, exit) }) })
|
|
6836
|
-
] });
|
|
6544
|
+
}
|
|
6545
|
+
);
|
|
6546
|
+
const transport = new StdioServerTransport();
|
|
6547
|
+
await server.connect(transport);
|
|
6837
6548
|
}
|
|
6838
|
-
function
|
|
6839
|
-
|
|
6840
|
-
|
|
6841
|
-
|
|
6842
|
-
|
|
6843
|
-
|
|
6844
|
-
|
|
6845
|
-
|
|
6846
|
-
|
|
6847
|
-
|
|
6848
|
-
|
|
6849
|
-
|
|
6850
|
-
|
|
6851
|
-
|
|
6852
|
-
|
|
6853
|
-
|
|
6854
|
-
|
|
6855
|
-
|
|
6856
|
-
|
|
6857
|
-
|
|
6858
|
-
|
|
6859
|
-
|
|
6860
|
-
|
|
6861
|
-
|
|
6862
|
-
|
|
6863
|
-
|
|
6864
|
-
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: "Press Esc to return without changes." }) })
|
|
6865
|
-
] });
|
|
6549
|
+
function formatToolError(error) {
|
|
6550
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
6551
|
+
return {
|
|
6552
|
+
content: [{ type: "text", text: message }],
|
|
6553
|
+
isError: true
|
|
6554
|
+
};
|
|
6555
|
+
}
|
|
6556
|
+
|
|
6557
|
+
// src/cli/commands/mcp.ts
|
|
6558
|
+
async function runMcpServeCommand() {
|
|
6559
|
+
await startIdeonMcpServer();
|
|
6560
|
+
}
|
|
6561
|
+
|
|
6562
|
+
// src/cli/commands/settings.tsx
|
|
6563
|
+
import { render } from "ink";
|
|
6564
|
+
|
|
6565
|
+
// src/cli/flows/settingsFlow.tsx
|
|
6566
|
+
import { useEffect, useMemo, useState } from "react";
|
|
6567
|
+
import { Box, Text, useApp, useInput } from "ink";
|
|
6568
|
+
import SelectInput from "ink-select-input";
|
|
6569
|
+
import TextInput from "ink-text-input";
|
|
6570
|
+
|
|
6571
|
+
// src/images/limnModelCatalog.ts
|
|
6572
|
+
import { getSupportedModelCatalog } from "@telepat/limn";
|
|
6573
|
+
function getLimnGenerationModels() {
|
|
6574
|
+
return getSupportedModelCatalog().filter((entry) => entry.generationEnabled);
|
|
6866
6575
|
}
|
|
6867
|
-
|
|
6576
|
+
|
|
6577
|
+
// src/cli/flows/settingsFlowLogic.ts
|
|
6578
|
+
function handleMenuSelect(action, settings, secrets, setEditing, setShowModelSelect, setMenuMode, onDone, exit) {
|
|
6868
6579
|
switch (action) {
|
|
6869
6580
|
case "openrouter":
|
|
6870
6581
|
setEditing({ key: action, label: "OpenRouter API key", value: secrets.openRouterApiKey ?? "" });
|
|
@@ -6893,9 +6604,22 @@ function handleMenuSelect(action, settings, secrets, setEditing, setShowModelSel
|
|
|
6893
6604
|
case "assetOutputDir":
|
|
6894
6605
|
setEditing({ key: action, label: "Asset output directory", value: settings.assetOutputDir });
|
|
6895
6606
|
return;
|
|
6607
|
+
case "t2i-settings":
|
|
6608
|
+
setMenuMode("t2i");
|
|
6609
|
+
return;
|
|
6896
6610
|
case "t2i-model":
|
|
6897
6611
|
setShowModelSelect(true);
|
|
6898
6612
|
return;
|
|
6613
|
+
case "t2i-input-overrides":
|
|
6614
|
+
setEditing({
|
|
6615
|
+
key: action,
|
|
6616
|
+
label: "T2I input overrides (JSON)",
|
|
6617
|
+
value: JSON.stringify(settings.t2i.inputOverrides, null, 2)
|
|
6618
|
+
});
|
|
6619
|
+
return;
|
|
6620
|
+
case "t2i-back":
|
|
6621
|
+
setMenuMode("main");
|
|
6622
|
+
return;
|
|
6899
6623
|
case "save":
|
|
6900
6624
|
onDone({ settings, secrets });
|
|
6901
6625
|
exit();
|
|
@@ -6904,30 +6628,20 @@ function handleMenuSelect(action, settings, secrets, setEditing, setShowModelSel
|
|
|
6904
6628
|
onDone(null);
|
|
6905
6629
|
exit();
|
|
6906
6630
|
return;
|
|
6907
|
-
default:
|
|
6908
|
-
if (action.startsWith("t2i:")) {
|
|
6909
|
-
const fieldName = action.slice(4);
|
|
6910
|
-
const currentModel = getT2IModel(settings.t2i.modelId);
|
|
6911
|
-
setEditing({
|
|
6912
|
-
key: action,
|
|
6913
|
-
label: `${currentModel.displayName} \u2022 ${fieldName}`,
|
|
6914
|
-
value: formatEditorValue(settings.t2i.inputOverrides[fieldName] ?? getT2IFieldDefault(settings.t2i.modelId, fieldName))
|
|
6915
|
-
});
|
|
6916
|
-
}
|
|
6917
6631
|
}
|
|
6918
6632
|
}
|
|
6919
6633
|
function applyEdit(action, value2, settings, secrets, setSettings, setSecrets) {
|
|
6920
6634
|
if (action === "openrouter") {
|
|
6921
6635
|
setSecrets({ ...secrets, openRouterApiKey: value2.trim() || null });
|
|
6922
|
-
return;
|
|
6636
|
+
return true;
|
|
6923
6637
|
}
|
|
6924
6638
|
if (action === "replicate") {
|
|
6925
6639
|
setSecrets({ ...secrets, replicateApiToken: value2.trim() || null });
|
|
6926
|
-
return;
|
|
6640
|
+
return true;
|
|
6927
6641
|
}
|
|
6928
6642
|
if (action === "llm-model") {
|
|
6929
6643
|
setSettings({ ...settings, model: value2.trim() || settings.model });
|
|
6930
|
-
return;
|
|
6644
|
+
return true;
|
|
6931
6645
|
}
|
|
6932
6646
|
if (action === "notifications-enabled") {
|
|
6933
6647
|
const parsed = parseBooleanOrFallback(value2, settings.notifications.enabled);
|
|
@@ -6935,475 +6649,354 @@ function applyEdit(action, value2, settings, secrets, setSettings, setSecrets) {
|
|
|
6935
6649
|
...settings,
|
|
6936
6650
|
notifications: {
|
|
6937
6651
|
...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
|
|
6961
|
-
}
|
|
6962
|
-
});
|
|
6963
|
-
return;
|
|
6964
|
-
}
|
|
6965
|
-
if (action === "topP") {
|
|
6966
|
-
const nextTopP = clampNumber2(parseNumberOrFallback(value2, settings.modelSettings.topP), 0, 1);
|
|
6967
|
-
setSettings({
|
|
6968
|
-
...settings,
|
|
6969
|
-
modelSettings: {
|
|
6970
|
-
...settings.modelSettings,
|
|
6971
|
-
topP: nextTopP
|
|
6972
|
-
}
|
|
6973
|
-
});
|
|
6974
|
-
return;
|
|
6975
|
-
}
|
|
6976
|
-
if (action === "markdownOutputDir") {
|
|
6977
|
-
setSettings({ ...settings, markdownOutputDir: value2.trim() || settings.markdownOutputDir });
|
|
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
|
-
}
|
|
6993
|
-
setSettings({
|
|
6994
|
-
...settings,
|
|
6995
|
-
t2i: {
|
|
6996
|
-
...settings.t2i,
|
|
6997
|
-
inputOverrides: nextOverrides
|
|
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;
|
|
6652
|
+
enabled: parsed
|
|
6653
|
+
}
|
|
6654
|
+
});
|
|
6655
|
+
return true;
|
|
7120
6656
|
}
|
|
7121
|
-
|
|
7122
|
-
|
|
7123
|
-
|
|
7124
|
-
|
|
6657
|
+
if (action === "temperature") {
|
|
6658
|
+
const nextTemperature = clampNumber(parseNumberOrFallback(value2, settings.modelSettings.temperature), 0, 2);
|
|
6659
|
+
setSettings({
|
|
6660
|
+
...settings,
|
|
6661
|
+
modelSettings: {
|
|
6662
|
+
...settings.modelSettings,
|
|
6663
|
+
temperature: nextTemperature
|
|
6664
|
+
}
|
|
6665
|
+
});
|
|
6666
|
+
return true;
|
|
7125
6667
|
}
|
|
7126
|
-
|
|
7127
|
-
|
|
7128
|
-
|
|
7129
|
-
|
|
7130
|
-
|
|
7131
|
-
|
|
7132
|
-
|
|
6668
|
+
if (action === "maxTokens") {
|
|
6669
|
+
const nextMaxTokens = Math.max(1, Math.round(parseNumberOrFallback(value2, settings.modelSettings.maxTokens)));
|
|
6670
|
+
setSettings({
|
|
6671
|
+
...settings,
|
|
6672
|
+
modelSettings: {
|
|
6673
|
+
...settings.modelSettings,
|
|
6674
|
+
maxTokens: nextMaxTokens
|
|
6675
|
+
}
|
|
6676
|
+
});
|
|
6677
|
+
return true;
|
|
7133
6678
|
}
|
|
7134
|
-
|
|
7135
|
-
|
|
7136
|
-
|
|
7137
|
-
|
|
7138
|
-
|
|
7139
|
-
|
|
7140
|
-
|
|
7141
|
-
|
|
7142
|
-
|
|
7143
|
-
return
|
|
6679
|
+
if (action === "topP") {
|
|
6680
|
+
const nextTopP = clampNumber(parseNumberOrFallback(value2, settings.modelSettings.topP), 0, 1);
|
|
6681
|
+
setSettings({
|
|
6682
|
+
...settings,
|
|
6683
|
+
modelSettings: {
|
|
6684
|
+
...settings.modelSettings,
|
|
6685
|
+
topP: nextTopP
|
|
6686
|
+
}
|
|
6687
|
+
});
|
|
6688
|
+
return true;
|
|
7144
6689
|
}
|
|
7145
|
-
|
|
7146
|
-
}
|
|
7147
|
-
|
|
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
|
-
);
|
|
6690
|
+
if (action === "markdownOutputDir") {
|
|
6691
|
+
setSettings({ ...settings, markdownOutputDir: value2.trim() || settings.markdownOutputDir });
|
|
6692
|
+
return true;
|
|
7153
6693
|
}
|
|
7154
|
-
|
|
7155
|
-
|
|
7156
|
-
|
|
7157
|
-
const fileStat = await stat4(candidate);
|
|
7158
|
-
if (fileStat.mtimeMs >= latestMtime) {
|
|
7159
|
-
latestMtime = fileStat.mtimeMs;
|
|
7160
|
-
latestPath = candidate;
|
|
7161
|
-
}
|
|
6694
|
+
if (action === "assetOutputDir") {
|
|
6695
|
+
setSettings({ ...settings, assetOutputDir: value2.trim() || settings.assetOutputDir });
|
|
6696
|
+
return true;
|
|
7162
6697
|
}
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
7168
|
-
|
|
7169
|
-
|
|
6698
|
+
if (action === "t2i-input-overrides") {
|
|
6699
|
+
const trimmed = value2.trim();
|
|
6700
|
+
if (trimmed.length === 0) {
|
|
6701
|
+
setSettings({
|
|
6702
|
+
...settings,
|
|
6703
|
+
t2i: {
|
|
6704
|
+
...settings.t2i,
|
|
6705
|
+
inputOverrides: {}
|
|
6706
|
+
}
|
|
6707
|
+
});
|
|
6708
|
+
return true;
|
|
7170
6709
|
}
|
|
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
6710
|
try {
|
|
7201
|
-
const
|
|
7202
|
-
|
|
7203
|
-
|
|
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]);
|
|
6711
|
+
const parsed = JSON.parse(trimmed);
|
|
6712
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
6713
|
+
return false;
|
|
7221
6714
|
}
|
|
6715
|
+
setSettings({
|
|
6716
|
+
...settings,
|
|
6717
|
+
t2i: {
|
|
6718
|
+
...settings.t2i,
|
|
6719
|
+
inputOverrides: parsed
|
|
6720
|
+
}
|
|
6721
|
+
});
|
|
6722
|
+
return true;
|
|
7222
6723
|
} catch {
|
|
6724
|
+
return false;
|
|
7223
6725
|
}
|
|
7224
6726
|
}
|
|
7225
|
-
|
|
7226
|
-
for (const [id, outputs] of grouped.entries()) {
|
|
7227
|
-
outputs.sort((a, b) => compareContentTypes(a.contentType, b.contentType) || a.index - b.index || b.mtime - a.mtime);
|
|
7228
|
-
const primaryContentType = await resolvePrimaryContentType(outputs);
|
|
7229
|
-
const primary = outputs.find((output) => output.contentType === primaryContentType) ?? outputs[0];
|
|
7230
|
-
if (!primary) {
|
|
7231
|
-
continue;
|
|
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
|
|
7242
|
-
});
|
|
7243
|
-
}
|
|
7244
|
-
generations.sort((a, b) => b.mtime - a.mtime);
|
|
7245
|
-
return generations;
|
|
6727
|
+
return false;
|
|
7246
6728
|
}
|
|
7247
|
-
function
|
|
7248
|
-
const
|
|
7249
|
-
|
|
7250
|
-
|
|
7251
|
-
|
|
6729
|
+
function parseNumberOrFallback(value2, fallback) {
|
|
6730
|
+
const parsed = Number(value2.trim());
|
|
6731
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
6732
|
+
}
|
|
6733
|
+
function clampNumber(value2, minimum, maximum) {
|
|
6734
|
+
return Math.min(maximum, Math.max(minimum, value2));
|
|
6735
|
+
}
|
|
6736
|
+
function parseBooleanOrFallback(value2, fallback) {
|
|
6737
|
+
const normalized = value2.trim().toLowerCase();
|
|
6738
|
+
if (normalized === "true") {
|
|
6739
|
+
return true;
|
|
7252
6740
|
}
|
|
7253
|
-
|
|
7254
|
-
|
|
7255
|
-
return path10.basename(markdownPath, ".md");
|
|
6741
|
+
if (normalized === "false") {
|
|
6742
|
+
return false;
|
|
7256
6743
|
}
|
|
7257
|
-
return
|
|
6744
|
+
return fallback;
|
|
7258
6745
|
}
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
|
|
7262
|
-
|
|
7263
|
-
|
|
7264
|
-
|
|
7265
|
-
|
|
6746
|
+
|
|
6747
|
+
// src/cli/flows/settingsFlow.tsx
|
|
6748
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6749
|
+
function SettingsFlow({ initialSettings, initialSecrets, onDone }) {
|
|
6750
|
+
const { exit } = useApp();
|
|
6751
|
+
const [settings, setSettings] = useState(initialSettings);
|
|
6752
|
+
const [secrets, setSecrets] = useState(initialSecrets);
|
|
6753
|
+
const [editing, setEditing] = useState(null);
|
|
6754
|
+
const [showModelSelect, setShowModelSelect] = useState(false);
|
|
6755
|
+
const [menuMode, setMenuMode] = useState("main");
|
|
6756
|
+
const currentModelEntry = getLimnGenerationModels().find((m) => m.family === settings.t2i.modelId) ?? getLimnGenerationModels()[0];
|
|
6757
|
+
useInput((input, key) => {
|
|
6758
|
+
if (key.escape) {
|
|
6759
|
+
if (editing) {
|
|
6760
|
+
setEditing(null);
|
|
6761
|
+
return;
|
|
6762
|
+
}
|
|
6763
|
+
if (showModelSelect) {
|
|
6764
|
+
setShowModelSelect(false);
|
|
6765
|
+
return;
|
|
6766
|
+
}
|
|
6767
|
+
if (menuMode === "t2i") {
|
|
6768
|
+
setMenuMode("main");
|
|
6769
|
+
return;
|
|
6770
|
+
}
|
|
7266
6771
|
}
|
|
7267
|
-
|
|
7268
|
-
|
|
7269
|
-
|
|
7270
|
-
} catch {
|
|
7271
|
-
continue;
|
|
6772
|
+
if (key.ctrl && input === "c") {
|
|
6773
|
+
onDone(null);
|
|
6774
|
+
exit();
|
|
7272
6775
|
}
|
|
7273
|
-
|
|
7274
|
-
|
|
7275
|
-
|
|
7276
|
-
|
|
7277
|
-
|
|
6776
|
+
});
|
|
6777
|
+
const formatT2iOverridesSummary = (overrides) => {
|
|
6778
|
+
const count = Object.keys(overrides).length;
|
|
6779
|
+
return count === 0 ? "none" : `${count} override${count === 1 ? "" : "s"}`;
|
|
6780
|
+
};
|
|
6781
|
+
const menuItems = useMemo(() => {
|
|
6782
|
+
const t2iSubmenu = [
|
|
6783
|
+
{
|
|
6784
|
+
label: `T2I model: ${currentModelEntry?.displayName ?? settings.t2i.modelId}`,
|
|
6785
|
+
value: "t2i-model"
|
|
6786
|
+
},
|
|
6787
|
+
{
|
|
6788
|
+
label: `T2I input overrides: ${formatT2iOverridesSummary(settings.t2i.inputOverrides)}`,
|
|
6789
|
+
value: "t2i-input-overrides"
|
|
6790
|
+
},
|
|
6791
|
+
{
|
|
6792
|
+
label: "Back",
|
|
6793
|
+
value: "t2i-back"
|
|
7278
6794
|
}
|
|
7279
|
-
|
|
7280
|
-
|
|
6795
|
+
];
|
|
6796
|
+
if (menuMode === "t2i") {
|
|
6797
|
+
return t2iSubmenu;
|
|
6798
|
+
}
|
|
6799
|
+
return [
|
|
6800
|
+
{
|
|
6801
|
+
label: `OpenRouter API key: ${secrets.openRouterApiKey ? "stored in keychain" : "missing"}`,
|
|
6802
|
+
value: "openrouter"
|
|
6803
|
+
},
|
|
6804
|
+
{
|
|
6805
|
+
label: `Replicate API token: ${secrets.replicateApiToken ? "stored in keychain" : "missing"}`,
|
|
6806
|
+
value: "replicate"
|
|
6807
|
+
},
|
|
6808
|
+
{
|
|
6809
|
+
label: `LLM model: ${settings.model}`,
|
|
6810
|
+
value: "llm-model"
|
|
6811
|
+
},
|
|
6812
|
+
{
|
|
6813
|
+
label: `Notifications > OS notifications enabled: ${settings.notifications.enabled ? "true" : "false"}`,
|
|
6814
|
+
value: "notifications-enabled"
|
|
6815
|
+
},
|
|
6816
|
+
{
|
|
6817
|
+
label: `Temperature: ${settings.modelSettings.temperature}`,
|
|
6818
|
+
value: "temperature"
|
|
6819
|
+
},
|
|
6820
|
+
{
|
|
6821
|
+
label: `Max tokens: ${settings.modelSettings.maxTokens}`,
|
|
6822
|
+
value: "maxTokens"
|
|
6823
|
+
},
|
|
6824
|
+
{
|
|
6825
|
+
label: `Top p: ${settings.modelSettings.topP}`,
|
|
6826
|
+
value: "topP"
|
|
6827
|
+
},
|
|
6828
|
+
{
|
|
6829
|
+
label: `Markdown output directory: ${settings.markdownOutputDir}`,
|
|
6830
|
+
value: "markdownOutputDir"
|
|
6831
|
+
},
|
|
6832
|
+
{
|
|
6833
|
+
label: `Asset output directory: ${settings.assetOutputDir}`,
|
|
6834
|
+
value: "assetOutputDir"
|
|
6835
|
+
},
|
|
6836
|
+
{
|
|
6837
|
+
label: `T2I settings: ${currentModelEntry?.displayName ?? settings.t2i.modelId}`,
|
|
6838
|
+
value: "t2i-settings"
|
|
6839
|
+
},
|
|
6840
|
+
{
|
|
6841
|
+
label: "Save and exit",
|
|
6842
|
+
value: "save"
|
|
6843
|
+
},
|
|
6844
|
+
{
|
|
6845
|
+
label: "Cancel",
|
|
6846
|
+
value: "cancel"
|
|
7281
6847
|
}
|
|
7282
|
-
|
|
7283
|
-
}
|
|
7284
|
-
|
|
7285
|
-
|
|
7286
|
-
|
|
7287
|
-
|
|
7288
|
-
|
|
7289
|
-
|
|
7290
|
-
|
|
7291
|
-
|
|
7292
|
-
|
|
7293
|
-
|
|
7294
|
-
|
|
7295
|
-
|
|
6848
|
+
];
|
|
6849
|
+
}, [currentModelEntry, menuMode, secrets.openRouterApiKey, secrets.replicateApiToken, settings]);
|
|
6850
|
+
if (showModelSelect) {
|
|
6851
|
+
const items = getLimnGenerationModels().map((model) => ({
|
|
6852
|
+
label: `${model.displayName} (${model.family})`,
|
|
6853
|
+
value: model.family
|
|
6854
|
+
}));
|
|
6855
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
6856
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "cyanBright", children: "Choose T2I Model" }),
|
|
6857
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: "Press Esc to go back." }),
|
|
6858
|
+
/* @__PURE__ */ jsx(
|
|
6859
|
+
SelectInput,
|
|
6860
|
+
{
|
|
6861
|
+
items,
|
|
6862
|
+
onSelect: (item) => {
|
|
6863
|
+
setSettings((current) => ({
|
|
6864
|
+
...current,
|
|
6865
|
+
t2i: {
|
|
6866
|
+
modelId: item.value,
|
|
6867
|
+
inputOverrides: {}
|
|
6868
|
+
}
|
|
6869
|
+
}));
|
|
6870
|
+
setShowModelSelect(false);
|
|
6871
|
+
}
|
|
6872
|
+
}
|
|
6873
|
+
)
|
|
6874
|
+
] });
|
|
7296
6875
|
}
|
|
7297
|
-
|
|
7298
|
-
|
|
7299
|
-
|
|
7300
|
-
|
|
7301
|
-
|
|
7302
|
-
|
|
7303
|
-
|
|
7304
|
-
|
|
7305
|
-
|
|
7306
|
-
|
|
7307
|
-
|
|
7308
|
-
|
|
7309
|
-
|
|
7310
|
-
|
|
7311
|
-
|
|
6876
|
+
if (editing) {
|
|
6877
|
+
return /* @__PURE__ */ jsx(
|
|
6878
|
+
EditorView,
|
|
6879
|
+
{
|
|
6880
|
+
editing,
|
|
6881
|
+
onSubmit: (value2) => {
|
|
6882
|
+
const accepted = applyEdit(editing.key, value2, settings, secrets, setSettings, setSecrets);
|
|
6883
|
+
if (accepted) {
|
|
6884
|
+
setEditing(null);
|
|
6885
|
+
}
|
|
6886
|
+
return accepted;
|
|
6887
|
+
},
|
|
6888
|
+
onCancel: () => {
|
|
6889
|
+
setEditing(null);
|
|
6890
|
+
}
|
|
6891
|
+
}
|
|
6892
|
+
);
|
|
7312
6893
|
}
|
|
7313
|
-
return
|
|
6894
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
6895
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "cyanBright", children: "Ideon Settings" }),
|
|
6896
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: "Enter to edit. Esc backs out of nested menus. Ctrl+C cancels." }),
|
|
6897
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, flexDirection: "column", children: /* @__PURE__ */ jsx(
|
|
6898
|
+
SelectInput,
|
|
6899
|
+
{
|
|
6900
|
+
items: menuItems,
|
|
6901
|
+
onSelect: (item) => handleMenuSelect(item.value, settings, secrets, setEditing, setShowModelSelect, setMenuMode, onDone, exit)
|
|
6902
|
+
}
|
|
6903
|
+
) })
|
|
6904
|
+
] });
|
|
7314
6905
|
}
|
|
7315
|
-
function
|
|
7316
|
-
|
|
7317
|
-
|
|
7318
|
-
|
|
7319
|
-
|
|
7320
|
-
|
|
6906
|
+
function EditorView({
|
|
6907
|
+
editing,
|
|
6908
|
+
onSubmit,
|
|
6909
|
+
onCancel
|
|
6910
|
+
}) {
|
|
6911
|
+
const [value2, setValue] = useState(editing.value);
|
|
6912
|
+
const [error, setError] = useState(null);
|
|
6913
|
+
useEffect(() => {
|
|
6914
|
+
setValue(editing.value);
|
|
6915
|
+
setError(null);
|
|
6916
|
+
}, [editing]);
|
|
6917
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
6918
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "cyanBright", children: editing.label }),
|
|
6919
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: "Enter saves. Blank value clears nullable secrets and overrides. Esc cancels." }),
|
|
6920
|
+
/* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
|
|
6921
|
+
/* @__PURE__ */ jsx(Text, { children: "> " }),
|
|
6922
|
+
/* @__PURE__ */ jsx(
|
|
6923
|
+
TextInput,
|
|
6924
|
+
{
|
|
6925
|
+
value: value2,
|
|
6926
|
+
onChange: setValue,
|
|
6927
|
+
onSubmit: (nextValue) => {
|
|
6928
|
+
const accepted = onSubmit(nextValue);
|
|
6929
|
+
if (!accepted) {
|
|
6930
|
+
setError("Invalid JSON. Please enter an object or leave blank to clear.");
|
|
6931
|
+
}
|
|
6932
|
+
}
|
|
6933
|
+
}
|
|
6934
|
+
)
|
|
6935
|
+
] }),
|
|
6936
|
+
error ? /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { color: "red", children: error }) }) : null,
|
|
6937
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
|
|
6938
|
+
"Current value: ",
|
|
6939
|
+
editing.value || "(empty)"
|
|
6940
|
+
] }) }),
|
|
6941
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: "Press Esc to return without changes." }) })
|
|
6942
|
+
] });
|
|
7321
6943
|
}
|
|
7322
|
-
|
|
7323
|
-
|
|
7324
|
-
|
|
7325
|
-
|
|
7326
|
-
|
|
6944
|
+
|
|
6945
|
+
// src/cli/commands/settings.tsx
|
|
6946
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
6947
|
+
async function openSettings() {
|
|
6948
|
+
const envSettings = readEnvSettings();
|
|
6949
|
+
const [settings, secrets] = await Promise.all([
|
|
6950
|
+
loadSavedSettings(),
|
|
6951
|
+
loadSecrets({ disableKeytar: envSettings.disableKeytar })
|
|
6952
|
+
]);
|
|
6953
|
+
let result = null;
|
|
6954
|
+
const app = render(
|
|
6955
|
+
/* @__PURE__ */ jsx2(
|
|
6956
|
+
SettingsFlow,
|
|
6957
|
+
{
|
|
6958
|
+
initialSettings: settings,
|
|
6959
|
+
initialSecrets: secrets,
|
|
6960
|
+
onDone: (value2) => {
|
|
6961
|
+
result = value2;
|
|
6962
|
+
}
|
|
6963
|
+
}
|
|
6964
|
+
)
|
|
6965
|
+
);
|
|
6966
|
+
await app.waitUntilExit();
|
|
6967
|
+
const finalResult = result;
|
|
6968
|
+
if (!finalResult) {
|
|
6969
|
+
console.log("Settings unchanged.");
|
|
6970
|
+
return;
|
|
7327
6971
|
}
|
|
7328
|
-
const
|
|
6972
|
+
const savedResult = finalResult;
|
|
6973
|
+
await saveSettings(savedResult.settings);
|
|
7329
6974
|
try {
|
|
7330
|
-
|
|
7331
|
-
|
|
7332
|
-
|
|
7333
|
-
|
|
7334
|
-
|
|
7335
|
-
return
|
|
6975
|
+
await saveSecrets(savedResult.secrets, { disableKeytar: envSettings.disableKeytar });
|
|
6976
|
+
} catch (error) {
|
|
6977
|
+
if (error instanceof KeytarUnavailableError) {
|
|
6978
|
+
console.log("Settings saved, but secrets were not stored in the system keychain.");
|
|
6979
|
+
console.log("Use IDEON_OPENROUTER_API_KEY and IDEON_REPLICATE_API_TOKEN in this environment.");
|
|
6980
|
+
return;
|
|
7336
6981
|
}
|
|
7337
|
-
|
|
7338
|
-
return fallback;
|
|
6982
|
+
throw error;
|
|
7339
6983
|
}
|
|
7340
|
-
|
|
6984
|
+
console.log(`Settings saved to ${getSettingsFilePath()}.`);
|
|
7341
6985
|
}
|
|
7342
6986
|
|
|
6987
|
+
// src/cli/commands/serve.ts
|
|
6988
|
+
import path13 from "path";
|
|
6989
|
+
import { spawn } from "child_process";
|
|
6990
|
+
|
|
7343
6991
|
// src/server/previewServer.ts
|
|
7344
6992
|
import { execFile } from "child_process";
|
|
7345
6993
|
import { promisify } from "util";
|
|
7346
|
-
import { readFile as
|
|
6994
|
+
import { readFile as readFile9, stat as stat6 } from "fs/promises";
|
|
7347
6995
|
import { watch as fsWatch } from "fs";
|
|
7348
|
-
import
|
|
6996
|
+
import path12 from "path";
|
|
7349
6997
|
import { fileURLToPath } from "url";
|
|
7350
6998
|
import express from "express";
|
|
7351
6999
|
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
7000
|
var execFileAsync = promisify(execFile);
|
|
7408
7001
|
var MissingArticleError = class extends Error {
|
|
7409
7002
|
constructor(message) {
|
|
@@ -7498,7 +7091,7 @@ async function startPreviewServer(options) {
|
|
|
7498
7091
|
if (options.watch) {
|
|
7499
7092
|
let html2;
|
|
7500
7093
|
try {
|
|
7501
|
-
html2 = await
|
|
7094
|
+
html2 = await readFile9(path12.join(previewClientDir, "index.html"), "utf8");
|
|
7502
7095
|
} catch {
|
|
7503
7096
|
res.status(200).type("html").send(
|
|
7504
7097
|
`<!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 +7102,7 @@ async function startPreviewServer(options) {
|
|
|
7509
7102
|
const injected = html2.replace("</body>", `${reloadScript}</body>`);
|
|
7510
7103
|
res.status(200).type("html").send(injected);
|
|
7511
7104
|
} else {
|
|
7512
|
-
res.status(200).sendFile(
|
|
7105
|
+
res.status(200).sendFile(path12.join(previewClientDir, "index.html"));
|
|
7513
7106
|
}
|
|
7514
7107
|
return;
|
|
7515
7108
|
}
|
|
@@ -7562,7 +7155,7 @@ async function getArticleContent(generationId, markdownOutputDir) {
|
|
|
7562
7155
|
generation.outputs.map(async (output) => {
|
|
7563
7156
|
let markdown = "";
|
|
7564
7157
|
try {
|
|
7565
|
-
markdown = await
|
|
7158
|
+
markdown = await readFile9(output.sourcePath, "utf8");
|
|
7566
7159
|
} catch (error) {
|
|
7567
7160
|
if (isMissingFileError(error)) {
|
|
7568
7161
|
throw new MissingArticleError(`Generation "${generationId}" no longer exists.`);
|
|
@@ -7580,7 +7173,7 @@ async function getArticleContent(generationId, markdownOutputDir) {
|
|
|
7580
7173
|
};
|
|
7581
7174
|
})
|
|
7582
7175
|
);
|
|
7583
|
-
const generationDir =
|
|
7176
|
+
const generationDir = path12.dirname(generation.outputs[0]?.sourcePath ?? "");
|
|
7584
7177
|
const interactions = generationDir ? await loadSavedInteractions(generationDir) : { llmCalls: [], t2iCalls: [] };
|
|
7585
7178
|
const analyticsSummary = generationDir ? await loadSavedAnalyticsSummary(generationDir) : null;
|
|
7586
7179
|
return {
|
|
@@ -7609,7 +7202,7 @@ async function resolveActivePreviewArticle(preferredMarkdownPath, markdownOutput
|
|
|
7609
7202
|
};
|
|
7610
7203
|
}
|
|
7611
7204
|
function resolveGenerationSourcePath(generation, markdownOutputDir) {
|
|
7612
|
-
return generation.outputs.find((output) => output.contentType === generation.primaryContentType)?.sourcePath ?? generation.outputs[0]?.sourcePath ??
|
|
7205
|
+
return generation.outputs.find((output) => output.contentType === generation.primaryContentType)?.sourcePath ?? generation.outputs[0]?.sourcePath ?? path12.join(markdownOutputDir, generation.id);
|
|
7613
7206
|
}
|
|
7614
7207
|
function isMissingFileError(error) {
|
|
7615
7208
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
@@ -7624,7 +7217,7 @@ async function renderArticleHtml(markdown, generationId, sourcePath) {
|
|
|
7624
7217
|
async function loadSavedLinks(markdownPath) {
|
|
7625
7218
|
const linksPath = resolveLinksPath(markdownPath);
|
|
7626
7219
|
try {
|
|
7627
|
-
const raw = await
|
|
7220
|
+
const raw = await readFile9(linksPath, "utf8");
|
|
7628
7221
|
const parsed = JSON.parse(raw);
|
|
7629
7222
|
if (!Array.isArray(parsed.links)) {
|
|
7630
7223
|
return [];
|
|
@@ -7648,9 +7241,9 @@ async function loadSavedLinks(markdownPath) {
|
|
|
7648
7241
|
}
|
|
7649
7242
|
}
|
|
7650
7243
|
async function loadSavedInteractions(generationDir) {
|
|
7651
|
-
const interactionsPath =
|
|
7244
|
+
const interactionsPath = path12.join(generationDir, "model.interactions.json");
|
|
7652
7245
|
try {
|
|
7653
|
-
const raw = await
|
|
7246
|
+
const raw = await readFile9(interactionsPath, "utf8");
|
|
7654
7247
|
const parsed = JSON.parse(raw);
|
|
7655
7248
|
const llmCalls = Array.isArray(parsed.llmCalls) ? parsed.llmCalls.filter(isPreviewLlmInteraction) : [];
|
|
7656
7249
|
const t2iCalls = Array.isArray(parsed.t2iCalls) ? parsed.t2iCalls.filter(isPreviewT2IInteraction) : [];
|
|
@@ -7666,9 +7259,9 @@ async function loadSavedInteractions(generationDir) {
|
|
|
7666
7259
|
}
|
|
7667
7260
|
}
|
|
7668
7261
|
async function loadSavedAnalyticsSummary(generationDir) {
|
|
7669
|
-
const analyticsPath =
|
|
7262
|
+
const analyticsPath = path12.join(generationDir, "generation.analytics.json");
|
|
7670
7263
|
try {
|
|
7671
|
-
const raw = await
|
|
7264
|
+
const raw = await readFile9(analyticsPath, "utf8");
|
|
7672
7265
|
const parsed = JSON.parse(raw);
|
|
7673
7266
|
const summary = parsed.summary;
|
|
7674
7267
|
if (!summary || typeof summary !== "object") {
|
|
@@ -7700,14 +7293,14 @@ async function getPreviewBootstrapData(preferredMarkdownPath, markdownOutputDir)
|
|
|
7700
7293
|
};
|
|
7701
7294
|
}
|
|
7702
7295
|
async function resolvePreviewClientBuildDir() {
|
|
7703
|
-
const currentDir =
|
|
7296
|
+
const currentDir = path12.dirname(fileURLToPath(import.meta.url));
|
|
7704
7297
|
const candidates = [
|
|
7705
|
-
|
|
7706
|
-
|
|
7298
|
+
path12.resolve(currentDir, "preview"),
|
|
7299
|
+
path12.resolve(currentDir, "../../dist/preview")
|
|
7707
7300
|
];
|
|
7708
7301
|
for (const candidate of candidates) {
|
|
7709
7302
|
try {
|
|
7710
|
-
const indexStat = await
|
|
7303
|
+
const indexStat = await stat6(path12.join(candidate, "index.html"));
|
|
7711
7304
|
if (indexStat.isFile()) {
|
|
7712
7305
|
return candidate;
|
|
7713
7306
|
}
|
|
@@ -7769,21 +7362,21 @@ async function resolveGenerationAssetPath(generationId, rawAssetPath, markdownOu
|
|
|
7769
7362
|
throw new MissingArticleError(`Generation "${generationId}" no longer exists.`);
|
|
7770
7363
|
}
|
|
7771
7364
|
const decodedAssetPath = decodeURIComponent(rawAssetPath);
|
|
7772
|
-
const normalizedRelative =
|
|
7773
|
-
if (normalizedRelative.length === 0 || normalizedRelative === "." || normalizedRelative.startsWith("../") || normalizedRelative.includes("/../") ||
|
|
7365
|
+
const normalizedRelative = path12.posix.normalize(decodedAssetPath.replace(/\\/g, "/"));
|
|
7366
|
+
if (normalizedRelative.length === 0 || normalizedRelative === "." || normalizedRelative.startsWith("../") || normalizedRelative.includes("/../") || path12.posix.isAbsolute(normalizedRelative)) {
|
|
7774
7367
|
throw new Error("Invalid generation asset path.");
|
|
7775
7368
|
}
|
|
7776
|
-
const generationDir =
|
|
7369
|
+
const generationDir = path12.dirname(generation.outputs[0]?.sourcePath ?? "");
|
|
7777
7370
|
if (!generationDir) {
|
|
7778
7371
|
throw new MissingArticleError(`Generation "${generationId}" has no source directory.`);
|
|
7779
7372
|
}
|
|
7780
|
-
const resolvedPath =
|
|
7781
|
-
const relativeToGeneration =
|
|
7782
|
-
if (relativeToGeneration.startsWith("..") ||
|
|
7373
|
+
const resolvedPath = path12.resolve(generationDir, normalizedRelative);
|
|
7374
|
+
const relativeToGeneration = path12.relative(generationDir, resolvedPath);
|
|
7375
|
+
if (relativeToGeneration.startsWith("..") || path12.isAbsolute(relativeToGeneration)) {
|
|
7783
7376
|
throw new Error("Invalid generation asset path.");
|
|
7784
7377
|
}
|
|
7785
7378
|
try {
|
|
7786
|
-
const fileStat = await
|
|
7379
|
+
const fileStat = await stat6(resolvedPath);
|
|
7787
7380
|
if (!fileStat.isFile()) {
|
|
7788
7381
|
throw new Error("Invalid generation asset path.");
|
|
7789
7382
|
}
|
|
@@ -9263,7 +8856,7 @@ async function runServeCommand(options) {
|
|
|
9263
8856
|
const markdownPath = await resolveMarkdownPath(options.markdownPath, outputPaths.markdownOutputDir, process.cwd());
|
|
9264
8857
|
const port = parsePort(options.port);
|
|
9265
8858
|
if (options.watch) {
|
|
9266
|
-
const viteBin =
|
|
8859
|
+
const viteBin = path13.resolve(process.cwd(), "node_modules", ".bin", "vite");
|
|
9267
8860
|
const viteProcess = spawn(viteBin, ["build", "--watch"], {
|
|
9268
8861
|
stdio: "inherit",
|
|
9269
8862
|
shell: process.platform === "win32"
|
|
@@ -9289,8 +8882,8 @@ async function runServeCommand(options) {
|
|
|
9289
8882
|
openBrowser: options.openBrowser,
|
|
9290
8883
|
watch: options.watch
|
|
9291
8884
|
});
|
|
9292
|
-
const relativeArticle =
|
|
9293
|
-
const relativeAssets =
|
|
8885
|
+
const relativeArticle = path13.relative(process.cwd(), markdownPath);
|
|
8886
|
+
const relativeAssets = path13.relative(process.cwd(), outputPaths.assetOutputDir);
|
|
9294
8887
|
console.log(`Previewing ${relativeArticle || markdownPath}`);
|
|
9295
8888
|
console.log(`Serving assets from ${relativeAssets || outputPaths.assetOutputDir}`);
|
|
9296
8889
|
console.log(`Open ${server.url}`);
|
|
@@ -9301,7 +8894,7 @@ async function runServeCommand(options) {
|
|
|
9301
8894
|
}
|
|
9302
8895
|
|
|
9303
8896
|
// src/cli/commands/write.tsx
|
|
9304
|
-
import React4, { useEffect as
|
|
8897
|
+
import React4, { useEffect as useEffect3, useState as useState4 } from "react";
|
|
9305
8898
|
import { render as render2, useApp as useApp3 } from "ink";
|
|
9306
8899
|
import { createInterface } from "readline/promises";
|
|
9307
8900
|
|
|
@@ -9428,7 +9021,7 @@ function FinalSummary({
|
|
|
9428
9021
|
}
|
|
9429
9022
|
|
|
9430
9023
|
// src/cli/ui/stageRow.tsx
|
|
9431
|
-
import { useEffect, useState as useState2 } from "react";
|
|
9024
|
+
import { useEffect as useEffect2, useState as useState2 } from "react";
|
|
9432
9025
|
import { Box as Box3, Text as Text3 } from "ink";
|
|
9433
9026
|
|
|
9434
9027
|
// src/cli/ui/progressVisibility.ts
|
|
@@ -9502,7 +9095,7 @@ function StageRow({
|
|
|
9502
9095
|
maxVisibleItems = 12
|
|
9503
9096
|
}) {
|
|
9504
9097
|
const [frameIndex, setFrameIndex] = useState2(0);
|
|
9505
|
-
|
|
9098
|
+
useEffect2(() => {
|
|
9506
9099
|
if (!isActive || stage.status !== "running") {
|
|
9507
9100
|
setFrameIndex(0);
|
|
9508
9101
|
return;
|
|
@@ -9563,7 +9156,7 @@ function ItemRows({
|
|
|
9563
9156
|
}
|
|
9564
9157
|
function ItemRow({ item, isActive }) {
|
|
9565
9158
|
const [frameIndex, setFrameIndex] = useState2(0);
|
|
9566
|
-
|
|
9159
|
+
useEffect2(() => {
|
|
9567
9160
|
if (!isActive || item.status !== "running") {
|
|
9568
9161
|
setFrameIndex(0);
|
|
9569
9162
|
return;
|
|
@@ -9776,7 +9369,7 @@ function formatPipelineStageCost(stage) {
|
|
|
9776
9369
|
}
|
|
9777
9370
|
return stage.costSource === "estimated" ? `~${formatted}` : formatted;
|
|
9778
9371
|
}
|
|
9779
|
-
async function renderPlainPipeline(input, dryRun, enrichLinks2, runMode, links, unlinks, maxLinks) {
|
|
9372
|
+
async function renderPlainPipeline(input, dryRun, enrichLinks2, runMode, links, unlinks, maxLinks, maxImages) {
|
|
9780
9373
|
let previousStages = /* @__PURE__ */ new Map();
|
|
9781
9374
|
let previousItemStatuses = /* @__PURE__ */ new Map();
|
|
9782
9375
|
const notificationsEnabled = input.config.settings.notifications.enabled;
|
|
@@ -9793,6 +9386,7 @@ async function renderPlainPipeline(input, dryRun, enrichLinks2, runMode, links,
|
|
|
9793
9386
|
customLinks: links,
|
|
9794
9387
|
unlinks,
|
|
9795
9388
|
maxLinks,
|
|
9389
|
+
maxImages,
|
|
9796
9390
|
onUpdate(stages) {
|
|
9797
9391
|
for (const stage of stages) {
|
|
9798
9392
|
const previous = previousStages.get(stage.id);
|
|
@@ -10154,6 +9748,7 @@ function WriteApp({
|
|
|
10154
9748
|
links,
|
|
10155
9749
|
unlinks,
|
|
10156
9750
|
maxLinks,
|
|
9751
|
+
maxImages,
|
|
10157
9752
|
onError
|
|
10158
9753
|
}) {
|
|
10159
9754
|
const { exit } = useApp3();
|
|
@@ -10164,7 +9759,7 @@ function WriteApp({
|
|
|
10164
9759
|
);
|
|
10165
9760
|
const [result, setResult] = useState4(null);
|
|
10166
9761
|
const [errorMessage, setErrorMessage] = useState4(null);
|
|
10167
|
-
|
|
9762
|
+
useEffect3(() => {
|
|
10168
9763
|
let mounted = true;
|
|
10169
9764
|
void (async () => {
|
|
10170
9765
|
try {
|
|
@@ -10180,6 +9775,7 @@ function WriteApp({
|
|
|
10180
9775
|
customLinks: links,
|
|
10181
9776
|
unlinks,
|
|
10182
9777
|
maxLinks,
|
|
9778
|
+
maxImages,
|
|
10183
9779
|
onUpdate(nextStages) {
|
|
10184
9780
|
if (mounted) {
|
|
10185
9781
|
setStages(nextStages);
|
|
@@ -10212,8 +9808,8 @@ function WriteApp({
|
|
|
10212
9808
|
return () => {
|
|
10213
9809
|
mounted = false;
|
|
10214
9810
|
};
|
|
10215
|
-
}, [dryRun, enrichLinks2, input, links, unlinks, maxLinks, onError, runMode]);
|
|
10216
|
-
|
|
9811
|
+
}, [dryRun, enrichLinks2, input, links, unlinks, maxLinks, maxImages, onError, runMode]);
|
|
9812
|
+
useEffect3(() => {
|
|
10217
9813
|
if (!result && !errorMessage) {
|
|
10218
9814
|
return;
|
|
10219
9815
|
}
|
|
@@ -10228,7 +9824,7 @@ function WriteApp({
|
|
|
10228
9824
|
}
|
|
10229
9825
|
async function runWriteCommand(options) {
|
|
10230
9826
|
const input = await resolveInputWithInteractiveIdeaFallback(options);
|
|
10231
|
-
await runWritePipeline(input, options.dryRun, options.enrichLinks, "fresh", options.noInteractive, options.links, options.unlinks, options.maxLinks);
|
|
9827
|
+
await runWritePipeline(input, options.dryRun, options.enrichLinks, "fresh", options.noInteractive, options.links, options.unlinks, options.maxLinks, options.maxImages);
|
|
10232
9828
|
}
|
|
10233
9829
|
async function runWriteResumeCommand(options = {}) {
|
|
10234
9830
|
const session = await loadWriteSession();
|
|
@@ -10250,9 +9846,9 @@ async function runWriteResumeCommand(options = {}) {
|
|
|
10250
9846
|
secrets: resolved.config.secrets
|
|
10251
9847
|
}
|
|
10252
9848
|
};
|
|
10253
|
-
await runWritePipeline(input, session.dryRun, options.enrichLinks ?? false, "resume", options.noInteractive ?? false, options.links, options.unlinks, options.maxLinks);
|
|
9849
|
+
await runWritePipeline(input, session.dryRun, options.enrichLinks ?? false, "resume", options.noInteractive ?? false, options.links, options.unlinks, options.maxLinks, options.maxImages);
|
|
10254
9850
|
}
|
|
10255
|
-
async function runWritePipeline(input, dryRun, enrichLinks2, runMode, noInteractive, links, unlinks, maxLinks) {
|
|
9851
|
+
async function runWritePipeline(input, dryRun, enrichLinks2, runMode, noInteractive, links, unlinks, maxLinks, maxImages) {
|
|
10256
9852
|
let interruptHandled = false;
|
|
10257
9853
|
const handleSignal = (signal) => {
|
|
10258
9854
|
if (interruptHandled) {
|
|
@@ -10286,7 +9882,7 @@ async function runWritePipeline(input, dryRun, enrichLinks2, runMode, noInteract
|
|
|
10286
9882
|
process.on("SIGTERM", onSigterm);
|
|
10287
9883
|
try {
|
|
10288
9884
|
if (noInteractive || !process.stdout.isTTY) {
|
|
10289
|
-
await renderPlainPipeline(input, dryRun, enrichLinks2, runMode, links, unlinks, maxLinks);
|
|
9885
|
+
await renderPlainPipeline(input, dryRun, enrichLinks2, runMode, links, unlinks, maxLinks, maxImages);
|
|
10290
9886
|
return;
|
|
10291
9887
|
}
|
|
10292
9888
|
let commandError = null;
|
|
@@ -10301,6 +9897,7 @@ async function runWritePipeline(input, dryRun, enrichLinks2, runMode, noInteract
|
|
|
10301
9897
|
links,
|
|
10302
9898
|
unlinks,
|
|
10303
9899
|
maxLinks,
|
|
9900
|
+
maxImages,
|
|
10304
9901
|
onError: (error) => {
|
|
10305
9902
|
commandError = error;
|
|
10306
9903
|
}
|
|
@@ -10468,7 +10065,7 @@ function collectOptionValue(value2, previous = []) {
|
|
|
10468
10065
|
}
|
|
10469
10066
|
async function runCli(argv) {
|
|
10470
10067
|
const program = new Command();
|
|
10471
|
-
program.name("ideon").description("Turn
|
|
10068
|
+
program.name("ideon").description("Turn one idea into articles, threads, and social posts \u2014 quality content without the token tax.").version(version);
|
|
10472
10069
|
program.command("settings").description("Show the current Ideon settings and storage state.").action(async () => {
|
|
10473
10070
|
await openSettings();
|
|
10474
10071
|
});
|
|
@@ -10513,6 +10110,14 @@ async function runCli(argv) {
|
|
|
10513
10110
|
maxLinks: options.maxLinks
|
|
10514
10111
|
});
|
|
10515
10112
|
});
|
|
10113
|
+
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) => {
|
|
10114
|
+
await runOutputCommand({
|
|
10115
|
+
generationId,
|
|
10116
|
+
destinationPath,
|
|
10117
|
+
index: options.index,
|
|
10118
|
+
overwrite: options.overwrite
|
|
10119
|
+
});
|
|
10120
|
+
});
|
|
10516
10121
|
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
10122
|
await runServeCommand({
|
|
10518
10123
|
markdownPath,
|
|
@@ -10521,7 +10126,7 @@ async function runCli(argv) {
|
|
|
10521
10126
|
watch: options.watch
|
|
10522
10127
|
});
|
|
10523
10128
|
});
|
|
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) => {
|
|
10129
|
+
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
10130
|
await runWriteCommand({
|
|
10526
10131
|
idea: options.idea ?? ideaArg,
|
|
10527
10132
|
audience: options.audience,
|
|
@@ -10536,16 +10141,18 @@ async function runCli(argv) {
|
|
|
10536
10141
|
enrichLinks: options.enrichLinks,
|
|
10537
10142
|
links: options.link,
|
|
10538
10143
|
unlinks: options.unlink,
|
|
10539
|
-
maxLinks: options.maxLinks
|
|
10144
|
+
maxLinks: options.maxLinks,
|
|
10145
|
+
maxImages: options.maxImages
|
|
10540
10146
|
});
|
|
10541
10147
|
});
|
|
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) => {
|
|
10148
|
+
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
10149
|
await runWriteResumeCommand({
|
|
10544
10150
|
noInteractive: options.noInteractive,
|
|
10545
10151
|
enrichLinks: options.enrichLinks,
|
|
10546
10152
|
links: options.link,
|
|
10547
10153
|
unlinks: options.unlink,
|
|
10548
|
-
maxLinks: options.maxLinks
|
|
10154
|
+
maxLinks: options.maxLinks,
|
|
10155
|
+
maxImages: options.maxImages
|
|
10549
10156
|
});
|
|
10550
10157
|
});
|
|
10551
10158
|
await program.parseAsync(argv);
|