cronfish 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  <h1 align="center">cronfish 🐟</h1>
6
6
 
7
- <p align="center"><strong>A file-based job scheduler for macOS. Drop a script in a folder; launchd runs it on a schedule.</strong></p>
7
+ <p align="center"><strong>Write a cron job in Markdown — an LLM runs it on a schedule. Or drop a <code>.ts</code>/<code>.sh</code> script for the deterministic stuff. launchd fires all three.</strong></p>
8
8
 
9
9
  <p align="center">
10
10
  <a href="https://www.npmjs.com/package/cronfish"><img src="https://img.shields.io/npm/v/cronfish?logo=npm" alt="npm"></a>
@@ -14,9 +14,24 @@
14
14
  <a href="./LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT License"></a>
15
15
  </p>
16
16
 
17
- Cronfish is for personal automation the small scheduled jobs you'd otherwise hand-roll as
18
- launchd plists or crontab lines. Drop a `.md` (Claude prompt), `.ts` (Bun program), or `.sh`
19
- (bash script) in `cron/`, run `cronfish sync`, and launchd takes it from there.
17
+ The pitch in one file. This is a complete, scheduled cron job:
18
+
19
+ ```markdown
20
+ ---
21
+ schedule: "every morning at 8"
22
+ model: sonnet
23
+ ---
24
+
25
+ Read my calendar for today, check the weather, and post a
26
+ one-paragraph brief to the #daily Slack channel.
27
+ ```
28
+
29
+ Drop it in `cron/`, run `cronfish sync`, and launchd runs it every morning — the body _is_ the job,
30
+ handed to an LLM at fire time. No script to write, no glue code. **Markdown is a valid cron job.**
31
+
32
+ When the work _is_ deterministic — a backup, a sync, a healthcheck — write it as `.ts` (Bun) or
33
+ `.sh` (bash) instead. Same frontmatter, same `cronfish sync`, no LLM in the loop. One scheduler,
34
+ three tiers; reach for the lightest that does the job.
20
35
 
21
36
  It makes the file the job: frontmatter is the schedule, the path is the slug — no hand-written
22
37
  plists, no registration step. You also get per-run logs, retries, concurrency guards, failure
@@ -31,20 +46,24 @@ every kind.
31
46
 
32
47
  ## Why cronfish?
33
48
 
34
- Claude's native scheduled agents are the obvious alternative, and for pure-LLM tasks they're great
35
- — zero setup, nothing to host. But everything runs on Anthropic's managed runtime: you pay their
36
- rates, and every job is a Claude job. Cronfish trades that convenience for control:
49
+ Two kinds of scheduler exist today, and neither does both halves:
50
+
51
+ - **cron / launchd** run scripts, never prose. A natural-language job isn't expressible.
52
+ - **Claude's native scheduled agents** run prose, but _only_ prose — every job is an LLM job on
53
+ Anthropic's managed runtime, at their rates.
37
54
 
38
- - **Any job, not just LLM jobs.** Most scheduled work is plain bash or a TypeScript script — a
39
- backup, a sync, a healthcheck. Cronfish runs those first-class, alongside `.md` Claude prompts.
40
- - **Any harness.** `.md` jobs shell out to a CLI you choose Claude Code by default, or another
41
- agent runner so you're not locked to one model or one vendor.
42
- - **Local, so you control the bill.** Jobs run on your machine against whatever model you point
43
- them at (hosted Claude, a local Ollama model, a LAN LiteLLM box). No managed-runtime markup, and
44
- your data stays on your hardware.
55
+ cronfish runs both from one folder:
56
+
57
+ - **Markdown jobs are natural-language cron.** The body is the instruction; cronfish hands it to an
58
+ agent CLI (Claude Code by default) at fire time. No other scheduler runs a prose job.
59
+ - **Script jobs stay deterministic.** `.ts` and `.sh` run code you wrote the standard cron trust
60
+ model, first-class alongside `.md`.
61
+ - **Any harness, any model, local or hosted.** `.md` jobs shell out to a CLI you choose — point them
62
+ at hosted Claude, a local Ollama model, or a LAN LiteLLM box. No managed-runtime markup; your data
63
+ stays on your hardware.
45
64
 
46
65
  Reach for Claude's scheduler when you want zero-ops and a single LLM task. Reach for cronfish when
47
- you have a mix of scripts and agents, care about cost, or want to choose the model.
66
+ you want a mix of prose and scripts, care about cost, or want to choose the model.
48
67
 
49
68
  ## Quickstart
50
69
 
@@ -79,7 +98,7 @@ you can document a folder of crons without the README getting parsed as a job.
79
98
  ```markdown
80
99
  ---
81
100
  schedule: "every 5 minutes" # see below for all accepted shapes
82
- model: haiku # claude alias | raw ID | local:<name>
101
+ model: haiku # claude alias | raw ID | local:<name> | subconscious/<name>
83
102
  enabled: true # default true
84
103
  timeout: 300 # seconds; runner kills past this
85
104
  retries: 0 # retry count on non-zero exit
@@ -129,7 +148,7 @@ invokes the file as `/bin/bash <path>` with `cwd = consumer repo root`; stdout/s
129
148
  per-run log. **A `.sh` file with no frontmatter block fails at discovery** — cronfish prints the
130
149
  error in `list`/`sync` so you know to add one.
131
150
 
132
- ## `model:` — claude alias, raw ID, or local
151
+ ## `model:` — claude alias, raw ID, local, or subconscious
133
152
 
134
153
  For Anthropic-hosted models, use the aliases `haiku` / `sonnet` / `opus` (resolve to the latest
135
154
  pinned IDs), or pass a raw ID like `claude-sonnet-4-6` verbatim.
@@ -153,6 +172,13 @@ will hallucinate tool calls. Use 14B+ for any agentic loop; 32B is the practical
153
172
  multi-step work. Local providers serve one request at a time — set `concurrency: queue` on
154
173
  overlapping jobs.
155
174
 
175
+ For a **[Subconscious](https://subconscious.dev)-hosted model**, use the id verbatim with its
176
+ `subconscious/` prefix — e.g. `subconscious/glm-5.2`. Same mechanics as `local:` (base URL +
177
+ slot overrides injected at spawn), pointed at `https://api.subconscious.dev` and authenticated
178
+ with `SUBCONSCIOUS_API_KEY` from the consumer `.env` (scope it into the job with
179
+ `env: [SUBCONSCIOUS_API_KEY]`). Override the endpoint with `SUBCONSCIOUS_BASE_URL`. The job
180
+ fails with a clear error if the key is unset.
181
+
156
182
  ## One-shot jobs — `cron/one-time/`
157
183
 
158
184
  Drop a `.md`, `.ts`, or `.sh` under `cron/one-time/` to schedule a job that
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cronfish",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "A file-based job scheduler for macOS. Drop a script in a folder; launchd runs it on a schedule.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/models.ts CHANGED
@@ -8,8 +8,12 @@
8
8
  // Messages natively; LiteLLM proxies it for everything else). The model
9
9
  // ID after the `local:` prefix is passed verbatim to `--model` and as
10
10
  // the three slot overrides so sub-agents route locally too.
11
+ //
12
+ // `subconscious/<id>` models route the same way to api.subconscious.dev
13
+ // (a hosted Anthropic-compatible endpoint), authenticated with
14
+ // SUBCONSCIOUS_API_KEY from the job env.
11
15
 
12
- export type Provider = "anthropic" | "local";
16
+ export type Provider = "anthropic" | "local" | "subconscious";
13
17
 
14
18
  export interface Resolved {
15
19
  provider: Provider;
@@ -27,6 +31,11 @@ export function resolveModel(input: string | undefined): Resolved {
27
31
  if (raw.startsWith("local:")) {
28
32
  return { provider: "local", id: raw.slice("local:".length) };
29
33
  }
34
+ if (raw.startsWith("subconscious/")) {
35
+ // Subconscious's model namespace includes the prefix (e.g.
36
+ // subconscious/glm-5.2) — pass the id through verbatim.
37
+ return { provider: "subconscious", id: raw };
38
+ }
30
39
  if (ALIASES[raw]) return { provider: "anthropic", id: ALIASES[raw] };
31
40
  return { provider: "anthropic", id: raw };
32
41
  }
@@ -39,6 +48,38 @@ export function localClaudeEnv(modelId: string): Record<string, string> {
39
48
  const baseUrl =
40
49
  process.env.CRONFISH_LOCAL_BASE_URL ?? "http://localhost:11434";
41
50
  const authToken = process.env.CRONFISH_LOCAL_AUTH_TOKEN ?? "ollama";
51
+ return backendClaudeEnv(baseUrl, authToken, modelId);
52
+ }
53
+
54
+ // Env block for a `subconscious/*` model — api.subconscious.dev speaks the
55
+ // Anthropic Messages API. Requires SUBCONSCIOUS_API_KEY in the job env
56
+ // (consumer .env; scope with frontmatter `env: [SUBCONSCIOUS_API_KEY]`).
57
+ export function subconsciousClaudeEnv(modelId: string): Record<string, string> {
58
+ const baseUrl = (
59
+ process.env.SUBCONSCIOUS_BASE_URL ?? "https://api.subconscious.dev"
60
+ ).replace(/\/$/, "");
61
+ const authToken = process.env.SUBCONSCIOUS_API_KEY;
62
+ if (!authToken) {
63
+ throw new Error(
64
+ "[models] SUBCONSCIOUS_API_KEY is not set — required for subconscious/* models",
65
+ );
66
+ }
67
+ return backendClaudeEnv(baseUrl, authToken, modelId);
68
+ }
69
+
70
+ // Env block for spawning `claude` against the given provider, or undefined
71
+ // for api.anthropic.com models. Single dispatch point for the runner.
72
+ export function claudeEnvFor(r: Resolved): Record<string, string> | undefined {
73
+ if (r.provider === "local") return localClaudeEnv(r.id);
74
+ if (r.provider === "subconscious") return subconsciousClaudeEnv(r.id);
75
+ return undefined;
76
+ }
77
+
78
+ function backendClaudeEnv(
79
+ baseUrl: string,
80
+ authToken: string,
81
+ modelId: string,
82
+ ): Record<string, string> {
42
83
  return {
43
84
  ANTHROPIC_BASE_URL: baseUrl,
44
85
  ANTHROPIC_AUTH_TOKEN: authToken,
package/src/runner.ts CHANGED
@@ -35,7 +35,7 @@ import {
35
35
  type InvocationTrigger,
36
36
  } from "./db.ts";
37
37
  import { loadJob, slugFromPath, type JobMeta } from "./jobs.ts";
38
- import { resolveModel, localClaudeEnv } from "./models.ts";
38
+ import { resolveModel, claudeEnvFor } from "./models.ts";
39
39
  import {
40
40
  DEFAULT_GRACE_SECONDS,
41
41
  archiveOneTime,
@@ -367,11 +367,11 @@ async function execMarkdown(
367
367
  appendLog(fd, `[runner] read-only: deny [${READ_ONLY_DENY.join(", ")}]`);
368
368
  }
369
369
  const cmd = buildClaudeArgs(CLAUDE_BIN, job, model.id, prompt);
370
- const env = model.provider === "local" ? localClaudeEnv(model.id) : undefined;
370
+ const env = claudeEnvFor(model);
371
371
  if (env) {
372
372
  appendLog(
373
373
  fd,
374
- `[runner] local base_url=${env.ANTHROPIC_BASE_URL} model=${model.id}`,
374
+ `[runner] ${model.provider} base_url=${env.ANTHROPIC_BASE_URL} model=${model.id}`,
375
375
  );
376
376
  }
377
377
  return runSpawn({ cmd, cwd: consumerRoot(), env }, fd, timeoutS);