@goodandready/dsh-cron 0.2.28 → 0.2.30

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/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ import fs from 'node:fs';
2
+ import { setLogger, logger } from './logger.js';
1
3
  import { registerPluginUpdater } from './updater.js';
2
4
  import { credentialRefStatus } from './secrets.js';
3
5
  import { TaskStore } from './store.js';
@@ -92,66 +94,29 @@ export const name = '@goodandready/dsh-cron';
92
94
  export const inject = ['tools', 'webServer', 'llm', 'agents', 'agentDefaultModel', 'credentials'];
93
95
 
94
96
  import { executeCreateTask } from './task-create.js';
95
- export { executeCreateTask };
96
-
97
- export const createTaskParameters = {
98
- title: { type: 'string', description: 'Short task name', required: true },
99
- schedule: { type: 'string', description: 'Schedule: cron ("0 9 * * *"), shorthand ("@daily", "@every 30m"), interval ("every 2h") or one-shot ("in 30m", "at: 2026-09-05T15:00:00Z")', required: true },
100
- prompt: { type: 'string', description: 'Prompt/instruction for the agent at run time', required: true },
101
- type: { type: 'string', enum: TASK_TYPES, description: 'Task type: llm | script | node | python | http | ssh | docker | skill | workflow' },
102
- delivery: { type: 'string', enum: ['current', 'isolated'], description: 'Run mode: current (in the current chat) or isolated (separate session)' },
103
- provider: { type: 'string', description: 'Model provider (optional)' },
104
- inspectOnFailure: { type: 'boolean', description: 'Diagnose failed runs with a model and store the diagnosis with the run (agent tasks)' },
105
- silentRule: { type: 'string', description: 'Plain-language condition for staying silent on a successful run (script/node/python/http tasks)' },
106
- fallbackModel: { type: 'string', description: 'Model tried once more when the primary model fails (llm/skill/workflow tasks)' },
107
- fallbackProvider: { type: 'string', description: 'Provider for the fallback model (defaults to the task provider)' },
108
- model: { type: 'string', description: 'Model (optional)' },
109
- timezone: { type: 'string', description: 'IANA time zone for the schedule, e.g. "Europe/Berlin" (optional, default = server local)' },
110
- notifyTelegram: { type: 'boolean', description: 'Send the run report to Telegram' },
111
- onlyOnFailure: { type: 'boolean', description: 'Send reports only for failures' },
112
- timeoutSeconds: { type: 'number', description: 'Execution time limit in seconds' },
113
- overlapPolicy: { type: 'string', enum: ['skip', 'queue', 'replace'], description: 'Overlap policy' },
114
- misfirePolicy: { type: 'string', enum: ['skip', 'runOnce', 'catchUpAll'], description: 'What to do with a run missed during downtime' },
115
- maxRetries: { type: 'number', description: 'Automatic retry attempts on failure (0 = off)' },
116
- retryBackoffMs: { type: 'number', description: 'Base backoff between retries in ms, doubled per attempt' },
117
- permissionPreset: { type: 'string', enum: ['default', 'read-only', 'workspace-write', 'full'], description: 'Permission preset applied to the task session' },
118
- kanbanMode: { type: 'string', enum: ['none', 'on_failure', 'always'], description: 'Create a dsh-kanban card for runs' },
119
- channels: { type: 'array', items: { type: 'string', enum: CHANNEL_IDS }, description: 'Notification channels for this task; empty = legacy telegram/kanban flags' },
120
- template: { type: 'string', description: 'Notification template with {title} {status} {output} {error} {duration} placeholders; empty = built-in text' },
121
- env: { type: 'object', additionalProperties: true, description: 'Environment variables for external runtimes (KEY: value); do not store secrets here' },
122
- cwd: { type: 'string', description: 'Working directory for the run (external runtimes and agent sessions)' },
123
- workspaceId: { type: 'string', description: 'Workspace bound to the task; resolved through the harness' },
124
- worktree: { type: 'boolean', description: 'Run code-modifying agent tasks in an isolated git worktree' },
125
- keepWorktree: { type: 'boolean', description: 'Keep the created worktree after the run' },
126
- nodePath: { type: 'string', description: 'Node.js binary for node tasks (default: the harness runtime)' },
127
- pythonPath: { type: 'string', description: 'Python interpreter for python tasks (default: auto-detected venv)' },
128
- httpMethod: { type: 'string', description: 'HTTP method for http tasks' },
129
- httpUrl: { type: 'string', description: 'URL for http tasks' },
130
- httpHeaders: { type: 'object', additionalProperties: true, description: 'Headers for http tasks' },
131
- httpBody: { type: 'string', description: 'Request body for http tasks' },
132
- sshProfileId: { type: 'string', description: 'dsh-remote-workspace profile id that provides host and credentials' },
133
- sshTarget: { type: 'string', description: 'Fallback user@host when no remote-workspace profile is used' },
134
- sshPort: { type: 'number', description: 'SSH port (with sshTarget fallback)' },
135
- sshKeyPath: { type: 'string', description: 'Private key path (with sshTarget fallback)' },
136
- dockerImage: { type: 'string', description: 'Container image for docker tasks' },
137
- skillName: { type: 'string', description: 'DSH skill name for skill tasks' },
138
- workflowName: { type: 'string', description: 'DSH workflow name for workflow tasks' },
139
- };
97
+ import {
98
+ cronToolParameters,
99
+ cronToolOutput,
100
+ executeCronTool,
101
+ rejectLegacyTool,
102
+ LEGACY_TOOL_NAMES,
103
+ LEGACY_ACTION_MAP,
104
+ } from './cron-tool.js';
140
105
 
141
- const createTaskOutput = {
142
- schema: {
143
- type: 'object',
144
- additionalProperties: true,
145
- properties: {
146
- success: { type: 'boolean' },
147
- message: { type: 'string' },
148
- task: { type: 'object', additionalProperties: true }
149
- }
150
- },
151
- render: (_args, val) => [{ type: 'text', text: val.message || JSON.stringify(val) }]
106
+ export {
107
+ executeCreateTask,
108
+ cronToolParameters,
109
+ cronToolOutput,
110
+ executeCronTool,
111
+ rejectLegacyTool,
112
+ LEGACY_TOOL_NAMES,
113
+ LEGACY_ACTION_MAP,
152
114
  };
115
+ const pkgUrl = new URL('../package.json', import.meta.url);
116
+ const pkgVersion = JSON.parse(fs.readFileSync(pkgUrl, 'utf8')).version || '0.2.29';
153
117
 
154
118
  export function apply(ctx, rawConfig) {
119
+ if (ctx && ctx.logger) setLogger(ctx.logger);
155
120
  const config = unwrapConfig(rawConfig);
156
121
  const store = new TaskStore();
157
122
 
@@ -210,9 +175,9 @@ export function apply(ctx, rawConfig) {
210
175
  try {
211
176
  const configJobs = Array.isArray(config && config.jobs) ? config.jobs : [];
212
177
  const summary = applyConfigSync({ store, scheduler, entries: configJobs });
213
- console.log('[dsh-cron] config jobs synced: ' + JSON.stringify(summary));
178
+ logger.info('[dsh-cron] config jobs synced: ' + JSON.stringify(summary));
214
179
  } catch (err) {
215
- console.error('[dsh-cron] config job sync failed:', err.message);
180
+ logger.error('[dsh-cron] config job sync failed:', err.message);
216
181
  }
217
182
 
218
183
  scheduler.start();
@@ -224,7 +189,7 @@ export function apply(ctx, rawConfig) {
224
189
  const intervalMs = Number(config.heartbeatIntervalSec) * 1000;
225
190
  heartbeatTimer = setInterval(() => {
226
191
  fetch(config.heartbeatUrl, { method: 'GET' }).catch((err) => {
227
- console.warn('[dsh-cron] heartbeat ping failed:', err.message);
192
+ logger.warn('[dsh-cron] heartbeat ping failed:', err.message);
228
193
  });
229
194
  }, intervalMs);
230
195
  }
@@ -262,7 +227,7 @@ export function apply(ctx, rawConfig) {
262
227
  handler: async (req, res) => {
263
228
  try {
264
229
  if (req.method === 'GET') {
265
- sendJson(res, 200, { ok: true, settings: store.getClientSettings() });
230
+ sendJson(res, 200, { ok: true, settings: { ...store.getClientSettings(), version: pkgVersion } });
266
231
  return;
267
232
  }
268
233
  if (req.method === 'POST') {
@@ -274,7 +239,7 @@ export function apply(ctx, rawConfig) {
274
239
  const scopeErrors = await applySettingsToScope(settingsService, payload);
275
240
  sendJson(res, 200, {
276
241
  ok: true,
277
- settings: store.getClientSettings(),
242
+ settings: { ...store.getClientSettings(), version: pkgVersion },
278
243
  scopeErrors: (scopeErrors && scopeErrors.length > 0) ? scopeErrors : undefined
279
244
  });
280
245
  return;
@@ -286,243 +251,12 @@ export function apply(ctx, rawConfig) {
286
251
  }
287
252
  }), 'dsh-cron: /settings');
288
253
 
289
- // 2. Tools
290
- ctx.tools.register(defineTool({
291
- name: 'cron_create_task',
292
- description: 'Create a new scheduled cron task (cron schedule, or one-shot via "at: <ISO timestamp>" or "in 20m")',
293
- parameters: createTaskParameters,
294
- output: createTaskOutput,
295
- execute: async (args) => executeCreateTask(store, scheduler, args)
296
- }));
297
-
298
- // Alias tool cron_schedule_task for compatibility with agent prompts (#70)
299
- ctx.tools.register(defineTool({
300
- name: 'cron_schedule_task',
301
- description: 'Alias for cron_create_task: create a new scheduled cron task',
302
- parameters: createTaskParameters,
303
- output: createTaskOutput,
304
- execute: async (args) => executeCreateTask(store, scheduler, args)
305
- }));
306
-
307
- ctx.tools.register(defineTool({
308
- name: 'cron_list_tasks',
309
- description: 'List all scheduled cron tasks',
310
- parameters: {
311
- status: { type: 'string', enum: ['all', 'active', 'paused', 'completed'], description: 'Filter by status' }
312
- },
313
- output: {
314
- schema: {
315
- type: 'object',
316
- additionalProperties: true,
317
- properties: {
318
- count: { type: 'number' },
319
- tasks: { type: 'array', items: { type: 'object', additionalProperties: true } }
320
- }
321
- },
322
- render: (_args, val) => [{ type: 'text', text: `Total tasks: ${val.count}` }]
323
- },
324
- execute: async (args) => {
325
- const tasks = store.list({ status: args.status || 'all' });
326
- return {
327
- count: tasks.length,
328
- tasks: tasks.map(t => ({
329
- id: t.id,
330
- title: t.title,
331
- schedule: t.scheduleText || t.schedule,
332
- status: t.status,
333
- nextRunAt: t.nextRunAt ? new Date(t.nextRunAt).toISOString() : null,
334
- lastStatus: t.lastStatus,
335
- totalTokens: t.totalTokens || 0,
336
- totalCostUsd: t.totalCostUsd || 0,
337
- oneShot: Boolean(t.oneShot),
338
- }))
339
- };
340
- }
341
- }));
342
-
343
- ctx.tools.register(defineTool({
344
- name: 'cron_pause_task',
345
- description: 'Pause a task by its ID',
346
- parameters: {
347
- id: { type: 'string', description: 'Task identifier', required: true }
348
- },
349
- output: {
350
- schema: {
351
- type: 'object',
352
- additionalProperties: true,
353
- properties: {
354
- success: { type: 'boolean' },
355
- message: { type: 'string' }
356
- }
357
- },
358
- render: (_args, val) => [{ type: 'text', text: val.message }]
359
- },
360
- execute: async (args) => {
361
- // A config-owned task cannot be paused from here either: the next start
362
- // would resume it, and the agent would have reported a change that never
363
- // happened (#50 review finding).
364
- if (isConfigOwned(store.get(args.id))) return { success: false, message: configOwnedMessage(args.id) };
365
- const task = scheduler.pauseTask(args.id);
366
- if (!task) return { success: false, message: 'Task not found' };
367
- return { success: true, message: `Task "${task.title}" paused` };
368
- }
369
- }));
370
-
371
- ctx.tools.register(defineTool({
372
- name: 'cron_resume_task',
373
- description: 'Resume a paused task',
374
- parameters: {
375
- id: { type: 'string', description: 'Task identifier', required: true }
376
- },
377
- output: {
378
- schema: {
379
- type: 'object',
380
- additionalProperties: true,
381
- properties: {
382
- success: { type: 'boolean' },
383
- message: { type: 'string' }
384
- }
385
- },
386
- render: (_args, val) => [{ type: 'text', text: val.message }]
387
- },
388
- execute: async (args) => {
389
- if (isConfigOwned(store.get(args.id))) return { success: false, message: configOwnedMessage(args.id) };
390
- const task = scheduler.resumeTask(args.id);
391
- if (!task) return { success: false, message: 'Task not found' };
392
- return { success: true, message: `Task "${task.title}" resumed (${task.scheduleText})` };
393
- }
394
- }));
395
-
254
+ // 2. Tools: unified background automation runner (Issue #196: 9 tools -> 1)
396
255
  ctx.tools.register(defineTool({
397
- name: 'cron_delete_task',
398
- description: 'Delete a task by its ID',
399
- parameters: {
400
- id: { type: 'string', description: 'Task identifier', required: true }
401
- },
402
- output: {
403
- schema: {
404
- type: 'object',
405
- additionalProperties: true,
406
- properties: {
407
- success: { type: 'boolean' },
408
- message: { type: 'string' }
409
- }
410
- },
411
- render: (_args, val) => [{ type: 'text', text: val.message }]
412
- },
413
- execute: async (args) => {
414
- // Deleting a config-owned task would also drop its run history, and the
415
- // task would come back at the next start without it.
416
- if (isConfigOwned(store.get(args.id))) return { success: false, message: configOwnedMessage(args.id) };
417
- scheduler.pauseTask(args.id);
418
- const ok = store.delete(args.id);
419
- return { success: ok, message: ok ? 'Task deleted' : 'Task not found' };
420
- }
421
- }));
422
-
423
- ctx.tools.register(defineTool({
424
- name: 'cron_get_task',
425
- description: 'Read the full configuration of one scheduled task, including fields that are not visible in the task list',
426
- parameters: {
427
- id: { type: 'string', description: 'Task identifier', required: true }
428
- },
429
- output: {
430
- schema: {
431
- type: 'object',
432
- additionalProperties: true,
433
- properties: {
434
- success: { type: 'boolean' },
435
- message: { type: 'string' },
436
- task: { type: 'object', additionalProperties: true }
437
- }
438
- },
439
- render: (_args, val) => [{ type: 'text', text: val.message }]
440
- },
441
- execute: async (args) => {
442
- const task = store.get(args.id);
443
- if (!task) return { success: false, message: 'Task not found' };
444
- const summary = [
445
- `Task "${task.title}" (${task.id})`,
446
- `type: ${task.type || 'llm'}`,
447
- `schedule: ${task.scheduleText || task.schedule}`,
448
- `status: ${task.status}`,
449
- task.type === 'llm' || !task.type ? `model: ${task.model || '(default)'}${task.fallbackModel ? ` (fallback: ${task.fallbackModel})` : ''}` : null,
450
- `channels: ${Array.isArray(task.channels) && task.channels.length ? task.channels.join(', ') : '(legacy flags)'}`,
451
- `prompt: ${String(task.prompt || '').slice(0, 500)}`,
452
- ].filter(Boolean).join(String.fromCharCode(10));
453
- return { success: true, message: summary, task };
454
- }
455
- }));
456
-
457
- ctx.tools.register(defineTool({
458
- name: 'cron_update_task',
459
- description: 'Change an existing scheduled task. Ask the user what to change first. Switching a task to a code-executing type (script/node/python/ssh/docker) is refused unless confirmCodeSwitch is set, which requires the user to have agreed explicitly. Only the documented task fields can be changed.',
460
- parameters: {
461
- id: { type: 'string', description: 'Task identifier', required: true },
462
- confirmCodeSwitch: { type: 'boolean', description: 'Set to true only after the user explicitly agreed to switch this task into a type that executes code' },
463
- title: { type: 'string', description: 'New title' },
464
- schedule: { type: 'string', description: 'New schedule (cron expression or interval)' },
465
- prompt: { type: 'string', description: 'New prompt or command' },
466
- type: { type: 'string', enum: TASK_TYPES, description: 'New execution type' },
467
- model: { type: 'string', description: 'New model for agent tasks' },
468
- fallbackModel: { type: 'string', description: 'New fallback model' },
469
- provider: { type: 'string', description: 'New provider' },
470
- channels: { type: 'array', items: { type: 'string', enum: CHANNEL_IDS }, description: 'New delivery channels' },
471
- template: { type: 'string', description: 'New message template' },
472
- onlyOnFailure: { type: 'boolean', description: 'Deliver only on failures' },
473
- timeoutSeconds: { type: 'number', description: 'New execution timeout in seconds' },
474
- overlapPolicy: { type: 'string', enum: ['skip', 'queue', 'replace'], description: 'New overlap policy' },
475
- maxRetries: { type: 'number', description: 'New retry count' },
476
- kanbanMode: { type: 'string', enum: ['none', 'on_failure', 'always'], description: 'New Kanban policy' },
477
- timezone: { type: 'string', description: 'New IANA time zone' },
478
- cwd: { type: 'string', description: 'New working directory' },
479
- env: { type: 'object', additionalProperties: true, description: 'New environment variables (KEY: value)' }
480
- },
481
- output: {
482
- schema: {
483
- type: 'object',
484
- additionalProperties: true,
485
- properties: {
486
- success: { type: 'boolean' },
487
- message: { type: 'string' }
488
- }
489
- },
490
- render: (_args, val) => [{ type: 'text', text: val.message }]
491
- },
492
- execute: async (args) => {
493
- const { id, confirmCodeSwitch, ...patch } = args || {};
494
- if (!id) return { success: false, message: 'A task id is required' };
495
- const result = applyTaskPatch({ store, scheduler, id, body: patch, allowCodeSwitch: confirmCodeSwitch === true });
496
- if (!result.ok) return { success: false, message: result.error };
497
- return {
498
- success: true,
499
- message: `Task "${result.task.title}" updated (${result.task.scheduleText || result.task.schedule}). Changed: ${describeTaskPatch(result.task, result.patch)}`,
500
- };
501
- }
502
- }));
503
-
504
- ctx.tools.register(defineTool({
505
- name: 'cron_run_task',
506
- description: 'Trigger a task immediately, out of band',
507
- parameters: {
508
- id: { type: 'string', description: 'Task identifier', required: true }
509
- },
510
- output: {
511
- schema: {
512
- type: 'object',
513
- additionalProperties: true,
514
- properties: {
515
- success: { type: 'boolean' },
516
- message: { type: 'string' }
517
- }
518
- },
519
- render: (_args, val) => [{ type: 'text', text: val.message }]
520
- },
521
- execute: async (args) => {
522
- const task = store.get(args.id);
523
- if (!task) return { success: false, message: 'Task not found' };
524
- scheduler.triggerManualRun(args.id);
525
- return { success: true, message: `Manual run started for task "${task.title}"` };
526
- }
256
+ name: 'cron',
257
+ description: 'Background automation runner for DSH: isolated agent runs, script/node/python/http/ssh/docker runtimes, cost guard, notifications, heartbeats. For simple reminders in the current chat, use the built-in schedule_create tool instead. Actions: create, list, get, update, pause, resume, run, delete.',
258
+ parameters: cronToolParameters,
259
+ output: cronToolOutput,
260
+ execute: async (args) => executeCronTool(store, scheduler, args),
527
261
  }));
528
262
  }
@@ -1,3 +1,4 @@
1
+ import { logger } from './logger.js';
1
2
  /**
2
3
  * Integration module for dsh-kanban and LLM token cost estimation.
3
4
  */
@@ -77,7 +78,7 @@ export async function createKanbanCard({
77
78
  const data = await res.json().catch(() => ({}));
78
79
  return { success: true, task: data.task };
79
80
  } catch (err) {
80
- console.error('[dsh-cron] failed to create kanban card:', err.message);
81
+ logger.error('[dsh-cron] failed to create kanban card:', err.message);
81
82
  return { success: false, error: err.message };
82
83
  }
83
84
  }
@@ -1,3 +1,4 @@
1
+ import { logger } from './logger.js';
1
2
  import { bestEffort } from './best-effort.js';
2
3
  import fs from 'node:fs';
3
4
 
@@ -103,7 +104,7 @@ export async function executeLlmActionDirectives({ directives, task, runInfo, sc
103
104
  results.push({ type, status: giteaRes ? 'created' : 'skipped', details: giteaRes });
104
105
  }
105
106
  } catch (err) {
106
- console.warn(`[dsh-cron] action directive ${type} execution failed:`, err.message);
107
+ logger.warn(`[dsh-cron] action directive ${type} execution failed:`, err.message);
107
108
  results.push({ type, status: 'error', error: err.message });
108
109
  }
109
110
  }
package/lib/logger.js ADDED
@@ -0,0 +1,38 @@
1
+ // Unified logging wrapper for dsh-cron (#204)
2
+ // Routes logs through ctx.logger when running inside DSH/Cordis,
3
+ // and safely falls back to console in tests or standalone runs.
4
+
5
+ let currentLogger = console;
6
+
7
+ export function setLogger(logger) {
8
+ if (logger && typeof logger === 'object') {
9
+ currentLogger = logger;
10
+ }
11
+ }
12
+
13
+ function getLogger() {
14
+ return currentLogger;
15
+ }
16
+
17
+ export const logger = {
18
+ info(...args) {
19
+ const fn = currentLogger.info || currentLogger.log || console.log;
20
+ return fn.call(currentLogger, ...args);
21
+ },
22
+ warn(...args) {
23
+ const fn = currentLogger.warn || currentLogger.log || console.warn;
24
+ return fn.call(currentLogger, ...args);
25
+ },
26
+ error(...args) {
27
+ const fn = currentLogger.error || currentLogger.log || console.error;
28
+ return fn.call(currentLogger, ...args);
29
+ },
30
+ debug(...args) {
31
+ const fn = currentLogger.debug || currentLogger.log || console.debug;
32
+ return fn.call(currentLogger, ...args);
33
+ },
34
+ log(...args) {
35
+ const fn = currentLogger.info || currentLogger.log || console.log;
36
+ return fn.call(currentLogger, ...args);
37
+ },
38
+ };
@@ -1,4 +1,8 @@
1
1
  export async function getModelsHandler(ctx, req, res, sendJson) {
2
+ if ((req.method || 'GET') !== 'GET') {
3
+ sendJson(res, 405, { ok: false, error: 'Method not allowed' });
4
+ return;
5
+ }
2
6
  const current = ctx.get('agentDefaultModel')?.currentSelection?.() ?? null;
3
7
  let providers = [];
4
8
  try {
package/lib/prompt.js CHANGED
@@ -12,9 +12,11 @@ The user's original request:
12
12
  "${text}"
13
13
 
14
14
  CRITICAL RULES FOR THE AGENT:
15
- 1. NEVER RUSH:
16
- It is STRICTLY FORBIDDEN to call the \`cron_create_task\` (or \`cron_schedule_task\`) tool in your first reply!
17
- You MUST NOT create the task until you have asked clarifying questions and received the user's confirmation.
15
+ 1. EVALUATE SIMPLE IN-CHAT REMINDER VS BACKGROUND AUTOMATION:
16
+ - If the user simply wants a reminder or alert delivered into the current conversation (e.g. "remind me in 2 hours", "say good morning at 9am here"), suggest using the core built-in \`schedule_create\` tool instead.
17
+ - Use the \`cron\` tool for background automation running in separate isolated sessions, shell/python/node/http/ssh/docker runtimes, multi-channel alerts (Telegram, Discord, Slack, etc.), or monitoring.
18
+ - NEVER RUSH: It is STRICTLY FORBIDDEN to call the \`cron\` tool in your first reply!
19
+ You MUST NOT create the task until you have asked clarifying questions and received the user's confirmation.
18
20
 
19
21
  2. FIRST QUESTION: LLM vs NO-LLM Shell (critical):
20
22
  - Evaluate the nature of the task:
@@ -46,7 +48,8 @@ CRITICAL RULES FOR THE AGENT:
46
48
  - Ask any remaining questions and ask the user to confirm task creation.
47
49
 
48
50
  5. CREATING THE TASK:
49
- Only AFTER the user replies and confirms the parameters, call the \`cron_create_task\` tool with:
51
+ Only AFTER the user replies and confirms the parameters, call the \`cron\` tool with:
52
+ - action: "create"
50
53
  - title: a clear task name
51
54
  - schedule: the agreed cron expression
52
55
  - prompt: the final shell command/script path (for type="script") OR detailed instructions including the Silent Rule (for type="llm")
package/lib/routes.js CHANGED
@@ -89,7 +89,7 @@ export function registerRoutes(ctx, { store, scheduler, getCronSettingsScope, re
89
89
  path: '/dsh-cron/recipes',
90
90
  handler: async (req, res) => {
91
91
  try {
92
- if (req.method !== 'GET') {
92
+ if ((req.method || 'GET') !== 'GET') {
93
93
  sendJson(res, 405, { ok: false, error: 'Method not allowed' });
94
94
  return;
95
95
  }
@@ -108,6 +108,10 @@ export function registerRoutes(ctx, { store, scheduler, getCronSettingsScope, re
108
108
  path: '/dsh-cron/heartbeat',
109
109
  handler: async (req, res) => {
110
110
  try {
111
+ if ((req.method || 'GET') !== 'GET') {
112
+ sendJson(res, 405, { ok: false, error: 'Method not allowed' });
113
+ return;
114
+ }
111
115
  const tasks = store.list({ status: 'active' });
112
116
  const lastRuns = tasks.map((t) => t.lastRunAt || 0);
113
117
  sendJson(res, 200, {
@@ -1,3 +1,4 @@
1
+ import { logger } from './logger.js';
1
2
  import { bestEffort } from './best-effort.js';
2
3
  import {
3
4
  buildInvocation,
@@ -74,7 +75,7 @@ export async function runHttp(task, request, signal) {
74
75
  if (!res.ok) {
75
76
  const err = new Error(`${summary}\n${String(text).slice(0, 500)}`);
76
77
  if (isTransientError(res.status) && attempts < maxAttempts && !signal?.aborted) {
77
- console.warn(`[dsh-cron] HTTP request returned ${res.status}, retrying attempt ${attempts + 1}/${maxAttempts}...`);
78
+ logger.warn(`[dsh-cron] HTTP request returned ${res.status}, retrying attempt ${attempts + 1}/${maxAttempts}...`);
78
79
  await new Promise((r) => setTimeout(r, attempts * 1000));
79
80
  continue;
80
81
  }
@@ -87,7 +88,7 @@ export async function runHttp(task, request, signal) {
87
88
  };
88
89
  } catch (err) {
89
90
  if (isTransientError(err) && attempts < maxAttempts && !signal?.aborted) {
90
- console.warn(`[dsh-cron] HTTP request failed (${err.message}), retrying attempt ${attempts + 1}/${maxAttempts}...`);
91
+ logger.warn(`[dsh-cron] HTTP request failed (${err.message}), retrying attempt ${attempts + 1}/${maxAttempts}...`);
91
92
  await new Promise((r) => setTimeout(r, attempts * 1000));
92
93
  continue;
93
94
  }
@@ -1,3 +1,4 @@
1
+ import { logger } from './logger.js';
1
2
  import { bestEffort } from './best-effort.js';
2
3
 
3
4
  /**
@@ -46,7 +47,7 @@ export async function removeTaskWorktree(cwd, wtPath) {
46
47
  await new Promise((resolve) => {
47
48
  execFile('git', ['-C', cwd, 'worktree', 'remove', wtPath], { maxBuffer: 4 * 1024 * 1024 }, (err, stdout, stderr) => {
48
49
  if (err) {
49
- console.warn(`[dsh-cron] task worktree kept (not removable without force): ${wtPath} — ${String(stderr || err.message).trim()}`);
50
+ logger.warn(`[dsh-cron] task worktree kept (not removable without force): ${wtPath} — ${String(stderr || err.message).trim()}`);
50
51
  }
51
52
  execFile('git', ['-C', cwd, 'worktree', 'prune'], () => resolve());
52
53
  });
package/lib/runner.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { logger } from './logger.js';
1
2
  import { bestEffort } from './best-effort.js';
2
3
  import { estimateTokenCost } from './integrations.js';
3
4
  import {
@@ -141,7 +142,7 @@ export class SessionRunner {
141
142
  }
142
143
 
143
144
  async _run(task, signal, options = {}) {
144
- console.log(`[dsh-cron] executing task "${task.title}" (${task.id})`);
145
+ logger.info(`[dsh-cron] executing task "${task.title}" (${task.id})`);
145
146
 
146
147
  // 1. Non-LLM runtimes: shell, Node.js, Python, HTTP, SSH, Docker (#4 #5 #7 #8 #9)
147
148
  const runtime = normalizeTaskType(task.type);
@@ -177,7 +178,7 @@ export class SessionRunner {
177
178
  return { output: out, usage, costUsd: estimateTokenCost(task.model, usage) };
178
179
  }
179
180
  } catch (err) {
180
- console.error(`[dsh-cron] session execution failed:`, err.message);
181
+ logger.error(`[dsh-cron] session execution failed:`, err.message);
181
182
  throw err;
182
183
  }
183
184
  return this._runBare(task);
@@ -207,7 +208,7 @@ export class SessionRunner {
207
208
  } catch (err) {
208
209
  // Context window overflow auto-recovery in persistent sessions (#145)
209
210
  if (prep.targetSessionId && isContextOverflowError(err) && !signal?.aborted) {
210
- console.warn(`[dsh-cron] Persistent session "${prep.targetSessionId}" exceeded context window (${err.message}). Auto-recovering with fresh session...`);
211
+ logger.warn(`[dsh-cron] Persistent session "${prep.targetSessionId}" exceeded context window (${err.message}). Auto-recovering with fresh session...`);
211
212
  bestEffort('archive-rotated-session', () => {
212
213
  const sessions = this.ctx?.get?.('sessions');
213
214
  if (sessions && typeof sessions.archive === 'function') {
@@ -215,7 +216,7 @@ export class SessionRunner {
215
216
  }
216
217
  });
217
218
  const rotatedSessionId = `${prep.targetSessionId}-${Date.now().toString(36)}`;
218
- console.log(`[dsh-cron] Rotated persistent session "${prep.targetSessionId}" -> "${rotatedSessionId}"`);
219
+ logger.info(`[dsh-cron] Rotated persistent session "${prep.targetSessionId}" -> "${rotatedSessionId}"`);
219
220
  if (this.store) {
220
221
  bestEffort('update-stored-rotated-session', () => {
221
222
  const stored = this.store.get(task.id);
@@ -230,7 +231,7 @@ export class SessionRunner {
230
231
  const freshPrep = await this._prepareAgentRun(effectiveTask, runtime, signal);
231
232
  return await this._executeAgentTurn(task, freshPrep);
232
233
  }
233
- console.error(`[dsh-cron] agent execution failed:`, err.message);
234
+ logger.error(`[dsh-cron] agent execution failed:`, err.message);
234
235
  throw err;
235
236
  }
236
237
  }
@@ -306,7 +307,7 @@ export class SessionRunner {
306
307
  taskWorktree = await this.createTaskWorktree(baseCwd, task);
307
308
  cwd = taskWorktree;
308
309
  } catch (wtErr) {
309
- console.warn('[dsh-cron] task worktree not created, running in the workspace:', wtErr.message);
310
+ logger.warn('[dsh-cron] task worktree not created, running in the workspace:', wtErr.message);
310
311
  }
311
312
  }
312
313
  return { baseCwd, cwd, taskWorktree };
@@ -349,7 +350,7 @@ export class SessionRunner {
349
350
  await presets.standingKeyFor(preset.id);
350
351
  }
351
352
  } catch (presetErr) {
352
- console.warn('[dsh-cron] agent preset unavailable, running without one:', presetErr && presetErr.message);
353
+ logger.warn('[dsh-cron] agent preset unavailable, running without one:', presetErr && presetErr.message);
353
354
  preset = null;
354
355
  }
355
356
  }
@@ -375,9 +376,9 @@ export class SessionRunner {
375
376
  ...presetSetup,
376
377
  });
377
378
  isResumed = true;
378
- console.log(`[dsh-cron] resumed existing persistent session: ${prep.targetSessionId}`);
379
+ logger.info(`[dsh-cron] resumed existing persistent session: ${prep.targetSessionId}`);
379
380
  } catch (resumeErr) {
380
- console.log(`[dsh-cron] could not resume session ${prep.targetSessionId} (${resumeErr.message}), creating new persistent session`);
381
+ logger.info(`[dsh-cron] could not resume session ${prep.targetSessionId} (${resumeErr.message}), creating new persistent session`);
381
382
  handle = null;
382
383
  }
383
384
  }
@@ -431,7 +432,7 @@ export class SessionRunner {
431
432
  presets.set(handle.agent.session, task.permissionPreset);
432
433
  }
433
434
  } catch (ppErr) {
434
- console.warn('[dsh-cron] permission preset not applied:', ppErr.message);
435
+ logger.warn('[dsh-cron] permission preset not applied:', ppErr.message);
435
436
  }
436
437
  }
437
438
 
@@ -462,7 +463,7 @@ export class SessionRunner {
462
463
  await handle.agent.destroy();
463
464
  }
464
465
  } catch (disposeErr) {
465
- console.warn('[dsh-cron] session dispose notice:', disposeErr.message);
466
+ logger.warn('[dsh-cron] session dispose notice:', disposeErr.message);
466
467
  }
467
468
  // Best-effort: keep ephemeral cron sessions out of the main chat list (#30).
468
469
  // Persistent sessions (targetSessionId configured) stay unarchived so users can interact with them (#143).