@wonsukchoi/crondex 0.9.0 → 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 +141 -8
- package/jobs/content/rss-to-social-repurpose.yaml +36 -0
- package/jobs/creator/youtube-clip-repurposing.yaml +37 -0
- package/jobs/devops/cost-alert.yaml +2 -2
- package/jobs/devops/repo-health-check.yaml +2 -2
- package/jobs/finance/subscription-price-hike-watch.yaml +2 -2
- package/jobs/growth/welcome-email-drip-check.yaml +36 -0
- package/jobs/marketing/competitor-page-diff-watch.yaml +6 -2
- package/jobs/personal/bill-due-reminder.yaml +2 -2
- package/jobs/personal/downloads-folder-auto-organize.yaml +34 -0
- package/jobs/productivity/recurring-task-queue-reset.yaml +2 -2
- package/jobs/sales/form-lead-crm-sync-check.yaml +36 -0
- package/jobs/security/open-port-check.yaml +2 -2
- package/lib/recommend.js +62 -0
- package/package.json +5 -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",
|
|
@@ -258,7 +258,7 @@
|
|
|
258
258
|
},
|
|
259
259
|
{
|
|
260
260
|
"id": "bill-due-reminder",
|
|
261
|
-
"version":
|
|
261
|
+
"version": 2,
|
|
262
262
|
"name": "Bill Due Reminder",
|
|
263
263
|
"description": "Warns you a few days before a recurring monthly bill is due. Use this if you've ever paid a late fee just because you forgot the date.",
|
|
264
264
|
"category": "personal",
|
|
@@ -656,7 +656,7 @@
|
|
|
656
656
|
},
|
|
657
657
|
{
|
|
658
658
|
"id": "competitor-page-diff-watch",
|
|
659
|
-
"version":
|
|
659
|
+
"version": 2,
|
|
660
660
|
"name": "Competitor Page Diff Watch",
|
|
661
661
|
"description": "Snapshots a competitor's pricing or landing page and diffs it against the last snapshot, turning any real change into a short action memo instead of a wall of markup diff. Use this if a competitor has ever changed pricing or repositioned a page and you found out from a customer instead of noticing it yourself.",
|
|
662
662
|
"category": "marketing",
|
|
@@ -760,7 +760,7 @@
|
|
|
760
760
|
},
|
|
761
761
|
{
|
|
762
762
|
"id": "cost-alert",
|
|
763
|
-
"version":
|
|
763
|
+
"version": 2,
|
|
764
764
|
"name": "Cloud/API Cost Alert",
|
|
765
765
|
"description": "Checks your recent cloud or API spend against a budget and warns you if you're over or on pace to go over. Use this if you've ever been surprised by a bill.",
|
|
766
766
|
"category": "devops",
|
|
@@ -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,
|
|
@@ -2637,7 +2689,7 @@
|
|
|
2637
2689
|
},
|
|
2638
2690
|
{
|
|
2639
2691
|
"id": "open-port-check",
|
|
2640
|
-
"version":
|
|
2692
|
+
"version": 2,
|
|
2641
2693
|
"name": "Expected Open Ports Check",
|
|
2642
2694
|
"description": "Checks that the ports you expect to be open on a host actually are, and flags any that aren't. Use this if a service has ever silently stopped listening and you found out from a user instead of a monitor.",
|
|
2643
2695
|
"category": "security",
|
|
@@ -3126,7 +3178,7 @@
|
|
|
3126
3178
|
},
|
|
3127
3179
|
{
|
|
3128
3180
|
"id": "recurring-task-queue-reset",
|
|
3129
|
-
"version":
|
|
3181
|
+
"version": 2,
|
|
3130
3182
|
"name": "Recurring Task Queue Reset",
|
|
3131
3183
|
"description": "Clears out last week's completed recurring-task checklist and creates a fresh one from your template, so every cycle starts with the same predefined owners and checklist instead of a copy-pasted mess. Use this if your recurring weekly checklist has ever quietly drifted because someone edited last week's copy instead of starting clean.",
|
|
3132
3184
|
"category": "productivity",
|
|
@@ -3199,7 +3251,7 @@
|
|
|
3199
3251
|
},
|
|
3200
3252
|
{
|
|
3201
3253
|
"id": "repo-health-check",
|
|
3202
|
-
"version":
|
|
3254
|
+
"version": 2,
|
|
3203
3255
|
"name": "Git Repo Health Check",
|
|
3204
3256
|
"description": "Checks a git repo for branches nobody's touched in a while, uncommitted changes sitting around, and pull requests waiting on review, then reports a short digest. Use this if you want a daily nudge on repo hygiene without checking everything by hand.",
|
|
3205
3257
|
"category": "devops",
|
|
@@ -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,
|
|
@@ -3973,7 +4052,7 @@
|
|
|
3973
4052
|
},
|
|
3974
4053
|
{
|
|
3975
4054
|
"id": "subscription-price-hike-watch",
|
|
3976
|
-
"version":
|
|
4055
|
+
"version": 2,
|
|
3977
4056
|
"name": "Subscription Price Hike Watch",
|
|
3978
4057
|
"description": "Checks a tracked subscription's current price against the last price you recorded and flags any increase. Use this if a service has ever quietly raised its price and you only noticed months later scrolling through a bank statement.",
|
|
3979
4058
|
"category": "finance",
|
|
@@ -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.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
id: cost-alert
|
|
2
|
-
version:
|
|
2
|
+
version: 2
|
|
3
3
|
name: Cloud/API Cost Alert
|
|
4
4
|
description: >
|
|
5
5
|
Checks your recent cloud or API spend against a budget and warns you if
|
|
@@ -14,7 +14,7 @@ command: >
|
|
|
14
14
|
if command -v aws >/dev/null 2>&1; then
|
|
15
15
|
start=$(date -d "-{{lookback_days}} days" +%Y-%m-%d 2>/dev/null || date -v-{{lookback_days}}d +%Y-%m-%d);
|
|
16
16
|
end=$(date +%Y-%m-%d);
|
|
17
|
-
aws ce get-cost-and-usage --time-period Start
|
|
17
|
+
aws ce get-cost-and-usage --time-period Start="$start",End="$end" --granularity DAILY --metrics UnblendedCost 2>/dev/null || echo "aws cost explorer call failed (check credentials/permissions)";
|
|
18
18
|
elif command -v vercel >/dev/null 2>&1; then
|
|
19
19
|
vercel billing 2>/dev/null || echo "vercel CLI present but no billing subcommand available";
|
|
20
20
|
else
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
id: repo-health-check
|
|
2
|
-
version:
|
|
2
|
+
version: 2
|
|
3
3
|
name: Git Repo Health Check
|
|
4
4
|
description: >
|
|
5
5
|
Checks a git repo for branches nobody's touched in a while, uncommitted
|
|
@@ -15,7 +15,7 @@ command: >
|
|
|
15
15
|
cd {{repo_path}} && now=$(date +%s) &&
|
|
16
16
|
echo "--- branches stale >{{stale_branch_days}}d ---" &&
|
|
17
17
|
git for-each-ref --format='%(committerdate:unix) %(refname:short)' refs/heads/ |
|
|
18
|
-
while read ts name; do age=$(( (now-ts)/86400 )); [ "$age" -gt {{stale_branch_days}} ] && echo "$name: ${age}d"; done;
|
|
18
|
+
while read -r ts name; do age=$(( (now-ts)/86400 )); [ "$age" -gt {{stale_branch_days}} ] && echo "$name: ${age}d"; done;
|
|
19
19
|
echo "--- uncommitted changes ---"; git status --short;
|
|
20
20
|
command -v gh >/dev/null && { echo "--- open PRs ---"; gh pr list --state open; } || echo "(gh not installed, skipping PR check)"
|
|
21
21
|
prompt: |
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
id: subscription-price-hike-watch
|
|
2
|
-
version:
|
|
2
|
+
version: 2
|
|
3
3
|
name: Subscription Price Hike Watch
|
|
4
4
|
description: >
|
|
5
5
|
Checks a tracked subscription's current price against the last price you
|
|
@@ -18,7 +18,7 @@ command: >
|
|
|
18
18
|
if [ -z "$current_price" ]; then
|
|
19
19
|
echo "could not extract a price from {{pricing_page_url}} — check {{price_pattern}} still matches the page.";
|
|
20
20
|
exit 0;
|
|
21
|
-
fi
|
|
21
|
+
fi;
|
|
22
22
|
last_price=$(tail -1 "$history_file" | cut -d',' -f2);
|
|
23
23
|
today=$(date +%Y-%m-%d);
|
|
24
24
|
echo "$today,$current_price" >> "$history_file";
|
|
@@ -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.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
id: competitor-page-diff-watch
|
|
2
|
-
version:
|
|
2
|
+
version: 2
|
|
3
3
|
name: Competitor Page Diff Watch
|
|
4
4
|
description: >
|
|
5
5
|
Snapshots a competitor's pricing or landing page and diffs it against the
|
|
@@ -17,7 +17,11 @@ command: >
|
|
|
17
17
|
mkdir -p "$snapshot_dir";
|
|
18
18
|
today_file="$snapshot_dir/$(date +%Y-%m-%d).html";
|
|
19
19
|
curl -s -A "Mozilla/5.0" "{{competitor_url}}" -o "$today_file";
|
|
20
|
-
prev_file
|
|
20
|
+
prev_file="";
|
|
21
|
+
for f in "$snapshot_dir"/*.html; do
|
|
22
|
+
[ "$(basename "$f")" = "$(basename "$today_file")" ] && continue;
|
|
23
|
+
prev_file=$(basename "$f");
|
|
24
|
+
done;
|
|
21
25
|
if [ -n "$prev_file" ]; then
|
|
22
26
|
diff "$snapshot_dir/$prev_file" "$today_file" | head -100;
|
|
23
27
|
else
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
id: bill-due-reminder
|
|
2
|
-
version:
|
|
2
|
+
version: 2
|
|
3
3
|
name: Bill Due Reminder
|
|
4
4
|
description: >
|
|
5
5
|
Warns you a few days before a recurring monthly bill is due. Use this if
|
|
@@ -13,7 +13,7 @@ command: >
|
|
|
13
13
|
due={{due_day_of_month}}; warn={{warn_days}}; name="{{bill_name}}";
|
|
14
14
|
today=$(date +%-d);
|
|
15
15
|
days_left=$(( due - today ));
|
|
16
|
-
if [ "$days_left" -lt 0 ]; then days_left=$(( days_left + 30 )); fi
|
|
16
|
+
if [ "$days_left" -lt 0 ]; then days_left=$(( days_left + 30 )); fi;
|
|
17
17
|
if [ "$days_left" -le "$warn" ]; then echo "REMINDER: $name due in $days_left day(s) (on day $due of the month)";
|
|
18
18
|
else echo "OK: $name due in $days_left day(s)"; fi
|
|
19
19
|
variables:
|
|
@@ -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`).
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
id: recurring-task-queue-reset
|
|
2
|
-
version:
|
|
2
|
+
version: 2
|
|
3
3
|
name: Recurring Task Queue Reset
|
|
4
4
|
description: >
|
|
5
5
|
Clears out last week's completed recurring-task checklist and creates a
|
|
@@ -16,7 +16,7 @@ command: >
|
|
|
16
16
|
if [ ! -f "{{template_path}}" ]; then
|
|
17
17
|
echo "template not found at {{template_path}} — nothing to reset from.";
|
|
18
18
|
exit 1;
|
|
19
|
-
fi
|
|
19
|
+
fi;
|
|
20
20
|
week_label=$(date +%G-W%V);
|
|
21
21
|
dest="{{output_dir}}/${week_label}.md";
|
|
22
22
|
mkdir -p "{{output_dir}}";
|
|
@@ -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.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
id: open-port-check
|
|
2
|
-
version:
|
|
2
|
+
version: 2
|
|
3
3
|
name: Expected Open Ports Check
|
|
4
4
|
description: >
|
|
5
5
|
Checks that the ports you expect to be open on a host actually are, and
|
|
@@ -16,7 +16,7 @@ command: >
|
|
|
16
16
|
if command -v nc >/dev/null 2>&1; then
|
|
17
17
|
nc -z -w2 "$host" "$p" 2>/dev/null && echo "OK: $host:$p open" || { echo "MISSING: $host:$p expected open but isn't"; fail=1; };
|
|
18
18
|
else
|
|
19
|
-
(echo > /dev/tcp/$host/$p) 2>/dev/null && echo "OK: $host:$p open" || { echo "MISSING: $host:$p expected open but isn't"; fail=1; };
|
|
19
|
+
(echo > "/dev/tcp/$host/$p") 2>/dev/null && echo "OK: $host:$p open" || { echo "MISSING: $host:$p expected open but isn't"; fail=1; };
|
|
20
20
|
fi
|
|
21
21
|
done;
|
|
22
22
|
exit $fail
|
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"
|
|
@@ -32,7 +33,9 @@
|
|
|
32
33
|
},
|
|
33
34
|
"scripts": {
|
|
34
35
|
"build-catalog": "node scripts/build-catalog.js",
|
|
35
|
-
"validate": "node scripts/validate-jobs.js"
|
|
36
|
+
"validate": "node scripts/validate-jobs.js",
|
|
37
|
+
"lint-shell": "node scripts/lint-shell.js",
|
|
38
|
+
"test": "node --test"
|
|
36
39
|
},
|
|
37
40
|
"devDependencies": {
|
|
38
41
|
"ajv": "^8.17.1",
|