@goodandready/dsh-cron 0.2.28 → 0.2.29
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 +42 -20
- package/README.ru.md +41 -18
- package/README.zh.md +41 -18
- package/lib/client.js +1 -1
- package/lib/cron-tool.js +402 -0
- package/lib/index.js +24 -293
- package/lib/prompt.js +7 -4
- package/lib/task-patch.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ Autonomous AI agents often need to perform recurring duties: generating daily mo
|
|
|
43
43
|
|
|
44
44
|
1. **Rich Visual Task Manager** — a sidebar button with a collapsible list of active jobs (next run or live state, capped and persisted), plus a full panel to inspect, filter by type/model/channel, pause, trigger, duplicate, export/import and create tasks.
|
|
45
45
|
2. **Interactive "Create with DSH" Workflow** — chat with your agent to translate high-level requirements into a well-formed scheduled task.
|
|
46
|
-
3. **Autonomous AI Tool Calling** —
|
|
46
|
+
3. **Autonomous AI Tool Calling** — single unified `cron` tool (`action: 'create' | 'list' | 'get' | 'update' | 'pause' | 'resume' | 'run' | 'delete'`) lets agents inspect, trigger, and manage background automation without schema bloat.
|
|
47
47
|
4. **Robust Scheduler & Atomic Storage** — built on `croner` with interval aliases, one-shot delays, atomic file persistence, run histories, and cost tracking.
|
|
48
48
|
5. **Six Execution Runtimes** — shell, Node.js, Python, HTTP/webhook, remote SSH and Docker, plus per-task environment variables, workspace binding and isolated git worktrees for code-modifying agent tasks.
|
|
49
49
|
6. **Multi-Channel Delivery With Templates** — one run fans out to Telegram, dsh-kanban, Discord, Slack, ntfy, Bark, PushPlus, voice (`dsh-tts`) and Gitea, with `{variable}` message templates and secrets referenced by DSH credential name.
|
|
@@ -64,7 +64,7 @@ graph TD
|
|
|
64
64
|
|
|
65
65
|
subgraph Server ["Server Runtime (Cordis & DSH Services)"]
|
|
66
66
|
HttpRoutes["HTTP REST API<br/>(/dsh-cron/*)"]
|
|
67
|
-
AgentTools["AI Tool Calling Gateway<br/>(
|
|
67
|
+
AgentTools["AI Tool Calling Gateway<br/>(cron)"]
|
|
68
68
|
Scheduler["TaskScheduler Engine<br/>(Croner instances + one-shot timers)"]
|
|
69
69
|
Store["Atomic TaskStore<br/>(tasks.json with atomic write)"]
|
|
70
70
|
AgentRunner["Agent Session Dispatcher<br/>(Executes prompt with chosen model)"]
|
|
@@ -104,27 +104,49 @@ Click the clock icon in the DSH sidebar (positioned next to the new-session butt
|
|
|
104
104
|
Transform natural language into a scheduled job without guessing cron syntax:
|
|
105
105
|
1. Click **Create ⌄** ➔ **Create with DSH**.
|
|
106
106
|
2. Describe what you want to automate (e.g. *"Check open PRs every weekday at 9:00 and draft review comments"*).
|
|
107
|
-
3. The plugin spawns a dedicated agent session pre-injected with scheduler instructions. The agent clarifies the details with you — LLM vs no-LLM shell task, the exact cron expression, an economical model from those available in your DSH installation, and whether a "silent rule" (alert only on new events or failures) should apply — and registers the task through the `
|
|
108
|
-
|
|
109
|
-
###
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
|
116
|
-
|
|
|
117
|
-
| `
|
|
118
|
-
| `
|
|
119
|
-
|
|
|
120
|
-
|
|
|
121
|
-
|
|
|
122
|
-
|
|
|
107
|
+
3. The plugin spawns a dedicated agent session pre-injected with scheduler instructions. The agent clarifies the details with you — LLM vs no-LLM shell task, the exact cron expression, an economical model from those available in your DSH installation, and whether a "silent rule" (alert only on new events or failures) should apply — evaluates simple in-chat reminder (`schedule_create`) vs background automation (`cron`), and registers the task through the `cron` tool (`action: 'create'`) only after your confirmation.
|
|
108
|
+
|
|
109
|
+
### 2. DSH Core schedule vs dsh-cron
|
|
110
|
+
|
|
111
|
+
DeepSeek Harness includes a lightweight built-in `@deepseek-ai/dsh-schedule` extension for basic in-chat reminders. Use this guide to choose the right tool:
|
|
112
|
+
|
|
113
|
+
| Capability | DSH Core `schedule` (`@deepseek-ai/dsh-schedule`) | `@goodandready/dsh-cron` |
|
|
114
|
+
|:---|:---|:---|
|
|
115
|
+
| **Primary Purpose** | In-chat reminders & timed prompts back into the active dialogue | Unattended background automation runner & orchestrator |
|
|
116
|
+
| **Execution Context** | Active conversation session | Isolated dedicated agent sessions or background runner |
|
|
117
|
+
| **Runtimes** | Chat session turn only (LLM prompt) | 9 runtimes: `llm`, `script` (bash/sh), `node`, `python`, `http` (REST/webhook), `ssh`, `docker`, `skill`, `workflow` |
|
|
118
|
+
| **Model Tools** | `schedule_create`, `schedule_list`, `schedule_delete` | Unified `cron` tool (action: `create`, `list`, `get`, `update`, `pause`, `resume`, `run`, `delete`) |
|
|
119
|
+
| **Tool Schema Footprint** | ~1.5k characters | ~1.5k characters (consolidated from 9 tools down to 1, saving ~12k characters of LLM context) |
|
|
120
|
+
| **Delivery Channels** | Current chat only | Multi-channel: Telegram, Discord, Slack, Webhook, Kanban, ntfy, Bark, PushPlus, Voice (TTS), Gitea |
|
|
121
|
+
| **Code Modifying Isolation** | None | Ephemeral or retained git worktrees (`worktree: true`) |
|
|
122
|
+
| **Cost & Token Limits** | None | Guard rails: `costLimitUsd`, `dailyCostLimitUsd`, `tokenLimit` auto-pausing |
|
|
123
|
+
| **Failure Handling & Health** | None | Automatic retries with exponential backoff, failure inspector, Dead Man's Snitch / Better Uptime heartbeats |
|
|
124
|
+
| **Silent Rule** | None | Suppress delivery when no new events or changes occur (zero noise / zero spam) |
|
|
125
|
+
| **Task Management** | Basic list / delete | Full UI manager, execution history, log viewer, run metrics, manual trigger, import/export, profile config sync |
|
|
126
|
+
|
|
127
|
+
### 3. Agent Tool (`cron`)
|
|
128
|
+
|
|
129
|
+
Autonomous agents manage schedules directly through a single unified `cron` tool, keeping LLM schema overhead minimal:
|
|
130
|
+
|
|
131
|
+
| Action | Description | Key Parameters |
|
|
132
|
+
|:---|:---|:---|
|
|
133
|
+
| `create` | Creates a new background scheduled task or automation job | `title`, `schedule`, `prompt`, `type`, `model`, `channels`, `delivery`, etc. |
|
|
134
|
+
| `list` | Lists tasks with status, next run timestamp, tokens, and cost | `status` (`'all'`, `'active'`, `'paused'`, `'completed'`) |
|
|
135
|
+
| `get` | Reads the full detailed configuration of one task | `id` |
|
|
136
|
+
| `update` | Modifies an existing task in place (requires `confirmCodeSwitch: true` when switching to code-executing runtimes) | `id`, patch fields |
|
|
137
|
+
| `pause` | Pauses an active schedule without deleting its configuration | `id` |
|
|
138
|
+
| `resume` | Resumes a paused schedule | `id` |
|
|
139
|
+
| `run` | Triggers an immediate out-of-band execution | `id` |
|
|
140
|
+
| `delete` | Permanently removes a task and its run history | `id` |
|
|
141
|
+
|
|
142
|
+
> [!NOTE]
|
|
143
|
+
> **Context Optimization & Migration**: Previously, 9 separate tool schemas consumed ~13.6k characters of context in every model turn. The consolidated `cron` tool cuts this footprint by ~88% down to ~1.5k characters. Legacy tool names (`cron_create_task`, `cron_schedule_task`, `cron_list_tasks`, etc.) are gracefully rejected with helpful guidance directing the model to `cron` with the matching `action`. For simple in-conversation reminders, models are instructed to recommend core `schedule_create`.
|
|
123
144
|
|
|
124
145
|
Example invocation the model can make during a conversation:
|
|
125
146
|
|
|
126
|
-
```
|
|
127
|
-
|
|
147
|
+
```json
|
|
148
|
+
cron({
|
|
149
|
+
"action": "create",
|
|
128
150
|
"title": "Morning digest",
|
|
129
151
|
"schedule": "0 8 * * 1-5",
|
|
130
152
|
"prompt": "Prepare a brief morning digest of active tasks and open tickets.",
|
package/README.ru.md
CHANGED
|
@@ -64,7 +64,7 @@ graph TD
|
|
|
64
64
|
|
|
65
65
|
subgraph Server ["Серверная часть (Cordis и сервисы DSH)"]
|
|
66
66
|
HttpRoutes["HTTP REST API<br/>(/dsh-cron/*)"]
|
|
67
|
-
AgentTools["Шлюз tool calling<br/>(
|
|
67
|
+
AgentTools["Шлюз tool calling<br/>(cron)"]
|
|
68
68
|
Scheduler["Движок TaskScheduler<br/>(экземпляры Croner + таймеры one-shot)"]
|
|
69
69
|
Store["Атомарный TaskStore<br/>(tasks.json, атомарная запись)"]
|
|
70
70
|
AgentRunner["Диспетчер агентских сессий<br/>(запуск промпта выбранной моделью)"]
|
|
@@ -104,26 +104,49 @@ graph TD
|
|
|
104
104
|
Превратите естественный язык в задачу без подбора cron-синтаксиса:
|
|
105
105
|
1. Нажмите **Создать ⌄** ➔ **Создать с DSH**.
|
|
106
106
|
2. Опишите, что нужно автоматизировать (например: *«Проверяй открытые PR по будням в 9:00 и готовь черновики комментариев»*).
|
|
107
|
-
3. Плагин создаст отдельную агентскую сессию с системными инструкциями планировщика. Агент уточнит детали — LLM или NO-LLM shell-задача, точное cron-выражение, экономичная модель из доступных в вашей установке DSH, нужно ли «правило тишины» (алерт только при новых событиях или сбоях) — и
|
|
108
|
-
|
|
109
|
-
###
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
|
114
|
-
|
|
115
|
-
|
|
|
116
|
-
|
|
|
117
|
-
| `
|
|
118
|
-
| `
|
|
119
|
-
|
|
|
120
|
-
|
|
|
121
|
-
|
|
|
107
|
+
3. Плагин создаст отдельную агентскую сессию с системными инструкциями планировщика. Агент уточнит детали — LLM или NO-LLM shell-задача, точное cron-выражение, экономичная модель из доступных в вашей установке DSH, нужно ли «правило тишины» (алерт только при новых событиях или сбоях) — оценивает тип задачи (простое напоминание в текущем чате через `schedule_create` ядра vs фоновая автоматизация через `cron`) и создаёт задачу через инструмент `cron` (`action: 'create'`) только после вашего подтверждения.
|
|
108
|
+
|
|
109
|
+
### 2. Встроенный schedule ядра DSH vs dsh-cron
|
|
110
|
+
|
|
111
|
+
В состав DeepSeek Harness входит легковесное встроенное расширение `@deepseek-ai/dsh-schedule` для базовых напоминаний в чате. Сравнительная таблица помогает выбрать правильный инструмент:
|
|
112
|
+
|
|
113
|
+
| Возможность | Встроенный `schedule` ядра (`@deepseek-ai/dsh-schedule`) | `@goodandready/dsh-cron` |
|
|
114
|
+
|:---|:---|:---|
|
|
115
|
+
| **Основное назначение** | Напоминания и таймеры, доставляемые обратно в текущий диалог пользователя | Автономный фоновый раннер и оркестратор запланированных задач |
|
|
116
|
+
| **Контекст исполнения** | Текущая активная сессия чата | Изолированные выделенные агентские сессии или фоновые процессы |
|
|
117
|
+
| **Рантаймы** | Только ход в диалоге (LLM-промпт) | 9 рантаймов: `llm`, `script` (bash/sh), `node`, `python`, `http` (REST/webhook), `ssh`, `docker`, `skill`, `workflow` |
|
|
118
|
+
| **Инструменты модели** | `schedule_create`, `schedule_list`, `schedule_delete` | Единый инструмент `cron` (action: `create`, `list`, `get`, `update`, `pause`, `resume`, `run`, `delete`) |
|
|
119
|
+
| **Размер схемы инструментов** | ~1.5k символов | ~1.5k символов (консолидация 9 инструментов в 1, экономия ~12k символов контекста) |
|
|
120
|
+
| **Каналы доставки** | Только текущий чат | Мультиканальность: Telegram, Discord, Slack, Webhook, Kanban, ntfy, Bark, PushPlus, Голос (TTS), Gitea |
|
|
121
|
+
| **Изоляция кода** | Отсутствует | Одноразовые или сохраняемые git worktree (`worktree: true`) |
|
|
122
|
+
| **Лимиты расходов и токенов** | Отсутствуют | Защита от перерасхода: `costLimitUsd`, `dailyCostLimitUsd`, `tokenLimit` с автопаузой |
|
|
123
|
+
| **Надёжность и мониторинг** | Отсутствуют | Повторы с экспоненциальным backoff, failure inspector, heartbeat-пинги (Dead Man's Snitch / Better Uptime) |
|
|
124
|
+
| **Тихое правило (Silent Rule)** | Отсутствует | Полная тишина при отсутствии изменений (ноль шума и спама в каналы) |
|
|
125
|
+
| **Управление задачами** | Базовый список / удаление | Полноценный UI-менеджер, история запусков, логи, метрики, ручной запуск, импорт/экспорт, синхронизация с конфигом |
|
|
126
|
+
|
|
127
|
+
### 3. Инструмент агента (`cron`)
|
|
128
|
+
|
|
129
|
+
Автономные агенты управляют расписаниями через единый компактный инструмент `cron`:
|
|
130
|
+
|
|
131
|
+
| Действие (action) | Описание | Основные параметры |
|
|
132
|
+
|:---|:---|:---|
|
|
133
|
+
| `create` | Создаёт фоновую задачу или периодический процесс | `title`, `schedule`, `prompt`, `type`, `model`, `channels`, `delivery` и др. |
|
|
134
|
+
| `list` | Список задач со статусами, временем следующего запуска, токенами и стоимостью | `status` (`'all'`, `'active'`, `'paused'`, `'completed'`) |
|
|
135
|
+
| `get` | Полная конфигурация одной задачи по её идентификатору | `id` |
|
|
136
|
+
| `update` | Изменяет существующую задачу на месте (переключение в код требует `confirmCodeSwitch: true`) | `id`, поля патча |
|
|
137
|
+
| `pause` | Приостанавливает расписание без удаления конфигурации | `id` |
|
|
138
|
+
| `resume` | Возобновляет приостановленное расписание | `id` |
|
|
139
|
+
| `run` | Немедленный внеплановый запуск задачи | `id` |
|
|
140
|
+
| `delete` | Полностью удаляет задачу и её историю | `id` |
|
|
141
|
+
|
|
142
|
+
> [!NOTE]
|
|
143
|
+
> **Оптимизация контекста и миграция**: Ранее 9 отдельных схем инструментов занимали ~13.6k символов в каждом ходе модели. Консолидация в один инструмент `cron` сократила этот объём на ~88% до ~1.5k символов. Легаси-имена инструментов (`cron_create_task`, `cron_schedule_task`, `cron_list_tasks` и т.д.) отклоняются с понятной подсказкой использовать `cron` с соответствующим `action`. Для простых напоминаний в диалоге модель ориентируется на встроенный `schedule_create`.
|
|
122
144
|
|
|
123
145
|
Пример вызова модели в диалоге:
|
|
124
146
|
|
|
125
|
-
```
|
|
126
|
-
|
|
147
|
+
```json
|
|
148
|
+
cron({
|
|
149
|
+
"action": "create",
|
|
127
150
|
"title": "Утренняя сводка",
|
|
128
151
|
"schedule": "0 8 * * 1-5",
|
|
129
152
|
"prompt": "Подготовь короткую утреннюю сводку активных задач и открытых тикетов.",
|
package/README.zh.md
CHANGED
|
@@ -64,7 +64,7 @@ graph TD
|
|
|
64
64
|
|
|
65
65
|
subgraph Server ["服务端 (Cordis 与 DSH 服务)"]
|
|
66
66
|
HttpRoutes["HTTP REST API<br/>(/dsh-cron/*)"]
|
|
67
|
-
AgentTools["工具调用网关<br/>(
|
|
67
|
+
AgentTools["工具调用网关<br/>(cron)"]
|
|
68
68
|
Scheduler["TaskScheduler 引擎<br/>(Croner 实例 + one-shot 定时器)"]
|
|
69
69
|
Store["原子 TaskStore<br/>(tasks.json 原子写入)"]
|
|
70
70
|
AgentRunner["智能体会话调度器<br/>(以指定模型执行提示词)"]
|
|
@@ -104,26 +104,49 @@ graph TD
|
|
|
104
104
|
无需猜测 cron 语法,用自然语言即可创建任务:
|
|
105
105
|
1. 点击 **Create ⌄** ➔ **Create with DSH**。
|
|
106
106
|
2. 描述要自动化的内容(例如:*“每个工作日早上 9 点检查未处理的 PR 并起草评论”*)。
|
|
107
|
-
3. 插件会创建一个注入了调度器指令的专属智能体会话。智能体会与你确认细节 —— LLM 还是 NO-LLM shell 任务、准确的 cron 表达式、在你的 DSH 安装中可用的经济型模型,以及是否启用“静默规则”(仅在新事件或故障时告警)——
|
|
108
|
-
|
|
109
|
-
###
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
|
114
|
-
|
|
115
|
-
|
|
|
116
|
-
|
|
|
117
|
-
|
|
|
118
|
-
| `
|
|
119
|
-
|
|
|
120
|
-
|
|
|
121
|
-
|
|
|
107
|
+
3. 插件会创建一个注入了调度器指令的专属智能体会话。智能体会与你确认细节 —— LLM 还是 NO-LLM shell 任务、准确的 cron 表达式、在你的 DSH 安装中可用的经济型模型,以及是否启用“静默规则”(仅在新事件或故障时告警)—— 评估属于对话内定时提醒(`schedule_create`)还是后台自动化(`cron`),并在获得您的确认后通过 `cron` 工具(`action: 'create'`)注册任务。
|
|
108
|
+
|
|
109
|
+
### 2. DSH 核心内置 schedule 与 dsh-cron 对比
|
|
110
|
+
|
|
111
|
+
DeepSeek Harness 内置了轻量级扩展 `@deepseek-ai/dsh-schedule`,用于会话内的基础定时提醒。下表帮助您根据场景选择合适的工具:
|
|
112
|
+
|
|
113
|
+
| 功能维度 | DSH 核心 `schedule` (`@deepseek-ai/dsh-schedule`) | `@goodandready/dsh-cron` |
|
|
114
|
+
|:---|:---|:---|
|
|
115
|
+
| **主要定位** | 当前会话内的定时提醒与催办消息 | 无人值守的后台自动化执行器与任务编排引擎 |
|
|
116
|
+
| **执行上下文** | 当前活动会话内 | 独立的隔离智能体会话或外部后台进程 |
|
|
117
|
+
| **执行运行时** | 仅当前会话提示词(LLM) | 9 种运行时:`llm`、`script` (bash/sh)、`node`、`python`、`http` (REST/webhook)、`ssh`、`docker`、`skill`、`workflow` |
|
|
118
|
+
| **模型工具** | `schedule_create`、`schedule_list`、`schedule_delete` | 统一 `cron` 工具(action: `create`、`list`、`get`、`update`、`pause`、`resume`、`run`、`delete`) |
|
|
119
|
+
| **工具模式体积** | 约 1.5k 字符 | 约 1.5k 字符(由 9 个工具合并为 1 个,节省约 12k 字符上下文) |
|
|
120
|
+
| **推送渠道** | 仅限当前会话 | 多渠道:Telegram、Discord、Slack、Webhook、Kanban、ntfy、Bark、PushPlus、语音 (TTS)、Gitea |
|
|
121
|
+
| **代码修改隔离** | 无 | 临时或保留的 git worktree 隔离环境(`worktree: true`) |
|
|
122
|
+
| **成本与 Token 限制** | 无 | 成本熔断防护:`costLimitUsd`、`dailyCostLimitUsd`、`tokenLimit` 自动暂停 |
|
|
123
|
+
| **容错与健康检查** | 无 | 指数退避自动重试、失败自动诊断、心跳监测 (Dead Man's Snitch / Better Uptime) |
|
|
124
|
+
| **静默规则 (Silent Rule)** | 无 | 无新事件或变更时完全静默(杜绝通道垃圾消息) |
|
|
125
|
+
| **任务管理** | 基础列表与删除 | 完整 UI 管理器、运行历史、日志查看器、指标统计、手动触发、导入导出、配置同步 |
|
|
126
|
+
|
|
127
|
+
### 3. 智能体工具 (`cron`)
|
|
128
|
+
|
|
129
|
+
自主智能体通过单个统一的 `cron` 工具直接管理定时任务,大幅降低模型模式开销:
|
|
130
|
+
|
|
131
|
+
| 动作 (action) | 说明 | 核心参数 |
|
|
132
|
+
|:---|:---|:---|
|
|
133
|
+
| `create` | 创建新的后台定时任务或自动化作业 | `title`、`schedule`、`prompt`、`type`、`model`、`channels`、`delivery` 等 |
|
|
134
|
+
| `list` | 列出任务的状态、下次运行时间、token 总量与成本估算 | `status` (`'all'`、`'active'`、`'paused'`、`'completed'`) |
|
|
135
|
+
| `get` | 根据任务 ID 获取单项任务的完整配置 | `id` |
|
|
136
|
+
| `update` | 就地修改现有任务(切换到代码执行运行时需 `confirmCodeSwitch: true`) | `id`、修改字段 |
|
|
137
|
+
| `pause` | 暂停调度而不删除配置 | `id` |
|
|
138
|
+
| `resume` | 恢复已暂停的调度 | `id` |
|
|
139
|
+
| `run` | 触发一次立即的带外运行 | `id` |
|
|
140
|
+
| `delete` | 永久删除任务及其历史 | `id` |
|
|
141
|
+
|
|
142
|
+
> [!NOTE]
|
|
143
|
+
> **上下文优化与平滑迁移**:此前 9 个单独的工具模式在每次模型轮次中消耗约 13.6k 字符。整合为单一 `cron` 工具后,模式开销减少约 88%(降至约 1.5k 字符)。旧工具名(`cron_create_task`、`cron_schedule_task`、`cron_list_tasks` 等)被优雅拦截,并返回清晰迁移提示,引导模型使用带对应 `action` 的 `cron` 工具。对于简单的会话内提醒,模型将建议使用核心内置的 `schedule_create`。
|
|
122
144
|
|
|
123
145
|
会话中模型可进行的调用示例:
|
|
124
146
|
|
|
125
|
-
```
|
|
126
|
-
|
|
147
|
+
```json
|
|
148
|
+
cron({
|
|
149
|
+
"action": "create",
|
|
127
150
|
"title": "Morning digest",
|
|
128
151
|
"schedule": "0 8 * * 1-5",
|
|
129
152
|
"prompt": "Prepare a brief morning digest of active tasks and open tickets.",
|
package/lib/client.js
CHANGED
|
@@ -1636,7 +1636,7 @@ window.__ModuleLoader__.load({
|
|
|
1636
1636
|
Array.isArray(task.channels) && task.channels.length ? `- channels: ${task.channels.join(', ')}` : null,
|
|
1637
1637
|
`- prompt: ${String(task.prompt || '').slice(0, 400)}`,
|
|
1638
1638
|
'',
|
|
1639
|
-
'Ask what should change, then apply it with the
|
|
1639
|
+
'Ask what should change, then apply it with the cron tool (action: "update", or action: "get" if you need the full configuration).',
|
|
1640
1640
|
'If the change makes the task execute code (script/node/python/ssh/docker), confirm it with me explicitly before applying it.',
|
|
1641
1641
|
].filter(Boolean);
|
|
1642
1642
|
try {
|
package/lib/cron-tool.js
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import { executeCreateTask } from './task-create.js';
|
|
2
|
+
import { applyTaskPatch, describeTaskPatch } from './task-patch.js';
|
|
3
|
+
import { isConfigOwned, configOwnedMessage } from './config-jobs.js';
|
|
4
|
+
import { TASK_TYPES } from './runtimes.js';
|
|
5
|
+
import { CHANNEL_IDS } from './channels.js';
|
|
6
|
+
|
|
7
|
+
export const LEGACY_TOOL_NAMES = [
|
|
8
|
+
'cron_create_task',
|
|
9
|
+
'cron_schedule_task',
|
|
10
|
+
'cron_list_tasks',
|
|
11
|
+
'cron_pause_task',
|
|
12
|
+
'cron_resume_task',
|
|
13
|
+
'cron_delete_task',
|
|
14
|
+
'cron_get_task',
|
|
15
|
+
'cron_update_task',
|
|
16
|
+
'cron_run_task',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export const LEGACY_ACTION_MAP = {
|
|
20
|
+
cron_create_task: 'create',
|
|
21
|
+
create_task: 'create',
|
|
22
|
+
cron_schedule_task: 'create',
|
|
23
|
+
schedule_task: 'create',
|
|
24
|
+
schedule: 'create',
|
|
25
|
+
cron_list_tasks: 'list',
|
|
26
|
+
list_tasks: 'list',
|
|
27
|
+
cron_get_task: 'get',
|
|
28
|
+
get_task: 'get',
|
|
29
|
+
cron_update_task: 'update',
|
|
30
|
+
update_task: 'update',
|
|
31
|
+
cron_pause_task: 'pause',
|
|
32
|
+
pause_task: 'pause',
|
|
33
|
+
cron_resume_task: 'resume',
|
|
34
|
+
resume_task: 'resume',
|
|
35
|
+
cron_run_task: 'run',
|
|
36
|
+
run_task: 'run',
|
|
37
|
+
cron_delete_task: 'delete',
|
|
38
|
+
delete_task: 'delete',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export function rejectLegacyTool(toolName) {
|
|
42
|
+
const mapped = LEGACY_ACTION_MAP[toolName] || 'list';
|
|
43
|
+
return {
|
|
44
|
+
success: false,
|
|
45
|
+
message: `Tool "${toolName}" has been removed and consolidated into the unified "cron" tool. Please call "cron" with action: "${mapped}" instead. For simple in-chat reminders, use the built-in "schedule_create" tool.`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const cronToolParameters = {
|
|
50
|
+
action: {
|
|
51
|
+
type: 'string',
|
|
52
|
+
enum: ['create', 'list', 'get', 'update', 'pause', 'resume', 'run', 'delete'],
|
|
53
|
+
description: 'Operation to perform: "create", "list", "get", "update", "pause", "resume", "run", or "delete". For simple reminders in the current chat, use the built-in "schedule_create" tool instead.',
|
|
54
|
+
required: true,
|
|
55
|
+
},
|
|
56
|
+
id: {
|
|
57
|
+
type: 'string',
|
|
58
|
+
description: 'Task ID (required for get, update, pause, resume, run, delete)',
|
|
59
|
+
},
|
|
60
|
+
title: {
|
|
61
|
+
type: 'string',
|
|
62
|
+
description: 'Short task name (for create and update)',
|
|
63
|
+
},
|
|
64
|
+
schedule: {
|
|
65
|
+
type: 'string',
|
|
66
|
+
description: 'Schedule expression: cron ("0 9 * * *"), shorthand ("@daily", "@every 30m"), interval ("every 2h"), or one-shot ("in 30m", "at: 2026-09-05T15:00:00Z")',
|
|
67
|
+
},
|
|
68
|
+
prompt: {
|
|
69
|
+
type: 'string',
|
|
70
|
+
description: 'Prompt/instruction for the agent at run time or command to execute',
|
|
71
|
+
},
|
|
72
|
+
type: {
|
|
73
|
+
type: 'string',
|
|
74
|
+
enum: TASK_TYPES,
|
|
75
|
+
description: 'Task runtime type: llm | script | node | python | http | ssh | docker | skill | workflow',
|
|
76
|
+
},
|
|
77
|
+
status: {
|
|
78
|
+
type: 'string',
|
|
79
|
+
enum: ['all', 'active', 'paused', 'completed'],
|
|
80
|
+
description: 'Filter tasks by status (for list action)',
|
|
81
|
+
},
|
|
82
|
+
confirmCodeSwitch: {
|
|
83
|
+
type: 'boolean',
|
|
84
|
+
description: 'Explicit confirmation required when updating a task into a code-executing type (script/node/python/ssh/docker)',
|
|
85
|
+
},
|
|
86
|
+
delivery: {
|
|
87
|
+
type: 'string',
|
|
88
|
+
enum: ['current', 'isolated'],
|
|
89
|
+
description: 'Run mode: current (in current chat) or isolated (separate session)',
|
|
90
|
+
},
|
|
91
|
+
provider: {
|
|
92
|
+
type: 'string',
|
|
93
|
+
description: 'Model provider (optional)',
|
|
94
|
+
},
|
|
95
|
+
model: {
|
|
96
|
+
type: 'string',
|
|
97
|
+
description: 'Model identifier (optional)',
|
|
98
|
+
},
|
|
99
|
+
fallbackModel: {
|
|
100
|
+
type: 'string',
|
|
101
|
+
description: 'Model tried once more when the primary model fails',
|
|
102
|
+
},
|
|
103
|
+
fallbackProvider: {
|
|
104
|
+
type: 'string',
|
|
105
|
+
description: 'Provider for the fallback model (defaults to task provider)',
|
|
106
|
+
},
|
|
107
|
+
inspectOnFailure: {
|
|
108
|
+
type: 'boolean',
|
|
109
|
+
description: 'Diagnose failed runs with a model and store the diagnosis with the run',
|
|
110
|
+
},
|
|
111
|
+
silentRule: {
|
|
112
|
+
type: 'string',
|
|
113
|
+
description: 'Plain-language condition for staying silent on a successful run',
|
|
114
|
+
},
|
|
115
|
+
timezone: {
|
|
116
|
+
type: 'string',
|
|
117
|
+
description: 'IANA time zone for the schedule, e.g. "Europe/Berlin"',
|
|
118
|
+
},
|
|
119
|
+
notifyTelegram: {
|
|
120
|
+
type: 'boolean',
|
|
121
|
+
description: 'Send the run report to Telegram',
|
|
122
|
+
},
|
|
123
|
+
onlyOnFailure: {
|
|
124
|
+
type: 'boolean',
|
|
125
|
+
description: 'Send reports only for failures',
|
|
126
|
+
},
|
|
127
|
+
timeoutSeconds: {
|
|
128
|
+
type: 'number',
|
|
129
|
+
description: 'Execution time limit in seconds',
|
|
130
|
+
},
|
|
131
|
+
overlapPolicy: {
|
|
132
|
+
type: 'string',
|
|
133
|
+
enum: ['skip', 'queue', 'replace'],
|
|
134
|
+
description: 'Overlap policy: skip | queue | replace',
|
|
135
|
+
},
|
|
136
|
+
misfirePolicy: {
|
|
137
|
+
type: 'string',
|
|
138
|
+
enum: ['skip', 'runOnce', 'catchUpAll'],
|
|
139
|
+
description: 'What to do with a run missed during downtime: skip | runOnce | catchUpAll',
|
|
140
|
+
},
|
|
141
|
+
maxRetries: {
|
|
142
|
+
type: 'number',
|
|
143
|
+
description: 'Automatic retry attempts on failure (0 = off)',
|
|
144
|
+
},
|
|
145
|
+
retryBackoffMs: {
|
|
146
|
+
type: 'number',
|
|
147
|
+
description: 'Base backoff between retries in ms, doubled per attempt',
|
|
148
|
+
},
|
|
149
|
+
permissionPreset: {
|
|
150
|
+
type: 'string',
|
|
151
|
+
enum: ['default', 'read-only', 'workspace-write', 'full'],
|
|
152
|
+
description: 'Permission preset applied to the task session',
|
|
153
|
+
},
|
|
154
|
+
kanbanMode: {
|
|
155
|
+
type: 'string',
|
|
156
|
+
enum: ['none', 'on_failure', 'always'],
|
|
157
|
+
description: 'Create a dsh-kanban card for runs: none | on_failure | always',
|
|
158
|
+
},
|
|
159
|
+
channels: {
|
|
160
|
+
type: 'array',
|
|
161
|
+
items: { type: 'string', enum: CHANNEL_IDS },
|
|
162
|
+
description: 'Notification channels for this task',
|
|
163
|
+
},
|
|
164
|
+
template: {
|
|
165
|
+
type: 'string',
|
|
166
|
+
description: 'Notification template with {title} {status} {output} {error} {duration} placeholders',
|
|
167
|
+
},
|
|
168
|
+
env: {
|
|
169
|
+
type: 'object',
|
|
170
|
+
additionalProperties: true,
|
|
171
|
+
description: 'Environment variables for external runtimes (KEY: value)',
|
|
172
|
+
},
|
|
173
|
+
cwd: {
|
|
174
|
+
type: 'string',
|
|
175
|
+
description: 'Working directory for the run',
|
|
176
|
+
},
|
|
177
|
+
workspaceId: {
|
|
178
|
+
type: 'string',
|
|
179
|
+
description: 'Workspace bound to the task',
|
|
180
|
+
},
|
|
181
|
+
worktree: {
|
|
182
|
+
type: 'boolean',
|
|
183
|
+
description: 'Run code-modifying agent tasks in an isolated git worktree',
|
|
184
|
+
},
|
|
185
|
+
keepWorktree: {
|
|
186
|
+
type: 'boolean',
|
|
187
|
+
description: 'Keep the created worktree after the run',
|
|
188
|
+
},
|
|
189
|
+
nodePath: {
|
|
190
|
+
type: 'string',
|
|
191
|
+
description: 'Node.js binary for node tasks',
|
|
192
|
+
},
|
|
193
|
+
pythonPath: {
|
|
194
|
+
type: 'string',
|
|
195
|
+
description: 'Python interpreter for python tasks',
|
|
196
|
+
},
|
|
197
|
+
httpMethod: {
|
|
198
|
+
type: 'string',
|
|
199
|
+
description: 'HTTP method for http tasks',
|
|
200
|
+
},
|
|
201
|
+
httpUrl: {
|
|
202
|
+
type: 'string',
|
|
203
|
+
description: 'URL for http tasks',
|
|
204
|
+
},
|
|
205
|
+
httpHeaders: {
|
|
206
|
+
type: 'object',
|
|
207
|
+
additionalProperties: true,
|
|
208
|
+
description: 'Headers for http tasks',
|
|
209
|
+
},
|
|
210
|
+
httpBody: {
|
|
211
|
+
type: 'string',
|
|
212
|
+
description: 'Request body for http tasks',
|
|
213
|
+
},
|
|
214
|
+
sshProfileId: {
|
|
215
|
+
type: 'string',
|
|
216
|
+
description: 'dsh-remote-workspace profile id',
|
|
217
|
+
},
|
|
218
|
+
sshTarget: {
|
|
219
|
+
type: 'string',
|
|
220
|
+
description: 'Fallback user@host when no remote-workspace profile is used',
|
|
221
|
+
},
|
|
222
|
+
sshPort: {
|
|
223
|
+
type: 'number',
|
|
224
|
+
description: 'SSH port',
|
|
225
|
+
},
|
|
226
|
+
sshKeyPath: {
|
|
227
|
+
type: 'string',
|
|
228
|
+
description: 'Private key path',
|
|
229
|
+
},
|
|
230
|
+
dockerImage: {
|
|
231
|
+
type: 'string',
|
|
232
|
+
description: 'Container image for docker tasks',
|
|
233
|
+
},
|
|
234
|
+
skillName: {
|
|
235
|
+
type: 'string',
|
|
236
|
+
description: 'DSH skill name for skill tasks',
|
|
237
|
+
},
|
|
238
|
+
workflowName: {
|
|
239
|
+
type: 'string',
|
|
240
|
+
description: 'DSH workflow name for workflow tasks',
|
|
241
|
+
},
|
|
242
|
+
costLimitUsd: {
|
|
243
|
+
type: 'number',
|
|
244
|
+
description: 'Maximum allowed total cost in USD before auto-pausing',
|
|
245
|
+
},
|
|
246
|
+
dailyCostLimitUsd: {
|
|
247
|
+
type: 'number',
|
|
248
|
+
description: 'Maximum allowed daily cost in USD before auto-pausing',
|
|
249
|
+
},
|
|
250
|
+
tokenLimit: {
|
|
251
|
+
type: 'number',
|
|
252
|
+
description: 'Maximum allowed token usage before auto-pausing',
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
export const cronToolOutput = {
|
|
257
|
+
schema: {
|
|
258
|
+
type: 'object',
|
|
259
|
+
additionalProperties: true,
|
|
260
|
+
properties: {
|
|
261
|
+
success: { type: 'boolean' },
|
|
262
|
+
message: { type: 'string' },
|
|
263
|
+
count: { type: 'number' },
|
|
264
|
+
task: { type: 'object', additionalProperties: true },
|
|
265
|
+
tasks: { type: 'array', items: { type: 'object', additionalProperties: true } },
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
render: (_args, val) => [{ type: 'text', text: val.message || (val.success ? 'Success' : 'Failed') }],
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
export async function executeCronTool(store, scheduler, args = {}) {
|
|
272
|
+
const action = args.action;
|
|
273
|
+
|
|
274
|
+
if (!action) {
|
|
275
|
+
if (args.schedule && args.prompt) {
|
|
276
|
+
return {
|
|
277
|
+
success: false,
|
|
278
|
+
message: 'Missing "action" parameter. The 9 separate cron tools have been consolidated into "cron". Did you mean action: "create"?',
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
success: false,
|
|
283
|
+
message: 'Parameter "action" is required: "create", "list", "get", "update", "pause", "resume", "run", or "delete". Note: For simple in-chat reminders, use built-in "schedule_create".',
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (LEGACY_ACTION_MAP[action]) {
|
|
288
|
+
const suggested = LEGACY_ACTION_MAP[action];
|
|
289
|
+
return {
|
|
290
|
+
success: false,
|
|
291
|
+
message: `Action "${action}" is deprecated. The 9 separate tools have been consolidated into the unified "cron" tool. Please use action: "${suggested}".`,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
switch (action) {
|
|
296
|
+
case 'create': {
|
|
297
|
+
if (!args.title || !args.schedule || !args.prompt) {
|
|
298
|
+
return {
|
|
299
|
+
success: false,
|
|
300
|
+
message: 'Fields "title", "schedule", and "prompt" are required to create a task.',
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
try {
|
|
304
|
+
return executeCreateTask(store, scheduler, args);
|
|
305
|
+
} catch (err) {
|
|
306
|
+
return { success: false, message: err.message };
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
case 'list': {
|
|
311
|
+
const tasks = store.list({ status: args.status || 'all' });
|
|
312
|
+
return {
|
|
313
|
+
success: true,
|
|
314
|
+
count: tasks.length,
|
|
315
|
+
tasks: tasks.map((t) => ({
|
|
316
|
+
id: t.id,
|
|
317
|
+
title: t.title,
|
|
318
|
+
schedule: t.scheduleText || t.schedule,
|
|
319
|
+
status: t.status,
|
|
320
|
+
nextRunAt: t.nextRunAt ? new Date(t.nextRunAt).toISOString() : null,
|
|
321
|
+
lastStatus: t.lastStatus,
|
|
322
|
+
totalTokens: t.totalTokens || 0,
|
|
323
|
+
totalCostUsd: t.totalCostUsd || 0,
|
|
324
|
+
oneShot: Boolean(t.oneShot),
|
|
325
|
+
})),
|
|
326
|
+
message: `Total tasks: ${tasks.length}`,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
case 'get': {
|
|
331
|
+
if (!args.id) return { success: false, message: 'Field "id" is required for get action' };
|
|
332
|
+
const task = store.get(args.id);
|
|
333
|
+
if (!task) return { success: false, message: 'Task not found' };
|
|
334
|
+
const summary = [
|
|
335
|
+
`Task "${task.title}" (${task.id})`,
|
|
336
|
+
`type: ${task.type || 'llm'}`,
|
|
337
|
+
`schedule: ${task.scheduleText || task.schedule}`,
|
|
338
|
+
`status: ${task.status}`,
|
|
339
|
+
task.type === 'llm' || !task.type ? `model: ${task.model || '(default)'}${task.fallbackModel ? ` (fallback: ${task.fallbackModel})` : ''}` : null,
|
|
340
|
+
`channels: ${Array.isArray(task.channels) && task.channels.length ? task.channels.join(', ') : '(legacy flags)'}`,
|
|
341
|
+
`prompt: ${String(task.prompt || '').slice(0, 500)}`,
|
|
342
|
+
].filter(Boolean).join('\n');
|
|
343
|
+
return { success: true, message: summary, task };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
case 'update': {
|
|
347
|
+
if (!args.id) return { success: false, message: 'Field "id" is required for update action' };
|
|
348
|
+
const { id, confirmCodeSwitch, action: _act, ...patch } = args;
|
|
349
|
+
const result = applyTaskPatch({
|
|
350
|
+
store,
|
|
351
|
+
scheduler,
|
|
352
|
+
id,
|
|
353
|
+
body: patch,
|
|
354
|
+
allowCodeSwitch: confirmCodeSwitch === true,
|
|
355
|
+
});
|
|
356
|
+
if (!result.ok) return { success: false, message: result.error };
|
|
357
|
+
return {
|
|
358
|
+
success: true,
|
|
359
|
+
message: `Task "${result.task.title}" updated (${result.task.scheduleText || result.task.schedule}). Changed: ${describeTaskPatch(result.task, result.patch)}`,
|
|
360
|
+
task: result.task,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
case 'pause': {
|
|
365
|
+
if (!args.id) return { success: false, message: 'Field "id" is required for pause action' };
|
|
366
|
+
if (isConfigOwned(store.get(args.id))) return { success: false, message: configOwnedMessage(args.id) };
|
|
367
|
+
const task = scheduler.pauseTask(args.id);
|
|
368
|
+
if (!task) return { success: false, message: 'Task not found' };
|
|
369
|
+
return { success: true, message: `Task "${task.title}" paused` };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
case 'resume': {
|
|
373
|
+
if (!args.id) return { success: false, message: 'Field "id" is required for resume action' };
|
|
374
|
+
if (isConfigOwned(store.get(args.id))) return { success: false, message: configOwnedMessage(args.id) };
|
|
375
|
+
const task = scheduler.resumeTask(args.id);
|
|
376
|
+
if (!task) return { success: false, message: 'Task not found' };
|
|
377
|
+
return { success: true, message: `Task "${task.title}" resumed (${task.scheduleText})` };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
case 'run': {
|
|
381
|
+
if (!args.id) return { success: false, message: 'Field "id" is required for run action' };
|
|
382
|
+
const task = store.get(args.id);
|
|
383
|
+
if (!task) return { success: false, message: 'Task not found' };
|
|
384
|
+
scheduler.triggerManualRun(args.id);
|
|
385
|
+
return { success: true, message: `Manual run started for task "${task.title}"` };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
case 'delete': {
|
|
389
|
+
if (!args.id) return { success: false, message: 'Field "id" is required for delete action' };
|
|
390
|
+
if (isConfigOwned(store.get(args.id))) return { success: false, message: configOwnedMessage(args.id) };
|
|
391
|
+
scheduler.pauseTask(args.id);
|
|
392
|
+
const ok = store.delete(args.id);
|
|
393
|
+
return { success: ok, message: ok ? 'Task deleted' : 'Task not found' };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
default:
|
|
397
|
+
return {
|
|
398
|
+
success: false,
|
|
399
|
+
message: `Unknown action "${action}". Allowed actions: create, list, get, update, pause, resume, run, delete.`,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -92,64 +92,26 @@ export const name = '@goodandready/dsh-cron';
|
|
|
92
92
|
export const inject = ['tools', 'webServer', 'llm', 'agents', 'agentDefaultModel', 'credentials'];
|
|
93
93
|
|
|
94
94
|
import { executeCreateTask } from './task-create.js';
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
};
|
|
95
|
+
import {
|
|
96
|
+
cronToolParameters,
|
|
97
|
+
cronToolOutput,
|
|
98
|
+
executeCronTool,
|
|
99
|
+
rejectLegacyTool,
|
|
100
|
+
LEGACY_TOOL_NAMES,
|
|
101
|
+
LEGACY_ACTION_MAP,
|
|
102
|
+
} from './cron-tool.js';
|
|
140
103
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
},
|
|
151
|
-
render: (_args, val) => [{ type: 'text', text: val.message || JSON.stringify(val) }]
|
|
104
|
+
export {
|
|
105
|
+
executeCreateTask,
|
|
106
|
+
cronToolParameters,
|
|
107
|
+
cronToolOutput,
|
|
108
|
+
executeCronTool,
|
|
109
|
+
rejectLegacyTool,
|
|
110
|
+
LEGACY_TOOL_NAMES,
|
|
111
|
+
LEGACY_ACTION_MAP,
|
|
152
112
|
};
|
|
113
|
+
export const createTaskParameters = cronToolParameters;
|
|
114
|
+
export const createTaskOutput = cronToolOutput;
|
|
153
115
|
|
|
154
116
|
export function apply(ctx, rawConfig) {
|
|
155
117
|
const config = unwrapConfig(rawConfig);
|
|
@@ -286,243 +248,12 @@ export function apply(ctx, rawConfig) {
|
|
|
286
248
|
}
|
|
287
249
|
}), 'dsh-cron: /settings');
|
|
288
250
|
|
|
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
|
-
|
|
251
|
+
// 2. Tools: unified background automation runner (Issue #196: 9 tools -> 1)
|
|
396
252
|
ctx.tools.register(defineTool({
|
|
397
|
-
name: '
|
|
398
|
-
description: '
|
|
399
|
-
parameters:
|
|
400
|
-
|
|
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
|
-
}
|
|
253
|
+
name: 'cron',
|
|
254
|
+
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.',
|
|
255
|
+
parameters: cronToolParameters,
|
|
256
|
+
output: cronToolOutput,
|
|
257
|
+
execute: async (args) => executeCronTool(store, scheduler, args),
|
|
527
258
|
}));
|
|
528
259
|
}
|
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.
|
|
16
|
-
|
|
17
|
-
|
|
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 \`
|
|
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/task-patch.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Partial task updates, shared by the HTTP PATCH route and the
|
|
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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-cron",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.29",
|
|
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": {
|