@ncukondo/slide-generation 0.2.5 → 0.4.0

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/cli/index.js CHANGED
@@ -923,25 +923,25 @@ var ReferenceManager = class {
923
923
  return items[0] || null;
924
924
  }
925
925
  /**
926
- * Get multiple references by IDs
926
+ * Get multiple references by IDs using ref export for better performance
927
927
  */
928
928
  async getByIds(ids) {
929
929
  if (ids.length === 0) {
930
930
  return /* @__PURE__ */ new Map();
931
931
  }
932
- const result = await this.execCommand(`${this.command} list --format json`);
933
- const allItems = this.parseJSON(result);
934
- const idSet = new Set(ids);
932
+ const idsArg = ids.map((id) => `"${id}"`).join(" ");
933
+ const result = await this.execCommand(
934
+ `${this.command} export ${idsArg}`
935
+ );
936
+ const items = this.parseJSON(result);
935
937
  const map = /* @__PURE__ */ new Map();
936
- for (const item of allItems) {
937
- if (idSet.has(item.id)) {
938
- map.set(item.id, item);
939
- }
938
+ for (const item of items) {
939
+ map.set(item.id, item);
940
940
  }
941
941
  return map;
942
942
  }
943
943
  execCommand(cmd) {
944
- return new Promise((resolve7, reject) => {
944
+ return new Promise((resolve8, reject) => {
945
945
  exec(cmd, (error, stdout) => {
946
946
  if (error) {
947
947
  reject(
@@ -949,7 +949,7 @@ var ReferenceManager = class {
949
949
  );
950
950
  return;
951
951
  }
952
- resolve7(stdout.toString());
952
+ resolve8(stdout.toString());
953
953
  });
954
954
  });
955
955
  }
@@ -1080,6 +1080,82 @@ var CitationExtractor = class {
1080
1080
  }
1081
1081
  };
1082
1082
 
1083
+ // src/references/utils.ts
1084
+ var JAPANESE_PATTERN = /[\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF]/;
1085
+ function isJapaneseAuthors(authors) {
1086
+ if (!authors || authors.length === 0) {
1087
+ return false;
1088
+ }
1089
+ return JAPANESE_PATTERN.test(authors[0].family);
1090
+ }
1091
+ function getYear(item) {
1092
+ const dateParts = item.issued?.["date-parts"];
1093
+ if (dateParts && dateParts[0] && dateParts[0][0]) {
1094
+ return dateParts[0][0];
1095
+ }
1096
+ return 0;
1097
+ }
1098
+ function getIdentifier(item) {
1099
+ if (item.PMID) {
1100
+ return `PMID: ${item.PMID}`;
1101
+ }
1102
+ if (item.DOI) {
1103
+ return `DOI: ${item.DOI}`;
1104
+ }
1105
+ return null;
1106
+ }
1107
+ function getFirstAuthorFamily(item) {
1108
+ return item.author?.[0]?.family || "";
1109
+ }
1110
+ function formatAuthorsFull(authors, isJapanese) {
1111
+ if (!authors || authors.length === 0) {
1112
+ return "Unknown";
1113
+ }
1114
+ if (isJapanese) {
1115
+ return authors.map((a) => `${a.family}${a.given || ""}`).join(", ");
1116
+ }
1117
+ if (authors.length === 1) {
1118
+ const a = authors[0];
1119
+ const initial = a.given ? `${a.given.charAt(0)}.` : "";
1120
+ return `${a.family}, ${initial}`;
1121
+ }
1122
+ const formatted = authors.map((a, i) => {
1123
+ const initial = a.given ? `${a.given.charAt(0)}.` : "";
1124
+ if (i === authors.length - 1) {
1125
+ return `& ${a.family}, ${initial}`;
1126
+ }
1127
+ return `${a.family}, ${initial}`;
1128
+ });
1129
+ return formatted.join(", ").replace(", &", ", &");
1130
+ }
1131
+ function formatFullEntry(item) {
1132
+ const parts = [];
1133
+ const japanese = isJapaneseAuthors(item.author);
1134
+ const authors = formatAuthorsFull(item.author, japanese);
1135
+ parts.push(authors);
1136
+ const year = getYear(item);
1137
+ parts.push(`(${year}).`);
1138
+ if (item.title) {
1139
+ parts.push(`${item.title}.`);
1140
+ }
1141
+ if (item["container-title"]) {
1142
+ const journal = japanese ? item["container-title"] : `*${item["container-title"]}*`;
1143
+ let location = "";
1144
+ if (item.volume) {
1145
+ location = item.issue ? `${item.volume}(${item.issue})` : item.volume;
1146
+ }
1147
+ if (item.page) {
1148
+ location = location ? `${location}, ${item.page}` : item.page;
1149
+ }
1150
+ parts.push(location ? `${journal}, ${location}.` : `${journal}.`);
1151
+ }
1152
+ const identifier = getIdentifier(item);
1153
+ if (identifier) {
1154
+ parts.push(identifier);
1155
+ }
1156
+ return parts.join(" ");
1157
+ }
1158
+
1083
1159
  // src/references/formatter.ts
1084
1160
  var DEFAULT_CONFIG = {
1085
1161
  author: {
@@ -1094,7 +1170,6 @@ var DEFAULT_CONFIG = {
1094
1170
  multiSep: "), ("
1095
1171
  }
1096
1172
  };
1097
- var JAPANESE_PATTERN = /[\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF]/;
1098
1173
  var CITATION_BRACKET_PATTERN2 = /\[(@[\w-]+(?:,\s*[^;\]]+)?(?:;\s*@[\w-]+(?:,\s*[^;\]]+)?)*)\]/g;
1099
1174
  var SINGLE_CITATION_PATTERN2 = /@([\w-]+)(?:,\s*([^;\]]+))?/g;
1100
1175
  var CitationFormatter = class {
@@ -1106,6 +1181,18 @@ var CitationFormatter = class {
1106
1181
  };
1107
1182
  }
1108
1183
  config;
1184
+ availabilityChecked = false;
1185
+ isAvailable = false;
1186
+ /**
1187
+ * Check if reference-manager is available (cached)
1188
+ */
1189
+ async checkAvailability() {
1190
+ if (!this.availabilityChecked) {
1191
+ this.isAvailable = await this.manager.isAvailable();
1192
+ this.availabilityChecked = true;
1193
+ }
1194
+ return this.isAvailable;
1195
+ }
1109
1196
  /**
1110
1197
  * Format an inline citation
1111
1198
  * e.g., "(Smith et al., 2024; PMID: 12345678)"
@@ -1125,13 +1212,16 @@ var CitationFormatter = class {
1125
1212
  if (!item) {
1126
1213
  return `[${id}]`;
1127
1214
  }
1128
- return this.formatFullItem(item);
1215
+ return formatFullEntry(item);
1129
1216
  }
1130
1217
  /**
1131
1218
  * Expand all citations in text
1132
1219
  * e.g., "[@smith2024]" -> "(Smith et al., 2024; PMID: 12345678)"
1133
1220
  */
1134
1221
  async expandCitations(text) {
1222
+ if (!await this.checkAvailability()) {
1223
+ return text;
1224
+ }
1135
1225
  const ids = /* @__PURE__ */ new Set();
1136
1226
  CITATION_BRACKET_PATTERN2.lastIndex = 0;
1137
1227
  let match;
@@ -1186,117 +1276,102 @@ var CitationFormatter = class {
1186
1276
  sortedItems = ids.map((id) => items.get(id)).filter((item) => item !== void 0);
1187
1277
  } else if (sort === "author") {
1188
1278
  sortedItems = [...items.values()].sort((a, b) => {
1189
- const authorA = this.getFirstAuthorFamily(a);
1190
- const authorB = this.getFirstAuthorFamily(b);
1279
+ const authorA = getFirstAuthorFamily(a);
1280
+ const authorB = getFirstAuthorFamily(b);
1191
1281
  return authorA.localeCompare(authorB);
1192
1282
  });
1193
1283
  } else {
1194
1284
  sortedItems = [...items.values()].sort((a, b) => {
1195
- const yearA = this.getYear(a);
1196
- const yearB = this.getYear(b);
1285
+ const yearA = getYear(a);
1286
+ const yearB = getYear(b);
1197
1287
  return yearA - yearB;
1198
1288
  });
1199
1289
  }
1200
- return sortedItems.map((item) => this.formatFullItem(item));
1290
+ return sortedItems.map((item) => formatFullEntry(item));
1201
1291
  }
1202
1292
  formatInlineItem(item) {
1203
1293
  const author = this.formatAuthorInline(item.author);
1204
- const year = this.getYear(item);
1205
- const identifier = this.getIdentifier(item);
1294
+ const year = getYear(item);
1295
+ const identifier = getIdentifier(item);
1206
1296
  if (identifier) {
1207
1297
  return `(${author}, ${year}; ${identifier})`;
1208
1298
  }
1209
1299
  return `(${author}, ${year})`;
1210
1300
  }
1211
- formatFullItem(item) {
1212
- const parts = [];
1213
- const isJapanese = this.isJapaneseAuthors(item.author);
1214
- const authors = this.formatAuthorsFull(item.author, isJapanese);
1215
- parts.push(authors);
1216
- const year = this.getYear(item);
1217
- parts.push(`(${year}).`);
1218
- if (item.title) {
1219
- parts.push(`${item.title}.`);
1220
- }
1221
- if (item["container-title"]) {
1222
- const journal = isJapanese ? item["container-title"] : `*${item["container-title"]}*`;
1223
- let location = "";
1224
- if (item.volume) {
1225
- location = item.issue ? `${item.volume}(${item.issue})` : item.volume;
1226
- }
1227
- if (item.page) {
1228
- location = location ? `${location}, ${item.page}` : item.page;
1229
- }
1230
- parts.push(location ? `${journal}, ${location}.` : `${journal}.`);
1231
- }
1232
- const identifier = this.getIdentifier(item);
1233
- if (identifier) {
1234
- parts.push(identifier);
1235
- }
1236
- return parts.join(" ");
1237
- }
1238
1301
  formatAuthorInline(authors) {
1239
1302
  if (!authors || authors.length === 0) {
1240
1303
  return "Unknown";
1241
1304
  }
1242
- const isJapanese = this.isJapaneseAuthors(authors);
1305
+ const japanese = isJapaneseAuthors(authors);
1243
1306
  const { etAl, etAlJa, separatorJa } = this.config.author;
1244
1307
  const firstAuthor = authors[0];
1245
1308
  if (authors.length === 1) {
1246
1309
  return firstAuthor.family;
1247
1310
  }
1248
1311
  if (authors.length === 2) {
1249
- const separator = isJapanese ? separatorJa : " & ";
1312
+ const separator = japanese ? separatorJa : " & ";
1250
1313
  return `${firstAuthor.family}${separator}${authors[1].family}`;
1251
1314
  }
1252
- const suffix = isJapanese ? etAlJa : ` ${etAl}`;
1315
+ const suffix = japanese ? etAlJa : ` ${etAl}`;
1253
1316
  return `${firstAuthor.family}${suffix}`;
1254
1317
  }
1255
- formatAuthorsFull(authors, isJapanese) {
1256
- if (!authors || authors.length === 0) {
1257
- return "Unknown";
1258
- }
1259
- if (isJapanese) {
1260
- return authors.map((a) => `${a.family}${a.given || ""}`).join(", ");
1261
- }
1262
- if (authors.length === 1) {
1263
- const a = authors[0];
1264
- const initial = a.given ? `${a.given.charAt(0)}.` : "";
1265
- return `${a.family}, ${initial}`;
1266
- }
1267
- const formatted = authors.map((a, i) => {
1268
- const initial = a.given ? `${a.given.charAt(0)}.` : "";
1269
- if (i === authors.length - 1) {
1270
- return `& ${a.family}, ${initial}`;
1271
- }
1272
- return `${a.family}, ${initial}`;
1273
- });
1274
- return formatted.join(", ").replace(", &", ", &");
1318
+ };
1319
+
1320
+ // src/references/bibliography.ts
1321
+ var BibliographyGenerator = class {
1322
+ constructor(manager) {
1323
+ this.manager = manager;
1275
1324
  }
1276
- isJapaneseAuthors(authors) {
1277
- if (!authors || authors.length === 0) {
1278
- return false;
1325
+ /**
1326
+ * Generate bibliography entries from citation IDs
1327
+ */
1328
+ async generate(citationIds, options = {}) {
1329
+ const { sort = "citation-order" } = options;
1330
+ if (citationIds.length === 0) {
1331
+ return { entries: [], items: [], missing: [] };
1279
1332
  }
1280
- return JAPANESE_PATTERN.test(authors[0].family);
1281
- }
1282
- getFirstAuthorFamily(item) {
1283
- return item.author?.[0]?.family || "";
1284
- }
1285
- getYear(item) {
1286
- const dateParts = item.issued?.["date-parts"];
1287
- if (dateParts && dateParts[0] && dateParts[0][0]) {
1288
- return dateParts[0][0];
1333
+ const uniqueIds = [...new Set(citationIds)];
1334
+ const itemsMap = await this.manager.getByIds(uniqueIds);
1335
+ const missing = [];
1336
+ const foundItems = [];
1337
+ for (const id of uniqueIds) {
1338
+ const item = itemsMap.get(id);
1339
+ if (item) {
1340
+ foundItems.push(item);
1341
+ } else {
1342
+ missing.push(id);
1343
+ }
1289
1344
  }
1290
- return 0;
1345
+ const sortedItems = this.sortItems(foundItems, sort);
1346
+ const entries = sortedItems.map((item) => formatFullEntry(item));
1347
+ return {
1348
+ entries,
1349
+ items: sortedItems,
1350
+ missing
1351
+ };
1291
1352
  }
1292
- getIdentifier(item) {
1293
- if (item.PMID) {
1294
- return `PMID: ${item.PMID}`;
1295
- }
1296
- if (item.DOI) {
1297
- return `DOI: ${item.DOI}`;
1353
+ /**
1354
+ * Sort items according to the specified order
1355
+ */
1356
+ sortItems(items, sort) {
1357
+ switch (sort) {
1358
+ case "citation-order":
1359
+ return items;
1360
+ case "author":
1361
+ return [...items].sort((a, b) => {
1362
+ const authorA = getFirstAuthorFamily(a);
1363
+ const authorB = getFirstAuthorFamily(b);
1364
+ return authorA.localeCompare(authorB);
1365
+ });
1366
+ case "year":
1367
+ return [...items].sort((a, b) => {
1368
+ const yearA = getYear(a);
1369
+ const yearB = getYear(b);
1370
+ return yearA - yearB;
1371
+ });
1372
+ default:
1373
+ return items;
1298
1374
  }
1299
- return null;
1300
1375
  }
1301
1376
  };
1302
1377
 
@@ -1335,6 +1410,7 @@ var Pipeline = class {
1335
1410
  }
1336
1411
  }
1337
1412
  );
1413
+ this.bibliographyGenerator = new BibliographyGenerator(this.referenceManager);
1338
1414
  this.transformer = new Transformer(
1339
1415
  this.templateEngine,
1340
1416
  this.templateLoader,
@@ -1351,6 +1427,7 @@ var Pipeline = class {
1351
1427
  referenceManager;
1352
1428
  citationExtractor;
1353
1429
  citationFormatter;
1430
+ bibliographyGenerator;
1354
1431
  transformer;
1355
1432
  renderer;
1356
1433
  warnings = [];
@@ -1360,9 +1437,10 @@ var Pipeline = class {
1360
1437
  async run(inputPath, options) {
1361
1438
  this.warnings = [];
1362
1439
  try {
1363
- const presentation = await this.parseSource(inputPath);
1440
+ let presentation = await this.parseSource(inputPath);
1364
1441
  const citationIds = this.collectCitations(presentation);
1365
1442
  await this.resolveReferences(citationIds);
1443
+ presentation = await this.processBibliography(presentation, citationIds);
1366
1444
  const transformedSlides = await this.transformSlides(presentation);
1367
1445
  const output = this.render(transformedSlides, presentation);
1368
1446
  if (options?.outputPath) {
@@ -1386,9 +1464,10 @@ var Pipeline = class {
1386
1464
  async runWithResult(inputPath, options) {
1387
1465
  this.warnings = [];
1388
1466
  try {
1389
- const presentation = await this.parseSource(inputPath);
1467
+ let presentation = await this.parseSource(inputPath);
1390
1468
  const citationIds = this.collectCitations(presentation);
1391
1469
  await this.resolveReferences(citationIds);
1470
+ presentation = await this.processBibliography(presentation, citationIds);
1392
1471
  const transformedSlides = await this.transformSlides(presentation);
1393
1472
  const output = this.render(transformedSlides, presentation);
1394
1473
  if (options?.outputPath) {
@@ -1464,6 +1543,13 @@ var Pipeline = class {
1464
1543
  if (!this.config.references.enabled || ids.length === 0) {
1465
1544
  return /* @__PURE__ */ new Map();
1466
1545
  }
1546
+ const isAvailable = await this.referenceManager.isAvailable();
1547
+ if (!isAvailable) {
1548
+ this.warnings.push(
1549
+ "reference-manager CLI is not available. Install it to enable citation features: npm install -g @ncukondo/reference-manager"
1550
+ );
1551
+ return /* @__PURE__ */ new Map();
1552
+ }
1467
1553
  try {
1468
1554
  const items = await this.referenceManager.getByIds(ids);
1469
1555
  for (const id of ids) {
@@ -1512,10 +1598,97 @@ var Pipeline = class {
1512
1598
  );
1513
1599
  }
1514
1600
  }
1601
+ /**
1602
+ * Process bibliography slides with autoGenerate: true
1603
+ * Populates references array from collected citations
1604
+ */
1605
+ async processBibliography(presentation, citationIds) {
1606
+ if (!this.config.references.enabled || citationIds.length === 0) {
1607
+ return presentation;
1608
+ }
1609
+ const hasBibliographyAutoGenerate = presentation.slides.some(
1610
+ (slide) => slide.template === "bibliography" && slide.content?.["autoGenerate"] === true
1611
+ );
1612
+ if (!hasBibliographyAutoGenerate) {
1613
+ return presentation;
1614
+ }
1615
+ const isAvailable = await this.referenceManager.isAvailable();
1616
+ if (!isAvailable) {
1617
+ return presentation;
1618
+ }
1619
+ try {
1620
+ const updatedSlides = await Promise.all(
1621
+ presentation.slides.map(async (slide) => {
1622
+ if (slide.template === "bibliography" && slide.content?.["autoGenerate"] === true) {
1623
+ const sort = slide.content["sort"] || "citation-order";
1624
+ const result = await this.bibliographyGenerator.generate(
1625
+ citationIds,
1626
+ { sort }
1627
+ );
1628
+ for (const id of result.missing) {
1629
+ this.warnings.push(`Bibliography: reference not found: ${id}`);
1630
+ }
1631
+ const references = result.items.map((item) => ({
1632
+ id: item.id,
1633
+ authors: this.formatAuthorsForTemplate(item.author),
1634
+ title: item.title || "",
1635
+ year: this.getYear(item),
1636
+ journal: item["container-title"],
1637
+ volume: item.volume,
1638
+ pages: item.page,
1639
+ doi: item.DOI,
1640
+ url: item.URL
1641
+ }));
1642
+ return {
1643
+ ...slide,
1644
+ content: {
1645
+ ...slide.content,
1646
+ references,
1647
+ _autoGenerated: true,
1648
+ _generatedEntries: result.entries
1649
+ }
1650
+ };
1651
+ }
1652
+ return slide;
1653
+ })
1654
+ );
1655
+ return {
1656
+ ...presentation,
1657
+ slides: updatedSlides
1658
+ };
1659
+ } catch (error) {
1660
+ this.warnings.push(
1661
+ `Failed to auto-generate bibliography: ${error instanceof Error ? error.message : "Unknown error"}`
1662
+ );
1663
+ return presentation;
1664
+ }
1665
+ }
1666
+ /**
1667
+ * Format authors for template-compatible format
1668
+ */
1669
+ formatAuthorsForTemplate(authors) {
1670
+ if (!authors || authors.length === 0) {
1671
+ return void 0;
1672
+ }
1673
+ return authors.map((a) => {
1674
+ const initial = a.given ? `${a.given.charAt(0)}.` : "";
1675
+ return initial ? `${a.family}, ${initial}` : a.family;
1676
+ });
1677
+ }
1678
+ /**
1679
+ * Get year from CSL item
1680
+ */
1681
+ getYear(item) {
1682
+ const dateParts = item.issued?.["date-parts"];
1683
+ if (dateParts && dateParts[0] && dateParts[0][0]) {
1684
+ return dateParts[0][0];
1685
+ }
1686
+ return void 0;
1687
+ }
1515
1688
  };
1516
1689
 
1517
1690
  // src/index.ts
1518
- var VERSION = "0.2.5";
1691
+ var VERSION = "0.4.0";
1519
1692
 
1520
1693
  // src/cli/commands/convert.ts
1521
1694
  import { Command } from "commander";
@@ -2593,12 +2766,167 @@ import { dirname as dirname3 } from "path";
2593
2766
  import chalk2 from "chalk";
2594
2767
  import ora2 from "ora";
2595
2768
  import { parse as parseYaml6, stringify as yamlStringify } from "yaml";
2769
+
2770
+ // src/references/validator.ts
2771
+ var ReferenceValidator = class {
2772
+ constructor(manager) {
2773
+ this.manager = manager;
2774
+ }
2775
+ extractor = new CitationExtractor();
2776
+ availableCache = null;
2777
+ /**
2778
+ * Check if reference-manager is available (cached)
2779
+ */
2780
+ async checkAvailable() {
2781
+ if (this.availableCache === null) {
2782
+ this.availableCache = await this.manager.isAvailable();
2783
+ }
2784
+ return this.availableCache;
2785
+ }
2786
+ async validateCitations(citationIds) {
2787
+ const available = await this.checkAvailable();
2788
+ if (!available) {
2789
+ return {
2790
+ valid: true,
2791
+ found: [],
2792
+ missing: [],
2793
+ skipped: true,
2794
+ reason: "reference-manager CLI is not available"
2795
+ };
2796
+ }
2797
+ if (citationIds.length === 0) {
2798
+ return {
2799
+ valid: true,
2800
+ found: [],
2801
+ missing: []
2802
+ };
2803
+ }
2804
+ const references = await this.manager.getByIds(citationIds);
2805
+ const found = [];
2806
+ const missing = [];
2807
+ for (const id of citationIds) {
2808
+ if (references.has(id)) {
2809
+ found.push(id);
2810
+ } else {
2811
+ missing.push(id);
2812
+ }
2813
+ }
2814
+ return {
2815
+ valid: missing.length === 0,
2816
+ found,
2817
+ missing
2818
+ };
2819
+ }
2820
+ async validateWithLocations(slides) {
2821
+ const available = await this.checkAvailable();
2822
+ if (!available) {
2823
+ return {
2824
+ valid: true,
2825
+ found: [],
2826
+ missing: [],
2827
+ skipped: true,
2828
+ reason: "reference-manager CLI is not available",
2829
+ missingDetails: []
2830
+ };
2831
+ }
2832
+ const citationLocations = /* @__PURE__ */ new Map();
2833
+ const allCitationIds = [];
2834
+ for (let i = 0; i < slides.length; i++) {
2835
+ const slide = slides[i];
2836
+ const slideNumber = i + 1;
2837
+ const citations = this.extractor.extractFromSlide(slide);
2838
+ for (const citation of citations) {
2839
+ if (!citationLocations.has(citation.id)) {
2840
+ citationLocations.set(citation.id, []);
2841
+ allCitationIds.push(citation.id);
2842
+ }
2843
+ const text = this.findCitationText(slide, citation.id);
2844
+ citationLocations.get(citation.id).push({
2845
+ slide: slideNumber,
2846
+ text
2847
+ });
2848
+ }
2849
+ }
2850
+ if (allCitationIds.length === 0) {
2851
+ return {
2852
+ valid: true,
2853
+ found: [],
2854
+ missing: [],
2855
+ missingDetails: []
2856
+ };
2857
+ }
2858
+ const references = await this.manager.getByIds(allCitationIds);
2859
+ const found = [];
2860
+ const missing = [];
2861
+ const missingDetails = [];
2862
+ for (const id of allCitationIds) {
2863
+ if (references.has(id)) {
2864
+ found.push(id);
2865
+ } else {
2866
+ missing.push(id);
2867
+ missingDetails.push({
2868
+ id,
2869
+ locations: citationLocations.get(id) || []
2870
+ });
2871
+ }
2872
+ }
2873
+ return {
2874
+ valid: missing.length === 0,
2875
+ found,
2876
+ missing,
2877
+ missingDetails
2878
+ };
2879
+ }
2880
+ /**
2881
+ * Generate suggestions for adding missing references
2882
+ */
2883
+ static generateSuggestions(missingIds) {
2884
+ if (missingIds.length === 0) {
2885
+ return "";
2886
+ }
2887
+ const lines = [
2888
+ "Missing citations:",
2889
+ ...missingIds.map((id) => ` - @${id}`),
2890
+ "",
2891
+ "To add these references, use:",
2892
+ " ref add --pmid <pmid> # Add by PubMed ID",
2893
+ ' ref add "<doi>" # Add by DOI',
2894
+ " ref add --isbn <isbn> # Add by ISBN"
2895
+ ];
2896
+ return lines.join("\n");
2897
+ }
2898
+ findCitationText(slide, citationId) {
2899
+ const searchValue = (value) => {
2900
+ if (typeof value === "string") {
2901
+ if (value.includes(`@${citationId}`)) {
2902
+ return value;
2903
+ }
2904
+ } else if (Array.isArray(value)) {
2905
+ for (const item of value) {
2906
+ const found = searchValue(item);
2907
+ if (found) return found;
2908
+ }
2909
+ } else if (value && typeof value === "object") {
2910
+ for (const v of Object.values(value)) {
2911
+ const found = searchValue(v);
2912
+ if (found) return found;
2913
+ }
2914
+ }
2915
+ return null;
2916
+ };
2917
+ return searchValue(slide.content) || `@${citationId}`;
2918
+ }
2919
+ };
2920
+
2921
+ // src/cli/commands/validate.ts
2596
2922
  function getHintForErrorType(errorType) {
2597
2923
  switch (errorType) {
2598
2924
  case "unknown_template":
2599
2925
  return "Run `slide-gen templates list --format llm` to see available templates.";
2600
2926
  case "unknown_icon":
2601
2927
  return "Run `slide-gen icons search <query>` to find icons.";
2928
+ case "missing_reference":
2929
+ return 'Run `ref add --pmid <pmid>` or `ref add "<doi>"` to add the reference.';
2602
2930
  default:
2603
2931
  return null;
2604
2932
  }
@@ -2660,7 +2988,9 @@ async function executeValidate(inputPath, options) {
2660
2988
  slideCount: 0,
2661
2989
  templatesFound: false,
2662
2990
  iconsResolved: false,
2663
- referencesCount: 0
2991
+ referencesCount: 0,
2992
+ referencesValidated: false,
2993
+ missingReferences: []
2664
2994
  }
2665
2995
  };
2666
2996
  try {
@@ -2841,15 +3171,45 @@ ${formatExampleAsYaml(template.example, 2)}` : void 0;
2841
3171
  }
2842
3172
  }
2843
3173
  }
2844
- const citationPattern = /@([a-zA-Z0-9_-]+)/g;
2845
- const references = /* @__PURE__ */ new Set();
2846
- for (const slide of presentation.slides) {
2847
- const contentStr = JSON.stringify(slide.content);
2848
- for (const match of contentStr.matchAll(citationPattern)) {
2849
- references.add(match[1]);
3174
+ const citationExtractor = new CitationExtractor();
3175
+ const extractedCitations = citationExtractor.extractFromPresentation(presentation);
3176
+ const citationIds = citationExtractor.getUniqueIds(extractedCitations);
3177
+ result.stats.referencesCount = citationIds.length;
3178
+ if (config.references?.enabled && citationIds.length > 0) {
3179
+ const refManager = new ReferenceManager();
3180
+ const refValidator = new ReferenceValidator(refManager);
3181
+ const refValidationResult = await refValidator.validateWithLocations(
3182
+ presentation.slides
3183
+ );
3184
+ if (refValidationResult.skipped) {
3185
+ result.warnings.push(
3186
+ `Reference validation skipped: ${refValidationResult.reason}`
3187
+ );
3188
+ } else {
3189
+ result.stats.referencesValidated = true;
3190
+ result.stats.missingReferences = refValidationResult.missing;
3191
+ if (!refValidationResult.valid) {
3192
+ for (const missingDetail of refValidationResult.missingDetails) {
3193
+ for (const location of missingDetail.locations) {
3194
+ const slideLine = result.slideLines[location.slide - 1];
3195
+ result.warnings.push(
3196
+ `Citation not found in library: ${missingDetail.id} (Slide ${location.slide})`
3197
+ );
3198
+ const structuredError = {
3199
+ slide: location.slide,
3200
+ template: presentation.slides[location.slide - 1]?.template || "unknown",
3201
+ message: `Citation not found in library: ${missingDetail.id}`,
3202
+ errorType: "missing_reference"
3203
+ };
3204
+ if (slideLine !== void 0) {
3205
+ structuredError.line = slideLine;
3206
+ }
3207
+ result.structuredErrors.push(structuredError);
3208
+ }
3209
+ }
3210
+ }
2850
3211
  }
2851
3212
  }
2852
- result.stats.referencesCount = references.size;
2853
3213
  if (result.errors.length > 0) {
2854
3214
  result.valid = false;
2855
3215
  }
@@ -2885,7 +3245,16 @@ function outputResult(result, options) {
2885
3245
  valid: result.valid,
2886
3246
  errors: result.errors,
2887
3247
  warnings: result.warnings,
2888
- stats: result.stats
3248
+ stats: {
3249
+ yamlSyntax: result.stats.yamlSyntax,
3250
+ metaValid: result.stats.metaValid,
3251
+ slideCount: result.stats.slideCount,
3252
+ templatesFound: result.stats.templatesFound,
3253
+ iconsResolved: result.stats.iconsResolved,
3254
+ referencesCount: result.stats.referencesCount,
3255
+ referencesValidated: result.stats.referencesValidated,
3256
+ missingReferences: result.stats.missingReferences
3257
+ }
2889
3258
  },
2890
3259
  null,
2891
3260
  2
@@ -2922,6 +3291,22 @@ function outputResult(result, options) {
2922
3291
  if (result.stats.referencesCount > 0) {
2923
3292
  console.log(chalk2.green("\u2713") + ` ${result.stats.referencesCount} references found`);
2924
3293
  }
3294
+ if (result.stats.referencesValidated) {
3295
+ if (result.stats.missingReferences.length === 0) {
3296
+ console.log(chalk2.green("\u2713") + " All references validated");
3297
+ } else {
3298
+ console.log(
3299
+ chalk2.yellow("\u26A0") + ` ${result.stats.missingReferences.length} reference(s) not found in library`
3300
+ );
3301
+ const suggestions = ReferenceValidator.generateSuggestions(
3302
+ result.stats.missingReferences
3303
+ );
3304
+ if (suggestions) {
3305
+ console.log("");
3306
+ console.log(chalk2.dim(suggestions));
3307
+ }
3308
+ }
3309
+ }
2925
3310
  for (const error of result.errors) {
2926
3311
  console.log(chalk2.red("\u2717") + ` ${error}`);
2927
3312
  }
@@ -2952,8 +3337,8 @@ import { tmpdir as tmpdir2 } from "os";
2952
3337
 
2953
3338
  // src/cli/commands/preview.ts
2954
3339
  import { Command as Command3 } from "commander";
2955
- import { access as access6, unlink as unlink2, readdir as readdir4, mkdir as mkdir2, writeFile as writeFile2, readFile as readFile10, rm } from "fs/promises";
2956
- import { basename as basename4, dirname as dirname4, join as join9, extname as extname2 } from "path";
3340
+ import { access as access6, readdir as readdir4, mkdir as mkdir2, writeFile as writeFile2, readFile as readFile10, rm } from "fs/promises";
3341
+ import { basename as basename4, dirname as dirname5, join as join9, extname as extname2 } from "path";
2957
3342
  import * as path6 from "path";
2958
3343
  import { tmpdir } from "os";
2959
3344
  import { createServer } from "http";
@@ -2962,16 +3347,24 @@ import { watch as chokidarWatch } from "chokidar";
2962
3347
 
2963
3348
  // src/cli/utils/marp-runner.ts
2964
3349
  import { existsSync } from "fs";
2965
- import { join as join8 } from "path";
3350
+ import { join as join8, resolve, dirname as dirname4 } from "path";
2966
3351
  import {
2967
3352
  execFileSync,
2968
3353
  spawn
2969
3354
  } from "child_process";
2970
3355
  function getMarpCommand(projectDir) {
2971
- const dir = projectDir ?? process.cwd();
2972
- const localMarp = join8(dir, "node_modules", ".bin", "marp");
2973
- if (existsSync(localMarp)) {
2974
- return localMarp;
3356
+ const startDir = resolve(projectDir ?? process.cwd());
3357
+ let currentDir = startDir;
3358
+ while (true) {
3359
+ const localMarp = join8(currentDir, "node_modules", ".bin", "marp");
3360
+ if (existsSync(localMarp)) {
3361
+ return localMarp;
3362
+ }
3363
+ const parentDir = dirname4(currentDir);
3364
+ if (parentDir === currentDir) {
3365
+ break;
3366
+ }
3367
+ currentDir = parentDir;
2975
3368
  }
2976
3369
  try {
2977
3370
  execFileSync("marp", ["--version"], { stdio: "ignore", timeout: 5e3 });
@@ -3128,23 +3521,19 @@ async function collectSlideInfo(dir, baseName, format) {
3128
3521
  async function checkMarpCliAvailable(projectDir) {
3129
3522
  return isMarpAvailable(projectDir);
3130
3523
  }
3131
- function getTempOutputPath(inputPath) {
3524
+ function getTempPreviewDir(inputPath) {
3132
3525
  const base = basename4(inputPath, ".yaml");
3133
- return join9(tmpdir(), `slide-gen-preview-${base}-${Date.now()}.md`);
3526
+ return join9(tmpdir(), `slide-gen-preview-${base}-${Date.now()}`);
3134
3527
  }
3135
- function buildMarpCommand(markdownPath, options) {
3136
- const parts = ["marp", "--preview"];
3137
- if (options.port) {
3138
- parts.push("-p", String(options.port));
3139
- }
3528
+ function buildMarpCommand(markdownDir, options) {
3529
+ const parts = ["marp", "--server", "-I", markdownDir];
3140
3530
  if (options.watch) {
3141
3531
  parts.push("--watch");
3142
3532
  }
3143
- parts.push(markdownPath);
3144
3533
  return parts.join(" ");
3145
3534
  }
3146
3535
  function startStaticServer(baseDir, port, options = {}) {
3147
- return new Promise((resolve7, reject) => {
3536
+ return new Promise((resolve8, reject) => {
3148
3537
  const mimeTypes = {
3149
3538
  ".html": "text/html",
3150
3539
  ".png": "image/png",
@@ -3181,7 +3570,7 @@ function startStaticServer(baseDir, port, options = {}) {
3181
3570
  const prefix = options.messagePrefix ?? "Server";
3182
3571
  console.log(`${prefix} running at ${chalk3.cyan(url)}`);
3183
3572
  }
3184
- resolve7(server);
3573
+ resolve8(server);
3185
3574
  });
3186
3575
  });
3187
3576
  }
@@ -3198,7 +3587,7 @@ async function executeGalleryPreview(inputPath, options) {
3198
3587
  return { success: false, errors };
3199
3588
  }
3200
3589
  console.log("Checking for Marp CLI...");
3201
- const projectDir = dirname4(inputPath);
3590
+ const projectDir = dirname5(inputPath);
3202
3591
  const marpAvailable = await checkMarpCliAvailable(projectDir);
3203
3592
  if (!marpAvailable) {
3204
3593
  console.error(
@@ -3215,7 +3604,7 @@ async function executeGalleryPreview(inputPath, options) {
3215
3604
  const configLoader = new ConfigLoader();
3216
3605
  let configPath = options.config;
3217
3606
  if (!configPath) {
3218
- configPath = await configLoader.findConfig(dirname4(inputPath));
3607
+ configPath = await configLoader.findConfig(dirname5(inputPath));
3219
3608
  }
3220
3609
  const config = await configLoader.load(configPath);
3221
3610
  console.log("Initializing pipeline...");
@@ -3313,11 +3702,11 @@ Showing ${slides.length} slides in gallery mode`);
3313
3702
  };
3314
3703
  process.on("SIGINT", signalHandler);
3315
3704
  process.on("SIGTERM", signalHandler);
3316
- await new Promise((resolve7) => {
3705
+ await new Promise((resolve8) => {
3317
3706
  if (options.signal) {
3318
- options.signal.addEventListener("abort", () => resolve7());
3707
+ options.signal.addEventListener("abort", () => resolve8());
3319
3708
  }
3320
- server.on("close", () => resolve7());
3709
+ server.on("close", () => resolve8());
3321
3710
  });
3322
3711
  return { success: true, errors };
3323
3712
  }
@@ -3351,7 +3740,7 @@ async function executePreview(inputPath, options) {
3351
3740
  return { success: false, errors };
3352
3741
  }
3353
3742
  console.log("Checking for Marp CLI...");
3354
- const marpAvailable = await checkMarpCliAvailable(dirname4(inputPath));
3743
+ const marpAvailable = await checkMarpCliAvailable(dirname5(inputPath));
3355
3744
  if (!marpAvailable) {
3356
3745
  console.error(
3357
3746
  chalk3.red(
@@ -3366,7 +3755,7 @@ async function executePreview(inputPath, options) {
3366
3755
  const configLoader = new ConfigLoader();
3367
3756
  let configPath = options.config;
3368
3757
  if (!configPath) {
3369
- configPath = await configLoader.findConfig(dirname4(inputPath));
3758
+ configPath = await configLoader.findConfig(dirname5(inputPath));
3370
3759
  }
3371
3760
  const config = await configLoader.load(configPath);
3372
3761
  console.log("Initializing pipeline...");
@@ -3380,7 +3769,9 @@ async function executePreview(inputPath, options) {
3380
3769
  process.exitCode = ExitCode.ConversionError;
3381
3770
  return { success: false, errors };
3382
3771
  }
3383
- const tempMarkdownPath = getTempOutputPath(inputPath);
3772
+ const tempDir = getTempPreviewDir(inputPath);
3773
+ await mkdir2(tempDir, { recursive: true });
3774
+ const tempMarkdownPath = join9(tempDir, "slides.md");
3384
3775
  console.log(`Converting ${chalk3.cyan(inputPath)}...`);
3385
3776
  try {
3386
3777
  await pipeline.runWithResult(inputPath, { outputPath: tempMarkdownPath });
@@ -3390,23 +3781,29 @@ async function executePreview(inputPath, options) {
3390
3781
  console.error(chalk3.red(`Error: Conversion failed: ${message}`));
3391
3782
  errors.push(message);
3392
3783
  process.exitCode = ExitCode.ConversionError;
3784
+ await rm(tempDir, { recursive: true, force: true });
3393
3785
  return { success: false, errors };
3394
3786
  }
3395
3787
  console.log(`
3396
3788
  Starting preview server on port ${chalk3.cyan(port)}...`);
3397
- const marpCommand = buildMarpCommand(tempMarkdownPath, {
3789
+ const marpCommand = buildMarpCommand(tempDir, {
3398
3790
  ...options,
3399
- port,
3400
- watch: false
3401
- // We handle watch ourselves if needed
3791
+ watch: options.watch ?? false
3402
3792
  });
3403
3793
  if (verbose) {
3404
3794
  console.log(`Running: ${marpCommand}`);
3405
3795
  }
3406
- const marpProcess = spawnMarp(["--preview", "-p", String(port), tempMarkdownPath], {
3407
- projectDir: dirname4(inputPath),
3796
+ const marpArgs = ["--server", "-I", tempDir];
3797
+ if (options.watch) {
3798
+ marpArgs.push("--watch");
3799
+ }
3800
+ const marpProcess = spawnMarp(marpArgs, {
3801
+ projectDir: dirname5(inputPath),
3408
3802
  stdio: "inherit"
3409
3803
  });
3804
+ console.log(`
3805
+ Preview available at ${chalk3.cyan(`http://localhost:${port}/slides.md`)}`);
3806
+ console.log("Open this URL in your browser to view the slides.");
3410
3807
  let watcher = null;
3411
3808
  let debounceTimer = null;
3412
3809
  if (options.watch) {
@@ -3444,7 +3841,7 @@ Watching ${chalk3.cyan(inputPath)} for changes...`);
3444
3841
  watcher?.close();
3445
3842
  marpProcess.kill();
3446
3843
  try {
3447
- await unlink2(tempMarkdownPath);
3844
+ await rm(tempDir, { recursive: true, force: true });
3448
3845
  } catch {
3449
3846
  }
3450
3847
  };
@@ -3460,13 +3857,13 @@ Watching ${chalk3.cyan(inputPath)} for changes...`);
3460
3857
  };
3461
3858
  process.on("SIGINT", signalHandler);
3462
3859
  process.on("SIGTERM", signalHandler);
3463
- await new Promise((resolve7) => {
3860
+ await new Promise((resolve8) => {
3464
3861
  marpProcess.on("exit", () => {
3465
3862
  cleanup();
3466
- resolve7();
3863
+ resolve8();
3467
3864
  });
3468
3865
  if (options.signal) {
3469
- options.signal.addEventListener("abort", () => resolve7());
3866
+ options.signal.addEventListener("abort", () => resolve8());
3470
3867
  }
3471
3868
  });
3472
3869
  return {
@@ -4011,8 +4408,8 @@ Showing ${templatePreviews.length} template preview(s)`);
4011
4408
  };
4012
4409
  process.on("SIGINT", signalHandler);
4013
4410
  process.on("SIGTERM", signalHandler);
4014
- await new Promise((resolve7) => {
4015
- server.on("close", () => resolve7());
4411
+ await new Promise((resolve8) => {
4412
+ server.on("close", () => resolve8());
4016
4413
  });
4017
4414
  }
4018
4415
  function createTemplatesCommand() {
@@ -4810,7 +5207,7 @@ import { mkdir as mkdir7, writeFile as writeFile7, access as access10, readdir a
4810
5207
  import { existsSync as existsSync2 } from "fs";
4811
5208
  import { execSync, spawnSync } from "child_process";
4812
5209
  import { createInterface } from "readline";
4813
- import { basename as basename7, dirname as dirname6, join as join16, resolve as resolve5, sep } from "path";
5210
+ import { basename as basename7, dirname as dirname7, join as join16, resolve as resolve6, sep } from "path";
4814
5211
  import { fileURLToPath } from "url";
4815
5212
  import chalk6 from "chalk";
4816
5213
  import ora3 from "ora";
@@ -4889,14 +5286,105 @@ var missingItemSchema = z8.object({
4889
5286
  status: z8.string().optional(),
4890
5287
  notes: z8.string().optional()
4891
5288
  });
5289
+ var referenceItemSchema = z8.object({
5290
+ id: z8.string(),
5291
+ status: z8.enum(["pending", "added", "existing"]),
5292
+ slide: z8.number(),
5293
+ purpose: z8.string(),
5294
+ requirement: z8.enum(["required", "recommended"]).optional(),
5295
+ added_date: z8.string().optional(),
5296
+ suggested_search: z8.array(z8.string()).optional(),
5297
+ notes: z8.string().optional()
5298
+ });
5299
+ var referencesStatusSchema = z8.object({
5300
+ required: z8.number().default(0),
5301
+ found: z8.number().default(0),
5302
+ pending: z8.number().default(0)
5303
+ });
5304
+ var referencesSectionSchema = z8.object({
5305
+ status: referencesStatusSchema.optional(),
5306
+ items: z8.array(referenceItemSchema).default([])
5307
+ });
4892
5308
  var sourcesYamlSchema = z8.object({
4893
5309
  project: projectSchema,
4894
5310
  context: contextSchema.optional(),
4895
5311
  sources: z8.array(sourceEntrySchema).optional(),
4896
5312
  dependencies: z8.record(dependencySchema).optional(),
4897
- missing: z8.array(missingItemSchema).optional()
5313
+ missing: z8.array(missingItemSchema).optional(),
5314
+ references: referencesSectionSchema.optional()
4898
5315
  });
4899
5316
 
5317
+ // src/sources/references-tracker.ts
5318
+ var ReferencesTracker = class {
5319
+ items = [];
5320
+ constructor(initial) {
5321
+ if (initial?.items) {
5322
+ this.items = [...initial.items];
5323
+ }
5324
+ }
5325
+ /**
5326
+ * Add a pending reference that needs to be found
5327
+ */
5328
+ addPending(ref) {
5329
+ this.items.push({
5330
+ ...ref,
5331
+ status: "pending"
5332
+ });
5333
+ }
5334
+ /**
5335
+ * Mark a pending reference as added with its actual citation key
5336
+ */
5337
+ markAdded(pendingId, actualId) {
5338
+ const item = this.items.find((i) => i.id === pendingId);
5339
+ if (item) {
5340
+ item.id = actualId;
5341
+ item.status = "added";
5342
+ item.added_date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
5343
+ }
5344
+ }
5345
+ /**
5346
+ * Mark a reference as existing in the bibliography
5347
+ */
5348
+ markExisting(ref) {
5349
+ this.items.push({
5350
+ ...ref,
5351
+ status: "existing"
5352
+ });
5353
+ }
5354
+ /**
5355
+ * Get all reference items
5356
+ */
5357
+ getItems() {
5358
+ return [...this.items];
5359
+ }
5360
+ /**
5361
+ * Get only pending references
5362
+ */
5363
+ getPending() {
5364
+ return this.items.filter((i) => i.status === "pending");
5365
+ }
5366
+ /**
5367
+ * Calculate status summary
5368
+ */
5369
+ getStatus() {
5370
+ const required = this.items.filter(
5371
+ (i) => i.requirement === "required"
5372
+ ).length;
5373
+ const pending = this.items.filter((i) => i.status === "pending").length;
5374
+ const found = this.items.filter((i) => i.status !== "pending").length;
5375
+ return { required, found, pending };
5376
+ }
5377
+ /**
5378
+ * Convert to YAML-compatible object
5379
+ */
5380
+ toYaml() {
5381
+ return {
5382
+ status: this.getStatus(),
5383
+ items: this.items
5384
+ };
5385
+ }
5386
+ };
5387
+
4900
5388
  // src/sources/manager.ts
4901
5389
  var SourcesManager = class _SourcesManager {
4902
5390
  sourcesDir;
@@ -5071,6 +5559,44 @@ var SourcesManager = class _SourcesManager {
5071
5559
  const data = await this.load();
5072
5560
  return data.sources?.filter((s) => s.type === type) ?? [];
5073
5561
  }
5562
+ /**
5563
+ * Get references from sources.yaml
5564
+ */
5565
+ async getReferences() {
5566
+ const data = await this.load();
5567
+ const tracker = new ReferencesTracker(data.references);
5568
+ return tracker.toYaml();
5569
+ }
5570
+ /**
5571
+ * Add a pending reference that needs to be found
5572
+ */
5573
+ async addPendingReference(ref) {
5574
+ const data = await this.load();
5575
+ const tracker = new ReferencesTracker(data.references);
5576
+ tracker.addPending(ref);
5577
+ data.references = tracker.toYaml();
5578
+ await this.save(data);
5579
+ }
5580
+ /**
5581
+ * Mark a pending reference as added with its actual citation key
5582
+ */
5583
+ async markReferenceAdded(pendingId, actualId) {
5584
+ const data = await this.load();
5585
+ const tracker = new ReferencesTracker(data.references);
5586
+ tracker.markAdded(pendingId, actualId);
5587
+ data.references = tracker.toYaml();
5588
+ await this.save(data);
5589
+ }
5590
+ /**
5591
+ * Mark a reference as existing in the bibliography
5592
+ */
5593
+ async markReferenceExisting(ref) {
5594
+ const data = await this.load();
5595
+ const tracker = new ReferencesTracker(data.references);
5596
+ tracker.markExisting(ref);
5597
+ data.references = tracker.toYaml();
5598
+ await this.save(data);
5599
+ }
5074
5600
  };
5075
5601
 
5076
5602
  // src/sources/importer.ts
@@ -5467,6 +5993,51 @@ allowed-tools: Read Write Edit Bash Glob Grep
5467
5993
 
5468
5994
  Helps create Marp slides using the slide-gen CLI tool.
5469
5995
 
5996
+ ## First Question
5997
+
5998
+ **Always start by asking about the user's material situation:**
5999
+
6000
+ > "Let's create slides. What materials do you have?
6001
+ >
6002
+ > A) I have detailed materials organized in a directory
6003
+ > (scenarios, scripts, data files, images, etc.)
6004
+ >
6005
+ > B) I have partial materials like a scenario or script
6006
+ > (but need to supplement with additional info)
6007
+ >
6008
+ > C) I don't have materials yet
6009
+ > (starting from scratch, will collect info through dialogue)"
6010
+
6011
+ Then follow the appropriate pattern below.
6012
+
6013
+ ## Workflow Patterns
6014
+
6015
+ ### Pattern A: Explore Mode (Detailed Materials Exist)
6016
+
6017
+ When user has materials organized in a directory:
6018
+ 1. Ask for directory path and scan with Glob
6019
+ 2. Read and classify files (scenario, scripts, data, images)
6020
+ 3. Summarize findings and confirm with user
6021
+ 4. Configure \`sources/\` directory
6022
+
6023
+ ### Pattern B: Supplement Mode (Partial Materials)
6024
+
6025
+ When user has only a scenario or partial materials:
6026
+ 1. Read and analyze provided content
6027
+ 2. Identify what's present vs. missing
6028
+ 3. Ask targeted questions to fill gaps
6029
+ 4. Configure \`sources/\` directory
6030
+
6031
+ ### Pattern C: Interview Mode (Starting from Scratch)
6032
+
6033
+ When user has no materials:
6034
+ 1. Ask basic questions (purpose, audience, duration)
6035
+ 2. Collect data and examples
6036
+ 3. Propose slide structure for approval
6037
+ 4. Configure \`sources/\` directory
6038
+
6039
+ **See [references/workflows.md](references/workflows.md) for detailed steps.**
6040
+
5470
6041
  ## Capabilities
5471
6042
 
5472
6043
  1. **Project initialization**: \`slide-gen init\`
@@ -5476,21 +6047,14 @@ Helps create Marp slides using the slide-gen CLI tool.
5476
6047
  5. **Conversion**: \`slide-gen convert\`
5477
6048
  6. **Screenshot**: \`slide-gen screenshot\` (for AI review)
5478
6049
 
5479
- ## Workflow
5480
-
5481
- ### New Project
5482
-
5483
- 1. Run \`slide-gen init <directory>\` to initialize
5484
- 2. Gather requirements from user
5485
- 3. Check templates with \`slide-gen templates list --format llm\`
5486
- 4. Create presentation.yaml
5487
- 5. Validate and convert
6050
+ ## Slide Creation Flow
5488
6051
 
5489
- ### Existing Project
6052
+ After material collection (Pattern A/B/C above):
5490
6053
 
5491
- 1. Read presentation.yaml
5492
- 2. Edit according to user request
5493
- 3. Validate and convert
6054
+ 1. Check templates with \`slide-gen templates list --format llm\`
6055
+ 2. Create presentation.yaml
6056
+ 3. Validate: \`slide-gen validate presentation.yaml\`
6057
+ 4. Convert: \`slide-gen convert presentation.yaml\`
5494
6058
 
5495
6059
  ## YAML Source Format
5496
6060
 
@@ -5573,6 +6137,76 @@ The \`validate --format llm\` command provides:
5573
6137
  - Error locations with line numbers
5574
6138
  - Fix examples from template definitions
5575
6139
  - Contextual hints for unknown templates/icons
6140
+
6141
+ ## Visual Feedback Loop
6142
+
6143
+ After creating or modifying slides, use this workflow to review and iterate:
6144
+
6145
+ ### Step 1: Take Screenshot
6146
+ \`\`\`bash
6147
+ # AI-optimized format (recommended)
6148
+ slide-gen screenshot presentation.yaml --format ai
6149
+
6150
+ # Or contact sheet for overview
6151
+ slide-gen screenshot presentation.yaml --contact-sheet
6152
+ \`\`\`
6153
+
6154
+ ### Step 2: Review Images
6155
+ Use the Read tool to view the generated screenshots:
6156
+ \`\`\`
6157
+ Read ./screenshots/presentation.001.jpeg
6158
+ \`\`\`
6159
+
6160
+ ### Step 3: Identify Issues
6161
+ Look for:
6162
+ - Layout problems (text overflow, alignment)
6163
+ - Visual balance (too much/little content)
6164
+ - Icon and color appropriateness
6165
+ - Readability of text and diagrams
6166
+
6167
+ ### Step 4: Make Adjustments
6168
+ Edit presentation.yaml to fix identified issues.
6169
+
6170
+ ### Step 5: Repeat
6171
+ Take new screenshots and verify improvements.
6172
+
6173
+ ### Example Iteration Cycle
6174
+
6175
+ 1. Create initial slides
6176
+ 2. \`slide-gen screenshot presentation.yaml --format ai --slide 3\`
6177
+ 3. \`Read ./screenshots/presentation.003.jpeg\`
6178
+ 4. Notice: "Title text is too long, wrapping awkwardly"
6179
+ 5. Edit presentation.yaml to shorten title
6180
+ 6. \`slide-gen screenshot presentation.yaml --format ai --slide 3\`
6181
+ 7. \`Read ./screenshots/presentation.003.jpeg\`
6182
+ 8. Verify fix, move to next slide
6183
+
6184
+ ## Reference Management
6185
+
6186
+ For academic presentations, manage citations and references:
6187
+
6188
+ 1. **Analyze** content for citation needs
6189
+ 2. **Search** existing library: \`ref search\`
6190
+ 3. **Add** new references: \`ref add pmid:XXX\`
6191
+ 4. **Validate** citations: \`slide-gen validate\`
6192
+
6193
+ See \`.skills/slide-assistant/references/skill.md\` for detailed workflow.
6194
+
6195
+ ### Quick Commands
6196
+
6197
+ \`\`\`bash
6198
+ # Search library
6199
+ ref search "keyword" --format json
6200
+
6201
+ # Add from PMID
6202
+ ref add pmid:38941256
6203
+
6204
+ # Add from DOI
6205
+ ref add "10.1038/xxxxx"
6206
+
6207
+ # Validate citations
6208
+ slide-gen validate presentation.yaml
6209
+ \`\`\`
5576
6210
  `;
5577
6211
  }
5578
6212
 
@@ -5633,7 +6267,24 @@ Read \`.skills/slide-assistant/SKILL.md\` for detailed instructions.
5633
6267
  - \`/slide-validate\` - Validate slide source file
5634
6268
  - \`/slide-preview\` - Preview slides in browser
5635
6269
  - \`/slide-screenshot\` - Take screenshots for review
6270
+ - \`/slide-review\` - Visual review and iteration workflow
5636
6271
  - \`/slide-theme\` - Adjust theme and styling
6272
+
6273
+ ## Important: Visual Review
6274
+
6275
+ **After creating or editing slides, always run visual review:**
6276
+
6277
+ \`\`\`bash
6278
+ /slide-review
6279
+ \`\`\`
6280
+
6281
+ Or manually:
6282
+ 1. \`slide-gen screenshot presentation.yaml --format ai\`
6283
+ 2. \`Read ./screenshots/presentation.001.jpeg\`
6284
+ 3. Check layout, text overflow, visual balance
6285
+ 4. Edit and repeat until satisfied
6286
+
6287
+ This ensures slides look correct before delivery.
5637
6288
  `;
5638
6289
  }
5639
6290
 
@@ -5835,31 +6486,72 @@ Image with text side by side.
5835
6486
  function generateWorkflowsRef() {
5836
6487
  return `# Workflow Reference
5837
6488
 
6489
+ ## Entry Point
6490
+
6491
+ **Always start by asking this question:**
6492
+
6493
+ > "Let's create slides. What materials do you have?
6494
+ >
6495
+ > A) I have detailed materials organized in a directory
6496
+ > (scenarios, scripts, data files, images, etc.)
6497
+ >
6498
+ > B) I have partial materials like a scenario or script
6499
+ > (but need to supplement with additional info)
6500
+ >
6501
+ > C) I don't have materials yet
6502
+ > (starting from scratch, will collect info through dialogue)"
6503
+
6504
+ Based on the answer, follow the appropriate pattern below.
6505
+
5838
6506
  ## Source Collection Flow
5839
6507
 
5840
- ### Pattern A: Directory Exploration
5841
- When user has materials in a directory:
5842
- 1. Ask for directory path
5843
- 2. Scan with Glob tool
5844
- 3. Read and classify files
5845
- 4. Summarize and confirm with user
5846
- 5. Create sources/sources.yaml
5847
-
5848
- ### Pattern B: Supplement Mode
5849
- When user has partial materials:
5850
- 1. Read provided file/text
5851
- 2. Analyze content
5852
- 3. Identify missing information
5853
- 4. Ask supplementary questions
5854
- 5. Create sources/sources.yaml
5855
-
5856
- ### Pattern C: Interview Mode
6508
+ ### Pattern A: Explore Mode (Detailed Materials Exist)
6509
+
6510
+ When user has materials organized in a directory:
6511
+
6512
+ 1. **Ask for the directory path**
6513
+ 2. **Scan directory structure** with Glob tool
6514
+ 3. **Read and analyze each file**
6515
+ 4. **Classify files** into categories:
6516
+ - Scenario/scripts
6517
+ - Data files (CSV, JSON, etc.)
6518
+ - Images and diagrams
6519
+ - Reference documents
6520
+ 5. **Summarize findings** and confirm with user
6521
+ 6. **Ask clarifying questions** about gaps
6522
+ 7. **Configure \`sources/\` directory** with organized materials
6523
+ 8. Proceed to slide creation
6524
+
6525
+ ### Pattern B: Supplement Mode (Partial Materials)
6526
+
6527
+ When user has only a scenario or partial materials:
6528
+
6529
+ 1. **Ask user** to provide the file path or paste content
6530
+ 2. **Analyze the content** thoroughly
6531
+ 3. **Identify what information is present** vs. missing
6532
+ 4. **Ask targeted questions** to fill gaps:
6533
+ - Purpose and audience
6534
+ - Duration and format
6535
+ - Key messages
6536
+ - Available data/examples
6537
+ 5. **Load any additional files** user mentions
6538
+ 6. **Configure \`sources/\` directory**
6539
+ 7. Proceed to slide creation
6540
+
6541
+ ### Pattern C: Interview Mode (Starting from Scratch)
6542
+
5857
6543
  When user has no materials:
5858
- 1. Ask basic questions (purpose, audience, duration)
5859
- 2. Deep-dive based on purpose
5860
- 3. Propose slide structure
5861
- 4. Iterate based on feedback
5862
- 5. Create sources/sources.yaml
6544
+
6545
+ 1. **Ask basic questions**:
6546
+ - "What is this presentation about?"
6547
+ - "Who is the audience?"
6548
+ - "How long is the presentation?"
6549
+ 2. **Ask purpose-specific questions** (proposal, report, introduction, etc.)
6550
+ 3. **Collect data and examples** user can provide
6551
+ 4. **Propose slide structure** for approval
6552
+ 5. **Incorporate feedback**
6553
+ 6. **Configure \`sources/\` directory** from conversation
6554
+ 7. Proceed to slide creation
5863
6555
 
5864
6556
  ## Slide Creation Flow
5865
6557
 
@@ -5908,6 +6600,221 @@ After user provides images:
5908
6600
 
5909
6601
  ### Phase 4: Iteration
5910
6602
  Handle adjustments (cropping, replacement) as needed.
6603
+
6604
+ ## Visual Review Flow
6605
+
6606
+ ### When to Use Visual Review
6607
+
6608
+ - After initial slide creation
6609
+ - When adjusting layouts or styling
6610
+ - Before final delivery
6611
+ - When user reports visual issues
6612
+
6613
+ ### Quick Review Workflow
6614
+
6615
+ 1. **Generate screenshots**:
6616
+ \`\`\`bash
6617
+ slide-gen screenshot presentation.yaml --format ai
6618
+ \`\`\`
6619
+
6620
+ 2. **Review each slide**:
6621
+ \`\`\`
6622
+ Read ./screenshots/presentation.001.jpeg
6623
+ Read ./screenshots/presentation.002.jpeg
6624
+ ...
6625
+ \`\`\`
6626
+
6627
+ 3. **Document issues** found in each slide
6628
+
6629
+ 4. **Make batch edits** to presentation.yaml
6630
+
6631
+ 5. **Regenerate and verify**:
6632
+ \`\`\`bash
6633
+ slide-gen screenshot presentation.yaml --format ai
6634
+ \`\`\`
6635
+
6636
+ ### Contact Sheet Review
6637
+
6638
+ For quick overview of all slides:
6639
+
6640
+ 1. **Generate contact sheet**:
6641
+ \`\`\`bash
6642
+ slide-gen screenshot presentation.yaml --contact-sheet
6643
+ \`\`\`
6644
+
6645
+ 2. **Review overview**:
6646
+ \`\`\`
6647
+ Read ./screenshots/presentation-contact.png
6648
+ \`\`\`
6649
+
6650
+ 3. **Identify slides needing attention**
6651
+
6652
+ 4. **Deep dive on specific slides**:
6653
+ \`\`\`bash
6654
+ slide-gen screenshot presentation.yaml --format ai --slide 5
6655
+ \`\`\`
6656
+
6657
+ ### Common Visual Issues to Check
6658
+
6659
+ | Issue | What to Look For | Fix |
6660
+ |-------|------------------|-----|
6661
+ | Text overflow | Text cut off or wrapped | Shorten text, use bullet-list |
6662
+ | Empty space | Large blank areas | Add content or use different template |
6663
+ | Cluttered | Too much content | Split into multiple slides |
6664
+ | Poor contrast | Hard to read text | Adjust colors in theme |
6665
+ | Icon mismatch | Icon doesn't fit context | Search for better icon |
6666
+ `;
6667
+ }
6668
+
6669
+ // src/cli/templates/ai/references/skill-references.ts
6670
+ function generateReferenceSkillMd() {
6671
+ return `## Reference Management Skill
6672
+
6673
+ ### When to Invoke
6674
+
6675
+ Use this skill when:
6676
+ - Creating academic or research presentations
6677
+ - User mentions needing citations or references
6678
+ - Scenario contains statistical claims or research findings
6679
+ - User provides literature (URL, PDF, DOI, PMID)
6680
+
6681
+ ### Citation Requirement Analysis
6682
+
6683
+ Analyze scenario/script for statements requiring citations:
6684
+
6685
+ | Statement Type | Requirement | Example |
6686
+ |---------------|-------------|---------|
6687
+ | Statistical claims | Required | "Accuracy exceeds 90%" |
6688
+ | Research findings | Required | "Studies show that..." |
6689
+ | Methodology references | Required | "Using the XYZ method" |
6690
+ | Comparative claims | Recommended | "Better than conventional" |
6691
+ | Historical facts | Recommended | "First introduced in 2020" |
6692
+ | General knowledge | Not required | "AI is widely used" |
6693
+
6694
+ ### Workflow
6695
+
6696
+ #### Phase 1: Analyze Content
6697
+ 1. Read scenario/script thoroughly
6698
+ 2. Identify statements requiring citations
6699
+ 3. Categorize as Required or Recommended
6700
+ 4. Note the slide number and exact statement
6701
+
6702
+ #### Phase 2: Search Existing Library
6703
+ \`\`\`bash
6704
+ # List all references
6705
+ ref list --format json
6706
+
6707
+ # Search by keyword
6708
+ ref search "diagnostic accuracy" --format json
6709
+ \`\`\`
6710
+
6711
+ #### Phase 3: Match or Request
6712
+
6713
+ **If found in library:**
6714
+ - Confirm relevance with user
6715
+ - Insert \`[@id]\` citation in YAML
6716
+
6717
+ **If not found:**
6718
+ - Present clear request to user
6719
+ - Specify what type of source is needed
6720
+ - Provide suggested search terms
6721
+
6722
+ #### Phase 4: Add New References
6723
+
6724
+ From user-provided input:
6725
+
6726
+ \`\`\`bash
6727
+ # From PMID
6728
+ ref add pmid:38941256
6729
+
6730
+ # From DOI
6731
+ ref add "10.1038/s41591-024-xxxxx"
6732
+
6733
+ # From ISBN
6734
+ ref add "ISBN:978-4-00-000000-0"
6735
+
6736
+ # From BibTeX file
6737
+ ref add paper.bib
6738
+ \`\`\`
6739
+
6740
+ #### Phase 5: Insert Citations
6741
+
6742
+ Update presentation.yaml:
6743
+ \`\`\`yaml
6744
+ items:
6745
+ - "This claim is supported [@smith2024]"
6746
+ \`\`\`
6747
+
6748
+ ### Extracting from Non-Standard Input
6749
+
6750
+ #### URL Patterns
6751
+ - PubMed: Extract PMID from \`pubmed.ncbi.nlm.nih.gov/XXXXXXXX\`
6752
+ - DOI: Extract from \`doi.org/10.XXXX/XXXXX\`
6753
+ - Publisher sites: Fetch page, extract DOI from metadata
6754
+
6755
+ #### PDF Files
6756
+ 1. Read PDF file
6757
+ 2. Extract DOI from first page or metadata
6758
+ 3. If not found, extract title and search databases
6759
+
6760
+ #### Free Text
6761
+ 1. Parse author, year, journal information
6762
+ 2. Search PubMed/CrossRef
6763
+ 3. Present candidates for user confirmation
6764
+
6765
+ ### User Communication Templates
6766
+
6767
+ **Analyzing content:**
6768
+ \`\`\`
6769
+ I've analyzed your scenario and identified citation needs:
6770
+
6771
+ Required Citations (N)
6772
+ ----------------------
6773
+ 1. Slide X: '[statement]'
6774
+ -> Needs: [type of source]
6775
+
6776
+ Recommended Citations (M)
6777
+ -------------------------
6778
+ ...
6779
+
6780
+ Let me check your reference library...
6781
+ \`\`\`
6782
+
6783
+ **Requesting references:**
6784
+ \`\`\`
6785
+ I need your help finding references:
6786
+
6787
+ [REQUIRED] Reference 1: [Topic]
6788
+ -------------------------------
6789
+ Purpose: Support claim '[statement]' on Slide X
6790
+
6791
+ Ideal source type:
6792
+ - [type1]
6793
+ - [type2]
6794
+
6795
+ Suggested search terms:
6796
+ - [term1]
6797
+ - [term2]
6798
+
6799
+ How to provide:
6800
+ A) DOI or PMID (e.g., 'PMID: 38941256')
6801
+ B) URL (PubMed, journal site, etc.)
6802
+ C) PDF file
6803
+ D) Manual citation details
6804
+ \`\`\`
6805
+
6806
+ **Confirming addition:**
6807
+ \`\`\`
6808
+ Reference added successfully:
6809
+ -----------------------------
6810
+ Citation key: [@id]
6811
+ Authors: ...
6812
+ Title: '...'
6813
+ Journal: ...
6814
+ Year: XXXX
6815
+
6816
+ I'll use this for Slide X.
6817
+ \`\`\`
5911
6818
  `;
5912
6819
  }
5913
6820
 
@@ -6032,18 +6939,128 @@ slide-gen screenshot $ARGUMENTS
6032
6939
 
6033
6940
  If no argument provided:
6034
6941
  \`\`\`bash
6035
- slide-gen screenshot presentation.yaml -o screenshots/
6942
+ slide-gen screenshot presentation.yaml --format ai
6036
6943
  \`\`\`
6037
6944
 
6038
6945
  ## Options
6039
6946
 
6040
- - \`--slide <number>\`: Screenshot specific slide only
6041
- - \`--width <pixels>\`: Image width (default: 1280)
6042
- - \`--output <dir>\`: Output directory
6947
+ | Option | Description | Default |
6948
+ |--------|-------------|---------|
6949
+ | \`--format <fmt>\` | Output format (png/jpeg/ai) | png |
6950
+ | \`--slide <number>\` | Screenshot specific slide only | All |
6951
+ | \`--contact-sheet\` | Generate overview of all slides | false |
6952
+ | \`--columns <num>\` | Contact sheet columns | 2 |
6953
+ | \`--width <pixels>\` | Image width | 1280 (ai: 640) |
6954
+ | \`--quality <num>\` | JPEG quality (1-100) | 80 |
6955
+ | \`--output <dir>\` | Output directory | ./screenshots |
6956
+
6957
+ ## AI Optimization Mode
6043
6958
 
6044
- ## After Screenshot
6959
+ Use \`--format ai\` for token-efficient screenshots:
6960
+ - 640px width (75% token reduction)
6961
+ - JPEG format
6962
+ - Shows estimated token consumption
6045
6963
 
6046
- Read the screenshot images to review slide content and provide feedback.
6964
+ \`\`\`bash
6965
+ # AI-optimized screenshots
6966
+ slide-gen screenshot presentation.yaml --format ai
6967
+
6968
+ # Contact sheet for overview
6969
+ slide-gen screenshot presentation.yaml --contact-sheet
6970
+
6971
+ # AI-optimized contact sheet
6972
+ slide-gen screenshot presentation.yaml --format ai --contact-sheet
6973
+ \`\`\`
6974
+
6975
+ ## Token Efficiency
6976
+
6977
+ | Format | Width | Est. Tokens/slide |
6978
+ |--------|-------|-------------------|
6979
+ | png/jpeg | 1280 | ~1,229 |
6980
+ | ai | 640 | ~308 (~75% reduction) |
6981
+
6982
+ ## Visual Feedback Workflow
6983
+
6984
+ 1. Take screenshot: \`slide-gen screenshot presentation.yaml --format ai\`
6985
+ 2. Review image: \`Read ./screenshots/presentation.001.jpeg\`
6986
+ 3. Identify issues (layout, text, icons)
6987
+ 4. Edit presentation.yaml
6988
+ 5. Repeat until satisfied
6989
+ `;
6990
+ }
6991
+
6992
+ // src/cli/templates/ai/commands/slide-review.ts
6993
+ function generateSlideReviewCommand() {
6994
+ return `Review slides visually and iterate on improvements.
6995
+
6996
+ ## Workflow
6997
+
6998
+ 1. **Take AI-optimized screenshots**:
6999
+ \`\`\`bash
7000
+ slide-gen screenshot $ARGUMENTS --format ai
7001
+ \`\`\`
7002
+ If no argument: \`slide-gen screenshot presentation.yaml --format ai\`
7003
+
7004
+ 2. **Review each slide image**:
7005
+ \`\`\`
7006
+ Read ./screenshots/presentation.001.jpeg
7007
+ Read ./screenshots/presentation.002.jpeg
7008
+ ...
7009
+ \`\`\`
7010
+
7011
+ 3. **Check for issues**:
7012
+ - Text overflow or awkward wrapping
7013
+ - Poor visual balance (too empty / too cluttered)
7014
+ - Icon appropriateness
7015
+ - Color contrast and readability
7016
+ - Diagram clarity
7017
+
7018
+ 4. **Report findings** to user with specific slide numbers
7019
+
7020
+ 5. **If issues found**, edit presentation.yaml and repeat from step 1
7021
+
7022
+ ## Quick Overview
7023
+
7024
+ For a quick overview of all slides:
7025
+ \`\`\`bash
7026
+ slide-gen screenshot presentation.yaml --contact-sheet
7027
+ Read ./screenshots/presentation-contact.png
7028
+ \`\`\`
7029
+
7030
+ ## Token Efficiency
7031
+
7032
+ Always use \`--format ai\` for ~75% token reduction:
7033
+ - Default: ~1,229 tokens/slide
7034
+ - AI mode: ~308 tokens/slide
7035
+
7036
+ ## Common Issues Checklist
7037
+
7038
+ | Issue | What to Look For | Fix |
7039
+ |-------|------------------|-----|
7040
+ | Text overflow | Text cut off or wrapped | Shorten text, use bullet-list |
7041
+ | Empty space | Large blank areas | Add content or use different template |
7042
+ | Cluttered | Too much content | Split into multiple slides |
7043
+ | Poor contrast | Hard to read text | Adjust colors in theme |
7044
+ | Icon mismatch | Icon doesn't fit context | Search for better icon |
7045
+
7046
+ ## Example Session
7047
+
7048
+ \`\`\`bash
7049
+ # Initial review
7050
+ slide-gen screenshot presentation.yaml --format ai
7051
+
7052
+ # Check slide 3
7053
+ Read ./screenshots/presentation.003.jpeg
7054
+ # Notice: "Title text is too long"
7055
+
7056
+ # Edit presentation.yaml to shorten title
7057
+
7058
+ # Re-take screenshot for slide 3
7059
+ slide-gen screenshot presentation.yaml --format ai --slide 3
7060
+
7061
+ # Verify fix
7062
+ Read ./screenshots/presentation.003.jpeg
7063
+ \`\`\`
6047
7064
  `;
6048
7065
  }
6049
7066
 
@@ -6070,9 +7087,75 @@ function generateSlideThemeCommand() {
6070
7087
  `;
6071
7088
  }
6072
7089
 
7090
+ // src/cli/templates/ai/commands/slide-references.ts
7091
+ function generateSlideReferencesCommand() {
7092
+ return `Manage references and citations for the presentation.
7093
+
7094
+ ## Available Actions
7095
+
7096
+ ### 1. Analyze - Find citation needs
7097
+ Analyze the scenario/content for statements that need citations.
7098
+
7099
+ ### 2. Search - Find in library
7100
+ Search existing reference-manager library for relevant papers.
7101
+
7102
+ ### 3. Add - Add new reference
7103
+ Add a reference from PMID, DOI, URL, or file.
7104
+
7105
+ ### 4. List - Show all references
7106
+ List all references currently in the library.
7107
+
7108
+ ## Usage
7109
+
7110
+ ### Analyze scenario for citation needs:
7111
+ \`\`\`bash
7112
+ # First, read the presentation
7113
+ cat presentation.yaml
7114
+
7115
+ # Then analyze content for citation requirements
7116
+ \`\`\`
7117
+ Report statements needing citations with slide numbers.
7118
+
7119
+ ### Search library:
7120
+ \`\`\`bash
7121
+ ref search "keyword" --format json
7122
+ ref list --format json
7123
+ \`\`\`
7124
+
7125
+ ### Add reference:
7126
+ \`\`\`bash
7127
+ # From PMID
7128
+ ref add pmid:38941256
7129
+
7130
+ # From DOI
7131
+ ref add "10.1038/s41591-024-xxxxx"
7132
+
7133
+ # From URL (extract identifier first)
7134
+ # PubMed URL -> extract PMID
7135
+ # DOI URL -> extract DOI
7136
+
7137
+ # From file
7138
+ ref add paper.bib
7139
+ ref add export.ris
7140
+ \`\`\`
7141
+
7142
+ ### Validate citations:
7143
+ \`\`\`bash
7144
+ slide-gen validate presentation.yaml
7145
+ \`\`\`
7146
+
7147
+ ## Notes
7148
+
7149
+ - Always check library before requesting new references
7150
+ - Extract PMID/DOI from URLs before adding
7151
+ - Report missing citations with suggested search terms
7152
+ - Update presentation.yaml with [@id] format
7153
+ `;
7154
+ }
7155
+
6073
7156
  // src/cli/commands/init.ts
6074
7157
  function getPackageRoot() {
6075
- const __dirname = dirname6(fileURLToPath(import.meta.url));
7158
+ const __dirname = dirname7(fileURLToPath(import.meta.url));
6076
7159
  if (__dirname.includes(`${sep}src${sep}`) || __dirname.includes("/src/")) {
6077
7160
  return join16(__dirname, "..", "..", "..");
6078
7161
  }
@@ -6085,7 +7168,7 @@ function createInitCommand() {
6085
7168
  }
6086
7169
  async function executeInit(directory, options) {
6087
7170
  const spinner = ora3();
6088
- const targetDir = resolve5(directory);
7171
+ const targetDir = resolve6(directory);
6089
7172
  const includeExamples = options.examples !== false;
6090
7173
  const includeAiConfig = options.aiConfig !== false;
6091
7174
  const includeSources = options.sources !== false;
@@ -6134,11 +7217,11 @@ async function executeInit(directory, options) {
6134
7217
  await sourcesManager.init({
6135
7218
  name: "Untitled Project",
6136
7219
  setup_pattern: options.fromDirectory ? "A" : void 0,
6137
- original_source: options.fromDirectory ? resolve5(options.fromDirectory) : void 0
7220
+ original_source: options.fromDirectory ? resolve6(options.fromDirectory) : void 0
6138
7221
  });
6139
7222
  if (options.fromDirectory) {
6140
7223
  const importer = new SourceImporter(targetDir, sourcesManager);
6141
- const result = await importer.importDirectory(resolve5(options.fromDirectory), {
7224
+ const result = await importer.importDirectory(resolve6(options.fromDirectory), {
6142
7225
  recursive: true
6143
7226
  });
6144
7227
  sourcesImported = result.imported;
@@ -6159,6 +7242,7 @@ async function executeInit(directory, options) {
6159
7242
  }
6160
7243
  if (includeAiConfig) {
6161
7244
  console.log(` ${chalk6.cyan(".skills/")} - AgentSkills configuration`);
7245
+ console.log(` ${chalk6.cyan(".claude/skills/")} - Claude Code skills`);
6162
7246
  console.log(` ${chalk6.cyan("CLAUDE.md")} - Claude Code configuration`);
6163
7247
  console.log(` ${chalk6.cyan("AGENTS.md")} - OpenCode configuration`);
6164
7248
  console.log(` ${chalk6.cyan(".cursorrules")} - Cursor configuration`);
@@ -6243,16 +7327,16 @@ async function promptMarpInstallChoice() {
6243
7327
  console.log(` ${chalk6.cyan("2)")} Local install ${chalk6.dim("(creates package.json)")}`);
6244
7328
  console.log(` ${chalk6.cyan("3)")} Skip ${chalk6.dim("(I'll install it later)")}`);
6245
7329
  console.log("");
6246
- return new Promise((resolve7) => {
7330
+ return new Promise((resolve8) => {
6247
7331
  rl.question("Choice [1]: ", (answer) => {
6248
7332
  rl.close();
6249
7333
  const normalized = answer.trim();
6250
7334
  if (normalized === "" || normalized === "1") {
6251
- resolve7("global");
7335
+ resolve8("global");
6252
7336
  } else if (normalized === "2") {
6253
- resolve7("local");
7337
+ resolve8("local");
6254
7338
  } else {
6255
- resolve7("skip");
7339
+ resolve8("skip");
6256
7340
  }
6257
7341
  });
6258
7342
  });
@@ -6366,27 +7450,37 @@ async function showMarpCliInfo(targetDir) {
6366
7450
  async function generateAiConfig(targetDir) {
6367
7451
  await mkdir7(join16(targetDir, ".skills", "slide-assistant", "references"), { recursive: true });
6368
7452
  await mkdir7(join16(targetDir, ".skills", "slide-assistant", "scripts"), { recursive: true });
7453
+ await mkdir7(join16(targetDir, ".claude", "skills", "slide-assistant", "references"), { recursive: true });
6369
7454
  await mkdir7(join16(targetDir, ".claude", "commands"), { recursive: true });
6370
7455
  await mkdir7(join16(targetDir, ".opencode", "agent"), { recursive: true });
6371
- await writeFileIfNotExists(
6372
- join16(targetDir, ".skills", "slide-assistant", "SKILL.md"),
6373
- generateSkillMd()
6374
- );
6375
- await writeFileIfNotExists(
6376
- join16(targetDir, ".skills", "slide-assistant", "references", "templates.md"),
6377
- generateTemplatesRef()
6378
- );
6379
- await writeFileIfNotExists(
6380
- join16(targetDir, ".skills", "slide-assistant", "references", "workflows.md"),
6381
- generateWorkflowsRef()
6382
- );
7456
+ const skillDirs = [
7457
+ join16(targetDir, ".skills", "slide-assistant"),
7458
+ join16(targetDir, ".claude", "skills", "slide-assistant")
7459
+ ];
7460
+ for (const skillDir of skillDirs) {
7461
+ await writeFileIfNotExists(join16(skillDir, "SKILL.md"), generateSkillMd());
7462
+ await writeFileIfNotExists(
7463
+ join16(skillDir, "references", "templates.md"),
7464
+ generateTemplatesRef()
7465
+ );
7466
+ await writeFileIfNotExists(
7467
+ join16(skillDir, "references", "workflows.md"),
7468
+ generateWorkflowsRef()
7469
+ );
7470
+ await writeFileIfNotExists(
7471
+ join16(skillDir, "references", "skill.md"),
7472
+ generateReferenceSkillMd()
7473
+ );
7474
+ }
6383
7475
  await writeFileIfNotExists(join16(targetDir, "CLAUDE.md"), generateClaudeMd());
6384
7476
  const commandGenerators = {
6385
7477
  "slide-create": generateSlideCreateCommand,
6386
7478
  "slide-validate": generateSlideValidateCommand,
6387
7479
  "slide-preview": generateSlidePreviewCommand,
6388
7480
  "slide-screenshot": generateSlideScreenshotCommand,
6389
- "slide-theme": generateSlideThemeCommand
7481
+ "slide-review": generateSlideReviewCommand,
7482
+ "slide-theme": generateSlideThemeCommand,
7483
+ "slide-references": generateSlideReferencesCommand
6390
7484
  };
6391
7485
  for (const [name, generator] of Object.entries(commandGenerators)) {
6392
7486
  await writeFileIfNotExists(
@@ -6497,7 +7591,7 @@ slides:
6497
7591
  // src/cli/commands/watch.ts
6498
7592
  import { Command as Command7 } from "commander";
6499
7593
  import { access as access11 } from "fs/promises";
6500
- import { basename as basename8, dirname as dirname7, join as join17 } from "path";
7594
+ import { basename as basename8, dirname as dirname8, join as join17 } from "path";
6501
7595
  import chalk7 from "chalk";
6502
7596
  import { watch as chokidarWatch2 } from "chokidar";
6503
7597
  var WatchState = class {
@@ -6530,7 +7624,7 @@ var WatchState = class {
6530
7624
  }
6531
7625
  };
6532
7626
  function getDefaultOutputPath2(inputPath) {
6533
- const dir = dirname7(inputPath);
7627
+ const dir = dirname8(inputPath);
6534
7628
  const base = basename8(inputPath, ".yaml");
6535
7629
  return join17(dir, `${base}.md`);
6536
7630
  }
@@ -6592,7 +7686,7 @@ async function executeWatch(inputPath, options) {
6592
7686
  const configLoader = new ConfigLoader();
6593
7687
  let configPath = options.config;
6594
7688
  if (!configPath) {
6595
- configPath = await configLoader.findConfig(dirname7(inputPath));
7689
+ configPath = await configLoader.findConfig(dirname8(inputPath));
6596
7690
  }
6597
7691
  const config = await configLoader.load(configPath);
6598
7692
  const pipeline = new Pipeline(config);
@@ -6661,9 +7755,9 @@ async function executeWatch(inputPath, options) {
6661
7755
  };
6662
7756
  process.on("SIGINT", signalHandler);
6663
7757
  process.on("SIGTERM", signalHandler);
6664
- await new Promise((resolve7) => {
7758
+ await new Promise((resolve8) => {
6665
7759
  if (options.signal) {
6666
- options.signal.addEventListener("abort", () => resolve7());
7760
+ options.signal.addEventListener("abort", () => resolve8());
6667
7761
  }
6668
7762
  });
6669
7763
  return {
@@ -6676,7 +7770,7 @@ async function executeWatch(inputPath, options) {
6676
7770
  // src/cli/commands/images.ts
6677
7771
  import { Command as Command8 } from "commander";
6678
7772
  import { readFile as readFile15, stat as stat2, mkdir as mkdir8 } from "fs/promises";
6679
- import { dirname as dirname8, basename as basename9, join as join18 } from "path";
7773
+ import { dirname as dirname9, basename as basename9, join as join18 } from "path";
6680
7774
  import chalk8 from "chalk";
6681
7775
  import { stringify as stringifyYaml3 } from "yaml";
6682
7776
  function createImagesCommand() {
@@ -6699,7 +7793,7 @@ function createImagesRequestCommand() {
6699
7793
  async function executeImagesStatus(inputPath) {
6700
7794
  try {
6701
7795
  const slides = await loadPresentation(inputPath);
6702
- const baseDir = dirname8(inputPath);
7796
+ const baseDir = dirname9(inputPath);
6703
7797
  const validator = new ImageValidator(baseDir);
6704
7798
  const imageRefs = validator.extractImageReferences(slides);
6705
7799
  if (imageRefs.length === 0) {
@@ -6799,7 +7893,7 @@ async function outputImageStatus(stats, imageRefs, _validator, baseDir) {
6799
7893
  async function executeImagesRequest(inputPath, options) {
6800
7894
  try {
6801
7895
  const slides = await loadPresentation(inputPath);
6802
- const baseDir = dirname8(inputPath);
7896
+ const baseDir = dirname9(inputPath);
6803
7897
  const validator = new ImageValidator(baseDir);
6804
7898
  const missingImages = await validator.getMissingImages(slides);
6805
7899
  if (missingImages.length === 0) {
@@ -6999,7 +8093,7 @@ async function processSingleFile(filePath, options) {
6999
8093
  return;
7000
8094
  }
7001
8095
  const processor = new ImageProcessor();
7002
- const dir = dirname8(filePath);
8096
+ const dir = dirname9(filePath);
7003
8097
  const filename = basename9(filePath);
7004
8098
  const outputDir = join18(dir, options.output);
7005
8099
  await mkdir8(outputDir, { recursive: true });
@@ -7097,10 +8191,11 @@ function parseBlurSpec(spec) {
7097
8191
 
7098
8192
  // src/cli/commands/screenshot.ts
7099
8193
  import { Command as Command9 } from "commander";
7100
- import { access as access12, mkdir as mkdir9, readdir as readdir7, unlink as unlink3 } from "fs/promises";
7101
- import { basename as basename10, dirname as dirname9, join as join19 } from "path";
8194
+ import { access as access12, mkdir as mkdir9, readdir as readdir7, unlink as unlink2 } from "fs/promises";
8195
+ import { basename as basename10, dirname as dirname10, join as join19 } from "path";
7102
8196
  import chalk9 from "chalk";
7103
8197
  import ora4 from "ora";
8198
+ import sharp2 from "sharp";
7104
8199
  async function filterToSpecificSlide(outputDir, baseName, slideNumber, format) {
7105
8200
  const slideStr = slideNumber.toString().padStart(3, "0");
7106
8201
  const targetFileName = `${baseName}.${slideStr}.${format}`;
@@ -7119,7 +8214,7 @@ async function filterToSpecificSlide(outputDir, baseName, slideNumber, format) {
7119
8214
  );
7120
8215
  for (const file of slideFiles) {
7121
8216
  if (file !== targetFileName) {
7122
- await unlink3(join19(outputDir, file));
8217
+ await unlink2(join19(outputDir, file));
7123
8218
  }
7124
8219
  }
7125
8220
  return {
@@ -7130,19 +8225,120 @@ async function filterToSpecificSlide(outputDir, baseName, slideNumber, format) {
7130
8225
  function checkMarpCliAvailable2(projectDir) {
7131
8226
  return isMarpAvailable(projectDir);
7132
8227
  }
8228
+ function estimateTokens(width, height) {
8229
+ return Math.ceil(width * height / 750);
8230
+ }
8231
+ function calculateGridDimensions(slideCount, columns) {
8232
+ const rows = Math.ceil(slideCount / columns);
8233
+ return { rows, columns };
8234
+ }
8235
+ function createNumberOverlay(number, width) {
8236
+ const svg = `
8237
+ <svg width="${width}" height="30">
8238
+ <rect width="${width}" height="30" fill="rgba(0,0,0,0.6)"/>
8239
+ <text x="10" y="22" font-family="system-ui, -apple-system, 'Segoe UI', sans-serif" font-size="16" fill="white">
8240
+ Slide ${number}
8241
+ </text>
8242
+ </svg>
8243
+ `;
8244
+ return Buffer.from(svg);
8245
+ }
8246
+ async function generateContactSheet(slides, options) {
8247
+ const {
8248
+ columns,
8249
+ padding = 10,
8250
+ showNumbers = true,
8251
+ slideWidth = 640,
8252
+ slideHeight = 360
8253
+ } = options;
8254
+ if (slides.length === 0) {
8255
+ return { success: false, error: "No slides provided" };
8256
+ }
8257
+ try {
8258
+ const { rows } = calculateGridDimensions(slides.length, columns);
8259
+ const canvasWidth = columns * slideWidth + (columns + 1) * padding;
8260
+ const canvasHeight = rows * slideHeight + (rows + 1) * padding;
8261
+ const composites = [];
8262
+ for (let i = 0; i < slides.length; i++) {
8263
+ const slide = slides[i];
8264
+ const col = i % columns;
8265
+ const row = Math.floor(i / columns);
8266
+ const x = padding + col * (slideWidth + padding);
8267
+ const y = padding + row * (slideHeight + padding);
8268
+ const resized = await sharp2(slide.path).resize(slideWidth, slideHeight, { fit: "contain", background: { r: 255, g: 255, b: 255 } }).toBuffer();
8269
+ composites.push({
8270
+ input: resized,
8271
+ left: x,
8272
+ top: y
8273
+ });
8274
+ if (showNumbers) {
8275
+ const numberOverlay = createNumberOverlay(slide.index, slideWidth);
8276
+ composites.push({
8277
+ input: numberOverlay,
8278
+ left: x,
8279
+ top: y + slideHeight - 30
8280
+ });
8281
+ }
8282
+ }
8283
+ await sharp2({
8284
+ create: {
8285
+ width: canvasWidth,
8286
+ height: canvasHeight,
8287
+ channels: 4,
8288
+ background: { r: 245, g: 245, b: 245, alpha: 1 }
8289
+ }
8290
+ }).composite(composites).png().toFile(options.outputPath);
8291
+ return {
8292
+ success: true,
8293
+ outputPath: options.outputPath
8294
+ };
8295
+ } catch (error) {
8296
+ return {
8297
+ success: false,
8298
+ error: error instanceof Error ? error.message : "Unknown error"
8299
+ };
8300
+ }
8301
+ }
8302
+ function formatAiOutput(options) {
8303
+ const { files, width, height, outputDir } = options;
8304
+ const tokensPerImage = estimateTokens(width, height);
8305
+ const totalTokens = tokensPerImage * files.length;
8306
+ const imageLabel = files.length === 1 ? "image" : "images";
8307
+ const lines = [
8308
+ "Screenshots saved (AI-optimized):",
8309
+ ""
8310
+ ];
8311
+ for (const file of files) {
8312
+ lines.push(` ${join19(outputDir, file)}`);
8313
+ }
8314
+ lines.push("");
8315
+ lines.push(`Estimated tokens: ~${totalTokens} (${files.length} ${imageLabel})`);
8316
+ if (files.length > 0) {
8317
+ lines.push("");
8318
+ lines.push("To review in Claude Code:");
8319
+ lines.push(` Read ${join19(outputDir, files[0])}`);
8320
+ }
8321
+ return lines.join("\n");
8322
+ }
7133
8323
  function buildMarpCommandArgs(markdownPath, outputDir, options) {
7134
- const format = options.format || "png";
7135
- const args = ["--images", format];
7136
- if (options.width && options.width !== 1280) {
7137
- const scale = options.width / 1280;
8324
+ const isAiFormat = options.format === "ai";
8325
+ const imageFormat = isAiFormat ? "jpeg" : options.format || "png";
8326
+ const width = isAiFormat ? 640 : options.width || 1280;
8327
+ const args = ["--images", imageFormat];
8328
+ if (width !== 1280) {
8329
+ const scale = width / 1280;
7138
8330
  args.push("--image-scale", String(scale));
7139
8331
  }
8332
+ if (imageFormat === "jpeg") {
8333
+ const quality = options.quality || 80;
8334
+ args.push("--jpeg-quality", String(quality));
8335
+ }
7140
8336
  args.push("-o", outputDir);
7141
8337
  args.push(markdownPath);
7142
8338
  return args;
7143
8339
  }
7144
8340
  function createScreenshotCommand() {
7145
- return new Command9("screenshot").description("Take screenshots of slides (requires Marp CLI)").argument("<input>", "Source YAML file").option("-o, --output <path>", "Output directory", "./screenshots").option("-s, --slide <number>", "Specific slide number (1-based)", parseInt).option("-w, --width <pixels>", "Image width", parseInt, 1280).option("-f, --format <fmt>", "Image format (png/jpeg)", "png").option("-c, --config <path>", "Config file path").option("-v, --verbose", "Verbose output").action(async (input, options) => {
8341
+ return new Command9("screenshot").description("Take screenshots of slides (requires Marp CLI)").argument("<input>", "Source YAML file").option("-o, --output <path>", "Output directory", "./screenshots").option("-s, --slide <number>", "Specific slide number (1-based)", parseInt).option("-w, --width <pixels>", "Image width", parseInt, 1280).option("-f, --format <fmt>", "Output format (png/jpeg/ai)", "png").option("-q, --quality <num>", "JPEG quality (1-100)", parseInt, 80).option("--contact-sheet", "Generate contact sheet").option("--columns <num>", "Contact sheet columns", parseInt, 2).option("-c, --config <path>", "Config file path").option("-v, --verbose", "Verbose output").action(async (input, options) => {
7146
8342
  await executeScreenshot(input, options);
7147
8343
  });
7148
8344
  }
@@ -7167,7 +8363,7 @@ async function executeScreenshot(inputPath, options) {
7167
8363
  return { success: false, errors };
7168
8364
  }
7169
8365
  spinner?.start("Checking for Marp CLI...");
7170
- if (!checkMarpCliAvailable2(dirname9(inputPath))) {
8366
+ if (!checkMarpCliAvailable2(dirname10(inputPath))) {
7171
8367
  spinner?.fail("Marp CLI not found");
7172
8368
  const message = "Marp CLI not found. Install it with: npm install -D @marp-team/marp-cli";
7173
8369
  console.error(chalk9.red(`Error: ${message}`));
@@ -7189,7 +8385,7 @@ async function executeScreenshot(inputPath, options) {
7189
8385
  const configLoader = new ConfigLoader();
7190
8386
  let configPath = options.config;
7191
8387
  if (!configPath) {
7192
- configPath = await configLoader.findConfig(dirname9(inputPath));
8388
+ configPath = await configLoader.findConfig(dirname10(inputPath));
7193
8389
  }
7194
8390
  const config = await configLoader.load(configPath);
7195
8391
  spinner?.succeed("Configuration loaded");
@@ -7210,7 +8406,7 @@ async function executeScreenshot(inputPath, options) {
7210
8406
  const tempMdPath = inputPath.replace(/\.ya?ml$/i, ".md");
7211
8407
  const cleanupTempFile = async () => {
7212
8408
  try {
7213
- await unlink3(tempMdPath);
8409
+ await unlink2(tempMdPath);
7214
8410
  } catch {
7215
8411
  }
7216
8412
  };
@@ -7233,7 +8429,7 @@ async function executeScreenshot(inputPath, options) {
7233
8429
  }
7234
8430
  try {
7235
8431
  runMarp(marpArgs, {
7236
- projectDir: dirname9(inputPath),
8432
+ projectDir: dirname10(inputPath),
7237
8433
  stdio: options.verbose ? "inherit" : "pipe"
7238
8434
  });
7239
8435
  spinner?.succeed(`Screenshots saved to ${outputDir}`);
@@ -7246,15 +8442,18 @@ async function executeScreenshot(inputPath, options) {
7246
8442
  await cleanupTempFile();
7247
8443
  return { success: false, errors };
7248
8444
  }
8445
+ const isAiFormat = options.format === "ai";
8446
+ const actualFormat = isAiFormat ? "jpeg" : options.format || "png";
8447
+ let actualWidth = isAiFormat ? 640 : options.width || 1280;
8448
+ let actualHeight = Math.round(actualWidth * 9 / 16);
7249
8449
  if (options.slide !== void 0) {
7250
8450
  spinner?.start(`Filtering to slide ${options.slide}...`);
7251
- const mdBaseName = basename10(tempMdPath, ".md");
7252
- const format = options.format || "png";
8451
+ const mdBaseName2 = basename10(tempMdPath, ".md");
7253
8452
  const filterResult = await filterToSpecificSlide(
7254
8453
  outputDir,
7255
- mdBaseName,
8454
+ mdBaseName2,
7256
8455
  options.slide,
7257
- format
8456
+ actualFormat
7258
8457
  );
7259
8458
  if (!filterResult.success) {
7260
8459
  spinner?.fail("Failed to filter slides");
@@ -7266,20 +8465,71 @@ async function executeScreenshot(inputPath, options) {
7266
8465
  }
7267
8466
  spinner?.succeed(`Kept slide ${options.slide}: ${filterResult.keptFile}`);
7268
8467
  }
8468
+ const allFiles = await readdir7(outputDir);
8469
+ const mdBaseName = basename10(tempMdPath, ".md");
8470
+ const generatedFiles = allFiles.filter((f) => f.startsWith(mdBaseName) && f.endsWith(`.${actualFormat}`)).sort();
8471
+ if (generatedFiles.length > 0) {
8472
+ try {
8473
+ const metadata = await sharp2(join19(outputDir, generatedFiles[0])).metadata();
8474
+ if (metadata.width && metadata.height) {
8475
+ actualWidth = metadata.width;
8476
+ actualHeight = metadata.height;
8477
+ }
8478
+ } catch {
8479
+ }
8480
+ }
8481
+ if (options.contactSheet && generatedFiles.length > 0) {
8482
+ spinner?.start("Generating contact sheet...");
8483
+ const slides = generatedFiles.map((file, index) => ({
8484
+ path: join19(outputDir, file),
8485
+ index: index + 1
8486
+ }));
8487
+ const contactSheetPath = join19(outputDir, `${mdBaseName}-contact.png`);
8488
+ const contactResult = await generateContactSheet(slides, {
8489
+ outputPath: contactSheetPath,
8490
+ columns: options.columns || 2,
8491
+ slideWidth: actualWidth,
8492
+ slideHeight: actualHeight
8493
+ });
8494
+ if (!contactResult.success) {
8495
+ spinner?.fail("Failed to generate contact sheet");
8496
+ console.error(chalk9.red(`Error: ${contactResult.error}`));
8497
+ errors.push(contactResult.error || "Contact sheet generation failed");
8498
+ } else {
8499
+ spinner?.succeed(`Contact sheet saved: ${basename10(contactSheetPath)}`);
8500
+ }
8501
+ }
7269
8502
  await cleanupTempFile();
7270
8503
  console.log("");
7271
- console.log(`Output: ${chalk9.cyan(outputDir)}`);
8504
+ if (isAiFormat && generatedFiles.length > 0) {
8505
+ const output = formatAiOutput({
8506
+ files: generatedFiles,
8507
+ width: actualWidth,
8508
+ height: actualHeight,
8509
+ outputDir
8510
+ });
8511
+ console.log(output);
8512
+ } else if (isAiFormat) {
8513
+ console.log(`Output: ${chalk9.cyan(outputDir)}`);
8514
+ console.log("No screenshots generated");
8515
+ } else {
8516
+ console.log(`Output: ${chalk9.cyan(outputDir)}`);
8517
+ if (generatedFiles.length > 0) {
8518
+ console.log(`Files: ${generatedFiles.length} screenshot(s)`);
8519
+ }
8520
+ }
7272
8521
  return {
7273
8522
  success: true,
7274
8523
  errors,
7275
- outputDir
8524
+ outputDir,
8525
+ files: generatedFiles
7276
8526
  };
7277
8527
  }
7278
8528
 
7279
8529
  // src/cli/commands/sources.ts
7280
8530
  import { Command as Command10 } from "commander";
7281
8531
  import { access as access13, stat as stat3 } from "fs/promises";
7282
- import { resolve as resolve6 } from "path";
8532
+ import { resolve as resolve7 } from "path";
7283
8533
  import chalk10 from "chalk";
7284
8534
  import ora5 from "ora";
7285
8535
  function createSourcesCommand() {
@@ -7328,7 +8578,7 @@ function createSourcesSyncCommand() {
7328
8578
  });
7329
8579
  }
7330
8580
  async function executeSourcesInit(projectDir, options) {
7331
- const resolvedDir = resolve6(projectDir);
8581
+ const resolvedDir = resolve7(projectDir);
7332
8582
  const spinner = ora5("Initializing sources...").start();
7333
8583
  try {
7334
8584
  const manager = new SourcesManager(resolvedDir);
@@ -7344,10 +8594,10 @@ async function executeSourcesInit(projectDir, options) {
7344
8594
  let originalSource;
7345
8595
  if (options.fromDirectory) {
7346
8596
  setupPattern = "A";
7347
- originalSource = resolve6(options.fromDirectory);
8597
+ originalSource = resolve7(options.fromDirectory);
7348
8598
  } else if (options.fromFile) {
7349
8599
  setupPattern = "B";
7350
- originalSource = resolve6(options.fromFile);
8600
+ originalSource = resolve7(options.fromFile);
7351
8601
  }
7352
8602
  await manager.init({
7353
8603
  name: projectName,
@@ -7358,14 +8608,14 @@ async function executeSourcesInit(projectDir, options) {
7358
8608
  if (options.fromDirectory) {
7359
8609
  const importer = new SourceImporter(resolvedDir, manager);
7360
8610
  const result = await importer.importDirectory(
7361
- resolve6(options.fromDirectory),
8611
+ resolve7(options.fromDirectory),
7362
8612
  { recursive: true }
7363
8613
  );
7364
8614
  filesImported = result.imported;
7365
8615
  }
7366
8616
  if (options.fromFile) {
7367
8617
  const importer = new SourceImporter(resolvedDir, manager);
7368
- await importer.importFile(resolve6(options.fromFile), {
8618
+ await importer.importFile(resolve7(options.fromFile), {
7369
8619
  type: "scenario"
7370
8620
  });
7371
8621
  filesImported = 1;
@@ -7390,8 +8640,8 @@ async function executeSourcesInit(projectDir, options) {
7390
8640
  }
7391
8641
  }
7392
8642
  async function executeSourcesImport(projectDir, sourcePath, options) {
7393
- const resolvedDir = resolve6(projectDir);
7394
- const resolvedSource = resolve6(sourcePath);
8643
+ const resolvedDir = resolve7(projectDir);
8644
+ const resolvedSource = resolve7(sourcePath);
7395
8645
  const spinner = ora5("Importing files...").start();
7396
8646
  try {
7397
8647
  const manager = new SourcesManager(resolvedDir);
@@ -7450,7 +8700,7 @@ async function executeSourcesImport(projectDir, sourcePath, options) {
7450
8700
  }
7451
8701
  }
7452
8702
  async function executeSourcesStatus(projectDir, _options) {
7453
- const resolvedDir = resolve6(projectDir);
8703
+ const resolvedDir = resolve7(projectDir);
7454
8704
  try {
7455
8705
  const manager = new SourcesManager(resolvedDir);
7456
8706
  if (!await manager.exists()) {
@@ -7499,6 +8749,26 @@ async function executeSourcesStatus(projectDir, _options) {
7499
8749
  output += "\n";
7500
8750
  }
7501
8751
  }
8752
+ const refs = await manager.getReferences();
8753
+ if (refs.items.length > 0) {
8754
+ output += "\n";
8755
+ output += chalk10.cyan("References:\n");
8756
+ output += ` Required: ${refs.status?.required ?? 0}
8757
+ `;
8758
+ output += ` Found: ${refs.status?.found ?? 0}
8759
+ `;
8760
+ output += ` Pending: ${refs.status?.pending ?? 0}
8761
+ `;
8762
+ const pendingRefs = refs.items.filter((i) => i.status === "pending");
8763
+ if (pendingRefs.length > 0) {
8764
+ output += "\n";
8765
+ output += chalk10.yellow(" \u26A0 Pending references:\n");
8766
+ for (const ref of pendingRefs) {
8767
+ output += ` - ${ref.id} (Slide ${ref.slide}): ${ref.purpose}
8768
+ `;
8769
+ }
8770
+ }
8771
+ }
7502
8772
  if (data.project.updated) {
7503
8773
  output += "\n";
7504
8774
  output += chalk10.gray(`Last updated: ${data.project.updated}
@@ -7517,7 +8787,7 @@ async function executeSourcesStatus(projectDir, _options) {
7517
8787
  }
7518
8788
  }
7519
8789
  async function executeSourcesSync(projectDir, options) {
7520
- const resolvedDir = resolve6(projectDir);
8790
+ const resolvedDir = resolve7(projectDir);
7521
8791
  try {
7522
8792
  const manager = new SourcesManager(resolvedDir);
7523
8793
  if (!await manager.exists()) {