cloudflare-to-claude-fix 1.0.1
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 +153 -0
- package/package.json +18 -0
- package/src/index.js +116 -0
- package/wrangler.jsonc +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img width="400px" src="https://i.imgur.com/RQP3lma.png" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
A Cloudflare Workers **Queue consumer** that fires a **Claude Code routine** whenever a Workers build fails. Required Workers Paid and Claude Pro.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Setup
|
|
10
|
+
|
|
11
|
+
### 1. Clone / copy this project
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
git clone <your-repo>
|
|
15
|
+
cd cloudflare-to-claude-fix
|
|
16
|
+
npm install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### 2. Create the Cloudflare Queue
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Main queue
|
|
23
|
+
wrangler queues create workers-build-events
|
|
24
|
+
|
|
25
|
+
# Dead-letter queue (catches messages that fail all 3 retries)
|
|
26
|
+
wrangler queues create workers-build-events-dlq
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 3. Enable Event Subscriptions on your target Worker
|
|
30
|
+
|
|
31
|
+
In the Cloudflare dashboard:
|
|
32
|
+
|
|
33
|
+
1. Open **Workers & Pages** → select the Worker you want to monitor
|
|
34
|
+
2. Go to **Settings** → **Event Subscriptions**
|
|
35
|
+
3. Set the queue to `workers-build-events`
|
|
36
|
+
4. Enable: **Build started**, **Build succeeded**, **Build failed**, **Build cancelled**
|
|
37
|
+
|
|
38
|
+
This Worker only acts on `status === "failed"` events; the rest are acknowledged and discarded.
|
|
39
|
+
|
|
40
|
+
### 4. Create a Claude Code routine
|
|
41
|
+
|
|
42
|
+
1. Go to [claude.ai/code/routines](https://claude.ai/code/routines)
|
|
43
|
+
2. Click **New routine**
|
|
44
|
+
3. Configure:
|
|
45
|
+
- **Prompt:** e.g. *"A Cloudflare Workers build has failed. The build ID, branch, commit, author and error log are below. Investigate the error, identify the root cause, and push a fix to the branch. Summarise what you changed."*
|
|
46
|
+
- **Repository:** the repo that this Worker is deployed from
|
|
47
|
+
4. Under **Select a trigger** → **Add another trigger** → choose **API**
|
|
48
|
+
5. Click **Generate token** — copy the token (shown **once**)
|
|
49
|
+
6. Copy the full fire URL shown in the modal (format: `https://api.anthropic.com/v1/claude_code/routines/trig_<id>/fire`)
|
|
50
|
+
|
|
51
|
+
### 5. Store secrets in Wrangler
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# Required
|
|
55
|
+
wrangler secret put ROUTINE_FIRE_URL
|
|
56
|
+
# paste: https://api.anthropic.com/v1/claude_code/routines/trig_<id>/fire
|
|
57
|
+
|
|
58
|
+
wrangler secret put ROUTINE_FIRE_TOKEN
|
|
59
|
+
# paste: sk-ant-oat01-...
|
|
60
|
+
|
|
61
|
+
# Optional — post session link to Slack or Discord
|
|
62
|
+
wrangler secret put NOTIFY_WEBHOOK_URL
|
|
63
|
+
# paste: https://hooks.slack.com/services/... (Slack)
|
|
64
|
+
# or: https://discord.com/api/webhooks/... (Discord)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
> **Never** commit secrets to source control or paste them in plaintext anywhere.
|
|
68
|
+
|
|
69
|
+
### 6. Deploy
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
wrangler deploy
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## How it works
|
|
78
|
+
|
|
79
|
+
1. Cloudflare Builds publishes a `BuildEvent` message to `workers-build-events` when a build status changes.
|
|
80
|
+
2. This consumer Worker receives the batch. Non-failure messages are `ack()`-ed immediately.
|
|
81
|
+
3. For `status === "failed"` messages the Worker:
|
|
82
|
+
- Formats the `build_id`, `worker_name`, `branch`, `commit_hash`, `author`, `timestamp`, and `error_messages` into a single plaintext block (≤ 65,536 chars).
|
|
83
|
+
- POSTs that block to the Claude Code routine `/fire` endpoint.
|
|
84
|
+
- If `NOTIFY_WEBHOOK_URL` is set, posts the resulting `claude_code_session_url` to Slack/Discord so your team can watch the live debugging session.
|
|
85
|
+
4. If the fire request fails, the message is `retry()`-ed up to 3 times (configured in `wrangler.toml`). After 3 failures the message lands in `workers-build-events-dlq`.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Claude Code routine `/fire` API quick-reference
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
POST https://api.anthropic.com/v1/claude_code/routines/{routine_id}/fire
|
|
93
|
+
Authorization: Bearer sk-ant-oat01-...
|
|
94
|
+
anthropic-version: 2023-06-01
|
|
95
|
+
anthropic-beta: experimental-cc-routine-2026-04-01
|
|
96
|
+
Content-Type: application/json
|
|
97
|
+
|
|
98
|
+
{ "text": "<up to 65,536 chars of context>" }
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Response:**
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"type": "routine_fire",
|
|
106
|
+
"claude_code_session_id": "session_01...",
|
|
107
|
+
"claude_code_session_url": "https://claude.ai/code/session_01..."
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
| Status | Cause |
|
|
112
|
+
| ------- | -------------------------------------------------------------- |
|
|
113
|
+
| `400` | Missing beta header, text > 65 536 chars, or routine is paused |
|
|
114
|
+
| `401` | Wrong or missing bearer token |
|
|
115
|
+
| `403` | Account doesn't have Claude Code on the web |
|
|
116
|
+
| `404` | Routine ID not found |
|
|
117
|
+
| `429` | Daily run allowance exhausted |
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Revoking / rotating the routine token
|
|
122
|
+
|
|
123
|
+
1. Open [claude.ai/code/routines](https://claude.ai/code/routines) → edit the routine
|
|
124
|
+
2. Click the API trigger → **Generate token** (this immediately revokes the old one)
|
|
125
|
+
3. Update the secret: `wrangler secret put ROUTINE_FIRE_TOKEN`
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Local testing
|
|
130
|
+
|
|
131
|
+
Send a synthetic failed-build message to the queue:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
wrangler queues publish workers-build-events \
|
|
135
|
+
--message '{
|
|
136
|
+
"build_id": "build_test001",
|
|
137
|
+
"status": "failed",
|
|
138
|
+
"worker_name": "my-api",
|
|
139
|
+
"branch": "feat/new-endpoint",
|
|
140
|
+
"commit_hash": "abc1234",
|
|
141
|
+
"author": "alex@example.com",
|
|
142
|
+
"error_messages": ["Error: Cannot find module '\''./utils'\''", " at Object.<anonymous> (src/index.ts:3:1)"],
|
|
143
|
+
"timestamp": "2026-05-01T19:00:00Z"
|
|
144
|
+
}'
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Then tail logs to verify:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
wrangler tail cloudflare-to-claude-fix
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
---
|
package/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cloudflare-to-claude-fix",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/OpenSourceAGI/appdemo-dev-tools/tree/master/packages/cloudflare-to-claude-fix"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/OpenSourceAGI/appdemo-dev-tools/tree/master/packages/cloudflare-to-claude-fix",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"deploy": "wrangler deploy",
|
|
12
|
+
"dev": "wrangler dev",
|
|
13
|
+
"tail": "wrangler tail"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"wrangler": "^3.x"
|
|
17
|
+
}
|
|
18
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare Workers Queue Consumer
|
|
3
|
+
* Fires a Claude Code routine when a Workers build fails.
|
|
4
|
+
*
|
|
5
|
+
* Event flow:
|
|
6
|
+
* CF Workers Builds → Queue → this Worker → Claude Code /fire endpoint
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const MAX_TEXT_BYTES = 65_536;
|
|
10
|
+
|
|
11
|
+
function truncate(s) {
|
|
12
|
+
if (s.length <= MAX_TEXT_BYTES) return s;
|
|
13
|
+
return (
|
|
14
|
+
s.slice(0, MAX_TEXT_BYTES - 200) +
|
|
15
|
+
"\n\n[... log truncated to fit 65,536-char limit ...]"
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function buildPayloadText(evt) {
|
|
20
|
+
const lines = [
|
|
21
|
+
`⚠️ Cloudflare Workers build FAILED`,
|
|
22
|
+
`Worker : ${evt.worker_name}`,
|
|
23
|
+
`Build : ${evt.build_id}`,
|
|
24
|
+
`Branch : ${evt.branch ?? "(unknown)"}`,
|
|
25
|
+
`Commit : ${evt.commit_hash ?? "(unknown)"}`,
|
|
26
|
+
`Author : ${evt.author ?? "(unknown)"}`,
|
|
27
|
+
`Time : ${evt.timestamp}`,
|
|
28
|
+
``,
|
|
29
|
+
`=== Error log ===`,
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
if (evt.error_messages && evt.error_messages.length > 0) {
|
|
33
|
+
lines.push(...evt.error_messages);
|
|
34
|
+
} else {
|
|
35
|
+
lines.push(
|
|
36
|
+
"(no structured error messages in event; check Logpush for full output)",
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return truncate(lines.join("\n"));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function fireRoutine(text, env) {
|
|
44
|
+
const res = await fetch(env.ROUTINE_FIRE_URL, {
|
|
45
|
+
method: "POST",
|
|
46
|
+
headers: {
|
|
47
|
+
Authorization: `Bearer ${env.ROUTINE_FIRE_TOKEN}`,
|
|
48
|
+
"anthropic-version": "2023-06-01",
|
|
49
|
+
"anthropic-beta": "experimental-cc-routine-2026-04-01",
|
|
50
|
+
"Content-Type": "application/json",
|
|
51
|
+
},
|
|
52
|
+
body: JSON.stringify({ text }),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (!res.ok) {
|
|
56
|
+
const body = await res.text();
|
|
57
|
+
throw new Error(`Routine fire failed (${res.status}): ${body}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return res.json();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function notifyWebhook(evt, sessionUrl, webhookUrl) {
|
|
64
|
+
const text =
|
|
65
|
+
`🤖 Claude Code is investigating the failed \`${evt.worker_name}\` build ` +
|
|
66
|
+
`(branch: \`${evt.branch ?? "?"}\`, commit: \`${evt.commit_hash?.slice(0, 7) ?? "?"}\`).\n` +
|
|
67
|
+
`👉 Live session: ${sessionUrl}`;
|
|
68
|
+
|
|
69
|
+
await fetch(webhookUrl, {
|
|
70
|
+
method: "POST",
|
|
71
|
+
headers: { "Content-Type": "application/json" },
|
|
72
|
+
body: JSON.stringify({ text, content: text }), // works for both Slack and Discord
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export default {
|
|
77
|
+
async queue(batch, env) {
|
|
78
|
+
for (const message of batch.messages) {
|
|
79
|
+
const evt = message.body;
|
|
80
|
+
|
|
81
|
+
if (evt.status !== "failed") {
|
|
82
|
+
message.ack();
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
console.log(
|
|
88
|
+
`[cloudflare-to-claude-fix] Build ${evt.build_id} for worker "${evt.worker_name}" failed — firing routine.`,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const text = buildPayloadText(evt);
|
|
92
|
+
const { claude_code_session_url } = await fireRoutine(text, env);
|
|
93
|
+
|
|
94
|
+
console.log(
|
|
95
|
+
`[cloudflare-to-claude-fix] Routine fired. Session: ${claude_code_session_url}`,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
if (env.NOTIFY_WEBHOOK_URL) {
|
|
99
|
+
await notifyWebhook(
|
|
100
|
+
evt,
|
|
101
|
+
claude_code_session_url,
|
|
102
|
+
env.NOTIFY_WEBHOOK_URL,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
message.ack();
|
|
107
|
+
} catch (err) {
|
|
108
|
+
console.error(
|
|
109
|
+
`[cloudflare-to-claude-fix] Error processing build ${evt.build_id}:`,
|
|
110
|
+
err,
|
|
111
|
+
);
|
|
112
|
+
message.retry();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
};
|
package/wrangler.jsonc
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
// ── Core ────────────────────────────────────────────────────────────────────
|
|
3
|
+
"name": "cloudflare-to-claude-fix",
|
|
4
|
+
"main": "src/index.js",
|
|
5
|
+
"compatibility_date": "2025-11-01",
|
|
6
|
+
|
|
7
|
+
// ── Queue consumer ──────────────────────────────────────────────────────────
|
|
8
|
+
// Create the queues first:
|
|
9
|
+
// wrangler queues create workers-build-events
|
|
10
|
+
// wrangler queues create workers-build-events-dlq
|
|
11
|
+
"queues": {
|
|
12
|
+
"consumers": [
|
|
13
|
+
{
|
|
14
|
+
"queue": "workers-build-events",
|
|
15
|
+
// Max messages pulled per invocation
|
|
16
|
+
"max_batch_size": 10,
|
|
17
|
+
// Seconds to wait to fill a batch before invoking early
|
|
18
|
+
"max_batch_timeout": 30,
|
|
19
|
+
// Per-message retry attempts before routing to DLQ
|
|
20
|
+
"max_retries": 3,
|
|
21
|
+
// Catches messages that exhaust all retries
|
|
22
|
+
"dead_letter_queue": "workers-build-events-dlq",
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
// ── Secrets ─────────────────────────────────────────────────────────────────
|
|
28
|
+
// Set these with `wrangler secret put <NAME>` — never hardcode values here.
|
|
29
|
+
//
|
|
30
|
+
// ROUTINE_FIRE_URL — https://api.anthropic.com/v1/claude_code/routines/trig_<id>/fire
|
|
31
|
+
// ROUTINE_FIRE_TOKEN — sk-ant-oat01-... (from Claude Code routine API trigger)
|
|
32
|
+
// NOTIFY_WEBHOOK_URL — (optional) Slack or Discord incoming webhook URL
|
|
33
|
+
"vars": {},
|
|
34
|
+
}
|