@heripo/research-radar 1.2.7 → 2.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/dist/index.cjs CHANGED
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ var google = require('@ai-sdk/google');
3
4
  var openai = require('@ai-sdk/openai');
4
5
  var core = require('@llm-newsletter-kit/core');
5
6
  var cheerio = require('cheerio');
@@ -2213,21 +2214,21 @@ const createNewsletterHtmlTemplate = (targets) => `<!DOCTYPE html>
2213
2214
 
2214
2215
  /**
2215
2216
  * Content generation provider implementation
2216
- * - LLM-based newsletter content generation
2217
+ * - LLM-based newsletter content generation (Google Generative AI)
2217
2218
  * - HTML template provisioning
2218
2219
  * - Newsletter persistence
2219
2220
  */
2220
2221
  class ContentGenerateProvider {
2221
- openai;
2222
+ google;
2222
2223
  articleRepository;
2223
2224
  newsletterRepository;
2224
2225
  _issueOrder = null;
2225
2226
  model;
2226
- constructor(openai, articleRepository, newsletterRepository) {
2227
- this.openai = openai;
2227
+ constructor(google, articleRepository, newsletterRepository) {
2228
+ this.google = google;
2228
2229
  this.articleRepository = articleRepository;
2229
2230
  this.newsletterRepository = newsletterRepository;
2230
- this.model = this.openai('gpt-5.1');
2231
+ this.model = this.google('gemini-3-pro-preview');
2231
2232
  }
2232
2233
  /** LLM temperature setting for content generation */
2233
2234
  temperature = llmConfig.generation.temperature;
@@ -2316,8 +2317,35 @@ class CrawlingProvider {
2316
2317
  * Date service implementation
2317
2318
  * - Provides current date and display date strings
2318
2319
  * - Always returns Korea Standard Time (KST, Asia/Seoul) regardless of server timezone
2320
+ * - Accepts optional publishDate to override the current date (e.g., for next-day publishing)
2319
2321
  */
2320
2322
  class DateService {
2323
+ targetDate;
2324
+ /**
2325
+ * @param publishDate - Optional ISO date string (YYYY-MM-DD) to use instead of current date.
2326
+ * When provided, the newsletter will use this date as its publication date.
2327
+ * @throws {Error} If publishDate is not in YYYY-MM-DD format or is not a real calendar date.
2328
+ */
2329
+ constructor(publishDate) {
2330
+ if (publishDate !== undefined) {
2331
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(publishDate)) {
2332
+ throw new Error(`Invalid publishDate format: "${publishDate}". Expected YYYY-MM-DD (e.g., "2025-02-12").`);
2333
+ }
2334
+ const date = new Date(publishDate + 'T00:00:00+09:00');
2335
+ // Round-trip check: format back to YYYY-MM-DD in KST and compare.
2336
+ // Catches invalid dates like "2025-02-30" that JS silently normalizes to "2025-03-02".
2337
+ const roundTrip = date.toLocaleDateString('en-CA', {
2338
+ timeZone: 'Asia/Seoul',
2339
+ });
2340
+ if (roundTrip !== publishDate) {
2341
+ throw new Error(`Invalid publishDate: "${publishDate}" is not a real calendar date.`);
2342
+ }
2343
+ this.targetDate = date;
2344
+ }
2345
+ else {
2346
+ this.targetDate = new Date();
2347
+ }
2348
+ }
2321
2349
  /**
2322
2350
  * Get current date in ISO format (YYYY-MM-DD)
2323
2351
  * - Always returns date in Korea Standard Time (UTC+9)
@@ -2326,7 +2354,7 @@ class DateService {
2326
2354
  getCurrentISODateString() {
2327
2355
  // Use Intl.DateTimeFormat to get date in Korea timezone
2328
2356
  // 'en-CA' locale returns YYYY-MM-DD format by default
2329
- const kstDate = new Date().toLocaleDateString('en-CA', {
2357
+ const kstDate = this.targetDate.toLocaleDateString('en-CA', {
2330
2358
  timeZone: 'Asia/Seoul',
2331
2359
  });
2332
2360
  return kstDate;
@@ -2343,7 +2371,7 @@ class DateService {
2343
2371
  month: 'long',
2344
2372
  day: 'numeric',
2345
2373
  });
2346
- return formatter.format(new Date());
2374
+ return formatter.format(this.targetDate);
2347
2375
  }
2348
2376
  }
2349
2377
 
@@ -2394,6 +2422,7 @@ class TaskService {
2394
2422
  * ```typescript
2395
2423
  * const generator = createNewsletterGenerator({
2396
2424
  * openAIApiKey: process.env.OPENAI_API_KEY,
2425
+ * googleGenerativeAIApiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY,
2397
2426
  * taskRepository: new PrismaTaskRepository(prisma),
2398
2427
  * articleRepository: new PrismaArticleRepository(prisma),
2399
2428
  * tagRepository: new PrismaTagRepository(prisma),
@@ -2413,11 +2442,14 @@ function createNewsletterGenerator(dependencies) {
2413
2442
  const openai$1 = openai.createOpenAI({
2414
2443
  apiKey: dependencies.openAIApiKey,
2415
2444
  });
2416
- const dateService = new DateService();
2445
+ const google$1 = google.createGoogleGenerativeAI({
2446
+ apiKey: dependencies.googleGenerativeAIApiKey,
2447
+ });
2448
+ const dateService = new DateService(dependencies.publishDate);
2417
2449
  const taskService = new TaskService(dependencies.taskRepository);
2418
2450
  const crawlingProvider = new CrawlingProvider(dependencies.articleRepository);
2419
2451
  const analysisProvider = new AnalysisProvider(openai$1, dependencies.articleRepository, dependencies.tagRepository);
2420
- const contentGenerateProvider = new ContentGenerateProvider(openai$1, dependencies.articleRepository, dependencies.newsletterRepository);
2452
+ const contentGenerateProvider = new ContentGenerateProvider(google$1, dependencies.articleRepository, dependencies.newsletterRepository);
2421
2453
  return new core.GenerateNewsletter({
2422
2454
  contentOptions,
2423
2455
  dateService,
@@ -2447,6 +2479,7 @@ function createNewsletterGenerator(dependencies) {
2447
2479
  * ```typescript
2448
2480
  * const newsletterId = await generateNewsletter({
2449
2481
  * openAIApiKey: process.env.OPENAI_API_KEY,
2482
+ * googleGenerativeAIApiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY,
2450
2483
  * taskRepository: new PrismaTaskRepository(prisma),
2451
2484
  * articleRepository: new PrismaArticleRepository(prisma),
2452
2485
  * 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,5 +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 } from '@llm-newsletter-kit/core';
2
2
  import { OpenAIProvider } from '@ai-sdk/openai';
3
+ import { GoogleGenerativeAIProvider } from '@ai-sdk/google';
3
4
 
4
5
  /**
5
6
  * Repository interface for task management
@@ -88,11 +89,9 @@ interface NewsletterRepository {
88
89
  }
89
90
 
90
91
  /**
91
- * This implementation is currently hardcoded to use OpenAI.
92
- * To use a different LLM provider, you need to modify:
93
- * - This file: Change `createOpenAI` import and initialization
94
- * - src/providers/analysis.provider.ts: Change OpenAIProvider type and model names
95
- * - src/providers/content-generate.provider.ts: Change OpenAIProvider type and model name
92
+ * Uses two LLM providers:
93
+ * - OpenAI (gpt-5-mini, gpt-5.1): Article analysis (tag classification, image analysis, importance scoring)
94
+ * - Google Generative AI (gemini-3-pro-preview): Newsletter content generation
96
95
  *
97
96
  * For details on switching providers, see README.md section:
98
97
  * "⚠️ Fork하여 나만의 뉴스레터 만들기 > 4. LLM 프로바이더 변경"
@@ -113,8 +112,10 @@ interface PreviewNewsletterOptions {
113
112
  * Newsletter generator dependencies interface
114
113
  */
115
114
  interface NewsletterGeneratorDependencies {
116
- /** OpenAI API key */
115
+ /** OpenAI API key (used for article analysis: tag classification, image analysis, importance scoring) */
117
116
  openAIApiKey: string;
117
+ /** Google Generative AI API key (used for newsletter content generation) */
118
+ googleGenerativeAIApiKey: string;
118
119
  /** Task management repository */
119
120
  taskRepository: TaskRepository;
120
121
  /** Article management repository */
@@ -127,6 +128,13 @@ interface NewsletterGeneratorDependencies {
127
128
  logger?: AppLogger;
128
129
  /** Preview email configuration (optional) */
129
130
  previewNewsletter?: PreviewNewsletterOptions;
131
+ /**
132
+ * Publication date override in ISO format (optional).
133
+ * When provided, this date is used as the newsletter's publication date instead of the current date.
134
+ * Useful for generating a newsletter today but publishing it on a future date.
135
+ * @example "2025-02-12"
136
+ */
137
+ publishDate?: string;
130
138
  }
131
139
  /**
132
140
  * Newsletter generation execution function
@@ -138,6 +146,7 @@ interface NewsletterGeneratorDependencies {
138
146
  * ```typescript
139
147
  * const newsletterId = await generateNewsletter({
140
148
  * openAIApiKey: process.env.OPENAI_API_KEY,
149
+ * googleGenerativeAIApiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY,
141
150
  * taskRepository: new PrismaTaskRepository(prisma),
142
151
  * articleRepository: new PrismaArticleRepository(prisma),
143
152
  * tagRepository: new PrismaTagRepository(prisma),
@@ -151,8 +160,16 @@ declare function generateNewsletter(dependencies: NewsletterGeneratorDependencie
151
160
  * Date service implementation
152
161
  * - Provides current date and display date strings
153
162
  * - Always returns Korea Standard Time (KST, Asia/Seoul) regardless of server timezone
163
+ * - Accepts optional publishDate to override the current date (e.g., for next-day publishing)
154
164
  */
155
165
  declare class DateService implements DateService$1 {
166
+ private readonly targetDate;
167
+ /**
168
+ * @param publishDate - Optional ISO date string (YYYY-MM-DD) to use instead of current date.
169
+ * When provided, the newsletter will use this date as its publication date.
170
+ * @throws {Error} If publishDate is not in YYYY-MM-DD format or is not a real calendar date.
171
+ */
172
+ constructor(publishDate?: string);
156
173
  /**
157
174
  * Get current date in ISO format (YYYY-MM-DD)
158
175
  * - Always returns date in Korea Standard Time (UTC+9)
@@ -231,17 +248,17 @@ declare class AnalysisProvider implements AnalysisProvider$1 {
231
248
 
232
249
  /**
233
250
  * Content generation provider implementation
234
- * - LLM-based newsletter content generation
251
+ * - LLM-based newsletter content generation (Google Generative AI)
235
252
  * - HTML template provisioning
236
253
  * - Newsletter persistence
237
254
  */
238
255
  declare class ContentGenerateProvider implements ContentGenerateProvider$1 {
239
- private readonly openai;
256
+ private readonly google;
240
257
  private readonly articleRepository;
241
258
  private readonly newsletterRepository;
242
259
  private _issueOrder;
243
- model: ReturnType<OpenAIProvider>;
244
- constructor(openai: OpenAIProvider, articleRepository: ArticleRepository, newsletterRepository: NewsletterRepository);
260
+ model: ReturnType<GoogleGenerativeAIProvider>;
261
+ constructor(google: GoogleGenerativeAIProvider, articleRepository: ArticleRepository, newsletterRepository: NewsletterRepository);
245
262
  /** LLM temperature setting for content generation */
246
263
  temperature: number;
247
264
  /** Newsletter brand name */
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { createGoogleGenerativeAI } from '@ai-sdk/google';
1
2
  import { createOpenAI } from '@ai-sdk/openai';
2
3
  import { DateType, GenerateNewsletter } from '@llm-newsletter-kit/core';
3
4
  import * as cheerio from 'cheerio';
@@ -2192,21 +2193,21 @@ const createNewsletterHtmlTemplate = (targets) => `<!DOCTYPE html>
2192
2193
 
2193
2194
  /**
2194
2195
  * Content generation provider implementation
2195
- * - LLM-based newsletter content generation
2196
+ * - LLM-based newsletter content generation (Google Generative AI)
2196
2197
  * - HTML template provisioning
2197
2198
  * - Newsletter persistence
2198
2199
  */
2199
2200
  class ContentGenerateProvider {
2200
- openai;
2201
+ google;
2201
2202
  articleRepository;
2202
2203
  newsletterRepository;
2203
2204
  _issueOrder = null;
2204
2205
  model;
2205
- constructor(openai, articleRepository, newsletterRepository) {
2206
- this.openai = openai;
2206
+ constructor(google, articleRepository, newsletterRepository) {
2207
+ this.google = google;
2207
2208
  this.articleRepository = articleRepository;
2208
2209
  this.newsletterRepository = newsletterRepository;
2209
- this.model = this.openai('gpt-5.1');
2210
+ this.model = this.google('gemini-3-pro-preview');
2210
2211
  }
2211
2212
  /** LLM temperature setting for content generation */
2212
2213
  temperature = llmConfig.generation.temperature;
@@ -2295,8 +2296,35 @@ class CrawlingProvider {
2295
2296
  * Date service implementation
2296
2297
  * - Provides current date and display date strings
2297
2298
  * - Always returns Korea Standard Time (KST, Asia/Seoul) regardless of server timezone
2299
+ * - Accepts optional publishDate to override the current date (e.g., for next-day publishing)
2298
2300
  */
2299
2301
  class DateService {
2302
+ targetDate;
2303
+ /**
2304
+ * @param publishDate - Optional ISO date string (YYYY-MM-DD) to use instead of current date.
2305
+ * When provided, the newsletter will use this date as its publication date.
2306
+ * @throws {Error} If publishDate is not in YYYY-MM-DD format or is not a real calendar date.
2307
+ */
2308
+ constructor(publishDate) {
2309
+ if (publishDate !== undefined) {
2310
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(publishDate)) {
2311
+ throw new Error(`Invalid publishDate format: "${publishDate}". Expected YYYY-MM-DD (e.g., "2025-02-12").`);
2312
+ }
2313
+ const date = new Date(publishDate + 'T00:00:00+09:00');
2314
+ // Round-trip check: format back to YYYY-MM-DD in KST and compare.
2315
+ // Catches invalid dates like "2025-02-30" that JS silently normalizes to "2025-03-02".
2316
+ const roundTrip = date.toLocaleDateString('en-CA', {
2317
+ timeZone: 'Asia/Seoul',
2318
+ });
2319
+ if (roundTrip !== publishDate) {
2320
+ throw new Error(`Invalid publishDate: "${publishDate}" is not a real calendar date.`);
2321
+ }
2322
+ this.targetDate = date;
2323
+ }
2324
+ else {
2325
+ this.targetDate = new Date();
2326
+ }
2327
+ }
2300
2328
  /**
2301
2329
  * Get current date in ISO format (YYYY-MM-DD)
2302
2330
  * - Always returns date in Korea Standard Time (UTC+9)
@@ -2305,7 +2333,7 @@ class DateService {
2305
2333
  getCurrentISODateString() {
2306
2334
  // Use Intl.DateTimeFormat to get date in Korea timezone
2307
2335
  // 'en-CA' locale returns YYYY-MM-DD format by default
2308
- const kstDate = new Date().toLocaleDateString('en-CA', {
2336
+ const kstDate = this.targetDate.toLocaleDateString('en-CA', {
2309
2337
  timeZone: 'Asia/Seoul',
2310
2338
  });
2311
2339
  return kstDate;
@@ -2322,7 +2350,7 @@ class DateService {
2322
2350
  month: 'long',
2323
2351
  day: 'numeric',
2324
2352
  });
2325
- return formatter.format(new Date());
2353
+ return formatter.format(this.targetDate);
2326
2354
  }
2327
2355
  }
2328
2356
 
@@ -2373,6 +2401,7 @@ class TaskService {
2373
2401
  * ```typescript
2374
2402
  * const generator = createNewsletterGenerator({
2375
2403
  * openAIApiKey: process.env.OPENAI_API_KEY,
2404
+ * googleGenerativeAIApiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY,
2376
2405
  * taskRepository: new PrismaTaskRepository(prisma),
2377
2406
  * articleRepository: new PrismaArticleRepository(prisma),
2378
2407
  * tagRepository: new PrismaTagRepository(prisma),
@@ -2392,11 +2421,14 @@ function createNewsletterGenerator(dependencies) {
2392
2421
  const openai = createOpenAI({
2393
2422
  apiKey: dependencies.openAIApiKey,
2394
2423
  });
2395
- const dateService = new DateService();
2424
+ const google = createGoogleGenerativeAI({
2425
+ apiKey: dependencies.googleGenerativeAIApiKey,
2426
+ });
2427
+ const dateService = new DateService(dependencies.publishDate);
2396
2428
  const taskService = new TaskService(dependencies.taskRepository);
2397
2429
  const crawlingProvider = new CrawlingProvider(dependencies.articleRepository);
2398
2430
  const analysisProvider = new AnalysisProvider(openai, dependencies.articleRepository, dependencies.tagRepository);
2399
- const contentGenerateProvider = new ContentGenerateProvider(openai, dependencies.articleRepository, dependencies.newsletterRepository);
2431
+ const contentGenerateProvider = new ContentGenerateProvider(google, dependencies.articleRepository, dependencies.newsletterRepository);
2400
2432
  return new GenerateNewsletter({
2401
2433
  contentOptions,
2402
2434
  dateService,
@@ -2426,6 +2458,7 @@ function createNewsletterGenerator(dependencies) {
2426
2458
  * ```typescript
2427
2459
  * const newsletterId = await generateNewsletter({
2428
2460
  * openAIApiKey: process.env.OPENAI_API_KEY,
2461
+ * googleGenerativeAIApiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY,
2429
2462
  * taskRepository: new PrismaTaskRepository(prisma),
2430
2463
  * articleRepository: new PrismaArticleRepository(prisma),
2431
2464
  * 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": "1.2.7",
5
+ "version": "2.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",
@@ -42,19 +42,20 @@
42
42
  "author": "kimhongyeon",
43
43
  "license": "Apache-2.0",
44
44
  "dependencies": {
45
- "@ai-sdk/openai": "^3.0.25",
45
+ "@ai-sdk/google": "^3.0.22",
46
+ "@ai-sdk/openai": "^3.0.26",
46
47
  "cheerio": "^1.2.0",
47
48
  "turndown": "^7.2.2"
48
49
  },
49
50
  "peerDependencies": {
50
- "@llm-newsletter-kit/core": "~1.1.0"
51
+ "@llm-newsletter-kit/core": "~1.2.0"
51
52
  },
52
53
  "devDependencies": {
53
54
  "@eslint/js": "^9.39.2",
54
- "@llm-newsletter-kit/core": "^1.1.5",
55
+ "@llm-newsletter-kit/core": "^1.2.0",
55
56
  "@trivago/prettier-plugin-sort-imports": "^6.0.2",
56
57
  "@types/express": "^5.0.6",
57
- "@types/node": "^25.2.0",
58
+ "@types/node": "^25.2.1",
58
59
  "@types/turndown": "^5.0.6",
59
60
  "eslint": "^9.39.2",
60
61
  "eslint-plugin-unused-imports": "^4.3.0",