@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 +88 -119
- package/bin/crondex.js +3 -57
- package/catalog.json +134 -1
- package/jobs/content/rss-to-social-repurpose.yaml +36 -0
- package/jobs/creator/youtube-clip-repurposing.yaml +37 -0
- package/jobs/growth/welcome-email-drip-check.yaml +36 -0
- package/jobs/personal/downloads-folder-auto-organize.yaml +34 -0
- package/jobs/sales/form-lead-crm-sync-check.yaml +36 -0
- package/lib/recommend.js +62 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,97 +1,100 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
```
|
|
4
|
+
______________ ____ ____/ /__ _ __
|
|
5
|
+
/ ___/ ___/ __ \/ __ \/ __ / _ \| |/_/
|
|
6
|
+
/ /__/ / / /_/ / / / / /_/ / __/> <
|
|
7
|
+
\___/_/ \____/_/ /_/\__,_/\___/_/|_|
|
|
8
|
+
```
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
**Pre-made cron jobs any AI agent can pull, tweak, and schedule.**
|
|
11
|
+
A directory, not a framework.
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
cat crondex/catalog.json # browse what's available
|
|
12
|
-
cat crondex/jobs/devops/dependency-audit.yaml # read one job
|
|
13
|
-
```
|
|
13
|
+
[](https://www.npmjs.com/package/@wonsukchoi/crondex)
|
|
14
|
+
[](LICENSE)
|
|
14
15
|
|
|
15
|
-
|
|
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
|
-
|
|
18
|
+
---
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
49
|
-
npm install -g @wonsukchoi/crondex
|
|
50
|
-
crondex list
|
|
51
|
-
```
|
|
44
|
+
---
|
|
52
45
|
|
|
53
|
-
|
|
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
|
-
|
|
48
|
+
Every job is one YAML file:
|
|
59
49
|
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
80
|
+
---
|
|
79
81
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
90
|
+
187 jobs across 37 categories:
|
|
88
91
|
|
|
89
92
|
| category | jobs |
|
|
90
93
|
|---|---|
|
|
91
94
|
| `agriculture` | 3 |
|
|
92
95
|
| `construction` | 3 |
|
|
93
|
-
| `content` |
|
|
94
|
-
| `creator` |
|
|
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` |
|
|
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` |
|
|
121
|
+
| `personal` | 9 |
|
|
119
122
|
| `podcast` | 3 |
|
|
120
123
|
| `productivity` | 9 |
|
|
121
124
|
| `realestate` | 3 |
|
|
122
125
|
| `restaurant` | 3 |
|
|
123
|
-
| `sales` |
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
Every job is one YAML file with a `runner`:
|
|
133
|
+
---
|
|
133
134
|
|
|
134
|
-
|
|
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
|
-
```
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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":
|
|
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.
|
package/lib/recommend.js
ADDED
|
@@ -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.
|
|
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",
|