@heripo/research-radar 5.0.5 → 5.0.6

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
@@ -15,15 +15,15 @@ English | [한국어](./README-ko.md)
15
15
 
16
16
  An AI-powered newsletter service for Korean cultural heritage. Built on [`@llm-newsletter-kit/core`](https://github.com/heripo-lab/llm-newsletter-kit-core), it's both a production service ([live at heripo.app](https://heripo.app/research-radar/subscribe)) and a reference implementation showing how to build automated newsletters with LLMs.
17
17
 
18
- **Production metrics**:
19
- - **Cost**: $0.2-1 USD per issue
20
- - **Operation**: Fully autonomous 24/7 (no human intervention)
21
- - **Engagement**: 15% CTR
18
+ This package provides crawling, analysis, content generation, and email templates. The hosted service supplies database repositories, scheduling, subscriber management, and email delivery separately.
19
+
20
+ Previously reported service metrics were $0.2–1 per issue and 15% CTR. These are historical figures, not benchmarks for the current models or deployment.
22
21
 
23
22
  **Technical highlights**:
23
+
24
24
  - Type-safe TypeScript with strict interfaces
25
25
  - Provider pattern for swapping components (Crawling/Analysis/Content/Email)
26
- - 66 crawling targets across heritage agencies, museums, academic societies
26
+ - 59 active crawling targets across heritage agencies, museums, academic societies
27
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
 
@@ -33,7 +33,7 @@ An AI-powered newsletter service for Korean cultural heritage. Built on [`@llm-n
33
33
 
34
34
  Created by archaeologist-turned-engineer Hongyeon Kim to answer: "Why must research rely on labor-intensive manual work?"
35
35
 
36
- A personal script evolved into a production service after completing research on [Archaeological Informatization Using LLMs](https://poc.heripo.org). This repository open-sources the running service so developers can build
36
+ A personal script evolved into a production service after completing research on [Archaeological Informatization Using LLMs](https://poc.heripo.org). This repository open-sources the running service so developers can build
37
37
  domain-specific newsletters without starting from scratch.
38
38
 
39
39
  ## License
@@ -67,63 +67,86 @@ For academic publications:
67
67
  ## Installation
68
68
 
69
69
  ```bash
70
- npm install @heripo/research-radar @llm-newsletter-kit/core
70
+ npm install @heripo/research-radar '@llm-newsletter-kit/core@~3.0.0'
71
71
  ```
72
72
 
73
- **Requirements**: Node.js >= 24, OpenAI API key, content generation API key (OpenAI / Anthropic / Google)
73
+ **Requirements**: Node.js >= 24 and an ESM application. The package exports `dist/index.js` and TypeScript declarations (`dist/index.d.ts`), with a JavaScript sourcemap. The core engine is a peer dependency; the current supported range is `~3.0.0`.
74
74
 
75
- **Note**: `@llm-newsletter-kit/core` is a peer dependency and must be installed separately.
75
+ Article analysis requires an OpenAI API key. Content generation requires a key for the selected provider (OpenAI / Anthropic / Google); OpenAI can use the same key for both. Keys are passed explicitly to the library; load environment variables in your application.
76
76
 
77
77
  ## Quick Start
78
78
 
79
+ Implement the four repository interfaces for your storage layer, then call this application-level wrapper. The repositories in this example are supplied by the caller; no database adapter is bundled.
80
+
79
81
  ```typescript
80
- import { generateNewsletter } from '@heripo/research-radar';
81
-
82
- const newsletterId = await generateNewsletter({
83
- openAIApiKey: process.env.OPENAI_API_KEY,
84
- contentGeneration: {
85
- provider: 'anthropic', // 'openai' | 'anthropic' | 'google'
86
- apiKey: process.env.ANTHROPIC_API_KEY,
87
- // model: 'claude-sonnet-4-6', // optional, uses sensible default
88
- },
82
+ import {
83
+ type ArticleRepository,
84
+ type NewsletterRepository,
85
+ type TagRepository,
86
+ type TaskRepository,
87
+ generateNewsletter,
88
+ } from '@heripo/research-radar';
89
+
90
+ export async function runNewsletter(repositories: {
91
+ taskRepository: TaskRepository;
92
+ articleRepository: ArticleRepository;
93
+ tagRepository: TagRepository;
94
+ newsletterRepository: NewsletterRepository;
95
+ }) {
96
+ const apiKey = process.env.OPENAI_API_KEY;
97
+ if (!apiKey) throw new Error('OPENAI_API_KEY is required');
98
+
99
+ const newsletterId = await generateNewsletter({
100
+ ...repositories,
101
+ openAIApiKey: apiKey,
102
+ contentGeneration: { provider: 'openai', apiKey },
103
+ logger: console,
104
+ });
105
+
106
+ if (newsletterId === null) {
107
+ console.log('No newsletter was created for this run.');
108
+ return null;
109
+ }
110
+
111
+ console.log('Saved newsletter:', newsletterId);
112
+ return newsletterId;
113
+ }
114
+ ```
89
115
 
90
- // Implement these repository interfaces (see src/types/dependencies.ts)
91
- taskRepository: {
92
- createTask: async () => db.tasks.create({ status: 'running' }),
93
- completeTask: async (id) => db.tasks.update(id, { status: 'completed' }),
94
- },
116
+ `generateNewsletter()` returns `Promise<string | number | null>`: the saved newsletter ID, or `null` when no issue is created (for example, when publication criteria are not met). Generation errors can reject the promise. The internal `createNewsletterGenerator()` factory is not exported.
95
117
 
96
- articleRepository: {
97
- findByUrls: async (urls) => db.articles.findByUrls(urls),
98
- saveCrawledArticles: async (articles, ctx) => db.articles.save(articles, ctx),
99
- findUnscoredArticles: async () => db.articles.findUnscored(),
100
- updateAnalysis: async (article) => db.articles.updateAnalysis(article),
101
- findCandidatesForNewsletter: async () => db.articles.findCandidates(),
102
- },
118
+ ### Repository contracts
103
119
 
104
- tagRepository: {
105
- findAllTags: async () => db.tags.findAll(),
106
- },
120
+ All interfaces are exported from the package root and defined in [src/types/dependencies.ts](./src/types/dependencies.ts). Article and newsletter data types come from `@llm-newsletter-kit/core`.
107
121
 
108
- newsletterRepository: {
109
- getNextIssueOrder: async () => db.newsletters.getNextOrder(),
110
- saveNewsletter: async (data) => db.newsletters.save(data),
111
- },
122
+ | Repository | Method | Required result |
123
+ | ---------------------- | ---------------------------------------------- | -------------------------------------------------------------------------------- |
124
+ | `TaskRepository` | `createTask()` | `Promise<number>` — task ID, not a task record |
125
+ | | `completeTask(taskId)` | `Promise<void>` |
126
+ | `ArticleRepository` | `findByUrls(urls)` | `Promise<ParsedTarget[]>` for deduplication |
127
+ | | `saveCrawledArticles(articles, context)` | `Promise<number>` — saved count; preserve task, target group, and target context |
128
+ | | `findUnscoredArticles()` | `Promise<UnscoredArticle[]>` |
129
+ | | `updateAnalysis(article)` | `Promise<void>` |
130
+ | | `findCandidatesForNewsletter()` | `Promise<ArticleForGenerateContent[]>` |
131
+ | `TagRepository` | `findAllTags()` | `Promise<string[]>` |
132
+ | `NewsletterRepository` | `getNextIssueOrder()` | `Promise<number>` |
133
+ | | `saveNewsletter({ newsletter, usedArticles })` | `Promise<{ id: string \| number }>` |
112
134
 
113
- // Optional parameters:
114
- logger: console,
115
- publishDate: '2026-02-20', // Override publication date (ISO format)
116
- templateOptions: { /* ... */ }, // Newsletter template customization
117
- customFetch: proxyFetch, // Custom fetch for proxy-based crawling
118
- previewNewsletter: {
119
- fetchNewsletterForPreview: async () => db.newsletters.latest(),
120
- emailService: resendEmailService,
121
- emailMessage: { from: 'news@example.com', to: 'preview@example.com' },
122
- },
123
- });
124
- ```
135
+ Candidate selection belongs to your repository. Persist `usedArticles` associations as needed to exclude previously published content. Issue numbering is initialized before the pipeline starts; coordinate concurrent runs in your application or database. `TaskService` tracks an active task within one service instance, not across processes or separate `generateNewsletter()` calls.
136
+
137
+ ### Optional generation settings
138
+
139
+ | Option | Behavior |
140
+ | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
141
+ | `logger` | Core `AppLogger` implementation, such as `console` |
142
+ | `publishDate` | A real calendar date in `YYYY-MM-DD` format; invalid values throw. Defaults to the current date in `Asia/Seoul` (KST), regardless of server timezone |
143
+ | `customFetch` | A `typeof fetch` implementation for crawling and parser API requests, such as a proxy adapter; does not configure LLM requests |
144
+ | `templateOptions` | Default or KRAS newsletter branding and Markdown sections (see below) |
145
+ | `previewNewsletter` | Fetch a saved `Newsletter` and send it through a supplied core `EmailService` |
125
146
 
126
- **Repository interfaces** are defined in `src/types/dependencies.ts`. Each method signature includes JSDoc with expected input/output types.
147
+ `previewNewsletter` requires `fetchNewsletterForPreview: () => Promise<Newsletter>`, `emailService` (with `send(message)`), and `emailMessage` (core `EmailMessage` without `subject`, `html`, or `text`). The core fills those three fields and skips preview delivery when no newsletter was created. Ensure the callback fetches the issue saved by this run.
148
+
149
+ Production integration must also supply a scheduler, subscriber storage, bulk delivery, and unsubscribe handling. The daily parser health-check workflow does not generate or distribute newsletters.
127
150
 
128
151
  ## Architecture
129
152
 
@@ -136,41 +159,134 @@ const newsletterId = await generateNewsletter({
136
159
 
137
160
  Uses the **Provider-Service pattern** from `@llm-newsletter-kit/core`. See [core docs](https://github.com/heripo-lab/llm-newsletter-kit-core#architecture--flow) for flow diagrams.
138
161
 
139
- ## Components
162
+ ## Configuration and models
163
+
164
+ Defaults below describe the checked-in code, not provider recommendations. This package uses AI SDK 7 and the OpenAI, Anthropic, and Google SDK adapters.
165
+
166
+ | Stage | Provider | Default model |
167
+ | ------------------ | --------- | ------------------------ |
168
+ | Tag classification | OpenAI | `gpt-5-mini` |
169
+ | Image analysis | OpenAI | `gpt-5.1` |
170
+ | Importance scoring | OpenAI | `gpt-5.1` |
171
+ | Content generation | OpenAI | `gpt-5.4` |
172
+ | Content generation | Anthropic | `claude-sonnet-4-6` |
173
+ | Content generation | Google | `gemini-3.1-pro-preview` |
174
+
175
+ Select content generation with `contentGeneration: { provider, apiKey, model? }`. `model` overrides that provider's default. Analysis models are configured in [analysis.provider.ts](./src/providers/analysis.provider.ts).
176
+
177
+ [src/config/index.ts](./src/config/index.ts) defines Korean output (`outputLanguage: '한국어'`), the cultural heritage domain (`expertField: ['문화유산']`), brand name, `subscribePageUrl`, LLM `maxRetries: 5`, chain `stopAfterAttempt: 3`, and generation `temperature: 0.3`. Publication settings are `minimumArticleCountForIssue: 5` and `priorityArticleScoreThreshold: 8`; the core engine evaluates them. In the locked core 3.0.4 implementation, the count check skips **5 or fewer** candidates unless at least one has importance score >= 8. An empty candidate list is always skipped.
178
+
179
+ ## Crawling targets and parsers
180
+
181
+ [src/config/crawling-targets.ts](./src/config/crawling-targets.ts) currently defines these board targets (multiple boards may belong to one organization):
182
+
183
+ | Group | Active | Commented out |
184
+ | ---------- | -----: | ------------: |
185
+ | News | 48 | 10 |
186
+ | Business | 4 | 0 |
187
+ | Employment | 7 | 3 |
188
+ | **Total** | **59** | **13** |
189
+
190
+ The 13 commented targets are excluded from runtime configuration: one excavation status board is marked as low-value fragmented data, and 12 museum boards are marked as restricted by robots.txt. Their parser code remains in the repository.
191
+
192
+ Sources include the Korea Heritage Service, National Research Institute of Cultural Heritage, National Research Institute of Maritime Heritage, Korea Heritage Agency, Korea Association of Archaeological Heritage, archaeological societies, and national museums.
193
+
194
+ [src/parsers/](./src/parsers/) contains 20 organization-specific parser modules plus shared date and URL utilities. List parsers return `ParsedTargetListItem[]` (title, date, detail URL, date type, and optional source ID); detail parsers return Markdown `detailContent` and attachment/image flags. Parsers can be synchronous or asynchronous. KRAS, Yeongnam Archaeological Society, and maritime heritage sources use additional API requests for client-rendered content.
195
+
196
+ `CrawlingProvider` uses a maximum concurrency of 5 and wraps the supplied fetch to route KRAS public detail URLs to its detail API, while retaining public URLs in article metadata. When constructing your own pipeline, use the provider's fetch together with its target groups.
197
+
198
+ To display the active sources without crawling:
199
+
200
+ ```typescript
201
+ import { getSourceList } from '@heripo/research-radar';
202
+
203
+ const groups = getSourceList(); // [{ id, name, sources: [{ id, name, url }] }]
204
+ ```
205
+
206
+ `createCrawlingTargetGroups(customFetch?)`, `getSourceList()`, `contentOptions`, `newsletterConfig`, and `llmConfig` are public exports. The package also exports the three provider classes, `DateService`, `TaskService`, and their public configuration/dependency types through [src/index.ts](./src/index.ts).
207
+
208
+ ## Email templates
140
209
 
141
- **Config** (`src/config/`): Brand, language, LLM settings
210
+ [src/templates/](./src/templates/) contains the responsive newsletter template, welcome email template, and shared logos, introduction, sanitization, and footer components. Templates include light/dark mode styles and heripo/KRAS variants. Markdown sections are converted with `safe-markdown2html`; welcome email names are sanitized with DOMPurify and CSS is inlined with `juice`.
142
211
 
143
- **Targets** (`src/config/crawling-targets.ts`): 66 sources (News 52, Business 4, Employment 10)
212
+ `NewsletterTemplateOptions` supports:
144
213
 
145
- **Parsers** (`src/parsers/`): Custom extractors per organization
214
+ | Option | Purpose |
215
+ | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
216
+ | `isKrasNewsletter` | Enable KRAS branding, the 50th-anniversary header, archaeology-first expertise, free-form introduction, and the brand name `한국고고학회 뉴스레터` |
217
+ | `krasNewsMarkdown` | Society news before generated content |
218
+ | `heripolabNewsMarkdown` | heripo lab news before generated content |
219
+ | `krasNoticeMarkdown` | Society notices after generated content |
220
+ | `titleContext` | Title-generation context, only when `isKrasNewsletter: true`; an empty string is ignored |
221
+ | `displayDate` | Header date; `generateNewsletter()` replaces it with the date resolved by `DateService` when template options are supplied |
146
222
 
147
- **Templates** (`src/templates/`): `newsletter-html.ts` (responsive email with light/dark mode), `welcome-html.ts` (`generateWelcomeHTML()` async), `shared.ts` (shared HTML components)
223
+ The three Markdown sections are available in either branding mode.
224
+
225
+ ```typescript
226
+ import {
227
+ type NewsletterTemplateOptions,
228
+ generateWelcomeHTML,
229
+ } from '@heripo/research-radar';
230
+
231
+ const templateOptions: NewsletterTemplateOptions = {
232
+ isKrasNewsletter: true,
233
+ titleContext: '한국고고학전국대회',
234
+ krasNewsMarkdown: '학회 소식을 입력하세요.',
235
+ krasNoticeMarkdown: '학회 공지사항을 입력하세요.',
236
+ };
237
+
238
+ const welcomeHtml = await generateWelcomeHTML('subscriber-id', '홍길동', {
239
+ isKrasNewsletter: true,
240
+ siteUrl: 'https://heripo.app',
241
+ });
242
+ ```
243
+
244
+ `generateWelcomeHTML(id, name, options?)` returns `Promise<string>` and only renders HTML. `siteUrl` defaults to `https://heripo.app`; unsubscribe links use `/research-radar/unsubscribe?id=...`. Newsletter templates retain Resend's `{{{RESEND_UNSUBSCRIBE_URL}}}` marker, which your delivery integration must resolve or replace. The raw newsletter template factory and shared HTML helpers are internal, not package-root exports.
148
245
 
149
246
  ## Development commands
150
247
 
151
248
  ```bash
249
+ # Install development dependencies from the lockfile
250
+ npm ci
251
+
152
252
  # build
253
+ npm run clean # remove dist/
153
254
  npm run build # clean dist/ and build with Rollup (ESM + types)
154
255
 
155
256
  # type-check & lint
156
257
  npm run lint # lint source files
157
258
  npm run lint:fix # lint with autofix
259
+ npm run lint:ci # quiet CI lint
158
260
  npm run typecheck # TypeScript type-check
159
261
 
160
262
  # formatting
161
- npm run format # Prettier formatting
263
+ npm run format # format src/ with Prettier
264
+ npm run format:check # check src/ formatting
162
265
  ```
163
266
 
267
+ The build emits ESM, declarations, and a JavaScript sourcemap; runtime dependencies remain external. CI runs `npm ci`, `format:check`, `lint:ci`, `typecheck`, and `build` on Node.js 24.x for pull requests and manual dispatch. Formatting scripts cover `src/`; to check the READMEs explicitly, run `npx prettier --check README.md README-ko.md`. There is no `npm test` script.
268
+
269
+ For maintainers, `release` publishes to npm, while `release:patch`, `release:minor`, and `release:major` bump the version and publish. The version hooks build first and push commits/tags; `prepublishOnly` builds before publishing.
270
+
164
271
  ### Crawler Debugger
165
272
 
166
273
  A web-based tool for testing crawling parsers during development. Built with Express.js and vanilla HTML/CSS/JS to minimize dependencies.
167
274
 
168
275
  ```bash
169
276
  npm run dev:crawler # Start at http://localhost:3333
170
- npm run dev:crawler:proxy # Start with proxy support (uses .env)
277
+ npm run dev:crawler:proxy # Load dev-tools/crawler-debugger/.env
278
+ ```
279
+
280
+ For proxy use, create the gitignored `dev-tools/crawler-debugger/.env` file with your proxy endpoint:
281
+
282
+ ```dotenv
283
+ PROXY_URL=http://127.0.0.1:8080
171
284
  ```
172
285
 
286
+ The library itself receives a `customFetch` function; it does not read `PROXY_URL`.
287
+
173
288
  **Features**:
289
+
174
290
  - Test `parseList()` and `parseDetail()` parsers via web UI
175
291
  - View raw HTML source for debugging
176
292
  - Copy parsed results as JSON
@@ -185,7 +301,7 @@ Preview rendered newsletter HTML with sample content.
185
301
  npm run dev:newsletter-preview # Start at http://localhost:3334
186
302
  ```
187
303
 
188
- Query params: `?kras=true` (KRAS mode), `?krasNews=true` (KRAS news section), `?heripolabNews=true` (heripo lab news section)
304
+ Use the UI controls, or open `/api/preview?kras=true&krasNews=true&krasNotice=true&heripolabNews=true` on port 3334. These query parameters belong to `/api/preview`, not the root UI URL. Each section switch includes sample Markdown; this preview does not call an LLM.
189
305
 
190
306
  ### Welcome Email Preview
191
307
 
@@ -195,7 +311,7 @@ Preview rendered welcome email HTML.
195
311
  npm run dev:welcome-preview # Start at http://localhost:3335
196
312
  ```
197
313
 
198
- Query params: `?kras=true` (KRAS mode), `?name=홍길동` (subscriber name)
314
+ Use the UI controls, or open `/api/preview?kras=true&name=홍길동` on port 3335. No API key is needed for either HTML preview tool.
199
315
 
200
316
  ### Parser Health-Check
201
317
 
@@ -203,19 +319,20 @@ CLI tool that validates all active crawling parsers against live websites. Detec
203
319
 
204
320
  ```bash
205
321
  npm run health-check # Run health-check
206
- npm run health-check:proxy # Run with proxy support (uses .env)
322
+ npm run health-check:proxy # Load the same .env and pass --proxy
207
323
  npm run health-check -- --skip-khs-excavation # Skip KHS excavation report/site-open targets
208
324
  ```
209
325
 
210
326
  **What it checks per target**:
211
- - `parseList()`: Returns non-empty array with valid title, date, and detailUrl
212
- - `parseDetail()`: Returns non-empty detailContent (20+ chars)
213
327
 
214
- Use `--skip-target=<id-or-name>` to exclude additional known-flaky targets from a run.
328
+ - `parseList()`: Non-empty array; checks the first item for non-empty title/date and a detailUrl starting with `http`
329
+ - `parseDetail()`: Fetches the first detail item and checks trimmed `detailContent` length >= 20
330
+
331
+ Use repeatable `--skip-target=<id-or-name>` or `--skip-target <id-or-name>` to exclude targets by exact ID or name. `npm run health-check -- --help` lists options without crawling. This is a live-site smoke check of the first item per target, not full article validation; it does not invoke LLM analysis. The script exits with code 1 if any checked target fails.
215
332
 
216
333
  **Output**: Console table summary + compact text summary for CI integrations.
217
334
 
218
- **CI**: Daily automated run via GitHub Actions (`.github/workflows/parser-health-check.yml`) with Slack notifications on pass/fail. CI skips the KHS excavation report/site-open targets because their fetches can fail in Actions even when normal crawling works.
335
+ **CI**: [.github/workflows/parser-health-check.yml](./.github/workflows/parser-health-check.yml) runs daily at 08:00 UTC (17:00 KST), or manually, on an `org-linux` runner with a 30-minute job timeout. It skips the two KHS excavation report/site-open targets (57 targets checked with the current configuration). Slack notifications require the `SLACK_BOT_TOKEN` secret and `SLACK_ALERT_DEV_CHANNEL` repository variable. Forks need a matching runner and notification configuration to use this workflow unchanged. The CLI also writes GitHub Actions outputs and a job summary when their environment variables are present.
219
336
 
220
337
  ## 🤝 Contributing
221
338
 
@@ -230,55 +347,58 @@ See [CONTRIBUTING.md](./CONTRIBUTING.md) for contribution workflow, dev setup, a
230
347
 
231
348
  To build your own newsletter, update these files:
232
349
 
233
- **1. Template** (`src/templates/newsletter-html.ts`):
350
+ **1. Templates** (`src/templates/newsletter-html.ts`, `welcome-html.ts`, `shared.ts`):
351
+
234
352
  - Logo URLs, brand colors (#D2691E, #E59866), contact info
235
353
  - Platform intro and footer text
236
354
  - Unsubscribe link format (currently Resend's `{{{RESEND_UNSUBSCRIBE_URL}}}`)
237
355
 
238
356
  **2. Config** (`src/config/index.ts`):
357
+
239
358
  ```typescript
240
- brandName: 'Your Newsletter Name'
241
- subscribeUrl: 'https://yourdomain.com/subscribe'
359
+ import type { NewsletterConfig } from '@heripo/research-radar';
360
+
361
+ export const newsletterConfig: NewsletterConfig = {
362
+ brandName: 'Your Newsletter Name',
363
+ subscribePageUrl: 'https://yourdomain.com/subscribe',
364
+ publicationCriteria: {
365
+ minimumArticleCountForIssue: 5,
366
+ priorityArticleScoreThreshold: 8,
367
+ },
368
+ };
242
369
  ```
243
370
 
244
371
  **3. Crawling targets** (`src/config/crawling-targets.ts`):
372
+
245
373
  - Replace Korean heritage sites with your domain sources
246
374
  - Implement parsers in `src/parsers/`
247
375
 
248
376
  **4. Switch content generation LLM provider** (optional):
249
377
 
250
378
  Content generation supports **3 built-in providers** — just change `contentGeneration.provider`:
379
+
251
380
  ```typescript
252
- contentGeneration: {
253
- provider: 'google', // 'openai' | 'anthropic' | 'google'
254
- apiKey: process.env.GOOGLE_API_KEY,
255
- model: 'gemini-3.1-pro-preview', // optional, each provider has a default
256
- }
257
- ```
258
- Default models: openai=`gpt-5.1`, anthropic=`claude-sonnet-4-6`, google=`gemini-3.1-pro-preview`
381
+ import type { ContentGenerationConfig } from '@heripo/research-radar';
259
382
 
260
- Analysis provider (OpenAI) can be changed by modifying `src/providers/analysis.provider.ts`. Any [Vercel AI SDK provider](https://sdk.vercel.ai/providers) works.
383
+ const apiKey = process.env.GOOGLE_API_KEY;
384
+ if (!apiKey) throw new Error('GOOGLE_API_KEY is required');
261
385
 
262
- **Search keywords**: `heripo`, `kimhongyeon`, `#D2691E`, `openai`, `gpt-5`, `contentGeneration`
386
+ const contentGeneration: ContentGenerationConfig = {
387
+ provider: 'google',
388
+ apiKey,
389
+ model: 'gemini-3.1-pro-preview',
390
+ };
391
+ ```
263
392
 
264
- ## Why Code-Based?
393
+ Default models: openai=`gpt-5.4`, anthropic=`claude-sonnet-4-6`, google=`gemini-3.1-pro-preview`
265
394
 
266
- Code-based automation delivers **superior output quality** through advanced AI techniques:
395
+ To change analysis providers, update both `src/providers/analysis.provider.ts` (provider type and models) and `src/newsletter-generator.ts` (provider construction), using a compatible AI SDK provider. Also adapt domain-specific minimum-score rules in the analysis provider, output language and expert fields in config, package metadata, and the GitHub Actions runner/Slack settings for your fork.
267
396
 
268
- **No-code platforms**: Generic content, limited to built-in features
269
- **This kit**: Self-reflection, chain-of-thought, multi-step verification workflows
397
+ **Search keywords**: `heripo`, `kimhongyeon`, `#D2691E`, `openai`, `gpt-5`, `contentGeneration`
270
398
 
271
- **Key advantages**:
272
- - **Quality**: Sophisticated prompting strategies, custom validation pipelines
273
- - **Cost control**: Different models per step, token limits, retry logic
274
- - **Flexibility**: Swap any component (Crawling/Analysis/Content/Email) via Provider interfaces
275
- - **Operations**: Built-in retries, preview emails, integrates with CI/CD
276
- - **No lock-in**: OSS, self-hostable, any LLM provider
399
+ ## Why Code-Based?
277
400
 
278
- **Design philosophy**:
279
- - Logic in code (orchestration, deduplication)
280
- - Reasoning in AI (analysis, scoring, content generation)
281
- - Connections in architecture (swappable Providers)
401
+ The domain logic is inspectable in source: parser behavior, model selection, score rules, publication settings, and HTML templates can be versioned together. Repository and provider interfaces let applications integrate existing storage and delivery systems, while the core engine supplies orchestration and retry handling.
282
402
 
283
403
  ## Related Projects
284
404
 
package/dist/index.js CHANGED
@@ -2013,18 +2013,14 @@ const heripoLogoHtml = (imgMarginBottom) => `
2013
2013
  <!--<![endif]-->
2014
2014
  </div>`;
2015
2015
  /**
2016
- * Heripo platform introduction section.
2016
+ * Heripo project introduction section.
2017
2017
  * Shared between newsletter and welcome email templates.
2018
2018
  *
2019
- * Note: Each template may append its own additional paragraph after this block
2020
- * (e.g., newsletter adds a line about source requests via GitHub Issues).
2021
2019
  */
2022
2020
  const platformIntroHtml = () => `
2023
- <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;">heripo는 고고학 연구 환경의 실질적인 디지털 전환을 지향하는 연구 플랫폼입니다.</p>
2024
- <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;">발굴조사보고서(PDF) 속에 갇힌 텍스트와 도면을 분석 가능한 구조화된 데이터로 전환하여, 연구자가 자료를 보다 체계적으로 탐색하고 재사용할 수 있는 인프라를 구축하고 있습니다.</p>
2025
- <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7;
2026
- color: #444444; margin: 0 0 18px 0;">현재는 소프트웨어 엔지니어와 고고학 연구자가 함께하는 <strong><a href="https://github.com/heripo-lab" target="_blank">heripo lab</a></strong>으로 운영 중이며, 2026년 1월 28일 핵심 엔진을 <strong><a href="https://github.com/heripo-lab/heripo-engine" target="_blank">오픈소스로 공개</a></strong>했습니다.</p>
2027
- <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;">오픈소스로 공개된 핵심 기능은 <strong><a href="https://engine-demo.heripo.org" target="_blank">데모 사이트</a></strong>에서 직접 체험해 보실 수 있으며, 플랫폼 프로토타입 출시 시 구독자분들께 우선 안내해 드리겠습니다.</p>`;
2021
+ <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;">heripo는 발굴조사보고서의 기록을 데이터로 전환하고 출처와 변환 경로를 함께 보존하기 위한 연구 인프라 프로젝트입니다. 지금 받아보시는 뉴스레터는 그 출발점이며, 관련 오픈소스 프로젝트는 <strong><a href="https://github.com/heripo-lab" target="_blank">heripo lab</a></strong>에서 공개하고 있습니다.</p>
2022
+ <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;">뉴스레터에 이어 오픈소스 <strong><a href="https://github.com/heripo-lab/heripo-engine" target="_blank">heripo engine</a></strong>을 바탕으로 발굴조사보고서의 기록을 데이터로 전환하고 이를 탐색하고 활용할 수 있는 연구 도구 <strong>heripo 베이스캠프</strong>를 준비 중입니다.</p>
2023
+ <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;">heripo 베이스캠프는 현재 초대 기반 비공개 알파 테스트 중입니다. 공개 테스터 모집 및 정식 출시 소식은 이 뉴스레터로 전해드리겠습니다.</p>`;
2028
2024
  /**
2029
2025
  * "Powered by LLM Newsletter Kit · View Source" footer line.
2030
2026
  */
@@ -2531,7 +2527,7 @@ ${options.heripolabNewsMarkdown}
2531
2527
  : ''}
2532
2528
 
2533
2529
  <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;">🔍 뉴스레터 출처</h2>
2534
- <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;">모든 소식의 출처는 <a href="https://heripo.app/research-radar/sources" target="_blank">이곳에서</a> 확인할 수 있습니다.</p>
2530
+ <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;">이 뉴스레터가 수집하는 모든 소식의 출처는 <a href="https://heripo.app/research-radar/sources" target="_blank">이곳에서</a> 확인할 수 있습니다. 새롭게 포함했으면 하는 출처나 오류가 있다면 <a href="https://github.com/heripo-lab/heripo-research-radar/issues" target="_blank">GitHub 이슈</a>로 알려주세요.</p>
2535
2531
  <hr style="border: 0; border-top: 2px solid #D2691E; margin: 32px 0;">
2536
2532
  <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;">📅 발행 정책</h2>
2537
2533
  <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;"><strong>${options?.isKrasNewsletter ? '한국고고학회 뉴스레터' : 'heripo 리서치 레이더'}</strong>는 매일 발행을 원칙으로 하되, 독자분들께 의미 있는 정보를 제공하기 위해 다음과 같은 발행 기준을 적용합니다:</p>
@@ -2542,10 +2538,8 @@ ${options.heripolabNewsMarkdown}
2542
2538
  </ul>
2543
2539
  <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;">이러한 정책을 통해 매일 의미 없는 소식으로 독자분들의 시간을 낭비하지 않고, 정말 중요한 정보를 적절한 타이밍에 제공하고자 합니다.</p>
2544
2540
  <hr style="border: 0; border-top: 2px solid #D2691E; margin: 32px 0;">
2545
- <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;">🔍 heripo(헤리포) 플랫폼 소개</h2>
2541
+ <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;">🔍 heripo 프로젝트 소개</h2>
2546
2542
  ${platformIntroHtml()}
2547
- <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7;
2548
- color: #444444; margin: 0 0 18px 0;">보고 계신 뉴스레터(리서치 레이더)는 heripo의 초기 선행 기능 중 하나입니다. 뉴스레터 소스 추가 요청은 <a href="https://github.com/heripo-lab/heripo-research-radar/issues" target="_blank">GitHub 이슈</a>를 통해 언제든 환영합니다.</p>
2549
2543
  <hr style="border: 0; border-top: 2px solid #D2691E; margin: 32px 0;">
2550
2544
  <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;">⚠️ 중요 안내</h2>
2551
2545
  <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;">본 뉴스레터는 국가유산청 공지사항, 관련 기관 입찰 정보 등 특정 웹 게시판의 모든 신규 소식을 빠짐없이 수집하여 제공합니다. 수집된 모든 정보는 정확한 크롤링 로직에 기반하므로 원본과 일치하여 신뢰할 수 있습니다.</p>
@@ -2936,7 +2930,7 @@ async function generateWelcomeHTML(id, name, options) {
2936
2930
  function createWelcomeHtmlRaw(name, isKras, siteUrl, unsubscribeUrl) {
2937
2931
  const title = isKras
2938
2932
  ? '한국고고학회 뉴스레터 구독 완료'
2939
- : 'heripo 리서치 레이더 구독 완료';
2933
+ : 'heripo 뉴스레터 구독 완료';
2940
2934
  const headerHtml = isKras
2941
2935
  ? `<!-- KRAS 50주년 헤더 -->
2942
2936
  <div style="text-align: center; margin-bottom: 36px;">
@@ -2947,12 +2941,7 @@ function createWelcomeHtmlRaw(name, isKras, siteUrl, unsubscribeUrl) {
2947
2941
  : `${heripoLogoHtml('8px')}
2948
2942
 
2949
2943
  <h1 style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; line-height: 1.2; margin: 0 0
2950
- 18px 0; letter-spacing: -0.5px; margin-top: 0; font-size: 32px; font-weight: bold; color: #111111; border-bottom: 3px solid #D2691E; padding-bottom: 8px;">${name}님, heripo 리서치 레이더에 오신 것을 환영합니다!</h1>`;
2951
- const feedbackHeading = `${name}님의 목소리가 heripo의 미래를 만듭니다`;
2952
- const feedbackText = 'heripo';
2953
- const newsletterLine = isKras
2954
- ? `<p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;">뉴스레터(리서치 레이더)는 heripo의 초기 선행 기능 중 하나입니다. 뉴스레터 소스 추가 요청은 <a href="https://github.com/heripo-lab/heripo-research-radar/issues" target="_blank">GitHub 이슈</a>를 통해 언제든 환영합니다.</p>`
2955
- : `<p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;">뉴스레터(리서치 레이더)는 heripo의 초기 선행 기능 중 하나입니다. 뉴스레터 소스 추가 요청은 <a href="https://github.com/heripo-lab/heripo-research-radar/issues" target="_blank">GitHub 이슈</a>를 통해 언제든 환영합니다.</p>`;
2944
+ 18px 0; letter-spacing: -0.5px; margin-top: 0; font-size: 32px; font-weight: bold; color: #111111; border-bottom: 3px solid #D2691E; padding-bottom: 8px;">${name}님, 환영합니다.</h1>`;
2956
2945
  const warningHtml = isKras
2957
2946
  ? `
2958
2947
  <blockquote style="background-color: #fef2f2; border-left: 5px solid #dc2626; margin: 24px 0; padding: 20px; border-radius: 4px;">
@@ -2968,7 +2957,7 @@ function createWelcomeHtmlRaw(name, isKras, siteUrl, unsubscribeUrl) {
2968
2957
  </blockquote>`;
2969
2958
  const footerDisclaimerText = isKras
2970
2959
  ? '이 이메일은 heripo.app에서 한국고고학회 뉴스레터를 구독하신 분들에게 발송됩니다.'
2971
- : '이 이메일은 heripo.app에서 리서치 레이더를 구독하신 분들에게 발송됩니다.';
2960
+ : '이 이메일은 heripo.app에서 heripo 뉴스레터를 구독하신 분들에게 발송됩니다.';
2972
2961
  const footerUnsubscribeHtml = isKras
2973
2962
  ? `<p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.7; color: #6b7280; margin: 0 0 18px 0; margin-bottom: 8px;">📱 구독 관리: <a href="${unsubscribeUrl}" class="footer-link">구독 해지</a></p>`
2974
2963
  : `<p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.7; color: #6b7280; margin: 0 0 18px 0; margin-bottom: 8px;">📱 구독 관리: <a href="${unsubscribeUrl}" class="footer-link">구독 해지</a></p>`;
@@ -3124,23 +3113,16 @@ function createWelcomeHtmlRaw(name, isKras, siteUrl, unsubscribeUrl) {
3124
3113
  <td bgcolor="#ffffff" align="left" class="content-cell dark-mode-content-bg${isKras ? ' kras-newsletter' : ''}" style="-webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; mso-table-lspace: 0pt; mso-table-rspace: 0pt; padding: 48px 44px 44px 44px; border-radius: 12px; box-shadow: 0 4px 18px rgba(0,0,0,0.07);">
3125
3114
  ${headerHtml}
3126
3115
 
3127
- <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 15px 0; letter-spacing: -0.2px; border-left: 5px solid #D2691E; padding-left: 12px; background: #fff7f2;">💬 ${feedbackHeading}</h2>
3116
+ <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 15px 0; letter-spacing: -0.2px; border-left: 5px solid #D2691E; padding-left: 12px; background: #fff7f2;">📬 뉴스레터 구독이 완료되었습니다</h2>
3128
3117
 
3129
- ${isKras ? ` <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;">${name}님, 한국고고학회 뉴스레터를 구독해주셔서 감사합니다.</p>` : ''}
3118
+ <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;">${name}님, 구독해주셔서 감사합니다. 고고학과 문화유산의 중요한 소식을 뉴스레터로 전해드리겠습니다.</p>
3130
3119
 
3131
- <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;"><strong style="color: #D2691E; font-weight: bold;">"이런 기능이 있다면 좋겠다"</strong> 혹은 <strong style="color: #D2691E; font-weight: bold;">"이런 점은 불편하다"</strong>와 같은 의견을 언제든 보내주세요.</p>
3120
+ <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;">놓치고 싶지 않은 소식이나 개선 의견이 있다면 언제든 <a href="${siteUrl}/contact">문의하기</a>로 알려주세요.</p>
3132
3121
 
3133
- <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;">여러분의 피드백 하나하나가 ${feedbackText}의 다음 발걸음을 결정합니다.</p>
3134
- ${isKras
3135
- ? `
3136
- <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;"><strong><a href="https://github.com/heripo-lab" target="_blank">heripo lab</a></strong>은 한국고고학회와 함께 뉴스레터 발행 및 고고학의 디지털 전환을 추진하고 있습니다. 앞으로도 연구 현장에 실질적으로 도움이 되는 정보와 기술을 제공해 드리겠습니다.</p>
3137
- `
3138
- : ''}
3139
3122
  <hr style="border: 0; border-top: 1px solid #e5e7eb; margin: 28px 0 20px;">
3140
3123
 
3141
- <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 15px 0; letter-spacing: -0.2px; border-left: 5px solid #D2691E; padding-left: 12px; background: #fff7f2;">🔍 heripo(헤리포) 플랫폼 소개</h2>
3124
+ <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 15px 0; letter-spacing: -0.2px; border-left: 5px solid #D2691E; padding-left: 12px; background: #fff7f2;">🔍 heripo 프로젝트 소개</h2>
3142
3125
  ${platformIntroHtml()}
3143
- ${newsletterLine}
3144
3126
  ${warningHtml}
3145
3127
 
3146
3128
  <hr style="border: 0; border-top: 1px solid #e5e7eb; margin: 32px 0;">
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": "5.0.5",
5
+ "version": "5.0.6",
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.js",
8
8
  "types": "dist/index.d.ts",