openclacky 0.9.19 → 0.9.20
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/docs/c-end-user-positioning.md +64 -0
- data/lib/clacky/agent/llm_caller.rb +1 -1
- data/lib/clacky/default_skills/cron-task-creator/SKILL.md +65 -58
- data/lib/clacky/server/http_server.rb +60 -91
- data/lib/clacky/server/scheduler.rb +55 -0
- data/lib/clacky/tools/browser.rb +2 -2
- data/lib/clacky/utils/encoding.rb +21 -0
- data/lib/clacky/utils/environment_detector.rb +9 -6
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +1 -1
- data/lib/clacky/web/tasks.js +10 -26
- data/scripts/install.sh +13 -12
- metadata +2 -5
- data/lib/clacky/default_skills/cron-task-creator/scripts/list_tasks.rb +0 -121
- data/lib/clacky/default_skills/cron-task-creator/scripts/manage_schedule.rb +0 -149
- data/lib/clacky/default_skills/cron-task-creator/scripts/manage_task.rb +0 -81
- data/lib/clacky/default_skills/cron-task-creator/scripts/task_history.rb +0 -137
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 803023a686f9005d8bbed56fa2604d6cb4a426ba2ff1c24ddf6e297242c7faed
|
|
4
|
+
data.tar.gz: ee8738600a36146e4f56b8605a917066746b31e370a41c3e245066ec4e4e961c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b5b27887365a698d193fee92d0672315c48963a9748509cae8a6b5426d0169f16b9fad709afa2232d80542999fc931ee48e09f2cee35100b39273b33c324b558
|
|
7
|
+
data.tar.gz: 5c5572d8a7b34af4eb03220dbf0e566ed0d2ab7b7278a89d8801f0c9ca58b100c90dd27fd17d1548016b21c20e1e458889a97a54983309c409479b863fc8b6ca
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.9.20] - 2026-03-30
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **SSL error retry**: LLM API calls now automatically retry on SSL errors (same as other network failures — up to 10 retries with 5s delay)
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- **Brand wrapper not found under root**: the install script now places the brand command wrapper in the same directory as the `openclacky` binary, so it is always on PATH regardless of whether running as root or a normal user
|
|
17
|
+
|
|
18
|
+
### Improved
|
|
19
|
+
- **Cron task management refactored to API**: cron task CRUD operations now go through the HTTP API instead of running ad-hoc Ruby scripts, making the scheduler more reliable and easier to maintain
|
|
20
|
+
- **UTF-8 encoding fix for browser tool on Windows**: browser command output with non-ASCII characters no longer causes encoding errors
|
|
21
|
+
|
|
22
|
+
### More
|
|
23
|
+
- Installer no longer adds `~/.local/bin` to PATH (wrapper now colocated with gem binary, making the extra PATH entry unnecessary)
|
|
24
|
+
- Brand install tips in Windows PowerShell installer
|
|
25
|
+
|
|
10
26
|
## [0.9.19] - 2026-03-29
|
|
11
27
|
|
|
12
28
|
### Added
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# C-End User Positioning
|
|
2
|
+
|
|
3
|
+
> Date: 2026-03-30
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Market Context
|
|
8
|
+
|
|
9
|
+
The "OpenClaw ecosystem" has exploded in 2026. Key players:
|
|
10
|
+
|
|
11
|
+
- **OpenClaw** — open-source, self-hosted, community Skills. Designed for technical users who configure everything themselves.
|
|
12
|
+
- **QClaw** — Tencent's fork. Bundled Kimi model, WeChat binding. Mass-market but Tencent-ecosystem only.
|
|
13
|
+
- **Others** (Wukong, etc.) — same lane.
|
|
14
|
+
|
|
15
|
+
OpenClaw has 5,700+ Skills, but almost all are open-source, free, and easily copied. The ecosystem lacks **expertise-backed, production-grade Skills worth paying for**.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Who openclacky Is For
|
|
20
|
+
|
|
21
|
+
**Ordinary users, not technical geeks.**
|
|
22
|
+
|
|
23
|
+
The target user knows OpenClaw exists, has heard about "raising a lobster", but can't or doesn't want to:
|
|
24
|
+
- configure Docker / environment / webhooks
|
|
25
|
+
- manage their own API keys without knowing what they'll spend
|
|
26
|
+
- troubleshoot when a long task breaks halfway
|
|
27
|
+
|
|
28
|
+
They want to use a lobster built by an expert (a lawyer, a trader, an SEO specialist) — not build one themselves.
|
|
29
|
+
|
|
30
|
+
> Core insight: **OpenClaw is built for people who create Skills. openclacky is built for people who use them.**
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Why openclacky Over OpenClaw: 3 Core Reasons
|
|
35
|
+
|
|
36
|
+
### 1. Zero-friction IM setup — the strongest differentiator
|
|
37
|
+
|
|
38
|
+
OpenClaw requires users to manually configure webhooks, tokens, and config files to connect WeChat / Feishu / WeCom. High technical barrier, most ordinary users give up.
|
|
39
|
+
|
|
40
|
+
openclacky uses **AI-automated channel setup**: one sentence, and the AI configures the IM connection for you — no plugins, no docs, no engineering knowledge required. This is a genuine technical moat.
|
|
41
|
+
|
|
42
|
+
### 2. Built for China, natively
|
|
43
|
+
|
|
44
|
+
- No VPN required, no overseas credit card
|
|
45
|
+
- WeChat / Feishu / WeCom are the primary daily tools for Chinese users — openclacky treats them as first-class citizens
|
|
46
|
+
- Supports domestic models (DeepSeek, Kimi, etc.) out of the box
|
|
47
|
+
- QClaw is domestic too, but locked to Tencent's ecosystem and model choices
|
|
48
|
+
|
|
49
|
+
### 3. Cost transparency and long-task reliability
|
|
50
|
+
|
|
51
|
+
- Real-time token cost tracking — users always know what they're spending
|
|
52
|
+
- Automatic compression (up to 90% savings via Insert-then-Compress + Prompt Caching)
|
|
53
|
+
- Long tasks don't break: sub-agent isolation + Time Machine architecture keeps context intact
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## The User Progression
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
Can use it → Dare to use it → Keep using it
|
|
61
|
+
(zero setup) (cost clarity) (tasks don't break)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Each of the 3 reasons maps directly to one stage of this progression.
|
|
@@ -34,7 +34,7 @@ module Clacky
|
|
|
34
34
|
max_tokens: @config.max_tokens,
|
|
35
35
|
enable_caching: @config.enable_prompt_caching
|
|
36
36
|
)
|
|
37
|
-
rescue Faraday::ConnectionFailed, Faraday::TimeoutError, Errno::ECONNREFUSED, Errno::ETIMEDOUT => e
|
|
37
|
+
rescue Faraday::ConnectionFailed, Faraday::TimeoutError, Faraday::SSLError, Errno::ECONNREFUSED, Errno::ETIMEDOUT => e
|
|
38
38
|
@ui&.clear_progress
|
|
39
39
|
retries += 1
|
|
40
40
|
if retries <= max_retries
|
|
@@ -17,21 +17,14 @@ Storage:
|
|
|
17
17
|
~/.clacky/schedules.yml # All scheduled plans (YAML list)
|
|
18
18
|
~/.clacky/logger/clacky-*.log # Execution logs (daily rotation)
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
POST /api/tasks → create task {name, content}
|
|
29
|
-
GET /api/tasks/:name → get task content
|
|
30
|
-
DELETE /api/tasks/:name → delete task
|
|
31
|
-
POST /api/tasks/run → run immediately {name}
|
|
32
|
-
GET /api/schedules → list all schedules
|
|
33
|
-
POST /api/schedules → create schedule {name, task, cron}
|
|
34
|
-
DELETE /api/schedules/:name → delete schedule
|
|
20
|
+
API Base: http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}
|
|
21
|
+
|
|
22
|
+
Cron-Tasks API (unified — manages task file + schedule together):
|
|
23
|
+
GET /api/cron-tasks → list all cron tasks with schedule info
|
|
24
|
+
POST /api/cron-tasks → create task + schedule {name, content, cron, enabled?}
|
|
25
|
+
PATCH /api/cron-tasks/:name → update {content?, cron?, enabled?}
|
|
26
|
+
DELETE /api/cron-tasks/:name → delete task file + schedule
|
|
27
|
+
POST /api/cron-tasks/:name/run → execute immediately (creates a new session)
|
|
35
28
|
```
|
|
36
29
|
|
|
37
30
|
## Cron Expression Quick Reference
|
|
@@ -54,10 +47,11 @@ Field order: `minute hour day-of-month month day-of-week`
|
|
|
54
47
|
|
|
55
48
|
### 1. LIST — Show all tasks
|
|
56
49
|
|
|
57
|
-
|
|
50
|
+
```bash
|
|
51
|
+
curl -s http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/cron-tasks
|
|
52
|
+
```
|
|
58
53
|
|
|
59
|
-
|
|
60
|
-
2. Display results: task name, cron schedule, enabled status, last run status
|
|
54
|
+
Display each task: name, cron schedule, enabled status, content preview.
|
|
61
55
|
|
|
62
56
|
If no tasks exist, inform the user and offer to create one or show templates.
|
|
63
57
|
|
|
@@ -69,15 +63,14 @@ If no tasks exist, inform the user and offer to create one or show templates.
|
|
|
69
63
|
|
|
70
64
|
**Step 1: Gather required info** (only ask for what's missing)
|
|
71
65
|
- What should the task DO? (goal, behavior, output format)
|
|
72
|
-
- How often should it run? (or is it manual-only?)
|
|
66
|
+
- How often should it run? (or is it manual-only without a schedule?)
|
|
73
67
|
- Any specific parameters? (URLs, file paths, output location, language)
|
|
74
68
|
|
|
75
69
|
**Step 2: Generate task name**
|
|
76
70
|
- Rule: only `[a-z0-9_-]`, lowercase, no spaces
|
|
77
71
|
- Examples: `daily_report`, `price_monitor`, `weekly_summary`
|
|
78
72
|
|
|
79
|
-
**Step 3: Write the task prompt
|
|
80
|
-
Path: `~/.clacky/tasks/<name>.md`
|
|
73
|
+
**Step 3: Write the task prompt**
|
|
81
74
|
|
|
82
75
|
The prompt must be:
|
|
83
76
|
- **Self-contained**: the agent running it has zero prior context — include everything needed
|
|
@@ -85,7 +78,7 @@ The prompt must be:
|
|
|
85
78
|
- **Detailed**: include URLs, file paths, output format, language, expected output location
|
|
86
79
|
|
|
87
80
|
Good task prompt example:
|
|
88
|
-
```
|
|
81
|
+
```
|
|
89
82
|
You are a price monitoring assistant. Complete the following task:
|
|
90
83
|
|
|
91
84
|
## Goal
|
|
@@ -100,14 +93,17 @@ Check the current BTC price on CoinGecko, compare with yesterday's price, and lo
|
|
|
100
93
|
Execute immediately.
|
|
101
94
|
```
|
|
102
95
|
|
|
103
|
-
**Step 4:
|
|
96
|
+
**Step 4: Create via API**
|
|
104
97
|
|
|
105
98
|
```bash
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
99
|
+
curl -s -X POST http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/cron-tasks \
|
|
100
|
+
-H "Content-Type: application/json" \
|
|
101
|
+
-d '{
|
|
102
|
+
"name": "task_name",
|
|
103
|
+
"content": "task prompt content...",
|
|
104
|
+
"cron": "0 9 * * *",
|
|
105
|
+
"enabled": true
|
|
106
|
+
}'
|
|
111
107
|
```
|
|
112
108
|
|
|
113
109
|
**Step 5: Confirm creation**
|
|
@@ -116,7 +112,6 @@ ruby ~/.clacky/skills/cron-task-creator/scripts/manage_schedule.rb add "<name>"
|
|
|
116
112
|
✅ Task created successfully!
|
|
117
113
|
|
|
118
114
|
📋 Task name: daily_standup
|
|
119
|
-
📄 File: ~/.clacky/tasks/daily_standup.md
|
|
120
115
|
⏰ Schedule: Weekdays at 09:00 (cron: 0 9 * * 1-5)
|
|
121
116
|
|
|
122
117
|
View and manage this task in the Clacky WebUI → Tasks panel. Click ▶ Run to execute immediately.
|
|
@@ -126,24 +121,27 @@ View and manage this task in the Clacky WebUI → Tasks panel. Click ▶ Run to
|
|
|
126
121
|
|
|
127
122
|
### 3. EDIT — Modify an existing task
|
|
128
123
|
|
|
129
|
-
|
|
124
|
+
**Step 1**: Identify the task (if unclear, LIST first and ask)
|
|
130
125
|
|
|
131
|
-
**Step
|
|
126
|
+
**Step 2**: Show current state via LIST or ask user to confirm
|
|
132
127
|
|
|
133
|
-
**Step
|
|
134
|
-
|
|
135
|
-
**Step 3**: Determine what to change
|
|
136
|
-
- Prompt only → rewrite `~/.clacky/tasks/<name>.md`
|
|
137
|
-
- Schedule only → update `cron` field in `schedules.yml`
|
|
138
|
-
- Both → do both
|
|
139
|
-
- Rename → rename .md file + update schedules.yml
|
|
128
|
+
**Step 3**: Update via API
|
|
140
129
|
|
|
141
130
|
```bash
|
|
142
|
-
# Update
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
131
|
+
# Update content only
|
|
132
|
+
curl -s -X PATCH http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/cron-tasks/task_name \
|
|
133
|
+
-H "Content-Type: application/json" \
|
|
134
|
+
-d '{"content": "new prompt content..."}'
|
|
135
|
+
|
|
136
|
+
# Update cron schedule only
|
|
137
|
+
curl -s -X PATCH http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/cron-tasks/task_name \
|
|
138
|
+
-H "Content-Type: application/json" \
|
|
139
|
+
-d '{"cron": "0 8 * * 1-5"}'
|
|
140
|
+
|
|
141
|
+
# Update both
|
|
142
|
+
curl -s -X PATCH http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/cron-tasks/task_name \
|
|
143
|
+
-H "Content-Type: application/json" \
|
|
144
|
+
-d '{"content": "...", "cron": "0 8 * * 1-5"}'
|
|
147
145
|
```
|
|
148
146
|
|
|
149
147
|
**Step 4**: Confirm changes
|
|
@@ -151,7 +149,6 @@ ruby ~/.clacky/skills/cron-task-creator/scripts/manage_schedule.rb update "<sche
|
|
|
151
149
|
```
|
|
152
150
|
✅ Task updated!
|
|
153
151
|
📋 daily_standup
|
|
154
|
-
Prompt: updated ✓
|
|
155
152
|
Schedule: 0 9 * * 1-5 → 0 8 * * 1-5 (now weekdays at 08:00)
|
|
156
153
|
```
|
|
157
154
|
|
|
@@ -160,7 +157,15 @@ ruby ~/.clacky/skills/cron-task-creator/scripts/manage_schedule.rb update "<sche
|
|
|
160
157
|
### 4. ENABLE / DISABLE — Toggle a task
|
|
161
158
|
|
|
162
159
|
```bash
|
|
163
|
-
|
|
160
|
+
# Disable
|
|
161
|
+
curl -s -X PATCH http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/cron-tasks/task_name \
|
|
162
|
+
-H "Content-Type: application/json" \
|
|
163
|
+
-d '{"enabled": false}'
|
|
164
|
+
|
|
165
|
+
# Enable
|
|
166
|
+
curl -s -X PATCH http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/cron-tasks/task_name \
|
|
167
|
+
-H "Content-Type: application/json" \
|
|
168
|
+
-d '{"enabled": true}'
|
|
164
169
|
```
|
|
165
170
|
|
|
166
171
|
Confirm:
|
|
@@ -180,15 +185,22 @@ Always confirm before deleting (unless the user has explicitly said to delete):
|
|
|
180
185
|
```
|
|
181
186
|
|
|
182
187
|
```bash
|
|
183
|
-
|
|
188
|
+
curl -s -X DELETE http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/cron-tasks/task_name
|
|
184
189
|
```
|
|
185
190
|
|
|
186
191
|
---
|
|
187
192
|
|
|
188
193
|
### 6. HISTORY — View run history
|
|
189
194
|
|
|
195
|
+
Read the daily log files directly:
|
|
196
|
+
|
|
190
197
|
```bash
|
|
191
|
-
|
|
198
|
+
grep "task_name" ~/.clacky/logger/clacky-$(date +%Y-%m-%d).log | tail -20
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Or search across recent days:
|
|
202
|
+
```bash
|
|
203
|
+
grep -h "task_name" ~/.clacky/logger/clacky-*.log | tail -30
|
|
192
204
|
```
|
|
193
205
|
|
|
194
206
|
Display format:
|
|
@@ -198,24 +210,21 @@ Display format:
|
|
|
198
210
|
Mar 10 19:00 ❌ Failed — JSON::ParserError: unexpected end of input
|
|
199
211
|
Mar 09 19:00 ✅ Success — took 1m 42s
|
|
200
212
|
Mar 08 19:00 ✅ Success — took 2m 10s
|
|
201
|
-
Mar 07 19:00 ⚠️ Skipped — (task was disabled)
|
|
202
|
-
|
|
203
|
-
Recent error:
|
|
204
|
-
JSON::ParserError at line 226
|
|
205
|
-
Possible cause: API response was truncated or empty
|
|
206
213
|
```
|
|
207
214
|
|
|
208
215
|
---
|
|
209
216
|
|
|
210
217
|
### 7. RUN NOW — Execute immediately
|
|
211
218
|
|
|
219
|
+
```bash
|
|
220
|
+
curl -s -X POST http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/cron-tasks/task_name/run
|
|
212
221
|
```
|
|
213
|
-
▶️ Go to the Clacky WebUI → Tasks panel, find the task, and click ▶ Run.
|
|
214
222
|
|
|
215
|
-
|
|
223
|
+
This creates a new session. Tell the user:
|
|
224
|
+
```
|
|
225
|
+
▶️ Task started in a new session.
|
|
226
|
+
View it in the Clacky WebUI → Sessions panel.
|
|
216
227
|
```
|
|
217
|
-
|
|
218
|
-
If the user wants to run the task in the current session, read the task file and execute the instructions directly.
|
|
219
228
|
|
|
220
229
|
---
|
|
221
230
|
|
|
@@ -243,8 +252,6 @@ Tell me which one interests you, or describe your own use case!
|
|
|
243
252
|
## Important Notes
|
|
244
253
|
|
|
245
254
|
- Task names: only `[a-z0-9_-]`, no spaces, no uppercase
|
|
246
|
-
- When modifying `schedules.yml`: always read → modify → write back the full file (never append directly)
|
|
247
255
|
- Task prompt files must be **self-contained** — the executing agent has no prior memory
|
|
248
256
|
- Clacky server must be running for cron to trigger automatically (checked every minute)
|
|
249
257
|
- The WebUI Task Panel is the preferred interface for managing tasks — always remind the user to check it after changes
|
|
250
|
-
- Path expansion: always use absolute paths, expand `~` to the actual home directory
|
|
@@ -315,11 +315,8 @@ module Clacky
|
|
|
315
315
|
case [method, path]
|
|
316
316
|
when ["GET", "/api/sessions"] then api_list_sessions(req, res)
|
|
317
317
|
when ["POST", "/api/sessions"] then api_create_session(req, res)
|
|
318
|
-
when ["GET", "/api/
|
|
319
|
-
when ["POST", "/api/
|
|
320
|
-
when ["GET", "/api/tasks"] then api_list_tasks(res)
|
|
321
|
-
when ["POST", "/api/tasks"] then api_create_task(req, res)
|
|
322
|
-
when ["POST", "/api/tasks/run"] then api_run_task(req, res)
|
|
318
|
+
when ["GET", "/api/cron-tasks"] then api_list_cron_tasks(res)
|
|
319
|
+
when ["POST", "/api/cron-tasks"] then api_create_cron_task(req, res)
|
|
323
320
|
when ["GET", "/api/skills"] then api_list_skills(res)
|
|
324
321
|
when ["GET", "/api/config"] then api_get_config(res)
|
|
325
322
|
when ["POST", "/api/config"] then api_save_config(req, res)
|
|
@@ -365,15 +362,15 @@ module Clacky
|
|
|
365
362
|
elsif method == "DELETE" && path.start_with?("/api/sessions/")
|
|
366
363
|
session_id = path.sub("/api/sessions/", "")
|
|
367
364
|
api_delete_session(session_id, res)
|
|
368
|
-
elsif method == "
|
|
369
|
-
name = URI.decode_www_form_component(path.sub("/api/
|
|
370
|
-
|
|
371
|
-
elsif method == "
|
|
372
|
-
name = URI.decode_www_form_component(path.sub("/api/tasks/", ""))
|
|
373
|
-
|
|
374
|
-
elsif method == "DELETE" && path.
|
|
375
|
-
name = URI.decode_www_form_component(path.sub("/api/tasks/", ""))
|
|
376
|
-
|
|
365
|
+
elsif method == "POST" && path.match?(%r{^/api/cron-tasks/[^/]+/run$})
|
|
366
|
+
name = URI.decode_www_form_component(path.sub("/api/cron-tasks/", "").sub("/run", ""))
|
|
367
|
+
api_run_cron_task(name, res)
|
|
368
|
+
elsif method == "PATCH" && path.match?(%r{^/api/cron-tasks/[^/]+$})
|
|
369
|
+
name = URI.decode_www_form_component(path.sub("/api/cron-tasks/", ""))
|
|
370
|
+
api_update_cron_task(name, req, res)
|
|
371
|
+
elsif method == "DELETE" && path.match?(%r{^/api/cron-tasks/[^/]+$})
|
|
372
|
+
name = URI.decode_www_form_component(path.sub("/api/cron-tasks/", ""))
|
|
373
|
+
api_delete_cron_task(name, res)
|
|
377
374
|
elsif method == "PATCH" && path.match?(%r{^/api/skills/[^/]+/toggle$})
|
|
378
375
|
name = URI.decode_www_form_component(path.sub("/api/skills/", "").sub("/toggle", ""))
|
|
379
376
|
api_toggle_skill(name, req, res)
|
|
@@ -1242,109 +1239,81 @@ module Clacky
|
|
|
1242
1239
|
}
|
|
1243
1240
|
end
|
|
1244
1241
|
|
|
1245
|
-
# ── Schedules API ─────────────────────────────────────────────────────────
|
|
1246
1242
|
|
|
1247
|
-
|
|
1248
|
-
|
|
1243
|
+
# ── Cron-Tasks API ───────────────────────────────────────────────────────
|
|
1244
|
+
# Unified API that manages task file + schedule as a single resource.
|
|
1245
|
+
|
|
1246
|
+
# GET /api/cron-tasks
|
|
1247
|
+
def api_list_cron_tasks(res)
|
|
1248
|
+
json_response(res, 200, { cron_tasks: @scheduler.list_cron_tasks })
|
|
1249
1249
|
end
|
|
1250
1250
|
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1251
|
+
# POST /api/cron-tasks — create task file + schedule in one step
|
|
1252
|
+
# Body: { name, content, cron, enabled? }
|
|
1253
|
+
def api_create_cron_task(req, res)
|
|
1254
|
+
body = parse_json_body(req)
|
|
1255
|
+
name = body["name"].to_s.strip
|
|
1256
|
+
content = body["content"].to_s
|
|
1257
|
+
cron = body["cron"].to_s.strip
|
|
1258
|
+
enabled = body.key?("enabled") ? body["enabled"] : true
|
|
1256
1259
|
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
end
|
|
1260
|
+
return json_response(res, 422, { error: "name is required" }) if name.empty?
|
|
1261
|
+
return json_response(res, 422, { error: "content is required" }) if content.empty?
|
|
1262
|
+
return json_response(res, 422, { error: "cron is required" }) if cron.empty?
|
|
1261
1263
|
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
return
|
|
1264
|
+
fields = cron.strip.split(/\s+/)
|
|
1265
|
+
unless fields.size == 5
|
|
1266
|
+
return json_response(res, 422, { error: "cron must have 5 fields (min hour dom month dow)" })
|
|
1265
1267
|
end
|
|
1266
1268
|
|
|
1267
|
-
@scheduler.
|
|
1269
|
+
@scheduler.create_cron_task(name: name, content: content, cron: cron, enabled: enabled)
|
|
1268
1270
|
json_response(res, 201, { ok: true, name: name })
|
|
1269
1271
|
end
|
|
1270
1272
|
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
# ── Tasks API ─────────────────────────────────────────────────────────────
|
|
1273
|
+
# PATCH /api/cron-tasks/:name — update content and/or cron/enabled
|
|
1274
|
+
# Body: { content?, cron?, enabled? }
|
|
1275
|
+
def api_update_cron_task(name, req, res)
|
|
1276
|
+
body = parse_json_body(req)
|
|
1277
|
+
content = body["content"]
|
|
1278
|
+
cron = body["cron"]&.to_s&.strip
|
|
1279
|
+
enabled = body["enabled"]
|
|
1280
1280
|
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
content = begin
|
|
1284
|
-
@scheduler.read_task(name)
|
|
1285
|
-
rescue StandardError
|
|
1286
|
-
""
|
|
1287
|
-
end
|
|
1288
|
-
{ name: name, path: @scheduler.task_file_path(name), content: content }
|
|
1281
|
+
if cron && cron.split(/\s+/).size != 5
|
|
1282
|
+
return json_response(res, 422, { error: "cron must have 5 fields (min hour dom month dow)" })
|
|
1289
1283
|
end
|
|
1290
|
-
json_response(res, 200, { tasks: tasks })
|
|
1291
|
-
end
|
|
1292
1284
|
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
json_response(res, 200, { name: name, content: content })
|
|
1285
|
+
@scheduler.update_cron_task(name, content: content, cron: cron, enabled: enabled)
|
|
1286
|
+
json_response(res, 200, { ok: true, name: name })
|
|
1296
1287
|
rescue => e
|
|
1297
1288
|
json_response(res, 404, { error: e.message })
|
|
1298
1289
|
end
|
|
1299
1290
|
|
|
1300
|
-
|
|
1301
|
-
|
|
1291
|
+
# DELETE /api/cron-tasks/:name — remove task file + schedule
|
|
1292
|
+
def api_delete_cron_task(name, res)
|
|
1293
|
+
if @scheduler.delete_cron_task(name)
|
|
1302
1294
|
json_response(res, 200, { ok: true })
|
|
1303
1295
|
else
|
|
1304
|
-
json_response(res, 404, { error: "
|
|
1296
|
+
json_response(res, 404, { error: "Cron task not found: #{name}" })
|
|
1305
1297
|
end
|
|
1306
1298
|
end
|
|
1307
1299
|
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
if name.empty?
|
|
1314
|
-
json_response(res, 422, { error: "name is required" })
|
|
1315
|
-
return
|
|
1316
|
-
end
|
|
1317
|
-
|
|
1318
|
-
@scheduler.write_task(name, content)
|
|
1319
|
-
json_response(res, 201, { ok: true, name: name })
|
|
1320
|
-
end
|
|
1321
|
-
|
|
1322
|
-
def api_run_task(req, res)
|
|
1323
|
-
body = parse_json_body(req)
|
|
1324
|
-
name = body["name"].to_s.strip
|
|
1325
|
-
|
|
1326
|
-
if name.empty?
|
|
1327
|
-
json_response(res, 422, { error: "name is required" })
|
|
1328
|
-
return
|
|
1300
|
+
# POST /api/cron-tasks/:name/run — execute immediately
|
|
1301
|
+
def api_run_cron_task(name, res)
|
|
1302
|
+
unless @scheduler.list_tasks.include?(name)
|
|
1303
|
+
return json_response(res, 404, { error: "Cron task not found: #{name}" })
|
|
1329
1304
|
end
|
|
1330
1305
|
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
FileUtils.mkdir_p(working_dir)
|
|
1336
|
-
|
|
1337
|
-
# Tasks run unattended — use auto_approve so request_user_feedback doesn't block.
|
|
1338
|
-
session_id = build_session(name: session_name, working_dir: working_dir, permission_mode: :auto_approve)
|
|
1306
|
+
prompt = @scheduler.read_task(name)
|
|
1307
|
+
session_name = "▶ #{name} #{Time.now.strftime("%H:%M")}"
|
|
1308
|
+
working_dir = File.expand_path("~/clacky_workspace")
|
|
1309
|
+
FileUtils.mkdir_p(working_dir)
|
|
1339
1310
|
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
@registry.update(session_id, pending_task: prompt, pending_working_dir: working_dir)
|
|
1311
|
+
session_id = build_session(name: session_name, working_dir: working_dir, permission_mode: :auto_approve)
|
|
1312
|
+
@registry.update(session_id, pending_task: prompt, pending_working_dir: working_dir)
|
|
1343
1313
|
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
end
|
|
1314
|
+
json_response(res, 202, { ok: true, session: @registry.session_summary(session_id) })
|
|
1315
|
+
rescue => e
|
|
1316
|
+
json_response(res, 422, { error: e.message })
|
|
1348
1317
|
end
|
|
1349
1318
|
|
|
1350
1319
|
# ── Skills API ────────────────────────────────────────────────────────────
|
|
@@ -85,6 +85,61 @@ module Clacky
|
|
|
85
85
|
list.size < before_count
|
|
86
86
|
end
|
|
87
87
|
|
|
88
|
+
# Update an existing schedule entry (cron and/or enabled).
|
|
89
|
+
# Returns false if the schedule does not exist.
|
|
90
|
+
def update_schedule(name, cron: nil, enabled: nil)
|
|
91
|
+
list = load_schedules
|
|
92
|
+
entry = list.find { |s| s["name"] == name }
|
|
93
|
+
return false unless entry
|
|
94
|
+
|
|
95
|
+
entry["cron"] = cron unless cron.nil?
|
|
96
|
+
entry["enabled"] = enabled unless enabled.nil?
|
|
97
|
+
save_schedules(list)
|
|
98
|
+
true
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# ── Composite cron-task helpers ──────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
# Create a task file and its schedule in one step.
|
|
104
|
+
def create_cron_task(name:, content:, cron:, enabled: true)
|
|
105
|
+
write_task(name, content)
|
|
106
|
+
add_schedule(name: name, task: name, cron: cron, enabled: enabled)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Update a cron-task: optionally update content and/or schedule fields.
|
|
110
|
+
def update_cron_task(name, content: nil, cron: nil, enabled: nil)
|
|
111
|
+
raise "Cron task not found: #{name}" unless list_tasks.include?(name)
|
|
112
|
+
|
|
113
|
+
write_task(name, content) unless content.nil?
|
|
114
|
+
update_schedule(name, cron: cron, enabled: enabled) if cron || !enabled.nil?
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Delete a cron-task: remove both the task file and its schedule.
|
|
118
|
+
def delete_cron_task(name)
|
|
119
|
+
removed_schedule = remove_schedule(name)
|
|
120
|
+
removed_task = delete_task(name)
|
|
121
|
+
removed_schedule || removed_task
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Return a merged list of cron-tasks (task content + schedule metadata).
|
|
125
|
+
def list_cron_tasks
|
|
126
|
+
schedule_map = load_schedules.each_with_object({}) do |s, h|
|
|
127
|
+
h[s["task"]] = s if s.is_a?(Hash)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
list_tasks.map do |task_name|
|
|
131
|
+
content = begin; read_task(task_name); rescue StandardError; ""; end
|
|
132
|
+
schedule = schedule_map[task_name] || {}
|
|
133
|
+
{
|
|
134
|
+
"name" => task_name,
|
|
135
|
+
"content" => content,
|
|
136
|
+
"cron" => schedule["cron"],
|
|
137
|
+
"enabled" => schedule.fetch("enabled", true),
|
|
138
|
+
"scheduled" => !schedule.empty?
|
|
139
|
+
}
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
88
143
|
# ── Task file helpers ────────────────────────────────────────────────────
|
|
89
144
|
|
|
90
145
|
# Read the prompt content of a named task.
|
data/lib/clacky/tools/browser.rb
CHANGED
|
@@ -486,7 +486,7 @@ module Clacky
|
|
|
486
486
|
# -----------------------------------------------------------------------
|
|
487
487
|
|
|
488
488
|
private def find_node_binary
|
|
489
|
-
path = `which node 2>/dev/null
|
|
489
|
+
path = Clacky::Utils::Encoding.cmd_to_utf8(`which node 2>/dev/null`, source_encoding: "UTF-8").strip
|
|
490
490
|
return nil if path.empty? || !File.executable?(path)
|
|
491
491
|
path
|
|
492
492
|
end
|
|
@@ -494,7 +494,7 @@ module Clacky
|
|
|
494
494
|
private def node_major_version
|
|
495
495
|
node = find_node_binary
|
|
496
496
|
return nil unless node
|
|
497
|
-
`#{node} --version 2>/dev/null
|
|
497
|
+
Clacky::Utils::Encoding.cmd_to_utf8(`#{node} --version 2>/dev/null`, source_encoding: "UTF-8").strip.gsub(/^v/, "").split(".").first.to_i
|
|
498
498
|
end
|
|
499
499
|
|
|
500
500
|
private def node_error
|
|
@@ -54,6 +54,27 @@ module Clacky
|
|
|
54
54
|
str.encode("UTF-8", "UTF-8", invalid: :replace, undef: :replace, replace: "")
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
+
# Convert raw shell command output to valid UTF-8.
|
|
58
|
+
# Handles two common cases:
|
|
59
|
+
# - Windows commands (e.g. powershell.exe) that output GBK/CP936 bytes
|
|
60
|
+
# - Unix commands that output UTF-8 or ASCII bytes with ASCII-8BIT encoding
|
|
61
|
+
#
|
|
62
|
+
# Strategy: try GBK decode first (superset of ASCII, covers Chinese Windows);
|
|
63
|
+
# if that fails fall back to UTF-8 scrub.
|
|
64
|
+
#
|
|
65
|
+
# @param data [String, nil] raw bytes from backtick / IO.popen
|
|
66
|
+
# @param source_encoding [String] hint for source encoding (default: "GBK")
|
|
67
|
+
# @return [String] valid UTF-8 string
|
|
68
|
+
def self.cmd_to_utf8(data, source_encoding: "GBK")
|
|
69
|
+
return "" if data.nil? || data.empty?
|
|
70
|
+
|
|
71
|
+
data.dup
|
|
72
|
+
.force_encoding(source_encoding)
|
|
73
|
+
.encode("UTF-8", invalid: :replace, undef: :replace, replace: "")
|
|
74
|
+
rescue Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError
|
|
75
|
+
to_utf8(data)
|
|
76
|
+
end
|
|
77
|
+
|
|
57
78
|
# Return an ASCII-safe UTF-8 copy of *str* suitable for security regex
|
|
58
79
|
# pattern matching. Any byte that is not valid in the source encoding, or
|
|
59
80
|
# that cannot be represented in UTF-8, is replaced with '?'. The
|