@t0u9h/agent-cron 0.2.1 → 0.3.0

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 CHANGED
@@ -2,12 +2,12 @@
2
2
 
3
3
  > 中文文档:[README.zh.md](./README.zh.md)
4
4
 
5
- Run Claude Agent SDK tasks on a cron schedule. Tasks are plain `.md` files — write a prompt, set a cron expression, pick an output channel. That's it.
5
+ Run Claude Agent SDK tasks on a cron schedule. Tasks are plain `.md` files — write a prompt, set a cron expression. That's it.
6
6
 
7
7
  ```
8
8
  tasks/
9
- daily-ai-news.md ← runs at 9am, sends to Feishu
10
- weekly-report.md ← runs Monday 8am, writes to GitHub
9
+ daily-ai-news.md ← runs at 9am daily
10
+ claude-code-changelog.md ← runs at 10am daily, sends Feishu notification
11
11
  ```
12
12
 
13
13
  ## Install
@@ -32,8 +32,6 @@ Once installed:
32
32
  - "立即运行 daily-ai-news"
33
33
  - "帮我配置 agent-cron 开机自启"
34
34
 
35
- The skill automatically configures macOS auto-start (launchd) when you create your first task.
36
-
37
35
  ## Quick Start
38
36
 
39
37
  **1. Create a task file**
@@ -44,8 +42,6 @@ cat > tasks/daily-news.md << 'EOF'
44
42
  ---
45
43
  name: Daily AI News
46
44
  cron: "0 9 * * *"
47
- output: file
48
- outputDir: ./output
49
45
  ---
50
46
 
51
47
  Today is {date}. Search for the latest AI coding news and summarize in 5 bullet points.
@@ -57,8 +53,7 @@ EOF
57
53
  **2. Set your API key**
58
54
 
59
55
  ```bash
60
- cp .env.example .env
61
- # edit .env and set ANTHROPIC_API_KEY
56
+ export ANTHROPIC_API_KEY=sk-ant-...
62
57
  ```
63
58
 
64
59
  **3. Run once to test**
@@ -81,9 +76,46 @@ agent-cron start ./my-tasks # use a different tasks directory
81
76
  agent-cron run # run all tasks immediately (one-off)
82
77
  agent-cron run daily-news # run one task by slug immediately
83
78
  agent-cron list # list all registered tasks with cron expressions
79
+ agent-cron status # show last run status for all tasks
80
+ agent-cron logs <slug> # show today's log for a task
81
+ agent-cron logs <slug> <date> # show log for a specific date (YYYY-MM-DD)
84
82
  ```
85
83
 
86
- `start` is a long-running process. Use launchd (macOS), systemd (Linux), or pm2 to keep it running.
84
+ `start` is a long-running process. Use [launchd](#auto-start-on-macos) (macOS), systemd (Linux), or pm2 to keep it running.
85
+
86
+ ### status
87
+
88
+ Shows a quick overview of all task runs, including live queue state:
89
+
90
+ ```
91
+ $ agent-cron status
92
+
93
+ TASK LAST RUN STATUS DURATION
94
+ --------------------------------------------------------------
95
+ ai-news-agent-reach → running - -
96
+ github-ai-projects → queued (#1) - -
97
+ claude-code-changelog 今天 00:00 ok 45s
98
+ ```
99
+
100
+ ### logs
101
+
102
+ Prints the structured log for a task. Falls back to the most recent log if today has none.
103
+
104
+ ```
105
+ $ agent-cron logs daily-ai-news
106
+
107
+ [2026-03-07 09:00:01.123] [START] task=daily-ai-news
108
+ [2026-03-07 09:00:03.456] [TOOL] name=web_search input={"query":"AI coding news"}
109
+ [2026-03-07 09:00:10.000] [END] status=ok duration=8877ms
110
+ ```
111
+
112
+ ## Task Queue
113
+
114
+ Tasks triggered at the same cron time are executed **serially** in filename (slug) order — not concurrently. This prevents silent failures from parallel execution.
115
+
116
+ - If the same task is already running or queued, duplicate triggers are skipped
117
+ - `agent-cron status` shows `→ running` and `→ queued (#N)` for live queue state
118
+ - `agent-cron run` bypasses the queue and runs tasks directly (for manual one-off use)
87
119
 
88
120
  ## Task File Format
89
121
 
@@ -93,8 +125,8 @@ Each task is a `.md` file in your tasks directory:
93
125
  ---
94
126
  name: Daily AI News # display name (default: filename slug)
95
127
  cron: "0 9 * * *" # cron expression (Asia/Shanghai timezone)
96
- output: feishu # output channel: file | feishu | github
97
- feishuWebhook: https://... # channel-specific config (see below)
128
+ agent: claude # agent runner (default: claude)
129
+ skills: true # load ~/.claude/ skills (set false to isolate)
98
130
  ---
99
131
 
100
132
  Today is {date}. Search for the latest AI news...
@@ -108,58 +140,54 @@ If nothing to report, output exactly: HEARTBEAT_OK
108
140
  |-------|----------|---------|-------|
109
141
  | `name` | no | filename slug | display name |
110
142
  | `cron` | **yes** | — | standard 5-field cron expression |
111
- | `output` | **yes** | | `file`, `feishu`, or `github` |
112
- | `agent` | no | `claude` | agent runner (currently only `claude`) |
113
- | `skills` | no | `true` | load `~/.claude/` skills (set `false` to isolate) |
114
-
115
- ### Output channel fields
116
-
117
- **`output: file`**
118
-
119
- | Field | Required | Default |
120
- |-------|----------|---------|
121
- | `outputDir` | no | `./output` |
143
+ | `agent` | no | `claude` | agent runner (`claude` or `shell`) |
144
+ | `skills` | no | `true` | load `~/.claude/` skills, or `["skill-name"]` for specific ones |
122
145
 
123
- Writes `{outputDir}/{slug}-{YYYY-MM-DD}.md`.
146
+ ### Template variables
124
147
 
125
- **`output: feishu`**
148
+ | Variable | Value |
149
+ |----------|-------|
150
+ | `{date}` | today's date in `YYYY-MM-DD` format (e.g. `2026-03-07`) |
126
151
 
127
- | Field | Required | Default |
128
- |-------|----------|---------|
129
- | `feishuWebhook` | no | `FEISHU_WEBHOOK` env |
152
+ ### Prompt-driven output
130
153
 
131
- Converts Markdown to Feishu rich text (post format). h1 becomes the card title.
154
+ There is no built-in output channel system. Instead, write the output logic directly in your prompt — Claude will execute it. This is more flexible than any abstraction:
132
155
 
133
- **`output: github`**
156
+ ````markdown
157
+ ---
158
+ name: Claude Code Changelog
159
+ cron: "0 10 * * *"
160
+ ---
134
161
 
135
- | Field | Required | Default |
136
- |-------|----------|---------|
137
- | `githubRepo` | **yes** | — |
138
- | `githubBranch` | no | `main` |
139
- | `githubDir` | no | repo root |
140
- | `githubToken` | no | `GITHUB_TOKEN` env |
162
+ Search for Claude Code release notes...
141
163
 
142
- Creates/updates `{githubDir}/{slug}-{YYYY-MM-DD}.md` via GitHub Contents API. No local git needed.
164
+ If new version found:
143
165
 
144
- ### Template variables
166
+ 1. Write summary to `~/workspace/memory/changelog/{date}.md`
167
+ 2. Git commit and push
168
+ 3. Send Feishu notification:
169
+ ```bash
170
+ curl -X POST "$FEISHU_WEBHOOK" \
171
+ -H "Content-Type: application/json" \
172
+ -d '{"msg_type":"text","content":{"text":"New version found!"}}'
173
+ ```
145
174
 
146
- | Variable | Value |
147
- |----------|-------|
148
- | `{date}` | today's date, locale `zh-CN` (e.g. `2026/3/3`) |
175
+ If nothing new, output: HEARTBEAT_OK
176
+ ````
149
177
 
150
178
  ### HEARTBEAT_OK protocol
151
179
 
152
- If the agent returns exactly `HEARTBEAT_OK` (trimmed), the output step is skipped silently. Use this when a task should only push when there is genuinely new content:
153
-
154
- ```
155
- If nothing new to report, output exactly: HEARTBEAT_OK
156
- ```
180
+ If the agent returns exactly `HEARTBEAT_OK` (trimmed), the task is considered a no-op — logged as `heartbeat` status. Use this when a task should only act when there is genuinely new content.
157
181
 
158
182
  ## Local Skills
159
183
 
160
- By default, all tasks load your locally installed Claude Code skills (`~/.claude/plugins/`, `~/.claude/skills/`) via `settingSources: ['user']`. This means any skill installed on your machine is available inside the agent's session.
184
+ By default, all tasks load your locally installed Claude Code skills (`~/.claude/plugins/`, `~/.claude/skills/`). To load specific skills only:
161
185
 
162
- To disable for a specific task (isolation, security):
186
+ ```yaml
187
+ skills: ["agent-reach", "my-skill"]
188
+ ```
189
+
190
+ To disable for a specific task (isolation):
163
191
 
164
192
  ```yaml
165
193
  skills: false
@@ -169,22 +197,41 @@ skills: false
169
197
 
170
198
  ```
171
199
  ANTHROPIC_API_KEY= # required
172
- GITHUB_TOKEN= # required for output: github (unless set per-task)
173
- FEISHU_WEBHOOK= # required for output: feishu (unless set per-task)
174
200
  ```
175
201
 
176
- Copy `.env.example` to `.env`. A `.env` file in the current working directory is loaded automatically at startup.
202
+ A `.env` file in the current working directory is loaded automatically at startup.
177
203
 
178
204
  ## Timezone
179
205
 
180
- All cron expressions run in `Asia/Shanghai` timezone. This is hardcoded in v0.1.
206
+ All cron expressions run in `Asia/Shanghai` timezone.
207
+
208
+ ## Logging
209
+
210
+ Each task run writes structured events to `~/.agent-cron/logs/<slug>/YYYY-MM-DD.log`:
211
+
212
+ - `[START]` — task begins
213
+ - `[TOOL]` — each agent tool call (name, input, truncated output)
214
+ - `[END]` — task ends with status (`ok`, `heartbeat`, or `error`) and duration
215
+
216
+ Use `agent-cron status` and `agent-cron logs <slug>` to inspect.
181
217
 
182
218
  ## Auto-start on macOS
183
219
 
184
- To run `agent-cron start` as a background service that survives reboots:
220
+ Create a shell wrapper that loads your environment:
221
+
222
+ ```bash
223
+ cat > /path/to/agent-cron/start.sh << 'EOF'
224
+ #!/bin/zsh
225
+ source ~/.zshrc 2>/dev/null
226
+ exec node /path/to/agent-cron/node_modules/.bin/tsx \
227
+ /path/to/agent-cron/src/cli.ts start /path/to/tasks
228
+ EOF
229
+ chmod +x /path/to/agent-cron/start.sh
230
+ ```
231
+
232
+ Then create the LaunchAgent:
185
233
 
186
234
  ```bash
187
- # Create LaunchAgent plist
188
235
  cat > ~/Library/LaunchAgents/com.agent-cron.plist << 'EOF'
189
236
  <?xml version="1.0" encoding="UTF-8"?>
190
237
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -193,16 +240,10 @@ cat > ~/Library/LaunchAgents/com.agent-cron.plist << 'EOF'
193
240
  <key>Label</key><string>com.agent-cron</string>
194
241
  <key>ProgramArguments</key>
195
242
  <array>
196
- <string>/usr/local/bin/npx</string>
197
- <string>@t0u9h/agent-cron</string>
198
- <string>start</string>
199
- <string>/path/to/your/tasks</string>
243
+ <string>/bin/zsh</string>
244
+ <string>/path/to/agent-cron/start.sh</string>
200
245
  </array>
201
- <key>EnvironmentVariables</key>
202
- <dict>
203
- <key>ANTHROPIC_API_KEY</key><string>sk-ant-...</string>
204
- </dict>
205
- <key>WorkingDirectory</key><string>/path/to/your/project</string>
246
+ <key>WorkingDirectory</key><string>/path/to/agent-cron</string>
206
247
  <key>RunAtLoad</key><true/>
207
248
  <key>KeepAlive</key><true/>
208
249
  <key>StandardOutPath</key><string>/tmp/agent-cron.log</string>
@@ -211,35 +252,29 @@ cat > ~/Library/LaunchAgents/com.agent-cron.plist << 'EOF'
211
252
  </plist>
212
253
  EOF
213
254
 
214
- # Load immediately (no reboot required)
255
+ # Load immediately
215
256
  launchctl load ~/Library/LaunchAgents/com.agent-cron.plist
216
257
 
217
- # Verify running
258
+ # Verify
218
259
  launchctl list | grep agent-cron
219
260
  ```
220
261
 
262
+ The shell wrapper (`start.sh`) ensures environment variables like `ANTHROPIC_API_KEY` are available — launchd does not inherit your shell environment.
263
+
221
264
  Or use the Claude Code skill — it handles all of this automatically.
222
265
 
223
266
  ## Architecture
224
267
 
225
268
  ```
226
- tasks/*.md → loader.ts → runner.ts → AgentRunner → OutputChannel
227
- (claude) (file/feishu/github)
228
- ```
229
-
230
- **Pluggable output channels** — implement `OutputChannel` and register in `src/outputs/index.ts`:
231
-
232
- ```typescript
233
- interface OutputChannel {
234
- send(result: string, task: Task): Promise<void>
235
- }
269
+ tasks/*.md → loader.ts → scheduler.ts → queue.ts → runner.ts → AgentRunner → Logger
270
+ (serial) agents/ ~/.agent-cron/logs/
236
271
  ```
237
272
 
238
273
  **Pluggable agent runners** — implement `AgentRunner` and register in `src/agents/index.ts`:
239
274
 
240
275
  ```typescript
241
276
  interface AgentRunner {
242
- run(prompt: string, task: Task): Promise<string>
277
+ run(prompt: string, task: Task, logger?: Logger): Promise<string>
243
278
  }
244
279
  ```
245
280
 
@@ -249,6 +284,6 @@ interface AgentRunner {
249
284
  git clone https://github.com/T0UGH/agent-cron
250
285
  cd agent-cron
251
286
  npm install
252
- npm test # run tests (39 tests, node:test)
287
+ npm test # run tests (node:test)
253
288
  npm run build # compile TypeScript → dist/
254
289
  ```
package/dist/cli.js CHANGED
@@ -2,7 +2,8 @@
2
2
  import 'dotenv/config';
3
3
  import path from 'path';
4
4
  import { loadTasks } from './loader.js';
5
- import { startScheduler, runNow, listTasks } from './scheduler.js';
5
+ import { startScheduler, runNow, listTasks, taskQueue } from './scheduler.js';
6
+ import { statusAll, logsFor } from './status.js';
6
7
  const args = process.argv.slice(2);
7
8
  const command = args[0];
8
9
  // Resolve tasks directory: default ./tasks, or explicit argument after command
@@ -23,6 +24,18 @@ switch (command) {
23
24
  case 'list':
24
25
  listTasks(tasks);
25
26
  break;
27
+ case 'status':
28
+ statusAll(tasks, taskQueue.getState());
29
+ break;
30
+ case 'logs': {
31
+ const slug = args[1];
32
+ if (!slug) {
33
+ console.error('Usage: agent-cron logs <slug> [date]');
34
+ process.exit(1);
35
+ }
36
+ logsFor(slug, args[2]);
37
+ break;
38
+ }
26
39
  default:
27
40
  console.log(`
28
41
  agent-cron — run Claude Agent SDK tasks on a cron schedule
@@ -32,6 +45,8 @@ Usage:
32
45
  agent-cron start ./my-tasks Specify tasks directory
33
46
  agent-cron run [slug] Run all tasks or one by slug immediately
34
47
  agent-cron list List all registered tasks
48
+ agent-cron status Show last run status for all tasks
49
+ agent-cron logs <slug> [date] Show logs for a task (date: YYYY-MM-DD, default: today)
35
50
 
36
51
  Options:
37
52
  dir Path to tasks directory (default: ./tasks)
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,eAAe,CAAC;AACvB,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEnE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACnC,MAAM,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;AAExB,+EAA+E;AAC/E,oCAAoC;AACpC,uDAAuD;AACvD,MAAM,MAAM,GACV,OAAO,KAAK,OAAO,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AACnF,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,IAAI,SAAS,CAAC,CAAC;AAEnD,MAAM,KAAK,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;AAElC,QAAQ,OAAO,EAAE,CAAC;IAChB,KAAK,OAAO;QACV,cAAc,CAAC,KAAK,CAAC,CAAC;QACtB,MAAM;IAER,KAAK,KAAK,CAAC,CAAC,CAAC;QACX,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACvE,MAAM,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QAC1B,MAAM;IACR,CAAC;IAED,KAAK,MAAM;QACT,SAAS,CAAC,KAAK,CAAC,CAAC;QACjB,MAAM;IAER;QACE,OAAO,CAAC,GAAG,CAAC;;;;;;;;;;;;;;;;;CAiBf,CAAC,CAAC;QACC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAClC,CAAC"}
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,eAAe,CAAC;AACvB,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC9E,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACnC,MAAM,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;AAExB,+EAA+E;AAC/E,oCAAoC;AACpC,uDAAuD;AACvD,MAAM,MAAM,GACV,OAAO,KAAK,OAAO,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AACnF,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,IAAI,SAAS,CAAC,CAAC;AAEnD,MAAM,KAAK,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;AAElC,QAAQ,OAAO,EAAE,CAAC;IAChB,KAAK,OAAO;QACV,cAAc,CAAC,KAAK,CAAC,CAAC;QACtB,MAAM;IAER,KAAK,KAAK,CAAC,CAAC,CAAC;QACX,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACvE,MAAM,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QAC1B,MAAM;IACR,CAAC;IAED,KAAK,MAAM;QACT,SAAS,CAAC,KAAK,CAAC,CAAC;QACjB,MAAM;IAER,KAAK,QAAQ;QACX,SAAS,CAAC,KAAK,EAAE,SAAS,CAAC,QAAQ,EAAE,CAAC,CAAC;QACvC,MAAM;IAER,KAAK,MAAM,CAAC,CAAC,CAAC;QACZ,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QACrB,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,OAAO,CAAC,KAAK,CAAC,sCAAsC,CAAC,CAAC;YACtD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QACD,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;QACvB,MAAM;IACR,CAAC;IAED;QACE,OAAO,CAAC,GAAG,CAAC;;;;;;;;;;;;;;;;;;;CAmBf,CAAC,CAAC;QACC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAClC,CAAC"}
@@ -0,0 +1,17 @@
1
+ import type { Task } from './types.js';
2
+ export interface QueueState {
3
+ running: string | null;
4
+ queued: string[];
5
+ }
6
+ export declare class TaskQueue {
7
+ private pending;
8
+ private processing;
9
+ private runningSlug;
10
+ private emptyResolvers;
11
+ enqueue(task: Task): void;
12
+ private process;
13
+ getState(): QueueState;
14
+ /** Resolves when the queue is empty and nothing is running. For testing. */
15
+ waitUntilEmpty(): Promise<void>;
16
+ }
17
+ //# sourceMappingURL=queue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queue.d.ts","sourceRoot":"","sources":["../src/queue.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAGvC,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,qBAAa,SAAS;IACpB,OAAO,CAAC,OAAO,CAAc;IAC7B,OAAO,CAAC,UAAU,CAAkB;IACpC,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,cAAc,CAAsB;IAE5C,OAAO,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI;YAaX,OAAO;IAiBrB,QAAQ,IAAI,UAAU;IAOtB,4EAA4E;IAC5E,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;CAIhC"}
package/dist/queue.js ADDED
@@ -0,0 +1,49 @@
1
+ import { runTask } from './runner.js';
2
+ export class TaskQueue {
3
+ pending = [];
4
+ processing = false;
5
+ runningSlug = null;
6
+ emptyResolvers = [];
7
+ enqueue(task) {
8
+ // Dedup: skip if this slug is currently running
9
+ if (this.runningSlug === task.slug)
10
+ return;
11
+ // Dedup: skip if this slug is already pending
12
+ if (this.pending.some(t => t.slug === task.slug))
13
+ return;
14
+ this.pending.push(task);
15
+ // Sort pending by slug for deterministic order
16
+ this.pending.sort((a, b) => a.slug.localeCompare(b.slug));
17
+ // Defer to microtask so synchronous enqueues batch together before processing
18
+ queueMicrotask(() => this.process());
19
+ }
20
+ async process() {
21
+ if (this.processing || this.pending.length === 0)
22
+ return;
23
+ this.processing = true;
24
+ while (this.pending.length > 0) {
25
+ const task = this.pending.shift();
26
+ this.runningSlug = task.slug;
27
+ await runTask(task);
28
+ this.runningSlug = null;
29
+ }
30
+ this.processing = false;
31
+ // Notify waiters
32
+ for (const resolve of this.emptyResolvers)
33
+ resolve();
34
+ this.emptyResolvers = [];
35
+ }
36
+ getState() {
37
+ return {
38
+ running: this.runningSlug,
39
+ queued: this.pending.map(t => t.slug),
40
+ };
41
+ }
42
+ /** Resolves when the queue is empty and nothing is running. For testing. */
43
+ waitUntilEmpty() {
44
+ if (!this.processing && this.pending.length === 0)
45
+ return Promise.resolve();
46
+ return new Promise(resolve => this.emptyResolvers.push(resolve));
47
+ }
48
+ }
49
+ //# sourceMappingURL=queue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queue.js","sourceRoot":"","sources":["../src/queue.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAOtC,MAAM,OAAO,SAAS;IACZ,OAAO,GAAW,EAAE,CAAC;IACrB,UAAU,GAAY,KAAK,CAAC;IAC5B,WAAW,GAAkB,IAAI,CAAC;IAClC,cAAc,GAAmB,EAAE,CAAC;IAE5C,OAAO,CAAC,IAAU;QAChB,gDAAgD;QAChD,IAAI,IAAI,CAAC,WAAW,KAAK,IAAI,CAAC,IAAI;YAAE,OAAO;QAC3C,8CAA8C;QAC9C,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,CAAC;YAAE,OAAO;QAEzD,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACxB,+CAA+C;QAC/C,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;QAC1D,8EAA8E;QAC9E,cAAc,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;IACvC,CAAC;IAEO,KAAK,CAAC,OAAO;QACnB,IAAI,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QACzD,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;QAEvB,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC/B,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,EAAG,CAAC;YACnC,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC;YAC7B,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;YACpB,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;QAC1B,CAAC;QAED,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC;QACxB,iBAAiB;QACjB,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,cAAc;YAAE,OAAO,EAAE,CAAC;QACrD,IAAI,CAAC,cAAc,GAAG,EAAE,CAAC;IAC3B,CAAC;IAED,QAAQ;QACN,OAAO;YACL,OAAO,EAAE,IAAI,CAAC,WAAW;YACzB,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;SACtC,CAAC;IACJ,CAAC;IAED,4EAA4E;IAC5E,cAAc;QACZ,IAAI,CAAC,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;QAC5E,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;IACnE,CAAC;CACF"}
package/dist/runner.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { runners } from './agents/index.js';
2
2
  import { Logger } from './logger.js';
3
3
  function buildPrompt(template) {
4
- const date = new Date().toLocaleDateString('zh-CN');
4
+ const date = new Date().toISOString().slice(0, 10);
5
5
  return template.replace(/\{date\}/g, date);
6
6
  }
7
7
  export async function runTask(task) {
@@ -1 +1 @@
1
- {"version":3,"file":"runner.js","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,SAAS,WAAW,CAAC,QAAgB;IACnC,MAAM,IAAI,GAAG,IAAI,IAAI,EAAE,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC;IACpD,OAAO,QAAQ,CAAC,OAAO,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;AAC7C,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,IAAU;IACtC,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACrC,MAAM,CAAC,KAAK,EAAE,CAAC;IAEf,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,IAAI,QAAQ,CAAC,CAAC;IACjD,MAAM,WAAW,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;IACvC,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,MAAM,GAAG,GAAG,mBAAmB,SAAS,GAAG,CAAC;QAC5C,OAAO,CAAC,KAAK,CAAC,gBAAgB,GAAG,KAAK,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC;QACpD,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;QACzB,OAAO;IACT,CAAC;IAED,MAAM,MAAM,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAExC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;QAE3D,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,GAAG,GAAG,oBAAoB,CAAC;YACjC,OAAO,CAAC,KAAK,CAAC,gBAAgB,GAAG,KAAK,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC;YACpD,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;YACzB,OAAO;QACT,CAAC;QAED,IAAI,MAAM,CAAC,IAAI,EAAE,KAAK,cAAc,EAAE,CAAC;YACrC,OAAO,CAAC,GAAG,CAAC,qCAAqC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC;YAC/D,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;YACxB,OAAO;QACT,CAAC;QAED,OAAO,CAAC,GAAG,CAAC,sBAAsB,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;QAC/C,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACnB,CAAC;IAAC,OAAO,GAAQ,EAAE,CAAC;QAClB,OAAO,CAAC,KAAK,CAAC,6BAA6B,IAAI,CAAC,IAAI,IAAI,EAAE,GAAG,CAAC,CAAC;QAC/D,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,EAAE,OAAO,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;IACnD,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"runner.js","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,SAAS,WAAW,CAAC,QAAgB;IACnC,MAAM,IAAI,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACnD,OAAO,QAAQ,CAAC,OAAO,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;AAC7C,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,IAAU;IACtC,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACrC,MAAM,CAAC,KAAK,EAAE,CAAC;IAEf,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,IAAI,QAAQ,CAAC,CAAC;IACjD,MAAM,WAAW,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;IACvC,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,MAAM,GAAG,GAAG,mBAAmB,SAAS,GAAG,CAAC;QAC5C,OAAO,CAAC,KAAK,CAAC,gBAAgB,GAAG,KAAK,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC;QACpD,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;QACzB,OAAO;IACT,CAAC;IAED,MAAM,MAAM,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAExC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;QAE3D,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,GAAG,GAAG,oBAAoB,CAAC;YACjC,OAAO,CAAC,KAAK,CAAC,gBAAgB,GAAG,KAAK,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC;YACpD,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;YACzB,OAAO;QACT,CAAC;QAED,IAAI,MAAM,CAAC,IAAI,EAAE,KAAK,cAAc,EAAE,CAAC;YACrC,OAAO,CAAC,GAAG,CAAC,qCAAqC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC;YAC/D,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;YACxB,OAAO;QACT,CAAC;QAED,OAAO,CAAC,GAAG,CAAC,sBAAsB,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;QAC/C,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACnB,CAAC;IAAC,OAAO,GAAQ,EAAE,CAAC;QAClB,OAAO,CAAC,KAAK,CAAC,6BAA6B,IAAI,CAAC,IAAI,IAAI,EAAE,GAAG,CAAC,CAAC;QAC/D,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,EAAE,OAAO,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;IACnD,CAAC;AACH,CAAC"}
@@ -1,4 +1,6 @@
1
1
  import type { Task } from './types.js';
2
+ import { TaskQueue } from './queue.js';
3
+ export declare const taskQueue: TaskQueue;
2
4
  export declare function startScheduler(tasks: Task[]): void;
3
5
  export declare function runNow(tasks: Task[], slug?: string): Promise<void>;
4
6
  export declare function listTasks(tasks: Task[]): void;
@@ -1 +1 @@
1
- {"version":3,"file":"scheduler.d.ts","sourceRoot":"","sources":["../src/scheduler.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAKvC,wBAAgB,cAAc,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,IAAI,CAoBlD;AAED,wBAAsB,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAaxE;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,IAAI,CAY7C"}
1
+ {"version":3,"file":"scheduler.d.ts","sourceRoot":"","sources":["../src/scheduler.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAEvC,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,eAAO,MAAM,SAAS,WAAkB,CAAC;AAIzC,wBAAgB,cAAc,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,IAAI,CAoBlD;AAED,wBAAsB,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAaxE;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,IAAI,CAY7C"}
package/dist/scheduler.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import cron from 'node-cron';
2
2
  import { runTask } from './runner.js';
3
+ import { TaskQueue } from './queue.js';
4
+ export const taskQueue = new TaskQueue();
3
5
  const TIMEZONE = 'Asia/Shanghai';
4
6
  export function startScheduler(tasks) {
5
7
  if (tasks.length === 0) {
@@ -11,7 +13,7 @@ export function startScheduler(tasks) {
11
13
  console.warn(`[agent-cron] invalid cron expression "${task.cron}" in task "${task.name}", skipping`);
12
14
  continue;
13
15
  }
14
- cron.schedule(task.cron, () => { void runTask(task); }, { timezone: TIMEZONE });
16
+ cron.schedule(task.cron, () => { taskQueue.enqueue(task); }, { timezone: TIMEZONE });
15
17
  console.log(`[agent-cron] scheduled: ${task.name} → ${task.cron} (${TIMEZONE})`);
16
18
  }
17
19
  console.log(`[agent-cron] scheduler running with ${tasks.length} task(s). Press Ctrl+C to stop.`);
@@ -1 +1 @@
1
- {"version":3,"file":"scheduler.js","sourceRoot":"","sources":["../src/scheduler.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAEtC,MAAM,QAAQ,GAAG,eAAe,CAAC;AAEjC,MAAM,UAAU,cAAc,CAAC,KAAa;IAC1C,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,CAAC,IAAI,CAAC,kDAAkD,CAAC,CAAC;QACjE,OAAO;IACT,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC9B,OAAO,CAAC,IAAI,CACV,yCAAyC,IAAI,CAAC,IAAI,cAAc,IAAI,CAAC,IAAI,aAAa,CACvF,CAAC;YACF,SAAS;QACX,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC;QAChF,OAAO,CAAC,GAAG,CAAC,2BAA2B,IAAI,CAAC,IAAI,MAAM,IAAI,CAAC,IAAI,KAAK,QAAQ,GAAG,CAAC,CAAC;IACnF,CAAC;IAED,OAAO,CAAC,GAAG,CACT,uCAAuC,KAAK,CAAC,MAAM,iCAAiC,CACrF,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,MAAM,CAAC,KAAa,EAAE,IAAa;IACvD,MAAM,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;IAEpE,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,CAAC,KAAK,CACX,IAAI,CAAC,CAAC,CAAC,iCAAiC,IAAI,GAAG,CAAC,CAAC,CAAC,8BAA8B,CACjF,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;QAC3B,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,CAAC;AACH,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,KAAa;IACrC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,CAAC,GAAG,CAAC,6BAA6B,CAAC,CAAC;QAC3C,OAAO;IACT,CAAC;IACD,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC;IACrC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,OAAO,CAAC,GAAG,CAAC,KAAK,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;QAC9B,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;QACxC,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;QACxC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAClB,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"scheduler.js","sourceRoot":"","sources":["../src/scheduler.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,MAAM,CAAC,MAAM,SAAS,GAAG,IAAI,SAAS,EAAE,CAAC;AAEzC,MAAM,QAAQ,GAAG,eAAe,CAAC;AAEjC,MAAM,UAAU,cAAc,CAAC,KAAa;IAC1C,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,CAAC,IAAI,CAAC,kDAAkD,CAAC,CAAC;QACjE,OAAO;IACT,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC9B,OAAO,CAAC,IAAI,CACV,yCAAyC,IAAI,CAAC,IAAI,cAAc,IAAI,CAAC,IAAI,aAAa,CACvF,CAAC;YACF,SAAS;QACX,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC;QACrF,OAAO,CAAC,GAAG,CAAC,2BAA2B,IAAI,CAAC,IAAI,MAAM,IAAI,CAAC,IAAI,KAAK,QAAQ,GAAG,CAAC,CAAC;IACnF,CAAC;IAED,OAAO,CAAC,GAAG,CACT,uCAAuC,KAAK,CAAC,MAAM,iCAAiC,CACrF,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,MAAM,CAAC,KAAa,EAAE,IAAa;IACvD,MAAM,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;IAEpE,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,CAAC,KAAK,CACX,IAAI,CAAC,CAAC,CAAC,iCAAiC,IAAI,GAAG,CAAC,CAAC,CAAC,8BAA8B,CACjF,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;QAC3B,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,CAAC;AACH,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,KAAa;IACrC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,CAAC,GAAG,CAAC,6BAA6B,CAAC,CAAC;QAC3C,OAAO;IACT,CAAC;IACD,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC;IACrC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,OAAO,CAAC,GAAG,CAAC,KAAK,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;QAC9B,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;QACxC,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;QACxC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAClB,CAAC;AACH,CAAC"}
@@ -0,0 +1,13 @@
1
+ import type { Task } from './types.js';
2
+ import type { QueueState } from './queue.js';
3
+ export interface TaskStatus {
4
+ slug: string;
5
+ name: string;
6
+ lastRun: string | null;
7
+ status: 'ok' | 'error' | 'heartbeat' | 'never';
8
+ duration: string | null;
9
+ error: string | null;
10
+ }
11
+ export declare function statusAll(tasks: Task[], queueState?: QueueState): void;
12
+ export declare function logsFor(slug: string, date?: string): void;
13
+ //# sourceMappingURL=status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAM7C,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,MAAM,EAAE,IAAI,GAAG,OAAO,GAAG,WAAW,GAAG,OAAO,CAAC;IAC/C,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AA8DD,wBAAgB,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,UAAU,CAAC,EAAE,UAAU,GAAG,IAAI,CAwEtE;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAsBzD"}
package/dist/status.js ADDED
@@ -0,0 +1,139 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ function logsDir() {
5
+ return path.join(os.homedir(), '.agent-cron', 'logs');
6
+ }
7
+ /** Parse the last [END] line from a log file. */
8
+ function parseLastEnd(logFile) {
9
+ let content;
10
+ try {
11
+ content = fs.readFileSync(logFile, 'utf-8');
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ // Find all END lines, take the last one
17
+ const lines = content.split('\n');
18
+ let lastEnd = null;
19
+ let lastStart = null;
20
+ for (const line of lines) {
21
+ if (line.includes('[END]'))
22
+ lastEnd = line;
23
+ if (line.includes('[START]'))
24
+ lastStart = line;
25
+ }
26
+ if (!lastEnd)
27
+ return null;
28
+ // Extract timestamp from "[2026-03-07 10:00:01.123] [END] status=ok duration=12345ms"
29
+ const tsMatch = lastEnd.match(/^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/);
30
+ const statusMatch = lastEnd.match(/status=(\w+)/);
31
+ const durationMatch = lastEnd.match(/duration=(\S+)/);
32
+ const errorMatch = lastEnd.match(/error="([^"]+)"/);
33
+ if (!statusMatch)
34
+ return null;
35
+ const rawStatus = statusMatch[1];
36
+ const status = (rawStatus === 'ok' || rawStatus === 'error' || rawStatus === 'heartbeat')
37
+ ? rawStatus : 'error';
38
+ return {
39
+ status,
40
+ duration: durationMatch?.[1] ?? '?',
41
+ error: errorMatch?.[1] ?? null,
42
+ time: tsMatch?.[1] ?? '',
43
+ };
44
+ }
45
+ /** Find the most recent log file for a slug. */
46
+ function latestLogFile(slug) {
47
+ const slugDir = path.join(logsDir(), slug);
48
+ if (!fs.existsSync(slugDir))
49
+ return null;
50
+ const files = fs.readdirSync(slugDir)
51
+ .filter(f => f.endsWith('.log'))
52
+ .sort()
53
+ .reverse();
54
+ if (files.length === 0)
55
+ return null;
56
+ return { file: path.join(slugDir, files[0]), date: files[0].replace('.log', '') };
57
+ }
58
+ /** Format a log date for display: "today HH:MM" or "YYYY-MM-DD HH:MM". */
59
+ function formatRunTime(logDate, time) {
60
+ const today = new Date().toISOString().slice(0, 10);
61
+ const hhmm = time.slice(11, 16); // "HH:MM"
62
+ return logDate === today ? `今天 ${hhmm}` : `${logDate} ${hhmm}`;
63
+ }
64
+ export function statusAll(tasks, queueState) {
65
+ const COL = { slug: 26, run: 14, status: 11, dur: 10 };
66
+ const header = 'TASK'.padEnd(COL.slug) +
67
+ 'LAST RUN'.padEnd(COL.run) +
68
+ 'STATUS'.padEnd(COL.status) +
69
+ 'DURATION';
70
+ console.log(header);
71
+ console.log('-'.repeat(header.length + 8));
72
+ for (const task of tasks) {
73
+ if (queueState?.running === task.slug) {
74
+ console.log(task.slug.padEnd(COL.slug) +
75
+ '-'.padEnd(COL.run) +
76
+ '→ running'.padEnd(COL.status) +
77
+ '-');
78
+ continue;
79
+ }
80
+ const queuedIndex = queueState?.queued.indexOf(task.slug) ?? -1;
81
+ if (queuedIndex >= 0) {
82
+ console.log(task.slug.padEnd(COL.slug) +
83
+ '-'.padEnd(COL.run) +
84
+ `→ queued (#${queuedIndex + 1})`.padEnd(COL.status) +
85
+ '-');
86
+ continue;
87
+ }
88
+ const latest = latestLogFile(task.slug);
89
+ if (!latest) {
90
+ console.log(task.slug.padEnd(COL.slug) +
91
+ 'never'.padEnd(COL.run) +
92
+ '-'.padEnd(COL.status) +
93
+ '-');
94
+ continue;
95
+ }
96
+ const parsed = parseLastEnd(latest.file);
97
+ if (!parsed) {
98
+ console.log(task.slug.padEnd(COL.slug) +
99
+ latest.date.padEnd(COL.run) +
100
+ 'running?'.padEnd(COL.status) +
101
+ '-');
102
+ continue;
103
+ }
104
+ const runTime = formatRunTime(latest.date, parsed.time);
105
+ const statusLabel = parsed.status === 'ok' ? 'ok' :
106
+ parsed.status === 'heartbeat' ? 'heartbeat' :
107
+ `error ⚠`;
108
+ const line = task.slug.padEnd(COL.slug) +
109
+ runTime.padEnd(COL.run) +
110
+ statusLabel.padEnd(COL.status) +
111
+ parsed.duration;
112
+ console.log(line);
113
+ if (parsed.error) {
114
+ console.log(' '.repeat(COL.slug) + `↳ ${parsed.error}`);
115
+ }
116
+ }
117
+ }
118
+ export function logsFor(slug, date) {
119
+ const targetDate = date ?? new Date().toISOString().slice(0, 10);
120
+ const logFile = path.join(logsDir(), slug, `${targetDate}.log`);
121
+ if (!fs.existsSync(logFile)) {
122
+ // Try most recent if today not found
123
+ const latest = latestLogFile(slug);
124
+ if (!latest) {
125
+ console.error(`No logs found for task: ${slug}`);
126
+ process.exit(1);
127
+ }
128
+ if (!date) {
129
+ // Fall back to latest
130
+ console.log(`(no log for today, showing ${latest.date})\n`);
131
+ console.log(fs.readFileSync(latest.file, 'utf-8'));
132
+ return;
133
+ }
134
+ console.error(`No log found: ${logFile}`);
135
+ process.exit(1);
136
+ }
137
+ console.log(fs.readFileSync(logFile, 'utf-8'));
138
+ }
139
+ //# sourceMappingURL=status.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status.js","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,MAAM,IAAI,CAAC;AAIpB,SAAS,OAAO;IACd,OAAO,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,aAAa,EAAE,MAAM,CAAC,CAAC;AACxD,CAAC;AAWD,iDAAiD;AACjD,SAAS,YAAY,CAAC,OAAe;IACnC,IAAI,OAAe,CAAC;IACpB,IAAI,CAAC;QACH,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC9C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IAED,wCAAwC;IACxC,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAClC,IAAI,OAAO,GAAkB,IAAI,CAAC;IAClC,IAAI,SAAS,GAAkB,IAAI,CAAC;IACpC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC;YAAE,OAAO,GAAG,IAAI,CAAC;QAC3C,IAAI,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC;YAAE,SAAS,GAAG,IAAI,CAAC;IACjD,CAAC;IACD,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAE1B,wFAAwF;IACxF,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,0CAA0C,CAAC,CAAC;IAC1E,MAAM,WAAW,GAAG,OAAO,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;IAClD,MAAM,aAAa,GAAG,OAAO,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC;IACtD,MAAM,UAAU,GAAG,OAAO,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC;IAEpD,IAAI,CAAC,WAAW;QAAE,OAAO,IAAI,CAAC;IAE9B,MAAM,SAAS,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC;IACjC,MAAM,MAAM,GAAG,CAAC,SAAS,KAAK,IAAI,IAAI,SAAS,KAAK,OAAO,IAAI,SAAS,KAAK,WAAW,CAAC;QACvF,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC;IAExB,OAAO;QACL,MAAM;QACN,QAAQ,EAAE,aAAa,EAAE,CAAC,CAAC,CAAC,IAAI,GAAG;QACnC,KAAK,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI;QAC9B,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE;KACzB,CAAC;AACJ,CAAC;AAED,gDAAgD;AAChD,SAAS,aAAa,CAAC,IAAY;IACjC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,IAAI,CAAC,CAAC;IAC3C,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC;QAAE,OAAO,IAAI,CAAC;IAEzC,MAAM,KAAK,GAAG,EAAE,CAAC,WAAW,CAAC,OAAO,CAAC;SAClC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;SAC/B,IAAI,EAAE;SACN,OAAO,EAAE,CAAC;IAEb,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACpC,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,EAAE,CAAC;AACpF,CAAC;AAED,0EAA0E;AAC1E,SAAS,aAAa,CAAC,OAAe,EAAE,IAAY;IAClD,MAAM,KAAK,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACpD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU;IAC3C,OAAO,OAAO,KAAK,KAAK,CAAC,CAAC,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,CAAC,GAAG,OAAO,IAAI,IAAI,EAAE,CAAC;AACjE,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,KAAa,EAAE,UAAuB;IAC9D,MAAM,GAAG,GAAG,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC;IAEvD,MAAM,MAAM,GACV,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC;QACvB,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC;QAC1B,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC;QAC3B,UAAU,CAAC;IACb,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACpB,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC;IAE3C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,UAAU,EAAE,OAAO,KAAK,IAAI,CAAC,IAAI,EAAE,CAAC;YACtC,OAAO,CAAC,GAAG,CACT,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC;gBAC1B,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC;gBACnB,WAAW,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC;gBAC9B,GAAG,CACJ,CAAC;YACF,SAAS;QACX,CAAC;QAED,MAAM,WAAW,GAAG,UAAU,EAAE,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QAChE,IAAI,WAAW,IAAI,CAAC,EAAE,CAAC;YACrB,OAAO,CAAC,GAAG,CACT,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC;gBAC1B,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC;gBACnB,cAAc,WAAW,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC;gBACnD,GAAG,CACJ,CAAC;YACF,SAAS;QACX,CAAC;QAED,MAAM,MAAM,GAAG,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACxC,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,OAAO,CAAC,GAAG,CACT,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC;gBAC1B,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC;gBACvB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC;gBACtB,GAAG,CACJ,CAAC;YACF,SAAS;QACX,CAAC;QAED,MAAM,MAAM,GAAG,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACzC,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,OAAO,CAAC,GAAG,CACT,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC;gBAC1B,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC;gBAC3B,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC;gBAC7B,GAAG,CACJ,CAAC;YACF,SAAS;QACX,CAAC;QAED,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;QACxD,MAAM,WAAW,GACf,MAAM,CAAC,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;YAC/B,MAAM,CAAC,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC;gBAC7C,SAAS,CAAC;QAEZ,MAAM,IAAI,GACR,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC;YAC1B,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC;YACvB,WAAW,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC;YAC9B,MAAM,CAAC,QAAQ,CAAC;QAElB,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAClB,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;YACjB,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,KAAK,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;QAC1D,CAAC;IACH,CAAC;AACH,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,IAAY,EAAE,IAAa;IACjD,MAAM,UAAU,GAAG,IAAI,IAAI,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACjE,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,GAAG,UAAU,MAAM,CAAC,CAAC;IAEhE,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QAC5B,qCAAqC;QACrC,MAAM,MAAM,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;QACnC,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,OAAO,CAAC,KAAK,CAAC,2BAA2B,IAAI,EAAE,CAAC,CAAC;YACjD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QACD,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,sBAAsB;YACtB,OAAO,CAAC,GAAG,CAAC,8BAA8B,MAAM,CAAC,IAAI,KAAK,CAAC,CAAC;YAC5D,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;YACnD,OAAO;QACT,CAAC;QACD,OAAO,CAAC,KAAK,CAAC,iBAAiB,OAAO,EAAE,CAAC,CAAC;QAC1C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;AACjD,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@t0u9h/agent-cron",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Run Claude Agent SDK tasks on a cron schedule. Tasks are defined as .md files.",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",