@llm-newsletter-kit/core 1.1.5 → 1.2.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/README.md CHANGED
@@ -168,6 +168,49 @@ This kit prioritizes flexibility over rigid tooling. Instead of locking you into
168
168
  - **Asynchronous Injection:** Parsing logic is injected asynchronously, allowing you to integrate third-party APIs or AI-based parsers effortlessly.
169
169
  - **Recommendation:** While the kit supports LLM-based parsing (HTML-to-JSON), we generally recommend **rule-based parsing** (e.g., CSS selectors) for production environments to ensure speed, cost-efficiency, and stability.
170
170
 
171
+ ## Playground
172
+
173
+ Playground scripts let you run individual LLM query classes in isolation — no full pipeline needed. Useful for prompt tuning, testing new options, or debugging output quality.
174
+
175
+ ### Setup
176
+
177
+ 1. Install playground dependencies:
178
+ ```bash
179
+ npm install -D tsx @ai-sdk/openai
180
+ ```
181
+
182
+ 2. Copy example data files and customize:
183
+ ```bash
184
+ mkdir -p playground/data
185
+ cp playground/data-examples/config.example.json playground/data/config.json
186
+ cp playground/data-examples/articles.example.json playground/data/articles.json
187
+ cp playground/data-examples/template.example.html playground/data/template.html
188
+ ```
189
+
190
+ 3. Edit `playground/data/config.json` with your OpenAI API key and options.
191
+ 4. Edit `playground/data/articles.json` with your target articles.
192
+ 5. (Optional) Replace `playground/data/template.html` with your actual email template.
193
+
194
+ ### Run
195
+
196
+ ```bash
197
+ npm run playground:generate-newsletter
198
+ ```
199
+
200
+ ### Output
201
+
202
+ Results are saved to `playground/output/` (git-ignored):
203
+ - `newsletter.md` — Generated markdown with title in frontmatter
204
+ - `newsletter.html` — Rendered HTML with CSS inlined (juice)
205
+
206
+ ### Data Management
207
+
208
+ | Directory | Git | Purpose |
209
+ |---|---|---|
210
+ | `playground/data-examples/` | Tracked | Format reference files (`.example.*`) |
211
+ | `playground/data/` | Ignored | Your actual config, articles, templates |
212
+ | `playground/output/` | Ignored | Generated results |
213
+
171
214
  ## Development / Build / Test / CI
172
215
 
173
216
  For the full developer guide (environment, scripts, testing/coverage, and CI), see [CONTRIBUTING.md](./CONTRIBUTING.md).
package/dist/index.cjs CHANGED
@@ -4,10 +4,9 @@ var runnables = require('@langchain/core/runnables');
4
4
  var esToolkit = require('es-toolkit');
5
5
  var ai = require('ai');
6
6
  var zod = require('zod');
7
- var juice = require('juice');
8
- var DOMPurify = require('dompurify');
9
7
  var jsdom = require('jsdom');
10
- var marked = require('marked');
8
+ var juice = require('juice');
9
+ var safeMarkdown2Html = require('safe-markdown2html');
11
10
  var node_crypto = require('node:crypto');
12
11
 
13
12
  /**
@@ -120,6 +119,17 @@ class LLMQuery extends BaseLLMQuery {
120
119
  }
121
120
  }
122
121
 
122
+ const ZERO_USAGE = {
123
+ inputTokens: undefined,
124
+ inputTokenDetails: {
125
+ noCacheTokens: undefined,
126
+ cacheReadTokens: undefined,
127
+ cacheWriteTokens: undefined,
128
+ },
129
+ outputTokens: undefined,
130
+ outputTokenDetails: { textTokens: undefined, reasoningTokens: undefined },
131
+ totalTokens: undefined,
132
+ };
123
133
  class AnalyzeImages extends LLMQuery {
124
134
  schema = zod.z.object({
125
135
  imageContext: zod.z
@@ -132,12 +142,12 @@ class AnalyzeImages extends LLMQuery {
132
142
  async execute() {
133
143
  if (!this.targetArticle.hasAttachedImage ||
134
144
  !this.targetArticle.detailContent) {
135
- return null;
145
+ return { result: null, usage: ZERO_USAGE };
136
146
  }
137
147
  if (this.imageMessages.length === 0) {
138
- return null;
148
+ return { result: null, usage: ZERO_USAGE };
139
149
  }
140
- const { output } = await ai.generateText({
150
+ const { output, usage } = await ai.generateText({
141
151
  model: this.model,
142
152
  maxRetries: this.options.llm.maxRetries,
143
153
  output: ai.Output.object({
@@ -151,7 +161,7 @@ class AnalyzeImages extends LLMQuery {
151
161
  },
152
162
  ],
153
163
  });
154
- return output.imageContext;
164
+ return { result: output.imageContext, usage };
155
165
  }
156
166
  get systemPrompt() {
157
167
  return `# Image Analysis Expert System
@@ -258,7 +268,7 @@ class ClassifyTags extends LLMQuery {
258
268
  }
259
269
  async execute({ existTags }) {
260
270
  this.existTags = existTags;
261
- const { output } = await ai.generateText({
271
+ const { output, usage } = await ai.generateText({
262
272
  model: this.model,
263
273
  maxRetries: this.options.llm.maxRetries,
264
274
  output: ai.Output.object({
@@ -267,7 +277,7 @@ class ClassifyTags extends LLMQuery {
267
277
  system: this.systemPrompt,
268
278
  prompt: this.userPrompt,
269
279
  });
270
- return output;
280
+ return { result: output, usage };
271
281
  }
272
282
  get systemPrompt() {
273
283
  return `You are an AI specializing in analyzing and categorizing articles for professionals in ${this.expertFields.join(', ')}.
@@ -340,7 +350,7 @@ class DetermineArticleImportance extends LLMQuery {
340
350
  this.dateService = config.dateService;
341
351
  }
342
352
  async execute() {
343
- const { output } = await ai.generateText({
353
+ const { output, usage } = await ai.generateText({
344
354
  model: this.model,
345
355
  maxRetries: this.options.llm.maxRetries,
346
356
  output: ai.Output.object({
@@ -349,7 +359,7 @@ class DetermineArticleImportance extends LLMQuery {
349
359
  system: this.systemPrompt,
350
360
  prompt: this.userPrompt,
351
361
  });
352
- return output.importanceScore;
362
+ return { result: output.importanceScore, usage };
353
363
  }
354
364
  get minPoint() {
355
365
  const targetRule = this.minimumImportanceScoreRules.find(({ targetUrl }) => targetUrl === this.targetArticle.targetUrl);
@@ -523,7 +533,9 @@ class ArticleInsightsChain extends PrivateChain {
523
533
  });
524
534
  try {
525
535
  const classifyTags = new ClassifyTags(this.getLlmQueryConfig(this.provider.classifyTagOptions.model, article));
526
- const generatedTags = await classifyTags.execute({ existTags });
536
+ const { result: generatedTags } = await classifyTags.execute({
537
+ existTags,
538
+ });
527
539
  pushTag(generatedTags.tag1);
528
540
  pushTag(generatedTags.tag2);
529
541
  pushTag(generatedTags.tag3);
@@ -596,7 +608,7 @@ class ArticleInsightsChain extends PrivateChain {
596
608
  });
597
609
  try {
598
610
  const analyzeImages = new AnalyzeImages(this.getLlmQueryConfig(this.provider.analyzeImagesOptions.model, article));
599
- const imageContextByLlm = await analyzeImages.execute();
611
+ const { result: imageContextByLlm } = await analyzeImages.execute();
600
612
  if (imageContextByLlm) {
601
613
  articlesWithImageContext.push({
602
614
  id: article.id,
@@ -695,7 +707,7 @@ class ArticleInsightsChain extends PrivateChain {
695
707
  minimumImportanceScoreRules: this.provider.determineScoreOptions.minimumImportanceScoreRules,
696
708
  dateService: this.dateService,
697
709
  });
698
- const importanceScore = await determineArticleImportance.execute();
710
+ const { result: importanceScore } = await determineArticleImportance.execute();
699
711
  const processedArticle = {
700
712
  ...article,
701
713
  importanceScore,
@@ -838,61 +850,6 @@ class AnalysisChain extends Chain {
838
850
  }
839
851
  }
840
852
 
841
- function markdownToHtml(markdown) {
842
- const html = marked.marked.parse(markdown);
843
- const window = new jsdom.JSDOM('').window;
844
- const purify = DOMPurify(window);
845
- const sanitized = purify.sanitize(html);
846
- const withCorrectedUrls = correctMalformedUrls(sanitized);
847
- const withTargetBlank = addTargetBlankToAnchors(withCorrectedUrls);
848
- const withDelReplaced = replaceDelTagsWithTilde(withTargetBlank);
849
- return correctUnconvertedBoldSyntax(withDelReplaced);
850
- }
851
- function addTargetBlankToAnchors(htmlString) {
852
- // Regular expression to find '<a>' tags
853
- // This regex matches '<a>' tags that contain 'href' attribute and optionally other attributes
854
- // Excludes 'target="[^"]*"' to check if target attribute already exists
855
- const regex = /<a(\s+[^>]*?)?(?<!target="[^"]*")>/gi;
856
- // Use regex to find '<a>' tags and add 'target="_blank"'
857
- return htmlString.replace(regex, (_match, attributes) => {
858
- // Handle undefined attributes as empty string
859
- const currentAttributes = attributes || '';
860
- // Double check if target attribute exists (safety check for regex limitations)
861
- if (currentAttributes.includes('target=')) {
862
- return `<a${currentAttributes}>`; // If target attribute exists, return without modification
863
- }
864
- else {
865
- // Add target="_blank" attribute
866
- return `<a${currentAttributes} target="_blank">`;
867
- }
868
- });
869
- }
870
- function replaceDelTagsWithTilde(htmlString) {
871
- // Replace opening and closing del tags with tilde (~)
872
- return htmlString.replace(/<del>/gi, '~').replace(/<\/del>/gi, '~');
873
- }
874
- function correctUnconvertedBoldSyntax(htmlString) {
875
- // Replace unconverted "**text**" markdown syntax with <b> tags
876
- // Matches "**" followed by one or more non-asterisk characters, followed by "**"
877
- return htmlString.replace(/\*\*([^*]+)\*\*/g, '<b>$1</b>');
878
- }
879
- function correctMalformedUrls(htmlString) {
880
- // Pattern matches anchors with `)` followed by URL-encoded characters
881
- // Capture groups:
882
- // 1: attributes before href
883
- // 2: URL base (before `)`)
884
- // 3: URL-encoded part (starts with %)
885
- // 4: attributes after href
886
- // 5: link text base (before `)`)
887
- // 6: decoded text after `)` in link text
888
- const regex = /<a\s+([^>]*?)href="([^"]*?)\)(%[0-9A-Fa-f]{2}[^"]*?)"([^>]*?)>([^<]*?)\)([^<]*?)<\/a>/g;
889
- return htmlString.replace(regex, (_match, beforeHref, urlBase, _encodedPart, afterHref, textBase, decodedTextInLink) => {
890
- // The decoded text is already in the link text (decodedTextInLink)
891
- // We just need to move it outside the anchor along with the `)`
892
- return `<a ${beforeHref}href="${urlBase}"${afterHref}>${textBase}</a>)${decodedTextInLink}`;
893
- });
894
- }
895
-
896
853
  let GenerateNewsletter$1 = class GenerateNewsletter extends BaseLLMQuery {
897
854
  maxOutputTokens;
898
855
  temperature;
@@ -935,7 +892,7 @@ let GenerateNewsletter$1 = class GenerateNewsletter extends BaseLLMQuery {
935
892
  this.newsletterBrandName = config.newsletterBrandName;
936
893
  }
937
894
  async execute() {
938
- const { output } = await ai.generateText({
895
+ const { output, usage } = await ai.generateText({
939
896
  model: this.model,
940
897
  maxRetries: this.options.llm.maxRetries,
941
898
  maxOutputTokens: this.maxOutputTokens,
@@ -950,16 +907,19 @@ let GenerateNewsletter$1 = class GenerateNewsletter extends BaseLLMQuery {
950
907
  system: this.systemPrompt,
951
908
  prompt: this.userPrompt,
952
909
  });
953
- if (!output.isWrittenInOutputLanguage) {
954
- return this.execute();
955
- }
956
- if (!output.copyrightVerified) {
957
- return this.execute();
958
- }
959
- if (!output.factAccuracy) {
960
- return this.execute();
910
+ const needsRetry = !output.isWrittenInOutputLanguage ||
911
+ !output.copyrightVerified ||
912
+ !output.factAccuracy ||
913
+ (this.options.content.titleContext &&
914
+ !output.title.includes(this.options.content.titleContext));
915
+ if (needsRetry) {
916
+ const retryResult = await this.execute();
917
+ return {
918
+ result: retryResult.result,
919
+ usage: addUsage(usage, retryResult.usage),
920
+ };
961
921
  }
962
- return esToolkit.pick(output, ['title', 'content']);
922
+ return { result: esToolkit.pick(output, ['title', 'content']), usage };
963
923
  }
964
924
  get systemPrompt() {
965
925
  return `You are a newsletter production expert for "${this.newsletterBrandName}" who analyzes and delivers trends in the fields of ${this.expertFields.join(', ')}. Your goal is to provide in-depth analysis that helps industry professionals easily understand complex information and make informed decisions.
@@ -1013,14 +973,14 @@ Copyright Protection & Fact-Checking Principles:
1013
973
  Output Format & Requirements:
1014
974
  1. Language: ${this.options.content.outputLanguage}
1015
975
 
1016
- 2. Start: Specify date (${this.dateService.getDisplayDateString()}) and begin with neutral, objective greeting. Briefly introduce key factual information to be covered in today's newsletter.
976
+ 2. Start: ${this.options.content.freeFormIntro ? 'Begin directly with the Overall Briefing section (no separate opening heading or greeting).' : `Specify date (${this.dateService.getDisplayDateString()}) and begin with neutral, objective greeting. Briefly introduce key factual information to be covered in today's newsletter.`}
1017
977
 
1018
- 3. Overall Briefing: Before the main listing, create a briefing section conveying objective facts about today's news in these aspects:
978
+ 3. Overall Briefing: Before the main listing, create a briefing section conveying objective facts about today's news${this.options.content.freeFormIntro ? `. Structure: Start with a Heading 2 (##) briefing section heading in the format "## 📮 ${this.dateService.getDisplayDateString()} [Briefing/Summary word in output language]" (e.g., "## 📮 2월 6일 브리핑" for Korean, "## 📮 Feb 6 Briefing" for English) — do NOT include domain or field names in the heading. Immediately follow with a brief paragraph introducing key factual information to be covered in today's newsletter, then include the following bullet points:` : ' in these aspects:'}
1019
979
  - Key Trends: Explain major patterns or trends found in this news based on data. Ex: 'Over 00% of today's news relates to 00'.
1020
980
  - Immediate Impact: Emphasize most important changes or decisions affecting industry immediately, specifically mentioning which fields will be most impacted.
1021
981
 
1022
982
  4. Category Classification & Content Organization:
1023
- - Group news by logical categories based on related tags and content (e.g., Policy/Regulation, Budget/Support, Research/Development, Products/Services, Operations/Process, Recruitment/Events) rather than just listing by importance.
983
+ - Group news by logical categories based on related tags and content (e.g., Policy/Regulation, Budget/Support, Research/Development, Products/Services, Operations/Process, Recruitment/Events) rather than just listing by importance.${this.options.content.freeFormIntro ? '\n - Use Heading 2 (##) for each category heading (same level as the briefing heading). Do NOT use Heading 3 (###) for categories.' : ''}
1024
984
  - Use appropriate emoticons for each category for visual distinction.
1025
985
  - Sort by importance within categories, making high-importance items more prominent.
1026
986
  - Add short paragraph at category start summarizing overall trends or changes in that area, specifying important points and areas to focus on.
@@ -1060,18 +1020,22 @@ Output Format & Requirements:
1060
1020
  - Do not write preview or anticipatory messages about next newsletter.
1061
1021
  - Do not include contact information for inquiries.
1062
1022
 
1063
- 7. Title Writing Guidelines:
1023
+ 7. Title Writing Guidelines:${this.options.content.titleContext
1024
+ ? `\n - **Required title keyword**: "${this.options.content.titleContext}". This phrase MUST appear in the title. Combine it with key context from today's newsletter content to form a natural, complete title.
1025
+ - Keep title length 20-100 characters and can include 1-2 relevant emoticons.
1026
+ - Use neutral and objective terms in title (e.g., 'announced', 'implementing', 'deadline approaching').
1027
+ - Write title clearly and factually to maintain professionalism and credibility.`
1028
+ : `
1064
1029
  - Title should objectively convey core facts of 1-2 most important news items today.
1065
1030
  - Write with key facts rather than simple "Newsletter", more effective with specific figures or schedules.
1066
1031
  - Use neutral and objective terms in title (e.g., 'announced', 'implementing', 'deadline approaching').
1067
1032
  - Keep title length 20-50 characters and can include 1-2 relevant emoticons.
1068
1033
  - Place most important key facts at beginning of title.
1069
- - Write title clearly and factually to maintain professionalism and credibility.
1034
+ - Write title clearly and factually to maintain professionalism and credibility.`}
1070
1035
 
1071
1036
  8. Additional Requirements:
1072
1037
  - Comprehensively analyze posts to create email containing most important information for ${this.expertFields.join(', ')} field experts.
1073
- - Naturally include date at beginning in the format: "${this.dateService.getDisplayDateString()} ${this.expertFields.join(', ')} [News Term]". Replace [News Term] with the word for "News" appropriate for the output language (e.g., "News" for English, "소식" for Korean). Declare this part as \`Heading 1\`(#).
1074
- - Write body in markdown format, effectively using headings(#, ##, ###), bold(**), italics(_), bullet points(-, *) etc. to improve readability.
1038
+ ${this.options.content.freeFormIntro ? '' : `- Naturally include date at beginning in the format: "${this.dateService.getDisplayDateString()} ${this.expertFields.join(', ')} [News Term]". Replace [News Term] with the word for "News" appropriate for the output language (e.g., "News" for English, "소식" for Korean). Declare this part as \`Heading 1\`(#).\n `}- Write body in markdown format, effectively using headings(#, ##, ###), bold(**), italics(_), bullet points(-, *) etc. to improve readability.
1075
1039
  - Group related news to provide broader context, and mention development status if there's continuity with content covered in previous issues.
1076
1040
  - **Source citation is most important for ensuring credibility.** Must provide links in [original title](URL) format using source's title. Do not write as "View", "Article", "[Post3](URL)" format.
1077
1041
  - Specify source whenever article titles or content are quoted in newsletter, ensure all information is provided with links.
@@ -1122,6 +1086,27 @@ Based on all post information provided above, please generate a ${this.expertFie
1122
1086
  Please follow the roles and output format defined in the system prompt (friendly introduction, overall briefing, category classification, in-depth analysis, polite closing, etc.).`;
1123
1087
  }
1124
1088
  };
1089
+ function addNum(a, b) {
1090
+ if (a == null && b == null)
1091
+ return undefined;
1092
+ return (a ?? 0) + (b ?? 0);
1093
+ }
1094
+ function addUsage(a, b) {
1095
+ return {
1096
+ inputTokens: addNum(a.inputTokens, b.inputTokens),
1097
+ inputTokenDetails: {
1098
+ noCacheTokens: addNum(a.inputTokenDetails?.noCacheTokens, b.inputTokenDetails?.noCacheTokens),
1099
+ cacheReadTokens: addNum(a.inputTokenDetails?.cacheReadTokens, b.inputTokenDetails?.cacheReadTokens),
1100
+ cacheWriteTokens: addNum(a.inputTokenDetails?.cacheWriteTokens, b.inputTokenDetails?.cacheWriteTokens),
1101
+ },
1102
+ outputTokens: addNum(a.outputTokens, b.outputTokens),
1103
+ outputTokenDetails: {
1104
+ textTokens: addNum(a.outputTokenDetails?.textTokens, b.outputTokenDetails?.textTokens),
1105
+ reasoningTokens: addNum(a.outputTokenDetails?.reasoningTokens, b.outputTokenDetails?.reasoningTokens),
1106
+ },
1107
+ totalTokens: addNum(a.totalTokens, b.totalTokens),
1108
+ };
1109
+ }
1125
1110
 
1126
1111
  class ContentGenerateChain extends Chain {
1127
1112
  dateService;
@@ -1214,7 +1199,8 @@ class ContentGenerateChain extends Chain {
1214
1199
  newsletterBrandName: this.provider.newsletterBrandName,
1215
1200
  dateService: this.dateService,
1216
1201
  });
1217
- return await generateNewsletter.execute();
1202
+ const { result } = await generateNewsletter.execute();
1203
+ return result;
1218
1204
  });
1219
1205
  }
1220
1206
  async renderHtml(coreContent) {
@@ -1229,7 +1215,13 @@ class ContentGenerateChain extends Chain {
1229
1215
  }
1230
1216
  return this.htmlTemplate.html
1231
1217
  .replaceAll(`{{${this.htmlTemplate.markers.title}}}`, coreContent.title)
1232
- .replaceAll(`{{${this.htmlTemplate.markers.content}}}`, markdownToHtml(coreContent.content));
1218
+ .replaceAll(`{{${this.htmlTemplate.markers.content}}}`, safeMarkdown2Html(coreContent.content, {
1219
+ window: new jsdom.JSDOM('').window,
1220
+ linkTargetBlank: true,
1221
+ fixMalformedUrls: true,
1222
+ fixBoldSyntax: true,
1223
+ convertStrikethrough: true,
1224
+ }));
1233
1225
  });
1234
1226
  }
1235
1227
  async createNewsletter(html, coreContent, candidateArticles) {
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
package/dist/index.d.ts CHANGED
@@ -254,6 +254,17 @@ type ContentOptions = {
254
254
  * @example ["AI", "Cloud"]
255
255
  */
256
256
  expertField: string | string[];
257
+ /**
258
+ * When true, removes the fixed date+field heading directive
259
+ * and moves the brief introduction into the briefing section.
260
+ */
261
+ freeFormIntro?: boolean;
262
+ /**
263
+ * Context string to prioritize when generating the newsletter title.
264
+ * When provided, the LLM will consider this value as the top priority
265
+ * along with the generated newsletter content for title creation.
266
+ */
267
+ titleContext?: string;
257
268
  };
258
269
  type LLMQueryOptions = {
259
270
  /**
package/dist/index.js CHANGED
@@ -2,10 +2,9 @@ import { RunnablePassthrough, RunnableSequence } from '@langchain/core/runnables
2
2
  import { pick, omit } from 'es-toolkit';
3
3
  import { generateText, Output } from 'ai';
4
4
  import { z } from 'zod';
5
- import juice from 'juice';
6
- import DOMPurify from 'dompurify';
7
5
  import { JSDOM } from 'jsdom';
8
- import { marked } from 'marked';
6
+ import juice from 'juice';
7
+ import safeMarkdown2Html from 'safe-markdown2html';
9
8
  import { randomUUID } from 'node:crypto';
10
9
 
11
10
  /**
@@ -118,6 +117,17 @@ class LLMQuery extends BaseLLMQuery {
118
117
  }
119
118
  }
120
119
 
120
+ const ZERO_USAGE = {
121
+ inputTokens: undefined,
122
+ inputTokenDetails: {
123
+ noCacheTokens: undefined,
124
+ cacheReadTokens: undefined,
125
+ cacheWriteTokens: undefined,
126
+ },
127
+ outputTokens: undefined,
128
+ outputTokenDetails: { textTokens: undefined, reasoningTokens: undefined },
129
+ totalTokens: undefined,
130
+ };
121
131
  class AnalyzeImages extends LLMQuery {
122
132
  schema = z.object({
123
133
  imageContext: z
@@ -130,12 +140,12 @@ class AnalyzeImages extends LLMQuery {
130
140
  async execute() {
131
141
  if (!this.targetArticle.hasAttachedImage ||
132
142
  !this.targetArticle.detailContent) {
133
- return null;
143
+ return { result: null, usage: ZERO_USAGE };
134
144
  }
135
145
  if (this.imageMessages.length === 0) {
136
- return null;
146
+ return { result: null, usage: ZERO_USAGE };
137
147
  }
138
- const { output } = await generateText({
148
+ const { output, usage } = await generateText({
139
149
  model: this.model,
140
150
  maxRetries: this.options.llm.maxRetries,
141
151
  output: Output.object({
@@ -149,7 +159,7 @@ class AnalyzeImages extends LLMQuery {
149
159
  },
150
160
  ],
151
161
  });
152
- return output.imageContext;
162
+ return { result: output.imageContext, usage };
153
163
  }
154
164
  get systemPrompt() {
155
165
  return `# Image Analysis Expert System
@@ -256,7 +266,7 @@ class ClassifyTags extends LLMQuery {
256
266
  }
257
267
  async execute({ existTags }) {
258
268
  this.existTags = existTags;
259
- const { output } = await generateText({
269
+ const { output, usage } = await generateText({
260
270
  model: this.model,
261
271
  maxRetries: this.options.llm.maxRetries,
262
272
  output: Output.object({
@@ -265,7 +275,7 @@ class ClassifyTags extends LLMQuery {
265
275
  system: this.systemPrompt,
266
276
  prompt: this.userPrompt,
267
277
  });
268
- return output;
278
+ return { result: output, usage };
269
279
  }
270
280
  get systemPrompt() {
271
281
  return `You are an AI specializing in analyzing and categorizing articles for professionals in ${this.expertFields.join(', ')}.
@@ -338,7 +348,7 @@ class DetermineArticleImportance extends LLMQuery {
338
348
  this.dateService = config.dateService;
339
349
  }
340
350
  async execute() {
341
- const { output } = await generateText({
351
+ const { output, usage } = await generateText({
342
352
  model: this.model,
343
353
  maxRetries: this.options.llm.maxRetries,
344
354
  output: Output.object({
@@ -347,7 +357,7 @@ class DetermineArticleImportance extends LLMQuery {
347
357
  system: this.systemPrompt,
348
358
  prompt: this.userPrompt,
349
359
  });
350
- return output.importanceScore;
360
+ return { result: output.importanceScore, usage };
351
361
  }
352
362
  get minPoint() {
353
363
  const targetRule = this.minimumImportanceScoreRules.find(({ targetUrl }) => targetUrl === this.targetArticle.targetUrl);
@@ -521,7 +531,9 @@ class ArticleInsightsChain extends PrivateChain {
521
531
  });
522
532
  try {
523
533
  const classifyTags = new ClassifyTags(this.getLlmQueryConfig(this.provider.classifyTagOptions.model, article));
524
- const generatedTags = await classifyTags.execute({ existTags });
534
+ const { result: generatedTags } = await classifyTags.execute({
535
+ existTags,
536
+ });
525
537
  pushTag(generatedTags.tag1);
526
538
  pushTag(generatedTags.tag2);
527
539
  pushTag(generatedTags.tag3);
@@ -594,7 +606,7 @@ class ArticleInsightsChain extends PrivateChain {
594
606
  });
595
607
  try {
596
608
  const analyzeImages = new AnalyzeImages(this.getLlmQueryConfig(this.provider.analyzeImagesOptions.model, article));
597
- const imageContextByLlm = await analyzeImages.execute();
609
+ const { result: imageContextByLlm } = await analyzeImages.execute();
598
610
  if (imageContextByLlm) {
599
611
  articlesWithImageContext.push({
600
612
  id: article.id,
@@ -693,7 +705,7 @@ class ArticleInsightsChain extends PrivateChain {
693
705
  minimumImportanceScoreRules: this.provider.determineScoreOptions.minimumImportanceScoreRules,
694
706
  dateService: this.dateService,
695
707
  });
696
- const importanceScore = await determineArticleImportance.execute();
708
+ const { result: importanceScore } = await determineArticleImportance.execute();
697
709
  const processedArticle = {
698
710
  ...article,
699
711
  importanceScore,
@@ -836,61 +848,6 @@ class AnalysisChain extends Chain {
836
848
  }
837
849
  }
838
850
 
839
- function markdownToHtml(markdown) {
840
- const html = marked.parse(markdown);
841
- const window = new JSDOM('').window;
842
- const purify = DOMPurify(window);
843
- const sanitized = purify.sanitize(html);
844
- const withCorrectedUrls = correctMalformedUrls(sanitized);
845
- const withTargetBlank = addTargetBlankToAnchors(withCorrectedUrls);
846
- const withDelReplaced = replaceDelTagsWithTilde(withTargetBlank);
847
- return correctUnconvertedBoldSyntax(withDelReplaced);
848
- }
849
- function addTargetBlankToAnchors(htmlString) {
850
- // Regular expression to find '<a>' tags
851
- // This regex matches '<a>' tags that contain 'href' attribute and optionally other attributes
852
- // Excludes 'target="[^"]*"' to check if target attribute already exists
853
- const regex = /<a(\s+[^>]*?)?(?<!target="[^"]*")>/gi;
854
- // Use regex to find '<a>' tags and add 'target="_blank"'
855
- return htmlString.replace(regex, (_match, attributes) => {
856
- // Handle undefined attributes as empty string
857
- const currentAttributes = attributes || '';
858
- // Double check if target attribute exists (safety check for regex limitations)
859
- if (currentAttributes.includes('target=')) {
860
- return `<a${currentAttributes}>`; // If target attribute exists, return without modification
861
- }
862
- else {
863
- // Add target="_blank" attribute
864
- return `<a${currentAttributes} target="_blank">`;
865
- }
866
- });
867
- }
868
- function replaceDelTagsWithTilde(htmlString) {
869
- // Replace opening and closing del tags with tilde (~)
870
- return htmlString.replace(/<del>/gi, '~').replace(/<\/del>/gi, '~');
871
- }
872
- function correctUnconvertedBoldSyntax(htmlString) {
873
- // Replace unconverted "**text**" markdown syntax with <b> tags
874
- // Matches "**" followed by one or more non-asterisk characters, followed by "**"
875
- return htmlString.replace(/\*\*([^*]+)\*\*/g, '<b>$1</b>');
876
- }
877
- function correctMalformedUrls(htmlString) {
878
- // Pattern matches anchors with `)` followed by URL-encoded characters
879
- // Capture groups:
880
- // 1: attributes before href
881
- // 2: URL base (before `)`)
882
- // 3: URL-encoded part (starts with %)
883
- // 4: attributes after href
884
- // 5: link text base (before `)`)
885
- // 6: decoded text after `)` in link text
886
- const regex = /<a\s+([^>]*?)href="([^"]*?)\)(%[0-9A-Fa-f]{2}[^"]*?)"([^>]*?)>([^<]*?)\)([^<]*?)<\/a>/g;
887
- return htmlString.replace(regex, (_match, beforeHref, urlBase, _encodedPart, afterHref, textBase, decodedTextInLink) => {
888
- // The decoded text is already in the link text (decodedTextInLink)
889
- // We just need to move it outside the anchor along with the `)`
890
- return `<a ${beforeHref}href="${urlBase}"${afterHref}>${textBase}</a>)${decodedTextInLink}`;
891
- });
892
- }
893
-
894
851
  let GenerateNewsletter$1 = class GenerateNewsletter extends BaseLLMQuery {
895
852
  maxOutputTokens;
896
853
  temperature;
@@ -933,7 +890,7 @@ let GenerateNewsletter$1 = class GenerateNewsletter extends BaseLLMQuery {
933
890
  this.newsletterBrandName = config.newsletterBrandName;
934
891
  }
935
892
  async execute() {
936
- const { output } = await generateText({
893
+ const { output, usage } = await generateText({
937
894
  model: this.model,
938
895
  maxRetries: this.options.llm.maxRetries,
939
896
  maxOutputTokens: this.maxOutputTokens,
@@ -948,16 +905,19 @@ let GenerateNewsletter$1 = class GenerateNewsletter extends BaseLLMQuery {
948
905
  system: this.systemPrompt,
949
906
  prompt: this.userPrompt,
950
907
  });
951
- if (!output.isWrittenInOutputLanguage) {
952
- return this.execute();
953
- }
954
- if (!output.copyrightVerified) {
955
- return this.execute();
956
- }
957
- if (!output.factAccuracy) {
958
- return this.execute();
908
+ const needsRetry = !output.isWrittenInOutputLanguage ||
909
+ !output.copyrightVerified ||
910
+ !output.factAccuracy ||
911
+ (this.options.content.titleContext &&
912
+ !output.title.includes(this.options.content.titleContext));
913
+ if (needsRetry) {
914
+ const retryResult = await this.execute();
915
+ return {
916
+ result: retryResult.result,
917
+ usage: addUsage(usage, retryResult.usage),
918
+ };
959
919
  }
960
- return pick(output, ['title', 'content']);
920
+ return { result: pick(output, ['title', 'content']), usage };
961
921
  }
962
922
  get systemPrompt() {
963
923
  return `You are a newsletter production expert for "${this.newsletterBrandName}" who analyzes and delivers trends in the fields of ${this.expertFields.join(', ')}. Your goal is to provide in-depth analysis that helps industry professionals easily understand complex information and make informed decisions.
@@ -1011,14 +971,14 @@ Copyright Protection & Fact-Checking Principles:
1011
971
  Output Format & Requirements:
1012
972
  1. Language: ${this.options.content.outputLanguage}
1013
973
 
1014
- 2. Start: Specify date (${this.dateService.getDisplayDateString()}) and begin with neutral, objective greeting. Briefly introduce key factual information to be covered in today's newsletter.
974
+ 2. Start: ${this.options.content.freeFormIntro ? 'Begin directly with the Overall Briefing section (no separate opening heading or greeting).' : `Specify date (${this.dateService.getDisplayDateString()}) and begin with neutral, objective greeting. Briefly introduce key factual information to be covered in today's newsletter.`}
1015
975
 
1016
- 3. Overall Briefing: Before the main listing, create a briefing section conveying objective facts about today's news in these aspects:
976
+ 3. Overall Briefing: Before the main listing, create a briefing section conveying objective facts about today's news${this.options.content.freeFormIntro ? `. Structure: Start with a Heading 2 (##) briefing section heading in the format "## 📮 ${this.dateService.getDisplayDateString()} [Briefing/Summary word in output language]" (e.g., "## 📮 2월 6일 브리핑" for Korean, "## 📮 Feb 6 Briefing" for English) — do NOT include domain or field names in the heading. Immediately follow with a brief paragraph introducing key factual information to be covered in today's newsletter, then include the following bullet points:` : ' in these aspects:'}
1017
977
  - Key Trends: Explain major patterns or trends found in this news based on data. Ex: 'Over 00% of today's news relates to 00'.
1018
978
  - Immediate Impact: Emphasize most important changes or decisions affecting industry immediately, specifically mentioning which fields will be most impacted.
1019
979
 
1020
980
  4. Category Classification & Content Organization:
1021
- - Group news by logical categories based on related tags and content (e.g., Policy/Regulation, Budget/Support, Research/Development, Products/Services, Operations/Process, Recruitment/Events) rather than just listing by importance.
981
+ - Group news by logical categories based on related tags and content (e.g., Policy/Regulation, Budget/Support, Research/Development, Products/Services, Operations/Process, Recruitment/Events) rather than just listing by importance.${this.options.content.freeFormIntro ? '\n - Use Heading 2 (##) for each category heading (same level as the briefing heading). Do NOT use Heading 3 (###) for categories.' : ''}
1022
982
  - Use appropriate emoticons for each category for visual distinction.
1023
983
  - Sort by importance within categories, making high-importance items more prominent.
1024
984
  - Add short paragraph at category start summarizing overall trends or changes in that area, specifying important points and areas to focus on.
@@ -1058,18 +1018,22 @@ Output Format & Requirements:
1058
1018
  - Do not write preview or anticipatory messages about next newsletter.
1059
1019
  - Do not include contact information for inquiries.
1060
1020
 
1061
- 7. Title Writing Guidelines:
1021
+ 7. Title Writing Guidelines:${this.options.content.titleContext
1022
+ ? `\n - **Required title keyword**: "${this.options.content.titleContext}". This phrase MUST appear in the title. Combine it with key context from today's newsletter content to form a natural, complete title.
1023
+ - Keep title length 20-100 characters and can include 1-2 relevant emoticons.
1024
+ - Use neutral and objective terms in title (e.g., 'announced', 'implementing', 'deadline approaching').
1025
+ - Write title clearly and factually to maintain professionalism and credibility.`
1026
+ : `
1062
1027
  - Title should objectively convey core facts of 1-2 most important news items today.
1063
1028
  - Write with key facts rather than simple "Newsletter", more effective with specific figures or schedules.
1064
1029
  - Use neutral and objective terms in title (e.g., 'announced', 'implementing', 'deadline approaching').
1065
1030
  - Keep title length 20-50 characters and can include 1-2 relevant emoticons.
1066
1031
  - Place most important key facts at beginning of title.
1067
- - Write title clearly and factually to maintain professionalism and credibility.
1032
+ - Write title clearly and factually to maintain professionalism and credibility.`}
1068
1033
 
1069
1034
  8. Additional Requirements:
1070
1035
  - Comprehensively analyze posts to create email containing most important information for ${this.expertFields.join(', ')} field experts.
1071
- - Naturally include date at beginning in the format: "${this.dateService.getDisplayDateString()} ${this.expertFields.join(', ')} [News Term]". Replace [News Term] with the word for "News" appropriate for the output language (e.g., "News" for English, "소식" for Korean). Declare this part as \`Heading 1\`(#).
1072
- - Write body in markdown format, effectively using headings(#, ##, ###), bold(**), italics(_), bullet points(-, *) etc. to improve readability.
1036
+ ${this.options.content.freeFormIntro ? '' : `- Naturally include date at beginning in the format: "${this.dateService.getDisplayDateString()} ${this.expertFields.join(', ')} [News Term]". Replace [News Term] with the word for "News" appropriate for the output language (e.g., "News" for English, "소식" for Korean). Declare this part as \`Heading 1\`(#).\n `}- Write body in markdown format, effectively using headings(#, ##, ###), bold(**), italics(_), bullet points(-, *) etc. to improve readability.
1073
1037
  - Group related news to provide broader context, and mention development status if there's continuity with content covered in previous issues.
1074
1038
  - **Source citation is most important for ensuring credibility.** Must provide links in [original title](URL) format using source's title. Do not write as "View", "Article", "[Post3](URL)" format.
1075
1039
  - Specify source whenever article titles or content are quoted in newsletter, ensure all information is provided with links.
@@ -1120,6 +1084,27 @@ Based on all post information provided above, please generate a ${this.expertFie
1120
1084
  Please follow the roles and output format defined in the system prompt (friendly introduction, overall briefing, category classification, in-depth analysis, polite closing, etc.).`;
1121
1085
  }
1122
1086
  };
1087
+ function addNum(a, b) {
1088
+ if (a == null && b == null)
1089
+ return undefined;
1090
+ return (a ?? 0) + (b ?? 0);
1091
+ }
1092
+ function addUsage(a, b) {
1093
+ return {
1094
+ inputTokens: addNum(a.inputTokens, b.inputTokens),
1095
+ inputTokenDetails: {
1096
+ noCacheTokens: addNum(a.inputTokenDetails?.noCacheTokens, b.inputTokenDetails?.noCacheTokens),
1097
+ cacheReadTokens: addNum(a.inputTokenDetails?.cacheReadTokens, b.inputTokenDetails?.cacheReadTokens),
1098
+ cacheWriteTokens: addNum(a.inputTokenDetails?.cacheWriteTokens, b.inputTokenDetails?.cacheWriteTokens),
1099
+ },
1100
+ outputTokens: addNum(a.outputTokens, b.outputTokens),
1101
+ outputTokenDetails: {
1102
+ textTokens: addNum(a.outputTokenDetails?.textTokens, b.outputTokenDetails?.textTokens),
1103
+ reasoningTokens: addNum(a.outputTokenDetails?.reasoningTokens, b.outputTokenDetails?.reasoningTokens),
1104
+ },
1105
+ totalTokens: addNum(a.totalTokens, b.totalTokens),
1106
+ };
1107
+ }
1123
1108
 
1124
1109
  class ContentGenerateChain extends Chain {
1125
1110
  dateService;
@@ -1212,7 +1197,8 @@ class ContentGenerateChain extends Chain {
1212
1197
  newsletterBrandName: this.provider.newsletterBrandName,
1213
1198
  dateService: this.dateService,
1214
1199
  });
1215
- return await generateNewsletter.execute();
1200
+ const { result } = await generateNewsletter.execute();
1201
+ return result;
1216
1202
  });
1217
1203
  }
1218
1204
  async renderHtml(coreContent) {
@@ -1227,7 +1213,13 @@ class ContentGenerateChain extends Chain {
1227
1213
  }
1228
1214
  return this.htmlTemplate.html
1229
1215
  .replaceAll(`{{${this.htmlTemplate.markers.title}}}`, coreContent.title)
1230
- .replaceAll(`{{${this.htmlTemplate.markers.content}}}`, markdownToHtml(coreContent.content));
1216
+ .replaceAll(`{{${this.htmlTemplate.markers.content}}}`, safeMarkdown2Html(coreContent.content, {
1217
+ window: new JSDOM('').window,
1218
+ linkTargetBlank: true,
1219
+ fixMalformedUrls: true,
1220
+ fixBoldSyntax: true,
1221
+ convertStrikethrough: true,
1222
+ }));
1231
1223
  });
1232
1224
  }
1233
1225
  async createNewsletter(html, coreContent, candidateArticles) {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@llm-newsletter-kit/core",
3
3
  "private": false,
4
4
  "type": "module",
5
- "version": "1.1.5",
5
+ "version": "1.2.0",
6
6
  "description": "An extensible framework to automate your entire newsletter workflow. Handles data collection, LLM-based content analysis, and email generation, letting you focus on your unique domain logic.",
7
7
  "main": "dist/index.cjs",
8
8
  "module": "dist/index.js",
@@ -41,25 +41,30 @@
41
41
  "lint:fix": "eslint --fix ./src",
42
42
  "lint:ci": "eslint --quiet ./src",
43
43
  "typecheck": "tsc --noEmit",
44
- "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css,md,mdx}\""
44
+ "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css,md,mdx}\"",
45
+ "playground": "tsx",
46
+ "playground:generate-newsletter": "tsx playground/generate-newsletter.ts"
45
47
  },
46
48
  "author": "kimhongyeon",
47
49
  "license": "Apache-2.0",
48
50
  "dependencies": {
49
51
  "@langchain/core": "^1.1.19",
50
- "ai": "^6.0.69",
51
- "dompurify": "^3.3.1",
52
+ "ai": "^6.0.77",
52
53
  "es-toolkit": "^1.44.0",
53
- "jsdom": "^27.4.0",
54
- "juice": "^11.1.0",
55
- "marked": "^17.0.1",
54
+ "jsdom": "^28.0.0",
55
+ "juice": "^11.1.1",
56
+ "safe-markdown2html": "^1.0.0",
56
57
  "zod": "^4.3.6"
57
58
  },
58
59
  "devDependencies": {
60
+ "@ai-sdk/anthropic": "^3.0.38",
61
+ "@ai-sdk/google": "^3.0.22",
62
+ "@ai-sdk/openai": "^3.0.26",
63
+ "@ai-sdk/togetherai": "^2.0.31",
59
64
  "@eslint/js": "^9.39.2",
60
65
  "@trivago/prettier-plugin-sort-imports": "^6.0.2",
61
66
  "@types/jsdom": "^27.0.0",
62
- "@types/node": "^25.2.0",
67
+ "@types/node": "^25.2.1",
63
68
  "@vitest/coverage-v8": "^3.2.4",
64
69
  "@vitest/expect": "^3.2.4",
65
70
  "eslint": "^9.39.2",
@@ -69,6 +74,7 @@
69
74
  "rollup": "^4.57.1",
70
75
  "rollup-plugin-dts": "^6.3.0",
71
76
  "rollup-plugin-typescript2": "^0.36.0",
77
+ "tsx": "^4.21.0",
72
78
  "typescript": "^5.9.3",
73
79
  "typescript-eslint": "^8.54.0",
74
80
  "vitest": "^3.2.4"