@opendirectory.dev/skills 0.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/.claude/skills/claude-md-generator/.env.example +7 -0
- package/.claude/skills/claude-md-generator/README.md +78 -0
- package/.claude/skills/claude-md-generator/SKILL.md +248 -0
- package/.claude/skills/claude-md-generator/evals/evals.json +35 -0
- package/.claude/skills/claude-md-generator/references/section-guide.md +175 -0
- package/dist/e2e.test.d.ts +1 -0
- package/dist/e2e.test.js +62 -0
- package/dist/fs-adapters.d.ts +4 -0
- package/dist/fs-adapters.js +101 -0
- package/dist/fs-adapters.test.d.ts +1 -0
- package/dist/fs-adapters.test.js +108 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +211 -0
- package/dist/transformers.d.ts +6 -0
- package/dist/transformers.js +2 -0
- package/package.json +25 -0
- package/registry.json +226 -0
- package/skills/blog-cover-image-cli/.github/workflows/publish.yml +19 -0
- package/skills/blog-cover-image-cli/LICENSE +15 -0
- package/skills/blog-cover-image-cli/README.md +126 -0
- package/skills/blog-cover-image-cli/SKILL.md +7 -0
- package/skills/blog-cover-image-cli/agent-skill/blog-cover-generator/README.md +30 -0
- package/skills/blog-cover-image-cli/agent-skill/blog-cover-generator/SKILL.md +72 -0
- package/skills/blog-cover-image-cli/bin/cli.js +226 -0
- package/skills/blog-cover-image-cli/examples/100x_UX_Research_AI_Agent.png +0 -0
- package/skills/blog-cover-image-cli/examples/Firecrawl-supabase-bolt.png +0 -0
- package/skills/blog-cover-image-cli/examples/Git-City_Case_study_Cover_Image.jpg +0 -0
- package/skills/blog-cover-image-cli/examples/THE DISTRIBUTION LAYER (2).png +0 -0
- package/skills/blog-cover-image-cli/examples/canva-perplexity-duolingo-cover-image.png +0 -0
- package/skills/blog-cover-image-cli/examples/gamma-mistral-veed.png +0 -0
- package/skills/blog-cover-image-cli/examples/server-survival-case-study-cover-image(1).png +0 -0
- package/skills/blog-cover-image-cli/examples/viral-meme-automation.png +0 -0
- package/skills/blog-cover-image-cli/index.js +2 -0
- package/skills/blog-cover-image-cli/package-lock.json +2238 -0
- package/skills/blog-cover-image-cli/package.json +37 -0
- package/skills/blog-cover-image-cli/src/geminiGenerator.js +126 -0
- package/skills/blog-cover-image-cli/src/imageValidator.js +54 -0
- package/skills/blog-cover-image-cli/src/logoFetcher.js +86 -0
- package/skills/claude-md-generator/.env.example +7 -0
- package/skills/claude-md-generator/README.md +78 -0
- package/skills/claude-md-generator/SKILL.md +254 -0
- package/skills/claude-md-generator/evals/evals.json +35 -0
- package/skills/claude-md-generator/references/section-guide.md +175 -0
- package/skills/cook-the-blog/README.md +86 -0
- package/skills/cook-the-blog/SKILL.md +130 -0
- package/skills/dependency-update-bot/.env.example +13 -0
- package/skills/dependency-update-bot/README.md +101 -0
- package/skills/dependency-update-bot/SKILL.md +376 -0
- package/skills/dependency-update-bot/evals/evals.json +45 -0
- package/skills/dependency-update-bot/references/changelog-patterns.md +201 -0
- package/skills/docs-from-code/.env.example +13 -0
- package/skills/docs-from-code/README.md +97 -0
- package/skills/docs-from-code/SKILL.md +160 -0
- package/skills/docs-from-code/evals/evals.json +29 -0
- package/skills/docs-from-code/references/extraction-guide.md +174 -0
- package/skills/docs-from-code/references/output-template.md +135 -0
- package/skills/docs-from-code/scripts/extract_py.py +238 -0
- package/skills/docs-from-code/scripts/extract_ts.ts +284 -0
- package/skills/docs-from-code/scripts/package.json +18 -0
- package/skills/explain-this-pr/README.md +74 -0
- package/skills/explain-this-pr/SKILL.md +130 -0
- package/skills/explain-this-pr/evals/evals.json +35 -0
- package/skills/google-trends-api-skills/README.md +78 -0
- package/skills/google-trends-api-skills/SKILL.md +7 -0
- package/skills/google-trends-api-skills/google-trends-api/SKILL.md +163 -0
- package/skills/google-trends-api-skills/google-trends-api/references/api-responses.md +188 -0
- package/skills/google-trends-api-skills/google-trends-api/scripts/discover_keywords.py +344 -0
- package/skills/google-trends-api-skills/seo-keyword-research/SKILL.md +205 -0
- package/skills/google-trends-api-skills/seo-keyword-research/references/keyword-placement-guide.md +89 -0
- package/skills/google-trends-api-skills/seo-keyword-research/references/tech-blog-examples.md +207 -0
- package/skills/google-trends-api-skills/seo-keyword-research/scripts/blog_seo_research.py +373 -0
- package/skills/hackernews-intel/.env.example +33 -0
- package/skills/hackernews-intel/README.md +161 -0
- package/skills/hackernews-intel/SKILL.md +156 -0
- package/skills/hackernews-intel/evals/evals.json +35 -0
- package/skills/hackernews-intel/package.json +15 -0
- package/skills/hackernews-intel/scripts/monitor-hn.js +258 -0
- package/skills/kill-the-standup/.env.example +22 -0
- package/skills/kill-the-standup/README.md +84 -0
- package/skills/kill-the-standup/SKILL.md +169 -0
- package/skills/kill-the-standup/evals/evals.json +35 -0
- package/skills/kill-the-standup/references/standup-format.md +102 -0
- package/skills/linkedin-post-generator/.env.example +14 -0
- package/skills/linkedin-post-generator/README.md +107 -0
- package/skills/linkedin-post-generator/SKILL.md +228 -0
- package/skills/linkedin-post-generator/evals/evals.json +35 -0
- package/skills/linkedin-post-generator/references/linkedin-format.md +216 -0
- package/skills/linkedin-post-generator/references/output-template.md +154 -0
- package/skills/llms-txt-generator/.env.example +18 -0
- package/skills/llms-txt-generator/README.md +142 -0
- package/skills/llms-txt-generator/SKILL.md +176 -0
- package/skills/llms-txt-generator/evals/evals.json +35 -0
- package/skills/llms-txt-generator/references/llms-txt-spec.md +88 -0
- package/skills/llms-txt-generator/references/output-template.md +76 -0
- package/skills/llms-txt-generator/test-output/genzcareer.in/llms.txt +31 -0
- package/skills/luma-attendees-scraper/README.md +170 -0
- package/skills/luma-attendees-scraper/SKILL.md +7 -0
- package/skills/luma-attendees-scraper/luma_attendees_export.js +223 -0
- package/skills/meeting-brief-generator/.env.example +21 -0
- package/skills/meeting-brief-generator/README.md +90 -0
- package/skills/meeting-brief-generator/SKILL.md +275 -0
- package/skills/meeting-brief-generator/evals/evals.json +35 -0
- package/skills/meeting-brief-generator/references/brief-format.md +114 -0
- package/skills/meeting-brief-generator/references/output-template.md +150 -0
- package/skills/meta-ads-skill/README.md +100 -0
- package/skills/meta-ads-skill/SKILL.md +7 -0
- package/skills/meta-ads-skill/meta-ads-skill/SKILL.md +41 -0
- package/skills/meta-ads-skill/meta-ads-skill/references/report_templates.md +47 -0
- package/skills/meta-ads-skill/meta-ads-skill/references/workflows.md +51 -0
- package/skills/meta-ads-skill/meta-ads-skill/scripts/auth_check.py +22 -0
- package/skills/meta-ads-skill/meta-ads-skill/scripts/formatters.py +46 -0
- package/skills/newsletter-digest/.env.example +20 -0
- package/skills/newsletter-digest/README.md +147 -0
- package/skills/newsletter-digest/SKILL.md +221 -0
- package/skills/newsletter-digest/evals/evals.json +35 -0
- package/skills/newsletter-digest/feeds.json +7 -0
- package/skills/newsletter-digest/package.json +15 -0
- package/skills/newsletter-digest/references/digest-format.md +123 -0
- package/skills/newsletter-digest/references/output-template.md +136 -0
- package/skills/newsletter-digest/scripts/fetch-feeds.js +141 -0
- package/skills/newsletter-digest/scripts/ghost-publish.js +147 -0
- package/skills/noise2blog/.env.example +16 -0
- package/skills/noise2blog/README.md +107 -0
- package/skills/noise2blog/SKILL.md +229 -0
- package/skills/noise2blog/evals/evals.json +35 -0
- package/skills/noise2blog/references/blog-format.md +188 -0
- package/skills/noise2blog/references/output-template.md +184 -0
- package/skills/outreach-sequence-builder/.env.example +12 -0
- package/skills/outreach-sequence-builder/README.md +108 -0
- package/skills/outreach-sequence-builder/SKILL.md +248 -0
- package/skills/outreach-sequence-builder/evals/evals.json +36 -0
- package/skills/outreach-sequence-builder/references/output-template.md +171 -0
- package/skills/outreach-sequence-builder/references/sequence-format.md +167 -0
- package/skills/outreach-sequence-builder/references/signal-playbook.md +117 -0
- package/skills/position-me/README.md +71 -0
- package/skills/position-me/SKILL.md +7 -0
- package/skills/position-me/position-me/SKILL.md +50 -0
- package/skills/position-me/position-me/references/EVALUATION_SOP.md +40 -0
- package/skills/position-me/position-me/references/REPORT_TEMPLATE.md +58 -0
- package/skills/position-me/position-me/scripts/extract_links.py +49 -0
- package/skills/pr-description-writer/README.md +81 -0
- package/skills/pr-description-writer/SKILL.md +141 -0
- package/skills/pr-description-writer/evals/evals.json +35 -0
- package/skills/pr-description-writer/references/pr-format-guide.md +145 -0
- package/skills/producthunt-launch-kit/.env.example +7 -0
- package/skills/producthunt-launch-kit/README.md +95 -0
- package/skills/producthunt-launch-kit/SKILL.md +380 -0
- package/skills/producthunt-launch-kit/evals/evals.json +35 -0
- package/skills/producthunt-launch-kit/references/copy-rules.md +124 -0
- package/skills/reddit-icp-monitor/.env.example +16 -0
- package/skills/reddit-icp-monitor/README.md +117 -0
- package/skills/reddit-icp-monitor/SKILL.md +271 -0
- package/skills/reddit-icp-monitor/evals/evals.json +40 -0
- package/skills/reddit-icp-monitor/references/icp-format.md +131 -0
- package/skills/reddit-icp-monitor/references/reply-rules.md +110 -0
- package/skills/reddit-post-engine/.env.example +13 -0
- package/skills/reddit-post-engine/README.md +103 -0
- package/skills/reddit-post-engine/SKILL.md +303 -0
- package/skills/reddit-post-engine/evals/evals.json +35 -0
- package/skills/reddit-post-engine/references/subreddit-playbook.md +156 -0
- package/skills/schema-markup-generator/.env.example +19 -0
- package/skills/schema-markup-generator/README.md +114 -0
- package/skills/schema-markup-generator/SKILL.md +192 -0
- package/skills/schema-markup-generator/evals/evals.json +35 -0
- package/skills/schema-markup-generator/references/json-ld-spec.md +263 -0
- package/skills/schema-markup-generator/references/output-template.md +556 -0
- package/skills/show-hn-writer/.env.example +14 -0
- package/skills/show-hn-writer/README.md +88 -0
- package/skills/show-hn-writer/SKILL.md +303 -0
- package/skills/show-hn-writer/evals/evals.json +35 -0
- package/skills/show-hn-writer/references/hn-rules.md +74 -0
- package/skills/show-hn-writer/references/title-formulas.md +93 -0
- package/skills/stargazer/README.md +79 -0
- package/skills/stargazer/SKILL.md +7 -0
- package/skills/stargazer/stargazer-skill/SKILL.md +58 -0
- package/skills/stargazer/stargazer-skill/assets/.env.example +18 -0
- package/skills/stargazer/stargazer-skill/scripts/convert_to_csv.py +63 -0
- package/skills/stargazer/stargazer-skill/scripts/count_emails.py +52 -0
- package/skills/stargazer/stargazer-skill/scripts/stargazer_deep_extractor.py +450 -0
- package/skills/tweet-thread-from-blog/.env.example +14 -0
- package/skills/tweet-thread-from-blog/README.md +109 -0
- package/skills/tweet-thread-from-blog/SKILL.md +177 -0
- package/skills/tweet-thread-from-blog/evals/evals.json +35 -0
- package/skills/tweet-thread-from-blog/references/output-template.md +193 -0
- package/skills/tweet-thread-from-blog/references/thread-format.md +107 -0
- package/skills/twitter-GTM-find-skill/README.md +43 -0
- package/skills/twitter-GTM-find-skill/SKILL.md +7 -0
- package/skills/twitter-GTM-find-skill/twitter-GTM-find/SKILL.md +37 -0
- package/skills/twitter-GTM-find-skill/twitter-GTM-find/references/icp-checklist.md +35 -0
- package/skills/twitter-GTM-find-skill/twitter-GTM-find/scripts/package.json +23 -0
- package/skills/twitter-GTM-find-skill/twitter-GTM-find/scripts/run_pipeline.sh +8 -0
- package/skills/twitter-GTM-find-skill/twitter-GTM-find/scripts/src/debug.ts +23 -0
- package/skills/twitter-GTM-find-skill/twitter-GTM-find/scripts/src/extractor.ts +79 -0
- package/skills/twitter-GTM-find-skill/twitter-GTM-find/scripts/src/icp-filter.ts +87 -0
- package/skills/twitter-GTM-find-skill/twitter-GTM-find/scripts/src/index.ts +94 -0
- package/skills/twitter-GTM-find-skill/twitter-GTM-find/scripts/src/scraper.ts +41 -0
- package/skills/twitter-GTM-find-skill/twitter-GTM-find/scripts/tsconfig.json +13 -0
- package/skills/yc-intent-radar-skill/README.md +39 -0
- package/skills/yc-intent-radar-skill/SKILL.md +7 -0
- package/skills/yc-intent-radar-skill/yc-jobs-scraper/SKILL.md +59 -0
- package/skills/yc-intent-radar-skill/yc-jobs-scraper/scripts/auth.js +29 -0
- package/skills/yc-intent-radar-skill/yc-jobs-scraper/scripts/db.js +62 -0
- package/skills/yc-intent-radar-skill/yc-jobs-scraper/scripts/export_radar_candidates.js +40 -0
- package/skills/yc-intent-radar-skill/yc-jobs-scraper/scripts/package-lock.json +1525 -0
- package/skills/yc-intent-radar-skill/yc-jobs-scraper/scripts/package.json +12 -0
- package/skills/yc-intent-radar-skill/yc-jobs-scraper/scripts/scraper.js +217 -0
- package/src/e2e.test.ts +35 -0
- package/src/fs-adapters.test.ts +91 -0
- package/src/fs-adapters.ts +65 -0
- package/src/index.ts +182 -0
- package/src/transformers.ts +6 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# hackernews-intel
|
|
2
|
+
|
|
3
|
+
<img width="1280" height="640" alt="hackernews-intel" src="https://github.com/user-attachments/assets/8d75cba9-7c2b-4693-8365-00779ed2b3d3" />
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
Monitor Hacker News for keywords. Get a Slack alert every time a new post matches your topics, without duplicates. Run it manually, on a cron schedule, or via GitHub Actions.
|
|
7
|
+
|
|
8
|
+
## What It Does
|
|
9
|
+
|
|
10
|
+
- Fetches new posts from HN using the free Algolia search API (no API key needed)
|
|
11
|
+
- Checks each match against a local SQLite cache and never alerts on the same post twice
|
|
12
|
+
- Sends a Slack notification with title, URL, points, and comment count
|
|
13
|
+
- Supports dry-run mode to preview matches without sending alerts
|
|
14
|
+
- Configurable lookback window, minimum points threshold, and comment inclusion
|
|
15
|
+
|
|
16
|
+
## Requirements
|
|
17
|
+
|
|
18
|
+
| Requirement | Purpose | Where to Get It |
|
|
19
|
+
|------------|---------|-----------------|
|
|
20
|
+
| HN_KEYWORDS | Keywords to monitor | Set in .env, comma-separated list |
|
|
21
|
+
| SLACK_WEBHOOK | Slack alerts | https://api.slack.com/apps, Incoming Webhooks |
|
|
22
|
+
| Node.js 20+ | Running the script | https://nodejs.org |
|
|
23
|
+
|
|
24
|
+
No API key needed for Hacker News. The HN Algolia API is free and public.
|
|
25
|
+
|
|
26
|
+
## Setup
|
|
27
|
+
|
|
28
|
+
### 1. Install dependencies
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
cd hackernews-intel
|
|
32
|
+
npm install
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 2. Configure environment variables
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
cp .env.example .env
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Edit `.env`:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
HN_KEYWORDS=claude code,LLM agents,deno runtime
|
|
45
|
+
SLACK_WEBHOOK=https://hooks.slack.com/services/your/webhook/url
|
|
46
|
+
HN_MIN_POINTS=0
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 3. Create a Slack webhook
|
|
50
|
+
|
|
51
|
+
1. Go to https://api.slack.com/apps and create or select your app
|
|
52
|
+
2. Enable "Incoming Webhooks" in the app settings
|
|
53
|
+
3. Add a webhook to your workspace and select the target channel
|
|
54
|
+
4. Copy the webhook URL (starts with `https://hooks.slack.com/services/`)
|
|
55
|
+
5. Set it as `SLACK_WEBHOOK` in `.env`
|
|
56
|
+
|
|
57
|
+
## How to Use
|
|
58
|
+
|
|
59
|
+
Preview the last 7 days without sending alerts:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npm run dry-run:week
|
|
63
|
+
# or
|
|
64
|
+
node scripts/monitor-hn.js --dry-run --days=7
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Live run:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npm run monitor
|
|
71
|
+
# or
|
|
72
|
+
node scripts/monitor-hn.js
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Schedule via cron (every 4 hours):
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
crontab -e
|
|
79
|
+
# Add:
|
|
80
|
+
0 */4 * * * cd /path/to/hackernews-intel && node scripts/monitor-hn.js >> /tmp/hn-intel.log 2>&1
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Schedule via GitHub Actions:
|
|
84
|
+
|
|
85
|
+
```yaml
|
|
86
|
+
name: HN Intel Monitor
|
|
87
|
+
on:
|
|
88
|
+
schedule:
|
|
89
|
+
- cron: '0 */4 * * *'
|
|
90
|
+
workflow_dispatch:
|
|
91
|
+
jobs:
|
|
92
|
+
monitor:
|
|
93
|
+
runs-on: ubuntu-latest
|
|
94
|
+
steps:
|
|
95
|
+
- uses: actions/checkout@v4
|
|
96
|
+
- uses: actions/setup-node@v4
|
|
97
|
+
with:
|
|
98
|
+
node-version: '20'
|
|
99
|
+
- run: npm install
|
|
100
|
+
- run: node scripts/monitor-hn.js
|
|
101
|
+
env:
|
|
102
|
+
HN_KEYWORDS: ${{ secrets.HN_KEYWORDS }}
|
|
103
|
+
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Add `HN_KEYWORDS` and `SLACK_WEBHOOK` as repository secrets.
|
|
107
|
+
|
|
108
|
+
## Slack Alert Format
|
|
109
|
+
|
|
110
|
+
Each alert looks like:
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
Story Title Here
|
|
114
|
+
47 points 23 comments authorname
|
|
115
|
+
Keyword: claude code | HN discussion
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Options
|
|
119
|
+
|
|
120
|
+
| Variable | Default | Purpose |
|
|
121
|
+
|----------|---------|---------|
|
|
122
|
+
| `HN_KEYWORDS` | required | Comma-separated keywords |
|
|
123
|
+
| `SLACK_WEBHOOK` | required | Slack Incoming Webhook URL |
|
|
124
|
+
| `HN_MIN_POINTS` | `0` | Minimum points to alert |
|
|
125
|
+
| `HN_INCLUDE_COMMENTS` | `false` | Also monitor HN comments |
|
|
126
|
+
| `HN_DB_PATH` | `./hn-intel.db` | SQLite cache file path |
|
|
127
|
+
|
|
128
|
+
## CLI Flags
|
|
129
|
+
|
|
130
|
+
| Flag | Purpose |
|
|
131
|
+
|------|---------|
|
|
132
|
+
| `--dry-run` | Preview matches without sending Slack alerts |
|
|
133
|
+
| `--days=N` | Lookback window for the first run (default: 1) |
|
|
134
|
+
| `--reset` | Clear the cache and start fresh |
|
|
135
|
+
|
|
136
|
+
## GitHub Actions Note
|
|
137
|
+
|
|
138
|
+
GitHub Actions does not persist files between workflow runs. The SQLite cache resets on each run, and the script re-fetches the last 1 day of posts every time. Posts from the previous run will be alerted again.
|
|
139
|
+
|
|
140
|
+
To fix this, either:
|
|
141
|
+
- Store `hn-intel.db` in an S3 bucket and download/upload it around the script run
|
|
142
|
+
- Use a self-hosted runner with persistent storage
|
|
143
|
+
- Use a cron job on your own machine instead
|
|
144
|
+
|
|
145
|
+
## Project Structure
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
hackernews-intel/
|
|
149
|
+
├── SKILL.md
|
|
150
|
+
├── README.md
|
|
151
|
+
├── .env.example
|
|
152
|
+
├── package.json
|
|
153
|
+
├── evals/
|
|
154
|
+
│ └── evals.json
|
|
155
|
+
└── scripts/
|
|
156
|
+
└── monitor-hn.js
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## License
|
|
160
|
+
|
|
161
|
+
MIT
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: hackernews-intel
|
|
3
|
+
description: Monitors Hacker News for user-configured keywords, deduplicates against a local SQLite cache, and sends Slack alerts for new matching posts. Use when asked to monitor Hacker News for mentions, track keywords on HN, get alerts when something is posted about a topic on Hacker News, or set up HN keyword monitoring. Trigger when a user mentions Hacker News alerts, HN monitoring, keyword tracking on HN, or wants to know when a topic appears on Hacker News.
|
|
4
|
+
compatibility: [claude-code, gemini-cli, github-copilot]
|
|
5
|
+
author: OpenDirectory
|
|
6
|
+
version: 1.0.0
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Hacker News Intel
|
|
10
|
+
|
|
11
|
+
Monitor Hacker News for keywords. On each run, fetch new posts via the HN Algolia API, deduplicate against a local SQLite cache, and send Slack alerts for unseen matches. Run manually or schedule via cron or GitHub Actions.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Step 1: Check Setup
|
|
16
|
+
|
|
17
|
+
**1a. Verify required environment variables**
|
|
18
|
+
|
|
19
|
+
Check that these are set:
|
|
20
|
+
- `HN_KEYWORDS` (required): comma-separated list of keywords to monitor
|
|
21
|
+
- `SLACK_WEBHOOK` (required): Slack Incoming Webhook URL for alerts
|
|
22
|
+
|
|
23
|
+
If either is missing, stop and tell the user:
|
|
24
|
+
- `HN_KEYWORDS`: list the topics you want to monitor, comma-separated. Example: `claude code,LLM agents,deno runtime`
|
|
25
|
+
- `SLACK_WEBHOOK`: create an Incoming Webhook at https://api.slack.com/apps. Select your workspace, enable Incoming Webhooks, and copy the URL.
|
|
26
|
+
|
|
27
|
+
**1b. Verify script dependencies**
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
ls /Users/ksd/Desktop/Varnan_skills/hackernews-intel/node_modules/better-sqlite3 2>/dev/null
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
If missing:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
cd /Users/ksd/Desktop/Varnan_skills/hackernews-intel && npm install
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Step 2: Run the Monitor
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
cd /Users/ksd/Desktop/Varnan_skills/hackernews-intel && node scripts/monitor-hn.js
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Flags:**
|
|
48
|
+
- `--dry-run`: print matches to stdout without sending any Slack alerts. Use this to preview what would be alerted before committing.
|
|
49
|
+
- `--days=N`: on the first run, look back N days (default: 1). Only applies when the SQLite cache is empty.
|
|
50
|
+
- `--reset`: clear the cache and start fresh. Use when you change your keyword list significantly.
|
|
51
|
+
|
|
52
|
+
**What the script does:**
|
|
53
|
+
1. Reads `HN_KEYWORDS` from environment, splits by comma, trims whitespace
|
|
54
|
+
2. Opens (or creates) a SQLite database at the path in `HN_DB_PATH` (default: `./hn-intel.db`)
|
|
55
|
+
3. For each keyword, calls `https://hn.algolia.com/api/v1/search_by_date` with `numericFilters=created_at_i>lastSeen`
|
|
56
|
+
4. For each result, checks if the `objectID` is already in `seen_posts` — skips if yes
|
|
57
|
+
5. If `HN_MIN_POINTS` is set, skips posts below that threshold
|
|
58
|
+
6. For new matching posts, sends a Slack alert and inserts the `objectID` into the cache
|
|
59
|
+
7. Updates `poll_log` with the run summary
|
|
60
|
+
8. Prints a summary to stdout
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Step 3: Review Results
|
|
65
|
+
|
|
66
|
+
After the script runs, read its stdout output. It will report:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
Run complete.
|
|
70
|
+
Keywords checked: N
|
|
71
|
+
Posts found: N
|
|
72
|
+
Posts alerted: N
|
|
73
|
+
Posts skipped (already seen): N
|
|
74
|
+
Errors: N
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
If `Posts found: 0` and you expect results, check:
|
|
78
|
+
- Are the keywords specific enough? Very broad terms (e.g. "AI") may return 0 results if HN Algolia returns too many and the time window is narrow
|
|
79
|
+
- Is the `--days` window large enough for the first run? Try `--days=7`
|
|
80
|
+
- Is the DB cache too aggressive? Try `--reset` to clear it and rerun
|
|
81
|
+
|
|
82
|
+
If `Posts alerted: 0` but `Posts found: N`, the deduplication is working — those posts were seen in a previous run.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Step 4: Schedule the Monitor
|
|
87
|
+
|
|
88
|
+
**Option A: cron (macOS/Linux)**
|
|
89
|
+
|
|
90
|
+
Add to crontab to run every 4 hours:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
crontab -e
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Add this line (adjust the path to match your install):
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
0 */4 * * * cd /Users/ksd/Desktop/Varnan_skills/hackernews-intel && node scripts/monitor-hn.js >> /tmp/hn-intel.log 2>&1
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Option B: GitHub Actions (recommended for teams)**
|
|
103
|
+
|
|
104
|
+
Create `.github/workflows/hn-intel.yml` in any repo:
|
|
105
|
+
|
|
106
|
+
```yaml
|
|
107
|
+
name: HN Intel Monitor
|
|
108
|
+
on:
|
|
109
|
+
schedule:
|
|
110
|
+
- cron: '0 */4 * * *'
|
|
111
|
+
workflow_dispatch:
|
|
112
|
+
|
|
113
|
+
jobs:
|
|
114
|
+
monitor:
|
|
115
|
+
runs-on: ubuntu-latest
|
|
116
|
+
steps:
|
|
117
|
+
- uses: actions/checkout@v4
|
|
118
|
+
- uses: actions/setup-node@v4
|
|
119
|
+
with:
|
|
120
|
+
node-version: '20'
|
|
121
|
+
- name: Install dependencies
|
|
122
|
+
run: npm install
|
|
123
|
+
working-directory: hackernews-intel
|
|
124
|
+
- name: Run monitor
|
|
125
|
+
run: node scripts/monitor-hn.js
|
|
126
|
+
working-directory: hackernews-intel
|
|
127
|
+
env:
|
|
128
|
+
HN_KEYWORDS: ${{ secrets.HN_KEYWORDS }}
|
|
129
|
+
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
|
|
130
|
+
HN_MIN_POINTS: ${{ secrets.HN_MIN_POINTS }}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Add `HN_KEYWORDS`, `SLACK_WEBHOOK`, and optionally `HN_MIN_POINTS` as repository secrets.
|
|
134
|
+
|
|
135
|
+
Note: GitHub Actions does not persist files between runs, so the SQLite cache resets each run. This means every run will find all posts within the default 1-day lookback window again. For persistent dedup on GitHub Actions, store the DB in a persistent location (S3, artifact cache, or a dedicated branch).
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Step 5: Tune Keywords and Thresholds
|
|
140
|
+
|
|
141
|
+
**Adding or changing keywords:**
|
|
142
|
+
Update `HN_KEYWORDS` in your `.env` or environment. No restart needed — the script reads it fresh each run.
|
|
143
|
+
|
|
144
|
+
**Setting a minimum points threshold:**
|
|
145
|
+
Set `HN_MIN_POINTS=10` to only alert on posts with 10 or more points. This cuts noise for broad keywords.
|
|
146
|
+
|
|
147
|
+
**Including comments (not just stories):**
|
|
148
|
+
Set `HN_INCLUDE_COMMENTS=true` to also monitor comments mentioning the keyword. Off by default — comments generate significantly more volume.
|
|
149
|
+
|
|
150
|
+
**Resetting the cache:**
|
|
151
|
+
Run `node scripts/monitor-hn.js --reset` to clear `seen_posts` and `poll_log`. The next run fetches fresh from the lookback window.
|
|
152
|
+
|
|
153
|
+
**Testing a new keyword:**
|
|
154
|
+
Run `node scripts/monitor-hn.js --dry-run --days=7` to see the last 7 days of matching posts without alerting anyone.
|
|
155
|
+
|
|
156
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"skill_name": "hackernews-intel",
|
|
3
|
+
"evals": [
|
|
4
|
+
{
|
|
5
|
+
"id": 1,
|
|
6
|
+
"prompt": "Set up HN monitoring for my product — monitor for 'claude code' and 'anthropic' mentions",
|
|
7
|
+
"expected_output": "Agent checks HN_KEYWORDS and SLACK_WEBHOOK are set. Runs npm install if node_modules is missing. Runs node scripts/monitor-hn.js --dry-run --days=7 first to show what posts would have been found in the last week. Reports the found posts with titles, URLs, points, and authors. Asks user to confirm the keywords look right before setting up live monitoring. On confirmation, runs node scripts/monitor-hn.js for a live run.",
|
|
8
|
+
"files": []
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"id": 2,
|
|
12
|
+
"prompt": "Run the HN monitor again",
|
|
13
|
+
"expected_output": "Agent runs node scripts/monitor-hn.js. The script reads the existing SQLite cache (hn-intel.db). For each keyword, it calls search_by_date with numericFilters=created_at_i>MAX(created_at_i from seen_posts). Only posts with objectIDs not in seen_posts are alerted. Script prints 'Posts skipped (already seen): N' showing the deduplication is working. New posts (if any) are alerted to Slack and inserted into the cache.",
|
|
14
|
+
"files": []
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"id": 3,
|
|
18
|
+
"prompt": "Preview what the monitor would alert without sending anything to Slack",
|
|
19
|
+
"expected_output": "Agent runs node scripts/monitor-hn.js --dry-run. Script fetches posts, checks dedup, but does NOT call the Slack webhook. Prints each matching new post with title, URL, points, and author. Prints '[DRY RUN] Would send Slack alert' for each. Final summary shows 'Alerts sent: N (dry run)' indicating no real alerts were sent.",
|
|
20
|
+
"files": []
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"id": 4,
|
|
24
|
+
"prompt": "The monitor found nothing — I know there were posts about 'deno' this week",
|
|
25
|
+
"expected_output": "Agent diagnoses the issue. First checks if HN_MIN_POINTS is too high (filtering out low-point posts). Then runs node scripts/monitor-hn.js --dry-run --days=7 to widen the lookback window. If the cache was recently reset or this is the first run, the default 1-day window may miss older posts. If still no results after --days=7, checks if the keyword in the cache covers the exact term — 'deno' as a keyword may not match 'Deno 2.0' in the title without substring matching. Reports findings clearly.",
|
|
26
|
+
"files": []
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"id": 5,
|
|
30
|
+
"prompt": "Set up a cron job to run HN monitoring every 4 hours",
|
|
31
|
+
"expected_output": "Agent shows the user how to add a crontab entry: '0 */4 * * * cd /path/to/hackernews-intel && node scripts/monitor-hn.js >> /tmp/hn-intel.log 2>&1'. Also shows the GitHub Actions workflow YAML as an alternative. Notes that GitHub Actions does not persist the SQLite file between runs, so the DB resets each run — explains the tradeoff (more re-alerts vs self-hosting). Recommends self-hosted cron if persistent dedup is needed.",
|
|
32
|
+
"files": []
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hackernews-intel",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Monitor Hacker News for keywords and send Slack alerts for new matching posts",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"monitor": "node scripts/monitor-hn.js",
|
|
8
|
+
"dry-run": "node scripts/monitor-hn.js --dry-run",
|
|
9
|
+
"dry-run:week": "node scripts/monitor-hn.js --dry-run --days=7",
|
|
10
|
+
"reset": "node scripts/monitor-hn.js --reset"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"better-sqlite3": "^9.4.3"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* monitor-hn.js
|
|
4
|
+
* Monitors Hacker News for keywords via the Algolia search API.
|
|
5
|
+
* Deduplicates via SQLite. Sends Slack alerts for new matching posts.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node monitor-hn.js # Normal run
|
|
9
|
+
* node monitor-hn.js --dry-run # Preview matches, no Slack alerts
|
|
10
|
+
* node monitor-hn.js --days=7 # Look back 7 days on first run
|
|
11
|
+
* node monitor-hn.js --reset # Clear cache and start fresh
|
|
12
|
+
*
|
|
13
|
+
* Required env vars:
|
|
14
|
+
* HN_KEYWORDS Comma-separated keywords (e.g. "claude code,LLM agents,deno")
|
|
15
|
+
* SLACK_WEBHOOK Slack Incoming Webhook URL
|
|
16
|
+
*
|
|
17
|
+
* Optional env vars:
|
|
18
|
+
* HN_MIN_POINTS Minimum points to alert (default: 0 = all posts)
|
|
19
|
+
* HN_INCLUDE_COMMENTS Include comments in search (default: false)
|
|
20
|
+
* HN_DB_PATH SQLite DB file path (default: ./hn-intel.db)
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import Database from 'better-sqlite3';
|
|
24
|
+
import { fileURLToPath } from 'url';
|
|
25
|
+
import { dirname, join } from 'path';
|
|
26
|
+
|
|
27
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
|
|
29
|
+
// Parse CLI args
|
|
30
|
+
const args = process.argv.slice(2);
|
|
31
|
+
const DRY_RUN = args.includes('--dry-run');
|
|
32
|
+
const RESET = args.includes('--reset');
|
|
33
|
+
const daysArg = args.find(a => a.startsWith('--days='));
|
|
34
|
+
const FIRST_RUN_DAYS = daysArg ? parseInt(daysArg.split('=')[1], 10) : 1;
|
|
35
|
+
|
|
36
|
+
// Config from environment
|
|
37
|
+
const RAW_KEYWORDS = process.env.HN_KEYWORDS || '';
|
|
38
|
+
const SLACK_WEBHOOK = process.env.SLACK_WEBHOOK || '';
|
|
39
|
+
const MIN_POINTS = parseInt(process.env.HN_MIN_POINTS || '0', 10);
|
|
40
|
+
const INCLUDE_COMMENTS = process.env.HN_INCLUDE_COMMENTS === 'true';
|
|
41
|
+
const DB_PATH = process.env.HN_DB_PATH || join(__dirname, '..', 'hn-intel.db');
|
|
42
|
+
|
|
43
|
+
// Validate required config
|
|
44
|
+
if (!RAW_KEYWORDS.trim()) {
|
|
45
|
+
console.error('ERROR: HN_KEYWORDS is not set.');
|
|
46
|
+
console.error('Set it to a comma-separated list: HN_KEYWORDS="claude code,LLM agents"');
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!SLACK_WEBHOOK && !DRY_RUN) {
|
|
51
|
+
console.error('ERROR: SLACK_WEBHOOK is not set.');
|
|
52
|
+
console.error('Create one at: https://api.slack.com/apps');
|
|
53
|
+
console.error('Or run with --dry-run to preview without sending alerts.');
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const keywords = RAW_KEYWORDS.split(',').map(k => k.trim()).filter(Boolean);
|
|
58
|
+
|
|
59
|
+
// Initialize SQLite
|
|
60
|
+
const db = new Database(DB_PATH);
|
|
61
|
+
|
|
62
|
+
if (RESET) {
|
|
63
|
+
db.exec('DROP TABLE IF EXISTS seen_posts');
|
|
64
|
+
db.exec('DROP TABLE IF EXISTS poll_log');
|
|
65
|
+
console.log('Cache cleared.');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
db.exec(`
|
|
69
|
+
CREATE TABLE IF NOT EXISTS seen_posts (
|
|
70
|
+
objectID TEXT PRIMARY KEY,
|
|
71
|
+
title TEXT,
|
|
72
|
+
url TEXT,
|
|
73
|
+
points INTEGER,
|
|
74
|
+
author TEXT,
|
|
75
|
+
keyword TEXT,
|
|
76
|
+
created_at_i INTEGER,
|
|
77
|
+
alerted_at INTEGER
|
|
78
|
+
);
|
|
79
|
+
CREATE TABLE IF NOT EXISTS poll_log (
|
|
80
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
81
|
+
polled_at INTEGER,
|
|
82
|
+
posts_found INTEGER,
|
|
83
|
+
posts_alerted INTEGER
|
|
84
|
+
);
|
|
85
|
+
`);
|
|
86
|
+
|
|
87
|
+
// Prepared statements
|
|
88
|
+
const insertPost = db.prepare(`
|
|
89
|
+
INSERT OR IGNORE INTO seen_posts (objectID, title, url, points, author, keyword, created_at_i, alerted_at)
|
|
90
|
+
VALUES (@objectID, @title, @url, @points, @author, @keyword, @created_at_i, @alerted_at)
|
|
91
|
+
`);
|
|
92
|
+
const hasPost = db.prepare('SELECT 1 FROM seen_posts WHERE objectID = ?');
|
|
93
|
+
const insertLog = db.prepare(`
|
|
94
|
+
INSERT INTO poll_log (polled_at, posts_found, posts_alerted)
|
|
95
|
+
VALUES (@polled_at, @posts_found, @posts_alerted)
|
|
96
|
+
`);
|
|
97
|
+
|
|
98
|
+
// Get the timestamp of the last seen post (for incremental polling)
|
|
99
|
+
function getLastSeenTimestamp() {
|
|
100
|
+
const row = db.prepare('SELECT MAX(created_at_i) as ts FROM seen_posts').get();
|
|
101
|
+
if (row && row.ts) return row.ts;
|
|
102
|
+
// First run: use FIRST_RUN_DAYS lookback
|
|
103
|
+
return Math.floor(Date.now() / 1000) - FIRST_RUN_DAYS * 86400;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Fetch posts from HN Algolia API
|
|
107
|
+
async function fetchPosts(keyword, sinceTimestamp) {
|
|
108
|
+
const tags = INCLUDE_COMMENTS ? 'story,comment' : 'story';
|
|
109
|
+
const url =
|
|
110
|
+
`https://hn.algolia.com/api/v1/search_by_date` +
|
|
111
|
+
`?query=${encodeURIComponent(keyword)}` +
|
|
112
|
+
`&tags=${tags}` +
|
|
113
|
+
`&numericFilters=created_at_i>${sinceTimestamp}` +
|
|
114
|
+
`&hitsPerPage=50`;
|
|
115
|
+
|
|
116
|
+
const res = await fetch(url);
|
|
117
|
+
if (!res.ok) {
|
|
118
|
+
throw new Error(`HN API returned ${res.status} for keyword "${keyword}"`);
|
|
119
|
+
}
|
|
120
|
+
const data = await res.json();
|
|
121
|
+
return data.hits || [];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Send a Slack alert for a single post
|
|
125
|
+
async function sendSlackAlert(post, keyword) {
|
|
126
|
+
const storyUrl = post.url || `https://news.ycombinator.com/item?id=${post.objectID}`;
|
|
127
|
+
const hnUrl = `https://news.ycombinator.com/item?id=${post.objectID}`;
|
|
128
|
+
|
|
129
|
+
const payload = {
|
|
130
|
+
blocks: [
|
|
131
|
+
{
|
|
132
|
+
type: 'section',
|
|
133
|
+
text: {
|
|
134
|
+
type: 'mrkdwn',
|
|
135
|
+
text: `*<${storyUrl}|${post.title}>*\n:arrow_up: ${post.points ?? 0} points :speech_balloon: ${post.num_comments ?? 0} comments :bust_in_silhouette: ${post.author}`,
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
type: 'context',
|
|
140
|
+
elements: [
|
|
141
|
+
{
|
|
142
|
+
type: 'mrkdwn',
|
|
143
|
+
text: `Keyword: \`${keyword}\` | <${hnUrl}|HN discussion>`,
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const res = await fetch(SLACK_WEBHOOK, {
|
|
151
|
+
method: 'POST',
|
|
152
|
+
headers: { 'Content-Type': 'application/json' },
|
|
153
|
+
body: JSON.stringify(payload),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (!res.ok) {
|
|
157
|
+
const body = await res.text();
|
|
158
|
+
throw new Error(`Slack webhook returned ${res.status}: ${body}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Sleep helper for rate limiting between keyword queries
|
|
163
|
+
function sleep(ms) {
|
|
164
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function main() {
|
|
168
|
+
console.log(`HN Intel Monitor`);
|
|
169
|
+
console.log(` Keywords: ${keywords.join(', ')}`);
|
|
170
|
+
console.log(` Min points: ${MIN_POINTS}`);
|
|
171
|
+
console.log(` Include comments: ${INCLUDE_COMMENTS}`);
|
|
172
|
+
console.log(` Mode: ${DRY_RUN ? 'DRY RUN (no alerts)' : 'LIVE'}`);
|
|
173
|
+
console.log('');
|
|
174
|
+
|
|
175
|
+
const sinceTimestamp = getLastSeenTimestamp();
|
|
176
|
+
const sinceDate = new Date(sinceTimestamp * 1000).toISOString();
|
|
177
|
+
console.log(`Fetching posts since: ${sinceDate}`);
|
|
178
|
+
console.log('');
|
|
179
|
+
|
|
180
|
+
let totalFound = 0;
|
|
181
|
+
let totalAlerted = 0;
|
|
182
|
+
let totalErrors = 0;
|
|
183
|
+
|
|
184
|
+
for (let i = 0; i < keywords.length; i++) {
|
|
185
|
+
const keyword = keywords[i];
|
|
186
|
+
|
|
187
|
+
// Rate limit: 1 second between keyword queries
|
|
188
|
+
if (i > 0) await sleep(1000);
|
|
189
|
+
|
|
190
|
+
let hits;
|
|
191
|
+
try {
|
|
192
|
+
hits = await fetchPosts(keyword, sinceTimestamp);
|
|
193
|
+
} catch (err) {
|
|
194
|
+
console.error(` ERROR fetching "${keyword}": ${err.message}`);
|
|
195
|
+
totalErrors++;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const filtered = hits.filter(hit => (hit.points ?? 0) >= MIN_POINTS);
|
|
200
|
+
const newPosts = filtered.filter(hit => !hasPost.get(hit.objectID));
|
|
201
|
+
|
|
202
|
+
console.log(`"${keyword}": ${hits.length} hits, ${filtered.length} above min points, ${newPosts.length} new`);
|
|
203
|
+
|
|
204
|
+
for (const post of newPosts) {
|
|
205
|
+
totalFound++;
|
|
206
|
+
|
|
207
|
+
const storyUrl = post.url || `https://news.ycombinator.com/item?id=${post.objectID}`;
|
|
208
|
+
console.log(` [NEW] ${post.title}`);
|
|
209
|
+
console.log(` ${storyUrl}`);
|
|
210
|
+
console.log(` Points: ${post.points ?? 0} Comments: ${post.num_comments ?? 0} Author: ${post.author}`);
|
|
211
|
+
|
|
212
|
+
if (!DRY_RUN) {
|
|
213
|
+
try {
|
|
214
|
+
await sendSlackAlert(post, keyword);
|
|
215
|
+
totalAlerted++;
|
|
216
|
+
console.log(` Slack alert sent.`);
|
|
217
|
+
} catch (err) {
|
|
218
|
+
console.error(` ERROR sending Slack alert: ${err.message}`);
|
|
219
|
+
totalErrors++;
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
console.log(` [DRY RUN] Would send Slack alert.`);
|
|
223
|
+
totalAlerted++;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Insert into cache regardless of Slack success (avoid double-alerting on retry)
|
|
227
|
+
insertPost.run({
|
|
228
|
+
objectID: post.objectID,
|
|
229
|
+
title: post.title || '',
|
|
230
|
+
url: post.url || '',
|
|
231
|
+
points: post.points ?? 0,
|
|
232
|
+
author: post.author || '',
|
|
233
|
+
keyword,
|
|
234
|
+
created_at_i: post.created_at_i,
|
|
235
|
+
alerted_at: Math.floor(Date.now() / 1000),
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Log this run
|
|
241
|
+
insertLog.run({
|
|
242
|
+
polled_at: Math.floor(Date.now() / 1000),
|
|
243
|
+
posts_found: totalFound,
|
|
244
|
+
posts_alerted: totalAlerted,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
console.log('');
|
|
248
|
+
console.log('Run complete.');
|
|
249
|
+
console.log(` Keywords checked: ${keywords.length}`);
|
|
250
|
+
console.log(` New posts found: ${totalFound}`);
|
|
251
|
+
console.log(` Alerts sent: ${totalAlerted}${DRY_RUN ? ' (dry run)' : ''}`);
|
|
252
|
+
if (totalErrors > 0) console.log(` Errors: ${totalErrors}`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
main().catch(err => {
|
|
256
|
+
console.error('Unexpected error:', err);
|
|
257
|
+
process.exit(1);
|
|
258
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# kill-the-standup — Environment Variables
|
|
2
|
+
# ==========================================
|
|
3
|
+
# No API keys needed for the AI — the agent reads Linear and GitHub directly.
|
|
4
|
+
# Only Linear and Slack credentials are required.
|
|
5
|
+
|
|
6
|
+
# Required: Linear API key
|
|
7
|
+
# Get it: Linear → Settings → API → Personal API keys → Create key
|
|
8
|
+
LINEAR_API_KEY=your_linear_api_key_here
|
|
9
|
+
|
|
10
|
+
# Required: Slack Incoming Webhook URL
|
|
11
|
+
# Get it: api.slack.com/apps → Create/select app → Incoming Webhooks → Add New Webhook
|
|
12
|
+
# The URL format is: https://hooks.slack.com/services/T.../B.../...
|
|
13
|
+
SLACK_WEBHOOK_URL=your_slack_webhook_url_here
|
|
14
|
+
|
|
15
|
+
# Optional: GitHub repo to pull commit activity from (owner/repo format)
|
|
16
|
+
# If not set, the GitHub section is skipped and only Linear issues appear in the standup
|
|
17
|
+
# Example: GITHUB_REPO=Varnan-Tech/my-app
|
|
18
|
+
GITHUB_REPO=owner/repo
|
|
19
|
+
|
|
20
|
+
# Optional: GitHub username for filtering commits to your own work
|
|
21
|
+
# If not set, the skill uses the username from your active gh auth session
|
|
22
|
+
GITHUB_USERNAME=your_github_username
|