@goodandready/dsh-cron 0.2.18 → 0.2.20
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 +16 -1
- package/README.ru.md +17 -2
- package/README.zh.md +17 -2
- package/lib/api-helpers.js +68 -0
- package/lib/api-import-export.js +145 -0
- package/lib/api-webhook.js +115 -0
- package/lib/api.js +24 -309
- package/lib/best-effort.js +29 -0
- package/lib/channels.js +6 -1
- package/lib/client.js +80 -41
- package/lib/index.js +25 -361
- package/lib/llm-actions.js +9 -6
- package/lib/llm-ask.js +8 -9
- package/lib/recipes.js +3 -1
- package/lib/routes.js +194 -0
- package/lib/runner-external.js +238 -0
- package/lib/runner-worktree.js +55 -0
- package/lib/runner.js +57 -292
- package/lib/runtimes.js +3 -3
- package/lib/scheduler-execution.js +494 -0
- package/lib/scheduler-lifecycle.js +143 -0
- package/lib/scheduler.js +120 -643
- package/lib/secrets.js +6 -7
- package/lib/settings.js +120 -0
- package/lib/store.js +5 -8
- package/lib/task-create.js +60 -0
- package/lib/templates.js +5 -1
- package/package.json +5 -4
package/lib/index.js
CHANGED
|
@@ -69,188 +69,26 @@ import { applyConfigSync, isConfigOwned, configOwnedMessage } from './config-job
|
|
|
69
69
|
import { createExternalApiHandler, EXTERNAL_API_PREFIX } from './external-api.js';
|
|
70
70
|
export { createCronApiHandler };
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
notifyTelegram: z.boolean().default(false).description('Deliver task reports to Telegram for every task'),
|
|
80
|
-
onlyOnFailure: z.boolean().default(false).description('Deliver reports only for failed runs'),
|
|
81
|
-
kanbanBaseUrl: z.string().default('http://127.0.0.1:3000').description('Base URL of the dsh-kanban HTTP API'),
|
|
82
|
-
defaultTimezone: z.string().default('').description('Default IANA time zone for schedules (empty = server local)'),
|
|
83
|
-
maxConcurrent: z.number().default(0).description('Max parallel task runs (0 = unlimited)'),
|
|
84
|
-
heartbeatUrl: z.string().default('').description('Dead man\'s snitch URL pinged on the heartbeat interval'),
|
|
85
|
-
heartbeatIntervalSec: z.number().default(0).description('Heartbeat ping interval in seconds (0 = off)'),
|
|
86
|
-
// --- Delivery channels (#20-#23, #26, #28, #47) ---
|
|
87
|
-
template: z.string().default('').description('Global notification template; placeholders: {title} {status} {output} {error} {duration} {schedule} {time} {tokens} {cost}'),
|
|
88
|
-
channelTemplates: z.object({}).default({}).description('Per-channel template overrides keyed by channel id'),
|
|
89
|
-
silentRuleModel: z.string().default('').description('Model used to evaluate a task silent rule (empty = the task model)'),
|
|
90
|
-
inspectorModel: z.string().default('').description('Model used to diagnose failed runs (empty = the task model)'),
|
|
91
|
-
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'),
|
|
92
|
-
discordWebhookUrl: z.string().default('').description('Discord webhook URL'),
|
|
93
|
-
slackWebhookUrl: z.string().default('').description('Slack incoming webhook URL'),
|
|
94
|
-
ntfyUrl: z.string().default('https://ntfy.sh').description('ntfy server base URL'),
|
|
95
|
-
ntfyTopic: z.string().default('').description('ntfy topic for push notifications'),
|
|
96
|
-
ntfyTokenRef: z.string().default('').description('Credential name for the ntfy access token'),
|
|
97
|
-
barkServerUrl: z.string().default('https://api.day.app').description('Bark server base URL'),
|
|
98
|
-
barkKey: z.string().default('').description('Bark device key'),
|
|
99
|
-
pushplusTokenRef: z.string().default('').description('Credential name for the PushPlus token'),
|
|
100
|
-
pushplusUrl: z.string().default('https://www.pushplus.plus/send').description('PushPlus send endpoint (override for a self-hosted proxy)'),
|
|
101
|
-
ttsBaseUrl: z.string().default('http://127.0.0.1:3080').description('Base URL of the harness used to reach the dsh-tts speak route'),
|
|
102
|
-
giteaBaseUrl: z.string().default('').description('Gitea base URL for alert issues'),
|
|
103
|
-
giteaRepo: z.string().default('').description('Gitea repository (owner/name) for alert issues'),
|
|
104
|
-
giteaTokenRef: z.string().default('').description('Credential name for the Gitea API token'),
|
|
105
|
-
prReviewerEnabled: z.boolean().default(false).description('Enable automated PR Reviewer scheduled agent (#33)'),
|
|
106
|
-
// --- Declarative jobs (#50, ADR-0001) ---
|
|
107
|
-
// The array is intentionally permissive: each entry is validated on its own
|
|
108
|
-
// with a precise message (see lib/config-jobs.js), so one broken entry cannot
|
|
109
|
-
// reject the whole profile config. Declared jobs are owned by the config:
|
|
110
|
-
// they are created or updated at startup, and removed when they disappear
|
|
111
|
-
// from the file. Tasks created in the UI, over the API or by an agent tool
|
|
112
|
-
// are never touched.
|
|
113
|
-
jobs: z.array(z.any()).default([]).description('Static jobs owned by the config; each entry needs id, title, schedule and (for agent types) prompt'),
|
|
114
|
-
// --- External REST API (#54, ADR-0001) ---
|
|
115
|
-
apiToken: z.string().role('secret').default('').description('Bearer token for the external /dsh-cron/api/* surface (empty = the surface answers 503)'),
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
export const SETTINGS_SYNC_KEYS = [
|
|
120
|
-
'botToken', 'botTokenRef', 'chatId', 'notifyTelegram', 'onlyOnFailure', 'kanbanBaseUrl',
|
|
121
|
-
'template', 'channelTemplates', 'deliveryTimeoutMs', 'silentRuleModel', 'inspectorModel',
|
|
122
|
-
'discordWebhookUrl', 'slackWebhookUrl',
|
|
123
|
-
'ntfyUrl', 'ntfyTopic', 'ntfyTokenRef',
|
|
124
|
-
'barkServerUrl', 'barkKey',
|
|
125
|
-
'pushplusTokenRef',
|
|
126
|
-
'pushplusUrl',
|
|
127
|
-
'ttsBaseUrl',
|
|
128
|
-
'giteaBaseUrl', 'giteaRepo', 'giteaTokenRef', 'prReviewerEnabled',
|
|
129
|
-
'apiToken',
|
|
130
|
-
];
|
|
72
|
+
import {
|
|
73
|
+
Config,
|
|
74
|
+
SETTINGS_SYNC_KEYS,
|
|
75
|
+
applySettingsToScope,
|
|
76
|
+
sanitizeSettingsPayload,
|
|
77
|
+
} from './settings.js';
|
|
78
|
+
import { registerRoutes } from './routes.js';
|
|
131
79
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (!settingsScope || typeof settingsScope.set !== 'function') return null;
|
|
139
|
-
const errors = [];
|
|
140
|
-
for (const key of SETTINGS_SYNC_KEYS) {
|
|
141
|
-
if (!Object.prototype.hasOwnProperty.call(body, key)) continue;
|
|
142
|
-
try {
|
|
143
|
-
await settingsScope.set(key, body[key]);
|
|
144
|
-
} catch (e) {
|
|
145
|
-
errors.push(key + ': ' + ((e && e.message) || e));
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
return errors;
|
|
149
|
-
}
|
|
80
|
+
export {
|
|
81
|
+
Config,
|
|
82
|
+
SETTINGS_SYNC_KEYS,
|
|
83
|
+
applySettingsToScope,
|
|
84
|
+
sanitizeSettingsPayload,
|
|
85
|
+
};
|
|
150
86
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
*
|
|
154
|
-
* - a masked secret echoed back by the settings UI would overwrite the real
|
|
155
|
-
* value (bot token, webhook URLs, Bark key);
|
|
156
|
-
* - the raw credential keys are refused outright — the *Ref fields are the
|
|
157
|
-
* supported way to configure them.
|
|
158
|
-
*
|
|
159
|
-
* Applied before both the settings scope and the store, because the scope is
|
|
160
|
-
* the source of truth and would otherwise persist a raw or masked value.
|
|
161
|
-
*/
|
|
162
|
-
export function sanitizeSettingsPayload(body) {
|
|
163
|
-
const payload = Object.assign({}, body);
|
|
164
|
-
for (const key of TaskStore.MASKED_SETTING_KEYS) {
|
|
165
|
-
const val = payload[key];
|
|
166
|
-
if (typeof val === 'string' && (val.includes('••') || val.includes('****'))) {
|
|
167
|
-
delete payload[key];
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
for (const key of ['botTokenRef', 'barkKeyRef', 'giteaTokenRef']) {
|
|
171
|
-
if (key in payload && typeof payload[key] === 'string' && payload[key].trim()) {
|
|
172
|
-
const refCheck = credentialRefStatus(payload[key]);
|
|
173
|
-
if (!refCheck.ok) {
|
|
174
|
-
console.warn(`[dsh-cron] invalid credential reference in "${key}": ${refCheck.error}`);
|
|
175
|
-
delete payload[key];
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
for (const key of TaskStore.FORBIDDEN_SETTING_KEYS) {
|
|
180
|
-
if (key in payload) {
|
|
181
|
-
console.warn(`[dsh-cron] refusing raw secret "${key}" from the client — use a credential reference`);
|
|
182
|
-
delete payload[key];
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
// #115: normalise the delivery deadline here, before it reaches the settings
|
|
186
|
-
// scope, so a value like 1 ms can never be persisted and then fail every
|
|
187
|
-
// delivery before the request is even sent.
|
|
188
|
-
if ('deliveryTimeoutMs' in payload) {
|
|
189
|
-
const raw = payload.deliveryTimeoutMs;
|
|
190
|
-
const n = Number(raw);
|
|
191
|
-
if (raw === '' || raw === null || !Number.isFinite(n) || n <= 0) {
|
|
192
|
-
delete payload.deliveryTimeoutMs;
|
|
193
|
-
} else {
|
|
194
|
-
payload.deliveryTimeoutMs = resolveDeliveryTimeoutMs(n);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
return payload;
|
|
198
|
-
}
|
|
87
|
+
export const name = '@goodandready/dsh-cron';
|
|
88
|
+
export const inject = ['tools', 'webServer', 'settings', 'llm', 'agents', 'agentDefaultModel', 'credentials'];
|
|
199
89
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const typeError = validateTaskType(taskType, args);
|
|
203
|
-
if (typeError) throw new Error(typeError);
|
|
204
|
-
const parsed = parseScheduleExpression(args.schedule);
|
|
205
|
-
const task = store.set({
|
|
206
|
-
title: args.title,
|
|
207
|
-
schedule: parsed.cronPattern || args.schedule,
|
|
208
|
-
scheduleText: parsed.humanText,
|
|
209
|
-
prompt: args.prompt,
|
|
210
|
-
type: taskType,
|
|
211
|
-
delivery: args.delivery || 'isolated',
|
|
212
|
-
status: 'active',
|
|
213
|
-
provider: args.provider,
|
|
214
|
-
model: args.model,
|
|
215
|
-
notifyTelegram: Boolean(args.notifyTelegram),
|
|
216
|
-
onlyOnFailure: Boolean(args.onlyOnFailure),
|
|
217
|
-
timeoutSeconds: Number(args.timeoutSeconds) || 1800,
|
|
218
|
-
overlapPolicy: args.overlapPolicy || 'skip',
|
|
219
|
-
timezone: args.timezone ? String(args.timezone).trim() : '',
|
|
220
|
-
misfirePolicy: ['skip', 'runOnce', 'catchUpAll'].includes(args.misfirePolicy) ? args.misfirePolicy : 'skip',
|
|
221
|
-
maxRetries: Number(args.maxRetries) > 0 ? Number(args.maxRetries) : 0,
|
|
222
|
-
retryBackoffMs: Number(args.retryBackoffMs) > 0 ? Number(args.retryBackoffMs) : 30000,
|
|
223
|
-
permissionPreset: ['default', 'read-only', 'workspace-write', 'full'].includes(args.permissionPreset) ? args.permissionPreset : 'default',
|
|
224
|
-
kanbanMode: args.kanbanMode || 'none',
|
|
225
|
-
channels: Array.isArray(args.channels) ? args.channels.filter((c) => CHANNEL_IDS.includes(c)) : undefined,
|
|
226
|
-
template: args.template ? String(args.template) : '',
|
|
227
|
-
env: args.env && typeof args.env === 'object' ? args.env : undefined,
|
|
228
|
-
cwd: args.cwd ? String(args.cwd).trim() : '',
|
|
229
|
-
workspaceId: args.workspaceId ? String(args.workspaceId).trim() : '',
|
|
230
|
-
worktree: Boolean(args.worktree),
|
|
231
|
-
keepWorktree: Boolean(args.keepWorktree),
|
|
232
|
-
httpMethod: args.httpMethod ? String(args.httpMethod).toUpperCase() : 'GET',
|
|
233
|
-
httpUrl: args.httpUrl ? String(args.httpUrl).trim() : '',
|
|
234
|
-
httpHeaders: args.httpHeaders,
|
|
235
|
-
httpBody: args.httpBody !== undefined ? String(args.httpBody) : '',
|
|
236
|
-
sshProfileId: args.sshProfileId ? String(args.sshProfileId).trim() : '',
|
|
237
|
-
sshTarget: args.sshTarget ? String(args.sshTarget).trim() : '',
|
|
238
|
-
sshPort: Number(args.sshPort) || 0,
|
|
239
|
-
sshKeyPath: args.sshKeyPath ? String(args.sshKeyPath).trim() : '',
|
|
240
|
-
dockerImage: args.dockerImage ? String(args.dockerImage).trim() : '',
|
|
241
|
-
pythonPath: args.pythonPath ? String(args.pythonPath).trim() : '',
|
|
242
|
-
nodePath: args.nodePath ? String(args.nodePath).trim() : '',
|
|
243
|
-
skillName: args.skillName ? String(args.skillName).trim() : '',
|
|
244
|
-
workflowName: args.workflowName ? String(args.workflowName).trim() : '',
|
|
245
|
-
oneShot: Boolean(parsed.isOneShot),
|
|
246
|
-
});
|
|
247
|
-
scheduler.scheduleTask(task);
|
|
248
|
-
return {
|
|
249
|
-
success: true,
|
|
250
|
-
message: `Task "${task.title}" scheduled successfully (${task.scheduleText})`,
|
|
251
|
-
task
|
|
252
|
-
};
|
|
253
|
-
}
|
|
90
|
+
import { executeCreateTask } from './task-create.js';
|
|
91
|
+
export { executeCreateTask };
|
|
254
92
|
|
|
255
93
|
export const createTaskParameters = {
|
|
256
94
|
title: { type: 'string', description: 'Short task name', required: true },
|
|
@@ -397,79 +235,15 @@ export function apply(ctx, config) {
|
|
|
397
235
|
}), 'dsh-cron: /api/dsh-cron/update');
|
|
398
236
|
|
|
399
237
|
// 1. REST API — collection (list/create) + item actions in one prefix
|
|
400
|
-
//
|
|
401
|
-
ctx
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
// Alias route /dsh-cron/action/:id/:action for backward compatibility (#66)
|
|
408
|
-
ctx.effect(() => ctx.webServer.register({
|
|
409
|
-
kind: 'prefix',
|
|
410
|
-
path: '/dsh-cron/action',
|
|
411
|
-
handler: createCronApiHandler(store, scheduler, null)
|
|
412
|
-
}), 'dsh-cron: /action/:id/:action');
|
|
413
|
-
|
|
414
|
-
ctx.effect(() => ctx.webServer.register({
|
|
415
|
-
kind: 'prefix',
|
|
416
|
-
path: '/dsh-cron/telegram',
|
|
417
|
-
handler: createCronApiHandler(store, scheduler, null)
|
|
418
|
-
}), 'dsh-cron: /telegram[...]');
|
|
419
|
-
|
|
420
|
-
ctx.effect(() => ctx.webServer.register({
|
|
421
|
-
kind: 'prefix',
|
|
422
|
-
path: '/dsh-cron/heartbeat-ping',
|
|
423
|
-
handler: createCronApiHandler(store, scheduler, null)
|
|
424
|
-
}), 'dsh-cron: /heartbeat-ping[...]');
|
|
425
|
-
|
|
426
|
-
ctx.effect(() => ctx.webServer.register({
|
|
427
|
-
kind: 'prefix',
|
|
428
|
-
path: '/dsh-cron/schedule',
|
|
429
|
-
handler: createCronApiHandler(store, scheduler, null)
|
|
430
|
-
}), 'dsh-cron: /schedule[...]');
|
|
431
|
-
|
|
432
|
-
// GET /dsh-cron/metrics (#53) — Prometheus text exposition
|
|
433
|
-
ctx.effect(() => ctx.webServer.register({
|
|
434
|
-
kind: 'exact',
|
|
435
|
-
path: '/dsh-cron/metrics',
|
|
436
|
-
handler: createMetricsHandler({ store, scheduler })
|
|
437
|
-
}), 'dsh-cron: /metrics');
|
|
438
|
-
|
|
439
|
-
// External REST API under /dsh-cron/api/* (#54, ADR-0001): the only surface
|
|
440
|
-
// guarded by a bearer token, so CI and host automation can drive the
|
|
441
|
-
// scheduler without a browser session. The token is read per request, so a
|
|
442
|
-
// settings change applies without a restart.
|
|
443
|
-
ctx.effect(() => ctx.webServer.register({
|
|
444
|
-
kind: 'prefix',
|
|
445
|
-
path: EXTERNAL_API_PREFIX,
|
|
446
|
-
handler: createExternalApiHandler({
|
|
447
|
-
store,
|
|
448
|
-
scheduler,
|
|
449
|
-
getToken: () => store.getSettings().apiToken || ''
|
|
450
|
-
})
|
|
451
|
-
}), 'dsh-cron: /api[...]');
|
|
452
|
-
|
|
453
|
-
// GET /dsh-cron/models
|
|
454
|
-
ctx.effect(() => ctx.webServer.register({
|
|
455
|
-
kind: 'exact',
|
|
456
|
-
path: '/dsh-cron/models',
|
|
457
|
-
handler: async (req, res) => {
|
|
458
|
-
await getModelsHandler(ctx, req, res, sendJson);
|
|
459
|
-
}
|
|
460
|
-
}), 'dsh-cron: /models');
|
|
461
|
-
|
|
462
|
-
// POST /dsh-cron/chat/start
|
|
463
|
-
ctx.effect(() => ctx.webServer.register({
|
|
464
|
-
kind: 'exact',
|
|
465
|
-
path: '/dsh-cron/chat/start',
|
|
466
|
-
handler: async (req, res) => {
|
|
467
|
-
if (req.method === 'POST' && rejectCrossOrigin(req, res)) return;
|
|
468
|
-
await chatStartHandler(ctx, req, res, parseJsonBody, sendJson, randomUUID);
|
|
469
|
-
}
|
|
470
|
-
}), 'dsh-cron: /chat/start');
|
|
238
|
+
// 1. Web server routes (extracted to ./routes.js)
|
|
239
|
+
registerRoutes(ctx, {
|
|
240
|
+
store,
|
|
241
|
+
scheduler,
|
|
242
|
+
getCronSettingsScope: () => cronSettingsScope,
|
|
243
|
+
recommendations,
|
|
244
|
+
});
|
|
471
245
|
|
|
472
|
-
// GET & POST /dsh-cron/settings
|
|
246
|
+
// GET & POST /dsh-cron/settings (#102)
|
|
473
247
|
ctx.effect(() => ctx.webServer.register({
|
|
474
248
|
kind: 'exact',
|
|
475
249
|
path: '/dsh-cron/settings',
|
|
@@ -483,8 +257,6 @@ export function apply(ctx, config) {
|
|
|
483
257
|
if (rejectCrossOrigin(req, res)) return;
|
|
484
258
|
const { body, error } = await readBody(req, res);
|
|
485
259
|
if (error) return;
|
|
486
|
-
// Masked secrets echoed back by the client must never overwrite the
|
|
487
|
-
// stored token (store.saveSettings guards this too, #86-era).
|
|
488
260
|
const payload = sanitizeSettingsPayload(body);
|
|
489
261
|
const scopeErrors = await applySettingsToScope(cronSettingsScope, payload);
|
|
490
262
|
if (scopeErrors === null) {
|
|
@@ -500,114 +272,6 @@ export function apply(ctx, config) {
|
|
|
500
272
|
}
|
|
501
273
|
}), 'dsh-cron: /settings');
|
|
502
274
|
|
|
503
|
-
// GET /dsh-cron/recipes (#48): the built-in recipe catalog.
|
|
504
|
-
ctx.effect(() => ctx.webServer.register({
|
|
505
|
-
kind: 'exact',
|
|
506
|
-
path: '/dsh-cron/recipes',
|
|
507
|
-
handler: async (req, res) => {
|
|
508
|
-
try {
|
|
509
|
-
if (req.method !== 'GET') {
|
|
510
|
-
sendJson(res, 405, { ok: false, error: 'Method not allowed' });
|
|
511
|
-
return;
|
|
512
|
-
}
|
|
513
|
-
const categories = recipesByCategory();
|
|
514
|
-
const recipes = categories.reduce((acc, category) => acc.concat(category.recipes), []);
|
|
515
|
-
sendJson(res, 200, { ok: true, categories, recipes });
|
|
516
|
-
} catch (err) {
|
|
517
|
-
sendJson(res, err.statusCode || 500, { ok: false, error: err.message });
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
}), 'dsh-cron: /recipes');
|
|
521
|
-
|
|
522
|
-
// GET /dsh-cron/heartbeat (#16): liveness probe for external monitors.
|
|
523
|
-
ctx.effect(() => ctx.webServer.register({
|
|
524
|
-
kind: 'exact',
|
|
525
|
-
path: '/dsh-cron/heartbeat',
|
|
526
|
-
handler: async (req, res) => {
|
|
527
|
-
try {
|
|
528
|
-
const tasks = store.list({ status: 'active' });
|
|
529
|
-
const lastRuns = tasks.map((t) => t.lastRunAt || 0);
|
|
530
|
-
sendJson(res, 200, {
|
|
531
|
-
ok: true,
|
|
532
|
-
activeTasks: tasks.length,
|
|
533
|
-
running: scheduler.runningCount(),
|
|
534
|
-
lastRunAt: lastRuns.length ? Math.max(...lastRuns) : null,
|
|
535
|
-
serverTime: Date.now(),
|
|
536
|
-
});
|
|
537
|
-
} catch (err) {
|
|
538
|
-
sendJson(res, 500, { ok: false, error: err.message });
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
}), 'dsh-cron: /heartbeat');
|
|
542
|
-
|
|
543
|
-
// POST /dsh-cron/telegram/test
|
|
544
|
-
ctx.effect(() => ctx.webServer.register({
|
|
545
|
-
kind: 'exact',
|
|
546
|
-
path: '/dsh-cron/telegram/test',
|
|
547
|
-
handler: async (req, res) => {
|
|
548
|
-
try {
|
|
549
|
-
if (req.method !== 'POST') {
|
|
550
|
-
sendJson(res, 405, { ok: false, error: 'Method not allowed' });
|
|
551
|
-
return;
|
|
552
|
-
}
|
|
553
|
-
if (rejectCrossOrigin(req, res)) return;
|
|
554
|
-
const { body, error } = await readBody(req, res);
|
|
555
|
-
if (error) return;
|
|
556
|
-
const settings = store.getSettings();
|
|
557
|
-
const botToken = body.botToken || settings.botToken;
|
|
558
|
-
const chatId = body.chatId || settings.chatId;
|
|
559
|
-
|
|
560
|
-
if (!botToken || !chatId) {
|
|
561
|
-
sendJson(res, 400, { ok: false, error: 'botToken or chatId is not configured' });
|
|
562
|
-
return;
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
const text = `🔔 *dsh-cron test notification*\n\nConnection between DSH Cron and Telegram is working! Checked at: \`${new Date().toISOString()}\``;
|
|
566
|
-
const replyMarkup = createTelegramTaskKeyboard(task, runInfo);
|
|
567
|
-
await sendTelegramMessage({ botToken, chatId, text, replyMarkup });
|
|
568
|
-
sendJson(res, 200, { ok: true, message: 'Test message sent successfully' });
|
|
569
|
-
} catch (err) {
|
|
570
|
-
sendJson(res, 500, { ok: false, error: err.message });
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
}), 'dsh-cron: /telegram/test');
|
|
574
|
-
|
|
575
|
-
// POST /dsh-cron/kanban/test
|
|
576
|
-
ctx.effect(() => ctx.webServer.register({
|
|
577
|
-
kind: 'exact',
|
|
578
|
-
path: '/dsh-cron/kanban/test',
|
|
579
|
-
handler: async (req, res) => {
|
|
580
|
-
try {
|
|
581
|
-
if (req.method !== 'POST') {
|
|
582
|
-
sendJson(res, 405, { ok: false, error: 'Method not allowed' });
|
|
583
|
-
return;
|
|
584
|
-
}
|
|
585
|
-
if (rejectCrossOrigin(req, res)) return;
|
|
586
|
-
const { body, error } = await readBody(req, res);
|
|
587
|
-
if (error) return;
|
|
588
|
-
const settings = store.getSettings();
|
|
589
|
-
const kanbanBaseUrl = body.kanbanBaseUrl || settings.kanbanBaseUrl || 'http://127.0.0.1:3000';
|
|
590
|
-
|
|
591
|
-
const result = await createKanbanCard({
|
|
592
|
-
title: '[Test] dsh-cron -> Kanban connectivity check',
|
|
593
|
-
body: `Test card created automatically by the \`dsh-cron\` module at ${new Date().toISOString()}.`,
|
|
594
|
-
board: 'main',
|
|
595
|
-
column: 'backlog',
|
|
596
|
-
labels: ['cron', 'test'],
|
|
597
|
-
kanbanBaseUrl
|
|
598
|
-
});
|
|
599
|
-
|
|
600
|
-
if (result.success) {
|
|
601
|
-
sendJson(res, 200, { ok: true, task: result.task });
|
|
602
|
-
} else {
|
|
603
|
-
sendJson(res, 500, { ok: false, error: result.error });
|
|
604
|
-
}
|
|
605
|
-
} catch (err) {
|
|
606
|
-
sendJson(res, 500, { ok: false, error: err.message });
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
}), 'dsh-cron: /kanban/test');
|
|
610
|
-
|
|
611
275
|
// 2. Tools
|
|
612
276
|
ctx.tools.register(defineTool({
|
|
613
277
|
name: 'cron_create_task',
|
package/lib/llm-actions.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { bestEffort } from './best-effort.js';
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -14,10 +15,10 @@ export function parseLlmActionDirectives(text) {
|
|
|
14
15
|
while ((match = blockRegex.exec(text)) !== null) {
|
|
15
16
|
const raw = match[1].trim();
|
|
16
17
|
if ((raw.startsWith('{') && raw.endsWith('}')) || (raw.startsWith('[') && raw.endsWith(']'))) {
|
|
17
|
-
|
|
18
|
+
bestEffort('parse-directive-json', () => {
|
|
18
19
|
const parsed = JSON.parse(raw);
|
|
19
20
|
collectDirectives(parsed, directives);
|
|
20
|
-
}
|
|
21
|
+
});
|
|
21
22
|
}
|
|
22
23
|
}
|
|
23
24
|
|
|
@@ -26,10 +27,10 @@ export function parseLlmActionDirectives(text) {
|
|
|
26
27
|
const inlineMatch = /\{(?:[^{}]|(\{(?:[^{}]|\{[^{}]*\})*\}))*"dsh_action[s]?"(?:[^{}]|(\{(?:[^{}]|\{[^{}]*\})*\}))*\}/gi;
|
|
27
28
|
let im;
|
|
28
29
|
while ((im = inlineMatch.exec(text)) !== null) {
|
|
29
|
-
|
|
30
|
+
bestEffort('parse-inline-directive', () => {
|
|
30
31
|
const parsed = JSON.parse(im[0]);
|
|
31
32
|
collectDirectives(parsed, directives);
|
|
32
|
-
}
|
|
33
|
+
});
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
|
|
@@ -116,7 +117,7 @@ export async function executeLlmActionDirectives({ directives, task, runInfo, sc
|
|
|
116
117
|
async function tryCreateGiteaIssue({ title, body, repo = 'goodandready/dsh-cron', fetchFn }) {
|
|
117
118
|
try {
|
|
118
119
|
let token = process.env.GITEA_TOKEN;
|
|
119
|
-
const credPath =
|
|
120
|
+
const credPath = process.env.GITEA_CREDENTIALS_PATH || '';
|
|
120
121
|
if (!token && fs.existsSync(credPath)) {
|
|
121
122
|
const data = JSON.parse(fs.readFileSync(credPath, 'utf-8'));
|
|
122
123
|
token = data?.agents?.antigravity?.token || data?.token;
|
|
@@ -136,6 +137,8 @@ async function tryCreateGiteaIssue({ title, body, repo = 'goodandready/dsh-cron'
|
|
|
136
137
|
const json = await res.json().catch(() => ({}));
|
|
137
138
|
return { number: json.number, url: json.html_url };
|
|
138
139
|
}
|
|
139
|
-
} catch {
|
|
140
|
+
} catch (err) {
|
|
141
|
+
/* ignore Gitea creation errors, best-effort */
|
|
142
|
+
}
|
|
140
143
|
return null;
|
|
141
144
|
}
|
package/lib/llm-ask.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { bestEffort } from './best-effort.js';
|
|
1
2
|
/**
|
|
2
3
|
* Small helper for asking the configured model a short question (#44, #43).
|
|
3
4
|
*
|
|
@@ -26,11 +27,11 @@ function getLlm(ctx) {
|
|
|
26
27
|
/** Provider and model for an auxiliary question: task first, then the default. */
|
|
27
28
|
export function resolveAskTarget(ctx, task = {}, preferredModel = '') {
|
|
28
29
|
let defaultSelection = null;
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
defaultSelection = bestEffort('resolve-default-model', () => {
|
|
31
|
+
return ctx && typeof ctx.get === 'function'
|
|
31
32
|
? ctx.get('agentDefaultModel')?.currentSelection?.() || null
|
|
32
33
|
: null;
|
|
33
|
-
}
|
|
34
|
+
}) || null;
|
|
34
35
|
return {
|
|
35
36
|
provider: task.provider || defaultSelection?.provider || '',
|
|
36
37
|
model: preferredModel || task.model || defaultSelection?.model || '',
|
|
@@ -60,12 +61,10 @@ function isCleanFinish(kind) {
|
|
|
60
61
|
async function buildUserMessage(text) {
|
|
61
62
|
const content = [{ type: 'text', text }];
|
|
62
63
|
const source = { kind: 'plugin', plugin: 'dsh-cron', form: 'helper-call' };
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
} catch {}
|
|
64
|
+
const mod = await bestEffort('import-dsh-llm', () => import('@deepseek-ai/dsh-llm'));
|
|
65
|
+
if (mod && typeof mod.createUserMessage === 'function') {
|
|
66
|
+
return mod.createUserMessage({ content, source });
|
|
67
|
+
}
|
|
69
68
|
return { role: 'user', content, source };
|
|
70
69
|
}
|
|
71
70
|
|
package/lib/recipes.js
CHANGED
|
@@ -207,7 +207,9 @@ export const RECIPES = [
|
|
|
207
207
|
|
|
208
208
|
/** Detached copy of the catalog, safe to hand to the UI. */
|
|
209
209
|
export function listRecipes() {
|
|
210
|
-
|
|
210
|
+
const destructive = findDestructiveRecipe();
|
|
211
|
+
const safeRecipes = destructive ? RECIPES.filter((r) => r.id !== destructive.id) : RECIPES;
|
|
212
|
+
return safeRecipes.map((recipe) => ({ ...recipe, channels: [...(recipe.channels || [])] }));
|
|
211
213
|
}
|
|
212
214
|
|
|
213
215
|
export function recipesByCategory() {
|