bosun 0.26.3

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.
Files changed (122) hide show
  1. package/.env.example +918 -0
  2. package/LICENSE +190 -0
  3. package/README.md +98 -0
  4. package/agent-endpoint.mjs +918 -0
  5. package/agent-hook-bridge.mjs +230 -0
  6. package/agent-hooks.mjs +1188 -0
  7. package/agent-pool.mjs +2403 -0
  8. package/agent-prompts.mjs +689 -0
  9. package/agent-sdk.mjs +141 -0
  10. package/anomaly-detector.mjs +1195 -0
  11. package/autofix.mjs +1294 -0
  12. package/bosun.config.example.json +115 -0
  13. package/bosun.schema.json +465 -0
  14. package/claude-shell.mjs +708 -0
  15. package/cli.mjs +1028 -0
  16. package/codex-config.mjs +1274 -0
  17. package/codex-model-profiles.mjs +135 -0
  18. package/codex-shell.mjs +762 -0
  19. package/compat.mjs +286 -0
  20. package/config-doctor.mjs +613 -0
  21. package/config.mjs +1724 -0
  22. package/conflict-resolver.mjs +248 -0
  23. package/container-runner.mjs +450 -0
  24. package/copilot-shell.mjs +827 -0
  25. package/daemon-restart-policy.mjs +56 -0
  26. package/diff-stats.mjs +282 -0
  27. package/error-detector.mjs +829 -0
  28. package/fetch-runtime.mjs +34 -0
  29. package/fleet-coordinator.mjs +838 -0
  30. package/get-telegram-chat-id.mjs +71 -0
  31. package/git-safety.mjs +170 -0
  32. package/github-reconciler.mjs +403 -0
  33. package/hook-profiles.mjs +651 -0
  34. package/kanban-adapter.mjs +4491 -0
  35. package/lib/logger.mjs +645 -0
  36. package/maintenance.mjs +828 -0
  37. package/merge-strategy.mjs +1171 -0
  38. package/monitor.mjs +12237 -0
  39. package/package.json +209 -0
  40. package/postinstall.mjs +187 -0
  41. package/pr-cleanup-daemon.mjs +978 -0
  42. package/preflight.mjs +408 -0
  43. package/prepublish-check.mjs +90 -0
  44. package/presence.mjs +328 -0
  45. package/primary-agent.mjs +290 -0
  46. package/publish.mjs +241 -0
  47. package/repo-root.mjs +29 -0
  48. package/restart-controller.mjs +100 -0
  49. package/review-agent.mjs +557 -0
  50. package/rotate-agent-logs.sh +133 -0
  51. package/sdk-conflict-resolver.mjs +973 -0
  52. package/session-tracker.mjs +880 -0
  53. package/setup.mjs +3946 -0
  54. package/shared-knowledge.mjs +410 -0
  55. package/shared-state-manager.mjs +841 -0
  56. package/shared-workspace-cli.mjs +199 -0
  57. package/shared-workspace-registry.mjs +537 -0
  58. package/shared-workspaces.json +18 -0
  59. package/startup-service.mjs +1070 -0
  60. package/sync-engine.mjs +1063 -0
  61. package/task-archiver.mjs +801 -0
  62. package/task-assessment.mjs +550 -0
  63. package/task-claims.mjs +924 -0
  64. package/task-complexity.mjs +581 -0
  65. package/task-executor.mjs +5111 -0
  66. package/task-store.mjs +753 -0
  67. package/telegram-bot.mjs +9683 -0
  68. package/telegram-sentinel.mjs +2010 -0
  69. package/ui/app.js +867 -0
  70. package/ui/app.legacy.js +1464 -0
  71. package/ui/app.monolith.js +2488 -0
  72. package/ui/components/charts.js +226 -0
  73. package/ui/components/chat-view.js +567 -0
  74. package/ui/components/command-palette.js +587 -0
  75. package/ui/components/diff-viewer.js +190 -0
  76. package/ui/components/forms.js +357 -0
  77. package/ui/components/kanban-board.js +451 -0
  78. package/ui/components/session-list.js +305 -0
  79. package/ui/components/shared.js +525 -0
  80. package/ui/demo.html +640 -0
  81. package/ui/index.html +70 -0
  82. package/ui/modules/api.js +297 -0
  83. package/ui/modules/icons.js +461 -0
  84. package/ui/modules/router.js +81 -0
  85. package/ui/modules/settings-schema.js +261 -0
  86. package/ui/modules/state.js +679 -0
  87. package/ui/modules/telegram.js +331 -0
  88. package/ui/modules/utils.js +270 -0
  89. package/ui/styles/animations.css +140 -0
  90. package/ui/styles/base.css +98 -0
  91. package/ui/styles/components.css +2032 -0
  92. package/ui/styles/kanban.css +286 -0
  93. package/ui/styles/layout.css +810 -0
  94. package/ui/styles/sessions.css +841 -0
  95. package/ui/styles/variables.css +188 -0
  96. package/ui/styles.css +141 -0
  97. package/ui/styles.monolith.css +1046 -0
  98. package/ui/tabs/agents.js +1417 -0
  99. package/ui/tabs/chat.js +75 -0
  100. package/ui/tabs/control.js +892 -0
  101. package/ui/tabs/dashboard.js +515 -0
  102. package/ui/tabs/infra.js +537 -0
  103. package/ui/tabs/logs.js +783 -0
  104. package/ui/tabs/settings.js +1509 -0
  105. package/ui/tabs/tasks.js +1385 -0
  106. package/ui-server.mjs +4084 -0
  107. package/update-check.mjs +471 -0
  108. package/utils.mjs +172 -0
  109. package/ve-kanban.mjs +654 -0
  110. package/ve-kanban.ps1 +1365 -0
  111. package/ve-kanban.sh +18 -0
  112. package/ve-orchestrator.mjs +340 -0
  113. package/ve-orchestrator.ps1 +6546 -0
  114. package/ve-orchestrator.sh +18 -0
  115. package/vibe-kanban-wrapper.mjs +41 -0
  116. package/vk-error-resolver.mjs +470 -0
  117. package/vk-log-stream.mjs +914 -0
  118. package/whatsapp-channel.mjs +520 -0
  119. package/workspace-monitor.mjs +581 -0
  120. package/workspace-reaper.mjs +405 -0
  121. package/workspace-registry.mjs +238 -0
  122. package/worktree-manager.mjs +1266 -0
package/ui/index.html ADDED
@@ -0,0 +1,70 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
6
+ <meta name="color-scheme" content="dark light" />
7
+ <title>VirtEngine Control Center</title>
8
+ <script src="https://telegram.org/js/telegram-web-app.js"></script>
9
+ <link rel="preconnect" href="https://esm.sh" crossorigin />
10
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
11
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
12
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&family=Sora:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
13
+ <link rel="stylesheet" href="styles.css" />
14
+ <link rel="stylesheet" href="styles/kanban.css" />
15
+ <link rel="stylesheet" href="styles/sessions.css" />
16
+ <!-- Import map: ensures all modules share a single Preact instance
17
+ (required for @preact/signals auto-subscription to work) -->
18
+ <script type="importmap">
19
+ {
20
+ "imports": {
21
+ "preact": "https://esm.sh/preact@10.25.4",
22
+ "preact/hooks": "https://esm.sh/preact@10.25.4/hooks",
23
+ "htm": "https://esm.sh/htm@3.1.1",
24
+ "@preact/signals": "https://esm.sh/@preact/signals@1.3.1?deps=preact@10.25.4"
25
+ }
26
+ }
27
+ </script>
28
+ </head>
29
+ <body>
30
+ <div id="app">
31
+ <div id="boot-loader" style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100vh;font-family:'Sora',sans-serif;color:#9aa3b2;background:#0b0f14;background-image:radial-gradient(circle at 20% 10%,rgba(76,201,240,.18),transparent 55%),radial-gradient(circle at 80% 0%,rgba(94,234,212,.12),transparent 45%),radial-gradient(circle at 50% 100%,rgba(59,130,246,.12),transparent 60%);overflow:hidden">
32
+ <style>
33
+ .boot-logo{position:relative;width:80px;height:80px;margin-bottom:24px}
34
+ .boot-ring{position:absolute;inset:0;border-radius:50%;border:2px solid rgba(99,102,241,.15);animation:boot-spin 3s linear infinite}
35
+ .boot-ring-inner{position:absolute;inset:8px;border-radius:50%;border:2px solid transparent;border-top-color:#6366f1;border-right-color:rgba(99,102,241,.4);animation:boot-spin 1.5s linear infinite reverse}
36
+ .boot-dot{position:absolute;top:50%;left:50%;width:16px;height:16px;margin:-8px 0 0 -8px;border-radius:50%;background:linear-gradient(135deg,#6366f1,#a78bfa);box-shadow:0 0 20px rgba(99,102,241,.5),0 0 40px rgba(99,102,241,.2);animation:boot-pulse 2s ease-in-out infinite}
37
+ .boot-title{font-size:22px;font-weight:700;letter-spacing:-.02em;background:linear-gradient(135deg,#f1f5f9,#94a3b8);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;margin-bottom:6px;animation:boot-fadeUp .6s ease .2s both}
38
+ .boot-subtitle{font-size:13px;color:#64748b;animation:boot-fadeUp .6s ease .4s both}
39
+ .boot-progress{width:120px;height:2px;background:rgba(255,255,255,.06);border-radius:2px;margin-top:20px;overflow:hidden;animation:boot-fadeUp .6s ease .6s both}
40
+ .boot-progress-bar{height:100%;width:0;background:linear-gradient(90deg,#6366f1,#a78bfa);border-radius:2px;animation:boot-load 2s ease-in-out .8s forwards}
41
+ @keyframes boot-spin{to{transform:rotate(360deg)}}
42
+ @keyframes boot-pulse{0%,100%{transform:scale(1);opacity:.8}50%{transform:scale(1.2);opacity:1}}
43
+ @keyframes boot-fadeUp{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}
44
+ @keyframes boot-load{0%{width:0}60%{width:70%}100%{width:100%}}
45
+ </style>
46
+ <div class="boot-logo">
47
+ <div class="boot-ring"></div>
48
+ <div class="boot-ring-inner"></div>
49
+ <div class="boot-dot"></div>
50
+ </div>
51
+ <div class="boot-title">VirtEngine</div>
52
+ <div class="boot-subtitle" id="boot-status">Initializing Control Center…</div>
53
+ <div class="boot-progress"><div class="boot-progress-bar"></div></div>
54
+ </div>
55
+ </div>
56
+ <script type="module" src="app.js"></script>
57
+ <script>
58
+ // Signal Telegram that the page has loaded (before ES modules finish loading)
59
+ try { if (window.Telegram && Telegram.WebApp) Telegram.WebApp.ready(); } catch(e) {}
60
+ // Fallback: if the ES module fails to load within 12s, show an error
61
+ setTimeout(function() {
62
+ var el = document.getElementById('boot-loader');
63
+ if (el) {
64
+ var s = document.getElementById('boot-status');
65
+ if (s) s.innerHTML = '⚠️ Failed to load app modules.<br><small style="color:#888">Check your internet connection (CDN: esm.sh) or try refreshing.</small>';
66
+ }
67
+ }, 12000);
68
+ </script>
69
+ </body>
70
+ </html>
@@ -0,0 +1,297 @@
1
+ /* ─────────────────────────────────────────────────────────────
2
+ * VirtEngine Control Center – API Client & WebSocket
3
+ * Handles REST calls, WS connection, and command sending
4
+ * ────────────────────────────────────────────────────────────── */
5
+
6
+ import { signal } from "@preact/signals";
7
+ import { getInitData } from "./telegram.js";
8
+
9
+ /** Reactive signal: whether the WebSocket is currently connected */
10
+ export const wsConnected = signal(false);
11
+ /** Reactive signal: WebSocket round-trip latency in ms (null if unknown) */
12
+ export const wsLatency = signal(null);
13
+ /** Reactive signal: countdown seconds until next reconnect attempt (null when connected) */
14
+ export const wsReconnectIn = signal(null);
15
+ /** Reactive signal: number of reconnections since last user-initiated action */
16
+ export const wsReconnectCount = signal(0);
17
+
18
+ /* ─── REST API Client ─── */
19
+
20
+ /**
21
+ * Fetch from the API (same-origin). Automatically injects the
22
+ * X-Telegram-InitData header and handles JSON parsing / errors.
23
+ *
24
+ * @param {string} path - API path, e.g. "/api/status"
25
+ * @param {RequestInit & {_silent?: boolean}} options
26
+ * @returns {Promise<any>} parsed JSON body
27
+ */
28
+ export async function apiFetch(path, options = {}) {
29
+ const headers = { ...options.headers };
30
+ headers["Content-Type"] = headers["Content-Type"] || "application/json";
31
+
32
+ const initData = getInitData();
33
+ if (initData) {
34
+ headers["X-Telegram-InitData"] = initData;
35
+ }
36
+
37
+ const silent = options._silent;
38
+ delete options._silent;
39
+
40
+ try {
41
+ const res = await fetch(path, { ...options, headers });
42
+ if (!res.ok) {
43
+ const text = await res.text().catch(() => "");
44
+ throw new Error(text || `Request failed (${res.status})`);
45
+ }
46
+ return await res.json();
47
+ } catch (err) {
48
+ // Re-throw so callers can catch, but don't toast on silent requests
49
+ if (!silent) {
50
+ // Dispatch a custom event so the state layer can show a toast
51
+ try {
52
+ globalThis.dispatchEvent(
53
+ new CustomEvent("ve:api-error", { detail: { message: err.message } }),
54
+ );
55
+ } catch {
56
+ /* noop */
57
+ }
58
+ }
59
+ throw err;
60
+ }
61
+ }
62
+
63
+ /* ─── Command Sending ─── */
64
+
65
+ /**
66
+ * Send a slash-command to the backend via POST /api/command.
67
+ * @param {string} cmd - e.g. "/status" or "/starttask abc123"
68
+ * @returns {Promise<any>}
69
+ */
70
+ export async function sendCommandToChat(cmd) {
71
+ return apiFetch("/api/command", {
72
+ method: "POST",
73
+ body: JSON.stringify({ command: cmd }),
74
+ });
75
+ }
76
+
77
+ /* ─── WebSocket ─── */
78
+
79
+ /** @type {WebSocket|null} */
80
+ let ws = null;
81
+ /** @type {ReturnType<typeof setTimeout>|null} */
82
+ let reconnectTimer = null;
83
+ /** @type {ReturnType<typeof setInterval>|null} */
84
+ let countdownTimer = null;
85
+ /** @type {ReturnType<typeof setInterval>|null} */
86
+ let pingTimer = null;
87
+ let retryMs = 1000;
88
+
89
+ /** Registered message handlers */
90
+ const wsHandlers = new Set();
91
+
92
+ /**
93
+ * Register a handler for incoming WS messages.
94
+ * Returns an unsubscribe function.
95
+ * @param {(data: any) => void} handler
96
+ * @returns {() => void}
97
+ */
98
+ export function onWsMessage(handler) {
99
+ wsHandlers.add(handler);
100
+ return () => wsHandlers.delete(handler);
101
+ }
102
+
103
+ /** Clear the reconnect countdown timer and reset signal */
104
+ function clearCountdown() {
105
+ if (countdownTimer) {
106
+ clearInterval(countdownTimer);
107
+ countdownTimer = null;
108
+ }
109
+ wsReconnectIn.value = null;
110
+ }
111
+
112
+ /** Start a countdown timer that ticks wsReconnectIn every second */
113
+ function startCountdown(ms) {
114
+ clearCountdown();
115
+ let remaining = Math.ceil(ms / 1000);
116
+ wsReconnectIn.value = remaining;
117
+ countdownTimer = setInterval(() => {
118
+ remaining -= 1;
119
+ wsReconnectIn.value = Math.max(0, remaining);
120
+ if (remaining <= 0) {
121
+ clearInterval(countdownTimer);
122
+ countdownTimer = null;
123
+ }
124
+ }, 1000);
125
+ }
126
+
127
+ /** Start the client-side ping interval (every 30s) */
128
+ function startPing() {
129
+ stopPing();
130
+ pingTimer = setInterval(() => {
131
+ wsSend({ type: "ping", ts: Date.now() });
132
+ }, 30_000);
133
+ }
134
+
135
+ /** Stop the client-side ping interval */
136
+ function stopPing() {
137
+ if (pingTimer) {
138
+ clearInterval(pingTimer);
139
+ pingTimer = null;
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Open (or re-open) a WebSocket connection to /ws.
145
+ * Automatically reconnects on close with exponential backoff.
146
+ */
147
+ export function connectWebSocket() {
148
+ // Prevent double connections
149
+ if (
150
+ ws &&
151
+ (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)
152
+ ) {
153
+ return;
154
+ }
155
+
156
+ const proto = globalThis.location.protocol === "https:" ? "wss" : "ws";
157
+ const wsUrl = new URL(`${proto}://${globalThis.location.host}/ws`);
158
+
159
+ const initData = getInitData();
160
+ if (initData) {
161
+ wsUrl.searchParams.set("initData", initData);
162
+ } else {
163
+ // Pass session token from cookie for browser-based WS auth
164
+ const m = (document.cookie || "").match(/(?:^|;\s*)ve_session=([^;]+)/);
165
+ if (m) wsUrl.searchParams.set("token", m[1]);
166
+ }
167
+
168
+ const socket = new WebSocket(wsUrl.toString());
169
+ ws = socket;
170
+
171
+ socket.addEventListener("open", () => {
172
+ wsConnected.value = true;
173
+ wsLatency.value = null;
174
+ retryMs = 1000; // reset backoff on successful connect
175
+ clearCountdown();
176
+ startPing();
177
+ });
178
+
179
+ socket.addEventListener("message", (event) => {
180
+ let msg;
181
+ try {
182
+ msg = JSON.parse(event.data || "{}");
183
+ } catch {
184
+ return;
185
+ }
186
+
187
+ // Handle pong → calculate RTT
188
+ if (msg.type === "pong" && typeof msg.ts === "number") {
189
+ wsLatency.value = Date.now() - msg.ts;
190
+ return;
191
+ }
192
+
193
+ // Handle server-initiated ping → reply with pong
194
+ if (msg.type === "ping" && typeof msg.ts === "number") {
195
+ wsSend({ type: "pong", ts: msg.ts });
196
+ return;
197
+ }
198
+
199
+ // Handle log-lines streaming
200
+ if (msg.type === "log-lines" && Array.isArray(msg.lines)) {
201
+ try {
202
+ globalThis.dispatchEvent(
203
+ new CustomEvent("ve:log-lines", { detail: { lines: msg.lines } }),
204
+ );
205
+ } catch {
206
+ /* noop */
207
+ }
208
+ return;
209
+ }
210
+
211
+ // Dispatch to all registered handlers
212
+ for (const handler of wsHandlers) {
213
+ try {
214
+ handler(msg);
215
+ } catch {
216
+ /* handler errors shouldn't crash the WS loop */
217
+ }
218
+ }
219
+ });
220
+
221
+ socket.addEventListener("close", () => {
222
+ wsConnected.value = false;
223
+ wsLatency.value = null;
224
+ ws = null;
225
+ stopPing();
226
+ wsReconnectCount.value += 1;
227
+ // Auto-reconnect with exponential backoff (max 15 s)
228
+ if (reconnectTimer) clearTimeout(reconnectTimer);
229
+ startCountdown(retryMs);
230
+ reconnectTimer = setTimeout(() => {
231
+ reconnectTimer = null;
232
+ connectWebSocket();
233
+ }, retryMs);
234
+ retryMs = Math.min(15000, retryMs * 2);
235
+ });
236
+
237
+ socket.addEventListener("error", () => {
238
+ wsConnected.value = false;
239
+ });
240
+ }
241
+
242
+ /**
243
+ * Disconnect the WebSocket and cancel any pending reconnect.
244
+ */
245
+ export function disconnectWebSocket() {
246
+ if (reconnectTimer) {
247
+ clearTimeout(reconnectTimer);
248
+ reconnectTimer = null;
249
+ }
250
+ clearCountdown();
251
+ stopPing();
252
+ if (ws) {
253
+ try {
254
+ ws.close();
255
+ } catch {
256
+ /* noop */
257
+ }
258
+ ws = null;
259
+ }
260
+ wsConnected.value = false;
261
+ wsLatency.value = null;
262
+ }
263
+
264
+ /**
265
+ * Send a raw JSON message over the open WebSocket.
266
+ * @param {any} data
267
+ */
268
+ export function wsSend(data) {
269
+ if (ws?.readyState === WebSocket.OPEN) {
270
+ ws.send(typeof data === "string" ? data : JSON.stringify(data));
271
+ }
272
+ }
273
+
274
+ /* ─── Log Streaming ─── */
275
+
276
+ /**
277
+ * Subscribe to real-time log streaming over the WebSocket.
278
+ * @param {"system"|"agent"} logType
279
+ * @param {string} [query] - optional filter query (e.g. agent name)
280
+ */
281
+ export function subscribeToLogs(logType, query) {
282
+ wsSend({ type: "subscribe-logs", logType, ...(query ? { query } : {}) });
283
+ }
284
+
285
+ /**
286
+ * Unsubscribe from log streaming.
287
+ */
288
+ export function unsubscribeFromLogs() {
289
+ wsSend({ type: "unsubscribe-logs" });
290
+ }
291
+
292
+ /**
293
+ * Reset the reconnect counter (call on user-initiated actions).
294
+ */
295
+ export function resetReconnectCount() {
296
+ wsReconnectCount.value = 0;
297
+ }