@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 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
- ## Install the babysitter-breakpoint skill
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 install-skill
122
+ breakpoints extension enable telegram --token <bot-token> --username <your-username>
121
123
  ```
122
- Defaults to global Codex install. Options:
123
- ```bash
124
- breakpoints install-skill --target codex --scope global
125
- breakpoints install-skill --target codex --scope local
126
- breakpoints install-skill --target claude --scope global
127
- breakpoints install-skill --target claude --scope local
128
- breakpoints install-skill --target cursor --scope global
129
- breakpoints install-skill --target cursor --scope local
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
- Global targets use `CODEX_HOME` or `~/.codex` for Codex, and `~/.claude` or `~/.cursor` for Claude/Cursor. Local installs write to `.codex/skills`, `.claude/skills`, or `.cursor/skills` under the package root. Restart the app after install.
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
- When installed from npm, the skill is bundled at `.codex/skills/babysitter-breakpoint/` inside the package and copied to the target location by `breakpoints install-skill`.
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:
@@ -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];
@@ -24,7 +24,11 @@ async function dispatch(event, db, breakpoint) {
24
24
  if (!extension || typeof extension[event] !== "function") {
25
25
  continue;
26
26
  }
27
- await extension[event](db, breakpoint, cfg.config);
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
- await extension.poll(db, cfg.config);
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
 
@@ -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
- https
15
- .get(url, (res) => {
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
- .on("error", reject);
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
- const req = https.request(url, {
58
- method: "POST",
59
- headers: {
60
- "Content-Type": `multipart/form-data; boundary=${boundary}`,
61
- "Content-Length": Buffer.byteLength(payload),
62
- },
63
- });
64
- req.on("response", (res) => {
65
- let data = "";
66
- res.on("data", (chunk) => {
67
- data += chunk;
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
- res.on("end", () => {
70
- try {
71
- resolve(JSON.parse(data));
72
- } catch (err) {
73
- reject(err);
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
- req.on("error", reject);
78
- req.write(payload);
79
- req.end();
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
- const response = await callTelegram(url);
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
- "Telegram connected. I will notify you about breakpoints here."
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a5c-ai/babysitter-breakpoints",
3
- "version": "0.0.29",
3
+ "version": "0.0.30",
4
4
  "private": false,
5
5
  "type": "commonjs",
6
6
  "bin": {
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
- await runOnce();
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().catch((err) => {
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.