@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.
@@ -1,3 +1,4 @@
1
+ import { logger } from './logger.js';
1
2
  import { checkAndApplyBurnGuard, checkTaskBudgetLimits } from './burn-guard.js';
2
3
  import { resolveTaskForExecution } from './prompt-interpolation.js';
3
4
  import { bestEffort } from './best-effort.js';
@@ -126,7 +127,7 @@ export async function executeWithFallback(scheduler, task, signal, options = {})
126
127
  provider: task.fallbackProvider || task.provider,
127
128
  model: task.fallbackModel,
128
129
  };
129
- console.log(`[dsh-cron] task "${task.title}" failed on ${task.model || 'the default model'}, retrying once on ${task.fallbackModel}`);
130
+ logger.info(`[dsh-cron] task "${task.title}" failed on ${task.model || 'the default model'}, retrying once on ${task.fallbackModel}`);
130
131
  const secondary = await scheduler.executeOnce(fallbackTask, signal, options);
131
132
  return {
132
133
  ...secondary,
@@ -143,12 +144,12 @@ export async function executeWithFallback(scheduler, task, signal, options = {})
143
144
  export function beginRun(scheduler, task, taskId) {
144
145
  if (!scheduler.running.has(taskId) && scheduler.maxConcurrent > 0 && scheduler.running.size >= scheduler.maxConcurrent) {
145
146
  if (task.overlapPolicy === 'queue') {
146
- console.log(`[dsh-cron] Task "${task.title}" (${taskId}) queued by concurrency limit (${scheduler.maxConcurrent})`);
147
+ logger.info(`[dsh-cron] Task "${task.title}" (${taskId}) queued by concurrency limit (${scheduler.maxConcurrent})`);
147
148
  scheduler.queue.push({ taskId, priority: task.priority !== undefined ? task.priority : 5, queuedAt: Date.now() });
148
149
  scheduler.queue.sort((a, b) => (a.priority - b.priority) || (a.queuedAt - b.queuedAt));
149
150
  return null;
150
151
  }
151
- console.log(`[dsh-cron] Task "${task.title}" (${taskId}) skipped: concurrency limit (${scheduler.maxConcurrent})`);
152
+ logger.info(`[dsh-cron] Task "${task.title}" (${taskId}) skipped: concurrency limit (${scheduler.maxConcurrent})`);
152
153
  scheduler.recordSkipped(taskId, `Skipped: concurrency limit reached (${scheduler.maxConcurrent} parallel runs)`);
153
154
  return null;
154
155
  }
@@ -157,16 +158,16 @@ export function beginRun(scheduler, task, taskId) {
157
158
  const active = scheduler.running.get(taskId);
158
159
  if (active) {
159
160
  if (overlapPolicy === 'skip') {
160
- console.log(`[dsh-cron] Task "${task.title}" (${taskId}) is already running, skipping run (overlapPolicy: skip)`);
161
+ logger.info(`[dsh-cron] Task "${task.title}" (${taskId}) is already running, skipping run (overlapPolicy: skip)`);
161
162
  scheduler.recordSkipped(taskId, 'Skipped: the previous run is still in progress (overlapPolicy: skip)');
162
163
  return null;
163
164
  }
164
165
  if (overlapPolicy === 'replace') {
165
- console.log(`[dsh-cron] Task "${task.title}" (${taskId}) is already running, canceling current run (overlapPolicy: replace)`);
166
+ logger.info(`[dsh-cron] Task "${task.title}" (${taskId}) is already running, canceling current run (overlapPolicy: replace)`);
166
167
  active.controller.abort(new Error('Aborted by overlap policy: replace'));
167
168
  }
168
169
  if (overlapPolicy === 'queue') {
169
- console.log(`[dsh-cron] Task "${task.title}" (${taskId}) is already running, queueing next run (overlapPolicy: queue)`);
170
+ logger.info(`[dsh-cron] Task "${task.title}" (${taskId}) is already running, queueing next run (overlapPolicy: queue)`);
170
171
  active.queueCount = (active.queueCount || 0) + 1;
171
172
  return null;
172
173
  }
@@ -189,7 +190,7 @@ export async function executeTask(scheduler, taskId, options = {}) {
189
190
  const runs = (scheduler.store.history && scheduler.store.history.get(taskId)) || [];
190
191
  const guard = checkTaskBudgetLimits(task, runs);
191
192
  if (guard.exceeded) {
192
- console.warn(`[dsh-cron] Burn Guard pre-flight blocked task "${task.title}" (${taskId}): ${guard.reason}`);
193
+ logger.warn(`[dsh-cron] Burn Guard pre-flight blocked task "${task.title}" (${taskId}): ${guard.reason}`);
193
194
  scheduler.pauseTask(taskId, guard.reason);
194
195
  scheduler.recordSkipped(taskId, `Burn Guard pre-flight pause: ${guard.reason}`);
195
196
  checkAndApplyBurnGuard(scheduler, taskId, { status: 'error', costUsd: 0, durationMs: 0 }).catch(() => {});
@@ -222,7 +223,7 @@ export async function executeTask(scheduler, taskId, options = {}) {
222
223
 
223
224
  const preflight = await executePreflight(task);
224
225
  if (!preflight.ok) {
225
- console.log(`[dsh-cron] task "${task.title}" (${taskId}) skipped: ${preflight.reason}`);
226
+ logger.info(`[dsh-cron] task "${task.title}" (${taskId}) skipped: ${preflight.reason}`);
226
227
  scheduler.running.delete(taskId);
227
228
  scheduler.recordSkipped(taskId, `Preflight check skipped: ${preflight.reason}`);
228
229
  return null;
@@ -282,18 +283,18 @@ export async function applyModelAssists(task, runInfo, options) {
282
283
  runInfo.diagnosis = inspection.diagnosis;
283
284
  runInfo.suggestion = inspection.suggestion;
284
285
  runInfo.confidence = inspection.confidence;
285
- console.log(`[dsh-cron] task "${task.title}" failure diagnosed (${inspection.confidence}): ${inspection.diagnosis}`);
286
+ logger.info(`[dsh-cron] task "${task.title}" failure diagnosed (${inspection.confidence}): ${inspection.diagnosis}`);
286
287
  } else if (inspection.error && task.inspectOnFailure) {
287
- console.warn(`[dsh-cron] failure inspection for "${task.title}" not applied: ${inspection.error}`);
288
+ logger.warn(`[dsh-cron] failure inspection for "${task.title}" not applied: ${inspection.error}`);
288
289
  }
289
290
 
290
291
  const silent = await applySilentRuleToRun(task, runInfo, options);
291
292
  if (silent.skipped) {
292
293
  runInfo.silentSkip = true;
293
294
  runInfo.silentReason = silent.reason;
294
- console.log(`[dsh-cron] task "${task.title}" stayed silent by rule: ${silent.reason || 'no reason given'}`);
295
+ logger.info(`[dsh-cron] task "${task.title}" stayed silent by rule: ${silent.reason || 'no reason given'}`);
295
296
  } else if (silent.verdictError) {
296
- console.warn(`[dsh-cron] silent rule for "${task.title}" not applied: ${silent.verdictError}`);
297
+ logger.warn(`[dsh-cron] silent rule for "${task.title}" not applied: ${silent.verdictError}`);
297
298
  }
298
299
  return silent;
299
300
  }
@@ -308,19 +309,19 @@ export async function deliverNotifications(task, runInfo, { store, resolveSecret
308
309
  try {
309
310
  settings = { ...settings, ...(await resolveSecrets(settings)) };
310
311
  } catch (secErr) {
311
- console.warn('[dsh-cron] credential resolution failed:', secErr.message);
312
+ logger.warn('[dsh-cron] credential resolution failed:', secErr.message);
312
313
  }
313
314
  }
314
315
  if (typeof deliver !== 'function') return;
315
316
  const result = await deliver(task, runInfo, settings);
316
317
  if (result && Array.isArray(result.delivered) && result.delivered.length) {
317
- console.log(`[dsh-cron] delivered "${task.title}" to: ${result.delivered.map((d) => d.channel).join(', ')}`);
318
+ logger.info(`[dsh-cron] delivered "${task.title}" to: ${result.delivered.map((d) => d.channel).join(', ')}`);
318
319
  }
319
320
  if (result && Array.isArray(result.failures) && result.failures.length) {
320
- console.warn(`[dsh-cron] delivery failures for "${task.title}": ${result.failures.map((f) => `${f.channel}: ${f.error}`).join('; ')}`);
321
+ logger.warn(`[dsh-cron] delivery failures for "${task.title}": ${result.failures.map((f) => `${f.channel}: ${f.error}`).join('; ')}`);
321
322
  }
322
323
  } catch (notifyErr) {
323
- console.error('[dsh-cron] notification error:', notifyErr.message);
324
+ logger.error('[dsh-cron] notification error:', notifyErr.message);
324
325
  }
325
326
  }
326
327
 
@@ -343,7 +344,7 @@ export function finishRun(scheduler, task, taskId, status, queuedCount) {
343
344
  }
344
345
  }
345
346
  } catch (err) {
346
- console.error('[dsh-cron] failed to persist the next run of task', taskId + ':', err.message);
347
+ logger.error('[dsh-cron] failed to persist the next run of task', taskId + ':', err.message);
347
348
  }
348
349
 
349
350
  try {
@@ -359,7 +360,7 @@ export function finishRun(scheduler, task, taskId, status, queuedCount) {
359
360
  scheduler.retryTimers.delete(taskId);
360
361
  scheduler.runTask(taskId, { isRetry: true });
361
362
  }, backoffMs));
362
- console.log(`[dsh-cron] retry ${attempt}/${maxRetries} for task "${task.title}" in ${backoffMs}ms`);
363
+ logger.info(`[dsh-cron] retry ${attempt}/${maxRetries} for task "${task.title}" in ${backoffMs}ms`);
363
364
  } else if (!failed && task.attempts) {
364
365
  task.attempts = 0;
365
366
  scheduler.store.set(task);
@@ -368,7 +369,7 @@ export function finishRun(scheduler, task, taskId, status, queuedCount) {
368
369
  scheduler.store.set(task);
369
370
  }
370
371
  } catch (err) {
371
- console.error('[dsh-cron] retry bookkeeping failed for task', taskId + ':', err.message);
372
+ logger.error('[dsh-cron] retry bookkeeping failed for task', taskId + ':', err.message);
372
373
  }
373
374
 
374
375
  if (queuedCount > 0) {
@@ -398,7 +399,7 @@ export async function handleCompleteRun(scheduler, task, taskId, currentRun, out
398
399
  };
399
400
 
400
401
  if (isFailure && task.selfHealingCommand) {
401
- console.log(`[dsh-cron] running self-healing command for task "${task.title}": ${task.selfHealingCommand}`);
402
+ logger.info(`[dsh-cron] running self-healing command for task "${task.title}": ${task.selfHealingCommand}`);
402
403
  try {
403
404
  await new Promise((resolve) => {
404
405
  exec(task.selfHealingCommand, { timeout: 30000 }, (shErr, stdout, stderr) => {
@@ -435,7 +436,7 @@ export async function handleCompleteRun(scheduler, task, taskId, currentRun, out
435
436
  try {
436
437
  scheduler.store.recordRun(taskId, runInfo);
437
438
  } catch (recordErr) {
438
- console.error(`[dsh-cron] failed to record the run of task ${taskId}:`, recordErr.message);
439
+ logger.error(`[dsh-cron] failed to record the run of task ${taskId}:`, recordErr.message);
439
440
  }
440
441
 
441
442
  const finishedRun = scheduler.running.get(taskId);
@@ -480,7 +481,7 @@ export async function handleCompleteRun(scheduler, task, taskId, currentRun, out
480
481
  }
481
482
  }
482
483
  } catch (actErr) {
483
- console.warn(`[dsh-cron] structured action error for ${taskId}:`, actErr.message);
484
+ logger.warn(`[dsh-cron] structured action error for ${taskId}:`, actErr.message);
484
485
  }
485
486
  }
486
487
 
@@ -492,7 +493,7 @@ export async function handleCompleteRun(scheduler, task, taskId, currentRun, out
492
493
  const targetId = nextTaskId.trim();
493
494
  const nextTask = scheduler.store.get(targetId);
494
495
  if (nextTask) {
495
- console.log(`[dsh-cron] triggering chained task "${nextTask.title}" (${targetId}) from "${task.title}" (depth ${currentDepth + 1})`);
496
+ logger.info(`[dsh-cron] triggering chained task "${nextTask.title}" (${targetId}) from "${task.title}" (depth ${currentDepth + 1})`);
496
497
  scheduler.runNow(targetId, {
497
498
  chainDepth: currentDepth + 1,
498
499
  prevOutput: outcome.output || outcome.error || '',
@@ -501,13 +502,13 @@ export async function handleCompleteRun(scheduler, task, taskId, currentRun, out
501
502
  prevDurationMs: Date.now() - start,
502
503
  prevCostUsd: outcome.costUsd || 0,
503
504
  }).catch((err) => {
504
- console.warn(`[dsh-cron] chained task ${targetId} failed to trigger:`, err.message);
505
+ logger.warn(`[dsh-cron] chained task ${targetId} failed to trigger:`, err.message);
505
506
  });
506
507
  } else {
507
- console.warn(`[dsh-cron] chained task target not found: ${targetId}`);
508
+ logger.warn(`[dsh-cron] chained task target not found: ${targetId}`);
508
509
  }
509
510
  } else {
510
- console.warn(`[dsh-cron] chained task depth limit (5) reached for task ${task.id} -> ${nextTaskId}. Loop prevented.`);
511
+ logger.warn(`[dsh-cron] chained task depth limit (5) reached for task ${task.id} -> ${nextTaskId}. Loop prevented.`);
511
512
  }
512
513
  }
513
514
  }
@@ -1,3 +1,4 @@
1
+ import { logger } from './logger.js';
1
2
  import { bestEffort } from './best-effort.js';
2
3
 
3
4
  /**
@@ -41,7 +42,7 @@ export async function checkHeartbeats(scheduler) {
41
42
  task.updatedAt = now;
42
43
  scheduler.store.save();
43
44
 
44
- console.warn(`[dsh-cron] Heartbeat missed for "${task.title}" (${task.id})!`);
45
+ logger.warn(`[dsh-cron] Heartbeat missed for "${task.title}" (${task.id})!`);
45
46
  const runInfo = {
46
47
  at: now,
47
48
  status: 'missed',
@@ -62,7 +63,7 @@ export async function checkHeartbeats(scheduler) {
62
63
  chainDepth: 1,
63
64
  prevOutput: runInfo.output,
64
65
  prevTaskId: task.id,
65
- }).catch((err) => console.warn('[dsh-cron] heartbeat onFailure trigger error:', err.message));
66
+ }).catch((err) => logger.warn('[dsh-cron] heartbeat onFailure trigger error:', err.message));
66
67
  }
67
68
  }
68
69
  }
package/lib/scheduler.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { logger } from './logger.js';
1
2
  import { bestEffort } from './best-effort.js';
2
3
  import { Cron } from 'croner';
3
4
  import {
@@ -297,7 +298,7 @@ export class TaskScheduler {
297
298
  if (missed) {
298
299
  const missedAt = task.nextRunAt;
299
300
  const policy = task.misfirePolicy || 'skip';
300
- console.warn(`[dsh-cron] Task "${task.title}" (${task.id}) missed scheduled run at ${new Date(missedAt).toISOString()} (misfire: ${policy})`);
301
+ logger.warn(`[dsh-cron] Task "${task.title}" (${task.id}) missed scheduled run at ${new Date(missedAt).toISOString()} (misfire: ${policy})`);
301
302
  this.countRun('missed');
302
303
  this.store.recordRun(task.id, {
303
304
  at: missedAt,
@@ -369,7 +370,7 @@ export class TaskScheduler {
369
370
  const timer = setTimeout(() => {
370
371
  this.timers.delete(task.id);
371
372
  this.runTask(task.id).catch((err) => {
372
- console.error(`[dsh-cron] one-shot run of task ${task.id} failed:`, (err && err.message) || err);
373
+ logger.error(`[dsh-cron] one-shot run of task ${task.id} failed:`, (err && err.message) || err);
373
374
  });
374
375
  }, delay);
375
376
  this.timers.set(task.id, timer);
@@ -380,7 +381,7 @@ export class TaskScheduler {
380
381
  const job = new Cron(parsed.cronPattern, {
381
382
  protect: true,
382
383
  sloppyRanges: true,
383
- catch: (err) => console.error(`[dsh-cron] scheduled run of "${task.title}" (${task.id}) failed:`, (err && err.message) || err),
384
+ catch: (err) => logger.error(`[dsh-cron] scheduled run of "${task.title}" (${task.id}) failed:`, (err && err.message) || err),
384
385
  ...(timezone ? { timezone } : {}),
385
386
  }, async () => {
386
387
  await this.runTask(task.id);
@@ -407,7 +408,7 @@ export class TaskScheduler {
407
408
  }
408
409
  this.scheduleCron(task, parsed);
409
410
  } catch (err) {
410
- console.error(`[dsh-cron] failed to schedule task ${task.id}:`, err.message);
411
+ logger.error(`[dsh-cron] failed to schedule task ${task.id}:`, err.message);
411
412
  }
412
413
  }
413
414
 
package/lib/settings.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { logger } from './logger.js';
1
2
  import z from '@deepseek-ai/schemastery';
2
3
  import { credentialRefStatus } from './secrets.js';
3
4
  import { TaskStore } from './store.js';
@@ -147,14 +148,14 @@ export function sanitizeSettingsPayload(body) {
147
148
  if (key in payload && typeof payload[key] === 'string' && payload[key].trim()) {
148
149
  const refCheck = credentialRefStatus(payload[key]);
149
150
  if (!refCheck.ok) {
150
- console.warn(`[dsh-cron] invalid credential reference in "${key}": ${refCheck.error}`);
151
+ logger.warn(`[dsh-cron] invalid credential reference in "${key}": ${refCheck.error}`);
151
152
  delete payload[key];
152
153
  }
153
154
  }
154
155
  }
155
156
  for (const key of TaskStore.FORBIDDEN_SETTING_KEYS) {
156
157
  if (key in payload) {
157
- console.warn(`[dsh-cron] refusing raw secret "${key}" from the client — use a credential reference`);
158
+ logger.warn(`[dsh-cron] refusing raw secret "${key}" from the client — use a credential reference`);
158
159
  delete payload[key];
159
160
  }
160
161
  }
package/lib/store.js CHANGED
@@ -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
  import path from 'node:path';
@@ -74,21 +75,21 @@ export class TaskStore {
74
75
  bestEffort('backup-initial', () => fs.copyFileSync(this.filePath, bakPath));
75
76
  }
76
77
  } catch (err) {
77
- console.error('[dsh-cron] store read error (corrupted file):', err.message);
78
+ logger.error('[dsh-cron] store read error (corrupted file):', err.message);
78
79
  try {
79
80
  const corruptedPath = `${this.filePath}.corrupted.${Date.now()}`;
80
81
  fs.copyFileSync(this.filePath, corruptedPath);
81
- console.warn(`[dsh-cron] corrupted store backed up to ${corruptedPath}`);
82
+ logger.warn(`[dsh-cron] corrupted store backed up to ${corruptedPath}`);
82
83
  } catch (snapErr) {
83
- console.error('[dsh-cron] failed to snapshot corrupted store:', snapErr.message);
84
+ logger.error('[dsh-cron] failed to snapshot corrupted store:', snapErr.message);
84
85
  }
85
86
  if (fs.existsSync(bakPath)) {
86
87
  try {
87
88
  const rawBak = fs.readFileSync(bakPath, 'utf-8');
88
89
  loadData(rawBak);
89
- console.warn(`[dsh-cron] successfully recovered tasks store from ${bakPath}`);
90
+ logger.warn(`[dsh-cron] successfully recovered tasks store from ${bakPath}`);
90
91
  } catch (bakErr) {
91
- console.error('[dsh-cron] failed to recover tasks store from .bak:', bakErr.message);
92
+ logger.error('[dsh-cron] failed to recover tasks store from .bak:', bakErr.message);
92
93
  }
93
94
  }
94
95
  }
@@ -97,9 +98,9 @@ export class TaskStore {
97
98
  const rawBak = fs.readFileSync(bakPath, 'utf-8');
98
99
  loadData(rawBak);
99
100
  bestEffort('restore-from-bak', () => fs.copyFileSync(bakPath, this.filePath));
100
- console.warn(`[dsh-cron] restored missing tasks store from ${bakPath}`);
101
+ logger.warn(`[dsh-cron] restored missing tasks store from ${bakPath}`);
101
102
  } catch (bakErr) {
102
- console.error('[dsh-cron] failed to restore tasks store from .bak:', bakErr.message);
103
+ logger.error('[dsh-cron] failed to restore tasks store from .bak:', bakErr.message);
103
104
  }
104
105
  }
105
106
  }
@@ -158,7 +159,7 @@ export class TaskStore {
158
159
  bestEffort('backup-on-save', () => fs.copyFileSync(this.filePath, `${this.filePath}.bak`));
159
160
  }
160
161
  } catch (err) {
161
- console.error('[dsh-cron] store save error:', err.message);
162
+ logger.error('[dsh-cron] store save error:', err.message);
162
163
  }
163
164
  }
164
165
 
@@ -222,7 +223,7 @@ export class TaskStore {
222
223
 
223
224
  for (const key of TaskStore.FORBIDDEN_SETTING_KEYS) {
224
225
  if (key in patch) {
225
- console.warn(`[dsh-cron] refusing to store raw secret "${key}" in settings — use a credential reference instead`);
226
+ logger.warn(`[dsh-cron] refusing to store raw secret "${key}" in settings — use a credential reference instead`);
226
227
  delete patch[key];
227
228
  }
228
229
  }
@@ -441,7 +442,7 @@ export class TaskStore {
441
442
  fs.writeFileSync(tmp, JSON.stringify(archive), 'utf-8');
442
443
  fs.renameSync(tmp, archivePath);
443
444
  } catch (archiveErr) {
444
- console.error('[dsh-cron] history archive error:', archiveErr.message);
445
+ logger.error('[dsh-cron] history archive error:', archiveErr.message);
445
446
  }
446
447
  }
447
448
 
package/lib/task-patch.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Partial task updates, shared by the HTTP PATCH route and the cron_update_task
2
+ * Partial task updates, shared by the HTTP PATCH route and the cron tool (action: 'update')
3
3
  * tool (#49).
4
4
  *
5
5
  * One implementation keeps the whitelist, the type validation, the schedule
package/lib/telegram.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { logger } from './logger.js';
1
2
  import fs from 'node:fs';
2
3
  import path from 'node:path';
3
4
  import os from 'node:os';
@@ -63,7 +64,7 @@ export function getDshDefaultTelegramCredentials() {
63
64
  try {
64
65
  raw = fs.readFileSync(p, 'utf-8');
65
66
  } catch (readErr) {
66
- console.warn('[dsh-cron] default Telegram credentials: cannot read DSH settings.yaml:', readErr.message);
67
+ logger.warn('[dsh-cron] default Telegram credentials: cannot read DSH settings.yaml:', readErr.message);
67
68
  return { botToken: undefined, chatId: undefined };
68
69
  }
69
70
  const botToken = extractSettingFromYaml(raw, 'dsh-messenger-gateway', 'botToken');
@@ -71,7 +72,7 @@ export function getDshDefaultTelegramCredentials() {
71
72
  return { botToken, chatId };
72
73
  }
73
74
  } catch (err) {
74
- console.warn('[dsh-cron] default Telegram credentials lookup failed:', err.message);
75
+ logger.warn('[dsh-cron] default Telegram credentials lookup failed:', err.message);
75
76
  }
76
77
  return { botToken: undefined, chatId: undefined };
77
78
  }
@@ -153,7 +154,7 @@ export async function answerTelegramCallbackQuery({ botToken, callbackQueryId, t
153
154
  body: JSON.stringify(body),
154
155
  });
155
156
  } catch (err) {
156
- console.warn('[dsh-cron] answerCallbackQuery failed:', err.message);
157
+ logger.warn('[dsh-cron] answerCallbackQuery failed:', err.message);
157
158
  }
158
159
  }
159
160
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-cron",
3
- "version": "0.2.28",
4
- "description": "Scheduled cron tasks, background automation and agent execution for DeepSeek Harness.",
3
+ "version": "0.2.30",
4
+ "description": "Background automation runner for DSH: isolated agent runs, script/HTTP/SSH/Docker runtimes, cost guard, notifications, heartbeats.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {