@ncukondo/slide-generation 0.2.5 → 0.3.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,20 +923,20 @@ 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
  }
@@ -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.3.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
  }
@@ -4889,14 +5274,105 @@ var missingItemSchema = z8.object({
4889
5274
  status: z8.string().optional(),
4890
5275
  notes: z8.string().optional()
4891
5276
  });
5277
+ var referenceItemSchema = z8.object({
5278
+ id: z8.string(),
5279
+ status: z8.enum(["pending", "added", "existing"]),
5280
+ slide: z8.number(),
5281
+ purpose: z8.string(),
5282
+ requirement: z8.enum(["required", "recommended"]).optional(),
5283
+ added_date: z8.string().optional(),
5284
+ suggested_search: z8.array(z8.string()).optional(),
5285
+ notes: z8.string().optional()
5286
+ });
5287
+ var referencesStatusSchema = z8.object({
5288
+ required: z8.number().default(0),
5289
+ found: z8.number().default(0),
5290
+ pending: z8.number().default(0)
5291
+ });
5292
+ var referencesSectionSchema = z8.object({
5293
+ status: referencesStatusSchema.optional(),
5294
+ items: z8.array(referenceItemSchema).default([])
5295
+ });
4892
5296
  var sourcesYamlSchema = z8.object({
4893
5297
  project: projectSchema,
4894
5298
  context: contextSchema.optional(),
4895
5299
  sources: z8.array(sourceEntrySchema).optional(),
4896
5300
  dependencies: z8.record(dependencySchema).optional(),
4897
- missing: z8.array(missingItemSchema).optional()
5301
+ missing: z8.array(missingItemSchema).optional(),
5302
+ references: referencesSectionSchema.optional()
4898
5303
  });
4899
5304
 
5305
+ // src/sources/references-tracker.ts
5306
+ var ReferencesTracker = class {
5307
+ items = [];
5308
+ constructor(initial) {
5309
+ if (initial?.items) {
5310
+ this.items = [...initial.items];
5311
+ }
5312
+ }
5313
+ /**
5314
+ * Add a pending reference that needs to be found
5315
+ */
5316
+ addPending(ref) {
5317
+ this.items.push({
5318
+ ...ref,
5319
+ status: "pending"
5320
+ });
5321
+ }
5322
+ /**
5323
+ * Mark a pending reference as added with its actual citation key
5324
+ */
5325
+ markAdded(pendingId, actualId) {
5326
+ const item = this.items.find((i) => i.id === pendingId);
5327
+ if (item) {
5328
+ item.id = actualId;
5329
+ item.status = "added";
5330
+ item.added_date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
5331
+ }
5332
+ }
5333
+ /**
5334
+ * Mark a reference as existing in the bibliography
5335
+ */
5336
+ markExisting(ref) {
5337
+ this.items.push({
5338
+ ...ref,
5339
+ status: "existing"
5340
+ });
5341
+ }
5342
+ /**
5343
+ * Get all reference items
5344
+ */
5345
+ getItems() {
5346
+ return [...this.items];
5347
+ }
5348
+ /**
5349
+ * Get only pending references
5350
+ */
5351
+ getPending() {
5352
+ return this.items.filter((i) => i.status === "pending");
5353
+ }
5354
+ /**
5355
+ * Calculate status summary
5356
+ */
5357
+ getStatus() {
5358
+ const required = this.items.filter(
5359
+ (i) => i.requirement === "required"
5360
+ ).length;
5361
+ const pending = this.items.filter((i) => i.status === "pending").length;
5362
+ const found = this.items.filter((i) => i.status !== "pending").length;
5363
+ return { required, found, pending };
5364
+ }
5365
+ /**
5366
+ * Convert to YAML-compatible object
5367
+ */
5368
+ toYaml() {
5369
+ return {
5370
+ status: this.getStatus(),
5371
+ items: this.items
5372
+ };
5373
+ }
5374
+ };
5375
+
4900
5376
  // src/sources/manager.ts
4901
5377
  var SourcesManager = class _SourcesManager {
4902
5378
  sourcesDir;
@@ -5071,6 +5547,44 @@ var SourcesManager = class _SourcesManager {
5071
5547
  const data = await this.load();
5072
5548
  return data.sources?.filter((s) => s.type === type) ?? [];
5073
5549
  }
5550
+ /**
5551
+ * Get references from sources.yaml
5552
+ */
5553
+ async getReferences() {
5554
+ const data = await this.load();
5555
+ const tracker = new ReferencesTracker(data.references);
5556
+ return tracker.toYaml();
5557
+ }
5558
+ /**
5559
+ * Add a pending reference that needs to be found
5560
+ */
5561
+ async addPendingReference(ref) {
5562
+ const data = await this.load();
5563
+ const tracker = new ReferencesTracker(data.references);
5564
+ tracker.addPending(ref);
5565
+ data.references = tracker.toYaml();
5566
+ await this.save(data);
5567
+ }
5568
+ /**
5569
+ * Mark a pending reference as added with its actual citation key
5570
+ */
5571
+ async markReferenceAdded(pendingId, actualId) {
5572
+ const data = await this.load();
5573
+ const tracker = new ReferencesTracker(data.references);
5574
+ tracker.markAdded(pendingId, actualId);
5575
+ data.references = tracker.toYaml();
5576
+ await this.save(data);
5577
+ }
5578
+ /**
5579
+ * Mark a reference as existing in the bibliography
5580
+ */
5581
+ async markReferenceExisting(ref) {
5582
+ const data = await this.load();
5583
+ const tracker = new ReferencesTracker(data.references);
5584
+ tracker.markExisting(ref);
5585
+ data.references = tracker.toYaml();
5586
+ await this.save(data);
5587
+ }
5074
5588
  };
5075
5589
 
5076
5590
  // src/sources/importer.ts
@@ -5467,6 +5981,51 @@ allowed-tools: Read Write Edit Bash Glob Grep
5467
5981
 
5468
5982
  Helps create Marp slides using the slide-gen CLI tool.
5469
5983
 
5984
+ ## First Question
5985
+
5986
+ **Always start by asking about the user's material situation:**
5987
+
5988
+ > "Let's create slides. What materials do you have?
5989
+ >
5990
+ > A) I have detailed materials organized in a directory
5991
+ > (scenarios, scripts, data files, images, etc.)
5992
+ >
5993
+ > B) I have partial materials like a scenario or script
5994
+ > (but need to supplement with additional info)
5995
+ >
5996
+ > C) I don't have materials yet
5997
+ > (starting from scratch, will collect info through dialogue)"
5998
+
5999
+ Then follow the appropriate pattern below.
6000
+
6001
+ ## Workflow Patterns
6002
+
6003
+ ### Pattern A: Explore Mode (Detailed Materials Exist)
6004
+
6005
+ When user has materials organized in a directory:
6006
+ 1. Ask for directory path and scan with Glob
6007
+ 2. Read and classify files (scenario, scripts, data, images)
6008
+ 3. Summarize findings and confirm with user
6009
+ 4. Configure \`sources/\` directory
6010
+
6011
+ ### Pattern B: Supplement Mode (Partial Materials)
6012
+
6013
+ When user has only a scenario or partial materials:
6014
+ 1. Read and analyze provided content
6015
+ 2. Identify what's present vs. missing
6016
+ 3. Ask targeted questions to fill gaps
6017
+ 4. Configure \`sources/\` directory
6018
+
6019
+ ### Pattern C: Interview Mode (Starting from Scratch)
6020
+
6021
+ When user has no materials:
6022
+ 1. Ask basic questions (purpose, audience, duration)
6023
+ 2. Collect data and examples
6024
+ 3. Propose slide structure for approval
6025
+ 4. Configure \`sources/\` directory
6026
+
6027
+ **See [references/workflows.md](references/workflows.md) for detailed steps.**
6028
+
5470
6029
  ## Capabilities
5471
6030
 
5472
6031
  1. **Project initialization**: \`slide-gen init\`
@@ -5476,21 +6035,14 @@ Helps create Marp slides using the slide-gen CLI tool.
5476
6035
  5. **Conversion**: \`slide-gen convert\`
5477
6036
  6. **Screenshot**: \`slide-gen screenshot\` (for AI review)
5478
6037
 
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
6038
+ ## Slide Creation Flow
5488
6039
 
5489
- ### Existing Project
6040
+ After material collection (Pattern A/B/C above):
5490
6041
 
5491
- 1. Read presentation.yaml
5492
- 2. Edit according to user request
5493
- 3. Validate and convert
6042
+ 1. Check templates with \`slide-gen templates list --format llm\`
6043
+ 2. Create presentation.yaml
6044
+ 3. Validate: \`slide-gen validate presentation.yaml\`
6045
+ 4. Convert: \`slide-gen convert presentation.yaml\`
5494
6046
 
5495
6047
  ## YAML Source Format
5496
6048
 
@@ -5573,6 +6125,33 @@ The \`validate --format llm\` command provides:
5573
6125
  - Error locations with line numbers
5574
6126
  - Fix examples from template definitions
5575
6127
  - Contextual hints for unknown templates/icons
6128
+
6129
+ ## Reference Management
6130
+
6131
+ For academic presentations, manage citations and references:
6132
+
6133
+ 1. **Analyze** content for citation needs
6134
+ 2. **Search** existing library: \`ref search\`
6135
+ 3. **Add** new references: \`ref add pmid:XXX\`
6136
+ 4. **Validate** citations: \`slide-gen validate\`
6137
+
6138
+ See \`.skills/slide-assistant/references/skill.md\` for detailed workflow.
6139
+
6140
+ ### Quick Commands
6141
+
6142
+ \`\`\`bash
6143
+ # Search library
6144
+ ref search "keyword" --format json
6145
+
6146
+ # Add from PMID
6147
+ ref add pmid:38941256
6148
+
6149
+ # Add from DOI
6150
+ ref add "10.1038/xxxxx"
6151
+
6152
+ # Validate citations
6153
+ slide-gen validate presentation.yaml
6154
+ \`\`\`
5576
6155
  `;
5577
6156
  }
5578
6157
 
@@ -5835,31 +6414,72 @@ Image with text side by side.
5835
6414
  function generateWorkflowsRef() {
5836
6415
  return `# Workflow Reference
5837
6416
 
6417
+ ## Entry Point
6418
+
6419
+ **Always start by asking this question:**
6420
+
6421
+ > "Let's create slides. What materials do you have?
6422
+ >
6423
+ > A) I have detailed materials organized in a directory
6424
+ > (scenarios, scripts, data files, images, etc.)
6425
+ >
6426
+ > B) I have partial materials like a scenario or script
6427
+ > (but need to supplement with additional info)
6428
+ >
6429
+ > C) I don't have materials yet
6430
+ > (starting from scratch, will collect info through dialogue)"
6431
+
6432
+ Based on the answer, follow the appropriate pattern below.
6433
+
5838
6434
  ## Source Collection Flow
5839
6435
 
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
6436
+ ### Pattern A: Explore Mode (Detailed Materials Exist)
6437
+
6438
+ When user has materials organized in a directory:
6439
+
6440
+ 1. **Ask for the directory path**
6441
+ 2. **Scan directory structure** with Glob tool
6442
+ 3. **Read and analyze each file**
6443
+ 4. **Classify files** into categories:
6444
+ - Scenario/scripts
6445
+ - Data files (CSV, JSON, etc.)
6446
+ - Images and diagrams
6447
+ - Reference documents
6448
+ 5. **Summarize findings** and confirm with user
6449
+ 6. **Ask clarifying questions** about gaps
6450
+ 7. **Configure \`sources/\` directory** with organized materials
6451
+ 8. Proceed to slide creation
6452
+
6453
+ ### Pattern B: Supplement Mode (Partial Materials)
6454
+
6455
+ When user has only a scenario or partial materials:
6456
+
6457
+ 1. **Ask user** to provide the file path or paste content
6458
+ 2. **Analyze the content** thoroughly
6459
+ 3. **Identify what information is present** vs. missing
6460
+ 4. **Ask targeted questions** to fill gaps:
6461
+ - Purpose and audience
6462
+ - Duration and format
6463
+ - Key messages
6464
+ - Available data/examples
6465
+ 5. **Load any additional files** user mentions
6466
+ 6. **Configure \`sources/\` directory**
6467
+ 7. Proceed to slide creation
6468
+
6469
+ ### Pattern C: Interview Mode (Starting from Scratch)
6470
+
5857
6471
  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
6472
+
6473
+ 1. **Ask basic questions**:
6474
+ - "What is this presentation about?"
6475
+ - "Who is the audience?"
6476
+ - "How long is the presentation?"
6477
+ 2. **Ask purpose-specific questions** (proposal, report, introduction, etc.)
6478
+ 3. **Collect data and examples** user can provide
6479
+ 4. **Propose slide structure** for approval
6480
+ 5. **Incorporate feedback**
6481
+ 6. **Configure \`sources/\` directory** from conversation
6482
+ 7. Proceed to slide creation
5863
6483
 
5864
6484
  ## Slide Creation Flow
5865
6485
 
@@ -5911,6 +6531,158 @@ Handle adjustments (cropping, replacement) as needed.
5911
6531
  `;
5912
6532
  }
5913
6533
 
6534
+ // src/cli/templates/ai/references/skill-references.ts
6535
+ function generateReferenceSkillMd() {
6536
+ return `## Reference Management Skill
6537
+
6538
+ ### When to Invoke
6539
+
6540
+ Use this skill when:
6541
+ - Creating academic or research presentations
6542
+ - User mentions needing citations or references
6543
+ - Scenario contains statistical claims or research findings
6544
+ - User provides literature (URL, PDF, DOI, PMID)
6545
+
6546
+ ### Citation Requirement Analysis
6547
+
6548
+ Analyze scenario/script for statements requiring citations:
6549
+
6550
+ | Statement Type | Requirement | Example |
6551
+ |---------------|-------------|---------|
6552
+ | Statistical claims | Required | "Accuracy exceeds 90%" |
6553
+ | Research findings | Required | "Studies show that..." |
6554
+ | Methodology references | Required | "Using the XYZ method" |
6555
+ | Comparative claims | Recommended | "Better than conventional" |
6556
+ | Historical facts | Recommended | "First introduced in 2020" |
6557
+ | General knowledge | Not required | "AI is widely used" |
6558
+
6559
+ ### Workflow
6560
+
6561
+ #### Phase 1: Analyze Content
6562
+ 1. Read scenario/script thoroughly
6563
+ 2. Identify statements requiring citations
6564
+ 3. Categorize as Required or Recommended
6565
+ 4. Note the slide number and exact statement
6566
+
6567
+ #### Phase 2: Search Existing Library
6568
+ \`\`\`bash
6569
+ # List all references
6570
+ ref list --format json
6571
+
6572
+ # Search by keyword
6573
+ ref search "diagnostic accuracy" --format json
6574
+ \`\`\`
6575
+
6576
+ #### Phase 3: Match or Request
6577
+
6578
+ **If found in library:**
6579
+ - Confirm relevance with user
6580
+ - Insert \`[@id]\` citation in YAML
6581
+
6582
+ **If not found:**
6583
+ - Present clear request to user
6584
+ - Specify what type of source is needed
6585
+ - Provide suggested search terms
6586
+
6587
+ #### Phase 4: Add New References
6588
+
6589
+ From user-provided input:
6590
+
6591
+ \`\`\`bash
6592
+ # From PMID
6593
+ ref add pmid:38941256
6594
+
6595
+ # From DOI
6596
+ ref add "10.1038/s41591-024-xxxxx"
6597
+
6598
+ # From ISBN
6599
+ ref add "ISBN:978-4-00-000000-0"
6600
+
6601
+ # From BibTeX file
6602
+ ref add paper.bib
6603
+ \`\`\`
6604
+
6605
+ #### Phase 5: Insert Citations
6606
+
6607
+ Update presentation.yaml:
6608
+ \`\`\`yaml
6609
+ items:
6610
+ - "This claim is supported [@smith2024]"
6611
+ \`\`\`
6612
+
6613
+ ### Extracting from Non-Standard Input
6614
+
6615
+ #### URL Patterns
6616
+ - PubMed: Extract PMID from \`pubmed.ncbi.nlm.nih.gov/XXXXXXXX\`
6617
+ - DOI: Extract from \`doi.org/10.XXXX/XXXXX\`
6618
+ - Publisher sites: Fetch page, extract DOI from metadata
6619
+
6620
+ #### PDF Files
6621
+ 1. Read PDF file
6622
+ 2. Extract DOI from first page or metadata
6623
+ 3. If not found, extract title and search databases
6624
+
6625
+ #### Free Text
6626
+ 1. Parse author, year, journal information
6627
+ 2. Search PubMed/CrossRef
6628
+ 3. Present candidates for user confirmation
6629
+
6630
+ ### User Communication Templates
6631
+
6632
+ **Analyzing content:**
6633
+ \`\`\`
6634
+ I've analyzed your scenario and identified citation needs:
6635
+
6636
+ Required Citations (N)
6637
+ ----------------------
6638
+ 1. Slide X: '[statement]'
6639
+ -> Needs: [type of source]
6640
+
6641
+ Recommended Citations (M)
6642
+ -------------------------
6643
+ ...
6644
+
6645
+ Let me check your reference library...
6646
+ \`\`\`
6647
+
6648
+ **Requesting references:**
6649
+ \`\`\`
6650
+ I need your help finding references:
6651
+
6652
+ [REQUIRED] Reference 1: [Topic]
6653
+ -------------------------------
6654
+ Purpose: Support claim '[statement]' on Slide X
6655
+
6656
+ Ideal source type:
6657
+ - [type1]
6658
+ - [type2]
6659
+
6660
+ Suggested search terms:
6661
+ - [term1]
6662
+ - [term2]
6663
+
6664
+ How to provide:
6665
+ A) DOI or PMID (e.g., 'PMID: 38941256')
6666
+ B) URL (PubMed, journal site, etc.)
6667
+ C) PDF file
6668
+ D) Manual citation details
6669
+ \`\`\`
6670
+
6671
+ **Confirming addition:**
6672
+ \`\`\`
6673
+ Reference added successfully:
6674
+ -----------------------------
6675
+ Citation key: [@id]
6676
+ Authors: ...
6677
+ Title: '...'
6678
+ Journal: ...
6679
+ Year: XXXX
6680
+
6681
+ I'll use this for Slide X.
6682
+ \`\`\`
6683
+ `;
6684
+ }
6685
+
5914
6686
  // src/cli/templates/ai/commands/slide-create.ts
5915
6687
  function generateSlideCreateCommand() {
5916
6688
  return `Create slides from user requirements.
@@ -6070,6 +6842,72 @@ function generateSlideThemeCommand() {
6070
6842
  `;
6071
6843
  }
6072
6844
 
6845
+ // src/cli/templates/ai/commands/slide-references.ts
6846
+ function generateSlideReferencesCommand() {
6847
+ return `Manage references and citations for the presentation.
6848
+
6849
+ ## Available Actions
6850
+
6851
+ ### 1. Analyze - Find citation needs
6852
+ Analyze the scenario/content for statements that need citations.
6853
+
6854
+ ### 2. Search - Find in library
6855
+ Search existing reference-manager library for relevant papers.
6856
+
6857
+ ### 3. Add - Add new reference
6858
+ Add a reference from PMID, DOI, URL, or file.
6859
+
6860
+ ### 4. List - Show all references
6861
+ List all references currently in the library.
6862
+
6863
+ ## Usage
6864
+
6865
+ ### Analyze scenario for citation needs:
6866
+ \`\`\`bash
6867
+ # First, read the presentation
6868
+ cat presentation.yaml
6869
+
6870
+ # Then analyze content for citation requirements
6871
+ \`\`\`
6872
+ Report statements needing citations with slide numbers.
6873
+
6874
+ ### Search library:
6875
+ \`\`\`bash
6876
+ ref search "keyword" --format json
6877
+ ref list --format json
6878
+ \`\`\`
6879
+
6880
+ ### Add reference:
6881
+ \`\`\`bash
6882
+ # From PMID
6883
+ ref add pmid:38941256
6884
+
6885
+ # From DOI
6886
+ ref add "10.1038/s41591-024-xxxxx"
6887
+
6888
+ # From URL (extract identifier first)
6889
+ # PubMed URL -> extract PMID
6890
+ # DOI URL -> extract DOI
6891
+
6892
+ # From file
6893
+ ref add paper.bib
6894
+ ref add export.ris
6895
+ \`\`\`
6896
+
6897
+ ### Validate citations:
6898
+ \`\`\`bash
6899
+ slide-gen validate presentation.yaml
6900
+ \`\`\`
6901
+
6902
+ ## Notes
6903
+
6904
+ - Always check library before requesting new references
6905
+ - Extract PMID/DOI from URLs before adding
6906
+ - Report missing citations with suggested search terms
6907
+ - Update presentation.yaml with [@id] format
6908
+ `;
6909
+ }
6910
+
6073
6911
  // src/cli/commands/init.ts
6074
6912
  function getPackageRoot() {
6075
6913
  const __dirname = dirname6(fileURLToPath(import.meta.url));
@@ -6380,13 +7218,18 @@ async function generateAiConfig(targetDir) {
6380
7218
  join16(targetDir, ".skills", "slide-assistant", "references", "workflows.md"),
6381
7219
  generateWorkflowsRef()
6382
7220
  );
7221
+ await writeFileIfNotExists(
7222
+ join16(targetDir, ".skills", "slide-assistant", "references", "skill.md"),
7223
+ generateReferenceSkillMd()
7224
+ );
6383
7225
  await writeFileIfNotExists(join16(targetDir, "CLAUDE.md"), generateClaudeMd());
6384
7226
  const commandGenerators = {
6385
7227
  "slide-create": generateSlideCreateCommand,
6386
7228
  "slide-validate": generateSlideValidateCommand,
6387
7229
  "slide-preview": generateSlidePreviewCommand,
6388
7230
  "slide-screenshot": generateSlideScreenshotCommand,
6389
- "slide-theme": generateSlideThemeCommand
7231
+ "slide-theme": generateSlideThemeCommand,
7232
+ "slide-references": generateSlideReferencesCommand
6390
7233
  };
6391
7234
  for (const [name, generator] of Object.entries(commandGenerators)) {
6392
7235
  await writeFileIfNotExists(
@@ -7499,6 +8342,26 @@ async function executeSourcesStatus(projectDir, _options) {
7499
8342
  output += "\n";
7500
8343
  }
7501
8344
  }
8345
+ const refs = await manager.getReferences();
8346
+ if (refs.items.length > 0) {
8347
+ output += "\n";
8348
+ output += chalk10.cyan("References:\n");
8349
+ output += ` Required: ${refs.status?.required ?? 0}
8350
+ `;
8351
+ output += ` Found: ${refs.status?.found ?? 0}
8352
+ `;
8353
+ output += ` Pending: ${refs.status?.pending ?? 0}
8354
+ `;
8355
+ const pendingRefs = refs.items.filter((i) => i.status === "pending");
8356
+ if (pendingRefs.length > 0) {
8357
+ output += "\n";
8358
+ output += chalk10.yellow(" \u26A0 Pending references:\n");
8359
+ for (const ref of pendingRefs) {
8360
+ output += ` - ${ref.id} (Slide ${ref.slide}): ${ref.purpose}
8361
+ `;
8362
+ }
8363
+ }
8364
+ }
7502
8365
  if (data.project.updated) {
7503
8366
  output += "\n";
7504
8367
  output += chalk10.gray(`Last updated: ${data.project.updated}