@karmaniverous/jeeves-runner 0.1.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/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2025, Jason Williscroft
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,401 @@
1
+ # jeeves-runner
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@karmaniverous/jeeves-runner.svg)](https://www.npmjs.com/package/@karmaniverous/jeeves-runner)
4
+ ![Node Current](https://img.shields.io/node/v/@karmaniverous/jeeves-runner)
5
+ [![license](https://img.shields.io/badge/license-BSD--3--Clause-blue.svg)](https://github.com/karmaniverous/jeeves-runner/tree/main/LICENSE.md)
6
+
7
+ Graph-aware job execution engine with SQLite state. Part of the [Jeeves platform](#the-jeeves-platform).
8
+
9
+ ## What It Does
10
+
11
+ jeeves-runner schedules and executes jobs, tracks their state in SQLite, and exposes status via a REST API. It replaces both n8n and Windows Task Scheduler as the substrate for data flow automation.
12
+
13
+ **Key properties:**
14
+
15
+ - **Domain-agnostic.** The runner knows graph primitives (source, sink, datastore, queue, process, auth), not business concepts. "Email polling" and "meeting extraction" are just jobs with scripts.
16
+ - **SQLite-native.** Job definitions, run history, cursors, and queues live in a single SQLite file. No external database, no Redis.
17
+ - **Zero new infrastructure.** One Node.js process, one SQLite file. Runs as a system service via NSSM (Windows) or systemd (Linux).
18
+ - **Scripts as config.** Job scripts live outside the runner repo at configurable absolute paths. The runner is generic; the scripts are instance-specific.
19
+
20
+ ## Architecture
21
+
22
+ ```
23
+ ┌─────────────────────────────────────────────────┐
24
+ │ jeeves-runner │
25
+ │ │
26
+ │ ┌───────────┐ ┌──────────┐ ┌──────────────┐ │
27
+ │ │ Scheduler │──│ Executor │──│ Notifier │ │
28
+ │ │ (croner) │ │ (spawn) │ │ (Slack) │ │
29
+ │ └───────────┘ └──────────┘ └──────────────┘ │
30
+ │ │
31
+ │ ┌───────────┐ ┌──────────┐ ┌──────────────┐ │
32
+ │ │ SQLite │ │ REST API │ │ Maintenance │ │
33
+ │ │ (DB) │ │(Fastify) │ │ (pruning) │ │
34
+ │ └───────────┘ └──────────┘ └──────────────┘ │
35
+ └─────────────────────────────────────────────────┘
36
+ │ │
37
+ ▼ ▼
38
+ runner.sqlite localhost:3100
39
+ ```
40
+
41
+ ### Stack
42
+
43
+ | Component | Technology |
44
+ |-----------|-----------|
45
+ | Runtime | Node.js v24+ (uses built-in `node:sqlite`) |
46
+ | Scheduler | [croner](https://www.npmjs.com/package/croner) |
47
+ | Database | SQLite via `node:sqlite` |
48
+ | Process isolation | `child_process.spawn` |
49
+ | HTTP API | [Fastify](https://fastify.dev/) |
50
+ | Logging | [pino](https://getpino.io/) |
51
+ | Config validation | [Zod](https://zod.dev/) |
52
+
53
+ ## Installation
54
+
55
+ ```bash
56
+ npm install @karmaniverous/jeeves-runner
57
+ ```
58
+
59
+ Requires Node.js 24+ for `node:sqlite` support.
60
+
61
+ ## Quick Start
62
+
63
+ ### 1. Create a config file
64
+
65
+ ```json
66
+ {
67
+ "port": 3100,
68
+ "dbPath": "./data/runner.sqlite",
69
+ "maxConcurrency": 4,
70
+ "runRetentionDays": 30,
71
+ "cursorCleanupIntervalMs": 3600000,
72
+ "shutdownGraceMs": 30000,
73
+ "notifications": {
74
+ "slackTokenPath": "./credentials/slack-bot-token",
75
+ "defaultOnFailure": "YOUR_SLACK_CHANNEL_ID",
76
+ "defaultOnSuccess": null
77
+ },
78
+ "log": {
79
+ "level": "info",
80
+ "file": "./data/runner.log"
81
+ }
82
+ }
83
+ ```
84
+
85
+ ### 2. Start the runner
86
+
87
+ ```bash
88
+ npx jeeves-runner start --config ./config.json
89
+ ```
90
+
91
+ ### 3. Add a job
92
+
93
+ ```bash
94
+ npx jeeves-runner add-job \
95
+ --id my-job \
96
+ --name "My Job" \
97
+ --schedule "*/5 * * * *" \
98
+ --script /absolute/path/to/script.js \
99
+ --config ./config.json
100
+ ```
101
+
102
+ ### 4. Check status
103
+
104
+ ```bash
105
+ npx jeeves-runner status --config ./config.json
106
+ npx jeeves-runner list-jobs --config ./config.json
107
+ ```
108
+
109
+ ## CLI Commands
110
+
111
+ | Command | Description |
112
+ |---------|-------------|
113
+ | `start` | Start the runner daemon |
114
+ | `status` | Show runner stats (queries the HTTP API) |
115
+ | `list-jobs` | List all configured jobs |
116
+ | `add-job` | Add a new job to the database |
117
+ | `trigger` | Manually trigger a job run (queries the HTTP API) |
118
+
119
+ All commands accept `--config <path>` to specify the config file.
120
+
121
+ ## HTTP API
122
+
123
+ The runner exposes a REST API on `localhost` (not externally accessible by default).
124
+
125
+ | Method | Path | Description |
126
+ |--------|------|-------------|
127
+ | `GET` | `/health` | Health check |
128
+ | `GET` | `/jobs` | List all jobs with last run status |
129
+ | `GET` | `/jobs/:id` | Single job detail |
130
+ | `GET` | `/jobs/:id/runs` | Run history (paginated via `?limit=N`) |
131
+ | `POST` | `/jobs/:id/run` | Trigger manual run |
132
+ | `POST` | `/jobs/:id/enable` | Enable a job |
133
+ | `POST` | `/jobs/:id/disable` | Disable a job |
134
+ | `GET` | `/stats` | Aggregate stats (jobs ok/error/running counts) |
135
+
136
+ ### Example response
137
+
138
+ ```json
139
+ // GET /jobs
140
+ {
141
+ "jobs": [
142
+ {
143
+ "id": "email-poll",
144
+ "name": "Poll Email",
145
+ "schedule": "*/11 * * * *",
146
+ "enabled": 1,
147
+ "last_status": "ok",
148
+ "last_run": "2026-02-24T10:30:00"
149
+ }
150
+ ]
151
+ }
152
+ ```
153
+
154
+ ## SQLite Schema
155
+
156
+ Four tables manage all runner state:
157
+
158
+ ### `jobs` — Job Definitions
159
+
160
+ Each job has an ID, name, cron schedule, script path, and behavioral configuration.
161
+
162
+ | Column | Type | Description |
163
+ |--------|------|-------------|
164
+ | `id` | TEXT PK | Job identifier (e.g. `email-poll`) |
165
+ | `name` | TEXT | Human-readable name |
166
+ | `schedule` | TEXT | Cron expression |
167
+ | `script` | TEXT | Absolute path to script |
168
+ | `type` | TEXT | `script` or `session` (LLM dispatcher) |
169
+ | `enabled` | INTEGER | 1 = active, 0 = paused |
170
+ | `timeout_ms` | INTEGER | Kill after this duration (null = no limit) |
171
+ | `overlap_policy` | TEXT | `skip` (default), `queue`, or `allow` |
172
+ | `on_failure` | TEXT | Slack channel ID for failure alerts |
173
+ | `on_success` | TEXT | Slack channel ID for success alerts |
174
+
175
+ ### `runs` — Run History
176
+
177
+ Every execution is recorded with status, timing, output capture, and optional token tracking.
178
+
179
+ | Column | Type | Description |
180
+ |--------|------|-------------|
181
+ | `id` | INTEGER PK | Auto-incrementing run ID |
182
+ | `job_id` | TEXT FK | References `jobs.id` |
183
+ | `status` | TEXT | `pending`, `running`, `ok`, `error`, `timeout`, `skipped` |
184
+ | `duration_ms` | INTEGER | Wall-clock execution time |
185
+ | `exit_code` | INTEGER | Process exit code |
186
+ | `tokens` | INTEGER | LLM token count (session jobs only) |
187
+ | `result_meta` | TEXT | JSON from `JR_RESULT:{json}` stdout lines |
188
+ | `stdout_tail` | TEXT | Last 100 lines of stdout |
189
+ | `stderr_tail` | TEXT | Last 100 lines of stderr |
190
+ | `trigger` | TEXT | `schedule`, `manual`, or `retry` |
191
+
192
+ Runs older than `runRetentionDays` are automatically pruned.
193
+
194
+ ### `cursors` — Key-Value State
195
+
196
+ General-purpose key-value store with optional TTL. Replaces JSONL registry files.
197
+
198
+ | Column | Type | Description |
199
+ |--------|------|-------------|
200
+ | `namespace` | TEXT | Logical grouping (typically job ID) |
201
+ | `key` | TEXT | State key |
202
+ | `value` | TEXT | State value (string or JSON) |
203
+ | `expires_at` | TEXT | Optional TTL (ISO timestamp, auto-cleaned) |
204
+
205
+ ### `queues` — Work Queues
206
+
207
+ Priority-ordered work queues with claim semantics. SQLite's serialized writes prevent double-claims.
208
+
209
+ | Column | Type | Description |
210
+ |--------|------|-------------|
211
+ | `id` | INTEGER PK | Auto-incrementing item ID |
212
+ | `queue` | TEXT | Queue name |
213
+ | `payload` | TEXT | JSON blob |
214
+ | `status` | TEXT | `pending`, `claimed`, `done`, `error` |
215
+ | `priority` | INTEGER | Higher = more urgent |
216
+ | `attempts` | INTEGER | Delivery attempt count |
217
+ | `max_attempts` | INTEGER | Maximum retries |
218
+
219
+ ## Job Scripts
220
+
221
+ Jobs are plain Node.js scripts executed as child processes. The runner passes context via environment variables:
222
+
223
+ | Variable | Description |
224
+ |----------|-------------|
225
+ | `JR_DB_PATH` | Path to the runner SQLite database |
226
+ | `JR_JOB_ID` | ID of the current job |
227
+ | `JR_RUN_ID` | ID of the current run |
228
+
229
+ ### Structured output
230
+
231
+ Scripts can emit structured results by writing a line to stdout:
232
+
233
+ ```
234
+ JR_RESULT:{"tokens":1500,"meta":"processed 42 items"}
235
+ ```
236
+
237
+ The runner parses this and stores the data in the `runs` table.
238
+
239
+ ### Client library
240
+
241
+ Job scripts can import the runner client for cursor and queue operations:
242
+
243
+ ```typescript
244
+ import { createClient } from '@karmaniverous/jeeves-runner';
245
+
246
+ const jr = createClient(); // reads JR_DB_PATH from env
247
+
248
+ // Cursors (key-value state)
249
+ const lastId = jr.getCursor('email-poll', 'last_history_id');
250
+ jr.setCursor('email-poll', 'last_history_id', newId);
251
+ jr.setCursor('email-poll', `seen:${threadId}`, '1', { ttl: '30d' });
252
+ jr.deleteCursor('email-poll', 'old_key');
253
+
254
+ // Queues
255
+ jr.enqueue('email-updates', { threadId, action: 'label' });
256
+ const items = jr.dequeue('email-updates', 10); // claim up to 10
257
+ jr.done(items[0].id);
258
+ jr.fail(items[1].id, 'API error');
259
+
260
+ jr.close();
261
+ ```
262
+
263
+ ## Job Lifecycle
264
+
265
+ ```
266
+ Cron fires
267
+ → Check overlap policy (skip if running & policy = 'skip')
268
+ → INSERT run (status = 'running')
269
+ → spawn('node', [script], { env: JR_* })
270
+ → Capture stdout/stderr (ring buffer, last 100 lines)
271
+ → Parse JR_RESULT lines → extract tokens + result_meta
272
+ → On timeout: kill process, status = 'timeout', notify
273
+ → On exit 0: status = 'ok', notify if on_success configured
274
+ → On exit ≠ 0: status = 'error', notify if on_failure configured
275
+ ```
276
+
277
+ ### Overlap policies
278
+
279
+ | Policy | Behavior |
280
+ |--------|----------|
281
+ | `skip` | Don't start if already running (default) |
282
+ | `queue` | Wait for current run to finish, then start |
283
+ | `allow` | Run concurrently |
284
+
285
+ ### Concurrency
286
+
287
+ A global semaphore limits concurrent jobs (default: 4, configurable via `maxConcurrency`). When the limit is hit, behavior follows the job's overlap policy.
288
+
289
+ ### Notifications
290
+
291
+ Slack notifications are sent via direct HTTP POST to `chat.postMessage` (no SDK dependency):
292
+
293
+ - **Failure:** `⚠️ *Job Name* failed (12.3s): error message`
294
+ - **Success:** `✅ *Job Name* completed (3.4s)`
295
+
296
+ Notifications require a Slack bot token (file path in config). Each job can override the default notification channels.
297
+
298
+ ## Maintenance
299
+
300
+ The runner automatically performs periodic maintenance:
301
+
302
+ - **Run pruning:** Deletes run records older than `runRetentionDays` (default: 30).
303
+ - **Cursor cleanup:** Deletes expired cursor entries (runs every `cursorCleanupIntervalMs`, default: 1 hour).
304
+
305
+ Both tasks run on startup and at the configured interval.
306
+
307
+ ## Programmatic Usage
308
+
309
+ ```typescript
310
+ import { createRunner, runnerConfigSchema } from '@karmaniverous/jeeves-runner';
311
+
312
+ const config = runnerConfigSchema.parse({
313
+ port: 3100,
314
+ dbPath: './data/runner.sqlite',
315
+ });
316
+
317
+ const runner = createRunner(config);
318
+ await runner.start();
319
+
320
+ // Graceful shutdown
321
+ process.on('SIGTERM', () => runner.stop());
322
+ ```
323
+
324
+ ## Configuration Reference
325
+
326
+ | Key | Type | Default | Description |
327
+ |-----|------|---------|-------------|
328
+ | `port` | number | `3100` | HTTP API port |
329
+ | `dbPath` | string | `./data/runner.sqlite` | SQLite database path |
330
+ | `maxConcurrency` | number | `4` | Max concurrent jobs |
331
+ | `runRetentionDays` | number | `30` | Days to keep run history |
332
+ | `cursorCleanupIntervalMs` | number | `3600000` | Cursor cleanup interval (ms) |
333
+ | `shutdownGraceMs` | number | `30000` | Grace period for running jobs on shutdown |
334
+ | `notifications.slackTokenPath` | string | — | Path to Slack bot token file |
335
+ | `notifications.defaultOnFailure` | string \| null | `null` | Default Slack channel for failures |
336
+ | `notifications.defaultOnSuccess` | string \| null | `null` | Default Slack channel for successes |
337
+ | `log.level` | string | `info` | Log level (trace/debug/info/warn/error/fatal) |
338
+ | `log.file` | string | — | Log file path (stdout if omitted) |
339
+
340
+ ## The Jeeves Platform
341
+
342
+ jeeves-runner is one component of a four-part platform:
343
+
344
+ | Component | Role | Status |
345
+ |-----------|------|--------|
346
+ | **jeeves-runner** | Execute: run processes, move data through the graph | This package |
347
+ | **[jeeves-watcher](https://github.com/karmaniverous/jeeves-watcher)** | Index: observe file-backed datastores, embed in Qdrant | Shipped |
348
+ | **jeeves-server** | Present: UI, API, file serving, search, dashboards | Shipped |
349
+ | **Jeeves skill** | Converse: configure, operate, and query via chat | Planned |
350
+
351
+ ## Project Status
352
+
353
+ **Phase 1** (current): Replicate existing job scheduling and status reporting. Replace n8n and the Notion Process Dashboard.
354
+
355
+ ### What's built
356
+
357
+ - ✅ SQLite schema (jobs, runs, cursors, queues)
358
+ - ✅ Cron scheduler with overlap policies and concurrency limits
359
+ - ✅ Job executor with output capture, timeout enforcement, and `JR_RESULT` parsing
360
+ - ✅ Client library for cursor/queue operations from job scripts
361
+ - ✅ Slack notifications for job success/failure
362
+ - ✅ REST API (Fastify) for job management and monitoring
363
+ - ✅ CLI for daemon management and job operations
364
+ - ✅ Maintenance tasks (run pruning, cursor cleanup)
365
+ - ✅ Zod-validated configuration
366
+ - ✅ Seed script for 27 existing n8n workflows
367
+ - ✅ 75 passing tests
368
+
369
+ ### What's next (Phase 1 remaining)
370
+
371
+ - [ ] NSSM service setup
372
+ - [ ] jeeves-server dashboard page (`/runner`)
373
+ - [ ] Migrate jobs from n8n one by one
374
+ - [ ] Retire n8n
375
+
376
+ ### Future phases
377
+
378
+ | Feature | Phase |
379
+ |---------|-------|
380
+ | Graph topology (nodes/edges schema) | 2 |
381
+ | Credential/auth management | 2 |
382
+ | REST API for graph mutations | 2 |
383
+ | OpenClaw plugin & Jeeves skill | 3 |
384
+ | Container packaging | 3 |
385
+
386
+ ## Development
387
+
388
+ ```bash
389
+ npm install
390
+ npx lefthook install
391
+
392
+ npm run lint # ESLint + Prettier
393
+ npm run test # Vitest
394
+ npm run knip # Unused code detection
395
+ npm run build # Rollup (ESM + types + CLI)
396
+ npm run typecheck # TypeScript (noEmit)
397
+ ```
398
+
399
+ ## License
400
+
401
+ BSD-3-Clause