crewly 1.5.11 → 1.5.13
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/config/constants.ts +3 -3
- package/config/orchestrator_tasks/prompts/orchestrator-prompt.md +73 -0
- package/config/roles/architect/prompt.md +9 -0
- package/config/roles/backend-developer/prompt.md +9 -0
- package/config/roles/content-strategist/prompt.md +10 -0
- package/config/roles/designer/prompt.md +9 -0
- package/config/roles/developer/prompt.md +9 -0
- package/config/roles/frontend-developer/prompt.md +9 -0
- package/config/roles/fullstack-dev/prompt.md +9 -0
- package/config/roles/generalist/prompt.md +9 -0
- package/config/roles/ops/prompt.md +9 -0
- package/config/roles/product-manager/prompt.md +9 -0
- package/config/roles/qa/prompt.md +9 -0
- package/config/roles/qa-engineer/prompt.md +9 -0
- package/config/roles/researcher/prompt.md +9 -0
- package/config/roles/sales/prompt.md +9 -0
- package/config/roles/support/prompt.md +9 -0
- package/config/roles/team-leader/prompt.md +11 -0
- package/config/roles/tpm/prompt.md +9 -0
- package/config/roles/ux-designer/prompt.md +9 -0
- package/config/skills/_common/lib.sh +31 -0
- package/config/skills/_common/lib.test.sh +164 -0
- package/config/skills/agent/core/block-task/execute.sh +3 -1
- package/config/skills/agent/core/pipe-to-sink/execute.sh +41 -0
- package/config/skills/agent/core/read-task/execute.sh +3 -1
- package/config/skills/agent/core/report-progress/execute.sh +3 -1
- package/config/skills/agent/screenshot-compare/SKILL.md +75 -0
- package/config/skills/agent/screenshot-compare/execute.sh +182 -0
- package/config/skills/agent/screenshot-compare/skill.json +10 -0
- package/config/skills/agent/xiaoyuzhoufm-transcript/SKILL.md +85 -0
- package/config/skills/agent/xiaoyuzhoufm-transcript/execute.sh +306 -0
- package/config/skills/agent/xiaoyuzhoufm-transcript/skill.json +10 -0
- package/config/skills/orchestrator/cancel-cron/SKILL.md +44 -0
- package/config/skills/orchestrator/create-cron/SKILL.md +58 -0
- package/config/skills/orchestrator/list-cron/SKILL.md +51 -0
- package/config/skills/orchestrator/update-cron/SKILL.md +52 -0
- package/dist/backend/backend/src/constants.d.ts +7 -4
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +6 -3
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/browser/browser.controller.d.ts +21 -2
- package/dist/backend/backend/src/controllers/browser/browser.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/browser/browser.controller.js +167 -29
- package/dist/backend/backend/src/controllers/browser/browser.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/browser/browser.routes.d.ts +1 -1
- package/dist/backend/backend/src/controllers/browser/browser.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/browser/browser.routes.js +7 -3
- package/dist/backend/backend/src/controllers/browser/browser.routes.js.map +1 -1
- package/dist/backend/backend/src/controllers/data/data.controller.d.ts +47 -0
- package/dist/backend/backend/src/controllers/data/data.controller.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/data/data.controller.js +201 -0
- package/dist/backend/backend/src/controllers/data/data.controller.js.map +1 -0
- package/dist/backend/backend/src/controllers/data/data.routes.d.ts +18 -0
- package/dist/backend/backend/src/controllers/data/data.routes.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/data/data.routes.js +44 -0
- package/dist/backend/backend/src/controllers/data/data.routes.js.map +1 -0
- package/dist/backend/backend/src/controllers/monitoring/token-usage.controller.d.ts +3 -2
- package/dist/backend/backend/src/controllers/monitoring/token-usage.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/monitoring/token-usage.controller.js +5 -3
- package/dist/backend/backend/src/controllers/monitoring/token-usage.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/system/cron-task.controller.d.ts +4 -0
- package/dist/backend/backend/src/controllers/system/cron-task.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/system/cron-task.controller.js +20 -0
- package/dist/backend/backend/src/controllers/system/cron-task.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/task-management/task-management.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/task-management/task-management.controller.js +18 -0
- package/dist/backend/backend/src/controllers/task-management/task-management.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/team/team-export.controller.d.ts +32 -0
- package/dist/backend/backend/src/controllers/team/team-export.controller.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/team/team-export.controller.js +61 -0
- package/dist/backend/backend/src/controllers/team/team-export.controller.js.map +1 -0
- package/dist/backend/backend/src/controllers/team/team.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/team/team.routes.js +7 -0
- package/dist/backend/backend/src/controllers/team/team.routes.js.map +1 -1
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +37 -7
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/routes/api.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/routes/api.routes.js +4 -1
- package/dist/backend/backend/src/routes/api.routes.js.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.js +6 -2
- package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/idle-detection.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/idle-detection.service.js +17 -2
- package/dist/backend/backend/src/services/agent/idle-detection.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.d.ts +1 -1
- package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.js +2 -2
- package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.js.map +1 -1
- package/dist/backend/backend/src/services/agent/task-planning.service.d.ts +134 -0
- package/dist/backend/backend/src/services/agent/task-planning.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/agent/task-planning.service.js +291 -0
- package/dist/backend/backend/src/services/agent/task-planning.service.js.map +1 -0
- package/dist/backend/backend/src/services/ai/prompt-modules/communication.module.d.ts +11 -0
- package/dist/backend/backend/src/services/ai/prompt-modules/communication.module.d.ts.map +1 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/communication.module.js +47 -18
- package/dist/backend/backend/src/services/ai/prompt-modules/communication.module.js.map +1 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts +14 -0
- package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js +47 -4
- package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
- package/dist/backend/backend/src/services/browser/browser-bridge.service.d.ts +13 -9
- package/dist/backend/backend/src/services/browser/browser-bridge.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/browser/browser-bridge.service.js +44 -12
- package/dist/backend/backend/src/services/browser/browser-bridge.service.js.map +1 -1
- package/dist/backend/backend/src/services/browser/browser-proxy.service.d.ts +176 -0
- package/dist/backend/backend/src/services/browser/browser-proxy.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/browser/browser-proxy.service.js +441 -0
- package/dist/backend/backend/src/services/browser/browser-proxy.service.js.map +1 -0
- package/dist/backend/backend/src/services/browser/browser-relay-adapter.service.d.ts +162 -0
- package/dist/backend/backend/src/services/browser/browser-relay-adapter.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/browser/browser-relay-adapter.service.js +350 -0
- package/dist/backend/backend/src/services/browser/browser-relay-adapter.service.js.map +1 -0
- package/dist/backend/backend/src/services/cloud/cloud-initializer.d.ts +8 -0
- package/dist/backend/backend/src/services/cloud/cloud-initializer.d.ts.map +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-initializer.js +27 -0
- package/dist/backend/backend/src/services/cloud/cloud-initializer.js.map +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-sync.types.d.ts +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-sync.types.d.ts.map +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-sync.types.js +2 -0
- package/dist/backend/backend/src/services/cloud/cloud-sync.types.js.map +1 -1
- package/dist/backend/backend/src/services/core/team-export.service.d.ts +103 -0
- package/dist/backend/backend/src/services/core/team-export.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/core/team-export.service.js +182 -0
- package/dist/backend/backend/src/services/core/team-export.service.js.map +1 -0
- package/dist/backend/backend/src/services/data/data-object-store.service.d.ts +160 -0
- package/dist/backend/backend/src/services/data/data-object-store.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/data/data-object-store.service.js +434 -0
- package/dist/backend/backend/src/services/data/data-object-store.service.js.map +1 -0
- package/dist/backend/backend/src/services/data/data-object.types.d.ts +190 -0
- package/dist/backend/backend/src/services/data/data-object.types.d.ts.map +1 -0
- package/dist/backend/backend/src/services/data/data-object.types.js +143 -0
- package/dist/backend/backend/src/services/data/data-object.types.js.map +1 -0
- package/dist/backend/backend/src/services/data/schema-registry.service.d.ts +108 -0
- package/dist/backend/backend/src/services/data/schema-registry.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/data/schema-registry.service.js +290 -0
- package/dist/backend/backend/src/services/data/schema-registry.service.js.map +1 -0
- package/dist/backend/backend/src/services/data/sink-registry.service.d.ts +87 -0
- package/dist/backend/backend/src/services/data/sink-registry.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/data/sink-registry.service.js +188 -0
- package/dist/backend/backend/src/services/data/sink-registry.service.js.map +1 -0
- package/dist/backend/backend/src/services/messaging/message-router.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/messaging/message-router.service.js +7 -0
- package/dist/backend/backend/src/services/messaging/message-router.service.js.map +1 -1
- package/dist/backend/backend/src/services/monitoring/token-usage.service.d.ts +55 -2
- package/dist/backend/backend/src/services/monitoring/token-usage.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/monitoring/token-usage.service.js +89 -5
- package/dist/backend/backend/src/services/monitoring/token-usage.service.js.map +1 -1
- package/dist/backend/backend/src/services/session/pty/pty-session-backend.js +1 -1
- package/dist/backend/backend/src/services/session/pty/pty-session-backend.js.map +1 -1
- package/dist/backend/backend/src/services/workflow/cron-task.service.d.ts +105 -14
- package/dist/backend/backend/src/services/workflow/cron-task.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/workflow/cron-task.service.js +400 -123
- package/dist/backend/backend/src/services/workflow/cron-task.service.js.map +1 -1
- package/dist/backend/backend/src/types/cron-task.types.d.ts +1 -1
- package/dist/backend/backend/src/types/data-object.types.d.ts +117 -0
- package/dist/backend/backend/src/types/data-object.types.d.ts.map +1 -0
- package/dist/backend/backend/src/types/data-object.types.js +23 -0
- package/dist/backend/backend/src/types/data-object.types.js.map +1 -0
- package/dist/backend/backend/src/types/settings.types.js +1 -1
- package/dist/backend/config/constants.d.ts +3 -3
- package/dist/backend/config/constants.js +3 -3
- package/dist/backend/config/constants.js.map +1 -1
- package/dist/cli/backend/src/constants.d.ts +7 -4
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +6 -3
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/types/settings.types.js +1 -1
- package/dist/cli/config/constants.d.ts +3 -3
- package/dist/cli/config/constants.js +3 -3
- package/dist/cli/config/constants.js.map +1 -1
- package/frontend/dist/assets/index-371b68d4.css +33 -0
- package/frontend/dist/assets/{index-9af2ea40.js → index-506f70da.js} +321 -321
- package/frontend/dist/index.html +2 -2
- package/package.json +1 -1
- package/frontend/dist/assets/index-b19b2478.css +0 -33
|
@@ -5,19 +5,27 @@
|
|
|
5
5
|
* Unlike scheduled messages (agent-created/cancellable), cron tasks
|
|
6
6
|
* can only be cancelled by user or orchestrator.
|
|
7
7
|
*
|
|
8
|
-
* Storage: ~/.crewly/cron-tasks.json
|
|
8
|
+
* Storage: Per-team at ~/.crewly/teams/{teamId}/cron-tasks.json
|
|
9
|
+
* Migration: Global ~/.crewly/cron-tasks.json is auto-migrated on first startup.
|
|
9
10
|
*
|
|
10
11
|
* @module services/workflow/cron-task.service
|
|
11
12
|
*/
|
|
12
13
|
import * as os from 'os';
|
|
13
14
|
import * as path from 'path';
|
|
14
|
-
import {
|
|
15
|
+
import { existsSync } from 'fs';
|
|
16
|
+
import { readFile, writeFile, mkdir, readdir } from 'fs/promises';
|
|
15
17
|
import { v4 as uuidv4 } from 'uuid';
|
|
16
18
|
import { LoggerService } from '../core/logger.service.js';
|
|
17
19
|
/**
|
|
18
20
|
* Check interval for cron task evaluation (60 seconds).
|
|
19
21
|
*/
|
|
20
22
|
const CRON_CHECK_INTERVAL_MS = 60_000;
|
|
23
|
+
/** Name of the per-team cron task file */
|
|
24
|
+
const CRON_TASKS_FILENAME = 'cron-tasks.json';
|
|
25
|
+
/** Suffix appended to the global file after successful migration */
|
|
26
|
+
const MIGRATED_SUFFIX = '.migrated';
|
|
27
|
+
/** Team ID value used for orchestrator-level cron tasks (not team-scoped) */
|
|
28
|
+
const ORCHESTRATOR_TEAM_ID = 'orchestrator';
|
|
21
29
|
/**
|
|
22
30
|
* Extract date/time parts from a Date in a specific IANA timezone.
|
|
23
31
|
*
|
|
@@ -153,26 +161,35 @@ export function getNextRunTime(cronExpression, timezone, after) {
|
|
|
153
161
|
return new Date(now.getTime() + 24 * 60 * 60 * 1000).toISOString();
|
|
154
162
|
}
|
|
155
163
|
/**
|
|
156
|
-
* Service for managing cron tasks.
|
|
164
|
+
* Service for managing cron tasks with per-team scoped storage.
|
|
165
|
+
*
|
|
166
|
+
* Storage layout:
|
|
167
|
+
* ~/.crewly/teams/{teamId}/cron-tasks.json
|
|
168
|
+
*
|
|
169
|
+
* On first startup, migrates the legacy global file
|
|
170
|
+
* (~/.crewly/cron-tasks.json) by grouping tasks by targetTeamId
|
|
171
|
+
* and writing per-team files. The global file is renamed to
|
|
172
|
+
* cron-tasks.json.migrated to prevent re-migration.
|
|
157
173
|
*
|
|
158
174
|
* Lifecycle:
|
|
159
175
|
* 1. Get singleton via `getInstance()`
|
|
160
176
|
* 2. Wire callbacks: `setExecutionCallback()`, `setAgentStatusCallback()`, `setAgentStartCallback()`
|
|
161
|
-
* 3. Call `
|
|
162
|
-
* 4.
|
|
177
|
+
* 3. Call `migrateGlobalToTeamScoped()` to auto-migrate legacy storage
|
|
178
|
+
* 4. Call `start()` to begin the evaluation loop
|
|
179
|
+
* 5. On startup, call `recalculateAllNextRunTimes()` to self-heal stale values
|
|
163
180
|
*/
|
|
164
181
|
export class CronTaskService {
|
|
165
182
|
static instance = null;
|
|
166
183
|
logger;
|
|
167
|
-
|
|
184
|
+
/** Root crewly home directory (e.g. ~/.crewly) */
|
|
185
|
+
crewlyHome;
|
|
168
186
|
timer = null;
|
|
169
187
|
executionCallback = null;
|
|
170
188
|
agentStatusCallback = null;
|
|
171
189
|
agentStartCallback = null;
|
|
172
190
|
constructor(crewlyHome) {
|
|
173
191
|
this.logger = LoggerService.getInstance().createComponentLogger('CronTaskService');
|
|
174
|
-
|
|
175
|
-
this.storeFile = path.join(home, 'cron-tasks.json');
|
|
192
|
+
this.crewlyHome = crewlyHome || path.join(os.homedir(), '.crewly');
|
|
176
193
|
}
|
|
177
194
|
/**
|
|
178
195
|
* Get singleton instance.
|
|
@@ -192,6 +209,98 @@ export class CronTaskService {
|
|
|
192
209
|
}
|
|
193
210
|
CronTaskService.instance = null;
|
|
194
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* Get the store file path for a specific team.
|
|
214
|
+
*
|
|
215
|
+
* @param teamId - Team ID
|
|
216
|
+
* @returns Absolute path to the team's cron-tasks.json
|
|
217
|
+
*/
|
|
218
|
+
getStoreFile(teamId) {
|
|
219
|
+
return path.join(this.crewlyHome, 'teams', teamId, CRON_TASKS_FILENAME);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Get the legacy global store file path.
|
|
223
|
+
*
|
|
224
|
+
* @returns Absolute path to the global cron-tasks.json
|
|
225
|
+
*/
|
|
226
|
+
getGlobalStoreFile() {
|
|
227
|
+
return path.join(this.crewlyHome, CRON_TASKS_FILENAME);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Migrate legacy global cron-tasks.json to per-team files.
|
|
231
|
+
*
|
|
232
|
+
* Orchestrator-targeted tasks (targetTeamId === 'orchestrator') are kept
|
|
233
|
+
* in the global file since they don't belong to any specific team.
|
|
234
|
+
* All other tasks are grouped by targetTeamId and written to per-team files.
|
|
235
|
+
*
|
|
236
|
+
* After migration, the global file is rewritten to contain only orchestrator
|
|
237
|
+
* tasks, and a .migrated marker is created to prevent re-running.
|
|
238
|
+
*
|
|
239
|
+
* Idempotent: skips if the .migrated marker already exists or no global file exists.
|
|
240
|
+
*
|
|
241
|
+
* @returns Number of tasks migrated to per-team files (excludes orchestrator tasks kept in global)
|
|
242
|
+
*/
|
|
243
|
+
async migrateGlobalToTeamScoped() {
|
|
244
|
+
const globalFile = this.getGlobalStoreFile();
|
|
245
|
+
const migratedFile = globalFile + MIGRATED_SUFFIX;
|
|
246
|
+
// Skip if already migrated or no global file exists
|
|
247
|
+
if (existsSync(migratedFile) || !existsSync(globalFile)) {
|
|
248
|
+
return 0;
|
|
249
|
+
}
|
|
250
|
+
let globalStore;
|
|
251
|
+
try {
|
|
252
|
+
const data = await readFile(globalFile, 'utf-8');
|
|
253
|
+
globalStore = JSON.parse(data);
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
// Cannot read/parse global file — nothing to migrate
|
|
257
|
+
return 0;
|
|
258
|
+
}
|
|
259
|
+
if (globalStore.tasks.length === 0) {
|
|
260
|
+
// Empty store — write marker and skip
|
|
261
|
+
await writeFile(migratedFile, '', 'utf-8');
|
|
262
|
+
return 0;
|
|
263
|
+
}
|
|
264
|
+
// Partition: orchestrator tasks stay global, everything else goes per-team
|
|
265
|
+
const orchestratorTasks = [];
|
|
266
|
+
const teamGroups = new Map();
|
|
267
|
+
for (const task of globalStore.tasks) {
|
|
268
|
+
if (task.targetTeamId === ORCHESTRATOR_TEAM_ID) {
|
|
269
|
+
orchestratorTasks.push(task);
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
const teamId = task.targetTeamId;
|
|
273
|
+
if (!teamGroups.has(teamId)) {
|
|
274
|
+
teamGroups.set(teamId, []);
|
|
275
|
+
}
|
|
276
|
+
teamGroups.get(teamId).push(task);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// Write team-scoped tasks to per-team files
|
|
280
|
+
let migratedCount = 0;
|
|
281
|
+
for (const [teamId, tasks] of teamGroups) {
|
|
282
|
+
const teamStore = await this.loadTeamStore(teamId);
|
|
283
|
+
// Merge: add migrated tasks that don't already exist
|
|
284
|
+
const existingIds = new Set(teamStore.tasks.map(t => t.id));
|
|
285
|
+
for (const task of tasks) {
|
|
286
|
+
if (!existingIds.has(task.id)) {
|
|
287
|
+
teamStore.tasks.push(task);
|
|
288
|
+
migratedCount++;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
await this.saveTeamStore(teamId, teamStore);
|
|
292
|
+
}
|
|
293
|
+
// Rewrite global file with only orchestrator tasks
|
|
294
|
+
await this.saveGlobalStore({ tasks: orchestratorTasks });
|
|
295
|
+
// Write migration marker to prevent re-running
|
|
296
|
+
await writeFile(migratedFile, new Date().toISOString(), 'utf-8');
|
|
297
|
+
this.logger.info('Migrated global cron-tasks.json to per-team storage', {
|
|
298
|
+
migratedCount,
|
|
299
|
+
orchestratorKept: orchestratorTasks.length,
|
|
300
|
+
teamCount: teamGroups.size,
|
|
301
|
+
});
|
|
302
|
+
return migratedCount;
|
|
303
|
+
}
|
|
195
304
|
/**
|
|
196
305
|
* Set the callback invoked when a cron task is due for execution.
|
|
197
306
|
* The callback receives the task and should delegate it to the target agent.
|
|
@@ -265,15 +374,15 @@ export class CronTaskService {
|
|
|
265
374
|
* @returns Number of tasks whose nextRunAt was updated
|
|
266
375
|
*/
|
|
267
376
|
async recalculateAllNextRunTimes() {
|
|
268
|
-
const
|
|
377
|
+
const allTasks = await this.loadAllTasks();
|
|
269
378
|
const now = new Date();
|
|
270
379
|
let updated = 0;
|
|
271
|
-
|
|
380
|
+
// Group tasks back by team for saving
|
|
381
|
+
const dirtyTeams = new Set();
|
|
382
|
+
for (const task of allTasks) {
|
|
272
383
|
if (!task.enabled)
|
|
273
384
|
continue;
|
|
274
385
|
// If task has never run AND its nextRunAt is in the past, this is a missed first run.
|
|
275
|
-
// Keep the stale nextRunAt so evaluateTasks() picks it up and fires immediately,
|
|
276
|
-
// rather than silently advancing to the next occurrence.
|
|
277
386
|
if (!task.lastRunAt && task.nextRunAt && new Date(task.nextRunAt) <= now) {
|
|
278
387
|
this.logger.info('Missed first run detected — keeping stale nextRunAt for immediate execution', {
|
|
279
388
|
id: task.id,
|
|
@@ -293,10 +402,15 @@ export class CronTaskService {
|
|
|
293
402
|
});
|
|
294
403
|
task.nextRunAt = recalculated;
|
|
295
404
|
updated++;
|
|
405
|
+
dirtyTeams.add(task.targetTeamId);
|
|
296
406
|
}
|
|
297
407
|
}
|
|
298
|
-
|
|
299
|
-
|
|
408
|
+
// Save only modified team stores
|
|
409
|
+
if (dirtyTeams.size > 0) {
|
|
410
|
+
for (const teamId of dirtyTeams) {
|
|
411
|
+
const teamTasks = allTasks.filter(t => t.targetTeamId === teamId);
|
|
412
|
+
await this.saveTeamStore(teamId, { tasks: teamTasks });
|
|
413
|
+
}
|
|
300
414
|
this.logger.info('Recalculated nextRunAt for stale tasks', { count: updated });
|
|
301
415
|
}
|
|
302
416
|
return updated;
|
|
@@ -323,20 +437,45 @@ export class CronTaskService {
|
|
|
323
437
|
lastRunAt: null,
|
|
324
438
|
nextRunAt,
|
|
325
439
|
};
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
440
|
+
// Orchestrator tasks go to global store; team tasks go to per-team store
|
|
441
|
+
if (task.targetTeamId === ORCHESTRATOR_TEAM_ID) {
|
|
442
|
+
const store = await this.loadGlobalStore();
|
|
443
|
+
store.tasks.push(task);
|
|
444
|
+
await this.saveGlobalStore(store);
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
const store = await this.loadTeamStore(task.targetTeamId);
|
|
448
|
+
store.tasks.push(task);
|
|
449
|
+
await this.saveTeamStore(task.targetTeamId, store);
|
|
450
|
+
}
|
|
451
|
+
this.logger.info('Cron task created', { id: task.id, cron: task.cronExpression, target: task.targetAgent, teamId: task.targetTeamId });
|
|
330
452
|
return task;
|
|
331
453
|
}
|
|
332
454
|
/**
|
|
333
|
-
* List all cron tasks, optionally filtered.
|
|
455
|
+
* List all cron tasks across all teams, optionally filtered.
|
|
334
456
|
*
|
|
335
457
|
* @param filter - Optional filter criteria
|
|
336
458
|
* @returns Array of matching cron tasks
|
|
337
459
|
*/
|
|
338
460
|
async list(filter) {
|
|
339
|
-
|
|
461
|
+
let tasks = await this.loadAllTasks();
|
|
462
|
+
if (filter?.targetAgent) {
|
|
463
|
+
tasks = tasks.filter(t => t.targetAgent === filter.targetAgent);
|
|
464
|
+
}
|
|
465
|
+
if (filter?.enabled !== undefined) {
|
|
466
|
+
tasks = tasks.filter(t => t.enabled === filter.enabled);
|
|
467
|
+
}
|
|
468
|
+
return tasks;
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* List cron tasks for a specific team.
|
|
472
|
+
*
|
|
473
|
+
* @param teamId - Team ID to filter by
|
|
474
|
+
* @param filter - Optional additional filter criteria
|
|
475
|
+
* @returns Array of matching cron tasks for the team
|
|
476
|
+
*/
|
|
477
|
+
async getByTeam(teamId, filter) {
|
|
478
|
+
const store = await this.loadTeamStore(teamId);
|
|
340
479
|
let tasks = store.tasks;
|
|
341
480
|
if (filter?.targetAgent) {
|
|
342
481
|
tasks = tasks.filter(t => t.targetAgent === filter.targetAgent);
|
|
@@ -347,61 +486,92 @@ export class CronTaskService {
|
|
|
347
486
|
return tasks;
|
|
348
487
|
}
|
|
349
488
|
/**
|
|
350
|
-
* Get a single cron task by ID.
|
|
489
|
+
* Get a single cron task by ID. Searches across all teams.
|
|
351
490
|
*
|
|
352
491
|
* @param id - Cron task ID
|
|
353
492
|
* @returns The cron task or null
|
|
354
493
|
*/
|
|
355
494
|
async get(id) {
|
|
356
|
-
const
|
|
357
|
-
return
|
|
495
|
+
const allTasks = await this.loadAllTasks();
|
|
496
|
+
return allTasks.find(t => t.id === id) || null;
|
|
358
497
|
}
|
|
359
498
|
/**
|
|
360
|
-
* Update a cron task.
|
|
361
|
-
* but cannot delete.
|
|
499
|
+
* Update a cron task. Searches across all teams to find the task.
|
|
362
500
|
*
|
|
363
501
|
* @param id - Cron task ID
|
|
364
502
|
* @param updates - Fields to update
|
|
365
503
|
* @returns Updated task or null if not found
|
|
366
504
|
*/
|
|
367
505
|
async update(id, updates) {
|
|
368
|
-
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
await this.
|
|
387
|
-
|
|
388
|
-
|
|
506
|
+
// Helper to apply updates to a task
|
|
507
|
+
const applyUpdates = (task) => {
|
|
508
|
+
if (updates.cronExpression !== undefined) {
|
|
509
|
+
task.cronExpression = updates.cronExpression;
|
|
510
|
+
task.nextRunAt = getNextRunTime(updates.cronExpression, task.timezone);
|
|
511
|
+
}
|
|
512
|
+
if (updates.timezone !== undefined) {
|
|
513
|
+
task.timezone = updates.timezone;
|
|
514
|
+
task.nextRunAt = getNextRunTime(task.cronExpression, task.timezone);
|
|
515
|
+
}
|
|
516
|
+
if (updates.taskDescription !== undefined) {
|
|
517
|
+
task.taskDescription = updates.taskDescription;
|
|
518
|
+
}
|
|
519
|
+
if (updates.enabled !== undefined) {
|
|
520
|
+
task.enabled = updates.enabled;
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
// Search global store first (orchestrator tasks)
|
|
524
|
+
const globalStore = await this.loadGlobalStore();
|
|
525
|
+
const globalTask = globalStore.tasks.find(t => t.id === id);
|
|
526
|
+
if (globalTask) {
|
|
527
|
+
applyUpdates(globalTask);
|
|
528
|
+
await this.saveGlobalStore(globalStore);
|
|
529
|
+
this.logger.info('Cron task updated', { id, location: 'global', updates: Object.keys(updates) });
|
|
530
|
+
return globalTask;
|
|
531
|
+
}
|
|
532
|
+
// Search per-team stores
|
|
533
|
+
const teamIds = await this.getTeamIds();
|
|
534
|
+
for (const teamId of teamIds) {
|
|
535
|
+
const store = await this.loadTeamStore(teamId);
|
|
536
|
+
const task = store.tasks.find(t => t.id === id);
|
|
537
|
+
if (!task)
|
|
538
|
+
continue;
|
|
539
|
+
applyUpdates(task);
|
|
540
|
+
await this.saveTeamStore(teamId, store);
|
|
541
|
+
this.logger.info('Cron task updated', { id, teamId, updates: Object.keys(updates) });
|
|
542
|
+
return task;
|
|
543
|
+
}
|
|
544
|
+
return null;
|
|
389
545
|
}
|
|
390
546
|
/**
|
|
391
|
-
* Delete a cron task.
|
|
547
|
+
* Delete a cron task. Searches across all teams to find and remove it.
|
|
392
548
|
*
|
|
393
549
|
* @param id - Cron task ID
|
|
394
550
|
* @returns true if deleted, false if not found
|
|
395
551
|
*/
|
|
396
552
|
async delete(id) {
|
|
397
|
-
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
553
|
+
// Search global store first (orchestrator tasks)
|
|
554
|
+
const globalStore = await this.loadGlobalStore();
|
|
555
|
+
const globalIndex = globalStore.tasks.findIndex(t => t.id === id);
|
|
556
|
+
if (globalIndex !== -1) {
|
|
557
|
+
globalStore.tasks.splice(globalIndex, 1);
|
|
558
|
+
await this.saveGlobalStore(globalStore);
|
|
559
|
+
this.logger.info('Cron task deleted', { id, location: 'global' });
|
|
560
|
+
return true;
|
|
561
|
+
}
|
|
562
|
+
// Search per-team stores
|
|
563
|
+
const teamIds = await this.getTeamIds();
|
|
564
|
+
for (const teamId of teamIds) {
|
|
565
|
+
const store = await this.loadTeamStore(teamId);
|
|
566
|
+
const index = store.tasks.findIndex(t => t.id === id);
|
|
567
|
+
if (index === -1)
|
|
568
|
+
continue;
|
|
569
|
+
store.tasks.splice(index, 1);
|
|
570
|
+
await this.saveTeamStore(teamId, store);
|
|
571
|
+
this.logger.info('Cron task deleted', { id, teamId });
|
|
572
|
+
return true;
|
|
573
|
+
}
|
|
574
|
+
return false;
|
|
405
575
|
}
|
|
406
576
|
/**
|
|
407
577
|
* Evaluate all enabled tasks and execute those whose nextRunAt has passed.
|
|
@@ -410,95 +580,153 @@ export class CronTaskService {
|
|
|
410
580
|
* attempts to auto-start the agent before executing.
|
|
411
581
|
*/
|
|
412
582
|
async evaluateTasks() {
|
|
413
|
-
const store = await this.loadStore();
|
|
414
583
|
const now = new Date();
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
const
|
|
420
|
-
if (
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
}
|
|
584
|
+
// Evaluate global orchestrator tasks
|
|
585
|
+
const globalStore = await this.loadGlobalStore();
|
|
586
|
+
let globalUpdated = false;
|
|
587
|
+
for (const task of globalStore.tasks) {
|
|
588
|
+
const result = await this.evaluateSingleTask(task, now);
|
|
589
|
+
if (result)
|
|
590
|
+
globalUpdated = true;
|
|
591
|
+
}
|
|
592
|
+
if (globalUpdated) {
|
|
593
|
+
await this.saveGlobalStore(globalStore);
|
|
594
|
+
}
|
|
595
|
+
// Evaluate per-team tasks
|
|
596
|
+
const teamIds = await this.getTeamIds();
|
|
597
|
+
for (const teamId of teamIds) {
|
|
598
|
+
const store = await this.loadTeamStore(teamId);
|
|
599
|
+
let updated = false;
|
|
600
|
+
for (const task of store.tasks) {
|
|
601
|
+
const result = await this.evaluateSingleTask(task, now);
|
|
602
|
+
if (result)
|
|
603
|
+
updated = true;
|
|
436
604
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
605
|
+
if (updated) {
|
|
606
|
+
await this.saveTeamStore(teamId, store);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Evaluate a single cron task: check if due, verify agent status, execute.
|
|
612
|
+
* Mutates the task in-place (lastRunAt, nextRunAt).
|
|
613
|
+
*
|
|
614
|
+
* @param task - The cron task to evaluate
|
|
615
|
+
* @param now - Current time for comparison
|
|
616
|
+
* @returns true if the task was modified (needs saving)
|
|
617
|
+
*/
|
|
618
|
+
async evaluateSingleTask(task, now) {
|
|
619
|
+
if (!task.enabled || !task.nextRunAt)
|
|
620
|
+
return false;
|
|
621
|
+
const nextRun = new Date(task.nextRunAt);
|
|
622
|
+
if (nextRun > now)
|
|
623
|
+
return false;
|
|
624
|
+
// Task is due — check agent status before executing
|
|
625
|
+
let agentOnline = true;
|
|
626
|
+
if (this.agentStatusCallback) {
|
|
627
|
+
try {
|
|
628
|
+
agentOnline = await this.agentStatusCallback(task.targetAgent, task.targetTeamId);
|
|
460
629
|
}
|
|
461
|
-
|
|
462
|
-
this.logger.warn('
|
|
630
|
+
catch (error) {
|
|
631
|
+
this.logger.warn('Agent status check failed, assuming offline', {
|
|
463
632
|
id: task.id,
|
|
464
633
|
target: task.targetAgent,
|
|
634
|
+
error: error instanceof Error ? error.message : String(error),
|
|
465
635
|
});
|
|
466
|
-
|
|
467
|
-
task.nextRunAt = getNextRunTime(task.cronExpression, task.timezone, now);
|
|
468
|
-
updated = true;
|
|
469
|
-
continue;
|
|
636
|
+
agentOnline = false;
|
|
470
637
|
}
|
|
471
|
-
|
|
638
|
+
}
|
|
639
|
+
// Auto-start offline agent if callback is available
|
|
640
|
+
if (!agentOnline && this.agentStartCallback) {
|
|
641
|
+
this.logger.info('Agent offline for cron task, attempting auto-start', {
|
|
472
642
|
id: task.id,
|
|
473
643
|
target: task.targetAgent,
|
|
474
|
-
nextRunAt: task.nextRunAt,
|
|
475
644
|
});
|
|
476
645
|
try {
|
|
477
|
-
|
|
478
|
-
|
|
646
|
+
const started = await this.agentStartCallback(task.targetAgent, task.targetTeamId);
|
|
647
|
+
if (started) {
|
|
648
|
+
agentOnline = true;
|
|
649
|
+
this.logger.info('Agent auto-started successfully', {
|
|
650
|
+
id: task.id,
|
|
651
|
+
target: task.targetAgent,
|
|
652
|
+
});
|
|
479
653
|
}
|
|
480
654
|
}
|
|
481
655
|
catch (error) {
|
|
482
|
-
this.logger.error('
|
|
656
|
+
this.logger.error('Agent auto-start failed', {
|
|
483
657
|
id: task.id,
|
|
658
|
+
target: task.targetAgent,
|
|
484
659
|
error: error instanceof Error ? error.message : String(error),
|
|
485
660
|
});
|
|
486
661
|
}
|
|
487
|
-
|
|
488
|
-
|
|
662
|
+
}
|
|
663
|
+
if (!agentOnline) {
|
|
664
|
+
this.logger.warn('Skipping cron task — agent offline and auto-start unavailable', {
|
|
665
|
+
id: task.id,
|
|
666
|
+
target: task.targetAgent,
|
|
667
|
+
});
|
|
668
|
+
// Still advance nextRunAt to prevent re-evaluating the same missed slot
|
|
489
669
|
task.nextRunAt = getNextRunTime(task.cronExpression, task.timezone, now);
|
|
490
|
-
|
|
670
|
+
return true;
|
|
671
|
+
}
|
|
672
|
+
this.logger.info('Cron task due, executing', {
|
|
673
|
+
id: task.id,
|
|
674
|
+
target: task.targetAgent,
|
|
675
|
+
nextRunAt: task.nextRunAt,
|
|
676
|
+
});
|
|
677
|
+
try {
|
|
678
|
+
if (this.executionCallback) {
|
|
679
|
+
await this.executionCallback(task);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
catch (error) {
|
|
683
|
+
this.logger.error('Cron task execution failed', {
|
|
684
|
+
id: task.id,
|
|
685
|
+
error: error instanceof Error ? error.message : String(error),
|
|
686
|
+
});
|
|
491
687
|
}
|
|
492
|
-
|
|
493
|
-
|
|
688
|
+
// Update run times regardless of execution success
|
|
689
|
+
task.lastRunAt = now.toISOString();
|
|
690
|
+
task.nextRunAt = getNextRunTime(task.cronExpression, task.timezone, now);
|
|
691
|
+
return true;
|
|
692
|
+
}
|
|
693
|
+
// ============ Storage Methods ============
|
|
694
|
+
/**
|
|
695
|
+
* Load cron task store for a specific team.
|
|
696
|
+
*
|
|
697
|
+
* @param teamId - Team ID
|
|
698
|
+
* @returns Team's cron task store (empty if file doesn't exist)
|
|
699
|
+
*/
|
|
700
|
+
async loadTeamStore(teamId) {
|
|
701
|
+
try {
|
|
702
|
+
const filePath = this.getStoreFile(teamId);
|
|
703
|
+
const data = await readFile(filePath, 'utf-8');
|
|
704
|
+
return JSON.parse(data);
|
|
494
705
|
}
|
|
706
|
+
catch {
|
|
707
|
+
return { tasks: [] };
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Save cron task store for a specific team.
|
|
712
|
+
*
|
|
713
|
+
* @param teamId - Team ID
|
|
714
|
+
* @param store - Store data to persist
|
|
715
|
+
*/
|
|
716
|
+
async saveTeamStore(teamId, store) {
|
|
717
|
+
const filePath = this.getStoreFile(teamId);
|
|
718
|
+
const dir = path.dirname(filePath);
|
|
719
|
+
await mkdir(dir, { recursive: true });
|
|
720
|
+
await writeFile(filePath, JSON.stringify(store, null, 2), 'utf-8');
|
|
495
721
|
}
|
|
496
722
|
/**
|
|
497
|
-
* Load the cron task store
|
|
723
|
+
* Load the global cron task store (orchestrator tasks only).
|
|
724
|
+
*
|
|
725
|
+
* @returns Global store (empty if file doesn't exist)
|
|
498
726
|
*/
|
|
499
|
-
async
|
|
727
|
+
async loadGlobalStore() {
|
|
500
728
|
try {
|
|
501
|
-
const data = await readFile(this.
|
|
729
|
+
const data = await readFile(this.getGlobalStoreFile(), 'utf-8');
|
|
502
730
|
return JSON.parse(data);
|
|
503
731
|
}
|
|
504
732
|
catch {
|
|
@@ -506,12 +734,61 @@ export class CronTaskService {
|
|
|
506
734
|
}
|
|
507
735
|
}
|
|
508
736
|
/**
|
|
509
|
-
* Save the cron task store
|
|
737
|
+
* Save the global cron task store (orchestrator tasks only).
|
|
738
|
+
*
|
|
739
|
+
* @param store - Store data to persist
|
|
510
740
|
*/
|
|
511
|
-
async
|
|
512
|
-
const
|
|
741
|
+
async saveGlobalStore(store) {
|
|
742
|
+
const filePath = this.getGlobalStoreFile();
|
|
743
|
+
const dir = path.dirname(filePath);
|
|
513
744
|
await mkdir(dir, { recursive: true });
|
|
514
|
-
await writeFile(
|
|
745
|
+
await writeFile(filePath, JSON.stringify(store, null, 2), 'utf-8');
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Load all tasks from all team stores plus the global store,
|
|
749
|
+
* aggregated into a single array.
|
|
750
|
+
*
|
|
751
|
+
* @returns All cron tasks across all teams and the global orchestrator store
|
|
752
|
+
*/
|
|
753
|
+
async loadAllTasks() {
|
|
754
|
+
const teamIds = await this.getTeamIds();
|
|
755
|
+
const allTasks = [];
|
|
756
|
+
// Load orchestrator tasks from global store
|
|
757
|
+
const globalStore = await this.loadGlobalStore();
|
|
758
|
+
allTasks.push(...globalStore.tasks);
|
|
759
|
+
// Load per-team tasks
|
|
760
|
+
for (const teamId of teamIds) {
|
|
761
|
+
const store = await this.loadTeamStore(teamId);
|
|
762
|
+
allTasks.push(...store.tasks);
|
|
763
|
+
}
|
|
764
|
+
return allTasks;
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Discover all team IDs that have cron task files.
|
|
768
|
+
*
|
|
769
|
+
* Scans ~/.crewly/teams/ for directories containing cron-tasks.json.
|
|
770
|
+
*
|
|
771
|
+
* @returns Array of team IDs
|
|
772
|
+
*/
|
|
773
|
+
async getTeamIds() {
|
|
774
|
+
const teamsDir = path.join(this.crewlyHome, 'teams');
|
|
775
|
+
try {
|
|
776
|
+
const entries = await readdir(teamsDir, { withFileTypes: true });
|
|
777
|
+
const teamIds = [];
|
|
778
|
+
for (const entry of entries) {
|
|
779
|
+
if (!entry.isDirectory())
|
|
780
|
+
continue;
|
|
781
|
+
const cronFile = path.join(teamsDir, entry.name, CRON_TASKS_FILENAME);
|
|
782
|
+
if (existsSync(cronFile)) {
|
|
783
|
+
teamIds.push(entry.name);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
return teamIds;
|
|
787
|
+
}
|
|
788
|
+
catch {
|
|
789
|
+
// teams directory doesn't exist yet
|
|
790
|
+
return [];
|
|
791
|
+
}
|
|
515
792
|
}
|
|
516
793
|
}
|
|
517
794
|
//# sourceMappingURL=cron-task.service.js.map
|