openclaw-scheduler 0.2.2 → 0.2.4

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/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.2.4] -- 2026-04-18
6
+
7
+ ### Fixed
8
+ - fix(package): include dispatch/completion.mjs in the published npm tarball so dispatch CLI and watcher startup no longer crash with ERR_MODULE_NOT_FOUND in installed deployments
9
+
10
+ ## [0.2.3] -- 2026-04-16
11
+
12
+ ### Fixed
13
+ - fix(cli): harden runtime DB path resolution so installed package layouts prefer `~/.openclaw/scheduler/scheduler.db` instead of a repo-local checkout DB
14
+ - fix(cli): refuse validation-only commands (`jobs validate`, `jobs add --dry-run`) when a source checkout detects an existing runtime DB mismatch
15
+ - test(cli): add installed-package and repo/runtime mismatch coverage for DB path hardening
16
+
17
+ ### Changed
18
+ - docs: bump minimum supported Node.js version from 20 to 22 to match the package engine requirement
19
+
5
20
  ## [0.2.2] -- 2026-04-15
6
21
 
7
22
  ### Fixed
@@ -12,7 +12,7 @@ This guide is for setting up the scheduler on a **second or additional OpenClaw
12
12
  | Requirement | Notes |
13
13
  |-------------|-------|
14
14
  | macOS or Linux | Tested on macOS arm64 |
15
- | Node.js >= 20 | `node --version` (use full path if needed: `/opt/homebrew/bin/node --version`) |
15
+ | Node.js >= 22 | `node --version` (use full path if needed: `/opt/homebrew/bin/node --version`) |
16
16
  | OpenClaw gateway running | With auth token |
17
17
  | Git or SCP access | To clone/copy the repo |
18
18
 
package/INSTALL-LINUX.md CHANGED
@@ -12,7 +12,7 @@ Step-by-step guide to deploy the scheduler on a Linux host running OpenClaw.
12
12
 
13
13
  | Requirement | Notes |
14
14
  |-------------|-------|
15
- | Node.js >= 20 | Install via [nvm](https://github.com/nvm-sh/nvm) or [NodeSource](https://github.com/nodesource/distributions) |
15
+ | Node.js >= 22 | Install via [nvm](https://github.com/nvm-sh/nvm) or [NodeSource](https://github.com/nodesource/distributions) |
16
16
  | build-essential | `sudo apt install build-essential python3` — required for `better-sqlite3` native compile |
17
17
  | OpenClaw gateway running | With auth token |
18
18
  | Git | `sudo apt install git` |
@@ -38,7 +38,7 @@ Use this path only if you can't use WSL2 — for example, if OpenClaw itself is
38
38
 
39
39
  | Requirement | Install |
40
40
  |-------------|---------|
41
- | Node.js >= 20 | [nodejs.org](https://nodejs.org) -- use the LTS installer |
41
+ | Node.js >= 22 | [nodejs.org](https://nodejs.org) -- use the LTS installer |
42
42
  | pm2 | `npm install -g pm2` |
43
43
  | OpenClaw gateway | Must be running with a valid auth token |
44
44
  | Git for Windows | [git-scm.com](https://git-scm.com) or use GitHub Desktop |
package/INSTALL.md CHANGED
@@ -11,7 +11,7 @@ If you just want the fastest path to a working local install, start with the npm
11
11
  | Requirement | Notes |
12
12
  |-------------|-------|
13
13
  | macOS or Linux | Tested on macOS arm64 |
14
- | Node.js >= 20 | `node --version` |
14
+ | Node.js >= 22 | `node --version` |
15
15
  | OpenClaw gateway running | With auth token |
16
16
  | Git or SCP access | To clone/copy the repo |
17
17
 
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![CI](https://github.com/amittell/openclaw-scheduler/actions/workflows/ci.yml/badge.svg)](https://github.com/amittell/openclaw-scheduler/actions/workflows/ci.yml)
4
4
  [![License](https://img.shields.io/badge/license-MIT-blue)]()
5
- [![Node](https://img.shields.io/badge/node-%E2%89%A520-green)](https://nodejs.org)
5
+ [![Node](https://img.shields.io/badge/node-%E2%89%A522-green)](https://nodejs.org)
6
6
 
7
7
  A durable orchestration runtime for [OpenClaw](https://openclaw.ai) agents and shell workflows. Use it when built-in cron and heartbeat stop being enough: jobs fail and disappear into logs, shell scripts depend on gateway uptime, multi-step workflows need retries and approvals, and you want a real audit trail for what ran, what failed, and what triggered what.
8
8
 
@@ -11,7 +11,7 @@ It replaces OpenClaw's built-in cron/heartbeat with a SQLite-backed scheduler th
11
11
  **Repo:** `github.com/amittell/openclaw-scheduler`
12
12
  **Default location:** `~/.openclaw/scheduler/`
13
13
  **Service:** `ai.openclaw.scheduler` (macOS launchd: LaunchAgent or LaunchDaemon)
14
- **Runtime:** Node.js 20+ (ESM), SQLite via `better-sqlite3`, cron parsing via `croner`
14
+ **Runtime:** Node.js 22+ (ESM), SQLite via `better-sqlite3`, cron parsing via `croner`
15
15
  **Tests:** run with `npm test` (full suite, in-memory SQLite)
16
16
  **Platform:** macOS · Linux · Windows (WSL2)
17
17
 
@@ -204,7 +204,7 @@ npm run verify:local # full local maintainer gate
204
204
  npm run verify:smoke # lightweight smoke gate used by GitHub Actions
205
205
  ```
206
206
 
207
- GitHub Actions runs the smoke gate plus the in-memory test suite on Linux, macOS, and Windows with Node 20. The full release gate still runs locally via `npm run verify:local` and is enforced again by `prepublishOnly`.
207
+ GitHub Actions runs the smoke gate plus the in-memory test suite on Linux and macOS with Node 22. Publishing uses Node 24 (npm 22+) for OIDC trusted publisher support. The full release gate still runs locally via `npm run verify:local` and is enforced again by `prepublishOnly`.
208
208
 
209
209
  ### Option C: local npm pack (simulate the published package from source)
210
210
 
package/cli.js CHANGED
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  // Scheduler CLI -- manage jobs, runs, messages, agents
3
- import { readFileSync } from 'fs';
4
- import { initDb, getDb } from './db.js';
3
+ import { existsSync, readFileSync } from 'fs';
4
+ import { dirname, join } from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import { initDb, getDb, getResolvedDbPath } from './db.js';
5
7
  import { createJob, getJob, listJobs, updateJob, deleteJob, cancelJob, runJobNow, validateJobSpec, parseInDuration, AT_JOB_CRON_SENTINEL } from './jobs.js';
6
8
  import { getRun, getRunsForJob, getRunningRuns, getStaleRuns, finishRun } from './runs.js';
7
9
  import {
@@ -9,14 +11,53 @@ import {
9
11
  ackMessage, listMessageReceipts, getTeamMessages,
10
12
  } from './messages.js';
11
13
  import { upsertAgent, getAgent, listAgents } from './agents.js';
14
+ import { resolveSchedulerHome } from './paths.js';
12
15
  import { SCHEDULER_SCHEMAS } from './scheduler-schema.js';
13
16
 
17
+ const __dirname = dirname(fileURLToPath(import.meta.url));
14
18
  const cliArgs = process.argv.slice(2);
15
19
  const jsonFlagIndex = cliArgs.indexOf('--json');
16
20
  const jsonMode = jsonFlagIndex >= 0;
17
21
  if (jsonFlagIndex >= 0) cliArgs.splice(jsonFlagIndex, 1);
18
22
  const [command, sub, ...args] = cliArgs;
19
23
 
24
+ function firstNonEmpty(value) {
25
+ if (typeof value !== 'string') return '';
26
+ const trimmed = value.trim();
27
+ return trimmed.length > 0 ? trimmed : '';
28
+ }
29
+
30
+ function isNodeModulesInstall(moduleDir) {
31
+ return /[\\/]node_modules[\\/](?:@[^\\/]+[\\/])?openclaw-scheduler(?:[\\/]|$)/.test(moduleDir);
32
+ }
33
+
34
+ function isValidationOnlyCommand(cmd, subcommand, rest) {
35
+ return cmd === 'jobs' && (
36
+ subcommand === 'validate'
37
+ || (subcommand === 'add' && rest.includes('--dry-run'))
38
+ );
39
+ }
40
+
41
+ function getDbPathMismatchNotice(env = process.env) {
42
+ if (firstNonEmpty(env.SCHEDULER_DB)) return null;
43
+ if (isNodeModulesInstall(__dirname)) return null;
44
+
45
+ const repoDbPath = join(__dirname, 'scheduler.db');
46
+ const resolvedDbPath = getResolvedDbPath();
47
+ if (resolvedDbPath !== repoDbPath) return null;
48
+
49
+ const runtimeDbPath = join(resolveSchedulerHome(env), 'scheduler.db');
50
+ if (runtimeDbPath === resolvedDbPath) return null;
51
+ if (!existsSync(runtimeDbPath)) return null;
52
+
53
+ return { resolvedDbPath, runtimeDbPath };
54
+ }
55
+
56
+ function formatDbPathMismatchNotice({ resolvedDbPath, runtimeDbPath }, { validation = false } = {}) {
57
+ const prefix = validation ? 'Refusing to run validation.' : 'Warning: source checkout CLI is using a repo-local DB.';
58
+ return `${prefix} repo-local=${resolvedDbPath} runtime=${runtimeDbPath}. Re-run via the installed package CLI or set SCHEDULER_DB explicitly.`;
59
+ }
60
+
20
61
  function usage() {
21
62
  console.log(`
22
63
  Usage: openclaw-scheduler <command> [subcommand] [options]
@@ -119,6 +160,14 @@ Capabilities:
119
160
  `);
120
161
  }
121
162
 
163
+ const dbPathMismatchNotice = getDbPathMismatchNotice(process.env);
164
+ if (dbPathMismatchNotice) {
165
+ if (isValidationOnlyCommand(command, sub, args)) {
166
+ fail(formatDbPathMismatchNotice(dbPathMismatchNotice, { validation: true }));
167
+ }
168
+ process.stderr.write(`${formatDbPathMismatchNotice(dbPathMismatchNotice)}\n`);
169
+ }
170
+
122
171
  await initDb();
123
172
 
124
173
  function fmt(obj) { return JSON.stringify(obj, null, 2); }
@@ -953,6 +1002,7 @@ switch (command) {
953
1002
  // -- Status ----------------------------------------------
954
1003
  case 'status': {
955
1004
  const db = getDb();
1005
+ const dbPath = getResolvedDbPath();
956
1006
  const jobs = listJobs();
957
1007
  const runningRuns = getRunningRuns();
958
1008
  const stale = getStaleRuns();
@@ -982,6 +1032,7 @@ switch (command) {
982
1032
  .filter(j => j.enabled && j.next_run_at)
983
1033
  .sort((a, b) => a.next_run_at.localeCompare(b.next_run_at))[0] || null;
984
1034
  const payload = {
1035
+ db_path: dbPath,
985
1036
  jobs_total: jobs.length,
986
1037
  jobs_enabled: jobs.filter(j => j.enabled).length,
987
1038
  running_runs: runningRuns.length,
@@ -997,6 +1048,7 @@ switch (command) {
997
1048
  };
998
1049
  emit(payload, () => {
999
1050
  console.log('=== OpenClaw Scheduler Status ===');
1051
+ console.log(`DB: ${dbPath}`);
1000
1052
  console.log(`Jobs: ${jobs.length} total, ${jobs.filter(j => j.enabled).length} enabled`);
1001
1053
  console.log(`Running: ${runningRuns.length}`);
1002
1054
  console.log(`Stale: ${stale.length}`);
@@ -0,0 +1,135 @@
1
+ const GENERIC_COMPLETION_TEXT_RE = /^(?:completed(?:\s*\([^\n)]*\))?|done|ok|okay|success|successful|complete|all set|none|n\/?a)$/i;
2
+ const TRIVIAL_CHATTER_RE = /^(?:hi|hello|hey|yo|sup|thanks|thank you|cool|nice|sure|yep|yeah|k|kk|roger|copy that)[.!?]*$/i;
3
+
4
+ export function normalizeCompletionText(value) {
5
+ if (typeof value !== 'string') return null;
6
+ const trimmed = value.trim();
7
+ return trimmed ? trimmed : null;
8
+ }
9
+
10
+ export function isMeaningfulCompletionText(value) {
11
+ const text = normalizeCompletionText(value);
12
+ if (!text) return false;
13
+
14
+ const normalized = text.toLowerCase().replace(/\s+/g, ' ').trim();
15
+ if (!normalized) return false;
16
+ if (GENERIC_COMPLETION_TEXT_RE.test(normalized)) return false;
17
+ if (TRIVIAL_CHATTER_RE.test(normalized)) return false;
18
+
19
+ const words = normalized.split(/\s+/).filter(Boolean);
20
+ if (words.length === 1) return false;
21
+
22
+ return true;
23
+ }
24
+
25
+ function shortSha(sha) {
26
+ const text = normalizeCompletionText(sha);
27
+ if (!text) return null;
28
+ return /^[0-9a-f]{7,40}$/i.test(text) ? text.slice(0, 7) : text;
29
+ }
30
+
31
+ function cloneChecklist(checklist) {
32
+ if (!checklist || typeof checklist !== 'object' || Array.isArray(checklist)) return null;
33
+ try {
34
+ return JSON.parse(JSON.stringify(checklist));
35
+ } catch {
36
+ return { ...checklist };
37
+ }
38
+ }
39
+
40
+ export function synthesizeCompletionReply({ checklist, sha } = {}) {
41
+ const normalizedChecklist = cloneChecklist(checklist);
42
+ const short = shortSha(sha);
43
+ const sentences = [];
44
+
45
+ if (normalizedChecklist?.tests_passed === true) {
46
+ sentences.push('Tests passed.');
47
+ }
48
+
49
+ if (normalizedChecklist?.pushed === true && short) {
50
+ sentences.push(`Pushed ${short}.`);
51
+ } else if (normalizedChecklist?.pushed === true) {
52
+ sentences.push('Changes pushed.');
53
+ } else if (short) {
54
+ sentences.push(`Commit ${short}.`);
55
+ }
56
+
57
+ const extraTrueFlags = normalizedChecklist
58
+ ? Object.entries(normalizedChecklist)
59
+ .filter(([key, value]) => value === true && !['work_complete', 'tests_passed', 'pushed'].includes(key))
60
+ .map(([key]) => key.replace(/_/g, ' '))
61
+ : [];
62
+
63
+ if (extraTrueFlags.length > 0) {
64
+ sentences.push(`Checks: ${extraTrueFlags.slice(0, 3).join(', ')}.`);
65
+ }
66
+
67
+ if (sentences.length === 0) return null;
68
+ return `Work complete. ${sentences.join(' ')}`.trim();
69
+ }
70
+
71
+ export function buildTerminalCompletionPayload({ summary, checklist, sha } = {}) {
72
+ const rawSummary = normalizeCompletionText(summary);
73
+ const normalizedChecklist = cloneChecklist(checklist);
74
+ const normalizedSha = normalizeCompletionText(sha);
75
+ const prose = isMeaningfulCompletionText(rawSummary) ? rawSummary : null;
76
+ const synthesizedReply = prose
77
+ ? null
78
+ : synthesizeCompletionReply({ checklist: normalizedChecklist, sha: normalizedSha });
79
+ const effectiveSummary = prose || synthesizedReply || rawSummary || null;
80
+ const deliveryText = prose || synthesizedReply || null;
81
+
82
+ return {
83
+ version: 1,
84
+ recordedAt: new Date().toISOString(),
85
+ summary: effectiveSummary,
86
+ deliveryText,
87
+ prose,
88
+ checklist: normalizedChecklist,
89
+ sha: normalizedSha,
90
+ debug: {
91
+ rawSummary,
92
+ synthesizedReply,
93
+ deliverySource: prose ? 'summary' : synthesizedReply ? 'synthesized' : 'none',
94
+ },
95
+ };
96
+ }
97
+
98
+ export function resolveCompletionDelivery({ lastReply, completion, fallbackSummary } = {}) {
99
+ const reply = normalizeCompletionText(lastReply);
100
+ const completionSummary = normalizeCompletionText(completion?.summary);
101
+ const completionDelivery = normalizeCompletionText(completion?.deliveryText);
102
+ const fallback = normalizeCompletionText(fallbackSummary);
103
+ const preferredSummary = completionSummary || fallback;
104
+ const meaningfulSummary = [completionSummary, fallback].find(isMeaningfulCompletionText) || null;
105
+
106
+ if (isMeaningfulCompletionText(reply)) {
107
+ return {
108
+ deliveryText: reply,
109
+ summary: preferredSummary || reply.slice(0, 500),
110
+ source: 'lastReply',
111
+ };
112
+ }
113
+
114
+ if (isMeaningfulCompletionText(completionDelivery)) {
115
+ return {
116
+ deliveryText: completionDelivery,
117
+ summary: completionSummary || completionDelivery,
118
+ source: completion?.debug?.deliverySource || 'completion',
119
+ };
120
+ }
121
+
122
+ if (meaningfulSummary) {
123
+ return {
124
+ deliveryText: meaningfulSummary,
125
+ summary: meaningfulSummary,
126
+ source: completionSummary && meaningfulSummary === completionSummary ? 'completion-summary' : 'summary',
127
+ };
128
+ }
129
+
130
+ return {
131
+ deliveryText: null,
132
+ summary: preferredSummary || null,
133
+ source: 'none',
134
+ };
135
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-scheduler",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "SQLite-backed job scheduler and workflow engine for OpenClaw agents",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -17,7 +17,7 @@
17
17
  "./package.json": "./package.json"
18
18
  },
19
19
  "engines": {
20
- "node": ">=20"
20
+ "node": ">=22"
21
21
  },
22
22
  "scripts": {
23
23
  "start": "node dispatcher.js",
@@ -37,6 +37,7 @@
37
37
  "files": [
38
38
  "bin",
39
39
  "dispatch/529-recovery.mjs",
40
+ "dispatch/completion.mjs",
40
41
  "dispatch/config.example.json",
41
42
  "dispatch/deliver-watcher.sh",
42
43
  "dispatch/hooks.mjs",