@goodandready/dsh-cron 0.2.23 → 0.2.24
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/lib/client.js +1 -1
- package/lib/index.js +41 -27
- package/lib/settings.js +90 -38
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -3380,7 +3380,7 @@ window.__ModuleLoader__.load({
|
|
|
3380
3380
|
const [updateState, setUpdateState] = React.useState({
|
|
3381
3381
|
checking: false,
|
|
3382
3382
|
updating: false,
|
|
3383
|
-
currentVersion: '0.2.
|
|
3383
|
+
currentVersion: '0.2.24',
|
|
3384
3384
|
latestVersion: undefined,
|
|
3385
3385
|
updateAvailable: false,
|
|
3386
3386
|
notice: null,
|
package/lib/index.js
CHANGED
|
@@ -74,6 +74,8 @@ import {
|
|
|
74
74
|
SETTINGS_SYNC_KEYS,
|
|
75
75
|
applySettingsToScope,
|
|
76
76
|
sanitizeSettingsPayload,
|
|
77
|
+
unwrapConfig,
|
|
78
|
+
unwrapConfigValue,
|
|
77
79
|
} from './settings.js';
|
|
78
80
|
import { registerRoutes } from './routes.js';
|
|
79
81
|
|
|
@@ -82,10 +84,12 @@ export {
|
|
|
82
84
|
SETTINGS_SYNC_KEYS,
|
|
83
85
|
applySettingsToScope,
|
|
84
86
|
sanitizeSettingsPayload,
|
|
87
|
+
unwrapConfig,
|
|
88
|
+
unwrapConfigValue,
|
|
85
89
|
};
|
|
86
90
|
|
|
87
91
|
export const name = '@goodandready/dsh-cron';
|
|
88
|
-
export const inject = ['tools', 'webServer', '
|
|
92
|
+
export const inject = ['tools', 'webServer', 'llm', 'agents', 'agentDefaultModel', 'credentials'];
|
|
89
93
|
|
|
90
94
|
import { executeCreateTask } from './task-create.js';
|
|
91
95
|
export { executeCreateTask };
|
|
@@ -147,8 +151,23 @@ const createTaskOutput = {
|
|
|
147
151
|
render: (_args, val) => [{ type: 'text', text: val.message || JSON.stringify(val) }]
|
|
148
152
|
};
|
|
149
153
|
|
|
150
|
-
export function apply(ctx,
|
|
154
|
+
export function apply(ctx, rawConfig) {
|
|
155
|
+
const config = unwrapConfig(rawConfig);
|
|
151
156
|
const store = new TaskStore();
|
|
157
|
+
|
|
158
|
+
// Sync settings declared in the profile config into the store (#184)
|
|
159
|
+
if (config && typeof config === 'object') {
|
|
160
|
+
const fromConfig = {};
|
|
161
|
+
for (const key of SETTINGS_SYNC_KEYS) {
|
|
162
|
+
const val = config[key];
|
|
163
|
+
if (val !== undefined && val !== null && val !== '') {
|
|
164
|
+
fromConfig[key] = val;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (Object.keys(fromConfig).length > 0) {
|
|
168
|
+
store.saveSettings(fromConfig);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
152
171
|
const runner = new SessionRunner(ctx, store);
|
|
153
172
|
const scheduler = new TaskScheduler(store, (task, opts) => runner.execute(task, opts), {
|
|
154
173
|
maxConcurrent: config && config.maxConcurrent,
|
|
@@ -166,29 +185,22 @@ export function apply(ctx, config) {
|
|
|
166
185
|
}),
|
|
167
186
|
});
|
|
168
187
|
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
let cronSettingsScope = null;
|
|
188
|
+
// Settings integration for DSH 0.1.7+ (#184):
|
|
189
|
+
// DSH 0.1.7 removed legacy settings.register and uses volatile Config fields.
|
|
190
|
+
// We attach to settings optionally, configure presentation, and synchronize writes.
|
|
191
|
+
let settingsService = null;
|
|
174
192
|
ctx.inject(['settings'], (sctx) => {
|
|
193
|
+
settingsService = sctx.settings;
|
|
175
194
|
try {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
});
|
|
179
|
-
if (cronSettingsScope && typeof cronSettingsScope.watch === 'function') {
|
|
180
|
-
cronSettingsScope.watch((updated) => {
|
|
181
|
-
if (updated && typeof updated === 'object') {
|
|
182
|
-
store.saveSettings(updated);
|
|
183
|
-
}
|
|
184
|
-
});
|
|
195
|
+
if (settingsService && typeof settingsService.configure === 'function') {
|
|
196
|
+
sctx.effect(() => settingsService.configure({ auto: false }, ctx.fiber));
|
|
185
197
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
});
|
|
189
|
-
} catch (e) {
|
|
190
|
-
console.warn('[dsh-cron] settings registration warning:', e.message);
|
|
198
|
+
} catch {
|
|
199
|
+
// ignore presentation policy errors
|
|
191
200
|
}
|
|
201
|
+
sctx.effect(() => () => {
|
|
202
|
+
settingsService = null;
|
|
203
|
+
});
|
|
192
204
|
});
|
|
193
205
|
|
|
194
206
|
// #50: jobs declared in the profile config belong to the config. The sync
|
|
@@ -239,7 +251,7 @@ export function apply(ctx, config) {
|
|
|
239
251
|
registerRoutes(ctx, {
|
|
240
252
|
store,
|
|
241
253
|
scheduler,
|
|
242
|
-
getCronSettingsScope: () =>
|
|
254
|
+
getCronSettingsScope: () => settingsService,
|
|
243
255
|
recommendations,
|
|
244
256
|
});
|
|
245
257
|
|
|
@@ -258,11 +270,13 @@ export function apply(ctx, config) {
|
|
|
258
270
|
const { body, error } = await readBody(req, res);
|
|
259
271
|
if (error) return;
|
|
260
272
|
const payload = sanitizeSettingsPayload(body);
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
273
|
+
store.saveSettings(payload);
|
|
274
|
+
const scopeErrors = await applySettingsToScope(settingsService, payload);
|
|
275
|
+
sendJson(res, 200, {
|
|
276
|
+
ok: true,
|
|
277
|
+
settings: store.getClientSettings(),
|
|
278
|
+
scopeErrors: (scopeErrors && scopeErrors.length > 0) ? scopeErrors : undefined
|
|
279
|
+
});
|
|
266
280
|
return;
|
|
267
281
|
}
|
|
268
282
|
sendJson(res, 405, { ok: false, error: 'Method not allowed' });
|
package/lib/settings.js
CHANGED
|
@@ -3,43 +3,76 @@ import { credentialRefStatus } from './secrets.js';
|
|
|
3
3
|
import { TaskStore } from './store.js';
|
|
4
4
|
import { MIN_DELIVERY_TIMEOUT_MS, resolveDeliveryTimeoutMs } from './channels.js';
|
|
5
5
|
|
|
6
|
+
// Polyfill Schema.prototype.volatile for schemastery < 3.18.3 environments
|
|
7
|
+
if (typeof z.prototype?.volatile !== 'function') {
|
|
8
|
+
z.prototype.volatile = function volatile() {
|
|
9
|
+
if (this.meta && this.meta.volatile) throw new TypeError('volatile schema is already wrapped');
|
|
10
|
+
return typeof this.extra === 'function' ? this.extra('volatile', true) : this;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
6
14
|
export const Config = z.object({
|
|
7
|
-
botToken: z.string().role('secret').default('').description('Legacy Telegram Bot API token (prefer botTokenRef with the DSH credentials service)'),
|
|
8
|
-
botTokenRef: z.string().default('').description('Name of the DSH credential that holds the Telegram bot token'),
|
|
9
|
-
chatId: z.string().default('').description('Telegram chat ID that receives task reports'),
|
|
10
|
-
notifyTelegram: z.boolean().default(false).description('Deliver task reports to Telegram for every task'),
|
|
11
|
-
onlyOnFailure: z.boolean().default(false).description('Deliver reports only for failed runs'),
|
|
12
|
-
kanbanBaseUrl: z.string().default('http://127.0.0.1:3000').description('Base URL of the dsh-kanban HTTP API'),
|
|
13
|
-
defaultTimezone: z.string().default('').description('Default IANA time zone for schedules (empty = server local)'),
|
|
14
|
-
maxConcurrent: z.number().default(0).description('Max parallel task runs (0 = unlimited)'),
|
|
15
|
-
heartbeatUrl: z.string().default('').description('Dead man\'s snitch URL pinged on the heartbeat interval'),
|
|
16
|
-
heartbeatIntervalSec: z.number().default(0).description('Heartbeat ping interval in seconds (0 = off)'),
|
|
15
|
+
botToken: z.string().role('secret').default('').description('Legacy Telegram Bot API token (prefer botTokenRef with the DSH credentials service)').volatile(),
|
|
16
|
+
botTokenRef: z.string().default('').description('Name of the DSH credential that holds the Telegram bot token').volatile(),
|
|
17
|
+
chatId: z.string().default('').description('Telegram chat ID that receives task reports').volatile(),
|
|
18
|
+
notifyTelegram: z.boolean().default(false).description('Deliver task reports to Telegram for every task').volatile(),
|
|
19
|
+
onlyOnFailure: z.boolean().default(false).description('Deliver reports only for failed runs').volatile(),
|
|
20
|
+
kanbanBaseUrl: z.string().default('http://127.0.0.1:3000').description('Base URL of the dsh-kanban HTTP API').volatile(),
|
|
21
|
+
defaultTimezone: z.string().default('').description('Default IANA time zone for schedules (empty = server local)').volatile(),
|
|
22
|
+
maxConcurrent: z.number().default(0).description('Max parallel task runs (0 = unlimited)').volatile(),
|
|
23
|
+
heartbeatUrl: z.string().default('').description('Dead man\'s snitch URL pinged on the heartbeat interval').volatile(),
|
|
24
|
+
heartbeatIntervalSec: z.number().default(0).description('Heartbeat ping interval in seconds (0 = off)').volatile(),
|
|
17
25
|
// --- Delivery channels (#20-#23, #26, #28, #47) ---
|
|
18
|
-
template: z.string().default('').description('Global notification template; placeholders: {title} {status} {output} {error} {duration} {schedule} {time} {tokens} {cost}'),
|
|
19
|
-
channelTemplates: z.object({}).default({}).description('Per-channel template overrides keyed by channel id'),
|
|
20
|
-
silentRuleModel: z.string().default('').description('Model used to evaluate a task silent rule (empty = the task model)'),
|
|
21
|
-
inspectorModel: z.string().default('').description('Model used to diagnose failed runs (empty = the task model)'),
|
|
22
|
-
deliveryTimeoutMs: z.number().min(MIN_DELIVERY_TIMEOUT_MS).default(15000).description('Per-channel delivery timeout in ms (minimum 1000); a slower endpoint is recorded as a failure and does not block other channels or the next tick'),
|
|
23
|
-
discordWebhookUrl: z.string().default('').description('Discord webhook URL'),
|
|
24
|
-
slackWebhookUrl: z.string().default('').description('Slack incoming webhook URL'),
|
|
25
|
-
ntfyUrl: z.string().default('https://ntfy.sh').description('ntfy server base URL'),
|
|
26
|
-
ntfyTopic: z.string().default('').description('ntfy topic for push notifications'),
|
|
27
|
-
ntfyTokenRef: z.string().default('').description('Credential name for the ntfy access token'),
|
|
28
|
-
barkServerUrl: z.string().default('https://api.day.app').description('Bark server base URL'),
|
|
29
|
-
barkKey: z.string().default('').description('Bark device key'),
|
|
30
|
-
pushplusTokenRef: z.string().default('').description('Credential name for the PushPlus token'),
|
|
31
|
-
pushplusUrl: z.string().default('https://www.pushplus.plus/send').description('PushPlus send endpoint (override for a self-hosted proxy)'),
|
|
32
|
-
ttsBaseUrl: z.string().default('http://127.0.0.1:3080').description('Base URL of the harness used to reach the dsh-tts speak route'),
|
|
33
|
-
giteaBaseUrl: z.string().default('').description('Gitea base URL for alert issues'),
|
|
34
|
-
giteaRepo: z.string().default('').description('Gitea repository (owner/name) for alert issues'),
|
|
35
|
-
giteaTokenRef: z.string().default('').description('Credential name for the Gitea API token'),
|
|
36
|
-
prReviewerEnabled: z.boolean().default(false).description('Enable automated PR Reviewer scheduled agent (#33)'),
|
|
26
|
+
template: z.string().default('').description('Global notification template; placeholders: {title} {status} {output} {error} {duration} {schedule} {time} {tokens} {cost}').volatile(),
|
|
27
|
+
channelTemplates: z.object({}).default({}).description('Per-channel template overrides keyed by channel id').volatile(),
|
|
28
|
+
silentRuleModel: z.string().default('').description('Model used to evaluate a task silent rule (empty = the task model)').volatile(),
|
|
29
|
+
inspectorModel: z.string().default('').description('Model used to diagnose failed runs (empty = the task model)').volatile(),
|
|
30
|
+
deliveryTimeoutMs: z.number().min(MIN_DELIVERY_TIMEOUT_MS).default(15000).description('Per-channel delivery timeout in ms (minimum 1000); a slower endpoint is recorded as a failure and does not block other channels or the next tick').volatile(),
|
|
31
|
+
discordWebhookUrl: z.string().default('').description('Discord webhook URL').volatile(),
|
|
32
|
+
slackWebhookUrl: z.string().default('').description('Slack incoming webhook URL').volatile(),
|
|
33
|
+
ntfyUrl: z.string().default('https://ntfy.sh').description('ntfy server base URL').volatile(),
|
|
34
|
+
ntfyTopic: z.string().default('').description('ntfy topic for push notifications').volatile(),
|
|
35
|
+
ntfyTokenRef: z.string().default('').description('Credential name for the ntfy access token').volatile(),
|
|
36
|
+
barkServerUrl: z.string().default('https://api.day.app').description('Bark server base URL').volatile(),
|
|
37
|
+
barkKey: z.string().default('').description('Bark device key').volatile(),
|
|
38
|
+
pushplusTokenRef: z.string().default('').description('Credential name for the PushPlus token').volatile(),
|
|
39
|
+
pushplusUrl: z.string().default('https://www.pushplus.plus/send').description('PushPlus send endpoint (override for a self-hosted proxy)').volatile(),
|
|
40
|
+
ttsBaseUrl: z.string().default('http://127.0.0.1:3080').description('Base URL of the harness used to reach the dsh-tts speak route').volatile(),
|
|
41
|
+
giteaBaseUrl: z.string().default('').description('Gitea base URL for alert issues').volatile(),
|
|
42
|
+
giteaRepo: z.string().default('').description('Gitea repository (owner/name) for alert issues').volatile(),
|
|
43
|
+
giteaTokenRef: z.string().default('').description('Credential name for the Gitea API token').volatile(),
|
|
44
|
+
prReviewerEnabled: z.boolean().default(false).description('Enable automated PR Reviewer scheduled agent (#33)').volatile(),
|
|
37
45
|
// --- Declarative jobs (#50, ADR-0001) ---
|
|
38
46
|
jobs: z.array(z.any()).default([]).description('Static jobs owned by the config; each entry needs id, title, schedule and (for agent types) prompt'),
|
|
39
47
|
// --- External REST API (#54, ADR-0001) ---
|
|
40
|
-
apiToken: z.string().role('secret').default('').description('Bearer token for the external /dsh-cron/api/* surface (empty = the surface answers 503)'),
|
|
48
|
+
apiToken: z.string().role('secret').default('').description('Bearer token for the external /dsh-cron/api/* surface (empty = the surface answers 503)').volatile(),
|
|
41
49
|
});
|
|
42
50
|
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Unwrap a volatile config reference or wrap a config object in a transparent Proxy.
|
|
54
|
+
*/
|
|
55
|
+
export function unwrapConfigValue(val) {
|
|
56
|
+
if (val && typeof val === 'object' && typeof val.get === 'function') {
|
|
57
|
+
return val.get();
|
|
58
|
+
}
|
|
59
|
+
return val;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function unwrapConfig(cfg) {
|
|
63
|
+
if (!cfg || typeof cfg !== 'object') return cfg;
|
|
64
|
+
if (typeof cfg.get === 'function') return unwrapConfig(cfg.get());
|
|
65
|
+
return new Proxy(cfg, {
|
|
66
|
+
get(target, prop, receiver) {
|
|
67
|
+
const val = Reflect.get(target, prop, receiver);
|
|
68
|
+
if (val && typeof val === 'object' && typeof val.get === 'function') {
|
|
69
|
+
return val.get();
|
|
70
|
+
}
|
|
71
|
+
return val;
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
43
76
|
export const SETTINGS_SYNC_KEYS = [
|
|
44
77
|
'botToken', 'botTokenRef', 'chatId', 'notifyTelegram', 'onlyOnFailure', 'kanbanBaseUrl',
|
|
45
78
|
'template', 'channelTemplates', 'deliveryTimeoutMs', 'silentRuleModel', 'inspectorModel',
|
|
@@ -58,18 +91,37 @@ export const SETTINGS_SYNC_KEYS = [
|
|
|
58
91
|
* snapshot stays authoritative (#102). Returns null when the settings
|
|
59
92
|
* service is unavailable (headless profiles fall back to store-only writes).
|
|
60
93
|
*/
|
|
61
|
-
export async function applySettingsToScope(
|
|
62
|
-
if (!
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (!Object.prototype.hasOwnProperty.call(body, key)) continue;
|
|
94
|
+
export async function applySettingsToScope(settingsTarget, body) {
|
|
95
|
+
if (!settingsTarget) return null;
|
|
96
|
+
// DSH 0.1.7: SettingsForms service has update(ns, patch)
|
|
97
|
+
if (typeof settingsTarget.update === 'function') {
|
|
66
98
|
try {
|
|
67
|
-
|
|
99
|
+
const patch = {};
|
|
100
|
+
for (const key of SETTINGS_SYNC_KEYS) {
|
|
101
|
+
if (Object.prototype.hasOwnProperty.call(body, key)) {
|
|
102
|
+
patch[key] = body[key];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
await settingsTarget.update('dsh-cron', patch);
|
|
106
|
+
return [];
|
|
68
107
|
} catch (e) {
|
|
69
|
-
|
|
108
|
+
return ['dsh-cron: ' + ((e && e.message) || e)];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Legacy / mock scope with set(key, value)
|
|
112
|
+
if (typeof settingsTarget.set === 'function') {
|
|
113
|
+
const errors = [];
|
|
114
|
+
for (const key of SETTINGS_SYNC_KEYS) {
|
|
115
|
+
if (!Object.prototype.hasOwnProperty.call(body, key)) continue;
|
|
116
|
+
try {
|
|
117
|
+
await settingsTarget.set(key, body[key]);
|
|
118
|
+
} catch (e) {
|
|
119
|
+
errors.push(key + ': ' + ((e && e.message) || e));
|
|
120
|
+
}
|
|
70
121
|
}
|
|
122
|
+
return errors;
|
|
71
123
|
}
|
|
72
|
-
return
|
|
124
|
+
return null;
|
|
73
125
|
}
|
|
74
126
|
|
|
75
127
|
/**
|