@wonsukchoi/crondex 0.9.1 → 0.11.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 +94 -120
- package/bin/crondex.js +44 -63
- package/catalog.json +186 -3
- 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/inventory/low-stock-alert.yaml +7 -4
- package/jobs/legal/court-deadline-reminder.yaml +40 -0
- package/jobs/personal/downloads-folder-auto-organize.yaml +34 -0
- package/jobs/realestate/property-maintenance-inspection-reminder.yaml +35 -0
- package/jobs/sales/form-lead-crm-sync-check.yaml +36 -0
- package/lib/recommend.js +62 -0
- package/package.json +9 -4
package/README.md
CHANGED
|
@@ -1,97 +1,104 @@
|
|
|
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
|
-
|
|
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
|
|
41
38
|
|
|
42
|
-
`
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
Add `--json` to `list`/`categories`/`show`/`recommend` for machine-readable
|
|
40
|
+
output — useful when an agent is parsing the result programmatically
|
|
41
|
+
instead of a human reading it.
|
|
45
42
|
|
|
46
|
-
|
|
43
|
+
No install needed — `npx` always runs against the latest catalog. Prefer
|
|
44
|
+
installing once? `npm install -g @wonsukchoi/crondex`, then drop the `npx`
|
|
45
|
+
prefix (run `npm update -g` later to pick up new jobs). Prefer no npm at
|
|
46
|
+
all? `git clone` this repo and run `node bin/crondex.js list`.
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
npm install -g @wonsukchoi/crondex
|
|
50
|
-
crondex list
|
|
51
|
-
```
|
|
48
|
+
---
|
|
52
49
|
|
|
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).
|
|
50
|
+
## What's in a job
|
|
57
51
|
|
|
58
|
-
|
|
52
|
+
Every job is one YAML file:
|
|
59
53
|
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
|
|
54
|
+
```yaml
|
|
55
|
+
id: dependency-audit
|
|
56
|
+
version: 1
|
|
57
|
+
name: Dependency Vulnerability Audit
|
|
58
|
+
category: devops
|
|
59
|
+
schedule: "0 8 * * 1" # standard 5-field cron
|
|
60
|
+
runner: hybrid # shell | agent-prompt | hybrid
|
|
61
|
+
command: | # for runner: shell/hybrid
|
|
62
|
+
...raw shell audit, zero tokens...
|
|
63
|
+
prompt: | # for runner: agent-prompt/hybrid
|
|
64
|
+
...instructions with {{repo_path}}, LLM synthesizes+prioritizes...
|
|
65
|
+
script_note: what you lose by using `command` instead of `prompt`
|
|
66
|
+
variables:
|
|
67
|
+
repo_path:
|
|
68
|
+
default: "."
|
|
69
|
+
compatible_agents: [claude, codex, hermes, openclaw, generic]
|
|
63
70
|
```
|
|
64
71
|
|
|
65
|
-
|
|
72
|
+
- **`shell`** — runs `command` only. Zero LLM tokens, deterministic.
|
|
73
|
+
- **`agent-prompt`** — hands `prompt` to an LLM each run. Costs tokens, but
|
|
74
|
+
can synthesize, prioritize, and draft prose.
|
|
75
|
+
- **`hybrid`** — ships both, pick per run: script to save tokens, prompt for
|
|
76
|
+
more judgment. `script_note` explains the tradeoff.
|
|
66
77
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
├── build-catalog.js regenerates catalog.json + the summary below from jobs/**/*.yaml
|
|
75
|
-
└── validate-jobs.js validates every job against the schema
|
|
76
|
-
```
|
|
78
|
+
`{{placeholders}}` resolve from `variables` — override them for your case,
|
|
79
|
+
then hand `command`/`prompt` plus `schedule` to whatever scheduler you have
|
|
80
|
+
(system crontab, a hosted cron, your agent's own scheduling mechanism). This
|
|
81
|
+
repo defines *what* to run and *when*, not the executor. Full field spec:
|
|
82
|
+
[`schema/job.schema.json`](schema/job.schema.json).
|
|
83
|
+
|
|
84
|
+
---
|
|
77
85
|
|
|
78
|
-
##
|
|
86
|
+
## Browse the catalog
|
|
79
87
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
from what's actually in `jobs/`.
|
|
88
|
+
The table below is regenerated by `npm run build-catalog`, so it never
|
|
89
|
+
drifts from what's actually in `jobs/`. For full details (description,
|
|
90
|
+
tags, variables) use `crondex list`, `crondex recommend`, or browse
|
|
91
|
+
`jobs/<category>/` directly.
|
|
85
92
|
|
|
86
93
|
<!-- BEGIN JOB SUMMARY -->
|
|
87
|
-
|
|
94
|
+
189 jobs across 37 categories:
|
|
88
95
|
|
|
89
96
|
| category | jobs |
|
|
90
97
|
|---|---|
|
|
91
98
|
| `agriculture` | 3 |
|
|
92
99
|
| `construction` | 3 |
|
|
93
|
-
| `content` |
|
|
94
|
-
| `creator` |
|
|
100
|
+
| `content` | 13 |
|
|
101
|
+
| `creator` | 4 |
|
|
95
102
|
| `crypto` | 3 |
|
|
96
103
|
| `devops` | 26 |
|
|
97
104
|
| `ecommerce` | 3 |
|
|
@@ -100,7 +107,7 @@ from what's actually in `jobs/`.
|
|
|
100
107
|
| `finance` | 10 |
|
|
101
108
|
| `fleet` | 3 |
|
|
102
109
|
| `gaming` | 3 |
|
|
103
|
-
| `growth` |
|
|
110
|
+
| `growth` | 5 |
|
|
104
111
|
| `healthcare` | 3 |
|
|
105
112
|
| `hiring` | 3 |
|
|
106
113
|
| `home` | 5 |
|
|
@@ -110,80 +117,47 @@ from what's actually in `jobs/`.
|
|
|
110
117
|
| `inventory` | 3 |
|
|
111
118
|
| `investing` | 3 |
|
|
112
119
|
| `learning` | 5 |
|
|
113
|
-
| `legal` |
|
|
120
|
+
| `legal` | 4 |
|
|
114
121
|
| `logistics` | 3 |
|
|
115
122
|
| `manufacturing` | 3 |
|
|
116
123
|
| `marketing` | 4 |
|
|
117
124
|
| `nonprofit` | 3 |
|
|
118
|
-
| `personal` |
|
|
125
|
+
| `personal` | 9 |
|
|
119
126
|
| `podcast` | 3 |
|
|
120
127
|
| `productivity` | 9 |
|
|
121
|
-
| `realestate` |
|
|
128
|
+
| `realestate` | 4 |
|
|
122
129
|
| `restaurant` | 3 |
|
|
123
|
-
| `sales` |
|
|
130
|
+
| `sales` | 5 |
|
|
124
131
|
| `security` | 14 |
|
|
125
132
|
| `support` | 3 |
|
|
126
133
|
| `team` | 4 |
|
|
127
134
|
| `travel` | 5 |
|
|
128
135
|
<!-- END JOB SUMMARY -->
|
|
129
136
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
Every job is one YAML file with a `runner`:
|
|
133
|
-
|
|
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.
|
|
137
|
+
---
|
|
142
138
|
|
|
143
|
-
|
|
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.
|
|
139
|
+
## Layout
|
|
147
140
|
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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]
|
|
141
|
+
```
|
|
142
|
+
crondex/
|
|
143
|
+
├── llms.txt agent-discovery manifest (llms.txt convention)
|
|
144
|
+
├── bin/crondex.js CLI: list / categories / show / add / recommend / init
|
|
145
|
+
├── lib/recommend.js recommend's scoring logic (unit tested in test/)
|
|
146
|
+
├── catalog.json generated index of every job — read this first
|
|
147
|
+
├── schema/job.schema.json spec every job file follows
|
|
148
|
+
├── jobs/ one YAML per job, grouped by category subdirectory
|
|
149
|
+
└── scripts/ build-catalog.js, validate-jobs.js, lint-shell.js
|
|
168
150
|
```
|
|
169
151
|
|
|
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.
|
|
152
|
+
---
|
|
182
153
|
|
|
183
154
|
## Contributing a job
|
|
184
155
|
|
|
185
|
-
See [`CONTRIBUTING.md`](CONTRIBUTING.md) — copy `templates/job.template.yaml
|
|
186
|
-
fill it in, `npm run validate && npm run
|
|
156
|
+
See [`CONTRIBUTING.md`](CONTRIBUTING.md) — copy `templates/job.template.yaml`
|
|
157
|
+
(or run `crondex init`), fill it in, `npm run validate && npm run
|
|
158
|
+
build-catalog`, open a PR.
|
|
159
|
+
|
|
160
|
+
---
|
|
187
161
|
|
|
188
162
|
## License
|
|
189
163
|
|
package/bin/crondex.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
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 yaml from "js-yaml";
|
|
6
|
+
import { tokenize, rankJobs } from "../lib/recommend.js";
|
|
5
7
|
|
|
6
8
|
const ROOT = new URL("..", import.meta.url).pathname;
|
|
7
9
|
const CATALOG = JSON.parse(readFileSync(join(ROOT, "catalog.json"), "utf8"));
|
|
@@ -14,6 +16,14 @@ function flag(name) {
|
|
|
14
16
|
return i === -1 ? undefined : args[i + 1];
|
|
15
17
|
}
|
|
16
18
|
|
|
19
|
+
function hasFlag(name) {
|
|
20
|
+
return args.includes(`--${name}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function printJson(value) {
|
|
24
|
+
console.log(JSON.stringify(value, null, 2));
|
|
25
|
+
}
|
|
26
|
+
|
|
17
27
|
function findJob(id) {
|
|
18
28
|
const meta = CATALOG.jobs.find((j) => j.id === id);
|
|
19
29
|
if (!meta) {
|
|
@@ -27,11 +37,11 @@ function printHelp() {
|
|
|
27
37
|
console.log(`crondex — browse and pull pre-made cron jobs
|
|
28
38
|
|
|
29
39
|
Usage:
|
|
30
|
-
crondex list [--category <name>] [--tag <name>]
|
|
31
|
-
crondex categories
|
|
32
|
-
crondex show <id>
|
|
40
|
+
crondex list [--category <name>] [--tag <name>] [--json]
|
|
41
|
+
crondex categories [--json]
|
|
42
|
+
crondex show <id> [--json]
|
|
33
43
|
crondex add <id> [--dest <path>]
|
|
34
|
-
crondex recommend "<what you want done>" [--limit <n>]
|
|
44
|
+
crondex recommend "<what you want done>" [--limit <n>] [--json]
|
|
35
45
|
crondex init <id> [--category <name>] [--dest <path>]
|
|
36
46
|
|
|
37
47
|
Examples:
|
|
@@ -41,75 +51,39 @@ Examples:
|
|
|
41
51
|
crondex add backup-reminder --dest ./cron/backup-reminder.yaml
|
|
42
52
|
crondex recommend "warn me before my SSL cert expires"
|
|
43
53
|
crondex init ssl-cert-expiry-check --category security
|
|
44
|
-
`);
|
|
45
|
-
}
|
|
46
|
-
|
|
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
54
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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] };
|
|
55
|
+
Add --json to list/categories/show/recommend for machine-readable output —
|
|
56
|
+
useful when an agent is parsing crondex's output programmatically instead
|
|
57
|
+
of a human reading it.
|
|
58
|
+
`);
|
|
95
59
|
}
|
|
96
60
|
|
|
97
61
|
function recommend(queryText) {
|
|
98
62
|
const limit = Number(flag("limit")) || 5;
|
|
99
|
-
const
|
|
100
|
-
if (!
|
|
63
|
+
const json = hasFlag("json");
|
|
64
|
+
if (!tokenize(queryText).length) {
|
|
65
|
+
if (json) return printJson([]);
|
|
101
66
|
console.log("query too vague to match on — describe what you want the job to check or remind you about.");
|
|
102
67
|
return;
|
|
103
68
|
}
|
|
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);
|
|
69
|
+
const ranked = rankJobs(CATALOG.jobs, queryText, limit);
|
|
109
70
|
if (!ranked.length) {
|
|
71
|
+
if (json) return printJson([]);
|
|
110
72
|
console.log(`no confident match for "${queryText}". Run "crondex list" to browse everything.`);
|
|
111
73
|
return;
|
|
112
74
|
}
|
|
75
|
+
if (json) {
|
|
76
|
+
return printJson(
|
|
77
|
+
ranked.map((r) => ({
|
|
78
|
+
id: r.job.id,
|
|
79
|
+
category: r.job.category,
|
|
80
|
+
score: r.score,
|
|
81
|
+
matched_terms: r.matchedTerms,
|
|
82
|
+
modes: r.job.modes,
|
|
83
|
+
description: r.job.description,
|
|
84
|
+
}))
|
|
85
|
+
);
|
|
86
|
+
}
|
|
113
87
|
console.log(`top match${ranked.length > 1 ? "es" : ""} for "${queryText}":`);
|
|
114
88
|
console.log();
|
|
115
89
|
for (const r of ranked) {
|
|
@@ -131,9 +105,11 @@ function catalogInfoLine() {
|
|
|
131
105
|
function list() {
|
|
132
106
|
const category = flag("category");
|
|
133
107
|
const tag = flag("tag");
|
|
108
|
+
const json = hasFlag("json");
|
|
134
109
|
const jobs = CATALOG.jobs.filter(
|
|
135
110
|
(j) => (!category || j.category === category) && (!tag || j.tags.includes(tag))
|
|
136
111
|
);
|
|
112
|
+
if (json) return printJson(jobs);
|
|
137
113
|
console.log(catalogInfoLine());
|
|
138
114
|
console.log();
|
|
139
115
|
if (!jobs.length) {
|
|
@@ -148,15 +124,20 @@ function list() {
|
|
|
148
124
|
}
|
|
149
125
|
|
|
150
126
|
function show(id) {
|
|
151
|
-
|
|
127
|
+
const meta = findJob(id);
|
|
128
|
+
const raw = readFileSync(join(ROOT, meta.path), "utf8");
|
|
129
|
+
if (hasFlag("json")) return printJson(yaml.load(raw));
|
|
130
|
+
console.log(raw);
|
|
152
131
|
}
|
|
153
132
|
|
|
154
133
|
function categories() {
|
|
155
134
|
const counts = {};
|
|
156
135
|
for (const j of CATALOG.jobs) counts[j.category] = (counts[j.category] ?? 0) + 1;
|
|
136
|
+
const sorted = Object.entries(counts).sort(([a], [b]) => a.localeCompare(b));
|
|
137
|
+
if (hasFlag("json")) return printJson(sorted.map(([category, count]) => ({ category, count })));
|
|
157
138
|
console.log(catalogInfoLine());
|
|
158
139
|
console.log();
|
|
159
|
-
for (const [cat, n] of
|
|
140
|
+
for (const [cat, n] of sorted) {
|
|
160
141
|
console.log(`${cat} (${n})`);
|
|
161
142
|
}
|
|
162
143
|
}
|
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": 189,
|
|
5
5
|
"jobs": [
|
|
6
6
|
{
|
|
7
7
|
"id": "1on1-prep-reminder",
|
|
@@ -809,6 +809,31 @@
|
|
|
809
809
|
],
|
|
810
810
|
"path": "jobs/learning/course-progress-checkin.yaml"
|
|
811
811
|
},
|
|
812
|
+
{
|
|
813
|
+
"id": "court-deadline-reminder",
|
|
814
|
+
"version": 1,
|
|
815
|
+
"name": "Court Deadline Reminder",
|
|
816
|
+
"description": "Checks a tracked list of court/filing deadlines and warns before any come due. Use this if a filing deadline has ever been missed because it lived only in one person's head or a sticky note instead of a shared, checked calendar.",
|
|
817
|
+
"category": "legal",
|
|
818
|
+
"tags": [
|
|
819
|
+
"legal",
|
|
820
|
+
"court",
|
|
821
|
+
"deadline",
|
|
822
|
+
"compliance"
|
|
823
|
+
],
|
|
824
|
+
"schedule": "0 7 * * *",
|
|
825
|
+
"timezone": "UTC",
|
|
826
|
+
"runner": "shell",
|
|
827
|
+
"modes": [
|
|
828
|
+
"script"
|
|
829
|
+
],
|
|
830
|
+
"compatible_agents": [
|
|
831
|
+
"generic",
|
|
832
|
+
"claude",
|
|
833
|
+
"codex"
|
|
834
|
+
],
|
|
835
|
+
"path": "jobs/legal/court-deadline-reminder.yaml"
|
|
836
|
+
},
|
|
812
837
|
{
|
|
813
838
|
"id": "coverage-gap-audit",
|
|
814
839
|
"version": 1,
|
|
@@ -1186,6 +1211,31 @@
|
|
|
1186
1211
|
],
|
|
1187
1212
|
"path": "jobs/devops/docker-image-prune.yaml"
|
|
1188
1213
|
},
|
|
1214
|
+
{
|
|
1215
|
+
"id": "downloads-folder-auto-organize",
|
|
1216
|
+
"version": 1,
|
|
1217
|
+
"name": "Downloads Folder Auto-Organize",
|
|
1218
|
+
"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.",
|
|
1219
|
+
"category": "personal",
|
|
1220
|
+
"tags": [
|
|
1221
|
+
"files",
|
|
1222
|
+
"organization",
|
|
1223
|
+
"downloads",
|
|
1224
|
+
"cleanup"
|
|
1225
|
+
],
|
|
1226
|
+
"schedule": "0 3 * * *",
|
|
1227
|
+
"timezone": "UTC",
|
|
1228
|
+
"runner": "shell",
|
|
1229
|
+
"modes": [
|
|
1230
|
+
"script"
|
|
1231
|
+
],
|
|
1232
|
+
"compatible_agents": [
|
|
1233
|
+
"generic",
|
|
1234
|
+
"claude",
|
|
1235
|
+
"codex"
|
|
1236
|
+
],
|
|
1237
|
+
"path": "jobs/personal/downloads-folder-auto-organize.yaml"
|
|
1238
|
+
},
|
|
1189
1239
|
{
|
|
1190
1240
|
"id": "email-deliverability-check",
|
|
1191
1241
|
"version": 1,
|
|
@@ -1581,6 +1631,33 @@
|
|
|
1581
1631
|
],
|
|
1582
1632
|
"path": "jobs/hospitality/food-safety-temp-log-check.yaml"
|
|
1583
1633
|
},
|
|
1634
|
+
{
|
|
1635
|
+
"id": "form-lead-crm-sync-check",
|
|
1636
|
+
"version": 1,
|
|
1637
|
+
"name": "Form Lead CRM Sync Check",
|
|
1638
|
+
"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.",
|
|
1639
|
+
"category": "sales",
|
|
1640
|
+
"tags": [
|
|
1641
|
+
"sales",
|
|
1642
|
+
"leads",
|
|
1643
|
+
"crm",
|
|
1644
|
+
"integration"
|
|
1645
|
+
],
|
|
1646
|
+
"schedule": "*/30 * * * *",
|
|
1647
|
+
"timezone": "UTC",
|
|
1648
|
+
"runner": "agent-prompt",
|
|
1649
|
+
"modes": [
|
|
1650
|
+
"agent-prompt"
|
|
1651
|
+
],
|
|
1652
|
+
"compatible_agents": [
|
|
1653
|
+
"claude",
|
|
1654
|
+
"codex",
|
|
1655
|
+
"hermes",
|
|
1656
|
+
"openclaw",
|
|
1657
|
+
"generic"
|
|
1658
|
+
],
|
|
1659
|
+
"path": "jobs/sales/form-lead-crm-sync-check.yaml"
|
|
1660
|
+
},
|
|
1584
1661
|
{
|
|
1585
1662
|
"id": "freight-rate-spike-watch",
|
|
1586
1663
|
"version": 1,
|
|
@@ -2333,12 +2410,12 @@
|
|
|
2333
2410
|
"id": "low-stock-alert",
|
|
2334
2411
|
"version": 1,
|
|
2335
2412
|
"name": "Low Stock Alert",
|
|
2336
|
-
"description": "Checks inventory
|
|
2413
|
+
"description": "Checks a general-purpose inventory database (warehouse, retail backroom, restaurant supplies — anything with a quantity and a set reorder point) against that fixed threshold and flags what's running low. Use this for physical stock tracked in a database, not tied to online sales; if you specifically need SKU reorder timing driven by e-commerce sales velocity and supplier lead time, see `low-stock-reorder-alert` instead.",
|
|
2337
2414
|
"category": "inventory",
|
|
2338
2415
|
"tags": [
|
|
2339
2416
|
"inventory",
|
|
2340
2417
|
"stock",
|
|
2341
|
-
"
|
|
2418
|
+
"warehouse"
|
|
2342
2419
|
],
|
|
2343
2420
|
"schedule": "0 7 * * *",
|
|
2344
2421
|
"timezone": "UTC",
|
|
@@ -2980,6 +3057,31 @@
|
|
|
2980
3057
|
],
|
|
2981
3058
|
"path": "jobs/manufacturing/production-line-downtime-watch.yaml"
|
|
2982
3059
|
},
|
|
3060
|
+
{
|
|
3061
|
+
"id": "property-maintenance-inspection-reminder",
|
|
3062
|
+
"version": 1,
|
|
3063
|
+
"name": "Property Maintenance Inspection Reminder",
|
|
3064
|
+
"description": "Tracks when each property was last inspected and flags any that are due for their next routine walkthrough. Use this if a property has ever gone a year or more without a proper inspection because nothing forced anyone to schedule one.",
|
|
3065
|
+
"category": "realestate",
|
|
3066
|
+
"tags": [
|
|
3067
|
+
"realestate",
|
|
3068
|
+
"maintenance",
|
|
3069
|
+
"inspection",
|
|
3070
|
+
"property"
|
|
3071
|
+
],
|
|
3072
|
+
"schedule": "0 8 * * 1",
|
|
3073
|
+
"timezone": "UTC",
|
|
3074
|
+
"runner": "shell",
|
|
3075
|
+
"modes": [
|
|
3076
|
+
"script"
|
|
3077
|
+
],
|
|
3078
|
+
"compatible_agents": [
|
|
3079
|
+
"generic",
|
|
3080
|
+
"claude",
|
|
3081
|
+
"codex"
|
|
3082
|
+
],
|
|
3083
|
+
"path": "jobs/realestate/property-maintenance-inspection-reminder.yaml"
|
|
3084
|
+
},
|
|
2983
3085
|
{
|
|
2984
3086
|
"id": "pto-balance-check",
|
|
2985
3087
|
"version": 1,
|
|
@@ -3397,6 +3499,33 @@
|
|
|
3397
3499
|
],
|
|
3398
3500
|
"path": "jobs/content/rss-feed-validate.yaml"
|
|
3399
3501
|
},
|
|
3502
|
+
{
|
|
3503
|
+
"id": "rss-to-social-repurpose",
|
|
3504
|
+
"version": 1,
|
|
3505
|
+
"name": "RSS-to-Social Repurpose Draft",
|
|
3506
|
+
"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.",
|
|
3507
|
+
"category": "content",
|
|
3508
|
+
"tags": [
|
|
3509
|
+
"content",
|
|
3510
|
+
"rss",
|
|
3511
|
+
"social",
|
|
3512
|
+
"repurposing"
|
|
3513
|
+
],
|
|
3514
|
+
"schedule": "0 10 * * *",
|
|
3515
|
+
"timezone": "UTC",
|
|
3516
|
+
"runner": "agent-prompt",
|
|
3517
|
+
"modes": [
|
|
3518
|
+
"agent-prompt"
|
|
3519
|
+
],
|
|
3520
|
+
"compatible_agents": [
|
|
3521
|
+
"claude",
|
|
3522
|
+
"codex",
|
|
3523
|
+
"hermes",
|
|
3524
|
+
"openclaw",
|
|
3525
|
+
"generic"
|
|
3526
|
+
],
|
|
3527
|
+
"path": "jobs/content/rss-to-social-repurpose.yaml"
|
|
3528
|
+
},
|
|
3400
3529
|
{
|
|
3401
3530
|
"id": "rsvp-followup-reminder",
|
|
3402
3531
|
"version": 1,
|
|
@@ -4535,6 +4664,60 @@
|
|
|
4535
4664
|
"generic"
|
|
4536
4665
|
],
|
|
4537
4666
|
"path": "jobs/productivity/weekly-report.yaml"
|
|
4667
|
+
},
|
|
4668
|
+
{
|
|
4669
|
+
"id": "welcome-email-drip-check",
|
|
4670
|
+
"version": 1,
|
|
4671
|
+
"name": "Welcome Email Drip Check",
|
|
4672
|
+
"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?\"",
|
|
4673
|
+
"category": "growth",
|
|
4674
|
+
"tags": [
|
|
4675
|
+
"growth",
|
|
4676
|
+
"email",
|
|
4677
|
+
"onboarding",
|
|
4678
|
+
"welcome-email"
|
|
4679
|
+
],
|
|
4680
|
+
"schedule": "0 10 * * *",
|
|
4681
|
+
"timezone": "UTC",
|
|
4682
|
+
"runner": "agent-prompt",
|
|
4683
|
+
"modes": [
|
|
4684
|
+
"agent-prompt"
|
|
4685
|
+
],
|
|
4686
|
+
"compatible_agents": [
|
|
4687
|
+
"claude",
|
|
4688
|
+
"codex",
|
|
4689
|
+
"hermes",
|
|
4690
|
+
"openclaw",
|
|
4691
|
+
"generic"
|
|
4692
|
+
],
|
|
4693
|
+
"path": "jobs/growth/welcome-email-drip-check.yaml"
|
|
4694
|
+
},
|
|
4695
|
+
{
|
|
4696
|
+
"id": "youtube-clip-repurposing",
|
|
4697
|
+
"version": 1,
|
|
4698
|
+
"name": "YouTube Clip Repurposing Draft",
|
|
4699
|
+
"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.",
|
|
4700
|
+
"category": "creator",
|
|
4701
|
+
"tags": [
|
|
4702
|
+
"youtube",
|
|
4703
|
+
"video",
|
|
4704
|
+
"clips",
|
|
4705
|
+
"repurposing"
|
|
4706
|
+
],
|
|
4707
|
+
"schedule": "0 12 * * *",
|
|
4708
|
+
"timezone": "UTC",
|
|
4709
|
+
"runner": "agent-prompt",
|
|
4710
|
+
"modes": [
|
|
4711
|
+
"agent-prompt"
|
|
4712
|
+
],
|
|
4713
|
+
"compatible_agents": [
|
|
4714
|
+
"claude",
|
|
4715
|
+
"codex",
|
|
4716
|
+
"hermes",
|
|
4717
|
+
"openclaw",
|
|
4718
|
+
"generic"
|
|
4719
|
+
],
|
|
4720
|
+
"path": "jobs/creator/youtube-clip-repurposing.yaml"
|
|
4538
4721
|
}
|
|
4539
4722
|
]
|
|
4540
4723
|
}
|
|
@@ -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.
|
|
@@ -2,11 +2,14 @@ id: low-stock-alert
|
|
|
2
2
|
version: 1
|
|
3
3
|
name: Low Stock Alert
|
|
4
4
|
description: >
|
|
5
|
-
Checks
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
Checks a general-purpose inventory database (warehouse, retail backroom,
|
|
6
|
+
restaurant supplies — anything with a quantity and a set reorder point)
|
|
7
|
+
against that fixed threshold and flags what's running low. Use this for
|
|
8
|
+
physical stock tracked in a database, not tied to online sales; if you
|
|
9
|
+
specifically need SKU reorder timing driven by e-commerce sales velocity
|
|
10
|
+
and supplier lead time, see `low-stock-reorder-alert` instead.
|
|
8
11
|
category: inventory
|
|
9
|
-
tags: [inventory, stock,
|
|
12
|
+
tags: [inventory, stock, warehouse]
|
|
10
13
|
schedule: "0 7 * * *"
|
|
11
14
|
timezone: "UTC"
|
|
12
15
|
runner: shell
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
id: court-deadline-reminder
|
|
2
|
+
version: 1
|
|
3
|
+
name: Court Deadline Reminder
|
|
4
|
+
description: >
|
|
5
|
+
Checks a tracked list of court/filing deadlines and warns before any come
|
|
6
|
+
due. Use this if a filing deadline has ever been missed because it lived
|
|
7
|
+
only in one person's head or a sticky note instead of a shared, checked
|
|
8
|
+
calendar.
|
|
9
|
+
category: legal
|
|
10
|
+
tags: [legal, court, deadline, compliance]
|
|
11
|
+
schedule: "0 7 * * *"
|
|
12
|
+
timezone: "UTC"
|
|
13
|
+
runner: shell
|
|
14
|
+
command: >
|
|
15
|
+
awk -F',' -v days="{{warn_days_before}}" '
|
|
16
|
+
NR==1 {next}
|
|
17
|
+
{
|
|
18
|
+
cmd = "date -d \"" $3 "\" +%s 2>/dev/null || date -j -f %Y-%m-%d \"" $3 "\" +%s 2>/dev/null";
|
|
19
|
+
cmd | getline due_epoch; close(cmd);
|
|
20
|
+
"date +%s" | getline now_epoch; close("date +%s");
|
|
21
|
+
diff_days = (due_epoch - now_epoch) / 86400;
|
|
22
|
+
if (diff_days <= days && diff_days >= 0) {
|
|
23
|
+
printf "DUE SOON: %s — %s due %s (%d days)\n", $1, $2, $3, diff_days;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
' "{{deadlines_csv_path}}"
|
|
27
|
+
variables:
|
|
28
|
+
deadlines_csv_path:
|
|
29
|
+
default: "./court-deadlines.csv"
|
|
30
|
+
description: "Path to a CSV with columns: matter_name,deadline_type,due_date (YYYY-MM-DD) — e.g. one row per filing/response/statute-of-limitations deadline."
|
|
31
|
+
warn_days_before:
|
|
32
|
+
default: 14
|
|
33
|
+
description: How many days before a deadline to start warning.
|
|
34
|
+
compatible_agents: [generic, claude, codex]
|
|
35
|
+
notes: >
|
|
36
|
+
This is a plain date-threshold check, not a rules-based calendaring
|
|
37
|
+
engine — it won't calculate deadlines from court rules the way dedicated
|
|
38
|
+
legal calendaring software does. Export computed due dates from that
|
|
39
|
+
system into the CSV, or maintain them manually, and use this as the
|
|
40
|
+
"did anyone actually notice" backstop.
|
|
@@ -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,35 @@
|
|
|
1
|
+
id: property-maintenance-inspection-reminder
|
|
2
|
+
version: 1
|
|
3
|
+
name: Property Maintenance Inspection Reminder
|
|
4
|
+
description: >
|
|
5
|
+
Tracks when each property was last inspected and flags any that are due
|
|
6
|
+
for their next routine walkthrough. Use this if a property has ever gone
|
|
7
|
+
a year or more without a proper inspection because nothing forced anyone
|
|
8
|
+
to schedule one.
|
|
9
|
+
category: realestate
|
|
10
|
+
tags: [realestate, maintenance, inspection, property]
|
|
11
|
+
schedule: "0 8 * * 1"
|
|
12
|
+
timezone: "UTC"
|
|
13
|
+
runner: shell
|
|
14
|
+
command: >
|
|
15
|
+
awk -F',' -v days="{{inspection_interval_days}}" '
|
|
16
|
+
NR==1 {next}
|
|
17
|
+
{
|
|
18
|
+
cmd = "date -d \"" $2 "\" +%s 2>/dev/null || date -j -f %Y-%m-%d \"" $2 "\" +%s 2>/dev/null";
|
|
19
|
+
cmd | getline last_epoch; close(cmd);
|
|
20
|
+
"date +%s" | getline now_epoch; close("date +%s");
|
|
21
|
+
days_since = (now_epoch - last_epoch) / 86400;
|
|
22
|
+
if (days_since >= days) {
|
|
23
|
+
printf "DUE: %s — last inspected %s (%d days ago)\n", $1, $2, days_since;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
' "{{properties_csv_path}}"
|
|
27
|
+
variables:
|
|
28
|
+
properties_csv_path:
|
|
29
|
+
default: "./property-inspections.csv"
|
|
30
|
+
description: "Path to a CSV with columns: property_name,last_inspection_date (YYYY-MM-DD)."
|
|
31
|
+
inspection_interval_days:
|
|
32
|
+
default: 180
|
|
33
|
+
description: How often each property should be inspected, in days.
|
|
34
|
+
compatible_agents: [generic, claude, codex]
|
|
35
|
+
notes: Assumes a maintained CSV of last-inspection dates per property — export it from whatever property management system you use, or update it manually after each walkthrough.
|
|
@@ -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.11.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,10 +34,14 @@
|
|
|
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
|
+
"check-duplicates": "node scripts/check-duplicates.js",
|
|
39
|
+
"test": "node --test"
|
|
37
40
|
},
|
|
38
|
-
"
|
|
39
|
-
"ajv": "^8.17.1",
|
|
41
|
+
"dependencies": {
|
|
40
42
|
"js-yaml": "^4.1.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"ajv": "^8.17.1"
|
|
41
46
|
}
|
|
42
47
|
}
|