@wonsukchoi/crondex 0.9.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,97 +1,100 @@
1
- # crondex
1
+ <div align="center">
2
2
 
3
- A public, growing directory of pre-made cron jobs — built so any AI agent
4
- (Claude, Codex, Hermes, OpenClaw, or a plain LLM with shell access) can pull
5
- one, tweak it, and schedule it. Clone it, grab a job, adjust the knobs, run it.
3
+ ```
4
+ ______________ ____ ____/ /__ _ __
5
+ / ___/ ___/ __ \/ __ \/ __ / _ \| |/_/
6
+ / /__/ / / /_/ / / / / /_/ / __/> <
7
+ \___/_/ \____/_/ /_/\__,_/\___/_/|_|
8
+ ```
6
9
 
7
- ## Quick start
10
+ **Pre-made cron jobs any AI agent can pull, tweak, and schedule.**
11
+ A directory, not a framework.
8
12
 
9
- ```bash
10
- git clone https://github.com/wonsukchoi/crondex.git
11
- cat crondex/catalog.json # browse what's available
12
- cat crondex/jobs/devops/dependency-audit.yaml # read one job
13
- ```
13
+ [![npm version](https://img.shields.io/npm/v/@wonsukchoi/crondex.svg)](https://www.npmjs.com/package/@wonsukchoi/crondex)
14
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
14
15
 
15
- Give an agent the repo and a goal ("set up something that checks my repo
16
- health every morning") — it reads `catalog.json`, finds the closest match,
17
- adjusts `schedule`/`variables`, and wires it into whatever scheduler it has.
16
+ </div>
18
17
 
19
- ## CLI
18
+ ---
20
19
 
21
- Published on npm as `@wonsukchoi/crondex` (the plain name `crondex` was
22
- blocked by npm's anti-typosquat check). The command itself is still
23
- `crondex` once installed — only the package name is scoped:
20
+ Works with Claude, Codex, Hermes, OpenClaw, or any plain LLM with shell
21
+ access.
22
+
23
+ ## Get a job
24
24
 
25
25
  ```bash
26
- npx @wonsukchoi/crondex list # browse everything
27
- npx @wonsukchoi/crondex list --category devops # filter by category or --tag
28
- npx @wonsukchoi/crondex categories # list categories with job counts
29
- npx @wonsukchoi/crondex show backup-reminder # print one job's YAML
30
- npx @wonsukchoi/crondex add backup-reminder --dest ./cron/backup-reminder.yaml
31
26
  npx @wonsukchoi/crondex recommend "warn me before my SSL cert expires"
32
- npx @wonsukchoi/crondex init ssl-cert-expiry-check --category security
27
+ npx @wonsukchoi/crondex show ssl-cert-expiry-check
28
+ npx @wonsukchoi/crondex add ssl-cert-expiry-check --dest ./cron/ssl-cert-expiry-check.yaml
33
29
  ```
34
30
 
35
- `recommend` takes a free-text description of what you want and ranks the
36
- catalog against it (weighted keyword match over tags/name/id/category/
37
- description, zero tokens, no network call) — handy for an agent that just
38
- got asked "can you set up something for X" and needs to check whether a
39
- ready-made job already covers it before writing one from scratch. Add
40
- `--limit <n>` to control how many results come back (default 5).
41
-
42
- `init` scaffolds a new job file from `templates/job.template.yaml` into your
43
- own project (default `./<id>.yaml`) — for writing a job that isn't in the
44
- catalog yet, whether or not you plan to contribute it back.
31
+ - `recommend "<what you want>"` find the closest matching job (zero
32
+ tokens, no network call, so an agent can check before writing one from
33
+ scratch)
34
+ - `list [--category x] [--tag y]` / `categories` browse everything
35
+ - `show <id>` print a job's full YAML
36
+ - `add <id> [--dest path]` copy it into your project to edit
37
+ - `init <id> [--category x]` — scaffold a brand-new job from the template
45
38
 
46
- Or install once and drop the scope prefix on every call:
39
+ No install needed `npx` always runs against the latest catalog. Prefer
40
+ installing once? `npm install -g @wonsukchoi/crondex`, then drop the `npx`
41
+ prefix (run `npm update -g` later to pick up new jobs). Prefer no npm at
42
+ all? `git clone` this repo and run `node bin/crondex.js list`.
47
43
 
48
- ```bash
49
- npm install -g @wonsukchoi/crondex
50
- crondex list
51
- ```
44
+ ---
52
45
 
53
- `add` copies the job's YAML as-is into your project — it's yours to edit
54
- from there. `npx` always pulls the latest published catalog; a global
55
- install needs `npm update -g @wonsukchoi/crondex` to see new jobs (`crondex
56
- list` prints a reminder either way).
46
+ ## What's in a job
57
47
 
58
- A clone works too, no npm required:
48
+ Every job is one YAML file:
59
49
 
60
- ```bash
61
- git clone https://github.com/wonsukchoi/crondex.git && cd crondex
62
- node bin/crondex.js list
50
+ ```yaml
51
+ id: dependency-audit
52
+ version: 1
53
+ name: Dependency Vulnerability Audit
54
+ category: devops
55
+ schedule: "0 8 * * 1" # standard 5-field cron
56
+ runner: hybrid # shell | agent-prompt | hybrid
57
+ command: | # for runner: shell/hybrid
58
+ ...raw shell audit, zero tokens...
59
+ prompt: | # for runner: agent-prompt/hybrid
60
+ ...instructions with {{repo_path}}, LLM synthesizes+prioritizes...
61
+ script_note: what you lose by using `command` instead of `prompt`
62
+ variables:
63
+ repo_path:
64
+ default: "."
65
+ compatible_agents: [claude, codex, hermes, openclaw, generic]
63
66
  ```
64
67
 
65
- ## Layout
68
+ - **`shell`** — runs `command` only. Zero LLM tokens, deterministic.
69
+ - **`agent-prompt`** — hands `prompt` to an LLM each run. Costs tokens, but
70
+ can synthesize, prioritize, and draft prose.
71
+ - **`hybrid`** — ships both, pick per run: script to save tokens, prompt for
72
+ more judgment. `script_note` explains the tradeoff.
66
73
 
67
- ```
68
- crondex/
69
- ├── bin/crondex.js CLI: list / categories / show / add / recommend / init
70
- ├── catalog.json generated index of every job read this first
71
- ├── schema/job.schema.json spec every job file follows
72
- ├── jobs/ one YAML per job, grouped by category subdirectory
73
- └── scripts/
74
- ├── build-catalog.js regenerates catalog.json + the summary below from jobs/**/*.yaml
75
- └── validate-jobs.js validates every job against the schema
76
- ```
74
+ `{{placeholders}}` resolve from `variables` — override them for your case,
75
+ then hand `command`/`prompt` plus `schedule` to whatever scheduler you have
76
+ (system crontab, a hosted cron, your agent's own scheduling mechanism). This
77
+ repo defines *what* to run and *when*, not the executor. Full field spec:
78
+ [`schema/job.schema.json`](schema/job.schema.json).
77
79
 
78
- ## Available jobs
80
+ ---
79
81
 
80
- Full details (description, tags, variables) live in `catalog.json` and each
81
- job's YAML file — run `crondex list`, `crondex recommend "<what you want>"`,
82
- or browse `jobs/<category>/` for the plain-language rundown of each. The
83
- counts below are regenerated by `npm run build-catalog`, so they never drift
84
- from what's actually in `jobs/`.
82
+ ## Browse the catalog
83
+
84
+ The table below is regenerated by `npm run build-catalog`, so it never
85
+ drifts from what's actually in `jobs/`. For full details (description,
86
+ tags, variables) use `crondex list`, `crondex recommend`, or browse
87
+ `jobs/<category>/` directly.
85
88
 
86
89
  <!-- BEGIN JOB SUMMARY -->
87
- 182 jobs across 37 categories:
90
+ 187 jobs across 37 categories:
88
91
 
89
92
  | category | jobs |
90
93
  |---|---|
91
94
  | `agriculture` | 3 |
92
95
  | `construction` | 3 |
93
- | `content` | 12 |
94
- | `creator` | 3 |
96
+ | `content` | 13 |
97
+ | `creator` | 4 |
95
98
  | `crypto` | 3 |
96
99
  | `devops` | 26 |
97
100
  | `ecommerce` | 3 |
@@ -100,7 +103,7 @@ from what's actually in `jobs/`.
100
103
  | `finance` | 10 |
101
104
  | `fleet` | 3 |
102
105
  | `gaming` | 3 |
103
- | `growth` | 4 |
106
+ | `growth` | 5 |
104
107
  | `healthcare` | 3 |
105
108
  | `hiring` | 3 |
106
109
  | `home` | 5 |
@@ -115,75 +118,41 @@ from what's actually in `jobs/`.
115
118
  | `manufacturing` | 3 |
116
119
  | `marketing` | 4 |
117
120
  | `nonprofit` | 3 |
118
- | `personal` | 8 |
121
+ | `personal` | 9 |
119
122
  | `podcast` | 3 |
120
123
  | `productivity` | 9 |
121
124
  | `realestate` | 3 |
122
125
  | `restaurant` | 3 |
123
- | `sales` | 4 |
126
+ | `sales` | 5 |
124
127
  | `security` | 14 |
125
128
  | `support` | 3 |
126
129
  | `team` | 4 |
127
130
  | `travel` | 5 |
128
131
  <!-- END JOB SUMMARY -->
129
132
 
130
- ## How a job works
131
-
132
- Every job is one YAML file with a `runner`:
133
+ ---
133
134
 
134
- - **`shell`** — run `command` directly. Zero LLM tokens, deterministic, no
135
- judgment calls.
136
- - **`agent-prompt`** — hand the `prompt` field to an LLM agent each run.
137
- Costs tokens, but can synthesize, prioritize, and draft prose.
138
- - **`hybrid`** — ships both. You (or your agent) pick per run: `command` to
139
- save tokens and get raw data, or `prompt` when you want the LLM to
140
- interpret it. `script_note` on the job explains exactly what you trade
141
- away by choosing the script.
142
-
143
- `{{placeholders}}` in `command`/`prompt` resolve from `variables`. Each job
144
- also carries a `version` — bumped whenever `prompt`/`command` behavior
145
- changes, so if you've already scheduled a job elsewhere you can tell when
146
- the upstream copy has moved on without you.
135
+ ## Layout
147
136
 
148
- ```yaml
149
- id: dependency-audit
150
- version: 1
151
- name: Dependency Vulnerability Audit
152
- description: ...
153
- category: devops
154
- tags: [security, dependencies]
155
- schedule: "0 8 * * 1" # standard 5-field cron
156
- timezone: "UTC"
157
- runner: hybrid
158
- command: |
159
- ...raw shell audit, zero tokens...
160
- prompt: |
161
- ...instructions with {{repo_path}}, LLM synthesizes+prioritizes...
162
- script_note: what you lose by using `command` instead of `prompt`
163
- variables:
164
- repo_path:
165
- default: "."
166
- description: ...
167
- compatible_agents: [claude, codex, hermes, openclaw, generic]
137
+ ```
138
+ crondex/
139
+ ├── bin/crondex.js CLI: list / categories / show / add / recommend / init
140
+ ├── lib/recommend.js recommend's scoring logic (unit tested in test/)
141
+ ├── catalog.json generated index of every job — read this first
142
+ ├── schema/job.schema.json spec every job file follows
143
+ ├── jobs/ one YAML per job, grouped by category subdirectory
144
+ └── scripts/ build-catalog.js, validate-jobs.js, lint-shell.js
168
145
  ```
169
146
 
170
- Full spec: [`schema/job.schema.json`](schema/job.schema.json).
171
-
172
- ## Using a job
173
-
174
- 1. Pick a job from `catalog.json` — check `modes` to see if it's script-only,
175
- agent-prompt-only, or both.
176
- 2. Decide script vs. agent-prompt if the job is `hybrid`: script saves
177
- tokens, agent-prompt gives you more detail/judgment (see `script_note`).
178
- 3. Override any `variables` and `schedule` for your case.
179
- 4. Hand `command` or `prompt` plus `schedule` to your scheduler — system
180
- crontab, a hosted cron, or your agent's own scheduling mechanism. This
181
- repo defines *what* to run and *when*, not the executor.
147
+ ---
182
148
 
183
149
  ## Contributing a job
184
150
 
185
- See [`CONTRIBUTING.md`](CONTRIBUTING.md) — copy `templates/job.template.yaml`,
186
- fill it in, `npm run validate && npm run build-catalog`, open a PR.
151
+ See [`CONTRIBUTING.md`](CONTRIBUTING.md) — copy `templates/job.template.yaml`
152
+ (or run `crondex init`), fill it in, `npm run validate && npm run
153
+ build-catalog`, open a PR.
154
+
155
+ ---
187
156
 
188
157
  ## License
189
158
 
package/bin/crondex.js CHANGED
@@ -2,6 +2,7 @@
2
2
  // CLI over catalog.json — browse jobs, read one, or pull one into your project.
3
3
  import { readFileSync, writeFileSync, existsSync } from "node:fs";
4
4
  import { join } from "node:path";
5
+ import { tokenize, rankJobs } from "../lib/recommend.js";
5
6
 
6
7
  const ROOT = new URL("..", import.meta.url).pathname;
7
8
  const CATALOG = JSON.parse(readFileSync(join(ROOT, "catalog.json"), "utf8"));
@@ -44,68 +45,13 @@ Examples:
44
45
  `);
45
46
  }
46
47
 
47
- const STOPWORDS = new Set([
48
- "a", "an", "the", "to", "for", "of", "in", "on", "at", "by", "and", "or", "my", "me", "i",
49
- "can", "you", "do", "does", "this", "that", "these", "those", "please", "want", "wants",
50
- "wanted", "need", "needs", "help", "with", "is", "are", "be", "it", "so", "when", "should",
51
- "could", "would", "how", "what", "which", "up", "down", "get", "make", "set", "just", "really",
52
- "some", "something", "any", "want", "us", "our", "your", "yours", "if", "then", "than",
53
- ]);
54
-
55
- function stem(word) {
56
- if (word.length > 4 && word.endsWith("ies")) return word.slice(0, -3) + "y";
57
- if (word.length > 4 && word.endsWith("es") && !word.endsWith("ses")) return word.slice(0, -2);
58
- if (word.length > 3 && word.endsWith("s") && !word.endsWith("ss")) return word.slice(0, -1);
59
- return word;
60
- }
61
-
62
- function tokenize(text) {
63
- return text
64
- .toLowerCase()
65
- .replace(/[^a-z0-9\s-]/g, " ")
66
- .split(/[\s-]+/)
67
- .filter((w) => w.length > 1 && !STOPWORDS.has(w))
68
- .map(stem);
69
- }
70
-
71
- // id and name are near-duplicates of each other (id is just the slugified
72
- // name), so they're merged into one "title" field — otherwise a match on a
73
- // shared word like "water" gets weighted 3+3 instead of 3, which can outrank
74
- // a more specific job that only matches once via tags.
75
- const RECOMMEND_WEIGHTS = { tags: 4, title: 3, category: 2, description: 1 };
76
-
77
- function scoreJob(queryTokens, job) {
78
- const fields = {
79
- tags: job.tags.map((t) => stem(t.toLowerCase())),
80
- title: [...new Set([...tokenize(job.name), ...tokenize(job.id.replace(/-/g, " "))])],
81
- category: tokenize(job.category ?? ""),
82
- description: tokenize(job.description ?? ""),
83
- };
84
- let score = 0;
85
- const matched = new Set();
86
- for (const qt of queryTokens) {
87
- for (const [field, weight] of Object.entries(RECOMMEND_WEIGHTS)) {
88
- if (fields[field].includes(qt)) {
89
- score += weight;
90
- matched.add(qt);
91
- }
92
- }
93
- }
94
- return { score, matchedTerms: [...matched] };
95
- }
96
-
97
48
  function recommend(queryText) {
98
49
  const limit = Number(flag("limit")) || 5;
99
- const queryTokens = tokenize(queryText);
100
- if (!queryTokens.length) {
50
+ if (!tokenize(queryText).length) {
101
51
  console.log("query too vague to match on — describe what you want the job to check or remind you about.");
102
52
  return;
103
53
  }
104
- const ranked = CATALOG.jobs
105
- .map((job) => ({ job, ...scoreJob(queryTokens, job) }))
106
- .filter((r) => r.score > 0)
107
- .sort((a, b) => b.score - a.score)
108
- .slice(0, limit);
54
+ const ranked = rankJobs(CATALOG.jobs, queryText, limit);
109
55
  if (!ranked.length) {
110
56
  console.log(`no confident match for "${queryText}". Run "crondex list" to browse everything.`);
111
57
  return;
package/catalog.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "generated_by": "scripts/build-catalog.js",
3
3
  "schema": "schema/job.schema.json",
4
- "count": 182,
4
+ "count": 187,
5
5
  "jobs": [
6
6
  {
7
7
  "id": "1on1-prep-reminder",
@@ -1186,6 +1186,31 @@
1186
1186
  ],
1187
1187
  "path": "jobs/devops/docker-image-prune.yaml"
1188
1188
  },
1189
+ {
1190
+ "id": "downloads-folder-auto-organize",
1191
+ "version": 1,
1192
+ "name": "Downloads Folder Auto-Organize",
1193
+ "description": "Sorts files sitting in your downloads folder into subfolders by type (images, documents, archives, installers), so it doesn't turn into an unsearchable pile. Use this if your downloads folder has ever hit hundreds of files with zero structure.",
1194
+ "category": "personal",
1195
+ "tags": [
1196
+ "files",
1197
+ "organization",
1198
+ "downloads",
1199
+ "cleanup"
1200
+ ],
1201
+ "schedule": "0 3 * * *",
1202
+ "timezone": "UTC",
1203
+ "runner": "shell",
1204
+ "modes": [
1205
+ "script"
1206
+ ],
1207
+ "compatible_agents": [
1208
+ "generic",
1209
+ "claude",
1210
+ "codex"
1211
+ ],
1212
+ "path": "jobs/personal/downloads-folder-auto-organize.yaml"
1213
+ },
1189
1214
  {
1190
1215
  "id": "email-deliverability-check",
1191
1216
  "version": 1,
@@ -1581,6 +1606,33 @@
1581
1606
  ],
1582
1607
  "path": "jobs/hospitality/food-safety-temp-log-check.yaml"
1583
1608
  },
1609
+ {
1610
+ "id": "form-lead-crm-sync-check",
1611
+ "version": 1,
1612
+ "name": "Form Lead CRM Sync Check",
1613
+ "description": "Checks that leads submitted through your web forms actually landed in your CRM, catching a broken integration instead of a slow rep. Use this if a form-to-CRM zap/integration has ever quietly broken and leads piled up on the form provider's side while sales thought the pipeline was just empty.",
1614
+ "category": "sales",
1615
+ "tags": [
1616
+ "sales",
1617
+ "leads",
1618
+ "crm",
1619
+ "integration"
1620
+ ],
1621
+ "schedule": "*/30 * * * *",
1622
+ "timezone": "UTC",
1623
+ "runner": "agent-prompt",
1624
+ "modes": [
1625
+ "agent-prompt"
1626
+ ],
1627
+ "compatible_agents": [
1628
+ "claude",
1629
+ "codex",
1630
+ "hermes",
1631
+ "openclaw",
1632
+ "generic"
1633
+ ],
1634
+ "path": "jobs/sales/form-lead-crm-sync-check.yaml"
1635
+ },
1584
1636
  {
1585
1637
  "id": "freight-rate-spike-watch",
1586
1638
  "version": 1,
@@ -3397,6 +3449,33 @@
3397
3449
  ],
3398
3450
  "path": "jobs/content/rss-feed-validate.yaml"
3399
3451
  },
3452
+ {
3453
+ "id": "rss-to-social-repurpose",
3454
+ "version": 1,
3455
+ "name": "RSS-to-Social Repurpose Draft",
3456
+ "description": "Checks your blog/RSS feed for new posts and drafts a set of social posts repurposing each one, instead of publishing and moving on without ever promoting it. Use this if you've ever shipped a blog post and then simply forgot to tell anyone it existed.",
3457
+ "category": "content",
3458
+ "tags": [
3459
+ "content",
3460
+ "rss",
3461
+ "social",
3462
+ "repurposing"
3463
+ ],
3464
+ "schedule": "0 10 * * *",
3465
+ "timezone": "UTC",
3466
+ "runner": "agent-prompt",
3467
+ "modes": [
3468
+ "agent-prompt"
3469
+ ],
3470
+ "compatible_agents": [
3471
+ "claude",
3472
+ "codex",
3473
+ "hermes",
3474
+ "openclaw",
3475
+ "generic"
3476
+ ],
3477
+ "path": "jobs/content/rss-to-social-repurpose.yaml"
3478
+ },
3400
3479
  {
3401
3480
  "id": "rsvp-followup-reminder",
3402
3481
  "version": 1,
@@ -4535,6 +4614,60 @@
4535
4614
  "generic"
4536
4615
  ],
4537
4616
  "path": "jobs/productivity/weekly-report.yaml"
4617
+ },
4618
+ {
4619
+ "id": "welcome-email-drip-check",
4620
+ "version": 1,
4621
+ "name": "Welcome Email Drip Check",
4622
+ "description": "Checks that everyone who signed up recently actually received the welcome email, and flags anyone who fell through the cracks. Use this if a welcome-email automation has ever silently failed for a batch of new users and nobody noticed until support tickets started asking \"where's my onboarding email?\"",
4623
+ "category": "growth",
4624
+ "tags": [
4625
+ "growth",
4626
+ "email",
4627
+ "onboarding",
4628
+ "welcome-email"
4629
+ ],
4630
+ "schedule": "0 10 * * *",
4631
+ "timezone": "UTC",
4632
+ "runner": "agent-prompt",
4633
+ "modes": [
4634
+ "agent-prompt"
4635
+ ],
4636
+ "compatible_agents": [
4637
+ "claude",
4638
+ "codex",
4639
+ "hermes",
4640
+ "openclaw",
4641
+ "generic"
4642
+ ],
4643
+ "path": "jobs/growth/welcome-email-drip-check.yaml"
4644
+ },
4645
+ {
4646
+ "id": "youtube-clip-repurposing",
4647
+ "version": 1,
4648
+ "name": "YouTube Clip Repurposing Draft",
4649
+ "description": "Checks for a newly published long-form video and drafts a shortlist of moments worth cutting into shorts/clips for other platforms. Use this if a video has ever gone up and just sat there as one long upload instead of becoming five pieces of content.",
4650
+ "category": "creator",
4651
+ "tags": [
4652
+ "youtube",
4653
+ "video",
4654
+ "clips",
4655
+ "repurposing"
4656
+ ],
4657
+ "schedule": "0 12 * * *",
4658
+ "timezone": "UTC",
4659
+ "runner": "agent-prompt",
4660
+ "modes": [
4661
+ "agent-prompt"
4662
+ ],
4663
+ "compatible_agents": [
4664
+ "claude",
4665
+ "codex",
4666
+ "hermes",
4667
+ "openclaw",
4668
+ "generic"
4669
+ ],
4670
+ "path": "jobs/creator/youtube-clip-repurposing.yaml"
4538
4671
  }
4539
4672
  ]
4540
4673
  }
@@ -0,0 +1,36 @@
1
+ id: rss-to-social-repurpose
2
+ version: 1
3
+ name: RSS-to-Social Repurpose Draft
4
+ description: >
5
+ Checks your blog/RSS feed for new posts and drafts a set of social posts
6
+ repurposing each one, instead of publishing and moving on without ever
7
+ promoting it. Use this if you've ever shipped a blog post and then simply
8
+ forgot to tell anyone it existed.
9
+ category: content
10
+ tags: [content, rss, social, repurposing]
11
+ schedule: "0 10 * * *"
12
+ timezone: "UTC"
13
+ runner: agent-prompt
14
+ prompt: |
15
+ Check {{feed_url}} for posts published in the last {{lookback_hours}}
16
+ hours.
17
+ 1. For each new post, read the title/summary (fetch the full post if the
18
+ feed only has a snippet).
19
+ 2. Draft {{platforms}} versions: a short one for X/Bluesky-style
20
+ character limits, and a longer one for LinkedIn-style posts — each
21
+ written to stand alone, not just "new post: [link]".
22
+ 3. Suggest one pull-quote or stat from the post that would work as a
23
+ standalone hook.
24
+ 4. If no new posts, say so in one line.
25
+ variables:
26
+ feed_url:
27
+ default: "https://example.com/rss.xml"
28
+ description: URL of the RSS/Atom feed to check.
29
+ lookback_hours:
30
+ default: 24
31
+ description: How far back to check for new posts.
32
+ platforms:
33
+ default: "X, LinkedIn"
34
+ description: Comma-separated list of platforms to draft posts for.
35
+ compatible_agents: [claude, codex, hermes, openclaw, generic]
36
+ notes: Draft-only — review before posting, and adjust tone/length per platform's actual current norms rather than trusting fixed character-count assumptions.
@@ -0,0 +1,37 @@
1
+ id: youtube-clip-repurposing
2
+ version: 1
3
+ name: YouTube Clip Repurposing Draft
4
+ description: >
5
+ Checks for a newly published long-form video and drafts a shortlist of
6
+ moments worth cutting into shorts/clips for other platforms. Use this if
7
+ a video has ever gone up and just sat there as one long upload instead of
8
+ becoming five pieces of content.
9
+ category: creator
10
+ tags: [youtube, video, clips, repurposing]
11
+ schedule: "0 12 * * *"
12
+ timezone: "UTC"
13
+ runner: agent-prompt
14
+ prompt: |
15
+ Check {{channel_url}} for videos published in the last
16
+ {{lookback_hours}} hours.
17
+ 1. For any new video, pull the transcript/chapters if available.
18
+ 2. Identify {{clip_count}} moments (with approximate timestamps) that
19
+ would work as standalone short-form clips — look for a strong hook in
20
+ the first few seconds, a self-contained point, or a punchy reaction.
21
+ 3. Draft a one-line caption per clip suited for {{short_platforms}}.
22
+ 4. If no new video, say so in one line.
23
+ variables:
24
+ channel_url:
25
+ default: "https://www.youtube.com/@yourchannel"
26
+ description: Channel or specific video URL to check.
27
+ lookback_hours:
28
+ default: 24
29
+ description: How far back to check for a new upload.
30
+ clip_count:
31
+ default: 3
32
+ description: How many clip candidates to suggest per video.
33
+ short_platforms:
34
+ default: "YouTube Shorts, TikTok, Instagram Reels"
35
+ description: Comma-separated short-form platforms to caption for.
36
+ compatible_agents: [claude, codex, hermes, openclaw, generic]
37
+ notes: Suggests timestamps and captions only — actual cutting/editing still needs a video tool; this just removes the "which moments are worth clipping" decision.
@@ -0,0 +1,36 @@
1
+ id: welcome-email-drip-check
2
+ version: 1
3
+ name: Welcome Email Drip Check
4
+ description: >
5
+ Checks that everyone who signed up recently actually received the
6
+ welcome email, and flags anyone who fell through the cracks. Use this if
7
+ a welcome-email automation has ever silently failed for a batch of new
8
+ users and nobody noticed until support tickets started asking "where's
9
+ my onboarding email?"
10
+ category: growth
11
+ tags: [growth, email, onboarding, welcome-email]
12
+ schedule: "0 10 * * *"
13
+ timezone: "UTC"
14
+ runner: agent-prompt
15
+ prompt: |
16
+ Check {{signup_source}} for accounts created in the last
17
+ {{lookback_hours}} hours, and cross-reference against
18
+ {{email_platform}} send logs for the welcome email campaign.
19
+ 1. List any new signup with no corresponding welcome email send logged.
20
+ 2. Note how long ago they signed up, sorted oldest-first.
21
+ 3. If any gap is found, flag whether it looks like an isolated miss or a
22
+ systemic failure (e.g. every signup in a time window is missing it,
23
+ which points at the automation itself breaking).
24
+ 4. If everyone got their email, say so in one line.
25
+ variables:
26
+ signup_source:
27
+ default: "your user database or signup form"
28
+ description: Where new signups are recorded.
29
+ email_platform:
30
+ default: "your transactional email platform (e.g. Postmark, SendGrid, Customer.io)"
31
+ description: Where welcome-email sends are logged.
32
+ lookback_hours:
33
+ default: 24
34
+ description: How far back to check for new signups.
35
+ compatible_agents: [claude, codex, hermes, openclaw, generic]
36
+ notes: Read-only check — wire up a manual resend for anyone flagged rather than having this job send email directly.
@@ -0,0 +1,34 @@
1
+ id: downloads-folder-auto-organize
2
+ version: 1
3
+ name: Downloads Folder Auto-Organize
4
+ description: >
5
+ Sorts files sitting in your downloads folder into subfolders by type
6
+ (images, documents, archives, installers), so it doesn't turn into an
7
+ unsearchable pile. Use this if your downloads folder has ever hit
8
+ hundreds of files with zero structure.
9
+ category: personal
10
+ tags: [files, organization, downloads, cleanup]
11
+ schedule: "0 3 * * *"
12
+ timezone: "UTC"
13
+ runner: shell
14
+ command: >
15
+ dir="{{downloads_dir}}";
16
+ cd "$dir" || exit 1;
17
+ mkdir -p images documents archives installers other;
18
+ for f in *; do
19
+ [ -f "$f" ] || continue;
20
+ case "$f" in
21
+ *.jpg|*.jpeg|*.png|*.gif|*.webp|*.heic) mv -n "$f" images/ ;;
22
+ *.pdf|*.doc|*.docx|*.txt|*.md|*.xlsx|*.csv) mv -n "$f" documents/ ;;
23
+ *.zip|*.tar|*.gz|*.7z|*.rar) mv -n "$f" archives/ ;;
24
+ *.dmg|*.pkg|*.exe|*.msi|*.deb) mv -n "$f" installers/ ;;
25
+ *) mv -n "$f" other/ ;;
26
+ esac;
27
+ done;
28
+ echo "sorted $(find images documents archives installers other -type f 2>/dev/null | wc -l) file(s) in $dir"
29
+ variables:
30
+ downloads_dir:
31
+ default: "$HOME/Downloads"
32
+ description: Path to the downloads folder to organize.
33
+ compatible_agents: [generic, claude, codex]
34
+ notes: Only sorts files sitting directly in the folder (not subfolders you've already made), and never overwrites a same-named file already in the destination (`mv -n`).
@@ -0,0 +1,36 @@
1
+ id: form-lead-crm-sync-check
2
+ version: 1
3
+ name: Form Lead CRM Sync Check
4
+ description: >
5
+ Checks that leads submitted through your web forms actually landed in
6
+ your CRM, catching a broken integration instead of a slow rep. Use this
7
+ if a form-to-CRM zap/integration has ever quietly broken and leads piled
8
+ up on the form provider's side while sales thought the pipeline was just
9
+ empty.
10
+ category: sales
11
+ tags: [sales, leads, crm, integration]
12
+ schedule: "*/30 * * * *"
13
+ timezone: "UTC"
14
+ runner: agent-prompt
15
+ prompt: |
16
+ Check {{form_platform}} for form submissions in the last
17
+ {{lookback_hours}} hours, and check {{crm_platform}} for matching lead
18
+ records (by email).
19
+ 1. List any form submission with no matching CRM record.
20
+ 2. If there's a gap, note how many are missing and since when — a
21
+ sudden cluster of misses starting at a specific time usually means
22
+ the integration itself broke, not that leads are individually
23
+ falling through.
24
+ 3. If everything matches, say so in one line.
25
+ variables:
26
+ form_platform:
27
+ default: "your web form tool (e.g. Typeform, HubSpot forms, a custom form)"
28
+ description: Where form submissions are recorded.
29
+ crm_platform:
30
+ default: "your CRM (e.g. HubSpot, Salesforce, Pipedrive)"
31
+ description: Where leads should be showing up.
32
+ lookback_hours:
33
+ default: 2
34
+ description: How far back to check for submissions, tight enough to catch a break quickly.
35
+ compatible_agents: [claude, codex, hermes, openclaw, generic]
36
+ notes: Read-only — this catches the gap, but reprocessing missed leads into the CRM is a separate, deliberate action.
@@ -0,0 +1,62 @@
1
+ // Scoring logic behind `crondex recommend` — pulled out of bin/crondex.js so it can
2
+ // be unit tested directly (see test/recommend.test.js).
3
+
4
+ export const STOPWORDS = new Set([
5
+ "a", "an", "the", "to", "for", "of", "in", "on", "at", "by", "and", "or", "my", "me", "i",
6
+ "can", "you", "do", "does", "this", "that", "these", "those", "please", "want", "wants",
7
+ "wanted", "need", "needs", "help", "with", "is", "are", "be", "it", "so", "when", "should",
8
+ "could", "would", "how", "what", "which", "up", "down", "get", "make", "set", "just", "really",
9
+ "some", "something", "any", "want", "us", "our", "your", "yours", "if", "then", "than",
10
+ ]);
11
+
12
+ export function stem(word) {
13
+ if (word.length > 4 && word.endsWith("ies")) return word.slice(0, -3) + "y";
14
+ if (word.length > 4 && word.endsWith("es") && !word.endsWith("ses")) return word.slice(0, -2);
15
+ if (word.length > 3 && word.endsWith("s") && !word.endsWith("ss")) return word.slice(0, -1);
16
+ return word;
17
+ }
18
+
19
+ export function tokenize(text) {
20
+ return text
21
+ .toLowerCase()
22
+ .replace(/[^a-z0-9\s-]/g, " ")
23
+ .split(/[\s-]+/)
24
+ .filter((w) => w.length > 1 && !STOPWORDS.has(w))
25
+ .map(stem);
26
+ }
27
+
28
+ // id and name are near-duplicates of each other (id is just the slugified
29
+ // name), so they're merged into one "title" field — otherwise a match on a
30
+ // shared word like "water" gets weighted 3+3 instead of 3, which can outrank
31
+ // a more specific job that only matches once via tags.
32
+ export const RECOMMEND_WEIGHTS = { tags: 4, title: 3, category: 2, description: 1 };
33
+
34
+ export function scoreJob(queryTokens, job) {
35
+ const fields = {
36
+ tags: job.tags.map((t) => stem(t.toLowerCase())),
37
+ title: [...new Set([...tokenize(job.name), ...tokenize(job.id.replace(/-/g, " "))])],
38
+ category: tokenize(job.category ?? ""),
39
+ description: tokenize(job.description ?? ""),
40
+ };
41
+ let score = 0;
42
+ const matched = new Set();
43
+ for (const qt of queryTokens) {
44
+ for (const [field, weight] of Object.entries(RECOMMEND_WEIGHTS)) {
45
+ if (fields[field].includes(qt)) {
46
+ score += weight;
47
+ matched.add(qt);
48
+ }
49
+ }
50
+ }
51
+ return { score, matchedTerms: [...matched] };
52
+ }
53
+
54
+ export function rankJobs(jobs, queryText, limit = 5) {
55
+ const queryTokens = tokenize(queryText);
56
+ if (!queryTokens.length) return [];
57
+ return jobs
58
+ .map((job) => ({ job, ...scoreJob(queryTokens, job) }))
59
+ .filter((r) => r.score > 0)
60
+ .sort((a, b) => b.score - a.score)
61
+ .slice(0, limit);
62
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wonsukchoi/crondex",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "description": "A public directory of pre-made, agent-editable cron jobs.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -23,6 +23,7 @@
23
23
  },
24
24
  "files": [
25
25
  "bin",
26
+ "lib",
26
27
  "jobs",
27
28
  "schema",
28
29
  "catalog.json"
@@ -33,7 +34,8 @@
33
34
  "scripts": {
34
35
  "build-catalog": "node scripts/build-catalog.js",
35
36
  "validate": "node scripts/validate-jobs.js",
36
- "lint-shell": "node scripts/lint-shell.js"
37
+ "lint-shell": "node scripts/lint-shell.js",
38
+ "test": "node --test"
37
39
  },
38
40
  "devDependencies": {
39
41
  "ajv": "^8.17.1",