{"id":45720090,"url":"https://github.com/karmaniverous/jeeves-runner","last_synced_at":"2026-05-16T08:12:14.959Z","repository":{"id":340355364,"uuid":"1165668746","full_name":"karmaniverous/jeeves-runner","owner":"karmaniverous","description":"Graph-aware job execution engine with SQLite state. Part of the Jeeves platform.","archived":false,"fork":false,"pushed_at":"2026-04-05T10:29:33.000Z","size":1964,"stargazers_count":0,"open_issues_count":3,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-05T12:24:40.762Z","etag":null,"topics":["cli","cron","fastify","job-scheduler","nodejs","queue","sqlite","task-runner","typescript"],"latest_commit_sha":null,"homepage":"https://docs.karmanivero.us/jeeves-runner/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/karmaniverous.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-24T12:17:43.000Z","updated_at":"2026-04-05T10:29:31.000Z","dependencies_parsed_at":"2026-04-02T12:11:45.934Z","dependency_job_id":null,"html_url":"https://github.com/karmaniverous/jeeves-runner","commit_stats":null,"previous_names":["karmaniverous/jeeves-runner"],"tags_count":40,"template":false,"template_full_name":"karmaniverous/npm-package-template-ts","purl":"pkg:github/karmaniverous/jeeves-runner","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/karmaniverous%2Fjeeves-runner","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/karmaniverous%2Fjeeves-runner/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/karmaniverous%2Fjeeves-runner/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/karmaniverous%2Fjeeves-runner/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/karmaniverous","download_url":"https://codeload.github.com/karmaniverous/jeeves-runner/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/karmaniverous%2Fjeeves-runner/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31547845,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-07T16:28:08.000Z","status":"online","status_checked_at":"2026-04-08T02:00:06.127Z","response_time":54,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cli","cron","fastify","job-scheduler","nodejs","queue","sqlite","task-runner","typescript"],"created_at":"2026-02-25T05:47:04.558Z","updated_at":"2026-04-08T09:01:19.767Z","avatar_url":"https://github.com/karmaniverous.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Jeeves Runner 🎩\n\n[![npm version](https://img.shields.io/npm/v/@karmaniverous/jeeves-runner.svg)](https://www.npmjs.com/package/@karmaniverous/jeeves-runner)\n![Node Current](https://img.shields.io/node/v/@karmaniverous/jeeves-runner)\n[![license](https://img.shields.io/badge/license-BSD--3--Clause-blue.svg)](https://github.com/karmaniverous/jeeves-runner/tree/main/LICENSE.md)\n\nGraph-aware job execution engine with SQLite state. Part of the [Jeeves platform](#the-jeeves-platform).\n\n## Monorepo Structure\n\nThis repository is a monorepo containing two packages:\n\n| Package | npm | Description |\n|---------|-----|-------------|\n| [`packages/service`](packages/service) | [`@karmaniverous/jeeves-runner`](https://www.npmjs.com/package/@karmaniverous/jeeves-runner) v0.7.4 | Job execution engine |\n| [`packages/openclaw`](packages/openclaw) | [`@karmaniverous/jeeves-runner-openclaw`](https://www.npmjs.com/package/@karmaniverous/jeeves-runner-openclaw) v0.5.1 | OpenClaw plugin |\n\n## What It Does\n\njeeves-runner schedules and executes jobs, tracks their state in SQLite, and exposes full CRUD management and monitoring via a REST API. It replaces both n8n and Windows Task Scheduler as the substrate for data flow automation.\n\n**Key properties:**\n\n- **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.\n- **SQLite-native.** Job definitions, run history, state, and queues live in a single SQLite file. No external database, no Redis.\n- **Zero new infrastructure.** One Node.js process, one SQLite file. Runs as a system service via NSSM (Windows) or systemd (Linux).\n- **Scripts as config.** Job scripts live outside the runner repo at configurable absolute paths. The runner is generic; the scripts are instance-specific.\n\n## Architecture\n\n![System Architecture](packages/service/assets/system-architecture.png)\n\n### Stack\n\n| Component | Technology |\n|-----------|-----------|\n| Runtime | Node.js v24+ (uses built-in `node:sqlite`) |\n| Scheduler | [croner](https://www.npmjs.com/package/croner) |\n| Database | SQLite via `node:sqlite` |\n| Process isolation | `child_process.spawn` |\n| HTTP API | [Fastify](https://fastify.dev/) |\n| Logging | [pino](https://getpino.io/) |\n| Config validation | [Zod](https://zod.dev/) |\n\n## Installation\n\n```bash\nnpm install @karmaniverous/jeeves-runner\n```\n\nRequires Node.js 24+ for `node:sqlite` support.\n\n## Quick Start\n\n### 1. Create a config file\n\nGenerate a starter config:\n\n```bash\nnpx jeeves-runner init\n```\n\nOr create `jeeves-runner/config.json` manually:\n\n```json\n{\n  \"port\": 1937,\n  \"host\": \"0.0.0.0\",\n  \"dbPath\": \"./data/runner.sqlite\",\n  \"maxConcurrency\": 4,\n  \"runRetentionDays\": 30,\n  \"stateCleanupIntervalMs\": 3600000,\n  \"reconcileIntervalMs\": 60000,\n  \"shutdownGraceMs\": 30000,\n  \"runners\": {\n    \"ts\": \"node ./scripts/node_modules/tsx/dist/cli.mjs\"\n  },\n  \"gateway\": {\n    \"url\": \"http://127.0.0.1:18789\",\n    \"tokenPath\": \"./credentials/gateway-token\"\n  },\n  \"notifications\": {\n    \"slackTokenPath\": \"./credentials/slack-bot-token\",\n    \"defaultOnFailure\": \"YOUR_SLACK_CHANNEL_ID\",\n    \"defaultOnSuccess\": null\n  },\n  \"log\": {\n    \"level\": \"info\",\n    \"file\": \"./data/runner.log\"\n  }\n}\n```\n\n### 2. Start the runner\n\n```bash\nnpx jeeves-runner start --config ./config.json\n```\n\n### 3. Add a job\n\nJobs are registered via the CLI or a seed script — there is no `schedule.json`.\n\n```bash\nnpx jeeves-runner add-job \\\n  --id my-job \\\n  --name \"My Job\" \\\n  --schedule \"*/5 * * * *\" \\\n  --script /absolute/path/to/script.js \\\n  --config ./config.json\n```\n\nOr programmatically via `scripts/seed-jobs.ts`.\n\n### 4. Check status\n\n```bash\nnpx jeeves-runner status\nnpx jeeves-runner list-jobs --config ./config.json\n```\n\n## CLI Commands\n\nBuilt with `createServiceCli(descriptor)` from core. Standard commands plus custom job management commands.\n\n| Command | Description |\n|---------|-------------|\n| `start` | Start the runner daemon (foreground) |\n| `status` | Probe service health and version (queries `GET /status`) |\n| `config [jsonpath]` | Query resolved config from running service |\n| `config validate` | Validate a config file against the Zod schema |\n| `config apply` | Apply a config patch to the running service |\n| `init` | Generate default config file |\n| `service install` | Install as a system service |\n| `service uninstall` | Uninstall the system service |\n| `service start/stop/restart` | Manage the system service |\n| `service status` | Query system service state |\n| `add-job` | Add a new job to the database |\n| `list-jobs` | List all configured jobs |\n| `trigger` | Manually trigger a job run (queries the HTTP API) |\n| `init-scripts` | Scaffold a scripts project from the template |\n\n## HTTP API\n\nThe runner exposes a REST API. Default bind address: `0.0.0.0` (all interfaces), default port: `1937`.\n\n| Method | Path | Description |\n|--------|------|-------------|\n| `GET` | `/status` | Service status (`{ name, version, uptime, status, health }`) |\n| `GET` | `/config` | Query resolved config (optional `?path=` JSONPath) |\n| `POST` | `/config/apply` | Apply a config patch (`{ patch, replace? }`) |\n| `GET` | `/jobs` | List all jobs with last run status |\n| `GET` | `/jobs/:id` | Single job detail |\n| `GET` | `/jobs/:id/runs` | Run history (paginated via `?limit=N`) |\n| `POST` | `/jobs/:id/run` | Trigger manual run |\n| `POST` | `/jobs` | Create a new job |\n| `PATCH` | `/jobs/:id` | Partial update of an existing job |\n| `DELETE` | `/jobs/:id` | Delete a job and its run history |\n| `PATCH` | `/jobs/:id/enable` | Enable a job |\n| `PATCH` | `/jobs/:id/disable` | Disable a job |\n| `PUT` | `/jobs/:id/script` | Update job script content or path |\n| `GET` | `/queues` | List all queues with items |\n| `GET` | `/queues/:name/status` | Queue depth, claimed/failed counts |\n| `GET` | `/queues/:name/peek` | Non-claiming read of pending items |\n| `GET` | `/state` | List all state namespaces |\n| `GET` | `/state/:namespace` | Read scalar state (optional `?path=` JSONPath) |\n| `GET` | `/state/:namespace/:key` | Read collection items |\n\n### Example response\n\n```json\n// GET /jobs\n{\n  \"jobs\": [\n    {\n      \"id\": \"email-poll\",\n      \"name\": \"Poll Email\",\n      \"schedule\": \"*/11 * * * *\",\n      \"enabled\": 1,\n      \"last_status\": \"ok\",\n      \"last_run\": \"2026-02-24T10:30:00\"\n    }\n  ]\n}\n```\n\n## SQLite Schema\n\nSix tables manage all runner state:\n\n### `jobs` — Job Definitions\n\nEach job has an ID, name, cron schedule, script path, and behavioral configuration.\n\n| Column | Type | Description |\n|--------|------|-------------|\n| `id` | TEXT PK | Job identifier (e.g. `email-poll`) |\n| `name` | TEXT | Human-readable name |\n| `schedule` | TEXT | Cron expression |\n| `script` | TEXT | Absolute path to script |\n| `type` | TEXT | `script` or `session` (LLM dispatcher) |\n| `enabled` | INTEGER | 1 = active, 0 = paused |\n| `timeout_ms` | INTEGER | Kill after this duration (null = no limit) |\n| `overlap_policy` | TEXT | `skip` (default) or `allow` |\n| `on_failure` | TEXT | Slack channel ID for failure alerts |\n| `on_success` | TEXT | Slack channel ID for success alerts |\n\n### `runs` — Run History\n\nEvery execution is recorded with status, timing, output capture, and optional token tracking.\n\n| Column | Type | Description |\n|--------|------|-------------|\n| `id` | INTEGER PK | Auto-incrementing run ID |\n| `job_id` | TEXT FK | References `jobs.id` |\n| `status` | TEXT | `pending`, `running`, `ok`, `error`, `timeout`, `skipped` |\n| `duration_ms` | INTEGER | Wall-clock execution time |\n| `exit_code` | INTEGER | Process exit code |\n| `tokens` | INTEGER | LLM token count (session jobs only) |\n| `result_meta` | TEXT | JSON from `JR_RESULT:{json}` stdout lines |\n| `stdout_tail` | TEXT | Last 100 lines of stdout |\n| `stderr_tail` | TEXT | Last 100 lines of stderr |\n| `trigger` | TEXT | `schedule`, `manual`, or `retry` |\n\nRuns older than `runRetentionDays` are automatically pruned.\n\n### `state` — Scalar State\n\nGeneral-purpose key-value store with optional TTL. Scripts use `getState`/`setState`/`deleteState` to track cursors, checkpoints, or any operational state.\n\n| Column | Type | Description |\n|--------|------|-------------|\n| `namespace` | TEXT | Logical grouping (typically job ID) |\n| `key` | TEXT | State key |\n| `value` | TEXT | State value (string or JSON) |\n| `expires_at` | TEXT | Optional TTL (ISO timestamp, auto-cleaned) |\n\n### `state_items` — Collection State\n\nCollection-oriented state store for tracking sets of items (e.g., seen thread IDs, processed message IDs).\n\n| Column | Type | Description |\n|--------|------|-------------|\n| `namespace` | TEXT | Logical grouping (typically job ID) |\n| `key` | TEXT | Collection key |\n| `item_key` | TEXT | Individual item identifier |\n| `value` | TEXT | Item value (string or JSON) |\n| `expires_at` | TEXT | Optional TTL (ISO timestamp, auto-cleaned) |\n\n### `queues` — Queue Metadata\n\nQueue-level configuration including deduplication and retention policies.\n\n| Column | Type | Description |\n|--------|------|-------------|\n| `id` | TEXT PK | Queue identifier |\n| `dedup_config` | TEXT | JSON deduplication configuration |\n| `retention` | TEXT | JSON retention policy |\n\n### `queue_items` — Work Queue Items\n\nPriority-ordered work queues with claim semantics. SQLite's serialized writes prevent double-claims.\n\n| Column | Type | Description |\n|--------|------|-------------|\n| `id` | INTEGER PK | Auto-incrementing item ID |\n| `queue_id` | TEXT FK | References `queues.id` |\n| `payload` | TEXT | JSON blob |\n| `status` | TEXT | `pending`, `claimed`, `done`, `error` |\n| `priority` | INTEGER | Higher = more urgent |\n| `attempts` | INTEGER | Delivery attempt count |\n| `max_attempts` | INTEGER | Maximum retries |\n\n## Job Scripts\n\nJobs are plain Node.js scripts executed as child processes. The runner passes context via environment variables:\n\n| Variable | Description |\n|----------|-------------|\n| `JR_DB_PATH` | Path to the runner SQLite database |\n| `JR_JOB_ID` | ID of the current job |\n| `JR_RUN_ID` | ID of the current run |\n\n### Structured output\n\nScripts can emit structured results by writing a line to stdout:\n\n```\nJR_RESULT:{\"tokens\":1500,\"meta\":\"processed 42 items\"}\n```\n\nThe runner parses this and stores the data in the `runs` table.\n\n### Client library\n\nJob scripts use the `runScript()` wrapper and `getRunnerClient()` for lifecycle management and API access:\n\n```typescript\nimport { getRunnerClient, runScript } from '@karmaniverous/jeeves-runner';\n\nawait runScript(import.meta, async () =\u003e {\n  const client = getRunnerClient(); // HTTP client pointed at the running runner\n\n  // Use state API\n  await client.setState('my-namespace', 'lastRun', new Date().toISOString());\n\n  // Use queue API\n  await client.enqueue('my-queue', { url: 'https://example.com' });\n});\n```\n\nThe package exports a full suite of script helper utilities. See the [Script Helper Utilities Guide](packages/service/guides/script-helpers.md) for detailed API documentation and examples.\n\n**Highlights:**\n\n| Module | Exports | Purpose |\n|--------|---------|---------|\n| `run-script` | `runScript` | Crash-handling entry point wrapper |\n| `runner-client` | `getRunnerClient` | SQLite client factory (state + queue API) |\n| `fs-utils` | `readJson`, `writeJsonAtomic`, `appendJsonl`, `sleepMs`, `uuid`, ... | Filesystem, time, process, CLI utilities |\n| `shell` | `run`, `runWithRetry` | Synchronous command execution with retry |\n| `google-auth` | `createGoogleAuth` | OAuth refresh + service account impersonation |\n| `spawn-worker` | `dispatchSession`, `runDispatcher` | OpenClaw Gateway LLM session dispatch |\n| `slack-workspace` | `getChannelWorkspace`, `saveSlackWorkspaceCache` | Slack channel → workspace resolution |\n\n## Job Lifecycle\n\n![Job Lifecycle](packages/service/assets/job-lifecycle.png)\n\n### Overlap policies\n\n| Policy | Behavior |\n|--------|----------|\n| `skip` | Don't start if already running (default) |\n| `allow` | Run concurrently |\n\n### Concurrency\n\nA global semaphore limits concurrent jobs (default: 4, configurable via `maxConcurrency`). When the limit is hit, behavior follows the job's overlap policy.\n\n### Notifications\n\nSlack notifications are sent via direct HTTP POST to `chat.postMessage` (no SDK dependency):\n\n- **Failure:** `⚠️ *Job Name* failed (12.3s): error message`\n- **Success:** `✅ *Job Name* completed (3.4s)`\n\nNotifications require a Slack bot token (file path in config). Each job can override the default notification channels.\n\n## Maintenance\n\nThe runner automatically performs periodic maintenance:\n\n- **Run pruning:** Deletes run records older than `runRetentionDays` (default: 30).\n- **State cleanup:** Deletes expired state entries (runs every `stateCleanupIntervalMs`, default: 1 hour).\n- **Job reconciliation:** Reconciles job schedules with the database (runs every `reconcileIntervalMs`, default: 60 seconds).\n\nAll tasks run on startup and at the configured interval.\n\n## Programmatic Usage\n\n```typescript\nimport { createRunner, runnerConfigSchema } from '@karmaniverous/jeeves-runner';\n\nconst config = runnerConfigSchema.parse({\n  port: 1937,\n  dbPath: './data/runner.sqlite',\n});\n\nconst runner = createRunner(config);\nawait runner.start();\n\n// Graceful shutdown\nprocess.on('SIGTERM', () =\u003e runner.stop());\n```\n\n## Configuration Reference\n\nConfig file: `jeeves-runner/config.json` (legacy `jeeves-runner.config.json` is auto-migrated).\n\n| Key | Type | Default | Description |\n|-----|------|---------|-------------|\n| `port` | number | `1937` | HTTP API port |\n| `host` | string | `0.0.0.0` | Bind address for the HTTP server |\n| `dbPath` | string | `./data/runner.sqlite` | SQLite database path |\n| `maxConcurrency` | number | `4` | Max concurrent jobs |\n| `runRetentionDays` | number | `30` | Days to keep run history |\n| `stateCleanupIntervalMs` | number | `3600000` | State cleanup interval (ms) |\n| `reconcileIntervalMs` | number | `60000` | Job reconciliation interval (ms) |\n| `shutdownGraceMs` | number | `30000` | Grace period for running jobs on shutdown |\n| `runners` | Record | `{}` | Custom command runners keyed by file extension |\n| `gateway.url` | string | `http://127.0.0.1:18789` | OpenClaw Gateway URL (for session jobs) |\n| `gateway.tokenPath` | string | — | Path to gateway auth token file |\n| `notifications.slackTokenPath` | string | — | Path to Slack bot token file |\n| `notifications.defaultOnFailure` | string \\| null | `null` | Default Slack channel for failures |\n| `notifications.defaultOnSuccess` | string \\| null | `null` | Default Slack channel for successes |\n| `log.level` | string | `info` | Log level (trace/debug/info/warn/error/fatal) |\n| `log.file` | string | — | Log file path (stdout if omitted) |\n\n## OpenClaw Plugin\n\nThe `@karmaniverous/jeeves-runner-openclaw` package provides an OpenClaw plugin that exposes runner management tools to your agent. See the [OpenClaw Integration Guide](packages/openclaw/guides/openclaw-integration.md) for setup and usage.\n\n## The Jeeves Platform\n\njeeves-runner is one component of a four-part platform:\n\n| Component | Role | Status |\n|-----------|------|--------|\n| **jeeves-runner** | Execute: run processes, move data through the graph | This package |\n| **[jeeves-watcher](https://github.com/karmaniverous/jeeves-watcher)** | Index: observe file-backed datastores, embed in Qdrant | Shipped |\n| **jeeves-server** | Present: UI, API, file serving, search, dashboards | Shipped |\n| **Jeeves skill** | Converse: configure, operate, and query via chat | Planned |\n\n## Project Status\n\n**Phase 1: Complete.** Job scheduling, status reporting, state management, and queue operations are fully functional. NSSM service deployed. OpenClaw plugin shipped.\n\n### What's built\n\n- ✅ SQLite schema (jobs, runs, state, state_items, queues, queue_items)\n- ✅ Cron scheduler with overlap policies and concurrency limits\n- ✅ Job executor with output capture, timeout enforcement, and `JR_RESULT` parsing\n- ✅ Client library for state and queue operations from job scripts\n- ✅ Slack notifications for job success/failure\n- ✅ REST API (Fastify) for job management and monitoring\n- ✅ CLI for daemon management and job operations\n- ✅ Maintenance tasks (run pruning, state cleanup, job reconciliation)\n- ✅ Zod-validated configuration\n- ✅ Seed script for 27 existing n8n workflows\n- ✅ NSSM service deployment\n- ✅ OpenClaw plugin (`@karmaniverous/jeeves-runner-openclaw`) with 20 tools (4 standard + 16 custom)\n- ✅ Job management API (create, update, delete, script update, enable/disable)\n- ✅ Queue and state inspection API (list queues, peek, query state/collections)\n- ✅ Dual-format scheduling (cron + RRStack)\n- ✅ Monorepo restructure complete\n- ✅ Component SDK adoption (`createServiceCli`, `createPluginToolset`)\n- ✅ Standard `/status`, `/config`, `/config/apply` endpoints\n- ✅ Config path migration (`jeeves-runner.config.json` to `jeeves-runner/config.json`)\n- ✅ Build-time version injection via `@rollup/plugin-replace`\n- ✅ `init-scripts` CLI command for scaffolding script projects\n- ✅ Client utility modules (`run-script`, `runner-client`, `fs-utils`, `shell`, `google-auth`, `spawn-worker`, `slack-workspace`)\n\n### What's next\n\n- [ ] jeeves-server dashboard page (`/runner`)\n- [ ] Migrate jobs from n8n one by one\n- [ ] Retire n8n\n\n### Future phases\n\n| Feature | Phase |\n|---------|-------|\n| Graph topology (nodes/edges schema) | 2 |\n| Credential/auth management | 2 |\n| REST API for graph mutations | 2 |\n| Container packaging | 3 |\n\n## Development\n\n```bash\nnpm install\nnpx lefthook install\n\nnpm run lint        # ESLint + Prettier\nnpm run test        # Vitest\nnpm run knip        # Unused code detection\nnpm run build       # Rollup (ESM + types + CLI)\nnpm run typecheck   # TypeScript (noEmit)\n```\n\n## License\n\nBSD-3-Clause\n\n---\n\nBuilt for you with ❤️ on Bali by [Jason Williscroft](https://github.com/karmaniverous) \u0026 [Jeeves](https://github.com/jgs-jeeves).\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkarmaniverous%2Fjeeves-runner","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkarmaniverous%2Fjeeves-runner","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkarmaniverous%2Fjeeves-runner/lists"}