@heripo/research-radar 2.3.7 → 3.1.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
@@ -24,7 +24,7 @@ An AI-powered newsletter service for Korean cultural heritage. Built on [`@llm-n
24
24
  - Type-safe TypeScript with strict interfaces
25
25
  - Provider pattern for swapping components (Crawling/Analysis/Content/Email)
26
26
  - 66 crawling targets across heritage agencies, museums, academic societies
27
- - Dual LLM providers: OpenAI GPT-5 (analysis) + Google Gemini (content generation)
27
+ - Multi LLM providers: OpenAI GPT-5 (analysis) + selectable content generation (OpenAI / Anthropic / Google)
28
28
  - Built-in retries, chain options, preview emails
29
29
 
30
30
  **Links**: [Live service](https://heripo.com/research-radar/subscribe) • [Newsletter example](https://heripo.com/research-radar-newsletter-example.html) • [Core engine](https://github.com/heripo-lab/llm-newsletter-kit-core)
@@ -69,7 +69,7 @@ For academic publications:
69
69
  npm install @heripo/research-radar @llm-newsletter-kit/core
70
70
  ```
71
71
 
72
- **Requirements**: Node.js >= 24, OpenAI API key, Google Generative AI API key
72
+ **Requirements**: Node.js >= 24, OpenAI API key, content generation API key (OpenAI / Anthropic / Google)
73
73
 
74
74
  **Note**: `@llm-newsletter-kit/core` is a peer dependency and must be installed separately.
75
75
 
@@ -80,7 +80,11 @@ import { generateNewsletter } from '@heripo/research-radar';
80
80
 
81
81
  const newsletterId = await generateNewsletter({
82
82
  openAIApiKey: process.env.OPENAI_API_KEY,
83
- googleGenerativeAIApiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY,
83
+ contentGeneration: {
84
+ provider: 'anthropic', // 'openai' | 'anthropic' | 'google'
85
+ apiKey: process.env.ANTHROPIC_API_KEY,
86
+ // model: 'claude-sonnet-4-6', // optional, uses sensible default
87
+ },
84
88
 
85
89
  // Implement these repository interfaces (see src/types/dependencies.ts)
86
90
  taskRepository: {
@@ -237,16 +241,21 @@ subscribeUrl: 'https://yourdomain.com/subscribe'
237
241
  - Replace Korean heritage sites with your domain sources
238
242
  - Implement parsers in `src/parsers/`
239
243
 
240
- **4. Switch LLM provider** (optional):
244
+ **4. Switch content generation LLM provider** (optional):
241
245
 
242
- Currently uses dual providers: **OpenAI** (analysis) + **Google Gemini** (content generation). To change:
243
- - `src/newsletter-generator.ts`: Change `createOpenAI()` / `createGoogleGenerativeAI()` to your provider
244
- - `src/providers/analysis.provider.ts`: Update model names (currently `gpt-5-mini`, `gpt-5.1`)
245
- - `src/providers/content-generate.provider.ts`: Update model name (currently `gemini-3-pro-preview`)
246
+ Content generation supports **3 built-in providers** just change `contentGeneration.provider`:
247
+ ```typescript
248
+ contentGeneration: {
249
+ provider: 'google', // 'openai' | 'anthropic' | 'google'
250
+ apiKey: process.env.GOOGLE_API_KEY,
251
+ model: 'gemini-3.1-pro-preview', // optional, each provider has a default
252
+ }
253
+ ```
254
+ Default models: openai=`gpt-5.1`, anthropic=`claude-sonnet-4-6`, google=`gemini-3.1-pro-preview`
246
255
 
247
- Any [Vercel AI SDK provider](https://sdk.vercel.ai/providers) works.
256
+ Analysis provider (OpenAI) can be changed by modifying `src/providers/analysis.provider.ts`. Any [Vercel AI SDK provider](https://sdk.vercel.ai/providers) works.
248
257
 
249
- **Search keywords**: `heripo`, `kimhongyeon`, `#D2691E`, `openai`, `gpt-5`, `google`, `GoogleGenerativeAI`, `gemini`, `createGoogleGenerativeAI`
258
+ **Search keywords**: `heripo`, `kimhongyeon`, `#D2691E`, `openai`, `gpt-5`, `contentGeneration`
250
259
 
251
260
  ## Why Code-Based?
252
261
 
package/dist/index.cjs CHANGED
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ var anthropic = require('@ai-sdk/anthropic');
3
4
  var google = require('@ai-sdk/google');
4
5
  var openai = require('@ai-sdk/openai');
5
6
  var core = require('@llm-newsletter-kit/core');
@@ -1922,7 +1923,7 @@ const createNewsletterHtmlTemplate = (targets, options) => `<!DOCTYPE html>
1922
1923
  -ms-text-size-adjust: 100%;
1923
1924
  mso-table-lspace: 0pt;
1924
1925
  mso-table-rspace: 0pt;
1925
- max-width: 800px;
1926
+ max-width: 700px;
1926
1927
  }
1927
1928
 
1928
1929
  .content-cell {
@@ -2087,7 +2088,7 @@ const createNewsletterHtmlTemplate = (targets, options) => `<!DOCTYPE html>
2087
2088
  background-color: #faf7f3;
2088
2089
  }
2089
2090
 
2090
- @media screen and (max-width: 800px) {
2091
+ @media screen and (max-width: 700px) {
2091
2092
  .container {
2092
2093
  width: 100% !important;
2093
2094
  max-width: 100% !important;
@@ -2246,6 +2247,18 @@ const createNewsletterHtmlTemplate = (targets, options) => `<!DOCTYPE html>
2246
2247
  .dark-logo-inline {
2247
2248
  display: inline-block !important;
2248
2249
  }
2250
+
2251
+ .kras-newsletter .kras-header-title {
2252
+ color: #eeeeee !important;
2253
+ }
2254
+
2255
+ .kras-newsletter .kras-header-date {
2256
+ color: #bbbbbb !important;
2257
+ }
2258
+
2259
+ .kras-newsletter .kras-header-divider {
2260
+ border-top-color: #E59866 !important;
2261
+ }
2249
2262
  }
2250
2263
  </style>
2251
2264
  </head>
@@ -2254,42 +2267,37 @@ const createNewsletterHtmlTemplate = (targets, options) => `<!DOCTYPE html>
2254
2267
  <tr>
2255
2268
  <td bgcolor="#f4f4f4" align="center" style="-webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; mso-table-lspace: 0pt; mso-table-rspace: 0pt; padding: 20px 0;" class="dark-mode-bg">
2256
2269
  <!--[if (gte mso 9)|(IE)]>
2257
- <table align="center" border="0" cellspacing="0" cellpadding="0" width="800">
2270
+ <table align="center" border="0" cellspacing="0" cellpadding="0" width="700">
2258
2271
  <tr>
2259
- <td align="center" valign="top" width="800">
2272
+ <td align="center" valign="top" width="700">
2260
2273
  <![endif]-->
2261
- <table border="0" cellpadding="0" cellspacing="0" width="100%" style="-webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; mso-table-lspace: 0pt; mso-table-rspace: 0pt; max-width: 800px;" class="container" role="presentation">
2274
+ <table border="0" cellpadding="0" cellspacing="0" width="100%" style="-webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; mso-table-lspace: 0pt; mso-table-rspace: 0pt; max-width: 700px;" class="container" role="presentation">
2262
2275
  <tr>
2263
- <td bgcolor="#ffffff" align="left" class="content-cell dark-mode-content-bg" style="-webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; mso-table-lspace: 0pt; mso-table-rspace: 0pt; padding: 44px 44px 36px 44px; border-radius: 12px; box-shadow: 0 4px 18px rgba(0,0,0,0.07);">
2276
+ <td bgcolor="#ffffff" align="left" class="content-cell dark-mode-content-bg${options?.isKrasNewsletter ? ' kras-newsletter' : ''}" style="-webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; mso-table-lspace: 0pt; mso-table-rspace: 0pt; padding: 44px 44px 36px 44px; border-radius: 12px; box-shadow: 0 4px 18px rgba(0,0,0,0.07);">
2264
2277
  ${options?.isKrasNewsletter
2265
- ? `${krasHeaderHtml()}
2266
- <!-- 헤더: 제목/날짜 -->
2267
- <table cellpadding="0" cellspacing="0" width="100%" role="presentation" class="header-title-border" style="width: 100%; border-collapse: collapse; margin: 0 0 18px 0; mso-table-lspace: 0pt; mso-table-rspace: 0pt; margin-bottom: 32px; border: none; border-bottom: 3px solid #D2691E;">
2268
- <tr>
2269
- <td align="left" valign="baseline" class="header-dark-text" style="text-align: left; padding: 0 0 14px 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 26px; font-weight: bold; color: #111111; line-height: 1.2; border: none;">
2270
- 한국고고학회 뉴스레터
2271
- </td>
2272
- <td align="left" valign="baseline" class="header-dark-text" style="text-align: left; padding: 0 0 14px 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 24px; color: #333333; white-space: nowrap; border: none; width: 1px;" width="1">
2273
- ${options?.displayDate ?? ''}
2274
- </td>
2275
- </tr>
2276
- </table>
2278
+ ? `<!-- KRAS 50주년 헤더 -->
2279
+ <div style="text-align: center; margin-bottom: 28px;">
2280
+ <div style="width: 180px; min-height: 141px; display: inline-block; margin-bottom: 20px;"><img src="https://heripo.com/kras-50.png" width="180" alt="한국고고학회 50주년" style="-ms-interpolation-mode: bicubic; border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; display: block;" height="auto"></div>
2281
+ <div class="kras-header-title" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 30px; font-weight: bold; color: #111111; line-height: 1.2; margin-bottom: 8px;">한국고고학회 뉴스레터</div>
2282
+ <div class="kras-header-date" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; color: #666666; line-height: 1.5;">${options?.displayDate ?? ''}</div>
2283
+ </div>
2284
+ <hr class="kras-header-divider" style="border: 0; border-top: 2px solid #D2691E; margin: 0 0 32px 0;">
2277
2285
  `
2278
2286
  : `${heripoLogoHtml('12px')}
2279
2287
  `}
2280
2288
 
2281
2289
  ${options?.krasNewsMarkdown
2282
- ? safeMarkdown2Html(`## 학회 소식
2283
- ${options.krasNewsMarkdown}
2290
+ ? `<h2 style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 24px; font-weight: bold; line-height: 1.3; color: #D2691E; margin: 0 0 16px 0; letter-spacing: -0.2px; border-left: 5px solid #D2691E; padding-left: 12px; background: none;"><span style="display: inline-block; width: 26px; height: 26px; vertical-align: -4px; margin-right: 6px;"><img src="https://heripo.com/kras-symbol.png" width="26" height="26" alt="" style="border: 0; display: block;"></span>학회 소식</h2>` +
2291
+ safeMarkdown2Html(`${options.krasNewsMarkdown}
2284
2292
 
2285
2293
  ---
2286
2294
  `, {
2287
- window: new jsdom.JSDOM('').window,
2288
- linkTargetBlank: true,
2289
- fixMalformedUrls: true,
2290
- fixBoldSyntax: true,
2291
- convertStrikethrough: true,
2292
- }).replaceAll('%7B%7B%7BRESEND_UNSUBSCRIBE_URL%7D%7D%7D', '{{{RESEND_UNSUBSCRIBE_URL}}}')
2295
+ window: new jsdom.JSDOM('').window,
2296
+ linkTargetBlank: true,
2297
+ fixMalformedUrls: true,
2298
+ fixBoldSyntax: true,
2299
+ convertStrikethrough: true,
2300
+ }).replaceAll('%7B%7B%7BRESEND_UNSUBSCRIBE_URL%7D%7D%7D', '{{{RESEND_UNSUBSCRIBE_URL}}}')
2293
2301
  : ''}
2294
2302
 
2295
2303
  ${options?.heripolabNewsMarkdown
@@ -2361,12 +2369,11 @@ ${poweredByFooterHtml()}
2361
2369
 
2362
2370
  /**
2363
2371
  * Content generation provider implementation
2364
- * - LLM-based newsletter content generation (Google Generative AI)
2372
+ * - LLM-based newsletter content generation
2365
2373
  * - HTML template provisioning
2366
2374
  * - Newsletter persistence
2367
2375
  */
2368
2376
  class ContentGenerateProvider {
2369
- google;
2370
2377
  articleRepository;
2371
2378
  newsletterRepository;
2372
2379
  _issueOrder = null;
@@ -2375,11 +2382,10 @@ class ContentGenerateProvider {
2375
2382
  htmlTemplate;
2376
2383
  /** Newsletter brand name (defaults to config, can be overridden via constructor) */
2377
2384
  newsletterBrandName;
2378
- constructor(google, articleRepository, newsletterRepository, templateOptions, brandName) {
2379
- this.google = google;
2385
+ constructor(model, articleRepository, newsletterRepository, templateOptions, brandName) {
2380
2386
  this.articleRepository = articleRepository;
2381
2387
  this.newsletterRepository = newsletterRepository;
2382
- this.model = this.google('gemini-3-pro-preview');
2388
+ this.model = model;
2383
2389
  this.newsletterBrandName = brandName ?? newsletterConfig.brandName;
2384
2390
  this.htmlTemplate = {
2385
2391
  html: createNewsletterHtmlTemplate(createCrawlingTargetGroups().flatMap((group) => group.targets), templateOptions),
@@ -2575,7 +2581,10 @@ class TaskService {
2575
2581
  * ```typescript
2576
2582
  * const generator = createNewsletterGenerator({
2577
2583
  * openAIApiKey: process.env.OPENAI_API_KEY,
2578
- * googleGenerativeAIApiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY,
2584
+ * contentGeneration: {
2585
+ * provider: 'anthropic',
2586
+ * apiKey: process.env.ANTHROPIC_API_KEY,
2587
+ * },
2579
2588
  * taskRepository: new PrismaTaskRepository(prisma),
2580
2589
  * articleRepository: new PrismaArticleRepository(prisma),
2581
2590
  * tagRepository: new PrismaTagRepository(prisma),
@@ -2591,13 +2600,26 @@ class TaskService {
2591
2600
  * const newsletterId = await generator.generate();
2592
2601
  * ```
2593
2602
  */
2603
+ function createContentGenerationModel(config) {
2604
+ switch (config.provider) {
2605
+ case 'openai': {
2606
+ const provider = openai.createOpenAI({ apiKey: config.apiKey });
2607
+ return provider(config.model ?? 'gpt-5.4');
2608
+ }
2609
+ case 'anthropic': {
2610
+ const provider = anthropic.createAnthropic({ apiKey: config.apiKey });
2611
+ return provider(config.model ?? 'claude-sonnet-4-6');
2612
+ }
2613
+ case 'google': {
2614
+ const provider = google.createGoogleGenerativeAI({ apiKey: config.apiKey });
2615
+ return provider(config.model ?? 'gemini-3.1-pro-preview');
2616
+ }
2617
+ }
2618
+ }
2594
2619
  function createNewsletterGenerator(dependencies) {
2595
2620
  const openai$1 = openai.createOpenAI({
2596
2621
  apiKey: dependencies.openAIApiKey,
2597
2622
  });
2598
- const google$1 = google.createGoogleGenerativeAI({
2599
- apiKey: dependencies.googleGenerativeAIApiKey,
2600
- });
2601
2623
  const dateService = new DateService(dependencies.publishDate);
2602
2624
  const taskService = new TaskService(dependencies.taskRepository);
2603
2625
  const crawlingProvider = new CrawlingProvider(dependencies.articleRepository, dependencies.customFetch);
@@ -2620,7 +2642,8 @@ function createNewsletterGenerator(dependencies) {
2620
2642
  };
2621
2643
  resolvedBrandName = '한국고고학회 뉴스레터';
2622
2644
  }
2623
- const contentGenerateProvider = new ContentGenerateProvider(google$1, dependencies.articleRepository, dependencies.newsletterRepository, templateOptions, resolvedBrandName);
2645
+ const contentModel = createContentGenerationModel(dependencies.contentGeneration);
2646
+ const contentGenerateProvider = new ContentGenerateProvider(contentModel, dependencies.articleRepository, dependencies.newsletterRepository, templateOptions, resolvedBrandName);
2624
2647
  return new core.GenerateNewsletter({
2625
2648
  contentOptions: resolvedContentOptions,
2626
2649
  dateService,
@@ -2650,7 +2673,10 @@ function createNewsletterGenerator(dependencies) {
2650
2673
  * ```typescript
2651
2674
  * const newsletterId = await generateNewsletter({
2652
2675
  * openAIApiKey: process.env.OPENAI_API_KEY,
2653
- * googleGenerativeAIApiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY,
2676
+ * contentGeneration: {
2677
+ * provider: 'anthropic',
2678
+ * apiKey: process.env.ANTHROPIC_API_KEY,
2679
+ * },
2654
2680
  * taskRepository: new PrismaTaskRepository(prisma),
2655
2681
  * articleRepository: new PrismaArticleRepository(prisma),
2656
2682
  * tagRepository: new PrismaTagRepository(prisma),
@@ -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
@@ -1,6 +1,6 @@
1
1
  import { UrlString, ParsedTarget, CrawlingTargetGroup, CrawlingTarget, UnscoredArticle, ArticleForUpdateByAnalysis, ArticleForGenerateContent, Newsletter, AppLogger, EmailService, EmailMessage, DateService as DateService$1, IsoDateString, TaskService as TaskService$1, AnalysisProvider as AnalysisProvider$1, ContentGenerateProvider as ContentGenerateProvider$1, HtmlTemplate, CrawlingProvider as CrawlingProvider$1, GenerateNewsletterConfig } from '@llm-newsletter-kit/core';
2
2
  import { OpenAIProvider } from '@ai-sdk/openai';
3
- import { GoogleGenerativeAIProvider } from '@ai-sdk/google';
3
+ import { LanguageModel } from 'ai';
4
4
 
5
5
  /**
6
6
  * Repository interface for task management
@@ -151,12 +151,9 @@ interface NewsletterRepository {
151
151
  }
152
152
 
153
153
  /**
154
- * Uses two LLM providers:
155
- * - OpenAI (gpt-5-mini, gpt-5.1): Article analysis (tag classification, image analysis, importance scoring)
156
- * - Google Generative AI (gemini-3-pro-preview): Newsletter content generation
154
+ * Uses OpenAI for article analysis and a configurable provider (OpenAI / Anthropic / Google) for content generation.
157
155
  *
158
- * For details on switching providers, see README.md section:
159
- * "⚠️ Fork하여 나만의 뉴스레터 만들기 > 4. LLM 프로바이더 변경"
156
+ * Content generation provider is selected via `contentGeneration.provider` in dependencies.
160
157
  */
161
158
 
162
159
  /**
@@ -170,14 +167,37 @@ interface PreviewNewsletterOptions {
170
167
  /** Email message configuration (subject, html, text are auto-generated) */
171
168
  emailMessage: Omit<EmailMessage, 'subject' | 'html' | 'text'>;
172
169
  }
170
+ /**
171
+ * Content generation LLM provider configuration.
172
+ * Choose one of the three supported providers and supply the API key.
173
+ * Each provider uses a sensible default model that can be overridden.
174
+ *
175
+ * Default models:
176
+ * - openai: `gpt-5.4`
177
+ * - anthropic: `claude-sonnet-4-6`
178
+ * - google: `gemini-3.1-pro-preview`
179
+ */
180
+ type ContentGenerationConfig = {
181
+ provider: 'openai';
182
+ apiKey: string;
183
+ model?: string;
184
+ } | {
185
+ provider: 'anthropic';
186
+ apiKey: string;
187
+ model?: string;
188
+ } | {
189
+ provider: 'google';
190
+ apiKey: string;
191
+ model?: string;
192
+ };
173
193
  /**
174
194
  * Newsletter generator dependencies interface
175
195
  */
176
196
  interface NewsletterGeneratorDependencies {
177
197
  /** OpenAI API key (used for article analysis: tag classification, image analysis, importance scoring) */
178
198
  openAIApiKey: string;
179
- /** Google Generative AI API key (used for newsletter content generation) */
180
- googleGenerativeAIApiKey: string;
199
+ /** Content generation LLM configuration (provider + API key + optional model) */
200
+ contentGeneration: ContentGenerationConfig;
181
201
  /** Task management repository */
182
202
  taskRepository: TaskRepository;
183
203
  /** Article management repository */
@@ -212,7 +232,10 @@ interface NewsletterGeneratorDependencies {
212
232
  * ```typescript
213
233
  * const newsletterId = await generateNewsletter({
214
234
  * openAIApiKey: process.env.OPENAI_API_KEY,
215
- * googleGenerativeAIApiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY,
235
+ * contentGeneration: {
236
+ * provider: 'anthropic',
237
+ * apiKey: process.env.ANTHROPIC_API_KEY,
238
+ * },
216
239
  * taskRepository: new PrismaTaskRepository(prisma),
217
240
  * articleRepository: new PrismaArticleRepository(prisma),
218
241
  * tagRepository: new PrismaTagRepository(prisma),
@@ -338,21 +361,20 @@ declare class AnalysisProvider implements AnalysisProvider$1 {
338
361
 
339
362
  /**
340
363
  * Content generation provider implementation
341
- * - LLM-based newsletter content generation (Google Generative AI)
364
+ * - LLM-based newsletter content generation
342
365
  * - HTML template provisioning
343
366
  * - Newsletter persistence
344
367
  */
345
368
  declare class ContentGenerateProvider implements ContentGenerateProvider$1 {
346
- private readonly google;
347
369
  private readonly articleRepository;
348
370
  private readonly newsletterRepository;
349
371
  private _issueOrder;
350
- model: ReturnType<GoogleGenerativeAIProvider>;
372
+ readonly model: LanguageModel;
351
373
  /** HTML template with markers for title and content injection */
352
374
  htmlTemplate: HtmlTemplate;
353
375
  /** Newsletter brand name (defaults to config, can be overridden via constructor) */
354
376
  newsletterBrandName: string;
355
- constructor(google: GoogleGenerativeAIProvider, articleRepository: ArticleRepository, newsletterRepository: NewsletterRepository, templateOptions?: NewsletterTemplateOptions, brandName?: string);
377
+ constructor(model: LanguageModel, articleRepository: ArticleRepository, newsletterRepository: NewsletterRepository, templateOptions?: NewsletterTemplateOptions, brandName?: string);
356
378
  /** LLM temperature setting for content generation */
357
379
  temperature: number;
358
380
  /** Subscribe page URL */
@@ -460,4 +482,4 @@ declare const llmConfig: {
460
482
  };
461
483
 
462
484
  export { AnalysisProvider, ContentGenerateProvider, CrawlingProvider, DateService, TaskService, contentOptions, createCrawlingTargetGroups, generateNewsletter, generateWelcomeHTML, llmConfig, newsletterConfig };
463
- export type { ArticleRepository, ContentOptions, NewsletterConfig, NewsletterGeneratorDependencies, NewsletterRepository, NewsletterTemplateOptions, PreviewNewsletterOptions, TagRepository, TaskRepository, WelcomeTemplateOptions };
485
+ export type { ArticleRepository, ContentGenerationConfig, ContentOptions, NewsletterConfig, NewsletterGeneratorDependencies, NewsletterRepository, NewsletterTemplateOptions, PreviewNewsletterOptions, TagRepository, TaskRepository, WelcomeTemplateOptions };
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { createAnthropic } from '@ai-sdk/anthropic';
1
2
  import { createGoogleGenerativeAI } from '@ai-sdk/google';
2
3
  import { createOpenAI } from '@ai-sdk/openai';
3
4
  import { DateType, GenerateNewsletter } from '@llm-newsletter-kit/core';
@@ -1901,7 +1902,7 @@ const createNewsletterHtmlTemplate = (targets, options) => `<!DOCTYPE html>
1901
1902
  -ms-text-size-adjust: 100%;
1902
1903
  mso-table-lspace: 0pt;
1903
1904
  mso-table-rspace: 0pt;
1904
- max-width: 800px;
1905
+ max-width: 700px;
1905
1906
  }
1906
1907
 
1907
1908
  .content-cell {
@@ -2066,7 +2067,7 @@ const createNewsletterHtmlTemplate = (targets, options) => `<!DOCTYPE html>
2066
2067
  background-color: #faf7f3;
2067
2068
  }
2068
2069
 
2069
- @media screen and (max-width: 800px) {
2070
+ @media screen and (max-width: 700px) {
2070
2071
  .container {
2071
2072
  width: 100% !important;
2072
2073
  max-width: 100% !important;
@@ -2225,6 +2226,18 @@ const createNewsletterHtmlTemplate = (targets, options) => `<!DOCTYPE html>
2225
2226
  .dark-logo-inline {
2226
2227
  display: inline-block !important;
2227
2228
  }
2229
+
2230
+ .kras-newsletter .kras-header-title {
2231
+ color: #eeeeee !important;
2232
+ }
2233
+
2234
+ .kras-newsletter .kras-header-date {
2235
+ color: #bbbbbb !important;
2236
+ }
2237
+
2238
+ .kras-newsletter .kras-header-divider {
2239
+ border-top-color: #E59866 !important;
2240
+ }
2228
2241
  }
2229
2242
  </style>
2230
2243
  </head>
@@ -2233,42 +2246,37 @@ const createNewsletterHtmlTemplate = (targets, options) => `<!DOCTYPE html>
2233
2246
  <tr>
2234
2247
  <td bgcolor="#f4f4f4" align="center" style="-webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; mso-table-lspace: 0pt; mso-table-rspace: 0pt; padding: 20px 0;" class="dark-mode-bg">
2235
2248
  <!--[if (gte mso 9)|(IE)]>
2236
- <table align="center" border="0" cellspacing="0" cellpadding="0" width="800">
2249
+ <table align="center" border="0" cellspacing="0" cellpadding="0" width="700">
2237
2250
  <tr>
2238
- <td align="center" valign="top" width="800">
2251
+ <td align="center" valign="top" width="700">
2239
2252
  <![endif]-->
2240
- <table border="0" cellpadding="0" cellspacing="0" width="100%" style="-webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; mso-table-lspace: 0pt; mso-table-rspace: 0pt; max-width: 800px;" class="container" role="presentation">
2253
+ <table border="0" cellpadding="0" cellspacing="0" width="100%" style="-webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; mso-table-lspace: 0pt; mso-table-rspace: 0pt; max-width: 700px;" class="container" role="presentation">
2241
2254
  <tr>
2242
- <td bgcolor="#ffffff" align="left" class="content-cell dark-mode-content-bg" style="-webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; mso-table-lspace: 0pt; mso-table-rspace: 0pt; padding: 44px 44px 36px 44px; border-radius: 12px; box-shadow: 0 4px 18px rgba(0,0,0,0.07);">
2255
+ <td bgcolor="#ffffff" align="left" class="content-cell dark-mode-content-bg${options?.isKrasNewsletter ? ' kras-newsletter' : ''}" style="-webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; mso-table-lspace: 0pt; mso-table-rspace: 0pt; padding: 44px 44px 36px 44px; border-radius: 12px; box-shadow: 0 4px 18px rgba(0,0,0,0.07);">
2243
2256
  ${options?.isKrasNewsletter
2244
- ? `${krasHeaderHtml()}
2245
- <!-- 헤더: 제목/날짜 -->
2246
- <table cellpadding="0" cellspacing="0" width="100%" role="presentation" class="header-title-border" style="width: 100%; border-collapse: collapse; margin: 0 0 18px 0; mso-table-lspace: 0pt; mso-table-rspace: 0pt; margin-bottom: 32px; border: none; border-bottom: 3px solid #D2691E;">
2247
- <tr>
2248
- <td align="left" valign="baseline" class="header-dark-text" style="text-align: left; padding: 0 0 14px 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 26px; font-weight: bold; color: #111111; line-height: 1.2; border: none;">
2249
- 한국고고학회 뉴스레터
2250
- </td>
2251
- <td align="left" valign="baseline" class="header-dark-text" style="text-align: left; padding: 0 0 14px 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 24px; color: #333333; white-space: nowrap; border: none; width: 1px;" width="1">
2252
- ${options?.displayDate ?? ''}
2253
- </td>
2254
- </tr>
2255
- </table>
2257
+ ? `<!-- KRAS 50주년 헤더 -->
2258
+ <div style="text-align: center; margin-bottom: 28px;">
2259
+ <div style="width: 180px; min-height: 141px; display: inline-block; margin-bottom: 20px;"><img src="https://heripo.com/kras-50.png" width="180" alt="한국고고학회 50주년" style="-ms-interpolation-mode: bicubic; border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; display: block;" height="auto"></div>
2260
+ <div class="kras-header-title" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 30px; font-weight: bold; color: #111111; line-height: 1.2; margin-bottom: 8px;">한국고고학회 뉴스레터</div>
2261
+ <div class="kras-header-date" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; color: #666666; line-height: 1.5;">${options?.displayDate ?? ''}</div>
2262
+ </div>
2263
+ <hr class="kras-header-divider" style="border: 0; border-top: 2px solid #D2691E; margin: 0 0 32px 0;">
2256
2264
  `
2257
2265
  : `${heripoLogoHtml('12px')}
2258
2266
  `}
2259
2267
 
2260
2268
  ${options?.krasNewsMarkdown
2261
- ? safeMarkdown2Html(`## 학회 소식
2262
- ${options.krasNewsMarkdown}
2269
+ ? `<h2 style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 24px; font-weight: bold; line-height: 1.3; color: #D2691E; margin: 0 0 16px 0; letter-spacing: -0.2px; border-left: 5px solid #D2691E; padding-left: 12px; background: none;"><span style="display: inline-block; width: 26px; height: 26px; vertical-align: -4px; margin-right: 6px;"><img src="https://heripo.com/kras-symbol.png" width="26" height="26" alt="" style="border: 0; display: block;"></span>학회 소식</h2>` +
2270
+ safeMarkdown2Html(`${options.krasNewsMarkdown}
2263
2271
 
2264
2272
  ---
2265
2273
  `, {
2266
- window: new JSDOM('').window,
2267
- linkTargetBlank: true,
2268
- fixMalformedUrls: true,
2269
- fixBoldSyntax: true,
2270
- convertStrikethrough: true,
2271
- }).replaceAll('%7B%7B%7BRESEND_UNSUBSCRIBE_URL%7D%7D%7D', '{{{RESEND_UNSUBSCRIBE_URL}}}')
2274
+ window: new JSDOM('').window,
2275
+ linkTargetBlank: true,
2276
+ fixMalformedUrls: true,
2277
+ fixBoldSyntax: true,
2278
+ convertStrikethrough: true,
2279
+ }).replaceAll('%7B%7B%7BRESEND_UNSUBSCRIBE_URL%7D%7D%7D', '{{{RESEND_UNSUBSCRIBE_URL}}}')
2272
2280
  : ''}
2273
2281
 
2274
2282
  ${options?.heripolabNewsMarkdown
@@ -2340,12 +2348,11 @@ ${poweredByFooterHtml()}
2340
2348
 
2341
2349
  /**
2342
2350
  * Content generation provider implementation
2343
- * - LLM-based newsletter content generation (Google Generative AI)
2351
+ * - LLM-based newsletter content generation
2344
2352
  * - HTML template provisioning
2345
2353
  * - Newsletter persistence
2346
2354
  */
2347
2355
  class ContentGenerateProvider {
2348
- google;
2349
2356
  articleRepository;
2350
2357
  newsletterRepository;
2351
2358
  _issueOrder = null;
@@ -2354,11 +2361,10 @@ class ContentGenerateProvider {
2354
2361
  htmlTemplate;
2355
2362
  /** Newsletter brand name (defaults to config, can be overridden via constructor) */
2356
2363
  newsletterBrandName;
2357
- constructor(google, articleRepository, newsletterRepository, templateOptions, brandName) {
2358
- this.google = google;
2364
+ constructor(model, articleRepository, newsletterRepository, templateOptions, brandName) {
2359
2365
  this.articleRepository = articleRepository;
2360
2366
  this.newsletterRepository = newsletterRepository;
2361
- this.model = this.google('gemini-3-pro-preview');
2367
+ this.model = model;
2362
2368
  this.newsletterBrandName = brandName ?? newsletterConfig.brandName;
2363
2369
  this.htmlTemplate = {
2364
2370
  html: createNewsletterHtmlTemplate(createCrawlingTargetGroups().flatMap((group) => group.targets), templateOptions),
@@ -2554,7 +2560,10 @@ class TaskService {
2554
2560
  * ```typescript
2555
2561
  * const generator = createNewsletterGenerator({
2556
2562
  * openAIApiKey: process.env.OPENAI_API_KEY,
2557
- * googleGenerativeAIApiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY,
2563
+ * contentGeneration: {
2564
+ * provider: 'anthropic',
2565
+ * apiKey: process.env.ANTHROPIC_API_KEY,
2566
+ * },
2558
2567
  * taskRepository: new PrismaTaskRepository(prisma),
2559
2568
  * articleRepository: new PrismaArticleRepository(prisma),
2560
2569
  * tagRepository: new PrismaTagRepository(prisma),
@@ -2570,13 +2579,26 @@ class TaskService {
2570
2579
  * const newsletterId = await generator.generate();
2571
2580
  * ```
2572
2581
  */
2582
+ function createContentGenerationModel(config) {
2583
+ switch (config.provider) {
2584
+ case 'openai': {
2585
+ const provider = createOpenAI({ apiKey: config.apiKey });
2586
+ return provider(config.model ?? 'gpt-5.4');
2587
+ }
2588
+ case 'anthropic': {
2589
+ const provider = createAnthropic({ apiKey: config.apiKey });
2590
+ return provider(config.model ?? 'claude-sonnet-4-6');
2591
+ }
2592
+ case 'google': {
2593
+ const provider = createGoogleGenerativeAI({ apiKey: config.apiKey });
2594
+ return provider(config.model ?? 'gemini-3.1-pro-preview');
2595
+ }
2596
+ }
2597
+ }
2573
2598
  function createNewsletterGenerator(dependencies) {
2574
2599
  const openai = createOpenAI({
2575
2600
  apiKey: dependencies.openAIApiKey,
2576
2601
  });
2577
- const google = createGoogleGenerativeAI({
2578
- apiKey: dependencies.googleGenerativeAIApiKey,
2579
- });
2580
2602
  const dateService = new DateService(dependencies.publishDate);
2581
2603
  const taskService = new TaskService(dependencies.taskRepository);
2582
2604
  const crawlingProvider = new CrawlingProvider(dependencies.articleRepository, dependencies.customFetch);
@@ -2599,7 +2621,8 @@ function createNewsletterGenerator(dependencies) {
2599
2621
  };
2600
2622
  resolvedBrandName = '한국고고학회 뉴스레터';
2601
2623
  }
2602
- const contentGenerateProvider = new ContentGenerateProvider(google, dependencies.articleRepository, dependencies.newsletterRepository, templateOptions, resolvedBrandName);
2624
+ const contentModel = createContentGenerationModel(dependencies.contentGeneration);
2625
+ const contentGenerateProvider = new ContentGenerateProvider(contentModel, dependencies.articleRepository, dependencies.newsletterRepository, templateOptions, resolvedBrandName);
2603
2626
  return new GenerateNewsletter({
2604
2627
  contentOptions: resolvedContentOptions,
2605
2628
  dateService,
@@ -2629,7 +2652,10 @@ function createNewsletterGenerator(dependencies) {
2629
2652
  * ```typescript
2630
2653
  * const newsletterId = await generateNewsletter({
2631
2654
  * openAIApiKey: process.env.OPENAI_API_KEY,
2632
- * googleGenerativeAIApiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY,
2655
+ * contentGeneration: {
2656
+ * provider: 'anthropic',
2657
+ * apiKey: process.env.ANTHROPIC_API_KEY,
2658
+ * },
2633
2659
  * taskRepository: new PrismaTaskRepository(prisma),
2634
2660
  * articleRepository: new PrismaArticleRepository(prisma),
2635
2661
  * tagRepository: new PrismaTagRepository(prisma),
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": "@heripo/research-radar",
3
3
  "private": false,
4
4
  "type": "module",
5
- "version": "2.3.7",
5
+ "version": "3.1.0",
6
6
  "description": "AI-driven intelligence for Korean cultural heritage. This package serves as both a ready-to-use newsletter service and a practical implementation example for the LLM-Newsletter-Kit.",
7
7
  "main": "dist/index.cjs",
8
8
  "module": "dist/index.js",
@@ -47,6 +47,7 @@
47
47
  "author": "kimhongyeon",
48
48
  "license": "Apache-2.0",
49
49
  "dependencies": {
50
+ "@ai-sdk/anthropic": "^3.0.58",
50
51
  "@ai-sdk/google": "^3.0.43",
51
52
  "@ai-sdk/openai": "^3.0.41",
52
53
  "cheerio": "^1.2.0",
@@ -65,9 +66,9 @@
65
66
  "@trivago/prettier-plugin-sort-imports": "^6.0.2",
66
67
  "@types/express": "^5.0.6",
67
68
  "@types/jsdom": "^28.0.0",
68
- "@types/node": "^25.3.3",
69
+ "@types/node": "^25.3.5",
69
70
  "@types/turndown": "^5.0.6",
70
- "eslint": "^10.0.2",
71
+ "eslint": "^10.0.3",
71
72
  "eslint-plugin-unused-imports": "^4.4.1",
72
73
  "express": "^5.2.1",
73
74
  "prettier": "^3.8.1",