@telepat/ideon 0.1.25 → 0.1.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ideon.js CHANGED
@@ -12,7 +12,8 @@ import { access, mkdir, writeFile } from "fs/promises";
12
12
  import os from "os";
13
13
  import path from "path";
14
14
  function resolveOutputPaths() {
15
- const base = path.join(os.homedir(), ".ideon", "output");
15
+ const ideonHome = process.env.IDEON_HOME || os.homedir();
16
+ const base = path.join(ideonHome, ".ideon", "output");
16
17
  return {
17
18
  markdownOutputDir: base,
18
19
  assetOutputDir: path.join(base, "assets")
@@ -1423,7 +1424,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
1423
1424
  // package.json
1424
1425
  var package_default = {
1425
1426
  name: "@telepat/ideon",
1426
- version: "0.1.25",
1427
+ version: "0.1.28",
1427
1428
  description: "CLI for generating rich articles and images from ideas.",
1428
1429
  type: "module",
1429
1430
  repository: {
@@ -1623,7 +1624,7 @@ function assertNoLegacyXMode(contentTargets, sourceLabel) {
1623
1624
  }
1624
1625
 
1625
1626
  // src/pipeline/runner.ts
1626
- import { mkdir as mkdir6, stat as stat2 } from "fs/promises";
1627
+ import { mkdir as mkdir6, readFile as readFile6, stat as stat2 } from "fs/promises";
1627
1628
  import { randomUUID } from "crypto";
1628
1629
  import path9 from "path";
1629
1630
 
@@ -2380,7 +2381,7 @@ function buildLongFormPlanJsonSchema(targetLengthWords) {
2380
2381
  required: ["description", "anchorAfterSection"],
2381
2382
  properties: {
2382
2383
  description: { type: "string" },
2383
- anchorAfterSection: { type: "number", minimum: 2 }
2384
+ anchorAfterSection: { type: "number", minimum: 1 }
2384
2385
  }
2385
2386
  }
2386
2387
  }
@@ -2446,7 +2447,7 @@ function buildLongFormPlanMessages(idea, options) {
2446
2447
  "- Each section description should name the mechanism, evidence type, or practical action that makes the section useful.",
2447
2448
  "- Sections are primary-only structure and must not be treated as requirements for non-primary channels.",
2448
2449
  `- Include a cover image description and ${imageCounts.min} to ${imageCounts.max} inline image descriptions.`,
2449
- "- Each inline image must specify which section it follows (anchorAfterSection, starting at 2). Choose sections where visual reinforcement adds the most value. Do not anchor inline images after the first section because the cover image already appears near the title.",
2450
+ "- Each inline image must specify which section it follows (anchorAfterSection, starting at 1). Choose sections where visual reinforcement adds the most value.",
2450
2451
  "- Image descriptions should capture the general concept and mood \u2014 the exact text-to-image prompt will be refined later using the actual section content.",
2451
2452
  "- Image descriptions must be concrete and contextual, not generic stock-photo phrasing.",
2452
2453
  "",
@@ -2468,7 +2469,7 @@ function buildLongFormPlanMessages(idea, options) {
2468
2469
  "- outroBrief: string",
2469
2470
  `- sections: array of ${sectionCounts.label} objects, each with title and description strings`,
2470
2471
  "- coverImageDescription: string",
2471
- `- inlineImages: array of ${imageCounts.min} to ${imageCounts.max} objects, each with a description string and an anchorAfterSection number (starting at 2, since the cover image already appears near the title)`,
2472
+ `- inlineImages: array of ${imageCounts.min} to ${imageCounts.max} objects, each with a description string and an anchorAfterSection number (starting at 1).`,
2472
2473
  "",
2473
2474
  "Do not omit any required fields. Return strict JSON only."
2474
2475
  ].join("\n")
@@ -2530,7 +2531,7 @@ var articleSectionPlanSchema = z5.object({
2530
2531
  });
2531
2532
  var inlineImagePlanSchema = z5.object({
2532
2533
  description: z5.string().min(1),
2533
- anchorAfterSection: z5.number().int().min(2)
2534
+ anchorAfterSection: z5.number().int().min(1)
2534
2535
  });
2535
2536
  var primaryPlanSchema = z5.object({
2536
2537
  contentType: z5.string().min(1).default("article"),
@@ -2619,7 +2620,7 @@ async function planPrimaryContent({
2619
2620
  keywords: longPlan.keywords.slice(0, 8),
2620
2621
  inlineImages: longPlan.inlineImages.slice(0, 3).map((img) => ({
2621
2622
  ...img,
2622
- anchorAfterSection: Math.max(2, Math.min(sectionCount, img.anchorAfterSection))
2623
+ anchorAfterSection: Math.max(1, Math.min(sectionCount, img.anchorAfterSection))
2623
2624
  }))
2624
2625
  };
2625
2626
  }
@@ -3276,7 +3277,7 @@ function buildImageSlots(plan, sections, options) {
3276
3277
  kind: "inline",
3277
3278
  prompt: "",
3278
3279
  description: img.description,
3279
- anchorAfterSection: Math.max(2, Math.min(sectionCount, img.anchorAfterSection))
3280
+ anchorAfterSection: Math.max(1, Math.min(sectionCount, img.anchorAfterSection))
3280
3281
  });
3281
3282
  }
3282
3283
  return slots;
@@ -5137,15 +5138,47 @@ async function runPipelineShell(input, options = {}) {
5137
5138
  markStageStarted(stageTracking, "links");
5138
5139
  options.onUpdate?.(cloneStages(stages));
5139
5140
  if (!shouldEnrichLinks) {
5140
- markStageCompleted(stageTracking, "links");
5141
- stages[6] = {
5142
- ...stages[6],
5143
- status: "succeeded",
5144
- detail: "Skipped link enrichment (enable with --enrich-links).",
5145
- summary: "Link enrichment disabled for this run",
5146
- stageAnalytics: snapshotStageAnalytics(stageTracking, "links")
5147
- };
5148
- options.onUpdate?.(cloneStages(stages));
5141
+ const customLinkActions = pipelineCustomLinkRaws.length > 0 || pipelineUnlinks.length > 0;
5142
+ if (customLinkActions && eligibleOutputsForLinks.length > 0) {
5143
+ for (const output of eligibleOutputsForLinks) {
5144
+ const existingLinks = await readExistingLinks(resolveLinksPath(output.markdownPath));
5145
+ const mergedCustomLinks = resolvePipelineCustomLinks(
5146
+ existingLinks?.customLinks ?? [],
5147
+ pipelineCustomLinkRaws,
5148
+ pipelineUnlinks
5149
+ );
5150
+ const generatedLinks = existingLinks?.links ?? [];
5151
+ await writeLinksFile(output.markdownPath, {
5152
+ version: 2,
5153
+ customLinks: mergedCustomLinks,
5154
+ links: generatedLinks
5155
+ });
5156
+ }
5157
+ markStageCompleted(stageTracking, "links");
5158
+ stages[6] = {
5159
+ ...stages[6],
5160
+ status: "succeeded",
5161
+ detail: "Updated custom links without generating new links.",
5162
+ summary: `${eligibleOutputsForLinks.length} files updated`,
5163
+ items: (stages[6].items ?? []).map((stageItem) => ({
5164
+ ...stageItem,
5165
+ status: "succeeded",
5166
+ detail: "Saved custom links sidecar."
5167
+ })),
5168
+ stageAnalytics: snapshotStageAnalytics(stageTracking, "links")
5169
+ };
5170
+ options.onUpdate?.(cloneStages(stages));
5171
+ } else {
5172
+ markStageCompleted(stageTracking, "links");
5173
+ stages[6] = {
5174
+ ...stages[6],
5175
+ status: "succeeded",
5176
+ detail: "Skipped link enrichment (enable with --enrich-links).",
5177
+ summary: "Link enrichment disabled for this run",
5178
+ stageAnalytics: snapshotStageAnalytics(stageTracking, "links")
5179
+ };
5180
+ options.onUpdate?.(cloneStages(stages));
5181
+ }
5149
5182
  } else if (eligibleOutputsForLinks.length === 0) {
5150
5183
  markStageCompleted(stageTracking, "links");
5151
5184
  stages[6] = {
@@ -5803,9 +5836,61 @@ function parsePipelineCustomLinks(rawLinks, unlinks) {
5803
5836
  }
5804
5837
  return Array.from(result.values());
5805
5838
  }
5839
+ function resolvePipelineCustomLinks(existing, rawLinks, unlinks) {
5840
+ const result = new Map(
5841
+ existing.map((entry) => [entry.expression.trim().toLowerCase(), entry])
5842
+ );
5843
+ for (const raw of rawLinks) {
5844
+ const separatorIndex = raw.indexOf("->");
5845
+ if (separatorIndex < 0) {
5846
+ continue;
5847
+ }
5848
+ const expression = raw.slice(0, separatorIndex).trim();
5849
+ const url = raw.slice(separatorIndex + 2).trim();
5850
+ if (expression && url) {
5851
+ result.set(expression.toLowerCase(), { expression, url, title: null });
5852
+ }
5853
+ }
5854
+ for (const expr of unlinks) {
5855
+ result.delete(expr.trim().toLowerCase());
5856
+ }
5857
+ return Array.from(result.values());
5858
+ }
5859
+ async function readExistingLinks(linksPath) {
5860
+ try {
5861
+ const raw = await readFile6(linksPath, "utf8");
5862
+ const parsed = JSON.parse(raw);
5863
+ const links = Array.isArray(parsed.links) ? parsed.links.filter((entry) => isValidLinkEntry(entry)).map((entry) => ({
5864
+ expression: entry.expression.trim(),
5865
+ url: entry.url.trim(),
5866
+ title: typeof entry.title === "string" ? entry.title : null
5867
+ })) : null;
5868
+ if (!links) {
5869
+ throw new Error(`Invalid links sidecar format at ${linksPath}. Expected { version, links[] }.`);
5870
+ }
5871
+ const customLinks = Array.isArray(parsed.customLinks) ? parsed.customLinks.filter((entry) => isValidLinkEntry(entry)).map((entry) => ({
5872
+ expression: entry.expression.trim(),
5873
+ url: entry.url.trim(),
5874
+ title: typeof entry.title === "string" ? entry.title : null
5875
+ })) : [];
5876
+ return {
5877
+ version: typeof parsed.version === "number" ? parsed.version : 2,
5878
+ customLinks,
5879
+ links
5880
+ };
5881
+ } catch (error) {
5882
+ if (error.code === "ENOENT") {
5883
+ return null;
5884
+ }
5885
+ throw error;
5886
+ }
5887
+ }
5888
+ function isValidLinkEntry(entry) {
5889
+ return typeof entry === "object" && entry !== null && typeof entry.expression === "string" && typeof entry.url === "string";
5890
+ }
5806
5891
 
5807
5892
  // src/cli/commands/links.ts
5808
- import { readFile as readFile6, stat as stat3 } from "fs/promises";
5893
+ import { readFile as readFile7, stat as stat3 } from "fs/promises";
5809
5894
  import path10 from "path";
5810
5895
  async function runLinksCommand(options, dependencies = {}) {
5811
5896
  const slug = normalizeSlug2(options.slug);
@@ -5828,7 +5913,7 @@ async function runLinksCommand(options, dependencies = {}) {
5828
5913
  }
5829
5914
  const openRouter = new OpenRouterClient(openRouterApiKey);
5830
5915
  const linksPath = resolveLinksPath(markdownPath);
5831
- const existing = await readExistingLinks(linksPath);
5916
+ const existing = await readExistingLinks2(linksPath);
5832
5917
  const updatedCustomLinks = resolveCustomLinks(existing?.customLinks ?? [], options.links ?? [], options.unlinks ?? []);
5833
5918
  const effectiveMaxLinks = options.maxLinks;
5834
5919
  const linksResult = await enrichLinks({
@@ -5923,7 +6008,7 @@ async function newestPath(paths) {
5923
6008
  return latestPath;
5924
6009
  }
5925
6010
  async function readFrontmatter(markdownPath) {
5926
- const markdown = await readFile6(markdownPath, "utf8");
6011
+ const markdown = await readFile7(markdownPath, "utf8");
5927
6012
  return parseFrontmatter(markdown);
5928
6013
  }
5929
6014
  function parseFrontmatter(markdown) {
@@ -5967,11 +6052,11 @@ async function isReadableFile(filePath) {
5967
6052
  return false;
5968
6053
  }
5969
6054
  }
5970
- async function readExistingLinks(linksPath) {
6055
+ async function readExistingLinks2(linksPath) {
5971
6056
  try {
5972
- const raw = await readFile6(linksPath, "utf8");
6057
+ const raw = await readFile7(linksPath, "utf8");
5973
6058
  const parsed = JSON.parse(raw);
5974
- const links = Array.isArray(parsed.links) ? parsed.links.filter((entry) => isValidLinkEntry(entry)).map((entry) => ({
6059
+ const links = Array.isArray(parsed.links) ? parsed.links.filter((entry) => isValidLinkEntry2(entry)).map((entry) => ({
5975
6060
  expression: entry.expression.trim(),
5976
6061
  url: entry.url.trim(),
5977
6062
  title: typeof entry.title === "string" ? entry.title : null
@@ -5979,7 +6064,7 @@ async function readExistingLinks(linksPath) {
5979
6064
  if (!links) {
5980
6065
  throw new ReportedError(`Invalid links sidecar format at ${linksPath}. Expected { version, links[] }.`);
5981
6066
  }
5982
- const customLinks = Array.isArray(parsed.customLinks) ? parsed.customLinks.filter((entry) => isValidLinkEntry(entry)).map((entry) => ({
6067
+ const customLinks = Array.isArray(parsed.customLinks) ? parsed.customLinks.filter((entry) => isValidLinkEntry2(entry)).map((entry) => ({
5983
6068
  expression: entry.expression.trim(),
5984
6069
  url: entry.url.trim(),
5985
6070
  title: typeof entry.title === "string" ? entry.title : null
@@ -6013,7 +6098,7 @@ function mergeLinks(existingLinks, generatedLinks) {
6013
6098
  }
6014
6099
  return merged;
6015
6100
  }
6016
- function isValidLinkEntry(value2) {
6101
+ function isValidLinkEntry2(value2) {
6017
6102
  if (typeof value2 !== "object" || value2 === null) {
6018
6103
  return false;
6019
6104
  }
@@ -6067,10 +6152,44 @@ function resolveCustomLinks(existing, addRaw, removeExpressions) {
6067
6152
  }
6068
6153
 
6069
6154
  // src/cli/commands/export.ts
6070
- import { copyFile as copyFile2, mkdir as mkdir7, readFile as readFile8, stat as stat5, writeFile as writeFile6 } from "fs/promises";
6155
+ import { copyFile as copyFile2, mkdir as mkdir7, readFile as readFile10, stat as stat5, writeFile as writeFile6 } from "fs/promises";
6071
6156
  import path12 from "path";
6072
6157
 
6073
6158
  // src/output/enrichMarkdownWithLinks.ts
6159
+ import { readFile as readFile8 } from "fs/promises";
6160
+ async function loadLinksFromSidecar(markdownPath) {
6161
+ const linksPath = resolveLinksPath(markdownPath);
6162
+ let raw;
6163
+ try {
6164
+ raw = await readFile8(linksPath, "utf8");
6165
+ } catch {
6166
+ return [];
6167
+ }
6168
+ let parsed;
6169
+ try {
6170
+ parsed = JSON.parse(raw);
6171
+ } catch {
6172
+ return [];
6173
+ }
6174
+ if (typeof parsed !== "object" || parsed === null) {
6175
+ return [];
6176
+ }
6177
+ const record = parsed;
6178
+ const links = Array.isArray(record.links) ? record.links : [];
6179
+ const customLinks = Array.isArray(record.customLinks) ? record.customLinks : [];
6180
+ const combined = [...customLinks, ...links];
6181
+ return combined.filter((entry) => {
6182
+ if (typeof entry !== "object" || entry === null) {
6183
+ return false;
6184
+ }
6185
+ const e = entry;
6186
+ return typeof e.expression === "string" && typeof e.url === "string" && (e.title === null || typeof e.title === "string");
6187
+ }).map((entry) => ({
6188
+ expression: entry.expression.trim(),
6189
+ url: entry.url.trim(),
6190
+ title: entry.title
6191
+ })).filter((entry) => entry.expression.length > 0 && entry.url.length > 0);
6192
+ }
6074
6193
  function enrichMarkdownWithLinks(markdown, links) {
6075
6194
  if (links.length === 0) {
6076
6195
  return markdown;
@@ -6117,14 +6236,29 @@ function isInProtectedSpan(content, start, end) {
6117
6236
  return true;
6118
6237
  }
6119
6238
  }
6239
+ if (/^#{1,6}\s/.test(line)) {
6240
+ return true;
6241
+ }
6120
6242
  return false;
6121
6243
  }
6122
6244
  function escapeRegExp(value2) {
6123
- return value2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6245
+ return value2.replace(/[.*+?^{}()|[\]\\]/g, "\\$&");
6246
+ }
6247
+ function enrichWithFrontmatterGuard(markdown, links) {
6248
+ if (links.length === 0) {
6249
+ return markdown;
6250
+ }
6251
+ const frontmatterMatch = markdown.match(/^---\s*\n[\s\S]*?\n---\s*\n?/);
6252
+ if (!frontmatterMatch) {
6253
+ return enrichMarkdownWithLinks(markdown, links);
6254
+ }
6255
+ const frontmatter = frontmatterMatch[0];
6256
+ const body = markdown.slice(frontmatter.length);
6257
+ return `${frontmatter}${enrichMarkdownWithLinks(body, links)}`;
6124
6258
  }
6125
6259
 
6126
6260
  // src/server/previewHelpers.ts
6127
- import { readdir, stat as stat4, readFile as readFile7 } from "fs/promises";
6261
+ import { readdir, stat as stat4, readFile as readFile9 } from "fs/promises";
6128
6262
  import path11 from "path";
6129
6263
  var DEFAULT_PORT = 4173;
6130
6264
  var CONTENT_TYPE_ORDER = ["article", "blog-post", "x-thread", "x-post", "linkedin-post", "reddit-post", "newsletter"];
@@ -6226,7 +6360,7 @@ function extractCoverImageUrl(markdown) {
6226
6360
  return match?.[1] ?? null;
6227
6361
  }
6228
6362
  async function extractArticleMetadata(markdownPath) {
6229
- const markdown = await readFile7(markdownPath, "utf8");
6363
+ const markdown = await readFile9(markdownPath, "utf8");
6230
6364
  const fileStat = await stat4(markdownPath);
6231
6365
  const slug = extractFrontmatterSlug(markdown) ?? path11.basename(markdownPath, ".md");
6232
6366
  const title = extractHeadingTitle(stripFrontmatter2(markdown)) ?? slug;
@@ -6243,7 +6377,7 @@ async function extractArticleMetadata(markdownPath) {
6243
6377
  }
6244
6378
  async function listAllGenerations(markdownOutputDir) {
6245
6379
  const markdownFiles = await findMarkdownFiles(markdownOutputDir);
6246
- const grouped = /* @__PURE__ */ new Map();
6380
+ const outputMap = /* @__PURE__ */ new Map();
6247
6381
  for (const filePath of markdownFiles) {
6248
6382
  try {
6249
6383
  const metadata = await extractArticleMetadata(filePath);
@@ -6261,15 +6395,23 @@ async function listAllGenerations(markdownOutputDir) {
6261
6395
  contentTypeLabel: toContentTypeLabel(identity.contentType),
6262
6396
  index: identity.index
6263
6397
  };
6264
- const existing = grouped.get(identity.generationId);
6265
- if (existing) {
6266
- existing.push(output);
6267
- } else {
6268
- grouped.set(identity.generationId, [output]);
6398
+ const outputKey = `${output.generationId}:${output.contentType}:${output.index}`;
6399
+ const existing = outputMap.get(outputKey);
6400
+ if (!existing || output.mtime > existing.mtime) {
6401
+ outputMap.set(outputKey, output);
6269
6402
  }
6270
6403
  } catch {
6271
6404
  }
6272
6405
  }
6406
+ const grouped = /* @__PURE__ */ new Map();
6407
+ for (const output of outputMap.values()) {
6408
+ const existing = grouped.get(output.generationId);
6409
+ if (existing) {
6410
+ existing.push(output);
6411
+ } else {
6412
+ grouped.set(output.generationId, [output]);
6413
+ }
6414
+ }
6273
6415
  const generations = [];
6274
6416
  for (const [id, outputs] of grouped.entries()) {
6275
6417
  outputs.sort((a, b) => compareContentTypes(a.contentType, b.contentType) || a.index - b.index || b.mtime - a.mtime);
@@ -6375,7 +6517,7 @@ async function resolvePrimaryContentType(outputs) {
6375
6517
  }
6376
6518
  const jobPath = path11.join(generationDir, "job.json");
6377
6519
  try {
6378
- const raw = await readFile7(jobPath, "utf8");
6520
+ const raw = await readFile9(jobPath, "utf8");
6379
6521
  const parsed = JSON.parse(raw);
6380
6522
  const targets = Array.isArray(parsed.contentTargets) ? parsed.contentTargets : Array.isArray(parsed.settings?.contentTargets) ? parsed.settings.contentTargets : [];
6381
6523
  const primary = targets.find((target) => target?.role === "primary" && typeof target.contentType === "string");
@@ -6416,7 +6558,7 @@ async function runOutputCommand(options, dependencies = {}) {
6416
6558
  );
6417
6559
  }
6418
6560
  const sourceMarkdownPath = articleOutput.sourcePath;
6419
- const sourceMarkdown = await readFile8(sourceMarkdownPath, "utf8");
6561
+ const sourceMarkdown = await readFile10(sourceMarkdownPath, "utf8");
6420
6562
  const slug = extractFrontmatterSlug2(sourceMarkdown) ?? path12.basename(sourceMarkdownPath, ".md");
6421
6563
  const exportFilename = `${slug}.md`;
6422
6564
  const destinationDir = await resolveDestinationDir(options.destinationPath, cwd2);
@@ -6427,7 +6569,7 @@ async function runOutputCommand(options, dependencies = {}) {
6427
6569
  );
6428
6570
  }
6429
6571
  await mkdir7(destinationDir, { recursive: true });
6430
- const links = await loadLinks(sourceMarkdownPath);
6572
+ const links = await loadLinksFromSidecar(sourceMarkdownPath);
6431
6573
  const enrichedMarkdown = enrichWithFrontmatterGuard(sourceMarkdown, links);
6432
6574
  const sourceDir = path12.dirname(sourceMarkdownPath);
6433
6575
  const allFiles = await listFilesRecursively(sourceDir, () => true);
@@ -6479,51 +6621,6 @@ async function fileExists2(filePath) {
6479
6621
  return false;
6480
6622
  }
6481
6623
  }
6482
- async function loadLinks(markdownPath) {
6483
- const linksPath = resolveLinksPath(markdownPath);
6484
- let raw;
6485
- try {
6486
- raw = await readFile8(linksPath, "utf8");
6487
- } catch {
6488
- return [];
6489
- }
6490
- let parsed;
6491
- try {
6492
- parsed = JSON.parse(raw);
6493
- } catch {
6494
- return [];
6495
- }
6496
- if (typeof parsed !== "object" || parsed === null) {
6497
- return [];
6498
- }
6499
- const record = parsed;
6500
- const links = Array.isArray(record.links) ? record.links : [];
6501
- const customLinks = Array.isArray(record.customLinks) ? record.customLinks : [];
6502
- const combined = [...customLinks, ...links];
6503
- return combined.filter((entry) => {
6504
- if (typeof entry !== "object" || entry === null) {
6505
- return false;
6506
- }
6507
- const e = entry;
6508
- return typeof e.expression === "string" && typeof e.url === "string" && (e.title === null || typeof e.title === "string");
6509
- }).map((entry) => ({
6510
- expression: entry.expression.trim(),
6511
- url: entry.url.trim(),
6512
- title: entry.title
6513
- })).filter((entry) => entry.expression.length > 0 && entry.url.length > 0);
6514
- }
6515
- function enrichWithFrontmatterGuard(markdown, links) {
6516
- if (links.length === 0) {
6517
- return markdown;
6518
- }
6519
- const frontmatterMatch = markdown.match(/^---\s*\n[\s\S]*?\n---\s*\n?/);
6520
- if (!frontmatterMatch) {
6521
- return enrichMarkdownWithLinks(markdown, links);
6522
- }
6523
- const frontmatter = frontmatterMatch[0];
6524
- const body = markdown.slice(frontmatter.length);
6525
- return `${frontmatter}${enrichMarkdownWithLinks(body, links)}`;
6526
- }
6527
6624
  function extractFrontmatterSlug2(markdown) {
6528
6625
  const frontmatterMatch = markdown.match(/^---\s*\n([\s\S]*?)\n---\s*\n?/);
6529
6626
  const block = frontmatterMatch?.[1];
@@ -7405,7 +7502,7 @@ import { spawn } from "child_process";
7405
7502
  // src/server/previewServer.ts
7406
7503
  import { execFile } from "child_process";
7407
7504
  import { promisify } from "util";
7408
- import { readFile as readFile9, stat as stat6 } from "fs/promises";
7505
+ import { readFile as readFile11, stat as stat6 } from "fs/promises";
7409
7506
  import { watch as fsWatch } from "fs";
7410
7507
  import path13 from "path";
7411
7508
  import { fileURLToPath } from "url";
@@ -7422,7 +7519,7 @@ async function startPreviewServer(options) {
7422
7519
  const app = express();
7423
7520
  const previewClientDir = await resolvePreviewClientBuildDir();
7424
7521
  app.disable("x-powered-by");
7425
- app.use("/assets", express.static(options.assetDir));
7522
+ app.use("/assets", express.static(options.assetDir, { dotfiles: "allow" }));
7426
7523
  if (previewClientDir) {
7427
7524
  app.use(express.static(previewClientDir, { index: false }));
7428
7525
  }
@@ -7456,7 +7553,7 @@ async function startPreviewServer(options) {
7456
7553
  const assetPathParam = req.params.assetPath;
7457
7554
  const rawAssetPath = Array.isArray(assetPathParam) ? assetPathParam.join("/") : assetPathParam ?? "";
7458
7555
  const resolvedAssetPath = await resolveGenerationAssetPath(generationId, rawAssetPath, options.markdownOutputDir);
7459
- res.sendFile(resolvedAssetPath);
7556
+ res.sendFile(resolvedAssetPath, { dotfiles: "allow" });
7460
7557
  } catch (error) {
7461
7558
  const message = error instanceof Error ? error.message : "Unknown error loading generation asset";
7462
7559
  const status = error instanceof MissingArticleError ? 404 : 400;
@@ -7505,7 +7602,7 @@ async function startPreviewServer(options) {
7505
7602
  if (options.watch) {
7506
7603
  let html2;
7507
7604
  try {
7508
- html2 = await readFile9(path13.join(previewClientDir, "index.html"), "utf8");
7605
+ html2 = await readFile11(path13.join(previewClientDir, "index.html"), "utf8");
7509
7606
  } catch {
7510
7607
  res.status(200).type("html").send(
7511
7608
  `<!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>`
@@ -7569,7 +7666,7 @@ async function getArticleContent(generationId, markdownOutputDir) {
7569
7666
  generation.outputs.map(async (output) => {
7570
7667
  let markdown = "";
7571
7668
  try {
7572
- markdown = await readFile9(output.sourcePath, "utf8");
7669
+ markdown = await readFile11(output.sourcePath, "utf8");
7573
7670
  } catch (error) {
7574
7671
  if (isMissingFileError(error)) {
7575
7672
  throw new MissingArticleError(`Generation "${generationId}" no longer exists.`);
@@ -7625,41 +7722,15 @@ function isMissingFileError(error) {
7625
7722
  }
7626
7723
  async function renderArticleHtml(markdown, generationId, sourcePath) {
7627
7724
  let content = stripFrontmatter2(markdown);
7628
- const links = await loadSavedLinks(sourcePath);
7725
+ const links = await loadLinksFromSidecar(sourcePath);
7629
7726
  content = enrichMarkdownWithLinks(content, links);
7630
7727
  const html = await marked.parse(content);
7631
7728
  return rewriteRelativeAssetUrls(html, generationId);
7632
7729
  }
7633
- async function loadSavedLinks(markdownPath) {
7634
- const linksPath = resolveLinksPath(markdownPath);
7635
- try {
7636
- const raw = await readFile9(linksPath, "utf8");
7637
- const parsed = JSON.parse(raw);
7638
- if (!Array.isArray(parsed.links)) {
7639
- return [];
7640
- }
7641
- return parsed.links.filter((entry) => {
7642
- if (typeof entry !== "object" || entry === null) {
7643
- return false;
7644
- }
7645
- const record = entry;
7646
- return typeof record.expression === "string" && typeof record.url === "string" && (record.title === null || typeof record.title === "string");
7647
- }).map((entry) => ({
7648
- expression: entry.expression.trim(),
7649
- url: entry.url.trim(),
7650
- title: entry.title
7651
- })).filter((entry) => entry.expression.length > 0 && entry.url.length > 0);
7652
- } catch (error) {
7653
- if (isMissingFileError(error)) {
7654
- return [];
7655
- }
7656
- return [];
7657
- }
7658
- }
7659
7730
  async function loadSavedInteractions(generationDir) {
7660
7731
  const interactionsPath = path13.join(generationDir, "model.interactions.json");
7661
7732
  try {
7662
- const raw = await readFile9(interactionsPath, "utf8");
7733
+ const raw = await readFile11(interactionsPath, "utf8");
7663
7734
  const parsed = JSON.parse(raw);
7664
7735
  const llmCalls = Array.isArray(parsed.llmCalls) ? parsed.llmCalls.filter(isPreviewLlmInteraction) : [];
7665
7736
  const t2iCalls = Array.isArray(parsed.t2iCalls) ? parsed.t2iCalls.filter(isPreviewT2IInteraction) : [];
@@ -7677,7 +7748,7 @@ async function loadSavedInteractions(generationDir) {
7677
7748
  async function loadSavedAnalyticsSummary(generationDir) {
7678
7749
  const analyticsPath = path13.join(generationDir, "generation.analytics.json");
7679
7750
  try {
7680
- const raw = await readFile9(analyticsPath, "utf8");
7751
+ const raw = await readFile11(analyticsPath, "utf8");
7681
7752
  const parsed = JSON.parse(raw);
7682
7753
  const summary = parsed.summary;
7683
7754
  if (!summary || typeof summary !== "object") {
@@ -7701,7 +7772,7 @@ async function loadSavedAnalyticsSummary(generationDir) {
7701
7772
  async function loadSavedMetaJson(generationDir) {
7702
7773
  const metaJsonPath = path13.join(generationDir, "meta.json");
7703
7774
  try {
7704
- const raw = await readFile9(metaJsonPath, "utf8");
7775
+ const raw = await readFile11(metaJsonPath, "utf8");
7705
7776
  return JSON.parse(raw);
7706
7777
  } catch {
7707
7778
  return null;