@a5c-ai/babysitter-breakpoints 0.0.29 → 0.0.30
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 +62 -17
- package/bin/breakpoints.js +0 -63
- package/extensions/index.js +10 -2
- package/extensions/telegram.js +320 -29
- package/package.json +1 -1
- package/web/app.js +3 -6
- package/web/styles.css +171 -0
- package/worker/worker.js +27 -6
- package/.codex/skills/babysitter-breakpoint/SKILL.md +0 -35
- package/.codex/skills/babysitter-breakpoint/references/breakpoint-api.md +0 -57
package/README.md
CHANGED
|
@@ -52,11 +52,6 @@ Wait for release:
|
|
|
52
52
|
breakpoints breakpoint wait <id> --interval 3
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
Install the babysitter-breakpoint skill:
|
|
56
|
-
```bash
|
|
57
|
-
breakpoints install-skill --target codex --scope global
|
|
58
|
-
```
|
|
59
|
-
|
|
60
55
|
## Configuration
|
|
61
56
|
Environment variables:
|
|
62
57
|
- `PORT` (default 3185)
|
|
@@ -115,22 +110,72 @@ breakpoint payload.
|
|
|
115
110
|
## Web UI
|
|
116
111
|
Open `http://localhost:3184` and provide the human token in the UI.
|
|
117
112
|
|
|
118
|
-
##
|
|
113
|
+
## Telegram Extension
|
|
114
|
+
|
|
115
|
+
The Telegram extension allows you to receive breakpoint notifications and interact with them via Telegram.
|
|
116
|
+
|
|
117
|
+
### Setup
|
|
118
|
+
1. Create a Telegram bot via [@BotFather](https://t.me/botfather)
|
|
119
|
+
2. Get your bot token
|
|
120
|
+
3. Configure the extension:
|
|
119
121
|
```bash
|
|
120
|
-
breakpoints
|
|
122
|
+
breakpoints extension enable telegram --token <bot-token> --username <your-username>
|
|
121
123
|
```
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
124
|
+
|
|
125
|
+
### Features
|
|
126
|
+
|
|
127
|
+
**Automatic Notifications:**
|
|
128
|
+
- Receive notification when a new breakpoint is created
|
|
129
|
+
- Get notified when a breakpoint is released
|
|
130
|
+
|
|
131
|
+
**Connection:**
|
|
132
|
+
Send any slash command (e.g., `/start`) to your bot to connect. The bot will:
|
|
133
|
+
- Confirm connection
|
|
134
|
+
- Show all waiting breakpoints with titles, IDs, run IDs, and questions
|
|
135
|
+
- Provide instructions for interacting with breakpoints
|
|
136
|
+
|
|
137
|
+
**Commands:**
|
|
138
|
+
- `list` (or `ls`, `waiting`) - Show all waiting breakpoints
|
|
139
|
+
- `preview <number>` (or `show <number>`) - View full details of a breakpoint
|
|
140
|
+
- `file <number>` - Download a context file by its number
|
|
141
|
+
- `file <path>` - Download a context file by its path
|
|
142
|
+
- `raw <path>` - View file inline with syntax highlighting (if short enough)
|
|
143
|
+
|
|
144
|
+
**Releasing Breakpoints:**
|
|
145
|
+
- Reply to any breakpoint message to release it
|
|
146
|
+
- Send the breakpoint ID to release it
|
|
147
|
+
- Send any text message to release the most recent breakpoint
|
|
148
|
+
|
|
149
|
+
**Example Workflow:**
|
|
130
150
|
```
|
|
131
|
-
|
|
151
|
+
You: /start
|
|
152
|
+
Bot: Telegram connected. I will notify you about breakpoints here.
|
|
153
|
+
|
|
154
|
+
🔔 You have 2 waiting breakpoints:
|
|
155
|
+
|
|
156
|
+
1. Approve refactoring plan
|
|
157
|
+
ID: abc-123
|
|
158
|
+
Run: run-20260120-refactor
|
|
159
|
+
Created: 5 mins ago
|
|
160
|
+
Question: Should I proceed with this refactoring?
|
|
132
161
|
|
|
133
|
-
|
|
162
|
+
2. Review API changes
|
|
163
|
+
ID: def-456
|
|
164
|
+
Run: run-20260120-api
|
|
165
|
+
Created: 2 mins ago
|
|
166
|
+
Question: Are these API changes acceptable?
|
|
167
|
+
|
|
168
|
+
You: preview 1
|
|
169
|
+
Bot: [Shows full details including all context files]
|
|
170
|
+
|
|
171
|
+
You: file 1
|
|
172
|
+
Bot: [Sends the first context file as document]
|
|
173
|
+
|
|
174
|
+
You: Looks good, proceed!
|
|
175
|
+
Bot: ✅ Breakpoint released
|
|
176
|
+
ID: abc-123
|
|
177
|
+
Feedback: Looks good, proceed!
|
|
178
|
+
```
|
|
134
179
|
|
|
135
180
|
## Breakpoint CLI (agent-friendly)
|
|
136
181
|
Create a breakpoint:
|
package/bin/breakpoints.js
CHANGED
|
@@ -9,7 +9,6 @@ function usage() {
|
|
|
9
9
|
console.log(`Usage:
|
|
10
10
|
breakpoints start
|
|
11
11
|
breakpoints run
|
|
12
|
-
breakpoints install-skill [--source <path>] [--target codex|claude|cursor] [--scope local|global]
|
|
13
12
|
breakpoints breakpoint create --question <text> [--run-id <id>] [--title <title>] [--agent-id <id>] [--tag <tag>] [--ttl <seconds>] [--file <path,format,language,label>]
|
|
14
13
|
breakpoints breakpoint status <id>
|
|
15
14
|
breakpoints breakpoint show <id>
|
|
@@ -169,61 +168,6 @@ function runSystem() {
|
|
|
169
168
|
runCommand("node", [runner], { cwd: repoRoot, env });
|
|
170
169
|
}
|
|
171
170
|
|
|
172
|
-
function installSkill(sourcePath) {
|
|
173
|
-
const skillSource =
|
|
174
|
-
sourcePath ||
|
|
175
|
-
path.join(__dirname, "..", ".codex", "skills", "babysitter-breakpoint");
|
|
176
|
-
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
177
|
-
if (!homeDir) {
|
|
178
|
-
console.error("HOME is not set.");
|
|
179
|
-
process.exitCode = 1;
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
const dest = resolveSkillDest(skillSource);
|
|
183
|
-
if (fs.existsSync(dest)) {
|
|
184
|
-
console.error(`Destination already exists: ${dest}`);
|
|
185
|
-
process.exitCode = 1;
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
189
|
-
fs.cpSync(skillSource, dest, { recursive: true });
|
|
190
|
-
console.log("Installed babysitter-breakpoint skill.");
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function resolveSkillDest(skillSource) {
|
|
194
|
-
const flags = parseFlags(process.argv.slice(2));
|
|
195
|
-
const target = String(flags.target || "codex");
|
|
196
|
-
const scope = String(flags.scope || "global");
|
|
197
|
-
const repoRoot = path.join(__dirname, "..");
|
|
198
|
-
const homeDir = process.env.HOME || process.env.USERPROFILE || "";
|
|
199
|
-
const skillName = path.basename(skillSource);
|
|
200
|
-
|
|
201
|
-
if (scope === "local") {
|
|
202
|
-
if (target === "codex") {
|
|
203
|
-
return path.join(repoRoot, ".codex", "skills", skillName);
|
|
204
|
-
}
|
|
205
|
-
if (target === "claude") {
|
|
206
|
-
return path.join(repoRoot, ".claude", "skills", skillName);
|
|
207
|
-
}
|
|
208
|
-
if (target === "cursor") {
|
|
209
|
-
return path.join(repoRoot, ".cursor", "skills", skillName);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
if (target === "codex") {
|
|
214
|
-
const codexHome = process.env.CODEX_HOME || path.join(homeDir, ".codex");
|
|
215
|
-
return path.join(codexHome, "skills", skillName);
|
|
216
|
-
}
|
|
217
|
-
if (target === "claude") {
|
|
218
|
-
return path.join(homeDir, ".claude", "skills", skillName);
|
|
219
|
-
}
|
|
220
|
-
if (target === "cursor") {
|
|
221
|
-
return path.join(homeDir, ".cursor", "skills", skillName);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
throw new Error(`Unknown target: ${target}`);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
171
|
const args = process.argv.slice(2);
|
|
228
172
|
const cmd = args[0];
|
|
229
173
|
|
|
@@ -237,13 +181,6 @@ if (cmd === "start" || cmd === "run") {
|
|
|
237
181
|
return;
|
|
238
182
|
}
|
|
239
183
|
|
|
240
|
-
if (cmd === "install-skill") {
|
|
241
|
-
const sourceIndex = args.indexOf("--source");
|
|
242
|
-
const sourcePath = sourceIndex >= 0 ? args[sourceIndex + 1] : undefined;
|
|
243
|
-
installSkill(sourcePath);
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
184
|
if (cmd === "breakpoint") {
|
|
248
185
|
const flags = parseFlags(args.slice(1));
|
|
249
186
|
const subcmd = flags._[0];
|
package/extensions/index.js
CHANGED
|
@@ -24,7 +24,11 @@ async function dispatch(event, db, breakpoint) {
|
|
|
24
24
|
if (!extension || typeof extension[event] !== "function") {
|
|
25
25
|
continue;
|
|
26
26
|
}
|
|
27
|
-
|
|
27
|
+
try {
|
|
28
|
+
await extension[event](db, breakpoint, cfg.config);
|
|
29
|
+
} catch (err) {
|
|
30
|
+
console.error(`[extensions] error in ${cfg.name}.${event}:`, err.message);
|
|
31
|
+
}
|
|
28
32
|
}
|
|
29
33
|
}
|
|
30
34
|
|
|
@@ -35,7 +39,11 @@ async function poll(db) {
|
|
|
35
39
|
if (!extension || typeof extension.poll !== "function") {
|
|
36
40
|
continue;
|
|
37
41
|
}
|
|
38
|
-
|
|
42
|
+
try {
|
|
43
|
+
await extension.poll(db, cfg.config);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
console.error(`[extensions] error in ${cfg.name}.poll:`, err.message);
|
|
46
|
+
}
|
|
39
47
|
}
|
|
40
48
|
}
|
|
41
49
|
|
package/extensions/telegram.js
CHANGED
|
@@ -9,10 +9,15 @@ function apiBase(token) {
|
|
|
9
9
|
return `https://api.telegram.org/bot${token}`;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
function callTelegram(url) {
|
|
12
|
+
function callTelegram(url, options = {}) {
|
|
13
|
+
const { timeout = 15000, retries = 3, retryDelay = 1000 } = options;
|
|
14
|
+
|
|
13
15
|
return new Promise((resolve, reject) => {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
let attempt = 0;
|
|
17
|
+
|
|
18
|
+
const makeRequest = () => {
|
|
19
|
+
attempt++;
|
|
20
|
+
const req = https.get(url, { timeout, family: 4 }, (res) => {
|
|
16
21
|
let data = "";
|
|
17
22
|
res.on("data", (chunk) => {
|
|
18
23
|
data += chunk;
|
|
@@ -24,8 +29,40 @@ function callTelegram(url) {
|
|
|
24
29
|
reject(err);
|
|
25
30
|
}
|
|
26
31
|
});
|
|
27
|
-
})
|
|
28
|
-
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
req.on("timeout", () => {
|
|
35
|
+
req.destroy();
|
|
36
|
+
handleError(new Error(`Request timeout after ${timeout}ms`));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
req.on("error", (err) => {
|
|
40
|
+
handleError(err);
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const handleError = (err) => {
|
|
45
|
+
const isRetryable =
|
|
46
|
+
err.code === "ETIMEDOUT" ||
|
|
47
|
+
err.code === "ECONNRESET" ||
|
|
48
|
+
err.code === "ENOTFOUND" ||
|
|
49
|
+
err.code === "EAI_AGAIN";
|
|
50
|
+
|
|
51
|
+
if (isRetryable && attempt < retries) {
|
|
52
|
+
const delay = retryDelay * Math.pow(2, attempt - 1);
|
|
53
|
+
if (process.env.TELEGRAM_DEBUG === "1") {
|
|
54
|
+
console.log(`[telegram] retry ${attempt}/${retries} after ${delay}ms: ${err.message}`);
|
|
55
|
+
}
|
|
56
|
+
setTimeout(makeRequest, delay);
|
|
57
|
+
} else {
|
|
58
|
+
if (process.env.TELEGRAM_DEBUG === "1") {
|
|
59
|
+
console.log(`[telegram] request failed after ${attempt} attempts: ${err.message}`);
|
|
60
|
+
}
|
|
61
|
+
reject(err);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
makeRequest();
|
|
29
66
|
});
|
|
30
67
|
}
|
|
31
68
|
|
|
@@ -37,7 +74,8 @@ async function sendMessage(token, chatId, text, parseMode) {
|
|
|
37
74
|
return callTelegram(url);
|
|
38
75
|
}
|
|
39
76
|
|
|
40
|
-
async function sendDocument(token, chatId, filename, content) {
|
|
77
|
+
async function sendDocument(token, chatId, filename, content, options = {}) {
|
|
78
|
+
const { timeout = 30000, retries = 3, retryDelay = 1000 } = options;
|
|
41
79
|
const boundary = `----bp-${Date.now()}`;
|
|
42
80
|
const payload = [
|
|
43
81
|
`--${boundary}`,
|
|
@@ -53,30 +91,71 @@ async function sendDocument(token, chatId, filename, content) {
|
|
|
53
91
|
"",
|
|
54
92
|
].join("\r\n");
|
|
55
93
|
const url = `${apiBase(token)}/sendDocument`;
|
|
94
|
+
|
|
56
95
|
return new Promise((resolve, reject) => {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
96
|
+
let attempt = 0;
|
|
97
|
+
|
|
98
|
+
const makeRequest = () => {
|
|
99
|
+
attempt++;
|
|
100
|
+
const req = https.request(url, {
|
|
101
|
+
method: "POST",
|
|
102
|
+
headers: {
|
|
103
|
+
"Content-Type": `multipart/form-data; boundary=${boundary}`,
|
|
104
|
+
"Content-Length": Buffer.byteLength(payload),
|
|
105
|
+
},
|
|
106
|
+
timeout,
|
|
107
|
+
family: 4,
|
|
68
108
|
});
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
109
|
+
|
|
110
|
+
req.on("response", (res) => {
|
|
111
|
+
let data = "";
|
|
112
|
+
res.on("data", (chunk) => {
|
|
113
|
+
data += chunk;
|
|
114
|
+
});
|
|
115
|
+
res.on("end", () => {
|
|
116
|
+
try {
|
|
117
|
+
resolve(JSON.parse(data));
|
|
118
|
+
} catch (err) {
|
|
119
|
+
reject(err);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
75
122
|
});
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
123
|
+
|
|
124
|
+
req.on("timeout", () => {
|
|
125
|
+
req.destroy();
|
|
126
|
+
handleError(new Error(`Request timeout after ${timeout}ms`));
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
req.on("error", (err) => {
|
|
130
|
+
handleError(err);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
req.write(payload);
|
|
134
|
+
req.end();
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const handleError = (err) => {
|
|
138
|
+
const isRetryable =
|
|
139
|
+
err.code === "ETIMEDOUT" ||
|
|
140
|
+
err.code === "ECONNRESET" ||
|
|
141
|
+
err.code === "ENOTFOUND" ||
|
|
142
|
+
err.code === "EAI_AGAIN";
|
|
143
|
+
|
|
144
|
+
if (isRetryable && attempt < retries) {
|
|
145
|
+
const delay = retryDelay * Math.pow(2, attempt - 1);
|
|
146
|
+
if (process.env.TELEGRAM_DEBUG === "1") {
|
|
147
|
+
console.log(`[telegram] retry ${attempt}/${retries} after ${delay}ms: ${err.message}`);
|
|
148
|
+
}
|
|
149
|
+
setTimeout(makeRequest, delay);
|
|
150
|
+
} else {
|
|
151
|
+
if (process.env.TELEGRAM_DEBUG === "1") {
|
|
152
|
+
console.log(`[telegram] request failed after ${attempt} attempts: ${err.message}`);
|
|
153
|
+
}
|
|
154
|
+
reject(err);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
makeRequest();
|
|
80
159
|
});
|
|
81
160
|
}
|
|
82
161
|
|
|
@@ -338,7 +417,15 @@ async function poll(db, config) {
|
|
|
338
417
|
if (process.env.TELEGRAM_DEBUG === "1") {
|
|
339
418
|
console.log(`[telegram] requesting updates offset=${offset}`);
|
|
340
419
|
}
|
|
341
|
-
|
|
420
|
+
|
|
421
|
+
let response;
|
|
422
|
+
try {
|
|
423
|
+
response = await callTelegram(url, { timeout: 15000, retries: 2 });
|
|
424
|
+
} catch (err) {
|
|
425
|
+
console.error(`[telegram] poll error: ${err.message}`);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
342
429
|
if (!response.ok) {
|
|
343
430
|
if (process.env.TELEGRAM_DEBUG === "1") {
|
|
344
431
|
console.log(
|
|
@@ -391,6 +478,80 @@ async function poll(db, config) {
|
|
|
391
478
|
await persistUser(db, config, message.chat.id, message.from.id);
|
|
392
479
|
}
|
|
393
480
|
const trimmed = message.text.trim();
|
|
481
|
+
|
|
482
|
+
// Handle list command to show waiting breakpoints
|
|
483
|
+
const listMatch = trimmed.match(/^(list|ls|waiting)$/i);
|
|
484
|
+
if (listMatch) {
|
|
485
|
+
const { all } = require("../api/db");
|
|
486
|
+
const waitingBreakpoints = await all(
|
|
487
|
+
db,
|
|
488
|
+
"SELECT id, title, created_at, run_id, payload FROM breakpoints WHERE status = ? ORDER BY created_at DESC",
|
|
489
|
+
["waiting"]
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
if (waitingBreakpoints.length === 0) {
|
|
493
|
+
if (message.chat?.id) {
|
|
494
|
+
await sendMessage(
|
|
495
|
+
config.token,
|
|
496
|
+
message.chat.id,
|
|
497
|
+
"✅ No waiting breakpoints."
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const messages = [
|
|
504
|
+
`🔔 ${waitingBreakpoints.length} waiting breakpoint${waitingBreakpoints.length === 1 ? '' : 's'}:`,
|
|
505
|
+
""
|
|
506
|
+
];
|
|
507
|
+
|
|
508
|
+
for (let i = 0; i < waitingBreakpoints.length; i++) {
|
|
509
|
+
const bp = waitingBreakpoints[i];
|
|
510
|
+
const title = bp.title || "Untitled";
|
|
511
|
+
const runId = bp.run_id || "unknown";
|
|
512
|
+
const age = Math.floor((Date.now() - new Date(bp.created_at).getTime()) / 1000 / 60);
|
|
513
|
+
const ageStr = age < 1 ? "just now" : age === 1 ? "1 min ago" : `${age} mins ago`;
|
|
514
|
+
|
|
515
|
+
messages.push(`${i + 1}. ${title}`);
|
|
516
|
+
messages.push(` ID: ${bp.id}`);
|
|
517
|
+
messages.push(` Run: ${runId}`);
|
|
518
|
+
messages.push(` Created: ${ageStr}`);
|
|
519
|
+
|
|
520
|
+
// Extract question from payload if available
|
|
521
|
+
try {
|
|
522
|
+
const payload = JSON.parse(bp.payload);
|
|
523
|
+
if (payload?.question) {
|
|
524
|
+
const question = payload.question.length > 100
|
|
525
|
+
? `${payload.question.substring(0, 100)}...`
|
|
526
|
+
: payload.question;
|
|
527
|
+
messages.push(` Question: ${question}`);
|
|
528
|
+
}
|
|
529
|
+
} catch {
|
|
530
|
+
// Ignore JSON parse errors
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (i < waitingBreakpoints.length - 1) {
|
|
534
|
+
messages.push("");
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
messages.push("");
|
|
539
|
+
messages.push("━━━━━━━━━━━━━━━━━");
|
|
540
|
+
messages.push("Commands:");
|
|
541
|
+
messages.push("• preview <number> - View full details");
|
|
542
|
+
messages.push("• file <number> - View context file");
|
|
543
|
+
messages.push("• Reply or send ID/text to release");
|
|
544
|
+
|
|
545
|
+
if (message.chat?.id) {
|
|
546
|
+
await sendMessage(
|
|
547
|
+
config.token,
|
|
548
|
+
message.chat.id,
|
|
549
|
+
messages.join("\n")
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
|
|
394
555
|
if (trimmed.startsWith("/")) {
|
|
395
556
|
if (message.chat?.id && message.from?.id) {
|
|
396
557
|
rememberUser(
|
|
@@ -401,10 +562,67 @@ async function poll(db, config) {
|
|
|
401
562
|
);
|
|
402
563
|
await persistUser(db, config, message.chat.id, message.from.id);
|
|
403
564
|
if (config.token) {
|
|
565
|
+
// Query for existing waiting breakpoints
|
|
566
|
+
const { all } = require("../api/db");
|
|
567
|
+
const waitingBreakpoints = await all(
|
|
568
|
+
db,
|
|
569
|
+
"SELECT id, title, created_at, run_id, payload FROM breakpoints WHERE status = ? ORDER BY created_at DESC",
|
|
570
|
+
["waiting"]
|
|
571
|
+
);
|
|
572
|
+
|
|
573
|
+
// Build connection message with waiting breakpoints
|
|
574
|
+
const messages = [
|
|
575
|
+
"Telegram connected. I will notify you about breakpoints here."
|
|
576
|
+
];
|
|
577
|
+
|
|
578
|
+
if (waitingBreakpoints.length > 0) {
|
|
579
|
+
messages.push("");
|
|
580
|
+
messages.push(`🔔 You have ${waitingBreakpoints.length} waiting breakpoint${waitingBreakpoints.length === 1 ? '' : 's'}:`);
|
|
581
|
+
messages.push("");
|
|
582
|
+
|
|
583
|
+
for (let i = 0; i < waitingBreakpoints.length; i++) {
|
|
584
|
+
const bp = waitingBreakpoints[i];
|
|
585
|
+
const title = bp.title || "Untitled";
|
|
586
|
+
const runId = bp.run_id || "unknown";
|
|
587
|
+
const age = Math.floor((Date.now() - new Date(bp.created_at).getTime()) / 1000 / 60);
|
|
588
|
+
const ageStr = age < 1 ? "just now" : age === 1 ? "1 min ago" : `${age} mins ago`;
|
|
589
|
+
|
|
590
|
+
messages.push(`${i + 1}. ${title}`);
|
|
591
|
+
messages.push(` ID: ${bp.id}`);
|
|
592
|
+
messages.push(` Run: ${runId}`);
|
|
593
|
+
messages.push(` Created: ${ageStr}`);
|
|
594
|
+
|
|
595
|
+
// Extract question from payload if available
|
|
596
|
+
try {
|
|
597
|
+
const payload = JSON.parse(bp.payload);
|
|
598
|
+
if (payload?.question) {
|
|
599
|
+
const question = payload.question.length > 100
|
|
600
|
+
? `${payload.question.substring(0, 100)}...`
|
|
601
|
+
: payload.question;
|
|
602
|
+
messages.push(` Question: ${question}`);
|
|
603
|
+
}
|
|
604
|
+
} catch {
|
|
605
|
+
// Ignore JSON parse errors
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (i < waitingBreakpoints.length - 1) {
|
|
609
|
+
messages.push("");
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
messages.push("");
|
|
614
|
+
messages.push("━━━━━━━━━━━━━━━━━");
|
|
615
|
+
messages.push("Commands:");
|
|
616
|
+
messages.push("• list - Show waiting breakpoints");
|
|
617
|
+
messages.push("• preview <number> - View full details");
|
|
618
|
+
messages.push("• file <number> - View context file");
|
|
619
|
+
messages.push("• Reply or send ID/text to release");
|
|
620
|
+
}
|
|
621
|
+
|
|
404
622
|
await sendMessage(
|
|
405
623
|
config.token,
|
|
406
624
|
message.chat.id,
|
|
407
|
-
|
|
625
|
+
messages.join("\n")
|
|
408
626
|
);
|
|
409
627
|
}
|
|
410
628
|
}
|
|
@@ -413,6 +631,79 @@ async function poll(db, config) {
|
|
|
413
631
|
}
|
|
414
632
|
continue;
|
|
415
633
|
}
|
|
634
|
+
// Handle preview/show command to display full breakpoint details by list number
|
|
635
|
+
const previewMatch = trimmed.match(/^(preview|show)\s+(\d+)$/i);
|
|
636
|
+
if (previewMatch) {
|
|
637
|
+
const listNumber = parseInt(previewMatch[2], 10);
|
|
638
|
+
const { all } = require("../api/db");
|
|
639
|
+
const waitingBreakpoints = await all(
|
|
640
|
+
db,
|
|
641
|
+
"SELECT id, title, created_at, run_id, payload FROM breakpoints WHERE status = ? ORDER BY created_at DESC",
|
|
642
|
+
["waiting"]
|
|
643
|
+
);
|
|
644
|
+
|
|
645
|
+
if (listNumber < 1 || listNumber > waitingBreakpoints.length) {
|
|
646
|
+
if (message.chat?.id) {
|
|
647
|
+
await sendMessage(
|
|
648
|
+
config.token,
|
|
649
|
+
message.chat.id,
|
|
650
|
+
`Invalid number. Please use a number between 1 and ${waitingBreakpoints.length}.`
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const bp = waitingBreakpoints[listNumber - 1];
|
|
657
|
+
let payload;
|
|
658
|
+
try {
|
|
659
|
+
payload = JSON.parse(bp.payload);
|
|
660
|
+
} catch {
|
|
661
|
+
payload = {};
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const title = bp.title || "Untitled";
|
|
665
|
+
const question = payload?.question || "No question provided.";
|
|
666
|
+
const files = payload?.context?.files || [];
|
|
667
|
+
const fileLines = files.length
|
|
668
|
+
? files
|
|
669
|
+
.map((file, index) => {
|
|
670
|
+
const format = file.format || "code";
|
|
671
|
+
const label = file.label || file.path || "unknown";
|
|
672
|
+
return `${index + 1}. ${label} (${format})`;
|
|
673
|
+
})
|
|
674
|
+
.join("\n")
|
|
675
|
+
: "None";
|
|
676
|
+
|
|
677
|
+
const age = Math.floor((Date.now() - new Date(bp.created_at).getTime()) / 1000 / 60);
|
|
678
|
+
const ageStr = age < 1 ? "just now" : age === 1 ? "1 min ago" : `${age} mins ago`;
|
|
679
|
+
|
|
680
|
+
const previewText = [
|
|
681
|
+
`🟠 Breakpoint #${listNumber} Details`,
|
|
682
|
+
"",
|
|
683
|
+
`Title: ${title}`,
|
|
684
|
+
`ID: ${bp.id}`,
|
|
685
|
+
`Run: ${bp.run_id || "unknown"}`,
|
|
686
|
+
`Created: ${ageStr}`,
|
|
687
|
+
"",
|
|
688
|
+
"Question:",
|
|
689
|
+
question,
|
|
690
|
+
"",
|
|
691
|
+
"Context files:",
|
|
692
|
+
fileLines,
|
|
693
|
+
"",
|
|
694
|
+
"━━━━━━━━━━━━━━━━━",
|
|
695
|
+
"To interact:",
|
|
696
|
+
`• View file: file ${bp.id} <path>`,
|
|
697
|
+
"• View file by number: file 1, file 2, etc.",
|
|
698
|
+
`• Release: Reply to this message or send: ${bp.id}`,
|
|
699
|
+
].join("\n");
|
|
700
|
+
|
|
701
|
+
if (message.chat?.id) {
|
|
702
|
+
await sendMessage(config.token, message.chat.id, previewText);
|
|
703
|
+
}
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
|
|
416
707
|
const fileMatch = trimmed.match(/^(file|raw)\s+(.+)$/i);
|
|
417
708
|
if (fileMatch) {
|
|
418
709
|
const mode = fileMatch[1].toLowerCase();
|
package/package.json
CHANGED
package/web/app.js
CHANGED
|
@@ -488,17 +488,14 @@ notifySelect.addEventListener("change", async () => {
|
|
|
488
488
|
if (Notification.permission !== "granted") {
|
|
489
489
|
await Notification.requestPermission();
|
|
490
490
|
}
|
|
491
|
-
|
|
492
|
-
updateNotifyStatus();
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
notifySelect.addEventListener("change", () => {
|
|
496
|
-
if ("Notification" in window) {
|
|
491
|
+
updateNotifyStatus();
|
|
497
492
|
if (Notification.permission === "granted") {
|
|
498
493
|
new Notification("Desktop alerts enabled", {
|
|
499
494
|
body: "You will be notified of new waiting breakpoints.",
|
|
500
495
|
});
|
|
501
496
|
}
|
|
497
|
+
} else {
|
|
498
|
+
updateNotifyStatus();
|
|
502
499
|
}
|
|
503
500
|
});
|
|
504
501
|
|
package/web/styles.css
CHANGED
|
@@ -429,3 +429,174 @@ pre {
|
|
|
429
429
|
grid-template-columns: 1fr;
|
|
430
430
|
}
|
|
431
431
|
}
|
|
432
|
+
|
|
433
|
+
@media (max-width: 768px) {
|
|
434
|
+
.page {
|
|
435
|
+
padding: 24px 16px 48px;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
.hero {
|
|
439
|
+
padding: 20px;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
h1 {
|
|
443
|
+
font-size: clamp(24px, 5vw, 36px);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
.controls {
|
|
447
|
+
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
|
448
|
+
gap: 12px;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
.stats {
|
|
452
|
+
grid-template-columns: repeat(2, 1fr);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
.extension-fields {
|
|
456
|
+
grid-template-columns: 1fr;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
@media (max-width: 640px) {
|
|
461
|
+
.page {
|
|
462
|
+
padding: 16px 12px 32px;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
.hero {
|
|
466
|
+
flex-direction: column;
|
|
467
|
+
padding: 16px;
|
|
468
|
+
gap: 16px;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.token-panel {
|
|
472
|
+
width: 100%;
|
|
473
|
+
min-width: 100%;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
h1 {
|
|
477
|
+
font-size: clamp(22px, 6vw, 32px);
|
|
478
|
+
margin: 6px 0;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
.subtitle {
|
|
482
|
+
font-size: 14px;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
.controls {
|
|
486
|
+
grid-template-columns: 1fr;
|
|
487
|
+
gap: 10px;
|
|
488
|
+
margin: 16px 0;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
.stats {
|
|
492
|
+
grid-template-columns: 1fr 1fr;
|
|
493
|
+
gap: 10px;
|
|
494
|
+
margin-bottom: 16px;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
.stat-card {
|
|
498
|
+
padding: 12px;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
.stat-label {
|
|
502
|
+
font-size: 10px;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
.stat-value {
|
|
506
|
+
font-size: 20px;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
.layout {
|
|
510
|
+
gap: 16px;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
.list,
|
|
514
|
+
.detail {
|
|
515
|
+
padding: 16px;
|
|
516
|
+
min-height: 300px;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
.section-title {
|
|
520
|
+
font-size: 14px;
|
|
521
|
+
margin-bottom: 10px;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
.breakpoint-card {
|
|
525
|
+
padding: 10px;
|
|
526
|
+
gap: 4px;
|
|
527
|
+
font-size: 14px;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
.detail-header {
|
|
531
|
+
flex-direction: column;
|
|
532
|
+
align-items: flex-start;
|
|
533
|
+
gap: 8px;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
.detail-header h2 {
|
|
537
|
+
font-size: 18px;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
.detail-meta {
|
|
541
|
+
font-size: 12px;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
pre {
|
|
545
|
+
padding: 10px;
|
|
546
|
+
font-size: 11px;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
.feedback-actions {
|
|
550
|
+
flex-direction: column;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
.feedback-actions input,
|
|
554
|
+
.feedback-actions button {
|
|
555
|
+
width: 100%;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
button {
|
|
559
|
+
padding: 10px 12px;
|
|
560
|
+
font-size: 14px;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
.context-layout {
|
|
564
|
+
gap: 10px;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
.context-item {
|
|
568
|
+
padding: 6px;
|
|
569
|
+
font-size: 12px;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
.context-viewer {
|
|
573
|
+
padding: 10px;
|
|
574
|
+
min-height: 150px;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
.context-render {
|
|
578
|
+
font-size: 13px;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
.extensions {
|
|
582
|
+
padding: 16px;
|
|
583
|
+
margin-top: 16px;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
.extension-card {
|
|
587
|
+
padding: 12px;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
.extension-fields {
|
|
591
|
+
grid-template-columns: 1fr;
|
|
592
|
+
gap: 10px;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
.extension-actions {
|
|
596
|
+
flex-direction: column;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
.extension-actions button {
|
|
600
|
+
width: 100%;
|
|
601
|
+
}
|
|
602
|
+
}
|
package/worker/worker.js
CHANGED
|
@@ -37,14 +37,35 @@ async function loop() {
|
|
|
37
37
|
// eslint-disable-next-line no-console
|
|
38
38
|
console.log("[telegram] debug enabled");
|
|
39
39
|
}
|
|
40
|
+
|
|
41
|
+
let consecutiveErrors = 0;
|
|
42
|
+
const MAX_CONSECUTIVE_ERRORS = 10;
|
|
43
|
+
|
|
40
44
|
while (true) {
|
|
41
|
-
|
|
45
|
+
try {
|
|
46
|
+
await runOnce();
|
|
47
|
+
consecutiveErrors = 0;
|
|
48
|
+
} catch (err) {
|
|
49
|
+
consecutiveErrors++;
|
|
50
|
+
// eslint-disable-next-line no-console
|
|
51
|
+
console.error(`[worker] error in runOnce (${consecutiveErrors}/${MAX_CONSECUTIVE_ERRORS}):`, err.message);
|
|
52
|
+
|
|
53
|
+
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
54
|
+
// eslint-disable-next-line no-console
|
|
55
|
+
console.error(`[worker] too many consecutive errors, exiting`);
|
|
56
|
+
process.exitCode = 1;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const backoffDelay = Math.min(1000 * Math.pow(2, consecutiveErrors - 1), 30000);
|
|
61
|
+
// eslint-disable-next-line no-console
|
|
62
|
+
console.log(`[worker] backing off for ${backoffDelay}ms before retry`);
|
|
63
|
+
await new Promise((resolve) => setTimeout(resolve, backoffDelay));
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
42
67
|
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
43
68
|
}
|
|
44
69
|
}
|
|
45
70
|
|
|
46
|
-
loop()
|
|
47
|
-
// eslint-disable-next-line no-console
|
|
48
|
-
console.error(err);
|
|
49
|
-
process.exitCode = 1;
|
|
50
|
-
});
|
|
71
|
+
loop();
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: babysitter-breakpoint
|
|
3
|
-
description: Use the breakpoint API to communicate with users during babysitter runs (post breakpoints, poll for release, fetch feedback, and read context files). Trigger whenever a babysitter workflow needs approval, input, or status updates via breakpoints.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# babysitter-breakpoint
|
|
7
|
-
|
|
8
|
-
Use this skill to handle all user communication via the `breakpoints` CLI. Do
|
|
9
|
-
not prompt the user directly. Always post a breakpoint and poll until it is
|
|
10
|
-
released, then fetch feedback and continue.
|
|
11
|
-
|
|
12
|
-
## Workflow
|
|
13
|
-
|
|
14
|
-
1. Create a breakpoint with the required payload and `payload.context.files`.
|
|
15
|
-
2. Poll `breakpoints breakpoint wait <id> --interval 3` until `released`.
|
|
16
|
-
3. Apply feedback from the printed details and continue.
|
|
17
|
-
|
|
18
|
-
## Payload structure
|
|
19
|
-
|
|
20
|
-
Always include a `context.files` array for referenced files:
|
|
21
|
-
|
|
22
|
-
```json
|
|
23
|
-
{
|
|
24
|
-
"context": {
|
|
25
|
-
"runId": "run-...",
|
|
26
|
-
"files": [
|
|
27
|
-
{ "path": ".a5c/runs/<runId>/artifacts/process.md", "format": "markdown" },
|
|
28
|
-
{ "path": ".a5c/runs/<runId>/inputs.json", "format": "code", "language": "json" },
|
|
29
|
-
{ "path": ".a5c/runs/<runId>/code/main.js", "format": "code", "language": "javascript" }
|
|
30
|
-
]
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
See `references/breakpoint-api.md` for curl examples and endpoints.
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
# Breakpoint CLI + API
|
|
2
|
-
|
|
3
|
-
Use the `breakpoints` CLI to communicate with the user. The CLI wraps the local
|
|
4
|
-
API. Never prompt directly.
|
|
5
|
-
|
|
6
|
-
## Base URL
|
|
7
|
-
- Default: `http://localhost:3000`
|
|
8
|
-
- Override with `BREAKPOINT_API_URL` if set.
|
|
9
|
-
|
|
10
|
-
## Auth
|
|
11
|
-
- Agent token: `AGENT_TOKEN`
|
|
12
|
-
- Human token: `HUMAN_TOKEN`
|
|
13
|
-
- Use `Authorization: Bearer <token>` when set.
|
|
14
|
-
|
|
15
|
-
## Create a breakpoint (agent)
|
|
16
|
-
```bash
|
|
17
|
-
breakpoints breakpoint create \
|
|
18
|
-
--question "Approve process + inputs + main.js?" \
|
|
19
|
-
--run-id run-... \
|
|
20
|
-
--title "Approval needed" \
|
|
21
|
-
--file ".a5c/runs/<runId>/artifacts/process.md,markdown" \
|
|
22
|
-
--file ".a5c/runs/<runId>/inputs.json,code,json" \
|
|
23
|
-
--file ".a5c/runs/<runId>/code/main.js,code,javascript"
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
## Poll for status
|
|
27
|
-
```bash
|
|
28
|
-
breakpoints breakpoint status <id>
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
## Fetch full details (including feedback)
|
|
32
|
-
```bash
|
|
33
|
-
breakpoints breakpoint show <id>
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
## Wait for release (prints details)
|
|
37
|
-
```bash
|
|
38
|
-
breakpoints breakpoint wait <id> --interval 3
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
## Fetch context file content (API)
|
|
42
|
-
```bash
|
|
43
|
-
curl -s "$BREAKPOINT_API_URL/api/breakpoints/<id>/context?path=path/to/file"
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
## Release with feedback (human)
|
|
47
|
-
```bash
|
|
48
|
-
curl -s -X POST "$BREAKPOINT_API_URL/api/breakpoints/<id>/feedback" \
|
|
49
|
-
-H "Content-Type: application/json" \
|
|
50
|
-
-H "Authorization: Bearer $HUMAN_TOKEN" \
|
|
51
|
-
-d '{"author":"reviewer","comment":"approved","release":true}'
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
## Notes
|
|
55
|
-
- Poll every 2-10 seconds.
|
|
56
|
-
- Continue orchestration only after `status` becomes `released` and feedback is retrieved.
|
|
57
|
-
- Context files must be listed in the breakpoint payload and use allowlisted extensions.
|